♻️ chore: refactor router runtime (#9306)

This commit is contained in:
YuTengjing
2025-09-17 23:30:04 +08:00
committed by GitHub
parent 2e928cd587
commit 1762dc9148
21 changed files with 240 additions and 151 deletions

View File

@@ -18,13 +18,13 @@ The project uses the following technologies:
- Next.js 15 for frontend and backend, using app router instead of pages router
- react 19, using hooks, functional components, react server components
- TypeScript programming language
- antd, @lobehub/ui for component framework
- antd, `@lobehub/ui` for component framework
- antd-style for css-in-js framework
- react-layout-kit for flex layout
- react-i18next for i18n
- lucide-react, @ant-design/icons for icons
- @lobehub/icons for AI provider/model logo icon
- @formkit/auto-animate for react list animation
- lucide-react, `@ant-design/icons` for icons
- `@lobehub/icons` for AI provider/model logo icon
- `@formkit/auto-animate` for react list animation
- zustand for global state management
- nuqs for type-safe search params state manager
- SWR for react data fetch

View File

@@ -86,9 +86,9 @@ const Card: FC<CardProps> = ({ title, content }) => {
## Lobe UI 包含的组件
- 不知道 @lobehub/ui 的组件怎么用,有哪些属性,就自己搜下这个项目其它地方怎么用的,不要瞎猜,大部分组件都是在 antd 的基础上扩展了属性
- 不知道 `@lobehub/ui` 的组件怎么用,有哪些属性,就自己搜下这个项目其它地方怎么用的,不要瞎猜,大部分组件都是在 antd 的基础上扩展了属性
- 具体用法不懂可以联网搜索,例如 ActionIcon 就爬取 https://ui.lobehub.com/components/action-icon
- 可以阅读 node_modules/@lobehub/ui/es/index.js 了解有哪些组件,每个组件的属性是什么
- 可以阅读 `node_modules/@lobehub/ui/es/index.js` 了解有哪些组件,每个组件的属性是什么
- General
- ActionIcon

View File

@@ -8,11 +8,63 @@ alwaysApply: false
## Types and Type Safety
- Avoid explicit type annotations when TypeScript can infer types.
- Avoid implicitly `any` variables; explicitly type when necessary (e.g., `let a: number` instead of `let a`).
- Use the most accurate type possible (e.g., prefer `Record<PropertyKey, unknown>` over `object`).
- Prefer `interface` over `type` for object shapes (e.g., React component props). Keep `type` for unions, intersections, and utility types.
- Prefer `as const satisfies XyzInterface` over plain `as const` when suitable.
- avoid explicit type annotations when TypeScript can infer types.
- avoid implicitly `any` variables; explicitly type when necessary (e.g., `let a: number` instead of `let a`).
- use the most accurate type possible (e.g., prefer `Record<PropertyKey, unknown>` over `object`).
- prefer `interface` over `type` for object shapes (e.g., React component props). Keep `type` for unions, intersections, and utility types.
- prefer `as const satisfies XyzInterface` over plain `as const` when suitable.
- prefer `@ts-expect-error` over `@ts-ignore`
- prefer `Record<string, any>` over `any`
- **Avoid unnecessary null checks**: Before adding `xxx !== null`, `?.`, `??`, or `!.`, read the type definition to confirm the necessary. **Example:**
```typescript
// ❌ Wrong: budget.spend and budget.maxBudget is number, not number | null
if (budget.spend !== null && budget.maxBudget !== null && budget.spend >= budget.maxBudget) {
// ...
}
// ✅ Right
if (budget.spend >= budget.maxBudget) {
// ...
}
```
- **Avoid redundant runtime checks**: Don't add runtime validation for conditions already guaranteed by types or previous checks. Trust the type system and calling contract. **Example:**
```typescript
// ❌ Wrong: Adding impossible-to-fail checks
const due = await db.query.budgets.findMany({
where: and(isNotNull(budgets.budgetDuration)), // Already filtered non-null
});
const result = due.map(b => {
const nextReset = computeNextResetAt(b.budgetResetAt!, b.budgetDuration!);
if (!nextReset) { // This check is impossible to fail
throw new Error(`Unexpected null nextResetAt`);
}
return nextReset;
});
// ✅ Right: Trust the contract
const due = await db.query.budgets.findMany({
where: and(isNotNull(budgets.budgetDuration)),
});
const result = due.map(b => computeNextResetAt(b.budgetResetAt!, b.budgetDuration!));
```
- **Avoid meaningless null/undefined parameters**: Don't accept null/undefined for parameters that have no business meaning when null. Design strict function contracts. **Example:**
```typescript
// ❌ Wrong: Function accepts meaningless null input
function computeNextResetAt(currentResetAt: Date, durationStr: string | null): Date | null {
if (!durationStr) return null; // Why accept null if it just returns null?
}
// ✅ Right: Strict contract, clear responsibility
function computeNextResetAt(currentResetAt: Date, durationStr: string): Date {
// Function has single clear purpose, caller ensures valid input
}
```
## Imports and Modules

2
.gitignore vendored
View File

@@ -23,6 +23,7 @@ Desktop.ini
.history/
.windsurfrules
*.code-workspace
.vscode/sessions.json
# Temporary files
.temp/
@@ -115,3 +116,4 @@ CLAUDE.local.md
*.xls*
prd
GEMINI.md

View File

@@ -88,6 +88,8 @@
"**/src/server/routers/async/*.ts": "${filename} • async",
"**/src/server/routers/edge/*.ts": "${filename} • edge",
"**/src/locales/default/*.ts": "${filename} • locale"
"**/src/locales/default/*.ts": "${filename} • locale",
"**/index.*": "${dirname}/${filename}.${extname}"
}
}

View File

@@ -12,7 +12,7 @@ Built with modern technologies:
- **Database**: PostgreSQL, PGLite, Drizzle ORM
- **Testing**: Vitest, Testing Library
- **Package Manager**: pnpm (monorepo structure)
- **Build Tools**: Next.js (Turbopack in dev, Webpack in prod), Vitest
- **Build Tools**: Next.js (Turbopack in dev, Webpack in prod)
## Directory Structure
@@ -28,7 +28,7 @@ The project follows a well-organized monorepo structure:
### Git Workflow
- Use rebase for git pull: `git pull --rebase`
- Use rebase for git pull
- Git commit messages should prefix with gitmoji
- Git branch name format: `username/feat/feature-name`
- Use `.github/PULL_REQUEST_TEMPLATE.md` for PR descriptions
@@ -52,9 +52,6 @@ The project follows a well-organized monorepo structure:
#### React Components
- Use functional components with hooks
- Follow the component structure guidelines
- Use antd-style & @lobehub/ui for styling
- Implement proper error boundaries
#### Database Schema

View File

@@ -75,7 +75,7 @@
"stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
"test": "npm run test-app && npm run test-server",
"test-app": "vitest run",
"test-app:coverage": "vitest run --coverage",
"test-app:coverage": "vitest --coverage --silent='passed-only'",
"test:update": "vitest -u",
"type-check": "tsgo --noEmit",
"webhook:ngrok": "ngrok http http://localhost:3011",

View File

@@ -6,7 +6,7 @@
"scripts": {
"simple": "tsx examples/tools-calling.ts",
"test": "vitest",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {},
"devDependencies": {

View File

@@ -14,7 +14,7 @@
"main": "src/index.ts",
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage",
"test:coverage": "vitest --coverage --silent='passed-only'",
"test:update": "vitest -u"
},
"dependencies": {

View File

@@ -9,7 +9,7 @@
"scripts": {
"test": "npm run test:client-db && npm run test:server-db",
"test:client-db": "vitest run",
"test:coverage": "vitest --coverage --config vitest.config.server.mts",
"test:coverage": "vitest --coverage --silent='passed-only' --config vitest.config.server.mts",
"test:server-db": "vitest run --config vitest.config.server.mts"
},
"dependencies": {

View File

@@ -6,7 +6,7 @@
"types": "src/index.ts",
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"debug": "^4.3.4"

View File

@@ -22,7 +22,7 @@
"main": "./src/index.ts",
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"@langchain/community": "^0.3.41",

View File

@@ -71,9 +71,9 @@
},
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"zod": "^3.25.76"
}
}
}

View File

@@ -9,7 +9,7 @@
},
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage",
"test:coverage": "vitest --coverage --silent='passed-only'",
"test:update": "vitest -u"
},
"dependencies": {

View File

@@ -9,14 +9,15 @@ describe('createRouterRuntime', () => {
});
describe('initialization', () => {
it('should throw error when routers array is empty', () => {
expect(() => {
const Runtime = createRouterRuntime({
id: 'test-runtime',
routers: [],
});
new Runtime();
}).toThrow('empty providers');
it('should throw error when routers array is empty', async () => {
const Runtime = createRouterRuntime({
id: 'test-runtime',
routers: [],
});
const runtime = new Runtime();
// 现在错误在使用时才抛出,因为是延迟创建
await expect(runtime.getRuntimeByModel('test-model')).rejects.toThrow('empty providers');
});
it('should create UniformRuntime class with valid routers', () => {
@@ -44,7 +45,7 @@ describe('createRouterRuntime', () => {
expect(runtime).toBeDefined();
});
it('should merge router options with constructor options', () => {
it('should merge router options with constructor options', async () => {
const mockConstructor = vi.fn();
class MockRuntime implements LobeRuntimeAI {
@@ -52,6 +53,10 @@ describe('createRouterRuntime', () => {
mockConstructor(options);
}
chat = vi.fn();
textToImage = vi.fn();
models = vi.fn();
embeddings = vi.fn();
textToSpeech = vi.fn();
}
const Runtime = createRouterRuntime({
@@ -61,11 +66,15 @@ describe('createRouterRuntime', () => {
apiType: 'openai',
options: { baseURL: 'https://api.example.com' },
runtime: MockRuntime as any,
models: ['test-model'],
},
],
});
new Runtime({ apiKey: 'constructor-key' });
const runtime = new Runtime({ apiKey: 'constructor-key' });
// 触发 runtime 创建
await runtime.getRuntimeByModel('test-model');
expect(mockConstructor).toHaveBeenCalledWith(
expect.objectContaining({
@@ -77,10 +86,14 @@ describe('createRouterRuntime', () => {
});
});
describe('getModels', () => {
it('should return synchronous models array directly', async () => {
describe('model matching', () => {
it('should return correct runtime for matching model', async () => {
const mockRuntime = {
chat: vi.fn(),
textToImage: vi.fn(),
models: vi.fn(),
embeddings: vi.fn(),
textToSpeech: vi.fn(),
} as unknown as LobeRuntimeAI;
const Runtime = createRouterRuntime({
@@ -96,50 +109,54 @@ describe('createRouterRuntime', () => {
});
const runtime = new Runtime();
const models = await runtime['getRouterMatchModels']({
id: 'test',
models: ['model-1', 'model-2'],
runtime: mockRuntime,
});
const selectedRuntime = await runtime.getRuntimeByModel('model-1');
expect(models).toEqual(['model-1', 'model-2']);
expect(selectedRuntime).toBeDefined();
});
it('should call asynchronous models function', async () => {
it('should support dynamic routers with asynchronous model fetching', async () => {
const mockRuntime = {
chat: vi.fn(),
textToImage: vi.fn(),
models: vi.fn(),
embeddings: vi.fn(),
textToSpeech: vi.fn(),
} as unknown as LobeRuntimeAI;
const mockModelsFunction = vi.fn().mockResolvedValue(['async-model-1', 'async-model-2']);
const Runtime = createRouterRuntime({
id: 'test-runtime',
routers: [
{
apiType: 'openai',
options: {},
runtime: mockRuntime.constructor as any,
models: mockModelsFunction,
},
],
routers: async () => {
// 异步获取模型列表
const models = await mockModelsFunction();
return [
{
apiType: 'openai',
options: {},
runtime: mockRuntime.constructor as any,
models, // 静态数组
},
];
},
});
const runtime = new Runtime();
const runtimeItem = {
id: 'test',
models: mockModelsFunction,
runtime: mockRuntime,
};
// Call the function
const models = await runtime['getRouterMatchModels'](runtimeItem);
expect(models).toEqual(['async-model-1', 'async-model-2']);
expect(mockModelsFunction).toHaveBeenCalledTimes(1);
// 触发 routers 函数调用
const selectedRuntime = await runtime.getRuntimeByModel('async-model-1');
expect(selectedRuntime).toBeDefined();
expect(mockModelsFunction).toHaveBeenCalled();
});
it('should return empty array when models is undefined', async () => {
it('should return fallback runtime when model not found', async () => {
const mockRuntime = {
chat: vi.fn(),
textToImage: vi.fn(),
models: vi.fn(),
embeddings: vi.fn(),
textToSpeech: vi.fn(),
} as unknown as LobeRuntimeAI;
const Runtime = createRouterRuntime({
@@ -149,17 +166,15 @@ describe('createRouterRuntime', () => {
apiType: 'openai',
options: {},
runtime: mockRuntime.constructor as any,
models: ['known-model'],
},
],
});
const runtime = new Runtime();
const models = await runtime['getRouterMatchModels']({
id: 'test',
runtime: mockRuntime,
});
const selectedRuntime = await runtime.getRuntimeByModel('unknown-model');
expect(models).toEqual([]);
expect(selectedRuntime).toBeDefined();
});
});
@@ -194,10 +209,10 @@ describe('createRouterRuntime', () => {
const runtime = new Runtime();
const result1 = await runtime.getRuntimeByModel('gpt-4');
expect(result1).toBe(runtime['_runtimes'][0].runtime);
expect(result1).toBeInstanceOf(MockRuntime1);
const result2 = await runtime.getRuntimeByModel('claude-3');
expect(result2).toBe(runtime['_runtimes'][1].runtime);
expect(result2).toBeInstanceOf(MockRuntime2);
});
it('should return last runtime when no model matches', async () => {
@@ -230,7 +245,7 @@ describe('createRouterRuntime', () => {
const runtime = new Runtime();
const result = await runtime.getRuntimeByModel('unknown-model');
expect(result).toBe(runtime['_runtimes'][1].runtime);
expect(result).toBeInstanceOf(MockRuntime2);
});
});
@@ -277,6 +292,9 @@ describe('createRouterRuntime', () => {
const Runtime = createRouterRuntime({
id: 'test-runtime',
chatCompletion: {
handleError,
},
routers: [
{
apiType: 'openai',
@@ -287,11 +305,7 @@ describe('createRouterRuntime', () => {
],
});
const runtime = new Runtime({
chat: {
handleError,
},
});
const runtime = new Runtime();
await expect(
runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 }),
@@ -452,7 +466,7 @@ describe('createRouterRuntime', () => {
});
describe('dynamic routers configuration', () => {
it('should support function-based routers configuration', () => {
it('should support function-based routers configuration', async () => {
class MockRuntime implements LobeRuntimeAI {
chat = vi.fn();
textToImage = vi.fn();
@@ -461,7 +475,7 @@ describe('createRouterRuntime', () => {
textToSpeech = vi.fn();
}
const dynamicRoutersFunction = (options: any) => [
const dynamicRoutersFunction = vi.fn((options: any) => [
{
apiType: 'openai' as const,
options: {
@@ -478,7 +492,7 @@ describe('createRouterRuntime', () => {
runtime: MockRuntime as any,
models: ['claude-3'],
},
];
]);
const Runtime = createRouterRuntime({
id: 'test-runtime',
@@ -493,21 +507,32 @@ describe('createRouterRuntime', () => {
const runtime = new Runtime(userOptions);
expect(runtime).toBeDefined();
expect(runtime['_runtimes']).toHaveLength(2);
expect(runtime['_runtimes'][0].id).toBe('openai');
expect(runtime['_runtimes'][1].id).toBe('anthropic');
// 测试动态 routers 是否能正确工作
const result = await runtime.getRuntimeByModel('gpt-4');
expect(result).toBeDefined();
// 验证动态函数被调用时传入了正确的参数
expect(dynamicRoutersFunction).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: 'test-key',
baseURL: 'https://yourapi.cn',
}),
{ model: 'gpt-4' },
);
});
it('should throw error when dynamic routers function returns empty array', () => {
it('should throw error when dynamic routers function returns empty array', async () => {
const emptyRoutersFunction = () => [];
expect(() => {
const Runtime = createRouterRuntime({
id: 'test-runtime',
routers: emptyRoutersFunction,
});
new Runtime();
}).toThrow('empty providers');
const Runtime = createRouterRuntime({
id: 'test-runtime',
routers: emptyRoutersFunction,
});
const runtime = new Runtime();
// 现在错误在使用时才抛出,因为是延迟创建
await expect(runtime.getRuntimeByModel('test-model')).rejects.toThrow('empty providers');
});
});
});

View File

@@ -25,7 +25,7 @@ import { baseRuntimeMap } from './baseRuntimeMap';
export interface RuntimeItem {
id: string;
models?: string[] | (() => Promise<string[]>);
models?: string[];
runtime: LobeRuntimeAI;
}
@@ -45,13 +45,22 @@ export type RuntimeClass = typeof LobeOpenAI;
interface RouterInstance {
apiType: keyof typeof baseRuntimeMap;
models?: string[] | (() => Promise<string[]>);
models?: string[];
options: ProviderIniOptions;
runtime?: RuntimeClass;
}
type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
type Routers =
| RouterInstance[]
| ((
options: ClientOptions & Record<string, any>,
runtimeContext: {
model?: string;
},
) => RouterInstance[] | Promise<RouterInstance[]>);
interface CreateRouterRuntimeOptions<T extends Record<string, any> = any> {
apiKey?: string;
chatCompletion?: {
@@ -104,77 +113,82 @@ interface CreateRouterRuntimeOptions<T extends Record<string, any> = any> {
options: ConstructorOptions<T>,
) => ChatStreamPayload;
};
routers: RouterInstance[] | ((options: ClientOptions & Record<string, any>) => RouterInstance[]);
routers: Routers;
}
export const createRouterRuntime = ({
id,
routers,
apiKey: DEFAULT_API_LEY,
models,
models: modelsOption,
...params
}: CreateRouterRuntimeOptions) => {
return class UniformRuntime implements LobeRuntimeAI {
private _runtimes: RuntimeItem[];
private _options: ClientOptions & Record<string, any>;
private _routers: Routers;
private _params: any;
private _id: string;
constructor(options: ClientOptions & Record<string, any> = {}) {
const _options = {
this._options = {
...options,
apiKey: options.apiKey?.trim() || DEFAULT_API_LEY,
baseURL: options.baseURL?.trim(),
};
// 支持动态 routers 配置
const resolvedRouters = typeof routers === 'function' ? routers(_options) : routers;
// 保存配置但不创建 runtimes
this._routers = routers;
this._params = params;
this._id = id;
}
/**
* TODO: routers 如果是静态对象,可以提前生成 runtimes, 避免运行时生成开销
*/
private async createRuntimesByRouters(model?: string): Promise<RuntimeItem[]> {
// 动态获取 routers支持传入 model
const resolvedRouters =
typeof this._routers === 'function'
? await this._routers(this._options, { model })
: this._routers;
if (resolvedRouters.length === 0) {
throw new Error('empty providers');
}
this._runtimes = resolvedRouters.map((router) => {
return resolvedRouters.map((router) => {
const providerAI = router.runtime ?? baseRuntimeMap[router.apiType] ?? LobeOpenAI;
const finalOptions = { ...this._params, ...this._options, ...router.options };
const runtime: LobeRuntimeAI = new providerAI({ ...finalOptions, id: this._id });
const finalOptions = { ...params, ...options, ...router.options };
// @ts-ignore
const runtime: LobeRuntimeAI = new providerAI({ ...finalOptions, id });
return { id: router.apiType, models: router.models, runtime };
return {
id: router.apiType,
models: router.models,
runtime,
};
});
this._options = _options;
}
// Get runtime's models list, supporting both synchronous arrays and asynchronous functions
private async getRouterMatchModels(runtimeItem: RuntimeItem): Promise<string[]> {
// If it's a synchronous array, return directly
if (typeof runtimeItem.models !== 'function') {
return runtimeItem.models || [];
}
// Get model list
return await runtimeItem.models();
}
// Check if it can match a specific model, otherwise default to using the last runtime
async getRuntimeByModel(model: string) {
for (const runtimeItem of this._runtimes) {
const models = await this.getRouterMatchModels(runtimeItem);
const runtimes = await this.createRuntimesByRouters(model);
for (const runtimeItem of runtimes) {
const models = runtimeItem.models || [];
if (models.includes(model)) {
return runtimeItem.runtime;
}
}
return this._runtimes.at(-1)!.runtime;
return runtimes.at(-1)!.runtime;
}
async chat(payload: ChatStreamPayload, options?: ChatMethodOptions) {
try {
const runtime = await this.getRuntimeByModel(payload.model);
return await runtime.chat!(payload, options);
} catch (e) {
if (this._options.chat?.handleError) {
const error = this._options.chat.handleError(e);
if (params.chatCompletion?.handleError) {
const error = params.chatCompletion.handleError(e, this._options);
if (error) {
throw error;
@@ -192,20 +206,24 @@ export const createRouterRuntime = ({
async textToImage(payload: TextToImagePayload) {
const runtime = await this.getRuntimeByModel(payload.model);
return runtime.textToImage!(payload);
}
async models() {
if (models && typeof models === 'function') {
// If it's function-style configuration, use the last runtime's client to call the function
const lastRuntime = this._runtimes.at(-1)?.runtime;
if (modelsOption && typeof modelsOption === 'function') {
// 延迟创建 runtimes
const runtimes = await this.createRuntimesByRouters();
// 如果是函数式配置,使用最后一个运行时的客户端来调用函数
const lastRuntime = runtimes.at(-1)?.runtime;
if (lastRuntime && 'client' in lastRuntime) {
const modelList = await models({ client: (lastRuntime as any).client });
const modelList = await modelsOption({ client: (lastRuntime as any).client });
return await postProcessModelList(modelList);
}
}
return this._runtimes.at(-1)?.runtime.models?.();
// 延迟创建 runtimes
const runtimes = await this.createRuntimesByRouters();
return runtimes.at(-1)?.runtime.models?.();
}
async embeddings(payload: EmbeddingsPayload, options?: EmbeddingsOptions) {

View File

@@ -5,7 +5,7 @@
"main": "./src/index.ts",
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"@lobechat/types": "workspace:*"

View File

@@ -9,7 +9,7 @@
},
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"@lobechat/const": "workspace:*",

View File

@@ -6,7 +6,7 @@
"types": "src/index.ts",
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"@mozilla/readability": "^0.6.0",

View File

@@ -2,16 +2,7 @@ import { EdgeConfigClient, createClient } from '@vercel/edge-config';
import { appEnv } from '@/envs/app';
const EdgeConfigKeys = {
/**
* Assistant whitelist
*/
AssistantBlacklist: 'assistant_blacklist',
/**
* Assistant whitelist
*/
AssistantWhitelist: 'assistant_whitelist',
};
import { EdgeConfigData } from './types';
export class EdgeConfig {
get client(): EdgeConfigClient {
@@ -30,14 +21,7 @@ export class EdgeConfig {
getAgentRestrictions = async () => {
const { assistant_blacklist: blacklist, assistant_whitelist: whitelist } =
await this.client.getAll([
EdgeConfigKeys.AssistantWhitelist,
EdgeConfigKeys.AssistantBlacklist,
]);
return { blacklist, whitelist } as {
blacklist: string[] | undefined;
whitelist: string[] | undefined;
};
await this.client.getAll<EdgeConfigData>(['assistant_whitelist', 'assistant_blacklist']);
return { blacklist, whitelist };
};
}

View File

@@ -0,0 +1,9 @@
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
/**
* EdgeConfig 完整配置类型
*/
export interface EdgeConfigData {
assistant_blacklist?: string[];
assistant_whitelist?: string[];
}