chatccc 0.2.259 → 0.2.261
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 +20 -11
- package/config.sample.json +23 -8
- package/dist/src/adapters/claude-adapter.js +3 -2
- package/dist/src/adapters/dsh-adapter.js +230 -0
- package/dist/src/agent-tool.js +2 -1
- package/dist/src/card-action-parser.js +1 -0
- package/dist/src/cards.js +11 -3
- package/dist/src/config.js +34 -2
- package/dist/src/engines/engine-manager.js +451 -0
- package/dist/src/engines/engine-specs.js +157 -0
- package/dist/src/feishu-api.js +2 -1
- package/dist/src/orchestrator.js +21 -10
- package/dist/src/session.js +23 -0
- package/dist/src/web-ui.js +286 -200
- package/package.json +1 -1
- package/dist/src/claude-sdk-installer.js +0 -249
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { delimiter, dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
const STEP_DEFINITIONS = [
|
|
8
|
+
["preflight", "检查运行环境"],
|
|
9
|
+
["prepare", "准备临时目录"],
|
|
10
|
+
["download_install", "下载并安装依赖"],
|
|
11
|
+
["verify_packages", "校验文件与版本"],
|
|
12
|
+
["runtime_handshake", "启动并验证 Runtime"],
|
|
13
|
+
["activate", "原子切换版本"],
|
|
14
|
+
["cleanup", "清理旧版本"],
|
|
15
|
+
];
|
|
16
|
+
export const DEFAULT_ENGINE_ROOT = join(homedir(), ".chatccc", "engines");
|
|
17
|
+
export function createCoalescedAsyncTask(task) {
|
|
18
|
+
let requested = false;
|
|
19
|
+
let active = null;
|
|
20
|
+
let failed = false;
|
|
21
|
+
let failure;
|
|
22
|
+
const start = () => {
|
|
23
|
+
if (active || failed)
|
|
24
|
+
return;
|
|
25
|
+
active = (async () => {
|
|
26
|
+
while (requested) {
|
|
27
|
+
requested = false;
|
|
28
|
+
await task();
|
|
29
|
+
}
|
|
30
|
+
})().catch((error) => {
|
|
31
|
+
failed = true;
|
|
32
|
+
failure = error;
|
|
33
|
+
requested = false;
|
|
34
|
+
}).finally(() => {
|
|
35
|
+
active = null;
|
|
36
|
+
if (requested)
|
|
37
|
+
start();
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
schedule() {
|
|
42
|
+
requested = true;
|
|
43
|
+
start();
|
|
44
|
+
},
|
|
45
|
+
async flush() {
|
|
46
|
+
if (!failed) {
|
|
47
|
+
requested = true;
|
|
48
|
+
start();
|
|
49
|
+
}
|
|
50
|
+
while (active)
|
|
51
|
+
await active;
|
|
52
|
+
if (failed)
|
|
53
|
+
throw failure;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function renameWithTransientRetry(source, destination) {
|
|
58
|
+
const retriable = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
59
|
+
for (let attempt = 0;; attempt += 1) {
|
|
60
|
+
try {
|
|
61
|
+
await rename(source, destination);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const code = error.code ?? "";
|
|
66
|
+
if (!retriable.has(code) || attempt >= 5)
|
|
67
|
+
throw error;
|
|
68
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 20 * (2 ** attempt)));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function resolveNpmInvocation() {
|
|
73
|
+
const candidates = [
|
|
74
|
+
process.env.npm_execpath,
|
|
75
|
+
join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
|
76
|
+
...(process.env.PATH ?? "").split(delimiter).filter(Boolean).map((pathDir) => join(pathDir, "node_modules", "npm", "bin", "npm-cli.js")),
|
|
77
|
+
];
|
|
78
|
+
const npmCliPath = candidates.find((candidate) => Boolean(candidate && existsSync(candidate)));
|
|
79
|
+
if (!npmCliPath) {
|
|
80
|
+
throw new Error("找不到 npm-cli.js;请先安装 npm,并确保 npm 与当前 Node.js 位于同一环境。");
|
|
81
|
+
}
|
|
82
|
+
return { command: process.execPath, argsPrefix: [npmCliPath] };
|
|
83
|
+
}
|
|
84
|
+
export class EngineManager {
|
|
85
|
+
rootDir;
|
|
86
|
+
specs = new Map();
|
|
87
|
+
installPackages;
|
|
88
|
+
verifyRuntime;
|
|
89
|
+
nodeVersion;
|
|
90
|
+
activeInstalls = new Map();
|
|
91
|
+
persistTails = new Map();
|
|
92
|
+
constructor(options) {
|
|
93
|
+
this.rootDir = options.rootDir ?? DEFAULT_ENGINE_ROOT;
|
|
94
|
+
for (const spec of options.specs)
|
|
95
|
+
this.specs.set(spec.id, spec);
|
|
96
|
+
this.installPackages = options.installPackages ?? defaultInstallPackages;
|
|
97
|
+
this.verifyRuntime = options.verifyRuntime;
|
|
98
|
+
this.nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
99
|
+
}
|
|
100
|
+
listSpecs() {
|
|
101
|
+
return [...this.specs.values()];
|
|
102
|
+
}
|
|
103
|
+
getSpec(engineId) {
|
|
104
|
+
const spec = this.specs.get(engineId);
|
|
105
|
+
if (!spec)
|
|
106
|
+
throw new Error(`Unknown engine: ${engineId}`);
|
|
107
|
+
return spec;
|
|
108
|
+
}
|
|
109
|
+
async getStatus(engineId) {
|
|
110
|
+
const spec = this.getSpec(engineId);
|
|
111
|
+
const pointer = await this.readPointer(spec);
|
|
112
|
+
const entryPath = pointer ? join(this.engineDir(spec), pointer.directory, spec.entryRelativePath) : null;
|
|
113
|
+
const installed = Boolean(entryPath && existsSync(entryPath));
|
|
114
|
+
return {
|
|
115
|
+
id: spec.id,
|
|
116
|
+
label: spec.label,
|
|
117
|
+
installed,
|
|
118
|
+
version: installed ? pointer?.version ?? null : null,
|
|
119
|
+
targetVersion: spec.version,
|
|
120
|
+
entryPath: installed ? entryPath : null,
|
|
121
|
+
running: this.activeInstalls.has(engineId),
|
|
122
|
+
job: await this.readJob(spec),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async getEntryPath(engineId) {
|
|
126
|
+
const status = await this.getStatus(engineId);
|
|
127
|
+
if (!status.installed || !status.entryPath) {
|
|
128
|
+
throw new Error(`${status.label} 尚未安装,请先在设置页安装引擎。`);
|
|
129
|
+
}
|
|
130
|
+
return status.entryPath;
|
|
131
|
+
}
|
|
132
|
+
async startInstall(engineId) {
|
|
133
|
+
const running = this.activeInstalls.get(engineId);
|
|
134
|
+
if (running)
|
|
135
|
+
return this.readJob(this.getSpec(engineId)).then((job) => job ?? this.newJob(this.getSpec(engineId)));
|
|
136
|
+
const spec = this.getSpec(engineId);
|
|
137
|
+
const job = this.newJob(spec);
|
|
138
|
+
await this.persistJob(spec, job);
|
|
139
|
+
const task = this.runInstall(spec, job).finally(() => this.activeInstalls.delete(engineId));
|
|
140
|
+
this.activeInstalls.set(engineId, task);
|
|
141
|
+
return cloneJob(job);
|
|
142
|
+
}
|
|
143
|
+
async install(engineId) {
|
|
144
|
+
await this.startInstall(engineId);
|
|
145
|
+
return this.waitForInstall(engineId);
|
|
146
|
+
}
|
|
147
|
+
async waitForInstall(engineId) {
|
|
148
|
+
const active = this.activeInstalls.get(engineId);
|
|
149
|
+
if (active)
|
|
150
|
+
return active;
|
|
151
|
+
const job = await this.readJob(this.getSpec(engineId));
|
|
152
|
+
if (!job)
|
|
153
|
+
throw new Error(`No install job for engine: ${engineId}`);
|
|
154
|
+
return job;
|
|
155
|
+
}
|
|
156
|
+
newJob(spec) {
|
|
157
|
+
const now = new Date().toISOString();
|
|
158
|
+
return {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
jobId: randomUUID(),
|
|
161
|
+
engineId: spec.id,
|
|
162
|
+
targetVersion: spec.version,
|
|
163
|
+
state: "running",
|
|
164
|
+
percent: 0,
|
|
165
|
+
startedAt: now,
|
|
166
|
+
updatedAt: now,
|
|
167
|
+
steps: STEP_DEFINITIONS.map(([id, label]) => ({ id, label, state: "pending", percent: 0, message: "等待中" })),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async runInstall(spec, job) {
|
|
171
|
+
const stagingDir = join(this.rootDir, ".staging", `${spec.id}-${job.jobId}`);
|
|
172
|
+
let publishedDir = null;
|
|
173
|
+
try {
|
|
174
|
+
await this.runStep(spec, job, "preflight", async (update) => {
|
|
175
|
+
await update(20, `Node.js ${this.nodeVersion}`);
|
|
176
|
+
if (compareVersions(this.nodeVersion, spec.minimumNodeVersion) < 0) {
|
|
177
|
+
throw new Error(`${spec.label} 要求 Node.js >= ${spec.minimumNodeVersion},当前为 ${this.nodeVersion}`);
|
|
178
|
+
}
|
|
179
|
+
await mkdir(this.rootDir, { recursive: true });
|
|
180
|
+
await update(100, "运行环境可用");
|
|
181
|
+
});
|
|
182
|
+
await this.runStep(spec, job, "prepare", async (update) => {
|
|
183
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
184
|
+
await mkdir(stagingDir, { recursive: true });
|
|
185
|
+
await writeFile(join(stagingDir, "package.json"), JSON.stringify({
|
|
186
|
+
name: `chatccc-engine-${spec.id}`,
|
|
187
|
+
private: true,
|
|
188
|
+
type: "module",
|
|
189
|
+
dependencies: spec.packages,
|
|
190
|
+
}, null, 2) + "\n", "utf8");
|
|
191
|
+
await spec.prepareInstallation?.(stagingDir);
|
|
192
|
+
await update(100, "临时安装目录已准备");
|
|
193
|
+
});
|
|
194
|
+
await this.runStep(spec, job, "download_install", async (update) => {
|
|
195
|
+
await this.installPackages(stagingDir, spec, update);
|
|
196
|
+
await update(100, "依赖安装完成");
|
|
197
|
+
});
|
|
198
|
+
await this.runStep(spec, job, "verify_packages", async (update) => {
|
|
199
|
+
const entries = Object.entries(spec.packages);
|
|
200
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
201
|
+
const [packageName, expectedVersion] = entries[index];
|
|
202
|
+
const packageJson = join(stagingDir, "node_modules", ...packageName.split("/"), "package.json");
|
|
203
|
+
const parsed = JSON.parse(await readFile(packageJson, "utf8"));
|
|
204
|
+
if (parsed.version !== expectedVersion) {
|
|
205
|
+
throw new Error(`${packageName} 版本校验失败:期望 ${expectedVersion},实际 ${String(parsed.version)}`);
|
|
206
|
+
}
|
|
207
|
+
await update(Math.round(((index + 1) / entries.length) * 90), `已校验 ${packageName}`);
|
|
208
|
+
}
|
|
209
|
+
const entry = join(stagingDir, spec.entryRelativePath);
|
|
210
|
+
if (!existsSync(entry))
|
|
211
|
+
throw new Error(`引擎入口不存在:${entry}`);
|
|
212
|
+
await update(100, "文件和版本校验通过");
|
|
213
|
+
});
|
|
214
|
+
await this.runStep(spec, job, "runtime_handshake", async (update) => {
|
|
215
|
+
await update(10, "正在启动 Runtime");
|
|
216
|
+
if (this.verifyRuntime)
|
|
217
|
+
await this.verifyRuntime(stagingDir, spec);
|
|
218
|
+
else
|
|
219
|
+
await spec.verifyRuntime?.(stagingDir);
|
|
220
|
+
await update(100, "Runtime 握手成功");
|
|
221
|
+
});
|
|
222
|
+
await this.runStep(spec, job, "activate", async (update) => {
|
|
223
|
+
const directoryName = `${spec.version}-${job.jobId}`;
|
|
224
|
+
const versionsDir = join(this.engineDir(spec), "versions");
|
|
225
|
+
publishedDir = join(versionsDir, directoryName);
|
|
226
|
+
await mkdir(versionsDir, { recursive: true });
|
|
227
|
+
await rename(stagingDir, publishedDir);
|
|
228
|
+
const pointer = {
|
|
229
|
+
schemaVersion: 1,
|
|
230
|
+
version: spec.version,
|
|
231
|
+
directory: relative(this.engineDir(spec), publishedDir).replaceAll("\\", "/"),
|
|
232
|
+
activatedAt: new Date().toISOString(),
|
|
233
|
+
};
|
|
234
|
+
const pointerPath = join(this.engineDir(spec), "current.json");
|
|
235
|
+
const temporaryPointer = `${pointerPath}.${job.jobId}.tmp`;
|
|
236
|
+
await writeFile(temporaryPointer, JSON.stringify(pointer, null, 2) + "\n", "utf8");
|
|
237
|
+
await renameWithTransientRetry(temporaryPointer, pointerPath);
|
|
238
|
+
await update(100, `已切换到 v${spec.version}`);
|
|
239
|
+
});
|
|
240
|
+
await this.runStep(spec, job, "cleanup", async (update) => {
|
|
241
|
+
const versionsDir = join(this.engineDir(spec), "versions");
|
|
242
|
+
const keep = publishedDir ? resolve(publishedDir) : "";
|
|
243
|
+
const entries = await readdir(versionsDir, { withFileTypes: true });
|
|
244
|
+
const old = entries.filter((entry) => entry.isDirectory() && resolve(versionsDir, entry.name) !== keep);
|
|
245
|
+
for (let index = 0; index < old.length; index += 1) {
|
|
246
|
+
await rm(join(versionsDir, old[index].name), { recursive: true, force: true });
|
|
247
|
+
await update(Math.round(((index + 1) / Math.max(old.length, 1)) * 100), `已清理 ${old[index].name}`);
|
|
248
|
+
}
|
|
249
|
+
await update(100, old.length ? "旧版本已清理" : "无需清理旧版本");
|
|
250
|
+
});
|
|
251
|
+
job.state = "succeeded";
|
|
252
|
+
job.percent = 100;
|
|
253
|
+
job.updatedAt = new Date().toISOString();
|
|
254
|
+
await this.persistJob(spec, job);
|
|
255
|
+
return cloneJob(job);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
259
|
+
job.state = "failed";
|
|
260
|
+
job.error = message.slice(0, 1000);
|
|
261
|
+
job.updatedAt = new Date().toISOString();
|
|
262
|
+
await this.persistJob(spec, job);
|
|
263
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => { });
|
|
264
|
+
return cloneJob(job);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async runStep(spec, job, stepId, operation) {
|
|
268
|
+
const step = job.steps.find((candidate) => candidate.id === stepId);
|
|
269
|
+
step.state = "running";
|
|
270
|
+
step.message = "进行中";
|
|
271
|
+
await this.recalculateAndPersist(spec, job);
|
|
272
|
+
const update = async (percent, message) => {
|
|
273
|
+
step.percent = Math.max(0, Math.min(100, Math.round(percent)));
|
|
274
|
+
step.message = message;
|
|
275
|
+
await this.recalculateAndPersist(spec, job);
|
|
276
|
+
};
|
|
277
|
+
try {
|
|
278
|
+
await operation(update);
|
|
279
|
+
step.state = "completed";
|
|
280
|
+
step.percent = 100;
|
|
281
|
+
await this.recalculateAndPersist(spec, job);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
step.state = "failed";
|
|
285
|
+
step.error = error instanceof Error ? error.message : String(error);
|
|
286
|
+
step.message = "失败";
|
|
287
|
+
await this.recalculateAndPersist(spec, job);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async recalculateAndPersist(spec, job) {
|
|
292
|
+
job.percent = Math.round(job.steps.reduce((sum, step) => sum + step.percent, 0) / job.steps.length);
|
|
293
|
+
job.updatedAt = new Date().toISOString();
|
|
294
|
+
await this.persistJob(spec, job);
|
|
295
|
+
}
|
|
296
|
+
engineDir(spec) {
|
|
297
|
+
return join(this.rootDir, spec.id);
|
|
298
|
+
}
|
|
299
|
+
jobPath(spec) {
|
|
300
|
+
return join(this.rootDir, "engine-jobs", `${spec.id}.json`);
|
|
301
|
+
}
|
|
302
|
+
async persistJob(spec, job) {
|
|
303
|
+
const path = this.jobPath(spec);
|
|
304
|
+
const contents = JSON.stringify(job, null, 2) + "\n";
|
|
305
|
+
const previous = this.persistTails.get(spec.id) ?? Promise.resolve();
|
|
306
|
+
const operation = previous.catch(() => { }).then(async () => {
|
|
307
|
+
await mkdir(dirname(path), { recursive: true });
|
|
308
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
309
|
+
try {
|
|
310
|
+
await writeFile(temporary, contents, "utf8");
|
|
311
|
+
await renameWithTransientRetry(temporary, path);
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
this.persistTails.set(spec.id, operation);
|
|
318
|
+
try {
|
|
319
|
+
await operation;
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
if (this.persistTails.get(spec.id) === operation)
|
|
323
|
+
this.persistTails.delete(spec.id);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async readJob(spec) {
|
|
327
|
+
try {
|
|
328
|
+
const parsed = JSON.parse(await readFile(this.jobPath(spec), "utf8"));
|
|
329
|
+
if (parsed.state === "running" && !this.activeInstalls.has(spec.id)) {
|
|
330
|
+
parsed.state = "failed";
|
|
331
|
+
parsed.error = "上一次安装任务因进程退出而中断,请重试。";
|
|
332
|
+
const running = parsed.steps.find((step) => step.state === "running");
|
|
333
|
+
if (running) {
|
|
334
|
+
running.state = "failed";
|
|
335
|
+
running.message = "安装任务已中断";
|
|
336
|
+
running.error = parsed.error;
|
|
337
|
+
}
|
|
338
|
+
parsed.updatedAt = new Date().toISOString();
|
|
339
|
+
await this.persistJob(spec, parsed);
|
|
340
|
+
}
|
|
341
|
+
return parsed;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async readPointer(spec) {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = JSON.parse(await readFile(join(this.engineDir(spec), "current.json"), "utf8"));
|
|
350
|
+
if (typeof parsed.version !== "string" || typeof parsed.directory !== "string")
|
|
351
|
+
return null;
|
|
352
|
+
const resolved = resolve(this.engineDir(spec), parsed.directory);
|
|
353
|
+
const versionsDir = resolve(this.engineDir(spec), "versions");
|
|
354
|
+
if (resolved !== versionsDir && !resolved.startsWith(`${versionsDir}\\`) && !resolved.startsWith(`${versionsDir}/`))
|
|
355
|
+
return null;
|
|
356
|
+
return { version: parsed.version, directory: parsed.directory };
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function defaultInstallPackages(installationDir, spec, onProgress) {
|
|
364
|
+
await new Promise((resolvePromise, reject) => {
|
|
365
|
+
let npmInvocation;
|
|
366
|
+
try {
|
|
367
|
+
npmInvocation = resolveNpmInvocation();
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
reject(error);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const child = spawn(npmInvocation.command, [...npmInvocation.argsPrefix, "install", "--prefix", installationDir, "--no-audit", "--no-fund", "--save-exact", "--loglevel=http"], {
|
|
374
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
375
|
+
windowsHide: true,
|
|
376
|
+
});
|
|
377
|
+
let stderr = "";
|
|
378
|
+
let requests = 0;
|
|
379
|
+
const progressReporter = createCoalescedAsyncTask(async () => {
|
|
380
|
+
const bytes = await directorySize(installationDir);
|
|
381
|
+
const sizePercent = spec.expectedBytes > 0 ? Math.min(92, (bytes / spec.expectedBytes) * 92) : 0;
|
|
382
|
+
const requestPercent = Math.min(85, requests * 2);
|
|
383
|
+
const percent = Math.max(3, sizePercent, requestPercent);
|
|
384
|
+
await onProgress(percent, `已下载/写入 ${(bytes / 1048576).toFixed(1)} MB`);
|
|
385
|
+
});
|
|
386
|
+
const report = () => progressReporter.schedule();
|
|
387
|
+
child.stdout.on("data", (chunk) => {
|
|
388
|
+
requests += (chunk.toString().match(/http fetch GET|GET \d{3}/g) ?? []).length;
|
|
389
|
+
report();
|
|
390
|
+
});
|
|
391
|
+
child.stderr.on("data", (chunk) => {
|
|
392
|
+
const text = chunk.toString();
|
|
393
|
+
stderr = (stderr + text).slice(-4000);
|
|
394
|
+
requests += (text.match(/http fetch GET|GET \d{3}/g) ?? []).length;
|
|
395
|
+
report();
|
|
396
|
+
});
|
|
397
|
+
const timer = setInterval(report, 1000);
|
|
398
|
+
timer.unref?.();
|
|
399
|
+
child.once("error", (error) => {
|
|
400
|
+
clearInterval(timer);
|
|
401
|
+
reject(error);
|
|
402
|
+
});
|
|
403
|
+
child.once("close", (code) => {
|
|
404
|
+
clearInterval(timer);
|
|
405
|
+
void progressReporter.flush().then(() => {
|
|
406
|
+
if (code === 0)
|
|
407
|
+
resolvePromise();
|
|
408
|
+
else
|
|
409
|
+
reject(new Error(`npm install 失败(退出码 ${String(code)}):${stderr.trim().split(/\r?\n/).slice(-6).join(" | ").slice(0, 1000)}`));
|
|
410
|
+
}).catch(reject);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
async function directorySize(root) {
|
|
415
|
+
let total = 0;
|
|
416
|
+
const pending = [root];
|
|
417
|
+
while (pending.length) {
|
|
418
|
+
const current = pending.pop();
|
|
419
|
+
let entries;
|
|
420
|
+
try {
|
|
421
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
for (const entry of entries) {
|
|
427
|
+
const path = join(current, entry.name);
|
|
428
|
+
if (entry.isDirectory())
|
|
429
|
+
pending.push(path);
|
|
430
|
+
else if (entry.isFile()) {
|
|
431
|
+
try {
|
|
432
|
+
total += (await stat(path)).size;
|
|
433
|
+
}
|
|
434
|
+
catch { /* file changed during scan */ }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return total;
|
|
439
|
+
}
|
|
440
|
+
function compareVersions(left, right) {
|
|
441
|
+
const a = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
442
|
+
const b = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
443
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
444
|
+
if ((a[index] ?? 0) !== (b[index] ?? 0))
|
|
445
|
+
return (a[index] ?? 0) > (b[index] ?? 0) ? 1 : -1;
|
|
446
|
+
}
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
function cloneJob(job) {
|
|
450
|
+
return JSON.parse(JSON.stringify(job));
|
|
451
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { EngineManager } from "./engine-manager.js";
|
|
13
|
+
export const CLAUDE_ENGINE_VERSION = "0.2.133";
|
|
14
|
+
export const DSH_ENGINE_VERSION = "0.1.0-rc.6";
|
|
15
|
+
const DSH_PACKAGES = {
|
|
16
|
+
"@deepseek-ai/dsh": DSH_ENGINE_VERSION,
|
|
17
|
+
"@deepseek-ai/dsh-sdk-client": DSH_ENGINE_VERSION,
|
|
18
|
+
"@deepseek-ai/dsh-sdk-jsonrpc-demo": DSH_ENGINE_VERSION,
|
|
19
|
+
"@deepseek-ai/dsh-sdk-jsonrpc-server": DSH_ENGINE_VERSION,
|
|
20
|
+
"@deepseek-ai/dsh-agent-spine-demo": DSH_ENGINE_VERSION,
|
|
21
|
+
"@deepseek-ai/dsh-llm-deepseek": DSH_ENGINE_VERSION,
|
|
22
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": DSH_ENGINE_VERSION,
|
|
23
|
+
"@deepseek-ai/dsh-session-checkpoint-policy": DSH_ENGINE_VERSION,
|
|
24
|
+
"@deepseek-ai/dsh-subprocess-local": DSH_ENGINE_VERSION,
|
|
25
|
+
"@deepseek-ai/dsh-bash-local": DSH_ENGINE_VERSION,
|
|
26
|
+
"@deepseek-ai/dsh-fs-local": DSH_ENGINE_VERSION,
|
|
27
|
+
"@deepseek-ai/dsh-subagent": DSH_ENGINE_VERSION,
|
|
28
|
+
"@deepseek-ai/dsh-subagent-spawn-in-process": DSH_ENGINE_VERSION,
|
|
29
|
+
"@deepseek-ai/dsh-tool-subagent": DSH_ENGINE_VERSION,
|
|
30
|
+
"@deepseek-ai/dsh-tool-todo": DSH_ENGINE_VERSION,
|
|
31
|
+
"@deepseek-ai/dsh-fs-observation-policy": DSH_ENGINE_VERSION,
|
|
32
|
+
"@deepseek-ai/dsh-tool-fs": DSH_ENGINE_VERSION,
|
|
33
|
+
"@deepseek-ai/dsh-token-meter": DSH_ENGINE_VERSION,
|
|
34
|
+
"@deepseek-ai/dsh-compaction-basic": DSH_ENGINE_VERSION,
|
|
35
|
+
};
|
|
36
|
+
export const DSH_RUNTIME_CONFIG = `# Generated and owned by ChatCCC. Secrets are supplied through the child environment.
|
|
37
|
+
- id: sdk-jsonrpc-server
|
|
38
|
+
name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
|
|
39
|
+
config:
|
|
40
|
+
maxTokensAsSuccess: true
|
|
41
|
+
- id: agent-core
|
|
42
|
+
name: '@deepseek-ai/dsh-agent-spine-demo'
|
|
43
|
+
config:
|
|
44
|
+
persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a coding agent.'
|
|
45
|
+
workspaceContext: false
|
|
46
|
+
skills:
|
|
47
|
+
enabled: false
|
|
48
|
+
toolBash:
|
|
49
|
+
enableRunInBackground: false
|
|
50
|
+
toolJobs: false
|
|
51
|
+
- id: llm-deepseek
|
|
52
|
+
name: '@deepseek-ai/dsh-llm-deepseek'
|
|
53
|
+
config:
|
|
54
|
+
thinking: enabled
|
|
55
|
+
reasoningEffort: max
|
|
56
|
+
- id: sessions
|
|
57
|
+
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
|
58
|
+
config:
|
|
59
|
+
root: !!js process.env.DSH_SESSION_ROOT
|
|
60
|
+
compression: zstd
|
|
61
|
+
- id: session-checkpoints
|
|
62
|
+
name: '@deepseek-ai/dsh-session-checkpoint-policy'
|
|
63
|
+
- id: subprocess
|
|
64
|
+
name: '@deepseek-ai/dsh-subprocess-local'
|
|
65
|
+
- id: bash
|
|
66
|
+
name: '@deepseek-ai/dsh-bash-local'
|
|
67
|
+
config:
|
|
68
|
+
cwd: !!js process.env.DSH_CWD
|
|
69
|
+
timeoutMs: 60000
|
|
70
|
+
- id: fs-local
|
|
71
|
+
name: '@deepseek-ai/dsh-fs-local'
|
|
72
|
+
config:
|
|
73
|
+
cwd: !!js process.env.DSH_CWD
|
|
74
|
+
- id: subagent
|
|
75
|
+
name: '@deepseek-ai/dsh-subagent'
|
|
76
|
+
- id: subagent-spawn-in-process
|
|
77
|
+
name: '@deepseek-ai/dsh-subagent-spawn-in-process'
|
|
78
|
+
config:
|
|
79
|
+
providerName: spawn
|
|
80
|
+
- id: tool-subagent
|
|
81
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
82
|
+
config:
|
|
83
|
+
provider: spawn
|
|
84
|
+
toolName: subagent
|
|
85
|
+
enableRunInBackground: false
|
|
86
|
+
- id: tool-todo
|
|
87
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
88
|
+
config:
|
|
89
|
+
allowParallelInProgress: true
|
|
90
|
+
- id: fs-observation-policy
|
|
91
|
+
name: '@deepseek-ai/dsh-fs-observation-policy'
|
|
92
|
+
- id: tool-fs
|
|
93
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
94
|
+
- id: token-meter
|
|
95
|
+
name: '@deepseek-ai/dsh-token-meter'
|
|
96
|
+
- id: compaction-basic
|
|
97
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
98
|
+
config:
|
|
99
|
+
thresholdRatio: 0.8
|
|
100
|
+
retainRatio: 0.16
|
|
101
|
+
maxTokens: 8192
|
|
102
|
+
compactionRetries: 1
|
|
103
|
+
`;
|
|
104
|
+
export const ENGINE_SPECS = [
|
|
105
|
+
{
|
|
106
|
+
id: "claude",
|
|
107
|
+
label: "Claude Code",
|
|
108
|
+
version: CLAUDE_ENGINE_VERSION,
|
|
109
|
+
packages: { "@anthropic-ai/claude-agent-sdk": CLAUDE_ENGINE_VERSION },
|
|
110
|
+
entryRelativePath: join("node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs"),
|
|
111
|
+
expectedBytes: 240 * 1024 * 1024,
|
|
112
|
+
minimumNodeVersion: "20.0.0",
|
|
113
|
+
verifyRuntime: async (dir) => {
|
|
114
|
+
await import(__rewriteRelativeImportExtension(pathToFileURL(join(dir, "node_modules", "@anthropic-ai", "claude-agent-sdk", "sdk.mjs")).href));
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "dsh",
|
|
119
|
+
label: "DeepSeek Harness",
|
|
120
|
+
version: DSH_ENGINE_VERSION,
|
|
121
|
+
packages: DSH_PACKAGES,
|
|
122
|
+
entryRelativePath: join("node_modules", "@deepseek-ai", "dsh-sdk-client", "lib", "index.js"),
|
|
123
|
+
expectedBytes: 265 * 1024 * 1024,
|
|
124
|
+
minimumNodeVersion: "22.19.0",
|
|
125
|
+
prepareInstallation: async (dir) => {
|
|
126
|
+
await writeFile(join(dir, "dsh-runtime.cordis.yml"), DSH_RUNTIME_CONFIG, "utf8");
|
|
127
|
+
await mkdir(join(dir, "sessions"), { recursive: true });
|
|
128
|
+
},
|
|
129
|
+
verifyRuntime: async (dir) => {
|
|
130
|
+
const modulePath = join(dir, "node_modules", "@deepseek-ai", "dsh-sdk-client", "lib", "index.js");
|
|
131
|
+
const sdk = await import(__rewriteRelativeImportExtension(pathToFileURL(modulePath).href));
|
|
132
|
+
const runtime = new sdk.DeepSeekHarness({
|
|
133
|
+
launch: {
|
|
134
|
+
command: process.execPath,
|
|
135
|
+
args: [join(dir, "node_modules", "@deepseek-ai", "dsh-sdk-jsonrpc-demo", "lib", "bin.js"), join(dir, "dsh-runtime.cordis.yml")],
|
|
136
|
+
cwd: dir,
|
|
137
|
+
env: {
|
|
138
|
+
...process.env,
|
|
139
|
+
DSH_CWD: dir,
|
|
140
|
+
DSH_SESSION_ROOT: join(dir, "sessions"),
|
|
141
|
+
},
|
|
142
|
+
requestTimeoutMs: 30_000,
|
|
143
|
+
},
|
|
144
|
+
cwd: dir,
|
|
145
|
+
provider: "deepseek-official",
|
|
146
|
+
model: "deepseek-v4-flash",
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
await runtime.start();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await runtime.close();
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
export const engineManager = new EngineManager({ specs: ENGINE_SPECS });
|
package/dist/src/feishu-api.js
CHANGED
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { extname, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import sharp from "sharp";
|
|
6
|
-
import { APP_ID, APP_SECRET, BASE_URL, CHAT_LOGS_DIR, PROJECT_ROOT, USER_DATA_DIR, CLAUDE_SESSION_PREFIX, CURSOR_SESSION_PREFIX, CODEX_SESSION_PREFIX, CCC_SESSION_PREFIX, ts, resolveDefaultAgentTool, toolDisplayName, config, } from "./config.js";
|
|
6
|
+
import { APP_ID, APP_SECRET, BASE_URL, CHAT_LOGS_DIR, PROJECT_ROOT, USER_DATA_DIR, CLAUDE_SESSION_PREFIX, CURSOR_SESSION_PREFIX, CODEX_SESSION_PREFIX, CCC_SESSION_PREFIX, DSH_SESSION_PREFIX, ts, resolveDefaultAgentTool, toolDisplayName, config, } from "./config.js";
|
|
7
7
|
import { getCursorUsageSummary } from "./cursor-usage.js";
|
|
8
8
|
import { applyPrivacy } from "./privacy.js";
|
|
9
9
|
import { buildHelpCard } from "./cards.js";
|
|
@@ -203,6 +203,7 @@ export function extractSessionInfo(description) {
|
|
|
203
203
|
{ prefix: CURSOR_SESSION_PREFIX, tool: "cursor" },
|
|
204
204
|
{ prefix: CODEX_SESSION_PREFIX, tool: "codex" },
|
|
205
205
|
{ prefix: CCC_SESSION_PREFIX, tool: "ccc" },
|
|
206
|
+
{ prefix: DSH_SESSION_PREFIX, tool: "dsh" },
|
|
206
207
|
];
|
|
207
208
|
for (const { prefix, tool } of PREFIXES) {
|
|
208
209
|
const idx = description.indexOf(prefix);
|