@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 +6 -2
- package/package.json +1 -1
- package/src/channel.ts +25 -0
- package/src/inbound-media-local.ts +37 -4
- package/src/types.ts +9 -1
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
|
|
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` 为
|
|
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
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
|
|
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
|
|
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 ─────────────────────────────────────────────
|