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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/palz-connector.prod.config.json +2 -2
- package/palz-connector.staging.config.json +2 -2
- package/src/bot.ts +19 -35
- package/src/media.ts +177 -69
- package/src/outbound.ts +2 -2
- package/src/reply-dispatcher.ts +1 -1
- package/src/targets.ts +1 -1
- package/src/types.ts +4 -4
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"enabled": true,
|
|
3
|
-
"streamUrl": "wss://claw-server.
|
|
4
|
-
"apiBaseUrl": "https://claw-server.
|
|
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.
|
|
4
|
-
"apiBaseUrl": "https://claw-server.
|
|
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
|
|
181
|
-
Array.isArray(content) &&
|
|
182
|
-
|
|
183
|
-
|
|
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})
|
|
187
|
+
log(`${tag}: [STEP 1 解析] plainText="${plainText}" (len=${plainText.length}) hasMedia=${hasMedia} mediaCount=${mediaCount}`);
|
|
187
188
|
|
|
188
|
-
if (!plainText && !
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
62
|
-
log?.(`palz-media: [resolve] 输入: parts=${content.length} imageParts=${imageParts.length}`);
|
|
157
|
+
const mediaUrls: string[] = [];
|
|
63
158
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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?.(
|
|
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
|
-
|
|
123
|
-
|
|
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:(
|
|
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?.(
|
|
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
|
-
|
|
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?.(
|
|
255
|
+
log?.(
|
|
256
|
+
`palz-media: [loadAsOssUrl] 本地文件 → OSS: path=${filePath} ossUrl=${ossUrl}`,
|
|
257
|
+
);
|
|
156
258
|
return ossUrl;
|
|
157
259
|
} catch (err: any) {
|
|
158
|
-
log?.(
|
|
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 =
|
|
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
|
|
278
|
+
const urlExt = extFromUrl(mediaUrl);
|
|
279
|
+
const ext = urlExt || mimeToExt(contentType);
|
|
174
280
|
const ossUrl = await uploadBufferToOss(buffer, ext, log);
|
|
175
|
-
log?.(
|
|
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: "
|
|
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}` });
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -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: "
|
|
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:._-]
|
|
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
|
|
9
|
-
type: "
|
|
10
|
-
|
|
8
|
+
export type FileUrlContentPart = {
|
|
9
|
+
type: "file";
|
|
10
|
+
file_url: { url: string };
|
|
11
11
|
};
|
|
12
|
-
export type ContentPart = TextContentPart |
|
|
12
|
+
export type ContentPart = TextContentPart | FileUrlContentPart;
|
|
13
13
|
export type OpenAIContent = string | ContentPart[];
|
|
14
14
|
|
|
15
15
|
// ============ Palz IM 消息事件 ============
|