ylib-syim 0.0.31 → 0.0.33

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/bridges/main.ts CHANGED
@@ -6909,6 +6909,7 @@ function readEffectiveWhitelistFromConfig(
6909
6909
  "modelName",
6910
6910
  "agentId",
6911
6911
  "asyncMode",
6912
+ "injectSenderContextToPrompt",
6912
6913
  "gatewayPort",
6913
6914
  "gatewayBaseUrl",
6914
6915
  "uploadHost",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ylib-syim",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -46,9 +46,9 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
49
- "ylib-dingtalk-connector": "0.7.10-beta.18",
50
- "ylib-openclaw-lark": "2026.3.17-beta.23",
51
- "ylib-openclaw-weixin": "2.1.7-beta.11",
49
+ "ylib-dingtalk-connector": "0.7.10-beta.20",
50
+ "ylib-openclaw-lark": "2026.3.17-beta.24",
51
+ "ylib-openclaw-weixin": "2.1.7-beta.12",
52
52
  "axios": "^1.6.0",
53
53
  "dingtalk-stream": "^2.1.4",
54
54
  "fluent-ffmpeg": "^2.1.3",
@@ -1,10 +1,168 @@
1
1
  import fs from "node:fs";
2
+ import fsPromises from "node:fs/promises";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
5
+ import crypto from "node:crypto";
6
+ import { fileTypeFromBuffer } from "file-type";
4
7
 
5
8
  const CHANNEL_KEY = "openclaw-weixin";
6
9
  let bridgeDefaultAccountAlias = "weixin-single";
7
10
  const bridgeAliasLogMemo = new Set<string>();
11
+ const MEDIA_FILE_MODE = 0o644;
12
+ const DEFAULT_MEDIA_MAX_BYTES = 5 * 1024 * 1024;
13
+ const EXT_BY_MIME: Record<string, string> = {
14
+ "image/heic": ".heic",
15
+ "image/heif": ".heif",
16
+ "image/jpeg": ".jpg",
17
+ "image/png": ".png",
18
+ "image/webp": ".webp",
19
+ "image/gif": ".gif",
20
+ "audio/ogg": ".ogg",
21
+ "audio/mpeg": ".mp3",
22
+ "audio/x-m4a": ".m4a",
23
+ "audio/mp4": ".m4a",
24
+ "video/mp4": ".mp4",
25
+ "video/quicktime": ".mov",
26
+ "application/pdf": ".pdf",
27
+ "application/json": ".json",
28
+ "application/zip": ".zip",
29
+ "application/gzip": ".gz",
30
+ "application/x-tar": ".tar",
31
+ "application/x-7z-compressed": ".7z",
32
+ "application/vnd.rar": ".rar",
33
+ "application/msword": ".doc",
34
+ "application/vnd.ms-excel": ".xls",
35
+ "application/vnd.ms-powerpoint": ".ppt",
36
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
37
+ ".docx",
38
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
39
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation":
40
+ ".pptx",
41
+ "text/csv": ".csv",
42
+ "text/plain": ".txt",
43
+ "text/markdown": ".md",
44
+ };
45
+ const MIME_BY_EXT: Record<string, string> = {
46
+ ...Object.fromEntries(Object.entries(EXT_BY_MIME).map(([mime, ext]) => [ext, mime])),
47
+ ".jpeg": "image/jpeg",
48
+ ".js": "text/javascript",
49
+ };
50
+
51
+ function sanitizeFilename(name: string): string {
52
+ const trimmed = name.trim();
53
+ if (!trimmed) return "";
54
+ const sanitized = trimmed.replace(/[^\p{L}\p{N}._-]+/gu, "_");
55
+ return sanitized.replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 60);
56
+ }
57
+
58
+ function normalizeMimeType(mime?: string | null): string | undefined {
59
+ if (!mime) return undefined;
60
+ const cleaned = mime.split(";")[0]?.trim().toLowerCase();
61
+ return cleaned || undefined;
62
+ }
63
+
64
+ function extensionForMime(mime?: string | null): string | undefined {
65
+ const normalized = normalizeMimeType(mime);
66
+ if (!normalized) return undefined;
67
+ return EXT_BY_MIME[normalized];
68
+ }
69
+
70
+ function getFileExtension(filePath?: string | null): string | undefined {
71
+ if (!filePath) return undefined;
72
+ try {
73
+ if (/^https?:\/\//i.test(filePath)) {
74
+ const url = new URL(filePath);
75
+ return path.extname(url.pathname).toLowerCase() || undefined;
76
+ }
77
+ } catch {
78
+ // fall back
79
+ }
80
+ const ext = path.extname(filePath).toLowerCase();
81
+ return ext || undefined;
82
+ }
83
+
84
+ function isGenericMime(mime?: string): boolean {
85
+ if (!mime) return true;
86
+ const lower = mime.toLowerCase();
87
+ return lower === "application/octet-stream" || lower === "application/zip";
88
+ }
89
+
90
+ function isMissingPathError(err: unknown): err is NodeJS.ErrnoException {
91
+ return err instanceof Error && "code" in err && err.code === "ENOENT";
92
+ }
93
+
94
+ async function retryAfterRecreatingDir<T>(
95
+ dir: string,
96
+ run: () => Promise<T>,
97
+ ): Promise<T> {
98
+ try {
99
+ return await run();
100
+ } catch (err) {
101
+ if (!isMissingPathError(err)) throw err;
102
+ await fsPromises.mkdir(dir, { recursive: true, mode: 0o700 });
103
+ return await run();
104
+ }
105
+ }
106
+
107
+ async function detectMime(opts: {
108
+ buffer?: Buffer;
109
+ headerMime?: string | null;
110
+ filePath?: string;
111
+ }): Promise<string | undefined> {
112
+ const ext = getFileExtension(opts.filePath);
113
+ const extMime = ext ? MIME_BY_EXT[ext] : undefined;
114
+ const headerMime = normalizeMimeType(opts.headerMime);
115
+ let sniffed: string | undefined;
116
+ if (opts.buffer) {
117
+ try {
118
+ const fileType = await fileTypeFromBuffer(opts.buffer);
119
+ sniffed = fileType?.mime ?? undefined;
120
+ } catch {
121
+ sniffed = undefined;
122
+ }
123
+ }
124
+ if (sniffed && (!isGenericMime(sniffed) || !extMime)) return sniffed;
125
+ if (extMime) return extMime;
126
+ if (headerMime && !isGenericMime(headerMime)) return headerMime;
127
+ if (sniffed) return sniffed;
128
+ if (headerMime) return headerMime;
129
+ return undefined;
130
+ }
131
+
132
+ async function saveMediaBufferFromOpenclaw(
133
+ buffer: Buffer,
134
+ contentType?: string,
135
+ subdir = "inbound",
136
+ maxBytes = DEFAULT_MEDIA_MAX_BYTES,
137
+ originalFilename?: string,
138
+ ): Promise<{ id: string; path: string; size: number; contentType?: string }> {
139
+ if (buffer.byteLength > maxBytes) {
140
+ throw new Error(
141
+ `Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`,
142
+ );
143
+ }
144
+ const dir = path.join(os.tmpdir(), "openclaw-media", subdir);
145
+ await fsPromises.mkdir(dir, { recursive: true, mode: 0o700 });
146
+ const uuid = crypto.randomUUID();
147
+ const headerExt = extensionForMime(contentType?.split(";")[0]?.trim());
148
+ const mime = await detectMime({ buffer, headerMime: contentType });
149
+ const ext = headerExt ?? extensionForMime(mime) ?? "";
150
+
151
+ let id: string;
152
+ if (originalFilename) {
153
+ const base = path.parse(originalFilename).name;
154
+ const sanitized = sanitizeFilename(base);
155
+ id = sanitized ? `${sanitized}---${uuid}${ext}` : `${uuid}${ext}`;
156
+ } else {
157
+ id = ext ? `${uuid}${ext}` : uuid;
158
+ }
159
+
160
+ const dest = path.join(dir, id);
161
+ await retryAfterRecreatingDir(dir, () =>
162
+ fsPromises.writeFile(dest, buffer, { mode: MEDIA_FILE_MODE }),
163
+ );
164
+ return { id, path: dest, size: buffer.byteLength, contentType: mime };
165
+ }
8
166
 
9
167
  function logBridgeAlias(from: string, to: string, reason: string): void {
10
168
  const key = `${from}->${to}:${reason}`;
@@ -503,6 +661,9 @@ function buildMinimalRuntime(
503
661
  activity: {
504
662
  record: () => {},
505
663
  },
664
+ media: {
665
+ saveMediaBuffer: saveMediaBufferFromOpenclaw,
666
+ },
506
667
  },
507
668
  system: {
508
669
  enqueueSystemEvent: () => {},