@xgjktech/xg_cwork_im 1.10.0 → 1.10.2

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/README.md CHANGED
@@ -175,8 +175,10 @@ openclaw gateway restart
175
175
  // 可选:首条 AI 回复超时时间(毫秒),默认 30 分钟
176
176
  // 未配置时:30 * 60_000 = 1800000
177
177
  "firstReplyTimeoutMs": 1800000,
178
- // 可选:入站附件先下载到当前 Agent workspace 下该子目录,再以 file:// 交给 OpenClaw
178
+ // 可选:入站附件先下载到当前 Agent workspace 下该子目录,MediaPath 为相对路径或 /workspace/...(见下)
179
179
  "inboundMediaWorkspaceSubdir": "xg_im_inbound",
180
+ // 沙箱 Agent 建议加:MediaPath 使用 /workspace/... 而不是 ./...(避免 read 按 cwd 解析失败)
181
+ "inboundMediaSandboxRootPrefix": "/workspace",
180
182
  "accounts": {
181
183
  "main": { "appKey": "appKey_机器人A", "agentId": "main", "name": "个人助手" },
182
184
  "sales": { "appKey": "appKey_机器人B", "agentId": "sales", "name": "销售助手" }
@@ -233,7 +235,8 @@ openclaw gateway restart
233
235
  | `reconnectJitter` | number | `0.3` | 重连抖动因子(0-1) |
234
236
  | `firstReplyTimeoutMs` | number | `300000` | **首条 AI 回复超时时间(毫秒)**。用于保护「思考中」占位消息:若在该时间窗口内 AI 没有任何回复,则自动将占位消息更新为「当前请求处理超时,请稍后重试」。|
235
237
  | `inboundMediaWorkspaceSubdir` | string | — | **可选**。非空时,将入站附件先下载到**当前 Agent workspace** 下该**相对子目录**(如 `xg_im_inbound`),路径约定:`{子目录}/{发送者 userId}/{YYYY-MM-DD}/{文件名}_{HHmmssmmm}.ext`(时间为本机写入时刻),再向 OpenClaw 传入 **MediaPath**;默认使用 **workspace 相对路径**(如 `./xg_im_inbound/...`),便于 **沙箱** 将 `/workspace` 映射到同一套目录树;未配置则仍使用 IM 返回的 URL。需 runtime 提供 `resolveAgentWorkspaceDir`。 |
236
- | `inboundMediaOpenClawPath` | string | `workspaceRelative`(默认) | `workspaceRelative`:入站落盘后 `MediaPath` 为 `./子目录/...`;`absoluteFileUrl`:宿主机 `file://...`(仅适合无沙箱、网关与 Agent 同盘场景)。 |
238
+ | `inboundMediaOpenClawPath` | string | `workspaceRelative`(默认) | `workspaceRelative`:入站落盘后 `MediaPath` 为 `./子目录/...`(若配置了 `inboundMediaSandboxRootPrefix` 则为该前缀 + 相对路径,如 `/workspace/xg_im_inbound/...`);`absoluteFileUrl`:宿主机 `file://...`(仅适合无沙箱、网关与 Agent 同盘场景)。 |
239
+ | `inboundMediaSandboxRootPrefix` | string | — | **可选**。沙箱内 workspace 挂载根,填 **`/workspace`** 时,在 `workspaceRelative` 模式下 `MediaPath` 为 **`/workspace/xg_im_inbound/...`**,避免 `./` 被 `read` 等工具按**进程 cwd**解析导致「不可访问」。须以 `/` 开头。 |
237
240
 
238
241
  ### 账户配置(`accounts.<accountId>` / `accounts[n]`)
239
242
 
@@ -245,6 +248,7 @@ openclaw gateway restart
245
248
  | `groupPolicy` | string | 继承顶层 | 可覆盖顶层的 groupPolicy |
246
249
  | `inboundMediaWorkspaceSubdir` | string | 继承顶层 | 可覆盖顶层的入站附件落盘子目录 |
247
250
  | `inboundMediaOpenClawPath` | string | 继承顶层 | 可覆盖顶层的 MediaPath 形式(相对路径 vs file://) |
251
+ | `inboundMediaSandboxRootPrefix` | string | 继承顶层 | 可覆盖沙箱根前缀(如 `/workspace`) |
248
252
 
249
253
  ---
250
254
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xgjktech/xg_cwork_im",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "XG CWork IM channel plugin for OpenClaw",
5
5
  "keywords": [
6
6
  "bot",
package/src/channel.ts CHANGED
@@ -56,6 +56,7 @@ const XgImAccountConfigSchema = z.object({
56
56
  maxAttachmentBytes: z.number().int().positive().optional(),
57
57
  inboundMediaWorkspaceSubdir: z.string().optional(),
58
58
  inboundMediaOpenClawPath: z.enum(["workspaceRelative", "absoluteFileUrl"]).optional(),
59
+ inboundMediaSandboxRootPrefix: z.string().optional(),
59
60
  });
60
61
 
61
62
  const XgImConfigSchema = z.object({
@@ -80,6 +81,8 @@ const XgImConfigSchema = z.object({
80
81
  */
81
82
  inboundMediaWorkspaceSubdir: z.string().optional(),
82
83
  inboundMediaOpenClawPath: z.enum(["workspaceRelative", "absoluteFileUrl"]).optional(),
84
+ /** 沙箱内 workspace 挂载根,如 `/workspace`;与 workspaceRelative 组合生成绝对引用 */
85
+ inboundMediaSandboxRootPrefix: z.string().optional(),
83
86
  // 多账户:对象 map(key 为 accountId)
84
87
  accounts: z.record(z.string(), XgImAccountConfigSchema).optional(),
85
88
  }).superRefine((val, ctx) => {
@@ -95,6 +98,17 @@ const XgImConfigSchema = z.object({
95
98
  });
96
99
  }
97
100
 
101
+ const badSandboxPrefix = (s: string | undefined): boolean =>
102
+ typeof s === "string" && s.trim().length > 0 && !s.trim().startsWith("/");
103
+
104
+ if (badSandboxPrefix(val.inboundMediaSandboxRootPrefix)) {
105
+ ctx.addIssue({
106
+ code: z.ZodIssueCode.custom,
107
+ path: ["inboundMediaSandboxRootPrefix"],
108
+ message: "inboundMediaSandboxRootPrefix must be empty or an absolute path such as /workspace",
109
+ });
110
+ }
111
+
98
112
  const accounts = val.accounts;
99
113
  if (!accounts) return;
100
114
  for (const [key, acc] of Object.entries(accounts)) {
@@ -115,6 +129,14 @@ const XgImConfigSchema = z.object({
115
129
  "inboundMediaWorkspaceSubdir must be a relative path under workspace (no .., no absolute path)",
116
130
  });
117
131
  }
132
+ if (badSandboxPrefix(acc.inboundMediaSandboxRootPrefix)) {
133
+ ctx.addIssue({
134
+ code: z.ZodIssueCode.custom,
135
+ path: ["accounts", key, "inboundMediaSandboxRootPrefix"],
136
+ message:
137
+ "inboundMediaSandboxRootPrefix must be empty or an absolute path such as /workspace",
138
+ });
139
+ }
118
140
  }
119
141
  });
120
142
 
@@ -671,6 +693,9 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
671
693
 
672
694
  const inboundSub = config.inboundMediaWorkspaceSubdir?.trim();
673
695
  if (inboundSub && fileItems.length > 0 && normalizeInboundWorkspaceSubdir(inboundSub)) {
696
+ log.info(
697
+ `${logPrefix} [inbound-local] enabled subdir=${inboundSub} sender=${senderId ?? "unknown"} files=${fileItems.length} pathStyle=${config.inboundMediaOpenClawPath ?? "workspaceRelative"} sandboxPrefix=${config.inboundMediaSandboxRootPrefix ?? "(none)"}`,
698
+ );
674
699
  fileItems = await saveInboundFilesToWorkspace({
675
700
  rt,
676
701
  cfg: ctx.cfg,
@@ -5,7 +5,7 @@
5
5
  * 时间取写入时的本地时区。
6
6
  */
7
7
 
8
- import { mkdir, writeFile } from "node:fs/promises";
8
+ import { mkdir, stat, writeFile } from "node:fs/promises";
9
9
  import path from "node:path";
10
10
  import { pathToFileURL } from "node:url";
11
11
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
@@ -98,16 +98,39 @@ export function absPathToWorkspaceRelativeOpenClawRef(workspaceRoot: string, abs
98
98
  return posix.startsWith("./") ? posix : `./${posix}`;
99
99
  }
100
100
 
101
+ /**
102
+ * 沙箱内常用挂载点(如 `/workspace`)+ 相对 workspace 的路径,供 read 等工具按「绝对路径」解析。
103
+ * 若未配置 sandboxRootPrefix,则仍用 `./xg_im_inbound/...`(相对当前 cwd,部分沙箱工具会误判为不可访问)。
104
+ */
105
+ export function sandboxWorkspaceMediaRef(
106
+ workspaceRoot: string,
107
+ absPath: string,
108
+ sandboxRootPrefix: string,
109
+ ): string | null {
110
+ const rel = absPathToWorkspaceRelativeOpenClawRef(workspaceRoot, absPath);
111
+ if (!rel) return null;
112
+ const root = sandboxRootPrefix.trim().replace(/\/+$/, "") || "/workspace";
113
+ const relStrip = rel.replace(/^\.\/+/, "").replace(/^\/+/, "");
114
+ return `${root}/${relStrip}`;
115
+ }
116
+
101
117
  function openClawMediaRefForSavedFile(args: {
102
118
  workspaceRoot: string;
103
119
  absPath: string;
104
120
  style: "workspaceRelative" | "absoluteFileUrl" | undefined;
121
+ /** 非空且 style 为 workspaceRelative 时,用 `/workspace/...` 形式代替 `./...` */
122
+ sandboxRootPrefix?: string;
105
123
  log?: Log;
106
124
  }): string {
107
- const { workspaceRoot, absPath, style, log } = args;
125
+ const { workspaceRoot, absPath, style, sandboxRootPrefix, log } = args;
108
126
  if (style === "absoluteFileUrl") {
109
127
  return pathToFileURL(absPath).href;
110
128
  }
129
+ const prefix = sandboxRootPrefix?.trim();
130
+ if (prefix) {
131
+ const boxed = sandboxWorkspaceMediaRef(workspaceRoot, absPath, prefix);
132
+ if (boxed) return boxed;
133
+ }
111
134
  const rel = absPathToWorkspaceRelativeOpenClawRef(workspaceRoot, absPath);
112
135
  if (rel) return rel;
113
136
  log?.warn?.(
@@ -154,6 +177,12 @@ export async function saveInboundFilesToWorkspace(args: {
154
177
  const maxBytes = resolveMaxAttachmentBytes(config);
155
178
  const userSeg = sanitizeUserIdForPath(senderUserId ?? "unknown");
156
179
  const relParts = normalized.split("/");
180
+ const pathStyle = config.inboundMediaOpenClawPath ?? "workspaceRelative";
181
+ const sandboxPrefix = config.inboundMediaSandboxRootPrefix?.trim() || "";
182
+
183
+ log?.info?.(
184
+ `[cwork_im:inbound-local] begin files=${fileItems.length} cwd=${process.cwd()} workspaceRoot=${workspaceRoot} subdir=${normalized} user=${userSeg} style=${pathStyle} sandboxPrefix=${sandboxPrefix || "(none)"}`,
185
+ );
157
186
 
158
187
  const out: MsgFileVO[] = [];
159
188
  for (const item of fileItems) {
@@ -169,18 +198,22 @@ export async function saveInboundFilesToWorkspace(args: {
169
198
  const absPath = path.join(dir, localName);
170
199
 
171
200
  try {
201
+ log?.info?.(
202
+ `[cwork_im:inbound-local] prepare src=${srcUrl.slice(0, 120)} dir=${dir} abs=${absPath}`,
203
+ );
172
204
  await mkdir(dir, { recursive: true });
173
205
  const { buffer } = await downloadMediaBuffer(srcUrl, maxBytes, log);
174
206
  await writeFile(absPath, buffer);
175
- const pathStyle = config.inboundMediaOpenClawPath ?? "workspaceRelative";
176
207
  const mediaRef = openClawMediaRefForSavedFile({
177
208
  workspaceRoot,
178
209
  absPath,
179
210
  style: pathStyle,
211
+ sandboxRootPrefix: sandboxPrefix,
180
212
  log,
181
213
  });
214
+ const st = await stat(absPath);
182
215
  log?.info?.(
183
- `[cwork_im:inbound-local] saved ${localName} bytes=${buffer.length} abs=${absPath} openclawRef=${mediaRef.slice(0, 120)}`,
216
+ `[cwork_im:inbound-local] saved ${localName} bytes=${buffer.length} stat.size=${st.size} stat.mtime=${st.mtime.toISOString()} abs=${absPath} openclawRef=${mediaRef}`,
184
217
  );
185
218
  out.push({
186
219
  ...item,
package/src/types.ts CHANGED
@@ -44,10 +44,16 @@ export interface XgImAccountConfig {
44
44
  inboundMediaWorkspaceSubdir?: string;
45
45
  /**
46
46
  * 覆盖顶层:入站落盘后交给 OpenClaw 的 `MediaPath` 形式。
47
- * - `workspaceRelative`(默认):`./xg_im_inbound/...`,沙箱内 `/workspace` 可解析。
47
+ * - `workspaceRelative`(默认):`./子目录/...`,或配合 `inboundMediaSandboxRootPrefix` 为 `/workspace/子目录/...`。
48
48
  * - `absoluteFileUrl`:宿主机 `file://...`,仅适合无沙箱、同进程读盘场景。
49
49
  */
50
50
  inboundMediaOpenClawPath?: "workspaceRelative" | "absoluteFileUrl";
51
+ /**
52
+ * 沙箱内 Agent workspace 挂载根路径(如 `/workspace`)。
53
+ * 与 `inboundMediaOpenClawPath: workspaceRelative` 联用时,入站 `MediaPath` 为
54
+ * `{inboundMediaSandboxRootPrefix}/xg_im_inbound/...`,避免 `./` 被工具按 cwd 解析而报「不可访问」。
55
+ */
56
+ inboundMediaSandboxRootPrefix?: string;
51
57
  }
52
58
 
53
59
  export interface XgImConfig extends OpenClawConfig {
@@ -107,6 +113,8 @@ export interface XgImConfig extends OpenClawConfig {
107
113
  * Agent 在沙箱中运行时应用 `workspaceRelative`,避免宿主机绝对 `file://` 在沙箱内不可读。
108
114
  */
109
115
  inboundMediaOpenClawPath?: "workspaceRelative" | "absoluteFileUrl";
116
+ /** 见 {@link XgImAccountConfig.inboundMediaSandboxRootPrefix} */
117
+ inboundMediaSandboxRootPrefix?: string;
110
118
  }
111
119
 
112
120
  // ─── IM 接口 Request / Response ─────────────────────────────────────────────