lark-remote 0.4.5 → 0.5.0

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 (124) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/bridge/approval-coordinator.d.ts +1 -1
  3. package/dist/bridge/approval-coordinator.js +3 -3
  4. package/dist/bridge/inbound-media.js +10 -7
  5. package/dist/bridge/index.d.ts +9 -9
  6. package/dist/bridge/index.js +41 -41
  7. package/dist/bridge/queue-manager.d.ts +12 -12
  8. package/dist/bridge/queue-manager.js +19 -19
  9. package/dist/card/approval-render.d.ts +1 -1
  10. package/dist/card/approval-render.js +6 -6
  11. package/dist/card/bash-card-session.d.ts +3 -3
  12. package/dist/card/bash-card-session.js +3 -3
  13. package/dist/card/bash-renderer.js +1 -1
  14. package/dist/card/card-session.d.ts +4 -4
  15. package/dist/card/card-session.js +6 -6
  16. package/dist/card/run-card-session.js +2 -2
  17. package/dist/card/run-renderer.d.ts +1 -1
  18. package/dist/card/run-renderer.js +14 -14
  19. package/dist/card/run-state.d.ts +2 -2
  20. package/dist/card/run-state.js +3 -3
  21. package/dist/card/tool-render.d.ts +1 -1
  22. package/dist/card/tool-render.js +6 -6
  23. package/dist/clone.d.ts +5 -3
  24. package/dist/clone.js +8 -4
  25. package/dist/common/truncate.d.ts +1 -1
  26. package/dist/common/truncate.js +2 -2
  27. package/dist/common/ttl-cache.d.ts +2 -2
  28. package/dist/common/ttl-cache.js +2 -2
  29. package/dist/config/codex-config.d.ts +9 -9
  30. package/dist/config/codex-config.js +18 -18
  31. package/dist/config/dir.js +5 -2
  32. package/dist/config/index.d.ts +3 -3
  33. package/dist/config/index.js +6 -6
  34. package/dist/config/opencode-config.js +3 -3
  35. package/dist/connector/index.d.ts +5 -5
  36. package/dist/connector/index.js +24 -11
  37. package/dist/inbound/event.d.ts +1 -1
  38. package/dist/inbound/event.js +1 -1
  39. package/dist/inbound/placeholder.d.ts +2 -2
  40. package/dist/inbound/placeholder.js +2 -2
  41. package/dist/index.js +9 -9
  42. package/dist/instance-lock.js +4 -3
  43. package/dist/logger/index.d.ts +1 -1
  44. package/dist/logger/index.js +4 -4
  45. package/dist/persistence/atomic-write.d.ts +1 -1
  46. package/dist/persistence/atomic-write.js +6 -2
  47. package/dist/platform/agent-stopper.d.ts +1 -1
  48. package/dist/platform/identity.d.ts +7 -1
  49. package/dist/platform/identity.js +2 -2
  50. package/dist/platform/path.d.ts +6 -0
  51. package/dist/platform/path.js +14 -8
  52. package/dist/platform/select.d.ts +1 -1
  53. package/dist/platform/select.js +1 -1
  54. package/dist/platform/shell.d.ts +2 -3
  55. package/dist/platform/spawn.d.ts +2 -2
  56. package/dist/platform/spawn.js +2 -2
  57. package/dist/platform/terminator-posix.js +1 -1
  58. package/dist/platform/terminator-win32.js +2 -2
  59. package/dist/platform/terminator.d.ts +3 -3
  60. package/dist/platform/terminator.js +2 -2
  61. package/dist/platform/types.d.ts +1 -1
  62. package/dist/restart.d.ts +1 -1
  63. package/dist/restart.js +5 -3
  64. package/dist/router/config/codex.d.ts +1 -1
  65. package/dist/router/config/codex.js +2 -2
  66. package/dist/router/config/opencode.js +1 -1
  67. package/dist/router/index.d.ts +9 -9
  68. package/dist/router/index.js +34 -34
  69. package/dist/router/order-exec-dispatch.js +2 -2
  70. package/dist/runner/bash/runner.d.ts +2 -2
  71. package/dist/runner/bash/runner.js +6 -6
  72. package/dist/runner/claude/runner.js +1 -1
  73. package/dist/runner/claude/session.d.ts +2 -2
  74. package/dist/runner/claude/session.js +14 -14
  75. package/dist/runner/codex/app-server/runner.js +3 -3
  76. package/dist/runner/codex/app-server/translator.d.ts +2 -2
  77. package/dist/runner/codex/app-server/translator.js +5 -5
  78. package/dist/runner/common/acp/acp-approval.d.ts +2 -2
  79. package/dist/runner/common/acp/acp-approval.js +3 -3
  80. package/dist/runner/common/acp/base-acp-runner.js +1 -1
  81. package/dist/runner/common/acp/base-translator.d.ts +3 -3
  82. package/dist/runner/common/acp/base-translator.js +4 -4
  83. package/dist/runner/common/acp/protocol-types.js +1 -1
  84. package/dist/runner/common/connection-based-runner.d.ts +1 -1
  85. package/dist/runner/common/connection-based-runner.js +2 -2
  86. package/dist/runner/common/jsonl-stream.d.ts +2 -2
  87. package/dist/runner/common/jsonl-stream.js +12 -12
  88. package/dist/runner/common/jsonrpc/client.d.ts +1 -1
  89. package/dist/runner/common/jsonrpc/client.js +1 -1
  90. package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
  91. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  92. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  93. package/dist/runner/common/jsonrpc/transport.js +2 -2
  94. package/dist/runner/common/spawning-runner.d.ts +21 -8
  95. package/dist/runner/common/spawning-runner.js +41 -15
  96. package/dist/runner/dsh/runner.js +2 -2
  97. package/dist/runner/kimi/acp/runner.d.ts +3 -3
  98. package/dist/runner/kimi/acp/runner.js +9 -9
  99. package/dist/runner/kimi/acp/translator.d.ts +2 -2
  100. package/dist/runner/kimi/acp/translator.js +4 -4
  101. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  102. package/dist/runner/opencode/acp/runner.js +3 -3
  103. package/dist/runner/opencode/acp/translator.js +1 -1
  104. package/dist/runner/pi/rpc/runner.js +2 -2
  105. package/dist/runner/pi/rpc/translator.d.ts +1 -1
  106. package/dist/runner/pi/rpc/translator.js +4 -4
  107. package/dist/runner/types.d.ts +1 -1
  108. package/dist/session/claude/session-index.d.ts +12 -0
  109. package/dist/session/claude/session-index.js +20 -5
  110. package/dist/session/claude/sessions.js +10 -10
  111. package/dist/session/codex/rollout-reader.d.ts +4 -4
  112. package/dist/session/codex/rollout-reader.js +12 -12
  113. package/dist/session/common/two-pass.js +2 -2
  114. package/dist/session/common/usage-accumulator.d.ts +2 -2
  115. package/dist/session/common/usage-accumulator.js +2 -2
  116. package/dist/session/dsh/sessions.js +2 -2
  117. package/dist/session/kimi/sessions.d.ts +2 -2
  118. package/dist/session/kimi/sessions.js +11 -11
  119. package/dist/session/opencode/sessions.d.ts +3 -3
  120. package/dist/session/opencode/sessions.js +12 -10
  121. package/dist/session/pi/sessions.js +13 -12
  122. package/dist/session/session-store.d.ts +1 -1
  123. package/dist/session/session-store.js +1 -1
  124. package/package.json +1 -1
package/dist/clone.d.ts CHANGED
@@ -25,7 +25,8 @@ import { type QRCodeInfo, type RegisterAppResult } from '@larksuite/channel';
25
25
  type CloneState = 'idle' | 'awaiting_scan' | 'finalizing';
26
26
  /**
27
27
  * 校验用户指定的分身名字:路径分隔符、以 . 开头、尾点/尾空格(win32 会静默
28
- * 吞掉)、win32 保留设备名一律拒绝。
28
+ * 吞掉)、win32 非法字符(`<>:"|?*`,NTFS/Win32 API 拒绝)、win32 保留设备名
29
+ * 一律拒绝。
29
30
  */
30
31
  export declare function isValidCloneName(name: string): boolean;
31
32
  /** 随机后缀:2 位数字 + 2 位小写字母/数字,如 `37ab`。 */
@@ -126,8 +127,9 @@ export declare class CloneSession {
126
127
  private readOldOwnerId;
127
128
  /**
128
129
  * 写配置失败时清理半成品目录。守卫:只删内容全部是本次产物
129
- * (config.yaml / startup-contact.json 及其 .tmp)的目录,混入其他
130
- * 文件时保留现场不动。
130
+ * (config.yaml / startup-contact.json 及其 tmp)的目录,混入其他
131
+ * 文件时保留现场不动。tmp 形态两种都认:atomic-write 现行命名
132
+ * (.{name}.tmp-{pid}-{ts}-{rand})与旧版固定 `.tmp`(升级瞬间崩溃的残留)。
131
133
  */
132
134
  private cleanupPartialDir;
133
135
  }
package/dist/clone.js CHANGED
@@ -24,13 +24,16 @@ const WIN_RESERVED_NAMES = new Set([
24
24
  ]);
25
25
  /**
26
26
  * 校验用户指定的分身名字:路径分隔符、以 . 开头、尾点/尾空格(win32 会静默
27
- * 吞掉)、win32 保留设备名一律拒绝。
27
+ * 吞掉)、win32 非法字符(`<>:"|?*`,NTFS/Win32 API 拒绝)、win32 保留设备名
28
+ * 一律拒绝。
28
29
  */
29
30
  export function isValidCloneName(name) {
30
31
  if (!name || name.includes('/') || name.includes('\\') || name.startsWith('.'))
31
32
  return false;
32
33
  if (/[\s.]$/.test(name))
33
34
  return false;
35
+ if (/[<>:"|?*]/.test(name))
36
+ return false;
34
37
  if (WIN_RESERVED_NAMES.has(name.toLowerCase()))
35
38
  return false;
36
39
  return true;
@@ -393,12 +396,13 @@ export class CloneSession {
393
396
  }
394
397
  /**
395
398
  * 写配置失败时清理半成品目录。守卫:只删内容全部是本次产物
396
- * (config.yaml / startup-contact.json 及其 .tmp)的目录,混入其他
397
- * 文件时保留现场不动。
399
+ * (config.yaml / startup-contact.json 及其 tmp)的目录,混入其他
400
+ * 文件时保留现场不动。tmp 形态两种都认:atomic-write 现行命名
401
+ * (.{name}.tmp-{pid}-{ts}-{rand})与旧版固定 `.tmp`(升级瞬间崩溃的残留)。
398
402
  */
399
403
  cleanupPartialDir(targetDir) {
400
404
  try {
401
- const allowed = /^(config\.yaml|startup-contact\.json)(\.tmp)?$/;
405
+ const allowed = /^(config\.yaml|startup-contact\.json)(\.tmp)?$|^\.(config\.yaml|startup-contact\.json)\.tmp-\d+-\d+-[a-z0-9]+$/;
402
406
  const entries = fs.readdirSync(targetDir);
403
407
  if (!entries.every((e) => allowed.test(e))) {
404
408
  getLogger().warn(`[clone] target dir has foreign entries, kept: ${targetDir}`);
@@ -11,7 +11,7 @@
11
11
  export declare const DEFAULT_TRUNCATE_SUFFIX = "\u2026\uFF08\u5DF2\u622A\u65AD\uFF09";
12
12
  /**
13
13
  * Max byte budget for a single session `tool_result` event's content when
14
- * replaying an opencode session (L2 pre-fold). Pre-folding here bounds the
14
+ * replaying an opencode session (pre-fold). Pre-folding here bounds the
15
15
  * in-memory `events[]` array and the intermediate card JSON so a single
16
16
  * pathological tool_result never inflates the replay payload to megabytes.
17
17
  */
@@ -11,7 +11,7 @@
11
11
  export const DEFAULT_TRUNCATE_SUFFIX = '…(已截断)';
12
12
  /**
13
13
  * Max byte budget for a single session `tool_result` event's content when
14
- * replaying an opencode session (L2 pre-fold). Pre-folding here bounds the
14
+ * replaying an opencode session (pre-fold). Pre-folding here bounds the
15
15
  * in-memory `events[]` array and the intermediate card JSON so a single
16
16
  * pathological tool_result never inflates the replay payload to megabytes.
17
17
  */
@@ -76,7 +76,7 @@ export function truncate(str, max, options) {
76
76
  if (budget <= 0)
77
77
  return suffix;
78
78
  // Iterate by Unicode codepoint (for...of) so the cut never lands inside a
79
- // surrogate pair (P2-31 anchor: must not emit a lone surrogate).
79
+ // surrogate pair (must not emit a lone surrogate).
80
80
  let end = 0;
81
81
  for (const ch of s) {
82
82
  if (end + ch.length > budget)
@@ -4,11 +4,11 @@
4
4
  * Consolidates the per-reader memory caches that previously hand-rolled the
5
5
  * same "mtime/TTL + entry cap" Map pattern (kimi/pi/opencode session readers).
6
6
  * Semantics follow the kimi implementation:
7
- * - TTL is measured from the cache-write time (P1-17 defect 2), not key mtime.
7
+ * - TTL is measured from the cache-write time (defect 2), not key mtime.
8
8
  * Note: ttlMs = 0 effectively disables caching — `Date.now() - at >= 0` is
9
9
  * always true, so every entry expires immediately.
10
10
  * - A hit refreshes recency (LRU); eviction removes the least recently
11
- * inserted/used entry past `maxEntries` (P1-17 defect 1: unbounded growth).
11
+ * inserted/used entry past `maxEntries` (defect 1: unbounded growth).
12
12
  * The old pi implementation did NOT re-insert on hit (FIFO eviction); the
13
13
  * eviction order for pi therefore changes from FIFO to LRU here.
14
14
  */
@@ -4,11 +4,11 @@
4
4
  * Consolidates the per-reader memory caches that previously hand-rolled the
5
5
  * same "mtime/TTL + entry cap" Map pattern (kimi/pi/opencode session readers).
6
6
  * Semantics follow the kimi implementation:
7
- * - TTL is measured from the cache-write time (P1-17 defect 2), not key mtime.
7
+ * - TTL is measured from the cache-write time (defect 2), not key mtime.
8
8
  * Note: ttlMs = 0 effectively disables caching — `Date.now() - at >= 0` is
9
9
  * always true, so every entry expires immediately.
10
10
  * - A hit refreshes recency (LRU); eviction removes the least recently
11
- * inserted/used entry past `maxEntries` (P1-17 defect 1: unbounded growth).
11
+ * inserted/used entry past `maxEntries` (defect 1: unbounded growth).
12
12
  * The old pi implementation did NOT re-insert on hit (FIFO eviction); the
13
13
  * eviction order for pi therefore changes from FIFO to LRU here.
14
14
  */
@@ -52,32 +52,32 @@ export interface BundledModelInfo {
52
52
  * 依据:codex 运行时对"已配置但文件缺失/不可解析"的目录是硬错误
53
53
  * (core/src/config/mod.rs load_model_catalog ?),绝不会回退内置目录;因此键存在即
54
54
  * catalog 模式,文件缺失自然落入 `codex debug models` 失败路径 → 模型列表回退
55
- * [currentModel](P1-1)。
55
+ * [currentModel]。
56
56
  */
57
57
  export declare function isCodexCatalogMode(codexHome?: string): boolean;
58
58
  /**
59
59
  * 单一解析器:把 `codex debug models`(活动目录)或 `--bundled`(内置目录)的原始 JSON
60
- * 输出解析为模型列表(P2-4 去重)。过滤/排序镜像 codex:
60
+ * 输出解析为模型列表(去重)。过滤/排序镜像 codex:
61
61
  * - show_in_picker = visibility == List(openai_models.rs ModelPreset::from);
62
62
  * - API 模式(lark 用 API key 认证)下 codex filter_by_auth 过滤 supported_in_api=false;
63
63
  * - 按 priority 升序(build_available_models)。
64
- * 档位按目录声明**原样透传**(含 custom/none,P2-5),不按标准枚举过滤、不虚构兜底:
64
+ * 档位按目录声明**原样透传**(含 custom/none),不按标准枚举过滤、不虚构兜底:
65
65
  * 未声明档位 → 空列表(codex with_model 语义:len=0 时用声明 default,都没有则不发
66
- * effort;P1)。空串档位/空串 default 被过滤(P3-9:codex ReasoningEffort::from_str
66
+ * effort;)。空串档位/空串 default 被过滤(codex ReasoningEffort::from_str
67
67
  * 对 "" 硬错误)。supported_in_api===true 过滤等价于 codex 的 chatgpt_mode ||
68
- * supported_in_api:本项目以 API-key 模式运行 codex,chatgpt_mode 恒 false(P3-3)。
68
+ * supported_in_api:本项目以 API-key 模式运行 codex,chatgpt_mode 恒 false。
69
69
  */
70
70
  export declare function parseCodexModelsOutput(stdout: string): BundledModelInfo[];
71
71
  /**
72
72
  * 统一目录入口:catalog 模式(model_catalog_json 已声明,含声明退化 '')跑
73
73
  * `debug models`(无 --bundled),否则(未声明或 config.toml 解析失败)跑
74
- * `debug models --bundled`。注(P3-4):codex 无参 `debug models` 在
74
+ * `debug models --bundled`。注:codex 无参 `debug models` 在
75
75
  * uses_codex_backend()/has_command_auth() 时会请求远端 /models(models-manager
76
76
  * manager.rs / models_endpoint.rs);本项目以 API-key 模式运行(无 ChatGPT/命令
77
77
  * 认证),结果与 bundled 一致,故非 catalog 用 --bundled(已文档化)。
78
78
  * 缓存:键含 binary/home/mode/models.json mtime:size + config.toml mtime:size
79
- * (P3-4/P3-15/P3-5);成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
80
- * 指纹为空(P1-1/P3-10)。
79
+ * 成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
80
+ * 指纹为空。
81
81
  *
82
82
  * 同步路径(卡片构建用):缓存命中直接返回;若有一个后台异步加载
83
83
  * (`loadCodexCatalogModelsAsync` / 启动 warm)在途,则不阻塞事件循环、不重复
@@ -117,7 +117,7 @@ export declare function getReasoningEffortOptions(model: string, codexHome?: str
117
117
  /**
118
118
  * Get default reasoning effort for a model.
119
119
  * 未声明/未知模型 → undefined(codex 语义:ModelPreset.default_reasoning_effort
120
- * unwrap_or(ReasoningEffort::None),即不传 effort;P3-12)。
120
+ * unwrap_or(ReasoningEffort::None),即不传 effort;)。
121
121
  */
122
122
  export declare function getDefaultReasoningEffort(model: string, codexHome?: string): string | undefined;
123
123
  interface LoadCodexConfigOpts {
@@ -48,7 +48,7 @@ const FALLBACK_MODELS = ['o3', 'o4-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini', '
48
48
  const ANTHROPIC_FALLBACK_MODELS = ['claude-sonnet-4-20250514', 'claude-opus-4-20250115'];
49
49
  /** Bundled models from codex binary, 1h TTL is enough (only change on binary upgrade) */
50
50
  const BUNDLED_CACHE_TTL_MS = 60 * 60 * 1000;
51
- /** 失败/空结果负缓存 TTL:避免每次卡片构建同步阻塞最长 8s(P3-5) */
51
+ /** 失败/空结果负缓存 TTL:避免每次卡片构建同步阻塞最长 8s */
52
52
  const NEGATIVE_CACHE_TTL_MS = 30 * 1000;
53
53
  let catalogCache = null;
54
54
  /** In-flight async catalog loads, keyed by cache key — dedup concurrent warm ups. */
@@ -58,7 +58,7 @@ const catalogInFlight = new Map();
58
58
  * 返回语义:
59
59
  * - undefined:config.toml 不存在或**未声明**该键 → 非 catalog 模式;
60
60
  * - ''(空串哨兵):已声明但不可用(值为空串/非字符串)→ 按"catalog 声明退化"处理
61
- * (P2-1,绝不回退 bundled/FALLBACK);
61
+ * (绝不回退 bundled/FALLBACK);
62
62
  * - TOML_PARSE_FAILED_SENTINEL:config.toml 存在但解析失败 → 无法得知是否声明,
63
63
  * 按非 catalog 处理(用户未声明 model_catalog_json 时的默认行为);
64
64
  * - 具体路径:声明有效(含文件缺失,文件缺失由调用方按 active 命令失败处理)。
@@ -87,7 +87,7 @@ function readModelCatalogJsonPath(codexHome) {
87
87
  }
88
88
  catch {
89
89
  // config.toml 解析失败:codex 对非法配置是硬错误;此处无法得知是否声明了
90
- // model_catalog_json,按非 catalog 处理(P3-5),不冒充 catalog 声明退化
90
+ // model_catalog_json,按非 catalog 处理,不冒充 catalog 声明退化
91
91
  return TOML_PARSE_FAILED_SENTINEL;
92
92
  }
93
93
  }
@@ -96,7 +96,7 @@ function readModelCatalogJsonPath(codexHome) {
96
96
  * 依据:codex 运行时对"已配置但文件缺失/不可解析"的目录是硬错误
97
97
  * (core/src/config/mod.rs load_model_catalog ?),绝不会回退内置目录;因此键存在即
98
98
  * catalog 模式,文件缺失自然落入 `codex debug models` 失败路径 → 模型列表回退
99
- * [currentModel](P1-1)。
99
+ * [currentModel]。
100
100
  */
101
101
  export function isCodexCatalogMode(codexHome) {
102
102
  const home = resolveCodexHome(codexHome);
@@ -105,15 +105,15 @@ export function isCodexCatalogMode(codexHome) {
105
105
  }
106
106
  /**
107
107
  * 单一解析器:把 `codex debug models`(活动目录)或 `--bundled`(内置目录)的原始 JSON
108
- * 输出解析为模型列表(P2-4 去重)。过滤/排序镜像 codex:
108
+ * 输出解析为模型列表(去重)。过滤/排序镜像 codex:
109
109
  * - show_in_picker = visibility == List(openai_models.rs ModelPreset::from);
110
110
  * - API 模式(lark 用 API key 认证)下 codex filter_by_auth 过滤 supported_in_api=false;
111
111
  * - 按 priority 升序(build_available_models)。
112
- * 档位按目录声明**原样透传**(含 custom/none,P2-5),不按标准枚举过滤、不虚构兜底:
112
+ * 档位按目录声明**原样透传**(含 custom/none),不按标准枚举过滤、不虚构兜底:
113
113
  * 未声明档位 → 空列表(codex with_model 语义:len=0 时用声明 default,都没有则不发
114
- * effort;P1)。空串档位/空串 default 被过滤(P3-9:codex ReasoningEffort::from_str
114
+ * effort;)。空串档位/空串 default 被过滤(codex ReasoningEffort::from_str
115
115
  * 对 "" 硬错误)。supported_in_api===true 过滤等价于 codex 的 chatgpt_mode ||
116
- * supported_in_api:本项目以 API-key 模式运行 codex,chatgpt_mode 恒 false(P3-3)。
116
+ * supported_in_api:本项目以 API-key 模式运行 codex,chatgpt_mode 恒 false。
117
117
  */
118
118
  export function parseCodexModelsOutput(stdout) {
119
119
  const parsed = JSON.parse(stdout);
@@ -133,7 +133,7 @@ export function parseCodexModelsOutput(stdout) {
133
133
  const declared = m.supported_reasoning_levels
134
134
  .filter((r) => r !== null && typeof r === 'object' && 'effort' in r)
135
135
  .map((r) => String(r.effort))
136
- // P3-9:codex 拒绝空串档位(openai_models.rs from_str "" → Err)
136
+ // codex 拒绝空串档位(openai_models.rs from_str "" → Err)
137
137
  .filter((effort) => effort.length > 0);
138
138
  supportedReasoningLevels.push(...declared);
139
139
  }
@@ -153,13 +153,13 @@ export function parseCodexModelsOutput(stdout) {
153
153
  /**
154
154
  * 统一目录入口:catalog 模式(model_catalog_json 已声明,含声明退化 '')跑
155
155
  * `debug models`(无 --bundled),否则(未声明或 config.toml 解析失败)跑
156
- * `debug models --bundled`。注(P3-4):codex 无参 `debug models` 在
156
+ * `debug models --bundled`。注:codex 无参 `debug models` 在
157
157
  * uses_codex_backend()/has_command_auth() 时会请求远端 /models(models-manager
158
158
  * manager.rs / models_endpoint.rs);本项目以 API-key 模式运行(无 ChatGPT/命令
159
159
  * 认证),结果与 bundled 一致,故非 catalog 用 --bundled(已文档化)。
160
160
  * 缓存:键含 binary/home/mode/models.json mtime:size + config.toml mtime:size
161
- * (P3-4/P3-15/P3-5);成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
162
- * 指纹为空(P1-1/P3-10)。
161
+ * 成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
162
+ * 指纹为空。
163
163
  *
164
164
  * 同步路径(卡片构建用):缓存命中直接返回;若有一个后台异步加载
165
165
  * (`loadCodexCatalogModelsAsync` / 启动 warm)在途,则不阻塞事件循环、不重复
@@ -372,13 +372,13 @@ export function _clearCodexCatalogInFlightForTest() {
372
372
  export function getReasoningEffortOptions(model, codexHome) {
373
373
  const models = getCodexCatalogModels(codexHome);
374
374
  const found = models.find((m) => m.slug === model);
375
- // 未命中(含未知模型,codex fallback 元数据 supported 为空)→ [],不虚构档位(P1)
375
+ // 未命中(含未知模型,codex fallback 元数据 supported 为空)→ [],不虚构档位
376
376
  return found?.supportedReasoningLevels ?? [];
377
377
  }
378
378
  /**
379
379
  * Get default reasoning effort for a model.
380
380
  * 未声明/未知模型 → undefined(codex 语义:ModelPreset.default_reasoning_effort
381
- * unwrap_or(ReasoningEffort::None),即不传 effort;P3-12)。
381
+ * unwrap_or(ReasoningEffort::None),即不传 effort;)。
382
382
  */
383
383
  export function getDefaultReasoningEffort(model, codexHome) {
384
384
  const models = getCodexCatalogModels(codexHome);
@@ -459,7 +459,7 @@ export function loadCodexConfig(opts = {}) {
459
459
  // 活动目录全局:codex 运行时 provider 与模型无绑定(StaticModelsManager),
460
460
  // 每个 provider 下拉都展示目录全部 slug
461
461
  // 活动目录不可用(命令失败/空)时回退 [currentModel]——无目录元数据时档位/能力
462
- // 不可信;禁止泄漏 FALLBACK_MODELS/内置目录。model 键缺失时不得虚构 'o3'(P1-2)。
462
+ // 不可信;禁止泄漏 FALLBACK_MODELS/内置目录。model 键缺失时不得虚构 'o3'。
463
463
  const catalogCurrentModel = modelKeyRaw ?? catalogSlugs[0] ?? '';
464
464
  const providerModelList = catalogSlugs.length > 0 ? catalogSlugs : modelKeyRaw ? [modelKeyRaw] : [];
465
465
  providerModels = {};
@@ -492,7 +492,7 @@ export function loadCodexConfig(opts = {}) {
492
492
  providerEnvKeys[name] = config.env_key;
493
493
  }
494
494
  }
495
- // anthropic 显式配置但未写 env_key 时保留默认键(P3-9:恢复旧版无条件映射的
495
+ // anthropic 显式配置但未写 env_key 时保留默认键(恢复旧版无条件映射的
496
496
  // 配置场景行为;未配置时不再虚构)
497
497
  if (rawProviderNames.includes('anthropic') &&
498
498
  !modelProviders['anthropic']?.env_key &&
@@ -519,7 +519,7 @@ export function loadCodexConfig(opts = {}) {
519
519
  }
520
520
  catch {
521
521
  // config.toml 解析失败(readModelCatalogJsonPath 已把此场景标记为 catalog 声明退化 ''):
522
- // catalogMode 为 true 时绝不回退 bundled/FALLBACK(P2-1),返回空 provider/模型列表;
522
+ // catalogMode 为 true 时绝不回退 bundled/FALLBACK,返回空 provider/模型列表;
523
523
  // 非 catalog 才走 FALLBACK 兜底。
524
524
  if (catalogMode) {
525
525
  return {
@@ -553,7 +553,7 @@ export function loadCodexConfig(opts = {}) {
553
553
  * - If no provider: return all models from all providers (with currentModel prepended if not in any list)
554
554
  */
555
555
  function buildModelOptions(currentModel, currentProvider, provider, providerModels) {
556
- // 不为空 currentModel 虚构默认值(catalog 退化场景由上层显式传入,P1-2);
556
+ // 不为空 currentModel 虚构默认值(catalog 退化场景由上层显式传入);
557
557
  // 空值时直接返回原始列表
558
558
  const cm = currentModel;
559
559
  if (provider && providerModels[provider]) {
@@ -11,6 +11,9 @@ export function resolveConfigDir(configDirArg) {
11
11
  p = os.homedir();
12
12
  else if (p.startsWith('~/'))
13
13
  p = path.join(os.homedir(), p.slice(2));
14
+ // cmd/PowerShell 不展开 ~;`~\` 是 Windows 侧的常见键入形态,与 `~/` 同口径
15
+ else if (p.startsWith('~\\'))
16
+ p = path.join(os.homedir(), p.slice(2));
14
17
  return path.resolve(p);
15
18
  }
16
19
  return path.join(os.homedir(), DEFAULT_DIR);
@@ -35,7 +38,7 @@ export function parseCliArgs(args = process.argv.slice(2)) {
35
38
  result.version = true;
36
39
  }
37
40
  else if (arg === '--config-dir' && i + 1 < args.length) {
38
- // P1-20: peek, don't consume — when the next arg is itself a flag,
41
+ // peek, don't consume — when the next arg is itself a flag,
39
42
  // roll back so it continues parsing (previously --settings/--help
40
43
  // right after --config-dir were silently swallowed).
41
44
  const nextArg = args[i + 1];
@@ -108,7 +111,7 @@ export function printHelp() {
108
111
  */
109
112
  export function printAdvancedHelp() {
110
113
  const configDir = resolveConfigDir(parseCliArgs().configDir);
111
- const configPath = configDir + '/config.yaml';
114
+ const configPath = path.join(configDir, 'config.yaml');
112
115
  const lines = [
113
116
  'lark-remote 高级配置参考(--advanced-help)',
114
117
  '',
@@ -17,7 +17,7 @@ export declare const PI_THINKING_LEVELS: readonly ["off", "minimal", "low", "med
17
17
  /**
18
18
  * Default SIGTERM→SIGKILL grace period (ms). Exported so runners (SpawningRunner)
19
19
  * and config-loading paths (src/index.ts) can reuse the same source of truth
20
- * instead of re-literalizing `5000` (Clean Code P3-1, G25).
20
+ * instead of re-literalizing `5000` (Clean Code G25).
21
21
  */
22
22
  export declare const DEFAULT_STOP_GRACE_MS: 5000;
23
23
  /** Codex app-server turn idle timeout default (minutes). */
@@ -299,7 +299,7 @@ export declare function loadConfig(configPath: string): AppConfig;
299
299
  */
300
300
  export declare function mapAgentKey(key: string): string;
301
301
  /**
302
- * 拒绝原型链危险 key 段(§P1-4)。
302
+ * 拒绝原型链危险 key 段。
303
303
  *
304
304
  * `/config` 直写路径的 key 是用户自由输入:`__proto__.polluted` 会通过
305
305
  * `current['__proto__']` 拿到 Object.prototype 并写入,污染整个进程且不可恢复;
@@ -324,7 +324,7 @@ export declare function setConfigValue(configPath: string, config: AppConfig, ke
324
324
  * Set multiple nested values in config, write back to YAML file once.
325
325
  *
326
326
  * 在 draft 副本上操作(structuredClone),验证通过后才写盘,避免校验失败时
327
- * 内存中的 config 已被污染(P0 atomicity)。
327
+ * 内存中的 config 已被污染(atomicity)。
328
328
  */
329
329
  export declare function setConfigValues(configPath: string, config: AppConfig, updates: Record<string, string | undefined>): AppConfig;
330
330
  /** Type helpers for agent config (used by getAgentConfig). */
@@ -45,7 +45,7 @@ const DEFAULTS = {
45
45
  /**
46
46
  * Default SIGTERM→SIGKILL grace period (ms). Exported so runners (SpawningRunner)
47
47
  * and config-loading paths (src/index.ts) can reuse the same source of truth
48
- * instead of re-literalizing `5000` (Clean Code P3-1, G25).
48
+ * instead of re-literalizing `5000` (Clean Code G25).
49
49
  */
50
50
  export const DEFAULT_STOP_GRACE_MS = DEFAULTS.STOP_GRACE_MS;
51
51
  /** Codex app-server turn idle timeout default (minutes). */
@@ -115,9 +115,9 @@ export const CodexConfigSchema = z.object({
115
115
  /**
116
116
  * Reasoning effort level。codex 标准档位 minimal/low/medium/high/xhigh/max/ultra
117
117
  * 之外还接受 none 与模型自定义值(ReasoningEffort::Custom),目录声明什么就存什么
118
- * (P2-5),故用字符串而非枚举。
118
+ * 故用字符串而非枚举。
119
119
  */
120
- // codex 拒绝空串档位(ReasoningEffort::from_str "" → Err,P3-3)
120
+ // codex 拒绝空串档位(ReasoningEffort::from_str "" → Err)
121
121
  reasoningEffort: z.string().min(1).optional(),
122
122
  /** Approval policy (Codex 官方 AskForApproval 标准值). 默认 on-request。 */
123
123
  approvalPolicy: z.enum(['untrusted', 'on-request', 'never']).default('on-request'),
@@ -363,7 +363,7 @@ function coerceValue(raw) {
363
363
  return raw;
364
364
  }
365
365
  /**
366
- * 拒绝原型链危险 key 段(§P1-4)。
366
+ * 拒绝原型链危险 key 段。
367
367
  *
368
368
  * `/config` 直写路径的 key 是用户自由输入:`__proto__.polluted` 会通过
369
369
  * `current['__proto__']` 拿到 Object.prototype 并写入,污染整个进程且不可恢复;
@@ -423,7 +423,7 @@ export function setConfigValue(configPath, config, key, value) {
423
423
  * Set multiple nested values in config, write back to YAML file once.
424
424
  *
425
425
  * 在 draft 副本上操作(structuredClone),验证通过后才写盘,避免校验失败时
426
- * 内存中的 config 已被污染(P0 atomicity)。
426
+ * 内存中的 config 已被污染(atomicity)。
427
427
  */
428
428
  export function setConfigValues(configPath, config, updates) {
429
429
  const draft = structuredClone(config);
@@ -472,7 +472,7 @@ function renderConfigPatch(configPath, updates, validated) {
472
472
  doc.deleteIn(parts);
473
473
  continue;
474
474
  }
475
- // 保存载荷里只写"校验结果中存活下来"的键:schema 不认识的键(§P2-24 的 foo.bar)
475
+ // 保存载荷里只写"校验结果中存活下来"的键:schema 不认识的键(foo.bar)
476
476
  // 会被 Zod 剥掉,跟着打在 Document 上就是把垃圾永久写进用户配置文件。
477
477
  // 磁盘上原有的未知键不受影响 —— 那是用户手写的,本函数不碰。
478
478
  const accepted = getConfigValue(validated, parts.join('.'));
@@ -22,7 +22,7 @@ const FALLBACK_PROVIDERS = Object.keys(FALLBACK_MODELS);
22
22
  const OPENCODE_CONFIG_CACHE_TTL_MS = 60_000;
23
23
  /**
24
24
  * 失败负缓存 TTL:命令失败后 30s 内直接用 fallback,避免每次 /config 卡片构建
25
- * 都同步阻塞事件循环最长 10s(§P1-8,与 codex-config 负缓存对齐)。
25
+ * 都同步阻塞事件循环最长 10s(与 codex-config 负缓存对齐)。
26
26
  */
27
27
  const OPENCODE_NEGATIVE_CACHE_TTL_MS = 30_000;
28
28
  let opencodeConfigCache = null;
@@ -57,7 +57,7 @@ function buildFallbackResult() {
57
57
  */
58
58
  export function loadOpencodeConfig() {
59
59
  const now = Date.now();
60
- // 正缓存命中:TTL 内不重复 execSync(P1-8)
60
+ // 正缓存命中:TTL 内不重复 execSync
61
61
  if (opencodeConfigCache && now - opencodeConfigCache.timestamp < OPENCODE_CONFIG_CACHE_TTL_MS) {
62
62
  return opencodeConfigCache.result;
63
63
  }
@@ -74,7 +74,7 @@ export function loadOpencodeConfig() {
74
74
  // 执行 opencode models --verbose 获取所有模型信息
75
75
  const res = spawnProcessSync(resolved.file, ['models', '--verbose'], {
76
76
  encoding: 'utf-8',
77
- // P1-8:30s → 10s,与 codex/kimi 对齐;maxBuffer 防模型清单 ENOBUFS 隐性截断
77
+ // 30s → 10s,与 codex/kimi 对齐;maxBuffer 防模型清单 ENOBUFS 隐性截断
78
78
  timeout: 10000,
79
79
  maxBuffer: 64 * 1024 * 1024,
80
80
  });
@@ -73,7 +73,7 @@ export interface InboundMediaPayload {
73
73
  }
74
74
  /**
75
75
  * 媒体消息到达的轻量描述(未下载):供上层先做 owner/配置闸门,
76
- * 通过后再调 downloadInboundMedia —— 下载发生在认证之后(P1 review 修复)。
76
+ * 通过后再调 downloadInboundMedia —— 下载发生在认证之后(修复)。
77
77
  */
78
78
  /** SDK 给出的可下载资源种类(`resources[].type`)。 */
79
79
  export type InboundResourceKind = 'image' | 'file' | 'audio' | 'video' | 'sticker';
@@ -180,14 +180,14 @@ export declare class FeishuConnector {
180
180
  */
181
181
  private checkUploadable;
182
182
  /**
183
- * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁,P2-18)
183
+ * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁)
184
184
  * → 发送媒体消息。上传端点 / form 字段 / 响应 key / msg_type 的差异由调用方
185
185
  * 以 uploadUrl + buildForm + keyField + msgType 表达;timeout、noKeepAliveAgent
186
186
  * (Bun keep-alive 修复)与错误包装在此单源。label 是对外错误前缀与日志
187
187
  * 标签('sendFile' | 'sendImage')。
188
188
  */
189
189
  private uploadAndSendMedia;
190
- /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁(P2-18)。 */
190
+ /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁。 */
191
191
  private uploadWithToken;
192
192
  /**
193
193
  * Upload a file to Feishu and send it to the specified chat.
@@ -201,13 +201,13 @@ export declare class FeishuConnector {
201
201
  */
202
202
  sendImage(chatId: string, filePath: string): Promise<string>;
203
203
  /**
204
- * Cached tenant_access_token for sendFile (P2-18). The token is valid ~2h;
204
+ * Cached tenant_access_token for sendFile. The token is valid ~2h;
205
205
  * fetching it on every file send wastes a round-trip and an unguarded fetch
206
206
  * could return data.code != 0 with an undefined token. Cache with expiry and
207
207
  * validate the response code.
208
208
  *
209
209
  * 缓存的两条失效路径:到期(下面 expire)与被服务端判 token 无效
210
- * (invalidateTenantToken,见 §B13)。
210
+ * (invalidateTenantToken)。
211
211
  */
212
212
  private cachedToken;
213
213
  private cachedTokenExpireAt;
@@ -111,7 +111,7 @@ function withTimeout(promise, ms, label, onLateSettle) {
111
111
  });
112
112
  }
113
113
  /**
114
- * 判断 send 失败是否值得重试一次(§9.5 限流重试口径,§P1-3 修正)。
114
+ * 判断 send 失败是否值得重试一次(§9.5 限流重试口径)。
115
115
  *
116
116
  * @larksuite/channel@0.7.1 的 classifyError 把飞书业务码 99991400/99991401
117
117
  * (频率控制)归类为 `code='permission_denied'`(SDK 源码实证),并保留原始
@@ -271,6 +271,19 @@ export class FeishuConnector {
271
271
  ttl: DEDUP_TTL_MS,
272
272
  maxEntries: 1000,
273
273
  },
274
+ // 关掉 SDK 的入站去抖合批(`ChatPipeline` + `mergeBatch`),delayMs=0 让每条
275
+ // 消息单独成批、保留自己的 messageId(`enabled` 仍为 true,per-chat 串行与
276
+ // 卡片动作同通道不受影响)。
277
+ //
278
+ // 背景:SDK 默认 600ms 内把同一会话的多条消息合并成一条,`mergeBatch` 只保留
279
+ // **最后一条**的 `messageId` 却把多条消息的 `resources` 求并集。飞书客户端
280
+ // "一次选多图/多文件"会背靠背秒发成多条独立消息,被合并后下载侧拿同一个
281
+ // `messageId` 下所有 `fileKey`,除最后一条外全部报错 234003(HTTP 400):
282
+ // 表现为"多图只有 1 张保存成功,其余下载失败"。合批本应由本项目自己的
283
+ // 700ms 装配器(`InboundTurnAssembler`)负责,SDK 这层是叠加的第二次合批。
284
+ batch: {
285
+ text: { delayMs: 0 },
286
+ },
274
287
  },
275
288
  });
276
289
  // 卡片 patch 的统一出口(观测 + 重试):
@@ -305,7 +318,7 @@ export class FeishuConnector {
305
318
  // SDK 只为 image/file/audio/video/media/sticker 五类产出非空 resources,
306
319
  // 枚举 msg_type 必然漏(2026-09-15 事故:mp4 被当文本转发给 agent)。
307
320
  // 一条消息可以**同时**产出资源事件与文本事件(post 既有文字又有图),
308
- // 由 B3 的装配器合并成同一个 turn。
321
+ // 由的装配器合并成同一个 turn。
309
322
  const hasResources = msg.resources.length > 0;
310
323
  if (hasResources) {
311
324
  // 只上报「媒体到达」,不在此下载:owner/配置闸门在 index.ts 里先执行,
@@ -489,7 +502,7 @@ export class FeishuConnector {
489
502
  return result.messageId;
490
503
  }
491
504
  catch (retryErr) {
492
- // P2-17: throw the RETRY error, not the outer `err`. Previously
505
+ // throw the RETRY error, not the outer `err`. Previously
493
506
  // `throw err` here re-threw the first (rate-limit) failure, hiding
494
507
  // the real reason the retry attempt failed (e.g. auth invalid).
495
508
  getLogger().warn('[feishu] retry after rate limit still failed');
@@ -595,7 +608,7 @@ export class FeishuConnector {
595
608
  }
596
609
  }
597
610
  /**
598
- * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁,P2-18)
611
+ * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁)
599
612
  * → 发送媒体消息。上传端点 / form 字段 / 响应 key / msg_type 的差异由调用方
600
613
  * 以 uploadUrl + buildForm + keyField + msgType 表达;timeout、noKeepAliveAgent
601
614
  * (Bun keep-alive 修复)与错误包装在此单源。label 是对外错误前缀与日志
@@ -627,13 +640,13 @@ export class FeishuConnector {
627
640
  throw new Error(`${label} failed: ${errorInfo}`, { cause: err });
628
641
  }
629
642
  }
630
- /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁(P2-18)。 */
643
+ /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁。 */
631
644
  async uploadWithToken(accessToken, chatId, filePath, uploadUrl, buildForm, keyField, msgType) {
632
645
  let fileStream = null;
633
646
  try {
634
647
  fileStream = fs.createReadStream(filePath);
635
648
  const form = buildForm(fileStream);
636
- // P2-18: timeout 120s(大文件慢链路);httpsAgent: noKeepAliveAgent 防止
649
+ // timeout 120s(大文件慢链路);httpsAgent: noKeepAliveAgent 防止
637
650
  // Bun 复用已被服务端 RST 的 keep-alive socket(ECONNRESET after ~30s)。
638
651
  const uploadResp = await axios.post(uploadUrl, form, {
639
652
  headers: {
@@ -647,7 +660,7 @@ export class FeishuConnector {
647
660
  throw new FeishuBusinessError(`Upload failed: ${uploadResp.data.msg}`, uploadResp.data.code);
648
661
  }
649
662
  const mediaKey = uploadResp.data.data[keyField];
650
- // P2-18: timeout 30s;noKeepAlive 同上。
663
+ // timeout 30s;noKeepAlive 同上。
651
664
  const sendResp = await axios.post('https://open.feishu.cn/open-apis/im/v1/messages', {
652
665
  receive_id: chatId,
653
666
  msg_type: msgType,
@@ -667,7 +680,7 @@ export class FeishuConnector {
667
680
  return sendResp.data.data.message_id;
668
681
  }
669
682
  catch (err) {
670
- // P2-18: destroy the read stream on failure so the fd is released.
683
+ // destroy the read stream on failure so the fd is released.
671
684
  if (fileStream) {
672
685
  try {
673
686
  fileStream.destroy();
@@ -708,13 +721,13 @@ export class FeishuConnector {
708
721
  }, 'image_key', 'image');
709
722
  }
710
723
  /**
711
- * Cached tenant_access_token for sendFile (P2-18). The token is valid ~2h;
724
+ * Cached tenant_access_token for sendFile. The token is valid ~2h;
712
725
  * fetching it on every file send wastes a round-trip and an unguarded fetch
713
726
  * could return data.code != 0 with an undefined token. Cache with expiry and
714
727
  * validate the response code.
715
728
  *
716
729
  * 缓存的两条失效路径:到期(下面 expire)与被服务端判 token 无效
717
- * (invalidateTenantToken,见 §B13)。
730
+ * (invalidateTenantToken)。
718
731
  */
719
732
  cachedToken = null;
720
733
  cachedTokenExpireAt = 0;
@@ -736,7 +749,7 @@ export class FeishuConnector {
736
749
  this.cachedTokenExpireAt = 0;
737
750
  }
738
751
  async fetchTenantAccessToken() {
739
- // P2-18: timeout 30s on the token request.
752
+ // timeout 30s on the token request.
740
753
  // httpsAgent: noKeepAlive — same Bun keep-alive fix as upload above.
741
754
  const tokenResp = await axios.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
742
755
  app_id: this.appId,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
3
3
  *
4
- * 设计文档早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
4
+ * 早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
5
5
  * 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
6
6
  * 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
7
7
  * 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
3
3
  *
4
- * 设计文档早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
4
+ * 早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
5
5
  * 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
6
6
  * 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
7
7
  * 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 入站消息结构占位符识别与剥离(纯函数,无 IO,便于表驱动测试)。
3
3
  *
4
- * 背景(P0 事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
4
+ * 背景(事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
5
5
  * 富文本(`post`)里的图片是 `![image](img_v3_…)` —— 首字符恰好是 `!`,于是被
6
6
  * `src/router/index.ts` 的裸前缀判定当成 bash 命令执行了整段消息。
7
7
  *
@@ -12,7 +12,7 @@
12
12
  * 取舍(有意为之):剥离对所有消息生效,所以用户在**纯文本**消息里手打
13
13
  * `![image](…)` / `<file key="…"/>` 这类字面量也会被当成占位符剥掉。代价是极少数
14
14
  * 「讨论本工具自身语法」的消息会丢片段;换来的是任何入口都不可能把占位符当命令
15
- * (P0 事故正是 `![image](img_v3_…)` 的首字符 `!` 命中 bash 判定)。
15
+ * (事故正是 `![image](img_v3_…)` 的首字符 `!` 命中 bash 判定)。
16
16
  */
17
17
  export type PlaceholderKind = 'image' | 'file' | 'video' | 'audio' | 'sticker' | 'share' | 'location' | 'folder' | 'vote' | 'todo' | 'meeting' | 'calendar' | 'hongbao' | 'forwarded' | 'unsupported' | 'unknown';
18
18
  export interface StripResult {