@us_momo/dsh-erp-analysis 0.0.0-stage → 1.3.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/README.md +18 -2
- package/client.mjs +265 -0
- package/company-public.json +1 -0
- package/package.json +44 -3
- package/plugin/client.js +185 -0
- package/plugin/cordis.patch.yml +3 -0
- package/plugin/icon.svg +1 -0
- package/plugin/index.mjs +27 -0
- package/plugin/locale/en.json +1 -0
- package/plugin/locale/zh.json +1 -0
- package/plugin/runtime.mjs +97 -0
- package/release.json +1 -0
- package/release.mjs +403 -0
package/release.mjs
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFile,
|
|
3
|
+
writeFile,
|
|
4
|
+
mkdir,
|
|
5
|
+
rename,
|
|
6
|
+
stat,
|
|
7
|
+
readdir,
|
|
8
|
+
rm,
|
|
9
|
+
cp,
|
|
10
|
+
copyFile,
|
|
11
|
+
rmdir,
|
|
12
|
+
} from "node:fs/promises";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
+
import { join, resolve, dirname } from "node:path";
|
|
15
|
+
|
|
16
|
+
export const digest = (bytes) =>
|
|
17
|
+
createHash("sha256").update(bytes).digest("hex");
|
|
18
|
+
export function trustedErpUrl(value) {
|
|
19
|
+
const url = new URL(value);
|
|
20
|
+
if (
|
|
21
|
+
url.origin === "http://211.97.68.49:10020" &&
|
|
22
|
+
!url.username &&
|
|
23
|
+
!url.password &&
|
|
24
|
+
!url.hash
|
|
25
|
+
)
|
|
26
|
+
return url;
|
|
27
|
+
return trustedUrl(value);
|
|
28
|
+
}
|
|
29
|
+
export function trustedUrl(value) {
|
|
30
|
+
const url = new URL(value);
|
|
31
|
+
if (
|
|
32
|
+
url.username ||
|
|
33
|
+
url.password ||
|
|
34
|
+
url.hash ||
|
|
35
|
+
!(
|
|
36
|
+
url.protocol === "https:" ||
|
|
37
|
+
(url.protocol === "http:" &&
|
|
38
|
+
["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
throw new Error("请使用 HTTPS 公司地址。");
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
export async function readJson(path, fallback) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (e.code === "ENOENT" && fallback !== undefined) return fallback;
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function atomicJson(path, value) {
|
|
53
|
+
await mkdir(dirname(path), { recursive: true });
|
|
54
|
+
const temp = path + "." + randomUUID() + ".tmp";
|
|
55
|
+
await writeFile(temp, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
56
|
+
try {
|
|
57
|
+
await rename(temp, path);
|
|
58
|
+
} finally {
|
|
59
|
+
await rm(temp, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function currentRelease(root) {
|
|
63
|
+
const marker = await readJson(join(root, "current.json"));
|
|
64
|
+
return { ...marker, path: join(root, "versions", marker.version) };
|
|
65
|
+
}
|
|
66
|
+
export async function fetchJson(url, maxBytes = 64 * 1024 * 1024) {
|
|
67
|
+
trustedUrl(url);
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
redirect: "error",
|
|
70
|
+
signal: AbortSignal.timeout(30000),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) throw new Error(`下载失败 (${response.status})`);
|
|
73
|
+
if (Number(response.headers.get("content-length")) > maxBytes)
|
|
74
|
+
throw new Error("更新包超过尺寸限制。");
|
|
75
|
+
let size = 0;
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for await (const chunk of response.body) {
|
|
78
|
+
size += chunk.length;
|
|
79
|
+
if (size > maxBytes) throw new Error("更新包超过尺寸限制。");
|
|
80
|
+
chunks.push(chunk);
|
|
81
|
+
}
|
|
82
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
83
|
+
}
|
|
84
|
+
export function validateRelease(manifest) {
|
|
85
|
+
if (
|
|
86
|
+
manifest.schemaVersion !== 1 ||
|
|
87
|
+
!/^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?$/.test(manifest.version) ||
|
|
88
|
+
!Array.isArray(manifest.files) ||
|
|
89
|
+
!manifest.files.length ||
|
|
90
|
+
manifest.files.length > 1000
|
|
91
|
+
)
|
|
92
|
+
throw new Error("更新包格式无效。");
|
|
93
|
+
const required = [
|
|
94
|
+
"app/cli.mjs",
|
|
95
|
+
"app/bridge.mjs",
|
|
96
|
+
"app/client.mjs",
|
|
97
|
+
"app/release.mjs",
|
|
98
|
+
"app/control.mjs",
|
|
99
|
+
"app/launcher.mjs",
|
|
100
|
+
"app/control.html",
|
|
101
|
+
"workspace/AGENTS.md",
|
|
102
|
+
];
|
|
103
|
+
for (const path of required)
|
|
104
|
+
if (!manifest.files.some((file) => file.path === path))
|
|
105
|
+
throw new Error("更新包缺少必要程序文件。");
|
|
106
|
+
const skills = manifest.files.filter((file) =>
|
|
107
|
+
/^workspace\/\.dsh\/skills\/[^/]+\/SKILL\.md$/.test(file.path),
|
|
108
|
+
);
|
|
109
|
+
if (![11, 12].includes(skills.length))
|
|
110
|
+
throw new Error("更新包业务 Skill 不完整。");
|
|
111
|
+
const paths = new Set();
|
|
112
|
+
let total = 0;
|
|
113
|
+
return manifest.files.map((file) => {
|
|
114
|
+
const path = file.path;
|
|
115
|
+
if (
|
|
116
|
+
typeof path !== "string" ||
|
|
117
|
+
path.length > 220 ||
|
|
118
|
+
path.includes("\\") ||
|
|
119
|
+
!path
|
|
120
|
+
.split("/")
|
|
121
|
+
.every(
|
|
122
|
+
(s) =>
|
|
123
|
+
s &&
|
|
124
|
+
s !== "." &&
|
|
125
|
+
s !== ".." &&
|
|
126
|
+
!/[<>:"|?*\x00-\x1f]/.test(s) &&
|
|
127
|
+
!/[. ]$/.test(s) &&
|
|
128
|
+
!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(s),
|
|
129
|
+
) ||
|
|
130
|
+
!/^(app|workspace)\//.test(path)
|
|
131
|
+
)
|
|
132
|
+
throw new Error("更新包含非法路径。");
|
|
133
|
+
const canonical = path.normalize("NFC").toLowerCase();
|
|
134
|
+
if (paths.has(canonical)) throw new Error("更新包含重复路径。");
|
|
135
|
+
paths.add(canonical);
|
|
136
|
+
if (
|
|
137
|
+
typeof file.contentBase64 !== "string" ||
|
|
138
|
+
file.contentBase64.length > 16 * 1024 * 1024 ||
|
|
139
|
+
!/^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
|
|
140
|
+
file.contentBase64,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
throw new Error("更新文件内容无效。");
|
|
144
|
+
const bytes = Buffer.from(file.contentBase64, "base64");
|
|
145
|
+
total += bytes.length;
|
|
146
|
+
if (
|
|
147
|
+
total > 48 * 1024 * 1024 ||
|
|
148
|
+
bytes.length !== file.bytes ||
|
|
149
|
+
digest(bytes) !== file.sha256
|
|
150
|
+
)
|
|
151
|
+
throw new Error("更新文件校验失败。");
|
|
152
|
+
return { path, bytes };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
export async function liveLeases(root) {
|
|
156
|
+
const dir = join(root, "leases");
|
|
157
|
+
await mkdir(dir, { recursive: true });
|
|
158
|
+
const live = [];
|
|
159
|
+
for (const name of await readdir(dir)) {
|
|
160
|
+
const path = join(dir, name);
|
|
161
|
+
const value = await readJson(path);
|
|
162
|
+
try {
|
|
163
|
+
process.kill(value.pid, 0);
|
|
164
|
+
live.push(value);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
if (e.code === "ESRCH") await rm(path, { force: true });
|
|
167
|
+
else live.push(value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return live;
|
|
171
|
+
}
|
|
172
|
+
async function installReleaseUnlocked(root, manifest, { defer = true } = {}) {
|
|
173
|
+
const files = validateRelease(manifest);
|
|
174
|
+
await mkdir(join(root, "versions"), { recursive: true });
|
|
175
|
+
const target = join(root, "versions", manifest.version);
|
|
176
|
+
let exists = false;
|
|
177
|
+
try {
|
|
178
|
+
await stat(target);
|
|
179
|
+
exists = true;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
if (e.code !== "ENOENT") throw e;
|
|
182
|
+
}
|
|
183
|
+
const fingerprint = digest(Buffer.from(JSON.stringify(manifest)));
|
|
184
|
+
if (exists) {
|
|
185
|
+
const receipt = await readJson(join(target, "release-receipt.json"));
|
|
186
|
+
if (receipt.fingerprint !== fingerprint)
|
|
187
|
+
throw new Error("相同版本已有不同内容,请发布新版本。");
|
|
188
|
+
} else {
|
|
189
|
+
const staging = join(root, "versions", ".stage-" + randomUUID());
|
|
190
|
+
await mkdir(staging);
|
|
191
|
+
try {
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const output = join(staging, ...file.path.split("/"));
|
|
194
|
+
await mkdir(dirname(output), { recursive: true });
|
|
195
|
+
await writeFile(output, file.bytes);
|
|
196
|
+
}
|
|
197
|
+
await atomicJson(join(staging, "release-receipt.json"), {
|
|
198
|
+
version: manifest.version,
|
|
199
|
+
fingerprint,
|
|
200
|
+
});
|
|
201
|
+
await rename(staging, target);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
await rm(staging, { recursive: true, force: true });
|
|
204
|
+
throw e;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const marker = { version: manifest.version, fingerprint };
|
|
208
|
+
const current = await readJson(join(root, "current.json"), null);
|
|
209
|
+
if (current?.version === marker.version && current.fingerprint === fingerprint)
|
|
210
|
+
return { ...marker, status: "installed" };
|
|
211
|
+
if (defer && (await liveLeases(root)).length) {
|
|
212
|
+
await atomicJson(join(root, "pending.json"), marker);
|
|
213
|
+
return { ...marker, status: "pending-reload" };
|
|
214
|
+
}
|
|
215
|
+
await publishWorkspace(root, target, marker);
|
|
216
|
+
await rm(join(root, "pending.json"), { force: true });
|
|
217
|
+
return { ...marker, status: "installed" };
|
|
218
|
+
}
|
|
219
|
+
async function publishWorkspace(root, target, marker) {
|
|
220
|
+
const source = join(target, "workspace"),
|
|
221
|
+
workspace = join(root, "workspace");
|
|
222
|
+
await mkdir(join(workspace, "reports"), { recursive: true });
|
|
223
|
+
const stage = join(workspace, ".company-stage-" + randomUUID()),
|
|
224
|
+
previous = join(workspace, ".company-previous-" + randomUUID());
|
|
225
|
+
await mkdir(stage);
|
|
226
|
+
await mkdir(previous);
|
|
227
|
+
const old = [],
|
|
228
|
+
installed = [];
|
|
229
|
+
try {
|
|
230
|
+
await copyFile(join(source, "AGENTS.md"), join(stage, "AGENTS.md"));
|
|
231
|
+
await cp(join(source, ".dsh"), join(stage, ".dsh"), {
|
|
232
|
+
recursive: true,
|
|
233
|
+
errorOnExist: true,
|
|
234
|
+
});
|
|
235
|
+
for (const name of ["AGENTS.md", ".dsh"]) {
|
|
236
|
+
try {
|
|
237
|
+
await rename(join(workspace, name), join(previous, name));
|
|
238
|
+
old.push(name);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
if (e.code !== "ENOENT") throw e;
|
|
241
|
+
}
|
|
242
|
+
await rename(join(stage, name), join(workspace, name));
|
|
243
|
+
installed.push(name);
|
|
244
|
+
}
|
|
245
|
+
await atomicJson(join(root, "current.json"), marker);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
for (const name of installed.reverse())
|
|
248
|
+
await rename(join(workspace, name), join(stage, name));
|
|
249
|
+
for (const name of old.reverse())
|
|
250
|
+
await rename(join(previous, name), join(workspace, name));
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function activatePendingUnlocked(root) {
|
|
255
|
+
const pending = await readJson(join(root, "pending.json"), null);
|
|
256
|
+
if (!pending || (await liveLeases(root)).length) return false;
|
|
257
|
+
await publishWorkspace(
|
|
258
|
+
root,
|
|
259
|
+
join(root, "versions", pending.version),
|
|
260
|
+
pending,
|
|
261
|
+
);
|
|
262
|
+
await rm(join(root, "pending.json"), { force: true });
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
export async function configureProfile(
|
|
266
|
+
profile,
|
|
267
|
+
root,
|
|
268
|
+
nodePath,
|
|
269
|
+
{ checkOnly = false } = {},
|
|
270
|
+
) {
|
|
271
|
+
const path = join(profile, "cordis.patch.yml");
|
|
272
|
+
await mkdir(profile, { recursive: true });
|
|
273
|
+
let existing = "";
|
|
274
|
+
try {
|
|
275
|
+
existing = await readFile(path, "utf8");
|
|
276
|
+
} catch (e) {
|
|
277
|
+
if (e.code !== "ENOENT") throw e;
|
|
278
|
+
}
|
|
279
|
+
const begin = "# BEGIN COMPANY DSH TEAM";
|
|
280
|
+
const end = "# END COMPANY DSH TEAM";
|
|
281
|
+
const start = existing.indexOf(begin);
|
|
282
|
+
let clean = existing;
|
|
283
|
+
if (start >= 0) {
|
|
284
|
+
const finish = existing.indexOf(end, start);
|
|
285
|
+
if (finish < 0) throw new Error("公司配置区不完整,请恢复配置备份。");
|
|
286
|
+
clean = existing.slice(0, start) + existing.slice(finish + end.length);
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
/serverName\s*:\s*['"]?erp_read\b/.test(clean) ||
|
|
290
|
+
/['"]serverName['"]\s*:\s*['"]erp_read['"]/.test(clean)
|
|
291
|
+
)
|
|
292
|
+
throw new Error(
|
|
293
|
+
"已存在 erp_read 连接。请先备份、停用并移除原连接配置,再安装公司连接。",
|
|
294
|
+
);
|
|
295
|
+
const store = await readJson(
|
|
296
|
+
join(dirname(dirname(profile)), "storages", "mcp_connector.json"),
|
|
297
|
+
null,
|
|
298
|
+
);
|
|
299
|
+
if (store && JSON.stringify(store).includes("erp_read"))
|
|
300
|
+
throw new Error("MCP 连接管理器已有 erp_read 记录。请先停用并移除旧记录。");
|
|
301
|
+
let homePatch = "";
|
|
302
|
+
try {
|
|
303
|
+
homePatch = await readFile(
|
|
304
|
+
join(dirname(dirname(profile)), "cordis.patch.yml"),
|
|
305
|
+
"utf8",
|
|
306
|
+
);
|
|
307
|
+
} catch (e) {
|
|
308
|
+
if (e.code !== "ENOENT") throw e;
|
|
309
|
+
}
|
|
310
|
+
if (/serverName\s*:\s*['"]?erp_read\b/.test(homePatch))
|
|
311
|
+
throw new Error("DSH 全局配置已有 erp_read 连接,请先备份、停用并移除原连接配置。");
|
|
312
|
+
if (checkOnly) return path;
|
|
313
|
+
const block = `${begin}\n- insert:\n - id: company-erp-read\n name: '@deepseek-ai/dsh-mcp-client'\n config:\n serverName: erp_read\n transport: stdio\n command: ${JSON.stringify(nodePath.replaceAll("\\", "/"))}\n args: [${JSON.stringify(join(root, "launcher.mjs").replaceAll("\\", "/"))}, bridge, --root, ${JSON.stringify(root.replaceAll("\\", "/"))}]\n toolCallTimeoutMs: 120000\n failOnStartupError: false\n${end}\n`;
|
|
314
|
+
await writeFile(path + ".company-before", existing, { flag: "wx" }).catch(
|
|
315
|
+
(e) => {
|
|
316
|
+
if (e.code !== "EEXIST") throw e;
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
const temp = path + ".company.tmp";
|
|
320
|
+
await writeFile(temp, clean.trimEnd() + "\n" + block);
|
|
321
|
+
await rename(temp, path);
|
|
322
|
+
return path;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function withActivationLock(root, operation) {
|
|
326
|
+
await mkdir(root, { recursive: true });
|
|
327
|
+
const lock = join(root, "activation.lock");
|
|
328
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
329
|
+
try {
|
|
330
|
+
await mkdir(lock);
|
|
331
|
+
await writeFile(
|
|
332
|
+
join(lock, "owner.json"),
|
|
333
|
+
JSON.stringify({ pid: process.pid }),
|
|
334
|
+
);
|
|
335
|
+
break;
|
|
336
|
+
} catch (e) {
|
|
337
|
+
if (e.code !== "EEXIST") throw e;
|
|
338
|
+
let owner = null;
|
|
339
|
+
try {
|
|
340
|
+
owner = await readJson(join(lock, "owner.json"), null);
|
|
341
|
+
} catch {}
|
|
342
|
+
let dead = false;
|
|
343
|
+
if (owner?.pid) {
|
|
344
|
+
try {
|
|
345
|
+
process.kill(owner.pid, 0);
|
|
346
|
+
} catch (failure) {
|
|
347
|
+
dead = failure.code === "ESRCH";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (dead) {
|
|
351
|
+
await rm(join(lock, "owner.json"), { force: true });
|
|
352
|
+
await rmdir(lock).catch((failure) => {
|
|
353
|
+
if (!["ENOENT", "ENOTEMPTY"].includes(failure.code)) throw failure;
|
|
354
|
+
});
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (!owner) {
|
|
358
|
+
const age = Date.now() - (await stat(lock)).mtimeMs;
|
|
359
|
+
if (age > 30000) {
|
|
360
|
+
await rm(join(lock, "owner.json"), { force: true });
|
|
361
|
+
await rmdir(lock).catch(() => {});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (attempt === 199) throw new Error("公司更新正在办理,请稍后重试。");
|
|
366
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
return await operation();
|
|
371
|
+
} finally {
|
|
372
|
+
await rm(join(lock, "owner.json"), { force: true });
|
|
373
|
+
await rmdir(lock);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
export async function installRelease(root, manifest, options) {
|
|
377
|
+
return withActivationLock(root, () =>
|
|
378
|
+
installReleaseUnlocked(root, manifest, options),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
export async function activatePending(root) {
|
|
382
|
+
return withActivationLock(root, () => activatePendingUnlocked(root));
|
|
383
|
+
}
|
|
384
|
+
export async function beginBridge(root, { version } = {}) {
|
|
385
|
+
return withActivationLock(root, async () => {
|
|
386
|
+
await activatePendingUnlocked(root);
|
|
387
|
+
let release = await currentRelease(root);
|
|
388
|
+
if (version !== undefined) {
|
|
389
|
+
if (!/^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?$/.test(version))
|
|
390
|
+
throw new Error("公司方法版本无效。");
|
|
391
|
+
const path = join(root, "versions", version);
|
|
392
|
+
const receipt = await readJson(join(path, "release-receipt.json"));
|
|
393
|
+
release = { ...receipt, version, path };
|
|
394
|
+
}
|
|
395
|
+
const lease = join(root, "leases", randomUUID() + ".json");
|
|
396
|
+
await atomicJson(lease, { pid: process.pid, version: release.version });
|
|
397
|
+
return { release, lease };
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
export async function endBridge(root, snapshot) {
|
|
401
|
+
await rm(snapshot.lease, { force: true });
|
|
402
|
+
await activatePending(root);
|
|
403
|
+
}
|