FinClaw V1 API Contract Design
状态:Accepted Initial Design / B-3 工程蓝图 日期:2026-05-16 项目:FinClaw 文档级别:项目级 API / SSE 契约 上游文档:v1-prd.md §6 / §7、v1-product-object-and-schema-design.md §3 ~ §13、v1-agent-orchestration-design.md §2 / §10A / §10B、v1-ui-ux-interaction-design.md §2 / §6A / §10、v1-commercial-signal-instrumentation-design.md §3 / §4 / §9 配套文档:v1-tech-stack-and-architecture-design.md(B-1)、v1-data-and-persistence-design.md(B-2)
1. Purpose
本文是 FinClaw V1 工程仓库 /Users/mlabs/Programs/CurvatureLabs/finclaw/ 后端对前端(以及未来对外部系统)的 API 契约 single source of truth。
回答 4 个工程问题:
- 全部 REST endpoint 的 URL / method / request / response 是什么?
- ReAct 期间通过 SSE 推给前端的事件清单是什么?payload 是什么?
- V1 内测的鉴权与隔离怎么做?
- 错误、限流、跨对象引用的统一约定是什么?
本文不重复 v1-product-object-and-schema-design.md 的字段定义;API 层只负责把对象 schema 暴露为 HTTP shape。
2. API Style and Versioning
2.1 API 风格
| 维度 | 选型 |
|---|---|
| 协议 | HTTP/1.1(Trial 期间 dev 用 HTTP;外发 dev 链接前升 HTTPS) |
| 风格 | RESTful(CRUD + 行为类 sub-resource) |
| 编码 | UTF-8 JSON(除 SSE) |
| 时间格式 | ISO-8601 + UTC(Z 后缀) |
| Streaming | text/event-stream(SSE);不引入 WebSocket |
| OpenAPI | FastAPI 自动生成 /api/openapi.json(dev 暴露;trial-prod 关闭) |
| Snake / Camel | request / response 均 snake_case,与 Pydantic 字段一致 |
| 文件上传 | V1 不支持(mvp-product-definition.md §15 不引入文件上传) |
2.2 路径前缀
/api/<resource> ← V1 默认(无 v1 前缀)
V1 内测期间所有 endpoint 不带 /v1/ 前缀:内测 cohort 仅 3 人,破坏性变更可直接通知。trial 退出 / 公开扩展时再升级到 /api/v1/...。本文所有 path 写 /api/...,工程实现时可全局替换。
2.3 HTTP 状态码语义
| Code | 用途 |
|---|---|
| 200 | 同步成功(含读、行为类 mutation) |
| 201 | POST 创建成功 + 返回新对象 |
| 202 | 任务已接收,进入 SSE 异步流(POST /api/tasks) |
| 204 | 成功无返回体(PATCH consent revoke 等) |
| 400 | 请求 schema 错误 |
| 401 | 未鉴权 |
| 403 | 已鉴权但无权限(含 BoundaryGuard 拒绝) |
| 404 | 对象不存在 |
| 409 | 状态冲突(如 thread 已 closed 不能 refresh) |
| 422 | Pydantic 字段校验失败(FastAPI 默认) |
| 429 | 限流(§8) |
| 451 | Sensitive input rejection(§7.2 RB-2 类) |
| 500 | 未捕获异常 |
| 503 | Kill switch 触发 / dependency unavailable |
2.4 Idempotency
| 类型 | 策略 |
|---|---|
| GET | 天然幂等 |
| DELETE | 幂等(重复 delete 返回 204) |
| PATCH | 幂等(同 payload 多次 PATCH 等价于一次) |
| POST /api/tasks | 非幂等;客户端可传 Idempotency-Key header,5 分钟内重复 key 返回上次的 task_id |
POST /api/threads/{id}/refresh | 同上:Idempotency-Key 5 分钟窗口 |
3. Authentication
3.1 V1 鉴权方案
| 项 | V1 选型 |
|---|---|
| 方案 | Bearer Token(每位内测用户一枚 long-lived token) |
| Header | Authorization: Bearer <token> |
| Token 来源 | trial 启动时 python -m server.scripts.seed_user --user_id=alice 生成;分发渠道为 Labs 内部协作(v1-engineering-kickoff-decisions.md D-01) |
| Token 存储 | 后端:data/_internal/tokens.json(仅项目发起人可读,git ignored);前端:浏览器 localStorage["finclaw_token"] |
| Token 撤回 | python -m server.scripts.revoke_user_token --user_id=alice 立即生效 |
| Token 旋转 | V1 不实现自动旋转;如泄露立即手动 revoke + reissue |
3.2 Anonymous(仅特定 endpoint)
| Endpoint | 允许匿名? |
|---|---|
GET /api/health | 是 |
GET /api/openapi.json | dev 是;trial-prod 否 |
| 其余全部 | 否 |
3.3 用户隔离
- 所有 user-scoped endpoint 的 path 含
{user_id}时,必须与 token 关联的 user_id 一致;不一致返回 403; - 行为类 endpoint(
POST /api/tasks等)不在 path 中带user_id;后端从 token 解析 user_id,所有数据写入data/cognition/<user_id>/...(v1-data-and-persistence-design.md §4.1); - 项目发起人有特殊 admin token(
scope: admin),可访问/api/_admin/*端点(v1-data-and-persistence-design.md §4.1 _internal 路径)。
3.4 后续 OAuth 升级路径预留
V1 之后切到 OAuth:
- 当前 Bearer token 中间件抽象为
Auth: Protocol; - 升级时增加
OAuth2Auth实现,token 提取逻辑替换; - API path / response shape 不变。
4. REST Endpoints
4.1 端点总览
| Method | Path | 用途 |
|---|---|---|
GET | /api/health | 健康检查 |
POST | /api/tasks | 创建任务 → 进入 SSE 流 |
GET | /api/tasks/{task_id} | 查询任务最终状态(fallback;SSE 是主路径) |
GET | /api/snapshots/{snapshot_id} | 读取 snapshot |
POST | /api/snapshots/{snapshot_id}/save-as-thread | 从 snapshot 创建 thread |
GET | /api/snapshots | 列出当前用户 snapshot(分页) |
GET | /api/threads/{thread_id} | 读取 thread header + linked snapshots refs |
GET | /api/threads/{thread_id}/history | 读取 thread refresh history(分页) |
POST | /api/threads/{thread_id}/refresh | 触发 refresh → 进入 SSE 流 |
PATCH | /api/threads/{thread_id} | 修改 thread 元数据(status / title / refresh_conditions) |
GET | /api/threads | 列出当前用户 thread(分页) |
GET | /api/checkpoints/{checkpoint_id} | 读取 checkpoint |
GET | /api/evidence/{evidence_id} | 读取单条 evidence |
GET | /api/snapshots/{snapshot_id}/evidence | 列出 snapshot 关联 evidence 与 quality notes |
POST | /api/feedback | 提交反馈 / 请求人工复核 |
GET | /api/users/me | 当前用户基础信息(user_id / scope / created_at) |
GET | /api/users/{user_id}/profile-consent | 读取 ProfileConsent 当前状态 |
POST | /api/users/{user_id}/profile-consent | 创建 / 更新 ProfileConsent |
DELETE | /api/users/{user_id}/profile-consent/{consent_id} | 撤回 consent(不删账号) |
POST | /api/events/cs | 前端上报商业信号事件(v1-commercial-signal-instrumentation-design.md §4) |
POST | /api/events/cs/batch | 批量上报(前端 buffer flush) |
Admin-only:
| Method | Path | 用途 |
|---|---|---|
GET | /api/_admin/feedback/queue | 人工复核待处理列表 |
POST | /api/_admin/feedback/{feedback_id}/review | reviewer 提交 grade |
POST | /api/_admin/kill-switch | 触发 / 解除 kill switch |
4.2 详细 schema
字段命名与 v1-product-object-and-schema-design.md 1:1 对齐;下文仅展示 envelope 形态与 V1 必要字段。完整字段定义以 schema 设计文档为准。
4.2.1 GET /api/health
response_200:
status: "ok"
version: "<git_short_sha>"
build_time: "<iso8601>"
kill_switch_active: false
providers:
primary: "gpt-5.5"
secondary: "kimi-k2.6"
4.2.2 POST /api/tasks
请求:
request:
question: string # 用户自然语言输入;必填
hint_task_type: enum | null # snapshot / thread_refresh / risk_challenge / pre_execution / null
thread_ref: string | null # 若是 thread refresh / continuation;格式见 §6
snapshot_ref: string | null # 若基于已有 snapshot 提问
user_context_overrides: # 可选:本次任务覆盖 saved profile
research_depth: enum | null
risk_prompt_preference: enum | null
language_preference: enum | null
client_metadata:
client: web | mobile-web
client_version: string
Header:Idempotency-Key: <client-generated-uuid>(可选)
响应:
response_202:
task_id: "task_<iso>_<hash>"
sse_url: "/api/tasks/task_xxx/stream" # 前端立即订阅
estimated_ms: integer | null
错误:
| 状态 | 触发 |
|---|---|
| 400 | question 为空 / 超长(> 4000 字符) |
| 451 | Sensitive Input Classifier 在前置检查中检测到凭证 / 私钥(详见 §7.2) |
| 503 | kill switch 触发 |
4.2.3 GET /api/tasks/{task_id}/stream(SSE)
详见 §5。
4.2.4 GET /api/tasks/{task_id}
response_200:
task_id: string
status: enum # running / completed / failed / cancelled
task_type: enum
created_at: datetime
completed_at: datetime | null
produced_object_refs: # 任务产出的 object refs(§6)
- "snapshot:snap_xxx"
- "checkpoint:chk_xxx"
failure:
code: string | null
message: string | null
4.2.5 GET /api/snapshots/{snapshot_id}
response_200:
snapshot_id: string
title: string
cognition_object: object # 见 schema §3.1
task_type: enum
time_context: object
market_context: object
main_thesis: string
supporting_reasons: array
counter_thesis: array
uncertainties: array
watch_questions: array
invalidators: array
evidence_items: array # 简化引用 + inline summary
data_quality_notes: array
advisor_outputs: array | null
pre_execution_checkpoint_refs: array
thread_proposal: object | null
execution_boundary: string
ui_state: enum # complete / needs_clarification / low_confidence / source_limited / action_adjacent / thread_candidate
created_at: datetime
schema_version: "1.0"
response_404:
error: { code: "snapshot_not_found", message: "..." }
4.2.6 POST /api/snapshots/{snapshot_id}/save-as-thread
请求:
request:
title: string # 用户可编辑
user_focus_reason: string
research_style: object | null
refresh_conditions: array
invalidators: array
watch_questions: array | null # 默认沿用 snapshot
save_user_context: bool # 与 ProfileConsent 联动
consent_ack: # 当 save_user_context=true 时必填
consent_id: string # 引用现有 consent 或新建
响应:
response_201:
thread_id: string
thread: { ...full thread header... }
错误:
| 状态 | 触发 |
|---|---|
| 409 | 同 snapshot 已 save 过为 thread(返回原 thread_id) |
| 403 | save_user_context=true 但无对应 consent |
4.2.7 GET /api/snapshots / GET /api/threads
分页约定:
?limit=20&cursor=<opaque>&sort=created_at:desc&status=active
response_200:
items: array
next_cursor: string | null
total_known: integer | null # 文件系统不一定全量 count;可为 null
4.2.8 GET /api/threads/{thread_id}
response_200:
thread_id: string
title: string
status: enum # proposed / active / refresh_due / refreshed / paused / closed / merged
object: object
user_focus_reason: string
research_style: object | null
linked_snapshots: array # ["snapshot:snap_xxx", ...]
current_thesis: string
counter_thesis: array
watch_questions: array
refresh_conditions: array
invalidators: array
evidence_state: object
pre_execution_checkpoints: array
profile_consent_refs: array
last_refreshed_at: datetime | null
closed_reason: string | null
object_version: integer
schema_version: "1.0"
4.2.9 GET /api/threads/{thread_id}/history
response_200:
thread_id: string
changes: # 一行一个 RefreshChange,见 schema §4.3
- refresh_id: string
trigger_type: enum
previous_snapshot_ref: string
new_snapshot_ref: string
new_facts: array
changed_inferences: array
risk_changes: array
evidence_changes: array
watch_question_updates: array
execution_boundary_update: string | null
created_at: datetime
next_cursor: string | null
4.2.10 POST /api/threads/{thread_id}/refresh
请求:
request:
trigger_type: enum # user / condition / time / evidence / counter_thesis
user_supplied_context: string | null
响应:
response_202:
task_id: "task_xxx"
sse_url: "/api/tasks/task_xxx/stream"
错误:
| 状态 | 触发 |
|---|---|
| 409 | thread.status ∈ merged |
4.2.11 PATCH /api/threads/{thread_id}
请求(仅传待修改字段):
request:
status: enum | null # 可改:active / paused / closed
closed_reason: string | null # status=closed 时必填
title: string | null
refresh_conditions: array | null
invalidators: array | null
响应:
response_200:
thread: { ...full thread header... }
4.2.12 GET /api/checkpoints/{checkpoint_id}
response_200:
checkpoint_id: string
source_question: string
normalized_cognition_task: string
related_snapshot_ref: string | null
related_thread_ref: string | null
strategy_hypothesis: object | null
supporting_conditions: array
risk_constraints: array
invalidators: array
user_confirmation_needed: array
data_quality_notes: array
forbidden_execution_fields: array # 必须包含;提示前端不渲染对应控件
non_execution_statement: string
created_at: datetime
schema_version: "1.0"
API 层硬约束:response 中必须包含 forbidden_execution_fields 字段(即使为空数组),且永不包含 order_* / quantity / leverage / account / wallet_* / private_key / api_key / auto_execute / alert_trigger_to_execute 字段(v1-product-object-and-schema-design.md §5.2)。
4.2.13 GET /api/evidence/{evidence_id} / GET /api/snapshots/{snapshot_id}/evidence
response_200:
evidence_id: string
claim_ref: string # §6
source_type: enum
source_ref: string | null
observed_at: datetime | null
retrieved_at: datetime | null
evidence_status: enum
limitation: string
schema_version: "1.0"
/api/snapshots/{snapshot_id}/evidence 返回 { evidence_items: [...], data_quality_notes: [...] }。
4.2.14 POST /api/feedback
request:
feedback_kind: enum # quick_thumbs / detailed_review_request / boundary_concern / sensitive_concern
related_object_ref: string # §6
rating: enum | null # helpful / partially_helpful / not_helpful / needs_review
comment: string | null
request_human_review: bool
client_metadata: object
response_201:
feedback_id: string
human_review_queued: bool
4.2.15 ProfileConsent endpoints
GET /api/users/{user_id}/profile-consent
response_200:
consent_id: string # 当前生效版本
context_ref: string
save_to_profile: bool
save_to_thread: bool
training_use_allowed: bool
de_identified: bool
retention_scope: enum
delete_or_revoke_available: true
confirmed_at: datetime
consent_version: integer
consent_history:
- { consent_id: string, confirmed_at: datetime, revoked_at: datetime | null }
POST /api/users/{user_id}/profile-consent
request:
context_ref: string
save_to_profile: bool
save_to_thread: bool
training_use_allowed: bool
de_identified: bool
retention_scope: enum # current_task / thread / profile / none
consent_text_version: string # 用户看到的同意书版本号
response_201:
consent_id: string
consent_version: integer
DELETE /api/users/{user_id}/profile-consent/{consent_id}
response_204: (no body)
side_effects: # 见 v1-data-and-persistence-design §6.5
- "已生成的 Snapshot / Thread / Checkpoint 保留"
- "后续 LLM 调用不再注入 saved profile"
- "Commercial signal events 自此刻起停止采集;历史 events 48h 内删除"
4.2.16 商业信号事件 endpoint
POST /api/events/cs:单条上报;POST /api/events/cs/batch:批量。
request_single:
event_name: string # 见 cs-instr §4
event_category: enum
ts: datetime
related_object_refs: object
payload: object
client_metadata: object
response_202:
event_id: string
accepted: bool # 若 ProfileConsent.consent_for_trial_data=false → false(不入库)
后端在落库前必须读取 ProfileConsent 验证 consent_state(v1-commercial-signal-instrumentation-design.md §3)。
4.2.17 Admin endpoints
GET /api/_admin/feedback/queue → 待复核 feedback 列表
POST /api/_admin/feedback/{id}/review → { grade, reviewer_notes, action }
POST /api/_admin/kill-switch → { action: "engage" | "release", reason }
仅 scope: admin token 可访问;非 admin 一律 403。
5. SSE Events Catalog
5.1 SSE 协议约定
GET /api/tasks/{task_id}/stream
Accept: text/event-stream
Authorization: Bearer <token>
→ stream:
event: task_started
id: 1
data: {"task_id": "...", "task_type": "snapshot", "ts": "..."}
event: cognition_step
id: 2
data: {...}
event: task_completed
id: N
data: {...}
| 字段 | 描述 |
|---|---|
event | 事件名(见 §5.2) |
id | 单调递增(断线重连时客户端可发 Last-Event-ID header,后端 best-effort 重放未确认事件,V1 不保证完整重放) |
data | JSON payload |
retry: 5000 | 第一条 message 中携带,告知客户端重连间隔(ms) |
5.2 事件清单
| Event | 触发点 | Payload 关键字段 |
|---|---|---|
task_started | 任务进入 ReAct loop | task_id, task_type, route, ts |
cognition_step | ReAct 每一步推进 | step_index, step_kind (task_routed / context_built / advisor_planned / skill_running / evidence_audited / object_drafting), summary |
tool_called | 内部 tool 被调用 | tool_name, args_summary, started_at, duration_ms(tool 执行完成后发) |
advisor_invoked | Advisor 视角进入 | advisor_role, provider_used, coordination_mode (sequential / parallel-then-merge / challenge-after-draft,v1-agent-orchestration-design.md §10A) |
boundary_event | BoundaryGuard 阻挡或重写 | event_kind (forbidden_field_stripped / action_adjacent_redirected / sensitive_input_rejected), details |
snapshot_completed | Snapshot 写盘完成 | snapshot_id, ui_state, has_thread_proposal, has_checkpoint_ref |
checkpoint_completed | Checkpoint 写盘完成 | checkpoint_id, linked_snapshot_ref |
thread_refreshed | Thread refresh 写盘完成 | thread_id, refresh_id, change_summary(new_facts_count / changed_inferences_count / risk_changes_count / evidence_changes_count) |
evidence_updated | 证据收集阶段产出新 evidence_items(增量更新 UI) | evidence_id, claim_ref, evidence_status |
data_quality_updated | 同上,但是 data_quality_notes | quality_id, applies_to, quality_state |
task_completed | 任务正常结束 | task_id, produced_object_refs, final_ui_state, total_duration_ms |
task_failed | 任务异常结束 | task_id, error: { code, message, retriable }, partial_object_refs |
degradation_notice | Agent 进入降级路径(v1-agent-orchestration-design.md §10B) | degradation_kind, affected_field, user_visible_message |
5.3 Sequence 模式(Snapshot 任务示例)
task_started
cognition_step (task_routed: snapshot)
cognition_step (context_built)
cognition_step (advisor_planned: 3 advisors)
advisor_invoked (Asset Research, gpt-5.5, sequential)
tool_called (crypto_data.get_market_overview)
evidence_updated (claim_ref: ...#main_thesis)
advisor_invoked (Market/Macro, kimi-k2.6, parallel-then-merge)
advisor_invoked (Counter-Thesis, kimi-k2.6, challenge-after-draft)
data_quality_updated
boundary_event (action_adjacent_redirected) | optional
cognition_step (object_drafting)
snapshot_completed
[checkpoint_completed] ← 仅 action_adjacent 时
task_completed
5.4 SSE 边界与降级
| 场景 | 行为 |
|---|---|
| 客户端断线 | 服务端继续完成任务并写盘;用户重新进入页面后用 GET /api/tasks/{task_id} + GET /api/snapshots/{snapshot_id} 拉取最终结果 |
| 服务端崩溃 | 当前任务标记 failed;客户端再次连接 /stream 收到 task_failed |
| Idle timeout | 60 秒无事件后服务端发 event: heartbeat\ndata: {}\n\n;任务平均时长 < 60 秒,超时不应发生 |
| 流被 nginx buffered | nginx 配置 proxy_buffering off; X-Accel-Buffering no; |
6. Object Reference Conventions
6.1 引用语法
<object_type>:<object_id>[#<jsonpath>]
| 例 | 含义 |
|---|---|
snapshot:snap_2026-05-18T10-23-04Z_a3f5b2 | 整个 snapshot |
snapshot:snap_xxx#supporting_reasons[2] | snapshot 的第 3 条 supporting reason |
thread:thr_xxx#current_thesis | thread 当前主判断 |
checkpoint:chk_xxx | 整个 checkpoint |
evidence:evi_xxx | 单条 evidence |
feedback:fb_xxx | 单条 feedback |
user:alice | 用户 |
6.2 跨对象引用清单
| 出现位置 | 字段 | 引用类型 |
|---|---|---|
| Snapshot | pre_execution_checkpoint_refs[] | checkpoint:... |
| Snapshot | evidence_items[].claim_ref | snapshot:<self>#supporting_reasons[N] 或 #counter_thesis[N] 等 |
| Thread | linked_snapshots[] | snapshot:... |
| Thread | pre_execution_checkpoints[] | checkpoint:... |
| Thread | profile_consent_refs[] | consent:... |
| Checkpoint | related_snapshot_ref / related_thread_ref | snapshot:... / thread:... |
| Feedback | related_object_ref | 任意 |
| CommercialSignalEvent | related_object_refs.{snapshot_id, thread_id, checkpoint_id} | 同 |
6.3 解析规则
后端 CognitionStore.resolve(ref) 实现:
- split on
:→(object_type, rest); - split rest on
#→(object_id, jsonpath); - lookup file under
data/cognition/<user_id>/<object_type plural>/<object_id>.json; - 若有
jsonpath,应用 jsonpath 解析; - 找不到返回
null(不 raise)。
6.4 跨用户引用
V1 禁止跨用户引用。引用格式不含 user_id;若 resolve 时 lookup 不到(因为引用来自另一用户的对象),返回 null。
7. Error Schema
7.1 统一错误对象
所有非 2xx / 非 SSE 错误都返回:
{
"error": {
"code": string, # snake_case 稳定标识符
"message": string, # 人类可读
"details": object | null, # 可选;具体字段错误等
"trace_id": string | null, # server-side trace id(traces/ 中可查)
"retriable": bool,
"user_visible": bool # 是否可直接给用户看(false 时前端用兜底文案)
}
}
7.2 错误码清单
| Code | HTTP | retriable | user_visible | 触发 |
|---|---|---|---|---|
unauthenticated | 401 | false | true | 无 token / token 无效 |
unauthorized | 403 | false | true | token 无该 user / 该 endpoint 权限 |
forbidden_admin_only | 403 | false | false | 非 admin 访问 /api/_admin/* |
not_found | 404 | false | true | 对象不存在 |
validation_error | 422 | false | false | Pydantic 字段校验失败;details 含字段路径 |
state_conflict | 409 | false | true | thread 已 closed 等 |
idempotency_key_in_use | 409 | false | false | 同 key 任务已在跑 |
boundary_block | 403 | false | true | BoundaryGuard 拒绝(含 forbidden execution field) |
sensitive_input_rejected | 451 | false | true | Sensitive Input Classifier 检测到 credential / private key |
provider_failure | 502 | true | false | 主备 provider 全部失败(v1-model-and-provider-policy.md §6) |
quota_exceeded | 429 | true after cooldown | true | per-user / daily / monthly budget hit |
kill_switch_active | 503 | true after release | true | data/.kill_switch 存在 |
degraded_no_advisor_output | 502 | true | true | 全部 advisor 都未产出可用结构化输出 |
internal_error | 500 | true | false | 未捕获异常 |
7.3 SSE 错误传播
SSE 中错误通过 task_failed 事件传,不断流;payload 中含同样的 error 对象。
7.4 Sensitive input rejection 细则
按 v1-product-object-and-schema-design.md §9 credential 类输入:
response_451:
error:
code: "sensitive_input_rejected"
message: "FinClaw does not accept credentials, private keys, exchange keys, or wallet recovery phrases."
user_visible: true
details:
classification: "credential_or_permission"
input_segment_index: integer # 用户输入中疑似命中的段索引
action: "rejected_no_save_no_train_no_echo"
事件同时记录到 data/sensitive/<user_id>/snh_xxx.json(仅 metadata,v1-data-and-persistence-design.md §9)。
8. Rate Limiting
8.1 V1 限流策略(per-user)
| 维度 | Limit | 滑窗 |
|---|---|---|
POST /api/tasks | 30 / hour | 1 小时滑窗 |
POST /api/threads/{id}/refresh | 20 / hour | 同 |
POST /api/feedback | 60 / hour | 同 |
POST /api/events/cs | 600 / 5 min | 5 分钟滑窗(前端 buffer flush 友好) |
| 其余 GET | 1200 / hour | 1 小时滑窗(read-heavy 可放宽) |
| Admin endpoints | 不限 | — |
V1 简化实现:内存 token bucket(per user_id);服务重启后重置(trial 内可接受)。
8.2 触达 limit 的响应
HTTP/1.1 429 Too Many Requests
Retry-After: <seconds>
{
"error": {
"code": "quota_exceeded",
"message": "Rate limit exceeded for create_task; retry in 120s.",
"user_visible": true,
"retriable": true,
"details": { "limit": 30, "window_seconds": 3600, "scope": "create_task" }
}
}
8.3 Cost-based budget(与 §7 quota_exceeded 共享 code)
按 v1-model-and-provider-policy.md §7:
- per-snapshot hard cap $0.10:超出立即 BoundaryGuard 拒绝,返回
quota_exceeded+details.scope: "per_snapshot_cost"; - daily / monthly budget 触达:整个工程仓库降级为「只读模式」——所有 mutation endpoint 返回
kill_switch_active,但 reads 继续。
8.4 不实现
- per-IP 限流(V1 内测无 abuse 风险);
- 地理 / 语言 限流;
- API key tier-based 限流(V1 没有付费层)。
9. Frontend Adapter Contract
9.1 前端 client 抽象
按 v1-tech-stack-and-architecture-design.md §4:前端用 zod schema 镜像后端 Pydantic 模型;REST 调用统一通过 web/src/api/client.ts。
// pseudocode
class FinclawClient {
constructor(opts: { baseUrl: string; getToken: () => string });
health(): Promise<HealthResponse>;
createTask(req: CreateTaskRequest, opts?: { idempotencyKey?: string }): Promise<CreateTaskResponse>;
streamTask(taskId: string, handlers: SSEHandlers): EventSource;
getSnapshot(snapshotId: string): Promise<Snapshot>;
saveSnapshotAsThread(snapshotId: string, req: SaveAsThreadRequest): Promise<Thread>;
listSnapshots(opts?: ListOpts): Promise<Page<SnapshotIndex>>;
getThread(threadId: string): Promise<Thread>;
getThreadHistory(threadId: string, opts?: ListOpts): Promise<Page<RefreshChange>>;
refreshThread(threadId: string, req: RefreshRequest, opts?: { idempotencyKey?: string }): Promise<CreateTaskResponse>;
patchThread(threadId: string, patch: ThreadPatch): Promise<Thread>;
getCheckpoint(checkpointId: string): Promise<Checkpoint>;
getEvidence(evidenceId: string): Promise<Evidence>;
getProfileConsent(userId: string): Promise<ProfileConsent>;
upsertProfileConsent(userId: string, req: ProfileConsentRequest): Promise<ProfileConsent>;
revokeConsent(userId: string, consentId: string): Promise<void>;
postFeedback(req: FeedbackRequest): Promise<FeedbackResponse>;
reportEvent(event: CommercialSignalEvent): Promise<void>; // fire-and-forget
reportEvents(events: CommercialSignalEvent[]): Promise<void>;
}
9.2 SSE 订阅契约
type SSEHandlers = {
onTaskStarted?(e: TaskStartedEvent): void;
onCognitionStep?(e: CognitionStepEvent): void;
onToolCalled?(e: ToolCalledEvent): void;
onAdvisorInvoked?(e: AdvisorInvokedEvent): void;
onBoundaryEvent?(e: BoundaryEvent): void;
onSnapshotCompleted?(e: SnapshotCompletedEvent): void;
onCheckpointCompleted?(e: CheckpointCompletedEvent): void;
onThreadRefreshed?(e: ThreadRefreshedEvent): void;
onEvidenceUpdated?(e: EvidenceUpdatedEvent): void;
onDataQualityUpdated?(e: DataQualityUpdatedEvent): void;
onDegradationNotice?(e: DegradationEvent): void;
onTaskCompleted?(e: TaskCompletedEvent): void;
onTaskFailed?(e: TaskFailedEvent): void;
onError?(err: Error): void; // 网络层错误
};
9.3 错误处理协定
前端遇到错误对象时按 error.user_visible 决定:
| user_visible | 行动 |
|---|---|
| true | 直接展示 error.message |
| false | 展示通用兜底文案("出错了,请稍后再试"),把 error.code + trace_id 写入 console + report_value_articulation_submitted 不必触发 |
error.retriable=true 且 code in {provider_failure, kill_switch_active, internal_error} 时,UI 显示「再试一次」按钮(POST /api/tasks 重发,复用同一 Idempotency-Key)。
9.4 与 UI states 对齐
| UI state | 来源 |
|---|---|
Snapshot complete / low_confidence / source_limited / action_adjacent / thread_candidate / needs_clarification | snapshot.ui_state 字段 |
Thread active / refresh_due / refreshed / paused / closed / merged | thread.status 字段 |
| Refresh diff "no material change" | change_summary 各 count == 0(v1-ui-ux-interaction-design.md §6A) |
| Sensitive rejected | HTTP 451 → 显示 v1-ui-ux-interaction-design.md §8 短文案 |
| Trial halted | HTTP 503 + kill_switch_active → v1-ui-ux-interaction-design.md §10 review needed 通用文案 |
9.5 商业信号事件上报
前端在以下时机调用 reportEvent / reportEvents:
session_started、task_submitted等行为事件(v1-commercial-signal-instrumentation-design.md §4);- 上报失败(401 / 451 / 5xx)静默 retry 1 次;仍失败丢弃(不阻塞主流程);
- 上报前不主动检查 ProfileConsent(前端不持有 consent state 副本,由后端门控)。
9.6 Frontend lint 守门
前端 CI 必须包含一条 lint 规则(自定义 ESLint / 文本规则):
- 禁止在任何源文件中出现以下字符串(避免 UI 误渲染交易控件):
order_side,order_type,quantity,leverage,wallet_address,private_key,seed_phrase,auto_execute,BuySellButton,LongShortToggle,LeverageSlider,WalletConnect;
- 例外白名单:
api/types.ts中作为forbidden_execution_fields字段值的字符串字面量(受 Pydantic schema 约束,不渲染为控件)。
10. Acceptance
本文满足 V1 工程化 B-3 任务的接收条件:
| 项 | 状态 |
|---|---|
| 全部 REST endpoint 列出 + request/response schema(§4) | 是 |
| SSE 事件清单 + payload + sequence 示例(§5) | 是 |
| Bearer token 鉴权 + 用户隔离 + 后续 OAuth 升级路径预留(§3) | 是 |
| 统一错误对象 + 错误码清单(§7) | 是 |
| Per-user rate limit 简化策略(§8) | 是 |
| Object reference convention 与 v1-product-object-and-schema-design.md §13 / v1-data-and-persistence-design.md §4.4 对齐 | 是 |
| Frontend adapter contract(§9) | 是 |
| Checkpoint endpoint 显式禁字段约束(§4.2.12) | 是 |
| Sensitive rejection / kill switch 在 API 层有明确 code(§7.2) | 是 |
| 商业信号事件 endpoint + consent 门控(§4.2.16) | 是 |
11. Open Items
- O-1:Idempotency-Key 的 5 分钟窗口实现细节(in-memory or 文件持久化) — 待 W-10 工程实现时定;
- O-2:SSE
Last-Event-ID重放策略 V1 是否实现,还是直接告知客户端 fallbackGET /api/tasks/{id}— 倾向后者(V1 简化),待与 frontend sub-packet owner 对齐; - O-3:admin token 是否需要单独的
Authorization: Admin <token>双头鉴权 — V1 用scope: admin字段足够;待 trial 启动前最终确认; - O-4:商业信号事件批量上报的 batch size 上限(建议 ≤ 50) — 与 v1-commercial-signal-instrumentation-design.md 后续 sub-packet 联调;
- O-5:
/api/openapi.json在 trial-prod 是否完全关闭还是仅对 admin 暴露 — 待 trial 启动前与项目发起人对齐。