lark-remote 0.4.0 → 0.4.1

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.
@@ -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) {
@@ -121,6 +121,11 @@ export declare class CommandRouter {
121
121
  * returns its pid. Injected from index.ts; /restart is unavailable without it.
122
122
  */
123
123
  private restartSpawner?;
124
+ /**
125
+ * 冲刷装配器窗口内未提交的入站 turn(/exit、/restart 干净退出前调用,
126
+ * 避免静默期窗口内的内容丢失)。index.ts 注入。
127
+ */
128
+ private flushInbound?;
124
129
  private idleTimeoutMs;
125
130
  /** Dev mode flag: --dev means the bridge was started from source (bun src/index.ts). */
126
131
  private devMode;
@@ -197,12 +202,24 @@ export declare class CommandRouter {
197
202
  * The reader is fetched from the registry each time to reflect config changes.
198
203
  */
199
204
  private get sessionReader();
205
+ /**
206
+ * 注入入站装配器的冲刷回调(/exit、/restart 干净退出前调用,
207
+ * 避免静默期窗口内未提交的 turn 丢失)。index.ts 在构造后接线。
208
+ */
209
+ setInboundFlusher(flush: () => Promise<void>): void;
200
210
  /**
201
211
  * Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
212
+ *
213
+ * 命令识别前置条件(2026-09-15 P0 修复):
214
+ * 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 `![image](img_v3_…)`,
215
+ * 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
216
+ * 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
217
+ * 三条同时成立时生效,其余一律按文本转发。
202
218
  */
203
219
  handle(message: string, ctx: CommandContext, opts?: {
204
220
  cwdOverride?: string;
205
221
  binding?: AgentBinding;
222
+ allowCommandPrefix?: boolean;
206
223
  }): Promise<CommandResult | null>;
207
224
  /**
208
225
  * 一次别名展开:bridge 收到用户消息后、命令分发前调用。
@@ -9,6 +9,7 @@ import { OrderStore } from '../order/index.js';
9
9
  import { resolveAlias } from '../order/alias-resolve.js';
10
10
  import { diffConfig, setNestedValue } from './config-diff.js';
11
11
  import { getLogger } from '../logger/index.js';
12
+ import { stripPlaceholders } from '../inbound/placeholder.js';
12
13
  import { activeRunUsage, clampInt } from './utils.js';
13
14
  import { markdownDiv, buildSessionHistoryCard, paginationBar, parseJumpOffset, PAGE_JUMP_INVALID_HINT, } from './card-helpers.js';
14
15
  import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
@@ -342,6 +343,11 @@ export class CommandRouter {
342
343
  * returns its pid. Injected from index.ts; /restart is unavailable without it.
343
344
  */
344
345
  restartSpawner;
346
+ /**
347
+ * 冲刷装配器窗口内未提交的入站 turn(/exit、/restart 干净退出前调用,
348
+ * 避免静默期窗口内的内容丢失)。index.ts 注入。
349
+ */
350
+ flushInbound;
345
351
  idleTimeoutMs;
346
352
  /** Dev mode flag: --dev means the bridge was started from source (bun src/index.ts). */
347
353
  devMode;
@@ -416,13 +422,33 @@ export class CommandRouter {
416
422
  get sessionReader() {
417
423
  return this.sessionReaderRegistry.get(this.config.defaultAgent);
418
424
  }
425
+ /**
426
+ * 注入入站装配器的冲刷回调(/exit、/restart 干净退出前调用,
427
+ * 避免静默期窗口内未提交的 turn 丢失)。index.ts 在构造后接线。
428
+ */
429
+ setInboundFlusher(flush) {
430
+ this.flushInbound = flush;
431
+ }
419
432
  /**
420
433
  * Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
434
+ *
435
+ * 命令识别前置条件(2026-09-15 P0 修复):
436
+ * 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 `![image](img_v3_…)`,
437
+ * 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
438
+ * 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
439
+ * 三条同时成立时生效,其余一律按文本转发。
421
440
  */
422
441
  async handle(message, ctx, opts) {
423
442
  const trimmed = message.trim();
424
- const startsWithBang = trimmed.startsWith('!');
425
- const startsWithSlash = trimmed.startsWith('/');
443
+ // 双保险:任何调用点都必须先剥离结构占位符,再判命令前缀
444
+ //(index.ts 传 allowCommandPrefix = rawContentType === 'text',但那层不是唯一入口)。
445
+ const guard = stripPlaceholders(trimmed);
446
+ // 默认 true:order.exec 等既有内部调用点传的是用户已确认的指令文本,
447
+ // 且剥离后无占位符 —— 默认放行不改变它们的语义(见 command-guard.test.ts)。
448
+ const allowCommandPrefix = opts?.allowCommandPrefix ?? true;
449
+ const commandEligible = allowCommandPrefix && guard.kinds.length === 0 && guard.clean !== '';
450
+ const startsWithBang = commandEligible && guard.clean.startsWith('!');
451
+ const startsWithSlash = commandEligible && guard.clean.startsWith('/');
426
452
  getLogger().info(`[router] handle message="${message.slice(0, 50)}..." trimmed="${trimmed.slice(0, 50)}..." startsWithSlash=${startsWithSlash} startsWithBang=${startsWithBang}`);
427
453
  // 复制分身流程活跃期:一切消息(含命令)交给 clone 状态机处理,
428
454
  // 不转发 coding agent、不进命令分发(index.ts 已在入队前拦截,这里
@@ -432,20 +458,20 @@ export class CommandRouter {
432
458
  return null;
433
459
  }
434
460
  if (startsWithSlash) {
435
- const result = await this.executeCommand(trimmed, ctx);
461
+ const result = await this.executeCommand(guard.clean, ctx);
436
462
  if (result) {
437
463
  await this.bridge.sendResult(result, ctx);
438
464
  }
439
465
  if (this.pendingExit) {
440
- // 干净退出前冲刷待合批的媒体保存提示,避免 500ms 窗口内的提示丢失。
441
- await this.bridge.flushAllMediaNotifications();
466
+ // 干净退出前冲刷装配器窗口内未提交的入站 turn,避免静默期窗口内的内容丢失。
467
+ await this.flushInbound?.();
442
468
  this.exitHandler();
443
469
  }
444
470
  return result;
445
471
  }
446
472
  // Handle bang commands (!command)
447
- if (trimmed.startsWith('!')) {
448
- const cmd = trimmed.slice(1).trim();
473
+ if (startsWithBang) {
474
+ const cmd = guard.clean.slice(1).trim();
449
475
  if (!cmd) {
450
476
  await this.bridge.sendResult({ text: '请输入要执行的命令,例如 !ls' }, ctx);
451
477
  return null;
@@ -456,7 +482,9 @@ export class CommandRouter {
456
482
  // Forward to Claude (P1-14: pass the enqueue-time workspace through so the
457
483
  // run uses the same cwd as the serial queue lane, even if /cd ran while
458
484
  // the message was queued)
459
- await this.bridge.forwardToClaude(trimmed, ctx, opts);
485
+ // 剥离占位符后再转发:agent 只该看到用户文本(附图已转成 attachments 块,
486
+ // 那是装配器生成的协议块,stripPlaceholders 会原样保留)。
487
+ await this.bridge.forwardToClaude(guard.clean, ctx, opts);
460
488
  return null;
461
489
  }
462
490
  /**
@@ -607,7 +635,7 @@ export class CommandRouter {
607
635
  // 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
608
636
  // 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
609
637
  if (this.pendingExit) {
610
- await this.bridge.flushAllMediaNotifications();
638
+ await this.flushInbound?.();
611
639
  this.exitHandler();
612
640
  }
613
641
  return;
@@ -1,8 +1,12 @@
1
1
  import { ConnectionLostError } from './jsonrpc/client.js';
2
2
  import { getLogger } from '../../logger/index.js';
3
3
  import { syntheticInitEvent } from './runner-utils.js';
4
- /** How long to wait for turn output notifications before failing. */
5
- const TURN_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
4
+ import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
5
+ /** How long to wait for turn output notifications before failing.
6
+ * 30 分钟(单源于 config 的 DEFAULT_TURN_IDLE_TIMEOUT_MINUTES):ACP/app-server
7
+ * 类 agent 常因长工具调用(pip/npm install 等)或等待 SubAgent 长时间静默,
8
+ * 10 分钟会误杀正常进行中的 run。未接配置的 runner(如 opencode)直接吃此默认。 */
9
+ const TURN_IDLE_TIMEOUT_MS = DEFAULT_TURN_IDLE_TIMEOUT_MINUTES * 60 * 1000;
6
10
  export class ConnectionBasedRunner {
7
11
  kind;
8
12
  sessionReader;
@@ -36,7 +36,7 @@ export interface KimiAcpRunnerOptions {
36
36
  requestTimeoutMs?: number;
37
37
  /** Idle TTL for connection manager. */
38
38
  idleTtlMs?: number;
39
- /** How long to wait for turn output before failing. Defaults to 10 min. */
39
+ /** How long to wait for turn output before failing. Defaults to 30 min. */
40
40
  turnIdleTimeoutMs?: number;
41
41
  /** §5.2: compaction 等待的沉默超时——距上次观察到任何新 compaction 记录
42
42
  * (begin/terminal)超过该时长仍未终态才放弃(outcome unknown,卡片 error,
@@ -27,7 +27,7 @@ import { mapAnswersByIndex } from '../../question-common.js';
27
27
  import { getLogger } from '../../../logger/index.js';
28
28
  import { BaseAcpRunner } from '../../common/acp/base-acp-runner.js';
29
29
  import { ProcessStopper } from '../../common/process-stopper.js';
30
- /** §5.2: compaction 等待的默认沉默超时(对齐 turnIdleTimeoutMs 默认值)。 */
30
+ /** §5.2: compaction 等待的默认沉默超时(10 分钟,独立于 turn idle 默认 30 分钟)。 */
31
31
  const COMPACT_IDLE_TIMEOUT_MS = 10 * 60_000;
32
32
  /** Poll interval for the wire.jsonl compaction record. */
33
33
  const COMPACT_POLL_INTERVAL_MS = 1_000;
@@ -40,7 +40,7 @@ export interface OpencodeAcpRunnerOptions {
40
40
  requestTimeoutMs?: number;
41
41
  /** Idle TTL for connection manager. */
42
42
  idleTtlMs?: number;
43
- /** How long to wait for turn output before failing. Defaults to 10 min. */
43
+ /** How long to wait for turn output before failing. Defaults to 30 min. */
44
44
  turnIdleTimeoutMs?: number;
45
45
  model?: string;
46
46
  /** Configured session mode: 'build' (default) or 'plan' (opencode agent names). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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",