@zhin.js/adapter-weixin-ilink 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/README.md +96 -0
- package/client/LoginAssistPanel.tsx +200 -0
- package/client/WeixinIlinkManagement.tsx +139 -0
- package/client/index.tsx +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/api.ts +30 -0
- package/dist/index.js +29 -0
- package/lib/adapter.d.ts +11 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/adapter.js +14 -0
- package/lib/adapter.js.map +1 -0
- package/lib/aes-ecb.d.ts +7 -0
- package/lib/aes-ecb.d.ts.map +1 -0
- package/lib/aes-ecb.js +19 -0
- package/lib/aes-ecb.js.map +1 -0
- package/lib/bot.d.ts +37 -0
- package/lib/bot.d.ts.map +1 -0
- package/lib/bot.js +383 -0
- package/lib/bot.js.map +1 -0
- package/lib/cdn-upload.d.ts +18 -0
- package/lib/cdn-upload.d.ts.map +1 -0
- package/lib/cdn-upload.js +73 -0
- package/lib/cdn-upload.js.map +1 -0
- package/lib/cdn-url.d.ts +14 -0
- package/lib/cdn-url.d.ts.map +1 -0
- package/lib/cdn-url.js +14 -0
- package/lib/cdn-url.js.map +1 -0
- package/lib/context-store.d.ts +2 -0
- package/lib/context-store.d.ts.map +1 -0
- package/lib/context-store.js +2 -0
- package/lib/context-store.js.map +1 -0
- package/lib/credentials.d.ts +15 -0
- package/lib/credentials.d.ts.map +1 -0
- package/lib/credentials.js +49 -0
- package/lib/credentials.js.map +1 -0
- package/lib/ilink-api.d.ts +81 -0
- package/lib/ilink-api.d.ts.map +1 -0
- package/lib/ilink-api.js +273 -0
- package/lib/ilink-api.js.map +1 -0
- package/lib/ilink-config-cache.d.ts +19 -0
- package/lib/ilink-config-cache.d.ts.map +1 -0
- package/lib/ilink-config-cache.js +64 -0
- package/lib/ilink-config-cache.js.map +1 -0
- package/lib/ilink-logger.d.ts +10 -0
- package/lib/ilink-logger.d.ts.map +1 -0
- package/lib/ilink-logger.js +27 -0
- package/lib/ilink-logger.js.map +1 -0
- package/lib/ilink-meta.d.ts +13 -0
- package/lib/ilink-meta.d.ts.map +1 -0
- package/lib/ilink-meta.js +57 -0
- package/lib/ilink-meta.js.map +1 -0
- package/lib/ilink-random.d.ts +11 -0
- package/lib/ilink-random.d.ts.map +1 -0
- package/lib/ilink-random.js +16 -0
- package/lib/ilink-random.js.map +1 -0
- package/lib/ilink-redact.d.ts +21 -0
- package/lib/ilink-redact.d.ts.map +1 -0
- package/lib/ilink-redact.js +54 -0
- package/lib/ilink-redact.js.map +1 -0
- package/lib/ilink-session-guard.d.ts +16 -0
- package/lib/ilink-session-guard.d.ts.map +1 -0
- package/lib/ilink-session-guard.js +49 -0
- package/lib/ilink-session-guard.js.map +1 -0
- package/lib/ilink-types.d.ts +233 -0
- package/lib/ilink-types.d.ts.map +1 -0
- package/lib/ilink-types.js +35 -0
- package/lib/ilink-types.js.map +1 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +38 -0
- package/lib/index.js.map +1 -0
- package/lib/login-assist-routes.d.ts +7 -0
- package/lib/login-assist-routes.d.ts.map +1 -0
- package/lib/login-assist-routes.js +29 -0
- package/lib/login-assist-routes.js.map +1 -0
- package/lib/login.d.ts +6 -0
- package/lib/login.d.ts.map +1 -0
- package/lib/login.js +121 -0
- package/lib/login.js.map +1 -0
- package/lib/media-download.d.ts +19 -0
- package/lib/media-download.d.ts.map +1 -0
- package/lib/media-download.js +95 -0
- package/lib/media-download.js.map +1 -0
- package/lib/mime.d.ts +9 -0
- package/lib/mime.d.ts.map +1 -0
- package/lib/mime.js +99 -0
- package/lib/mime.js.map +1 -0
- package/lib/outbound-media.d.ts +5 -0
- package/lib/outbound-media.d.ts.map +1 -0
- package/lib/outbound-media.js +122 -0
- package/lib/outbound-media.js.map +1 -0
- package/lib/pic-decrypt.d.ts +10 -0
- package/lib/pic-decrypt.d.ts.map +1 -0
- package/lib/pic-decrypt.js +89 -0
- package/lib/pic-decrypt.js.map +1 -0
- package/lib/routes.d.ts +8 -0
- package/lib/routes.d.ts.map +1 -0
- package/lib/routes.js +29 -0
- package/lib/routes.js.map +1 -0
- package/lib/silk-transcode.d.ts +9 -0
- package/lib/silk-transcode.d.ts.map +1 -0
- package/lib/silk-transcode.js +64 -0
- package/lib/silk-transcode.js.map +1 -0
- package/lib/types.d.ts +26 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/typing-indicator.d.ts +32 -0
- package/lib/typing-indicator.d.ts.map +1 -0
- package/lib/typing-indicator.js +95 -0
- package/lib/typing-indicator.js.map +1 -0
- package/lib/upload.d.ts +43 -0
- package/lib/upload.d.ts.map +1 -0
- package/lib/upload.js +106 -0
- package/lib/upload.js.map +1 -0
- package/lib/weixin-inbound.d.ts +71 -0
- package/lib/weixin-inbound.d.ts.map +1 -0
- package/lib/weixin-inbound.js +201 -0
- package/lib/weixin-inbound.js.map +1 -0
- package/lib/weixin-send-media.d.ts +22 -0
- package/lib/weixin-send-media.d.ts.map +1 -0
- package/lib/weixin-send-media.js +54 -0
- package/lib/weixin-send-media.js.map +1 -0
- package/lib/weixin-send.d.ts +65 -0
- package/lib/weixin-send.d.ts.map +1 -0
- package/lib/weixin-send.js +181 -0
- package/lib/weixin-send.js.map +1 -0
- package/package.json +91 -0
- package/src/adapter.ts +16 -0
- package/src/aes-ecb.ts +21 -0
- package/src/bot.ts +483 -0
- package/src/cdn-upload.test.ts +177 -0
- package/src/cdn-upload.ts +87 -0
- package/src/cdn-url.ts +20 -0
- package/src/context-store.ts +8 -0
- package/src/credentials.ts +64 -0
- package/src/ilink-api.ts +349 -0
- package/src/ilink-config-cache.ts +79 -0
- package/src/ilink-logger.ts +36 -0
- package/src/ilink-meta.ts +60 -0
- package/src/ilink-random.ts +17 -0
- package/src/ilink-redact.ts +54 -0
- package/src/ilink-session-guard.ts +58 -0
- package/src/ilink-types.ts +261 -0
- package/src/index.ts +60 -0
- package/src/login-assist-routes.ts +36 -0
- package/src/login.ts +166 -0
- package/src/media-download.ts +149 -0
- package/src/mime.test.ts +74 -0
- package/src/mime.ts +105 -0
- package/src/outbound-media.ts +141 -0
- package/src/pic-decrypt.ts +101 -0
- package/src/routes.ts +39 -0
- package/src/silk-transcode.test.ts +58 -0
- package/src/silk-transcode.ts +74 -0
- package/src/silk-wasm.d.ts +6 -0
- package/src/types.ts +26 -0
- package/src/typing-indicator.ts +139 -0
- package/src/upload.test.ts +305 -0
- package/src/upload.ts +157 -0
- package/src/weixin-inbound.ts +257 -0
- package/src/weixin-send-media.ts +72 -0
- package/src/weixin-send.ts +237 -0
package/lib/routes.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weixin-ilink HTTP 路由
|
|
3
|
+
*/
|
|
4
|
+
import type { Router } from "@zhin.js/host-router";
|
|
5
|
+
import type { Plugin } from "zhin.js";
|
|
6
|
+
import type { WeixinIlinkAdapter } from "./adapter.js";
|
|
7
|
+
export declare function registerRoutes(router: Router, adapter: WeixinIlinkAdapter, root: Plugin): void;
|
|
8
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGvD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,MAAM,GACX,IAAI,CA0BN"}
|
package/lib/routes.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { registerLoginAssistRoutes } from "./login-assist-routes.js";
|
|
2
|
+
export function registerRoutes(router, adapter, root) {
|
|
3
|
+
registerLoginAssistRoutes(router, root);
|
|
4
|
+
router.get("/api/weixin-ilink/bots", async (ctx) => {
|
|
5
|
+
try {
|
|
6
|
+
const bots = Array.from(adapter.bots.values());
|
|
7
|
+
const data = bots.map((bot) => ({
|
|
8
|
+
name: bot.$config.name,
|
|
9
|
+
connected: bot.$connected,
|
|
10
|
+
status: bot.$connected ? "online" : "offline",
|
|
11
|
+
hasCredentials: Boolean(bot.hasCredentials),
|
|
12
|
+
}));
|
|
13
|
+
ctx.body = {
|
|
14
|
+
success: true,
|
|
15
|
+
data,
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
ctx.status = 500;
|
|
21
|
+
ctx.body = {
|
|
22
|
+
success: false,
|
|
23
|
+
error: "WEIXIN_ILINK_API_ERROR",
|
|
24
|
+
message: error.message,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAErE,MAAM,UAAU,cAAc,CAC5B,MAAc,EACd,OAA2B,EAC3B,IAAY;IAEZ,yBAAyB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAExC,MAAM,CAAC,GAAG,CAAC,wBAAwB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACjD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC9B,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI;gBACtB,SAAS,EAAE,GAAG,CAAC,UAAU;gBACzB,MAAM,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;gBAC7C,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;aAC5C,CAAC,CAAC,CAAC;YACJ,GAAG,CAAC,IAAI,GAAG;gBACT,OAAO,EAAE,IAAI;gBACb,IAAI;gBACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;YACjB,GAAG,CAAC,IAAI,GAAG;gBACT,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,wBAAwB;gBAC/B,OAAO,EAAG,KAAe,CAAC,OAAO;aAClC,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Try to transcode a SILK audio buffer to WAV using silk-wasm.
|
|
3
|
+
* silk-wasm's decode() returns { data: Uint8Array (pcm_s16le), duration: number }.
|
|
4
|
+
*
|
|
5
|
+
* Returns a WAV Buffer on success, or null if silk-wasm is unavailable or decoding fails.
|
|
6
|
+
* Callers should fall back to passing the raw SILK file when null is returned.
|
|
7
|
+
*/
|
|
8
|
+
export declare function silkToWav(silkBuf: Buffer): Promise<Buffer | null>;
|
|
9
|
+
//# sourceMappingURL=silk-transcode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silk-transcode.d.ts","sourceRoot":"","sources":["../src/silk-transcode.ts"],"names":[],"mappings":"AAiDA;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBvE"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { logger } from "./ilink-logger.js";
|
|
2
|
+
/** Default sample rate for Weixin voice messages. */
|
|
3
|
+
const SILK_SAMPLE_RATE = 24_000;
|
|
4
|
+
/**
|
|
5
|
+
* Wrap raw pcm_s16le bytes in a WAV container.
|
|
6
|
+
* Mono channel, 16-bit signed little-endian.
|
|
7
|
+
*/
|
|
8
|
+
function pcmBytesToWav(pcm, sampleRate) {
|
|
9
|
+
const pcmBytes = pcm.byteLength;
|
|
10
|
+
const totalSize = 44 + pcmBytes;
|
|
11
|
+
const buf = Buffer.allocUnsafe(totalSize);
|
|
12
|
+
let offset = 0;
|
|
13
|
+
buf.write("RIFF", offset);
|
|
14
|
+
offset += 4;
|
|
15
|
+
buf.writeUInt32LE(totalSize - 8, offset);
|
|
16
|
+
offset += 4;
|
|
17
|
+
buf.write("WAVE", offset);
|
|
18
|
+
offset += 4;
|
|
19
|
+
buf.write("fmt ", offset);
|
|
20
|
+
offset += 4;
|
|
21
|
+
buf.writeUInt32LE(16, offset);
|
|
22
|
+
offset += 4; // fmt chunk size
|
|
23
|
+
buf.writeUInt16LE(1, offset);
|
|
24
|
+
offset += 2; // PCM format
|
|
25
|
+
buf.writeUInt16LE(1, offset);
|
|
26
|
+
offset += 2; // mono
|
|
27
|
+
buf.writeUInt32LE(sampleRate, offset);
|
|
28
|
+
offset += 4;
|
|
29
|
+
buf.writeUInt32LE(sampleRate * 2, offset);
|
|
30
|
+
offset += 4; // byte rate (mono 16-bit)
|
|
31
|
+
buf.writeUInt16LE(2, offset);
|
|
32
|
+
offset += 2; // block align
|
|
33
|
+
buf.writeUInt16LE(16, offset);
|
|
34
|
+
offset += 2; // bits per sample
|
|
35
|
+
buf.write("data", offset);
|
|
36
|
+
offset += 4;
|
|
37
|
+
buf.writeUInt32LE(pcmBytes, offset);
|
|
38
|
+
offset += 4;
|
|
39
|
+
Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength).copy(buf, offset);
|
|
40
|
+
return buf;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Try to transcode a SILK audio buffer to WAV using silk-wasm.
|
|
44
|
+
* silk-wasm's decode() returns { data: Uint8Array (pcm_s16le), duration: number }.
|
|
45
|
+
*
|
|
46
|
+
* Returns a WAV Buffer on success, or null if silk-wasm is unavailable or decoding fails.
|
|
47
|
+
* Callers should fall back to passing the raw SILK file when null is returned.
|
|
48
|
+
*/
|
|
49
|
+
export async function silkToWav(silkBuf) {
|
|
50
|
+
try {
|
|
51
|
+
const { decode } = await import("silk-wasm");
|
|
52
|
+
logger.debug(`silkToWav: decoding ${silkBuf.length} bytes of SILK`);
|
|
53
|
+
const result = await decode(silkBuf, SILK_SAMPLE_RATE);
|
|
54
|
+
logger.debug(`silkToWav: decoded duration=${result.duration}ms pcmBytes=${result.data.byteLength}`);
|
|
55
|
+
const wav = pcmBytesToWav(result.data, SILK_SAMPLE_RATE);
|
|
56
|
+
logger.debug(`silkToWav: WAV size=${wav.length}`);
|
|
57
|
+
return wav;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
logger.warn(`silkToWav: transcode failed, will use raw silk err=${String(err)}`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=silk-transcode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silk-transcode.js","sourceRoot":"","sources":["../src/silk-transcode.ts"],"names":[],"mappings":"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"}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface WeixinIlinkBotConfig {
|
|
2
|
+
context: "weixin-ilink";
|
|
3
|
+
name: string;
|
|
4
|
+
/** 观测用 bot_agent,默认 Zhin.js/<version> */
|
|
5
|
+
botAgent?: string;
|
|
6
|
+
/** iLink API 根地址,默认 https://ilinkai.weixin.qq.com */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** CDN 根地址 */
|
|
9
|
+
cdnBaseUrl?: string;
|
|
10
|
+
/** 长轮询超时(ms),默认 35000 */
|
|
11
|
+
longPollTimeoutMs?: number;
|
|
12
|
+
/** 直接配置 token(优先于侧车凭证文件) */
|
|
13
|
+
botToken?: string;
|
|
14
|
+
typingIndicator?: {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
/** sendTyping 保活间隔(ms) */
|
|
17
|
+
keepaliveIntervalMs?: number;
|
|
18
|
+
privateConfig?: {
|
|
19
|
+
type?: "typing";
|
|
20
|
+
autoRemove?: boolean;
|
|
21
|
+
platformConfig?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export type { WeixinMessage, MessageItem } from "./ilink-types.js";
|
|
26
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4BAA4B;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,0BAA0B;QAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,aAAa,CAAC,EAAE;YACd,IAAI,CAAC,EAAE,QAAQ,CAAC;YAChB,UAAU,CAAC,EAAE,OAAO,CAAC;YACrB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;SAC1C,CAAC;KACH,CAAC;CACH;AAED,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
|
package/lib/types.js
ADDED
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信 iLink Typing Indicator(iLink sendTyping API → 微信「正在输入」)
|
|
3
|
+
*/
|
|
4
|
+
import type { WeixinIlinkBot } from "./bot.js";
|
|
5
|
+
import { type TypingIndicator, type TypingIndicatorConfig } from "@zhin.js/agent";
|
|
6
|
+
export interface WeixinIlinkTypingIndicatorConfig {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** sendTyping 保活间隔(ms),长任务期间重复发送;默认 5000 */
|
|
9
|
+
keepaliveIntervalMs?: number;
|
|
10
|
+
privateConfig?: Partial<TypingIndicatorConfig>;
|
|
11
|
+
}
|
|
12
|
+
export declare class WeixinIlinkTypingIndicatorManager {
|
|
13
|
+
private manager;
|
|
14
|
+
private config;
|
|
15
|
+
private bot;
|
|
16
|
+
constructor(bot: WeixinIlinkBot, config?: WeixinIlinkTypingIndicatorConfig);
|
|
17
|
+
private registerAdapter;
|
|
18
|
+
start(options: {
|
|
19
|
+
messageId?: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
userId?: string;
|
|
22
|
+
groupId?: string;
|
|
23
|
+
sceneType: "private" | "group";
|
|
24
|
+
}): Promise<TypingIndicator>;
|
|
25
|
+
stop(options: {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
userId?: string;
|
|
28
|
+
groupId?: string;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function enableTypingIndicator(bot: WeixinIlinkBot, config?: WeixinIlinkTypingIndicatorConfig): WeixinIlinkTypingIndicatorManager;
|
|
32
|
+
//# sourceMappingURL=typing-indicator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typing-indicator.d.ts","sourceRoot":"","sources":["../src/typing-indicator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAE3B,MAAM,gBAAgB,CAAC;AAGxB,MAAM,WAAW,gCAAgC;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAChD;AAYD,qBAAa,iCAAiC;IAC5C,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,GAAG,CAAiB;gBAEhB,GAAG,EAAE,cAAc,EAAE,MAAM,GAAE,gCAAqC;IAsB9E,OAAO,CAAC,eAAe;IAiBjB,KAAK,CAAC,OAAO,EAAE;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC;KAChC,GAAG,OAAO,CAAC,eAAe,CAAC;IAuBtB,IAAI,CAAC,OAAO,EAAE;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;CAWlB;AAaD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,cAAc,EACnB,MAAM,CAAC,EAAE,gCAAgC,GACxC,iCAAiC,CAInC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { NativeTypingIndicatorAdapter, TypingIndicatorManager, } from "@zhin.js/agent";
|
|
2
|
+
import { TypingStatus } from "./ilink-types.js";
|
|
3
|
+
const DEFAULT_CONFIG = {
|
|
4
|
+
enabled: true,
|
|
5
|
+
keepaliveIntervalMs: 5_000,
|
|
6
|
+
privateConfig: {
|
|
7
|
+
type: "typing",
|
|
8
|
+
autoRemove: true,
|
|
9
|
+
platformConfig: { keepaliveIntervalMs: 5_000 },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
export class WeixinIlinkTypingIndicatorManager {
|
|
13
|
+
manager;
|
|
14
|
+
config;
|
|
15
|
+
bot;
|
|
16
|
+
constructor(bot, config = {}) {
|
|
17
|
+
this.bot = bot;
|
|
18
|
+
this.config = {
|
|
19
|
+
...DEFAULT_CONFIG,
|
|
20
|
+
...config,
|
|
21
|
+
privateConfig: {
|
|
22
|
+
...DEFAULT_CONFIG.privateConfig,
|
|
23
|
+
...config.privateConfig,
|
|
24
|
+
platformConfig: {
|
|
25
|
+
keepaliveIntervalMs: config.keepaliveIntervalMs ?? DEFAULT_CONFIG.keepaliveIntervalMs,
|
|
26
|
+
...config.privateConfig?.platformConfig,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
this.manager = new TypingIndicatorManager({
|
|
31
|
+
type: "typing",
|
|
32
|
+
autoRemove: true,
|
|
33
|
+
});
|
|
34
|
+
this.registerAdapter();
|
|
35
|
+
}
|
|
36
|
+
registerAdapter() {
|
|
37
|
+
const adapter = new NativeTypingIndicatorAdapter("weixin-ilink", async (options) => {
|
|
38
|
+
const peerId = resolvePeerId(options);
|
|
39
|
+
if (!peerId)
|
|
40
|
+
return;
|
|
41
|
+
await this.bot.sendTypingToUser(peerId, TypingStatus.TYPING);
|
|
42
|
+
}, async (options) => {
|
|
43
|
+
const peerId = resolvePeerId(options);
|
|
44
|
+
if (!peerId)
|
|
45
|
+
return;
|
|
46
|
+
await this.bot.sendTypingToUser(peerId, TypingStatus.CANCEL);
|
|
47
|
+
});
|
|
48
|
+
this.manager.registerAdapter(adapter);
|
|
49
|
+
}
|
|
50
|
+
async start(options) {
|
|
51
|
+
if (!this.config.enabled) {
|
|
52
|
+
return {
|
|
53
|
+
start: async () => { },
|
|
54
|
+
stop: async () => { },
|
|
55
|
+
isActive: () => false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const peerId = resolvePeerId(options);
|
|
59
|
+
const typingOptions = {
|
|
60
|
+
messageId: options.messageId,
|
|
61
|
+
sessionId: options.sessionId,
|
|
62
|
+
userId: peerId,
|
|
63
|
+
groupId: options.groupId,
|
|
64
|
+
platform: "weixin-ilink",
|
|
65
|
+
botId: this.bot.$id,
|
|
66
|
+
sceneType: "private",
|
|
67
|
+
};
|
|
68
|
+
return this.manager.start(typingOptions, this.config.privateConfig);
|
|
69
|
+
}
|
|
70
|
+
async stop(options) {
|
|
71
|
+
const peerId = resolvePeerId(options);
|
|
72
|
+
await this.manager.stop({
|
|
73
|
+
sessionId: options.sessionId,
|
|
74
|
+
userId: peerId,
|
|
75
|
+
groupId: options.groupId,
|
|
76
|
+
platform: "weixin-ilink",
|
|
77
|
+
botId: this.bot.$id,
|
|
78
|
+
sceneType: "private",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function resolvePeerId(options) {
|
|
83
|
+
const raw = options.userId?.trim()
|
|
84
|
+
|| (options.sessionId ? options.sessionId.split(":").filter(Boolean).at(-1) : undefined);
|
|
85
|
+
if (!raw)
|
|
86
|
+
return undefined;
|
|
87
|
+
const hash = raw.indexOf("#");
|
|
88
|
+
return hash >= 0 ? raw.slice(0, hash) : raw;
|
|
89
|
+
}
|
|
90
|
+
export function enableTypingIndicator(bot, config) {
|
|
91
|
+
const manager = new WeixinIlinkTypingIndicatorManager(bot, config);
|
|
92
|
+
bot.$typingIndicator = manager;
|
|
93
|
+
return manager;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=typing-indicator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typing-indicator.js","sourceRoot":"","sources":["../src/typing-indicator.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,GAIvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAShD,MAAM,cAAc,GAAqC;IACvD,OAAO,EAAE,IAAI;IACb,mBAAmB,EAAE,KAAK;IAC1B,aAAa,EAAE;QACb,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE,IAAI;QAChB,cAAc,EAAE,EAAE,mBAAmB,EAAE,KAAK,EAAE;KAC/C;CACF,CAAC;AAEF,MAAM,OAAO,iCAAiC;IACpC,OAAO,CAAyB;IAChC,MAAM,CAAmC;IACzC,GAAG,CAAiB;IAE5B,YAAY,GAAmB,EAAE,SAA2C,EAAE;QAC5E,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,MAAM,GAAG;YACZ,GAAG,cAAc;YACjB,GAAG,MAAM;YACT,aAAa,EAAE;gBACb,GAAG,cAAc,CAAC,aAAa;gBAC/B,GAAG,MAAM,CAAC,aAAa;gBACvB,cAAc,EAAE;oBACd,mBAAmB,EAAE,MAAM,CAAC,mBAAmB,IAAI,cAAc,CAAC,mBAAmB;oBACrF,GAAG,MAAM,CAAC,aAAa,EAAE,cAAc;iBACxC;aACF;SACF,CAAC;QAEF,IAAI,CAAC,OAAO,GAAG,IAAI,sBAAsB,CAAC;YACxC,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IAEO,eAAe;QACrB,MAAM,OAAO,GAAG,IAAI,4BAA4B,CAC9C,cAAc,EACd,KAAK,EAAE,OAA+B,EAAE,EAAE;YACxC,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;YACtC,IAAI,CAAC,MAAM;gBAAE,OAAO;YACpB,MAAM,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;QAC/D,CAAC,EACD,KAAK,EAAE,OAA+B,EAAE,EAAE;YACxC,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;YACtC,IAAI,CAAC,MAAM;gBAAE,OAAO;YACpB,MAAM,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;QAC/D,CAAC,CACF,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,OAMX;QACC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACzB,OAAO;gBACL,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;gBACrB,IAAI,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;gBACpB,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK;aACtB,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,aAAa,GAA2B;YAC5C,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,QAAQ,EAAE,cAAc;YACxB,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG;YACnB,SAAS,EAAE,SAAS;SACrB,CAAC;QAEF,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACtE,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAIV;QACC,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YACtB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,QAAQ,EAAE,cAAc;YACxB,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG;YACnB,SAAS,EAAE,SAAS;SACrB,CAAC,CAAC;IACL,CAAC;CACF;AAED,SAAS,aAAa,CAAC,OAGtB;IACC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE;WAC7B,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC3F,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9B,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,GAAmB,EACnB,MAAyC;IAEzC,MAAM,OAAO,GAAG,IAAI,iCAAiC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACnE,GAAG,CAAC,gBAAgB,GAAG,OAAO,CAAC;IAC/B,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/lib/upload.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { WeixinApiOptions } from "./ilink-api.js";
|
|
2
|
+
export type UploadedFileInfo = {
|
|
3
|
+
filekey: string;
|
|
4
|
+
/** 由 upload_param 上传后 CDN 返回的下载加密参数; fill into ImageItem.media.encrypt_query_param */
|
|
5
|
+
downloadEncryptedQueryParam: string;
|
|
6
|
+
/** AES-128-ECB key, hex-encoded; convert to base64 for CDNMedia.aes_key */
|
|
7
|
+
aeskey: string;
|
|
8
|
+
/** Plaintext file size in bytes */
|
|
9
|
+
fileSize: number;
|
|
10
|
+
/** Ciphertext file size in bytes (AES-128-ECB with PKCS7 padding); use for ImageItem.hd_size / mid_size */
|
|
11
|
+
fileSizeCiphertext: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Download a remote media URL (image, video, file) to a local temp file in destDir.
|
|
15
|
+
* Returns the local file path; extension is inferred from Content-Type / URL.
|
|
16
|
+
*/
|
|
17
|
+
export declare function downloadRemoteImageToTemp(url: string, destDir: string): Promise<string>;
|
|
18
|
+
/** Upload a local image file to the Weixin CDN with AES-128-ECB encryption. */
|
|
19
|
+
export declare function uploadFileToWeixin(params: {
|
|
20
|
+
filePath: string;
|
|
21
|
+
toUserId: string;
|
|
22
|
+
opts: WeixinApiOptions;
|
|
23
|
+
cdnBaseUrl: string;
|
|
24
|
+
}): Promise<UploadedFileInfo>;
|
|
25
|
+
/** Upload a local video file to the Weixin CDN. */
|
|
26
|
+
export declare function uploadVideoToWeixin(params: {
|
|
27
|
+
filePath: string;
|
|
28
|
+
toUserId: string;
|
|
29
|
+
opts: WeixinApiOptions;
|
|
30
|
+
cdnBaseUrl: string;
|
|
31
|
+
}): Promise<UploadedFileInfo>;
|
|
32
|
+
/**
|
|
33
|
+
* Upload a local file attachment (non-image, non-video) to the Weixin CDN.
|
|
34
|
+
* Uses media_type=FILE; no thumbnail required.
|
|
35
|
+
*/
|
|
36
|
+
export declare function uploadFileAttachmentToWeixin(params: {
|
|
37
|
+
filePath: string;
|
|
38
|
+
fileName: string;
|
|
39
|
+
toUserId: string;
|
|
40
|
+
opts: WeixinApiOptions;
|
|
41
|
+
cdnBaseUrl: string;
|
|
42
|
+
}): Promise<UploadedFileInfo>;
|
|
43
|
+
//# sourceMappingURL=upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAQvD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,2BAA2B,EAAE,MAAM,CAAC;IACpC,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,2GAA2G;IAC3G,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;;GAGG;AACH,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB7F;AAkED,+EAA+E;AAC/E,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAM5B;AAED,mDAAmD;AACnD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAM5B;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAM5B"}
|
package/lib/upload.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getUploadUrl } from "./ilink-api.js";
|
|
5
|
+
import { aesEcbPaddedSize } from "./aes-ecb.js";
|
|
6
|
+
import { uploadBufferToCdn } from "./cdn-upload.js";
|
|
7
|
+
import { logger } from "./ilink-logger.js";
|
|
8
|
+
import { getExtensionFromContentTypeOrUrl } from "./mime.js";
|
|
9
|
+
import { tempFileName } from "./ilink-random.js";
|
|
10
|
+
import { UploadMediaType } from "./ilink-types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Download a remote media URL (image, video, file) to a local temp file in destDir.
|
|
13
|
+
* Returns the local file path; extension is inferred from Content-Type / URL.
|
|
14
|
+
*/
|
|
15
|
+
export async function downloadRemoteImageToTemp(url, destDir) {
|
|
16
|
+
logger.debug(`downloadRemoteImageToTemp: fetching url=${url}`);
|
|
17
|
+
const res = await fetch(url);
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const msg = `remote media download failed: ${res.status} ${res.statusText} url=${url}`;
|
|
20
|
+
logger.error(`downloadRemoteImageToTemp: ${msg}`);
|
|
21
|
+
throw new Error(msg);
|
|
22
|
+
}
|
|
23
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
24
|
+
logger.debug(`downloadRemoteImageToTemp: downloaded ${buf.length} bytes`);
|
|
25
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
26
|
+
const ext = getExtensionFromContentTypeOrUrl(res.headers.get("content-type"), url);
|
|
27
|
+
const name = tempFileName("weixin-remote", ext);
|
|
28
|
+
const filePath = path.join(destDir, name);
|
|
29
|
+
await fs.writeFile(filePath, buf);
|
|
30
|
+
logger.debug(`downloadRemoteImageToTemp: saved to ${filePath} ext=${ext}`);
|
|
31
|
+
return filePath;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Common upload pipeline: read file → hash → gen aeskey → getUploadUrl → uploadBufferToCdn → return info.
|
|
35
|
+
*/
|
|
36
|
+
async function uploadMediaToCdn(params) {
|
|
37
|
+
const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
|
|
38
|
+
const plaintext = await fs.readFile(filePath);
|
|
39
|
+
const rawsize = plaintext.length;
|
|
40
|
+
const rawfilemd5 = crypto.createHash("md5").update(plaintext).digest("hex");
|
|
41
|
+
const filesize = aesEcbPaddedSize(rawsize);
|
|
42
|
+
const filekey = crypto.randomBytes(16).toString("hex");
|
|
43
|
+
const aeskey = crypto.randomBytes(16);
|
|
44
|
+
logger.debug(`${label}: file=${filePath} rawsize=${rawsize} filesize=${filesize} md5=${rawfilemd5} filekey=${filekey}`);
|
|
45
|
+
const uploadUrlResp = await getUploadUrl({
|
|
46
|
+
...opts,
|
|
47
|
+
filekey,
|
|
48
|
+
media_type: mediaType,
|
|
49
|
+
to_user_id: toUserId,
|
|
50
|
+
rawsize,
|
|
51
|
+
rawfilemd5,
|
|
52
|
+
filesize,
|
|
53
|
+
no_need_thumb: true,
|
|
54
|
+
aeskey: aeskey.toString("hex"),
|
|
55
|
+
});
|
|
56
|
+
const uploadFullUrl = uploadUrlResp.upload_full_url?.trim();
|
|
57
|
+
const uploadParam = uploadUrlResp.upload_param;
|
|
58
|
+
if (!uploadFullUrl && !uploadParam) {
|
|
59
|
+
logger.error(`${label}: getUploadUrl returned no upload URL (need upload_full_url or upload_param), resp=${JSON.stringify(uploadUrlResp)}`);
|
|
60
|
+
throw new Error(`${label}: getUploadUrl returned no upload URL`);
|
|
61
|
+
}
|
|
62
|
+
const { downloadParam: downloadEncryptedQueryParam } = await uploadBufferToCdn({
|
|
63
|
+
buf: plaintext,
|
|
64
|
+
uploadFullUrl: uploadFullUrl || undefined,
|
|
65
|
+
uploadParam: uploadParam ?? undefined,
|
|
66
|
+
filekey,
|
|
67
|
+
cdnBaseUrl,
|
|
68
|
+
aeskey,
|
|
69
|
+
label: `${label}[orig filekey=${filekey}]`,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
filekey,
|
|
73
|
+
downloadEncryptedQueryParam,
|
|
74
|
+
aeskey: aeskey.toString("hex"),
|
|
75
|
+
fileSize: rawsize,
|
|
76
|
+
fileSizeCiphertext: filesize,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Upload a local image file to the Weixin CDN with AES-128-ECB encryption. */
|
|
80
|
+
export async function uploadFileToWeixin(params) {
|
|
81
|
+
return uploadMediaToCdn({
|
|
82
|
+
...params,
|
|
83
|
+
mediaType: UploadMediaType.IMAGE,
|
|
84
|
+
label: "uploadFileToWeixin",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Upload a local video file to the Weixin CDN. */
|
|
88
|
+
export async function uploadVideoToWeixin(params) {
|
|
89
|
+
return uploadMediaToCdn({
|
|
90
|
+
...params,
|
|
91
|
+
mediaType: UploadMediaType.VIDEO,
|
|
92
|
+
label: "uploadVideoToWeixin",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Upload a local file attachment (non-image, non-video) to the Weixin CDN.
|
|
97
|
+
* Uses media_type=FILE; no thumbnail required.
|
|
98
|
+
*/
|
|
99
|
+
export async function uploadFileAttachmentToWeixin(params) {
|
|
100
|
+
return uploadMediaToCdn({
|
|
101
|
+
...params,
|
|
102
|
+
mediaType: UploadMediaType.FILE,
|
|
103
|
+
label: "uploadFileAttachmentToWeixin",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { WeixinMessage, MessageItem } from "./ilink-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Restore persisted context tokens for an account into the in-memory map.
|
|
4
|
+
* Called once during gateway startAccount to survive restarts.
|
|
5
|
+
*/
|
|
6
|
+
export declare function restoreContextTokens(accountId: string): void;
|
|
7
|
+
/** Remove all context tokens for a given account (memory + disk). */
|
|
8
|
+
export declare function clearContextTokensForAccount(accountId: string): void;
|
|
9
|
+
/** Store a context token for a given account+user pair (memory + disk). */
|
|
10
|
+
export declare function setContextToken(accountId: string, userId: string, token: string): void;
|
|
11
|
+
/** Retrieve the cached context token for a given account+user pair. */
|
|
12
|
+
export declare function getContextToken(accountId: string, userId: string): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Find all accountIds that have an active contextToken for the given userId.
|
|
15
|
+
* Used to infer the sending bot account from the recipient address when
|
|
16
|
+
* accountId is not explicitly provided (e.g. cron delivery).
|
|
17
|
+
*
|
|
18
|
+
* Returns all matching accountIds (not just the first) so the caller can
|
|
19
|
+
* detect ambiguity when multiple accounts have sessions with the same user.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findAccountIdsByContextToken(accountIds: string[], userId: string): string[];
|
|
22
|
+
/** Inbound context passed to the OpenClaw core pipeline (matches MsgContext shape). */
|
|
23
|
+
export type WeixinMsgContext = {
|
|
24
|
+
Body: string;
|
|
25
|
+
From: string;
|
|
26
|
+
To: string;
|
|
27
|
+
AccountId: string;
|
|
28
|
+
OriginatingChannel: "openclaw-weixin";
|
|
29
|
+
OriginatingTo: string;
|
|
30
|
+
MessageSid: string;
|
|
31
|
+
Timestamp?: number;
|
|
32
|
+
Provider: "openclaw-weixin";
|
|
33
|
+
ChatType: "direct";
|
|
34
|
+
/** Set by monitor after resolveAgentRoute so dispatchReplyFromConfig uses the correct session. */
|
|
35
|
+
SessionKey?: string;
|
|
36
|
+
context_token?: string;
|
|
37
|
+
MediaUrl?: string;
|
|
38
|
+
MediaPath?: string;
|
|
39
|
+
MediaType?: string;
|
|
40
|
+
/** Raw message body for framework command authorization. */
|
|
41
|
+
CommandBody?: string;
|
|
42
|
+
/** Whether the sender is authorized to execute slash commands. */
|
|
43
|
+
CommandAuthorized?: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** Returns true if the message item is a media type (image, video, file, or voice). */
|
|
46
|
+
export declare function isMediaItem(item: MessageItem): boolean;
|
|
47
|
+
export declare function bodyFromItemList(itemList?: MessageItem[]): string;
|
|
48
|
+
export type WeixinInboundMediaOpts = {
|
|
49
|
+
/** Local path to decrypted image file. */
|
|
50
|
+
decryptedPicPath?: string;
|
|
51
|
+
/** Local path to transcoded/raw voice file (.wav or .silk). */
|
|
52
|
+
decryptedVoicePath?: string;
|
|
53
|
+
/** MIME type for the voice file (e.g. "audio/wav" or "audio/silk"). */
|
|
54
|
+
voiceMediaType?: string;
|
|
55
|
+
/** Local path to decrypted file attachment. */
|
|
56
|
+
decryptedFilePath?: string;
|
|
57
|
+
/** MIME type for the file attachment (guessed from file_name). */
|
|
58
|
+
fileMediaType?: string;
|
|
59
|
+
/** Local path to decrypted video file. */
|
|
60
|
+
decryptedVideoPath?: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Convert a WeixinMessage from getUpdates to the inbound MsgContext for the core pipeline.
|
|
64
|
+
* Media: only pass MediaPath (local file, after CDN download + decrypt).
|
|
65
|
+
* We never pass MediaUrl — the upstream CDN URL is encrypted/auth-only.
|
|
66
|
+
* Priority when multiple media types present: image > video > file > voice.
|
|
67
|
+
*/
|
|
68
|
+
export declare function weixinMessageToMsgContext(msg: WeixinMessage, accountId: string, opts?: WeixinInboundMediaOpts): WeixinMsgContext;
|
|
69
|
+
/** Extract the context_token from an inbound WeixinMsgContext. */
|
|
70
|
+
export declare function getContextTokenFromMsgContext(ctx: WeixinMsgContext): string | undefined;
|
|
71
|
+
//# sourceMappingURL=weixin-inbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-inbound.d.ts","sourceRoot":"","sources":["../src/weixin-inbound.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA8CnE;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAiB5D;AAED,qEAAqE;AACrE,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAcpE;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAKtF;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrF;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,EAAE,EACpB,MAAM,EAAE,MAAM,GACb,MAAM,EAAE,CAEV;AAUD,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,iBAAiB,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,kGAAkG;IAClG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAOtD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,GAAG,MAAM,CAyBjE;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,GAC5B,gBAAgB,CAiClB;AAED,kEAAkE;AAClE,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAEvF"}
|