cc-wechat 0.1.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/.claude-plugin/plugin.json +6 -0
- package/.mcp.json +8 -0
- package/.pace/stop-block-count +1 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/auth.d.ts +18 -0
- package/dist/auth.js +351 -0
- package/dist/auth.js.map +1 -0
- package/dist/cdn.d.ts +39 -0
- package/dist/cdn.js +228 -0
- package/dist/cdn.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +127 -0
- package/dist/cli.js.map +1 -0
- package/dist/ilink-api.d.ts +33 -0
- package/dist/ilink-api.js +206 -0
- package/dist/ilink-api.js.map +1 -0
- package/dist/patch.d.ts +7 -0
- package/dist/patch.js +165 -0
- package/dist/patch.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +406 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +57 -0
- package/dist/store.js.map +1 -0
- package/dist/text-utils.d.ts +7 -0
- package/dist/text-utils.js +56 -0
- package/dist/text-utils.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +24 -0
- package/packages/cc-channel-patch/README.md +36 -0
- package/packages/cc-channel-patch/index.mjs +228 -0
- package/packages/cc-channel-patch/package.json +11 -0
- package/skills/configure/SKILL.md +32 -0
- package/src/auth.ts +400 -0
- package/src/cdn.ts +261 -0
- package/src/cli.ts +121 -0
- package/src/ilink-api.ts +279 -0
- package/src/patch.ts +182 -0
- package/src/qrcode-terminal.d.ts +10 -0
- package/src/server.ts +445 -0
- package/src/store.ts +62 -0
- package/src/text-utils.ts +56 -0
- package/src/types.ts +94 -0
- package/tsconfig.json +17 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc-wechat 凭证持久化 — account.json 原子写入 + sync buf
|
|
3
|
+
*/
|
|
4
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
const ACCOUNT_FILE = 'account.json';
|
|
8
|
+
const SYNC_BUF_FILE = 'sync-buf.txt';
|
|
9
|
+
/**
|
|
10
|
+
* 获取状态目录路径,不存在则自动创建
|
|
11
|
+
*/
|
|
12
|
+
export function getStateDir() {
|
|
13
|
+
const dir = join(homedir(), '.claude', 'channels', 'wechat');
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 原子写入账号数据(先写 tmp 再 rename)
|
|
19
|
+
*/
|
|
20
|
+
export function saveAccount(data) {
|
|
21
|
+
const dir = getStateDir();
|
|
22
|
+
const tmpPath = join(dir, `${ACCOUNT_FILE}.tmp`);
|
|
23
|
+
const finalPath = join(dir, ACCOUNT_FILE);
|
|
24
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
25
|
+
renameSync(tmpPath, finalPath);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 读取当前活跃账号,文件不存在返回 null
|
|
29
|
+
*/
|
|
30
|
+
export function getActiveAccount() {
|
|
31
|
+
try {
|
|
32
|
+
const filePath = join(getStateDir(), ACCOUNT_FILE);
|
|
33
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 读取 sync buf,不存在返回空字符串
|
|
42
|
+
*/
|
|
43
|
+
export function loadSyncBuf() {
|
|
44
|
+
try {
|
|
45
|
+
return readFileSync(join(getStateDir(), SYNC_BUF_FILE), 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 写入 sync buf
|
|
53
|
+
*/
|
|
54
|
+
export function saveSyncBuf(buf) {
|
|
55
|
+
writeFileSync(join(getStateDir(), SYNC_BUF_FILE), buf, 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,MAAM,YAAY,GAAG,cAAc,CAAC;AACpC,MAAM,aAAa,GAAG,cAAc,CAAC;AAErC;;GAEG;AACH,MAAM,UAAU,WAAW;IACzB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC7D,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,IAAiB;IAC3C,MAAM,GAAG,GAAG,WAAW,EAAE,CAAC;IAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,MAAM,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC1C,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC/D,UAAU,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACjC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,EAAE,YAAY,CAAC,CAAC;QACnD,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgB,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,EAAE,OAAO,CAAC,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,aAAa,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本处理工具 — Markdown 清理 + 分段
|
|
3
|
+
*/
|
|
4
|
+
/** 去除 Markdown 格式,转为微信纯文本 */
|
|
5
|
+
export function stripMarkdown(text) {
|
|
6
|
+
let result = text;
|
|
7
|
+
// 代码围栏:去除 ``` 保留内容
|
|
8
|
+
result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
|
|
9
|
+
// 图片链接:移除
|
|
10
|
+
result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
|
|
11
|
+
// 链接:保留显示文字
|
|
12
|
+
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
|
|
13
|
+
// 粗体
|
|
14
|
+
result = result.replace(/\*\*(.+?)\*\*/g, '$1');
|
|
15
|
+
result = result.replace(/__(.+?)__/g, '$1');
|
|
16
|
+
// 斜体
|
|
17
|
+
result = result.replace(/\*(.+?)\*/g, '$1');
|
|
18
|
+
result = result.replace(/_(.+?)_/g, '$1');
|
|
19
|
+
// 标题
|
|
20
|
+
result = result.replace(/^#{1,6}\s+/gm, '');
|
|
21
|
+
// 水平线
|
|
22
|
+
result = result.replace(/^[-*_]{3,}$/gm, '');
|
|
23
|
+
// 引用
|
|
24
|
+
result = result.replace(/^>\s?/gm, '');
|
|
25
|
+
return result.trim();
|
|
26
|
+
}
|
|
27
|
+
/** 将长文本分段(微信限制约 4000 字符) */
|
|
28
|
+
export function chunkText(text, maxLen = 3900) {
|
|
29
|
+
if (text.length <= maxLen)
|
|
30
|
+
return [text];
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let remaining = text;
|
|
33
|
+
while (remaining.length > 0) {
|
|
34
|
+
if (remaining.length <= maxLen) {
|
|
35
|
+
chunks.push(remaining);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
// 优先在双换行处分割
|
|
39
|
+
let breakAt = remaining.lastIndexOf('\n\n', maxLen);
|
|
40
|
+
if (breakAt < maxLen * 0.3) {
|
|
41
|
+
// 其次单换行
|
|
42
|
+
breakAt = remaining.lastIndexOf('\n', maxLen);
|
|
43
|
+
}
|
|
44
|
+
if (breakAt < maxLen * 0.3) {
|
|
45
|
+
// 其次空格(避免截断 URL)
|
|
46
|
+
breakAt = remaining.lastIndexOf(' ', maxLen);
|
|
47
|
+
}
|
|
48
|
+
if (breakAt < maxLen * 0.3) {
|
|
49
|
+
breakAt = maxLen;
|
|
50
|
+
}
|
|
51
|
+
chunks.push(remaining.slice(0, breakAt));
|
|
52
|
+
remaining = remaining.slice(breakAt).trimStart();
|
|
53
|
+
}
|
|
54
|
+
return chunks;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=text-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-utils.js","sourceRoot":"","sources":["../src/text-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,6BAA6B;AAC7B,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,mBAAmB;IACnB,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,4BAA4B,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACxF,UAAU;IACV,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC;IACrD,YAAY;IACZ,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC;IACxD,KAAK;IACL,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;IAChD,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAC5C,KAAK;IACL,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAC5C,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC1C,KAAK;IACL,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAC5C,MAAM;IACN,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAC7C,KAAK;IACL,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACvB,CAAC;AAED,4BAA4B;AAC5B,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,MAAM,GAAG,IAAI;IACnD,IAAI,IAAI,CAAC,MAAM,IAAI,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,SAAS,GAAG,IAAI,CAAC;IACrB,OAAO,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,IAAI,SAAS,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACvB,MAAM;QACR,CAAC;QACD,YAAY;QACZ,IAAI,OAAO,GAAG,SAAS,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACpD,IAAI,OAAO,GAAG,MAAM,GAAG,GAAG,EAAE,CAAC;YAC3B,QAAQ;YACR,OAAO,GAAG,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,OAAO,GAAG,MAAM,GAAG,GAAG,EAAE,CAAC;YAC3B,iBAAiB;YACjB,OAAO,GAAG,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC;QACD,IAAI,OAAO,GAAG,MAAM,GAAG,GAAG,EAAE,CAAC;YAC3B,OAAO,GAAG,MAAM,CAAC;QACnB,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACzC,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC;IACnD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc-wechat 类型定义 — iLink Bot API 请求/响应/消息结构
|
|
3
|
+
*/
|
|
4
|
+
export declare const MessageItemType: {
|
|
5
|
+
readonly NONE: 0;
|
|
6
|
+
readonly TEXT: 1;
|
|
7
|
+
readonly IMAGE: 2;
|
|
8
|
+
readonly VOICE: 3;
|
|
9
|
+
readonly FILE: 4;
|
|
10
|
+
readonly VIDEO: 5;
|
|
11
|
+
};
|
|
12
|
+
export interface CDNMedia {
|
|
13
|
+
encrypt_query_param?: string;
|
|
14
|
+
aes_key?: string;
|
|
15
|
+
encrypt_type?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface MessageItem {
|
|
18
|
+
type?: number;
|
|
19
|
+
text_item?: {
|
|
20
|
+
text?: string;
|
|
21
|
+
};
|
|
22
|
+
image_item?: {
|
|
23
|
+
media?: CDNMedia;
|
|
24
|
+
url?: string;
|
|
25
|
+
mid_size?: number;
|
|
26
|
+
};
|
|
27
|
+
voice_item?: {
|
|
28
|
+
media?: CDNMedia;
|
|
29
|
+
text?: string;
|
|
30
|
+
playtime?: number;
|
|
31
|
+
};
|
|
32
|
+
file_item?: {
|
|
33
|
+
media?: CDNMedia;
|
|
34
|
+
file_name?: string;
|
|
35
|
+
len?: string;
|
|
36
|
+
md5?: string;
|
|
37
|
+
};
|
|
38
|
+
video_item?: {
|
|
39
|
+
media?: CDNMedia;
|
|
40
|
+
video_size?: number;
|
|
41
|
+
};
|
|
42
|
+
ref_msg?: {
|
|
43
|
+
title?: string;
|
|
44
|
+
message_item?: MessageItem;
|
|
45
|
+
};
|
|
46
|
+
msg_id?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface WeixinMessage {
|
|
49
|
+
seq?: number;
|
|
50
|
+
message_id?: number;
|
|
51
|
+
from_user_id?: string;
|
|
52
|
+
to_user_id?: string;
|
|
53
|
+
client_id?: string;
|
|
54
|
+
create_time_ms?: number;
|
|
55
|
+
session_id?: string;
|
|
56
|
+
message_type?: number;
|
|
57
|
+
message_state?: number;
|
|
58
|
+
item_list?: MessageItem[];
|
|
59
|
+
context_token?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface GetUpdatesResp {
|
|
62
|
+
ret?: number;
|
|
63
|
+
errcode?: number;
|
|
64
|
+
errmsg?: string;
|
|
65
|
+
msgs?: WeixinMessage[];
|
|
66
|
+
get_updates_buf?: string;
|
|
67
|
+
longpolling_timeout_ms?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface QRCodeResponse {
|
|
70
|
+
qrcode: string;
|
|
71
|
+
qrcode_img_content: string;
|
|
72
|
+
}
|
|
73
|
+
export interface QRStatusResponse {
|
|
74
|
+
status: 'wait' | 'scaned' | 'confirmed' | 'expired';
|
|
75
|
+
bot_token?: string;
|
|
76
|
+
ilink_bot_id?: string;
|
|
77
|
+
baseurl?: string;
|
|
78
|
+
ilink_user_id?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface GetConfigResp {
|
|
81
|
+
ret?: number;
|
|
82
|
+
typing_ticket?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface GetUploadUrlResp {
|
|
85
|
+
upload_param?: string;
|
|
86
|
+
thumb_upload_param?: string;
|
|
87
|
+
filekey?: string;
|
|
88
|
+
}
|
|
89
|
+
export interface AccountData {
|
|
90
|
+
token: string;
|
|
91
|
+
baseUrl: string;
|
|
92
|
+
botId: string;
|
|
93
|
+
userId?: string;
|
|
94
|
+
savedAt: string;
|
|
95
|
+
}
|
|
96
|
+
export interface BaseInfo {
|
|
97
|
+
channel_version?: string;
|
|
98
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,WAAW;AACX,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACA,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-wechat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WeChat channel for Claude Code via iLink Bot API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-wechat": "dist/cli.js",
|
|
8
|
+
"cc-wechat-server": "dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch"
|
|
13
|
+
},
|
|
14
|
+
"engines": { "node": ">=22" },
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
18
|
+
"qrcode-terminal": "^0.12.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"typescript": "^5.7.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# cc-channel-patch
|
|
2
|
+
|
|
3
|
+
一键启用 Claude Code Channels 功能。
|
|
4
|
+
|
|
5
|
+
适用于使用代理认证(非 claude.ai 直接登录)的用户,绕过 Anthropic 的 `tengu_harbor` 云控开关和 `accessToken` 检查。
|
|
6
|
+
|
|
7
|
+
## 用法
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 修补(启用 Channels)
|
|
11
|
+
npx cc-channel-patch
|
|
12
|
+
|
|
13
|
+
# 恢复原始版本
|
|
14
|
+
npx cc-channel-patch unpatch
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 原理
|
|
18
|
+
|
|
19
|
+
Claude Code v2.1.80+ 内置了 Channels 功能(通过 MCP Server 桥接外部消息平台),但受服务端 feature flag 灰度控制。此补丁修改 3 处检查:
|
|
20
|
+
|
|
21
|
+
1. `PaH()` — `tengu_harbor` feature flag 始终返回 true
|
|
22
|
+
2. `S1_ gate` — 跳过 `accessToken` 认证检查
|
|
23
|
+
3. `xl1() UI` — 跳过 UI 层的 noAuth 提示
|
|
24
|
+
|
|
25
|
+
补丁按特征字符串搜索(非硬编码偏移量),CC 小版本更新后通常仍可用。
|
|
26
|
+
|
|
27
|
+
## 注意事项
|
|
28
|
+
|
|
29
|
+
- 如果 Claude Code 正在运行,补丁无法直接写入(文件被锁),会生成 `.patched` 文件并提示手动替换
|
|
30
|
+
- 原始文件自动备份到 `.bak`
|
|
31
|
+
- CC 大版本更新后可能需要重新 patch
|
|
32
|
+
- `npx cc-channel-patch unpatch` 可随时恢复
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cc-channel-patch — 一键启用 Claude Code Channels
|
|
4
|
+
*
|
|
5
|
+
* 绕过 Anthropic 的 tengu_harbor 云控 + accessToken 认证检查,
|
|
6
|
+
* 使代理认证模式下也能使用 --dangerously-load-development-channels。
|
|
7
|
+
*
|
|
8
|
+
* 用法:npx cc-channel-patch # 修补
|
|
9
|
+
* npx cc-channel-patch unpatch # 恢复
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
|
|
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
|
+
],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// ─── 查找 claude 可执行文件 ──────────────────────────────
|
|
40
|
+
|
|
41
|
+
function findClaude() {
|
|
42
|
+
// 1. which/where 查找
|
|
43
|
+
try {
|
|
44
|
+
const p = execSync(process.platform === 'win32' ? 'where claude' : 'which claude', {
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
}).trim().split('\n')[0].trim();
|
|
48
|
+
if (p && fs.existsSync(p)) return p;
|
|
49
|
+
} catch { /* ignore */ }
|
|
50
|
+
|
|
51
|
+
// 2. 常见路径
|
|
52
|
+
const home = homedir();
|
|
53
|
+
const candidates = [
|
|
54
|
+
path.join(home, '.local', 'bin', 'claude.exe'),
|
|
55
|
+
path.join(home, '.local', 'bin', 'claude'),
|
|
56
|
+
path.join(home, 'AppData', 'Roaming', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
57
|
+
'/usr/local/bin/claude',
|
|
58
|
+
];
|
|
59
|
+
for (const c of candidates) {
|
|
60
|
+
if (fs.existsSync(c)) return c;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── patch ──────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function patch() {
|
|
68
|
+
console.log('\n cc-channel-patch — 启用 Claude Code Channels\n');
|
|
69
|
+
|
|
70
|
+
const exePath = findClaude();
|
|
71
|
+
if (!exePath) {
|
|
72
|
+
console.error(' 找不到 Claude Code 可执行文件。');
|
|
73
|
+
console.error(' 请确认已安装 Claude Code 并在 PATH 中。\n');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log(` 目标: ${exePath}`);
|
|
77
|
+
|
|
78
|
+
const buf = fs.readFileSync(exePath);
|
|
79
|
+
let patched = 0;
|
|
80
|
+
let skipped = 0;
|
|
81
|
+
let missing = 0;
|
|
82
|
+
|
|
83
|
+
for (const [original, replacement, desc] of PATCHES) {
|
|
84
|
+
if (original.length !== replacement.length) {
|
|
85
|
+
console.error(` 致命错误: "${desc}" 补丁长度不匹配`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const origBuf = Buffer.from(original);
|
|
90
|
+
const patchBuf = Buffer.from(replacement);
|
|
91
|
+
|
|
92
|
+
// 搜索并替换所有出现
|
|
93
|
+
let pos = 0;
|
|
94
|
+
let count = 0;
|
|
95
|
+
while (true) {
|
|
96
|
+
const idx = buf.indexOf(origBuf, pos);
|
|
97
|
+
if (idx === -1) break;
|
|
98
|
+
patchBuf.copy(buf, idx);
|
|
99
|
+
count++;
|
|
100
|
+
pos = idx + 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 检查是否已 patch
|
|
104
|
+
let alreadyCount = 0;
|
|
105
|
+
pos = 0;
|
|
106
|
+
while (true) {
|
|
107
|
+
const idx = buf.indexOf(patchBuf, pos);
|
|
108
|
+
if (idx === -1) break;
|
|
109
|
+
alreadyCount++;
|
|
110
|
+
pos = idx + 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (count > 0) {
|
|
114
|
+
console.log(` ✅ ${desc} — ${count} 处已修补`);
|
|
115
|
+
patched += count;
|
|
116
|
+
} else if (alreadyCount > 0) {
|
|
117
|
+
console.log(` ⏭️ ${desc} — 已修补 (${alreadyCount} 处)`);
|
|
118
|
+
skipped += alreadyCount;
|
|
119
|
+
} else {
|
|
120
|
+
console.log(` ⚠️ ${desc} — 未找到 (CC 版本可能不兼容)`);
|
|
121
|
+
missing++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (patched === 0 && skipped > 0) {
|
|
126
|
+
console.log('\n 所有补丁已生效,无需操作。\n');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (patched === 0) {
|
|
131
|
+
console.error('\n 未找到可修补位置。Claude Code 版本可能不兼容。\n');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 备份
|
|
136
|
+
const backupPath = exePath + '.bak';
|
|
137
|
+
if (!fs.existsSync(backupPath)) {
|
|
138
|
+
fs.copyFileSync(exePath, backupPath);
|
|
139
|
+
console.log(`\n 📦 已备份: ${backupPath}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 尝试直接写入
|
|
143
|
+
try {
|
|
144
|
+
fs.writeFileSync(exePath, buf);
|
|
145
|
+
console.log('\n ✅ 补丁已直接写入,立即生效!\n');
|
|
146
|
+
} catch {
|
|
147
|
+
// 文件被锁(CC 正在运行),写到临时文件
|
|
148
|
+
const tmpPath = exePath + '.patched';
|
|
149
|
+
fs.writeFileSync(tmpPath, buf);
|
|
150
|
+
console.log(`\n ⚠️ Claude Code 正在运行,无法直接写入。`);
|
|
151
|
+
console.log(` 补丁已保存到: ${tmpPath}\n`);
|
|
152
|
+
console.log(' 请退出所有 Claude Code 后执行:\n');
|
|
153
|
+
|
|
154
|
+
if (process.platform === 'win32') {
|
|
155
|
+
const dir = path.dirname(exePath);
|
|
156
|
+
const name = path.basename(exePath);
|
|
157
|
+
console.log(` cd "${dir}"`);
|
|
158
|
+
console.log(` Move-Item ${name} ${name}.old -Force`);
|
|
159
|
+
console.log(` Move-Item ${name}.patched ${name} -Force`);
|
|
160
|
+
} else {
|
|
161
|
+
console.log(` mv "${exePath}" "${exePath}.old"`);
|
|
162
|
+
console.log(` mv "${tmpPath}" "${exePath}"`);
|
|
163
|
+
}
|
|
164
|
+
console.log();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(' 恢复方法: npx cc-channel-patch unpatch\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── unpatch ────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function unpatch() {
|
|
173
|
+
console.log('\n cc-channel-patch unpatch — 恢复原始 Claude Code\n');
|
|
174
|
+
|
|
175
|
+
const exePath = findClaude();
|
|
176
|
+
if (!exePath) {
|
|
177
|
+
console.error(' 找不到 Claude Code。\n');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const backupPath = exePath + '.bak';
|
|
182
|
+
if (!fs.existsSync(backupPath)) {
|
|
183
|
+
console.error(` 未找到备份文件: ${backupPath}`);
|
|
184
|
+
console.error(' 无法恢复(可能从未 patch 过)。\n');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
fs.copyFileSync(backupPath, exePath);
|
|
190
|
+
console.log(' ✅ 已恢复原始 Claude Code。\n');
|
|
191
|
+
} catch {
|
|
192
|
+
const tmpPath = exePath + '.restore';
|
|
193
|
+
fs.copyFileSync(backupPath, tmpPath);
|
|
194
|
+
console.log(' ⚠️ Claude Code 正在运行,无法直接恢复。');
|
|
195
|
+
console.log(` 恢复文件已保存到: ${tmpPath}\n`);
|
|
196
|
+
console.log(' 请退出所有 Claude Code 后执行:\n');
|
|
197
|
+
|
|
198
|
+
if (process.platform === 'win32') {
|
|
199
|
+
const dir = path.dirname(exePath);
|
|
200
|
+
const name = path.basename(exePath);
|
|
201
|
+
console.log(` cd "${dir}"`);
|
|
202
|
+
console.log(` Move-Item ${name} ${name}.patched -Force`);
|
|
203
|
+
console.log(` Move-Item ${name}.restore ${name} -Force`);
|
|
204
|
+
} else {
|
|
205
|
+
console.log(` mv "${exePath}" "${exePath}.patched"`);
|
|
206
|
+
console.log(` mv "${tmpPath}" "${exePath}"`);
|
|
207
|
+
}
|
|
208
|
+
console.log();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── 入口 ───────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
const cmd = process.argv[2];
|
|
215
|
+
if (cmd === 'unpatch' || cmd === 'restore') {
|
|
216
|
+
unpatch();
|
|
217
|
+
} else if (cmd === '--help' || cmd === '-h') {
|
|
218
|
+
console.log(`
|
|
219
|
+
cc-channel-patch — 启用 Claude Code Channels 功能
|
|
220
|
+
|
|
221
|
+
用法:
|
|
222
|
+
npx cc-channel-patch 修补 Claude Code
|
|
223
|
+
npx cc-channel-patch unpatch 恢复原始版本
|
|
224
|
+
npx cc-channel-patch --help 显示帮助
|
|
225
|
+
`);
|
|
226
|
+
} else {
|
|
227
|
+
patch();
|
|
228
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-channel-patch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-command patch to enable Claude Code Channels (bypasses tengu_harbor feature flag)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-channel-patch": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": { "node": ">=18" },
|
|
10
|
+
"license": "MIT"
|
|
11
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: configure
|
|
3
|
+
description: Set up the WeChat channel — scan QR code to login. Use when the user wants to connect WeChat or needs to re-login.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
- Bash(node *)
|
|
9
|
+
- Bash(ls *)
|
|
10
|
+
- Bash(rm *)
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# /wechat:configure — WeChat Channel Setup
|
|
14
|
+
|
|
15
|
+
## No args — status and login
|
|
16
|
+
|
|
17
|
+
1. Check `~/.claude/channels/wechat/account.json`
|
|
18
|
+
- If exists: show botId (first 12 chars + "..."), savedAt. Ask if re-login needed.
|
|
19
|
+
- If missing: use the `login` tool to start QR code login.
|
|
20
|
+
|
|
21
|
+
2. After success: tell user "WeChat connected! Send a message from WeChat to test."
|
|
22
|
+
|
|
23
|
+
## `logout` — remove credentials
|
|
24
|
+
|
|
25
|
+
1. Delete `~/.claude/channels/wechat/account.json`
|
|
26
|
+
2. Delete `~/.claude/channels/wechat/sync-buf.txt`
|
|
27
|
+
3. Confirm: "Logged out. Run /wechat:configure to re-login."
|
|
28
|
+
|
|
29
|
+
## `reset` — full reset
|
|
30
|
+
|
|
31
|
+
1. Delete entire `~/.claude/channels/wechat/` directory
|
|
32
|
+
2. Confirm: "Reset complete. Run /wechat:configure to start fresh."
|