codex-to-im 1.0.44 → 1.0.45
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/daemon.mjs +836 -401
- package/dist/ui-server.mjs +48 -10
- package/docs/dev-plan.md +653 -0
- package/package.json +1 -1
package/docs/dev-plan.md
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# Codex-to-IM Turn 架构重构开发与测试计划
|
|
2
|
+
|
|
3
|
+
## 背景
|
|
4
|
+
|
|
5
|
+
当前 IM 通信链路中,纯 IM 对话、IM 复用 Codex Desktop thread、IM 镜像 Codex Desktop thread 三类场景没有被显式建模。运行时主要依赖 `sdk_session_id`、mirror subscription、mirror suppression、SDK stream 结果和 Desktop JSONL terminal record 之间的时序关系来推断最终行为。
|
|
6
|
+
|
|
7
|
+
这导致几个长期问题:
|
|
8
|
+
|
|
9
|
+
- `sdk_session_id` 同时表示 Codex SDK thread id 和 Codex Desktop thread id,语义不清。
|
|
10
|
+
- 纯 IM 会话执行过一次后也会写入 `sdk_session_id`,后续可能被误判成 Desktop thread。
|
|
11
|
+
- IM 复用 Desktop thread 时,SDK stream 可能先于 Desktop JSONL `task_complete.last_agent_message` 收尾,导致最终附件协议没有进入正常 IM 发送路径。
|
|
12
|
+
- mirror suppression 同时承担防重复、回声过滤、IM Desktop turn ownership 兜底,职责过重。
|
|
13
|
+
- `<cti-send>` 附件解析分散在 SDK 消费、interactive runner、mirror delivery 多处。
|
|
14
|
+
- 流式正文、工具、计划、运行时长、上次响应距今的状态来源不统一。
|
|
15
|
+
- 健康检查和状态查询必须保持只读,不能作为运行态修复入口。
|
|
16
|
+
|
|
17
|
+
本计划目标是通过最大程度的结构化重构解决上述根因,而不是继续追加补丁式判断。
|
|
18
|
+
|
|
19
|
+
## 当前进度
|
|
20
|
+
|
|
21
|
+
更新时间:2026-04-27
|
|
22
|
+
|
|
23
|
+
已完成:
|
|
24
|
+
|
|
25
|
+
- 阶段 1 已完成:新增 turn 类型与 turn 分类器。
|
|
26
|
+
- 阶段 2 已完成主路径:新增 `TurnCoordinator` 与 Desktop terminal router,并接入 mirror runtime。
|
|
27
|
+
- 阶段 3 已完成主路径:新增 `ResponseAssembler` 与 `DeliveryPipeline`,并接入 interactive final 和 mirror final。
|
|
28
|
+
- 阶段 4 已完成主路径:新增 `StreamState`,并接入 interactive 与 mirror streaming 状态区。
|
|
29
|
+
- 阶段 5 已完成第一组清理:删除 bridge-manager 中旧的 mirror terminal 兜底收尾/附件补发路径。
|
|
30
|
+
- 阶段 5 已完成第二组清理:新增最终响应附件清理模块,`conversation-engine` 和 `response-assembler` 不再各自直接解析 `<cti-send>`。
|
|
31
|
+
- 阶段 5 已完成第三组清理:新增 mirror feedback controller,镜像流式状态、卡片收尾和最终投递从 `bridge-manager` 抽离。
|
|
32
|
+
- 阶段 5 已完成第四组清理:`mirror suppression` 不再作为 mirror delivery 的全局 blocked 条件,只作为 records 过滤器。
|
|
33
|
+
- `BridgeSession` 已增加 `codex_thread_id`、`desktop_thread_id`、`thread_origin`。
|
|
34
|
+
- 纯 IM SDK 线程与 Codex Desktop 线程已在数据模型上拆分。
|
|
35
|
+
- `/t` / Desktop 绑定路径会写入 `desktop_thread_id` 和 `thread_origin = 'desktop'`。
|
|
36
|
+
- 普通 IM SDK resume 只写入 `codex_thread_id`,不会自动成为 Desktop mirror 来源。
|
|
37
|
+
- mirror subscription registry 已改为只认显式 Desktop thread。
|
|
38
|
+
- `/model`、`/status`、`/history` 中“共享桌面线程”的判断已改为基于 `desktop_thread_id`。
|
|
39
|
+
- `interactive-message-runner` 的 Desktop terminal 等待只对显式 Desktop-backed session 生效,避免纯 IM 会话因为已有 SDK thread id 被误判。
|
|
40
|
+
- active `im_desktop_reuse` turn 会注册到 coordinator;Desktop JSONL `task_complete/task_aborted` 先尝试被 active IM turn 认领。
|
|
41
|
+
- 已被 active IM turn 认领的 Desktop records 会从 mirror delivery 输入中移除,降低重复回复风险。
|
|
42
|
+
- SDK final、Desktop terminal final、mirror final 已统一通过 response assembler 清理正文、解析附件、去重附件。
|
|
43
|
+
- interactive final 和 mirror final 已统一通过 delivery pipeline 执行“卡片已收尾则跳过正文、但继续补发附件”的规则。
|
|
44
|
+
- `lastActivityAt` 与 `lastContentResponseAt` 已拆分;工具/计划/状态说明不会再重置“上次响应距今”的正文基准。
|
|
45
|
+
- mirror runtime 的 health observe 不再顺带触发 IM task 收尾;IM Desktop terminal ownership 只通过 `TurnCoordinator` 认领。
|
|
46
|
+
- Desktop-backed IM 会话在 SDK 先结束时等待 Desktop terminal JSONL 的配置已从 4 秒 grace 改为 30 秒 terminal timeout。
|
|
47
|
+
- runtime terminal reconcile 不再用 terminal health 状态收尾 active IM task;health/reconcile 只做诊断和无 active task 时的 stale runtime 清理。
|
|
48
|
+
- Desktop terminal final 等待已从 mirror suppression 状态解耦;suppression 只作为 Desktop-backed IM turn 的 mirror 回声过滤。
|
|
49
|
+
- 最终响应附件清理已集中到 `src/lib/bridge/turns/final-response-artifacts.ts`,低层 `outbound-artifacts` 只保留纯解析能力。
|
|
50
|
+
- mirror streaming text/status/tools/tasks、stream end、mirror final delivery 已集中到 `src/lib/bridge/mirror-feedback-controller.ts`。
|
|
51
|
+
- `mirror-runtime` 的 delivery blocked 条件只剩 active IM task;suppression 只通过 `filterSuppressedMirrorRecords` 去掉回声 records,不再整体阻塞其他可投递 mirror turn。
|
|
52
|
+
|
|
53
|
+
已验证:
|
|
54
|
+
|
|
55
|
+
- `npm run typecheck` 通过。
|
|
56
|
+
- `npm run build` 通过。
|
|
57
|
+
- `npm test -- --test-name-pattern='bridge-manager mirror subscription recovery|mirror-runtime pending deliveries|turn-classifier|mirror-subscription-registry|session-bindings uniqueness|JsonFileStore'` 通过。当前测试脚本实际执行了 336 个测试,全部通过。
|
|
58
|
+
- `npm test -- --test-name-pattern='turn-coordinator|desktop-terminal-router|bridge-manager mirror terminal finalization|mirror-runtime pending deliveries|interactive-message-runner|mirror-subscription-registry'` 通过。当前测试脚本实际执行了 341 个测试,全部通过。
|
|
59
|
+
- `npm test -- --test-name-pattern='response-assembler|delivery-pipeline|interactive-message-runner|bridge-manager mirror terminal finalization|mirror-runtime pending deliveries|outbound-artifacts|mirror-subscription-registry|turn-coordinator|desktop-terminal-router'` 通过。当前测试脚本实际执行了 347 个测试,全部通过。
|
|
60
|
+
- `npm test -- --test-name-pattern='stream-state|interactive-message-runner|mirror-turns|bridge-manager status formatting|mirror-runtime pending deliveries|response-assembler|delivery-pipeline|turn-coordinator|desktop-terminal-router'` 通过。当前测试脚本实际执行了 352 个测试,全部通过。
|
|
61
|
+
- `npm test -- --test-name-pattern='interactive-message-runner|turn-coordinator|desktop-terminal-router|bridge-manager status formatting|mirror-runtime pending deliveries|mirror-subscription-registry|response-assembler|delivery-pipeline'` 通过。当前测试脚本实际执行了 350 个测试,全部通过。
|
|
62
|
+
- `git diff --check` 通过,仅有工作区 CRLF 提示。
|
|
63
|
+
|
|
64
|
+
下一步:
|
|
65
|
+
|
|
66
|
+
- 阶段 5 已完成;当前不建议继续大拆。
|
|
67
|
+
- 后续建议进入上线前审查、全量验证、提交发布准备。
|
|
68
|
+
- 后续任何清理仍必须保持 health/status 查询只读,不能把诊断命令当作运行态修复入口。
|
|
69
|
+
|
|
70
|
+
## 目标
|
|
71
|
+
|
|
72
|
+
- 明确建模三类 turn:`im_sdk`、`im_desktop_reuse`、`desktop_mirror`。
|
|
73
|
+
- 拆清 `codex_thread_id` 与 `desktop_thread_id`,避免用一个字段表达两种线程。
|
|
74
|
+
- 建立统一 `TurnCoordinator`,由它负责 active turn ownership、terminal 认领、取消、完成。
|
|
75
|
+
- Desktop JSONL records 先尝试被 active IM Desktop turn 认领,未认领的才进入 mirror delivery。
|
|
76
|
+
- 将最终响应组装集中到 `ResponseAssembler`。
|
|
77
|
+
- 将卡片收尾、文本发送、附件发送集中到 `DeliveryPipeline`。
|
|
78
|
+
- 将流式状态区集中到 `StreamState`。
|
|
79
|
+
- 保证 `/status` 和 `//` 健康检查只读,无任何写入副作用。
|
|
80
|
+
- 保证测试覆盖三类 turn 的主路径和主要竞态。
|
|
81
|
+
|
|
82
|
+
## 非目标
|
|
83
|
+
|
|
84
|
+
- 不重写 Feishu 或 Weixin adapter 的底层 API 实现。
|
|
85
|
+
- 不改变用户可见命令语义,除非是为了消除 bug。
|
|
86
|
+
- 不移除现有配置文件格式,但会增加向后兼容迁移。
|
|
87
|
+
- 不在开发阶段自动重启本机服务。
|
|
88
|
+
|
|
89
|
+
## 目标模型
|
|
90
|
+
|
|
91
|
+
新增统一 turn 类型:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
export type BridgeTurnKind =
|
|
95
|
+
| 'im_sdk'
|
|
96
|
+
| 'im_desktop_reuse'
|
|
97
|
+
| 'desktop_mirror';
|
|
98
|
+
|
|
99
|
+
export type BridgeTurnOrigin = 'im' | 'desktop';
|
|
100
|
+
export type BridgeTurnProgressSource = 'sdk_stream' | 'desktop_jsonl';
|
|
101
|
+
export type BridgeTurnFinalSource = 'sdk_result' | 'desktop_task_complete';
|
|
102
|
+
|
|
103
|
+
export interface ActiveBridgeTurn {
|
|
104
|
+
id: string;
|
|
105
|
+
sessionId: string;
|
|
106
|
+
kind: BridgeTurnKind;
|
|
107
|
+
origin: BridgeTurnOrigin;
|
|
108
|
+
progressSource: BridgeTurnProgressSource;
|
|
109
|
+
finalSource: BridgeTurnFinalSource;
|
|
110
|
+
codexThreadId?: string;
|
|
111
|
+
desktopThreadId?: string;
|
|
112
|
+
requestMessageId?: string;
|
|
113
|
+
streamKey?: string;
|
|
114
|
+
startedAt: number;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
三类 turn 的权威来源:
|
|
119
|
+
|
|
120
|
+
| Turn 类型 | 发起方 | 进度来源 | 最终来源 | mirror delivery |
|
|
121
|
+
| --- | --- | --- | --- | --- |
|
|
122
|
+
| `im_sdk` | IM | SDK stream | SDK result | 否 |
|
|
123
|
+
| `im_desktop_reuse` | IM | SDK stream + Desktop JSONL | Desktop `task_complete.last_agent_message` | 否 |
|
|
124
|
+
| `desktop_mirror` | Desktop | Desktop JSONL | Desktop `task_complete.last_agent_message` | 是 |
|
|
125
|
+
|
|
126
|
+
## 数据模型重构
|
|
127
|
+
|
|
128
|
+
当前 `BridgeSession.sdk_session_id` 需要保留兼容,但新增明确字段:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
export interface BridgeSession {
|
|
132
|
+
sdk_session_id?: string;
|
|
133
|
+
codex_thread_id?: string;
|
|
134
|
+
desktop_thread_id?: string;
|
|
135
|
+
thread_origin?: 'bridge' | 'desktop';
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
字段语义:
|
|
140
|
+
|
|
141
|
+
- `codex_thread_id`:Codex SDK resume id,用于 SDK provider 继续对话。
|
|
142
|
+
- `desktop_thread_id`:Codex Desktop thread id,用于 Desktop JSONL mirror。
|
|
143
|
+
- `thread_origin = 'bridge'`:由 IM/bridge 创建的 SDK thread。
|
|
144
|
+
- `thread_origin = 'desktop'`:由 Codex Desktop 创建或被 `/t` 接管的 thread。
|
|
145
|
+
- `sdk_session_id`:旧字段,仅用于兼容读取和迁移,后续业务逻辑不再直接依赖它判断 Desktop thread。
|
|
146
|
+
|
|
147
|
+
迁移规则:
|
|
148
|
+
|
|
149
|
+
- 如果旧 `sdk_session_id` 能在 Desktop session 列表中找到,则迁移为 `desktop_thread_id` 和 `codex_thread_id`,`thread_origin = 'desktop'`。
|
|
150
|
+
- 如果旧 `sdk_session_id` 找不到 Desktop session,则迁移为 `codex_thread_id`,`thread_origin = 'bridge'`。
|
|
151
|
+
- 新建纯 IM session 不写 `desktop_thread_id`。
|
|
152
|
+
- `/t` 接管 Desktop thread 时写 `desktop_thread_id`,并允许 `codex_thread_id` 同步为同一个 thread id。
|
|
153
|
+
- mirror subscription 只能基于 `desktop_thread_id` 创建。
|
|
154
|
+
|
|
155
|
+
涉及文件:
|
|
156
|
+
|
|
157
|
+
- `src/lib/bridge/host.ts`
|
|
158
|
+
- `src/store.ts`
|
|
159
|
+
- `src/session-bindings.ts`
|
|
160
|
+
- `src/lib/bridge/channel-router.ts`
|
|
161
|
+
- `src/lib/bridge/mirror-subscription-registry.ts`
|
|
162
|
+
|
|
163
|
+
## 新增模块
|
|
164
|
+
|
|
165
|
+
### `src/lib/bridge/turns/turn-types.ts`
|
|
166
|
+
|
|
167
|
+
职责:
|
|
168
|
+
|
|
169
|
+
- 定义 turn 类型、最终响应类型、流式事件类型。
|
|
170
|
+
- 所有模块共享这些类型,避免隐式对象结构。
|
|
171
|
+
|
|
172
|
+
主要导出:
|
|
173
|
+
|
|
174
|
+
- `BridgeTurnKind`
|
|
175
|
+
- `ActiveBridgeTurn`
|
|
176
|
+
- `BridgeTurnClassification`
|
|
177
|
+
- `FinalizedBridgeResponse`
|
|
178
|
+
- `BridgeTurnTerminalRecord`
|
|
179
|
+
|
|
180
|
+
### `src/lib/bridge/turns/turn-classifier.ts`
|
|
181
|
+
|
|
182
|
+
职责:
|
|
183
|
+
|
|
184
|
+
- 根据 `binding`、`session`、`desktop_thread_id`、`codex_thread_id` 判断 IM 消息属于哪类 turn。
|
|
185
|
+
- 纯 IM 已有 `codex_thread_id` 时仍应判定为 `im_sdk`。
|
|
186
|
+
- 只有存在 `desktop_thread_id` 且能定位 Desktop session 时,才判定为 `im_desktop_reuse`。
|
|
187
|
+
|
|
188
|
+
主要导出:
|
|
189
|
+
|
|
190
|
+
- `classifyInteractiveTurn(binding, session, desktopLookup): BridgeTurnClassification`
|
|
191
|
+
- `isDesktopBackedSession(session): boolean`
|
|
192
|
+
|
|
193
|
+
### `src/lib/bridge/turns/turn-coordinator.ts`
|
|
194
|
+
|
|
195
|
+
职责:
|
|
196
|
+
|
|
197
|
+
- 注册 active IM turn。
|
|
198
|
+
- 管理 active turn 的 abort、final terminal、完成结果。
|
|
199
|
+
- 提供 Desktop terminal record 的认领入口。
|
|
200
|
+
- 保证同一个 terminal record 不会同时被 IM turn 和 mirror delivery 消费。
|
|
201
|
+
- 对 binding stale 的情况生成旧会话完成提示。
|
|
202
|
+
|
|
203
|
+
主要能力:
|
|
204
|
+
|
|
205
|
+
- `registerInteractiveTurn(turn)`
|
|
206
|
+
- `getActiveTurn(sessionId)`
|
|
207
|
+
- `claimDesktopTerminal(sessionId, record)`
|
|
208
|
+
- `completeFromSdkResult(turnId, result)`
|
|
209
|
+
- `completeFromDesktopTerminal(turnId, terminal)`
|
|
210
|
+
- `abortTurn(sessionId, reason)`
|
|
211
|
+
- `releaseTurn(turnId)`
|
|
212
|
+
|
|
213
|
+
### `src/lib/bridge/turns/desktop-terminal-router.ts`
|
|
214
|
+
|
|
215
|
+
职责:
|
|
216
|
+
|
|
217
|
+
- mirror runtime 读取 Desktop records 后,先交给该模块分流。
|
|
218
|
+
- 被 active `im_desktop_reuse` 认领的 records 不再进入 mirror delivery。
|
|
219
|
+
- 未被认领的 records 保留给 `desktop_mirror`。
|
|
220
|
+
|
|
221
|
+
主要导出:
|
|
222
|
+
|
|
223
|
+
- `routeDesktopRecords(sessionId, records, coordinator): { claimed: DesktopMirrorRecord[]; unclaimed: DesktopMirrorRecord[] }`
|
|
224
|
+
|
|
225
|
+
### `src/lib/bridge/turns/response-assembler.ts`
|
|
226
|
+
|
|
227
|
+
职责:
|
|
228
|
+
|
|
229
|
+
- 统一组装最终响应。
|
|
230
|
+
- 统一解析 `<cti-send>`。
|
|
231
|
+
- 统一 strip streaming 中不应该展示的附件协议。
|
|
232
|
+
- 去重附件。
|
|
233
|
+
- 处理 stale binding notice。
|
|
234
|
+
|
|
235
|
+
主要导出:
|
|
236
|
+
|
|
237
|
+
- `assembleSdkFinalResponse(input): FinalizedBridgeResponse`
|
|
238
|
+
- `assembleDesktopFinalResponse(input): FinalizedBridgeResponse`
|
|
239
|
+
- `mergeFinalResponses(primary, fallback): FinalizedBridgeResponse`
|
|
240
|
+
- `stripFinalOnlyBlocksForStreaming(text): string`
|
|
241
|
+
|
|
242
|
+
### `src/lib/bridge/turns/delivery-pipeline.ts`
|
|
243
|
+
|
|
244
|
+
职责:
|
|
245
|
+
|
|
246
|
+
- 统一处理最终发送。
|
|
247
|
+
- Feishu 卡片已 finalize 时,只补发附件。
|
|
248
|
+
- 普通文本和附件发送顺序明确。
|
|
249
|
+
- 失败 fallback 统一。
|
|
250
|
+
- 审计和 dedup key 统一。
|
|
251
|
+
|
|
252
|
+
主要导出:
|
|
253
|
+
|
|
254
|
+
- `deliverFinalResponse(context, response): Promise<SendResult>`
|
|
255
|
+
- `finalizeStreamingUi(context, response): Promise<boolean>`
|
|
256
|
+
|
|
257
|
+
### `src/lib/bridge/turns/stream-state.ts`
|
|
258
|
+
|
|
259
|
+
职责:
|
|
260
|
+
|
|
261
|
+
- 管理流式 UI 状态。
|
|
262
|
+
- 统一正文、工具、计划、状态文案、运行时长、上次响应距今。
|
|
263
|
+
- 分离 `lastActivityAt` 和 `lastContentResponseAt`。
|
|
264
|
+
|
|
265
|
+
关键规则:
|
|
266
|
+
|
|
267
|
+
- 正文输出更新 `lastContentResponseAt`。
|
|
268
|
+
- 工具、计划、状态说明只更新 `lastActivityAt`。
|
|
269
|
+
- 状态区刷新应随正文、工具、计划更新一起触发。
|
|
270
|
+
- 三分钟后如果正文无更新,`上次响应距今` 应继续变化。
|
|
271
|
+
- 从未有正文输出时,以 turn start 作为兜底基准。
|
|
272
|
+
|
|
273
|
+
## 现有模块调整
|
|
274
|
+
|
|
275
|
+
### `src/lib/bridge/bridge-manager.ts`
|
|
276
|
+
|
|
277
|
+
调整方向:
|
|
278
|
+
|
|
279
|
+
- 保留 bridge lifecycle、adapter loop、command 分发。
|
|
280
|
+
- 普通消息进入后交给 `turn-classifier` 和 `turn-coordinator`。
|
|
281
|
+
- mirror records 进入后先交给 `desktop-terminal-router`。
|
|
282
|
+
- 删除或降级 `deliverTerminalArtifactsFromMirrorRecord`。
|
|
283
|
+
- 减少对 mirror suppression 的直接业务判断。
|
|
284
|
+
|
|
285
|
+
### `src/lib/bridge/interactive-message-runner.ts`
|
|
286
|
+
|
|
287
|
+
调整方向:
|
|
288
|
+
|
|
289
|
+
- 收窄为 IM turn 执行器。
|
|
290
|
+
- 不再直接持有 Desktop terminal 补偿逻辑。
|
|
291
|
+
- 不再直接决定 Desktop final source。
|
|
292
|
+
- SDK stream 回调统一写入 `StreamState`。
|
|
293
|
+
- 最终结果交给 `TurnCoordinator` 和 `DeliveryPipeline`。
|
|
294
|
+
|
|
295
|
+
### `src/lib/bridge/conversation-engine.ts`
|
|
296
|
+
|
|
297
|
+
调整方向:
|
|
298
|
+
|
|
299
|
+
- 只负责向 LLM provider 发起请求并消费 SDK stream。
|
|
300
|
+
- 返回 SDK result,不直接承担最终业务发送决策。
|
|
301
|
+
- 可以保留 SDK stream 中的初步 artifact parse,但最终 parse 权威在 `ResponseAssembler`。
|
|
302
|
+
- `onPromptPrepared` 只作为 turn metadata 输入,不再驱动 mirror suppression ownership。
|
|
303
|
+
|
|
304
|
+
### `src/lib/bridge/mirror-runtime.ts`
|
|
305
|
+
|
|
306
|
+
调整方向:
|
|
307
|
+
|
|
308
|
+
- 继续负责 Desktop JSONL 文件监听、读取、cursor。
|
|
309
|
+
- 读到 records 后调用 `desktop-terminal-router`。
|
|
310
|
+
- unclaimed records 才进入 mirror delivery plan。
|
|
311
|
+
- claimed records 仍可用于健康状态观察,但不能重复投递。
|
|
312
|
+
|
|
313
|
+
### `src/lib/bridge/mirror-delivery-plan.ts`
|
|
314
|
+
|
|
315
|
+
调整方向:
|
|
316
|
+
|
|
317
|
+
- 只处理 unclaimed Desktop mirror records。
|
|
318
|
+
- 不再参与 IM Desktop reuse 的 terminal ownership。
|
|
319
|
+
|
|
320
|
+
### `src/lib/bridge/mirror-suppression.ts`
|
|
321
|
+
|
|
322
|
+
调整方向:
|
|
323
|
+
|
|
324
|
+
- 仅保留防重复、回声过滤。
|
|
325
|
+
- 不再承担 active IM turn terminal record 的主路由。
|
|
326
|
+
- 逐步降低 suppression 对正确性的依赖。
|
|
327
|
+
|
|
328
|
+
### `src/lib/bridge/feedback-delivery.ts`
|
|
329
|
+
|
|
330
|
+
调整方向:
|
|
331
|
+
|
|
332
|
+
- 保留底层文本和附件发送。
|
|
333
|
+
- 上层调用集中到 `DeliveryPipeline`。
|
|
334
|
+
- 不再让多个上层模块各自拼装附件发送流程。
|
|
335
|
+
|
|
336
|
+
### `src/lib/bridge/outbound-artifacts.ts`
|
|
337
|
+
|
|
338
|
+
调整方向:
|
|
339
|
+
|
|
340
|
+
- 保留纯解析函数。
|
|
341
|
+
- 业务调用集中到 `ResponseAssembler`。
|
|
342
|
+
|
|
343
|
+
### `src/lib/bridge/session-health-runtime.ts`
|
|
344
|
+
|
|
345
|
+
调整方向:
|
|
346
|
+
|
|
347
|
+
- 用户触发的诊断保持只读。
|
|
348
|
+
- 运行态写入只来自 turn lifecycle 和 mirror observe。
|
|
349
|
+
- 不允许健康检查触发任务收尾、卡片收尾、session 修复。
|
|
350
|
+
|
|
351
|
+
## 分阶段开发计划
|
|
352
|
+
|
|
353
|
+
### 阶段 1:类型与线程字段地基
|
|
354
|
+
|
|
355
|
+
状态:已完成(2026-04-27)
|
|
356
|
+
|
|
357
|
+
开发内容:
|
|
358
|
+
|
|
359
|
+
- 新增 `turn-types.ts`。
|
|
360
|
+
- 新增 `turn-classifier.ts`。
|
|
361
|
+
- `BridgeSession` 增加 `codex_thread_id`、`desktop_thread_id`、`thread_origin`。
|
|
362
|
+
- `store` 增加字段读写和迁移兼容。
|
|
363
|
+
- `session-bindings` 拆分 Desktop thread 绑定和 Codex SDK thread 绑定。
|
|
364
|
+
- `mirror-subscription-registry` 改为只认 `desktop_thread_id`。
|
|
365
|
+
|
|
366
|
+
实际落地:
|
|
367
|
+
|
|
368
|
+
- 新增 `src/lib/bridge/turns/turn-types.ts`。
|
|
369
|
+
- 新增 `src/lib/bridge/turns/turn-classifier.ts`。
|
|
370
|
+
- `src/lib/bridge/host.ts` 为 `BridgeSession` 增加线程身份字段。
|
|
371
|
+
- `src/store.ts` 的 `updateSdkSessionId` 只标记 bridge-side Codex resume thread,`findSessionBySdkSessionId` 兼容旧字段和新字段。
|
|
372
|
+
- `src/session-bindings.ts` 在 Desktop 绑定时显式写入 `desktop_thread_id`。
|
|
373
|
+
- `src/lib/bridge/mirror-subscription-registry.ts` 只基于显式 Desktop thread 生成 mirror subscription。
|
|
374
|
+
- `src/lib/bridge/mirror-runtime.ts` 使用 `desktop_thread_id` 作为镜像线程来源。
|
|
375
|
+
- `src/lib/bridge/command-dispatch.ts` 的 `/model`、`/status`、`/history` 使用 `desktop_thread_id` 判断是否共享桌面线程。
|
|
376
|
+
- `src/lib/bridge/interactive-message-runner.ts` 只在 Desktop-backed session 上等待 Desktop terminal finalization。
|
|
377
|
+
- 补充 `src/__tests__/turn-classifier.test.ts`,并更新 store、session binding、mirror subscription、mirror runtime、bridge manager 相关测试。
|
|
378
|
+
|
|
379
|
+
验收标准:
|
|
380
|
+
|
|
381
|
+
- 纯 IM 已有 `codex_thread_id` 时不会创建 mirror subscription。
|
|
382
|
+
- `/t` 接管 Desktop thread 后会写入 `desktop_thread_id`。
|
|
383
|
+
- 旧数据能兼容读取。
|
|
384
|
+
|
|
385
|
+
阶段备注:
|
|
386
|
+
|
|
387
|
+
- 旧字段 `sdk_session_id` 暂时保留为兼容字段,后续业务判断应优先使用 `codex_thread_id` / `desktop_thread_id`。
|
|
388
|
+
- 历史上没有 `thread_origin` 的旧纯 IM 数据不会被自动当作 Desktop thread。
|
|
389
|
+
- 后续阶段仍需要把 terminal ownership 从 mirror suppression/grace wait 中彻底抽出。
|
|
390
|
+
|
|
391
|
+
### 阶段 2:TurnCoordinator 与 terminal ownership
|
|
392
|
+
|
|
393
|
+
状态:主路径已完成(2026-04-27)
|
|
394
|
+
|
|
395
|
+
开发内容:
|
|
396
|
+
|
|
397
|
+
- 新增 `turn-coordinator.ts`。
|
|
398
|
+
- 新增 `desktop-terminal-router.ts`。
|
|
399
|
+
- `interactive-message-runner` 注册 active turn。
|
|
400
|
+
- `mirror-runtime` 读到 records 后先走 terminal router。
|
|
401
|
+
- `task_complete` 被 active `im_desktop_reuse` 认领后,不再进入 mirror delivery。
|
|
402
|
+
|
|
403
|
+
实际落地:
|
|
404
|
+
|
|
405
|
+
- 新增 `src/lib/bridge/turns/turn-coordinator.ts`。
|
|
406
|
+
- 新增 `src/lib/bridge/turns/desktop-terminal-router.ts`。
|
|
407
|
+
- `interactive-message-runner` 注册并释放 active bridge turn。
|
|
408
|
+
- `bridge-manager` 创建全局 `TURN_COORDINATOR`,并将 Desktop terminal finalization 连接到 `INTERACTIVE_RUNTIME.finalizeTerminalActiveTask`。
|
|
409
|
+
- `mirror-runtime` 读取 Desktop records 后先调用 `routeDesktopRecords`,claimed records 不再进入 health observe / mirror delivery。
|
|
410
|
+
- 补充 `src/__tests__/turn-coordinator.test.ts` 和 `src/__tests__/desktop-terminal-router.test.ts`。
|
|
411
|
+
|
|
412
|
+
验收标准:
|
|
413
|
+
|
|
414
|
+
- SDK 先结束,Desktop terminal 后到,最终使用 Desktop final。
|
|
415
|
+
- Desktop terminal 先到,SDK stream 被正确收尾。
|
|
416
|
+
- Desktop 自己发起的 turn 不会被 IM active turn 误认领。
|
|
417
|
+
- 不出现重复回复。
|
|
418
|
+
|
|
419
|
+
阶段备注:
|
|
420
|
+
|
|
421
|
+
- 旧的 `finalizeInteractiveTaskFromMirrorRecords` 已在阶段 5 删除,不再作为 mirror observe 的隐式副作用。
|
|
422
|
+
- 旧的 `desktopTerminalFinalizationGraceMs` 已在阶段 5 移除,替换为 Desktop terminal final timeout。
|
|
423
|
+
- 当前阶段已经把 terminal ownership 的主入口前移到 mirror runtime record routing,但最终文本/附件组装仍未集中,下一阶段处理。
|
|
424
|
+
|
|
425
|
+
### 阶段 3:ResponseAssembler 与 DeliveryPipeline
|
|
426
|
+
|
|
427
|
+
状态:主路径已完成(2026-04-27)
|
|
428
|
+
|
|
429
|
+
开发内容:
|
|
430
|
+
|
|
431
|
+
- 新增 `response-assembler.ts`。
|
|
432
|
+
- 新增 `delivery-pipeline.ts`。
|
|
433
|
+
- 将 SDK final、Desktop terminal、mirror final 都改为统一组装。
|
|
434
|
+
- 附件解析、附件去重、stale notice 集中处理。
|
|
435
|
+
- 卡片 finalize 和附件补发集中处理。
|
|
436
|
+
|
|
437
|
+
实际落地:
|
|
438
|
+
|
|
439
|
+
- 新增 `src/lib/bridge/turns/response-assembler.ts`。
|
|
440
|
+
- 新增 `src/lib/bridge/turns/delivery-pipeline.ts`。
|
|
441
|
+
- `interactive-message-runner` 使用 assembler 处理 SDK final、Desktop terminal final、stale notice 和 error response。
|
|
442
|
+
- `interactive-message-runner` 使用 pipeline 统一处理最终发送和流式 UI finalize。
|
|
443
|
+
- `bridge-manager` 的 mirror final 使用 assembler 清理 Desktop final text 和提取附件。
|
|
444
|
+
- `bridge-manager` 的 mirror final 使用 pipeline 发送普通镜像文本、卡片 finalize 后附件补发。
|
|
445
|
+
- 补充 `src/__tests__/response-assembler.test.ts` 和 `src/__tests__/delivery-pipeline.test.ts`。
|
|
446
|
+
|
|
447
|
+
验收标准:
|
|
448
|
+
|
|
449
|
+
- `<cti-send>` 在所有路径只解析一次。
|
|
450
|
+
- Feishu 卡片正文不显示附件协议。
|
|
451
|
+
- 卡片已收尾时,附件仍能发送。
|
|
452
|
+
- Weixin 不支持直发附件时能返回本地路径提示。
|
|
453
|
+
|
|
454
|
+
阶段备注:
|
|
455
|
+
|
|
456
|
+
- `conversation-engine` 当前仍会在保存 assistant message 时解析一次 `<cti-send>`,用于持久化清理和返回附件;assembler 对已清理文本再次处理是幂等的。后续若要做到严格“物理只解析一次”,需要把 conversation-engine 的持久化解析也改为调用 assembler 或返回 raw final blocks。
|
|
457
|
+
- mirror 文本仍需先用 clean final text 进入 `formatMirrorMessage`,因此 assembler 在 mirror path 中先处理 raw Desktop final,再将渲染后的镜像正文交给 delivery pipeline。
|
|
458
|
+
- 旧的 `deliverTerminalArtifactsFromMirrorRecord` 已在阶段 5 删除,terminal 附件不再通过 mirror observe 旁路补发。
|
|
459
|
+
|
|
460
|
+
### 阶段 4:StreamState 收敛状态区
|
|
461
|
+
|
|
462
|
+
状态:主路径已完成(2026-04-27)
|
|
463
|
+
|
|
464
|
+
开发内容:
|
|
465
|
+
|
|
466
|
+
- 新增 `stream-state.ts`。
|
|
467
|
+
- `interactive-message-runner` 和 mirror streaming 都改用统一状态。
|
|
468
|
+
- 分离 `lastActivityAt` 和 `lastContentResponseAt`。
|
|
469
|
+
- 工具、计划、正文、状态说明更新时都触发状态区刷新。
|
|
470
|
+
|
|
471
|
+
实际落地:
|
|
472
|
+
|
|
473
|
+
- 新增 `src/lib/bridge/turns/stream-state.ts`,集中处理运行时长、`上次响应距今`、正文响应基准和 activity 基准。
|
|
474
|
+
- `src/lib/bridge/interactive-message-runner.ts` 使用 `StreamState` 记录正文响应、工具、计划、权限等待和状态说明。
|
|
475
|
+
- `src/lib/bridge/mirror-turns.ts` 为 mirror pending turn 增加 `lastContentResponseAt`,并保留旧 `lastResponseAt` 兼容读取。
|
|
476
|
+
- `src/lib/bridge/bridge-manager.ts` 的 mirror streaming status 刷新改用 `StreamState` 格式化逻辑,旧数据缺少正文响应时间时回退到 turn start。
|
|
477
|
+
- 补充 `src/__tests__/stream-state.test.ts`,并更新 interactive runner、mirror turns、bridge manager 相关测试。
|
|
478
|
+
|
|
479
|
+
验收标准:
|
|
480
|
+
|
|
481
|
+
- 长任务三分钟后显示并持续更新 `上次响应距今`。
|
|
482
|
+
- 工具和计划更新不会重置正文响应时间。
|
|
483
|
+
- 正文更新会重置正文响应时间。
|
|
484
|
+
- `已运行` 和 `上次响应距今` 至少一个会按预期变化。
|
|
485
|
+
|
|
486
|
+
阶段备注:
|
|
487
|
+
|
|
488
|
+
- `lastActivityAt` 仍用于健康状态和“是否有运行进展”的判断;用户可见的 `上次响应距今` 只看正文成功响应时间。
|
|
489
|
+
- 从未产生正文时,`上次响应距今` 以 turn start 为兜底基准,避免长任务没有任何正文时状态区完全静止。
|
|
490
|
+
- 旧 mirror pending turn 的 `lastResponseAt` 暂时保留为兼容字段,后续阶段 5 再评估是否清理。
|
|
491
|
+
|
|
492
|
+
### 阶段 5:删除旧补丁和降低 suppression 依赖
|
|
493
|
+
|
|
494
|
+
状态:已完成(2026-04-27)
|
|
495
|
+
|
|
496
|
+
开发内容:
|
|
497
|
+
|
|
498
|
+
- 删除 `desktopTerminalFinalizationGraceMs` 作为主路径。
|
|
499
|
+
- 删除或降级 `deliverTerminalArtifactsFromMirrorRecord`。
|
|
500
|
+
- 删除散落的最终附件解析。
|
|
501
|
+
- 收窄 mirror suppression。
|
|
502
|
+
- 清理 bridge-manager 中的 turn orchestration。
|
|
503
|
+
|
|
504
|
+
已完成:
|
|
505
|
+
|
|
506
|
+
- 删除 `src/lib/bridge/bridge-manager.ts` 中的 `finalizeInteractiveTaskFromMirrorRecords`。
|
|
507
|
+
- 删除 `src/lib/bridge/bridge-manager.ts` 中的 `deliverTerminalArtifactsFromMirrorRecord`。
|
|
508
|
+
- mirror runtime 的 `observeSessionHealthRecords` 只做 health observe,不再触发 IM active task finalize 或 terminal artifact 补发。
|
|
509
|
+
- `_testOnly` 不再暴露旧的 mirror terminal finalization 入口。
|
|
510
|
+
- `desktopTerminalFinalizationGraceMs` 改为 `desktopTerminalFinalizationTimeoutMs`,并把默认等待从 4 秒提升到 30 秒,明确表达“等待 Desktop terminal 作为 canonical final source”。
|
|
511
|
+
- 新增 `src/lib/bridge/turns/final-response-artifacts.ts`,统一最终响应正文清理和附件去重。
|
|
512
|
+
- `src/lib/bridge/turns/response-assembler.ts` 和 `src/lib/bridge/conversation-engine.ts` 都改为复用 `final-response-artifacts`。
|
|
513
|
+
- 新增 `src/lib/bridge/mirror-feedback-controller.ts`,集中 mirror streaming UI 更新、状态刷新、卡片收尾和最终消息投递。
|
|
514
|
+
- `src/lib/bridge/bridge-manager.ts` 只保留 mirror feedback controller 的初始化和对 `mirror-runtime` 的回调包装。
|
|
515
|
+
- `src/lib/bridge/mirror-runtime.ts` 不再依赖 `isMirrorSuppressed` 判断全局 blocked;suppression 只影响 `filterSuppressedMirrorRecords` 的 records 过滤结果。
|
|
516
|
+
- `src/lib/bridge/interactive-runtime.ts` 不再根据 terminal health 状态 finalize active IM task,避免 health/reconcile 成为隐式收尾路径。
|
|
517
|
+
- `src/lib/bridge/interactive-message-runner.ts` 的 Desktop terminal final 等待不再依赖 `mirrorSuppressionId`,并且只在 Desktop-backed IM turn 创建 suppression。
|
|
518
|
+
- 补充 `src/__tests__/mirror-runtime.test.ts` 用例,覆盖 suppression filtering 后剩余 mirror records 仍会正常投递。
|
|
519
|
+
- 已评估 `src/lib/bridge/bridge-manager.ts` 中剩余 mirror reconcile wiring:当前主要是依赖装配、配置读取、状态同步和测试出口,继续下沉收益低,不建议为了拆分而拆分。
|
|
520
|
+
|
|
521
|
+
验收标准:
|
|
522
|
+
|
|
523
|
+
- 代码路径更短,turn ownership 单一。
|
|
524
|
+
- mirror suppression 失效也不会导致 IM Desktop final 丢失。
|
|
525
|
+
- 全量测试通过。
|
|
526
|
+
|
|
527
|
+
## 测试计划
|
|
528
|
+
|
|
529
|
+
### 单元测试
|
|
530
|
+
|
|
531
|
+
新增测试文件:
|
|
532
|
+
|
|
533
|
+
- `src/__tests__/turn-classifier.test.ts`
|
|
534
|
+
- `src/__tests__/turn-coordinator.test.ts`
|
|
535
|
+
- `src/__tests__/desktop-terminal-router.test.ts`
|
|
536
|
+
- `src/__tests__/response-assembler.test.ts`
|
|
537
|
+
- `src/__tests__/delivery-pipeline.test.ts`
|
|
538
|
+
- `src/__tests__/stream-state.test.ts`
|
|
539
|
+
|
|
540
|
+
### 关键测试用例
|
|
541
|
+
|
|
542
|
+
`turn-classifier`:
|
|
543
|
+
|
|
544
|
+
- 无 `desktop_thread_id`,有 `codex_thread_id`,判定为 `im_sdk`。
|
|
545
|
+
- 有真实 `desktop_thread_id`,判定为 `im_desktop_reuse`。
|
|
546
|
+
- `desktop_thread_id` 找不到 Desktop session 时降级或标记 stale。
|
|
547
|
+
|
|
548
|
+
`turn-coordinator`:
|
|
549
|
+
|
|
550
|
+
- 注册 active IM turn。
|
|
551
|
+
- SDK result 先到,等待 Desktop terminal。
|
|
552
|
+
- Desktop terminal 先到,完成 active turn。
|
|
553
|
+
- `/stop` abort 后 terminal 到达不重复发送。
|
|
554
|
+
- binding 已切换时生成 stale notice。
|
|
555
|
+
|
|
556
|
+
`desktop-terminal-router`:
|
|
557
|
+
|
|
558
|
+
- active IM Desktop turn 能认领同一 turn 的 `task_complete`。
|
|
559
|
+
- 未匹配的 Desktop records 保留给 mirror。
|
|
560
|
+
- 多个 active/queued turn 不串线。
|
|
561
|
+
|
|
562
|
+
`response-assembler`:
|
|
563
|
+
|
|
564
|
+
- `<cti-send>` 被移除并解析为附件。
|
|
565
|
+
- 多附件去重。
|
|
566
|
+
- 无正文但有附件时仍能发送。
|
|
567
|
+
- stale notice 时丢弃旧附件。
|
|
568
|
+
|
|
569
|
+
`delivery-pipeline`:
|
|
570
|
+
|
|
571
|
+
- 卡片 finalize 成功后只发送附件。
|
|
572
|
+
- 卡片 finalize 失败后发送文本和附件。
|
|
573
|
+
- 附件发送失败时给出可见错误。
|
|
574
|
+
- Weixin 不支持直发时给出本地路径提示。
|
|
575
|
+
|
|
576
|
+
`stream-state`:
|
|
577
|
+
|
|
578
|
+
- 正文更新刷新 `lastContentResponseAt`。
|
|
579
|
+
- 工具更新只刷新 `lastActivityAt`。
|
|
580
|
+
- 计划更新触发状态区刷新。
|
|
581
|
+
- 超过三分钟后 `上次响应距今` 持续变化。
|
|
582
|
+
|
|
583
|
+
### 回归测试
|
|
584
|
+
|
|
585
|
+
需要保留并改造现有测试:
|
|
586
|
+
|
|
587
|
+
- `interactive-message-runner.test.ts`
|
|
588
|
+
- `bridge-manager.test.ts`
|
|
589
|
+
- `mirror-runtime.test.ts`
|
|
590
|
+
- `mirror-delivery-plan.test.ts`
|
|
591
|
+
- `mirror-subscription-registry.test.ts`
|
|
592
|
+
- `session-health-runtime.test.ts`
|
|
593
|
+
- `command-dispatch.test.ts`
|
|
594
|
+
- `feishu-adapter.test.ts`
|
|
595
|
+
|
|
596
|
+
### 集成验证
|
|
597
|
+
|
|
598
|
+
不启动生产服务的自动验证:
|
|
599
|
+
|
|
600
|
+
- `npm run typecheck`
|
|
601
|
+
- `npm test`
|
|
602
|
+
- `npm run build`
|
|
603
|
+
|
|
604
|
+
手动验证场景:
|
|
605
|
+
|
|
606
|
+
- 纯 IM 连续两轮对话,无 4 秒等待,无 mirror subscription。
|
|
607
|
+
- `/t 1` 接管 Desktop thread 后发送 IM 消息,最终以 Desktop terminal 为准。
|
|
608
|
+
- Desktop 中直接执行任务,IM mirror 正常显示。
|
|
609
|
+
- 飞书长任务卡片状态区持续刷新。
|
|
610
|
+
- 生成图片后通过 `<cti-send>` 发送附件。
|
|
611
|
+
- `/status` 和 `//` 不创建 binding,不修改 session。
|
|
612
|
+
|
|
613
|
+
## 风险与应对
|
|
614
|
+
|
|
615
|
+
风险:旧 `sdk_session_id` 数据无法准确判断来源。
|
|
616
|
+
|
|
617
|
+
应对:迁移时优先查 Desktop session index,找不到则标为 `bridge`,并保留旧字段以便回退。
|
|
618
|
+
|
|
619
|
+
风险:Desktop JSONL `turn_id` 缺失或延迟。
|
|
620
|
+
|
|
621
|
+
应对:优先用 `turn_id` 匹配,缺失时用 prompt signature 和时间窗口兜底。
|
|
622
|
+
|
|
623
|
+
风险:重构期间出现重复回复。
|
|
624
|
+
|
|
625
|
+
应对:`TurnCoordinator` 必须保证 terminal record 一旦 claimed,就不能进入 mirror delivery。
|
|
626
|
+
|
|
627
|
+
风险:Feishu 卡片 finalize 和附件发送顺序变化。
|
|
628
|
+
|
|
629
|
+
应对:`DeliveryPipeline` 明确卡片 finalize 与附件发送幂等规则,并补测试。
|
|
630
|
+
|
|
631
|
+
风险:一次性改动过大。
|
|
632
|
+
|
|
633
|
+
应对:按阶段落地,每阶段都保持可测试、可构建、可回退。
|
|
634
|
+
|
|
635
|
+
## 推荐提交边界
|
|
636
|
+
|
|
637
|
+
建议拆成 5 个提交:
|
|
638
|
+
|
|
639
|
+
1. `Add explicit turn and thread identity model`
|
|
640
|
+
2. `Route desktop terminal records through turn coordinator`
|
|
641
|
+
3. `Unify final response assembly and attachment delivery`
|
|
642
|
+
4. `Centralize streaming state updates`
|
|
643
|
+
5. `Remove legacy terminal artifact fallback paths`
|
|
644
|
+
|
|
645
|
+
## 完成标准
|
|
646
|
+
|
|
647
|
+
- 三类 turn 的来源、进度、最终回复来源在代码中显式可见。
|
|
648
|
+
- 纯 IM 不再被误判为 Desktop mirror。
|
|
649
|
+
- IM 复用 Desktop thread 不再依赖补发附件。
|
|
650
|
+
- Desktop mirror 不再和 active IM turn 抢同一条 terminal record。
|
|
651
|
+
- `<cti-send>` 只在一个统一模块被业务解析。
|
|
652
|
+
- `/status` 和 `//` 无副作用。
|
|
653
|
+
- 全量测试、类型检查、构建通过。
|
package/package.json
CHANGED