lark-remote 0.5.19 → 0.5.21

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 (136) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +72 -46
  10. package/dist/bridge/index.d.ts +106 -153
  11. package/dist/bridge/index.js +400 -1000
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +141 -130
  18. package/dist/card/card-budget.js +123 -98
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +114 -159
  24. package/dist/card/run-state.d.ts +1 -15
  25. package/dist/card/run-state.js +358 -292
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +21 -17
  28. package/dist/card/usage-format.d.ts +22 -7
  29. package/dist/card/usage-format.js +119 -100
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/common/text-width.js +36 -24
  33. package/dist/common/tool-summary.js +132 -90
  34. package/dist/config/codex-config.js +130 -106
  35. package/dist/config/index.d.ts +16 -2
  36. package/dist/config/index.js +23 -8
  37. package/dist/connector/index.js +6 -6
  38. package/dist/error-classification.js +3 -2
  39. package/dist/inbound/placeholder.d.ts +1 -1
  40. package/dist/inbound/placeholder.js +1 -1
  41. package/dist/index.js +387 -281
  42. package/dist/logger/index.js +6 -6
  43. package/dist/message-handlers.d.ts +0 -5
  44. package/dist/message-handlers.js +254 -237
  45. package/dist/model-catalog.d.ts +2 -2
  46. package/dist/model-catalog.js +3 -8
  47. package/dist/platform/identity.js +1 -1
  48. package/dist/platform/path.d.ts +1 -1
  49. package/dist/platform/path.js +1 -1
  50. package/dist/platform/sleep-blocker.js +1 -1
  51. package/dist/platform/terminator-win32.js +2 -1
  52. package/dist/router/commands/ls.d.ts +3 -0
  53. package/dist/router/commands/ls.js +256 -175
  54. package/dist/router/commands/order.js +184 -159
  55. package/dist/router/commands/resume.js +190 -152
  56. package/dist/router/commands/ws.js +219 -174
  57. package/dist/router/config/dsh.d.ts +4 -8
  58. package/dist/router/config/dsh.js +3 -10
  59. package/dist/router/config/kimi.js +1 -1
  60. package/dist/router/config/types.d.ts +1 -1
  61. package/dist/router/config-diff.js +1 -1
  62. package/dist/router/index.d.ts +56 -11
  63. package/dist/router/index.js +401 -391
  64. package/dist/runner/bash/runner.js +2 -4
  65. package/dist/runner/claude/session.d.ts +1 -1
  66. package/dist/runner/claude/session.js +6 -4
  67. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  68. package/dist/runner/codex/app-server/runner.js +30 -25
  69. package/dist/runner/codex/app-server/translator.d.ts +2 -5
  70. package/dist/runner/codex/app-server/translator.js +291 -238
  71. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  72. package/dist/runner/common/acp/acp-approval.js +1 -1
  73. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  74. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  75. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  76. package/dist/runner/common/acp/base-translator.js +50 -26
  77. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  78. package/dist/runner/common/acp/protocol-types.js +2 -2
  79. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  80. package/dist/runner/common/connection-based-runner.js +32 -2
  81. package/dist/runner/common/jsonl-stream.js +154 -187
  82. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  83. package/dist/runner/common/jsonrpc/client.js +43 -12
  84. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  85. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  86. package/dist/runner/common/spawning-runner.d.ts +6 -1
  87. package/dist/runner/common/spawning-runner.js +9 -4
  88. package/dist/runner/common/turn-started.d.ts +8 -0
  89. package/dist/runner/common/turn-started.js +11 -0
  90. package/dist/runner/dsh/approval.js +84 -51
  91. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  92. package/dist/runner/dsh/catalog-probe.js +10 -0
  93. package/dist/runner/dsh/client.d.ts +7 -11
  94. package/dist/runner/dsh/client.js +356 -289
  95. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  96. package/dist/runner/dsh/exit-cancel.js +2 -2
  97. package/dist/runner/dsh/inflight.d.ts +1 -1
  98. package/dist/runner/dsh/inflight.js +1 -1
  99. package/dist/runner/dsh/mux-stream.js +28 -8
  100. package/dist/runner/dsh/runner.d.ts +44 -4
  101. package/dist/runner/dsh/runner.js +296 -199
  102. package/dist/runner/dsh/translator.d.ts +1 -31
  103. package/dist/runner/dsh/translator.js +180 -178
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  106. package/dist/runner/kimi/acp/runner.js +82 -46
  107. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  108. package/dist/runner/kimi/acp/translator.js +3 -4
  109. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  110. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  111. package/dist/runner/opencode/acp/runner.js +4 -4
  112. package/dist/runner/pi/rpc/client.js +4 -13
  113. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  114. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  115. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  116. package/dist/runner/pi/rpc/runner.js +27 -42
  117. package/dist/runner/pi/rpc/translator.js +61 -60
  118. package/dist/runner/types.d.ts +2 -2
  119. package/dist/runner/types.js +1 -1
  120. package/dist/session/claude/sessions.d.ts +45 -0
  121. package/dist/session/claude/sessions.js +160 -122
  122. package/dist/session/codex/rollout-reader.js +181 -182
  123. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  124. package/dist/session/common/throw-session-list-error.js +16 -0
  125. package/dist/session/dsh/sessions.js +291 -235
  126. package/dist/session/kimi/sessions.d.ts +40 -8
  127. package/dist/session/kimi/sessions.js +373 -301
  128. package/dist/session/opencode/sessions.d.ts +142 -1
  129. package/dist/session/opencode/sessions.js +378 -321
  130. package/dist/session/pi/sessions.js +4 -5
  131. package/dist/session/session-store.d.ts +9 -1
  132. package/dist/session/session-store.js +99 -99
  133. package/dist/update/version-check.js +2 -1
  134. package/dist/workspace/index.d.ts +1 -1
  135. package/dist/workspace/index.js +1 -1
  136. package/package.json +1 -1
@@ -21,12 +21,12 @@ function todayStr(d) {
21
21
  return `${y}-${m}-${day}`;
22
22
  }
23
23
  function timestampStr(d) {
24
- // local ISO 8601 with milliseconds and explicit offset, e.g.
25
- // 2026-08-03T01:30:00.000+08:00. Previously this used toISOString() (UTC)
26
- // while the daily directory boundary (todayStr) used local time — after
27
- // local midnight in a +08:00 zone the new day's directory held lines
28
- // timestamped with the previous UTC day. Local offset keeps the timestamp's
29
- // calendar date consistent with the directory date across all timezones.
24
+ // Local ISO 8601 with milliseconds and explicit offset, e.g.
25
+ // 2026-08-03T01:30:00.000+08:00. Must match the daily directory boundary
26
+ // (todayStr), which is local time: a UTC timestamp would put lines written
27
+ // after local midnight into the previous UTC day's file. Local offset keeps
28
+ // the timestamp's calendar date consistent with the directory date across
29
+ // all timezones.
30
30
  const pad = (n) => String(n).padStart(2, '0');
31
31
  const y = d.getFullYear();
32
32
  const mo = pad(d.getMonth() + 1);
@@ -7,11 +7,6 @@ import type { SessionStore } from './session/index.js';
7
7
  import type { WorkspaceStore } from './workspace/index.js';
8
8
  import type { OwnerBinder } from './binder.js';
9
9
  import type { CloneSession } from './clone.js';
10
- /**
11
- * /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
12
- * 共用同一判定,防止将来加别名时两处漂移。
13
- */
14
- export declare function isStopCommand(text: string): boolean;
15
10
  export interface MessageHandlerDeps {
16
11
  connector: FeishuConnector;
17
12
  router: CommandRouter;
@@ -14,264 +14,281 @@ const SESSION_MUTATING_FLUSH_BUDGET_MS = 2_000;
14
14
  * /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
15
15
  * 共用同一判定,防止将来加别名时两处漂移。
16
16
  */
17
- export function isStopCommand(text) {
17
+ function isStopCommand(text) {
18
18
  const t = text.trim().toLowerCase();
19
19
  return t === '/stop' || t === '/t';
20
20
  }
21
- export function setupMessageHandlers(deps) {
22
- const { connector, router, bridge, sessionStore, workspaceStore, binder, logger, config, cloneSession, } = deps;
23
- // 入站统一装配器:图/文任意顺序 → 同一个 turn、同一个 prompt。
24
- const assembler = new InboundTurnAssembler({
25
- onCommit: (turn, prompt) => {
26
- const messageId = turn.messageIds[turn.messageIds.length - 1] ?? '';
27
- const ctx = {
28
- userId: turn.userId,
29
- chatId: turn.chatId,
30
- messageId,
31
- turnMessageIds: turn.messageIds,
32
- };
33
- let cwd = sessionStore.getCwd(turn.userId) ?? '';
34
- if (!cwd) {
35
- const recent = [...workspaceStore.list()].sort((a, b) => b.lastUsedAt - a.lastUsedAt);
36
- if (recent.length > 0)
37
- cwd = recent[0].path;
38
- }
39
- const binding = bridge.currentBinding(turn.userId);
40
- bridge.enqueue(cwd, async () => {
41
- await router.handle(prompt, ctx, {
42
- cwdOverride: cwd,
43
- binding,
44
- allowCommandPrefix: false,
45
- });
46
- }, {
47
- taskMeta: {
48
- userId: turn.userId,
49
- chatId: turn.chatId,
50
- messageId,
51
- messagePreview: prompt.slice(0, 3000),
52
- binding,
53
- },
54
- });
55
- },
56
- onReceipt: async (ctx, text) => {
57
- await bridge.sendResult({ text }, ctx);
21
+ /** 入站统一装配器 commit:图/文组装成 turn → 进 agent 队列。 */
22
+ function handleTurnCommit(ctx, turn, prompt) {
23
+ const messageId = turn.messageIds[turn.messageIds.length - 1] ?? '';
24
+ const taskCtx = {
25
+ userId: turn.userId,
26
+ chatId: turn.chatId,
27
+ messageId,
28
+ turnMessageIds: turn.messageIds,
29
+ };
30
+ let cwd = ctx.sessionStore.getCwd(turn.userId) ?? '';
31
+ if (!cwd) {
32
+ const recent = [...ctx.workspaceStore.list()].sort((a, b) => b.lastUsedAt - a.lastUsedAt);
33
+ if (recent.length > 0)
34
+ cwd = recent[0].path;
35
+ }
36
+ const binding = ctx.bridge.currentBinding(turn.userId);
37
+ ctx.bridge.enqueue(cwd, async () => {
38
+ await ctx.router.handle(prompt, taskCtx, {
39
+ cwdOverride: cwd,
40
+ binding,
41
+ allowCommandPrefix: false,
42
+ });
43
+ }, {
44
+ taskMeta: {
45
+ userId: turn.userId,
46
+ chatId: turn.chatId,
47
+ messageId,
48
+ messagePreview: prompt.slice(0, 3000),
49
+ binding,
58
50
  },
59
51
  });
60
- router.setInboundFlusher(() => assembler.flushAll('flush'));
61
- const authorizer = new InboundAuthorizer({
62
- binder,
63
- onBind: (userId, chatId) => {
64
- let resolvedCwd;
65
- try {
66
- resolvedCwd = fs.realpathSync(process.cwd());
67
- }
68
- catch {
69
- resolvedCwd = process.cwd();
70
- }
71
- if (!sessionStore.getCwd(userId)) {
72
- sessionStore.setCwd(userId, resolvedCwd);
52
+ }
53
+ /** 入站回执(无文本纯附件 / rejected 项):面向用户的提示。 */
54
+ async function handleTurnReceipt(ctx, receiptCtx, text) {
55
+ await ctx.bridge.sendResult({ text }, receiptCtx);
56
+ }
57
+ /** owner 绑定成功后的 onboarding 消息(fire-and-forget)。 */
58
+ function handleBind(ctx, userId, chatId) {
59
+ let resolvedCwd;
60
+ try {
61
+ resolvedCwd = fs.realpathSync(process.cwd());
62
+ }
63
+ catch {
64
+ resolvedCwd = process.cwd();
65
+ }
66
+ if (!ctx.sessionStore.getCwd(userId)) {
67
+ ctx.sessionStore.setCwd(userId, resolvedCwd);
68
+ }
69
+ return (async () => {
70
+ try {
71
+ await ctx.connector.sendWithRetry(chatId, {
72
+ text: `✅ 已绑定到本账号,此后仅你可使用本应用\n` +
73
+ `📂 当前工作目录: \`${resolvedCwd}\`\n` +
74
+ `🤖 默认 Agent: ${agentDisplayName(ctx.config.defaultAgent)}\n` +
75
+ `💡 直接输入消息即可开始对话,或 /cd 切换目录`,
76
+ });
77
+ const helpResult = ctx.router.cmdHelp();
78
+ if (helpResult.card) {
79
+ await ctx.connector.sendWithRetry(chatId, { card: helpResult.card });
73
80
  }
74
- return (async () => {
75
- try {
76
- await connector.sendWithRetry(chatId, {
77
- text: `✅ 已绑定到本账号,此后仅你可使用本应用\n` +
78
- `📂 当前工作目录: \`${resolvedCwd}\`\n` +
79
- `🤖 默认 Agent: ${agentDisplayName(config.defaultAgent)}\n` +
80
- `💡 直接输入消息即可开始对话,或 /cd 切换目录`,
81
- });
82
- const helpResult = router.cmdHelp();
83
- if (helpResult.card) {
84
- await connector.sendWithRetry(chatId, { card: helpResult.card });
85
- }
86
- }
87
- catch (err) {
88
- logger.error('[binder] onboarding send failed:', err);
89
- }
90
- })();
91
- },
92
- });
93
- connector.setInboundMediaDetectedHandler((msg) => {
94
- const authorization = authorizer.authorize(msg.userId, msg.chatId);
95
- if (authorization === 'rejected') {
96
- logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
97
- return;
98
81
  }
99
- if (cloneSession.isActive()) {
100
- logger.info(`[media] dropped inbound media during clone session ${msg.messageId}`);
101
- return;
82
+ catch (err) {
83
+ ctx.logger.error('[binder] onboarding send failed:', err);
102
84
  }
103
- if (!router.config.inboundMedia.enabled) {
104
- assembler.ingest({
105
- kind: 'rejected',
106
- userId: msg.userId,
107
- chatId: msg.chatId,
108
- messageId: msg.messageId,
109
- replyToMessageId: msg.replyToMessageId,
110
- rawContentType: msg.rawContentType,
111
- rejectedKind: 'file',
112
- reason: '入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
113
- '如需自动保存图片/文件,请开启后重试',
114
- });
115
- return;
116
- }
117
- const outcome = (async () => {
118
- const payload = await connector.downloadInboundMedia(msg, {
119
- maxFileSizeMb: router.config.inboundMedia.maxFileSizeMb,
120
- });
121
- try {
122
- return await bridge.saveInboundMedia(payload);
123
- }
124
- catch (err) {
125
- for (const item of payload.media)
126
- bestEffortUnlink(item.tempPath);
127
- throw err;
128
- }
129
- })();
130
- assembler.ingest({
131
- kind: 'media',
85
+ })();
86
+ }
87
+ /** 入站媒体(image/file)检测:鉴权 → clone 拦截 → 落盘或 rejected 回执。 */
88
+ function handleInboundMedia(ctx, msg) {
89
+ const authorization = ctx.authorizer.authorize(msg.userId, msg.chatId);
90
+ if (authorization === 'rejected') {
91
+ ctx.logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
92
+ return;
93
+ }
94
+ if (ctx.cloneSession.isActive()) {
95
+ ctx.logger.info(`[media] dropped inbound media during clone session ${msg.messageId}`);
96
+ return;
97
+ }
98
+ if (!ctx.router.config.inboundMedia.enabled) {
99
+ ctx.assembler.ingest({
100
+ kind: 'rejected',
132
101
  userId: msg.userId,
133
102
  chatId: msg.chatId,
134
103
  messageId: msg.messageId,
135
104
  replyToMessageId: msg.replyToMessageId,
136
105
  rawContentType: msg.rawContentType,
137
- outcome,
106
+ rejectedKind: 'file',
107
+ reason: '入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
108
+ '如需自动保存图片/文件,请开启后重试',
138
109
  });
139
- });
140
- connector.setMessageHandler((msg) => {
141
- const decision = authorizer.authorize(msg.userId, msg.chatId);
142
- if (decision === 'rejected' || decision === 'bind_success')
143
- return;
144
- logger.info(`message from ${msg.userId} (${msg.rawContentType}): ${msg.content.slice(0, 100)}`);
145
- void connector.addReaction(msg.messageId, 'Typing');
146
- if (cloneSession.isActive() && !isStopCommand(msg.content)) {
147
- void cloneSession
148
- .handleMessage(msg.content, {
149
- userId: msg.userId,
150
- chatId: msg.chatId,
151
- messageId: msg.messageId,
152
- })
153
- .catch((err) => logger.error('[clone] message handling failed:', err));
154
- return;
155
- }
156
- const content = router.expandAliasMessage(msg.content);
157
- if (isStopCommand(content)) {
158
- void (async () => {
159
- const interrupt = await bridge.interruptCurrentRunDetailed({
160
- userId: msg.userId,
161
- chatId: msg.chatId,
162
- });
163
- const ctx = { userId: msg.userId, chatId: msg.chatId, messageId: msg.messageId };
164
- if (!interrupt.stopped) {
165
- await bridge.sendResult({ text: '当前没有运行中的进程' }, ctx);
166
- }
167
- else if ((interrupt.stoppedCount ?? 1) > 1) {
168
- await bridge.sendResult({ text: `✅ 已停止 ${interrupt.stoppedCount} 个运行中的任务` }, ctx);
169
- }
170
- })().catch((err) => logger.error('[control] /stop failed:', err));
171
- return;
172
- }
173
- const stripped = stripPlaceholders(content);
174
- if (stripped.unknownTags.length > 0) {
175
- logger.warn(`[inbound] unknown placeholder tags stripped: ${stripped.unknownTags.join(', ')} ` +
176
- `(msg_type=${msg.rawContentType})`);
110
+ return;
111
+ }
112
+ const outcome = (async () => {
113
+ const payload = await ctx.connector.downloadInboundMedia(msg, {
114
+ maxFileSizeMb: ctx.router.config.inboundMedia.maxFileSizeMb,
115
+ });
116
+ try {
117
+ return await ctx.bridge.saveInboundMedia(payload);
177
118
  }
178
- const commandEligible = msg.rawContentType === 'text' && stripped.kinds.length === 0 && stripped.clean !== '';
179
- if (commandEligible && (stripped.clean.startsWith('/') || stripped.clean.startsWith('!'))) {
180
- void (async () => {
181
- await assembler.flush(msg.userId, msg.chatId, 'flush');
182
- await router.handle(stripped.clean, {
183
- userId: msg.userId,
184
- chatId: msg.chatId,
185
- messageId: msg.messageId,
186
- }, { allowCommandPrefix: true });
187
- })().catch((err) => logger.error('[control] command failed:', err));
188
- return;
119
+ catch (err) {
120
+ for (const item of payload.media)
121
+ bestEffortUnlink(item.tempPath);
122
+ throw err;
189
123
  }
190
- assembler.ingest({
191
- kind: 'text',
124
+ })();
125
+ ctx.assembler.ingest({
126
+ kind: 'media',
127
+ userId: msg.userId,
128
+ chatId: msg.chatId,
129
+ messageId: msg.messageId,
130
+ replyToMessageId: msg.replyToMessageId,
131
+ rawContentType: msg.rawContentType,
132
+ outcome,
133
+ });
134
+ }
135
+ /** 普通入站消息:鉴权 → clone 拦截 → /stop → 命令前缀 → 装配器。 */
136
+ function handleInboundMessage(ctx, msg) {
137
+ const decision = ctx.authorizer.authorize(msg.userId, msg.chatId);
138
+ if (decision === 'rejected' || decision === 'bind_success')
139
+ return;
140
+ ctx.logger.info(`message from ${msg.userId} (${msg.rawContentType}): ${msg.content.slice(0, 100)}`);
141
+ void ctx.connector.addReaction(msg.messageId, 'Typing');
142
+ if (ctx.cloneSession.isActive() && !isStopCommand(msg.content)) {
143
+ void ctx.cloneSession
144
+ .handleMessage(msg.content, {
192
145
  userId: msg.userId,
193
146
  chatId: msg.chatId,
194
147
  messageId: msg.messageId,
195
- replyToMessageId: msg.replyToMessageId,
196
- rawContentType: msg.rawContentType,
197
- text: stripped.clean,
198
- placeholders: stripped.kinds,
199
- unknownTags: stripped.unknownTags,
200
- });
148
+ })
149
+ .catch((err) => ctx.logger.error('[clone] message handling failed:', err));
150
+ return;
151
+ }
152
+ const content = ctx.router.expandAliasMessage(msg.content);
153
+ if (isStopCommand(content)) {
154
+ void (async () => {
155
+ const interrupt = await ctx.bridge.interruptCurrentRunDetailed({
156
+ userId: msg.userId,
157
+ chatId: msg.chatId,
158
+ });
159
+ const stopCtx = { userId: msg.userId, chatId: msg.chatId, messageId: msg.messageId };
160
+ if (!interrupt.stopped) {
161
+ await ctx.bridge.sendResult({ text: '当前没有运行中的进程' }, stopCtx);
162
+ }
163
+ else if ((interrupt.stoppedCount ?? 1) > 1) {
164
+ await ctx.bridge.sendResult({ text: `✅ 已停止 ${interrupt.stoppedCount} 个运行中的任务` }, stopCtx);
165
+ }
166
+ })().catch((err) => ctx.logger.error('[control] /stop failed:', err));
167
+ return;
168
+ }
169
+ const stripped = stripPlaceholders(content);
170
+ if (stripped.unknownTags.length > 0) {
171
+ ctx.logger.warn(`[inbound] unknown placeholder tags stripped: ${stripped.unknownTags.join(', ')} ` +
172
+ `(msg_type=${msg.rawContentType})`);
173
+ }
174
+ const commandEligible = msg.rawContentType === 'text' && stripped.kinds.length === 0 && stripped.clean !== '';
175
+ if (commandEligible && (stripped.clean.startsWith('/') || stripped.clean.startsWith('!'))) {
176
+ void (async () => {
177
+ await ctx.assembler.flush(msg.userId, msg.chatId, 'flush');
178
+ await ctx.router.handle(stripped.clean, {
179
+ userId: msg.userId,
180
+ chatId: msg.chatId,
181
+ messageId: msg.messageId,
182
+ }, { allowCommandPrefix: true });
183
+ })().catch((err) => ctx.logger.error('[control] command failed:', err));
184
+ return;
185
+ }
186
+ ctx.assembler.ingest({
187
+ kind: 'text',
188
+ userId: msg.userId,
189
+ chatId: msg.chatId,
190
+ messageId: msg.messageId,
191
+ replyToMessageId: msg.replyToMessageId,
192
+ rawContentType: msg.rawContentType,
193
+ text: stripped.clean,
194
+ placeholders: stripped.kinds,
195
+ unknownTags: stripped.unknownTags,
201
196
  });
202
- connector.setCardActionHandler(async (action) => {
203
- if (!binder.isOwner(action.operator.openId)) {
204
- binder.recordRejectedCardAction(action.operator.openId);
205
- return;
206
- }
207
- const actionValue = action.action.value;
208
- if (!actionValue?.cmd)
209
- return;
210
- logger.info(actionValue.cmd === 'stop'
211
- ? `card action: stop runId=${actionValue.runId ?? '(none)'} messageId=${action.messageId}`
212
- : `card action: ${actionValue.cmd}`);
213
- const userId = action.operator.openId;
214
- const chatId = action.chatId;
215
- const messageId = action.messageId;
216
- void connector.addReaction(messageId, 'Typing');
217
- if (actionValue.cmd === 'stop') {
218
- void router
219
- .handleCardAction({ ...actionValue, cmd: 'stop' }, { userId, chatId, messageId })
220
- .catch((err) => logger.error('[control] card stop failed:', err));
221
- return;
222
- }
223
- const isImmediate = isImmediateAction(actionValue.cmd);
224
- const cwd = sessionStore.getCwd(userId) ?? '';
225
- const fullValue = buildCardActionFullValue(actionValue, action);
226
- if (SESSION_MUTATING_ACTION_CMDS.has(actionValue.cmd)) {
227
- const flushP = assembler
228
- .flush(userId, chatId, 'flush')
229
- .catch((err) => logger.error('[control] inbound flush failed:', err));
230
- await Promise.race([flushP, sleep(SESSION_MUTATING_FLUSH_BUDGET_MS)]);
231
- }
232
- if (DIRECT_RETURN_CMDS.has(actionValue.cmd)) {
233
- return router.handleCardAction(fullValue, { userId, chatId, messageId });
234
- }
235
- if (actionValue.cmd === 'order.exec') {
236
- void dispatchOrderExecForQueue({
237
- router,
238
- bridge,
239
- cwd,
240
- orderId: actionValue.orderId,
241
- ctx: { userId, chatId, messageId },
242
- }).catch((err) => logger.error('[control] order.exec dispatch failed:', err));
197
+ }
198
+ /** 卡片动作:鉴权 → stop/直返/order.exec → immediate 或普通队列。 */
199
+ async function handleCardAction(ctx, action) {
200
+ if (!ctx.binder.isOwner(action.operator.openId)) {
201
+ ctx.binder.recordRejectedCardAction(action.operator.openId);
202
+ return;
203
+ }
204
+ const actionValue = action.action.value;
205
+ if (!actionValue?.cmd)
206
+ return;
207
+ ctx.logger.info(actionValue.cmd === 'stop'
208
+ ? `card action: stop runId=${actionValue.runId ?? '(none)'} messageId=${action.messageId}`
209
+ : `card action: ${actionValue.cmd}`);
210
+ const userId = action.operator.openId;
211
+ const chatId = action.chatId;
212
+ const messageId = action.messageId;
213
+ void ctx.connector.addReaction(messageId, 'Typing');
214
+ if (actionValue.cmd === 'stop') {
215
+ void ctx.router
216
+ .handleCardAction({ ...actionValue, cmd: 'stop' }, { userId, chatId, messageId })
217
+ .catch((err) => ctx.logger.error('[control] card stop failed:', err));
218
+ return;
219
+ }
220
+ const isImmediate = isImmediateAction(actionValue.cmd);
221
+ const cwd = ctx.sessionStore.getCwd(userId) ?? '';
222
+ const fullValue = buildCardActionFullValue(actionValue, action);
223
+ if (SESSION_MUTATING_ACTION_CMDS.has(actionValue.cmd)) {
224
+ const flushP = ctx.assembler
225
+ .flush(userId, chatId, 'flush')
226
+ .catch((err) => ctx.logger.error('[control] inbound flush failed:', err));
227
+ await Promise.race([flushP, sleep(SESSION_MUTATING_FLUSH_BUDGET_MS)]);
228
+ }
229
+ if (DIRECT_RETURN_CMDS.has(actionValue.cmd)) {
230
+ return ctx.router.handleCardAction(fullValue, { userId, chatId, messageId });
231
+ }
232
+ if (actionValue.cmd === 'order.exec') {
233
+ void dispatchOrderExecForQueue({
234
+ router: ctx.router,
235
+ bridge: ctx.bridge,
236
+ cwd,
237
+ orderId: actionValue.orderId,
238
+ ctx: { userId, chatId, messageId },
239
+ }).catch((err) => ctx.logger.error('[control] order.exec dispatch failed:', err));
240
+ return;
241
+ }
242
+ const forwardActionFeedback = (res) => {
243
+ const text = actionFeedbackText(res);
244
+ if (!text)
243
245
  return;
244
- }
245
- const forwardActionFeedback = (res) => {
246
- const text = actionFeedbackText(res);
247
- if (!text)
248
- return;
249
- void bridge
250
- .sendResult({ text }, { userId, chatId, messageId })
251
- .catch((err) => logger.error('[control] card action feedback failed:', err));
252
- };
253
- if (isImmediate) {
254
- bridge.enqueueImmediate(cwd, async () => {
255
- const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
256
- forwardActionFeedback(res);
257
- });
258
- }
259
- else {
260
- bridge.enqueue(cwd, async () => {
261
- const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
262
- forwardActionFeedback(res);
263
- }, {
264
- taskMeta: {
265
- userId,
266
- chatId,
267
- messageId: router.mintQueueTaskKey('card', actionValue.cmd),
268
- feishuReplyTo: messageId,
269
- messagePreview: `card action: ${actionValue.cmd}`,
270
- kind: COMPACT_ACTION_CMDS.has(actionValue.cmd) ? 'compact' : 'command',
271
- editable: !COMPACT_ACTION_CMDS.has(actionValue.cmd),
272
- },
273
- });
274
- }
246
+ void ctx.bridge
247
+ .sendResult({ text }, { userId, chatId, messageId })
248
+ .catch((err) => ctx.logger.error('[control] card action feedback failed:', err));
249
+ };
250
+ if (isImmediate) {
251
+ ctx.bridge.enqueueImmediate(cwd, async () => {
252
+ const res = await ctx.router.handleCardAction(fullValue, { userId, chatId, messageId });
253
+ forwardActionFeedback(res);
254
+ });
255
+ }
256
+ else {
257
+ ctx.bridge.enqueue(cwd, async () => {
258
+ const res = await ctx.router.handleCardAction(fullValue, { userId, chatId, messageId });
259
+ forwardActionFeedback(res);
260
+ }, {
261
+ taskMeta: {
262
+ userId,
263
+ chatId,
264
+ messageId: ctx.router.mintQueueTaskKey('card', actionValue.cmd),
265
+ feishuReplyTo: messageId,
266
+ messagePreview: `card action: ${actionValue.cmd}`,
267
+ kind: COMPACT_ACTION_CMDS.has(actionValue.cmd) ? 'compact' : 'command',
268
+ editable: !COMPACT_ACTION_CMDS.has(actionValue.cmd),
269
+ },
270
+ });
271
+ }
272
+ }
273
+ export function setupMessageHandlers(deps) {
274
+ // ctx 在 assembler/authorizer 创建后填充;转发箭头只在事件触发时读它。
275
+ // 所有决策逻辑都在模块级 handler 中,本函数只做注册,圈复杂度保持最低。
276
+ // 循环初始化:两个实例的回调捕获 ctx,而 ctx 又需要这两个实例,无法在
277
+ // 声明处一次性初始化;回调只在事件到达时执行,那时填充早已完成。
278
+ // eslint-disable-next-line prefer-const
279
+ let ctx;
280
+ const assembler = new InboundTurnAssembler({
281
+ onCommit: (turn, prompt) => handleTurnCommit(ctx, turn, prompt),
282
+ onReceipt: (receiptCtx, text) => handleTurnReceipt(ctx, receiptCtx, text),
283
+ });
284
+ deps.router.setInboundFlusher(() => assembler.flushAll('flush'));
285
+ const authorizer = new InboundAuthorizer({
286
+ binder: deps.binder,
287
+ onBind: (userId, chatId) => handleBind(ctx, userId, chatId),
275
288
  });
289
+ ctx = { ...deps, assembler, authorizer };
290
+ deps.connector.setInboundMediaDetectedHandler((msg) => handleInboundMedia(ctx, msg));
291
+ deps.connector.setMessageHandler((msg) => handleInboundMessage(ctx, msg));
292
+ deps.connector.setCardActionHandler((action) => handleCardAction(ctx, action));
276
293
  }
277
294
  //# sourceMappingURL=message-handlers.js.map
@@ -6,9 +6,9 @@
6
6
  * 「地基层被 runner 反向 import」;挪到这里后三方共同引用同一份。
7
7
  *
8
8
  * 表内容来自 Claude CLI 的模型别名(alias 是 CLI 侧的稳定名字,model ID 会随
9
- * 版本变),新增一行只改这里。
9
+ * 版本变),新增一行只改 MODEL_ID_TO_ALIAS,反向表自动派生。
10
10
  */
11
11
  /** 模型 ID → alias 映射。 */
12
12
  export declare const MODEL_ID_TO_ALIAS: Record<string, string>;
13
- /** alias → model ID 反向映射。 */
13
+ /** alias → model ID 反向映射(从正向表派生,新增别名只改上面一张表)。 */
14
14
  export declare const MODEL_ALIAS_TO_ID: Record<string, string>;
@@ -6,7 +6,7 @@
6
6
  * 「地基层被 runner 反向 import」;挪到这里后三方共同引用同一份。
7
7
  *
8
8
  * 表内容来自 Claude CLI 的模型别名(alias 是 CLI 侧的稳定名字,model ID 会随
9
- * 版本变),新增一行只改这里。
9
+ * 版本变),新增一行只改 MODEL_ID_TO_ALIAS,反向表自动派生。
10
10
  */
11
11
  /** 模型 ID → alias 映射。 */
12
12
  export const MODEL_ID_TO_ALIAS = {
@@ -15,11 +15,6 @@ export const MODEL_ID_TO_ALIAS = {
15
15
  'claude-haiku-4-5-20250501': 'haiku',
16
16
  'claude-fable-5': 'fable',
17
17
  };
18
- /** alias → model ID 反向映射。 */
19
- export const MODEL_ALIAS_TO_ID = {
20
- opus: 'claude-opus-4-8',
21
- sonnet: 'claude-sonnet-4-6',
22
- haiku: 'claude-haiku-4-5-20250501',
23
- fable: 'claude-fable-5',
24
- };
18
+ /** alias → model ID 反向映射(从正向表派生,新增别名只改上面一张表)。 */
19
+ export const MODEL_ALIAS_TO_ID = Object.fromEntries(Object.entries(MODEL_ID_TO_ALIAS).map(([id, alias]) => [alias, id]));
25
20
  //# sourceMappingURL=model-catalog.js.map
@@ -154,7 +154,7 @@ export function binaryName(token, platform = currentPlatform) {
154
154
  return caseInsensitive(platform) ? name.toLowerCase() : name;
155
155
  }
156
156
  /**
157
- * 命令行与期望二进制是否属于同一程序(review 2026-09-05 收紧 + 2026-09-20 分档)。
157
+ * 命令行与期望二进制是否属于同一程序。
158
158
  *
159
159
  * 1. 可执行位匹配:首 token 的 basename 相等——`/usr/local/bin/claude --verbose`
160
160
  * 匹配 claude;native 单文件 CLI(claude 的 `bin/claude.exe`)也走这条;
@@ -21,7 +21,7 @@ export declare function samePath(a: string, b: string, opts?: PathOptions): bool
21
21
  * cwd → 会话项目目录名(Claude/pi 的 on-disk 编码,lossy N-to-N)。
22
22
  *
23
23
  * 只可用于**定位**目录,绝不可反解回 cwd——cwd 必须从 JSONL 内容读取
24
- * (回归 2026-06-21 /resume & /cd 路径错乱)。
24
+ * (否则 /resume & /cd 路径会错乱)。
25
25
  *
26
26
  * posix:`/` → `-`(`_` 同样归一,与 Claude Code 一致)。
27
27
  * win32:`\` 与 `/` **都是**分隔符、`:` 是非法文件名字符,三者都必须归一,
@@ -63,7 +63,7 @@ export function samePath(a, b, opts) {
63
63
  * cwd → 会话项目目录名(Claude/pi 的 on-disk 编码,lossy N-to-N)。
64
64
  *
65
65
  * 只可用于**定位**目录,绝不可反解回 cwd——cwd 必须从 JSONL 内容读取
66
- * (回归 2026-06-21 /resume & /cd 路径错乱)。
66
+ * (否则 /resume & /cd 路径会错乱)。
67
67
  *
68
68
  * posix:`/` → `-`(`_` 同样归一,与 Claude Code 一致)。
69
69
  * win32:`\` 与 `/` **都是**分隔符、`:` 是非法文件名字符,三者都必须归一,
@@ -6,7 +6,7 @@ import { getLogger } from '../logger/index.js';
6
6
  * **必须按无符号 32 位取值**:`0x80000001` = 2147483649 超出 Int32 正数上界
7
7
  * (2147483647),而 PowerShell 的数字字面量按 Int32 解析 —— 十六进制写法一旦
8
8
  * 进了脚本就会被解析成 **-2147483647**,传给 P/Invoke 声明的 `uint` 形参时抛
9
- * `MethodArgumentConversionInvalidCastArgument`(2026-09-22 真机复现)。
9
+ * `MethodArgumentConversionInvalidCastArgument`。
10
10
  */
11
11
  const ES_FLAGS_UINT32 = (0x80000000 | 0x00000001) >>> 0;
12
12
  /**
@@ -42,8 +42,9 @@ export function createWin32Terminator(deps) {
42
42
  stdio: 'ignore',
43
43
  windowsHide: true,
44
44
  });
45
- killer?.on?.('error', () => {
45
+ killer?.on?.('error', (err) => {
46
46
  // taskkill 拉起失败不致命:调用方仍可走 killOrphan / 陈旧锁自愈
47
+ log?.(`win32 树杀失败:taskkill error 事件 ${err?.message ?? err}`);
47
48
  });
48
49
  }
49
50
  catch (error) {