chatccc 0.2.56 → 0.2.58
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/README.md +1 -1
- package/config.sample.json +8 -8
- package/package.json +1 -1
- package/src/__tests__/claude-adapter.test.ts +48 -48
- package/src/__tests__/config-reload.test.ts +14 -14
- package/src/__tests__/config-sample.test.ts +22 -22
- package/src/__tests__/orchestrator.test.ts +168 -0
- package/src/__tests__/privacy.test.ts +142 -142
- package/src/__tests__/session.test.ts +2 -2
- package/src/__tests__/simplify.test.ts +282 -282
- package/src/__tests__/web-ui.test.ts +23 -23
- package/src/adapters/claude-adapter.ts +40 -40
- package/src/index.ts +15 -15
- package/src/orchestrator.ts +31 -19
- package/src/privacy.ts +67 -67
- package/src/session.ts +2 -1
- package/src/simplify.ts +119 -119
- package/src/wechat-platform.ts +3 -15
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ ChatCCC 把本地 AI 编程工具接入即时通讯软件。你可以在手机
|
|
|
32
32
|
| 会话形态 | 群聊,一群一会话 | 私聊,一对一 |
|
|
33
33
|
| 消息展示 | CardKit 卡片,流式更新 | 纯文本,增量推送 |
|
|
34
34
|
| `/new` | 自动创建新群并绑定新会话 | 在当前私聊里创建新会话 |
|
|
35
|
-
| 多会话并行 | 直接切换不同群 |
|
|
35
|
+
| 多会话并行 | 直接切换不同群 | 支持并行,使用切换指令后未完成的任务会继续在后台进行,但不如飞书直观方便 |
|
|
36
36
|
| 群管理 | 支持创建、重命名、解散、头像 | 不支持 |
|
|
37
37
|
| 接入成本 | 需要配置飞书应用 | 启动后扫码登录 |
|
|
38
38
|
|
package/config.sample.json
CHANGED
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
"port": 18080,
|
|
11
11
|
"gitTimeoutSeconds": 180,
|
|
12
12
|
"allowInterrupt": false,
|
|
13
|
-
"claude": {
|
|
14
|
-
"enabled": false,
|
|
15
|
-
"defaultAgent": true,
|
|
16
|
-
"model": "claude-sonnet-4-6",
|
|
17
|
-
"subagentModel": "",
|
|
18
|
-
"effort": "",
|
|
19
|
-
"apiKey": "",
|
|
20
|
-
"baseUrl": ""
|
|
13
|
+
"claude": {
|
|
14
|
+
"enabled": false,
|
|
15
|
+
"defaultAgent": true,
|
|
16
|
+
"model": "claude-sonnet-4-6",
|
|
17
|
+
"subagentModel": "",
|
|
18
|
+
"effort": "",
|
|
19
|
+
"apiKey": "",
|
|
20
|
+
"baseUrl": ""
|
|
21
21
|
},
|
|
22
22
|
"cursor": {
|
|
23
23
|
"enabled": false,
|
package/package.json
CHANGED
|
@@ -771,10 +771,10 @@ describe("createClaudeAdapter — env 注入", () => {
|
|
|
771
771
|
expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://api.deepseek.com/anthropic");
|
|
772
772
|
});
|
|
773
773
|
|
|
774
|
-
it("apiKey + baseUrl 都设置 → 同时覆盖", async () => {
|
|
775
|
-
setupMockCreateSession();
|
|
776
|
-
const adapter = createClaudeAdapter({
|
|
777
|
-
model: "",
|
|
774
|
+
it("apiKey + baseUrl 都设置 → 同时覆盖", async () => {
|
|
775
|
+
setupMockCreateSession();
|
|
776
|
+
const adapter = createClaudeAdapter({
|
|
777
|
+
model: "",
|
|
778
778
|
effort: "",
|
|
779
779
|
isEmpty: (v) => v.trim() === "",
|
|
780
780
|
apiKey: "sk-x",
|
|
@@ -784,50 +784,50 @@ describe("createClaudeAdapter — env 注入", () => {
|
|
|
784
784
|
await adapter.createSession("/cwd");
|
|
785
785
|
|
|
786
786
|
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
787
|
-
expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-x");
|
|
788
|
-
expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
it("官方 API 模式下 subagentModel 不注入 env,也不覆盖 Claude 登录环境", async () => {
|
|
792
|
-
setupMockCreateSession();
|
|
793
|
-
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
|
|
794
|
-
const adapter = createClaudeAdapter({
|
|
795
|
-
model: "claude-sonnet-4-6",
|
|
796
|
-
subagentModel: "claude-haiku-4-5-20251001",
|
|
797
|
-
effort: "",
|
|
798
|
-
isEmpty: (v) => v.trim() === "",
|
|
799
|
-
apiKey: "",
|
|
800
|
-
baseUrl: "",
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
await adapter.createSession("/cwd");
|
|
804
|
-
|
|
805
|
-
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
806
|
-
expect(opts).not.toHaveProperty("env");
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
it("第三方 API 模式下 subagentModel 通过 CLAUDE_CODE_SUBAGENT_MODEL 覆盖 Haiku", async () => {
|
|
810
|
-
setupMockCreateSession();
|
|
811
|
-
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
|
|
812
|
-
const adapter = createClaudeAdapter({
|
|
813
|
-
model: "claude-sonnet-4-6",
|
|
814
|
-
subagentModel: "claude-haiku-4-5-20251001",
|
|
815
|
-
effort: "",
|
|
816
|
-
isEmpty: (v) => v.trim() === "",
|
|
817
|
-
apiKey: "sk-thirdparty",
|
|
818
|
-
baseUrl: "https://gateway.example/anthropic",
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
await adapter.createSession("/cwd");
|
|
822
|
-
|
|
823
|
-
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
824
|
-
expect(opts.model).toBe("claude-sonnet-4-6");
|
|
825
|
-
expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-thirdparty");
|
|
826
|
-
expect(opts.env.CLAUDE_CODE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
|
|
827
|
-
expect(opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
it("ChatCCC API config is isolated from Claude settings auth/model env", async () => {
|
|
787
|
+
expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-x");
|
|
788
|
+
expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("官方 API 模式下 subagentModel 不注入 env,也不覆盖 Claude 登录环境", async () => {
|
|
792
|
+
setupMockCreateSession();
|
|
793
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
|
|
794
|
+
const adapter = createClaudeAdapter({
|
|
795
|
+
model: "claude-sonnet-4-6",
|
|
796
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
797
|
+
effort: "",
|
|
798
|
+
isEmpty: (v) => v.trim() === "",
|
|
799
|
+
apiKey: "",
|
|
800
|
+
baseUrl: "",
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
await adapter.createSession("/cwd");
|
|
804
|
+
|
|
805
|
+
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
806
|
+
expect(opts).not.toHaveProperty("env");
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("第三方 API 模式下 subagentModel 通过 CLAUDE_CODE_SUBAGENT_MODEL 覆盖 Haiku", async () => {
|
|
810
|
+
setupMockCreateSession();
|
|
811
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
|
|
812
|
+
const adapter = createClaudeAdapter({
|
|
813
|
+
model: "claude-sonnet-4-6",
|
|
814
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
815
|
+
effort: "",
|
|
816
|
+
isEmpty: (v) => v.trim() === "",
|
|
817
|
+
apiKey: "sk-thirdparty",
|
|
818
|
+
baseUrl: "https://gateway.example/anthropic",
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
await adapter.createSession("/cwd");
|
|
822
|
+
|
|
823
|
+
const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
|
|
824
|
+
expect(opts.model).toBe("claude-sonnet-4-6");
|
|
825
|
+
expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-thirdparty");
|
|
826
|
+
expect(opts.env.CLAUDE_CODE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
|
|
827
|
+
expect(opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it("ChatCCC API config is isolated from Claude settings auth/model env", async () => {
|
|
831
831
|
setupMockCreateSession();
|
|
832
832
|
process.env.ANTHROPIC_AUTH_TOKEN = "token-from-claude-settings";
|
|
833
833
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-claude-settings";
|
|
@@ -10,9 +10,9 @@ import {
|
|
|
10
10
|
CHATCCC_PORT,
|
|
11
11
|
CLAUDE_API_KEY,
|
|
12
12
|
CLAUDE_BASE_URL,
|
|
13
|
-
CLAUDE_EFFORT,
|
|
14
|
-
CLAUDE_MODEL,
|
|
15
|
-
CLAUDE_SUBAGENT_MODEL,
|
|
13
|
+
CLAUDE_EFFORT,
|
|
14
|
+
CLAUDE_MODEL,
|
|
15
|
+
CLAUDE_SUBAGENT_MODEL,
|
|
16
16
|
CURSOR_AGENT_ARGS,
|
|
17
17
|
CURSOR_AGENT_COMMAND,
|
|
18
18
|
FEISHU_ENABLED,
|
|
@@ -44,10 +44,10 @@ const baseAppConfig: AppConfig = {
|
|
|
44
44
|
allowInterrupt: false,
|
|
45
45
|
claude: {
|
|
46
46
|
enabled: true,
|
|
47
|
-
defaultAgent: true,
|
|
48
|
-
model: "initial-model",
|
|
49
|
-
subagentModel: "initial-subagent-model",
|
|
50
|
-
effort: "initial-effort",
|
|
47
|
+
defaultAgent: true,
|
|
48
|
+
model: "initial-model",
|
|
49
|
+
subagentModel: "initial-subagent-model",
|
|
50
|
+
effort: "initial-effort",
|
|
51
51
|
apiKey: "sk-initial",
|
|
52
52
|
baseUrl: "https://initial.gw/anthropic",
|
|
53
53
|
},
|
|
@@ -80,22 +80,22 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
80
80
|
expect(APP_SECRET).toBe("NEW_APP_SECRET");
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
it("更新 Claude 配置(model / subagentModel / effort / apiKey / baseUrl)", () => {
|
|
83
|
+
it("更新 Claude 配置(model / subagentModel / effort / apiKey / baseUrl)", () => {
|
|
84
84
|
applyLoadedConfig({
|
|
85
85
|
...structuredClone(baseAppConfig),
|
|
86
86
|
claude: {
|
|
87
87
|
enabled: true,
|
|
88
|
-
defaultAgent: true,
|
|
89
|
-
model: "claude-sonnet-4-6",
|
|
90
|
-
subagentModel: "claude-haiku-4-5-20251001",
|
|
91
|
-
effort: "high",
|
|
88
|
+
defaultAgent: true,
|
|
89
|
+
model: "claude-sonnet-4-6",
|
|
90
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
91
|
+
effort: "high",
|
|
92
92
|
apiKey: "sk-newkey",
|
|
93
93
|
baseUrl: "https://gw2.example/anthropic",
|
|
94
94
|
},
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
|
|
98
|
-
expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
|
|
97
|
+
expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
|
|
98
|
+
expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
|
|
99
99
|
expect(CLAUDE_EFFORT).toBe("high");
|
|
100
100
|
expect(CLAUDE_API_KEY).toBe("sk-newkey");
|
|
101
101
|
expect(CLAUDE_BASE_URL).toBe("https://gw2.example/anthropic");
|
|
@@ -6,25 +6,25 @@ import { describe, expect, it } from "vitest";
|
|
|
6
6
|
describe("config.sample.json", () => {
|
|
7
7
|
it("enables Feishu and WeChat iLink by default", () => {
|
|
8
8
|
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
9
|
-
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
10
|
-
platforms?: {
|
|
11
|
-
feishu?: { enabled?: unknown };
|
|
12
|
-
ilink?: { enabled?: unknown };
|
|
13
|
-
};
|
|
14
|
-
claude?: { model?: unknown; subagentModel?: unknown };
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
expect(sample.platforms?.feishu?.enabled).toBe(true);
|
|
18
|
-
expect(sample.platforms?.ilink?.enabled).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("uses an Anthropic official main model and leaves subagent override empty by default", () => {
|
|
22
|
-
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
23
|
-
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
24
|
-
claude?: { model?: unknown; subagentModel?: unknown };
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
expect(sample.claude?.model).toBe("claude-sonnet-4-6");
|
|
28
|
-
expect(sample.claude?.subagentModel).toBe("");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
9
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
10
|
+
platforms?: {
|
|
11
|
+
feishu?: { enabled?: unknown };
|
|
12
|
+
ilink?: { enabled?: unknown };
|
|
13
|
+
};
|
|
14
|
+
claude?: { model?: unknown; subagentModel?: unknown };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
expect(sample.platforms?.feishu?.enabled).toBe(true);
|
|
18
|
+
expect(sample.platforms?.ilink?.enabled).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("uses an Anthropic official main model and leaves subagent override empty by default", () => {
|
|
22
|
+
const configSamplePath = join(process.cwd(), "config.sample.json");
|
|
23
|
+
const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
|
|
24
|
+
claude?: { model?: unknown; subagentModel?: unknown };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
expect(sample.claude?.model).toBe("claude-sonnet-4-6");
|
|
28
|
+
expect(sample.claude?.subagentModel).toBe("");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { PlatformAdapter } from "../platform-adapter.ts";
|
|
7
|
+
import type { SessionInfo, ToolAdapter } from "../adapters/adapter-interface.ts";
|
|
8
|
+
|
|
9
|
+
const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
|
|
10
|
+
|
|
11
|
+
vi.mock("../im-skills.ts", () => ({
|
|
12
|
+
buildImSkillsPrompt: async () => "",
|
|
13
|
+
exportSkillSubDocs: async () => {},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("../stream-state.ts", () => ({
|
|
17
|
+
readStreamState: async (sessionId: string) => {
|
|
18
|
+
const state = mockStreamStates.get(sessionId);
|
|
19
|
+
if (!state) return null;
|
|
20
|
+
return {
|
|
21
|
+
sessionId,
|
|
22
|
+
status: state.status,
|
|
23
|
+
accumulatedContent: "",
|
|
24
|
+
finalReply: state.finalReply,
|
|
25
|
+
chunkCount: 0,
|
|
26
|
+
turnCount: 1,
|
|
27
|
+
contextTokens: 0,
|
|
28
|
+
updatedAt: Date.now(),
|
|
29
|
+
cwd: "F:\\repo",
|
|
30
|
+
tool: "claude",
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
writeStreamState: async (state: { sessionId: string; status: "running" | "done" | "stopped"; finalReply: string }) => {
|
|
34
|
+
mockStreamStates.set(state.sessionId, {
|
|
35
|
+
status: state.status,
|
|
36
|
+
finalReply: state.finalReply,
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
createEmptyStreamState: (sessionId: string, cwd: string, tool: string, turnCount: number) => ({
|
|
40
|
+
sessionId,
|
|
41
|
+
status: "running" as const,
|
|
42
|
+
accumulatedContent: "",
|
|
43
|
+
finalReply: "",
|
|
44
|
+
chunkCount: 0,
|
|
45
|
+
turnCount,
|
|
46
|
+
contextTokens: 0,
|
|
47
|
+
updatedAt: Date.now(),
|
|
48
|
+
cwd,
|
|
49
|
+
tool,
|
|
50
|
+
}),
|
|
51
|
+
fixStaleStreamStates: async () => {},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
import { handleCommand } from "../orchestrator.ts";
|
|
55
|
+
import {
|
|
56
|
+
_clearAdapterCacheForTest,
|
|
57
|
+
_resetSessionRegistryFileForTest,
|
|
58
|
+
_resetSessionToolsFileForTest,
|
|
59
|
+
_setAdapterForToolForTest,
|
|
60
|
+
_setSessionRegistryFileForTest,
|
|
61
|
+
_setSessionToolsFileForTest,
|
|
62
|
+
recordSessionRegistry,
|
|
63
|
+
resetState,
|
|
64
|
+
} from "../session.ts";
|
|
65
|
+
import { activePrompts, resetBindingState } from "../session-chat-binding.ts";
|
|
66
|
+
|
|
67
|
+
function mockPlatform(): PlatformAdapter {
|
|
68
|
+
return {
|
|
69
|
+
kind: "wechat",
|
|
70
|
+
sendText: vi.fn(async () => true),
|
|
71
|
+
sendCard: vi.fn(async () => true),
|
|
72
|
+
sendRawCard: vi.fn(async () => true),
|
|
73
|
+
createGroup: vi.fn(async () => "unused-group"),
|
|
74
|
+
updateChatInfo: vi.fn(async () => {}),
|
|
75
|
+
getChatInfo: vi.fn(async () => ({ name: "微信会话", description: "" })),
|
|
76
|
+
disbandChat: vi.fn(async () => {}),
|
|
77
|
+
setChatAvatar: vi.fn(async () => {}),
|
|
78
|
+
extractSessionInfo: vi.fn(() => null),
|
|
79
|
+
cardCreate: vi.fn(async () => "card-id"),
|
|
80
|
+
cardSend: vi.fn(async () => "message-id"),
|
|
81
|
+
cardUpdate: vi.fn(async () => {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mockAdapter(): ToolAdapter {
|
|
86
|
+
return {
|
|
87
|
+
displayName: "Claude",
|
|
88
|
+
sessionDescPrefix: "Claude Session:",
|
|
89
|
+
createSession: async () => ({ sessionId: "sid-wechat" }),
|
|
90
|
+
prompt: async function* () {
|
|
91
|
+
yield {
|
|
92
|
+
type: "assistant",
|
|
93
|
+
blocks: [{ type: "text", text: "done" }],
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
|
|
97
|
+
sessionId,
|
|
98
|
+
cwd: "F:\\repo",
|
|
99
|
+
}),
|
|
100
|
+
closeSession: async () => {},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe("handleCommand WeChat processing ack", () => {
|
|
105
|
+
let tempDir: string;
|
|
106
|
+
|
|
107
|
+
beforeEach(async () => {
|
|
108
|
+
vi.useFakeTimers();
|
|
109
|
+
tempDir = await mkdtemp(join(tmpdir(), "chatccc-orchestrator-"));
|
|
110
|
+
_setSessionRegistryFileForTest(join(tempDir, "session-registry.json"));
|
|
111
|
+
_setSessionToolsFileForTest(join(tempDir, "sessions.json"));
|
|
112
|
+
resetState();
|
|
113
|
+
resetBindingState();
|
|
114
|
+
mockStreamStates.clear();
|
|
115
|
+
_setAdapterForToolForTest("claude", mockAdapter());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(async () => {
|
|
119
|
+
resetState();
|
|
120
|
+
resetBindingState();
|
|
121
|
+
_clearAdapterCacheForTest();
|
|
122
|
+
_resetSessionRegistryFileForTest();
|
|
123
|
+
_resetSessionToolsFileForTest();
|
|
124
|
+
vi.useRealTimers();
|
|
125
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("does not send the WeChat processing ack when the session is already running", async () => {
|
|
129
|
+
const platform = mockPlatform();
|
|
130
|
+
await recordSessionRegistry({
|
|
131
|
+
chatId: "wx-chat",
|
|
132
|
+
sessionId: "sid-wechat",
|
|
133
|
+
tool: "claude",
|
|
134
|
+
chatName: "busy-session",
|
|
135
|
+
running: true,
|
|
136
|
+
});
|
|
137
|
+
activePrompts.set("sid-wechat", {
|
|
138
|
+
controller: new AbortController(),
|
|
139
|
+
stopped: false,
|
|
140
|
+
startTime: Date.now(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
144
|
+
|
|
145
|
+
expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
146
|
+
expect(platform.sendCard).toHaveBeenCalledWith(
|
|
147
|
+
"wx-chat",
|
|
148
|
+
"生成中",
|
|
149
|
+
"该会话正在生成回复中,请等待完成后再发送新消息。",
|
|
150
|
+
"yellow",
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("sends the WeChat processing ack after the busy check for normal prompts", async () => {
|
|
155
|
+
const platform = mockPlatform();
|
|
156
|
+
await recordSessionRegistry({
|
|
157
|
+
chatId: "wx-chat",
|
|
158
|
+
sessionId: "sid-wechat",
|
|
159
|
+
tool: "claude",
|
|
160
|
+
chatName: "ready-session",
|
|
161
|
+
running: false,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await handleCommand(platform, "继续说明", "wx-chat", "wx-user", Date.now(), "p2p");
|
|
165
|
+
|
|
166
|
+
expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
|
|
167
|
+
});
|
|
168
|
+
});
|