chatccc 0.2.161 → 0.2.163

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.161",
3
+ "version": "0.2.163",
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
 
@@ -156,7 +156,7 @@ describe("buildHelpCard", () => {
156
156
  const parsed = JSON.parse(card);
157
157
  const action = parsed.elements[2];
158
158
  expect(action.tag).toBe("action");
159
- expect(action.actions).toHaveLength(6);
159
+ expect(action.actions).toHaveLength(7);
160
160
  });
161
161
  });
162
162
 
package/src/cards.ts CHANGED
@@ -135,6 +135,8 @@ export function buildHelpCard(
135
135
  "发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
136
136
  "发送 **/plan** 以规划模式提问(只读,不执行写操作)",
137
137
  "发送 **/ask** 以问答模式提问(只读,不执行写操作)",
138
+ "发送 **/restart** 重启 ChatCCC 进程",
139
+ "发送 **/updateg** 更新并重启(仅 npm 全局安装可用)",
138
140
  ].join("\n");
139
141
  return JSON.stringify({
140
142
  config: { wide_screen_mode: true },
@@ -148,6 +150,7 @@ export function buildHelpCard(
148
150
  { text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
149
151
  { text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
150
152
  { text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
153
+ { text: "更新并重启(/updateg)", value: JSON.stringify({ cmd: "updateg" }), type: "danger" },
151
154
  { text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
152
155
  ]),
153
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,10 +5,11 @@
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";
11
- import { resolve, dirname } from "node:path";
10
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { join, resolve, dirname } from "node:path";
12
+ import { homedir } from "node:os";
12
13
 
13
14
  import { makeTraceId, logTrace } from "./trace.ts";
14
15
  import { appendStartupTrace } from "./shared.ts";
@@ -159,6 +160,58 @@ async function cleanupNonWechatP2pBinding(
159
160
  }
160
161
  }
161
162
 
163
+ /** 检测当前进程是否从 npm 全局安装启动 */
164
+ function isRunningFromGlobalNpm(): boolean {
165
+ try {
166
+ const globalRoot = execSync("npm root -g", { encoding: "utf8", timeout: 5000, windowsHide: true }).trim();
167
+ return resolve(PROJECT_ROOT).startsWith(resolve(globalRoot));
168
+ } catch {
169
+ return false;
170
+ }
171
+ }
172
+
173
+ const UPDATEG_LOG = join(homedir(), ".chatccc", "logs", "updateg-watcher.log");
174
+
175
+ function updLog(msg: string): void {
176
+ const ts = new Date().toISOString();
177
+ try { appendFileSync(UPDATEG_LOG, `${ts} [UPG-SYNC] ${msg}\n`, "utf-8"); } catch {}
178
+ }
179
+
180
+ /** 同步更新 npm 全局包并重启(父进程存活等待,避免 systemd KillMode=control-group 杀 watcher) */
181
+ function syncUpdateAndRestart(): void {
182
+ updLog(`sync update start, pid=${process.pid}`);
183
+
184
+ // 1. npm update
185
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
186
+ try {
187
+ const out = execSync(`${npmCmd} update -g chatccc`, { encoding: "utf8", timeout: 120000, windowsHide: true });
188
+ updLog(`npm update OK: ${out.slice(0, 200)}`);
189
+ } catch (e) {
190
+ const errMsg = (e as Error).message;
191
+ updLog(`npm update failed: ${errMsg}, falling back to npm install`);
192
+ try {
193
+ const out2 = execSync(`${npmCmd} install -g chatccc@latest`, { encoding: "utf8", timeout: 120000, windowsHide: true });
194
+ updLog(`npm install fallback OK: ${out2.slice(0, 200)}`);
195
+ } catch (e2) {
196
+ updLog(`npm install fallback also failed: ${(e2 as Error).message}`);
197
+ }
198
+ }
199
+
200
+ // 2. spawn new chatccc
201
+ const npmPrefix = process.env.NPM_PREFIX || "";
202
+ const binName = process.platform === "win32" ? "chatccc.cmd" : "chatccc";
203
+ const binPath = npmPrefix ? join(npmPrefix, binName) : "chatccc";
204
+ try {
205
+ const child = spawn(binPath, [], { detached: true, stdio: "ignore", shell: true });
206
+ child.unref();
207
+ updLog(`spawn new chatccc OK, childPid=${child.pid}, bin=${binPath}`);
208
+ } catch (e) {
209
+ updLog(`spawn new chatccc failed: ${(e as Error).message}`);
210
+ }
211
+
212
+ updLog("sync update done, parent exiting in 2s");
213
+ }
214
+
162
215
  // ---------------------------------------------------------------------------
163
216
  // handleCommand — 平台无关的命令分发
164
217
  // ---------------------------------------------------------------------------
@@ -212,6 +265,21 @@ export async function handleCommand(
212
265
  return;
213
266
  }
214
267
 
268
+ if (textLower === "/updateg") {
269
+ logTrace(tid, "BRANCH", { cmd: "/updateg" });
270
+ if (!isRunningFromGlobalNpm()) {
271
+ await platform.sendText(chatId, "当前进程非 npm 全局安装,无法使用 /updateg 更新。请通过 npm install -g chatccc 安装后使用。").catch(() => {});
272
+ logTrace(tid, "DONE", { outcome: "updateg_not_global" });
273
+ return;
274
+ }
275
+ await platform.sendText(chatId, "正在更新并重启,请稍候...").catch(() => {});
276
+ logTrace(tid, "DONE", { outcome: "updateg" });
277
+ appendStartupTrace("updateg: sync update begin", { fromPid: process.pid });
278
+ syncUpdateAndRestart();
279
+ setTimeout(() => process.exit(0), 2000);
280
+ return;
281
+ }
282
+
215
283
  if (textLower === "/cd" || textLower.startsWith("/cd ")) {
216
284
  logTrace(tid, "BRANCH", {
217
285
  cmd: "/cd",