dsh-files-native 0.1.0

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/lib/index.js ADDED
@@ -0,0 +1,236 @@
1
+ // src/index.ts
2
+ import { randomUUID } from "node:crypto";
3
+ import { createWriteStream } from "node:fs";
4
+ import { mkdir, rename, rm, stat } from "node:fs/promises";
5
+ import { dirname, join, resolve, sep } from "node:path";
6
+
7
+ // src/lib.ts
8
+ var MAX_FILE_BYTES = 50 * 1024 * 1024;
9
+ var UPLOAD_DIR = ".dsh-uploads";
10
+ function sanitizeFileName(name2) {
11
+ const base = name2.replace(/^.*[/\\]/, "").replace(/[\u0000-\u001f]/g, "");
12
+ const cleaned = base.replace(/[<>:"/\\|?*]/g, "_").replace(/^\.+/, "").trim();
13
+ const sliced = cleaned.slice(0, 180);
14
+ return sliced === "" ? "file" : sliced;
15
+ }
16
+ function uniqueName(name2, taken) {
17
+ if (!taken.has(name2)) return name2;
18
+ const dot = name2.lastIndexOf(".");
19
+ const stem = dot > 0 ? name2.slice(0, dot) : name2;
20
+ const ext = dot > 0 ? name2.slice(dot) : "";
21
+ let i = 1;
22
+ while (taken.has(`${stem}-${i}${ext}`)) i += 1;
23
+ return `${stem}-${i}${ext}`;
24
+ }
25
+ function isSafeRelPath(relPath) {
26
+ if (relPath === "" || relPath.startsWith("/") || relPath.startsWith("\\")) return false;
27
+ const parts = relPath.split(/[/\\]/);
28
+ if (parts[0] !== UPLOAD_DIR) return false;
29
+ return parts.every((part) => part !== "" && part !== "." && part !== "..");
30
+ }
31
+ function fileListText(files) {
32
+ const lines = files.map((file) => `- ${file.name} \u2014 path="${file.relPath}" size=${file.size} type="${file.mediaType}"`);
33
+ return [
34
+ "\u7528\u6237\u968F\u672C\u6761\u6D88\u606F\u4E0A\u4F20\u4E86\u4EE5\u4E0B\u6587\u4EF6,\u5DF2\u4FDD\u5B58\u5230\u5F53\u524D\u5DE5\u4F5C\u533A,\u53EF\u7528 read \u5DE5\u5177\u6309\u8DEF\u5F84\u8BFB\u53D6:",
35
+ ...lines
36
+ ].join("\n");
37
+ }
38
+
39
+ // src/index.ts
40
+ var name = "file-native";
41
+ var inject = ["webServer", "sessions"];
42
+ var pending = /* @__PURE__ */ new Map();
43
+ var lastSent = /* @__PURE__ */ new Map();
44
+ function sessionCwd(ctx, sessionId, clientCwd) {
45
+ const headerCwd = ctx.sessions.get(sessionId)?.header?.cwd;
46
+ if (headerCwd) return headerCwd;
47
+ if (clientCwd && resolve(clientCwd) === clientCwd) return clientCwd;
48
+ return process.cwd();
49
+ }
50
+ function json(res, code, body) {
51
+ res.writeHead(code, { "content-type": "application/json; charset=utf-8" });
52
+ res.end(JSON.stringify(body));
53
+ }
54
+ function isLoopback(req) {
55
+ const host = String(req.headers.host ?? "").split(":")[0] ?? "";
56
+ if (host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1") return true;
57
+ const parts = host.split(".");
58
+ return parts.length === 4 && parts[0] === "127" && parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255);
59
+ }
60
+ function query(req) {
61
+ return new URL(req.url ?? "/", "http://dsh.internal").searchParams;
62
+ }
63
+ async function readBody(req, limit) {
64
+ const chunks = [];
65
+ let size = 0;
66
+ for await (const chunk of req) {
67
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
68
+ size += buf.length;
69
+ if (size > limit) {
70
+ const error = new Error("too-large");
71
+ error.code = "too-large";
72
+ throw error;
73
+ }
74
+ chunks.push(buf);
75
+ }
76
+ return Buffer.concat(chunks);
77
+ }
78
+ async function writeUpload(cwd, fileName, bytes) {
79
+ const dir = join(cwd, UPLOAD_DIR);
80
+ await mkdir(dir, { recursive: true });
81
+ const taken = /* @__PURE__ */ new Set();
82
+ try {
83
+ const { readdir } = await import("node:fs/promises");
84
+ for (const item of await readdir(dir)) taken.add(item);
85
+ } catch {
86
+ }
87
+ const safe = uniqueName(sanitizeFileName(fileName), taken);
88
+ const target = join(dir, safe);
89
+ const resolvedCwd = resolve(cwd);
90
+ const resolvedTarget = resolve(target);
91
+ if (resolvedTarget !== resolvedCwd && !resolvedTarget.startsWith(resolvedCwd + sep)) {
92
+ throw new Error("path-escape");
93
+ }
94
+ const tmp = join(dirname(target), `.${safe}.${randomUUID()}.tmp`);
95
+ await new Promise((resolveWrite, reject) => {
96
+ const stream = createWriteStream(tmp, { flags: "wx" });
97
+ stream.on("error", reject);
98
+ stream.end(bytes, () => resolveWrite());
99
+ });
100
+ await rename(tmp, target);
101
+ const info = await stat(target);
102
+ return {
103
+ name: fileName.replace(/^.*[/\\]/, "") || safe,
104
+ relPath: `${UPLOAD_DIR}/${safe}`,
105
+ size: info.size,
106
+ mediaType: "application/octet-stream"
107
+ };
108
+ }
109
+ function takePending(sessionId) {
110
+ const files = pending.get(sessionId) ?? [];
111
+ pending.delete(sessionId);
112
+ if (files.length > 0) lastSent.set(sessionId, files);
113
+ return files;
114
+ }
115
+ function registerRoutes(ctx, webServer) {
116
+ const upload = webServer.register({
117
+ kind: "exact",
118
+ path: "/plugins/file-native/upload",
119
+ handler: async (req, res) => {
120
+ if (!isLoopback(req)) return json(res, 403, { ok: false, error: "forbidden" });
121
+ if (req.method !== "POST") return json(res, 405, { ok: false, error: "method" });
122
+ const q = query(req);
123
+ const sessionId = q.get("sessionId") ?? "";
124
+ const fileName = q.get("name") ?? "file";
125
+ if (sessionId === "") return json(res, 400, { ok: false, error: "sessionId" });
126
+ try {
127
+ const bytes = await readBody(req, MAX_FILE_BYTES);
128
+ if (bytes.length === 0) return json(res, 400, { ok: false, error: "empty" });
129
+ const cwd = sessionCwd(ctx, sessionId, q.get("cwd") ?? void 0);
130
+ const file = await writeUpload(cwd, fileName, bytes);
131
+ file.mediaType = q.get("type") || file.mediaType;
132
+ const list = pending.get(sessionId) ?? [];
133
+ list.push(file);
134
+ pending.set(sessionId, list);
135
+ json(res, 200, { ok: true, file });
136
+ } catch (error) {
137
+ const code = error.code === "too-large" ? 413 : 400;
138
+ ctx.logger.warn("[file-native] upload failed: %o", error);
139
+ json(res, code, { ok: false, error: code === 413 ? "too-large" : "write-failed" });
140
+ }
141
+ }
142
+ });
143
+ const remove = webServer.register({
144
+ kind: "exact",
145
+ path: "/plugins/file-native/remove",
146
+ handler: async (req, res) => {
147
+ if (!isLoopback(req)) return json(res, 403, { ok: false, error: "forbidden" });
148
+ if (req.method !== "POST") return json(res, 405, { ok: false, error: "method" });
149
+ const q = query(req);
150
+ const sessionId = q.get("sessionId") ?? "";
151
+ const relPath = q.get("path") ?? "";
152
+ if (sessionId === "" || !isSafeRelPath(relPath)) return json(res, 400, { ok: false, error: "bad-path" });
153
+ const cwd = sessionCwd(ctx, sessionId, q.get("cwd") ?? void 0);
154
+ pending.set(sessionId, (pending.get(sessionId) ?? []).filter((f) => f.relPath !== relPath));
155
+ try {
156
+ await rm(join(cwd, relPath), { force: true });
157
+ } catch {
158
+ }
159
+ json(res, 200, { ok: true });
160
+ }
161
+ });
162
+ const last = webServer.register({
163
+ kind: "exact",
164
+ path: "/plugins/file-native/last",
165
+ handler: (req, res) => {
166
+ if (!isLoopback(req)) return json(res, 403, { ok: false, error: "forbidden" });
167
+ const sessionId = query(req).get("sessionId") ?? "";
168
+ json(res, 200, { ok: true, files: lastSent.get(sessionId) ?? [] });
169
+ }
170
+ });
171
+ return () => {
172
+ upload();
173
+ remove();
174
+ last();
175
+ };
176
+ }
177
+ function apply(ctx) {
178
+ const tryRegister = () => {
179
+ const webServer = ctx.get("webServer") ?? ctx.webServer;
180
+ if (webServer === void 0) return void 0;
181
+ return registerRoutes(ctx, webServer);
182
+ };
183
+ const direct = tryRegister();
184
+ if (direct) {
185
+ ctx.effect(() => direct, "file-native: routes");
186
+ } else {
187
+ let dispose;
188
+ const timer = setInterval(() => {
189
+ const d = tryRegister();
190
+ if (d) {
191
+ clearInterval(timer);
192
+ dispose = d;
193
+ ctx.logger.info("[file-native] upload route registered (deferred)");
194
+ }
195
+ }, 400);
196
+ ctx.effect(() => () => {
197
+ clearInterval(timer);
198
+ dispose?.();
199
+ }, "file-native: routes deferred");
200
+ }
201
+ ctx.on("agent/pre-step", async (...args) => {
202
+ const payload = args[0];
203
+ const next = args[1];
204
+ const decision = await next();
205
+ if (decision?.kind === "reject") return decision;
206
+ const agent = payload.agent;
207
+ const sessionId = String(agent?.id ?? "");
208
+ const files = takePending(sessionId);
209
+ if (files.length === 0 || !Array.isArray(decision.messages) || decision.messages.length === 0) {
210
+ return decision;
211
+ }
212
+ const lastIndex = decision.messages.length - 1;
213
+ const notice = {
214
+ id: `file-native-${String(decision.messages[lastIndex]?.id ?? randomUUID())}`,
215
+ role: "user",
216
+ content: [{ type: "text", text: fileListText(files) }],
217
+ source: {
218
+ kind: "plugin",
219
+ plugin: "file-native",
220
+ form: "notice",
221
+ summary: `\u{1F4CE} \u9644\u4EF6 ${files.length} \u4E2A\u6587\u4EF6`
222
+ }
223
+ };
224
+ return {
225
+ ...decision,
226
+ messages: [...decision.messages.slice(0, lastIndex), notice, decision.messages[lastIndex], ...decision.messages.slice(lastIndex + 1)]
227
+ };
228
+ });
229
+ ctx.logger.info("[file-native] host loaded");
230
+ }
231
+ export {
232
+ apply,
233
+ inject,
234
+ name
235
+ };
236
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/lib.ts"],
4
+ "sourcesContent": ["/**\n * file-native \u5BBF\u4E3B\u534A:\u628A\u975E\u56FE\u7247\u9644\u4EF6\u843D\u5230\u4F1A\u8BDD\u5DE5\u4F5C\u533A `.dsh-uploads/`,\n * \u53D1\u9001\u65F6\u7ECF agent/pre-step \u628A\u76F8\u5BF9\u8DEF\u5F84\u4EA4\u7ED9\u6A21\u578B\u3002\u56FE\u7247\u4ECD\u8D70\u5B98\u65B9\u8349\u7A3F\u56FE\u3002\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { createWriteStream } from 'node:fs';\nimport { mkdir, rename, rm, stat } from 'node:fs/promises';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport {\n fileListText,\n isSafeRelPath,\n MAX_FILE_BYTES,\n type RailFile,\n sanitizeFileName,\n uniqueName,\n UPLOAD_DIR,\n} from './lib.ts';\n\nconst name = 'file-native';\n/** \u8BBF\u95EE webServer / sessions \u524D\u5FC5\u987B\u5199\u8FDB inject,\u5426\u5219 DSH \u629B cannot get property without inject,\u8FDB\u7A0B\u8D77\u4E0D\u6765\u3002\u89C1 docs/plugin-dev-gate.md\u3002 */\nconst inject = ['webServer', 'sessions'];\n\nexport type { RailFile };\n\ninterface HostCtx {\n logger: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void };\n sessions: { get: (id: string) => { header?: { cwd?: string } } | undefined };\n webServer?: {\n register: (route: {\n kind: 'exact';\n path: string;\n handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;\n }) => () => void;\n };\n get: (key: string) => unknown;\n effect: (fn: () => (() => void) | void, label?: string) => void;\n on: (event: string, listener: (...args: never[]) => unknown) => void;\n}\n\nconst pending = new Map<string, RailFile[]>();\nconst lastSent = new Map<string, RailFile[]>();\n\nfunction sessionCwd(ctx: HostCtx, sessionId: string, clientCwd?: string): string {\n const headerCwd = ctx.sessions.get(sessionId)?.header?.cwd;\n if (headerCwd) return headerCwd;\n if (clientCwd && resolve(clientCwd) === clientCwd) return clientCwd;\n return process.cwd();\n}\n\nfunction json(res: ServerResponse, code: number, body: unknown): void {\n res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' });\n res.end(JSON.stringify(body));\n}\n\nfunction isLoopback(req: IncomingMessage): boolean {\n const host = String(req.headers.host ?? '').split(':')[0] ?? '';\n if (host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host === '::1') return true;\n const parts = host.split('.');\n return parts.length === 4 && parts[0] === '127' && parts.every((p) => /^\\d{1,3}$/.test(p) && Number(p) <= 255);\n}\n\nfunction query(req: IncomingMessage): URLSearchParams {\n return new URL(req.url ?? '/', 'http://dsh.internal').searchParams;\n}\n\nasync function readBody(req: IncomingMessage, limit: number): Promise<Buffer> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n size += buf.length;\n if (size > limit) {\n const error = new Error('too-large');\n (error as { code?: string }).code = 'too-large';\n throw error;\n }\n chunks.push(buf);\n }\n return Buffer.concat(chunks);\n}\n\nasync function writeUpload(cwd: string, fileName: string, bytes: Buffer): Promise<RailFile> {\n const dir = join(cwd, UPLOAD_DIR);\n await mkdir(dir, { recursive: true });\n const taken = new Set<string>();\n try {\n const { readdir } = await import('node:fs/promises');\n for (const item of await readdir(dir)) taken.add(item);\n } catch {\n // \u76EE\u5F55\u521A\u521B\u5EFA\n }\n const safe = uniqueName(sanitizeFileName(fileName), taken);\n const target = join(dir, safe);\n const resolvedCwd = resolve(cwd);\n const resolvedTarget = resolve(target);\n if (resolvedTarget !== resolvedCwd && !resolvedTarget.startsWith(resolvedCwd + sep)) {\n throw new Error('path-escape');\n }\n const tmp = join(dirname(target), `.${safe}.${randomUUID()}.tmp`);\n await new Promise<void>((resolveWrite, reject) => {\n const stream = createWriteStream(tmp, { flags: 'wx' });\n stream.on('error', reject);\n stream.end(bytes, () => resolveWrite());\n });\n await rename(tmp, target);\n const info = await stat(target);\n return {\n name: fileName.replace(/^.*[/\\\\]/, '') || safe,\n relPath: `${UPLOAD_DIR}/${safe}`,\n size: info.size,\n mediaType: 'application/octet-stream',\n };\n}\n\nfunction takePending(sessionId: string): RailFile[] {\n const files = pending.get(sessionId) ?? [];\n pending.delete(sessionId);\n if (files.length > 0) lastSent.set(sessionId, files);\n return files;\n}\n\nfunction registerRoutes(ctx: HostCtx, webServer: NonNullable<HostCtx['webServer']>): () => void {\n const upload = webServer.register({\n kind: 'exact',\n path: '/plugins/file-native/upload',\n handler: async (req, res) => {\n if (!isLoopback(req)) return json(res, 403, { ok: false, error: 'forbidden' });\n if (req.method !== 'POST') return json(res, 405, { ok: false, error: 'method' });\n const q = query(req);\n const sessionId = q.get('sessionId') ?? '';\n const fileName = q.get('name') ?? 'file';\n if (sessionId === '') return json(res, 400, { ok: false, error: 'sessionId' });\n try {\n const bytes = await readBody(req, MAX_FILE_BYTES);\n if (bytes.length === 0) return json(res, 400, { ok: false, error: 'empty' });\n const cwd = sessionCwd(ctx, sessionId, q.get('cwd') ?? undefined);\n const file = await writeUpload(cwd, fileName, bytes);\n file.mediaType = q.get('type') || file.mediaType;\n const list = pending.get(sessionId) ?? [];\n list.push(file);\n pending.set(sessionId, list);\n json(res, 200, { ok: true, file });\n } catch (error) {\n const code = (error as { code?: string }).code === 'too-large' ? 413 : 400;\n ctx.logger.warn('[file-native] upload failed: %o', error);\n json(res, code, { ok: false, error: code === 413 ? 'too-large' : 'write-failed' });\n }\n },\n });\n\n const remove = webServer.register({\n kind: 'exact',\n path: '/plugins/file-native/remove',\n handler: async (req, res) => {\n if (!isLoopback(req)) return json(res, 403, { ok: false, error: 'forbidden' });\n if (req.method !== 'POST') return json(res, 405, { ok: false, error: 'method' });\n const q = query(req);\n const sessionId = q.get('sessionId') ?? '';\n const relPath = q.get('path') ?? '';\n if (sessionId === '' || !isSafeRelPath(relPath)) return json(res, 400, { ok: false, error: 'bad-path' });\n const cwd = sessionCwd(ctx, sessionId, q.get('cwd') ?? undefined);\n pending.set(sessionId, (pending.get(sessionId) ?? []).filter((f) => f.relPath !== relPath));\n try {\n await rm(join(cwd, relPath), { force: true });\n } catch {\n // \u5DF2\u4E0D\u5728\u78C1\u76D8\u4E0A\u4E0D\u7B97\u5931\u8D25\n }\n json(res, 200, { ok: true });\n },\n });\n\n const last = webServer.register({\n kind: 'exact',\n path: '/plugins/file-native/last',\n handler: (req, res) => {\n if (!isLoopback(req)) return json(res, 403, { ok: false, error: 'forbidden' });\n const sessionId = query(req).get('sessionId') ?? '';\n json(res, 200, { ok: true, files: lastSent.get(sessionId) ?? [] });\n },\n });\n\n return () => {\n upload();\n remove();\n last();\n };\n}\n\nexport function apply(ctx: HostCtx): void {\n const tryRegister = (): (() => void) | undefined => {\n const webServer = (ctx.get('webServer') as HostCtx['webServer']) ?? ctx.webServer;\n if (webServer === undefined) return undefined;\n return registerRoutes(ctx, webServer);\n };\n\n const direct = tryRegister();\n if (direct) {\n ctx.effect(() => direct, 'file-native: routes');\n } else {\n let dispose: (() => void) | undefined;\n const timer = setInterval(() => {\n const d = tryRegister();\n if (d) {\n clearInterval(timer);\n dispose = d;\n ctx.logger.info('[file-native] upload route registered (deferred)');\n }\n }, 400);\n ctx.effect(() => () => {\n clearInterval(timer);\n dispose?.();\n }, 'file-native: routes deferred');\n }\n\n ctx.on('agent/pre-step', async (...args: never[]) => {\n const payload = args[0] as { agent?: { id?: unknown } };\n const next = args[1] as () => Promise<unknown>;\n const decision = await next() as {\n kind?: string;\n messages?: Array<{ id?: string; role?: string; content?: unknown; source?: unknown }>;\n };\n if (decision?.kind === 'reject') return decision;\n const agent = (payload as { agent?: { id?: unknown } }).agent;\n const sessionId = String(agent?.id ?? '');\n const files = takePending(sessionId);\n if (files.length === 0 || !Array.isArray(decision.messages) || decision.messages.length === 0) {\n return decision;\n }\n const lastIndex = decision.messages.length - 1;\n const notice = {\n id: `file-native-${String(decision.messages[lastIndex]?.id ?? randomUUID())}`,\n role: 'user',\n content: [{ type: 'text', text: fileListText(files) }],\n source: {\n kind: 'plugin',\n plugin: 'file-native',\n form: 'notice',\n summary: `\uD83D\uDCCE \u9644\u4EF6 ${files.length} \u4E2A\u6587\u4EF6`,\n },\n };\n return {\n ...decision,\n messages: [...decision.messages.slice(0, lastIndex), notice, decision.messages[lastIndex], ...decision.messages.slice(lastIndex + 1)],\n };\n });\n\n ctx.logger.info('[file-native] host loaded');\n}\n\nexport { inject, name };\n", "/** \u6587\u4EF6\u540D\u6E05\u6D17\u3001\u56FE\u7247\u5206\u6D41\u3001\u4F53\u79EF\u6587\u6848\u2014\u2014host / client / \u6D4B\u8BD5\u5171\u7528\u7684\u7EAF\u51FD\u6570\u3002 */\n\nexport interface RailFile {\n name: string;\n relPath: string;\n size: number;\n mediaType: string;\n}\n\n/** \u5B98\u65B9\u8349\u7A3F\u56FE\u53EA\u6536\u8FD9\u56DB\u79CD\uFF1B\u5176\u4F59\u56FE\u7247\u8D70\u6587\u4EF6\u5361\u3002 */\nexport const NATIVE_IMAGE_TYPES = new Set([\n 'image/png',\n 'image/jpeg',\n 'image/webp',\n 'image/gif',\n]);\n\nconst NATIVE_IMAGE_EXT = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif']);\n\n/** \u5355\u6587\u4EF6\u9ED8\u8BA4\u4E0A\u9650 50 MiB\u3002 */\nexport const MAX_FILE_BYTES = 50 * 1024 * 1024;\n\n/** \u5355\u6279\u6700\u591A 20 \u4E2A\u6587\u4EF6\u3002 */\nexport const MAX_FILES_PER_BATCH = 20;\n\n/** \u5DE5\u4F5C\u533A\u4E0A\u4F20\u76EE\u5F55\uFF08\u76F8\u5BF9\u4F1A\u8BDD cwd\uFF09\u3002 */\nexport const UPLOAD_DIR = '.dsh-uploads';\n\n/** \u662F\u5426\u8D70\u5B98\u65B9\u56FE\u7247\u8F68\u3002 */\nexport function isNativeImage(file: { type?: string; name?: string }): boolean {\n const type = (file.type ?? '').toLowerCase();\n if (NATIVE_IMAGE_TYPES.has(type)) return true;\n if (type !== '' && type !== 'application/octet-stream') return false;\n const ext = extOf(file.name ?? '');\n return NATIVE_IMAGE_EXT.has(ext.toLowerCase());\n}\n\n/** \u6269\u5C55\u540D\u89D2\u6807\uFF08\u6700\u591A 4 \u5B57\u7B26\uFF1B\u65E0\u6269\u5C55\u540D\u7528 FILE\uFF09\u3002 */\nexport function extOf(name: string): string {\n const base = name.replace(/^.*[/\\\\]/, '');\n const dot = base.lastIndexOf('.');\n if (dot <= 0 || dot === base.length - 1) return 'FILE';\n return base.slice(dot + 1).slice(0, 4).toUpperCase();\n}\n\n/** \u5265\u8DEF\u5F84\u3001\u63A7\u5236\u5B57\u7B26\u3001Windows \u975E\u6CD5\u5B57\u7B26\uFF1B\u7A7A\u540D\u56DE\u9000 file\u3002 */\nexport function sanitizeFileName(name: string): string {\n const base = name.replace(/^.*[/\\\\]/, '').replace(/[\\u0000-\\u001f]/g, '');\n const cleaned = base.replace(/[<>:\"/\\\\|?*]/g, '_').replace(/^\\.+/, '').trim();\n const sliced = cleaned.slice(0, 180);\n return sliced === '' ? 'file' : sliced;\n}\n\n/** \u5DF2\u5360\u7528\u540D\u4E0A\u8FFD\u52A0 -1 / -2\u3002 */\nexport function uniqueName(name: string, taken: ReadonlySet<string>): string {\n if (!taken.has(name)) return name;\n const dot = name.lastIndexOf('.');\n const stem = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : '';\n let i = 1;\n while (taken.has(`${stem}-${i}${ext}`)) i += 1;\n return `${stem}-${i}${ext}`;\n}\n\nexport function formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n}\n\n/** \u76F8\u5BF9\u8DEF\u5F84\u5FC5\u987B\u843D\u5728 UPLOAD_DIR \u4E0B\u3001\u4E0D\u542B .. */\nexport function isSafeRelPath(relPath: string): boolean {\n if (relPath === '' || relPath.startsWith('/') || relPath.startsWith('\\\\')) return false;\n const parts = relPath.split(/[/\\\\]/);\n if (parts[0] !== UPLOAD_DIR) return false;\n return parts.every((part) => part !== '' && part !== '.' && part !== '..');\n}\n\nexport function splitIntake(files: readonly File[]): { images: File[]; others: File[] } {\n const images: File[] = [];\n const others: File[] = [];\n for (const file of files) {\n if (isNativeImage(file)) images.push(file);\n else others.push(file);\n }\n return { images, others };\n}\n\n/** \u53D1\u7ED9\u6A21\u578B\u7684\u53EF\u8BFB\u6E05\u5355\u3002\u5BF9\u8BDD\u533A\u7528 parseNoticeFiles \u8FD8\u539F\u5361\u7247\u3002 */\nexport function fileListText(files: readonly RailFile[]): string {\n const lines = files.map((file) => `- ${file.name} \u2014 path=\"${file.relPath}\" size=${file.size} type=\"${file.mediaType}\"`);\n return [\n '\u7528\u6237\u968F\u672C\u6761\u6D88\u606F\u4E0A\u4F20\u4E86\u4EE5\u4E0B\u6587\u4EF6,\u5DF2\u4FDD\u5B58\u5230\u5F53\u524D\u5DE5\u4F5C\u533A,\u53EF\u7528 read \u5DE5\u5177\u6309\u8DEF\u5F84\u8BFB\u53D6:',\n ...lines,\n ].join('\\n');\n}\n\nconst NOTICE_LINE = /^- (.+?) \u2014 path=\"([^\"]+)\" size=(\\d+)(?: type=\"([^\"]*)\")?/;\n\nexport function parseNoticeFiles(text: string): RailFile[] {\n const files: RailFile[] = [];\n for (const line of text.split('\\n')) {\n const match = NOTICE_LINE.exec(line.trim());\n if (match === null) continue;\n const relPath = match[2] ?? '';\n if (!isSafeRelPath(relPath)) continue;\n files.push({\n name: match[1] ?? 'file',\n relPath,\n size: Number(match[3] ?? 0),\n mediaType: match[4] || 'application/octet-stream',\n });\n }\n return files;\n}\n\nexport function contentText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n const parts: string[] = [];\n for (const block of content) {\n if (block && typeof block === 'object' && 'text' in block && typeof (block as { text: unknown }).text === 'string') {\n parts.push((block as { text: string }).text);\n }\n }\n return parts.join('\\n');\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["name"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "dsh-files-native",
3
+ "description": "DSH 插件:接近原生质感的附件上传——拖入/粘贴/回形针,图片缩略图与文件卡片混排在官方附件栏",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/meyaomiao/dsh-files-native.git"
15
+ },
16
+ "homepage": "https://github.com/meyaomiao/dsh-files-native#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/meyaomiao/dsh-files-native/issues"
19
+ },
20
+ "author": "meyaomiao",
21
+ "keywords": [
22
+ "dsh",
23
+ "dsh-plugin",
24
+ "deepseek-harness",
25
+ "attachments",
26
+ "file-upload"
27
+ ],
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ },
32
+ "client": {
33
+ "inject": [
34
+ "@deepseek-ai/dsh-client-runtime",
35
+ "@deepseek-ai/dsh-client-ui-conversation"
36
+ ],
37
+ "platform": "web"
38
+ }
39
+ },
40
+ "files": [
41
+ "lib",
42
+ "cordis.patch.yml",
43
+ "LICENSE",
44
+ "screenshots"
45
+ ],
46
+ "license": "MIT",
47
+ "scripts": {
48
+ "build": "node scripts/build.mjs",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "node --experimental-transform-types --test \"test/*.test.ts\""
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^26.2.0",
54
+ "@types/react": "^18.3.31",
55
+ "@types/react-dom": "^18",
56
+ "esbuild": "^0.28.2",
57
+ "typescript": "^5.6.0"
58
+ }
59
+ }
Binary file
Binary file
Binary file
Binary file