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.
- package/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +7 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +20 -0
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +12 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +40 -7
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +87 -87
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/cli/deferred-topic-send.d.ts +11 -0
- package/dist/cli/deferred-topic-send.d.ts.map +1 -1
- package/dist/cli/deferred-topic-send.js +21 -4
- package/dist/cli/deferred-topic-send.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +177 -38
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +40 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dispatch.d.ts +43 -5
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +104 -6
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +1 -1
- package/dist/core/passthrough-commands.js.map +1 -1
- package/dist/daemon.d.ts +11 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +32 -3
- package/dist/daemon.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +2 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +9 -0
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/issue-card.d.ts +223 -0
- package/dist/im/lark/issue-card.d.ts.map +1 -0
- package/dist/im/lark/issue-card.js +542 -0
- package/dist/im/lark/issue-card.js.map +1 -0
- package/dist/im/lark/issue-command-deps.d.ts +41 -0
- package/dist/im/lark/issue-command-deps.d.ts.map +1 -0
- package/dist/im/lark/issue-command-deps.js +195 -0
- package/dist/im/lark/issue-command-deps.js.map +1 -0
- package/dist/im/lark/issue-command.d.ts +137 -0
- package/dist/im/lark/issue-command.d.ts.map +1 -0
- package/dist/im/lark/issue-command.js +335 -0
- package/dist/im/lark/issue-command.js.map +1 -0
- package/dist/platform/issue-client.d.ts +201 -0
- package/dist/platform/issue-client.d.ts.map +1 -0
- package/dist/platform/issue-client.js +142 -0
- package/dist/platform/issue-client.js.map +1 -0
- package/dist/platform/platform-http.d.ts +4 -0
- package/dist/platform/platform-http.d.ts.map +1 -1
- package/dist/platform/platform-http.js +38 -2
- package/dist/platform/platform-http.js.map +1 -1
- package/dist/services/codex-composer-state.d.ts +27 -0
- package/dist/services/codex-composer-state.d.ts.map +1 -0
- package/dist/services/codex-composer-state.js +53 -0
- package/dist/services/codex-composer-state.js.map +1 -0
- package/dist/services/codex-transcript.d.ts +33 -12
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js +82 -20
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/services/issue-board-store.d.ts +214 -0
- package/dist/services/issue-board-store.d.ts.map +1 -0
- package/dist/services/issue-board-store.js +472 -0
- package/dist/services/issue-board-store.js.map +1 -0
- package/dist/services/issue-claim-flow.d.ts +160 -0
- package/dist/services/issue-claim-flow.d.ts.map +1 -0
- package/dist/services/issue-claim-flow.js +222 -0
- package/dist/services/issue-claim-flow.js.map +1 -0
- package/dist/services/issue-outbox-pump.d.ts +25 -0
- package/dist/services/issue-outbox-pump.d.ts.map +1 -0
- package/dist/services/issue-outbox-pump.js +102 -0
- package/dist/services/issue-outbox-pump.js.map +1 -0
- package/dist/services/issue-release.d.ts +78 -0
- package/dist/services/issue-release.d.ts.map +1 -0
- package/dist/services/issue-release.js +89 -0
- package/dist/services/issue-release.js.map +1 -0
- package/dist/services/issue-report.d.ts +72 -0
- package/dist/services/issue-report.d.ts.map +1 -0
- package/dist/services/issue-report.js +84 -0
- package/dist/services/issue-report.js.map +1 -0
- package/dist/services/issue-status-view.d.ts +52 -0
- package/dist/services/issue-status-view.d.ts.map +1 -0
- package/dist/services/issue-status-view.js +42 -0
- package/dist/services/issue-status-view.js.map +1 -0
- package/dist/services/issue-status-writer.d.ts +98 -0
- package/dist/services/issue-status-writer.d.ts.map +1 -0
- package/dist/services/issue-status-writer.js +123 -0
- package/dist/services/issue-status-writer.js.map +1 -0
- package/dist/services/traex-paths.d.ts +14 -2
- package/dist/services/traex-paths.d.ts.map +1 -1
- package/dist/services/traex-paths.js +16 -2
- package/dist/services/traex-paths.js.map +1 -1
- package/dist/services/traex-transcript.d.ts +33 -0
- package/dist/services/traex-transcript.d.ts.map +1 -1
- package/dist/services/traex-transcript.js +135 -28
- package/dist/services/traex-transcript.js.map +1 -1
- package/dist/setup/lark-scopes.json +0 -2
- package/dist/worker.js +269 -7
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Board 本地状态:binding(issue ↔ 本机会话锚点)与 outbox(状态回写发件箱)。
|
|
3
|
+
*
|
|
4
|
+
* 平台侧契约见 platform 仓 DESIGN-issue-board.md §六/§七。那份设计是按 Desktop 写的,
|
|
5
|
+
* Desktop 有 SQLite、能把「建本地任务」和「写 binding」放进**同一个事务**。botmux 没有
|
|
6
|
+
* SQLite(全仓都是 JSON + 原子写),所以这里换一套等价的崩溃安全约定。
|
|
7
|
+
*
|
|
8
|
+
* ## 时序:binding 只能在拿到 anchor 之后写
|
|
9
|
+
*
|
|
10
|
+
* binding 的主键 `anchorId` 是 `chatId` / `rootMessageId`——**建群/发 seed 之后才存在**,
|
|
11
|
+
* 所以不可能「先写 binding 再建群」。实际顺序是:
|
|
12
|
+
*
|
|
13
|
+
* 1. platform claim → 拿到 claimId / claimEpoch / stateRev
|
|
14
|
+
* 2. 建群(或发无 @ 的 seed) → 拿到 anchorId。**held**:不传 kickoffBot/kickoffPrompt
|
|
15
|
+
* 3. createBinding(anchorId…) → 落盘,durable boundary(放在 createGroupWithBots 的
|
|
16
|
+
* `onChatCreated` 同步钩子里写,函数返回前就已持久化)
|
|
17
|
+
* 4. platform bind(localTaskRef) → bindState 转 'bound'
|
|
18
|
+
* 5. kickoff(@bot)→ activate → daemon 这时才真正起会话
|
|
19
|
+
*
|
|
20
|
+
* ## 崩溃语义:危险窗口在 2→3,靠「held」兜底
|
|
21
|
+
*
|
|
22
|
+
* 「群已建、binding 未写」= **孤儿群**。它比旧叙事里的「binding 有、群没有」难对账,但
|
|
23
|
+
* 危害被第 2 步的 held 挡住了:没发 kickoff 就没有任何 bot 被 @ 起来,daemon 不会创建
|
|
24
|
+
* 会话,**本机没有 agent 在跑**——与旧叙事想要的安全性质相同,只是换了达成方式。
|
|
25
|
+
*
|
|
26
|
+
* 对账要认出孤儿群,就必须留下 claimId→issue 的痕迹(否则 claimId 只活在内存里,崩了既
|
|
27
|
+
* 找不回 claim、也认不出哪个群是它的)。落点要钉在 **claim 成功之后、建群之前**——危险
|
|
28
|
+
* 窗口从 claim 返回的那一刻就开始了,写「建群前」不够精确:claim 已成功而意图未落盘时崩
|
|
29
|
+
* 溃,同样丢 claimId。held 只保证「没有 agent 在跑」,不保证「能对账续跑」,这两件事别混。
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ 这份「claim 意图」存储**本刀还没有**——本模块只含 binding + outbox。拉群领取那一刀
|
|
32
|
+
* 必须补上(存 claimId / issueId / teamId / claimEpoch / stateRev),并把 claimId 同时写进
|
|
33
|
+
* 群名或 seed 正文,形成「本地意图 + 群侧标记」双通道反查。届时的对账策略:
|
|
34
|
+
* - 有 claim 意图、无 binding → 按 claimId 反查群:找到就补写 binding 续跑,找不到就
|
|
35
|
+
* 释放 claim(或直接等平台 lease 到期由 sweeper 回收)
|
|
36
|
+
* - 有 binding 且 `bindState==='pending'` → 重试 bind;被平台拒(issue 被回收/别人领走/
|
|
37
|
+
* 代次过期)则置 void 并解散那个 held 群,此刻同样没有 agent 在跑
|
|
38
|
+
*
|
|
39
|
+
* ## 写者不止一个,所以每次 RMW 都要拿锁
|
|
40
|
+
*
|
|
41
|
+
* 曾经这里写着「单写者假设:claim 流程与 outbox pump 都只跑在 dashboard 进程,故不加锁」。
|
|
42
|
+
* **那句话是错的**:`botmux report` 跑在 **agent 的 CLI 子进程**里(见 cli.ts 的 report
|
|
43
|
+
* issue 分支),它照样 enqueue/claim/settle 这两个文件,而发件箱 pump 跑在 **daemon 进程**。
|
|
44
|
+
*
|
|
45
|
+
* 危害不是"字段短暂回退"这么轻。两个文件都是**整份 map/数组重写**:进程 A 读到快照、进程 B
|
|
46
|
+
* 在这期间新建了一条 binding、A 再把自己的快照写回去 —— **B 那条 binding 直接消失**,那个群
|
|
47
|
+
* 与 issue 的关联凭空丢掉,而且全程无声。概率低(要求毫秒级交错),后果不可恢复。
|
|
48
|
+
*
|
|
49
|
+
* 所以所有 RMW 走 `withIssueLock`([[utils/file-lock]] 的跨进程 advisory lock,与多 daemon
|
|
50
|
+
* 并发写 `bots.json` 同一套)。只读函数不加锁:原子写保证读者要么看到旧的完整文件、要么看到
|
|
51
|
+
* 新的完整文件,不存在半截状态。
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ 锁**不可重入**:加锁的公开函数之间不能互相调用(`settleOutboxRow` 要改 binding,就用
|
|
54
|
+
* 下面的 `updateBindingUnlocked` 而不是 `updateBinding`)。
|
|
55
|
+
*
|
|
56
|
+
* 存储:`{dataDir}/issue-bindings.json`、`{dataDir}/issue-outbox.json`。
|
|
57
|
+
*/
|
|
58
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
59
|
+
import { join } from 'node:path';
|
|
60
|
+
import { randomUUID } from 'node:crypto';
|
|
61
|
+
import { atomicWriteFileSync } from '../utils/atomic-write.js';
|
|
62
|
+
import { withFileLockSync } from '../utils/file-lock.js';
|
|
63
|
+
import { sessionKey } from '../core/types.js';
|
|
64
|
+
/**
|
|
65
|
+
* 串行化对 issue board 本地状态的读-改-写。
|
|
66
|
+
*
|
|
67
|
+
* 一把锁覆盖 bindings / outbox / claim-intents 三个文件而不是各锁各的:`enqueueDesiredStatus`
|
|
68
|
+
* 本来就要跨两个文件写(bump 计数器 + 排行),分开锁等于没锁。
|
|
69
|
+
*
|
|
70
|
+
* 3s 上限:这些临界区都是一次读一次写的毫秒级操作,等到 3 秒基本就是持锁进程死了——
|
|
71
|
+
* file-lock 自己会 stale-break,这里超时抛错让调用方看见,好过无限等。
|
|
72
|
+
*/
|
|
73
|
+
function withIssueLock(dataDir, fn) {
|
|
74
|
+
return withFileLockSync(join(dataDir, 'issue-board'), fn, { maxWaitMs: 3_000 });
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* localTaskRef 的**唯一**构造入口,且 `createBinding` 会自己调它——`CreateBindingInput`
|
|
78
|
+
* 里没有 localTaskRef 字段,调用方**没有**手拼的机会。
|
|
79
|
+
*
|
|
80
|
+
* 之所以做成内生而不是「约定调用方用这个函数」:拼反了顺序不会当场报错,要等平台 bind
|
|
81
|
+
* 记下的 localTaskRef 与崩溃恢复时的重确认对不上,那条 held 会话被判 claim_mismatch 作废
|
|
82
|
+
* 才暴露——离出错点太远。直接复用 botmux 自己的 `sessionKey`,与 activeSessions 键同源。
|
|
83
|
+
*/
|
|
84
|
+
export function buildLocalTaskRef(anchorId, larkAppId) {
|
|
85
|
+
return sessionKey(anchorId, larkAppId);
|
|
86
|
+
}
|
|
87
|
+
function bindingsPath(dataDir) {
|
|
88
|
+
return join(dataDir, 'issue-bindings.json');
|
|
89
|
+
}
|
|
90
|
+
function outboxPath(dataDir) {
|
|
91
|
+
return join(dataDir, 'issue-outbox.json');
|
|
92
|
+
}
|
|
93
|
+
function readJson(fp, fallback) {
|
|
94
|
+
if (!existsSync(fp))
|
|
95
|
+
return fallback;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(readFileSync(fp, 'utf-8'));
|
|
98
|
+
if (parsed && typeof parsed === 'object')
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// 文件损坏 → fallback。binding 丢了会让在跑的会话失去 issue 关联(人工 force-detach
|
|
103
|
+
// 兜底),但绝不能因为一个坏文件让整个 daemon 起不来。
|
|
104
|
+
}
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
// ── bindings ────────────────────────────────────────────────────────────────
|
|
108
|
+
export function listBindings(dataDir) {
|
|
109
|
+
return Object.values(readJson(bindingsPath(dataDir), {}));
|
|
110
|
+
}
|
|
111
|
+
export function getBinding(dataDir, anchorId) {
|
|
112
|
+
return readJson(bindingsPath(dataDir), {})[anchorId] ?? null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 这条 binding 是否还代表「本机正持有这个 issue」。
|
|
116
|
+
*
|
|
117
|
+
* 抽成一个谓词而不是到处写 `!== 'void'`:终态从一个变成两个(`void` / `released`)时,
|
|
118
|
+
* 散落各处的判断只要漏改一处,就会出现「已释放的 issue 仍被当成已领取」——重领被拦、
|
|
119
|
+
* 回写继续发,而且全程不报错。加状态就改这一个地方。
|
|
120
|
+
*/
|
|
121
|
+
export function isActiveBindState(state) {
|
|
122
|
+
return state === 'pending' || state === 'bound';
|
|
123
|
+
}
|
|
124
|
+
/** 按 claimId 反查——崩溃恢复的入口(§六:不依赖平台「按 claimId 查 issue」的接口,那个接口不存在)。 */
|
|
125
|
+
export function findBindingByClaimId(dataDir, claimId) {
|
|
126
|
+
return listBindings(dataDir).find((b) => b.claimId === claimId) ?? null;
|
|
127
|
+
}
|
|
128
|
+
/** 某 issue 当前在本机的活跃 binding(作废/已释放的不算)。用于「这个 issue 是不是我已经领了」。 */
|
|
129
|
+
export function findActiveBindingByIssue(dataDir, issueId) {
|
|
130
|
+
return listBindings(dataDir).find((b) => b.issueId === issueId && isActiveBindState(b.bindState)) ?? null;
|
|
131
|
+
}
|
|
132
|
+
function writeBindings(dataDir, all) {
|
|
133
|
+
atomicWriteFileSync(bindingsPath(dataDir), JSON.stringify(all, null, 2) + '\n');
|
|
134
|
+
}
|
|
135
|
+
function claimIntentsPath(dataDir) {
|
|
136
|
+
return join(dataDir, 'issue-claims.json');
|
|
137
|
+
}
|
|
138
|
+
function writeClaimIntents(dataDir, all) {
|
|
139
|
+
atomicWriteFileSync(claimIntentsPath(dataDir), JSON.stringify(all, null, 2) + '\n');
|
|
140
|
+
}
|
|
141
|
+
export function listClaimIntents(dataDir) {
|
|
142
|
+
return Object.values(readJson(claimIntentsPath(dataDir), {}));
|
|
143
|
+
}
|
|
144
|
+
export function getClaimIntent(dataDir, claimId) {
|
|
145
|
+
return readJson(claimIntentsPath(dataDir), {})[claimId] ?? null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 记下一次领取意图。**必须在 platform claim 返回成功后立刻调用,早于任何建群动作**。
|
|
149
|
+
*
|
|
150
|
+
* 同 claimId 重入返回既有记录(claim 本身幂等,重试不该产生第二条意图)。
|
|
151
|
+
*/
|
|
152
|
+
export function recordClaimIntent(dataDir, input, now = Date.now()) {
|
|
153
|
+
return withIssueLock(dataDir, () => {
|
|
154
|
+
const all = readJson(claimIntentsPath(dataDir), {});
|
|
155
|
+
const existing = all[input.claimId];
|
|
156
|
+
if (existing)
|
|
157
|
+
return existing;
|
|
158
|
+
const intent = { ...input, createdAt: now, updatedAt: now };
|
|
159
|
+
all[intent.claimId] = intent;
|
|
160
|
+
writeClaimIntents(dataDir, all);
|
|
161
|
+
return intent;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** 回填 anchorId / chatId(建群成功后)。 */
|
|
165
|
+
export function updateClaimIntent(dataDir, claimId, patch, now = Date.now()) {
|
|
166
|
+
return withIssueLock(dataDir, () => {
|
|
167
|
+
const all = readJson(claimIntentsPath(dataDir), {});
|
|
168
|
+
const cur = all[claimId];
|
|
169
|
+
if (!cur)
|
|
170
|
+
return null;
|
|
171
|
+
const next = { ...cur, ...patch, claimId: cur.claimId, createdAt: cur.createdAt, updatedAt: now };
|
|
172
|
+
all[claimId] = next;
|
|
173
|
+
writeClaimIntents(dataDir, all);
|
|
174
|
+
return next;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 领取流程走完(binding 已 bound)后清掉意图——此后 binding 自己就是完整证据。
|
|
179
|
+
*
|
|
180
|
+
* ⚠️ 清除必须**晚于** binding 落盘。反过来先清意图再写 binding,中间崩溃就同时丢掉了两份
|
|
181
|
+
* 证据,那个群彻底认不回来——这正是意图存在的意义。
|
|
182
|
+
*/
|
|
183
|
+
export function clearClaimIntent(dataDir, claimId) {
|
|
184
|
+
return withIssueLock(dataDir, () => {
|
|
185
|
+
const all = readJson(claimIntentsPath(dataDir), {});
|
|
186
|
+
if (!all[claimId])
|
|
187
|
+
return false;
|
|
188
|
+
delete all[claimId];
|
|
189
|
+
writeClaimIntents(dataDir, all);
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** 对账分类:意图还在、但没有对应 binding 的,就是需要人/流程接手的悬空领取。 */
|
|
194
|
+
export function listDanglingClaimIntents(dataDir) {
|
|
195
|
+
const bound = new Set(listBindings(dataDir).map((b) => b.claimId));
|
|
196
|
+
return listClaimIntents(dataDir).filter((i) => !bound.has(i.claimId));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* 写入一条 pending binding。**在建群/发 seed 拿到 anchorId 之后调用**——主键就是那个
|
|
200
|
+
* anchor,之前不可能有。放在 `createGroupWithBots` 的 `onChatCreated` 同步钩子里写,
|
|
201
|
+
* 让它成为「群已存在」到「群已归属某个 issue」之间的 durable boundary(见文件头时序)。
|
|
202
|
+
*
|
|
203
|
+
* 两条不变式都在这层强制,不指望调用方自觉:
|
|
204
|
+
* - **claimId 唯一**:同 claimId 重入直接返回既有行(本地幂等,对应 Desktop 侧的
|
|
205
|
+
* `UNIQUE(claim_id)`)——领取重试不会产生第二条 binding、也不会重复建群。
|
|
206
|
+
* - **一 issue 一活跃 binding**:同 issue 已有非 void 的 binding 却拿着不同 claimId 进来,
|
|
207
|
+
* 是调用方漏查(比如没先 `findActiveBindingByIssue`)。此时**抛错**而不是照写——写下去
|
|
208
|
+
* 就是同一个 issue 两个群、两个 agent 同时开工,而平台侧只认最后一次 claim,另一个群
|
|
209
|
+
* 会变成谁也不管的孤儿。抛在这里离原因最近;释放后重领是安全的(旧 binding 已置 void)。
|
|
210
|
+
*/
|
|
211
|
+
export function createBinding(dataDir, input, now = Date.now()) {
|
|
212
|
+
return withIssueLock(dataDir, () => {
|
|
213
|
+
const all = readJson(bindingsPath(dataDir), {});
|
|
214
|
+
const existing = Object.values(all).find((b) => b.claimId === input.claimId);
|
|
215
|
+
if (existing)
|
|
216
|
+
return existing;
|
|
217
|
+
const activeSameIssue = Object.values(all).find((b) => b.issueId === input.issueId && isActiveBindState(b.bindState));
|
|
218
|
+
if (activeSameIssue) {
|
|
219
|
+
throw new Error(`issue ${input.issueId} 已有活跃 binding(anchor=${activeSameIssue.anchorId}, claimId=${activeSameIssue.claimId});`
|
|
220
|
+
+ '重复领取前应先 findActiveBindingByIssue 复用,或释放后再领');
|
|
221
|
+
}
|
|
222
|
+
const binding = {
|
|
223
|
+
...input,
|
|
224
|
+
localTaskRef: buildLocalTaskRef(input.anchorId, input.larkAppId),
|
|
225
|
+
bindState: input.bindState ?? 'pending',
|
|
226
|
+
nextSourceSeq: 1,
|
|
227
|
+
createdAt: now,
|
|
228
|
+
updatedAt: now,
|
|
229
|
+
};
|
|
230
|
+
all[binding.anchorId] = binding;
|
|
231
|
+
writeBindings(dataDir, all);
|
|
232
|
+
return binding;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** 局部更新一条 binding(read-modify-write,保住并发写入的其它字段)。 */
|
|
236
|
+
export function updateBinding(dataDir, anchorId, patch, now = Date.now()) {
|
|
237
|
+
return withIssueLock(dataDir, () => updateBindingUnlocked(dataDir, anchorId, patch, now));
|
|
238
|
+
}
|
|
239
|
+
/** 已经持有 issue 锁时用这个——锁不可重入,嵌套调 `updateBinding` 会自己等死自己。 */
|
|
240
|
+
function updateBindingUnlocked(dataDir, anchorId, patch, now) {
|
|
241
|
+
const all = readJson(bindingsPath(dataDir), {});
|
|
242
|
+
const cur = all[anchorId];
|
|
243
|
+
if (!cur)
|
|
244
|
+
return null;
|
|
245
|
+
const next = { ...cur, ...patch, anchorId: cur.anchorId, createdAt: cur.createdAt, updatedAt: now };
|
|
246
|
+
all[anchorId] = next;
|
|
247
|
+
writeBindings(dataDir, all);
|
|
248
|
+
return next;
|
|
249
|
+
}
|
|
250
|
+
/** 删除一条 binding(会话彻底结束且 issue 已终态后清理)。返回是否删掉。 */
|
|
251
|
+
export function removeBinding(dataDir, anchorId) {
|
|
252
|
+
return withIssueLock(dataDir, () => {
|
|
253
|
+
const all = readJson(bindingsPath(dataDir), {});
|
|
254
|
+
if (!(anchorId in all))
|
|
255
|
+
return false;
|
|
256
|
+
delete all[anchorId];
|
|
257
|
+
writeBindings(dataDir, all);
|
|
258
|
+
return true;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// ── outbox ──────────────────────────────────────────────────────────────────
|
|
262
|
+
function readOutbox(dataDir) {
|
|
263
|
+
const raw = readJson(outboxPath(dataDir), []);
|
|
264
|
+
return Array.isArray(raw) ? raw : [];
|
|
265
|
+
}
|
|
266
|
+
function writeOutbox(dataDir, rows) {
|
|
267
|
+
atomicWriteFileSync(outboxPath(dataDir), JSON.stringify(rows, null, 2) + '\n');
|
|
268
|
+
}
|
|
269
|
+
export function listOutbox(dataDir, anchorId) {
|
|
270
|
+
const rows = readOutbox(dataDir);
|
|
271
|
+
return anchorId === undefined ? rows : rows.filter((r) => r.anchorId === anchorId);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 投影一次「本会话应达的 issue 状态」到发件箱(§七 去重合并)。
|
|
275
|
+
*
|
|
276
|
+
* 合并规则(**与最新未完成目标比,不是与 lastSyncedStatus 比**):
|
|
277
|
+
* - 该 binding 已有 `pending` 行 → **就地覆盖**它的 targetStatus 并重分配 sourceSeq,不新增行;
|
|
278
|
+
* - 否则仅当 desired 与「最新目标 ∪ lastSyncedStatus」都不同才排一条新行。
|
|
279
|
+
*
|
|
280
|
+
* 为什么必须这样:离线期间 30s 一次的 tick 会反复投影,若每次都追加,恢复后会把一长串
|
|
281
|
+
* 早已过时的中间态依次发给平台——既刷无效自迁移,又让**最新**状态排在队尾迟迟到不了。
|
|
282
|
+
*
|
|
283
|
+
* 返回排出的行;无需回写时返回 null。
|
|
284
|
+
*/
|
|
285
|
+
export function enqueueDesiredStatus(dataDir, anchorId, desired, opts = {}, now = Date.now()) {
|
|
286
|
+
return withIssueLock(dataDir, () => {
|
|
287
|
+
const bindings = readJson(bindingsPath(dataDir), {});
|
|
288
|
+
const binding = bindings[anchorId];
|
|
289
|
+
// 终态(void/released)之后不再回写:那个 issue 已经不归本机管了,继续发只会被平台拒。
|
|
290
|
+
// 释放本身也走这里,但它是在 binding 还是 bound 的时候排的行——先排队再改状态,见
|
|
291
|
+
// [[issue-release]] 的「平台先行、本地后写」。
|
|
292
|
+
if (!binding || !isActiveBindState(binding.bindState))
|
|
293
|
+
return null;
|
|
294
|
+
const rows = readOutbox(dataDir);
|
|
295
|
+
const mine = rows.filter((r) => r.anchorId === anchorId);
|
|
296
|
+
const pendingIdx = rows.findIndex((r) => r.anchorId === anchorId && r.state === 'pending');
|
|
297
|
+
const latestUnsettled = mine
|
|
298
|
+
.filter((r) => r.state === 'pending' || r.state === 'inflight')
|
|
299
|
+
.sort((a, b) => b.sourceSeq - a.sourceSeq)[0];
|
|
300
|
+
// 已经在追同一个目标(或已经同步到位)→ 无事可做。
|
|
301
|
+
if (pendingIdx < 0) {
|
|
302
|
+
const latestTarget = latestUnsettled?.targetStatus;
|
|
303
|
+
if (desired === latestTarget || (latestTarget === undefined && desired === binding.lastSyncedStatus)) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (rows[pendingIdx].targetStatus === desired && rows[pendingIdx].attentionReason === opts.attentionReason) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
// 分配序号时取「计数器」与「本 binding 在 outbox 里出现过的最大序号 + 1」的较大者。
|
|
311
|
+
//
|
|
312
|
+
// 写序(见下)已经保证正常路径不会复用序号;这一层是**自愈**:万一计数器因为任何原因
|
|
313
|
+
// 落后于已排出的行(历史遗留数据、外部改文件、未来重构把写序改回去),复用序号的后果是
|
|
314
|
+
// 平台按 `sourceSeq <= lastSourceSeq` 静默 no-op ——**新状态永远上不去且毫无报错**。
|
|
315
|
+
// 这种失败太安静,值得用两行把它变成不可能,而不是只靠写序的正确性来担保。
|
|
316
|
+
// prune 删掉旧的 done 行会让 max 变小,但计数器只增不减,取较大者天然安全。
|
|
317
|
+
const maxSeen = mine.reduce((m, r) => Math.max(m, r.sourceSeq), 0);
|
|
318
|
+
const sourceSeq = Math.max(binding.nextSourceSeq, maxSeen + 1);
|
|
319
|
+
const row = {
|
|
320
|
+
writeId: randomUUID(),
|
|
321
|
+
anchorId,
|
|
322
|
+
sourceSeq,
|
|
323
|
+
targetStatus: desired,
|
|
324
|
+
...(opts.attentionReason ? { attentionReason: opts.attentionReason } : {}),
|
|
325
|
+
claimId: binding.claimId,
|
|
326
|
+
claimEpoch: binding.claimEpoch,
|
|
327
|
+
...(opts.expectedStateRev !== undefined
|
|
328
|
+
? { expectedStateRev: opts.expectedStateRev }
|
|
329
|
+
: binding.platformStateRev !== undefined
|
|
330
|
+
? { expectedStateRev: binding.platformStateRev }
|
|
331
|
+
: {}),
|
|
332
|
+
state: 'pending',
|
|
333
|
+
attempts: 0,
|
|
334
|
+
createdAt: now,
|
|
335
|
+
};
|
|
336
|
+
// ⚠️ 写序不能反:**先 bump 计数器落盘,再写 outbox**。
|
|
337
|
+
//
|
|
338
|
+
// 这是两个文件的 RMW,单文件 atomicWrite 保不了跨文件原子性,中间必然存在崩溃窗口。
|
|
339
|
+
// 两种写序的崩溃后果完全不同:
|
|
340
|
+
// - 先 outbox 后 binding(错):outbox 已有 seq=N、计数器还停在 N。该行 settle 后
|
|
341
|
+
// 下一次投影会**复用 N**,平台见 `sourceSeq <= lastSourceSeq` 直接 no-op —— 新状态
|
|
342
|
+
// **永远上不去**,而且一路静默,真机上极难查。
|
|
343
|
+
// - 先 binding 后 outbox(对):最坏是 outbox 那条没写成、序号 N 被跳过。平台只要求
|
|
344
|
+
// 单调、不要求连续,跳号无害;丢掉的投影下一拍 tick 会用更大的 seq 重新排出来。
|
|
345
|
+
bindings[anchorId] = { ...binding, nextSourceSeq: sourceSeq + 1, updatedAt: now };
|
|
346
|
+
writeBindings(dataDir, bindings);
|
|
347
|
+
if (pendingIdx >= 0)
|
|
348
|
+
rows[pendingIdx] = row; // 就地覆盖未发送行
|
|
349
|
+
else
|
|
350
|
+
rows.push(row);
|
|
351
|
+
writeOutbox(dataDir, rows);
|
|
352
|
+
return row;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* 领取该 binding 的下一条待发行(`pending → inflight` 的 CAS)。
|
|
357
|
+
*
|
|
358
|
+
* **串行**:同一 binding 已有 inflight 行时返回 null——平台侧要求同一 issue 的 sourceSeq
|
|
359
|
+
* 单调到达,并发发送会让顺序乱掉。`nextRetryAt` 未到的行也不返回(退避)。
|
|
360
|
+
*/
|
|
361
|
+
export function claimNextOutboxRow(dataDir, anchorId, now = Date.now()) {
|
|
362
|
+
return withIssueLock(dataDir, () => {
|
|
363
|
+
const rows = readOutbox(dataDir);
|
|
364
|
+
const mine = rows.filter((r) => r.anchorId === anchorId);
|
|
365
|
+
if (mine.some((r) => r.state === 'inflight'))
|
|
366
|
+
return null;
|
|
367
|
+
// 按 sourceSeq 最小的先发,不依赖数组顺序。今天「追加保序 + 就地覆盖」恰好也对,但
|
|
368
|
+
// 一旦以后 prune/compaction 重排数组,按下标取就可能先发高序号再发低序号——平台按
|
|
369
|
+
// 单调判重,低序号那条会被静默丢弃。显式排序把这条契约钉在代码里而不是数组布局上。
|
|
370
|
+
const nextRow = mine
|
|
371
|
+
.filter((r) => r.state === 'pending' && (r.nextRetryAt ?? 0) <= now)
|
|
372
|
+
.sort((a, b) => a.sourceSeq - b.sourceSeq)[0];
|
|
373
|
+
if (!nextRow)
|
|
374
|
+
return null;
|
|
375
|
+
const idx = rows.findIndex((r) => r.writeId === nextRow.writeId);
|
|
376
|
+
if (idx < 0)
|
|
377
|
+
return null;
|
|
378
|
+
rows[idx] = { ...rows[idx], state: 'inflight', attempts: rows[idx].attempts + 1 };
|
|
379
|
+
writeOutbox(dataDir, rows);
|
|
380
|
+
return rows[idx];
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 发送成功:标 done,并把平台返回的 stateRev / 已同步状态写回 binding。
|
|
385
|
+
*
|
|
386
|
+
* `applied: false` 用于「这条行不必再发了,但平台**没有**采纳它」——目前只有一种情况:
|
|
387
|
+
* 409 对账发现 claim 已易主。此时**绝不能**写 `lastSyncedStatus`:写了本地就以为
|
|
388
|
+
* "已经是 in_review 了",下一次交付会走幂等分支直接回成功,而平台上那条压根没变。
|
|
389
|
+
*/
|
|
390
|
+
export function settleOutboxRow(dataDir, writeId, result, now = Date.now()) {
|
|
391
|
+
withIssueLock(dataDir, () => {
|
|
392
|
+
const rows = readOutbox(dataDir);
|
|
393
|
+
const idx = rows.findIndex((r) => r.writeId === writeId);
|
|
394
|
+
if (idx < 0)
|
|
395
|
+
return;
|
|
396
|
+
const row = rows[idx];
|
|
397
|
+
rows[idx] = { ...row, state: 'done' };
|
|
398
|
+
writeOutbox(dataDir, rows);
|
|
399
|
+
const binding = getBinding(dataDir, row.anchorId);
|
|
400
|
+
// 用平台回传的 `claim.lastSourceSeq` 校准本地计数器。
|
|
401
|
+
//
|
|
402
|
+
// 平台才是「已应用到哪一号」的权威:它按 `sourceSeq <= lastSourceSeq` 判重,本地计数器
|
|
403
|
+
// 一旦落后(dataDir 从旧快照恢复、文件被外部改、其它路径抢跑过序号),之后每一条回写都
|
|
404
|
+
// 会被静默丢弃——平台回 200、本地记成功、状态却永远上不去。真机 e2e 里就撞到过这一幕。
|
|
405
|
+
//
|
|
406
|
+
// 单写者假设下计数器本该恒领先,所以这只是兜底;但代价是一次 max(),而漏掉的代价是
|
|
407
|
+
// 无声的状态停更,值得。
|
|
408
|
+
const syncedSeq = result.platformLastSourceSeq !== undefined && binding
|
|
409
|
+
? Math.max(binding.nextSourceSeq, result.platformLastSourceSeq + 1)
|
|
410
|
+
: undefined;
|
|
411
|
+
updateBindingUnlocked(dataDir, row.anchorId, {
|
|
412
|
+
...(result.applied === false ? {} : { lastSyncedStatus: row.targetStatus }),
|
|
413
|
+
...(result.platformStateRev !== undefined ? { platformStateRev: result.platformStateRev } : {}),
|
|
414
|
+
...(syncedSeq !== undefined && syncedSeq !== binding?.nextSourceSeq ? { nextSourceSeq: syncedSeq } : {}),
|
|
415
|
+
}, now);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/** 发送失败:退回 pending + 指数退避(上限 5min)。`fatal` 用于平台明确拒绝、重试无意义的情况。 */
|
|
419
|
+
export function failOutboxRow(dataDir, writeId, error, opts = {}, now = Date.now()) {
|
|
420
|
+
withIssueLock(dataDir, () => {
|
|
421
|
+
const rows = readOutbox(dataDir);
|
|
422
|
+
const idx = rows.findIndex((r) => r.writeId === writeId);
|
|
423
|
+
if (idx < 0)
|
|
424
|
+
return;
|
|
425
|
+
const row = rows[idx];
|
|
426
|
+
const backoff = Math.min(1_000 * 2 ** Math.max(0, row.attempts - 1), 5 * 60_000);
|
|
427
|
+
rows[idx] = {
|
|
428
|
+
...row,
|
|
429
|
+
state: opts.fatal ? 'failed' : 'pending',
|
|
430
|
+
lastError: error.slice(0, 500),
|
|
431
|
+
...(opts.fatal ? {} : { nextRetryAt: now + backoff }),
|
|
432
|
+
};
|
|
433
|
+
writeOutbox(dataDir, rows);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 启动对账:把本机所有 `inflight` 退回 `pending`。
|
|
438
|
+
*
|
|
439
|
+
* 进程在「标 inflight 后、记录响应前」崩溃时,那一行会永远停在 inflight 并**堵死该
|
|
440
|
+
* binding 的串行 pump**(claimNextOutboxRow 见到 inflight 就返回 null)。平台侧的
|
|
441
|
+
* sourceSeq 单调 + 终态幂等保证重复投递是安全的,所以无脑退回即可,不需要发送租约。
|
|
442
|
+
*
|
|
443
|
+
* 返回被退回的行数。
|
|
444
|
+
*/
|
|
445
|
+
export function resetInflightToPending(dataDir) {
|
|
446
|
+
return withIssueLock(dataDir, () => {
|
|
447
|
+
const rows = readOutbox(dataDir);
|
|
448
|
+
let n = 0;
|
|
449
|
+
const next = rows.map((r) => {
|
|
450
|
+
if (r.state !== 'inflight')
|
|
451
|
+
return r;
|
|
452
|
+
n += 1;
|
|
453
|
+
return { ...r, state: 'pending' };
|
|
454
|
+
});
|
|
455
|
+
if (n > 0)
|
|
456
|
+
writeOutbox(dataDir, next);
|
|
457
|
+
return n;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/** 清理已完成的发件箱行(保留最近 `keep` 条便于排查)。返回清掉的行数。 */
|
|
461
|
+
export function pruneOutbox(dataDir, keep = 50) {
|
|
462
|
+
return withIssueLock(dataDir, () => {
|
|
463
|
+
const rows = readOutbox(dataDir);
|
|
464
|
+
const settled = rows.filter((r) => r.state === 'done');
|
|
465
|
+
if (settled.length <= keep)
|
|
466
|
+
return 0;
|
|
467
|
+
const drop = new Set(settled.sort((a, b) => a.createdAt - b.createdAt).slice(0, settled.length - keep).map((r) => r.writeId));
|
|
468
|
+
writeOutbox(dataDir, rows.filter((r) => !drop.has(r.writeId)));
|
|
469
|
+
return drop.size;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
//# sourceMappingURL=issue-board-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-board-store.js","sourceRoot":"","sources":["../../src/services/issue-board-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { CreateGroupOpts, CreateGroupResult } from './group-creator.js';
|
|
2
|
+
import type { IssueClientResult, PlatformIssue } from '../platform/issue-client.js';
|
|
3
|
+
import { type IssueBinding, type IssueStatus } from './issue-board-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* 群名里带的领取标记。对账时靠它把「平台上有 claim、本地没有 binding」的孤儿群认回来
|
|
6
|
+
* ——本地意图是一条腿,这个标记是另一条腿,少哪条都会留下认不出的群。
|
|
7
|
+
*
|
|
8
|
+
* 只取前 8 位十六进制(32 bit):群名是给人看的,不该被一串 32 字符的随机数占满;
|
|
9
|
+
* 同时在手可数的候选群里,32 bit 足以唯一定位。
|
|
10
|
+
*/
|
|
11
|
+
export declare function claimMarker(claimId: string): string;
|
|
12
|
+
export declare function matchesClaimMarker(groupName: string, claimId: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 拼领取群名:`<标题> #<claimId 前 8 位>`,**标题被截、标记一定完整**。
|
|
15
|
+
*
|
|
16
|
+
* 不截的话飞书自己会截,而它从尾部截——先吃掉的正是尾部那个 `#claimId` 标记,也就是双通道
|
|
17
|
+
* 反查的一条腿。那条腿断了不会报错:对账时只是"这个群不像是任何一次领取的",然后被当成
|
|
18
|
+
* 无主群跳过。宁可标题短一点。
|
|
19
|
+
*
|
|
20
|
+
* 标记放尾部而不是挪到前面:群列表里先看到的应该是任务在说什么,一串十六进制不配占开头。
|
|
21
|
+
* 长度自己控住了,位置就不再是风险。
|
|
22
|
+
*/
|
|
23
|
+
export declare function claimGroupName(title: string, claimId: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* 平台上这条 issue 的详情深链。
|
|
26
|
+
*
|
|
27
|
+
* 路由形状来自平台前端:tab 在 hash(`#issues`)、详情在 query(`?issue=<id>`),见
|
|
28
|
+
* platform 仓 `frontend/src/App.tsx` 的 Tabs 与 `IssueBoard.tsx` 的 `openDetail`。
|
|
29
|
+
* 拼不出来(平台地址为空/非法)就返回 undefined,卡片少一个按钮而已,不该因此报错。
|
|
30
|
+
*/
|
|
31
|
+
export declare function issueDetailUrl(platformBaseUrl: string, issueId: string): string | undefined;
|
|
32
|
+
export interface ClaimFlowDeps {
|
|
33
|
+
dataDir: string;
|
|
34
|
+
platformBaseUrl: string;
|
|
35
|
+
claim: (issueId: string, args: {
|
|
36
|
+
claimId: string;
|
|
37
|
+
agent?: string;
|
|
38
|
+
repoLabel?: string;
|
|
39
|
+
expectedStateRev: number;
|
|
40
|
+
}) => Promise<IssueClientResult<{
|
|
41
|
+
claim: {
|
|
42
|
+
claimEpoch: number;
|
|
43
|
+
};
|
|
44
|
+
issue: PlatformIssue;
|
|
45
|
+
}>>;
|
|
46
|
+
bind: (issueId: string, args: {
|
|
47
|
+
claimId: string;
|
|
48
|
+
localTaskRef: string;
|
|
49
|
+
expectedStateRev: number;
|
|
50
|
+
localTaskLabel?: string;
|
|
51
|
+
localTaskUrl?: string;
|
|
52
|
+
}) => Promise<IssueClientResult<{
|
|
53
|
+
issue: PlatformIssue;
|
|
54
|
+
}>>;
|
|
55
|
+
createGroup: (opts: CreateGroupOpts) => Promise<CreateGroupResult>;
|
|
56
|
+
/**
|
|
57
|
+
* 往新群里发开工播报([[issue-card]] 的 `buildIssueKickoffCard`)。
|
|
58
|
+
*
|
|
59
|
+
* **best-effort**:失败只记日志,绝不让领取失败——它是给人看的,不是协议的一环。
|
|
60
|
+
* 不传就不播报(单测里默认不传)。
|
|
61
|
+
*/
|
|
62
|
+
announce?: (chatId: string, card: string) => Promise<void>;
|
|
63
|
+
/** 播报失败时的旁路(生产接 logger;不注入就静默)。 */
|
|
64
|
+
onAnnounceError?: (reason: string) => void;
|
|
65
|
+
/** in_progress 回写失败时的旁路(同上)。失败不推翻领取,但必须留下痕迹。 */
|
|
66
|
+
onStatusError?: (reason: string) => void;
|
|
67
|
+
/** 发 kickoff(@ 目标 bot)把会话激活。返回 messageId。 */
|
|
68
|
+
activate: (chatId: string, botLarkAppId: string, prompt: string) => Promise<string>;
|
|
69
|
+
/**
|
|
70
|
+
* 激活成功后把平台状态推到 `in_progress`。
|
|
71
|
+
*
|
|
72
|
+
* **必填,不是可选**:平台的 activation lease 只扫 `status=claimed`(5 分钟),不回写
|
|
73
|
+
* in_progress 的话 sweeper 会把任务打成 `needs_attention(claim_activate_timeout)`,而
|
|
74
|
+
* 那个状态**回不去**——平台只放行 `task_blocked` 恢复成 in_progress,超时的只能
|
|
75
|
+
* open/reopened(都清 claim),群里的活就废了。做成可选依赖的话,漏传不会有任何报错,
|
|
76
|
+
* 引信就这么静默装回去了;宁可编译期逼调用方给。
|
|
77
|
+
*
|
|
78
|
+
* 与 fetchIssue 一起构成 [[issue-status-writer]] 的 `StatusWriterDeps`,回写走那边的
|
|
79
|
+
* `projectStatus`——sourceSeq 分配、串行、409 对账、退避只实现一次。
|
|
80
|
+
*/
|
|
81
|
+
writeStatus: (issueId: string, args: {
|
|
82
|
+
claimId: string;
|
|
83
|
+
claimEpoch: number;
|
|
84
|
+
sourceSeq: number;
|
|
85
|
+
status: IssueStatus;
|
|
86
|
+
expectedStateRev: number;
|
|
87
|
+
}) => Promise<IssueClientResult<{
|
|
88
|
+
issue: PlatformIssue;
|
|
89
|
+
}>>;
|
|
90
|
+
/** 撞 409 时判断「平台还认不认这个 claim」。见 issue-status-writer。 */
|
|
91
|
+
fetchIssue: (teamId: string, issueId: string) => Promise<PlatformIssue | null>;
|
|
92
|
+
newClaimId?: () => string;
|
|
93
|
+
now?: () => number;
|
|
94
|
+
}
|
|
95
|
+
export interface ClaimFlowArgs {
|
|
96
|
+
/** `body` 只进开工播报(平台侧允许为空),所以这里放宽成可选。 */
|
|
97
|
+
issue: Pick<PlatformIssue, '_id' | 'title' | 'stateRev' | 'targetRepoLabel'> & {
|
|
98
|
+
body?: string;
|
|
99
|
+
};
|
|
100
|
+
teamId: string;
|
|
101
|
+
/** 由哪个 bot 承接这个 issue —— 决定 localTaskRef 的 appId 段与 kickoff 目标。 */
|
|
102
|
+
larkAppId: string;
|
|
103
|
+
/**
|
|
104
|
+
* 上送平台 `claim.agent` 的展示名。平台把这个值原样渲染在 issue 详情里,所以必须是**人能
|
|
105
|
+
* 认出来的名字**(`claude-loopy`),不是 `cli_xxx` ——早先直接传 larkAppId,看板上只能看到
|
|
106
|
+
* 一串 appId,谁在干这活完全看不出来。缺省回落到 larkAppId,保证总有值。
|
|
107
|
+
*/
|
|
108
|
+
agentLabel?: string;
|
|
109
|
+
/** 建群者(通常是 dashboard 所属的那个 bot)。 */
|
|
110
|
+
creatorLarkAppId: string;
|
|
111
|
+
/** 一并拉进群的其它 bot。 */
|
|
112
|
+
peerLarkAppIds?: string[];
|
|
113
|
+
/** 把人按 union_id 拉进群(open_id 是 app-scoped 的,跨 bot 不通用)。 */
|
|
114
|
+
ownerUnionIds?: string[];
|
|
115
|
+
/**
|
|
116
|
+
* agent 干活的目录。会作为 `bindWorkingDir` 绑到新群上(oncall 绑定),**必须传**——
|
|
117
|
+
* 不传的话人在卡片里选的仓库根本不会生效,会话会起在 bot 的默认目录里,
|
|
118
|
+
* 等于让 agent 在错误的仓库动手,而且没有任何报错。
|
|
119
|
+
*/
|
|
120
|
+
workingDir: string;
|
|
121
|
+
kickoffPrompt: string;
|
|
122
|
+
}
|
|
123
|
+
export type ClaimFlowResult = {
|
|
124
|
+
ok: true;
|
|
125
|
+
binding: IssueBinding;
|
|
126
|
+
chatId: string;
|
|
127
|
+
kickoffMessageId: string;
|
|
128
|
+
}
|
|
129
|
+
/** 领取本身没成功,本地没有留下任何痕迹,重试是安全的。 */
|
|
130
|
+
| {
|
|
131
|
+
ok: false;
|
|
132
|
+
stage: 'claim';
|
|
133
|
+
reason: string;
|
|
134
|
+
alreadyLocal?: IssueBinding;
|
|
135
|
+
}
|
|
136
|
+
/** 已 claim 但群没建成:意图在盘上,对账会接手(释放 claim 或补建)。 */
|
|
137
|
+
| {
|
|
138
|
+
ok: false;
|
|
139
|
+
stage: 'group';
|
|
140
|
+
reason: string;
|
|
141
|
+
claimId: string;
|
|
142
|
+
}
|
|
143
|
+
/** 群建好了、binding 也落盘了,但 bind 被平台拒 → binding 置 void,群留给人处理。 */
|
|
144
|
+
| {
|
|
145
|
+
ok: false;
|
|
146
|
+
stage: 'bind';
|
|
147
|
+
reason: string;
|
|
148
|
+
binding: IssueBinding;
|
|
149
|
+
chatId: string;
|
|
150
|
+
}
|
|
151
|
+
/** 前面全成,只差 kickoff。binding 已 bound,重试 activate 即可,不必重建群。 */
|
|
152
|
+
| {
|
|
153
|
+
ok: false;
|
|
154
|
+
stage: 'activate';
|
|
155
|
+
reason: string;
|
|
156
|
+
binding: IssueBinding;
|
|
157
|
+
chatId: string;
|
|
158
|
+
};
|
|
159
|
+
export declare function claimIssueIntoGroup(deps: ClaimFlowDeps, args: ClaimFlowArgs): Promise<ClaimFlowResult>;
|
|
160
|
+
//# sourceMappingURL=issue-claim-flow.d.ts.map
|