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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "palz-connector",
3
3
  "name": "Palz Connector Channel",
4
- "version": "1.2.9",
4
+ "version": "1.3.1",
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.9",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Palz IM 接入 OpenClaw — 模块化架构,基于 OpenClaw Runtime 消息管道",
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: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `<senderId>:<conversationId>`.",
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 : path.resolve(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 objectName = `${OSS_UPLOAD_PREFIX}/${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
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}`);
@@ -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}`);