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.
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/session.test.ts +91 -1
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/adapters/adapter-interface.ts +13 -0
- package/src/adapters/codex-adapter.ts +4 -0
- package/src/adapters/cursor-adapter.ts +4 -0
- package/src/builtin/cli.ts +196 -196
- package/src/builtin/index.ts +166 -166
- package/src/cards.ts +60 -0
- package/src/config.ts +5 -0
- package/src/index.ts +1 -1
- package/src/orchestrator.ts +116 -6
- package/src/session-chat-binding.ts +9 -0
- package/src/session.ts +185 -23
- package/src/stream-state.ts +33 -33
- package/src/turn-cards.ts +117 -117
package/src/builtin/index.ts
CHANGED
|
@@ -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;
|
package/src/orchestrator.ts
CHANGED
|
@@ -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();
|