@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +416 -0
  3. package/dist/adapter-factory.d.ts +30 -0
  4. package/dist/adapter-factory.js +198 -0
  5. package/dist/attachments.d.ts +13 -0
  6. package/dist/attachments.js +74 -0
  7. package/dist/channel.d.ts +112 -0
  8. package/dist/channel.js +60 -0
  9. package/dist/chat-gateway.d.ts +58 -0
  10. package/dist/chat-gateway.js +232 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +259 -0
  13. package/dist/codeshell.d.ts +6 -0
  14. package/dist/codeshell.js +6 -0
  15. package/dist/config.d.ts +125 -0
  16. package/dist/config.js +469 -0
  17. package/dist/delivery-queue.d.ts +93 -0
  18. package/dist/delivery-queue.js +401 -0
  19. package/dist/desktop-control-client.d.ts +51 -0
  20. package/dist/desktop-control-client.js +309 -0
  21. package/dist/dingtalk.d.ts +19 -0
  22. package/dist/dingtalk.js +93 -0
  23. package/dist/discord.d.ts +23 -0
  24. package/dist/discord.js +134 -0
  25. package/dist/gateway.d.ts +34 -0
  26. package/dist/gateway.js +287 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.js +13 -0
  29. package/dist/ingress.d.ts +8 -0
  30. package/dist/ingress.js +61 -0
  31. package/dist/instance-lock.d.ts +24 -0
  32. package/dist/instance-lock.js +100 -0
  33. package/dist/lark.d.ts +20 -0
  34. package/dist/lark.js +184 -0
  35. package/dist/lifecycle.d.ts +7 -0
  36. package/dist/lifecycle.js +16 -0
  37. package/dist/line.d.ts +17 -0
  38. package/dist/line.js +123 -0
  39. package/dist/matrix.d.ts +21 -0
  40. package/dist/matrix.js +208 -0
  41. package/dist/mattermost.d.ts +18 -0
  42. package/dist/mattermost.js +170 -0
  43. package/dist/media.d.ts +39 -0
  44. package/dist/media.js +223 -0
  45. package/dist/notification-progress.d.ts +46 -0
  46. package/dist/notification-progress.js +299 -0
  47. package/dist/notification-relay.d.ts +43 -0
  48. package/dist/notification-relay.js +285 -0
  49. package/dist/platform-canary.d.ts +22 -0
  50. package/dist/platform-canary.js +55 -0
  51. package/dist/protocol.d.ts +102 -0
  52. package/dist/protocol.js +1 -0
  53. package/dist/service-manager.d.ts +47 -0
  54. package/dist/service-manager.js +222 -0
  55. package/dist/slack.d.ts +17 -0
  56. package/dist/slack.js +136 -0
  57. package/dist/teams.d.ts +26 -0
  58. package/dist/teams.js +193 -0
  59. package/dist/telegram.d.ts +33 -0
  60. package/dist/telegram.js +263 -0
  61. package/dist/webhook.d.ts +26 -0
  62. package/dist/webhook.js +91 -0
  63. package/dist/wechat-login.d.ts +21 -0
  64. package/dist/wechat-login.js +97 -0
  65. package/dist/wechat-storage.d.ts +61 -0
  66. package/dist/wechat-storage.js +251 -0
  67. package/dist/wechat.d.ts +129 -0
  68. package/dist/wechat.js +1082 -0
  69. package/dist/wecom.d.ts +16 -0
  70. package/dist/wecom.js +149 -0
  71. package/dist/whatsapp.d.ts +27 -0
  72. package/dist/whatsapp.js +218 -0
  73. 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
+ }
@@ -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>;