lark-remote 0.5.20 → 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 (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/bridge/inbound-media.js +71 -45
  3. package/dist/bridge/index.d.ts +9 -0
  4. package/dist/bridge/index.js +230 -203
  5. package/dist/card/approval-render.js +32 -33
  6. package/dist/card/card-budget.js +123 -98
  7. package/dist/card/run-renderer.js +82 -68
  8. package/dist/card/run-state.d.ts +0 -14
  9. package/dist/card/run-state.js +335 -207
  10. package/dist/card/tool-render.js +20 -16
  11. package/dist/card/usage-format.d.ts +12 -9
  12. package/dist/card/usage-format.js +111 -97
  13. package/dist/common/text-width.js +36 -24
  14. package/dist/common/tool-summary.js +132 -90
  15. package/dist/index.js +226 -190
  16. package/dist/message-handlers.js +253 -236
  17. package/dist/router/commands/ls.d.ts +3 -0
  18. package/dist/router/commands/ls.js +256 -175
  19. package/dist/router/commands/order.js +184 -159
  20. package/dist/router/commands/resume.js +189 -133
  21. package/dist/router/commands/ws.js +219 -174
  22. package/dist/router/index.js +192 -201
  23. package/dist/runner/bash/runner.js +2 -4
  24. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  25. package/dist/runner/codex/app-server/translator.js +287 -221
  26. package/dist/runner/common/acp/base-translator.js +19 -17
  27. package/dist/runner/common/jsonl-stream.js +154 -187
  28. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  29. package/dist/runner/common/jsonrpc/client.js +43 -12
  30. package/dist/runner/common/spawning-runner.d.ts +5 -0
  31. package/dist/runner/common/spawning-runner.js +8 -3
  32. package/dist/runner/common/turn-started.d.ts +8 -0
  33. package/dist/runner/common/turn-started.js +11 -0
  34. package/dist/runner/dsh/approval.js +84 -51
  35. package/dist/runner/dsh/client.d.ts +7 -9
  36. package/dist/runner/dsh/client.js +356 -287
  37. package/dist/runner/dsh/mux-stream.js +28 -8
  38. package/dist/runner/dsh/runner.d.ts +4 -3
  39. package/dist/runner/dsh/runner.js +88 -64
  40. package/dist/runner/dsh/translator.d.ts +1 -31
  41. package/dist/runner/dsh/translator.js +180 -178
  42. package/dist/runner/kimi/acp/runner.js +81 -44
  43. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  44. package/dist/runner/pi/rpc/client.js +4 -13
  45. package/dist/runner/pi/rpc/translator.js +61 -60
  46. package/dist/session/claude/sessions.js +113 -101
  47. package/dist/session/codex/rollout-reader.js +179 -180
  48. package/dist/session/dsh/sessions.js +281 -231
  49. package/dist/session/kimi/sessions.d.ts +0 -7
  50. package/dist/session/kimi/sessions.js +129 -109
  51. package/dist/session/opencode/sessions.d.ts +0 -11
  52. package/dist/session/opencode/sessions.js +161 -137
  53. package/dist/session/session-store.d.ts +8 -0
  54. package/dist/session/session-store.js +98 -98
  55. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -160,138 +160,153 @@ function createSessionReaders(config, latest) {
160
160
  });
161
161
  return { claude, codex, opencode, pi, kimi, dsh };
162
162
  }
163
- /**
164
- * 按 agent 家族注册 runner factory。
165
- *
166
- * factory 一律从 configContainer 读最新 config(bridge.setConfig() +
167
- * clearRunners() 后运行时改动生效);`latest()` 未接线时回退启动快照。
168
- */
163
+ // Claude factory reads from configContainer (not closure) so runtime
164
+ // config changes (model, effort, stopGraceMs) take effect after
165
+ // bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
166
+ function claudeRunnerOptions(deps, cwdOpt) {
167
+ const { cliArgs, configDir, latest, readers } = deps;
168
+ const claudeConfig = latest().claude;
169
+ return {
170
+ model: claudeConfig.model,
171
+ effort: claudeConfig.effort,
172
+ stopGraceMs: claudeConfig.stopGraceMs,
173
+ settings: cliArgs.settings,
174
+ pidDir: configDir,
175
+ cwd: cwdOpt.cwd,
176
+ sessionReader: readers.claude,
177
+ permissionMode: claudeConfig.permissionMode,
178
+ idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
179
+ };
180
+ }
181
+ // Register Codex runner (app-server only). The registry factory reads latest
182
+ // config from container.current so runtime config changes (model, sandbox,
183
+ // approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
184
+ // + clearRunners().
185
+ function codexRunnerOptions(deps) {
186
+ const codexConfig = getAgentConfig(deps.latest(), 'codex');
187
+ return {
188
+ kind: 'codex',
189
+ model: codexConfig?.model,
190
+ modelProvider: codexConfig?.modelProvider,
191
+ reasoningEffort: codexConfig?.reasoningEffort,
192
+ sessionReader: deps.readers.codex,
193
+ sandbox: codexConfig?.sandbox,
194
+ approvalPolicy: codexConfig?.approvalPolicy,
195
+ binary: codexConfig?.appServer?.binary,
196
+ requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
197
+ idleTtlMs: codexConfig?.appServer?.idleTtlMs,
198
+ turnTimeoutMs: minutesToMs(codexConfig?.appServer?.turnIdleTimeoutMinutes),
199
+ };
200
+ }
201
+ // Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
202
+ function opencodeRunnerOptions(deps) {
203
+ const ocConfig = getAgentConfig(deps.latest(), 'opencode');
204
+ // Build model string with validation: provider/model format
205
+ let model;
206
+ if (ocConfig?.modelID) {
207
+ const provider = ocConfig.providerID ?? 'anthropic';
208
+ // Validate: if modelID contains '/', it's already in provider/model format
209
+ if (ocConfig.modelID.includes('/')) {
210
+ model = ocConfig.modelID;
211
+ }
212
+ else {
213
+ model = `${provider}/${ocConfig.modelID}`;
214
+ }
215
+ }
216
+ return {
217
+ kind: 'opencode',
218
+ model,
219
+ sessionReader: deps.readers.opencode,
220
+ mode: ocConfig?.mode ?? 'build',
221
+ binary: ocConfig?.acp?.binary,
222
+ requestTimeoutMs: ocConfig?.acp?.requestTimeoutMs,
223
+ idleTtlMs: ocConfig?.acp?.idleTtlMs,
224
+ turnIdleTimeoutMs: minutesToMs(ocConfig?.acp?.turnIdleTimeoutMinutes),
225
+ };
226
+ }
227
+ // Register PiRpcRunner.
228
+ // factory 闭包必须读最新 config 而不是启动时的快照,否则 /config 保存后
229
+ // provider 等字段对 pi 不生效:config 引用放在可变容器里(见上文
230
+ // setConfigContainer),factory 每次从容器取最新值。
231
+ // PiSessionReader 的创建与注册见 registerSessionReaders。
232
+ function piRunnerOptions(deps, cwdOpt) {
233
+ const piConf = getAgentConfig(deps.latest(), 'pi');
234
+ return {
235
+ provider: piConf?.provider ?? PI_DEFAULTS.provider,
236
+ model: piConf?.model ?? PI_DEFAULTS.model,
237
+ thinking: piConf?.thinking ?? PI_DEFAULTS.thinking,
238
+ tools: (piConf?.tools ?? PI_DEFAULTS.tools)
239
+ .split(',')
240
+ .map((t) => t.trim())
241
+ .filter(Boolean),
242
+ cwd: cwdOpt.cwd,
243
+ sessionReader: deps.readers.pi,
244
+ };
245
+ }
246
+ // Register KimiAcpRunner (pure ACP mode)
247
+ function kimiRunnerOptions(deps) {
248
+ const kimiConf = getAgentConfig(deps.latest(), 'kimi');
249
+ const acpConf = kimiConf?.acp;
250
+ return {
251
+ kind: 'kimi',
252
+ sessionReader: deps.readers.kimi,
253
+ binary: acpConf?.binary ?? 'kimi',
254
+ requestTimeoutMs: acpConf?.requestTimeoutMs,
255
+ idleTtlMs: acpConf?.idleTtlMs,
256
+ turnIdleTimeoutMs: minutesToMs(acpConf?.turnIdleTimeoutMinutes),
257
+ model: kimiConf?.model ?? KIMI_DEFAULTS.model,
258
+ thinkingEffort: kimiConf?.thinkingEffort,
259
+ permissionMode: kimiConf?.permissionMode ?? 'manual',
260
+ };
261
+ }
262
+ // Warm the shared process in the background so the first message and
263
+ // `/resume` do not pay the full startup delay. Failures surface from
264
+ // DshRunner.run(), which awaits the same resolver.
265
+ function warmDshDaemon(dshDaemon, failureLabel) {
266
+ void dshDaemon.ensure().catch((error) => {
267
+ getLogger().warn(`[dsh] ${failureLabel}: ${error.message}`);
268
+ });
269
+ }
270
+ // Register DshRunner (HTTP-only DSH Web Host agent).
271
+ function dshRunnerOptions(deps) {
272
+ const dshConf = getAgentConfig(deps.latest(), 'dsh');
273
+ return {
274
+ kind: 'dsh',
275
+ sessionReader: deps.readers.dsh,
276
+ daemon: deps.dshDaemon,
277
+ host: dshConf?.host,
278
+ dshHome: dshConf?.dshHome,
279
+ token: dshConf?.token,
280
+ credentialsFile: dshConf?.credentialsFile,
281
+ agentPreset: dshConf?.agentPreset,
282
+ permissionPreset: dshConf?.permissionPreset,
283
+ provider: dshConf?.provider,
284
+ model: dshConf?.model,
285
+ reasoningEffort: dshConf?.reasoningEffort,
286
+ // Durable markers for turns that outlive this process; reconciled on the
287
+ // next startup (see reconcileInflightDshRuns).
288
+ inflightPath: dshInflightPath(deps.configDir),
289
+ };
290
+ }
169
291
  function registerRunners(agentRegistry, deps) {
170
- const { configDir, cliArgs, latest, dshDaemon, readers } = deps;
171
- // Claude factory reads from configContainer (not closure) so runtime
172
- // config changes (model, effort, stopGraceMs) take effect after
173
- // bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
174
292
  agentRegistry.register('claude', (ws) => {
175
- const claudeConfig = latest().claude;
176
- return new ClaudeRunner({
177
- model: claudeConfig.model,
178
- effort: claudeConfig.effort,
179
- stopGraceMs: claudeConfig.stopGraceMs,
180
- settings: cliArgs.settings,
181
- pidDir: configDir,
182
- cwd: ws,
183
- sessionReader: readers.claude,
184
- permissionMode: claudeConfig.permissionMode,
185
- idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
186
- });
293
+ return new ClaudeRunner(claudeRunnerOptions(deps, { cwd: ws }));
187
294
  });
188
- // Register Codex runner (app-server only). The registry factory reads latest
189
- // config from container.current so runtime config changes (model, sandbox,
190
- // approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
191
- // + clearRunners().
192
295
  agentRegistry.register('codex', (_ws) => {
193
- const codexConfig = getAgentConfig(latest(), 'codex');
194
- return new CodexAppServerRunner({
195
- kind: 'codex',
196
- model: codexConfig?.model,
197
- modelProvider: codexConfig?.modelProvider,
198
- reasoningEffort: codexConfig?.reasoningEffort,
199
- sessionReader: readers.codex,
200
- sandbox: codexConfig?.sandbox,
201
- approvalPolicy: codexConfig?.approvalPolicy,
202
- binary: codexConfig?.appServer?.binary,
203
- requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
204
- idleTtlMs: codexConfig?.appServer?.idleTtlMs,
205
- turnTimeoutMs: minutesToMs(codexConfig?.appServer?.turnIdleTimeoutMinutes),
206
- });
296
+ return new CodexAppServerRunner(codexRunnerOptions(deps));
207
297
  });
208
- // Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
209
298
  agentRegistry.register('opencode', (_ws) => {
210
- const ocConfig = getAgentConfig(latest(), 'opencode');
211
- // Build model string with validation: provider/model format
212
- let model;
213
- if (ocConfig?.modelID) {
214
- const provider = ocConfig.providerID ?? 'anthropic';
215
- // Validate: if modelID contains '/', it's already in provider/model format
216
- if (ocConfig.modelID.includes('/')) {
217
- model = ocConfig.modelID;
218
- }
219
- else {
220
- model = `${provider}/${ocConfig.modelID}`;
221
- }
222
- }
223
- return new OpencodeAcpRunner({
224
- kind: 'opencode',
225
- model,
226
- sessionReader: readers.opencode,
227
- mode: ocConfig?.mode ?? 'build',
228
- binary: ocConfig?.acp?.binary,
229
- requestTimeoutMs: ocConfig?.acp?.requestTimeoutMs,
230
- idleTtlMs: ocConfig?.acp?.idleTtlMs,
231
- turnIdleTimeoutMs: minutesToMs(ocConfig?.acp?.turnIdleTimeoutMinutes),
232
- });
299
+ return new OpencodeAcpRunner(opencodeRunnerOptions(deps));
233
300
  });
234
- // Register PiRpcRunner.
235
- // factory 闭包必须读最新 config 而不是启动时的快照,否则 /config 保存后
236
- // provider 等字段对 pi 不生效:config 引用放在可变容器里(见上文
237
- // setConfigContainer),factory 每次从容器取最新值。
238
- // PiSessionReader 的创建与注册见 registerSessionReaders。
239
301
  agentRegistry.register('pi', (ws) => {
240
- const piConf = getAgentConfig(latest(), 'pi');
241
- return new PiRpcRunner({
242
- provider: piConf?.provider ?? PI_DEFAULTS.provider,
243
- model: piConf?.model ?? PI_DEFAULTS.model,
244
- thinking: piConf?.thinking ?? PI_DEFAULTS.thinking,
245
- tools: (piConf?.tools ?? PI_DEFAULTS.tools)
246
- .split(',')
247
- .map((t) => t.trim())
248
- .filter(Boolean),
249
- cwd: ws,
250
- sessionReader: readers.pi,
251
- });
302
+ return new PiRpcRunner(piRunnerOptions(deps, { cwd: ws }));
252
303
  });
253
- // Register KimiAcpRunner (pure ACP mode)
254
304
  agentRegistry.register('kimi', (_ws) => {
255
- const kimiConf = getAgentConfig(latest(), 'kimi');
256
- const acpConf = kimiConf?.acp;
257
- return new KimiAcpRunner({
258
- kind: 'kimi',
259
- sessionReader: readers.kimi,
260
- binary: acpConf?.binary ?? 'kimi',
261
- requestTimeoutMs: acpConf?.requestTimeoutMs,
262
- idleTtlMs: acpConf?.idleTtlMs,
263
- turnIdleTimeoutMs: minutesToMs(acpConf?.turnIdleTimeoutMinutes),
264
- model: kimiConf?.model ?? KIMI_DEFAULTS.model,
265
- thinkingEffort: kimiConf?.thinkingEffort,
266
- permissionMode: kimiConf?.permissionMode ?? 'manual',
267
- });
305
+ return new KimiAcpRunner(kimiRunnerOptions(deps));
268
306
  });
269
- // Register DshRunner (HTTP-only DSH Web Host agent).
270
307
  agentRegistry.register('dsh', (_ws) => {
271
- const dshConf = getAgentConfig(latest(), 'dsh');
272
- // Warm the shared process in the background so the first message and
273
- // `/resume` do not pay the full startup delay. Failures surface from
274
- // DshRunner.run(), which awaits the same resolver.
275
- void dshDaemon.ensure().catch((error) => {
276
- getLogger().warn(`[dsh] background start failed: ${error.message}`);
277
- });
278
- return new DshRunner({
279
- kind: 'dsh',
280
- sessionReader: readers.dsh,
281
- daemon: dshDaemon,
282
- host: dshConf?.host,
283
- dshHome: dshConf?.dshHome,
284
- token: dshConf?.token,
285
- credentialsFile: dshConf?.credentialsFile,
286
- agentPreset: dshConf?.agentPreset,
287
- permissionPreset: dshConf?.permissionPreset,
288
- provider: dshConf?.provider,
289
- model: dshConf?.model,
290
- reasoningEffort: dshConf?.reasoningEffort,
291
- // Durable markers for turns that outlive this process; reconciled on the
292
- // next startup (see reconcileInflightDshRuns).
293
- inflightPath: dshInflightPath(configDir),
294
- });
308
+ warmDshDaemon(deps.dshDaemon, 'background start failed');
309
+ return new DshRunner(dshRunnerOptions(deps));
295
310
  });
296
311
  }
297
312
  /** 按 agent 家族注册 session reader(与 runner factory 共享同一实例)。 */
@@ -303,47 +318,57 @@ function registerSessionReaders(registry, readers) {
303
318
  registry.register('kimi', readers.kimi);
304
319
  registry.register('dsh', readers.dsh);
305
320
  }
321
+ /** 探测 agent CLI 可用性(fire-and-forget,填充 /config 卡片缓存)。 */
322
+ function probeAgentsInBackground() {
323
+ probeAllAgents().then(logAgentProbeResults).catch(logAgentProbeFailure);
324
+ }
325
+ function logAgentProbeResults(availability) {
326
+ const unavailable = [...availability.entries()].filter(([, ok]) => !ok).map(([kind]) => kind);
327
+ if (unavailable.length > 0) {
328
+ getLogger().warn(`[probe] agent CLI not found or not functional: ${unavailable.join(', ')}`);
329
+ }
330
+ else {
331
+ getLogger().info('[probe] all agent CLIs available');
332
+ }
333
+ }
334
+ function logAgentProbeFailure(err) {
335
+ getLogger().warn(`[probe] background probe failed: ${err.message}`);
336
+ }
337
+ /** runner factory 取最新 config 的唯一入口(container 未接线时回退启动快照)。 */
338
+ function createConfigLatest(agentRegistry, config) {
339
+ return () => agentRegistry.getConfigContainer()?.current ?? config;
340
+ }
341
+ /** One global DSH per machine/DSH_HOME:provider 箭头在模块级函数内定义。 */
342
+ function createDshDaemon(latest) {
343
+ return new DshDaemon({
344
+ host: () => getAgentConfig(latest(), 'dsh')?.host,
345
+ dshHome: () => getAgentConfig(latest(), 'dsh')?.dshHome,
346
+ dshCommand: () => getAgentConfig(latest(), 'dsh')?.dshCommand,
347
+ manageProcess: () => getAgentConfig(latest(), 'dsh')?.manageProcess ?? true,
348
+ startTimeoutMs: () => getAgentConfig(latest(), 'dsh')?.startTimeoutMs,
349
+ });
350
+ }
306
351
  /** Initialize per-workspace runner factory + agent registry. */
307
352
  function initializeRunner(config, configDir, cliArgs) {
308
353
  const agentRegistry = new AgentRegistry();
309
354
  // 单点化:runner factory 取最新 config 的唯一入口(语义保持,
310
355
  // container 未接线时回退启动快照)。
311
- const latest = () => agentRegistry.getConfigContainer()?.current ?? config;
356
+ const latest = createConfigLatest(agentRegistry, config);
312
357
  // 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值。
313
358
  agentRegistry.setConfigContainer({ current: config });
314
359
  // One global DSH per machine/DSH_HOME. This resolver reuses a listening
315
360
  // process or starts `dsh web` detached; it never owns the child's lifetime.
316
- const dshDaemon = new DshDaemon({
317
- host: () => getAgentConfig(latest(), 'dsh')?.host,
318
- dshHome: () => getAgentConfig(latest(), 'dsh')?.dshHome,
319
- dshCommand: () => getAgentConfig(latest(), 'dsh')?.dshCommand,
320
- manageProcess: () => getAgentConfig(latest(), 'dsh')?.manageProcess ?? true,
321
- startTimeoutMs: () => getAgentConfig(latest(), 'dsh')?.startTimeoutMs,
322
- });
361
+ const dshDaemon = createDshDaemon(latest);
323
362
  const readers = createSessionReaders(config, latest);
324
363
  registerRunners(agentRegistry, { configDir, cliArgs, latest, dshDaemon, readers });
325
364
  const sessionReaderRegistry = new SessionReaderRegistry();
326
365
  registerSessionReaders(sessionReaderRegistry, readers);
327
366
  if (config.defaultAgent === 'dsh') {
328
- void dshDaemon.ensure().catch((error) => {
329
- getLogger().warn(`[dsh] startup probe failed: ${error.message}`);
330
- });
367
+ warmDshDaemon(dshDaemon, 'startup probe failed');
331
368
  }
332
369
  // Probe agent CLI availability (fire-and-forget, populates cache for /config card).
333
370
  // Don't block startup — log unavailable agents as warnings.
334
- probeAllAgents()
335
- .then((availability) => {
336
- const unavailable = [...availability.entries()].filter(([, ok]) => !ok).map(([kind]) => kind);
337
- if (unavailable.length > 0) {
338
- getLogger().warn(`[probe] agent CLI not found or not functional: ${unavailable.join(', ')}`);
339
- }
340
- else {
341
- getLogger().info('[probe] all agent CLIs available');
342
- }
343
- })
344
- .catch((err) => {
345
- getLogger().warn(`[probe] background probe failed: ${err.message}`);
346
- });
371
+ probeAgentsInBackground();
347
372
  // Warm the codex model catalog in the background so the first Codex /config
348
373
  // card open reads a warm cache instead of blocking the event loop for up to
349
374
  // 8s (execFileSync cold path). Fire-and-forget; failure lands in negative
@@ -556,13 +581,67 @@ async function restoreLastSession(app) {
556
581
  getLogger().warn('[startup] sendAutoResumeCard failed:', err);
557
582
  }
558
583
  }
584
+ /**
585
+ * 启动时静默检查版本(由 checkUpdateOnStartup 控制,默认关闭)。
586
+ * 抽成模块级 async 函数(原为 main 内的 fire-and-forget IIFE);失败只告警,
587
+ * 不允许经 main().catch → process.exit(1) 拖垮整个桥接。
588
+ */
589
+ async function checkUpdateOnStartupNotify(app) {
590
+ try {
591
+ const cachePath = path.join(app.configDir, 'update-cache.json');
592
+ const { current, latest } = await checkLatestVersion({ cachePath });
593
+ const hint = formatUpdateHint(current, latest);
594
+ if (hint) {
595
+ const contact = app.startupContactStore.getContact();
596
+ const recipient = contact?.chatId ?? contact?.userId;
597
+ if (recipient) {
598
+ await app.connector.sendWithRetry(recipient, { text: hint });
599
+ }
600
+ }
601
+ }
602
+ catch (err) {
603
+ // Non-fatal: startup check failure must not crash the bridge
604
+ app.logger.warn(`[startup] update check failed: ${err.message}`);
605
+ }
606
+ }
607
+ /**
608
+ * 上次进程可能死在 DSH turn 中途:清掉残留的待执行 prompt,并对仍在跑的
609
+ * turn 提示 owner(不自动 cancel——它可能是 Web UI 的 turn)。清理不依赖是否
610
+ * 已绑定;没有收件人时只是不发提示。
611
+ */
612
+ function reconcileDshInflightOnStartup(app) {
613
+ const dshConf = getAgentConfig(app.config, 'dsh');
614
+ if (!dshConf)
615
+ return;
616
+ const contact = app.startupContactStore.getContact();
617
+ const recipient = contact?.chatId ?? contact?.userId;
618
+ void reconcileInflightDshRuns({
619
+ store: new DshInflightStore(dshInflightPath(app.configDir)),
620
+ client: new DshClient(dshConf.host ?? 'http://127.0.0.1:3080', {
621
+ dshHome: dshConf.dshHome,
622
+ token: dshConf.token,
623
+ credentialsFile: dshConf.credentialsFile,
624
+ // Host may be down: bound the probe instead of waiting the default 60s.
625
+ requestTimeoutMs: 3_000,
626
+ }),
627
+ ...(recipient
628
+ ? {
629
+ notify: async (text) => {
630
+ await app.connector.sendWithRetry(recipient, { text });
631
+ },
632
+ }
633
+ : {}),
634
+ }).catch((err) => {
635
+ app.logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
636
+ });
637
+ }
559
638
  async function main() {
560
639
  const cliArgs = parseCliArgs();
561
640
  if (await handleCliEarlyExit(cliArgs))
562
641
  return;
563
642
  const boot = await bootstrap(cliArgs);
564
643
  const app = wire(boot);
565
- const { config, configDir, logger, connector, startupContactStore, binder } = app;
644
+ const { config, logger, connector, startupContactStore, binder } = app;
566
645
  try {
567
646
  await connector.connect();
568
647
  }
@@ -575,53 +654,10 @@ async function main() {
575
654
  await sendStartupHello(connector, startupContactStore, { dev: app.cliArgs.dev });
576
655
  // 启动时静默检查版本(由 checkUpdateOnStartup 控制,默认关闭)
577
656
  if (config.checkUpdateOnStartup) {
578
- void (async () => {
579
- try {
580
- const cachePath = path.join(configDir, 'update-cache.json');
581
- const { current, latest } = await checkLatestVersion({ cachePath });
582
- const hint = formatUpdateHint(current, latest);
583
- if (hint) {
584
- const contact = startupContactStore.getContact();
585
- const recipient = contact?.chatId ?? contact?.userId;
586
- if (recipient) {
587
- await connector.sendWithRetry(recipient, { text: hint });
588
- }
589
- }
590
- }
591
- catch (err) {
592
- // Non-fatal: startup check failure must not crash the bridge
593
- logger.warn(`[startup] update check failed: ${err.message}`);
594
- }
595
- })();
657
+ void checkUpdateOnStartupNotify(app);
596
658
  }
597
659
  }
598
- // 上次进程可能死在 DSH turn 中途:清掉残留的待执行 prompt,并对仍在跑的
599
- // turn 提示 owner(不自动 cancel——它可能是 Web UI 的 turn)。清理不依赖是否
600
- // 已绑定;没有收件人时只是不发提示。
601
- const dshConf = getAgentConfig(config, 'dsh');
602
- if (dshConf) {
603
- const contact = startupContactStore.getContact();
604
- const recipient = contact?.chatId ?? contact?.userId;
605
- void reconcileInflightDshRuns({
606
- store: new DshInflightStore(dshInflightPath(configDir)),
607
- client: new DshClient(dshConf.host ?? 'http://127.0.0.1:3080', {
608
- dshHome: dshConf.dshHome,
609
- token: dshConf.token,
610
- credentialsFile: dshConf.credentialsFile,
611
- // Host may be down: bound the probe instead of waiting the default 60s.
612
- requestTimeoutMs: 3_000,
613
- }),
614
- ...(recipient
615
- ? {
616
- notify: async (text) => {
617
- await connector.sendWithRetry(recipient, { text });
618
- },
619
- }
620
- : {}),
621
- }).catch((err) => {
622
- logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
623
- });
624
- }
660
+ reconcileDshInflightOnStartup(app);
625
661
  await restoreLastSession(app);
626
662
  logger.info('lark-remote is running, press Ctrl+C to exit');
627
663
  }