chatccc 0.2.258 → 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.
@@ -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 });
@@ -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);
@@ -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 baseURL = config.ccc.DEEPSEEK_BASE_URL;
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 = "CCC 用量查询仅支持官方 DeepSeek API (api.deepseek.com),当前使用的非官方接口不支持余额查询。";
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, "CCC Usage", msg, "blue");
361
+ await platform.sendCard(chatId, `${toolLabel} Usage`, msg, "blue");
357
362
  }
358
363
  return;
359
364
  }
360
- const balance = await fetchDeepSeekBalance(config.ccc.DEEPSEEK_API_KEY, baseURL);
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, "CCC Usage", content, "blue");
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 !== "cursor" && s.tool !== "codex");
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") {