chatccc 0.2.122 → 0.2.124

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.
@@ -169,48 +169,6 @@ describe("handleCommand WeChat processing ack", () => {
169
169
  expect(platform.sendText).toHaveBeenCalledWith("wx-chat", "生成中...");
170
170
  });
171
171
 
172
- it.each([
173
- { tool: "claude" as const, sessionId: "sid-claude", text: "/ask 只回答不要改文件" },
174
- { tool: "cursor" as const, sessionId: "sid-cursor", text: "/plan 先给我计划" },
175
- ])("passes unsupported mode commands through as raw prompts for $tool", async ({ tool, sessionId, text }) => {
176
- const platform = mockPlatform();
177
- const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
178
- yield {
179
- type: "assistant" as const,
180
- blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
181
- };
182
- });
183
- _setAdapterForToolForTest(tool, {
184
- displayName: tool,
185
- sessionDescPrefix: `${tool} Session:`,
186
- createSession: vi.fn(async () => ({ sessionId })),
187
- prompt,
188
- getSessionInfo: async (sessionId: string): Promise<SessionInfo> => ({
189
- sessionId,
190
- cwd: "F:\\repo",
191
- }),
192
- closeSession: async () => {},
193
- });
194
- await recordSessionRegistry({
195
- chatId: "wx-chat",
196
- sessionId,
197
- tool,
198
- chatName: "ready-session",
199
- running: false,
200
- });
201
-
202
- await handleCommand(platform, text, "wx-chat", "wx-user", Date.now(), "p2p");
203
-
204
- expect(platform.sendText).not.toHaveBeenCalledWith("wx-chat", expect.stringContaining("仅在以下 Agent 会话中可用"));
205
- expect(prompt).toHaveBeenCalledWith(
206
- sessionId,
207
- expect.stringContaining(text),
208
- "F:\\repo",
209
- expect.any(AbortSignal),
210
- expect.objectContaining({ permissionMode: undefined }),
211
- );
212
- });
213
-
214
172
  it("cleans stale Feishu p2p binding, creates a group, and sends the private message as first prompt", async () => {
215
173
  const platform = mockPlatform("feishu");
216
174
  const prompt = vi.fn(async function* (_sessionId: string, userText: string) {
@@ -219,7 +177,7 @@ describe("handleCommand WeChat processing ack", () => {
219
177
  blocks: [{ type: "text" as const, text: `收到: ${userText}` }],
220
178
  };
221
179
  });
222
- _setAdapterForToolForTest("claude", {
180
+ _setAdapterForToolForTest("cursor", {
223
181
  displayName: "Claude",
224
182
  sessionDescPrefix: "Claude Session:",
225
183
  createSession: vi.fn(async () => ({ sessionId: "sid-feishu-new" })),
@@ -128,13 +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
- /**
132
- * 权限模式覆盖:
133
- * - "plan": Claude Code --permission-mode plan(只读/规划)
134
- * - "ask": Cursor Agent --mode ask(只读/问答)
135
- * 不传或为 undefined 时使用适配器默认行为(正常编辑模式)。
136
- */
137
- permissionMode?: "plan" | "ask";
138
131
  }
139
132
 
140
133
  // ---------------------------------------------------------------------------
@@ -274,17 +274,14 @@ function buildCliArgs(
274
274
  isEmpty: (value: string) => boolean,
275
275
  mcpConfigJson: string | null,
276
276
  extraArgs: string[],
277
- permissionMode?: "plan" | "ask",
278
277
  ): string[] {
279
- const permMode = permissionMode === "plan" ? "plan" : permissionMode === "ask" ? "default" : "bypassPermissions";
280
- const skipPermissions = permissionMode !== "plan" && permissionMode !== "ask";
281
278
  const args = [
282
279
  "-p",
283
280
  "--output-format", "stream-json",
284
281
  "--verbose",
285
282
  "--setting-sources", "user,project,local",
286
- "--permission-mode", permMode,
287
- ...(skipPermissions ? ["--dangerously-skip-permissions"] : []),
283
+ "--permission-mode", "bypassPermissions",
284
+ "--dangerously-skip-permissions",
288
285
  "--settings", "{\"maxTurns\":0}",
289
286
  ];
290
287
 
@@ -435,7 +432,6 @@ class ClaudeAdapter implements ToolAdapter {
435
432
  const args = buildCliArgs(
436
433
  this.model, this.effort, this.isEmpty, mcpConfigJson,
437
434
  ["--resume", sessionId, "--input-format", "stream-json", "--replay-user-messages"],
438
- options?.permissionMode,
439
435
  );
440
436
 
441
437
  const proc = spawnCli(args, cwd, env, true);
@@ -261,18 +261,8 @@ function spawnAgent(
261
261
  cwd?: string,
262
262
  stdinText?: string,
263
263
  modelOverride?: string,
264
- permissionMode?: "plan" | "ask",
265
264
  ): ChildProcess {
266
265
  let allArgs = [...CURSOR_AGENT_ARGS, ...extraArgs];
267
- if (permissionMode === "ask") {
268
- // 替换已有的 --mode 或追加
269
- const modeIdx = allArgs.findIndex((a, i) => a === "--mode" && i + 1 < allArgs.length);
270
- if (modeIdx >= 0) {
271
- allArgs[modeIdx + 1] = "ask";
272
- } else {
273
- allArgs.push("--mode", "ask");
274
- }
275
- }
276
266
  if (modelOverride) {
277
267
  // 替换全局 --model 为 per-session override
278
268
  const modelIdx = allArgs.findIndex((a, i) => a === "--model" && i + 1 < allArgs.length);
@@ -380,7 +370,7 @@ class CursorAdapter implements ToolAdapter {
380
370
  options?: ToolPromptOptions,
381
371
  ): AsyncIterable<UnifiedStreamMessage> {
382
372
  console.log(`[Cursor debug] prompt start: sessionId=${sessionId}, cwd=${cwd}, userTextLen=${userText.length}`);
383
- const proc = spawnAgent(["--resume", sessionId], cwd, userText, this.modelOverride, options?.permissionMode);
373
+ const proc = spawnAgent(["--resume", sessionId], cwd, userText, this.modelOverride);
384
374
  this.activeProcs.add(proc);
385
375
  if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
386
376
 
@@ -1,141 +1,141 @@
1
- // =============================================================================
2
- // resource-monitor.ts — CLI 进程资源监控(CPU + 内存)
3
- // =============================================================================
4
- // 对所有 chatccc 启动的 CLI 进程持续监控 CPU 和内存占用。
5
- // 若连续 3 分钟两项指标均无变化,判定为僵死,发出 "stuck" 事件。
6
- // =============================================================================
7
-
8
- import { exec } from "node:child_process";
9
- import { EventEmitter } from "node:events";
10
-
11
- const CHECK_INTERVAL_MS = 30_000; // 30 秒检查一次
12
- const STUCK_THRESHOLD = 6; // 连续 6 次无变化 = 3 分钟
13
-
14
- interface ProcessSnapshot {
15
- cpu: number;
16
- memory: number;
17
- unchangedCount: number;
18
- }
19
-
20
- interface TrackedProcess {
21
- pid: number;
22
- sessionId: string;
23
- snapshot: ProcessSnapshot;
24
- }
25
-
26
- export const resourceMonitor = new EventEmitter();
27
-
28
- const tracked = new Map<number, TrackedProcess>();
29
-
30
- let timer: ReturnType<typeof setInterval> | null = null;
31
-
32
- function startIfNeeded(): void {
33
- if (timer) return;
34
- timer = setInterval(checkAll, CHECK_INTERVAL_MS);
35
- timer.unref?.();
36
- }
37
-
38
- function stopIfIdle(): void {
39
- if (tracked.size > 0) return;
40
- if (timer) { clearInterval(timer); timer = null; }
41
- }
42
-
43
- export function registerProcess(pid: number, sessionId: string): void {
44
- tracked.set(pid, { pid, sessionId, snapshot: { cpu: -1, memory: -1, unchangedCount: 0 } });
45
- startIfNeeded();
46
- }
47
-
48
- export function unregisterProcess(pid: number): void {
49
- tracked.delete(pid);
50
- stopIfIdle();
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // 批量查询进程指标(Windows PowerShell)
55
- // ---------------------------------------------------------------------------
56
-
57
- function execPowerShell(script: string, timeoutMs: number): Promise<string> {
58
- return new Promise((resolve, reject) => {
59
- exec(
60
- `powershell -NoProfile -Command "${script}"`,
61
- { timeout: timeoutMs, windowsHide: true },
62
- (err, stdout) => {
63
- if (err) reject(err);
64
- else resolve(stdout);
65
- },
66
- );
67
- });
68
- }
69
-
70
- async function getProcessMetrics(pids: number[]): Promise<Map<number, { cpu: number; memory: number }>> {
71
- const result = new Map<number, { cpu: number; memory: number }>();
72
- if (pids.length === 0) return result;
73
-
74
- const psScript = `Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Id)|$($_.CPU)|$($_.WorkingSet64)" }`;
75
-
76
- try {
77
- const stdout = await execPowerShell(psScript, 10_000);
78
- for (const line of stdout.trim().split(/\r?\n/)) {
79
- const trimmed = line.trim();
80
- if (!trimmed) continue;
81
- const [idStr, cpuStr, memStr] = trimmed.split("|");
82
- const id = parseInt(idStr, 10);
83
- const cpu = parseFloat(cpuStr);
84
- const memory = parseInt(memStr, 10);
85
- if (!isNaN(id) && !isNaN(cpu) && !isNaN(memory)) {
86
- result.set(id, { cpu, memory });
87
- }
88
- }
89
- } catch {
90
- // PowerShell 查询失败时跳过本轮,下次重试
91
- }
92
-
93
- return result;
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // 定时检查
98
- // ---------------------------------------------------------------------------
99
-
100
- async function checkAll(): Promise<void> {
101
- if (tracked.size === 0) return;
102
-
103
- const pids = [...tracked.keys()];
104
- const metrics = await getProcessMetrics(pids);
105
-
106
- for (const [pid, tp] of tracked) {
107
- const m = metrics.get(pid);
108
- if (!m) {
109
- // 进程已不存在,停止追踪
110
- tracked.delete(pid);
111
- continue;
112
- }
113
-
114
- const prev = tp.snapshot;
115
- const cpuChanged = m.cpu !== prev.cpu;
116
- // 内存允许 ±1% 波动,避免正常抖动触发误判
117
- const memTolerance = prev.memory > 0 ? Math.max(prev.memory * 0.01, 1024 * 1024) : 1024 * 1024;
118
- const memChanged = Math.abs(m.memory - prev.memory) > memTolerance;
119
-
120
- if (!cpuChanged && !memChanged) {
121
- tp.snapshot.unchangedCount++;
122
- if (tp.snapshot.unchangedCount >= STUCK_THRESHOLD) {
123
- const idleMinutes = Math.round(
124
- (tp.snapshot.unchangedCount * CHECK_INTERVAL_MS) / 60_000,
125
- );
126
- resourceMonitor.emit("stuck", {
127
- pid: tp.pid,
128
- sessionId: tp.sessionId,
129
- idleMinutes,
130
- });
131
- tracked.delete(pid);
132
- }
133
- } else {
134
- tp.snapshot.unchangedCount = 0;
135
- }
136
- tp.snapshot.cpu = m.cpu;
137
- tp.snapshot.memory = m.memory;
138
- }
139
-
140
- stopIfIdle();
1
+ // =============================================================================
2
+ // resource-monitor.ts — CLI 进程资源监控(CPU + 内存)
3
+ // =============================================================================
4
+ // 对所有 chatccc 启动的 CLI 进程持续监控 CPU 和内存占用。
5
+ // 若连续 3 分钟两项指标均无变化,判定为僵死,发出 "stuck" 事件。
6
+ // =============================================================================
7
+
8
+ import { exec } from "node:child_process";
9
+ import { EventEmitter } from "node:events";
10
+
11
+ const CHECK_INTERVAL_MS = 30_000; // 30 秒检查一次
12
+ const STUCK_THRESHOLD = 6; // 连续 6 次无变化 = 3 分钟
13
+
14
+ interface ProcessSnapshot {
15
+ cpu: number;
16
+ memory: number;
17
+ unchangedCount: number;
18
+ }
19
+
20
+ interface TrackedProcess {
21
+ pid: number;
22
+ sessionId: string;
23
+ snapshot: ProcessSnapshot;
24
+ }
25
+
26
+ export const resourceMonitor = new EventEmitter();
27
+
28
+ const tracked = new Map<number, TrackedProcess>();
29
+
30
+ let timer: ReturnType<typeof setInterval> | null = null;
31
+
32
+ function startIfNeeded(): void {
33
+ if (timer) return;
34
+ timer = setInterval(checkAll, CHECK_INTERVAL_MS);
35
+ timer.unref?.();
36
+ }
37
+
38
+ function stopIfIdle(): void {
39
+ if (tracked.size > 0) return;
40
+ if (timer) { clearInterval(timer); timer = null; }
41
+ }
42
+
43
+ export function registerProcess(pid: number, sessionId: string): void {
44
+ tracked.set(pid, { pid, sessionId, snapshot: { cpu: -1, memory: -1, unchangedCount: 0 } });
45
+ startIfNeeded();
46
+ }
47
+
48
+ export function unregisterProcess(pid: number): void {
49
+ tracked.delete(pid);
50
+ stopIfIdle();
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // 批量查询进程指标(Windows PowerShell)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function execPowerShell(script: string, timeoutMs: number): Promise<string> {
58
+ return new Promise((resolve, reject) => {
59
+ exec(
60
+ `powershell -NoProfile -Command "${script}"`,
61
+ { timeout: timeoutMs, windowsHide: true },
62
+ (err, stdout) => {
63
+ if (err) reject(err);
64
+ else resolve(stdout);
65
+ },
66
+ );
67
+ });
68
+ }
69
+
70
+ async function getProcessMetrics(pids: number[]): Promise<Map<number, { cpu: number; memory: number }>> {
71
+ const result = new Map<number, { cpu: number; memory: number }>();
72
+ if (pids.length === 0) return result;
73
+
74
+ const psScript = `Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Id)|$($_.CPU)|$($_.WorkingSet64)" }`;
75
+
76
+ try {
77
+ const stdout = await execPowerShell(psScript, 10_000);
78
+ for (const line of stdout.trim().split(/\r?\n/)) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed) continue;
81
+ const [idStr, cpuStr, memStr] = trimmed.split("|");
82
+ const id = parseInt(idStr, 10);
83
+ const cpu = parseFloat(cpuStr);
84
+ const memory = parseInt(memStr, 10);
85
+ if (!isNaN(id) && !isNaN(cpu) && !isNaN(memory)) {
86
+ result.set(id, { cpu, memory });
87
+ }
88
+ }
89
+ } catch {
90
+ // PowerShell 查询失败时跳过本轮,下次重试
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // 定时检查
98
+ // ---------------------------------------------------------------------------
99
+
100
+ async function checkAll(): Promise<void> {
101
+ if (tracked.size === 0) return;
102
+
103
+ const pids = [...tracked.keys()];
104
+ const metrics = await getProcessMetrics(pids);
105
+
106
+ for (const [pid, tp] of tracked) {
107
+ const m = metrics.get(pid);
108
+ if (!m) {
109
+ // 进程已不存在,停止追踪
110
+ tracked.delete(pid);
111
+ continue;
112
+ }
113
+
114
+ const prev = tp.snapshot;
115
+ const cpuChanged = m.cpu !== prev.cpu;
116
+ // 内存允许 ±1% 波动,避免正常抖动触发误判
117
+ const memTolerance = prev.memory > 0 ? Math.max(prev.memory * 0.01, 1024 * 1024) : 1024 * 1024;
118
+ const memChanged = Math.abs(m.memory - prev.memory) > memTolerance;
119
+
120
+ if (!cpuChanged && !memChanged) {
121
+ tp.snapshot.unchangedCount++;
122
+ if (tp.snapshot.unchangedCount >= STUCK_THRESHOLD) {
123
+ const idleMinutes = Math.round(
124
+ (tp.snapshot.unchangedCount * CHECK_INTERVAL_MS) / 60_000,
125
+ );
126
+ resourceMonitor.emit("stuck", {
127
+ pid: tp.pid,
128
+ sessionId: tp.sessionId,
129
+ idleMinutes,
130
+ });
131
+ tracked.delete(pid);
132
+ }
133
+ } else {
134
+ tp.snapshot.unchangedCount = 0;
135
+ }
136
+ tp.snapshot.cpu = m.cpu;
137
+ tp.snapshot.memory = m.memory;
138
+ }
139
+
140
+ stopIfIdle();
141
141
  }
package/src/cards.ts CHANGED
@@ -131,8 +131,6 @@ export function buildHelpCard(
131
131
  "发送 **/new cursor** 创建新 Cursor 会话",
132
132
  "发送 **/new codex** 创建新 Codex 会话",
133
133
  "发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
134
- "发送 **/plan <消息>** 在 Claude 会话中以只读规划模式提问",
135
- "发送 **/ask <消息>** 在 Cursor 会话中以只读问答模式提问",
136
134
  ].join("\n");
137
135
  return JSON.stringify({
138
136
  config: { wide_screen_mode: true },
package/src/config.ts CHANGED
@@ -122,24 +122,6 @@ export interface AppConfig {
122
122
  export type AgentTool = "claude" | "cursor" | "codex";
123
123
  export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
124
124
 
125
- /**
126
- * Agent 专属模式指令(内部配置,非用户可配)。
127
- * 映射每个 Agent 支持的特殊指令及其对应的 CLI 权限模式。
128
- *
129
- * - claude: /plan → --permission-mode plan(只读/规划)
130
- * - cursor: /ask → --mode ask(只读/问答)
131
- * - codex: 暂无原生只读模式,不注册任何模式指令
132
- */
133
- export const AGENT_MODE_COMMANDS: Record<AgentTool, { command: string; mode: "plan" | "ask"; description: string }[]> = {
134
- claude: [
135
- { command: "/plan", mode: "plan", description: "以只读规划模式提问(Claude 专属)" },
136
- ],
137
- cursor: [
138
- { command: "/ask", mode: "ask", description: "以只读问答模式提问(Cursor 专属)" },
139
- ],
140
- codex: [],
141
- };
142
-
143
125
  /** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
144
126
  export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): string[] {
145
127
  const seen = new Set<string>();