Merge branch 'master' into fix/typecheck-test
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m4s
Project CI / Backend tests (pull_request) Failing after 4m30s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m43s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m23s
Project CI / Frontend tests (pull_request) Successful in 3m21s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m17s
Project CI / Repository checks (pull_request) Failing after 4m28s
Project CI / Native shell tests (pull_request) Successful in 7m43s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m4s
Project CI / Backend tests (pull_request) Failing after 4m30s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m43s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m23s
Project CI / Frontend tests (pull_request) Successful in 3m21s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m17s
Project CI / Repository checks (pull_request) Failing after 4m28s
Project CI / Native shell tests (pull_request) Successful in 7m43s
This commit is contained in:
@@ -128,6 +128,8 @@
|
||||
- [预览画布缩放滑杆](./【交互设计】预览画布缩放滑杆-2026-09-05.md)
|
||||
- [图片画布撤销、恢复范围与操作提示方案](./【图片画布】撤销范围与操作提示方案-2026-07-17.md)
|
||||
- [微信虚拟支付接入](./【技术方案】微信虚拟支付接入-2026-05-26.md)
|
||||
- [外部产品支付服务接入](./【技术方案】外部产品支付服务接入-2026-10-03.md)
|
||||
- [支付服务接入与收银台使用说明](./【使用说明】支付服务接入与收银台使用说明-2026-10-03.md)
|
||||
- [SSE 客户端传输层收口约定](./technical/【前端架构】SSE客户端传输层收口约定-2026-06-03.md)
|
||||
- [全站客服悬浮入口接入约定](./technical/【前端架构】全站客服悬浮入口接入约定-2026-06-23.md)
|
||||
- [图片画布素材导出方案](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
{
|
||||
"name": "Agent Integration",
|
||||
"description": "远程 MCP、OpenAPI 和完整 Skill 包发现"
|
||||
},
|
||||
{
|
||||
"name": "Payment",
|
||||
"description": "平台统一收款与 Web 收银台"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
@@ -1614,6 +1618,52 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/external/v1/payment/orders": {
|
||||
"post": {
|
||||
"tags": ["Payment"],
|
||||
"operationId": "createExternalPaymentOrder",
|
||||
"summary": "创建支付订单并返回收银台地址",
|
||||
"description": "使用支付应用 API Key 创建平台统一收款订单。支付金额按整数分传入;支付成功必须以平台通知或服务端查单为准。",
|
||||
"security": [{"PaymentApiKey": []}],
|
||||
"parameters": [{"$ref": "#/components/parameters/IdempotencyKey"}],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/PaymentOrderCreateRequest"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "支付订单与收银台信息",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentOrderCreateResponse"}}}
|
||||
},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"502": {"$ref": "#/components/responses/UpstreamError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/external/v1/payment/orders/{orderId}": {
|
||||
"get": {
|
||||
"tags": ["Payment"],
|
||||
"operationId": "getExternalPaymentOrder",
|
||||
"summary": "查询支付订单",
|
||||
"security": [{"PaymentApiKey": []}],
|
||||
"parameters": [{"name": "orderId", "in": "path", "required": true, "schema": {"type": "string"}}],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "支付订单",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentOrderCreateResponse"}}}
|
||||
},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -1622,6 +1672,11 @@
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "tnr_sk"
|
||||
},
|
||||
"PaymentApiKey": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "tnr_pay"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
@@ -1747,6 +1802,47 @@
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"PaymentOrderCreateRequest": {
|
||||
"type": "object",
|
||||
"required": ["merchantOrderId", "title", "amountCents"],
|
||||
"properties": {
|
||||
"merchantOrderId": {"type": "string", "minLength": 1, "maxLength": 96},
|
||||
"title": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"items": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
|
||||
"amountCents": {"type": "integer", "minimum": 1},
|
||||
"currency": {"type": "string", "default": "CNY"},
|
||||
"provider": {"type": "string", "enum": ["wechat_native"], "default": "wechat_native"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PaymentOrder": {
|
||||
"type": "object",
|
||||
"required": ["orderId", "appId", "merchantOrderId", "title", "items", "amountCents", "currency", "provider", "checkoutToken", "status", "checkoutUrl", "createdAt", "expiresAt", "updatedAt"],
|
||||
"properties": {
|
||||
"orderId": {"type": "string"},
|
||||
"appId": {"type": "string"},
|
||||
"merchantOrderId": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"items": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
|
||||
"amountCents": {"type": "integer", "minimum": 1},
|
||||
"currency": {"type": "string"},
|
||||
"provider": {"type": "string", "enum": ["wechat_native"]},
|
||||
"providerTradeNo": {"type": ["string", "null"]},
|
||||
"checkoutToken": {"type": "string"},
|
||||
"status": {"type": "string", "enum": ["pending", "paying", "paid", "expired", "closed", "refunded"]},
|
||||
"checkoutUrl": {"type": "string"},
|
||||
"providerQrCode": {"type": ["string", "null"]},
|
||||
"createdAt": {"type": "string", "format": "date-time"},
|
||||
"expiresAt": {"type": "string", "format": "date-time"},
|
||||
"paidAt": {"type": ["string", "null"], "format": "date-time"},
|
||||
"updatedAt": {"type": "string", "format": "date-time"}
|
||||
}
|
||||
},
|
||||
"PaymentOrderCreateResponse": {
|
||||
"type": "object",
|
||||
"required": ["order"],
|
||||
"properties": {"order": {"$ref": "#/components/schemas/PaymentOrder"}}
|
||||
},
|
||||
"ExternalEditorProjectCreateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
|
||||
- 位置与层级:画布左下角(`left: 14px; bottom: 14px; z-index: 40`)。右下角是既有的缩放 / 撤销 Dock(`right: 14px; bottom: 14px`),左下角是画布上唯一两者都不占的稳定空位。工具栏是管理区 `.game-resource-book-manager` 的**直接子节点**、与画本场景并列,不进带 `scale()` 的场景层;二级菜单与「入口不可用原因」都贴着工具栏上沿弹出,不做内嵌内容。
|
||||
- 外壳用共享 chrome(`packages/image-canvas-react` 的 `CanvasToolbar / CanvasToolbarGroup / CanvasChromeButton`),样式落在 AGC 的 `resourceCanvasChrome.css`;共享包只承接通用表现,不含业务规则。
|
||||
- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
|
||||
- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`,并带上当前栏目 `targetCategory`(上传的 manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都派生成 `unclassified`;不带入口栏目素材就落进「待归类」、在上传它的那一栏里看不见,Issue 359)。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
|
||||
- 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(位置与开合形态照抄网页端美术画布的任务侧栏的右上角锚点,颜色与外形仍走 AGC 平台 token;2026-09-21 由「左侧贴边 + 工具条入口」改为「右上角锚点 + 常驻开关」):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除)、折叠即整块让出画布、只留那一枚右上角开关(工具条上不再有重复入口),开合只走画布右上角那一枚「生成任务 · N」开关(两个页签下都在;**展开后开关让位、只留面板**,收起走面板头部 × 或点画布外部);锚点是画布那一格网格里的条目(不是写死 `top` 的绝对定位),工具条换行变高也不会压上去;每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布右上角锚点里(照抄美术画布那一处)、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。锚点按工作面分档:资源栏目画布与 UI 编辑器用画布顶边那一档,运行表现层下移让开右上角的版本入口;锚点是**画布那一格网格里的条目**(不是写死 `top` 的绝对定位),所以工具条换行变高也不会压上去。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。
|
||||
- 面板形态:独立浮层(`ThemedModal`),**不在当前面板下面追加内容**;面板内不写功能说明或规则解释文案。**点「生成」即同步关闭面板**(不等 IPC、不等排队、不等生成),面板里**不出现**「排队中。」「正在生成。」「提交中…」这类阶段文案——阶段文案的唯一去处是任务侧栏与工具栏提示条。**只有「点击瞬间就失败」**(校验不过、权限拒绝、start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消请求(请求挂在任务与账本上,不挂在面板生命周期上)。
|
||||
- 参数口径:比例 / 尺寸选项来自网页端美术画布的纯模型(`src/components/image-editor/ImageCanvasGenerationModel.ts`),并按本地 IPC 白名单收窄(本地通道明确拒绝 `4:3`);默认档 `1:1 · 1K`,生成 UI 设计图沿用网页端 UI 设计面板的默认 `16:9 · 1K`。本地 IPC 没有 `model` 入参,因此面板**不渲染模型选择器**(渲染一个改不了请求的控件就是假控件)。
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
## 验证命令
|
||||
|
||||
1. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml direct_thread -- --nocapture`
|
||||
2. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`(生成 `src/features/project-workspace/generated/`),随后用 `prettier --write` 格式化生成目录,避免未格式化的 ts-rs 输出混进提交
|
||||
2. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`(生成 ts-rs 绑定);生成物保持原始输出直接提交,不要再跑 prettier(见 `decision-log.md` 2026-10-03「生成绑定不再经 prettier」)
|
||||
3. `npm run test -- apps/ai-game-creator-shell/tests/directThreadChat.test.ts apps/ai-game-creator-shell/tests/directTurnPresentation.test.ts apps/ai-game-creator-shell/tests/directHistoryPaging.test.ts apps/ai-game-creator-shell/tests/directHistoryAnchorGate.test.ts`
|
||||
4. `npm run test -- apps/ai-game-creator-shell/tests/appSurface.test.ts`(`project-development.suite.ts` 由该入口注册,不能作为独立测试入口;同时验证现役 Design Agent 的会话恢复与审批界面。)
|
||||
5. TypeScript 类型检查与 ESLint(范围同前次 DirectProject 迁移)。
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Web 预检浏览器失败恢复实现计划
|
||||
|
||||
- Version: `1`
|
||||
- Status: `active`
|
||||
- Date: `2026-10-02`
|
||||
- Milestone: [`Web 预检浏览器失败恢复`](./【里程碑】Web预检浏览器失败恢复-2026-10-02.md)
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 将浏览器启动失败建模为带阶段、原因、子进程退出确认、清理确认的内部结果;Windows root spawn 后立即绑定 Job,并把绑定瞬间已经出现的子树逐 PID 纳入同一 Job,失败收束必须复用本轮 `BrowserProcessGuard`,并在成功收束时确认 Windows Job 为空。
|
||||
2. 在 Web 预检使用的一次浏览器启动入口加入一次有界恢复:仅 WS/CDP 瞬态失败且首次清理确认后创建新的临时目录/Profile 重试;其余失败直接失败关闭。
|
||||
3. 保留/加强 owner 与 sweep 的归属门禁,使用 Windows argv 解析核对完整 Profile 参数;无 owner、身份未知、PID 退出或复用均不得按猜测杀进程。
|
||||
4. 将启动失败和清理失败的安全诊断保留到预检 blocked 报告和 Tauri 错误中;稳定错误码独立于诊断文本,前端继续区分宿主阻塞与 IPC 故障。
|
||||
5. 补充 Rust 纯策略测试、清理边界测试和现有真实 Edge/Chromium ignored smoke;补充首页错误展示的定向测试。
|
||||
|
||||
## 验证命令
|
||||
|
||||
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell browser::`
|
||||
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell environment_check::web_creation::tests`
|
||||
- `npx vitest run apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx`
|
||||
- `npm run typecheck --workspace apps/ai-game-creator-shell`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
|
||||
可选真实环境证据:安装 Windows Edge 时运行现有 `real_browser_health_checks_can_run_concurrently` 及新增恢复 smoke;无浏览器时保持 ignored,不把缺失环境写成通过。
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
- Windows 进程命令行读取失败按不匹配处理,不执行杀进程;这可能留下临时目录,但保证不误杀。
|
||||
- 首次失败进程树收束未确认时不自动重试,避免第二个 Edge 与残留树并存;错误返回安全诊断。
|
||||
- 回滚点为浏览器启动恢复入口和 owner/sweep 归属校验,不触及 Web 预检的 Node/npm 或项目写入流程。
|
||||
@@ -0,0 +1,52 @@
|
||||
# 【实施计划】支付应用与订单收银台
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】支付应用与订单收银台-2026-10-03.md` |
|
||||
| Status | completed |
|
||||
| Owner | Codex |
|
||||
|
||||
## 修改边界
|
||||
|
||||
允许修改:
|
||||
|
||||
- 主规范、shared-contracts、OpenAPI、支付领域 module、spacetime-module / migration / bindings、spacetime-client facade、api-server payment module、platform-wechat provider。
|
||||
- `src` 的 profile 支付入口、收银台页面、支付客户端和页面样式。
|
||||
- `apps/admin-web` 的支付 API 类型、路由、页面和后台样式。
|
||||
- 与本里程碑直接相关的测试和文档索引。
|
||||
|
||||
明确不修改:
|
||||
|
||||
- 现有个人钱包充值、微信虚拟支付和退款状态机的行为。
|
||||
- 生产商户密钥、环境文件、个人配置和构建产物。
|
||||
- 支付宝 provider、分账、提现、手续费和自动结算。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 从现有充值订单、微信 Native provider、外部 API Key、profile 路由和后台列表组件提取可复用边界,确认没有重复的公开订单模型。
|
||||
2. 冻结支付 DTO、状态枚举、错误码、幂等语义和 OpenAPI,然后补契约测试。
|
||||
3. 增加支付领域表、migration、procedure / facade 和 schema 绑定,先完成订单创建、查询、关闭与幂等。
|
||||
4. 在 `platform-wechat` 增加支付服务需要的 Native 请求 / 响应和通知确认复用,保持商户配置只在服务端。
|
||||
5. 在 `api-server` 接入外部 API、收银台 read model、微信通知、查单和外部回调 outbox;所有到账入口复用统一确认事务。
|
||||
6. 增加个人中心支付应用 / Key / 订单页面和公共收银台,按截图参考实现桌面双栏与移动端纵向布局。
|
||||
7. 增加后台支付概览、应用、订单、通知和回调投递页面,复用 `packages/shared` 后台公共组件。
|
||||
8. 运行定向测试、schema / OpenAPI / 编码检查,启动 api-server 做 healthz 和 mock provider smoke;条件具备时执行真实沙箱二维码 smoke。
|
||||
|
||||
## 验证命令
|
||||
|
||||
1. `npm run check:encoding`
|
||||
2. `git diff --check`
|
||||
3. `npm run check:spacetime-schema`
|
||||
4. `npm run typecheck`
|
||||
5. `npm run admin-web:typecheck`
|
||||
6. 支付领域和 api-server 定向 `cargo test`
|
||||
7. 外部 API、profile、收银台和 admin 页面定向 Vitest
|
||||
8. `npm run dev:api-server` 后检查 `/healthz`
|
||||
9. mock provider + Playwright 收银台 smoke;真实商户沙箱可用时再补动态二维码验证
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- 商户 API 证书或通知地址不可用时,保留 provider mock 证据,不把 mock 结果标成真实支付通过。
|
||||
- schema 变更失败时停止在绑定生成前,保留主规范和计划,不能通过删除数据绕过迁移。
|
||||
- 订单确认事务或回调 outbox 未完成时,关闭新支付渠道开关,不影响现有充值渠道。
|
||||
- 发现需要余额、提现、分账或支付宝真实接入时,先更新主规范和后续里程碑,再扩展实现。
|
||||
@@ -0,0 +1,41 @@
|
||||
# Web 预检浏览器失败恢复
|
||||
|
||||
- Version: `1`
|
||||
- Status: `active`
|
||||
- Date: `2026-10-02`
|
||||
- Parent Spec: [`AI游戏创作智能体 App 实施计划`](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#自动预检与可信脚手架)
|
||||
|
||||
## 目标
|
||||
|
||||
修复 Issue #584:Web 游戏环境预检在受控 Edge/Chrome 的 WS 握手或 CDP 连接失败时,能够只针对本轮 AGC 浏览器实例完成清理、使用新的隔离 Profile 重试一次,并在仍失败时保留安全、可行动的诊断信息。
|
||||
|
||||
## 边界
|
||||
|
||||
- 只改 AGC Rust 浏览器启动/清理、Web 预检错误投影及其定向测试、对应现行专题文档。
|
||||
- 清理对象必须同时满足 AGC 临时 Profile、进程启动身份、可信浏览器可执行文件和 Windows 进程树归属;无法确认时 fail-closed。
|
||||
- 不执行全量 `taskkill /IM msedge.exe`,不影响用户 Edge、其它 AGC 实例、其它项目或 worktree。
|
||||
- 不改变 Web 预检的失败关闭、Node/npm 检查、桌面/移动双视口检查和首次生成顺序。
|
||||
- 不新增网络、自动下载、跳过浏览器验证或伪造 ready 的旁路。
|
||||
|
||||
## 行为合同
|
||||
|
||||
1. WS 握手失败、WS 超时、CDP 连接失败或 CDP 连接超时属于可恢复的浏览器启动瞬态失败。
|
||||
2. 第一次失败后,宿主必须先确认本轮进程树已退出并清理本轮 Profile / owner 记录 / 临时目录;清理未确认时不得启动第二次浏览器。
|
||||
3. 清理确认后,第二次启动必须创建新的隔离临时目录和 Profile;第二次成功后继续现有 desktop/mobile 预检。
|
||||
4. 连续失败或清理被阻断时,结果保持 blocked,并带有阶段(WS 握手或 CDP 连接)、子进程是否退出、清理是否确认、是否执行恢复重试等安全诊断。
|
||||
5. owner.json 缺失/写入失败、目录过新、进程已退出、PID 被复用、身份未知或归属不明时只能按保守路径处理:不杀不明进程;可安全删除的临时目录才删除。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 定向测试构造 `browser-ws-failed` 后证明只在清理确认时重试,且重试使用新的 Profile;第二次成功返回成功。
|
||||
- 定向测试覆盖 WS/CDP 阶段、连续失败、清理未确认、owner.json 缺失/无效、刚创建目录、进程退出、PID 复用和非 AGC 进程跳过。
|
||||
- Windows 真实 Edge 安装版 smoke 保留在现有真实浏览器测试入口中;无 Edge 的环境明确跳过而不是伪造通过。
|
||||
- 现有首页预检、正常 Edge 使用、首次生成失败关闭和前端 IPC/宿主错误区分回归通过。
|
||||
|
||||
## 依赖
|
||||
|
||||
- `apps/ai-game-creator-shell/src-tauri/src/browser/process.rs`
|
||||
- `apps/ai-game-creator-shell/src-tauri/src/browser/sweep.rs`
|
||||
- `apps/ai-game-creator-shell/src-tauri/src/environment_check/web_creation.rs`
|
||||
- `apps/ai-game-creator-shell/src/features/app-shell/homeWebPreflight.ts`
|
||||
- 对应 Rust 与 Vitest 测试。
|
||||
@@ -0,0 +1,59 @@
|
||||
# 【里程碑】支付应用与订单收银台
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | completed |
|
||||
| Date | 2026-10-03 |
|
||||
| Parent Spec | `docs/【技术方案】外部产品支付服务接入-2026-10-03.md` |
|
||||
|
||||
## 目标
|
||||
|
||||
建立首个可运行的支付服务闭环:账号创建支付应用并管理 API Key,客户端或外部产品创建订单,用户进入 Web 收银台,平台通过微信 Native provider 生成动态二维码,通知 / 查单确认订单,之后通过签名回调通知接入方,后台可查询应用和订单。
|
||||
|
||||
## 范围
|
||||
|
||||
- 支付应用、API Key 元数据和账号归属。
|
||||
- 外部支付订单、商品快照、支付尝试和状态机。
|
||||
- 微信 Native 动态二维码 provider 适配;支付宝只保留渠道扩展结构,不纳入本里程碑的可用渠道。
|
||||
- 公共收银台和移动端纵向布局。
|
||||
- 支付平台通知、主动查单、确认幂等和接入方回调 outbox。
|
||||
- 个人中心支付服务入口、应用配置页和订单列表。
|
||||
- 后台支付总览、应用列表、订单列表、通知 / 回调失败列表。
|
||||
- `/api/external/v1/payment/*` OpenAPI、共享 DTO、契约测试和支付领域 schema。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 支付宝真实下单、退款、分账和沙箱联调。
|
||||
- 外部产品余额、手续费、提现、自动结算和自动分账。
|
||||
- 用户钱包充值商品的迁移或现有微信虚拟支付协议改造。
|
||||
- 自动履约发货、数字权益回收和支付争议处理。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
- 平台微信 Native 商户号、API 证书 / 密钥和正式或沙箱通知地址可由部署环境注入。
|
||||
- 当前微信支付 Native provider、充值订单状态与验签测试可复用。
|
||||
- 外部回调域名必须通过应用配置的 HTTPS 和白名单校验。
|
||||
- SpacetimeDB schema、migration、生成绑定和 API OpenAPI 可以在同一里程碑同步更新。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [x] 账号能创建、停用支付应用,API Key 仅在创建响应中展示一次,撤销后立即拒绝请求。
|
||||
- [x] 外部 API 使用 `Idempotency-Key` 创建订单,重放返回原订单,字段冲突返回业务错误。
|
||||
- [x] 订单金额以整数分保存,商品和金额快照创建后不可被应用配置改写。
|
||||
- [x] 收银台使用不可猜测 token,桌面端双栏、移动端纵向显示商品、金额、二维码、过期和状态。
|
||||
- [x] 微信 Native 下单返回的二维码对应当前本地平台订单号和金额。
|
||||
- [x] 支付平台通知 / 查单确认前,客户端轮询或伪造回调不能把订单改为 `paid`。
|
||||
- [x] 重复、乱序、错误金额、错误签名、错误应用归属的通知不会重复确认或改变订单。
|
||||
- [x] 接入方回调异步投递、签名、重试和失败状态可查询。
|
||||
- [x] 个人中心和后台均能按归属和权限查看应用、订单及失败记录。
|
||||
- [x] `shared-contracts`、OpenAPI、schema 绑定和契约测试同步通过。
|
||||
|
||||
## 证据要求
|
||||
|
||||
2026-10-03 验收复核:支付专用 schema、typed facade、API、应用管理、provider-attempt claim、通知确认、签名回调 outbox、后台投递状态、profile/payment 路由和前端 build 已完成。项目 dev stack 发布后 `GET /healthz` 返回 HTTP 200,live schema 包含 `payment_order` / `payment_webhook_delivery`;真实商户沙箱二维码和真实微信通知因环境未注入商户凭据未验证,按“有可用沙箱时补充”处理。
|
||||
|
||||
- 自动化:Rust 领域 / API / provider 测试、前端页面与契约测试、schema 检查、OpenAPI 检查、编码和 diff 检查。
|
||||
- 运行时:项目 dev stack 发布当前 schema 后,api-server 使用正确端口和 bootstrap 配置,`GET /healthz` 返回 HTTP 200;live schema 已包含 `payment_order` 与 `payment_webhook_delivery`;微信 provider mock 由现有 provider 测试覆盖。
|
||||
- 未验证:真实商户沙箱动态二维码和真实微信通知,当前环境没有可安全使用的商户凭据;代码保留 provider 配置失败和支付状态失败关闭语义。
|
||||
- 边界:权限、幂等、重放、金额不一致、回调失败、过期、未知通知和敏感日志检查。
|
||||
@@ -1,5 +1,21 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-10-03 AGC 栏目画布上传素材按入口栏目登记(Issue 359)
|
||||
|
||||
- 背景:AGC 客户端在资源栏目子画布(「UI 交互 / 角色与对象 / 场景与环境 / 音频」)左下角工具栏点「上传」后,提示条给出「已上传 1 个素材」,但当前栏目计数不变(仍「0 项」)、素材出现在「待归类」,用户看到的是"上传成功了但它从这一页消失了"。原因是上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套,而 `upload_local_asset` 原先不接受入口栏目。
|
||||
- 决策:`upload_local_asset` 增加可选 `targetCategory`,Rust 走既有的 `register_local_asset_entry_with_category`(与生成入口 `start_local_project_asset_generation` 的 `targetCategory` **同一口径**:GUI 完成登记以入口栏目为准);前端 `uploadProjectAssetFilesAndReadSnapshot` 透传该字段,栏目画布工具栏上传取工具栏自己的栏目(`resourceCanvasBottomToolbarCategory`)。取值只接受共享分类枚举,非法值由原生失败关闭,前端不做伪分类。
|
||||
- 边界:不传 `targetCategory` 时保持既有 kind 派生行为——资源面板(`ResourceCanvasPanelView`,跨栏目列表而不是栏目工具)、UI 编辑器图片导入(`import_ui_editor_local_files`)、聊天附件上传都不变。`upload_local_asset_at` 签名保持不变(委托到新的 `upload_local_asset_at_with_category`),既有约 25 处调用点与 Rust 单测零改动。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{assets.rs,commands/desktop.rs}`、`apps/ai-game-creator-shell/src/view/project-development/{projectResourceLiveUpdateModel.ts,index.tsx}`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、PRD §3.10、`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`(§4 载荷表)、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S11a)、`pitfalls.md`。
|
||||
- 验证:`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell assets::tests`(新增 `upload_registers_into_the_explicit_entry_category`:显式栏目 → `character`、不传 → `unclassified`、非法 `version` 失败关闭且不新增登记);`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`(196 passed / 9 skipped,含新增「uploads toolbar files into the entry column so they stay visible where they were uploaded」断言工具栏上传载荷带 `targetCategory: 'character'`);`npm run agc:typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-10-03 生成绑定不再经 prettier:ts-rs 原始输出即提交形态
|
||||
|
||||
- 背景:`scripts/check-generated-bindings.mjs` 对 AGC 的 `chat/generated` / `services/generated` 在重生成后会就地跑一遍 `npx prettier --write` 再比较。这会直接改写生成文件,还把「Rust 声明真的变了」与「prettier 版本 / 配置造成的格式漂移」混在同一条告警里——本次报出的 `ThreadRequestKind.ts` / `TurnCompletedStatus.ts`「内容变化」无法复现为语义变化(已提交内容与当前 Rust 枚举一致),prettier 归一化把格式差异也报成了「与 Rust 声明不一致」;生成物被仓库格式化工具二次改写后,重跑 `cargo test export_bindings` 也不再幂等。
|
||||
- 决策:生成绑定一律以 ts-rs 原始输出提交,不接受 prettier / eslint 等工具二次改写。`check-generated-bindings.mjs` 删掉 prettier 步骤,直接逐字节比较原始输出;`packages/shared/src/contracts/generated`、`chat/generated`、`services/generated`、`features/ui-editor/types` 四个目录统一登记进 `.prettierignore` 与 `.eslintrc.cjs` 的 `ignorePatterns`;`.prettierrc.json` 里为 `contracts/generated` 设的 `printWidth: 1000` / `singleQuote: false` 覆盖随之删除。
|
||||
- 边界:ts-rs 原始输出在多行对象 / 枚举变体行尾带空格,`.gitattributes` 对四个生成目录设 `whitespace=-trailing-space`,CI 的 `git diff --check` 不再误报;手写文件的行尾空白检查不变。生成 `.ts` 仍提交进仓库供前端消费,并标 `linguist-generated=true`。
|
||||
- 影响范围:`scripts/check-generated-bindings.mjs`、`.prettierignore`、`.prettierrc.json`、`.eslintrc.cjs`、`.gitattributes`、`apps/ai-game-creator-shell/src/{view/project-development/chat,services}/generated/**`。
|
||||
- 验证:`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`、`npm run check:generated-bindings`(shared-contracts 1 + AGC 104 = 105 个文件)、`npx prettier --check` 四个生成目录(全部跳过)、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-10-03 回合错误的处置分类层与线程条目脱敏边界
|
||||
|
||||
- 背景:`TurnError::terminal_failure -> Option<TurnFailure>` 用一个 `None` 同时表达"没有失败"和"不是失败、要继续跑(返修 / 复核控制流)",调用方看到 `None` 只会理解成前者,`dispatch.rs` 两处只能 `.expect("回合失败必可投影成失败载荷")`;同时 ThreadManager 在搬运线程条目时做字段级脱敏与限长,前端要被截断,而且与失败载荷的脱敏是两套实现。
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
|
||||
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
|
||||
|
||||
## 2026-10-03 AGC 栏目画布上传素材落「待归类」:kind 派生分类不等于入口栏目
|
||||
|
||||
- **现象**(Issue 359):在 AGC 资源栏目子画布(如「UI 交互」「角色与对象」)左下角工具栏点「上传」选图片 / 视频 / 代码类文件,提示条给出「已上传 1 个素材」,但当前栏目计数纹丝不动(仍「0 项」),素材出现在「待归类」。用户看到的是"上传成功了,可它就消失在这个页面里"。
|
||||
- **原因**:上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套;`upload_local_asset` 原先不接受入口栏目,GUI 工具栏上传只能落 kind 派生分类。
|
||||
- **处理(现行口径)**:`upload_local_asset` 增加可选 `targetCategory`,Rust 走既有的 `register_local_asset_entry_with_category`(非法值失败关闭,不回退 kind 派生);栏目画布工具栏上传时取工具栏自己的栏目(`resourceCanvasBottomToolbarCategory`)。生成入口 `start_local_project_asset_generation` 的 `targetCategory` 是同一口径——GUI 完成登记以入口栏目为准。
|
||||
- **判据/取证**:`cargo test --locked ... --bin genarrative-ai-game-creator-shell upload_` 的 `assets::tests::upload_registers_into_the_explicit_entry_category`(显式栏目 → `character`;不传 → `unclassified`;非法 `version` 失败关闭且不新增登记);appSurface「uploads toolbar files into the entry column so they stay visible where they were uploaded」断言工具栏上传载荷带 `targetCategory: 'character'`。
|
||||
- **边界**:资源面板(跨栏目列表)、UI 编辑器图片导入、聊天附件上传都**不带**入口栏目,保持 kind 派生。任何新增的「某个栏目里的上传入口」都必须显式带上当前栏目,否则又会复现本坑。
|
||||
|
||||
## 2026-10-01 用户输错一次密码被当成"客户端出问题了"引导上报
|
||||
|
||||
- **现象**:登录页密码输错(或密码长度不合规)后弹出「发现问题」,报告面板「错误事件(2)」列出 `密码长度需要在 6 到 128 位之间 — auth · 1 次` 与 `手机号或密码错误 — auth · 1 次`,默认全选,与 react-render / 5xx / agent-runtime 终态失败视觉等价。
|
||||
- **原因**:① 登录已下沉 Rust,`login_client_with_password` 等命令失败返回 `Err(String)`,Tauri 以**裸字符串**拒绝 `invoke`,前端拿不到任何类型信息;② `shouldCaptureClientError` 对非 object 值走默认 `return true`,`handleLoginSubmit` 的 catch 把预期业务拒绝报进了错误池。技术方案里"预期 4xx 登录/鉴权失败不进池"的口径早就成立,是错误通道的实现方式违背了它。
|
||||
- **处理(现行口径)**:命令错误一律按具体变体结构化(`Result<_, ClientAuthError>` + ts-rs 导出),认证命令统一经 `invokeClientAuth` 调用:结构化拒绝原样装进已有的 `ClientAuthErrorWrapper`(只有一个 `error` 字段,值是判别联合),UI 在 catch 里按具体变体分流——认得的业务 / 会话变体只给用户反馈,系统变体原样 `throw` 经 `unhandledrejection` 入池,非结构化拒绝原样抛出;删除 `shouldCaptureClientError`。详见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 与 [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](../../adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md)。
|
||||
- **判据/取证**:`npx vitest run apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx`——登录返回结构化业务变体时 `report_client_error` 不被调用;系统变体只上报一次(`source` 取全局 `unhandledrejection` handler 的显式入参;载体不再携带 `action`)。Rust 侧 `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml auth_error` 钉住变体 `type` 与 400/401/429/5xx/网络映射。
|
||||
- **形状约定**:`ClientAuthError` 顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段——无字段变体在 TS 里就是 `{ type: 'x' }`;带载荷变体是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体),细分原因枚举(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`)同样由 ts-rs 生成。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律写在 catch 子句里:`const failure = error.error; switch (failure.type)`,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as X` 再读自己的字段,`reason` 是枚举时再 `switch (payload.reason)`,两处 `default` 都用 `expectNever` 保证漏接编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物随后交给 prettier;ts-rs 只写文件、不删文件,变体从有载荷改成无字段时要手动清掉孤立的 `X.ts`(本次 `AuthResponseServerRejected.ts` 就是这样删的)。
|
||||
- **形状约定**:`ClientAuthError` 顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段——无字段变体在 TS 里就是 `{ type: 'x' }`;带载荷变体是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体),细分原因枚举(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`)同样由 ts-rs 生成。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律写在 catch 子句里:`const failure = error.error; switch (failure.type)`,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as X` 再读自己的字段,`reason` 是枚举时再 `switch (payload.reason)`,两处 `default` 都用 `expectNever` 保证漏接编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物保持 ts-rs 原始输出(不再经 prettier / eslint 二次改写,见本节「生成绑定」口径);ts-rs 只写文件、不删文件,变体从有载荷改成无字段时要手动清掉孤立的 `X.ts`(本次 `AuthResponseServerRejected.ts` 就是这样删的)。
|
||||
- **Rust 侧不得把结构化错误降级成字符串**:`refresh_session_inner` 的非权威失败直接返回 `Err(ClientAuthError)`,视图不带 `errorMessage`;一旦折成 `String`,前端就只能拿文案判断,变体信息永久丢失。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、`apps/ai-game-creator-shell/src/services/{clientAuth.ts,errorReporting.ts,platformSession.ts}`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`。
|
||||
|
||||
@@ -5795,8 +5803,8 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **处理(现行口径)**:不要把重写结果当改动提交。跑过 `cargo test` 或构建后只恢复 `apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectCodexUser*.ts` 的仓库版本,再删掉多出来的 `DirectCodexUserMessageEnvelope.ts`;不要恢复整个 `generated/` 目录,以免误删 DirectThread 的现役绑定。绑定与前端形状冲突时以**已提交的 DirectCodexUser 绑定 + 前端**为基准排查。
|
||||
- **验证**:恢复仓库版本后 `npm run ai-game-creator-shell:typecheck` exit 0(`[skill-pack] OK`);保留重写结果时同一条命令 exit 2。release 构建本身还会在 `src/features/ui-editor/types/` 落下 `BindingChange.ts` / `BindingDTO.ts` 两个无人引用的生成产物;它们不属于前端契约,发现后直接删除,不提交。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/`(ts-rs 导出源)、`apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts`、`apps/ai-game-creator-shell/scripts/build-release.mjs`(`beforeBuildCommand`)。
|
||||
- **2026-09-24 补充(纯格式 churn 的一档)**:直接运行 AGC 的测试二进制(含全量套件,里面带 `export_bindings`)同样会把 `src/view/project-development/chat/generated/DirectThread*.ts` 重写成 raw ts-rs 形态——双引号、行尾空格、`};` 变 `, };`——于是 `git diff --check` 报一片 trailing whitespace,而 `npm run check:generated-bindings`(只覆盖 `packages/shared/src/contracts/generated`)不会报警。处理:对这组文件跑一次 `npx prettier --write "apps/ai-game-creator-shell/src/view/project-development/chat/generated/*.ts"` 做归一化,再确认 `git diff --stat` 为空——为空就说明这一档只是格式 churn,没有语义变化;不要为它盲目 `git checkout` 整个目录。该目录目前没有自动漂移门禁,靠「跑完测试后归一化 + diff 检查」把关。
|
||||
- **2026-10-02 补充(过滤跑也躲不开)**:`cargo test` 的位置参数是**子串匹配**,`-- agent::thread_manager::` 这类"只跑几个模块"的过滤会连带命中 `agent::thread_manager::wire::failure::export_bindings_turnfailure`,于是照样重写 `chat/generated/` 与 `src/services/generated/`(后者是 auth 绑定)。要按模块跑又不碰生成目录,用 `cargo test --bin genarrative-ai-game-creator-shell -- agent:: --skip export_bindings`;已经被重写时,`chat/generated/` 跑一次 prettier 归一化,`src/services/generated/` 直接 `git checkout --`(那一侧没有本轮的语义改动)。
|
||||
- **2026-09-24 补充(纯格式 churn 的一档)**:直接运行 AGC 的测试二进制(含全量套件,里面带 `export_bindings`)同样会把 `src/view/project-development/chat/generated/DirectThread*.ts` 重写成 raw ts-rs 形态——双引号、行尾空格、`};` 变 `, };`——于是 `git diff --check` 报一片 trailing whitespace,而 `npm run check:generated-bindings`(只覆盖 `packages/shared/src/contracts/generated`)不会报警。处理(2026-10-03 更新,口径已变):不再跑 prettier 归一化——`chat/generated` 与 `services/generated` 的提交形态就是 ts-rs 原始输出;两个目录已进 `.prettierignore` + eslint `ignorePatterns`,`git diff --check` 由 `.gitattributes` 的 `whitespace=-trailing-space` 豁免,`npm run check:generated-bindings` 会逐字节比较原始输出。跑完 `cargo test` 后 `git diff` 为空即为幂等。
|
||||
- **2026-10-02 补充(过滤跑也躲不开)**:`cargo test` 的位置参数是**子串匹配**,`-- agent::thread_manager::` 这类"只跑几个模块"的过滤会连带命中 `agent::thread_manager::wire::failure::export_bindings_turnfailure`,于是照样重写 `chat/generated/` 与 `src/services/generated/`(后者是 auth 绑定)。要按模块跑又不碰生成目录,用 `cargo test --bin genarrative-ai-game-creator-shell -- agent:: --skip export_bindings`;已经被重写时不要再做 prettier 归一化:两个目录的提交形态就是原始输出,按需重新生成或 `git checkout --`,再用 `npm run check:generated-bindings` 确认无漂移。
|
||||
|
||||
## 2026-09-16 Node 26 下 vitest 的 jsdom 用例拿不到 window.localStorage
|
||||
|
||||
@@ -6280,7 +6288,8 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **根因 3(工具被拒)**:sidecar 用 `permissionMode: 'dontAsk'` 且没有 `allowedTools`,宿主 MCP 工具(`mcp__agc__*`)一律被直接拒绝,模型只能回"没有权限"。
|
||||
- **根因 4(界面看不到回复)**:聊天区是按 `item.completed` 事件流投影的(codex 路径在 `rawResponseItem/completed` 时下发 `ThreadItem::Message`),只把回复落进 `project.jsonl` 不会让本轮出现在界面上——用户看到"用户气泡 + 本轮结束于 … · 耗时",回复只在重进项目时从历史读出来。
|
||||
- **根因 5(验收反馈复用 assistant ID)**:同一 client turn 进入 `ReviewRequired` 后会再次调用 cc。首次回复已经占用 `direct-codex:<clientTurnId>:assistant`,第二次不同正文沿用该 ID 会被历史层正确拒绝为冲突,随后却被错误投影成 `runtime-unclassified`。真实诊断中可见「写入本项目对话历史失败:…assistant」且历史已经有该条回复。
|
||||
- **现行口径**:cc 成功出口由放行侧补写 `DirectTurnTerminal::completed()`(`finish_if_unfinished` 幂等,codex 已写过终态时是空操作);cc 每次解析成功后都把实际落盘的回复 item id 同步下发 `ThreadEvent::item_completed(ThreadItem::Message{role:"assistant"})`。首个回复沿用 `direct-codex:<clientTurnId>:assistant`,同一回合的反馈回复遇到内容冲突时追加 `:assistant:<uuid>`,相同内容仍按原 ID 幂等;落盘失败按回合失败收口。sidecar 按 `mcp__<server>` 前缀整体放行请求里声明的 MCP 服务器(权限策略在宿主侧执行)。
|
||||
- **根因 6(cc 字符串错误覆盖了上游分类)**:DirectProject 的 Claude Code 路由原本把 sidecar 返回的所有字符串都包装成 `LlmError::Transport`,因此 HTTP 429、401、408、5xx、sidecar 超时、空回执和无效 JSON 都显示成「执行通道未能建立或已断开」。
|
||||
- **现行口径**:cc 成功出口由放行侧补写 `DirectTurnTerminal::completed()`(`finish_if_unfinished` 幂等,codex 已写过终态时是空操作);cc 每次解析成功后都把实际落盘的回复 item id 同步下发 `ThreadEvent::item_completed(ThreadItem::Message{role:"assistant"})`。首个回复沿用 `direct-codex:<clientTurnId>:assistant`,同一回合的反馈回复遇到内容冲突时追加 `:assistant:<uuid>`,相同内容仍按原 ID 幂等;落盘失败按回合失败收口。Claude Code 的失败文本先投影到与 Codex 相同的 `LlmError` 分类:HTTP 状态、sidecar 超时、空回执和无效 JSON 分别复用上游、超时、空响应和反序列化语义;上游状态摘要与重试建议按状态码给出。sidecar 按 `mcp__<server>` 前缀整体放行请求里声明的 MCP 服务器(权限策略在宿主侧执行)。
|
||||
- **诊断口径**:`agent.direct_turn.host_dropped` / `agent.direct_turn.panic` 里的令牌字段必须写 `tt=`,写 `turnToken=` 会命中脱敏标记,整行变成 `<sensitive diagnostic details redacted>`,离线只剩"说不出原因"的 HostDropped。
|
||||
- **验证**:dev 栈里用 CDP 注入真实回合(`node %TEMP%\agc-cdp.mjs <expr>`):①读文件轮 `claude-parse-done chars=108`,`.agent/conversations/project.jsonl` 出现 `direct-codex:cdp-…:assistant` 条目,回复内容与 `game/index.html` 前两行(`<!doctype html>` / `<html lang="zh-CN">`)逐字一致(证明宿主工具真的执行了);②聊天视图打开时注入 `只回三个字:收到了`,DOM 断言(`document.body.innerText`)同时出现用户气泡 `11:40:05`、助手回复 `收到了` 与 `本轮结束于 11:40:16 · 耗时 10.7秒`(证明 `item.completed` 实时投影生效,不必重进项目);同一日志不再出现新的 `host_dropped`。另有 `agent::claude_code_cli::tests::direct_claude_feedback_reply_does_not_fail_on_a_reused_client_turn_id` 回归覆盖同一回合两次不同回复。`cargo test … -- claude_code_cli::tests direct_turn_failure::tests` 19 passed。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`、`apps/ai-game-creator-shell/agent-sidecar/src/index.mjs`。
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 559 KiB |
@@ -6,7 +6,7 @@
|
||||
|
||||
## 1. 一句话
|
||||
|
||||
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类与音频入口都走本地 `start_local_project_asset_generation`(提交即返回、后台生成,见 2026-09-20 音频并入后台任务账本),上传复用 `upload_local_asset`;生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
|
||||
功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类与音频入口都走本地 `start_local_project_asset_generation`(提交即返回、后台生成,见 2026-09-20 音频并入后台任务账本),上传复用 `upload_local_asset`(带当前栏目 `targetCategory`,素材登记进上传它的那一栏);生成 / 上传成功后一律走既有 manifest 刷新与资源定位。
|
||||
|
||||
## 2. 入口矩阵(事实源)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
| 生成 UI 设计图 | 同上 | `kind: 'ui-design'`,默认 `16:9 · 1K`;前置同上 |
|
||||
| 生成背景音乐 | `start_local_project_asset_generation` | `kind: 'background-music'`、`idempotencyKey`;任务 id 即该次生成的 operation id |
|
||||
| 生成音效 | `start_local_project_asset_generation` | `kind: 'sound-effect'`、其余同上 |
|
||||
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes }` |
|
||||
| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes, targetCategory }`;`targetCategory` = 当前栏目(Issue 359) |
|
||||
|
||||
`start_local_project_asset_generation` 的完整参数是 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath, idempotencyKey }`(Rust `src-tauri/src/asset_generation_tasks.rs`);图片类入口沿用 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }` 逐字不变,音频入口只带 `{ projectPath, projectId, taskId, kind, prompt, assetName, idempotencyKey }`(`idempotencyKey` = 该次生成的幂等键,任务 id 即 operation id;原生命令内部仍复用既有音频无源生成链路,不新增平台路由与请求体口径),**提交即返回**一条任务记录(`taskId / status / phaseDetail / assetId / error` 等);生成在 `tauri::async_runtime::spawn` 出来的后台任务里跑,账本落在项目内 `.agent/runtime/asset-generation-tasks/tasks.json`,进度由 `list_local_project_asset_generations` 读回。截图面板不再「等生成结束」,所以**点「生成」即同步关闭面板**(不等 IPC),面板内不出现阶段文案;只有「点击瞬间就失败」才带草稿重开(关闭 ≠ 取消)。
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@
|
||||
|
||||
- `project.jsonl` 与项目主对话共用信封类型,不要为了"可见但不喂模型"新增行结构。
|
||||
- 埋点 `settle` 早于成绩入库会静默丢事件(未来"进历史但不喂模型"的条目同理要落在注入侧,不在读取侧)。
|
||||
- `cargo test export_bindings` 会重写全部 `chat/generated/`(引号风格漂移),跑完要 `git checkout --`
|
||||
掉不是本次新增的文件。
|
||||
- `cargo test export_bindings` 会重写全部 `chat/generated/`;生成目录的提交形态就是 ts-rs 原始输出
|
||||
(2026-10-03 起已从 prettier / eslint 排除),跑完用 `git diff` 为空确认幂等,不要做 prettier 归一化。
|
||||
- 本机 rust 全量 `--bins` 测试会挂在 mock server 的 `inet_csk_accept` 上,用 `--bins "agent::"` 之类过滤跑。
|
||||
|
||||
## review 收口第二轮(2026-09-24)
|
||||
|
||||
@@ -134,7 +134,7 @@ UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_sema
|
||||
- 对客户端刚创建且内容仍匹配可信模板的 Web 脚手架,在正式生成前由宿主执行受控依赖准备和真实 Vite 构建。依赖安装禁用生命周期脚本;不自动安装或覆盖导入/用户修改过的工程。
|
||||
- 准备凭证的 `ready` 只证明初次环境准备成功,不代表当前游戏已验收,后续正常修改不得因此重新安装。`preparing` 中断恢复必须证明原拥有者已结束且其执行子树已回收;身份或归属未知时保留阻断,不重复执行。
|
||||
- 输出明确区分“Node/npm 构建能力通过”与“本项目 Vite 构建通过”;任何一步失败保留可行动错误,不降级为预检成功。
|
||||
- 首页必须区分宿主预检阻塞与 Tauri IPC 调用失败:Rust 返回的安全错误码可透传到状态栏;无错误码的瞬态 IPC 失败只允许一次有界重试后显示独立的客户端连接故障,不得统一伪装成 Node/npm 或浏览器故障。预检仍保持失败关闭,不得因为展示错误变得可绕过。
|
||||
- 首页必须区分宿主预检阻塞与 Tauri IPC 调用失败:Rust 返回的安全错误码可透传到状态栏;无错误码的瞬态 IPC 失败只允许一次有界重试后显示独立的客户端连接故障,不得统一伪装成 Node/npm 或浏览器故障。预检仍保持失败关闭,不得因为展示错误变得可绕过。浏览器 WS 握手、WS 超时和 CDP 连接失败只允许在确认本轮 AGC 浏览器进程树已退出、Profile/owner/临时目录已按归属清理后使用新的隔离 Profile 自动恢复一次;清理未确认时不得重试,连续失败必须返回包含阶段、子进程退出确认、清理确认和是否重试的安全诊断。
|
||||
- 安装载荷提供相同的安全预检 CLI,验证无系统 Node 的独立运行、缺包/篡改失败关闭。NSIS 解包载荷 smoke 与真实安装器注册流程分开报告,不覆盖当前用户的既有安装。
|
||||
|
||||
### 跑酷固定基线
|
||||
@@ -188,7 +188,7 @@ UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_sema
|
||||
### 环境与工作流
|
||||
|
||||
- 客户端交付配套 Node/npm;发布包从本机已安装且与目标平台/架构一致的工具链制作受校验资源,保留许可并校验内容摘要。安装态不依赖系统 PATH 的 Node;开发态可使用已验证的宿主运行时。不得从项目或相对 PATH 加载伪造运行时。随包运行时是**单架构**官方发行版,因此 macOS 当前只构建 `aarch64-apple-darwin` 单架构包;要出 universal 必须先让 staging 支持按架构各带一份同版本运行时,在此之前 universal 目标失败关闭,不得只带宿主架构那一份糊过去。
|
||||
- 新建 Web 游戏在生图和大量实现前执行客户端环境预检,检查 Node/npm 的实际版本、浏览器启动和 CDP 可用性。报告只包含安全状态、版本、耗时和错误码;错误码必须按真实原因分流,浏览器验证只允许在确有证据时使用 `web-preflight-browser-missing` / `-launch-timeout` / `-launch-failed` / `-browser-cleanup-failed`,取消、证据写入、输入与页面校验各有独立码,未识别原因落回专用 `web-preflight-unclassified`,不得用一个具体子系统码兜底。缺失或异常必须尽早返回阻塞,不能指示模型改宿主环境、全盘搜索或自行下载一套运行时。编辑器工程不强制 Web 工具链。
|
||||
- 新建 Web 游戏在生图和大量实现前执行客户端环境预检,检查 Node/npm 的实际版本、浏览器启动和 CDP 可用性。报告只包含安全状态、版本、耗时和错误码;错误码必须按真实原因分流,浏览器验证只允许在确有证据时使用 `web-preflight-browser-missing` / `-launch-timeout` / `-launch-failed` / `-browser-environment-failed` / `-browser-cleanup-failed` / `-browser-recovery-failed`,取消、证据写入、输入与页面校验各有独立码,未识别原因落回专用 `web-preflight-unclassified`,不得用一个具体子系统码兜底。浏览器恢复诊断只保留阶段、进程退出、清理确认和重试状态等安全字段,不暴露命令行、路径或上游原文。缺失或异常必须尽早返回阻塞,不能指示模型改宿主环境、全盘搜索或自行下载一套运行时。编辑器工程不强制 Web 工具链。
|
||||
- 预检不安装依赖、不修改项目 revision、不请求平台生成;构建仍执行项目自己的 npm 脚本。Codex 隔离 HOME 与平台凭据边界保持不变,客户端把已验证的运行时加入执行 PATH,不能把宿主凭据目录交给模型。
|
||||
- 第一轮先明确本次必需玩法、素材和验收项。同批独立读取尽量合并,必需图片一次规划;已有且可用的资产复用。已有目标全部通过后给出交付结果,非阻塞的新点子列为后续工作,不在收尾时主动开启新的生产链。
|
||||
|
||||
@@ -329,7 +329,7 @@ Rust 侧在 `server-rs/crates/shared-contracts` 维护唯一权威 `GameCreation
|
||||
- 参考选择范围为同一项目已登记图片,可跨栏目、多选,无规范前置时最多 5 张,有规范前置时最多 4 张用户参考(总计最多 5 张);复用资源引用选择组件,不允许文档、音视频、占位或跨项目素材。本地生成命令补最小引用 ID 参数并转换为当前账号绑定下的远端资源 ID,沿用图片生成 API 已有 `referenceImageSrcs`。需要规范图的普通图片请求合并并去重规范引用,总数不超过现有 API 限制;只接受单规范引用的图集操作不显示用户参考选择器,原生提交拒绝额外参考而非静默丢弃。不得降级成纯提示词。
|
||||
- 占位由宿主按项目与独立草稿 ID 管理,提交后关联任务 ID;失败重试使用同一占位。切项目清理未提交草稿与界面位置,已提交任务继续沿用账本恢复,重开后不承诺恢复未持久化的占位位置。迟到结果先核对项目和任务归属;只有本会话仍存在的占位才应用最新位置。删除占位只隐藏展示,不取消后台任务或丢弃正式结果。
|
||||
- 参考必须是原生可解码的栅格图片,SVG 不进入参考候选;原生在上传任何引用前预校验整组素材的归属、受控路径、文件及解码,失败不静默丢图。需要重新上传当前账号绑定的参考遵循 `asset.upload` 权限。manifest 读侧的引用形状检查不证明远端账号归属,实际生成始终通过当前账号 binding 解析,不凭历史来源 ID 发起请求。
|
||||
- 图片类 GUI 生成通过可选 `targetCategory` 在原生登记时写入入口栏目,使用既有 manifest `category` 字段及合法分类词表;不传时保留按 kind 派生的行为,Agent 不传。该值不改变远端生成内容及计费幂等槽,仅决定本地生成结果分类;重试保持原占位栏目。同路径重新生成时,主产物按本次入口栏目更新分类(包含覆盖此前手动分类),图集附属切片保持既有独立分类规则。
|
||||
- 图片类 GUI 生成通过可选 `targetCategory` 在原生登记时写入入口栏目,使用既有 manifest `category` 字段及合法分类词表;不传时保留按 kind 派生的行为,Agent 不传。该值不改变远端生成内容及计费幂等槽,仅决定本地生成结果分类;重试保持原占位栏目。同路径重新生成时,主产物按本次入口栏目更新分类(包含覆盖此前手动分类),图集附属切片保持既有独立分类规则。GUI 上传(栏目画布底部工具栏的 `upload_local_asset`)与图片类生成**同一口径**:带可选 `targetCategory` 在当前栏目登记,不传(资源面板、UI 编辑器导入、聊天附件)时保持按内容证据派生 kind 的行为。
|
||||
- 生成面板打开后,占位和面板需处于当前画布标题栏与底部工具栏之间;面板复用公共外观,空间不足时面板内部滚动,提交按钮可达。音频/BGM 占位绑定原有 operation/idempotency 身份,进行中不允许换身份重复提交;失败可用原身份重试,成功结果与图片一样接管占位。关闭未提交浮层保留可继续编辑的草稿,删除占位才丢弃该草稿。
|
||||
- 整理范围为当前栏目页全部资源;“所有资源”页为当前项目所有可展示资源,总览不新增整理行为。重排结果成为自动坐标,可撤销恢复原坐标与手动标记;历史仅保留当前会话,切项目清空。多选仅作用于当前画布可见选中资源,不携带筛选隐藏或跨栏目残留选择;取消手势恢复拖动前坐标,切项目清空选择。
|
||||
- 当前素材名以现有正式命名链路为准:生成时 assetName 参与落盘名称,重命名更新文件名;卡片消费正式资源 label,不从临时输入或历史任务名覆盖后续重命名,不新增平行显示名持久化。若原有命名链路丢失 assetName,则修复原链路,而非只在卡片本地伪造。文档卡不显示任何正文摘要,但详情原文与 JSON 识别读取不变。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,310 @@
|
||||
# 支付服务接入与收银台使用说明
|
||||
|
||||
> 适用版本:2026-10-03 支付应用与订单收银台里程碑
|
||||
>
|
||||
> 本说明面向三类使用者:平台账户管理员、接入平台支付的游戏或外部产品开发者、负责查看订单与回调的后台运营人员。
|
||||
|
||||
## 1. 服务范围
|
||||
|
||||
平台提供一套统一支付服务:接入方提交商品、订单和金额,平台使用已配置的微信支付商户号创建 Native 支付订单,并返回网页收银台地址与动态二维码。用户在网页中扫码付款,平台收到微信支付结果后更新订单,再向接入方配置的回调地址通知。
|
||||
|
||||
当前里程碑的边界如下:
|
||||
|
||||
| 能力 | 当前状态 |
|
||||
| --- | --- |
|
||||
| 微信 Native 扫码支付 | 已支持 |
|
||||
| 游戏客户端或外部产品接入 | 已支持,统一使用 External v1 API |
|
||||
| 网页收银台 | 已支持,桌面端双栏、移动端上下布局 |
|
||||
| 订单记账、查单、回调通知 | 已支持 |
|
||||
| 后台订单与回调投递记录 | 已支持 |
|
||||
| 自动分账、自动提现、人工结算工作流 | 本里程碑不包含,暂由平台人工处理 |
|
||||
| 支付宝、退款、周期扣款 | 本里程碑不包含 |
|
||||
|
||||
金额以整数分传递,默认币种为 `CNY`。生产环境必须使用平台已经开通接口的微信支付商户号;前端和外部产品都不能直接持有商户证书或调用微信支付接口。
|
||||
|
||||
## 2. 一次完整支付流程
|
||||
|
||||
1. 平台账户管理员在“我的 → 支付服务”创建一个支付应用。
|
||||
2. 平台生成该应用的支付 API Key。完整 Key 只在创建成功时显示一次,应立即复制到接入方服务端的密钥管理系统。
|
||||
3. 外部产品服务端使用 API Key 和 `Idempotency-Key` 创建订单。
|
||||
4. 平台服务端向微信支付商户号下单,返回 `checkoutUrl` 和 `providerQrCode`。
|
||||
5. 外部产品将用户跳转到 `checkoutUrl`,或在自己的客户端中打开该网页。
|
||||
6. 用户使用微信扫描收银台二维码付款。
|
||||
7. 微信支付通知平台,平台校验订单号、金额和支付状态后将订单改为 `paid`。
|
||||
8. 如果应用配置了成功回调地址,平台通过回调投递器通知外部产品;外部产品再按自己的业务规则发放道具、虚拟币或权益。
|
||||
9. 外部产品遇到网络超时或回调延迟时,使用查单接口恢复订单状态,不要换一个幂等键重新下单。
|
||||
|
||||
## 3. 创建和管理支付应用
|
||||
|
||||
### 3.1 打开入口
|
||||
|
||||
登录平台后进入“我的”,选择“支付服务”。未登录时会看到登录入口:
|
||||
|
||||

|
||||
|
||||
图 1:支付服务入口截图。截图来自本地前端预览环境,未登录状态不会读取或展示任何支付应用。
|
||||
|
||||
### 3.2 创建应用
|
||||
|
||||
在“创建接入应用”中填写:
|
||||
|
||||
| 字段 | 填写规则 |
|
||||
| --- | --- |
|
||||
| 应用名称 | 用于区分游戏或外部产品,例如“星港商城”或“我的游戏支付” |
|
||||
| 支付成功回调地址 | 可选。建议填写接入方服务端的 HTTPS 地址,例如 `https://game.example.com/payment/callback` |
|
||||
|
||||
回调地址必须使用 HTTPS;本地开发可以使用 loopback HTTP 地址。不能包含用户名、密码或 URL fragment。保存后平台会创建应用和默认支付 API Key。
|
||||
|
||||
### 3.3 保存 API Key
|
||||
|
||||
创建成功后页面会显示完整 API Key,并提示“完整 API Key 只显示这一次”。操作要求:
|
||||
|
||||
1. 立即点击复制按钮。
|
||||
2. 只保存到接入方服务端的环境变量或密钥管理服务。
|
||||
3. 不要提交到 Git、客户端安装包、网页源码、截图或工单。
|
||||
4. API Key 泄露时,在“支付 API Key”区域撤销旧 Key,再创建新的支付应用 Key。
|
||||
|
||||
页面只展示 Key 前缀、所属应用、权限、创建时间、最后使用时间和撤销状态,不再次展示完整密钥。
|
||||
|
||||
### 3.4 停用应用和撤销 Key
|
||||
|
||||
- “我的支付应用”中的“停用”会阻止该应用继续创建支付订单;历史订单和后台记录保留。
|
||||
- “支付 API Key”中的“撤销 Key”会使该 Key 立即失效;已经创建的订单不因撤销 Key 自动取消。
|
||||
- 更换 Key 后,必须同步更新接入方服务端配置,并保留一次查单验证。
|
||||
|
||||
## 4. 外部产品接入
|
||||
|
||||
### 4.1 公共地址和鉴权
|
||||
|
||||
以部署域名作为 API 根地址,以下示例中的 `$BASE_URL` 替换为实际地址:
|
||||
|
||||
```text
|
||||
$BASE_URL/api/external/v1/payment/orders
|
||||
```
|
||||
|
||||
请求头:
|
||||
|
||||
```http
|
||||
Authorization: Bearer tnr_pay_xxxxxxxxxxxxxxxxx
|
||||
Content-Type: application/json
|
||||
Idempotency-Key: game-order-20261003-000001
|
||||
```
|
||||
|
||||
支付 API Key 的格式前缀为 `tnr_pay_`。`Idempotency-Key` 要求为 1-128 个可打印 ASCII 字符且不能包含空格;它代表一次业务下单意图。网络结果不确定时,必须复用原值。
|
||||
|
||||
### 4.2 创建订单
|
||||
|
||||
```bash
|
||||
curl -X POST "$BASE_URL/api/external/v1/payment/orders" \
|
||||
-H "Authorization: Bearer $PAYMENT_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Idempotency-Key: game-order-20261003-000001" \
|
||||
-d '{
|
||||
"merchantOrderId": "game-order-20261003-000001",
|
||||
"title": "星港商城 - 600 水晶",
|
||||
"items": [
|
||||
{"sku": "crystal-600", "name": "600 水晶", "quantity": 1}
|
||||
],
|
||||
"amountCents": 19600,
|
||||
"currency": "CNY",
|
||||
"provider": "wechat_native"
|
||||
}'
|
||||
```
|
||||
|
||||
请求字段:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `merchantOrderId` | 是 | 接入方自己的订单号;建议使用可追踪的 ASCII 字符串,微信 Native 外部订单号最终不超过 32 个字符 |
|
||||
| `title` | 是 | 收银台展示的商品或订单标题 |
|
||||
| `items` | 否 | 商品明细,会被原样记账;不参与平台金额计算 |
|
||||
| `amountCents` | 是 | 应付金额,整数分;例如 `19600` 表示人民币 `196.00` |
|
||||
| `currency` | 否 | 当前使用 `CNY` |
|
||||
| `provider` | 否 | 当前使用 `wechat_native` |
|
||||
|
||||
金额由服务端订单快照决定。客户端传来的商品明细不能作为最终收款金额,接入方应在自己的服务端完成价格校验后再调用创建订单。
|
||||
|
||||
### 4.3 创建响应
|
||||
|
||||
成功响应为 HTTP `200`,核心字段如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"order": {
|
||||
"orderId": "6e7d8c9a0b1c4d2e9f8a7b6c5d4e3f2a",
|
||||
"merchantOrderId": "game-order-20261003-000001",
|
||||
"title": "星港商城 - 600 水晶",
|
||||
"amountCents": 19600,
|
||||
"currency": "CNY",
|
||||
"provider": "wechat_native",
|
||||
"status": "paying",
|
||||
"checkoutToken": "pay_xxxxxxxxx",
|
||||
"checkoutUrl": "/pay/pay_xxxxxxxxx",
|
||||
"providerQrCode": "weixin://wxpay/bizpayurl?...",
|
||||
"expiresAt": "2026-10-03T07:05:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
接入方应优先使用完整部署域名拼接 `checkoutUrl`,例如 `https://www.example.com/pay/pay_xxxxxxxxx`。不要把 `providerQrCode` 直接当作支付成功依据;它只是动态二维码内容,支付状态仍以平台订单为准。
|
||||
|
||||
### 4.4 查询订单
|
||||
|
||||
```bash
|
||||
curl "$BASE_URL/api/external/v1/payment/orders/6e7d8c9a0b1c4d2e9f8a7b6c5d4e3f2a" \
|
||||
-H "Authorization: Bearer $PAYMENT_API_KEY"
|
||||
```
|
||||
|
||||
建议在收银台打开后每 3-5 秒查询一次,直到状态进入终态:
|
||||
|
||||
| 状态 | 含义 | 接入方动作 |
|
||||
| --- | --- | --- |
|
||||
| `pending` | 已创建,尚未完成支付平台下单 | 继续查单;如长时间不变,查看后台或服务日志 |
|
||||
| `paying` | 已拿到支付平台二维码,等待用户付款 | 展示收银台并等待通知或查单 |
|
||||
| `paid` | 已确认到账 | 只在此状态发放游戏商品或权益 |
|
||||
| `expired` | 订单超过有效期 | 不发货,提示用户重新下单 |
|
||||
| `closed` | 订单已关闭 | 不发货 |
|
||||
| `refunded` | 已退款 | 按接入方规则撤销或冻结权益 |
|
||||
|
||||
同一个 `Idempotency-Key` 携带相同请求体会返回首次订单;同一个 Key 携带不同请求体会返回 `409`。遇到连接超时,不要生成新 Key 盲目重试。
|
||||
|
||||
## 5. 网页收银台
|
||||
|
||||
外部产品拿到 `checkoutUrl` 后,可以:
|
||||
|
||||
- 游戏客户端使用系统浏览器或内嵌 WebView 打开。
|
||||
- Web 产品使用新窗口、弹窗或当前页面跳转。
|
||||
- 自己展示商品说明,但最终金额应以平台收银台显示金额为准。
|
||||
|
||||
桌面端收银台左侧展示订单号、商品标题、付款提示和等待状态,右侧展示应付金额、微信 Native 动态二维码和支付渠道:
|
||||
|
||||

|
||||
|
||||
图 2:支付收银台桌面版。此图使用浏览器会话内的模拟订单 `测试游戏道具 / ¥196.00`,用于说明布局,不对应真实商户订单。
|
||||
|
||||
移动端会自动改为上下布局,用户可以在较窄屏幕中滚动查看金额、二维码和支付说明:
|
||||
|
||||

|
||||
|
||||
图 3:支付收银台移动版。二维码内容由页面的 QR 组件根据模拟支付链接生成;生产环境二维码由微信支付商户接口返回。
|
||||
|
||||
收银台的支付提示:
|
||||
|
||||
1. 用户必须完全按照页面显示金额付款,少付或多付都可能无法自动确认。
|
||||
2. 付款后页面会轮询订单并自动更新状态,但接入方仍应以服务端查单或回调为最终依据。
|
||||
3. 订单过期后不要继续使用旧二维码,应重新创建订单。
|
||||
|
||||
## 6. 支付成功回调
|
||||
|
||||
### 6.1 平台支付通知
|
||||
|
||||
微信支付通知地址由平台环境变量 `WECHAT_PAYMENT_NOTIFY_URL` 配置,生产环境必须是外网可访问的 HTTPS 地址,例如:
|
||||
|
||||
```text
|
||||
https://www.example.com/api/payment/wechat/notify
|
||||
```
|
||||
|
||||
该地址接收微信支付商户通知。平台会校验微信通知签名、商户订单号、金额、支付状态和交易号,校验通过后才把本地订单改为 `paid`。
|
||||
|
||||
### 6.2 外部产品回调
|
||||
|
||||
如果创建支付应用时填写了“支付成功回调地址”,平台会向该地址投递 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "payment.paid",
|
||||
"orderId": "6e7d8c9a0b1c4d2e9f8a7b6c5d4e3f2a",
|
||||
"appId": "payapp_xxxxxxxxx",
|
||||
"merchantOrderId": "game-order-20261003-000001",
|
||||
"amountCents": 19600,
|
||||
"currency": "CNY",
|
||||
"provider": "wechat_native",
|
||||
"providerTradeNo": "4200000000000000000000000000",
|
||||
"status": "paid",
|
||||
"paidAt": "2026-10-03T07:01:12Z"
|
||||
}
|
||||
```
|
||||
|
||||
回调请求头:
|
||||
|
||||
```http
|
||||
X-Genarrative-Signature: sha256=<hex>
|
||||
X-Genarrative-Event: payment.paid
|
||||
X-Genarrative-Delivery-Id: delivery_xxxxxxxxx
|
||||
```
|
||||
|
||||
首期签名规则为:使用创建该支付应用时生成的 API Key 哈希作为 HMAC 密钥,对原始请求体字节执行 `HMAC-SHA256`,并将结果编码为小写十六进制;请求头值为 `sha256=` 加十六进制结果。接入方必须使用原始 body 验签,不要先反序列化再重新序列化。独立可轮换的 callback secret 属于后续安全增强,不要在当前版本按未实现字段接入。
|
||||
|
||||
回调处理要求:
|
||||
|
||||
1. 先验签,再按 `orderId` 或 `merchantOrderId` 查询自己的订单。
|
||||
2. 对 `deliveryId` 和业务订单号做幂等处理,重复通知返回成功,不重复发货。
|
||||
3. 成功处理后返回任意 HTTP `2xx`,建议返回 `204`。
|
||||
4. 非 `2xx` 会进入平台回调重试;平台最多尝试 8 次,最终失败会出现在后台“外部回调投递”表中。
|
||||
|
||||
## 7. 后台查看订单和回调
|
||||
|
||||
管理员进入后台,在侧边栏选择“支付订单”,或打开后台路由 `#payment-orders`。页面分为两张表:
|
||||
|
||||
### 7.1 外部回调投递
|
||||
|
||||
用于确认平台是否成功通知了接入方,包含:投递 ID、订单号、应用、回调地址、投递状态、尝试次数、最近错误和更新时间。
|
||||
|
||||
常见状态:
|
||||
|
||||
- `pending`:等待投递或等待下一次重试。
|
||||
- `delivered`:接入方返回了成功状态码。
|
||||
- `failed`:达到重试上限,需人工排查回调地址、验签和接入方服务状态。
|
||||
|
||||
### 7.2 订单记录
|
||||
|
||||
用于查看外部订单号、平台订单号、应用、商品标题、金额、渠道、订单状态、创建时间和支付时间。金额显示为币种和两位小数,订单状态以服务端记录为准。
|
||||
|
||||
本阶段后台用于收款记账和通知核对,不提供自动给外部产品打款或分账按钮。需要结算时,按订单记录和商户账单人工处理。
|
||||
|
||||
## 8. 生产配置清单
|
||||
|
||||
部署前由后端运维配置,不要把下列值写入前端:
|
||||
|
||||
```text
|
||||
WECHAT_PAY_ENABLED=true
|
||||
WECHAT_PAY_PROVIDER=real
|
||||
WECHAT_PAYMENT_NOTIFY_URL=https://<公开域名>/api/payment/wechat/notify
|
||||
```
|
||||
|
||||
同时配置现有微信支付 V3 商户信息、商户证书、私钥、平台证书或平台证书密钥等环境变量。真实商户配置必须通过部署系统的密钥注入完成。`WECHAT_PAY_PROVIDER=mock` 只用于本地开发和页面联调,不能用于生产收款。
|
||||
|
||||
上线前至少验证:
|
||||
|
||||
1. 使用真实商户沙箱或小额测试单创建 Native 订单。
|
||||
2. 微信扫码后平台订单能变为 `paid`。
|
||||
3. `WECHAT_PAYMENT_NOTIFY_URL` 能收到并确认微信通知。
|
||||
4. 接入方回调能通过验签,并且重复回调不会重复发货。
|
||||
5. 后台订单表和回调投递表都有对应记录。
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
| 现象 | 优先检查 |
|
||||
| --- | --- |
|
||||
| 返回 `401` | Authorization 是否是支付应用的 Key;Key 是否已撤销;是否误用了普通 External API Key |
|
||||
| 返回 `403` | 支付应用是否已停用;Key 是否具备订单创建或查询 scope |
|
||||
| 返回 `409` | 是否复用了已有 `Idempotency-Key` 但请求体不同;网络超时场景应复用原请求体和原 Key |
|
||||
| 返回“缺少 `WECHAT_PAYMENT_NOTIFY_URL`” | 生产真实 provider 必须配置公开 HTTPS 通知地址;本地 mock 才使用默认 loopback 地址 |
|
||||
| 一直是 `pending` | 查看微信下单是否成功、后台订单是否有 provider 码、部署日志和商户配置;不要直接给用户发货 |
|
||||
| 用户已付款但接入方未发货 | 先调用查单接口;再看后台“外部回调投递”的状态和最近错误;必要时人工补偿,但要保留幂等记录 |
|
||||
| 二维码无法识别 | 检查是否把 `providerQrCode` 原样交给 QR 组件;不要对内容做 URL 编码或截断 |
|
||||
| 本地截图能显示、生产无二维码 | 截图使用的是 mock 数据;生产必须确认微信商户接口返回了 `code_url`,并检查支付服务配置 |
|
||||
|
||||
## 10. 安全与责任边界
|
||||
|
||||
- API Key 只能放在外部产品服务端,不能放进游戏客户端、浏览器 localStorage 或公开仓库。
|
||||
- 客户端传回的“支付成功”事件只能作为 UI 提示,不能作为发货凭据。
|
||||
- 发货必须由外部产品服务端根据平台查单或验签回调执行,并以订单号做幂等。
|
||||
- 不要记录完整 API Key、微信商户私钥、微信通知原文中的敏感字段或用户支付凭据。
|
||||
- 截图中的订单号、金额和二维码均为演示数据;生产环境会由后端订单和平台微信商户接口动态生成。
|
||||
|
||||
## 11. 相关契约
|
||||
|
||||
- [外部产品支付服务接入技术方案](./【技术方案】外部产品支付服务接入-2026-10-03.md)
|
||||
- [External v1 OpenAPI](./openapi/genarrative-external-v1.openapi.json)
|
||||
- [支付应用与订单收银台里程碑](./project-memory/plans/【里程碑】支付应用与订单收银台-2026-10-03.md)
|
||||
@@ -59,7 +59,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
`spacetime-client` 的 Cargo `lib.path` 指向 `src/active.rs`,现役 mapper 聚合入口是 `src/active/mapper.rs`;原旧 facade 和 mapper 已删除。
|
||||
|
||||
当前 mapper 按现役调用领域拆分为 `admin_account.rs`、`admin_dashboard.rs`、`ai.rs`、`assets.rs`、`auth.rs`、`editor_agent.rs`、`editor_project.rs`、`error_reports.rs`、`external_api_key.rs`、`external_generation.rs`、`game_distribution.rs`、`llm_router_account.rs`、`runtime.rs`、`runtime_profile.rs` 和 `story.rs`;每个模块只承接对应现役 procedure / result 类型的映射。跨领域轻量 helper 和共享 record 放在 `common.rs`;不得把领域聚合或跨层 JSON 兼容结构放入 mapper。
|
||||
当前 mapper 按现役调用领域拆分为 `admin_account.rs`、`admin_dashboard.rs`、`ai.rs`、`assets.rs`、`auth.rs`、`editor_agent.rs`、`editor_project.rs`、`error_reports.rs`、`external_api_key.rs`、`external_generation.rs`、`game_distribution.rs`、`llm_router_account.rs`、`payment.rs`、`runtime.rs`、`runtime_profile.rs` 和 `story.rs`;每个模块只承接对应现役 procedure / result 类型的映射。跨领域轻量 helper 和共享 record 放在 `common.rs`;不得把领域聚合或跨层 JSON 兼容结构放入 mapper。
|
||||
|
||||
## API 路由分组
|
||||
|
||||
@@ -451,6 +451,30 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- 2026-09-01 修订:每次账号认证、Router Key 准备或 LLM 请求解析既有账号时,api-server 都会在同一 owner 级 provisioning 锁内检查固定套餐 `plan_id=1`。没有 active 订阅、订阅已过期或 `end_time` 距当前 Unix 秒不超过 24 小时时,使用管理员接口新建一条订阅;超过 24 小时则复用现有订阅。订阅检查不进入 Responses 流式 chunk,管理员 Token 缺失时仅保留启动告警并跳过续期检查。
|
||||
- LLM Router 计费:读取 Router 用户 `used_quota`,以 `50000 quota = 1 泥点` 结算(`500000 quota = 1 USD`,美元数值直接乘 10,不乘汇率)。上游调用前建立首次基线并补结算,成功响应后同步;不足整数部分、扣费失败和余额不足未支付部分留到后续累计同步。扣钱包、写 `llm_router_consume` 流水与推进 checkpoint 在同一事务完成;流水展示“LLM 调用消耗”。历史费用不追扣。详细契约见 `technical/【技术方案】LLM累计额度结算-2026-09-05.md`。
|
||||
- 2026-09-05 修订:`/api/llm/responses` 与 `/api/llm/chat/completions` 仍在 Router provisioning 前用钱包总余额阻止零余额账号创建或续期;解析凭据后、上游调用前再执行累计额度同步,以扣除退款占用后的剩余可消费余额为准。余额为 `0` 时返回 `409 MUD_POINTS_INSUFFICIENT`;余额或额度同步失败时失败关闭。上游已成功时,后置同步失败只记录错误并留待下次调用前补结算,不把成功模型响应改写为失败。
|
||||
|
||||
### `payment_app`
|
||||
|
||||
- Rust 结构体:`PaymentApp`;源码:`server-rs/crates/spacetime-module/src/payment_storage.rs`。
|
||||
- 作用:平台账号创建的外部支付应用,保存应用归属、展示名称、回调地址、启用状态和更新时间。应用使用平台统一商户主体,不保存支付平台私钥。
|
||||
- 索引:`by_payment_app_owner_user_id` 用于个人中心应用列表。
|
||||
|
||||
### `payment_api_key`
|
||||
|
||||
- Rust 结构体:`PaymentApiKey`;源码:`server-rs/crates/spacetime-module/src/payment_storage.rs`。
|
||||
- 作用:支付应用 API Key 元数据。明文只在创建支付应用响应中返回一次,表内仅保存摘要、可见前缀、作用域、使用时间和撤销状态。
|
||||
- 索引:`by_payment_api_key_app_id`、`by_payment_api_key_owner_user_id`;`key_hash` 唯一用于支付 API 鉴权。
|
||||
|
||||
### `payment_order`
|
||||
|
||||
- Rust 结构体:`PaymentOrder`;源码:`server-rs/crates/spacetime-module/src/payment_storage.rs`。
|
||||
- 作用:客户端游戏和外部产品的统一收款订单。保存不可变商品 / 金额快照、应用归属、接入方订单号、幂等查找键、微信 Native `code_url`、平台交易号、支付状态、过期时间和到账时间。订单确认只能由验签通知或服务端查单进入 `paid`,不与 `profile_recharge_order` 共享入账路径。
|
||||
- 索引:`by_payment_order_app_id`、`by_payment_order_owner_user_id`、`by_payment_order_status`;`idempotency_lookup`、`merchant_order_lookup`、`checkout_token` 唯一用于幂等和公共收银台访问。
|
||||
|
||||
### `payment_webhook_delivery`
|
||||
|
||||
- Rust 结构体:`PaymentWebhookDelivery`;源码:`server-rs/crates/spacetime-module/src/payment_storage.rs`。
|
||||
- 作用:支付成功后的外部回调 durable outbox。保存 callback URL、签名、固定 payload、租约、尝试次数、下次可投递时间、最近错误和 delivered / failed 终态;api-server worker 通过 claim / complete procedure 投递。
|
||||
- 索引:`by_payment_webhook_delivery_status_available`、`by_payment_webhook_delivery_owner_user_id`、`by_payment_webhook_delivery_order_id`;`delivery_id` 按订单唯一幂等。
|
||||
- Windows 私有文件准备:AGC 自有 AppData、凭据目录和 `.agent` 运行态继续使用 managed 范围;用户通过原生选择器明确选中的项目根或文件,若 owner/DACL 仅因权限不足无法读取,则由一次性 UAC helper 在严格复核普通文件/目录、非 reparse/symlink、路径类型和目标 TokenUser 后接管并收紧为当前用户私有 DACL。项目放在当前 profile 之外(例如其他磁盘)不再因为路径位置被拒绝;未经过原生选择器或 AGC 项目根入口的内部路径仍不获得任意提权资格。
|
||||
|
||||
### `game_distribution_game`
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
# 外部产品支付服务接入
|
||||
|
||||
更新时间:`2026-10-03`
|
||||
|
||||
## 目标
|
||||
|
||||
为 Genarrative 提供一套由平台统一收款的支付服务,支持:
|
||||
|
||||
- 客户端制作的游戏通过公开接口创建订单并打开 Web 收银台。
|
||||
- 外部产品通过应用身份、API Key 和签名调用同一套订单接口。
|
||||
- 平台使用已开通的微信支付 / 支付宝商户接口生成动态支付二维码,并以服务端验签、查单或支付通知确认到账。
|
||||
- 用户在自己的账户中查看支付服务,创建和配置接入应用,管理 API Key、回调地址和订单。
|
||||
- 后台查看支付应用、订单、支付通知、回调投递和异常状态。
|
||||
- 首期只负责收款、记账和结果通知;外部产品的余额、手续费、提现和自动分账不在首期范围内,人工结算依据后台账单处理。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不把外部产品订单记入现有的个人钱包充值订单,不向购买者自动发放泥点或会员权益。
|
||||
- 不允许客户端或外部产品直接声明“支付成功”改变订单状态。
|
||||
- 不保存或向前端下发商户私钥、API v3 密钥、支付宝应用私钥、支付平台 access token 等敏感凭据。
|
||||
- 不在首期实现平台余额、外部产品提现、手续费规则、自动分账、佣金和税务结算。
|
||||
- 不使用个人收款码、静态收款码或无法验签查单的人工转账作为支付服务正式渠道。
|
||||
|
||||
## 入口与边界
|
||||
|
||||
### 参与方
|
||||
|
||||
- 平台:持有并配置自己的微信支付 / 支付宝商户账户,维护支付服务、订单和通知。
|
||||
- 接入方:平台账号下创建的游戏或外部产品应用,使用平台发放的 API Key 创建订单。
|
||||
- 购买者:在公共 Web 收银台查看商品信息并完成扫码支付,可不登录平台账号。
|
||||
- 支付平台:微信支付 Native、支付宝当面付等已开通的商户 API,负责实际收款、通知和查单。
|
||||
|
||||
### 入口
|
||||
|
||||
- 账号侧:`/profile/payment`,展示支付服务状态、应用列表、API Key、回调配置和订单入口。
|
||||
- 公共收银台:`/pay/{checkoutToken}`,由服务端返回订单快照、可用支付方式和动态二维码。
|
||||
- 外部 API:`/api/external/v1/payment/orders` 及同一资源下的查单、关闭和收银台地址接口。
|
||||
- 支付平台通知:按渠道配置独立的 HTTPS 通知入口,统一进入支付订单确认流程。
|
||||
- 后台:增加支付服务概览、接入应用、支付订单、通知与回调投递页面,并受现有后台 Tab 权限控制。
|
||||
|
||||
### 职责边界
|
||||
|
||||
- `api-server`:HTTP 鉴权、公开 API、收银台 BFF、支付平台通知、回调投递编排和错误 envelope。
|
||||
- `module-*`:金额、订单状态、幂等、归属、回调重放和人工复核等领域规则。
|
||||
- `spacetime-module`:支付应用、订单、支付尝试、通知观察和投递 outbox 的持久化与原子事务。
|
||||
- `spacetime-client`:为 api-server 提供支付领域 procedure 的 typed facade。
|
||||
- `platform-*`:微信 / 支付宝的下单、二维码、验签、解密、查单和响应解析。
|
||||
- `shared-contracts` / `packages/shared`:账号页、后台页和外部 API 共用的 DTO,不保存业务真相。
|
||||
- 前端:展示后端返回的订单状态,收银台轮询或 SSE 只用于刷新,不自行判定到账。
|
||||
|
||||
## 接入应用与账户配置
|
||||
|
||||
### 应用
|
||||
|
||||
每个应用属于一个平台账号,至少包含:
|
||||
|
||||
- `appId`、应用名称、产品类型、启用状态和创建 / 更新时间。
|
||||
- 允许使用的支付渠道和金额上下限。
|
||||
- 支付成功回调地址、回调签名密钥版本、回调重试策略。
|
||||
- 可选的返回地址;必须按 HTTPS、域名白名单和协议校验,不能接受任意用户提交的跳转地址。
|
||||
- 应用级订单前缀和展示品牌信息,不能覆盖平台商户主体。
|
||||
|
||||
### API Key
|
||||
|
||||
- 创建时只展示一次完整 secret,数据库只保存 hash、可见前缀、scope、创建时间、最后使用时间、撤销时间和版本。
|
||||
- 首期 scope 至少区分 `payment:orders:create`、`payment:orders:read`、`payment:orders:close`。
|
||||
- Key 只属于一个应用,撤销后立即禁止创建订单、查单和关闭操作。
|
||||
- 请求使用 `Authorization: Bearer` 或约定的 API Key 头,并要求 `Idempotency-Key`。
|
||||
- 管理员页面可以查看安全元数据,不能查看完整 secret。
|
||||
|
||||
### 商户配置
|
||||
|
||||
- 商户号、应用 ID、证书序列号、通知地址和支付平台密钥只由服务端环境变量或受控密文配置提供。
|
||||
- 用户账户页面只展示已启用的支付渠道和配置状态,不允许用户上传或读取平台级商户私钥。
|
||||
- 后台只展示脱敏的商户配置和连通性状态;密钥写入日志、追踪事件和错误响应均禁止。
|
||||
- 每个渠道必须明确 `enabled / unavailable / misconfigured` 状态,未完成配置的渠道不出现在收银台可选项。
|
||||
|
||||
## 订单与金额契约
|
||||
|
||||
### 创建订单
|
||||
|
||||
接入方提交:
|
||||
|
||||
- 应用 API Key、幂等键和接入方订单号。
|
||||
- 商品标题、商品明细、商品数量、币种和金额,金额使用整数分。
|
||||
- 接入方用户标识和可选的业务 metadata;metadata 有字节上限,禁止放入凭据、Data URL 或大段正文。
|
||||
- 支付成功后的业务回调要求由应用配置提供,不能在每次请求中绕过已审核的地址白名单。
|
||||
|
||||
服务端执行:
|
||||
|
||||
1. 校验应用、API Key、scope、幂等键、金额范围、币种和商品字段。
|
||||
2. 以 `appId + idempotencyKey` 建立唯一幂等记录;相同请求重放返回原订单,相同幂等键但字段不一致则失败。
|
||||
3. 生成平台订单号、不可猜测的 `checkoutToken` 和过期时间,保存不可变商品与金额快照。
|
||||
4. 调用选定的支付平台创建订单,要求平台返回的商户订单号、金额和渠道与本地快照一致。
|
||||
5. 返回平台无关的订单摘要、收银台 URL、过期时间和可用渠道,不返回商户请求签名或支付平台敏感字段。
|
||||
|
||||
### 状态
|
||||
|
||||
订单状态为:
|
||||
|
||||
`pending → paying → paid`
|
||||
|
||||
并允许从 `pending / paying` 进入 `expired / closed`,从 `paid` 进入 `refunded`(退款能力在后续里程碑接入)。支付通知、查单和人工复核只能按允许的状态转移推进;重复通知、乱序通知和重复查单必须幂等。
|
||||
|
||||
本地 `paid` 必须同时满足:
|
||||
|
||||
- 平台通知验签或主动查单成功。
|
||||
- 商户订单号、平台交易号、应用、渠道、金额和币种与本地订单一致。
|
||||
- 支付平台确认时间合法且来自平台响应,不能使用服务器当前时间补齐。
|
||||
|
||||
## Web 收银台
|
||||
|
||||
收银台使用服务端生成的 `checkoutToken`,页面展示:
|
||||
|
||||
- 商品名称、商品明细、订单号、应付金额和过期时间。
|
||||
- 可用支付渠道,例如“微信支付 / 支付宝”,渠道由商户配置和订单约束共同决定。
|
||||
- 当前渠道的动态二维码。二维码由平台商户 API 以当前订单金额和平台订单号生成,不复用静态码。
|
||||
- “待支付、支付确认中、支付成功、已关闭、已过期、支付异常”等后端状态。
|
||||
- 查询状态、复制金额、返回应用等操作;返回地址只使用应用白名单中的地址。
|
||||
|
||||
收银台不展示平台密钥、用户 API Key、完整回调地址和内部错误文本。浏览器轮询或 SSE 只能读取公开订单状态;出现“支付成功”页面前仍需经过平台回调或服务端查单确认。
|
||||
|
||||
页面视觉采用截图中的双栏收银台结构作为参考:左侧是订单信息和金额提示,右侧是二维码和渠道状态;移动端改为纵向布局。截图只作为布局参考,不作为支付协议、金额或商户身份依据。
|
||||
|
||||
## 通知、查单与外部回调
|
||||
|
||||
- 平台支付通知入口按渠道独立配置,先验签 / 解密,再解析订单和金额,再进入统一的订单确认事务。
|
||||
- 通知成功后快速返回平台要求的成功响应;外部产品回调通过 outbox 异步投递,不能让第三方回调慢速阻塞支付平台通知。
|
||||
- 外部回调 payload 至少包含平台订单号、接入方订单号、应用 ID、支付状态、金额、币种、平台交易号和确认时间。
|
||||
- 首期回调签名使用 `HMAC-SHA256(sha256(rawApiKey), rawBody)`,HTTP 头为 `X-Genarrative-Signature: sha256=<hex>`、`X-Genarrative-Event` 和 `X-Genarrative-Delivery-Id`;接入方验签后按订单号和 delivery id 幂等消费。独立可轮换 callback secret 作为后续安全增强项,不在本里程碑伪称已实现。
|
||||
- 投递按有限次数退避重试,记录 HTTP 状态、响应摘要、下一次重试时间和最终失败原因;不得记录完整 secret、签名原文或敏感请求体。
|
||||
- 查询订单只返回应用归属范围内的订单;公共收银台只返回公开展示字段和状态。
|
||||
- 支付确认与唯一回调 outbox 在同一数据库事务提交,回调 payload 从已确认订单构建;失败不会留下 paid 但缺 outbox 的半完成状态。每个投递使用唯一租约代次,过期 worker 不能覆盖新 worker 结果。
|
||||
- 外部下单使用不超过 32 字符的平台订单号和冻结的过期时间,独立通知 URL 为 `/api/payment/wechat/notify`;下单 claim 在库内完成,结果不确定时先查单,同一商户单号不因重试更换。
|
||||
- 应用、订单、回调 procedure 都要求已注册后端运行服务身份,匿名 SpacetimeDB 调用不得创建或确认订单。回调地址要求 HTTPS、禁止凭据和内网地址,发送时再次检查解析地址且禁止跳转;仅隔离测试环境可以允许 loopback fixture。
|
||||
- 支付平台通知、主动查单和后台人工重试全部复用同一条幂等确认路径,不各自实现入账。
|
||||
|
||||
## 后台数据页
|
||||
|
||||
后台新增支付服务入口,至少包含:
|
||||
|
||||
- 总览:订单数、已支付金额、待确认订单、通知失败、外部回调失败和按渠道统计。
|
||||
- 接入应用:应用状态、所属账号、渠道、回调地址脱敏、Key 状态和最近使用时间。
|
||||
- 支付订单:应用、商品、接入方订单号、平台订单号、金额、渠道、状态、创建 / 支付时间和平台交易号。
|
||||
- 通知与投递:验签结果、通知类型、订单关联、重复次数、投递状态和下次重试时间。
|
||||
- 详情与人工处理:展示本地快照和平台事实的对照;人工操作必须记录管理员、原因、时间和期望状态,不能直接改订单金额或伪造支付成功。
|
||||
|
||||
分页列表必须使用后端分页和排序参数;金额使用整数分并在展示层格式化,时间统一转成人可读时间,不能把 SATS enum、Option 或微秒 Timestamp 原样展示。
|
||||
|
||||
## 安全、幂等与失败关闭
|
||||
|
||||
- 所有金额和商品快照以服务端持久化值为准,客户端传回的金额只作为展示输入,不能作为支付成功依据。
|
||||
- 订单创建、支付平台下单、通知确认和外部回调投递分别设置幂等键,重放不会重复创建订单、记账或发放商品。
|
||||
- 支付平台超时、网络断开、响应解析失败或结果不确定时,订单保持待确认并进入查单 / 人工复核,不自动标记失败或再次创建新订单。
|
||||
- 未知通知、金额不一致、订单归属不一致、签名失败和平台交易号冲突必须拒绝确认,并保留脱敏诊断事实。
|
||||
- 日志只记录稳定 HMAC 引用、状态、金额、渠道、阶段和错误分类,不记录二维码原文、密钥、签名、完整订单请求或敏感 metadata。
|
||||
- 所有用户、应用、订单、回调和后台查询都检查归属和权限;外部 API 不允许通过猜测订单号访问其它应用数据。
|
||||
|
||||
## 契约与迁移
|
||||
|
||||
### API / DTO / OpenAPI
|
||||
|
||||
- 新增支付服务公开 API 时同步更新 `docs/openapi/genarrative-external-v1.openapi.json`、`shared-contracts`、契约测试和公开发现文档。
|
||||
- 内部账号接口放在 `/api/profile/payment/*`,后台接口放在 `/admin/api/payment/*`,支付平台通知不走用户鉴权。
|
||||
- 公开 DTO 不暴露 provider 原始响应、密钥、完整二维码 payload 或内部数据库 ID。
|
||||
|
||||
### SpacetimeDB
|
||||
|
||||
计划增加独立的支付领域表,具体表结构在里程碑实施前冻结:
|
||||
|
||||
- 支付接入应用和 API Key 元数据。
|
||||
- 平台支付订单和不可变商品快照。
|
||||
- 渠道支付尝试与平台交易号。
|
||||
- 支付通知观察记录。
|
||||
- `payment_webhook_delivery` 外部回调投递 outbox 与投递结果。
|
||||
|
||||
新增字段必须追加到已有 Rust 表结构体末尾并设置明确默认值;不得重排、改名或删除现有充值表字段。修改 schema 时同步 migration、表目录、生成绑定并运行 `npm run check:spacetime-schema`。
|
||||
|
||||
### 兼容策略
|
||||
|
||||
现有个人钱包充值、微信虚拟支付和充值退款链路保持原契约。支付服务用独立订单前缀、表和 API namespace 标识,不把历史充值订单迁移成外部支付订单。
|
||||
|
||||
## 首期验收标准
|
||||
|
||||
| 条款 | 验收方式 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| 账号可创建支付应用并管理 API Key | 浏览器 smoke、接口测试 | 个人中心页面、Key 只展示一次、撤销立即生效 |
|
||||
| 客户端游戏和外部产品可创建订单 | API 契约测试 | 同一幂等键返回原订单,字段冲突被拒绝 |
|
||||
| 收银台展示不可篡改的商品和金额 | 浏览器 smoke | `checkoutToken` 只能读取公开字段,金额来自服务端快照 |
|
||||
| 商户账户生成动态二维码 | provider mock + 真实沙箱 smoke | 二维码订单号和金额与本地订单一致 |
|
||||
| 支付结果以服务端事实确认 | 通知 / 查单测试 | 客户端伪造成功不会改变订单,通知重放不重复确认 |
|
||||
| 外部产品收到成功通知 | outbox 测试与回调 smoke | 签名、重试、最终失败和幂等投递可观测 |
|
||||
| 后台可查看运营数据 | 后台页面测试 | 应用、订单、通知和回调列表分页、筛选、详情可用 |
|
||||
| 权限与敏感数据安全 | 负向测试、日志检查 | 跨应用访问、失效 Key、错误金额、错误签名均失败且无密钥泄漏 |
|
||||
| 首期不产生自动分账 | 领域测试与文档核对 | 没有余额、提现或自动分账写路径 |
|
||||
|
||||
## 未决问题与决策
|
||||
|
||||
- 首期接入渠道:优先复用现有微信 Native 动态二维码链路;支付宝作为同一收银台的第二个 provider,只有商户 API 配置和验签协议完成后才显示。
|
||||
- 首期结算:平台收款、记账、通知,外部产品人工结算;手续费、余额和提现另立规范。
|
||||
- 商户主体:使用平台自己的商户号;接入应用不能选择或替换商户主体。
|
||||
- 退款:首期只展示支付事实和人工处理入口,正式退款与外部产品权益回收另立里程碑。
|
||||
Reference in New Issue
Block a user