chatccc 0.2.259 → 0.2.260
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 +408 -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,408 @@
|
|
|
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
|
+
const start = () => {
|
|
21
|
+
if (active)
|
|
22
|
+
return;
|
|
23
|
+
active = (async () => {
|
|
24
|
+
while (requested) {
|
|
25
|
+
requested = false;
|
|
26
|
+
await task();
|
|
27
|
+
}
|
|
28
|
+
})().finally(() => {
|
|
29
|
+
active = null;
|
|
30
|
+
if (requested)
|
|
31
|
+
start();
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
schedule() {
|
|
36
|
+
requested = true;
|
|
37
|
+
start();
|
|
38
|
+
},
|
|
39
|
+
async flush() {
|
|
40
|
+
requested = true;
|
|
41
|
+
start();
|
|
42
|
+
while (active)
|
|
43
|
+
await active;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function resolveNpmInvocation() {
|
|
48
|
+
const candidates = [
|
|
49
|
+
process.env.npm_execpath,
|
|
50
|
+
join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
|
51
|
+
...(process.env.PATH ?? "").split(delimiter).filter(Boolean).map((pathDir) => join(pathDir, "node_modules", "npm", "bin", "npm-cli.js")),
|
|
52
|
+
];
|
|
53
|
+
const npmCliPath = candidates.find((candidate) => Boolean(candidate && existsSync(candidate)));
|
|
54
|
+
if (!npmCliPath) {
|
|
55
|
+
throw new Error("找不到 npm-cli.js;请先安装 npm,并确保 npm 与当前 Node.js 位于同一环境。");
|
|
56
|
+
}
|
|
57
|
+
return { command: process.execPath, argsPrefix: [npmCliPath] };
|
|
58
|
+
}
|
|
59
|
+
export class EngineManager {
|
|
60
|
+
rootDir;
|
|
61
|
+
specs = new Map();
|
|
62
|
+
installPackages;
|
|
63
|
+
verifyRuntime;
|
|
64
|
+
nodeVersion;
|
|
65
|
+
activeInstalls = new Map();
|
|
66
|
+
constructor(options) {
|
|
67
|
+
this.rootDir = options.rootDir ?? DEFAULT_ENGINE_ROOT;
|
|
68
|
+
for (const spec of options.specs)
|
|
69
|
+
this.specs.set(spec.id, spec);
|
|
70
|
+
this.installPackages = options.installPackages ?? defaultInstallPackages;
|
|
71
|
+
this.verifyRuntime = options.verifyRuntime;
|
|
72
|
+
this.nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
73
|
+
}
|
|
74
|
+
listSpecs() {
|
|
75
|
+
return [...this.specs.values()];
|
|
76
|
+
}
|
|
77
|
+
getSpec(engineId) {
|
|
78
|
+
const spec = this.specs.get(engineId);
|
|
79
|
+
if (!spec)
|
|
80
|
+
throw new Error(`Unknown engine: ${engineId}`);
|
|
81
|
+
return spec;
|
|
82
|
+
}
|
|
83
|
+
async getStatus(engineId) {
|
|
84
|
+
const spec = this.getSpec(engineId);
|
|
85
|
+
const pointer = await this.readPointer(spec);
|
|
86
|
+
const entryPath = pointer ? join(this.engineDir(spec), pointer.directory, spec.entryRelativePath) : null;
|
|
87
|
+
const installed = Boolean(entryPath && existsSync(entryPath));
|
|
88
|
+
return {
|
|
89
|
+
id: spec.id,
|
|
90
|
+
label: spec.label,
|
|
91
|
+
installed,
|
|
92
|
+
version: installed ? pointer?.version ?? null : null,
|
|
93
|
+
targetVersion: spec.version,
|
|
94
|
+
entryPath: installed ? entryPath : null,
|
|
95
|
+
running: this.activeInstalls.has(engineId),
|
|
96
|
+
job: await this.readJob(spec),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async getEntryPath(engineId) {
|
|
100
|
+
const status = await this.getStatus(engineId);
|
|
101
|
+
if (!status.installed || !status.entryPath) {
|
|
102
|
+
throw new Error(`${status.label} 尚未安装,请先在设置页安装引擎。`);
|
|
103
|
+
}
|
|
104
|
+
return status.entryPath;
|
|
105
|
+
}
|
|
106
|
+
async startInstall(engineId) {
|
|
107
|
+
const running = this.activeInstalls.get(engineId);
|
|
108
|
+
if (running)
|
|
109
|
+
return this.readJob(this.getSpec(engineId)).then((job) => job ?? this.newJob(this.getSpec(engineId)));
|
|
110
|
+
const spec = this.getSpec(engineId);
|
|
111
|
+
const job = this.newJob(spec);
|
|
112
|
+
await this.persistJob(spec, job);
|
|
113
|
+
const task = this.runInstall(spec, job).finally(() => this.activeInstalls.delete(engineId));
|
|
114
|
+
this.activeInstalls.set(engineId, task);
|
|
115
|
+
return cloneJob(job);
|
|
116
|
+
}
|
|
117
|
+
async install(engineId) {
|
|
118
|
+
await this.startInstall(engineId);
|
|
119
|
+
return this.waitForInstall(engineId);
|
|
120
|
+
}
|
|
121
|
+
async waitForInstall(engineId) {
|
|
122
|
+
const active = this.activeInstalls.get(engineId);
|
|
123
|
+
if (active)
|
|
124
|
+
return active;
|
|
125
|
+
const job = await this.readJob(this.getSpec(engineId));
|
|
126
|
+
if (!job)
|
|
127
|
+
throw new Error(`No install job for engine: ${engineId}`);
|
|
128
|
+
return job;
|
|
129
|
+
}
|
|
130
|
+
newJob(spec) {
|
|
131
|
+
const now = new Date().toISOString();
|
|
132
|
+
return {
|
|
133
|
+
schemaVersion: 1,
|
|
134
|
+
jobId: randomUUID(),
|
|
135
|
+
engineId: spec.id,
|
|
136
|
+
targetVersion: spec.version,
|
|
137
|
+
state: "running",
|
|
138
|
+
percent: 0,
|
|
139
|
+
startedAt: now,
|
|
140
|
+
updatedAt: now,
|
|
141
|
+
steps: STEP_DEFINITIONS.map(([id, label]) => ({ id, label, state: "pending", percent: 0, message: "等待中" })),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async runInstall(spec, job) {
|
|
145
|
+
const stagingDir = join(this.rootDir, ".staging", `${spec.id}-${job.jobId}`);
|
|
146
|
+
let publishedDir = null;
|
|
147
|
+
try {
|
|
148
|
+
await this.runStep(spec, job, "preflight", async (update) => {
|
|
149
|
+
await update(20, `Node.js ${this.nodeVersion}`);
|
|
150
|
+
if (compareVersions(this.nodeVersion, spec.minimumNodeVersion) < 0) {
|
|
151
|
+
throw new Error(`${spec.label} 要求 Node.js >= ${spec.minimumNodeVersion},当前为 ${this.nodeVersion}`);
|
|
152
|
+
}
|
|
153
|
+
await mkdir(this.rootDir, { recursive: true });
|
|
154
|
+
await update(100, "运行环境可用");
|
|
155
|
+
});
|
|
156
|
+
await this.runStep(spec, job, "prepare", async (update) => {
|
|
157
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
158
|
+
await mkdir(stagingDir, { recursive: true });
|
|
159
|
+
await writeFile(join(stagingDir, "package.json"), JSON.stringify({
|
|
160
|
+
name: `chatccc-engine-${spec.id}`,
|
|
161
|
+
private: true,
|
|
162
|
+
type: "module",
|
|
163
|
+
dependencies: spec.packages,
|
|
164
|
+
}, null, 2) + "\n", "utf8");
|
|
165
|
+
await spec.prepareInstallation?.(stagingDir);
|
|
166
|
+
await update(100, "临时安装目录已准备");
|
|
167
|
+
});
|
|
168
|
+
await this.runStep(spec, job, "download_install", async (update) => {
|
|
169
|
+
await this.installPackages(stagingDir, spec, update);
|
|
170
|
+
await update(100, "依赖安装完成");
|
|
171
|
+
});
|
|
172
|
+
await this.runStep(spec, job, "verify_packages", async (update) => {
|
|
173
|
+
const entries = Object.entries(spec.packages);
|
|
174
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
175
|
+
const [packageName, expectedVersion] = entries[index];
|
|
176
|
+
const packageJson = join(stagingDir, "node_modules", ...packageName.split("/"), "package.json");
|
|
177
|
+
const parsed = JSON.parse(await readFile(packageJson, "utf8"));
|
|
178
|
+
if (parsed.version !== expectedVersion) {
|
|
179
|
+
throw new Error(`${packageName} 版本校验失败:期望 ${expectedVersion},实际 ${String(parsed.version)}`);
|
|
180
|
+
}
|
|
181
|
+
await update(Math.round(((index + 1) / entries.length) * 90), `已校验 ${packageName}`);
|
|
182
|
+
}
|
|
183
|
+
const entry = join(stagingDir, spec.entryRelativePath);
|
|
184
|
+
if (!existsSync(entry))
|
|
185
|
+
throw new Error(`引擎入口不存在:${entry}`);
|
|
186
|
+
await update(100, "文件和版本校验通过");
|
|
187
|
+
});
|
|
188
|
+
await this.runStep(spec, job, "runtime_handshake", async (update) => {
|
|
189
|
+
await update(10, "正在启动 Runtime");
|
|
190
|
+
if (this.verifyRuntime)
|
|
191
|
+
await this.verifyRuntime(stagingDir, spec);
|
|
192
|
+
else
|
|
193
|
+
await spec.verifyRuntime?.(stagingDir);
|
|
194
|
+
await update(100, "Runtime 握手成功");
|
|
195
|
+
});
|
|
196
|
+
await this.runStep(spec, job, "activate", async (update) => {
|
|
197
|
+
const directoryName = `${spec.version}-${job.jobId}`;
|
|
198
|
+
const versionsDir = join(this.engineDir(spec), "versions");
|
|
199
|
+
publishedDir = join(versionsDir, directoryName);
|
|
200
|
+
await mkdir(versionsDir, { recursive: true });
|
|
201
|
+
await rename(stagingDir, publishedDir);
|
|
202
|
+
const pointer = {
|
|
203
|
+
schemaVersion: 1,
|
|
204
|
+
version: spec.version,
|
|
205
|
+
directory: relative(this.engineDir(spec), publishedDir).replaceAll("\\", "/"),
|
|
206
|
+
activatedAt: new Date().toISOString(),
|
|
207
|
+
};
|
|
208
|
+
const pointerPath = join(this.engineDir(spec), "current.json");
|
|
209
|
+
const temporaryPointer = `${pointerPath}.${job.jobId}.tmp`;
|
|
210
|
+
await writeFile(temporaryPointer, JSON.stringify(pointer, null, 2) + "\n", "utf8");
|
|
211
|
+
await rename(temporaryPointer, pointerPath);
|
|
212
|
+
await update(100, `已切换到 v${spec.version}`);
|
|
213
|
+
});
|
|
214
|
+
await this.runStep(spec, job, "cleanup", async (update) => {
|
|
215
|
+
const versionsDir = join(this.engineDir(spec), "versions");
|
|
216
|
+
const keep = publishedDir ? resolve(publishedDir) : "";
|
|
217
|
+
const entries = await readdir(versionsDir, { withFileTypes: true });
|
|
218
|
+
const old = entries.filter((entry) => entry.isDirectory() && resolve(versionsDir, entry.name) !== keep);
|
|
219
|
+
for (let index = 0; index < old.length; index += 1) {
|
|
220
|
+
await rm(join(versionsDir, old[index].name), { recursive: true, force: true });
|
|
221
|
+
await update(Math.round(((index + 1) / Math.max(old.length, 1)) * 100), `已清理 ${old[index].name}`);
|
|
222
|
+
}
|
|
223
|
+
await update(100, old.length ? "旧版本已清理" : "无需清理旧版本");
|
|
224
|
+
});
|
|
225
|
+
job.state = "succeeded";
|
|
226
|
+
job.percent = 100;
|
|
227
|
+
job.updatedAt = new Date().toISOString();
|
|
228
|
+
await this.persistJob(spec, job);
|
|
229
|
+
return cloneJob(job);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
233
|
+
job.state = "failed";
|
|
234
|
+
job.error = message.slice(0, 1000);
|
|
235
|
+
job.updatedAt = new Date().toISOString();
|
|
236
|
+
await this.persistJob(spec, job);
|
|
237
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => { });
|
|
238
|
+
return cloneJob(job);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async runStep(spec, job, stepId, operation) {
|
|
242
|
+
const step = job.steps.find((candidate) => candidate.id === stepId);
|
|
243
|
+
step.state = "running";
|
|
244
|
+
step.message = "进行中";
|
|
245
|
+
await this.recalculateAndPersist(spec, job);
|
|
246
|
+
const update = async (percent, message) => {
|
|
247
|
+
step.percent = Math.max(0, Math.min(100, Math.round(percent)));
|
|
248
|
+
step.message = message;
|
|
249
|
+
await this.recalculateAndPersist(spec, job);
|
|
250
|
+
};
|
|
251
|
+
try {
|
|
252
|
+
await operation(update);
|
|
253
|
+
step.state = "completed";
|
|
254
|
+
step.percent = 100;
|
|
255
|
+
await this.recalculateAndPersist(spec, job);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
step.state = "failed";
|
|
259
|
+
step.error = error instanceof Error ? error.message : String(error);
|
|
260
|
+
step.message = "失败";
|
|
261
|
+
await this.recalculateAndPersist(spec, job);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async recalculateAndPersist(spec, job) {
|
|
266
|
+
job.percent = Math.round(job.steps.reduce((sum, step) => sum + step.percent, 0) / job.steps.length);
|
|
267
|
+
job.updatedAt = new Date().toISOString();
|
|
268
|
+
await this.persistJob(spec, job);
|
|
269
|
+
}
|
|
270
|
+
engineDir(spec) {
|
|
271
|
+
return join(this.rootDir, spec.id);
|
|
272
|
+
}
|
|
273
|
+
jobPath(spec) {
|
|
274
|
+
return join(this.rootDir, "engine-jobs", `${spec.id}.json`);
|
|
275
|
+
}
|
|
276
|
+
async persistJob(spec, job) {
|
|
277
|
+
const path = this.jobPath(spec);
|
|
278
|
+
await mkdir(dirname(path), { recursive: true });
|
|
279
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
280
|
+
await writeFile(temporary, JSON.stringify(job, null, 2) + "\n", "utf8");
|
|
281
|
+
await rename(temporary, path);
|
|
282
|
+
}
|
|
283
|
+
async readJob(spec) {
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(await readFile(this.jobPath(spec), "utf8"));
|
|
286
|
+
if (parsed.state === "running" && !this.activeInstalls.has(spec.id)) {
|
|
287
|
+
parsed.state = "failed";
|
|
288
|
+
parsed.error = "上一次安装任务因进程退出而中断,请重试。";
|
|
289
|
+
const running = parsed.steps.find((step) => step.state === "running");
|
|
290
|
+
if (running) {
|
|
291
|
+
running.state = "failed";
|
|
292
|
+
running.message = "安装任务已中断";
|
|
293
|
+
running.error = parsed.error;
|
|
294
|
+
}
|
|
295
|
+
parsed.updatedAt = new Date().toISOString();
|
|
296
|
+
await this.persistJob(spec, parsed);
|
|
297
|
+
}
|
|
298
|
+
return parsed;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async readPointer(spec) {
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(await readFile(join(this.engineDir(spec), "current.json"), "utf8"));
|
|
307
|
+
if (typeof parsed.version !== "string" || typeof parsed.directory !== "string")
|
|
308
|
+
return null;
|
|
309
|
+
const resolved = resolve(this.engineDir(spec), parsed.directory);
|
|
310
|
+
const versionsDir = resolve(this.engineDir(spec), "versions");
|
|
311
|
+
if (resolved !== versionsDir && !resolved.startsWith(`${versionsDir}\\`) && !resolved.startsWith(`${versionsDir}/`))
|
|
312
|
+
return null;
|
|
313
|
+
return { version: parsed.version, directory: parsed.directory };
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function defaultInstallPackages(installationDir, spec, onProgress) {
|
|
321
|
+
await new Promise((resolvePromise, reject) => {
|
|
322
|
+
let npmInvocation;
|
|
323
|
+
try {
|
|
324
|
+
npmInvocation = resolveNpmInvocation();
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
reject(error);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const child = spawn(npmInvocation.command, [...npmInvocation.argsPrefix, "install", "--prefix", installationDir, "--no-audit", "--no-fund", "--save-exact", "--loglevel=http"], {
|
|
331
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
332
|
+
windowsHide: true,
|
|
333
|
+
});
|
|
334
|
+
let stderr = "";
|
|
335
|
+
let requests = 0;
|
|
336
|
+
const progressReporter = createCoalescedAsyncTask(async () => {
|
|
337
|
+
const bytes = await directorySize(installationDir);
|
|
338
|
+
const sizePercent = spec.expectedBytes > 0 ? Math.min(92, (bytes / spec.expectedBytes) * 92) : 0;
|
|
339
|
+
const requestPercent = Math.min(85, requests * 2);
|
|
340
|
+
const percent = Math.max(3, sizePercent, requestPercent);
|
|
341
|
+
await onProgress(percent, `已下载/写入 ${(bytes / 1048576).toFixed(1)} MB`);
|
|
342
|
+
});
|
|
343
|
+
const report = () => progressReporter.schedule();
|
|
344
|
+
child.stdout.on("data", (chunk) => {
|
|
345
|
+
requests += (chunk.toString().match(/http fetch GET|GET \d{3}/g) ?? []).length;
|
|
346
|
+
report();
|
|
347
|
+
});
|
|
348
|
+
child.stderr.on("data", (chunk) => {
|
|
349
|
+
const text = chunk.toString();
|
|
350
|
+
stderr = (stderr + text).slice(-4000);
|
|
351
|
+
requests += (text.match(/http fetch GET|GET \d{3}/g) ?? []).length;
|
|
352
|
+
report();
|
|
353
|
+
});
|
|
354
|
+
const timer = setInterval(report, 1000);
|
|
355
|
+
timer.unref?.();
|
|
356
|
+
child.once("error", (error) => {
|
|
357
|
+
clearInterval(timer);
|
|
358
|
+
reject(error);
|
|
359
|
+
});
|
|
360
|
+
child.once("close", (code) => {
|
|
361
|
+
clearInterval(timer);
|
|
362
|
+
void progressReporter.flush().catch(() => { }).finally(() => {
|
|
363
|
+
if (code === 0)
|
|
364
|
+
resolvePromise();
|
|
365
|
+
else
|
|
366
|
+
reject(new Error(`npm install 失败(退出码 ${String(code)}):${stderr.trim().split(/\r?\n/).slice(-6).join(" | ").slice(0, 1000)}`));
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async function directorySize(root) {
|
|
372
|
+
let total = 0;
|
|
373
|
+
const pending = [root];
|
|
374
|
+
while (pending.length) {
|
|
375
|
+
const current = pending.pop();
|
|
376
|
+
let entries;
|
|
377
|
+
try {
|
|
378
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
for (const entry of entries) {
|
|
384
|
+
const path = join(current, entry.name);
|
|
385
|
+
if (entry.isDirectory())
|
|
386
|
+
pending.push(path);
|
|
387
|
+
else if (entry.isFile()) {
|
|
388
|
+
try {
|
|
389
|
+
total += (await stat(path)).size;
|
|
390
|
+
}
|
|
391
|
+
catch { /* file changed during scan */ }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return total;
|
|
396
|
+
}
|
|
397
|
+
function compareVersions(left, right) {
|
|
398
|
+
const a = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
399
|
+
const b = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
400
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
401
|
+
if ((a[index] ?? 0) !== (b[index] ?? 0))
|
|
402
|
+
return (a[index] ?? 0) > (b[index] ?? 0) ? 1 : -1;
|
|
403
|
+
}
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
function cloneJob(job) {
|
|
407
|
+
return JSON.parse(JSON.stringify(job));
|
|
408
|
+
}
|
|
@@ -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);
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -298,6 +298,8 @@ async function resolveUsageTarget(chatId) {
|
|
|
298
298
|
return { tool: "cursor", sessionId: record?.sessionId };
|
|
299
299
|
if (tool === "ccc")
|
|
300
300
|
return { tool: "ccc", sessionId: record?.sessionId };
|
|
301
|
+
if (tool === "dsh")
|
|
302
|
+
return { tool: "dsh", sessionId: record?.sessionId };
|
|
301
303
|
return { tool: "codex", sessionId: record?.sessionId };
|
|
302
304
|
}
|
|
303
305
|
catch {
|
|
@@ -345,25 +347,28 @@ function refreshUsageAvatar(platform, chatId, tool, status, usageHints, sessionI
|
|
|
345
347
|
});
|
|
346
348
|
}
|
|
347
349
|
async function sendUsageSummary(platform, chatId, tool, avatarStatus = "idle", sessionId) {
|
|
348
|
-
if (tool === "ccc") {
|
|
349
|
-
const
|
|
350
|
+
if (tool === "ccc" || tool === "dsh") {
|
|
351
|
+
const isDsh = tool === "dsh";
|
|
352
|
+
const baseURL = isDsh ? config.dsh.baseUrl : config.ccc.DEEPSEEK_BASE_URL;
|
|
353
|
+
const apiKey = isDsh ? config.dsh.apiKey : config.ccc.DEEPSEEK_API_KEY;
|
|
354
|
+
const toolLabel = isDsh ? "DeepSeek Harness" : "CCC";
|
|
350
355
|
if (!isOfficialDeepSeek(baseURL)) {
|
|
351
|
-
const msg =
|
|
356
|
+
const msg = `${toolLabel} 用量查询仅支持官方 DeepSeek API (api.deepseek.com),当前使用的非官方接口不支持余额查询。`;
|
|
352
357
|
if (platform.kind === "wechat") {
|
|
353
358
|
await platform.sendText(chatId, msg).catch(() => { });
|
|
354
359
|
}
|
|
355
360
|
else {
|
|
356
|
-
await platform.sendCard(chatId,
|
|
361
|
+
await platform.sendCard(chatId, `${toolLabel} Usage`, msg, "blue");
|
|
357
362
|
}
|
|
358
363
|
return;
|
|
359
364
|
}
|
|
360
|
-
const balance = await fetchDeepSeekBalance(
|
|
365
|
+
const balance = await fetchDeepSeekBalance(apiKey, baseURL);
|
|
361
366
|
const content = formatDeepSeekBalance(balance);
|
|
362
367
|
if (platform.kind === "wechat") {
|
|
363
368
|
await platform.sendText(chatId, content).catch(() => { });
|
|
364
369
|
}
|
|
365
370
|
else {
|
|
366
|
-
await platform.sendCard(chatId,
|
|
371
|
+
await platform.sendCard(chatId, `${toolLabel} Usage`, content, "blue");
|
|
367
372
|
}
|
|
368
373
|
return;
|
|
369
374
|
}
|
|
@@ -396,7 +401,7 @@ async function sendUsageSummary(platform, chatId, tool, avatarStatus = "idle", s
|
|
|
396
401
|
refreshUsageAvatar(platform, chatId, tool, avatarStatus, { codexUsage: usage }, sessionId);
|
|
397
402
|
}
|
|
398
403
|
async function sendUsageError(platform, chatId, tool, err) {
|
|
399
|
-
const toolLabel = tool === "cursor" ? "Cursor" : tool === "ccc" ? "CCC" : "Codex";
|
|
404
|
+
const toolLabel = tool === "cursor" ? "Cursor" : tool === "ccc" ? "CCC" : tool === "dsh" ? "DeepSeek Harness" : "Codex";
|
|
400
405
|
const message = `${toolLabel} 用量获取失败:${err.message}`;
|
|
401
406
|
if (platform.kind === "wechat") {
|
|
402
407
|
await platform.sendText(chatId, message).catch(() => { });
|
|
@@ -985,10 +990,10 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
985
990
|
const toolArg = text.slice(5).trim().toLowerCase();
|
|
986
991
|
const tool = toolArg || resolveDefaultAgentTool();
|
|
987
992
|
logTrace(tid, "BRANCH", { cmd: "/new", tool });
|
|
988
|
-
const validTools = ["claude", "cursor", "codex", "ccc"];
|
|
993
|
+
const validTools = ["claude", "cursor", "codex", "ccc", "dsh"];
|
|
989
994
|
if (!validTools.includes(tool)) {
|
|
990
995
|
logTrace(tid, "DONE", { outcome: "new_invalid_tool", tool });
|
|
991
|
-
await platform.sendCard(chatId, "Error", `未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor), codex (Codex), ccc (CCC Agent)。`, "red");
|
|
996
|
+
await platform.sendCard(chatId, "Error", `未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor), codex (Codex), ccc (CCC Agent), dsh (DeepSeek Harness)。`, "red");
|
|
992
997
|
return;
|
|
993
998
|
}
|
|
994
999
|
const toolLabel = toolDisplayName(tool);
|
|
@@ -1525,13 +1530,17 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
1525
1530
|
const index = parseInt(sessionMatch[1], 10) - 1;
|
|
1526
1531
|
logTrace(tid, "BRANCH", { cmd: "/session", index: index + 1 });
|
|
1527
1532
|
const allSessions = await getAllSessionsStatus();
|
|
1528
|
-
const claudeOrdered = allSessions.filter((s) => s.tool
|
|
1533
|
+
const claudeOrdered = allSessions.filter((s) => s.tool === "claude");
|
|
1529
1534
|
const cursorOrdered = allSessions.filter((s) => s.tool === "cursor");
|
|
1530
1535
|
const codexOrdered = allSessions.filter((s) => s.tool === "codex");
|
|
1536
|
+
const cccOrdered = allSessions.filter((s) => s.tool === "ccc");
|
|
1537
|
+
const dshOrdered = allSessions.filter((s) => s.tool === "dsh");
|
|
1531
1538
|
const ordered = [
|
|
1532
1539
|
...claudeOrdered,
|
|
1533
1540
|
...cursorOrdered,
|
|
1534
1541
|
...codexOrdered,
|
|
1542
|
+
...cccOrdered,
|
|
1543
|
+
...dshOrdered,
|
|
1535
1544
|
];
|
|
1536
1545
|
if (ordered.length === 0) {
|
|
1537
1546
|
await platform.sendCard(chatId, "/session", "暂无历史会话。", "yellow");
|
|
@@ -1941,6 +1950,8 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
|
|
|
1941
1950
|
currentModel = config.codex.model;
|
|
1942
1951
|
else if (defaultTool === "ccc")
|
|
1943
1952
|
currentModel = config.ccc.model;
|
|
1953
|
+
else if (defaultTool === "dsh")
|
|
1954
|
+
currentModel = config.dsh.model;
|
|
1944
1955
|
else
|
|
1945
1956
|
currentModel = CLAUDE_MODEL;
|
|
1946
1957
|
if (platform.kind === "wechat") {
|