chatccc 0.2.244 → 0.2.245
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 +1 -1
- package/deepccc-agent/README.md +14 -8
- package/deepccc-agent/os-prompts/darwin.md +8 -8
- package/deepccc-agent/os-prompts/linux.md +8 -8
- package/deepccc-agent/os-prompts/win32.md +9 -9
- package/deepccc-agent/package-lock.json +2 -2
- package/deepccc-agent/package.json +63 -63
- package/deepccc-agent/src/__tests__/chat-session.test.ts +682 -617
- package/deepccc-agent/src/__tests__/privacy.test.ts +8 -5
- package/deepccc-agent/src/cli.ts +7 -6
- package/deepccc-agent/src/config.ts +4 -0
- package/deepccc-agent/src/index.ts +57 -20
- package/package.json +73 -73
- package/src/__tests__/ccc-adapter.test.ts +15 -0
- package/src/__tests__/session.test.ts +71 -8
- package/src/adapters/adapter-interface.ts +8 -2
- package/src/adapters/ccc-adapter.ts +4 -0
- package/src/session.ts +13 -10
|
@@ -55,7 +55,8 @@ import { ChatSession } from "../index.js";
|
|
|
55
55
|
const streamTextMock = aiMocks.streamText;
|
|
56
56
|
const generateTextMock = aiMocks.generateText;
|
|
57
57
|
|
|
58
|
-
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
58
|
+
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
59
|
+
const originalStreaming = config.streaming;
|
|
59
60
|
const PRIVACY_FILE = join(privacyState.dir, "privacy.json");
|
|
60
61
|
|
|
61
62
|
function writePrivacy(content: string): void {
|
|
@@ -78,16 +79,18 @@ beforeEach(() => {
|
|
|
78
79
|
} catch {}
|
|
79
80
|
reloadPrivacyRules();
|
|
80
81
|
streamTextMock.mockReset();
|
|
81
|
-
generateTextMock.mockReset();
|
|
82
|
-
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
82
|
+
generateTextMock.mockReset();
|
|
83
|
+
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
84
|
+
config.streaming = true;
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
afterEach(() => {
|
|
86
88
|
try {
|
|
87
89
|
rmSync(PRIVACY_FILE, { force: true });
|
|
88
90
|
} catch {}
|
|
89
|
-
reloadPrivacyRules();
|
|
90
|
-
|
|
91
|
+
reloadPrivacyRules();
|
|
92
|
+
config.streaming = originalStreaming;
|
|
93
|
+
});
|
|
91
94
|
|
|
92
95
|
afterAll(() => {
|
|
93
96
|
try {
|
package/deepccc-agent/src/cli.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* 非 TTY(管道/CI)或 --plain 回退为纯文本流式输出;--stream-json 机器接口不变。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import * as readline from "node:readline";
|
|
15
|
-
import
|
|
14
|
+
import * as readline from "node:readline";
|
|
15
|
+
import process from "node:process";
|
|
16
16
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join, resolve as resolvePath } from "node:path";
|
|
@@ -637,10 +637,11 @@ async function main(): Promise<void> {
|
|
|
637
637
|
|
|
638
638
|
const args = parseArgs();
|
|
639
639
|
|
|
640
|
-
if (args.streamJson) {
|
|
641
|
-
const code = await runStreamJson(args);
|
|
642
|
-
process.
|
|
643
|
-
|
|
640
|
+
if (args.streamJson) {
|
|
641
|
+
const code = await runStreamJson(args);
|
|
642
|
+
process.exitCode = code;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
644
645
|
|
|
645
646
|
if (args.help) {
|
|
646
647
|
const { appConfig } = await loadRuntime();
|
|
@@ -8,6 +8,8 @@ export interface DeepCccConfig {
|
|
|
8
8
|
model: string;
|
|
9
9
|
/** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
|
|
10
10
|
effort: string;
|
|
11
|
+
/** 主对话是否使用流式请求;默认开启 */
|
|
12
|
+
streaming: boolean;
|
|
11
13
|
rawStreamLogs: {
|
|
12
14
|
enabled: boolean;
|
|
13
15
|
maxBytesPerTurn: number;
|
|
@@ -25,6 +27,7 @@ const DEFAULT_CONFIG: DeepCccConfig = {
|
|
|
25
27
|
baseURL: "https://api.deepseek.com/v1",
|
|
26
28
|
model: "deepseek-v4-pro",
|
|
27
29
|
effort: "",
|
|
30
|
+
streaming: true,
|
|
28
31
|
rawStreamLogs: {
|
|
29
32
|
enabled: false,
|
|
30
33
|
maxBytesPerTurn: 1024 * 1024,
|
|
@@ -68,6 +71,7 @@ function loadConfig(): DeepCccConfig {
|
|
|
68
71
|
baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
|
|
69
72
|
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
70
73
|
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
74
|
+
streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
|
|
71
75
|
rawStreamLogs: {
|
|
72
76
|
enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
|
|
73
77
|
maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
|
|
@@ -280,10 +280,11 @@ export class ChatSession {
|
|
|
280
280
|
const modelId = overrides.model ?? appConfig.model;
|
|
281
281
|
this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
|
|
282
282
|
|
|
283
|
-
const provider = createOpenAICompatible({
|
|
284
|
-
name: "deepccc",
|
|
285
|
-
baseURL,
|
|
286
|
-
apiKey,
|
|
283
|
+
const provider = createOpenAICompatible({
|
|
284
|
+
name: "deepccc",
|
|
285
|
+
baseURL,
|
|
286
|
+
apiKey,
|
|
287
|
+
includeUsage: true,
|
|
287
288
|
});
|
|
288
289
|
this.model = provider(modelId);
|
|
289
290
|
this.cwd = options.cwd ?? process.cwd();
|
|
@@ -383,20 +384,27 @@ export class ChatSession {
|
|
|
383
384
|
const skills = await scanSkillsDirs(this.skillDirs);
|
|
384
385
|
const system = this.buildSystemPrompt(skills);
|
|
385
386
|
this.systemPrompt = system;
|
|
386
|
-
const
|
|
387
|
-
model: this.model,
|
|
388
|
-
system,
|
|
389
|
-
messages: this.context.buildModelMessages() as any,
|
|
387
|
+
const generationOptions = {
|
|
388
|
+
model: this.model,
|
|
389
|
+
system,
|
|
390
|
+
messages: this.context.buildModelMessages() as any,
|
|
390
391
|
tools: createBuiltinFileTools(this.cwd, { permissionGate: this.permissionGate }),
|
|
391
392
|
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
392
393
|
abortSignal: signal,
|
|
393
|
-
// DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
|
|
394
|
-
// 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
|
|
395
|
-
...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
394
|
+
// DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
|
|
395
|
+
// 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
|
|
396
|
+
...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
|
|
397
|
+
};
|
|
398
|
+
let stream: AsyncIterable<TextStreamPart<any>>;
|
|
399
|
+
if (appConfig.streaming) {
|
|
400
|
+
const result = streamText(generationOptions);
|
|
401
|
+
stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
402
|
+
} else {
|
|
403
|
+
const result = await generateText(generationOptions);
|
|
404
|
+
stream = generateResultToFullStream(result);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
|
|
400
408
|
rawLog?.writeLine(safeRawStreamJson(part));
|
|
401
409
|
if (part.type === "text-delta") {
|
|
402
410
|
fullText += part.text;
|
|
@@ -554,11 +562,40 @@ export class ChatSession {
|
|
|
554
562
|
}
|
|
555
563
|
}
|
|
556
564
|
|
|
557
|
-
async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
|
|
558
|
-
for await (const text of stream) {
|
|
559
|
-
yield { type: "text-delta", text };
|
|
560
|
-
}
|
|
561
|
-
}
|
|
565
|
+
async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
|
|
566
|
+
for await (const text of stream) {
|
|
567
|
+
yield { type: "text-delta", text };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function* generateResultToFullStream(result: any): AsyncIterable<TextStreamPart<any>> {
|
|
572
|
+
let emittedText = false;
|
|
573
|
+
for (const step of result.steps ?? []) {
|
|
574
|
+
for (const call of step.toolCalls ?? []) {
|
|
575
|
+
yield {
|
|
576
|
+
type: "tool-call",
|
|
577
|
+
toolCallId: call.toolCallId,
|
|
578
|
+
toolName: call.toolName,
|
|
579
|
+
input: call.input,
|
|
580
|
+
} as TextStreamPart<any>;
|
|
581
|
+
}
|
|
582
|
+
for (const toolResult of step.toolResults ?? []) {
|
|
583
|
+
yield {
|
|
584
|
+
type: "tool-result",
|
|
585
|
+
toolCallId: toolResult.toolCallId,
|
|
586
|
+
toolName: toolResult.toolName,
|
|
587
|
+
output: toolResult.output,
|
|
588
|
+
} as TextStreamPart<any>;
|
|
589
|
+
}
|
|
590
|
+
if (step.text) {
|
|
591
|
+
emittedText = true;
|
|
592
|
+
yield { type: "text-delta", text: step.text } as TextStreamPart<any>;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (!emittedText && result.text) {
|
|
596
|
+
yield { type: "text-delta", text: result.text } as TextStreamPart<any>;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
562
599
|
|
|
563
600
|
function safeJson(value: unknown): string {
|
|
564
601
|
try {
|
package/package.json
CHANGED
|
@@ -1,73 +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
|
-
"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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "chatccc",
|
|
3
|
+
"version": "0.2.245",
|
|
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
|
+
}
|
|
@@ -34,6 +34,21 @@ afterEach(() => {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
describe("createCccAdapter", () => {
|
|
37
|
+
it("disables response-stall detection when DeepCCC streaming is disabled", async () => {
|
|
38
|
+
const { config: deepCccConfig } = await import("../../deepccc-agent/src/config.ts");
|
|
39
|
+
const previousStreaming = deepCccConfig.streaming;
|
|
40
|
+
deepCccConfig.streaming = false;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
|
|
44
|
+
const adapter = createCccAdapter({ apiKey: "sk-test" });
|
|
45
|
+
|
|
46
|
+
expect(adapter.responseStallDetectionEnabled).toBe(false);
|
|
47
|
+
} finally {
|
|
48
|
+
deepCccConfig.streaming = previousStreaming;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
37
52
|
it("creates a persisted ccc session and exposes model/cwd metadata", async () => {
|
|
38
53
|
const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
|
|
39
54
|
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-meta-"));
|
|
@@ -928,7 +928,7 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
928
928
|
if (tempDir) await rm(tempDir, { recursive: true, force: true });
|
|
929
929
|
});
|
|
930
930
|
|
|
931
|
-
it("auto-ends
|
|
931
|
+
it("auto-ends adapters with stall detection after three minutes of unchanged reply characters, including zero", async () => {
|
|
932
932
|
vi.setSystemTime(0);
|
|
933
933
|
_setResponseStallTimeoutForTest(180_000);
|
|
934
934
|
_setResponseStallCheckIntervalForTest(1_000);
|
|
@@ -994,13 +994,76 @@ describe("runAgentSession response stall watchdog", () => {
|
|
|
994
994
|
expect(closeSession).toHaveBeenCalledTimes(1);
|
|
995
995
|
expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
|
|
996
996
|
expect(activePrompts.has("sid-response-stall")).toBe(false);
|
|
997
|
-
expect(mockStreamStates.get("sid-response-stall")).toMatchObject({
|
|
998
|
-
status: "auto_ended",
|
|
999
|
-
finalReply: "",
|
|
1000
|
-
autoEndedAt: expect.any(Number),
|
|
1001
|
-
});
|
|
1002
|
-
});
|
|
1003
|
-
|
|
997
|
+
expect(mockStreamStates.get("sid-response-stall")).toMatchObject({
|
|
998
|
+
status: "auto_ended",
|
|
999
|
+
finalReply: "",
|
|
1000
|
+
autoEndedAt: expect.any(Number),
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it("does not detect response stalls when the adapter disables streamed-output monitoring", async () => {
|
|
1005
|
+
vi.setSystemTime(0);
|
|
1006
|
+
_setResponseStallTimeoutForTest(180_000);
|
|
1007
|
+
_setResponseStallCheckIntervalForTest(1_000);
|
|
1008
|
+
_setProcessAliveForTest(() => true);
|
|
1009
|
+
|
|
1010
|
+
const platform = mockPlatform("feishu");
|
|
1011
|
+
setSessionPlatform(platform);
|
|
1012
|
+
bindChatToSession("sid-non-streaming", "chat-non-streaming");
|
|
1013
|
+
recordLastActiveChat("sid-non-streaming", "chat-non-streaming");
|
|
1014
|
+
|
|
1015
|
+
let finishPrompt: (() => void) | undefined;
|
|
1016
|
+
const closeSession = vi.fn();
|
|
1017
|
+
const adapter: ToolAdapter = {
|
|
1018
|
+
displayName: "Non-streaming DeepCCC",
|
|
1019
|
+
sessionDescPrefix: "CCC Session:",
|
|
1020
|
+
responseStallDetectionEnabled: false,
|
|
1021
|
+
createSession: async () => ({ sessionId: "sid-non-streaming" }),
|
|
1022
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
|
|
1023
|
+
closeSession: async () => {},
|
|
1024
|
+
prompt: async function* (
|
|
1025
|
+
_sid: string,
|
|
1026
|
+
_text: string,
|
|
1027
|
+
_cwd: string,
|
|
1028
|
+
_signal?: AbortSignal,
|
|
1029
|
+
options?: ToolPromptOptions,
|
|
1030
|
+
) {
|
|
1031
|
+
options?.onSessionCreated?.(closeSession);
|
|
1032
|
+
options?.onProcessStart?.({ pid: 4545 });
|
|
1033
|
+
yield { type: "assistant", blocks: [{ type: "agent_status", status: "responding" }] };
|
|
1034
|
+
await new Promise<void>((resolve) => {
|
|
1035
|
+
finishPrompt = resolve;
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
};
|
|
1039
|
+
_setAdapterForToolForTest("ccc", adapter);
|
|
1040
|
+
|
|
1041
|
+
const runPromise = runAgentSession(
|
|
1042
|
+
"sid-non-streaming",
|
|
1043
|
+
"prompt",
|
|
1044
|
+
platform,
|
|
1045
|
+
"chat-non-streaming",
|
|
1046
|
+
Date.now(),
|
|
1047
|
+
"ccc",
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
await vi.waitFor(() => {
|
|
1051
|
+
expect(activePrompts.get("sid-non-streaming")?.processPid).toBe(4545);
|
|
1052
|
+
expect(finishPrompt).toBeTypeOf("function");
|
|
1053
|
+
});
|
|
1054
|
+
expect(activePrompts.get("sid-non-streaming")?.responseStallMonitor).toBeUndefined();
|
|
1055
|
+
|
|
1056
|
+
await vi.advanceTimersByTimeAsync(181_001);
|
|
1057
|
+
|
|
1058
|
+
expect(activePrompts.has("sid-non-streaming")).toBe(true);
|
|
1059
|
+
expect(closeSession).not.toHaveBeenCalled();
|
|
1060
|
+
expect(killProcessTreeMock).not.toHaveBeenCalledWith(4545);
|
|
1061
|
+
expect(mockStreamStates.get("sid-non-streaming")?.status).toBe("running");
|
|
1062
|
+
|
|
1063
|
+
finishPrompt?.();
|
|
1064
|
+
await runPromise;
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1004
1067
|
it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
|
|
1005
1068
|
vi.setSystemTime(0);
|
|
1006
1069
|
_setResponseStallTimeoutForTest(180_000);
|
|
@@ -151,12 +151,18 @@ export interface ToolPromptOptions {
|
|
|
151
151
|
// ToolAdapter — 统一的 AI 工具适配器接口
|
|
152
152
|
// ---------------------------------------------------------------------------
|
|
153
153
|
|
|
154
|
-
export interface ToolAdapter {
|
|
154
|
+
export interface ToolAdapter {
|
|
155
155
|
/** 日志/展示用名称,如 "Claude" */
|
|
156
156
|
readonly displayName: string;
|
|
157
157
|
|
|
158
158
|
/** 群描述中会话 ID 的前缀,如 "Claude Session:" */
|
|
159
|
-
readonly sessionDescPrefix: string;
|
|
159
|
+
readonly sessionDescPrefix: string;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether ChatCCC may infer a stalled response from unchanged streamed output.
|
|
163
|
+
* Defaults to true. Adapters that only emit output after a request completes must disable it.
|
|
164
|
+
*/
|
|
165
|
+
readonly responseStallDetectionEnabled?: boolean;
|
|
160
166
|
|
|
161
167
|
/**
|
|
162
168
|
* 创建新会话,返回会话 ID。
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChatSession, type ChatSessionConfig, type ChatSessionOptions } from "../../deepccc-agent/src/index.ts";
|
|
2
|
+
import { config as deepCccConfig } from "../../deepccc-agent/src/config.ts";
|
|
2
3
|
import {
|
|
3
4
|
getBuiltinContextSession,
|
|
4
5
|
newBuiltinSessionId,
|
|
@@ -56,6 +57,9 @@ export function createCccAdapter(options: CccAdapterOptions = {}): ToolAdapter {
|
|
|
56
57
|
return {
|
|
57
58
|
displayName: "CCC Agent",
|
|
58
59
|
sessionDescPrefix: CCC_SESSION_PREFIX,
|
|
60
|
+
// Non-streaming DeepCCC emits its reply only after the provider request finishes,
|
|
61
|
+
// so unchanged output cannot distinguish a slow request from a stalled response.
|
|
62
|
+
responseStallDetectionEnabled: deepCccConfig.streaming,
|
|
59
63
|
|
|
60
64
|
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
61
65
|
const sessionId = newBuiltinSessionId();
|
package/src/session.ts
CHANGED
|
@@ -1521,7 +1521,8 @@ export async function runAgentSession(
|
|
|
1521
1521
|
let streamErrored = false;
|
|
1522
1522
|
let streamTerminalError: TerminalErrorInfo | undefined;
|
|
1523
1523
|
let runOutcome: SessionRunOutcome = "error";
|
|
1524
|
-
|
|
1524
|
+
const responseStallDetectionEnabled = adapter.responseStallDetectionEnabled !== false;
|
|
1525
|
+
|
|
1525
1526
|
const runningPrompt = activePrompts.get(sessionId);
|
|
1526
1527
|
if (runningPrompt) {
|
|
1527
1528
|
startPromptAvatarRefresh(sessionId, tool, platform, runningPrompt);
|
|
@@ -1607,14 +1608,16 @@ export async function runAgentSession(
|
|
|
1607
1608
|
);
|
|
1608
1609
|
};
|
|
1609
1610
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1611
|
+
if (responseStallDetectionEnabled) {
|
|
1612
|
+
const responseStallMonitor = setInterval(() => {
|
|
1613
|
+
void checkResponseStall().catch((err) => {
|
|
1614
|
+
console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
|
|
1615
|
+
});
|
|
1616
|
+
}, responseStallCheckIntervalMs);
|
|
1617
|
+
responseStallMonitor.unref?.();
|
|
1618
|
+
runningPrompt.responseStallMonitor = responseStallMonitor;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1618
1621
|
|
|
1619
1622
|
try {
|
|
1620
1623
|
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
@@ -1664,7 +1667,7 @@ export async function runAgentSession(
|
|
|
1664
1667
|
}
|
|
1665
1668
|
|
|
1666
1669
|
const prompt = activePrompts.get(sessionId);
|
|
1667
|
-
if (prompt && !prompt.autoEnded) {
|
|
1670
|
+
if (prompt && !prompt.autoEnded) {
|
|
1668
1671
|
const totalChars = state.accumulatedContent.length + pickFinalReply(state).length;
|
|
1669
1672
|
prompt.responseProgress = observeResponseProgress(
|
|
1670
1673
|
// starting → responding 本身是一次有效进展,即便首个文本块仍为空也应
|