lark-remote 0.4.0 → 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 +35 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/bridge/inbound-media.d.ts +13 -20
- package/dist/bridge/inbound-media.js +107 -93
- package/dist/bridge/index.d.ts +6 -6
- package/dist/bridge/index.js +6 -12
- package/dist/card/approval-render.js +9 -8
- package/dist/cli.js +5 -1
- package/dist/config/dir.d.ts +9 -0
- package/dist/config/dir.js +69 -0
- package/dist/config/index.d.ts +22 -3
- package/dist/config/index.js +25 -3
- package/dist/connector/index.d.ts +22 -1
- package/dist/connector/index.js +79 -14
- package/dist/inbound/event.d.ts +41 -0
- package/dist/inbound/event.js +11 -0
- package/dist/inbound/placeholder.d.ts +26 -0
- package/dist/inbound/placeholder.js +149 -0
- package/dist/inbound/prompt.d.ts +20 -0
- package/dist/inbound/prompt.js +47 -0
- package/dist/inbound/receipt.d.ts +28 -0
- package/dist/inbound/receipt.js +62 -0
- package/dist/inbound/turn-assembler.d.ts +63 -0
- package/dist/inbound/turn-assembler.js +233 -0
- package/dist/inbound/turn.d.ts +48 -0
- package/dist/inbound/turn.js +10 -0
- package/dist/index.js +96 -71
- package/dist/router/card-helpers.d.ts +38 -2
- package/dist/router/card-helpers.js +79 -2
- package/dist/router/config/opencode.d.ts +1 -1
- package/dist/router/config/opencode.js +11 -0
- package/dist/router/index.d.ts +56 -6
- package/dist/router/index.js +256 -78
- package/dist/runner/common/connection-based-runner.js +6 -2
- package/dist/runner/dsh/fake-dsh-server.d.ts +6 -0
- package/dist/runner/dsh/fake-dsh-server.js +6 -0
- package/dist/runner/kimi/acp/runner.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.js +1 -1
- package/dist/runner/opencode/acp/runner.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,6 +24,8 @@ import { startSleepBlocker } from './platform/sleep-blocker.js';
|
|
|
24
24
|
import { checkLatestVersion, isNewer, runInstallLatest, formatUpdateHint } from './update/index.js';
|
|
25
25
|
import { classifyRejection } from './error-classification.js';
|
|
26
26
|
import { WorkspaceStore } from './workspace/index.js';
|
|
27
|
+
import { InboundTurnAssembler } from './inbound/turn-assembler.js';
|
|
28
|
+
import { stripPlaceholders } from './inbound/placeholder.js';
|
|
27
29
|
import { buildSessionHistoryCard } from './router/card-helpers.js';
|
|
28
30
|
import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
|
|
29
31
|
import path from 'node:path';
|
|
@@ -197,6 +199,12 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
197
199
|
model,
|
|
198
200
|
sessionReader: opencodeSessionReader,
|
|
199
201
|
mode: ocConfig?.mode ?? 'build',
|
|
202
|
+
binary: ocConfig?.acp?.binary,
|
|
203
|
+
requestTimeoutMs: ocConfig?.acp?.requestTimeoutMs,
|
|
204
|
+
idleTtlMs: ocConfig?.acp?.idleTtlMs,
|
|
205
|
+
turnIdleTimeoutMs: ocConfig?.acp?.turnIdleTimeoutMinutes != null
|
|
206
|
+
? ocConfig.acp.turnIdleTimeoutMinutes * 60_000
|
|
207
|
+
: undefined,
|
|
200
208
|
});
|
|
201
209
|
});
|
|
202
210
|
sessionReaderRegistry.register('opencode', opencodeSessionReader);
|
|
@@ -292,9 +300,48 @@ function initializeRunner(config, configDir, cliArgs) {
|
|
|
292
300
|
}
|
|
293
301
|
/** Setup message and card action handlers. */
|
|
294
302
|
function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger, config, cloneSession) {
|
|
295
|
-
//
|
|
303
|
+
// 入站统一装配器(2026-09-15):图/文任意顺序 → 同一个 turn、同一个 prompt,
|
|
304
|
+
// 附件路径统一注入 prompt;无文本纯附件只回执(决策 2)。
|
|
305
|
+
const assembler = new InboundTurnAssembler({
|
|
306
|
+
onCommit: (turn, prompt) => {
|
|
307
|
+
// 入队时刻(T0)快照 workspace + agent/session 绑定,语义与旧路径一致
|
|
308
|
+
// (排队期间 /cd、/config 不再导致语义漂移)。
|
|
309
|
+
const messageId = turn.messageIds[turn.messageIds.length - 1] ?? '';
|
|
310
|
+
const ctx = { userId: turn.userId, chatId: turn.chatId, messageId };
|
|
311
|
+
let workspace = sessionStore.getCwd(turn.userId) ?? '';
|
|
312
|
+
if (!workspace && workspaceStore) {
|
|
313
|
+
const workspaces = workspaceStore.list();
|
|
314
|
+
if (workspaces.length > 0)
|
|
315
|
+
workspace = workspaces[0].path;
|
|
316
|
+
}
|
|
317
|
+
const binding = bridge.currentBinding(turn.userId);
|
|
318
|
+
bridge.enqueue(workspace, async () => {
|
|
319
|
+
// allowCommandPrefix: false —— prompt 已剥离占位符,绝不再做命令分发。
|
|
320
|
+
await router.handle(prompt, ctx, {
|
|
321
|
+
cwdOverride: workspace,
|
|
322
|
+
binding,
|
|
323
|
+
allowCommandPrefix: false,
|
|
324
|
+
});
|
|
325
|
+
}, {
|
|
326
|
+
taskMeta: {
|
|
327
|
+
userId: turn.userId,
|
|
328
|
+
chatId: turn.chatId,
|
|
329
|
+
messageId,
|
|
330
|
+
messagePreview: prompt.slice(0, 3000),
|
|
331
|
+
binding,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
onReceipt: async (ctx, text) => {
|
|
336
|
+
await bridge.sendResult({ text }, ctx);
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
router.setInboundFlusher(() => assembler.flushAll('flush'));
|
|
340
|
+
// 入站媒体(图片/文件/视频/语音/表情):先认证后下载(P1 review 修复)。
|
|
296
341
|
// connector 只上报"媒体到达",这里先过 owner + enabled 闸门,通过后才
|
|
297
342
|
// 下载——未认证/关闭配置时不会发生任何网络下载或内存/磁盘占用。
|
|
343
|
+
// 下载是异步的:立刻把在途 promise 交给装配器(`kind: 'media'`),
|
|
344
|
+
// 保证「先图后文」不会先提交一个只含文本的 turn。
|
|
298
345
|
connector.setInboundMediaDetectedHandler((msg) => {
|
|
299
346
|
if (!binder.isOwner(msg.userId)) {
|
|
300
347
|
logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
|
|
@@ -304,35 +351,45 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
304
351
|
// 启动时捕获的 config 参数会过期(P2 review 修复)。
|
|
305
352
|
if (!router.config.inboundMedia.enabled) {
|
|
306
353
|
// 不静默(P3 review):关闭时给 owner 明确反馈,避免发图后无任何反应。
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
text: '⚠️ 入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
|
|
311
|
-
'如需自动保存图片/文件,请开启后重试',
|
|
312
|
-
}, {
|
|
354
|
+
// 不下载、不落盘,只进 rejected(随 turn 回执一起发出)。
|
|
355
|
+
assembler.ingest({
|
|
356
|
+
kind: 'rejected',
|
|
313
357
|
userId: msg.userId,
|
|
314
358
|
chatId: msg.chatId,
|
|
315
359
|
messageId: msg.messageId,
|
|
316
|
-
|
|
317
|
-
|
|
360
|
+
replyToMessageId: msg.replyToMessageId,
|
|
361
|
+
rawContentType: msg.rawContentType,
|
|
362
|
+
rejectedKind: 'file',
|
|
363
|
+
reason: '入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
|
|
364
|
+
'如需自动保存图片/文件,请开启后重试',
|
|
365
|
+
});
|
|
318
366
|
return;
|
|
319
367
|
}
|
|
320
|
-
|
|
368
|
+
const outcome = (async () => {
|
|
321
369
|
const payload = await connector.downloadInboundMedia(msg, {
|
|
322
370
|
maxFileSizeMb: router.config.inboundMedia.maxFileSizeMb,
|
|
323
371
|
});
|
|
324
372
|
try {
|
|
325
|
-
await bridge.
|
|
373
|
+
return await bridge.saveInboundMedia(payload);
|
|
326
374
|
}
|
|
327
375
|
catch (err) {
|
|
328
|
-
//
|
|
376
|
+
// save() 的正常路径会清理临时文件;这里兜底 save 进入 mkdir
|
|
329
377
|
// try 之前意外抛错的情况,避免 os.tmpdir 残留。
|
|
330
378
|
for (const item of payload.media) {
|
|
331
379
|
silentlyUnlink(item.tempPath);
|
|
332
380
|
}
|
|
333
381
|
throw err;
|
|
334
382
|
}
|
|
335
|
-
})()
|
|
383
|
+
})();
|
|
384
|
+
assembler.ingest({
|
|
385
|
+
kind: 'media',
|
|
386
|
+
userId: msg.userId,
|
|
387
|
+
chatId: msg.chatId,
|
|
388
|
+
messageId: msg.messageId,
|
|
389
|
+
replyToMessageId: msg.replyToMessageId,
|
|
390
|
+
rawContentType: msg.rawContentType,
|
|
391
|
+
outcome,
|
|
392
|
+
});
|
|
336
393
|
});
|
|
337
394
|
connector.setMessageHandler((msg) => {
|
|
338
395
|
// 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时首条消息(任意内容)认领
|
|
@@ -387,10 +444,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
387
444
|
return;
|
|
388
445
|
}
|
|
389
446
|
// decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
|
|
390
|
-
logger.info(`message from ${msg.userId}: ${msg.content.slice(0, 100)}`);
|
|
391
|
-
// 文本到达先冲刷待合批的媒体保存提示(先图后文字时文字到达
|
|
392
|
-
// 先冲刷批次,避免合批提示被后续消息淹没/丢图)。
|
|
393
|
-
bridge.flushMediaNotifications(msg.userId, msg.chatId);
|
|
447
|
+
logger.info(`message from ${msg.userId} (${msg.rawContentType}): ${msg.content.slice(0, 100)}`);
|
|
394
448
|
// Add Typing reaction to indicate "still alive"
|
|
395
449
|
void connector.addReaction(msg.messageId, 'Typing');
|
|
396
450
|
// 复制分身流程活跃期:一切消息先经 clone 状态机(stop 命令除外,保留其
|
|
@@ -425,67 +479,38 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
|
|
|
425
479
|
})().catch((err) => logger.error('[control] /stop failed:', err));
|
|
426
480
|
return;
|
|
427
481
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
})
|
|
435
|
-
.catch((err) => logger.error('[control] slash command failed:', err));
|
|
436
|
-
return;
|
|
482
|
+
// 先清洗,后判语义(§5.6):结构占位符永远不能触发命令分发。
|
|
483
|
+
// `` 的首字符恰好是 `!` —— 2026-09-15 事故就是这么进 shell 的。
|
|
484
|
+
const stripped = stripPlaceholders(content);
|
|
485
|
+
if (stripped.unknownTags.length > 0) {
|
|
486
|
+
logger.warn(`[inbound] unknown placeholder tags stripped: ${stripped.unknownTags.join(', ')} ` +
|
|
487
|
+
`(msg_type=${msg.rawContentType})`);
|
|
437
488
|
}
|
|
438
|
-
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (content.trim().startsWith('!')) {
|
|
489
|
+
const commandEligible = msg.rawContentType === 'text' && stripped.kinds.length === 0 && stripped.clean !== '';
|
|
490
|
+
// 命令消息旁路装配窗口:其它 /命令 立即执行,窗口内已装配的内容
|
|
491
|
+
// 作为独立提交冲刷(/stop 已在上面处理,立即生效不等待)。
|
|
492
|
+
if (commandEligible && (stripped.clean.startsWith('/') || stripped.clean.startsWith('!'))) {
|
|
493
|
+
void assembler.flush(msg.userId, msg.chatId, 'flush');
|
|
444
494
|
void router
|
|
445
|
-
.handle(
|
|
495
|
+
.handle(stripped.clean, {
|
|
446
496
|
userId: msg.userId,
|
|
447
497
|
chatId: msg.chatId,
|
|
448
498
|
messageId: msg.messageId,
|
|
449
|
-
})
|
|
450
|
-
.catch((err) => logger.error('[control]
|
|
499
|
+
}, { allowCommandPrefix: true })
|
|
500
|
+
.catch((err) => logger.error('[control] command failed:', err));
|
|
451
501
|
return;
|
|
452
502
|
}
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// Step4/D4: 入队时刻(T0)快照 agent+session,随任务闭包带到 T1 执行时刻,
|
|
465
|
-
// 避免排队期间 /new、/config 改写 live 状态导致语义漂移(方案 D4)。唯一
|
|
466
|
-
// 捕获点 Bridge.currentBinding。
|
|
467
|
-
const binding = bridge.currentBinding(msg.userId);
|
|
468
|
-
bridge.enqueue(workspace, async () => {
|
|
469
|
-
await router.handle(content, {
|
|
470
|
-
userId: msg.userId,
|
|
471
|
-
chatId: msg.chatId,
|
|
472
|
-
messageId: msg.messageId,
|
|
473
|
-
},
|
|
474
|
-
// P1-14: lane 与执行 cwd 同源 —— 入队时捕获的 workspace 显式传给
|
|
475
|
-
// forwardToClaude,排队期间 /cd 不再导致旧 lane 消息被 busy-drop。
|
|
476
|
-
// D4/Step4: binding 同源 —— 入队时快照的 agent+session 一并透传,
|
|
477
|
-
// 排队期间 /new、/config 不再导致旧 lane 消息语义漂移。
|
|
478
|
-
{ cwdOverride: workspace, binding });
|
|
479
|
-
}, {
|
|
480
|
-
taskMeta: {
|
|
481
|
-
userId: msg.userId,
|
|
482
|
-
chatId: msg.chatId,
|
|
483
|
-
messageId: msg.messageId,
|
|
484
|
-
messagePreview: content.slice(0, 3000),
|
|
485
|
-
// D3/Step4: binding 随 taskMeta 存进 QueuedTask,供替换闭包
|
|
486
|
-
// (queue.edit/queue.immediate)复用,不重新快照。
|
|
487
|
-
binding,
|
|
488
|
-
},
|
|
503
|
+
// 普通消息:进装配器,等静默期窗口到期(下载落定)后合并成一个 turn。
|
|
504
|
+
assembler.ingest({
|
|
505
|
+
kind: 'text',
|
|
506
|
+
userId: msg.userId,
|
|
507
|
+
chatId: msg.chatId,
|
|
508
|
+
messageId: msg.messageId,
|
|
509
|
+
replyToMessageId: msg.replyToMessageId,
|
|
510
|
+
rawContentType: msg.rawContentType,
|
|
511
|
+
text: stripped.clean,
|
|
512
|
+
placeholders: stripped.kinds,
|
|
513
|
+
unknownTags: stripped.unknownTags,
|
|
489
514
|
});
|
|
490
515
|
});
|
|
491
516
|
connector.setCardActionHandler(async (action) => {
|
|
@@ -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).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* OpenCode config builder — builds fields and handles field changes for OpenCode agent.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentConfigCardBuilder, ConfigField } from './types.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type AppConfig } from '../../config/index.js';
|
|
6
6
|
export declare class OpencodeConfigBuilder implements AgentConfigCardBuilder {
|
|
7
7
|
buildFields(displayConfig: AppConfig): ConfigField[];
|
|
8
8
|
handleFieldChange(key: string, value: unknown, config: AppConfig): Array<{
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* OpenCode config builder — builds fields and handles field changes for OpenCode agent.
|
|
3
3
|
*/
|
|
4
4
|
import { loadOpencodeConfig } from '../../config/opencode-config.js';
|
|
5
|
+
import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
|
|
5
6
|
import { resetModelPatch } from './common/model-patch.js';
|
|
6
7
|
const MODE_OPTIONS = [
|
|
7
8
|
{ text: 'build(默认,逐项审批)', value: 'build' },
|
|
@@ -38,6 +39,16 @@ export class OpencodeConfigBuilder {
|
|
|
38
39
|
options: MODE_OPTIONS,
|
|
39
40
|
currentValue: currentMode,
|
|
40
41
|
});
|
|
42
|
+
// Turn 空闲超时(纯 ACP 对齐 kimi:卡片暴露 turnIdleTimeoutMinutes,
|
|
43
|
+
// binary/requestTimeoutMs/idleTtlMs 为 YAML-only 工程参数)
|
|
44
|
+
const currentTurnIdleTimeoutMinutes = displayConfig.agents?.opencode?.acp?.turnIdleTimeoutMinutes ??
|
|
45
|
+
DEFAULT_TURN_IDLE_TIMEOUT_MINUTES;
|
|
46
|
+
fields.push({
|
|
47
|
+
key: 'agents.opencode.acp.turnIdleTimeoutMinutes',
|
|
48
|
+
label: 'Turn 空闲超时(分钟, 0关闭)',
|
|
49
|
+
type: 'input',
|
|
50
|
+
currentValue: String(currentTurnIdleTimeoutMinutes),
|
|
51
|
+
});
|
|
41
52
|
return fields;
|
|
42
53
|
}
|
|
43
54
|
handleFieldChange(key, value, config) {
|
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. */
|
|
@@ -121,6 +131,11 @@ export declare class CommandRouter {
|
|
|
121
131
|
* returns its pid. Injected from index.ts; /restart is unavailable without it.
|
|
122
132
|
*/
|
|
123
133
|
private restartSpawner?;
|
|
134
|
+
/**
|
|
135
|
+
* 冲刷装配器窗口内未提交的入站 turn(/exit、/restart 干净退出前调用,
|
|
136
|
+
* 避免静默期窗口内的内容丢失)。index.ts 注入。
|
|
137
|
+
*/
|
|
138
|
+
private flushInbound?;
|
|
124
139
|
private idleTimeoutMs;
|
|
125
140
|
/** Dev mode flag: --dev means the bridge was started from source (bun src/index.ts). */
|
|
126
141
|
private devMode;
|
|
@@ -197,12 +212,24 @@ export declare class CommandRouter {
|
|
|
197
212
|
* The reader is fetched from the registry each time to reflect config changes.
|
|
198
213
|
*/
|
|
199
214
|
private get sessionReader();
|
|
215
|
+
/**
|
|
216
|
+
* 注入入站装配器的冲刷回调(/exit、/restart 干净退出前调用,
|
|
217
|
+
* 避免静默期窗口内未提交的 turn 丢失)。index.ts 在构造后接线。
|
|
218
|
+
*/
|
|
219
|
+
setInboundFlusher(flush: () => Promise<void>): void;
|
|
200
220
|
/**
|
|
201
221
|
* Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
|
|
222
|
+
*
|
|
223
|
+
* 命令识别前置条件(2026-09-15 P0 修复):
|
|
224
|
+
* 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 ``,
|
|
225
|
+
* 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
|
|
226
|
+
* 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
|
|
227
|
+
* 三条同时成立时生效,其余一律按文本转发。
|
|
202
228
|
*/
|
|
203
229
|
handle(message: string, ctx: CommandContext, opts?: {
|
|
204
230
|
cwdOverride?: string;
|
|
205
231
|
binding?: AgentBinding;
|
|
232
|
+
allowCommandPrefix?: boolean;
|
|
206
233
|
}): Promise<CommandResult | null>;
|
|
207
234
|
/**
|
|
208
235
|
* 一次别名展开:bridge 收到用户消息后、命令分发前调用。
|
|
@@ -270,6 +297,14 @@ export declare class CommandRouter {
|
|
|
270
297
|
* Handle ls.page: paginate to a specific offset.
|
|
271
298
|
*/
|
|
272
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;
|
|
273
308
|
/**
|
|
274
309
|
* Handle resume.page: paginate the /resume session list in place.
|
|
275
310
|
* Mirrors handleLsPage — updates the same card, never sends a new one.
|
|
@@ -314,11 +349,12 @@ export declare class CommandRouter {
|
|
|
314
349
|
} | null;
|
|
315
350
|
/**
|
|
316
351
|
* W2.8 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
|
|
352
|
+
* `q` 只服务 ws 卡的关键词筛选;order 卡无此功能,不透传。
|
|
317
353
|
*/
|
|
318
354
|
private rebuildListCard;
|
|
319
355
|
/**
|
|
320
356
|
* W2.8 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
|
|
321
|
-
*
|
|
357
|
+
* 翻页/移除/排序/筛选动作共用)。返回重建后的卡片;无卡时返回 undefined 且跳过更新。
|
|
322
358
|
*/
|
|
323
359
|
private refreshListCard;
|
|
324
360
|
/**
|
|
@@ -339,7 +375,7 @@ export declare class CommandRouter {
|
|
|
339
375
|
*/
|
|
340
376
|
private handleOrderPage;
|
|
341
377
|
/**
|
|
342
|
-
* Handle order.aliasEdit: 展示别名编辑卡(input
|
|
378
|
+
* Handle order.aliasEdit: 展示别名编辑卡(input,回车/完成键提交)。
|
|
343
379
|
* 有别名时预填;提交 `order.aliasInput` 处理(含校验与唯一性)。
|
|
344
380
|
*/
|
|
345
381
|
/**
|
|
@@ -356,7 +392,7 @@ export declare class CommandRouter {
|
|
|
356
392
|
/** Handle order.aliasRemove: 删除某条指令的别名并原地重渲染列表卡。 */
|
|
357
393
|
private handleOrderAliasRemove;
|
|
358
394
|
/**
|
|
359
|
-
* Handle order.textEdit: 展示指令文本编辑卡(input
|
|
395
|
+
* Handle order.textEdit: 展示指令文本编辑卡(input,回车/完成键提交)。
|
|
360
396
|
* 预填当前 text(完整内容,不截断——input 组件有自有滚动条);
|
|
361
397
|
* 提交 `order.textInput` 处理(trim + 长度校验 + OrderStore.updateText)。
|
|
362
398
|
* 完全镜像 handleOrderAliasEdit 的 4 段结构(reload → find → 构造 editCard →
|
|
@@ -391,6 +427,11 @@ export declare class CommandRouter {
|
|
|
391
427
|
* Mirrors handleOrderPage — updates the same card, never sends a new one.
|
|
392
428
|
*/
|
|
393
429
|
private handleWsPage;
|
|
430
|
+
/**
|
|
431
|
+
* Handle ws.filter: 按搜索框提交的关键词重新渲染列表卡。
|
|
432
|
+
* offset 恒重置为 0(新筛选从第 1 页开始);提交空串 = 清除筛选。
|
|
433
|
+
*/
|
|
434
|
+
private handleWsFilter;
|
|
394
435
|
/**
|
|
395
436
|
* Handle ws.sort: toggle sort preference and refresh the /ws list card.
|
|
396
437
|
* Sort preference is memory-only (not persisted); restart resets to 'recent'.
|
|
@@ -514,8 +555,10 @@ export declare class CommandRouter {
|
|
|
514
555
|
* @param rootDir 浏览起点:卡片「返回」按钮回到这里。省略时默认 = 本次列出的
|
|
515
556
|
* 目录(`/ls <dir>` 的起点即该目录),因此「返回」不会把用户
|
|
516
557
|
* 丢回 workspace cwd(用户明确反馈过)。
|
|
558
|
+
* @param q 关键词筛选:只匹配**当前层**条目名(不递归),大小写不敏感。
|
|
559
|
+
* 省略/空 = 无筛选。
|
|
517
560
|
*/
|
|
518
|
-
cmdLs(args: string[], ctx: CommandContext, offset?: number, rootDir?: string): CommandResult;
|
|
561
|
+
cmdLs(args: string[], ctx: CommandContext, offset?: number, rootDir?: string, q?: string): CommandResult;
|
|
519
562
|
/**
|
|
520
563
|
* `/download <path>`(别名 `/d`):把本地文件直接发送到飞书。
|
|
521
564
|
*
|
|
@@ -530,7 +573,14 @@ export declare class CommandRouter {
|
|
|
530
573
|
* 此前直接回 `ls: xxx: Not a directory` 把文件路径当成错误处理。
|
|
531
574
|
*/
|
|
532
575
|
private buildFileCard;
|
|
533
|
-
|
|
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;
|
|
534
584
|
/**
|
|
535
585
|
* L3: degraded card when a session's export is non-empty but unparseable
|
|
536
586
|
* (truncated by opencode's pipe cap on huge sessions, or otherwise corrupt).
|