chatccc 0.2.270 → 0.2.276

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.
Files changed (42) hide show
  1. package/README.md +16 -10
  2. package/config.sample.json +4 -3
  3. package/deepccc-agent/README.md +147 -61
  4. package/deepccc-agent/package.json +5 -2
  5. package/dist/deepccc-agent/src/attachments.js +192 -0
  6. package/dist/deepccc-agent/src/cli.js +59 -13
  7. package/dist/deepccc-agent/src/config.js +57 -4
  8. package/dist/deepccc-agent/src/context.js +299 -16
  9. package/dist/deepccc-agent/src/file-tools.js +33 -0
  10. package/dist/deepccc-agent/src/index.js +68 -21
  11. package/dist/deepccc-agent/src/tool-protocol.js +14 -3
  12. package/dist/deepccc-agent/src/web-entry.js +72 -0
  13. package/dist/deepccc-agent/src/web-page.js +414 -0
  14. package/dist/deepccc-agent/src/web-runtime.js +331 -0
  15. package/dist/deepccc-agent/src/web-server.js +476 -0
  16. package/dist/deepccc-agent/src/web-session-store.js +162 -0
  17. package/dist/deepccc-agent/src/web-tool-presentation.js +123 -0
  18. package/dist/src/adapters/ccc-adapter.js +5 -1
  19. package/dist/src/agent-capability-grants.js +26 -0
  20. package/dist/src/agent-delegate-task.js +5 -2
  21. package/dist/src/agent-file-rpc.js +6 -1
  22. package/dist/src/agent-image-rpc.js +6 -1
  23. package/dist/src/agent-team/application/task-execution-service.js +330 -97
  24. package/dist/src/agent-team/domain/task-run.js +14 -1
  25. package/dist/src/agent-team/infrastructure/task-execution-runtime.js +7 -2
  26. package/dist/src/agent-team/main-agent-bootstrap.js +24 -1
  27. package/dist/src/agent-team/repositories/json-task-run-repository.js +22 -4
  28. package/dist/src/agent-team/web/agent-team-page.js +14 -7
  29. package/dist/src/cards.js +7 -4
  30. package/dist/src/config.js +12 -0
  31. package/dist/src/im-skills.js +9 -2
  32. package/dist/src/orchestrator.js +117 -29
  33. package/dist/src/safe-maintenance.js +4 -1
  34. package/dist/src/session-name.js +15 -0
  35. package/dist/src/session.js +54 -9
  36. package/dist/src/web-ui.js +76 -32
  37. package/im-skills/feishu-skill/receive-send-file.md +3 -2
  38. package/im-skills/feishu-skill/receive-send-image.md +3 -2
  39. package/im-skills/feishu-skill/send-file.mjs +6 -5
  40. package/im-skills/feishu-skill/send-image.mjs +6 -5
  41. package/im-skills/feishu-skill/skill.md +4 -2
  42. package/package.json +1 -1
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
7
7
  import { createAnthropic } from "@ai-sdk/anthropic";
8
- import { generateText, isLoopFinished, stepCountIs, streamText } from "ai";
8
+ import { generateText, isLoopFinished, stepCountIs, streamText, } from "ai";
9
9
  import { existsSync, readFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { isAbsolute, join, resolve } from "node:path";
@@ -77,6 +77,7 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
77
77
  ].join("\n");
78
78
  export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
79
79
  const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
80
+ const OPENAI_COMPATIBLE_PROVIDER_NAME = "deepccc";
80
81
  /** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
81
82
  const TASK_MAX_STEPS = 20;
82
83
  /** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
@@ -143,7 +144,7 @@ function addAnthropicToolJsonCompatibilityNote(messages) {
143
144
  // 部分 Anthropic→OpenAI/Ark 转换器会用 response_format=json_object
144
145
  // 实现工具调用,却只在 messages 中校验 JSON 关键词、不读取顶层 system。
145
146
  // 这里仅声明工具参数的编码方式,并明确不要求普通最终回复输出 JSON。
146
- return messages.map((message, index) => (index === lastUserIndex
147
+ return messages.map((message, index) => (index === lastUserIndex && message.role === "user" && typeof message.content === "string"
147
148
  ? {
148
149
  ...message,
149
150
  content: `${message.content}\n\n${ANTHROPIC_TOOL_JSON_COMPATIBILITY_NOTE}`,
@@ -160,11 +161,13 @@ function addAnthropicToolJsonCompatibilityNote(messages) {
160
161
  function maybeAppendCompactionRecoveryHint(messages, summary, rawLogsEnabled, sessionId) {
161
162
  if (!summary.trim())
162
163
  return messages;
163
- const summaryIndex = messages.findIndex((message) => message.role === "user" && message.content.startsWith("以下是更早的对话摘要"));
164
+ const summaryIndex = messages.findIndex((message) => message.role === "user"
165
+ && typeof message.content === "string"
166
+ && message.content.startsWith("以下是更早的对话摘要"));
164
167
  if (summaryIndex < 0)
165
168
  return messages;
166
169
  const hint = rawLogsEnabled ? buildCompactionRecoveryHint(sessionId) : COMPACTION_RECOVERY_HINT_DISABLED;
167
- return messages.map((message, index) => (index === summaryIndex
170
+ return messages.map((message, index) => (index === summaryIndex && message.role === "user" && typeof message.content === "string"
168
171
  ? { ...message, content: `${message.content}\n\n${hint}` }
169
172
  : message));
170
173
  }
@@ -219,6 +222,14 @@ function normalizeMaxSteps(value) {
219
222
  }
220
223
  return value;
221
224
  }
225
+ function normalizeMaxOutputTokens(value) {
226
+ if (value === undefined)
227
+ return undefined;
228
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
229
+ throw new Error("maxOutputTokens must be a positive integer when provided");
230
+ }
231
+ return value;
232
+ }
222
233
  function normalizeAnthropicBaseURL(baseURL) {
223
234
  // 完全按用户填写的地址使用,不自动补 /v1(AI SDK 仅对官方 api.anthropic.com
224
235
  // 特判补一次 /v1,其他地址原样拼接 /messages)。
@@ -239,6 +250,8 @@ export class ChatSession {
239
250
  compactionTimeoutMs;
240
251
  maxSteps;
241
252
  effort;
253
+ maxOutputTokens;
254
+ streaming;
242
255
  permissionMode;
243
256
  permissionResolver;
244
257
  permissionGate;
@@ -267,6 +280,7 @@ export class ChatSession {
267
280
  baseURL: this.baseURL,
268
281
  model: this.subModelId || this.modelId,
269
282
  effort: this.effort,
283
+ maxOutputTokens: this.maxOutputTokens,
270
284
  }, {
271
285
  cwd: taskCwd,
272
286
  persist: false,
@@ -304,6 +318,8 @@ export class ChatSession {
304
318
  this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
305
319
  this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
306
320
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
321
+ this.maxOutputTokens = normalizeMaxOutputTokens(overrides.maxOutputTokens ?? appConfig.maxOutputTokens);
322
+ this.streaming = overrides.streaming ?? appConfig.streaming;
307
323
  this.apiKey = apiKey;
308
324
  this.baseURL = baseURL;
309
325
  const provider = this.provider === "anthropic"
@@ -312,7 +328,7 @@ export class ChatSession {
312
328
  apiKey,
313
329
  })
314
330
  : createOpenAICompatible({
315
- name: "deepccc",
331
+ name: OPENAI_COMPATIBLE_PROVIDER_NAME,
316
332
  baseURL,
317
333
  apiKey,
318
334
  includeUsage: true,
@@ -339,6 +355,7 @@ export class ChatSession {
339
355
  cwd: this.cwd,
340
356
  contextWindow: options.contextWindow ?? appConfig.contextWindow,
341
357
  compactAtTokens: options.compactAtTokens,
358
+ maxToolContextTokens: options.maxToolContextTokens,
342
359
  keepRecentMessages: options.keepRecentMessages,
343
360
  });
344
361
  this.permissionGate = new PermissionGate(this.permissionMode, this.permissionResolver);
@@ -379,6 +396,8 @@ export class ChatSession {
379
396
  // assistant 消息 toolCalls 字段;[Tool transcript] 文本视图仍按原格式生成。
380
397
  const toolCallsById = new Map();
381
398
  const toolCallOrder = [];
399
+ const timeline = [];
400
+ let toolContext = [];
382
401
  try {
383
402
  if (this.context.planCompaction()) {
384
403
  yield { type: "status", phase: "compacting" };
@@ -415,14 +434,14 @@ export class ChatSession {
415
434
  ? addAnthropicToolJsonCompatibilityNote(hintedMessages)
416
435
  : hintedMessages;
417
436
  // effort 按协议映射:
418
- // - OpenAI 兼容:providerOptions.deepseek.reasoningEffort 由 @ai-sdk/openai-compatible
419
- // 自动映射为请求体 reasoning_effort 字段(DeepSeek 原生支持);
437
+ // - OpenAI 兼容:providerOptions 的 key 必须与 createOpenAICompatible.name 一致;
438
+ // reasoningEffort 由 SDK 映射为请求体 reasoning_effort 字段;
420
439
  // - Anthropic:providerOptions.anthropic.effort 由 @ai-sdk/anthropic 组装为请求体
421
440
  // output_config.effort(官方 Effort API,见 platform.claude.com/docs/en/build-with-claude/effort)
422
441
  let effortProviderOptions;
423
442
  if (this.effort) {
424
443
  effortProviderOptions = this.provider === "openai"
425
- ? { deepseek: { reasoningEffort: this.effort } }
444
+ ? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: this.effort } }
426
445
  : { anthropic: { effort: this.effort } };
427
446
  }
428
447
  const baseGenerationOptions = {
@@ -438,14 +457,18 @@ export class ChatSession {
438
457
  }),
439
458
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
440
459
  abortSignal: signal,
460
+ ...(this.maxOutputTokens !== undefined
461
+ ? { maxOutputTokens: this.maxOutputTokens }
462
+ : {}),
441
463
  ...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
442
464
  };
443
465
  for (let attempt = 0; attempt < 2; attempt += 1) {
444
466
  fullText = "";
445
467
  safeAccumulated = "";
446
- const toolContext = [];
468
+ toolContext = [];
447
469
  toolCallsById.clear();
448
470
  toolCallOrder.length = 0;
471
+ timeline.length = 0;
449
472
  let lastReasoningProgressAt;
450
473
  const attemptMessages = attempt === 0
451
474
  ? modelMessages
@@ -455,7 +478,7 @@ export class ChatSession {
455
478
  messages: attemptMessages,
456
479
  };
457
480
  let stream;
458
- if (appConfig.streaming) {
481
+ if (this.streaming) {
459
482
  const result = streamText(generationOptions);
460
483
  stream = result.fullStream ?? textStreamToFullStream(result.textStream);
461
484
  }
@@ -476,6 +499,11 @@ export class ChatSession {
476
499
  }
477
500
  else if (part.type === "text-delta") {
478
501
  fullText += part.text;
502
+ const previous = timeline[timeline.length - 1];
503
+ if (previous?.type === "text")
504
+ previous.text += part.text;
505
+ else
506
+ timeline.push({ type: "text", text: part.text });
479
507
  // 隐私替换只在展示层:safeAccumulated 供事件消费者(终端/JSONL)使用,
480
508
  // fullText 原文用于持久化上下文,避免替换结果回流污染上下文。
481
509
  const safeText = applyPrivacy(part.text);
@@ -483,9 +511,11 @@ export class ChatSession {
483
511
  yield { type: "text", text: safeText, accumulated: safeAccumulated };
484
512
  }
485
513
  else if (part.type === "tool-call") {
486
- toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
487
- toolCallsById.set(part.toolCallId, { name: part.toolName, input: safeJson(part.input) });
514
+ const input = safeJson(part.input);
515
+ toolContext.push(`tool_call ${part.toolName}: ${input}`);
516
+ toolCallsById.set(part.toolCallId, { id: part.toolCallId, name: part.toolName, input });
488
517
  toolCallOrder.push(part.toolCallId);
518
+ timeline.push({ type: "tool_use", id: part.toolCallId, name: part.toolName, input });
489
519
  yield {
490
520
  type: "tool_use",
491
521
  id: part.toolCallId,
@@ -494,10 +524,12 @@ export class ChatSession {
494
524
  };
495
525
  }
496
526
  else if (part.type === "tool-result") {
497
- toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
527
+ const output = truncateToolContext(safeJson(part.output));
528
+ toolContext.push(`tool_result ${part.toolName}: ${output}`);
498
529
  const call = toolCallsById.get(part.toolCallId);
499
530
  if (call)
500
- call.output = truncateToolContext(safeJson(part.output));
531
+ call.output = output;
532
+ timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output });
501
533
  yield {
502
534
  type: "tool_result",
503
535
  tool_use_id: part.toolCallId,
@@ -514,6 +546,7 @@ export class ChatSession {
514
546
  call.output = message;
515
547
  call.is_error = true;
516
548
  }
549
+ timeline.push({ type: "tool_result", tool_use_id: part.toolCallId, name: part.toolName, output: message, is_error: true });
517
550
  yield {
518
551
  type: "tool_result",
519
552
  tool_use_id: part.toolCallId,
@@ -529,7 +562,7 @@ export class ChatSession {
529
562
  }
530
563
  }
531
564
  if (hasMalformedToolProtocolText(fullText)) {
532
- console.warn(`[DeepCCC] malformed DSML tool output detected for ${this.context.sessionId} `
565
+ console.warn(`[DeepCCC] malformed tool protocol text detected for ${this.context.sessionId} `
533
566
  + `(attempt ${attempt + 1}/2, structuredToolCalls=${toolCallOrder.length})`);
534
567
  rawLog?.writeLine(safeRawStreamJson({
535
568
  type: "deepccc_tool_protocol_recovery",
@@ -542,8 +575,8 @@ export class ChatSession {
542
575
  continue;
543
576
  }
544
577
  throw new Error(toolCallOrder.length > 0
545
- ? "工具调用协议异常:检测到混合的结构化与 DSML 文本调用,为避免重复执行工具,本轮已安全终止"
546
- : "工具调用协议异常:模型重试后仍输出了无效的 DSML 工具调用");
578
+ ? "工具调用协议异常:检测到混合的结构化调用与伪造工具文本,为避免重复执行工具,本轮已安全终止"
579
+ : "工具调用协议异常:模型重试后仍输出了无效或伪造的工具调用文本");
547
580
  }
548
581
  completed = true;
549
582
  const collectedToolCalls = toolCallOrder
@@ -553,6 +586,7 @@ export class ChatSession {
553
586
  fullText,
554
587
  transcriptLines: toolContext,
555
588
  toolCalls: collectedToolCalls,
589
+ timeline,
556
590
  }));
557
591
  yield { type: "done", text: safeAccumulated };
558
592
  return;
@@ -564,9 +598,22 @@ export class ChatSession {
564
598
  if (malformedProtocolOutput)
565
599
  yield { type: "text_reset" };
566
600
  if (err.name === "AbortError" || signal?.aborted) {
567
- // 被中断时,不保存不完整的助手消息
568
- if (fullText && !malformedProtocolOutput) {
569
- this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
601
+ if ((fullText || toolCallOrder.length > 0) && !malformedProtocolOutput) {
602
+ const collectedToolCalls = toolCallOrder
603
+ .map((id) => toolCallsById.get(id))
604
+ .filter((call) => call !== undefined);
605
+ const interruptedTimeline = timeline.map((entry) => ({ ...entry }));
606
+ const previous = interruptedTimeline[interruptedTimeline.length - 1];
607
+ if (previous?.type === "text")
608
+ previous.text += "\n[interrupted]";
609
+ else
610
+ interruptedTimeline.push({ type: "text", text: "[interrupted]" });
611
+ this.context.appendMessage(buildPersistedAssistantMessage({
612
+ fullText: `${fullText}\n[interrupted]`,
613
+ transcriptLines: toolContext,
614
+ toolCalls: collectedToolCalls,
615
+ timeline: interruptedTimeline,
616
+ }));
570
617
  }
571
618
  yield { type: "done", text: safeAccumulated };
572
619
  return;
@@ -630,7 +677,7 @@ export class ChatSession {
630
677
  // output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段。
631
678
  maxOutputTokens: MAX_COMPACTION_OUTPUT_TOKENS,
632
679
  providerOptions: this.provider === "openai"
633
- ? { deepseek: { reasoningEffort: "none" } }
680
+ ? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: "none" } }
634
681
  : { anthropic: { effort: "low" } },
635
682
  });
636
683
  if (!result.text.trim()) {
@@ -6,12 +6,23 @@
6
6
  const MALFORMED_DSML_TRAILER = /<\/[||]{2}DSML[||]{2}(?:parameter|invoke)>\s*$/iu;
7
7
  const RENDERED_TOOL_CALL = /\[调用\s+[A-Za-z_][\w.-]*\]/u;
8
8
  const DSML_INVOKE_TAG = /<[||]{2}DSML[||]{2}invoke\b/iu;
9
+ const TOOL_TRANSCRIPT_HEADER = /(?:^|\n)\s*\[工具记录\]\s*(?:\n|$)/u;
10
+ const TOOL_TRANSCRIPT_EVENT = /(?:^|\n)\s*tool_(?:call|result|error)\s+[A-Za-z_][\w.-]*\s*:/u;
11
+ /**
12
+ * Detects a model copying DeepCCC's persisted/debug transcript syntax into
13
+ * normal assistant text. Both the dedicated header and an event line are
14
+ * required so documentation and ordinary discussion remain valid.
15
+ */
16
+ export function hasImitatedToolTranscriptText(text) {
17
+ return TOOL_TRANSCRIPT_HEADER.test(text) && TOOL_TRANSCRIPT_EVENT.test(text);
18
+ }
9
19
  export function hasMalformedToolProtocolText(text) {
10
- return MALFORMED_DSML_TRAILER.test(text)
20
+ const malformedDsml = MALFORMED_DSML_TRAILER.test(text)
11
21
  && (RENDERED_TOOL_CALL.test(text) || DSML_INVOKE_TAG.test(text));
22
+ return malformedDsml || hasImitatedToolTranscriptText(text);
12
23
  }
13
24
  export const TOOL_PROTOCOL_RECOVERY_PROMPT = [
14
25
  "[系统恢复提示] 上一次响应泄漏了内部工具调用协议,因此已被丢弃。",
15
- "请重新完成当前用户请求。需要调用工具时,只能使用 API 提供的结构化工具调用;不要把 DSML 或工具参数作为普通文本输出。",
16
- "不要提及本恢复提示,也不要假装工具已经执行。",
26
+ "请重新完成当前用户请求。需要调用工具时,只能使用 API 提供的结构化工具调用;不要把 DSML、[工具调用]、[工具记录] 或工具参数作为普通文本输出。",
27
+ "不要提及本恢复提示,不要伪造工具已执行,也不要根据未执行的命令声称得到了结果。",
17
28
  ].join("\n");
@@ -0,0 +1,72 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { resolve } from "node:path";
3
+ import { startDeepCccWebServer } from "./web-server.js";
4
+ const WEB_FLAGS = new Set(["web", "--reuse-existing", "--port", "--no-open", "--help", "-h"]);
5
+ export function isLegacyCliInvocation(args) {
6
+ if (!args.length)
7
+ return false;
8
+ if (args[0] === "web")
9
+ return false;
10
+ return !WEB_FLAGS.has(args[0]);
11
+ }
12
+ export function parseWebEntryArgs(rawArgs) {
13
+ const args = rawArgs[0] === "web" ? rawArgs.slice(1) : [...rawArgs];
14
+ if (args.includes("--help") || args.includes("-h"))
15
+ return {};
16
+ let port;
17
+ let openBrowser;
18
+ let reuseExisting = false;
19
+ for (let index = 0; index < args.length; index++) {
20
+ const arg = args[index];
21
+ if (arg === "--reuse-existing")
22
+ reuseExisting = true;
23
+ else if (arg === "--no-open")
24
+ openBrowser = false;
25
+ else if (arg === "--port") {
26
+ const value = Number(args[++index]);
27
+ if (!Number.isInteger(value) || value < 1 || value > 65_535)
28
+ throw new Error("--port must be an integer between 1 and 65535");
29
+ port = value;
30
+ }
31
+ else {
32
+ throw new Error(`Unknown DeepCCC Web option: ${arg}`);
33
+ }
34
+ }
35
+ return {
36
+ reuseExisting,
37
+ ...(port ? { port } : {}),
38
+ ...(openBrowser === false ? { openBrowser: false } : {}),
39
+ };
40
+ }
41
+ export function printWebHelp() {
42
+ console.log([
43
+ "DeepCCC Web UI",
44
+ "",
45
+ "Usage: deepccc [web] [options]",
46
+ "",
47
+ " --reuse-existing Reuse an existing verified DeepCCC Web instance",
48
+ " --port <port> Override config.web.port for this run",
49
+ " --no-open Do not open the browser automatically",
50
+ " --help Show this help",
51
+ "",
52
+ "Terminal CLI: deepccc-cli [options]",
53
+ ].join("\n"));
54
+ }
55
+ export async function runWebEntry(rawArgs = process.argv.slice(2)) {
56
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
57
+ printWebHelp();
58
+ return;
59
+ }
60
+ const options = parseWebEntryArgs(rawArgs);
61
+ const handle = await startDeepCccWebServer(options);
62
+ console.log(`DeepCCC Web UI: ${handle.url}${handle.reused ? " (reused existing server)" : ""}`);
63
+ }
64
+ function isDirectInvocation() {
65
+ return resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1] ?? "");
66
+ }
67
+ if (isDirectInvocation()) {
68
+ runWebEntry().catch((err) => {
69
+ console.error(`DeepCCC Web startup failed: ${err.message}`);
70
+ process.exit(1);
71
+ });
72
+ }