API 契约:层③规则试验台 — social-media-organic-tool
增量契约,只定义规则试验台 3 接口。照docs/templates/COMPONENT-BACKEND.md §4。 其余接口见现有API-CONTRACT.md。前端标准件test-bench.js消费本契约,前端模板不改。
通用约定
- 鉴权:全部挂现有登录态(JWT)。前端组件通过
window.AOS_ANNOTATE_AUTH同款 token getter 注入Authorization: Bearer <access_token>(从sessionStorage.access_token读)。未带 = 401。 - 租户盖章:
tenant_id/updated_by一律服务端从登录态盖章,不信前端 body。当前单租户 →tenant_id="default"。 - 限流:全部接口挂
fastapi-limiterRateLimiter(按用户/IP)。dryrun额外挂 llm_guard 三门 + 每日 dryrun 次数上限。 - 响应包络:沿用现有项目 envelope(success/data/error)。错误信息不泄露栈/密钥。
- JSON 收发;
config是自描述 JSON(见RULES.md §2每条规则的 config 形状)。
1. GET /api/rules
拉当前 tenant 的规则清单(R1–R6),试验台开面板时调。
Request:无 body。可选 ?id=R1 只取一条。
Response 200:
{ "success": true, "data": [
{ "id": "R1", "action": "搜索方向生成",
"why": "方向太泛搜出全是meme/太窄漏爆款,营销方向性错则找爆款链从源头歪",
"config": { "knobs": { "direction_count_min": 3, "direction_count_max": 5,
"anchor_intent": "commercial_review", "avoid_nsfw": true, "max_tokens": 1000 },
"prompt_template": "You are a social media MARKETING researcher ..." },
"version": 1, "state": "theory" }
], "error": null }
state:theory(🔴)/yellow(🟡)/green(🟢),对应RULES.md三态。- 只返回当前
active版本的 config;历史版本走GET /versions。 - R6 的 config 只有
knobs,无prompt_template。
2. POST /api/rules/{id}/dryrun
用传入的 config(可能是 Anderson 刚改、未存的调整值)对 sample 跑一次该动作,返回 output。不落库、不计正式账。
Request body:
{ "config": { "knobs": { "...": "..." }, "prompt_template": "..." },
"sample": { "material_id": 123 } }
config:试验台当前旋钮值(前端从面板收集)。dryrun 用这个跑,不是库里的 active 值——这样能"改旋钮→立刻看新输出→并排对比"。sample:该动作的输入。按规则不同:R1/R3/R4/R5 传{material_id}或{text};R2 传{posts:[...]};R6 传{keywords:[...], platform}。
Response 200:
{ "success": true, "data": { "output": { "...": "该动作的产出" },
"usage": { "input_tokens": 0, "output_tokens": 0, "est_cost_usd": 0.0 } },
"error": null }
- LLM 规则(R1–R5):
output= 方向数组/标签/改写文/成片/模板;usage报本次 dryrun 花费(仅供展示,不写 llm_usage 正式账)。 - R6(非 LLM):
output= 过滤+排序后留下的帖子 + 每条 engagement_score 计算;usage全 0(不花 LLM 钱)。
花钱红线(硬约束,backend 必做):
- 复用
check_input_length(Gate-2)+check_daily_cost(Gate-4)+clamp_max_tokens(Gate-3,把 config 里的max_tokens夹到 .env 全局上限内); - 复用
fastapi-limiterRateLimiter; - 额外:每 tenant 每日 dryrun 次数上限(防刷爆费用),超限返回 429。
错误码表:
| HTTP | error.code | 触发 | 前端表现 |
|---|---|---|---|
| 400 | INVALID_CONFIG | config 结构非法 / 缺必填旋钮 / prompt_template 非字符串 | 提示"配置格式错误" |
| 400 | INVALID_SAMPLE | sample 缺字段 / material_id 不存在 | 提示"样本无效" |
| 401 | UNAUTHORIZED | 无 / 过期 token | 跳登录 |
| 404 | RULE_NOT_FOUND | {id} 不是 R1–R6 | 提示"规则不存在" |
| 413 | INPUT_TOO_LONG | 超 LLM_MAX_INPUT_CHARS(Gate-2) | 提示"输入过长" |
| 429 | RATE_LIMITED | 触限流(用户/IP) | 提示"操作太频繁,稍后再试" |
| 429 | DRYRUN_QUOTA_EXCEEDED | 超当日 dryrun 次数上限 | 提示"今日试跑次数已用完" |
| 429 | DAILY_COST_CAP | 触 Gate-4 日成本上限 | 提示"今日 AI 额度已用完" |
| 502 | LLM_UPSTREAM_ERROR | DeepSeek/上游异常 | 提示"AI 服务暂时不可用" |
3. POST /api/rules/{id}/versions
把 config 存为该规则新生效版本(version 自增,旧 active 归档),可回退。
Request body:
{ "config": { "knobs": { "...": "..." }, "prompt_template": "..." },
"note": "把方向数调到 5、语气更商业" }
tenant_id/updated_by服务端盖章,不接受 body 传入。
Response 201:
{ "success": true, "data": { "id": "R1", "version": 2, "status": "active", "updated_at": "ISO8601" },
"error": null }
服务端事务(不变量):同一 (rule_id, tenant_id) 内:插入新行 version=旧max+1, status="active" 且 把旧 active 行置 archived,同一事务保证"只有一行 active"。
错误码表:
| HTTP | error.code | 触发 |
|---|---|---|
| 400 | INVALID_CONFIG | config 结构非法 / 缺必填旋钮 |
| 401 | UNAUTHORIZED | 无/过期 token |
| 403 | FORBIDDEN | 角色无"改规则"权限(多租户只读角色) |
| 404 | RULE_NOT_FOUND | {id} 不是 R1–R6 |
| 409 | VERSION_CONFLICT | 并发写同版本(唯一约束冲突),前端重取后重试 |
4. (可选)GET /api/rules/{id}/versions
版本历史,供回退。
Response 200:
{ "success": true, "data": [
{ "version": 2, "status": "active", "note": "...", "updated_by": "anderson", "updated_at": "..." },
{ "version": 1, "status": "archived", "note": "seed", "updated_by": "system-seed", "updated_at": "..." }
], "error": null }
回退:前端选历史版本 → 调 POST /versions 用该 config 存为新 active 版本(不"原地复活",保持 append-only 血缘可追溯)。
降级
前端 test-bench.js 在无后端 / 401 时优雅降级为"只读提示",不白屏。
本次引用的 skills:api-design、cost-aware-llm-pipeline、mcp-server-patterns(JSON-RPC 契约参考)