@xgjktech/xg_cwork_im 1.0.8 → 1.0.9

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.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * 入站附件:可选落盘到当前 Agent workspace 下指定子目录(相对路径)。
3
+ *
4
+ * 目录约定:{subdir}/{userId}/{YYYY-MM-DD}/{stem}_{HHmmssmmm}{ext}
5
+ * 时间取写入时的本地时区。
6
+ */
7
+
8
+ import { mkdir, writeFile } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { pathToFileURL } from "node:url";
11
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
12
+ import type { Log } from "./auth.js";
13
+ import { downloadMediaBuffer, resolveMaxAttachmentBytes } from "./resource-file.js";
14
+ import type { MsgFileVO, XgImConfig } from "./types.js";
15
+
16
+ /** 校验:非空、相对路径、不含 `..` 段 */
17
+ export function normalizeInboundWorkspaceSubdir(subdir: string): string | null {
18
+ const t = subdir.trim().replace(/\\/g, "/");
19
+ if (!t || t === "." || t === "..") return null;
20
+ const parts = t.split("/").filter(Boolean);
21
+ for (const p of parts) {
22
+ if (p === "..") return null;
23
+ }
24
+ if (path.isAbsolute(t)) return null;
25
+ return parts.join("/");
26
+ }
27
+
28
+ export function resolveAgentWorkspaceDirFromRuntime(rt: unknown, cfg: OpenClawConfig): string | undefined {
29
+ const agent = (rt as { agent?: { resolveAgentWorkspaceDir?: (c: OpenClawConfig) => unknown } })?.agent;
30
+ const fn = agent?.resolveAgentWorkspaceDir;
31
+ if (typeof fn !== "function") return undefined;
32
+ try {
33
+ const out = fn(cfg);
34
+ if (typeof out === "string" && out.trim()) return path.resolve(out.trim());
35
+ } catch {
36
+ /* ignore */
37
+ }
38
+ return undefined;
39
+ }
40
+
41
+ export async function ensureAgentWorkspaceFromRuntime(rt: unknown, cfg: OpenClawConfig): Promise<void> {
42
+ const agent = (rt as { agent?: { ensureAgentWorkspace?: (c: OpenClawConfig) => Promise<void> } })?.agent;
43
+ const fn = agent?.ensureAgentWorkspace;
44
+ if (typeof fn !== "function") return;
45
+ try {
46
+ await fn(cfg);
47
+ } catch {
48
+ /* 落盘前仍会 mkdir,不阻断 */
49
+ }
50
+ }
51
+
52
+ function sanitizeUserIdForPath(id: string): string {
53
+ const t = id.trim();
54
+ if (/^-?\d+$/.test(t) && t.length <= 32) return t;
55
+ const safe = t.replace(/[^\w.-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
56
+ return safe.slice(0, 64) || "unknown";
57
+ }
58
+
59
+ function formatDateFolderLocal(d: Date): string {
60
+ const y = d.getFullYear();
61
+ const m = String(d.getMonth() + 1).padStart(2, "0");
62
+ const day = String(d.getDate()).padStart(2, "0");
63
+ return `${y}-${m}-${day}`;
64
+ }
65
+
66
+ /** `HHmmss` + 三位毫秒,如 `120459237` */
67
+ export function formatInboundTimeSuffix(d: Date): string {
68
+ const h = String(d.getHours()).padStart(2, "0");
69
+ const min = String(d.getMinutes()).padStart(2, "0");
70
+ const s = String(d.getSeconds()).padStart(2, "0");
71
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
72
+ return `${h}${min}${s}${ms}`;
73
+ }
74
+
75
+ const MAX_STEM_LEN = 120;
76
+
77
+ function sanitizeStem(stem: string): string {
78
+ const s = stem
79
+ .replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_")
80
+ .replace(/_+/g, "_")
81
+ .replace(/^\.+|\.+$/g, "")
82
+ .trim();
83
+ const out = s || "file";
84
+ return out.length > MAX_STEM_LEN ? out.slice(0, MAX_STEM_LEN) : out;
85
+ }
86
+
87
+ /** `report_120459237.pdf` */
88
+ export function buildInboundStoredFilename(originalFilename: string, at: Date): string {
89
+ const base = path.basename(originalFilename) || "file";
90
+ const ext = path.extname(base);
91
+ const stem = ext ? base.slice(0, -ext.length) : base;
92
+ const suffix = formatInboundTimeSuffix(at);
93
+ return `${sanitizeStem(stem)}_${suffix}${ext.toLowerCase()}`;
94
+ }
95
+
96
+ /**
97
+ * 将入站附件下载到 workspace 下子目录;失败项保留原 URL。
98
+ */
99
+ export async function saveInboundFilesToWorkspace(args: {
100
+ rt: unknown;
101
+ cfg: OpenClawConfig;
102
+ config: XgImConfig;
103
+ workspaceSubdir: string;
104
+ senderUserId: string | undefined;
105
+ fileItems: MsgFileVO[];
106
+ log?: Log;
107
+ }): Promise<MsgFileVO[]> {
108
+ const { rt, cfg, config, workspaceSubdir, senderUserId, fileItems, log } = args;
109
+ const normalized = normalizeInboundWorkspaceSubdir(workspaceSubdir);
110
+ if (!normalized || fileItems.length === 0) return fileItems;
111
+
112
+ const workspaceRoot = resolveAgentWorkspaceDirFromRuntime(rt, cfg);
113
+ if (!workspaceRoot) {
114
+ log?.warn?.(
115
+ "[cwork_im:inbound-local] resolveAgentWorkspaceDir unavailable, skip inbound download; keep remote URLs",
116
+ );
117
+ return fileItems;
118
+ }
119
+
120
+ await ensureAgentWorkspaceFromRuntime(rt, cfg);
121
+
122
+ const maxBytes = resolveMaxAttachmentBytes(config);
123
+ const userSeg = sanitizeUserIdForPath(senderUserId ?? "unknown");
124
+ const relParts = normalized.split("/");
125
+
126
+ const out: MsgFileVO[] = [];
127
+ for (const item of fileItems) {
128
+ const srcUrl = item.url?.trim();
129
+ if (!srcUrl) {
130
+ out.push(item);
131
+ continue;
132
+ }
133
+ const now = new Date();
134
+ const dateFolder = formatDateFolderLocal(now);
135
+ const localName = buildInboundStoredFilename(item.name?.trim() || "file", now);
136
+ const dir = path.join(workspaceRoot, ...relParts, userSeg, dateFolder);
137
+ const absPath = path.join(dir, localName);
138
+
139
+ try {
140
+ await mkdir(dir, { recursive: true });
141
+ const { buffer } = await downloadMediaBuffer(srcUrl, maxBytes, log);
142
+ await writeFile(absPath, buffer);
143
+ const fileUrl = pathToFileURL(absPath).href;
144
+ log?.info?.(`[cwork_im:inbound-local] saved ${localName} bytes=${buffer.length} -> ${absPath}`);
145
+ out.push({
146
+ ...item,
147
+ url: fileUrl,
148
+ size: buffer.length,
149
+ name: localName,
150
+ });
151
+ } catch (e: unknown) {
152
+ log?.warn?.(
153
+ `[cwork_im:inbound-local] download/save failed, keep original URL: ${String(e)} ref=${srcUrl.slice(0, 80)}`,
154
+ );
155
+ out.push(item);
156
+ }
157
+ }
158
+ return out;
159
+ }