@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.
Files changed (54) hide show
  1. package/README.md +93 -0
  2. package/lib/adapter.d.ts +76 -0
  3. package/lib/adapter.d.ts.map +1 -0
  4. package/lib/adapter.js +299 -0
  5. package/lib/adapter.js.map +1 -0
  6. package/lib/config.d.ts +62 -0
  7. package/lib/config.d.ts.map +1 -0
  8. package/lib/config.js +43 -0
  9. package/lib/config.js.map +1 -0
  10. package/lib/definition.d.ts +75 -0
  11. package/lib/definition.d.ts.map +1 -0
  12. package/lib/definition.js +232 -0
  13. package/lib/definition.js.map +1 -0
  14. package/lib/image-hydrator.d.ts +13 -0
  15. package/lib/image-hydrator.d.ts.map +1 -0
  16. package/lib/image-hydrator.js +13 -0
  17. package/lib/image-hydrator.js.map +1 -0
  18. package/lib/inbound.d.ts +49 -0
  19. package/lib/inbound.d.ts.map +1 -0
  20. package/lib/inbound.js +98 -0
  21. package/lib/inbound.js.map +1 -0
  22. package/lib/index.d.ts +53 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +113 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/interaction-mapper.d.ts +43 -0
  27. package/lib/interaction-mapper.d.ts.map +1 -0
  28. package/lib/interaction-mapper.js +91 -0
  29. package/lib/interaction-mapper.js.map +1 -0
  30. package/lib/manifest.d.ts +22 -0
  31. package/lib/manifest.d.ts.map +1 -0
  32. package/lib/manifest.js +35 -0
  33. package/lib/manifest.js.map +1 -0
  34. package/lib/mapper.d.ts +38 -0
  35. package/lib/mapper.d.ts.map +1 -0
  36. package/lib/mapper.js +109 -0
  37. package/lib/mapper.js.map +1 -0
  38. package/lib/media-hydrator.d.ts +61 -0
  39. package/lib/media-hydrator.d.ts.map +1 -0
  40. package/lib/media-hydrator.js +107 -0
  41. package/lib/media-hydrator.js.map +1 -0
  42. package/lib/outbound.d.ts +41 -0
  43. package/lib/outbound.d.ts.map +1 -0
  44. package/lib/outbound.js +111 -0
  45. package/lib/outbound.js.map +1 -0
  46. package/lib/sdk-client.d.ts +247 -0
  47. package/lib/sdk-client.d.ts.map +1 -0
  48. package/lib/sdk-client.js +312 -0
  49. package/lib/sdk-client.js.map +1 -0
  50. package/lib/streaming-reply.d.ts +19 -0
  51. package/lib/streaming-reply.d.ts.map +1 -0
  52. package/lib/streaming-reply.js +26 -0
  53. package/lib/streaming-reply.js.map +1 -0
  54. 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
@@ -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"}
@@ -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"}
@@ -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"}