@krischoichoi/channel-weixin 0.6.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 +101 -0
- package/lib/adapter.d.ts +90 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/adapter.js +247 -0
- package/lib/adapter.js.map +1 -0
- package/lib/auth/account-store.d.ts +60 -0
- package/lib/auth/account-store.d.ts.map +1 -0
- package/lib/auth/account-store.js +84 -0
- package/lib/auth/account-store.js.map +1 -0
- package/lib/auth/login.d.ts +83 -0
- package/lib/auth/login.d.ts.map +1 -0
- package/lib/auth/login.js +202 -0
- package/lib/auth/login.js.map +1 -0
- package/lib/config.d.ts +52 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +36 -0
- package/lib/config.js.map +1 -0
- package/lib/definition.d.ts +59 -0
- package/lib/definition.d.ts.map +1 -0
- package/lib/definition.js +146 -0
- package/lib/definition.js.map +1 -0
- package/lib/ilink/base-info.d.ts +22 -0
- package/lib/ilink/base-info.d.ts.map +1 -0
- package/lib/ilink/base-info.js +11 -0
- package/lib/ilink/base-info.js.map +1 -0
- package/lib/ilink/client.d.ts +110 -0
- package/lib/ilink/client.d.ts.map +1 -0
- package/lib/ilink/client.js +308 -0
- package/lib/ilink/client.js.map +1 -0
- package/lib/ilink/constants.d.ts +59 -0
- package/lib/ilink/constants.d.ts.map +1 -0
- package/lib/ilink/constants.js +59 -0
- package/lib/ilink/constants.js.map +1 -0
- package/lib/ilink/errors.d.ts +79 -0
- package/lib/ilink/errors.d.ts.map +1 -0
- package/lib/ilink/errors.js +121 -0
- package/lib/ilink/errors.js.map +1 -0
- package/lib/ilink/headers.d.ts +40 -0
- package/lib/ilink/headers.d.ts.map +1 -0
- package/lib/ilink/headers.js +70 -0
- package/lib/ilink/headers.js.map +1 -0
- package/lib/ilink/schema.d.ts +154 -0
- package/lib/ilink/schema.d.ts.map +1 -0
- package/lib/ilink/schema.js +140 -0
- package/lib/ilink/schema.js.map +1 -0
- package/lib/ilink/types.d.ts +235 -0
- package/lib/ilink/types.d.ts.map +1 -0
- package/lib/ilink/types.js +21 -0
- package/lib/ilink/types.js.map +1 -0
- package/lib/index.d.ts +50 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +91 -0
- package/lib/index.js.map +1 -0
- package/lib/manifest.d.ts +54 -0
- package/lib/manifest.d.ts.map +1 -0
- package/lib/manifest.js +32 -0
- package/lib/manifest.js.map +1 -0
- package/lib/media/decrypt.d.ts +3 -0
- package/lib/media/decrypt.d.ts.map +1 -0
- package/lib/media/decrypt.js +16 -0
- package/lib/media/decrypt.js.map +1 -0
- package/lib/media/download.d.ts +22 -0
- package/lib/media/download.d.ts.map +1 -0
- package/lib/media/download.js +62 -0
- package/lib/media/download.js.map +1 -0
- package/lib/media/encrypt.d.ts +3 -0
- package/lib/media/encrypt.d.ts.map +1 -0
- package/lib/media/encrypt.js +16 -0
- package/lib/media/encrypt.js.map +1 -0
- package/lib/media/send-media.d.ts +41 -0
- package/lib/media/send-media.d.ts.map +1 -0
- package/lib/media/send-media.js +52 -0
- package/lib/media/send-media.js.map +1 -0
- package/lib/media/silk-transcode.d.ts +38 -0
- package/lib/media/silk-transcode.d.ts.map +1 -0
- package/lib/media/silk-transcode.js +88 -0
- package/lib/media/silk-transcode.js.map +1 -0
- package/lib/media/upload.d.ts +38 -0
- package/lib/media/upload.d.ts.map +1 -0
- package/lib/media/upload.js +122 -0
- package/lib/media/upload.js.map +1 -0
- package/lib/messaging/dedup.d.ts +87 -0
- package/lib/messaging/dedup.d.ts.map +1 -0
- package/lib/messaging/dedup.js +127 -0
- package/lib/messaging/dedup.js.map +1 -0
- package/lib/messaging/mapper.d.ts +26 -0
- package/lib/messaging/mapper.d.ts.map +1 -0
- package/lib/messaging/mapper.js +107 -0
- package/lib/messaging/mapper.js.map +1 -0
- package/lib/messaging/monitor.d.ts +109 -0
- package/lib/messaging/monitor.d.ts.map +1 -0
- package/lib/messaging/monitor.js +234 -0
- package/lib/messaging/monitor.js.map +1 -0
- package/lib/messaging/quote.d.ts +9 -0
- package/lib/messaging/quote.d.ts.map +1 -0
- package/lib/messaging/quote.js +35 -0
- package/lib/messaging/quote.js.map +1 -0
- package/lib/messaging/send.d.ts +54 -0
- package/lib/messaging/send.d.ts.map +1 -0
- package/lib/messaging/send.js +173 -0
- package/lib/messaging/send.js.map +1 -0
- package/lib/messaging/typing.d.ts +34 -0
- package/lib/messaging/typing.d.ts.map +1 -0
- package/lib/messaging/typing.js +56 -0
- package/lib/messaging/typing.js.map +1 -0
- package/lib/storage/context-token.d.ts +26 -0
- package/lib/storage/context-token.d.ts.map +1 -0
- package/lib/storage/context-token.js +26 -0
- package/lib/storage/context-token.js.map +1 -0
- package/lib/storage/sync-cursor.d.ts +27 -0
- package/lib/storage/sync-cursor.d.ts.map +1 -0
- package/lib/storage/sync-cursor.js +25 -0
- package/lib/storage/sync-cursor.js.map +1 -0
- package/lib/transport.d.ts +38 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +68 -0
- package/lib/transport.js.map +1 -0
- package/lib/upstream/compat.d.ts +54 -0
- package/lib/upstream/compat.d.ts.map +1 -0
- package/lib/upstream/compat.js +54 -0
- package/lib/upstream/compat.js.map +1 -0
- package/lib/upstream/config-manager.d.ts +35 -0
- package/lib/upstream/config-manager.d.ts.map +1 -0
- package/lib/upstream/config-manager.js +91 -0
- package/lib/upstream/config-manager.js.map +1 -0
- package/lib/upstream/index.d.ts +42 -0
- package/lib/upstream/index.d.ts.map +1 -0
- package/lib/upstream/index.js +13 -0
- package/lib/upstream/index.js.map +1 -0
- package/lib/upstream/port.d.ts +153 -0
- package/lib/upstream/port.d.ts.map +1 -0
- package/lib/upstream/port.js +15 -0
- package/lib/upstream/port.js.map +1 -0
- package/lib/upstream/tencent-upstream.d.ts +116 -0
- package/lib/upstream/tencent-upstream.d.ts.map +1 -0
- package/lib/upstream/tencent-upstream.js +518 -0
- package/lib/upstream/tencent-upstream.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @krischoichoi/channel-weixin
|
|
2
|
+
|
|
3
|
+
Weixin / 微信 channel adapter for DeepSeek Harness.
|
|
4
|
+
|
|
5
|
+
Direct Tencent Weixin iLink integration (QR login, long-poll receive, text/image
|
|
6
|
+
send). This adapter is an upstream-gap source-port: it speaks the iLink
|
|
7
|
+
protocol directly rather than consuming an official host-neutral npm package.
|
|
8
|
+
|
|
9
|
+
> **Status:** `experimental` — real-Weixin live verification is still pending.
|
|
10
|
+
> The adapter is fully implemented and offline-tested, but it must not claim
|
|
11
|
+
> `tested` until the live platform gate passes.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @krischoichoi/channel-weixin
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install the whole bundle:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @deepseek-ai/dsh plugin --profile web add -w @krischoichoi/dsh-channels@latest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Weixin has no credential or setup-form fields; the only configurable values are
|
|
28
|
+
network/reconnect tuning (all with defaults). Login happens on the mounted
|
|
29
|
+
adapter, not through YAML.
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
- id: channels-weixin
|
|
33
|
+
name: '@krischoichoi/dsh-channels/weixin'
|
|
34
|
+
inject: [channels, channelControl]
|
|
35
|
+
config:
|
|
36
|
+
enabled: true
|
|
37
|
+
accountId: main
|
|
38
|
+
ilink:
|
|
39
|
+
baseUrl: https://ilinkai.weixin.qq.com # default iLink API base
|
|
40
|
+
cdnBaseUrl: https://novac2c.cdn.weixin.qq.com/c2c # default CDN base
|
|
41
|
+
network:
|
|
42
|
+
timeoutMs: 15000
|
|
43
|
+
longPollTimeoutMs: 35000
|
|
44
|
+
reconnect:
|
|
45
|
+
enabled: true
|
|
46
|
+
baseDelayMs: 2000
|
|
47
|
+
maxDelayMs: 30000
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The row above is the one shipped by the bundle patch
|
|
51
|
+
(`packages/channels/cordis.patch.yml`). Two details are easy to get wrong:
|
|
52
|
+
|
|
53
|
+
- `name` addresses the **bundle** subpath (`@krischoichoi/dsh-channels/weixin`), because
|
|
54
|
+
the bundle re-exports every plugin from one direct profile dependency. The
|
|
55
|
+
adapter package `@krischoichoi/channel-weixin` has no `./weixin` subpath of its own.
|
|
56
|
+
- The plugin module itself declares `inject = ['channels']`; the row adds
|
|
57
|
+
`channelControl`. The loader merges the row's `inject` into the plugin's own,
|
|
58
|
+
so the two are additive rather than required to be identical.
|
|
59
|
+
|
|
60
|
+
Login is QR-based: the control plane exposes the QR in Harness Web
|
|
61
|
+
「设置 → 渠道」, or the adapter can be driven headlessly through
|
|
62
|
+
`beginAuth` / `pollAuth` / `submitAuthInput`.
|
|
63
|
+
|
|
64
|
+
## Capabilities
|
|
65
|
+
|
|
66
|
+
| Capability | Value |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| text | ✅ |
|
|
69
|
+
| image | ✅ (Harness-native `saveImage` / `ImageBlock`) |
|
|
70
|
+
| file | ✅ (inbound download + AES decrypt; outbound `uploadMedia` + `sendMedia`) |
|
|
71
|
+
| video | ✅ (inbound download; outbound `sendVideo`) |
|
|
72
|
+
| audio | ❌ (inbound `localData` only; no Tencent 2.4.6 outbound voice path) |
|
|
73
|
+
| markdown / cards / reactions / threads | ❌ |
|
|
74
|
+
| streaming | `buffered` |
|
|
75
|
+
|
|
76
|
+
## Upstream
|
|
77
|
+
|
|
78
|
+
| Field | Value |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| Reference | `Tencent/openclaw-weixin` |
|
|
81
|
+
| Strategy | `source-port` (iLink protocol) |
|
|
82
|
+
| Protocol | `weixin-ilink` |
|
|
83
|
+
| Official package | `@tencent-weixin/openclaw-weixin` |
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pnpm --filter @krischoichoi/channel-weixin build
|
|
89
|
+
pnpm --filter @krischoichoi/channel-weixin typecheck
|
|
90
|
+
pnpm --filter @krischoichoi/channel-weixin test
|
|
91
|
+
pnpm --filter @krischoichoi/channel-weixin test:live # live verification (requires real Weixin)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Related
|
|
95
|
+
|
|
96
|
+
- [Repository root](../../README.md)
|
|
97
|
+
- [Weixin live verification runbook](../../docs/weixin-live-verification-runbook.md)
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
[MIT](../../LICENSE)
|
package/lib/adapter.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeixinChannelAdapter — maps the Weixin upstream facade to the stable Channel
|
|
3
|
+
* Contract.
|
|
4
|
+
*
|
|
5
|
+
* This adapter holds ONLY the Channel-contract lifecycle + credential wiring:
|
|
6
|
+
* - start(): wired the upstream host env (secrets/storage/ctx) -> load
|
|
7
|
+
* credential -> unauthenticated if missing; else start the monitor.
|
|
8
|
+
* - send(): map an OutboundMessage to upstream.sendText / sendImage.
|
|
9
|
+
* - beginAuth/pollAuth/submitVerifyCode: QR login via the upstream facade.
|
|
10
|
+
* - stop(): stop the monitor.
|
|
11
|
+
*
|
|
12
|
+
* Endpoint knowledge (iLink endpoints, AES, getuploadurl, CDN query assembly)
|
|
13
|
+
* lives inside the upstream facade layer — this adapter NEVER touches AES or
|
|
14
|
+
* getuploadurl details (plan DoD #1 / §78). The upstream is the
|
|
15
|
+
* `WeixinUpstream` port from `./upstream`.
|
|
16
|
+
*/
|
|
17
|
+
import type { AuthChallenge, AuthInput, AuthStatePoll, ChannelAdapter, ChannelAdapterContext, ChannelCapabilities, ChannelHealth, ChannelTarget, OutboundMessage, SendResult } from '@krischoichoi/channel-core';
|
|
18
|
+
import type { WeixinConfig } from './config.js';
|
|
19
|
+
import type { HttpTransport } from './transport.js';
|
|
20
|
+
import type { WeixinUpstream } from './upstream/port.js';
|
|
21
|
+
import { type WeixinManifest } from './manifest.js';
|
|
22
|
+
export interface WeixinAdapterDeps {
|
|
23
|
+
/** Injectable transport (tests); defaults to FetchTransport. */
|
|
24
|
+
transport?: HttpTransport;
|
|
25
|
+
/** Injectable clock (tests). */
|
|
26
|
+
now?: () => number;
|
|
27
|
+
/** Injectable random source for X-WECHAT-UIN (tests). */
|
|
28
|
+
rand?: () => number;
|
|
29
|
+
/**
|
|
30
|
+
* Injectable upstream (tests). Defaults to the real
|
|
31
|
+
* [createWeixinUpstream]. When supplied, the adapter drives this fake as its
|
|
32
|
+
* protocol boundary.
|
|
33
|
+
*/
|
|
34
|
+
upstream?: AdapterUpstreamLike;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The upstream surface the adapter actually consumes. Narrower than the full
|
|
38
|
+
* port to keep the fake easy to construct in tests.
|
|
39
|
+
*/
|
|
40
|
+
export type AdapterUpstreamLike = (Pick<WeixinUpstream, 'id' | 'beginQrAuth' | 'pollQrAuth' | 'submitVerifyCode' | 'sendText' | 'sendImage' | 'sendFile' | 'sendVideo' | 'downloadImage' | 'downloadFile' | 'downloadAudio' | 'downloadVideo' | 'hasCredential'> & {
|
|
41
|
+
bind(env: import('./upstream/port.js').WeixinUpstreamHostEnv): void;
|
|
42
|
+
loadCredential(): Promise<boolean>;
|
|
43
|
+
startMonitor(): Promise<void>;
|
|
44
|
+
stopMonitor(): Promise<void>;
|
|
45
|
+
startTyping(peer: string): Promise<void>;
|
|
46
|
+
stopTyping(peer: string): Promise<void>;
|
|
47
|
+
getHealthInfo(): Promise<{
|
|
48
|
+
authenticated: boolean;
|
|
49
|
+
connection: string;
|
|
50
|
+
}>;
|
|
51
|
+
get_iLinkClient(): unknown;
|
|
52
|
+
});
|
|
53
|
+
export declare class WeixinAdapter implements ChannelAdapter {
|
|
54
|
+
readonly id = "weixin";
|
|
55
|
+
/** Upstream compatibility manifest (read structurally by `channels doctor`). */
|
|
56
|
+
readonly manifest: WeixinManifest;
|
|
57
|
+
readonly capabilities: ChannelCapabilities;
|
|
58
|
+
private readonly config;
|
|
59
|
+
private readonly upstream;
|
|
60
|
+
private ctx?;
|
|
61
|
+
private started;
|
|
62
|
+
private connected;
|
|
63
|
+
private readonly startedAt;
|
|
64
|
+
constructor(config: WeixinConfig, deps?: WeixinAdapterDeps);
|
|
65
|
+
private requireStarted;
|
|
66
|
+
start(ctx: ChannelAdapterContext): Promise<void>;
|
|
67
|
+
/** Start the getUpdates monitor with the currently-wired credential. */
|
|
68
|
+
private startMonitor;
|
|
69
|
+
stop(): Promise<void>;
|
|
70
|
+
send(target: ChannelTarget, message: OutboundMessage): Promise<SendResult>;
|
|
71
|
+
/** Begin QR login and return the Channel Contract AuthChallenge. */
|
|
72
|
+
beginAuth(): Promise<AuthChallenge>;
|
|
73
|
+
/** Poll the active QR challenge. */
|
|
74
|
+
pollAuth(challenge: AuthChallenge): Promise<AuthStatePoll>;
|
|
75
|
+
/** Submit a phone-verify code for the active QR challenge (adapter-specific). */
|
|
76
|
+
submitVerifyCode(code: string): void;
|
|
77
|
+
/** Generic auth-input hook: maps verification-code inputs to submitVerifyCode. */
|
|
78
|
+
submitAuthInput(_challenge: AuthChallenge, input: AuthInput): void;
|
|
79
|
+
getHealth(): Promise<ChannelHealth>;
|
|
80
|
+
/** Expose the underlying upstream (tests introspection). */
|
|
81
|
+
get_iLinkClient(): unknown;
|
|
82
|
+
/** Best-effort typing start (WX6). Failures never break the main flow. */
|
|
83
|
+
startTyping(conversationId: string): Promise<void>;
|
|
84
|
+
/** Best-effort typing stop (WX6). Failures never break the main flow. */
|
|
85
|
+
stopTyping(conversationId: string): Promise<void>;
|
|
86
|
+
private requireStartedForAuth;
|
|
87
|
+
private emitConnection;
|
|
88
|
+
private onConnectionChange;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/lib/adapter.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { ChannelError, collectText } from '@krischoichoi/channel-core';
|
|
2
|
+
import { redactMessage } from './ilink/errors.js';
|
|
3
|
+
import { createWeixinUpstream } from './upstream/index.js';
|
|
4
|
+
import { manifest as weixinManifest } from './manifest.js';
|
|
5
|
+
export class WeixinAdapter {
|
|
6
|
+
id = 'weixin';
|
|
7
|
+
/** Upstream compatibility manifest (read structurally by `channels doctor`). */
|
|
8
|
+
manifest = weixinManifest;
|
|
9
|
+
capabilities = {
|
|
10
|
+
text: true,
|
|
11
|
+
image: true,
|
|
12
|
+
file: true,
|
|
13
|
+
audio: false,
|
|
14
|
+
video: true,
|
|
15
|
+
markdown: false,
|
|
16
|
+
cards: false,
|
|
17
|
+
reactions: false,
|
|
18
|
+
threads: false,
|
|
19
|
+
streaming: 'buffered',
|
|
20
|
+
// Directional per-kind media precision.
|
|
21
|
+
// CODE-CONFIRMED:
|
|
22
|
+
// inbound — TencentWeixinUpstream.enrichInboundMedia downloads + AES
|
|
23
|
+
// decrypts image/file/video and download+decrypt+transcodeSilkVoice for
|
|
24
|
+
// audio, setting localData before the monitor emits; failures keep the
|
|
25
|
+
// part with `ingressFailure`.
|
|
26
|
+
// outbound — concrete sendImage/sendFile/sendVideo (uploadMedia +
|
|
27
|
+
// sendMedia over iLink 2.4.6) upload real bytes; audio has no Tencent
|
|
28
|
+
// 2.4.6 outbound path (adapter.send() rejects voice parts with
|
|
29
|
+
// CHANNEL_UNSUPPORTED), so it is `unsupported`.
|
|
30
|
+
// Legacy booleans above stay unchanged (coarse flags).
|
|
31
|
+
media: {
|
|
32
|
+
inbound: {
|
|
33
|
+
image: 'bytes',
|
|
34
|
+
file: 'bytes',
|
|
35
|
+
audio: 'bytes',
|
|
36
|
+
video: 'bytes',
|
|
37
|
+
},
|
|
38
|
+
outbound: {
|
|
39
|
+
image: 'bytes',
|
|
40
|
+
file: 'bytes',
|
|
41
|
+
video: 'bytes',
|
|
42
|
+
audio: 'unsupported',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
config;
|
|
47
|
+
upstream;
|
|
48
|
+
ctx;
|
|
49
|
+
started = false;
|
|
50
|
+
connected = false;
|
|
51
|
+
startedAt;
|
|
52
|
+
constructor(config, deps = {}) {
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.startedAt = deps.now ? deps.now() : Date.now();
|
|
55
|
+
this.upstream = deps.upstream ?? createWeixinUpstream({
|
|
56
|
+
config,
|
|
57
|
+
transport: deps.transport,
|
|
58
|
+
now: deps.now,
|
|
59
|
+
rand: deps.rand,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
requireStarted() {
|
|
63
|
+
if (!this.started || !this.ctx) {
|
|
64
|
+
throw new ChannelError('CHANNEL_NOT_STARTED', 'weixin adapter is not started');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async start(ctx) {
|
|
68
|
+
if (this.started)
|
|
69
|
+
return;
|
|
70
|
+
this.ctx = ctx;
|
|
71
|
+
this.upstream.bind({
|
|
72
|
+
ctx,
|
|
73
|
+
secrets: ctx.secrets,
|
|
74
|
+
storage: ctx.storage,
|
|
75
|
+
onConnectionChange: (state) => this.onConnectionChange(state),
|
|
76
|
+
});
|
|
77
|
+
// Unauthenticated: allow beginAuth/pollAuth flow to configure later.
|
|
78
|
+
const hasCredential = await this.upstream.loadCredential();
|
|
79
|
+
if (!hasCredential) {
|
|
80
|
+
this.started = true;
|
|
81
|
+
this.emitConnection('disconnected');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await this.startMonitor(ctx);
|
|
85
|
+
this.started = true;
|
|
86
|
+
}
|
|
87
|
+
/** Start the getUpdates monitor with the currently-wired credential. */
|
|
88
|
+
async startMonitor(ctx) {
|
|
89
|
+
await this.upstream.startMonitor();
|
|
90
|
+
this.connected = true;
|
|
91
|
+
}
|
|
92
|
+
async stop() {
|
|
93
|
+
if (!this.started)
|
|
94
|
+
return;
|
|
95
|
+
await this.upstream.stopMonitor();
|
|
96
|
+
this.started = false;
|
|
97
|
+
this.connected = false;
|
|
98
|
+
}
|
|
99
|
+
async send(target, message) {
|
|
100
|
+
this.requireStarted();
|
|
101
|
+
const to = target.conversationId;
|
|
102
|
+
const runId = target.runId;
|
|
103
|
+
const media = outboundMediaParts(message.parts);
|
|
104
|
+
try {
|
|
105
|
+
if (hasUnsupportedMediaPart(message.parts)) {
|
|
106
|
+
throw new ChannelError('CHANNEL_UNSUPPORTED', 'weixin outbound media requires local bytes; voice is not supported');
|
|
107
|
+
}
|
|
108
|
+
const text = message.text ?? collectText(message.parts ?? []);
|
|
109
|
+
let result;
|
|
110
|
+
if (media.length > 0 && text) {
|
|
111
|
+
result = await this.upstream.sendText({ to, runId, text });
|
|
112
|
+
}
|
|
113
|
+
for (const part of media) {
|
|
114
|
+
if (part.type === 'image') {
|
|
115
|
+
result = await this.upstream.sendImage({
|
|
116
|
+
to,
|
|
117
|
+
runId,
|
|
118
|
+
data: part.localData,
|
|
119
|
+
mimeType: part.mimeType,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else if (part.type === 'file') {
|
|
123
|
+
result = await this.upstream.sendFile({
|
|
124
|
+
to,
|
|
125
|
+
runId,
|
|
126
|
+
data: part.localData,
|
|
127
|
+
fileName: part.name ?? 'attachment',
|
|
128
|
+
mimeType: part.mimeType,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
result = await this.upstream.sendVideo({
|
|
133
|
+
to,
|
|
134
|
+
runId,
|
|
135
|
+
data: part.localData,
|
|
136
|
+
mimeType: part.mimeType,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result ?? this.upstream.sendText({ to, runId, text });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
144
|
+
throw new ChannelError('CHANNEL_SEND_FAILED', redactMessage(messageText), { cause: error });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Begin QR login and return the Channel Contract AuthChallenge. */
|
|
148
|
+
async beginAuth() {
|
|
149
|
+
this.requireStartedForAuth();
|
|
150
|
+
const ticket = await this.upstream.beginQrAuth();
|
|
151
|
+
return {
|
|
152
|
+
id: ticket.id,
|
|
153
|
+
instruction: ticket.instruction ?? '请使用微信扫描二维码',
|
|
154
|
+
qrUrl: ticket.qrUrl,
|
|
155
|
+
expiresAt: ticket.expiresAt,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Poll the active QR challenge. */
|
|
159
|
+
async pollAuth(challenge) {
|
|
160
|
+
this.requireStartedForAuth();
|
|
161
|
+
const poll = await this.upstream.pollQrAuth(challenge.id);
|
|
162
|
+
if (poll.state === 'authenticated') {
|
|
163
|
+
// Persist the fresh credential on the upstream + (re)wire the sender.
|
|
164
|
+
return { state: 'authenticated', detail: poll.detail };
|
|
165
|
+
}
|
|
166
|
+
return { state: poll.state, detail: poll.detail };
|
|
167
|
+
}
|
|
168
|
+
/** Submit a phone-verify code for the active QR challenge (adapter-specific). */
|
|
169
|
+
submitVerifyCode(code) {
|
|
170
|
+
this.requireStartedForAuth();
|
|
171
|
+
this.upstream.submitVerifyCode(code);
|
|
172
|
+
}
|
|
173
|
+
/** Generic auth-input hook: maps verification-code inputs to submitVerifyCode. */
|
|
174
|
+
submitAuthInput(_challenge, input) {
|
|
175
|
+
if (input.kind === 'verification-code') {
|
|
176
|
+
this.submitVerifyCode(input.value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async getHealth() {
|
|
180
|
+
if (!this.started) {
|
|
181
|
+
return { status: 'down', detail: 'weixin adapter is not started', authenticated: false };
|
|
182
|
+
}
|
|
183
|
+
const info = await this.upstream.getHealthInfo();
|
|
184
|
+
const { authenticated } = info;
|
|
185
|
+
if (!authenticated) {
|
|
186
|
+
return { status: 'down', detail: 'weixin not authenticated', connection: 'disconnected', authenticated: false };
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
status: this.connected ? 'ok' : 'degraded',
|
|
190
|
+
detail: this.connected ? 'connected' : 'authenticated but receive loop down',
|
|
191
|
+
connection: this.connected ? 'connected' : 'disconnected',
|
|
192
|
+
authenticated: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** Expose the underlying upstream (tests introspection). */
|
|
196
|
+
get_iLinkClient() {
|
|
197
|
+
return this.upstream.get_iLinkClient();
|
|
198
|
+
}
|
|
199
|
+
/** Best-effort typing start (WX6). Failures never break the main flow. */
|
|
200
|
+
async startTyping(conversationId) {
|
|
201
|
+
await this.upstream.startTyping(conversationId);
|
|
202
|
+
}
|
|
203
|
+
/** Best-effort typing stop (WX6). Failures never break the main flow. */
|
|
204
|
+
async stopTyping(conversationId) {
|
|
205
|
+
await this.upstream.stopTyping(conversationId);
|
|
206
|
+
}
|
|
207
|
+
requireStartedForAuth() {
|
|
208
|
+
if (!this.ctx) {
|
|
209
|
+
throw new ChannelError('CHANNEL_NOT_STARTED', 'weixin adapter is not started');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
emitConnection(state) {
|
|
213
|
+
if (!this.ctx)
|
|
214
|
+
return;
|
|
215
|
+
void this.ctx
|
|
216
|
+
.emit({
|
|
217
|
+
type: 'connection.changed',
|
|
218
|
+
channel: this.id,
|
|
219
|
+
accountId: this.config.accountId,
|
|
220
|
+
state,
|
|
221
|
+
})
|
|
222
|
+
.catch(() => undefined);
|
|
223
|
+
}
|
|
224
|
+
onConnectionChange(state) {
|
|
225
|
+
this.connected = state === 'connected';
|
|
226
|
+
this.emitConnection(state);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function outboundMediaParts(parts) {
|
|
230
|
+
if (!parts)
|
|
231
|
+
return [];
|
|
232
|
+
return parts.filter((part) => (part.type === 'image' || part.type === 'file' || part.type === 'video') &&
|
|
233
|
+
part.localData !== undefined &&
|
|
234
|
+
part.localData.byteLength > 0);
|
|
235
|
+
}
|
|
236
|
+
/** True when the message carries unsupported or non-uploadable media. */
|
|
237
|
+
function hasUnsupportedMediaPart(parts) {
|
|
238
|
+
if (!parts)
|
|
239
|
+
return false;
|
|
240
|
+
return parts.some((p) => p.type === 'audio' ||
|
|
241
|
+
((p.type === 'image' || p.type === 'file' || p.type === 'video') && (!p.localData || p.localData.byteLength === 0)));
|
|
242
|
+
}
|
|
243
|
+
/** Minimal token redaction for send-failure messages (no pattern dependency here). */
|
|
244
|
+
function redactSafe(message) {
|
|
245
|
+
return message.replace(/(token|authorization|aes_key|context_token)/gi, '<redacted>');
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential store for Weixin accounts.
|
|
3
|
+
*
|
|
4
|
+
* Splits the credential across the two storage boundaries the channel runtime
|
|
5
|
+
* provides:
|
|
6
|
+
* - the **token** (secret) lives in the {@link SecretStore};
|
|
7
|
+
* - the non-secret identity/base-URL metadata lives as JSON in the
|
|
8
|
+
* {@link ChannelStorage}.
|
|
9
|
+
*
|
|
10
|
+
* An account is "configured" only when both halves exist. Tokens are never
|
|
11
|
+
* logged, never embedded in emitted events or exception messages, and never
|
|
12
|
+
* rounded-trip through config dumps.
|
|
13
|
+
*/
|
|
14
|
+
import type { SecretStore } from '@krischoichoi/channel-core';
|
|
15
|
+
export interface WeixinAccountCredential {
|
|
16
|
+
/** Bot token — the secret. Never logged. */
|
|
17
|
+
token: string;
|
|
18
|
+
/** Remote Weixin bot identity (ilink_bot_id). */
|
|
19
|
+
ilinkBotId: string;
|
|
20
|
+
/** Weixin user id of the scanning user (when reported). */
|
|
21
|
+
userId?: string;
|
|
22
|
+
/** Effective API base URL (redirect-adjusted). */
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
/** ISO timestamp when the credential was saved. */
|
|
25
|
+
savedAt: string;
|
|
26
|
+
}
|
|
27
|
+
export interface AccountCredentialStoreOptions {
|
|
28
|
+
secrets: SecretStore;
|
|
29
|
+
storage: import('@krischoichoi/channel-core').ChannelStorage;
|
|
30
|
+
/** Local DSH account alias; defaults to `main`. */
|
|
31
|
+
accountId?: string;
|
|
32
|
+
/** Injectable clock (tests). */
|
|
33
|
+
now?: () => number;
|
|
34
|
+
}
|
|
35
|
+
/** Helper: one JSON field of the credential metadata. */
|
|
36
|
+
export declare class AccountCredentialStore {
|
|
37
|
+
private readonly secrets;
|
|
38
|
+
private readonly storage;
|
|
39
|
+
private readonly accountId;
|
|
40
|
+
private readonly now;
|
|
41
|
+
constructor(options: AccountCredentialStoreOptions);
|
|
42
|
+
private metaKey;
|
|
43
|
+
private tokenKey;
|
|
44
|
+
/** Save (or replace) a credential atomically. */
|
|
45
|
+
save(credential: WeixinAccountCredential & {
|
|
46
|
+
token?: string;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
/** Load the credential; resolves `undefined` when absent or partial. */
|
|
49
|
+
load(): Promise<WeixinAccountCredential | undefined>;
|
|
50
|
+
/** Delete the credential (both halves). */
|
|
51
|
+
delete(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/** Redacted summary of a credential for diagnostics (never includes the token). */
|
|
54
|
+
export declare function redactCredential(c: WeixinAccountCredential): {
|
|
55
|
+
ilinkBotId: string;
|
|
56
|
+
userId?: string;
|
|
57
|
+
baseUrl: string;
|
|
58
|
+
savedAt: string;
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=account-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-store.d.ts","sourceRoot":"","sources":["../../src/auth/account-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAG9D,MAAM,WAAW,uBAAuB;IACtC,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,OAAO,4BAA4B,EAAE,cAAc,CAAC;IAC7D,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAcD,yDAAyD;AACzD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsD;IAC9E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,EAAE,6BAA6B;IAOlD,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,QAAQ;IAIhB,iDAAiD;IAC3C,IAAI,CAAC,UAAU,EAAE,uBAAuB,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAenF,wEAAwE;IAClE,IAAI,IAAI,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC;IAsB1D,2CAA2C;IACrC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAG9B;AAED,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,uBAAuB,GAAG;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAOA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Storage key prefix for the non-secret credential metadata. */
|
|
3
|
+
const META_KEY_PREFIX = 'weixin:credential:';
|
|
4
|
+
const credentialMetadataSchema = z.object({
|
|
5
|
+
ilinkBotId: z.string().trim().min(1),
|
|
6
|
+
userId: z.string().trim().min(1).optional(),
|
|
7
|
+
baseUrl: z.string().url().refine((value) => new URL(value).protocol === 'https:', {
|
|
8
|
+
message: 'baseUrl must use https',
|
|
9
|
+
}),
|
|
10
|
+
savedAt: z.string().trim().min(1),
|
|
11
|
+
});
|
|
12
|
+
/** Helper: one JSON field of the credential metadata. */
|
|
13
|
+
export class AccountCredentialStore {
|
|
14
|
+
secrets;
|
|
15
|
+
storage;
|
|
16
|
+
accountId;
|
|
17
|
+
now;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.secrets = options.secrets;
|
|
20
|
+
this.storage = options.storage;
|
|
21
|
+
this.accountId = options.accountId ?? 'main';
|
|
22
|
+
this.now = options.now ?? Date.now;
|
|
23
|
+
}
|
|
24
|
+
metaKey() {
|
|
25
|
+
return `${META_KEY_PREFIX}${this.accountId}`;
|
|
26
|
+
}
|
|
27
|
+
tokenKey() {
|
|
28
|
+
return `weixin:token:${this.accountId}`;
|
|
29
|
+
}
|
|
30
|
+
/** Save (or replace) a credential atomically. */
|
|
31
|
+
async save(credential) {
|
|
32
|
+
const meta = {
|
|
33
|
+
ilinkBotId: credential.ilinkBotId,
|
|
34
|
+
userId: credential.userId,
|
|
35
|
+
baseUrl: credential.baseUrl,
|
|
36
|
+
savedAt: credential.savedAt ?? new Date(this.now()).toISOString(),
|
|
37
|
+
};
|
|
38
|
+
const parsed = credentialMetadataSchema.safeParse(meta);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
throw new TypeError('invalid Weixin credential metadata');
|
|
41
|
+
}
|
|
42
|
+
await this.secrets.set(this.tokenKey(), credential.token);
|
|
43
|
+
await this.storage.set(this.metaKey(), JSON.stringify(parsed.data));
|
|
44
|
+
}
|
|
45
|
+
/** Load the credential; resolves `undefined` when absent or partial. */
|
|
46
|
+
async load() {
|
|
47
|
+
const token = await this.secrets.get(this.tokenKey());
|
|
48
|
+
const rawMeta = await this.storage.get(this.metaKey());
|
|
49
|
+
if (!token || !rawMeta)
|
|
50
|
+
return undefined;
|
|
51
|
+
try {
|
|
52
|
+
const parsedJson = JSON.parse(rawMeta);
|
|
53
|
+
const parsed = credentialMetadataSchema.safeParse(parsedJson);
|
|
54
|
+
if (!parsed.success)
|
|
55
|
+
return undefined;
|
|
56
|
+
const meta = parsed.data;
|
|
57
|
+
return {
|
|
58
|
+
token,
|
|
59
|
+
ilinkBotId: meta.ilinkBotId,
|
|
60
|
+
userId: meta.userId,
|
|
61
|
+
baseUrl: meta.baseUrl,
|
|
62
|
+
savedAt: meta.savedAt,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Corrupt metadata — treat as unauthenticated.
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Delete the credential (both halves). */
|
|
71
|
+
async delete() {
|
|
72
|
+
await Promise.all([this.secrets.delete(this.tokenKey()), this.storage.delete(this.metaKey())]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Redacted summary of a credential for diagnostics (never includes the token). */
|
|
76
|
+
export function redactCredential(c) {
|
|
77
|
+
return {
|
|
78
|
+
ilinkBotId: c.ilinkBotId,
|
|
79
|
+
userId: c.userId,
|
|
80
|
+
baseUrl: c.baseUrl,
|
|
81
|
+
savedAt: c.savedAt,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=account-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-store.js","sourceRoot":"","sources":["../../src/auth/account-store.ts"],"names":[],"mappings":"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"}
|