AI 配置改为按用户独立存储
每个用户 AI 配置存 data/user_data/ai_settings/{username}.json,
读配置时用户级优先,无则回退到全局 secrets.json。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -48,15 +48,28 @@ class TickflowKeyIn(BaseModel):
|
||||
api_key: str
|
||||
|
||||
|
||||
def _get_ai_config(username: str | None, key: str, default: str = "") -> str:
|
||||
"""读 AI 配置: 用户级优先, 无则全局。"""
|
||||
if username:
|
||||
val = secrets_store.load_ai_config(username).get(key)
|
||||
if val:
|
||||
return val
|
||||
return secrets_store.get_ai_config(key, default)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_settings() -> dict:
|
||||
def get_settings(request: Request) -> dict:
|
||||
"""返回当前配置概况(Key 脱敏)。"""
|
||||
from app.config import settings
|
||||
from app.services import preferences
|
||||
from app.services.ai_provider import ai_configured, current_ai_model, current_codex_command
|
||||
|
||||
username = getattr(request.state, "username", None)
|
||||
key = secrets_store.get_tickflow_key()
|
||||
ai_provider = secrets_store.get_ai_config("ai_provider", settings.ai_provider)
|
||||
ai_provider = _get_ai_config(username, "ai_provider", settings.ai_provider)
|
||||
ai_api_key = secrets_store.load_ai_config(username).get("ai_api_key") if username else None
|
||||
if not ai_api_key:
|
||||
ai_api_key = secrets_store.get_ai_key()
|
||||
return {
|
||||
"mode": tf_client.current_mode(),
|
||||
"tickflow_api_key_masked": secrets_store.mask(key),
|
||||
@@ -68,15 +81,15 @@ def get_settings() -> dict:
|
||||
"extras_caps": extras_caps(),
|
||||
# 首次使用引导
|
||||
"onboarding_completed": preferences.get_onboarding_completed(),
|
||||
# AI 配置
|
||||
# AI 配置 (用户级优先)
|
||||
"ai_provider": ai_provider,
|
||||
"ai_base_url": secrets_store.get_ai_config("ai_base_url", settings.ai_base_url),
|
||||
"ai_api_key_masked": secrets_store.mask(secrets_store.get_ai_key()),
|
||||
"has_ai_key": bool(secrets_store.get_ai_key()),
|
||||
"ai_base_url": _get_ai_config(username, "ai_base_url", settings.ai_base_url),
|
||||
"ai_api_key_masked": secrets_store.mask(ai_api_key),
|
||||
"has_ai_key": bool(ai_api_key),
|
||||
"ai_configured": ai_configured(ai_provider),
|
||||
"ai_model": current_ai_model(),
|
||||
"ai_codex_command": current_codex_command(),
|
||||
"ai_user_agent": secrets_store.get_ai_config("ai_user_agent", settings.ai_user_agent),
|
||||
"ai_user_agent": _get_ai_config(username, "ai_user_agent", settings.ai_user_agent),
|
||||
}
|
||||
|
||||
|
||||
@@ -238,11 +251,14 @@ class AiSettingsIn(BaseModel):
|
||||
|
||||
|
||||
@router.post("/ai")
|
||||
def save_ai_settings(req: AiSettingsIn) -> dict:
|
||||
"""保存 AI 配置(全部持久化到 secrets.json)"""
|
||||
def save_ai_settings(req: AiSettingsIn, request: Request) -> dict:
|
||||
"""保存 AI 配置(按用户独立存储)。"""
|
||||
from app.config import settings
|
||||
from app.services.ai_provider import ai_configured, current_ai_model, current_ai_provider, current_codex_command, normalize_codex_command
|
||||
|
||||
username = getattr(request.state, "username", None)
|
||||
target = username if username else "global"
|
||||
|
||||
updates: dict = {}
|
||||
if req.provider:
|
||||
updates["ai_provider"] = req.provider
|
||||
@@ -255,10 +271,10 @@ def save_ai_settings(req: AiSettingsIn) -> dict:
|
||||
updates["ai_api_key"] = req.api_key
|
||||
settings.ai_api_key = req.api_key
|
||||
else:
|
||||
secrets_store.clear("ai_api_key")
|
||||
secrets_store.clear_ai_config(target, "ai_api_key")
|
||||
settings.ai_api_key = ""
|
||||
if req.provider == "codex_cli" and not req.model:
|
||||
secrets_store.clear("ai_model")
|
||||
secrets_store.clear_ai_config(target, "ai_model")
|
||||
settings.ai_model = ""
|
||||
elif req.model:
|
||||
updates["ai_model"] = req.model
|
||||
@@ -270,12 +286,12 @@ def save_ai_settings(req: AiSettingsIn) -> dict:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
updates["ai_codex_command"] = codex_command
|
||||
settings.ai_codex_command = codex_command
|
||||
# user_agent 允许清空(回到默认浏览器 UA),故无条件持久化
|
||||
# user_agent 允许清空,故无条件持久化
|
||||
updates["ai_user_agent"] = req.user_agent
|
||||
settings.ai_user_agent = req.user_agent
|
||||
|
||||
if updates:
|
||||
secrets_store.save(updates)
|
||||
secrets_store.save_ai_config(target, updates)
|
||||
|
||||
provider = current_ai_provider()
|
||||
return {
|
||||
@@ -288,15 +304,15 @@ def save_ai_settings(req: AiSettingsIn) -> dict:
|
||||
|
||||
|
||||
@router.delete("/ai")
|
||||
def clear_ai_settings() -> dict:
|
||||
"""一键清空 AI 配置(provider / base_url / api_key / model)。
|
||||
|
||||
保留 ai_user_agent —— 自定义请求头与凭证解耦,清空凭证不影响绕过 CDN 拦截的设置。
|
||||
"""
|
||||
def clear_ai_settings(request: Request) -> dict:
|
||||
"""一键清空当前用户的 AI 配置。"""
|
||||
from app.config import settings
|
||||
|
||||
secrets_store.clear("ai_provider", "ai_base_url", "ai_api_key", "ai_model", "ai_codex_command")
|
||||
# 同步重置运行时内存(provider 回默认值,其余置空)
|
||||
username = getattr(request.state, "username", None)
|
||||
target = username if username else "global"
|
||||
|
||||
secrets_store.clear_ai_config(target, "ai_provider", "ai_base_url", "ai_api_key", "ai_model", "ai_codex_command")
|
||||
# 同步重置运行时内存
|
||||
settings.ai_provider = "openai_compat"
|
||||
settings.ai_base_url = ""
|
||||
settings.ai_api_key = ""
|
||||
|
||||
@@ -94,3 +94,53 @@ def mask(key: str, prefix: int = 4, suffix: int = 4) -> str:
|
||||
if len(key) <= prefix + suffix:
|
||||
return "•" * len(key)
|
||||
return f"{key[:prefix]}{'•' * 6}{key[-suffix:]}"
|
||||
|
||||
|
||||
# ================================================================
|
||||
# 按用户 AI 配置 (data/user_data/ai_settings/{username}.json)
|
||||
# ================================================================
|
||||
|
||||
def _ai_config_path(username: str) -> Path:
|
||||
from app.config import settings
|
||||
p = settings.data_dir / "user_data" / "ai_settings" / f"{username}.json"
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
def load_ai_config(username: str) -> dict:
|
||||
"""加载指定用户的 AI 配置。用户不存在时返回空 dict。"""
|
||||
p = _ai_config_path(username)
|
||||
if p.exists():
|
||||
try:
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("ai_settings/%s.json malformed: %s", username, e)
|
||||
return {}
|
||||
|
||||
|
||||
def save_ai_config(username: str, updates: dict) -> dict:
|
||||
"""合并写入指定用户的 AI 配置。返回新内容。"""
|
||||
current = load_ai_config(username)
|
||||
current.update({k: v for k, v in updates.items() if v is not None})
|
||||
p = _ai_config_path(username)
|
||||
p.write_text(json.dumps(current, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
try:
|
||||
os.chmod(p, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
return current
|
||||
|
||||
|
||||
def clear_ai_config(username: str, *keys: str) -> dict:
|
||||
"""清掉指定用户的 AI 配置字段。不传 keys 则删除整个文件。"""
|
||||
p = _ai_config_path(username)
|
||||
if not p.exists():
|
||||
return {}
|
||||
if not keys:
|
||||
p.unlink()
|
||||
return {}
|
||||
current = load_ai_config(username)
|
||||
for k in keys:
|
||||
current.pop(k, None)
|
||||
p.write_text(json.dumps(current, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
return current
|
||||
|
||||
Reference in New Issue
Block a user