chatccc 0.2.137 → 0.2.139

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
@@ -311,12 +311,6 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
311
311
  | `/sessions` | 查看所有会话状态 |
312
312
  | `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
313
313
  | `/restart` | 重启机器人进程 |
314
- | `/plan [内容]` | 以 plan 模式传递给 Agent,**不跳过权限检查**(无 `--dangerously-skip-permissions`) |
315
- | `/ask [内容]` | 以 plan 模式传递给 Agent,**不跳过权限检查**(同 `/plan`) |
316
-
317
- > **`/plan` 与 `/ask`**:这两个指令不会被 ChatCCC 当作内置命令处理,而是把用户原话(含指令前缀)完整传给 Agent。与普通消息的区别是:调用时不添加 `--dangerously-skip-permissions`,改为 `--permission-mode plan`,Agent 会先在权限沙盒内运行,遇到命令执行等操作时需要确认。适合需要安全审查的任务场景。
318
- >
319
- > **群名**:`/plan` / `/ask` 消息如果是群内第一条消息,也会触发群名更新。为美观起见,群名会自动去除 `/plan` 或 `/ask` 前缀。
320
314
 
321
315
  > **模型切换**:`/model` 查看可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。当前仅 Claude Code 支持模型切换(需同时填写 `claude.model` 和 `claude.subagentModel`),Cursor / Codex 切换正在开发中。
322
316
 
@@ -50,6 +50,4 @@ Never execute the same successful shell command three times for the same task in
50
50
 
51
51
  If the same command has already succeeded twice in a row, the next assistant action must be a final response to the user, not another tool call.
52
52
 
53
- Successful repeated command execution is a terminal condition. Prefer a concise final response over further tool calls.
54
-
55
- 如果计划写好了等待你的审阅,将计划文件通过当前聊天软件发给用户
53
+ Successful repeated command execution is a terminal condition. Prefer a concise final response over further tool calls.
@@ -18,7 +18,7 @@
18
18
  "effort": "",
19
19
  "apiKey": "",
20
20
  "baseUrl": "",
21
- "maxTurn": 20
21
+ "maxTurn": 0
22
22
  },
23
23
  "cursor": {
24
24
  "enabled": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.137",
3
+ "version": "0.2.139",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@ai-sdk/openai-compatible": "^2.0.47",
44
- "@anthropic-ai/claude-agent-sdk": "0.3.153",
44
+ "@anthropic-ai/claude-agent-sdk": "0.2.133",
45
45
  "@larksuiteoapi/node-sdk": "^1.59.0",
46
46
  "@openilink/openilink-sdk-node": "^0.6.0",
47
47
  "ai": "^6.0.184",
@@ -8,10 +8,11 @@ import {
8
8
  APP_ID,
9
9
  APP_SECRET,
10
10
  CHATCCC_PORT,
11
- CLAUDE_API_KEY,
12
- CLAUDE_BASE_URL,
13
- CLAUDE_EFFORT,
14
- CLAUDE_MODEL,
11
+ CLAUDE_API_KEY,
12
+ CLAUDE_BASE_URL,
13
+ CLAUDE_EFFORT,
14
+ CLAUDE_MAX_TURN,
15
+ CLAUDE_MODEL,
15
16
  CLAUDE_SUBAGENT_MODEL,
16
17
  CURSOR_AGENT_ARGS,
17
18
  CURSOR_AGENT_COMMAND,
@@ -81,7 +82,7 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
81
82
  expect(APP_SECRET).toBe("NEW_APP_SECRET");
82
83
  });
83
84
 
84
- it("更新 Claude 配置(model / subagentModel / effort / apiKey / baseUrl)", () => {
85
+ it("更新 Claude 配置(model / subagentModel / effort / maxTurn / apiKey / baseUrl)", () => {
85
86
  applyLoadedConfig({
86
87
  ...structuredClone(baseAppConfig),
87
88
  claude: {
@@ -89,17 +90,18 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
89
90
  defaultAgent: true,
90
91
  model: "claude-sonnet-4-6",
91
92
  subagentModel: "claude-haiku-4-5-20251001",
92
- effort: "high",
93
- apiKey: "sk-test",
94
- baseUrl: "https://api.example.com",
95
- maxTurn: 0,
96
- },
97
- });
98
-
99
- expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
100
- expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
101
- expect(CLAUDE_EFFORT).toBe("high");
102
- expect(CLAUDE_API_KEY).toBe("sk-test");
93
+ effort: "high",
94
+ apiKey: "sk-test",
95
+ baseUrl: "https://api.example.com",
96
+ maxTurn: 7,
97
+ },
98
+ });
99
+
100
+ expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
101
+ expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
102
+ expect(CLAUDE_EFFORT).toBe("high");
103
+ expect(CLAUDE_MAX_TURN).toBe(7);
104
+ expect(CLAUDE_API_KEY).toBe("sk-test");
103
105
  expect(CLAUDE_BASE_URL).toBe("https://api.example.com");
104
106
  });
105
107
 
@@ -128,8 +128,6 @@ export interface ToolPromptOptions {
128
128
  onProcessStart?: (info: ToolProcessInfo) => void;
129
129
  /** Called when the adapter leaves the prompt process scope normally or by abort. */
130
130
  onProcessExit?: (info: ToolProcessInfo) => void;
131
- /** 是否为 /plan 或 /ask 模式:不开启 dangerous skip permissions */
132
- planMode?: boolean;
133
131
  }
134
132
 
135
133
  // ---------------------------------------------------------------------------
@@ -5,9 +5,12 @@ import { fileURLToPath } from "node:url";
5
5
 
6
6
  import {
7
7
  getSessionInfo as sdkGetSessionInfo,
8
- query,
9
- type Options as ClaudeSdkOptions,
8
+ unstable_v2_createSession,
9
+ unstable_v2_resumeSession,
10
10
  type SDKMessage,
11
+ type SDKSession,
12
+ type SDKSessionOptions,
13
+ type EffortLevel,
11
14
  } from "@anthropic-ai/claude-agent-sdk";
12
15
 
13
16
  import type {
@@ -220,6 +223,17 @@ export function buildClaudePromptText(
220
223
  ].join("\n");
221
224
  }
222
225
 
226
+ type ClaudeSdkSessionOptions = Omit<SDKSessionOptions, "model"> & {
227
+ model?: string;
228
+ abortController?: AbortController;
229
+ autoCompactEnabled?: boolean;
230
+ effort?: EffortLevel | number;
231
+ maxTurns?: number;
232
+ mcpServers?: Record<string, unknown>;
233
+ skills?: "all";
234
+ stderr?: (data: string) => void;
235
+ };
236
+
223
237
  function buildSdkOptions(args: {
224
238
  cwd: string;
225
239
  model: string;
@@ -229,10 +243,8 @@ function buildSdkOptions(args: {
229
243
  apiKey?: string;
230
244
  baseUrl?: string;
231
245
  maxTurn: number;
232
- planMode?: boolean;
233
- resume?: string;
234
246
  abortController?: AbortController;
235
- }): ClaudeSdkOptions {
247
+ }): ClaudeSdkSessionOptions {
236
248
  const {
237
249
  cwd,
238
250
  model,
@@ -242,18 +254,16 @@ function buildSdkOptions(args: {
242
254
  apiKey,
243
255
  baseUrl,
244
256
  maxTurn,
245
- planMode,
246
- resume,
247
257
  abortController,
248
258
  } = args;
249
259
 
250
- const options: ClaudeSdkOptions = {
260
+ const options: ClaudeSdkSessionOptions = {
251
261
  cwd,
252
262
  abortController,
253
263
  settingSources: ["user", "project", "local"] as SettingSource[],
254
- permissionMode: planMode ? "plan" : "bypassPermissions",
264
+ permissionMode: "bypassPermissions",
265
+ autoCompactEnabled: true,
255
266
  maxTurns: maxTurn,
256
- maxBudgetUsd: 999999999,
257
267
  skills: "all",
258
268
  stderr: (data) => {
259
269
  const trimmed = data.trim();
@@ -263,17 +273,12 @@ function buildSdkOptions(args: {
263
273
  },
264
274
  };
265
275
 
266
- if (!planMode) {
267
- options.allowDangerouslySkipPermissions = true;
268
- }
276
+ options.allowDangerouslySkipPermissions = true;
269
277
  if (!isEmpty(model)) {
270
278
  options.model = model;
271
279
  }
272
280
  if (!isEmpty(effort)) {
273
- options.effort = effort as ClaudeSdkOptions["effort"];
274
- }
275
- if (resume) {
276
- options.resume = resume;
281
+ options.effort = effort as ClaudeSdkSessionOptions["effort"];
277
282
  }
278
283
 
279
284
  const env = buildSdkEnv(subagentModel, apiKey, baseUrl);
@@ -283,7 +288,7 @@ function buildSdkOptions(args: {
283
288
 
284
289
  const mcpServers = readMcpServersConfig();
285
290
  if (mcpServers) {
286
- options.mcpServers = mcpServers as ClaudeSdkOptions["mcpServers"];
291
+ options.mcpServers = mcpServers;
287
292
  }
288
293
 
289
294
  return options;
@@ -307,6 +312,14 @@ function bridgeAbortSignal(
307
312
  return () => signal.removeEventListener("abort", onAbort);
308
313
  }
309
314
 
315
+ function closeSdkSession(session: SDKSession): void {
316
+ session.close();
317
+ }
318
+
319
+ function toSdkSessionOptions(options: ClaudeSdkSessionOptions): SDKSessionOptions {
320
+ return options as SDKSessionOptions;
321
+ }
322
+
310
323
  class ClaudeAdapter implements ToolAdapter {
311
324
  readonly displayName = "Claude Code";
312
325
  readonly sessionDescPrefix = "Claude Code Session:";
@@ -334,9 +347,8 @@ class ClaudeAdapter implements ToolAdapter {
334
347
  logMcpConfig();
335
348
  const abortController = new AbortController();
336
349
  let sessionId: string | undefined;
337
- const stream = query({
338
- prompt: "ok",
339
- options: buildSdkOptions({
350
+ const session = unstable_v2_createSession(
351
+ toSdkSessionOptions(buildSdkOptions({
340
352
  cwd,
341
353
  model: this.model,
342
354
  effort: this.effort,
@@ -346,11 +358,12 @@ class ClaudeAdapter implements ToolAdapter {
346
358
  baseUrl: this.baseUrl,
347
359
  maxTurn: this.maxTurn,
348
360
  abortController,
349
- }),
350
- });
361
+ })),
362
+ );
351
363
 
352
364
  try {
353
- for await (const raw of stream) {
365
+ await session.send("ok");
366
+ for await (const raw of session.stream()) {
354
367
  const msg = toMessageLike(raw);
355
368
  if (msg.session_id && !sessionId) {
356
369
  sessionId = msg.session_id;
@@ -363,7 +376,7 @@ class ClaudeAdapter implements ToolAdapter {
363
376
  }
364
377
  }
365
378
  } finally {
366
- stream.close();
379
+ closeSdkSession(session);
367
380
  }
368
381
 
369
382
  if (sessionId) return { sessionId };
@@ -382,9 +395,9 @@ class ClaudeAdapter implements ToolAdapter {
382
395
  if (abortController.signal.aborted) return;
383
396
  let aborted = false;
384
397
 
385
- const stream = query({
386
- prompt: buildClaudePromptText(userText),
387
- options: buildSdkOptions({
398
+ const session = unstable_v2_resumeSession(
399
+ sessionId,
400
+ toSdkSessionOptions(buildSdkOptions({
388
401
  cwd,
389
402
  model: this.model,
390
403
  effort: this.effort,
@@ -393,14 +406,13 @@ class ClaudeAdapter implements ToolAdapter {
393
406
  apiKey: this.apiKey,
394
407
  baseUrl: this.baseUrl,
395
408
  maxTurn: this.maxTurn,
396
- planMode: options?.planMode,
397
- resume: sessionId,
398
409
  abortController,
399
- }),
400
- });
410
+ })),
411
+ );
401
412
 
402
413
  try {
403
- for await (const raw of stream) {
414
+ await session.send(buildClaudePromptText(userText));
415
+ for await (const raw of session.stream()) {
404
416
  if (abortController.signal.aborted) {
405
417
  aborted = true;
406
418
  break;
@@ -423,8 +435,8 @@ class ClaudeAdapter implements ToolAdapter {
423
435
  removeAbortListener?.();
424
436
  if (aborted || abortController.signal.aborted) {
425
437
  abortController.abort();
426
- stream.close();
427
438
  }
439
+ closeSdkSession(session);
428
440
  }
429
441
  }
430
442
 
package/src/config.ts CHANGED
@@ -605,8 +605,9 @@ export function applyLoadedConfig(next: AppConfig): void {
605
605
  ILINK_REUSE_TOKEN_ON_START = next.platforms.ilink.reuseTokenOnStart ?? true;
606
606
  CLAUDE_MODEL = next.claude.model;
607
607
  CLAUDE_SUBAGENT_MODEL = next.claude.subagentModel;
608
- CLAUDE_EFFORT = next.claude.effort;
609
- CLAUDE_API_KEY = next.claude.apiKey;
608
+ CLAUDE_EFFORT = next.claude.effort;
609
+ CLAUDE_MAX_TURN = next.claude.maxTurn;
610
+ CLAUDE_API_KEY = next.claude.apiKey;
610
611
  CLAUDE_BASE_URL = next.claude.baseUrl;
611
612
  GIT_TIMEOUT_SECONDS = next.gitTimeoutSeconds;
612
613
  GIT_TIMEOUT_MS = GIT_TIMEOUT_SECONDS * 1000;
@@ -94,21 +94,6 @@ export function sessionChatName(left: string, cwd: string): string {
94
94
  return `${left}-${cwdDisplayName(cwd)}`;
95
95
  }
96
96
 
97
- /** 判断用户消息是否以 /plan 或 /ask 开头(忽略大小写和尾部空格) */
98
- function isPlanOrAsk(text: string): boolean {
99
- const t = text.toLowerCase().trim();
100
- return t === "/plan" || t.startsWith("/plan ") || t === "/ask" || t.startsWith("/ask ");
101
- }
102
-
103
- /** 剥离 /plan 或 /ask 前缀,用于群名等展示场景 */
104
- function stripPlanOrAskPrefix(text: string): string {
105
- if (isPlanOrAsk(text)) {
106
- const match = text.match(/^\/(?:plan|ask)\s*(.*)/i);
107
- return (match?.[1] ?? "").trim() || text.trim();
108
- }
109
- return text;
110
- }
111
-
112
97
  /** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
113
98
  function findModelMatch(input: string, models: string[]): string | null {
114
99
  if (models.length === 0) return null;
@@ -134,7 +119,7 @@ function shouldSendWechatProcessingAck(
134
119
  chatType: string,
135
120
  text: string,
136
121
  ): boolean {
137
- return platform.kind === "wechat" && chatType === "p2p" && (!textLower.startsWith("/") || isPlanOrAsk(text));
122
+ return platform.kind === "wechat" && chatType === "p2p" && !textLower.startsWith("/");
138
123
  }
139
124
 
140
125
  function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
@@ -616,10 +601,10 @@ export async function handleCommand(
616
601
  if (
617
602
  chatType !== "p2p" &&
618
603
  isUntitledSessionChatName(chatInfo!.name) &&
619
- (!textLower.startsWith("/") || isPlanOrAsk(text))
604
+ !textLower.startsWith("/")
620
605
  ) {
621
606
  const MAX_PREFIX = 10;
622
- const prefix = stripPlanOrAskPrefix(text).slice(0, MAX_PREFIX);
607
+ const prefix = text.slice(0, MAX_PREFIX);
623
608
  const adapter = getAdapterForTool(descriptionTool, sessionId);
624
609
  const info = await adapter
625
610
  .getSessionInfo(sessionId)
@@ -651,7 +636,7 @@ export async function handleCommand(
651
636
  if (
652
637
  chatType === "p2p" &&
653
638
  platform.kind === "wechat" &&
654
- (!textLower.startsWith("/") || isPlanOrAsk(text))
639
+ !textLower.startsWith("/")
655
640
  ) {
656
641
  try {
657
642
  const reg = await loadSessionRegistryForBinding();
@@ -1253,7 +1238,7 @@ export async function handleCommand(
1253
1238
  msgTimestamp,
1254
1239
  descriptionTool,
1255
1240
  tid,
1256
- isPlanOrAsk(text),
1241
+ false,
1257
1242
  );
1258
1243
  logTrace(tid, "DONE", { outcome: "resume_done", sessionId });
1259
1244
  console.log(`[${ts()}] [RESUME] Session ${sessionId} done`);
@@ -1328,7 +1313,7 @@ export async function handleCommand(
1328
1313
  }
1329
1314
 
1330
1315
  // 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
1331
- if (isNonWechatP2p(platform, chatType) && (!textLower.startsWith("/") || isPlanOrAsk(text))) {
1316
+ if (isNonWechatP2p(platform, chatType) && !textLower.startsWith("/")) {
1332
1317
  const tool = resolveDefaultAgentTool();
1333
1318
  const toolLabel = toolDisplayName(tool);
1334
1319
  logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
@@ -1369,7 +1354,7 @@ export async function handleCommand(
1369
1354
  }
1370
1355
 
1371
1356
  const cwd = sessionCwd;
1372
- const initialName = sessionChatName(stripPlanOrAskPrefix(text).slice(0, 10) || "新会话", cwd);
1357
+ const initialName = sessionChatName(text.slice(0, 10) || "新会话", cwd);
1373
1358
  let newChatId: string;
1374
1359
  try {
1375
1360
  newChatId = await platform.createGroup(initialName, [openId]);
@@ -1453,7 +1438,7 @@ export async function handleCommand(
1453
1438
  msgTimestamp,
1454
1439
  tool,
1455
1440
  tid,
1456
- isPlanOrAsk(text),
1441
+ false,
1457
1442
  );
1458
1443
  console.log(`[${ts()}] [AUTO-P2P 5/5] First prompt sent → OK`);
1459
1444
  logTrace(tid, "DONE", { outcome: "auto_new_p2p_prompt_done", newChatId, sessionId, tool });
package/src/session.ts CHANGED
@@ -834,9 +834,8 @@ export async function resumeAndPrompt(
834
834
  msgTimestamp: number,
835
835
  tool: string,
836
836
  traceId?: string,
837
- planMode?: boolean,
838
837
  ): Promise<void> {
839
- return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId, planMode);
838
+ return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
840
839
  }
841
840
 
842
841
  // ---------------------------------------------------------------------------
@@ -851,7 +850,6 @@ export async function runAgentSession(
851
850
  msgTimestamp: number,
852
851
  tool: string,
853
852
  traceId?: string,
854
- planMode?: boolean,
855
853
  ): Promise<void> {
856
854
  const tid = traceId ?? "";
857
855
 
@@ -1110,7 +1108,6 @@ export async function runAgentSession(
1110
1108
  clearPromptProcessMonitor(sessionId);
1111
1109
  if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
1112
1110
  },
1113
- planMode,
1114
1111
  })) {
1115
1112
  for (const block of unifiedMsg.blocks) {
1116
1113
  accumulateBlockContent(block, state, toolCallMap);