上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事

- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段);
  修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品
- dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开;
  settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch
- 新增用户管理(users API/admin service/改密页) + alembic 022/023/024
- 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example
- A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-30 18:08:13 +08:00
parent a77212781c
commit df1856d793
150 changed files with 8616 additions and 1765 deletions

View File

@@ -43,6 +43,10 @@ class CreateApiKeyRequest(BaseModel):
# 注:不接收 url 字段token站固定自家站基石B
class TestApiKeyRequest(CreateApiKeyRequest):
pass
def _format_key(k: UserApiKey) -> dict:
"""只显 provider + key_last4不暴露余额/用量/encrypted_key红线"""
return {
@@ -77,8 +81,13 @@ def create_api_key(
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
db: Session = Depends(get_db),
):
"""录入 API KeyFernet 加密存储只保存后4位明文用于展示"""
from app.core.response import raise_business
"""录入/更新 API KeyFernet 加密存储只保存后4位明文用于展示
覆盖式倩倩姐2026-06-23拍板同 user+workspace+provider 已存在则直接更新,
不报错——「切换/修改 key」无需先删再录前端点「修改」重录即覆盖。
"""
encrypted = encrypt_api_key(body.api_key)
last4 = mask_api_key(body.api_key)
# 检查同 user+workspace+provider 是否已有
existing = (
db.query(UserApiKey)
@@ -90,15 +99,19 @@ def create_api_key(
.first()
)
if existing:
raise_business(f"已存在 {body.provider} 的 API Key请先删除再录入")
existing.encrypted_key = encrypted
existing.key_last4 = last4
db.commit()
db.refresh(existing)
logger.info("API key updated: user=%s provider=%s", current_user.user_id, body.provider)
return ok(_format_key(existing))
encrypted = encrypt_api_key(body.api_key)
key_obj = UserApiKey(
user_id=current_user.user_id,
workspace_id=current_user.workspace_id,
provider=body.provider,
encrypted_key=encrypted,
key_last4=mask_api_key(body.api_key),
key_last4=last4,
)
db.add(key_obj)
db.commit()
@@ -107,6 +120,48 @@ def create_api_key(
return ok(_format_key(key_obj))
@router.post("/test")
async def test_api_key(
body: TestApiKeyRequest,
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
):
"""用客户输入的明文 key 做一次轻量连通性测试,不落库。"""
import httpx
from app.core.config import get_settings
from app.core.response import raise_business
settings = get_settings()
provider = body.provider
if provider in ("openai", "apiports"):
base = (settings.APIPORTS_BASE_URL or settings.IMAGE_API_BASE).rstrip("/")
model = settings.MODEL_TEXT
elif provider == "codeproxy":
base = settings.CODEPROXY_BASE_URL.rstrip("/")
model = "gpt-5.5"
else:
raise_business("该 Provider 暂不支持在线测试")
if not base:
raise_business("服务端未配置该 Provider 的 Base URL")
try:
async with httpx.AsyncClient(timeout=12) as client:
resp = await client.post(
f"{base}/chat/completions",
headers={"Authorization": f"Bearer {body.api_key}"},
json={
"model": model,
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 4,
},
)
resp.raise_for_status()
except Exception as exc:
raise_business(f"Key 测试失败:{type(exc).__name__}")
logger.info("API key test ok: user=%s provider=%s", current_user.user_id, provider)
return ok({"ok": True, "provider": provider})
@router.delete("/{key_id}")
def delete_api_key(
key_id: int,