lark-remote 0.1.6 → 0.1.7

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 (188) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +57 -34
  5. package/dist/bridge/approval-coordinator.js +191 -40
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +78 -7
  9. package/dist/bridge/index.js +202 -79
  10. package/dist/bridge/queue-manager.d.ts +2 -1
  11. package/dist/bridge/queue-manager.js +1 -0
  12. package/dist/card/approval-render.js +152 -36
  13. package/dist/card/bash-card-session.d.ts +2 -24
  14. package/dist/card/bash-card-session.js +6 -73
  15. package/dist/card/bash-renderer.js +2 -3
  16. package/dist/card/card-budget.js +29 -75
  17. package/dist/card/card-session.d.ts +48 -0
  18. package/dist/card/card-session.js +86 -0
  19. package/dist/card/card-shared.d.ts +1 -12
  20. package/dist/card/card-shared.js +1 -26
  21. package/dist/card/run-card-session.d.ts +2 -55
  22. package/dist/card/run-card-session.js +6 -104
  23. package/dist/card/run-renderer.js +99 -232
  24. package/dist/card/run-state.d.ts +13 -0
  25. package/dist/card/run-state.js +25 -16
  26. package/dist/card/text-truncate.d.ts +13 -12
  27. package/dist/card/text-truncate.js +13 -63
  28. package/dist/common/fs.d.ts +8 -0
  29. package/dist/common/fs.js +17 -0
  30. package/dist/common/guards.d.ts +0 -20
  31. package/dist/common/guards.js +0 -43
  32. package/dist/common/sleep.d.ts +2 -0
  33. package/dist/common/sleep.js +5 -0
  34. package/dist/common/truncate.d.ts +39 -0
  35. package/dist/common/truncate.js +92 -0
  36. package/dist/config/codex-config.d.ts +1 -1
  37. package/dist/config/codex-config.js +1 -1
  38. package/dist/config/index.d.ts +88 -23
  39. package/dist/config/index.js +105 -34
  40. package/dist/connector/index.d.ts +69 -2
  41. package/dist/connector/index.js +107 -1
  42. package/dist/index.js +123 -105
  43. package/dist/instance-lock.js +4 -12
  44. package/dist/order/alias-resolve.d.ts +13 -0
  45. package/dist/order/alias-resolve.js +22 -0
  46. package/dist/order/alias-store.d.ts +38 -0
  47. package/dist/order/alias-store.js +106 -0
  48. package/dist/persistence/atomic-write.d.ts +8 -2
  49. package/dist/persistence/atomic-write.js +23 -7
  50. package/dist/restart.js +2 -1
  51. package/dist/router/card-helpers.d.ts +63 -0
  52. package/dist/router/card-helpers.js +146 -0
  53. package/dist/router/config/claude.js +25 -1
  54. package/dist/router/config/codex.js +29 -50
  55. package/dist/router/config/common/model-patch.d.ts +22 -0
  56. package/dist/router/config/common/model-patch.js +26 -0
  57. package/dist/router/config/common/render.js +5 -4
  58. package/dist/router/config/index.d.ts +12 -0
  59. package/dist/router/config/index.js +25 -1
  60. package/dist/router/config/kimi.d.ts +4 -1
  61. package/dist/router/config/kimi.js +27 -0
  62. package/dist/router/config/opencode.js +18 -5
  63. package/dist/router/config/pi.js +4 -5
  64. package/dist/router/config/types.d.ts +11 -2
  65. package/dist/router/index.d.ts +56 -21
  66. package/dist/router/index.js +373 -548
  67. package/dist/router/utils.d.ts +2 -0
  68. package/dist/router/utils.js +4 -0
  69. package/dist/runner/agent-choices-common.d.ts +16 -0
  70. package/dist/runner/agent-choices-common.js +31 -0
  71. package/dist/runner/claude/runner.d.ts +47 -10
  72. package/dist/runner/claude/runner.js +117 -46
  73. package/dist/runner/claude/session.d.ts +166 -0
  74. package/dist/runner/claude/session.js +749 -0
  75. package/dist/runner/codex/app-server/protocol-types.d.ts +31 -39
  76. package/dist/runner/codex/app-server/protocol-types.js +1 -12
  77. package/dist/runner/codex/app-server/runner.d.ts +42 -59
  78. package/dist/runner/codex/app-server/runner.js +160 -337
  79. package/dist/runner/codex/app-server/translator.d.ts +1 -21
  80. package/dist/runner/codex/app-server/translator.js +0 -18
  81. package/dist/runner/codex/index.d.ts +0 -2
  82. package/dist/runner/codex/index.js +0 -2
  83. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  84. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  85. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  86. package/dist/runner/common/acp/protocol-types.js +75 -0
  87. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  88. package/dist/runner/common/connection-based-runner.js +261 -0
  89. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  90. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  91. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  92. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  93. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  95. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  96. package/dist/runner/common/jsonrpc/types.js +9 -0
  97. package/dist/runner/common/process-stopper.d.ts +2 -1
  98. package/dist/runner/common/process-stopper.js +1 -0
  99. package/dist/runner/common/runner-utils.d.ts +6 -13
  100. package/dist/runner/common/runner-utils.js +6 -47
  101. package/dist/runner/common/spawning-runner.d.ts +24 -70
  102. package/dist/runner/common/spawning-runner.js +98 -152
  103. package/dist/runner/index.d.ts +4 -4
  104. package/dist/runner/index.js +4 -4
  105. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  106. package/dist/runner/kimi/acp/runner.js +448 -0
  107. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  108. package/dist/runner/kimi/acp/translator.js +336 -0
  109. package/dist/runner/kimi/index.d.ts +1 -1
  110. package/dist/runner/kimi/index.js +1 -1
  111. package/dist/runner/opencode/acp/index.d.ts +1 -0
  112. package/dist/runner/opencode/acp/index.js +2 -0
  113. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  114. package/dist/runner/opencode/acp/runner.js +429 -0
  115. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  116. package/dist/runner/opencode/acp/translator.js +345 -0
  117. package/dist/runner/opencode/index.d.ts +1 -1
  118. package/dist/runner/opencode/index.js +1 -1
  119. package/dist/runner/pi/index.d.ts +1 -1
  120. package/dist/runner/pi/index.js +1 -1
  121. package/dist/runner/pi/rpc/client.d.ts +52 -0
  122. package/dist/runner/pi/rpc/client.js +114 -0
  123. package/dist/runner/pi/rpc/index.d.ts +4 -0
  124. package/dist/runner/pi/rpc/index.js +4 -0
  125. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  126. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  127. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  128. package/dist/runner/pi/rpc/runner.js +199 -0
  129. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  130. package/dist/runner/pi/rpc/translator.js +258 -0
  131. package/dist/runner/registry.d.ts +7 -8
  132. package/dist/runner/registry.js +3 -4
  133. package/dist/runner/resolve-agent-choices.js +18 -59
  134. package/dist/runner/sync-agent-choices.js +16 -67
  135. package/dist/runner/types.d.ts +43 -13
  136. package/dist/session/claude/session-index.js +2 -3
  137. package/dist/session/claude/session-reader.js +3 -6
  138. package/dist/session/claude/sessions.js +6 -13
  139. package/dist/session/codex/rollout-reader.d.ts +2 -2
  140. package/dist/session/codex/rollout-reader.js +14 -17
  141. package/dist/session/codex/sessions.d.ts +1 -1
  142. package/dist/session/codex/sessions.js +2 -2
  143. package/dist/session/common/content-blocks.d.ts +7 -0
  144. package/dist/session/common/content-blocks.js +13 -9
  145. package/dist/session/common/jsonl.d.ts +8 -25
  146. package/dist/session/common/jsonl.js +126 -261
  147. package/dist/session/common/pagination.d.ts +28 -0
  148. package/dist/session/common/pagination.js +33 -0
  149. package/dist/session/kimi/sessions.d.ts +34 -1
  150. package/dist/session/kimi/sessions.js +208 -98
  151. package/dist/session/opencode/sessions.d.ts +17 -3
  152. package/dist/session/opencode/sessions.js +29 -29
  153. package/dist/session/pi/sessions.d.ts +5 -1
  154. package/dist/session/pi/sessions.js +12 -16
  155. package/dist/session/session-store.d.ts +11 -1
  156. package/dist/session/session-store.js +75 -178
  157. package/dist/update/index.d.ts +2 -2
  158. package/dist/update/index.js +2 -2
  159. package/dist/update/install.d.ts +20 -2
  160. package/dist/update/install.js +51 -7
  161. package/package.json +1 -1
  162. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  163. package/dist/runner/codex/argv.d.ts +0 -33
  164. package/dist/runner/codex/argv.js +0 -40
  165. package/dist/runner/codex/factory.d.ts +0 -29
  166. package/dist/runner/codex/factory.js +0 -39
  167. package/dist/runner/codex/jsonl.d.ts +0 -37
  168. package/dist/runner/codex/jsonl.js +0 -221
  169. package/dist/runner/codex/runner.d.ts +0 -66
  170. package/dist/runner/codex/runner.js +0 -129
  171. package/dist/runner/common/exec-translator.d.ts +0 -73
  172. package/dist/runner/common/exec-translator.js +0 -105
  173. package/dist/runner/kimi/jsonl.d.ts +0 -28
  174. package/dist/runner/kimi/jsonl.js +0 -143
  175. package/dist/runner/kimi/runner.d.ts +0 -49
  176. package/dist/runner/kimi/runner.js +0 -105
  177. package/dist/runner/opencode/argv.d.ts +0 -32
  178. package/dist/runner/opencode/argv.js +0 -35
  179. package/dist/runner/opencode/jsonl.d.ts +0 -68
  180. package/dist/runner/opencode/jsonl.js +0 -295
  181. package/dist/runner/opencode/runner.d.ts +0 -61
  182. package/dist/runner/opencode/runner.js +0 -93
  183. package/dist/runner/pi/jsonl.d.ts +0 -128
  184. package/dist/runner/pi/jsonl.js +0 -249
  185. package/dist/runner/pi/runner.d.ts +0 -36
  186. package/dist/runner/pi/runner.js +0 -76
  187. package/dist/session/common/catch-up.d.ts +0 -12
  188. package/dist/session/common/catch-up.js +0 -31
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { loadConfig, getAgentConfig, DEFAULT_STOP_GRACE_MS, } from './config/index.js';
1
+ import { loadConfig, getAgentConfig } from './config/index.js';
2
2
  import { resolveAgentChoices, } from './runner/index.js';
3
3
  import { ensureConfig } from './config/wizard.js';
4
4
  import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion, } from './config/dir.js';
5
5
  import { FeishuConnector } from './connector/index.js';
6
- import { ClaudeRunner, createCodexRunner, OpencodeExecRunner, PiRunner, KimiRunner, } from './runner/index.js';
6
+ import { ClaudeRunner, CodexAppServerRunner, OpencodeAcpRunner, PiRpcRunner, KimiAcpRunner, } from './runner/index.js';
7
7
  import { AgentRegistry } from './runner/registry.js';
8
8
  import { probeAllAgents } from './runner/probe.js';
9
9
  import { SessionReaderRegistry, SessionStore } from './session/index.js';
10
10
  import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, } from './session/index.js';
11
- import { CommandRouter, isImmediateAction, formatUsageStats, } from './router/index.js';
11
+ import { CommandRouter, isImmediateAction } from './router/index.js';
12
12
  import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
13
13
  import { Bridge } from './bridge/index.js';
14
14
  import { initLogger, getLogger } from './logger/index.js';
@@ -19,11 +19,11 @@ import { spawnReplacementBridge, waitForPreviousInstance } from './restart.js';
19
19
  import { checkLatestVersion, isNewer, runInstallLatest, formatUpdateHint } from './update/index.js';
20
20
  import { classifyRejection } from './error-classification.js';
21
21
  import { WorkspaceStore } from './workspace/index.js';
22
- import { markdownDiv } from './card/collapsible.js';
23
- import { sessionEventPanel } from './router/card-helpers.js';
22
+ import { buildSessionHistoryCard } from './router/card-helpers.js';
24
23
  import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
25
24
  import path from 'node:path';
26
25
  import fs from 'node:fs';
26
+ import { silentlyUnlink } from './common/fs.js';
27
27
  /** Send auto-resume card for a restored session */
28
28
  async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false) {
29
29
  if (!contact.userId)
@@ -31,68 +31,32 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state, compa
31
31
  const recipient = contact.chatId ?? contact.userId;
32
32
  if (!recipient)
33
33
  return;
34
- // Build header with displayTitle (aiTitle or last user message) and recap
35
- let header = `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`;
36
- const sections = [];
37
- if (state.displayTitle) {
38
- const label = state.aiTitle ? 'AI 标题' : '最近输入';
39
- sections.push(`🏷️ **${label}**\n${state.displayTitle}`);
40
- }
41
- if (state.recap) {
42
- const recapPreview = state.recap.length > 200 ? state.recap.slice(0, 197) + '...' : state.recap;
43
- sections.push(`📝 **Recap**\n${recapPreview}`);
44
- }
45
- if (sections.length > 0) {
46
- header += '\n\n' + sections.join('\n\n──\n\n');
47
- }
48
- const elements = [markdownDiv(header), { tag: 'hr' }];
49
34
  // Limit history events to prevent card size exceeded error (11310)
50
35
  const MAX_HISTORY_EVENTS = 5;
51
36
  const eventsToShow = state.events.slice(-MAX_HISTORY_EVENTS);
52
37
  const hiddenCount = state.events.length - MAX_HISTORY_EVENTS;
53
- // Add indicator if events were hidden
54
- if (hiddenCount > 0) {
55
- elements.push(markdownDiv(`📜 还有 ${hiddenCount} 个更早的事件未显示`));
56
- }
57
- // Fold session history into collapsible panels: the last 2 events stay expanded
38
+ // Action buttons: Compact (runCompact-capable runner) + new session.
58
39
  const agentKind = state.agentKind ?? 'claude';
59
- eventsToShow.forEach((ev, i) => {
60
- elements.push(sessionEventPanel(ev, i, eventsToShow.length, 2, agentKind));
61
- });
62
- // Add usage/stats at the end
63
- if (state.usage) {
64
- const usageStr = formatUsageStats(state.usage);
65
- elements.push(markdownDiv(usageStr));
66
- }
67
- // Add action buttons - use column_set+column (div+elements not supported in 2.0)
68
40
  const actionButtons = [];
69
- // Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且未在跑。
70
- // 启动自动恢复时不会存在该会话的 activeRun,直接渲染。
71
41
  if (compactSupported) {
72
42
  actionButtons.push(resumeCompactButton(session.sessionId, state.agentKind ?? 'codex'));
73
43
  }
74
44
  actionButtons.push(newSessionButton());
75
- elements.push({
76
- tag: 'column_set',
77
- columns: actionButtons.map((btn) => ({
78
- tag: 'column',
79
- width: 'auto',
80
- elements: [btn],
81
- })),
45
+ const card = buildSessionHistoryCard({
46
+ sessionId: session.sessionId,
47
+ cwd,
48
+ displayTitle: state.displayTitle,
49
+ aiTitle: state.aiTitle,
50
+ recap: state.recap,
51
+ events: eventsToShow,
52
+ usage: state.usage,
53
+ }, {
54
+ agentKind,
55
+ headerText: `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`,
56
+ title: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
57
+ hiddenCount,
58
+ actions: actionButtons,
82
59
  });
83
- const card = {
84
- schema: '2.0',
85
- config: {
86
- wide_screen_mode: true,
87
- },
88
- header: {
89
- title: {
90
- tag: 'plain_text',
91
- content: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
92
- },
93
- },
94
- body: { elements },
95
- };
96
60
  await connector.sendWithRetry(recipient, { card });
97
61
  }
98
62
  /** Initialize CLI args, config, and logger. Returns { config, configDir, logger, instanceLock }. */
@@ -165,37 +129,36 @@ function initializeRunner(config, configDir, cliArgs) {
165
129
  settings: cliArgs.settings,
166
130
  pidDir: configDir,
167
131
  workspace: ws,
132
+ permissionMode: claudeConfig.permissionMode,
133
+ idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
168
134
  });
169
135
  });
170
- // Register Codex runner. Factory reads latest config from container.current
171
- // so runtime config changes (serviceMode, model, sandbox, approvalPolicy,
172
- // reasoningEffort, etc.) take effect after bridge.setConfig() + clearRunners().
173
- // serviceMode=app-server → CodexAppServerRunner (persistent connection with
174
- // approval + sandbox); otherwise CodexExecRunner (codex exec --json).
136
+ // Register Codex runner (app-server only). The registry factory reads latest
137
+ // config from container.current so runtime config changes (model, sandbox,
138
+ // approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
139
+ // + clearRunners().
175
140
  const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
176
- agentRegistry.register('codex', (ws) => {
141
+ agentRegistry.register('codex', (_ws) => {
177
142
  const container = agentRegistry.getConfigContainer();
178
143
  const latestConfig = container?.current ?? config;
179
144
  const codexConfig = getAgentConfig(latestConfig, 'codex');
180
- return createCodexRunner({
145
+ return new CodexAppServerRunner({
146
+ kind: 'codex',
181
147
  model: codexConfig?.model,
182
148
  modelProvider: codexConfig?.modelProvider,
183
149
  reasoningEffort: codexConfig?.reasoningEffort,
184
- stopGraceMs: codexConfig?.stopGraceMs ?? latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
185
- pidDir: configDir,
186
- workspace: ws,
187
150
  sessionReader: codexSessionReader,
188
- serviceMode: codexConfig?.serviceMode,
189
151
  sandbox: codexConfig?.sandbox,
190
152
  approvalPolicy: codexConfig?.approvalPolicy,
191
- appServerBinary: codexConfig?.appServer?.binary,
192
- appServerRequestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
193
- appServerIdleTtlMs: codexConfig?.appServer?.idleTtlMs,
194
- appServerTurnIdleTimeoutMinutes: codexConfig?.appServer?.turnIdleTimeoutMinutes,
153
+ binary: codexConfig?.appServer?.binary,
154
+ requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
155
+ idleTtlMs: codexConfig?.appServer?.idleTtlMs,
156
+ turnTimeoutMs: codexConfig?.appServer?.turnIdleTimeoutMinutes != null
157
+ ? codexConfig.appServer.turnIdleTimeoutMinutes * 60_000
158
+ : undefined,
195
159
  });
196
160
  });
197
- // Register OpencodeExecRunner (run mode)
198
- // Using `opencode run --format json --auto` instead of HTTP+SSE server mode
161
+ // Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
199
162
  // Register session reader (same instance as used by runner)
200
163
  const sessionReaderRegistry = new SessionReaderRegistry();
201
164
  sessionReaderRegistry.register('claude', new ClaudeSessionReader());
@@ -203,7 +166,7 @@ function initializeRunner(config, configDir, cliArgs) {
203
166
  // Register OpencodeSessionReader (CLI version)
204
167
  // Using `opencode session list` and `opencode export` instead of HTTP API
205
168
  const opencodeSessionReader = new OpencodeSessionReader();
206
- agentRegistry.register('opencode', (ws) => {
169
+ agentRegistry.register('opencode', (_ws) => {
207
170
  // Get latest config from container (set below for pi)
208
171
  const container = agentRegistry.getConfigContainer();
209
172
  const latestConfig = container?.current ?? config;
@@ -220,17 +183,15 @@ function initializeRunner(config, configDir, cliArgs) {
220
183
  model = `${provider}/${ocConfig.modelID}`;
221
184
  }
222
185
  }
223
- return new OpencodeExecRunner({
186
+ return new OpencodeAcpRunner({
187
+ kind: 'opencode',
224
188
  model,
225
- // P1-15: read stopGraceMs from latestConfig (not startup closure)
226
- stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
227
- pidDir: configDir,
228
- workspace: ws,
229
189
  sessionReader: opencodeSessionReader,
190
+ mode: ocConfig?.mode ?? 'build',
230
191
  });
231
192
  });
232
193
  sessionReaderRegistry.register('opencode', opencodeSessionReader);
233
- // Register PiRunner and PiSessionReader
194
+ // Register PiRpcRunner and PiSessionReader
234
195
  // 2026-07-12: 修复 config.save 后 pi provider 不生效的问题
235
196
  // 关键:factory 闭包必须能访问到最新的 config,而不是启动时的快照
236
197
  // 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值
@@ -242,7 +203,7 @@ function initializeRunner(config, configDir, cliArgs) {
242
203
  const container = agentRegistry.getConfigContainer();
243
204
  const latestConfig = container?.current ?? config;
244
205
  const piConf = getAgentConfig(latestConfig, 'pi');
245
- return new PiRunner({
206
+ return new PiRpcRunner({
246
207
  provider: piConf?.provider ?? 'Volcano',
247
208
  model: piConf?.model ?? 'glm-5.2',
248
209
  thinking: piConf?.thinking ?? 'medium',
@@ -250,27 +211,30 @@ function initializeRunner(config, configDir, cliArgs) {
250
211
  .split(',')
251
212
  .map((t) => t.trim())
252
213
  .filter(Boolean),
253
- pidDir: configDir,
254
214
  workspace: ws,
255
215
  sessionReader: piSessionReader,
256
216
  });
257
217
  });
258
218
  sessionReaderRegistry.register('pi', piSessionReader);
259
- // Register KimiRunner and KimiSessionReader
219
+ // Register KimiAcpRunner (pure ACP mode) and KimiSessionReader
260
220
  const kimiSessionReader = new KimiSessionReader();
261
- agentRegistry.register('kimi', (ws) => {
221
+ agentRegistry.register('kimi', (_ws) => {
262
222
  // 每次 factory 调用时从 registry 获取最新 config
263
223
  const container = agentRegistry.getConfigContainer();
264
224
  const latestConfig = container?.current ?? config;
265
225
  const kimiConf = getAgentConfig(latestConfig, 'kimi');
266
- return new KimiRunner({
267
- model: kimiConf?.model ?? 'kimi-code/k3',
268
- thinkingEffort: kimiConf?.thinkingEffort ?? 'max',
269
- // P1-15: read stopGraceMs from latestConfig (not startup closure)
270
- stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
271
- pidDir: configDir,
272
- workspace: ws,
226
+ const acpConf = kimiConf?.acp;
227
+ return new KimiAcpRunner({
228
+ kind: 'kimi',
273
229
  sessionReader: kimiSessionReader,
230
+ binary: acpConf?.binary ?? 'kimi',
231
+ requestTimeoutMs: acpConf?.requestTimeoutMs,
232
+ idleTtlMs: acpConf?.idleTtlMs,
233
+ turnIdleTimeoutMs: acpConf?.turnIdleTimeoutMinutes != null
234
+ ? acpConf.turnIdleTimeoutMinutes * 60_000
235
+ : undefined,
236
+ model: kimiConf?.model ?? 'kimi-code/k3',
237
+ permissionMode: kimiConf?.permissionMode ?? 'manual',
274
238
  });
275
239
  });
276
240
  sessionReaderRegistry.register('kimi', kimiSessionReader);
@@ -301,6 +265,48 @@ function initializeRunner(config, configDir, cliArgs) {
301
265
  }
302
266
  /** Setup message and card action handlers. */
303
267
  function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger, config) {
268
+ // 入站媒体(图片/文件):先认证后下载(P1 review 修复)。
269
+ // connector 只上报"媒体到达",这里先过 owner + enabled 闸门,通过后才
270
+ // 下载——未认证/关闭配置时不会发生任何网络下载或内存/磁盘占用。
271
+ connector.setInboundMediaDetectedHandler((msg) => {
272
+ if (!binder.isOwner(msg.userId)) {
273
+ logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
274
+ return;
275
+ }
276
+ // 读 router.config(活引用):/config 保存后 setConfigValues 返回新对象,
277
+ // 启动时捕获的 config 参数会过期(P2 review 修复)。
278
+ if (!router.config.inboundMedia.enabled) {
279
+ // 不静默(P3 review):关闭时给 owner 明确反馈,避免发图后无任何反应。
280
+ // 不下载、不落盘,只回一条说明。
281
+ void bridge
282
+ .sendResult({
283
+ text: '⚠️ 入站媒体保存已关闭(inboundMedia.enabled: false),未保存文件。' +
284
+ '如需自动保存图片/文件,请开启后重试',
285
+ }, {
286
+ userId: msg.userId,
287
+ chatId: msg.chatId,
288
+ messageId: msg.messageId,
289
+ })
290
+ .catch((err) => logger.error('[media] disabled feedback send failed:', err));
291
+ return;
292
+ }
293
+ void (async () => {
294
+ const payload = await connector.downloadInboundMedia(msg, {
295
+ maxFileSizeMb: router.config.inboundMedia.maxFileSizeMb,
296
+ });
297
+ try {
298
+ await bridge.onInboundMedia(payload);
299
+ }
300
+ catch (err) {
301
+ // handle() 的正常路径会清理临时文件;这里兜底 handle 进入 mkdir
302
+ // try 之前意外抛错的情况,避免 os.tmpdir 残留。
303
+ for (const item of payload.media) {
304
+ silentlyUnlink(item.tempPath);
305
+ }
306
+ throw err;
307
+ }
308
+ })().catch((err) => logger.error('[media] inbound media download/save failed:', err));
309
+ });
304
310
  connector.setMessageHandler((msg) => {
305
311
  // 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时要求 PIN 认领
306
312
  const decision = binder.classify(msg.userId, msg.content, msg.chatId);
@@ -358,9 +364,16 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
358
364
  }
359
365
  // decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
360
366
  logger.info(`message from ${msg.userId}: ${msg.content.slice(0, 100)}`);
367
+ // 文本到达先冲刷待合批的媒体保存提示(先图后文字时文字到达
368
+ // 先冲刷批次,避免合批提示被后续消息淹没/丢图)。
369
+ bridge.flushMediaNotifications(msg.userId, msg.chatId);
361
370
  // Add Typing reaction to indicate "still alive"
362
371
  void connector.addReaction(msg.messageId, 'Typing');
363
- const trimmedLower = msg.content.trim().toLowerCase();
372
+ // 别名展开:命令分发前对消息做一次 $name 展开(不递归)。
373
+ // `!` / `/` 开头的消息不会进入展开路径($PATH、$HOME 等 shell 变量不受影响),
374
+ // 未知 $xxx 原样透传;展开结果若以 `/` 开头会自然落入命令路径。
375
+ const content = router.expandAliasMessage(msg.content);
376
+ const trimmedLower = content.trim().toLowerCase();
364
377
  if (trimmedLower === '/stop' || trimmedLower === '/t') {
365
378
  void (async () => {
366
379
  const stopped = await bridge.interruptCurrentRun({
@@ -377,9 +390,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
377
390
  })().catch((err) => logger.error('[control] /stop failed:', err));
378
391
  return;
379
392
  }
380
- if (msg.content.trim().startsWith('/')) {
393
+ if (content.trim().startsWith('/')) {
381
394
  void router
382
- .handle(msg.content, {
395
+ .handle(content, {
383
396
  userId: msg.userId,
384
397
  chatId: msg.chatId,
385
398
  messageId: msg.messageId,
@@ -392,9 +405,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
392
405
  // in the same workspace (design.md §9.6: the queue exists to prevent
393
406
  // concurrent claude runs, not to serialize bash). router.handle dispatches
394
407
  // `!` to bridge.executeBash, which tracks the run independently.
395
- if (msg.content.trim().startsWith('!')) {
408
+ if (content.trim().startsWith('!')) {
396
409
  void router
397
- .handle(msg.content, {
410
+ .handle(content, {
398
411
  userId: msg.userId,
399
412
  chatId: msg.chatId,
400
413
  messageId: msg.messageId,
@@ -418,7 +431,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
418
431
  // 捕获点 Bridge.currentBinding。
419
432
  const binding = bridge.currentBinding(msg.userId);
420
433
  bridge.enqueue(workspace, async () => {
421
- await router.handle(msg.content, {
434
+ await router.handle(content, {
422
435
  userId: msg.userId,
423
436
  chatId: msg.chatId,
424
437
  messageId: msg.messageId,
@@ -433,7 +446,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
433
446
  userId: msg.userId,
434
447
  chatId: msg.chatId,
435
448
  messageId: msg.messageId,
436
- messagePreview: msg.content.slice(0, 3000),
449
+ messagePreview: content.slice(0, 3000),
437
450
  // D3/Step4: binding 随 taskMeta 存进 QueuedTask,供替换闭包
438
451
  // (queue.edit/queue.immediate)复用,不重新快照。
439
452
  binding,
@@ -492,15 +505,20 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
492
505
  inputValue: action.raw?.action?.input_value ??
493
506
  action.action.input_value,
494
507
  };
495
- // queue.input / config.save / approval.respond / approval.toggle 返回
496
- // CardActionResponse toast 给点击用户即时反馈。必须直接返回值 --
497
- // enqueueImmediate / enqueueConfigAction 是 fire-and-forget,会吞掉返回值。
508
+ // queue.input / config.save / approval.respond / approval.toggle /
509
+ // approval.answer 系列返回 CardActionResponse toast 给点击用户即时反馈。
510
+ // 必须直接返回值 -- enqueueImmediate / enqueueConfigAction 是
511
+ // fire-and-forget,会吞掉返回值(2026-08-17 review:answer 家族曾漏在
512
+ // 直返列表外,过期/重复 nonce/非法选项等错误 toast 被静默吞掉)。
498
513
  // 审批响应尤其不能落串行队列:run 任务占用队列头直到 turn 结束,审批响应
499
514
  // 排在后面会形成死锁(run 不结束不执行、run 结束 coordinator 已删响应空转)。
500
515
  if (actionValue.cmd === 'queue.input' ||
501
516
  actionValue.cmd === 'config.save' ||
502
517
  actionValue.cmd === 'approval.respond' ||
503
- actionValue.cmd === 'approval.toggle') {
518
+ actionValue.cmd === 'approval.toggle' ||
519
+ actionValue.cmd === 'approval.answer' ||
520
+ actionValue.cmd === 'approval.answerSubmit' ||
521
+ actionValue.cmd === 'approval.answerCustom') {
504
522
  return router.handleCardAction(fullValue, { userId, chatId, messageId });
505
523
  }
506
524
  // order.exec → equivalent queued message (Plan A): resolve the order text
@@ -618,6 +636,7 @@ async function main() {
618
636
  configPath: path.join(configDir, 'config.yaml'),
619
637
  workspacePath: path.join(configDir, 'workspace.json'),
620
638
  ordersPath: path.join(configDir, 'orders.json'),
639
+ aliasesPath: path.join(configDir, 'aliases.json'),
621
640
  restartSpawner: () => spawnReplacementBridge(path.join(configDir, 'logs')),
622
641
  sessionReaderRegistry,
623
642
  devMode: cliArgs.dev,
@@ -683,9 +702,8 @@ async function main() {
683
702
  displayTitle: content.displayTitle,
684
703
  agentKind: config.defaultAgent,
685
704
  },
686
- // Compact 仅 codex app-server 模式有实现(runner.runCompact)。
687
- config.defaultAgent === 'codex' &&
688
- getAgentConfig(config, 'codex')?.serviceMode === 'app-server');
705
+ // Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
706
+ bridge.hasRunCompact(restoredCwd, config.defaultAgent));
689
707
  }
690
708
  catch (err) {
691
709
  getLogger().warn('[startup] sendAutoResumeCard failed:', err);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
+ import { silentlyUnlink } from './common/fs.js';
4
5
  export class InstanceAlreadyRunningError extends Error {
5
6
  pid;
6
7
  lockPath;
@@ -68,10 +69,7 @@ export class InstanceLock {
68
69
  throw new InstanceAlreadyRunningError(pid, this.lockPath);
69
70
  }
70
71
  // Stale lock: remove and retry once
71
- try {
72
- fs.unlinkSync(this.lockPath);
73
- }
74
- catch { }
72
+ silentlyUnlink(this.lockPath);
75
73
  try {
76
74
  const fd = fs.openSync(this.lockPath, 'wx');
77
75
  fs.writeSync(fd, this.lockContents(process.pid));
@@ -99,10 +97,7 @@ export class InstanceLock {
99
97
  release() {
100
98
  const { pid } = this.readPidAndComm();
101
99
  if (pid === process.pid) {
102
- try {
103
- fs.unlinkSync(this.lockPath);
104
- }
105
- catch { }
100
+ silentlyUnlink(this.lockPath);
106
101
  }
107
102
  }
108
103
  registerExitHandlers() {
@@ -142,10 +137,7 @@ export class InstanceLock {
142
137
  if (Number.isInteger(pid) && pid > 0 && comm)
143
138
  return { pid, comm };
144
139
  // Malformed or missing comm — treat as corrupt, unlink and return undefined
145
- try {
146
- fs.unlinkSync(this.lockPath);
147
- }
148
- catch { }
140
+ silentlyUnlink(this.lockPath);
149
141
  return { pid: undefined, comm: undefined };
150
142
  }
151
143
  }
@@ -0,0 +1,13 @@
1
+ import type { AliasEntry } from './alias-store.js';
2
+ /**
3
+ * 一次别名展开。
4
+ *
5
+ * - 仅匹配消息开头的 `$name`(`^$name`),`$name` 后接空格或行尾,全词精确匹配;
6
+ * - 名称必须 `[A-Za-z_][A-Za-z0-9_]*`(数字开头的 `$500` 永不匹配);
7
+ * - 未知别名返回 undefined(原样透传给 agent,不报错);
8
+ * - 单次不递归:展开结果不再二次展开,防死循环;
9
+ * - `!` / `/` 开头消息天然不匹配(正则要求 `$` 开头),bash 的 `$PATH` 不受影响。
10
+ *
11
+ * @returns 展开后的完整消息;无匹配时返回 undefined。
12
+ */
13
+ export declare function resolveAlias(message: string, aliases: readonly AliasEntry[]): string | undefined;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 一次别名展开。
3
+ *
4
+ * - 仅匹配消息开头的 `$name`(`^$name`),`$name` 后接空格或行尾,全词精确匹配;
5
+ * - 名称必须 `[A-Za-z_][A-Za-z0-9_]*`(数字开头的 `$500` 永不匹配);
6
+ * - 未知别名返回 undefined(原样透传给 agent,不报错);
7
+ * - 单次不递归:展开结果不再二次展开,防死循环;
8
+ * - `!` / `/` 开头消息天然不匹配(正则要求 `$` 开头),bash 的 `$PATH` 不受影响。
9
+ *
10
+ * @returns 展开后的完整消息;无匹配时返回 undefined。
11
+ */
12
+ export function resolveAlias(message, aliases) {
13
+ const match = /^\$([A-Za-z_][A-Za-z0-9_]*)(?=$|[ \t])/.exec(message);
14
+ if (!match)
15
+ return undefined;
16
+ const entry = aliases.find((a) => a.name === match[1]);
17
+ if (!entry)
18
+ return undefined;
19
+ // lookahead 不消费空格:match[0] 仅含 $name,参数(含前导空格)原样拼接在别名文本后。
20
+ return entry.text + message.slice(match[0].length);
21
+ }
22
+ //# sourceMappingURL=alias-resolve.js.map
@@ -0,0 +1,38 @@
1
+ export declare const MAX_ALIASES = 50;
2
+ export declare const MAX_ALIAS_NAME_LENGTH = 20;
3
+ export declare const MAX_ALIAS_TEXT_LENGTH = 200;
4
+ /**
5
+ * 别名名称规则:`[A-Za-z_][A-Za-z0-9_]*`,≤20 字符,不能数字开头
6
+ * —— `$500`、`$3d` 这类正常文本永远不匹配,不会误展开。
7
+ */
8
+ export declare const ALIAS_NAME_PATTERN: RegExp;
9
+ export interface AliasEntry {
10
+ name: string;
11
+ text: string;
12
+ createdAt: string;
13
+ }
14
+ /**
15
+ * 全局别名存储。
16
+ *
17
+ * 独立于 OrderStore(aliases.json),避免破坏 orders.json 向后兼容。
18
+ * 原子写(tmp + rename)+ 损坏恢复,复用 OrderStore/WorkspaceStore 同一模式。
19
+ */
20
+ export declare class AliasStore {
21
+ private filePath;
22
+ private data;
23
+ constructor(filePath?: string);
24
+ /** Reload data from disk (useful when another process may have written). */
25
+ reload(): void;
26
+ private load;
27
+ private persist;
28
+ has(name: string): boolean;
29
+ get(name: string): AliasEntry | undefined;
30
+ list(): AliasEntry[];
31
+ /**
32
+ * 注册/更新别名(同名覆盖文本,保留 createdAt)。返回写入后的条目。
33
+ * 名称/文本/数量超限时抛错,由调用方提示用户。
34
+ */
35
+ set(name: string, text: string): AliasEntry;
36
+ /** 删除别名;不存在返回 false。 */
37
+ remove(name: string): boolean;
38
+ }
@@ -0,0 +1,106 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { atomicWriteJson } from '../persistence/atomic-write.js';
4
+ import { loadJsonFile } from '../persistence/load-json-file.js';
5
+ export const MAX_ALIASES = 50;
6
+ export const MAX_ALIAS_NAME_LENGTH = 20;
7
+ export const MAX_ALIAS_TEXT_LENGTH = 200;
8
+ /**
9
+ * 别名名称规则:`[A-Za-z_][A-Za-z0-9_]*`,≤20 字符,不能数字开头
10
+ * —— `$500`、`$3d` 这类正常文本永远不匹配,不会误展开。
11
+ */
12
+ export const ALIAS_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
13
+ /**
14
+ * 全局别名存储。
15
+ *
16
+ * 独立于 OrderStore(aliases.json),避免破坏 orders.json 向后兼容。
17
+ * 原子写(tmp + rename)+ 损坏恢复,复用 OrderStore/WorkspaceStore 同一模式。
18
+ */
19
+ export class AliasStore {
20
+ filePath;
21
+ data;
22
+ constructor(filePath) {
23
+ this.filePath = filePath ?? path.join(os.homedir(), '.lark-remote', 'aliases.json');
24
+ this.data = [];
25
+ this.load();
26
+ }
27
+ /** Reload data from disk (useful when another process may have written). */
28
+ reload() {
29
+ this.load();
30
+ }
31
+ load() {
32
+ const parsed = loadJsonFile(this.filePath, undefined);
33
+ if (!parsed || !Array.isArray(parsed)) {
34
+ this.data = [];
35
+ return;
36
+ }
37
+ const valid = [];
38
+ for (const e of parsed) {
39
+ if (e &&
40
+ typeof e.name === 'string' &&
41
+ typeof e.text === 'string' &&
42
+ typeof e.createdAt === 'string' &&
43
+ ALIAS_NAME_PATTERN.test(e.name) &&
44
+ e.name.length <= MAX_ALIAS_NAME_LENGTH) {
45
+ valid.push({
46
+ name: e.name,
47
+ text: e.text.slice(0, MAX_ALIAS_TEXT_LENGTH),
48
+ createdAt: e.createdAt,
49
+ });
50
+ }
51
+ }
52
+ this.data = valid;
53
+ }
54
+ persist() {
55
+ atomicWriteJson(this.filePath, this.data);
56
+ }
57
+ has(name) {
58
+ return this.data.some((a) => a.name === name);
59
+ }
60
+ get(name) {
61
+ return this.data.find((a) => a.name === name);
62
+ }
63
+ list() {
64
+ return [...this.data];
65
+ }
66
+ /**
67
+ * 注册/更新别名(同名覆盖文本,保留 createdAt)。返回写入后的条目。
68
+ * 名称/文本/数量超限时抛错,由调用方提示用户。
69
+ */
70
+ set(name, text) {
71
+ if (!ALIAS_NAME_PATTERN.test(name) || name.length > MAX_ALIAS_NAME_LENGTH) {
72
+ throw new Error(`别名名称只能包含字母/数字/下划线,不能数字开头,且不超过 ${MAX_ALIAS_NAME_LENGTH} 字符`);
73
+ }
74
+ if (text.length > MAX_ALIAS_TEXT_LENGTH) {
75
+ throw new Error(`别名文本超过 ${MAX_ALIAS_TEXT_LENGTH} 字符限制`);
76
+ }
77
+ if (!this.has(name) && this.data.length >= MAX_ALIASES) {
78
+ throw new Error(`别名数量已达上限 ${MAX_ALIASES}`);
79
+ }
80
+ const existing = this.get(name);
81
+ const entry = {
82
+ name,
83
+ text,
84
+ createdAt: existing?.createdAt ?? new Date().toISOString(),
85
+ };
86
+ const idx = this.data.findIndex((a) => a.name === name);
87
+ if (idx >= 0) {
88
+ this.data[idx] = entry;
89
+ }
90
+ else {
91
+ this.data.push(entry);
92
+ }
93
+ this.persist();
94
+ return entry;
95
+ }
96
+ /** 删除别名;不存在返回 false。 */
97
+ remove(name) {
98
+ const before = this.data.length;
99
+ this.data = this.data.filter((a) => a.name !== name);
100
+ if (this.data.length === before)
101
+ return false;
102
+ this.persist();
103
+ return true;
104
+ }
105
+ }
106
+ //# sourceMappingURL=alias-store.js.map
@@ -1,10 +1,16 @@
1
1
  /**
2
- * Atomically write string content to a file via tmp + rename.
2
+ * Atomically write content (string or binary Buffer) to a file via tmp + rename.
3
3
  * Falls back to copy+unlink when rename fails with EXDEV (cross-device).
4
4
  */
5
- export declare function atomicWrite(filePath: string, content: string, encoding?: BufferEncoding): void;
5
+ export declare function atomicWrite(filePath: string, content: string | Buffer, encoding?: BufferEncoding): void;
6
6
  /**
7
7
  * Atomically write JSON data to a file via tmp + rename.
8
8
  * Ensures a crash mid-write cannot leave a truncated file.
9
9
  */
10
10
  export declare function atomicWriteJson(filePath: string, data: unknown): void;
11
+ /**
12
+ * 把已写好的文件原子移动到目标位置(rename 同设备即原子;EXDEV 跨设备
13
+ * 回退 copy + unlink)。失败时抛出,由调用方负责清理源文件(best-effort)。
14
+ * 用于入站媒体:下载已流式写入临时文件,落盘只需一次 move,不再复制进内存。
15
+ */
16
+ export declare function atomicMoveFile(srcPath: string, destPath: string): void;