lark-remote 0.1.5 → 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 (190) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +5 -1
  3. package/README.md +10 -2
  4. package/dist/bridge/approval-coordinator.d.ts +131 -0
  5. package/dist/bridge/approval-coordinator.js +409 -0
  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 +138 -5
  9. package/dist/bridge/index.js +569 -65
  10. package/dist/bridge/queue-manager.d.ts +11 -1
  11. package/dist/bridge/queue-manager.js +20 -11
  12. package/dist/card/approval-render.d.ts +16 -0
  13. package/dist/card/approval-render.js +363 -0
  14. package/dist/card/bash-card-session.d.ts +2 -24
  15. package/dist/card/bash-card-session.js +6 -73
  16. package/dist/card/bash-renderer.js +2 -3
  17. package/dist/card/card-budget.js +29 -75
  18. package/dist/card/card-session.d.ts +48 -0
  19. package/dist/card/card-session.js +86 -0
  20. package/dist/card/card-shared.d.ts +13 -10
  21. package/dist/card/card-shared.js +27 -24
  22. package/dist/card/run-card-session.d.ts +2 -55
  23. package/dist/card/run-card-session.js +6 -104
  24. package/dist/card/run-renderer.js +140 -202
  25. package/dist/card/run-state.d.ts +38 -3
  26. package/dist/card/run-state.js +221 -11
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/dir.js +1 -1
  40. package/dist/config/index.d.ts +116 -9
  41. package/dist/config/index.js +122 -29
  42. package/dist/connector/index.d.ts +74 -2
  43. package/dist/connector/index.js +119 -1
  44. package/dist/index.js +139 -99
  45. package/dist/instance-lock.js +4 -12
  46. package/dist/order/alias-resolve.d.ts +13 -0
  47. package/dist/order/alias-resolve.js +22 -0
  48. package/dist/order/alias-store.d.ts +38 -0
  49. package/dist/order/alias-store.js +106 -0
  50. package/dist/persistence/atomic-write.d.ts +8 -2
  51. package/dist/persistence/atomic-write.js +23 -7
  52. package/dist/restart.js +2 -1
  53. package/dist/router/card-helpers.d.ts +63 -0
  54. package/dist/router/card-helpers.js +146 -0
  55. package/dist/router/config/claude.js +25 -1
  56. package/dist/router/config/codex.d.ts +1 -1
  57. package/dist/router/config/codex.js +33 -5
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +9 -4
  61. package/dist/router/config/index.d.ts +12 -0
  62. package/dist/router/config/index.js +25 -1
  63. package/dist/router/config/kimi.d.ts +4 -1
  64. package/dist/router/config/kimi.js +27 -0
  65. package/dist/router/config/opencode.js +18 -5
  66. package/dist/router/config/pi.js +4 -5
  67. package/dist/router/config/types.d.ts +12 -3
  68. package/dist/router/index.d.ts +77 -17
  69. package/dist/router/index.js +455 -549
  70. package/dist/router/utils.d.ts +4 -0
  71. package/dist/router/utils.js +9 -2
  72. package/dist/runner/agent-choices-common.d.ts +16 -0
  73. package/dist/runner/agent-choices-common.js +31 -0
  74. package/dist/runner/claude/runner.d.ts +47 -10
  75. package/dist/runner/claude/runner.js +117 -46
  76. package/dist/runner/claude/session.d.ts +166 -0
  77. package/dist/runner/claude/session.js +749 -0
  78. package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
  79. package/dist/runner/codex/app-server/protocol-types.js +56 -0
  80. package/dist/runner/codex/app-server/runner.d.ts +104 -0
  81. package/dist/runner/codex/app-server/runner.js +377 -0
  82. package/dist/runner/codex/app-server/translator.d.ts +89 -0
  83. package/dist/runner/codex/app-server/translator.js +716 -0
  84. package/dist/runner/codex/index.d.ts +1 -1
  85. package/dist/runner/codex/index.js +1 -1
  86. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  87. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  88. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  89. package/dist/runner/common/acp/protocol-types.js +75 -0
  90. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  91. package/dist/runner/common/connection-based-runner.js +261 -0
  92. package/dist/runner/common/jsonrpc/client.d.ts +89 -0
  93. package/dist/runner/common/jsonrpc/client.js +243 -0
  94. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  95. package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
  96. package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
  97. package/dist/runner/common/jsonrpc/transport.js +230 -0
  98. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  99. package/dist/runner/common/jsonrpc/types.js +9 -0
  100. package/dist/runner/common/process-stopper.d.ts +2 -1
  101. package/dist/runner/common/process-stopper.js +1 -0
  102. package/dist/runner/common/runner-utils.d.ts +6 -13
  103. package/dist/runner/common/runner-utils.js +6 -47
  104. package/dist/runner/common/spawning-runner.d.ts +24 -70
  105. package/dist/runner/common/spawning-runner.js +98 -152
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/index.d.ts +5 -5
  109. package/dist/runner/index.js +4 -4
  110. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  111. package/dist/runner/kimi/acp/runner.js +448 -0
  112. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  113. package/dist/runner/kimi/acp/translator.js +336 -0
  114. package/dist/runner/kimi/index.d.ts +1 -1
  115. package/dist/runner/kimi/index.js +1 -1
  116. package/dist/runner/opencode/acp/index.d.ts +1 -0
  117. package/dist/runner/opencode/acp/index.js +2 -0
  118. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  119. package/dist/runner/opencode/acp/runner.js +429 -0
  120. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  121. package/dist/runner/opencode/acp/translator.js +345 -0
  122. package/dist/runner/opencode/index.d.ts +1 -1
  123. package/dist/runner/opencode/index.js +1 -1
  124. package/dist/runner/pi/index.d.ts +1 -1
  125. package/dist/runner/pi/index.js +1 -1
  126. package/dist/runner/pi/rpc/client.d.ts +52 -0
  127. package/dist/runner/pi/rpc/client.js +114 -0
  128. package/dist/runner/pi/rpc/index.d.ts +4 -0
  129. package/dist/runner/pi/rpc/index.js +4 -0
  130. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  131. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  132. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  133. package/dist/runner/pi/rpc/runner.js +199 -0
  134. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  135. package/dist/runner/pi/rpc/translator.js +258 -0
  136. package/dist/runner/registry.d.ts +7 -8
  137. package/dist/runner/registry.js +3 -4
  138. package/dist/runner/resolve-agent-choices.js +18 -59
  139. package/dist/runner/sync-agent-choices.js +16 -67
  140. package/dist/runner/types.d.ts +153 -2
  141. package/dist/session/claude/session-index.js +2 -3
  142. package/dist/session/claude/session-reader.js +3 -6
  143. package/dist/session/claude/sessions.js +6 -13
  144. package/dist/session/codex/rollout-reader.d.ts +2 -2
  145. package/dist/session/codex/rollout-reader.js +57 -17
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +2 -2
  148. package/dist/session/common/content-blocks.d.ts +7 -0
  149. package/dist/session/common/content-blocks.js +13 -9
  150. package/dist/session/common/jsonl.d.ts +8 -25
  151. package/dist/session/common/jsonl.js +126 -261
  152. package/dist/session/common/pagination.d.ts +28 -0
  153. package/dist/session/common/pagination.js +33 -0
  154. package/dist/session/kimi/sessions.d.ts +34 -1
  155. package/dist/session/kimi/sessions.js +208 -98
  156. package/dist/session/opencode/sessions.d.ts +17 -3
  157. package/dist/session/opencode/sessions.js +29 -29
  158. package/dist/session/pi/sessions.d.ts +5 -1
  159. package/dist/session/pi/sessions.js +12 -16
  160. package/dist/session/session-store.d.ts +11 -1
  161. package/dist/session/session-store.js +75 -178
  162. package/dist/update/index.d.ts +2 -2
  163. package/dist/update/index.js +2 -2
  164. package/dist/update/install.d.ts +20 -2
  165. package/dist/update/install.js +51 -7
  166. package/package.json +1 -1
  167. package/dist/runner/codex/argv.d.ts +0 -33
  168. package/dist/runner/codex/argv.js +0 -40
  169. package/dist/runner/codex/jsonl.d.ts +0 -37
  170. package/dist/runner/codex/jsonl.js +0 -221
  171. package/dist/runner/codex/runner.d.ts +0 -63
  172. package/dist/runner/codex/runner.js +0 -124
  173. package/dist/runner/common/exec-translator.d.ts +0 -73
  174. package/dist/runner/common/exec-translator.js +0 -105
  175. package/dist/runner/kimi/jsonl.d.ts +0 -28
  176. package/dist/runner/kimi/jsonl.js +0 -143
  177. package/dist/runner/kimi/runner.d.ts +0 -49
  178. package/dist/runner/kimi/runner.js +0 -105
  179. package/dist/runner/opencode/argv.d.ts +0 -32
  180. package/dist/runner/opencode/argv.js +0 -35
  181. package/dist/runner/opencode/jsonl.d.ts +0 -68
  182. package/dist/runner/opencode/jsonl.js +0 -295
  183. package/dist/runner/opencode/runner.d.ts +0 -61
  184. package/dist/runner/opencode/runner.js +0 -93
  185. package/dist/runner/pi/jsonl.d.ts +0 -128
  186. package/dist/runner/pi/jsonl.js +0 -249
  187. package/dist/runner/pi/runner.d.ts +0 -36
  188. package/dist/runner/pi/runner.js +0 -76
  189. package/dist/session/common/catch-up.d.ts +0 -12
  190. 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, CodexExecRunner, 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,76 +19,44 @@ 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';
24
- import { newSessionButton, agentDisplayName } from './card/card-shared.js';
22
+ import { buildSessionHistoryCard } from './router/card-helpers.js';
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
- async function sendAutoResumeCard(connector, contact, cwd, session, state) {
28
+ async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false) {
29
29
  if (!contact.userId)
30
30
  return;
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));
40
+ const actionButtons = [];
41
+ if (compactSupported) {
42
+ actionButtons.push(resumeCompactButton(session.sessionId, state.agentKind ?? 'codex'));
66
43
  }
67
- // Add new session button - use column_set+column (div+elements not supported in 2.0)
68
- const newSessionBtn = newSessionButton();
69
- elements.push({
70
- tag: 'column_set',
71
- columns: [
72
- {
73
- tag: 'column',
74
- width: 'auto',
75
- elements: [newSessionBtn],
76
- },
77
- ],
44
+ actionButtons.push(newSessionButton());
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,
78
59
  });
79
- const card = {
80
- schema: '2.0',
81
- config: {
82
- wide_screen_mode: true,
83
- },
84
- header: {
85
- title: {
86
- tag: 'plain_text',
87
- content: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
88
- },
89
- },
90
- body: { elements },
91
- };
92
60
  await connector.sendWithRetry(recipient, { card });
93
61
  }
94
62
  /** Initialize CLI args, config, and logger. Returns { config, configDir, logger, instanceLock }. */
@@ -161,28 +129,36 @@ function initializeRunner(config, configDir, cliArgs) {
161
129
  settings: cliArgs.settings,
162
130
  pidDir: configDir,
163
131
  workspace: ws,
132
+ permissionMode: claudeConfig.permissionMode,
133
+ idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
164
134
  });
165
135
  });
166
- // Register CodexExecRunner (codex exec --json, approval_policy=never).
167
- // Factory reads latest config from container.current so runtime config changes
168
- // (reasoningEffort, model, etc.) take effect after bridge.setConfig() + clearRunners().
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().
169
140
  const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
170
- agentRegistry.register('codex', (ws) => {
141
+ agentRegistry.register('codex', (_ws) => {
171
142
  const container = agentRegistry.getConfigContainer();
172
143
  const latestConfig = container?.current ?? config;
173
144
  const codexConfig = getAgentConfig(latestConfig, 'codex');
174
- return new CodexExecRunner({
145
+ return new CodexAppServerRunner({
146
+ kind: 'codex',
175
147
  model: codexConfig?.model,
176
148
  modelProvider: codexConfig?.modelProvider,
177
149
  reasoningEffort: codexConfig?.reasoningEffort,
178
- stopGraceMs: codexConfig?.stopGraceMs ?? latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
179
- pidDir: configDir,
180
- workspace: ws,
181
150
  sessionReader: codexSessionReader,
151
+ sandbox: codexConfig?.sandbox,
152
+ approvalPolicy: codexConfig?.approvalPolicy,
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,
182
159
  });
183
160
  });
184
- // Register OpencodeExecRunner (run mode)
185
- // Using `opencode run --format json --auto` instead of HTTP+SSE server mode
161
+ // Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
186
162
  // Register session reader (same instance as used by runner)
187
163
  const sessionReaderRegistry = new SessionReaderRegistry();
188
164
  sessionReaderRegistry.register('claude', new ClaudeSessionReader());
@@ -190,7 +166,7 @@ function initializeRunner(config, configDir, cliArgs) {
190
166
  // Register OpencodeSessionReader (CLI version)
191
167
  // Using `opencode session list` and `opencode export` instead of HTTP API
192
168
  const opencodeSessionReader = new OpencodeSessionReader();
193
- agentRegistry.register('opencode', (ws) => {
169
+ agentRegistry.register('opencode', (_ws) => {
194
170
  // Get latest config from container (set below for pi)
195
171
  const container = agentRegistry.getConfigContainer();
196
172
  const latestConfig = container?.current ?? config;
@@ -207,17 +183,15 @@ function initializeRunner(config, configDir, cliArgs) {
207
183
  model = `${provider}/${ocConfig.modelID}`;
208
184
  }
209
185
  }
210
- return new OpencodeExecRunner({
186
+ return new OpencodeAcpRunner({
187
+ kind: 'opencode',
211
188
  model,
212
- // P1-15: read stopGraceMs from latestConfig (not startup closure)
213
- stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
214
- pidDir: configDir,
215
- workspace: ws,
216
189
  sessionReader: opencodeSessionReader,
190
+ mode: ocConfig?.mode ?? 'build',
217
191
  });
218
192
  });
219
193
  sessionReaderRegistry.register('opencode', opencodeSessionReader);
220
- // Register PiRunner and PiSessionReader
194
+ // Register PiRpcRunner and PiSessionReader
221
195
  // 2026-07-12: 修复 config.save 后 pi provider 不生效的问题
222
196
  // 关键:factory 闭包必须能访问到最新的 config,而不是启动时的快照
223
197
  // 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值
@@ -229,7 +203,7 @@ function initializeRunner(config, configDir, cliArgs) {
229
203
  const container = agentRegistry.getConfigContainer();
230
204
  const latestConfig = container?.current ?? config;
231
205
  const piConf = getAgentConfig(latestConfig, 'pi');
232
- return new PiRunner({
206
+ return new PiRpcRunner({
233
207
  provider: piConf?.provider ?? 'Volcano',
234
208
  model: piConf?.model ?? 'glm-5.2',
235
209
  thinking: piConf?.thinking ?? 'medium',
@@ -237,27 +211,30 @@ function initializeRunner(config, configDir, cliArgs) {
237
211
  .split(',')
238
212
  .map((t) => t.trim())
239
213
  .filter(Boolean),
240
- pidDir: configDir,
241
214
  workspace: ws,
242
215
  sessionReader: piSessionReader,
243
216
  });
244
217
  });
245
218
  sessionReaderRegistry.register('pi', piSessionReader);
246
- // Register KimiRunner and KimiSessionReader
219
+ // Register KimiAcpRunner (pure ACP mode) and KimiSessionReader
247
220
  const kimiSessionReader = new KimiSessionReader();
248
- agentRegistry.register('kimi', (ws) => {
221
+ agentRegistry.register('kimi', (_ws) => {
249
222
  // 每次 factory 调用时从 registry 获取最新 config
250
223
  const container = agentRegistry.getConfigContainer();
251
224
  const latestConfig = container?.current ?? config;
252
225
  const kimiConf = getAgentConfig(latestConfig, 'kimi');
253
- return new KimiRunner({
254
- model: kimiConf?.model ?? 'kimi-code/k3',
255
- thinkingEffort: kimiConf?.thinkingEffort ?? 'max',
256
- // P1-15: read stopGraceMs from latestConfig (not startup closure)
257
- stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
258
- pidDir: configDir,
259
- workspace: ws,
226
+ const acpConf = kimiConf?.acp;
227
+ return new KimiAcpRunner({
228
+ kind: 'kimi',
260
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',
261
238
  });
262
239
  });
263
240
  sessionReaderRegistry.register('kimi', kimiSessionReader);
@@ -288,6 +265,48 @@ function initializeRunner(config, configDir, cliArgs) {
288
265
  }
289
266
  /** Setup message and card action handlers. */
290
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
+ });
291
310
  connector.setMessageHandler((msg) => {
292
311
  // 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时要求 PIN 认领
293
312
  const decision = binder.classify(msg.userId, msg.content, msg.chatId);
@@ -345,9 +364,16 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
345
364
  }
346
365
  // decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
347
366
  logger.info(`message from ${msg.userId}: ${msg.content.slice(0, 100)}`);
367
+ // 文本到达先冲刷待合批的媒体保存提示(先图后文字时文字到达
368
+ // 先冲刷批次,避免合批提示被后续消息淹没/丢图)。
369
+ bridge.flushMediaNotifications(msg.userId, msg.chatId);
348
370
  // Add Typing reaction to indicate "still alive"
349
371
  void connector.addReaction(msg.messageId, 'Typing');
350
- 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();
351
377
  if (trimmedLower === '/stop' || trimmedLower === '/t') {
352
378
  void (async () => {
353
379
  const stopped = await bridge.interruptCurrentRun({
@@ -364,9 +390,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
364
390
  })().catch((err) => logger.error('[control] /stop failed:', err));
365
391
  return;
366
392
  }
367
- if (msg.content.trim().startsWith('/')) {
393
+ if (content.trim().startsWith('/')) {
368
394
  void router
369
- .handle(msg.content, {
395
+ .handle(content, {
370
396
  userId: msg.userId,
371
397
  chatId: msg.chatId,
372
398
  messageId: msg.messageId,
@@ -379,9 +405,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
379
405
  // in the same workspace (design.md §9.6: the queue exists to prevent
380
406
  // concurrent claude runs, not to serialize bash). router.handle dispatches
381
407
  // `!` to bridge.executeBash, which tracks the run independently.
382
- if (msg.content.trim().startsWith('!')) {
408
+ if (content.trim().startsWith('!')) {
383
409
  void router
384
- .handle(msg.content, {
410
+ .handle(content, {
385
411
  userId: msg.userId,
386
412
  chatId: msg.chatId,
387
413
  messageId: msg.messageId,
@@ -405,7 +431,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
405
431
  // 捕获点 Bridge.currentBinding。
406
432
  const binding = bridge.currentBinding(msg.userId);
407
433
  bridge.enqueue(workspace, async () => {
408
- await router.handle(msg.content, {
434
+ await router.handle(content, {
409
435
  userId: msg.userId,
410
436
  chatId: msg.chatId,
411
437
  messageId: msg.messageId,
@@ -420,7 +446,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
420
446
  userId: msg.userId,
421
447
  chatId: msg.chatId,
422
448
  messageId: msg.messageId,
423
- messagePreview: msg.content.slice(0, 3000),
449
+ messagePreview: content.slice(0, 3000),
424
450
  // D3/Step4: binding 随 taskMeta 存进 QueuedTask,供替换闭包
425
451
  // (queue.edit/queue.immediate)复用,不重新快照。
426
452
  binding,
@@ -479,11 +505,20 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
479
505
  inputValue: action.raw?.action?.input_value ??
480
506
  action.action.input_value,
481
507
  };
482
- // queue.input 和 config.save 返回 CardActionResponse toast 给点击用户即时反馈。
483
- // 必须直接返回值 -- enqueueImmediate / enqueueConfigAction 是 fire-and-forget,
484
- // 会吞掉返回值。queue.input 只做一次卡片更新 + toast,快速且不需要串行队列序列化。
485
- // config.save 内部已有 enqueueConfigAction 串行化保证,但返回值需要直接返回给飞书。
486
- if (actionValue.cmd === 'queue.input' || actionValue.cmd === 'config.save') {
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 被静默吞掉)。
513
+ // 审批响应尤其不能落串行队列:run 任务占用队列头直到 turn 结束,审批响应
514
+ // 排在后面会形成死锁(run 不结束不执行、run 结束 coordinator 已删响应空转)。
515
+ if (actionValue.cmd === 'queue.input' ||
516
+ actionValue.cmd === 'config.save' ||
517
+ actionValue.cmd === 'approval.respond' ||
518
+ actionValue.cmd === 'approval.toggle' ||
519
+ actionValue.cmd === 'approval.answer' ||
520
+ actionValue.cmd === 'approval.answerSubmit' ||
521
+ actionValue.cmd === 'approval.answerCustom') {
487
522
  return router.handleCardAction(fullValue, { userId, chatId, messageId });
488
523
  }
489
524
  // order.exec → equivalent queued message (Plan A): resolve the order text
@@ -514,6 +549,8 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
514
549
  chatId,
515
550
  messageId,
516
551
  messagePreview: `card action: ${actionValue.cmd}`,
552
+ // Compact 是单向操作,排队卡不允许编辑(编辑预览无意义)。
553
+ editable: actionValue.cmd !== 'codex.compact' && actionValue.cmd !== 'resume.compact',
517
554
  },
518
555
  });
519
556
  }
@@ -599,6 +636,7 @@ async function main() {
599
636
  configPath: path.join(configDir, 'config.yaml'),
600
637
  workspacePath: path.join(configDir, 'workspace.json'),
601
638
  ordersPath: path.join(configDir, 'orders.json'),
639
+ aliasesPath: path.join(configDir, 'aliases.json'),
602
640
  restartSpawner: () => spawnReplacementBridge(path.join(configDir, 'logs')),
603
641
  sessionReaderRegistry,
604
642
  devMode: cliArgs.dev,
@@ -663,7 +701,9 @@ async function main() {
663
701
  recap: content.recap,
664
702
  displayTitle: content.displayTitle,
665
703
  agentKind: config.defaultAgent,
666
- });
704
+ },
705
+ // Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
706
+ bridge.hasRunCompact(restoredCwd, config.defaultAgent));
667
707
  }
668
708
  catch (err) {
669
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;