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"
|
env_path = Path(__file__).resolve().parents[1] / ".env"
|
||||||
if not env_path.exists():
|
if not env_path.exists():
|
||||||
return
|
return
|
||||||
|
# 仅保护启动前已有的环境变量;.env 内重复键以后者为准
|
||||||
|
pre_existing_keys = set(os.environ.keys())
|
||||||
try:
|
try:
|
||||||
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
||||||
line = raw_line.strip()
|
line = raw_line.strip()
|
||||||
@ -19,8 +21,12 @@ def _load_dotenv():
|
|||||||
key, value = line.split("=", 1)
|
key, value = line.split("=", 1)
|
||||||
key = key.strip()
|
key = key.strip()
|
||||||
value = value.strip().strip('"').strip("'")
|
value = value.strip().strip('"').strip("'")
|
||||||
if key and key not in os.environ:
|
if not key:
|
||||||
os.environ[key] = value
|
continue
|
||||||
|
# 若在进程启动前已存在,则尊重外部环境;否则允许 .env 内后续行覆盖前面行
|
||||||
|
if key in pre_existing_keys:
|
||||||
|
continue
|
||||||
|
os.environ[key] = value
|
||||||
except Exception:
|
except Exception:
|
||||||
# 加载失败时静默继续,保持兼容
|
# 加载失败时静默继续,保持兼容
|
||||||
pass
|
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"
|
PROMPTS_DIR = "./prompts"
|
||||||
DATA_DIR = "./data"
|
DATA_DIR = "./data"
|
||||||
LOGS_DIR = "./logs"
|
LOGS_DIR = "./logs"
|
||||||
@ -19,6 +24,7 @@ API_USAGE_FILE = f"{DATA_DIR}/api_usage.json"
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DEFAULT_PROJECT_PATH",
|
"DEFAULT_PROJECT_PATH",
|
||||||
|
"HOST_PROJECT_PATH",
|
||||||
"PROMPTS_DIR",
|
"PROMPTS_DIR",
|
||||||
"DATA_DIR",
|
"DATA_DIR",
|
||||||
"LOGS_DIR",
|
"LOGS_DIR",
|
||||||
|
|||||||
7
main.py
7
main.py
@ -114,7 +114,12 @@ class AgentSystem:
|
|||||||
|
|
||||||
async def setup_project_path(self):
|
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()
|
project_path = Path(path_input).resolve()
|
||||||
|
|
||||||
if self.is_unsafe_path(str(project_path)):
|
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.personalization_manager import load_personalization_config
|
||||||
from modules.user_manager import UserWorkspace
|
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 .auth_helpers import login_required, api_login_required, get_current_user_record, get_current_username
|
||||||
from .security import (
|
from .security import (
|
||||||
@ -95,6 +102,49 @@ def login():
|
|||||||
return jsonify({"success": True})
|
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'])
|
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
|
|||||||
@ -11,6 +11,13 @@ from modules.personalization_manager import load_personalization_config
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from modules.usage_tracker import UsageTracker
|
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 . import state
|
||||||
from .utils_common import debug_log
|
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())
|
username = (username or get_current_username())
|
||||||
if not username:
|
if not username:
|
||||||
return None, None
|
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
|
is_api_user = bool(session.get("is_api_user")) if has_request_context() else False
|
||||||
# API 用户与网页用户使用不同的 manager
|
# API 用户与网页用户使用不同的 manager
|
||||||
if is_api_user:
|
if is_api_user:
|
||||||
|
|||||||
@ -51,7 +51,7 @@ THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "
|
|||||||
CSRF_HEADER_NAME = "X-CSRF-Token"
|
CSRF_HEADER_NAME = "X-CSRF-Token"
|
||||||
CSRF_SESSION_KEY = "_csrf_token"
|
CSRF_SESSION_KEY = "_csrf_token"
|
||||||
CSRF_SAFE_METHODS = {"GET", "HEAD", "OPTIONS", "TRACE"}
|
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_PROTECTED_PREFIXES = ("/api/",)
|
||||||
CSRF_EXEMPT_PATHS = {"/api/csrf-token"}
|
CSRF_EXEMPT_PATHS = {"/api/csrf-token"}
|
||||||
FAILED_LOGIN_LIMIT = 5
|
FAILED_LOGIN_LIMIT = 5
|
||||||
|
|||||||
@ -71,6 +71,21 @@
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
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 {
|
.error {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
color: #c0392b;
|
color: #c0392b;
|
||||||
@ -100,6 +115,7 @@
|
|||||||
<input type="password" id="password" autocomplete="current-password" />
|
<input type="password" id="password" autocomplete="current-password" />
|
||||||
</div>
|
</div>
|
||||||
<button id="login-btn">登录</button>
|
<button id="login-btn">登录</button>
|
||||||
|
<button id="host-btn" class="ghost-btn" title="仅当服务器配置为宿主机模式时可用">宿主机模式(免登录)</button>
|
||||||
<div class="error" id="error"></div>
|
<div class="error" id="error"></div>
|
||||||
<div class="link">
|
<div class="link">
|
||||||
还没有账号?<a href="/register">点击注册</a>
|
还没有账号?<a href="/register">点击注册</a>
|
||||||
@ -153,6 +169,29 @@
|
|||||||
btn.click();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user