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/dist/patch.d.ts +3 -3
- package/dist/patch.js +234 -108
- package/dist/patch.js.map +1 -1
- package/package.json +1 -1
- package/packages/cc-channel-patch/index.mjs +260 -119
- package/packages/cc-channel-patch/package.json +1 -1
- package/src/patch.ts +235 -153
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* cc-channel-patch — 一键启用 Claude Code Channels
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
// ───
|
|
163
|
-
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
// ───
|
|
395
|
+
// ─── patch 入口 ────────────────────────────────────────────
|
|
274
396
|
|
|
275
|
-
function
|
|
276
|
-
console.log('\n cc-channel-patch
|
|
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
|
|
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
|
}
|