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
package/src/patch.ts
CHANGED
|
@@ -1,49 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* cc-wechat
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
15
|
-
|
|
16
|
-
[
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
325
|
+
if (isBinaryFile(exePath)) { patchBinary(exePath); }
|
|
326
|
+
else { await patchAst(exePath); }
|
|
327
|
+
}
|
|
243
328
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
console.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.log(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
console.log(`
|
|
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
|
-
|
|
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); }); }
|