@openclaw-china/qqbot 2026.3.12 → 2026.3.18
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/dist/index.d.ts +120 -0
- package/dist/index.js +1426 -162
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +79 -0
- package/package.json +4 -3
- package/skills/qqbot-contact-send/SKILL.md +112 -0
- package/skills/qqbot-contact-send/scripts/prepare_send.py +141 -0
- package/skills/qqbot-contact-send/scripts/resolve_known_target.py +74 -0
package/dist/index.js
CHANGED
|
@@ -4223,12 +4223,21 @@ var optionalCoercedString = external_exports.preprocess(
|
|
|
4223
4223
|
(value) => toTrimmedString(value),
|
|
4224
4224
|
external_exports.string().min(1).optional()
|
|
4225
4225
|
);
|
|
4226
|
+
var displayAliasesSchema = external_exports.record(
|
|
4227
|
+
external_exports.preprocess((value) => toTrimmedString(value), external_exports.string().min(1))
|
|
4228
|
+
).optional();
|
|
4226
4229
|
var QQBotC2CMarkdownDeliveryModeSchema = external_exports.enum(["passive", "proactive-table-only", "proactive-all"]).optional().default("proactive-table-only");
|
|
4230
|
+
var QQBotC2CMarkdownChunkStrategySchema = external_exports.enum(["markdown-block", "length"]).optional().default("markdown-block");
|
|
4231
|
+
var QQBotTypingHeartbeatModeSchema = external_exports.enum(["none", "idle", "always"]).optional().default("idle");
|
|
4232
|
+
var DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE = "idle";
|
|
4233
|
+
var DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
4234
|
+
var DEFAULT_QQBOT_TYPING_INPUT_SECONDS = 60;
|
|
4227
4235
|
var QQBotAccountSchema = external_exports.object({
|
|
4228
4236
|
name: external_exports.string().optional(),
|
|
4229
4237
|
enabled: external_exports.boolean().optional(),
|
|
4230
4238
|
appId: optionalCoercedString,
|
|
4231
4239
|
clientSecret: optionalCoercedString,
|
|
4240
|
+
displayAliases: displayAliasesSchema,
|
|
4232
4241
|
asr: external_exports.object({
|
|
4233
4242
|
enabled: external_exports.boolean().optional().default(false),
|
|
4234
4243
|
appId: optionalCoercedString,
|
|
@@ -4237,6 +4246,14 @@ var QQBotAccountSchema = external_exports.object({
|
|
|
4237
4246
|
}).optional(),
|
|
4238
4247
|
markdownSupport: external_exports.boolean().optional().default(true),
|
|
4239
4248
|
c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
|
|
4249
|
+
c2cMarkdownChunkStrategy: QQBotC2CMarkdownChunkStrategySchema,
|
|
4250
|
+
typingHeartbeatMode: QQBotTypingHeartbeatModeSchema,
|
|
4251
|
+
typingHeartbeatIntervalMs: external_exports.number().int().positive().optional().default(
|
|
4252
|
+
DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS
|
|
4253
|
+
),
|
|
4254
|
+
typingInputSeconds: external_exports.number().int().positive().optional().default(
|
|
4255
|
+
DEFAULT_QQBOT_TYPING_INPUT_SECONDS
|
|
4256
|
+
),
|
|
4240
4257
|
dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
|
|
4241
4258
|
groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
|
|
4242
4259
|
requireMention: external_exports.boolean().optional().default(true),
|
|
@@ -4261,6 +4278,21 @@ QQBotAccountSchema.extend({
|
|
|
4261
4278
|
var DEFAULT_INBOUND_MEDIA_DIR = join(homedir(), ".openclaw", "media", "qqbot", "inbound");
|
|
4262
4279
|
var DEFAULT_INBOUND_MEDIA_KEEP_DAYS = 7;
|
|
4263
4280
|
var DEFAULT_INBOUND_MEDIA_TEMP_DIR = join(tmpdir(), "qqbot-media");
|
|
4281
|
+
function normalizeDisplayAliasesMap(raw) {
|
|
4282
|
+
if (!raw || typeof raw !== "object") {
|
|
4283
|
+
return {};
|
|
4284
|
+
}
|
|
4285
|
+
const aliases = {};
|
|
4286
|
+
for (const [rawKey, rawValue] of Object.entries(raw)) {
|
|
4287
|
+
const key = rawKey.trim();
|
|
4288
|
+
const value = toTrimmedString(rawValue);
|
|
4289
|
+
if (!key || !value) {
|
|
4290
|
+
continue;
|
|
4291
|
+
}
|
|
4292
|
+
aliases[key] = value;
|
|
4293
|
+
}
|
|
4294
|
+
return aliases;
|
|
4295
|
+
}
|
|
4264
4296
|
function resolveInboundMediaDir(config) {
|
|
4265
4297
|
return String(config?.inboundMedia?.dir ?? "").trim() || DEFAULT_INBOUND_MEDIA_DIR;
|
|
4266
4298
|
}
|
|
@@ -4271,6 +4303,15 @@ function resolveInboundMediaKeepDays(config) {
|
|
|
4271
4303
|
function resolveQQBotAutoSendLocalPathMedia(config) {
|
|
4272
4304
|
return config?.autoSendLocalPathMedia ?? true;
|
|
4273
4305
|
}
|
|
4306
|
+
function resolveQQBotTypingHeartbeatMode(config) {
|
|
4307
|
+
return config?.typingHeartbeatMode ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE;
|
|
4308
|
+
}
|
|
4309
|
+
function resolveQQBotTypingHeartbeatIntervalMs(config) {
|
|
4310
|
+
return config?.typingHeartbeatIntervalMs ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS;
|
|
4311
|
+
}
|
|
4312
|
+
function resolveQQBotTypingInputSeconds(config) {
|
|
4313
|
+
return config?.typingInputSeconds ?? DEFAULT_QQBOT_TYPING_INPUT_SECONDS;
|
|
4314
|
+
}
|
|
4274
4315
|
function resolveInboundMediaTempDir() {
|
|
4275
4316
|
return DEFAULT_INBOUND_MEDIA_TEMP_DIR;
|
|
4276
4317
|
}
|
|
@@ -4301,7 +4342,15 @@ function mergeQQBotAccountConfig(cfg, accountId) {
|
|
|
4301
4342
|
const base = cfg.channels?.qqbot ?? {};
|
|
4302
4343
|
const { accounts: _ignored, defaultAccount: _ignored2, ...baseConfig } = base;
|
|
4303
4344
|
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
4304
|
-
|
|
4345
|
+
const mergedDisplayAliases = {
|
|
4346
|
+
...normalizeDisplayAliasesMap(baseConfig.displayAliases),
|
|
4347
|
+
...normalizeDisplayAliasesMap(account.displayAliases)
|
|
4348
|
+
};
|
|
4349
|
+
return {
|
|
4350
|
+
...baseConfig,
|
|
4351
|
+
...account,
|
|
4352
|
+
...Object.keys(mergedDisplayAliases).length > 0 ? { displayAliases: mergedDisplayAliases } : {}
|
|
4353
|
+
};
|
|
4305
4354
|
}
|
|
4306
4355
|
function resolveQQBotCredentials(config) {
|
|
4307
4356
|
const appId = toTrimmedString(config?.appId);
|
|
@@ -6518,6 +6567,7 @@ var CHANNEL_ORDER = [
|
|
|
6518
6567
|
"qqbot",
|
|
6519
6568
|
"wecom",
|
|
6520
6569
|
"wecom-app",
|
|
6570
|
+
"wecom-kf",
|
|
6521
6571
|
"feishu-china"
|
|
6522
6572
|
];
|
|
6523
6573
|
var CHANNEL_DISPLAY_LABELS = {
|
|
@@ -6525,6 +6575,7 @@ var CHANNEL_DISPLAY_LABELS = {
|
|
|
6525
6575
|
"feishu-china": "Feishu\uFF08\u98DE\u4E66\uFF09",
|
|
6526
6576
|
wecom: "WeCom\uFF08\u4F01\u4E1A\u5FAE\u4FE1-\u667A\u80FD\u673A\u5668\u4EBA\uFF09",
|
|
6527
6577
|
"wecom-app": "WeCom App\uFF08\u81EA\u5EFA\u5E94\u7528-\u53EF\u63A5\u5165\u5FAE\u4FE1\uFF09",
|
|
6578
|
+
"wecom-kf": "WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09",
|
|
6528
6579
|
qqbot: "QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09"
|
|
6529
6580
|
};
|
|
6530
6581
|
var CHANNEL_GUIDE_LINKS = {
|
|
@@ -6532,6 +6583,7 @@ var CHANNEL_GUIDE_LINKS = {
|
|
|
6532
6583
|
"feishu-china": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/README.md",
|
|
6533
6584
|
wecom: `${GUIDES_BASE}/wecom/configuration.md`,
|
|
6534
6585
|
"wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
|
|
6586
|
+
"wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
|
|
6535
6587
|
qqbot: `${GUIDES_BASE}/qqbot/configuration.md`
|
|
6536
6588
|
};
|
|
6537
6589
|
var CHINA_CLI_STATE_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-cli-state");
|
|
@@ -6740,6 +6792,8 @@ function isChannelConfigured(cfg, channelId) {
|
|
|
6740
6792
|
return hasWecomWsCredentialPair(channelCfg);
|
|
6741
6793
|
case "wecom-app":
|
|
6742
6794
|
return hasTokenPair(channelCfg);
|
|
6795
|
+
case "wecom-kf":
|
|
6796
|
+
return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.corpSecret) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
|
|
6743
6797
|
default:
|
|
6744
6798
|
return false;
|
|
6745
6799
|
}
|
|
@@ -6996,6 +7050,55 @@ async function configureWecomApp(prompter, cfg) {
|
|
|
6996
7050
|
patch.asr = asr;
|
|
6997
7051
|
return mergeChannelConfig(cfg, "wecom-app", patch);
|
|
6998
7052
|
}
|
|
7053
|
+
async function configureWecomKf(prompter, cfg) {
|
|
7054
|
+
section("\u914D\u7F6E WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09");
|
|
7055
|
+
showGuideLink("wecom-kf");
|
|
7056
|
+
const existing = getChannelConfig(cfg, "wecom-kf");
|
|
7057
|
+
const webhookPath = await prompter.askText({
|
|
7058
|
+
label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wecom-kf\uFF09",
|
|
7059
|
+
defaultValue: toTrimmedString2(existing.webhookPath) ?? "/wecom-kf",
|
|
7060
|
+
required: true
|
|
7061
|
+
});
|
|
7062
|
+
const token = await prompter.askSecret({
|
|
7063
|
+
label: "\u5FAE\u4FE1\u5BA2\u670D\u56DE\u8C03 Token",
|
|
7064
|
+
existingValue: toTrimmedString2(existing.token),
|
|
7065
|
+
required: true
|
|
7066
|
+
});
|
|
7067
|
+
const encodingAESKey = await prompter.askSecret({
|
|
7068
|
+
label: "\u5FAE\u4FE1\u5BA2\u670D\u56DE\u8C03 EncodingAESKey",
|
|
7069
|
+
existingValue: toTrimmedString2(existing.encodingAESKey),
|
|
7070
|
+
required: true
|
|
7071
|
+
});
|
|
7072
|
+
const corpId = await prompter.askText({
|
|
7073
|
+
label: "corpId",
|
|
7074
|
+
defaultValue: toTrimmedString2(existing.corpId),
|
|
7075
|
+
required: true
|
|
7076
|
+
});
|
|
7077
|
+
const corpSecret = await prompter.askSecret({
|
|
7078
|
+
label: "\u5FAE\u4FE1\u5BA2\u670D Secret",
|
|
7079
|
+
existingValue: toTrimmedString2(existing.corpSecret),
|
|
7080
|
+
required: true
|
|
7081
|
+
});
|
|
7082
|
+
const openKfId = await prompter.askText({
|
|
7083
|
+
label: "open_kfid",
|
|
7084
|
+
defaultValue: toTrimmedString2(existing.openKfId),
|
|
7085
|
+
required: true
|
|
7086
|
+
});
|
|
7087
|
+
const welcomeText = await prompter.askText({
|
|
7088
|
+
label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
|
|
7089
|
+
defaultValue: toTrimmedString2(existing.welcomeText),
|
|
7090
|
+
required: false
|
|
7091
|
+
});
|
|
7092
|
+
return mergeChannelConfig(cfg, "wecom-kf", {
|
|
7093
|
+
webhookPath,
|
|
7094
|
+
token,
|
|
7095
|
+
encodingAESKey,
|
|
7096
|
+
corpId,
|
|
7097
|
+
corpSecret,
|
|
7098
|
+
openKfId,
|
|
7099
|
+
welcomeText: welcomeText || void 0
|
|
7100
|
+
});
|
|
7101
|
+
}
|
|
6999
7102
|
async function configureQQBot(prompter, cfg) {
|
|
7000
7103
|
section("\u914D\u7F6E QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09");
|
|
7001
7104
|
showGuideLink("qqbot");
|
|
@@ -7052,6 +7155,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
|
|
|
7052
7155
|
return configureWecom(prompter, cfg);
|
|
7053
7156
|
case "wecom-app":
|
|
7054
7157
|
return configureWecomApp(prompter, cfg);
|
|
7158
|
+
case "wecom-kf":
|
|
7159
|
+
return configureWecomKf(prompter, cfg);
|
|
7055
7160
|
case "qqbot":
|
|
7056
7161
|
return configureQQBot(prompter, cfg);
|
|
7057
7162
|
default:
|
|
@@ -7192,6 +7297,7 @@ var SUPPORTED_CHANNELS = [
|
|
|
7192
7297
|
"feishu-china",
|
|
7193
7298
|
"wecom",
|
|
7194
7299
|
"wecom-app",
|
|
7300
|
+
"wecom-kf",
|
|
7195
7301
|
"qqbot"
|
|
7196
7302
|
];
|
|
7197
7303
|
var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
|
|
@@ -8015,21 +8121,22 @@ async function readMediaWithConfig(source, options) {
|
|
|
8015
8121
|
|
|
8016
8122
|
// src/outbound.ts
|
|
8017
8123
|
function stripPrefix(value, prefix) {
|
|
8018
|
-
return value.
|
|
8124
|
+
return value.slice(0, prefix.length).toLowerCase() === prefix ? value.slice(prefix.length) : value;
|
|
8019
8125
|
}
|
|
8020
8126
|
function parseTarget(to) {
|
|
8021
8127
|
let raw = to.trim();
|
|
8022
8128
|
raw = stripPrefix(raw, "qqbot:");
|
|
8023
|
-
|
|
8129
|
+
const normalizedRaw = raw.toLowerCase();
|
|
8130
|
+
if (normalizedRaw.startsWith("group:")) {
|
|
8024
8131
|
return { kind: "group", id: raw.slice("group:".length) };
|
|
8025
8132
|
}
|
|
8026
|
-
if (
|
|
8133
|
+
if (normalizedRaw.startsWith("channel:")) {
|
|
8027
8134
|
return { kind: "channel", id: raw.slice("channel:".length) };
|
|
8028
8135
|
}
|
|
8029
|
-
if (
|
|
8136
|
+
if (normalizedRaw.startsWith("user:")) {
|
|
8030
8137
|
return { kind: "c2c", id: raw.slice("user:".length) };
|
|
8031
8138
|
}
|
|
8032
|
-
if (
|
|
8139
|
+
if (normalizedRaw.startsWith("c2c:")) {
|
|
8033
8140
|
return { kind: "c2c", id: raw.slice("c2c:".length) };
|
|
8034
8141
|
}
|
|
8035
8142
|
return { kind: "c2c", id: raw };
|
|
@@ -9077,6 +9184,51 @@ function getQQBotRuntime() {
|
|
|
9077
9184
|
return runtime;
|
|
9078
9185
|
}
|
|
9079
9186
|
var sessionDispatchQueue = /* @__PURE__ */ new Map();
|
|
9187
|
+
var QQBOT_ABORT_TRIGGERS = /* @__PURE__ */ new Set([
|
|
9188
|
+
"stop",
|
|
9189
|
+
"esc",
|
|
9190
|
+
"abort",
|
|
9191
|
+
"wait",
|
|
9192
|
+
"exit",
|
|
9193
|
+
"interrupt",
|
|
9194
|
+
"detente",
|
|
9195
|
+
"deten",
|
|
9196
|
+
"det\xE9n",
|
|
9197
|
+
"arrete",
|
|
9198
|
+
"arr\xEAte",
|
|
9199
|
+
"\u505C\u6B62",
|
|
9200
|
+
"\u3084\u3081\u3066",
|
|
9201
|
+
"\u6B62\u3081\u3066",
|
|
9202
|
+
"\u0930\u0941\u0915\u094B",
|
|
9203
|
+
"\u062A\u0648\u0642\u0641",
|
|
9204
|
+
"\u0441\u0442\u043E\u043F",
|
|
9205
|
+
"\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0441\u044C",
|
|
9206
|
+
"\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438",
|
|
9207
|
+
"\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C",
|
|
9208
|
+
"\u043F\u0440\u0435\u043A\u0440\u0430\u0442\u0438",
|
|
9209
|
+
"halt",
|
|
9210
|
+
"anhalten",
|
|
9211
|
+
"aufh\xF6ren",
|
|
9212
|
+
"hoer auf",
|
|
9213
|
+
"stopp",
|
|
9214
|
+
"pare",
|
|
9215
|
+
"stop openclaw",
|
|
9216
|
+
"openclaw stop",
|
|
9217
|
+
"stop action",
|
|
9218
|
+
"stop current action",
|
|
9219
|
+
"stop run",
|
|
9220
|
+
"stop current run",
|
|
9221
|
+
"stop agent",
|
|
9222
|
+
"stop the agent",
|
|
9223
|
+
"stop don't do anything",
|
|
9224
|
+
"stop dont do anything",
|
|
9225
|
+
"stop do not do anything",
|
|
9226
|
+
"stop doing anything",
|
|
9227
|
+
"do not do that",
|
|
9228
|
+
"please stop",
|
|
9229
|
+
"stop please"
|
|
9230
|
+
]);
|
|
9231
|
+
var QQBOT_ABORT_TRAILING_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u;
|
|
9080
9232
|
function resolveQQBotRouteSessionKey(route) {
|
|
9081
9233
|
const effectiveSessionKey = route.effectiveSessionKey?.trim();
|
|
9082
9234
|
if (effectiveSessionKey) {
|
|
@@ -9088,6 +9240,100 @@ function buildSessionDispatchQueueKey(route) {
|
|
|
9088
9240
|
const accountId = route.accountId?.trim() || DEFAULT_ACCOUNT_ID;
|
|
9089
9241
|
return `${accountId}:${resolveQQBotRouteSessionKey(route)}`;
|
|
9090
9242
|
}
|
|
9243
|
+
function createSessionDispatchState() {
|
|
9244
|
+
return {
|
|
9245
|
+
queue: [],
|
|
9246
|
+
processing: false,
|
|
9247
|
+
immediateActiveCount: 0,
|
|
9248
|
+
waiters: [],
|
|
9249
|
+
abortGeneration: 0
|
|
9250
|
+
};
|
|
9251
|
+
}
|
|
9252
|
+
function getSessionDispatchState(queueKey) {
|
|
9253
|
+
const existing = sessionDispatchQueue.get(queueKey);
|
|
9254
|
+
if (existing) {
|
|
9255
|
+
return existing;
|
|
9256
|
+
}
|
|
9257
|
+
const created = createSessionDispatchState();
|
|
9258
|
+
sessionDispatchQueue.set(queueKey, created);
|
|
9259
|
+
return created;
|
|
9260
|
+
}
|
|
9261
|
+
function signalSessionDispatchState(state) {
|
|
9262
|
+
const waiters = state.waiters.splice(0, state.waiters.length);
|
|
9263
|
+
for (const resolve3 of waiters) {
|
|
9264
|
+
resolve3();
|
|
9265
|
+
}
|
|
9266
|
+
}
|
|
9267
|
+
function waitForSessionDispatchState(state) {
|
|
9268
|
+
return new Promise((resolve3) => {
|
|
9269
|
+
state.waiters.push(resolve3);
|
|
9270
|
+
});
|
|
9271
|
+
}
|
|
9272
|
+
function cleanupSessionDispatchState(queueKey, state) {
|
|
9273
|
+
if (state.processing || state.immediateActiveCount > 0 || state.queue.length > 0 || state.waiters.length > 0) {
|
|
9274
|
+
return;
|
|
9275
|
+
}
|
|
9276
|
+
if (sessionDispatchQueue.get(queueKey) === state) {
|
|
9277
|
+
sessionDispatchQueue.delete(queueKey);
|
|
9278
|
+
}
|
|
9279
|
+
}
|
|
9280
|
+
function hasSessionDispatchBacklog(queueKey) {
|
|
9281
|
+
const state = sessionDispatchQueue.get(queueKey);
|
|
9282
|
+
return Boolean(
|
|
9283
|
+
state && (state.processing || state.immediateActiveCount > 0 || state.queue.length > 0)
|
|
9284
|
+
);
|
|
9285
|
+
}
|
|
9286
|
+
async function processSerializedSessionDispatchQueue(queueKey) {
|
|
9287
|
+
const state = sessionDispatchQueue.get(queueKey);
|
|
9288
|
+
if (!state || state.processing) {
|
|
9289
|
+
return;
|
|
9290
|
+
}
|
|
9291
|
+
state.processing = true;
|
|
9292
|
+
try {
|
|
9293
|
+
for (; ; ) {
|
|
9294
|
+
if (state.immediateActiveCount > 0) {
|
|
9295
|
+
await waitForSessionDispatchState(state);
|
|
9296
|
+
continue;
|
|
9297
|
+
}
|
|
9298
|
+
const next = state.queue.shift();
|
|
9299
|
+
if (!next) {
|
|
9300
|
+
break;
|
|
9301
|
+
}
|
|
9302
|
+
try {
|
|
9303
|
+
await next.task();
|
|
9304
|
+
next.resolve();
|
|
9305
|
+
} catch (err) {
|
|
9306
|
+
next.reject(err);
|
|
9307
|
+
}
|
|
9308
|
+
}
|
|
9309
|
+
} finally {
|
|
9310
|
+
state.processing = false;
|
|
9311
|
+
if (state.queue.length > 0) {
|
|
9312
|
+
void processSerializedSessionDispatchQueue(queueKey);
|
|
9313
|
+
return;
|
|
9314
|
+
}
|
|
9315
|
+
cleanupSessionDispatchState(queueKey, state);
|
|
9316
|
+
}
|
|
9317
|
+
}
|
|
9318
|
+
function dropQueuedSessionDispatches(queueKey) {
|
|
9319
|
+
const state = sessionDispatchQueue.get(queueKey);
|
|
9320
|
+
if (!state || state.queue.length === 0) {
|
|
9321
|
+
return 0;
|
|
9322
|
+
}
|
|
9323
|
+
const dropped = state.queue.splice(0, state.queue.length);
|
|
9324
|
+
for (const item of dropped) {
|
|
9325
|
+
item.resolve();
|
|
9326
|
+
}
|
|
9327
|
+
signalSessionDispatchState(state);
|
|
9328
|
+
cleanupSessionDispatchState(queueKey, state);
|
|
9329
|
+
return dropped.length;
|
|
9330
|
+
}
|
|
9331
|
+
function markSessionDispatchAbort(queueKey) {
|
|
9332
|
+
const state = getSessionDispatchState(queueKey);
|
|
9333
|
+
state.abortGeneration += 1;
|
|
9334
|
+
signalSessionDispatchState(state);
|
|
9335
|
+
return state.abortGeneration;
|
|
9336
|
+
}
|
|
9091
9337
|
function normalizeQQBotSessionKeyPart(value) {
|
|
9092
9338
|
const trimmed = value.trim();
|
|
9093
9339
|
return trimmed ? trimmed.toLowerCase() : "unknown";
|
|
@@ -9123,22 +9369,163 @@ function normalizeQQBotReplyTarget(value) {
|
|
|
9123
9369
|
return /^(user|group|channel):/i.test(trimmed) ? trimmed : void 0;
|
|
9124
9370
|
}
|
|
9125
9371
|
async function runSerializedSessionDispatch(queueKey, task) {
|
|
9126
|
-
const
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9372
|
+
const state = getSessionDispatchState(queueKey);
|
|
9373
|
+
return new Promise((resolve3, reject) => {
|
|
9374
|
+
let settled = false;
|
|
9375
|
+
state.queue.push({
|
|
9376
|
+
task: async () => {
|
|
9377
|
+
if (settled) {
|
|
9378
|
+
return;
|
|
9379
|
+
}
|
|
9380
|
+
try {
|
|
9381
|
+
const result = await task();
|
|
9382
|
+
if (!settled) {
|
|
9383
|
+
settled = true;
|
|
9384
|
+
resolve3(result);
|
|
9385
|
+
}
|
|
9386
|
+
} catch (err) {
|
|
9387
|
+
if (!settled) {
|
|
9388
|
+
settled = true;
|
|
9389
|
+
reject(err);
|
|
9390
|
+
}
|
|
9391
|
+
}
|
|
9392
|
+
},
|
|
9393
|
+
resolve: () => {
|
|
9394
|
+
if (settled) {
|
|
9395
|
+
return;
|
|
9396
|
+
}
|
|
9397
|
+
settled = true;
|
|
9398
|
+
resolve3(void 0);
|
|
9399
|
+
},
|
|
9400
|
+
reject: (err) => {
|
|
9401
|
+
if (settled) {
|
|
9402
|
+
return;
|
|
9403
|
+
}
|
|
9404
|
+
settled = true;
|
|
9405
|
+
reject(err);
|
|
9406
|
+
}
|
|
9407
|
+
});
|
|
9408
|
+
signalSessionDispatchState(state);
|
|
9409
|
+
void processSerializedSessionDispatchQueue(queueKey);
|
|
9410
|
+
});
|
|
9411
|
+
}
|
|
9412
|
+
async function runImmediateSessionDispatch(queueKey, task) {
|
|
9413
|
+
const state = getSessionDispatchState(queueKey);
|
|
9414
|
+
state.immediateActiveCount += 1;
|
|
9415
|
+
signalSessionDispatchState(state);
|
|
9130
9416
|
try {
|
|
9131
|
-
return await
|
|
9417
|
+
return await task();
|
|
9132
9418
|
} finally {
|
|
9133
|
-
|
|
9134
|
-
|
|
9419
|
+
state.immediateActiveCount = Math.max(0, state.immediateActiveCount - 1);
|
|
9420
|
+
signalSessionDispatchState(state);
|
|
9421
|
+
if (state.queue.length > 0) {
|
|
9422
|
+
void processSerializedSessionDispatchQueue(queueKey);
|
|
9135
9423
|
}
|
|
9424
|
+
cleanupSessionDispatchState(queueKey, state);
|
|
9425
|
+
}
|
|
9426
|
+
}
|
|
9427
|
+
function normalizeQQBotAbortTriggerText(text) {
|
|
9428
|
+
return text.trim().toLowerCase().replace(/[’`]/g, "'").replace(/\s+/g, " ").replace(QQBOT_ABORT_TRAILING_PUNCTUATION_RE, "").trim();
|
|
9429
|
+
}
|
|
9430
|
+
function isQQBotAbortTrigger(text) {
|
|
9431
|
+
if (!text) {
|
|
9432
|
+
return false;
|
|
9136
9433
|
}
|
|
9434
|
+
return QQBOT_ABORT_TRIGGERS.has(normalizeQQBotAbortTriggerText(text));
|
|
9435
|
+
}
|
|
9436
|
+
function isQQBotFastAbortCommandText(text) {
|
|
9437
|
+
if (!text) {
|
|
9438
|
+
return false;
|
|
9439
|
+
}
|
|
9440
|
+
const normalized = text.trim();
|
|
9441
|
+
if (!normalized) {
|
|
9442
|
+
return false;
|
|
9443
|
+
}
|
|
9444
|
+
const lower = normalized.toLowerCase();
|
|
9445
|
+
return lower === "/stop" || normalizeQQBotAbortTriggerText(lower) === "/stop" || isQQBotAbortTrigger(lower);
|
|
9137
9446
|
}
|
|
9138
9447
|
function toString(value) {
|
|
9139
9448
|
if (typeof value === "string" && value.trim()) return value;
|
|
9140
9449
|
return void 0;
|
|
9141
9450
|
}
|
|
9451
|
+
function asRecord(value) {
|
|
9452
|
+
if (!value || typeof value !== "object") {
|
|
9453
|
+
return void 0;
|
|
9454
|
+
}
|
|
9455
|
+
return value;
|
|
9456
|
+
}
|
|
9457
|
+
function normalizeQQBotDisplayAliasesMap(raw) {
|
|
9458
|
+
if (!raw || typeof raw !== "object") {
|
|
9459
|
+
return {};
|
|
9460
|
+
}
|
|
9461
|
+
const aliases = {};
|
|
9462
|
+
for (const [rawKey, rawValue] of Object.entries(raw)) {
|
|
9463
|
+
const key = rawKey.trim();
|
|
9464
|
+
const value = toString(rawValue);
|
|
9465
|
+
if (!key || !value) {
|
|
9466
|
+
continue;
|
|
9467
|
+
}
|
|
9468
|
+
aliases[key] = value;
|
|
9469
|
+
}
|
|
9470
|
+
return aliases;
|
|
9471
|
+
}
|
|
9472
|
+
function resolveQQBotDisplayAliasMaps(cfg, accountId) {
|
|
9473
|
+
const qqbot = cfg?.channels?.qqbot;
|
|
9474
|
+
return {
|
|
9475
|
+
globalAliases: normalizeQQBotDisplayAliasesMap(qqbot?.displayAliases),
|
|
9476
|
+
accountAliases: normalizeQQBotDisplayAliasesMap(qqbot?.accounts?.[accountId]?.displayAliases)
|
|
9477
|
+
};
|
|
9478
|
+
}
|
|
9479
|
+
function resolveQQBotSenderName(params) {
|
|
9480
|
+
const { inbound, cfg, accountId } = params;
|
|
9481
|
+
const stableId = inbound.c2cOpenid?.trim() || inbound.senderId.trim();
|
|
9482
|
+
const { globalAliases, accountAliases } = resolveQQBotDisplayAliasMaps(cfg, accountId);
|
|
9483
|
+
if (inbound.type === "direct") {
|
|
9484
|
+
const knownTarget = stableId ? getKnownQQBotTarget({ accountId, target: `user:${stableId}` }) : void 0;
|
|
9485
|
+
const knownTargetDisplayName = knownTarget?.displayName?.trim();
|
|
9486
|
+
if (knownTargetDisplayName) {
|
|
9487
|
+
return {
|
|
9488
|
+
displayName: knownTargetDisplayName,
|
|
9489
|
+
persistentDisplayName: knownTargetDisplayName,
|
|
9490
|
+
source: "known-target",
|
|
9491
|
+
knownTargetDisplayName
|
|
9492
|
+
};
|
|
9493
|
+
}
|
|
9494
|
+
const aliasKeys = [...new Set([`user:${stableId}`, stableId, inbound.senderId.trim()].filter(Boolean))];
|
|
9495
|
+
for (const aliasKey of aliasKeys) {
|
|
9496
|
+
const alias = accountAliases[aliasKey];
|
|
9497
|
+
if (alias) {
|
|
9498
|
+
return {
|
|
9499
|
+
displayName: alias,
|
|
9500
|
+
persistentDisplayName: alias,
|
|
9501
|
+
source: "account-alias",
|
|
9502
|
+
matchedAliasKey: aliasKey
|
|
9503
|
+
};
|
|
9504
|
+
}
|
|
9505
|
+
}
|
|
9506
|
+
for (const aliasKey of aliasKeys) {
|
|
9507
|
+
const alias = globalAliases[aliasKey];
|
|
9508
|
+
if (alias) {
|
|
9509
|
+
return {
|
|
9510
|
+
displayName: alias,
|
|
9511
|
+
persistentDisplayName: alias,
|
|
9512
|
+
source: "global-alias",
|
|
9513
|
+
matchedAliasKey: aliasKey
|
|
9514
|
+
};
|
|
9515
|
+
}
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
9518
|
+
return {
|
|
9519
|
+
displayName: stableId,
|
|
9520
|
+
source: "stable-id"
|
|
9521
|
+
};
|
|
9522
|
+
}
|
|
9523
|
+
function logQQBotSenderNameResolution(params) {
|
|
9524
|
+
const { logger, inbound, accountId, resolution } = params;
|
|
9525
|
+
logger.debug?.(
|
|
9526
|
+
`[display-name] accountId=${accountId} type=${inbound.type} senderId=${inbound.senderId} knownTarget=${resolution.knownTargetDisplayName ?? "-"} alias=${resolution.matchedAliasKey ?? "-"} final=${JSON.stringify(resolution.displayName)} source=${resolution.source}`
|
|
9527
|
+
);
|
|
9528
|
+
}
|
|
9142
9529
|
function toNumber2(value) {
|
|
9143
9530
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
9144
9531
|
if (typeof value === "string") {
|
|
@@ -9256,6 +9643,37 @@ function startLongTaskNoticeTimer(params) {
|
|
|
9256
9643
|
}
|
|
9257
9644
|
};
|
|
9258
9645
|
}
|
|
9646
|
+
function startQQBotTypingHeartbeat(params) {
|
|
9647
|
+
const { intervalMs, renew, shouldRenew } = params;
|
|
9648
|
+
let stopped = false;
|
|
9649
|
+
let timer = null;
|
|
9650
|
+
let renewalInFlight = false;
|
|
9651
|
+
const clear = () => {
|
|
9652
|
+
if (!timer) return;
|
|
9653
|
+
clearInterval(timer);
|
|
9654
|
+
timer = null;
|
|
9655
|
+
};
|
|
9656
|
+
const stop = () => {
|
|
9657
|
+
if (stopped) return;
|
|
9658
|
+
stopped = true;
|
|
9659
|
+
clear();
|
|
9660
|
+
};
|
|
9661
|
+
if (intervalMs > 0) {
|
|
9662
|
+
timer = setInterval(() => {
|
|
9663
|
+
if (stopped || renewalInFlight) return;
|
|
9664
|
+
if (shouldRenew && !shouldRenew()) return;
|
|
9665
|
+
renewalInFlight = true;
|
|
9666
|
+
void renew().catch(() => void 0).finally(() => {
|
|
9667
|
+
renewalInFlight = false;
|
|
9668
|
+
});
|
|
9669
|
+
}, intervalMs);
|
|
9670
|
+
timer.unref?.();
|
|
9671
|
+
}
|
|
9672
|
+
return {
|
|
9673
|
+
stop,
|
|
9674
|
+
dispose: stop
|
|
9675
|
+
};
|
|
9676
|
+
}
|
|
9259
9677
|
function isHttpUrl3(value) {
|
|
9260
9678
|
return /^https?:\/\//i.test(value);
|
|
9261
9679
|
}
|
|
@@ -9522,14 +9940,13 @@ function parseC2CMessage(data, fallbackEventId) {
|
|
|
9522
9940
|
const id = toString(payload.id);
|
|
9523
9941
|
const eventId = resolveEventId(payload, fallbackEventId);
|
|
9524
9942
|
const timestamp = toNumber2(payload.timestamp) ?? Date.now();
|
|
9525
|
-
const author = payload.author ?? {};
|
|
9943
|
+
const author = asRecord(payload.author) ?? {};
|
|
9526
9944
|
const senderId = toString(author.user_openid);
|
|
9527
9945
|
if (!text && attachments.length === 0 || !id || !senderId) return null;
|
|
9528
9946
|
return {
|
|
9529
9947
|
type: "direct",
|
|
9530
9948
|
senderId,
|
|
9531
9949
|
c2cOpenid: senderId,
|
|
9532
|
-
senderName: toString(author.username),
|
|
9533
9950
|
content: text,
|
|
9534
9951
|
attachments: attachments.length > 0 ? attachments : void 0,
|
|
9535
9952
|
messageId: id,
|
|
@@ -9546,13 +9963,12 @@ function parseGroupMessage(data, fallbackEventId) {
|
|
|
9546
9963
|
const eventId = resolveEventId(payload, fallbackEventId);
|
|
9547
9964
|
const timestamp = toNumber2(payload.timestamp) ?? Date.now();
|
|
9548
9965
|
const groupOpenid = toString(payload.group_openid);
|
|
9549
|
-
const author = payload.author ?? {};
|
|
9966
|
+
const author = asRecord(payload.author) ?? {};
|
|
9550
9967
|
const senderId = toString(author.member_openid);
|
|
9551
9968
|
if (!text && attachments.length === 0 || !id || !senderId || !groupOpenid) return null;
|
|
9552
9969
|
return {
|
|
9553
9970
|
type: "group",
|
|
9554
9971
|
senderId,
|
|
9555
|
-
senderName: toString(author.nickname) ?? toString(author.username),
|
|
9556
9972
|
content: text,
|
|
9557
9973
|
attachments: attachments.length > 0 ? attachments : void 0,
|
|
9558
9974
|
messageId: id,
|
|
@@ -9570,13 +9986,12 @@ function parseChannelMessage(data, fallbackEventId) {
|
|
|
9570
9986
|
const timestamp = toNumber2(payload.timestamp) ?? Date.now();
|
|
9571
9987
|
const channelId = toString(payload.channel_id);
|
|
9572
9988
|
const guildId = toString(payload.guild_id);
|
|
9573
|
-
const author = payload.author ?? {};
|
|
9989
|
+
const author = asRecord(payload.author) ?? {};
|
|
9574
9990
|
const senderId = toString(author.id);
|
|
9575
9991
|
if (!text && attachments.length === 0 || !id || !senderId || !channelId) return null;
|
|
9576
9992
|
return {
|
|
9577
9993
|
type: "channel",
|
|
9578
9994
|
senderId,
|
|
9579
|
-
senderName: toString(author.username),
|
|
9580
9995
|
content: text,
|
|
9581
9996
|
attachments: attachments.length > 0 ? attachments : void 0,
|
|
9582
9997
|
messageId: id,
|
|
@@ -9594,13 +10009,12 @@ function parseDirectMessage(data, fallbackEventId) {
|
|
|
9594
10009
|
const eventId = resolveEventId(payload, fallbackEventId);
|
|
9595
10010
|
const timestamp = toNumber2(payload.timestamp) ?? Date.now();
|
|
9596
10011
|
const guildId = toString(payload.guild_id);
|
|
9597
|
-
const author = payload.author ?? {};
|
|
10012
|
+
const author = asRecord(payload.author) ?? {};
|
|
9598
10013
|
const senderId = toString(author.id);
|
|
9599
10014
|
if (!text && attachments.length === 0 || !id || !senderId) return null;
|
|
9600
10015
|
return {
|
|
9601
10016
|
type: "direct",
|
|
9602
10017
|
senderId,
|
|
9603
|
-
senderName: toString(author.username),
|
|
9604
10018
|
content: text,
|
|
9605
10019
|
attachments: attachments.length > 0 ? attachments : void 0,
|
|
9606
10020
|
messageId: id,
|
|
@@ -9675,7 +10089,7 @@ function resolveEnvelopeFrom(event) {
|
|
|
9675
10089
|
return event.senderName?.trim() || event.senderId;
|
|
9676
10090
|
}
|
|
9677
10091
|
function resolveKnownQQBotTargetFromInbound(params) {
|
|
9678
|
-
const { inbound, accountId } = params;
|
|
10092
|
+
const { inbound, accountId, persistentDisplayName } = params;
|
|
9679
10093
|
if (inbound.type === "direct") {
|
|
9680
10094
|
if (!inbound.c2cOpenid?.trim()) {
|
|
9681
10095
|
return void 0;
|
|
@@ -9684,7 +10098,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
|
|
|
9684
10098
|
accountId,
|
|
9685
10099
|
kind: "user",
|
|
9686
10100
|
target: `user:${inbound.c2cOpenid}`,
|
|
9687
|
-
displayName:
|
|
10101
|
+
...persistentDisplayName ? { displayName: persistentDisplayName } : {},
|
|
9688
10102
|
sourceChatType: "direct",
|
|
9689
10103
|
firstSeenAt: inbound.timestamp,
|
|
9690
10104
|
lastSeenAt: inbound.timestamp
|
|
@@ -9695,7 +10109,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
|
|
|
9695
10109
|
accountId,
|
|
9696
10110
|
kind: "group",
|
|
9697
10111
|
target: `group:${inbound.groupOpenid}`,
|
|
9698
|
-
displayName:
|
|
10112
|
+
...persistentDisplayName ? { displayName: persistentDisplayName } : {},
|
|
9699
10113
|
sourceChatType: "group",
|
|
9700
10114
|
firstSeenAt: inbound.timestamp,
|
|
9701
10115
|
lastSeenAt: inbound.timestamp
|
|
@@ -9706,7 +10120,7 @@ function resolveKnownQQBotTargetFromInbound(params) {
|
|
|
9706
10120
|
accountId,
|
|
9707
10121
|
kind: "channel",
|
|
9708
10122
|
target: `channel:${inbound.channelId}`,
|
|
9709
|
-
displayName:
|
|
10123
|
+
...persistentDisplayName ? { displayName: persistentDisplayName } : {},
|
|
9710
10124
|
sourceChatType: "channel",
|
|
9711
10125
|
firstSeenAt: inbound.timestamp,
|
|
9712
10126
|
lastSeenAt: inbound.timestamp
|
|
@@ -9821,6 +10235,14 @@ var DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:[^\]]+|audio_as_v
|
|
|
9821
10235
|
var VOICE_EMOTION_TAG_RE = /\[(?:happy|excited|calm|sad|angry|frustrated|softly|whispers|loudly|cheerfully|deadpan|sarcastically|laughs|sighs|chuckles|gasps|pause|slowly|rushed|hesitates|playfully|warmly|gently)\]/gi;
|
|
9822
10236
|
var TTS_LIKE_RAW_TEXT_RE = /\[\[\s*(?:tts(?::text)?|\/tts(?::text)?|audio_as_voice|reply_to_current|reply_to\s*:)/i;
|
|
9823
10237
|
var MARKDOWN_TABLE_SEPARATOR_RE = /^\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?)?\|?$/;
|
|
10238
|
+
var MARKDOWN_THEMATIC_BREAK_RE = /^\s{0,3}(?:(?:-\s*){3,}|(?:_\s*){3,}|(?:\*\s*){3,})$/;
|
|
10239
|
+
var MARKDOWN_ATX_HEADING_RE = /^\s{0,3}#{1,6}\s+\S/;
|
|
10240
|
+
var MARKDOWN_BLOCKQUOTE_RE = /^\s{0,3}>\s?/;
|
|
10241
|
+
var MARKDOWN_FENCE_RE = /^\s*(`{3,}|~{3,})(.*)$/;
|
|
10242
|
+
var MARKDOWN_LIST_ITEM_RE = /^\s*(?:[-+*]|\d+\.)\s+/;
|
|
10243
|
+
var MARKDOWN_LIST_CONTINUATION_RE = /^\s{2,}\S/;
|
|
10244
|
+
var MARKDOWN_INLINE_STRUCTURE_RE = /(?:\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|~~[^~\n]+~~|\*[^*\n]+\*)/;
|
|
10245
|
+
var MARKDOWN_BOUNDARY_GUARD_RE = /[`*_~|]/;
|
|
9824
10246
|
var EXPLICIT_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*(?:markdown|md)\s*\n([\s\S]*?)\n\2(?=\n|$)/gi;
|
|
9825
10247
|
var GENERIC_MARKDOWN_FENCE_RE = /(^|\n)(`{3,}|~{3,})\s*\n([\s\S]*?)\n\2(?=\n|$)/g;
|
|
9826
10248
|
function extractFinalBlocks(text) {
|
|
@@ -9889,8 +10311,9 @@ function evaluateReplyFinalOnlyDelivery(params) {
|
|
|
9889
10311
|
}
|
|
9890
10312
|
function isQQBotC2CTarget(to) {
|
|
9891
10313
|
const trimmed = to.trim();
|
|
9892
|
-
const raw = trimmed.
|
|
9893
|
-
|
|
10314
|
+
const raw = trimmed.slice(0, "qqbot:".length).toLowerCase() === "qqbot:" ? trimmed.slice("qqbot:".length) : trimmed;
|
|
10315
|
+
const normalizedRaw = raw.toLowerCase();
|
|
10316
|
+
return !normalizedRaw.startsWith("group:") && !normalizedRaw.startsWith("channel:");
|
|
9894
10317
|
}
|
|
9895
10318
|
function splitQQBotMarkdownTransportMediaUrls(mediaUrls) {
|
|
9896
10319
|
const markdownImageUrls = [];
|
|
@@ -9992,10 +10415,592 @@ function normalizeQQBotRenderedMarkdown(text) {
|
|
|
9992
10415
|
);
|
|
9993
10416
|
return changed ? next.trim() : text.trim();
|
|
9994
10417
|
}
|
|
10418
|
+
function normalizeQQBotMarkdownSegment(text) {
|
|
10419
|
+
return text.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
10420
|
+
}
|
|
10421
|
+
function isBlankQQBotMarkdownLine(line) {
|
|
10422
|
+
return line.trim().length === 0;
|
|
10423
|
+
}
|
|
10424
|
+
function resolveQQBotFenceDelimiter(line) {
|
|
10425
|
+
const match = line.match(MARKDOWN_FENCE_RE);
|
|
10426
|
+
return match?.[1];
|
|
10427
|
+
}
|
|
10428
|
+
function isQQBotFenceClosingLine(line, delimiter) {
|
|
10429
|
+
const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10430
|
+
const closingRe = new RegExp(`^\\s*${escapedDelimiter}${delimiter[0]}*\\s*$`);
|
|
10431
|
+
return closingRe.test(line);
|
|
10432
|
+
}
|
|
10433
|
+
function joinQQBotMarkdownPieces(parts) {
|
|
10434
|
+
return parts.filter(Boolean).join("\n\n").trim();
|
|
10435
|
+
}
|
|
10436
|
+
function isQQBotMarkdownTableStart(lines, index) {
|
|
10437
|
+
const header = lines[index]?.trim() ?? "";
|
|
10438
|
+
const separator = lines[index + 1]?.trim() ?? "";
|
|
10439
|
+
return Boolean(header.includes("|") && MARKDOWN_TABLE_SEPARATOR_RE.test(separator));
|
|
10440
|
+
}
|
|
10441
|
+
function collectQQBotFencedCodeBlock(lines, startIndex) {
|
|
10442
|
+
const openingLine = lines[startIndex] ?? "";
|
|
10443
|
+
const delimiter = resolveQQBotFenceDelimiter(openingLine) ?? "```";
|
|
10444
|
+
let index = startIndex + 1;
|
|
10445
|
+
while (index < lines.length) {
|
|
10446
|
+
if (isQQBotFenceClosingLine(lines[index] ?? "", delimiter)) {
|
|
10447
|
+
index += 1;
|
|
10448
|
+
break;
|
|
10449
|
+
}
|
|
10450
|
+
index += 1;
|
|
10451
|
+
}
|
|
10452
|
+
return {
|
|
10453
|
+
block: {
|
|
10454
|
+
kind: "code",
|
|
10455
|
+
text: lines.slice(startIndex, index).join("\n").trimEnd()
|
|
10456
|
+
},
|
|
10457
|
+
nextIndex: index
|
|
10458
|
+
};
|
|
10459
|
+
}
|
|
10460
|
+
function collectQQBotMarkdownTableBlock(lines, startIndex) {
|
|
10461
|
+
let index = startIndex + 2;
|
|
10462
|
+
while (index < lines.length) {
|
|
10463
|
+
const line = lines[index] ?? "";
|
|
10464
|
+
if (isBlankQQBotMarkdownLine(line) || !line.includes("|")) {
|
|
10465
|
+
break;
|
|
10466
|
+
}
|
|
10467
|
+
index += 1;
|
|
10468
|
+
}
|
|
10469
|
+
return {
|
|
10470
|
+
block: {
|
|
10471
|
+
kind: "table",
|
|
10472
|
+
text: lines.slice(startIndex, index).join("\n").trimEnd()
|
|
10473
|
+
},
|
|
10474
|
+
nextIndex: index
|
|
10475
|
+
};
|
|
10476
|
+
}
|
|
10477
|
+
function collectQQBotBlockquoteBlock(lines, startIndex) {
|
|
10478
|
+
const collected = [];
|
|
10479
|
+
let index = startIndex;
|
|
10480
|
+
while (index < lines.length) {
|
|
10481
|
+
const line = lines[index] ?? "";
|
|
10482
|
+
if (MARKDOWN_BLOCKQUOTE_RE.test(line)) {
|
|
10483
|
+
collected.push(line);
|
|
10484
|
+
index += 1;
|
|
10485
|
+
continue;
|
|
10486
|
+
}
|
|
10487
|
+
if (isBlankQQBotMarkdownLine(line) && index + 1 < lines.length && MARKDOWN_BLOCKQUOTE_RE.test(lines[index + 1] ?? "")) {
|
|
10488
|
+
collected.push(line);
|
|
10489
|
+
index += 1;
|
|
10490
|
+
continue;
|
|
10491
|
+
}
|
|
10492
|
+
break;
|
|
10493
|
+
}
|
|
10494
|
+
return {
|
|
10495
|
+
block: {
|
|
10496
|
+
kind: "blockquote",
|
|
10497
|
+
text: collected.join("\n").trimEnd()
|
|
10498
|
+
},
|
|
10499
|
+
nextIndex: index
|
|
10500
|
+
};
|
|
10501
|
+
}
|
|
10502
|
+
function collectQQBotListBlock(lines, startIndex) {
|
|
10503
|
+
const collected = [];
|
|
10504
|
+
let index = startIndex;
|
|
10505
|
+
while (index < lines.length) {
|
|
10506
|
+
const line = lines[index] ?? "";
|
|
10507
|
+
if (isBlankQQBotMarkdownLine(line)) {
|
|
10508
|
+
break;
|
|
10509
|
+
}
|
|
10510
|
+
if (MARKDOWN_FENCE_RE.test(line) || MARKDOWN_BLOCKQUOTE_RE.test(line) || MARKDOWN_ATX_HEADING_RE.test(line) || MARKDOWN_THEMATIC_BREAK_RE.test(line) || isQQBotMarkdownTableStart(lines, index)) {
|
|
10511
|
+
break;
|
|
10512
|
+
}
|
|
10513
|
+
if (collected.length > 0 && !MARKDOWN_LIST_ITEM_RE.test(line) && !MARKDOWN_LIST_CONTINUATION_RE.test(line)) {
|
|
10514
|
+
collected.push(line);
|
|
10515
|
+
index += 1;
|
|
10516
|
+
continue;
|
|
10517
|
+
}
|
|
10518
|
+
collected.push(line);
|
|
10519
|
+
index += 1;
|
|
10520
|
+
}
|
|
10521
|
+
return {
|
|
10522
|
+
block: {
|
|
10523
|
+
kind: "list",
|
|
10524
|
+
text: collected.join("\n").trimEnd()
|
|
10525
|
+
},
|
|
10526
|
+
nextIndex: index
|
|
10527
|
+
};
|
|
10528
|
+
}
|
|
10529
|
+
function collectQQBotParagraphBlock(lines, startIndex) {
|
|
10530
|
+
const collected = [];
|
|
10531
|
+
let index = startIndex;
|
|
10532
|
+
while (index < lines.length) {
|
|
10533
|
+
const line = lines[index] ?? "";
|
|
10534
|
+
if (isBlankQQBotMarkdownLine(line)) {
|
|
10535
|
+
break;
|
|
10536
|
+
}
|
|
10537
|
+
if (collected.length > 0 && (MARKDOWN_FENCE_RE.test(line) || MARKDOWN_BLOCKQUOTE_RE.test(line) || MARKDOWN_ATX_HEADING_RE.test(line) || MARKDOWN_THEMATIC_BREAK_RE.test(line) || MARKDOWN_LIST_ITEM_RE.test(line) || isQQBotMarkdownTableStart(lines, index))) {
|
|
10538
|
+
break;
|
|
10539
|
+
}
|
|
10540
|
+
collected.push(line);
|
|
10541
|
+
index += 1;
|
|
10542
|
+
}
|
|
10543
|
+
return {
|
|
10544
|
+
block: {
|
|
10545
|
+
kind: "paragraph",
|
|
10546
|
+
text: collected.join("\n").trimEnd()
|
|
10547
|
+
},
|
|
10548
|
+
nextIndex: index
|
|
10549
|
+
};
|
|
10550
|
+
}
|
|
10551
|
+
function parseQQBotMarkdownBlocks(text) {
|
|
10552
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
10553
|
+
const blocks = [];
|
|
10554
|
+
let index = 0;
|
|
10555
|
+
while (index < lines.length) {
|
|
10556
|
+
while (index < lines.length && isBlankQQBotMarkdownLine(lines[index] ?? "")) {
|
|
10557
|
+
index += 1;
|
|
10558
|
+
}
|
|
10559
|
+
if (index >= lines.length) {
|
|
10560
|
+
break;
|
|
10561
|
+
}
|
|
10562
|
+
const line = lines[index] ?? "";
|
|
10563
|
+
if (MARKDOWN_FENCE_RE.test(line)) {
|
|
10564
|
+
const result2 = collectQQBotFencedCodeBlock(lines, index);
|
|
10565
|
+
blocks.push(result2.block);
|
|
10566
|
+
index = result2.nextIndex;
|
|
10567
|
+
continue;
|
|
10568
|
+
}
|
|
10569
|
+
if (isQQBotMarkdownTableStart(lines, index)) {
|
|
10570
|
+
const result2 = collectQQBotMarkdownTableBlock(lines, index);
|
|
10571
|
+
blocks.push(result2.block);
|
|
10572
|
+
index = result2.nextIndex;
|
|
10573
|
+
continue;
|
|
10574
|
+
}
|
|
10575
|
+
if (MARKDOWN_THEMATIC_BREAK_RE.test(line)) {
|
|
10576
|
+
blocks.push({ kind: "thematic-break", text: line.trim() });
|
|
10577
|
+
index += 1;
|
|
10578
|
+
continue;
|
|
10579
|
+
}
|
|
10580
|
+
if (MARKDOWN_BLOCKQUOTE_RE.test(line)) {
|
|
10581
|
+
const result2 = collectQQBotBlockquoteBlock(lines, index);
|
|
10582
|
+
blocks.push(result2.block);
|
|
10583
|
+
index = result2.nextIndex;
|
|
10584
|
+
continue;
|
|
10585
|
+
}
|
|
10586
|
+
if (MARKDOWN_ATX_HEADING_RE.test(line)) {
|
|
10587
|
+
blocks.push({ kind: "heading", text: line.trimEnd() });
|
|
10588
|
+
index += 1;
|
|
10589
|
+
continue;
|
|
10590
|
+
}
|
|
10591
|
+
if (MARKDOWN_LIST_ITEM_RE.test(line)) {
|
|
10592
|
+
const result2 = collectQQBotListBlock(lines, index);
|
|
10593
|
+
blocks.push(result2.block);
|
|
10594
|
+
index = result2.nextIndex;
|
|
10595
|
+
continue;
|
|
10596
|
+
}
|
|
10597
|
+
const result = collectQQBotParagraphBlock(lines, index);
|
|
10598
|
+
blocks.push(result.block);
|
|
10599
|
+
index = result.nextIndex;
|
|
10600
|
+
}
|
|
10601
|
+
return blocks;
|
|
10602
|
+
}
|
|
10603
|
+
function hasQQBotBoundaryGuard(text) {
|
|
10604
|
+
return MARKDOWN_BOUNDARY_GUARD_RE.test(text);
|
|
10605
|
+
}
|
|
10606
|
+
function isQQBotSafeMarkdownBoundary(text, index) {
|
|
10607
|
+
const left = text.slice(Math.max(0, index - 3), index).replace(/\s+/g, "");
|
|
10608
|
+
const right = text.slice(index, Math.min(text.length, index + 3)).replace(/\s+/g, "");
|
|
10609
|
+
const leftEdge = left.slice(-1);
|
|
10610
|
+
const rightEdge = right.slice(0, 1);
|
|
10611
|
+
return !hasQQBotBoundaryGuard(leftEdge) && !hasQQBotBoundaryGuard(rightEdge);
|
|
10612
|
+
}
|
|
10613
|
+
function findQQBotRegexBoundary(text, limit, pattern) {
|
|
10614
|
+
const scopedText = text.slice(0, Math.min(limit + 1, text.length));
|
|
10615
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
10616
|
+
let match = regex.exec(scopedText);
|
|
10617
|
+
let lastBoundary;
|
|
10618
|
+
while (match) {
|
|
10619
|
+
const boundary = match.index + match[0].length;
|
|
10620
|
+
if (boundary > 0 && boundary <= limit && isQQBotSafeMarkdownBoundary(text, boundary)) {
|
|
10621
|
+
lastBoundary = boundary;
|
|
10622
|
+
}
|
|
10623
|
+
match = regex.exec(scopedText);
|
|
10624
|
+
}
|
|
10625
|
+
return lastBoundary;
|
|
10626
|
+
}
|
|
10627
|
+
function findQQBotFallbackBoundary(text, limit) {
|
|
10628
|
+
const minIndex = Math.max(1, limit - 120);
|
|
10629
|
+
for (let index = limit; index >= minIndex; index -= 1) {
|
|
10630
|
+
if (isQQBotSafeMarkdownBoundary(text, index)) {
|
|
10631
|
+
return index;
|
|
10632
|
+
}
|
|
10633
|
+
}
|
|
10634
|
+
return limit;
|
|
10635
|
+
}
|
|
10636
|
+
function findQQBotSafeSplitIndex(text, limit) {
|
|
10637
|
+
const boundaryPatterns = [
|
|
10638
|
+
/\n\n+/g,
|
|
10639
|
+
/\n/g,
|
|
10640
|
+
/[。!?.!?;;::](?:\s+|$)/g,
|
|
10641
|
+
/[,,](?:\s+|$)/g,
|
|
10642
|
+
/\s+/g
|
|
10643
|
+
];
|
|
10644
|
+
for (const pattern of boundaryPatterns) {
|
|
10645
|
+
const boundary = findQQBotRegexBoundary(text, limit, pattern);
|
|
10646
|
+
if (boundary && boundary > 0) {
|
|
10647
|
+
return boundary;
|
|
10648
|
+
}
|
|
10649
|
+
}
|
|
10650
|
+
return findQQBotFallbackBoundary(text, limit);
|
|
10651
|
+
}
|
|
10652
|
+
function splitQQBotHardText(text, limit) {
|
|
10653
|
+
if (limit <= 0 || text.length <= limit) {
|
|
10654
|
+
return [text];
|
|
10655
|
+
}
|
|
10656
|
+
const chunks = [];
|
|
10657
|
+
let remaining = text;
|
|
10658
|
+
while (remaining.length > limit) {
|
|
10659
|
+
chunks.push(remaining.slice(0, limit));
|
|
10660
|
+
remaining = remaining.slice(limit);
|
|
10661
|
+
}
|
|
10662
|
+
if (remaining) {
|
|
10663
|
+
chunks.push(remaining);
|
|
10664
|
+
}
|
|
10665
|
+
return chunks;
|
|
10666
|
+
}
|
|
10667
|
+
function splitQQBotTextSafely(text, limit, options) {
|
|
10668
|
+
if (limit <= 0 || text.length <= limit) {
|
|
10669
|
+
return [text];
|
|
10670
|
+
}
|
|
10671
|
+
const trimLeading = options?.trimLeading ?? true;
|
|
10672
|
+
const trimTrailing = options?.trimTrailing ?? true;
|
|
10673
|
+
const chunks = [];
|
|
10674
|
+
let remaining = text;
|
|
10675
|
+
while (remaining.length > limit) {
|
|
10676
|
+
const splitIndex = findQQBotSafeSplitIndex(remaining, limit);
|
|
10677
|
+
let nextChunk = remaining.slice(0, splitIndex);
|
|
10678
|
+
let nextRemaining = remaining.slice(splitIndex);
|
|
10679
|
+
if (trimTrailing) {
|
|
10680
|
+
nextChunk = nextChunk.trimEnd();
|
|
10681
|
+
}
|
|
10682
|
+
if (trimLeading) {
|
|
10683
|
+
nextRemaining = nextRemaining.trimStart();
|
|
10684
|
+
}
|
|
10685
|
+
if (!nextChunk) {
|
|
10686
|
+
const hardChunk = remaining.slice(0, limit);
|
|
10687
|
+
chunks.push(hardChunk);
|
|
10688
|
+
remaining = remaining.slice(hardChunk.length);
|
|
10689
|
+
continue;
|
|
10690
|
+
}
|
|
10691
|
+
chunks.push(nextChunk);
|
|
10692
|
+
remaining = nextRemaining;
|
|
10693
|
+
}
|
|
10694
|
+
const finalChunk = trimTrailing ? remaining.trimEnd() : remaining;
|
|
10695
|
+
if (finalChunk) {
|
|
10696
|
+
chunks.push(finalChunk);
|
|
10697
|
+
}
|
|
10698
|
+
return chunks;
|
|
10699
|
+
}
|
|
10700
|
+
function splitQQBotMarkdownLineBlock(text, limit) {
|
|
10701
|
+
if (limit <= 0 || text.length <= limit) {
|
|
10702
|
+
return [text];
|
|
10703
|
+
}
|
|
10704
|
+
const lines = text.split("\n");
|
|
10705
|
+
const chunks = [];
|
|
10706
|
+
let currentLines = [];
|
|
10707
|
+
const flushCurrent = () => {
|
|
10708
|
+
if (currentLines.length === 0) {
|
|
10709
|
+
return;
|
|
10710
|
+
}
|
|
10711
|
+
const chunk = currentLines.join("\n").trimEnd();
|
|
10712
|
+
if (chunk) {
|
|
10713
|
+
chunks.push(chunk);
|
|
10714
|
+
}
|
|
10715
|
+
currentLines = [];
|
|
10716
|
+
};
|
|
10717
|
+
for (const line of lines) {
|
|
10718
|
+
const candidate = currentLines.length > 0 ? `${currentLines.join("\n")}
|
|
10719
|
+
${line}` : line;
|
|
10720
|
+
if (candidate.length <= limit) {
|
|
10721
|
+
currentLines.push(line);
|
|
10722
|
+
continue;
|
|
10723
|
+
}
|
|
10724
|
+
flushCurrent();
|
|
10725
|
+
if (line.length <= limit) {
|
|
10726
|
+
currentLines.push(line);
|
|
10727
|
+
continue;
|
|
10728
|
+
}
|
|
10729
|
+
for (const piece of splitQQBotTextSafely(line, limit, {
|
|
10730
|
+
trimLeading: false,
|
|
10731
|
+
trimTrailing: false
|
|
10732
|
+
})) {
|
|
10733
|
+
if (piece) {
|
|
10734
|
+
chunks.push(piece);
|
|
10735
|
+
}
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
flushCurrent();
|
|
10739
|
+
return chunks;
|
|
10740
|
+
}
|
|
10741
|
+
function splitQQBotMarkdownTableBlock(text, limit) {
|
|
10742
|
+
if (limit <= 0 || text.length <= limit) {
|
|
10743
|
+
return [text];
|
|
10744
|
+
}
|
|
10745
|
+
const lines = text.split("\n");
|
|
10746
|
+
const header = lines[0] ?? "";
|
|
10747
|
+
const separator = lines[1] ?? "";
|
|
10748
|
+
const rows = lines.slice(2);
|
|
10749
|
+
const tablePrefix = `${header}
|
|
10750
|
+
${separator}`;
|
|
10751
|
+
const chunks = [];
|
|
10752
|
+
let currentRows = [];
|
|
10753
|
+
const flushCurrent = () => {
|
|
10754
|
+
if (currentRows.length === 0) {
|
|
10755
|
+
return;
|
|
10756
|
+
}
|
|
10757
|
+
chunks.push(`${tablePrefix}
|
|
10758
|
+
${currentRows.join("\n")}`);
|
|
10759
|
+
currentRows = [];
|
|
10760
|
+
};
|
|
10761
|
+
for (const row of rows) {
|
|
10762
|
+
const candidate = currentRows.length > 0 ? `${tablePrefix}
|
|
10763
|
+
${currentRows.join("\n")}
|
|
10764
|
+
${row}` : `${tablePrefix}
|
|
10765
|
+
${row}`;
|
|
10766
|
+
if (candidate.length <= limit) {
|
|
10767
|
+
currentRows.push(row);
|
|
10768
|
+
continue;
|
|
10769
|
+
}
|
|
10770
|
+
flushCurrent();
|
|
10771
|
+
if (`${tablePrefix}
|
|
10772
|
+
${row}`.length <= limit) {
|
|
10773
|
+
currentRows.push(row);
|
|
10774
|
+
continue;
|
|
10775
|
+
}
|
|
10776
|
+
const maxRowLength = Math.max(16, limit - tablePrefix.length - 1);
|
|
10777
|
+
for (const rowPiece of splitQQBotTextSafely(row, maxRowLength, {
|
|
10778
|
+
trimLeading: false,
|
|
10779
|
+
trimTrailing: false
|
|
10780
|
+
})) {
|
|
10781
|
+
chunks.push(`${tablePrefix}
|
|
10782
|
+
${rowPiece}`);
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10785
|
+
flushCurrent();
|
|
10786
|
+
return chunks.length > 0 ? chunks : [text];
|
|
10787
|
+
}
|
|
10788
|
+
function splitQQBotMarkdownCodeFence(text, limit) {
|
|
10789
|
+
if (limit <= 0 || text.length <= limit) {
|
|
10790
|
+
return [text];
|
|
10791
|
+
}
|
|
10792
|
+
const lines = text.split("\n");
|
|
10793
|
+
const openingLine = lines[0] ?? "```";
|
|
10794
|
+
const delimiter = resolveQQBotFenceDelimiter(openingLine) ?? "```";
|
|
10795
|
+
const hasClosingFence = lines.length > 1 && isQQBotFenceClosingLine(lines[lines.length - 1] ?? "", delimiter);
|
|
10796
|
+
const closingLine = hasClosingFence ? lines[lines.length - 1] ?? delimiter : delimiter;
|
|
10797
|
+
const codeLines = lines.slice(1, hasClosingFence ? -1 : lines.length);
|
|
10798
|
+
const fixedOverhead = openingLine.length + closingLine.length + 2;
|
|
10799
|
+
const availableLineLength = Math.max(1, limit - fixedOverhead);
|
|
10800
|
+
const chunks = [];
|
|
10801
|
+
let currentCodeLines = [];
|
|
10802
|
+
const flushCurrent = () => {
|
|
10803
|
+
if (currentCodeLines.length === 0) {
|
|
10804
|
+
return;
|
|
10805
|
+
}
|
|
10806
|
+
chunks.push(`${openingLine}
|
|
10807
|
+
${currentCodeLines.join("\n")}
|
|
10808
|
+
${closingLine}`);
|
|
10809
|
+
currentCodeLines = [];
|
|
10810
|
+
};
|
|
10811
|
+
for (const codeLine of codeLines) {
|
|
10812
|
+
const candidate = currentCodeLines.length > 0 ? `${openingLine}
|
|
10813
|
+
${currentCodeLines.join("\n")}
|
|
10814
|
+
${codeLine}
|
|
10815
|
+
${closingLine}` : `${openingLine}
|
|
10816
|
+
${codeLine}
|
|
10817
|
+
${closingLine}`;
|
|
10818
|
+
if (candidate.length <= limit) {
|
|
10819
|
+
currentCodeLines.push(codeLine);
|
|
10820
|
+
continue;
|
|
10821
|
+
}
|
|
10822
|
+
flushCurrent();
|
|
10823
|
+
if (`${openingLine}
|
|
10824
|
+
${codeLine}
|
|
10825
|
+
${closingLine}`.length <= limit) {
|
|
10826
|
+
currentCodeLines.push(codeLine);
|
|
10827
|
+
continue;
|
|
10828
|
+
}
|
|
10829
|
+
for (const linePiece of splitQQBotHardText(codeLine, availableLineLength)) {
|
|
10830
|
+
chunks.push(`${openingLine}
|
|
10831
|
+
${linePiece}
|
|
10832
|
+
${closingLine}`);
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
flushCurrent();
|
|
10836
|
+
return chunks.length > 0 ? chunks : [text];
|
|
10837
|
+
}
|
|
10838
|
+
function splitQQBotMarkdownBlock(block, limit) {
|
|
10839
|
+
if (limit <= 0 || block.text.length <= limit) {
|
|
10840
|
+
return [block.text];
|
|
10841
|
+
}
|
|
10842
|
+
switch (block.kind) {
|
|
10843
|
+
case "table":
|
|
10844
|
+
return splitQQBotMarkdownTableBlock(block.text, limit);
|
|
10845
|
+
case "code":
|
|
10846
|
+
return splitQQBotMarkdownCodeFence(block.text, limit);
|
|
10847
|
+
case "blockquote":
|
|
10848
|
+
return splitQQBotMarkdownLineBlock(block.text, limit);
|
|
10849
|
+
case "list":
|
|
10850
|
+
return splitQQBotMarkdownLineBlock(block.text, limit);
|
|
10851
|
+
case "paragraph":
|
|
10852
|
+
case "heading":
|
|
10853
|
+
return splitQQBotTextSafely(block.text, limit);
|
|
10854
|
+
case "thematic-break":
|
|
10855
|
+
return [block.text];
|
|
10856
|
+
default:
|
|
10857
|
+
return [block.text];
|
|
10858
|
+
}
|
|
10859
|
+
}
|
|
10860
|
+
function chunkQQBotStructuredMarkdown(text, limit) {
|
|
10861
|
+
const blocks = parseQQBotMarkdownBlocks(text);
|
|
10862
|
+
if (blocks.length === 0 || limit <= 0) {
|
|
10863
|
+
return [text.trim()];
|
|
10864
|
+
}
|
|
10865
|
+
const chunks = [];
|
|
10866
|
+
let currentPieces = [];
|
|
10867
|
+
let pendingPrefixPieces = [];
|
|
10868
|
+
const flushCurrent = () => {
|
|
10869
|
+
if (currentPieces.length === 0) {
|
|
10870
|
+
return;
|
|
10871
|
+
}
|
|
10872
|
+
const chunk = joinQQBotMarkdownPieces(currentPieces);
|
|
10873
|
+
if (chunk) {
|
|
10874
|
+
chunks.push(chunk);
|
|
10875
|
+
}
|
|
10876
|
+
currentPieces = [];
|
|
10877
|
+
};
|
|
10878
|
+
const appendPiece = (piece) => {
|
|
10879
|
+
if (!piece) {
|
|
10880
|
+
return;
|
|
10881
|
+
}
|
|
10882
|
+
const pieces = piece.length > limit ? splitQQBotTextSafely(piece, limit) : [piece];
|
|
10883
|
+
for (const nextPiece of pieces) {
|
|
10884
|
+
const normalizedPiece = nextPiece.trim();
|
|
10885
|
+
if (!normalizedPiece) {
|
|
10886
|
+
continue;
|
|
10887
|
+
}
|
|
10888
|
+
const candidate = joinQQBotMarkdownPieces([...currentPieces, normalizedPiece]);
|
|
10889
|
+
if (currentPieces.length === 0 || candidate.length <= limit) {
|
|
10890
|
+
currentPieces.push(normalizedPiece);
|
|
10891
|
+
continue;
|
|
10892
|
+
}
|
|
10893
|
+
flushCurrent();
|
|
10894
|
+
currentPieces.push(normalizedPiece);
|
|
10895
|
+
}
|
|
10896
|
+
};
|
|
10897
|
+
const consumePendingPrefix = (piece) => {
|
|
10898
|
+
if (pendingPrefixPieces.length === 0) {
|
|
10899
|
+
return piece;
|
|
10900
|
+
}
|
|
10901
|
+
const prefixed = joinQQBotMarkdownPieces([...pendingPrefixPieces, piece]);
|
|
10902
|
+
pendingPrefixPieces = [];
|
|
10903
|
+
return prefixed;
|
|
10904
|
+
};
|
|
10905
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
10906
|
+
const block = blocks[index];
|
|
10907
|
+
if (!block) {
|
|
10908
|
+
continue;
|
|
10909
|
+
}
|
|
10910
|
+
if (block.kind === "thematic-break") {
|
|
10911
|
+
if (currentPieces.length > 0) {
|
|
10912
|
+
const candidate = joinQQBotMarkdownPieces([...currentPieces, block.text]);
|
|
10913
|
+
if (candidate.length <= limit) {
|
|
10914
|
+
currentPieces.push(block.text);
|
|
10915
|
+
continue;
|
|
10916
|
+
}
|
|
10917
|
+
flushCurrent();
|
|
10918
|
+
}
|
|
10919
|
+
pendingPrefixPieces.push(block.text);
|
|
10920
|
+
continue;
|
|
10921
|
+
}
|
|
10922
|
+
if (block.kind === "heading") {
|
|
10923
|
+
const headingText = consumePendingPrefix(block.text);
|
|
10924
|
+
const nextBlock = blocks[index + 1];
|
|
10925
|
+
if (nextBlock && nextBlock.kind !== "thematic-break") {
|
|
10926
|
+
const nextPieces = splitQQBotMarkdownBlock(nextBlock, limit);
|
|
10927
|
+
const firstBodyPiece = nextPieces[0];
|
|
10928
|
+
if (firstBodyPiece) {
|
|
10929
|
+
const pairedText = joinQQBotMarkdownPieces([headingText, firstBodyPiece]);
|
|
10930
|
+
const pairedCandidate = joinQQBotMarkdownPieces([
|
|
10931
|
+
...currentPieces,
|
|
10932
|
+
headingText,
|
|
10933
|
+
firstBodyPiece
|
|
10934
|
+
]);
|
|
10935
|
+
if (pairedText.length <= limit && (currentPieces.length === 0 || pairedCandidate.length <= limit)) {
|
|
10936
|
+
currentPieces.push(headingText, firstBodyPiece);
|
|
10937
|
+
for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
|
|
10938
|
+
appendPiece(nextPieces[pieceIndex] ?? "");
|
|
10939
|
+
}
|
|
10940
|
+
index += 1;
|
|
10941
|
+
continue;
|
|
10942
|
+
}
|
|
10943
|
+
if (currentPieces.length > 0 && pairedText.length <= limit) {
|
|
10944
|
+
flushCurrent();
|
|
10945
|
+
currentPieces.push(headingText, firstBodyPiece);
|
|
10946
|
+
for (let pieceIndex = 1; pieceIndex < nextPieces.length; pieceIndex += 1) {
|
|
10947
|
+
appendPiece(nextPieces[pieceIndex] ?? "");
|
|
10948
|
+
}
|
|
10949
|
+
index += 1;
|
|
10950
|
+
continue;
|
|
10951
|
+
}
|
|
10952
|
+
}
|
|
10953
|
+
}
|
|
10954
|
+
appendPiece(headingText);
|
|
10955
|
+
continue;
|
|
10956
|
+
}
|
|
10957
|
+
const blockText = consumePendingPrefix(block.text);
|
|
10958
|
+
for (const piece of splitQQBotMarkdownBlock({ ...block, text: blockText }, limit)) {
|
|
10959
|
+
appendPiece(piece);
|
|
10960
|
+
}
|
|
10961
|
+
}
|
|
10962
|
+
if (pendingPrefixPieces.length > 0 && currentPieces.length > 0) {
|
|
10963
|
+
const trailingCandidate = joinQQBotMarkdownPieces([...currentPieces, ...pendingPrefixPieces]);
|
|
10964
|
+
if (trailingCandidate.length <= limit) {
|
|
10965
|
+
currentPieces.push(...pendingPrefixPieces);
|
|
10966
|
+
}
|
|
10967
|
+
}
|
|
10968
|
+
flushCurrent();
|
|
10969
|
+
return chunks.length > 0 ? chunks : [text.trim()];
|
|
10970
|
+
}
|
|
10971
|
+
function looksLikeStructuredMarkdown(text) {
|
|
10972
|
+
const normalized = normalizeQQBotMarkdownSegment(text);
|
|
10973
|
+
if (!normalized) {
|
|
10974
|
+
return false;
|
|
10975
|
+
}
|
|
10976
|
+
const lines = normalized.split("\n");
|
|
10977
|
+
if (hasQQBotMarkdownTable(normalized)) {
|
|
10978
|
+
return true;
|
|
10979
|
+
}
|
|
10980
|
+
return normalized.includes("\n\n") || lines.some((line) => MARKDOWN_ATX_HEADING_RE.test(line)) || lines.some((line) => MARKDOWN_BLOCKQUOTE_RE.test(line)) || lines.some((line) => MARKDOWN_FENCE_RE.test(line)) || lines.some((line) => MARKDOWN_THEMATIC_BREAK_RE.test(line)) || lines.some((line) => MARKDOWN_LIST_ITEM_RE.test(line)) || MARKDOWN_INLINE_STRUCTURE_RE.test(normalized);
|
|
10981
|
+
}
|
|
10982
|
+
function chunkC2CMarkdownText(params) {
|
|
10983
|
+
const normalized = params.text.trim();
|
|
10984
|
+
if (!normalized) {
|
|
10985
|
+
return [];
|
|
10986
|
+
}
|
|
10987
|
+
const strategy = params.strategy ?? "markdown-block";
|
|
10988
|
+
if (strategy === "length") {
|
|
10989
|
+
return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
|
|
10990
|
+
}
|
|
10991
|
+
if (params.limit <= 0 || !looksLikeStructuredMarkdown(normalized)) {
|
|
10992
|
+
return params.fallbackChunkText ? params.fallbackChunkText(normalized) : [normalized];
|
|
10993
|
+
}
|
|
10994
|
+
return chunkQQBotStructuredMarkdown(normalized, params.limit);
|
|
10995
|
+
}
|
|
9995
10996
|
async function sendQQBotMediaWithFallback(params) {
|
|
9996
10997
|
const { qqCfg, to, mediaQueue, replyToId, replyEventId, accountId, logger, onDelivered, onError } = params;
|
|
9997
10998
|
const outbound = params.outbound ?? qqbotOutbound;
|
|
10999
|
+
const shouldContinue = params.shouldContinue ?? (() => true);
|
|
9998
11000
|
for (const mediaUrl of mediaQueue) {
|
|
11001
|
+
if (!shouldContinue()) {
|
|
11002
|
+
return;
|
|
11003
|
+
}
|
|
9999
11004
|
const result = await outbound.sendMedia({
|
|
10000
11005
|
cfg: { channels: { qqbot: qqCfg } },
|
|
10001
11006
|
to,
|
|
@@ -10011,6 +11016,9 @@ async function sendQQBotMediaWithFallback(params) {
|
|
|
10011
11016
|
if (!fallback) {
|
|
10012
11017
|
continue;
|
|
10013
11018
|
}
|
|
11019
|
+
if (!shouldContinue()) {
|
|
11020
|
+
return;
|
|
11021
|
+
}
|
|
10014
11022
|
const fallbackResult = await outbound.sendText({
|
|
10015
11023
|
cfg: { channels: { qqbot: qqCfg } },
|
|
10016
11024
|
to,
|
|
@@ -10063,16 +11071,26 @@ async function dispatchToAgent(params) {
|
|
|
10063
11071
|
const { inbound, cfg, qqCfg, accountId, logger, route } = params;
|
|
10064
11072
|
const runtime2 = getQQBotRuntime();
|
|
10065
11073
|
const routeSessionKey = resolveQQBotRouteSessionKey(route);
|
|
11074
|
+
const queueKey = buildSessionDispatchQueueKey(route);
|
|
11075
|
+
const isFastAbortCommand = isQQBotFastAbortCommandText(inbound.content);
|
|
11076
|
+
const dispatchAbortGeneration = getSessionDispatchState(queueKey).abortGeneration;
|
|
11077
|
+
const shouldSuppressVisibleReplies = () => {
|
|
11078
|
+
const currentAbortGeneration = sessionDispatchQueue.get(queueKey)?.abortGeneration ?? dispatchAbortGeneration;
|
|
11079
|
+
return currentAbortGeneration !== dispatchAbortGeneration;
|
|
11080
|
+
};
|
|
10066
11081
|
const target = resolveChatTarget(inbound);
|
|
10067
11082
|
const outboundAccountId = route.accountId ?? accountId;
|
|
11083
|
+
const typingHeartbeatMode = resolveQQBotTypingHeartbeatMode(qqCfg);
|
|
11084
|
+
const typingHeartbeatIntervalMs = resolveQQBotTypingHeartbeatIntervalMs(qqCfg);
|
|
11085
|
+
const typingInputSeconds = resolveQQBotTypingInputSeconds(qqCfg);
|
|
10068
11086
|
let typingRefIdx;
|
|
10069
|
-
if (inbound.c2cOpenid) {
|
|
11087
|
+
if (inbound.c2cOpenid && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
|
|
10070
11088
|
const typing = await qqbotOutbound.sendTyping({
|
|
10071
11089
|
cfg: { channels: { qqbot: qqCfg } },
|
|
10072
11090
|
to: `user:${inbound.c2cOpenid}`,
|
|
10073
11091
|
replyToId: inbound.messageId,
|
|
10074
11092
|
replyEventId: inbound.eventId,
|
|
10075
|
-
inputSecond:
|
|
11093
|
+
inputSecond: typingInputSeconds,
|
|
10076
11094
|
accountId: outboundAccountId
|
|
10077
11095
|
});
|
|
10078
11096
|
if (typing.error) {
|
|
@@ -10088,10 +11106,15 @@ async function dispatchToAgent(params) {
|
|
|
10088
11106
|
}
|
|
10089
11107
|
let replyDelivered = false;
|
|
10090
11108
|
let groupMessageInterfaceBlocked = false;
|
|
11109
|
+
let lastVisibleOutboundAt = Date.now();
|
|
11110
|
+
let typingHeartbeat = null;
|
|
10091
11111
|
const markReplyDelivered = () => {
|
|
10092
11112
|
replyDelivered = true;
|
|
10093
11113
|
longTaskNotice.markReplyDelivered();
|
|
10094
11114
|
};
|
|
11115
|
+
const markVisibleOutboundStarted = () => {
|
|
11116
|
+
lastVisibleOutboundAt = Date.now();
|
|
11117
|
+
};
|
|
10095
11118
|
const markGroupMessageInterfaceBlocked = (error) => {
|
|
10096
11119
|
if (!isQQBotGroupMessageInterfaceBlocked(error)) return;
|
|
10097
11120
|
if (!groupMessageInterfaceBlocked) {
|
|
@@ -10099,11 +11122,40 @@ async function dispatchToAgent(params) {
|
|
|
10099
11122
|
}
|
|
10100
11123
|
groupMessageInterfaceBlocked = true;
|
|
10101
11124
|
};
|
|
11125
|
+
if (inbound.c2cOpenid && typingHeartbeatMode !== "none" && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
|
|
11126
|
+
typingHeartbeat = startQQBotTypingHeartbeat({
|
|
11127
|
+
intervalMs: typingHeartbeatIntervalMs,
|
|
11128
|
+
shouldRenew: () => {
|
|
11129
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11130
|
+
return false;
|
|
11131
|
+
}
|
|
11132
|
+
if (typingHeartbeatMode === "always") {
|
|
11133
|
+
return true;
|
|
11134
|
+
}
|
|
11135
|
+
return Date.now() - lastVisibleOutboundAt >= typingHeartbeatIntervalMs;
|
|
11136
|
+
},
|
|
11137
|
+
renew: async () => {
|
|
11138
|
+
try {
|
|
11139
|
+
const typing = await qqbotOutbound.sendTyping({
|
|
11140
|
+
cfg: { channels: { qqbot: qqCfg } },
|
|
11141
|
+
to: `user:${inbound.c2cOpenid}`,
|
|
11142
|
+
replyToId: inbound.messageId,
|
|
11143
|
+
replyEventId: inbound.eventId,
|
|
11144
|
+
inputSecond: typingInputSeconds,
|
|
11145
|
+
accountId: outboundAccountId
|
|
11146
|
+
});
|
|
11147
|
+
void typing;
|
|
11148
|
+
} catch {
|
|
11149
|
+
}
|
|
11150
|
+
}
|
|
11151
|
+
});
|
|
11152
|
+
}
|
|
10102
11153
|
const longTaskNotice = startLongTaskNoticeTimer({
|
|
10103
11154
|
delayMs: qqCfg.longTaskNoticeDelayMs ?? DEFAULT_LONG_TASK_NOTICE_DELAY_MS,
|
|
10104
11155
|
logger,
|
|
10105
11156
|
sendNotice: async () => {
|
|
10106
|
-
if (groupMessageInterfaceBlocked) return;
|
|
11157
|
+
if (groupMessageInterfaceBlocked || isFastAbortCommand || shouldSuppressVisibleReplies()) return;
|
|
11158
|
+
markVisibleOutboundStarted();
|
|
10107
11159
|
const result = await qqbotOutbound.sendText({
|
|
10108
11160
|
cfg: { channels: { qqbot: qqCfg } },
|
|
10109
11161
|
to: target.to,
|
|
@@ -10116,7 +11168,7 @@ async function dispatchToAgent(params) {
|
|
|
10116
11168
|
logger.warn(`send long-task notice failed: ${result.error}`);
|
|
10117
11169
|
markGroupMessageInterfaceBlocked(result.error);
|
|
10118
11170
|
} else {
|
|
10119
|
-
|
|
11171
|
+
markReplyDelivered();
|
|
10120
11172
|
}
|
|
10121
11173
|
}
|
|
10122
11174
|
});
|
|
@@ -10137,6 +11189,10 @@ async function dispatchToAgent(params) {
|
|
|
10137
11189
|
logger
|
|
10138
11190
|
});
|
|
10139
11191
|
if (qqCfg.asr?.enabled && resolvedAttachmentResult.hasVoiceAttachment && !resolvedAttachmentResult.hasVoiceTranscript) {
|
|
11192
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11193
|
+
return;
|
|
11194
|
+
}
|
|
11195
|
+
markVisibleOutboundStarted();
|
|
10140
11196
|
const fallback = await qqbotOutbound.sendText({
|
|
10141
11197
|
cfg: { channels: { qqbot: qqCfg } },
|
|
10142
11198
|
to: target.to,
|
|
@@ -10149,7 +11205,7 @@ async function dispatchToAgent(params) {
|
|
|
10149
11205
|
logger.error(`sendText ASR fallback failed: ${fallback.error}`);
|
|
10150
11206
|
markGroupMessageInterfaceBlocked(fallback.error);
|
|
10151
11207
|
} else {
|
|
10152
|
-
|
|
11208
|
+
markReplyDelivered();
|
|
10153
11209
|
}
|
|
10154
11210
|
return;
|
|
10155
11211
|
}
|
|
@@ -10246,29 +11302,43 @@ async function dispatchToAgent(params) {
|
|
|
10246
11302
|
}
|
|
10247
11303
|
finalCtx.BodyForAgent = appendCronHiddenPrompt(agentBody);
|
|
10248
11304
|
}
|
|
10249
|
-
if (storePath
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
11305
|
+
if (storePath) {
|
|
11306
|
+
const mainSessionKeyRaw = route.mainSessionKey;
|
|
11307
|
+
const mainSessionKey = typeof mainSessionKeyRaw === "string" && mainSessionKeyRaw.trim() ? mainSessionKeyRaw.trim() : void 0;
|
|
11308
|
+
const isGroup = inbound.type === "group" || inbound.type === "channel";
|
|
11309
|
+
const updateLastRoute = !isGroup ? {
|
|
11310
|
+
sessionKey: mainSessionKey ?? route.sessionKey,
|
|
11311
|
+
channel: "qqbot",
|
|
11312
|
+
to: stableTo,
|
|
11313
|
+
accountId: outboundAccountId
|
|
11314
|
+
} : void 0;
|
|
11315
|
+
const recordSessionKey = typeof finalCtx.SessionKey === "string" && finalCtx.SessionKey.trim() ? finalCtx.SessionKey : routeSessionKey;
|
|
11316
|
+
if (sessionApi?.recordInboundSession) {
|
|
11317
|
+
try {
|
|
11318
|
+
await sessionApi.recordInboundSession({
|
|
11319
|
+
storePath,
|
|
11320
|
+
sessionKey: recordSessionKey,
|
|
11321
|
+
ctx: finalCtx,
|
|
11322
|
+
updateLastRoute,
|
|
11323
|
+
onRecordError: (err) => {
|
|
11324
|
+
logger.warn(`failed to record inbound session: ${String(err)}`);
|
|
11325
|
+
}
|
|
11326
|
+
});
|
|
11327
|
+
} catch (err) {
|
|
11328
|
+
logger.warn(`failed to record inbound session: ${String(err)}`);
|
|
11329
|
+
}
|
|
11330
|
+
}
|
|
11331
|
+
if (sessionApi?.recordSessionMetaFromInbound) {
|
|
11332
|
+
try {
|
|
11333
|
+
await sessionApi.recordSessionMetaFromInbound({
|
|
11334
|
+
storePath,
|
|
11335
|
+
sessionKey: recordSessionKey,
|
|
11336
|
+
ctx: finalCtx,
|
|
11337
|
+
createIfMissing: true
|
|
11338
|
+
});
|
|
11339
|
+
} catch (err) {
|
|
11340
|
+
logger.warn(`failed to record inbound session meta: ${String(err)}`);
|
|
11341
|
+
}
|
|
10272
11342
|
}
|
|
10273
11343
|
}
|
|
10274
11344
|
const textApi = runtime2.channel?.text;
|
|
@@ -10296,10 +11366,13 @@ async function dispatchToAgent(params) {
|
|
|
10296
11366
|
const replyFinalOnly = qqCfg.replyFinalOnly ?? false;
|
|
10297
11367
|
const markdownSupport = qqCfg.markdownSupport ?? true;
|
|
10298
11368
|
const c2cMarkdownDeliveryMode = qqCfg.c2cMarkdownDeliveryMode ?? "proactive-table-only";
|
|
10299
|
-
const
|
|
11369
|
+
const c2cMarkdownChunkStrategy = qqCfg.c2cMarkdownChunkStrategy ?? "markdown-block";
|
|
11370
|
+
const isC2CTarget = isQQBotC2CTarget(target.to);
|
|
11371
|
+
const useC2CMarkdownTransport = markdownSupport && isC2CTarget;
|
|
10300
11372
|
let bufferedC2CMarkdownTexts = [];
|
|
10301
11373
|
let bufferedC2CMarkdownMediaUrls = [];
|
|
10302
11374
|
const bufferedC2CMarkdownMediaSeen = /* @__PURE__ */ new Set();
|
|
11375
|
+
const hasBufferedC2CMarkdownReply = () => bufferedC2CMarkdownTexts.length > 0 || bufferedC2CMarkdownMediaUrls.length > 0;
|
|
10303
11376
|
const bufferC2CMarkdownMedia = (url) => {
|
|
10304
11377
|
const next = url?.trim();
|
|
10305
11378
|
if (!next || bufferedC2CMarkdownMediaSeen.has(next)) return;
|
|
@@ -10307,12 +11380,18 @@ async function dispatchToAgent(params) {
|
|
|
10307
11380
|
bufferedC2CMarkdownMediaUrls.push(next);
|
|
10308
11381
|
};
|
|
10309
11382
|
const sendC2CMarkdownTransportPayload = async (params2) => {
|
|
11383
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11384
|
+
return;
|
|
11385
|
+
}
|
|
10310
11386
|
const normalizedText = normalizeQQBotRenderedMarkdown(params2.text);
|
|
10311
11387
|
const { markdownImageUrls, mediaQueue } = splitQQBotMarkdownTransportMediaUrls(params2.mediaUrls);
|
|
10312
11388
|
const finalMarkdownText = await normalizeQQBotMarkdownImages({
|
|
10313
11389
|
text: normalizedText,
|
|
10314
11390
|
appendImageUrls: markdownImageUrls
|
|
10315
11391
|
});
|
|
11392
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11393
|
+
return;
|
|
11394
|
+
}
|
|
10316
11395
|
const textReplyRefs = resolveQQBotTextReplyRefs({
|
|
10317
11396
|
to: target.to,
|
|
10318
11397
|
text: finalMarkdownText || normalizedText,
|
|
@@ -10321,52 +11400,63 @@ async function dispatchToAgent(params) {
|
|
|
10321
11400
|
replyToId: inbound.messageId,
|
|
10322
11401
|
replyEventId: inbound.eventId
|
|
10323
11402
|
});
|
|
10324
|
-
const
|
|
11403
|
+
const textChunks = finalMarkdownText ? chunkC2CMarkdownText({
|
|
11404
|
+
text: finalMarkdownText,
|
|
11405
|
+
limit,
|
|
11406
|
+
strategy: c2cMarkdownChunkStrategy,
|
|
11407
|
+
fallbackChunkText: chunkText
|
|
11408
|
+
}) : [];
|
|
10325
11409
|
const deliveryLabel = textReplyRefs.forceProactive ? "c2c-markdown-proactive" : "c2c-markdown-passive";
|
|
10326
11410
|
logger.info(
|
|
10327
|
-
`delivery=${deliveryLabel} to=${target.to}
|
|
11411
|
+
`delivery=${deliveryLabel} to=${target.to} chunks=${textChunks.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} phase=${params2.phase} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")} chunkStrategy=${c2cMarkdownChunkStrategy}`
|
|
10328
11412
|
);
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
mediaQueue,
|
|
10333
|
-
replyToId: textReplyRefs.replyToId,
|
|
10334
|
-
replyEventId: textReplyRefs.replyEventId,
|
|
10335
|
-
accountId: outboundAccountId,
|
|
10336
|
-
logger,
|
|
10337
|
-
onDelivered: () => {
|
|
10338
|
-
markReplyDelivered();
|
|
10339
|
-
},
|
|
10340
|
-
onError: (error) => {
|
|
10341
|
-
markGroupMessageInterfaceBlocked(error);
|
|
11413
|
+
if (!shouldSuppressVisibleReplies()) {
|
|
11414
|
+
if (mediaQueue.length > 0) {
|
|
11415
|
+
markVisibleOutboundStarted();
|
|
10342
11416
|
}
|
|
10343
|
-
|
|
11417
|
+
await sendQQBotMediaWithFallback({
|
|
11418
|
+
qqCfg,
|
|
11419
|
+
to: target.to,
|
|
11420
|
+
mediaQueue,
|
|
11421
|
+
replyToId: textReplyRefs.replyToId,
|
|
11422
|
+
replyEventId: textReplyRefs.replyEventId,
|
|
11423
|
+
accountId: outboundAccountId,
|
|
11424
|
+
logger,
|
|
11425
|
+
onDelivered: () => {
|
|
11426
|
+
markReplyDelivered();
|
|
11427
|
+
},
|
|
11428
|
+
onError: (error) => {
|
|
11429
|
+
markGroupMessageInterfaceBlocked(error);
|
|
11430
|
+
},
|
|
11431
|
+
shouldContinue: () => !shouldSuppressVisibleReplies()
|
|
11432
|
+
});
|
|
11433
|
+
}
|
|
10344
11434
|
if (!finalMarkdownText) {
|
|
10345
11435
|
return;
|
|
10346
11436
|
}
|
|
10347
|
-
for (let
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
11437
|
+
for (let chunkIndex = 0; chunkIndex < textChunks.length; chunkIndex += 1) {
|
|
11438
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11439
|
+
return;
|
|
11440
|
+
}
|
|
11441
|
+
const chunk = textChunks[chunkIndex] ?? "";
|
|
11442
|
+
logger.info(
|
|
11443
|
+
`delivery=${deliveryLabel} segment=1/1 chunk=${chunkIndex + 1}/${textChunks.length} phase=${params2.phase} preview=${formatQQBotOutboundPreview(chunk)}`
|
|
11444
|
+
);
|
|
11445
|
+
markVisibleOutboundStarted();
|
|
11446
|
+
const result = await qqbotOutbound.sendText({
|
|
11447
|
+
cfg: { channels: { qqbot: qqCfg } },
|
|
11448
|
+
to: target.to,
|
|
11449
|
+
text: chunk,
|
|
11450
|
+
replyToId: textReplyRefs.replyToId,
|
|
11451
|
+
replyEventId: textReplyRefs.replyEventId,
|
|
11452
|
+
accountId: outboundAccountId
|
|
11453
|
+
});
|
|
11454
|
+
if (result.error) {
|
|
11455
|
+
logger.error(`send QQ markdown reply failed: ${result.error}`);
|
|
11456
|
+
markGroupMessageInterfaceBlocked(result.error);
|
|
11457
|
+
} else {
|
|
11458
|
+
logger.info(`sent QQ markdown reply (phase=${params2.phase}, len=${chunk.length})`);
|
|
11459
|
+
markReplyDelivered();
|
|
10370
11460
|
}
|
|
10371
11461
|
}
|
|
10372
11462
|
};
|
|
@@ -10377,6 +11467,12 @@ async function dispatchToAgent(params) {
|
|
|
10377
11467
|
bufferedC2CMarkdownMediaSeen.clear();
|
|
10378
11468
|
return;
|
|
10379
11469
|
}
|
|
11470
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11471
|
+
bufferedC2CMarkdownTexts = [];
|
|
11472
|
+
bufferedC2CMarkdownMediaUrls = [];
|
|
11473
|
+
bufferedC2CMarkdownMediaSeen.clear();
|
|
11474
|
+
return;
|
|
11475
|
+
}
|
|
10380
11476
|
const combinedText = bufferedC2CMarkdownTexts.join("\n\n").trim();
|
|
10381
11477
|
const combinedMediaUrls = [...bufferedC2CMarkdownMediaUrls];
|
|
10382
11478
|
bufferedC2CMarkdownTexts = [];
|
|
@@ -10389,6 +11485,9 @@ async function dispatchToAgent(params) {
|
|
|
10389
11485
|
});
|
|
10390
11486
|
};
|
|
10391
11487
|
const deliver = async (payload, info) => {
|
|
11488
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11489
|
+
return;
|
|
11490
|
+
}
|
|
10392
11491
|
const typed = payload;
|
|
10393
11492
|
const extractedTextMedia = extractQQBotReplyMedia({
|
|
10394
11493
|
text: typed?.text ?? "",
|
|
@@ -10420,7 +11519,8 @@ async function dispatchToAgent(params) {
|
|
|
10420
11519
|
const textToSend = suppressText ? "" : cleanedText;
|
|
10421
11520
|
if (useC2CMarkdownTransport) {
|
|
10422
11521
|
const shouldBufferFinalOnlyPayload = replyFinalOnly && (!info?.kind || info.kind === "final");
|
|
10423
|
-
|
|
11522
|
+
const shouldBufferStructuredMarkdownPayload = !replyFinalOnly && c2cMarkdownChunkStrategy === "markdown-block" && info?.kind !== "tool" && looksLikeStructuredMarkdown(textToSend);
|
|
11523
|
+
if (shouldBufferFinalOnlyPayload || shouldBufferStructuredMarkdownPayload) {
|
|
10424
11524
|
if (textToSend) {
|
|
10425
11525
|
bufferedC2CMarkdownTexts = appendQQBotBufferedText(bufferedC2CMarkdownTexts, textToSend);
|
|
10426
11526
|
}
|
|
@@ -10429,6 +11529,12 @@ async function dispatchToAgent(params) {
|
|
|
10429
11529
|
}
|
|
10430
11530
|
return;
|
|
10431
11531
|
}
|
|
11532
|
+
if (hasBufferedC2CMarkdownReply()) {
|
|
11533
|
+
await flushBufferedC2CMarkdownReply();
|
|
11534
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11535
|
+
return;
|
|
11536
|
+
}
|
|
11537
|
+
}
|
|
10432
11538
|
await sendC2CMarkdownTransportPayload({
|
|
10433
11539
|
text: textToSend,
|
|
10434
11540
|
mediaUrls: mediaQueue,
|
|
@@ -10448,6 +11554,10 @@ async function dispatchToAgent(params) {
|
|
|
10448
11554
|
});
|
|
10449
11555
|
const chunks = chunkText(converted);
|
|
10450
11556
|
for (const chunk of chunks) {
|
|
11557
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11558
|
+
return;
|
|
11559
|
+
}
|
|
11560
|
+
markVisibleOutboundStarted();
|
|
10451
11561
|
const result = await qqbotOutbound.sendText({
|
|
10452
11562
|
cfg: { channels: { qqbot: qqCfg } },
|
|
10453
11563
|
to: target.to,
|
|
@@ -10464,6 +11574,12 @@ async function dispatchToAgent(params) {
|
|
|
10464
11574
|
}
|
|
10465
11575
|
}
|
|
10466
11576
|
}
|
|
11577
|
+
if (shouldSuppressVisibleReplies()) {
|
|
11578
|
+
return;
|
|
11579
|
+
}
|
|
11580
|
+
if (mediaQueue.length > 0) {
|
|
11581
|
+
markVisibleOutboundStarted();
|
|
11582
|
+
}
|
|
10467
11583
|
await sendQQBotMediaWithFallback({
|
|
10468
11584
|
qqCfg,
|
|
10469
11585
|
to: target.to,
|
|
@@ -10477,12 +11593,38 @@ async function dispatchToAgent(params) {
|
|
|
10477
11593
|
},
|
|
10478
11594
|
onError: (error) => {
|
|
10479
11595
|
markGroupMessageInterfaceBlocked(error);
|
|
10480
|
-
}
|
|
11596
|
+
},
|
|
11597
|
+
shouldContinue: () => !shouldSuppressVisibleReplies()
|
|
10481
11598
|
});
|
|
10482
11599
|
};
|
|
10483
11600
|
const humanDelay = replyApi.resolveHumanDelayConfig?.(cfg, route.agentId);
|
|
11601
|
+
const dispatchDirect = replyApi.dispatchReplyWithDispatcher;
|
|
10484
11602
|
const dispatchBuffered = replyApi.dispatchReplyWithBufferedBlockDispatcher;
|
|
10485
|
-
|
|
11603
|
+
const streamingReplyOptions = isC2CTarget && !replyFinalOnly ? {
|
|
11604
|
+
disableBlockStreaming: false
|
|
11605
|
+
} : void 0;
|
|
11606
|
+
if (isC2CTarget && !replyFinalOnly && dispatchDirect) {
|
|
11607
|
+
logger.debug(`[dispatch] mode=direct session=${routeSessionKey} to=${target.to}`);
|
|
11608
|
+
await dispatchDirect({
|
|
11609
|
+
ctx: finalCtx,
|
|
11610
|
+
cfg,
|
|
11611
|
+
dispatcherOptions: {
|
|
11612
|
+
deliver,
|
|
11613
|
+
humanDelay,
|
|
11614
|
+
onError: (err, info) => {
|
|
11615
|
+
logger.error(`${info.kind} reply failed: ${String(err)}`);
|
|
11616
|
+
},
|
|
11617
|
+
onSkip: (_payload, info) => {
|
|
11618
|
+
if (info.reason !== "silent") {
|
|
11619
|
+
logger.info(`reply skipped: ${info.reason}`);
|
|
11620
|
+
}
|
|
11621
|
+
}
|
|
11622
|
+
},
|
|
11623
|
+
replyOptions: streamingReplyOptions
|
|
11624
|
+
});
|
|
11625
|
+
await flushBufferedC2CMarkdownReply();
|
|
11626
|
+
} else if (dispatchBuffered) {
|
|
11627
|
+
logger.debug(`[dispatch] mode=buffered session=${routeSessionKey} to=${target.to}`);
|
|
10486
11628
|
await dispatchBuffered({
|
|
10487
11629
|
ctx: finalCtx,
|
|
10488
11630
|
cfg,
|
|
@@ -10497,10 +11639,12 @@ async function dispatchToAgent(params) {
|
|
|
10497
11639
|
logger.info(`reply skipped: ${info.reason}`);
|
|
10498
11640
|
}
|
|
10499
11641
|
}
|
|
10500
|
-
}
|
|
11642
|
+
},
|
|
11643
|
+
replyOptions: streamingReplyOptions
|
|
10501
11644
|
});
|
|
10502
11645
|
await flushBufferedC2CMarkdownReply();
|
|
10503
11646
|
} else {
|
|
11647
|
+
logger.debug(`[dispatch] mode=legacy session=${routeSessionKey} to=${target.to}`);
|
|
10504
11648
|
const dispatcherResult = replyApi.createReplyDispatcherWithTyping ? replyApi.createReplyDispatcherWithTyping({
|
|
10505
11649
|
deliver,
|
|
10506
11650
|
humanDelay,
|
|
@@ -10526,7 +11670,10 @@ async function dispatchToAgent(params) {
|
|
|
10526
11670
|
ctx: finalCtx,
|
|
10527
11671
|
cfg,
|
|
10528
11672
|
dispatcher: dispatcherResult.dispatcher,
|
|
10529
|
-
replyOptions:
|
|
11673
|
+
replyOptions: {
|
|
11674
|
+
...typeof dispatcherResult.replyOptions === "object" && dispatcherResult.replyOptions ? dispatcherResult.replyOptions : {},
|
|
11675
|
+
...streamingReplyOptions ?? {}
|
|
11676
|
+
}
|
|
10530
11677
|
});
|
|
10531
11678
|
dispatcherResult.markDispatchIdle?.();
|
|
10532
11679
|
await flushBufferedC2CMarkdownReply();
|
|
@@ -10535,8 +11682,9 @@ async function dispatchToAgent(params) {
|
|
|
10535
11682
|
inbound,
|
|
10536
11683
|
replyDelivered
|
|
10537
11684
|
});
|
|
10538
|
-
if (noReplyFallback && !groupMessageInterfaceBlocked) {
|
|
11685
|
+
if (noReplyFallback && !groupMessageInterfaceBlocked && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
|
|
10539
11686
|
logger.info("no visible reply generated for group mention; sending fallback text");
|
|
11687
|
+
markVisibleOutboundStarted();
|
|
10540
11688
|
const fallbackResult = await qqbotOutbound.sendText({
|
|
10541
11689
|
cfg: { channels: { qqbot: qqCfg } },
|
|
10542
11690
|
to: target.to,
|
|
@@ -10553,6 +11701,7 @@ async function dispatchToAgent(params) {
|
|
|
10553
11701
|
}
|
|
10554
11702
|
}
|
|
10555
11703
|
} finally {
|
|
11704
|
+
typingHeartbeat?.dispose();
|
|
10556
11705
|
longTaskNotice.dispose();
|
|
10557
11706
|
try {
|
|
10558
11707
|
await pruneInboundMediaDir({
|
|
@@ -10609,18 +11758,39 @@ async function handleQQBotDispatch(params) {
|
|
|
10609
11758
|
logger.info("qqbot disabled, ignoring inbound message");
|
|
10610
11759
|
return;
|
|
10611
11760
|
}
|
|
10612
|
-
const
|
|
11761
|
+
const senderNameResolution = resolveQQBotSenderName({
|
|
11762
|
+
inbound,
|
|
11763
|
+
cfg: params.cfg,
|
|
11764
|
+
accountId
|
|
11765
|
+
});
|
|
11766
|
+
const resolvedInbound = {
|
|
11767
|
+
...inbound,
|
|
11768
|
+
senderName: senderNameResolution.displayName
|
|
11769
|
+
};
|
|
11770
|
+
logQQBotSenderNameResolution({
|
|
11771
|
+
logger,
|
|
11772
|
+
inbound,
|
|
11773
|
+
accountId,
|
|
11774
|
+
resolution: senderNameResolution
|
|
11775
|
+
});
|
|
11776
|
+
const content = resolvedInbound.content.trim();
|
|
10613
11777
|
const inboundLogContent = sanitizeInboundLogText(
|
|
10614
11778
|
resolveInboundLogContent({
|
|
10615
11779
|
content,
|
|
10616
|
-
attachments:
|
|
11780
|
+
attachments: resolvedInbound.attachments
|
|
10617
11781
|
})
|
|
10618
11782
|
);
|
|
10619
|
-
logger.info(
|
|
10620
|
-
|
|
11783
|
+
logger.info(
|
|
11784
|
+
`[inbound-user] accountId=${accountId} senderId=${resolvedInbound.senderId} senderName=${JSON.stringify(resolvedInbound.senderName ?? resolvedInbound.senderId)} content=${inboundLogContent}`
|
|
11785
|
+
);
|
|
11786
|
+
if (!shouldHandleMessage(resolvedInbound, qqCfg, logger)) {
|
|
10621
11787
|
return;
|
|
10622
11788
|
}
|
|
10623
|
-
const knownTarget = resolveKnownQQBotTargetFromInbound({
|
|
11789
|
+
const knownTarget = resolveKnownQQBotTargetFromInbound({
|
|
11790
|
+
inbound: resolvedInbound,
|
|
11791
|
+
accountId,
|
|
11792
|
+
persistentDisplayName: senderNameResolution.persistentDisplayName
|
|
11793
|
+
});
|
|
10624
11794
|
if (knownTarget) {
|
|
10625
11795
|
try {
|
|
10626
11796
|
upsertKnownQQBotTarget({ target: knownTarget });
|
|
@@ -10628,7 +11798,7 @@ async function handleQQBotDispatch(params) {
|
|
|
10628
11798
|
logger.warn(`failed to record known qqbot target: ${String(err)}`);
|
|
10629
11799
|
}
|
|
10630
11800
|
}
|
|
10631
|
-
const attachmentCount =
|
|
11801
|
+
const attachmentCount = resolvedInbound.attachments?.length ?? 0;
|
|
10632
11802
|
if (attachmentCount > 0) {
|
|
10633
11803
|
logger.info(`inbound message includes ${attachmentCount} attachment(s)`);
|
|
10634
11804
|
}
|
|
@@ -10641,7 +11811,7 @@ async function handleQQBotDispatch(params) {
|
|
|
10641
11811
|
logger.warn("routing API not available");
|
|
10642
11812
|
return;
|
|
10643
11813
|
}
|
|
10644
|
-
const target = resolveChatTarget(
|
|
11814
|
+
const target = resolveChatTarget(resolvedInbound);
|
|
10645
11815
|
const route = routing({
|
|
10646
11816
|
cfg: params.cfg,
|
|
10647
11817
|
channel: "qqbot",
|
|
@@ -10649,7 +11819,7 @@ async function handleQQBotDispatch(params) {
|
|
|
10649
11819
|
peer: { kind: target.peerKind, id: target.peerId }
|
|
10650
11820
|
});
|
|
10651
11821
|
const effectiveSessionKey = resolveQQBotEffectiveSessionKey({
|
|
10652
|
-
inbound,
|
|
11822
|
+
inbound: resolvedInbound,
|
|
10653
11823
|
route,
|
|
10654
11824
|
accountId
|
|
10655
11825
|
});
|
|
@@ -10659,13 +11829,36 @@ async function handleQQBotDispatch(params) {
|
|
|
10659
11829
|
effectiveSessionKey
|
|
10660
11830
|
};
|
|
10661
11831
|
const queueKey = buildSessionDispatchQueueKey(resolvedRoute);
|
|
10662
|
-
if (
|
|
11832
|
+
if (isQQBotFastAbortCommandText(content)) {
|
|
11833
|
+
const routeSessionKey = resolveQQBotRouteSessionKey(resolvedRoute);
|
|
11834
|
+
markSessionDispatchAbort(queueKey);
|
|
11835
|
+
const droppedCount = dropQueuedSessionDispatches(queueKey);
|
|
11836
|
+
logger.info(
|
|
11837
|
+
`session fast-abort command detected; executing immediately sessionKey=${routeSessionKey}`
|
|
11838
|
+
);
|
|
11839
|
+
logger.info(
|
|
11840
|
+
`session fast-abort command dropped ${droppedCount} queued messages sessionKey=${routeSessionKey}`
|
|
11841
|
+
);
|
|
11842
|
+
await runImmediateSessionDispatch(
|
|
11843
|
+
queueKey,
|
|
11844
|
+
async () => dispatchToAgent({
|
|
11845
|
+
inbound: { ...resolvedInbound, content },
|
|
11846
|
+
cfg: params.cfg,
|
|
11847
|
+
qqCfg,
|
|
11848
|
+
accountId,
|
|
11849
|
+
logger,
|
|
11850
|
+
route: resolvedRoute
|
|
11851
|
+
})
|
|
11852
|
+
);
|
|
11853
|
+
return;
|
|
11854
|
+
}
|
|
11855
|
+
if (hasSessionDispatchBacklog(queueKey)) {
|
|
10663
11856
|
logger.info(`session busy; queueing inbound dispatch sessionKey=${resolveQQBotRouteSessionKey(resolvedRoute)}`);
|
|
10664
11857
|
}
|
|
10665
11858
|
await runSerializedSessionDispatch(
|
|
10666
11859
|
queueKey,
|
|
10667
11860
|
async () => dispatchToAgent({
|
|
10668
|
-
inbound: { ...
|
|
11861
|
+
inbound: { ...resolvedInbound, content },
|
|
10669
11862
|
cfg: params.cfg,
|
|
10670
11863
|
qqCfg,
|
|
10671
11864
|
accountId,
|
|
@@ -10693,6 +11886,10 @@ function formatGatewayConnectError(err) {
|
|
|
10693
11886
|
}
|
|
10694
11887
|
return String(err);
|
|
10695
11888
|
}
|
|
11889
|
+
function isConnectionIdle(conn) {
|
|
11890
|
+
if (!conn) return true;
|
|
11891
|
+
return !conn.socket && !conn.promise && !conn.connecting;
|
|
11892
|
+
}
|
|
10696
11893
|
var activeConnections = /* @__PURE__ */ new Map();
|
|
10697
11894
|
function getOrCreateConnection(accountId) {
|
|
10698
11895
|
let conn = activeConnections.get(accountId);
|
|
@@ -10722,7 +11919,10 @@ function clearTimers(conn) {
|
|
|
10722
11919
|
conn.reconnectTimer = null;
|
|
10723
11920
|
}
|
|
10724
11921
|
}
|
|
10725
|
-
function cleanupSocket(conn) {
|
|
11922
|
+
function cleanupSocket(conn, expectedSocket) {
|
|
11923
|
+
if (expectedSocket && conn.socket !== expectedSocket) {
|
|
11924
|
+
return false;
|
|
11925
|
+
}
|
|
10726
11926
|
clearTimers(conn);
|
|
10727
11927
|
if (conn.socket) {
|
|
10728
11928
|
try {
|
|
@@ -10733,6 +11933,7 @@ function cleanupSocket(conn) {
|
|
|
10733
11933
|
}
|
|
10734
11934
|
conn.socket = null;
|
|
10735
11935
|
}
|
|
11936
|
+
return true;
|
|
10736
11937
|
}
|
|
10737
11938
|
async function monitorQQBotProvider(opts = {}) {
|
|
10738
11939
|
const { config, runtime: runtime2, abortSignal, accountId = DEFAULT_ACCOUNT_ID, setStatus } = opts;
|
|
@@ -10740,11 +11941,16 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10740
11941
|
log: runtime2?.log,
|
|
10741
11942
|
error: runtime2?.error
|
|
10742
11943
|
});
|
|
10743
|
-
const
|
|
10744
|
-
if (
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
11944
|
+
const existingConn = activeConnections.get(accountId);
|
|
11945
|
+
if (!existingConn) ; else if (isConnectionIdle(existingConn)) {
|
|
11946
|
+
activeConnections.delete(accountId);
|
|
11947
|
+
}
|
|
11948
|
+
const conn = activeConnections.get(accountId);
|
|
11949
|
+
const existingPromise = conn?.promise;
|
|
11950
|
+
if (existingPromise) {
|
|
11951
|
+
return existingPromise;
|
|
11952
|
+
}
|
|
11953
|
+
if (conn?.socket) {
|
|
10748
11954
|
throw new Error(`QQBot monitor state invalid for account ${accountId}: active socket without promise`);
|
|
10749
11955
|
}
|
|
10750
11956
|
const qqCfg = config ? mergeQQBotAccountConfig(config, accountId) : void 0;
|
|
@@ -10754,18 +11960,20 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10754
11960
|
if (!qqCfg.appId || !qqCfg.clientSecret) {
|
|
10755
11961
|
throw new Error(`QQBot not configured for account ${accountId} (missing appId or clientSecret)`);
|
|
10756
11962
|
}
|
|
10757
|
-
|
|
11963
|
+
const nextConn = conn ?? getOrCreateConnection(accountId);
|
|
11964
|
+
nextConn.promise = new Promise((resolve3, reject) => {
|
|
10758
11965
|
let stopped = false;
|
|
10759
11966
|
const finish = (err) => {
|
|
10760
11967
|
if (stopped) return;
|
|
10761
11968
|
stopped = true;
|
|
10762
11969
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
10763
|
-
cleanupSocket(
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
11970
|
+
cleanupSocket(nextConn);
|
|
11971
|
+
nextConn.connecting = false;
|
|
11972
|
+
nextConn.sessionId = null;
|
|
11973
|
+
nextConn.lastSeq = null;
|
|
11974
|
+
nextConn.promise = null;
|
|
11975
|
+
nextConn.stop = null;
|
|
11976
|
+
nextConn.reconnectAttempt = 0;
|
|
10769
11977
|
activeConnections.delete(accountId);
|
|
10770
11978
|
{
|
|
10771
11979
|
resolve3();
|
|
@@ -10775,33 +11983,33 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10775
11983
|
logger.info("abort signal received, stopping gateway");
|
|
10776
11984
|
finish();
|
|
10777
11985
|
};
|
|
10778
|
-
|
|
11986
|
+
nextConn.stop = () => {
|
|
10779
11987
|
logger.info("stop requested");
|
|
10780
11988
|
finish();
|
|
10781
11989
|
};
|
|
10782
11990
|
const scheduleReconnect = (reason) => {
|
|
10783
11991
|
if (stopped) return;
|
|
10784
|
-
if (
|
|
10785
|
-
const delay = RECONNECT_DELAYS_MS[Math.min(
|
|
10786
|
-
|
|
11992
|
+
if (nextConn.reconnectTimer) return;
|
|
11993
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(nextConn.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
11994
|
+
nextConn.reconnectAttempt += 1;
|
|
10787
11995
|
logger.warn(`[reconnect] ${reason}; retry in ${delay}ms`);
|
|
10788
|
-
|
|
10789
|
-
|
|
11996
|
+
nextConn.reconnectTimer = setTimeout(() => {
|
|
11997
|
+
nextConn.reconnectTimer = null;
|
|
10790
11998
|
void connect();
|
|
10791
11999
|
}, delay);
|
|
10792
12000
|
};
|
|
10793
12001
|
const startHeartbeat = (intervalMs) => {
|
|
10794
|
-
if (
|
|
10795
|
-
clearInterval(
|
|
12002
|
+
if (nextConn.heartbeatTimer) {
|
|
12003
|
+
clearInterval(nextConn.heartbeatTimer);
|
|
10796
12004
|
}
|
|
10797
|
-
|
|
10798
|
-
if (!
|
|
10799
|
-
const payload = JSON.stringify({ op: 1, d:
|
|
10800
|
-
|
|
12005
|
+
nextConn.heartbeatTimer = setInterval(() => {
|
|
12006
|
+
if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
|
|
12007
|
+
const payload = JSON.stringify({ op: 1, d: nextConn.lastSeq });
|
|
12008
|
+
nextConn.socket.send(payload);
|
|
10801
12009
|
}, intervalMs);
|
|
10802
12010
|
};
|
|
10803
12011
|
const sendIdentify = (token) => {
|
|
10804
|
-
if (!
|
|
12012
|
+
if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
|
|
10805
12013
|
const payload = {
|
|
10806
12014
|
op: 2,
|
|
10807
12015
|
d: {
|
|
@@ -10810,10 +12018,10 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10810
12018
|
shard: [0, 1]
|
|
10811
12019
|
}
|
|
10812
12020
|
};
|
|
10813
|
-
|
|
12021
|
+
nextConn.socket.send(JSON.stringify(payload));
|
|
10814
12022
|
};
|
|
10815
12023
|
const sendResume = (token, session, seq) => {
|
|
10816
|
-
if (!
|
|
12024
|
+
if (!nextConn.socket || nextConn.socket.readyState !== WebSocket.OPEN) return;
|
|
10817
12025
|
const payload = {
|
|
10818
12026
|
op: 6,
|
|
10819
12027
|
d: {
|
|
@@ -10822,11 +12030,14 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10822
12030
|
seq
|
|
10823
12031
|
}
|
|
10824
12032
|
};
|
|
10825
|
-
|
|
12033
|
+
nextConn.socket.send(JSON.stringify(payload));
|
|
10826
12034
|
};
|
|
10827
|
-
const handleGatewayPayload = async (payload) => {
|
|
12035
|
+
const handleGatewayPayload = async (payload, activeSocket) => {
|
|
12036
|
+
if (stopped || nextConn.socket !== activeSocket) {
|
|
12037
|
+
return;
|
|
12038
|
+
}
|
|
10828
12039
|
if (typeof payload.s === "number") {
|
|
10829
|
-
|
|
12040
|
+
nextConn.lastSeq = payload.s;
|
|
10830
12041
|
}
|
|
10831
12042
|
switch (payload.op) {
|
|
10832
12043
|
case 10: {
|
|
@@ -10834,8 +12045,11 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10834
12045
|
const interval = hello?.heartbeat_interval ?? 3e4;
|
|
10835
12046
|
startHeartbeat(interval);
|
|
10836
12047
|
const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
|
|
10837
|
-
if (
|
|
10838
|
-
|
|
12048
|
+
if (stopped || nextConn.socket !== activeSocket) {
|
|
12049
|
+
return;
|
|
12050
|
+
}
|
|
12051
|
+
if (nextConn.sessionId && typeof nextConn.lastSeq === "number") {
|
|
12052
|
+
sendResume(token, nextConn.sessionId, nextConn.lastSeq);
|
|
10839
12053
|
} else {
|
|
10840
12054
|
sendIdentify(token);
|
|
10841
12055
|
}
|
|
@@ -10845,14 +12059,18 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10845
12059
|
setStatus?.({ lastEventAt: Date.now() });
|
|
10846
12060
|
return;
|
|
10847
12061
|
case 7:
|
|
10848
|
-
cleanupSocket(
|
|
12062
|
+
if (!cleanupSocket(nextConn, activeSocket)) {
|
|
12063
|
+
return;
|
|
12064
|
+
}
|
|
10849
12065
|
scheduleReconnect("server requested reconnect");
|
|
10850
12066
|
return;
|
|
10851
12067
|
case 9:
|
|
10852
|
-
|
|
10853
|
-
|
|
12068
|
+
nextConn.sessionId = null;
|
|
12069
|
+
nextConn.lastSeq = null;
|
|
10854
12070
|
clearTokenCache(qqCfg.appId);
|
|
10855
|
-
cleanupSocket(
|
|
12071
|
+
if (!cleanupSocket(nextConn, activeSocket)) {
|
|
12072
|
+
return;
|
|
12073
|
+
}
|
|
10856
12074
|
scheduleReconnect("invalid session");
|
|
10857
12075
|
return;
|
|
10858
12076
|
case 0: {
|
|
@@ -10860,14 +12078,14 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10860
12078
|
if (eventType === "READY") {
|
|
10861
12079
|
const ready = payload.d;
|
|
10862
12080
|
if (ready?.session_id) {
|
|
10863
|
-
|
|
12081
|
+
nextConn.sessionId = ready.session_id;
|
|
10864
12082
|
}
|
|
10865
|
-
|
|
12083
|
+
nextConn.reconnectAttempt = 0;
|
|
10866
12084
|
logger.info("gateway ready");
|
|
10867
12085
|
return;
|
|
10868
12086
|
}
|
|
10869
12087
|
if (eventType === "RESUMED") {
|
|
10870
|
-
|
|
12088
|
+
nextConn.reconnectAttempt = 0;
|
|
10871
12089
|
logger.info("gateway resumed");
|
|
10872
12090
|
return;
|
|
10873
12091
|
}
|
|
@@ -10888,15 +12106,21 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10888
12106
|
}
|
|
10889
12107
|
};
|
|
10890
12108
|
const connect = async () => {
|
|
10891
|
-
if (stopped ||
|
|
10892
|
-
|
|
12109
|
+
if (stopped || nextConn.connecting) return;
|
|
12110
|
+
nextConn.connecting = true;
|
|
10893
12111
|
try {
|
|
10894
|
-
cleanupSocket(
|
|
12112
|
+
cleanupSocket(nextConn);
|
|
10895
12113
|
const token = await getAccessToken(qqCfg.appId, qqCfg.clientSecret);
|
|
12114
|
+
if (stopped) return;
|
|
10896
12115
|
const gatewayUrl = await getGatewayUrl(token);
|
|
12116
|
+
if (stopped) return;
|
|
10897
12117
|
logger.info(`connecting gateway: ${gatewayUrl}`);
|
|
10898
12118
|
const ws = new WebSocket(gatewayUrl);
|
|
10899
|
-
|
|
12119
|
+
nextConn.socket = ws;
|
|
12120
|
+
if (stopped) {
|
|
12121
|
+
cleanupSocket(nextConn, ws);
|
|
12122
|
+
return;
|
|
12123
|
+
}
|
|
10900
12124
|
ws.on("open", () => {
|
|
10901
12125
|
logger.info("gateway socket opened");
|
|
10902
12126
|
});
|
|
@@ -10909,24 +12133,29 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10909
12133
|
logger.warn(`failed to parse gateway payload: ${String(err)}`);
|
|
10910
12134
|
return;
|
|
10911
12135
|
}
|
|
10912
|
-
void handleGatewayPayload(payload).catch((err) => {
|
|
12136
|
+
void handleGatewayPayload(payload, ws).catch((err) => {
|
|
10913
12137
|
logger.error(`gateway dispatch error: ${String(err)}`);
|
|
10914
12138
|
});
|
|
10915
12139
|
});
|
|
10916
12140
|
ws.on("close", (code, reason) => {
|
|
12141
|
+
if (!cleanupSocket(nextConn, ws)) {
|
|
12142
|
+
return;
|
|
12143
|
+
}
|
|
10917
12144
|
logger.warn(`gateway socket closed (${code}) ${String(reason)}`);
|
|
10918
|
-
cleanupSocket(conn);
|
|
10919
12145
|
scheduleReconnect("socket closed");
|
|
10920
12146
|
});
|
|
10921
12147
|
ws.on("error", (err) => {
|
|
12148
|
+
if (stopped || nextConn.socket !== ws) {
|
|
12149
|
+
return;
|
|
12150
|
+
}
|
|
10922
12151
|
logger.error(`gateway socket error: ${String(err)}`);
|
|
10923
12152
|
});
|
|
10924
12153
|
} catch (err) {
|
|
10925
12154
|
logger.error(`gateway connect failed: ${formatGatewayConnectError(err)}`);
|
|
10926
|
-
cleanupSocket(
|
|
12155
|
+
cleanupSocket(nextConn);
|
|
10927
12156
|
scheduleReconnect("connect failed");
|
|
10928
12157
|
} finally {
|
|
10929
|
-
|
|
12158
|
+
nextConn.connecting = false;
|
|
10930
12159
|
}
|
|
10931
12160
|
};
|
|
10932
12161
|
if (abortSignal?.aborted) {
|
|
@@ -10936,7 +12165,7 @@ async function monitorQQBotProvider(opts = {}) {
|
|
|
10936
12165
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
10937
12166
|
void connect();
|
|
10938
12167
|
});
|
|
10939
|
-
return
|
|
12168
|
+
return nextConn.promise;
|
|
10940
12169
|
}
|
|
10941
12170
|
function stopQQBotMonitorForAccount(accountId = DEFAULT_ACCOUNT_ID) {
|
|
10942
12171
|
const conn = activeConnections.get(accountId);
|
|
@@ -10973,7 +12202,8 @@ function resolveQQBotAccount(params) {
|
|
|
10973
12202
|
configured,
|
|
10974
12203
|
appId: credentials?.appId,
|
|
10975
12204
|
markdownSupport: merged.markdownSupport ?? true,
|
|
10976
|
-
c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only"
|
|
12205
|
+
c2cMarkdownDeliveryMode: merged.c2cMarkdownDeliveryMode ?? "proactive-table-only",
|
|
12206
|
+
c2cMarkdownChunkStrategy: merged.c2cMarkdownChunkStrategy ?? "markdown-block"
|
|
10977
12207
|
};
|
|
10978
12208
|
}
|
|
10979
12209
|
var qqbotPlugin = {
|
|
@@ -11051,6 +12281,10 @@ var qqbotPlugin = {
|
|
|
11051
12281
|
defaultAccount: { type: "string" },
|
|
11052
12282
|
appId: { type: ["string", "number"] },
|
|
11053
12283
|
clientSecret: { type: "string" },
|
|
12284
|
+
displayAliases: {
|
|
12285
|
+
type: "object",
|
|
12286
|
+
additionalProperties: { type: "string" }
|
|
12287
|
+
},
|
|
11054
12288
|
asr: {
|
|
11055
12289
|
type: "object",
|
|
11056
12290
|
additionalProperties: false,
|
|
@@ -11066,6 +12300,10 @@ var qqbotPlugin = {
|
|
|
11066
12300
|
type: "string",
|
|
11067
12301
|
enum: ["passive", "proactive-table-only", "proactive-all"]
|
|
11068
12302
|
},
|
|
12303
|
+
c2cMarkdownChunkStrategy: {
|
|
12304
|
+
type: "string",
|
|
12305
|
+
enum: ["markdown-block", "length"]
|
|
12306
|
+
},
|
|
11069
12307
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
11070
12308
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
11071
12309
|
requireMention: { type: "boolean" },
|
|
@@ -11096,6 +12334,10 @@ var qqbotPlugin = {
|
|
|
11096
12334
|
enabled: { type: "boolean" },
|
|
11097
12335
|
appId: { type: ["string", "number"] },
|
|
11098
12336
|
clientSecret: { type: "string" },
|
|
12337
|
+
displayAliases: {
|
|
12338
|
+
type: "object",
|
|
12339
|
+
additionalProperties: { type: "string" }
|
|
12340
|
+
},
|
|
11099
12341
|
asr: {
|
|
11100
12342
|
type: "object",
|
|
11101
12343
|
additionalProperties: false,
|
|
@@ -11111,6 +12353,10 @@ var qqbotPlugin = {
|
|
|
11111
12353
|
type: "string",
|
|
11112
12354
|
enum: ["passive", "proactive-table-only", "proactive-all"]
|
|
11113
12355
|
},
|
|
12356
|
+
c2cMarkdownChunkStrategy: {
|
|
12357
|
+
type: "string",
|
|
12358
|
+
enum: ["markdown-block", "length"]
|
|
12359
|
+
},
|
|
11114
12360
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
11115
12361
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
11116
12362
|
requireMention: { type: "boolean" },
|
|
@@ -11262,7 +12508,9 @@ var qqbotPlugin = {
|
|
|
11262
12508
|
ctx.log?.info(`[qqbot] starting gateway for account ${ctx.accountId}`);
|
|
11263
12509
|
if (ctx.runtime) {
|
|
11264
12510
|
const candidate = ctx.runtime;
|
|
11265
|
-
|
|
12511
|
+
const hasRouting = Boolean(candidate.channel?.routing?.resolveAgentRoute);
|
|
12512
|
+
const hasReply = Boolean(candidate.channel?.reply?.dispatchReplyWithDispatcher) || Boolean(candidate.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) || Boolean(candidate.channel?.reply?.dispatchReplyFromConfig);
|
|
12513
|
+
if (hasRouting && hasReply) {
|
|
11266
12514
|
setQQBotRuntime(ctx.runtime);
|
|
11267
12515
|
}
|
|
11268
12516
|
}
|
|
@@ -11298,6 +12546,10 @@ var plugin = {
|
|
|
11298
12546
|
defaultAccount: { type: "string" },
|
|
11299
12547
|
appId: { type: ["string", "number"] },
|
|
11300
12548
|
clientSecret: { type: "string" },
|
|
12549
|
+
displayAliases: {
|
|
12550
|
+
type: "object",
|
|
12551
|
+
additionalProperties: { type: "string" }
|
|
12552
|
+
},
|
|
11301
12553
|
asr: {
|
|
11302
12554
|
type: "object",
|
|
11303
12555
|
additionalProperties: false,
|
|
@@ -11313,6 +12565,10 @@ var plugin = {
|
|
|
11313
12565
|
type: "string",
|
|
11314
12566
|
enum: ["passive", "proactive-table-only", "proactive-all"]
|
|
11315
12567
|
},
|
|
12568
|
+
c2cMarkdownChunkStrategy: {
|
|
12569
|
+
type: "string",
|
|
12570
|
+
enum: ["markdown-block", "length"]
|
|
12571
|
+
},
|
|
11316
12572
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
11317
12573
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
11318
12574
|
requireMention: { type: "boolean" },
|
|
@@ -11343,6 +12599,10 @@ var plugin = {
|
|
|
11343
12599
|
enabled: { type: "boolean" },
|
|
11344
12600
|
appId: { type: ["string", "number"] },
|
|
11345
12601
|
clientSecret: { type: "string" },
|
|
12602
|
+
displayAliases: {
|
|
12603
|
+
type: "object",
|
|
12604
|
+
additionalProperties: { type: "string" }
|
|
12605
|
+
},
|
|
11346
12606
|
asr: {
|
|
11347
12607
|
type: "object",
|
|
11348
12608
|
additionalProperties: false,
|
|
@@ -11358,6 +12618,10 @@ var plugin = {
|
|
|
11358
12618
|
type: "string",
|
|
11359
12619
|
enum: ["passive", "proactive-table-only", "proactive-all"]
|
|
11360
12620
|
},
|
|
12621
|
+
c2cMarkdownChunkStrategy: {
|
|
12622
|
+
type: "string",
|
|
12623
|
+
enum: ["markdown-block", "length"]
|
|
12624
|
+
},
|
|
11361
12625
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
11362
12626
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
11363
12627
|
requireMention: { type: "boolean" },
|