chatccc 0.2.93 → 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 +128 -3
- 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/session.ts +73 -20
- 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,6 +87,7 @@ import {
|
|
|
41
87
|
setSessionPlatform,
|
|
42
88
|
recordChatPlatform,
|
|
43
89
|
_getPlatformForChatForTest,
|
|
90
|
+
runAgentSession,
|
|
44
91
|
startUnifiedDisplayLoop,
|
|
45
92
|
stopUnifiedDisplayLoop,
|
|
46
93
|
} from "../session.ts";
|
|
@@ -185,6 +232,82 @@ describe("chat platform routing", () => {
|
|
|
185
232
|
// processedMessages 全部保留——SDK 重连不应当影响后台 prompt 的执行。
|
|
186
233
|
// ---------------------------------------------------------------------------
|
|
187
234
|
|
|
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
|
+
|
|
188
311
|
describe("unified display loop WeChat delta", () => {
|
|
189
312
|
beforeEach(() => {
|
|
190
313
|
vi.useFakeTimers();
|
|
@@ -1004,6 +1127,7 @@ describe("switchChatBinding", () => {
|
|
|
1004
1127
|
displayCards.set("chat-1", {
|
|
1005
1128
|
cardId: "c1", sequence: 1, cardBusy: false,
|
|
1006
1129
|
cardCreatedAt: 0, lastSentContent: "", streamErrorNotified: false,
|
|
1130
|
+
sessionId: "old-sid", turnCount: 5, dotCount: 0,
|
|
1007
1131
|
});
|
|
1008
1132
|
|
|
1009
1133
|
const result = await switchChatBinding({
|
|
@@ -1068,6 +1192,7 @@ describe("switchChatBinding", () => {
|
|
|
1068
1192
|
const oldDisplay = {
|
|
1069
1193
|
cardId: "c1", sequence: 1, cardBusy: false,
|
|
1070
1194
|
cardCreatedAt: 0, lastSentContent: "", streamErrorNotified: false,
|
|
1195
|
+
sessionId: "old-sid", turnCount: 5, dotCount: 0,
|
|
1071
1196
|
};
|
|
1072
1197
|
displayCards.set("chat-1", oldDisplay);
|
|
1073
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
|
});
|