mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-16 20:46:08 +00:00
✨ feat: support new ai provider in client pglite (#5488)
* update to pglite mode * add service * 新增 DevPanel * 新增数据库预览 UI * Update useCategory.tsx * add postgres table viewer * improve table detail * fix * fix list * fix custom provider in client mode * fix build * fix tests * fix url * add test for service
This commit is contained in:
+4
-3
@@ -27,11 +27,11 @@
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"build:analyze": "ANALYZE=true next build",
|
||||
"build:docker": "DOCKER=true next build && npm run build-sitemap",
|
||||
"postbuild": "npm run build-sitemap && npm run build-migrate-db",
|
||||
"build-migrate-db": "bun run db:migrate",
|
||||
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
||||
"build:analyze": "ANALYZE=true next build",
|
||||
"build:docker": "DOCKER=true next build && npm run build-sitemap",
|
||||
"db:generate": "drizzle-kit generate && npm run db:generate-client",
|
||||
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
|
||||
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
|
||||
@@ -59,11 +59,11 @@
|
||||
"start": "next start -p 3210",
|
||||
"stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
|
||||
"test": "npm run test-app && npm run test-server",
|
||||
"test:update": "vitest -u",
|
||||
"test-app": "vitest run --config vitest.config.ts",
|
||||
"test-app:coverage": "vitest run --config vitest.config.ts --coverage",
|
||||
"test-server": "vitest run --config vitest.server.config.ts",
|
||||
"test-server:coverage": "vitest run --config vitest.server.config.ts --coverage",
|
||||
"test:update": "vitest -u",
|
||||
"type-check": "tsc --noEmit",
|
||||
"webhook:ngrok": "ngrok http http://localhost:3011",
|
||||
"workflow:cdn": "tsx ./scripts/cdnWorkflow/index.ts",
|
||||
@@ -206,6 +206,7 @@
|
||||
"react-layout-kit": "^1.9.1",
|
||||
"react-lazy-load": "^4.0.1",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-rnd": "^10.4.14",
|
||||
"react-scan": "^0.0.54",
|
||||
"react-virtuoso": "^4.12.3",
|
||||
"react-wrap-balancer": "^1.1.1",
|
||||
|
||||
@@ -10,7 +10,7 @@ import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FlexboxProps } from 'react-layout-kit';
|
||||
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { DiscoverProviderItem } from '@/types/discover';
|
||||
|
||||
const useStyles = createStyles(({ css }) => ({
|
||||
@@ -32,7 +32,7 @@ const ProviderConfig = memo<ProviderConfigProps>(({ data, identifier }) => {
|
||||
|
||||
const router = useRouter();
|
||||
const openSettings = () => {
|
||||
router.push(!isServerMode ? '/settings/llm' : `/settings/provider/${identifier}`);
|
||||
router.push(isDeprecatedEdition ? '/settings/llm' : `/settings/provider/${identifier}`);
|
||||
};
|
||||
|
||||
const icon = <Icon icon={SquareArrowOutUpRight} size={{ fontSize: 16 }} />;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import type { MenuProps } from '@/components/Menu';
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { SettingsTabs } from '@/store/global/initialState';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
|
||||
@@ -53,7 +53,7 @@ export const useCategory = () => {
|
||||
},
|
||||
showLLM &&
|
||||
// TODO: Remove /llm when v2.0
|
||||
!isServerMode
|
||||
(isDeprecatedEdition
|
||||
? {
|
||||
icon: <Icon icon={Brain} />,
|
||||
key: SettingsTabs.LLM,
|
||||
@@ -71,7 +71,7 @@ export const useCategory = () => {
|
||||
{t('tab.provider')}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
}),
|
||||
|
||||
enableSTT && {
|
||||
icon: <Icon icon={Mic2} />,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { aiProviderService } from '@/services/aiProvider';
|
||||
|
||||
import ModelList from '../../features/ModelList';
|
||||
import ProviderConfig from '../../features/ProviderConfig';
|
||||
|
||||
const ClientMode = memo<{ id: string }>(({ id }) => {
|
||||
const { data, isLoading } = useSWR('get-client-provider', () =>
|
||||
aiProviderService.getAiProviderById(id),
|
||||
);
|
||||
|
||||
return (
|
||||
<Flexbox gap={24} paddingBlock={8}>
|
||||
{!isLoading && data && <ProviderConfig {...data} />}
|
||||
<ModelList id={id} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default ClientMode;
|
||||
@@ -8,6 +8,7 @@ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
import { PagePropsWithId } from '@/types/next';
|
||||
import { getUserAuth } from '@/utils/server/auth';
|
||||
|
||||
import ClientMode from './ClientMode';
|
||||
import ProviderDetail from './index';
|
||||
|
||||
const Page = async (props: PagePropsWithId) => {
|
||||
@@ -33,7 +34,7 @@ const Page = async (props: PagePropsWithId) => {
|
||||
return <ProviderDetail {...userCard} />;
|
||||
}
|
||||
|
||||
return <div>not found</div>;
|
||||
return <ClientMode id={params.id} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -72,7 +72,6 @@ const ConfigGroupModal = memo<ConfigGroupModalProps>(({ open, onCancel, defaultI
|
||||
id: item.id,
|
||||
sort: index,
|
||||
}));
|
||||
console.log(sortMap);
|
||||
setLoading(true);
|
||||
await updateAiProviderSort(sortMap);
|
||||
setLoading(false);
|
||||
|
||||
@@ -282,5 +282,16 @@
|
||||
"bps": true,
|
||||
"folderMillis": 1731858381716,
|
||||
"hash": "d8263bfefe296ed366379c7b7fc65195d12e6a1c0a9f1c96097ea28f2123fe50"
|
||||
},
|
||||
{
|
||||
"sql": [
|
||||
"CREATE TABLE \"ai_models\" (\n\t\"id\" varchar(150) NOT NULL,\n\t\"display_name\" varchar(200),\n\t\"description\" text,\n\t\"organization\" varchar(100),\n\t\"enabled\" boolean,\n\t\"provider_id\" varchar(64) NOT NULL,\n\t\"type\" varchar(20) DEFAULT 'chat' NOT NULL,\n\t\"sort\" integer,\n\t\"user_id\" text NOT NULL,\n\t\"pricing\" jsonb,\n\t\"parameters\" jsonb DEFAULT '{}'::jsonb,\n\t\"config\" jsonb,\n\t\"abilities\" jsonb DEFAULT '{}'::jsonb,\n\t\"context_window_tokens\" integer,\n\t\"source\" varchar(20),\n\t\"released_at\" varchar(10),\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"ai_models_id_provider_id_user_id_pk\" PRIMARY KEY(\"id\",\"provider_id\",\"user_id\")\n);\n",
|
||||
"\nCREATE TABLE \"ai_providers\" (\n\t\"id\" varchar(64) NOT NULL,\n\t\"name\" text,\n\t\"user_id\" text NOT NULL,\n\t\"sort\" integer,\n\t\"enabled\" boolean,\n\t\"fetch_on_client\" boolean,\n\t\"check_model\" text,\n\t\"logo\" text,\n\t\"description\" text,\n\t\"key_vaults\" text,\n\t\"source\" varchar(20),\n\t\"settings\" jsonb,\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"ai_providers_id_user_id_pk\" PRIMARY KEY(\"id\",\"user_id\")\n);\n",
|
||||
"\nALTER TABLE \"ai_models\" ADD CONSTRAINT \"ai_models_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;",
|
||||
"\nALTER TABLE \"ai_providers\" ADD CONSTRAINT \"ai_providers_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;"
|
||||
],
|
||||
"bps": true,
|
||||
"folderMillis": 1735834653361,
|
||||
"hash": "845a692ceabbfc3caf252a97d3e19a213bc0c433df2689900135f9cfded2cf49"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { clientDB, initializeDB } from '@/database/client/db';
|
||||
|
||||
import { TableViewerRepo } from './index';
|
||||
|
||||
const userId = 'user-table-viewer';
|
||||
const repo = new TableViewerRepo(clientDB as any, userId);
|
||||
|
||||
// Mock database execution
|
||||
const mockExecute = vi.fn();
|
||||
const mockDB = {
|
||||
execute: mockExecute,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await initializeDB();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('TableViewerRepo', () => {
|
||||
describe('getAllTables', () => {
|
||||
it('should return all tables with counts', async () => {
|
||||
const result = await repo.getAllTables();
|
||||
|
||||
expect(result.length).toEqual(39);
|
||||
expect(result[0]).toEqual({ name: 'agents', count: 0, type: 'BASE TABLE' });
|
||||
});
|
||||
|
||||
it('should handle custom schema', async () => {
|
||||
const result = await repo.getAllTables('custom_schema');
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTableDetails', () => {
|
||||
it('should return table column details', async () => {
|
||||
const tableName = 'test_table';
|
||||
const mockColumns = {
|
||||
rows: [
|
||||
{
|
||||
column_name: 'id',
|
||||
data_type: 'uuid',
|
||||
is_nullable: 'NO',
|
||||
column_default: null,
|
||||
is_primary_key: true,
|
||||
foreign_key: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
mockExecute.mockResolvedValueOnce(mockColumns);
|
||||
|
||||
const result = await testRepo.getTableDetails(tableName);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
nullable: false,
|
||||
defaultValue: null,
|
||||
isPrimaryKey: true,
|
||||
foreignKey: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTableData', () => {
|
||||
it('should return paginated data with filters', async () => {
|
||||
const tableName = 'test_table';
|
||||
const pagination = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
sortBy: 'id',
|
||||
sortOrder: 'desc' as const,
|
||||
};
|
||||
const filters = [
|
||||
{
|
||||
column: 'name',
|
||||
operator: 'contains' as const,
|
||||
value: 'test',
|
||||
},
|
||||
];
|
||||
|
||||
const mockData = {
|
||||
rows: [{ id: 1, name: 'test' }],
|
||||
};
|
||||
const mockCount = {
|
||||
rows: [{ total: 1 }],
|
||||
};
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
|
||||
|
||||
const result = await testRepo.getTableData(tableName, pagination, filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: mockData.rows,
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRow', () => {
|
||||
it('should update and return row data', async () => {
|
||||
const tableName = 'test_table';
|
||||
const id = '123';
|
||||
const primaryKeyColumn = 'id';
|
||||
const data = { name: 'updated' };
|
||||
|
||||
const mockResult = {
|
||||
rows: [{ id: '123', name: 'updated' }],
|
||||
};
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
mockExecute.mockResolvedValueOnce(mockResult);
|
||||
|
||||
const result = await testRepo.updateRow(tableName, id, primaryKeyColumn, data);
|
||||
|
||||
expect(result).toEqual(mockResult.rows[0]);
|
||||
expect(mockExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteRow', () => {
|
||||
it('should delete a row', async () => {
|
||||
const tableName = 'test_table';
|
||||
const id = '123';
|
||||
const primaryKeyColumn = 'id';
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
mockExecute.mockResolvedValueOnce({ rows: [] });
|
||||
|
||||
await testRepo.deleteRow(tableName, id, primaryKeyColumn);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertRow', () => {
|
||||
it('should insert and return new row data', async () => {
|
||||
const tableName = 'test_table';
|
||||
const data = { name: 'new row' };
|
||||
|
||||
const mockResult = {
|
||||
rows: [{ id: '123', name: 'new row' }],
|
||||
};
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
mockExecute.mockResolvedValueOnce(mockResult);
|
||||
|
||||
const result = await testRepo.insertRow(tableName, data);
|
||||
|
||||
expect(result).toEqual(mockResult.rows[0]);
|
||||
expect(mockExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTableCount', () => {
|
||||
it('should return table count', async () => {
|
||||
const tableName = 'test_table';
|
||||
const mockResult = {
|
||||
rows: [{ total: 42 }],
|
||||
};
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
mockExecute.mockResolvedValueOnce(mockResult);
|
||||
|
||||
const result = await testRepo.getTableCount(tableName);
|
||||
|
||||
expect(result).toBe(42);
|
||||
expect(mockExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchDelete', () => {
|
||||
it('should delete multiple rows', async () => {
|
||||
const tableName = 'test_table';
|
||||
const ids = ['1', '2', '3'];
|
||||
const primaryKeyColumn = 'id';
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
mockExecute.mockResolvedValueOnce({ rows: [] });
|
||||
|
||||
await testRepo.batchDelete(tableName, ids, primaryKeyColumn);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportTableData', () => {
|
||||
it('should export table data with default pagination', async () => {
|
||||
const tableName = 'test_table';
|
||||
const mockData = {
|
||||
rows: [{ id: 1, name: 'test' }],
|
||||
};
|
||||
const mockCount = {
|
||||
rows: [{ total: 1 }],
|
||||
};
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
|
||||
|
||||
const result = await testRepo.exportTableData(tableName);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: mockData.rows,
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should export table data with custom pagination and filters', async () => {
|
||||
const tableName = 'test_table';
|
||||
const pagination = { page: 2, pageSize: 50 };
|
||||
const filters = [
|
||||
{
|
||||
column: 'status',
|
||||
operator: 'equals' as const,
|
||||
value: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
const mockData = {
|
||||
rows: [{ id: 1, status: 'active' }],
|
||||
};
|
||||
const mockCount = {
|
||||
rows: [{ total: 1 }],
|
||||
};
|
||||
|
||||
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
|
||||
|
||||
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
||||
|
||||
const result = await testRepo.exportTableData(tableName, pagination, filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: mockData.rows,
|
||||
pagination: {
|
||||
page: 2,
|
||||
pageSize: 50,
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import pMap from 'p-map';
|
||||
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
import {
|
||||
FilterCondition,
|
||||
PaginationParams,
|
||||
TableBasicInfo,
|
||||
TableColumnInfo,
|
||||
} from '@/types/tableViewer';
|
||||
|
||||
export class TableViewerRepo {
|
||||
private userId: string;
|
||||
private db: LobeChatDatabase;
|
||||
|
||||
constructor(db: LobeChatDatabase, userId: string) {
|
||||
this.userId = userId;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库中所有的表
|
||||
*/
|
||||
async getAllTables(schema = 'public'): Promise<TableBasicInfo[]> {
|
||||
const query = sql`
|
||||
SELECT
|
||||
table_name as name,
|
||||
table_type as type
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = ${schema}
|
||||
ORDER BY table_name;
|
||||
`;
|
||||
|
||||
const tables = await this.db.execute(query);
|
||||
|
||||
const tableNames = tables.rows.map((row) => row.name) as string[];
|
||||
|
||||
const counts = await pMap(tableNames, async (name) => this.getTableCount(name), {
|
||||
concurrency: 10,
|
||||
});
|
||||
|
||||
return tables.rows.map((row, index) => ({
|
||||
count: counts[index],
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
})) as TableBasicInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定表的详细结构信息
|
||||
*/
|
||||
async getTableDetails(tableName: string): Promise<TableColumnInfo[]> {
|
||||
const query = sql`
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
-- 主键信息
|
||||
(
|
||||
SELECT true
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_name = c.table_name
|
||||
AND kcu.column_name = c.column_name
|
||||
AND tc.constraint_type = 'PRIMARY KEY'
|
||||
) is_primary_key,
|
||||
-- 外键信息
|
||||
(
|
||||
SELECT json_build_object(
|
||||
'table', ccu.table_name,
|
||||
'column', ccu.column_name
|
||||
)
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
WHERE tc.table_name = c.table_name
|
||||
AND kcu.column_name = c.column_name
|
||||
AND tc.constraint_type = 'FOREIGN KEY'
|
||||
) foreign_key
|
||||
FROM information_schema.columns c
|
||||
WHERE c.table_name = ${tableName}
|
||||
AND c.table_schema = 'public'
|
||||
ORDER BY c.ordinal_position;
|
||||
`;
|
||||
|
||||
const columns = await this.db.execute(query);
|
||||
|
||||
return columns.rows.map((col: any) => ({
|
||||
defaultValue: col.column_default,
|
||||
foreignKey: col.foreign_key,
|
||||
isPrimaryKey: !!col.is_primary_key,
|
||||
name: col.column_name,
|
||||
nullable: col.is_nullable === 'YES',
|
||||
type: col.data_type,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表数据,支持分页、排序和筛选
|
||||
*/
|
||||
async getTableData(tableName: string, pagination: PaginationParams, filters?: FilterCondition[]) {
|
||||
const offset = (pagination.page - 1) * pagination.pageSize;
|
||||
|
||||
// 构建基础查询
|
||||
let baseQuery = sql`SELECT * FROM ${sql.identifier(tableName)}`;
|
||||
|
||||
// 添加筛选条件
|
||||
if (filters && filters.length > 0) {
|
||||
const whereConditions = filters.map((filter) => {
|
||||
const column = sql.identifier(filter.column);
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'equals': {
|
||||
return sql`${column} = ${filter.value}`;
|
||||
}
|
||||
case 'contains': {
|
||||
return sql`${column} ILIKE ${`%${filter.value}%`}`;
|
||||
}
|
||||
case 'startsWith': {
|
||||
return sql`${column} ILIKE ${`${filter.value}%`}`;
|
||||
}
|
||||
case 'endsWith': {
|
||||
return sql`${column} ILIKE ${`%${filter.value}`}`;
|
||||
}
|
||||
default: {
|
||||
return sql`1=1`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
baseQuery = sql`${baseQuery} WHERE ${sql.join(whereConditions, sql` AND `)}`;
|
||||
}
|
||||
|
||||
// 添加排序
|
||||
if (pagination.sortBy) {
|
||||
const direction = pagination.sortOrder === 'desc' ? sql`DESC` : sql`ASC`;
|
||||
baseQuery = sql`${baseQuery} ORDER BY ${sql.identifier(pagination.sortBy)} ${direction}`;
|
||||
}
|
||||
|
||||
// 添加分页
|
||||
const query = sql`${baseQuery} LIMIT ${pagination.pageSize} OFFSET ${offset}`;
|
||||
|
||||
// 获取总数
|
||||
const countQuery = sql`SELECT COUNT(*) as total FROM ${sql.identifier(tableName)}`;
|
||||
|
||||
// 并行执行查询
|
||||
const [data, count] = await Promise.all([this.db.execute(query), this.db.execute(countQuery)]);
|
||||
|
||||
return {
|
||||
data: data.rows,
|
||||
pagination: {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
total: Number(count.rows[0].total),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新表中的一行数据
|
||||
*/
|
||||
async updateRow(
|
||||
tableName: string,
|
||||
id: string,
|
||||
primaryKeyColumn: string,
|
||||
data: Record<string, any>,
|
||||
) {
|
||||
const setColumns = Object.entries(data).map(([key, value]) => {
|
||||
return sql`${sql.identifier(key)} = ${value}`;
|
||||
});
|
||||
|
||||
const query = sql`
|
||||
UPDATE ${sql.identifier(tableName)}
|
||||
SET ${sql.join(setColumns, sql`, `)}
|
||||
WHERE ${sql.identifier(primaryKeyColumn)} = ${id}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.db.execute(query);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除表中的一行数据
|
||||
*/
|
||||
async deleteRow(tableName: string, id: string, primaryKeyColumn: string) {
|
||||
const query = sql`
|
||||
DELETE FROM ${sql.identifier(tableName)}
|
||||
WHERE ${sql.identifier(primaryKeyColumn)} = ${id}
|
||||
`;
|
||||
|
||||
await this.db.execute(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入新行数据
|
||||
*/
|
||||
async insertRow(tableName: string, data: Record<string, any>) {
|
||||
const columns = Object.keys(data).map((key) => sql.identifier(key));
|
||||
const values = Object.values(data);
|
||||
|
||||
const query = sql`
|
||||
INSERT INTO ${sql.identifier(tableName)}
|
||||
(${sql.join(columns, sql`, `)})
|
||||
VALUES (${sql.join(
|
||||
values.map((v) => sql`${v}`),
|
||||
sql`, `,
|
||||
)})
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.db.execute(query);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表的总记录数
|
||||
*/
|
||||
async getTableCount(tableName: string): Promise<number> {
|
||||
const query = sql`SELECT COUNT(*) as total FROM ${sql.identifier(tableName)}`;
|
||||
const result = await this.db.execute(query);
|
||||
return Number(result.rows[0].total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除数据
|
||||
*/
|
||||
async batchDelete(tableName: string, ids: string[], primaryKeyColumn: string) {
|
||||
const query = sql`
|
||||
DELETE FROM ${sql.identifier(tableName)}
|
||||
WHERE ${sql.identifier(primaryKeyColumn)} = ANY(${ids})
|
||||
`;
|
||||
|
||||
await this.db.execute(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出表数据(支持分页导出)
|
||||
*/
|
||||
async exportTableData(
|
||||
tableName: string,
|
||||
pagination?: PaginationParams,
|
||||
filters?: FilterCondition[],
|
||||
) {
|
||||
return this.getTableData(tableName, pagination || { page: 1, pageSize: 1000 }, filters);
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ export class AiProviderModel {
|
||||
|
||||
getAiProviderById = async (
|
||||
id: string,
|
||||
decryptor: DecryptUserKeyVaults,
|
||||
decryptor?: DecryptUserKeyVaults,
|
||||
): Promise<AiProviderDetailItem | undefined> => {
|
||||
const query = this.db
|
||||
.select({
|
||||
@@ -205,7 +205,7 @@ export class AiProviderModel {
|
||||
return { ...result, keyVaults } as AiProviderDetailItem;
|
||||
};
|
||||
|
||||
getAiProviderRuntimeConfig = async (decryptor: DecryptUserKeyVaults) => {
|
||||
getAiProviderRuntimeConfig = async (decryptor?: DecryptUserKeyVaults) => {
|
||||
const result = await this.db
|
||||
.select({
|
||||
fetchOnClient: aiProviders.fetchOnClient,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { App, FloatButton, Spin } from 'antd';
|
||||
import { DatabaseIcon, Loader2 } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { debugService } from '@/services/debug';
|
||||
|
||||
const DebugUI = memo(() => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { message } = App.useApp();
|
||||
return (
|
||||
<>
|
||||
{loading && <Spin fullscreen />}
|
||||
<FloatButton
|
||||
icon={<Icon icon={loading ? Loader2 : DatabaseIcon} spin={loading} />}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await debugService.insertLargeDataToDB();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
setLoading(false);
|
||||
message.success(`插入成功,耗时:${(duration / 1000).toFixed(1)} s`);
|
||||
}}
|
||||
tooltip={'性能压测,插入100w数据'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default DebugUI;
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { getDebugConfig } from '@/config/debug';
|
||||
|
||||
let DebugUI: FC = () => null;
|
||||
|
||||
// we need use Constant Folding to remove code below in production
|
||||
// refs: https://webpack.js.org/plugins/internal-plugins/#constplugin
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// eslint-disable-next-line unicorn/no-lonely-if
|
||||
if (getDebugConfig().DEBUG_MODE) {
|
||||
// @ts-ignore
|
||||
DebugUI = dynamic(() => import('./Content'), { ssr: false });
|
||||
}
|
||||
}
|
||||
|
||||
export default DebugUI;
|
||||
@@ -0,0 +1,136 @@
|
||||
import { ActionIcon, Icon } from '@lobehub/ui';
|
||||
import { FloatButton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { BugIcon, BugOff, XIcon } from 'lucide-react';
|
||||
import React, { PropsWithChildren, useEffect, useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Rnd } from 'react-rnd';
|
||||
|
||||
// 定义样式
|
||||
const useStyles = createStyles(({ token, css }) => {
|
||||
return {
|
||||
collapsed: css`
|
||||
pointer-events: none;
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
`,
|
||||
content: css`
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
color: ${token.colorText};
|
||||
`,
|
||||
|
||||
expanded: css`
|
||||
pointer-events: auto;
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
`,
|
||||
|
||||
header: css`
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
|
||||
padding-block: 8px;
|
||||
padding-inline: 16px;
|
||||
border-block-end: 1px solid ${token.colorBorderSecondary};
|
||||
border-start-start-radius: 12px;
|
||||
border-start-end-radius: 12px;
|
||||
|
||||
font-weight: ${token.fontWeightStrong};
|
||||
color: ${token.colorText};
|
||||
|
||||
background: ${token.colorFillAlter};
|
||||
`,
|
||||
panel: css`
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
|
||||
display: flex;
|
||||
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
background: ${token.colorBgContainer};
|
||||
box-shadow: ${token.boxShadow};
|
||||
|
||||
transition: opacity ${token.motionDurationMid} ${token.motionEaseInOut};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const minWidth = 800;
|
||||
const minHeight = 600;
|
||||
|
||||
const CollapsibleFloatPanel = ({ children }: PropsWithChildren) => {
|
||||
const { styles } = useStyles();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [position, setPosition] = useState({ x: 100, y: 100 });
|
||||
const [size, setSize] = useState({ height: minHeight, width: minWidth });
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const localStoragePosition = localStorage.getItem('debug-panel-position');
|
||||
if (localStoragePosition && JSON.parse(localStoragePosition)) {
|
||||
setPosition(JSON.parse(localStoragePosition));
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
try {
|
||||
const localStorageSize = localStorage.getItem('debug-panel-size');
|
||||
if (localStorageSize && JSON.parse(localStorageSize)) {
|
||||
setSize(JSON.parse(localStorageSize));
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FloatButton
|
||||
icon={<Icon icon={isExpanded ? BugOff : BugIcon} />}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
style={{ bottom: 24, right: 24 }}
|
||||
/>
|
||||
{isExpanded && (
|
||||
<Rnd
|
||||
bounds="window"
|
||||
className={`${styles.panel} ${isExpanded ? styles.expanded : styles.collapsed}`}
|
||||
dragHandleClassName="panel-drag-handle"
|
||||
minHeight={minHeight}
|
||||
minWidth={minWidth}
|
||||
onDragStop={(e, d) => {
|
||||
setPosition({ x: d.x, y: d.y });
|
||||
}}
|
||||
onResizeStop={(e, direction, ref, delta, position) => {
|
||||
setSize({
|
||||
height: Number(ref.style.height),
|
||||
width: Number(ref.style.width),
|
||||
});
|
||||
setPosition(position);
|
||||
}}
|
||||
position={position}
|
||||
size={size}
|
||||
>
|
||||
<Flexbox height={'100%'}>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={`panel-drag-handle ${styles.header}`}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
>
|
||||
开发者面板
|
||||
<ActionIcon icon={XIcon} onClick={() => setIsExpanded(false)} />
|
||||
</Flexbox>
|
||||
<Flexbox className={styles.content}>{children}</Flexbox>
|
||||
</Flexbox>
|
||||
</Rnd>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollapsibleFloatPanel;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import React from 'react';
|
||||
import { Center } from 'react-layout-kit';
|
||||
import { TableVirtuoso } from 'react-virtuoso';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { tableViewerService } from '@/services/tableViewer';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
|
||||
import { useTableColumns } from '../useTableColumns';
|
||||
import TableCell from './TableCell';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
columnList: css`
|
||||
margin-inline-start: 32px;
|
||||
font-size: ${token.fontSizeSM}px;
|
||||
color: ${token.colorTextSecondary};
|
||||
|
||||
> div {
|
||||
padding-block: ${token.paddingXS}px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
`,
|
||||
table: css`
|
||||
overflow: scroll hidden;
|
||||
flex: 1;
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-family: ${token.fontFamilyCode};
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
thead {
|
||||
tr {
|
||||
outline: 1px solid ${token.colorBorderSecondary};
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
border-right: 1px solid ${token.colorBorderSecondary};
|
||||
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
inset-block-start: 0;
|
||||
|
||||
border-block-end: 1px solid ${token.colorBorderSecondary};
|
||||
|
||||
font-weight: ${token.fontWeightStrong};
|
||||
text-align: start;
|
||||
text-wrap: nowrap;
|
||||
|
||||
background: ${token.colorBgElevated};
|
||||
}
|
||||
|
||||
td {
|
||||
border-block-end: 1px solid ${token.colorBorderSecondary};
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr:hover {
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
}
|
||||
`,
|
||||
tableItem: css`
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: ${token.padding}px;
|
||||
align-items: center;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: ${token.borderRadius}px;
|
||||
|
||||
color: ${token.colorText};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface TableProps {
|
||||
tableName?: string;
|
||||
}
|
||||
|
||||
const Table = ({ tableName }: TableProps) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
const tableColumns = useTableColumns(tableName);
|
||||
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
||||
|
||||
const tableData = useSWR(
|
||||
isDBInited && tableName ? ['fetch-table-data', tableName] : null,
|
||||
([, table]) => tableViewerService.getTableData(table),
|
||||
);
|
||||
|
||||
const columns = tableColumns.data?.map((t) => t.name) || [];
|
||||
const isLoading = tableColumns.isLoading || tableData.isLoading;
|
||||
|
||||
if (!tableName) return <Center height={'80%'}>Select a table to view data</Center>;
|
||||
|
||||
if (isLoading) return <Center height={'80%'}>Loading...</Center>;
|
||||
|
||||
const dataSource = tableData.data?.data || [];
|
||||
const header = (
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th key={column}>{column}</th>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
{dataSource.length === 0 ? (
|
||||
<>
|
||||
<table>
|
||||
<thead>{header}</thead>
|
||||
</table>
|
||||
<Center height={400}>no rows</Center>
|
||||
</>
|
||||
) : (
|
||||
<TableVirtuoso
|
||||
data={dataSource}
|
||||
fixedHeaderContent={() => header}
|
||||
itemContent={(index, row) => (
|
||||
<>
|
||||
{columns.map((column) => (
|
||||
<TableCell
|
||||
column={column}
|
||||
dataItem={row}
|
||||
key={`${column}_${index}`}
|
||||
rowIndex={index}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
interface TableCellProps {
|
||||
column: string;
|
||||
dataItem: any;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
const TableCell = ({ dataItem, column, rowIndex }: TableCellProps) => {
|
||||
const data = dataItem[column];
|
||||
const content = useMemo(() => {
|
||||
switch (typeof data) {
|
||||
case 'object': {
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
case 'boolean': {
|
||||
return data ? 'True' : 'False';
|
||||
}
|
||||
|
||||
default: {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<td key={column} onDoubleClick={() => console.log('Edit cell:', rowIndex, column)}>
|
||||
{content}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableCell;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ActionIcon, Icon } from '@lobehub/ui';
|
||||
import { Button } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { Download, Filter, RefreshCw } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
import Table from './Table';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
dataPanel: css`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
background: ${token.colorBgContainer};
|
||||
`,
|
||||
toolbar: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
padding-block: 12px;
|
||||
padding-inline: 16px;
|
||||
border-block-end: 1px solid ${token.colorBorderSecondary};
|
||||
`,
|
||||
toolbarButtons: css`
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
`,
|
||||
toolbarTitle: css`
|
||||
font-size: ${token.fontSizeLG}px;
|
||||
font-weight: ${token.fontWeightStrong};
|
||||
color: ${token.colorText};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface DataTableProps {
|
||||
tableName: string;
|
||||
}
|
||||
|
||||
const DataTable = ({ tableName }: DataTableProps) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.dataPanel}>
|
||||
{/* Toolbar */}
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.toolbarTitle}>{tableName || 'Select a table'}</div>
|
||||
<div className={styles.toolbarButtons}>
|
||||
<Button color={'default'} icon={<Icon icon={Filter} />} variant={'filled'}>
|
||||
Filter
|
||||
</Button>
|
||||
<ActionIcon icon={Download} title={'Export'} />
|
||||
<ActionIcon icon={RefreshCw} title={'Refresh'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<Table tableName={tableName} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ChevronDown, ChevronRight, Database, Table as TableIcon } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { tableViewerService } from '@/services/tableViewer';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
|
||||
import TableColumns from './TableColumns';
|
||||
|
||||
// 样式定义
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
button: css`
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
padding-block: ${token.paddingXS}px;
|
||||
padding-inline: ${token.padding}px;
|
||||
border: none;
|
||||
border-radius: ${token.borderRadius}px;
|
||||
|
||||
color: ${token.colorText};
|
||||
|
||||
background: ${token.colorFillSecondary};
|
||||
|
||||
transition: all ${token.motionDurationMid};
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
count: css`
|
||||
font-size: 12px;
|
||||
color: ${token.colorTextTertiary};
|
||||
`,
|
||||
dataPanel: css`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
background: ${token.colorBgContainer};
|
||||
`,
|
||||
schema: css`
|
||||
font-size: 12px;
|
||||
color: ${token.colorTextTertiary};
|
||||
font-weight: normal;
|
||||
font-family: ${token.fontFamilyCode};
|
||||
`,
|
||||
schemaHeader: css`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
padding-block: 12px;
|
||||
padding-inline: 16px;
|
||||
|
||||
font-weight: ${token.fontWeightStrong};
|
||||
color: ${token.colorText};
|
||||
`,
|
||||
schemaPanel: css`
|
||||
overflow: scroll;
|
||||
|
||||
width: 280px;
|
||||
height: 100%;
|
||||
border-inline-end: 1px solid ${token.colorBorderSecondary};
|
||||
|
||||
background: ${token.colorBgContainer};
|
||||
`,
|
||||
selected: css`
|
||||
background: ${token.colorFillSecondary};
|
||||
`,
|
||||
table: css`
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
inset-block-start: 0;
|
||||
|
||||
padding: ${token.padding}px;
|
||||
border-block-end: 1px solid ${token.colorBorderSecondary};
|
||||
|
||||
font-weight: ${token.fontWeightStrong};
|
||||
text-align: start;
|
||||
|
||||
background: ${token.colorFillQuaternary};
|
||||
}
|
||||
|
||||
td {
|
||||
padding: ${token.padding}px;
|
||||
border-block-end: 1px solid ${token.colorBorderSecondary};
|
||||
transition: all ${token.motionDurationMid};
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillQuaternary};
|
||||
}
|
||||
}
|
||||
`,
|
||||
tableItem: css`
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
margin-inline: 8px;
|
||||
padding: 8px;
|
||||
border-radius: ${token.borderRadius}px;
|
||||
|
||||
color: ${token.colorText};
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillSecondary};
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
interface SchemaPanelProps {
|
||||
onTableSelect: (tableName: string) => void;
|
||||
selectedTable?: string;
|
||||
}
|
||||
const SchemaPanel = ({ onTableSelect, selectedTable }: SchemaPanelProps) => {
|
||||
const { styles, cx } = useStyles();
|
||||
const [expandedTables, setExpandedTables] = useState(new Set());
|
||||
|
||||
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
||||
|
||||
const { data, isLoading } = useSWR(isDBInited ? 'fetch-tables' : null, () =>
|
||||
tableViewerService.getAllTables(),
|
||||
);
|
||||
|
||||
const toggleTable = (tableName: string) => {
|
||||
const newExpanded = new Set(expandedTables);
|
||||
if (newExpanded.has(tableName)) {
|
||||
newExpanded.delete(tableName);
|
||||
} else {
|
||||
newExpanded.add(tableName);
|
||||
}
|
||||
setExpandedTables(newExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.schemaPanel}>
|
||||
<div className={styles.schemaHeader}>
|
||||
<Database size={16} />
|
||||
<Flexbox align={'center'} horizontal justify={'space-between'}>
|
||||
<span>Tables {data?.length}</span>
|
||||
<span className={styles.schema}>public</span>
|
||||
</Flexbox>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<Flexbox>
|
||||
{data?.map((table) => (
|
||||
<div key={table.name}>
|
||||
<Flexbox
|
||||
className={cx(styles.tableItem, selectedTable === table.name && styles.selected)}
|
||||
horizontal
|
||||
onClick={() => {
|
||||
toggleTable(table.name);
|
||||
onTableSelect(table.name);
|
||||
}}
|
||||
>
|
||||
<Icon icon={expandedTables.has(table.name) ? ChevronDown : ChevronRight} />
|
||||
<TableIcon size={16} />
|
||||
<Flexbox align={'center'} horizontal justify={'space-between'}>
|
||||
<span>{table.name}</span>
|
||||
<span className={styles.count}>{table.count}</span>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
{expandedTables.has(table.name) && <TableColumns tableName={table.name} />}
|
||||
</div>
|
||||
))}
|
||||
</Flexbox>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchemaPanel;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Tag } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import React from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useTableColumns } from './useTableColumns';
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
container: css`
|
||||
margin-inline: 40px 4px;
|
||||
|
||||
font-size: ${token.fontSizeSM}px;
|
||||
color: ${token.colorTextSecondary};
|
||||
`,
|
||||
item: css`
|
||||
padding-block: 4px;
|
||||
padding-inline: 0;
|
||||
font-family: ${token.fontFamilyCode};
|
||||
`,
|
||||
type: css`
|
||||
color: ${token.red9};
|
||||
font-size: 10px;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface TableColumnsProps {
|
||||
tableName: string;
|
||||
}
|
||||
|
||||
const TableColumns = ({ tableName }: TableColumnsProps) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
const { data, isLoading } = useTableColumns(tableName);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<Flexbox>
|
||||
{data?.map((column) => (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.item}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
key={column.name}
|
||||
>
|
||||
<Flexbox>
|
||||
<Flexbox>{column.name}</Flexbox>
|
||||
<span className={styles.type}>{column.type}</span>
|
||||
</Flexbox>
|
||||
{column.isPrimaryKey && (
|
||||
<div>
|
||||
<Tag bordered={false} color={'cyan'}>
|
||||
Primary
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
</Flexbox>
|
||||
))}
|
||||
</Flexbox>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableColumns;
|
||||
@@ -0,0 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import DataTable from './DataTable';
|
||||
import SchemaPanel from './Schema';
|
||||
|
||||
// Main Database Panel Component
|
||||
const DatabasePanel = () => {
|
||||
const [selectedTable, setSelectedTable] = useState<string>('');
|
||||
|
||||
return (
|
||||
<Flexbox height={'100%'} horizontal>
|
||||
<SchemaPanel onTableSelect={setSelectedTable} selectedTable={selectedTable} />
|
||||
<DataTable tableName={selectedTable} />
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabasePanel;
|
||||
@@ -0,0 +1,13 @@
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { tableViewerService } from '@/services/tableViewer';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
|
||||
export const useTableColumns = (tableName?: string) => {
|
||||
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
||||
|
||||
return useSWR(isDBInited && tableName ? ['fetch-table-columns', tableName] : null, ([, table]) =>
|
||||
tableViewerService.getTableDetails(table),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import FloatPanel from './FloatPanel';
|
||||
import PostgresViewer from './PostgresViewer';
|
||||
|
||||
const DevPanel = () => (
|
||||
<FloatPanel>
|
||||
<PostgresViewer />
|
||||
</FloatPanel>
|
||||
);
|
||||
|
||||
export default DevPanel;
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
@@ -76,7 +76,9 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
|
||||
</Flexbox>
|
||||
),
|
||||
onClick: () => {
|
||||
router.push(!isServerMode ? '/settings/llm' : `/settings/provider/${provider.id}`);
|
||||
router.push(
|
||||
isDeprecatedEdition ? '/settings/llm' : `/settings/provider/${provider.id}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import isEqual from 'fast-deep-equal';
|
||||
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { useAiInfraStore } from '@/store/aiInfra';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { modelProviderSelectors } from '@/store/user/selectors';
|
||||
@@ -10,7 +10,7 @@ export const useEnabledChatModels = (): EnabledProviderWithModels[] => {
|
||||
const enabledList = useUserStore(modelProviderSelectors.modelProviderListForModelSelect, isEqual);
|
||||
const enabledChatModelList = useAiInfraStore((s) => s.enabledChatModelList, isEqual);
|
||||
|
||||
if (!isServerMode) {
|
||||
if (isDeprecatedEdition) {
|
||||
return enabledList;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { modelProviderSelectors } from '@/store/user/selectors';
|
||||
@@ -8,7 +8,7 @@ export const useModelContextWindowTokens = (model: string, provider: string) =>
|
||||
|
||||
// TODO: remove this in V2.0
|
||||
const oldValue = useUserStore(modelProviderSelectors.modelMaxToken(model));
|
||||
if (!isServerMode) return oldValue;
|
||||
if (isDeprecatedEdition) return oldValue;
|
||||
//
|
||||
|
||||
return newValue as number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/slices/chat';
|
||||
import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
@@ -12,7 +12,7 @@ export const useModelHasContextWindowToken = () => {
|
||||
|
||||
// TODO: remove this in V2.0
|
||||
const oldValue = useUserStore(modelProviderSelectors.isModelHasMaxToken(model));
|
||||
if (!isServerMode) return oldValue;
|
||||
if (isDeprecatedEdition) return oldValue;
|
||||
//
|
||||
|
||||
return newValue;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { modelProviderSelectors } from '@/store/user/selectors';
|
||||
@@ -8,7 +8,7 @@ export const useModelSupportToolUse = (model: string, provider: string) => {
|
||||
|
||||
// TODO: remove this in V2.0
|
||||
const oldValue = useUserStore(modelProviderSelectors.isModelEnabledFunctionCall(model));
|
||||
if (!isServerMode) return oldValue;
|
||||
if (isDeprecatedEdition) return oldValue;
|
||||
//
|
||||
|
||||
return newValue;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { modelProviderSelectors } from '@/store/user/selectors';
|
||||
@@ -8,7 +8,7 @@ export const useModelSupportVision = (model: string, provider: string) => {
|
||||
|
||||
// TODO: remove this in V2.0
|
||||
const oldValue = useUserStore(modelProviderSelectors.isModelEnabledVision(model));
|
||||
if (!isServerMode) return oldValue;
|
||||
if (isDeprecatedEdition) return oldValue;
|
||||
//
|
||||
|
||||
return newValue;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
LOBE_THEME_NEUTRAL_COLOR,
|
||||
LOBE_THEME_PRIMARY_COLOR,
|
||||
} from '@/const/theme';
|
||||
import DebugUI from '@/features/DebugUI';
|
||||
import DevPanel from '@/features/DevPanel';
|
||||
import { getServerGlobalConfig } from '@/server/globalConfig';
|
||||
import { ServerConfigStoreProvider } from '@/store/serverConfig';
|
||||
import { getAntdLocale, parseBrowserLanguage } from '@/utils/locale';
|
||||
@@ -65,9 +65,9 @@ const GlobalLayout = async ({ children }: PropsWithChildren) => {
|
||||
<Suspense>
|
||||
<StoreInitialization />
|
||||
<ReactScan />
|
||||
{process.env.NODE_ENV === 'development' && <DevPanel />}
|
||||
</Suspense>
|
||||
</ServerConfigStoreProvider>
|
||||
<DebugUI />
|
||||
</AppTheme>
|
||||
<AntdV5MonkeyPatch />
|
||||
</Locale>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { ModelProvider } from '@/libs/agent-runtime';
|
||||
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
import { useUserStore } from '@/store/user';
|
||||
@@ -94,7 +94,7 @@ export const createPayloadWithKeyVaults = (provider: string) => {
|
||||
let keyVaults = {};
|
||||
|
||||
// TODO: remove this condition in V2.0
|
||||
if (!isServerMode) {
|
||||
if (isDeprecatedEdition) {
|
||||
keyVaults = keyVaultsConfigSelectors.getVaultByProvider(provider as any)(
|
||||
useUserStore.getState(),
|
||||
);
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import {
|
||||
AiModelSortMap,
|
||||
AiProviderModelListItem,
|
||||
CreateAiModelParams,
|
||||
ToggleAiModelEnableParams,
|
||||
UpdateAiModelParams,
|
||||
} from '@/types/aiModel';
|
||||
|
||||
class AiModelService {
|
||||
createAiModel = async (params: CreateAiModelParams) => {
|
||||
return lambdaClient.aiModel.createAiModel.mutate(params);
|
||||
};
|
||||
|
||||
getAiProviderModelList = async (id: string): Promise<AiProviderModelListItem[]> => {
|
||||
return lambdaClient.aiModel.getAiProviderModelList.query({ id });
|
||||
};
|
||||
|
||||
getAiModelById = async (id: string) => {
|
||||
return lambdaClient.aiModel.getAiModelById.query({ id });
|
||||
};
|
||||
|
||||
toggleModelEnabled = async (params: ToggleAiModelEnableParams) => {
|
||||
return lambdaClient.aiModel.toggleModelEnabled.mutate(params);
|
||||
};
|
||||
|
||||
updateAiModel = async (id: string, providerId: string, value: UpdateAiModelParams) => {
|
||||
return lambdaClient.aiModel.updateAiModel.mutate({ id, providerId, value });
|
||||
};
|
||||
|
||||
batchUpdateAiModels = async (id: string, models: AiProviderModelListItem[]) => {
|
||||
return lambdaClient.aiModel.batchUpdateAiModels.mutate({ id, models });
|
||||
};
|
||||
|
||||
batchToggleAiModels = async (id: string, models: string[], enabled: boolean) => {
|
||||
return lambdaClient.aiModel.batchToggleAiModels.mutate({ enabled, id, models });
|
||||
};
|
||||
|
||||
clearRemoteModels = async (providerId: string) => {
|
||||
return lambdaClient.aiModel.clearRemoteModels.mutate({ providerId });
|
||||
};
|
||||
|
||||
updateAiModelOrder = async (providerId: string, items: AiModelSortMap[]) => {
|
||||
return lambdaClient.aiModel.updateAiModelOrder.mutate({ providerId, sortMap: items });
|
||||
};
|
||||
|
||||
deleteAiModel = async (params: { id: string; providerId: string }) => {
|
||||
return lambdaClient.aiModel.removeAiModel.mutate(params);
|
||||
};
|
||||
}
|
||||
|
||||
export const aiModelService = new AiModelService();
|
||||
@@ -0,0 +1,60 @@
|
||||
import { clientDB } from '@/database/client/db';
|
||||
import { AiInfraRepos } from '@/database/repositories/aiInfra';
|
||||
import { AiModelModel } from '@/database/server/models/aiModel';
|
||||
import { BaseClientService } from '@/services/baseClientService';
|
||||
|
||||
import { IAiModelService } from './type';
|
||||
|
||||
export class ClientService extends BaseClientService implements IAiModelService {
|
||||
private get aiModel(): AiModelModel {
|
||||
return new AiModelModel(clientDB as any, this.userId);
|
||||
}
|
||||
private get aiInfraRepos(): AiInfraRepos {
|
||||
return new AiInfraRepos(clientDB as any, this.userId, {});
|
||||
}
|
||||
|
||||
createAiModel: IAiModelService['createAiModel'] = async (params) => {
|
||||
const data = await this.aiModel.create(params);
|
||||
|
||||
return data?.id;
|
||||
};
|
||||
|
||||
getAiProviderModelList: IAiModelService['getAiProviderModelList'] = async (id) => {
|
||||
return this.aiInfraRepos.getAiProviderModelList(id);
|
||||
};
|
||||
|
||||
getAiModelById: IAiModelService['getAiModelById'] = async (id) => {
|
||||
return this.aiModel.findById(id);
|
||||
};
|
||||
|
||||
toggleModelEnabled: IAiModelService['toggleModelEnabled'] = async (params) => {
|
||||
return this.aiModel.toggleModelEnabled(params);
|
||||
};
|
||||
|
||||
updateAiModel: IAiModelService['updateAiModel'] = async (id, providerId, value) => {
|
||||
return this.aiModel.update(id, providerId, value);
|
||||
};
|
||||
|
||||
batchUpdateAiModels: IAiModelService['batchUpdateAiModels'] = async (id, models) => {
|
||||
return this.aiModel.batchUpdateAiModels(id, models);
|
||||
};
|
||||
|
||||
batchToggleAiModels: IAiModelService['batchToggleAiModels'] = async (id, models, enabled) => {
|
||||
return this.aiModel.batchToggleAiModels(id, models, enabled);
|
||||
};
|
||||
|
||||
clearRemoteModels: IAiModelService['clearRemoteModels'] = async (providerId) => {
|
||||
return this.aiModel.clearRemoteModels(providerId);
|
||||
};
|
||||
|
||||
updateAiModelOrder: IAiModelService['updateAiModelOrder'] = async (providerId, items) => {
|
||||
return this.aiModel.updateModelsOrder(providerId, items);
|
||||
};
|
||||
|
||||
deleteAiModel: IAiModelService['deleteAiModel'] = async (params: {
|
||||
id: string;
|
||||
providerId: string;
|
||||
}) => {
|
||||
return this.aiModel.delete(params.id, params.providerId);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { testService } from '~test-utils';
|
||||
|
||||
import { ClientService } from './client';
|
||||
import { ServerService } from './server';
|
||||
|
||||
describe('aiModelService', () => {
|
||||
testService(ServerService);
|
||||
|
||||
testService(ClientService);
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ClientService } from './client';
|
||||
import { ServerService } from './server';
|
||||
|
||||
export const aiModelService =
|
||||
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService();
|
||||
@@ -0,0 +1,47 @@
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import { IAiModelService } from '@/services/aiModel/type';
|
||||
|
||||
export class ServerService implements IAiModelService {
|
||||
createAiModel: IAiModelService['createAiModel'] = async (params) => {
|
||||
return lambdaClient.aiModel.createAiModel.mutate(params);
|
||||
};
|
||||
|
||||
getAiProviderModelList: IAiModelService['getAiProviderModelList'] = async (id) => {
|
||||
return lambdaClient.aiModel.getAiProviderModelList.query({ id });
|
||||
};
|
||||
|
||||
getAiModelById: IAiModelService['getAiModelById'] = async (id) => {
|
||||
return lambdaClient.aiModel.getAiModelById.query({ id });
|
||||
};
|
||||
|
||||
toggleModelEnabled: IAiModelService['toggleModelEnabled'] = async (params) => {
|
||||
return lambdaClient.aiModel.toggleModelEnabled.mutate(params);
|
||||
};
|
||||
|
||||
updateAiModel: IAiModelService['updateAiModel'] = async (id, providerId, value) => {
|
||||
return lambdaClient.aiModel.updateAiModel.mutate({ id, providerId, value });
|
||||
};
|
||||
|
||||
batchUpdateAiModels: IAiModelService['batchUpdateAiModels'] = async (id, models) => {
|
||||
return lambdaClient.aiModel.batchUpdateAiModels.mutate({ id, models });
|
||||
};
|
||||
|
||||
batchToggleAiModels: IAiModelService['batchToggleAiModels'] = async (id, models, enabled) => {
|
||||
return lambdaClient.aiModel.batchToggleAiModels.mutate({ enabled, id, models });
|
||||
};
|
||||
|
||||
clearRemoteModels: IAiModelService['clearRemoteModels'] = async (providerId) => {
|
||||
return lambdaClient.aiModel.clearRemoteModels.mutate({ providerId });
|
||||
};
|
||||
|
||||
updateAiModelOrder: IAiModelService['updateAiModelOrder'] = async (providerId, items) => {
|
||||
return lambdaClient.aiModel.updateAiModelOrder.mutate({ providerId, sortMap: items });
|
||||
};
|
||||
|
||||
deleteAiModel: IAiModelService['deleteAiModel'] = async (params: {
|
||||
id: string;
|
||||
providerId: string;
|
||||
}) => {
|
||||
return lambdaClient.aiModel.removeAiModel.mutate(params);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable typescript-sort-keys/interface */
|
||||
import {
|
||||
AiModelSortMap,
|
||||
AiProviderModelListItem,
|
||||
CreateAiModelParams,
|
||||
ToggleAiModelEnableParams,
|
||||
UpdateAiModelParams,
|
||||
} from '@/types/aiModel';
|
||||
|
||||
export interface IAiModelService {
|
||||
createAiModel: (params: CreateAiModelParams) => Promise<any>;
|
||||
|
||||
getAiProviderModelList: (id: string) => Promise<AiProviderModelListItem[]>;
|
||||
|
||||
getAiModelById: (id: string) => Promise<any>;
|
||||
|
||||
toggleModelEnabled: (params: ToggleAiModelEnableParams) => Promise<any>;
|
||||
|
||||
updateAiModel: (id: string, providerId: string, value: UpdateAiModelParams) => Promise<any>;
|
||||
|
||||
batchUpdateAiModels: (id: string, models: AiProviderModelListItem[]) => Promise<any>;
|
||||
|
||||
batchToggleAiModels: (id: string, models: string[], enabled: boolean) => Promise<any>;
|
||||
|
||||
clearRemoteModels: (providerId: string) => Promise<any>;
|
||||
|
||||
updateAiModelOrder: (providerId: string, items: AiModelSortMap[]) => Promise<any>;
|
||||
|
||||
deleteAiModel: (params: { id: string; providerId: string }) => Promise<any>;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import {
|
||||
AiProviderRuntimeState,
|
||||
AiProviderSortMap,
|
||||
CreateAiProviderParams,
|
||||
UpdateAiProviderConfigParams,
|
||||
} from '@/types/aiProvider';
|
||||
|
||||
class AiProviderService {
|
||||
createAiProvider = async (params: CreateAiProviderParams) => {
|
||||
return lambdaClient.aiProvider.createAiProvider.mutate(params);
|
||||
};
|
||||
|
||||
getAiProviderList = async () => {
|
||||
return lambdaClient.aiProvider.getAiProviderList.query();
|
||||
};
|
||||
|
||||
getAiProviderById = async (id: string) => {
|
||||
return lambdaClient.aiProvider.getAiProviderById.query({ id });
|
||||
};
|
||||
|
||||
toggleProviderEnabled = async (id: string, enabled: boolean) => {
|
||||
return lambdaClient.aiProvider.toggleProviderEnabled.mutate({ enabled, id });
|
||||
};
|
||||
|
||||
updateAiProvider = async (id: string, value: any) => {
|
||||
return lambdaClient.aiProvider.updateAiProvider.mutate({ id, value });
|
||||
};
|
||||
|
||||
updateAiProviderConfig = async (id: string, value: UpdateAiProviderConfigParams) => {
|
||||
return lambdaClient.aiProvider.updateAiProviderConfig.mutate({ id, value });
|
||||
};
|
||||
|
||||
updateAiProviderOrder = async (items: AiProviderSortMap[]) => {
|
||||
return lambdaClient.aiProvider.updateAiProviderOrder.mutate({ sortMap: items });
|
||||
};
|
||||
|
||||
deleteAiProvider = async (id: string) => {
|
||||
return lambdaClient.aiProvider.removeAiProvider.mutate({ id });
|
||||
};
|
||||
|
||||
getAiProviderRuntimeState = async (isLogin?: boolean): Promise<AiProviderRuntimeState> => {
|
||||
return lambdaClient.aiProvider.getAiProviderRuntimeState.query({ isLogin });
|
||||
};
|
||||
}
|
||||
|
||||
export const aiProviderService = new AiProviderService();
|
||||
@@ -0,0 +1,64 @@
|
||||
import { clientDB } from '@/database/client/db';
|
||||
import { AiInfraRepos } from '@/database/repositories/aiInfra';
|
||||
import { AiProviderModel } from '@/database/server/models/aiProvider';
|
||||
import { BaseClientService } from '@/services/baseClientService';
|
||||
|
||||
import { IAiProviderService } from './type';
|
||||
|
||||
export class ClientService extends BaseClientService implements IAiProviderService {
|
||||
private get aiProviderModel(): AiProviderModel {
|
||||
return new AiProviderModel(clientDB as any, this.userId);
|
||||
}
|
||||
private get aiInfraRepos(): AiInfraRepos {
|
||||
let config = {};
|
||||
if (typeof window !== 'undefined') {
|
||||
config = window.global_serverConfigStore.getState().serverConfig.aiProvider || {};
|
||||
}
|
||||
|
||||
return new AiInfraRepos(clientDB as any, this.userId, config);
|
||||
}
|
||||
|
||||
createAiProvider: IAiProviderService['createAiProvider'] = async (params) => {
|
||||
const data = await this.aiProviderModel.create(params);
|
||||
|
||||
return data?.id;
|
||||
};
|
||||
|
||||
getAiProviderById: IAiProviderService['getAiProviderById'] = async (id) => {
|
||||
return this.aiProviderModel.getAiProviderById(id);
|
||||
};
|
||||
|
||||
getAiProviderList: IAiProviderService['getAiProviderList'] = async () => {
|
||||
return await this.aiInfraRepos.getAiProviderList();
|
||||
};
|
||||
|
||||
getAiProviderRuntimeState: IAiProviderService['getAiProviderRuntimeState'] = async () => {
|
||||
const runtimeConfig = await this.aiProviderModel.getAiProviderRuntimeConfig();
|
||||
|
||||
const enabledAiProviders = await this.aiInfraRepos.getUserEnabledProviderList();
|
||||
|
||||
const enabledAiModels = await this.aiInfraRepos.getEnabledModels();
|
||||
|
||||
return { enabledAiModels, enabledAiProviders, runtimeConfig };
|
||||
};
|
||||
|
||||
toggleProviderEnabled: IAiProviderService['toggleProviderEnabled'] = async (id, enabled) => {
|
||||
return this.aiProviderModel.toggleProviderEnabled(id, enabled);
|
||||
};
|
||||
|
||||
updateAiProvider: IAiProviderService['updateAiProvider'] = async (id, value) => {
|
||||
return this.aiProviderModel.update(id, value);
|
||||
};
|
||||
|
||||
updateAiProviderConfig: IAiProviderService['updateAiProviderConfig'] = async (id, value) => {
|
||||
return this.aiProviderModel.updateConfig(id, value);
|
||||
};
|
||||
|
||||
updateAiProviderOrder: IAiProviderService['updateAiProviderOrder'] = async (items) => {
|
||||
return this.aiProviderModel.updateOrder(items);
|
||||
};
|
||||
|
||||
deleteAiProvider: IAiProviderService['deleteAiProvider'] = async (id) => {
|
||||
return this.aiProviderModel.delete(id);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { testService } from '~test-utils';
|
||||
|
||||
import { ClientService } from './client';
|
||||
import { ServerService } from './server';
|
||||
|
||||
describe('aiProviderService', () => {
|
||||
testService(ServerService);
|
||||
|
||||
testService(ClientService);
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ClientService } from './client';
|
||||
import { ServerService } from './server';
|
||||
|
||||
export const aiProviderService =
|
||||
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService();
|
||||
@@ -0,0 +1,43 @@
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
|
||||
import { IAiProviderService } from './type';
|
||||
|
||||
export class ServerService implements IAiProviderService {
|
||||
createAiProvider: IAiProviderService['createAiProvider'] = async (params) => {
|
||||
return lambdaClient.aiProvider.createAiProvider.mutate(params);
|
||||
};
|
||||
|
||||
getAiProviderList: IAiProviderService['getAiProviderList'] = async () => {
|
||||
return lambdaClient.aiProvider.getAiProviderList.query();
|
||||
};
|
||||
|
||||
getAiProviderById: IAiProviderService['getAiProviderById'] = async (id) => {
|
||||
return lambdaClient.aiProvider.getAiProviderById.query({ id });
|
||||
};
|
||||
|
||||
toggleProviderEnabled: IAiProviderService['toggleProviderEnabled'] = async (id, enabled) => {
|
||||
return lambdaClient.aiProvider.toggleProviderEnabled.mutate({ enabled, id });
|
||||
};
|
||||
|
||||
updateAiProvider: IAiProviderService['updateAiProvider'] = async (id, value) => {
|
||||
return lambdaClient.aiProvider.updateAiProvider.mutate({ id, value });
|
||||
};
|
||||
|
||||
updateAiProviderConfig: IAiProviderService['updateAiProviderConfig'] = async (id, value) => {
|
||||
return lambdaClient.aiProvider.updateAiProviderConfig.mutate({ id, value });
|
||||
};
|
||||
|
||||
updateAiProviderOrder: IAiProviderService['updateAiProviderOrder'] = async (items) => {
|
||||
return lambdaClient.aiProvider.updateAiProviderOrder.mutate({ sortMap: items });
|
||||
};
|
||||
|
||||
deleteAiProvider: IAiProviderService['deleteAiProvider'] = async (id) => {
|
||||
return lambdaClient.aiProvider.removeAiProvider.mutate({ id });
|
||||
};
|
||||
|
||||
getAiProviderRuntimeState: IAiProviderService['getAiProviderRuntimeState'] = async (
|
||||
isLogin?: boolean,
|
||||
) => {
|
||||
return lambdaClient.aiProvider.getAiProviderRuntimeState.query({ isLogin });
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
AiProviderRuntimeState,
|
||||
AiProviderSortMap,
|
||||
CreateAiProviderParams,
|
||||
UpdateAiProviderConfigParams,
|
||||
} from '@/types/aiProvider';
|
||||
|
||||
export interface IAiProviderService {
|
||||
createAiProvider: (params: CreateAiProviderParams) => Promise<any>;
|
||||
|
||||
deleteAiProvider: (id: string) => Promise<any>;
|
||||
|
||||
getAiProviderById: (id: string) => Promise<any>;
|
||||
|
||||
getAiProviderList: () => Promise<any>;
|
||||
|
||||
getAiProviderRuntimeState: (isLogin?: boolean) => Promise<AiProviderRuntimeState>;
|
||||
|
||||
toggleProviderEnabled: (id: string, enabled: boolean) => Promise<any>;
|
||||
|
||||
updateAiProvider: (id: string, value: any) => Promise<any>;
|
||||
|
||||
updateAiProviderConfig: (id: string, value: UpdateAiProviderConfigParams) => Promise<any>;
|
||||
|
||||
updateAiProviderOrder: (items: AiProviderSortMap[]) => Promise<any>;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { INBOX_GUIDE_SYSTEMROLE } from '@/const/guide';
|
||||
import { INBOX_SESSION_ID } from '@/const/session';
|
||||
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
||||
import { TracePayload, TraceTagMap } from '@/const/trace';
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition, isServerMode } from '@/const/version';
|
||||
import {
|
||||
AgentRuntime,
|
||||
AgentRuntimeError,
|
||||
@@ -42,7 +42,7 @@ import { API_ENDPOINTS } from './_url';
|
||||
|
||||
const isCanUseFC = (model: string, provider: string) => {
|
||||
// TODO: remove isDeprecatedEdition condition in V2.0
|
||||
if (!isServerMode) {
|
||||
if (isDeprecatedEdition) {
|
||||
return modelProviderSelectors.isModelEnabledFunctionCall(model)(useUserStore.getState());
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ const findAzureDeploymentName = (model: string) => {
|
||||
let deploymentId = model;
|
||||
|
||||
// TODO: remove isDeprecatedEdition condition in V2.0
|
||||
if (!isServerMode) {
|
||||
if (isDeprecatedEdition) {
|
||||
const chatModelCards = modelProviderSelectors.getModelCardsById(ModelProvider.Azure)(
|
||||
useUserStore.getState(),
|
||||
);
|
||||
@@ -74,7 +74,7 @@ const findAzureDeploymentName = (model: string) => {
|
||||
|
||||
const isEnableFetchOnClient = (provider: string) => {
|
||||
// TODO: remove this condition in V2.0
|
||||
if (!isServerMode) {
|
||||
if (isDeprecatedEdition) {
|
||||
return modelConfigSelectors.isProviderFetchOnClient(provider)(useUserStore.getState());
|
||||
} else {
|
||||
return aiProviderSelectors.isProviderFetchOnClient(provider)(useAiInfraStore.getState());
|
||||
@@ -341,7 +341,7 @@ class ChatService {
|
||||
const isBuiltin = Object.values(ModelProvider).includes(provider as any);
|
||||
|
||||
// TODO: remove `!isDeprecatedEdition` condition in V2.0
|
||||
if (isServerMode && !isBuiltin) {
|
||||
if (!isDeprecatedEdition && !isBuiltin) {
|
||||
const providerConfig = aiProviderSelectors.providerConfigById(provider)(
|
||||
useAiInfraStore.getState(),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { clientDB } from '@/database/client/db';
|
||||
import { TableViewerRepo } from '@/database/repositories/tableViewer';
|
||||
import { BaseClientService } from '@/services/baseClientService';
|
||||
|
||||
export class ClientService extends BaseClientService {
|
||||
private get tableViewerRepo(): TableViewerRepo {
|
||||
return new TableViewerRepo(clientDB as any, this.userId);
|
||||
}
|
||||
|
||||
getAllTables = async () => this.tableViewerRepo.getAllTables();
|
||||
|
||||
getTableDetails = async (tableName: string) => this.tableViewerRepo.getTableDetails(tableName);
|
||||
|
||||
getTableData = async (tableName: string) =>
|
||||
this.tableViewerRepo.getTableData(tableName, { page: 1, pageSize: 300 });
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ClientService } from './client';
|
||||
|
||||
export const tableViewerService = new ClientService();
|
||||
@@ -3,7 +3,7 @@ import { SWRResponse, mutate } from 'swr';
|
||||
import { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { DEFAULT_MODEL_PROVIDER_LIST } from '@/config/modelProviders';
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { useClientDataSWR } from '@/libs/swr';
|
||||
import { aiProviderService } from '@/services/aiProvider';
|
||||
import { AiInfraStore } from '@/store/aiInfra/store';
|
||||
@@ -170,7 +170,7 @@ export const createAiProviderSlice: StateCreator<
|
||||
|
||||
useFetchAiProviderRuntimeState: (isLogin) =>
|
||||
useClientDataSWR<AiProviderRuntimeState | undefined>(
|
||||
isServerMode ? [AiProviderSwrKey.fetchAiProviderRuntimeState, isLogin] : null,
|
||||
!isDeprecatedEdition ? [AiProviderSwrKey.fetchAiProviderRuntimeState, isLogin] : null,
|
||||
async ([, isLogin]) => {
|
||||
if (isLogin) return aiProviderService.getAiProviderRuntimeState();
|
||||
|
||||
|
||||
@@ -24,7 +24,13 @@ export interface GlobalServerConfig {
|
||||
defaultAgent?: DeepPartial<UserDefaultAgent>;
|
||||
enableUploadFileToServer?: boolean;
|
||||
enabledAccessCode?: boolean;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
enabledOAuthSSO?: boolean;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
languageModel?: ServerLanguageModel;
|
||||
oAuthSSOProviders?: string[];
|
||||
systemAgent?: DeepPartial<UserSystemAgentConfig>;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export interface TableBasicInfo {
|
||||
count: number;
|
||||
name: string;
|
||||
type: 'BASE TABLE' | 'VIEW';
|
||||
}
|
||||
|
||||
export interface TableColumnInfo {
|
||||
defaultValue?: string;
|
||||
foreignKey?: {
|
||||
column: string;
|
||||
table: string;
|
||||
};
|
||||
isPrimaryKey: boolean;
|
||||
name: string;
|
||||
nullable: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface FilterCondition {
|
||||
column: string;
|
||||
operator: 'equals' | 'contains' | 'startsWith' | 'endsWith';
|
||||
value: any;
|
||||
}
|
||||
@@ -9,3 +9,49 @@ const swrConfig = {
|
||||
export const withSWR = ({ children }: PropsWithChildren) => (
|
||||
<SWRConfig value={swrConfig}>{children}</SWRConfig>
|
||||
);
|
||||
|
||||
interface TestServiceOptions {
|
||||
/** 是否检查 async */
|
||||
checkAsync?: boolean;
|
||||
/** 自定义的额外检查 */
|
||||
extraChecks?: (method: string, func: () => any) => void;
|
||||
/** 是否跳过某些方法 */
|
||||
skipMethods?: string[];
|
||||
}
|
||||
|
||||
const builtinSkipProps = new Set(['userId']);
|
||||
|
||||
export const testService = (ServiceClass: new () => any, options: TestServiceOptions = {}) => {
|
||||
const { checkAsync = true, skipMethods = ['userId'], extraChecks } = options;
|
||||
|
||||
describe(ServiceClass.name, () => {
|
||||
it('should implement all methods as arrow functions', () => {
|
||||
const service = new ServiceClass();
|
||||
|
||||
const methods = Object.getOwnPropertyNames(service).filter(
|
||||
(method) => !builtinSkipProps.has(method) || !skipMethods.includes(method),
|
||||
);
|
||||
|
||||
methods.forEach((method) => {
|
||||
const func = service[method];
|
||||
// 检查是否为函数
|
||||
expect(typeof func).toBe('function');
|
||||
|
||||
const funcString = func.toString();
|
||||
|
||||
// 验证是否是箭头函数
|
||||
expect(funcString).toContain('=>');
|
||||
|
||||
// 可选的 async 检查
|
||||
if (checkAsync) {
|
||||
expect(funcString).toMatch(/^async.*=>/);
|
||||
}
|
||||
|
||||
// 运行额外的自定义检查
|
||||
if (extraChecks) {
|
||||
extraChecks(method, func);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user