chatccc 0.2.94 → 0.2.96

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.
@@ -1,167 +1,167 @@
1
- /**
2
- * builtin/index.ts — ChatCCC 内置 Agent 核心 API
3
- *
4
- * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块(如 ToolAdapter)调用。
5
- */
6
-
7
- import { streamText } from "ai";
8
-
9
- // ---------------------------------------------------------------------------
10
- // 系统提示词 — 编译期冻结常量
11
- // ---------------------------------------------------------------------------
12
-
13
- const SYSTEM_PROMPT = [
14
- "你是 ChatCCC 内置 AI 编程助手,运行在终端环境中。",
15
- "",
16
- "## 基本规则",
17
- "- 用中文回复,但代码、命令、文件名保持原文",
18
- "- 优先给出直接可用的方案,而非长篇解释",
19
- "- 如果用户的问题涉及代码,直接给出代码并说明用法",
20
- "- 保持简洁,一次聚焦一个问题",
21
- ].join("\n");
22
-
23
- // ---------------------------------------------------------------------------
24
- // 类型定义
25
- // ---------------------------------------------------------------------------
26
-
27
- export interface ChatSessionConfig {
28
- /** DeepSeek API 兼容的服务地址,默认 https://api.deepseek.com/v1 */
29
- baseURL?: string;
30
- /** API Key(优先用 DEEPSEEK_API_KEY 环境变量) */
31
- apiKey?: string;
32
- /** 模型名称,默认 deepseek-chat */
33
- model?: string;
34
- }
35
-
36
- export interface ChatSessionOptions {
37
- /** 会话工作目录 */
38
- cwd?: string;
39
- /** 自定义系统提示词(会拼接到默认提示词之后) */
40
- systemPrompt?: string;
41
- }
42
-
43
- /**
44
- * 流式响应事件
45
- */
46
- export type ChatEvent =
47
- | { type: "text"; text: string; accumulated: string }
48
- | { type: "done"; text: string }
49
- | { type: "error"; message: string };
50
-
51
- // ---------------------------------------------------------------------------
52
- // ChatSession
53
- // ---------------------------------------------------------------------------
54
-
55
- /** 消息角色 */
56
- type MessageRole = "system" | "user" | "assistant" | "tool";
57
-
58
- /** 内部消息类型 */
59
- interface ChatMessage {
60
- role: MessageRole;
61
- content: string;
62
- }
63
-
64
- export class ChatSession {
65
- private model: any;
66
- private messages: ChatMessage[];
67
-
68
- constructor(
69
- config: ChatSessionConfig = {},
70
- options: ChatSessionOptions = {},
71
- ) {
72
- const apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY;
73
- if (!apiKey) {
74
- throw new Error(
75
- "DEEPSEEK_API_KEY 未设置。请设置环境变量 DEEPSEEK_API_KEY 或传入 apiKey",
76
- );
77
- }
78
-
79
- const baseURL = config.baseURL ?? "https://api.deepseek.com/v1";
80
- const modelId = config.model ?? "deepseek-chat";
81
-
82
- // 动态 import 以支持 ESM 包在 CJS 环境下的加载(tsx 处理)
83
- const { createOpenAICompatible } = require("@ai-sdk/openai-compatible");
84
- const provider = createOpenAICompatible({
85
- name: "deepseek",
86
- baseURL,
87
- apiKey,
88
- });
89
- this.model = provider(modelId);
90
-
91
- // 构建系统提示词
92
- const systemContent = [SYSTEM_PROMPT];
93
- if (options?.systemPrompt) {
94
- systemContent.push("", options.systemPrompt);
95
- }
96
- if (options?.cwd) {
97
- systemContent.push("", `当前工作目录: ${options.cwd}`);
98
- }
99
-
100
- this.messages = [{ role: "system", content: systemContent.join("\n") }];
101
- }
102
-
103
- /**
104
- * 发送用户消息,返回异步可迭代的文本流。
105
- *
106
- * 使用方式:
107
- * ```typescript
108
- * const session = new ChatSession();
109
- * for await (const event of session.chat("帮我看看 package.json")) {
110
- * if (event.type === "text") process.stdout.write(event.text);
111
- * }
112
- * console.log("完成");
113
- * ```
114
- */
115
- async *chat(
116
- userMessage: string,
117
- signal?: AbortSignal,
118
- ): AsyncIterable<ChatEvent> {
119
- this.messages.push({ role: "user", content: userMessage });
120
-
121
- let fullText = "";
122
-
123
- try {
124
- const result = streamText({
125
- model: this.model,
126
- messages: this.messages as any,
127
- abortSignal: signal,
128
- });
129
-
130
- for await (const chunk of result.textStream) {
131
- fullText += chunk;
132
- yield { type: "text", text: chunk, accumulated: fullText };
133
- }
134
-
135
- this.messages.push({ role: "assistant", content: fullText });
136
- yield { type: "done", text: fullText };
137
- } catch (err) {
138
- const message = err instanceof Error ? err.message : String(err);
139
- if ((err as Error).name === "AbortError" || signal?.aborted) {
140
- // 被中断时,不保存不完整的助手消息
141
- if (fullText) {
142
- this.messages.push({ role: "assistant", content: fullText + "\n[已中断]" });
143
- }
144
- yield { type: "done", text: fullText };
145
- return;
146
- }
147
- yield { type: "error", message };
148
- throw err;
149
- }
150
- }
151
-
152
- /** 返回当前的会话历史(只读) */
153
- get history(): ReadonlyArray<ChatMessage> {
154
- return this.messages;
155
- }
156
-
157
- /** 返回当前轮数(不含 system 消息) */
158
- get turnCount(): number {
159
- return this.messages.length - 1;
160
- }
161
-
162
- /** 清空会话历史,保留 system 消息 */
163
- reset(): void {
164
- const system = this.messages[0];
165
- this.messages = [system];
166
- }
1
+ /**
2
+ * builtin/index.ts — ChatCCC 内置 Agent 核心 API
3
+ *
4
+ * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块(如 ToolAdapter)调用。
5
+ */
6
+
7
+ import { streamText } from "ai";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // 系统提示词 — 编译期冻结常量
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const SYSTEM_PROMPT = [
14
+ "你是 ChatCCC 内置 AI 编程助手,运行在终端环境中。",
15
+ "",
16
+ "## 基本规则",
17
+ "- 用中文回复,但代码、命令、文件名保持原文",
18
+ "- 优先给出直接可用的方案,而非长篇解释",
19
+ "- 如果用户的问题涉及代码,直接给出代码并说明用法",
20
+ "- 保持简洁,一次聚焦一个问题",
21
+ ].join("\n");
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // 类型定义
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface ChatSessionConfig {
28
+ /** DeepSeek API 兼容的服务地址,默认 https://api.deepseek.com/v1 */
29
+ baseURL?: string;
30
+ /** API Key(优先用 DEEPSEEK_API_KEY 环境变量) */
31
+ apiKey?: string;
32
+ /** 模型名称,默认 deepseek-chat */
33
+ model?: string;
34
+ }
35
+
36
+ export interface ChatSessionOptions {
37
+ /** 会话工作目录 */
38
+ cwd?: string;
39
+ /** 自定义系统提示词(会拼接到默认提示词之后) */
40
+ systemPrompt?: string;
41
+ }
42
+
43
+ /**
44
+ * 流式响应事件
45
+ */
46
+ export type ChatEvent =
47
+ | { type: "text"; text: string; accumulated: string }
48
+ | { type: "done"; text: string }
49
+ | { type: "error"; message: string };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // ChatSession
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** 消息角色 */
56
+ type MessageRole = "system" | "user" | "assistant" | "tool";
57
+
58
+ /** 内部消息类型 */
59
+ interface ChatMessage {
60
+ role: MessageRole;
61
+ content: string;
62
+ }
63
+
64
+ export class ChatSession {
65
+ private model: any;
66
+ private messages: ChatMessage[];
67
+
68
+ constructor(
69
+ config: ChatSessionConfig = {},
70
+ options: ChatSessionOptions = {},
71
+ ) {
72
+ const apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY;
73
+ if (!apiKey) {
74
+ throw new Error(
75
+ "DEEPSEEK_API_KEY 未设置。请设置环境变量 DEEPSEEK_API_KEY 或传入 apiKey",
76
+ );
77
+ }
78
+
79
+ const baseURL = config.baseURL ?? "https://api.deepseek.com/v1";
80
+ const modelId = config.model ?? "deepseek-chat";
81
+
82
+ // 动态 import 以支持 ESM 包在 CJS 环境下的加载(tsx 处理)
83
+ const { createOpenAICompatible } = require("@ai-sdk/openai-compatible");
84
+ const provider = createOpenAICompatible({
85
+ name: "deepseek",
86
+ baseURL,
87
+ apiKey,
88
+ });
89
+ this.model = provider(modelId);
90
+
91
+ // 构建系统提示词
92
+ const systemContent = [SYSTEM_PROMPT];
93
+ if (options?.systemPrompt) {
94
+ systemContent.push("", options.systemPrompt);
95
+ }
96
+ if (options?.cwd) {
97
+ systemContent.push("", `当前工作目录: ${options.cwd}`);
98
+ }
99
+
100
+ this.messages = [{ role: "system", content: systemContent.join("\n") }];
101
+ }
102
+
103
+ /**
104
+ * 发送用户消息,返回异步可迭代的文本流。
105
+ *
106
+ * 使用方式:
107
+ * ```typescript
108
+ * const session = new ChatSession();
109
+ * for await (const event of session.chat("帮我看看 package.json")) {
110
+ * if (event.type === "text") process.stdout.write(event.text);
111
+ * }
112
+ * console.log("完成");
113
+ * ```
114
+ */
115
+ async *chat(
116
+ userMessage: string,
117
+ signal?: AbortSignal,
118
+ ): AsyncIterable<ChatEvent> {
119
+ this.messages.push({ role: "user", content: userMessage });
120
+
121
+ let fullText = "";
122
+
123
+ try {
124
+ const result = streamText({
125
+ model: this.model,
126
+ messages: this.messages as any,
127
+ abortSignal: signal,
128
+ });
129
+
130
+ for await (const chunk of result.textStream) {
131
+ fullText += chunk;
132
+ yield { type: "text", text: chunk, accumulated: fullText };
133
+ }
134
+
135
+ this.messages.push({ role: "assistant", content: fullText });
136
+ yield { type: "done", text: fullText };
137
+ } catch (err) {
138
+ const message = err instanceof Error ? err.message : String(err);
139
+ if ((err as Error).name === "AbortError" || signal?.aborted) {
140
+ // 被中断时,不保存不完整的助手消息
141
+ if (fullText) {
142
+ this.messages.push({ role: "assistant", content: fullText + "\n[已中断]" });
143
+ }
144
+ yield { type: "done", text: fullText };
145
+ return;
146
+ }
147
+ yield { type: "error", message };
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ /** 返回当前的会话历史(只读) */
153
+ get history(): ReadonlyArray<ChatMessage> {
154
+ return this.messages;
155
+ }
156
+
157
+ /** 返回当前轮数(不含 system 消息) */
158
+ get turnCount(): number {
159
+ return this.messages.length - 1;
160
+ }
161
+
162
+ /** 清空会话历史,保留 system 消息 */
163
+ reset(): void {
164
+ const system = this.messages[0];
165
+ this.messages = [system];
166
+ }
167
167
  }
package/src/cards.ts CHANGED
@@ -407,3 +407,63 @@ export function buildStatusCard(statusText: string, template = "blue"): string {
407
407
  ],
408
408
  });
409
409
  }
410
+
411
+ export interface ModelSwitchOption {
412
+ label: string;
413
+ model: string;
414
+ }
415
+
416
+ /** 模型切换卡片(/model 命令,飞书专用,含切换按钮) */
417
+ export function buildModelCard(
418
+ currentModel: string,
419
+ available: ModelSwitchOption[],
420
+ ): string {
421
+ const currentLine = currentModel
422
+ ? `**当前模型:** \`${currentModel}\``
423
+ : "**当前模型:** 未指定";
424
+ const hasPro = available.some(o => o.label === "pro");
425
+ const hasFlash = available.some(o => o.label === "flash");
426
+
427
+ const lines: string[] = [currentLine];
428
+ if (available.length > 0) {
429
+ lines.push("", "**可切换模型:**");
430
+ for (const opt of available) {
431
+ lines.push(`- ${opt.label}: \`${opt.model}\``);
432
+ }
433
+ } else {
434
+ lines.push("", "没有可切换的模型。");
435
+ }
436
+
437
+ const buttons: { text: string; value: string; type?: "primary" | "default" | "danger" }[] = [];
438
+ if (hasPro) {
439
+ buttons.push({
440
+ text: "/model pro",
441
+ value: JSON.stringify({ action: "model_pro" }),
442
+ type: "primary",
443
+ });
444
+ }
445
+ if (hasFlash) {
446
+ buttons.push({
447
+ text: "/model flash",
448
+ value: JSON.stringify({ action: "model_flash" }),
449
+ type: "primary",
450
+ });
451
+ }
452
+ if (!hasPro && !hasFlash) {
453
+ buttons.push({
454
+ text: "/model clear",
455
+ value: JSON.stringify({ action: "model_clear" }),
456
+ type: "default",
457
+ });
458
+ }
459
+
460
+ return JSON.stringify({
461
+ config: { wide_screen_mode: true },
462
+ header: { template: "blue", title: { content: "模型切换", tag: "plain_text" } },
463
+ elements: [
464
+ { tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
465
+ { tag: "hr" },
466
+ buildButtons(buttons),
467
+ ],
468
+ });
469
+ }
package/src/config.ts CHANGED
@@ -511,6 +511,11 @@ export let CLAUDE_API_KEY = config.claude.apiKey;
511
511
  /** Anthropic 兼容网关的 base URL(仅经 SDK 子进程 env 传递,从不写入主进程 process.env) */
512
512
  export let CLAUDE_BASE_URL = config.claude.baseUrl;
513
513
 
514
+ /** 返回当前生效的 Claude 模型(per-session 覆盖由 session.ts 管理,此处仅返回全局配置) */
515
+ export function getEffectiveClaudeModel(): string {
516
+ return CLAUDE_MODEL;
517
+ }
518
+
514
519
  // ---------------------------------------------------------------------------
515
520
  // /git 超时配置(实际值来自 config.json,纯函数与常量见 ./config-utils.ts)
516
521
  // ---------------------------------------------------------------------------
package/src/index.ts CHANGED
@@ -285,7 +285,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
285
285
  }
286
286
  if (!cmd) return null;
287
287
 
288
- const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", newh: "/newh" };
288
+ const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", newh: "/newh", model_pro: "/model pro", model_flash: "/model flash", model_clear: "/model clear" };
289
289
  let text = CMD_MAP[cmd] ?? "";
290
290
  if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
291
291
  const path = (action.value as Record<string, string>).path;
@@ -13,6 +13,7 @@ import { makeTraceId, logTrace } from "./trace.ts";
13
13
  import {
14
14
  CLAUDE_EFFORT,
15
15
  CLAUDE_MODEL,
16
+ CLAUDE_SUBAGENT_MODEL,
16
17
  GIT_TIMEOUT_MS,
17
18
  PROJECT_ROOT,
18
19
  anthropicConfigDisplay,
@@ -28,6 +29,7 @@ import {
28
29
  } from "./config.ts";
29
30
  import {
30
31
  buildHelpCard,
32
+ buildModelCard,
31
33
  buildStatusCard,
32
34
  buildCdContent,
33
35
  buildCdCard,
@@ -41,12 +43,14 @@ import {
41
43
  runGitCommand,
42
44
  } from "./git-command.ts";
43
45
  import {
46
+ clearSessionModelOverride,
44
47
  getSessionStatus,
45
48
  getAllSessionsStatus,
46
49
  initClaudeSession,
47
50
  lastMsgTimestamps,
48
51
  resumeAndPrompt,
49
52
  sessionInfoMap,
53
+ setSessionModelOverride,
50
54
  switchChatBinding,
51
55
  recordSessionRegistry,
52
56
  getAdapterForTool,
@@ -141,7 +145,7 @@ export async function handleCommand(
141
145
  chatInfo.description,
142
146
  );
143
147
  if (sessionInfoResult) {
144
- const adapter = getAdapterForTool(sessionInfoResult.tool);
148
+ const adapter = getAdapterForTool(sessionInfoResult.tool, sessionInfoResult.sessionId);
145
149
  const info = await adapter.getSessionInfo(sessionInfoResult.sessionId);
146
150
  sessionCwd = info?.cwd;
147
151
  }
@@ -243,6 +247,54 @@ export async function handleCommand(
243
247
  return;
244
248
  }
245
249
 
250
+ if (textLower === "/model") {
251
+ logTrace(tid, "BRANCH", { cmd: "/model" });
252
+
253
+ const isDeepSeek = CLAUDE_MODEL.toLowerCase().includes("deepseek")
254
+ || CLAUDE_SUBAGENT_MODEL.toLowerCase().includes("deepseek");
255
+ if (!isDeepSeek) {
256
+ const msg = "当前配置不支持模型切换(模型名中未找到 DeepSeek 相关内容)。";
257
+ await (platform.kind === "wechat"
258
+ ? platform.sendText(chatId, msg)
259
+ : platform.sendCard(chatId, "模型切换", msg, "red")
260
+ ).catch(() => {});
261
+ logTrace(tid, "DONE", { outcome: "model_unsupported" });
262
+ return;
263
+ }
264
+
265
+ const findModel = (keyword: string): string | null => {
266
+ for (const name of [CLAUDE_MODEL.trim(), CLAUDE_SUBAGENT_MODEL.trim()]) {
267
+ if (!name) continue;
268
+ const nl = name.toLowerCase();
269
+ if (nl.includes("deepseek") && nl.includes(keyword)) return name;
270
+ }
271
+ return null;
272
+ };
273
+
274
+ const available: { label: string; model: string }[] = [];
275
+ const pm = findModel("pro");
276
+ if (pm) available.push({ label: "pro", model: pm });
277
+ const fm = findModel("flash");
278
+ if (fm) available.push({ label: "flash", model: fm });
279
+
280
+ if (platform.kind === "wechat") {
281
+ const lines = [CLAUDE_MODEL ? `当前模型: ${CLAUDE_MODEL}` : "当前模型: 未指定"];
282
+ if (available.length > 0) {
283
+ lines.push("", "可切换模型:");
284
+ for (const o of available) lines.push(` ${o.label}: ${o.model}`);
285
+ lines.push("", "输入 /model pro 或 /model flash 切换模型");
286
+ } else {
287
+ lines.push("", "没有可切换的模型。请在 config.json 的 claude.model 或 claude.subagentModel 中配置含 pro/flash 与 deepseek 关键字的模型名。");
288
+ }
289
+ await platform.sendText(chatId, lines.join("\n")).catch(() => {});
290
+ } else {
291
+ const card = buildModelCard(CLAUDE_MODEL, available);
292
+ await platform.sendRawCard(chatId, card);
293
+ }
294
+ logTrace(tid, "DONE", { outcome: "model_query" });
295
+ return;
296
+ }
297
+
246
298
  if (textLower === "/new" || textLower.startsWith("/new ")) {
247
299
  const toolArg = text.slice(5).trim().toLowerCase();
248
300
  const tool = toolArg || resolveDefaultAgentTool();
@@ -510,7 +562,7 @@ export async function handleCommand(
510
562
  ) {
511
563
  const MAX_PREFIX = 10;
512
564
  const prefix = text.slice(0, MAX_PREFIX);
513
- const adapter = getAdapterForTool(descriptionTool);
565
+ const adapter = getAdapterForTool(descriptionTool, sessionId);
514
566
  const info = await adapter
515
567
  .getSessionInfo(sessionId)
516
568
  .catch(() => undefined);
@@ -553,7 +605,7 @@ export async function handleCommand(
553
605
  ) {
554
606
  const MAX_PREFIX = 10;
555
607
  const prefix = text.slice(0, MAX_PREFIX);
556
- const adapter = getAdapterForTool(descriptionTool!);
608
+ const adapter = getAdapterForTool(descriptionTool!, sessionId);
557
609
  const info = await adapter
558
610
  .getSessionInfo(sessionId)
559
611
  .catch(() => undefined);
@@ -674,7 +726,7 @@ export async function handleCommand(
674
726
 
675
727
  if (textLower === "/newh") {
676
728
  logTrace(tid, "BRANCH", { cmd: "/newh" });
677
- const adapter = getAdapterForTool(descriptionTool);
729
+ const adapter = getAdapterForTool(descriptionTool, sessionId);
678
730
  let cwd: string;
679
731
  try {
680
732
  const info = await adapter.getSessionInfo(sessionId);
@@ -849,7 +901,7 @@ export async function handleCommand(
849
901
  return;
850
902
  }
851
903
 
852
- const targetAdapter = getAdapterForTool(target.tool);
904
+ const targetAdapter = getAdapterForTool(target.tool, target.sessionId);
853
905
  let cwd2: string;
854
906
  try {
855
907
  const targetInfo = await targetAdapter.getSessionInfo(
@@ -920,6 +972,64 @@ export async function handleCommand(
920
972
  return;
921
973
  }
922
974
 
975
+ // /model pro、/model flash、/model clear(仅对当前 session 生效)
976
+ if (textLower === "/model pro" || textLower === "/model flash" || textLower === "/model clear") {
977
+ const modelArg = textLower === "/model clear" ? "clear" : textLower.slice(7).trim();
978
+ logTrace(tid, "BRANCH", { cmd: "/model", arg: modelArg, sessionId });
979
+
980
+ // 仅 Claude Code 支持模型切换
981
+ if (descriptionTool !== "claude") {
982
+ const msg = "模型切换仅支持 Claude Code 会话。";
983
+ await (platform.kind === "wechat"
984
+ ? platform.sendText(chatId, msg)
985
+ : platform.sendCard(chatId, "模型切换", msg, "red")
986
+ ).catch(() => {});
987
+ logTrace(tid, "DONE", { outcome: "model_wrong_tool", tool: descriptionTool });
988
+ return;
989
+ }
990
+
991
+ const findModel = (keyword: string): string | null => {
992
+ for (const name of [CLAUDE_MODEL.trim(), CLAUDE_SUBAGENT_MODEL.trim()]) {
993
+ if (!name) continue;
994
+ const nl = name.toLowerCase();
995
+ if (nl.includes("deepseek") && nl.includes(keyword)) return name;
996
+ }
997
+ return null;
998
+ };
999
+
1000
+ if (modelArg === "clear") {
1001
+ clearSessionModelOverride(sessionId);
1002
+ const restored = CLAUDE_MODEL.trim() || "(未指定)";
1003
+ const msg = `已清除当前会话的模型覆盖,恢复使用: \`${restored}\``;
1004
+ await (platform.kind === "wechat"
1005
+ ? platform.sendText(chatId, msg)
1006
+ : platform.sendCard(chatId, "模型切换", msg, "green")
1007
+ ).catch(() => {});
1008
+ logTrace(tid, "DONE", { outcome: "model_cleared", sessionId });
1009
+ return;
1010
+ }
1011
+
1012
+ const target = findModel(modelArg);
1013
+ if (!target) {
1014
+ const msg = `未找到同时含 "${modelArg}" 和 "deepseek" 关键字的模型。请在 config.json 的 claude.model 或 claude.subagentModel 中配置。`;
1015
+ await (platform.kind === "wechat"
1016
+ ? platform.sendText(chatId, msg)
1017
+ : platform.sendCard(chatId, "模型切换", msg, "red")
1018
+ ).catch(() => {});
1019
+ logTrace(tid, "DONE", { outcome: "model_not_found", arg: modelArg });
1020
+ return;
1021
+ }
1022
+
1023
+ setSessionModelOverride(sessionId, target);
1024
+ const msg = `已切换当前会话模型为 ${modelArg}: \`${target}\``;
1025
+ await (platform.kind === "wechat"
1026
+ ? platform.sendText(chatId, msg)
1027
+ : platform.sendCard(chatId, "模型切换", msg, "green")
1028
+ ).catch(() => {});
1029
+ logTrace(tid, "DONE", { outcome: "model_switched", arg: modelArg, target, sessionId });
1030
+ return;
1031
+ }
1032
+
923
1033
  // /git <args>:在「当前会话工作目录」执行 git 命令
924
1034
  if (textLower.startsWith("/git ") || textLower === "/git") {
925
1035
  const args = text === "/git" ? "" : text.slice(5).trim();
@@ -935,7 +1045,7 @@ export async function handleCommand(
935
1045
  return;
936
1046
  }
937
1047
 
938
- const adapter = getAdapterForTool(descriptionTool);
1048
+ const adapter = getAdapterForTool(descriptionTool, sessionId);
939
1049
  let cwd: string | undefined;
940
1050
  try {
941
1051
  const info = await adapter.getSessionInfo(sessionId);
@@ -65,6 +65,12 @@ export interface ActivePrompt {
65
65
  controller: AbortController;
66
66
  stopped: boolean;
67
67
  startTime: number;
68
+ /** Root PID for the CLI process currently serving this prompt, if the adapter exposes one. */
69
+ processPid?: number;
70
+ processMonitor?: ReturnType<typeof setInterval>;
71
+ /** Set when the watchdog detects that the CLI process disappeared before stream finalization. */
72
+ abnormalExit?: boolean;
73
+ abnormalExitNotified?: boolean;
68
74
  }
69
75
 
70
76
  export const activePrompts = new Map<string, ActivePrompt>();
@@ -206,6 +212,9 @@ export function consumeQueuedMessage(platform: PlatformAdapter, msg: QueuedMessa
206
212
  export function resetBindingState(): void {
207
213
  sessionChatsMap.clear();
208
214
  lastActiveChatMap.clear();
215
+ for (const prompt of activePrompts.values()) {
216
+ if (prompt.processMonitor) clearInterval(prompt.processMonitor);
217
+ }
209
218
  activePrompts.clear();
210
219
  queuedMessages.clear();
211
220
  displayCards.clear();