From ce8609116b45b9402a13a7eac1c696389183959a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E7=85=9C?= <1871263099@qq.com> Date: Sun, 4 Oct 2026 22:55:12 +0800 Subject: [PATCH] fix:mcp-sibling-create-draft-confirmation --- README.md | 2 + apps/api/src/mcp/operations.ts | 16 ++++++ apps/api/test/mcp.test.ts | 73 ++++++++++++++++++++++++ docs/fix-mcp-create-drafts-2026-10-04.md | 22 +++++++ update.md | 3 +- 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 docs/fix-mcp-create-drafts-2026-10-04.md diff --git a/README.md b/README.md index 95c4da1..4f83d9d 100644 --- a/README.md +++ b/README.md @@ -131,3 +131,5 @@ git config remote.pushDefault github | [Gold API](https://gold-api.com/docs) | [黄金 XAU](https://api.gold-api.com/price/XAU)、[白银 XAG](https://api.gold-api.com/price/XAG) | 美元/金衡盎司参考价;以 31.1034768 克/金衡盎司换算为每克价格。已配置贵金属每日尝试更新,可在网站手动刷新。 | 无需密钥;请求固定 XAU/XAG 品种,不发送个人数据和持仓。 | 12 秒超时;格式、时间或汇率异常时保留之前的报价与估值;已有历史导入报价保留。 | 上述接口由后端访问,浏览器业务请求使用本站 `/api`,第三方不会收到本应用登录 Cookie。Gold API 返回的参考价只用于持仓估值,不包含饰品工费、买卖价差或回收折价。外币价格使用最新可用公共日汇率,休市时两类报价时间可能不同;报价日期会显示在界面并随备份保存。自动估价为可选项,开启后新增估值历史,不改写历史记录。 + +MCP 同一连接、同一账目快照下的多个新增账号草稿可依次审阅确认;其他修改仍保留状态冲突检查。修复前已失效的草稿需只为尚未新增的账号重新生成,见 [多账号草稿确认修复](docs/fix-mcp-create-drafts-2026-10-04.md)。 diff --git a/apps/api/src/mcp/operations.ts b/apps/api/src/mcp/operations.ts index 5d46e81..2044886 100644 --- a/apps/api/src/mcp/operations.ts +++ b/apps/api/src/mcp/operations.ts @@ -337,6 +337,22 @@ export class AgentOperations { if ((await this.state(r.userId)) !== row.snapshot) throw new ConflictException('账目已变化,请取消并重新创建操作'); const result = await this.execute(t, grant, p); + if (row.tool === 'position_create') { + // Advance only sibling additions reviewed against the same snapshot. + // The user lock and transaction keep edits and other grants stale. + await this.db.agentOperation.updateMany({ + where: { + userId: r.userId, + grantId: row.grantId, + tool: 'position_create', + status: 'pending', + snapshot: row.snapshot, + id: { not: row.id }, + expiresAt: { gt: new Date() }, + }, + data: { snapshot: await this.state(r.userId) }, + }); + } return this.view( await this.db.agentOperation.update({ where: { id }, diff --git a/apps/api/test/mcp.test.ts b/apps/api/test/mcp.test.ts index 5bd80e9..5b83c96 100644 --- a/apps/api/test/mcp.test.ts +++ b/apps/api/test/mcp.test.ts @@ -198,6 +198,79 @@ test('official SDK Streamable HTTP: real MySQL full business paths, drafts, isol const applied = await confirm(d, draft); assert.ok(applied.result.id); assert.equal((await call(d, 'positions_list')).total, 1); + // Only independent creates from the same connection and snapshot may advance. + const batchState = (await call(d, 'state_get')).state; + const batchArgs = [0, 1, 2].map((i) => ({ + ...position, + name: 'batch account ' + i, + amount: '8.86420975', + expectedState: batchState, + idempotencyKey: randomUUID(), + })); + const batch = await Promise.all(batchArgs.map((args) => call(d, 'position_create', args))); + const edit = await call(d, 'balance_record', { + id: applied.result.id, + data: { amount: '2', date: day }, + expectedState: batchState, + idempotencyKey: randomUUID(), + }); + const otherToken = await web(d.cookie, '/agent/tokens', 'POST', { + name: 'separate draft connection', + days: 1, + scopes: ['read', 'draft'], + password: d.password, + }); + assert.equal(otherToken.status, 201); + const otherClient = new Client({ name: 'other draft connection', version: '1.31.0' }); + clients.push(otherClient); + await otherClient.connect( + new StreamableHTTPClientTransport(new URL(resource), { + requestInit: { headers: { Authorization: 'Bearer ' + otherToken.data.token } }, + }), + ); + const otherDraft = await call({ client: otherClient }, 'position_create', { + ...position, + name: 'other grant create', + expectedState: batchState, + idempotencyKey: randomUUID(), + }); + const firstBatch = await confirm(d, batch[0]); + assert.equal( + (await call(d, 'position_create', batchArgs[0])).operationId, + firstBatch.operationId, + ); + await Promise.all(batch.slice(1).map((operation) => confirm(d, operation))); + assert.equal((await call(d, 'positions_list', { q: 'batch account' })).total, 3); + assert.equal( + (await call(d, 'position_get', { id: firstBatch.result.id })).amount, + '8.86420975', + ); + for (const operation of [edit, otherDraft]) { + assert.equal( + ( + await web(d.cookie, '/agent/operations/' + operation.operationId, 'POST', { + approve: true, + }) + ).status, + 409, + ); + } + const webStale = await write(d, 'position_create', { ...position, name: 'web stale' }); + assert.equal( + ( + await web(d.cookie, '/positions', 'POST', { + ...position, + name: 'manual web account', + }) + ).status, + 201, + ); + assert.equal( + (await web(d.cookie, '/agent/operations/' + webStale.operationId, 'POST', { approve: true })) + .status, + 409, + ); + const expired = await write(d, 'position_create', { ...position, name: 'expired' }); await db.agentOperation.update({ where: { id: expired.operationId }, diff --git a/docs/fix-mcp-create-drafts-2026-10-04.md b/docs/fix-mcp-create-drafts-2026-10-04.md new file mode 100644 index 0000000..6fa5795 --- /dev/null +++ b/docs/fix-mcp-create-drafts-2026-10-04.md @@ -0,0 +1,22 @@ +# MCP 多账号草稿确认修复(2026-10-04) + +## 问题与原因 + +通过 MCP 在同一个 state_get 状态下创建多个 position_create 草稿时,草稿都保存同一账目快照。确认第一个新增后账目变化,剩余新增草稿会报“账目已变化,请取消并重新创建操作”。 + +## 修复范围 + +确认 position_create 成功后,在用户锁和同一数据库事务内,将同一用户、同一连接、相同旧快照下尚未过期的待确认 position_create 草稿快照推进到最新状态。每份草稿仍需用户逐项审阅确认,不会自动执行。 + +仅独立新增草稿适用此规则。余额记录、修改、转账等其他草稿,以及不同连接的新增草稿,不会随之推进。网页新增或修改、设置变化仍触发原有冲突检查。原始幂等键和请求摘要不变,重试已完成操作不会重复创建账户。不需要数据库迁移。 + +修复前已因部分确认而落后于当前账目的草稿不自动恢复。应重新查询当前账号,只重新生成尚未新增的账号草稿,避免重复创建已经成功的账号。 + +## 验证 + +- 修复前通过官方 SDK 和真实 MySQL 复现后续草稿确认返回 409。 +- 修复后三份同快照新增草稿全部确认成功,后两份并发确认也通过。 +- 幂等重试返回原操作;金额十进制字符串保持精确。 +- 不同连接的新增草稿、同快照余额草稿仍返回 409;网页新增也使旧草稿失效。 +- MCP/OAuth 集成测试 6 项通过,单元测试 52 项通过,后端编译通过。 +- 测试仅使用隔离用户,结束后清理。未确认或修改实际用户的草稿、账号或余额。 diff --git a/update.md b/update.md index e306637..97125ce 100644 --- a/update.md +++ b/update.md @@ -92,4 +92,5 @@ - ~~快速记账的转账按钮 和快速记账显示在附近,不在显示在右边~~ — 已完成并验证:2026-10-04 16:00(UTC+8),见 docs/update-quick-entry-2026-10-04.md - ~~快速记账时,可以出现点击当天已完成记账的按钮,点击可以快速设置某个账号当天已完成记账,同时在快速记账中可以显示或隐藏当天已完成记账~~ — 已完成并验证:2026-10-04 16:00(UTC+8),见 docs/update-quick-entry-2026-10-04.md - ~~还款可以优惠功能(负数表示手续费)~~ — 已完成并验证:2026-10-04 22:18(UTC+8),见 docs/update-repayment-trend-2026-10-04.md -- ~~净资产轨迹,可以设置隐藏某条折线,鼠标放线线可以查看当天数据~~ — 已完成并验证:2026-10-04 22:18(UTC+8),见 docs/update-repayment-trend-2026-10-04.md \ No newline at end of file +- ~~净资产轨迹,可以设置隐藏某条折线,鼠标放线线可以查看当天数据~~ — 已完成并验证:2026-10-04 22:18(UTC+8),见 docs/update-repayment-trend-2026-10-04.md +- ~~修复 MCP 同批新增多个账号时后续草稿提示账目已变化~~ — 已完成并验证:2026-10-04 22:53(UTC+8),见 docs/fix-mcp-create-drafts-2026-10-04.md