feat(qq): parse inbound QQ face/emoji tags into readable text

QQ delivers emoji messages as opaque <faceType=..,faceId="..",ext="..">
markup fragments (the ext field holds a base64 JSON blob with the face
name, e.g. {"text":"吃瓜"}). Without translation, the Harness receives
only this unusable tag and loses what the sender actually meant.

Register the SDK's contentSanitizer({ parseFaceTags: true }) middleware,
mirroring tencent-connect/dsh-qqbot, so C2C and group inbound messages
surface as e.g. 【表情: 吃瓜】 instead of the raw tag.

- qq-runtime.mjs: register contentSanitizer before the typing indicator
- runtime.test.mjs: assert sanitizer registration and update middleware order
- lib/index.js: regenerated host bundle
This commit is contained in:
Diluka 2026-08-31 09:07:55 +08:00
parent a0845a7a25
commit 5efcfa6725
3 changed files with 52 additions and 47 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
import { QQBot, typingIndicator } from '@tencent-connect/qqbot-nodejs';
import { QQBot, contentSanitizer, typingIndicator } from '@tencent-connect/qqbot-nodejs';
import {
connectionTestTarget,
@ -171,6 +171,10 @@ export class QqRuntime {
replyTimeoutMs: this.#replyTimeoutMs,
signal: controller.signal,
});
// QQ delivers emoji/face messages as opaque `<faceType=..,faceId="..",ext="..">`
// tags. Parse them into readable text so the Harness sees what the sender
// actually meant instead of an unusable markup fragment.
bot.use(contentSanitizer({ parseFaceTags: true }));
bot.use?.(this.#typingMiddleware({
keepAlive: true,
predicate: (ctx) => this.#config.ownerUserOpenid === '*'

View file

@ -47,10 +47,11 @@ test('QQ runtime waits for gateway ready, installs typing, and stops its client'
const status = await runtime.start();
assert.equal(status.ready, true);
assert.equal(status.qqConnectionState, 'connected');
assert.equal(bot.middlewares[0].name, 'typing-middleware');
assert.equal(bot.middlewares[0].options.keepAlive, true);
assert.equal(bot.middlewares[0].options.predicate({ message: { senderId: 'owner' } }), true);
assert.equal(bot.middlewares[0].options.predicate({ message: { senderId: 'other' } }), false);
assert.equal(typeof bot.middlewares[0], 'function'); // contentSanitizer (face-tag parsing)
assert.equal(bot.middlewares[1].name, 'typing-middleware');
assert.equal(bot.middlewares[1].options.keepAlive, true);
assert.equal(bot.middlewares[1].options.predicate({ message: { senderId: 'owner' } }), true);
assert.equal(bot.middlewares[1].options.predicate({ message: { senderId: 'other' } }), false);
assert.equal(botOptions.markdownSupport, false);
botOptions.logger.debug('raw gateway payload');
botOptions.logger.info('gateway ready');