feat: 统一内置插件包入口契约
This commit is contained in:
parent
1c6659def5
commit
0aa2e470cd
@ -17,10 +17,22 @@
|
||||
"plugin.storage.write"
|
||||
],
|
||||
"runtime": {
|
||||
"workerType": "node-worker",
|
||||
"workerType": "thread",
|
||||
"timeoutMs": 30000,
|
||||
"memoryMb": 3072,
|
||||
"maxConcurrency": 2
|
||||
"memoryMb": 512,
|
||||
"maxConcurrency": 1,
|
||||
"configKeys": [
|
||||
"BANGDREAM_TSUGU_BESTDORI_BASE_URL",
|
||||
"BANGDREAM_TSUGU_CACHE_ROOT",
|
||||
"BANGDREAM_TSUGU_COMPRESS",
|
||||
"BANGDREAM_TSUGU_DISPLAYED_SERVERS",
|
||||
"BANGDREAM_TSUGU_HHWX_BASE_URL",
|
||||
"BANGDREAM_TSUGU_MAIN_DATA_READY_TIMEOUT_MS",
|
||||
"BANGDREAM_TSUGU_MAIN_SERVER",
|
||||
"BANGDREAM_TSUGU_REQUEST_TIMEOUT_MS",
|
||||
"BANGDREAM_TSUGU_RETRY_COUNT",
|
||||
"BANGDREAM_TSUGU_USE_EASY_BG"
|
||||
]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object"
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import * as path from 'path';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
import {
|
||||
BangDreamCommandContext,
|
||||
type BangDreamCommandContextOptions,
|
||||
type BangDreamConfigReader,
|
||||
type BangDreamDictionaryReader,
|
||||
} from './application/bangdream-command-context';
|
||||
import {
|
||||
createBangDreamOperationLifecycleContext,
|
||||
@ -24,6 +29,10 @@ import type {
|
||||
} from './domain/common/bangdream.types';
|
||||
import { waitForBangDreamCatalogReady } from './application/catalog/bangdream-catalog-cache';
|
||||
import { preloadBangDreamRenderAssets } from './application/render-assets';
|
||||
import {
|
||||
fuzzySearchPath,
|
||||
projectRoot as bangDreamProjectRoot,
|
||||
} from './config/runtime-config';
|
||||
|
||||
type BangDreamPluginRuntimeOptions = BangDreamCommandContextOptions & {
|
||||
description?: string;
|
||||
@ -52,11 +61,69 @@ type BangDreamResolvedOperation = BangDreamManifestOperation & {
|
||||
execute: BangDreamOperationModule['execute'];
|
||||
};
|
||||
|
||||
type BangDreamGenericManifest = {
|
||||
description?: string;
|
||||
entry?: string;
|
||||
events?: unknown[];
|
||||
key?: string;
|
||||
legacyAliases?: string[];
|
||||
name?: string;
|
||||
operations?: BangDreamManifestOperation[];
|
||||
pluginKey?: string;
|
||||
runtime?: Record<string, unknown>;
|
||||
tasks?: unknown[];
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type QqbotGenericPluginCreateOptions = {
|
||||
host: Record<string, unknown>;
|
||||
manifest: BangDreamGenericManifest;
|
||||
normalizeError: (error: unknown, fallback?: string) => string | Error;
|
||||
now: () => Date;
|
||||
runtime: {
|
||||
configSnapshot: Record<string, string | undefined>;
|
||||
installationId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type BangDreamPluginCreateOptions =
|
||||
| BangDreamPluginRuntimeOptions
|
||||
| QqbotGenericPluginCreateOptions;
|
||||
|
||||
type BangDreamCommandPlugin = ReturnType<
|
||||
typeof createBangDreamPluginFromRuntimeOptions
|
||||
>;
|
||||
|
||||
type BangDreamGenericPathMapper = (filePath: string) => string;
|
||||
|
||||
/**
|
||||
* 创建 BangDream 插件对象或配置。
|
||||
* @param options - BangDream列表;使用 `io`、`operations`、`normalizeError`、`description` 字段生成结果。
|
||||
* Creates the BangDream plugin entry for either the legacy package loader or the generic worker runtime.
|
||||
* @param options - Legacy BangDream runtime options or generic worker create context with manifest and config snapshot.
|
||||
* @returns BangDream command plugin instance exposing operations, health checks, and scheduled tasks.
|
||||
*/
|
||||
export function createPlugin(options: BangDreamPluginRuntimeOptions) {
|
||||
export function createPlugin(
|
||||
options: BangDreamPluginRuntimeOptions,
|
||||
): BangDreamCommandPlugin;
|
||||
export function createPlugin(
|
||||
options: QqbotGenericPluginCreateOptions,
|
||||
): Promise<BangDreamCommandPlugin>;
|
||||
export function createPlugin(
|
||||
options: BangDreamPluginCreateOptions,
|
||||
): BangDreamCommandPlugin | Promise<BangDreamCommandPlugin> {
|
||||
if (isBangDreamGenericPluginCreateOptions(options)) {
|
||||
return createBangDreamPluginFromGenericOptions(options);
|
||||
}
|
||||
return createBangDreamPluginFromRuntimeOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the BangDream plugin through the historical package-local option shape.
|
||||
* @param options - Package-local options carrying synchronous config, optional runtime IO, and manifest operations.
|
||||
* @returns BangDream command plugin used by legacy loaders and tests.
|
||||
*/
|
||||
function createBangDreamPluginFromRuntimeOptions(
|
||||
options: BangDreamPluginRuntimeOptions,
|
||||
) {
|
||||
if (options.io) configureBangDreamRuntimeIo(options.io);
|
||||
const context = new BangDreamCommandContext(options);
|
||||
const lifecycle = new BangDreamOperationLifecycle([
|
||||
@ -155,6 +222,394 @@ export function createPlugin(options: BangDreamPluginRuntimeOptions) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the BangDream plugin from the generic worker contract while keeping adapters inside the package.
|
||||
* @param options - Generic worker context; config is read from the snapshot and IO calls delegate to host RPC methods.
|
||||
* @returns BangDream command plugin compatible with the generic worker runtime.
|
||||
*/
|
||||
async function createBangDreamPluginFromGenericOptions(
|
||||
options: QqbotGenericPluginCreateOptions,
|
||||
): Promise<BangDreamCommandPlugin> {
|
||||
const manifest = options.manifest;
|
||||
const pathMapper = createBangDreamGenericPathMapper(
|
||||
options.runtime.installationId,
|
||||
);
|
||||
const syncJsonCache = await preloadBangDreamGenericSyncJson(
|
||||
options.host,
|
||||
pathMapper,
|
||||
);
|
||||
return createBangDreamPluginFromRuntimeOptions({
|
||||
configReader: createBangDreamGenericConfigReader(
|
||||
options.runtime.configSnapshot,
|
||||
),
|
||||
description: manifest.description,
|
||||
dictionaryReader: createBangDreamGenericDictionaryReader(options.host),
|
||||
io: createBangDreamGenericRuntimeIo(options, pathMapper, syncJsonCache),
|
||||
legacyAliases: manifest.legacyAliases,
|
||||
name: manifest.name,
|
||||
normalizeError: (error) =>
|
||||
normalizeBangDreamGenericError(options.normalizeError, error),
|
||||
operations: manifest.operations || [],
|
||||
pluginKey: manifest.pluginKey || manifest.key || 'bangdream',
|
||||
version: manifest.version,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a create call is using the generic worker runtime shape.
|
||||
* @param options - Unknown BangDream create options supplied by a loader.
|
||||
* @returns `true` when the options include a runtime config snapshot.
|
||||
*/
|
||||
function isBangDreamGenericPluginCreateOptions(
|
||||
options: BangDreamPluginCreateOptions,
|
||||
): options is QqbotGenericPluginCreateOptions {
|
||||
return (
|
||||
!!(options as QqbotGenericPluginCreateOptions).runtime?.configSnapshot &&
|
||||
!!(options as QqbotGenericPluginCreateOptions).manifest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a synchronous BangDream config reader over the worker startup snapshot.
|
||||
* @param snapshot - Manifest-owned config key snapshot captured before the worker call.
|
||||
* @returns Config reader that never performs async host RPC during BangDream command execution.
|
||||
*/
|
||||
function createBangDreamGenericConfigReader(
|
||||
snapshot: Record<string, string | undefined>,
|
||||
): BangDreamConfigReader {
|
||||
return {
|
||||
/**
|
||||
* Reads one BangDream config value from the immutable worker snapshot.
|
||||
* @param key - Runtime config key declared by the BangDream package manifest.
|
||||
* @returns Snapshot value cast to the requested BangDream config type.
|
||||
*/
|
||||
get: <T = string>(key: string) => snapshot[key] as T | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dictionary reader that delegates BangDream dictionary lookups to the generic host.
|
||||
* @param host - Generic worker host facade with dictionary RPC methods.
|
||||
* @returns Dictionary reader used by BangDream command context cache refresh.
|
||||
*/
|
||||
function createBangDreamGenericDictionaryReader(
|
||||
host: Record<string, unknown>,
|
||||
): BangDreamDictionaryReader {
|
||||
return {
|
||||
/**
|
||||
* Reads dictionary items through the worker host bridge.
|
||||
* @param dictCode - Admin dictionary code requested by BangDream alias/config lookup.
|
||||
* @returns Dictionary items normalized to label/value pairs by the command context.
|
||||
*/
|
||||
getDictItemsByKey: async (dictCode) =>
|
||||
await callBangDreamGenericHost(host, 'getDictItemsByKey', dictCode),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds BangDream runtime IO adapters over generic worker host methods.
|
||||
* @param options - Generic worker context containing host RPC methods and config snapshot.
|
||||
* @param pathMapper - Converts BangDream absolute package paths into host-safe package-relative paths.
|
||||
* @param syncJsonCache - Preloaded JSON payloads available to synchronous BangDream readers.
|
||||
* @returns Runtime IO implementation consumed by BangDream package infrastructure.
|
||||
*/
|
||||
function createBangDreamGenericRuntimeIo(
|
||||
options: QqbotGenericPluginCreateOptions,
|
||||
pathMapper: BangDreamGenericPathMapper,
|
||||
syncJsonCache: Map<string, unknown>,
|
||||
): BangDreamRuntimeIo {
|
||||
const { host, runtime } = options;
|
||||
return {
|
||||
/**
|
||||
* Reads a BangDream config value from the startup snapshot.
|
||||
* @param key - Runtime config key declared by the BangDream package manifest.
|
||||
* @returns Snapshot value or `undefined` when the key is not configured.
|
||||
*/
|
||||
getConfig: (key) => runtime.configSnapshot[key],
|
||||
/**
|
||||
* Reads a package asset file through the generic host bridge.
|
||||
* @param filePath - Package-relative asset path requested by BangDream rendering or catalog code.
|
||||
* @returns Asset bytes from the package root.
|
||||
*/
|
||||
readAssetFile: async (filePath) =>
|
||||
normalizeBangDreamHostBuffer(
|
||||
await callBangDreamGenericHost(
|
||||
host,
|
||||
'readAssetFile',
|
||||
pathMapper(filePath),
|
||||
),
|
||||
),
|
||||
/**
|
||||
* Reads Excel rows from a package-local workbook buffer through the generic host bridge.
|
||||
* @param filePath - Absolute or relative BangDream static workbook path requested by package code.
|
||||
* @returns First-sheet rows parsed from host-provided XLSX bytes.
|
||||
*/
|
||||
readExcelRows: async <T extends Record<string, unknown>>(
|
||||
filePath: string,
|
||||
) =>
|
||||
parseBangDreamExcelRows<T>(
|
||||
normalizeBangDreamHostBuffer(
|
||||
await callBangDreamGenericHost(
|
||||
host,
|
||||
'readAssetFile',
|
||||
pathMapper(filePath),
|
||||
),
|
||||
),
|
||||
),
|
||||
/**
|
||||
* Reads a package-local JSON file through the generic host bridge.
|
||||
* @param filePath - Package-relative JSON path requested by BangDream storage or static data code.
|
||||
* @returns Parsed JSON payload returned by the host bridge.
|
||||
*/
|
||||
readJsonFile: async <T = unknown>(filePath: string) =>
|
||||
await callBangDreamGenericHost<T>(
|
||||
host,
|
||||
'readJsonFile',
|
||||
pathMapper(filePath),
|
||||
),
|
||||
/**
|
||||
* Reads preloaded package-local JSON synchronously for BangDream search/config modules.
|
||||
* @param filePath - Absolute or relative BangDream JSON path requested by synchronous package code.
|
||||
* @returns Cached JSON payload populated during generic plugin creation.
|
||||
*/
|
||||
readJsonFileSync: <T = unknown>(filePath: string) => {
|
||||
const relativePath = pathMapper(filePath);
|
||||
if (!syncJsonCache.has(relativePath)) {
|
||||
throw new Error(
|
||||
`BangDream generic runtime JSON 未预加载:${relativePath}`,
|
||||
);
|
||||
}
|
||||
return syncJsonCache.get(relativePath) as T;
|
||||
},
|
||||
/**
|
||||
* Renames a package-local file through the generic host bridge.
|
||||
* @param from - Package-relative temporary path created by BangDream storage code.
|
||||
* @param to - Package-relative final path for the atomic write target.
|
||||
*/
|
||||
renameFile: async (from, to) => {
|
||||
await callBangDreamGenericHost(
|
||||
host,
|
||||
'renameFile',
|
||||
pathMapper(from),
|
||||
pathMapper(to),
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Requests binary HTTP content through the generic host bridge.
|
||||
* @param url - Absolute HTTP URL requested by BangDream external integrations.
|
||||
* @param requestOptions - Optional headers and timeout used for the host-mediated request.
|
||||
* @returns Buffer body wrapped in the BangDream runtime IO response shape.
|
||||
*/
|
||||
requestArrayBuffer: async (url, requestOptions) => ({
|
||||
body: normalizeBangDreamHostBuffer(
|
||||
await callBangDreamGenericHost(host, 'requestBuffer', {
|
||||
context: 'BangDream 资源下载',
|
||||
failureMessageTemplate: 'BangDream 资源下载失败:{statusCode}',
|
||||
headers: requestOptions?.headers,
|
||||
timeoutMessage: 'BangDream 资源下载超时',
|
||||
timeoutMs: requestOptions?.timeoutMs,
|
||||
url,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
/**
|
||||
* Requests JSON HTTP content through the generic host bridge.
|
||||
* @param url - Absolute HTTP URL requested by BangDream external integrations.
|
||||
* @param requestOptions - Optional headers and timeout used for the host-mediated request.
|
||||
* @returns JSON body wrapped in the BangDream runtime IO response shape.
|
||||
*/
|
||||
requestJson: async <T = unknown>(url: string, requestOptions) => ({
|
||||
body: await callBangDreamGenericHost<T>(host, 'requestJson', {
|
||||
context: 'BangDream 数据接口',
|
||||
failureMessageTemplate: 'BangDream 数据接口失败:{statusCode}',
|
||||
headers: requestOptions?.headers,
|
||||
invalidJsonMessage: 'BangDream 数据接口返回不是合法 JSON',
|
||||
timeoutMessage: 'BangDream 数据接口请求超时',
|
||||
timeoutMs: requestOptions?.timeoutMs,
|
||||
url,
|
||||
}),
|
||||
}),
|
||||
/**
|
||||
* Sleeps through the generic host bridge so worker delays remain bounded by platform policy.
|
||||
* @param ms - Delay duration in milliseconds requested by BangDream retry logic.
|
||||
*/
|
||||
sleep: async (ms) => {
|
||||
await callBangDreamGenericHost(host, 'sleep', ms);
|
||||
},
|
||||
/**
|
||||
* Writes a package-local JSON file through the generic host bridge.
|
||||
* @param filePath - Package-relative storage path requested by BangDream cache code.
|
||||
* @param data - JSON-serializable payload to persist in package storage.
|
||||
*/
|
||||
writeJsonFile: async (filePath, data) => {
|
||||
await callBangDreamGenericHost(
|
||||
host,
|
||||
'writeJsonFile',
|
||||
pathMapper(filePath),
|
||||
data,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BangDream package path mapper for generic host file calls.
|
||||
* @param installationId - Runtime installation id used to namespace package-external cache paths.
|
||||
* @returns Function converting absolute package paths to package-relative host paths with forward slashes.
|
||||
*/
|
||||
function createBangDreamGenericPathMapper(
|
||||
installationId: string,
|
||||
): BangDreamGenericPathMapper {
|
||||
const packageRoot = path.resolve(bangDreamProjectRoot, '..');
|
||||
const runtimePrefix = `runtime/${normalizeBangDreamPathSegment(
|
||||
installationId || 'default',
|
||||
)}`;
|
||||
|
||||
/**
|
||||
* Converts a BangDream runtime file path into a host-safe package-relative path.
|
||||
* @param filePath - Absolute package path, package-relative path, or package-external cache path from BangDream code.
|
||||
* @returns Forward-slash relative path accepted by the generic host bridge.
|
||||
*/
|
||||
return (filePath: string) => {
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
return normalizeBangDreamHostPath(filePath);
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(filePath);
|
||||
const relativePath = path.relative(packageRoot, absolutePath);
|
||||
if (
|
||||
relativePath &&
|
||||
relativePath !== '..' &&
|
||||
!relativePath.startsWith(`..${path.sep}`) &&
|
||||
!path.isAbsolute(relativePath)
|
||||
) {
|
||||
return normalizeBangDreamHostPath(relativePath);
|
||||
}
|
||||
|
||||
return `${runtimePrefix}/${normalizeBangDreamExternalPath(absolutePath)}`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads synchronous BangDream JSON files through async generic host methods before the plugin is returned.
|
||||
* @param host - Generic worker host facade used to read package JSON files.
|
||||
* @param pathMapper - Converts BangDream absolute paths to host-safe package-relative paths.
|
||||
* @returns Map keyed by normalized package-relative JSON paths for sync readers.
|
||||
*/
|
||||
async function preloadBangDreamGenericSyncJson(
|
||||
host: Record<string, unknown>,
|
||||
pathMapper: BangDreamGenericPathMapper,
|
||||
) {
|
||||
const cache = new Map<string, unknown>();
|
||||
if (typeof host.readJsonFile !== 'function') return cache;
|
||||
|
||||
const fuzzyPath = pathMapper(fuzzySearchPath);
|
||||
cache.set(
|
||||
fuzzyPath,
|
||||
await callBangDreamGenericHost(host, 'readJsonFile', fuzzyPath),
|
||||
);
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses rows from the first worksheet of a host-provided XLSX buffer.
|
||||
* @param buffer - Workbook bytes read through the generic host bridge.
|
||||
* @returns JSON rows from the first workbook sheet.
|
||||
*/
|
||||
function parseBangDreamExcelRows<T extends Record<string, unknown>>(
|
||||
buffer: Buffer,
|
||||
): T[] {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
if (!sheetName) return [];
|
||||
return XLSX.utils.sheet_to_json<T>(workbook.Sheets[sheetName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a host path to forward-slash package-relative form.
|
||||
* @param filePath - Package-relative path with either Windows or POSIX separators.
|
||||
* @returns Forward-slash path without leading current-directory markers.
|
||||
*/
|
||||
function normalizeBangDreamHostPath(filePath: string) {
|
||||
return filePath
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\.\/+/, '')
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an absolute package-external path into a stable runtime storage key.
|
||||
* @param filePath - Absolute cache or storage path outside the BangDream package root.
|
||||
* @returns Forward-slash runtime storage suffix that is safe for package-local host APIs.
|
||||
*/
|
||||
function normalizeBangDreamExternalPath(filePath: string) {
|
||||
return normalizeBangDreamHostPath(
|
||||
filePath.replace(/^[A-Za-z]:/, (drive) => drive.slice(0, 1)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes one path segment used by generic runtime storage prefixes.
|
||||
* @param value - Installation id or other untrusted segment value.
|
||||
* @returns Path-safe segment with separators and punctuation collapsed.
|
||||
*/
|
||||
function normalizeBangDreamPathSegment(value: string) {
|
||||
return (
|
||||
value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls one generic BangDream host capability and fails with a package-owned error when absent.
|
||||
* @param host - Generic worker host facade supplied to `createPlugin`.
|
||||
* @param method - Host capability name required by the BangDream adapter.
|
||||
* @param args - Positional arguments accepted by the host facade method.
|
||||
* @returns Host method result cast to the requested package-local type.
|
||||
*/
|
||||
async function callBangDreamGenericHost<TResult = any>(
|
||||
host: Record<string, unknown>,
|
||||
method: string,
|
||||
...args: unknown[]
|
||||
): Promise<TResult> {
|
||||
const fn = host[method];
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`BangDream generic host 缺少 ${method}`);
|
||||
}
|
||||
return (await fn(...args)) as TResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a generic host binary response into a Node Buffer for BangDream render code.
|
||||
* @param value - Buffer, Uint8Array, or object containing a `body` field returned by the host bridge.
|
||||
* @returns Buffer instance safe for BangDream runtime IO consumers.
|
||||
*/
|
||||
function normalizeBangDreamHostBuffer(value: unknown): Buffer {
|
||||
const body =
|
||||
value && typeof value === 'object' && 'body' in value
|
||||
? (value as { body?: unknown }).body
|
||||
: value;
|
||||
if (Buffer.isBuffer(body)) return body;
|
||||
if (body instanceof Uint8Array) return Buffer.from(body);
|
||||
if (Array.isArray(body)) return Buffer.from(body);
|
||||
return Buffer.from([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes generic worker errors to the legacy BangDream string error contract.
|
||||
* @param normalizeError - Generic worker error normalizer supplied by the platform runtime.
|
||||
* @param error - Error or arbitrary thrown value from BangDream package code.
|
||||
* @returns Error message consumed by BangDream lifecycle logging and thrown operation errors.
|
||||
*/
|
||||
function normalizeBangDreamGenericError(
|
||||
normalizeError: QqbotGenericPluginCreateOptions['normalizeError'],
|
||||
error: unknown,
|
||||
) {
|
||||
const normalized = normalizeError(error, 'BangDream 命令执行失败');
|
||||
return normalized instanceof Error ? normalized.message : `${normalized}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Bang Dream Operations。
|
||||
* @param operations - BangDream列表;转换 BangDream列表项。
|
||||
|
||||
@ -9,10 +9,16 @@
|
||||
"legacyAliases": ["ff14Market"],
|
||||
"permissions": ["runtime.http", "plugin.config.read"],
|
||||
"runtime": {
|
||||
"workerType": "node-worker",
|
||||
"timeoutMs": 15000,
|
||||
"workerType": "thread",
|
||||
"timeoutMs": 30000,
|
||||
"memoryMb": 256,
|
||||
"maxConcurrency": 4
|
||||
"maxConcurrency": 1,
|
||||
"configKeys": [
|
||||
"FF14_DEFAULT_WORLD",
|
||||
"FF14_UNIVERSALIS_BASE_URL",
|
||||
"FF14_XIVAPI_BASE_URL",
|
||||
"FF14_XIVAPI_CHS_BASE_URL"
|
||||
]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { Ff14MarketApplication } from './application/ff14-market-application';
|
||||
import { Ff14MarketClient } from './infrastructure/integration/ff14-market-client';
|
||||
import {
|
||||
Ff14MarketClient,
|
||||
type Ff14MarketPluginHost,
|
||||
} from './infrastructure/integration/ff14-market-client';
|
||||
import {
|
||||
buildFf14MarketOperations,
|
||||
type Ff14MarketManifest,
|
||||
@ -12,11 +15,49 @@ type Ff14MarketPluginOptions = {
|
||||
now?: () => Date;
|
||||
};
|
||||
|
||||
export type QqbotGenericPluginCreateOptions = {
|
||||
host: Record<string, unknown>;
|
||||
manifest: Ff14MarketManifest & { key?: string };
|
||||
normalizeError: (error: unknown, fallback?: string) => string | Error;
|
||||
now: () => Date;
|
||||
runtime: {
|
||||
configSnapshot: Record<string, string | undefined>;
|
||||
installationId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Ff14MarketPluginCreateOptions =
|
||||
| Ff14MarketPluginOptions
|
||||
| QqbotGenericPluginCreateOptions;
|
||||
|
||||
/**
|
||||
* 创建 FF14 市场插件对象或配置。
|
||||
* @param options - FF14 市场列表;使用 `host`、`manifest`、`now`、`normalizeError` 字段生成结果。
|
||||
* Creates the FF14 Market plugin entry for the legacy loader or generic worker runtime.
|
||||
* @param options - Legacy package-local options or generic worker options with config snapshot and host RPC facade.
|
||||
* @returns FF14 Market command plugin instance.
|
||||
*/
|
||||
export function createPlugin(options: Ff14MarketPluginOptions) {
|
||||
export function createPlugin(options: Ff14MarketPluginCreateOptions) {
|
||||
if (isFf14GenericPluginCreateOptions(options)) {
|
||||
return createFf14MarketPluginFromOptions({
|
||||
host: createFf14MarketGenericHostAdapter(options),
|
||||
manifest: normalizeFf14MarketManifest(options.manifest),
|
||||
normalizeError: (error, fallback) =>
|
||||
normalizeFf14MarketGenericError(
|
||||
options.normalizeError,
|
||||
error,
|
||||
fallback,
|
||||
),
|
||||
now: options.now,
|
||||
});
|
||||
}
|
||||
return createFf14MarketPluginFromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the FF14 Market plugin from the package-local host contract.
|
||||
* @param options - Package-local host, manifest, clock, and error normalizer.
|
||||
* @returns FF14 Market command plugin used by current tests and legacy loaders.
|
||||
*/
|
||||
function createFf14MarketPluginFromOptions(options: Ff14MarketPluginOptions) {
|
||||
const application = new Ff14MarketApplication(
|
||||
new Ff14MarketClient(options.host),
|
||||
);
|
||||
@ -55,6 +96,138 @@ export function createPlugin(options: Ff14MarketPluginOptions) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether FF14 Market create options came from the generic worker runtime.
|
||||
* @param options - Candidate create options supplied by the plugin loader.
|
||||
* @returns `true` when a runtime config snapshot is present.
|
||||
*/
|
||||
function isFf14GenericPluginCreateOptions(
|
||||
options: Ff14MarketPluginCreateOptions,
|
||||
): options is QqbotGenericPluginCreateOptions {
|
||||
return (
|
||||
!!(options as QqbotGenericPluginCreateOptions).runtime?.configSnapshot &&
|
||||
!!(options as QqbotGenericPluginCreateOptions).manifest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes generic manifest aliases so FF14 Market keeps a package-local plugin key.
|
||||
* @param manifest - Manifest supplied by the generic worker descriptor.
|
||||
* @returns Manifest with `pluginKey` filled from `key` when needed.
|
||||
*/
|
||||
function normalizeFf14MarketManifest(
|
||||
manifest: QqbotGenericPluginCreateOptions['manifest'],
|
||||
): Ff14MarketManifest {
|
||||
return {
|
||||
...manifest,
|
||||
pluginKey: manifest.pluginKey || manifest.key || 'ff14-market',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the FF14 Market client host over generic worker host methods.
|
||||
* @param options - Generic worker context containing host RPC methods and config snapshot.
|
||||
* @returns Package-local host expected by `Ff14MarketClient`.
|
||||
*/
|
||||
function createFf14MarketGenericHostAdapter(
|
||||
options: QqbotGenericPluginCreateOptions,
|
||||
): Ff14MarketPluginHost {
|
||||
const { host, runtime } = options;
|
||||
return {
|
||||
/**
|
||||
* Reads FF14 Market config synchronously from the worker startup snapshot.
|
||||
* @param key - Runtime config key declared by the FF14 Market package manifest.
|
||||
* @returns Snapshot value cast to the requested config type.
|
||||
*/
|
||||
getConfig: <T = string>(key: string) =>
|
||||
runtime.configSnapshot[key] as T | undefined,
|
||||
/**
|
||||
* Reads dictionary items through the generic host bridge.
|
||||
* @param dictCode - FF14 dictionary code used for region, data center, or world lookup.
|
||||
* @returns Dictionary items returned by the host dictionary service.
|
||||
*/
|
||||
getDictItemsByKey: async (dictCode) =>
|
||||
await callFf14GenericHost(host, 'getDictItemsByKey', dictCode),
|
||||
/**
|
||||
* Reads the FF14 dictionary relation tree through the generic host bridge.
|
||||
* @param input - Relation tree root dictionary code requested by the FF14 catalog builder.
|
||||
* @returns Relation tree nodes returned by the host dictionary service.
|
||||
*/
|
||||
relationTree: async (input) =>
|
||||
await callFf14GenericHost(host, 'relationTree', input),
|
||||
/**
|
||||
* Performs FF14 Market HTTP JSON requests through the generic host bridge.
|
||||
* @param request - Package-local HTTP request options from `Ff14MarketClient`.
|
||||
* @returns Parsed JSON payload returned by the host HTTP client.
|
||||
*/
|
||||
requestJson: async <T>(request) =>
|
||||
await callFf14GenericHost<T>(
|
||||
host,
|
||||
'requestJson',
|
||||
serializeFf14GenericHttpRequest(request),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls one FF14 Market generic host method and fails with a package-owned error when absent.
|
||||
* @param host - Generic worker host facade supplied by the platform runtime.
|
||||
* @param method - Host capability required by the FF14 Market adapter.
|
||||
* @param args - Positional arguments accepted by the host facade method.
|
||||
* @returns Host method result cast to the requested package-local type.
|
||||
*/
|
||||
async function callFf14GenericHost<TResult = any>(
|
||||
host: Record<string, unknown>,
|
||||
method: string,
|
||||
...args: unknown[]
|
||||
): Promise<TResult> {
|
||||
const fn = host[method];
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`FF14 Market generic host 缺少 ${method}`);
|
||||
}
|
||||
return (await fn(...args)) as TResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts package-local HTTP options to worker-safe generic host request data.
|
||||
* @param request - FF14 Market HTTP request containing URL and function-based failure message.
|
||||
* @returns Serializable request data accepted by the generic host bridge.
|
||||
*/
|
||||
function serializeFf14GenericHttpRequest(
|
||||
request: Parameters<Ff14MarketPluginHost['requestJson']>[0],
|
||||
) {
|
||||
const statusPlaceholder = 599;
|
||||
return {
|
||||
body: undefined,
|
||||
context: request.context,
|
||||
failureMessageTemplate: request
|
||||
.failureMessage(statusPlaceholder)
|
||||
.replaceAll(`${statusPlaceholder}`, '{statusCode}'),
|
||||
headers: undefined,
|
||||
invalidJsonMessage: request.invalidJsonMessage,
|
||||
method: request.method,
|
||||
timeoutMessage: request.timeoutMessage,
|
||||
timeoutMs: request.timeoutMs,
|
||||
url: request.url.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes generic worker errors to the legacy FF14 Market string error contract.
|
||||
* @param normalizeError - Generic worker error normalizer supplied by the platform runtime.
|
||||
* @param error - Error or arbitrary thrown value from package code.
|
||||
* @param fallback - FF14 Market fallback message used by health checks.
|
||||
* @returns String message consumed by legacy plugin health output.
|
||||
*/
|
||||
function normalizeFf14MarketGenericError(
|
||||
normalizeError: QqbotGenericPluginCreateOptions['normalizeError'],
|
||||
error: unknown,
|
||||
fallback: string,
|
||||
) {
|
||||
const normalized = normalizeError(error, fallback);
|
||||
return normalized instanceof Error ? normalized.message : `${normalized}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 FF14 市场插件输入。
|
||||
* @param date - date 输入;执行 `date.getFullYear()`、`date.getMonth()`、`date.getDate()`、`date.getHours()` 对应的 FF14 市场步骤。
|
||||
|
||||
@ -8,10 +8,21 @@
|
||||
"entry": "src/index.ts",
|
||||
"permissions": ["runtime.http", "plugin.config.read"],
|
||||
"runtime": {
|
||||
"workerType": "node-worker",
|
||||
"timeoutMs": 20000,
|
||||
"workerType": "thread",
|
||||
"timeoutMs": 30000,
|
||||
"memoryMb": 256,
|
||||
"maxConcurrency": 2
|
||||
"maxConcurrency": 1,
|
||||
"configKeys": [
|
||||
"FFLOGS_WEB_BASE_URL",
|
||||
"FFLOGS_BASE_URL",
|
||||
"FFLOGS_CLIENT_ID",
|
||||
"FFLOGS_CLIENT_SECRET",
|
||||
"FFLOGS_GRAPHQL_URL",
|
||||
"FFLOGS_TOKEN_URL",
|
||||
"FFLOGS_DEFAULT_SERVER",
|
||||
"FFLOGS_DEFAULT_SERVER_REGION",
|
||||
"FFLOGS_REQUEST_TIMEOUT_MS"
|
||||
]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { FflogsApplication } from './application/fflogs-application';
|
||||
import { FflogsClient } from './infrastructure/integration/fflogs-client';
|
||||
import {
|
||||
FflogsClient,
|
||||
type FflogsPluginHost,
|
||||
} from './infrastructure/integration/fflogs-client';
|
||||
import { buildFflogsOperations, type FflogsManifest } from './operations';
|
||||
|
||||
type FflogsPluginOptions = {
|
||||
@ -9,11 +12,45 @@ type FflogsPluginOptions = {
|
||||
now?: () => Date;
|
||||
};
|
||||
|
||||
export type QqbotGenericPluginCreateOptions = {
|
||||
host: Record<string, unknown>;
|
||||
manifest: FflogsManifest & { key?: string };
|
||||
normalizeError: (error: unknown, fallback?: string) => string | Error;
|
||||
now: () => Date;
|
||||
runtime: {
|
||||
configSnapshot: Record<string, string | undefined>;
|
||||
installationId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type FflogsPluginCreateOptions =
|
||||
| FflogsPluginOptions
|
||||
| QqbotGenericPluginCreateOptions;
|
||||
|
||||
/**
|
||||
* 创建 FFLogs 插件对象或配置。
|
||||
* @param options - FFLogs列表;使用 `host`、`manifest`、`now`、`normalizeError` 字段生成结果。
|
||||
* Creates the FFLogs plugin entry for the legacy loader or generic worker runtime.
|
||||
* @param options - Legacy package-local options or generic worker options with config snapshot and host RPC facade.
|
||||
* @returns FFLogs command plugin instance.
|
||||
*/
|
||||
export function createPlugin(options: FflogsPluginOptions) {
|
||||
export function createPlugin(options: FflogsPluginCreateOptions) {
|
||||
if (isFflogsGenericPluginCreateOptions(options)) {
|
||||
return createFflogsPluginFromOptions({
|
||||
host: createFflogsGenericHostAdapter(options),
|
||||
manifest: normalizeFflogsManifest(options.manifest),
|
||||
normalizeError: (error, fallback) =>
|
||||
normalizeFflogsGenericError(options.normalizeError, error, fallback),
|
||||
now: options.now,
|
||||
});
|
||||
}
|
||||
return createFflogsPluginFromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the FFLogs plugin from the package-local host contract.
|
||||
* @param options - Package-local host, manifest, clock, and error normalizer.
|
||||
* @returns FFLogs command plugin used by current tests and legacy loaders.
|
||||
*/
|
||||
function createFflogsPluginFromOptions(options: FflogsPluginOptions) {
|
||||
const application = new FflogsApplication(new FflogsClient(options.host));
|
||||
return {
|
||||
description: options.manifest.description,
|
||||
@ -46,6 +83,162 @@ export function createPlugin(options: FflogsPluginOptions) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether FFLogs create options came from the generic worker runtime.
|
||||
* @param options - Candidate create options supplied by the plugin loader.
|
||||
* @returns `true` when a runtime config snapshot is present.
|
||||
*/
|
||||
function isFflogsGenericPluginCreateOptions(
|
||||
options: FflogsPluginCreateOptions,
|
||||
): options is QqbotGenericPluginCreateOptions {
|
||||
return (
|
||||
!!(options as QqbotGenericPluginCreateOptions).runtime?.configSnapshot &&
|
||||
!!(options as QqbotGenericPluginCreateOptions).manifest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes generic manifest aliases so FFLogs keeps a package-local plugin key.
|
||||
* @param manifest - Manifest supplied by the generic worker descriptor.
|
||||
* @returns Manifest with `pluginKey` filled from `key` when needed.
|
||||
*/
|
||||
function normalizeFflogsManifest(
|
||||
manifest: QqbotGenericPluginCreateOptions['manifest'],
|
||||
): FflogsManifest {
|
||||
return {
|
||||
...manifest,
|
||||
pluginKey: manifest.pluginKey || manifest.key || 'fflogs',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the FFLogs client host over generic worker host methods.
|
||||
* @param options - Generic worker context containing host RPC methods and config snapshot.
|
||||
* @returns Package-local host expected by `FflogsClient`.
|
||||
*/
|
||||
function createFflogsGenericHostAdapter(
|
||||
options: QqbotGenericPluginCreateOptions,
|
||||
): FflogsPluginHost {
|
||||
const { host, runtime } = options;
|
||||
return {
|
||||
/**
|
||||
* Reads FFLogs config synchronously from the worker startup snapshot.
|
||||
* @param key - Runtime config key declared by the FFLogs package manifest.
|
||||
* @returns Snapshot value cast to the requested config type.
|
||||
*/
|
||||
getConfig: <T = string>(key: string) =>
|
||||
runtime.configSnapshot[key] as T | undefined,
|
||||
/**
|
||||
* Reads dictionary items through the legacy dictionary method name when FFLogs localization asks for it.
|
||||
* @param dictCode - Dictionary code used by FFLogs localization or FF14 world lookup.
|
||||
* @returns Dictionary items returned by the host dictionary service.
|
||||
*/
|
||||
getDictByKey: async (dictCode) =>
|
||||
await callFflogsGenericDictHost(host, dictCode),
|
||||
/**
|
||||
* Reads dictionary items through the generic dictionary method name for package-owned FF14 world lookup.
|
||||
* @param dictCode - Dictionary code used by FFLogs known-world resolution.
|
||||
* @returns Dictionary items returned by the host dictionary service.
|
||||
*/
|
||||
getDictItemsByKey: async (dictCode) =>
|
||||
await callFflogsGenericDictHost(host, dictCode),
|
||||
/**
|
||||
* Reads the FF14 dictionary relation tree through the generic host bridge.
|
||||
* @param input - Relation tree root dictionary code requested by FFLogs known-world resolution.
|
||||
* @returns Relation tree nodes returned by the host dictionary service.
|
||||
*/
|
||||
relationTree: async (input) =>
|
||||
await callFflogsGenericHost(host, 'relationTree', input),
|
||||
/**
|
||||
* Performs FFLogs HTTP JSON requests through the generic host bridge.
|
||||
* @param request - Package-local HTTP request options from `FflogsClient`.
|
||||
* @returns Parsed JSON payload returned by the host HTTP client.
|
||||
*/
|
||||
requestJson: async <T>(request) =>
|
||||
await callFflogsGenericHost<T>(
|
||||
host,
|
||||
'requestJson',
|
||||
serializeFflogsGenericHttpRequest(request),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads dictionary items through the preferred generic host method with a legacy name fallback.
|
||||
* @param host - Generic worker host facade supplied by the platform runtime.
|
||||
* @param dictCode - Dictionary code requested by FFLogs package code.
|
||||
* @returns Dictionary items returned by the host bridge.
|
||||
*/
|
||||
async function callFflogsGenericDictHost(
|
||||
host: Record<string, unknown>,
|
||||
dictCode: string,
|
||||
) {
|
||||
const method =
|
||||
typeof host.getDictItemsByKey === 'function'
|
||||
? 'getDictItemsByKey'
|
||||
: 'getDictByKey';
|
||||
return await callFflogsGenericHost(host, method, dictCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls one FFLogs generic host method and fails with a package-owned error when absent.
|
||||
* @param host - Generic worker host facade supplied by the platform runtime.
|
||||
* @param method - Host capability required by the FFLogs adapter.
|
||||
* @param args - Positional arguments accepted by the host facade method.
|
||||
* @returns Host method result cast to the requested package-local type.
|
||||
*/
|
||||
async function callFflogsGenericHost<TResult = any>(
|
||||
host: Record<string, unknown>,
|
||||
method: string,
|
||||
...args: unknown[]
|
||||
): Promise<TResult> {
|
||||
const fn = host[method];
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`FFLogs generic host 缺少 ${method}`);
|
||||
}
|
||||
return (await fn(...args)) as TResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts package-local HTTP options to worker-safe generic host request data.
|
||||
* @param request - FFLogs HTTP request containing URL and function-based failure message.
|
||||
* @returns Serializable request data accepted by the generic host bridge.
|
||||
*/
|
||||
function serializeFflogsGenericHttpRequest(
|
||||
request: Parameters<FflogsPluginHost['requestJson']>[0],
|
||||
) {
|
||||
const statusPlaceholder = 599;
|
||||
return {
|
||||
body: request.body,
|
||||
context: request.context,
|
||||
failureMessageTemplate: request
|
||||
.failureMessage(statusPlaceholder)
|
||||
.replaceAll(`${statusPlaceholder}`, '{statusCode}'),
|
||||
headers: request.headers,
|
||||
invalidJsonMessage: request.invalidJsonMessage,
|
||||
method: request.method,
|
||||
timeoutMessage: request.timeoutMessage,
|
||||
timeoutMs: request.timeoutMs,
|
||||
url: request.url.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes generic worker errors to the legacy FFLogs string error contract.
|
||||
* @param normalizeError - Generic worker error normalizer supplied by the platform runtime.
|
||||
* @param error - Error or arbitrary thrown value from package code.
|
||||
* @param fallback - FFLogs fallback message used by health checks.
|
||||
* @returns String message consumed by legacy plugin health output.
|
||||
*/
|
||||
function normalizeFflogsGenericError(
|
||||
normalizeError: QqbotGenericPluginCreateOptions['normalizeError'],
|
||||
error: unknown,
|
||||
fallback: string,
|
||||
) {
|
||||
const normalized = normalizeError(error, fallback);
|
||||
return normalized instanceof Error ? normalized.message : `${normalized}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 FFLogs 插件输入。
|
||||
* @param date - date 输入;执行 `date.getFullYear()`、`date.getMonth()`、`date.getDate()`、`date.getHours()` 对应的 FFLogs步骤。
|
||||
|
||||
@ -19,6 +19,14 @@ import type {
|
||||
import type { FflogsKnownWorldResolver } from '../../application/fflogs-input-parser';
|
||||
import { resolveFflogsConfig } from '../../config/fflogs-config';
|
||||
import { FflogsOAuthTokenCache } from '../storage/oauth-token-cache';
|
||||
import {
|
||||
buildFf14MarketCatalog,
|
||||
buildFf14MarketCatalogFromTree,
|
||||
isFf14LocationName,
|
||||
QQBOT_FF14_MARKET_DICT_CODES,
|
||||
splitFf14WorldPath,
|
||||
type Ff14DictItem,
|
||||
} from '../../../../ff14-market/src/domain/ff14-worlds';
|
||||
|
||||
const FFLOGS_LOCALIZATION_DICT_CODES = {
|
||||
job: 'FFLOGS_JOB_LABEL',
|
||||
@ -37,9 +45,11 @@ type FflogsEncounterCatalogItem = {
|
||||
|
||||
export type FflogsPluginHost = {
|
||||
getConfig: <T = string>(key: string) => T | undefined;
|
||||
getDictByKey: (
|
||||
getDictByKey?: (
|
||||
dictCode: string,
|
||||
) => Promise<Array<{ label?: string; value?: string }>>;
|
||||
getDictItemsByKey?: (dictCode: string) => Promise<Ff14DictItem[]>;
|
||||
relationTree?: (input: { dictCode: string }) => Promise<Ff14DictItem[]>;
|
||||
requestJson: <T>(options: {
|
||||
body?: string;
|
||||
context: string;
|
||||
@ -244,10 +254,20 @@ export class FflogsClient {
|
||||
|
||||
/**
|
||||
* 解析Known World。
|
||||
* @param value - 待转换值;影响 resolveKnownWorld 的返回值。
|
||||
* @param value - FF14 server, region, data-center, or path token parsed from FFLogs command text.
|
||||
* @returns Legacy host resolution when available, otherwise package-owned dictionary fallback for generic workers.
|
||||
*/
|
||||
async resolveKnownWorld(value: string) {
|
||||
return this.host.resolveKnownWorld?.(value) || null;
|
||||
if (this.host.resolveKnownWorld) {
|
||||
return this.host.resolveKnownWorld(value);
|
||||
}
|
||||
|
||||
const catalog = await this.loadFf14MarketCatalog();
|
||||
if (!isFf14LocationName(catalog, value)) return null;
|
||||
const worldPath = splitFf14WorldPath(value);
|
||||
return {
|
||||
serverSlug: worldPath.world || value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1700,7 +1720,7 @@ export class FflogsClient {
|
||||
* @param dictCode - dictCode 输入;驱动 `host.getDictByKey()` 的 FFLogs步骤。
|
||||
*/
|
||||
private async getNormalizedDictMap(dictCode: string) {
|
||||
const dicts = await this.host.getDictByKey(dictCode);
|
||||
const dicts = await this.getDictItems(dictCode);
|
||||
const map = new Map<string, string>();
|
||||
for (const { label, value } of dicts) {
|
||||
for (const key of this.buildLookupKeys(`${value}`, `${label}`)) {
|
||||
@ -1710,6 +1730,47 @@ export class FflogsClient {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the FF14 market dictionary catalog inside the FFLogs package for known-world parsing.
|
||||
* @returns FF14 region/data-center/world catalog assembled from generic host dictionary methods.
|
||||
*/
|
||||
private async loadFf14MarketCatalog() {
|
||||
if (this.host.relationTree) {
|
||||
const treeCatalog = buildFf14MarketCatalogFromTree(
|
||||
await this.host.relationTree({
|
||||
dictCode: QQBOT_FF14_MARKET_DICT_CODES.region,
|
||||
}),
|
||||
);
|
||||
if (treeCatalog.dataCenters.length > 0) return treeCatalog;
|
||||
}
|
||||
|
||||
const [regions, dataCenters, worlds] = await Promise.all([
|
||||
this.getDictItems(QQBOT_FF14_MARKET_DICT_CODES.region),
|
||||
this.getDictItems(QQBOT_FF14_MARKET_DICT_CODES.dataCenter),
|
||||
this.getDictItems(QQBOT_FF14_MARKET_DICT_CODES.world),
|
||||
]);
|
||||
return buildFf14MarketCatalog({
|
||||
dataCenters,
|
||||
regions,
|
||||
worlds,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads dictionary items through whichever generic or legacy host dictionary method is available.
|
||||
* @param dictCode - Dictionary code used by FFLogs localization or FF14 world catalog lookup.
|
||||
* @returns Dictionary items from `getDictItemsByKey`, legacy `getDictByKey`, or an empty list.
|
||||
*/
|
||||
private async getDictItems(dictCode: string): Promise<Ff14DictItem[]> {
|
||||
if (this.host.getDictItemsByKey) {
|
||||
return this.host.getDictItemsByKey(dictCode);
|
||||
}
|
||||
if (this.host.getDictByKey) {
|
||||
return this.host.getDictByKey(dictCode);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 FFLogs 插件流程。
|
||||
* @param value - 待转换值;影响 localizeEncounter 的返回值。
|
||||
|
||||
@ -14,10 +14,17 @@
|
||||
"plugin.storage.write"
|
||||
],
|
||||
"runtime": {
|
||||
"workerType": "node-worker",
|
||||
"timeoutMs": 5000,
|
||||
"workerType": "thread",
|
||||
"timeoutMs": 10000,
|
||||
"memoryMb": 128,
|
||||
"maxConcurrency": 1
|
||||
"maxConcurrency": 1,
|
||||
"configKeys": [
|
||||
"QQBOT_REPEATER_CONFIG_CACHE_TTL_MS",
|
||||
"QQBOT_REPEATER_MAX_TEXT_LENGTH",
|
||||
"QQBOT_REPEATER_MIN_INTERVAL_MS",
|
||||
"QQBOT_REPEATER_STATE_TTL_MS",
|
||||
"QQBOT_REPEATER_THRESHOLD"
|
||||
]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
|
||||
@ -9,11 +9,43 @@ type RepeaterPluginOptions = {
|
||||
now?: () => number;
|
||||
};
|
||||
|
||||
export type QqbotGenericPluginCreateOptions = {
|
||||
host: Record<string, unknown>;
|
||||
manifest: RepeaterManifest & { key?: string };
|
||||
normalizeError: (error: unknown, fallback?: string) => string | Error;
|
||||
now: () => Date;
|
||||
runtime: {
|
||||
configSnapshot: Record<string, string | undefined>;
|
||||
installationId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type RepeaterPluginCreateOptions =
|
||||
| RepeaterPluginOptions
|
||||
| QqbotGenericPluginCreateOptions;
|
||||
|
||||
/**
|
||||
* 创建 复读插件对象或配置。
|
||||
* @param options - 模块列表;使用 `host`、`manifest`、`now` 字段生成结果。
|
||||
* Creates the Repeater plugin entry for the legacy loader or generic worker runtime.
|
||||
* @param options - Legacy package-local options or generic worker options with config snapshot and host RPC facade.
|
||||
* @returns Repeater event plugin instance.
|
||||
*/
|
||||
export function createPlugin(options: RepeaterPluginOptions) {
|
||||
export function createPlugin(options: RepeaterPluginCreateOptions) {
|
||||
if (isRepeaterGenericPluginCreateOptions(options)) {
|
||||
return createRepeaterPluginFromOptions({
|
||||
host: createRepeaterGenericHostAdapter(options),
|
||||
manifest: normalizeRepeaterManifest(options.manifest),
|
||||
now: () => options.now().getTime(),
|
||||
});
|
||||
}
|
||||
return createRepeaterPluginFromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Repeater plugin from the package-local host contract.
|
||||
* @param options - Package-local host, manifest, and millisecond clock used by repeat policy state.
|
||||
* @returns Repeater event plugin used by current tests, legacy loaders, and generic workers.
|
||||
*/
|
||||
function createRepeaterPluginFromOptions(options: RepeaterPluginOptions) {
|
||||
const application = new RepeaterApplication(
|
||||
options.host,
|
||||
options.manifest,
|
||||
@ -45,6 +77,18 @@ export function createPlugin(options: RepeaterPluginOptions) {
|
||||
connectStatus?: string;
|
||||
selfId: string;
|
||||
}) => application.getSummary(params),
|
||||
/**
|
||||
* Routes generic worker event calls to the package-owned Repeater message handler.
|
||||
* @param eventKey - Manifest event key or event name supplied by the generic worker.
|
||||
* @param event - Normalized QQBot message event payload.
|
||||
*/
|
||||
handleEvent: (eventKey: string, event: unknown) =>
|
||||
handleRepeaterGenericEvent(
|
||||
eventKey,
|
||||
event,
|
||||
options.manifest,
|
||||
handleMessage,
|
||||
),
|
||||
handleMessage,
|
||||
/**
|
||||
* 维护 模块事件绑定。
|
||||
@ -53,3 +97,136 @@ export function createPlugin(options: RepeaterPluginOptions) {
|
||||
unbind: (selfId: string) => application.unbind(selfId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether Repeater create options came from the generic worker runtime.
|
||||
* @param options - Candidate create options supplied by the plugin loader.
|
||||
* @returns `true` when a runtime config snapshot is present.
|
||||
*/
|
||||
function isRepeaterGenericPluginCreateOptions(
|
||||
options: RepeaterPluginCreateOptions,
|
||||
): options is QqbotGenericPluginCreateOptions {
|
||||
return (
|
||||
!!(options as QqbotGenericPluginCreateOptions).runtime?.configSnapshot &&
|
||||
!!(options as QqbotGenericPluginCreateOptions).manifest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes generic manifest aliases so Repeater keeps a package-local plugin key.
|
||||
* @param manifest - Manifest supplied by the generic worker descriptor.
|
||||
* @returns Manifest with `pluginKey` filled from `key` when needed.
|
||||
*/
|
||||
function normalizeRepeaterManifest(
|
||||
manifest: QqbotGenericPluginCreateOptions['manifest'],
|
||||
): RepeaterManifest {
|
||||
return {
|
||||
...manifest,
|
||||
events: manifest.events || [],
|
||||
pluginKey: manifest.pluginKey || manifest.key || 'repeater',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Repeater application host over generic worker host methods.
|
||||
* @param options - Generic worker context containing host RPC methods and config snapshot.
|
||||
* @returns Package-local host expected by `RepeaterApplication`.
|
||||
*/
|
||||
function createRepeaterGenericHostAdapter(
|
||||
options: QqbotGenericPluginCreateOptions,
|
||||
): RepeaterPluginHost {
|
||||
const { host, runtime } = options;
|
||||
return {
|
||||
/**
|
||||
* Binds this Repeater package to one account through the generic host bridge.
|
||||
* @param selfId - QQBot self account id whose event binding is being enabled.
|
||||
* @param pluginKey - Package plugin key; parent bridge still uses its authoritative descriptor key.
|
||||
*/
|
||||
bindEventPlugin: async (selfId, pluginKey) => {
|
||||
await callRepeaterGenericHost(host, 'bindEventPlugin', selfId, pluginKey);
|
||||
},
|
||||
/**
|
||||
* Reads currently bound event plugin keys for one QQBot account.
|
||||
* @param selfId - QQBot self account id whose event plugin bindings are being queried.
|
||||
* @returns Bound plugin keys returned by the host account service.
|
||||
*/
|
||||
getBoundEventPluginKeys: async (selfId) =>
|
||||
await callRepeaterGenericHost(host, 'getBoundEventPluginKeys', selfId),
|
||||
/**
|
||||
* Reads Repeater config synchronously from the worker startup snapshot.
|
||||
* @param key - Runtime config key declared by the Repeater package manifest.
|
||||
* @returns Snapshot value cast to the requested config type.
|
||||
*/
|
||||
getConfig: <T = string>(key: string) =>
|
||||
runtime.configSnapshot[key] as T | undefined,
|
||||
/**
|
||||
* Sends text through the QQBot host send queue.
|
||||
* @param input - Target account, conversation, and message text produced by the repeat policy.
|
||||
* @returns Host send result for observability.
|
||||
*/
|
||||
sendText: async (input) =>
|
||||
await callRepeaterGenericHost(host, 'sendText', input),
|
||||
/**
|
||||
* Unbinds this Repeater package from one account through the generic host bridge.
|
||||
* @param selfId - QQBot self account id whose event binding is being disabled.
|
||||
* @param pluginKey - Package plugin key; parent bridge still uses its authoritative descriptor key.
|
||||
*/
|
||||
unbindEventPlugin: async (selfId, pluginKey) => {
|
||||
await callRepeaterGenericHost(
|
||||
host,
|
||||
'unbindEventPlugin',
|
||||
selfId,
|
||||
pluginKey,
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Emits a package warning through the generic host bridge without blocking message handling.
|
||||
* @param message - Repeater warning text to write into host logs.
|
||||
*/
|
||||
warn: (message) => {
|
||||
void callRepeaterGenericHost(host, 'warn', message).catch(
|
||||
() => undefined,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes a generic worker event to the Repeater message handler when it matches the manifest event.
|
||||
* @param eventKey - Manifest event key, event name, or handler name supplied by the generic worker.
|
||||
* @param event - Normalized QQBot event payload forwarded by the platform.
|
||||
* @param manifest - Repeater manifest containing package-owned event metadata.
|
||||
* @param handleMessage - Package-local message handler produced by `createRepeaterMessageEventHandler`.
|
||||
* @returns Repeater message handling result or `false` for unrelated events.
|
||||
*/
|
||||
async function handleRepeaterGenericEvent(
|
||||
eventKey: string,
|
||||
event: unknown,
|
||||
manifest: RepeaterManifest,
|
||||
handleMessage: (message: any) => Promise<boolean>,
|
||||
) {
|
||||
const matched = (manifest.events || []).some((item: any) =>
|
||||
[item.key, item.eventName, item.handlerName].includes(eventKey),
|
||||
);
|
||||
if (!matched && eventKey !== 'message') return false;
|
||||
return handleMessage(event as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls one Repeater generic host method and fails with a package-owned error when absent.
|
||||
* @param host - Generic worker host facade supplied by the platform runtime.
|
||||
* @param method - Host capability required by the Repeater adapter.
|
||||
* @param args - Positional arguments accepted by the host facade method.
|
||||
* @returns Host method result cast to the requested package-local type.
|
||||
*/
|
||||
async function callRepeaterGenericHost<TResult = any>(
|
||||
host: Record<string, unknown>,
|
||||
method: string,
|
||||
...args: unknown[]
|
||||
): Promise<TResult> {
|
||||
const fn = host[method];
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`Repeater generic host 缺少 ${method}`);
|
||||
}
|
||||
return (await fn(...args)) as TResult;
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import * as path from 'path';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
jest.mock(
|
||||
'@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache',
|
||||
() => ({
|
||||
@ -82,11 +85,101 @@ jest.mock('@/modules/qqbot/plugins/bangdream/src/operations', () => ({
|
||||
import { createPlugin } from '@/modules/qqbot/plugins/bangdream/src';
|
||||
import { BangDreamCommandContext } from '@/modules/qqbot/plugins/bangdream/src/application/bangdream-command-context';
|
||||
import { waitForBangDreamCatalogReady } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
|
||||
import {
|
||||
fuzzySearchPath,
|
||||
projectRoot,
|
||||
} from '@/modules/qqbot/plugins/bangdream/src/config/runtime-config';
|
||||
import type { BangDreamRuntimeIo } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
|
||||
|
||||
const manifestOperations = mockManifestOperations.map(([key, handlerName]) => ({
|
||||
handlerName,
|
||||
key,
|
||||
}));
|
||||
const bangDreamPackageRoot = path.resolve(projectRoot, '..');
|
||||
|
||||
/**
|
||||
* Creates a minimal generic BangDream manifest for package-entry adapter tests.
|
||||
* @returns Manifest descriptor fields required by the generic worker `createPlugin` contract.
|
||||
*/
|
||||
function createGenericBangDreamManifest() {
|
||||
return {
|
||||
key: 'bangdream',
|
||||
pluginKey: 'bangdream',
|
||||
name: 'BangDream',
|
||||
version: '1.0.0',
|
||||
entry: 'src/index.ts',
|
||||
runtime: {
|
||||
configKeys: ['BANGDREAM_TSUGU_BESTDORI_BASE_URL'],
|
||||
maxConcurrency: 1,
|
||||
memoryMb: 256,
|
||||
timeoutMs: 30000,
|
||||
workerType: 'thread',
|
||||
},
|
||||
operations: manifestOperations,
|
||||
tasks: [],
|
||||
events: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the BangDream runtime IO adapter that the package entry registered during generic creation.
|
||||
* @returns Runtime IO object passed to `configureBangDreamRuntimeIo`.
|
||||
*/
|
||||
function getConfiguredRuntimeIo(): BangDreamRuntimeIo {
|
||||
const io = mockConfigureBangDreamRuntimeIo.mock.calls.at(-1)?.[0];
|
||||
if (!io) throw new Error('BangDream runtime IO was not configured');
|
||||
return io as BangDreamRuntimeIo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deterministic XLSX workbook buffer for generic readExcelRows adapter tests.
|
||||
* @returns XLSX buffer containing one data row on the first worksheet.
|
||||
*/
|
||||
function createWorkbookBuffer() {
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const worksheet = XLSX.utils.json_to_sheet([
|
||||
{ name: 'Test Card', score: 42 },
|
||||
]);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
return XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }) as Buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes arbitrary BangDream package errors the same way the generic worker passes errors to entries.
|
||||
* @param error - Error or thrown value produced while creating or executing the plugin.
|
||||
* @returns Stable message string consumed by the package-local error adapter.
|
||||
*/
|
||||
function normalizeGenericBangDreamError(error: unknown) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a deterministic generic worker clock for package entry creation.
|
||||
* @returns Fixed Date used to prove the entry accepts the generic `now` callback.
|
||||
*/
|
||||
function getGenericBangDreamNow() {
|
||||
return new Date('2026-06-18T00:00:00.000Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the generic BangDream plugin with a supplied host and default config snapshot.
|
||||
* @param host - Generic host facade whose methods are inspected by adapter tests.
|
||||
* @returns Plugin instance created through the generic worker contract.
|
||||
*/
|
||||
async function createGenericBangDreamPlugin(host: Record<string, unknown>) {
|
||||
return await createPlugin({
|
||||
host,
|
||||
manifest: createGenericBangDreamManifest(),
|
||||
normalizeError: normalizeGenericBangDreamError,
|
||||
now: getGenericBangDreamNow,
|
||||
runtime: {
|
||||
configSnapshot: {
|
||||
BANGDREAM_TSUGU_BESTDORI_BASE_URL: 'https://example.invalid/bestdori',
|
||||
},
|
||||
installationId: 'install-bangdream',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('BangDream package entry', () => {
|
||||
beforeEach(() => {
|
||||
@ -160,4 +253,196 @@ describe('BangDream package entry', () => {
|
||||
plugin.executeOperation('bangdream.event.search', { text: '50' }),
|
||||
).rejects.toThrow('normalized:图片渲染失败');
|
||||
});
|
||||
|
||||
it('accepts the generic plugin platform createPlugin contract', /**
|
||||
* Verifies the BangDream package entry can be created from the generic worker descriptor contract.
|
||||
* @returns Assertion promise proving the entry exposes command execution and health hooks.
|
||||
*/
|
||||
async () => {
|
||||
const plugin = await createGenericBangDreamPlugin({
|
||||
/**
|
||||
* Simulates the generic host config RPC; the BangDream adapter should prefer runtime.configSnapshot for sync config reads.
|
||||
* @returns No value so the test fails if sync package code depends on this async RPC path.
|
||||
*/
|
||||
getConfig: async () => undefined,
|
||||
/**
|
||||
* Simulates the generic host batch config RPC that is present on real worker hosts.
|
||||
* @returns Empty config map because this contract test uses the startup snapshot as its config source.
|
||||
*/
|
||||
getConfigMany: async () => ({}),
|
||||
/**
|
||||
* Simulates dictionary lookup for BangDream alias/server mappings during plugin activation.
|
||||
* @returns Empty dictionary list because this test only checks entry construction.
|
||||
*/
|
||||
getDictItemsByKey: async () => [],
|
||||
/**
|
||||
* Simulates package asset reads through the generic worker host boundary.
|
||||
* @returns Empty buffer because no render asset is consumed while creating the plugin instance.
|
||||
*/
|
||||
readAssetFile: async () => Buffer.from([]),
|
||||
/**
|
||||
* Simulates binary HTTP requests for BangDream resource downloads.
|
||||
* @returns Empty buffer because this contract test never executes a networked operation.
|
||||
*/
|
||||
requestBuffer: async () => Buffer.from([]),
|
||||
/**
|
||||
* Simulates JSON HTTP requests for BangDream catalog/data clients.
|
||||
* @returns Empty object because this contract test only asserts the created plugin shape.
|
||||
*/
|
||||
requestJson: async () => ({}),
|
||||
/**
|
||||
* Simulates host-mediated sleep used by retry logic.
|
||||
* @returns Resolved promise without delaying the package-entry contract test.
|
||||
*/
|
||||
sleep: async () => undefined,
|
||||
/**
|
||||
* Simulates host warning logging for non-fatal worker adapter notices.
|
||||
* @returns Nothing because warning output is irrelevant to plugin construction.
|
||||
*/
|
||||
warn: () => undefined,
|
||||
});
|
||||
|
||||
expect(plugin).toEqual(
|
||||
expect.objectContaining({
|
||||
executeOperation: expect.any(Function),
|
||||
health: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('maps absolute package asset paths to package-relative generic host reads', /**
|
||||
* Verifies the generic BangDream IO adapter never sends absolute package paths to host file APIs.
|
||||
* @returns Assertion promise covering package-root relative host path normalization.
|
||||
*/
|
||||
async () => {
|
||||
const readAssetFile = jest.fn(
|
||||
/**
|
||||
* Simulates generic host package asset reads and records the normalized path.
|
||||
* @param filePath - Package-relative path received by the host bridge.
|
||||
* @returns Distinct asset bytes used to prove the adapter returns host content.
|
||||
*/
|
||||
async (filePath: string) => Buffer.from(`asset:${filePath}`),
|
||||
);
|
||||
await createGenericBangDreamPlugin({
|
||||
/**
|
||||
* Simulates the generic host config RPC; sync BangDream config must come from the runtime snapshot.
|
||||
* @returns No value because this test only exercises file IO path normalization.
|
||||
*/
|
||||
getConfig: async () => undefined,
|
||||
/**
|
||||
* Simulates dictionary lookup for activation-time cache refresh.
|
||||
* @returns Empty list because file IO path normalization is independent of dictionaries.
|
||||
*/
|
||||
getDictItemsByKey: async () => [],
|
||||
readAssetFile,
|
||||
/**
|
||||
* Simulates JSON HTTP host calls that are not used in this path-normalization test.
|
||||
* @returns Empty object for unused request paths.
|
||||
*/
|
||||
requestJson: async () => ({}),
|
||||
/**
|
||||
* Simulates host sleep used by retry paths outside this test.
|
||||
* @returns Resolved promise without delay.
|
||||
*/
|
||||
sleep: async () => undefined,
|
||||
/**
|
||||
* Simulates host warning logging for non-fatal adapter messages.
|
||||
* @returns Nothing because warning output is irrelevant here.
|
||||
*/
|
||||
warn: () => undefined,
|
||||
});
|
||||
const io = getConfiguredRuntimeIo();
|
||||
const absoluteAssetPath = path.join(
|
||||
bangDreamPackageRoot,
|
||||
'src',
|
||||
'assets',
|
||||
'BG',
|
||||
'live.png',
|
||||
);
|
||||
|
||||
await expect(io.readAssetFile?.(absoluteAssetPath)).resolves.toEqual(
|
||||
Buffer.from('asset:src/assets/BG/live.png'),
|
||||
);
|
||||
|
||||
expect(readAssetFile).toHaveBeenCalledWith('src/assets/BG/live.png');
|
||||
});
|
||||
|
||||
it('preloads sync JSON and parses Excel rows through generic host file reads', /**
|
||||
* Verifies generic BangDream IO supplies sync JSON reads and XLSX parsing without absolute host paths.
|
||||
* @returns Assertion promise covering synchronous JSON cache and XLSX buffer parsing.
|
||||
*/
|
||||
async () => {
|
||||
const fuzzySearchConfig = {
|
||||
aliases: { 夏祭り: ['natsumatsuri'] },
|
||||
servers: ['cn'],
|
||||
};
|
||||
const workbookBuffer = createWorkbookBuffer();
|
||||
const readJsonFile = jest.fn(
|
||||
/**
|
||||
* Simulates generic host JSON reads used to preload synchronous BangDream config files.
|
||||
* @param filePath - Package-relative JSON path received by the host bridge.
|
||||
* @returns Fuzzy-search config snapshot for sync package consumers.
|
||||
*/
|
||||
async (filePath: string) =>
|
||||
filePath === 'src/config/static/fuzzy-search-settings.json'
|
||||
? fuzzySearchConfig
|
||||
: { filePath },
|
||||
);
|
||||
const readAssetFile = jest.fn(
|
||||
/**
|
||||
* Simulates generic host binary package reads for Excel static patch files.
|
||||
* @param filePath - Package-relative asset or config path received by the host bridge.
|
||||
* @returns XLSX workbook bytes for static patch parsing.
|
||||
*/
|
||||
async (filePath: string) =>
|
||||
filePath.endsWith('.xlsx') ? workbookBuffer : Buffer.from([]),
|
||||
);
|
||||
await createGenericBangDreamPlugin({
|
||||
/**
|
||||
* Simulates the generic host config RPC; sync BangDream config must come from the runtime snapshot.
|
||||
* @returns No value because this test only exercises package file IO.
|
||||
*/
|
||||
getConfig: async () => undefined,
|
||||
/**
|
||||
* Simulates dictionary lookup for activation-time cache refresh.
|
||||
* @returns Empty list because file IO behavior is independent of dictionaries.
|
||||
*/
|
||||
getDictItemsByKey: async () => [],
|
||||
readAssetFile,
|
||||
readJsonFile,
|
||||
/**
|
||||
* Simulates JSON HTTP host calls that are not used in this file IO test.
|
||||
* @returns Empty object for unused request paths.
|
||||
*/
|
||||
requestJson: async () => ({}),
|
||||
/**
|
||||
* Simulates host sleep used by retry paths outside this test.
|
||||
* @returns Resolved promise without delay.
|
||||
*/
|
||||
sleep: async () => undefined,
|
||||
/**
|
||||
* Simulates host warning logging for non-fatal adapter messages.
|
||||
* @returns Nothing because warning output is irrelevant here.
|
||||
*/
|
||||
warn: () => undefined,
|
||||
});
|
||||
const io = getConfiguredRuntimeIo();
|
||||
const absoluteExcelPath = path.join(
|
||||
bangDreamPackageRoot,
|
||||
'src',
|
||||
'config',
|
||||
'static',
|
||||
'cards.xlsx',
|
||||
);
|
||||
|
||||
expect(io.readJsonFileSync?.(fuzzySearchPath)).toEqual(fuzzySearchConfig);
|
||||
await expect(io.readExcelRows?.(absoluteExcelPath)).resolves.toEqual([
|
||||
{ name: 'Test Card', score: 42 },
|
||||
]);
|
||||
|
||||
expect(readJsonFile).toHaveBeenCalledWith(
|
||||
'src/config/static/fuzzy-search-settings.json',
|
||||
);
|
||||
expect(readAssetFile).toHaveBeenCalledWith('src/config/static/cards.xlsx');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user