lark-remote 0.1.3 → 0.1.5
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 +86 -0
- package/README.en.md +4 -1
- package/README.md +4 -1
- package/dist/bridge/index.d.ts +5 -1
- package/dist/bridge/index.js +25 -1
- package/dist/card/run-renderer.js +1 -1
- package/dist/card/run-state.js +7 -0
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +5 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +2 -0
- package/dist/connector/index.js +20 -0
- package/dist/index.js +54 -2
- package/dist/router/index.d.ts +62 -1
- package/dist/router/index.js +466 -46
- package/dist/runner/codex/argv.d.ts +1 -1
- package/dist/runner/codex/argv.js +1 -1
- 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/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 +3 -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/dist/workspace/index.d.ts +19 -1
- package/dist/workspace/index.js +51 -5
- package/package.json +1 -14
package/dist/router/index.js
CHANGED
|
@@ -11,6 +11,7 @@ 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
16
|
import { getConfigBuilder, listRegisteredAgents } from './config/index.js';
|
|
16
17
|
import { probeAllAgents, getCachedAvailability } from '../runner/probe.js';
|
|
@@ -23,8 +24,19 @@ const AUTO_RESUME_MAX_EVENTS = 5;
|
|
|
23
24
|
const RESUME_PAGE_SIZE = 5;
|
|
24
25
|
/** /active 卡片每页显示的最大条目数(agent run + bash run 合计)。 */
|
|
25
26
|
const ACTIVE_PAGE_SIZE = 20;
|
|
26
|
-
/**
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* /order 列表页大小;指令超过此数量时显示分页导航栏。
|
|
29
|
+
* 2026-08-13 实测:每行 2 个元素(div + column_set,行间 hr),20 行 + 分页栏
|
|
30
|
+
* = 61 个 body 元素,触发飞书 ErrCode 11310 "element exceeds the limit"
|
|
31
|
+
* (21+ 条指令时第 1 页必炸)。改为 15 行:3*15 - 1 + 2 = 46 个元素,留足余量。
|
|
32
|
+
*/
|
|
33
|
+
const ORDER_PAGE_SIZE = 15;
|
|
34
|
+
/**
|
|
35
|
+
* /ws 列表页大小。从 15 降为 5:头部 2 + 5 行 × 3(div + column_set + hr)+
|
|
36
|
+
* 底栏排序 1 + 分页栏 2 ≈ 20 个 body 元素,远低于单卡 60 元素上限(ErrCode 11310),
|
|
37
|
+
* 留足余量。
|
|
38
|
+
*/
|
|
39
|
+
const WS_PAGE_SIZE = 5;
|
|
28
40
|
/** /resume 列表页内全量预取(readSessionContent)的行数上限;其余行用轻量 summary 兜底。 */
|
|
29
41
|
const RESUME_CONTENT_PREFETCH = 5;
|
|
30
42
|
/**
|
|
@@ -39,6 +51,7 @@ import { newSessionButton, agentDisplayName } from '../card/card-shared.js';
|
|
|
39
51
|
* These bypass the serial queue and execute immediately.
|
|
40
52
|
*
|
|
41
53
|
* ws.use — /ws list card "使用 <name>"
|
|
54
|
+
* ws.page — /ws list card "上一页/下一页"
|
|
42
55
|
* queue.immediate — queue card "立即执行" button (§9.6)
|
|
43
56
|
* queue.cancel — queue card "撤销" button
|
|
44
57
|
* queue.diagnose — queue card "诊断" button
|
|
@@ -47,6 +60,7 @@ import { newSessionButton, agentDisplayName } from '../card/card-shared.js';
|
|
|
47
60
|
* new-session — clear sessionId only
|
|
48
61
|
* stop — interrupt current run
|
|
49
62
|
* ls.file — send file only
|
|
63
|
+
* ws.page — paginate /ws list only
|
|
50
64
|
* ws.remove — delete workspace alias only
|
|
51
65
|
* resume.use — set sessionId + agent for correct reader routing
|
|
52
66
|
* help.* — read-only help commands
|
|
@@ -67,7 +81,9 @@ export function isImmediateAction(cmd) {
|
|
|
67
81
|
cmd === 'ls.page' ||
|
|
68
82
|
cmd === 'resume.page' || // control operation: paginate only, never spawns claude
|
|
69
83
|
cmd === 'active.page' || // control operation: paginate active card
|
|
84
|
+
cmd === 'ws.page' || // control operation: paginate /ws list only
|
|
70
85
|
cmd === 'ws.remove' ||
|
|
86
|
+
cmd === 'ws.sort' || // control operation: toggle sort mode only
|
|
71
87
|
cmd === 'resume.use' ||
|
|
72
88
|
cmd === 'ws.use' ||
|
|
73
89
|
cmd === 'queue.immediate' ||
|
|
@@ -99,6 +115,12 @@ export class CommandRouter {
|
|
|
99
115
|
*/
|
|
100
116
|
restartSpawner;
|
|
101
117
|
idleTimeoutMs;
|
|
118
|
+
/** Dev mode flag: --dev means the bridge was started from source (bun src/index.ts). */
|
|
119
|
+
devMode;
|
|
120
|
+
/** Cache file for version checks (<configDir>/update-cache.json). */
|
|
121
|
+
updateCachePath;
|
|
122
|
+
/** Injected update functions (for testability; defaults to real implementation). */
|
|
123
|
+
updateFns;
|
|
102
124
|
pendingConfig = null; // /config 卡片编辑暂存区
|
|
103
125
|
/**
|
|
104
126
|
* Monotonic counter minting unique internal keys for order.exec enqueue
|
|
@@ -108,6 +130,11 @@ export class CommandRouter {
|
|
|
108
130
|
* millisecond); the counter guarantees uniqueness regardless of timing.
|
|
109
131
|
*/
|
|
110
132
|
orderExecKeyCounter = 0;
|
|
133
|
+
/**
|
|
134
|
+
* Per-user sort preference for /ws list (memory-only, not persisted).
|
|
135
|
+
* Default 'recent' (most recently used first), can toggle to 'alpha'.
|
|
136
|
+
*/
|
|
137
|
+
wsSortPreference = new Map();
|
|
111
138
|
/**
|
|
112
139
|
* Session reader registry for multi-agent path. Resolves the right
|
|
113
140
|
* reader by `config.defaultAgent` (and per-agent override params).
|
|
@@ -141,6 +168,13 @@ export class CommandRouter {
|
|
|
141
168
|
this.bridge.setIdleTimeout(this.idleTimeoutMs);
|
|
142
169
|
}
|
|
143
170
|
this.sessionReaderRegistry = opts.sessionReaderRegistry;
|
|
171
|
+
this.devMode = opts.devMode ?? false;
|
|
172
|
+
this.updateCachePath = opts.updateCachePath;
|
|
173
|
+
this.updateFns = {
|
|
174
|
+
checkLatestVersion: opts.updateFns?.checkLatestVersion ?? defaultCheckLatestVersion,
|
|
175
|
+
isNewer: opts.updateFns?.isNewer ?? defaultIsNewer,
|
|
176
|
+
runInstallLatest: opts.updateFns?.runInstallLatest ?? defaultRunInstallLatest,
|
|
177
|
+
};
|
|
144
178
|
}
|
|
145
179
|
/**
|
|
146
180
|
* Resolve the active session reader for the current defaultAgent.
|
|
@@ -204,8 +238,11 @@ export class CommandRouter {
|
|
|
204
238
|
case 'ls.page':
|
|
205
239
|
return this.handleLsPage(value, ctx);
|
|
206
240
|
case 'ws.use':
|
|
207
|
-
await this.
|
|
208
|
-
|
|
241
|
+
return await this.handleWsUse(value, ctx);
|
|
242
|
+
case 'ws.sort':
|
|
243
|
+
return await this.handleWsSort(value, ctx);
|
|
244
|
+
case 'ws.page':
|
|
245
|
+
return await this.handleWsPage(value, ctx);
|
|
209
246
|
case 'ws.remove':
|
|
210
247
|
return await this.handleWsRemove(value, ctx);
|
|
211
248
|
case 'resume.use': {
|
|
@@ -299,7 +336,7 @@ export class CommandRouter {
|
|
|
299
336
|
}
|
|
300
337
|
return;
|
|
301
338
|
}
|
|
302
|
-
//
|
|
339
|
+
// Design constraint: miss paths must reply via bridge.sendResult —
|
|
303
340
|
// don't silently swallow unknown card actions.
|
|
304
341
|
getLogger().warn(`[router] unknown card action: ${value.cmd}`);
|
|
305
342
|
await this.bridge.sendResult({ text: `⚠️ 未知的卡片操作: ${value.cmd}` }, ctx);
|
|
@@ -932,6 +969,39 @@ export class CommandRouter {
|
|
|
932
969
|
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
933
970
|
return { toast: { type: 'success', content: '' } };
|
|
934
971
|
}
|
|
972
|
+
/**
|
|
973
|
+
* Handle ws.use card action: switch to the workspace, then refresh the /ws
|
|
974
|
+
* list card in place so "recent" sort order is immediately visible.
|
|
975
|
+
*
|
|
976
|
+
* Unlike the command-line `/ws use` (which returns text), the card action
|
|
977
|
+
* uses toast + inplace refresh for a cleaner UX (matching ws.remove/ws.sort).
|
|
978
|
+
* If auto-resume produces a card, it is sent as a separate reply.
|
|
979
|
+
*/
|
|
980
|
+
async handleWsUse(value, ctx) {
|
|
981
|
+
const name = value.name ?? '';
|
|
982
|
+
const useResult = this.cmdWs(['use', name], ctx);
|
|
983
|
+
// Error cases (workspace not found, path invalid, etc.): relay as error toast
|
|
984
|
+
if (useResult.text && !useResult.text.includes('已切换')) {
|
|
985
|
+
return { toast: { type: 'error', content: useResult.text } };
|
|
986
|
+
}
|
|
987
|
+
// Success: if auto-resume produced a card, send it as a reply
|
|
988
|
+
if (useResult.card) {
|
|
989
|
+
await this.bridge.sendResult(useResult, ctx);
|
|
990
|
+
}
|
|
991
|
+
// Refresh the /ws list card in place so "recent" sort is immediately visible
|
|
992
|
+
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
993
|
+
const refreshed = this.cmdWs([], ctx, currentOffset);
|
|
994
|
+
if (refreshed.card) {
|
|
995
|
+
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
996
|
+
}
|
|
997
|
+
// Toast feedback (suppress text when auto-resume card was already sent)
|
|
998
|
+
return {
|
|
999
|
+
toast: {
|
|
1000
|
+
type: 'success',
|
|
1001
|
+
content: useResult.card ? '' : (useResult.text ?? `已切换到 workspace "${name}"`),
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
935
1005
|
/**
|
|
936
1006
|
* Handle ws.remove: remove the workspace alias and refresh the /ws list card
|
|
937
1007
|
* in place (mirrors handleOrderDelete). Without this the stale alias would
|
|
@@ -945,11 +1015,40 @@ export class CommandRouter {
|
|
|
945
1015
|
// Execute the removal (consumes the write-side effect; the returned text
|
|
946
1016
|
// is discarded — feedback flows through the toast + refreshed card).
|
|
947
1017
|
this.cmdWs(['remove', name], ctx);
|
|
948
|
-
// Rebuild the /ws list card and update it in place
|
|
949
|
-
|
|
1018
|
+
// Rebuild the /ws list card and update it in place, preserving the page
|
|
1019
|
+
// the user was on when they clicked 删除.
|
|
1020
|
+
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
1021
|
+
const refreshed = this.cmdWs([], ctx, currentOffset);
|
|
950
1022
|
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
951
1023
|
return { toast: { type: 'success', content: `已删除 workspace "${name}"` } };
|
|
952
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Handle ws.page: paginate the /ws list in place.
|
|
1027
|
+
* Mirrors handleOrderPage — updates the same card, never sends a new one.
|
|
1028
|
+
*/
|
|
1029
|
+
async handleWsPage(value, ctx) {
|
|
1030
|
+
const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
1031
|
+
// cmdWs internally clamps stale/out-of-range offsets
|
|
1032
|
+
const result = this.cmdWs([], ctx, offset);
|
|
1033
|
+
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
1034
|
+
return { toast: { type: 'success', content: '' } };
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Handle ws.sort: toggle sort preference and refresh the /ws list card.
|
|
1038
|
+
* Sort preference is memory-only (not persisted); restart resets to 'recent'.
|
|
1039
|
+
*/
|
|
1040
|
+
async handleWsSort(value, ctx) {
|
|
1041
|
+
const userId = ctx.userId;
|
|
1042
|
+
const current = this.wsSortPreference.get(userId) ?? 'recent';
|
|
1043
|
+
const next = current === 'recent' ? 'alpha' : 'recent';
|
|
1044
|
+
this.wsSortPreference.set(userId, next);
|
|
1045
|
+
const result = this.cmdWs([], ctx, 0);
|
|
1046
|
+
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
1047
|
+
const nextLabel = next === 'recent' ? '🕐 最近使用' : '🔤 字母顺序';
|
|
1048
|
+
return {
|
|
1049
|
+
toast: { type: 'success', content: `已切换为 ${nextLabel}` },
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
953
1052
|
/**
|
|
954
1053
|
* `ls.file` card button: send file to Feishu, rejecting if > 30MB.
|
|
955
1054
|
*/
|
|
@@ -1106,7 +1205,7 @@ export class CommandRouter {
|
|
|
1106
1205
|
this.config = setConfigValues(this.configPath, this.config, updates);
|
|
1107
1206
|
// P1-6:运行时传播(idleTimeout / clearRunners / syncAgentChoices /
|
|
1108
1207
|
// defaultAgent 切换的 session 处理)抽到共享方法,与文本直写路径共用。
|
|
1109
|
-
const
|
|
1208
|
+
const switchResult = this.propagateConfigSave(oldDefaultAgent, updates, ctx);
|
|
1110
1209
|
this.pendingConfig = null;
|
|
1111
1210
|
// 原地更新卡片:保存后卡片刷新为已保存状态(2026-07-04)
|
|
1112
1211
|
// 2026-08-03: 卡片刷新失败不吞切换通知——保存已成功,仅记录日志,
|
|
@@ -1117,14 +1216,15 @@ export class CommandRouter {
|
|
|
1117
1216
|
catch (refreshErr) {
|
|
1118
1217
|
getLogger().warn(`[router] config.save 卡片刷新失败(保存已成功): ${refreshErr.message}`);
|
|
1119
1218
|
}
|
|
1120
|
-
// 2026-08-
|
|
1121
|
-
//
|
|
1122
|
-
// toast
|
|
1123
|
-
if (
|
|
1124
|
-
const
|
|
1219
|
+
// 2026-08-13: agent 切换时发送 Resume 卡片(替代纯文本通知),
|
|
1220
|
+
// 让用户直观看到新 agent 的会话状态和历史。
|
|
1221
|
+
// 发送失败(resolve false)时兜底回退 toast 即时反馈。
|
|
1222
|
+
if (switchResult.notice) {
|
|
1223
|
+
const switchCard = this.buildConfigSwitchCard(switchResult, ctx);
|
|
1224
|
+
const sent = await this.bridge.sendResult(switchCard, ctx);
|
|
1125
1225
|
if (!sent) {
|
|
1126
1226
|
return {
|
|
1127
|
-
toast: { type: 'info', content:
|
|
1227
|
+
toast: { type: 'info', content: switchResult.notice },
|
|
1128
1228
|
};
|
|
1129
1229
|
}
|
|
1130
1230
|
}
|
|
@@ -1226,7 +1326,7 @@ export class CommandRouter {
|
|
|
1226
1326
|
*
|
|
1227
1327
|
* @param updates 以 config 路径形式给出的变更 key(卡片路径来自 diffConfig;
|
|
1228
1328
|
* 直写路径传 mapAgentKey(key),保证 pi./codex./opencode./kimi. 命中 agents.*)。
|
|
1229
|
-
* @returns
|
|
1329
|
+
* @returns 结构化切换结果(含 notice 文案 + 新 agent/sessionId 信息)
|
|
1230
1330
|
*/
|
|
1231
1331
|
propagateConfigSave(oldDefaultAgent, updates, ctx) {
|
|
1232
1332
|
this.bridge.setConfig(this.config);
|
|
@@ -1251,13 +1351,12 @@ export class CommandRouter {
|
|
|
1251
1351
|
// 同时清除新 agent 的 sessionId(等效于 /new 命令),
|
|
1252
1352
|
// 并支持 session 恢复 - 切换回来时如果条件满足则恢复之前的 session。
|
|
1253
1353
|
let switchNotice;
|
|
1354
|
+
let switchNewAgent;
|
|
1355
|
+
let switchSessionId;
|
|
1254
1356
|
if (hasDefaultAgentChange) {
|
|
1255
1357
|
const newAgent = this.config.defaultAgent;
|
|
1358
|
+
switchNewAgent = newAgent;
|
|
1256
1359
|
// Step 0: 在清空 old 之前计算「用户活动」基线差(arrival 基线):
|
|
1257
|
-
// arrivalSessions[old] 是用户最近一次经 config.save 切换「到达」该 agent
|
|
1258
|
-
// 时被赋予的 session id(清空到达 = '')。当前 session 与到达基线不一致
|
|
1259
|
-
// ⇒ 用户在 oldAgent 上产生过用户活动(发消息 /resume /new /cd 等,均
|
|
1260
|
-
// 不更新 arrival),离开时恢复必须被阻断。
|
|
1261
1360
|
const oldSessionId = this.sessionStore.getSessionId(ctx.userId, oldDefaultAgent);
|
|
1262
1361
|
const oldArrivalSessionId = this.sessionStore.getArrivalSessionId(ctx.userId, oldDefaultAgent);
|
|
1263
1362
|
const userChangedOld = (oldSessionId ?? '') !== (oldArrivalSessionId ?? '');
|
|
@@ -1267,48 +1366,36 @@ export class CommandRouter {
|
|
|
1267
1366
|
}
|
|
1268
1367
|
// Step 2: 清除旧 agent 的 session(保存后清除,为新会话腾出空间)
|
|
1269
1368
|
this.sessionStore.clearSessionId(ctx.userId, oldDefaultAgent);
|
|
1270
|
-
// Step 2.5:
|
|
1271
|
-
// 显式改选了 new 的 session(/resume 不更新 arrival),切换时必须让选择存活。
|
|
1272
|
-
// 优先级:显式选择 > 停车恢复 > 清空;newSessionId 为空(防御)不触发本分支。
|
|
1369
|
+
// Step 2.5: 显式选择判定
|
|
1273
1370
|
const newSessionId = this.sessionStore.getSessionId(ctx.userId, newAgent);
|
|
1274
1371
|
const newArrivalSessionId = this.sessionStore.getArrivalSessionId(ctx.userId, newAgent);
|
|
1275
1372
|
const userSelectedNew = !!newSessionId && (newSessionId ?? '') !== (newArrivalSessionId ?? '');
|
|
1276
|
-
// Step 3:
|
|
1277
|
-
// 没有用户活动(session 未变于到达基线)。判断方法:arrival 基线差
|
|
1278
|
-
// userChangedOld 为 false 时,oldAgent 的 session 只可能是 config.save
|
|
1279
|
-
// 自动恢复/清空得到的,不构成用户活动,恢复不被阻断。
|
|
1373
|
+
// Step 3: 恢复判定
|
|
1280
1374
|
const previousSessionId = this.sessionStore.getPreviousSessionId(ctx.userId, newAgent);
|
|
1281
1375
|
const canRestore = !!previousSessionId && !userChangedOld;
|
|
1282
1376
|
if (userSelectedNew) {
|
|
1283
|
-
// 显式选择优先于停车恢复与用户活动阻断:保留 sessions[new](不动),更新
|
|
1284
|
-
// arrival 基线为所选 session;prev[new] 停车位原样保留(下次离开时会被
|
|
1285
|
-
// sessions[new] 覆盖停车)。
|
|
1286
1377
|
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, newSessionId);
|
|
1287
1378
|
const newAgentName = agentDisplayName(newAgent);
|
|
1288
1379
|
switchNotice = `已切换到 ${newAgentName},已使用所选 session,sessionId: ${newSessionId}`;
|
|
1380
|
+
switchSessionId = newSessionId;
|
|
1289
1381
|
}
|
|
1290
1382
|
else if (canRestore) {
|
|
1291
|
-
// 可以恢复之前的 session
|
|
1292
1383
|
this.sessionStore.setSessionId(ctx.userId, newAgent, previousSessionId);
|
|
1293
|
-
// 恢复分支消费 previousSessions,并记录新的到达基线(自动恢复的 session
|
|
1294
|
-
// 不构成用户活动,后续离开时 userChangedOld 仍为 false)
|
|
1295
1384
|
this.sessionStore.clearPreviousSessionId(ctx.userId, newAgent);
|
|
1296
1385
|
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, previousSessionId);
|
|
1297
1386
|
const newAgentName = agentDisplayName(newAgent);
|
|
1298
1387
|
switchNotice = `已切换到 ${newAgentName},将继续之前的 session,sessionId: ${previousSessionId}`;
|
|
1388
|
+
switchSessionId = previousSessionId;
|
|
1299
1389
|
}
|
|
1300
1390
|
else {
|
|
1301
|
-
// Step 4: 清除新 agent 的 session(等效于 /new)
|
|
1302
1391
|
this.sessionStore.clearSessionId(ctx.userId, newAgent, { clearSessionCwd: true });
|
|
1303
|
-
// 清空分支不清除 previousSessions[newAgent](停车语义:被拒绝的恢复
|
|
1304
|
-
// 机会保留,未来无用户活动的返回仍可恢复);记录清空到达基线
|
|
1305
1392
|
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, '');
|
|
1306
1393
|
const newAgentName = agentDisplayName(newAgent);
|
|
1307
1394
|
switchNotice = `已切换到 ${newAgentName},session 已清空,下次消息将启动新对话`;
|
|
1395
|
+
switchSessionId = '';
|
|
1308
1396
|
}
|
|
1309
1397
|
}
|
|
1310
1398
|
// 2026-07-13: 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
|
|
1311
|
-
// 检测是否有当前 agent 的配置变更
|
|
1312
1399
|
const currentAgent = this.config.defaultAgent;
|
|
1313
1400
|
const currentAgentConfigKey = `agents.${currentAgent}`;
|
|
1314
1401
|
const hasCurrentAgentUpdate = Object.keys(updates).some((k) => k === currentAgentConfigKey || k.startsWith(`${currentAgentConfigKey}.`));
|
|
@@ -1319,11 +1406,10 @@ export class CommandRouter {
|
|
|
1319
1406
|
this.bridge.setConfig(this.config);
|
|
1320
1407
|
}
|
|
1321
1408
|
// 2026-07-13: 任何 agent 配置变更或 defaultAgent 切换,都清除 runner 缓存
|
|
1322
|
-
// 否则 runner 仍使用旧配置值
|
|
1323
1409
|
if (hasDefaultAgentChange || hasAgentConfigChange) {
|
|
1324
1410
|
this.bridge.clearRunners();
|
|
1325
1411
|
}
|
|
1326
|
-
return switchNotice;
|
|
1412
|
+
return { notice: switchNotice, newAgent: switchNewAgent, sessionId: switchSessionId };
|
|
1327
1413
|
}
|
|
1328
1414
|
async executeCommand(message, ctx) {
|
|
1329
1415
|
const parts = message.slice(1).split(/\s+/);
|
|
@@ -1368,6 +1454,9 @@ export class CommandRouter {
|
|
|
1368
1454
|
case 'order':
|
|
1369
1455
|
case 'o':
|
|
1370
1456
|
return this.cmdOrder(args, ctx);
|
|
1457
|
+
case 'update':
|
|
1458
|
+
case 'u':
|
|
1459
|
+
return await this.cmdUpdate(args, ctx);
|
|
1371
1460
|
default:
|
|
1372
1461
|
return { text: `未知命令 /${cmd},输入 /help 查看可用命令` };
|
|
1373
1462
|
}
|
|
@@ -1387,6 +1476,7 @@ export class CommandRouter {
|
|
|
1387
1476
|
{ cmd: 'active', label: '/active', desc: '查看所有正在进行中的 session' },
|
|
1388
1477
|
{ cmd: 'exit', label: '/exit /e', desc: '退出 bridge' },
|
|
1389
1478
|
{ cmd: 'restart', label: '/restart', desc: '重启 bridge(新进程,config 不变)' },
|
|
1479
|
+
{ cmd: 'update', label: '/update /u', desc: '检查并升级到最新版本' },
|
|
1390
1480
|
{ cmd: 'status', label: '/status /s', desc: '显示当前状态' },
|
|
1391
1481
|
{ cmd: 'reconnect', label: '/reconnect', desc: '重连飞书' },
|
|
1392
1482
|
].sort((a, b) => a.label.length - b.label.length || a.cmd.localeCompare(b.cmd));
|
|
@@ -1573,6 +1663,61 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1573
1663
|
return { text: `重启失败:${err.message},旧进程仍在运行` };
|
|
1574
1664
|
}
|
|
1575
1665
|
}
|
|
1666
|
+
/**
|
|
1667
|
+
* /update: check for newer version and upgrade if available.
|
|
1668
|
+
* /update check: only check, don't upgrade.
|
|
1669
|
+
*
|
|
1670
|
+
* Flow: check version → if newer, install → spawn replacement bridge → exit.
|
|
1671
|
+
* Reuses /restart's restartSpawner + pendingExit mechanism.
|
|
1672
|
+
*/
|
|
1673
|
+
async cmdUpdate(args, ctx) {
|
|
1674
|
+
const checkOnly = args[0] === 'check';
|
|
1675
|
+
// 1. Version query(/update check 始终绕过缓存,/update 用 TTL 缓存)
|
|
1676
|
+
let current;
|
|
1677
|
+
let latest;
|
|
1678
|
+
try {
|
|
1679
|
+
const result = await this.updateFns.checkLatestVersion({
|
|
1680
|
+
cachePath: this.updateCachePath,
|
|
1681
|
+
bypassCache: checkOnly,
|
|
1682
|
+
});
|
|
1683
|
+
current = result.current;
|
|
1684
|
+
latest = result.latest;
|
|
1685
|
+
}
|
|
1686
|
+
catch (err) {
|
|
1687
|
+
return { text: `❌ 版本检查失败: ${err.message}` };
|
|
1688
|
+
}
|
|
1689
|
+
if (!this.updateFns.isNewer(current, latest)) {
|
|
1690
|
+
return { text: `✅ 已是最新版本 ${current}` };
|
|
1691
|
+
}
|
|
1692
|
+
if (checkOnly) {
|
|
1693
|
+
return { text: `📦 有新版本 ${latest} 可用(当前 ${current}),发送 /update 升级` };
|
|
1694
|
+
}
|
|
1695
|
+
// 2. Dev mode guard
|
|
1696
|
+
if (this.devMode) {
|
|
1697
|
+
return { text: '⚠️ 开发模式不支持自更新,请 git pull 后重新构建' };
|
|
1698
|
+
}
|
|
1699
|
+
// 3. Install (send intermediate status before the blocking operation)
|
|
1700
|
+
await this.bridge.sendResult({ text: `⬆️ 正在升级 ${current} → ${latest} ...` }, ctx);
|
|
1701
|
+
const installResult = await this.updateFns.runInstallLatest();
|
|
1702
|
+
if (!installResult.success) {
|
|
1703
|
+
return { text: `❌ 升级失败: ${installResult.error}` };
|
|
1704
|
+
}
|
|
1705
|
+
// 4. Restart (reuse /restart's restartSpawner + pendingExit)
|
|
1706
|
+
if (!this.restartSpawner) {
|
|
1707
|
+
return { text: `✅ 已升级到 ${latest},请手动 /restart 重启 bridge` };
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
const pid = this.restartSpawner();
|
|
1711
|
+
this.pendingExit = true;
|
|
1712
|
+
return { text: `✅ 升级成功 (${current} → ${latest}),正在重启(新进程 pid ${pid})…` };
|
|
1713
|
+
}
|
|
1714
|
+
catch (err) {
|
|
1715
|
+
getLogger().error('[router] update restart spawn failed:', err);
|
|
1716
|
+
return {
|
|
1717
|
+
text: `✅ 已升级到 ${latest},但重启失败:${err.message},请手动 /restart`,
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1576
1721
|
cmdNew(ctx) {
|
|
1577
1722
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
1578
1723
|
// 设置工作目录",但用户的 workspace 还在。2026-06-21 复盘:用户
|
|
@@ -1755,6 +1900,136 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1755
1900
|
};
|
|
1756
1901
|
return { card };
|
|
1757
1902
|
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Build card for config.save agent switch.
|
|
1905
|
+
* When the new agent has a restored session, renders a Resume card (mirrors
|
|
1906
|
+
* buildAutoResumeCard) with session history + stop/new buttons.
|
|
1907
|
+
* When the session was cleared, renders a compact notice card instead.
|
|
1908
|
+
*/
|
|
1909
|
+
buildConfigSwitchCard(switchResult, ctx) {
|
|
1910
|
+
const newAgent = switchResult.newAgent;
|
|
1911
|
+
const agentName = agentDisplayName(newAgent);
|
|
1912
|
+
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
1913
|
+
const sessionId = switchResult.sessionId;
|
|
1914
|
+
// No cwd or no session → compact notice card
|
|
1915
|
+
if (!cwd || !sessionId) {
|
|
1916
|
+
const elements = [markdownDiv(switchResult.notice)];
|
|
1917
|
+
// New session button
|
|
1918
|
+
elements.push({
|
|
1919
|
+
tag: 'column_set',
|
|
1920
|
+
columns: [
|
|
1921
|
+
{
|
|
1922
|
+
tag: 'column',
|
|
1923
|
+
width: 'auto',
|
|
1924
|
+
elements: [newSessionButton()],
|
|
1925
|
+
},
|
|
1926
|
+
],
|
|
1927
|
+
});
|
|
1928
|
+
return {
|
|
1929
|
+
card: {
|
|
1930
|
+
schema: '2.0',
|
|
1931
|
+
config: { wide_screen_mode: true },
|
|
1932
|
+
header: {
|
|
1933
|
+
title: { tag: 'plain_text', content: `🔀 切换到 ${agentName}` },
|
|
1934
|
+
},
|
|
1935
|
+
body: { elements },
|
|
1936
|
+
},
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
// Has session → Resume card (mirrors buildAutoResumeCard)
|
|
1940
|
+
// Guard: if the session reader for the new agent is not available (e.g. agent
|
|
1941
|
+
// not registered), fall back to compact notice card to avoid NPE.
|
|
1942
|
+
const readerForNewAgent = newAgent === this.config.defaultAgent
|
|
1943
|
+
? this.sessionReader
|
|
1944
|
+
: this.sessionReaderRegistry.get(newAgent);
|
|
1945
|
+
if (!readerForNewAgent) {
|
|
1946
|
+
// Fallback: compact notice card with the notice text
|
|
1947
|
+
const elements = [markdownDiv(switchResult.notice)];
|
|
1948
|
+
elements.push({
|
|
1949
|
+
tag: 'column_set',
|
|
1950
|
+
columns: [
|
|
1951
|
+
{
|
|
1952
|
+
tag: 'column',
|
|
1953
|
+
width: 'auto',
|
|
1954
|
+
elements: [newSessionButton()],
|
|
1955
|
+
},
|
|
1956
|
+
],
|
|
1957
|
+
});
|
|
1958
|
+
return {
|
|
1959
|
+
card: {
|
|
1960
|
+
schema: '2.0',
|
|
1961
|
+
config: { wide_screen_mode: true },
|
|
1962
|
+
header: { title: { tag: 'plain_text', content: `🔀 切换到 ${agentName}` } },
|
|
1963
|
+
body: { elements },
|
|
1964
|
+
},
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionId, cwd, {
|
|
1968
|
+
agentKind: newAgent,
|
|
1969
|
+
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
1970
|
+
});
|
|
1971
|
+
let header = `📂 \`${cwd}\`\n已恢复会话: **${sessionId}**`;
|
|
1972
|
+
const sections = [];
|
|
1973
|
+
if (displayTitle) {
|
|
1974
|
+
const label = aiTitle ? 'AI 标题' : '最近输入';
|
|
1975
|
+
sections.push(`🏷️ **${label}**\n${displayTitle}`);
|
|
1976
|
+
}
|
|
1977
|
+
if (recap) {
|
|
1978
|
+
const recapPreview = recap.length > 200 ? recap.slice(0, 197) + '...' : recap;
|
|
1979
|
+
sections.push(`📝 **Recap**\n${recapPreview}`);
|
|
1980
|
+
}
|
|
1981
|
+
if (sections.length > 0) {
|
|
1982
|
+
header += '\n\n' + sections.join('\n\n──\n\n');
|
|
1983
|
+
}
|
|
1984
|
+
const elements = [markdownDiv(header), { tag: 'hr' }];
|
|
1985
|
+
if (content.length === 0) {
|
|
1986
|
+
elements.push(markdownDiv('_该会话暂无新消息可显示(最后一条为用户输入)_'));
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
content.forEach((ev, i) => {
|
|
1990
|
+
elements.push(sessionEventPanel(ev, i, content.length, 2, newAgent));
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
if (usage) {
|
|
1994
|
+
const usageStr = formatUsageStats(usage, { showResult: true, result: 'success' });
|
|
1995
|
+
elements.push(markdownDiv(usageStr));
|
|
1996
|
+
}
|
|
1997
|
+
if (elements.length > 0 && elements[elements.length - 1].tag === 'hr') {
|
|
1998
|
+
elements.pop();
|
|
1999
|
+
}
|
|
2000
|
+
// Action buttons
|
|
2001
|
+
const actions = [];
|
|
2002
|
+
if (isActive) {
|
|
2003
|
+
actions.push({
|
|
2004
|
+
tag: 'button',
|
|
2005
|
+
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
2006
|
+
type: 'danger',
|
|
2007
|
+
behaviors: [{ type: 'callback', value: { cmd: 'stop', runId: activeRunRunId, cwd } }],
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
actions.push(newSessionButton());
|
|
2011
|
+
elements.push({
|
|
2012
|
+
tag: 'column_set',
|
|
2013
|
+
columns: actions.map((btn) => ({
|
|
2014
|
+
tag: 'column',
|
|
2015
|
+
width: 'auto',
|
|
2016
|
+
elements: [btn],
|
|
2017
|
+
})),
|
|
2018
|
+
});
|
|
2019
|
+
return {
|
|
2020
|
+
card: {
|
|
2021
|
+
schema: '2.0',
|
|
2022
|
+
config: { wide_screen_mode: true },
|
|
2023
|
+
header: {
|
|
2024
|
+
title: {
|
|
2025
|
+
tag: 'plain_text',
|
|
2026
|
+
content: `${isActive ? '⏳ 切换 Agent(完成中)' : '🔀 切换 Agent'} · ${agentName}`,
|
|
2027
|
+
},
|
|
2028
|
+
},
|
|
2029
|
+
body: { elements },
|
|
2030
|
+
},
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
1758
2033
|
static LS_PAGE_SIZE = 30;
|
|
1759
2034
|
cmdLs(args, ctx, offset = 0) {
|
|
1760
2035
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
@@ -2030,7 +2305,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2030
2305
|
return { text: `读取目录失败: ${err.message}` };
|
|
2031
2306
|
}
|
|
2032
2307
|
}
|
|
2033
|
-
cmdWs(args, ctx) {
|
|
2308
|
+
cmdWs(args, ctx, offset = 0) {
|
|
2034
2309
|
const sub = args[0]?.toLowerCase();
|
|
2035
2310
|
switch (sub) {
|
|
2036
2311
|
case 'save': {
|
|
@@ -2063,6 +2338,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2063
2338
|
catch {
|
|
2064
2339
|
return { text: `路径无效: ${wsPath}` };
|
|
2065
2340
|
}
|
|
2341
|
+
// Record usage timestamp AFTER successful path validation
|
|
2342
|
+
this.workspaceStore.touch(name);
|
|
2066
2343
|
// Switch cwd with auto-resume and user feedback (shared with /cd and ls.switch)
|
|
2067
2344
|
return this.switchCwdAndNotify(ctx.userId, canonical, ctx);
|
|
2068
2345
|
}
|
|
@@ -2079,6 +2356,29 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2079
2356
|
default: {
|
|
2080
2357
|
const entries = this.workspaceStore.list();
|
|
2081
2358
|
const currentCwd = this.sessionStore.getCwd(ctx.userId);
|
|
2359
|
+
const sortMode = this.wsSortPreference.get(ctx.userId) ?? 'recent';
|
|
2360
|
+
// Sort entries by the current preference (view concern, not stored)
|
|
2361
|
+
if (sortMode === 'recent') {
|
|
2362
|
+
// Most recently used first; same lastUsedAt → alphabetical by name (deterministic)
|
|
2363
|
+
entries.sort((a, b) => {
|
|
2364
|
+
if (b.lastUsedAt !== a.lastUsedAt)
|
|
2365
|
+
return b.lastUsedAt - a.lastUsedAt;
|
|
2366
|
+
return a.name.localeCompare(b.name);
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
else {
|
|
2370
|
+
// Alphabetical by alias name
|
|
2371
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2372
|
+
}
|
|
2373
|
+
// Pagination calculations (mirror cmdOrder): clamp stale/out-of-range
|
|
2374
|
+
// offsets so prev/next always step by WS_PAGE_SIZE.
|
|
2375
|
+
const totalCount = entries.length;
|
|
2376
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / WS_PAGE_SIZE));
|
|
2377
|
+
const maxOffset = Math.max(0, (totalPages - 1) * WS_PAGE_SIZE);
|
|
2378
|
+
const safeOffset = Math.min(Math.max(offset, 0), maxOffset);
|
|
2379
|
+
const currentPage = Math.floor(safeOffset / WS_PAGE_SIZE) + 1;
|
|
2380
|
+
const pageEntries = entries.slice(safeOffset, safeOffset + WS_PAGE_SIZE);
|
|
2381
|
+
const hasPagination = totalCount > WS_PAGE_SIZE;
|
|
2082
2382
|
// Build body elements: current cwd + workspace list with dividers
|
|
2083
2383
|
const bodyElements = [];
|
|
2084
2384
|
// Current working directory section
|
|
@@ -2086,18 +2386,56 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2086
2386
|
tag: 'div',
|
|
2087
2387
|
text: { tag: 'lark_md', content: `📂 当前工作目录:\`${currentCwd ?? '(未设置)'}\`` },
|
|
2088
2388
|
});
|
|
2089
|
-
bodyElements.push({ tag: 'hr' });
|
|
2090
2389
|
if (entries.length === 0) {
|
|
2390
|
+
bodyElements.push({ tag: 'hr' });
|
|
2091
2391
|
bodyElements.push({
|
|
2092
2392
|
tag: 'div',
|
|
2093
2393
|
text: { tag: 'lark_md', content: '没有保存的 workspace' },
|
|
2094
2394
|
});
|
|
2095
2395
|
}
|
|
2096
2396
|
else {
|
|
2097
|
-
|
|
2397
|
+
// Sort mode indicator + toggle button: placed above the list so the user
|
|
2398
|
+
// sees "current mode" and "switch to X" before scanning entries.
|
|
2399
|
+
// On narrow (mobile) screens, the label and button must convey both
|
|
2400
|
+
// what-is-active and what-clicking-does to avoid ambiguity.
|
|
2401
|
+
const currentLabel = sortMode === 'recent' ? '🕐 最近使用' : '🔤 字母顺序';
|
|
2402
|
+
const switchLabel = sortMode === 'recent' ? '🔤 字母顺序' : '🕐 最近使用';
|
|
2403
|
+
bodyElements.push({ tag: 'hr' });
|
|
2404
|
+
bodyElements.push({
|
|
2405
|
+
tag: 'column_set',
|
|
2406
|
+
columns: [
|
|
2407
|
+
{
|
|
2408
|
+
tag: 'column',
|
|
2409
|
+
width: 'weighted',
|
|
2410
|
+
weight: 1,
|
|
2411
|
+
vertical_align: 'center',
|
|
2412
|
+
elements: [
|
|
2413
|
+
{
|
|
2414
|
+
tag: 'div',
|
|
2415
|
+
text: { tag: 'lark_md', content: `排序:${currentLabel}` },
|
|
2416
|
+
},
|
|
2417
|
+
],
|
|
2418
|
+
},
|
|
2419
|
+
{
|
|
2420
|
+
tag: 'column',
|
|
2421
|
+
width: 'auto',
|
|
2422
|
+
elements: [
|
|
2423
|
+
{
|
|
2424
|
+
tag: 'button',
|
|
2425
|
+
text: { tag: 'plain_text', content: `切换为 ${switchLabel}` },
|
|
2426
|
+
type: 'default',
|
|
2427
|
+
size: 'small',
|
|
2428
|
+
behaviors: [{ type: 'callback', value: { cmd: 'ws.sort' } }],
|
|
2429
|
+
},
|
|
2430
|
+
],
|
|
2431
|
+
},
|
|
2432
|
+
],
|
|
2433
|
+
});
|
|
2434
|
+
bodyElements.push({ tag: 'hr' });
|
|
2435
|
+
for (const entry of pageEntries) {
|
|
2098
2436
|
bodyElements.push({
|
|
2099
2437
|
tag: 'div',
|
|
2100
|
-
text: { tag: 'lark_md', content: `**${name}** → \`${
|
|
2438
|
+
text: { tag: 'lark_md', content: `**${entry.name}** → \`${entry.path}\`` },
|
|
2101
2439
|
});
|
|
2102
2440
|
// Use column_set+column for 2.0 (action tag not supported in 2.0)
|
|
2103
2441
|
bodyElements.push({
|
|
@@ -2112,7 +2450,12 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2112
2450
|
text: { tag: 'plain_text', content: '切换' },
|
|
2113
2451
|
type: 'primary',
|
|
2114
2452
|
size: 'small',
|
|
2115
|
-
behaviors: [
|
|
2453
|
+
behaviors: [
|
|
2454
|
+
{
|
|
2455
|
+
type: 'callback',
|
|
2456
|
+
value: { cmd: 'ws.use', name: entry.name, offset: safeOffset },
|
|
2457
|
+
},
|
|
2458
|
+
],
|
|
2116
2459
|
},
|
|
2117
2460
|
],
|
|
2118
2461
|
},
|
|
@@ -2125,15 +2468,86 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2125
2468
|
text: { tag: 'plain_text', content: '删除' },
|
|
2126
2469
|
type: 'danger',
|
|
2127
2470
|
size: 'small',
|
|
2128
|
-
behaviors: [
|
|
2471
|
+
behaviors: [
|
|
2472
|
+
{
|
|
2473
|
+
type: 'callback',
|
|
2474
|
+
value: { cmd: 'ws.remove', name: entry.name, offset: safeOffset },
|
|
2475
|
+
},
|
|
2476
|
+
],
|
|
2129
2477
|
},
|
|
2130
2478
|
],
|
|
2131
2479
|
},
|
|
2132
2480
|
],
|
|
2133
2481
|
});
|
|
2134
2482
|
bodyElements.push({ tag: 'hr' });
|
|
2135
|
-
}
|
|
2483
|
+
}
|
|
2136
2484
|
bodyElements.pop();
|
|
2485
|
+
// Pagination bar — separate row below the list (not crowded with sort
|
|
2486
|
+
// toggle; mobile-friendly vertical layout).
|
|
2487
|
+
if (hasPagination) {
|
|
2488
|
+
const hasPrev = safeOffset > 0;
|
|
2489
|
+
const hasNext = safeOffset + WS_PAGE_SIZE < totalCount;
|
|
2490
|
+
const pageColumns = [];
|
|
2491
|
+
if (hasPrev) {
|
|
2492
|
+
pageColumns.push({
|
|
2493
|
+
tag: 'column',
|
|
2494
|
+
width: 'auto',
|
|
2495
|
+
vertical_align: 'center',
|
|
2496
|
+
elements: [
|
|
2497
|
+
{
|
|
2498
|
+
tag: 'button',
|
|
2499
|
+
text: { tag: 'plain_text', content: '⬅' },
|
|
2500
|
+
type: 'default',
|
|
2501
|
+
size: 'small',
|
|
2502
|
+
behaviors: [
|
|
2503
|
+
{
|
|
2504
|
+
type: 'callback',
|
|
2505
|
+
value: { cmd: 'ws.page', offset: safeOffset - WS_PAGE_SIZE },
|
|
2506
|
+
},
|
|
2507
|
+
],
|
|
2508
|
+
},
|
|
2509
|
+
],
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
pageColumns.push({
|
|
2513
|
+
tag: 'column',
|
|
2514
|
+
width: 'weighted',
|
|
2515
|
+
weight: 1,
|
|
2516
|
+
vertical_align: 'center',
|
|
2517
|
+
elements: [
|
|
2518
|
+
{
|
|
2519
|
+
tag: 'div',
|
|
2520
|
+
text: {
|
|
2521
|
+
tag: 'lark_md',
|
|
2522
|
+
content: `**${currentPage}/${totalPages}**(${totalCount})`,
|
|
2523
|
+
},
|
|
2524
|
+
},
|
|
2525
|
+
],
|
|
2526
|
+
});
|
|
2527
|
+
if (hasNext) {
|
|
2528
|
+
pageColumns.push({
|
|
2529
|
+
tag: 'column',
|
|
2530
|
+
width: 'auto',
|
|
2531
|
+
vertical_align: 'center',
|
|
2532
|
+
elements: [
|
|
2533
|
+
{
|
|
2534
|
+
tag: 'button',
|
|
2535
|
+
text: { tag: 'plain_text', content: '➡' },
|
|
2536
|
+
type: 'default',
|
|
2537
|
+
size: 'small',
|
|
2538
|
+
behaviors: [
|
|
2539
|
+
{
|
|
2540
|
+
type: 'callback',
|
|
2541
|
+
value: { cmd: 'ws.page', offset: safeOffset + WS_PAGE_SIZE },
|
|
2542
|
+
},
|
|
2543
|
+
],
|
|
2544
|
+
},
|
|
2545
|
+
],
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
bodyElements.push({ tag: 'hr' });
|
|
2549
|
+
bodyElements.push({ tag: 'column_set', columns: pageColumns });
|
|
2550
|
+
}
|
|
2137
2551
|
}
|
|
2138
2552
|
const card = {
|
|
2139
2553
|
schema: '2.0',
|
|
@@ -2682,8 +3096,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2682
3096
|
// clearRunners / syncAgentChoices / defaultAgent 切换)。key 经 mapAgentKey
|
|
2683
3097
|
// 归一化,pi./codex./opencode./kimi. 命中 agents.* 判定;claude. 与
|
|
2684
3098
|
// defaultAgent 不再漏判。
|
|
2685
|
-
this.propagateConfigSave(oldDefaultAgent, { [mapAgentKey(key)]: value }, ctx);
|
|
3099
|
+
const switchResult = this.propagateConfigSave(oldDefaultAgent, { [mapAgentKey(key)]: value }, ctx);
|
|
2686
3100
|
this.pendingConfig = null; // 命令式写盘后清空暂存,避免不一致
|
|
3101
|
+
// 2026-08-13: agent 切换时额外发送 Resume 卡片
|
|
3102
|
+
if (switchResult.notice) {
|
|
3103
|
+
const switchCard = this.buildConfigSwitchCard(switchResult, ctx);
|
|
3104
|
+
// 文本路径返回 config card,Resume card 通过 bridge 异步发送
|
|
3105
|
+
void this.bridge.sendResult(switchCard, ctx);
|
|
3106
|
+
}
|
|
2687
3107
|
const card = this.buildConfigCard();
|
|
2688
3108
|
if (discarded > 0 && card.card && typeof card.card === 'object') {
|
|
2689
3109
|
const body = card.card.body;
|