refactor: prune stale qqbot helpers

This commit is contained in:
Peter Steinberger
2026-05-01 09:10:36 +01:00
parent 8a399ec5b4
commit 4ee6068ced
6 changed files with 3 additions and 183 deletions

View File

@@ -11,10 +11,7 @@
* QQBot falls back to "always handle, anyone can approve".
*/
import {
createChannelApprovalCapability,
splitChannelApprovalCapability,
} from "openclaw/plugin-sdk/approval-delivery-runtime";
import { createChannelApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime";
import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
import { resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime";
@@ -232,8 +229,6 @@ function createQQBotApprovalCapability(): ChannelApprovalCapability {
export const qqbotApprovalCapability = createQQBotApprovalCapability();
export const qqbotNativeApprovalAdapter = splitChannelApprovalCapability(qqbotApprovalCapability);
let _cachedCapability: ChannelApprovalCapability | undefined;
export function getQQBotApprovalCapability(): ChannelApprovalCapability {

View File

@@ -12,13 +12,6 @@ function isQQBotAccountConfigured(cfg: OpenClawConfig, accountId: string): boole
return Boolean(account.appId && account.clientSecret);
}
export async function detectQQBotConfigured(
cfg: OpenClawConfig,
accountId: string,
): Promise<boolean> {
return isQQBotAccountConfigured(cfg, accountId);
}
async function linkViaQrCode(params: {
cfg: OpenClawConfig;
accountId: string;

View File

@@ -238,32 +238,6 @@ function tailFileLines(
}
}
function normalizeCommandAllowlistEntry(entry: unknown): string {
if (
typeof entry === "string" ||
typeof entry === "number" ||
typeof entry === "boolean" ||
typeof entry === "bigint"
) {
return `${entry}`
.trim()
.replace(/^qqbot:\s*/i, "")
.trim();
}
return "";
}
export function hasExplicitCommandAllowlist(accountConfig?: Record<string, unknown>): boolean {
const allowFrom = accountConfig?.allowFrom;
if (!Array.isArray(allowFrom) || allowFrom.length === 0) {
return false;
}
return allowFrom.every((entry) => {
const normalized = normalizeCommandAllowlistEntry(entry);
return normalized.length > 0 && normalized !== "*";
});
}
/**
* Build the /bot-logs result: collect recent log files, write them to a temp file.
*/

View File

@@ -193,8 +193,8 @@ export interface FirstClosedMediaTag {
/**
* 在文本中查找**第一个**完整闭合的媒体标签
*
* 与 splitByMediaTags 不同,此函数只匹配一个标签就停止,
* 用于流式场景的"循环消费"模式:每次处理一个标签,更新偏移,再找下一个。
* 只匹配一个标签就停止,用于流式场景的"循环消费"模式:
* 每次处理一个标签,更新偏移,再找下一个。
*
* @param text 待检查的文本(应已 normalize 过)
* @returns 第一个闭合标签的信息,没有则返回 null
@@ -250,119 +250,6 @@ export function findFirstClosedMediaTag(
return null;
}
/**
* 媒体标签拆分结果
*/
export interface MediaSplitResult {
/** 是否包含媒体标签 */
hasMediaTags: boolean;
/** 媒体标签前的纯文本 */
textBeforeFirstTag: string;
/** 媒体标签后的剩余文本 */
textAfterLastTag: string;
/** 完整的发送队列(标签间的文本 + 媒体项) */
mediaQueue: SendQueueItem[];
}
/**
* 将文本按富媒体标签拆分为三部分
*
* 用于两个场景:
* 1. 流式模式:中断-恢复流程(标签前文本 → 结束流式 → 发送媒体 → 新流式 → 标签后文本)
* 2. 普通模式:构建按顺序发送的队列
*/
export function splitByMediaTags(
text: string,
log?: {
info?: (msg: string) => void;
debug?: (msg: string) => void;
error?: (msg: string) => void;
},
): MediaSplitResult {
const normalized = normalizeMediaTags(text);
const regex = createMediaTagRegex();
// 过滤掉代码块内的匹配
const matches = [...normalized.matchAll(regex)].filter(
(m) => !isInsideCodeBlock(normalized, m.index),
);
if (matches.length === 0) {
return {
hasMediaTags: false,
textBeforeFirstTag: normalized,
textAfterLastTag: "",
mediaQueue: [],
};
}
// 第一个标签前的纯文本
const firstMatch = matches[0];
const textBeforeFirstTag = normalized
.slice(0, firstMatch.index)
.replace(/\n{3,}/g, "\n\n")
.trim();
// 最后一个标签后的纯文本
const lastMatch = matches[matches.length - 1];
const lastMatchEnd = lastMatch.index + lastMatch[0].length;
const textAfterLastTag = normalized
.slice(lastMatchEnd)
.replace(/\n{3,}/g, "\n\n")
.trim();
// 构建媒体发送队列
const mediaQueue: SendQueueItem[] = [];
let lastIndex = firstMatch.index;
for (const match of matches) {
// 标签前的文本(标签之间的间隔文本)
const textBetween = normalized
.slice(lastIndex, match.index)
.replace(/\n{3,}/g, "\n\n")
.trim();
if (textBetween && lastIndex !== firstMatch.index) {
// 只添加非首段的间隔文本(首段由 textBeforeFirstTag 覆盖)
mediaQueue.push({ type: "text", content: textBetween });
}
// 解析标签内容
const tagName = match[1].toLowerCase();
let mediaPath = match[2]?.trim() ?? "";
// 剥离 MEDIA: 前缀
if (mediaPath.startsWith("MEDIA:")) {
mediaPath = mediaPath.slice("MEDIA:".length);
}
mediaPath = normalizePath(mediaPath);
// 修复路径编码问题
mediaPath = fixPathEncoding(mediaPath, log);
// 根据标签类型加入队列
const typeMap: Record<string, SendQueueItem["type"]> = {
qqimg: "image",
qqvoice: "voice",
qqvideo: "video",
qqfile: "file",
qqmedia: "media",
};
const itemType = typeMap[tagName] ?? "image";
if (mediaPath) {
mediaQueue.push({ type: itemType, content: mediaPath });
log?.info?.(`Found ${itemType} in <${tagName}>: ${mediaPath.slice(0, 80)}`);
}
lastIndex = match.index + match[0].length;
}
return {
hasMediaTags: true,
textBeforeFirstTag,
textAfterLastTag,
mediaQueue,
};
}
// ============ 发送队列执行 ============
/**

View File

@@ -66,24 +66,6 @@ export function parseTarget(to: string): ParsedTarget {
return { type: "c2c", id };
}
/**
* Map a parsed target type to a ChatScope for API calls.
*
* Channel and DM targets are not C2C/Group scoped and should be handled
* separately by the caller.
*
* @returns `'c2c'` or `'group'`, or `undefined` for channel targets.
*/
export function targetToChatScope(target: ParsedTarget): "c2c" | "group" | undefined {
if (target.type === "c2c") {
return "c2c";
}
if (target.type === "group") {
return "group";
}
return undefined;
}
/**
* Normalize a QQ Bot target string into the canonical `qqbot:...` form.
*

View File

@@ -215,15 +215,4 @@ const qqbotExecApprovalProfile = createChannelExecApprovalProfile({
export const isQQBotExecApprovalClientEnabled = qqbotExecApprovalProfile.isClientEnabled;
export const isQQBotExecApprovalApprover = qqbotExecApprovalProfile.isApprover;
export const isQQBotExecApprovalAuthorizedSender = qqbotExecApprovalProfile.isAuthorizedSender;
export const resolveQQBotExecApprovalTarget = qqbotExecApprovalProfile.resolveTarget;
export const shouldHandleQQBotExecApprovalRequest = qqbotExecApprovalProfile.shouldHandleRequest;
export function isQQBotExecApprovalHandlerConfigured(params: {
cfg: OpenClawConfig;
accountId?: string | null;
}): boolean {
return isChannelExecApprovalClientEnabledFromConfig({
enabled: resolveQQBotExecApprovalConfig(params)?.enabled,
approverCount: getQQBotExecApprovalApprovers(params).length,
});
}