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
@@ -0,0 +1,62 @@
1
+ import { type AppConfig } from '../config/index.js';
2
+ import type { InboundMediaItem, InboundMediaPayload } from '../connector/index.js';
3
+ export interface InboundMediaDeps {
4
+ /** 解析用户 cwd(bridge.resolveCwd 注入,统一三份复制点)。 */
5
+ resolveCwd: (userId: string) => string | undefined;
6
+ /** 读取当前配置(活引用,随 /config 保存更新,避免启动快照过期)。 */
7
+ getConfig: () => AppConfig;
8
+ /** 发送提示消息(bridge.sendResult 包装)。 */
9
+ send(ctx: {
10
+ userId: string;
11
+ chatId: string;
12
+ messageId: string;
13
+ }, text: string): Promise<boolean>;
14
+ }
15
+ /**
16
+ * 文件名 sanitize:
17
+ * path.basename 剥离目录(防 `../` 穿越),再替换控制字符与路径分隔符;
18
+ * 空名 / `.` / `..` 视为无效(返回空串由调用方生成兜底名)。
19
+ */
20
+ export declare function sanitizeFileName(name: string): string;
21
+ /**
22
+ * image 消息扩展名:优先 MIME 映射,未知时按魔数兜底;
23
+ * 两者都无法识别时返回 undefined(调用方省略扩展名,避免错误标注格式)。
24
+ */
25
+ export declare function imageExtension(mimeType: string | undefined, head: Buffer): string | undefined;
26
+ /** 按 UTF-8 字节数截断字符串(避免截在多字节字符中间)。 */
27
+ import { truncateUtf8 } from '../common/truncate.js';
28
+ export { truncateUtf8 };
29
+ /**
30
+ * 文件名长度限制(字节级,保留扩展名),避免 ENAMETOOLONG 整文件失败。
31
+ * 极罕见情况(扩展名本身超限)直接截断全名。
32
+ */
33
+ export declare function limitFileNameLength(name: string, maxBytes?: number): string;
34
+ /**
35
+ * 生成落盘文件名:
36
+ * - file 消息:保留原始文件名(已 sanitize);无文件名兜底 file_<HHmmss>_<n>;
37
+ * - image 消息:image_<HHmmss>_<n>.<ext>,ext 按 MIME。
38
+ */
39
+ export declare function buildFileName(item: InboundMediaItem, index: number, receivedAt: Date): string;
40
+ /** 同名冲突自动加序号(name-1.ext、name-2.ext…),不覆盖已有文件。 */
41
+ export declare function uniqueTargetPath(dir: string, fileName: string): string;
42
+ export declare class InboundMediaHandler {
43
+ private readonly deps;
44
+ private readonly pending;
45
+ private readonly batchWindowMs;
46
+ constructor(deps: InboundMediaDeps);
47
+ /** 落盘并安排/合并提示。 */
48
+ handle(payload: InboundMediaPayload): Promise<void>;
49
+ /** 立即冲刷某用户的全部待合批提示(文本到达时调用)。 */
50
+ flushAll(userId: string, chatId: string): void;
51
+ /** 冲刷全部待合批提示(/exit、/restart 干净退出前调用,避免丢最后一条提示)。 */
52
+ flushAllPending(): Promise<void>;
53
+ /**
54
+ * dirName 配置校验:只允许单层目录名(不允许 `/`、`\`、`..`、`.`、控制字符),
55
+ * 非法值回退默认 `.lark-remote-temp`,避免配置意外把文件写到 cwd 之外。
56
+ */
57
+ private safeDirName;
58
+ /** 清理未被移动的临时文件(无 cwd / 目录创建失败等提前返回路径)。 */
59
+ private cleanupTemps;
60
+ private resolveCwd;
61
+ private flush;
62
+ }
@@ -0,0 +1,282 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DEFAULT_INBOUND_MEDIA_DIR_NAME } from '../config/index.js';
4
+ import { atomicMoveFile } from '../persistence/atomic-write.js';
5
+ import { getLogger } from '../logger/index.js';
6
+ import { silentlyUnlink } from '../common/fs.js';
7
+ /**
8
+ * 入站媒体落盘。
9
+ *
10
+ * 核心设计:
11
+ * - 存储"到达即存":每个 media 立即原子写入
12
+ * `<cwd>/.lark-remote-temp/<YYYYMMDDHHmm>/`,不等待合批窗口;
13
+ * - 合批只影响提示:500ms 窗口内同一 (userId, chatId, replyTo) 的保存结果
14
+ * 合并成一条"已保存 N 个文件"提示;
15
+ * - 文本消息到达时由 Bridge.flushMediaNotifications 立即冲刷批次
16
+ * ("先图后文字时文字到达先冲刷批次"),避免提示被后续消息淹没。
17
+ */
18
+ /** 合批窗口(毫秒)。同一窗口内到达的保存提示合并为一条。 */
19
+ const DEFAULT_BATCH_WINDOW_MS = 500;
20
+ /** 单条提示最多列出的文件数;超出折叠为 "… 等 N 个文件"。 */
21
+ const MAX_PATHS_IN_NOTIFICATION = 10;
22
+ /** 文件名最长字节数(macOS/APFS 单组件上限 255 字节,留余量用 240)。 */
23
+ const MAX_FILE_NAME_BYTES = 240;
24
+ /** 魔数检测读取的头部字节数(png/jpg/gif/webp 签名都在前 12 字节内)。 */
25
+ const MAGIC_HEAD_BYTES = 12;
26
+ const MIME_TO_EXT = {
27
+ 'image/png': 'png',
28
+ 'image/jpeg': 'jpg',
29
+ 'image/gif': 'gif',
30
+ 'image/webp': 'webp',
31
+ };
32
+ function pad2(n) {
33
+ return String(n).padStart(2, '0');
34
+ }
35
+ function hasControlChar(s) {
36
+ for (const ch of s) {
37
+ const code = ch.charCodeAt(0);
38
+ if (code <= 0x1f || code === 0x7f)
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ /** 子目录时间戳:YYYYMMDDHHmm(本地时间,精确到分钟)。 */
44
+ function timeStampDir(d) {
45
+ return (`${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}` +
46
+ `${pad2(d.getHours())}${pad2(d.getMinutes())}`);
47
+ }
48
+ /** 文件名时间戳:HHmmss(本地时间)。 */
49
+ function timeStampHms(d) {
50
+ return `${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
51
+ }
52
+ /**
53
+ * 文件名 sanitize:
54
+ * path.basename 剥离目录(防 `../` 穿越),再替换控制字符与路径分隔符;
55
+ * 空名 / `.` / `..` 视为无效(返回空串由调用方生成兜底名)。
56
+ */
57
+ export function sanitizeFileName(name) {
58
+ const base = path.basename(name).trim();
59
+ const chars = [];
60
+ for (const ch of base) {
61
+ const code = ch.charCodeAt(0);
62
+ // 控制字符与路径分隔符一律替换为下划线(no-control-regex 不允许字面
63
+ // 控制字符转义出现在正则里,逐字符判断语义等价且无 lint 冲突)。
64
+ if (code <= 0x1f || code === 0x7f || ch === '/' || ch === '\\') {
65
+ chars.push('_');
66
+ }
67
+ else {
68
+ chars.push(ch);
69
+ }
70
+ }
71
+ const sanitized = chars.join('');
72
+ if (sanitized === '' || sanitized === '.' || sanitized === '..')
73
+ return '';
74
+ return sanitized;
75
+ }
76
+ /**
77
+ * image 消息扩展名:优先 MIME 映射,未知时按魔数兜底;
78
+ * 两者都无法识别时返回 undefined(调用方省略扩展名,避免错误标注格式)。
79
+ */
80
+ export function imageExtension(mimeType, head) {
81
+ if (mimeType && MIME_TO_EXT[mimeType])
82
+ return MIME_TO_EXT[mimeType];
83
+ if (head.length >= 12 &&
84
+ head.subarray(0, 4).toString('latin1') === 'RIFF' &&
85
+ head.subarray(8, 12).toString('latin1') === 'WEBP') {
86
+ return 'webp';
87
+ }
88
+ if (head.length >= 8 &&
89
+ head.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
90
+ return 'png';
91
+ }
92
+ if (head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff) {
93
+ return 'jpg';
94
+ }
95
+ if (head.length >= 4 && head.subarray(0, 4).toString('latin1') === 'GIF8') {
96
+ return 'gif';
97
+ }
98
+ return undefined;
99
+ }
100
+ /** 按 UTF-8 字节数截断字符串(避免截在多字节字符中间)。 */
101
+ import { truncateUtf8 } from '../common/truncate.js';
102
+ export { truncateUtf8 };
103
+ /**
104
+ * 文件名长度限制(字节级,保留扩展名),避免 ENAMETOOLONG 整文件失败。
105
+ * 极罕见情况(扩展名本身超限)直接截断全名。
106
+ */
107
+ export function limitFileNameLength(name, maxBytes = MAX_FILE_NAME_BYTES) {
108
+ if (Buffer.byteLength(name, 'utf8') <= maxBytes)
109
+ return name;
110
+ const ext = path.extname(name);
111
+ const stem = name.slice(0, name.length - ext.length);
112
+ const maxStemBytes = maxBytes - Buffer.byteLength(ext, 'utf8');
113
+ if (maxStemBytes <= 0)
114
+ return truncateUtf8(name, maxBytes, false, '');
115
+ return truncateUtf8(stem, maxStemBytes, false, '') + ext;
116
+ }
117
+ /** 只读临时文件头部若干字节(魔数检测用,不把整个文件读进内存)。 */
118
+ function readFileHead(filePath, maxBytes = MAGIC_HEAD_BYTES) {
119
+ const fd = fs.openSync(filePath, 'r');
120
+ try {
121
+ const buf = Buffer.alloc(maxBytes);
122
+ const read = fs.readSync(fd, buf, 0, maxBytes, 0);
123
+ return buf.subarray(0, read);
124
+ }
125
+ finally {
126
+ fs.closeSync(fd);
127
+ }
128
+ }
129
+ /**
130
+ * 生成落盘文件名:
131
+ * - file 消息:保留原始文件名(已 sanitize);无文件名兜底 file_<HHmmss>_<n>;
132
+ * - image 消息:image_<HHmmss>_<n>.<ext>,ext 按 MIME。
133
+ */
134
+ export function buildFileName(item, index, receivedAt) {
135
+ if (item.type === 'file') {
136
+ const sanitized = item.fileName ? sanitizeFileName(item.fileName) : '';
137
+ return ((sanitized ? limitFileNameLength(sanitized) : '') ||
138
+ `file_${timeStampHms(receivedAt)}_${index}`);
139
+ }
140
+ const ext = imageExtension(item.mimeType, readFileHead(item.tempPath));
141
+ return `image_${timeStampHms(receivedAt)}_${index}${ext ? `.${ext}` : ''}`;
142
+ }
143
+ /** 同名冲突自动加序号(name-1.ext、name-2.ext…),不覆盖已有文件。 */
144
+ export function uniqueTargetPath(dir, fileName) {
145
+ const ext = path.extname(fileName);
146
+ const stem = fileName.slice(0, fileName.length - ext.length);
147
+ let candidate = path.join(dir, fileName);
148
+ let n = 1;
149
+ while (fs.existsSync(candidate)) {
150
+ candidate = path.join(dir, `${stem}-${n}${ext}`);
151
+ n += 1;
152
+ }
153
+ return candidate;
154
+ }
155
+ function formatFailure(f) {
156
+ return f.fileName ? `${f.fileName}: ${f.reason}` : f.reason;
157
+ }
158
+ function batchKey(payload) {
159
+ return `${payload.userId}:${payload.chatId}:${payload.replyToMessageId ?? ''}`;
160
+ }
161
+ export class InboundMediaHandler {
162
+ deps;
163
+ pending = new Map();
164
+ batchWindowMs;
165
+ constructor(deps) {
166
+ this.deps = deps;
167
+ this.batchWindowMs = DEFAULT_BATCH_WINDOW_MS;
168
+ }
169
+ /** 落盘并安排/合并提示。 */
170
+ async handle(payload) {
171
+ const cwd = this.resolveCwd(payload.userId);
172
+ if (!cwd) {
173
+ this.cleanupTemps(payload);
174
+ await this.deps.send({ userId: payload.userId, chatId: payload.chatId, messageId: payload.messageId }, '⚠️ 未设置工作目录,无法保存文件。请先使用 /cd <path> 或 /ws use 设置');
175
+ return;
176
+ }
177
+ const receivedAt = new Date();
178
+ const dir = path.join(cwd, this.safeDirName(), timeStampDir(receivedAt));
179
+ try {
180
+ fs.mkdirSync(dir, { recursive: true });
181
+ }
182
+ catch (err) {
183
+ // 写盘失败必须明确提示;临时文件一并清理避免泄漏。
184
+ this.cleanupTemps(payload);
185
+ await this.deps.send({ userId: payload.userId, chatId: payload.chatId, messageId: payload.messageId }, `⚠️ 保存失败:无法创建目录 ${dir}:${err.message}`);
186
+ return;
187
+ }
188
+ const errors = payload.failures.map(formatFailure);
189
+ const saved = [];
190
+ for (let i = 0; i < payload.media.length; i += 1) {
191
+ const item = payload.media[i];
192
+ try {
193
+ const target = uniqueTargetPath(dir, buildFileName(item, i + 1, receivedAt));
194
+ atomicMoveFile(item.tempPath, target);
195
+ saved.push(target);
196
+ }
197
+ catch (err) {
198
+ silentlyUnlink(item.tempPath);
199
+ const label = item.fileName
200
+ ? sanitizeFileName(item.fileName) || item.fileName
201
+ : `第 ${i + 1} 个`;
202
+ errors.push(`${label}: ${err.message}`);
203
+ }
204
+ }
205
+ if (saved.length === 0) {
206
+ await this.deps.send({ userId: payload.userId, chatId: payload.chatId, messageId: payload.messageId }, `⚠️ 保存失败:${errors.join(';') || '未知错误'}`);
207
+ return;
208
+ }
209
+ const key = batchKey(payload);
210
+ const existing = this.pending.get(key);
211
+ if (existing) {
212
+ existing.saved.push(...saved);
213
+ existing.errors.push(...errors);
214
+ clearTimeout(existing.timer);
215
+ existing.timer = setTimeout(() => void this.flush(key), this.batchWindowMs);
216
+ return;
217
+ }
218
+ const timer = setTimeout(() => void this.flush(key), this.batchWindowMs);
219
+ this.pending.set(key, { payload, saved, errors, timer });
220
+ }
221
+ /** 立即冲刷某用户的全部待合批提示(文本到达时调用)。 */
222
+ flushAll(userId, chatId) {
223
+ for (const key of [...this.pending.keys()]) {
224
+ const batch = this.pending.get(key);
225
+ if (batch && batch.payload.userId === userId && batch.payload.chatId === chatId) {
226
+ void this.flush(key);
227
+ }
228
+ }
229
+ }
230
+ /** 冲刷全部待合批提示(/exit、/restart 干净退出前调用,避免丢最后一条提示)。 */
231
+ async flushAllPending() {
232
+ const keys = [...this.pending.keys()];
233
+ await Promise.all(keys.map((key) => this.flush(key)));
234
+ }
235
+ /**
236
+ * dirName 配置校验:只允许单层目录名(不允许 `/`、`\`、`..`、`.`、控制字符),
237
+ * 非法值回退默认 `.lark-remote-temp`,避免配置意外把文件写到 cwd 之外。
238
+ */
239
+ safeDirName() {
240
+ const name = this.deps.getConfig().inboundMedia.dirName;
241
+ if (name &&
242
+ name !== '.' &&
243
+ name !== '..' &&
244
+ !name.includes('/') &&
245
+ !name.includes('\\') &&
246
+ !hasControlChar(name)) {
247
+ return name;
248
+ }
249
+ getLogger().warn(`[media] invalid inboundMedia.dirName "${String(name)}", falling back to .lark-remote-temp`);
250
+ return DEFAULT_INBOUND_MEDIA_DIR_NAME;
251
+ }
252
+ /** 清理未被移动的临时文件(无 cwd / 目录创建失败等提前返回路径)。 */
253
+ cleanupTemps(payload) {
254
+ for (const item of payload.media) {
255
+ silentlyUnlink(item.tempPath);
256
+ }
257
+ }
258
+ resolveCwd(userId) {
259
+ return this.deps.resolveCwd(userId);
260
+ }
261
+ async flush(key) {
262
+ const batch = this.pending.get(key);
263
+ if (!batch)
264
+ return;
265
+ this.pending.delete(key);
266
+ clearTimeout(batch.timer);
267
+ const lines = batch.saved.slice(0, MAX_PATHS_IN_NOTIFICATION).map((p) => `- ${p}`);
268
+ const overflow = batch.saved.length > MAX_PATHS_IN_NOTIFICATION ? `\n… 等 ${batch.saved.length} 个文件` : '';
269
+ const failures = batch.errors.length > 0 ? `\n⚠️ ${batch.errors.join(';')}` : '';
270
+ const text = `📎 已保存 ${batch.saved.length} 个文件:\n` +
271
+ lines.join('\n') +
272
+ overflow +
273
+ failures +
274
+ '\n💡 你可以直接说:请处理刚才保存的文件';
275
+ await this.deps.send({
276
+ userId: batch.payload.userId,
277
+ chatId: batch.payload.chatId,
278
+ messageId: batch.payload.messageId,
279
+ }, text);
280
+ }
281
+ }
282
+ //# sourceMappingURL=inbound-media.js.map
@@ -4,7 +4,9 @@ import type { SessionReaderRegistry, SessionStore } from '../session/index.js';
4
4
  import type { AppConfig } from '../config/index.js';
5
5
  import { type RunCardChannel } from '../card/run-card-session.js';
6
6
  import type { RunTerminal } from '../card/run-state.js';
7
- import { type QueuedTask, type EnqueueOptions, type AgentBinding } from './queue-manager.js';
7
+ import type { AgentKind } from '../runner/types.js';
8
+ import { QueueManager, type QueuedTask, type EnqueueOptions, type AgentBinding } from './queue-manager.js';
9
+ import type { InboundMediaPayload } from '../connector/index.js';
8
10
  /**
9
11
  * Seam (§7): the channel capabilities the bridge needs. `sendWithRetry` is
10
12
  * the minimum a Feishu channel must offer for the bridge to push messages
@@ -25,6 +27,7 @@ interface BridgeChannel extends RunCardChannel {
25
27
  reconnect(): Promise<void>;
26
28
  sendFile(chatId: string, filePath: string): Promise<string>;
27
29
  addReaction(messageId: string, emoji: string): Promise<void>;
30
+ removeReactionByEmoji(messageId: string, emoji: string): Promise<void>;
28
31
  }
29
32
  /** Caller context for a bridge operation. */
30
33
  interface BridgeContext {
@@ -96,8 +99,10 @@ interface BridgeDeps {
96
99
  * Runner/BridgeChannel seams.
97
100
  */
98
101
  export declare class Bridge {
99
- private connector;
100
- private sessionStore;
102
+ /** 飞书连接通道(public:集成测试的 seam,替代 as unknown as 访问)。 */
103
+ connector: BridgeChannel;
104
+ /** 会话存储(public:测试辅助读取/写入 cwd 等,替代私有访问)。 */
105
+ sessionStore: SessionStore;
101
106
  private config;
102
107
  private workspaceStore?;
103
108
  private idleTimeoutMs;
@@ -105,6 +110,8 @@ export declare class Bridge {
105
110
  private agentRegistry;
106
111
  /** Session reader registry for usage and completion cards. */
107
112
  private sessionReaderRegistry;
113
+ /** 入站媒体(图片/文件)落盘 + 合批提示。 */
114
+ private readonly mediaHandler;
108
115
  /**
109
116
  * Per-workspace runner instances, keyed by (cwd, agentKind).
110
117
  * Fix 4 (2026-07-18): Each agentKind has its own cache slot, so switching
@@ -115,9 +122,17 @@ export declare class Bridge {
115
122
  */
116
123
  private runners;
117
124
  /** Queue manager for per-workspace serial processing queue. */
118
- private queueManager;
125
+ /** 普通消息队列管理器(public:测试注入 queue-card 状态用)。 */
126
+ queueManager: QueueManager;
119
127
  /** Active runs per workspace (cwd). Multiple workspaces can run in parallel. */
120
128
  private activeRuns;
129
+ /** Approval coordinators keyed by runId. */
130
+ private approvalCoordinators;
131
+ /**
132
+ * 最近一次已完成的 codex app-server run(按 cwd)。run 结束后 activeRuns
133
+ * 已清空,Compact 需要它来校验 runId 并提供 sessionId。
134
+ */
135
+ private lastCompactableCodexRun;
121
136
  /**
122
137
  * Active `!` bash runs, keyed by runId (NOT cwd). Bash runs bypass the serial
123
138
  * queue and may run in parallel with claude runs / other bash runs in the same
@@ -136,8 +151,25 @@ export declare class Bridge {
136
151
  private getRunner;
137
152
  /** Update the config reference (call after `/config set` reloads). */
138
153
  setConfig(config: AppConfig): void;
154
+ /**
155
+ * Push updated approval-mode settings to active workspace-lifetime runners.
156
+ * `clearRunners()` deliberately skips workspaces with active runs; this
157
+ * gives those runners a chance to apply the new settings immediately via
158
+ * the unified duck method `updateApprovalMode` (codex → thread/settings/
159
+ * update, kimi → session/set_mode, opencode → session/set_mode) instead of
160
+ * keeping a stale snapshot until the runner is evicted/recreated.
161
+ */
162
+ syncActiveApprovalModes(): void;
163
+ /** Per-agent approval-mode settings snapshot from the current config. */
164
+ private approvalModeSettingsFor;
139
165
  /** Get the current runner for a workspace. Used by router for agent-specific commands (P3.2). */
140
166
  getCurrentRunner(workspace: string): AgentRunner;
167
+ /**
168
+ * Whether the runner for the given agent kind has runCompact capability.
169
+ * Used by router for compact button gating (design doc §6.2-2: duck-typing
170
+ * instead of hardcoding agentKind === 'codex').
171
+ */
172
+ hasRunCompact(workspace: string, kind?: AgentKind): boolean;
141
173
  /** 入队时刻快照:当前 defaultAgent + 该 agent 的 sessionId(无 session 则 undefined)。
142
174
  * 唯一捕获点:排队消息在 T0 把 agent+session 钉进 AgentBinding,随任务闭包带到 T1
143
175
  * 执行时刻,避免 /new、/config 在排队期间改写 live 状态导致语义漂移(方案 D4)。 */
@@ -166,7 +198,13 @@ export declare class Bridge {
166
198
  * Extract agent-specific run options from config.
167
199
  * Returns model/effort/etc. based on current defaultAgent.
168
200
  */
169
- private getAgentRunOptions;
201
+ /** 解析某 agent 的 run 选项(forwardToClaude 用;测试直接调用的 seam)。 */
202
+ getAgentRunOptions(agent?: AgentKind): {
203
+ model?: string;
204
+ effort?: string;
205
+ thinking?: string;
206
+ reasoningEffort?: string;
207
+ };
170
208
  /**
171
209
  * Enqueue a task into the workspace-level serial bridge queue.
172
210
  * Delegates to QueueManager for actual implementation.
@@ -275,6 +313,12 @@ export declare class Bridge {
275
313
  updateCardInPlace(card: object, ctx: BridgeContext): Promise<boolean>;
276
314
  /** Send a file to the user via Feishu. */
277
315
  sendFile(filePath: string, ctx: BridgeContext): Promise<void>;
316
+ /** 入站媒体落盘入口(connector 下载完成后调用)。 */
317
+ onInboundMedia(payload: InboundMediaPayload): Promise<void>;
318
+ /** 文本消息到达时立即冲刷待合批的媒体保存提示。 */
319
+ flushMediaNotifications(userId: string, chatId: string): void;
320
+ /** 冲刷全部待合批提示(/exit、/restart 干净退出前调用)。 */
321
+ flushAllMediaNotifications(): Promise<void>;
278
322
  /** Reconnect the underlying Feishu channel. */
279
323
  reconnect(): Promise<void>;
280
324
  /**
@@ -324,6 +368,95 @@ export declare class Bridge {
324
368
  * find and return to this session without scrolling up.
325
369
  */
326
370
  private sendCompletionNotificationCard;
371
+ /**
372
+ * Resolve the ApprovalCoordinator for a run, or throw the standard
373
+ * "已结束" error. Shared preamble of all approval card-action handlers.
374
+ */
375
+ private withCoordinator;
376
+ /**
377
+ * Handle an approval response from a card button click.
378
+ * Routes to the ApprovalCoordinator for the given run.
379
+ */
380
+ handleApprovalRespond(opts: {
381
+ runId: string;
382
+ requestId: number | string;
383
+ decision: string;
384
+ nonce: string;
385
+ }): Promise<void>;
386
+ /**
387
+ * Handle a permission toggle from a card button click.
388
+ * Routes to the ApprovalCoordinator for the given run.
389
+ */
390
+ handleApprovalToggle(opts: {
391
+ runId: string;
392
+ requestId: number | string;
393
+ permId: string;
394
+ selected?: boolean;
395
+ }): Promise<void>;
396
+ /**
397
+ * Claude AskUserQuestion 选项点击(单选即时提交 / 多选切换勾选)。
398
+ * 路由到 ApprovalCoordinator.toggleAnswer。
399
+ */
400
+ handleApprovalAnswer(opts: {
401
+ runId: string;
402
+ requestId: number | string;
403
+ questionIndex: number;
404
+ option: string;
405
+ nonce: string;
406
+ }): Promise<void>;
407
+ /**
408
+ * Claude AskUserQuestion 多选问题的「提交答案」按钮。
409
+ * 路由到 ApprovalCoordinator.submitAnswers。
410
+ */
411
+ handleApprovalAnswerSubmit(opts: {
412
+ runId: string;
413
+ requestId: number | string;
414
+ questionIndex: number;
415
+ nonce: string;
416
+ }): Promise<void>;
417
+ /**
418
+ * Claude AskUserQuestion 自定义答案(Other,review P3-4):自由文本直接
419
+ * 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
420
+ */
421
+ handleApprovalAnswerCustom(opts: {
422
+ runId: string;
423
+ requestId: number | string;
424
+ questionIndex: number;
425
+ text: string;
426
+ nonce: string;
427
+ }): Promise<void>;
428
+ /**
429
+ * Handle codex.compact card action — trigger a Codex compaction request.
430
+ * Validates the run exists and is in a terminal state, then calls
431
+ * the app-server runner's runCompact().
432
+ */
433
+ handleCodexCompact(value: {
434
+ runId?: string;
435
+ }, ctx: BridgeContext): Promise<void>;
436
+ /**
437
+ * Stream a Codex compaction to a card: start a RunCardSession with
438
+ * operationKind='compaction' (no recursive Compact button), consume
439
+ * runner.runCompact() events, read authoritative jsonl usage, and finish
440
+ * the card. Shared by handleCodexCompact (run card button) and
441
+ * handleResumeCompact (resume cards) so both flows stay in sync.
442
+ *
443
+ * Precondition: callers have already validated that `runner` implements
444
+ * runCompact and that the session exists in `cwd`.
445
+ */
446
+ private streamCodexCompact;
447
+ /**
448
+ * Handle resume.compact card action — compact a session directly from a
449
+ * resume card (auto-resume / `/resume <id>`), without a runId.
450
+ *
451
+ * Unlike handleCodexCompact (which validates against the last finished run's
452
+ * runId), the resume card carries the sessionId + agent it was rendered for.
453
+ * Validation: the session must exist in the current cwd (same rule as
454
+ * cmdResume / resume.use) and the runner must implement runCompact.
455
+ */
456
+ handleResumeCompact(value: {
457
+ sessionId?: string;
458
+ agent?: string;
459
+ }, ctx: BridgeContext): Promise<void>;
327
460
  /**
328
461
  * Execute a bash command and stream output to a card.
329
462
  * `!` commands bypass the serial queue entirely (see body comment + §9.6):