chatccc 0.2.138 → 0.2.140

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
 
@@ -1,13 +1,3 @@
1
- # Claude-Specific Injection Prompt
2
-
3
- Use this prompt as the Claude Agent SDK injection prompt for this project.
4
-
5
- Project workspace:
6
-
7
- ```text
8
- f:\users\weizhangjian\feishuclauderprivate
9
- ```
10
-
11
1
  ## Repeated Successful Command Guard
12
2
 
13
3
  When working in this project through Claude Agent SDK, repeated successful shell commands are a completion signal, not a reason to keep using tools.
@@ -50,6 +40,4 @@ Never execute the same successful shell command three times for the same task in
50
40
 
51
41
  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
42
 
53
- Successful repeated command execution is a terminal condition. Prefer a concise final response over further tool calls.
54
-
55
- 如果计划写好了等待你的审阅,将计划文件通过当前聊天软件发给用户
43
+ Successful repeated command execution is a terminal condition. Prefer a concise final response over further tool calls.
@@ -1 +0,0 @@
1
- 如果计划写好了等待你的审阅,将计划文件通过当前聊天软件发给用户
@@ -1 +0,0 @@
1
- 如果计划写好了等待你的审阅,将计划文件通过当前聊天软件发给用户
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.138",
3
+ "version": "0.2.140",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -467,7 +467,37 @@ describe("buildClaudePromptText", () => {
467
467
 
468
468
  describe("buildSdkEnv", () => {
469
469
  it("returns undefined when no SDK env override is configured", () => {
470
- expect(buildSdkEnv("", " ", undefined)).toBeUndefined();
470
+ const originalPath = process.env.PATH;
471
+ try {
472
+ process.env.PATH = process.platform === "win32"
473
+ ? "C:\\Program Files\\Git\\usr\\bin;C:\\Windows\\System32"
474
+ : "/usr/bin:/bin";
475
+ expect(buildSdkEnv("", " ", undefined)).toBeUndefined();
476
+ } finally {
477
+ process.env.PATH = originalPath;
478
+ }
479
+ });
480
+
481
+ it("prefers Git Bash before WindowsApps for Claude SDK subprocesses on Windows", () => {
482
+ if (process.platform !== "win32") return;
483
+ const originalPath = process.env.PATH;
484
+ try {
485
+ process.env.PATH = [
486
+ "C:\\Users\\weizhangjian\\AppData\\Local\\Microsoft\\WindowsApps",
487
+ "C:\\Program Files\\Git\\usr\\bin",
488
+ "%PATH%",
489
+ "C:\\Windows\\System32",
490
+ ].join(";");
491
+
492
+ const env = buildSdkEnv("", "", "");
493
+
494
+ expect(env).toBeDefined();
495
+ const parts = env!.PATH!.split(";");
496
+ expect(parts[0]).toBe("C:\\Program Files\\Git\\usr\\bin");
497
+ expect(parts).not.toContain("%PATH%");
498
+ } finally {
499
+ process.env.PATH = originalPath;
500
+ }
471
501
  });
472
502
 
473
503
  it("sets requested SDK env overrides and removes conflicting Claude auth/model vars", () => {
@@ -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
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { delimiter, dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  import {
@@ -82,23 +82,73 @@ export function buildSdkEnv(
82
82
  const apiKeyTrim = (apiKey ?? "").trim();
83
83
  const baseUrlTrim = (baseUrl ?? "").trim();
84
84
 
85
- if (!subagentModelTrim && !apiKeyTrim && !baseUrlTrim) return undefined;
86
-
87
85
  const env: Record<string, string | undefined> = { ...process.env };
88
- delete env.ANTHROPIC_AUTH_TOKEN;
89
- delete env.CLAUDE_CODE_OAUTH_TOKEN;
90
- delete env.ANTHROPIC_MODEL;
91
- delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
92
- delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
93
- delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
94
- delete env.CLAUDE_CODE_EFFORT_LEVEL;
95
- delete env.CLAUDE_CODE_SUBAGENT_MODEL;
86
+ let mutated = preferGitBashOnWindows(env);
87
+
88
+ if (subagentModelTrim || apiKeyTrim || baseUrlTrim) {
89
+ delete env.ANTHROPIC_AUTH_TOKEN;
90
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
91
+ delete env.ANTHROPIC_MODEL;
92
+ delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
93
+ delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
94
+ delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
95
+ delete env.CLAUDE_CODE_EFFORT_LEVEL;
96
+ delete env.CLAUDE_CODE_SUBAGENT_MODEL;
97
+ mutated = true;
98
+ }
96
99
 
97
100
  if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
98
101
  if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
99
102
  if (baseUrlTrim) env.ANTHROPIC_BASE_URL = baseUrlTrim;
100
103
 
101
- return env;
104
+ return mutated ? env : undefined;
105
+ }
106
+
107
+ function preferGitBashOnWindows(env: Record<string, string | undefined>): boolean {
108
+ if (process.platform !== "win32") return false;
109
+
110
+ const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
111
+ const rawPath = env[pathKey];
112
+ if (!rawPath) return false;
113
+
114
+ const parts = rawPath.split(delimiter).filter((part) => part && part !== "%PATH%");
115
+ const preferred = findPreferredGitBashPath(parts);
116
+ if (!preferred) {
117
+ const nextPath = parts.join(delimiter);
118
+ if (nextPath !== rawPath) {
119
+ env[pathKey] = nextPath;
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ const preferredLower = preferred.toLowerCase();
126
+ const reordered = [
127
+ preferred,
128
+ ...parts.filter((part) => part.toLowerCase() !== preferredLower),
129
+ ];
130
+ const nextPath = reordered.join(delimiter);
131
+ if (nextPath === rawPath) return false;
132
+ env[pathKey] = nextPath;
133
+ return true;
134
+ }
135
+
136
+ function findPreferredGitBashPath(pathParts: string[]): string | undefined {
137
+ const programFilesGit = join(
138
+ process.env.ProgramFiles ?? "C:\\Program Files",
139
+ "Git",
140
+ "usr",
141
+ "bin",
142
+ );
143
+ if (existsSync(join(programFilesGit, "bash.exe"))) return programFilesGit;
144
+
145
+ return pathParts.find((part) => {
146
+ if (!/(\\|\/)(Git)(\\|\/)usr(\\|\/)bin$/i.test(part) &&
147
+ !/(\\|\/)Fork(\\|\/)gitInstance(\\|\/)[^\\/]+(\\|\/)bin$/i.test(part)) {
148
+ return false;
149
+ }
150
+ return existsSync(join(part, "bash.exe"));
151
+ });
102
152
  }
103
153
 
104
154
  function readMcpServersConfig(): Record<string, unknown> | undefined {
@@ -243,7 +293,6 @@ function buildSdkOptions(args: {
243
293
  apiKey?: string;
244
294
  baseUrl?: string;
245
295
  maxTurn: number;
246
- planMode?: boolean;
247
296
  abortController?: AbortController;
248
297
  }): ClaudeSdkSessionOptions {
249
298
  const {
@@ -255,7 +304,6 @@ function buildSdkOptions(args: {
255
304
  apiKey,
256
305
  baseUrl,
257
306
  maxTurn,
258
- planMode,
259
307
  abortController,
260
308
  } = args;
261
309
 
@@ -263,7 +311,7 @@ function buildSdkOptions(args: {
263
311
  cwd,
264
312
  abortController,
265
313
  settingSources: ["user", "project", "local"] as SettingSource[],
266
- permissionMode: planMode ? "plan" : "bypassPermissions",
314
+ permissionMode: "bypassPermissions",
267
315
  autoCompactEnabled: true,
268
316
  maxTurns: maxTurn,
269
317
  skills: "all",
@@ -275,9 +323,7 @@ function buildSdkOptions(args: {
275
323
  },
276
324
  };
277
325
 
278
- if (!planMode) {
279
- options.allowDangerouslySkipPermissions = true;
280
- }
326
+ options.allowDangerouslySkipPermissions = true;
281
327
  if (!isEmpty(model)) {
282
328
  options.model = model;
283
329
  }
@@ -410,7 +456,6 @@ class ClaudeAdapter implements ToolAdapter {
410
456
  apiKey: this.apiKey,
411
457
  baseUrl: this.baseUrl,
412
458
  maxTurn: this.maxTurn,
413
- planMode: options?.planMode,
414
459
  abortController,
415
460
  })),
416
461
  );
@@ -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();
@@ -1250,11 +1235,10 @@ export async function handleCommand(
1250
1235
  text,
1251
1236
  platform,
1252
1237
  chatId,
1253
- msgTimestamp,
1254
- descriptionTool,
1255
- tid,
1256
- isPlanOrAsk(text),
1257
- );
1238
+ msgTimestamp,
1239
+ descriptionTool,
1240
+ tid,
1241
+ );
1258
1242
  logTrace(tid, "DONE", { outcome: "resume_done", sessionId });
1259
1243
  console.log(`[${ts()}] [RESUME] Session ${sessionId} done`);
1260
1244
  } catch (err) {
@@ -1328,7 +1312,7 @@ export async function handleCommand(
1328
1312
  }
1329
1313
 
1330
1314
  // 飞书私聊普通消息:不再绑定私聊本身,而是自动创建会话群并把私聊内容作为首轮 prompt。
1331
- if (isNonWechatP2p(platform, chatType) && (!textLower.startsWith("/") || isPlanOrAsk(text))) {
1315
+ if (isNonWechatP2p(platform, chatType) && !textLower.startsWith("/")) {
1332
1316
  const tool = resolveDefaultAgentTool();
1333
1317
  const toolLabel = toolDisplayName(tool);
1334
1318
  logTrace(tid, "BRANCH", { cmd: "auto_new_from_p2p", tool });
@@ -1369,7 +1353,7 @@ export async function handleCommand(
1369
1353
  }
1370
1354
 
1371
1355
  const cwd = sessionCwd;
1372
- const initialName = sessionChatName(stripPlanOrAskPrefix(text).slice(0, 10) || "新会话", cwd);
1356
+ const initialName = sessionChatName(text.slice(0, 10) || "新会话", cwd);
1373
1357
  let newChatId: string;
1374
1358
  try {
1375
1359
  newChatId = await platform.createGroup(initialName, [openId]);
@@ -1450,11 +1434,10 @@ export async function handleCommand(
1450
1434
  text,
1451
1435
  platform,
1452
1436
  newChatId,
1453
- msgTimestamp,
1454
- tool,
1455
- tid,
1456
- isPlanOrAsk(text),
1457
- );
1437
+ msgTimestamp,
1438
+ tool,
1439
+ tid,
1440
+ );
1458
1441
  console.log(`[${ts()}] [AUTO-P2P 5/5] First prompt sent → OK`);
1459
1442
  logTrace(tid, "DONE", { outcome: "auto_new_p2p_prompt_done", newChatId, sessionId, tool });
1460
1443
  } catch (err) {
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
 
@@ -1015,7 +1013,9 @@ export async function runAgentSession(
1015
1013
  const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
1016
1014
  const cardContent = truncateContent(prevState.accumulatedContent + prevState.finalReply) || " ";
1017
1015
  const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
1018
- await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
1016
+ await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
1017
+ console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${(err as Error).message}`);
1018
+ });
1019
1019
  // cardUpdate IO 期间统一 loop 可能也已处理此 display → 删前检查引用
1020
1020
  const stillOursAfterUpdate = displayCards.get(displayChatId) === display;
1021
1021
  displayCards.delete(displayChatId);
@@ -1110,7 +1110,6 @@ export async function runAgentSession(
1110
1110
  clearPromptProcessMonitor(sessionId);
1111
1111
  if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
1112
1112
  },
1113
- planMode,
1114
1113
  })) {
1115
1114
  for (const block of unifiedMsg.blocks) {
1116
1115
  accumulateBlockContent(block, state, toolCallMap);
@@ -1347,7 +1346,9 @@ export function startUnifiedDisplayLoop(): void {
1347
1346
  const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
1348
1347
  const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
1349
1348
  const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
1350
- await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
1349
+ await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
1350
+ console.error(`[${ts()}] [DISPLAY] terminal cardUpdate failed: ${(err as Error).message}`);
1351
+ });
1351
1352
 
1352
1353
  // 若 session 仍在 activePrompts 中,说明 runAgentSession 的 finally
1353
1354
  // 还没执行,当前 stream state 可能是 stopSession fire-and-forget
@@ -1422,7 +1423,9 @@ export function startUnifiedDisplayLoop(): void {
1422
1423
  const oldSeqBase = display.sequence;
1423
1424
  const oldContent = state.accumulatedContent + state.finalReply;
1424
1425
  const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "生成中(上轮)" });
1425
- await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).catch(() => {});
1426
+ await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).catch(err => {
1427
+ console.error(`[${ts()}] [DISPLAY] rotation old cardUpdate failed: ${(err as Error).message}`);
1428
+ });
1426
1429
  markCardDone(sessionId, display.turnCount, display.cardId).catch(() => {});
1427
1430
  const newCardId = await p.cardCreate(buildProgressCard(
1428
1431
  "",