lark-remote 0.1.13 → 0.2.0
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 +25 -0
- package/README.en.md +2 -6
- package/README.md +3 -7
- package/dist/bridge/final-usage.d.ts +24 -0
- package/dist/bridge/final-usage.js +55 -0
- package/dist/bridge/index.d.ts +0 -7
- package/dist/bridge/index.js +21 -63
- package/dist/bridge/queue-manager.js +4 -3
- package/dist/card/approval-render.js +39 -94
- package/dist/card/bash-renderer.js +12 -21
- package/dist/card/card-shared.d.ts +13 -1
- package/dist/card/card-shared.js +21 -30
- package/dist/card/run-renderer.d.ts +0 -22
- package/dist/card/run-renderer.js +206 -153
- package/dist/card/run-state.d.ts +29 -1
- package/dist/card/run-state.js +172 -84
- package/dist/card/text-truncate.d.ts +0 -9
- package/dist/card/text-truncate.js +0 -9
- package/dist/card/tool-render.d.ts +4 -1
- package/dist/card/tool-render.js +76 -32
- package/dist/cli.js +2 -2
- package/dist/common/fs.d.ts +14 -0
- package/dist/common/fs.js +57 -3
- package/dist/config/codex-config.js +2 -0
- package/dist/config/index.d.ts +0 -5
- package/dist/config/index.js +0 -11
- package/dist/config/kimi-config.js +1 -0
- package/dist/config/opencode-config.js +13 -2
- package/dist/config/qr.d.ts +36 -0
- package/dist/config/qr.js +124 -0
- package/dist/config/wizard.js +69 -27
- package/dist/connector/index.d.ts +3 -2
- package/dist/connector/index.js +5 -3
- package/dist/index.js +14 -41
- package/dist/instance-lock.js +7 -0
- package/dist/persistence/atomic-write.d.ts +2 -0
- package/dist/persistence/atomic-write.js +20 -5
- package/dist/persistence/load-json-file.d.ts +1 -2
- package/dist/persistence/load-json-file.js +2 -3
- package/dist/platform/agent-stopper.d.ts +22 -0
- package/dist/platform/agent-stopper.js +19 -0
- package/dist/platform/command.d.ts +11 -0
- package/dist/platform/command.js +107 -0
- package/dist/platform/identity.d.ts +33 -0
- package/dist/platform/identity.js +187 -0
- package/dist/platform/path.d.ts +24 -0
- package/dist/platform/path.js +110 -0
- package/dist/platform/select.d.ts +9 -0
- package/dist/platform/select.js +12 -0
- package/dist/platform/shell.d.ts +51 -0
- package/dist/platform/shell.js +75 -0
- package/dist/platform/spawn.d.ts +41 -0
- package/dist/platform/spawn.js +77 -0
- package/dist/platform/terminator-posix.d.ts +38 -0
- package/dist/platform/terminator-posix.js +103 -0
- package/dist/platform/terminator-win32.d.ts +21 -0
- package/dist/platform/terminator-win32.js +110 -0
- package/dist/platform/terminator.d.ts +44 -0
- package/dist/platform/terminator.js +24 -0
- package/dist/platform/types.d.ts +23 -0
- package/dist/platform/types.js +5 -0
- package/dist/restart.js +4 -2
- package/dist/router/card-helpers.d.ts +33 -6
- package/dist/router/card-helpers.js +67 -11
- package/dist/router/config/common/render.d.ts +1 -1
- package/dist/router/config/common/render.js +1 -1
- package/dist/router/config-diff.d.ts +4 -0
- package/dist/router/config-diff.js +70 -0
- package/dist/router/index.d.ts +66 -6
- package/dist/router/index.js +450 -315
- package/dist/router/utils.d.ts +6 -0
- package/dist/router/utils.js +12 -0
- package/dist/runner/bash/runner.d.ts +9 -2
- package/dist/runner/bash/runner.js +45 -20
- package/dist/runner/claude/session.d.ts +4 -1
- package/dist/runner/claude/session.js +18 -4
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
- package/dist/runner/codex/app-server/translator.d.ts +18 -0
- package/dist/runner/codex/app-server/translator.js +146 -0
- package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
- package/dist/runner/common/acp/base-acp-runner.js +248 -0
- package/dist/runner/common/acp/base-translator.d.ts +141 -0
- package/dist/runner/common/acp/base-translator.js +271 -0
- package/dist/runner/common/acp/protocol-helpers.d.ts +39 -0
- package/dist/runner/common/acp/protocol-helpers.js +34 -0
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
- package/dist/runner/common/jsonrpc/transport.js +25 -5
- package/dist/runner/common/process-stopper.d.ts +4 -26
- package/dist/runner/common/process-stopper.js +4 -76
- package/dist/runner/common/runner-utils.d.ts +0 -5
- package/dist/runner/common/runner-utils.js +0 -7
- package/dist/runner/common/spawning-runner.d.ts +42 -132
- package/dist/runner/common/spawning-runner.js +52 -271
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +45 -42
- package/dist/runner/kimi/acp/runner.js +140 -293
- package/dist/runner/kimi/acp/translator.d.ts +18 -100
- package/dist/runner/kimi/acp/translator.js +56 -242
- package/dist/runner/opencode/acp/runner.d.ts +22 -46
- package/dist/runner/opencode/acp/runner.js +73 -256
- package/dist/runner/opencode/acp/translator.d.ts +28 -103
- package/dist/runner/opencode/acp/translator.js +51 -236
- package/dist/runner/pi/rpc/translator.d.ts +3 -0
- package/dist/runner/pi/rpc/translator.js +47 -9
- package/dist/runner/probe.d.ts +8 -11
- package/dist/runner/probe.js +17 -21
- package/dist/runner/registry.d.ts +4 -4
- package/dist/runner/registry.js +4 -4
- package/dist/runner/types.d.ts +38 -1
- package/dist/session/claude/sessions.js +34 -57
- package/dist/session/codex/rollout-reader.d.ts +10 -0
- package/dist/session/codex/rollout-reader.js +123 -41
- package/dist/session/common/constants.d.ts +7 -0
- package/dist/session/common/constants.js +9 -0
- package/dist/session/common/two-pass.d.ts +22 -0
- package/dist/session/common/two-pass.js +70 -0
- package/dist/session/dsh/sessions.js +6 -5
- package/dist/session/kimi/sessions.d.ts +6 -0
- package/dist/session/kimi/sessions.js +50 -73
- package/dist/session/opencode/sessions.js +26 -8
- package/dist/session/pi/sessions.js +54 -73
- package/dist/update/index.d.ts +1 -2
- package/dist/update/index.js +1 -2
- package/dist/update/install.d.ts +2 -1
- package/dist/update/install.js +58 -9
- package/dist/update/version-check.d.ts +5 -0
- package/dist/update/version-check.js +9 -0
- package/package.json +9 -6
- package/dist/update/startup-hint.d.ts +0 -5
- package/dist/update/startup-hint.js +0 -11
package/dist/router/index.js
CHANGED
|
@@ -2,14 +2,15 @@ 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,
|
|
5
|
+
import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgentKey, getAgentConfig, } 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';
|
|
9
9
|
import { resolveAlias } from '../order/alias-resolve.js';
|
|
10
|
+
import { diffConfig, setNestedValue } from './config-diff.js';
|
|
10
11
|
import { getLogger } from '../logger/index.js';
|
|
11
12
|
import { activeRunUsage, clampInt } from './utils.js';
|
|
12
|
-
import { markdownDiv, buildSessionHistoryCard, paginationBar } from './card-helpers.js';
|
|
13
|
+
import { markdownDiv, buildSessionHistoryCard, paginationBar, parseJumpOffset, PAGE_JUMP_INVALID_HINT, } from './card-helpers.js';
|
|
13
14
|
import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
|
|
14
15
|
import { atomicWrite } from '../persistence/atomic-write.js';
|
|
15
16
|
import { checkLatestVersion as defaultCheckLatestVersion, isNewer as defaultIsNewer, runInstallLatest as defaultRunInstallLatest, } from '../update/index.js';
|
|
@@ -48,6 +49,16 @@ function pageSlice(items, offset, pageSize) {
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
const ACTIVE_PAGE_SIZE = 20;
|
|
52
|
+
/** 人类可读字节数(B/KB/MB/GB);/ls 列表与单文件卡共用。 */
|
|
53
|
+
function formatBytes(bytes) {
|
|
54
|
+
if (bytes < 1024)
|
|
55
|
+
return `${bytes}B`;
|
|
56
|
+
if (bytes < 1024 * 1024)
|
|
57
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
58
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
59
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
60
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
61
|
+
}
|
|
51
62
|
/**
|
|
52
63
|
* /order 列表页大小;指令超过此数量时显示分页导航栏。
|
|
53
64
|
* 2026-08-13 实测:每行 2 个元素(div + column_set,行间 hr),20 行 + 分页栏
|
|
@@ -68,6 +79,9 @@ const WS_PAGE_SIZE = 5;
|
|
|
68
79
|
* 归一化卡片分页参数:clamp pageSize 到 [1,maxPageSize],offset 归整为
|
|
69
80
|
* 非负整数并对齐到页边界。非数字 payload(如 'abc')不得产生 NaN slice。
|
|
70
81
|
*
|
|
82
|
+
* 「跳转页码」输入框(paginationBar 内置)的提交值优先于 offset;非法输入
|
|
83
|
+
* 返回 `error` 文案,调用方应回错误 toast 且不刷新卡片。
|
|
84
|
+
*
|
|
71
85
|
* resume.page / active.page 共用,避免两处重复实现分页归一化。
|
|
72
86
|
*/
|
|
73
87
|
function normalizePageArgs(value, maxPageSize) {
|
|
@@ -75,6 +89,13 @@ function normalizePageArgs(value, maxPageSize) {
|
|
|
75
89
|
const pageSize = Number.isFinite(rawPageSize)
|
|
76
90
|
? clampInt(Math.trunc(rawPageSize), 1, maxPageSize)
|
|
77
91
|
: maxPageSize;
|
|
92
|
+
const jump = parseJumpOffset(value.inputValue, pageSize);
|
|
93
|
+
if (jump === null) {
|
|
94
|
+
return { pageSize, alignedOffset: 0, error: PAGE_JUMP_INVALID_HINT };
|
|
95
|
+
}
|
|
96
|
+
if (jump !== undefined) {
|
|
97
|
+
return { pageSize, alignedOffset: Math.floor(jump / pageSize) * pageSize };
|
|
98
|
+
}
|
|
78
99
|
const rawOffset = Number(value.offset);
|
|
79
100
|
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
|
|
80
101
|
return { pageSize, alignedOffset: Math.floor(offset / pageSize) * pageSize };
|
|
@@ -88,6 +109,7 @@ const RESUME_CONTENT_PREFETCH = 5;
|
|
|
88
109
|
*/
|
|
89
110
|
const RESUME_SUMMARY_PLACEHOLDERS = new Set(['', '(no user message)', '(无摘要)', 'New Session']);
|
|
90
111
|
import { newSessionButton, resumeCompactButton, agentDisplayName } from '../card/card-shared.js';
|
|
112
|
+
import { displayName } from '../platform/path.js';
|
|
91
113
|
/**
|
|
92
114
|
* Card-action commands that must run with enqueue({ immediate: true }).
|
|
93
115
|
* These bypass the serial queue and execute immediately.
|
|
@@ -114,46 +136,8 @@ export function isImmediateAction(cmd) {
|
|
|
114
136
|
// help.* wildcard: any command starting with "help."
|
|
115
137
|
if (cmd.startsWith('help.'))
|
|
116
138
|
return true;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
cmd === 'ls.file' ||
|
|
120
|
-
cmd === 'ls.refresh' ||
|
|
121
|
-
cmd === 'ls.browse' ||
|
|
122
|
-
cmd === 'ls.switch' ||
|
|
123
|
-
cmd === 'ls.page' ||
|
|
124
|
-
cmd === 'resume.page' || // control operation: paginate only, never spawns claude
|
|
125
|
-
cmd === 'active.page' || // control operation: paginate active card
|
|
126
|
-
cmd === 'ws.page' || // control operation: paginate /ws list only
|
|
127
|
-
cmd === 'ws.remove' ||
|
|
128
|
-
cmd === 'ws.sort' || // control operation: toggle sort mode only
|
|
129
|
-
cmd === 'resume.use' ||
|
|
130
|
-
cmd === 'ws.use' ||
|
|
131
|
-
cmd === 'queue.immediate' ||
|
|
132
|
-
cmd === 'queue.cancel' ||
|
|
133
|
-
cmd === 'queue.diagnose' ||
|
|
134
|
-
cmd === 'queue.edit' ||
|
|
135
|
-
cmd === 'queue.input' ||
|
|
136
|
-
cmd === 'order.delete' ||
|
|
137
|
-
cmd === 'order.page' ||
|
|
138
|
-
cmd === 'order.aliasEdit' ||
|
|
139
|
-
cmd === 'order.aliasInput' ||
|
|
140
|
-
cmd === 'order.aliasRemove' ||
|
|
141
|
-
cmd === 'order.textEdit' ||
|
|
142
|
-
cmd === 'order.textInput' ||
|
|
143
|
-
cmd === 'config.toggle' ||
|
|
144
|
-
cmd === 'config.set' ||
|
|
145
|
-
cmd === 'config.input' ||
|
|
146
|
-
cmd === 'config.save' ||
|
|
147
|
-
// 审批响应/权限切换必须即时触达在途 run(同 stop 类控制动作)。若走串行
|
|
148
|
-
// 队列会排在等待审批的 run 之后形成死锁:run 不结束审批不执行,run 结束
|
|
149
|
-
// coordinator 已删响应空转(线上复现:approval.respond 排队卡、审批永不生效)。
|
|
150
|
-
cmd === 'approval.respond' ||
|
|
151
|
-
cmd === 'approval.toggle' ||
|
|
152
|
-
cmd === 'approval.answer' ||
|
|
153
|
-
cmd === 'approval.answerSubmit' ||
|
|
154
|
-
cmd === 'approval.answerCustom' ||
|
|
155
|
-
cmd === 'approval.answerNote' ||
|
|
156
|
-
cmd === 'approval.planFeedback');
|
|
139
|
+
// 名单在 APPROVAL_ACTION_SPECS 之后定义(模块初始化完成后才可能被调用)。
|
|
140
|
+
return IMMEDIATE_ACTION_CMDS.has(cmd);
|
|
157
141
|
}
|
|
158
142
|
const QUESTION_INDEX_PRESENT = (v) => v.questionIndex !== undefined;
|
|
159
143
|
const APPROVAL_ACTION_SPECS = {
|
|
@@ -256,6 +240,68 @@ const APPROVAL_ACTION_SPECS = {
|
|
|
256
240
|
failureToast: (msg) => ({ toast: { type: 'error', content: `修改意见保存失败:${msg}` } }),
|
|
257
241
|
},
|
|
258
242
|
};
|
|
243
|
+
// ===========================================================================
|
|
244
|
+
// W2.1 命令名单单一事实源
|
|
245
|
+
// 直返语义(cardAction 同步返回 toast 给飞书回调,index.ts 控制层据此不经
|
|
246
|
+
// 队列直接处理)与即时语义(绕串行队列 enqueueImmediate,§9.6)正交,两个
|
|
247
|
+
// 独立清单只共享审批命令名来源(APPROVAL_ACTION_SPECS keys)——历史上同一
|
|
248
|
+
// 份知识四份拷贝曾两次漂移(order.textInput 漏 immediate、answer 家族漏直返)。
|
|
249
|
+
// ===========================================================================
|
|
250
|
+
/** 审批命令名清单(APPROVAL_ACTION_SPECS keys,handleCardAction 分发共用)。 */
|
|
251
|
+
export const APPROVAL_ACTION_CMDS = Object.keys(APPROVAL_ACTION_SPECS);
|
|
252
|
+
/**
|
|
253
|
+
* 直返命令:点击后同步返回 toast 给飞书回调,不经队列。
|
|
254
|
+
* index.ts 控制层直返清单的唯一来源(勿在别处复制名单)。
|
|
255
|
+
*/
|
|
256
|
+
export const DIRECT_RETURN_CMDS = new Set([
|
|
257
|
+
'queue.input',
|
|
258
|
+
'order.aliasInput',
|
|
259
|
+
'order.aliasRemove',
|
|
260
|
+
'order.textInput',
|
|
261
|
+
'config.save',
|
|
262
|
+
...APPROVAL_ACTION_CMDS,
|
|
263
|
+
]);
|
|
264
|
+
/** 即时命令清单(绕串行队列 enqueueImmediate)。 */
|
|
265
|
+
const IMMEDIATE_ACTION_CMDS = new Set([
|
|
266
|
+
'new-session',
|
|
267
|
+
'stop',
|
|
268
|
+
'ls.file',
|
|
269
|
+
'ls.refresh',
|
|
270
|
+
'ls.browse',
|
|
271
|
+
'ls.switch',
|
|
272
|
+
'ls.page', // control operation: paginate only, never spawns claude
|
|
273
|
+
'resume.page', // control operation: paginate only, never spawns claude
|
|
274
|
+
'active.page', // control operation: paginate active card
|
|
275
|
+
'ws.page', // control operation: paginate /ws list only
|
|
276
|
+
'ws.remove',
|
|
277
|
+
'ws.sort', // control operation: toggle sort mode only
|
|
278
|
+
'resume.use',
|
|
279
|
+
'ws.use',
|
|
280
|
+
'queue.immediate',
|
|
281
|
+
'queue.cancel',
|
|
282
|
+
'queue.diagnose',
|
|
283
|
+
'queue.edit',
|
|
284
|
+
'queue.input',
|
|
285
|
+
'order.delete',
|
|
286
|
+
'order.page',
|
|
287
|
+
'order.aliasEdit',
|
|
288
|
+
'order.aliasInput',
|
|
289
|
+
'order.aliasRemove',
|
|
290
|
+
'order.textEdit',
|
|
291
|
+
'order.textInput',
|
|
292
|
+
'config.toggle',
|
|
293
|
+
'config.set',
|
|
294
|
+
'config.input',
|
|
295
|
+
'config.save',
|
|
296
|
+
// 审批响应/权限切换必须即时触达在途 run(同 stop 类控制动作)。若走串行
|
|
297
|
+
// 队列会排在等待审批的 run 之后形成死锁:run 不结束审批不执行,run 结束
|
|
298
|
+
// coordinator 已删响应空转(线上复现:approval.respond 排队卡、审批永不生效)。
|
|
299
|
+
...APPROVAL_ACTION_CMDS,
|
|
300
|
+
]);
|
|
301
|
+
/** W2.8 单源:payload.offset → 钳位 offset(原先 9 处逐字副本)。 */
|
|
302
|
+
function payloadOffset(value) {
|
|
303
|
+
return Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
304
|
+
}
|
|
259
305
|
/** 卡片 cardAction payload 缺字段的统一报错文案(原先 12 处手写且已漂移出两种前缀)。 */
|
|
260
306
|
const CARD_PAYLOAD_MISSING = '⚠️ 卡片 payload 缺少必要信息';
|
|
261
307
|
export class CommandRouter {
|
|
@@ -401,6 +447,11 @@ export class CommandRouter {
|
|
|
401
447
|
* Validates payload and dispatches to the corresponding command.
|
|
402
448
|
*/
|
|
403
449
|
async handleCardAction(value, ctx) {
|
|
450
|
+
// W2.1:审批家族以 APPROVAL_ACTION_SPECS keys 为单一来源分发
|
|
451
|
+
// (原 7 个 case 与 spec keys 是同一份知识的两份拷贝,已两次漂移)。
|
|
452
|
+
if (APPROVAL_ACTION_CMDS.includes(value.cmd)) {
|
|
453
|
+
return this.handleApprovalAction(value, ctx);
|
|
454
|
+
}
|
|
404
455
|
switch (value.cmd) {
|
|
405
456
|
case 'ls.file':
|
|
406
457
|
await this.cardLsFile(value.path, ctx);
|
|
@@ -508,14 +559,6 @@ export class CommandRouter {
|
|
|
508
559
|
// 调 updateCardInPlace → Feishu API 乱序到达导致 toggle 卡死。
|
|
509
560
|
// 2026-07-18: 返回 enqueueConfigAction 的结果以支持 toast 响应
|
|
510
561
|
return this.enqueueConfigAction(value, ctx);
|
|
511
|
-
case 'approval.respond':
|
|
512
|
-
case 'approval.toggle':
|
|
513
|
-
case 'approval.answer':
|
|
514
|
-
case 'approval.answerSubmit':
|
|
515
|
-
case 'approval.answerCustom':
|
|
516
|
-
case 'approval.answerNote':
|
|
517
|
-
case 'approval.planFeedback':
|
|
518
|
-
return this.handleApprovalAction(value, ctx);
|
|
519
562
|
case 'codex.compact':
|
|
520
563
|
await this.bridge.handleCodexCompact(value, ctx);
|
|
521
564
|
return;
|
|
@@ -955,6 +998,21 @@ export class CommandRouter {
|
|
|
955
998
|
card: { type: 'raw', data: card },
|
|
956
999
|
};
|
|
957
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* 分页卡动作的 offset 解析:优先读取「跳转页码」输入框(paginationBar 内置
|
|
1003
|
+
* 的 CardKit 2.0 input),未输入则回退 `payload.offset`(上一页/下一页按钮)。
|
|
1004
|
+
* 非法页码返回 `{ error }`,调用方应回错误 toast 且不刷新卡片。
|
|
1005
|
+
*/
|
|
1006
|
+
pagingOffset(value, defaultPageSize) {
|
|
1007
|
+
const rawSize = Number(value.pageSize);
|
|
1008
|
+
const pageSize = Number.isFinite(rawSize) && rawSize > 0 ? Math.trunc(rawSize) : defaultPageSize;
|
|
1009
|
+
const jump = parseJumpOffset(value.inputValue, pageSize);
|
|
1010
|
+
if (jump === null)
|
|
1011
|
+
return { error: PAGE_JUMP_INVALID_HINT };
|
|
1012
|
+
if (jump !== undefined)
|
|
1013
|
+
return { offset: jump };
|
|
1014
|
+
return { offset: payloadOffset(value) };
|
|
1015
|
+
}
|
|
958
1016
|
/**
|
|
959
1017
|
* Handle ls.switch: switch cwd to target directory after path validation.
|
|
960
1018
|
*/
|
|
@@ -988,8 +1046,10 @@ export class CommandRouter {
|
|
|
988
1046
|
}
|
|
989
1047
|
// Set cwd + auto-resume + user notification (shared with /cd and /ws use)
|
|
990
1048
|
const notifyResult = this.switchCwdAndNotify(ctx.userId, canonical, ctx);
|
|
991
|
-
// Card channel: refresh ls card in-place (existing behavior preserved)
|
|
992
|
-
|
|
1049
|
+
// Card channel: refresh ls card in-place (existing behavior preserved)。
|
|
1050
|
+
// value.root 是本次 /ls 浏览的起点(绝对路径,不受 cwd 变更影响),透传后
|
|
1051
|
+
// 「返回」仍回到用户最初 /ls 的那个目录。
|
|
1052
|
+
const card = this.cmdLs([canonical], ctx, 0, value.root);
|
|
993
1053
|
await this.bridge.updateCardInPlace(card.card, ctx);
|
|
994
1054
|
// Message channel: send switch confirmation / auto-resume card to chat
|
|
995
1055
|
await this.bridge.sendResult(notifyResult, ctx);
|
|
@@ -1009,11 +1069,11 @@ export class CommandRouter {
|
|
|
1009
1069
|
try {
|
|
1010
1070
|
card =
|
|
1011
1071
|
targetPath && fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()
|
|
1012
|
-
? this.cmdLs([targetPath], ctx, offset)
|
|
1013
|
-
: this.cmdLs([], ctx, offset);
|
|
1072
|
+
? this.cmdLs([targetPath], ctx, offset, value.root)
|
|
1073
|
+
: this.cmdLs([], ctx, offset, value.root);
|
|
1014
1074
|
}
|
|
1015
1075
|
catch {
|
|
1016
|
-
card = this.cmdLs([], ctx, offset);
|
|
1076
|
+
card = this.cmdLs([], ctx, offset, value.root);
|
|
1017
1077
|
}
|
|
1018
1078
|
await this.bridge.updateCardInPlace(card.card, ctx);
|
|
1019
1079
|
}
|
|
@@ -1022,10 +1082,13 @@ export class CommandRouter {
|
|
|
1022
1082
|
*/
|
|
1023
1083
|
async handleLsPage(value, ctx) {
|
|
1024
1084
|
const targetPath = value.path;
|
|
1025
|
-
const offset = value.offset ?? 0;
|
|
1026
1085
|
if (!targetPath) {
|
|
1027
1086
|
return { toast: { type: 'error', content: '卡片 payload 缺少 path' } };
|
|
1028
1087
|
}
|
|
1088
|
+
const paging = this.pagingOffset(value, CommandRouter.LS_PAGE_SIZE);
|
|
1089
|
+
if ('error' in paging) {
|
|
1090
|
+
return { toast: { type: 'error', content: paging.error } };
|
|
1091
|
+
}
|
|
1029
1092
|
const resolvedTarget = path.resolve(targetPath);
|
|
1030
1093
|
// TOCTOU guard (review P2-2): existsSync → statSync can race; without the
|
|
1031
1094
|
// try/catch the throw lands in enqueueImmediate's silent .catch and the
|
|
@@ -1040,7 +1103,7 @@ export class CommandRouter {
|
|
|
1040
1103
|
if (!isDir) {
|
|
1041
1104
|
return { toast: { type: 'error', content: `路径无效: ${resolvedTarget}` } };
|
|
1042
1105
|
}
|
|
1043
|
-
const card = this.cmdLs([resolvedTarget], ctx, offset);
|
|
1106
|
+
const card = this.cmdLs([resolvedTarget], ctx, paging.offset, value.root);
|
|
1044
1107
|
// 直接更新卡片,与 ls.browse/ls.refresh 行为一致
|
|
1045
1108
|
await this.bridge.updateCardInPlace(card.card, ctx);
|
|
1046
1109
|
return { toast: { type: 'success', content: '' } };
|
|
@@ -1055,7 +1118,10 @@ export class CommandRouter {
|
|
|
1055
1118
|
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
1056
1119
|
? resumeAgent
|
|
1057
1120
|
: this.config.defaultAgent;
|
|
1058
|
-
const { pageSize, alignedOffset } = normalizePageArgs(value, RESUME_PAGE_SIZE);
|
|
1121
|
+
const { pageSize, alignedOffset, error } = normalizePageArgs(value, RESUME_PAGE_SIZE);
|
|
1122
|
+
if (error) {
|
|
1123
|
+
return { toast: { type: 'error', content: error } };
|
|
1124
|
+
}
|
|
1059
1125
|
const entry = this.sessionStore.get(ctx.userId);
|
|
1060
1126
|
const cwd = entry?.cwd;
|
|
1061
1127
|
if (!cwd) {
|
|
@@ -1082,7 +1148,10 @@ export class CommandRouter {
|
|
|
1082
1148
|
* Mirrors handleResumePage — updates the same card, never sends a new one.
|
|
1083
1149
|
*/
|
|
1084
1150
|
async handleActivePage(value, ctx) {
|
|
1085
|
-
const { alignedOffset } = normalizePageArgs(value, ACTIVE_PAGE_SIZE);
|
|
1151
|
+
const { alignedOffset, error } = normalizePageArgs(value, ACTIVE_PAGE_SIZE);
|
|
1152
|
+
if (error) {
|
|
1153
|
+
return { toast: { type: 'error', content: error } };
|
|
1154
|
+
}
|
|
1086
1155
|
const result = this.cmdActive([], ctx, alignedOffset);
|
|
1087
1156
|
if (!result.card) {
|
|
1088
1157
|
return { toast: { type: 'error', content: '当前没有正在进行中的任务' } };
|
|
@@ -1105,7 +1174,7 @@ export class CommandRouter {
|
|
|
1105
1174
|
return;
|
|
1106
1175
|
}
|
|
1107
1176
|
// Always reset to page 0 when browsing to a new directory
|
|
1108
|
-
const card = this.cmdLs([resolvedTarget], ctx, 0);
|
|
1177
|
+
const card = this.cmdLs([resolvedTarget], ctx, 0, value.root);
|
|
1109
1178
|
await this.bridge.updateCardInPlace(card.card, ctx);
|
|
1110
1179
|
}
|
|
1111
1180
|
/**
|
|
@@ -1159,6 +1228,51 @@ export class CommandRouter {
|
|
|
1159
1228
|
internalKey: `order-${order.id}-${this.orderExecKeyCounter++}`,
|
|
1160
1229
|
};
|
|
1161
1230
|
}
|
|
1231
|
+
/**
|
|
1232
|
+
* W2.8 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
|
|
1233
|
+
*/
|
|
1234
|
+
rebuildListCard(kind, offset, ctx) {
|
|
1235
|
+
return kind === 'order'
|
|
1236
|
+
? this.cmdOrder([], ctx, offset).card
|
|
1237
|
+
: this.cmdWs([], ctx, offset).card;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* W2.8 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
|
|
1241
|
+
* 翻页/移除/排序动作共用)。返回重建后的卡片;无卡时返回 undefined 且跳过更新。
|
|
1242
|
+
*/
|
|
1243
|
+
async refreshListCard(kind, value, ctx) {
|
|
1244
|
+
const card = this.rebuildListCard(kind, payloadOffset(value), ctx);
|
|
1245
|
+
if (!card)
|
|
1246
|
+
return undefined;
|
|
1247
|
+
await this.bridge.updateCardInPlace(card, ctx);
|
|
1248
|
+
return card;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* W2.8 单源:order 三个 input handler(aliasInput/aliasRemove/textInput)
|
|
1252
|
+
* 的公共骨架——校验 orderId → reload+find → mutate → 成功后 toast + raw 卡。
|
|
1253
|
+
* mutate 返回 error 时仅回 error toast,不重渲染。
|
|
1254
|
+
* 注意:本 helper 只服务「toast + card 进响应体」语义(飞书需用响应体原地
|
|
1255
|
+
* 替换 pre-click 编辑卡);updateCardInPlace 原地刷新走 refreshListCard。
|
|
1256
|
+
*/
|
|
1257
|
+
async mutateOrderAndRefreshCard(value, ctx, opts, mutate) {
|
|
1258
|
+
if (!value.orderId) {
|
|
1259
|
+
return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
|
|
1260
|
+
}
|
|
1261
|
+
this.orderStore.reload();
|
|
1262
|
+
const order = this.orderStore.get().find((o) => o.id === value.orderId);
|
|
1263
|
+
if (!order) {
|
|
1264
|
+
return { toast: { type: 'error', content: opts.missingToast } };
|
|
1265
|
+
}
|
|
1266
|
+
const outcome = mutate(order);
|
|
1267
|
+
if (!outcome.ok) {
|
|
1268
|
+
return { toast: { type: 'error', content: outcome.error } };
|
|
1269
|
+
}
|
|
1270
|
+
const result = this.cmdOrder([], ctx, payloadOffset(value));
|
|
1271
|
+
return {
|
|
1272
|
+
toast: { type: 'success', content: outcome.toast },
|
|
1273
|
+
card: { type: 'raw', data: result.card },
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1162
1276
|
/**
|
|
1163
1277
|
* Handle order.delete: remove the order and update card in place.
|
|
1164
1278
|
*/
|
|
@@ -1173,9 +1287,7 @@ export class CommandRouter {
|
|
|
1173
1287
|
// Delete the order
|
|
1174
1288
|
this.orderStore.remove(orderId);
|
|
1175
1289
|
// Refresh the order list and update card in place, preserving current page
|
|
1176
|
-
|
|
1177
|
-
const result = this.cmdOrder([], ctx, currentOffset);
|
|
1178
|
-
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
1290
|
+
await this.refreshListCard('order', value, ctx);
|
|
1179
1291
|
return { toast: { type: 'success', content: '已删除指令' } };
|
|
1180
1292
|
}
|
|
1181
1293
|
/**
|
|
@@ -1183,10 +1295,12 @@ export class CommandRouter {
|
|
|
1183
1295
|
* Mirrors handleLsPage — updates the same card, never sends a new one.
|
|
1184
1296
|
*/
|
|
1185
1297
|
async handleOrderPage(value, ctx) {
|
|
1186
|
-
const
|
|
1298
|
+
const paging = this.pagingOffset(value, ORDER_PAGE_SIZE);
|
|
1299
|
+
if ('error' in paging) {
|
|
1300
|
+
return { toast: { type: 'error', content: paging.error } };
|
|
1301
|
+
}
|
|
1187
1302
|
// cmdOrder internally clamps stale/out-of-range offsets
|
|
1188
|
-
|
|
1189
|
-
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
1303
|
+
await this.refreshListCard('order', { offset: paging.offset }, ctx);
|
|
1190
1304
|
return { toast: { type: 'success', content: '' } };
|
|
1191
1305
|
}
|
|
1192
1306
|
/**
|
|
@@ -1199,7 +1313,7 @@ export class CommandRouter {
|
|
|
1199
1313
|
*/
|
|
1200
1314
|
async showOrderEditCard(value, ctx, opts) {
|
|
1201
1315
|
const orderId = value.orderId;
|
|
1202
|
-
const offset =
|
|
1316
|
+
const offset = payloadOffset(value);
|
|
1203
1317
|
if (!orderId) {
|
|
1204
1318
|
return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
|
|
1205
1319
|
}
|
|
@@ -1277,62 +1391,36 @@ export class CommandRouter {
|
|
|
1277
1391
|
* 留空 = 解绑;非空按命名规则 + 全局唯一校验,失败仅 toast 不重渲染。
|
|
1278
1392
|
*/
|
|
1279
1393
|
async handleOrderAliasInput(value, ctx) {
|
|
1280
|
-
const orderId = value.orderId;
|
|
1281
|
-
const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
1282
1394
|
const name = value.inputValue ?? value.formValue?.['aliasName'];
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
return { toast: { type: 'error', content: err.message } };
|
|
1300
|
-
}
|
|
1301
|
-
// 成功:重渲染列表卡(保持页码)。必须把 card 放进 callback 响应体让飞书
|
|
1302
|
-
// 原地替换 pre-click 编辑卡——toast-only 响应会停留在编辑界面(飞书保留
|
|
1303
|
-
// 点击前那张卡),见 handleQueueInput 同款注释。
|
|
1304
|
-
const result = this.cmdOrder([], ctx, offset);
|
|
1305
|
-
return {
|
|
1306
|
-
toast: {
|
|
1307
|
-
type: 'success',
|
|
1308
|
-
content: trimmed === '' ? `✅ 已移除别名 $${prevAlias ?? ''}` : `✅ 已绑定别名 $${trimmed}`,
|
|
1309
|
-
},
|
|
1310
|
-
card: { type: 'raw', data: result.card },
|
|
1311
|
-
};
|
|
1395
|
+
return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: '指令不存在或已被删除' }, (order) => {
|
|
1396
|
+
const trimmed = name?.trim() ?? '';
|
|
1397
|
+
// 变更前快照旧别名:setAlias 会原地变异 order 对象(delete entry.alias),
|
|
1398
|
+
// 删除后 order.alias 已为 undefined,直接用会让成功文案显示 `$undefined`。
|
|
1399
|
+
const prevAlias = order.alias;
|
|
1400
|
+
try {
|
|
1401
|
+
this.orderStore.setAlias(order.id, trimmed === '' ? undefined : trimmed);
|
|
1402
|
+
}
|
|
1403
|
+
catch (err) {
|
|
1404
|
+
return { ok: false, error: err.message };
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
ok: true,
|
|
1408
|
+
toast: trimmed === '' ? `✅ 已移除别名 $${prevAlias ?? ''}` : `✅ 已绑定别名 $${trimmed}`,
|
|
1409
|
+
};
|
|
1410
|
+
});
|
|
1312
1411
|
}
|
|
1313
1412
|
/** Handle order.aliasRemove: 删除某条指令的别名并原地重渲染列表卡。 */
|
|
1314
1413
|
async handleOrderAliasRemove(value, ctx) {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
return {
|
|
1324
|
-
}
|
|
1325
|
-
// 变更前快照旧别名:setAlias(undefined) 会原地变异 order(delete alias),
|
|
1326
|
-
// 删除后 order.alias 已为 undefined,直接拼会让成功文案显示 `$undefined`。
|
|
1327
|
-
const removedAlias = order.alias;
|
|
1328
|
-
this.orderStore.setAlias(order.id, undefined);
|
|
1329
|
-
// 必须把 card 放进 callback 响应体让飞书原地替换 pre-click 卡片,否则删除
|
|
1330
|
-
// 后卡片停留在旧状态(别名视觉上未消失)。同 handleQueueInput 语义。
|
|
1331
|
-
const result = this.cmdOrder([], ctx, offset);
|
|
1332
|
-
return {
|
|
1333
|
-
toast: { type: 'success', content: `✅ 已移除别名 $${removedAlias}` },
|
|
1334
|
-
card: { type: 'raw', data: result.card },
|
|
1335
|
-
};
|
|
1414
|
+
return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: '指令不存在或没有别名' }, (order) => {
|
|
1415
|
+
if (!order.alias) {
|
|
1416
|
+
return { ok: false, error: '指令不存在或没有别名' };
|
|
1417
|
+
}
|
|
1418
|
+
// 变更前快照旧别名:setAlias(undefined) 会原地变异 order(delete alias),
|
|
1419
|
+
// 删除后 order.alias 已为 undefined,直接拼会让成功文案显示 `$undefined`。
|
|
1420
|
+
const removedAlias = order.alias;
|
|
1421
|
+
this.orderStore.setAlias(order.id, undefined);
|
|
1422
|
+
return { ok: true, toast: `✅ 已移除别名 $${removedAlias}` };
|
|
1423
|
+
});
|
|
1336
1424
|
}
|
|
1337
1425
|
/**
|
|
1338
1426
|
* Handle order.textEdit: 展示指令文本编辑卡(input + ✓ 提交图标)。
|
|
@@ -1365,37 +1453,22 @@ export class CommandRouter {
|
|
|
1365
1453
|
* 保留 usedAt / alias / createdAt(OrderStore.updateText 只动 text 字段)。
|
|
1366
1454
|
*/
|
|
1367
1455
|
async handleOrderTextInput(value, ctx) {
|
|
1368
|
-
const orderId = value.orderId;
|
|
1369
|
-
const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
1370
1456
|
const raw = value.inputValue ?? value.formValue?.['text'];
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
return {
|
|
1385
|
-
}
|
|
1386
|
-
if (!updated) {
|
|
1387
|
-
// reload + find 后 updateText 不应返回 undefined;防御性兜底(如并发删除)。
|
|
1388
|
-
return { toast: { type: 'error', content: '指令不存在或已被删除' } };
|
|
1389
|
-
}
|
|
1390
|
-
// 成功:重渲染列表卡(保持页码)。必须把 card 放进 callback 响应体让飞书
|
|
1391
|
-
// 原地替换 pre-click 编辑卡——toast-only 响应会停留在编辑界面(飞书保留
|
|
1392
|
-
// 点击前那张卡),见 handleQueueInput / handleOrderAliasInput 同款注释。
|
|
1393
|
-
const result = this.cmdOrder([], ctx, offset);
|
|
1394
|
-
const preview = updated.text.length > 50 ? updated.text.slice(0, 50) + '...' : updated.text;
|
|
1395
|
-
return {
|
|
1396
|
-
toast: { type: 'success', content: `✅ 已更新指令: ${preview}` },
|
|
1397
|
-
card: { type: 'raw', data: result.card },
|
|
1398
|
-
};
|
|
1457
|
+
return this.mutateOrderAndRefreshCard(value, ctx, { missingToast: '指令不存在或已被删除' }, (order) => {
|
|
1458
|
+
let updated;
|
|
1459
|
+
try {
|
|
1460
|
+
updated = this.orderStore.updateText(order.id, raw ?? '');
|
|
1461
|
+
}
|
|
1462
|
+
catch (err) {
|
|
1463
|
+
return { ok: false, error: err.message };
|
|
1464
|
+
}
|
|
1465
|
+
if (!updated) {
|
|
1466
|
+
// reload + find 后 updateText 不应返回 undefined;防御性兜底(如并发删除)。
|
|
1467
|
+
return { ok: false, error: '指令不存在或已被删除' };
|
|
1468
|
+
}
|
|
1469
|
+
const preview = updated.text.length > 50 ? updated.text.slice(0, 50) + '...' : updated.text;
|
|
1470
|
+
return { ok: true, toast: `✅ 已更新指令: ${preview}` };
|
|
1471
|
+
});
|
|
1399
1472
|
}
|
|
1400
1473
|
/**
|
|
1401
1474
|
* Handle ws.use card action: switch to the workspace, then refresh the /ws
|
|
@@ -1427,11 +1500,7 @@ export class CommandRouter {
|
|
|
1427
1500
|
}
|
|
1428
1501
|
}
|
|
1429
1502
|
// Refresh the /ws list card in place so "recent" sort is immediately visible
|
|
1430
|
-
|
|
1431
|
-
const refreshed = this.cmdWs([], ctx, currentOffset);
|
|
1432
|
-
if (refreshed.card) {
|
|
1433
|
-
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
1434
|
-
}
|
|
1503
|
+
await this.refreshListCard('ws', value, ctx);
|
|
1435
1504
|
// Toast feedback (suppress text when a persistent message was already sent)
|
|
1436
1505
|
return {
|
|
1437
1506
|
toast: {
|
|
@@ -1455,9 +1524,7 @@ export class CommandRouter {
|
|
|
1455
1524
|
this.cmdWs(['remove', name], ctx);
|
|
1456
1525
|
// Rebuild the /ws list card and update it in place, preserving the page
|
|
1457
1526
|
// the user was on when they clicked 删除.
|
|
1458
|
-
|
|
1459
|
-
const refreshed = this.cmdWs([], ctx, currentOffset);
|
|
1460
|
-
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
1527
|
+
await this.refreshListCard('ws', value, ctx);
|
|
1461
1528
|
return { toast: { type: 'success', content: `已删除 workspace "${name}"` } };
|
|
1462
1529
|
}
|
|
1463
1530
|
/**
|
|
@@ -1465,10 +1532,12 @@ export class CommandRouter {
|
|
|
1465
1532
|
* Mirrors handleOrderPage — updates the same card, never sends a new one.
|
|
1466
1533
|
*/
|
|
1467
1534
|
async handleWsPage(value, ctx) {
|
|
1468
|
-
const
|
|
1535
|
+
const paging = this.pagingOffset(value, WS_PAGE_SIZE);
|
|
1536
|
+
if ('error' in paging) {
|
|
1537
|
+
return { toast: { type: 'error', content: paging.error } };
|
|
1538
|
+
}
|
|
1469
1539
|
// cmdWs internally clamps stale/out-of-range offsets
|
|
1470
|
-
|
|
1471
|
-
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
1540
|
+
await this.refreshListCard('ws', { offset: paging.offset }, ctx);
|
|
1472
1541
|
return { toast: { type: 'success', content: '' } };
|
|
1473
1542
|
}
|
|
1474
1543
|
/**
|
|
@@ -1480,8 +1549,7 @@ export class CommandRouter {
|
|
|
1480
1549
|
const current = this.wsSortPreference.get(userId) ?? 'recent';
|
|
1481
1550
|
const next = current === 'recent' ? 'alpha' : 'recent';
|
|
1482
1551
|
this.wsSortPreference.set(userId, next);
|
|
1483
|
-
|
|
1484
|
-
await this.bridge.updateCardInPlace(result.card, ctx);
|
|
1552
|
+
await this.refreshListCard('ws', { offset: 0 }, ctx);
|
|
1485
1553
|
const nextLabel = next === 'recent' ? '🕐 最近使用' : '🔤 字母顺序';
|
|
1486
1554
|
return {
|
|
1487
1555
|
toast: { type: 'success', content: `已切换为 ${nextLabel}` },
|
|
@@ -1495,26 +1563,42 @@ export class CommandRouter {
|
|
|
1495
1563
|
await this.bridge.sendResult({ text: '卡片 payload 缺少 path' }, ctx);
|
|
1496
1564
|
return;
|
|
1497
1565
|
}
|
|
1498
|
-
const
|
|
1499
|
-
if (
|
|
1500
|
-
await this.bridge.sendResult(
|
|
1501
|
-
|
|
1566
|
+
const result = await this.deliverFileToFeishu(path.resolve(target), ctx);
|
|
1567
|
+
if (result)
|
|
1568
|
+
await this.bridge.sendResult(result, ctx);
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* 校验并发送本地文件到飞书(≤30MB)。成功返回 null(文件已发出),失败返回
|
|
1572
|
+
* 错误 CommandResult 由调用方展示。
|
|
1573
|
+
*
|
|
1574
|
+
* `/download` 与 `ls.file` 卡片按钮共用,避免两处重复 30MB/存在性校验。
|
|
1575
|
+
*/
|
|
1576
|
+
async deliverFileToFeishu(resolved, ctx) {
|
|
1577
|
+
if (!fs.existsSync(resolved)) {
|
|
1578
|
+
return { text: `文件不存在: ${resolved}` };
|
|
1502
1579
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1580
|
+
let stat;
|
|
1581
|
+
try {
|
|
1582
|
+
stat = fs.statSync(resolved);
|
|
1583
|
+
}
|
|
1584
|
+
catch {
|
|
1585
|
+
return { text: `无法读取: ${resolved}` };
|
|
1586
|
+
}
|
|
1587
|
+
if (stat.isDirectory()) {
|
|
1588
|
+
return { text: `这是一个目录,不能作为文件发送: ${resolved}\n用 /ls 查看目录内容` };
|
|
1507
1589
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1590
|
+
if (!stat.isFile()) {
|
|
1591
|
+
return { text: `不是普通文件: ${resolved}` };
|
|
1592
|
+
}
|
|
1593
|
+
// 30MB,与飞书 im/v1/files API 对齐
|
|
1510
1594
|
if (stat.size > MAX_FILE_UPLOAD_SIZE) {
|
|
1511
1595
|
const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
|
|
1512
|
-
|
|
1596
|
+
return {
|
|
1513
1597
|
text: `文件太大 (${sizeMB}MB),超过 ${MAX_FILE_UPLOAD_SIZE / (1024 * 1024)}MB 限制不能发送`,
|
|
1514
|
-
}
|
|
1515
|
-
return;
|
|
1598
|
+
};
|
|
1516
1599
|
}
|
|
1517
|
-
await this.bridge.sendFile(
|
|
1600
|
+
await this.bridge.sendFile(resolved, ctx);
|
|
1601
|
+
return null;
|
|
1518
1602
|
}
|
|
1519
1603
|
// --- PendingConfig 辅助方法 ---
|
|
1520
1604
|
/** 确保 pendingConfig 已初始化(从当前 config 克隆) */
|
|
@@ -1553,6 +1637,8 @@ export class CommandRouter {
|
|
|
1553
1637
|
async dispatchConfigAction(value, ctx) {
|
|
1554
1638
|
switch (value.cmd) {
|
|
1555
1639
|
case 'config.toggle': {
|
|
1640
|
+
// 通用 boolean 翻转:目前无卡片字段使用(output.show* 已移除),
|
|
1641
|
+
// 保留供未来 boolean 配置项复用;configActionQueue 串行化覆盖连击。
|
|
1556
1642
|
const key = value.key;
|
|
1557
1643
|
if (!key) {
|
|
1558
1644
|
await this.bridge.sendResult({ text: '缺少配置项 key' }, ctx);
|
|
@@ -1682,68 +1768,10 @@ export class CommandRouter {
|
|
|
1682
1768
|
/** 设置嵌套属性值(仅修改内存对象,不写盘) */
|
|
1683
1769
|
/** 在 pendingConfig 上按 dot-separated key 设置嵌套值(config 卡片编辑用)。 */
|
|
1684
1770
|
setNestedValue(target, key, value) {
|
|
1685
|
-
|
|
1686
|
-
// pi.xxx/codex.xxx/opencode.xxx → agents.xxx,claude.xxx 保持顶层
|
|
1687
|
-
const mappedKey = mapAgentKey(key);
|
|
1688
|
-
const parts = mappedKey.split('.');
|
|
1689
|
-
// 复用 config 模块的 walkNestedContainer(含 __proto__/prototype/constructor
|
|
1690
|
-
// 守卫与中间段补 {}),router 只保留 value=undefined → delete 分支。
|
|
1691
|
-
const container = walkNestedContainer(target, parts, true);
|
|
1692
|
-
const lastPart = parts[parts.length - 1];
|
|
1693
|
-
assertSafeKeyPart(lastPart);
|
|
1694
|
-
if (value === undefined) {
|
|
1695
|
-
// value=undefined 表示"删除键"(如清空 reasoningEffort),
|
|
1696
|
-
// 不能写成 undefined 值——diffConfig 会把 undefined 转成字面量 "undefined"
|
|
1697
|
-
// 写入 config.yaml 并透传给 codex(ReasoningEffort::Custom("undefined"))。
|
|
1698
|
-
delete container[lastPart];
|
|
1699
|
-
}
|
|
1700
|
-
else {
|
|
1701
|
-
container[lastPart] = value;
|
|
1702
|
-
}
|
|
1771
|
+
setNestedValue(target, key, value);
|
|
1703
1772
|
}
|
|
1704
|
-
/** 对比原始 config 与 pendingConfig,返回变化的 key→string 映射 */
|
|
1705
|
-
/** config diff(public:测试直接调用,替代 as unknown as)。 */
|
|
1706
1773
|
diffConfig(original, pending) {
|
|
1707
|
-
|
|
1708
|
-
this.collectDiff('', original, pending, updates);
|
|
1709
|
-
return updates;
|
|
1710
|
-
}
|
|
1711
|
-
/** 递归收集差异 */
|
|
1712
|
-
collectDiff(prefix, original, pending, result) {
|
|
1713
|
-
if (original === pending)
|
|
1714
|
-
return;
|
|
1715
|
-
// 如果 pending 是对象(非 null)但 original 不是对象,
|
|
1716
|
-
// 把 original 当作空对象递归进入 pending 内部,
|
|
1717
|
-
// 避免把整个对象转成 "[object Object]" 字符串
|
|
1718
|
-
if (typeof pending === 'object' &&
|
|
1719
|
-
pending !== null &&
|
|
1720
|
-
(typeof original !== 'object' || original === null)) {
|
|
1721
|
-
const pendObj = pending;
|
|
1722
|
-
for (const k of Object.keys(pendObj)) {
|
|
1723
|
-
const newKey = prefix ? `${prefix}.${k}` : k;
|
|
1724
|
-
this.collectDiff(newKey, undefined, pendObj[k], result);
|
|
1725
|
-
}
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
if (typeof original !== 'object' ||
|
|
1729
|
-
typeof pending !== 'object' ||
|
|
1730
|
-
original === null ||
|
|
1731
|
-
pending === null) {
|
|
1732
|
-
// 叶子节点,记录差异
|
|
1733
|
-
const key = prefix || 'root';
|
|
1734
|
-
// pending 为 undefined 表示键被删除,必须保留 undefined 语义,
|
|
1735
|
-
// 不能 String(pending) 成 "undefined"
|
|
1736
|
-
result[key] = pending === undefined ? undefined : String(pending);
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
// 两者都是对象,递归比较
|
|
1740
|
-
const origObj = original;
|
|
1741
|
-
const pendObj = pending;
|
|
1742
|
-
const allKeys = new Set([...Object.keys(origObj), ...Object.keys(pendObj)]);
|
|
1743
|
-
for (const k of allKeys) {
|
|
1744
|
-
const newKey = prefix ? `${prefix}.${k}` : k;
|
|
1745
|
-
this.collectDiff(newKey, origObj[k], pendObj[k], result);
|
|
1746
|
-
}
|
|
1774
|
+
return diffConfig(original, pending);
|
|
1747
1775
|
}
|
|
1748
1776
|
/**
|
|
1749
1777
|
* config 写盘后的运行时传播(P1-6:自 config.save 卡片路径抽出,卡片保存与
|
|
@@ -1925,6 +1953,9 @@ export class CommandRouter {
|
|
|
1925
1953
|
return this.cmdCd(args, ctx);
|
|
1926
1954
|
case 'ls':
|
|
1927
1955
|
return this.cmdLs(args, ctx);
|
|
1956
|
+
case 'download':
|
|
1957
|
+
case 'd':
|
|
1958
|
+
return await this.cmdDownload(args, ctx);
|
|
1928
1959
|
case 'ws':
|
|
1929
1960
|
return this.cmdWs(args, ctx);
|
|
1930
1961
|
case 'resume':
|
|
@@ -1969,7 +2000,16 @@ export class CommandRouter {
|
|
|
1969
2000
|
// 文本组(需带参数,不宜用按钮;以纯文本行展示)
|
|
1970
2001
|
const textCommands = [
|
|
1971
2002
|
{ cmd: 'cd', label: '/cd <path>', desc: '切换工作目录' },
|
|
1972
|
-
{
|
|
2003
|
+
{
|
|
2004
|
+
cmd: 'ls',
|
|
2005
|
+
label: '/ls [dir|file]',
|
|
2006
|
+
desc: '列出当前目录(可指定子目录);指定文件时列出该文件',
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
cmd: 'download',
|
|
2010
|
+
label: '/download /d <path>',
|
|
2011
|
+
desc: '直接下载(发送)指定文件到飞书,上限 30MB',
|
|
2012
|
+
},
|
|
1973
2013
|
{ cmd: 'resume', label: '/resume /r [agent] [list|id|N]', desc: '列出或切换 Agent session' },
|
|
1974
2014
|
{
|
|
1975
2015
|
cmd: 'order',
|
|
@@ -2469,8 +2509,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2469
2509
|
return actions;
|
|
2470
2510
|
}
|
|
2471
2511
|
static LS_PAGE_SIZE = 30;
|
|
2472
|
-
/**
|
|
2473
|
-
|
|
2512
|
+
/**
|
|
2513
|
+
* `/ls` 实现(public:测试直接调用,替代 as unknown as)。
|
|
2514
|
+
*
|
|
2515
|
+
* @param args [] 列出 cwd;[path] 列出指定目录(或文件)
|
|
2516
|
+
* @param offset 分页起点(条目数)
|
|
2517
|
+
* @param rootDir 浏览起点:卡片「返回」按钮回到这里。省略时默认 = 本次列出的
|
|
2518
|
+
* 目录(`/ls <dir>` 的起点即该目录),因此「返回」不会把用户
|
|
2519
|
+
* 丢回 workspace cwd(用户明确反馈过)。
|
|
2520
|
+
*/
|
|
2521
|
+
cmdLs(args, ctx, offset = 0, rootDir) {
|
|
2474
2522
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
2475
2523
|
if (!cwd) {
|
|
2476
2524
|
return { text: '请先使用 /cd <path> 设置工作目录' };
|
|
@@ -2490,11 +2538,25 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2490
2538
|
if (!fs.existsSync(potentialDir)) {
|
|
2491
2539
|
return { text: `ls: ${args[0]}: No such file or directory` };
|
|
2492
2540
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2541
|
+
let isDir;
|
|
2542
|
+
try {
|
|
2543
|
+
isDir = fs.statSync(potentialDir).isDirectory();
|
|
2544
|
+
}
|
|
2545
|
+
catch {
|
|
2546
|
+
// TOCTOU:existsSync 与 statSync 之间目标被删
|
|
2547
|
+
return { text: `ls: ${args[0]}: No such file or directory` };
|
|
2548
|
+
}
|
|
2549
|
+
if (!isDir) {
|
|
2550
|
+
// 命中文件:列出该文件本身(用户可能想下载它或确认是否存在),
|
|
2551
|
+
// 不再回 `ls: xxx: Not a directory` 把文件路径当错误。
|
|
2552
|
+
return this.buildFileCard(potentialDir);
|
|
2495
2553
|
}
|
|
2496
2554
|
targetDir = potentialDir;
|
|
2497
2555
|
}
|
|
2556
|
+
// 浏览起点(「返回」按钮的目标):`/ls <dir>` 的起点就是该目录本身,之后
|
|
2557
|
+
// 进入子目录/翻页/刷新时由卡片 payload 的 root 一路带过来。省略时 = 本次
|
|
2558
|
+
// 列出的目录,因此「返回」永远不会把用户丢回 workspace cwd。
|
|
2559
|
+
const browseRoot = rootDir ? path.resolve(cwd, rootDir) : targetDir;
|
|
2498
2560
|
try {
|
|
2499
2561
|
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
2500
2562
|
// Separate directories and files
|
|
@@ -2516,7 +2578,10 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2516
2578
|
// Check if we need to show parent directory button
|
|
2517
2579
|
const parentDir = path.dirname(targetDir);
|
|
2518
2580
|
const hasParent = parentDir !== targetDir;
|
|
2581
|
+
// 「切换」相对 cwd(把工作目录切到当前浏览目录)
|
|
2519
2582
|
const isSubdir = targetDir !== cwd;
|
|
2583
|
+
// 「返回」相对浏览起点:在 `/ls <dir>` 的起点上不显示(无路可返)
|
|
2584
|
+
const canReturn = targetDir !== browseRoot;
|
|
2520
2585
|
// Build header with parent button
|
|
2521
2586
|
const headerElements = [];
|
|
2522
2587
|
if (hasParent) {
|
|
@@ -2525,17 +2590,21 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2525
2590
|
text: { tag: 'plain_text', content: '上级' },
|
|
2526
2591
|
type: 'default',
|
|
2527
2592
|
size: 'small',
|
|
2528
|
-
behaviors: [
|
|
2593
|
+
behaviors: [
|
|
2594
|
+
{ type: 'callback', value: { cmd: 'ls.browse', path: parentDir, root: browseRoot } },
|
|
2595
|
+
],
|
|
2529
2596
|
});
|
|
2530
2597
|
}
|
|
2531
|
-
// Show "返回" button when
|
|
2532
|
-
if (
|
|
2598
|
+
// Show "返回" button when browsing away from the /ls root (回到 /ls 指定的目录)
|
|
2599
|
+
if (canReturn) {
|
|
2533
2600
|
headerElements.push({
|
|
2534
2601
|
tag: 'button',
|
|
2535
2602
|
text: { tag: 'plain_text', content: '返回' },
|
|
2536
2603
|
type: 'default',
|
|
2537
2604
|
size: 'small',
|
|
2538
|
-
behaviors: [
|
|
2605
|
+
behaviors: [
|
|
2606
|
+
{ type: 'callback', value: { cmd: 'ls.browse', path: browseRoot, root: browseRoot } },
|
|
2607
|
+
],
|
|
2539
2608
|
});
|
|
2540
2609
|
}
|
|
2541
2610
|
headerElements.push({
|
|
@@ -2544,7 +2613,15 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2544
2613
|
type: 'default',
|
|
2545
2614
|
size: 'small',
|
|
2546
2615
|
behaviors: [
|
|
2547
|
-
{
|
|
2616
|
+
{
|
|
2617
|
+
type: 'callback',
|
|
2618
|
+
value: {
|
|
2619
|
+
cmd: 'ls.refresh',
|
|
2620
|
+
path: targetDir,
|
|
2621
|
+
offset: safeOffset,
|
|
2622
|
+
root: browseRoot,
|
|
2623
|
+
},
|
|
2624
|
+
},
|
|
2548
2625
|
],
|
|
2549
2626
|
});
|
|
2550
2627
|
// Show "切换" button when viewing a subdirectory (to switch cwd to this directory)
|
|
@@ -2554,28 +2631,32 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2554
2631
|
text: { tag: 'plain_text', content: '切换' },
|
|
2555
2632
|
type: 'primary',
|
|
2556
2633
|
size: 'small',
|
|
2557
|
-
|
|
2634
|
+
// 切换只改 cwd,**不重置浏览起点**:root 一路带到切换后的卡片,
|
|
2635
|
+
// 否则「返回」会退化成「回到刚切换到的目录」(用户反馈的缺陷)。
|
|
2636
|
+
behaviors: [
|
|
2637
|
+
{ type: 'callback', value: { cmd: 'ls.switch', path: targetDir, root: browseRoot } },
|
|
2638
|
+
],
|
|
2558
2639
|
});
|
|
2559
2640
|
}
|
|
2560
|
-
const formatSize = (bytes) => {
|
|
2561
|
-
if (bytes < 1024)
|
|
2562
|
-
return `${bytes}B`;
|
|
2563
|
-
if (bytes < 1024 * 1024)
|
|
2564
|
-
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
2565
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
2566
|
-
};
|
|
2567
2641
|
// Helper to create directory button - use targetDir
|
|
2568
2642
|
const dirButton = (name) => ({
|
|
2569
2643
|
tag: 'button',
|
|
2570
2644
|
text: { tag: 'plain_text', content: `📁 ${name}` },
|
|
2571
2645
|
type: 'default',
|
|
2572
2646
|
behaviors: [
|
|
2573
|
-
{
|
|
2647
|
+
{
|
|
2648
|
+
type: 'callback',
|
|
2649
|
+
value: {
|
|
2650
|
+
cmd: 'ls.browse',
|
|
2651
|
+
path: path.join(targetDir, name),
|
|
2652
|
+
root: browseRoot,
|
|
2653
|
+
},
|
|
2654
|
+
},
|
|
2574
2655
|
],
|
|
2575
2656
|
});
|
|
2576
2657
|
// Helper to create file button with size - use targetDir
|
|
2577
2658
|
const fileButton = (name, size) => {
|
|
2578
|
-
const sizeStr = size !== undefined ? ` (${
|
|
2659
|
+
const sizeStr = size !== undefined ? ` (${formatBytes(size)})` : '';
|
|
2579
2660
|
return {
|
|
2580
2661
|
tag: 'button',
|
|
2581
2662
|
text: { tag: 'plain_text', content: `📄 ${name}${sizeStr}` },
|
|
@@ -2589,10 +2670,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2589
2670
|
// Build each section's elements (no tabs - use section headers)
|
|
2590
2671
|
// Note: div+elements is not supported in CardKit 2.0, use column_set instead
|
|
2591
2672
|
const elements = [];
|
|
2592
|
-
// Status line
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
: `\n共 ${dirs.length} 目录, ${files.length} 文件`;
|
|
2673
|
+
// Status line:只报条目构成;页码/总数交给分页栏文案行(同一信息不重复
|
|
2674
|
+
// 渲染两遍——2026-09-10 分页栏改成整行文案后重复会很明显)。
|
|
2675
|
+
const status = `\n共 ${dirs.length} 目录, ${files.length} 文件`;
|
|
2596
2676
|
// Header info + navigation buttons - show targetDir in header
|
|
2597
2677
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: `\`${targetDir}\`${status}` } });
|
|
2598
2678
|
elements.push({
|
|
@@ -2656,11 +2736,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2656
2736
|
offset: safeOffset,
|
|
2657
2737
|
pageSize: CommandRouter.LS_PAGE_SIZE,
|
|
2658
2738
|
total: totalCount,
|
|
2659
|
-
extra: { path: targetDir },
|
|
2739
|
+
extra: { path: targetDir, root: browseRoot },
|
|
2660
2740
|
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 项)`,
|
|
2661
2741
|
});
|
|
2662
2742
|
elements.push({ tag: 'hr' });
|
|
2663
|
-
elements.push(bar);
|
|
2743
|
+
elements.push(...bar);
|
|
2664
2744
|
}
|
|
2665
2745
|
return {
|
|
2666
2746
|
card: {
|
|
@@ -2680,6 +2760,99 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2680
2760
|
return { text: `读取目录失败: ${err.message}` };
|
|
2681
2761
|
}
|
|
2682
2762
|
}
|
|
2763
|
+
/**
|
|
2764
|
+
* `/download <path>`(别名 `/d`):把本地文件直接发送到飞书。
|
|
2765
|
+
*
|
|
2766
|
+
* 路径解析与 `/ls` 一致:`~` 展开到 home;相对路径基于当前 cwd(无 cwd 时
|
|
2767
|
+
* 必须给绝对路径)。成功时文件本身即回复,返回 null(不额外发文本)。
|
|
2768
|
+
*/
|
|
2769
|
+
async cmdDownload(args, ctx) {
|
|
2770
|
+
const raw = args.join(' ').trim();
|
|
2771
|
+
if (raw === '') {
|
|
2772
|
+
return { text: '用法: /download <path>(别名 /d)' };
|
|
2773
|
+
}
|
|
2774
|
+
// 路径可能含空格:args 按空白切分后再拼回(未加引号也能还原原路径)
|
|
2775
|
+
let input = raw;
|
|
2776
|
+
if (input.startsWith('~')) {
|
|
2777
|
+
input = path.join(os.homedir(), input.slice(1));
|
|
2778
|
+
}
|
|
2779
|
+
if (!path.isAbsolute(input)) {
|
|
2780
|
+
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
2781
|
+
if (!cwd)
|
|
2782
|
+
return { text: '请先使用 /cd <path> 设置工作目录' };
|
|
2783
|
+
input = path.resolve(cwd, input);
|
|
2784
|
+
}
|
|
2785
|
+
return this.deliverFileToFeishu(path.resolve(input), ctx);
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* 单文件 ls 结果卡:路径 / 大小 / 修改时间 + 「下载」+「上级」。
|
|
2789
|
+
*
|
|
2790
|
+
* `/ls <file>` 命中普通文件时使用——用户可能想下载该文件或确认它是否存在,
|
|
2791
|
+
* 此前直接回 `ls: xxx: Not a directory` 把文件路径当成错误处理。
|
|
2792
|
+
*/
|
|
2793
|
+
buildFileCard(filePath) {
|
|
2794
|
+
let stat;
|
|
2795
|
+
try {
|
|
2796
|
+
stat = fs.statSync(filePath);
|
|
2797
|
+
}
|
|
2798
|
+
catch {
|
|
2799
|
+
stat = undefined;
|
|
2800
|
+
}
|
|
2801
|
+
const sizeText = stat ? formatBytes(stat.size) : '未知';
|
|
2802
|
+
const mtimeText = stat ? stat.mtime.toLocaleString('zh-CN', { hour12: false }) : '未知';
|
|
2803
|
+
const parentDir = path.dirname(filePath);
|
|
2804
|
+
const buttons = [
|
|
2805
|
+
{
|
|
2806
|
+
tag: 'button',
|
|
2807
|
+
text: { tag: 'plain_text', content: '📎 下载' },
|
|
2808
|
+
type: 'primary',
|
|
2809
|
+
size: 'small',
|
|
2810
|
+
behaviors: [{ type: 'callback', value: { cmd: 'ls.file', path: filePath } }],
|
|
2811
|
+
},
|
|
2812
|
+
];
|
|
2813
|
+
if (parentDir !== filePath) {
|
|
2814
|
+
buttons.push({
|
|
2815
|
+
tag: 'button',
|
|
2816
|
+
text: { tag: 'plain_text', content: '上级' },
|
|
2817
|
+
type: 'default',
|
|
2818
|
+
size: 'small',
|
|
2819
|
+
// 浏览起点 = 文件所在目录:进入后「返回」回到这里,不回 workspace cwd
|
|
2820
|
+
behaviors: [
|
|
2821
|
+
{ type: 'callback', value: { cmd: 'ls.browse', path: parentDir, root: parentDir } },
|
|
2822
|
+
],
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
return {
|
|
2826
|
+
card: {
|
|
2827
|
+
schema: '2.0',
|
|
2828
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
2829
|
+
header: {
|
|
2830
|
+
title: { tag: 'plain_text', content: `📄 ${path.basename(filePath)}` },
|
|
2831
|
+
template: 'blue',
|
|
2832
|
+
},
|
|
2833
|
+
body: {
|
|
2834
|
+
elements: [
|
|
2835
|
+
{
|
|
2836
|
+
tag: 'div',
|
|
2837
|
+
text: {
|
|
2838
|
+
tag: 'lark_md',
|
|
2839
|
+
content: `\`${filePath}\`\n文件 · 大小 ${sizeText} · 修改时间 ${mtimeText}`,
|
|
2840
|
+
},
|
|
2841
|
+
},
|
|
2842
|
+
{
|
|
2843
|
+
tag: 'column_set',
|
|
2844
|
+
columns: buttons.map((b) => ({ tag: 'column', width: 'auto', elements: [b] })),
|
|
2845
|
+
},
|
|
2846
|
+
{ tag: 'hr' },
|
|
2847
|
+
{
|
|
2848
|
+
tag: 'div',
|
|
2849
|
+
text: { tag: 'lark_md', content: '💡 也可用 `/download <path>` 直接下载该文件' },
|
|
2850
|
+
},
|
|
2851
|
+
],
|
|
2852
|
+
},
|
|
2853
|
+
},
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2683
2856
|
cmdWs(args, ctx, offset = 0) {
|
|
2684
2857
|
const sub = args[0]?.toLowerCase();
|
|
2685
2858
|
switch (sub) {
|
|
@@ -2864,7 +3037,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2864
3037
|
nextText: '➡',
|
|
2865
3038
|
});
|
|
2866
3039
|
bodyElements.push({ tag: 'hr' });
|
|
2867
|
-
bodyElements.push(bar);
|
|
3040
|
+
bodyElements.push(...bar);
|
|
2868
3041
|
}
|
|
2869
3042
|
}
|
|
2870
3043
|
const card = {
|
|
@@ -3170,7 +3343,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3170
3343
|
label: `第 ${currentPage}/${totalPages} 页 · 共 ${total} 个会话`,
|
|
3171
3344
|
});
|
|
3172
3345
|
elements.push({ tag: 'hr' });
|
|
3173
|
-
elements.push(bar);
|
|
3346
|
+
elements.push(...bar);
|
|
3174
3347
|
}
|
|
3175
3348
|
const card = {
|
|
3176
3349
|
schema: '2.0',
|
|
@@ -3209,16 +3382,6 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3209
3382
|
const { totalPages, safeOffset, currentPage } = pageInfo(totalCount, offset, pageSize);
|
|
3210
3383
|
// Slice: distribute offset across agent runs first, then bash runs
|
|
3211
3384
|
const elements = [];
|
|
3212
|
-
// Page info
|
|
3213
|
-
if (totalPages > 1) {
|
|
3214
|
-
elements.push({
|
|
3215
|
-
tag: 'div',
|
|
3216
|
-
text: {
|
|
3217
|
-
tag: 'lark_md',
|
|
3218
|
-
content: `**第 ${currentPage}/${totalPages} 页** (共 ${totalCount} 项)`,
|
|
3219
|
-
},
|
|
3220
|
-
});
|
|
3221
|
-
}
|
|
3222
3385
|
let remaining = pageSize;
|
|
3223
3386
|
let skipped = safeOffset;
|
|
3224
3387
|
// Agent runs section
|
|
@@ -3231,7 +3394,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3231
3394
|
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
3232
3395
|
elements.push({
|
|
3233
3396
|
tag: 'div',
|
|
3234
|
-
text: { tag: 'lark_md', content: `**📂 ${run.cwd
|
|
3397
|
+
text: { tag: 'lark_md', content: `**📂 ${displayName(run.cwd)}**` },
|
|
3235
3398
|
});
|
|
3236
3399
|
elements.push({
|
|
3237
3400
|
tag: 'div',
|
|
@@ -3283,37 +3446,18 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3283
3446
|
elements.push({ tag: 'hr' });
|
|
3284
3447
|
}
|
|
3285
3448
|
}
|
|
3286
|
-
// Pagination
|
|
3449
|
+
// Pagination bar(W2.9 复用 paginationBar:safeOffset 已对齐页边界,
|
|
3450
|
+
// offset±pageSize 与原先 currentPage±1 的页码换算等价)
|
|
3287
3451
|
if (totalPages > 1) {
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
});
|
|
3298
|
-
}
|
|
3299
|
-
if (currentPage < totalPages) {
|
|
3300
|
-
const nextOffset = currentPage * pageSize;
|
|
3301
|
-
paginationButtons.push({
|
|
3302
|
-
tag: 'button',
|
|
3303
|
-
text: { tag: 'plain_text', content: '下一页 ▶' },
|
|
3304
|
-
type: 'primary',
|
|
3305
|
-
size: 'small',
|
|
3306
|
-
behaviors: [{ type: 'callback', value: { cmd: 'active.page', offset: nextOffset } }],
|
|
3307
|
-
});
|
|
3308
|
-
}
|
|
3309
|
-
elements.push({
|
|
3310
|
-
tag: 'column_set',
|
|
3311
|
-
columns: paginationButtons.map((btn) => ({
|
|
3312
|
-
tag: 'column',
|
|
3313
|
-
width: 'auto',
|
|
3314
|
-
elements: [btn],
|
|
3315
|
-
})),
|
|
3316
|
-
});
|
|
3452
|
+
elements.push(...paginationBar({
|
|
3453
|
+
cmd: 'active.page',
|
|
3454
|
+
offset: safeOffset,
|
|
3455
|
+
pageSize,
|
|
3456
|
+
total: totalCount,
|
|
3457
|
+
label: `**第 ${currentPage}/${totalPages} 页** (共 ${totalCount} 项)`,
|
|
3458
|
+
prevText: '◀ 上一页',
|
|
3459
|
+
nextText: '下一页 ▶',
|
|
3460
|
+
}));
|
|
3317
3461
|
}
|
|
3318
3462
|
const card = {
|
|
3319
3463
|
schema: '2.0',
|
|
@@ -3463,15 +3607,6 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3463
3607
|
label: '⏱️ 空闲',
|
|
3464
3608
|
fields: [{ key: 'idle.watchdogMinutes', label: '空闲超时(分钟, 0关闭)', type: 'input' }],
|
|
3465
3609
|
},
|
|
3466
|
-
{
|
|
3467
|
-
id: 'output',
|
|
3468
|
-
label: '📤 输出',
|
|
3469
|
-
fields: [
|
|
3470
|
-
{ key: 'output.showThinking', label: '显示思考过程', type: 'boolean' },
|
|
3471
|
-
{ key: 'output.showToolUse', label: '显示工具调用', type: 'boolean' },
|
|
3472
|
-
{ key: 'output.showToolResult', label: '显示工具结果', type: 'boolean' },
|
|
3473
|
-
],
|
|
3474
|
-
},
|
|
3475
3610
|
{
|
|
3476
3611
|
id: 'logging',
|
|
3477
3612
|
label: '📝 日志',
|
|
@@ -3685,7 +3820,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3685
3820
|
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 条)`,
|
|
3686
3821
|
});
|
|
3687
3822
|
elements.push({ tag: 'hr' });
|
|
3688
|
-
elements.push(bar);
|
|
3823
|
+
elements.push(...bar);
|
|
3689
3824
|
}
|
|
3690
3825
|
}
|
|
3691
3826
|
return {
|