palz-connector 1.2.9 → 1.3.1
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 +13 -1
- package/src/channel.ts +1 -1
- package/src/media.ts +38 -1
- package/src/oss.ts +3 -2
- package/src/outbound.ts +14 -2
- package/src/reply-dispatcher.ts +3 -1
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { getPalzRuntime } from "./runtime.js";
|
|
|
14
14
|
import { resolvePalzAccount } from "./config.js";
|
|
15
15
|
import { tryClaimMessage } from "./dedup.js";
|
|
16
16
|
import { createPalzReplyDispatcher } from "./reply-dispatcher.js";
|
|
17
|
-
import { resolvePalzMediaList } from "./media.js";
|
|
17
|
+
import { resolvePalzMediaList, resolveMediaLocalRoots } from "./media.js";
|
|
18
18
|
import { tracer, trace, context, SpanStatusCode } from "./tracing.js";
|
|
19
19
|
import type { PalzMessageEvent, OpenAIContent, ContentPart, TextContentPart, PalzMediaInfo } from "./types.js";
|
|
20
20
|
|
|
@@ -533,6 +533,17 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
|
|
|
533
533
|
CommandAuthorized: commandAuthorized,
|
|
534
534
|
OriginatingChannel: "palz-connector",
|
|
535
535
|
OriginatingTo: palzTo,
|
|
536
|
+
GroupSystemPrompt: [
|
|
537
|
+
"## File Sharing",
|
|
538
|
+
"When you create or generate a file that the user requested (documents, images, presentations, spreadsheets, archives, etc.), you MUST include a MEDIA: token in your reply so the file gets delivered to the user.",
|
|
539
|
+
"Always write your normal text reply first (description, summary, or explanation), then append the MEDIA: line(s) at the end.",
|
|
540
|
+
"Format: place `MEDIA: <absolute-file-path>` on its own line at the end of your reply.",
|
|
541
|
+
"Example:",
|
|
542
|
+
" Here is the presentation you requested. It includes 5 slides covering the main topics.",
|
|
543
|
+
" MEDIA: /path/to/workspace/output.pptx",
|
|
544
|
+
"Without the MEDIA: line, the user only sees text and cannot download the file.",
|
|
545
|
+
"Multiple files: one MEDIA: line per file.",
|
|
546
|
+
].join("\n"),
|
|
536
547
|
...mediaPayload,
|
|
537
548
|
});
|
|
538
549
|
const step6bOutput = `[STEP 6b 输出] finalized context keys=[${Object.keys(ctx).join(",")}] CommandAuthorized=${ctx.CommandAuthorized}`;
|
|
@@ -568,6 +579,7 @@ async function _dispatchPalzMessageInner(params: HandlePalzMessageParams): Promi
|
|
|
568
579
|
msgId: msg.msg_id,
|
|
569
580
|
msgType: msg.msg_type,
|
|
570
581
|
groupId,
|
|
582
|
+
mediaLocalRoots: resolveMediaLocalRoots(effectiveAgentId),
|
|
571
583
|
});
|
|
572
584
|
|
|
573
585
|
// STEP 6d: 分发消息给 AI
|
package/src/channel.ts
CHANGED
|
@@ -72,7 +72,7 @@ export const palzPlugin = {
|
|
|
72
72
|
|
|
73
73
|
agentPrompt: {
|
|
74
74
|
messageToolHints: () => [
|
|
75
|
-
"- Palz targeting:
|
|
75
|
+
"- Palz targeting: DO NOT set `target` — always omit it so the system auto-infers the correct conversation. Never use sender_id or conversation_id alone as target. If you must specify an explicit target, the only valid format is `<senderId>:<conversationId>`.",
|
|
76
76
|
],
|
|
77
77
|
},
|
|
78
78
|
|
package/src/media.ts
CHANGED
|
@@ -18,6 +18,23 @@ import { uploadFileToOss, uploadBufferToOss } from "./oss.js";
|
|
|
18
18
|
|
|
19
19
|
/** OpenClaw 允许访问的媒体目录 */
|
|
20
20
|
const MEDIA_DIR = path.join(os.homedir(), ".openclaw", "media");
|
|
21
|
+
const STATE_DIR = path.join(os.homedir(), ".openclaw");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 根据 agentId 构造媒体文件搜索根路径列表,
|
|
25
|
+
* 与 OpenClaw 的 getAgentScopedMediaLocalRoots 逻辑对齐。
|
|
26
|
+
*/
|
|
27
|
+
export function resolveMediaLocalRoots(agentId?: string): string[] {
|
|
28
|
+
const roots = [MEDIA_DIR, path.join(STATE_DIR, "workspace"), path.join(STATE_DIR, "sandboxes")];
|
|
29
|
+
if (agentId?.trim()) {
|
|
30
|
+
// workspace-{agentId} 形式(非 default agent,或 main 作为非 default 时)
|
|
31
|
+
const workspaceDirById = path.join(STATE_DIR, `workspace-${agentId}`);
|
|
32
|
+
if (!roots.includes(workspaceDirById)) {
|
|
33
|
+
roots.push(workspaceDirById);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return roots;
|
|
37
|
+
}
|
|
21
38
|
|
|
22
39
|
function saveBufferToMediaDir(
|
|
23
40
|
buffer: Buffer,
|
|
@@ -205,11 +222,13 @@ export async function resolvePalzMediaList(
|
|
|
205
222
|
export async function loadMediaAsOssUrl(
|
|
206
223
|
mediaUrl: string,
|
|
207
224
|
log?: (...args: any[]) => void,
|
|
225
|
+
localRoots?: readonly string[],
|
|
208
226
|
): Promise<string | null> {
|
|
209
227
|
log?.(`palz-media: [loadAsOssUrl] 输入: url=${mediaUrl.slice(0, 200)}`);
|
|
210
228
|
|
|
211
229
|
if (
|
|
212
230
|
mediaUrl.startsWith("https://oss.csaiagent.com/") ||
|
|
231
|
+
mediaUrl.startsWith("https://claw.csaiagent.com") ||
|
|
213
232
|
mediaUrl.startsWith("https://cstv-data.oss-cn-beijing.aliyuncs.com/")
|
|
214
233
|
) {
|
|
215
234
|
log?.(`palz-media: [loadAsOssUrl] 已是OSS链接, 直接返回`);
|
|
@@ -241,7 +260,25 @@ export async function loadMediaAsOssUrl(
|
|
|
241
260
|
|
|
242
261
|
// 本地文件路径(绝对或相对)→ 上传到 OSS
|
|
243
262
|
const rawPath = mediaUrl.replace(/^MEDIA:/, "");
|
|
244
|
-
let filePath = path.isAbsolute(rawPath) ? rawPath :
|
|
263
|
+
let filePath = path.isAbsolute(rawPath) ? rawPath : "";
|
|
264
|
+
|
|
265
|
+
// 相对路径:在 localRoots 中逐个查找
|
|
266
|
+
if (!filePath && localRoots && localRoots.length > 0) {
|
|
267
|
+
for (const root of localRoots) {
|
|
268
|
+
const candidate = path.resolve(root, rawPath);
|
|
269
|
+
if (fs.existsSync(candidate)) {
|
|
270
|
+
log?.(`palz-media: [loadAsOssUrl] 在localRoot找到: root=${root} → ${candidate}`);
|
|
271
|
+
filePath = candidate;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 兜底:cwd 解析
|
|
278
|
+
if (!filePath) {
|
|
279
|
+
filePath = path.resolve(rawPath);
|
|
280
|
+
}
|
|
281
|
+
|
|
245
282
|
if (!fs.existsSync(filePath)) {
|
|
246
283
|
const fallback = path.join(MEDIA_DIR, path.basename(filePath));
|
|
247
284
|
if (fs.existsSync(fallback)) {
|
package/src/oss.ts
CHANGED
|
@@ -52,12 +52,13 @@ export async function uploadFileToOss(
|
|
|
52
52
|
try {
|
|
53
53
|
const client = getOssClient();
|
|
54
54
|
const ext = path.extname(localPath) || ".png";
|
|
55
|
-
const
|
|
55
|
+
const baseName = path.basename(localPath, ext);
|
|
56
|
+
const objectName = `${OSS_UPLOAD_PREFIX}/${baseName}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
|
|
56
57
|
|
|
57
58
|
const fileBuffer = fs.readFileSync(localPath);
|
|
58
59
|
await client.put(objectName, fileBuffer);
|
|
59
60
|
|
|
60
|
-
const fileUrl = `https://${OSS_CUSTOM_DOMAIN}/${objectName}`;
|
|
61
|
+
const fileUrl = `https://${OSS_CUSTOM_DOMAIN}/${objectName.split('/').map(s => encodeURIComponent(s)).join('/')}`;
|
|
61
62
|
log?.(`palz-oss: [upload] 成功: objectName=${objectName} url=${fileUrl}`);
|
|
62
63
|
return fileUrl;
|
|
63
64
|
} catch (err: any) {
|
package/src/outbound.ts
CHANGED
|
@@ -14,6 +14,18 @@ import type { ContentPart, TextContentPart, OpenAIContent } from "./types.js";
|
|
|
14
14
|
export const palzOutbound = {
|
|
15
15
|
deliveryMode: "direct" as const,
|
|
16
16
|
|
|
17
|
+
resolveTarget: (params: { to?: string; mode?: string }) => {
|
|
18
|
+
const to = params.to?.trim();
|
|
19
|
+
if (!to) {
|
|
20
|
+
return { ok: false as const, error: new Error("Palz target is required. Format: <senderId>:<conversationId> or chat:<conversationId>") };
|
|
21
|
+
}
|
|
22
|
+
// Must contain ":" — bare sender_id or bare conversation_id is invalid
|
|
23
|
+
if (!to.includes(":")) {
|
|
24
|
+
return { ok: false as const, error: new Error(`Invalid Palz target "${to}": must be <senderId>:<conversationId> or chat:<conversationId>. A bare ID without ":" is not a valid target.`) };
|
|
25
|
+
}
|
|
26
|
+
return { ok: true as const, to };
|
|
27
|
+
},
|
|
28
|
+
|
|
17
29
|
sendText: async (ctx: any) => {
|
|
18
30
|
const { cfg, to, text, accountId } = ctx;
|
|
19
31
|
const log = typeof ctx.log === "function" ? ctx.log : console.log;
|
|
@@ -37,7 +49,7 @@ export const palzOutbound = {
|
|
|
37
49
|
},
|
|
38
50
|
|
|
39
51
|
sendMedia: async (ctx: any) => {
|
|
40
|
-
const { cfg, to, text, mediaUrl, accountId } = ctx;
|
|
52
|
+
const { cfg, to, text, mediaUrl, accountId, mediaLocalRoots } = ctx;
|
|
41
53
|
const log = typeof ctx.log === "function" ? ctx.log : console.log;
|
|
42
54
|
|
|
43
55
|
const account = resolvePalzAccount({ cfg, accountId });
|
|
@@ -52,7 +64,7 @@ export const palzOutbound = {
|
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
if (mediaUrl) {
|
|
55
|
-
const ossUrl = await loadMediaAsOssUrl(mediaUrl, log);
|
|
67
|
+
const ossUrl = await loadMediaAsOssUrl(mediaUrl, log, mediaLocalRoots);
|
|
56
68
|
if (ossUrl) {
|
|
57
69
|
contentParts.push({ type: "file", file_url: { url: ossUrl } });
|
|
58
70
|
log(`palz-outbound: [sendMedia] 媒体转换成功: ossUrl=${ossUrl}`);
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface CreatePalzReplyDispatcherParams {
|
|
|
43
43
|
msgId: string;
|
|
44
44
|
msgType?: string;
|
|
45
45
|
groupId?: string;
|
|
46
|
+
mediaLocalRoots?: readonly string[];
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParams) {
|
|
@@ -59,6 +60,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
|
|
|
59
60
|
msgId,
|
|
60
61
|
msgType,
|
|
61
62
|
groupId,
|
|
63
|
+
mediaLocalRoots,
|
|
62
64
|
} = params;
|
|
63
65
|
|
|
64
66
|
const log = typeof runtime?.log === "function" ? runtime.log : console.log;
|
|
@@ -125,7 +127,7 @@ export function createPalzReplyDispatcher(params: CreatePalzReplyDispatcherParam
|
|
|
125
127
|
|
|
126
128
|
for (let i = 0; i < mediaUrls.length; i++) {
|
|
127
129
|
log(`${tag}: [DELIVER 媒体] ${i + 1}/${mediaUrls.length} url=${mediaUrls[i].slice(0, 200)}`);
|
|
128
|
-
const ossUrl = await loadMediaAsOssUrl(mediaUrls[i], log);
|
|
130
|
+
const ossUrl = await loadMediaAsOssUrl(mediaUrls[i], log, mediaLocalRoots);
|
|
129
131
|
if (ossUrl) {
|
|
130
132
|
contentParts.push({ type: "file", file_url: { url: ossUrl } });
|
|
131
133
|
log(`${tag}: [DELIVER 媒体转换成功] ${i + 1}/${mediaUrls.length} ossUrl=${ossUrl}`);
|