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.
- package/CHANGELOG.md +10 -0
- package/dist/bridge/inbound-media.js +71 -45
- package/dist/bridge/index.d.ts +9 -0
- package/dist/bridge/index.js +230 -203
- package/dist/card/approval-render.js +32 -33
- package/dist/card/card-budget.js +123 -98
- package/dist/card/run-renderer.js +82 -68
- package/dist/card/run-state.d.ts +0 -14
- package/dist/card/run-state.js +335 -207
- package/dist/card/tool-render.js +20 -16
- package/dist/card/usage-format.d.ts +12 -9
- package/dist/card/usage-format.js +111 -97
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/index.js +226 -190
- package/dist/message-handlers.js +253 -236
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +189 -133
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/index.js +192 -201
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +287 -221
- package/dist/runner/common/acp/base-translator.js +19 -17
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/spawning-runner.d.ts +5 -0
- package/dist/runner/common/spawning-runner.js +8 -3
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/client.d.ts +7 -9
- package/dist/runner/dsh/client.js +356 -287
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +4 -3
- package/dist/runner/dsh/runner.js +88 -64
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/kimi/acp/runner.js +81 -44
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/session/claude/sessions.js +113 -101
- package/dist/session/codex/rollout-reader.js +179 -180
- package/dist/session/dsh/sessions.js +281 -231
- package/dist/session/kimi/sessions.d.ts +0 -7
- package/dist/session/kimi/sessions.js +129 -109
- package/dist/session/opencode/sessions.d.ts +0 -11
- package/dist/session/opencode/sessions.js +161 -137
- package/dist/session/session-store.d.ts +8 -0
- package/dist/session/session-store.js +98 -98
- 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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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 = (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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
|
}
|