@cjhyy/code-shell-chat 0.8.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/LICENSE +21 -0
- package/README.md +416 -0
- package/dist/adapter-factory.d.ts +30 -0
- package/dist/adapter-factory.js +198 -0
- package/dist/attachments.d.ts +13 -0
- package/dist/attachments.js +74 -0
- package/dist/channel.d.ts +112 -0
- package/dist/channel.js +60 -0
- package/dist/chat-gateway.d.ts +58 -0
- package/dist/chat-gateway.js +232 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +259 -0
- package/dist/codeshell.d.ts +6 -0
- package/dist/codeshell.js +6 -0
- package/dist/config.d.ts +125 -0
- package/dist/config.js +469 -0
- package/dist/delivery-queue.d.ts +93 -0
- package/dist/delivery-queue.js +401 -0
- package/dist/desktop-control-client.d.ts +51 -0
- package/dist/desktop-control-client.js +309 -0
- package/dist/dingtalk.d.ts +19 -0
- package/dist/dingtalk.js +93 -0
- package/dist/discord.d.ts +23 -0
- package/dist/discord.js +134 -0
- package/dist/gateway.d.ts +34 -0
- package/dist/gateway.js +287 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/ingress.d.ts +8 -0
- package/dist/ingress.js +61 -0
- package/dist/instance-lock.d.ts +24 -0
- package/dist/instance-lock.js +100 -0
- package/dist/lark.d.ts +20 -0
- package/dist/lark.js +184 -0
- package/dist/lifecycle.d.ts +7 -0
- package/dist/lifecycle.js +16 -0
- package/dist/line.d.ts +17 -0
- package/dist/line.js +123 -0
- package/dist/matrix.d.ts +21 -0
- package/dist/matrix.js +208 -0
- package/dist/mattermost.d.ts +18 -0
- package/dist/mattermost.js +170 -0
- package/dist/media.d.ts +39 -0
- package/dist/media.js +223 -0
- package/dist/notification-progress.d.ts +46 -0
- package/dist/notification-progress.js +299 -0
- package/dist/notification-relay.d.ts +43 -0
- package/dist/notification-relay.js +285 -0
- package/dist/platform-canary.d.ts +22 -0
- package/dist/platform-canary.js +55 -0
- package/dist/protocol.d.ts +102 -0
- package/dist/protocol.js +1 -0
- package/dist/service-manager.d.ts +47 -0
- package/dist/service-manager.js +222 -0
- package/dist/slack.d.ts +17 -0
- package/dist/slack.js +136 -0
- package/dist/teams.d.ts +26 -0
- package/dist/teams.js +193 -0
- package/dist/telegram.d.ts +33 -0
- package/dist/telegram.js +263 -0
- package/dist/webhook.d.ts +26 -0
- package/dist/webhook.js +91 -0
- package/dist/wechat-login.d.ts +21 -0
- package/dist/wechat-login.js +97 -0
- package/dist/wechat-storage.d.ts +61 -0
- package/dist/wechat-storage.js +251 -0
- package/dist/wechat.d.ts +129 -0
- package/dist/wechat.js +1082 -0
- package/dist/wecom.d.ts +16 -0
- package/dist/wecom.js +149 -0
- package/dist/whatsapp.d.ts +27 -0
- package/dist/whatsapp.js +218 -0
- package/package.json +123 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { defaultGatewayConfigPath } from "./config.js";
|
|
4
|
+
import { loginWechatWithQr, } from "./wechat.js";
|
|
5
|
+
import { defaultWechatDataDirectory, FileWechatCredentialStore, wechatCredentialFingerprint, } from "./wechat-storage.js";
|
|
6
|
+
/**
|
|
7
|
+
* Complete the personal-WeChat QR flow and persist both the owner-only token
|
|
8
|
+
* and the matching CodeShell gateway configuration. Shared by the CLI and the
|
|
9
|
+
* Desktop Link page so both entry points produce exactly the same files.
|
|
10
|
+
*/
|
|
11
|
+
export async function loginCodeShellWechat(options = {}) {
|
|
12
|
+
const configPath = resolve(options.configPath ?? defaultGatewayConfigPath());
|
|
13
|
+
const credentialsDir = resolve(options.credentialsDir ?? defaultWechatDataDirectory());
|
|
14
|
+
const store = new FileWechatCredentialStore(credentialsDir);
|
|
15
|
+
const login = options.login ?? loginWechatWithQr;
|
|
16
|
+
const result = await login({
|
|
17
|
+
signal: options.signal,
|
|
18
|
+
localTokens: store.listTokens(),
|
|
19
|
+
onQrCode: options.onQrCode,
|
|
20
|
+
onStatus: options.onStatus,
|
|
21
|
+
requestVerificationCode: options.requestVerificationCode,
|
|
22
|
+
});
|
|
23
|
+
let credentials;
|
|
24
|
+
let credentialsChanged = false;
|
|
25
|
+
if (result.connected && result.credentials) {
|
|
26
|
+
const previous = store.load(result.credentials.accountId);
|
|
27
|
+
credentials = store.save(result.credentials);
|
|
28
|
+
credentialsChanged =
|
|
29
|
+
!previous || previous.token !== credentials.token || previous.baseUrl !== credentials.baseUrl;
|
|
30
|
+
}
|
|
31
|
+
else if (result.alreadyConnected) {
|
|
32
|
+
credentials = store.load();
|
|
33
|
+
if (!credentials) {
|
|
34
|
+
throw new Error("微信端已绑定,但本机没有可用凭据;请在微信中先解除后重新登录");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw new Error("个人微信未完成连接");
|
|
39
|
+
}
|
|
40
|
+
const stateStore = store.stateStore(credentials.accountId, wechatCredentialFingerprint(credentials.token));
|
|
41
|
+
if (credentialsChanged) {
|
|
42
|
+
// Cursor and context tokens belong to the credential session that
|
|
43
|
+
// produced them. Reusing them after a QR rebind can yield prepare failed
|
|
44
|
+
// or poll from an unrelated cursor, so make the new binding start clean.
|
|
45
|
+
// reset() stamps the new credential fingerprint, so an adapter still
|
|
46
|
+
// running with the revoked token refuses to write its stale state back.
|
|
47
|
+
await stateStore.reset({});
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
try {
|
|
51
|
+
await stateStore.load();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Keep ordinary adapter startup fail-closed. An explicit successful
|
|
55
|
+
// login (including an already-connected confirmation) is the recovery
|
|
56
|
+
// boundary where replacing an unsafe state file is intentional.
|
|
57
|
+
await stateStore.reset({});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
updateWechatConfig({
|
|
61
|
+
configPath,
|
|
62
|
+
credentials,
|
|
63
|
+
credentialsDir: options.credentialsDir ? credentialsDir : undefined,
|
|
64
|
+
});
|
|
65
|
+
return { accountId: credentials.accountId, configPath };
|
|
66
|
+
}
|
|
67
|
+
function updateWechatConfig(options) {
|
|
68
|
+
let raw = {};
|
|
69
|
+
if (existsSync(options.configPath)) {
|
|
70
|
+
if (process.platform !== "win32" && (statSync(options.configPath).mode & 0o077) !== 0) {
|
|
71
|
+
throw new Error(`Chat gateway 配置权限必须为 0600:${options.configPath}`);
|
|
72
|
+
}
|
|
73
|
+
const parsed = JSON.parse(readFileSync(options.configPath, "utf8"));
|
|
74
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
75
|
+
throw new Error(`Chat gateway 配置不是 JSON object:${options.configPath}`);
|
|
76
|
+
}
|
|
77
|
+
raw = parsed;
|
|
78
|
+
}
|
|
79
|
+
const existing = raw.wechat && typeof raw.wechat === "object" && !Array.isArray(raw.wechat)
|
|
80
|
+
? raw.wechat
|
|
81
|
+
: {};
|
|
82
|
+
const updated = {
|
|
83
|
+
...raw,
|
|
84
|
+
wechat: {
|
|
85
|
+
...existing,
|
|
86
|
+
enabled: true,
|
|
87
|
+
accountId: options.credentials.accountId,
|
|
88
|
+
...(options.credentialsDir ? { credentialsDir: options.credentialsDir } : {}),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
mkdirSync(dirname(options.configPath), { recursive: true, mode: 0o700 });
|
|
92
|
+
const temporary = `${options.configPath}.${process.pid}.tmp`;
|
|
93
|
+
writeFileSync(temporary, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
|
|
94
|
+
renameSync(temporary, options.configPath);
|
|
95
|
+
if (process.platform !== "win32")
|
|
96
|
+
chmodSync(options.configPath, 0o600);
|
|
97
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface WechatCredentials {
|
|
2
|
+
accountId: string;
|
|
3
|
+
token: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
userId?: string;
|
|
6
|
+
savedAt?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface WechatAdapterState {
|
|
9
|
+
cursor?: string;
|
|
10
|
+
contextTokens?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export interface WechatStateStore {
|
|
13
|
+
load(): Promise<WechatAdapterState | undefined>;
|
|
14
|
+
save(state: WechatAdapterState): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Short non-reversible identity of the bot token. Stored inside the state file
|
|
18
|
+
* so a state written by one credential generation can never be silently
|
|
19
|
+
* overwritten by an adapter still running with a revoked one.
|
|
20
|
+
*/
|
|
21
|
+
export declare function wechatCredentialFingerprint(token: string): string;
|
|
22
|
+
export declare class WechatStateOwnershipError extends Error {
|
|
23
|
+
constructor(filePath: string);
|
|
24
|
+
}
|
|
25
|
+
export declare function defaultWechatDataDirectory(env?: NodeJS.ProcessEnv): string;
|
|
26
|
+
export declare function normalizeWechatAccountId(accountId: string): string;
|
|
27
|
+
export declare class FileWechatCredentialStore {
|
|
28
|
+
readonly directory: string;
|
|
29
|
+
constructor(directory?: string);
|
|
30
|
+
listAccountIds(): string[];
|
|
31
|
+
listTokens(): string[];
|
|
32
|
+
load(accountId?: string): WechatCredentials | undefined;
|
|
33
|
+
save(credentials: WechatCredentials): WechatCredentials;
|
|
34
|
+
stateStore(accountId: string, credentialFingerprint?: string): FileWechatStateStore;
|
|
35
|
+
credentialPath(accountId: string): string;
|
|
36
|
+
statePath(accountId: string): string;
|
|
37
|
+
}
|
|
38
|
+
export declare class FileWechatStateStore implements WechatStateStore {
|
|
39
|
+
readonly filePath: string;
|
|
40
|
+
/** Binds this store to one credential generation; omitted disables the check. */
|
|
41
|
+
readonly credentialFingerprint?: string | undefined;
|
|
42
|
+
constructor(filePath: string,
|
|
43
|
+
/** Binds this store to one credential generation; omitted disables the check. */
|
|
44
|
+
credentialFingerprint?: string | undefined);
|
|
45
|
+
load(): Promise<WechatAdapterState | undefined>;
|
|
46
|
+
save(state: WechatAdapterState): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Claim the state file for this credential unconditionally. Only the QR
|
|
49
|
+
* login flow may use it: an explicit successful login is the one boundary
|
|
50
|
+
* where replacing another generation's state is intentional.
|
|
51
|
+
*/
|
|
52
|
+
reset(state: WechatAdapterState): Promise<void>;
|
|
53
|
+
private write;
|
|
54
|
+
private assertOwnership;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Synchronous readiness probe for hosts that expose proactive destinations in
|
|
58
|
+
* a status/catalog call. It deliberately shares the exact parser used by the
|
|
59
|
+
* adapter instead of letting each host reinterpret the state-file schema.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hasWechatStoredContextToken(filePath: string, target: string): boolean;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
const MAX_WECHAT_STORE_BYTES = 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Short non-reversible identity of the bot token. Stored inside the state file
|
|
8
|
+
* so a state written by one credential generation can never be silently
|
|
9
|
+
* overwritten by an adapter still running with a revoked one.
|
|
10
|
+
*/
|
|
11
|
+
export function wechatCredentialFingerprint(token) {
|
|
12
|
+
return createHash("sha256").update(token).digest("base64url").slice(0, 16);
|
|
13
|
+
}
|
|
14
|
+
export class WechatStateOwnershipError extends Error {
|
|
15
|
+
constructor(filePath) {
|
|
16
|
+
super(`微信状态文件属于另一个登录凭据(可能已重新扫码绑定),请重启 Gateway:${filePath}`);
|
|
17
|
+
this.name = "WechatStateOwnershipError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function defaultWechatDataDirectory(env = process.env) {
|
|
21
|
+
return join(env.HOME ?? homedir(), ".code-shell", "chat", "wechat");
|
|
22
|
+
}
|
|
23
|
+
export function normalizeWechatAccountId(accountId) {
|
|
24
|
+
const normalized = accountId
|
|
25
|
+
.trim()
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
28
|
+
.replace(/^-+|-+$/g, "");
|
|
29
|
+
if (!normalized)
|
|
30
|
+
throw new Error("微信 accountId 无效");
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
export class FileWechatCredentialStore {
|
|
34
|
+
directory;
|
|
35
|
+
constructor(directory = defaultWechatDataDirectory()) {
|
|
36
|
+
this.directory = directory;
|
|
37
|
+
}
|
|
38
|
+
listAccountIds() {
|
|
39
|
+
const value = readJson(join(this.directory, "accounts.json"));
|
|
40
|
+
if (!Array.isArray(value))
|
|
41
|
+
return [];
|
|
42
|
+
return value.filter((item) => typeof item === "string" && Boolean(item.trim()));
|
|
43
|
+
}
|
|
44
|
+
listTokens() {
|
|
45
|
+
return this.listAccountIds()
|
|
46
|
+
.map((accountId) => this.load(accountId)?.token)
|
|
47
|
+
.filter((token) => Boolean(token));
|
|
48
|
+
}
|
|
49
|
+
load(accountId) {
|
|
50
|
+
const selected = accountId ? normalizeWechatAccountId(accountId) : this.listAccountIds().at(-1);
|
|
51
|
+
if (!selected)
|
|
52
|
+
return undefined;
|
|
53
|
+
const value = readJson(this.credentialPath(selected));
|
|
54
|
+
if (!isRecord(value))
|
|
55
|
+
return undefined;
|
|
56
|
+
const token = readString(value.token);
|
|
57
|
+
const storedAccountId = readString(value.accountId) ?? selected;
|
|
58
|
+
const baseUrl = readString(value.baseUrl);
|
|
59
|
+
if (!token || !baseUrl)
|
|
60
|
+
return undefined;
|
|
61
|
+
return {
|
|
62
|
+
accountId: storedAccountId,
|
|
63
|
+
token,
|
|
64
|
+
baseUrl,
|
|
65
|
+
userId: readString(value.userId),
|
|
66
|
+
savedAt: readString(value.savedAt),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
save(credentials) {
|
|
70
|
+
const normalizedId = normalizeWechatAccountId(credentials.accountId);
|
|
71
|
+
const value = {
|
|
72
|
+
...credentials,
|
|
73
|
+
accountId: normalizedId,
|
|
74
|
+
savedAt: new Date().toISOString(),
|
|
75
|
+
};
|
|
76
|
+
writeOwnerOnlyJson(this.credentialPath(normalizedId), value);
|
|
77
|
+
const accountIds = this.listAccountIds().filter((id) => id !== normalizedId);
|
|
78
|
+
writeOwnerOnlyJson(join(this.directory, "accounts.json"), [...accountIds, normalizedId]);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
stateStore(accountId, credentialFingerprint) {
|
|
82
|
+
return new FileWechatStateStore(this.statePath(accountId), credentialFingerprint);
|
|
83
|
+
}
|
|
84
|
+
credentialPath(accountId) {
|
|
85
|
+
return join(this.directory, "accounts", `${normalizeWechatAccountId(accountId)}.json`);
|
|
86
|
+
}
|
|
87
|
+
statePath(accountId) {
|
|
88
|
+
return join(this.directory, "accounts", `${normalizeWechatAccountId(accountId)}.state.json`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export class FileWechatStateStore {
|
|
92
|
+
filePath;
|
|
93
|
+
credentialFingerprint;
|
|
94
|
+
constructor(filePath,
|
|
95
|
+
/** Binds this store to one credential generation; omitted disables the check. */
|
|
96
|
+
credentialFingerprint) {
|
|
97
|
+
this.filePath = filePath;
|
|
98
|
+
this.credentialFingerprint = credentialFingerprint;
|
|
99
|
+
}
|
|
100
|
+
async load() {
|
|
101
|
+
const value = readJson(this.filePath, true);
|
|
102
|
+
this.assertOwnership(value);
|
|
103
|
+
const parsed = parseWechatAdapterState(value);
|
|
104
|
+
if (value !== undefined && !parsed) {
|
|
105
|
+
throw new Error(`微信状态文件格式无效:${this.filePath}`);
|
|
106
|
+
}
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
async save(state) {
|
|
110
|
+
// Cheap ownership probe before every write. A file stamped by another
|
|
111
|
+
// credential (fresh QR rebind) must never be overwritten by a stale
|
|
112
|
+
// adapter, while a legacy file without a fingerprint is adopted by the
|
|
113
|
+
// first stamped writer. A check-then-write race with a concurrent rebind
|
|
114
|
+
// is still possible, but the exposure shrinks from "every future persist"
|
|
115
|
+
// to at most the single write already in flight during the rebind.
|
|
116
|
+
this.assertOwnership(readJson(this.filePath));
|
|
117
|
+
this.write(state);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Claim the state file for this credential unconditionally. Only the QR
|
|
121
|
+
* login flow may use it: an explicit successful login is the one boundary
|
|
122
|
+
* where replacing another generation's state is intentional.
|
|
123
|
+
*/
|
|
124
|
+
async reset(state) {
|
|
125
|
+
this.write(state);
|
|
126
|
+
}
|
|
127
|
+
write(state) {
|
|
128
|
+
writeOwnerOnlyJson(this.filePath, this.credentialFingerprint
|
|
129
|
+
? { ...state, credentialFingerprint: this.credentialFingerprint }
|
|
130
|
+
: state);
|
|
131
|
+
}
|
|
132
|
+
assertOwnership(value) {
|
|
133
|
+
if (!this.credentialFingerprint)
|
|
134
|
+
return;
|
|
135
|
+
const existing = isRecord(value) ? readString(value.credentialFingerprint) : undefined;
|
|
136
|
+
if (existing && existing !== this.credentialFingerprint) {
|
|
137
|
+
throw new WechatStateOwnershipError(this.filePath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Synchronous readiness probe for hosts that expose proactive destinations in
|
|
143
|
+
* a status/catalog call. It deliberately shares the exact parser used by the
|
|
144
|
+
* adapter instead of letting each host reinterpret the state-file schema.
|
|
145
|
+
*/
|
|
146
|
+
export function hasWechatStoredContextToken(filePath, target) {
|
|
147
|
+
if (!target.trim())
|
|
148
|
+
return false;
|
|
149
|
+
const state = parseWechatAdapterState(readJson(filePath));
|
|
150
|
+
const token = state?.contextTokens?.[target];
|
|
151
|
+
return typeof token === "string" && Boolean(token.trim());
|
|
152
|
+
}
|
|
153
|
+
function writeOwnerOnlyJson(filePath, value) {
|
|
154
|
+
const dir = dirname(filePath);
|
|
155
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
156
|
+
const dirInfo = lstatSync(dir);
|
|
157
|
+
if (dirInfo.isSymbolicLink() || !dirInfo.isDirectory()) {
|
|
158
|
+
throw new Error(`微信凭据或状态目录不是普通目录:${dir}`);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
chmodSync(dir, 0o700);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Best effort on platforms without POSIX modes.
|
|
165
|
+
}
|
|
166
|
+
const body = `${JSON.stringify(value, null, 2)}\n`;
|
|
167
|
+
if (Buffer.byteLength(body, "utf8") > MAX_WECHAT_STORE_BYTES) {
|
|
168
|
+
throw new Error("微信凭据或状态文件超过大小限制");
|
|
169
|
+
}
|
|
170
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
171
|
+
try {
|
|
172
|
+
writeFileSync(temporary, body, { mode: 0o600, flag: "wx" });
|
|
173
|
+
renameSync(temporary, filePath);
|
|
174
|
+
try {
|
|
175
|
+
chmodSync(filePath, 0o600);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Best effort on platforms without POSIX modes.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
try {
|
|
183
|
+
rmSync(temporary, { force: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Preserve the original write/rename error.
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function readJson(filePath, strict = false) {
|
|
192
|
+
let handle;
|
|
193
|
+
try {
|
|
194
|
+
const info = lstatSync(filePath);
|
|
195
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
196
|
+
throw new Error(`微信凭据或状态文件不是普通文件:${filePath}`);
|
|
197
|
+
}
|
|
198
|
+
if (info.size > MAX_WECHAT_STORE_BYTES) {
|
|
199
|
+
throw new Error(`微信凭据或状态文件超过大小限制:${filePath}`);
|
|
200
|
+
}
|
|
201
|
+
if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
|
|
202
|
+
throw new Error(`微信凭据或状态文件权限必须为 0600:${filePath}`);
|
|
203
|
+
}
|
|
204
|
+
const noFollow = process.platform === "win32" ? 0 : constants.O_NOFOLLOW;
|
|
205
|
+
handle = openSync(filePath, constants.O_RDONLY | noFollow);
|
|
206
|
+
const openedInfo = fstatSync(handle);
|
|
207
|
+
if (!openedInfo.isFile() || openedInfo.size > MAX_WECHAT_STORE_BYTES) {
|
|
208
|
+
throw new Error(`微信凭据或状态文件无效:${filePath}`);
|
|
209
|
+
}
|
|
210
|
+
if (process.platform !== "win32" && (openedInfo.mode & 0o077) !== 0) {
|
|
211
|
+
throw new Error(`微信凭据或状态文件权限必须为 0600:${filePath}`);
|
|
212
|
+
}
|
|
213
|
+
const raw = readFileSync(handle, "utf8");
|
|
214
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_WECHAT_STORE_BYTES) {
|
|
215
|
+
throw new Error(`微信凭据或状态文件超过大小限制:${filePath}`);
|
|
216
|
+
}
|
|
217
|
+
return JSON.parse(raw);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error.code === "ENOENT")
|
|
221
|
+
return undefined;
|
|
222
|
+
if (strict) {
|
|
223
|
+
const detail = error instanceof Error ? `:${error.message}` : "";
|
|
224
|
+
throw new Error(`无法安全读取微信状态文件:${filePath}${detail}`, {
|
|
225
|
+
cause: error,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
if (handle !== undefined)
|
|
232
|
+
closeSync(handle);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function parseWechatAdapterState(value) {
|
|
236
|
+
if (!isRecord(value))
|
|
237
|
+
return undefined;
|
|
238
|
+
const contextTokens = isRecord(value.contextTokens)
|
|
239
|
+
? Object.fromEntries(Object.entries(value.contextTokens).filter((entry) => Boolean(entry[0].trim()) && typeof entry[1] === "string" && Boolean(entry[1].trim())))
|
|
240
|
+
: undefined;
|
|
241
|
+
return {
|
|
242
|
+
cursor: readString(value.cursor),
|
|
243
|
+
...(contextTokens ? { contextTokens } : {}),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function isRecord(value) {
|
|
247
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
248
|
+
}
|
|
249
|
+
function readString(value) {
|
|
250
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
251
|
+
}
|
package/dist/wechat.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { ChannelAdapter, ChannelMessageHandler, OutgoingMessage } from "./channel.js";
|
|
2
|
+
import { type WechatCredentials, type WechatStateStore } from "./wechat-storage.js";
|
|
3
|
+
export * from "./wechat-storage.js";
|
|
4
|
+
export declare const WECHAT_DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
5
|
+
export declare const WECHAT_PROTOCOL_VERSION = "2.4.6";
|
|
6
|
+
export declare const WECHAT_DEFAULT_BOT_AGENT = "CodeShellChat/0.7.1";
|
|
7
|
+
export interface WechatAdapterConfig {
|
|
8
|
+
accountId: string;
|
|
9
|
+
token: string;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
botAgent?: string;
|
|
12
|
+
protocolVersion?: string;
|
|
13
|
+
/** Only enable for an explicitly trusted self-hosted compatible backend. */
|
|
14
|
+
allowUnsafeBaseUrl?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Exact extra hostnames trusted for inbound media downloads, extending the
|
|
17
|
+
* built-in WeChat CDN allowlist (e.g. a regional CDN variant). Downloads
|
|
18
|
+
* stay HTTPS-only; this never affects the API baseUrl trust decision.
|
|
19
|
+
*/
|
|
20
|
+
extraCdnDownloadHosts?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export interface WechatAdapterOptions {
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
25
|
+
log?: (message: string) => void;
|
|
26
|
+
stateStore?: WechatStateStore;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
maxMessageAgeMs?: number;
|
|
29
|
+
/** Retries only explicit platform rate-limit responses, reusing client_id. */
|
|
30
|
+
rateLimitRetries?: number;
|
|
31
|
+
rateLimitRetryBaseMs?: number;
|
|
32
|
+
/** Account-wide cooldown after explicit rate-limit retries are exhausted. */
|
|
33
|
+
rateLimitCooldownMs?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface WechatDirectSendResult {
|
|
36
|
+
channel: "wechat";
|
|
37
|
+
target: string;
|
|
38
|
+
status: "accepted";
|
|
39
|
+
/** iLink acknowledges API acceptance, not display on the recipient device. */
|
|
40
|
+
terminalDeliveryConfirmed: false;
|
|
41
|
+
/** True when the helper reused the adapter already long-polling this account. */
|
|
42
|
+
viaLiveAdapter: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Personal WeChat ClawBot adapter using Tencent's documented iLink Bot HTTP protocol. */
|
|
45
|
+
export declare class WechatAdapter implements ChannelAdapter {
|
|
46
|
+
private readonly config;
|
|
47
|
+
readonly channel = "wechat";
|
|
48
|
+
readonly capabilities: import("./channel.js").ChannelCapabilities;
|
|
49
|
+
readonly supportsOutgoingAttachments = true;
|
|
50
|
+
private readonly fetchFn;
|
|
51
|
+
private readonly sleepFn;
|
|
52
|
+
private readonly log;
|
|
53
|
+
private readonly stateStore?;
|
|
54
|
+
private readonly now;
|
|
55
|
+
private readonly maxMessageAgeMs;
|
|
56
|
+
private readonly rateLimitRetries;
|
|
57
|
+
private readonly rateLimitRetryBaseMs;
|
|
58
|
+
private readonly rateLimitCooldownMs;
|
|
59
|
+
private readonly accountKey;
|
|
60
|
+
private readonly credentialKey;
|
|
61
|
+
private readonly extraCdnDownloadHosts;
|
|
62
|
+
private readonly baseUrl;
|
|
63
|
+
private readonly protocolVersion;
|
|
64
|
+
private readonly botAgent;
|
|
65
|
+
private state;
|
|
66
|
+
private stateReady?;
|
|
67
|
+
private stateWrite;
|
|
68
|
+
/** Set once the state file is owned by a newer QR binding; never cleared. */
|
|
69
|
+
private staleCredential;
|
|
70
|
+
/** Preserve visible ordering and keep context/media state mutations single-writer. */
|
|
71
|
+
private outboundQueue;
|
|
72
|
+
private readonly seenMessageIds;
|
|
73
|
+
private readonly delivery;
|
|
74
|
+
constructor(config: WechatAdapterConfig, options?: WechatAdapterOptions);
|
|
75
|
+
run(handler: ChannelMessageHandler, signal: AbortSignal): Promise<void>;
|
|
76
|
+
send(target: string, message: OutgoingMessage): Promise<void>;
|
|
77
|
+
private sendInOrder;
|
|
78
|
+
private sendItem;
|
|
79
|
+
private postWechatMessage;
|
|
80
|
+
private postWechatMessageWithRateLimitRetry;
|
|
81
|
+
private assertRateLimitCircuitClosed;
|
|
82
|
+
private clearContextToken;
|
|
83
|
+
private uploadImage;
|
|
84
|
+
private uploadFile;
|
|
85
|
+
private uploadVideo;
|
|
86
|
+
private uploadMedia;
|
|
87
|
+
private uploadCiphertext;
|
|
88
|
+
private normalizeInbound;
|
|
89
|
+
private toAttachment;
|
|
90
|
+
private downloadMedia;
|
|
91
|
+
private fetchWechatCdn;
|
|
92
|
+
private isDuplicate;
|
|
93
|
+
private forgetDuplicate;
|
|
94
|
+
private commitCursor;
|
|
95
|
+
private ensureState;
|
|
96
|
+
private persistState;
|
|
97
|
+
private baseInfo;
|
|
98
|
+
private post;
|
|
99
|
+
private notifyLifecycle;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Direct WeChat delivery for schedulers and notification workers. It reuses a
|
|
103
|
+
* live long-poll adapter when available and otherwise creates a one-shot
|
|
104
|
+
* adapter. Both routes share persisted context lookup, stale-context eviction,
|
|
105
|
+
* best-effort tokenless fallback, media upload, and response validation.
|
|
106
|
+
*/
|
|
107
|
+
export declare function sendWechatDirect(config: WechatAdapterConfig, target: string, message: OutgoingMessage, options?: WechatAdapterOptions): Promise<WechatDirectSendResult>;
|
|
108
|
+
type QrStatus = "wait" | "scaned" | "confirmed" | "expired" | "scaned_but_redirect" | "need_verifycode" | "verify_code_blocked" | "binded_redirect";
|
|
109
|
+
export interface WechatQrLoginOptions {
|
|
110
|
+
fetch?: typeof fetch;
|
|
111
|
+
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
timeoutMs?: number;
|
|
114
|
+
apiBaseUrl?: string;
|
|
115
|
+
botType?: string;
|
|
116
|
+
protocolVersion?: string;
|
|
117
|
+
localTokens?: string[];
|
|
118
|
+
allowUnsafeBaseUrl?: boolean;
|
|
119
|
+
onQrCode?: (url: string) => void | Promise<void>;
|
|
120
|
+
onStatus?: (status: QrStatus) => void;
|
|
121
|
+
requestVerificationCode?: () => Promise<string>;
|
|
122
|
+
}
|
|
123
|
+
export interface WechatQrLoginResult {
|
|
124
|
+
connected: boolean;
|
|
125
|
+
alreadyConnected?: boolean;
|
|
126
|
+
credentials?: WechatCredentials;
|
|
127
|
+
}
|
|
128
|
+
/** QR login flow compatible with Tencent's official ClawBot plugin protocol. */
|
|
129
|
+
export declare function loginWechatWithQr(options?: WechatQrLoginOptions): Promise<WechatQrLoginResult>;
|