Spaces:
Paused
Paused
File size: 6,459 Bytes
40b21ae ead7e93 40b21ae d0b6cac 40b21ae b6bcc3c ead7e93 40b21ae d0b6cac 40b21ae b6bcc3c 40b21ae ead7e93 40b21ae ead7e93 40b21ae ead7e93 40b21ae b6bcc3c 40b21ae 60afec4 ead7e93 60afec4 40b21ae a80afa0 40b21ae 60afec4 ead7e93 60afec4 40b21ae a80afa0 40b21ae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """管理员登录态持久化。
统一使用项目根目录下的 `state.json` 文件保存:
- session_token
- email
- password
- account_id
- workspace_name
- updated_at
兼容:
- 旧的纯文本 `session`(仅保存 session token)
"""
import json
import os
import time
from contextlib import contextmanager
from contextvars import ContextVar
from pathlib import Path
from autoteam.textio import read_text, write_text
PROJECT_ROOT = Path(__file__).parent.parent.parent
STATE_FILE = PROJECT_ROOT / "state.json"
LEGACY_SESSION_FILE = PROJECT_ROOT / "session"
STATE_FILE_MODE = 0o666
_ADMIN_STATE_OVERRIDE: ContextVar[dict | None] = ContextVar("autoteam_admin_state_override", default=None)
def _normalize_state(data):
if not isinstance(data, dict):
return {}
return {
"email": data.get("email", "") or "",
"session_token": data.get("session_token", "") or "",
"password": data.get("password", "") or "",
"account_id": data.get("account_id", "") or "",
"workspace_name": data.get("workspace_name", "") or "",
"updated_at": data.get("updated_at"),
}
def _load_state_from_file(path: Path):
if not path.exists():
return {}
try:
raw = read_text(path).strip()
except Exception:
return {}
if not raw:
return {}
try:
return _normalize_state(json.loads(raw))
except Exception:
# 兼容旧版纯文本 session 文件
return {
"email": "",
"session_token": raw,
"account_id": "",
"workspace_name": "",
"updated_at": path.stat().st_mtime,
}
def _save_state(state):
# 如果是软链,写入目标路径(避免 Docker 场景下误删/替换软链)
target = STATE_FILE.resolve()
write_text(target, json.dumps(_normalize_state(state), indent=2, ensure_ascii=False))
# Docker bind mount 下文件常由容器用户写入;给宿主机用户保留可访问权限
os.chmod(target, STATE_FILE_MODE)
def _migrate_legacy_state():
if STATE_FILE.exists():
return
state = _load_state_from_file(LEGACY_SESSION_FILE)
if state:
_save_state(state)
try:
LEGACY_SESSION_FILE.unlink()
except Exception:
pass
def load_admin_state():
override = _ADMIN_STATE_OVERRIDE.get()
if isinstance(override, dict):
return dict(override)
_migrate_legacy_state()
return _load_state_from_file(STATE_FILE)
def save_admin_state(state):
_save_state(state)
def update_admin_state(**kwargs):
override = _ADMIN_STATE_OVERRIDE.get()
if isinstance(override, dict):
state = _normalize_state({**override, **kwargs})
state["updated_at"] = time.time()
_ADMIN_STATE_OVERRIDE.set(state)
return state
state = load_admin_state()
state.update(kwargs)
state["updated_at"] = time.time()
save_admin_state(state)
return state
@contextmanager
def temporary_admin_state(**kwargs):
"""Use an in-thread admin state without mutating global ``state.json``."""
base = load_admin_state()
state = _normalize_state({**base, **kwargs})
state["updated_at"] = state.get("updated_at") or time.time()
token = _ADMIN_STATE_OVERRIDE.set(state)
try:
yield state
finally:
_ADMIN_STATE_OVERRIDE.reset(token)
def clear_admin_state():
if STATE_FILE.exists():
# 写空内容而不是删除(保护 Docker 软链)
target = STATE_FILE.resolve()
write_text(target, "{}")
os.chmod(target, STATE_FILE_MODE)
if LEGACY_SESSION_FILE.exists():
LEGACY_SESSION_FILE.unlink()
def get_admin_email():
"""Round 12 S7 — route through WorkspacePool active workspace.
Backwards-compat: if the pool is empty / module unavailable, fall back
to the legacy state.json read so single-workspace installs are unaffected.
"""
override = _ADMIN_STATE_OVERRIDE.get()
if isinstance(override, dict) and override.get("email"):
return override["email"]
try:
from autoteam.workspace_pool import default_pool
active = default_pool.get_active()
if active and active.get("admin_email"):
return active["admin_email"]
except Exception:
pass
return load_admin_state().get("email", "")
def get_admin_session_token():
return load_admin_state().get("session_token", "")
def _is_valid_uuid(value: str) -> bool:
"""检查是否为有效的 UUID 格式"""
import re
return bool(re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", value, re.I))
def get_chatgpt_account_id():
"""Round 12 S7 — route through WorkspacePool active workspace.
Backwards-compat: pool empty → legacy state.json read → CHATGPT_ACCOUNT_ID env.
Pool ID still must pass UUID validation (filters legacy `user-xxx` ids).
"""
override = _ADMIN_STATE_OVERRIDE.get()
if isinstance(override, dict):
override_id = (override.get("account_id") or "").strip()
if override_id and _is_valid_uuid(override_id):
return override_id
try:
from autoteam.workspace_pool import default_pool
active = default_pool.get_active()
if active:
pool_aid = (active.get("account_id") or "").strip()
if pool_aid and _is_valid_uuid(pool_aid):
return pool_aid
except Exception:
pass
state = load_admin_state()
state_id = state.get("account_id", "")
# state.json 里的值必须是 UUID 格式才有效(user-xxx 是 user ID 不是 account ID)
if state_id and _is_valid_uuid(state_id):
return state_id
return os.environ.get("CHATGPT_ACCOUNT_ID", "")
def get_admin_password():
return load_admin_state().get("password", "")
def get_chatgpt_workspace_name():
state = load_admin_state()
return state.get("workspace_name", "")
def get_admin_state_summary():
state = load_admin_state()
return {
"configured": bool(state.get("session_token") and state.get("account_id")),
"email": state.get("email", ""),
"account_id": state.get("account_id", ""),
"workspace_name": state.get("workspace_name", ""),
"session_present": bool(state.get("session_token")),
"password_saved": bool(state.get("password")),
"updated_at": state.get("updated_at"),
}
|