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.
- package/CHANGELOG.md +29 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +72 -46
- package/dist/bridge/index.d.ts +106 -153
- package/dist/bridge/index.js +400 -1000
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +141 -130
- package/dist/card/card-budget.js +123 -98
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +114 -159
- package/dist/card/run-state.d.ts +1 -15
- package/dist/card/run-state.js +358 -292
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +21 -17
- package/dist/card/usage-format.d.ts +22 -7
- package/dist/card/usage-format.js +119 -100
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +387 -281
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +254 -237
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- 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 +190 -152
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +401 -391
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +2 -5
- package/dist/runner/codex/app-server/translator.js +291 -238
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +50 -26
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- 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/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +6 -1
- package/dist/runner/common/spawning-runner.js +9 -4
- 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/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +7 -11
- package/dist/runner/dsh/client.js +356 -289
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +44 -4
- package/dist/runner/dsh/runner.js +296 -199
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +82 -46
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +160 -122
- package/dist/session/codex/rollout-reader.js +181 -182
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +291 -235
- package/dist/session/kimi/sessions.d.ts +40 -8
- package/dist/session/kimi/sessions.js +373 -301
- package/dist/session/opencode/sessions.d.ts +142 -1
- package/dist/session/opencode/sessions.js +378 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +9 -1
- package/dist/session/session-store.js +99 -99
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfig, getAgentConfig } from './config/index.js';
|
|
1
|
+
import { loadConfig, getAgentConfig, PI_DEFAULTS, KIMI_DEFAULTS, } 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';
|
|
@@ -29,6 +29,13 @@ import { buildSessionHistoryCard, pickResumeCardEvents } from './card/card-helpe
|
|
|
29
29
|
import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import { setupMessageHandlers } from './message-handlers.js';
|
|
32
|
+
/**
|
|
33
|
+
* 分钟 → 毫秒(turn idle timeout 等 config 字段共用)。
|
|
34
|
+
* 未配置(undefined/null)→ undefined = 不设该超时,与原三处内联三元逐字等价。
|
|
35
|
+
*/
|
|
36
|
+
function minutesToMs(minutes) {
|
|
37
|
+
return minutes != null ? minutes * 60_000 : undefined;
|
|
38
|
+
}
|
|
32
39
|
/** Send auto-resume card for a restored session */
|
|
33
40
|
async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false, stillRunning = false) {
|
|
34
41
|
if (!contact.userId)
|
|
@@ -38,7 +45,7 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state, compa
|
|
|
38
45
|
return;
|
|
39
46
|
// Limit history events to prevent card size exceeded error (11310).
|
|
40
47
|
// 窗口按"最后 N 条"截,但强制保住最后一条用户消息与最后一条 assistant 正文,
|
|
41
|
-
//
|
|
48
|
+
// 否则工具往返多时(如连续 bash 面板占满窗口)会把这两条挤出卡。
|
|
42
49
|
const MAX_HISTORY_EVENTS = 5;
|
|
43
50
|
const eventsToShow = pickResumeCardEvents(state.events, MAX_HISTORY_EVENTS);
|
|
44
51
|
const hiddenCount = state.events.length - eventsToShow.length;
|
|
@@ -122,151 +129,28 @@ async function setupInstanceLockAndHandlers(instanceLock, logger, configDir) {
|
|
|
122
129
|
}
|
|
123
130
|
});
|
|
124
131
|
}
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const latest = () => agentRegistry.getConfigContainer()?.current ?? config;
|
|
131
|
-
// One global DSH per machine/DSH_HOME. This resolver reuses a listening
|
|
132
|
-
// process or starts `dsh web` detached; it never owns the child's lifetime.
|
|
133
|
-
const dshDaemon = new DshDaemon({
|
|
134
|
-
host: () => getAgentConfig(latest(), 'dsh')?.host,
|
|
135
|
-
dshHome: () => getAgentConfig(latest(), 'dsh')?.dshHome,
|
|
136
|
-
dshCommand: () => getAgentConfig(latest(), 'dsh')?.dshCommand,
|
|
137
|
-
manageProcess: () => getAgentConfig(latest(), 'dsh')?.manageProcess ?? true,
|
|
138
|
-
startTimeoutMs: () => getAgentConfig(latest(), 'dsh')?.startTimeoutMs,
|
|
139
|
-
});
|
|
132
|
+
/**
|
|
133
|
+
* 创建全部 session reader。runner 侧不 import session 层(否则 runner ↔ session
|
|
134
|
+
* 互相依赖),统一由组合根注入。
|
|
135
|
+
*/
|
|
136
|
+
function createSessionReaders(config, latest) {
|
|
140
137
|
// claude 的会话读取器与其余 agent 一样由组合根注入:runner 侧不 import session
|
|
141
138
|
// 层(否则 runner ↔ session 互相依赖),且 bridge 读 usage / router 列 session
|
|
142
139
|
// 用的是同一个实例(缓存共用)。
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
// config changes (model, effort, stopGraceMs) take effect after
|
|
146
|
-
// bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
|
|
147
|
-
agentRegistry.register('claude', (ws) => {
|
|
148
|
-
const claudeConfig = latest().claude;
|
|
149
|
-
return new ClaudeRunner({
|
|
150
|
-
model: claudeConfig.model,
|
|
151
|
-
effort: claudeConfig.effort,
|
|
152
|
-
stopGraceMs: claudeConfig.stopGraceMs,
|
|
153
|
-
settings: cliArgs.settings,
|
|
154
|
-
pidDir: configDir,
|
|
155
|
-
cwd: ws,
|
|
156
|
-
sessionReader: claudeSessionReader,
|
|
157
|
-
permissionMode: claudeConfig.permissionMode,
|
|
158
|
-
idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
// Register Codex runner (app-server only). The registry factory reads latest
|
|
162
|
-
// config from container.current so runtime config changes (model, sandbox,
|
|
163
|
-
// approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
|
|
164
|
-
// + clearRunners().
|
|
165
|
-
const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
|
|
166
|
-
agentRegistry.register('codex', (_ws) => {
|
|
167
|
-
const codexConfig = getAgentConfig(latest(), 'codex');
|
|
168
|
-
return new CodexAppServerRunner({
|
|
169
|
-
kind: 'codex',
|
|
170
|
-
model: codexConfig?.model,
|
|
171
|
-
modelProvider: codexConfig?.modelProvider,
|
|
172
|
-
reasoningEffort: codexConfig?.reasoningEffort,
|
|
173
|
-
sessionReader: codexSessionReader,
|
|
174
|
-
sandbox: codexConfig?.sandbox,
|
|
175
|
-
approvalPolicy: codexConfig?.approvalPolicy,
|
|
176
|
-
binary: codexConfig?.appServer?.binary,
|
|
177
|
-
requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
|
|
178
|
-
idleTtlMs: codexConfig?.appServer?.idleTtlMs,
|
|
179
|
-
turnTimeoutMs: codexConfig?.appServer?.turnIdleTimeoutMinutes != null
|
|
180
|
-
? codexConfig.appServer.turnIdleTimeoutMinutes * 60_000
|
|
181
|
-
: undefined,
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
// Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
|
|
185
|
-
// Register session reader (same instance as used by runner)
|
|
186
|
-
const sessionReaderRegistry = new SessionReaderRegistry();
|
|
187
|
-
sessionReaderRegistry.register('claude', claudeSessionReader);
|
|
188
|
-
sessionReaderRegistry.register('codex', codexSessionReader);
|
|
140
|
+
const claude = new ClaudeSessionReader();
|
|
141
|
+
const codex = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
|
|
189
142
|
// Register OpencodeSessionReader (CLI version)
|
|
190
143
|
// Using `opencode session list` and `session export` instead of HTTP API.
|
|
191
144
|
// Share the configured binary with the ACP runner so `opencode2` (v2) can
|
|
192
145
|
// be used without the reader silently falling back to a v1 `opencode`.
|
|
193
146
|
// The reader is registered once; changing acp.binary at runtime requires a
|
|
194
147
|
// lark-remote restart.
|
|
195
|
-
const
|
|
148
|
+
const opencode = new OpencodeSessionReader({
|
|
196
149
|
binary: getAgentConfig(config, 'opencode')?.acp?.binary,
|
|
197
150
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let model;
|
|
202
|
-
if (ocConfig?.modelID) {
|
|
203
|
-
const provider = ocConfig.providerID ?? 'anthropic';
|
|
204
|
-
// Validate: if modelID contains '/', it's already in provider/model format
|
|
205
|
-
if (ocConfig.modelID.includes('/')) {
|
|
206
|
-
model = ocConfig.modelID;
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
model = `${provider}/${ocConfig.modelID}`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return new OpencodeAcpRunner({
|
|
213
|
-
kind: 'opencode',
|
|
214
|
-
model,
|
|
215
|
-
sessionReader: opencodeSessionReader,
|
|
216
|
-
mode: ocConfig?.mode ?? 'build',
|
|
217
|
-
binary: ocConfig?.acp?.binary,
|
|
218
|
-
requestTimeoutMs: ocConfig?.acp?.requestTimeoutMs,
|
|
219
|
-
idleTtlMs: ocConfig?.acp?.idleTtlMs,
|
|
220
|
-
turnIdleTimeoutMs: ocConfig?.acp?.turnIdleTimeoutMinutes != null
|
|
221
|
-
? ocConfig.acp.turnIdleTimeoutMinutes * 60_000
|
|
222
|
-
: undefined,
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
sessionReaderRegistry.register('opencode', opencodeSessionReader);
|
|
226
|
-
// Register PiRpcRunner and PiSessionReader
|
|
227
|
-
// 2026-07-12: 修复 config.save 后 pi provider 不生效的问题
|
|
228
|
-
// 关键:factory 闭包必须能访问到最新的 config,而不是启动时的快照
|
|
229
|
-
// 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值
|
|
230
|
-
const configContainer = { current: config };
|
|
231
|
-
agentRegistry.setConfigContainer(configContainer);
|
|
232
|
-
const piSessionReader = new PiSessionReader();
|
|
233
|
-
agentRegistry.register('pi', (ws) => {
|
|
234
|
-
const piConf = getAgentConfig(latest(), 'pi');
|
|
235
|
-
return new PiRpcRunner({
|
|
236
|
-
provider: piConf?.provider ?? 'Volcano',
|
|
237
|
-
model: piConf?.model ?? 'glm-5.2',
|
|
238
|
-
thinking: piConf?.thinking ?? 'medium',
|
|
239
|
-
tools: (piConf?.tools ?? 'read,bash,edit,write,grep,find,ls')
|
|
240
|
-
.split(',')
|
|
241
|
-
.map((t) => t.trim())
|
|
242
|
-
.filter(Boolean),
|
|
243
|
-
cwd: ws,
|
|
244
|
-
sessionReader: piSessionReader,
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
sessionReaderRegistry.register('pi', piSessionReader);
|
|
248
|
-
// Register KimiAcpRunner (pure ACP mode) and KimiSessionReader
|
|
249
|
-
const kimiSessionReader = new KimiSessionReader();
|
|
250
|
-
agentRegistry.register('kimi', (_ws) => {
|
|
251
|
-
const kimiConf = getAgentConfig(latest(), 'kimi');
|
|
252
|
-
const acpConf = kimiConf?.acp;
|
|
253
|
-
return new KimiAcpRunner({
|
|
254
|
-
kind: 'kimi',
|
|
255
|
-
sessionReader: kimiSessionReader,
|
|
256
|
-
binary: acpConf?.binary ?? 'kimi',
|
|
257
|
-
requestTimeoutMs: acpConf?.requestTimeoutMs,
|
|
258
|
-
idleTtlMs: acpConf?.idleTtlMs,
|
|
259
|
-
turnIdleTimeoutMs: acpConf?.turnIdleTimeoutMinutes != null
|
|
260
|
-
? acpConf.turnIdleTimeoutMinutes * 60_000
|
|
261
|
-
: undefined,
|
|
262
|
-
model: kimiConf?.model ?? 'kimi-code/k3',
|
|
263
|
-
thinkingEffort: kimiConf?.thinkingEffort,
|
|
264
|
-
permissionMode: kimiConf?.permissionMode ?? 'manual',
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
sessionReaderRegistry.register('kimi', kimiSessionReader);
|
|
268
|
-
// Register DshRunner (HTTP-only DSH Web Host agent) and DshSessionReader.
|
|
269
|
-
const dshSessionReader = new DshSessionReader({
|
|
151
|
+
const pi = new PiSessionReader();
|
|
152
|
+
const kimi = new KimiSessionReader();
|
|
153
|
+
const dsh = new DshSessionReader({
|
|
270
154
|
// hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
|
|
271
155
|
// 新 host,reader(/resume、用量、完成卡)也跟随新 host。
|
|
272
156
|
hostProvider: () => getAgentConfig(latest(), 'dsh')?.host,
|
|
@@ -274,53 +158,217 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
274
158
|
tokenProvider: () => getAgentConfig(latest(), 'dsh')?.token,
|
|
275
159
|
credentialsFileProvider: () => getAgentConfig(latest(), 'dsh')?.credentialsFile,
|
|
276
160
|
});
|
|
161
|
+
return { claude, codex, opencode, pi, kimi, dsh };
|
|
162
|
+
}
|
|
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
|
+
}
|
|
291
|
+
function registerRunners(agentRegistry, deps) {
|
|
292
|
+
agentRegistry.register('claude', (ws) => {
|
|
293
|
+
return new ClaudeRunner(claudeRunnerOptions(deps, { cwd: ws }));
|
|
294
|
+
});
|
|
295
|
+
agentRegistry.register('codex', (_ws) => {
|
|
296
|
+
return new CodexAppServerRunner(codexRunnerOptions(deps));
|
|
297
|
+
});
|
|
298
|
+
agentRegistry.register('opencode', (_ws) => {
|
|
299
|
+
return new OpencodeAcpRunner(opencodeRunnerOptions(deps));
|
|
300
|
+
});
|
|
301
|
+
agentRegistry.register('pi', (ws) => {
|
|
302
|
+
return new PiRpcRunner(piRunnerOptions(deps, { cwd: ws }));
|
|
303
|
+
});
|
|
304
|
+
agentRegistry.register('kimi', (_ws) => {
|
|
305
|
+
return new KimiAcpRunner(kimiRunnerOptions(deps));
|
|
306
|
+
});
|
|
277
307
|
agentRegistry.register('dsh', (_ws) => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// `/resume` do not pay the full startup delay. Failures surface from
|
|
281
|
-
// DshRunner.run(), which awaits the same resolver.
|
|
282
|
-
void dshDaemon.ensure().catch((error) => {
|
|
283
|
-
getLogger().warn(`[dsh] background start failed: ${error.message}`);
|
|
284
|
-
});
|
|
285
|
-
return new DshRunner({
|
|
286
|
-
kind: 'dsh',
|
|
287
|
-
sessionReader: dshSessionReader,
|
|
288
|
-
daemon: dshDaemon,
|
|
289
|
-
host: dshConf?.host,
|
|
290
|
-
dshHome: dshConf?.dshHome,
|
|
291
|
-
token: dshConf?.token,
|
|
292
|
-
credentialsFile: dshConf?.credentialsFile,
|
|
293
|
-
agentPreset: dshConf?.agentPreset,
|
|
294
|
-
permissionPreset: dshConf?.permissionPreset,
|
|
295
|
-
provider: dshConf?.provider,
|
|
296
|
-
model: dshConf?.model,
|
|
297
|
-
reasoningEffort: dshConf?.reasoningEffort,
|
|
298
|
-
// Durable markers for turns that outlive this process; reconciled on the
|
|
299
|
-
// next startup (see reconcileInflightDshRuns).
|
|
300
|
-
inflightPath: dshInflightPath(configDir),
|
|
301
|
-
});
|
|
308
|
+
warmDshDaemon(deps.dshDaemon, 'background start failed');
|
|
309
|
+
return new DshRunner(dshRunnerOptions(deps));
|
|
302
310
|
});
|
|
303
|
-
|
|
311
|
+
}
|
|
312
|
+
/** 按 agent 家族注册 session reader(与 runner factory 共享同一实例)。 */
|
|
313
|
+
function registerSessionReaders(registry, readers) {
|
|
314
|
+
registry.register('claude', readers.claude);
|
|
315
|
+
registry.register('codex', readers.codex);
|
|
316
|
+
registry.register('opencode', readers.opencode);
|
|
317
|
+
registry.register('pi', readers.pi);
|
|
318
|
+
registry.register('kimi', readers.kimi);
|
|
319
|
+
registry.register('dsh', readers.dsh);
|
|
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
|
+
}
|
|
351
|
+
/** Initialize per-workspace runner factory + agent registry. */
|
|
352
|
+
function initializeRunner(config, configDir, cliArgs) {
|
|
353
|
+
const agentRegistry = new AgentRegistry();
|
|
354
|
+
// 单点化:runner factory 取最新 config 的唯一入口(语义保持,
|
|
355
|
+
// container 未接线时回退启动快照)。
|
|
356
|
+
const latest = createConfigLatest(agentRegistry, config);
|
|
357
|
+
// 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值。
|
|
358
|
+
agentRegistry.setConfigContainer({ current: config });
|
|
359
|
+
// One global DSH per machine/DSH_HOME. This resolver reuses a listening
|
|
360
|
+
// process or starts `dsh web` detached; it never owns the child's lifetime.
|
|
361
|
+
const dshDaemon = createDshDaemon(latest);
|
|
362
|
+
const readers = createSessionReaders(config, latest);
|
|
363
|
+
registerRunners(agentRegistry, { configDir, cliArgs, latest, dshDaemon, readers });
|
|
364
|
+
const sessionReaderRegistry = new SessionReaderRegistry();
|
|
365
|
+
registerSessionReaders(sessionReaderRegistry, readers);
|
|
304
366
|
if (config.defaultAgent === 'dsh') {
|
|
305
|
-
|
|
306
|
-
getLogger().warn(`[dsh] startup probe failed: ${error.message}`);
|
|
307
|
-
});
|
|
367
|
+
warmDshDaemon(dshDaemon, 'startup probe failed');
|
|
308
368
|
}
|
|
309
369
|
// Probe agent CLI availability (fire-and-forget, populates cache for /config card).
|
|
310
370
|
// Don't block startup — log unavailable agents as warnings.
|
|
311
|
-
|
|
312
|
-
.then((availability) => {
|
|
313
|
-
const unavailable = [...availability.entries()].filter(([, ok]) => !ok).map(([kind]) => kind);
|
|
314
|
-
if (unavailable.length > 0) {
|
|
315
|
-
getLogger().warn(`[probe] agent CLI not found or not functional: ${unavailable.join(', ')}`);
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
getLogger().info('[probe] all agent CLIs available');
|
|
319
|
-
}
|
|
320
|
-
})
|
|
321
|
-
.catch((err) => {
|
|
322
|
-
getLogger().warn(`[probe] background probe failed: ${err.message}`);
|
|
323
|
-
});
|
|
371
|
+
probeAgentsInBackground();
|
|
324
372
|
// Warm the codex model catalog in the background so the first Codex /config
|
|
325
373
|
// card open reads a warm cache instead of blocking the event loop for up to
|
|
326
374
|
// 8s (execFileSync cold path). Fire-and-forget; failure lands in negative
|
|
@@ -328,8 +376,14 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
328
376
|
warmCodexCatalogCache();
|
|
329
377
|
return { agentRegistry, sessionReaderRegistry };
|
|
330
378
|
}
|
|
331
|
-
|
|
332
|
-
|
|
379
|
+
/**
|
|
380
|
+
* CLI 早期退出分支:--help / --version / --update。
|
|
381
|
+
*
|
|
382
|
+
* 红线:三者都必须在 config 加载与单例锁之前完成——--help/--version 是纯信息
|
|
383
|
+
* 查询,另一实例在跑、或还没有 config 时也必须能应答;--update 不是守护类命令,
|
|
384
|
+
* 等锁只会误报 "already running"。返回 true 表示进程应停止。
|
|
385
|
+
*/
|
|
386
|
+
async function handleCliEarlyExit(cliArgs) {
|
|
333
387
|
// --help/--version must bypass config loading and singleton lock — it's a pure
|
|
334
388
|
// informational query that must work even when another instance is running
|
|
335
389
|
// or no config exists yet.
|
|
@@ -367,6 +421,14 @@ async function main() {
|
|
|
367
421
|
process.exit(1);
|
|
368
422
|
}
|
|
369
423
|
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Bootstrap:configDir → config → logger → 单例锁 → 防休眠 → 启动配置日志。
|
|
428
|
+
* 顺序红线:waitForPreviousInstance 必须先于 instanceLock.acquire()(/restart
|
|
429
|
+
* 替补进程要等旧实例释放锁才能拿到单例锁);PID 存活校验在 acquire 内完成。
|
|
430
|
+
*/
|
|
431
|
+
async function bootstrap(cliArgs) {
|
|
370
432
|
const { config, configDir, logger, instanceLock } = await initializeCliAndConfig();
|
|
371
433
|
// Restart handoff: if spawned as a /restart replacement, wait for the old
|
|
372
434
|
// lark-remote to die (and release the instance lock) before acquiring it.
|
|
@@ -387,6 +449,11 @@ async function main() {
|
|
|
387
449
|
if (cliArgs.settings) {
|
|
388
450
|
logger.info(`claude.settings = ${cliArgs.settings}`);
|
|
389
451
|
}
|
|
452
|
+
return { cliArgs, config, configDir, logger };
|
|
453
|
+
}
|
|
454
|
+
/** Wiring:装配 connector / stores / bridge / router / 消息处理器(不连接)。 */
|
|
455
|
+
function wire(boot) {
|
|
456
|
+
const { cliArgs, config, configDir, logger } = boot;
|
|
390
457
|
const { agentRegistry, sessionReaderRegistry } = initializeRunner(config, configDir, cliArgs);
|
|
391
458
|
const connector = new FeishuConnector(config);
|
|
392
459
|
const startupContactStore = new StartupContactStore(path.join(configDir, 'startup-contact.json'));
|
|
@@ -438,6 +505,143 @@ async function main() {
|
|
|
438
505
|
config,
|
|
439
506
|
cloneSession,
|
|
440
507
|
});
|
|
508
|
+
return {
|
|
509
|
+
cliArgs,
|
|
510
|
+
config,
|
|
511
|
+
configDir,
|
|
512
|
+
logger,
|
|
513
|
+
connector,
|
|
514
|
+
startupContactStore,
|
|
515
|
+
binder,
|
|
516
|
+
sessionStore,
|
|
517
|
+
bridge,
|
|
518
|
+
sessionReaderRegistry,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Auto-restore: resume the persisted sessionId if available.
|
|
523
|
+
* 非关键启动优化:发送失败只告警(logger 已初始化),不允许经
|
|
524
|
+
* main().catch → process.exit(1) 拖垮整个桥接。
|
|
525
|
+
*/
|
|
526
|
+
async function restoreLastSession(app) {
|
|
527
|
+
const { config, logger, sessionStore, sessionReaderRegistry, bridge, connector, startupContactStore, } = app;
|
|
528
|
+
const restoredContact = startupContactStore.getContact();
|
|
529
|
+
if (!restoredContact?.userId)
|
|
530
|
+
return;
|
|
531
|
+
const restoredCwd = sessionStore.getCwd(restoredContact.userId);
|
|
532
|
+
if (!restoredCwd)
|
|
533
|
+
return;
|
|
534
|
+
const sessionIdToResume = sessionStore.getSessionId(restoredContact.userId, config.defaultAgent);
|
|
535
|
+
if (!sessionIdToResume)
|
|
536
|
+
return;
|
|
537
|
+
logger.info(`[startup] auto-resuming persisted session ${sessionIdToResume} in ${restoredCwd}`);
|
|
538
|
+
const startupReader = sessionReaderRegistry.get(config.defaultAgent);
|
|
539
|
+
sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
|
|
540
|
+
const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
|
|
541
|
+
// 该会话可能仍有在途 turn(上一个实例退出/崩溃后留在常驻 host 里)。
|
|
542
|
+
// 不取消、不接管,但必须在卡上说明,否则用户会把它当"卡住了"。
|
|
543
|
+
//
|
|
544
|
+
// 只有 DSH reader 的 isSessionActive() 是服务端的 running 事实。其他
|
|
545
|
+
// reader 的同名方法只表示"最近一段时间有写入",拿它当 turn 在跑会在
|
|
546
|
+
// 每次自动恢复时误报。
|
|
547
|
+
const stillRunning = config.defaultAgent === 'dsh'
|
|
548
|
+
? (() => {
|
|
549
|
+
try {
|
|
550
|
+
return startupReader.isSessionActive(sessionIdToResume, restoredCwd);
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
getLogger().warn(`[startup] isSessionActive failed: ${err.message}`);
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
})()
|
|
557
|
+
: false;
|
|
558
|
+
if (stillRunning) {
|
|
559
|
+
getLogger().warn(`[startup] session ${sessionIdToResume} still has a running turn; auto-resume card is a snapshot`);
|
|
560
|
+
}
|
|
561
|
+
// 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
|
|
562
|
+
// 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
|
|
563
|
+
const usage = content.usage;
|
|
564
|
+
// Build auto-resume card and send to the user. Auto-resume is a
|
|
565
|
+
// non-critical startup nicety: a sendWithRetry failure here (e.g.
|
|
566
|
+
// transient feishu API error) must not crash the bridge via
|
|
567
|
+
// main().catch → process.exit(1). Logger is already initialized.
|
|
568
|
+
try {
|
|
569
|
+
await sendAutoResumeCard(connector, restoredContact, restoredCwd, { sessionId: sessionIdToResume, summary: '' }, {
|
|
570
|
+
events: content.events,
|
|
571
|
+
usage,
|
|
572
|
+
aiTitle: content.aiTitle,
|
|
573
|
+
recap: content.recap,
|
|
574
|
+
displayTitle: content.displayTitle,
|
|
575
|
+
agentKind: config.defaultAgent,
|
|
576
|
+
},
|
|
577
|
+
// Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
|
|
578
|
+
bridge.hasRunCompact(restoredCwd, config.defaultAgent), stillRunning);
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
getLogger().warn('[startup] sendAutoResumeCard failed:', err);
|
|
582
|
+
}
|
|
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
|
+
}
|
|
638
|
+
async function main() {
|
|
639
|
+
const cliArgs = parseCliArgs();
|
|
640
|
+
if (await handleCliEarlyExit(cliArgs))
|
|
641
|
+
return;
|
|
642
|
+
const boot = await bootstrap(cliArgs);
|
|
643
|
+
const app = wire(boot);
|
|
644
|
+
const { config, logger, connector, startupContactStore, binder } = app;
|
|
441
645
|
try {
|
|
442
646
|
await connector.connect();
|
|
443
647
|
}
|
|
@@ -447,112 +651,14 @@ async function main() {
|
|
|
447
651
|
}
|
|
448
652
|
if (binder.isBound()) {
|
|
449
653
|
// 仅已绑定时发送启动通知;未绑定时不打扰(PIN 引导已在控制台输出)
|
|
450
|
-
await sendStartupHello(connector, startupContactStore, { dev: cliArgs.dev });
|
|
654
|
+
await sendStartupHello(connector, startupContactStore, { dev: app.cliArgs.dev });
|
|
451
655
|
// 启动时静默检查版本(由 checkUpdateOnStartup 控制,默认关闭)
|
|
452
656
|
if (config.checkUpdateOnStartup) {
|
|
453
|
-
void (
|
|
454
|
-
try {
|
|
455
|
-
const cachePath = path.join(configDir, 'update-cache.json');
|
|
456
|
-
const { current, latest } = await checkLatestVersion({ cachePath });
|
|
457
|
-
const hint = formatUpdateHint(current, latest);
|
|
458
|
-
if (hint) {
|
|
459
|
-
const contact = startupContactStore.getContact();
|
|
460
|
-
const recipient = contact?.chatId ?? contact?.userId;
|
|
461
|
-
if (recipient) {
|
|
462
|
-
await connector.sendWithRetry(recipient, { text: hint });
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
catch (err) {
|
|
467
|
-
// Non-fatal: startup check failure must not crash the bridge
|
|
468
|
-
logger.warn(`[startup] update check failed: ${err.message}`);
|
|
469
|
-
}
|
|
470
|
-
})();
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
// 上次进程可能死在 DSH turn 中途:清掉残留的待执行 prompt,并对仍在跑的
|
|
474
|
-
// turn 提示 owner(不自动 cancel——它可能是 Web UI 的 turn)。清理不依赖是否
|
|
475
|
-
// 已绑定;没有收件人时只是不发提示。
|
|
476
|
-
const dshConf = getAgentConfig(config, 'dsh');
|
|
477
|
-
if (dshConf) {
|
|
478
|
-
const contact = startupContactStore.getContact();
|
|
479
|
-
const recipient = contact?.chatId ?? contact?.userId;
|
|
480
|
-
void reconcileInflightDshRuns({
|
|
481
|
-
store: new DshInflightStore(dshInflightPath(configDir)),
|
|
482
|
-
client: new DshClient(dshConf.host ?? 'http://127.0.0.1:3080', {
|
|
483
|
-
dshHome: dshConf.dshHome,
|
|
484
|
-
token: dshConf.token,
|
|
485
|
-
credentialsFile: dshConf.credentialsFile,
|
|
486
|
-
// Host may be down: bound the probe instead of waiting the default 60s.
|
|
487
|
-
requestTimeoutMs: 3_000,
|
|
488
|
-
}),
|
|
489
|
-
...(recipient
|
|
490
|
-
? {
|
|
491
|
-
notify: async (text) => {
|
|
492
|
-
await connector.sendWithRetry(recipient, { text });
|
|
493
|
-
},
|
|
494
|
-
}
|
|
495
|
-
: {}),
|
|
496
|
-
}).catch((err) => {
|
|
497
|
-
logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
// Auto-restore: resume the persisted sessionId if available
|
|
501
|
-
const restoredContact = startupContactStore.getContact();
|
|
502
|
-
if (restoredContact?.userId) {
|
|
503
|
-
const restoredCwd = sessionStore.getCwd(restoredContact.userId);
|
|
504
|
-
if (restoredCwd) {
|
|
505
|
-
const sessionIdToResume = sessionStore.getSessionId(restoredContact.userId, config.defaultAgent);
|
|
506
|
-
if (sessionIdToResume) {
|
|
507
|
-
logger.info(`[startup] auto-resuming persisted session ${sessionIdToResume} in ${restoredCwd}`);
|
|
508
|
-
const startupReader = sessionReaderRegistry.get(config.defaultAgent);
|
|
509
|
-
sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
|
|
510
|
-
const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
|
|
511
|
-
// 该会话可能仍有在途 turn(上一个实例退出/崩溃后留在常驻 host 里)。
|
|
512
|
-
// 不取消、不接管,但必须在卡上说明,否则用户会把它当"卡住了"。
|
|
513
|
-
//
|
|
514
|
-
// 只有 DSH reader 的 isSessionActive() 是服务端的 running 事实。其他
|
|
515
|
-
// reader 的同名方法只表示"最近一段时间有写入",拿它当 turn 在跑会在
|
|
516
|
-
// 每次自动恢复时误报。
|
|
517
|
-
const stillRunning = config.defaultAgent === 'dsh'
|
|
518
|
-
? (() => {
|
|
519
|
-
try {
|
|
520
|
-
return startupReader.isSessionActive(sessionIdToResume, restoredCwd);
|
|
521
|
-
}
|
|
522
|
-
catch (err) {
|
|
523
|
-
getLogger().warn(`[startup] isSessionActive failed: ${err.message}`);
|
|
524
|
-
return false;
|
|
525
|
-
}
|
|
526
|
-
})()
|
|
527
|
-
: false;
|
|
528
|
-
if (stillRunning) {
|
|
529
|
-
getLogger().warn(`[startup] session ${sessionIdToResume} still has a running turn; auto-resume card is a snapshot`);
|
|
530
|
-
}
|
|
531
|
-
// 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
|
|
532
|
-
// 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
|
|
533
|
-
const usage = content.usage;
|
|
534
|
-
// Build auto-resume card and send to the user. Auto-resume is a
|
|
535
|
-
// non-critical startup nicety: a sendWithRetry failure here (e.g.
|
|
536
|
-
// transient feishu API error) must not crash the bridge via
|
|
537
|
-
// main().catch → process.exit(1). Logger is already initialized.
|
|
538
|
-
try {
|
|
539
|
-
await sendAutoResumeCard(connector, restoredContact, restoredCwd, { sessionId: sessionIdToResume, summary: '' }, {
|
|
540
|
-
events: content.events,
|
|
541
|
-
usage,
|
|
542
|
-
aiTitle: content.aiTitle,
|
|
543
|
-
recap: content.recap,
|
|
544
|
-
displayTitle: content.displayTitle,
|
|
545
|
-
agentKind: config.defaultAgent,
|
|
546
|
-
},
|
|
547
|
-
// Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
|
|
548
|
-
bridge.hasRunCompact(restoredCwd, config.defaultAgent), stillRunning);
|
|
549
|
-
}
|
|
550
|
-
catch (err) {
|
|
551
|
-
getLogger().warn('[startup] sendAutoResumeCard failed:', err);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
657
|
+
void checkUpdateOnStartupNotify(app);
|
|
554
658
|
}
|
|
555
659
|
}
|
|
660
|
+
reconcileDshInflightOnStartup(app);
|
|
661
|
+
await restoreLastSession(app);
|
|
556
662
|
logger.info('lark-remote is running, press Ctrl+C to exit');
|
|
557
663
|
}
|
|
558
664
|
main().catch((err) => {
|