chatccc 0.2.256 → 0.2.258

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 CHANGED
@@ -157,7 +157,7 @@ chatccc
157
157
 
158
158
  Agent Team 提供本地任务看板和项目主 Agent 入口,但暂不根据任务自动分配 Agent。每个规范化后的本机目录对应一个独立项目和看板,固定包含“头脑风暴、Todo、Doing、Done、搁置”五列,支持新增、编辑、拖动排序和删除任务。页面会自动保存,不需要手动提交。当前按路径识别项目,不检查 Git 仓库或 worktree 关系,任意子目录都可以作为独立项目。
159
159
 
160
- 打开项目后可以选择 CCC、Claude、Cursor 或 Codex 作为主 Agent。首次设置会创建固定命名为 `主Agent-<目录短名>` 的飞书群和空 Agent Session;后续切换 Agent 或重新关联目录会复用原群,运行中的主 Agent 不允许切换。建群成员取自机器人最近一次收到的飞书私聊;如果尚无私聊记录,网页会提示先给机器人发送任意私聊消息并自动检测。项目群名不会随第一句话或 `/newh` 改变。
160
+ 打开项目后可以选择 CCC、Claude、Cursor 或 Codex 作为主 Agent。首次设置会创建固定命名为 `主Agent-<目录短名>` 的飞书群和空 Agent Session;后续切换 Agent 或重新关联目录会复用原群,运行中的主 Agent 不允许切换。建群成员取自机器人最近一次收到的飞书私聊;如果尚无私聊记录,网页会提示先给机器人发送任意私聊消息并自动检测。项目群名不会随第一句话或 `/forget` 改变。
161
161
 
162
162
  看板数据默认保存在 `~/.chatccc/agent-team/`,其中 `workspaces.json` 保存最近工作目录索引,`boards/` 保存按稳定 ID 分隔的看板 JSON,`main-agent-bindings/` 单独保存本机飞书群与 Session 绑定。目录移动或重命名后,可从最近目录列表重新关联。数据访问通过仓储接口隔离,后续接入飞书多维表格时可增加双向同步适配器,无需把本机群聊和 Session 状态混入任务同步模型。选择目录、创建群聊和启动 Agent 等操作都由本机 Node 后端执行;当前实现不依赖 Electron。
163
163
 
@@ -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
 
@@ -396,7 +397,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
396
397
  | `/new cursor` | 创建 Cursor 会话;飞书中会创建新群 |
397
398
  | `/new codex` | 创建 Codex 会话;飞书中会创建新群 |
398
399
  | `/new ccc` | 创建内置 CCC Agent 会话;飞书中会创建新群 |
399
- | `/newh` | 在当前聊天原地重置会话;群聊保留工作目录,飞书私聊固定使用系统用户目录 |
400
+ | `/forget` | 在当前聊天原地重置会话(忘掉上下文);群聊保留工作目录,飞书私聊固定使用系统用户目录 |
400
401
  | `/model` | 查看或切换当前会话的模型 |
401
402
  | `/fast` | 查看当前 Codex 会话的 Fast 模式;使用 `/fast on` 或 `/fast off` 切换 |
402
403
  | `/stop` | 停止当前回复 |
@@ -50,6 +50,7 @@
50
50
  "DEEPSEEK_API_KEY": "",
51
51
  "DEEPSEEK_BASE_URL": "https://api.deepseek.com/v1",
52
52
  "model": "deepseek-v4-pro",
53
+ "subModel": "",
53
54
  "alternativeModel": "",
54
55
  "effort": "",
55
56
  "provider": "",
@@ -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` 工具在会话被压缩后找回被压缩消息的精确原文(检索时设置
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -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,
@@ -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,12 +8,12 @@ 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
18
  import { buildDefaultSkillDirs, buildSkillsIndexPrompt, scanSkillsDirs, } from "./skills.js";
19
19
  import { applyPrivacy, applyPrivacyToJson } from "./privacy.js";
@@ -76,6 +76,15 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
76
76
  ].join("\n");
77
77
  export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
78
78
  const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
79
+ /** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
80
+ const TASK_MAX_STEPS = 20;
81
+ /** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
82
+ function truncateTaskOutput(text) {
83
+ if (text.length <= MAX_TASK_OUTPUT_CHARS)
84
+ return text;
85
+ return (text.slice(0, MAX_TASK_OUTPUT_CHARS) +
86
+ `\n…[子代理输出已截断,共 ${text.length} 字符,仅保留前 ${MAX_TASK_OUTPUT_CHARS} 字符]`);
87
+ }
79
88
  const ANTHROPIC_TOOL_JSON_COMPATIBILITY_NOTE = [
80
89
  "[Protocol compatibility note]",
81
90
  "tool-call arguments use JSON encoding; the final reply does not need to be JSON unless the user requests it.",
@@ -217,17 +226,71 @@ function normalizeAnthropicBaseURL(baseURL) {
217
226
  }
218
227
  export class ChatSession {
219
228
  model;
229
+ /** 子模型实例;未配置 subModel 时与主模型同一实例 */
230
+ subModel;
220
231
  provider;
232
+ apiKey;
233
+ baseURL;
234
+ modelId;
235
+ subModelId;
221
236
  cwd;
222
237
  context;
223
238
  compactionTimeoutMs;
224
239
  maxSteps;
225
240
  effort;
241
+ permissionMode;
242
+ permissionResolver;
226
243
  permissionGate;
227
244
  skillDirs;
228
245
  customSystemPrompt;
229
246
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
230
247
  systemPrompt = "";
248
+ /**
249
+ * task 子代理工具执行器:用子模型开独立子会话(独立上下文、独立 cwd)执行子任务,
250
+ * 回传最终文本(截断 + 隐私替换)。单层:子会话的工具集不包含 runTask,天然禁止嵌套。
251
+ */
252
+ runTask = async (input, signal) => {
253
+ const rawCwd = input.cwd?.trim();
254
+ const taskCwd = rawCwd ? (isAbsolute(rawCwd) ? rawCwd : resolve(this.cwd, rawCwd)) : this.cwd;
255
+ const timeoutController = new AbortController();
256
+ const timeout = setTimeout(() => timeoutController.abort(), this.compactionTimeoutMs);
257
+ timeout.unref?.();
258
+ const taskSignal = signal
259
+ ? AbortSignal.any([signal, timeoutController.signal])
260
+ : timeoutController.signal;
261
+ try {
262
+ const child = new ChatSession({
263
+ provider: this.provider,
264
+ apiKey: this.apiKey,
265
+ baseURL: this.baseURL,
266
+ model: this.subModelId || this.modelId,
267
+ effort: this.effort,
268
+ }, {
269
+ cwd: taskCwd,
270
+ persist: false,
271
+ permissionMode: this.permissionMode,
272
+ permissionResolver: this.permissionResolver,
273
+ maxSteps: TASK_MAX_STEPS,
274
+ compactionTimeoutMs: this.compactionTimeoutMs,
275
+ skillsDirs: this.skillDirs.map((s) => s.dir),
276
+ });
277
+ let full = "";
278
+ for await (const event of child.chat(input.description, taskSignal)) {
279
+ if (event.type === "text") {
280
+ full += event.text;
281
+ }
282
+ else if (event.type === "error") {
283
+ throw new Error(`task 子代理执行失败: ${event.message}`);
284
+ }
285
+ }
286
+ if (!full.trim())
287
+ return "(子代理未返回文本内容)";
288
+ return applyPrivacy(truncateTaskOutput(full));
289
+ }
290
+ finally {
291
+ clearTimeout(timeout);
292
+ }
293
+ };
231
294
  constructor(overrides = {}, options = {}) {
232
295
  const apiKey = overrides.apiKey ?? appConfig.apiKey;
233
296
  if (!apiKey) {
@@ -235,28 +298,32 @@ export class ChatSession {
235
298
  }
236
299
  const baseURL = overrides.baseURL ?? appConfig.baseURL;
237
300
  const modelId = overrides.model ?? appConfig.model;
301
+ this.modelId = modelId;
302
+ this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
238
303
  this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
239
304
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
240
- if (this.provider === "anthropic") {
241
- const provider = createAnthropic({
305
+ this.apiKey = apiKey;
306
+ this.baseURL = baseURL;
307
+ const provider = this.provider === "anthropic"
308
+ ? createAnthropic({
242
309
  baseURL: normalizeAnthropicBaseURL(baseURL),
243
310
  apiKey,
244
- });
245
- this.model = provider(modelId);
246
- }
247
- else {
248
- const provider = createOpenAICompatible({
311
+ })
312
+ : createOpenAICompatible({
249
313
  name: "deepccc",
250
314
  baseURL,
251
315
  apiKey,
252
316
  includeUsage: true,
253
317
  });
254
- this.model = provider(modelId);
255
- }
318
+ this.model = provider(modelId);
319
+ // 子模型:留空时与主模型共用同一实例(行为与旧版完全一致,零开销)
320
+ this.subModel = this.subModelId ? provider(this.subModelId) : this.model;
256
321
  this.cwd = options.cwd ?? process.cwd();
257
322
  this.maxSteps = normalizeMaxSteps(options.maxSteps);
258
323
  this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
259
324
  this.customSystemPrompt = options.systemPrompt ?? "";
325
+ this.permissionMode = options.permissionMode ?? "ask";
326
+ this.permissionResolver = options.permissionResolver;
260
327
  // 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
261
328
  // 因此创建/修改技能后下一次对话自动生效,无需重启。
262
329
  this.skillDirs =
@@ -271,7 +338,7 @@ export class ChatSession {
271
338
  compactAtTokens: options.compactAtTokens,
272
339
  keepRecentMessages: options.keepRecentMessages,
273
340
  });
274
- this.permissionGate = new PermissionGate(options.permissionMode ?? "ask", options.permissionResolver);
341
+ this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
275
342
  }
276
343
  /**
277
344
  * 组装系统提示词。顺序遵循“稳定性优先”原则(缓存命中友好):
@@ -360,7 +427,10 @@ export class ChatSession {
360
427
  model: this.model,
361
428
  system,
362
429
  messages: modelMessages,
363
- tools: createBuiltinFileTools(this.cwd, { permissionGate: this.permissionGate }),
430
+ tools: createBuiltinFileTools(this.cwd, {
431
+ permissionGate: this.permissionGate,
432
+ runTask: this.runTask,
433
+ }),
364
434
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
365
435
  abortSignal: signal,
366
436
  ...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
@@ -500,7 +570,7 @@ export class ChatSession {
500
570
  if (!plan)
501
571
  return 0;
502
572
  const result = await generateText({
503
- model: this.model,
573
+ model: this.subModel,
504
574
  system: SUMMARY_SYSTEM_PROMPT,
505
575
  messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
506
576
  abortSignal: compactionSignal,
@@ -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 {
@@ -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/cards.js CHANGED
@@ -137,7 +137,7 @@ export function buildHelpCard(userText, opts = {}) {
137
137
  "发送 **/new cursor** 创建新 Cursor 会话",
138
138
  "发送 **/new codex** 创建新 Codex 会话",
139
139
  "发送 **/new ccc** 创建新 CCC Agent 会话",
140
- "发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
140
+ "发送 **/forget** 重置当前会话(忘掉上下文,沿用当前工作目录,不切换)",
141
141
  "发送 **/plan** 以规划模式提问(只读,不执行写操作)",
142
142
  "发送 **/ask** 以问答模式提问(只读,不执行写操作)",
143
143
  "发送 **/usage** 查看当前 Agent 的用量或余额",
@@ -329,8 +329,8 @@ export function buildSessionsCard(sessions, opts = {}) {
329
329
  { tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
330
330
  { tag: "hr" },
331
331
  { tag: "div", text: { tag: "lark_md", content: fixedPrivateSession
332
- ? "当前飞书私聊使用专属会话;默认 Agent 变化后,下一条普通消息会自动创建对应 Agent 的新空会话。发送 **/newh** 可在私聊中原地重置。群聊会话请回到对应群聊继续,私聊不支持 **/session** 切换。"
333
- : "在会话群内发送 **/newh** 可重置当前会话(创建新 Session,保留工作目录和群聊)。\n发送 **/session 数字**(如 `/session 1`)可将当前群聊切换到列表中对应编号的会话。" } },
332
+ ? "当前飞书私聊使用专属会话;默认 Agent 变化后,下一条普通消息会自动创建对应 Agent 的新空会话。发送 **/forget** 可在私聊中原地重置(忘掉当前上下文)。群聊会话请回到对应群聊继续,私聊不支持 **/session** 切换。"
333
+ : "在会话群内发送 **/forget** 可重置当前会话(忘掉上下文,创建新 Session,保留工作目录和群聊)。\n发送 **/session 数字**(如 `/session 1`)可将当前群聊切换到列表中对应编号的会话。" } },
334
334
  { tag: "hr" },
335
335
  {
336
336
  tag: "action",
@@ -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, extractFeishuEventId, } from "./update-command-guard.js";
117
- function parseCardAction(data) {
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", newh: "/newh" };
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
  // ---------------------------------------------------------------------------
@@ -1021,7 +1021,7 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
1021
1021
  // /new 始终创建独立群聊。飞书私聊自己的常驻 session 不在这里切换。
1022
1022
  if (chatType === "p2p" && platform.kind === "wechat") {
1023
1023
  // 先解绑旧 session(如果存在),避免旧 session 的 display loop
1024
- // 继续往同一个 chat 推送内容(/newh 走 switchChatBinding 已有此逻辑,
1024
+ // 继续往同一个 chat 推送内容(/forget 走 switchChatBinding 已有此逻辑,
1025
1025
  // 但 /new p2p 之前遗漏了解绑)。
1026
1026
  const oldRegistry = await loadSessionRegistryForBinding();
1027
1027
  const oldRecord = oldRegistry[chatId];
@@ -1056,7 +1056,7 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
1056
1056
  `直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
1057
1057
  `发送 **/cd** 切换新建会话的默认目录。\n` +
1058
1058
  `发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(tool)}\n` +
1059
- `发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
1059
+ `发送 **/new** 创建新会话,**/forget** 重置当前会话(沿用工作目录)。\n` +
1060
1060
  `发送 **/sessions** 查看所有会话状态。\n` +
1061
1061
  `发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
1062
1062
  usageHelpLine(tool), "green");
@@ -1117,7 +1117,7 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
1117
1117
  `直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
1118
1118
  `发送 **/cd** 切换新建会话的默认目录。\n` +
1119
1119
  `发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(tool)}\n` +
1120
- `发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
1120
+ `发送 **/new** 创建新会话,**/forget** 重置当前会话(沿用工作目录)。\n` +
1121
1121
  `发送 **/sessions** 查看所有会话状态。\n` +
1122
1122
  `发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。` +
1123
1123
  usageHelpLine(tool), "green");
@@ -1413,12 +1413,12 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
1413
1413
  logTrace(tid, "DONE", { outcome: "sessions", ok, count: cardData.length });
1414
1414
  return;
1415
1415
  }
1416
- if (isCommandText && textLower === "/newh") {
1417
- logTrace(tid, "BRANCH", { cmd: "/newh" });
1416
+ if (isCommandText && textLower === "/forget") {
1417
+ logTrace(tid, "BRANCH", { cmd: "/forget" });
1418
1418
  let cwd;
1419
1419
  if (isFeishuP2p(platform, chatType)) {
1420
1420
  // 飞书私聊不支持切换工作目录。即使 /cd 已为后续 /new 群聊
1421
- // 保存了其他默认目录,/newh 仍必须在运行 ChatCCC 的用户目录重建。
1421
+ // 保存了其他默认目录,/forget 仍必须在运行 ChatCCC 的用户目录重建。
1422
1422
  cwd = homedir();
1423
1423
  }
1424
1424
  else {
@@ -1440,7 +1440,7 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
1440
1440
  }
1441
1441
  catch (err) {
1442
1442
  logTrace(tid, "DONE", {
1443
- outcome: "newh_session_fail",
1443
+ outcome: "forget_session_fail",
1444
1444
  error: err.message,
1445
1445
  });
1446
1446
  await platform.sendCard(chatId, "Error", `Failed to create new session:\n${err.message}`, "red");
@@ -1463,14 +1463,14 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
1463
1463
  });
1464
1464
  if (!switchResult.ok) {
1465
1465
  logTrace(tid, "DONE", {
1466
- outcome: "newh_update_chat_fail",
1466
+ outcome: "forget_update_chat_fail",
1467
1467
  error: switchResult.error?.message,
1468
1468
  });
1469
1469
  await platform.sendCard(chatId, "Error", `更新群描述失败,会话未切换(新 session 已创建但未启用):\n${switchResult.error?.message}`, "red");
1470
1470
  return;
1471
1471
  }
1472
1472
  if (chatType !== "p2p") {
1473
- console.log(`[${ts()}] [NEWH] Group updated: name="${newName}" desc="${descPrefix} ${newSessionId}"`);
1473
+ console.log(`[${ts()}] [FORGET] Group updated: name="${newName}" desc="${descPrefix} ${newSessionId}"`);
1474
1474
  }
1475
1475
  setChatAvatarForSession(platform, chatId, descriptionTool, "new", newSessionId).catch(() => { });
1476
1476
  await platform.sendCard(chatId, `${toolLabel} Session Reset`, `会话已重置为新的 **${toolLabel}** 会话。\n\n` +
@@ -1479,8 +1479,8 @@ export async function handleCommand(platform, text, chatId, openId, msgTimestamp
1479
1479
  `直接在这里发消息即可继续对话。\n` +
1480
1480
  `发送 **/cd** 可切换新建会话的默认目录。\n` +
1481
1481
  `发送 **/model** 查看或切换当前会话的模型。${fastHelpAfterModel(descriptionTool)}`, "green");
1482
- console.log(`[${ts()}] [NEWH] Session ${sessionId} → ${newSessionId} (same cwd=${cwd})`);
1483
- logTrace(tid, "DONE", { outcome: "newh", newSessionId, cwd });
1482
+ console.log(`[${ts()}] [FORGET] Session ${sessionId} → ${newSessionId} (same cwd=${cwd})`);
1483
+ logTrace(tid, "DONE", { outcome: "forget", newSessionId, cwd });
1484
1484
  return;
1485
1485
  }
1486
1486
  if (isCommandText && textLower === "/deleteg") {
@@ -30,7 +30,7 @@ export function unbindChatFromSession(sessionId, chatId) {
30
30
  sessionChatsMap.delete(sessionId);
31
31
  }
32
32
  // 双保险:lastActiveChatMap 是 sessionId → 最后活跃 chatId 的快照,
33
- // 若被解绑的 chatId 正好是该 session 的 lastActive(典型场景:/newh
33
+ // 若被解绑的 chatId 正好是该 session 的 lastActive(典型场景:/forget
34
34
  // 把当前群从旧 session 改嫁到新 session),不清理会让旧 session 的
35
35
  // display loop 继续向已离开的群推送卡片。
36
36
  if (lastActiveChatMap.get(sessionId)?.chatId === chatId) {
@@ -99,7 +99,7 @@ export function getLastActiveChat(sessionId) {
99
99
  * display loop 决定推送目标 chat 的纯函数:
100
100
  * 仅当 lastActiveChat 仍然绑定到该 session 时才返回,否则返回 undefined。
101
101
  *
102
- * 修复 bug:用户 `/newh` 把当前群从旧 session 改嫁到新 session 后,旧
102
+ * 修复 bug:用户 `/forget` 把当前群从旧 session 改嫁到新 session 后,旧
103
103
  * session 的 lastActiveChatMap 残留旧 chatId 快照,display loop 会继续
104
104
  * 在该群创建/更新卡片。这里通过交叉验证 sessionChatsMap 把这种悬挂记录
105
105
  * 排除掉——loop 拿到 undefined 就走"无活跃群"分支自然停推。
@@ -533,6 +533,7 @@ export function getAdapterForTool(tool, sessionId) {
533
533
  contextWindow: config.ccc.contextWindow,
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 {
@@ -1622,7 +1623,7 @@ export function startUnifiedDisplayLoop() {
1622
1623
  continue;
1623
1624
  }
1624
1625
  // 交叉验证:chat 当前绑定的 session 是否仍是 display 记录的 session。
1625
- // 若 chat 已被切换到其他 session(如 /newh),旧 display 必须停推。
1626
+ // 若 chat 已被切换到其他 session(如 /forget),旧 display 必须停推。
1626
1627
  const currentSessionForChat = sessionInfoMap.get(chatId)?.sessionId;
1627
1628
  if (currentSessionForChat && currentSessionForChat !== sessionId) {
1628
1629
  if (state.status !== "running") {
@@ -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';
@@ -537,7 +537,7 @@ async function handleWechatMessage(message, handler) {
537
537
  appendChatLog(chatId, chatId, fullText);
538
538
  // 用户回复,重置 claw 连发计数
539
539
  consecutiveSendCount.set(chatId, 0);
540
- // WeChat 中所有会话都视为 p2p,/new 复用 p2p 路径(等同飞书 /newh 效果)
540
+ // WeChat 中所有会话都视为 p2p,/new 复用 p2p 路径(等同飞书 /forget 效果)
541
541
  // 不 await:避免长 prompt 阻塞后续消息处理(如 /cd、/stop 等命令)
542
542
  handler(fullText, chatId, chatId, msgTimestamp, "p2p").catch((err) => {
543
543
  platformLog(`消息处理失败: ${err.stack ?? err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.256",
3
+ "version": "0.2.258",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",