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.
- package/CHANGELOG.md +18 -0
- package/dist/bridge/approval-coordinator.d.ts +1 -1
- package/dist/bridge/approval-coordinator.js +3 -3
- package/dist/bridge/inbound-media.js +10 -7
- package/dist/bridge/index.d.ts +9 -9
- package/dist/bridge/index.js +41 -41
- package/dist/bridge/queue-manager.d.ts +12 -12
- package/dist/bridge/queue-manager.js +19 -19
- package/dist/card/approval-render.d.ts +1 -1
- package/dist/card/approval-render.js +6 -6
- package/dist/card/bash-card-session.d.ts +3 -3
- package/dist/card/bash-card-session.js +3 -3
- package/dist/card/bash-renderer.js +1 -1
- package/dist/card/card-session.d.ts +4 -4
- package/dist/card/card-session.js +6 -6
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.d.ts +1 -1
- package/dist/card/run-renderer.js +14 -14
- package/dist/card/run-state.d.ts +2 -2
- package/dist/card/run-state.js +3 -3
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +6 -6
- package/dist/clone.d.ts +5 -3
- package/dist/clone.js +8 -4
- package/dist/common/truncate.d.ts +1 -1
- package/dist/common/truncate.js +2 -2
- package/dist/common/ttl-cache.d.ts +2 -2
- package/dist/common/ttl-cache.js +2 -2
- package/dist/config/codex-config.d.ts +9 -9
- package/dist/config/codex-config.js +18 -18
- package/dist/config/dir.js +5 -2
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +6 -6
- package/dist/config/opencode-config.js +3 -3
- package/dist/connector/index.d.ts +5 -5
- package/dist/connector/index.js +24 -11
- package/dist/inbound/event.d.ts +1 -1
- package/dist/inbound/event.js +1 -1
- package/dist/inbound/placeholder.d.ts +2 -2
- package/dist/inbound/placeholder.js +2 -2
- package/dist/index.js +9 -9
- package/dist/instance-lock.js +4 -3
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.js +4 -4
- package/dist/persistence/atomic-write.d.ts +1 -1
- package/dist/persistence/atomic-write.js +6 -2
- package/dist/platform/agent-stopper.d.ts +1 -1
- package/dist/platform/identity.d.ts +7 -1
- package/dist/platform/identity.js +2 -2
- package/dist/platform/path.d.ts +6 -0
- package/dist/platform/path.js +14 -8
- package/dist/platform/select.d.ts +1 -1
- package/dist/platform/select.js +1 -1
- package/dist/platform/shell.d.ts +2 -3
- package/dist/platform/spawn.d.ts +2 -2
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/terminator-posix.js +1 -1
- package/dist/platform/terminator-win32.js +2 -2
- package/dist/platform/terminator.d.ts +3 -3
- package/dist/platform/terminator.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/restart.d.ts +1 -1
- package/dist/restart.js +5 -3
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +2 -2
- package/dist/router/config/opencode.js +1 -1
- package/dist/router/index.d.ts +9 -9
- package/dist/router/index.js +34 -34
- package/dist/router/order-exec-dispatch.js +2 -2
- package/dist/runner/bash/runner.d.ts +2 -2
- package/dist/runner/bash/runner.js +6 -6
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/claude/session.d.ts +2 -2
- package/dist/runner/claude/session.js +14 -14
- package/dist/runner/codex/app-server/runner.js +3 -3
- package/dist/runner/codex/app-server/translator.d.ts +2 -2
- package/dist/runner/codex/app-server/translator.js +5 -5
- package/dist/runner/common/acp/acp-approval.d.ts +2 -2
- package/dist/runner/common/acp/acp-approval.js +3 -3
- package/dist/runner/common/acp/base-acp-runner.js +1 -1
- package/dist/runner/common/acp/base-translator.d.ts +3 -3
- package/dist/runner/common/acp/base-translator.js +4 -4
- package/dist/runner/common/acp/protocol-types.js +1 -1
- package/dist/runner/common/connection-based-runner.d.ts +1 -1
- package/dist/runner/common/connection-based-runner.js +2 -2
- package/dist/runner/common/jsonl-stream.d.ts +2 -2
- package/dist/runner/common/jsonl-stream.js +12 -12
- package/dist/runner/common/jsonrpc/client.d.ts +1 -1
- package/dist/runner/common/jsonrpc/client.js +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +2 -2
- package/dist/runner/common/spawning-runner.d.ts +21 -8
- package/dist/runner/common/spawning-runner.js +41 -15
- package/dist/runner/dsh/runner.js +2 -2
- package/dist/runner/kimi/acp/runner.d.ts +3 -3
- package/dist/runner/kimi/acp/runner.js +9 -9
- package/dist/runner/kimi/acp/translator.d.ts +2 -2
- package/dist/runner/kimi/acp/translator.js +4 -4
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +3 -3
- package/dist/runner/opencode/acp/translator.js +1 -1
- package/dist/runner/pi/rpc/runner.js +2 -2
- package/dist/runner/pi/rpc/translator.d.ts +1 -1
- package/dist/runner/pi/rpc/translator.js +4 -4
- package/dist/runner/types.d.ts +1 -1
- package/dist/session/claude/session-index.d.ts +12 -0
- package/dist/session/claude/session-index.js +20 -5
- package/dist/session/claude/sessions.js +10 -10
- package/dist/session/codex/rollout-reader.d.ts +4 -4
- package/dist/session/codex/rollout-reader.js +12 -12
- package/dist/session/common/two-pass.js +2 -2
- package/dist/session/common/usage-accumulator.d.ts +2 -2
- package/dist/session/common/usage-accumulator.js +2 -2
- package/dist/session/dsh/sessions.js +2 -2
- package/dist/session/kimi/sessions.d.ts +2 -2
- package/dist/session/kimi/sessions.js +11 -11
- package/dist/session/opencode/sessions.d.ts +3 -3
- package/dist/session/opencode/sessions.js +12 -10
- package/dist/session/pi/sessions.js +13 -12
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- 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 及其
|
|
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 及其
|
|
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 (
|
|
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
|
*/
|
package/dist/common/truncate.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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 (
|
|
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` (
|
|
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
|
*/
|
package/dist/common/ttl-cache.js
CHANGED
|
@@ -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 (
|
|
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` (
|
|
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]
|
|
55
|
+
* [currentModel]。
|
|
56
56
|
*/
|
|
57
57
|
export declare function isCodexCatalogMode(codexHome?: string): boolean;
|
|
58
58
|
/**
|
|
59
59
|
* 单一解析器:把 `codex debug models`(活动目录)或 `--bundled`(内置目录)的原始 JSON
|
|
60
|
-
*
|
|
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
|
|
64
|
+
* 档位按目录声明**原样透传**(含 custom/none),不按标准枚举过滤、不虚构兜底:
|
|
65
65
|
* 未声明档位 → 空列表(codex with_model 语义:len=0 时用声明 default,都没有则不发
|
|
66
|
-
* effort
|
|
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
|
|
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
|
|
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
|
-
*
|
|
80
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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]
|
|
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
|
-
*
|
|
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
|
|
112
|
+
* 档位按目录声明**原样透传**(含 custom/none),不按标准枚举过滤、不虚构兜底:
|
|
113
113
|
* 未声明档位 → 空列表(codex with_model 语义:len=0 时用声明 default,都没有则不发
|
|
114
|
-
* effort
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
162
|
-
*
|
|
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 为空)→ []
|
|
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
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
556
|
+
// 不为空 currentModel 虚构默认值(catalog 退化场景由上层显式传入);
|
|
557
557
|
// 空值时直接返回原始列表
|
|
558
558
|
const cm = currentModel;
|
|
559
559
|
if (provider && providerModels[provider]) {
|
package/dist/config/dir.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
114
|
+
const configPath = path.join(configDir, 'config.yaml');
|
|
112
115
|
const lines = [
|
|
113
116
|
'lark-remote 高级配置参考(--advanced-help)',
|
|
114
117
|
'',
|
package/dist/config/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 已被污染(
|
|
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). */
|
package/dist/config/index.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
118
|
+
* 故用字符串而非枚举。
|
|
119
119
|
*/
|
|
120
|
-
// codex 拒绝空串档位(ReasoningEffort::from_str "" → Err
|
|
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
|
|
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 已被污染(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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 ——
|
|
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 →
|
|
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
|
-
/** 单次「上传 +
|
|
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
|
|
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
|
|
210
|
+
* (invalidateTenantToken)。
|
|
211
211
|
*/
|
|
212
212
|
private cachedToken;
|
|
213
213
|
private cachedTokenExpireAt;
|
package/dist/connector/index.js
CHANGED
|
@@ -111,7 +111,7 @@ function withTimeout(promise, ms, label, onLateSettle) {
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
|
-
* 判断 send 失败是否值得重试一次(§9.5
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 →
|
|
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
|
-
/** 单次「上传 +
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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,
|
package/dist/inbound/event.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
|
|
5
5
|
* 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
|
|
6
6
|
* 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
|
|
7
7
|
* 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
|
package/dist/inbound/event.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
4
|
+
* 背景(事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
|
|
5
5
|
* 富文本(`post`)里的图片是 `` —— 首字符恰好是 `!`,于是被
|
|
6
6
|
* `src/router/index.ts` 的裸前缀判定当成 bash 命令执行了整段消息。
|
|
7
7
|
*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 取舍(有意为之):剥离对所有消息生效,所以用户在**纯文本**消息里手打
|
|
13
13
|
* `` / `<file key="…"/>` 这类字面量也会被当成占位符剥掉。代价是极少数
|
|
14
14
|
* 「讨论本工具自身语法」的消息会丢片段;换来的是任何入口都不可能把占位符当命令
|
|
15
|
-
*
|
|
15
|
+
* (事故正是 `` 的首字符 `!` 命中 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 {
|