chatccc 0.2.92 → 0.2.94
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 +148 -6
- package/src/__tests__/stream-state.test.ts +30 -0
- package/src/builtin/cli.ts +196 -196
- package/src/builtin/index.ts +166 -166
- package/src/index.ts +3 -1
- package/src/orchestrator.ts +0 -10
- package/src/session-chat-binding.ts +14 -4
- package/src/session.ts +212 -209
- package/src/stream-state.ts +20 -1
- package/src/turn-cards.ts +117 -117
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
// postinstall 检测 sharp 原生模块是否可用(常见于 Linux 缺少 libvips)
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
5
|
-
let ok = false;
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
require("sharp");
|
|
9
|
-
ok = true;
|
|
10
|
-
} catch (err) {
|
|
11
|
-
const msg = err.message;
|
|
12
|
-
const platform = process.platform;
|
|
13
|
-
|
|
14
|
-
console.warn("");
|
|
15
|
-
console.warn("============================================================");
|
|
16
|
-
console.warn(" ChatCCC: sharp 图像处理模块检测失败");
|
|
17
|
-
console.warn("============================================================");
|
|
18
|
-
|
|
19
|
-
if (platform === "linux") {
|
|
20
|
-
if (msg.includes("libvips") || msg.includes("vips") || msg.includes("libglib")) {
|
|
21
|
-
console.warn(" 原因:系统缺少 libvips 图像处理库。");
|
|
22
|
-
console.warn("");
|
|
23
|
-
console.warn(" 请先安装 libvips 然后重建 sharp:");
|
|
24
|
-
console.warn("");
|
|
25
|
-
console.warn(" Debian / Ubuntu:");
|
|
26
|
-
console.warn(" sudo apt install libvips-dev");
|
|
27
|
-
console.warn("");
|
|
28
|
-
console.warn(" CentOS / RHEL / Fedora:");
|
|
29
|
-
console.warn(" sudo yum install vips-devel");
|
|
30
|
-
console.warn("");
|
|
31
|
-
console.warn(" Alpine:");
|
|
32
|
-
console.warn(" sudo apk add vips-dev");
|
|
33
|
-
console.warn("");
|
|
34
|
-
console.warn(" 安装 libvips 后重建 sharp:");
|
|
35
|
-
console.warn(" npm install -g chatccc --force");
|
|
36
|
-
} else if (msg.includes("Cannot find module")) {
|
|
37
|
-
console.warn(" 原因:sharp 模块未成功安装。");
|
|
38
|
-
console.warn("");
|
|
39
|
-
console.warn(" 请尝试重建(可能需要先安装 libvips):");
|
|
40
|
-
console.warn(" npm install -g chatccc --force");
|
|
41
|
-
} else {
|
|
42
|
-
console.warn(" 错误详情:", msg);
|
|
43
|
-
}
|
|
44
|
-
} else {
|
|
45
|
-
console.warn(" 错误详情:", msg);
|
|
46
|
-
console.warn(" 平台:", platform);
|
|
47
|
-
console.warn(" 请尝试:npm install -g chatccc --force");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
console.warn("");
|
|
51
|
-
console.warn(" 缺少 sharp 不影响消息收发,但群聊头像状态指示");
|
|
52
|
-
console.warn(" (运行中/空闲/新建)将不会更新。");
|
|
53
|
-
console.warn("============================================================");
|
|
54
|
-
console.warn("");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!ok && process.env.npm_config_verbose) {
|
|
58
|
-
process.exitCode = 0; // 永远不阻塞安装
|
|
1
|
+
// postinstall 检测 sharp 原生模块是否可用(常见于 Linux 缺少 libvips)
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
let ok = false;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
require("sharp");
|
|
9
|
+
ok = true;
|
|
10
|
+
} catch (err) {
|
|
11
|
+
const msg = err.message;
|
|
12
|
+
const platform = process.platform;
|
|
13
|
+
|
|
14
|
+
console.warn("");
|
|
15
|
+
console.warn("============================================================");
|
|
16
|
+
console.warn(" ChatCCC: sharp 图像处理模块检测失败");
|
|
17
|
+
console.warn("============================================================");
|
|
18
|
+
|
|
19
|
+
if (platform === "linux") {
|
|
20
|
+
if (msg.includes("libvips") || msg.includes("vips") || msg.includes("libglib")) {
|
|
21
|
+
console.warn(" 原因:系统缺少 libvips 图像处理库。");
|
|
22
|
+
console.warn("");
|
|
23
|
+
console.warn(" 请先安装 libvips 然后重建 sharp:");
|
|
24
|
+
console.warn("");
|
|
25
|
+
console.warn(" Debian / Ubuntu:");
|
|
26
|
+
console.warn(" sudo apt install libvips-dev");
|
|
27
|
+
console.warn("");
|
|
28
|
+
console.warn(" CentOS / RHEL / Fedora:");
|
|
29
|
+
console.warn(" sudo yum install vips-devel");
|
|
30
|
+
console.warn("");
|
|
31
|
+
console.warn(" Alpine:");
|
|
32
|
+
console.warn(" sudo apk add vips-dev");
|
|
33
|
+
console.warn("");
|
|
34
|
+
console.warn(" 安装 libvips 后重建 sharp:");
|
|
35
|
+
console.warn(" npm install -g chatccc --force");
|
|
36
|
+
} else if (msg.includes("Cannot find module")) {
|
|
37
|
+
console.warn(" 原因:sharp 模块未成功安装。");
|
|
38
|
+
console.warn("");
|
|
39
|
+
console.warn(" 请尝试重建(可能需要先安装 libvips):");
|
|
40
|
+
console.warn(" npm install -g chatccc --force");
|
|
41
|
+
} else {
|
|
42
|
+
console.warn(" 错误详情:", msg);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
console.warn(" 错误详情:", msg);
|
|
46
|
+
console.warn(" 平台:", platform);
|
|
47
|
+
console.warn(" 请尝试:npm install -g chatccc --force");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.warn("");
|
|
51
|
+
console.warn(" 缺少 sharp 不影响消息收发,但群聊头像状态指示");
|
|
52
|
+
console.warn(" (运行中/空闲/新建)将不会更新。");
|
|
53
|
+
console.warn("============================================================");
|
|
54
|
+
console.warn("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!ok && process.env.npm_config_verbose) {
|
|
58
|
+
process.exitCode = 0; // 永远不阻塞安装
|
|
59
59
|
}
|
|
@@ -4,17 +4,63 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
|
|
6
6
|
// mock stream-state 以支持在测试中控制累积长度
|
|
7
|
-
const mockStreamStates = new Map<string, {
|
|
7
|
+
const mockStreamStates = new Map<string, {
|
|
8
|
+
accumulatedContent: string;
|
|
9
|
+
finalReply: string;
|
|
10
|
+
status?: "running" | "done" | "stopped" | "error";
|
|
11
|
+
turnCount?: number;
|
|
12
|
+
finalReplySentTurn?: number;
|
|
13
|
+
finalReplySentAt?: number;
|
|
14
|
+
}>();
|
|
8
15
|
vi.mock("../stream-state.ts", () => ({
|
|
9
16
|
readStreamState: async (sid: string) => {
|
|
10
17
|
const state = mockStreamStates.get(sid);
|
|
11
18
|
if (!state) return null;
|
|
12
|
-
return {
|
|
19
|
+
return {
|
|
20
|
+
sessionId: sid,
|
|
21
|
+
accumulatedContent: state.accumulatedContent,
|
|
22
|
+
finalReply: state.finalReply,
|
|
23
|
+
finalReplySentTurn: state.finalReplySentTurn,
|
|
24
|
+
finalReplySentAt: state.finalReplySentAt,
|
|
25
|
+
status: state.status ?? "running",
|
|
26
|
+
chunkCount: 0,
|
|
27
|
+
turnCount: state.turnCount ?? 0,
|
|
28
|
+
contextTokens: 0,
|
|
29
|
+
updatedAt: Date.now(),
|
|
30
|
+
cwd: "",
|
|
31
|
+
tool: "claude",
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
writeStreamState: async (state: {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
accumulatedContent: string;
|
|
37
|
+
finalReply: string;
|
|
38
|
+
status?: "running" | "done" | "stopped" | "error";
|
|
39
|
+
turnCount?: number;
|
|
40
|
+
finalReplySentTurn?: number;
|
|
41
|
+
finalReplySentAt?: number;
|
|
42
|
+
}) => {
|
|
43
|
+
mockStreamStates.set(state.sessionId, {
|
|
44
|
+
accumulatedContent: state.accumulatedContent,
|
|
45
|
+
finalReply: state.finalReply,
|
|
46
|
+
status: state.status,
|
|
47
|
+
turnCount: state.turnCount,
|
|
48
|
+
finalReplySentTurn: state.finalReplySentTurn,
|
|
49
|
+
finalReplySentAt: state.finalReplySentAt,
|
|
50
|
+
});
|
|
13
51
|
},
|
|
14
|
-
writeStreamState: async () => {},
|
|
15
52
|
createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
|
|
16
53
|
sessionId: sid, status: "running" as const, accumulatedContent: "", finalReply: "", chunkCount: 0, turnCount, contextTokens: 0, updatedAt: Date.now(), cwd, tool,
|
|
17
54
|
}),
|
|
55
|
+
isFinalReplySentForTurn: (state: { turnCount: number; finalReplySentTurn?: number }) => state.finalReplySentTurn === state.turnCount,
|
|
56
|
+
markFinalReplySent: async (sid: string, turnCount: number, sentAt = Date.now()) => {
|
|
57
|
+
const state = mockStreamStates.get(sid);
|
|
58
|
+
if (!state) return;
|
|
59
|
+
if ((state.turnCount ?? 0) !== turnCount) return;
|
|
60
|
+
if ((state.status ?? "running") === "running") return;
|
|
61
|
+
state.finalReplySentTurn = turnCount;
|
|
62
|
+
state.finalReplySentAt = sentAt;
|
|
63
|
+
},
|
|
18
64
|
fixStaleStreamStates: async () => {},
|
|
19
65
|
}));
|
|
20
66
|
import {
|
|
@@ -41,7 +87,9 @@ import {
|
|
|
41
87
|
setSessionPlatform,
|
|
42
88
|
recordChatPlatform,
|
|
43
89
|
_getPlatformForChatForTest,
|
|
44
|
-
|
|
90
|
+
runAgentSession,
|
|
91
|
+
startUnifiedDisplayLoop,
|
|
92
|
+
stopUnifiedDisplayLoop,
|
|
45
93
|
} from "../session.ts";
|
|
46
94
|
import {
|
|
47
95
|
activePrompts,
|
|
@@ -184,7 +232,83 @@ describe("chat platform routing", () => {
|
|
|
184
232
|
// processedMessages 全部保留——SDK 重连不应当影响后台 prompt 的执行。
|
|
185
233
|
// ---------------------------------------------------------------------------
|
|
186
234
|
|
|
187
|
-
describe("
|
|
235
|
+
describe("runAgentSession previous final delivery guard", () => {
|
|
236
|
+
let registryFile = "";
|
|
237
|
+
let toolsFile = "";
|
|
238
|
+
|
|
239
|
+
beforeEach(async () => {
|
|
240
|
+
resetState();
|
|
241
|
+
resetBindingState();
|
|
242
|
+
mockStreamStates.clear();
|
|
243
|
+
const dir = await mkdtemp(join(tmpdir(), "chatccc-final-guard-"));
|
|
244
|
+
registryFile = join(dir, "session-registry.json");
|
|
245
|
+
toolsFile = join(dir, "session-tools.json");
|
|
246
|
+
_setSessionRegistryFileForTest(registryFile);
|
|
247
|
+
_setSessionToolsFileForTest(toolsFile);
|
|
248
|
+
_setAdapterForToolForTest(
|
|
249
|
+
"claude",
|
|
250
|
+
mockAdapter((sid) => ({ sessionId: sid, cwd: "/tmp" })),
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
afterEach(() => {
|
|
255
|
+
_resetSessionRegistryFileForTest();
|
|
256
|
+
_resetSessionToolsFileForTest();
|
|
257
|
+
_clearAdapterCacheForTest();
|
|
258
|
+
resetBindingState();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("does not resend the previous terminal final when the same turn is already marked sent", async () => {
|
|
262
|
+
const platform = mockPlatform("feishu");
|
|
263
|
+
setSessionPlatform(platform);
|
|
264
|
+
bindChatToSession("sid-final", "chat-final");
|
|
265
|
+
recordLastActiveChat("sid-final", "chat-final");
|
|
266
|
+
sessionInfoMap.set("chat-final", {
|
|
267
|
+
sessionId: "sid-final",
|
|
268
|
+
turnCount: 1,
|
|
269
|
+
lastContextTokens: 0,
|
|
270
|
+
startTime: 0,
|
|
271
|
+
tool: "claude",
|
|
272
|
+
});
|
|
273
|
+
mockStreamStates.set("sid-final", {
|
|
274
|
+
accumulatedContent: "",
|
|
275
|
+
finalReply: "old final",
|
|
276
|
+
status: "done",
|
|
277
|
+
turnCount: 1,
|
|
278
|
+
finalReplySentTurn: 1,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await runAgentSession("sid-final", "next prompt", platform, "chat-final", Date.now(), "claude");
|
|
282
|
+
|
|
283
|
+
expect(platform.sendText).not.toHaveBeenCalledWith("chat-final", "old final");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("resends the previous terminal final when there is no delivery marker", async () => {
|
|
287
|
+
const platform = mockPlatform("feishu");
|
|
288
|
+
setSessionPlatform(platform);
|
|
289
|
+
bindChatToSession("sid-unsent", "chat-unsent");
|
|
290
|
+
recordLastActiveChat("sid-unsent", "chat-unsent");
|
|
291
|
+
sessionInfoMap.set("chat-unsent", {
|
|
292
|
+
sessionId: "sid-unsent",
|
|
293
|
+
turnCount: 1,
|
|
294
|
+
lastContextTokens: 0,
|
|
295
|
+
startTime: 0,
|
|
296
|
+
tool: "claude",
|
|
297
|
+
});
|
|
298
|
+
mockStreamStates.set("sid-unsent", {
|
|
299
|
+
accumulatedContent: "",
|
|
300
|
+
finalReply: "old final",
|
|
301
|
+
status: "done",
|
|
302
|
+
turnCount: 1,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await runAgentSession("sid-unsent", "next prompt", platform, "chat-unsent", Date.now(), "claude");
|
|
306
|
+
|
|
307
|
+
expect(platform.sendText).toHaveBeenCalledWith("chat-unsent", "old final");
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("unified display loop WeChat delta", () => {
|
|
188
312
|
beforeEach(() => {
|
|
189
313
|
vi.useFakeTimers();
|
|
190
314
|
resetState();
|
|
@@ -193,6 +317,7 @@ describe("ensureDisplayLoop WeChat delta", () => {
|
|
|
193
317
|
});
|
|
194
318
|
|
|
195
319
|
afterEach(() => {
|
|
320
|
+
stopUnifiedDisplayLoop();
|
|
196
321
|
resetBindingState();
|
|
197
322
|
vi.useRealTimers();
|
|
198
323
|
});
|
|
@@ -212,16 +337,31 @@ describe("ensureDisplayLoop WeChat delta", () => {
|
|
|
212
337
|
tool: "claude",
|
|
213
338
|
});
|
|
214
339
|
|
|
340
|
+
// 模拟 runAgentSession 创建的 WeChat display 条目
|
|
341
|
+
displayCards.set("chat-wechat", {
|
|
342
|
+
cardId: "",
|
|
343
|
+
sequence: 0,
|
|
344
|
+
cardBusy: false,
|
|
345
|
+
cardCreatedAt: Date.now(),
|
|
346
|
+
lastSentContent: "",
|
|
347
|
+
streamErrorNotified: false,
|
|
348
|
+
sessionId: "sid-wechat",
|
|
349
|
+
turnCount: 1,
|
|
350
|
+
dotCount: 0,
|
|
351
|
+
});
|
|
352
|
+
|
|
215
353
|
mockStreamStates.set("sid-wechat", {
|
|
216
354
|
accumulatedContent: "",
|
|
217
355
|
finalReply: "partial reply",
|
|
356
|
+
status: "running",
|
|
218
357
|
});
|
|
219
|
-
|
|
358
|
+
startUnifiedDisplayLoop();
|
|
220
359
|
await vi.advanceTimersByTimeAsync(3000);
|
|
221
360
|
|
|
222
361
|
mockStreamStates.set("sid-wechat", {
|
|
223
362
|
accumulatedContent: "tool output\n",
|
|
224
363
|
finalReply: "partial reply",
|
|
364
|
+
status: "running",
|
|
225
365
|
});
|
|
226
366
|
await vi.advanceTimersByTimeAsync(3000);
|
|
227
367
|
|
|
@@ -987,6 +1127,7 @@ describe("switchChatBinding", () => {
|
|
|
987
1127
|
displayCards.set("chat-1", {
|
|
988
1128
|
cardId: "c1", sequence: 1, cardBusy: false,
|
|
989
1129
|
cardCreatedAt: 0, lastSentContent: "", streamErrorNotified: false,
|
|
1130
|
+
sessionId: "old-sid", turnCount: 5, dotCount: 0,
|
|
990
1131
|
});
|
|
991
1132
|
|
|
992
1133
|
const result = await switchChatBinding({
|
|
@@ -1051,6 +1192,7 @@ describe("switchChatBinding", () => {
|
|
|
1051
1192
|
const oldDisplay = {
|
|
1052
1193
|
cardId: "c1", sequence: 1, cardBusy: false,
|
|
1053
1194
|
cardCreatedAt: 0, lastSentContent: "", streamErrorNotified: false,
|
|
1195
|
+
sessionId: "old-sid", turnCount: 5, dotCount: 0,
|
|
1054
1196
|
};
|
|
1055
1197
|
displayCards.set("chat-1", oldDisplay);
|
|
1056
1198
|
|
|
@@ -18,6 +18,8 @@ const {
|
|
|
18
18
|
writeStreamState,
|
|
19
19
|
readStreamState,
|
|
20
20
|
createEmptyStreamState,
|
|
21
|
+
isFinalReplySentForTurn,
|
|
22
|
+
markFinalReplySent,
|
|
21
23
|
STREAMS_DIR,
|
|
22
24
|
_setRenameForTest,
|
|
23
25
|
_resetRenameForTest,
|
|
@@ -117,6 +119,33 @@ describe("writeStreamState — atomic rename", () => {
|
|
|
117
119
|
const got = await readStreamState("never-existed");
|
|
118
120
|
expect(got).toBeNull();
|
|
119
121
|
});
|
|
122
|
+
it("marks final reply delivery for the matching terminal turn only", async () => {
|
|
123
|
+
const state = createEmptyStreamState("sid-delivered", "/tmp", "claude", 2);
|
|
124
|
+
state.status = "done";
|
|
125
|
+
state.finalReply = "done";
|
|
126
|
+
await writeStreamState(state);
|
|
127
|
+
|
|
128
|
+
await markFinalReplySent("sid-delivered", 2, 12345);
|
|
129
|
+
|
|
130
|
+
const got = await readStreamState("sid-delivered");
|
|
131
|
+
expect(got).not.toBeNull();
|
|
132
|
+
expect(got!.finalReplySentTurn).toBe(2);
|
|
133
|
+
expect(got!.finalReplySentAt).toBe(12345);
|
|
134
|
+
expect(isFinalReplySentForTurn(got!)).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not mark a running state or a different turn as delivered", async () => {
|
|
138
|
+
const state = createEmptyStreamState("sid-running", "/tmp", "claude", 3);
|
|
139
|
+
await writeStreamState(state);
|
|
140
|
+
|
|
141
|
+
await markFinalReplySent("sid-running", 2, 12345);
|
|
142
|
+
await markFinalReplySent("sid-running", 3, 12345);
|
|
143
|
+
|
|
144
|
+
const got = await readStreamState("sid-running");
|
|
145
|
+
expect(got).not.toBeNull();
|
|
146
|
+
expect(got!.finalReplySentTurn).toBeUndefined();
|
|
147
|
+
expect(isFinalReplySentForTurn(got!)).toBe(false);
|
|
148
|
+
});
|
|
120
149
|
});
|
|
121
150
|
|
|
122
151
|
describe("createEmptyStreamState", () => {
|
|
@@ -129,6 +158,7 @@ describe("createEmptyStreamState", () => {
|
|
|
129
158
|
expect(s.status).toBe("running");
|
|
130
159
|
expect(s.accumulatedContent).toBe("");
|
|
131
160
|
expect(s.finalReply).toBe("");
|
|
161
|
+
expect(s.finalReplySentTurn).toBeUndefined();
|
|
132
162
|
expect(s.chunkCount).toBe(0);
|
|
133
163
|
});
|
|
134
164
|
});
|