@openclaw-china/shared 0.1.29 → 0.1.31

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.
@@ -1,82 +1,82 @@
1
- /**
2
- * DM 策略引擎
3
- *
4
- * 实现 open/pairing/allowlist 策略检查
5
- */
6
-
7
- /**
8
- * DM 策略类型
9
- * - open: 允许所有单聊消息
10
- * - pairing: 配对模式(允许所有,配对逻辑由上层处理)
11
- * - allowlist: 仅允许白名单中的发送者
12
- */
13
- export type DmPolicyType = "open" | "pairing" | "allowlist";
14
-
15
- /**
16
- * 策略检查结果
17
- */
18
- export interface PolicyCheckResult {
19
- /** 是否允许处理该消息 */
20
- allowed: boolean;
21
- /** 拒绝原因(如果被拒绝) */
22
- reason?: string;
23
- }
24
-
25
- /**
26
- * DM 策略检查参数
27
- */
28
- export interface DmPolicyCheckParams {
29
- /** DM 策略类型 */
30
- dmPolicy: DmPolicyType;
31
- /** 发送者 ID */
32
- senderId: string;
33
- /** 白名单(allowlist 策略时使用) */
34
- allowFrom?: string[];
35
- }
36
-
37
- /**
38
- * 检查单聊策略
39
- *
40
- * @param params 检查参数
41
- * @returns 策略检查结果
42
- *
43
- * @example
44
- * ```ts
45
- * // 开放策略
46
- * checkDmPolicy({ dmPolicy: "open", senderId: "user1" });
47
- * // => { allowed: true }
48
- *
49
- * // 白名单策略
50
- * checkDmPolicy({ dmPolicy: "allowlist", senderId: "user1", allowFrom: ["user1", "user2"] });
51
- * // => { allowed: true }
52
- *
53
- * checkDmPolicy({ dmPolicy: "allowlist", senderId: "user3", allowFrom: ["user1", "user2"] });
54
- * // => { allowed: false, reason: "sender user3 not in DM allowlist" }
55
- * ```
56
- */
57
- export function checkDmPolicy(params: DmPolicyCheckParams): PolicyCheckResult {
58
- const { dmPolicy, senderId, allowFrom = [] } = params;
59
-
60
- switch (dmPolicy) {
61
- case "open":
62
- // 开放策略:允许所有单聊消息
63
- return { allowed: true };
64
-
65
- case "pairing":
66
- // 配对策略:允许所有单聊消息(配对逻辑由上层处理)
67
- return { allowed: true };
68
-
69
- case "allowlist":
70
- // 白名单策略:仅允许 allowFrom 中的发送者
71
- if (allowFrom.includes(senderId)) {
72
- return { allowed: true };
73
- }
74
- return {
75
- allowed: false,
76
- reason: `sender ${senderId} not in DM allowlist`,
77
- };
78
-
79
- default:
80
- return { allowed: true };
81
- }
82
- }
1
+ /**
2
+ * DM 策略引擎
3
+ *
4
+ * 实现 open/pairing/allowlist 策略检查
5
+ */
6
+
7
+ /**
8
+ * DM 策略类型
9
+ * - open: 允许所有单聊消息
10
+ * - pairing: 配对模式(允许所有,配对逻辑由上层处理)
11
+ * - allowlist: 仅允许白名单中的发送者
12
+ */
13
+ export type DmPolicyType = "open" | "pairing" | "allowlist";
14
+
15
+ /**
16
+ * 策略检查结果
17
+ */
18
+ export interface PolicyCheckResult {
19
+ /** 是否允许处理该消息 */
20
+ allowed: boolean;
21
+ /** 拒绝原因(如果被拒绝) */
22
+ reason?: string;
23
+ }
24
+
25
+ /**
26
+ * DM 策略检查参数
27
+ */
28
+ export interface DmPolicyCheckParams {
29
+ /** DM 策略类型 */
30
+ dmPolicy: DmPolicyType;
31
+ /** 发送者 ID */
32
+ senderId: string;
33
+ /** 白名单(allowlist 策略时使用) */
34
+ allowFrom?: string[];
35
+ }
36
+
37
+ /**
38
+ * 检查单聊策略
39
+ *
40
+ * @param params 检查参数
41
+ * @returns 策略检查结果
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * // 开放策略
46
+ * checkDmPolicy({ dmPolicy: "open", senderId: "user1" });
47
+ * // => { allowed: true }
48
+ *
49
+ * // 白名单策略
50
+ * checkDmPolicy({ dmPolicy: "allowlist", senderId: "user1", allowFrom: ["user1", "user2"] });
51
+ * // => { allowed: true }
52
+ *
53
+ * checkDmPolicy({ dmPolicy: "allowlist", senderId: "user3", allowFrom: ["user1", "user2"] });
54
+ * // => { allowed: false, reason: "sender user3 not in DM allowlist" }
55
+ * ```
56
+ */
57
+ export function checkDmPolicy(params: DmPolicyCheckParams): PolicyCheckResult {
58
+ const { dmPolicy, senderId, allowFrom = [] } = params;
59
+
60
+ switch (dmPolicy) {
61
+ case "open":
62
+ // 开放策略:允许所有单聊消息
63
+ return { allowed: true };
64
+
65
+ case "pairing":
66
+ // 配对策略:允许所有单聊消息(配对逻辑由上层处理)
67
+ return { allowed: true };
68
+
69
+ case "allowlist":
70
+ // 白名单策略:仅允许 allowFrom 中的发送者
71
+ if (allowFrom.includes(senderId)) {
72
+ return { allowed: true };
73
+ }
74
+ return {
75
+ allowed: false,
76
+ reason: `sender ${senderId} not in DM allowlist`,
77
+ };
78
+
79
+ default:
80
+ return { allowed: true };
81
+ }
82
+ }
@@ -1,93 +1,93 @@
1
- /**
2
- * 群组策略引擎
3
- *
4
- * 实现 open/allowlist/disabled 策略检查
5
- */
6
-
7
- import type { PolicyCheckResult } from "./dm-policy.js";
8
-
9
- /**
10
- * 群组策略类型
11
- * - open: 允许所有群聊消息
12
- * - allowlist: 仅允许白名单中的群组
13
- * - disabled: 禁用所有群聊消息
14
- */
15
- export type GroupPolicyType = "open" | "allowlist" | "disabled";
16
-
17
- /**
18
- * 群组策略检查参数
19
- */
20
- export interface GroupPolicyCheckParams {
21
- /** 群组策略类型 */
22
- groupPolicy: GroupPolicyType;
23
- /** 会话 ID(群组 ID) */
24
- conversationId: string;
25
- /** 群组白名单(allowlist 策略时使用) */
26
- groupAllowFrom?: string[];
27
- /** 是否要求 @提及机器人 */
28
- requireMention: boolean;
29
- /** 是否 @提及了机器人 */
30
- mentionedBot: boolean;
31
- }
32
-
33
- /**
34
- * 检查群聊策略
35
- *
36
- * @param params 检查参数
37
- * @returns 策略检查结果
38
- *
39
- * @example
40
- * ```ts
41
- * // 禁用策略
42
- * checkGroupPolicy({ groupPolicy: "disabled", conversationId: "g1", requireMention: false, mentionedBot: false });
43
- * // => { allowed: false, reason: "group messages disabled" }
44
- *
45
- * // 开放策略 + 要求 @提及
46
- * checkGroupPolicy({ groupPolicy: "open", conversationId: "g1", requireMention: true, mentionedBot: false });
47
- * // => { allowed: false, reason: "message did not mention bot" }
48
- *
49
- * // 白名单策略
50
- * checkGroupPolicy({ groupPolicy: "allowlist", conversationId: "g1", groupAllowFrom: ["g1"], requireMention: false, mentionedBot: false });
51
- * // => { allowed: true }
52
- * ```
53
- */
54
- export function checkGroupPolicy(params: GroupPolicyCheckParams): PolicyCheckResult {
55
- const { groupPolicy, conversationId, groupAllowFrom = [], requireMention, mentionedBot } = params;
56
-
57
- // 首先检查群聊策略
58
- switch (groupPolicy) {
59
- case "disabled":
60
- // 禁用策略:拒绝所有群聊消息
61
- return {
62
- allowed: false,
63
- reason: "group messages disabled",
64
- };
65
-
66
- case "allowlist":
67
- // 白名单策略:仅允许 groupAllowFrom 中的群组
68
- if (!groupAllowFrom.includes(conversationId)) {
69
- return {
70
- allowed: false,
71
- reason: `group ${conversationId} not in allowlist`,
72
- };
73
- }
74
- break;
75
-
76
- case "open":
77
- // 开放策略:允许所有群聊
78
- break;
79
-
80
- default:
81
- break;
82
- }
83
-
84
- // 然后检查 @提及要求
85
- if (requireMention && !mentionedBot) {
86
- return {
87
- allowed: false,
88
- reason: "message did not mention bot",
89
- };
90
- }
91
-
92
- return { allowed: true };
93
- }
1
+ /**
2
+ * 群组策略引擎
3
+ *
4
+ * 实现 open/allowlist/disabled 策略检查
5
+ */
6
+
7
+ import type { PolicyCheckResult } from "./dm-policy.js";
8
+
9
+ /**
10
+ * 群组策略类型
11
+ * - open: 允许所有群聊消息
12
+ * - allowlist: 仅允许白名单中的群组
13
+ * - disabled: 禁用所有群聊消息
14
+ */
15
+ export type GroupPolicyType = "open" | "allowlist" | "disabled";
16
+
17
+ /**
18
+ * 群组策略检查参数
19
+ */
20
+ export interface GroupPolicyCheckParams {
21
+ /** 群组策略类型 */
22
+ groupPolicy: GroupPolicyType;
23
+ /** 会话 ID(群组 ID) */
24
+ conversationId: string;
25
+ /** 群组白名单(allowlist 策略时使用) */
26
+ groupAllowFrom?: string[];
27
+ /** 是否要求 @提及机器人 */
28
+ requireMention: boolean;
29
+ /** 是否 @提及了机器人 */
30
+ mentionedBot: boolean;
31
+ }
32
+
33
+ /**
34
+ * 检查群聊策略
35
+ *
36
+ * @param params 检查参数
37
+ * @returns 策略检查结果
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // 禁用策略
42
+ * checkGroupPolicy({ groupPolicy: "disabled", conversationId: "g1", requireMention: false, mentionedBot: false });
43
+ * // => { allowed: false, reason: "group messages disabled" }
44
+ *
45
+ * // 开放策略 + 要求 @提及
46
+ * checkGroupPolicy({ groupPolicy: "open", conversationId: "g1", requireMention: true, mentionedBot: false });
47
+ * // => { allowed: false, reason: "message did not mention bot" }
48
+ *
49
+ * // 白名单策略
50
+ * checkGroupPolicy({ groupPolicy: "allowlist", conversationId: "g1", groupAllowFrom: ["g1"], requireMention: false, mentionedBot: false });
51
+ * // => { allowed: true }
52
+ * ```
53
+ */
54
+ export function checkGroupPolicy(params: GroupPolicyCheckParams): PolicyCheckResult {
55
+ const { groupPolicy, conversationId, groupAllowFrom = [], requireMention, mentionedBot } = params;
56
+
57
+ // 首先检查群聊策略
58
+ switch (groupPolicy) {
59
+ case "disabled":
60
+ // 禁用策略:拒绝所有群聊消息
61
+ return {
62
+ allowed: false,
63
+ reason: "group messages disabled",
64
+ };
65
+
66
+ case "allowlist":
67
+ // 白名单策略:仅允许 groupAllowFrom 中的群组
68
+ if (!groupAllowFrom.includes(conversationId)) {
69
+ return {
70
+ allowed: false,
71
+ reason: `group ${conversationId} not in allowlist`,
72
+ };
73
+ }
74
+ break;
75
+
76
+ case "open":
77
+ // 开放策略:允许所有群聊
78
+ break;
79
+
80
+ default:
81
+ break;
82
+ }
83
+
84
+ // 然后检查 @提及要求
85
+ if (requireMention && !mentionedBot) {
86
+ return {
87
+ allowed: false,
88
+ reason: "message did not mention bot",
89
+ };
90
+ }
91
+
92
+ return { allowed: true };
93
+ }
@@ -1,2 +1,2 @@
1
- export * from "./dm-policy.js";
2
- export * from "./group-policy.js";
1
+ export * from "./dm-policy.js";
2
+ export * from "./group-policy.js";
@@ -1,24 +1,24 @@
1
- // 共享类型定义
2
-
3
- export type DmPolicy = "open" | "pairing" | "allowlist";
4
- export type GroupPolicy = "open" | "allowlist" | "disabled";
5
-
6
- export interface ParsedMessage {
7
- chatId: string;
8
- messageId: string;
9
- senderId: string;
10
- senderName?: string;
11
- chatType: "direct" | "group";
12
- content: string;
13
- contentType: string;
14
- mentionedBot: boolean;
15
- replyToMessageId?: string;
16
- timestamp: number;
17
- }
18
-
19
- export interface HistoryEntry {
20
- sender: string;
21
- body: string;
22
- timestamp: number;
23
- messageId: string;
24
- }
1
+ // 共享类型定义
2
+
3
+ export type DmPolicy = "open" | "pairing" | "allowlist";
4
+ export type GroupPolicy = "open" | "allowlist" | "disabled";
5
+
6
+ export interface ParsedMessage {
7
+ chatId: string;
8
+ messageId: string;
9
+ senderId: string;
10
+ senderName?: string;
11
+ chatType: "direct" | "group";
12
+ content: string;
13
+ contentType: string;
14
+ mentionedBot: boolean;
15
+ replyToMessageId?: string;
16
+ timestamp: number;
17
+ }
18
+
19
+ export interface HistoryEntry {
20
+ sender: string;
21
+ body: string;
22
+ timestamp: number;
23
+ messageId: string;
24
+ }
package/tsconfig.json CHANGED
@@ -1,8 +1,8 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"]
8
- }
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
package/vitest.config.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- include: ["src/**/*.test.ts"],
6
- globals: false,
7
- },
8
- });
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/**/*.test.ts"],
6
+ globals: false,
7
+ },
8
+ });
@@ -1,180 +0,0 @@
1
- import { afterEach, describe, expect, it } from "vitest";
2
- import * as fs from "node:fs";
3
- import * as fsPromises from "node:fs/promises";
4
- import * as os from "node:os";
5
- import * as path from "node:path";
6
- import {
7
- FileSizeLimitError,
8
- MediaTimeoutError,
9
- cleanupFileSafe,
10
- downloadToTempFile,
11
- finalizeInboundMediaFile,
12
- pruneInboundMediaDir,
13
- } from "./media-io.js";
14
-
15
- const tempDirs: string[] = [];
16
-
17
- async function createTempDir(prefix: string): Promise<string> {
18
- const dir = await fsPromises.mkdtemp(path.join(os.tmpdir(), prefix));
19
- tempDirs.push(dir);
20
- return dir;
21
- }
22
-
23
- afterEach(async () => {
24
- for (const dir of tempDirs.splice(0, tempDirs.length)) {
25
- await fsPromises.rm(dir, { recursive: true, force: true });
26
- }
27
- });
28
-
29
- describe("downloadToTempFile", () => {
30
- it("downloads HTTP response and stores a temp file", async () => {
31
- const dir = await createTempDir("shared-media-io-");
32
- const body = Buffer.from("hello-media", "utf8");
33
- const fetchFn: typeof globalThis.fetch = async () =>
34
- new Response(body, {
35
- status: 200,
36
- headers: {
37
- "content-type": "image/png",
38
- "content-length": String(body.length),
39
- },
40
- });
41
-
42
- const result = await downloadToTempFile("https://example.com/a.png", {
43
- fetch: fetchFn,
44
- tempDir: dir,
45
- tempPrefix: "dingtalk-file",
46
- });
47
-
48
- expect(result.path.startsWith(dir)).toBe(true);
49
- expect(result.fileName.endsWith(".png")).toBe(true);
50
- expect(result.size).toBe(body.length);
51
- expect(result.contentType).toBe("image/png");
52
-
53
- const saved = await fsPromises.readFile(result.path);
54
- expect(saved.equals(body)).toBe(true);
55
- });
56
-
57
- it("throws FileSizeLimitError when Content-Length exceeds maxSize", async () => {
58
- const fetchFn: typeof globalThis.fetch = async () =>
59
- new Response("too-large", {
60
- status: 200,
61
- headers: {
62
- "content-type": "application/octet-stream",
63
- "content-length": "1024",
64
- },
65
- });
66
-
67
- await expect(
68
- downloadToTempFile("https://example.com/too-large.bin", {
69
- fetch: fetchFn,
70
- maxSize: 100,
71
- })
72
- ).rejects.toBeInstanceOf(FileSizeLimitError);
73
- });
74
-
75
- it("throws MediaTimeoutError on timeout", async () => {
76
- const fetchFn: typeof globalThis.fetch = async (_url, init) =>
77
- await new Promise<Response>((_resolve, reject) => {
78
- const signal = init?.signal;
79
- signal?.addEventListener("abort", () => {
80
- const err = new Error("aborted");
81
- (err as Error & { name: string }).name = "AbortError";
82
- reject(err);
83
- });
84
- });
85
-
86
- await expect(
87
- downloadToTempFile("https://example.com/slow.bin", {
88
- fetch: fetchFn,
89
- timeout: 10,
90
- })
91
- ).rejects.toBeInstanceOf(MediaTimeoutError);
92
- });
93
- });
94
-
95
- describe("cleanupFileSafe", () => {
96
- it("removes file and ignores missing file", async () => {
97
- const dir = await createTempDir("shared-media-clean-");
98
- const filePath = path.join(dir, "a.txt");
99
- await fsPromises.writeFile(filePath, "x", "utf8");
100
- expect(fs.existsSync(filePath)).toBe(true);
101
-
102
- await cleanupFileSafe(filePath);
103
- expect(fs.existsSync(filePath)).toBe(false);
104
-
105
- await expect(cleanupFileSafe(filePath)).resolves.toBeUndefined();
106
- await expect(cleanupFileSafe(undefined)).resolves.toBeUndefined();
107
- });
108
- });
109
-
110
- describe("inbound media retention", () => {
111
- it("finalizes temp media into inbound/YYYY-MM-DD", async () => {
112
- const tempDir = await createTempDir("shared-media-temp-");
113
- const inboundDir = await createTempDir("shared-media-inbound-");
114
- const sourcePath = path.join(tempDir, "img-1.jpg");
115
- await fsPromises.writeFile(sourcePath, "abc", "utf8");
116
-
117
- const finalPath = await finalizeInboundMediaFile({
118
- filePath: sourcePath,
119
- tempDir,
120
- inboundDir,
121
- });
122
-
123
- expect(finalPath.startsWith(inboundDir)).toBe(true);
124
- expect(fs.existsSync(finalPath)).toBe(true);
125
- expect(fs.existsSync(sourcePath)).toBe(false);
126
- });
127
-
128
- it("does not move files outside tempDir", async () => {
129
- const tempDir = await createTempDir("shared-media-temp-");
130
- const inboundDir = await createTempDir("shared-media-inbound-");
131
- const outsideDir = await createTempDir("shared-media-outside-");
132
- const sourcePath = path.join(outsideDir, "a.txt");
133
- await fsPromises.writeFile(sourcePath, "x", "utf8");
134
-
135
- const finalPath = await finalizeInboundMediaFile({
136
- filePath: sourcePath,
137
- tempDir,
138
- inboundDir,
139
- });
140
-
141
- expect(finalPath).toBe(sourcePath);
142
- expect(fs.existsSync(sourcePath)).toBe(true);
143
- });
144
-
145
- it("prunes only expired files in date dirs and keeps recent files", async () => {
146
- const inboundDir = await createTempDir("shared-media-prune-");
147
- const oldDir = path.join(inboundDir, "2024-01-01");
148
- const newDir = path.join(inboundDir, "2024-01-02");
149
- await fsPromises.mkdir(oldDir, { recursive: true });
150
- await fsPromises.mkdir(newDir, { recursive: true });
151
-
152
- const oldFile = path.join(oldDir, "old.jpg");
153
- const newFile = path.join(newDir, "new.jpg");
154
- const nestedDir = path.join(oldDir, "nested");
155
- const nestedFile = path.join(nestedDir, "nested.jpg");
156
- await fsPromises.writeFile(oldFile, "old", "utf8");
157
- await fsPromises.writeFile(newFile, "new", "utf8");
158
- await fsPromises.mkdir(nestedDir, { recursive: true });
159
- await fsPromises.writeFile(nestedFile, "nested", "utf8");
160
-
161
- const oldTs = new Date("2024-01-01T00:00:00.000Z");
162
- const newTs = new Date("2024-01-02T00:00:00.000Z");
163
- await fsPromises.utimes(oldDir, oldTs, oldTs);
164
- await fsPromises.utimes(oldFile, oldTs, oldTs);
165
- await fsPromises.utimes(newDir, newTs, newTs);
166
- await fsPromises.utimes(newFile, newTs, newTs);
167
-
168
- const nowMs = new Date("2024-01-03T00:00:00.000Z").getTime();
169
- await pruneInboundMediaDir({
170
- inboundDir,
171
- keepDays: 1,
172
- nowMs,
173
- });
174
-
175
- expect(fs.existsSync(oldFile)).toBe(false);
176
- expect(fs.existsSync(newFile)).toBe(true);
177
- expect(fs.existsSync(nestedFile)).toBe(true);
178
- expect(fs.existsSync(oldDir)).toBe(true);
179
- });
180
- });