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"),
    }