chatccc 0.2.186 → 0.2.188

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.
@@ -184,12 +184,13 @@ export function normalizeCodexMessage(
184
184
  // 子进程辅助函数
185
185
  // ---------------------------------------------------------------------------
186
186
 
187
- function spawnCodex(
188
- args: string[],
189
- cwd?: string,
190
- stdinText?: string,
191
- modelOverride?: string,
192
- ): ChildProcess {
187
+ function spawnCodex(
188
+ args: string[],
189
+ cwd?: string,
190
+ stdinText?: string,
191
+ modelOverride?: string,
192
+ effortOverride?: string,
193
+ ): ChildProcess {
193
194
  const allArgs = [...args];
194
195
  const model = modelOverride ?? resolveCodexModel();
195
196
  if (model) {
@@ -197,7 +198,7 @@ function spawnCodex(
197
198
  const execIdx = allArgs.indexOf("exec");
198
199
  allArgs.splice(execIdx + 1, 0, "-m", model);
199
200
  }
200
- const effort = resolveCodexEffort();
201
+ const effort = effortOverride ?? resolveCodexEffort();
201
202
  if (effort) {
202
203
  allArgs.push("-c", `model_reasoning_effort="${effort}"`);
203
204
  }
@@ -258,15 +259,17 @@ async function* readJsonLines(
258
259
  // ---------------------------------------------------------------------------
259
260
 
260
261
  class CodexAdapter implements ToolAdapter {
261
- readonly displayName = "Codex";
262
- readonly sessionDescPrefix = "Codex Session:";
263
- private metaStore: CodexSessionMetaStore;
264
- private modelOverride: string | undefined;
265
-
266
- constructor(metaStore: CodexSessionMetaStore, modelOverride?: string) {
267
- this.metaStore = metaStore;
268
- this.modelOverride = modelOverride;
269
- }
262
+ readonly displayName = "Codex";
263
+ readonly sessionDescPrefix = "Codex Session:";
264
+ private metaStore: CodexSessionMetaStore;
265
+ private modelOverride: string | undefined;
266
+ private effortOverride: string | undefined;
267
+
268
+ constructor(metaStore: CodexSessionMetaStore, modelOverride?: string, effortOverride?: string) {
269
+ this.metaStore = metaStore;
270
+ this.modelOverride = modelOverride;
271
+ this.effortOverride = effortOverride;
272
+ }
270
273
 
271
274
  // createSession: 生成 sessionId,记录 cwd,不创建 Codex 线程(延迟到首次 prompt)
272
275
  async createSession(cwd: string): Promise<CreateSessionResult> {
@@ -296,7 +299,7 @@ class CodexAdapter implements ToolAdapter {
296
299
  ? [...baseArgs, "-C", cwd, "-"]
297
300
  : [...baseArgs, "resume", threadId, "-"];
298
301
 
299
- const proc = spawnCodex(args, cwd, buildCodexPromptText(userText), this.modelOverride);
302
+ const proc = spawnCodex(args, cwd, buildCodexPromptText(userText), this.modelOverride, this.effortOverride);
300
303
  if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
301
304
 
302
305
  // 关键:spawn 用了 shell:true,proc.pid 指向的是壳进程(cmd.exe / sh)。
@@ -350,7 +353,8 @@ class CodexAdapter implements ToolAdapter {
350
353
  export interface CreateCodexAdapterOptions {
351
354
  metaStore?: CodexSessionMetaStore;
352
355
  /** per-session 模型覆盖(/model 命令);传了就用,不传走全局 codex.model */
353
- model?: string;
356
+ model?: string;
357
+ effort?: string;
354
358
  }
355
359
 
356
360
  export function createCodexAdapter(
@@ -358,6 +362,7 @@ export function createCodexAdapter(
358
362
  ): ToolAdapter {
359
363
  return new CodexAdapter(
360
364
  options.metaStore ?? defaultCodexSessionMetaStore,
361
- options.model,
362
- );
363
- }
365
+ options.model,
366
+ options.effort,
367
+ );
368
+ }
@@ -5,14 +5,11 @@
5
5
  * npx tsx src/builtin/cli.ts
6
6
  * npx tsx src/builtin/cli.ts --model deepseek-chat
7
7
  * npx tsx src/builtin/cli.ts --cwd /path/to/project
8
- *
9
- * 环境变量:
10
- * DEEPSEEK_API_KEY — DeepSeek API Key(必需)
11
- * DEEPSEEK_BASE_URL — API 地址(可选,默认 https://api.deepseek.com/v1)
12
8
  */
13
9
 
14
10
  import * as readline from "node:readline";
15
11
  import * as process from "node:process";
12
+ import { config as appConfig } from "../config.ts";
16
13
  import { ChatSession, type ChatSessionConfig, type ChatSessionOptions } from "./index.js";
17
14
 
18
15
  // ---------------------------------------------------------------------------
@@ -55,15 +52,14 @@ function printHelp(): void {
55
52
  "用法: npx tsx src/builtin/cli.ts [选项]",
56
53
  "",
57
54
  "选项:",
58
- " --model <name> 模型名称(默认 deepseek-chat)",
59
- " --base-url <url> API 地址(默认 https://api.deepseek.com/v1)",
60
- " --api-key <key> API Key(默认读 DEEPSEEK_API_KEY 环境变量)",
55
+ ` --model <name> 模型名称(覆盖 config.ccc.model,当前默认 ${appConfig.ccc.model})`,
56
+ ` --base-url <url> API 地址(覆盖 config.ccc.DEEPSEEK_BASE_URL,当前默认 ${appConfig.ccc.DEEPSEEK_BASE_URL})`,
57
+ " --api-key <key> API Key(覆盖 config.ccc.DEEPSEEK_API_KEY",
61
58
  " --cwd <path> 工作目录",
62
59
  " --help, -h 显示帮助",
63
60
  "",
64
- "环境变量:",
65
- " DEEPSEEK_API_KEY DeepSeek API Key",
66
- " DEEPSEEK_BASE_URL API 地址",
61
+ "默认配置来源:",
62
+ " ~/.chatccc/config.json 的 ccc.DEEPSEEK_API_KEY / ccc.DEEPSEEK_BASE_URL / ccc.model",
67
63
  "",
68
64
  ].join("\n"));
69
65
  }
@@ -87,12 +83,8 @@ const C = {
87
83
  async function main(): Promise<void> {
88
84
  const { config, options } = parseArgs();
89
85
 
90
- // 环境变量回退
91
- if (!config.baseURL) config.baseURL = process.env.DEEPSEEK_BASE_URL;
92
- if (!config.apiKey) config.apiKey = process.env.DEEPSEEK_API_KEY;
93
-
94
86
  console.log(`${C.dim}ChatCCC 内置 Agent 原型${C.reset}`);
95
- console.log(`${C.dim}模型: ${config.model ?? "deepseek-chat"}${C.reset}`);
87
+ console.log(`${C.dim}模型: ${config.model ?? appConfig.ccc.model}${C.reset}`);
96
88
  if (options.cwd) {
97
89
  console.log(`${C.dim}目录: ${options.cwd}${C.reset}`);
98
90
  }
@@ -194,4 +186,4 @@ async function main(): Promise<void> {
194
186
  main().catch((err) => {
195
187
  console.error(`${C.yellow}启动失败: ${(err as Error).message}${C.reset}`);
196
188
  process.exit(1);
197
- });
189
+ });
@@ -4,8 +4,11 @@
4
4
  * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块(如 ToolAdapter)调用。
5
5
  */
6
6
 
7
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
7
8
  import { streamText } from "ai";
8
9
 
10
+ import { config as appConfig } from "../config.ts";
11
+
9
12
  // ---------------------------------------------------------------------------
10
13
  // 系统提示词 — 编译期冻结常量
11
14
  // ---------------------------------------------------------------------------
@@ -25,11 +28,11 @@ const SYSTEM_PROMPT = [
25
28
  // ---------------------------------------------------------------------------
26
29
 
27
30
  export interface ChatSessionConfig {
28
- /** DeepSeek API 兼容的服务地址,默认 https://api.deepseek.com/v1 */
31
+ /** DeepSeek API 兼容的服务地址;传入时覆盖 config.ccc.DEEPSEEK_BASE_URL */
29
32
  baseURL?: string;
30
- /** API Key(优先用 DEEPSEEK_API_KEY 环境变量) */
33
+ /** API Key;传入时覆盖 config.ccc.DEEPSEEK_API_KEY */
31
34
  apiKey?: string;
32
- /** 模型名称,默认 deepseek-chat */
35
+ /** 模型名称;传入时覆盖 config.ccc.model */
33
36
  model?: string;
34
37
  }
35
38
 
@@ -66,21 +69,19 @@ export class ChatSession {
66
69
  private messages: ChatMessage[];
67
70
 
68
71
  constructor(
69
- config: ChatSessionConfig = {},
72
+ overrides: ChatSessionConfig = {},
70
73
  options: ChatSessionOptions = {},
71
74
  ) {
72
- const apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY;
75
+ const apiKey = overrides.apiKey ?? appConfig.ccc.DEEPSEEK_API_KEY;
73
76
  if (!apiKey) {
74
77
  throw new Error(
75
- "DEEPSEEK_API_KEY 未设置。请设置环境变量 DEEPSEEK_API_KEY 或传入 apiKey",
78
+ "ccc.DEEPSEEK_API_KEY 未设置。请在 config.json 中配置,或通过 --api-key 临时传入",
76
79
  );
77
80
  }
78
81
 
79
- const baseURL = config.baseURL ?? "https://api.deepseek.com/v1";
80
- const modelId = config.model ?? "deepseek-chat";
82
+ const baseURL = overrides.baseURL ?? appConfig.ccc.DEEPSEEK_BASE_URL;
83
+ const modelId = overrides.model ?? appConfig.ccc.model;
81
84
 
82
- // 动态 import 以支持 ESM 包在 CJS 环境下的加载(tsx 处理)
83
- const { createOpenAICompatible } = require("@ai-sdk/openai-compatible");
84
85
  const provider = createOpenAICompatible({
85
86
  name: "deepseek",
86
87
  baseURL,
@@ -164,4 +165,4 @@ export class ChatSession {
164
165
  const system = this.messages[0];
165
166
  this.messages = [system];
166
167
  }
167
- }
168
+ }
package/src/cards.ts CHANGED
@@ -498,10 +498,10 @@ export function buildCodexResetConfirmCard(params: {
498
498
 
499
499
  /** 模型切换卡片(/model 命令,飞书专用,含切换按钮) */
500
500
  export function buildModelCard(
501
- currentModel: string,
502
- models: string[],
503
- tool?: string,
504
- ): string {
501
+ currentModel: string,
502
+ models: string[],
503
+ tool?: string,
504
+ ): string {
505
505
  const toolLabel = tool ? ` (${tool})` : "";
506
506
  const currentLine = currentModel
507
507
  ? `**当前模型:** \`${currentModel}\``
@@ -535,7 +535,48 @@ export function buildModelCard(
535
535
  { tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
536
536
  { tag: "hr" },
537
537
  buildButtons(buttons),
538
- ],
539
- });
540
- }
538
+ ],
539
+ });
540
+ }
541
+
542
+ export function buildEffortCard(
543
+ currentEffort: string,
544
+ efforts: string[],
545
+ tool?: string,
546
+ ): string {
547
+ const toolLabel = tool ? ` (${tool})` : "";
548
+ const currentLine = currentEffort
549
+ ? `**当前 effort:** \`${currentEffort}\``
550
+ : "**当前 effort:** 未指定";
551
+
552
+ const lines: string[] = [currentLine];
553
+ if (efforts.length > 0) {
554
+ lines.push("", "**可切换 effort**");
555
+ for (const effort of efforts) {
556
+ lines.push(`- \`${effort}\``);
557
+ }
558
+ lines.push("", "点击按钮切换 effort,或输入 `/effort clear` 恢复默认");
559
+ } else {
560
+ lines.push("", "当前 agent 不支持 effort 切换。");
561
+ }
562
+
563
+ const buttons: ButtonDef[] = [];
564
+ for (const effort of efforts.slice(0, 20)) {
565
+ buttons.push({
566
+ text: `/effort ${effort}`,
567
+ value: JSON.stringify({ cmd: `/effort ${effort}` }),
568
+ type: "primary",
569
+ });
570
+ }
571
+
572
+ return JSON.stringify({
573
+ config: { wide_screen_mode: true },
574
+ header: { template: "blue", title: { content: `Effort 切换${toolLabel}`, tag: "plain_text" } },
575
+ elements: [
576
+ { tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
577
+ { tag: "hr" },
578
+ buildButtons(buttons),
579
+ ],
580
+ });
581
+ }
541
582
 
@@ -33,6 +33,18 @@ export interface ChromeCdpEnsureResult {
33
33
 
34
34
  export type ChromeCdpProbeStatus = "healthy" | "occupied" | "unreachable";
35
35
 
36
+ interface ChromeCdpPage {
37
+ id?: string;
38
+ type?: string;
39
+ url?: string;
40
+ }
41
+
42
+ export interface EnsureChatcccPageResult {
43
+ ok: boolean;
44
+ opened: boolean;
45
+ error?: string;
46
+ }
47
+
36
48
  let guardTimer: ReturnType<typeof setInterval> | null = null;
37
49
  let ensureInFlight: Promise<ChromeCdpEnsureResult> | null = null;
38
50
 
@@ -81,11 +93,11 @@ export function resolveChromeUserDataDir(port: number, env: NodeJS.ProcessEnv =
81
93
  return join(root, `chrome-cdp-${port}`);
82
94
  }
83
95
 
84
- async function fetchWithTimeout(url: string, fetchImpl: FetchLike, timeoutMs: number): Promise<Response> {
96
+ async function fetchWithTimeout(url: string, fetchImpl: FetchLike, timeoutMs: number, init: RequestInit = {}): Promise<Response> {
85
97
  const controller = new AbortController();
86
98
  const timer = setTimeout(() => controller.abort(), timeoutMs);
87
99
  try {
88
- return await fetchImpl(url, { signal: controller.signal });
100
+ return await fetchImpl(url, { ...init, signal: controller.signal });
89
101
  } finally {
90
102
  clearTimeout(timer);
91
103
  }
@@ -163,6 +175,61 @@ function sleep(ms: number): Promise<void> {
163
175
  return new Promise((resolve) => setTimeout(resolve, ms));
164
176
  }
165
177
 
178
+ function cdpBaseUrl(port: number): string {
179
+ return `http://${CDP_HOST}:${port}`;
180
+ }
181
+
182
+ function isChatcccPageUrl(value: string | undefined, chatcccPort: number): boolean {
183
+ if (!value) return false;
184
+ try {
185
+ const url = new URL(value);
186
+ return url.protocol === "http:" &&
187
+ (url.hostname === "localhost" || url.hostname === CDP_HOST) &&
188
+ url.port === String(chatcccPort);
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+
194
+ export async function ensureChatcccPageOpen(
195
+ cdpPort: number,
196
+ chatcccPort: number,
197
+ deps: Pick<ChromeDevtoolsGuardDeps, "fetchImpl"> = {},
198
+ ): Promise<EnsureChatcccPageResult> {
199
+ const normalizedCdpPort = normalizePort(cdpPort);
200
+ const normalizedChatcccPort = normalizePort(chatcccPort);
201
+ const fetchImpl = deps.fetchImpl ?? fetch;
202
+
203
+ try {
204
+ const listResponse = await fetchWithTimeout(
205
+ `${cdpBaseUrl(normalizedCdpPort)}/json/list`,
206
+ fetchImpl,
207
+ HEALTH_TIMEOUT_MS,
208
+ );
209
+ if (!listResponse.ok) {
210
+ return { ok: false, opened: false, error: `Cannot list Chrome CDP pages: HTTP ${listResponse.status}` };
211
+ }
212
+ const pages = await listResponse.json() as unknown;
213
+ if (Array.isArray(pages) && pages.some((page: ChromeCdpPage) => isChatcccPageUrl(page?.url, normalizedChatcccPort))) {
214
+ return { ok: true, opened: false };
215
+ }
216
+
217
+ const targetUrl = `http://localhost:${normalizedChatcccPort}/`;
218
+ const openResponse = await fetchWithTimeout(
219
+ `${cdpBaseUrl(normalizedCdpPort)}/json/new?${encodeURIComponent(targetUrl)}`,
220
+ fetchImpl,
221
+ HEALTH_TIMEOUT_MS,
222
+ { method: "PUT" },
223
+ );
224
+ if (!openResponse.ok) {
225
+ return { ok: false, opened: false, error: `Cannot open ${targetUrl}: HTTP ${openResponse.status}` };
226
+ }
227
+ return { ok: true, opened: true };
228
+ } catch (err) {
229
+ return { ok: false, opened: false, error: (err as Error).message };
230
+ }
231
+ }
232
+
166
233
  export async function ensureChromeCdpRunning(
167
234
  cfg: ChromeDevtoolsConfig = config.chromeDevtools,
168
235
  deps: ChromeDevtoolsGuardDeps = {},
@@ -202,6 +269,9 @@ async function runGuardOnce(reason: string, deps: ChromeDevtoolsGuardDeps = {}):
202
269
  ensureInFlight = ensureChromeCdpRunning(config.chromeDevtools, deps);
203
270
  try {
204
271
  const result = await ensureInFlight;
272
+ const chatcccPage = config.chromeDevtools.enabled && result.ok
273
+ ? await ensureChatcccPageOpen(result.port, config.port, deps)
274
+ : null;
205
275
  appendStartupTrace("chrome-devtools-guard: ensure result", {
206
276
  reason,
207
277
  enabled: config.chromeDevtools.enabled,
@@ -209,6 +279,7 @@ async function runGuardOnce(reason: string, deps: ChromeDevtoolsGuardDeps = {}):
209
279
  ok: result.ok,
210
280
  started: result.started,
211
281
  error: result.error,
282
+ chatcccPage,
212
283
  });
213
284
  if (!config.chromeDevtools.enabled) return;
214
285
  if (result.ok && result.started) {
@@ -216,6 +287,11 @@ async function runGuardOnce(reason: string, deps: ChromeDevtoolsGuardDeps = {}):
216
287
  } else if (!result.ok) {
217
288
  log(`[${ts()}] [Chrome CDP] Guard failed: ${result.error}`);
218
289
  }
290
+ if (chatcccPage?.opened) {
291
+ log(`[${ts()}] [Chrome CDP] Opened ChatCCC page http://localhost:${config.port}/`);
292
+ } else if (chatcccPage && !chatcccPage.ok) {
293
+ log(`[${ts()}] [Chrome CDP] Failed to ensure ChatCCC page: ${chatcccPage.error}`);
294
+ }
219
295
  } finally {
220
296
  ensureInFlight = null;
221
297
  }