palz-connector 1.4.1 → 1.4.3

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,7 +1,7 @@
1
1
  {
2
2
  "id": "palz-connector",
3
3
  "name": "Palz Connector Channel",
4
- "version": "1.4.1",
4
+ "version": "1.4.3",
5
5
  "description": "Palz IM 接入 OpenClaw",
6
6
  "channels": [
7
7
  "palz-connector"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palz-connector",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
package/src/bot.ts CHANGED
@@ -545,6 +545,15 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
545
545
  if (msg.resource_id != null) {
546
546
  untrustedContext.push(`resource_id: ${msg.resource_id}`);
547
547
  }
548
+ if (msg.lobster_name) {
549
+ untrustedContext.push(`lobster_name: ${msg.lobster_name}`);
550
+ }
551
+ if (msg.lobster_type) {
552
+ untrustedContext.push(`lobster_type: ${msg.lobster_type}`);
553
+ }
554
+ if (msg.sender_account_type) {
555
+ untrustedContext.push(`sender_account_type: ${msg.sender_account_type}`);
556
+ }
548
557
  if (groupId) {
549
558
  untrustedContext.push(`group_id: ${groupId}`);
550
559
  }
package/src/media.ts CHANGED
@@ -36,22 +36,64 @@ export function resolveMediaLocalRoots(agentId?: string): string[] {
36
36
  return roots;
37
37
  }
38
38
 
39
+ /**
40
+ * 将原始文件名清洁化:
41
+ * - 去掉路径分隔符与控制字符
42
+ * - 保留中文、字母、数字、常见符号(- _ .)
43
+ * - 其它特殊字符统一替换为 "_"
44
+ * - 限制长度避免文件系统报错
45
+ */
46
+ function sanitizeFileName(name: string): string {
47
+ const ext = path.extname(name);
48
+ const stem = name.slice(0, name.length - ext.length);
49
+
50
+ // CJK 范围:CJK统一汉字 + 扩展A + 兼容汉字 + 全角标点等
51
+ const CJK = /[\u2E80-\u9FFF\uF900-\uFAFF]/;
52
+ const ASCII = /[A-Za-z0-9]/;
53
+
54
+ let cleaned = stem
55
+ .replace(/[\\/]/g, "")
56
+ // eslint-disable-next-line no-control-regex
57
+ .replace(/[<>:"|?*\x00-\x1f-]/g, "_")
58
+ .replace(/\s+/g, "")
59
+ .trim();
60
+
61
+ // 在 CJK 与 ASCII 字符交界处插入 _
62
+ cleaned = cleaned.replace(/(.)(?=.)/g, (_, cur, offset, str) => {
63
+ const next = str[offset + 1];
64
+ if (CJK.test(cur) && ASCII.test(next)) return cur + "_";
65
+ if (ASCII.test(cur) && CJK.test(next)) return cur + "_";
66
+ return cur;
67
+ });
68
+
69
+ const final = cleaned + ext;
70
+ const MAX = 120;
71
+ if (final.length <= MAX) return final;
72
+ const trimmedStem = cleaned.slice(0, MAX - ext.length);
73
+ return trimmedStem + ext;
74
+ }
75
+
39
76
  function saveBufferToMediaDir(
40
77
  buffer: Buffer,
41
78
  contentType: string,
42
79
  ext: string,
43
80
  log?: (...args: any[]) => void,
81
+ originalName?: string,
44
82
  ): PalzMediaInfo | null {
45
83
  try {
46
84
  fs.mkdirSync(MEDIA_DIR, { recursive: true });
47
- const filePath = path.join(
48
- MEDIA_DIR,
49
- `palz_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`,
50
- );
85
+ let fileName: string;
86
+ if (originalName && originalName.trim()) {
87
+ const safe = sanitizeFileName(originalName);
88
+ fileName = safe || `palz_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
89
+ } else {
90
+ fileName = `palz_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
91
+ }
92
+ const filePath = path.join(MEDIA_DIR, fileName);
51
93
  fs.writeFileSync(filePath, buffer);
52
94
  const placeholder = isImageMime(contentType) ? "<media:image>" : `<media:file:${ext}>`;
53
95
  log?.(
54
- `palz-media: [saveToMediaDir] 成功: path=${filePath} size=${buffer.length}bytes mime=${contentType}`,
96
+ `palz-media: [saveToMediaDir] 成功: path=${filePath} size=${buffer.length}bytes mime=${contentType}${originalName ? ` originalName=${originalName}` : ""}`,
55
97
  );
56
98
  return { path: filePath, contentType, placeholder };
57
99
  } catch (err: any) {
@@ -107,6 +149,24 @@ function extFromUrl(url: string): string {
107
149
  return "";
108
150
  }
109
151
 
152
+ /**
153
+ * 从 URL 中提取原始文件名(对 URL 编码做解码),失败返回空串。
154
+ */
155
+ function fileNameFromUrl(url: string): string {
156
+ try {
157
+ const pathname = new URL(url).pathname;
158
+ const base = path.basename(pathname);
159
+ if (!base) return "";
160
+ try {
161
+ return decodeURIComponent(base);
162
+ } catch {
163
+ return base;
164
+ }
165
+ } catch {
166
+ return "";
167
+ }
168
+ }
169
+
110
170
  function extToMime(ext: string): string {
111
171
  for (const [mime, e] of Object.entries(MIME_TO_EXT)) {
112
172
  if (e === ext) return mime;
@@ -120,7 +180,7 @@ function extToMime(ext: string): string {
120
180
  async function fetchUrlToBuffer(
121
181
  url: string,
122
182
  log?: (...args: any[]) => void,
123
- ): Promise<{ buffer: Buffer; contentType: string; ext: string } | null> {
183
+ ): Promise<{ buffer: Buffer; contentType: string; ext: string; originalName?: string } | null> {
124
184
  if (url.startsWith("data:")) {
125
185
  const match = url.match(/^data:([^;]+);base64,(.+)$/);
126
186
  if (!match) {
@@ -148,7 +208,8 @@ async function fetchUrlToBuffer(
148
208
  const ext = urlExt || mimeToExt(contentType);
149
209
  const finalContentType =
150
210
  contentType === "application/octet-stream" && urlExt ? extToMime(urlExt) : contentType;
151
- return { buffer, contentType: finalContentType, ext };
211
+ const originalName = fileNameFromUrl(url) || undefined;
212
+ return { buffer, contentType: finalContentType, ext, originalName };
152
213
  } catch (err: any) {
153
214
  log?.(`palz-media: [fetchUrl] HTTP下载异常: ${err.message}`);
154
215
  return null;
@@ -200,7 +261,13 @@ export async function resolvePalzMediaList(
200
261
 
201
262
  const fetched = await fetchUrlToBuffer(url, log);
202
263
  if (fetched) {
203
- const info = saveBufferToMediaDir(fetched.buffer, fetched.contentType, fetched.ext, log);
264
+ const info = saveBufferToMediaDir(
265
+ fetched.buffer,
266
+ fetched.contentType,
267
+ fetched.ext,
268
+ log,
269
+ fetched.originalName,
270
+ );
204
271
  if (info) {
205
272
  results.push(info);
206
273
  log?.(`palz-media: [resolve] 第 ${i + 1} 完成: ${JSON.stringify(info)}`);
package/src/monitor.ts CHANGED
@@ -49,7 +49,13 @@ export async function monitorPalzProvider(params: MonitorPalzParams): Promise<vo
49
49
  }
50
50
  if (currentWs) {
51
51
  currentWs.removeAllListeners();
52
- currentWs.close();
52
+ if (currentWs.readyState === WebSocket.OPEN || currentWs.readyState === WebSocket.CLOSING) {
53
+ currentWs.close();
54
+ } else if (currentWs.readyState === WebSocket.CONNECTING) {
55
+ // WebSocket 还在连接中,等 open 后再关,或者连接失败自动清理
56
+ currentWs.once("open", () => currentWs?.close());
57
+ currentWs.once("error", () => {}); // 防止 unhandled error
58
+ }
53
59
  currentWs = null;
54
60
  }
55
61
  };
package/src/types.ts CHANGED
@@ -43,6 +43,12 @@ export interface PalzMessageEvent {
43
43
  group_kind?: number;
44
44
  /** 资源 ID,由 IM 下发 */
45
45
  resource_id?: string;
46
+ /** Lobster 名称,由 IM 下发 */
47
+ lobster_name?: string;
48
+ /** Lobster 类型,由 IM 下发 */
49
+ lobster_type?: string;
50
+ /** 发送者账号类型,由 IM 下发 */
51
+ sender_account_type?: string;
46
52
  /** W3C Trace Context traceparent,由 IM 上游传递 */
47
53
  traceparent?: string;
48
54
  }