@tencent-weixin/openclaw-weixin 1.0.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/CHANGELOG.md +5 -0
- package/CHANGELOG.zh_CN.md +3 -0
- package/LICENSE +21 -0
- package/README.md +271 -0
- package/README.zh_CN.md +269 -0
- package/index.ts +27 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +55 -0
- package/src/api/api.ts +240 -0
- package/src/api/config-cache.ts +79 -0
- package/src/api/session-guard.ts +58 -0
- package/src/api/types.ts +222 -0
- package/src/auth/accounts.ts +321 -0
- package/src/auth/login-qr.ts +331 -0
- package/src/auth/pairing.ts +120 -0
- package/src/cdn/aes-ecb.ts +21 -0
- package/src/cdn/cdn-upload.ts +77 -0
- package/src/cdn/cdn-url.ts +17 -0
- package/src/cdn/pic-decrypt.ts +85 -0
- package/src/cdn/upload.ts +155 -0
- package/src/channel.ts +380 -0
- package/src/config/config-schema.ts +22 -0
- package/src/log-upload.ts +126 -0
- package/src/media/media-download.ts +141 -0
- package/src/media/mime.ts +76 -0
- package/src/media/silk-transcode.ts +74 -0
- package/src/messaging/debug-mode.ts +69 -0
- package/src/messaging/error-notice.ts +31 -0
- package/src/messaging/inbound.ts +171 -0
- package/src/messaging/process-message.ts +381 -0
- package/src/messaging/send-media.ts +72 -0
- package/src/messaging/send.ts +267 -0
- package/src/messaging/slash-commands.ts +110 -0
- package/src/monitor/monitor.ts +221 -0
- package/src/runtime.ts +70 -0
- package/src/storage/state-dir.ts +11 -0
- package/src/storage/sync-buf.ts +81 -0
- package/src/util/logger.ts +143 -0
- package/src/util/random.ts +17 -0
- package/src/util/redact.ts +46 -0
- package/src/vendor.d.ts +25 -0
package/src/api/api.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { loadConfigRouteTag } from "../auth/accounts.js";
|
|
7
|
+
import { logger } from "../util/logger.js";
|
|
8
|
+
import { redactBody, redactUrl } from "../util/redact.js";
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
BaseInfo,
|
|
12
|
+
GetUploadUrlReq,
|
|
13
|
+
GetUploadUrlResp,
|
|
14
|
+
GetUpdatesReq,
|
|
15
|
+
GetUpdatesResp,
|
|
16
|
+
SendMessageReq,
|
|
17
|
+
SendTypingReq,
|
|
18
|
+
GetConfigResp,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
|
|
21
|
+
export type WeixinApiOptions = {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
token?: string;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Long-poll timeout for getUpdates (server may hold the request up to this). */
|
|
26
|
+
longPollTimeoutMs?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// BaseInfo — attached to every outgoing CGI request
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function readChannelVersion(): string {
|
|
34
|
+
try {
|
|
35
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const pkgPath = path.resolve(dir, "..", "..", "package.json");
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { version?: string };
|
|
38
|
+
return pkg.version ?? "unknown";
|
|
39
|
+
} catch {
|
|
40
|
+
return "unknown";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const CHANNEL_VERSION = readChannelVersion();
|
|
45
|
+
|
|
46
|
+
/** Build the `base_info` payload included in every API request. */
|
|
47
|
+
export function buildBaseInfo(): BaseInfo {
|
|
48
|
+
return { channel_version: CHANNEL_VERSION };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Default timeout for long-poll getUpdates requests. */
|
|
52
|
+
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
53
|
+
/** Default timeout for regular API requests (sendMessage, getUploadUrl). */
|
|
54
|
+
const DEFAULT_API_TIMEOUT_MS = 15_000;
|
|
55
|
+
/** Default timeout for lightweight API requests (getConfig, sendTyping). */
|
|
56
|
+
const DEFAULT_CONFIG_TIMEOUT_MS = 10_000;
|
|
57
|
+
|
|
58
|
+
function ensureTrailingSlash(url: string): string {
|
|
59
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** X-WECHAT-UIN header: random uint32 -> decimal string -> base64. */
|
|
63
|
+
function randomWechatUin(): string {
|
|
64
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
65
|
+
return Buffer.from(String(uint32), "utf-8").toString("base64");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildHeaders(opts: { token?: string; body: string }): Record<string, string> {
|
|
69
|
+
const headers: Record<string, string> = {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
AuthorizationType: "ilink_bot_token",
|
|
72
|
+
"Content-Length": String(Buffer.byteLength(opts.body, "utf-8")),
|
|
73
|
+
"X-WECHAT-UIN": randomWechatUin(),
|
|
74
|
+
};
|
|
75
|
+
if (opts.token?.trim()) {
|
|
76
|
+
headers.Authorization = `Bearer ${opts.token.trim()}`;
|
|
77
|
+
}
|
|
78
|
+
const routeTag = loadConfigRouteTag();
|
|
79
|
+
if (routeTag) {
|
|
80
|
+
headers.SKRouteTag = routeTag;
|
|
81
|
+
}
|
|
82
|
+
logger.debug(
|
|
83
|
+
`requestHeaders: ${JSON.stringify({ ...headers, Authorization: headers.Authorization ? "Bearer ***" : undefined })}`,
|
|
84
|
+
);
|
|
85
|
+
return headers;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Common fetch wrapper: POST JSON to a Weixin API endpoint with timeout + abort.
|
|
90
|
+
* Returns the raw response text on success; throws on HTTP error or timeout.
|
|
91
|
+
*/
|
|
92
|
+
async function apiFetch(params: {
|
|
93
|
+
baseUrl: string;
|
|
94
|
+
endpoint: string;
|
|
95
|
+
body: string;
|
|
96
|
+
token?: string;
|
|
97
|
+
timeoutMs: number;
|
|
98
|
+
label: string;
|
|
99
|
+
}): Promise<string> {
|
|
100
|
+
const base = ensureTrailingSlash(params.baseUrl);
|
|
101
|
+
const url = new URL(params.endpoint, base);
|
|
102
|
+
const hdrs = buildHeaders({ token: params.token, body: params.body });
|
|
103
|
+
logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
|
|
104
|
+
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const t = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(url.toString(), {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: hdrs,
|
|
111
|
+
body: params.body,
|
|
112
|
+
signal: controller.signal,
|
|
113
|
+
});
|
|
114
|
+
clearTimeout(t);
|
|
115
|
+
const rawText = await res.text();
|
|
116
|
+
logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
119
|
+
}
|
|
120
|
+
return rawText;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
clearTimeout(t);
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Long-poll getUpdates. Server should hold the request until new messages or timeout.
|
|
129
|
+
*
|
|
130
|
+
* On client-side timeout (no server response within timeoutMs), returns an empty response
|
|
131
|
+
* with ret=0 so the caller can simply retry. This is normal for long-poll.
|
|
132
|
+
*/
|
|
133
|
+
export async function getUpdates(
|
|
134
|
+
params: GetUpdatesReq & {
|
|
135
|
+
baseUrl: string;
|
|
136
|
+
token?: string;
|
|
137
|
+
timeoutMs?: number;
|
|
138
|
+
},
|
|
139
|
+
): Promise<GetUpdatesResp> {
|
|
140
|
+
const timeout = params.timeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
141
|
+
try {
|
|
142
|
+
const rawText = await apiFetch({
|
|
143
|
+
baseUrl: params.baseUrl,
|
|
144
|
+
endpoint: "ilink/bot/getupdates",
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
get_updates_buf: params.get_updates_buf ?? "",
|
|
147
|
+
base_info: buildBaseInfo(),
|
|
148
|
+
}),
|
|
149
|
+
token: params.token,
|
|
150
|
+
timeoutMs: timeout,
|
|
151
|
+
label: "getUpdates",
|
|
152
|
+
});
|
|
153
|
+
const resp: GetUpdatesResp = JSON.parse(rawText);
|
|
154
|
+
return resp;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// Long-poll timeout is normal; return empty response so caller can retry
|
|
157
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
158
|
+
logger.debug(`getUpdates: client-side timeout after ${timeout}ms, returning empty response`);
|
|
159
|
+
return { ret: 0, msgs: [], get_updates_buf: params.get_updates_buf };
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Get a pre-signed CDN upload URL for a file. */
|
|
166
|
+
export async function getUploadUrl(
|
|
167
|
+
params: GetUploadUrlReq & WeixinApiOptions,
|
|
168
|
+
): Promise<GetUploadUrlResp> {
|
|
169
|
+
const rawText = await apiFetch({
|
|
170
|
+
baseUrl: params.baseUrl,
|
|
171
|
+
endpoint: "ilink/bot/getuploadurl",
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
filekey: params.filekey,
|
|
174
|
+
media_type: params.media_type,
|
|
175
|
+
to_user_id: params.to_user_id,
|
|
176
|
+
rawsize: params.rawsize,
|
|
177
|
+
rawfilemd5: params.rawfilemd5,
|
|
178
|
+
filesize: params.filesize,
|
|
179
|
+
thumb_rawsize: params.thumb_rawsize,
|
|
180
|
+
thumb_rawfilemd5: params.thumb_rawfilemd5,
|
|
181
|
+
thumb_filesize: params.thumb_filesize,
|
|
182
|
+
no_need_thumb: params.no_need_thumb,
|
|
183
|
+
aeskey: params.aeskey,
|
|
184
|
+
base_info: buildBaseInfo(),
|
|
185
|
+
}),
|
|
186
|
+
token: params.token,
|
|
187
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
188
|
+
label: "getUploadUrl",
|
|
189
|
+
});
|
|
190
|
+
const resp: GetUploadUrlResp = JSON.parse(rawText);
|
|
191
|
+
return resp;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Send a single message downstream. */
|
|
195
|
+
export async function sendMessage(
|
|
196
|
+
params: WeixinApiOptions & { body: SendMessageReq },
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
await apiFetch({
|
|
199
|
+
baseUrl: params.baseUrl,
|
|
200
|
+
endpoint: "ilink/bot/sendmessage",
|
|
201
|
+
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
|
|
202
|
+
token: params.token,
|
|
203
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
|
|
204
|
+
label: "sendMessage",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Fetch bot config (includes typing_ticket) for a given user. */
|
|
209
|
+
export async function getConfig(
|
|
210
|
+
params: WeixinApiOptions & { ilinkUserId: string; contextToken?: string },
|
|
211
|
+
): Promise<GetConfigResp> {
|
|
212
|
+
const rawText = await apiFetch({
|
|
213
|
+
baseUrl: params.baseUrl,
|
|
214
|
+
endpoint: "ilink/bot/getconfig",
|
|
215
|
+
body: JSON.stringify({
|
|
216
|
+
ilink_user_id: params.ilinkUserId,
|
|
217
|
+
context_token: params.contextToken,
|
|
218
|
+
base_info: buildBaseInfo(),
|
|
219
|
+
}),
|
|
220
|
+
token: params.token,
|
|
221
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
|
|
222
|
+
label: "getConfig",
|
|
223
|
+
});
|
|
224
|
+
const resp: GetConfigResp = JSON.parse(rawText);
|
|
225
|
+
return resp;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Send a typing indicator to a user. */
|
|
229
|
+
export async function sendTyping(
|
|
230
|
+
params: WeixinApiOptions & { body: SendTypingReq },
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
await apiFetch({
|
|
233
|
+
baseUrl: params.baseUrl,
|
|
234
|
+
endpoint: "ilink/bot/sendtyping",
|
|
235
|
+
body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
|
|
236
|
+
token: params.token,
|
|
237
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
|
|
238
|
+
label: "sendTyping",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getConfig } from "./api.js";
|
|
2
|
+
|
|
3
|
+
/** Subset of getConfig fields that we actually need; add new fields here as needed. */
|
|
4
|
+
export interface CachedConfig {
|
|
5
|
+
typingTicket: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const CONFIG_CACHE_INITIAL_RETRY_MS = 2_000;
|
|
10
|
+
const CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
interface ConfigCacheEntry {
|
|
13
|
+
config: CachedConfig;
|
|
14
|
+
everSucceeded: boolean;
|
|
15
|
+
nextFetchAt: number;
|
|
16
|
+
retryDelayMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Per-user getConfig cache with periodic random refresh (within 24h) and
|
|
21
|
+
* exponential-backoff retry (up to 1h) on failure.
|
|
22
|
+
*/
|
|
23
|
+
export class WeixinConfigManager {
|
|
24
|
+
private cache = new Map<string, ConfigCacheEntry>();
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private apiOpts: { baseUrl: string; token?: string },
|
|
28
|
+
private log: (msg: string) => void,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
async getForUser(userId: string, contextToken?: string): Promise<CachedConfig> {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const entry = this.cache.get(userId);
|
|
34
|
+
const shouldFetch = !entry || now >= entry.nextFetchAt;
|
|
35
|
+
|
|
36
|
+
if (shouldFetch) {
|
|
37
|
+
let fetchOk = false;
|
|
38
|
+
try {
|
|
39
|
+
const resp = await getConfig({
|
|
40
|
+
baseUrl: this.apiOpts.baseUrl,
|
|
41
|
+
token: this.apiOpts.token,
|
|
42
|
+
ilinkUserId: userId,
|
|
43
|
+
contextToken,
|
|
44
|
+
});
|
|
45
|
+
if (resp.ret === 0) {
|
|
46
|
+
this.cache.set(userId, {
|
|
47
|
+
config: { typingTicket: resp.typing_ticket ?? "" },
|
|
48
|
+
everSucceeded: true,
|
|
49
|
+
nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
|
|
50
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
51
|
+
});
|
|
52
|
+
this.log(
|
|
53
|
+
`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`,
|
|
54
|
+
);
|
|
55
|
+
fetchOk = true;
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
|
|
59
|
+
}
|
|
60
|
+
if (!fetchOk) {
|
|
61
|
+
const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
|
|
62
|
+
const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
|
|
63
|
+
if (entry) {
|
|
64
|
+
entry.nextFetchAt = now + nextDelay;
|
|
65
|
+
entry.retryDelayMs = nextDelay;
|
|
66
|
+
} else {
|
|
67
|
+
this.cache.set(userId, {
|
|
68
|
+
config: { typingTicket: "" },
|
|
69
|
+
everSucceeded: false,
|
|
70
|
+
nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
71
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this.cache.get(userId)?.config ?? { typingTicket: "" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { logger } from "../util/logger.js";
|
|
2
|
+
|
|
3
|
+
const SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
/** Error code returned by the server when the bot session has expired. */
|
|
6
|
+
export const SESSION_EXPIRED_ERRCODE = -14;
|
|
7
|
+
|
|
8
|
+
const pauseUntilMap = new Map<string, number>();
|
|
9
|
+
|
|
10
|
+
/** Pause all inbound/outbound API calls for `accountId` for one hour. */
|
|
11
|
+
export function pauseSession(accountId: string): void {
|
|
12
|
+
const until = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
13
|
+
pauseUntilMap.set(accountId, until);
|
|
14
|
+
logger.info(
|
|
15
|
+
`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Returns `true` when the bot is still within its one-hour cooldown window. */
|
|
20
|
+
export function isSessionPaused(accountId: string): boolean {
|
|
21
|
+
const until = pauseUntilMap.get(accountId);
|
|
22
|
+
if (until === undefined) return false;
|
|
23
|
+
if (Date.now() >= until) {
|
|
24
|
+
pauseUntilMap.delete(accountId);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Milliseconds remaining until the pause expires (0 when not paused). */
|
|
31
|
+
export function getRemainingPauseMs(accountId: string): number {
|
|
32
|
+
const until = pauseUntilMap.get(accountId);
|
|
33
|
+
if (until === undefined) return 0;
|
|
34
|
+
const remaining = until - Date.now();
|
|
35
|
+
if (remaining <= 0) {
|
|
36
|
+
pauseUntilMap.delete(accountId);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
return remaining;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Throw if the session is currently paused. Call before any API request. */
|
|
43
|
+
export function assertSessionActive(accountId: string): void {
|
|
44
|
+
if (isSessionPaused(accountId)) {
|
|
45
|
+
const remainingMin = Math.ceil(getRemainingPauseMs(accountId) / 60_000);
|
|
46
|
+
throw new Error(
|
|
47
|
+
`session paused for accountId=${accountId}, ${remainingMin} min remaining (errcode ${SESSION_EXPIRED_ERRCODE})`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reset internal state — only for tests.
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export function _resetForTest(): void {
|
|
57
|
+
pauseUntilMap.clear();
|
|
58
|
+
}
|
package/src/api/types.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weixin protocol types (mirrors proto: GetUpdatesReq/Resp, WeixinMessage, SendMessageReq).
|
|
3
|
+
* API uses JSON over HTTP; bytes fields are base64 strings in JSON.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Common request metadata attached to every CGI request. */
|
|
7
|
+
export interface BaseInfo {
|
|
8
|
+
channel_version?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** proto: UploadMediaType */
|
|
12
|
+
export const UploadMediaType = {
|
|
13
|
+
IMAGE: 1,
|
|
14
|
+
VIDEO: 2,
|
|
15
|
+
FILE: 3,
|
|
16
|
+
VOICE: 4,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export interface GetUploadUrlReq {
|
|
20
|
+
filekey?: string;
|
|
21
|
+
/** proto field 2: media_type, see UploadMediaType */
|
|
22
|
+
media_type?: number;
|
|
23
|
+
to_user_id?: string;
|
|
24
|
+
/** 原文件明文大小 */
|
|
25
|
+
rawsize?: number;
|
|
26
|
+
/** 原文件明文 MD5 */
|
|
27
|
+
rawfilemd5?: string;
|
|
28
|
+
/** 原文件密文大小(AES-128-ECB 加密后) */
|
|
29
|
+
filesize?: number;
|
|
30
|
+
/** 缩略图明文大小(IMAGE/VIDEO 时必填) */
|
|
31
|
+
thumb_rawsize?: number;
|
|
32
|
+
/** 缩略图明文 MD5(IMAGE/VIDEO 时必填) */
|
|
33
|
+
thumb_rawfilemd5?: string;
|
|
34
|
+
/** 缩略图密文大小(IMAGE/VIDEO 时必填) */
|
|
35
|
+
thumb_filesize?: number;
|
|
36
|
+
/** 不需要缩略图上传 URL,默认 false */
|
|
37
|
+
no_need_thumb?: boolean;
|
|
38
|
+
/** 加密 key */
|
|
39
|
+
aeskey?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface GetUploadUrlResp {
|
|
43
|
+
/** 原图上传加密参数 */
|
|
44
|
+
upload_param?: string;
|
|
45
|
+
/** 缩略图上传加密参数,无缩略图时为空 */
|
|
46
|
+
thumb_upload_param?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const MessageType = {
|
|
50
|
+
NONE: 0,
|
|
51
|
+
USER: 1,
|
|
52
|
+
BOT: 2,
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export const MessageItemType = {
|
|
56
|
+
NONE: 0,
|
|
57
|
+
TEXT: 1,
|
|
58
|
+
IMAGE: 2,
|
|
59
|
+
VOICE: 3,
|
|
60
|
+
FILE: 4,
|
|
61
|
+
VIDEO: 5,
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
export const MessageState = {
|
|
65
|
+
NEW: 0,
|
|
66
|
+
GENERATING: 1,
|
|
67
|
+
FINISH: 2,
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
export interface TextItem {
|
|
71
|
+
text?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** CDN media reference; aes_key is base64-encoded bytes in JSON. */
|
|
75
|
+
export interface CDNMedia {
|
|
76
|
+
encrypt_query_param?: string;
|
|
77
|
+
aes_key?: string;
|
|
78
|
+
/** 加密类型: 0=只加密fileid, 1=打包缩略图/中图等信息 */
|
|
79
|
+
encrypt_type?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ImageItem {
|
|
83
|
+
/** 原图 CDN 引用 */
|
|
84
|
+
media?: CDNMedia;
|
|
85
|
+
/** 缩略图 CDN 引用 */
|
|
86
|
+
thumb_media?: CDNMedia;
|
|
87
|
+
/** Raw AES-128 key as hex string (16 bytes); preferred over media.aes_key for inbound decryption. */
|
|
88
|
+
aeskey?: string;
|
|
89
|
+
url?: string;
|
|
90
|
+
mid_size?: number;
|
|
91
|
+
thumb_size?: number;
|
|
92
|
+
thumb_height?: number;
|
|
93
|
+
thumb_width?: number;
|
|
94
|
+
hd_size?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface VoiceItem {
|
|
98
|
+
media?: CDNMedia;
|
|
99
|
+
/** 语音编码类型:1=pcm 2=adpcm 3=feature 4=speex 5=amr 6=silk 7=mp3 8=ogg-speex */
|
|
100
|
+
encode_type?: number;
|
|
101
|
+
bits_per_sample?: number;
|
|
102
|
+
/** 采样率 (Hz) */
|
|
103
|
+
sample_rate?: number;
|
|
104
|
+
/** 语音长度 (毫秒) */
|
|
105
|
+
playtime?: number;
|
|
106
|
+
/** 语音转文字内容 */
|
|
107
|
+
text?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface FileItem {
|
|
111
|
+
media?: CDNMedia;
|
|
112
|
+
file_name?: string;
|
|
113
|
+
md5?: string;
|
|
114
|
+
len?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface VideoItem {
|
|
118
|
+
media?: CDNMedia;
|
|
119
|
+
video_size?: number;
|
|
120
|
+
play_length?: number;
|
|
121
|
+
video_md5?: string;
|
|
122
|
+
thumb_media?: CDNMedia;
|
|
123
|
+
thumb_size?: number;
|
|
124
|
+
thumb_height?: number;
|
|
125
|
+
thumb_width?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface RefMessage {
|
|
129
|
+
message_item?: MessageItem;
|
|
130
|
+
title?: string; // 摘要
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface MessageItem {
|
|
134
|
+
type?: number;
|
|
135
|
+
create_time_ms?: number;
|
|
136
|
+
update_time_ms?: number;
|
|
137
|
+
is_completed?: boolean;
|
|
138
|
+
msg_id?: string;
|
|
139
|
+
ref_msg?: RefMessage;
|
|
140
|
+
text_item?: TextItem;
|
|
141
|
+
image_item?: ImageItem;
|
|
142
|
+
voice_item?: VoiceItem;
|
|
143
|
+
file_item?: FileItem;
|
|
144
|
+
video_item?: VideoItem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Unified message (proto: WeixinMessage). Replaces the old split Message + MessageContent + FullMessage. */
|
|
148
|
+
export interface WeixinMessage {
|
|
149
|
+
seq?: number;
|
|
150
|
+
message_id?: number;
|
|
151
|
+
from_user_id?: string;
|
|
152
|
+
to_user_id?: string;
|
|
153
|
+
client_id?: string;
|
|
154
|
+
create_time_ms?: number;
|
|
155
|
+
update_time_ms?: number;
|
|
156
|
+
delete_time_ms?: number;
|
|
157
|
+
session_id?: string;
|
|
158
|
+
group_id?: string;
|
|
159
|
+
message_type?: number;
|
|
160
|
+
message_state?: number;
|
|
161
|
+
item_list?: MessageItem[];
|
|
162
|
+
context_token?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** GetUpdates request: bytes fields are base64 strings in JSON. */
|
|
166
|
+
export interface GetUpdatesReq {
|
|
167
|
+
/** @deprecated compat only, will be removed */
|
|
168
|
+
sync_buf?: string;
|
|
169
|
+
/** Full context buf cached locally; send "" when none (first request or after reset). */
|
|
170
|
+
get_updates_buf?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** GetUpdates response: bytes fields are base64 strings in JSON. */
|
|
174
|
+
export interface GetUpdatesResp {
|
|
175
|
+
ret?: number;
|
|
176
|
+
/** Error code returned by the server (e.g. -14 = session timeout). Present when request fails. */
|
|
177
|
+
errcode?: number;
|
|
178
|
+
errmsg?: string;
|
|
179
|
+
msgs?: WeixinMessage[];
|
|
180
|
+
/** @deprecated compat only */
|
|
181
|
+
sync_buf?: string;
|
|
182
|
+
/** Full context buf to cache locally and send on next request. */
|
|
183
|
+
get_updates_buf?: string;
|
|
184
|
+
/** Server-suggested timeout (ms) for the next getUpdates long-poll. */
|
|
185
|
+
longpolling_timeout_ms?: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** SendMessage request: wraps a single WeixinMessage. */
|
|
189
|
+
export interface SendMessageReq {
|
|
190
|
+
msg?: WeixinMessage;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface SendMessageResp {
|
|
194
|
+
// empty
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Typing status: 1 = typing (default), 2 = cancel typing. */
|
|
198
|
+
export const TypingStatus = {
|
|
199
|
+
TYPING: 1,
|
|
200
|
+
CANCEL: 2,
|
|
201
|
+
} as const;
|
|
202
|
+
|
|
203
|
+
/** SendTyping request: send a typing indicator to a user. */
|
|
204
|
+
export interface SendTypingReq {
|
|
205
|
+
ilink_user_id?: string;
|
|
206
|
+
typing_ticket?: string;
|
|
207
|
+
/** 1=typing (default), 2=cancel typing */
|
|
208
|
+
status?: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface SendTypingResp {
|
|
212
|
+
ret?: number;
|
|
213
|
+
errmsg?: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** GetConfig response: bot config including typing_ticket. */
|
|
217
|
+
export interface GetConfigResp {
|
|
218
|
+
ret?: number;
|
|
219
|
+
errmsg?: string;
|
|
220
|
+
/** Base64-encoded typing ticket for sendTyping. */
|
|
221
|
+
typing_ticket?: string;
|
|
222
|
+
}
|