chatccc 0.2.240 → 0.2.242
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/bin/cccagent.mjs +1 -1
- package/deepccc-agent/LICENSE +201 -0
- package/deepccc-agent/README.md +291 -0
- package/deepccc-agent/bin/deepccc.mjs +26 -0
- package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
- package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
- package/deepccc-agent/package-lock.json +1959 -0
- package/deepccc-agent/package.json +62 -0
- package/deepccc-agent/src/__tests__/chat-session.test.ts +522 -0
- package/deepccc-agent/src/__tests__/cli-json.test.ts +49 -0
- package/deepccc-agent/src/__tests__/config.test.ts +26 -0
- package/deepccc-agent/src/__tests__/context.test.ts +319 -0
- package/deepccc-agent/src/__tests__/file-tools.test.ts +240 -0
- package/deepccc-agent/src/__tests__/permissions.test.ts +195 -0
- package/deepccc-agent/src/__tests__/privacy.test.ts +311 -0
- package/deepccc-agent/src/__tests__/progress-reducer.test.ts +121 -0
- package/deepccc-agent/src/__tests__/session-search.test.ts +262 -0
- package/deepccc-agent/src/__tests__/session-select.test.ts +116 -0
- package/deepccc-agent/src/__tests__/sigint.test.ts +56 -0
- package/deepccc-agent/src/__tests__/skills.test.ts +284 -0
- package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +247 -0
- package/deepccc-agent/src/__tests__/web-tools.test.ts +220 -0
- package/{src/builtin → deepccc-agent/src}/index.ts +25 -11
- package/deepccc-agent/tsconfig.build.json +13 -0
- package/deepccc-agent/tsconfig.json +13 -0
- package/deepccc-agent/vitest.config.ts +7 -0
- package/package.json +73 -71
- package/src/__tests__/builtin-chat-session.test.ts +53 -16
- package/src/__tests__/builtin-config.test.ts +2 -2
- package/src/__tests__/builtin-context.test.ts +1 -1
- package/src/__tests__/builtin-file-tools.test.ts +1 -1
- package/src/__tests__/builtin-permissions.test.ts +4 -4
- package/src/__tests__/builtin-session-search.test.ts +1 -1
- package/src/__tests__/builtin-session-select.test.ts +2 -2
- package/src/__tests__/builtin-sigint.test.ts +1 -1
- package/src/__tests__/builtin-skills.test.ts +1 -1
- package/src/__tests__/builtin-web-tools.test.ts +1 -1
- package/src/__tests__/progress-reducer.test.ts +1 -1
- package/src/adapters/ccc-adapter.ts +2 -2
- package/src/progress/reducer.ts +1 -1
- /package/{src/builtin → deepccc-agent/src}/cli.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/config.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/context.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/file-log.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/file-tools.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/permissions.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/privacy.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/proc-tree-kill.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/progress/cards-helpers.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/progress/reducer.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/progress/terminal-renderer.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/progress/view.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/raw-stream-log.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/session-search.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/session-select.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/sigint.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/skills.ts +0 -0
- /package/{src/builtin → deepccc-agent/src}/web-tools.ts +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
decodeDdgHref,
|
|
5
|
+
extractHtmlTitle,
|
|
6
|
+
htmlToPlainText,
|
|
7
|
+
parseDuckDuckGoHtml,
|
|
8
|
+
webFetchForTool,
|
|
9
|
+
webSearchForTool,
|
|
10
|
+
type FetchLike,
|
|
11
|
+
} from "../web-tools.js";
|
|
12
|
+
|
|
13
|
+
const DDG_HTML = `<!DOCTYPE html>
|
|
14
|
+
<html>
|
|
15
|
+
<head><title>Search results</title></head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="result results_links results_links_deep web-result">
|
|
18
|
+
<h2 class="result__title">
|
|
19
|
+
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fnodejs.org%2Fen%2Fdocs&rut=abc">Node.js <b>Docs</b></a>
|
|
20
|
+
</h2>
|
|
21
|
+
<a rel="nofollow" class="result__snippet" href="//duckduckgo.com/l/?uddg=...">Official documentation for Node.js & npm.</a>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="result">
|
|
24
|
+
<a rel="nofollow" class="result__a" href="https://example.com/page">Example "quoted" page</a>
|
|
25
|
+
<a rel="nofollow" class="result__snippet" href="https://example.com/page">A plain snippet without highlight.</a>
|
|
26
|
+
</div>
|
|
27
|
+
</body>
|
|
28
|
+
</html>`;
|
|
29
|
+
|
|
30
|
+
const WEB_FETCH_HTML = `<!DOCTYPE html>
|
|
31
|
+
<html><head><title>Fetch & Me</title></head>
|
|
32
|
+
<body>
|
|
33
|
+
<style>.x { color: red }</style>
|
|
34
|
+
<script>alert("x")</script>
|
|
35
|
+
<h1>Hello World</h1>
|
|
36
|
+
<p>First paragraph with & entity.</p>
|
|
37
|
+
<p>Second paragraph<br>with a break.</p>
|
|
38
|
+
<footer>bye</footer>
|
|
39
|
+
</body></html>`;
|
|
40
|
+
|
|
41
|
+
describe("parseDuckDuckGoHtml", () => {
|
|
42
|
+
it("extracts title, decoded url, and snippet per result", () => {
|
|
43
|
+
const results = parseDuckDuckGoHtml(DDG_HTML);
|
|
44
|
+
expect(results).toHaveLength(2);
|
|
45
|
+
expect(results[0]).toEqual({
|
|
46
|
+
title: "Node.js Docs",
|
|
47
|
+
url: "https://nodejs.org/en/docs",
|
|
48
|
+
snippet: "Official documentation for Node.js & npm.",
|
|
49
|
+
});
|
|
50
|
+
expect(results[1]).toEqual({
|
|
51
|
+
title: 'Example "quoted" page',
|
|
52
|
+
url: "https://example.com/page",
|
|
53
|
+
snippet: "A plain snippet without highlight.",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns empty array for pages without results", () => {
|
|
58
|
+
expect(parseDuckDuckGoHtml("<html><body>no results here</body></html>")).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("decodeDdgHref", () => {
|
|
63
|
+
it("decodes uddg redirect params and restores protocol", () => {
|
|
64
|
+
expect(
|
|
65
|
+
decodeDdgHref("//duckduckgo.com/l/?uddg=https%3A%2F%2Fnodejs.org%2Fen%2Fdocs&rut=abc"),
|
|
66
|
+
).toBe("https://nodejs.org/en/docs");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("passes through plain urls", () => {
|
|
70
|
+
expect(decodeDdgHref("https://example.com/a?b=1")).toBe("https://example.com/a?b=1");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns input untouched when unparseable", () => {
|
|
74
|
+
expect(decodeDdgHref("not a url")).toBe("not a url");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("htmlToPlainText", () => {
|
|
79
|
+
it("strips scripts/styles, keeps block boundaries, decodes entities, compresses whitespace", () => {
|
|
80
|
+
const text = htmlToPlainText(WEB_FETCH_HTML);
|
|
81
|
+
expect(text).not.toContain("alert");
|
|
82
|
+
expect(text).not.toContain(".x {");
|
|
83
|
+
expect(text).toContain("Hello World");
|
|
84
|
+
expect(text).toContain("First paragraph with & entity.");
|
|
85
|
+
expect(text).toContain("Second paragraph\nwith a break.");
|
|
86
|
+
expect(text).toContain("bye");
|
|
87
|
+
// 段落间允许单个空行,但不得出现连续空行
|
|
88
|
+
expect(text).not.toContain("\n\n\n");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("extractHtmlTitle", () => {
|
|
93
|
+
it("extracts and decodes the title", () => {
|
|
94
|
+
expect(extractHtmlTitle(WEB_FETCH_HTML)).toBe("Fetch & Me");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns empty string when no title", () => {
|
|
98
|
+
expect(extractHtmlTitle("<html><body>x</body></html>")).toBe("");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("webSearchForTool", () => {
|
|
103
|
+
function mockFetch(html: string, status = 200): FetchLike {
|
|
104
|
+
return async () =>
|
|
105
|
+
new Response(html, {
|
|
106
|
+
status,
|
|
107
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
it("returns parsed results from the search page", async () => {
|
|
112
|
+
const output = await webSearchForTool(
|
|
113
|
+
{ query: "nodejs docs" },
|
|
114
|
+
{ fetchImpl: mockFetch(DDG_HTML) },
|
|
115
|
+
);
|
|
116
|
+
expect(output.query).toBe("nodejs docs");
|
|
117
|
+
expect(output.results).toHaveLength(2);
|
|
118
|
+
expect(output.results[0].url).toBe("https://nodejs.org/en/docs");
|
|
119
|
+
expect(output.results[0].snippet).toContain("Node.js");
|
|
120
|
+
expect(typeof output.durationMs).toBe("number");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("respects maxResults and caps at 10", async () => {
|
|
124
|
+
const html = Array.from({ length: 12 }, (_, i) => {
|
|
125
|
+
const title = `<a rel="nofollow" class="result__a" href="https://e.com/${i}">R${i}</a>`;
|
|
126
|
+
const snippet = `<a rel="nofollow" class="result__snippet" href="https://e.com/${i}">s${i}</a>`;
|
|
127
|
+
return `<div>${title}${snippet}</div>`;
|
|
128
|
+
}).join("");
|
|
129
|
+
const output = await webSearchForTool({ query: "many" }, { fetchImpl: mockFetch(html) });
|
|
130
|
+
expect(output.results).toHaveLength(5);
|
|
131
|
+
expect(output.truncated).toBe(true);
|
|
132
|
+
|
|
133
|
+
const output2 = await webSearchForTool({ query: "many", maxResults: 20 }, { fetchImpl: mockFetch(html) });
|
|
134
|
+
expect(output2.results).toHaveLength(10);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns empty results (not an error) when the page has no matches", async () => {
|
|
138
|
+
const output = await webSearchForTool(
|
|
139
|
+
{ query: "zzz-nothing" },
|
|
140
|
+
{ fetchImpl: mockFetch("<html><body>no results</body></html>") },
|
|
141
|
+
);
|
|
142
|
+
expect(output.results).toEqual([]);
|
|
143
|
+
expect(output.truncated).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("throws on empty query", async () => {
|
|
147
|
+
await expect(webSearchForTool({ query: " " }, { fetchImpl: mockFetch("") })).rejects.toThrow(
|
|
148
|
+
"query is required",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("throws on non-2xx response", async () => {
|
|
153
|
+
await expect(
|
|
154
|
+
webSearchForTool({ query: "x" }, { fetchImpl: mockFetch("blocked", 429) }),
|
|
155
|
+
).rejects.toThrow("HTTP 429");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("webFetchForTool", () => {
|
|
160
|
+
function mockFetch(html: string, status = 200, contentType = "text/html; charset=utf-8"): FetchLike {
|
|
161
|
+
return async () =>
|
|
162
|
+
new Response(html, {
|
|
163
|
+
status,
|
|
164
|
+
headers: { "content-type": contentType },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
it("fetches and converts html to plain text with title", async () => {
|
|
169
|
+
const output = await webFetchForTool(
|
|
170
|
+
{ url: "https://example.com/page" },
|
|
171
|
+
{ fetchImpl: mockFetch(WEB_FETCH_HTML) },
|
|
172
|
+
);
|
|
173
|
+
expect(output.title).toBe("Fetch & Me");
|
|
174
|
+
expect(output.text).toContain("Hello World");
|
|
175
|
+
expect(output.text).toContain("Second paragraph\nwith a break.");
|
|
176
|
+
expect(output.text).not.toContain("alert");
|
|
177
|
+
expect(output.contentType).toContain("text/html");
|
|
178
|
+
expect(output.chars).toBe(output.text.length);
|
|
179
|
+
expect(output.truncated).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("truncates text beyond maxChars", async () => {
|
|
183
|
+
const output = await webFetchForTool(
|
|
184
|
+
{ url: "https://example.com/long", maxChars: 10 },
|
|
185
|
+
{ fetchImpl: mockFetch("<p>hello world and more</p>") },
|
|
186
|
+
);
|
|
187
|
+
expect(output.text).toBe("hello worl");
|
|
188
|
+
expect(output.truncated).toBe(true);
|
|
189
|
+
expect(output.chars).toBe(10);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("truncates oversized response bodies", async () => {
|
|
193
|
+
const big = `<!DOCTYPE html><html><body><p>${"a".repeat(600 * 1024)}</p></body></html>`;
|
|
194
|
+
const output = await webFetchForTool(
|
|
195
|
+
{ url: "https://example.com/big" },
|
|
196
|
+
{ fetchImpl: mockFetch(big) },
|
|
197
|
+
);
|
|
198
|
+
expect(output.truncated).toBe(true);
|
|
199
|
+
expect(output.text.length).toBeLessThan(600 * 1024);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("rejects non-http(s) protocols", async () => {
|
|
203
|
+
await expect(
|
|
204
|
+
webFetchForTool({ url: "file:///etc/passwd" }, { fetchImpl: mockFetch("") }),
|
|
205
|
+
).rejects.toThrow("unsupported protocol");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("rejects invalid urls", async () => {
|
|
209
|
+
await expect(
|
|
210
|
+
webFetchForTool({ url: "not a url" }, { fetchImpl: mockFetch("") }),
|
|
211
|
+
).rejects.toThrow("invalid URL");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("throws on missing url and non-2xx", async () => {
|
|
215
|
+
await expect(webFetchForTool({ url: " " })).rejects.toThrow("url is required");
|
|
216
|
+
await expect(
|
|
217
|
+
webFetchForTool({ url: "https://example.com/404" }, { fetchImpl: mockFetch("nf", 404) }),
|
|
218
|
+
).rejects.toThrow("HTTP 404");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -42,17 +42,31 @@ const SYSTEM_PROMPT = [
|
|
|
42
42
|
"- Respond in the user's language unless they ask otherwise.",
|
|
43
43
|
"- Prefer direct, usable answers and concrete actions over long explanations.",
|
|
44
44
|
"- For code tasks, inspect the relevant files before editing and verify with tests or checks when practical.",
|
|
45
|
-
"- Preserve user work. Do not overwrite concurrent changes unless the user explicitly asks.",
|
|
46
|
-
"- Keep immutable platform rules above project guidance and runtime details.",
|
|
47
|
-
"",
|
|
48
|
-
"## Evidence-Gated Conclusions",
|
|
49
|
-
"- Apply this gate before consequential claims or actions that could affect code, data, deployments, or user decisions, and whenever the available evidence is indirect.",
|
|
50
|
-
"- Identify the claim and its authoritative source of truth. Separate direct observations from inferences, and test plausible alternative explanations before choosing one.",
|
|
51
|
-
"- Use the strongest practical decisive check at the same semantic level as the claim: runtime behavior for runtime claims, effective configuration for configuration claims, deployed state for deployment claims, and transformed output for transformation claims.",
|
|
52
|
-
"- Do not treat proxy signals such as names, timestamps, file sizes, line counts, partial samples, or a clean command exit as decisive when a direct check is practical.",
|
|
53
|
-
"- Use definitive language only after the evidence closes the loop. Otherwise state uncertainty, identify the missing evidence, and name the next check.",
|
|
54
|
-
"- Do not repeat checks once decisive evidence exists.",
|
|
55
|
-
|
|
45
|
+
"- Preserve user work. Do not overwrite concurrent changes unless the user explicitly asks.",
|
|
46
|
+
"- Keep immutable platform rules above project guidance and runtime details.",
|
|
47
|
+
"",
|
|
48
|
+
"## Evidence-Gated Conclusions",
|
|
49
|
+
"- Apply this gate before consequential claims or actions that could affect code, data, deployments, or user decisions, and whenever the available evidence is indirect.",
|
|
50
|
+
"- Identify the claim and its authoritative source of truth. Separate direct observations from inferences, and test plausible alternative explanations before choosing one.",
|
|
51
|
+
"- Use the strongest practical decisive check at the same semantic level as the claim: runtime behavior for runtime claims, effective configuration for configuration claims, deployed state for deployment claims, and transformed output for transformation claims.",
|
|
52
|
+
"- Do not treat proxy signals such as names, timestamps, file sizes, line counts, partial samples, or a clean command exit as decisive when a direct check is practical.",
|
|
53
|
+
"- Use definitive language only after the evidence closes the loop. Otherwise state uncertainty, identify the missing evidence, and name the next check.",
|
|
54
|
+
"- Do not repeat checks once decisive evidence exists.",
|
|
55
|
+
"",
|
|
56
|
+
"## Survey Before Acting",
|
|
57
|
+
"- Before diving into any task, first map the landscape at low cost: project instructions, directory layout, routes/APIs, existing tests, and git state.",
|
|
58
|
+
"- Produce a brief execution plan that includes how you will verify the result, then execute.",
|
|
59
|
+
"- When evidence contradicts an early assumption, revisit the plan instead of tunneling on one direction.",
|
|
60
|
+
"",
|
|
61
|
+
"## Delegated Authority",
|
|
62
|
+
"- When the user delegates decisions (\"you decide\", \"do it elegantly\", \"up to you\"), act autonomously on implementation details.",
|
|
63
|
+
"- Only ask about genuine blockers: irreversible actions, security boundaries, credentials, or scope changes.",
|
|
64
|
+
"- Do not bounce implementation-level multiple-choice questions back to the user after they delegated.",
|
|
65
|
+
"",
|
|
66
|
+
"## Pre-Delivery Self-Check",
|
|
67
|
+
"- Before reporting completion, verify: the change runs, edge cases are covered, assumptions are listed, and anything unverified is explicitly labeled as such.",
|
|
68
|
+
"- State what was done, how it was verified, and what remains unverified or risky.",
|
|
69
|
+
].join("\n");
|
|
56
70
|
|
|
57
71
|
const SUMMARY_SYSTEM_PROMPT = [
|
|
58
72
|
"You are DeepCCC's context compactor.",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": false,
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": false,
|
|
9
|
+
"sourceMap": false
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/*.ts"],
|
|
12
|
+
"exclude": ["src/__tests__/**"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"noEmit": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts", "vitest.config.ts"]
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,71 +1,73 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./src/index.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"chatccc": "bin/chatccc.mjs",
|
|
10
|
-
"cccagent": "bin/cccagent.mjs"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"src/",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
".
|
|
21
|
-
".
|
|
22
|
-
"
|
|
23
|
-
"images/img_readme_*.
|
|
24
|
-
"images/
|
|
25
|
-
"images/avatars/
|
|
26
|
-
"images/avatars/
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"demo:bot-test
|
|
37
|
-
"demo:
|
|
38
|
-
"demo:create-group
|
|
39
|
-
"demo:
|
|
40
|
-
"demo:
|
|
41
|
-
"demo:
|
|
42
|
-
"demo:codex-
|
|
43
|
-
"demo:
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"test
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"@types/
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "chatccc",
|
|
3
|
+
"version": "0.2.242",
|
|
4
|
+
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"chatccc": "bin/chatccc.mjs",
|
|
10
|
+
"cccagent": "bin/cccagent.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src/",
|
|
14
|
+
"deepccc-agent/",
|
|
15
|
+
"bin/",
|
|
16
|
+
"scripts/postinstall-sharp-check.mjs",
|
|
17
|
+
"demo/ilink_echo_probe.ts",
|
|
18
|
+
"agent-prompts/",
|
|
19
|
+
"im-skills/",
|
|
20
|
+
".agents/skills/create-chatccc-feishu-app/",
|
|
21
|
+
".claude/skills/create-chatccc-feishu-app/",
|
|
22
|
+
".cursor/skills/create-chatccc-feishu-app/",
|
|
23
|
+
"images/img_readme_*.jpg",
|
|
24
|
+
"images/img_readme_*.png",
|
|
25
|
+
"images/avatars/status_*.png",
|
|
26
|
+
"images/avatars/badges/",
|
|
27
|
+
"images/avatars/combinations/",
|
|
28
|
+
"package.json",
|
|
29
|
+
"README.md",
|
|
30
|
+
"config.sample.json"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"dev": "tsx src/index.ts",
|
|
34
|
+
"chatccc": "tsx src/index.ts",
|
|
35
|
+
"start": "tsx src/index.ts",
|
|
36
|
+
"demo:bot-test": "tsx demo/bot_test.ts",
|
|
37
|
+
"demo:bot-test:local": "tsx demo/bot_test.ts --local",
|
|
38
|
+
"demo:create-group": "tsx src/index.ts",
|
|
39
|
+
"demo:create-group:local": "tsx src/index.ts --local",
|
|
40
|
+
"demo:permission-check": "tsx demo/permission_check.ts",
|
|
41
|
+
"demo:claude-hi": "tsx demo/claude_say_hi.ts",
|
|
42
|
+
"demo:codex-hi": "tsx demo/codex_say_hi.ts",
|
|
43
|
+
"demo:codex-app-server-approval": "tsx demo/codex-app-server-approval/approval_demo.ts",
|
|
44
|
+
"demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
|
|
45
|
+
"claude-proxy": "tsx src/litellm-proxy.ts",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:deepccc": "vitest run --root deepccc-agent",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"postinstall": "node scripts/postinstall-sharp-check.mjs"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
53
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
54
|
+
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
55
|
+
"@vscode/ripgrep": "^1.18.0",
|
|
56
|
+
"ai": "^6.0.184",
|
|
57
|
+
"nodemailer": "^8.0.7",
|
|
58
|
+
"qrcode-terminal": "^0.12.0",
|
|
59
|
+
"sharp": "^0.34.5",
|
|
60
|
+
"tsx": "^4.0.0",
|
|
61
|
+
"ws": "^8.18.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/node": "^20.0.0",
|
|
65
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
66
|
+
"@types/ws": "^8.18.1",
|
|
67
|
+
"typescript": "^5.0.0",
|
|
68
|
+
"vitest": "^3.2.4"
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=20"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -4,8 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { config } from "
|
|
8
|
-
import { estimateBuiltinContextTokens } from "
|
|
7
|
+
import { config } from "../../deepccc-agent/src/config.ts";
|
|
8
|
+
import { estimateBuiltinContextTokens } from "../../deepccc-agent/src/context.ts";
|
|
9
9
|
|
|
10
10
|
const streamTextMock = vi.fn();
|
|
11
11
|
const generateTextMock = vi.fn();
|
|
@@ -27,7 +27,7 @@ vi.mock("ai", () => ({
|
|
|
27
27
|
tool: vi.fn((definition: unknown) => definition),
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
-
vi.mock("
|
|
30
|
+
vi.mock("../../deepccc-agent/src/raw-stream-log.js", () => ({
|
|
31
31
|
createRawStreamLog: createRawStreamLogMock,
|
|
32
32
|
}));
|
|
33
33
|
|
|
@@ -57,7 +57,7 @@ afterEach(() => {
|
|
|
57
57
|
|
|
58
58
|
describe("ChatSession context management", () => {
|
|
59
59
|
it("keeps the generalized evidence gate in the stable system prompt prefix", async () => {
|
|
60
|
-
const { ChatSession } = await import("
|
|
60
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
61
61
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-evidence-gate-"));
|
|
62
62
|
await writeFile(join(dir, "AGENTS.md"), "PROJECT GUIDANCE MARKER", "utf-8");
|
|
63
63
|
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
@@ -92,8 +92,45 @@ describe("ChatSession context management", () => {
|
|
|
92
92
|
);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it("keeps execution discipline sections in the stable system prompt prefix", async () => {
|
|
96
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
97
|
+
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-discipline-"));
|
|
98
|
+
await writeFile(join(dir, "AGENTS.md"), "PROJECT GUIDANCE MARKER", "utf-8");
|
|
99
|
+
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
100
|
+
|
|
101
|
+
const session = new ChatSession(
|
|
102
|
+
{ apiKey: "sk-test" },
|
|
103
|
+
{
|
|
104
|
+
cwd: dir,
|
|
105
|
+
sessionId: "execution-discipline",
|
|
106
|
+
systemPrompt: "CUSTOM PROMPT MARKER",
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
await collect(session.chat("build a feature"));
|
|
110
|
+
|
|
111
|
+
const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
|
|
112
|
+
// 先盘点再动手:动手前低开销盘点 + 输出含验证策略的计划
|
|
113
|
+
expect(system).toContain("## Survey Before Acting");
|
|
114
|
+
expect(system).toContain("map the landscape");
|
|
115
|
+
expect(system).toContain("how you will verify the result");
|
|
116
|
+
// 授权自主:用户委托决策后只问真正阻塞项,不抛实现级选择题
|
|
117
|
+
expect(system).toContain("## Delegated Authority");
|
|
118
|
+
expect(system).toContain("irreversible actions");
|
|
119
|
+
expect(system).toContain("Do not bounce implementation-level multiple-choice");
|
|
120
|
+
// 交付自检:声明做了什么/如何验证/未验证项
|
|
121
|
+
expect(system).toContain("## Pre-Delivery Self-Check");
|
|
122
|
+
expect(system).toContain("remains unverified or risky");
|
|
123
|
+
// 稳定前缀全部位于项目指令与 runtime 上下文之前
|
|
124
|
+
for (const section of ["## Survey Before Acting", "## Delegated Authority", "## Pre-Delivery Self-Check"]) {
|
|
125
|
+
expect(system.indexOf(section)).toBeGreaterThan(0);
|
|
126
|
+
expect(system.indexOf(section)).toBeLessThan(system.indexOf("PROJECT GUIDANCE MARKER"));
|
|
127
|
+
expect(system.indexOf(section)).toBeLessThan(system.indexOf("Current working directory"));
|
|
128
|
+
expect(system.indexOf(section)).toBeLessThan(system.indexOf("CUSTOM PROMPT MARKER"));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
95
132
|
it("injects cwd project instruction files before runtime workspace details", async () => {
|
|
96
|
-
const { ChatSession } = await import("
|
|
133
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
97
134
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-instructions-"));
|
|
98
135
|
await writeFile(join(dir, "AGENTS.md"), "agents root guidance", "utf-8");
|
|
99
136
|
await writeFile(join(dir, "AGENTS.local.md"), "agents local guidance", "utf-8");
|
|
@@ -128,7 +165,7 @@ describe("ChatSession context management", () => {
|
|
|
128
165
|
});
|
|
129
166
|
|
|
130
167
|
it("places the volatile skills index at the very end of system prompt", async () => {
|
|
131
|
-
const { ChatSession } = await import("
|
|
168
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
132
169
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-skill-order-"));
|
|
133
170
|
const skillsDir = join(dir, "skills");
|
|
134
171
|
await mkdir(join(skillsDir, "demo-skill"), { recursive: true });
|
|
@@ -162,7 +199,7 @@ describe("ChatSession context management", () => {
|
|
|
162
199
|
});
|
|
163
200
|
|
|
164
201
|
it("does not read project instruction files from parent directories", async () => {
|
|
165
|
-
const { ChatSession } = await import("
|
|
202
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
166
203
|
const parent = await mkdtemp(join(tmpdir(), "deepccc-session-parent-instructions-"));
|
|
167
204
|
const child = join(parent, "child");
|
|
168
205
|
await mkdir(child);
|
|
@@ -183,7 +220,7 @@ describe("ChatSession context management", () => {
|
|
|
183
220
|
});
|
|
184
221
|
|
|
185
222
|
it("uses loop-finished stopping by default", async () => {
|
|
186
|
-
const { ChatSession } = await import("
|
|
223
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
187
224
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-unlimited-"));
|
|
188
225
|
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
189
226
|
|
|
@@ -202,7 +239,7 @@ describe("ChatSession context management", () => {
|
|
|
202
239
|
});
|
|
203
240
|
|
|
204
241
|
it("uses a configured tool step limit when provided", async () => {
|
|
205
|
-
const { ChatSession } = await import("
|
|
242
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
206
243
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-step-budget-"));
|
|
207
244
|
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
208
245
|
|
|
@@ -222,7 +259,7 @@ describe("ChatSession context management", () => {
|
|
|
222
259
|
});
|
|
223
260
|
|
|
224
261
|
it("loads persisted context, compacts older messages, and persists the new assistant reply", async () => {
|
|
225
|
-
const { ChatSession } = await import("
|
|
262
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
226
263
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-context-"));
|
|
227
264
|
|
|
228
265
|
const seed = new ChatSession(
|
|
@@ -270,7 +307,7 @@ describe("ChatSession context management", () => {
|
|
|
270
307
|
|
|
271
308
|
it("times out context compaction independently before reply generation", async () => {
|
|
272
309
|
vi.useFakeTimers();
|
|
273
|
-
const { ChatSession } = await import("
|
|
310
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
274
311
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-compaction-timeout-"));
|
|
275
312
|
const seed = new ChatSession(
|
|
276
313
|
{ apiKey: "sk-test" },
|
|
@@ -304,7 +341,7 @@ describe("ChatSession context management", () => {
|
|
|
304
341
|
});
|
|
305
342
|
|
|
306
343
|
it("streams tool calls and tool results from fullStream", async () => {
|
|
307
|
-
const { ChatSession } = await import("
|
|
344
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
308
345
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-tools-"));
|
|
309
346
|
const session = new ChatSession(
|
|
310
347
|
{ apiKey: "sk-test" },
|
|
@@ -342,7 +379,7 @@ describe("ChatSession context management", () => {
|
|
|
342
379
|
});
|
|
343
380
|
|
|
344
381
|
it("caps the total persisted tool transcript for a turn", async () => {
|
|
345
|
-
const { ChatSession } = await import("
|
|
382
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
346
383
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-tool-cap-"));
|
|
347
384
|
const session = new ChatSession(
|
|
348
385
|
{ apiKey: "sk-test" },
|
|
@@ -366,7 +403,7 @@ describe("ChatSession context management", () => {
|
|
|
366
403
|
});
|
|
367
404
|
|
|
368
405
|
it("persists structured tool calls alongside the text transcript", async () => {
|
|
369
|
-
const { ChatSession } = await import("
|
|
406
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
370
407
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-structured-tools-"));
|
|
371
408
|
const session = new ChatSession(
|
|
372
409
|
{ apiKey: "sk-test" },
|
|
@@ -393,7 +430,7 @@ describe("ChatSession context management", () => {
|
|
|
393
430
|
});
|
|
394
431
|
|
|
395
432
|
it("records tool errors and preserves tool call order in structured tool calls", async () => {
|
|
396
|
-
const { ChatSession } = await import("
|
|
433
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
397
434
|
const dir = await mkdtemp(join(tmpdir(), "deepccc-session-structured-tools-order-"));
|
|
398
435
|
const session = new ChatSession(
|
|
399
436
|
{ apiKey: "sk-test" },
|
|
@@ -422,7 +459,7 @@ describe("ChatSession context management", () => {
|
|
|
422
459
|
});
|
|
423
460
|
|
|
424
461
|
it("writes raw DeepCCC fullStream parts when raw stream logs are enabled", async () => {
|
|
425
|
-
const { ChatSession } = await import("
|
|
462
|
+
const { ChatSession } = await import("../../deepccc-agent/src/index.ts");
|
|
426
463
|
config.rawStreamLogs = {
|
|
427
464
|
enabled: true,
|
|
428
465
|
maxBytesPerTurn: 4096,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { ChatSession } from "
|
|
4
|
-
import { config } from "
|
|
3
|
+
import { ChatSession } from "../../deepccc-agent/src/index.ts";
|
|
4
|
+
import { config } from "../../deepccc-agent/src/config.ts";
|
|
5
5
|
|
|
6
6
|
const originalDeepSeekApiKey = process.env.DEEPSEEK_API_KEY;
|
|
7
7
|
const originalDeepCccApiKey = config.apiKey;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
listBuiltinContextSessions,
|
|
13
13
|
newBuiltinSessionId,
|
|
14
14
|
serializeMessagesForSummary,
|
|
15
|
-
} from "
|
|
15
|
+
} from "../../deepccc-agent/src/context.ts";
|
|
16
16
|
|
|
17
17
|
describe("BuiltinContextManager", () => {
|
|
18
18
|
it("keeps recent messages within the token budget instead of a fixed count", () => {
|