lark-remote 0.4.1 → 0.4.2
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 +17 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/card/approval-render.js +9 -8
- package/dist/router/card-helpers.d.ts +38 -2
- package/dist/router/card-helpers.js +79 -2
- package/dist/router/index.d.ts +39 -6
- package/dist/router/index.js +219 -69
- package/dist/runner/dsh/fake-dsh-server.d.ts +6 -0
- package/dist/runner/dsh/fake-dsh-server.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.2] - 2026-09-19
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- **`/ws` `/ls` 列表卡关键词搜索**:两张列表卡各多出一行搜索输入框,输入关键词后**按回车提交**即可筛选。`/ws` 匹配别名或完整路径,`/ls` 只匹配**当前层**条目名(不递归子目录),均大小写不敏感、子串包含;计数与页码一律按过滤后的结果计算。筛选词不落服务端(只随卡片回调 payload 传递,且仅非空时注入),服务端保持零状态,未筛选时的卡片形态与旧版完全一致。筛选态下翻页/刷新/排序/使用/删除会保留关键词,进入其他目录或切换工作目录则清除筛选(换目录后旧关键词没有意义)
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
|
|
16
|
+
- **卡片提交指引不再写「点 ✓」**:飞书移动端的 `input` 组件没有提交图标,只有键盘的回车/完成键(真机反馈)。所有用户可见文案统一为「按回车提交」,包括分页页码跳转、别名编辑、指令文本编辑与审批卡的修改意见
|
|
17
|
+
- **取消筛选改走显式按钮**:「清空输入再回车」在部分客户端不会触发回调(官方文档未承诺空串会发回调),实测清空后回车没有反应。现在筛选态下搜索行右侧会出现**「清除筛选」**按钮,走显式标记强制清除,不再依赖「读不到输入值」这一隐式条件
|
|
18
|
+
- **别名编辑卡不再把「留空提交」当删除入口**:同一处隐式条件问题,删除别名统一走列表卡别名旁的 ✕
|
|
19
|
+
|
|
20
|
+
### 变更
|
|
21
|
+
|
|
22
|
+
- 卡片布局文档(中英)补齐 CardKit `input` 的三条约束:没有逐键回调所以「边输边过滤」做不到、文案不写「点 ✓」、取消语义要有显式控件;`design.md` 中「input 提交图标」的表述改为「input 提交回传」(图标只是桌面端的一种触发方式)
|
|
23
|
+
- 英文文档补齐 `/ws` `/ls` 关键词搜索:`README.en.md` 与 `docs/en/usage.md` 的命令表和功能段跟上中文口径
|
|
24
|
+
|
|
8
25
|
## [0.4.1] - 2026-09-18
|
|
9
26
|
|
|
10
27
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -108,9 +108,9 @@ Your original message also gets an emoji reaction: `Typing` while the run is in
|
|
|
108
108
|
|---------|-------|----------|
|
|
109
109
|
| `/help` | `/h` | Command list |
|
|
110
110
|
| `/cd <path>` | - | Switch agent working directory (`~`, absolute/relative; clears the session) |
|
|
111
|
-
| `/ls [dir\|file]` | - | Directory/file card; click a directory to browse, click a file ≤30MB to send it to Feishu; paginates beyond 30 entries with a page-number jump input. Passing a file path lists that file itself |
|
|
111
|
+
| `/ls [dir\|file]` | - | Directory/file card; click a directory to browse, click a file ≤30MB to send it to Feishu; paginates beyond 30 entries with a page-number jump input, and current-level entries can be filtered by keyword. Passing a file path lists that file itself |
|
|
112
112
|
| `/download <path>` | `/d` | Send a local file straight to the chat (`~`, absolute/relative paths, 30MB cap) |
|
|
113
|
-
| `/ws save\|use\|remove` | - | Named directory aliases (`/ws` lists by default) |
|
|
113
|
+
| `/ws save\|use\|remove` | - | Named directory aliases (`/ws` lists by default, filterable by alias name or path keyword) |
|
|
114
114
|
| `/resume [agent] [N\|id]` | `/r` | List/switch agent sessions for the current directory (card) |
|
|
115
115
|
| `/active` | - | List all running sessions |
|
|
116
116
|
| `/new` | - | Clear the current session (keeps cwd) |
|
package/README.md
CHANGED
|
@@ -108,9 +108,9 @@ lark-remote 启动后不在终端输出,运行日志写入 `~/.lark-remote/log
|
|
|
108
108
|
|------|------|------|
|
|
109
109
|
| `/help` | `/h` | 命令列表 |
|
|
110
110
|
| `/cd <path>` | - | 切换 agent 工作目录(支持 `~`、绝对/相对路径,清空会话) |
|
|
111
|
-
| `/ls [dir\|file]` | - | 弹出目录/文件卡片;点击目录切换,点击 30MB 内文件发送到飞书;条目 >30
|
|
111
|
+
| `/ls [dir\|file]` | - | 弹出目录/文件卡片;点击目录切换,点击 30MB 内文件发送到飞书;条目 >30 时支持翻页与直接输入页码跳转,卡片内可按关键词筛选本层条目。传文件路径时直接列出该文件(可下载) |
|
|
112
112
|
| `/download <path>` | `/d` | 直接下载(发送)指定文件到飞书(支持 `~`、绝对/相对路径,上限 30MB) |
|
|
113
|
-
| `/ws save\|use\|remove` | - | 命名目录别名管理(`/ws`
|
|
113
|
+
| `/ws save\|use\|remove` | - | 命名目录别名管理(`/ws` 默认列出,可按名称/路径关键词筛选) |
|
|
114
114
|
| `/resume [agent] [N\|id]` | `/r` | 列出/切换当前目录的 agent session(卡片) |
|
|
115
115
|
| `/active` | - | 列出所有正在进行中的 session |
|
|
116
116
|
| `/new` | - | 清空当前会话(保留工作目录) |
|
|
@@ -207,9 +207,9 @@ function renderPlanApproval(approval, expired, runId) {
|
|
|
207
207
|
return elements;
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
|
-
*
|
|
211
|
-
* raw 事件回传(CardKit input 的 input_value 会被
|
|
212
|
-
*
|
|
210
|
+
* 计划修改意见输入行:输入后提交(移动端键盘回车/完成键,桌面端右侧提交图标),
|
|
211
|
+
* input_value 经 connector raw 事件回传(CardKit input 的 input_value 会被
|
|
212
|
+
* SDK normalizer 丢弃,必须走 includeRawEvent 路径,同 answerNote)。
|
|
213
213
|
*/
|
|
214
214
|
function renderPlanFeedbackInput(requestId, runId) {
|
|
215
215
|
return {
|
|
@@ -430,8 +430,9 @@ function renderQuestionApproval(approval, expired, runId) {
|
|
|
430
430
|
text: { tag: 'lark_md', content: `✍️ 自定义答案:${customSelected}` },
|
|
431
431
|
});
|
|
432
432
|
}
|
|
433
|
-
// review P3-4:单选问题提供自定义答案(Other
|
|
434
|
-
//
|
|
433
|
+
// review P3-4:单选问题提供自定义答案(Other)输入——输入文本后提交
|
|
434
|
+
// (移动端键盘回车/完成键,桌面端输入框右侧提交图标),input_value 经
|
|
435
|
+
// connector raw 事件回传。
|
|
435
436
|
// 显隐按 isOther !== false:Kimi form 会丢弃非声明选项值,翻译时置
|
|
436
437
|
// isOther=false 隐藏输入;Claude 未设置该字段默认显示(行为不变)。
|
|
437
438
|
if (!q.multiSelect && q.isOther !== false) {
|
|
@@ -517,9 +518,9 @@ function renderOptionRow(requestId, questionIndex, option, isSelected, icon, run
|
|
|
517
518
|
};
|
|
518
519
|
}
|
|
519
520
|
/**
|
|
520
|
-
* 文本输入行(Other
|
|
521
|
-
* input_value 经 connector raw 事件回传(CardKit input 的
|
|
522
|
-
* SDK normalizer 丢弃,必须走 includeRawEvent 路径)。
|
|
521
|
+
* 文本输入行(Other 输入与自由文本题共用):输入后提交(移动端键盘回车/完成键,
|
|
522
|
+
* 桌面端右侧提交图标),input_value 经 connector raw 事件回传(CardKit input 的
|
|
523
|
+
* input_value 会被 SDK normalizer 丢弃,必须走 includeRawEvent 路径)。
|
|
523
524
|
*/
|
|
524
525
|
function renderAnswerInput(requestId, questionIndex, placeholder, runId, cmd = 'approval.answerCustom') {
|
|
525
526
|
// name 必须含 cmd,保证同一题同时渲染多个输入框(如单选选项题的
|
|
@@ -58,8 +58,9 @@ export declare function parseJumpOffset(inputValue: unknown, pageSize: number):
|
|
|
58
58
|
* Returns an array of body elements: `[page-label div, controls column_set]`;
|
|
59
59
|
* 调用方展开进 `body.elements`(+ 自己的 `hr`)。input 仍留在控件 column_set
|
|
60
60
|
* 内部,不用 form 容器(form 触发 300123 / 200621 整卡不可用)。CardKit 2.0
|
|
61
|
-
* input
|
|
62
|
-
* includeRawEvent +
|
|
61
|
+
* input 提交(移动端键盘回车/完成键,桌面端输入框右侧提交图标)时值经 raw
|
|
62
|
+
* `action.input_value` 回传,见 connector includeRawEvent +
|
|
63
|
+
* router/card-action-payload.ts。
|
|
63
64
|
*
|
|
64
65
|
* 每个调用方自带 callback cmd / extra value / 文案(语义差异保留,不强行统一)。
|
|
65
66
|
*
|
|
@@ -81,6 +82,41 @@ export declare function paginationBar(opts: {
|
|
|
81
82
|
prevText?: string;
|
|
82
83
|
nextText?: string;
|
|
83
84
|
}): object[];
|
|
85
|
+
/**
|
|
86
|
+
* 搜索输入框的组件 name(仅用于结构断言与可读性)。input 在非 form 容器下
|
|
87
|
+
* 提交值经 raw.action.input_value 回传,formValue 路径作为回退通道。
|
|
88
|
+
*/
|
|
89
|
+
export declare const SEARCH_INPUT_NAME = "searchInput";
|
|
90
|
+
/**
|
|
91
|
+
* Build the CardKit 2.0 keyword-search row shared by /ws and /ls list cards.
|
|
92
|
+
*
|
|
93
|
+
* 飞书 input 组件没有逐键回调,只有提交(键盘回车/完成键)时才触发一次
|
|
94
|
+
* callback,所以交互是「输完回车」确认制,placeholder 必须写明。
|
|
95
|
+
*
|
|
96
|
+
* 清空输入再回车在部分客户端不可靠(用户实测:手机飞书清空后回车没反应,
|
|
97
|
+
* 空串是否发回调也没有文档保证),因此**有 `currentQuery` 就自动**在输入框
|
|
98
|
+
* 右侧补一个「清除筛选」按钮:按钮 callback 不带 input_value,handler 靠
|
|
99
|
+
* `clearQuery` 标记强制清除,不依赖「读不到输入值」这一隐式条件。
|
|
100
|
+
*
|
|
101
|
+
* 与 `paginationBar` 的控件行同一套窄屏安全模式:input 用 `weighted/weight: 1`
|
|
102
|
+
* 吃满清除按钮之外的剩余宽度,清除按钮 `auto`,手机上输入框仍可点。禁用 form
|
|
103
|
+
* 容器(300123 / 200621)与 `tag: "action"`(200861)。
|
|
104
|
+
*
|
|
105
|
+
* 返回**单个** `column_set` 元素(`paginationBar` 返回两个,调用处写法不同:
|
|
106
|
+
* `elements.push(searchBar({...}))` vs `elements.push(...paginationBar({...}))`)。
|
|
107
|
+
*
|
|
108
|
+
* @param opts.cmd 提交回调的 cmd('ws.filter' / 'ls.filter')。
|
|
109
|
+
* @param opts.placeholder 输入框占位文案(必须含「输完回车」)。
|
|
110
|
+
* @param opts.currentQuery 当前筛选词,非空时写进 default_value 回显,并据此
|
|
111
|
+
* 显示「清除筛选」按钮。
|
|
112
|
+
* @param opts.extra 透传进 callback value 的附加字段(/ls 用 path/root)。
|
|
113
|
+
*/
|
|
114
|
+
export declare function searchBar(opts: {
|
|
115
|
+
cmd: string;
|
|
116
|
+
placeholder: string;
|
|
117
|
+
currentQuery?: string;
|
|
118
|
+
extra?: Record<string, unknown>;
|
|
119
|
+
}): object;
|
|
84
120
|
/**
|
|
85
121
|
* Build a session-history resume card (shared by auto-restore, /resume <id>,
|
|
86
122
|
* config-switch resume, and run-completion notification).
|
|
@@ -104,8 +104,9 @@ export function parseJumpOffset(inputValue, pageSize) {
|
|
|
104
104
|
* Returns an array of body elements: `[page-label div, controls column_set]`;
|
|
105
105
|
* 调用方展开进 `body.elements`(+ 自己的 `hr`)。input 仍留在控件 column_set
|
|
106
106
|
* 内部,不用 form 容器(form 触发 300123 / 200621 整卡不可用)。CardKit 2.0
|
|
107
|
-
* input
|
|
108
|
-
* includeRawEvent +
|
|
107
|
+
* input 提交(移动端键盘回车/完成键,桌面端输入框右侧提交图标)时值经 raw
|
|
108
|
+
* `action.input_value` 回传,见 connector includeRawEvent +
|
|
109
|
+
* router/card-action-payload.ts。
|
|
109
110
|
*
|
|
110
111
|
* 每个调用方自带 callback cmd / extra value / 文案(语义差异保留,不强行统一)。
|
|
111
112
|
*
|
|
@@ -192,6 +193,82 @@ export function paginationBar(opts) {
|
|
|
192
193
|
{ tag: 'column_set', columns: controls },
|
|
193
194
|
];
|
|
194
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* 搜索输入框的组件 name(仅用于结构断言与可读性)。input 在非 form 容器下
|
|
198
|
+
* 提交值经 raw.action.input_value 回传,formValue 路径作为回退通道。
|
|
199
|
+
*/
|
|
200
|
+
export const SEARCH_INPUT_NAME = 'searchInput';
|
|
201
|
+
/**
|
|
202
|
+
* Build the CardKit 2.0 keyword-search row shared by /ws and /ls list cards.
|
|
203
|
+
*
|
|
204
|
+
* 飞书 input 组件没有逐键回调,只有提交(键盘回车/完成键)时才触发一次
|
|
205
|
+
* callback,所以交互是「输完回车」确认制,placeholder 必须写明。
|
|
206
|
+
*
|
|
207
|
+
* 清空输入再回车在部分客户端不可靠(用户实测:手机飞书清空后回车没反应,
|
|
208
|
+
* 空串是否发回调也没有文档保证),因此**有 `currentQuery` 就自动**在输入框
|
|
209
|
+
* 右侧补一个「清除筛选」按钮:按钮 callback 不带 input_value,handler 靠
|
|
210
|
+
* `clearQuery` 标记强制清除,不依赖「读不到输入值」这一隐式条件。
|
|
211
|
+
*
|
|
212
|
+
* 与 `paginationBar` 的控件行同一套窄屏安全模式:input 用 `weighted/weight: 1`
|
|
213
|
+
* 吃满清除按钮之外的剩余宽度,清除按钮 `auto`,手机上输入框仍可点。禁用 form
|
|
214
|
+
* 容器(300123 / 200621)与 `tag: "action"`(200861)。
|
|
215
|
+
*
|
|
216
|
+
* 返回**单个** `column_set` 元素(`paginationBar` 返回两个,调用处写法不同:
|
|
217
|
+
* `elements.push(searchBar({...}))` vs `elements.push(...paginationBar({...}))`)。
|
|
218
|
+
*
|
|
219
|
+
* @param opts.cmd 提交回调的 cmd('ws.filter' / 'ls.filter')。
|
|
220
|
+
* @param opts.placeholder 输入框占位文案(必须含「输完回车」)。
|
|
221
|
+
* @param opts.currentQuery 当前筛选词,非空时写进 default_value 回显,并据此
|
|
222
|
+
* 显示「清除筛选」按钮。
|
|
223
|
+
* @param opts.extra 透传进 callback value 的附加字段(/ls 用 path/root)。
|
|
224
|
+
*/
|
|
225
|
+
export function searchBar(opts) {
|
|
226
|
+
const columns = [
|
|
227
|
+
{
|
|
228
|
+
tag: 'column',
|
|
229
|
+
width: 'weighted',
|
|
230
|
+
weight: 1,
|
|
231
|
+
vertical_align: 'center',
|
|
232
|
+
elements: [
|
|
233
|
+
{
|
|
234
|
+
tag: 'input',
|
|
235
|
+
name: SEARCH_INPUT_NAME,
|
|
236
|
+
placeholder: { tag: 'plain_text', content: opts.placeholder },
|
|
237
|
+
...(opts.currentQuery ? { default_value: opts.currentQuery } : {}),
|
|
238
|
+
max_length: 100,
|
|
239
|
+
behaviors: [
|
|
240
|
+
{
|
|
241
|
+
type: 'callback',
|
|
242
|
+
value: { cmd: opts.cmd, ...opts.extra },
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
if (opts.currentQuery) {
|
|
250
|
+
columns.push({
|
|
251
|
+
tag: 'column',
|
|
252
|
+
width: 'auto',
|
|
253
|
+
vertical_align: 'center',
|
|
254
|
+
elements: [
|
|
255
|
+
{
|
|
256
|
+
tag: 'button',
|
|
257
|
+
text: { tag: 'plain_text', content: '清除筛选' },
|
|
258
|
+
type: 'default',
|
|
259
|
+
size: 'small',
|
|
260
|
+
behaviors: [
|
|
261
|
+
{
|
|
262
|
+
type: 'callback',
|
|
263
|
+
value: { cmd: opts.cmd, clearQuery: true, ...opts.extra },
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return { tag: 'column_set', columns };
|
|
271
|
+
}
|
|
195
272
|
/**
|
|
196
273
|
* Build a session-history resume card (shared by auto-restore, /resume <id>,
|
|
197
274
|
* config-switch resume, and run-completion notification).
|
package/dist/router/index.d.ts
CHANGED
|
@@ -61,9 +61,19 @@ export interface CardActionPayload {
|
|
|
61
61
|
agent?: string;
|
|
62
62
|
/** /resume 列表页大小覆盖。 */
|
|
63
63
|
pageSize?: number;
|
|
64
|
+
/**
|
|
65
|
+
* /ws 与 /ls 列表的关键词筛选(空/缺省 = 无筛选)。服务端零状态:只随卡片
|
|
66
|
+
* callback value 传递,且仅在非空时注入,旧卡片 payload 形态不变。
|
|
67
|
+
*/
|
|
68
|
+
q?: string;
|
|
69
|
+
/**
|
|
70
|
+
* 「清除筛选」按钮标记。按钮 callback 不带 input_value,但显式标记比依赖
|
|
71
|
+
* 「读不到输入值」更稳(部分客户端可能把输入框残值一并回传)。
|
|
72
|
+
*/
|
|
73
|
+
clearQuery?: boolean;
|
|
64
74
|
option?: string;
|
|
65
75
|
formValue?: Record<string, unknown>;
|
|
66
|
-
/** CardKit 2.0 input
|
|
76
|
+
/** CardKit 2.0 input 组件提交(键盘回车/完成键)时回传的输入值 */
|
|
67
77
|
inputValue?: string;
|
|
68
78
|
orderId?: string;
|
|
69
79
|
/** Approval request ID for approval card actions. */
|
|
@@ -287,6 +297,14 @@ export declare class CommandRouter {
|
|
|
287
297
|
* Handle ls.page: paginate to a specific offset.
|
|
288
298
|
*/
|
|
289
299
|
private handleLsPage;
|
|
300
|
+
/**
|
|
301
|
+
* Handle ls.filter: 按搜索框提交的关键词重新渲染当前目录卡。
|
|
302
|
+
*
|
|
303
|
+
* offset 恒重置为 0(新筛选从第 1 页开始)。路径校验与 TOCTOU 兜底照
|
|
304
|
+
* handleLsPage——校验失败只回错误 toast,绝不动卡片(否则用户看到一张空卡)。
|
|
305
|
+
* ls.browse / ls.switch 的 value 里刻意不带 q,换目录即清除筛选(设计文档 §2.3)。
|
|
306
|
+
*/
|
|
307
|
+
private handleLsFilter;
|
|
290
308
|
/**
|
|
291
309
|
* Handle resume.page: paginate the /resume session list in place.
|
|
292
310
|
* Mirrors handleLsPage — updates the same card, never sends a new one.
|
|
@@ -331,11 +349,12 @@ export declare class CommandRouter {
|
|
|
331
349
|
} | null;
|
|
332
350
|
/**
|
|
333
351
|
* W2.8 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
|
|
352
|
+
* `q` 只服务 ws 卡的关键词筛选;order 卡无此功能,不透传。
|
|
334
353
|
*/
|
|
335
354
|
private rebuildListCard;
|
|
336
355
|
/**
|
|
337
356
|
* W2.8 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
|
|
338
|
-
*
|
|
357
|
+
* 翻页/移除/排序/筛选动作共用)。返回重建后的卡片;无卡时返回 undefined 且跳过更新。
|
|
339
358
|
*/
|
|
340
359
|
private refreshListCard;
|
|
341
360
|
/**
|
|
@@ -356,7 +375,7 @@ export declare class CommandRouter {
|
|
|
356
375
|
*/
|
|
357
376
|
private handleOrderPage;
|
|
358
377
|
/**
|
|
359
|
-
* Handle order.aliasEdit: 展示别名编辑卡(input
|
|
378
|
+
* Handle order.aliasEdit: 展示别名编辑卡(input,回车/完成键提交)。
|
|
360
379
|
* 有别名时预填;提交 `order.aliasInput` 处理(含校验与唯一性)。
|
|
361
380
|
*/
|
|
362
381
|
/**
|
|
@@ -373,7 +392,7 @@ export declare class CommandRouter {
|
|
|
373
392
|
/** Handle order.aliasRemove: 删除某条指令的别名并原地重渲染列表卡。 */
|
|
374
393
|
private handleOrderAliasRemove;
|
|
375
394
|
/**
|
|
376
|
-
* Handle order.textEdit: 展示指令文本编辑卡(input
|
|
395
|
+
* Handle order.textEdit: 展示指令文本编辑卡(input,回车/完成键提交)。
|
|
377
396
|
* 预填当前 text(完整内容,不截断——input 组件有自有滚动条);
|
|
378
397
|
* 提交 `order.textInput` 处理(trim + 长度校验 + OrderStore.updateText)。
|
|
379
398
|
* 完全镜像 handleOrderAliasEdit 的 4 段结构(reload → find → 构造 editCard →
|
|
@@ -408,6 +427,11 @@ export declare class CommandRouter {
|
|
|
408
427
|
* Mirrors handleOrderPage — updates the same card, never sends a new one.
|
|
409
428
|
*/
|
|
410
429
|
private handleWsPage;
|
|
430
|
+
/**
|
|
431
|
+
* Handle ws.filter: 按搜索框提交的关键词重新渲染列表卡。
|
|
432
|
+
* offset 恒重置为 0(新筛选从第 1 页开始);提交空串 = 清除筛选。
|
|
433
|
+
*/
|
|
434
|
+
private handleWsFilter;
|
|
411
435
|
/**
|
|
412
436
|
* Handle ws.sort: toggle sort preference and refresh the /ws list card.
|
|
413
437
|
* Sort preference is memory-only (not persisted); restart resets to 'recent'.
|
|
@@ -531,8 +555,10 @@ export declare class CommandRouter {
|
|
|
531
555
|
* @param rootDir 浏览起点:卡片「返回」按钮回到这里。省略时默认 = 本次列出的
|
|
532
556
|
* 目录(`/ls <dir>` 的起点即该目录),因此「返回」不会把用户
|
|
533
557
|
* 丢回 workspace cwd(用户明确反馈过)。
|
|
558
|
+
* @param q 关键词筛选:只匹配**当前层**条目名(不递归),大小写不敏感。
|
|
559
|
+
* 省略/空 = 无筛选。
|
|
534
560
|
*/
|
|
535
|
-
cmdLs(args: string[], ctx: CommandContext, offset?: number, rootDir?: string): CommandResult;
|
|
561
|
+
cmdLs(args: string[], ctx: CommandContext, offset?: number, rootDir?: string, q?: string): CommandResult;
|
|
536
562
|
/**
|
|
537
563
|
* `/download <path>`(别名 `/d`):把本地文件直接发送到飞书。
|
|
538
564
|
*
|
|
@@ -547,7 +573,14 @@ export declare class CommandRouter {
|
|
|
547
573
|
* 此前直接回 `ls: xxx: Not a directory` 把文件路径当成错误处理。
|
|
548
574
|
*/
|
|
549
575
|
private buildFileCard;
|
|
550
|
-
|
|
576
|
+
/**
|
|
577
|
+
* `/ws` 实现(public:测试直接调用,替代 as unknown as,与 cmdLs 同款)。
|
|
578
|
+
*
|
|
579
|
+
* @param offset list 分支的分页起点(条目数)
|
|
580
|
+
* @param q list 分支的关键词筛选:name 或完整 path 命中即保留,大小写
|
|
581
|
+
* 不敏感。省略/空 = 无筛选。
|
|
582
|
+
*/
|
|
583
|
+
cmdWs(args: string[], ctx: CommandContext, offset?: number, q?: string): CommandResult;
|
|
551
584
|
/**
|
|
552
585
|
* L3: degraded card when a session's export is non-empty but unparseable
|
|
553
586
|
* (truncated by opencode's pipe cap on huge sessions, or otherwise corrupt).
|
package/dist/router/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { diffConfig, setNestedValue } from './config-diff.js';
|
|
|
11
11
|
import { getLogger } from '../logger/index.js';
|
|
12
12
|
import { stripPlaceholders } from '../inbound/placeholder.js';
|
|
13
13
|
import { activeRunUsage, clampInt } from './utils.js';
|
|
14
|
-
import { markdownDiv, buildSessionHistoryCard, paginationBar, parseJumpOffset, PAGE_JUMP_INVALID_HINT, } from './card-helpers.js';
|
|
14
|
+
import { markdownDiv, buildSessionHistoryCard, paginationBar, searchBar, parseJumpOffset, SEARCH_INPUT_NAME, PAGE_JUMP_INVALID_HINT, } from './card-helpers.js';
|
|
15
15
|
import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
|
|
16
16
|
import { atomicWrite } from '../persistence/atomic-write.js';
|
|
17
17
|
import { checkLatestVersion as defaultCheckLatestVersion, isNewer as defaultIsNewer, runInstallLatest as defaultRunInstallLatest, } from '../update/index.js';
|
|
@@ -290,9 +290,11 @@ const IMMEDIATE_ACTION_CMDS = new Set([
|
|
|
290
290
|
'ls.browse',
|
|
291
291
|
'ls.switch',
|
|
292
292
|
'ls.page', // control operation: paginate only, never spawns claude
|
|
293
|
+
'ls.filter', // control operation: refilter the /ls list only
|
|
293
294
|
'resume.page', // control operation: paginate only, never spawns claude
|
|
294
295
|
'active.page', // control operation: paginate active card
|
|
295
296
|
'ws.page', // control operation: paginate /ws list only
|
|
297
|
+
'ws.filter', // control operation: refilter the /ws list only
|
|
296
298
|
'ws.remove',
|
|
297
299
|
'ws.sort', // control operation: toggle sort mode only
|
|
298
300
|
'resume.use',
|
|
@@ -322,6 +324,19 @@ const IMMEDIATE_ACTION_CMDS = new Set([
|
|
|
322
324
|
function payloadOffset(value) {
|
|
323
325
|
return Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
324
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* 搜索框提交值单源读取:`inputValue` 主路(CardKit 2.0 input 提交(键盘回车/
|
|
329
|
+
* 完成键)经 raw `action.input_value` 回传,SDK normalizer 会丢弃
|
|
330
|
+
* `action.input_value`,依赖 connector 的 includeRawEvent)+
|
|
331
|
+
* `formValue[searchInput]` 回退。空串/纯空白一律归一为 undefined = 清除筛选,
|
|
332
|
+
* 不是报错也不是无操作。「清除筛选」按钮带 `clearQuery`,直接短路为 undefined。
|
|
333
|
+
*/
|
|
334
|
+
function searchQueryFrom(value) {
|
|
335
|
+
if (value.clearQuery)
|
|
336
|
+
return undefined;
|
|
337
|
+
const raw = value.inputValue ?? value.formValue?.[SEARCH_INPUT_NAME];
|
|
338
|
+
return raw?.trim() || undefined;
|
|
339
|
+
}
|
|
325
340
|
/** 卡片 cardAction payload 缺字段的统一报错文案(原先 12 处手写且已漂移出两种前缀)。 */
|
|
326
341
|
const CARD_PAYLOAD_MISSING = '⚠️ 卡片 payload 缺少必要信息';
|
|
327
342
|
export class CommandRouter {
|
|
@@ -524,8 +539,12 @@ export class CommandRouter {
|
|
|
524
539
|
return;
|
|
525
540
|
case 'ls.page':
|
|
526
541
|
return this.handleLsPage(value, ctx);
|
|
542
|
+
case 'ls.filter':
|
|
543
|
+
return this.handleLsFilter(value, ctx);
|
|
527
544
|
case 'ws.page':
|
|
528
545
|
return this.handleWsPage(value, ctx);
|
|
546
|
+
case 'ws.filter':
|
|
547
|
+
return this.handleWsFilter(value, ctx);
|
|
529
548
|
case 'ws.use':
|
|
530
549
|
return await this.handleWsUse(value, ctx);
|
|
531
550
|
case 'ws.sort':
|
|
@@ -932,7 +951,7 @@ export class CommandRouter {
|
|
|
932
951
|
return;
|
|
933
952
|
}
|
|
934
953
|
// Build edit card
|
|
935
|
-
// CardKit 2.0 input
|
|
954
|
+
// CardKit 2.0 input 提交(回车/完成键)时输入值走 raw.action.input_value
|
|
936
955
|
// (SDK normalizer 丢弃,需 connector includeRawEvent: true + index.ts 从 raw 提取)。
|
|
937
956
|
// 用 column_set + input + behaviors,不用 form 容器(form 触发 300123 无 submit button /
|
|
938
957
|
// 200621 嵌套 column,submit-typed button 也被 CardKit 2.0 拒绝 HTTP 400)。
|
|
@@ -952,7 +971,7 @@ export class CommandRouter {
|
|
|
952
971
|
{ tag: 'hr' },
|
|
953
972
|
{
|
|
954
973
|
tag: 'div',
|
|
955
|
-
text: { tag: 'lark_md', content: '💡
|
|
974
|
+
text: { tag: 'lark_md', content: '💡 输入新内容后按回车提交' },
|
|
956
975
|
},
|
|
957
976
|
{
|
|
958
977
|
tag: 'column_set',
|
|
@@ -1126,11 +1145,11 @@ export class CommandRouter {
|
|
|
1126
1145
|
try {
|
|
1127
1146
|
card =
|
|
1128
1147
|
targetPath && fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()
|
|
1129
|
-
? this.cmdLs([targetPath], ctx, offset, value.root)
|
|
1130
|
-
: this.cmdLs([], ctx, offset, value.root);
|
|
1148
|
+
? this.cmdLs([targetPath], ctx, offset, value.root, value.q)
|
|
1149
|
+
: this.cmdLs([], ctx, offset, value.root, value.q);
|
|
1131
1150
|
}
|
|
1132
1151
|
catch {
|
|
1133
|
-
card = this.cmdLs([], ctx, offset, value.root);
|
|
1152
|
+
card = this.cmdLs([], ctx, offset, value.root, value.q);
|
|
1134
1153
|
}
|
|
1135
1154
|
await this.bridge.updateCardInPlace(card.card, ctx);
|
|
1136
1155
|
}
|
|
@@ -1160,11 +1179,39 @@ export class CommandRouter {
|
|
|
1160
1179
|
if (!isDir) {
|
|
1161
1180
|
return { toast: { type: 'error', content: `路径无效: ${resolvedTarget}` } };
|
|
1162
1181
|
}
|
|
1163
|
-
const card = this.cmdLs([resolvedTarget], ctx, paging.offset, value.root);
|
|
1182
|
+
const card = this.cmdLs([resolvedTarget], ctx, paging.offset, value.root, value.q);
|
|
1164
1183
|
// 直接更新卡片,与 ls.browse/ls.refresh 行为一致
|
|
1165
1184
|
await this.bridge.updateCardInPlace(card.card, ctx);
|
|
1166
1185
|
return { toast: { type: 'success', content: '' } };
|
|
1167
1186
|
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Handle ls.filter: 按搜索框提交的关键词重新渲染当前目录卡。
|
|
1189
|
+
*
|
|
1190
|
+
* offset 恒重置为 0(新筛选从第 1 页开始)。路径校验与 TOCTOU 兜底照
|
|
1191
|
+
* handleLsPage——校验失败只回错误 toast,绝不动卡片(否则用户看到一张空卡)。
|
|
1192
|
+
* ls.browse / ls.switch 的 value 里刻意不带 q,换目录即清除筛选(设计文档 §2.3)。
|
|
1193
|
+
*/
|
|
1194
|
+
async handleLsFilter(value, ctx) {
|
|
1195
|
+
const targetPath = value.path;
|
|
1196
|
+
if (!targetPath) {
|
|
1197
|
+
return { toast: { type: 'error', content: '卡片 payload 缺少 path' } };
|
|
1198
|
+
}
|
|
1199
|
+
const q = searchQueryFrom(value);
|
|
1200
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
1201
|
+
let isDir;
|
|
1202
|
+
try {
|
|
1203
|
+
isDir = fs.existsSync(resolvedTarget) && fs.statSync(resolvedTarget).isDirectory();
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
isDir = false;
|
|
1207
|
+
}
|
|
1208
|
+
if (!isDir) {
|
|
1209
|
+
return { toast: { type: 'error', content: `路径无效: ${resolvedTarget}` } };
|
|
1210
|
+
}
|
|
1211
|
+
const card = this.cmdLs([resolvedTarget], ctx, 0, value.root, q);
|
|
1212
|
+
await this.bridge.updateCardInPlace(card.card, ctx);
|
|
1213
|
+
return { toast: { type: 'success', content: q ? `筛选:"${q}"` : '已清除筛选' } };
|
|
1214
|
+
}
|
|
1168
1215
|
/**
|
|
1169
1216
|
* Handle resume.page: paginate the /resume session list in place.
|
|
1170
1217
|
* Mirrors handleLsPage — updates the same card, never sends a new one.
|
|
@@ -1287,18 +1334,19 @@ export class CommandRouter {
|
|
|
1287
1334
|
}
|
|
1288
1335
|
/**
|
|
1289
1336
|
* W2.8 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
|
|
1337
|
+
* `q` 只服务 ws 卡的关键词筛选;order 卡无此功能,不透传。
|
|
1290
1338
|
*/
|
|
1291
|
-
rebuildListCard(kind, offset, ctx) {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1339
|
+
rebuildListCard(kind, offset, ctx, q) {
|
|
1340
|
+
if (kind === 'order')
|
|
1341
|
+
return this.cmdOrder([], ctx, offset).card;
|
|
1342
|
+
return this.cmdWs([], ctx, offset, q).card;
|
|
1295
1343
|
}
|
|
1296
1344
|
/**
|
|
1297
1345
|
* W2.8 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
|
|
1298
|
-
*
|
|
1346
|
+
* 翻页/移除/排序/筛选动作共用)。返回重建后的卡片;无卡时返回 undefined 且跳过更新。
|
|
1299
1347
|
*/
|
|
1300
1348
|
async refreshListCard(kind, value, ctx) {
|
|
1301
|
-
const card = this.rebuildListCard(kind, payloadOffset(value), ctx);
|
|
1349
|
+
const card = this.rebuildListCard(kind, payloadOffset(value), ctx, value.q);
|
|
1302
1350
|
if (!card)
|
|
1303
1351
|
return undefined;
|
|
1304
1352
|
await this.bridge.updateCardInPlace(card, ctx);
|
|
@@ -1361,7 +1409,7 @@ export class CommandRouter {
|
|
|
1361
1409
|
return { toast: { type: 'success', content: '' } };
|
|
1362
1410
|
}
|
|
1363
1411
|
/**
|
|
1364
|
-
* Handle order.aliasEdit: 展示别名编辑卡(input
|
|
1412
|
+
* Handle order.aliasEdit: 展示别名编辑卡(input,回车/完成键提交)。
|
|
1365
1413
|
* 有别名时预填;提交 `order.aliasInput` 处理(含校验与唯一性)。
|
|
1366
1414
|
*/
|
|
1367
1415
|
/**
|
|
@@ -1381,7 +1429,7 @@ export class CommandRouter {
|
|
|
1381
1429
|
}
|
|
1382
1430
|
// 列表卡显示的是截断版(≤100 字符 + ...),编辑卡预览保持一致
|
|
1383
1431
|
const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;
|
|
1384
|
-
// CardKit 2.0 input
|
|
1432
|
+
// CardKit 2.0 input 提交(回车/完成键;input_value 经 raw 回传,红线)。
|
|
1385
1433
|
// 不用 form 容器(触发 300123 无 submit button / 200621 嵌套 column)。
|
|
1386
1434
|
const editCard = {
|
|
1387
1435
|
schema: '2.0',
|
|
@@ -1436,7 +1484,7 @@ export class CommandRouter {
|
|
|
1436
1484
|
const order = this.orderStore.get().find((o) => o.id === value.orderId);
|
|
1437
1485
|
return this.showOrderEditCard(value, ctx, {
|
|
1438
1486
|
title: '✏️ 给指令起别名',
|
|
1439
|
-
hint: '💡
|
|
1487
|
+
hint: '💡 输入别名名后按回车提交;删除别名请点列表卡别名旁的 ✕',
|
|
1440
1488
|
inputName: 'aliasName',
|
|
1441
1489
|
placeholder: '输入 $别名名(如 all)...',
|
|
1442
1490
|
defaultValue: order?.alias ?? '',
|
|
@@ -1480,7 +1528,7 @@ export class CommandRouter {
|
|
|
1480
1528
|
});
|
|
1481
1529
|
}
|
|
1482
1530
|
/**
|
|
1483
|
-
* Handle order.textEdit: 展示指令文本编辑卡(input
|
|
1531
|
+
* Handle order.textEdit: 展示指令文本编辑卡(input,回车/完成键提交)。
|
|
1484
1532
|
* 预填当前 text(完整内容,不截断——input 组件有自有滚动条);
|
|
1485
1533
|
* 提交 `order.textInput` 处理(trim + 长度校验 + OrderStore.updateText)。
|
|
1486
1534
|
* 完全镜像 handleOrderAliasEdit 的 4 段结构(reload → find → 构造 editCard →
|
|
@@ -1492,7 +1540,7 @@ export class CommandRouter {
|
|
|
1492
1540
|
// input 组件 default_value 预填完整 text(不受截断影响),提交后生效完整文本
|
|
1493
1541
|
return this.showOrderEditCard(value, ctx, {
|
|
1494
1542
|
title: '✏️ 编辑指令',
|
|
1495
|
-
hint: '💡
|
|
1543
|
+
hint: '💡 修改后按回车提交;空白提交 = 报错;最多 200 字符',
|
|
1496
1544
|
inputName: 'text',
|
|
1497
1545
|
placeholder: '输入新的指令文本...',
|
|
1498
1546
|
defaultValue: order?.text ?? '',
|
|
@@ -1594,9 +1642,18 @@ export class CommandRouter {
|
|
|
1594
1642
|
return { toast: { type: 'error', content: paging.error } };
|
|
1595
1643
|
}
|
|
1596
1644
|
// cmdWs internally clamps stale/out-of-range offsets
|
|
1597
|
-
await this.refreshListCard('ws', { offset: paging.offset }, ctx);
|
|
1645
|
+
await this.refreshListCard('ws', { offset: paging.offset, q: value.q }, ctx);
|
|
1598
1646
|
return { toast: { type: 'success', content: '' } };
|
|
1599
1647
|
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Handle ws.filter: 按搜索框提交的关键词重新渲染列表卡。
|
|
1650
|
+
* offset 恒重置为 0(新筛选从第 1 页开始);提交空串 = 清除筛选。
|
|
1651
|
+
*/
|
|
1652
|
+
async handleWsFilter(value, ctx) {
|
|
1653
|
+
const q = searchQueryFrom(value);
|
|
1654
|
+
await this.refreshListCard('ws', { offset: 0, q }, ctx);
|
|
1655
|
+
return { toast: { type: 'success', content: q ? `筛选:"${q}"` : '已清除筛选' } };
|
|
1656
|
+
}
|
|
1600
1657
|
/**
|
|
1601
1658
|
* Handle ws.sort: toggle sort preference and refresh the /ws list card.
|
|
1602
1659
|
* Sort preference is memory-only (not persisted); restart resets to 'recent'.
|
|
@@ -1606,7 +1663,7 @@ export class CommandRouter {
|
|
|
1606
1663
|
const current = this.wsSortPreference.get(userId) ?? 'recent';
|
|
1607
1664
|
const next = current === 'recent' ? 'alpha' : 'recent';
|
|
1608
1665
|
this.wsSortPreference.set(userId, next);
|
|
1609
|
-
await this.refreshListCard('ws', { offset: 0 }, ctx);
|
|
1666
|
+
await this.refreshListCard('ws', { offset: 0, q: value.q }, ctx);
|
|
1610
1667
|
const nextLabel = next === 'recent' ? '🕐 最近使用' : '🔤 字母顺序';
|
|
1611
1668
|
return {
|
|
1612
1669
|
toast: { type: 'success', content: `已切换为 ${nextLabel}` },
|
|
@@ -1741,7 +1798,7 @@ export class CommandRouter {
|
|
|
1741
1798
|
}
|
|
1742
1799
|
case 'config.input': {
|
|
1743
1800
|
// Handle input field: value.key is the field key.
|
|
1744
|
-
// 2026-07-04: CardKit 2.0 input
|
|
1801
|
+
// 2026-07-04: CardKit 2.0 input 提交(回车/完成键)触发 callback,回传 input_value。
|
|
1745
1802
|
// Fall back to formValue[key]: the SDK currently reports input values
|
|
1746
1803
|
// via action.formValue for form-wrapped inputs.
|
|
1747
1804
|
const key = value.key;
|
|
@@ -2609,12 +2666,15 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2609
2666
|
* @param rootDir 浏览起点:卡片「返回」按钮回到这里。省略时默认 = 本次列出的
|
|
2610
2667
|
* 目录(`/ls <dir>` 的起点即该目录),因此「返回」不会把用户
|
|
2611
2668
|
* 丢回 workspace cwd(用户明确反馈过)。
|
|
2669
|
+
* @param q 关键词筛选:只匹配**当前层**条目名(不递归),大小写不敏感。
|
|
2670
|
+
* 省略/空 = 无筛选。
|
|
2612
2671
|
*/
|
|
2613
|
-
cmdLs(args, ctx, offset = 0, rootDir) {
|
|
2672
|
+
cmdLs(args, ctx, offset = 0, rootDir, q) {
|
|
2614
2673
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
2615
2674
|
if (!cwd) {
|
|
2616
2675
|
return { text: '请先使用 /cd <path> 设置工作目录' };
|
|
2617
2676
|
}
|
|
2677
|
+
const filter = q?.trim() || undefined;
|
|
2618
2678
|
// args can be:
|
|
2619
2679
|
// - [] : list current cwd
|
|
2620
2680
|
// - [dirName] : list subdirectory if it exists
|
|
@@ -2665,8 +2725,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2665
2725
|
size: fs.statSync(path.join(targetDir, f.name)).size,
|
|
2666
2726
|
})),
|
|
2667
2727
|
];
|
|
2728
|
+
// 关键词筛选:只匹配当前层条目名(不递归子目录)。大小写不敏感的子串包含
|
|
2729
|
+
// ——禁止 new RegExp(q)(用户搜 `foo(` 会直接抛)。插在排序之后、分页之前。
|
|
2730
|
+
const ql = filter?.toLowerCase();
|
|
2731
|
+
const visibleItems = ql
|
|
2732
|
+
? allItems.filter((i) => i.name.toLowerCase().includes(ql))
|
|
2733
|
+
: allItems;
|
|
2734
|
+
const visibleDirCount = visibleItems.reduce((n, i) => n + (i.isDir ? 1 : 0), 0);
|
|
2735
|
+
const visibleFileCount = visibleItems.length - visibleDirCount;
|
|
2668
2736
|
// Pagination calculations(clamp 到最后一页边界,原先不 clamp 会显示空页)
|
|
2669
|
-
const { totalCount, totalPages, safeOffset, currentPage, pageItems, hasPagination } = pageSlice(
|
|
2737
|
+
const { totalCount, totalPages, safeOffset, currentPage, pageItems, hasPagination } = pageSlice(visibleItems, offset, CommandRouter.LS_PAGE_SIZE);
|
|
2670
2738
|
// Check if we need to show parent directory button
|
|
2671
2739
|
const parentDir = path.dirname(targetDir);
|
|
2672
2740
|
const hasParent = parentDir !== targetDir;
|
|
@@ -2712,6 +2780,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2712
2780
|
path: targetDir,
|
|
2713
2781
|
offset: safeOffset,
|
|
2714
2782
|
root: browseRoot,
|
|
2783
|
+
...(filter ? { q: filter } : {}),
|
|
2715
2784
|
},
|
|
2716
2785
|
},
|
|
2717
2786
|
],
|
|
@@ -2763,8 +2832,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2763
2832
|
// Note: div+elements is not supported in CardKit 2.0, use column_set instead
|
|
2764
2833
|
const elements = [];
|
|
2765
2834
|
// Status line:只报条目构成;页码/总数交给分页栏文案行(同一信息不重复
|
|
2766
|
-
// 渲染两遍——2026-09-10
|
|
2767
|
-
|
|
2835
|
+
// 渲染两遍——2026-09-10 分页栏改成整行文案后重复会很明显)。筛选态一律用
|
|
2836
|
+
// 过滤后的计数,否则标题说 12 项、列表只有 3 项,卡片自相矛盾。
|
|
2837
|
+
const status = `\n共 ${visibleDirCount} 目录, ${visibleFileCount} 文件`;
|
|
2768
2838
|
// Header info + navigation buttons - show targetDir in header
|
|
2769
2839
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: `\`${targetDir}\`${status}` } });
|
|
2770
2840
|
elements.push({
|
|
@@ -2772,54 +2842,81 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2772
2842
|
columns: headerElements.map((e) => ({ tag: 'column', width: 'auto', elements: [e] })),
|
|
2773
2843
|
});
|
|
2774
2844
|
elements.push({ tag: 'hr' });
|
|
2775
|
-
//
|
|
2845
|
+
// 搜索行:头部导航按钮之下、内容区之上(主导航在上、过滤紧随其后、内容区
|
|
2846
|
+
// 在下)。path/root 必须进 callback value,否则提交后 handler 不知道刷哪。
|
|
2847
|
+
elements.push(searchBar({
|
|
2848
|
+
cmd: 'ls.filter',
|
|
2849
|
+
placeholder: '搜索本层目录/文件名,输完回车',
|
|
2850
|
+
currentQuery: filter,
|
|
2851
|
+
extra: { path: targetDir, root: browseRoot },
|
|
2852
|
+
}));
|
|
2853
|
+
if (filter) {
|
|
2854
|
+
elements.push({
|
|
2855
|
+
tag: 'div',
|
|
2856
|
+
text: {
|
|
2857
|
+
tag: 'lark_md',
|
|
2858
|
+
content: `🔍 筛选:"${filter}" · 命中 ${totalCount} 项(点「清除筛选」取消)`,
|
|
2859
|
+
},
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
// Section 1 + 2: Directories / Files (show only items on current page)
|
|
2776
2863
|
const pageDirs = pageItems.filter((i) => i.isDir);
|
|
2777
2864
|
const pageFiles = pageItems.filter((i) => !i.isDir);
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
tag: 'column_set',
|
|
2786
|
-
flex_mode: 'none',
|
|
2787
|
-
columns: [{ tag: 'column', width: 'auto', elements: [dirButton(d.name)] }],
|
|
2788
|
-
});
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
else if (dirs.length === 0) {
|
|
2792
|
-
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '无子目录' } });
|
|
2865
|
+
if (filter && visibleItems.length === 0) {
|
|
2866
|
+
// 筛选零命中:两个区标题都不渲染(两行空区标题只是噪音),搜索行仍在,
|
|
2867
|
+
// 用户可直接改词再回车,或点「清除筛选」。
|
|
2868
|
+
elements.push({
|
|
2869
|
+
tag: 'div',
|
|
2870
|
+
text: { tag: 'lark_md', content: `🔍 无匹配条目(关键词:"${filter}")` },
|
|
2871
|
+
});
|
|
2793
2872
|
}
|
|
2794
2873
|
else {
|
|
2795
2874
|
elements.push({
|
|
2796
2875
|
tag: 'div',
|
|
2797
|
-
text: { tag: 'lark_md', content:
|
|
2876
|
+
text: { tag: 'lark_md', content: `**📂 目录 (${visibleDirCount})**` },
|
|
2798
2877
|
});
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2878
|
+
if (pageDirs.length > 0) {
|
|
2879
|
+
for (const d of pageDirs) {
|
|
2880
|
+
elements.push({
|
|
2881
|
+
tag: 'column_set',
|
|
2882
|
+
flex_mode: 'none',
|
|
2883
|
+
columns: [{ tag: 'column', width: 'auto', elements: [dirButton(d.name)] }],
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
else if (visibleDirCount === 0) {
|
|
2888
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '无子目录' } });
|
|
2889
|
+
}
|
|
2890
|
+
else {
|
|
2808
2891
|
elements.push({
|
|
2809
|
-
tag: '
|
|
2810
|
-
|
|
2811
|
-
columns: [{ tag: 'column', width: 'auto', elements: [fileButton(f.name, f.size)] }],
|
|
2892
|
+
tag: 'div',
|
|
2893
|
+
text: { tag: 'lark_md', content: `(第 ${currentPage} 页无目录)` },
|
|
2812
2894
|
});
|
|
2813
2895
|
}
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '无文件' } });
|
|
2817
|
-
}
|
|
2818
|
-
else {
|
|
2896
|
+
elements.push({ tag: 'hr' });
|
|
2897
|
+
// Section 2: Files
|
|
2819
2898
|
elements.push({
|
|
2820
2899
|
tag: 'div',
|
|
2821
|
-
text: { tag: 'lark_md', content:
|
|
2900
|
+
text: { tag: 'lark_md', content: `**📄 文件 (${visibleFileCount})**` },
|
|
2822
2901
|
});
|
|
2902
|
+
if (pageFiles.length > 0) {
|
|
2903
|
+
for (const f of pageFiles) {
|
|
2904
|
+
elements.push({
|
|
2905
|
+
tag: 'column_set',
|
|
2906
|
+
flex_mode: 'none',
|
|
2907
|
+
columns: [{ tag: 'column', width: 'auto', elements: [fileButton(f.name, f.size)] }],
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
else if (visibleFileCount === 0) {
|
|
2912
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '无文件' } });
|
|
2913
|
+
}
|
|
2914
|
+
else {
|
|
2915
|
+
elements.push({
|
|
2916
|
+
tag: 'div',
|
|
2917
|
+
text: { tag: 'lark_md', content: `(第 ${currentPage} 页无文件)` },
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2823
2920
|
}
|
|
2824
2921
|
// Pagination bar (only shown when there are more items than PAGE_SIZE)
|
|
2825
2922
|
if (hasPagination) {
|
|
@@ -2828,7 +2925,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2828
2925
|
offset: safeOffset,
|
|
2829
2926
|
pageSize: CommandRouter.LS_PAGE_SIZE,
|
|
2830
2927
|
total: totalCount,
|
|
2831
|
-
extra: { path: targetDir, root: browseRoot },
|
|
2928
|
+
extra: { path: targetDir, root: browseRoot, ...(filter ? { q: filter } : {}) },
|
|
2832
2929
|
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 项)`,
|
|
2833
2930
|
});
|
|
2834
2931
|
elements.push({ tag: 'hr' });
|
|
@@ -2945,7 +3042,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2945
3042
|
},
|
|
2946
3043
|
};
|
|
2947
3044
|
}
|
|
2948
|
-
|
|
3045
|
+
/**
|
|
3046
|
+
* `/ws` 实现(public:测试直接调用,替代 as unknown as,与 cmdLs 同款)。
|
|
3047
|
+
*
|
|
3048
|
+
* @param offset list 分支的分页起点(条目数)
|
|
3049
|
+
* @param q list 分支的关键词筛选:name 或完整 path 命中即保留,大小写
|
|
3050
|
+
* 不敏感。省略/空 = 无筛选。
|
|
3051
|
+
*/
|
|
3052
|
+
cmdWs(args, ctx, offset = 0, q) {
|
|
2949
3053
|
const sub = args[0]?.toLowerCase();
|
|
2950
3054
|
switch (sub) {
|
|
2951
3055
|
case 'save': {
|
|
@@ -2994,6 +3098,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2994
3098
|
}
|
|
2995
3099
|
case 'list':
|
|
2996
3100
|
default: {
|
|
3101
|
+
const filter = q?.trim() || undefined;
|
|
2997
3102
|
const entries = this.workspaceStore.list();
|
|
2998
3103
|
const currentCwd = this.sessionStore.getCwd(ctx.userId);
|
|
2999
3104
|
const sortMode = this.wsSortPreference.get(ctx.userId) ?? 'recent';
|
|
@@ -3010,9 +3115,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3010
3115
|
// Alphabetical by alias name
|
|
3011
3116
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
3012
3117
|
}
|
|
3118
|
+
// 关键词筛选:alias name 或完整 path 命中即保留,大小写不敏感的子串包含
|
|
3119
|
+
// (禁止 new RegExp(q)——用户搜 `foo(` 会直接抛)。插在排序之后、分页
|
|
3120
|
+
// 之前:先分页再过滤会出现某页只剩 1 条的残页,且翻页时筛选像是丢了。
|
|
3121
|
+
const ql = filter?.toLowerCase();
|
|
3122
|
+
const visible = ql
|
|
3123
|
+
? entries.filter((e) => e.name.toLowerCase().includes(ql) || e.path.toLowerCase().includes(ql))
|
|
3124
|
+
: entries;
|
|
3013
3125
|
// Pagination calculations (mirror cmdOrder): clamp stale/out-of-range
|
|
3014
3126
|
// offsets so prev/next always step by WS_PAGE_SIZE.
|
|
3015
|
-
const { totalCount, totalPages, safeOffset, currentPage, pageItems: pageEntries, hasPagination, } = pageSlice(
|
|
3127
|
+
const { totalCount, totalPages, safeOffset, currentPage, pageItems: pageEntries, hasPagination, } = pageSlice(visible, offset, WS_PAGE_SIZE);
|
|
3016
3128
|
// Build body elements: current cwd + workspace list with dividers
|
|
3017
3129
|
const bodyElements = [];
|
|
3018
3130
|
// Current working directory section
|
|
@@ -3020,13 +3132,35 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3020
3132
|
tag: 'div',
|
|
3021
3133
|
text: { tag: 'lark_md', content: `📂 当前工作目录:\`${currentCwd ?? '(未设置)'}\`` },
|
|
3022
3134
|
});
|
|
3023
|
-
|
|
3024
|
-
|
|
3135
|
+
bodyElements.push({ tag: 'hr' });
|
|
3136
|
+
// 搜索行:「当前工作目录」之下、排序行之上(与 /ls 同一视觉动线:主信息
|
|
3137
|
+
// 在上、过滤紧随其后、列表在下)
|
|
3138
|
+
bodyElements.push(searchBar({
|
|
3139
|
+
cmd: 'ws.filter',
|
|
3140
|
+
placeholder: '搜索名称或路径,输完回车',
|
|
3141
|
+
currentQuery: filter,
|
|
3142
|
+
}));
|
|
3143
|
+
if (filter) {
|
|
3144
|
+
bodyElements.push({
|
|
3145
|
+
tag: 'div',
|
|
3146
|
+
text: {
|
|
3147
|
+
tag: 'lark_md',
|
|
3148
|
+
content: `🔍 筛选:"${filter}" · 命中 ${totalCount} 个 workspace(点「清除筛选」取消)`,
|
|
3149
|
+
},
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
if (entries.length === 0 && !filter) {
|
|
3025
3153
|
bodyElements.push({
|
|
3026
3154
|
tag: 'div',
|
|
3027
3155
|
text: { tag: 'lark_md', content: '没有保存的 workspace' },
|
|
3028
3156
|
});
|
|
3029
3157
|
}
|
|
3158
|
+
else if (filter && visible.length === 0) {
|
|
3159
|
+
bodyElements.push({
|
|
3160
|
+
tag: 'div',
|
|
3161
|
+
text: { tag: 'lark_md', content: `🔍 无匹配条目(关键词:"${filter}")` },
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3030
3164
|
else {
|
|
3031
3165
|
// Sort mode indicator + toggle button: placed above the list so the user
|
|
3032
3166
|
// sees "current mode" and "switch to X" before scanning entries.
|
|
@@ -3059,7 +3193,12 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3059
3193
|
text: { tag: 'plain_text', content: `切换为 ${switchLabel}` },
|
|
3060
3194
|
type: 'default',
|
|
3061
3195
|
size: 'small',
|
|
3062
|
-
behaviors: [
|
|
3196
|
+
behaviors: [
|
|
3197
|
+
{
|
|
3198
|
+
type: 'callback',
|
|
3199
|
+
value: { cmd: 'ws.sort', ...(filter ? { q: filter } : {}) },
|
|
3200
|
+
},
|
|
3201
|
+
],
|
|
3063
3202
|
},
|
|
3064
3203
|
],
|
|
3065
3204
|
},
|
|
@@ -3087,7 +3226,12 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3087
3226
|
behaviors: [
|
|
3088
3227
|
{
|
|
3089
3228
|
type: 'callback',
|
|
3090
|
-
value: {
|
|
3229
|
+
value: {
|
|
3230
|
+
cmd: 'ws.use',
|
|
3231
|
+
name: entry.name,
|
|
3232
|
+
offset: safeOffset,
|
|
3233
|
+
...(filter ? { q: filter } : {}),
|
|
3234
|
+
},
|
|
3091
3235
|
},
|
|
3092
3236
|
],
|
|
3093
3237
|
},
|
|
@@ -3105,7 +3249,12 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3105
3249
|
behaviors: [
|
|
3106
3250
|
{
|
|
3107
3251
|
type: 'callback',
|
|
3108
|
-
value: {
|
|
3252
|
+
value: {
|
|
3253
|
+
cmd: 'ws.remove',
|
|
3254
|
+
name: entry.name,
|
|
3255
|
+
offset: safeOffset,
|
|
3256
|
+
...(filter ? { q: filter } : {}),
|
|
3257
|
+
},
|
|
3109
3258
|
},
|
|
3110
3259
|
],
|
|
3111
3260
|
},
|
|
@@ -3124,6 +3273,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3124
3273
|
offset: safeOffset,
|
|
3125
3274
|
pageSize: WS_PAGE_SIZE,
|
|
3126
3275
|
total: totalCount,
|
|
3276
|
+
extra: filter ? { q: filter } : {},
|
|
3127
3277
|
label: `**${currentPage}/${totalPages}**(${totalCount})`,
|
|
3128
3278
|
prevText: '⬅',
|
|
3129
3279
|
nextText: '➡',
|
|
@@ -3836,7 +3986,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3836
3986
|
],
|
|
3837
3987
|
},
|
|
3838
3988
|
{
|
|
3839
|
-
// 编辑:弹蓝色 header 输入卡(CardKit 2.0 input
|
|
3989
|
+
// 编辑:弹蓝色 header 输入卡(CardKit 2.0 input,回车提交),
|
|
3840
3990
|
// 镜像 order.aliasEdit 流程。order.textEdit 是 control-only action
|
|
3841
3991
|
// (isImmediateAction 注册),不 spawn agent。
|
|
3842
3992
|
tag: 'column',
|
|
@@ -48,6 +48,12 @@ export declare class FakeDshServer {
|
|
|
48
48
|
setMuxFrames(frames: Array<object | {
|
|
49
49
|
close: true;
|
|
50
50
|
}>): void;
|
|
51
|
+
/** Append a single frame at response time (real DSH only produces a turn's
|
|
52
|
+
* events after accepting its prompt) — avoids pre-queued frames racing a
|
|
53
|
+
* previous run's still-closing mux subscription. */
|
|
54
|
+
pushMuxFrame(frame: object | {
|
|
55
|
+
close: true;
|
|
56
|
+
}): void;
|
|
51
57
|
setHistoryValue(value: unknown): void;
|
|
52
58
|
/** Synchronous unary dispatch (no HTTP) — used by tests that inject the
|
|
53
59
|
* reader's sync transport to avoid the spawnSync-in-process deadlock. */
|
|
@@ -95,6 +95,12 @@ export class FakeDshServer {
|
|
|
95
95
|
setMuxFrames(frames) {
|
|
96
96
|
this.muxFrames = frames;
|
|
97
97
|
}
|
|
98
|
+
/** Append a single frame at response time (real DSH only produces a turn's
|
|
99
|
+
* events after accepting its prompt) — avoids pre-queued frames racing a
|
|
100
|
+
* previous run's still-closing mux subscription. */
|
|
101
|
+
pushMuxFrame(frame) {
|
|
102
|
+
this.muxFrames.push(frame);
|
|
103
|
+
}
|
|
98
104
|
setHistoryValue(value) {
|
|
99
105
|
this.historyValue = value;
|
|
100
106
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-remote",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|