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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/bot.ts +9 -0
- package/src/media.ts +75 -8
- package/src/monitor.ts +7 -1
- package/src/types.ts +6 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
}
|