lark-remote 0.4.4 → 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 +28 -0
- package/dist/binder.d.ts +11 -10
- package/dist/binder.js +17 -16
- package/dist/bridge/approval-coordinator.d.ts +1 -1
- package/dist/bridge/approval-coordinator.js +3 -3
- package/dist/bridge/inbound-media.d.ts +1 -27
- package/dist/bridge/inbound-media.js +23 -21
- package/dist/bridge/index.d.ts +57 -57
- package/dist/bridge/index.js +136 -129
- package/dist/bridge/queue-manager.d.ts +45 -38
- package/dist/bridge/queue-manager.js +136 -136
- 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 +5 -5
- package/dist/card/card-session.d.ts +4 -4
- package/dist/card/card-session.js +6 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.d.ts +1 -1
- package/dist/card/run-renderer.js +22 -22
- 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/cli.js +5 -6
- package/dist/clone.d.ts +6 -9
- package/dist/clone.js +12 -8
- package/dist/common/clamp.d.ts +2 -0
- package/dist/common/clamp.js +5 -0
- package/dist/common/fs.d.ts +4 -3
- package/dist/common/fs.js +5 -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.d.ts +4 -5
- package/dist/config/dir.js +12 -10
- 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/config/wizard.js +2 -2
- package/dist/connector/index.d.ts +5 -7
- package/dist/connector/index.js +29 -16
- 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/inbound/receipt.d.ts +1 -3
- package/dist/inbound/receipt.js +2 -2
- package/dist/inbound/turn-assembler.d.ts +1 -3
- package/dist/inbound/turn-assembler.js +2 -2
- package/dist/index.js +38 -30
- package/dist/instance-lock.js +8 -7
- 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 +9 -5
- package/dist/platform/agent-stopper.d.ts +2 -2
- 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 +6 -6
- package/dist/platform/terminator-win32.js +3 -3
- 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/card-helpers.d.ts +1 -3
- package/dist/router/card-helpers.js +2 -2
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +3 -3
- package/dist/router/config/common/model-patch.d.ts +1 -2
- package/dist/router/config/common/model-patch.js +1 -2
- package/dist/router/config/opencode.js +2 -2
- package/dist/router/config/pi.js +1 -1
- package/dist/router/index.d.ts +30 -30
- package/dist/router/index.js +103 -84
- package/dist/router/order-exec-dispatch.d.ts +3 -10
- package/dist/router/order-exec-dispatch.js +5 -5
- package/dist/router/{utils.d.ts → usage-format.d.ts} +2 -4
- package/dist/router/{utils.js → usage-format.js} +3 -8
- package/dist/runner/bash/runner.d.ts +2 -2
- package/dist/runner/bash/runner.js +6 -6
- package/dist/runner/claude/runner.d.ts +1 -1
- package/dist/runner/claude/runner.js +2 -3
- package/dist/runner/claude/session.d.ts +4 -6
- package/dist/runner/claude/session.js +21 -23
- 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/{protocol-helpers.d.ts → acp-approval.d.ts} +4 -10
- package/dist/runner/common/acp/{protocol-helpers.js → acp-approval.js} +7 -6
- package/dist/runner/common/acp/base-acp-runner.d.ts +1 -1
- package/dist/runner/common/acp/base-acp-runner.js +2 -2
- 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 +5 -5
- package/dist/runner/common/connection-based-runner.js +5 -5
- 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 +13 -13
- package/dist/runner/common/jsonrpc/connection-manager.js +37 -37
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +2 -2
- package/dist/runner/common/{usage.d.ts → result-usage.d.ts} +1 -1
- package/dist/runner/common/{usage.js → result-usage.js} +2 -2
- package/dist/runner/common/spawning-runner.d.ts +23 -10
- package/dist/runner/common/spawning-runner.js +48 -22
- package/dist/runner/common/{runner-utils.d.ts → synthetic-events.d.ts} +4 -4
- package/dist/runner/common/{runner-utils.js → synthetic-events.js} +5 -5
- package/dist/runner/dsh/client.d.ts +1 -1
- package/dist/runner/dsh/client.js +1 -1
- package/dist/runner/dsh/runner.js +3 -3
- package/dist/runner/kimi/acp/runner.d.ts +4 -4
- package/dist/runner/kimi/acp/runner.js +10 -10
- package/dist/runner/kimi/acp/translator.d.ts +2 -2
- package/dist/runner/kimi/acp/translator.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +4 -4
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/opencode/acp/translator.js +2 -2
- package/dist/runner/pi/rpc/runner.d.ts +1 -1
- package/dist/runner/pi/rpc/runner.js +2 -2
- package/dist/runner/pi/rpc/translator.d.ts +1 -2
- package/dist/runner/pi/rpc/translator.js +4 -6
- package/dist/runner/registry.d.ts +3 -3
- package/dist/runner/registry.js +3 -3
- package/dist/runner/types.d.ts +1 -1
- package/dist/session/claude/session-index.d.ts +13 -1
- package/dist/session/claude/session-index.js +21 -6
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/session-reader.js +1 -1
- 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 +14 -14
- 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.d.ts +1 -1
- package/dist/session/dsh/sessions.js +3 -3
- package/dist/session/kimi/sessions.d.ts +2 -6
- package/dist/session/kimi/sessions.js +13 -19
- package/dist/session/opencode/sessions.d.ts +3 -3
- package/dist/session/opencode/sessions.js +15 -13
- package/dist/session/pi/sessions.js +13 -12
- package/dist/session/registry.d.ts +1 -1
- package/dist/session/registry.js +1 -1
- package/dist/session/session-store.d.ts +3 -4
- package/dist/session/session-store.js +3 -5
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/version-check.d.ts +5 -3
- package/dist/update/version-check.js +10 -8
- package/dist/workspace/index.d.ts +4 -4
- package/dist/workspace/index.js +5 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.0] - 2026-10-07
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- **CI 增加 Windows 全量验证**:新增 `windows-latest` job(install → typecheck → test → build → `--version`/`--help` 冒烟)。此前只有 ubuntu job,被平台门控跳过的用例在 CI 里是盲区,win32 独有回归只能靠本地发现
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
|
|
16
|
+
- **一次发多图/多文件不再只保存 1 个**:飞书客户端「一次选多图/多文件」会背靠背秒发成多条独立消息,SDK 层 600ms 去抖把它们合并成一条——只保留最后一条的 `messageId`,却把多条消息的附件求并集。下载侧于是拿同一个 `messageId` 去下所有文件,除最后一条外全部报 234003(HTTP 400)。现关掉 SDK 这层合批(`chatQueue` 串行与卡片动作通道保留),合批交回项目自身的合并窗口
|
|
17
|
+
- **Windows 上会话偶尔「消失」**:按工作目录查会话时,大小写/分隔符/尾斜杠的归一不统一,`C:\Work` 与 `c:/work/` 被当成两个目录,`/resume` 列表会漏列。现统一走同一套判等键
|
|
18
|
+
- **Windows 上含 `<>:"|?*` 或尾点的文件名落盘失败**:入站附件名现在会剥掉 win32 非法字符与尾部点/空格(win32 会静默剥离它们,导致落盘名与记录名不一致)
|
|
19
|
+
- **Windows 上可能误杀其他工作目录的 agent**:pid 文件名此前只按消毒后的目录名区分,`/w/a+b` 与 `/w/a_b` 会撞同一个文件,启动时的孤儿回收可能杀掉对方工作目录里存活的 agent。现后缀掺入原始目录的哈希
|
|
20
|
+
- **`~` 开头的配置目录在 Windows 上不展开**:`~\` 形式此前不识别
|
|
21
|
+
|
|
22
|
+
### 变更
|
|
23
|
+
|
|
24
|
+
- **注释与测试命名清理**:去掉源码、测试注释与用例名中指向未发布内部文档的引用(评审条目号、场景号、轮次号、文件名前缀),让公开仓库里的引用都能落地。无行为变化
|
|
25
|
+
|
|
26
|
+
## [0.4.5] - 2026-10-07
|
|
27
|
+
|
|
28
|
+
### 修复
|
|
29
|
+
|
|
30
|
+
- **消息不再漂移到新会话**:紧跟在文字消息之后点击「新会话」「切换工作目录」「继续会话」等会变更会话的卡片按钮时,此前消息可能落到新会话里执行。现在这类卡片动作会先等消息合并窗口落地(消息仍绑定旧会话/旧目录),再执行动作;同时入队时刻固定会话代际,堵住「已开新会话后又被旧消息把旧会话复活」的漏判
|
|
31
|
+
|
|
32
|
+
### 变更
|
|
33
|
+
|
|
34
|
+
- **内部清理重构**:删除生产死代码与重复测试脚手架,统一内部命名,无行为变化
|
|
35
|
+
|
|
8
36
|
## [0.4.4] - 2026-10-04
|
|
9
37
|
|
|
10
38
|
### 修复
|
package/dist/binder.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ type BindDecision = {
|
|
|
8
8
|
kind: 'bind_success';
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* Owner 绑定器:首次私聊发送任意消息完成"认领",此后仅该
|
|
11
|
+
* Owner 绑定器:首次私聊发送任意消息完成"认领",此后仅该 userId 可用。
|
|
12
12
|
*
|
|
13
13
|
* 设计依据:lark-remote 进程持有用户本机完整权限,唯一合法主体是 owner 本人。
|
|
14
14
|
* 飞书自建应用的私聊入口本身就是 owner 掌控的(应用只有 owner 能看到/使用,
|
|
@@ -17,30 +17,31 @@ type BindDecision = {
|
|
|
17
17
|
*
|
|
18
18
|
* 状态机:
|
|
19
19
|
* - 未绑定:第一条私聊消息(任意内容)即完成绑定 -> 写入 startup-contact.json。
|
|
20
|
-
* - 已绑定:仅
|
|
20
|
+
* - 已绑定:仅 userId === bound.userId 的消息放行;其余静默丢弃(计数 + debug)。
|
|
21
21
|
*
|
|
22
|
-
* cardAction 同样校验 operator.openId
|
|
22
|
+
* cardAction 同样校验 operator.openId(飞书 SDK 字段,值即 userId)=== bound.userId;
|
|
23
|
+
* 未绑定也视为非 owner。
|
|
23
24
|
*/
|
|
24
25
|
export declare class OwnerBinder {
|
|
25
26
|
private readonly store;
|
|
26
27
|
private rejectedTotal;
|
|
27
28
|
constructor(store: StartupContactStore);
|
|
28
29
|
isBound(): boolean;
|
|
29
|
-
/** 已绑定的 owner
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
isOwner(
|
|
30
|
+
/** 已绑定的 owner userId;未绑定时 undefined。 */
|
|
31
|
+
boundUserId(): string | undefined;
|
|
32
|
+
/** 用户是否为已绑定的 owner(未绑定返回 false)。 */
|
|
33
|
+
isOwner(userId: string): boolean;
|
|
33
34
|
/** 累计被拒(非 owner)消息/卡片数,用于 DoS 可观测。 */
|
|
34
35
|
get rejectedCount(): number;
|
|
35
36
|
/**
|
|
36
37
|
* 对入站私聊消息做绑定/授权判定,含副作用:
|
|
37
38
|
* - `owner`:放行,调用方继续正常处理
|
|
38
39
|
* - `rejected`:已绑定但非 owner,静默丢弃(计数 + debug)
|
|
39
|
-
* - `bind_success
|
|
40
|
+
* - `bind_success`:未绑定收到首条消息,写入绑定(任意消息均可)
|
|
40
41
|
*/
|
|
41
|
-
|
|
42
|
+
authorize(userId: string, chatId: string): BindDecision;
|
|
42
43
|
/** 卡片操作被拒时计数(与消息路径共用计数器)。 */
|
|
43
|
-
recordRejectedCardAction(
|
|
44
|
+
recordRejectedCardAction(userId: string): void;
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* 控制台绑定引导文案(首次未绑定时输出到 stderr;守护模式下被 watchdog
|
package/dist/binder.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLogger } from './logger/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* Owner 绑定器:首次私聊发送任意消息完成"认领",此后仅该
|
|
3
|
+
* Owner 绑定器:首次私聊发送任意消息完成"认领",此后仅该 userId 可用。
|
|
4
4
|
*
|
|
5
5
|
* 设计依据:lark-remote 进程持有用户本机完整权限,唯一合法主体是 owner 本人。
|
|
6
6
|
* 飞书自建应用的私聊入口本身就是 owner 掌控的(应用只有 owner 能看到/使用,
|
|
@@ -9,9 +9,10 @@ import { getLogger } from './logger/index.js';
|
|
|
9
9
|
*
|
|
10
10
|
* 状态机:
|
|
11
11
|
* - 未绑定:第一条私聊消息(任意内容)即完成绑定 -> 写入 startup-contact.json。
|
|
12
|
-
* - 已绑定:仅
|
|
12
|
+
* - 已绑定:仅 userId === bound.userId 的消息放行;其余静默丢弃(计数 + debug)。
|
|
13
13
|
*
|
|
14
|
-
* cardAction 同样校验 operator.openId
|
|
14
|
+
* cardAction 同样校验 operator.openId(飞书 SDK 字段,值即 userId)=== bound.userId;
|
|
15
|
+
* 未绑定也视为非 owner。
|
|
15
16
|
*/
|
|
16
17
|
export class OwnerBinder {
|
|
17
18
|
store;
|
|
@@ -22,14 +23,14 @@ export class OwnerBinder {
|
|
|
22
23
|
isBound() {
|
|
23
24
|
return this.store.getContact() !== undefined;
|
|
24
25
|
}
|
|
25
|
-
/** 已绑定的 owner
|
|
26
|
-
|
|
26
|
+
/** 已绑定的 owner userId;未绑定时 undefined。 */
|
|
27
|
+
boundUserId() {
|
|
27
28
|
return this.store.getContact()?.userId;
|
|
28
29
|
}
|
|
29
|
-
/**
|
|
30
|
-
isOwner(
|
|
30
|
+
/** 用户是否为已绑定的 owner(未绑定返回 false)。 */
|
|
31
|
+
isOwner(userId) {
|
|
31
32
|
const bound = this.store.getContact()?.userId;
|
|
32
|
-
return bound !== undefined && bound ===
|
|
33
|
+
return bound !== undefined && bound === userId;
|
|
33
34
|
}
|
|
34
35
|
/** 累计被拒(非 owner)消息/卡片数,用于 DoS 可观测。 */
|
|
35
36
|
get rejectedCount() {
|
|
@@ -39,26 +40,26 @@ export class OwnerBinder {
|
|
|
39
40
|
* 对入站私聊消息做绑定/授权判定,含副作用:
|
|
40
41
|
* - `owner`:放行,调用方继续正常处理
|
|
41
42
|
* - `rejected`:已绑定但非 owner,静默丢弃(计数 + debug)
|
|
42
|
-
* - `bind_success
|
|
43
|
+
* - `bind_success`:未绑定收到首条消息,写入绑定(任意消息均可)
|
|
43
44
|
*/
|
|
44
|
-
|
|
45
|
+
authorize(userId, chatId) {
|
|
45
46
|
const bound = this.store.getContact();
|
|
46
47
|
if (bound) {
|
|
47
|
-
if (
|
|
48
|
+
if (userId === bound.userId)
|
|
48
49
|
return { kind: 'owner' };
|
|
49
50
|
this.rejectedTotal++;
|
|
50
|
-
getLogger().debug(`[binder] rejected message from ${
|
|
51
|
+
getLogger().debug(`[binder] rejected message from ${userId} (total rejected=${this.rejectedCount})`);
|
|
51
52
|
return { kind: 'rejected' };
|
|
52
53
|
}
|
|
53
54
|
// 未绑定:任意内容即绑定(首条消息完成认领)
|
|
54
|
-
this.store.save({ chatId, userId
|
|
55
|
-
getLogger().info(`[binder] owner bound:
|
|
55
|
+
this.store.save({ chatId, userId });
|
|
56
|
+
getLogger().info(`[binder] owner bound: userId=${userId} chatId=${chatId}`);
|
|
56
57
|
return { kind: 'bind_success' };
|
|
57
58
|
}
|
|
58
59
|
/** 卡片操作被拒时计数(与消息路径共用计数器)。 */
|
|
59
|
-
recordRejectedCardAction(
|
|
60
|
+
recordRejectedCardAction(userId) {
|
|
60
61
|
this.rejectedTotal++;
|
|
61
|
-
getLogger().debug(`[binder] rejected card action from ${
|
|
62
|
+
getLogger().debug(`[binder] rejected card action from ${userId} (total rejected=${this.rejectedCount})`);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
@@ -95,7 +95,7 @@ export declare class ApprovalCoordinator {
|
|
|
95
95
|
nonce?: string;
|
|
96
96
|
}): Promise<void>;
|
|
97
97
|
/**
|
|
98
|
-
* AskUserQuestion 自定义答案(Other
|
|
98
|
+
* AskUserQuestion 自定义答案(Other):自由文本直接作为该
|
|
99
99
|
* 单选问题的答案。与单选 toggle 同语义:全部问题答完自动提交。
|
|
100
100
|
*/
|
|
101
101
|
answerCustom(action: {
|
|
@@ -180,7 +180,7 @@ export class ApprovalCoordinator {
|
|
|
180
180
|
}
|
|
181
181
|
const selected = new Set(q.selected ?? []);
|
|
182
182
|
if (q.multiSelect) {
|
|
183
|
-
//
|
|
183
|
+
// 多选切换按钮同一 nonce 重复投递(双击/飞书重投递)只应
|
|
184
184
|
// toggle 一次,否则勾选会被二次 toggle 抵消。单选用例的重复防护在
|
|
185
185
|
// respondQuestionAnswers(提交即幂等,选中是 set 语义)。
|
|
186
186
|
this.assertFreshNonce(ctx.requestId, ctx.nonce);
|
|
@@ -226,7 +226,7 @@ export class ApprovalCoordinator {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
|
-
* AskUserQuestion 自定义答案(Other
|
|
229
|
+
* AskUserQuestion 自定义答案(Other):自由文本直接作为该
|
|
230
230
|
* 单选问题的答案。与单选 toggle 同语义:全部问题答完自动提交。
|
|
231
231
|
*/
|
|
232
232
|
async answerCustom(action, ctx) {
|
|
@@ -451,7 +451,7 @@ export class ApprovalCoordinator {
|
|
|
451
451
|
void this.responder(requestId, { action: 'cancel' }).catch((err) => {
|
|
452
452
|
getLogger().warn(`[approval-coordinator] expiry cancel failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
453
453
|
// 响应失败不是「正常过期」:状态升级为 failed,避免误判为已正常通知
|
|
454
|
-
// server(spec
|
|
454
|
+
// server(spec responder 抛错时状态置 failed)。
|
|
455
455
|
tracked.state = 'failed';
|
|
456
456
|
// 兜底:cancel 未能送达时主动中断 turn,避免 server 无限等待审批响应。
|
|
457
457
|
void this.interruptTurn().catch((interruptErr) => {
|
|
@@ -1,38 +1,12 @@
|
|
|
1
1
|
import { type AppConfig } from '../config/index.js';
|
|
2
2
|
import type { MediaOutcome } from '../inbound/turn.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { InboundMediaPayload } from '../connector/index.js';
|
|
4
4
|
export interface InboundMediaDeps {
|
|
5
5
|
/** 解析用户 cwd(bridge.resolveCwd 注入,统一三份复制点)。 */
|
|
6
6
|
resolveCwd: (userId: string) => string | undefined;
|
|
7
7
|
/** 读取当前配置(活引用,随 /config 保存更新,避免启动快照过期)。 */
|
|
8
8
|
getConfig: () => AppConfig;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* 文件名 sanitize:
|
|
12
|
-
* path.basename 剥离目录(防 `../` 穿越),再替换控制字符与路径分隔符;
|
|
13
|
-
* 空名 / `.` / `..` 视为无效(返回空串由调用方生成兜底名)。
|
|
14
|
-
*/
|
|
15
|
-
export declare function sanitizeFileName(name: string): string;
|
|
16
|
-
/**
|
|
17
|
-
* 资源扩展名(图片/视频/语音):优先 MIME 映射,未知时按魔数兜底;
|
|
18
|
-
* 两者都无法识别时返回 undefined(调用方省略扩展名,避免错误标注格式)。
|
|
19
|
-
*/
|
|
20
|
-
export declare function extensionFor(_kind: InboundResourceKind, mimeType: string | undefined, head: Buffer): string | undefined;
|
|
21
|
-
/**
|
|
22
|
-
* 文件名长度限制(字节级,保留扩展名),避免 ENAMETOOLONG 整文件失败。
|
|
23
|
-
* 极罕见情况(扩展名本身超限)直接截断全名。
|
|
24
|
-
*/
|
|
25
|
-
export declare function limitFileNameLength(name: string, maxBytes?: number): string;
|
|
26
|
-
/**
|
|
27
|
-
* 生成落盘文件名:
|
|
28
|
-
* - image:`image_<HHmmss>_<n>.<ext>`(ext 按 MIME/魔数);
|
|
29
|
-
* - 其它有原名的资源(file/video/audio/sticker):保留原名(已 sanitize);
|
|
30
|
-
* - 无原名的资源:`<kind>_<HHmmss>_<n>.<ext>`,ext 按 MIME/魔数
|
|
31
|
-
* (旧实现落成无扩展名的 `file_HHmmss_n`,mp4/opus/gif 都不可识别)。
|
|
32
|
-
*/
|
|
33
|
-
export declare function buildFileName(item: InboundMediaItem, index: number, receivedAt: Date): string;
|
|
34
|
-
/** 同名冲突自动加序号(name-1.ext、name-2.ext…),不覆盖已有文件。 */
|
|
35
|
-
export declare function uniqueTargetPath(dir: string, fileName: string): string;
|
|
36
10
|
export declare class InboundMediaHandler {
|
|
37
11
|
private readonly deps;
|
|
38
12
|
constructor(deps: InboundMediaDeps);
|
|
@@ -3,7 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { DEFAULT_INBOUND_MEDIA_DIR_NAME } from '../config/index.js';
|
|
4
4
|
import { atomicMoveFile } from '../persistence/atomic-write.js';
|
|
5
5
|
import { getLogger } from '../logger/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { bestEffortUnlink } from '../common/fs.js';
|
|
7
|
+
import { truncateUtf8 } from '../common/truncate.js';
|
|
7
8
|
/**
|
|
8
9
|
* 入站媒体落盘。
|
|
9
10
|
*
|
|
@@ -52,43 +53,46 @@ function hasControlChar(s) {
|
|
|
52
53
|
return false;
|
|
53
54
|
}
|
|
54
55
|
/** 子目录时间戳:YYYYMMDDHHmm(本地时间,精确到分钟)。 */
|
|
55
|
-
function
|
|
56
|
+
function timestampDir(d) {
|
|
56
57
|
return (`${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}` +
|
|
57
58
|
`${pad2(d.getHours())}${pad2(d.getMinutes())}`);
|
|
58
59
|
}
|
|
59
60
|
/** 文件名时间戳:HHmmss(本地时间)。 */
|
|
60
|
-
function
|
|
61
|
+
function timestampHms(d) {
|
|
61
62
|
return `${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
65
|
* 文件名 sanitize:
|
|
65
|
-
* path.basename 剥离目录(防 `../`
|
|
66
|
+
* path.basename 剥离目录(防 `../` 穿越),再替换控制字符、路径分隔符与
|
|
67
|
+
* win32 非法字符(`<>:"|?*`,NTFS/Win32 API 拒绝,落盘直接 EPERM/EINVAL),
|
|
68
|
+
* 并剥掉尾部的点与空格(win32 会静默剥离它们,导致落盘名 ≠ originalName);
|
|
66
69
|
* 空名 / `.` / `..` 视为无效(返回空串由调用方生成兜底名)。
|
|
67
70
|
*/
|
|
68
|
-
|
|
71
|
+
function sanitizeFileName(name) {
|
|
69
72
|
const base = path.basename(name).trim();
|
|
70
73
|
const chars = [];
|
|
71
74
|
for (const ch of base) {
|
|
72
75
|
const code = ch.charCodeAt(0);
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
if (code <= 0x1f || code === 0x7f ||
|
|
76
|
+
// 控制字符、路径分隔符与 win32 非法字符一律替换为下划线(no-control-regex
|
|
77
|
+
// 不允许字面控制字符转义出现在正则里,逐字符判断语义等价且无 lint 冲突)。
|
|
78
|
+
if (code <= 0x1f || code === 0x7f || '/\\<>:"|?*'.includes(ch)) {
|
|
76
79
|
chars.push('_');
|
|
77
80
|
}
|
|
78
81
|
else {
|
|
79
82
|
chars.push(ch);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
// 尾部的点与空格:win32 落盘时被静默剥离,提前剥掉保证记录名与磁盘名一致
|
|
86
|
+
const trimmedEnd = chars.join('').replace(/[. ]+$/, '');
|
|
87
|
+
if (trimmedEnd === '' || trimmedEnd === '.' || trimmedEnd === '..')
|
|
84
88
|
return '';
|
|
85
|
-
return
|
|
89
|
+
return trimmedEnd;
|
|
86
90
|
}
|
|
87
91
|
/**
|
|
88
92
|
* 资源扩展名(图片/视频/语音):优先 MIME 映射,未知时按魔数兜底;
|
|
89
93
|
* 两者都无法识别时返回 undefined(调用方省略扩展名,避免错误标注格式)。
|
|
90
94
|
*/
|
|
91
|
-
|
|
95
|
+
function extensionFor(_kind, mimeType, head) {
|
|
92
96
|
const normalized = mimeType?.split(';')[0]?.trim().toLowerCase();
|
|
93
97
|
if (normalized && MIME_TO_EXT[normalized])
|
|
94
98
|
return MIME_TO_EXT[normalized];
|
|
@@ -137,13 +141,11 @@ export function extensionFor(_kind, mimeType, head) {
|
|
|
137
141
|
}
|
|
138
142
|
return undefined;
|
|
139
143
|
}
|
|
140
|
-
/** 按 UTF-8 字节数截断字符串(避免截在多字节字符中间)。 */
|
|
141
|
-
import { truncateUtf8 } from '../common/truncate.js';
|
|
142
144
|
/**
|
|
143
145
|
* 文件名长度限制(字节级,保留扩展名),避免 ENAMETOOLONG 整文件失败。
|
|
144
146
|
* 极罕见情况(扩展名本身超限)直接截断全名。
|
|
145
147
|
*/
|
|
146
|
-
|
|
148
|
+
function limitFileNameLength(name, maxBytes = MAX_FILE_NAME_BYTES) {
|
|
147
149
|
if (Buffer.byteLength(name, 'utf8') <= maxBytes)
|
|
148
150
|
return name;
|
|
149
151
|
const ext = path.extname(name);
|
|
@@ -172,15 +174,15 @@ function readFileHead(filePath, maxBytes = MAGIC_HEAD_BYTES) {
|
|
|
172
174
|
* - 无原名的资源:`<kind>_<HHmmss>_<n>.<ext>`,ext 按 MIME/魔数
|
|
173
175
|
* (旧实现落成无扩展名的 `file_HHmmss_n`,mp4/opus/gif 都不可识别)。
|
|
174
176
|
*/
|
|
175
|
-
|
|
177
|
+
function buildFileName(item, index, receivedAt) {
|
|
176
178
|
const original = item.fileName ? limitFileNameLength(sanitizeFileName(item.fileName)) : '';
|
|
177
179
|
if (item.kind !== 'image' && original)
|
|
178
180
|
return original;
|
|
179
181
|
const ext = extensionFor(item.kind, item.mimeType, readFileHead(item.tempPath));
|
|
180
|
-
return `${item.kind}_${
|
|
182
|
+
return `${item.kind}_${timestampHms(receivedAt)}_${index}${ext ? `.${ext}` : ''}`;
|
|
181
183
|
}
|
|
182
184
|
/** 同名冲突自动加序号(name-1.ext、name-2.ext…),不覆盖已有文件。 */
|
|
183
|
-
|
|
185
|
+
function uniqueTargetPath(dir, fileName) {
|
|
184
186
|
const ext = path.extname(fileName);
|
|
185
187
|
const stem = fileName.slice(0, fileName.length - ext.length);
|
|
186
188
|
let candidate = path.join(dir, fileName);
|
|
@@ -216,7 +218,7 @@ export class InboundMediaHandler {
|
|
|
216
218
|
};
|
|
217
219
|
}
|
|
218
220
|
const receivedAt = new Date();
|
|
219
|
-
const dir = path.join(cwd, this.safeDirName(),
|
|
221
|
+
const dir = path.join(cwd, this.safeDirName(), timestampDir(receivedAt));
|
|
220
222
|
try {
|
|
221
223
|
fs.mkdirSync(dir, { recursive: true });
|
|
222
224
|
}
|
|
@@ -254,7 +256,7 @@ export class InboundMediaHandler {
|
|
|
254
256
|
});
|
|
255
257
|
}
|
|
256
258
|
catch (err) {
|
|
257
|
-
|
|
259
|
+
bestEffortUnlink(item.tempPath);
|
|
258
260
|
const label = item.fileName
|
|
259
261
|
? sanitizeFileName(item.fileName) || item.fileName
|
|
260
262
|
: `第 ${i + 1} 个`;
|
|
@@ -287,7 +289,7 @@ export class InboundMediaHandler {
|
|
|
287
289
|
/** 清理未被移动的临时文件(无 cwd / 目录创建失败等提前返回路径)。 */
|
|
288
290
|
cleanupTemps(payload) {
|
|
289
291
|
for (const item of payload.media) {
|
|
290
|
-
|
|
292
|
+
bestEffortUnlink(item.tempPath);
|
|
291
293
|
}
|
|
292
294
|
}
|
|
293
295
|
resolveCwd(userId) {
|