lark-remote 0.1.6 → 0.1.8

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 (216) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +71 -34
  5. package/dist/bridge/approval-coordinator.js +225 -41
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +97 -13
  9. package/dist/bridge/index.js +227 -87
  10. package/dist/bridge/queue-manager.d.ts +2 -1
  11. package/dist/bridge/queue-manager.js +1 -0
  12. package/dist/card/approval-render.js +294 -36
  13. package/dist/card/bash-card-session.d.ts +2 -24
  14. package/dist/card/bash-card-session.js +6 -73
  15. package/dist/card/bash-renderer.js +2 -3
  16. package/dist/card/card-budget.js +29 -75
  17. package/dist/card/card-session.d.ts +48 -0
  18. package/dist/card/card-session.js +86 -0
  19. package/dist/card/card-shared.d.ts +7 -16
  20. package/dist/card/card-shared.js +7 -30
  21. package/dist/card/run-card-session.d.ts +2 -55
  22. package/dist/card/run-card-session.js +6 -104
  23. package/dist/card/run-renderer.d.ts +7 -0
  24. package/dist/card/run-renderer.js +116 -238
  25. package/dist/card/run-state.d.ts +13 -0
  26. package/dist/card/run-state.js +25 -16
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/index.d.ts +112 -24
  40. package/dist/config/index.js +139 -37
  41. package/dist/connector/index.d.ts +69 -2
  42. package/dist/connector/index.js +107 -1
  43. package/dist/index.js +150 -118
  44. package/dist/instance-lock.js +4 -12
  45. package/dist/order/alias-resolve.d.ts +13 -0
  46. package/dist/order/alias-resolve.js +22 -0
  47. package/dist/order/alias-store.d.ts +38 -0
  48. package/dist/order/alias-store.js +106 -0
  49. package/dist/persistence/atomic-write.d.ts +8 -2
  50. package/dist/persistence/atomic-write.js +23 -7
  51. package/dist/restart.js +2 -1
  52. package/dist/router/card-action-payload.d.ts +30 -0
  53. package/dist/router/card-action-payload.js +39 -0
  54. package/dist/router/card-helpers.d.ts +63 -0
  55. package/dist/router/card-helpers.js +146 -0
  56. package/dist/router/config/claude.js +30 -1
  57. package/dist/router/config/codex.js +29 -50
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +5 -4
  61. package/dist/router/config/dsh.d.ts +51 -0
  62. package/dist/router/config/dsh.js +179 -0
  63. package/dist/router/config/index.d.ts +12 -0
  64. package/dist/router/config/index.js +28 -1
  65. package/dist/router/config/kimi.d.ts +4 -1
  66. package/dist/router/config/kimi.js +27 -0
  67. package/dist/router/config/opencode.js +18 -5
  68. package/dist/router/config/pi.js +4 -5
  69. package/dist/router/config/types.d.ts +17 -2
  70. package/dist/router/index.d.ts +76 -22
  71. package/dist/router/index.js +479 -548
  72. package/dist/router/utils.d.ts +2 -0
  73. package/dist/router/utils.js +16 -2
  74. package/dist/runner/agent-choices-common.d.ts +16 -0
  75. package/dist/runner/agent-choices-common.js +34 -0
  76. package/dist/runner/claude/runner.d.ts +47 -10
  77. package/dist/runner/claude/runner.js +117 -46
  78. package/dist/runner/claude/session.d.ts +166 -0
  79. package/dist/runner/claude/session.js +768 -0
  80. package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
  81. package/dist/runner/codex/app-server/protocol-types.js +2 -13
  82. package/dist/runner/codex/app-server/runner.d.ts +52 -59
  83. package/dist/runner/codex/app-server/runner.js +216 -338
  84. package/dist/runner/codex/app-server/translator.d.ts +8 -21
  85. package/dist/runner/codex/app-server/translator.js +45 -18
  86. package/dist/runner/codex/index.d.ts +0 -2
  87. package/dist/runner/codex/index.js +0 -2
  88. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  89. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  90. package/dist/runner/common/acp/protocol-types.d.ts +271 -0
  91. package/dist/runner/common/acp/protocol-types.js +77 -0
  92. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  93. package/dist/runner/common/connection-based-runner.js +261 -0
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  95. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  96. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  97. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  98. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  99. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  100. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  101. package/dist/runner/common/jsonrpc/types.js +9 -0
  102. package/dist/runner/common/process-stopper.d.ts +2 -1
  103. package/dist/runner/common/process-stopper.js +1 -0
  104. package/dist/runner/common/runner-utils.d.ts +6 -13
  105. package/dist/runner/common/runner-utils.js +6 -47
  106. package/dist/runner/common/spawning-runner.d.ts +24 -70
  107. package/dist/runner/common/spawning-runner.js +98 -152
  108. package/dist/runner/dsh/client.d.ts +95 -0
  109. package/dist/runner/dsh/client.js +418 -0
  110. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  111. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  112. package/dist/runner/dsh/index.d.ts +3 -0
  113. package/dist/runner/dsh/index.js +4 -0
  114. package/dist/runner/dsh/runner.d.ts +57 -0
  115. package/dist/runner/dsh/runner.js +191 -0
  116. package/dist/runner/dsh/translator.d.ts +28 -0
  117. package/dist/runner/dsh/translator.js +98 -0
  118. package/dist/runner/dsh/types.d.ts +155 -0
  119. package/dist/runner/dsh/types.js +14 -0
  120. package/dist/runner/index.d.ts +5 -4
  121. package/dist/runner/index.js +5 -4
  122. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  123. package/dist/runner/kimi/acp/runner.js +516 -0
  124. package/dist/runner/kimi/acp/translator.d.ts +138 -0
  125. package/dist/runner/kimi/acp/translator.js +441 -0
  126. package/dist/runner/kimi/index.d.ts +1 -1
  127. package/dist/runner/kimi/index.js +1 -1
  128. package/dist/runner/opencode/acp/index.d.ts +1 -0
  129. package/dist/runner/opencode/acp/index.js +2 -0
  130. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  131. package/dist/runner/opencode/acp/runner.js +429 -0
  132. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  133. package/dist/runner/opencode/acp/translator.js +345 -0
  134. package/dist/runner/opencode/index.d.ts +1 -1
  135. package/dist/runner/opencode/index.js +1 -1
  136. package/dist/runner/pi/index.d.ts +1 -1
  137. package/dist/runner/pi/index.js +1 -1
  138. package/dist/runner/pi/rpc/client.d.ts +52 -0
  139. package/dist/runner/pi/rpc/client.js +114 -0
  140. package/dist/runner/pi/rpc/index.d.ts +4 -0
  141. package/dist/runner/pi/rpc/index.js +4 -0
  142. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  143. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  144. package/dist/runner/pi/rpc/runner.d.ts +74 -0
  145. package/dist/runner/pi/rpc/runner.js +190 -0
  146. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  147. package/dist/runner/pi/rpc/translator.js +258 -0
  148. package/dist/runner/probe.js +6 -1
  149. package/dist/runner/question-common.d.ts +26 -0
  150. package/dist/runner/question-common.js +42 -0
  151. package/dist/runner/registry.d.ts +7 -8
  152. package/dist/runner/registry.js +3 -4
  153. package/dist/runner/resolve-agent-choices.js +18 -59
  154. package/dist/runner/sync-agent-choices.js +16 -67
  155. package/dist/runner/types.d.ts +97 -14
  156. package/dist/session/claude/session-index.js +2 -3
  157. package/dist/session/claude/session-reader.js +3 -6
  158. package/dist/session/claude/sessions.js +43 -28
  159. package/dist/session/codex/rollout-reader.d.ts +2 -2
  160. package/dist/session/codex/rollout-reader.js +14 -17
  161. package/dist/session/codex/sessions.d.ts +1 -1
  162. package/dist/session/codex/sessions.js +2 -2
  163. package/dist/session/common/content-blocks.d.ts +7 -0
  164. package/dist/session/common/content-blocks.js +13 -9
  165. package/dist/session/common/jsonl.d.ts +8 -25
  166. package/dist/session/common/jsonl.js +126 -261
  167. package/dist/session/common/pagination.d.ts +28 -0
  168. package/dist/session/common/pagination.js +33 -0
  169. package/dist/session/dsh/index.d.ts +1 -0
  170. package/dist/session/dsh/index.js +2 -0
  171. package/dist/session/dsh/sessions.d.ts +39 -0
  172. package/dist/session/dsh/sessions.js +237 -0
  173. package/dist/session/index.d.ts +1 -0
  174. package/dist/session/index.js +1 -0
  175. package/dist/session/kimi/sessions.d.ts +34 -1
  176. package/dist/session/kimi/sessions.js +208 -98
  177. package/dist/session/opencode/sessions.d.ts +17 -3
  178. package/dist/session/opencode/sessions.js +29 -29
  179. package/dist/session/pi/sessions.d.ts +5 -1
  180. package/dist/session/pi/sessions.js +12 -16
  181. package/dist/session/session-store.d.ts +11 -1
  182. package/dist/session/session-store.js +75 -178
  183. package/dist/update/index.d.ts +2 -2
  184. package/dist/update/index.js +2 -2
  185. package/dist/update/install.d.ts +20 -2
  186. package/dist/update/install.js +51 -7
  187. package/dist/workspace/index.d.ts +4 -0
  188. package/dist/workspace/index.js +8 -1
  189. package/package.json +2 -1
  190. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  191. package/dist/runner/codex/argv.d.ts +0 -33
  192. package/dist/runner/codex/argv.js +0 -40
  193. package/dist/runner/codex/factory.d.ts +0 -29
  194. package/dist/runner/codex/factory.js +0 -39
  195. package/dist/runner/codex/jsonl.d.ts +0 -37
  196. package/dist/runner/codex/jsonl.js +0 -221
  197. package/dist/runner/codex/runner.d.ts +0 -66
  198. package/dist/runner/codex/runner.js +0 -129
  199. package/dist/runner/common/exec-translator.d.ts +0 -73
  200. package/dist/runner/common/exec-translator.js +0 -105
  201. package/dist/runner/kimi/jsonl.d.ts +0 -28
  202. package/dist/runner/kimi/jsonl.js +0 -143
  203. package/dist/runner/kimi/runner.d.ts +0 -49
  204. package/dist/runner/kimi/runner.js +0 -105
  205. package/dist/runner/opencode/argv.d.ts +0 -32
  206. package/dist/runner/opencode/argv.js +0 -35
  207. package/dist/runner/opencode/jsonl.d.ts +0 -68
  208. package/dist/runner/opencode/jsonl.js +0 -295
  209. package/dist/runner/opencode/runner.d.ts +0 -61
  210. package/dist/runner/opencode/runner.js +0 -93
  211. package/dist/runner/pi/jsonl.d.ts +0 -128
  212. package/dist/runner/pi/jsonl.js +0 -249
  213. package/dist/runner/pi/runner.d.ts +0 -36
  214. package/dist/runner/pi/runner.js +0 -76
  215. package/dist/session/common/catch-up.d.ts +0 -12
  216. package/dist/session/common/catch-up.js +0 -31
@@ -1,9 +1,15 @@
1
1
  import { createLarkChannel, } from '@larksuite/channel';
2
2
  import { getLogger } from '../logger/index.js';
3
3
  import { MAX_FILE_UPLOAD_SIZE } from './file-limits.js';
4
+ import { DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB } from '../config/index.js';
5
+ import { sleep } from '../common/sleep.js';
4
6
  import axios from 'axios';
5
7
  import fs from 'node:fs';
8
+ import { silentlyUnlink } from '../common/fs.js';
6
9
  import https from 'node:https';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { randomUUID } from 'node:crypto';
7
13
  import FormData from 'form-data';
8
14
  /**
9
15
  * Dedicated https.Agent with keepAlive disabled for sendFile's three axios
@@ -36,6 +42,20 @@ const noKeepAliveAgent = new https.Agent({ keepAlive: false });
36
42
  * 详见 design.md §9.8。原地更新逻辑新增按钮时参考该节去重风险分类。
37
43
  */
38
44
  export const DEDUP_TTL_MS = 300;
45
+ /** 单资源下载超时(毫秒):SDK 无内置超时,挂起会导致提示丢失 + 残留临时文件。 */
46
+ const RESOURCE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
47
+ function withTimeout(promise, ms, label) {
48
+ return new Promise((resolve, reject) => {
49
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
50
+ promise.then((value) => {
51
+ clearTimeout(timer);
52
+ resolve(value);
53
+ }, (err) => {
54
+ clearTimeout(timer);
55
+ reject(err);
56
+ });
57
+ });
58
+ }
39
59
  /**
40
60
  * 判断 send 失败是否值得重试一次(§9.5 限流重试口径,§P1-3 修正)。
41
61
  *
@@ -75,9 +95,11 @@ function tryGetPatchService(channel) {
75
95
  return c?.im?.v1?.message;
76
96
  }
77
97
  export class FeishuConnector {
98
+ /** 飞书 channel 实例(public:测试直接访问/注入 mock,替代 as unknown as)。 */
78
99
  channel;
79
100
  onMessage;
80
101
  onCardAction;
102
+ onInboundMediaDetected;
81
103
  isConnected = false;
82
104
  appId;
83
105
  appSecret;
@@ -125,6 +147,15 @@ export class FeishuConnector {
125
147
  this.channel.on('message', (msg) => {
126
148
  if (msg.chatType !== 'p2p')
127
149
  return;
150
+ // 入站图片/文件:不转发文本内容(图片消息的 content 只是占位文案),
151
+ // 而是下载资源后交给 bridge 落盘(runner 零改动)。
152
+ if ((msg.rawContentType === 'image' || msg.rawContentType === 'file') &&
153
+ msg.resources.length > 0) {
154
+ // 只上报"媒体到达",不在此下载:owner/配置闸门在 index.ts 里先执行,
155
+ // 通过后才调用 downloadInboundMedia(避免未认证大文件下载打爆内存)。
156
+ void this.handleInboundMediaDetected(msg);
157
+ return;
158
+ }
128
159
  this.onMessage?.({
129
160
  userId: msg.senderId,
130
161
  messageId: msg.messageId,
@@ -158,6 +189,81 @@ export class FeishuConnector {
158
189
  setCardActionHandler(handler) {
159
190
  this.onCardAction = handler;
160
191
  }
192
+ setInboundMediaDetectedHandler(handler) {
193
+ this.onInboundMediaDetected = handler;
194
+ }
195
+ /**
196
+ * 上报媒体消息到达(轻量,不含下载内容)。上层认证/配置闸门通过后
197
+ * 再调 downloadInboundMedia。
198
+ */
199
+ async handleInboundMediaDetected(msg) {
200
+ if (!this.onInboundMediaDetected) {
201
+ getLogger().warn('[feishu] inbound media received but no detected handler registered');
202
+ return;
203
+ }
204
+ try {
205
+ await this.onInboundMediaDetected({
206
+ userId: msg.senderId,
207
+ chatId: msg.chatId,
208
+ messageId: msg.messageId,
209
+ replyToMessageId: msg.replyToMessageId,
210
+ resources: msg.resources.map((r) => ({
211
+ type: r.type === 'image' ? 'image' : 'file',
212
+ fileKey: r.fileKey,
213
+ fileName: r.fileName,
214
+ })),
215
+ });
216
+ }
217
+ catch (err) {
218
+ getLogger().error('[feishu] inbound media detected handler failed:', err.message);
219
+ }
220
+ }
221
+ /**
222
+ * 下载消息里的全部资源(流式写临时文件,避免大文件全量进堆内存)。
223
+ * 逐资源 try/catch:单个失败不影响其他资源;超限项不进入 media(不落盘)。
224
+ * 下载失败/超限都随 failures 交给 bridge 提示用户(对齐"发送文件失败"文案风格)。
225
+ * 调用方必须已通过 owner/配置闸门(index.ts);maxFileSizeMb 由调用方从
226
+ * 当前配置传入(避免 connector 持有过期的 config 快照)。
227
+ */
228
+ async downloadInboundMedia(msg, opts) {
229
+ const maxBytes = (opts?.maxFileSizeMb ?? DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB) * 1024 * 1024;
230
+ const timeoutMs = opts?.downloadTimeoutMs ?? RESOURCE_DOWNLOAD_TIMEOUT_MS;
231
+ const media = [];
232
+ const failures = [];
233
+ for (const res of msg.resources) {
234
+ const tmpPath = path.join(os.tmpdir(), `lark-remote-inbound-${randomUUID()}`);
235
+ try {
236
+ const { contentType, bytesWritten } = await withTimeout(this.channel.downloadResourceToFile(msg.messageId, res.fileKey, res.type, tmpPath), timeoutMs, `downloadResource fileKey=${res.fileKey}`);
237
+ if (bytesWritten > maxBytes) {
238
+ silentlyUnlink(tmpPath);
239
+ failures.push({
240
+ fileName: res.fileName,
241
+ reason: `超过 ${maxBytes / (1024 * 1024)}MB 大小限制`,
242
+ });
243
+ continue;
244
+ }
245
+ media.push({
246
+ type: res.type,
247
+ fileName: res.fileName,
248
+ mimeType: contentType,
249
+ tempPath: tmpPath,
250
+ });
251
+ }
252
+ catch (err) {
253
+ silentlyUnlink(tmpPath);
254
+ getLogger().warn(`[feishu] downloadResource failed fileKey=${res.fileKey} type=${res.type}:`, err.message);
255
+ failures.push({ fileName: res.fileName, reason: `下载失败: ${err.message}` });
256
+ }
257
+ }
258
+ return {
259
+ userId: msg.userId,
260
+ chatId: msg.chatId,
261
+ messageId: msg.messageId,
262
+ replyToMessageId: msg.replyToMessageId,
263
+ media,
264
+ failures,
265
+ };
266
+ }
161
267
  async connect() {
162
268
  try {
163
269
  await this.channel.connect();
@@ -200,7 +306,7 @@ export class FeishuConnector {
200
306
  catch (err) {
201
307
  if (shouldRetrySendError(err)) {
202
308
  getLogger().warn('[feishu] rate limited, retrying in 200ms...');
203
- await new Promise((r) => setTimeout(r, 200));
309
+ await sleep(200);
204
310
  try {
205
311
  const result = await this.channel.send(chatId, input, { replyTo: opts?.replyTo });
206
312
  return result.messageId;
package/dist/index.js CHANGED
@@ -1,15 +1,16 @@
1
- import { loadConfig, getAgentConfig, DEFAULT_STOP_GRACE_MS, } from './config/index.js';
1
+ import { loadConfig, getAgentConfig } from './config/index.js';
2
2
  import { resolveAgentChoices, } from './runner/index.js';
3
3
  import { ensureConfig } from './config/wizard.js';
4
4
  import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion, } from './config/dir.js';
5
5
  import { FeishuConnector } from './connector/index.js';
6
- import { ClaudeRunner, createCodexRunner, OpencodeExecRunner, PiRunner, KimiRunner, } from './runner/index.js';
6
+ import { ClaudeRunner, CodexAppServerRunner, OpencodeAcpRunner, PiRpcRunner, KimiAcpRunner, DshRunner, } from './runner/index.js';
7
7
  import { AgentRegistry } from './runner/registry.js';
8
8
  import { probeAllAgents } from './runner/probe.js';
9
9
  import { SessionReaderRegistry, SessionStore } from './session/index.js';
10
- import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, } from './session/index.js';
11
- import { CommandRouter, isImmediateAction, formatUsageStats, } from './router/index.js';
10
+ import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
11
+ import { CommandRouter, isImmediateAction } from './router/index.js';
12
12
  import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
13
+ import { buildCardActionFullValue } from './router/card-action-payload.js';
13
14
  import { Bridge } from './bridge/index.js';
14
15
  import { initLogger, getLogger } from './logger/index.js';
15
16
  import { StartupContactStore, sendStartupHello } from './startup-contact.js';
@@ -19,11 +20,11 @@ import { spawnReplacementBridge, waitForPreviousInstance } from './restart.js';
19
20
  import { checkLatestVersion, isNewer, runInstallLatest, formatUpdateHint } from './update/index.js';
20
21
  import { classifyRejection } from './error-classification.js';
21
22
  import { WorkspaceStore } from './workspace/index.js';
22
- import { markdownDiv } from './card/collapsible.js';
23
- import { sessionEventPanel } from './router/card-helpers.js';
23
+ import { buildSessionHistoryCard } from './router/card-helpers.js';
24
24
  import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
25
25
  import path from 'node:path';
26
26
  import fs from 'node:fs';
27
+ import { silentlyUnlink } from './common/fs.js';
27
28
  /** Send auto-resume card for a restored session */
28
29
  async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false) {
29
30
  if (!contact.userId)
@@ -31,68 +32,32 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state, compa
31
32
  const recipient = contact.chatId ?? contact.userId;
32
33
  if (!recipient)
33
34
  return;
34
- // Build header with displayTitle (aiTitle or last user message) and recap
35
- let header = `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`;
36
- const sections = [];
37
- if (state.displayTitle) {
38
- const label = state.aiTitle ? 'AI 标题' : '最近输入';
39
- sections.push(`🏷️ **${label}**\n${state.displayTitle}`);
40
- }
41
- if (state.recap) {
42
- const recapPreview = state.recap.length > 200 ? state.recap.slice(0, 197) + '...' : state.recap;
43
- sections.push(`📝 **Recap**\n${recapPreview}`);
44
- }
45
- if (sections.length > 0) {
46
- header += '\n\n' + sections.join('\n\n──\n\n');
47
- }
48
- const elements = [markdownDiv(header), { tag: 'hr' }];
49
35
  // Limit history events to prevent card size exceeded error (11310)
50
36
  const MAX_HISTORY_EVENTS = 5;
51
37
  const eventsToShow = state.events.slice(-MAX_HISTORY_EVENTS);
52
38
  const hiddenCount = state.events.length - MAX_HISTORY_EVENTS;
53
- // Add indicator if events were hidden
54
- if (hiddenCount > 0) {
55
- elements.push(markdownDiv(`📜 还有 ${hiddenCount} 个更早的事件未显示`));
56
- }
57
- // Fold session history into collapsible panels: the last 2 events stay expanded
39
+ // Action buttons: Compact (runCompact-capable runner) + new session.
58
40
  const agentKind = state.agentKind ?? 'claude';
59
- eventsToShow.forEach((ev, i) => {
60
- elements.push(sessionEventPanel(ev, i, eventsToShow.length, 2, agentKind));
61
- });
62
- // Add usage/stats at the end
63
- if (state.usage) {
64
- const usageStr = formatUsageStats(state.usage);
65
- elements.push(markdownDiv(usageStr));
66
- }
67
- // Add action buttons - use column_set+column (div+elements not supported in 2.0)
68
41
  const actionButtons = [];
69
- // Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且未在跑。
70
- // 启动自动恢复时不会存在该会话的 activeRun,直接渲染。
71
42
  if (compactSupported) {
72
43
  actionButtons.push(resumeCompactButton(session.sessionId, state.agentKind ?? 'codex'));
73
44
  }
74
45
  actionButtons.push(newSessionButton());
75
- elements.push({
76
- tag: 'column_set',
77
- columns: actionButtons.map((btn) => ({
78
- tag: 'column',
79
- width: 'auto',
80
- elements: [btn],
81
- })),
46
+ const card = buildSessionHistoryCard({
47
+ sessionId: session.sessionId,
48
+ cwd,
49
+ displayTitle: state.displayTitle,
50
+ aiTitle: state.aiTitle,
51
+ recap: state.recap,
52
+ events: eventsToShow,
53
+ usage: state.usage,
54
+ }, {
55
+ agentKind,
56
+ headerText: `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`,
57
+ title: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
58
+ hiddenCount,
59
+ actions: actionButtons,
82
60
  });
83
- const card = {
84
- schema: '2.0',
85
- config: {
86
- wide_screen_mode: true,
87
- },
88
- header: {
89
- title: {
90
- tag: 'plain_text',
91
- content: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
92
- },
93
- },
94
- body: { elements },
95
- };
96
61
  await connector.sendWithRetry(recipient, { card });
97
62
  }
98
63
  /** Initialize CLI args, config, and logger. Returns { config, configDir, logger, instanceLock }. */
@@ -165,37 +130,36 @@ function initializeRunner(config, configDir, cliArgs) {
165
130
  settings: cliArgs.settings,
166
131
  pidDir: configDir,
167
132
  workspace: ws,
133
+ permissionMode: claudeConfig.permissionMode,
134
+ idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
168
135
  });
169
136
  });
170
- // Register Codex runner. Factory reads latest config from container.current
171
- // so runtime config changes (serviceMode, model, sandbox, approvalPolicy,
172
- // reasoningEffort, etc.) take effect after bridge.setConfig() + clearRunners().
173
- // serviceMode=app-server → CodexAppServerRunner (persistent connection with
174
- // approval + sandbox); otherwise CodexExecRunner (codex exec --json).
137
+ // Register Codex runner (app-server only). The registry factory reads latest
138
+ // config from container.current so runtime config changes (model, sandbox,
139
+ // approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
140
+ // + clearRunners().
175
141
  const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
176
- agentRegistry.register('codex', (ws) => {
142
+ agentRegistry.register('codex', (_ws) => {
177
143
  const container = agentRegistry.getConfigContainer();
178
144
  const latestConfig = container?.current ?? config;
179
145
  const codexConfig = getAgentConfig(latestConfig, 'codex');
180
- return createCodexRunner({
146
+ return new CodexAppServerRunner({
147
+ kind: 'codex',
181
148
  model: codexConfig?.model,
182
149
  modelProvider: codexConfig?.modelProvider,
183
150
  reasoningEffort: codexConfig?.reasoningEffort,
184
- stopGraceMs: codexConfig?.stopGraceMs ?? latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
185
- pidDir: configDir,
186
- workspace: ws,
187
151
  sessionReader: codexSessionReader,
188
- serviceMode: codexConfig?.serviceMode,
189
152
  sandbox: codexConfig?.sandbox,
190
153
  approvalPolicy: codexConfig?.approvalPolicy,
191
- appServerBinary: codexConfig?.appServer?.binary,
192
- appServerRequestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
193
- appServerIdleTtlMs: codexConfig?.appServer?.idleTtlMs,
194
- appServerTurnIdleTimeoutMinutes: codexConfig?.appServer?.turnIdleTimeoutMinutes,
154
+ binary: codexConfig?.appServer?.binary,
155
+ requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
156
+ idleTtlMs: codexConfig?.appServer?.idleTtlMs,
157
+ turnTimeoutMs: codexConfig?.appServer?.turnIdleTimeoutMinutes != null
158
+ ? codexConfig.appServer.turnIdleTimeoutMinutes * 60_000
159
+ : undefined,
195
160
  });
196
161
  });
197
- // Register OpencodeExecRunner (run mode)
198
- // Using `opencode run --format json --auto` instead of HTTP+SSE server mode
162
+ // Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
199
163
  // Register session reader (same instance as used by runner)
200
164
  const sessionReaderRegistry = new SessionReaderRegistry();
201
165
  sessionReaderRegistry.register('claude', new ClaudeSessionReader());
@@ -203,7 +167,7 @@ function initializeRunner(config, configDir, cliArgs) {
203
167
  // Register OpencodeSessionReader (CLI version)
204
168
  // Using `opencode session list` and `opencode export` instead of HTTP API
205
169
  const opencodeSessionReader = new OpencodeSessionReader();
206
- agentRegistry.register('opencode', (ws) => {
170
+ agentRegistry.register('opencode', (_ws) => {
207
171
  // Get latest config from container (set below for pi)
208
172
  const container = agentRegistry.getConfigContainer();
209
173
  const latestConfig = container?.current ?? config;
@@ -220,17 +184,15 @@ function initializeRunner(config, configDir, cliArgs) {
220
184
  model = `${provider}/${ocConfig.modelID}`;
221
185
  }
222
186
  }
223
- return new OpencodeExecRunner({
187
+ return new OpencodeAcpRunner({
188
+ kind: 'opencode',
224
189
  model,
225
- // P1-15: read stopGraceMs from latestConfig (not startup closure)
226
- stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
227
- pidDir: configDir,
228
- workspace: ws,
229
190
  sessionReader: opencodeSessionReader,
191
+ mode: ocConfig?.mode ?? 'build',
230
192
  });
231
193
  });
232
194
  sessionReaderRegistry.register('opencode', opencodeSessionReader);
233
- // Register PiRunner and PiSessionReader
195
+ // Register PiRpcRunner and PiSessionReader
234
196
  // 2026-07-12: 修复 config.save 后 pi provider 不生效的问题
235
197
  // 关键:factory 闭包必须能访问到最新的 config,而不是启动时的快照
236
198
  // 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值
@@ -242,7 +204,7 @@ function initializeRunner(config, configDir, cliArgs) {
242
204
  const container = agentRegistry.getConfigContainer();
243
205
  const latestConfig = container?.current ?? config;
244
206
  const piConf = getAgentConfig(latestConfig, 'pi');
245
- return new PiRunner({
207
+ return new PiRpcRunner({
246
208
  provider: piConf?.provider ?? 'Volcano',
247
209
  model: piConf?.model ?? 'glm-5.2',
248
210
  thinking: piConf?.thinking ?? 'medium',
@@ -250,36 +212,60 @@ function initializeRunner(config, configDir, cliArgs) {
250
212
  .split(',')
251
213
  .map((t) => t.trim())
252
214
  .filter(Boolean),
253
- pidDir: configDir,
254
215
  workspace: ws,
255
216
  sessionReader: piSessionReader,
256
217
  });
257
218
  });
258
219
  sessionReaderRegistry.register('pi', piSessionReader);
259
- // Register KimiRunner and KimiSessionReader
220
+ // Register KimiAcpRunner (pure ACP mode) and KimiSessionReader
260
221
  const kimiSessionReader = new KimiSessionReader();
261
- agentRegistry.register('kimi', (ws) => {
222
+ agentRegistry.register('kimi', (_ws) => {
262
223
  // 每次 factory 调用时从 registry 获取最新 config
263
224
  const container = agentRegistry.getConfigContainer();
264
225
  const latestConfig = container?.current ?? config;
265
226
  const kimiConf = getAgentConfig(latestConfig, 'kimi');
266
- return new KimiRunner({
267
- model: kimiConf?.model ?? 'kimi-code/k3',
268
- thinkingEffort: kimiConf?.thinkingEffort ?? 'max',
269
- // P1-15: read stopGraceMs from latestConfig (not startup closure)
270
- stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
271
- pidDir: configDir,
272
- workspace: ws,
227
+ const acpConf = kimiConf?.acp;
228
+ return new KimiAcpRunner({
229
+ kind: 'kimi',
273
230
  sessionReader: kimiSessionReader,
231
+ binary: acpConf?.binary ?? 'kimi',
232
+ requestTimeoutMs: acpConf?.requestTimeoutMs,
233
+ idleTtlMs: acpConf?.idleTtlMs,
234
+ turnIdleTimeoutMs: acpConf?.turnIdleTimeoutMinutes != null
235
+ ? acpConf.turnIdleTimeoutMinutes * 60_000
236
+ : undefined,
237
+ model: kimiConf?.model ?? 'kimi-code/k3',
238
+ permissionMode: kimiConf?.permissionMode ?? 'manual',
274
239
  });
275
240
  });
276
241
  sessionReaderRegistry.register('kimi', kimiSessionReader);
242
+ // Register DshRunner (HTTP-only DSH Web Host agent) and DshSessionReader.
243
+ const dshSessionReader = new DshSessionReader({
244
+ host: getAgentConfig(config, 'dsh')?.host,
245
+ });
246
+ agentRegistry.register('dsh', (_ws) => {
247
+ // Read latest config from container so /config host changes take effect
248
+ // after bridge.setConfig() + clearRunners().
249
+ const container = agentRegistry.getConfigContainer();
250
+ const latestConfig = container?.current ?? config;
251
+ const dshConf = getAgentConfig(latestConfig, 'dsh');
252
+ return new DshRunner({
253
+ kind: 'dsh',
254
+ sessionReader: dshSessionReader,
255
+ host: dshConf?.host,
256
+ agentPreset: dshConf?.agentPreset,
257
+ model: dshConf?.model,
258
+ reasoningEffort: dshConf?.reasoningEffort,
259
+ });
260
+ });
261
+ sessionReaderRegistry.register('dsh', dshSessionReader);
277
262
  // 2026-07-17: 注册所有 agent 的显示名,实现单点真相
278
263
  agentRegistry.registerDisplayName('claude', 'Claude');
279
264
  agentRegistry.registerDisplayName('codex', 'Codex');
280
265
  agentRegistry.registerDisplayName('opencode', 'Opencode');
281
266
  agentRegistry.registerDisplayName('pi', 'Pi');
282
267
  agentRegistry.registerDisplayName('kimi', 'Kimi');
268
+ agentRegistry.registerDisplayName('dsh', 'DSH');
283
269
  // 设置全局 registry,供 agentDisplayName 等全局函数使用
284
270
  AgentRegistry.setGlobalInstance(agentRegistry);
285
271
  // Probe agent CLI availability (fire-and-forget, populates cache for /config card).
@@ -301,6 +287,48 @@ function initializeRunner(config, configDir, cliArgs) {
301
287
  }
302
288
  /** Setup message and card action handlers. */
303
289
  function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger, config) {
290
+ // 入站媒体(图片/文件):先认证后下载(P1 review 修复)。
291
+ // connector 只上报"媒体到达",这里先过 owner + enabled 闸门,通过后才
292
+ // 下载——未认证/关闭配置时不会发生任何网络下载或内存/磁盘占用。
293
+ connector.setInboundMediaDetectedHandler((msg) => {
294
+ if (!binder.isOwner(msg.userId)) {
295
+ logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
296
+ return;
297
+ }
298
+ // 读 router.config(活引用):/config 保存后 setConfigValues 返回新对象,
299
+ // 启动时捕获的 config 参数会过期(P2 review 修复)。
300
+ if (!router.config.inboundMedia.enabled) {
301
+ // 不静默(P3 review):关闭时给 owner 明确反馈,避免发图后无任何反应。
302
+ // 不下载、不落盘,只回一条说明。
303
+ void bridge
304
+ .sendResult({
305
+ text: '⚠️ 入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
306
+ '如需自动保存图片/文件,请开启后重试',
307
+ }, {
308
+ userId: msg.userId,
309
+ chatId: msg.chatId,
310
+ messageId: msg.messageId,
311
+ })
312
+ .catch((err) => logger.error('[media] disabled feedback send failed:', err));
313
+ return;
314
+ }
315
+ void (async () => {
316
+ const payload = await connector.downloadInboundMedia(msg, {
317
+ maxFileSizeMb: router.config.inboundMedia.maxFileSizeMb,
318
+ });
319
+ try {
320
+ await bridge.onInboundMedia(payload);
321
+ }
322
+ catch (err) {
323
+ // handle() 的正常路径会清理临时文件;这里兜底 handle 进入 mkdir
324
+ // try 之前意外抛错的情况,避免 os.tmpdir 残留。
325
+ for (const item of payload.media) {
326
+ silentlyUnlink(item.tempPath);
327
+ }
328
+ throw err;
329
+ }
330
+ })().catch((err) => logger.error('[media] inbound media download/save failed:', err));
331
+ });
304
332
  connector.setMessageHandler((msg) => {
305
333
  // 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时要求 PIN 认领
306
334
  const decision = binder.classify(msg.userId, msg.content, msg.chatId);
@@ -358,9 +386,16 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
358
386
  }
359
387
  // decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
360
388
  logger.info(`message from ${msg.userId}: ${msg.content.slice(0, 100)}`);
389
+ // 文本到达先冲刷待合批的媒体保存提示(先图后文字时文字到达
390
+ // 先冲刷批次,避免合批提示被后续消息淹没/丢图)。
391
+ bridge.flushMediaNotifications(msg.userId, msg.chatId);
361
392
  // Add Typing reaction to indicate "still alive"
362
393
  void connector.addReaction(msg.messageId, 'Typing');
363
- const trimmedLower = msg.content.trim().toLowerCase();
394
+ // 别名展开:命令分发前对消息做一次 $name 展开(不递归)。
395
+ // `!` / `/` 开头的消息不会进入展开路径($PATH、$HOME 等 shell 变量不受影响),
396
+ // 未知 $xxx 原样透传;展开结果若以 `/` 开头会自然落入命令路径。
397
+ const content = router.expandAliasMessage(msg.content);
398
+ const trimmedLower = content.trim().toLowerCase();
364
399
  if (trimmedLower === '/stop' || trimmedLower === '/t') {
365
400
  void (async () => {
366
401
  const stopped = await bridge.interruptCurrentRun({
@@ -377,9 +412,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
377
412
  })().catch((err) => logger.error('[control] /stop failed:', err));
378
413
  return;
379
414
  }
380
- if (msg.content.trim().startsWith('/')) {
415
+ if (content.trim().startsWith('/')) {
381
416
  void router
382
- .handle(msg.content, {
417
+ .handle(content, {
383
418
  userId: msg.userId,
384
419
  chatId: msg.chatId,
385
420
  messageId: msg.messageId,
@@ -392,9 +427,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
392
427
  // in the same workspace (design.md §9.6: the queue exists to prevent
393
428
  // concurrent claude runs, not to serialize bash). router.handle dispatches
394
429
  // `!` to bridge.executeBash, which tracks the run independently.
395
- if (msg.content.trim().startsWith('!')) {
430
+ if (content.trim().startsWith('!')) {
396
431
  void router
397
- .handle(msg.content, {
432
+ .handle(content, {
398
433
  userId: msg.userId,
399
434
  chatId: msg.chatId,
400
435
  messageId: msg.messageId,
@@ -418,7 +453,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
418
453
  // 捕获点 Bridge.currentBinding。
419
454
  const binding = bridge.currentBinding(msg.userId);
420
455
  bridge.enqueue(workspace, async () => {
421
- await router.handle(msg.content, {
456
+ await router.handle(content, {
422
457
  userId: msg.userId,
423
458
  chatId: msg.chatId,
424
459
  messageId: msg.messageId,
@@ -433,7 +468,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
433
468
  userId: msg.userId,
434
469
  chatId: msg.chatId,
435
470
  messageId: msg.messageId,
436
- messagePreview: msg.content.slice(0, 3000),
471
+ messagePreview: content.slice(0, 3000),
437
472
  // D3/Step4: binding 随 taskMeta 存进 QueuedTask,供替换闭包
438
473
  // (queue.edit/queue.immediate)复用,不重新快照。
439
474
  binding,
@@ -480,27 +515,24 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
480
515
  }
481
516
  const isImmediate = isImmediateAction(actionValue.cmd);
482
517
  const workspace = sessionStore.getCwd(userId) ?? '';
483
- // Build full value object — spread all fields from actionValue,
484
- // then override option/formValue/inputValue from action for CardKit 2.0 compatibility.
485
- // 2026-07-04: inputValue 来自 CardKit 2.0 input 组件自带提交图标触发 callback
486
- const fullValue = {
487
- ...actionValue,
488
- option: action.action.option,
489
- formValue: action.action.formValue,
490
- // CardKit 2.0 input ✓ 提交图标:输入值走 raw.action.input_value(SDK normalizer
491
- // 丢弃 action.action.input_value,需 connector includeRawEvent: true)。
492
- inputValue: action.raw?.action?.input_value ??
493
- action.action.input_value,
494
- };
495
- // queue.input / config.save / approval.respond / approval.toggle 返回
496
- // CardActionResponse toast 给点击用户即时反馈。必须直接返回值 --
497
- // enqueueImmediate / enqueueConfigAction 是 fire-and-forget,会吞掉返回值。
518
+ // Build full value object — spread all fields from actionValue, then merge
519
+ // component out-of-band fields (option/formValue/inputValue/options) where
520
+ // present. See card-action-payload.ts for the component-type contract.
521
+ const fullValue = buildCardActionFullValue(actionValue, action);
522
+ // queue.input / config.save / approval.respond / approval.toggle /
523
+ // approval.answer 系列返回 CardActionResponse toast 给点击用户即时反馈。
524
+ // 必须直接返回值 -- enqueueImmediate / enqueueConfigAction 是
525
+ // fire-and-forget,会吞掉返回值(2026-08-17 review:answer 家族曾漏在
526
+ // 直返列表外,过期/重复 nonce/非法选项等错误 toast 被静默吞掉)。
498
527
  // 审批响应尤其不能落串行队列:run 任务占用队列头直到 turn 结束,审批响应
499
528
  // 排在后面会形成死锁(run 不结束不执行、run 结束 coordinator 已删响应空转)。
500
529
  if (actionValue.cmd === 'queue.input' ||
501
530
  actionValue.cmd === 'config.save' ||
502
531
  actionValue.cmd === 'approval.respond' ||
503
- actionValue.cmd === 'approval.toggle') {
532
+ actionValue.cmd === 'approval.toggle' ||
533
+ actionValue.cmd === 'approval.answer' ||
534
+ actionValue.cmd === 'approval.answerSubmit' ||
535
+ actionValue.cmd === 'approval.answerCustom') {
504
536
  return router.handleCardAction(fullValue, { userId, chatId, messageId });
505
537
  }
506
538
  // order.exec → equivalent queued message (Plan A): resolve the order text
@@ -618,6 +650,7 @@ async function main() {
618
650
  configPath: path.join(configDir, 'config.yaml'),
619
651
  workspacePath: path.join(configDir, 'workspace.json'),
620
652
  ordersPath: path.join(configDir, 'orders.json'),
653
+ aliasesPath: path.join(configDir, 'aliases.json'),
621
654
  restartSpawner: () => spawnReplacementBridge(path.join(configDir, 'logs')),
622
655
  sessionReaderRegistry,
623
656
  devMode: cliArgs.dev,
@@ -683,9 +716,8 @@ async function main() {
683
716
  displayTitle: content.displayTitle,
684
717
  agentKind: config.defaultAgent,
685
718
  },
686
- // Compact 仅 codex app-server 模式有实现(runner.runCompact)。
687
- config.defaultAgent === 'codex' &&
688
- getAgentConfig(config, 'codex')?.serviceMode === 'app-server');
719
+ // Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
720
+ bridge.hasRunCompact(restoredCwd, config.defaultAgent));
689
721
  }
690
722
  catch (err) {
691
723
  getLogger().warn('[startup] sendAutoResumeCard failed:', err);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
+ import { silentlyUnlink } from './common/fs.js';
4
5
  export class InstanceAlreadyRunningError extends Error {
5
6
  pid;
6
7
  lockPath;
@@ -68,10 +69,7 @@ export class InstanceLock {
68
69
  throw new InstanceAlreadyRunningError(pid, this.lockPath);
69
70
  }
70
71
  // Stale lock: remove and retry once
71
- try {
72
- fs.unlinkSync(this.lockPath);
73
- }
74
- catch { }
72
+ silentlyUnlink(this.lockPath);
75
73
  try {
76
74
  const fd = fs.openSync(this.lockPath, 'wx');
77
75
  fs.writeSync(fd, this.lockContents(process.pid));
@@ -99,10 +97,7 @@ export class InstanceLock {
99
97
  release() {
100
98
  const { pid } = this.readPidAndComm();
101
99
  if (pid === process.pid) {
102
- try {
103
- fs.unlinkSync(this.lockPath);
104
- }
105
- catch { }
100
+ silentlyUnlink(this.lockPath);
106
101
  }
107
102
  }
108
103
  registerExitHandlers() {
@@ -142,10 +137,7 @@ export class InstanceLock {
142
137
  if (Number.isInteger(pid) && pid > 0 && comm)
143
138
  return { pid, comm };
144
139
  // Malformed or missing comm — treat as corrupt, unlink and return undefined
145
- try {
146
- fs.unlinkSync(this.lockPath);
147
- }
148
- catch { }
140
+ silentlyUnlink(this.lockPath);
149
141
  return { pid: undefined, comm: undefined };
150
142
  }
151
143
  }
@@ -0,0 +1,13 @@
1
+ import type { AliasEntry } from './alias-store.js';
2
+ /**
3
+ * 一次别名展开。
4
+ *
5
+ * - 仅匹配消息开头的 `$name`(`^$name`),`$name` 后接空格或行尾,全词精确匹配;
6
+ * - 名称必须 `[A-Za-z_][A-Za-z0-9_]*`(数字开头的 `$500` 永不匹配);
7
+ * - 未知别名返回 undefined(原样透传给 agent,不报错);
8
+ * - 单次不递归:展开结果不再二次展开,防死循环;
9
+ * - `!` / `/` 开头消息天然不匹配(正则要求 `$` 开头),bash 的 `$PATH` 不受影响。
10
+ *
11
+ * @returns 展开后的完整消息;无匹配时返回 undefined。
12
+ */
13
+ export declare function resolveAlias(message: string, aliases: readonly AliasEntry[]): string | undefined;