@krischoichoi/channel-qq 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 +93 -0
- package/lib/adapter.d.ts +76 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/adapter.js +299 -0
- package/lib/adapter.js.map +1 -0
- package/lib/config.d.ts +62 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +43 -0
- package/lib/config.js.map +1 -0
- package/lib/definition.d.ts +75 -0
- package/lib/definition.d.ts.map +1 -0
- package/lib/definition.js +232 -0
- package/lib/definition.js.map +1 -0
- package/lib/image-hydrator.d.ts +13 -0
- package/lib/image-hydrator.d.ts.map +1 -0
- package/lib/image-hydrator.js +13 -0
- package/lib/image-hydrator.js.map +1 -0
- package/lib/inbound.d.ts +49 -0
- package/lib/inbound.d.ts.map +1 -0
- package/lib/inbound.js +98 -0
- package/lib/inbound.js.map +1 -0
- package/lib/index.d.ts +53 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +113 -0
- package/lib/index.js.map +1 -0
- package/lib/interaction-mapper.d.ts +43 -0
- package/lib/interaction-mapper.d.ts.map +1 -0
- package/lib/interaction-mapper.js +91 -0
- package/lib/interaction-mapper.js.map +1 -0
- package/lib/manifest.d.ts +22 -0
- package/lib/manifest.d.ts.map +1 -0
- package/lib/manifest.js +35 -0
- package/lib/manifest.js.map +1 -0
- package/lib/mapper.d.ts +38 -0
- package/lib/mapper.d.ts.map +1 -0
- package/lib/mapper.js +109 -0
- package/lib/mapper.js.map +1 -0
- package/lib/media-hydrator.d.ts +61 -0
- package/lib/media-hydrator.d.ts.map +1 -0
- package/lib/media-hydrator.js +107 -0
- package/lib/media-hydrator.js.map +1 -0
- package/lib/outbound.d.ts +41 -0
- package/lib/outbound.d.ts.map +1 -0
- package/lib/outbound.js +111 -0
- package/lib/outbound.js.map +1 -0
- package/lib/sdk-client.d.ts +247 -0
- package/lib/sdk-client.d.ts.map +1 -0
- package/lib/sdk-client.js +312 -0
- package/lib/sdk-client.js.map +1 -0
- package/lib/streaming-reply.d.ts +19 -0
- package/lib/streaming-reply.d.ts.map +1 -0
- package/lib/streaming-reply.js +26 -0
- package/lib/streaming-reply.js.map +1 -0
- package/package.json +46 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @krischoichoi/channel-qq — QQ channel adapter for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* Maps the QQ platform (via the official Tencent SDK
|
|
5
|
+
* `@tencent-connect/qqbot-nodejs`) to the stable Channel Contract. The SDK
|
|
6
|
+
* owns Token acquisition/refresh, the WebSocket gateway, media upload and C2C
|
|
7
|
+
* streaming; DSH keeps its own dedup policy and reply routing.
|
|
8
|
+
*
|
|
9
|
+
* The adapter requires `ctx.channels` and `ctx.credentials`: the QQ AppSecret
|
|
10
|
+
* is resolved at startup through `ctx.credentials` (`appSecretRef`) and passed
|
|
11
|
+
* to the adapter as `deps.appSecret`. The secret value never enters the
|
|
12
|
+
* profile config — only its reference name does (v1.1 §7, QQ-R5).
|
|
13
|
+
*
|
|
14
|
+
* Since M2B, apply() is channel-control-aware (doc §25/§27/§47): when the
|
|
15
|
+
* universal Channel Control Plane (ctx.channelControl) is present it registers
|
|
16
|
+
* the QQ `ChannelDefinition` and lets the plane drive setup / credentials /
|
|
17
|
+
* auto-start. When channel-control is absent it falls back to the legacy
|
|
18
|
+
* headless mount — mounting ONLY when configured, and never throwing on an
|
|
19
|
+
* unconfigured channel.
|
|
20
|
+
*
|
|
21
|
+
* Streaming is target-aware: C2C with a triggering message id streams natively
|
|
22
|
+
* (replace-semantics full-text); groups are buffered and delivered once at
|
|
23
|
+
* `turn/end`.
|
|
24
|
+
*/
|
|
25
|
+
import {} from '@deepseek-ai/cordis';
|
|
26
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
27
|
+
import { mountChannelAdapter, resolveVolatileConfig, resolveVolatileValue } from '@krischoichoi/channel-core';
|
|
28
|
+
import { Config, QQ_APP_SECRET_REF } from './config.js';
|
|
29
|
+
import { QQAdapter } from './adapter.js';
|
|
30
|
+
import { createQQDefinition, qqConversationScopeFingerprint } from './definition.js';
|
|
31
|
+
export const name = 'channel-qq';
|
|
32
|
+
export const inject = ['channels', 'credentials'];
|
|
33
|
+
export { Config, QQ_APP_SECRET_REF };
|
|
34
|
+
export { QQAdapter } from './adapter.js';
|
|
35
|
+
export { createQQDefinition } from './definition.js';
|
|
36
|
+
export { TencentQQSdkClient, FakeQQSdkClient, FakeStreamSession, adaptLogger, mediaOpts, decodeDataUri, QQ_MINIMAL_INTENTS, QQ_BUTTON_ACTION_TYPE, QQ_BUTTON_PERMISSION_TYPE, QQ_BUTTON_STYLE_PRIMARY, QQ_BUTTON_STYLE_DEFAULT, } from './sdk-client.js';
|
|
37
|
+
export { InboundProcessor } from './inbound.js';
|
|
38
|
+
export { hydrateMediaParts } from './media-hydrator.js';
|
|
39
|
+
// One-version compatibility alias: the hydrator was renamed
|
|
40
|
+
// image-hydrator → media-hydrator; keep the legacy names importable for now.
|
|
41
|
+
export { hydrateImageParts } from './image-hydrator.js';
|
|
42
|
+
export { OutboundSender, toReplyTarget, toQqKeyboard } from './outbound.js';
|
|
43
|
+
export { QQStreamingReply } from './streaming-reply.js';
|
|
44
|
+
export { mapInbound, mapMessageParts } from './mapper.js';
|
|
45
|
+
export { mapInteraction } from './interaction-mapper.js';
|
|
46
|
+
export { manifest } from './manifest.js';
|
|
47
|
+
/**
|
|
48
|
+
* Entry plugin apply. Accepts both the activation-time live shape (volatile
|
|
49
|
+
* fields resolve to `Volatile<T>` handles under Harness 0.2.0) and a plain
|
|
50
|
+
* snapshot (tests / programmatic mounting); plain fields pass through
|
|
51
|
+
* `resolveVolatileConfig` untouched. The shape is validated by Schemastery at
|
|
52
|
+
* the Loader boundary via the attached `Config` schema.
|
|
53
|
+
*/
|
|
54
|
+
export function apply(ctx, rawConfig, deps = {}) {
|
|
55
|
+
const config = resolveVolatileConfig(rawConfig);
|
|
56
|
+
const control = ctx.get('channelControl');
|
|
57
|
+
if (control) {
|
|
58
|
+
// Universal Channel Control Plane present: register the definition EVEN
|
|
59
|
+
// when disabled — the plane drives setup/credential/auto-start (doc
|
|
60
|
+
// §25/§27) and a disabled definition must stay visible so the Web control
|
|
61
|
+
// plane can re-enable it later (doc §19/§20).
|
|
62
|
+
const credentials = ctx.credentials;
|
|
63
|
+
const settings = ctx.get('settings');
|
|
64
|
+
control.definitions.register(createQQDefinition({
|
|
65
|
+
config,
|
|
66
|
+
deps,
|
|
67
|
+
credentials,
|
|
68
|
+
persistSetup: (patch) => settings?.update('channels-qq', patch) ?? Promise.resolve(),
|
|
69
|
+
persistEnabled: (enabled) => settings?.update('channels-qq', { enabled }) ?? Promise.resolve(),
|
|
70
|
+
resolveOwnerIdentity: async (accountId, appId) => {
|
|
71
|
+
const key = `qq:owner-openid:${accountId}:${qqConversationScopeFingerprint(appId) ?? 'unscoped'}`;
|
|
72
|
+
return ctx.channels.resources.storage.get(key);
|
|
73
|
+
},
|
|
74
|
+
persistOwnerIdentity: async (ownerId, accountId, appId) => {
|
|
75
|
+
const fingerprint = qqConversationScopeFingerprint(appId);
|
|
76
|
+
if (!fingerprint)
|
|
77
|
+
return;
|
|
78
|
+
const key = `qq:owner-openid:${accountId}:${fingerprint}`;
|
|
79
|
+
if ((await ctx.channels.resources.storage.get(key)) === undefined) {
|
|
80
|
+
await ctx.channels.resources.storage.set(key, ownerId);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Legacy / headless fallback when channel-control is absent. There is no
|
|
87
|
+
// directory/control surface to re-enable a disabled channel, so the config
|
|
88
|
+
// `enabled` gate still applies (doc §20). Mount ONLY when configured: an
|
|
89
|
+
// unconfigured channel logs a warning and returns WITHOUT throwing, so it can
|
|
90
|
+
// never crash profile startup (doc §25).
|
|
91
|
+
if (!resolveVolatileValue(config.enabled))
|
|
92
|
+
return;
|
|
93
|
+
if (!config.appSecretRef) {
|
|
94
|
+
ctx.logger.warn(`[channel-qq] no appSecretRef configured; skipping mount`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
ctx.effect(async () => {
|
|
98
|
+
const credential = await ctx.credentials.resolve(credentialRef(config.appSecretRef));
|
|
99
|
+
if (!credential) {
|
|
100
|
+
ctx.logger.warn(`[channel-qq] QQ credential "${config.appSecretRef}" is not configured; skipping mount`);
|
|
101
|
+
return () => { };
|
|
102
|
+
}
|
|
103
|
+
const adapter = new QQAdapter(config, { ...deps, appSecret: credential.value });
|
|
104
|
+
// Share the ChannelService's durable runtime resources (logger / emit /
|
|
105
|
+
// secrets / storage / signal) instead of hand-rolling per-mount Memory
|
|
106
|
+
// stores — QQ must not bypass the unified persistence backend.
|
|
107
|
+
mountChannelAdapter(ctx, adapter, (signal) => ctx.channels.createAdapterContext({ channelId: 'qq', signal }));
|
|
108
|
+
// The mount owns the adapter lifecycle; this outer effect only scopes the
|
|
109
|
+
// async credential resolution, so its disposer is a no-op.
|
|
110
|
+
return () => { };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure mapping of QQ `interaction` events → canonical `interaction.received`.
|
|
3
|
+
*
|
|
4
|
+
* The QQ `InteractionEvent` is UNTRUSTED platform input, so it is validated
|
|
5
|
+
* with a zod schema at the trust boundary (skill hard rule: zod `safeParse`
|
|
6
|
+
* for all external input; never cast). Validation failure or an ambiguous
|
|
7
|
+
* conversation is a logged drop — never a throw, never a guessed event.
|
|
8
|
+
*
|
|
9
|
+
* The adapter only emits the canonical event here; authorization (Access Gate)
|
|
10
|
+
* belongs to `channel-harness`, never to this adapter (red line 13).
|
|
11
|
+
*/
|
|
12
|
+
import type { InteractionReceived } from '@krischoichoi/channel-core';
|
|
13
|
+
import type { QQInboundMeta } from './mapper.js';
|
|
14
|
+
/** Fail-closed drop reason for a QQ interaction that cannot be mapped. */
|
|
15
|
+
export type QQInteractionDropReason = 'invalid-payload' | 'ambiguous-conversation' | 'missing-action';
|
|
16
|
+
export type QQInteractionMapping = {
|
|
17
|
+
ok: true;
|
|
18
|
+
event: InteractionReceived;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
reason: QQInteractionDropReason;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Map a QQ `InteractionEvent` (untrusted) to a canonical `InteractionReceived`,
|
|
25
|
+
* or a fail-closed drop reason.
|
|
26
|
+
*
|
|
27
|
+
* Conversation derivation mirrors the message mapper conventions:
|
|
28
|
+
* - C2C → `user_openid` present (and no group ids) → conversation.id =
|
|
29
|
+
* `user_openid`, type `dm`, sender = `user_openid`;
|
|
30
|
+
* - Group → `group_openid` + `group_member_openid` present →
|
|
31
|
+
* conversation.id = `group_openid`, type `group`, sender =
|
|
32
|
+
* `group_member_openid`.
|
|
33
|
+
*
|
|
34
|
+
* Anything else (no openids, or a mix that cannot be pinned to one
|
|
35
|
+
* conversation) fails closed — never guess a conversation (security doc:
|
|
36
|
+
* unidentified sender / invalid conversation → DENY).
|
|
37
|
+
*
|
|
38
|
+
* The action id is recovered from `data.resolved.button_data` (the verbatim
|
|
39
|
+
* echo of the outbound button's `action.data`) with `button_id` as fallback;
|
|
40
|
+
* a press that carries neither cannot be correlated and is dropped.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mapInteraction(raw: unknown, meta: QQInboundMeta): QQInteractionMapping;
|
|
43
|
+
//# sourceMappingURL=interaction-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interaction-mapper.d.ts","sourceRoot":"","sources":["../src/interaction-mapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAIV,mBAAmB,EAEpB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA0BjD,0EAA0E;AAC1E,MAAM,MAAM,uBAAuB,GAC/B,iBAAiB,GACjB,wBAAwB,GACxB,gBAAgB,CAAC;AAErB,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,mBAAmB,CAAA;CAAE,GACxC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,CAAC;AAEnD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,oBAAoB,CAiDtF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Zod schema for the interaction payload slice the adapter consumes. Unknown
|
|
4
|
+
* fields are stripped by default (never read by this mapper), so no
|
|
5
|
+
* `passthrough`/`loose` is needed; the fields that matter for identity and
|
|
6
|
+
* round-trip are strictly typed.
|
|
7
|
+
*/
|
|
8
|
+
const qqInteractionSchema = z.object({
|
|
9
|
+
id: z.string().min(1),
|
|
10
|
+
chat_type: z.number().optional(),
|
|
11
|
+
user_openid: z.string().min(1).optional(),
|
|
12
|
+
group_openid: z.string().min(1).optional(),
|
|
13
|
+
group_member_openid: z.string().min(1).optional(),
|
|
14
|
+
data: z
|
|
15
|
+
.object({
|
|
16
|
+
resolved: z
|
|
17
|
+
.object({
|
|
18
|
+
button_id: z.string().min(1).optional(),
|
|
19
|
+
button_data: z.string().min(1).optional(),
|
|
20
|
+
})
|
|
21
|
+
.optional(),
|
|
22
|
+
})
|
|
23
|
+
.optional(),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Map a QQ `InteractionEvent` (untrusted) to a canonical `InteractionReceived`,
|
|
27
|
+
* or a fail-closed drop reason.
|
|
28
|
+
*
|
|
29
|
+
* Conversation derivation mirrors the message mapper conventions:
|
|
30
|
+
* - C2C → `user_openid` present (and no group ids) → conversation.id =
|
|
31
|
+
* `user_openid`, type `dm`, sender = `user_openid`;
|
|
32
|
+
* - Group → `group_openid` + `group_member_openid` present →
|
|
33
|
+
* conversation.id = `group_openid`, type `group`, sender =
|
|
34
|
+
* `group_member_openid`.
|
|
35
|
+
*
|
|
36
|
+
* Anything else (no openids, or a mix that cannot be pinned to one
|
|
37
|
+
* conversation) fails closed — never guess a conversation (security doc:
|
|
38
|
+
* unidentified sender / invalid conversation → DENY).
|
|
39
|
+
*
|
|
40
|
+
* The action id is recovered from `data.resolved.button_data` (the verbatim
|
|
41
|
+
* echo of the outbound button's `action.data`) with `button_id` as fallback;
|
|
42
|
+
* a press that carries neither cannot be correlated and is dropped.
|
|
43
|
+
*/
|
|
44
|
+
export function mapInteraction(raw, meta) {
|
|
45
|
+
const parsed = qqInteractionSchema.safeParse(raw);
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
return { ok: false, reason: 'invalid-payload' };
|
|
48
|
+
}
|
|
49
|
+
const interaction = parsed.data;
|
|
50
|
+
const userOpenid = interaction.user_openid;
|
|
51
|
+
const groupOpenid = interaction.group_openid;
|
|
52
|
+
const groupMemberOpenid = interaction.group_member_openid;
|
|
53
|
+
let conversationId;
|
|
54
|
+
let conversationType;
|
|
55
|
+
let senderId;
|
|
56
|
+
if (userOpenid !== undefined && groupOpenid === undefined && groupMemberOpenid === undefined) {
|
|
57
|
+
conversationId = userOpenid;
|
|
58
|
+
conversationType = 'dm';
|
|
59
|
+
senderId = userOpenid;
|
|
60
|
+
}
|
|
61
|
+
else if (groupOpenid !== undefined && groupMemberOpenid !== undefined) {
|
|
62
|
+
conversationId = groupOpenid;
|
|
63
|
+
conversationType = 'group';
|
|
64
|
+
senderId = groupMemberOpenid;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
return { ok: false, reason: 'ambiguous-conversation' };
|
|
68
|
+
}
|
|
69
|
+
const resolved = interaction.data?.resolved;
|
|
70
|
+
const action = resolved?.button_data ?? resolved?.button_id;
|
|
71
|
+
if (action === undefined) {
|
|
72
|
+
return { ok: false, reason: 'missing-action' };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
event: {
|
|
77
|
+
type: 'interaction.received',
|
|
78
|
+
channel: meta.channel,
|
|
79
|
+
accountId: meta.accountId,
|
|
80
|
+
conversation: { id: conversationId, type: conversationType },
|
|
81
|
+
sender: { id: senderId },
|
|
82
|
+
interactionId: interaction.id,
|
|
83
|
+
// The recovered `uq_*` action id rides verbatim; the adapter never
|
|
84
|
+
// parses it into Harness question semantics (red line 5).
|
|
85
|
+
action,
|
|
86
|
+
// Debug-only raw payload (core/bridge never depend on its shape).
|
|
87
|
+
raw,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=interaction-mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interaction-mapper.js","sourceRoot":"","sources":["../src/interaction-mapper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface QQUpstreamManifest {
|
|
2
|
+
reference: string;
|
|
3
|
+
testedVersion: string;
|
|
4
|
+
versionRange: string;
|
|
5
|
+
strategy: 'sdk';
|
|
6
|
+
}
|
|
7
|
+
export interface QQSdkManifest {
|
|
8
|
+
package: string;
|
|
9
|
+
testedVersion: string;
|
|
10
|
+
}
|
|
11
|
+
export interface QQManifest {
|
|
12
|
+
id: 'qq';
|
|
13
|
+
adapterVersion: string;
|
|
14
|
+
upstream: QQUpstreamManifest;
|
|
15
|
+
sdk: QQSdkManifest;
|
|
16
|
+
status: 'tested';
|
|
17
|
+
/** ISO date the upstream version was last verified (extra metadata). */
|
|
18
|
+
lastVerifiedDate?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Current manifest: Tencent official `qqbot-nodejs` SDK @ 1.0.4. */
|
|
21
|
+
export declare const manifest: QQManifest;
|
|
22
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,IAAI,CAAC;IACT,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,EAAE,QAAQ,CAAC;IACjB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qEAAqE;AACrE,eAAO,MAAM,QAAQ,EAAE,UAetB,CAAC"}
|
package/lib/manifest.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ upstream compatibility manifest (Tencent official SDK).
|
|
3
|
+
*
|
|
4
|
+
* Records the upstream reference and tested version so `channels doctor` and
|
|
5
|
+
* the upgrade pipeline can govern compatibility without re-verifying by hand.
|
|
6
|
+
*
|
|
7
|
+
* Strategy: 'sdk' — the adapter consumes the official
|
|
8
|
+
* `@tencent-connect/qqbot-nodejs` SDK (Token, WebSocket gateway, media,
|
|
9
|
+
* streaming). No in-source gateway protocol and no OpenClaw runtime
|
|
10
|
+
* dependency.
|
|
11
|
+
*
|
|
12
|
+
* Status 'tested' is justified by the Channel Contract + fixture tests plus
|
|
13
|
+
* the offline adapter/mapper/outbound/streaming/lifecycle/E2E suites (Fake
|
|
14
|
+
* QQSdkClient) passing — fully offline. Live verification against a real QQ
|
|
15
|
+
* app (AppId/AppSecret) is a manual step.
|
|
16
|
+
*/
|
|
17
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
18
|
+
/** Current manifest: Tencent official `qqbot-nodejs` SDK @ 1.0.4. */
|
|
19
|
+
export const manifest = {
|
|
20
|
+
id: 'qq',
|
|
21
|
+
adapterVersion: pkg.version,
|
|
22
|
+
upstream: {
|
|
23
|
+
reference: 'tencent-connect/qqbot-nodejs (https://www.npmjs.com/package/@tencent-connect/qqbot-nodejs)',
|
|
24
|
+
testedVersion: '1.0.4',
|
|
25
|
+
versionRange: '1.0.4',
|
|
26
|
+
strategy: 'sdk',
|
|
27
|
+
},
|
|
28
|
+
sdk: {
|
|
29
|
+
package: '@tencent-connect/qqbot-nodejs',
|
|
30
|
+
testedVersion: '1.0.4',
|
|
31
|
+
},
|
|
32
|
+
status: 'tested',
|
|
33
|
+
lastVerifiedDate: '2026-08-14',
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAwBxD,qEAAqE;AACrE,MAAM,CAAC,MAAM,QAAQ,GAAe;IAClC,EAAE,EAAE,IAAI;IACR,cAAc,EAAE,GAAG,CAAC,OAAO;IAC3B,QAAQ,EAAE;QACR,SAAS,EAAE,4FAA4F;QACvG,aAAa,EAAE,OAAO;QACtB,YAAY,EAAE,OAAO;QACrB,QAAQ,EAAE,KAAK;KAChB;IACD,GAAG,EAAE;QACH,OAAO,EAAE,+BAA+B;QACxC,aAAa,EAAE,OAAO;KACvB;IACD,MAAM,EAAE,QAAQ;IAChB,gBAAgB,EAAE,YAAY;CAC/B,CAAC"}
|
package/lib/mapper.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure payload mapping (no I/O) — Tencent SDK inbound messages → Channel
|
|
3
|
+
* Contract, plus the outbound media extraction helper shared with
|
|
4
|
+
* OutboundSender.
|
|
5
|
+
*
|
|
6
|
+
* Raw payloads only ever ride along in `event.raw` for debugging — core and
|
|
7
|
+
* the harness bridge never depend on their shape (red line 6).
|
|
8
|
+
*/
|
|
9
|
+
import type { AccountId, ChannelId, MessagePart, MessageReceived } from '@krischoichoi/channel-core';
|
|
10
|
+
import type { QQBotInboundMessage } from '@tencent-connect/qqbot-nodejs';
|
|
11
|
+
export interface QQInboundMeta {
|
|
12
|
+
channel: ChannelId;
|
|
13
|
+
accountId: AccountId;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Map one Tencent SDK inbound message into the stable channel event shape.
|
|
17
|
+
*
|
|
18
|
+
* - C2C → conversation.id = `senderId`, type `dm`
|
|
19
|
+
* - Group → conversation.id = `groupOpenid`, type `group`
|
|
20
|
+
* Other kinds (`guild`/`dm`) are not formally supported in V1 and are dropped
|
|
21
|
+
* by the `InboundProcessor` before they reach this mapper.
|
|
22
|
+
*/
|
|
23
|
+
export declare function mapInbound(msg: QQBotInboundMessage, meta: QQInboundMeta): MessageReceived;
|
|
24
|
+
/**
|
|
25
|
+
* Map an SDK inbound message's text + attachments into structured parts.
|
|
26
|
+
*
|
|
27
|
+
* Text comes from `msg.content`; each attachment is mapped by its
|
|
28
|
+
* `content_type` (image/voice/audio/video/file → typed parts, unknown →
|
|
29
|
+
* unsupported).
|
|
30
|
+
*/
|
|
31
|
+
export declare function mapMessageParts(msg: QQBotInboundMessage): MessagePart[];
|
|
32
|
+
/**
|
|
33
|
+
* New QQ group-at events are a reliable activation fact. Their textual body
|
|
34
|
+
* may still start with the platform `<@...>` marker; remove only that leading
|
|
35
|
+
* addressed-bot marker so Harness sees the user's actual answer (`2`, etc.).
|
|
36
|
+
*/
|
|
37
|
+
export declare function qqMessageMentionedBot(msg: QQBotInboundMessage): boolean;
|
|
38
|
+
//# sourceMappingURL=mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mapper.d.ts","sourceRoot":"","sources":["../src/mapper.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EACV,SAAS,EACT,SAAS,EAGT,WAAW,EACX,eAAe,EAEhB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAQzE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,aAAa,GAAG,eAAe,CAyBzF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,mBAAmB,GAAG,WAAW,EAAE,CAoBvE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAIvE"}
|
package/lib/mapper.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { textParts } from '@krischoichoi/channel-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const qqMentionsSchema = z.array(z.object({
|
|
4
|
+
is_you: z.boolean().optional(),
|
|
5
|
+
}).passthrough());
|
|
6
|
+
/**
|
|
7
|
+
* Map one Tencent SDK inbound message into the stable channel event shape.
|
|
8
|
+
*
|
|
9
|
+
* - C2C → conversation.id = `senderId`, type `dm`
|
|
10
|
+
* - Group → conversation.id = `groupOpenid`, type `group`
|
|
11
|
+
* Other kinds (`guild`/`dm`) are not formally supported in V1 and are dropped
|
|
12
|
+
* by the `InboundProcessor` before they reach this mapper.
|
|
13
|
+
*/
|
|
14
|
+
export function mapInbound(msg, meta) {
|
|
15
|
+
const group = msg.kind === 'group';
|
|
16
|
+
return {
|
|
17
|
+
type: 'message.received',
|
|
18
|
+
channel: meta.channel,
|
|
19
|
+
accountId: meta.accountId,
|
|
20
|
+
conversation: {
|
|
21
|
+
id: (group ? msg.groupOpenid : msg.senderId),
|
|
22
|
+
type: group ? 'group' : 'dm',
|
|
23
|
+
},
|
|
24
|
+
sender: {
|
|
25
|
+
id: msg.senderId,
|
|
26
|
+
name: msg.senderName,
|
|
27
|
+
},
|
|
28
|
+
message: {
|
|
29
|
+
id: msg.messageId,
|
|
30
|
+
content: mapMessageParts(msg),
|
|
31
|
+
createdAt: Date.parse(msg.timestamp),
|
|
32
|
+
...(group
|
|
33
|
+
? { activation: { mentionedBot: qqMessageMentionedBot(msg) } }
|
|
34
|
+
: {}),
|
|
35
|
+
},
|
|
36
|
+
raw: msg.raw,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Map an SDK inbound message's text + attachments into structured parts.
|
|
41
|
+
*
|
|
42
|
+
* Text comes from `msg.content`; each attachment is mapped by its
|
|
43
|
+
* `content_type` (image/voice/audio/video/file → typed parts, unknown →
|
|
44
|
+
* unsupported).
|
|
45
|
+
*/
|
|
46
|
+
export function mapMessageParts(msg) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
const content = normalizeInboundText(msg);
|
|
49
|
+
if (content) {
|
|
50
|
+
parts.push(...textParts(content));
|
|
51
|
+
}
|
|
52
|
+
for (const attachment of msg.attachments ?? []) {
|
|
53
|
+
const part = mapAttachment(attachment);
|
|
54
|
+
if (part)
|
|
55
|
+
parts.push(part);
|
|
56
|
+
}
|
|
57
|
+
// A message with no content and no attachments becomes a single unsupported
|
|
58
|
+
// part so it is never silently empty.
|
|
59
|
+
if (parts.length === 0) {
|
|
60
|
+
parts.push({ type: 'unsupported', reason: 'empty qq message' });
|
|
61
|
+
}
|
|
62
|
+
return parts;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* New QQ group-at events are a reliable activation fact. Their textual body
|
|
66
|
+
* may still start with the platform `<@...>` marker; remove only that leading
|
|
67
|
+
* addressed-bot marker so Harness sees the user's actual answer (`2`, etc.).
|
|
68
|
+
*/
|
|
69
|
+
export function qqMessageMentionedBot(msg) {
|
|
70
|
+
if (msg.rawEventType === 'GROUP_AT_MESSAGE_CREATE')
|
|
71
|
+
return true;
|
|
72
|
+
const parsed = qqMentionsSchema.safeParse(msg.mentions);
|
|
73
|
+
return parsed.success && parsed.data.some((mention) => mention.is_you === true);
|
|
74
|
+
}
|
|
75
|
+
function normalizeInboundText(msg) {
|
|
76
|
+
if (!qqMessageMentionedBot(msg))
|
|
77
|
+
return msg.content;
|
|
78
|
+
return msg.content.replace(/^\s*<@!?[^>]+>\s*/u, '').trimStart();
|
|
79
|
+
}
|
|
80
|
+
/** Map one SDK inbound attachment to a structured part (or undefined). */
|
|
81
|
+
function mapAttachment(attachment) {
|
|
82
|
+
const type = attachment.content_type?.toLowerCase() ?? '';
|
|
83
|
+
if (type.includes('image')) {
|
|
84
|
+
return { type: 'image', url: attachment.url, alt: attachment.filename };
|
|
85
|
+
}
|
|
86
|
+
if (type.includes('voice') || type.includes('audio')) {
|
|
87
|
+
return {
|
|
88
|
+
type: 'audio',
|
|
89
|
+
// QQ exposes a server-converted WAV when available; prefer it.
|
|
90
|
+
url: attachment.voice_wav_url ?? attachment.url,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (type.includes('video')) {
|
|
94
|
+
return { type: 'video', url: attachment.url };
|
|
95
|
+
}
|
|
96
|
+
if (type.includes('file')) {
|
|
97
|
+
return {
|
|
98
|
+
type: 'file',
|
|
99
|
+
url: attachment.url,
|
|
100
|
+
name: attachment.filename,
|
|
101
|
+
size: attachment.size,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
type: 'unsupported',
|
|
106
|
+
reason: `unknown qq attachment type '${attachment.content_type}'`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mapper.js","sourceRoot":"","sources":["../src/mapper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary hydration for the QQ inbound path.
|
|
3
|
+
*
|
|
4
|
+
* The mapper stays pure: it preserves the real `attachment.url` on image,
|
|
5
|
+
* generic-file, audio (`voice_wav_url` ?? `url`) and video parts.
|
|
6
|
+
* This module is the single place that turns a genuine `http(s)` URL into
|
|
7
|
+
* trusted bytes, using the shared `SecureRemoteMediaFetcher` from
|
|
8
|
+
* `@krischoichoi/channel-core` as the DSH host's generic security boundary. It
|
|
9
|
+
* never implements QQ upload / token / gateway protocol —
|
|
10
|
+
* those belong to `qqbot-nodejs`.
|
|
11
|
+
*
|
|
12
|
+
* Native image ingress (M2A) hydrates `image` parts so the harness
|
|
13
|
+
* `saveImage()` / `ImageBlock` path receives real bytes. Generic file
|
|
14
|
+
* ingress hydrates `file` parts the same way: the produced
|
|
15
|
+
* `localData` is picked up automatically by the harness private asset store
|
|
16
|
+
* + extractor, so the adapter never implements QQ file upload. Since
|
|
17
|
+
* `audio` and `video` parts are hydrated through the same secure
|
|
18
|
+
* fetcher — the adapter is the transport layer and must deliver bytes for
|
|
19
|
+
* every binary kind it maps, regardless of whether a model consumer exists
|
|
20
|
+
* yet.
|
|
21
|
+
*
|
|
22
|
+
* The apply step reuses core's protocol-agnostic `applyHydrationResult`
|
|
23
|
+
* for all four binary kinds: it owns the byte cap, the
|
|
24
|
+
* AbortSignal handling, the localData/size/mime merge and the stable
|
|
25
|
+
* `ingressFailure` mapping. This module only owns the QQ-specific gate
|
|
26
|
+
* (which parts, which URLs) and the URL-level failure mapping.
|
|
27
|
+
*
|
|
28
|
+
* Key guarantees:
|
|
29
|
+
* - Only the four binary kinds (`image` / `file` / `audio` / `video`) with a
|
|
30
|
+
* genuine `http(s)` `url` are hydrated. A part already carrying `localData`
|
|
31
|
+
* / `dataUri`, or carrying an opaque `resourceRef` / no locator, is left
|
|
32
|
+
* untouched.
|
|
33
|
+
* - On success the part gets `localData` (the downloaded bytes), `size` (the
|
|
34
|
+
* hydrated byte length) and `mimeType` (prefer the fetcher's Content-Type,
|
|
35
|
+
* else keep the platform hint, else sniff the filename). Image parts keep
|
|
36
|
+
* their intrinsic shape: the `size` field is intentionally NOT set there.
|
|
37
|
+
* - On ANY failure the part is NOT dropped: its `url` is kept, a stable
|
|
38
|
+
* `ingressFailure` code is set, and hydration of other parts continues.
|
|
39
|
+
* A download failure must never block text delivery, and this
|
|
40
|
+
* function never throws.
|
|
41
|
+
*/
|
|
42
|
+
import { SecureRemoteMediaFetcher } from '@krischoichoi/channel-core';
|
|
43
|
+
import type { MessagePart } from '@krischoichoi/channel-core';
|
|
44
|
+
export interface MediaHydratorOptions {
|
|
45
|
+
/** Hard byte cap for one download. Defaults to 20 MiB. */
|
|
46
|
+
maxBytes?: number;
|
|
47
|
+
/** Read-idle timeout in ms (no body chunk for this long → fail). Defaults to 15_000. */
|
|
48
|
+
idleTimeoutMs?: number;
|
|
49
|
+
/** Header-probe timeout in ms (no response headers in this long → fail). Defaults to 15_000. */
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
/** External cancellation signal (from the adapter context). */
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Hydrate binary bytes on `parts` in place (the same array the mapper
|
|
56
|
+
* produced). Returns the mutated array. Never throws — every download failure
|
|
57
|
+
* is recorded as `ingressFailure` on the part and the event still carries
|
|
58
|
+
* the part's `url` plus any text parts.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hydrateMediaParts(parts: MessagePart[], fetcher: SecureRemoteMediaFetcher, options?: MediaHydratorOptions): Promise<MessagePart[]>;
|
|
61
|
+
//# sourceMappingURL=media-hydrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-hydrator.d.ts","sourceRoot":"","sources":["../src/media-hydrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EACL,wBAAwB,EAIzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAa9D,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,EAAE,wBAAwB,EACjC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,WAAW,EAAE,CAAC,CAsDxB"}
|