chatccc 0.2.244 → 0.2.246

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.
@@ -1,84 +1,101 @@
1
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
-
5
- export interface DeepCccConfig {
6
- apiKey: string;
7
- baseURL: string;
8
- model: string;
9
- /** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
10
- effort: string;
11
- rawStreamLogs: {
12
- enabled: boolean;
13
- maxBytesPerTurn: number;
14
- retentionDays: number;
15
- keepCompleted: boolean;
16
- };
17
- }
18
-
19
- export const DEEPCCC_HOME = join(homedir(), ".deepccc");
20
- export const RAW_STREAM_LOGS_DIR = join(DEEPCCC_HOME, "raw-stream-logs");
21
- const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
22
-
23
- const DEFAULT_CONFIG: DeepCccConfig = {
24
- apiKey: "",
25
- baseURL: "https://api.deepseek.com/v1",
26
- model: "deepseek-v4-pro",
27
- effort: "",
28
- rawStreamLogs: {
29
- enabled: false,
30
- maxBytesPerTurn: 1024 * 1024,
31
- retentionDays: 7,
32
- keepCompleted: false,
33
- },
34
- };
35
-
36
- function readConfigFile(): Partial<DeepCccConfig> {
37
- if (!existsSync(CONFIG_PATH)) return {};
38
- const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<DeepCccConfig>;
39
- return raw && typeof raw === "object" ? raw : {};
40
- }
41
-
42
- function env(name: string): string | undefined {
43
- const value = process.env[name]?.trim();
44
- return value ? value : undefined;
45
- }
46
-
47
- function boolEnv(name: string): boolean | undefined {
48
- const value = env(name)?.toLowerCase();
49
- if (value === undefined) return undefined;
50
- if (["1", "true", "yes", "on"].includes(value)) return true;
51
- if (["0", "false", "no", "off"].includes(value)) return false;
52
- return undefined;
53
- }
54
-
55
- function numberEnv(name: string): number | undefined {
56
- const value = Number(env(name));
57
- return Number.isFinite(value) && value >= 0 ? value : undefined;
58
- }
59
-
60
- function loadConfig(): DeepCccConfig {
61
- const file = readConfigFile();
62
- const rawLogs: Partial<DeepCccConfig["rawStreamLogs"]> = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
63
- ? file.rawStreamLogs
64
- : {};
65
-
66
- return {
67
- apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
68
- baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
69
- model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
70
- effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
71
- rawStreamLogs: {
72
- enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
73
- maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
74
- retentionDays: numberEnv("DEEPCCC_RAW_STREAM_RETENTION_DAYS") ?? rawLogs.retentionDays ?? DEFAULT_CONFIG.rawStreamLogs.retentionDays,
75
- keepCompleted: boolEnv("DEEPCCC_RAW_STREAM_KEEP_COMPLETED") ?? rawLogs.keepCompleted ?? DEFAULT_CONFIG.rawStreamLogs.keepCompleted,
76
- },
77
- };
78
- }
79
-
80
- export function ensureConfigDir(): void {
81
- mkdirSync(dirname(CONFIG_PATH), { recursive: true });
82
- }
83
-
84
- export const config = loadConfig();
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export type DeepCccProvider = "openai" | "anthropic";
6
+
7
+ export interface DeepCccConfig {
8
+ /** API protocol/provider. Defaults to OpenAI-compatible. */
9
+ provider: DeepCccProvider;
10
+ apiKey: string;
11
+ baseURL: string;
12
+ model: string;
13
+ /** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
14
+ effort: string;
15
+ /** 主对话是否使用流式请求;默认开启 */
16
+ streaming: boolean;
17
+ rawStreamLogs: {
18
+ enabled: boolean;
19
+ maxBytesPerTurn: number;
20
+ retentionDays: number;
21
+ keepCompleted: boolean;
22
+ };
23
+ }
24
+
25
+ export const DEEPCCC_HOME = join(homedir(), ".deepccc");
26
+ export const RAW_STREAM_LOGS_DIR = join(DEEPCCC_HOME, "raw-stream-logs");
27
+ const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
28
+
29
+ const DEFAULT_CONFIG: DeepCccConfig = {
30
+ provider: "openai",
31
+ apiKey: "",
32
+ baseURL: "https://api.deepseek.com/v1",
33
+ model: "deepseek-v4-pro",
34
+ effort: "",
35
+ streaming: true,
36
+ rawStreamLogs: {
37
+ enabled: false,
38
+ maxBytesPerTurn: 1024 * 1024,
39
+ retentionDays: 7,
40
+ keepCompleted: false,
41
+ },
42
+ };
43
+
44
+ function readConfigFile(): Partial<DeepCccConfig> {
45
+ if (!existsSync(CONFIG_PATH)) return {};
46
+ const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<DeepCccConfig>;
47
+ return raw && typeof raw === "object" ? raw : {};
48
+ }
49
+
50
+ function env(name: string): string | undefined {
51
+ const value = process.env[name]?.trim();
52
+ return value ? value : undefined;
53
+ }
54
+
55
+ function boolEnv(name: string): boolean | undefined {
56
+ const value = env(name)?.toLowerCase();
57
+ if (value === undefined) return undefined;
58
+ if (["1", "true", "yes", "on"].includes(value)) return true;
59
+ if (["0", "false", "no", "off"].includes(value)) return false;
60
+ return undefined;
61
+ }
62
+
63
+ function numberEnv(name: string): number | undefined {
64
+ const value = Number(env(name));
65
+ return Number.isFinite(value) && value >= 0 ? value : undefined;
66
+ }
67
+
68
+ export function normalizeDeepCccProvider(value: unknown): DeepCccProvider {
69
+ if (value === undefined || value === null || String(value).trim() === "") return "openai";
70
+ const normalized = String(value).trim().toLowerCase();
71
+ if (normalized === "openai" || normalized === "anthropic") return normalized;
72
+ throw new Error(`DEEPCCC_PROVIDER/provider must be "openai" or "anthropic", received: ${String(value)}`);
73
+ }
74
+
75
+ function loadConfig(): DeepCccConfig {
76
+ const file = readConfigFile();
77
+ const rawLogs: Partial<DeepCccConfig["rawStreamLogs"]> = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
78
+ ? file.rawStreamLogs
79
+ : {};
80
+
81
+ return {
82
+ provider: normalizeDeepCccProvider(env("DEEPCCC_PROVIDER") ?? file.provider ?? DEFAULT_CONFIG.provider),
83
+ apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
84
+ baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
85
+ model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
86
+ effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
87
+ streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
88
+ rawStreamLogs: {
89
+ enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
90
+ maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
91
+ retentionDays: numberEnv("DEEPCCC_RAW_STREAM_RETENTION_DAYS") ?? rawLogs.retentionDays ?? DEFAULT_CONFIG.rawStreamLogs.retentionDays,
92
+ keepCompleted: boolEnv("DEEPCCC_RAW_STREAM_KEEP_COMPLETED") ?? rawLogs.keepCompleted ?? DEFAULT_CONFIG.rawStreamLogs.keepCompleted,
93
+ },
94
+ };
95
+ }
96
+
97
+ export function ensureConfigDir(): void {
98
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
99
+ }
100
+
101
+ export const config = loadConfig();
@@ -5,13 +5,19 @@
5
5
  */
6
6
 
7
7
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
8
+ import { createAnthropic } from "@ai-sdk/anthropic";
8
9
  import { generateText, isLoopFinished, stepCountIs, streamText, type TextStreamPart } from "ai";
9
10
  import { existsSync, readFileSync } from "node:fs";
10
11
  import { homedir } from "node:os";
11
12
  import { join } from "node:path";
12
13
  import { fileURLToPath } from "node:url";
13
14
 
14
- import { config as appConfig, RAW_STREAM_LOGS_DIR } from "./config.js";
15
+ import {
16
+ config as appConfig,
17
+ normalizeDeepCccProvider,
18
+ RAW_STREAM_LOGS_DIR,
19
+ type DeepCccProvider,
20
+ } from "./config.js";
15
21
  import {
16
22
  createRawStreamLog,
17
23
  type RawStreamLogHandle,
@@ -176,8 +182,15 @@ function normalizeMaxSteps(value: number | undefined): number | undefined {
176
182
  return value;
177
183
  }
178
184
 
185
+ function normalizeAnthropicBaseURL(baseURL: string): string {
186
+ const normalized = baseURL.trim().replace(/\/+$/, "");
187
+ return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
188
+ }
189
+
179
190
  export interface ChatSessionConfig {
180
- /** OpenAI-compatible service base URL. Defaults to DEEPCCC_BASE_URL/config. */
191
+ /** API protocol/provider. Defaults to DEEPCCC_PROVIDER/config, then openai. */
192
+ provider?: DeepCccProvider;
193
+ /** Provider service base URL. Defaults to DEEPCCC_BASE_URL/config. */
181
194
  baseURL?: string;
182
195
  /** API key. Defaults to DEEPCCC_API_KEY/config. */
183
196
  apiKey?: string;
@@ -254,6 +267,7 @@ interface ChatMessage {
254
267
 
255
268
  export class ChatSession {
256
269
  private model: any;
270
+ private provider: DeepCccProvider;
257
271
  private cwd: string;
258
272
  private context: BuiltinContextManager;
259
273
  private compactionTimeoutMs: number;
@@ -278,14 +292,24 @@ export class ChatSession {
278
292
 
279
293
  const baseURL = overrides.baseURL ?? appConfig.baseURL;
280
294
  const modelId = overrides.model ?? appConfig.model;
295
+ this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
281
296
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
282
297
 
283
- const provider = createOpenAICompatible({
284
- name: "deepccc",
285
- baseURL,
286
- apiKey,
287
- });
288
- this.model = provider(modelId);
298
+ if (this.provider === "anthropic") {
299
+ const provider = createAnthropic({
300
+ baseURL: normalizeAnthropicBaseURL(baseURL),
301
+ apiKey,
302
+ });
303
+ this.model = provider(modelId);
304
+ } else {
305
+ const provider = createOpenAICompatible({
306
+ name: "deepccc",
307
+ baseURL,
308
+ apiKey,
309
+ includeUsage: true,
310
+ });
311
+ this.model = provider(modelId);
312
+ }
289
313
  this.cwd = options.cwd ?? process.cwd();
290
314
  this.maxSteps = normalizeMaxSteps(options.maxSteps);
291
315
  this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
@@ -383,7 +407,7 @@ export class ChatSession {
383
407
  const skills = await scanSkillsDirs(this.skillDirs);
384
408
  const system = this.buildSystemPrompt(skills);
385
409
  this.systemPrompt = system;
386
- const result = streamText({
410
+ const generationOptions = {
387
411
  model: this.model,
388
412
  system,
389
413
  messages: this.context.buildModelMessages() as any,
@@ -392,10 +416,19 @@ export class ChatSession {
392
416
  abortSignal: signal,
393
417
  // DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
394
418
  // 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
395
- ...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
396
- });
419
+ ...(this.provider === "openai" && this.effort
420
+ ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } }
421
+ : {}),
422
+ };
423
+ let stream: AsyncIterable<TextStreamPart<any>>;
424
+ if (appConfig.streaming) {
425
+ const result = streamText(generationOptions);
426
+ stream = result.fullStream ?? textStreamToFullStream(result.textStream);
427
+ } else {
428
+ const result = await generateText(generationOptions);
429
+ stream = generateResultToFullStream(result);
430
+ }
397
431
 
398
- const stream = result.fullStream ?? textStreamToFullStream(result.textStream);
399
432
  for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
400
433
  rawLog?.writeLine(safeRawStreamJson(part));
401
434
  if (part.type === "text-delta") {
@@ -560,6 +593,35 @@ async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIter
560
593
  }
561
594
  }
562
595
 
596
+ async function* generateResultToFullStream(result: any): AsyncIterable<TextStreamPart<any>> {
597
+ let emittedText = false;
598
+ for (const step of result.steps ?? []) {
599
+ for (const call of step.toolCalls ?? []) {
600
+ yield {
601
+ type: "tool-call",
602
+ toolCallId: call.toolCallId,
603
+ toolName: call.toolName,
604
+ input: call.input,
605
+ } as TextStreamPart<any>;
606
+ }
607
+ for (const toolResult of step.toolResults ?? []) {
608
+ yield {
609
+ type: "tool-result",
610
+ toolCallId: toolResult.toolCallId,
611
+ toolName: toolResult.toolName,
612
+ output: toolResult.output,
613
+ } as TextStreamPart<any>;
614
+ }
615
+ if (step.text) {
616
+ emittedText = true;
617
+ yield { type: "text-delta", text: step.text } as TextStreamPart<any>;
618
+ }
619
+ }
620
+ if (!emittedText && result.text) {
621
+ yield { type: "text-delta", text: result.text } as TextStreamPart<any>;
622
+ }
623
+ }
624
+
563
625
  function safeJson(value: unknown): string {
564
626
  try {
565
627
  return JSON.stringify(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.244",
3
+ "version": "0.2.246",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -49,6 +49,7 @@
49
49
  "postinstall": "node scripts/postinstall-sharp-check.mjs"
50
50
  },
51
51
  "dependencies": {
52
+ "@ai-sdk/anthropic": "^3.0.105",
52
53
  "@ai-sdk/openai-compatible": "^2.0.47",
53
54
  "@larksuiteoapi/node-sdk": "^1.59.0",
54
55
  "@openilink/openilink-sdk-node": "^0.6.0",