@clowder-ai/connector-weixin 0.0.0-stage → 0.1.0-alpha.5
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.en.md +55 -0
- package/README.md +27 -2
- package/assets/icon.svg +3 -0
- package/dist/WeixinAdapter.d.ts +255 -0
- package/dist/WeixinAdapter.d.ts.map +1 -0
- package/dist/WeixinAdapter.js +1130 -0
- package/dist/WeixinAdapter.js.map +1 -0
- package/dist/command-ingress.d.ts +10 -0
- package/dist/command-ingress.d.ts.map +1 -0
- package/dist/command-ingress.js +13 -0
- package/dist/command-ingress.js.map +1 -0
- package/dist/inbound-download.d.ts +13 -0
- package/dist/inbound-download.d.ts.map +1 -0
- package/dist/inbound-download.js +70 -0
- package/dist/inbound-download.js.map +1 -0
- package/dist/inbound-media-source.d.ts +25 -0
- package/dist/inbound-media-source.d.ts.map +1 -0
- package/dist/inbound-media-source.js +111 -0
- package/dist/inbound-media-source.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/materialize-media.d.ts +7 -0
- package/dist/materialize-media.d.ts.map +1 -0
- package/dist/materialize-media.js +35 -0
- package/dist/materialize-media.js.map +1 -0
- package/dist/media-notice.d.ts +6 -0
- package/dist/media-notice.d.ts.map +1 -0
- package/dist/media-notice.js +50 -0
- package/dist/media-notice.js.map +1 -0
- package/dist/plugin-entrypoint.d.ts +8 -0
- package/dist/plugin-entrypoint.d.ts.map +1 -0
- package/dist/plugin-entrypoint.js +295 -0
- package/dist/plugin-entrypoint.js.map +1 -0
- package/dist/reply-sender-map.d.ts +10 -0
- package/dist/reply-sender-map.d.ts.map +1 -0
- package/dist/reply-sender-map.js +123 -0
- package/dist/reply-sender-map.js.map +1 -0
- package/dist/rich-block-plaintext.d.ts +5 -0
- package/dist/rich-block-plaintext.d.ts.map +1 -0
- package/dist/rich-block-plaintext.js +45 -0
- package/dist/rich-block-plaintext.js.map +1 -0
- package/dist/runtime.d.ts +51 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +142 -0
- package/dist/runtime.js.map +1 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +39 -0
- package/dist/settings.js.map +1 -0
- package/dist/thread-mappings.d.ts +46 -0
- package/dist/thread-mappings.d.ts.map +1 -0
- package/dist/thread-mappings.js +62 -0
- package/dist/thread-mappings.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/weixin-cdn.d.ts +43 -0
- package/dist/weixin-cdn.d.ts.map +1 -0
- package/dist/weixin-cdn.js +226 -0
- package/dist/weixin-cdn.js.map +1 -0
- package/npm-shrinkwrap.json +1013 -0
- package/package.json +58 -4
- package/plugin.yaml +107 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { WeixinAdapter, } from './WeixinAdapter.js';
|
|
2
|
+
function hostMessage(message) {
|
|
3
|
+
const attachments = message.attachments?.map((attachment) => ({
|
|
4
|
+
type: attachment.type,
|
|
5
|
+
platformKey: attachment.mediaUrl,
|
|
6
|
+
...(attachment.fileName === undefined ? {} : { fileName: attachment.fileName }),
|
|
7
|
+
}));
|
|
8
|
+
return {
|
|
9
|
+
externalConversationId: message.chatId,
|
|
10
|
+
externalSenderId: message.senderId,
|
|
11
|
+
providerMessageId: message.messageId,
|
|
12
|
+
text: message.text,
|
|
13
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function createWeixinConnectorRuntime(options) {
|
|
17
|
+
const initialToken = options.config.botToken.trim();
|
|
18
|
+
const { botToken: _botToken, ...runtimeOptions } = options.config;
|
|
19
|
+
const createAdapter = options.createAdapter ?? ((token, logger, state, value) => new WeixinAdapter(token, logger, state, value));
|
|
20
|
+
// The Host starts the runtime healthy BEFORE credentials exist: without a token
|
|
21
|
+
// the runtime stays idle (no connect, no polling) until QR login adopts one.
|
|
22
|
+
let outbound = initialToken.length > 0
|
|
23
|
+
? createAdapter(initialToken, options.logger, options.state, runtimeOptions)
|
|
24
|
+
: undefined;
|
|
25
|
+
let state = 'idle';
|
|
26
|
+
// Armed = start() ran and stop() has not. Unlike startPromise, disconnect()
|
|
27
|
+
// never clears it: a reconnect after an owner disconnect must still begin
|
|
28
|
+
// polling (Host write-back does not restart the plugin).
|
|
29
|
+
let armed = false;
|
|
30
|
+
let startPromise;
|
|
31
|
+
let stopPromise;
|
|
32
|
+
// Bumped by every connect()/disconnect() that supersedes in-flight work: a
|
|
33
|
+
// polling chain captured an older generation must not start polling on a
|
|
34
|
+
// stale adapter or publish state after the connection it belongs to was
|
|
35
|
+
// dropped or replaced.
|
|
36
|
+
let generation = 0;
|
|
37
|
+
const deliverIfRunning = async (message) => {
|
|
38
|
+
if (state !== 'running')
|
|
39
|
+
return;
|
|
40
|
+
await options.host.deliver(hostMessage(message));
|
|
41
|
+
};
|
|
42
|
+
const requireOutbound = () => {
|
|
43
|
+
if (outbound === undefined) {
|
|
44
|
+
throw new Error('WeChat connector is not connected — complete QR code login (weixin_qr_login) first');
|
|
45
|
+
}
|
|
46
|
+
return outbound;
|
|
47
|
+
};
|
|
48
|
+
const beginPolling = () => {
|
|
49
|
+
const adapter = requireOutbound();
|
|
50
|
+
const generationAtStart = generation;
|
|
51
|
+
state = 'starting';
|
|
52
|
+
startPromise = Promise.resolve()
|
|
53
|
+
.then(() => adapter.restoreSessionState())
|
|
54
|
+
.then(() => {
|
|
55
|
+
// disconnect() or a newer connect() superseded this chain while the
|
|
56
|
+
// session state was restoring: polling must not start on the stale
|
|
57
|
+
// adapter (it may already be disconnected, with an empty token).
|
|
58
|
+
if (generationAtStart !== generation)
|
|
59
|
+
return;
|
|
60
|
+
return adapter.startPolling(deliverIfRunning);
|
|
61
|
+
})
|
|
62
|
+
.then(() => {
|
|
63
|
+
if (state !== 'stopped' && generationAtStart === generation) {
|
|
64
|
+
state = 'running';
|
|
65
|
+
options.logger.info('[WeixinRuntime] Provider polling started');
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.catch((error) => {
|
|
69
|
+
if (state !== 'stopped' && generationAtStart === generation) {
|
|
70
|
+
state = 'idle';
|
|
71
|
+
startPromise = undefined;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
});
|
|
75
|
+
return startPromise;
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
get outbound() {
|
|
79
|
+
return requireOutbound();
|
|
80
|
+
},
|
|
81
|
+
start() {
|
|
82
|
+
if (state === 'stopped')
|
|
83
|
+
return Promise.reject(new Error('Weixin connector runtime has been stopped'));
|
|
84
|
+
if (startPromise !== undefined)
|
|
85
|
+
return startPromise;
|
|
86
|
+
armed = true;
|
|
87
|
+
if (outbound === undefined) {
|
|
88
|
+
// Armed but idle: no provider I/O until connect() adopts a token.
|
|
89
|
+
startPromise = Promise.resolve();
|
|
90
|
+
return startPromise;
|
|
91
|
+
}
|
|
92
|
+
return beginPolling();
|
|
93
|
+
},
|
|
94
|
+
connect(botToken) {
|
|
95
|
+
const token = botToken.trim();
|
|
96
|
+
if (token.length === 0)
|
|
97
|
+
return Promise.reject(new TypeError('botToken must be a non-empty value'));
|
|
98
|
+
if (state === 'stopped')
|
|
99
|
+
return Promise.reject(new Error('Weixin connector runtime has been stopped'));
|
|
100
|
+
if (outbound === undefined) {
|
|
101
|
+
outbound = createAdapter(token, options.logger, options.state, runtimeOptions);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
outbound.setBotToken(token);
|
|
105
|
+
}
|
|
106
|
+
if (!armed || state === 'running')
|
|
107
|
+
return Promise.resolve();
|
|
108
|
+
generation += 1;
|
|
109
|
+
return beginPolling();
|
|
110
|
+
},
|
|
111
|
+
async disconnect() {
|
|
112
|
+
// Supersede any polling chain still restoring session state: it belongs
|
|
113
|
+
// to the connection being dropped and must not publish state afterwards.
|
|
114
|
+
generation += 1;
|
|
115
|
+
const adapter = outbound;
|
|
116
|
+
outbound = undefined;
|
|
117
|
+
state = 'idle';
|
|
118
|
+
startPromise = undefined;
|
|
119
|
+
if (adapter !== undefined)
|
|
120
|
+
await adapter.disconnect();
|
|
121
|
+
},
|
|
122
|
+
isConnected() {
|
|
123
|
+
return outbound !== undefined && outbound.hasBotToken() && outbound.isPolling();
|
|
124
|
+
},
|
|
125
|
+
stop() {
|
|
126
|
+
if (stopPromise !== undefined)
|
|
127
|
+
return stopPromise;
|
|
128
|
+
armed = false;
|
|
129
|
+
if (state === 'idle') {
|
|
130
|
+
state = 'stopped';
|
|
131
|
+
return Promise.resolve();
|
|
132
|
+
}
|
|
133
|
+
state = 'stopped';
|
|
134
|
+
stopPromise = (async () => {
|
|
135
|
+
await startPromise?.catch(() => undefined);
|
|
136
|
+
await outbound?.stopPolling();
|
|
137
|
+
})();
|
|
138
|
+
return stopPromise;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAId,MAAM,oBAAoB,CAAC;AA6D5B,SAAS,WAAW,CAAC,OAA6B;IAChD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,WAAW,EAAE,UAAU,CAAC,QAAQ;QAChC,GAAG,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC;KAChF,CAAC,CAAC,CAAC;IACJ,OAAO;QACL,sBAAsB,EAAE,OAAO,CAAC,MAAM;QACtC,gBAAgB,EAAE,OAAO,CAAC,QAAQ;QAClC,iBAAiB,EAAE,OAAO,CAAC,SAAS;QACpC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;KACtD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,4BAA4B,CAC1C,OAA+C;IAE/C,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IACpD,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAClE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,CAAC,CAC9C,KAAa,EACb,MAAuB,EACvB,KAA8B,EAC9B,KAA2B,EAC3B,EAAE,CAAC,IAAI,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAuB,CAAC,CAAC;IAC3E,gFAAgF;IAChF,6EAA6E;IAC7E,IAAI,QAAQ,GAAwB,YAAY,CAAC,MAAM,GAAG,CAAC;QACzD,CAAC,CAAC,aAAa,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,cAAc,CAAC;QAC5E,CAAC,CAAC,SAAS,CAAC;IACd,IAAI,KAAK,GAAgD,MAAM,CAAC;IAChE,4EAA4E;IAC5E,0EAA0E;IAC1E,yDAAyD;IACzD,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,YAAuC,CAAC;IAC5C,IAAI,WAAsC,CAAC;IAC3C,2EAA2E;IAC3E,yEAAyE;IACzE,wEAAwE;IACxE,uBAAuB;IACvB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,MAAM,gBAAgB,GAAG,KAAK,EAAE,OAA6B,EAAE,EAAE;QAC/D,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC;IACF,MAAM,eAAe,GAAG,GAAY,EAAE;QACpC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;QACxG,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;IACF,MAAM,YAAY,GAAG,GAAkB,EAAE;QACvC,MAAM,OAAO,GAAG,eAAe,EAAE,CAAC;QAClC,MAAM,iBAAiB,GAAG,UAAU,CAAC;QACrC,KAAK,GAAG,UAAU,CAAC;QACnB,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE;aAC7B,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,mBAAmB,EAAE,CAAC;aACzC,IAAI,CAAC,GAAG,EAAE;YACT,oEAAoE;YACpE,mEAAmE;YACnE,iEAAiE;YACjE,IAAI,iBAAiB,KAAK,UAAU;gBAAE,OAAO;YAC7C,OAAO,OAAO,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC;QAChD,CAAC,CAAC;aACD,IAAI,CAAC,GAAG,EAAE;YACT,IAAI,KAAK,KAAK,SAAS,IAAI,iBAAiB,KAAK,UAAU,EAAE,CAAC;gBAC5D,KAAK,GAAG,SAAS,CAAC;gBAClB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;YAClE,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,IAAI,KAAK,KAAK,SAAS,IAAI,iBAAiB,KAAK,UAAU,EAAE,CAAC;gBAC5D,KAAK,GAAG,MAAM,CAAC;gBACf,YAAY,GAAG,SAAS,CAAC;YAC3B,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC,CAAC,CAAC;QACL,OAAO,YAAY,CAAC;IACtB,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,QAAQ;YACV,OAAO,eAAe,EAAE,CAAC;QAC3B,CAAC;QACD,KAAK;YACH,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC,CAAC;YACvG,IAAI,YAAY,KAAK,SAAS;gBAAE,OAAO,YAAY,CAAC;YACpD,KAAK,GAAG,IAAI,CAAC;YACb,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,kEAAkE;gBAClE,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;gBACjC,OAAO,YAAY,CAAC;YACtB,CAAC;YACD,OAAO,YAAY,EAAE,CAAC;QACxB,CAAC;QACD,OAAO,CAAC,QAAgB;YACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;YAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,oCAAoC,CAAC,CAAC,CAAC;YACnG,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC,CAAC;YACvG,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,QAAQ,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YACjF,CAAC;iBAAM,CAAC;gBACN,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAC9B,CAAC;YACD,IAAI,CAAC,KAAK,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;YAC5D,UAAU,IAAI,CAAC,CAAC;YAChB,OAAO,YAAY,EAAE,CAAC;QACxB,CAAC;QACD,KAAK,CAAC,UAAU;YACd,wEAAwE;YACxE,yEAAyE;YACzE,UAAU,IAAI,CAAC,CAAC;YAChB,MAAM,OAAO,GAAG,QAAQ,CAAC;YACzB,QAAQ,GAAG,SAAS,CAAC;YACrB,KAAK,GAAG,MAAM,CAAC;YACf,YAAY,GAAG,SAAS,CAAC;YACzB,IAAI,OAAO,KAAK,SAAS;gBAAE,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC;QACxD,CAAC;QACD,WAAW;YACT,OAAO,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,WAAW,EAAE,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;QAClF,CAAC;QACD,IAAI;YACF,IAAI,WAAW,KAAK,SAAS;gBAAE,OAAO,WAAW,CAAC;YAClD,KAAK,GAAG,KAAK,CAAC;YACd,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;gBACrB,KAAK,GAAG,SAAS,CAAC;gBAClB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;YAC3B,CAAC;YACD,KAAK,GAAG,SAAS,CAAC;YAClB,WAAW,GAAG,CAAC,KAAK,IAAI,EAAE;gBACxB,MAAM,YAAY,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;gBAC3C,MAAM,QAAQ,EAAE,WAAW,EAAE,CAAC;YAChC,CAAC,CAAC,EAAE,CAAC;YACL,OAAO,WAAW,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { definePluginModule } from '@clowder-ai/plugin-sdk';
|
|
2
|
+
import type { ModulePluginHostShape } from '@clowder-ai/plugin-sdk';
|
|
3
|
+
export type SettingsHost = Pick<ModulePluginHostShape, 'config' | 'secrets' | 'storage' | 'log'>;
|
|
4
|
+
interface SettingsCall {
|
|
5
|
+
readonly signal?: AbortSignal;
|
|
6
|
+
assertCurrent(): void | Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export interface SettingsDefinition {
|
|
9
|
+
readonly actions: Readonly<Record<string, (params: unknown, action?: unknown, call?: SettingsCall) => Promise<unknown>>>;
|
|
10
|
+
dispose?(): void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface SettingsEntrypoint {
|
|
13
|
+
create(manifest: unknown): ReturnType<ReturnType<typeof definePluginModule>['create']> & {
|
|
14
|
+
settings(host: SettingsHost): SettingsDefinition;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function createWeixinSettings(host: SettingsHost): SettingsDefinition;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAGpE,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,qBAAqB,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,KAAK,CAAC,CAAC;AACjG,UAAU,YAAY;IAAG,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,aAAa,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAAE;AAEhG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACzH,OAAO,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AACD,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,QAAQ,EAAE,OAAO,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CAAA;KAAE,CAAC;CAC/I;AAKD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CAgC3E"}
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { WeixinAdapter } from './WeixinAdapter.js';
|
|
2
|
+
const current = async (call) => { call?.signal?.throwIfAborted(); await call?.assertCurrent(); call?.signal?.throwIfAborted(); };
|
|
3
|
+
const QR_SESSION_KEY = 'weixin.qr-session.v1';
|
|
4
|
+
export function createWeixinSettings(host) {
|
|
5
|
+
return { actions: {
|
|
6
|
+
'weixin.qr-generate': async (_params, _action, call) => {
|
|
7
|
+
await current(call);
|
|
8
|
+
const result = await WeixinAdapter.fetchQrCode();
|
|
9
|
+
const QRCode = await import('qrcode');
|
|
10
|
+
const url = await QRCode.toDataURL(result.qrUrl, { width: 384, margin: 2 });
|
|
11
|
+
await current(call);
|
|
12
|
+
await host.storage.set(QR_SESSION_KEY, { qrPayload: result.qrPayload });
|
|
13
|
+
return { render: 'img', data: { url } };
|
|
14
|
+
},
|
|
15
|
+
'weixin.qr-status': async (_params, _action, call) => {
|
|
16
|
+
await current(call);
|
|
17
|
+
const value = (await host.storage.get(QR_SESSION_KEY))?.value;
|
|
18
|
+
const qrPayload = typeof value?.qrPayload === 'string' ? value.qrPayload : undefined;
|
|
19
|
+
if (!qrPayload)
|
|
20
|
+
return { render: 'polling', data: { status: 'error', message: 'No QR payload — generate first' }, advance: false };
|
|
21
|
+
const status = await WeixinAdapter.pollQrCodeStatus(qrPayload);
|
|
22
|
+
await current(call);
|
|
23
|
+
if (status.status === 'confirmed') {
|
|
24
|
+
if (!status.botToken)
|
|
25
|
+
return { render: 'polling', data: { status: 'error', message: 'confirmed but no bot_token' }, advance: false };
|
|
26
|
+
await host.storage.set(QR_SESSION_KEY, null);
|
|
27
|
+
return { render: 'status', data: { status: 'confirmed' }, label: '已连接', targetValues: { botToken: status.botToken } };
|
|
28
|
+
}
|
|
29
|
+
return { render: 'polling', data: { status: status.status, ...('message' in status ? { message: status.message } : {}) }, advance: false };
|
|
30
|
+
},
|
|
31
|
+
'weixin.disconnect': async (_params, _action, call) => {
|
|
32
|
+
await current(call);
|
|
33
|
+
await host.storage.set('provider-session', null);
|
|
34
|
+
await host.storage.set(QR_SESSION_KEY, null);
|
|
35
|
+
return { render: 'status', data: { status: 'disconnected' }, label: '已断开', targetValues: { botToken: '' } };
|
|
36
|
+
},
|
|
37
|
+
} };
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FeatureContext } from '@clowder-ai/plugin-sdk';
|
|
2
|
+
type Mapping = {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
readonly threadId: string;
|
|
5
|
+
readonly createdAt: number;
|
|
6
|
+
};
|
|
7
|
+
type Creation = {
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly parentThreadId?: string;
|
|
10
|
+
readonly participants?: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function threadMappings(context: FeatureContext): {
|
|
13
|
+
findByKey: (name: string) => Promise<import("@clowder-ai/plugin-sdk").PluginThreadSummary | null>;
|
|
14
|
+
bind: (name: string, threadId: string) => Promise<boolean | Mapping>;
|
|
15
|
+
unbind: (name: string) => Promise<boolean | Mapping>;
|
|
16
|
+
listBindings: () => Promise<Mapping[]>;
|
|
17
|
+
ensureByKey: (name: string, input: Creation) => Promise<import("@clowder-ai/plugin-sdk").PluginThreadSummary>;
|
|
18
|
+
ensureSystemThread: () => Promise<import("@clowder-ai/plugin-sdk").PluginThreadSummary>;
|
|
19
|
+
get(threadId: string): Promise<import("@clowder-ai/plugin-sdk").PluginThreadSummary | null>;
|
|
20
|
+
list(options?: {
|
|
21
|
+
readonly limit?: number;
|
|
22
|
+
readonly before?: {
|
|
23
|
+
readonly lastActiveAt: number;
|
|
24
|
+
readonly id: string;
|
|
25
|
+
};
|
|
26
|
+
}): Promise<readonly import("@clowder-ai/plugin-sdk").PluginThreadSummary[]>;
|
|
27
|
+
readMessages(threadId: string, options: {
|
|
28
|
+
readonly before?: {
|
|
29
|
+
readonly timestamp: number;
|
|
30
|
+
readonly id: string;
|
|
31
|
+
};
|
|
32
|
+
readonly limit: number;
|
|
33
|
+
}): Promise<readonly import("@clowder-ai/plugin-sdk").PluginThreadMessage[]>;
|
|
34
|
+
getCats(threadId: string): Promise<import("@clowder-ai/plugin-sdk").PluginThreadCats>;
|
|
35
|
+
addParticipants(input: {
|
|
36
|
+
readonly threadId: string;
|
|
37
|
+
readonly catIds: readonly string[];
|
|
38
|
+
}): Promise<import("@clowder-ai/plugin-sdk").PluginThreadSummary>;
|
|
39
|
+
create(input: import("@clowder-ai/plugin-sdk").PluginThreadCreateInput): Promise<import("@clowder-ai/plugin-sdk").PluginThreadSummary>;
|
|
40
|
+
update(threadId: string, patch: {
|
|
41
|
+
readonly title?: string;
|
|
42
|
+
readonly preferredCats?: readonly string[];
|
|
43
|
+
}): Promise<import("@clowder-ai/plugin-sdk").PluginThreadSummary>;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=thread-mappings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-mappings.d.ts","sourceRoot":"","sources":["../src/thread-mappings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,KAAK,OAAO,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAC/F,KAAK,QAAQ,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AA+CxH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc;sBA/B1B,MAAM;iBAwBjB,MAAM,YAAY,MAAM;mBACtB,MAAM;;wBAVD,MAAM,SAAS,QAAQ;;;;sBAkBT,CAAC;uBAAiC,CAAC;;;;;;uBAG2M,CAAC;;;;;;;;;;;;;sBAA2f,CAAC;8BAAwC,CAAC;;EADzzB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const contexts = new WeakMap();
|
|
2
|
+
/** Package-owned external-conversation mapping. Host receives ordinary thread calls only. */
|
|
3
|
+
function createMappings(context) {
|
|
4
|
+
const key = 'thread-mappings.v1';
|
|
5
|
+
let tail = Promise.resolve();
|
|
6
|
+
const serialize = (run) => {
|
|
7
|
+
const next = tail.then(run, run);
|
|
8
|
+
tail = next.catch(() => { });
|
|
9
|
+
return next;
|
|
10
|
+
};
|
|
11
|
+
const read = async () => {
|
|
12
|
+
const stored = await context.storage.get(key);
|
|
13
|
+
const rows = stored?.value ?? [];
|
|
14
|
+
if (!Array.isArray(rows) || rows.some(row => !row || typeof row.key !== 'string' || typeof row.threadId !== 'string' || typeof row.createdAt !== 'number'))
|
|
15
|
+
throw new Error('Invalid plugin thread mappings');
|
|
16
|
+
return { stored, rows: rows };
|
|
17
|
+
};
|
|
18
|
+
const find = async (name) => {
|
|
19
|
+
const { rows } = await read();
|
|
20
|
+
const row = rows.find(row => row.key === name);
|
|
21
|
+
return row ? context.threads.get(row.threadId) : null;
|
|
22
|
+
};
|
|
23
|
+
const save = async (name, threadId) => {
|
|
24
|
+
const { stored, rows } = await read();
|
|
25
|
+
const next = rows.filter(row => row.key !== name);
|
|
26
|
+
if (threadId !== null) {
|
|
27
|
+
if (!await context.threads.get(threadId))
|
|
28
|
+
throw new Error('Thread unavailable');
|
|
29
|
+
next.push({ key: name, threadId, createdAt: Date.now() });
|
|
30
|
+
}
|
|
31
|
+
const result = await context.storage.compareAndSet(key, stored?.revision ?? null, next);
|
|
32
|
+
if (!result.applied)
|
|
33
|
+
throw new Error('Plugin thread mappings changed concurrently');
|
|
34
|
+
return threadId === null ? rows.length !== next.length : next.find(row => row.key === name);
|
|
35
|
+
};
|
|
36
|
+
const ensure = (name, input) => serialize(async () => {
|
|
37
|
+
const existing = await find(name);
|
|
38
|
+
if (existing)
|
|
39
|
+
return existing;
|
|
40
|
+
const thread = await context.threads.create(input);
|
|
41
|
+
await save(name, thread.id);
|
|
42
|
+
return thread;
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
...context.threads,
|
|
46
|
+
findByKey: find,
|
|
47
|
+
bind: (name, threadId) => serialize(() => save(name, threadId)),
|
|
48
|
+
unbind: (name) => serialize(() => save(name, null)),
|
|
49
|
+
listBindings: async () => (await read()).rows,
|
|
50
|
+
ensureByKey: ensure,
|
|
51
|
+
ensureSystemThread: () => ensure('command-audit', { title: '连接器命令记录' }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function threadMappings(context) {
|
|
55
|
+
let value = contexts.get(context);
|
|
56
|
+
if (!value) {
|
|
57
|
+
value = createMappings(context);
|
|
58
|
+
contexts.set(context, value);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=thread-mappings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-mappings.js","sourceRoot":"","sources":["../src/thread-mappings.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KAAK,IAAI,CAAC;AACvE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,kBAAkB,CAAC;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,KAAK,EAAE,kBAAkB,CAAC;CAC3B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ConnectorLogger } from './types.js';
|
|
2
|
+
export declare const UploadMediaType: {
|
|
3
|
+
readonly IMAGE: 1;
|
|
4
|
+
readonly VIDEO: 2;
|
|
5
|
+
readonly FILE: 3;
|
|
6
|
+
readonly VOICE: 4;
|
|
7
|
+
};
|
|
8
|
+
export interface UploadedFileInfo {
|
|
9
|
+
filekey: string;
|
|
10
|
+
downloadEncryptedQueryParam: string;
|
|
11
|
+
aeskey: string;
|
|
12
|
+
fileSize: number;
|
|
13
|
+
fileSizeCiphertext: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer;
|
|
16
|
+
export declare function decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer;
|
|
17
|
+
/**
|
|
18
|
+
* Decode aesKey string from iLink protocol into a 16-byte Buffer.
|
|
19
|
+
* Handles: hex (32 chars), standard base64, base64url (- and _), and missing padding.
|
|
20
|
+
*/
|
|
21
|
+
export declare function decodeAesKey(aesKey: string, log?: ConnectorLogger): Buffer;
|
|
22
|
+
/**
|
|
23
|
+
* Download encrypted media from WeChat CDN and decrypt.
|
|
24
|
+
* `platformKey` is a JSON-encoded string:
|
|
25
|
+
* - { encryptQueryParam, aesKey } for legacy CDN download API
|
|
26
|
+
* - { fullUrl, aesKey } for iLink full_url responses
|
|
27
|
+
*/
|
|
28
|
+
export declare function downloadMediaFromCdn(params: {
|
|
29
|
+
platformKey: string;
|
|
30
|
+
cdnBaseUrl: string;
|
|
31
|
+
log: ConnectorLogger;
|
|
32
|
+
fetchFn?: typeof fetch;
|
|
33
|
+
}): Promise<Buffer>;
|
|
34
|
+
export declare function uploadMediaToCdn(params: {
|
|
35
|
+
filePath: string;
|
|
36
|
+
toUserId: string;
|
|
37
|
+
mediaType: number;
|
|
38
|
+
botToken: string;
|
|
39
|
+
cdnBaseUrl: string;
|
|
40
|
+
log: ConnectorLogger;
|
|
41
|
+
fetchFn?: typeof fetch;
|
|
42
|
+
}): Promise<UploadedFileInfo>;
|
|
43
|
+
//# sourceMappingURL=weixin-cdn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weixin-cdn.d.ts","sourceRoot":"","sources":["../src/weixin-cdn.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAKlD,eAAO,MAAM,eAAe;;;;;CAAqD,CAAC;AAElF,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B,EAAE,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAeD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGrE;AAQD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,eAAe,GAAG,MAAM,CA6B1E;AAuBD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,eAAe,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkClB;AAID,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,eAAe,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA8C5B"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat CDN upload/download with AES-128-ECB encryption.
|
|
3
|
+
* Aligned with @tencent-weixin/openclaw-weixin@2.0.1 cdn/ module.
|
|
4
|
+
*
|
|
5
|
+
* Flow: readFile → md5 → genKey → getUploadUrl → AES encrypt → POST to CDN → get downloadParam
|
|
6
|
+
*/
|
|
7
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { basename } from 'node:path';
|
|
10
|
+
import { fetchBoundedInboundMedia } from './inbound-download.js';
|
|
11
|
+
const ILINK_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
|
12
|
+
const UPLOAD_MAX_RETRIES = 3;
|
|
13
|
+
export const UploadMediaType = { IMAGE: 1, VIDEO: 2, FILE: 3, VOICE: 4 };
|
|
14
|
+
// ── AES-128-ECB ──
|
|
15
|
+
export function encryptAesEcb(plaintext, key) {
|
|
16
|
+
const cipher = createCipheriv('aes-128-ecb', key, null);
|
|
17
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
18
|
+
}
|
|
19
|
+
export function decryptAesEcb(ciphertext, key) {
|
|
20
|
+
const decipher = createDecipheriv('aes-128-ecb', key, null);
|
|
21
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
22
|
+
}
|
|
23
|
+
function aesEcbPaddedSize(plaintextSize) {
|
|
24
|
+
return Math.ceil((plaintextSize + 1) / 16) * 16;
|
|
25
|
+
}
|
|
26
|
+
// ── AES Key Decode ──
|
|
27
|
+
/**
|
|
28
|
+
* Decode aesKey string from iLink protocol into a 16-byte Buffer.
|
|
29
|
+
* Handles: hex (32 chars), standard base64, base64url (- and _), and missing padding.
|
|
30
|
+
*/
|
|
31
|
+
export function decodeAesKey(aesKey, log) {
|
|
32
|
+
// 1. Hex: exactly 32 hex chars → 16 bytes
|
|
33
|
+
if (/^[0-9a-f]{32}$/i.test(aesKey)) {
|
|
34
|
+
return Buffer.from(aesKey, 'hex');
|
|
35
|
+
}
|
|
36
|
+
// 2. Normalize base64url → standard base64 (replace - with +, _ with /)
|
|
37
|
+
let normalized = aesKey.replace(/-/g, '+').replace(/_/g, '/');
|
|
38
|
+
// 3. Restore padding if missing
|
|
39
|
+
const pad = normalized.length % 4;
|
|
40
|
+
if (pad === 2)
|
|
41
|
+
normalized += '==';
|
|
42
|
+
else if (pad === 3)
|
|
43
|
+
normalized += '=';
|
|
44
|
+
const key = Buffer.from(normalized, 'base64');
|
|
45
|
+
if (key.length === 16)
|
|
46
|
+
return key;
|
|
47
|
+
// 4. Official iLink compat: base64(hex-string) — 32 ASCII hex chars → 16 bytes
|
|
48
|
+
if (key.length === 32) {
|
|
49
|
+
const hexStr = key.toString('ascii');
|
|
50
|
+
if (/^[0-9a-f]{32}$/i.test(hexStr)) {
|
|
51
|
+
return Buffer.from(hexStr, 'hex');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
log?.warn({ aesKeyLen: aesKey.length, decodedLen: key.length }, '[weixin-cdn] Invalid AES key length after decode');
|
|
55
|
+
throw new Error(`Invalid AES key: decoded ${key.length} bytes from ${aesKey.length}-char string (expected 16)`);
|
|
56
|
+
}
|
|
57
|
+
// ── CDN Download Pipeline ──
|
|
58
|
+
function resolveWeChatCdnFullUrl(fullUrl, fieldName) {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = new URL(fullUrl);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error(`[weixin-cdn] Invalid ${fieldName}`);
|
|
65
|
+
}
|
|
66
|
+
const host = parsed.hostname.toLowerCase();
|
|
67
|
+
const isWeChatCdnHost = host === 'cdn.weixin.qq.com' ? true : host.endsWith('.cdn.weixin.qq.com');
|
|
68
|
+
if (parsed.protocol !== 'https:') {
|
|
69
|
+
throw new Error(`[weixin-cdn] Refusing ${fieldName} from non-WeChat CDN host: ${host}`);
|
|
70
|
+
}
|
|
71
|
+
if (!isWeChatCdnHost) {
|
|
72
|
+
throw new Error(`[weixin-cdn] Refusing ${fieldName} from non-WeChat CDN host: ${host}`);
|
|
73
|
+
}
|
|
74
|
+
return parsed.toString();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Download encrypted media from WeChat CDN and decrypt.
|
|
78
|
+
* `platformKey` is a JSON-encoded string:
|
|
79
|
+
* - { encryptQueryParam, aesKey } for legacy CDN download API
|
|
80
|
+
* - { fullUrl, aesKey } for iLink full_url responses
|
|
81
|
+
*/
|
|
82
|
+
export async function downloadMediaFromCdn(params) {
|
|
83
|
+
const { platformKey, cdnBaseUrl, log, fetchFn = globalThis.fetch } = params;
|
|
84
|
+
const { encryptQueryParam, fullUrl, aesKey } = JSON.parse(platformKey);
|
|
85
|
+
if (!aesKey) {
|
|
86
|
+
throw new Error('[weixin-cdn] CDN platformKey missing aesKey');
|
|
87
|
+
}
|
|
88
|
+
const cdnUrl = fullUrl
|
|
89
|
+
? resolveWeChatCdnFullUrl(fullUrl, 'full_url')
|
|
90
|
+
: encryptQueryParam
|
|
91
|
+
? `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptQueryParam)}`
|
|
92
|
+
: '';
|
|
93
|
+
if (!cdnUrl) {
|
|
94
|
+
throw new Error('[weixin-cdn] CDN platformKey missing encryptQueryParam or fullUrl');
|
|
95
|
+
}
|
|
96
|
+
log.info({ source: fullUrl ? 'full_url' : 'encrypted_query_param' }, '[weixin-cdn] Downloading media from CDN');
|
|
97
|
+
const { response: res, bytes: ciphertext } = await fetchBoundedInboundMedia(fetchFn, cdnUrl, {
|
|
98
|
+
method: 'GET',
|
|
99
|
+
});
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
throw new Error(`CDN download HTTP ${res.status}`);
|
|
102
|
+
}
|
|
103
|
+
const key = decodeAesKey(aesKey, log);
|
|
104
|
+
const plaintext = decryptAesEcb(ciphertext, key);
|
|
105
|
+
log.info({ ciphertextLen: ciphertext.length, plaintextLen: plaintext.length }, '[weixin-cdn] Media decrypted');
|
|
106
|
+
return plaintext;
|
|
107
|
+
}
|
|
108
|
+
// ── CDN Upload Pipeline ──
|
|
109
|
+
export async function uploadMediaToCdn(params) {
|
|
110
|
+
const { filePath, toUserId, mediaType, botToken, cdnBaseUrl, log, fetchFn = globalThis.fetch } = params;
|
|
111
|
+
const plaintext = await readFile(filePath);
|
|
112
|
+
const rawsize = plaintext.length;
|
|
113
|
+
const rawfilemd5 = createHash('md5').update(plaintext).digest('hex');
|
|
114
|
+
const filesize = aesEcbPaddedSize(rawsize);
|
|
115
|
+
const filekey = randomBytes(16).toString('hex');
|
|
116
|
+
const aeskey = randomBytes(16);
|
|
117
|
+
log.info({ filePath: basename(filePath), rawsize, filesize, mediaType }, '[weixin-cdn] Uploading media');
|
|
118
|
+
const uploadUrlResp = await callGetUploadUrl({
|
|
119
|
+
filekey,
|
|
120
|
+
mediaType,
|
|
121
|
+
toUserId,
|
|
122
|
+
rawsize,
|
|
123
|
+
rawfilemd5,
|
|
124
|
+
filesize,
|
|
125
|
+
aeskey: aeskey.toString('hex'),
|
|
126
|
+
botToken,
|
|
127
|
+
fetchFn,
|
|
128
|
+
});
|
|
129
|
+
if (!uploadUrlResp.upload_param && !uploadUrlResp.upload_full_url) {
|
|
130
|
+
throw new Error('[weixin-cdn] getUploadUrl returned no upload_param/upload_full_url');
|
|
131
|
+
}
|
|
132
|
+
const downloadParam = await uploadBufferToCdn({
|
|
133
|
+
buf: plaintext,
|
|
134
|
+
uploadParam: uploadUrlResp.upload_param,
|
|
135
|
+
uploadFullUrl: uploadUrlResp.upload_full_url,
|
|
136
|
+
filekey,
|
|
137
|
+
cdnBaseUrl,
|
|
138
|
+
aeskey,
|
|
139
|
+
log,
|
|
140
|
+
fetchFn,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
filekey,
|
|
144
|
+
downloadEncryptedQueryParam: downloadParam,
|
|
145
|
+
aeskey: aeskey.toString('hex'),
|
|
146
|
+
fileSize: rawsize,
|
|
147
|
+
fileSizeCiphertext: filesize,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// ── Internal API calls ──
|
|
151
|
+
function getHeaders(botToken) {
|
|
152
|
+
const uin = Buffer.from(String(Math.floor(Math.random() * 0xffffffff))).toString('base64');
|
|
153
|
+
return {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
AuthorizationType: 'ilink_bot_token',
|
|
156
|
+
Authorization: `Bearer ${botToken}`,
|
|
157
|
+
'X-WECHAT-UIN': uin,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function callGetUploadUrl(params) {
|
|
161
|
+
const body = JSON.stringify({
|
|
162
|
+
filekey: params.filekey,
|
|
163
|
+
media_type: params.mediaType,
|
|
164
|
+
to_user_id: params.toUserId,
|
|
165
|
+
rawsize: params.rawsize,
|
|
166
|
+
rawfilemd5: params.rawfilemd5,
|
|
167
|
+
filesize: params.filesize,
|
|
168
|
+
no_need_thumb: true,
|
|
169
|
+
aeskey: params.aeskey,
|
|
170
|
+
base_info: { channel_version: '1.0.0' },
|
|
171
|
+
});
|
|
172
|
+
const res = await params.fetchFn(`${ILINK_BASE_URL}/ilink/bot/getuploadurl`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: getHeaders(params.botToken),
|
|
175
|
+
body,
|
|
176
|
+
signal: AbortSignal.timeout(15_000),
|
|
177
|
+
});
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
const text = await res.text().catch(() => '');
|
|
180
|
+
throw new Error(`getuploadurl HTTP ${res.status}: ${text}`);
|
|
181
|
+
}
|
|
182
|
+
return (await res.json());
|
|
183
|
+
}
|
|
184
|
+
async function uploadBufferToCdn(params) {
|
|
185
|
+
const { buf, uploadParam, uploadFullUrl, filekey, cdnBaseUrl, aeskey, log, fetchFn } = params;
|
|
186
|
+
const ciphertext = encryptAesEcb(buf, aeskey);
|
|
187
|
+
const cdnUrl = uploadFullUrl
|
|
188
|
+
? resolveWeChatCdnFullUrl(uploadFullUrl, 'upload_full_url')
|
|
189
|
+
: uploadParam
|
|
190
|
+
? `${cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`
|
|
191
|
+
: '';
|
|
192
|
+
if (!cdnUrl) {
|
|
193
|
+
throw new Error('[weixin-cdn] Missing upload_param/upload_full_url for CDN upload');
|
|
194
|
+
}
|
|
195
|
+
for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt++) {
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetchFn(cdnUrl, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
200
|
+
body: new Uint8Array(ciphertext),
|
|
201
|
+
});
|
|
202
|
+
if (res.status >= 400 && res.status < 500) {
|
|
203
|
+
const msg = res.headers.get('x-error-message') ?? (await res.text());
|
|
204
|
+
throw new Error(`CDN client error ${res.status}: ${msg}`);
|
|
205
|
+
}
|
|
206
|
+
if (res.status !== 200) {
|
|
207
|
+
throw new Error(`CDN server error ${res.status}`);
|
|
208
|
+
}
|
|
209
|
+
const downloadParam = res.headers.get('x-encrypted-param');
|
|
210
|
+
if (!downloadParam) {
|
|
211
|
+
throw new Error('CDN response missing x-encrypted-param header');
|
|
212
|
+
}
|
|
213
|
+
log.info({ attempt, filekey }, '[weixin-cdn] CDN upload success');
|
|
214
|
+
return downloadParam;
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
if (err instanceof Error && err.message.includes('client error'))
|
|
218
|
+
throw err;
|
|
219
|
+
if (attempt === UPLOAD_MAX_RETRIES)
|
|
220
|
+
throw err;
|
|
221
|
+
log.warn({ attempt, err: String(err) }, '[weixin-cdn] CDN upload retry');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts`);
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=weixin-cdn.js.map
|