chatccc 0.2.239 → 0.2.241

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
@@ -210,9 +210,11 @@ chatccc
210
210
 
211
211
  Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于 ChatCCC,开箱即用,**模型接入不限于 DeepSeek**——它走 OpenAI 兼容协议,任意兼容端点都可以直接替换(详见下文 CCC Agent)。
212
212
 
213
- #### CCC Agent
214
-
215
- CCC Agent 是 ChatCCC 内置的编程 Agent,不需要额外安装 CLI,开箱即用。在首次配置向导或 Web 管理页中启用后,填写 API Key、Base URL 和模型即可使用;它可以设为 `/new` 的默认 Agent,也可以通过 `/new ccc` 显式创建会话。
213
+ #### CCC Agent
214
+
215
+ CCC Agent 是 ChatCCC 内置的编程 Agent,不需要额外安装 CLI,开箱即用。在首次配置向导或 Web 管理页中启用后,填写 API Key、Base URL 和模型即可使用;它可以设为 `/new` 的默认 Agent,也可以通过 `/new ccc` 显式创建会话。
216
+
217
+ ChatCCC 会把 `ccc.DEEPSEEK_API_KEY`、`ccc.DEEPSEEK_BASE_URL`、模型和 effort 显式传给内置 Agent;这些配置不会回退读取 `~/.deepccc/config.json`,因此用户无需安装或配置独立的 `deepccc` 包。API Key 为空时 CCC Agent 会自动保持禁用。
216
218
 
217
219
  **API 支持不限于 DeepSeek。** CCC Agent 底层使用 OpenAI 兼容协议(`@ai-sdk/openai-compatible`),DeepSeek 只是出厂默认端点。`ccc.DEEPSEEK_API_KEY` / `ccc.DEEPSEEK_BASE_URL` 可以指向**任意 OpenAI 兼容服务**,例如:
218
220
 
package/package.json CHANGED
@@ -1,71 +1,71 @@
1
- {
2
- "name": "chatccc",
3
- "version": "0.2.239",
4
- "description": "Feishu bot bridge for Claude Code",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "main": "./src/index.ts",
8
- "bin": {
9
- "chatccc": "bin/chatccc.mjs",
10
- "cccagent": "bin/cccagent.mjs"
11
- },
12
- "files": [
13
- "src/",
14
- "bin/",
15
- "scripts/postinstall-sharp-check.mjs",
16
- "demo/ilink_echo_probe.ts",
17
- "agent-prompts/",
18
- "im-skills/",
19
- ".agents/skills/create-chatccc-feishu-app/",
20
- ".claude/skills/create-chatccc-feishu-app/",
21
- ".cursor/skills/create-chatccc-feishu-app/",
22
- "images/img_readme_*.jpg",
23
- "images/img_readme_*.png",
24
- "images/avatars/status_*.png",
25
- "images/avatars/badges/",
26
- "images/avatars/combinations/",
27
- "package.json",
28
- "README.md",
29
- "config.sample.json"
30
- ],
31
- "scripts": {
32
- "dev": "tsx src/index.ts",
33
- "chatccc": "tsx src/index.ts",
34
- "start": "tsx src/index.ts",
35
- "demo:bot-test": "tsx demo/bot_test.ts",
36
- "demo:bot-test:local": "tsx demo/bot_test.ts --local",
37
- "demo:create-group": "tsx src/index.ts",
38
- "demo:create-group:local": "tsx src/index.ts --local",
39
- "demo:permission-check": "tsx demo/permission_check.ts",
40
- "demo:claude-hi": "tsx demo/claude_say_hi.ts",
41
- "demo:codex-hi": "tsx demo/codex_say_hi.ts",
42
- "demo:codex-app-server-approval": "tsx demo/codex-app-server-approval/approval_demo.ts",
43
- "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
44
- "claude-proxy": "tsx src/litellm-proxy.ts",
45
- "test": "vitest run",
46
- "test:watch": "vitest",
47
- "postinstall": "node scripts/postinstall-sharp-check.mjs"
48
- },
49
- "dependencies": {
50
- "@ai-sdk/openai-compatible": "^2.0.47",
51
- "@larksuiteoapi/node-sdk": "^1.59.0",
52
- "@openilink/openilink-sdk-node": "^0.6.0",
53
- "@vscode/ripgrep": "^1.18.0",
54
- "ai": "^6.0.184",
55
- "nodemailer": "^8.0.7",
56
- "qrcode-terminal": "^0.12.0",
57
- "sharp": "^0.34.5",
58
- "tsx": "^4.0.0",
59
- "ws": "^8.18.0"
60
- },
61
- "devDependencies": {
62
- "@types/node": "^20.0.0",
63
- "@types/qrcode-terminal": "^0.12.2",
64
- "@types/ws": "^8.18.1",
65
- "typescript": "^5.0.0",
66
- "vitest": "^3.2.4"
67
- },
68
- "engines": {
69
- "node": ">=20"
70
- }
71
- }
1
+ {
2
+ "name": "chatccc",
3
+ "version": "0.2.241",
4
+ "description": "Feishu bot bridge for Claude Code",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "bin": {
9
+ "chatccc": "bin/chatccc.mjs",
10
+ "cccagent": "bin/cccagent.mjs"
11
+ },
12
+ "files": [
13
+ "src/",
14
+ "bin/",
15
+ "scripts/postinstall-sharp-check.mjs",
16
+ "demo/ilink_echo_probe.ts",
17
+ "agent-prompts/",
18
+ "im-skills/",
19
+ ".agents/skills/create-chatccc-feishu-app/",
20
+ ".claude/skills/create-chatccc-feishu-app/",
21
+ ".cursor/skills/create-chatccc-feishu-app/",
22
+ "images/img_readme_*.jpg",
23
+ "images/img_readme_*.png",
24
+ "images/avatars/status_*.png",
25
+ "images/avatars/badges/",
26
+ "images/avatars/combinations/",
27
+ "package.json",
28
+ "README.md",
29
+ "config.sample.json"
30
+ ],
31
+ "scripts": {
32
+ "dev": "tsx src/index.ts",
33
+ "chatccc": "tsx src/index.ts",
34
+ "start": "tsx src/index.ts",
35
+ "demo:bot-test": "tsx demo/bot_test.ts",
36
+ "demo:bot-test:local": "tsx demo/bot_test.ts --local",
37
+ "demo:create-group": "tsx src/index.ts",
38
+ "demo:create-group:local": "tsx src/index.ts --local",
39
+ "demo:permission-check": "tsx demo/permission_check.ts",
40
+ "demo:claude-hi": "tsx demo/claude_say_hi.ts",
41
+ "demo:codex-hi": "tsx demo/codex_say_hi.ts",
42
+ "demo:codex-app-server-approval": "tsx demo/codex-app-server-approval/approval_demo.ts",
43
+ "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
44
+ "claude-proxy": "tsx src/litellm-proxy.ts",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "postinstall": "node scripts/postinstall-sharp-check.mjs"
48
+ },
49
+ "dependencies": {
50
+ "@ai-sdk/openai-compatible": "^2.0.47",
51
+ "@larksuiteoapi/node-sdk": "^1.59.0",
52
+ "@openilink/openilink-sdk-node": "^0.6.0",
53
+ "@vscode/ripgrep": "^1.18.0",
54
+ "ai": "^6.0.184",
55
+ "nodemailer": "^8.0.7",
56
+ "qrcode-terminal": "^0.12.0",
57
+ "sharp": "^0.34.5",
58
+ "tsx": "^4.0.0",
59
+ "ws": "^8.18.0"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^20.0.0",
63
+ "@types/qrcode-terminal": "^0.12.2",
64
+ "@types/ws": "^8.18.1",
65
+ "typescript": "^5.0.0",
66
+ "vitest": "^3.2.4"
67
+ },
68
+ "engines": {
69
+ "node": ">=20"
70
+ }
71
+ }
@@ -92,6 +92,43 @@ describe("ChatSession context management", () => {
92
92
  );
93
93
  });
94
94
 
95
+ it("keeps execution discipline sections in the stable system prompt prefix", async () => {
96
+ const { ChatSession } = await import("../builtin/index.ts");
97
+ const dir = await mkdtemp(join(tmpdir(), "deepccc-session-discipline-"));
98
+ await writeFile(join(dir, "AGENTS.md"), "PROJECT GUIDANCE MARKER", "utf-8");
99
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
100
+
101
+ const session = new ChatSession(
102
+ { apiKey: "sk-test" },
103
+ {
104
+ cwd: dir,
105
+ sessionId: "execution-discipline",
106
+ systemPrompt: "CUSTOM PROMPT MARKER",
107
+ },
108
+ );
109
+ await collect(session.chat("build a feature"));
110
+
111
+ const system = streamTextMock.mock.calls.at(-1)?.[0].system as string;
112
+ // 先盘点再动手:动手前低开销盘点 + 输出含验证策略的计划
113
+ expect(system).toContain("## Survey Before Acting");
114
+ expect(system).toContain("map the landscape");
115
+ expect(system).toContain("how you will verify the result");
116
+ // 授权自主:用户委托决策后只问真正阻塞项,不抛实现级选择题
117
+ expect(system).toContain("## Delegated Authority");
118
+ expect(system).toContain("irreversible actions");
119
+ expect(system).toContain("Do not bounce implementation-level multiple-choice");
120
+ // 交付自检:声明做了什么/如何验证/未验证项
121
+ expect(system).toContain("## Pre-Delivery Self-Check");
122
+ expect(system).toContain("remains unverified or risky");
123
+ // 稳定前缀全部位于项目指令与 runtime 上下文之前
124
+ for (const section of ["## Survey Before Acting", "## Delegated Authority", "## Pre-Delivery Self-Check"]) {
125
+ expect(system.indexOf(section)).toBeGreaterThan(0);
126
+ expect(system.indexOf(section)).toBeLessThan(system.indexOf("PROJECT GUIDANCE MARKER"));
127
+ expect(system.indexOf(section)).toBeLessThan(system.indexOf("Current working directory"));
128
+ expect(system.indexOf(section)).toBeLessThan(system.indexOf("CUSTOM PROMPT MARKER"));
129
+ }
130
+ });
131
+
95
132
  it("injects cwd project instruction files before runtime workspace details", async () => {
96
133
  const { ChatSession } = await import("../builtin/index.ts");
97
134
  const dir = await mkdtemp(join(tmpdir(), "deepccc-session-instructions-"));
@@ -15,8 +15,23 @@ import {
15
15
  autoDetectCursorPath,
16
16
  normalizeOptionalConfigField,
17
17
  parseGitTimeoutSeconds,
18
- readToolCliPath,
19
- } from "../config-utils.ts";
18
+ readToolCliPath,
19
+ resolveCccEnabled,
20
+ } from "../config-utils.ts";
21
+
22
+ describe("resolveCccEnabled", () => {
23
+ it("never enables CCC Agent without a ChatCCC API key", () => {
24
+ expect(resolveCccEnabled(true, "")).toBe(false);
25
+ expect(resolveCccEnabled(true, " ")).toBe(false);
26
+ expect(resolveCccEnabled(undefined, undefined)).toBe(false);
27
+ });
28
+
29
+ it("respects the explicit enabled flag when a ChatCCC API key exists", () => {
30
+ expect(resolveCccEnabled(undefined, "sk-chatccc")).toBe(true);
31
+ expect(resolveCccEnabled(true, "sk-chatccc")).toBe(true);
32
+ expect(resolveCccEnabled(false, "sk-chatccc")).toBe(false);
33
+ });
34
+ });
20
35
 
21
36
  describe("parseGitTimeoutSeconds", () => {
22
37
  it("returns default when raw is undefined", () => {
@@ -0,0 +1,45 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const createCccAdapterMock = vi.hoisted(() => vi.fn(() => ({
4
+ displayName: "CCC Agent",
5
+ sessionDescPrefix: "CCC Session:",
6
+ createSession: vi.fn(),
7
+ prompt: vi.fn(),
8
+ getSessionInfo: vi.fn(),
9
+ closeSession: vi.fn(),
10
+ })));
11
+
12
+ vi.mock("../adapters/ccc-adapter.ts", () => ({
13
+ createCccAdapter: createCccAdapterMock,
14
+ }));
15
+
16
+ import { config } from "../config.ts";
17
+ import { _clearAdapterCacheForTest, getAdapterForTool } from "../session.ts";
18
+
19
+ describe("CCC Agent ChatCCC configuration", () => {
20
+ const original = { ...config.ccc };
21
+
22
+ afterEach(() => {
23
+ Object.assign(config.ccc, original);
24
+ _clearAdapterCacheForTest();
25
+ createCccAdapterMock.mockClear();
26
+ });
27
+
28
+ it("injects ChatCCC credentials and endpoint instead of relying on ~/.deepccc", () => {
29
+ Object.assign(config.ccc, {
30
+ DEEPSEEK_API_KEY: "chatccc-api-key",
31
+ DEEPSEEK_BASE_URL: "https://chatccc.example.com/v1",
32
+ model: "chatccc-model",
33
+ effort: "high",
34
+ });
35
+
36
+ getAdapterForTool("ccc");
37
+
38
+ expect(createCccAdapterMock).toHaveBeenCalledWith({
39
+ apiKey: "chatccc-api-key",
40
+ baseURL: "https://chatccc.example.com/v1",
41
+ model: "chatccc-model",
42
+ effort: "high",
43
+ });
44
+ });
45
+ });
@@ -42,8 +42,12 @@ function toChatSessionOptions(
42
42
  }
43
43
 
44
44
  export function createCccAdapter(options: CccAdapterOptions = {}): ToolAdapter {
45
+ if (!options.apiKey?.trim()) {
46
+ throw new Error("ChatCCC 未配置 CCC Agent API Key。请先填写 ccc.DEEPSEEK_API_KEY 后再启用 CCC Agent。");
47
+ }
48
+
45
49
  const chatConfig: ChatSessionConfig = {
46
- ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
50
+ apiKey: options.apiKey,
47
51
  ...(options.baseURL !== undefined ? { baseURL: options.baseURL } : {}),
48
52
  ...(options.model !== undefined ? { model: options.model } : {}),
49
53
  ...(options.effort !== undefined ? { effort: options.effort } : {}),
@@ -42,17 +42,31 @@ const SYSTEM_PROMPT = [
42
42
  "- Respond in the user's language unless they ask otherwise.",
43
43
  "- Prefer direct, usable answers and concrete actions over long explanations.",
44
44
  "- For code tasks, inspect the relevant files before editing and verify with tests or checks when practical.",
45
- "- Preserve user work. Do not overwrite concurrent changes unless the user explicitly asks.",
46
- "- Keep immutable platform rules above project guidance and runtime details.",
47
- "",
48
- "## Evidence-Gated Conclusions",
49
- "- Apply this gate before consequential claims or actions that could affect code, data, deployments, or user decisions, and whenever the available evidence is indirect.",
50
- "- Identify the claim and its authoritative source of truth. Separate direct observations from inferences, and test plausible alternative explanations before choosing one.",
51
- "- Use the strongest practical decisive check at the same semantic level as the claim: runtime behavior for runtime claims, effective configuration for configuration claims, deployed state for deployment claims, and transformed output for transformation claims.",
52
- "- Do not treat proxy signals such as names, timestamps, file sizes, line counts, partial samples, or a clean command exit as decisive when a direct check is practical.",
53
- "- Use definitive language only after the evidence closes the loop. Otherwise state uncertainty, identify the missing evidence, and name the next check.",
54
- "- Do not repeat checks once decisive evidence exists.",
55
- ].join("\n");
45
+ "- Preserve user work. Do not overwrite concurrent changes unless the user explicitly asks.",
46
+ "- Keep immutable platform rules above project guidance and runtime details.",
47
+ "",
48
+ "## Evidence-Gated Conclusions",
49
+ "- Apply this gate before consequential claims or actions that could affect code, data, deployments, or user decisions, and whenever the available evidence is indirect.",
50
+ "- Identify the claim and its authoritative source of truth. Separate direct observations from inferences, and test plausible alternative explanations before choosing one.",
51
+ "- Use the strongest practical decisive check at the same semantic level as the claim: runtime behavior for runtime claims, effective configuration for configuration claims, deployed state for deployment claims, and transformed output for transformation claims.",
52
+ "- Do not treat proxy signals such as names, timestamps, file sizes, line counts, partial samples, or a clean command exit as decisive when a direct check is practical.",
53
+ "- Use definitive language only after the evidence closes the loop. Otherwise state uncertainty, identify the missing evidence, and name the next check.",
54
+ "- Do not repeat checks once decisive evidence exists.",
55
+ "",
56
+ "## Survey Before Acting",
57
+ "- Before diving into any task, first map the landscape at low cost: project instructions, directory layout, routes/APIs, existing tests, and git state.",
58
+ "- Produce a brief execution plan that includes how you will verify the result, then execute.",
59
+ "- When evidence contradicts an early assumption, revisit the plan instead of tunneling on one direction.",
60
+ "",
61
+ "## Delegated Authority",
62
+ "- When the user delegates decisions (\"you decide\", \"do it elegantly\", \"up to you\"), act autonomously on implementation details.",
63
+ "- Only ask about genuine blockers: irreversible actions, security boundaries, credentials, or scope changes.",
64
+ "- Do not bounce implementation-level multiple-choice questions back to the user after they delegated.",
65
+ "",
66
+ "## Pre-Delivery Self-Check",
67
+ "- Before reporting completion, verify: the change runs, edge cases are covered, assumptions are listed, and anything unverified is explicitly labeled as such.",
68
+ "- State what was done, how it was verified, and what remains unverified or risky.",
69
+ ].join("\n");
56
70
 
57
71
  const SUMMARY_SYSTEM_PROMPT = [
58
72
  "You are DeepCCC's context compactor.",
@@ -20,7 +20,7 @@ import { join } from "node:path";
20
20
  * - `value` 去除空白后等于 `"default"`(不区分大小写)→ 视作 `""` 并 warn。
21
21
  * - 其余情况原样返回(不裁剪两端空白,留给具体调用方决定)。
22
22
  */
23
- export function normalizeOptionalConfigField(
23
+ export function normalizeOptionalConfigField(
24
24
  value: unknown,
25
25
  options: { label: string; fallback?: string },
26
26
  ): string {
@@ -33,8 +33,18 @@ export function normalizeOptionalConfigField(
33
33
  );
34
34
  return "";
35
35
  }
36
- return value;
37
- }
36
+ return value;
37
+ }
38
+
39
+ /**
40
+ * CCC Agent requires credentials owned by ChatCCC. An explicit enabled=true
41
+ * must not make the agent selectable when that credential is absent.
42
+ */
43
+ export function resolveCccEnabled(rawEnabled: unknown, apiKey: unknown): boolean {
44
+ const hasApiKey = typeof apiKey === "string" && apiKey.trim().length > 0;
45
+ if (!hasApiKey) return false;
46
+ return typeof rawEnabled === "boolean" ? rawEnabled : true;
47
+ }
38
48
 
39
49
  // ---------------------------------------------------------------------------
40
50
  // /git 超时配置相关
package/src/config.ts CHANGED
@@ -11,9 +11,10 @@ import {
11
11
  anthropicConfigDisplay,
12
12
  autoDetectCodexPath,
13
13
  autoDetectCursorPath,
14
- normalizeOptionalConfigField,
15
- readToolCliPath,
16
- } from "./config-utils.ts";
14
+ normalizeOptionalConfigField,
15
+ readToolCliPath,
16
+ resolveCccEnabled,
17
+ } from "./config-utils.ts";
17
18
 
18
19
  // 重新导出 config-utils 中的纯函数/常量,保持对外 API 不变
19
20
  // (历史上这些符号都从 ./config.ts 导入;新代码可直接从 ./config-utils.ts 导入以避免触发本文件的副作用)
@@ -22,10 +23,11 @@ export {
22
23
  MIN_GIT_TIMEOUT_SECONDS,
23
24
  MAX_GIT_TIMEOUT_SECONDS,
24
25
  parseGitTimeoutSeconds,
25
- normalizeOptionalConfigField,
26
- isAnthropicConfigEmpty,
27
- anthropicConfigDisplay,
28
- } from "./config-utils.ts";
26
+ normalizeOptionalConfigField,
27
+ isAnthropicConfigEmpty,
28
+ anthropicConfigDisplay,
29
+ resolveCccEnabled,
30
+ } from "./config-utils.ts";
29
31
  export type { ParsedGitTimeout } from "./config-utils.ts";
30
32
 
31
33
  // ---------------------------------------------------------------------------
@@ -595,13 +597,10 @@ function loadConfig(): AppConfig {
595
597
  );
596
598
  // 旧版 ccc 配置没有 enabled。只用 API Key 推断启用,避免 sample 中自带的
597
599
  // 默认 Base URL / model 让升级用户在未配置凭证时意外启用 CCC Agent。
598
- const cccNonEmpty = (): boolean =>
599
- Boolean(typeof cccRaw.DEEPSEEK_API_KEY === "string" && cccRaw.DEEPSEEK_API_KEY.trim());
600
-
601
- const claudeEnabled = resolveEnabled(claude.enabled, claudeNonEmpty);
602
- const cursorEnabled = resolveEnabled(cursorRaw.enabled, cursorNonEmpty);
603
- const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
604
- const cccEnabled = resolveEnabled(cccRaw.enabled, cccNonEmpty);
600
+ const claudeEnabled = resolveEnabled(claude.enabled, claudeNonEmpty);
601
+ const cursorEnabled = resolveEnabled(cursorRaw.enabled, cursorNonEmpty);
602
+ const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
603
+ const cccEnabled = resolveCccEnabled(cccRaw.enabled, cccRaw.DEEPSEEK_API_KEY);
605
604
  const chromeDevtoolsPort = Number(chromeDevtoolsRaw.port);
606
605
  const explicitDefaultTool: AgentTool | null =
607
606
  typeof claude.defaultAgent === "boolean" && claude.defaultAgent && claudeEnabled ? "claude" :
package/src/session.ts CHANGED
@@ -707,11 +707,13 @@ export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter
707
707
  effort: effectiveEffort || undefined,
708
708
  fastMode: effectiveFastMode,
709
709
  });
710
- } else if (tool === "ccc") {
711
- adapter = createCccAdapter({
712
- model: effectiveModel || undefined,
713
- effort: effectiveEffort || undefined,
714
- });
710
+ } else if (tool === "ccc") {
711
+ adapter = createCccAdapter({
712
+ apiKey: config.ccc.DEEPSEEK_API_KEY,
713
+ baseURL: config.ccc.DEEPSEEK_BASE_URL,
714
+ model: effectiveModel || undefined,
715
+ effort: effectiveEffort || undefined,
716
+ });
715
717
  } else {
716
718
  adapter = createClaudeAdapter({
717
719
  model: effectiveModel,