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.
@@ -2,8 +2,9 @@
2
2
  /**
3
3
  * cc-channel-patch — 一键启用 Claude Code Channels
4
4
  *
5
- * 绕过 Anthropic 的 tengu_harbor 云控 + accessToken 认证检查,
6
- * 使代理认证模式下也能使用 --dangerously-load-development-channels。
5
+ * 双模式补丁:
6
+ * - 二进制模式(exe):按固定字符串搜索替换
7
+ * - AST 模式(cli.js / npm 安装):用 acorn 解析 JS AST,按语义特征定位替换
7
8
  *
8
9
  * 用法:npx cc-channel-patch # 修补
9
10
  * npx cc-channel-patch unpatch # 恢复
@@ -14,93 +15,55 @@ import path from 'node:path';
14
15
  import { homedir } from 'node:os';
15
16
  import { execSync } from 'node:child_process';
16
17
 
17
- // ─── 补丁定义 ──────────────────────────────────────────
18
- // [原始字符串, 替换字符串, 说明]
19
- // 长度必须完全一致(二进制原地替换)
20
-
21
- const PATCHES = [
22
- [
23
- 'function PaH(){return lA("tengu_harbor",!1)}',
24
- 'function PaH(){return !0 }',
25
- 'Channels feature flag (tengu_harbor)',
26
- ],
27
- [
28
- 'if(!yf()?.accessToken)',
29
- 'if( false )',
30
- 'Channel gate auth check',
31
- ],
32
- [
33
- 'noAuth:!yf()?.accessToken',
34
- 'noAuth: false ',
35
- 'UI noAuth display check',
36
- ],
18
+ // ─── 二进制模式补丁定义(exe 专用)────────────────────────
19
+ const BINARY_PATCHES = [
20
+ ['function PaH(){return lA("tengu_harbor",!1)}', 'function PaH(){return !0 }', 'Channels feature flag (tengu_harbor)'],
21
+ ['if(!yf()?.accessToken)', 'if( false )', 'Channel gate auth check'],
22
+ ['noAuth:!yf()?.accessToken', 'noAuth: false ', 'UI noAuth display check'],
37
23
  ];
38
24
 
39
25
  // ─── 查找 claude 可执行文件 ──────────────────────────────
40
26
 
41
27
  function findClaude() {
42
- // 1. which/where 查找(最可靠,覆盖所有自定义安装路径)
43
28
  try {
44
29
  const cmd = process.platform === 'win32' ? 'where claude 2>nul' : 'which claude 2>/dev/null';
45
- const p = execSync(cmd, {
46
- encoding: 'utf-8',
47
- stdio: ['pipe', 'pipe', 'pipe'],
48
- }).trim().split('\n')[0].trim();
30
+ const p = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0].trim();
49
31
  if (p && fs.existsSync(p)) return p;
50
32
  } catch { /* ignore */ }
51
33
 
52
- // 2. 常见安装路径(覆盖各种安装方式)
53
34
  const home = homedir();
54
35
  const candidates = [
55
- // Claude Code native installer(默认)
56
36
  path.join(home, '.local', 'bin', 'claude.exe'),
57
37
  path.join(home, '.local', 'bin', 'claude'),
58
- // npm 全局安装
59
38
  path.join(home, 'AppData', 'Roaming', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
60
39
  path.join(home, 'AppData', 'Roaming', 'npm', 'claude.cmd'),
61
- // scoop / chocolatey / winget 等 Windows 包管理器
62
40
  path.join(home, 'scoop', 'shims', 'claude.exe'),
63
41
  path.join('C:', 'ProgramData', 'chocolatey', 'bin', 'claude.exe'),
64
- // Program Files
65
42
  path.join('C:', 'Program Files', 'Claude Code', 'claude.exe'),
66
43
  path.join('C:', 'Program Files (x86)', 'Claude Code', 'claude.exe'),
67
- // AppData 常见路径
68
44
  path.join(home, 'AppData', 'Local', 'Programs', 'claude-code', 'claude.exe'),
69
45
  path.join(home, 'AppData', 'Local', 'claude-code', 'claude.exe'),
70
46
  path.join(home, 'AppData', 'Local', 'AnthropicClaude', 'claude.exe'),
71
- // macOS
72
47
  '/usr/local/bin/claude',
73
48
  '/opt/homebrew/bin/claude',
74
- path.join(home, '.nvm', 'versions', 'node'), // 标记,下面特殊处理
75
- // Linux
76
49
  '/usr/bin/claude',
77
50
  '/snap/bin/claude',
78
51
  ];
79
-
80
52
  for (const c of candidates) {
81
53
  if (fs.existsSync(c)) return c;
82
54
  }
83
55
 
84
- // 3. npm global prefix 动态查找(覆盖自定义 npm prefix)
85
56
  try {
86
- const prefix = execSync('npm config get prefix', {
87
- encoding: 'utf-8',
88
- stdio: ['pipe', 'pipe', 'pipe'],
89
- }).trim();
57
+ const prefix = execSync('npm config get prefix', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
90
58
  if (prefix) {
91
- const npmCandidates = [
92
- path.join(prefix, 'claude.cmd'),
93
- path.join(prefix, 'claude'),
59
+ for (const c of [
60
+ path.join(prefix, 'claude.cmd'), path.join(prefix, 'claude'),
94
61
  path.join(prefix, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
95
62
  path.join(prefix, 'bin', 'claude'),
96
- ];
97
- for (const c of npmCandidates) {
98
- if (fs.existsSync(c)) return c;
99
- }
63
+ ]) { if (fs.existsSync(c)) return c; }
100
64
  }
101
65
  } catch { /* ignore */ }
102
66
 
103
- // 4. PATH 环境变量逐目录搜索(兜底)
104
67
  const pathDirs = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':');
105
68
  const exeNames = process.platform === 'win32' ? ['claude.exe', 'claude.cmd'] : ['claude'];
106
69
  for (const dir of pathDirs) {
@@ -109,36 +72,25 @@ function findClaude() {
109
72
  if (fs.existsSync(p)) return p;
110
73
  }
111
74
  }
112
-
113
75
  return null;
114
76
  }
115
77
 
116
78
  // ─── 解析到真正的可 patch 文件 ─────────────────────────────
117
79
 
118
- /**
119
- * findClaude() 可能返回 .cmd wrapper 或符号链接。
120
- * 此函数解析到真正包含代码的文件(exe 二进制或 cli.js)。
121
- * 对于 npm 全局安装的 CC,.cmd 旁边有 node_modules/@anthropic-ai/claude-code/cli.js
122
- */
123
80
  function resolvePatchTarget(claudePath) {
124
81
  const ext = path.extname(claudePath).toLowerCase();
125
82
  const dir = path.dirname(claudePath);
126
83
 
127
- // 如果已经是 .exe 或大文件(>1MB),直接 patch
128
84
  if (ext === '.exe') return claudePath;
129
85
  const stat = fs.statSync(claudePath);
130
86
  if (stat.size > 1_000_000) return claudePath;
131
87
 
132
- // .cmd / 小文件 → 是 npm wrapper,查找真正的 cli.js
133
- // 策略 1:同目录下 node_modules/@anthropic-ai/claude-code/cli.js
134
88
  const cliJs = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
135
89
  if (fs.existsSync(cliJs)) return cliJs;
136
90
 
137
- // 策略 2:读取 .cmd 内容,提取实际路径
138
91
  if (ext === '.cmd') {
139
92
  try {
140
93
  const content = fs.readFileSync(claudePath, 'utf-8');
141
- // npm .cmd 格式:@IF EXIST "%~dp0\node.exe" (...) ELSE (... "%~dp0\node_modules\@anthropic-ai\claude-code\cli.js" ...)
142
94
  const match = content.match(/node_modules[\\/]@anthropic-ai[\\/]claude-code[\\/]cli\.js/);
143
95
  if (match) {
144
96
  const resolved = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
@@ -147,54 +99,48 @@ function resolvePatchTarget(claudePath) {
147
99
  } catch { /* ignore */ }
148
100
  }
149
101
 
150
- // 策略 3:如果是 symlink,resolve 到真实路径
151
102
  try {
152
103
  const realPath = fs.realpathSync(claudePath);
153
- if (realPath !== claudePath) {
154
- return resolvePatchTarget(realPath);
155
- }
104
+ if (realPath !== claudePath) return resolvePatchTarget(realPath);
156
105
  } catch { /* ignore */ }
157
106
 
158
- // 兜底:原路径
159
107
  return claudePath;
160
108
  }
161
109
 
162
- // ─── patch ──────────────────────────────────────────────
163
-
164
- function patch() {
165
- console.log('\n cc-channel-patch 启用 Claude Code Channels\n');
166
-
167
- const claudePath = findClaude();
168
- if (!claudePath) {
169
- console.error(' 找不到 Claude Code 可执行文件。');
170
- console.error(' 请确认已安装 Claude Code 并在 PATH 中。\n');
171
- process.exit(1);
172
- }
110
+ // ─── 判断文件类型 ──────────────────────────────────────────
111
+
112
+ function isBinaryFile(filePath) {
113
+ const ext = path.extname(filePath).toLowerCase();
114
+ if (ext === '.exe') return true;
115
+ // 检查文件头是否是 PE/ELF/Mach-O 二进制
116
+ const header = Buffer.alloc(4);
117
+ const fd = fs.openSync(filePath, 'r');
118
+ fs.readSync(fd, header, 0, 4, 0);
119
+ fs.closeSync(fd);
120
+ // PE: MZ, ELF: \x7fELF, Mach-O: \xfe\xed\xfa\xce / \xcf\xfa\xed\xfe
121
+ if (header[0] === 0x4D && header[1] === 0x5A) return true; // MZ (PE)
122
+ if (header[0] === 0x7F && header[1] === 0x45) return true; // ELF
123
+ if (header[0] === 0xFE && header[1] === 0xED) return true; // Mach-O
124
+ if (header[0] === 0xCF && header[1] === 0xFA) return true; // Mach-O 64
125
+ return false;
126
+ }
173
127
 
174
- const exePath = resolvePatchTarget(claudePath);
175
- console.log(` 查找: ${claudePath}`);
176
- if (exePath !== claudePath) {
177
- console.log(` 解析: ${exePath}`);
178
- }
179
- console.log(` 目标: ${exePath}`);
128
+ // ─── 二进制模式 patch ──────────────────────────────────────
180
129
 
130
+ function patchBinary(exePath) {
131
+ console.log(' 模式: 二进制字符串搜索\n');
181
132
  const buf = fs.readFileSync(exePath);
182
- let patched = 0;
183
- let skipped = 0;
184
- let missing = 0;
133
+ let patched = 0, skipped = 0;
185
134
 
186
- for (const [original, replacement, desc] of PATCHES) {
135
+ for (const [original, replacement, desc] of BINARY_PATCHES) {
187
136
  if (original.length !== replacement.length) {
188
137
  console.error(` 致命错误: "${desc}" 补丁长度不匹配`);
189
138
  process.exit(1);
190
139
  }
191
-
192
140
  const origBuf = Buffer.from(original);
193
141
  const patchBuf = Buffer.from(replacement);
194
142
 
195
- // 搜索并替换所有出现
196
- let pos = 0;
197
- let count = 0;
143
+ let pos = 0, count = 0;
198
144
  while (true) {
199
145
  const idx = buf.indexOf(origBuf, pos);
200
146
  if (idx === -1) break;
@@ -203,7 +149,6 @@ function patch() {
203
149
  pos = idx + 1;
204
150
  }
205
151
 
206
- // 检查是否已 patch
207
152
  let alreadyCount = 0;
208
153
  pos = 0;
209
154
  while (true) {
@@ -213,47 +158,225 @@ function patch() {
213
158
  pos = idx + 1;
214
159
  }
215
160
 
216
- if (count > 0) {
217
- console.log(` ${desc} — ${count} 处已修补`);
218
- patched += count;
219
- } else if (alreadyCount > 0) {
220
- console.log(` ⏭️ ${desc} — 已修补 (${alreadyCount} 处)`);
221
- skipped += alreadyCount;
222
- } else {
223
- console.log(` ⚠️ ${desc} — 未找到 (CC 版本可能不兼容)`);
224
- missing++;
161
+ if (count > 0) { console.log(` ✅ ${desc} — ${count} 处已修补`); patched += count; }
162
+ else if (alreadyCount > 0) { console.log(` ⏭️ ${desc} — 已修补 (${alreadyCount} )`); skipped += alreadyCount; }
163
+ else { console.log(` ⚠️ ${desc} — 未找到`); }
164
+ }
165
+
166
+ if (patched === 0 && skipped > 0) { console.log('\n 所有补丁已生效,无需操作。\n'); return; }
167
+ if (patched === 0) { console.error('\n 未找到可修补位置。\n'); process.exit(1); }
168
+
169
+ writeResult(exePath, buf);
170
+ }
171
+
172
+ // ─── AST 模式 patch(npm cli.js)──────────────────────────
173
+
174
+ async function patchAst(exePath) {
175
+ console.log(' 模式: AST 语义分析\n');
176
+
177
+ // 动态下载 acorn(不加依赖)
178
+ const acornPath = path.join(homedir(), '.cache', 'cc-channel-patch', 'acorn.js');
179
+ if (!fs.existsSync(acornPath)) {
180
+ console.log(' 下载 acorn 解析器...');
181
+ fs.mkdirSync(path.dirname(acornPath), { recursive: true });
182
+ try {
183
+ const resp = await fetch('https://unpkg.com/acorn@8.14.0/dist/acorn.js');
184
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
185
+ fs.writeFileSync(acornPath, await resp.text());
186
+ } catch (e) {
187
+ console.error(` 下载 acorn 失败: ${e.message}`);
188
+ console.error(' 可手动下载 https://unpkg.com/acorn@8.14.0/dist/acorn.js 到 ' + acornPath);
189
+ process.exit(1);
225
190
  }
226
191
  }
227
192
 
228
- if (patched === 0 && skipped > 0) {
229
- console.log('\n 所有补丁已生效,无需操作。\n');
230
- return;
193
+ // 加载 acorn
194
+ const { createRequire } = await import('node:module');
195
+ const require = createRequire(import.meta.url);
196
+ const acorn = require(acornPath);
197
+
198
+ // 读取 cli.js
199
+ let code = fs.readFileSync(exePath, 'utf-8');
200
+ let shebang = '';
201
+ if (code.startsWith('#!')) {
202
+ const idx = code.indexOf('\n');
203
+ shebang = code.slice(0, idx + 1);
204
+ code = code.slice(idx + 1);
205
+ }
206
+
207
+ // 解析 AST
208
+ let ast;
209
+ try {
210
+ ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' });
211
+ } catch (e) {
212
+ console.error(` AST 解析失败: ${e.message}`);
213
+ process.exit(1);
214
+ }
215
+
216
+ const src = (node) => code.slice(node.start, node.end);
217
+
218
+ function findNodes(node, predicate, results = []) {
219
+ if (!node || typeof node !== 'object') return results;
220
+ if (predicate(node)) results.push(node);
221
+ for (const key in node) {
222
+ if (node[key] && typeof node[key] === 'object') {
223
+ if (Array.isArray(node[key])) node[key].forEach(child => findNodes(child, predicate, results));
224
+ else findNodes(node[key], predicate, results);
225
+ }
226
+ }
227
+ return results;
231
228
  }
232
229
 
233
- if (patched === 0) {
230
+ const replacements = [];
231
+ let patchCount = 0;
232
+
233
+ // ── Patch 1: tengu_harbor feature flag ──
234
+ const harborCalls = findNodes(ast, n =>
235
+ n.type === 'CallExpression' && n.arguments?.length === 2 &&
236
+ n.arguments[0].type === 'Literal' && n.arguments[0].value === 'tengu_harbor' &&
237
+ n.arguments[0].value !== 'tengu_harbor_ledger'
238
+ );
239
+
240
+ let harborPatched = false;
241
+ let harborCalleeName = '';
242
+ for (const call of harborCalls) {
243
+ harborCalleeName = src(call.callee);
244
+ const arg = call.arguments[1];
245
+ if (arg.type === 'UnaryExpression' && arg.operator === '!' && arg.argument.type === 'Literal' && arg.argument.value === 1) {
246
+ replacements.push({ start: arg.start, end: arg.end, replacement: '!0', name: 'harborFlag' });
247
+ patchCount++;
248
+ console.log(` ✅ tengu_harbor flag — ${harborCalleeName}("tengu_harbor", !1) → !0`);
249
+ break;
250
+ }
251
+ if (arg.type === 'UnaryExpression' && arg.operator === '!' && arg.argument.type === 'Literal' && arg.argument.value === 0) {
252
+ harborPatched = true;
253
+ console.log(' ⏭️ tengu_harbor flag — 已修补');
254
+ break;
255
+ }
256
+ }
257
+
258
+ // ── Patch 2: channel decision function(qMq)──
259
+ const markerLiterals = findNodes(ast, n =>
260
+ n.type === 'Literal' && n.value === 'channels feature is not currently available'
261
+ );
262
+
263
+ let qMqPatched = false;
264
+ if (markerLiterals.length > 0) {
265
+ const markerPos = markerLiterals[0].start;
266
+ const enclosingFuncs = findNodes(ast, n =>
267
+ (n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression') &&
268
+ n.start < markerPos && n.end > markerPos
269
+ );
270
+ if (enclosingFuncs.length > 0) {
271
+ const targetFunc = enclosingFuncs.sort((a, b) => (a.end - a.start) - (b.end - b.start))[0];
272
+ const funcName = targetFunc.id?.name || '(anonymous)';
273
+ const bodyStatements = targetFunc.body.body;
274
+
275
+ if (bodyStatements?.length > 0 && bodyStatements[0].type === 'IfStatement') {
276
+ const firstStmt = bodyStatements[0];
277
+ const firstSrc = src(firstStmt);
278
+ if (firstSrc.includes('claude/channel')) {
279
+ // 保留第一个 capability check,删除其余所有 check,直接 return register
280
+ const capCheckSrc = src(firstStmt);
281
+ const newBody = '{' + capCheckSrc + 'return{action:"register"}}';
282
+ replacements.push({ start: targetFunc.body.start, end: targetFunc.body.end, replacement: newBody, name: 'qMq' });
283
+ patchCount++;
284
+ console.log(` ✅ Channel decision ${funcName}() — 绕过 check 2-7,保留 capability check`);
285
+ }
286
+ }
287
+ }
288
+ } else {
289
+ // 检查是否已 patch
290
+ if (code.includes('claude/channel capability') && !code.includes('channels feature is not currently available')) {
291
+ qMqPatched = true;
292
+ console.log(' ⏭️ Channel decision — 已修补');
293
+ }
294
+ }
295
+
296
+ // ── Patch 3: UI notice function(xl1/Kq_)──
297
+ const policyProps = findNodes(ast, n =>
298
+ n.type === 'Property' && n.key?.type === 'Identifier' && n.key.name === 'policyBlocked' &&
299
+ n.value && src(n.value).includes('channelsEnabled')
300
+ );
301
+
302
+ let noticePatched = false;
303
+ if (policyProps.length > 0) {
304
+ const propPos = policyProps[0].start;
305
+ const noticeFuncs = findNodes(ast, n =>
306
+ (n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression') &&
307
+ n.start < propPos && n.end > propPos
308
+ );
309
+ if (noticeFuncs.length > 0) {
310
+ const noticeFunc = noticeFuncs.sort((a, b) => (a.end - a.start) - (b.end - b.start))[0];
311
+ const nfName = noticeFunc.id?.name || '(anonymous)';
312
+
313
+ // 提取 getAllowedChannels 和 formatter
314
+ const firstCalls = findNodes(noticeFunc.body.body[0], n =>
315
+ n.type === 'CallExpression' && n.callee?.type === 'Identifier'
316
+ );
317
+ const getAllowedChannels = firstCalls.length > 0 ? src(firstCalls[0]) : '$N()';
318
+
319
+ const mapCalls = findNodes(noticeFunc, n =>
320
+ n.type === 'CallExpression' && n.callee?.type === 'MemberExpression' && n.callee.property?.name === 'map'
321
+ );
322
+ let formatter = 'naH';
323
+ if (mapCalls.length > 0 && mapCalls[0].arguments[0]) formatter = src(mapCalls[0].arguments[0]);
324
+
325
+ const newBody = '{let A=' + getAllowedChannels + ';let q=A.length>0?A.map(' + formatter + ').join(", "):"";return{channels:A,disabled:!1,noAuth:!1,policyBlocked:!1,list:q}}';
326
+ replacements.push({ start: noticeFunc.body.start, end: noticeFunc.body.end, replacement: newBody, name: 'notice' });
327
+ patchCount++;
328
+ console.log(` ✅ UI notice ${nfName}() — disabled/noAuth/policyBlocked 全部置 false`);
329
+ }
330
+ } else {
331
+ if (code.includes('policyBlocked') && !code.includes('channelsEnabled!==!0')) {
332
+ noticePatched = true;
333
+ console.log(' ⏭️ UI notice — 已修补');
334
+ }
335
+ }
336
+
337
+ if (patchCount === 0) {
338
+ if (harborPatched || qMqPatched || noticePatched) {
339
+ console.log('\n 所有补丁已生效,无需操作。\n');
340
+ return;
341
+ }
234
342
  console.error('\n 未找到可修补位置。Claude Code 版本可能不兼容。\n');
235
343
  process.exit(1);
236
344
  }
237
345
 
238
- // 备份
346
+ // 从后往前替换(保持位置不变)
347
+ replacements.sort((a, b) => b.start - a.start);
348
+ let newCode = code;
349
+ for (const r of replacements) {
350
+ newCode = newCode.slice(0, r.start) + r.replacement + newCode.slice(r.end);
351
+ }
352
+
353
+ // 验证
354
+ if (!newCode.includes('claude/channel')) {
355
+ console.error(' 验证失败: capability check 未保留');
356
+ process.exit(1);
357
+ }
358
+
359
+ writeResult(exePath, Buffer.from(shebang + newCode, 'utf-8'));
360
+ }
361
+
362
+ // ─── 写入结果 ──────────────────────────────────────────────
363
+
364
+ function writeResult(exePath, buf) {
239
365
  const backupPath = exePath + '.bak';
240
366
  if (!fs.existsSync(backupPath)) {
241
367
  fs.copyFileSync(exePath, backupPath);
242
368
  console.log(`\n 📦 已备份: ${backupPath}`);
243
369
  }
244
370
 
245
- // 尝试直接写入
246
371
  try {
247
372
  fs.writeFileSync(exePath, buf);
248
373
  console.log('\n ✅ 补丁已直接写入,立即生效!\n');
249
374
  } catch {
250
- // 文件被锁(CC 正在运行),写到临时文件
251
375
  const tmpPath = exePath + '.patched';
252
376
  fs.writeFileSync(tmpPath, buf);
253
377
  console.log(`\n ⚠️ Claude Code 正在运行,无法直接写入。`);
254
378
  console.log(` 补丁已保存到: ${tmpPath}\n`);
255
379
  console.log(' 请退出所有 Claude Code 后执行:\n');
256
-
257
380
  if (process.platform === 'win32') {
258
381
  const dir = path.dirname(exePath);
259
382
  const name = path.basename(exePath);
@@ -266,27 +389,44 @@ function patch() {
266
389
  }
267
390
  console.log();
268
391
  }
269
-
270
392
  console.log(' 恢复方法: npx cc-channel-patch unpatch\n');
271
393
  }
272
394
 
273
- // ─── unpatch ────────────────────────────────────────────
395
+ // ─── patch 入口 ────────────────────────────────────────────
274
396
 
275
- function unpatch() {
276
- console.log('\n cc-channel-patch unpatch 恢复原始 Claude Code\n');
397
+ async function patch() {
398
+ console.log('\n cc-channel-patch — 启用 Claude Code Channels\n');
277
399
 
278
400
  const claudePath = findClaude();
279
401
  if (!claudePath) {
280
- console.error(' 找不到 Claude Code。\n');
402
+ console.error(' 找不到 Claude Code。请确认已安装并在 PATH 中。\n');
281
403
  process.exit(1);
282
404
  }
283
405
 
284
406
  const exePath = resolvePatchTarget(claudePath);
407
+ console.log(` 查找: ${claudePath}`);
408
+ if (exePath !== claudePath) console.log(` 解析: ${exePath}`);
409
+ console.log(` 目标: ${exePath}\n`);
410
+
411
+ if (isBinaryFile(exePath)) {
412
+ patchBinary(exePath);
413
+ } else {
414
+ await patchAst(exePath);
415
+ }
416
+ }
285
417
 
418
+ // ─── unpatch ────────────────────────────────────────────────
419
+
420
+ function unpatch() {
421
+ console.log('\n cc-channel-patch unpatch — 恢复原始 Claude Code\n');
422
+
423
+ const claudePath = findClaude();
424
+ if (!claudePath) { console.error(' 找不到 Claude Code。\n'); process.exit(1); }
425
+
426
+ const exePath = resolvePatchTarget(claudePath);
286
427
  const backupPath = exePath + '.bak';
287
428
  if (!fs.existsSync(backupPath)) {
288
- console.error(` 未找到备份文件: ${backupPath}`);
289
- console.error(' 无法恢复(可能从未 patch 过)。\n');
429
+ console.error(` 未找到备份文件: ${backupPath}\n`);
290
430
  process.exit(1);
291
431
  }
292
432
 
@@ -299,7 +439,6 @@ function unpatch() {
299
439
  console.log(' ⚠️ Claude Code 正在运行,无法直接恢复。');
300
440
  console.log(` 恢复文件已保存到: ${tmpPath}\n`);
301
441
  console.log(' 请退出所有 Claude Code 后执行:\n');
302
-
303
442
  if (process.platform === 'win32') {
304
443
  const dir = path.dirname(exePath);
305
444
  const name = path.basename(exePath);
@@ -314,7 +453,7 @@ function unpatch() {
314
453
  }
315
454
  }
316
455
 
317
- // ─── 入口 ───────────────────────────────────────────────
456
+ // ─── 入口 ───────────────────────────────────────────────────
318
457
 
319
458
  const cmd = process.argv[2];
320
459
  if (cmd === 'unpatch' || cmd === 'restore') {
@@ -323,11 +462,13 @@ if (cmd === 'unpatch' || cmd === 'restore') {
323
462
  console.log(`
324
463
  cc-channel-patch — 启用 Claude Code Channels 功能
325
464
 
465
+ 支持 exe 安装版(二进制模式)和 npm 安装版(AST 模式),自动检测。
466
+
326
467
  用法:
327
468
  npx cc-channel-patch 修补 Claude Code
328
469
  npx cc-channel-patch unpatch 恢复原始版本
329
470
  npx cc-channel-patch --help 显示帮助
330
471
  `);
331
472
  } else {
332
- patch();
473
+ patch().catch(e => { console.error(` 错误: ${e.message}\n`); process.exit(1); });
333
474
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-channel-patch",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "One-command patch to enable Claude Code Channels (bypasses tengu_harbor feature flag)",
5
5
  "type": "module",
6
6
  "bin": {