botmux 3.9.0 → 3.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/backend/tmux-pipe-backend.d.ts +7 -0
  3. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/tmux-pipe-backend.js +20 -0
  5. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  6. package/dist/adapters/backend/types.d.ts +12 -0
  7. package/dist/adapters/backend/types.d.ts.map +1 -1
  8. package/dist/adapters/backend/types.js.map +1 -1
  9. package/dist/adapters/cli/codex.d.ts.map +1 -1
  10. package/dist/adapters/cli/codex.js +40 -7
  11. package/dist/adapters/cli/codex.js.map +1 -1
  12. package/dist/adapters/cli/traex.d.ts.map +1 -1
  13. package/dist/adapters/cli/traex.js +87 -87
  14. package/dist/adapters/cli/traex.js.map +1 -1
  15. package/dist/cli/deferred-topic-send.d.ts +11 -0
  16. package/dist/cli/deferred-topic-send.d.ts.map +1 -1
  17. package/dist/cli/deferred-topic-send.js +21 -4
  18. package/dist/cli/deferred-topic-send.js.map +1 -1
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +177 -38
  21. package/dist/cli.js.map +1 -1
  22. package/dist/core/command-handler.d.ts.map +1 -1
  23. package/dist/core/command-handler.js +40 -1
  24. package/dist/core/command-handler.js.map +1 -1
  25. package/dist/core/dispatch.d.ts +43 -5
  26. package/dist/core/dispatch.d.ts.map +1 -1
  27. package/dist/core/dispatch.js +104 -6
  28. package/dist/core/dispatch.js.map +1 -1
  29. package/dist/core/passthrough-commands.d.ts.map +1 -1
  30. package/dist/core/passthrough-commands.js +1 -1
  31. package/dist/core/passthrough-commands.js.map +1 -1
  32. package/dist/daemon.d.ts +11 -1
  33. package/dist/daemon.d.ts.map +1 -1
  34. package/dist/daemon.js +32 -3
  35. package/dist/daemon.js.map +1 -1
  36. package/dist/i18n/en.d.ts.map +1 -1
  37. package/dist/i18n/en.js +2 -0
  38. package/dist/i18n/en.js.map +1 -1
  39. package/dist/i18n/zh.d.ts.map +1 -1
  40. package/dist/i18n/zh.js +2 -0
  41. package/dist/i18n/zh.js.map +1 -1
  42. package/dist/im/lark/card-handler.d.ts.map +1 -1
  43. package/dist/im/lark/card-handler.js +9 -0
  44. package/dist/im/lark/card-handler.js.map +1 -1
  45. package/dist/im/lark/issue-card.d.ts +223 -0
  46. package/dist/im/lark/issue-card.d.ts.map +1 -0
  47. package/dist/im/lark/issue-card.js +542 -0
  48. package/dist/im/lark/issue-card.js.map +1 -0
  49. package/dist/im/lark/issue-command-deps.d.ts +41 -0
  50. package/dist/im/lark/issue-command-deps.d.ts.map +1 -0
  51. package/dist/im/lark/issue-command-deps.js +195 -0
  52. package/dist/im/lark/issue-command-deps.js.map +1 -0
  53. package/dist/im/lark/issue-command.d.ts +137 -0
  54. package/dist/im/lark/issue-command.d.ts.map +1 -0
  55. package/dist/im/lark/issue-command.js +335 -0
  56. package/dist/im/lark/issue-command.js.map +1 -0
  57. package/dist/platform/issue-client.d.ts +201 -0
  58. package/dist/platform/issue-client.d.ts.map +1 -0
  59. package/dist/platform/issue-client.js +142 -0
  60. package/dist/platform/issue-client.js.map +1 -0
  61. package/dist/platform/platform-http.d.ts +4 -0
  62. package/dist/platform/platform-http.d.ts.map +1 -1
  63. package/dist/platform/platform-http.js +38 -2
  64. package/dist/platform/platform-http.js.map +1 -1
  65. package/dist/services/codex-composer-state.d.ts +27 -0
  66. package/dist/services/codex-composer-state.d.ts.map +1 -0
  67. package/dist/services/codex-composer-state.js +53 -0
  68. package/dist/services/codex-composer-state.js.map +1 -0
  69. package/dist/services/codex-transcript.d.ts +33 -12
  70. package/dist/services/codex-transcript.d.ts.map +1 -1
  71. package/dist/services/codex-transcript.js +82 -20
  72. package/dist/services/codex-transcript.js.map +1 -1
  73. package/dist/services/issue-board-store.d.ts +214 -0
  74. package/dist/services/issue-board-store.d.ts.map +1 -0
  75. package/dist/services/issue-board-store.js +472 -0
  76. package/dist/services/issue-board-store.js.map +1 -0
  77. package/dist/services/issue-claim-flow.d.ts +160 -0
  78. package/dist/services/issue-claim-flow.d.ts.map +1 -0
  79. package/dist/services/issue-claim-flow.js +222 -0
  80. package/dist/services/issue-claim-flow.js.map +1 -0
  81. package/dist/services/issue-outbox-pump.d.ts +25 -0
  82. package/dist/services/issue-outbox-pump.d.ts.map +1 -0
  83. package/dist/services/issue-outbox-pump.js +102 -0
  84. package/dist/services/issue-outbox-pump.js.map +1 -0
  85. package/dist/services/issue-release.d.ts +78 -0
  86. package/dist/services/issue-release.d.ts.map +1 -0
  87. package/dist/services/issue-release.js +89 -0
  88. package/dist/services/issue-release.js.map +1 -0
  89. package/dist/services/issue-report.d.ts +72 -0
  90. package/dist/services/issue-report.d.ts.map +1 -0
  91. package/dist/services/issue-report.js +84 -0
  92. package/dist/services/issue-report.js.map +1 -0
  93. package/dist/services/issue-status-view.d.ts +52 -0
  94. package/dist/services/issue-status-view.d.ts.map +1 -0
  95. package/dist/services/issue-status-view.js +42 -0
  96. package/dist/services/issue-status-view.js.map +1 -0
  97. package/dist/services/issue-status-writer.d.ts +98 -0
  98. package/dist/services/issue-status-writer.d.ts.map +1 -0
  99. package/dist/services/issue-status-writer.js +123 -0
  100. package/dist/services/issue-status-writer.js.map +1 -0
  101. package/dist/services/traex-paths.d.ts +14 -2
  102. package/dist/services/traex-paths.d.ts.map +1 -1
  103. package/dist/services/traex-paths.js +16 -2
  104. package/dist/services/traex-paths.js.map +1 -1
  105. package/dist/services/traex-transcript.d.ts +33 -0
  106. package/dist/services/traex-transcript.d.ts.map +1 -1
  107. package/dist/services/traex-transcript.js +135 -28
  108. package/dist/services/traex-transcript.js.map +1 -1
  109. package/dist/setup/lark-scopes.json +0 -2
  110. package/dist/worker.js +269 -7
  111. package/dist/worker.js.map +1 -1
  112. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-claim-flow.d.ts","sourceRoot":"","sources":["../../src/services/issue-claim-flow.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEpF,OAAO,EASL,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAE9E;AAKD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS3F;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,CACL,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,KACpF,OAAO,CAAC,iBAAiB,CAAC;QAAE,KAAK,EAAE;YAAE,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;IACzF,IAAI,EAAE,CACJ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,gBAAgB,EAAE,MAAM,CAAC;QACzB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,KACE,OAAO,CAAC,iBAAiB,CAAC;QAAE,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;IAC1D,WAAW,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACnE;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,mCAAmC;IACnC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,gDAAgD;IAChD,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,6CAA6C;IAC7C,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpF;;;;;;;;;;;OAWG;IACH,WAAW,EAAE,CACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,WAAW,CAAC;QACpB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,KACE,OAAO,CAAC,iBAAiB,CAAC;QAAE,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;IAC1D,uDAAuD;IACvD,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC/E,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,GAAG,OAAO,GAAG,UAAU,GAAG,iBAAiB,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjG,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB;IACpB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE;AAC/E,iCAAiC;GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE;AAC5E,+CAA+C;GAC7C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAChE,6DAA6D;GAC3D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACrF,4DAA4D;GAC1D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5F,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,eAAe,CAAC,CAkK1B"}
@@ -0,0 +1,222 @@
1
+ /**
2
+ * 拉群领取:把一个平台 issue 变成一个飞书群 + 一个跑着的 CLI 会话。
3
+ *
4
+ * 顺序在 [[issue-board-store]] 文件头定死,这里是它的可执行版本:
5
+ *
6
+ * 1. platform claim
7
+ * 2. recordClaimIntent ← 立刻。claim 已成功而意图未落盘时崩溃 = 丢 claimId
8
+ * 3. createGroupWithBots ← **held**:不传 kickoffBot/kickoffPrompt,群里没有 agent 在跑
9
+ * 4. onChatCreated → createBinding + 回填意图 ← durable boundary
10
+ * 5. platform bind(localTaskRef)
11
+ * 6. kickoff(@bot)→ activate ← 到这一步 daemon 才真的起会话
12
+ * 7. clearClaimIntent ← 必须晚于 binding 落盘
13
+ *
14
+ * 每一步都可能崩,且崩了都不该产生「群在跑但平台不知道」或「平台以为在跑但本机没有」的
15
+ * 不一致。这里的做法是:**任何一步失败都不回滚已完成的持久化**,而是把状态留在可对账的
16
+ * 形态上(意图在 / binding pending / binding void),交给启动对账收拾。回滚才是危险的
17
+ * ——撤销远端副作用本身也会失败,只会把状态搅得更不可辨认。
18
+ *
19
+ * 依赖全部注入:单测里不碰真实飞书、不打真实平台。
20
+ */
21
+ import { randomBytes } from 'node:crypto';
22
+ import { buildIssueKickoffCard } from '../im/lark/issue-card.js';
23
+ import { buildLocalTaskRef, clearClaimIntent, createBinding, findActiveBindingByIssue, getBinding, recordClaimIntent, updateBinding, updateClaimIntent, } from './issue-board-store.js';
24
+ import { projectStatus } from './issue-status-writer.js';
25
+ /**
26
+ * 群名里带的领取标记。对账时靠它把「平台上有 claim、本地没有 binding」的孤儿群认回来
27
+ * ——本地意图是一条腿,这个标记是另一条腿,少哪条都会留下认不出的群。
28
+ *
29
+ * 只取前 8 位十六进制(32 bit):群名是给人看的,不该被一串 32 字符的随机数占满;
30
+ * 同时在手可数的候选群里,32 bit 足以唯一定位。
31
+ */
32
+ export function claimMarker(claimId) {
33
+ return `#${claimId.slice(0, 8)}`;
34
+ }
35
+ export function matchesClaimMarker(groupName, claimId) {
36
+ return groupName.includes(claimMarker(claimId));
37
+ }
38
+ /** 群名长度上限。与本仓其它建群点一致(command-handler 的 MAX_NAME / FORK_MAX_NAME)。 */
39
+ const GROUP_NAME_MAX = 50;
40
+ /**
41
+ * 拼领取群名:`<标题> #<claimId 前 8 位>`,**标题被截、标记一定完整**。
42
+ *
43
+ * 不截的话飞书自己会截,而它从尾部截——先吃掉的正是尾部那个 `#claimId` 标记,也就是双通道
44
+ * 反查的一条腿。那条腿断了不会报错:对账时只是"这个群不像是任何一次领取的",然后被当成
45
+ * 无主群跳过。宁可标题短一点。
46
+ *
47
+ * 标记放尾部而不是挪到前面:群列表里先看到的应该是任务在说什么,一串十六进制不配占开头。
48
+ * 长度自己控住了,位置就不再是风险。
49
+ */
50
+ export function claimGroupName(title, claimId) {
51
+ const marker = claimMarker(claimId);
52
+ const room = GROUP_NAME_MAX - marker.length - 1; // 1 = 中间那个空格
53
+ const trimmed = title.trim();
54
+ const head = trimmed.length > room ? `${trimmed.slice(0, Math.max(0, room - 1))}…` : trimmed;
55
+ return `${head} ${marker}`;
56
+ }
57
+ /**
58
+ * 平台上这条 issue 的详情深链。
59
+ *
60
+ * 路由形状来自平台前端:tab 在 hash(`#issues`)、详情在 query(`?issue=<id>`),见
61
+ * platform 仓 `frontend/src/App.tsx` 的 Tabs 与 `IssueBoard.tsx` 的 `openDetail`。
62
+ * 拼不出来(平台地址为空/非法)就返回 undefined,卡片少一个按钮而已,不该因此报错。
63
+ */
64
+ export function issueDetailUrl(platformBaseUrl, issueId) {
65
+ const base = (platformBaseUrl ?? '').trim().replace(/\/+$/, '');
66
+ if (!base)
67
+ return undefined;
68
+ try {
69
+ new URL(base);
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ return `${base}/?issue=${encodeURIComponent(issueId)}#issues`;
75
+ }
76
+ export async function claimIssueIntoGroup(deps, args) {
77
+ const newClaimId = deps.newClaimId ?? (() => randomBytes(16).toString('hex'));
78
+ const now = deps.now ?? Date.now;
79
+ // 先查本地:同一个 issue 已经有活跃 binding 就直接拦住。放到 store 里抛也拦得住,
80
+ // 但那是最后一道;在这里挡住可以连平台 claim 都不发,不白占一次代次。
81
+ const existing = findActiveBindingByIssue(deps.dataDir, args.issue._id);
82
+ if (existing) {
83
+ return { ok: false, stage: 'claim', reason: 'already_claimed_locally', alreadyLocal: existing };
84
+ }
85
+ // ── 1. claim ──────────────────────────────────────────────────────────────
86
+ const claimId = newClaimId();
87
+ const claimed = await deps.claim(args.issue._id, {
88
+ claimId,
89
+ agent: args.agentLabel || args.larkAppId,
90
+ ...(args.issue.targetRepoLabel ? { repoLabel: args.issue.targetRepoLabel } : {}),
91
+ expectedStateRev: args.issue.stateRev,
92
+ });
93
+ if (!claimed.ok)
94
+ return { ok: false, stage: 'claim', reason: claimed.reason };
95
+ const claimEpoch = claimed.value.claim.claimEpoch;
96
+ let stateRev = claimed.value.issue.stateRev;
97
+ // ── 2. 意图落盘(早于任何建群动作)──────────────────────────────────────
98
+ recordClaimIntent(deps.dataDir, {
99
+ claimId,
100
+ issueId: args.issue._id,
101
+ teamId: args.teamId,
102
+ claimEpoch,
103
+ platformBaseUrl: deps.platformBaseUrl,
104
+ platformStateRev: stateRev,
105
+ larkAppId: args.larkAppId,
106
+ scope: 'chat',
107
+ }, now());
108
+ // ── 3~4. 建群(held)+ onChatCreated 里写 binding ────────────────────────
109
+ //
110
+ // 群名带 claimId 标记,与本地意图构成双通道反查。
111
+ // 刻意**不传** kickoffBotLarkAppId/kickoffPrompt:群里没有 bot 被 @ 起来,daemon 不会
112
+ // 创建会话。这是「群已建、binding 未写」那个窗口的兜底——孤儿群里没有 agent 在跑。
113
+ let binding = null;
114
+ let chatId = '';
115
+ let shareLink;
116
+ const groupName = claimGroupName(args.issue.title, claimId);
117
+ const botIds = Array.from(new Set([args.larkAppId, ...(args.peerLarkAppIds ?? [])]));
118
+ try {
119
+ const group = await deps.createGroup({
120
+ creatorLarkAppId: args.creatorLarkAppId,
121
+ larkAppIds: botIds,
122
+ name: groupName,
123
+ bindWorkingDir: args.workingDir,
124
+ ...(args.ownerUnionIds?.length ? { ownerUnionIds: args.ownerUnionIds } : {}),
125
+ onChatCreated: (createdChatId) => {
126
+ // 同步钩子:createGroupWithBots 返回前这两次写就已经落盘。后面的邀人/转让/
127
+ // 分享链接都是 best-effort,就算挂住或失败,这个群也已经认得回来了。
128
+ chatId = createdChatId;
129
+ updateClaimIntent(deps.dataDir, claimId, { anchorId: createdChatId, chatId: createdChatId }, now());
130
+ binding = createBinding(deps.dataDir, {
131
+ anchorId: createdChatId,
132
+ larkAppId: args.larkAppId,
133
+ scope: 'chat',
134
+ issueId: args.issue._id,
135
+ teamId: args.teamId,
136
+ platformBaseUrl: deps.platformBaseUrl,
137
+ claimId,
138
+ claimEpoch,
139
+ chatId: createdChatId,
140
+ }, now());
141
+ },
142
+ });
143
+ chatId = group.chatId || chatId;
144
+ shareLink = group.shareLink ?? undefined;
145
+ }
146
+ catch (e) {
147
+ return { ok: false, stage: 'group', reason: String(e?.message ?? e), claimId };
148
+ }
149
+ if (!binding || !chatId) {
150
+ return { ok: false, stage: 'group', reason: 'chat_created_but_binding_missing', claimId };
151
+ }
152
+ const bound = binding;
153
+ // ── 5. bind ───────────────────────────────────────────────────────────────
154
+ // 一并把「这个本地任务长什么样」告诉平台:localTaskRef 是机器标识(`oc_xxx::cli_xxx`),
155
+ // 平台详情页直接渲染它等于给人看一串 ID。群名 + applink 让人一眼认出是哪个群、点一下就能
156
+ // 进去。两个都是纯展示、可缺省(分享链接是 best-effort 拿的,拿不到就只显示群名)。
157
+ const bindRes = await deps.bind(args.issue._id, {
158
+ claimId,
159
+ localTaskRef: buildLocalTaskRef(chatId, args.larkAppId),
160
+ expectedStateRev: stateRev,
161
+ localTaskLabel: groupName,
162
+ ...(shareLink ? { localTaskUrl: shareLink } : {}),
163
+ });
164
+ if (!bindRes.ok) {
165
+ // 平台拒绝 = 这条领取作废(issue 被回收 / 别人领走 / 代次过期)。置 void 而不是删:
166
+ // 群还在,留着记录才知道那个群是哪次失败领取的产物。
167
+ const voided = updateBinding(deps.dataDir, bound.anchorId, { bindState: 'void' }, now()) ?? bound;
168
+ return { ok: false, stage: 'bind', reason: bindRes.reason, binding: voided, chatId };
169
+ }
170
+ stateRev = bindRes.value.issue.stateRev;
171
+ const readyBinding = updateBinding(deps.dataDir, bound.anchorId, { bindState: 'bound', platformStateRev: stateRev }, now()) ?? bound;
172
+ // ── 5.5 开工播报 ──────────────────────────────────────────────────────────
173
+ //
174
+ // 放在 activate **之前**:kickoff prompt 是内部投递的,群里看不到;这张卡是人进群后
175
+ // 唯一能看到的「在干什么」。先播报,agent 的输出随后跟上,顺序读起来才对。
176
+ // activate 万一失败,群里至少还留着任务说明,不是一个空群。
177
+ //
178
+ // try/catch 吞掉异常:播报纯展示,不该有能力弄砸一次已经 bind 成功的领取。
179
+ if (deps.announce) {
180
+ try {
181
+ await deps.announce(chatId, buildIssueKickoffCard({
182
+ title: args.issue.title,
183
+ ...(args.issue.body ? { body: args.issue.body } : {}),
184
+ workingDir: args.workingDir,
185
+ issueId: args.issue._id,
186
+ ...(issueDetailUrl(deps.platformBaseUrl, args.issue._id)
187
+ ? { issueUrl: issueDetailUrl(deps.platformBaseUrl, args.issue._id) }
188
+ : {}),
189
+ }));
190
+ }
191
+ catch (e) {
192
+ deps.onAnnounceError?.(String(e?.message ?? e));
193
+ }
194
+ }
195
+ // ── 6. activate ───────────────────────────────────────────────────────────
196
+ let kickoffMessageId;
197
+ try {
198
+ kickoffMessageId = await deps.activate(chatId, args.larkAppId, args.kickoffPrompt);
199
+ }
200
+ catch (e) {
201
+ // binding 已经是 bound,平台也知道 localTaskRef,重试 activate 即可,别重建群。
202
+ return { ok: false, stage: 'activate', reason: String(e?.message ?? e), binding: readyBinding, chatId };
203
+ }
204
+ // ── 6.5 投影 in_progress(拆掉平台的 activation lease 引信)──────────────
205
+ //
206
+ // 平台 sweeper 只扫 `status=claimed` 且租约过期的任务;进了 in_progress 就不再按 5 分钟
207
+ // activation lease 回收。必须在 activate 成功后**立刻**推:晚一步 agent 就可能被打成
208
+ // needs_attention(claim_activate_timeout),而那是条单向门——之后既回不到在跑、也交付不了。
209
+ //
210
+ // 走 projectStatus 而不是自己拼一次发送:那边带 409 对账(bind 到 activate 之间隔着几秒,
211
+ // 期间任何人动一下这条 issue,stateRev 就变了)。发不出去也不推翻领取——binding 已 bound、
212
+ // 行留在发件箱里,[[issue-outbox-pump]] 会接着重投。
213
+ const projected = await projectStatus(deps, readyBinding.anchorId, 'in_progress');
214
+ if (!projected.ok && projected.reason === 'platform') {
215
+ deps.onStatusError?.(projected.detail);
216
+ }
217
+ const finalBinding = getBinding(deps.dataDir, readyBinding.anchorId) ?? readyBinding;
218
+ // ── 7. 意图退休(晚于 binding 落盘)───────────────────────────────────────
219
+ clearClaimIntent(deps.dataDir, claimId);
220
+ return { ok: true, binding: finalBinding, chatId, kickoffMessageId };
221
+ }
222
+ //# sourceMappingURL=issue-claim-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-claim-flow.js","sourceRoot":"","sources":["../../src/services/issue-claim-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { type StatusWriterDeps } from './issue-status-writer.js';
2
+ export interface OutboxPumpOptions {
3
+ dataDir: string;
4
+ intervalMs?: number;
5
+ /** 注入发送侧(测试用)。缺省接真实平台客户端。 */
6
+ writer?: Omit<StatusWriterDeps, 'dataDir'>;
7
+ /** 平台是否已绑定。未绑定时整个 issue 功能是关的,空转没意义。 */
8
+ isBound?: () => boolean;
9
+ }
10
+ /**
11
+ * 跑一轮:把每个活跃 binding 的下一条待发行发出去。
12
+ *
13
+ * 每个 binding 每轮**只发一条**——同一 issue 要求 sourceSeq 单调到达,串行是硬约束;
14
+ * 一次多发只会让顺序乱掉被平台静默丢弃。积压多条时下一轮继续,30s 一条对人看的看板足够。
15
+ *
16
+ * 返回这一轮发出去的条数,便于日志与测试断言。
17
+ */
18
+ export declare function pumpOnce(opts: OutboxPumpOptions): Promise<number>;
19
+ /**
20
+ * 启动后台泵。返回 stop 函数。
21
+ *
22
+ * 启动时先 `resetInflightToPending` 解卡(见文件头),再起定时器。
23
+ */
24
+ export declare function startIssueOutboxPump(opts: OutboxPumpOptions): () => void;
25
+ //# sourceMappingURL=issue-outbox-pump.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-outbox-pump.d.ts","sourceRoot":"","sources":["../../src/services/issue-outbox-pump.ts"],"names":[],"mappings":"AAmCA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAKlF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;IAC3C,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCvE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAsBxE"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * 发件箱后台泵:把 [[issue-board-store]] 里积压的状态回写真的发出去。
3
+ *
4
+ * ## 没有它的话,那些「留待重投」的注释全是假的
5
+ *
6
+ * 回写失败时各处都写着「行留在 outbox 里,pump 会接着重投」——在这个文件存在之前,**没有
7
+ * 任何东西会重投**。后果不是"晚一点同步",而是不可恢复:
8
+ *
9
+ * - `in_progress` 那一次写只要失败(网络抖动、平台 502、一次 409),5 分钟后平台 sweeper
10
+ * 就把任务打成 `needs_attention(claim_activate_timeout)`,而那是**单向门**——平台只放行
11
+ * `task_blocked` 恢复成 in_progress,超时的只能 open/reopened(都清 claim)。群里的活废了。
12
+ * - `in_review` 交付失败 → 验收的人永远等不到。
13
+ *
14
+ * 所以这个泵不是"优化吞吐",是让那条防护路径真的闭环。
15
+ *
16
+ * ## 启动先解卡:inflight 是会永久堵死的
17
+ *
18
+ * `claimNextOutboxRow` 见到 inflight 就返回 null(同一 issue 的 sourceSeq 必须单调到达,
19
+ * 所以必须串行)。进程若在「标了 inflight、还没记下响应」之间挂掉,那一行**永远**停在
20
+ * inflight:此后该 binding 的所有回写都发不出去,`botmux report` / `/issue release` 每次
21
+ * 都回「稍后重试」而永远不会好。
22
+ *
23
+ * `resetInflightToPending` 就是为这个写的(store 里连注释都备好了),但一直没人调。启动时
24
+ * 无脑退回即可——平台的 sourceSeq 单调 + 终态幂等保证重复投递是安全的,不需要发送租约。
25
+ */
26
+ import { logger } from '../utils/logger.js';
27
+ import { readPlatformBinding } from '../platform/binding.js';
28
+ import { findIssueById, writeIssueStatus } from '../platform/issue-client.js';
29
+ import { isActiveBindState, listBindings, listOutbox, pruneOutbox, resetInflightToPending, } from './issue-board-store.js';
30
+ import { flushNextStatus } from './issue-status-writer.js';
31
+ /** 扫描间隔。回写不是实时通道(平台那边是人在看板上看),30s 足够;退避本身也在行上。 */
32
+ const PUMP_INTERVAL_MS = 30_000;
33
+ /**
34
+ * 跑一轮:把每个活跃 binding 的下一条待发行发出去。
35
+ *
36
+ * 每个 binding 每轮**只发一条**——同一 issue 要求 sourceSeq 单调到达,串行是硬约束;
37
+ * 一次多发只会让顺序乱掉被平台静默丢弃。积压多条时下一轮继续,30s 一条对人看的看板足够。
38
+ *
39
+ * 返回这一轮发出去的条数,便于日志与测试断言。
40
+ */
41
+ export async function pumpOnce(opts) {
42
+ const deps = {
43
+ dataDir: opts.dataDir,
44
+ writeStatus: opts.writer?.writeStatus ?? ((issueId, args) => writeIssueStatus(issueId, args)),
45
+ fetchIssue: opts.writer?.fetchIssue ?? ((teamId, issueId) => findIssueById(teamId, issueId)),
46
+ ...(opts.writer?.now ? { now: opts.writer.now } : {}),
47
+ };
48
+ let sent = 0;
49
+ for (const binding of listBindings(opts.dataDir)) {
50
+ // 终态 binding(void/released)不再回写:它的 issue 已经不归本机管,发过去只会被拒。
51
+ if (!isActiveBindState(binding.bindState))
52
+ continue;
53
+ // 先看有没有到期的待发行,没有就不进 flush——避免每轮对每个 binding 都读一遍文件。
54
+ const due = listOutbox(opts.dataDir, binding.anchorId).some((r) => r.state === 'pending' && (r.nextRetryAt ?? 0) <= Date.now());
55
+ if (!due)
56
+ continue;
57
+ const r = await flushNextStatus(deps, binding.anchorId);
58
+ if (r.ok) {
59
+ sent += 1;
60
+ logger.info(`[issue] 发件箱重投成功 issue=${binding.issueId} ${r.applied ? `→ ${r.issue.status}` : '(平台已不认本机 claim,就此结算)'}`);
61
+ }
62
+ else if (r.reason === 'platform') {
63
+ // permanent = 平台明确拒绝(凭证失效 / issue 已删或归档),行已标 fatal 不再重投。
64
+ // 用 error 而不是 warn:它不会自愈,等人去看;`/issue status` 上也会显示这一条。
65
+ if (r.permanent) {
66
+ logger.error(`[issue] 发件箱回写被平台拒绝、已放弃重投 issue=${binding.issueId}: ${r.detail}`);
67
+ }
68
+ else {
69
+ logger.warn(`[issue] 发件箱重投失败 issue=${binding.issueId}: ${r.detail}`);
70
+ }
71
+ }
72
+ }
73
+ return sent;
74
+ }
75
+ /**
76
+ * 启动后台泵。返回 stop 函数。
77
+ *
78
+ * 启动时先 `resetInflightToPending` 解卡(见文件头),再起定时器。
79
+ */
80
+ export function startIssueOutboxPump(opts) {
81
+ const isBound = opts.isBound ?? (() => !!readPlatformBinding());
82
+ const requeued = resetInflightToPending(opts.dataDir);
83
+ if (requeued > 0) {
84
+ logger.info(`[issue] 启动对账:${requeued} 条卡在 inflight 的回写已退回待发`);
85
+ }
86
+ const tick = () => {
87
+ if (!isBound())
88
+ return;
89
+ void pumpOnce(opts)
90
+ .then((n) => {
91
+ // 顺手做一次发件箱清理,别让 done 行无限增长。放在有实际发送的那一轮,
92
+ // 避免空闲时也反复重写文件。
93
+ if (n > 0)
94
+ pruneOutbox(opts.dataDir);
95
+ })
96
+ .catch((e) => logger.warn(`[issue] 发件箱泵异常:${String(e?.message ?? e)}`));
97
+ };
98
+ const timer = setInterval(tick, opts.intervalMs ?? PUMP_INTERVAL_MS);
99
+ timer.unref?.();
100
+ return () => clearInterval(timer);
101
+ }
102
+ //# sourceMappingURL=issue-outbox-pump.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-outbox-pump.js","sourceRoot":"","sources":["../../src/services/issue-outbox-pump.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 结束一次领取。两种结局,共用一套写法:
3
+ * - **释放**(`/issue release`)→ 平台 `open`,任务退回待领取池,本地 `released`
4
+ * - **验收完成**(`/issue done`)→ 平台 `done`,任务终结,本地 `done`
5
+ *
6
+ * 放在同一个文件是因为它们在协议层是**同一件事**:平台侧只有 `open`/`reopened`/`done` 会
7
+ * 清 claim(`clearsClaim`),所以两者都是「平台先行、本地后写」加同一套 409 判据,差别只在
8
+ * 目标状态与本地终态。分成两份实现的话,那套顺序与 409 语义就得维护两遍——这正是本仓已经
9
+ * 踩过一次的坑(三份回写实现,缺的那份恰好是最要命的)。
10
+ *
11
+ * 释放是 [[issue-claim-flow]] 的逆操作,但**不是回滚**——领取过程中的失败靠「留下可对账的
12
+ * 状态」收拾(见那边的注释),这里处理的是完全不同的场景:领取成功、干到一半、人决定不做了。
13
+ *
14
+ * ## 为什么释放写 `open` 而不是别的
15
+ *
16
+ * 三个会清 claim 的目标里,`open` 是唯一语义正确的:任务没做完,退回去让别人领。`done` 是
17
+ * 撒谎,`reopened` 是"做完又被打回"。
18
+ *
19
+ * ## 平台先行,本地后写
20
+ *
21
+ * 顺序是**先让平台确认清掉 claim,再改本地 binding**,不能反:
22
+ * - 先本地后平台:中间崩溃 → 本机认为已释放(群不再回写)、平台仍记着这台机器持有,
23
+ * 直到 lease 到期才回收。这段时间 issue 卡在 in_progress,谁也领不走。
24
+ * - 先平台后本地:中间崩溃 → 平台已放开可以被别人领,本机 binding 还在。下次回写会被
25
+ * 平台按 claim 不匹配拒掉(stale_epoch / claim_mismatch),是**响的**失败,能查。
26
+ *
27
+ * 宁可留一条会报错的本地记录,也不要留一个静默卡死的平台状态。
28
+ *
29
+ * ## 409 的两种含义要分开
30
+ *
31
+ * 撞 409 时必须先弄清楚"平台还认不认我这个 claim",两种情况的正确动作相反:
32
+ * - **stateRev 过期**(别人改了这条 issue)→ claim 还是我的,拿新 stateRev 重发一次即可
33
+ * - **claim 已经不是我的**(平台上被 force-detach / lease 过期 / 别人领走)→ 平台早就
34
+ * 释放了,这里只需把本地补记成 released。这正是"我想撒手但平台一直拒绝我"的死结场景,
35
+ * 不处理的话人只能去手改 JSON。
36
+ */
37
+ import { type IssueBinding } from './issue-board-store.js';
38
+ import { type StatusWriterDeps } from './issue-status-writer.js';
39
+ /** 发送侧完全复用 [[issue-status-writer]]:回写只有一条路径。 */
40
+ export type ReleaseDeps = StatusWriterDeps;
41
+ export type ReleaseResult = {
42
+ ok: true;
43
+ binding: IssueBinding;
44
+ issueId: string;
45
+ alreadyReleasedOnPlatform: boolean;
46
+ }
47
+ /** 这个锚点上没有 issue 绑定——不是错误,只是没什么可释放的。 */
48
+ | {
49
+ ok: false;
50
+ reason: 'no_binding';
51
+ }
52
+ /** 已经是终态(释放过 / 作废 / 已验收完成)。幂等,不重复打平台。终态是哪一种看 `binding.bindState`。 */
53
+ | {
54
+ ok: false;
55
+ reason: 'already_released';
56
+ binding: IssueBinding;
57
+ }
58
+ /** 平台拒绝且 claim 仍归本机所有 —— 本地**不改**。`permanent` 时重试也不会好(见
59
+ * [[issue-client]] 的 isPermanentFailure),调用方别再劝人"稍后再试一次"。 */
60
+ | {
61
+ ok: false;
62
+ reason: 'platform';
63
+ detail: string;
64
+ binding: IssueBinding;
65
+ permanent?: boolean;
66
+ };
67
+ /** 释放 `anchorId` 上绑定的 issue:平台退回 `open`,本地记 `released`。 */
68
+ export declare function releaseIssue(deps: ReleaseDeps, anchorId: string): Promise<ReleaseResult>;
69
+ /**
70
+ * 验收完成 `anchorId` 上绑定的 issue:平台 `done`,本地记 `done`。
71
+ *
72
+ * ⚠️ 平台的追赶白名单里 `needs_attention` **不能**直接到 `done`(只放行 in_progress /
73
+ * open / reopened),所以对一条已经掉进「需要关注」的任务发 `/issue done` 会拿到
74
+ * `invalid_transition`。这不是 bug 是设计:出过状况的任务得先在平台上处理干净,不能靠
75
+ * 本地一句命令抹平。调用方需要把这个原因原样说给人听。
76
+ */
77
+ export declare function completeIssue(deps: ReleaseDeps, anchorId: string): Promise<ReleaseResult>;
78
+ //# sourceMappingURL=issue-release.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-release.d.ts","sourceRoot":"","sources":["../../src/services/issue-release.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EAKL,KAAK,YAAY,EAElB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAiB,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEhF,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAE3C,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,yBAAyB,EAAE,OAAO,CAAA;CAAE;AAC1F,wCAAwC;GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE;AACrC,qEAAqE;GACnE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE;AAClE;8DAC8D;GAC5D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA+ClG,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAExF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAEzF"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * 结束一次领取。两种结局,共用一套写法:
3
+ * - **释放**(`/issue release`)→ 平台 `open`,任务退回待领取池,本地 `released`
4
+ * - **验收完成**(`/issue done`)→ 平台 `done`,任务终结,本地 `done`
5
+ *
6
+ * 放在同一个文件是因为它们在协议层是**同一件事**:平台侧只有 `open`/`reopened`/`done` 会
7
+ * 清 claim(`clearsClaim`),所以两者都是「平台先行、本地后写」加同一套 409 判据,差别只在
8
+ * 目标状态与本地终态。分成两份实现的话,那套顺序与 409 语义就得维护两遍——这正是本仓已经
9
+ * 踩过一次的坑(三份回写实现,缺的那份恰好是最要命的)。
10
+ *
11
+ * 释放是 [[issue-claim-flow]] 的逆操作,但**不是回滚**——领取过程中的失败靠「留下可对账的
12
+ * 状态」收拾(见那边的注释),这里处理的是完全不同的场景:领取成功、干到一半、人决定不做了。
13
+ *
14
+ * ## 为什么释放写 `open` 而不是别的
15
+ *
16
+ * 三个会清 claim 的目标里,`open` 是唯一语义正确的:任务没做完,退回去让别人领。`done` 是
17
+ * 撒谎,`reopened` 是"做完又被打回"。
18
+ *
19
+ * ## 平台先行,本地后写
20
+ *
21
+ * 顺序是**先让平台确认清掉 claim,再改本地 binding**,不能反:
22
+ * - 先本地后平台:中间崩溃 → 本机认为已释放(群不再回写)、平台仍记着这台机器持有,
23
+ * 直到 lease 到期才回收。这段时间 issue 卡在 in_progress,谁也领不走。
24
+ * - 先平台后本地:中间崩溃 → 平台已放开可以被别人领,本机 binding 还在。下次回写会被
25
+ * 平台按 claim 不匹配拒掉(stale_epoch / claim_mismatch),是**响的**失败,能查。
26
+ *
27
+ * 宁可留一条会报错的本地记录,也不要留一个静默卡死的平台状态。
28
+ *
29
+ * ## 409 的两种含义要分开
30
+ *
31
+ * 撞 409 时必须先弄清楚"平台还认不认我这个 claim",两种情况的正确动作相反:
32
+ * - **stateRev 过期**(别人改了这条 issue)→ claim 还是我的,拿新 stateRev 重发一次即可
33
+ * - **claim 已经不是我的**(平台上被 force-detach / lease 过期 / 别人领走)→ 平台早就
34
+ * 释放了,这里只需把本地补记成 released。这正是"我想撒手但平台一直拒绝我"的死结场景,
35
+ * 不处理的话人只能去手改 JSON。
36
+ */
37
+ import { clearClaimIntent, getBinding, isActiveBindState, updateBinding, } from './issue-board-store.js';
38
+ import { projectStatus } from './issue-status-writer.js';
39
+ /**
40
+ * 结束一次领取的共同实现:投影终态 → 平台确认 → 才改本地 bindState。
41
+ *
42
+ * 走 outbox 而不是裸调 `writeStatus`:`enqueueDesiredStatus` 是 sourceSeq 的**唯一**分配
43
+ * 入口(含落后自愈,见那边注释),绕过它自己编号迟早会撞上平台的静默去重。发送失败时行留在
44
+ * outbox 里,[[issue-outbox-pump]] 会接着重投——不会因为一次网络抖动就丢。
45
+ */
46
+ async function settleTerminal(deps, anchorId, target, localTerminal) {
47
+ const now = deps.now ?? Date.now;
48
+ const binding = getBinding(deps.dataDir, anchorId);
49
+ if (!binding)
50
+ return { ok: false, reason: 'no_binding' };
51
+ if (!isActiveBindState(binding.bindState)) {
52
+ return { ok: false, reason: 'already_released', binding };
53
+ }
54
+ const r = await projectStatus(deps, anchorId, target);
55
+ // `idle` = 发件箱里没有待发行且 lastSyncedStatus 已经是目标态 —— 上一次发成功了、只是崩在
56
+ // 改本地 binding 之前("平台先行、本地后写"留下的可恢复形态)。补完本地那一半。
57
+ // `r.ok && !r.applied` = 409 对账发现 claim 已不归本机:平台侧这条早就结束了,同样只需补本地。
58
+ const alreadySettledOnPlatform = (r.ok && !r.applied) || (!r.ok && r.reason === 'idle' && binding.lastSyncedStatus === target);
59
+ if (!r.ok && !alreadySettledOnPlatform) {
60
+ // 平台还认为这台机器持有,本机也得继续这么认,否则就是分裂。
61
+ const detail = r.reason === 'busy'
62
+ ? 'outbox_busy(上一条回写还在发送中,稍后重试)'
63
+ : r.reason === 'platform' ? r.detail : r.reason;
64
+ const permanent = !r.ok && r.reason === 'platform' && r.permanent === true;
65
+ return { ok: false, reason: 'platform', detail, binding, ...(permanent ? { permanent } : {}) };
66
+ }
67
+ // 平台已确认,现在才动本地。
68
+ const settled = updateBinding(deps.dataDir, anchorId, { bindState: localTerminal }, now()) ?? binding;
69
+ // 领取意图正常情况下在领取成功时就清了;这里兜底清一次,免得对账把一条已结束的领取
70
+ // 当成悬空领取再去补建群。
71
+ clearClaimIntent(deps.dataDir, binding.claimId);
72
+ return { ok: true, binding: settled, issueId: binding.issueId, alreadyReleasedOnPlatform: alreadySettledOnPlatform };
73
+ }
74
+ /** 释放 `anchorId` 上绑定的 issue:平台退回 `open`,本地记 `released`。 */
75
+ export function releaseIssue(deps, anchorId) {
76
+ return settleTerminal(deps, anchorId, 'open', 'released');
77
+ }
78
+ /**
79
+ * 验收完成 `anchorId` 上绑定的 issue:平台 `done`,本地记 `done`。
80
+ *
81
+ * ⚠️ 平台的追赶白名单里 `needs_attention` **不能**直接到 `done`(只放行 in_progress /
82
+ * open / reopened),所以对一条已经掉进「需要关注」的任务发 `/issue done` 会拿到
83
+ * `invalid_transition`。这不是 bug 是设计:出过状况的任务得先在平台上处理干净,不能靠
84
+ * 本地一句命令抹平。调用方需要把这个原因原样说给人听。
85
+ */
86
+ export function completeIssue(deps, anchorId) {
87
+ return settleTerminal(deps, anchorId, 'done', 'done');
88
+ }
89
+ //# sourceMappingURL=issue-release.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issue-release.js","sourceRoot":"","sources":["../../src/services/issue-release.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * issue 交付:agent 在领取群里跑完后执行 `botmux report`,把平台 issue 推到 `in_review`(待验收)。
3
+ *
4
+ * 这是 kickoff 正文里那句「完成后执行 botmux report 交付」的落地实现——见
5
+ * [[issue-command-deps]] 的 buildKickoffPrompt。平台生命周期:
6
+ * claimed → in_progress → **in_review** → done
7
+ * (`/status` 允许正向追赶,所以 claimed 也可直接到 in_review。)
8
+ *
9
+ * ## 为什么不复用 dispatch 的 report 路径
10
+ *
11
+ * `botmux report` 原本只服务「多话题协作」:把回报发回主编排会话。领取群是 chat-scope
12
+ * 会话,没有 creatorOpenId / dispatch 注册表,那条路会硬失败。issue 交付的「验收人」
13
+ * 在平台看板,不在飞书主编排话题,所以这里走 binding → outbox → `/status`,与
14
+ * [[issue-release]] 同一套发件箱。
15
+ *
16
+ * ## 平台先行,本地 lastSynced 后写
17
+ *
18
+ * 与 release 相同:先让平台确认 `in_review`,再 settle outbox。中间崩溃最多多发一次
19
+ * (sourceSeq 幂等),不会出现「本机以为已交付、平台还停在 claimed」。
20
+ *
21
+ * 与 release 的差异:交付**不**改 bindState——claim 仍归本机,等人在平台上 accept/reject。
22
+ */
23
+ import { type IssueBinding } from './issue-board-store.js';
24
+ import { type StatusWriterDeps } from './issue-status-writer.js';
25
+ /** 发送侧复用 [[issue-status-writer]]:回写只有一条路径(409 对账、退避、串行都在那边)。 */
26
+ export type ReportDeps = StatusWriterDeps;
27
+ export type ReportIssueResult = {
28
+ ok: true;
29
+ binding: IssueBinding;
30
+ issueId: string;
31
+ alreadyInReview: boolean;
32
+ } | {
33
+ ok: false;
34
+ reason: 'no_binding';
35
+ } | {
36
+ ok: false;
37
+ reason: 'not_active';
38
+ binding: IssueBinding;
39
+ }
40
+ /** 平台上这条 claim 已经不归本机(force-detach / 租约过期 / 被别人领走)。交付**没有**落地,
41
+ * 而且重试也不会落地——单列一类,别混进 platform 让人以为再跑一次就好。 */
42
+ | {
43
+ ok: false;
44
+ reason: 'detached';
45
+ binding: IssueBinding;
46
+ }
47
+ /** `permanent` 时重试也不会好(平台 401/403/400/404),别劝人再跑一次 report。 */
48
+ | {
49
+ ok: false;
50
+ reason: 'platform';
51
+ detail: string;
52
+ binding: IssueBinding;
53
+ permanent?: boolean;
54
+ };
55
+ /**
56
+ * 从会话候选锚点里找活跃 issue binding。
57
+ *
58
+ * 拉群领取的 binding 主键是 chatId;话题领取(若将来支持)主键是 rootMessageId。
59
+ * 两个候选都查一遍,先命中活跃 binding 的为准。
60
+ */
61
+ export declare function findActiveBindingForSession(dataDir: string, session: {
62
+ chatId?: string;
63
+ rootMessageId?: string;
64
+ }): IssueBinding | null;
65
+ /**
66
+ * 把 `anchorId` 上绑定的 issue 推到平台 `in_review`。
67
+ *
68
+ * 走 outbox:`enqueueDesiredStatus` 是 sourceSeq 唯一分配入口。发送失败时行留在
69
+ * pending,可重试同一 `botmux report`。
70
+ */
71
+ export declare function reportIssueInReview(deps: ReportDeps, anchorId: string): Promise<ReportIssueResult>;
72
+ //# sourceMappingURL=issue-report.d.ts.map