chatccc 0.2.52 → 0.2.54
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 +95 -256
- package/bin/chatccc.mjs +23 -23
- package/demo/ilink_echo_probe.ts +222 -222
- package/im-skills/feishu-skill/download-video.mjs +162 -162
- package/im-skills/feishu-skill/receive-send-file.md +66 -66
- package/im-skills/feishu-skill/receive-send-image.md +27 -27
- package/im-skills/feishu-skill/send-file.mjs +50 -50
- package/im-skills/feishu-skill/send-image.mjs +50 -50
- package/im-skills/feishu-skill/skill.md +10 -10
- package/package.json +59 -59
- package/src/__tests__/agent-image-rpc.test.ts +33 -33
- package/src/__tests__/agent-rpc-body.test.ts +41 -41
- package/src/__tests__/card-plain-text.test.ts +42 -42
- package/src/__tests__/codex-adapter.test.ts +304 -304
- package/src/__tests__/config-reload.test.ts +26 -26
- package/src/__tests__/config-sample.test.ts +19 -19
- package/src/__tests__/crash-logging.test.ts +62 -62
- package/src/__tests__/fixtures/codex_simple_text.jsonl +4 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +6 -6
- package/src/__tests__/im-skills.test.ts +46 -46
- package/src/__tests__/privacy.test.ts +143 -0
- package/src/__tests__/session.test.ts +57 -2
- package/src/__tests__/sim-agent.test.ts +173 -173
- package/src/__tests__/sim-platform.test.ts +76 -76
- package/src/__tests__/sim-store.test.ts +213 -213
- package/src/__tests__/stream-state.test.ts +134 -134
- package/src/__tests__/wechat-platform.test.ts +57 -32
- package/src/adapters/codex-adapter.ts +294 -294
- package/src/adapters/codex-session-meta-store.ts +130 -130
- package/src/agent-file-rpc.ts +156 -156
- package/src/agent-image-rpc.ts +152 -152
- package/src/agent-rpc-body.ts +48 -48
- package/src/card-plain-text.ts +108 -108
- package/src/feishu-api.ts +7 -3
- package/src/im-skills.ts +109 -109
- package/src/platform-adapter.ts +60 -60
- package/src/privacy.ts +68 -0
- package/src/session-chat-binding.ts +6 -1
- package/src/session.ts +40 -35
- package/src/sim-agent.ts +167 -167
- package/src/trace.ts +50 -50
- package/src/wechat-platform.ts +4 -1
|
@@ -12,14 +12,14 @@ import {
|
|
|
12
12
|
CLAUDE_BASE_URL,
|
|
13
13
|
CLAUDE_EFFORT,
|
|
14
14
|
CLAUDE_MODEL,
|
|
15
|
-
CURSOR_AGENT_ARGS,
|
|
16
|
-
CURSOR_AGENT_COMMAND,
|
|
17
|
-
FEISHU_ENABLED,
|
|
18
|
-
GIT_TIMEOUT_MS,
|
|
19
|
-
GIT_TIMEOUT_SECONDS,
|
|
20
|
-
ILINK_ENABLED,
|
|
21
|
-
applyLoadedConfig,
|
|
22
|
-
config,
|
|
15
|
+
CURSOR_AGENT_ARGS,
|
|
16
|
+
CURSOR_AGENT_COMMAND,
|
|
17
|
+
FEISHU_ENABLED,
|
|
18
|
+
GIT_TIMEOUT_MS,
|
|
19
|
+
GIT_TIMEOUT_SECONDS,
|
|
20
|
+
ILINK_ENABLED,
|
|
21
|
+
applyLoadedConfig,
|
|
22
|
+
config,
|
|
23
23
|
resolveDefaultAgentTool,
|
|
24
24
|
type AppConfig,
|
|
25
25
|
} from "../config.ts";
|
|
@@ -35,9 +35,9 @@ import {
|
|
|
35
35
|
// 原地切换复用同一 server,重新读端口只会引入混乱)
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
37
|
|
|
38
|
-
const baseAppConfig: AppConfig = {
|
|
39
|
-
feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
|
|
40
|
-
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
38
|
+
const baseAppConfig: AppConfig = {
|
|
39
|
+
feishu: { appId: "INITIAL_APP", appSecret: "INITIAL_SECRET" },
|
|
40
|
+
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
41
41
|
port: 18080,
|
|
42
42
|
gitTimeoutSeconds: 180,
|
|
43
43
|
allowInterrupt: false,
|
|
@@ -97,7 +97,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
97
97
|
expect(CLAUDE_BASE_URL).toBe("https://gw2.example/anthropic");
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
it("更新 GIT_TIMEOUT_SECONDS 与 GIT_TIMEOUT_MS(毫秒派生值同步刷新)", () => {
|
|
100
|
+
it("更新 GIT_TIMEOUT_SECONDS 与 GIT_TIMEOUT_MS(毫秒派生值同步刷新)", () => {
|
|
101
101
|
applyLoadedConfig({
|
|
102
102
|
...structuredClone(baseAppConfig),
|
|
103
103
|
gitTimeoutSeconds: 240,
|
|
@@ -106,20 +106,20 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
|
|
|
106
106
|
expect(GIT_TIMEOUT_SECONDS).toBe(240);
|
|
107
107
|
// 派生值必须跟着更新,否则 /git 仍然按旧 timeout 运行
|
|
108
108
|
expect(GIT_TIMEOUT_MS).toBe(240 * 1000);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("更新平台开关(默认飞书和微信都开启)", () => {
|
|
112
|
-
expect(FEISHU_ENABLED).toBe(true);
|
|
113
|
-
expect(ILINK_ENABLED).toBe(true);
|
|
114
|
-
|
|
115
|
-
applyLoadedConfig({
|
|
116
|
-
...structuredClone(baseAppConfig),
|
|
117
|
-
platforms: { feishu: { enabled: false }, ilink: { enabled: true } },
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
expect(FEISHU_ENABLED).toBe(false);
|
|
121
|
-
expect(ILINK_ENABLED).toBe(true);
|
|
122
|
-
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("更新平台开关(默认飞书和微信都开启)", () => {
|
|
112
|
+
expect(FEISHU_ENABLED).toBe(true);
|
|
113
|
+
expect(ILINK_ENABLED).toBe(true);
|
|
114
|
+
|
|
115
|
+
applyLoadedConfig({
|
|
116
|
+
...structuredClone(baseAppConfig),
|
|
117
|
+
platforms: { feishu: { enabled: false }, ilink: { enabled: true } },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(FEISHU_ENABLED).toBe(false);
|
|
121
|
+
expect(ILINK_ENABLED).toBe(true);
|
|
122
|
+
});
|
|
123
123
|
|
|
124
124
|
it("CURSOR_AGENT_ARGS 跟随 cursor.model 重新解析", () => {
|
|
125
125
|
applyLoadedConfig({
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
|
|
6
|
-
describe("config.sample.json", () => {
|
|
7
|
-
it("enables Feishu and WeChat iLink by default", () => {
|
|
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
|
-
};
|
|
15
|
-
|
|
16
|
-
expect(sample.platforms?.feishu?.enabled).toBe(true);
|
|
17
|
-
expect(sample.platforms?.ilink?.enabled).toBe(true);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
describe("config.sample.json", () => {
|
|
7
|
+
it("enables Feishu and WeChat iLink by default", () => {
|
|
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
|
+
};
|
|
15
|
+
|
|
16
|
+
expect(sample.platforms?.feishu?.enabled).toBe(true);
|
|
17
|
+
expect(sample.platforms?.ilink?.enabled).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { describe, it, expect, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
import { buildCrashLoggingHandlers, installCrashLogging, setupFileLogging } from "../shared.ts";
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, it, expect, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { buildCrashLoggingHandlers, installCrashLogging, setupFileLogging } from "../shared.ts";
|
|
7
7
|
|
|
8
8
|
describe("buildCrashLoggingHandlers", () => {
|
|
9
9
|
it("uncaughtException: 写诊断、刷新日志、调用 onFatal", () => {
|
|
@@ -144,7 +144,7 @@ describe("buildCrashLoggingHandlers", () => {
|
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
describe("installCrashLogging", () => {
|
|
147
|
+
describe("installCrashLogging", () => {
|
|
148
148
|
it("注册所有相关事件监听器", () => {
|
|
149
149
|
const before = {
|
|
150
150
|
uncaught: process.listenerCount("uncaughtException"),
|
|
@@ -209,58 +209,58 @@ describe("installCrashLogging", () => {
|
|
|
209
209
|
} finally {
|
|
210
210
|
cleanup();
|
|
211
211
|
}
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
describe("setupFileLogging", () => {
|
|
216
|
-
it("flush 后继续写日志不会触发 write after end,且日志已落盘", async () => {
|
|
217
|
-
const originalLog = console.log;
|
|
218
|
-
const originalError = console.error;
|
|
219
|
-
console.log = vi.fn() as never;
|
|
220
|
-
console.error = vi.fn() as never;
|
|
221
|
-
const dir = await mkdtemp(join(tmpdir(), "chatccc-log-"));
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
const fileLog = setupFileLogging(dir, "index");
|
|
225
|
-
|
|
226
|
-
console.log("before flush");
|
|
227
|
-
fileLog.flush();
|
|
228
|
-
|
|
229
|
-
expect(() => console.error("after flush")).not.toThrow();
|
|
230
|
-
fileLog.flush();
|
|
231
|
-
|
|
232
|
-
const content = await readFile(fileLog.logPath, "utf8");
|
|
233
|
-
expect(content).toContain("[LOG] before flush");
|
|
234
|
-
expect(content).toContain("[ERR] after flush");
|
|
235
|
-
} finally {
|
|
236
|
-
console.log = originalLog;
|
|
237
|
-
console.error = originalError;
|
|
238
|
-
await rm(dir, { recursive: true, force: true });
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("日志参数无法 JSON 序列化时也不会让 console 调用抛错", async () => {
|
|
243
|
-
const originalLog = console.log;
|
|
244
|
-
const originalError = console.error;
|
|
245
|
-
console.log = vi.fn() as never;
|
|
246
|
-
console.error = vi.fn() as never;
|
|
247
|
-
const dir = await mkdtemp(join(tmpdir(), "chatccc-log-"));
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
const fileLog = setupFileLogging(dir, "index");
|
|
251
|
-
const circular: Record<string, unknown> = { name: "root" };
|
|
252
|
-
circular.self = circular;
|
|
253
|
-
|
|
254
|
-
expect(() => console.log("circular", circular)).not.toThrow();
|
|
255
|
-
fileLog.flush();
|
|
256
|
-
|
|
257
|
-
const content = await readFile(fileLog.logPath, "utf8");
|
|
258
|
-
expect(content).toContain("[LOG] circular");
|
|
259
|
-
expect(content).toContain("self");
|
|
260
|
-
} finally {
|
|
261
|
-
console.log = originalLog;
|
|
262
|
-
console.error = originalError;
|
|
263
|
-
await rm(dir, { recursive: true, force: true });
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("setupFileLogging", () => {
|
|
216
|
+
it("flush 后继续写日志不会触发 write after end,且日志已落盘", async () => {
|
|
217
|
+
const originalLog = console.log;
|
|
218
|
+
const originalError = console.error;
|
|
219
|
+
console.log = vi.fn() as never;
|
|
220
|
+
console.error = vi.fn() as never;
|
|
221
|
+
const dir = await mkdtemp(join(tmpdir(), "chatccc-log-"));
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const fileLog = setupFileLogging(dir, "index");
|
|
225
|
+
|
|
226
|
+
console.log("before flush");
|
|
227
|
+
fileLog.flush();
|
|
228
|
+
|
|
229
|
+
expect(() => console.error("after flush")).not.toThrow();
|
|
230
|
+
fileLog.flush();
|
|
231
|
+
|
|
232
|
+
const content = await readFile(fileLog.logPath, "utf8");
|
|
233
|
+
expect(content).toContain("[LOG] before flush");
|
|
234
|
+
expect(content).toContain("[ERR] after flush");
|
|
235
|
+
} finally {
|
|
236
|
+
console.log = originalLog;
|
|
237
|
+
console.error = originalError;
|
|
238
|
+
await rm(dir, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("日志参数无法 JSON 序列化时也不会让 console 调用抛错", async () => {
|
|
243
|
+
const originalLog = console.log;
|
|
244
|
+
const originalError = console.error;
|
|
245
|
+
console.log = vi.fn() as never;
|
|
246
|
+
console.error = vi.fn() as never;
|
|
247
|
+
const dir = await mkdtemp(join(tmpdir(), "chatccc-log-"));
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const fileLog = setupFileLogging(dir, "index");
|
|
251
|
+
const circular: Record<string, unknown> = { name: "root" };
|
|
252
|
+
circular.self = circular;
|
|
253
|
+
|
|
254
|
+
expect(() => console.log("circular", circular)).not.toThrow();
|
|
255
|
+
fileLog.flush();
|
|
256
|
+
|
|
257
|
+
const content = await readFile(fileLog.logPath, "utf8");
|
|
258
|
+
expect(content).toContain("[LOG] circular");
|
|
259
|
+
expect(content).toContain("self");
|
|
260
|
+
} finally {
|
|
261
|
+
console.log = originalLog;
|
|
262
|
+
console.error = originalError;
|
|
263
|
+
await rm(dir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{"type":"thread.started","thread_id":"019e15c7-t001-7901-b92c-9a4371e07750"}
|
|
2
|
-
{"type":"turn.started"}
|
|
3
|
-
{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"hello"}}
|
|
4
|
-
{"type":"turn.completed","usage":{"input_tokens":12964,"cached_input_tokens":11136,"output_tokens":5,"reasoning_output_tokens":0}}
|
|
1
|
+
{"type":"thread.started","thread_id":"019e15c7-t001-7901-b92c-9a4371e07750"}
|
|
2
|
+
{"type":"turn.started"}
|
|
3
|
+
{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"hello"}}
|
|
4
|
+
{"type":"turn.completed","usage":{"input_tokens":12964,"cached_input_tokens":11136,"output_tokens":5,"reasoning_output_tokens":0}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{"type":"thread.started","thread_id":"019e15c8-t002-7441-9bfb-eec7b1ef4c4c"}
|
|
2
|
-
{"type":"turn.started"}
|
|
3
|
-
{"type":"item.started","item":{"id":"item_0","type":"command_execution","command":"powershell.exe -Command echo tool_test","aggregated_output":"","exit_code":null,"status":"in_progress"}}
|
|
4
|
-
{"type":"item.completed","item":{"id":"item_0","type":"command_execution","command":"powershell.exe -Command echo tool_test","aggregated_output":"tool_test\r\n","exit_code":0,"status":"completed"}}
|
|
5
|
-
{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"tool_test"}}
|
|
6
|
-
{"type":"turn.completed","usage":{"input_tokens":26021,"cached_input_tokens":22272,"output_tokens":52,"reasoning_output_tokens":0}}
|
|
1
|
+
{"type":"thread.started","thread_id":"019e15c8-t002-7441-9bfb-eec7b1ef4c4c"}
|
|
2
|
+
{"type":"turn.started"}
|
|
3
|
+
{"type":"item.started","item":{"id":"item_0","type":"command_execution","command":"powershell.exe -Command echo tool_test","aggregated_output":"","exit_code":null,"status":"in_progress"}}
|
|
4
|
+
{"type":"item.completed","item":{"id":"item_0","type":"command_execution","command":"powershell.exe -Command echo tool_test","aggregated_output":"tool_test\r\n","exit_code":0,"status":"completed"}}
|
|
5
|
+
{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"tool_test"}}
|
|
6
|
+
{"type":"turn.completed","usage":{"input_tokens":26021,"cached_input_tokens":22272,"output_tokens":52,"reasoning_output_tokens":0}}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
|
|
6
|
-
import { buildImSkillsPrompt } from "../im-skills.ts";
|
|
7
|
-
|
|
8
|
-
let tempRoot: string | null = null;
|
|
9
|
-
|
|
10
|
-
describe("IM skills prompt rendering", () => {
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
if (tempRoot) await rm(tempRoot, { recursive: true, force: true });
|
|
13
|
-
tempRoot = null;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("loads skill.md files and renders runtime variables", async () => {
|
|
17
|
-
tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-"));
|
|
18
|
-
const skillDir = join(tempRoot, "feishu-skill");
|
|
19
|
-
await mkdir(skillDir);
|
|
20
|
-
await writeFile(
|
|
21
|
-
join(skillDir, "skill.md"),
|
|
22
|
-
[
|
|
23
|
-
"---",
|
|
24
|
-
"name: feishu-skill",
|
|
25
|
-
"---",
|
|
26
|
-
"GET {{session_grants_url}}?sid={{session_id}}",
|
|
27
|
-
"cwd={{cwd}}",
|
|
28
|
-
].join("\n"),
|
|
29
|
-
"utf-8",
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const prompt = await buildImSkillsPrompt({
|
|
33
|
-
skillsDir: tempRoot,
|
|
34
|
-
variables: {
|
|
35
|
-
cwd: "C:/work",
|
|
36
|
-
session_grants_url: "http://127.0.0.1:18080/api/agent/session-grants",
|
|
37
|
-
session_id: "sid_test",
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
expect(prompt).toContain("[ChatCCC IM skill: feishu-skill]");
|
|
42
|
-
expect(prompt).toContain("GET http://127.0.0.1:18080/api/agent/session-grants?sid=sid_test");
|
|
43
|
-
expect(prompt).toContain("cwd=C:/work");
|
|
44
|
-
expect(prompt).not.toContain("{{");
|
|
45
|
-
});
|
|
46
|
-
});
|
|
1
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { buildImSkillsPrompt } from "../im-skills.ts";
|
|
7
|
+
|
|
8
|
+
let tempRoot: string | null = null;
|
|
9
|
+
|
|
10
|
+
describe("IM skills prompt rendering", () => {
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
if (tempRoot) await rm(tempRoot, { recursive: true, force: true });
|
|
13
|
+
tempRoot = null;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("loads skill.md files and renders runtime variables", async () => {
|
|
17
|
+
tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-"));
|
|
18
|
+
const skillDir = join(tempRoot, "feishu-skill");
|
|
19
|
+
await mkdir(skillDir);
|
|
20
|
+
await writeFile(
|
|
21
|
+
join(skillDir, "skill.md"),
|
|
22
|
+
[
|
|
23
|
+
"---",
|
|
24
|
+
"name: feishu-skill",
|
|
25
|
+
"---",
|
|
26
|
+
"GET {{session_grants_url}}?sid={{session_id}}",
|
|
27
|
+
"cwd={{cwd}}",
|
|
28
|
+
].join("\n"),
|
|
29
|
+
"utf-8",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const prompt = await buildImSkillsPrompt({
|
|
33
|
+
skillsDir: tempRoot,
|
|
34
|
+
variables: {
|
|
35
|
+
cwd: "C:/work",
|
|
36
|
+
session_grants_url: "http://127.0.0.1:18080/api/agent/session-grants",
|
|
37
|
+
session_id: "sid_test",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(prompt).toContain("[ChatCCC IM skill: feishu-skill]");
|
|
42
|
+
expect(prompt).toContain("GET http://127.0.0.1:18080/api/agent/session-grants?sid=sid_test");
|
|
43
|
+
expect(prompt).toContain("cwd=C:/work");
|
|
44
|
+
expect(prompt).not.toContain("{{");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// 在 import privacy 之前 mock config,让 USER_DATA_DIR 指向临时目录
|
|
7
|
+
const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
|
|
8
|
+
vi.mock("../config.ts", async () => {
|
|
9
|
+
const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
USER_DATA_DIR: TEST_DATA_DIR,
|
|
13
|
+
ts: () => "test-ts",
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let applyPrivacy: (text: string) => string;
|
|
18
|
+
let reloadPrivacyRules: () => void;
|
|
19
|
+
let getPrivacyRules: () => Record<string, string>;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
// 清理临时目录中的 privacy.json
|
|
24
|
+
try {
|
|
25
|
+
await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
|
|
26
|
+
} catch {}
|
|
27
|
+
const mod = await import("../privacy.ts");
|
|
28
|
+
applyPrivacy = mod.applyPrivacy;
|
|
29
|
+
reloadPrivacyRules = mod.reloadPrivacyRules;
|
|
30
|
+
getPrivacyRules = mod.getPrivacyRules;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
try {
|
|
35
|
+
await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
|
|
36
|
+
} catch {}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterAll(async () => {
|
|
40
|
+
try {
|
|
41
|
+
await rm(TEST_DATA_DIR, { recursive: true, force: true });
|
|
42
|
+
} catch {}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("applyPrivacy", () => {
|
|
46
|
+
it("无 privacy.json 时返回原文", () => {
|
|
47
|
+
reloadPrivacyRules();
|
|
48
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("privacy.json 存在时按规则替换", async () => {
|
|
52
|
+
await writeFile(
|
|
53
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
54
|
+
JSON.stringify({ weizhangjian: "wzj", secret: "***" }),
|
|
55
|
+
"utf-8",
|
|
56
|
+
);
|
|
57
|
+
reloadPrivacyRules();
|
|
58
|
+
|
|
59
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
|
|
60
|
+
expect(applyPrivacy("my secret is safe")).toBe("my *** is safe");
|
|
61
|
+
expect(applyPrivacy("weizhangjian and secret")).toBe("wzj and ***");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("多规则替换多次出现", async () => {
|
|
65
|
+
await writeFile(
|
|
66
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
67
|
+
JSON.stringify({ a: "A", b: "B" }),
|
|
68
|
+
"utf-8",
|
|
69
|
+
);
|
|
70
|
+
reloadPrivacyRules();
|
|
71
|
+
|
|
72
|
+
expect(applyPrivacy("a b a b")).toBe("A B A B");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("空文本直接返回", async () => {
|
|
76
|
+
await writeFile(
|
|
77
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
78
|
+
JSON.stringify({ x: "y" }),
|
|
79
|
+
"utf-8",
|
|
80
|
+
);
|
|
81
|
+
reloadPrivacyRules();
|
|
82
|
+
|
|
83
|
+
expect(applyPrivacy("")).toBe("");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("规则中的特殊字符不会被当作正则", async () => {
|
|
87
|
+
await writeFile(
|
|
88
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
89
|
+
JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
|
|
90
|
+
"utf-8",
|
|
91
|
+
);
|
|
92
|
+
reloadPrivacyRules();
|
|
93
|
+
|
|
94
|
+
expect(applyPrivacy("hello a.b world")).toBe("hello X world");
|
|
95
|
+
expect(applyPrivacy("text (test) here")).toBe("text Y here");
|
|
96
|
+
expect(applyPrivacy("a *star shines")).toBe("a Z shines");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("reloadPrivacyRules 强制重新加载", async () => {
|
|
100
|
+
await writeFile(
|
|
101
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
102
|
+
JSON.stringify({ old: "OLD" }),
|
|
103
|
+
"utf-8",
|
|
104
|
+
);
|
|
105
|
+
reloadPrivacyRules();
|
|
106
|
+
|
|
107
|
+
expect(applyPrivacy("old")).toBe("OLD");
|
|
108
|
+
expect(getPrivacyRules()).toEqual({ old: "OLD" });
|
|
109
|
+
|
|
110
|
+
// 变更磁盘内容后 reload
|
|
111
|
+
await writeFile(
|
|
112
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
113
|
+
JSON.stringify({ new: "NEW" }),
|
|
114
|
+
"utf-8",
|
|
115
|
+
);
|
|
116
|
+
reloadPrivacyRules();
|
|
117
|
+
|
|
118
|
+
expect(applyPrivacy("new")).toBe("NEW");
|
|
119
|
+
expect(getPrivacyRules()).toEqual({ new: "NEW" });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("格式错误的 JSON 不抛异常,返回原文", async () => {
|
|
123
|
+
await writeFile(
|
|
124
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
125
|
+
"not json",
|
|
126
|
+
"utf-8",
|
|
127
|
+
);
|
|
128
|
+
reloadPrivacyRules();
|
|
129
|
+
|
|
130
|
+
expect(applyPrivacy("hello")).toBe("hello");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("数组格式的 JSON 不抛异常,返回原文", async () => {
|
|
134
|
+
await writeFile(
|
|
135
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
136
|
+
JSON.stringify(["a", "b"]),
|
|
137
|
+
"utf-8",
|
|
138
|
+
);
|
|
139
|
+
reloadPrivacyRules();
|
|
140
|
+
|
|
141
|
+
expect(applyPrivacy("hello")).toBe("hello");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -4,12 +4,12 @@ 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, { accumulatedContent: string; finalReply: string }>();
|
|
7
|
+
const mockStreamStates = new Map<string, { accumulatedContent: string; finalReply: string; status?: "running" | "done" | "stopped" }>();
|
|
8
8
|
vi.mock("../stream-state.ts", () => ({
|
|
9
9
|
readStreamState: async (sid: string) => {
|
|
10
10
|
const state = mockStreamStates.get(sid);
|
|
11
11
|
if (!state) return null;
|
|
12
|
-
return { sessionId: sid, accumulatedContent: state.accumulatedContent, finalReply: state.finalReply, status: "running", chunkCount: 0, turnCount: 0, contextTokens: 0, updatedAt: Date.now(), cwd: "", tool: "claude" };
|
|
12
|
+
return { sessionId: sid, accumulatedContent: state.accumulatedContent, finalReply: state.finalReply, status: state.status ?? "running", chunkCount: 0, turnCount: 0, contextTokens: 0, updatedAt: Date.now(), cwd: "", tool: "claude" };
|
|
13
13
|
},
|
|
14
14
|
writeStreamState: async () => {},
|
|
15
15
|
createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
setSessionPlatform,
|
|
42
42
|
recordChatPlatform,
|
|
43
43
|
_getPlatformForChatForTest,
|
|
44
|
+
ensureDisplayLoop,
|
|
44
45
|
} from "../session.ts";
|
|
45
46
|
import {
|
|
46
47
|
activePrompts,
|
|
@@ -183,6 +184,60 @@ describe("chat platform routing", () => {
|
|
|
183
184
|
// processedMessages 全部保留——SDK 重连不应当影响后台 prompt 的执行。
|
|
184
185
|
// ---------------------------------------------------------------------------
|
|
185
186
|
|
|
187
|
+
describe("ensureDisplayLoop WeChat delta", () => {
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
vi.useFakeTimers();
|
|
190
|
+
resetState();
|
|
191
|
+
resetBindingState();
|
|
192
|
+
mockStreamStates.clear();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
afterEach(() => {
|
|
196
|
+
resetBindingState();
|
|
197
|
+
vi.useRealTimers();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("sends only new accumulated content when tool output arrives before an already-sent final reply", async () => {
|
|
201
|
+
const platform = mockPlatform("wechat");
|
|
202
|
+
platform.kind = "wechat";
|
|
203
|
+
setSessionPlatform(platform);
|
|
204
|
+
|
|
205
|
+
bindChatToSession("sid-wechat", "chat-wechat");
|
|
206
|
+
recordLastActiveChat("sid-wechat", "chat-wechat");
|
|
207
|
+
sessionInfoMap.set("chat-wechat", {
|
|
208
|
+
sessionId: "sid-wechat",
|
|
209
|
+
turnCount: 1,
|
|
210
|
+
lastContextTokens: 0,
|
|
211
|
+
startTime: 0,
|
|
212
|
+
tool: "claude",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
mockStreamStates.set("sid-wechat", {
|
|
216
|
+
accumulatedContent: "",
|
|
217
|
+
finalReply: "partial reply",
|
|
218
|
+
});
|
|
219
|
+
ensureDisplayLoop("sid-wechat");
|
|
220
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
221
|
+
|
|
222
|
+
mockStreamStates.set("sid-wechat", {
|
|
223
|
+
accumulatedContent: "tool output\n",
|
|
224
|
+
finalReply: "partial reply",
|
|
225
|
+
});
|
|
226
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
227
|
+
|
|
228
|
+
expect(platform.sendText).toHaveBeenNthCalledWith(
|
|
229
|
+
1,
|
|
230
|
+
"chat-wechat",
|
|
231
|
+
"partial reply",
|
|
232
|
+
);
|
|
233
|
+
expect(platform.sendText).toHaveBeenNthCalledWith(
|
|
234
|
+
2,
|
|
235
|
+
"chat-wechat",
|
|
236
|
+
"tool output",
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
186
241
|
describe("rebuildBindingsFromRegistry", () => {
|
|
187
242
|
let registryFile = "";
|
|
188
243
|
|