palz-connector 1.2.2 → 1.2.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.2.2",
4
+ "version": "1.2.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.2.2",
3
+ "version": "1.2.3",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "enabled": true,
3
- "streamUrl": "wss://claw-server.uniclaw.tech/ws/bot",
4
- "apiBaseUrl": "https://claw-server.uniclaw.tech/api",
3
+ "streamUrl": "wss://claw-server.csagentai.com/ws/bot",
4
+ "apiBaseUrl": "https://claw-server.csagentai.com/api",
5
5
  "sessionTimeout": 1800000
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "enabled": true,
3
- "streamUrl": "wss://claw-server.csagentai.com/ws/bot",
4
- "apiBaseUrl": "https://claw-server.csagentai.com/api",
3
+ "streamUrl": "wss://claw-server.csjkagent.com/ws/bot",
4
+ "apiBaseUrl": "https://claw-server.csjkagent.com/api",
5
5
  "sessionTimeout": 1800000
6
6
  }
package/src/bot.ts CHANGED
@@ -177,16 +177,17 @@ export async function handlePalzMessage(params: HandlePalzMessageParams): Promis
177
177
  }
178
178
 
179
179
  const plainText = extractPlainText(content).trim();
180
- const hasImages =
181
- Array.isArray(content) && content.some((p: ContentPart) => p.type === "image_url");
182
- const imageCount = Array.isArray(content)
183
- ? content.filter((p: ContentPart) => p.type === "image_url").length
180
+ const hasMedia =
181
+ Array.isArray(content) &&
182
+ content.some((p: ContentPart) => p.type === "file");
183
+ const mediaCount = Array.isArray(content)
184
+ ? content.filter((p: ContentPart) => p.type === "file").length
184
185
  : 0;
185
186
 
186
- log(`${tag}: [STEP 1 解析] plainText="${plainText}" (len=${plainText.length}) hasImages=${hasImages} imageCount=${imageCount}`);
187
+ log(`${tag}: [STEP 1 解析] plainText="${plainText}" (len=${plainText.length}) hasMedia=${hasMedia} mediaCount=${mediaCount}`);
187
188
 
188
- if (!plainText && !hasImages) {
189
- log(`${tag}: [STEP 1 跳过] 原因=无文本且无图片`);
189
+ if (!plainText && !hasMedia) {
190
+ log(`${tag}: [STEP 1 跳过] 原因=无文本且无媒体`);
190
191
  return;
191
192
  }
192
193
 
@@ -252,7 +253,10 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
252
253
  const peerId = isGroup ? `chat:${msg.conversation_id}` : `${msg.sender_id}:${msg.conversation_id}`;
253
254
 
254
255
  // STEP 4: 解析媒体
255
- log(`${tag}: [STEP 4/6 媒体解析] 输入: contentType=${typeof msg.content === "string" ? "string" : "array"} imageCount=${Array.isArray(msg.content) ? msg.content.filter((p: ContentPart) => p.type === "image_url").length : 0}`);
256
+ const mediaCount = Array.isArray(msg.content)
257
+ ? msg.content.filter((p: ContentPart) => p.type === "file").length
258
+ : 0;
259
+ log(`${tag}: [STEP 4/6 媒体解析] 输入: contentType=${typeof msg.content === "string" ? "string" : "array"} mediaCount=${mediaCount}`);
256
260
  const mediaList = await resolvePalzMediaList(msg.content, log);
257
261
  const mediaPayload = buildMediaPayload(mediaList);
258
262
  log(`${tag}: [STEP 4 输出] mediaList=${JSON.stringify(mediaList.map((m) => ({ path: m.path, contentType: m.contentType })))} mediaPayload=${JSON.stringify(mediaPayload)}`);
@@ -345,32 +349,6 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
345
349
  : undefined;
346
350
  log(`${tag}: [STEP 6b InboundHistory] count=${inboundHistory?.length ?? 0}`);
347
351
 
348
- const rawCtx = {
349
- Body: `(envelope, len=${combinedBody.length})`,
350
- BodyForAgent: messageBody.slice(0, 100),
351
- InboundHistory: inboundHistory ? `(${inboundHistory.length} entries)` : undefined,
352
- RawBody: plainText.slice(0, 100),
353
- CommandBody: plainText.slice(0, 100),
354
- From: palzFrom,
355
- To: palzTo,
356
- SessionKey: route.sessionKey,
357
- AccountId: route.accountId,
358
- ChatType: chatType,
359
- GroupSubject: isGroup ? msg.conversation_id : undefined,
360
- SenderId: msg.sender_id,
361
- SenderName: senderName,
362
- Provider: "palz-connector",
363
- Surface: "palz-connector",
364
- MessageSid: msg.msg_id,
365
- Timestamp: Date.now(),
366
- WasMentioned: wasMentioned,
367
- CommandAuthorized: commandAuthorized,
368
- OriginatingChannel: "palz-connector",
369
- OriginatingTo: palzTo,
370
- ...mediaPayload,
371
- };
372
- log(`${tag}: [STEP 6b inbound context] 输入: ${JSON.stringify(rawCtx)}`);
373
-
374
352
  const ctx = core.channel.reply.finalizeInboundContext({
375
353
  Body: combinedBody,
376
354
  BodyForAgent: messageBody,
@@ -396,7 +374,11 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
396
374
  ...mediaPayload,
397
375
  });
398
376
  log(`${tag}: [STEP 6b 输出] finalized context keys=[${Object.keys(ctx).join(",")}] CommandAuthorized=${ctx.CommandAuthorized}`);
399
-
377
+ ctx.metadata = {
378
+ ...ctx.metadata,
379
+ traceId: msg.msg_id,
380
+ source: "palz-connector"
381
+ };
400
382
  // STEP 6c: 创建回复分发器
401
383
  const dispatcherParams = {
402
384
  accountId,
@@ -421,6 +403,8 @@ async function dispatchPalzMessage(params: HandlePalzMessageParams): Promise<voi
421
403
  });
422
404
 
423
405
  // STEP 6d: 分发消息给 AI
406
+ // channel registry 守卫已在 index.ts 中通过 defineProperty 安装,
407
+ // 每次读取 state.registry 时会自动注入 palz-connector channel。
424
408
  log(`${tag}: [STEP 6d AI分发] 开始 session=${route.sessionKey} stream=${useStream}`);
425
409
  const dispatchStartMs = Date.now();
426
410
 
package/src/media.ts CHANGED
@@ -1,22 +1,24 @@
1
1
  /**
2
2
  * Palz Connector 媒体处理
3
3
  *
4
- * 将 IM 消息中的图片上传到 OSS,返回公网 URL,
4
+ * 将 IM 消息中的图片/文件上传到 OSS,返回公网 URL,
5
5
  * 供 OpenClaw Runtime 作为媒体附件处理。
6
+ * 支持图片、PDF、DOCX、MD 等各类文件。
6
7
  */
7
8
 
8
9
  import fs from "fs";
9
10
  import path from "path";
10
11
  import os from "os";
11
- import type { OpenAIContent, ContentPart, ImageUrlContentPart, PalzMediaInfo } from "./types.js";
12
+ import type {
13
+ OpenAIContent,
14
+ FileUrlContentPart,
15
+ PalzMediaInfo,
16
+ } from "./types.js";
12
17
  import { uploadFileToOss, uploadBufferToOss } from "./oss.js";
13
18
 
14
19
  /** OpenClaw 允许访问的媒体目录 */
15
20
  const MEDIA_DIR = path.join(os.homedir(), ".openclaw", "media");
16
21
 
17
- /**
18
- * 将 Buffer 保存到 OpenClaw 媒体目录,返回 PalzMediaInfo。
19
- */
20
22
  function saveBufferToMediaDir(
21
23
  buffer: Buffer,
22
24
  contentType: string,
@@ -25,29 +27,123 @@ function saveBufferToMediaDir(
25
27
  ): PalzMediaInfo | null {
26
28
  try {
27
29
  fs.mkdirSync(MEDIA_DIR, { recursive: true });
28
- const filePath = path.join(MEDIA_DIR, `palz_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`);
30
+ const filePath = path.join(
31
+ MEDIA_DIR,
32
+ `palz_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`,
33
+ );
29
34
  fs.writeFileSync(filePath, buffer);
30
- log?.(`palz-media: [saveToMediaDir] 成功: path=${filePath} size=${buffer.length}bytes mime=${contentType}`);
31
- return { path: filePath, contentType, placeholder: "<media:image>" };
35
+ const placeholder = isImageMime(contentType) ? "<media:image>" : `<media:file:${ext}>`;
36
+ log?.(
37
+ `palz-media: [saveToMediaDir] 成功: path=${filePath} size=${buffer.length}bytes mime=${contentType}`,
38
+ );
39
+ return { path: filePath, contentType, placeholder };
32
40
  } catch (err: any) {
33
41
  log?.(`palz-media: [saveToMediaDir] 失败: error=${err.message}`);
34
42
  return null;
35
43
  }
36
44
  }
37
45
 
46
+ function isImageMime(mime: string): boolean {
47
+ return mime.startsWith("image/");
48
+ }
49
+
50
+ const MIME_TO_EXT: Record<string, string> = {
51
+ "image/jpeg": ".jpg",
52
+ "image/png": ".png",
53
+ "image/gif": ".gif",
54
+ "image/webp": ".webp",
55
+ "image/bmp": ".bmp",
56
+ "image/svg+xml": ".svg",
57
+ "application/pdf": ".pdf",
58
+ "application/msword": ".doc",
59
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
60
+ "application/vnd.ms-excel": ".xls",
61
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
62
+ "application/vnd.ms-powerpoint": ".ppt",
63
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
64
+ "text/markdown": ".md",
65
+ "text/plain": ".txt",
66
+ "text/csv": ".csv",
67
+ "text/html": ".html",
68
+ "application/json": ".json",
69
+ "application/zip": ".zip",
70
+ "application/x-tar": ".tar",
71
+ "application/gzip": ".gz",
72
+ "audio/mpeg": ".mp3",
73
+ "audio/wav": ".wav",
74
+ "audio/ogg": ".ogg",
75
+ "video/mp4": ".mp4",
76
+ "video/webm": ".webm",
77
+ "application/octet-stream": ".bin",
78
+ };
79
+
38
80
  function mimeToExt(mime: string): string {
39
- const map: Record<string, string> = {
40
- "image/jpeg": ".jpg",
41
- "image/png": ".png",
42
- "image/gif": ".gif",
43
- "image/webp": ".webp",
44
- "image/bmp": ".bmp",
45
- };
46
- return map[mime] || ".png";
81
+ return MIME_TO_EXT[mime] || ".bin";
82
+ }
83
+
84
+ function extFromUrl(url: string): string {
85
+ try {
86
+ const pathname = new URL(url).pathname;
87
+ const ext = path.extname(pathname).toLowerCase();
88
+ if (ext && ext.length <= 10) return ext;
89
+ } catch {}
90
+ return "";
91
+ }
92
+
93
+ function extToMime(ext: string): string {
94
+ for (const [mime, e] of Object.entries(MIME_TO_EXT)) {
95
+ if (e === ext) return mime;
96
+ }
97
+ return "application/octet-stream";
98
+ }
99
+
100
+ /**
101
+ * 从 URL 获取文件(data URL / HTTP URL),返回 buffer + contentType + ext。
102
+ */
103
+ async function fetchUrlToBuffer(
104
+ url: string,
105
+ log?: (...args: any[]) => void,
106
+ ): Promise<{ buffer: Buffer; contentType: string; ext: string } | null> {
107
+ if (url.startsWith("data:")) {
108
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
109
+ if (!match) {
110
+ log?.(`palz-media: [fetchUrl] data URL 格式不匹配`);
111
+ return null;
112
+ }
113
+ const mimeType = match[1];
114
+ const base64Data = match[2];
115
+ const ext = mimeToExt(mimeType);
116
+ const buffer = Buffer.from(base64Data, "base64");
117
+ return { buffer, contentType: mimeType, ext };
118
+ }
119
+
120
+ if (url.startsWith("http://") || url.startsWith("https://")) {
121
+ try {
122
+ const resp = await fetch(url);
123
+ if (!resp.ok) {
124
+ log?.(`palz-media: [fetchUrl] HTTP下载失败: status=${resp.status}`);
125
+ return null;
126
+ }
127
+ const contentType =
128
+ resp.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream";
129
+ const buffer = Buffer.from(await resp.arrayBuffer());
130
+ const urlExt = extFromUrl(url);
131
+ const ext = urlExt || mimeToExt(contentType);
132
+ const finalContentType =
133
+ contentType === "application/octet-stream" && urlExt ? extToMime(urlExt) : contentType;
134
+ return { buffer, contentType: finalContentType, ext };
135
+ } catch (err: any) {
136
+ log?.(`palz-media: [fetchUrl] HTTP下载异常: ${err.message}`);
137
+ return null;
138
+ }
139
+ }
140
+
141
+ log?.(`palz-media: [fetchUrl] 无法识别的URL格式: ${url.slice(0, 200)}`);
142
+ return null;
47
143
  }
48
144
 
49
145
  /**
50
- * 从 OpenAI Content 中提取所有图片,上传到 OSS 并返回公网 URL。
146
+ * 从 OpenAI Content 中提取所有 type:"file" 媒体,下载并保存到本地。
51
147
  */
52
148
  export async function resolvePalzMediaList(
53
149
  content: OpenAIContent,
@@ -58,60 +154,53 @@ export async function resolvePalzMediaList(
58
154
  return [];
59
155
  }
60
156
 
61
- const imageParts = content.filter((p: ContentPart): p is ImageUrlContentPart => p.type === "image_url" && (p as ImageUrlContentPart).image_url?.url !== undefined);
62
- log?.(`palz-media: [resolve] 输入: parts=${content.length} imageParts=${imageParts.length}`);
157
+ const mediaUrls: string[] = [];
63
158
 
64
- const results: PalzMediaInfo[] = [];
65
- for (let i = 0; i < imageParts.length; i++) {
66
- const part = imageParts[i];
67
- const url = part.image_url!.url;
68
- const urlType = url.startsWith("data:") ? "data-url" : url.startsWith("http") ? "http-url" : "unknown";
69
- log?.(`palz-media: [resolve] 处理第 ${i + 1}/${imageParts.length} 个图片, type=${urlType} urlLen=${url.length}`);
70
-
71
- let info: PalzMediaInfo | null = null;
72
-
73
- if (url.startsWith("data:")) {
74
- // data URL → 解码 → 保存到 OpenClaw 媒体目录
75
- const match = url.match(/^data:(image\/[^;]+);base64,(.+)$/);
76
- if (match) {
77
- const mimeType = match[1];
78
- const base64Data = match[2];
79
- const ext = mimeToExt(mimeType);
80
- const buffer = Buffer.from(base64Data, "base64");
81
- info = saveBufferToMediaDir(buffer, mimeType, ext, log);
82
- }
83
- } else if (url.startsWith("http://") || url.startsWith("https://")) {
84
- // HTTP URL → 下载 → 保存到 OpenClaw 媒体目录
85
- try {
86
- const resp = await fetch(url);
87
- if (resp.ok) {
88
- const contentType = resp.headers.get("content-type")?.split(";")[0]?.trim() || "image/png";
89
- const buffer = Buffer.from(await resp.arrayBuffer());
90
- const ext = mimeToExt(contentType);
91
- info = saveBufferToMediaDir(buffer, contentType, ext, log);
92
- } else {
93
- log?.(`palz-media: [resolve] HTTP下载失败: status=${resp.status}`);
94
- }
95
- } catch (err: any) {
96
- log?.(`palz-media: [resolve] HTTP下载异常: ${err.message}`);
159
+ for (const part of content) {
160
+ if (part.type === "file") {
161
+ const filePart = part as FileUrlContentPart;
162
+ if (filePart.file_url?.url) {
163
+ mediaUrls.push(filePart.file_url.url);
97
164
  }
98
165
  }
166
+ }
167
+
168
+ log?.(
169
+ `palz-media: [resolve] 输入: parts=${content.length} fileParts=${mediaUrls.length}`,
170
+ );
99
171
 
100
- if (info) {
101
- results.push(info);
102
- log?.(`palz-media: [resolve] ${i + 1} 完成: ${JSON.stringify(info)}`);
103
- } else {
104
- log?.(`palz-media: [resolve] 第 ${i + 1} 失败`);
172
+ const results: PalzMediaInfo[] = [];
173
+ for (let i = 0; i < mediaUrls.length; i++) {
174
+ const url = mediaUrls[i];
175
+ const urlType = url.startsWith("data:")
176
+ ? "data-url"
177
+ : url.startsWith("http")
178
+ ? "http-url"
179
+ : "unknown";
180
+ log?.(
181
+ `palz-media: [resolve] 处理第 ${i + 1}/${mediaUrls.length} 个媒体, urlType=${urlType} urlLen=${url.length}`,
182
+ );
183
+
184
+ const fetched = await fetchUrlToBuffer(url, log);
185
+ if (fetched) {
186
+ const info = saveBufferToMediaDir(fetched.buffer, fetched.contentType, fetched.ext, log);
187
+ if (info) {
188
+ results.push(info);
189
+ log?.(`palz-media: [resolve] 第 ${i + 1} 完成: ${JSON.stringify(info)}`);
190
+ continue;
191
+ }
105
192
  }
193
+ log?.(`palz-media: [resolve] 第 ${i + 1} 失败`);
106
194
  }
107
195
 
108
- log?.(`palz-media: [resolve] 输出: 共解析 ${results.length}/${imageParts.length} 个媒体文件`);
196
+ log?.(
197
+ `palz-media: [resolve] 输出: 共解析 ${results.length}/${mediaUrls.length} 个媒体文件`,
198
+ );
109
199
  return results;
110
200
  }
111
201
 
112
202
  /**
113
203
  * 将本地文件路径、data URL 或 HTTP URL 转为 OSS 公网链接(用于出站消息)。
114
- * 替代 loadMediaAsDataUrl,避免 base64 传输。
115
204
  */
116
205
  export async function loadMediaAsOssUrl(
117
206
  mediaUrl: string,
@@ -119,15 +208,17 @@ export async function loadMediaAsOssUrl(
119
208
  ): Promise<string | null> {
120
209
  log?.(`palz-media: [loadAsOssUrl] 输入: url=${mediaUrl.slice(0, 200)}`);
121
210
 
122
- // 已经是 OSS 链接,直接返回
123
- if (mediaUrl.startsWith("https://oss.csaiagent.com/") || mediaUrl.startsWith("https://cstv-data.oss-cn-beijing.aliyuncs.com/")) {
211
+ if (
212
+ mediaUrl.startsWith("https://oss.csaiagent.com/") ||
213
+ mediaUrl.startsWith("https://cstv-data.oss-cn-beijing.aliyuncs.com/")
214
+ ) {
124
215
  log?.(`palz-media: [loadAsOssUrl] 已是OSS链接, 直接返回`);
125
216
  return mediaUrl;
126
217
  }
127
218
 
128
219
  // data URL → 解码 → 上传到 OSS
129
220
  if (mediaUrl.startsWith("data:")) {
130
- const match = mediaUrl.match(/^data:(image\/[^;]+);base64,(.+)$/);
221
+ const match = mediaUrl.match(/^data:([^;]+);base64,(.+)$/);
131
222
  if (!match) {
132
223
  log?.(`palz-media: [loadAsOssUrl] data URL 格式不匹配`);
133
224
  return null;
@@ -138,7 +229,9 @@ export async function loadMediaAsOssUrl(
138
229
  try {
139
230
  const buffer = Buffer.from(base64Data, "base64");
140
231
  const ossUrl = await uploadBufferToOss(buffer, ext, log);
141
- log?.(`palz-media: [loadAsOssUrl] data URL → OSS: mime=${mimeType} bufSize=${buffer.length} ossUrl=${ossUrl}`);
232
+ log?.(
233
+ `palz-media: [loadAsOssUrl] data URL → OSS: mime=${mimeType} bufSize=${buffer.length} ossUrl=${ossUrl}`,
234
+ );
142
235
  return ossUrl;
143
236
  } catch (err: any) {
144
237
  log?.(`palz-media: [loadAsOssUrl] data URL上传OSS失败: ${err.message}`);
@@ -148,14 +241,25 @@ export async function loadMediaAsOssUrl(
148
241
 
149
242
  // 本地文件路径(绝对或相对)→ 上传到 OSS
150
243
  const rawPath = mediaUrl.replace(/^MEDIA:/, "");
151
- const filePath = path.isAbsolute(rawPath) ? rawPath : path.resolve(rawPath);
244
+ let filePath = path.isAbsolute(rawPath) ? rawPath : path.resolve(rawPath);
245
+ if (!fs.existsSync(filePath)) {
246
+ const fallback = path.join(MEDIA_DIR, path.basename(filePath));
247
+ if (fs.existsSync(fallback)) {
248
+ log?.(`palz-media: [loadAsOssUrl] 路径fallback: ${filePath} → ${fallback}`);
249
+ filePath = fallback;
250
+ }
251
+ }
152
252
  if (fs.existsSync(filePath)) {
153
253
  try {
154
254
  const ossUrl = await uploadFileToOss(filePath, log);
155
- log?.(`palz-media: [loadAsOssUrl] 本地文件 → OSS: path=${filePath} ossUrl=${ossUrl}`);
255
+ log?.(
256
+ `palz-media: [loadAsOssUrl] 本地文件 → OSS: path=${filePath} ossUrl=${ossUrl}`,
257
+ );
156
258
  return ossUrl;
157
259
  } catch (err: any) {
158
- log?.(`palz-media: [loadAsOssUrl] 本地文件上传OSS失败: ${filePath} error=${err.message}`);
260
+ log?.(
261
+ `palz-media: [loadAsOssUrl] 本地文件上传OSS失败: ${filePath} error=${err.message}`,
262
+ );
159
263
  return null;
160
264
  }
161
265
  }
@@ -168,11 +272,15 @@ export async function loadMediaAsOssUrl(
168
272
  log?.(`palz-media: [loadAsOssUrl] HTTP下载失败: status=${resp.status}`);
169
273
  return null;
170
274
  }
171
- const contentType = resp.headers.get("content-type")?.split(";")[0]?.trim() || "image/png";
275
+ const contentType =
276
+ resp.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream";
172
277
  const buffer = Buffer.from(await resp.arrayBuffer());
173
- const ext = mimeToExt(contentType);
278
+ const urlExt = extFromUrl(mediaUrl);
279
+ const ext = urlExt || mimeToExt(contentType);
174
280
  const ossUrl = await uploadBufferToOss(buffer, ext, log);
175
- log?.(`palz-media: [loadAsOssUrl] HTTP → OSS: size=${buffer.length} mime=${contentType} ossUrl=${ossUrl}`);
281
+ log?.(
282
+ `palz-media: [loadAsOssUrl] HTTP → OSS: size=${buffer.length} mime=${contentType} ossUrl=${ossUrl}`,
283
+ );
176
284
  return ossUrl;
177
285
  } catch (err: any) {
178
286
  log?.(`palz-media: [loadAsOssUrl] HTTP下载上传OSS异常: ${err.message}`);
package/src/outbound.ts CHANGED
@@ -39,10 +39,10 @@ export const palzOutbound = {
39
39
  sendMedia: async (ctx: any) => {
40
40
  const { cfg, to, text, mediaUrl, accountId } = ctx;
41
41
  const log = typeof ctx.log === "function" ? ctx.log : console.log;
42
- log(`palz-outbound: [sendMedia] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} mediaUrl="${(mediaUrl || "").slice(0, 200)}"`);
43
42
 
44
43
  const account = resolvePalzAccount({ cfg, accountId });
45
44
  const { senderId, conversationId, conversationType } = parsePalzTarget(to);
45
+ log(`palz-outbound: [sendMedia] 输入: to="${to}" accountId="${accountId}" textLen=${text?.length || 0} mediaUrl="${(mediaUrl || "").slice(0, 200)}"`);
46
46
  log(`palz-outbound: [sendMedia] 解析: senderId="${senderId}" conversationId="${conversationId}" conversationType="${conversationType}" botId=${account.config.botId}`);
47
47
 
48
48
  const contentParts: ContentPart[] = [];
@@ -54,7 +54,7 @@ export const palzOutbound = {
54
54
  if (mediaUrl) {
55
55
  const ossUrl = await loadMediaAsOssUrl(mediaUrl, log);
56
56
  if (ossUrl) {
57
- contentParts.push({ type: "image_url", image_url: { url: ossUrl } });
57
+ contentParts.push({ type: "file", file_url: { url: ossUrl } });
58
58
  log(`palz-outbound: [sendMedia] 媒体转换成功: ossUrl=${ossUrl}`);
59
59
  } else {
60
60
  contentParts.push({ type: "text", text: `\n📎 ${mediaUrl}` });
@@ -121,7 +121,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
121
121
  log(`${tag}: [DELIVER 媒体] ${i + 1}/${mediaUrls.length} url=${mediaUrls[i].slice(0, 200)}`);
122
122
  const ossUrl = await loadMediaAsOssUrl(mediaUrls[i], log);
123
123
  if (ossUrl) {
124
- contentParts.push({ type: "image_url", image_url: { url: ossUrl } });
124
+ contentParts.push({ type: "file", file_url: { url: ossUrl } });
125
125
  log(`${tag}: [DELIVER 媒体转换成功] ${i + 1}/${mediaUrls.length} ossUrl=${ossUrl}`);
126
126
  } else {
127
127
  contentParts.push({ type: "text", text: `\n📎 ${mediaUrls[i]}` });
package/src/targets.ts CHANGED
@@ -11,7 +11,7 @@ export function normalizePalzTarget(raw: string): string | undefined {
11
11
 
12
12
  export function looksLikePalzId(raw: string): boolean {
13
13
  const trimmed = raw.trim().replace(/^(palz-connector|palz):/i, "");
14
- return /^[\w:._-]+$/.test(trimmed);
14
+ return /^[\p{L}\p{N}\w:._-]+$/u.test(trimmed);
15
15
  }
16
16
 
17
17
  /**
package/src/types.ts CHANGED
@@ -5,11 +5,11 @@
5
5
  // ============ IM 消息格式(OpenAI Content 协议) ============
6
6
 
7
7
  export type TextContentPart = { type: "text"; text: string };
8
- export type ImageUrlContentPart = {
9
- type: "image_url";
10
- image_url: { url: string; detail?: string };
8
+ export type FileUrlContentPart = {
9
+ type: "file";
10
+ file_url: { url: string };
11
11
  };
12
- export type ContentPart = TextContentPart | ImageUrlContentPart;
12
+ export type ContentPart = TextContentPart | FileUrlContentPart;
13
13
  export type OpenAIContent = string | ContentPart[];
14
14
 
15
15
  // ============ Palz IM 消息事件 ============