chatccc 0.2.257 → 0.2.259
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 -0
- package/config.sample.json +1 -0
- package/deepccc-agent/README.md +11 -0
- package/deepccc-agent/package.json +1 -1
- package/dist/deepccc-agent/src/cli.js +5 -0
- package/dist/deepccc-agent/src/config.js +2 -0
- package/dist/deepccc-agent/src/context.js +12 -3
- package/dist/deepccc-agent/src/file-tools.js +22 -0
- package/dist/deepccc-agent/src/index.js +200 -87
- package/dist/deepccc-agent/src/progress/reducer.js +4 -0
- package/dist/deepccc-agent/src/tool-protocol.js +17 -0
- package/dist/src/adapters/ccc-adapter.js +13 -0
- package/dist/src/agent-activity.js +4 -0
- package/dist/src/card-action-parser.js +75 -0
- package/dist/src/config.js +2 -0
- package/dist/src/index.js +2 -48
- package/dist/src/progress/reducer.js +4 -0
- package/dist/src/response-stall.js +2 -2
- package/dist/src/session.js +21 -4
- package/dist/src/web-ui.js +17 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -372,6 +372,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
372
372
|
| `cursor.alternativeModel` / `codex.alternativeModel` / `ccc.alternativeModel` | 单个备选模型;加入 `/model` 人工切换列表,不会自动故障转移 |
|
|
373
373
|
| `ccc.DEEPSEEK_API_KEY` / `ccc.DEEPSEEK_BASE_URL` | CCC Agent 的 API Key 和服务地址;**不限于 DeepSeek**——可填任意 OpenAI 兼容端点(OpenAI、Kimi、通义、智谱、Ollama 本地等) |
|
|
374
374
|
| `ccc.model` | CCC Agent 默认模型 |
|
|
375
|
+
| `ccc.subModel` | CCC Agent 子模型(选填):用于内部轻量环节(压缩摘要生成、task 子代理任务);留空跟随主模型 |
|
|
375
376
|
| `ccc.compactionTimeoutMs` | CCC Agent 上下文压缩单轮超时(毫秒),默认 300000(5 分钟);压缩超时会让整轮对话失败,建议保持默认或调大 |
|
|
376
377
|
| `ccc.contextWindow` | CCC Agent 模型上下文窗口(token),默认 1048576(1M,DeepSeek V4 Pro/Flash 原生规格);压缩阈值自动 = 窗口 × 80%;超过模型/服务端实际上限会被 API 拒绝,可在 Web UI 下拉选择或自定义(单位 k) |
|
|
377
378
|
|
package/config.sample.json
CHANGED
package/deepccc-agent/README.md
CHANGED
|
@@ -75,6 +75,7 @@ $env:DEEPCCC_STREAMING="true"
|
|
|
75
75
|
"apiKey": "sk-...",
|
|
76
76
|
"baseURL": "https://api.deepseek.com/v1",
|
|
77
77
|
"model": "deepseek-v4-pro",
|
|
78
|
+
"subModel": "",
|
|
78
79
|
"effort": "",
|
|
79
80
|
"streaming": true,
|
|
80
81
|
"contextWindow": 1048576,
|
|
@@ -102,6 +103,16 @@ $env:DEEPCCC_STREAMING="true"
|
|
|
102
103
|
API 拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 的
|
|
103
104
|
`max_input_tokens`)。
|
|
104
105
|
|
|
106
|
+
`subModel` 是子模型(选填),默认 `""`(留空跟随主模型)。配置后,DeepCCC 内部的轻量
|
|
107
|
+
环节——上下文压缩摘要生成、`task` 子代理任务——使用子模型执行,主对话仍用主模型。
|
|
108
|
+
典型用法:主模型用 pro 承担复杂推理,子模型用 flash 做高频廉价的摘要与子任务。
|
|
109
|
+
可通过 `DEEPCCC_SUB_MODEL` 环境变量或命令行 `--sub-model` 覆盖。
|
|
110
|
+
|
|
111
|
+
`task` 子代理工具:主模型可把边界清晰的独立子任务(仓库调研、长文档阅读、独立模块生成)
|
|
112
|
+
委派给子代理执行——子代理使用子模型、独立上下文,不污染主对话上下文;结果截断回传。
|
|
113
|
+
子代理**不能再次委派**(禁止嵌套),单轮最多 20 个工具步,超时与主会话压缩超时一致。
|
|
114
|
+
仅在配置了子模型时建议使用(未配置时子代理跟随主模型,节省有限)。
|
|
115
|
+
|
|
105
116
|
`rawStreamLogs.enabled` 默认 `true`,通过 `DEEPCCC_RAW_STREAM_LOGS` 环境变量或配置 JSON 关闭。
|
|
106
117
|
开启时,每次对话的原始流按 gzip JSONL 落到 `~/.deepccc/raw-stream-logs/`,供
|
|
107
118
|
`session_search` 工具在会话被压缩后找回被压缩消息的精确原文(检索时设置
|
|
@@ -57,6 +57,10 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
57
57
|
config.model = next;
|
|
58
58
|
i++;
|
|
59
59
|
}
|
|
60
|
+
else if (arg === "--sub-model" && next !== undefined) {
|
|
61
|
+
config.subModel = next;
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
60
64
|
else if (arg === "--effort" && next !== undefined) {
|
|
61
65
|
config.effort = next;
|
|
62
66
|
i++;
|
|
@@ -125,6 +129,7 @@ function printHelp(appConfig) {
|
|
|
125
129
|
"Options:",
|
|
126
130
|
` --provider <name> API protocol: openai or anthropic (current default ${appConfig.provider})`,
|
|
127
131
|
` --model <name> Model name (current default ${appConfig.model})`,
|
|
132
|
+
` --sub-model <name> Sub-model for lightweight steps (compaction/task; empty = follow main model)`,
|
|
128
133
|
` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
|
|
129
134
|
` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
|
|
130
135
|
" --api-key <key> API key",
|
|
@@ -12,6 +12,7 @@ export const DEFAULT_CONFIG = {
|
|
|
12
12
|
apiKey: "",
|
|
13
13
|
baseURL: "https://api.deepseek.com/v1",
|
|
14
14
|
model: "deepseek-v4-pro",
|
|
15
|
+
subModel: "",
|
|
15
16
|
effort: "",
|
|
16
17
|
streaming: true,
|
|
17
18
|
contextWindow: 1_048_576,
|
|
@@ -66,6 +67,7 @@ function loadConfig() {
|
|
|
66
67
|
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
67
68
|
baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
|
|
68
69
|
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
70
|
+
subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
|
|
69
71
|
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
70
72
|
streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
|
|
71
73
|
contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
|
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { hasMalformedToolProtocolText } from "./tool-protocol.js";
|
|
5
6
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
6
7
|
/** 默认模型上下文窗口:1M(DeepSeek V4 Pro/Flash 原生规格)。 */
|
|
7
8
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 1_048_576;
|
|
@@ -290,14 +291,19 @@ export class BuiltinContextManager {
|
|
|
290
291
|
].join("\n"),
|
|
291
292
|
});
|
|
292
293
|
}
|
|
293
|
-
|
|
294
|
+
// Keep malformed provider output on disk for diagnosis, but quarantine it
|
|
295
|
+
// from future prompts so one protocol failure cannot teach the model to
|
|
296
|
+
// repeat the same invalid tool syntax on every later turn.
|
|
297
|
+
messages.push(...this.modelSafeMessages());
|
|
294
298
|
return messages;
|
|
295
299
|
}
|
|
296
300
|
planCompaction() {
|
|
297
|
-
|
|
301
|
+
// The compaction model is still a model input: exclude quarantined protocol
|
|
302
|
+
// leaks here too, otherwise a later summary could reintroduce the bad syntax.
|
|
303
|
+
const messages = this.modelSafeMessages();
|
|
304
|
+
const estimated = estimateBuiltinContextTokens(this.state.summary, messages);
|
|
298
305
|
if (estimated <= this.compactAtTokens)
|
|
299
306
|
return null;
|
|
300
|
-
const messages = this.state.messages;
|
|
301
307
|
if (messages.length <= 1)
|
|
302
308
|
return null;
|
|
303
309
|
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
@@ -340,6 +346,9 @@ export class BuiltinContextManager {
|
|
|
340
346
|
writeFileSync(tmp, content, "utf8");
|
|
341
347
|
renameSync(tmp, this.contextFilePath);
|
|
342
348
|
}
|
|
349
|
+
modelSafeMessages() {
|
|
350
|
+
return this.state.messages.filter((message) => message.role !== "assistant" || !hasMalformedToolProtocolText(message.content));
|
|
351
|
+
}
|
|
343
352
|
load() {
|
|
344
353
|
if (!this.persist || !existsSync(this.contextFilePath))
|
|
345
354
|
return emptyState(this.sessionId, this.cwd);
|
|
@@ -22,6 +22,8 @@ const SEARCH_TIMEOUT_MS = 15_000;
|
|
|
22
22
|
const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024;
|
|
23
23
|
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
24
24
|
const MAX_COMMAND_TIMEOUT_MS = 900_000;
|
|
25
|
+
/** task 子代理工具:子任务结果回传主会话前的最大字符数(防止子代理长输出撑爆主上下文) */
|
|
26
|
+
export const MAX_TASK_OUTPUT_CHARS = 32_000;
|
|
25
27
|
const requireFromHere = createRequire(import.meta.url);
|
|
26
28
|
const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
|
|
27
29
|
/**
|
|
@@ -939,6 +941,7 @@ export async function applyPatchForTool(cwd, input) {
|
|
|
939
941
|
}
|
|
940
942
|
export function createBuiltinFileTools(cwd, options = {}) {
|
|
941
943
|
const gate = options.permissionGate;
|
|
944
|
+
const runTask = options.runTask;
|
|
942
945
|
/** 文件路径的候选匹配键:原始路径 + 绝对路径 + 相对 cwd 路径(正/反斜杠双版本,规则可任选其一) */
|
|
943
946
|
const pathKeys = (p) => {
|
|
944
947
|
const abs = resolveToolPath(cwd, p);
|
|
@@ -1023,6 +1026,25 @@ export function createBuiltinFileTools(cwd, options = {}) {
|
|
|
1023
1026
|
return runCommandForTool(cwd, input, options.abortSignal);
|
|
1024
1027
|
},
|
|
1025
1028
|
}),
|
|
1029
|
+
task: tool({
|
|
1030
|
+
description: "把独立子任务委派给子代理执行:子代理使用子模型、拥有独立上下文,不污染主对话上下文。适合边界清晰、可独立交付的调研/代码生成子任务(如扫描整个仓库、阅读长文档、生成独立模块)。子代理不能再次委派任务(禁止嵌套),结果会截断回传。",
|
|
1031
|
+
inputSchema: jsonSchema({
|
|
1032
|
+
type: "object",
|
|
1033
|
+
additionalProperties: false,
|
|
1034
|
+
properties: {
|
|
1035
|
+
description: { type: "string", description: "子任务描述:目标、约束与交付物。请写清楚子代理需要返回什么。" },
|
|
1036
|
+
cwd: { type: "string", description: "可选子任务工作目录(绝对路径或相对主会话 cwd),默认继承主会话工作目录。" },
|
|
1037
|
+
},
|
|
1038
|
+
required: ["description"],
|
|
1039
|
+
}),
|
|
1040
|
+
execute: async (input, execOptions) => {
|
|
1041
|
+
if (!runTask) {
|
|
1042
|
+
throw new Error("task 工具不可用:当前环境未启用子代理执行器");
|
|
1043
|
+
}
|
|
1044
|
+
const result = await runTask(input, execOptions.abortSignal);
|
|
1045
|
+
return { result };
|
|
1046
|
+
},
|
|
1047
|
+
}),
|
|
1026
1048
|
edit_file: tool({
|
|
1027
1049
|
description: "通过精确的 oldText -> newText 替换编辑现有 UTF-8 文本文件。可行时使用 SHA-256 前置条件以避免覆盖并发编辑。",
|
|
1028
1050
|
inputSchema: jsonSchema({
|
|
@@ -8,13 +8,14 @@ import { createAnthropic } from "@ai-sdk/anthropic";
|
|
|
8
8
|
import { generateText, isLoopFinished, stepCountIs, streamText } from "ai";
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
11
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { config as appConfig, normalizeDeepCccProvider, RAW_STREAM_LOGS_DIR, } from "./config.js";
|
|
14
14
|
import { createRawStreamLog, } from "./raw-stream-log.js";
|
|
15
15
|
import { buildPersistedAssistantMessage, buildSummaryPrompt, BuiltinContextManager, defaultBuiltinSessionId, } from "./context.js";
|
|
16
|
-
import { createBuiltinFileTools } from "./file-tools.js";
|
|
16
|
+
import { createBuiltinFileTools, MAX_TASK_OUTPUT_CHARS } from "./file-tools.js";
|
|
17
17
|
import { PermissionGate } from "./permissions.js";
|
|
18
|
+
import { hasMalformedToolProtocolText, TOOL_PROTOCOL_RECOVERY_PROMPT, } from "./tool-protocol.js";
|
|
18
19
|
import { buildDefaultSkillDirs, buildSkillsIndexPrompt, scanSkillsDirs, } from "./skills.js";
|
|
19
20
|
import { applyPrivacy, applyPrivacyToJson } from "./privacy.js";
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -76,6 +77,15 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
|
|
|
76
77
|
].join("\n");
|
|
77
78
|
export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
78
79
|
const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
|
|
80
|
+
/** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
|
|
81
|
+
const TASK_MAX_STEPS = 20;
|
|
82
|
+
/** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
|
|
83
|
+
function truncateTaskOutput(text) {
|
|
84
|
+
if (text.length <= MAX_TASK_OUTPUT_CHARS)
|
|
85
|
+
return text;
|
|
86
|
+
return (text.slice(0, MAX_TASK_OUTPUT_CHARS) +
|
|
87
|
+
`\n…[子代理输出已截断,共 ${text.length} 字符,仅保留前 ${MAX_TASK_OUTPUT_CHARS} 字符]`);
|
|
88
|
+
}
|
|
79
89
|
const ANTHROPIC_TOOL_JSON_COMPATIBILITY_NOTE = [
|
|
80
90
|
"[Protocol compatibility note]",
|
|
81
91
|
"tool-call arguments use JSON encoding; the final reply does not need to be JSON unless the user requests it.",
|
|
@@ -217,17 +227,71 @@ function normalizeAnthropicBaseURL(baseURL) {
|
|
|
217
227
|
}
|
|
218
228
|
export class ChatSession {
|
|
219
229
|
model;
|
|
230
|
+
/** 子模型实例;未配置 subModel 时与主模型同一实例 */
|
|
231
|
+
subModel;
|
|
220
232
|
provider;
|
|
233
|
+
apiKey;
|
|
234
|
+
baseURL;
|
|
235
|
+
modelId;
|
|
236
|
+
subModelId;
|
|
221
237
|
cwd;
|
|
222
238
|
context;
|
|
223
239
|
compactionTimeoutMs;
|
|
224
240
|
maxSteps;
|
|
225
241
|
effort;
|
|
242
|
+
permissionMode;
|
|
243
|
+
permissionResolver;
|
|
226
244
|
permissionGate;
|
|
227
245
|
skillDirs;
|
|
228
246
|
customSystemPrompt;
|
|
229
247
|
/** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
|
|
230
248
|
systemPrompt = "";
|
|
249
|
+
/**
|
|
250
|
+
* task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
|
|
251
|
+
* 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
|
|
252
|
+
*/
|
|
253
|
+
runTask = async (input, signal) => {
|
|
254
|
+
const rawCwd = input.cwd?.trim();
|
|
255
|
+
const taskCwd = rawCwd ? (isAbsolute(rawCwd) ? rawCwd : resolve(this.cwd, rawCwd)) : this.cwd;
|
|
256
|
+
const timeoutController = new AbortController();
|
|
257
|
+
const timeout = setTimeout(() => timeoutController.abort(), this.compactionTimeoutMs);
|
|
258
|
+
timeout.unref?.();
|
|
259
|
+
const taskSignal = signal
|
|
260
|
+
? AbortSignal.any([signal, timeoutController.signal])
|
|
261
|
+
: timeoutController.signal;
|
|
262
|
+
try {
|
|
263
|
+
const child = new ChatSession({
|
|
264
|
+
provider: this.provider,
|
|
265
|
+
apiKey: this.apiKey,
|
|
266
|
+
baseURL: this.baseURL,
|
|
267
|
+
model: this.subModelId || this.modelId,
|
|
268
|
+
effort: this.effort,
|
|
269
|
+
}, {
|
|
270
|
+
cwd: taskCwd,
|
|
271
|
+
persist: false,
|
|
272
|
+
permissionMode: this.permissionMode,
|
|
273
|
+
permissionResolver: this.permissionResolver,
|
|
274
|
+
maxSteps: TASK_MAX_STEPS,
|
|
275
|
+
compactionTimeoutMs: this.compactionTimeoutMs,
|
|
276
|
+
skillsDirs: this.skillDirs.map((s) => s.dir),
|
|
277
|
+
});
|
|
278
|
+
let full = "";
|
|
279
|
+
for await (const event of child.chat(input.description, taskSignal)) {
|
|
280
|
+
if (event.type === "text") {
|
|
281
|
+
full += event.text;
|
|
282
|
+
}
|
|
283
|
+
else if (event.type === "error") {
|
|
284
|
+
throw new Error(`task 子代理执行失败: ${event.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!full.trim())
|
|
288
|
+
return "(子代理未返回文本内容)";
|
|
289
|
+
return applyPrivacy(truncateTaskOutput(full));
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
clearTimeout(timeout);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
231
295
|
constructor(overrides = {}, options = {}) {
|
|
232
296
|
const apiKey = overrides.apiKey ?? appConfig.apiKey;
|
|
233
297
|
if (!apiKey) {
|
|
@@ -235,28 +299,32 @@ export class ChatSession {
|
|
|
235
299
|
}
|
|
236
300
|
const baseURL = overrides.baseURL ?? appConfig.baseURL;
|
|
237
301
|
const modelId = overrides.model ?? appConfig.model;
|
|
302
|
+
this.modelId = modelId;
|
|
303
|
+
this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
|
|
238
304
|
this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
|
|
239
305
|
this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
|
|
240
|
-
|
|
241
|
-
|
|
306
|
+
this.apiKey = apiKey;
|
|
307
|
+
this.baseURL = baseURL;
|
|
308
|
+
const provider = this.provider === "anthropic"
|
|
309
|
+
? createAnthropic({
|
|
242
310
|
baseURL: normalizeAnthropicBaseURL(baseURL),
|
|
243
311
|
apiKey,
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
const provider = createOpenAICompatible({
|
|
312
|
+
})
|
|
313
|
+
: createOpenAICompatible({
|
|
249
314
|
name: "deepccc",
|
|
250
315
|
baseURL,
|
|
251
316
|
apiKey,
|
|
252
317
|
includeUsage: true,
|
|
253
318
|
});
|
|
254
|
-
|
|
255
|
-
|
|
319
|
+
this.model = provider(modelId);
|
|
320
|
+
// 子模型:留空时与主模型共用同一实例(行为与旧版完全一致,零开销)
|
|
321
|
+
this.subModel = this.subModelId ? provider(this.subModelId) : this.model;
|
|
256
322
|
this.cwd = options.cwd ?? process.cwd();
|
|
257
323
|
this.maxSteps = normalizeMaxSteps(options.maxSteps);
|
|
258
324
|
this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
|
|
259
325
|
this.customSystemPrompt = options.systemPrompt ?? "";
|
|
326
|
+
this.permissionMode = options.permissionMode ?? "ask";
|
|
327
|
+
this.permissionResolver = options.permissionResolver;
|
|
260
328
|
// 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
|
|
261
329
|
// 因此创建/修改技能后下一次对话自动生效,无需重启。
|
|
262
330
|
this.skillDirs =
|
|
@@ -271,7 +339,7 @@ export class ChatSession {
|
|
|
271
339
|
compactAtTokens: options.compactAtTokens,
|
|
272
340
|
keepRecentMessages: options.keepRecentMessages,
|
|
273
341
|
});
|
|
274
|
-
this.permissionGate = new PermissionGate(
|
|
342
|
+
this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
|
|
275
343
|
}
|
|
276
344
|
/**
|
|
277
345
|
* 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
|
|
@@ -333,7 +401,6 @@ export class ChatSession {
|
|
|
333
401
|
catch (err) {
|
|
334
402
|
console.error(`[DeepCCC raw stream log] create failed: ${errorMessage(err)}`);
|
|
335
403
|
}
|
|
336
|
-
const toolContext = [];
|
|
337
404
|
const maxSteps = this.maxSteps;
|
|
338
405
|
// 每次对话前重新扫描技能索引(并行 + mtime 缓存,开销极小):
|
|
339
406
|
// 新技能/修改的技能在下一次对话自动生效(热加载)。
|
|
@@ -356,97 +423,143 @@ export class ChatSession {
|
|
|
356
423
|
? { deepseek: { reasoningEffort: this.effort } }
|
|
357
424
|
: { anthropic: { effort: this.effort } };
|
|
358
425
|
}
|
|
359
|
-
const
|
|
426
|
+
const baseGenerationOptions = {
|
|
360
427
|
model: this.model,
|
|
361
428
|
system,
|
|
362
|
-
|
|
363
|
-
|
|
429
|
+
tools: createBuiltinFileTools(this.cwd, {
|
|
430
|
+
permissionGate: this.permissionGate,
|
|
431
|
+
runTask: this.runTask,
|
|
432
|
+
}),
|
|
364
433
|
stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
|
|
365
434
|
abortSignal: signal,
|
|
366
435
|
...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
|
|
367
436
|
};
|
|
368
|
-
let
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
437
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
438
|
+
fullText = "";
|
|
439
|
+
safeAccumulated = "";
|
|
440
|
+
const toolContext = [];
|
|
441
|
+
toolCallsById.clear();
|
|
442
|
+
toolCallOrder.length = 0;
|
|
443
|
+
let lastReasoningProgressAt;
|
|
444
|
+
const attemptMessages = attempt === 0
|
|
445
|
+
? modelMessages
|
|
446
|
+
: [...modelMessages, { role: "user", content: TOOL_PROTOCOL_RECOVERY_PROMPT }];
|
|
447
|
+
const generationOptions = {
|
|
448
|
+
...baseGenerationOptions,
|
|
449
|
+
messages: attemptMessages,
|
|
450
|
+
};
|
|
451
|
+
let stream;
|
|
452
|
+
if (appConfig.streaming) {
|
|
453
|
+
const result = streamText(generationOptions);
|
|
454
|
+
stream = result.fullStream ?? textStreamToFullStream(result.textStream);
|
|
386
455
|
}
|
|
387
|
-
else
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
toolCallOrder.push(part.toolCallId);
|
|
391
|
-
yield {
|
|
392
|
-
type: "tool_use",
|
|
393
|
-
id: part.toolCallId,
|
|
394
|
-
name: part.toolName,
|
|
395
|
-
input: applyPrivacyToJson(part.input),
|
|
396
|
-
};
|
|
456
|
+
else {
|
|
457
|
+
const result = await generateText(generationOptions);
|
|
458
|
+
stream = generateResultToFullStream(result);
|
|
397
459
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
460
|
+
for await (const part of stream) {
|
|
461
|
+
rawLog?.writeLine(safeRawStreamJson(part));
|
|
462
|
+
if (part.type === "reasoning-start" || part.type === "reasoning-delta") {
|
|
463
|
+
// Reasoning content remains private. A throttled heartbeat is enough
|
|
464
|
+
// for ChatCCC to distinguish active inference from a stalled stream.
|
|
465
|
+
const now = Date.now();
|
|
466
|
+
if (lastReasoningProgressAt === undefined || now - lastReasoningProgressAt >= 1_000) {
|
|
467
|
+
lastReasoningProgressAt = now;
|
|
468
|
+
yield { type: "progress", phase: "reasoning" };
|
|
469
|
+
}
|
|
403
470
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
471
|
+
else if (part.type === "text-delta") {
|
|
472
|
+
fullText += part.text;
|
|
473
|
+
// 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
|
|
474
|
+
// fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
|
|
475
|
+
const safeText = applyPrivacy(part.text);
|
|
476
|
+
safeAccumulated += safeText;
|
|
477
|
+
yield { type: "text", text: safeText, accumulated: safeAccumulated };
|
|
478
|
+
}
|
|
479
|
+
else if (part.type === "tool-call") {
|
|
480
|
+
toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
|
|
481
|
+
toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
|
|
482
|
+
toolCallOrder.push(part.toolCallId);
|
|
483
|
+
yield {
|
|
484
|
+
type: "tool_use",
|
|
485
|
+
id: part.toolCallId,
|
|
486
|
+
name: part.toolName,
|
|
487
|
+
input: applyPrivacyToJson(part.input),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
else if (part.type === "tool-result") {
|
|
491
|
+
toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
|
|
492
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
493
|
+
if (call)
|
|
494
|
+
call.output = truncateToolContext(safeJson(part.output));
|
|
495
|
+
yield {
|
|
496
|
+
type: "tool_result",
|
|
497
|
+
tool_use_id: part.toolCallId,
|
|
498
|
+
name: part.toolName,
|
|
499
|
+
content: applyPrivacyToJson(part.output),
|
|
500
|
+
is_error: false,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
else if (part.type === "tool-error") {
|
|
504
|
+
const message = errorMessage(part.error);
|
|
505
|
+
toolContext.push(`tool_error ${part.toolName}: ${message}`);
|
|
506
|
+
const call = toolCallsById.get(part.toolCallId);
|
|
507
|
+
if (call) {
|
|
508
|
+
call.output = message;
|
|
509
|
+
call.is_error = true;
|
|
510
|
+
}
|
|
511
|
+
yield {
|
|
512
|
+
type: "tool_result",
|
|
513
|
+
tool_use_id: part.toolCallId,
|
|
514
|
+
name: part.toolName,
|
|
515
|
+
content: applyPrivacy(message),
|
|
516
|
+
is_error: true,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
else if (part.type === "error") {
|
|
520
|
+
const message = errorMessage(part.error);
|
|
521
|
+
yield { type: "error", message: applyPrivacy(message) };
|
|
522
|
+
throw new Error(message);
|
|
419
523
|
}
|
|
420
|
-
yield {
|
|
421
|
-
type: "tool_result",
|
|
422
|
-
tool_use_id: part.toolCallId,
|
|
423
|
-
name: part.toolName,
|
|
424
|
-
content: applyPrivacy(message),
|
|
425
|
-
is_error: true,
|
|
426
|
-
};
|
|
427
524
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
525
|
+
if (hasMalformedToolProtocolText(fullText)) {
|
|
526
|
+
console.warn(`[DeepCCC] malformed DSML tool output detected for ${this.context.sessionId} `
|
|
527
|
+
+ `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
|
|
528
|
+
rawLog?.writeLine(safeRawStreamJson({
|
|
529
|
+
type: "deepccc_tool_protocol_recovery",
|
|
530
|
+
attempt: attempt + 1,
|
|
531
|
+
structuredToolCalls: toolCallOrder.length,
|
|
532
|
+
}));
|
|
533
|
+
yield { type: "text_reset" };
|
|
534
|
+
if (attempt === 0 && toolCallOrder.length === 0) {
|
|
535
|
+
yield { type: "status", phase: "generating" };
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
throw new Error(toolCallOrder.length > 0
|
|
539
|
+
? "工具调用协议异常:检测到混合的结构化与 DSML 文本调用,为避免重复执行工具,本轮已安全终止"
|
|
540
|
+
: "工具调用协议异常:模型重试后仍输出了无效的 DSML 工具调用");
|
|
432
541
|
}
|
|
542
|
+
completed = true;
|
|
543
|
+
const collectedToolCalls = toolCallOrder
|
|
544
|
+
.map((id) => toolCallsById.get(id))
|
|
545
|
+
.filter((call) => call !== undefined);
|
|
546
|
+
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
547
|
+
fullText,
|
|
548
|
+
transcriptLines: toolContext,
|
|
549
|
+
toolCalls: collectedToolCalls,
|
|
550
|
+
}));
|
|
551
|
+
yield { type: "done", text: safeAccumulated };
|
|
552
|
+
return;
|
|
433
553
|
}
|
|
434
|
-
completed = true;
|
|
435
|
-
const collectedToolCalls = toolCallOrder
|
|
436
|
-
.map((id) => toolCallsById.get(id))
|
|
437
|
-
.filter((call) => call !== undefined);
|
|
438
|
-
this.context.appendMessage(buildPersistedAssistantMessage({
|
|
439
|
-
fullText,
|
|
440
|
-
transcriptLines: toolContext,
|
|
441
|
-
toolCalls: collectedToolCalls,
|
|
442
|
-
}));
|
|
443
|
-
yield { type: "done", text: safeAccumulated };
|
|
444
554
|
}
|
|
445
555
|
catch (err) {
|
|
446
556
|
const message = err instanceof Error ? err.message : String(err);
|
|
557
|
+
const malformedProtocolOutput = hasMalformedToolProtocolText(fullText);
|
|
558
|
+
if (malformedProtocolOutput)
|
|
559
|
+
yield { type: "text_reset" };
|
|
447
560
|
if (err.name === "AbortError" || signal?.aborted) {
|
|
448
561
|
// 被中断时,不保存不完整的助手消息
|
|
449
|
-
if (fullText) {
|
|
562
|
+
if (fullText && !malformedProtocolOutput) {
|
|
450
563
|
this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
|
|
451
564
|
}
|
|
452
565
|
yield { type: "done", text: safeAccumulated };
|
|
@@ -500,7 +613,7 @@ export class ChatSession {
|
|
|
500
613
|
if (!plan)
|
|
501
614
|
return 0;
|
|
502
615
|
const result = await generateText({
|
|
503
|
-
model: this.
|
|
616
|
+
model: this.subModel,
|
|
504
617
|
system: SUMMARY_SYSTEM_PROMPT,
|
|
505
618
|
messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
|
|
506
619
|
abortSignal: compactionSignal,
|
|
@@ -57,6 +57,10 @@ export function reduceProgress(prev, event) {
|
|
|
57
57
|
case "text":
|
|
58
58
|
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
59
59
|
return withProgressView(prev, { text: event.accumulated });
|
|
60
|
+
case "progress":
|
|
61
|
+
return withProgressView(prev, { headerTitle: "思考中..." });
|
|
62
|
+
case "text_reset":
|
|
63
|
+
return withProgressView(prev, { text: "", tools: [] });
|
|
60
64
|
case "tool_use": {
|
|
61
65
|
const tool = {
|
|
62
66
|
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects provider output that leaked DeepSeek's internal DSML tool-call syntax
|
|
3
|
+
* into normal assistant text. A trailing closing tag is intentionally required:
|
|
4
|
+
* mentioning DSML in prose or a code sample must not trigger recovery.
|
|
5
|
+
*/
|
|
6
|
+
const MALFORMED_DSML_TRAILER = /<\/[||]{2}DSML[||]{2}(?:parameter|invoke)>\s*$/iu;
|
|
7
|
+
const RENDERED_TOOL_CALL = /\[调用\s+[A-Za-z_][\w.-]*\]/u;
|
|
8
|
+
const DSML_INVOKE_TAG = /<[||]{2}DSML[||]{2}invoke\b/iu;
|
|
9
|
+
export function hasMalformedToolProtocolText(text) {
|
|
10
|
+
return MALFORMED_DSML_TRAILER.test(text)
|
|
11
|
+
&& (RENDERED_TOOL_CALL.test(text) || DSML_INVOKE_TAG.test(text));
|
|
12
|
+
}
|
|
13
|
+
export const TOOL_PROTOCOL_RECOVERY_PROMPT = [
|
|
14
|
+
"[系统恢复提示] 上一次响应泄漏了内部工具调用协议,因此已被丢弃。",
|
|
15
|
+
"请重新完成当前用户请求。需要调用工具时,只能使用 API 提供的结构化工具调用;不要把 DSML 或工具参数作为普通文本输出。",
|
|
16
|
+
"不要提及本恢复提示,也不要假装工具已经执行。",
|
|
17
|
+
].join("\n");
|
|
@@ -27,6 +27,7 @@ export function createCccAdapter(options = {}) {
|
|
|
27
27
|
...(options.provider !== undefined ? { provider: options.provider } : {}),
|
|
28
28
|
...(options.baseURL !== undefined ? { baseURL: options.baseURL } : {}),
|
|
29
29
|
...(options.model !== undefined ? { model: options.model } : {}),
|
|
30
|
+
...(options.subModel !== undefined ? { subModel: options.subModel } : {}),
|
|
30
31
|
...(options.effort !== undefined ? { effort: options.effort } : {}),
|
|
31
32
|
};
|
|
32
33
|
return {
|
|
@@ -54,6 +55,18 @@ export function createCccAdapter(options = {}) {
|
|
|
54
55
|
}],
|
|
55
56
|
};
|
|
56
57
|
}
|
|
58
|
+
else if (event.type === "progress") {
|
|
59
|
+
yield {
|
|
60
|
+
type: "assistant",
|
|
61
|
+
blocks: [{ type: "agent_progress", phase: event.phase }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
else if (event.type === "text_reset") {
|
|
65
|
+
yield {
|
|
66
|
+
type: "assistant",
|
|
67
|
+
blocks: [{ type: "text_reset" }],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
57
70
|
else if (event.type === "text") {
|
|
58
71
|
yield {
|
|
59
72
|
type: "assistant",
|
|
@@ -67,6 +67,10 @@ export function updateAgentActivity(tracker, block, now = Date.now()) {
|
|
|
67
67
|
if (tracker.activeTools.size > 0)
|
|
68
68
|
return false;
|
|
69
69
|
switch (block.type) {
|
|
70
|
+
case "agent_progress":
|
|
71
|
+
return setActivity(tracker, { kind: "thinking", startedAt: now });
|
|
72
|
+
case "text_reset":
|
|
73
|
+
return setActivity(tracker, { kind: "responding", startedAt: now });
|
|
70
74
|
case "agent_status":
|
|
71
75
|
return setActivity(tracker, {
|
|
72
76
|
kind: block.status === "compacting" ? "compacting" : "responding",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Card action helper: parse button click into text command
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { buildUpdateCommandId, extractFeishuEventId } from "./update-command-guard.js";
|
|
5
|
+
/**
|
|
6
|
+
* 卡片按钮 value → 文本命令的映射表。
|
|
7
|
+
*
|
|
8
|
+
* 新增 help / progress 卡片按钮时,必须同步在此登记,否则点击会被
|
|
9
|
+
* parseCardAction 判定为未知 cmd 并静默丢弃(按钮无任何响应)。
|
|
10
|
+
*/
|
|
11
|
+
export const CARD_ACTION_CMD_MAP = {
|
|
12
|
+
stop: "/stop",
|
|
13
|
+
cancel: "/cancel",
|
|
14
|
+
new: "/new",
|
|
15
|
+
"new claude": "/new claude",
|
|
16
|
+
"new cursor": "/new cursor",
|
|
17
|
+
"new codex": "/new codex",
|
|
18
|
+
"new ccc": "/new ccc",
|
|
19
|
+
restart: "/restart",
|
|
20
|
+
update: "/update",
|
|
21
|
+
state: "/state",
|
|
22
|
+
cd: "/cd",
|
|
23
|
+
sessions: "/sessions",
|
|
24
|
+
forget: "/forget",
|
|
25
|
+
};
|
|
26
|
+
/** 把按钮 cmd 映射为文本命令;未登记返回空字符串。 */
|
|
27
|
+
export function cardActionToCommand(cmd) {
|
|
28
|
+
return CARD_ACTION_CMD_MAP[cmd] ?? "";
|
|
29
|
+
}
|
|
30
|
+
export function parseCardAction(data) {
|
|
31
|
+
const raw = data?.event ?? data;
|
|
32
|
+
const action = raw?.action;
|
|
33
|
+
if (!action?.value)
|
|
34
|
+
return null;
|
|
35
|
+
let cmd;
|
|
36
|
+
if (typeof action.value === "object" && action.value !== null) {
|
|
37
|
+
cmd = action.value.action;
|
|
38
|
+
}
|
|
39
|
+
else if (typeof action.value === "string") {
|
|
40
|
+
try {
|
|
41
|
+
let v = JSON.parse(action.value);
|
|
42
|
+
if (typeof v === "string")
|
|
43
|
+
v = JSON.parse(v);
|
|
44
|
+
cmd = v.cmd ?? v.action;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!cmd)
|
|
51
|
+
return null;
|
|
52
|
+
let text = cardActionToCommand(cmd);
|
|
53
|
+
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
54
|
+
const path = action.value.path;
|
|
55
|
+
if (path)
|
|
56
|
+
text = `/cd ${path}`;
|
|
57
|
+
}
|
|
58
|
+
// cmd 本身就是以 / 开头的完整指令时,直接使用(如 /model <name> 动态按钮)
|
|
59
|
+
if (!text && cmd.startsWith("/"))
|
|
60
|
+
text = cmd;
|
|
61
|
+
if (!text)
|
|
62
|
+
return null;
|
|
63
|
+
const chatId = raw.open_chat_id ??
|
|
64
|
+
raw.context?.open_chat_id ??
|
|
65
|
+
raw.message?.chat_id ??
|
|
66
|
+
"";
|
|
67
|
+
const openId = raw.operator?.open_id ??
|
|
68
|
+
"";
|
|
69
|
+
return {
|
|
70
|
+
text,
|
|
71
|
+
chatId,
|
|
72
|
+
openId,
|
|
73
|
+
commandId: buildUpdateCommandId("card", extractFeishuEventId(data)),
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/src/config.js
CHANGED
|
@@ -309,6 +309,7 @@ function loadConfig() {
|
|
|
309
309
|
DEEPSEEK_API_KEY: "",
|
|
310
310
|
DEEPSEEK_BASE_URL: DEFAULT_CCC_DEEPSEEK_BASE_URL,
|
|
311
311
|
model: DEFAULT_CCC_MODEL,
|
|
312
|
+
subModel: "",
|
|
312
313
|
alternativeModel: "",
|
|
313
314
|
effort: "",
|
|
314
315
|
provider: "",
|
|
@@ -501,6 +502,7 @@ function loadConfig() {
|
|
|
501
502
|
fallback: DEFAULT_CCC_DEEPSEEK_BASE_URL,
|
|
502
503
|
}),
|
|
503
504
|
model: normalizeOptionalConfigField(cccRaw.model, { label: "ccc.model", fallback: DEFAULT_CCC_MODEL }),
|
|
505
|
+
subModel: normalizeOptionalConfigField(cccRaw.subModel, { label: "ccc.subModel" }),
|
|
504
506
|
alternativeModel: normalizeOptionalConfigField(cccRaw.alternativeModel, { label: "ccc.alternativeModel" }),
|
|
505
507
|
effort: normalizeOptionalConfigField(cccRaw.effort, { label: "ccc.effort" }),
|
|
506
508
|
provider: normalizeCccProviderOverride(cccRaw.provider),
|
package/dist/src/index.js
CHANGED
|
@@ -113,54 +113,8 @@ function getInnerEvent(data) {
|
|
|
113
113
|
return (data.event ?? data);
|
|
114
114
|
}
|
|
115
115
|
import { formatMessageContent } from "./format-message.js";
|
|
116
|
-
import { buildUpdateCommandId
|
|
117
|
-
|
|
118
|
-
const raw = data?.event ?? data;
|
|
119
|
-
const action = raw?.action;
|
|
120
|
-
if (!action?.value)
|
|
121
|
-
return null;
|
|
122
|
-
let cmd;
|
|
123
|
-
if (typeof action.value === "object" && action.value !== null) {
|
|
124
|
-
cmd = action.value.action;
|
|
125
|
-
}
|
|
126
|
-
else if (typeof action.value === "string") {
|
|
127
|
-
try {
|
|
128
|
-
let v = JSON.parse(action.value);
|
|
129
|
-
if (typeof v === "string")
|
|
130
|
-
v = JSON.parse(v);
|
|
131
|
-
cmd = v.cmd ?? v.action;
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (!cmd)
|
|
138
|
-
return null;
|
|
139
|
-
const CMD_MAP = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", update: "/update", state: "/state", cd: "/cd", sessions: "/sessions", forget: "/forget" };
|
|
140
|
-
let text = CMD_MAP[cmd] ?? "";
|
|
141
|
-
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
142
|
-
const path = action.value.path;
|
|
143
|
-
if (path)
|
|
144
|
-
text = `/cd ${path}`;
|
|
145
|
-
}
|
|
146
|
-
// cmd 本身就是以 / 开头的完整指令时,直接使用(如 /model <name> 动态按钮)
|
|
147
|
-
if (!text && cmd.startsWith("/"))
|
|
148
|
-
text = cmd;
|
|
149
|
-
if (!text)
|
|
150
|
-
return null;
|
|
151
|
-
const chatId = raw.open_chat_id ??
|
|
152
|
-
raw.context?.open_chat_id ??
|
|
153
|
-
raw.message?.chat_id ??
|
|
154
|
-
"";
|
|
155
|
-
const openId = raw.operator?.open_id ??
|
|
156
|
-
"";
|
|
157
|
-
return {
|
|
158
|
-
text,
|
|
159
|
-
chatId,
|
|
160
|
-
openId,
|
|
161
|
-
commandId: buildUpdateCommandId("card", extractFeishuEventId(data)),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
116
|
+
import { buildUpdateCommandId } from "./update-command-guard.js";
|
|
117
|
+
import { parseCardAction } from "./card-action-parser.js";
|
|
164
118
|
// ---------------------------------------------------------------------------
|
|
165
119
|
// WebSocket relay broadcast
|
|
166
120
|
// ---------------------------------------------------------------------------
|
|
@@ -57,6 +57,10 @@ export function reduceProgress(prev, event) {
|
|
|
57
57
|
case "text":
|
|
58
58
|
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
59
59
|
return withProgressView(prev, { text: event.accumulated });
|
|
60
|
+
case "progress":
|
|
61
|
+
return withProgressView(prev, { headerTitle: "思考中..." });
|
|
62
|
+
case "text_reset":
|
|
63
|
+
return withProgressView(prev, { text: "", tools: [] });
|
|
60
64
|
case "tool_use": {
|
|
61
65
|
const tool = {
|
|
62
66
|
id: event.id ?? `tool-${prev.tools.length + 1}`,
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Tracks how long the displayed output character count has remained unchanged.
|
|
3
3
|
* Leaving a monitored phase clears the window; returning starts a fresh one.
|
|
4
4
|
*/
|
|
5
|
-
export function observeResponseProgress(previous, isMonitoredPhase, totalChars, now = Date.now()) {
|
|
5
|
+
export function observeResponseProgress(previous, isMonitoredPhase, totalChars, now = Date.now(), heartbeat = false) {
|
|
6
6
|
if (!isMonitoredPhase)
|
|
7
7
|
return undefined;
|
|
8
|
-
if (previous?.totalChars === totalChars)
|
|
8
|
+
if (!heartbeat && previous?.totalChars === totalChars)
|
|
9
9
|
return previous;
|
|
10
10
|
return { totalChars, unchangedSince: now };
|
|
11
11
|
}
|
package/dist/src/session.js
CHANGED
|
@@ -233,7 +233,7 @@ function formatAutoEndedReply(finalReply) {
|
|
|
233
233
|
* 状态或资源保护,不能把它们消耗的时间算入回复停滞窗口。
|
|
234
234
|
*/
|
|
235
235
|
function monitorsOutputProgress(kind) {
|
|
236
|
-
return kind === "responding";
|
|
236
|
+
return kind === "responding" || kind === "thinking";
|
|
237
237
|
}
|
|
238
238
|
function formatTerminalReply(status, finalReply, terminalError) {
|
|
239
239
|
if (status === "auto_ended")
|
|
@@ -528,11 +528,12 @@ export function getAdapterForTool(tool, sessionId) {
|
|
|
528
528
|
apiKey: config.ccc.DEEPSEEK_API_KEY,
|
|
529
529
|
baseURL: config.ccc.DEEPSEEK_BASE_URL,
|
|
530
530
|
model: effectiveModel || undefined,
|
|
531
|
-
effort: effectiveEffort || undefined,
|
|
532
531
|
compactionTimeoutMs: config.ccc.compactionTimeoutMs,
|
|
533
532
|
contextWindow: config.ccc.contextWindow,
|
|
533
|
+
...(effectiveEffort ? { effort: effectiveEffort } : {}),
|
|
534
534
|
// 留空("")不传 → ChatSession 跟随 DeepCCC 内核配置(~/.deepccc/config.json 或 DEEPCCC_PROVIDER)
|
|
535
535
|
...(config.ccc.provider ? { provider: config.ccc.provider } : {}),
|
|
536
|
+
...(config.ccc.subModel ? { subModel: config.ccc.subModel } : {}),
|
|
536
537
|
});
|
|
537
538
|
}
|
|
538
539
|
else {
|
|
@@ -760,6 +761,14 @@ export function accumulateBlockContent(block, state, toolCallMap) {
|
|
|
760
761
|
// 覆盖而非追加:适配器已保证这是一段完整最终文本(如 Cursor 流末快照)
|
|
761
762
|
state.finalCompleteText = block.text;
|
|
762
763
|
break;
|
|
764
|
+
case "text_reset":
|
|
765
|
+
state.accumulatedContent = "";
|
|
766
|
+
state.finalText = "";
|
|
767
|
+
state.finalCompleteText = "";
|
|
768
|
+
state.chunkCount = 0;
|
|
769
|
+
break;
|
|
770
|
+
case "agent_progress":
|
|
771
|
+
break;
|
|
763
772
|
case "compact_boundary": {
|
|
764
773
|
const triggerLabel = block.trigger === "manual" ? "手动" : "自动"; // 手动 / 自动
|
|
765
774
|
state.accumulatedContent +=
|
|
@@ -1237,7 +1246,15 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1237
1246
|
}
|
|
1238
1247
|
}
|
|
1239
1248
|
let activityChanged = false;
|
|
1249
|
+
let progressHeartbeat = false;
|
|
1250
|
+
let outputReset = false;
|
|
1240
1251
|
for (const block of unifiedMsg.blocks) {
|
|
1252
|
+
if (block.type === "agent_progress")
|
|
1253
|
+
progressHeartbeat = true;
|
|
1254
|
+
if (block.type === "text_reset") {
|
|
1255
|
+
outputReset = true;
|
|
1256
|
+
toolCallMap.clear();
|
|
1257
|
+
}
|
|
1241
1258
|
if (updateAgentActivity(activityTracker, block))
|
|
1242
1259
|
activityChanged = true;
|
|
1243
1260
|
accumulateBlockContent(block, state, toolCallMap);
|
|
@@ -1262,11 +1279,11 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1262
1279
|
prompt.responseProgress = observeResponseProgress(
|
|
1263
1280
|
// starting → responding 本身是一次有效进展,即便首个文本块仍为空也应
|
|
1264
1281
|
// 重新计时;其它活动阶段会清空观察窗口。
|
|
1265
|
-
activityChanged ? undefined : prompt.responseProgress, monitorsOutputProgress(activityTracker.activity.kind), totalChars, Date.now());
|
|
1282
|
+
activityChanged ? undefined : prompt.responseProgress, monitorsOutputProgress(activityTracker.activity.kind), totalChars, Date.now(), progressHeartbeat);
|
|
1266
1283
|
}
|
|
1267
1284
|
// 定时写入文件
|
|
1268
1285
|
const now2 = Date.now();
|
|
1269
|
-
if (activityChanged || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1286
|
+
if (activityChanged || outputReset || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1270
1287
|
lastFileWrite = now2;
|
|
1271
1288
|
await writeStreamState({
|
|
1272
1289
|
sessionId,
|
package/dist/src/web-ui.js
CHANGED
|
@@ -483,6 +483,10 @@ export function unflattenConfig(flat) {
|
|
|
483
483
|
result.ccc = result.ccc || {};
|
|
484
484
|
result.ccc.model = val;
|
|
485
485
|
}
|
|
486
|
+
else if (key === "CHATCCC_CCC_SUB_MODEL") {
|
|
487
|
+
result.ccc = result.ccc || {};
|
|
488
|
+
result.ccc.subModel = val;
|
|
489
|
+
}
|
|
486
490
|
else if (key === "CHATCCC_CCC_ALTERNATIVE_MODEL") {
|
|
487
491
|
result.ccc = result.ccc || {};
|
|
488
492
|
result.ccc.alternativeModel = val;
|
|
@@ -864,6 +868,10 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
864
868
|
<label>模型</label>
|
|
865
869
|
<input type="text" id="field-CHATCCC_CCC_MODEL" placeholder="deepseek-v4-pro">
|
|
866
870
|
</div>
|
|
871
|
+
<div class="form-group">
|
|
872
|
+
<label>子模型(选填)</label>
|
|
873
|
+
<input type="text" id="field-CHATCCC_CCC_SUB_MODEL" placeholder="留空跟随主模型;用于压缩摘要、子代理任务等轻量环节">
|
|
874
|
+
</div>
|
|
867
875
|
<div class="form-group">
|
|
868
876
|
<label>备选模型(选填)</label>
|
|
869
877
|
<input type="text" id="field-CHATCCC_CCC_ALTERNATIVE_MODEL" placeholder="加入 /model 列表,便于会话内切换">
|
|
@@ -1178,6 +1186,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
1178
1186
|
<div class="config-row"><span class="key">Base URL</span><span class="val" id="cfg-CCC_BASE_URL">-</span></div>
|
|
1179
1187
|
<div class="config-row"><span class="key">API 协议</span><span class="val" id="cfg-CCC_PROVIDER">-</span></div>
|
|
1180
1188
|
<div class="config-row"><span class="key">模型</span><span class="val" id="cfg-CCC_MODEL">-</span></div>
|
|
1189
|
+
<div class="config-row"><span class="key">子模型</span><span class="val" id="cfg-CCC_SUB_MODEL">-</span></div>
|
|
1181
1190
|
<div class="config-row"><span class="key">备选模型</span><span class="val" id="cfg-CCC_ALTERNATIVE_MODEL">-</span></div>
|
|
1182
1191
|
<label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-ccc" onchange="setDashboardDefaultAgent('ccc', this.checked)"> 设为默认 Agent</label>
|
|
1183
1192
|
<div class="hint" style="margin-top:6px;line-height:1.6">备选模型仅加入 /model 人工切换列表;保存后下一条消息或下个新会话生效。</div>
|
|
@@ -1229,7 +1238,7 @@ const AGENT_FIELDS = {
|
|
|
1229
1238
|
claude: ['CHATCCC_ANTHROPIC_MODEL','CHATCCC_ANTHROPIC_SUBAGENT_MODEL','CHATCCC_ANTHROPIC_EFFORT','CHATCCC_ANTHROPIC_API_KEY','CHATCCC_ANTHROPIC_BASE_URL','CHATCCC_ANTHROPIC_MAX_TURN'],
|
|
1230
1239
|
cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL','CHATCCC_CURSOR_ALTERNATIVE_MODEL','CHATCCC_CURSOR_AVATAR_BATTERY_MODE','CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET'],
|
|
1231
1240
|
codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_ALTERNATIVE_MODEL','CHATCCC_CODEX_EFFORT','CHATCCC_CODEX_FAST_MODE'],
|
|
1232
|
-
ccc: ['CHATCCC_CCC_API_KEY','CHATCCC_CCC_BASE_URL','CHATCCC_CCC_MODEL','CHATCCC_CCC_ALTERNATIVE_MODEL','CHATCCC_CCC_EFFORT','CHATCCC_CCC_PROVIDER','CHATCCC_CCC_CONTEXT_WINDOW']
|
|
1241
|
+
ccc: ['CHATCCC_CCC_API_KEY','CHATCCC_CCC_BASE_URL','CHATCCC_CCC_MODEL','CHATCCC_CCC_SUB_MODEL','CHATCCC_CCC_ALTERNATIVE_MODEL','CHATCCC_CCC_EFFORT','CHATCCC_CCC_PROVIDER','CHATCCC_CCC_CONTEXT_WINDOW']
|
|
1233
1242
|
};
|
|
1234
1243
|
const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
|
|
1235
1244
|
const WEB_UI_FIELDS = ['CHATCCC_WEB_UI_OPEN_ON_START'];
|
|
@@ -1616,6 +1625,7 @@ function renderStep2() {
|
|
|
1616
1625
|
prefillNested('field-CHATCCC_CCC_BASE_URL', c.ccc.DEEPSEEK_BASE_URL);
|
|
1617
1626
|
prefillNested('field-CHATCCC_CCC_PROVIDER', c.ccc.provider);
|
|
1618
1627
|
prefillNested('field-CHATCCC_CCC_MODEL', c.ccc.model);
|
|
1628
|
+
prefillNested('field-CHATCCC_CCC_SUB_MODEL', c.ccc.subModel);
|
|
1619
1629
|
prefillNested('field-CHATCCC_CCC_ALTERNATIVE_MODEL', c.ccc.alternativeModel);
|
|
1620
1630
|
prefillNested('field-CHATCCC_CCC_EFFORT', c.ccc.effort);
|
|
1621
1631
|
prefillContextWindow('field-', c.ccc.contextWindow);
|
|
@@ -1802,6 +1812,7 @@ function renderStep3() {
|
|
|
1802
1812
|
lines.push('<div class="config-row"><span class="key">API Key</span><span class="val">' + (vars.CHATCCC_CCC_API_KEY ? '***已设置***' : '(留空)') + '</span></div>');
|
|
1803
1813
|
lines.push('<div class="config-row"><span class="key">Base URL</span><span class="val">' + (vars.CHATCCC_CCC_BASE_URL || '(留空)') + '</span></div>');
|
|
1804
1814
|
lines.push('<div class="config-row"><span class="key">模型</span><span class="val">' + (vars.CHATCCC_CCC_MODEL || '(留空)') + '</span></div>');
|
|
1815
|
+
lines.push('<div class="config-row"><span class="key">子模型</span><span class="val">' + (vars.CHATCCC_CCC_SUB_MODEL || '(留空,跟随主模型)') + '</span></div>');
|
|
1805
1816
|
lines.push('<div class="config-row"><span class="key">备选模型</span><span class="val">' + (vars.CHATCCC_CCC_ALTERNATIVE_MODEL || '(留空)') + '</span></div>');
|
|
1806
1817
|
lines.push('<div class="config-row"><span class="key">Effort</span><span class="val">' + (vars.CHATCCC_CCC_EFFORT || '(留空)') + '</span></div>');
|
|
1807
1818
|
lines.push('<div class="config-row"><span class="key">上下文窗口</span><span class="val">' + contextWindowTokensLabel(vars.CHATCCC_CCC_CONTEXT_WINDOW || 1048576) + '</span></div>');
|
|
@@ -2023,6 +2034,7 @@ function updateDashboardUI() {
|
|
|
2023
2034
|
document.getElementById('cfg-CCC_BASE_URL').textContent = (c.ccc && c.ccc.DEEPSEEK_BASE_URL) || '(留空)';
|
|
2024
2035
|
document.getElementById('cfg-CCC_PROVIDER').textContent = (c.ccc && c.ccc.provider) ? c.ccc.provider : '(跟随 DeepCCC 内核配置)';
|
|
2025
2036
|
document.getElementById('cfg-CCC_MODEL').textContent = (c.ccc && c.ccc.model) || '(留空)';
|
|
2037
|
+
document.getElementById('cfg-CCC_SUB_MODEL').textContent = (c.ccc && c.ccc.subModel) || '(留空,跟随主模型)';
|
|
2026
2038
|
document.getElementById('cfg-CCC_ALTERNATIVE_MODEL').textContent = (c.ccc && c.ccc.alternativeModel) || '(留空)';
|
|
2027
2039
|
}
|
|
2028
2040
|
|
|
@@ -2100,7 +2112,7 @@ function editSection(section) {
|
|
|
2100
2112
|
'CHATCCC_CODEX_FAST_MODE': 'Fast 模式',
|
|
2101
2113
|
'CHATCCC_CCC_API_KEY': 'API Key', 'CHATCCC_CCC_BASE_URL': 'Base URL',
|
|
2102
2114
|
'CHATCCC_CCC_PROVIDER': 'API 协议(选填)',
|
|
2103
|
-
'CHATCCC_CCC_MODEL': '模型', 'CHATCCC_CCC_ALTERNATIVE_MODEL': '备选模型', 'CHATCCC_CCC_EFFORT': 'Effort', 'CHATCCC_CCC_CONTEXT_WINDOW': '上下文窗口'
|
|
2115
|
+
'CHATCCC_CCC_MODEL': '模型', 'CHATCCC_CCC_SUB_MODEL': '子模型', 'CHATCCC_CCC_ALTERNATIVE_MODEL': '备选模型', 'CHATCCC_CCC_EFFORT': 'Effort', 'CHATCCC_CCC_CONTEXT_WINDOW': '上下文窗口'
|
|
2104
2116
|
};
|
|
2105
2117
|
var hintMap = {
|
|
2106
2118
|
'CHATCCC_WEB_UI_OPEN_ON_START': '关闭后可继续手动访问 http://localhost:<端口>/;/restart、/update 和 Web UI 重启无论此项为何值都不会自动打开。',
|
|
@@ -2108,7 +2120,8 @@ function editSection(section) {
|
|
|
2108
2120
|
'CHATCCC_CHROME_DEVTOOLS_PORT': '默认 15166,健康检查端点为 http://127.0.0.1:15166/json/version。',
|
|
2109
2121
|
'CHATCCC_CHROME_DEVTOOLS_PATH': '选填。留空时自动探测 Google Chrome。',
|
|
2110
2122
|
'CHATCCC_CCC_PROVIDER': '与 Base URL 强相关:OpenAI 兼容端点选 openai;Anthropic Messages 端点选 anthropic。留空 = 跟随 DeepCCC 内核配置(~/.deepccc/config.json 或 DEEPCCC_PROVIDER),改动需重启 ChatCCC 生效。',
|
|
2111
|
-
'CHATCCC_CCC_CONTEXT_WINDOW': '压缩阈值自动 = 窗口 × 80%(超出即把较早消息压缩为摘要)。⚠️ 超过模型/服务端实际上限时请求会被 API 直接拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 代理的 max_input_tokens);单位 k = 1024 tokens,1M = 1,048,576 tokens。'
|
|
2123
|
+
'CHATCCC_CCC_CONTEXT_WINDOW': '压缩阈值自动 = 窗口 × 80%(超出即把较早消息压缩为摘要)。⚠️ 超过模型/服务端实际上限时请求会被 API 直接拒绝(context length exceeded),实际窗口以模型与所用服务端为准(如 litellm 代理的 max_input_tokens);单位 k = 1024 tokens,1M = 1,048,576 tokens。',
|
|
2124
|
+
'CHATCCC_CCC_SUB_MODEL': '用于 DeepCCC 内部轻量环节(上下文压缩摘要生成、task 子代理任务)。留空 = 跟随主模型;改动需重启 ChatCCC 生效。'
|
|
2112
2125
|
};
|
|
2113
2126
|
|
|
2114
2127
|
if (section === 'chromeDevtools') {
|
|
@@ -2152,6 +2165,7 @@ function editSection(section) {
|
|
|
2152
2165
|
else if (key === 'CHATCCC_CCC_BASE_URL') val = state.config.ccc.DEEPSEEK_BASE_URL || '';
|
|
2153
2166
|
else if (key === 'CHATCCC_CCC_PROVIDER') val = state.config.ccc.provider || '';
|
|
2154
2167
|
else if (key === 'CHATCCC_CCC_MODEL') val = state.config.ccc.model || '';
|
|
2168
|
+
else if (key === 'CHATCCC_CCC_SUB_MODEL') val = state.config.ccc.subModel || '';
|
|
2155
2169
|
else if (key === 'CHATCCC_CCC_ALTERNATIVE_MODEL') val = state.config.ccc.alternativeModel || '';
|
|
2156
2170
|
else if (key === 'CHATCCC_CCC_EFFORT') val = state.config.ccc.effort || '';
|
|
2157
2171
|
else if (key === 'CHATCCC_CCC_CONTEXT_WINDOW') val = state.config.ccc.contextWindow || '1048576';
|