chatccc 0.2.58 → 0.2.59
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/config.sample.json +8 -8
- package/im-skills/wechat-skill/receive-send-image.md +39 -0
- package/im-skills/wechat-skill/send-image.mjs +80 -0
- package/im-skills/wechat-skill/skill.md +11 -0
- package/package.json +59 -59
- 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 -168
- package/src/__tests__/web-ui.test.ts +23 -23
- package/src/__tests__/wechat-platform.test.ts +44 -2
- package/src/adapters/claude-adapter.ts +40 -40
- package/src/index.ts +17 -17
- package/src/orchestrator.ts +31 -31
- package/src/session.ts +2 -0
- package/src/web-ui.ts +9 -5
- package/src/wechat-platform.ts +141 -16
|
@@ -1,168 +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
|
-
});
|
|
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
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
applyClaudeApiMode,
|
|
4
|
-
chooseStartPath,
|
|
5
|
-
detectClaudeApiMode,
|
|
6
|
-
unflattenConfig,
|
|
7
|
-
} from "../web-ui.ts";
|
|
3
|
+
applyClaudeApiMode,
|
|
4
|
+
chooseStartPath,
|
|
5
|
+
detectClaudeApiMode,
|
|
6
|
+
unflattenConfig,
|
|
7
|
+
} from "../web-ui.ts";
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// detectClaudeApiMode — 加载已有 config 时如何判定 UI 初始模式
|
|
@@ -57,7 +57,7 @@ describe("detectClaudeApiMode", () => {
|
|
|
57
57
|
// - mode 未传 默认按 official 兌底(不保留可能误填的密钥)
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
59
59
|
|
|
60
|
-
describe("applyClaudeApiMode", () => {
|
|
60
|
+
describe("applyClaudeApiMode", () => {
|
|
61
61
|
it("mode=official 时清空 CLAUDE_API_KEY / CLAUDE_BASE_URL(即使 vars 没传)", () => {
|
|
62
62
|
const out = applyClaudeApiMode({ CHATCCC_APP_ID: "x" }, "official");
|
|
63
63
|
expect(out).toEqual({
|
|
@@ -128,23 +128,23 @@ describe("applyClaudeApiMode", () => {
|
|
|
128
128
|
expect(input).toEqual({ CLAUDE_API_KEY: "sk-x" }); // 原对象未变
|
|
129
129
|
expect(out).not.toBe(input);
|
|
130
130
|
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe("unflattenConfig", () => {
|
|
134
|
-
it("maps Claude subagent model into claude.subagentModel", () => {
|
|
135
|
-
expect(
|
|
136
|
-
unflattenConfig({
|
|
137
|
-
CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
|
|
138
|
-
CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
|
|
139
|
-
}),
|
|
140
|
-
).toEqual({
|
|
141
|
-
claude: {
|
|
142
|
-
model: "claude-sonnet-4-6",
|
|
143
|
-
subagentModel: "claude-haiku-4-5-20251001",
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("unflattenConfig", () => {
|
|
134
|
+
it("maps Claude subagent model into claude.subagentModel", () => {
|
|
135
|
+
expect(
|
|
136
|
+
unflattenConfig({
|
|
137
|
+
CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
|
|
138
|
+
CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
|
|
139
|
+
}),
|
|
140
|
+
).toEqual({
|
|
141
|
+
claude: {
|
|
142
|
+
model: "claude-sonnet-4-6",
|
|
143
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
148
|
|
|
149
149
|
// ---------------------------------------------------------------------------
|
|
150
150
|
// chooseStartPath — /api/start 的路径选择
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { buildHelpCard } from "../cards.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
_flushPendingClawFinalTextForTest,
|
|
6
|
+
_resetWechatClawStateForTest,
|
|
7
|
+
createWechatAdapter,
|
|
8
|
+
} from "../wechat-platform.ts";
|
|
5
9
|
|
|
6
10
|
describe("createWechatAdapter", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
_resetWechatClawStateForTest();
|
|
13
|
+
});
|
|
14
|
+
|
|
7
15
|
it("degrades raw cards to plain text messages", async () => {
|
|
8
16
|
const wire = {
|
|
9
17
|
push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
|
|
@@ -54,4 +62,38 @@ describe("createWechatAdapter", () => {
|
|
|
54
62
|
"[WECHAT] sendText skipped (claw limit): chatId=wx-chat-limit count=11",
|
|
55
63
|
);
|
|
56
64
|
});
|
|
65
|
+
|
|
66
|
+
it("queues final messages after the claw limit until the user wakes the chat", async () => {
|
|
67
|
+
const wire = {
|
|
68
|
+
push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
|
|
69
|
+
sendText: vi.fn(
|
|
70
|
+
async (_chatId: string, _text: string, _contextToken?: string) =>
|
|
71
|
+
"msg-id",
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
const log = vi.fn();
|
|
75
|
+
const platform = createWechatAdapter({
|
|
76
|
+
getWire: () => wire,
|
|
77
|
+
log,
|
|
78
|
+
});
|
|
79
|
+
const chatId = "wx-chat-final-limit";
|
|
80
|
+
const finalText = "done\n━━━ 回答结束 ━━━";
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < 10; i++) {
|
|
83
|
+
await expect(platform.sendText(chatId, `chunk ${i}`)).resolves.toBe(true);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await expect(platform.sendText(chatId, finalText)).resolves.toBe(true);
|
|
87
|
+
expect(wire.push).toHaveBeenCalledTimes(10);
|
|
88
|
+
expect(log).toHaveBeenCalledWith(
|
|
89
|
+
`[WECHAT] final queued (claw limit): chatId=${chatId} count=11 len=${finalText.length}`,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
await expect(_flushPendingClawFinalTextForTest(chatId, wire, log)).resolves.toBe(true);
|
|
93
|
+
expect(wire.push).toHaveBeenCalledTimes(11);
|
|
94
|
+
expect(wire.push).toHaveBeenLastCalledWith(chatId, finalText);
|
|
95
|
+
expect(log).toHaveBeenCalledWith(
|
|
96
|
+
`[WECHAT] pending final sent after claw wake: chatId=${chatId} len=${finalText.length}`,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
57
99
|
});
|
|
@@ -86,33 +86,33 @@ function buildSdkEnv(
|
|
|
86
86
|
baseUrl: string | undefined,
|
|
87
87
|
subagentModel: string | undefined,
|
|
88
88
|
): Record<string, string | undefined> | undefined {
|
|
89
|
-
const apiKeyTrim = (apiKey ?? "").trim();
|
|
90
|
-
const baseUrlTrim = (baseUrl ?? "").trim();
|
|
91
|
-
const subagentModelTrim = (subagentModel ?? "").trim();
|
|
92
|
-
const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
|
|
93
|
-
if (!hasApiOverride) return undefined;
|
|
94
|
-
|
|
95
|
-
const env: Record<string, string | undefined> = { ...process.env };
|
|
96
|
-
// ChatCCC's third-party Claude API config is authoritative when present.
|
|
97
|
-
// Remove Claude Code/user settings env that can silently override gateway/auth/model choice.
|
|
98
|
-
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
99
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
100
|
-
delete env.ANTHROPIC_MODEL;
|
|
101
|
-
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
102
|
-
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
103
|
-
delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
104
|
-
delete env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
105
|
-
delete env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
106
|
-
|
|
107
|
-
if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
|
|
108
|
-
if (baseUrlTrim) {
|
|
109
|
-
env.ANTHROPIC_BASE_URL = baseUrlTrim;
|
|
110
|
-
} else {
|
|
111
|
-
delete env.ANTHROPIC_BASE_URL;
|
|
112
|
-
}
|
|
113
|
-
if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
|
|
114
|
-
return env;
|
|
115
|
-
}
|
|
89
|
+
const apiKeyTrim = (apiKey ?? "").trim();
|
|
90
|
+
const baseUrlTrim = (baseUrl ?? "").trim();
|
|
91
|
+
const subagentModelTrim = (subagentModel ?? "").trim();
|
|
92
|
+
const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
|
|
93
|
+
if (!hasApiOverride) return undefined;
|
|
94
|
+
|
|
95
|
+
const env: Record<string, string | undefined> = { ...process.env };
|
|
96
|
+
// ChatCCC's third-party Claude API config is authoritative when present.
|
|
97
|
+
// Remove Claude Code/user settings env that can silently override gateway/auth/model choice.
|
|
98
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
99
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
100
|
+
delete env.ANTHROPIC_MODEL;
|
|
101
|
+
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
102
|
+
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
103
|
+
delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
104
|
+
delete env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
105
|
+
delete env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
106
|
+
|
|
107
|
+
if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
|
|
108
|
+
if (baseUrlTrim) {
|
|
109
|
+
env.ANTHROPIC_BASE_URL = baseUrlTrim;
|
|
110
|
+
} else {
|
|
111
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
112
|
+
}
|
|
113
|
+
if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
|
|
114
|
+
return env;
|
|
115
|
+
}
|
|
116
116
|
|
|
117
117
|
function resolveSettingSources(
|
|
118
118
|
_apiKey: string | undefined,
|
|
@@ -291,19 +291,19 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
291
291
|
this.baseUrl,
|
|
292
292
|
this.subagentModel,
|
|
293
293
|
);
|
|
294
|
-
const session = unstable_v2_resumeSession(
|
|
295
|
-
sessionId,
|
|
296
|
-
sessionOpts as any,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
if (signal?.aborted) {
|
|
300
|
-
session.close();
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const onAbort = () => { session.close(); };
|
|
304
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
305
|
-
|
|
306
|
-
await session.send(userText);
|
|
294
|
+
const session = unstable_v2_resumeSession(
|
|
295
|
+
sessionId,
|
|
296
|
+
sessionOpts as any,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (signal?.aborted) {
|
|
300
|
+
session.close();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const onAbort = () => { session.close(); };
|
|
304
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
305
|
+
|
|
306
|
+
await session.send(userText);
|
|
307
307
|
|
|
308
308
|
const stream = session.stream();
|
|
309
309
|
|
package/src/index.ts
CHANGED
|
@@ -84,9 +84,9 @@ import {
|
|
|
84
84
|
updateCardKitCard,
|
|
85
85
|
} from "./cardkit.ts";
|
|
86
86
|
import {
|
|
87
|
-
MAX_PROCESSED,
|
|
88
|
-
clearAdapterCache,
|
|
89
|
-
loadSessionRegistryForBinding,
|
|
87
|
+
MAX_PROCESSED,
|
|
88
|
+
clearAdapterCache,
|
|
89
|
+
loadSessionRegistryForBinding,
|
|
90
90
|
processedMessages,
|
|
91
91
|
rebuildBindingsFromRegistry,
|
|
92
92
|
resetState,
|
|
@@ -744,12 +744,12 @@ async function main(): Promise<void> {
|
|
|
744
744
|
// "保存并启动" 时,web-ui 会调用本回调,把磁盘上刚保存的 config.json
|
|
745
745
|
// 刷进进程内的 export let 常量(live binding 让 CLAUDE_MODEL 等下次创建
|
|
746
746
|
// 会话时自动看到新值)。setup 首次激活走 onActivate 路径,不依赖此 hook。
|
|
747
|
-
setReloadConfigHook(() => {
|
|
748
|
-
reloadConfigFromDisk();
|
|
749
|
-
clearAdapterCache();
|
|
750
|
-
appendStartupTrace("reload-from-ui: config reloaded", {
|
|
751
|
-
appIdMask: maskAppId(APP_ID),
|
|
752
|
-
});
|
|
747
|
+
setReloadConfigHook(() => {
|
|
748
|
+
reloadConfigFromDisk();
|
|
749
|
+
clearAdapterCache();
|
|
750
|
+
appendStartupTrace("reload-from-ui: config reloaded", {
|
|
751
|
+
appIdMask: maskAppId(APP_ID),
|
|
752
|
+
});
|
|
753
753
|
});
|
|
754
754
|
setExtraApiHandler(async (req, res) => {
|
|
755
755
|
return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
|
|
@@ -769,12 +769,12 @@ async function main(): Promise<void> {
|
|
|
769
769
|
// 凭证不全:进 setup 向导。注入 onActivate 回调让用户点"保存并启动"
|
|
770
770
|
// 时,原地(同进程)调用 startBotService,复用 setup HTTP server。
|
|
771
771
|
startSetupMode(CHATCCC_PORT, {
|
|
772
|
-
onActivate: async (httpServer: Server) => {
|
|
773
|
-
reloadConfigFromDisk();
|
|
774
|
-
clearAdapterCache();
|
|
775
|
-
appendStartupTrace("setup-activate: reloaded config from disk", {
|
|
776
|
-
appIdMaskAfterReload: maskAppId(APP_ID),
|
|
777
|
-
});
|
|
772
|
+
onActivate: async (httpServer: Server) => {
|
|
773
|
+
reloadConfigFromDisk();
|
|
774
|
+
clearAdapterCache();
|
|
775
|
+
appendStartupTrace("setup-activate: reloaded config from disk", {
|
|
776
|
+
appIdMaskAfterReload: maskAppId(APP_ID),
|
|
777
|
+
});
|
|
778
778
|
try {
|
|
779
779
|
await startBotService({ httpServer, port: CHATCCC_PORT });
|
|
780
780
|
installShutdownHandlers(httpServer);
|
|
@@ -818,8 +818,8 @@ async function main(): Promise<void> {
|
|
|
818
818
|
try {
|
|
819
819
|
await startBotService({ httpServer, port: CHATCCC_PORT });
|
|
820
820
|
} catch (err) {
|
|
821
|
-
|
|
822
|
-
|
|
821
|
+
console.error(`\n[飞书] 启动失败: ${(err as Error).message}`);
|
|
822
|
+
console.error("[飞书] 微信等其他平台不受影响,将继续启动。\n");
|
|
823
823
|
}
|
|
824
824
|
}
|
|
825
825
|
|