chatccc 0.2.160 → 0.2.162

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
@@ -313,6 +313,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
313
313
  | `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
314
314
  | `/ask <内容>` | 只读问答模式:与 /plan 相同,仅允许读文件和 stop-stuck-loop 请求 |
315
315
  | `/restart` | 重启机器人进程 |
316
+ | `/updateg` | 更新 npm 全局包并重启(仅限 `npm install -g chatccc` 安装的全局进程) |
316
317
 
317
318
  > **模型切换**:`/model` 查看可选模型清单,`/model <名称>` 模糊匹配切换,`/model clear` 恢复默认。当前仅 Claude Code 支持模型切换(需同时填写 `claude.model` 和 `claude.subagentModel`),Cursor / Codex 切换正在开发中。
318
319
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.160",
3
+ "version": "0.2.162",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,6 +17,7 @@ describe("cardJsonToPlainText", () => {
17
17
  expect(text).toContain("/new cursor");
18
18
  expect(text).toContain("/new codex");
19
19
  expect(text).toContain("/restart");
20
+ expect(text).toContain("/updateg");
20
21
  expect(text).toContain("/cd");
21
22
  });
22
23
 
@@ -46,6 +46,16 @@ describe("truncateContent", () => {
46
46
  expect(resultLines[resultLines.length - 1]).toBe("line 10");
47
47
  expect(resultLines.length).toBe(6); // 1 first + "..." + 4 last
48
48
  });
49
+
50
+ it("skips leading empty lines, preserves first non-empty line", () => {
51
+ const lines = Array.from({ length: 25 }, (_, i) => `line ${i + 1}`);
52
+ const text = "\n\n\n" + lines.join("\n");
53
+ const result = truncateContent(text);
54
+ const resultLines = result.split("\n");
55
+ expect(resultLines[0]).toBe("line 1");
56
+ expect(resultLines[1]).toBe("...");
57
+ expect(resultLines.length).toBe(21); // 1 first + "..." + 19 last
58
+ });
49
59
  });
50
60
 
51
61
  // ---------------------------------------------------------------------------
@@ -146,7 +156,7 @@ describe("buildHelpCard", () => {
146
156
  const parsed = JSON.parse(card);
147
157
  const action = parsed.elements[2];
148
158
  expect(action.tag).toBe("action");
149
- expect(action.actions).toHaveLength(6);
159
+ expect(action.actions).toHaveLength(7);
150
160
  });
151
161
  });
152
162
 
package/src/cards.ts CHANGED
@@ -32,10 +32,16 @@ export function isCodeBlockOpen(text: string): boolean {
32
32
 
33
33
  export function truncateContent(text: string, maxLines = 20, maxChars = 8000): string {
34
34
  const lines = text.split("\n");
35
+ // 跳过开头空行
36
+ let startIdx = 0;
37
+ while (startIdx < lines.length && lines[startIdx].trim() === "") {
38
+ startIdx++;
39
+ }
40
+ const effectiveLines = lines.slice(startIdx);
35
41
  let displayText: string;
36
- if (lines.length > maxLines) {
37
- const firstLine = lines[0];
38
- const lastLines = lines.slice(-(maxLines - 1)).join("\n");
42
+ if (effectiveLines.length > maxLines) {
43
+ const firstLine = effectiveLines[0];
44
+ const lastLines = effectiveLines.slice(-(maxLines - 1)).join("\n");
39
45
  displayText = firstLine + "\n...\n" + lastLines;
40
46
  } else {
41
47
  displayText = text;
@@ -129,6 +135,8 @@ export function buildHelpCard(
129
135
  "发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
130
136
  "发送 **/plan** 以规划模式提问(只读,不执行写操作)",
131
137
  "发送 **/ask** 以问答模式提问(只读,不执行写操作)",
138
+ "发送 **/restart** 重启 ChatCCC 进程",
139
+ "发送 **/updateg** 更新并重启(仅 npm 全局安装可用)",
132
140
  ].join("\n");
133
141
  return JSON.stringify({
134
142
  config: { wide_screen_mode: true },
@@ -142,6 +150,7 @@ export function buildHelpCard(
142
150
  { text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
143
151
  { text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
144
152
  { text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
153
+ { text: "更新并重启(/updateg)", value: JSON.stringify({ cmd: "updateg" }), type: "danger" },
145
154
  { text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
146
155
  ]),
147
156
  ],
package/src/index.ts CHANGED
@@ -213,7 +213,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
213
213
  }
214
214
  if (!cmd) return null;
215
215
 
216
- const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
216
+ const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", updateg: "/updateg", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
217
217
  let text = CMD_MAP[cmd] ?? "";
218
218
  if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
219
219
  const path = (action.value as Record<string, string>).path;
@@ -5,9 +5,9 @@
5
5
  * 所有 IM 平台操作通过 PlatformAdapter 接口注入,不直接依赖 feishu-platform.ts。
6
6
  */
7
7
 
8
- import { spawn } from "node:child_process";
8
+ import { execSync, spawn } from "node:child_process";
9
9
  import { readdir, stat } from "node:fs/promises";
10
- import { readFileSync, writeFileSync } from "node:fs";
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { resolve, dirname } from "node:path";
12
12
 
13
13
  import { makeTraceId, logTrace } from "./trace.ts";
@@ -159,6 +159,45 @@ async function cleanupNonWechatP2pBinding(
159
159
  }
160
160
  }
161
161
 
162
+ /** 检测当前进程是否从 npm 全局安装启动 */
163
+ function isRunningFromGlobalNpm(): boolean {
164
+ try {
165
+ const globalRoot = execSync("npm root -g", { encoding: "utf8", timeout: 5000, windowsHide: true }).trim();
166
+ return resolve(PROJECT_ROOT).startsWith(resolve(globalRoot));
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ /** 更新并重启:spawn detached Node.js 中间脚本,轮询父进程退出后执行 npm update + chatccc */
173
+ function scheduleUpdateRestart(): void {
174
+ const parentPid = process.pid;
175
+ const watcherScript = `
176
+ var spawn = require('child_process').spawn;
177
+ var execSync = require('child_process').execSync;
178
+ var parentPid = ${parentPid};
179
+ (function wait() {
180
+ try { process.kill(parentPid, 0); setTimeout(wait, 300); } catch (_) {
181
+ try {
182
+ var npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
183
+ execSync(npmCmd + ' update -g chatccc', { stdio: 'inherit' });
184
+ } catch (e) {
185
+ console.error('npm update failed:', e.message);
186
+ }
187
+ var c = spawn('chatccc', [], { detached: true, stdio: 'ignore', shell: true });
188
+ c.unref();
189
+ process.exit(0);
190
+ }
191
+ })();
192
+ `;
193
+ const child = spawn("node", ["-e", watcherScript], {
194
+ detached: true,
195
+ stdio: "ignore",
196
+ shell: true,
197
+ });
198
+ child.unref();
199
+ }
200
+
162
201
  // ---------------------------------------------------------------------------
163
202
  // handleCommand — 平台无关的命令分发
164
203
  // ---------------------------------------------------------------------------
@@ -212,6 +251,22 @@ export async function handleCommand(
212
251
  return;
213
252
  }
214
253
 
254
+ if (textLower === "/updateg") {
255
+ logTrace(tid, "BRANCH", { cmd: "/updateg" });
256
+ if (!isRunningFromGlobalNpm()) {
257
+ await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /updateg 更新。请通过 npm install -g chatccc 安装后使用。").catch(() => {});
258
+ logTrace(tid, "DONE", { outcome: "updateg_not_global" });
259
+ return;
260
+ }
261
+ await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => {});
262
+ logTrace(tid, "DONE", { outcome: "updateg" });
263
+ appendStartupTrace("updateg: spawn watcher begin", { fromPid: process.pid });
264
+ scheduleUpdateRestart();
265
+ // 短暂延迟确保消息发送后再退出
266
+ setTimeout(() => process.exit(0), 500);
267
+ return;
268
+ }
269
+
215
270
  if (textLower === "/cd" || textLower.startsWith("/cd ")) {
216
271
  logTrace(tid, "BRANCH", {
217
272
  cmd: "/cd",