lark-remote 0.1.4 → 0.1.6
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 +38 -0
- package/README.en.md +0 -1
- package/README.md +0 -1
- package/dist/bridge/approval-coordinator.d.ts +108 -0
- package/dist/bridge/approval-coordinator.js +258 -0
- package/dist/bridge/index.d.ts +62 -0
- package/dist/bridge/index.js +423 -20
- package/dist/bridge/queue-manager.d.ts +9 -0
- package/dist/bridge/queue-manager.js +19 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +247 -0
- package/dist/card/card-shared.d.ts +14 -0
- package/dist/card/card-shared.js +28 -0
- package/dist/card/run-renderer.js +78 -7
- package/dist/card/run-state.d.ts +25 -3
- package/dist/card/run-state.js +210 -2
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +4 -0
- package/dist/config/index.d.ts +43 -0
- package/dist/config/index.js +25 -1
- package/dist/connector/index.d.ts +5 -0
- package/dist/connector/index.js +32 -0
- package/dist/index.js +95 -23
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +49 -0
- package/dist/router/config/common/render.js +4 -0
- package/dist/router/config/index.d.ts +0 -10
- package/dist/router/config/index.js +0 -17
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/index.d.ts +61 -1
- package/dist/router/index.js +323 -48
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +5 -2
- package/dist/runner/codex/app-server/client.d.ts +82 -0
- package/dist/runner/codex/app-server/client.js +234 -0
- package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
- package/dist/runner/codex/app-server/connection-manager.js +170 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
- package/dist/runner/codex/app-server/protocol-types.js +67 -0
- package/dist/runner/codex/app-server/runner.d.ts +121 -0
- package/dist/runner/codex/app-server/runner.js +554 -0
- package/dist/runner/codex/app-server/translator.d.ts +109 -0
- package/dist/runner/codex/app-server/translator.js +734 -0
- package/dist/runner/codex/app-server/transport.d.ts +49 -0
- package/dist/runner/codex/app-server/transport.js +231 -0
- package/dist/runner/codex/argv.d.ts +1 -1
- package/dist/runner/codex/argv.js +1 -1
- package/dist/runner/codex/factory.d.ts +29 -0
- package/dist/runner/codex/factory.js +39 -0
- package/dist/runner/codex/index.d.ts +2 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/runner.d.ts +3 -0
- package/dist/runner/codex/runner.js +5 -0
- package/dist/runner/common/runner-utils.d.ts +12 -0
- package/dist/runner/common/runner-utils.js +21 -0
- package/dist/runner/common/spawning-runner.js +9 -1
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +1 -1
- package/dist/runner/types.d.ts +123 -2
- package/dist/session/claude/session-index.d.ts +93 -0
- package/dist/session/claude/session-index.js +374 -0
- package/dist/session/claude/sessions.d.ts +0 -18
- package/dist/session/claude/sessions.js +102 -319
- package/dist/session/codex/rollout-reader.js +46 -3
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +11 -0
- package/dist/update/install.d.ts +29 -0
- package/dist/update/install.js +93 -0
- package/dist/update/startup-hint.d.ts +5 -0
- package/dist/update/startup-hint.js +11 -0
- package/dist/update/version-check.d.ts +31 -0
- package/dist/update/version-check.js +149 -0
- package/package.json +1 -14
package/dist/router/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
|
-
import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgentKey, assertSafeKeyPart, } from '../config/index.js';
|
|
5
|
+
import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, getAgentConfig, mapAgentKey, assertSafeKeyPart, } from '../config/index.js';
|
|
6
6
|
import { syncAgentChoices } from '../runner/index.js';
|
|
7
7
|
import { WorkspaceStore } from '../workspace/index.js';
|
|
8
8
|
import { OrderStore } from '../order/index.js';
|
|
@@ -11,8 +11,9 @@ import { activeRunUsage, formatUsageStats } from './utils.js';
|
|
|
11
11
|
import { markdownDiv, sessionEventPanel } from './card-helpers.js';
|
|
12
12
|
import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
|
|
13
13
|
import { atomicWrite } from '../persistence/atomic-write.js';
|
|
14
|
+
import { checkLatestVersion as defaultCheckLatestVersion, isNewer as defaultIsNewer, runInstallLatest as defaultRunInstallLatest, } from '../update/index.js';
|
|
14
15
|
/** Config card builder - delegates to per-agent builders */
|
|
15
|
-
import { getConfigBuilder, listRegisteredAgents
|
|
16
|
+
import { getConfigBuilder, listRegisteredAgents } from './config/index.js';
|
|
16
17
|
import { probeAllAgents, getCachedAvailability } from '../runner/probe.js';
|
|
17
18
|
import { buildConfigCardFromTabs } from './config/common/render.js';
|
|
18
19
|
/** Max events to read for auto-resume and /resume cards.
|
|
@@ -44,7 +45,7 @@ const RESUME_CONTENT_PREFETCH = 5;
|
|
|
44
45
|
* `New Session`、空串),都不是真实输入,不得渲染成行标题。
|
|
45
46
|
*/
|
|
46
47
|
const RESUME_SUMMARY_PLACEHOLDERS = new Set(['', '(no user message)', '(无摘要)', 'New Session']);
|
|
47
|
-
import { newSessionButton, agentDisplayName } from '../card/card-shared.js';
|
|
48
|
+
import { newSessionButton, resumeCompactButton, agentDisplayName } from '../card/card-shared.js';
|
|
48
49
|
/**
|
|
49
50
|
* Card-action commands that must run with enqueue({ immediate: true }).
|
|
50
51
|
* These bypass the serial queue and execute immediately.
|
|
@@ -95,7 +96,12 @@ export function isImmediateAction(cmd) {
|
|
|
95
96
|
cmd === 'config.toggle' ||
|
|
96
97
|
cmd === 'config.set' ||
|
|
97
98
|
cmd === 'config.input' ||
|
|
98
|
-
cmd === 'config.save'
|
|
99
|
+
cmd === 'config.save' ||
|
|
100
|
+
// 审批响应/权限切换必须即时触达在途 run(同 stop 类控制动作)。若走串行
|
|
101
|
+
// 队列会排在等待审批的 run 之后形成死锁:run 不结束审批不执行,run 结束
|
|
102
|
+
// coordinator 已删响应空转(线上复现:approval.respond 排队卡、审批永不生效)。
|
|
103
|
+
cmd === 'approval.respond' ||
|
|
104
|
+
cmd === 'approval.toggle');
|
|
99
105
|
}
|
|
100
106
|
export class CommandRouter {
|
|
101
107
|
/** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
|
|
@@ -114,6 +120,12 @@ export class CommandRouter {
|
|
|
114
120
|
*/
|
|
115
121
|
restartSpawner;
|
|
116
122
|
idleTimeoutMs;
|
|
123
|
+
/** Dev mode flag: --dev means the bridge was started from source (bun src/index.ts). */
|
|
124
|
+
devMode;
|
|
125
|
+
/** Cache file for version checks (<configDir>/update-cache.json). */
|
|
126
|
+
updateCachePath;
|
|
127
|
+
/** Injected update functions (for testability; defaults to real implementation). */
|
|
128
|
+
updateFns;
|
|
117
129
|
pendingConfig = null; // /config 卡片编辑暂存区
|
|
118
130
|
/**
|
|
119
131
|
* Monotonic counter minting unique internal keys for order.exec enqueue
|
|
@@ -161,6 +173,13 @@ export class CommandRouter {
|
|
|
161
173
|
this.bridge.setIdleTimeout(this.idleTimeoutMs);
|
|
162
174
|
}
|
|
163
175
|
this.sessionReaderRegistry = opts.sessionReaderRegistry;
|
|
176
|
+
this.devMode = opts.devMode ?? false;
|
|
177
|
+
this.updateCachePath = opts.updateCachePath;
|
|
178
|
+
this.updateFns = {
|
|
179
|
+
checkLatestVersion: opts.updateFns?.checkLatestVersion ?? defaultCheckLatestVersion,
|
|
180
|
+
isNewer: opts.updateFns?.isNewer ?? defaultIsNewer,
|
|
181
|
+
runInstallLatest: opts.updateFns?.runInstallLatest ?? defaultRunInstallLatest,
|
|
182
|
+
};
|
|
164
183
|
}
|
|
165
184
|
/**
|
|
166
185
|
* Resolve the active session reader for the current defaultAgent.
|
|
@@ -223,12 +242,12 @@ export class CommandRouter {
|
|
|
223
242
|
return;
|
|
224
243
|
case 'ls.page':
|
|
225
244
|
return this.handleLsPage(value, ctx);
|
|
245
|
+
case 'ws.page':
|
|
246
|
+
return this.handleWsPage(value, ctx);
|
|
226
247
|
case 'ws.use':
|
|
227
248
|
return await this.handleWsUse(value, ctx);
|
|
228
249
|
case 'ws.sort':
|
|
229
250
|
return await this.handleWsSort(value, ctx);
|
|
230
|
-
case 'ws.page':
|
|
231
|
-
return await this.handleWsPage(value, ctx);
|
|
232
251
|
case 'ws.remove':
|
|
233
252
|
return await this.handleWsRemove(value, ctx);
|
|
234
253
|
case 'resume.use': {
|
|
@@ -305,6 +324,15 @@ export class CommandRouter {
|
|
|
305
324
|
// 调 updateCardInPlace → Feishu API 乱序到达导致 toggle 卡死。
|
|
306
325
|
// 2026-07-18: 返回 enqueueConfigAction 的结果以支持 toast 响应
|
|
307
326
|
return this.enqueueConfigAction(value, ctx);
|
|
327
|
+
case 'approval.respond':
|
|
328
|
+
case 'approval.toggle':
|
|
329
|
+
return this.handleApprovalAction(value, ctx);
|
|
330
|
+
case 'codex.compact':
|
|
331
|
+
await this.bridge.handleCodexCompact(value, ctx);
|
|
332
|
+
return;
|
|
333
|
+
case 'resume.compact':
|
|
334
|
+
await this.bridge.handleResumeCompact(value, ctx);
|
|
335
|
+
return;
|
|
308
336
|
default: {
|
|
309
337
|
// Help card buttons: help.<cmd> → execute /<cmd>
|
|
310
338
|
if (value.cmd.startsWith('help.')) {
|
|
@@ -322,7 +350,7 @@ export class CommandRouter {
|
|
|
322
350
|
}
|
|
323
351
|
return;
|
|
324
352
|
}
|
|
325
|
-
//
|
|
353
|
+
// Design constraint: miss paths must reply via bridge.sendResult —
|
|
326
354
|
// don't silently swallow unknown card actions.
|
|
327
355
|
getLogger().warn(`[router] unknown card action: ${value.cmd}`);
|
|
328
356
|
await this.bridge.sendResult({ text: `⚠️ 未知的卡片操作: ${value.cmd}` }, ctx);
|
|
@@ -330,6 +358,57 @@ export class CommandRouter {
|
|
|
330
358
|
}
|
|
331
359
|
}
|
|
332
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Handle approval-related card actions.
|
|
363
|
+
* Routes to the appropriate bridge method based on value.cmd.
|
|
364
|
+
* Returns a toast response for immediate user feedback.
|
|
365
|
+
*/
|
|
366
|
+
async handleApprovalAction(value, _ctx) {
|
|
367
|
+
const { runId, requestId, decision, scope, nonce, permId } = value;
|
|
368
|
+
if (value.cmd === 'approval.respond') {
|
|
369
|
+
if (!runId || requestId === undefined || !decision || !nonce) {
|
|
370
|
+
return { toast: { type: 'error', content: '缺少审批响应参数' } };
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await this.bridge.handleApprovalRespond({
|
|
374
|
+
runId,
|
|
375
|
+
requestId,
|
|
376
|
+
decision,
|
|
377
|
+
scope,
|
|
378
|
+
nonce,
|
|
379
|
+
});
|
|
380
|
+
return { toast: { type: 'success', content: '审批已提交' } };
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
384
|
+
// 审批已过期:给用户明确反馈,不静默不误导(2026-08-12 事故:点了允许无任何反馈)。
|
|
385
|
+
const content = /state=expired/.test(msg)
|
|
386
|
+
? '⏰ 审批已过期,无法响应'
|
|
387
|
+
: `审批响应失败:${msg}`;
|
|
388
|
+
return { toast: { type: 'error', content } };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (value.cmd === 'approval.toggle') {
|
|
392
|
+
if (!runId || requestId === undefined || !permId || !nonce) {
|
|
393
|
+
return { toast: { type: 'error', content: '缺少权限切换参数' } };
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
await this.bridge.handleApprovalToggle({
|
|
397
|
+
runId,
|
|
398
|
+
requestId,
|
|
399
|
+
permId,
|
|
400
|
+
nonce,
|
|
401
|
+
selected: value.selected ?? true,
|
|
402
|
+
});
|
|
403
|
+
return { toast: { type: 'info', content: '已切换' } };
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
407
|
+
return { toast: { type: 'error', content: `权限切换失败:${msg}` } };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return { toast: { type: 'error', content: '未知的审批操作' } };
|
|
411
|
+
}
|
|
333
412
|
/**
|
|
334
413
|
* Handle queue.cancel: remove this message from the queue.
|
|
335
414
|
*/
|
|
@@ -970,9 +1049,19 @@ export class CommandRouter {
|
|
|
970
1049
|
if (useResult.text && !useResult.text.includes('已切换')) {
|
|
971
1050
|
return { toast: { type: 'error', content: useResult.text } };
|
|
972
1051
|
}
|
|
973
|
-
// Success:
|
|
974
|
-
|
|
975
|
-
|
|
1052
|
+
// Success: send auto-resume card, or a persistent text confirmation when
|
|
1053
|
+
// there is no history session. Toast alone is insufficient: for this
|
|
1054
|
+
// immediate action the callback response is swallowed by enqueueImmediate
|
|
1055
|
+
// (fire-and-forget), and even if delivered it is transient — the user gets
|
|
1056
|
+
// no perceivable feedback on the switch. Same rationale as config.save
|
|
1057
|
+
// agent-switch notice: persistent message first, toast only as a
|
|
1058
|
+
// fallback if sendResult fails.
|
|
1059
|
+
let fallbackToast;
|
|
1060
|
+
if (useResult.card || useResult.text) {
|
|
1061
|
+
const sent = await this.bridge.sendResult(useResult, ctx);
|
|
1062
|
+
if (!sent && useResult.text) {
|
|
1063
|
+
fallbackToast = useResult.text;
|
|
1064
|
+
}
|
|
976
1065
|
}
|
|
977
1066
|
// Refresh the /ws list card in place so "recent" sort is immediately visible
|
|
978
1067
|
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
@@ -980,11 +1069,11 @@ export class CommandRouter {
|
|
|
980
1069
|
if (refreshed.card) {
|
|
981
1070
|
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
982
1071
|
}
|
|
983
|
-
// Toast feedback (suppress text when
|
|
1072
|
+
// Toast feedback (suppress text when a persistent message was already sent)
|
|
984
1073
|
return {
|
|
985
1074
|
toast: {
|
|
986
1075
|
type: 'success',
|
|
987
|
-
content:
|
|
1076
|
+
content: fallbackToast ?? '',
|
|
988
1077
|
},
|
|
989
1078
|
};
|
|
990
1079
|
}
|
|
@@ -1191,7 +1280,7 @@ export class CommandRouter {
|
|
|
1191
1280
|
this.config = setConfigValues(this.configPath, this.config, updates);
|
|
1192
1281
|
// P1-6:运行时传播(idleTimeout / clearRunners / syncAgentChoices /
|
|
1193
1282
|
// defaultAgent 切换的 session 处理)抽到共享方法,与文本直写路径共用。
|
|
1194
|
-
const
|
|
1283
|
+
const switchResult = this.propagateConfigSave(oldDefaultAgent, updates, ctx);
|
|
1195
1284
|
this.pendingConfig = null;
|
|
1196
1285
|
// 原地更新卡片:保存后卡片刷新为已保存状态(2026-07-04)
|
|
1197
1286
|
// 2026-08-03: 卡片刷新失败不吞切换通知——保存已成功,仅记录日志,
|
|
@@ -1202,14 +1291,15 @@ export class CommandRouter {
|
|
|
1202
1291
|
catch (refreshErr) {
|
|
1203
1292
|
getLogger().warn(`[router] config.save 卡片刷新失败(保存已成功): ${refreshErr.message}`);
|
|
1204
1293
|
}
|
|
1205
|
-
// 2026-08-
|
|
1206
|
-
//
|
|
1207
|
-
// toast
|
|
1208
|
-
if (
|
|
1209
|
-
const
|
|
1294
|
+
// 2026-08-13: agent 切换时发送 Resume 卡片(替代纯文本通知),
|
|
1295
|
+
// 让用户直观看到新 agent 的会话状态和历史。
|
|
1296
|
+
// 发送失败(resolve false)时兜底回退 toast 即时反馈。
|
|
1297
|
+
if (switchResult.notice) {
|
|
1298
|
+
const switchCard = this.buildConfigSwitchCard(switchResult, ctx);
|
|
1299
|
+
const sent = await this.bridge.sendResult(switchCard, ctx);
|
|
1210
1300
|
if (!sent) {
|
|
1211
1301
|
return {
|
|
1212
|
-
toast: { type: 'info', content:
|
|
1302
|
+
toast: { type: 'info', content: switchResult.notice },
|
|
1213
1303
|
};
|
|
1214
1304
|
}
|
|
1215
1305
|
}
|
|
@@ -1311,7 +1401,7 @@ export class CommandRouter {
|
|
|
1311
1401
|
*
|
|
1312
1402
|
* @param updates 以 config 路径形式给出的变更 key(卡片路径来自 diffConfig;
|
|
1313
1403
|
* 直写路径传 mapAgentKey(key),保证 pi./codex./opencode./kimi. 命中 agents.*)。
|
|
1314
|
-
* @returns
|
|
1404
|
+
* @returns 结构化切换结果(含 notice 文案 + 新 agent/sessionId 信息)
|
|
1315
1405
|
*/
|
|
1316
1406
|
propagateConfigSave(oldDefaultAgent, updates, ctx) {
|
|
1317
1407
|
this.bridge.setConfig(this.config);
|
|
@@ -1336,13 +1426,12 @@ export class CommandRouter {
|
|
|
1336
1426
|
// 同时清除新 agent 的 sessionId(等效于 /new 命令),
|
|
1337
1427
|
// 并支持 session 恢复 - 切换回来时如果条件满足则恢复之前的 session。
|
|
1338
1428
|
let switchNotice;
|
|
1429
|
+
let switchNewAgent;
|
|
1430
|
+
let switchSessionId;
|
|
1339
1431
|
if (hasDefaultAgentChange) {
|
|
1340
1432
|
const newAgent = this.config.defaultAgent;
|
|
1433
|
+
switchNewAgent = newAgent;
|
|
1341
1434
|
// Step 0: 在清空 old 之前计算「用户活动」基线差(arrival 基线):
|
|
1342
|
-
// arrivalSessions[old] 是用户最近一次经 config.save 切换「到达」该 agent
|
|
1343
|
-
// 时被赋予的 session id(清空到达 = '')。当前 session 与到达基线不一致
|
|
1344
|
-
// ⇒ 用户在 oldAgent 上产生过用户活动(发消息 /resume /new /cd 等,均
|
|
1345
|
-
// 不更新 arrival),离开时恢复必须被阻断。
|
|
1346
1435
|
const oldSessionId = this.sessionStore.getSessionId(ctx.userId, oldDefaultAgent);
|
|
1347
1436
|
const oldArrivalSessionId = this.sessionStore.getArrivalSessionId(ctx.userId, oldDefaultAgent);
|
|
1348
1437
|
const userChangedOld = (oldSessionId ?? '') !== (oldArrivalSessionId ?? '');
|
|
@@ -1352,48 +1441,36 @@ export class CommandRouter {
|
|
|
1352
1441
|
}
|
|
1353
1442
|
// Step 2: 清除旧 agent 的 session(保存后清除,为新会话腾出空间)
|
|
1354
1443
|
this.sessionStore.clearSessionId(ctx.userId, oldDefaultAgent);
|
|
1355
|
-
// Step 2.5:
|
|
1356
|
-
// 显式改选了 new 的 session(/resume 不更新 arrival),切换时必须让选择存活。
|
|
1357
|
-
// 优先级:显式选择 > 停车恢复 > 清空;newSessionId 为空(防御)不触发本分支。
|
|
1444
|
+
// Step 2.5: 显式选择判定
|
|
1358
1445
|
const newSessionId = this.sessionStore.getSessionId(ctx.userId, newAgent);
|
|
1359
1446
|
const newArrivalSessionId = this.sessionStore.getArrivalSessionId(ctx.userId, newAgent);
|
|
1360
1447
|
const userSelectedNew = !!newSessionId && (newSessionId ?? '') !== (newArrivalSessionId ?? '');
|
|
1361
|
-
// Step 3:
|
|
1362
|
-
// 没有用户活动(session 未变于到达基线)。判断方法:arrival 基线差
|
|
1363
|
-
// userChangedOld 为 false 时,oldAgent 的 session 只可能是 config.save
|
|
1364
|
-
// 自动恢复/清空得到的,不构成用户活动,恢复不被阻断。
|
|
1448
|
+
// Step 3: 恢复判定
|
|
1365
1449
|
const previousSessionId = this.sessionStore.getPreviousSessionId(ctx.userId, newAgent);
|
|
1366
1450
|
const canRestore = !!previousSessionId && !userChangedOld;
|
|
1367
1451
|
if (userSelectedNew) {
|
|
1368
|
-
// 显式选择优先于停车恢复与用户活动阻断:保留 sessions[new](不动),更新
|
|
1369
|
-
// arrival 基线为所选 session;prev[new] 停车位原样保留(下次离开时会被
|
|
1370
|
-
// sessions[new] 覆盖停车)。
|
|
1371
1452
|
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, newSessionId);
|
|
1372
1453
|
const newAgentName = agentDisplayName(newAgent);
|
|
1373
1454
|
switchNotice = `已切换到 ${newAgentName},已使用所选 session,sessionId: ${newSessionId}`;
|
|
1455
|
+
switchSessionId = newSessionId;
|
|
1374
1456
|
}
|
|
1375
1457
|
else if (canRestore) {
|
|
1376
|
-
// 可以恢复之前的 session
|
|
1377
1458
|
this.sessionStore.setSessionId(ctx.userId, newAgent, previousSessionId);
|
|
1378
|
-
// 恢复分支消费 previousSessions,并记录新的到达基线(自动恢复的 session
|
|
1379
|
-
// 不构成用户活动,后续离开时 userChangedOld 仍为 false)
|
|
1380
1459
|
this.sessionStore.clearPreviousSessionId(ctx.userId, newAgent);
|
|
1381
1460
|
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, previousSessionId);
|
|
1382
1461
|
const newAgentName = agentDisplayName(newAgent);
|
|
1383
1462
|
switchNotice = `已切换到 ${newAgentName},将继续之前的 session,sessionId: ${previousSessionId}`;
|
|
1463
|
+
switchSessionId = previousSessionId;
|
|
1384
1464
|
}
|
|
1385
1465
|
else {
|
|
1386
|
-
// Step 4: 清除新 agent 的 session(等效于 /new)
|
|
1387
1466
|
this.sessionStore.clearSessionId(ctx.userId, newAgent, { clearSessionCwd: true });
|
|
1388
|
-
// 清空分支不清除 previousSessions[newAgent](停车语义:被拒绝的恢复
|
|
1389
|
-
// 机会保留,未来无用户活动的返回仍可恢复);记录清空到达基线
|
|
1390
1467
|
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, '');
|
|
1391
1468
|
const newAgentName = agentDisplayName(newAgent);
|
|
1392
1469
|
switchNotice = `已切换到 ${newAgentName},session 已清空,下次消息将启动新对话`;
|
|
1470
|
+
switchSessionId = '';
|
|
1393
1471
|
}
|
|
1394
1472
|
}
|
|
1395
1473
|
// 2026-07-13: 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
|
|
1396
|
-
// 检测是否有当前 agent 的配置变更
|
|
1397
1474
|
const currentAgent = this.config.defaultAgent;
|
|
1398
1475
|
const currentAgentConfigKey = `agents.${currentAgent}`;
|
|
1399
1476
|
const hasCurrentAgentUpdate = Object.keys(updates).some((k) => k === currentAgentConfigKey || k.startsWith(`${currentAgentConfigKey}.`));
|
|
@@ -1404,11 +1481,10 @@ export class CommandRouter {
|
|
|
1404
1481
|
this.bridge.setConfig(this.config);
|
|
1405
1482
|
}
|
|
1406
1483
|
// 2026-07-13: 任何 agent 配置变更或 defaultAgent 切换,都清除 runner 缓存
|
|
1407
|
-
// 否则 runner 仍使用旧配置值
|
|
1408
1484
|
if (hasDefaultAgentChange || hasAgentConfigChange) {
|
|
1409
1485
|
this.bridge.clearRunners();
|
|
1410
1486
|
}
|
|
1411
|
-
return switchNotice;
|
|
1487
|
+
return { notice: switchNotice, newAgent: switchNewAgent, sessionId: switchSessionId };
|
|
1412
1488
|
}
|
|
1413
1489
|
async executeCommand(message, ctx) {
|
|
1414
1490
|
const parts = message.slice(1).split(/\s+/);
|
|
@@ -1453,6 +1529,9 @@ export class CommandRouter {
|
|
|
1453
1529
|
case 'order':
|
|
1454
1530
|
case 'o':
|
|
1455
1531
|
return this.cmdOrder(args, ctx);
|
|
1532
|
+
case 'update':
|
|
1533
|
+
case 'u':
|
|
1534
|
+
return await this.cmdUpdate(args, ctx);
|
|
1456
1535
|
default:
|
|
1457
1536
|
return { text: `未知命令 /${cmd},输入 /help 查看可用命令` };
|
|
1458
1537
|
}
|
|
@@ -1472,6 +1551,7 @@ export class CommandRouter {
|
|
|
1472
1551
|
{ cmd: 'active', label: '/active', desc: '查看所有正在进行中的 session' },
|
|
1473
1552
|
{ cmd: 'exit', label: '/exit /e', desc: '退出 bridge' },
|
|
1474
1553
|
{ cmd: 'restart', label: '/restart', desc: '重启 bridge(新进程,config 不变)' },
|
|
1554
|
+
{ cmd: 'update', label: '/update /u', desc: '检查并升级到最新版本' },
|
|
1475
1555
|
{ cmd: 'status', label: '/status /s', desc: '显示当前状态' },
|
|
1476
1556
|
{ cmd: 'reconnect', label: '/reconnect', desc: '重连飞书' },
|
|
1477
1557
|
].sort((a, b) => a.label.length - b.label.length || a.cmd.localeCompare(b.cmd));
|
|
@@ -1658,6 +1738,47 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1658
1738
|
return { text: `重启失败:${err.message},旧进程仍在运行` };
|
|
1659
1739
|
}
|
|
1660
1740
|
}
|
|
1741
|
+
/**
|
|
1742
|
+
* /update: check for newer version and upgrade if available.
|
|
1743
|
+
* /update check: only check, don't upgrade.
|
|
1744
|
+
*
|
|
1745
|
+
* Flow: check version → if newer, install → report success (no auto-restart).
|
|
1746
|
+
*/
|
|
1747
|
+
async cmdUpdate(args, ctx) {
|
|
1748
|
+
const checkOnly = args[0] === 'check';
|
|
1749
|
+
// 1. Version query(/update check 始终绕过缓存,/update 用 TTL 缓存)
|
|
1750
|
+
let current;
|
|
1751
|
+
let latest;
|
|
1752
|
+
try {
|
|
1753
|
+
const result = await this.updateFns.checkLatestVersion({
|
|
1754
|
+
cachePath: this.updateCachePath,
|
|
1755
|
+
bypassCache: checkOnly,
|
|
1756
|
+
});
|
|
1757
|
+
current = result.current;
|
|
1758
|
+
latest = result.latest;
|
|
1759
|
+
}
|
|
1760
|
+
catch (err) {
|
|
1761
|
+
return { text: `❌ 版本检查失败: ${err.message}` };
|
|
1762
|
+
}
|
|
1763
|
+
if (!this.updateFns.isNewer(current, latest)) {
|
|
1764
|
+
return { text: `✅ 已是最新版本 ${current}` };
|
|
1765
|
+
}
|
|
1766
|
+
if (checkOnly) {
|
|
1767
|
+
return { text: `📦 有新版本 ${latest} 可用(当前 ${current}),发送 /update 升级` };
|
|
1768
|
+
}
|
|
1769
|
+
// 2. Dev mode guard
|
|
1770
|
+
if (this.devMode) {
|
|
1771
|
+
return { text: '⚠️ 开发模式不支持自更新,请 git pull 后重新构建' };
|
|
1772
|
+
}
|
|
1773
|
+
// 3. Install (send intermediate status before the blocking operation)
|
|
1774
|
+
await this.bridge.sendResult({ text: `⬆️ 正在升级 ${current} → ${latest} ...` }, ctx);
|
|
1775
|
+
const installResult = await this.updateFns.runInstallLatest();
|
|
1776
|
+
if (!installResult.success) {
|
|
1777
|
+
return { text: `❌ 升级失败: ${installResult.error}` };
|
|
1778
|
+
}
|
|
1779
|
+
// 4. Report success (no auto-restart — user decides when to /restart)
|
|
1780
|
+
return { text: `✅ 升级成功 (${current} → ${latest}),发送 /restart 重启后生效` };
|
|
1781
|
+
}
|
|
1661
1782
|
cmdNew(ctx) {
|
|
1662
1783
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
1663
1784
|
// 设置工作目录",但用户的 workspace 还在。2026-06-21 复盘:用户
|
|
@@ -1816,6 +1937,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1816
1937
|
],
|
|
1817
1938
|
});
|
|
1818
1939
|
}
|
|
1940
|
+
// Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且会话未在跑。
|
|
1941
|
+
// 在途 run 会占住 app-server runner,压缩必然失败,此时不渲染。
|
|
1942
|
+
if (!isActive && this.canCompactSession(this.config.defaultAgent)) {
|
|
1943
|
+
actions.push(resumeCompactButton(session.sessionId, this.config.defaultAgent));
|
|
1944
|
+
}
|
|
1819
1945
|
// New session button always shown (use shared function for consistency)
|
|
1820
1946
|
actions.push(newSessionButton());
|
|
1821
1947
|
// Use column_set+column for 2.0 (action tag not supported in 2.0)
|
|
@@ -1840,6 +1966,146 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1840
1966
|
};
|
|
1841
1967
|
return { card };
|
|
1842
1968
|
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Build card for config.save agent switch.
|
|
1971
|
+
* When the new agent has a restored session, renders a Resume card (mirrors
|
|
1972
|
+
* buildAutoResumeCard) with session history + stop/new buttons.
|
|
1973
|
+
* When the session was cleared, renders a compact notice card instead.
|
|
1974
|
+
*/
|
|
1975
|
+
buildConfigSwitchCard(switchResult, ctx) {
|
|
1976
|
+
const newAgent = switchResult.newAgent;
|
|
1977
|
+
const agentName = agentDisplayName(newAgent);
|
|
1978
|
+
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
1979
|
+
const sessionId = switchResult.sessionId;
|
|
1980
|
+
// No cwd or no session → compact notice card
|
|
1981
|
+
if (!cwd || !sessionId) {
|
|
1982
|
+
const elements = [markdownDiv(switchResult.notice)];
|
|
1983
|
+
// New session button
|
|
1984
|
+
elements.push({
|
|
1985
|
+
tag: 'column_set',
|
|
1986
|
+
columns: [
|
|
1987
|
+
{
|
|
1988
|
+
tag: 'column',
|
|
1989
|
+
width: 'auto',
|
|
1990
|
+
elements: [newSessionButton()],
|
|
1991
|
+
},
|
|
1992
|
+
],
|
|
1993
|
+
});
|
|
1994
|
+
return {
|
|
1995
|
+
card: {
|
|
1996
|
+
schema: '2.0',
|
|
1997
|
+
config: { wide_screen_mode: true },
|
|
1998
|
+
header: {
|
|
1999
|
+
title: { tag: 'plain_text', content: `🔀 切换到 ${agentName}` },
|
|
2000
|
+
},
|
|
2001
|
+
body: { elements },
|
|
2002
|
+
},
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
// Has session → Resume card (mirrors buildAutoResumeCard)
|
|
2006
|
+
// Guard: if the session reader for the new agent is not available (e.g. agent
|
|
2007
|
+
// not registered), fall back to compact notice card to avoid NPE.
|
|
2008
|
+
const readerForNewAgent = newAgent === this.config.defaultAgent
|
|
2009
|
+
? this.sessionReader
|
|
2010
|
+
: this.sessionReaderRegistry.get(newAgent);
|
|
2011
|
+
if (!readerForNewAgent) {
|
|
2012
|
+
// Fallback: compact notice card with the notice text
|
|
2013
|
+
const elements = [markdownDiv(switchResult.notice)];
|
|
2014
|
+
elements.push({
|
|
2015
|
+
tag: 'column_set',
|
|
2016
|
+
columns: [
|
|
2017
|
+
{
|
|
2018
|
+
tag: 'column',
|
|
2019
|
+
width: 'auto',
|
|
2020
|
+
elements: [newSessionButton()],
|
|
2021
|
+
},
|
|
2022
|
+
],
|
|
2023
|
+
});
|
|
2024
|
+
return {
|
|
2025
|
+
card: {
|
|
2026
|
+
schema: '2.0',
|
|
2027
|
+
config: { wide_screen_mode: true },
|
|
2028
|
+
header: { title: { tag: 'plain_text', content: `🔀 切换到 ${agentName}` } },
|
|
2029
|
+
body: { elements },
|
|
2030
|
+
},
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionId, cwd, {
|
|
2034
|
+
agentKind: newAgent,
|
|
2035
|
+
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2036
|
+
});
|
|
2037
|
+
let header = `📂 \`${cwd}\`\n已恢复会话: **${sessionId}**`;
|
|
2038
|
+
const sections = [];
|
|
2039
|
+
if (displayTitle) {
|
|
2040
|
+
const label = aiTitle ? 'AI 标题' : '最近输入';
|
|
2041
|
+
sections.push(`🏷️ **${label}**\n${displayTitle}`);
|
|
2042
|
+
}
|
|
2043
|
+
if (recap) {
|
|
2044
|
+
const recapPreview = recap.length > 200 ? recap.slice(0, 197) + '...' : recap;
|
|
2045
|
+
sections.push(`📝 **Recap**\n${recapPreview}`);
|
|
2046
|
+
}
|
|
2047
|
+
if (sections.length > 0) {
|
|
2048
|
+
header += '\n\n' + sections.join('\n\n──\n\n');
|
|
2049
|
+
}
|
|
2050
|
+
const elements = [markdownDiv(header), { tag: 'hr' }];
|
|
2051
|
+
if (content.length === 0) {
|
|
2052
|
+
elements.push(markdownDiv('_该会话暂无新消息可显示(最后一条为用户输入)_'));
|
|
2053
|
+
}
|
|
2054
|
+
else {
|
|
2055
|
+
content.forEach((ev, i) => {
|
|
2056
|
+
elements.push(sessionEventPanel(ev, i, content.length, 2, newAgent));
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
if (usage) {
|
|
2060
|
+
const usageStr = formatUsageStats(usage, { showResult: true, result: 'success' });
|
|
2061
|
+
elements.push(markdownDiv(usageStr));
|
|
2062
|
+
}
|
|
2063
|
+
if (elements.length > 0 && elements[elements.length - 1].tag === 'hr') {
|
|
2064
|
+
elements.pop();
|
|
2065
|
+
}
|
|
2066
|
+
// Action buttons
|
|
2067
|
+
const actions = [];
|
|
2068
|
+
if (isActive) {
|
|
2069
|
+
actions.push({
|
|
2070
|
+
tag: 'button',
|
|
2071
|
+
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
2072
|
+
type: 'danger',
|
|
2073
|
+
behaviors: [{ type: 'callback', value: { cmd: 'stop', runId: activeRunRunId, cwd } }],
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
actions.push(newSessionButton());
|
|
2077
|
+
elements.push({
|
|
2078
|
+
tag: 'column_set',
|
|
2079
|
+
columns: actions.map((btn) => ({
|
|
2080
|
+
tag: 'column',
|
|
2081
|
+
width: 'auto',
|
|
2082
|
+
elements: [btn],
|
|
2083
|
+
})),
|
|
2084
|
+
});
|
|
2085
|
+
return {
|
|
2086
|
+
card: {
|
|
2087
|
+
schema: '2.0',
|
|
2088
|
+
config: { wide_screen_mode: true },
|
|
2089
|
+
header: {
|
|
2090
|
+
title: {
|
|
2091
|
+
tag: 'plain_text',
|
|
2092
|
+
content: `${isActive ? '⏳ 切换 Agent(完成中)' : '🔀 切换 Agent'} · ${agentName}`,
|
|
2093
|
+
},
|
|
2094
|
+
},
|
|
2095
|
+
body: { elements },
|
|
2096
|
+
},
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Whether a session of the given agent kind can be compacted from a resume
|
|
2101
|
+
* card. Compact (thread/compact/start) is only implemented by the codex
|
|
2102
|
+
* app-server runner (`runCompact`), so the button is gated on
|
|
2103
|
+
* `serviceMode === 'app-server'` — exec mode spawns a fresh CLI per message
|
|
2104
|
+
* and has no persistent session to compact.
|
|
2105
|
+
*/
|
|
2106
|
+
canCompactSession(agentKind) {
|
|
2107
|
+
return (agentKind === 'codex' && getAgentConfig(this.config, 'codex')?.serviceMode === 'app-server');
|
|
2108
|
+
}
|
|
1843
2109
|
static LS_PAGE_SIZE = 30;
|
|
1844
2110
|
cmdLs(args, ctx, offset = 0) {
|
|
1845
2111
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
@@ -2361,7 +2627,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2361
2627
|
}
|
|
2362
2628
|
const card = {
|
|
2363
2629
|
schema: '2.0',
|
|
2364
|
-
config: { wide_screen_mode: true },
|
|
2630
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
2365
2631
|
header: { title: { tag: 'plain_text', content: 'Workspaces' } },
|
|
2366
2632
|
body: { elements: bodyElements },
|
|
2367
2633
|
};
|
|
@@ -2476,6 +2742,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2476
2742
|
behaviors: [{ type: 'callback', value: { cmd: 'stop', runId: activeRunRunId, cwd } }],
|
|
2477
2743
|
});
|
|
2478
2744
|
}
|
|
2745
|
+
// Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且会话未在跑。
|
|
2746
|
+
// agentKind 来自 /resume [agent] <id> / resume.use 卡片值,非 defaultAgent。
|
|
2747
|
+
if (!isActive && this.canCompactSession(agentKind)) {
|
|
2748
|
+
buttons.push(resumeCompactButton(sessionIdArg, agentKind));
|
|
2749
|
+
}
|
|
2479
2750
|
if (buttons.length > 0) {
|
|
2480
2751
|
elements.push({
|
|
2481
2752
|
tag: 'column_set',
|
|
@@ -2906,8 +3177,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2906
3177
|
// clearRunners / syncAgentChoices / defaultAgent 切换)。key 经 mapAgentKey
|
|
2907
3178
|
// 归一化,pi./codex./opencode./kimi. 命中 agents.* 判定;claude. 与
|
|
2908
3179
|
// defaultAgent 不再漏判。
|
|
2909
|
-
this.propagateConfigSave(oldDefaultAgent, { [mapAgentKey(key)]: value }, ctx);
|
|
3180
|
+
const switchResult = this.propagateConfigSave(oldDefaultAgent, { [mapAgentKey(key)]: value }, ctx);
|
|
2910
3181
|
this.pendingConfig = null; // 命令式写盘后清空暂存,避免不一致
|
|
3182
|
+
// 2026-08-13: agent 切换时额外发送 Resume 卡片
|
|
3183
|
+
if (switchResult.notice) {
|
|
3184
|
+
const switchCard = this.buildConfigSwitchCard(switchResult, ctx);
|
|
3185
|
+
// 文本路径返回 config card,Resume card 通过 bridge 异步发送
|
|
3186
|
+
void this.bridge.sendResult(switchCard, ctx);
|
|
3187
|
+
}
|
|
2911
3188
|
const card = this.buildConfigCard();
|
|
2912
3189
|
if (discarded > 0 && card.card && typeof card.card === 'object') {
|
|
2913
3190
|
const body = card.card.body;
|
|
@@ -2942,9 +3219,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2942
3219
|
// Direct inline construction (not via ConfigField) so that text (display label)
|
|
2943
3220
|
// and value (agentKind for config storage) can differ — uninstalled agents get
|
|
2944
3221
|
// a "⚠️ (未安装)" suffix in the label while the value stays clean.
|
|
2945
|
-
|
|
2946
|
-
// order (claude → codex → pi → opencode → kimi).
|
|
2947
|
-
const allAgents = sortAgentsByAvailability(listRegisteredAgents(), getCachedAvailability);
|
|
3222
|
+
const allAgents = listRegisteredAgents();
|
|
2948
3223
|
const agentOptions = allAgents.map((kind) => {
|
|
2949
3224
|
const available = getCachedAvailability(kind);
|
|
2950
3225
|
const label = available === false ? `${agentDisplayName(kind)} ⚠️ (未安装)` : agentDisplayName(kind);
|
package/dist/router/utils.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export declare function formatUsageStats(usage?: {
|
|
|
22
22
|
contextLength?: number;
|
|
23
23
|
contextLimit?: number;
|
|
24
24
|
compactCount?: number;
|
|
25
|
+
/** 压缩前上下文水位;与 contextLength(压缩后)成对出现在 Compact 卡上。 */
|
|
26
|
+
compactPreContextLength?: number;
|
|
25
27
|
cacheReadTokens?: number;
|
|
26
28
|
cacheCreationTokens?: number;
|
|
27
29
|
/** Agent-declared total; when present the display uses max(total, sum of parts). */
|
package/dist/router/utils.js
CHANGED
|
@@ -46,13 +46,16 @@ export function formatUsageStats(usage, options) {
|
|
|
46
46
|
if (options?.showResult && options?.result) {
|
|
47
47
|
lines.push(`结果 - ${options.result}`);
|
|
48
48
|
}
|
|
49
|
-
// 2. Context
|
|
49
|
+
// 2. Context 长度(compact 卡同时展示压缩前水位)
|
|
50
50
|
if (usage?.contextLength !== undefined) {
|
|
51
51
|
const ctxLimit = usage.contextLimit;
|
|
52
52
|
const ctxPercent = ctxLimit !== undefined && ctxLimit > 0
|
|
53
53
|
? ` (${Math.round((usage.contextLength / ctxLimit) * 100)}%)`
|
|
54
54
|
: '';
|
|
55
|
-
|
|
55
|
+
const preCompact = usage.compactPreContextLength !== undefined
|
|
56
|
+
? `(压缩前 ${formatTokenK(usage.compactPreContextLength)})`
|
|
57
|
+
: '';
|
|
58
|
+
lines.push(`Context - ${formatTokenK(usage.contextLength)}${ctxPercent}${preCompact}`);
|
|
56
59
|
}
|
|
57
60
|
// 3. Compact 次数
|
|
58
61
|
if (usage?.compactCount) {
|