refactor: trim qqbot helper exports

This commit is contained in:
Peter Steinberger
2026-05-01 18:44:46 +01:00
parent 7897ca90b7
commit a607661a71
37 changed files with 70 additions and 105 deletions

View File

@@ -29,19 +29,6 @@
import type { FetchMediaOptions, FetchMediaResult, SecretInputRef } from "./types.js";
// ============ Re-exports (port interfaces) ============
export type { HistoryPort, HistoryEntryLike } from "./history.port.js";
export type {
MentionGatePort,
MentionFacts,
MentionPolicy,
MentionGateDecision,
ImplicitMentionKind,
} from "./mention-gate.port.js";
export type { AudioConvertPort, OutboundAudioPort } from "./audio.port.js";
export type { CommandsPort, ApproveRuntimeGetter } from "./commands.port.js";
// ============ EngineAdapters (aggregated port injection) ============
/**

View File

@@ -8,11 +8,7 @@
*/
/** Implicit mention kind aligned with SDK's `InboundImplicitMentionKind`. */
export type ImplicitMentionKind =
| "reply_to_bot"
| "quoted_bot"
| "bot_thread_participant"
| "native";
type ImplicitMentionKind = "reply_to_bot" | "quoted_bot" | "bot_thread_participant" | "native";
/** Facts about the current message's mention state. */
export interface MentionFacts {

View File

@@ -42,14 +42,14 @@ export interface PluginApprovalRequest {
};
}
export type ApprovalDecision = "allow-once" | "allow-always" | "deny";
type ApprovalDecision = "allow-once" | "allow-always" | "deny";
export interface ApprovalTarget {
interface ApprovalTarget {
type: ChatScope;
id: string;
}
export interface ParsedApprovalAction {
interface ParsedApprovalAction {
approvalId: string;
decision: ApprovalDecision;
}

View File

@@ -81,7 +81,7 @@ const CLEAR_STORAGE_MAX_DISPLAY = 10;
* under `~/.openclaw/media/qqbot/downloads/` without appId subdivision.
* The clear-storage command therefore cleans the entire downloads root.
*/
export function resolveQqbotDownloadsDir(): string {
function resolveQqbotDownloadsDir(): string {
return getQQBotMediaPath("downloads");
}

View File

@@ -31,14 +31,6 @@ export function initCommands(port: CommandsPort): void {
initSlashCommandDeps(port);
}
export type {
SlashCommandContext,
SlashCommandResult,
SlashCommandFileResult,
QQBotFrameworkCommand,
QueueSnapshot,
} from "./slash-commands.js";
/**
* Return all commands that require authorization, for registration with the
* framework via api.registerCommand() in registerFull().

View File

@@ -58,14 +58,14 @@ export interface QueueSnapshot {
export type SlashCommandResult = string | SlashCommandFileResult | null;
/** Slash command result that sends text first and then a local file. */
export interface SlashCommandFileResult {
interface SlashCommandFileResult {
text: string;
/** Local file path to send. */
filePath: string;
}
/** Slash command definition. */
export interface SlashCommand {
interface SlashCommand {
/** Command name without the leading slash. */
name: string;
/** Short description. */

View File

@@ -20,7 +20,7 @@ import type {
// ============ Dispatch result ============
export type DispatchResult =
type DispatchResult =
| { action: "ready"; data: unknown; sessionId: string }
| { action: "resumed"; data: unknown }
| { action: "message"; msg: QueuedMessage }

View File

@@ -31,7 +31,7 @@ import type { GatewayAccount, EngineLogger, GatewayPluginRuntime, WSPayload } fr
// ============ Connection context ============
export interface GatewayConnectionContext {
interface GatewayConnectionContext {
account: GatewayAccount;
abortSignal: AbortSignal;
cfg: unknown;

View File

@@ -53,7 +53,7 @@ export interface QueuedMention {
* representative turn, the merge information lands here instead of
* being scattered across `_` -prefixed fields on {@link QueuedMessage}.
*/
export interface QueuedMergeInfo {
interface QueuedMergeInfo {
/** Number of original messages folded in. Always >= 2. */
count: number;
/** Original messages in insertion order — `messages.at(-1)` is "current". */
@@ -129,7 +129,7 @@ export function isMergedTurn(msg: QueuedMessage): msg is QueuedMessage & {
return (msg.merge?.count ?? 0) > 1;
}
export interface MessageQueueContext {
interface MessageQueueContext {
accountId: string;
log?: {
info: (msg: string, meta?: Record<string, unknown>) => void;
@@ -149,14 +149,14 @@ export interface MessageQueueContext {
}
/** Snapshot of the queue state for diagnostics. */
export interface QueueSnapshot {
interface QueueSnapshot {
totalPending: number;
activeUsers: number;
maxConcurrentUsers: number;
senderPending: number;
}
export interface MessageQueue {
interface MessageQueue {
enqueue: (msg: QueuedMessage) => void;
startProcessor: (handleMessageFn: (msg: QueuedMessage) => Promise<void>) => void;
getSnapshot: (senderPeerId: string) => QueueSnapshot;

View File

@@ -49,7 +49,7 @@ const TOOL_MEDIA_SEND_TIMEOUT = 45_000;
// ============ Dependencies ============
export interface OutboundDispatchDeps {
interface OutboundDispatchDeps {
runtime: GatewayPluginRuntime;
cfg: unknown;
account: GatewayAccount;

View File

@@ -18,7 +18,7 @@ import {
} from "./constants.js";
/** Actions the caller should take after processing a close event. */
export interface CloseAction {
interface CloseAction {
/** Whether to schedule a reconnect. */
shouldReconnect: boolean;
/** Custom delay override (ms), or undefined to use the default backoff. */

View File

@@ -120,7 +120,7 @@ export interface WSPayload {
}
/** Attachment shape shared by all message event types. */
export interface RawMessageAttachment {
interface RawMessageAttachment {
content_type: string;
url: string;
filename?: string;
@@ -129,7 +129,7 @@ export interface RawMessageAttachment {
}
/** Referenced message element (used for quote messages). */
export interface RawMsgElement {
interface RawMsgElement {
msg_idx?: string;
content?: string;
attachments?: Array<
@@ -203,7 +203,7 @@ import type { EngineAdapters } from "../adapter/index.js";
* future additions (admin lookup, proactive push, per-group toggles)
* don't keep polluting the top-level context type.
*/
export interface GatewayGroupOptions {
interface GatewayGroupOptions {
/**
* Whether group-chat gating is enabled. Defaults to `true`; set to
* `false` to disable all group processing (e.g. for a DM-only smoke

View File

@@ -8,7 +8,7 @@
import { formatErrorMessage } from "../utils/format.js";
/** Function that sends a typing indicator to one user. */
export type SendInputNotifyFn = (
type SendInputNotifyFn = (
token: string,
openid: string,
msgId: string | undefined,
@@ -16,7 +16,7 @@ export type SendInputNotifyFn = (
) => Promise<unknown>;
/** Refresh every 50s for the QQ API's 60s input-notify window. */
export const TYPING_INTERVAL_MS = 50_000;
const TYPING_INTERVAL_MS = 50_000;
export const TYPING_INPUT_SECOND = 60;
export class TypingKeepAlive {

View File

@@ -88,7 +88,7 @@ export function resolveGroupActivation(params: {
* 2. `$OPENCLAW_STATE_DIR` / `$CLAWDBOT_STATE_DIR`
* 3. `~/.openclaw/agents/{agentId}/sessions/sessions.json`
*/
export function resolveSessionStorePath(
function resolveSessionStorePath(
cfg: Record<string, unknown>,
agentId: string | undefined,
): string {

View File

@@ -59,10 +59,10 @@ const MAX_HISTORY_KEYS = 1000;
* attachments (group history cache, ref-index store, and the dynamic
* context block on the current message) all share a single shape.
*/
export type AttachmentSummary = RefAttachmentSummary;
type AttachmentSummary = RefAttachmentSummary;
/** Raw attachment fields carried in a QQ event (the union we actually read). */
export interface RawAttachment {
interface RawAttachment {
content_type: string;
filename?: string;
/** Pre-computed ASR transcription text provided by QQ's gateway. */
@@ -83,7 +83,7 @@ export interface HistoryEntry {
}
/** Parameters for {@link formatMessageContent}. */
export interface FormatMessageContentParams {
interface FormatMessageContentParams {
content: string;
/** Message channel — `stripMentionText` only fires for `"group"`. */
chatType?: string;

View File

@@ -42,7 +42,7 @@ export interface RawMention {
}
/** Input for {@link detectWasMentioned}. */
export interface DetectWasMentionedInput {
interface DetectWasMentionedInput {
/**
* Raw event type. `"GROUP_AT_MESSAGE_CREATE"` unambiguously identifies
* that the bot was @-ed, even when the mentions array is empty.
@@ -59,7 +59,7 @@ export interface DetectWasMentionedInput {
}
/** Input for {@link hasAnyMention}. */
export interface HasAnyMentionInput {
interface HasAnyMentionInput {
mentions?: RawMention[];
content?: string;
}

View File

@@ -41,7 +41,7 @@
* skip AI dispatch.
* - `pass` — forward the message to the AI pipeline.
*/
export type GroupMessageGateAction =
type GroupMessageGateAction =
| "drop_other_mention"
| "block_unauthorized_command"
| "skip_no_mention"

View File

@@ -73,7 +73,7 @@ const DATA_URL_RE = /^data:([^;,]+);base64,(.+)$/i;
* base64 encoding. Non-base64 data URLs are intentionally rejected because
* the QQ upload API ingests raw base64, not arbitrary URL-encoded payloads.
*/
export function tryParseDataUrl(value: string): { mime: string; data: string } | null {
function tryParseDataUrl(value: string): { mime: string; data: string } | null {
if (!value.startsWith("data:")) {
return null;
}
@@ -92,7 +92,7 @@ export function tryParseDataUrl(value: string): { mime: string; data: string } |
*
* Callers MUST call {@link OpenedLocalFile.close} (typically in a `finally`).
*/
export interface OpenedLocalFile {
interface OpenedLocalFile {
handle: fs.promises.FileHandle;
size: number;
close(): Promise<void>;

View File

@@ -11,7 +11,7 @@ const VIDEO_EXTENSIONS = new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv
/**
* Extract a lowercase file extension from a path or URL, ignoring query and hash.
*/
export function getCleanExtension(filePath: string): string {
function getCleanExtension(filePath: string): string {
const cleanPath = filePath.split("?")[0].split("#")[0];
const lastDot = cleanPath.lastIndexOf(".");
if (lastDot < 0) {

View File

@@ -28,7 +28,7 @@ import {
// ---- Injected dependency interfaces ----
/** Media target context — describes where to send media. */
export interface MediaTargetContext {
interface MediaTargetContext {
targetType: "c2c" | "group" | "channel" | "dm";
targetId: string;
account: GatewayAccount;
@@ -36,14 +36,14 @@ export interface MediaTargetContext {
}
/** Media send result. */
export interface MediaSendResult {
interface MediaSendResult {
channel?: string;
error?: string;
messageId?: string;
}
/** Media sender interface — implemented by the upper-layer outbound.ts module. */
export interface MediaSender {
interface MediaSender {
sendPhoto(target: MediaTargetContext, imageUrl: string): Promise<MediaSendResult>;
sendVoice(
target: MediaTargetContext,
@@ -75,7 +75,7 @@ export interface DeliverDeps {
/** Maximum text length for a single QQ Bot message. */
const TEXT_CHUNK_LIMIT = 5000;
export interface DeliverEventContext {
interface DeliverEventContext {
type: "c2c" | "guild" | "dm" | "group";
senderId: string;
messageId: string;
@@ -85,7 +85,7 @@ export interface DeliverEventContext {
msgIdx?: string;
}
export interface DeliverAccountContext {
interface DeliverAccountContext {
account: GatewayAccount;
qualifiedTarget: string;
log?: {
@@ -96,10 +96,10 @@ export interface DeliverAccountContext {
}
/** Wrapper that retries when the access token expires. */
export type SendWithRetryFn = <T>(sendFn: (token: string) => Promise<T>) => Promise<T>;
type SendWithRetryFn = <T>(sendFn: (token: string) => Promise<T>) => Promise<T>;
/** Consume a quote ref exactly once. */
export type ConsumeQuoteRefFn = () => string | undefined;
type ConsumeQuoteRefFn = () => string | undefined;
// ---- Internal helpers ----
@@ -458,7 +458,7 @@ export async function parseAndSendMediaTags(
// ---- Plain reply ----
export interface PlainReplyPayload {
interface PlainReplyPayload {
text?: string;
mediaUrls?: string[];
mediaUrl?: string;

View File

@@ -32,7 +32,7 @@ import {
// ---- Injected dependencies ----
/** TTS provider interface — injected from the outer layer. */
export interface TTSProvider {
interface TTSProvider {
/** Framework TTS: text → audio file path. */
textToSpeech(params: {
text: string;
@@ -57,7 +57,7 @@ export interface ReplyDispatcherDeps {
// ---- Exported types ----
export interface MessageTarget {
interface MessageTarget {
type: "c2c" | "guild" | "dm" | "group";
senderId: string;
messageId: string;
@@ -66,7 +66,7 @@ export interface MessageTarget {
groupOpenid?: string;
}
export interface ReplyContext {
interface ReplyContext {
target: MessageTarget;
account: GatewayAccount;
cfg: unknown;
@@ -93,11 +93,7 @@ export async function sendWithTokenRetry<T>(
// ---- Text routing ----
/** Route a text message to the correct QQ target type. */
export async function sendTextToTarget(
ctx: ReplyContext,
text: string,
refIdx?: string,
): Promise<void> {
async function sendTextToTarget(ctx: ReplyContext, text: string, refIdx?: string): Promise<void> {
const { target, account } = ctx;
const deliveryTarget = buildDeliveryTarget(target);
const creds = accountToCreds(account);

View File

@@ -11,7 +11,7 @@
*/
/** Configuration for the reply limiter. */
export interface ReplyLimiterConfig {
interface ReplyLimiterConfig {
/** Maximum passive replies per message. Defaults to 4. */
limit?: number;
/** TTL in milliseconds for the passive reply window. Defaults to 1 hour. */

View File

@@ -50,9 +50,6 @@ import { normalizeSource, type MediaSource, type RawMediaSource } from "./media-
// ============ Re-exported types ============
export { ApiError } from "../types.js";
export type { OutboundMeta, MessageResponse, UploadMediaResponse } from "../types.js";
export { MediaFileType } from "../types.js";
export { UploadDailyLimitExceededError } from "../api/media-chunked.js";
// ============ Plugin User-Agent ============
@@ -357,7 +354,7 @@ export interface DeliveryTarget {
}
/** Account credentials for API authentication. */
export interface AccountCreds {
interface AccountCreds {
appId: string;
clientSecret: string;
}
@@ -528,7 +525,7 @@ export function createRawInputNotifyFn(
// ============ Media sending (unified) ============
/** Rich-media kind accepted by {@link sendMedia}. */
export type MediaKind = "image" | "voice" | "video" | "file";
type MediaKind = "image" | "voice" | "video" | "file";
/** Map a {@link MediaKind} to the wire-level {@link MediaFileType} code. */
const KIND_TO_FILE_TYPE: Record<MediaKind, MediaFileType> = {
@@ -538,16 +535,13 @@ const KIND_TO_FILE_TYPE: Record<MediaKind, MediaFileType> = {
file: MediaFileType.FILE,
};
/** Re-export source types so callers can construct them without importing media-source. */
export type { MediaSource, RawMediaSource } from "./media-source.js";
/**
* Options for the unified {@link sendMedia} API.
*
* This replaces the legacy four-method surface
* (`sendImage / sendVoiceMessage / sendVideoMessage / sendFileMessage`).
*/
export interface SendMediaOptions {
interface SendMediaOptions {
/** Delivery target. Only `c2c` and `group` support rich media. */
target: DeliveryTarget;
/** Account credentials. */

View File

@@ -210,7 +210,7 @@ class FlushController {
// ============ StreamingController ============
/** StreamingController 的依赖注入 */
export interface StreamingControllerDeps {
interface StreamingControllerDeps {
/** QQ Bot 账户配置 */
account: GatewayAccount;
/** 目标用户 openid流式 API 仅支持 C2C */
@@ -1105,7 +1105,7 @@ export class StreamingController {
// ============ 流式媒体发送 ============
/** 流式媒体发送上下文(由 gateway 注入到 StreamingController */
export interface StreamingMediaContext {
interface StreamingMediaContext {
/** 账户信息 */
account: GatewayAccount;
/** 事件信息 */

View File

@@ -161,7 +161,7 @@ function isInsideCodeBlock(text: string, position: number): boolean {
// ============ 媒体标签解析 ============
/** findFirstClosedMediaTag 的返回值 */
export interface FirstClosedMediaTag {
interface FirstClosedMediaTag {
/** 标签前的纯文本 */
textBefore: string;
/** 标签类型(小写,如 "qqvoice" */

View File

@@ -7,10 +7,10 @@
*/
/** Supported target types. */
export type TargetType = "c2c" | "group" | "channel";
type TargetType = "c2c" | "group" | "channel";
/** Parsed delivery target. */
export interface ParsedTarget {
interface ParsedTarget {
type: TargetType;
id: string;
}

View File

@@ -108,7 +108,7 @@ function json(data: unknown) {
* Options provided by the caller when executing a channel API request.
* 执行频道 API 请求时由调用方提供的选项。
*/
export interface ChannelApiExecuteOptions {
interface ChannelApiExecuteOptions {
accessToken: string;
}

View File

@@ -28,7 +28,7 @@ export interface RemindParams {
* `fallbackAccountId` are consulted only when the corresponding AI-supplied
* parameter is missing.
*/
export interface RemindExecuteContext {
interface RemindExecuteContext {
fallbackTo?: string;
fallbackAccountId?: string;
}
@@ -41,9 +41,9 @@ export type RemindCronAction =
job: ReturnType<typeof buildOnceJob>["job"] | ReturnType<typeof buildCronJob>["job"];
};
export type RemindCronScheduler = (params: RemindCronAction) => Promise<unknown>;
type RemindCronScheduler = (params: RemindCronAction) => Promise<unknown>;
export type RemindCronPlan =
type RemindCronPlan =
| {
ok: true;
action: RemindParams["action"];
@@ -181,7 +181,7 @@ export function buildReminderPrompt(content: string): string {
}
/** Build cron job params for a one-shot delayed reminder. */
export function buildOnceJob(params: RemindParams, delayMs: number, to: string, accountId: string) {
function buildOnceJob(params: RemindParams, delayMs: number, to: string, accountId: string) {
const atMs = Date.now() + delayMs;
const content = params.content!;
const name = params.name || generateJobName(content);
@@ -208,7 +208,7 @@ export function buildOnceJob(params: RemindParams, delayMs: number, to: string,
}
/** Build cron job params for a recurring cron reminder. */
export function buildCronJob(params: RemindParams, to: string, accountId: string) {
function buildCronJob(params: RemindParams, to: string, accountId: string) {
const content = params.content!;
const name = params.name || generateJobName(content);
const tz = params.timezone || "Asia/Shanghai";

View File

@@ -44,7 +44,7 @@ export type AttachmentSummary = RefAttachmentSummary;
* transcripts when `transcriptSource` is present. Tags are separated
* by spaces so the block fits on one line.
*/
export type RenderMode = "inline" | "ref";
type RenderMode = "inline" | "ref";
/** Human-readable labels for transcript provenance (prompt contract). */
export const TRANSCRIPT_SOURCE_LABELS: Record<
@@ -58,7 +58,7 @@ export const TRANSCRIPT_SOURCE_LABELS: Record<
};
/** Options controlling how the tag list is rendered. */
export interface RenderOptions {
interface RenderOptions {
mode: RenderMode;
/** Separator between tags. Defaults per mode: inline=`\n`, ref=` `. */
separator?: string;

View File

@@ -18,7 +18,7 @@ import {
checkSilkWasmAvailable,
} from "./platform.js";
export interface DiagnosticReport {
interface DiagnosticReport {
platform: string;
arch: string;
nodeVersion: string;

View File

@@ -11,7 +11,7 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./stri
export const MAX_UPLOAD_SIZE = 20 * 1024 * 1024;
/** Absolute upper bound enforced on the chunked upload path (matches server policy). */
export const CHUNKED_UPLOAD_MAX_SIZE = 100 * 1024 * 1024;
const CHUNKED_UPLOAD_MAX_SIZE = 100 * 1024 * 1024;
/** Threshold used to treat an upload as a large file (dispatch to chunked path). */
export const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024;
@@ -24,7 +24,7 @@ export const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024;
* `MEDIA_FILE_TYPE_INFO[MediaFileType.IMAGE].maxSize`, and adding a new
* type forces both fields to be supplied in a single place.
*/
export const MEDIA_FILE_TYPE_INFO: Record<MediaFileType, { maxSize: number; name: string }> = {
const MEDIA_FILE_TYPE_INFO: Record<MediaFileType, { maxSize: number; name: string }> = {
[MediaFileType.IMAGE]: { maxSize: 30 * 1024 * 1024, name: "图片" },
[MediaFileType.VIDEO]: { maxSize: 100 * 1024 * 1024, name: "视频" },
[MediaFileType.VOICE]: { maxSize: 20 * 1024 * 1024, name: "语音" },
@@ -63,7 +63,7 @@ export const QQBOT_MEDIA_SSRF_POLICY: SsrfPolicyConfig = {
};
/** Result of local file-size validation. */
export interface FileSizeCheckResult {
interface FileSizeCheckResult {
ok: boolean;
size: number;
error?: string;

View File

@@ -10,7 +10,7 @@ import type { SsrfPolicyConfig } from "../adapter/types.js";
import { formatErrorMessage } from "./format.js";
import { debugLog } from "./log.js";
export interface ImageSize {
interface ImageSize {
width: number;
height: number;
}

View File

@@ -27,10 +27,10 @@ export interface MediaPayload {
caption?: string;
}
export type QQBotPayload = CronReminderPayload | MediaPayload;
type QQBotPayload = CronReminderPayload | MediaPayload;
/** Result of parsing model output into a structured payload. */
export interface ParseResult {
interface ParseResult {
isPayload: boolean;
payload?: QQBotPayload;
text?: string;

View File

@@ -90,7 +90,7 @@ function getOpenClawMediaDir(): string {
// ---- Basic platform information ----
export type PlatformType = "darwin" | "linux" | "win32" | "other";
type PlatformType = "darwin" | "linux" | "win32" | "other";
export function getPlatform(): PlatformType {
const p = process.platform;

View File

@@ -18,7 +18,7 @@
import { AsyncLocalStorage } from "node:async_hooks";
/** Context values available during one inbound message handling cycle. */
export interface RequestContext {
interface RequestContext {
/** The account ID handling this request. */
accountId: string;
/**

View File

@@ -12,7 +12,7 @@
// ---- String coercion ----
/** Return the trimmed string or `null` when the value is not a non-empty string. */
export function normalizeNullableString(value: unknown): string | null {
function normalizeNullableString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}

View File

@@ -14,7 +14,7 @@ import {
sanitizeFileName,
} from "./string-normalize.js";
export interface STTConfig {
interface STTConfig {
baseUrl: string;
apiKey: string;
model: string;