mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
refactor: prune unused extension helpers
This commit is contained in:
@@ -74,47 +74,3 @@ export function clearAccountCredentials(
|
||||
|
||||
return { nextCfg, cleared, changed };
|
||||
}
|
||||
|
||||
// ---- Setup: clear a single credential field ----
|
||||
|
||||
export type CredentialField = "appId" | "clientSecret";
|
||||
|
||||
/**
|
||||
* Clear a single credential field from a QQBot account config.
|
||||
*
|
||||
* Used by setup flows when switching to env-backed credential resolution.
|
||||
* Returns a new config with the specified field removed.
|
||||
*/
|
||||
export function clearCredentialField(
|
||||
cfg: Record<string, unknown>,
|
||||
accountId: string,
|
||||
field: CredentialField,
|
||||
): Record<string, unknown> {
|
||||
const next = { ...cfg };
|
||||
const channels = asRecord(cfg.channels);
|
||||
const qqbot = { ...asRecord(channels?.qqbot) };
|
||||
|
||||
const clearField = (entry: Record<string, unknown>) => {
|
||||
if (field === "appId") {
|
||||
delete entry.appId;
|
||||
return;
|
||||
}
|
||||
delete entry.clientSecret;
|
||||
delete entry.clientSecretFile;
|
||||
};
|
||||
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
clearField(qqbot);
|
||||
} else {
|
||||
const accounts = { ...(qqbot.accounts as Record<string, Record<string, unknown>> | undefined) };
|
||||
if (accounts[accountId]) {
|
||||
const entry = { ...accounts[accountId] };
|
||||
clearField(entry);
|
||||
accounts[accountId] = entry;
|
||||
qqbot.accounts = accounts;
|
||||
}
|
||||
}
|
||||
|
||||
next.channels = { ...channels, qqbot };
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -213,43 +213,3 @@ export async function normalizeSource(
|
||||
mime: raw.mime,
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Materialization helpers ============
|
||||
|
||||
/**
|
||||
* Read a {@link MediaSource} into the `{ url?, fileData?, fileName? }` shape
|
||||
* expected by {@link MediaApi.uploadMedia} today (one-shot upload path).
|
||||
*
|
||||
* Chunked upload (future) should bypass this helper and feed the uploader
|
||||
* directly from the `localPath` / `buffer` branch.
|
||||
*/
|
||||
export async function materializeForOneShotUpload(
|
||||
source: MediaSource,
|
||||
): Promise<{ url?: string; fileData?: string; fileName?: string }> {
|
||||
switch (source.kind) {
|
||||
case "url":
|
||||
return { url: source.url };
|
||||
case "base64":
|
||||
return { fileData: source.data };
|
||||
case "localPath": {
|
||||
const opened = await openLocalFile(source.path);
|
||||
try {
|
||||
const buf = await opened.handle.readFile();
|
||||
return { fileData: buf.toString("base64") };
|
||||
} finally {
|
||||
await opened.close();
|
||||
}
|
||||
}
|
||||
case "buffer":
|
||||
return {
|
||||
fileData: source.buffer.toString("base64"),
|
||||
fileName: source.fileName,
|
||||
};
|
||||
default: {
|
||||
const _exhaustive: never = source;
|
||||
throw new Error(
|
||||
`materializeForOneShotUpload: unsupported MediaSource kind: ${JSON.stringify(_exhaustive)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,6 @@
|
||||
/** Supported media kind for QQ Bot outbound routing. */
|
||||
export type MediaKind = "image" | "voice" | "video" | "file";
|
||||
|
||||
/** Display labels for media kinds. */
|
||||
export const MEDIA_KIND_LABELS: Record<MediaKind | "media", string> = {
|
||||
image: "Image",
|
||||
voice: "Voice",
|
||||
video: "Video",
|
||||
file: "File",
|
||||
media: "Media",
|
||||
};
|
||||
|
||||
const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]);
|
||||
const VIDEO_EXTENSIONS = new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv"]);
|
||||
const AUDIO_EXTENSIONS = new Set([
|
||||
@@ -77,46 +68,3 @@ export function isAudioFile(filePath: string, mimeType?: string): boolean {
|
||||
}
|
||||
return AUDIO_EXTENSIONS.has(getCleanExtension(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect the media kind from a file path and optional MIME type.
|
||||
*
|
||||
* Priority: audio → video → image → file (default).
|
||||
*/
|
||||
export function detectMediaKind(filePath: string, mimeType?: string): MediaKind {
|
||||
if (isAudioFile(filePath, mimeType)) {
|
||||
return "voice";
|
||||
}
|
||||
if (isVideoFile(filePath, mimeType)) {
|
||||
return "video";
|
||||
}
|
||||
if (isImageFile(filePath, mimeType)) {
|
||||
return "image";
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
|
||||
/** Return true when the source is a remote HTTP(S) URL. */
|
||||
export function isHttpSource(source: string): boolean {
|
||||
return source.startsWith("http://") || source.startsWith("https://");
|
||||
}
|
||||
|
||||
/** Return true when the source is a Base64 data URL. */
|
||||
export function isDataSource(source: string): boolean {
|
||||
return source.startsWith("data:");
|
||||
}
|
||||
|
||||
/** Return true when the source is a remote URL or data URL. */
|
||||
export function isRemoteOrDataSource(source: string): boolean {
|
||||
return isHttpSource(source) || isDataSource(source);
|
||||
}
|
||||
|
||||
/** Common MIME type mapping for image extensions. */
|
||||
export const IMAGE_MIME_TYPES: Record<string, string> = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
};
|
||||
|
||||
@@ -363,47 +363,6 @@ export function splitByMediaTags(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文本中解析出完整的发送队列(含标签前后的纯文本)
|
||||
*
|
||||
* 与 splitByMediaTags 的区别:
|
||||
* - splitByMediaTags 分为 before / queue / after 三段(供流式模式的中断-恢复)
|
||||
* - parseMediaTagsToSendQueue 返回一个扁平的完整队列(供普通模式按顺序发送)
|
||||
*
|
||||
* 适用于 gateway.ts deliver 回调和 outbound.ts sendText。
|
||||
*/
|
||||
export function parseMediaTagsToSendQueue(
|
||||
text: string,
|
||||
log?: {
|
||||
info?: (msg: string) => void;
|
||||
debug?: (msg: string) => void;
|
||||
error?: (msg: string) => void;
|
||||
},
|
||||
): { hasMediaTags: boolean; sendQueue: SendQueueItem[] } {
|
||||
const split = splitByMediaTags(text, log);
|
||||
|
||||
if (!split.hasMediaTags) {
|
||||
return { hasMediaTags: false, sendQueue: [] };
|
||||
}
|
||||
|
||||
const sendQueue: SendQueueItem[] = [];
|
||||
|
||||
// 标签前的文本
|
||||
if (split.textBeforeFirstTag) {
|
||||
sendQueue.push({ type: "text", content: split.textBeforeFirstTag });
|
||||
}
|
||||
|
||||
// 媒体队列(含标签间文本)
|
||||
sendQueue.push(...split.mediaQueue);
|
||||
|
||||
// 标签后的文本
|
||||
if (split.textAfterLastTag) {
|
||||
sendQueue.push({ type: "text", content: split.textAfterLastTag });
|
||||
}
|
||||
|
||||
return { hasMediaTags: true, sendQueue };
|
||||
}
|
||||
|
||||
// ============ 发送队列执行 ============
|
||||
|
||||
/**
|
||||
@@ -527,17 +486,6 @@ export async function executeSendQueue(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文本中剥离所有媒体标签(用于最终显示)
|
||||
*/
|
||||
export function stripMediaTags(text: string): string {
|
||||
const regex = createMediaTagRegex();
|
||||
return text
|
||||
.replace(regex, "")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测文本中是否有未闭合的媒体标签,如果有则截断到安全位置。
|
||||
*
|
||||
|
||||
@@ -107,17 +107,6 @@ export async function fileExistsAsync(filePath: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Get file size asynchronously. */
|
||||
export async function getFileSizeAsync(filePath: string): Promise<number> {
|
||||
const stat = await fs.promises.stat(filePath);
|
||||
return stat.size;
|
||||
}
|
||||
|
||||
/** Return true when a file should be treated as large. */
|
||||
export function isLargeFile(sizeBytes: number): boolean {
|
||||
return sizeBytes >= LARGE_FILE_THRESHOLD;
|
||||
}
|
||||
|
||||
/** Format a byte count into a human-readable size string. */
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
|
||||
@@ -247,12 +247,3 @@ export function formatQQBotMarkdownImage(url: string, size: ImageSize | null): s
|
||||
export function hasQQBotImageSize(markdownImage: string): boolean {
|
||||
return /!\[#\d+px\s+#\d+px\]/.test(markdownImage);
|
||||
}
|
||||
|
||||
/** Extract width and height from QQBot markdown image syntax: ``. */
|
||||
export function extractQQBotImageSize(markdownImage: string): ImageSize | null {
|
||||
const match = markdownImage.match(/!\[#(\d+)px\s+#(\d+)px\]/);
|
||||
if (match) {
|
||||
return { width: Number.parseInt(match[1], 10), height: Number.parseInt(match[2], 10) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -185,12 +185,6 @@ function testExecutable(cmd: string, args: string[]): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
/** Reset ffmpeg detection state, mainly for tests. */
|
||||
export function resetFfmpegCache(): void {
|
||||
_ffmpegPath = undefined;
|
||||
_ffmpegCheckPromise = null;
|
||||
}
|
||||
|
||||
// ---- silk-wasm detection ----
|
||||
|
||||
let _silkWasmAvailable: boolean | null = null;
|
||||
@@ -273,14 +267,6 @@ export function isLocalPath(p: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Looser local-path heuristic used for markdown-extracted paths. */
|
||||
export function looksLikeLocalPath(p: string): boolean {
|
||||
if (isLocalPath(p)) {
|
||||
return true;
|
||||
}
|
||||
return /^(?:Users|home|tmp|var|private|[A-Z]:)/i.test(p);
|
||||
}
|
||||
|
||||
// ---- QQBot media path resolution ----
|
||||
|
||||
function isPathWithinRoot(candidate: string, root: string): boolean {
|
||||
|
||||
@@ -58,18 +58,3 @@ export function runWithRequestContext<T>(ctx: RequestContext, fn: () => T): T {
|
||||
export function getRequestContext(): RequestContext | undefined {
|
||||
return store.getStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience accessor for the current request's fully qualified
|
||||
* delivery target.
|
||||
*/
|
||||
export function getRequestTarget(): string | undefined {
|
||||
return store.getStore()?.target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience accessor for the current request's account ID.
|
||||
*/
|
||||
export function getRequestAccountId(): string | undefined {
|
||||
return store.getStore()?.accountId;
|
||||
}
|
||||
|
||||
@@ -94,14 +94,3 @@ export function setCachedFileInfo(
|
||||
`[upload-cache] Cache SET: key=${key.slice(0, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Return cache stats for diagnostics. */
|
||||
export function getUploadCacheStats(): { size: number; maxSize: number } {
|
||||
return { size: cache.size, maxSize: MAX_CACHE_SIZE };
|
||||
}
|
||||
|
||||
/** Clear the upload cache. */
|
||||
export function clearUploadCache(): void {
|
||||
cache.clear();
|
||||
debugLog(`[upload-cache] Cache cleared`);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import {
|
||||
resolveChannelEntryMatchWithFallback,
|
||||
type ChannelMatchSource,
|
||||
} from "openclaw/plugin-sdk/channel-targets";
|
||||
import type { SlackReactionNotificationMode } from "openclaw/plugin-sdk/config-types";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { SlackMessageEvent } from "../types.js";
|
||||
import { allowListMatches, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
|
||||
import { normalizeSlackSlug } from "./allow-list.js";
|
||||
|
||||
export type SlackChannelConfigResolved = {
|
||||
allowed: boolean;
|
||||
@@ -40,41 +39,6 @@ function firstDefined<T>(...values: Array<T | undefined>) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function shouldEmitSlackReactionNotification(params: {
|
||||
mode: SlackReactionNotificationMode | undefined;
|
||||
botId?: string | null;
|
||||
messageAuthorId?: string | null;
|
||||
userId: string;
|
||||
userName?: string | null;
|
||||
allowlist?: Array<string | number> | null;
|
||||
allowNameMatching?: boolean;
|
||||
}) {
|
||||
const { mode, botId, messageAuthorId, userId, userName, allowlist } = params;
|
||||
const effectiveMode = mode ?? "own";
|
||||
if (effectiveMode === "off") {
|
||||
return false;
|
||||
}
|
||||
if (effectiveMode === "own") {
|
||||
if (!botId || !messageAuthorId) {
|
||||
return false;
|
||||
}
|
||||
return messageAuthorId === botId;
|
||||
}
|
||||
if (effectiveMode === "allowlist") {
|
||||
if (!Array.isArray(allowlist) || allowlist.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const users = normalizeAllowListLower(allowlist);
|
||||
return allowListMatches({
|
||||
allowList: users,
|
||||
id: userId,
|
||||
name: userName ?? undefined,
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveSlackChannelLabel(params: { channelId?: string; channelName?: string }) {
|
||||
const channelName = params.channelName?.trim();
|
||||
if (channelName) {
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
||||
import type { Foreigns } from "../urbit/foreigns.js";
|
||||
import { asRecord, formatChangesDate, formatErrorMessage } from "./utils.js";
|
||||
|
||||
export async function fetchGroupChanges(
|
||||
api: { scry: (path: string) => Promise<unknown> },
|
||||
runtime: RuntimeEnv,
|
||||
daysAgo = 5,
|
||||
) {
|
||||
try {
|
||||
const changeDate = formatChangesDate(daysAgo);
|
||||
runtime.log?.(`[tlon] Fetching group changes since ${daysAgo} days ago (${changeDate})...`);
|
||||
const changes = await api.scry(`/groups-ui/v5/changes/${changeDate}.json`);
|
||||
if (changes) {
|
||||
runtime.log?.("[tlon] Successfully fetched changes data");
|
||||
return changes;
|
||||
}
|
||||
return null;
|
||||
} catch (error: unknown) {
|
||||
runtime.log?.(
|
||||
`[tlon] Failed to fetch changes (falling back to full init): ${formatErrorMessage(error)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
import { asRecord, formatErrorMessage } from "./utils.js";
|
||||
|
||||
export interface InitData {
|
||||
channels: string[];
|
||||
|
||||
@@ -330,18 +330,3 @@ export function markdownToStory(markdown: string): Story {
|
||||
|
||||
return story;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert plain text to simple story (no markdown parsing)
|
||||
*/
|
||||
export function textToStory(text: string): Story {
|
||||
return [{ inline: [text] }];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if text contains markdown formatting
|
||||
*/
|
||||
export function hasMarkdown(text: string): boolean {
|
||||
// Check for common markdown patterns
|
||||
return /(\*\*|__|~~|`|^#{1,6}\s|^```|^\s*[-*]\s|\[.*\]\(.*\)|^>\s)/m.test(text);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { WebhookContext } from "../../types.js";
|
||||
|
||||
export type TwimlResponseKind = "empty" | "pause" | "queue" | "stored" | "stream";
|
||||
|
||||
export type TwimlRequestView = {
|
||||
callStatus: string | null;
|
||||
direction: string | null;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
export {
|
||||
convertPcmToMulaw8k,
|
||||
pcmToMulaw,
|
||||
resamplePcmTo8k,
|
||||
} from "openclaw/plugin-sdk/realtime-voice";
|
||||
export { convertPcmToMulaw8k, resamplePcmTo8k } from "openclaw/plugin-sdk/realtime-voice";
|
||||
|
||||
/**
|
||||
* Chunk audio buffer into 20ms frames for streaming (8kHz mono mu-law).
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
export function isStatusCommand(body: string) {
|
||||
const trimmed = normalizeLowercaseStringOrEmpty(body);
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
return trimmed === "/status" || trimmed === "status" || trimmed.startsWith("/status ");
|
||||
}
|
||||
|
||||
export function stripMentionsForCommand(
|
||||
text: string,
|
||||
mentionRegexes: RegExp[],
|
||||
|
||||
@@ -59,9 +59,3 @@ export function combineWhatsAppSendResults(
|
||||
providerAccepted: results.some((result) => result.providerAccepted),
|
||||
};
|
||||
}
|
||||
|
||||
export function hasAcceptedWhatsAppSendResult(
|
||||
result: WhatsAppSendResult | undefined,
|
||||
): result is WhatsAppSendResult {
|
||||
return result?.providerAccepted === true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user