feat: add host mode quick entry
This commit is contained in:
parent
1c2ad9eb72
commit
d8cffa30cc
@ -9,6 +9,8 @@ def _load_dotenv():
|
||||
env_path = Path(__file__).resolve().parents[1] / ".env"
|
||||
if not env_path.exists():
|
||||
return
|
||||
# 仅保护启动前已有的环境变量;.env 内重复键以后者为准
|
||||
pre_existing_keys = set(os.environ.keys())
|
||||
try:
|
||||
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
@ -19,8 +21,12 @@ def _load_dotenv():
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if key and key not in os.environ:
|
||||
os.environ[key] = value
|
||||
if not key:
|
||||
continue
|
||||
# 若在进程启动前已存在,则尊重外部环境;否则允许 .env 内后续行覆盖前面行
|
||||
if key in pre_existing_keys:
|
||||
continue
|
||||
os.environ[key] = value
|
||||
except Exception:
|
||||
# 加载失败时静默继续,保持兼容
|
||||
pass
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
"""项目路径与目录配置。"""
|
||||
|
||||
DEFAULT_PROJECT_PATH = "./project"
|
||||
import os
|
||||
|
||||
# 默认项目路径,可通过环境变量覆盖以指向宿主机任意目录
|
||||
DEFAULT_PROJECT_PATH = os.environ.get("DEFAULT_PROJECT_PATH", "./project")
|
||||
# 当终端运行在宿主机模式时,可显式指定工作目录;未设置时回退到 DEFAULT_PROJECT_PATH
|
||||
HOST_PROJECT_PATH = os.environ.get("HOST_PROJECT_PATH", DEFAULT_PROJECT_PATH)
|
||||
PROMPTS_DIR = "./prompts"
|
||||
DATA_DIR = "./data"
|
||||
LOGS_DIR = "./logs"
|
||||
@ -19,6 +24,7 @@ API_USAGE_FILE = f"{DATA_DIR}/api_usage.json"
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_PROJECT_PATH",
|
||||
"HOST_PROJECT_PATH",
|
||||
"PROMPTS_DIR",
|
||||
"DATA_DIR",
|
||||
"LOGS_DIR",
|
||||
|
||||
7
main.py
7
main.py
@ -114,7 +114,12 @@ class AgentSystem:
|
||||
|
||||
async def setup_project_path(self):
|
||||
"""设置项目路径"""
|
||||
path_input = os.path.expanduser(str(DEFAULT_PROJECT_PATH))
|
||||
# 宿主机模式可通过 HOST_PROJECT_PATH 显式指定任意目录;否则使用默认
|
||||
if (TERMINAL_SANDBOX_MODE or "").lower() == "host":
|
||||
path_input = os.environ.get("HOST_PROJECT_PATH") or os.environ.get("DEFAULT_PROJECT_PATH") or str(DEFAULT_PROJECT_PATH)
|
||||
else:
|
||||
path_input = os.environ.get("DEFAULT_PROJECT_PATH") or str(DEFAULT_PROJECT_PATH)
|
||||
path_input = os.path.expanduser(str(path_input))
|
||||
project_path = Path(path_input).resolve()
|
||||
|
||||
if self.is_unsafe_path(str(project_path)):
|
||||
|
||||
@ -5,6 +5,13 @@ from flask import Blueprint, request, jsonify, session, redirect, send_from_dire
|
||||
|
||||
from modules.personalization_manager import load_personalization_config
|
||||
from modules.user_manager import UserWorkspace
|
||||
from config import (
|
||||
TERMINAL_SANDBOX_MODE,
|
||||
HOST_PROJECT_PATH,
|
||||
DATA_DIR,
|
||||
LOGS_DIR,
|
||||
UPLOAD_QUARANTINE_SUBDIR,
|
||||
)
|
||||
|
||||
from .auth_helpers import login_required, api_login_required, get_current_user_record, get_current_username
|
||||
from .security import (
|
||||
@ -95,6 +102,49 @@ def login():
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@auth_bp.route('/host-login', methods=['POST'])
|
||||
def host_login():
|
||||
"""宿主机模式一键进入(仅当 TERMINAL_SANDBOX_MODE=host 时可用)。"""
|
||||
if (TERMINAL_SANDBOX_MODE or "").lower() != "host":
|
||||
return jsonify({"success": False, "error": "宿主机模式未启用"}), 403
|
||||
if not state.container_manager.has_capacity("host"):
|
||||
return jsonify({"success": False, "error": "资源繁忙,请稍后再试"}), 503
|
||||
|
||||
host_path = Path(HOST_PROJECT_PATH).expanduser().resolve()
|
||||
host_path.mkdir(parents=True, exist_ok=True)
|
||||
data_dir = Path(DATA_DIR).expanduser().resolve()
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
logs_dir = Path(LOGS_DIR).expanduser().resolve()
|
||||
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
uploads_dir = host_path / "user_upload"
|
||||
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||
quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser()
|
||||
if not quarantine_root.is_absolute():
|
||||
quarantine_root = (host_path.parent / UPLOAD_QUARANTINE_SUBDIR).resolve()
|
||||
quarantine_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 初始化 session,跳过账号体系
|
||||
session.clear()
|
||||
session['logged_in'] = True
|
||||
session['username'] = 'host'
|
||||
session['role'] = 'admin'
|
||||
session['host_mode'] = True
|
||||
default_thinking = current_app.config.get('DEFAULT_THINKING_MODE', False)
|
||||
session['thinking_mode'] = default_thinking
|
||||
session['run_mode'] = current_app.config.get('DEFAULT_RUN_MODE', "deep" if default_thinking else "fast")
|
||||
session.permanent = True
|
||||
|
||||
# 预先创建宿主机模式的终端/容器句柄(host 模式不会启动 Docker)
|
||||
try:
|
||||
state.container_manager.ensure_container("host", str(host_path), container_key="host")
|
||||
except RuntimeError as exc:
|
||||
session.clear()
|
||||
return jsonify({"success": False, "error": str(exc)}), 503
|
||||
|
||||
get_csrf_token(force_new=True)
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if request.method == 'GET':
|
||||
|
||||
@ -11,6 +11,13 @@ from modules.personalization_manager import load_personalization_config
|
||||
import json
|
||||
from pathlib import Path
|
||||
from modules.usage_tracker import UsageTracker
|
||||
from config import (
|
||||
HOST_PROJECT_PATH,
|
||||
DATA_DIR,
|
||||
LOGS_DIR,
|
||||
TERMINAL_SANDBOX_MODE,
|
||||
UPLOAD_QUARANTINE_SUBDIR,
|
||||
)
|
||||
|
||||
from . import state
|
||||
from .utils_common import debug_log
|
||||
@ -45,6 +52,75 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
||||
username = (username or get_current_username())
|
||||
if not username:
|
||||
return None, None
|
||||
|
||||
# 宿主机免登录模式:使用 HOST_PROJECT_PATH 直接进入,不创建 /users/<user>/project
|
||||
host_mode_session = bool(session.get("host_mode")) if has_request_context() else False
|
||||
sandbox_is_host = (TERMINAL_SANDBOX_MODE or "host").lower() == "host"
|
||||
if host_mode_session and sandbox_is_host:
|
||||
project_path = Path(HOST_PROJECT_PATH).expanduser().resolve()
|
||||
project_path.mkdir(parents=True, exist_ok=True)
|
||||
data_dir = Path(DATA_DIR).expanduser().resolve()
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
logs_dir = Path(LOGS_DIR).expanduser().resolve()
|
||||
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
uploads_dir = project_path / "user_upload"
|
||||
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||
quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser()
|
||||
if not quarantine_root.is_absolute():
|
||||
quarantine_root = (project_path.parent / UPLOAD_QUARANTINE_SUBDIR).resolve()
|
||||
quarantine_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
workspace = UserWorkspace(
|
||||
username="host",
|
||||
root=project_path.parent,
|
||||
project_path=project_path,
|
||||
data_dir=data_dir,
|
||||
logs_dir=logs_dir,
|
||||
uploads_dir=uploads_dir,
|
||||
quarantine_dir=quarantine_root,
|
||||
)
|
||||
if not hasattr(workspace, "workspace_id"):
|
||||
workspace.workspace_id = "host"
|
||||
|
||||
term_key = "host"
|
||||
container_handle = state.container_manager.ensure_container("host", str(project_path), container_key=term_key)
|
||||
usage_tracker = get_or_create_usage_tracker("host", workspace)
|
||||
terminal = state.user_terminals.get(term_key)
|
||||
if not terminal:
|
||||
run_mode = session.get('run_mode') if has_request_context() else None
|
||||
thinking_mode_flag = session.get('thinking_mode') if has_request_context() else None
|
||||
if run_mode not in {"fast", "thinking", "deep"}:
|
||||
run_mode = "fast"
|
||||
thinking_mode_flag = False
|
||||
thinking_mode = bool(thinking_mode_flag) if thinking_mode_flag is not None else (run_mode != "fast")
|
||||
terminal = WebTerminal(
|
||||
project_path=str(project_path),
|
||||
thinking_mode=thinking_mode,
|
||||
run_mode=run_mode,
|
||||
message_callback=make_terminal_callback("host"),
|
||||
data_dir=str(data_dir),
|
||||
container_session=container_handle,
|
||||
usage_tracker=usage_tracker
|
||||
)
|
||||
if terminal.terminal_manager:
|
||||
terminal.terminal_manager.broadcast = terminal.message_callback
|
||||
state.user_terminals[term_key] = terminal
|
||||
terminal.username = "host"
|
||||
terminal.user_role = "admin"
|
||||
terminal.quota_update_callback = None
|
||||
if has_request_context():
|
||||
session['run_mode'] = terminal.run_mode
|
||||
session['thinking_mode'] = terminal.thinking_mode
|
||||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||||
else:
|
||||
terminal.update_container_session(container_handle)
|
||||
attach_user_broadcast(terminal, "host")
|
||||
terminal.username = "host"
|
||||
terminal.user_role = "admin"
|
||||
if has_request_context():
|
||||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||||
return terminal, workspace
|
||||
|
||||
is_api_user = bool(session.get("is_api_user")) if has_request_context() else False
|
||||
# API 用户与网页用户使用不同的 manager
|
||||
if is_api_user:
|
||||
|
||||
@ -51,7 +51,7 @@ THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "
|
||||
CSRF_HEADER_NAME = "X-CSRF-Token"
|
||||
CSRF_SESSION_KEY = "_csrf_token"
|
||||
CSRF_SAFE_METHODS = {"GET", "HEAD", "OPTIONS", "TRACE"}
|
||||
CSRF_PROTECTED_PATHS = {"/login", "/register", "/logout"}
|
||||
CSRF_PROTECTED_PATHS = {"/login", "/register", "/logout", "/host-login"}
|
||||
CSRF_PROTECTED_PREFIXES = ("/api/",)
|
||||
CSRF_EXEMPT_PATHS = {"/api/csrf-token"}
|
||||
FAILED_LOGIN_LIMIT = 5
|
||||
|
||||
@ -71,6 +71,21 @@
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ghost-btn {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
padding: 11px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #d8894c;
|
||||
background: #fffefc;
|
||||
color: #d8894c;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ghost-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
@ -100,6 +115,7 @@
|
||||
<input type="password" id="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<button id="login-btn">登录</button>
|
||||
<button id="host-btn" class="ghost-btn" title="仅当服务器配置为宿主机模式时可用">宿主机模式(免登录)</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
还没有账号?<a href="/register">点击注册</a>
|
||||
@ -153,6 +169,29 @@
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
|
||||
const hostBtn = document.getElementById('host-btn');
|
||||
hostBtn.addEventListener('click', async () => {
|
||||
hostBtn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/host-login', { method: 'POST' });
|
||||
if (resp.status === 503) {
|
||||
window.location.href = '/resource_busy';
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '宿主机模式不可用';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请重试';
|
||||
} finally {
|
||||
hostBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user