chatccc 0.2.263 → 0.2.265
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/dist/src/adapters/dsh-adapter.js +114 -42
- package/package.json +1 -1
|
@@ -20,6 +20,18 @@ export function __setDshSdkModuleForTest(module) {
|
|
|
20
20
|
injectedSdk = module;
|
|
21
21
|
}
|
|
22
22
|
export function createDshAdapter(options = {}) {
|
|
23
|
+
// DSH 引擎的 JSON-RPC `session/prompt` 只支持 create、不支持 resume:已持久化的
|
|
24
|
+
// session 必须在同一个 runtime 进程内复用,否则第二次 prompt 会因 "id collision"
|
|
25
|
+
// 失败(新 runtime 内存里没有该 session,走 create 路径与磁盘 log 冲突)。
|
|
26
|
+
// 因此这里按 sessionId 缓存长驻 runtime,正常完成不关闭,close/abort/崩溃时关闭。
|
|
27
|
+
const runtimes = new Map();
|
|
28
|
+
const disposeRuntime = (sessionId) => {
|
|
29
|
+
const entry = runtimes.get(sessionId);
|
|
30
|
+
if (!entry)
|
|
31
|
+
return;
|
|
32
|
+
runtimes.delete(sessionId);
|
|
33
|
+
void entry.harness.close().catch(() => { });
|
|
34
|
+
};
|
|
23
35
|
return {
|
|
24
36
|
displayName: "DeepSeek Harness",
|
|
25
37
|
sessionDescPrefix: "DSH Session:",
|
|
@@ -36,31 +48,42 @@ export function createDshAdapter(options = {}) {
|
|
|
36
48
|
const sdk = injectedSdk ?? await import(__rewriteRelativeImportExtension(pathToFileURL(entryPath).href));
|
|
37
49
|
const sessionRoot = join(homedir(), ".chatccc", "dsh-sessions");
|
|
38
50
|
await mkdir(sessionRoot, { recursive: true });
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
// 复用长驻 runtime:同一 session 的多次 prompt 必须在同一 runtime 进程内完成
|
|
52
|
+
// (DSH 的 session/prompt 无 resume 能力,见上方 runtimes 注释)。cwd 变化时
|
|
53
|
+
// 重建(旧 runtime 已绑定旧 cwd,无法切换工作目录)。
|
|
54
|
+
let entry = runtimes.get(sessionId);
|
|
55
|
+
if (!entry || entry.cwd !== cwd) {
|
|
56
|
+
if (entry)
|
|
57
|
+
disposeRuntime(sessionId);
|
|
58
|
+
const runtime = new sdk.DeepSeekHarness({
|
|
59
|
+
launch: {
|
|
60
|
+
command: process.execPath,
|
|
61
|
+
args: [
|
|
62
|
+
join(installationDir, "node_modules", "@deepseek-ai", "dsh-sdk-jsonrpc-demo", "lib", "bin.js"),
|
|
63
|
+
join(installationDir, "dsh-runtime.cordis.yml"),
|
|
64
|
+
],
|
|
65
|
+
cwd,
|
|
66
|
+
env: {
|
|
67
|
+
...process.env,
|
|
68
|
+
DSH_CWD: cwd,
|
|
69
|
+
DSH_SESSION_ROOT: sessionRoot,
|
|
70
|
+
...(options.apiKey ? { DEEPSEEK_API_KEY: options.apiKey } : {}),
|
|
71
|
+
...(options.baseUrl ? { DEEPSEEK_BASE_URL: options.baseUrl } : {}),
|
|
72
|
+
// 子代理模型:subModel 留空时跟随主模型(与 CCC 的 ccc.subModel 语义一致)
|
|
73
|
+
DSH_SUBAGENT_PROVIDER: options.provider ?? "deepseek-official",
|
|
74
|
+
DSH_SUBAGENT_MODEL: options.subModel || options.model || "deepseek-v4-flash",
|
|
75
|
+
DSH_SUBAGENT_MAX_TOKENS: String(options.maxTokens ?? 49152),
|
|
76
|
+
},
|
|
57
77
|
},
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
cwd,
|
|
79
|
+
provider: options.provider ?? "deepseek-official",
|
|
80
|
+
model: options.model ?? "deepseek-v4-flash",
|
|
81
|
+
...(options.maxTokens ? { maxTokens: options.maxTokens } : {}),
|
|
82
|
+
});
|
|
83
|
+
entry = { harness: runtime, cwd };
|
|
84
|
+
runtimes.set(sessionId, entry);
|
|
85
|
+
}
|
|
86
|
+
const runtime = entry.harness;
|
|
64
87
|
const queue = new AsyncMessageQueue();
|
|
65
88
|
const rawLogConfig = config.rawStreamLogs.dsh;
|
|
66
89
|
let rawLog = null;
|
|
@@ -78,24 +101,25 @@ export function createDshAdapter(options = {}) {
|
|
|
78
101
|
catch (error) {
|
|
79
102
|
console.error(`[DSH raw stream log] create failed: ${error.message}`);
|
|
80
103
|
}
|
|
81
|
-
let closed = false;
|
|
82
104
|
let completed = false;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
void runtime.close();
|
|
88
|
-
};
|
|
89
|
-
promptOptions?.onSessionCreated?.(closeRuntime);
|
|
90
|
-
const onAbort = () => closeRuntime();
|
|
105
|
+
// abort / stop-stuck 时关闭该 session 的 runtime 以中止进行中的请求;
|
|
106
|
+
// 正常完成不关闭,保留 runtime 供同一 session 的后续 prompt 复用。
|
|
107
|
+
promptOptions?.onSessionCreated?.(() => disposeRuntime(sessionId));
|
|
108
|
+
const onAbort = () => disposeRuntime(sessionId);
|
|
91
109
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
92
110
|
const task = (async () => {
|
|
93
111
|
try {
|
|
94
112
|
await runtime.start();
|
|
113
|
+
// DSH 引擎在 LLM 调用失败时不会让 run() reject:错误被写进 turn/end 事件
|
|
114
|
+
// 的 reason(kind="error")后,session 进入 idle,run() 返回空 finalResponse。
|
|
115
|
+
// 这里把第一个 turn 错误收集起来,run() 结束后重新抛出,交给 session.ts
|
|
116
|
+
// 的统一终态错误分类与脱敏展示,避免飞书里"静默失败"。
|
|
117
|
+
let turnError = null;
|
|
95
118
|
const result = await runtime.run(userText, {
|
|
96
119
|
sessionId,
|
|
97
120
|
onNotification: (notification) => {
|
|
98
121
|
rawLog?.writeLine(JSON.stringify(notification));
|
|
122
|
+
turnError ??= extractTurnError(notification);
|
|
99
123
|
const message = notificationToMessage(notification);
|
|
100
124
|
if (message)
|
|
101
125
|
queue.push(message);
|
|
@@ -104,19 +128,41 @@ export function createDshAdapter(options = {}) {
|
|
|
104
128
|
if (result.finalResponse) {
|
|
105
129
|
rawLog?.writeLine(JSON.stringify({ type: "run.result", result }));
|
|
106
130
|
queue.push({ type: "assistant", blocks: [{ type: "text_final", text: result.finalResponse }], isFinalResponse: true });
|
|
131
|
+
completed = true;
|
|
132
|
+
}
|
|
133
|
+
else if (turnError) {
|
|
134
|
+
queue.fail(turnError);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// 引擎正常结束但零输出(无错误、无回复):给出明确提示,避免用户只看到"失败"。
|
|
138
|
+
queue.push({
|
|
139
|
+
type: "assistant",
|
|
140
|
+
blocks: [{ type: "text_final", text: "DeepSeek Harness 本轮未产生任何回复(引擎未报告错误)。" }],
|
|
141
|
+
isFinalResponse: true,
|
|
142
|
+
});
|
|
143
|
+
completed = true;
|
|
144
|
+
}
|
|
145
|
+
if (completed) {
|
|
146
|
+
knownSessions.set(sessionId, { sessionId, cwd, lastModified: Date.now(), model: options.model ?? "deepseek-v4-flash" });
|
|
147
|
+
queue.end();
|
|
107
148
|
}
|
|
108
|
-
completed = true;
|
|
109
|
-
knownSessions.set(sessionId, { sessionId, cwd, lastModified: Date.now(), model: options.model ?? "deepseek-v4-flash" });
|
|
110
|
-
queue.end();
|
|
111
149
|
}
|
|
112
150
|
catch (error) {
|
|
113
|
-
|
|
151
|
+
// runtime 崩溃 / 传输关闭 / id collision 等:关闭并移除,下次 prompt 重建。
|
|
152
|
+
// 注意:DSH 无 resume 能力,重启 ChatCCC 后磁盘残留同 id log 会再次 id collision;
|
|
153
|
+
// 这里附加清晰的用户提示,引导 /forget 清空会话而非静默失败。
|
|
154
|
+
disposeRuntime(sessionId);
|
|
155
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
156
|
+
if (/id collision/i.test(failure.message)) {
|
|
157
|
+
queue.fail(new Error(`${failure.message}。该 DeepSeek Harness 会话的历史无法跨进程恢复(引擎的 JSON-RPC 仅支持 create 不支持 resume),请使用 /forget 清空本会话或新建会话后重试。`));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
queue.fail(failure);
|
|
161
|
+
}
|
|
114
162
|
}
|
|
115
163
|
finally {
|
|
116
164
|
signal?.removeEventListener("abort", onAbort);
|
|
117
|
-
await runtime.close().catch(() => { });
|
|
118
165
|
await rawLog?.close({ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !completed });
|
|
119
|
-
closed = true;
|
|
120
166
|
}
|
|
121
167
|
})();
|
|
122
168
|
try {
|
|
@@ -125,17 +171,43 @@ export function createDshAdapter(options = {}) {
|
|
|
125
171
|
await task;
|
|
126
172
|
}
|
|
127
173
|
finally {
|
|
128
|
-
|
|
174
|
+
// 正常完成保留 runtime 供复用;abort 已在 onAbort 中 dispose。
|
|
129
175
|
}
|
|
130
176
|
},
|
|
131
177
|
async getSessionInfo(sessionId) {
|
|
132
178
|
return knownSessions.get(sessionId) ?? { sessionId };
|
|
133
179
|
},
|
|
134
|
-
async closeSession() {
|
|
135
|
-
|
|
180
|
+
async closeSession(sessionId) {
|
|
181
|
+
disposeRuntime(sessionId);
|
|
136
182
|
},
|
|
137
183
|
};
|
|
138
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* 从 DSH 引擎的 session.event 里提取第一个 turn/end 错误。
|
|
187
|
+
*
|
|
188
|
+
* DSH 引擎(dsh-agent-loop)在 turn 失败时把错误持久化到 `turn/end` 事件的
|
|
189
|
+
* `data.reason`(`{ kind: "error", error: { message, code, status? } }`),随后
|
|
190
|
+
* `kick()` 吞掉该错误并让 session 进入 idle。SDK 的 `run()` 因此正常返回空
|
|
191
|
+
* `finalResponse`,而不是 reject。此函数把该错误还原成 Error,供 prompt()
|
|
192
|
+
* 重新抛出,复用 session.ts 的统一终态错误分类(401/429/网络等)与脱敏。
|
|
193
|
+
*/
|
|
194
|
+
function extractTurnError(notification) {
|
|
195
|
+
if (notification.method !== "session.event")
|
|
196
|
+
return null;
|
|
197
|
+
const event = asRecord(notification.params.event);
|
|
198
|
+
if (!event || event.type !== "turn/end")
|
|
199
|
+
return null;
|
|
200
|
+
const data = asRecord(event.data);
|
|
201
|
+
const reason = asRecord(data?.reason);
|
|
202
|
+
if (!reason || reason.kind !== "error")
|
|
203
|
+
return null;
|
|
204
|
+
const failure = asRecord(reason.error);
|
|
205
|
+
const code = typeof failure?.code === "string" ? failure.code : "";
|
|
206
|
+
const status = typeof failure?.status === "number" ? `HTTP ${failure.status}` : "";
|
|
207
|
+
const message = typeof failure?.message === "string" ? failure.message : "";
|
|
208
|
+
const detail = [status, code ? `[${code}]` : "", message].filter(Boolean).join(" ").trim();
|
|
209
|
+
return new Error(detail || "DeepSeek Harness 引擎报告了未提供详情的执行错误");
|
|
210
|
+
}
|
|
139
211
|
function notificationToMessage(notification) {
|
|
140
212
|
if (notification.method === "session.status") {
|
|
141
213
|
return notification.params.status === "running"
|