cc-wechat 0.2.0 → 0.3.0

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/src/patch.ts CHANGED
@@ -1,49 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * cc-wechat 补丁工具
4
- * 绕过 Claude Code 的 Channels 云控检查(tengu_harbor feature flag + accessToken auth)
5
- * 用法:node dist/patch.js npx cc-wechat patch
3
+ * cc-wechat 补丁工具(双模式)
4
+ * - 二进制模式(exe):按固定字符串搜索替换
5
+ * - AST 模式(cli.js / npm 安装):acorn 解析 JS AST,按语义特征定位
6
6
  */
7
7
 
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { execSync } from 'node:child_process';
12
+ import { createRequire } from 'node:module';
12
13
 
13
- // 补丁定义:[特征字符串, 替换字符串, 说明]
14
- const PATCHES: Array<[string, string, string]> = [
15
- // 1. Channels feature flag 云控
16
- [
17
- 'function PaH(){return lA("tengu_harbor",!1)}',
18
- 'function PaH(){return !0 }',
19
- 'Channels feature flag (tengu_harbor)',
20
- ],
21
- // 2. S1_ gate auth 检查
22
- [
23
- 'if(!yf()?.accessToken)',
24
- 'if( false )',
25
- 'Channel gate accessToken check',
26
- ],
27
- // 3. UI 层 noAuth 检查
28
- [
29
- 'noAuth:!yf()?.accessToken',
30
- 'noAuth: false ',
31
- 'UI noAuth display check',
32
- ],
14
+ // ─── 二进制模式补丁定义 ──────────────────────────────────
15
+ const BINARY_PATCHES: Array<[string, string, string]> = [
16
+ ['function PaH(){return lA("tengu_harbor",!1)}', 'function PaH(){return !0 }', 'Channels feature flag (tengu_harbor)'],
17
+ ['if(!yf()?.accessToken)', 'if( false )', 'Channel gate auth check'],
18
+ ['noAuth:!yf()?.accessToken', 'noAuth: false ', 'UI noAuth display check'],
33
19
  ];
34
20
 
35
- /** 查找 claude 可执行文件路径 */
21
+ // ─── 查找 claude ──────────────────────────────────────────
36
22
  function findClaudeExe(): string | null {
37
23
  const home = homedir();
38
-
39
- // 1. which/where(最可靠)
40
24
  try {
41
25
  const cmd = process.platform === 'win32' ? 'where claude 2>nul' : 'which claude 2>/dev/null';
42
26
  const p = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0].trim();
43
27
  if (p && fs.existsSync(p)) return p;
44
28
  } catch { /* ignore */ }
45
29
 
46
- // 2. 常见安装路径
47
30
  const candidates = [
48
31
  path.join(home, '.local', 'bin', 'claude.exe'),
49
32
  path.join(home, '.local', 'bin', 'claude'),
@@ -56,16 +39,10 @@ function findClaudeExe(): string | null {
56
39
  path.join(home, 'AppData', 'Local', 'Programs', 'claude-code', 'claude.exe'),
57
40
  path.join(home, 'AppData', 'Local', 'claude-code', 'claude.exe'),
58
41
  path.join(home, 'AppData', 'Local', 'AnthropicClaude', 'claude.exe'),
59
- '/usr/local/bin/claude',
60
- '/opt/homebrew/bin/claude',
61
- '/usr/bin/claude',
62
- '/snap/bin/claude',
42
+ '/usr/local/bin/claude', '/opt/homebrew/bin/claude', '/usr/bin/claude', '/snap/bin/claude',
63
43
  ];
64
- for (const p of candidates) {
65
- if (fs.existsSync(p)) return p;
66
- }
44
+ for (const p of candidates) { if (fs.existsSync(p)) return p; }
67
45
 
68
- // 3. npm global prefix 动态查找
69
46
  try {
70
47
  const prefix = execSync('npm config get prefix', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
71
48
  if (prefix) {
@@ -75,7 +52,6 @@ function findClaudeExe(): string | null {
75
52
  }
76
53
  } catch { /* ignore */ }
77
54
 
78
- // 4. PATH 逐目录搜索(兜底)
79
55
  const pathDirs = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':');
80
56
  const exeNames = process.platform === 'win32' ? ['claude.exe', 'claude.cmd'] : ['claude'];
81
57
  for (const dir of pathDirs) {
@@ -84,184 +60,290 @@ function findClaudeExe(): string | null {
84
60
  if (fs.existsSync(p)) return p;
85
61
  }
86
62
  }
87
-
88
63
  return null;
89
64
  }
90
65
 
91
- /** wrapper/cmd 解析到真正包含代码的文件(exe cli.js) */
66
+ // ─── 解析到真正的可 patch 文件 ────────────────────────────
92
67
  function resolvePatchTarget(claudePath: string): string {
93
68
  const ext = path.extname(claudePath).toLowerCase();
94
69
  const dir = path.dirname(claudePath);
95
-
96
- // exe 或大文件(>1MB)→ 直接 patch
97
70
  if (ext === '.exe') return claudePath;
98
71
  const stat = fs.statSync(claudePath);
99
72
  if (stat.size > 1_000_000) return claudePath;
100
73
 
101
- // .cmd / 小文件 → npm wrapper,查找 cli.js
102
74
  const cliJs = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
103
75
  if (fs.existsSync(cliJs)) return cliJs;
104
76
 
105
- // 读 .cmd 内容提取路径
106
77
  if (ext === '.cmd') {
107
78
  try {
108
79
  const content = fs.readFileSync(claudePath, 'utf-8');
109
- const match = content.match(/node_modules[\\/]@anthropic-ai[\\/]claude-code[\\/]cli\.js/);
110
- if (match) {
80
+ if (content.match(/node_modules[\\/]@anthropic-ai[\\/]claude-code[\\/]cli\.js/)) {
111
81
  const resolved = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
112
82
  if (fs.existsSync(resolved)) return resolved;
113
83
  }
114
84
  } catch { /* ignore */ }
115
85
  }
116
86
 
117
- // symlink → resolve
118
87
  try {
119
88
  const realPath = fs.realpathSync(claudePath);
120
89
  if (realPath !== claudePath) return resolvePatchTarget(realPath);
121
90
  } catch { /* ignore */ }
122
-
123
91
  return claudePath;
124
92
  }
125
93
 
126
- function patch(): void {
127
- console.log('\n cc-wechat patch Claude Code Channels 补丁\n');
94
+ // ─── 判断是否二进制文件 ──────────────────────────────────
95
+ function isBinaryFile(filePath: string): boolean {
96
+ const ext = path.extname(filePath).toLowerCase();
97
+ if (ext === '.exe') return true;
98
+ const header = Buffer.alloc(4);
99
+ const fd = fs.openSync(filePath, 'r');
100
+ fs.readSync(fd, header, 0, 4, 0);
101
+ fs.closeSync(fd);
102
+ if (header[0] === 0x4D && header[1] === 0x5A) return true;
103
+ if (header[0] === 0x7F && header[1] === 0x45) return true;
104
+ if (header[0] === 0xFE && header[1] === 0xED) return true;
105
+ if (header[0] === 0xCF && header[1] === 0xFA) return true;
106
+ return false;
107
+ }
128
108
 
129
- const claudePath = findClaudeExe();
130
- if (!claudePath) {
131
- console.error(' 找不到 Claude Code。请确认已安装。');
132
- process.exit(1);
109
+ // ─── 写入结果 ──────────────────────────────────────────────
110
+ function writeResult(exePath: string, buf: Buffer): void {
111
+ const backupPath = exePath + '.bak';
112
+ if (!fs.existsSync(backupPath)) {
113
+ fs.copyFileSync(exePath, backupPath);
114
+ console.log(`\n 📦 已备份: ${backupPath}`);
133
115
  }
134
-
135
- const exePath = resolvePatchTarget(claudePath);
136
- if (exePath !== claudePath) {
137
- console.log(` 查找: ${claudePath}`);
138
- console.log(` 解析: ${exePath}`);
116
+ try {
117
+ fs.writeFileSync(exePath, buf);
118
+ console.log('\n 补丁已直接写入,立即生效!\n');
119
+ } catch {
120
+ const tmpPath = exePath + '.patched';
121
+ fs.writeFileSync(tmpPath, buf);
122
+ console.log(`\n ⚠️ Claude Code 正在运行,无法直接写入。`);
123
+ console.log(` 补丁已保存到: ${tmpPath}\n`);
124
+ console.log(' 请退出所有 Claude Code 后执行:\n');
125
+ if (process.platform === 'win32') {
126
+ const dir = path.dirname(exePath);
127
+ const name = path.basename(exePath);
128
+ console.log(` cd "${dir}"`);
129
+ console.log(` Move-Item ${name} ${name}.old -Force`);
130
+ console.log(` Move-Item ${name}.patched ${name} -Force`);
131
+ } else {
132
+ console.log(` mv "${exePath}" "${exePath}.old"`);
133
+ console.log(` mv "${tmpPath}" "${exePath}"`);
134
+ }
135
+ console.log();
139
136
  }
140
- console.log(` 目标: ${exePath}`);
137
+ console.log(' 恢复方法: npx cc-wechat unpatch\n');
138
+ }
141
139
 
142
- // 读取二进制
140
+ // ─── 二进制模式 patch ──────────────────────────────────────
141
+ function patchBinary(exePath: string): void {
142
+ console.log(' 模式: 二进制字符串搜索\n');
143
143
  const buf = fs.readFileSync(exePath);
144
- let totalPatched = 0;
145
- let alreadyPatched = 0;
146
-
147
- for (const [original, replacement, desc] of PATCHES) {
148
- if (original.length !== replacement.length) {
149
- console.error(` 错误: "${desc}" 长度不匹配 (${original.length} vs ${replacement.length})`);
150
- process.exit(1);
151
- }
144
+ let patched = 0, skipped = 0;
152
145
 
146
+ for (const [original, replacement, desc] of BINARY_PATCHES) {
153
147
  const origBuf = Buffer.from(original);
154
148
  const patchBuf = Buffer.from(replacement);
149
+ let pos = 0, count = 0;
150
+ while (true) { const idx = buf.indexOf(origBuf, pos); if (idx === -1) break; patchBuf.copy(buf, idx); count++; pos = idx + 1; }
151
+ let alreadyCount = 0; pos = 0;
152
+ while (true) { const idx = buf.indexOf(patchBuf, pos); if (idx === -1) break; alreadyCount++; pos = idx + 1; }
153
+
154
+ if (count > 0) { console.log(` ✅ ${desc} — ${count} 处已修补`); patched += count; }
155
+ else if (alreadyCount > 0) { console.log(` ⏭️ ${desc} — 已修补`); skipped += alreadyCount; }
156
+ else { console.log(` ⚠️ ${desc} — 未找到`); }
157
+ }
155
158
 
156
- // 搜索所有出现位置
157
- let pos = 0;
158
- let count = 0;
159
- let alreadyCount = 0;
160
-
161
- while (true) {
162
- const idx = buf.indexOf(origBuf, pos);
163
- if (idx === -1) break;
164
- patchBuf.copy(buf, idx);
165
- count++;
166
- pos = idx + 1;
167
- }
159
+ if (patched === 0 && skipped > 0) { console.log('\n 所有补丁已生效。\n'); return; }
160
+ if (patched === 0) { console.error('\n 未找到可修补位置。\n'); process.exit(1); }
161
+ writeResult(exePath, buf);
162
+ }
168
163
 
169
- // 检查是否已经 patch
170
- pos = 0;
171
- while (true) {
172
- const idx = buf.indexOf(patchBuf, pos);
173
- if (idx === -1) break;
174
- alreadyCount++;
175
- pos = idx + 1;
176
- }
164
+ // ─── AST 模式 patch ──────────────────────────────────────
165
+ async function patchAst(exePath: string): Promise<void> {
166
+ console.log(' 模式: AST 语义分析\n');
167
+
168
+ // 动态下载 acorn
169
+ const acornPath = path.join(homedir(), '.cache', 'cc-channel-patch', 'acorn.js');
170
+ if (!fs.existsSync(acornPath)) {
171
+ console.log(' 下载 acorn 解析器...');
172
+ fs.mkdirSync(path.dirname(acornPath), { recursive: true });
173
+ const resp = await fetch('https://unpkg.com/acorn@8.14.0/dist/acorn.js');
174
+ if (!resp.ok) throw new Error(`下载 acorn 失败: HTTP ${resp.status}`);
175
+ fs.writeFileSync(acornPath, await resp.text());
176
+ }
177
177
 
178
- if (count > 0) {
179
- console.log(` [PATCH] ${desc}: ${count} 处已修补`);
180
- totalPatched += count;
181
- } else if (alreadyCount > 0) {
182
- console.log(` [SKIP] ${desc}: 已修补过 (${alreadyCount} )`);
183
- alreadyPatched += alreadyCount;
184
- } else {
185
- console.log(` [WARN] ${desc}: 未找到特征字符串(CC 版本可能已更新)`);
178
+ const require = createRequire(import.meta.url);
179
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
180
+ const acorn = require(acornPath) as { parse: (code: string, opts: Record<string, unknown>) => ASTNode };
181
+
182
+ let code = fs.readFileSync(exePath, 'utf-8');
183
+ let shebang = '';
184
+ if (code.startsWith('#!')) { const idx = code.indexOf('\n'); shebang = code.slice(0, idx + 1); code = code.slice(idx + 1); }
185
+
186
+ let ast: ASTNode;
187
+ try { ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' }); }
188
+ catch (e) { console.error(` AST 解析失败: ${(e as Error).message}`); process.exit(1); }
189
+
190
+ const src = (node: ASTNode) => code.slice(node.start, node.end);
191
+
192
+ function findNodes(node: unknown, predicate: (n: ASTNode) => boolean, results: ASTNode[] = []): ASTNode[] {
193
+ if (!node || typeof node !== 'object') return results;
194
+ const n = node as ASTNode;
195
+ if (predicate(n)) results.push(n);
196
+ for (const key in n) {
197
+ const val = (n as Record<string, unknown>)[key];
198
+ if (val && typeof val === 'object') {
199
+ if (Array.isArray(val)) val.forEach(child => findNodes(child, predicate, results));
200
+ else findNodes(val, predicate, results);
201
+ }
186
202
  }
203
+ return results;
187
204
  }
188
205
 
189
- if (totalPatched === 0 && alreadyPatched > 0) {
190
- console.log('\n 所有补丁已生效,无需操作。\n');
191
- return;
206
+ const replacements: Array<{ start: number; end: number; replacement: string }> = [];
207
+ let patchCount = 0;
208
+
209
+ // Patch 1: tengu_harbor
210
+ const harborCalls = findNodes(ast, n =>
211
+ n.type === 'CallExpression' && n.arguments?.length === 2 &&
212
+ n.arguments[0]?.type === 'Literal' && n.arguments[0]?.value === 'tengu_harbor'
213
+ );
214
+ let harborPatched = false;
215
+ for (const call of harborCalls) {
216
+ const arg = call.arguments![1]!;
217
+ if (arg.type === 'UnaryExpression' && arg.operator === '!' && arg.argument?.type === 'Literal' && arg.argument?.value === 1) {
218
+ replacements.push({ start: arg.start, end: arg.end, replacement: '!0' });
219
+ patchCount++;
220
+ console.log(` ✅ tengu_harbor flag → !0`);
221
+ break;
222
+ }
223
+ if (arg.type === 'UnaryExpression' && arg.operator === '!' && arg.argument?.type === 'Literal' && arg.argument?.value === 0) {
224
+ harborPatched = true;
225
+ console.log(' ⏭️ tengu_harbor flag — 已修补');
226
+ break;
227
+ }
192
228
  }
193
229
 
194
- if (totalPatched === 0) {
195
- console.error('\n 未找到任何可修补的位置。Claude Code 版本可能不兼容。\n');
196
- process.exit(1);
230
+ // Patch 2: channel decision function
231
+ const markerLiterals = findNodes(ast, n => n.type === 'Literal' && n.value === 'channels feature is not currently available');
232
+ let qMqPatched = false;
233
+ if (markerLiterals.length > 0) {
234
+ const markerPos = markerLiterals[0].start;
235
+ const enclosing = findNodes(ast, n =>
236
+ (n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression') && n.start < markerPos && n.end > markerPos
237
+ ).sort((a, b) => (a.end - a.start) - (b.end - b.start));
238
+
239
+ if (enclosing.length > 0) {
240
+ const fn = enclosing[0];
241
+ const body = fn.body?.body;
242
+ if (body?.length && body[0].type === 'IfStatement' && src(body[0]).includes('claude/channel')) {
243
+ const newBody = '{' + src(body[0]) + 'return{action:"register"}}';
244
+ replacements.push({ start: fn.body!.start, end: fn.body!.end, replacement: newBody });
245
+ patchCount++;
246
+ console.log(` ✅ Channel decision — 绕过 check 2-7`);
247
+ }
248
+ }
249
+ } else if (code.includes('claude/channel capability') && !code.includes('channels feature is not currently available')) {
250
+ qMqPatched = true;
251
+ console.log(' ⏭️ Channel decision — 已修补');
197
252
  }
198
253
 
199
- // 备份
200
- const backupPath = exePath + '.bak';
201
- if (!fs.existsSync(backupPath)) {
202
- fs.copyFileSync(exePath, backupPath);
203
- console.log(` [BACKUP] 已备份到 ${backupPath}`);
254
+ // Patch 3: UI notice
255
+ const policyProps = findNodes(ast, n =>
256
+ n.type === 'Property' && n.key?.type === 'Identifier' && n.key?.name === 'policyBlocked' &&
257
+ n.value != null && src(n.value as ASTNode).includes('channelsEnabled')
258
+ );
259
+ let noticePatched = false;
260
+ if (policyProps.length > 0) {
261
+ const propPos = policyProps[0].start;
262
+ const noticeFuncs = findNodes(ast, n =>
263
+ (n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression') && n.start < propPos && n.end > propPos
264
+ ).sort((a, b) => (a.end - a.start) - (b.end - b.start));
265
+
266
+ if (noticeFuncs.length > 0) {
267
+ const nf = noticeFuncs[0];
268
+ const firstCalls = findNodes(nf.body!.body![0], n => n.type === 'CallExpression' && n.callee?.type === 'Identifier');
269
+ const getAllowed = firstCalls.length > 0 ? src(firstCalls[0]) : '$N()';
270
+ const mapCalls = findNodes(nf, n => n.type === 'CallExpression' && n.callee?.type === 'MemberExpression' && n.callee?.property?.name === 'map');
271
+ const formatter = mapCalls.length > 0 && mapCalls[0].arguments?.[0] ? src(mapCalls[0].arguments[0]) : 'naH';
272
+
273
+ const newBody = '{let A=' + getAllowed + ';let q=A.length>0?A.map(' + formatter + ').join(", "):"";return{channels:A,disabled:!1,noAuth:!1,policyBlocked:!1,list:q}}';
274
+ replacements.push({ start: nf.body!.start, end: nf.body!.end, replacement: newBody });
275
+ patchCount++;
276
+ console.log(` ✅ UI notice — disabled/noAuth/policyBlocked 全部置 false`);
277
+ }
278
+ } else if (code.includes('policyBlocked') && !code.includes('channelsEnabled!==!0')) {
279
+ noticePatched = true;
280
+ console.log(' ⏭️ UI notice — 已修补');
204
281
  }
205
282
 
206
- // 写入 先写临时文件再替换
207
- const tmpPath = exePath + '.patched';
208
- fs.writeFileSync(tmpPath, buf);
209
-
210
- console.log(`\n 补丁已写入 ${tmpPath}`);
211
- console.log(' 请关闭所有 Claude Code 进程后手动替换:\n');
212
-
213
- if (process.platform === 'win32') {
214
- const dir = path.dirname(exePath);
215
- const name = path.basename(exePath);
216
- console.log(` cd ${dir}`);
217
- console.log(` Move-Item ${name} ${name}.old -Force`);
218
- console.log(` Move-Item ${name}.patched ${name} -Force\n`);
219
- } else {
220
- console.log(` mv "${exePath}" "${exePath}.old"`);
221
- console.log(` mv "${tmpPath}" "${exePath}"\n`);
283
+ if (patchCount === 0) {
284
+ if (harborPatched || qMqPatched || noticePatched) { console.log('\n 所有补丁已生效。\n'); return; }
285
+ console.error('\n 未找到可修补位置。CC 版本可能不兼容。\n'); process.exit(1);
222
286
  }
223
287
 
224
- console.log(` 恢复方法: ${backupPath} 替换即可\n`);
288
+ replacements.sort((a, b) => b.start - a.start);
289
+ let newCode = code;
290
+ for (const r of replacements) { newCode = newCode.slice(0, r.start) + r.replacement + newCode.slice(r.end); }
291
+
292
+ writeResult(exePath, Buffer.from(shebang + newCode, 'utf-8'));
225
293
  }
226
294
 
227
- function unpatch(): void {
228
- console.log('\n cc-wechat unpatch — 恢复原始 Claude Code\n');
295
+ // ─── AST 节点类型(简化) ─────────────────────────────────
296
+ interface ASTNode {
297
+ type: string;
298
+ start: number;
299
+ end: number;
300
+ value?: unknown;
301
+ name?: string;
302
+ operator?: string;
303
+ arguments?: ASTNode[];
304
+ argument?: ASTNode;
305
+ callee?: ASTNode;
306
+ property?: ASTNode;
307
+ key?: ASTNode;
308
+ body?: ASTNode & { body?: ASTNode[] };
309
+ id?: ASTNode;
310
+ [key: string]: unknown;
311
+ }
312
+
313
+ // ─── patch 入口 ────────────────────────────────────────────
314
+ async function patch(): Promise<void> {
315
+ console.log('\n cc-wechat patch — Claude Code Channels 补丁\n');
229
316
 
230
317
  const claudePath = findClaudeExe();
231
- if (!claudePath) {
232
- console.error(' 找不到 Claude Code。');
233
- process.exit(1);
234
- }
318
+ if (!claudePath) { console.error(' 找不到 Claude Code。\n'); process.exit(1); }
235
319
 
236
320
  const exePath = resolvePatchTarget(claudePath);
321
+ console.log(` 查找: ${claudePath}`);
322
+ if (exePath !== claudePath) console.log(` 解析: ${exePath}`);
323
+ console.log(` 目标: ${exePath}\n`);
237
324
 
238
- const backupPath = exePath + '.bak';
239
- if (!fs.existsSync(backupPath)) {
240
- console.error(` 未找到备份文件 ${backupPath}`);
241
- process.exit(1);
242
- }
325
+ if (isBinaryFile(exePath)) { patchBinary(exePath); }
326
+ else { await patchAst(exePath); }
327
+ }
243
328
 
244
- const tmpPath = exePath + '.restore';
245
- fs.copyFileSync(backupPath, tmpPath);
246
- console.log(` 已准备恢复文件 ${tmpPath}`);
247
- console.log(' 请关闭所有 Claude Code 进程后手动替换:\n');
248
-
249
- if (process.platform === 'win32') {
250
- const dir = path.dirname(exePath);
251
- const name = path.basename(exePath);
252
- console.log(` cd ${dir}`);
253
- console.log(` Move-Item ${name} ${name}.patched -Force`);
254
- console.log(` Move-Item ${name}.restore ${name} -Force\n`);
255
- } else {
256
- console.log(` mv "${exePath}" "${exePath}.patched"`);
257
- console.log(` mv "${tmpPath}" "${exePath}"\n`);
329
+ function unpatch(): void {
330
+ console.log('\n cc-wechat unpatch — 恢复原始 Claude Code\n');
331
+ const claudePath = findClaudeExe();
332
+ if (!claudePath) { console.error(' 找不到 Claude Code。\n'); process.exit(1); }
333
+ const exePath = resolvePatchTarget(claudePath);
334
+ const backupPath = exePath + '.bak';
335
+ if (!fs.existsSync(backupPath)) { console.error(` 未找到备份 ${backupPath}\n`); process.exit(1); }
336
+ try {
337
+ fs.copyFileSync(backupPath, exePath);
338
+ console.log(' 已恢复。\n');
339
+ } catch {
340
+ const tmpPath = exePath + '.restore';
341
+ fs.copyFileSync(backupPath, tmpPath);
342
+ console.log(` ⚠️ CC 正在运行,恢复文件: ${tmpPath}\n`);
258
343
  }
259
344
  }
260
345
 
261
346
  // 入口
262
347
  const cmd = process.argv[2];
263
- if (cmd === 'unpatch' || cmd === 'restore') {
264
- unpatch();
265
- } else {
266
- patch();
267
- }
348
+ if (cmd === 'unpatch' || cmd === 'restore') { unpatch(); }
349
+ else { patch().catch(e => { console.error(` 错误: ${(e as Error).message}\n`); process.exit(1); }); }