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.
@@ -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
- const runtime = new sdk.DeepSeekHarness({
40
- launch: {
41
- command: process.execPath,
42
- args: [
43
- join(installationDir, "node_modules", "@deepseek-ai", "dsh-sdk-jsonrpc-demo", "lib", "bin.js"),
44
- join(installationDir, "dsh-runtime.cordis.yml"),
45
- ],
46
- cwd,
47
- env: {
48
- ...process.env,
49
- DSH_CWD: cwd,
50
- DSH_SESSION_ROOT: sessionRoot,
51
- ...(options.apiKey ? { DEEPSEEK_API_KEY: options.apiKey } : {}),
52
- ...(options.baseUrl ? { DEEPSEEK_BASE_URL: options.baseUrl } : {}),
53
- // 子代理模型:subModel 留空时跟随主模型(与 CCC 的 ccc.subModel 语义一致)
54
- DSH_SUBAGENT_PROVIDER: options.provider ?? "deepseek-official",
55
- DSH_SUBAGENT_MODEL: options.subModel || options.model || "deepseek-v4-flash",
56
- DSH_SUBAGENT_MAX_TOKENS: String(options.maxTokens ?? 49152),
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
- cwd,
60
- provider: options.provider ?? "deepseek-official",
61
- model: options.model ?? "deepseek-v4-flash",
62
- ...(options.maxTokens ? { maxTokens: options.maxTokens } : {}),
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
- const closeRuntime = () => {
84
- if (closed)
85
- return;
86
- closed = true;
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
- queue.fail(error instanceof Error ? error : new Error(String(error)));
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
- closeRuntime();
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
- // Each prompt owns and closes its runtime. Durable state lives under ~/.chatccc/dsh-sessions.
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.263",
3
+ "version": "0.2.265",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",