lark-remote 0.5.12 → 0.5.13

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/bridge/approval-coordinator.d.ts +1 -0
  5. package/dist/bridge/approval-coordinator.js +12 -24
  6. package/dist/bridge/index.d.ts +7 -0
  7. package/dist/bridge/index.js +77 -227
  8. package/dist/bridge/run-approval.d.ts +15 -0
  9. package/dist/bridge/run-approval.js +41 -0
  10. package/dist/bridge/run-usage.d.ts +46 -0
  11. package/dist/bridge/run-usage.js +96 -0
  12. package/dist/bridge/session-writeback.d.ts +18 -0
  13. package/dist/bridge/session-writeback.js +36 -0
  14. package/dist/card/bash-card-session.js +2 -5
  15. package/dist/card/card-session.d.ts +2 -0
  16. package/dist/card/card-session.js +20 -22
  17. package/dist/card/card-shared.d.ts +4 -4
  18. package/dist/card/card-shared.js +3 -9
  19. package/dist/card/collapsible.js +3 -2
  20. package/dist/card/final-answer-overflow.d.ts +7 -0
  21. package/dist/card/final-answer-overflow.js +46 -0
  22. package/dist/card/run-card-session.js +2 -7
  23. package/dist/card/run-renderer.d.ts +14 -0
  24. package/dist/card/run-renderer.js +119 -1
  25. package/dist/card/run-state.d.ts +12 -1
  26. package/dist/card/run-state.js +73 -34
  27. package/dist/config/codex-config.js +4 -4
  28. package/dist/config/index.d.ts +4 -2
  29. package/dist/config/index.js +6 -4
  30. package/dist/inbound/turn-assembler.js +6 -0
  31. package/dist/index.js +15 -409
  32. package/dist/message-handlers.d.ts +26 -0
  33. package/dist/message-handlers.js +275 -0
  34. package/dist/router/card-action-payload.d.ts +1 -1
  35. package/dist/router/card-action-payload.js +1 -1
  36. package/dist/router/commands/ls.d.ts +9 -0
  37. package/dist/router/commands/ls.js +243 -0
  38. package/dist/router/commands/order.d.ts +12 -0
  39. package/dist/router/commands/order.js +178 -0
  40. package/dist/router/commands/resume.d.ts +47 -0
  41. package/dist/router/commands/resume.js +246 -0
  42. package/dist/router/commands/ws.d.ts +15 -0
  43. package/dist/router/commands/ws.js +189 -0
  44. package/dist/router/config/claude.js +3 -3
  45. package/dist/router/config/codex.js +2 -6
  46. package/dist/router/config/common/turn-idle.d.ts +2 -0
  47. package/dist/router/config/common/turn-idle.js +9 -0
  48. package/dist/router/config/kimi.js +2 -6
  49. package/dist/router/config/opencode.js +2 -6
  50. package/dist/router/index.d.ts +14 -22
  51. package/dist/router/index.js +70 -1152
  52. package/dist/router/paging.d.ts +13 -0
  53. package/dist/router/paging.js +20 -0
  54. package/dist/router/path-validation.d.ts +27 -0
  55. package/dist/router/path-validation.js +45 -0
  56. package/dist/runner/claude/runner.d.ts +3 -4
  57. package/dist/runner/claude/runner.js +4 -5
  58. package/dist/runner/claude/session.js +1 -1
  59. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  60. package/dist/runner/codex/app-server/translator.js +65 -9
  61. package/dist/runner/common/acp/base-translator.d.ts +3 -8
  62. package/dist/runner/common/acp/base-translator.js +16 -7
  63. package/dist/runner/common/spawning-runner.js +10 -4
  64. package/dist/runner/common/turn-started.d.ts +2 -0
  65. package/dist/runner/common/turn-started.js +10 -0
  66. package/dist/runner/dsh/auth.js +2 -3
  67. package/dist/runner/dsh/client.d.ts +1 -6
  68. package/dist/runner/dsh/client.js +11 -165
  69. package/dist/runner/dsh/daemon.d.ts +0 -2
  70. package/dist/runner/dsh/daemon.js +1 -6
  71. package/dist/runner/dsh/errors.d.ts +5 -0
  72. package/dist/runner/dsh/errors.js +11 -0
  73. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  74. package/dist/runner/dsh/exit-cancel.js +1 -1
  75. package/dist/runner/dsh/home.d.ts +2 -0
  76. package/dist/runner/dsh/home.js +8 -0
  77. package/dist/runner/dsh/inflight.d.ts +0 -2
  78. package/dist/runner/dsh/inflight.js +1 -1
  79. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  80. package/dist/runner/dsh/mux-stream.js +157 -0
  81. package/dist/runner/dsh/translator.d.ts +0 -10
  82. package/dist/runner/dsh/translator.js +1 -1
  83. package/dist/runner/index.d.ts +1 -1
  84. package/dist/runner/kimi/acp/translator.js +1 -12
  85. package/dist/runner/opencode/acp/translator.js +1 -12
  86. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  87. package/dist/runner/pi/rpc/translator.js +2 -7
  88. package/dist/runner/registry.d.ts +0 -19
  89. package/dist/runner/registry.js +0 -32
  90. package/dist/runner/types.d.ts +17 -1
  91. package/dist/session/claude/session-index.js +4 -2
  92. package/dist/session/kimi/sessions.js +2 -2
  93. package/dist/session/opencode/sessions.js +2 -1
  94. package/dist/session/pi/sessions.js +3 -2
  95. package/package.json +1 -1
@@ -0,0 +1,275 @@
1
+ import fs from 'node:fs';
2
+ import { COMPACT_ACTION_CMDS, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, isImmediateAction, } from './router/index.js';
3
+ import { InboundTurnAssembler } from './inbound/turn-assembler.js';
4
+ import { InboundAuthorizer } from './inbound/authorize.js';
5
+ import { stripPlaceholders } from './inbound/placeholder.js';
6
+ import { agentDisplayName } from './card/card-shared.js';
7
+ import { sleep } from './common/sleep.js';
8
+ import { bestEffortUnlink } from './common/fs.js';
9
+ import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
10
+ import { buildCardActionFullValue } from './router/card-action-payload.js';
11
+ import { actionFeedbackText } from './router/card-action-feedback.js';
12
+ const SESSION_MUTATING_FLUSH_BUDGET_MS = 2_000;
13
+ /**
14
+ * /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
15
+ * 共用同一判定,防止将来加别名时两处漂移。
16
+ */
17
+ export function isStopCommand(text) {
18
+ const t = text.trim().toLowerCase();
19
+ return t === '/stop' || t === '/t';
20
+ }
21
+ export function setupMessageHandlers(deps) {
22
+ const { connector, router, bridge, sessionStore, workspaceStore, binder, logger, config, cloneSession, } = deps;
23
+ // 入站统一装配器:图/文任意顺序 → 同一个 turn、同一个 prompt。
24
+ const assembler = new InboundTurnAssembler({
25
+ onCommit: (turn, prompt) => {
26
+ const messageId = turn.messageIds[turn.messageIds.length - 1] ?? '';
27
+ const ctx = {
28
+ userId: turn.userId,
29
+ chatId: turn.chatId,
30
+ messageId,
31
+ turnMessageIds: turn.messageIds,
32
+ };
33
+ let cwd = sessionStore.getCwd(turn.userId) ?? '';
34
+ if (!cwd) {
35
+ const recent = [...workspaceStore.list()].sort((a, b) => b.lastUsedAt - a.lastUsedAt);
36
+ if (recent.length > 0)
37
+ cwd = recent[0].path;
38
+ }
39
+ const binding = bridge.currentBinding(turn.userId);
40
+ bridge.enqueue(cwd, async () => {
41
+ await router.handle(prompt, ctx, {
42
+ cwdOverride: cwd,
43
+ binding,
44
+ allowCommandPrefix: false,
45
+ });
46
+ }, {
47
+ taskMeta: {
48
+ userId: turn.userId,
49
+ chatId: turn.chatId,
50
+ messageId,
51
+ messagePreview: prompt.slice(0, 3000),
52
+ binding,
53
+ },
54
+ });
55
+ },
56
+ onReceipt: async (ctx, text) => {
57
+ await bridge.sendResult({ text }, ctx);
58
+ },
59
+ });
60
+ router.setInboundFlusher(() => assembler.flushAll('flush'));
61
+ const authorizer = new InboundAuthorizer({
62
+ binder,
63
+ onBind: (userId, chatId) => {
64
+ let resolvedCwd;
65
+ try {
66
+ resolvedCwd = fs.realpathSync(process.cwd());
67
+ }
68
+ catch {
69
+ resolvedCwd = process.cwd();
70
+ }
71
+ if (!sessionStore.getCwd(userId)) {
72
+ sessionStore.setCwd(userId, resolvedCwd);
73
+ }
74
+ return (async () => {
75
+ try {
76
+ await connector.sendWithRetry(chatId, {
77
+ text: `✅ 已绑定到本账号,此后仅你可使用本应用\n` +
78
+ `📂 当前工作目录: \`${resolvedCwd}\`\n` +
79
+ `🤖 默认 Agent: ${agentDisplayName(config.defaultAgent)}\n` +
80
+ `💡 直接输入消息即可开始对话,或 /cd 切换目录`,
81
+ });
82
+ const helpResult = router.cmdHelp();
83
+ if (helpResult.card) {
84
+ await connector.sendWithRetry(chatId, { card: helpResult.card });
85
+ }
86
+ }
87
+ catch (err) {
88
+ logger.error('[binder] onboarding send failed:', err);
89
+ }
90
+ })();
91
+ },
92
+ });
93
+ connector.setInboundMediaDetectedHandler((msg) => {
94
+ const authorization = authorizer.authorize(msg.userId, msg.chatId);
95
+ if (authorization === 'rejected') {
96
+ logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
97
+ return;
98
+ }
99
+ if (cloneSession.isActive()) {
100
+ logger.info(`[media] dropped inbound media during clone session ${msg.messageId}`);
101
+ return;
102
+ }
103
+ if (!router.config.inboundMedia.enabled) {
104
+ assembler.ingest({
105
+ kind: 'rejected',
106
+ userId: msg.userId,
107
+ chatId: msg.chatId,
108
+ messageId: msg.messageId,
109
+ replyToMessageId: msg.replyToMessageId,
110
+ rawContentType: msg.rawContentType,
111
+ rejectedKind: 'file',
112
+ reason: '入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
113
+ '如需自动保存图片/文件,请开启后重试',
114
+ });
115
+ return;
116
+ }
117
+ const outcome = (async () => {
118
+ const payload = await connector.downloadInboundMedia(msg, {
119
+ maxFileSizeMb: router.config.inboundMedia.maxFileSizeMb,
120
+ });
121
+ try {
122
+ return await bridge.saveInboundMedia(payload);
123
+ }
124
+ catch (err) {
125
+ for (const item of payload.media)
126
+ bestEffortUnlink(item.tempPath);
127
+ throw err;
128
+ }
129
+ })();
130
+ assembler.ingest({
131
+ kind: 'media',
132
+ userId: msg.userId,
133
+ chatId: msg.chatId,
134
+ messageId: msg.messageId,
135
+ replyToMessageId: msg.replyToMessageId,
136
+ rawContentType: msg.rawContentType,
137
+ outcome,
138
+ });
139
+ });
140
+ connector.setMessageHandler((msg) => {
141
+ const decision = authorizer.authorize(msg.userId, msg.chatId);
142
+ if (decision === 'rejected' || decision === 'bind_success')
143
+ return;
144
+ logger.info(`message from ${msg.userId} (${msg.rawContentType}): ${msg.content.slice(0, 100)}`);
145
+ void connector.addReaction(msg.messageId, 'Typing');
146
+ if (cloneSession.isActive() && !isStopCommand(msg.content)) {
147
+ void cloneSession
148
+ .handleMessage(msg.content, {
149
+ userId: msg.userId,
150
+ chatId: msg.chatId,
151
+ messageId: msg.messageId,
152
+ })
153
+ .catch((err) => logger.error('[clone] message handling failed:', err));
154
+ return;
155
+ }
156
+ const content = router.expandAliasMessage(msg.content);
157
+ if (isStopCommand(content)) {
158
+ void (async () => {
159
+ const interrupt = await bridge.interruptCurrentRunDetailed({
160
+ userId: msg.userId,
161
+ chatId: msg.chatId,
162
+ });
163
+ const ctx = { userId: msg.userId, chatId: msg.chatId, messageId: msg.messageId };
164
+ if (!interrupt.stopped) {
165
+ await bridge.sendResult({ text: '当前没有运行中的进程' }, ctx);
166
+ }
167
+ else if ((interrupt.stoppedCount ?? 1) > 1) {
168
+ await bridge.sendResult({ text: `✅ 已停止 ${interrupt.stoppedCount} 个运行中的任务` }, ctx);
169
+ }
170
+ })().catch((err) => logger.error('[control] /stop failed:', err));
171
+ return;
172
+ }
173
+ const stripped = stripPlaceholders(content);
174
+ if (stripped.unknownTags.length > 0) {
175
+ logger.warn(`[inbound] unknown placeholder tags stripped: ${stripped.unknownTags.join(', ')} ` +
176
+ `(msg_type=${msg.rawContentType})`);
177
+ }
178
+ const commandEligible = msg.rawContentType === 'text' && stripped.kinds.length === 0 && stripped.clean !== '';
179
+ if (commandEligible && (stripped.clean.startsWith('/') || stripped.clean.startsWith('!'))) {
180
+ void (async () => {
181
+ await assembler.flush(msg.userId, msg.chatId, 'flush');
182
+ await router.handle(stripped.clean, {
183
+ userId: msg.userId,
184
+ chatId: msg.chatId,
185
+ messageId: msg.messageId,
186
+ }, { allowCommandPrefix: true });
187
+ })().catch((err) => logger.error('[control] command failed:', err));
188
+ return;
189
+ }
190
+ assembler.ingest({
191
+ kind: 'text',
192
+ userId: msg.userId,
193
+ chatId: msg.chatId,
194
+ messageId: msg.messageId,
195
+ replyToMessageId: msg.replyToMessageId,
196
+ rawContentType: msg.rawContentType,
197
+ text: stripped.clean,
198
+ placeholders: stripped.kinds,
199
+ unknownTags: stripped.unknownTags,
200
+ });
201
+ });
202
+ connector.setCardActionHandler(async (action) => {
203
+ if (!binder.isOwner(action.operator.openId)) {
204
+ binder.recordRejectedCardAction(action.operator.openId);
205
+ return;
206
+ }
207
+ const actionValue = action.action.value;
208
+ if (!actionValue?.cmd)
209
+ return;
210
+ logger.info(`card action: ${actionValue.cmd}`);
211
+ const userId = action.operator.openId;
212
+ const chatId = action.chatId;
213
+ const messageId = action.messageId;
214
+ void connector.addReaction(messageId, 'Typing');
215
+ if (actionValue.cmd === 'stop') {
216
+ void router
217
+ .handleCardAction({ ...actionValue, cmd: 'stop' }, { userId, chatId, messageId })
218
+ .catch((err) => logger.error('[control] card stop failed:', err));
219
+ return;
220
+ }
221
+ const isImmediate = isImmediateAction(actionValue.cmd);
222
+ const cwd = sessionStore.getCwd(userId) ?? '';
223
+ const fullValue = buildCardActionFullValue(actionValue, action);
224
+ if (SESSION_MUTATING_ACTION_CMDS.has(actionValue.cmd)) {
225
+ const flushP = assembler
226
+ .flush(userId, chatId, 'flush')
227
+ .catch((err) => logger.error('[control] inbound flush failed:', err));
228
+ await Promise.race([flushP, sleep(SESSION_MUTATING_FLUSH_BUDGET_MS)]);
229
+ }
230
+ if (DIRECT_RETURN_CMDS.has(actionValue.cmd)) {
231
+ return router.handleCardAction(fullValue, { userId, chatId, messageId });
232
+ }
233
+ if (actionValue.cmd === 'order.exec') {
234
+ void dispatchOrderExecForQueue({
235
+ router,
236
+ bridge,
237
+ cwd,
238
+ orderId: actionValue.orderId,
239
+ ctx: { userId, chatId, messageId },
240
+ }).catch((err) => logger.error('[control] order.exec dispatch failed:', err));
241
+ return;
242
+ }
243
+ const forwardActionFeedback = (res) => {
244
+ const text = actionFeedbackText(res);
245
+ if (!text)
246
+ return;
247
+ void bridge
248
+ .sendResult({ text }, { userId, chatId, messageId })
249
+ .catch((err) => logger.error('[control] card action feedback failed:', err));
250
+ };
251
+ if (isImmediate) {
252
+ bridge.enqueueImmediate(cwd, async () => {
253
+ const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
254
+ forwardActionFeedback(res);
255
+ });
256
+ }
257
+ else {
258
+ bridge.enqueue(cwd, async () => {
259
+ const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
260
+ forwardActionFeedback(res);
261
+ }, {
262
+ taskMeta: {
263
+ userId,
264
+ chatId,
265
+ messageId: router.mintQueueTaskKey('card', actionValue.cmd),
266
+ feishuReplyTo: messageId,
267
+ messagePreview: `card action: ${actionValue.cmd}`,
268
+ kind: COMPACT_ACTION_CMDS.has(actionValue.cmd) ? 'compact' : 'command',
269
+ editable: !COMPACT_ACTION_CMDS.has(actionValue.cmd),
270
+ },
271
+ });
272
+ }
273
+ });
274
+ }
275
+ //# sourceMappingURL=message-handlers.js.map
@@ -9,7 +9,7 @@
9
9
  * out-of-band fields.
10
10
  *
11
11
  * Extracted from src/index.ts so the entry transform is behavior-testable
12
- * (the main() wiring itself cannot be injected, see index-wiring.test.ts).
12
+ * (see message-handlers.ts / message-handlers.test.ts).
13
13
  */
14
14
  import type { CardActionEvent } from '@larksuite/channel';
15
15
  import type { CardActionPayload } from './index.js';
@@ -9,7 +9,7 @@
9
9
  * out-of-band fields.
10
10
  *
11
11
  * Extracted from src/index.ts so the entry transform is behavior-testable
12
- * (the main() wiring itself cannot be injected, see index-wiring.test.ts).
12
+ * (see message-handlers.ts / message-handlers.test.ts).
13
13
  */
14
14
  /**
15
15
  * Build the full card action payload for router.handleCardAction.
@@ -0,0 +1,9 @@
1
+ import type { CommandContext, CommandResult } from '../../bridge/index.js';
2
+ export declare const LS_PAGE_SIZE = 30;
3
+ export interface LsCommandOptions {
4
+ cwd?: string;
5
+ offset?: number;
6
+ rootDir?: string;
7
+ q?: string;
8
+ }
9
+ export declare function buildLsCommand(args: string[], _ctx: CommandContext, options: LsCommandOptions): CommandResult;
@@ -0,0 +1,243 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { cardButton } from '../../card/card-shared.js';
5
+ import { paginationBar, searchBar } from '../card-helpers.js';
6
+ import { inspectPath, statPath } from '../path-validation.js';
7
+ import { pageSlice } from '../paging.js';
8
+ export const LS_PAGE_SIZE = 30;
9
+ function formatBytes(bytes) {
10
+ if (bytes < 1024)
11
+ return `${bytes}B`;
12
+ if (bytes < 1024 * 1024)
13
+ return `${(bytes / 1024).toFixed(1)}KB`;
14
+ if (bytes < 1024 * 1024 * 1024)
15
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
16
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
17
+ }
18
+ function buildFileCard(filePath) {
19
+ const resolution = statPath(filePath);
20
+ const stat = resolution.ok ? resolution.stat : undefined;
21
+ const sizeText = stat ? formatBytes(stat.size) : '未知';
22
+ const mtimeText = stat ? stat.mtime.toLocaleString('zh-CN', { hour12: false }) : '未知';
23
+ const parentDir = path.dirname(filePath);
24
+ const buttons = [
25
+ cardButton('📎 下载', { cmd: 'ls.file', path: filePath }, { type: 'primary', size: 'small' }),
26
+ ];
27
+ if (parentDir !== filePath) {
28
+ buttons.push(cardButton('上级', { cmd: 'ls.browse', path: parentDir, root: parentDir }, { type: 'default', size: 'small' }));
29
+ }
30
+ return {
31
+ card: {
32
+ schema: '2.0',
33
+ config: { wide_screen_mode: true, update_multi: true },
34
+ header: {
35
+ title: { tag: 'plain_text', content: `📄 ${path.basename(filePath)}` },
36
+ template: 'blue',
37
+ },
38
+ body: {
39
+ elements: [
40
+ { tag: 'div', text: { tag: 'lark_md', content: `\`${filePath}\`\n${sizeText}` } },
41
+ { tag: 'div', text: { tag: 'lark_md', content: `修改时间: ${mtimeText}` } },
42
+ {
43
+ tag: 'column_set',
44
+ columns: buttons.map((button) => ({
45
+ tag: 'column',
46
+ width: 'auto',
47
+ elements: [button],
48
+ })),
49
+ },
50
+ ],
51
+ },
52
+ },
53
+ };
54
+ }
55
+ export function buildLsCommand(args, _ctx, options) {
56
+ const { cwd, offset = 0, rootDir } = options;
57
+ if (!cwd)
58
+ return { text: '请先使用 /cd <path> 设置工作目录' };
59
+ const filter = options.q?.trim() || undefined;
60
+ let targetDir = cwd;
61
+ if (args[0]) {
62
+ const arg = args[0].startsWith('~') ? path.join(os.homedir(), args[0].slice(1)) : args[0];
63
+ const potentialDir = path.resolve(cwd, arg);
64
+ const kind = inspectPath(potentialDir);
65
+ if (kind === 'missing' || kind === 'unreadable') {
66
+ return { text: `ls: ${args[0]}: No such file or directory` };
67
+ }
68
+ if (kind === 'file')
69
+ return buildFileCard(potentialDir);
70
+ targetDir = potentialDir;
71
+ }
72
+ const browseRoot = rootDir ? path.resolve(cwd, rootDir) : targetDir;
73
+ try {
74
+ const entries = fs.readdirSync(targetDir, { withFileTypes: true });
75
+ const dirs = [];
76
+ const files = [];
77
+ for (const entry of entries) {
78
+ let isDir = entry.isDirectory();
79
+ let size;
80
+ if (!isDir) {
81
+ const resolution = statPath(path.join(targetDir, entry.name));
82
+ if (resolution.ok) {
83
+ isDir = resolution.stat.isDirectory();
84
+ if (!isDir)
85
+ size = resolution.stat.size;
86
+ }
87
+ }
88
+ if (isDir)
89
+ dirs.push(entry.name);
90
+ else
91
+ files.push({ name: entry.name, size });
92
+ }
93
+ dirs.sort((a, b) => a.localeCompare(b));
94
+ files.sort((a, b) => a.name.localeCompare(b.name));
95
+ const allItems = [
96
+ ...dirs.map((name) => ({ name, isDir: true })),
97
+ ...files.map((file) => ({ name: file.name, isDir: false, size: file.size })),
98
+ ];
99
+ const ql = filter?.toLowerCase();
100
+ const visibleItems = ql
101
+ ? allItems.filter((item) => item.name.toLowerCase().includes(ql))
102
+ : allItems;
103
+ const visibleDirCount = visibleItems.reduce((count, item) => count + (item.isDir ? 1 : 0), 0);
104
+ const visibleFileCount = visibleItems.length - visibleDirCount;
105
+ const { totalCount, totalPages, safeOffset, currentPage, pageItems, hasPagination } = pageSlice(visibleItems, offset, LS_PAGE_SIZE);
106
+ const parentDir = path.dirname(targetDir);
107
+ const headerElements = [];
108
+ if (parentDir !== targetDir) {
109
+ headerElements.push(cardButton('上级', { cmd: 'ls.browse', path: parentDir, root: browseRoot }, { type: 'default', size: 'small' }));
110
+ }
111
+ if (targetDir !== browseRoot) {
112
+ headerElements.push(cardButton('返回', { cmd: 'ls.browse', path: browseRoot, root: browseRoot }, { type: 'default', size: 'small' }));
113
+ }
114
+ headerElements.push(cardButton('刷新', {
115
+ cmd: 'ls.refresh',
116
+ path: targetDir,
117
+ offset: safeOffset,
118
+ root: browseRoot,
119
+ ...(filter ? { q: filter } : {}),
120
+ }, { type: 'default', size: 'small' }));
121
+ if (targetDir !== cwd) {
122
+ headerElements.push(cardButton('切换', { cmd: 'ls.switch', path: targetDir, root: browseRoot }, { type: 'primary', size: 'small' }));
123
+ }
124
+ const dirButton = (name) => cardButton(`📁 ${name}`, { cmd: 'ls.browse', path: path.join(targetDir, name), root: browseRoot }, { type: 'default' });
125
+ const fileButton = (name, size) => cardButton(`📄 ${name}${size !== undefined ? ` (${formatBytes(size)})` : ''}`, { cmd: 'ls.file', path: path.join(targetDir, name) }, { type: 'default', size: 'small' });
126
+ const elements = [
127
+ {
128
+ tag: 'div',
129
+ text: {
130
+ tag: 'lark_md',
131
+ content: `\`${targetDir}\`\n共 ${visibleDirCount} 目录, ${visibleFileCount} 文件`,
132
+ },
133
+ },
134
+ {
135
+ tag: 'column_set',
136
+ columns: headerElements.map((element) => ({
137
+ tag: 'column',
138
+ width: 'auto',
139
+ elements: [element],
140
+ })),
141
+ },
142
+ { tag: 'hr' },
143
+ searchBar({
144
+ cmd: 'ls.filter',
145
+ placeholder: '搜索本层目录/文件名,输完回车',
146
+ currentQuery: filter,
147
+ extra: { path: targetDir, root: browseRoot },
148
+ }),
149
+ ];
150
+ if (filter) {
151
+ elements.push({
152
+ tag: 'div',
153
+ text: {
154
+ tag: 'lark_md',
155
+ content: `🔍 筛选:"${filter}" · 命中 ${totalCount} 项(点「清除筛选」取消)`,
156
+ },
157
+ });
158
+ }
159
+ const pageDirs = pageItems.filter((item) => item.isDir);
160
+ const pageFiles = pageItems.filter((item) => !item.isDir);
161
+ if (filter && visibleItems.length === 0) {
162
+ elements.push({
163
+ tag: 'div',
164
+ text: { tag: 'lark_md', content: `🔍 无匹配条目(关键词:"${filter}")` },
165
+ });
166
+ }
167
+ else {
168
+ elements.push({
169
+ tag: 'div',
170
+ text: { tag: 'lark_md', content: `**📂 目录 (${visibleDirCount})**` },
171
+ });
172
+ if (pageDirs.length > 0) {
173
+ for (const dir of pageDirs) {
174
+ elements.push({
175
+ tag: 'column_set',
176
+ flex_mode: 'none',
177
+ columns: [{ tag: 'column', width: 'auto', elements: [dirButton(dir.name)] }],
178
+ });
179
+ }
180
+ }
181
+ else if (visibleDirCount === 0) {
182
+ elements.push({ tag: 'div', text: { tag: 'lark_md', content: '无子目录' } });
183
+ }
184
+ else {
185
+ elements.push({
186
+ tag: 'div',
187
+ text: { tag: 'lark_md', content: `(第 ${currentPage} 页无目录)` },
188
+ });
189
+ }
190
+ elements.push({ tag: 'hr' });
191
+ elements.push({
192
+ tag: 'div',
193
+ text: { tag: 'lark_md', content: `**📄 文件 (${visibleFileCount})**` },
194
+ });
195
+ if (pageFiles.length > 0) {
196
+ for (const file of pageFiles) {
197
+ elements.push({
198
+ tag: 'column_set',
199
+ flex_mode: 'none',
200
+ columns: [
201
+ { tag: 'column', width: 'auto', elements: [fileButton(file.name, file.size)] },
202
+ ],
203
+ });
204
+ }
205
+ }
206
+ else if (visibleFileCount === 0) {
207
+ elements.push({ tag: 'div', text: { tag: 'lark_md', content: '无文件' } });
208
+ }
209
+ else {
210
+ elements.push({
211
+ tag: 'div',
212
+ text: { tag: 'lark_md', content: `(第 ${currentPage} 页无文件)` },
213
+ });
214
+ }
215
+ }
216
+ if (hasPagination) {
217
+ elements.push({ tag: 'hr' });
218
+ elements.push(...paginationBar({
219
+ cmd: 'ls.page',
220
+ offset: safeOffset,
221
+ pageSize: LS_PAGE_SIZE,
222
+ total: totalCount,
223
+ extra: { path: targetDir, root: browseRoot, ...(filter ? { q: filter } : {}) },
224
+ label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 项)`,
225
+ }));
226
+ }
227
+ return {
228
+ card: {
229
+ schema: '2.0',
230
+ config: { wide_screen_mode: true, update_multi: true },
231
+ header: {
232
+ title: { tag: 'plain_text', content: `📁 ${path.basename(targetDir)}` },
233
+ template: 'blue',
234
+ },
235
+ body: { elements },
236
+ },
237
+ };
238
+ }
239
+ catch (err) {
240
+ return { text: `读取目录失败: ${err.message}` };
241
+ }
242
+ }
243
+ //# sourceMappingURL=ls.js.map
@@ -0,0 +1,12 @@
1
+ import type { CommandContext, CommandResult } from '../../bridge/index.js';
2
+ import type { OrderEntry } from '../../order/index.js';
3
+ export declare const ORDER_PAGE_SIZE = 8;
4
+ export interface OrderCommandDeps {
5
+ reload(): void;
6
+ list(): OrderEntry[];
7
+ save(text: string): void;
8
+ updateText(id: string, text: string): void;
9
+ setAlias(id: string, alias: string | undefined): void;
10
+ resolveTarget(target: string): OrderEntry | undefined;
11
+ }
12
+ export declare function buildOrderCommand(args: string[], ctx: CommandContext, offset: number, deps: OrderCommandDeps): CommandResult;