💄 style: support default config for system agent and pre-merge some desktop code (#7296)

* refactor

* update

* improve scripts

* fix changelog issue

* improve system agent config

* fix tests

* update scripts

* update ollama models

* Update ollama.ts
This commit is contained in:
Arvin Xu
2025-04-06 12:09:57 +08:00
committed by GitHub
parent 540aaf681f
commit addea48b52
19 changed files with 323 additions and 118 deletions

View File

@@ -1,59 +1,69 @@
/**
* Generate PR comment with download links for desktop builds
* and handle comment creation/update logic
*/
module.exports = async ({ github, context, releaseUrl, version, tag }) => {
try {
// Get release assets to create download links
const release = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag,
});
// 用于识别构建评论的标识符
const COMMENT_IDENTIFIER = '<!-- DESKTOP-BUILD-COMMENT -->';
// Organize assets by platform
const macAssets = release.data.assets.filter(
(asset) =>
((asset.name.includes('.dmg') || asset.name.includes('.zip')) &&
!asset.name.includes('.blockmap')) ||
(asset.name.includes('latest-mac') && asset.name.endsWith('.yml')),
);
/**
* 生成评论内容
*/
const generateCommentBody = async () => {
try {
// Get release assets to create download links
const release = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag,
});
const winAssets = release.data.assets.filter(
(asset) =>
(asset.name.includes('.exe') && !asset.name.includes('.blockmap')) ||
(asset.name.includes('latest-win') && asset.name.endsWith('.yml')),
);
// Organize assets by platform
const macAssets = release.data.assets.filter(
(asset) =>
((asset.name.includes('.dmg') || asset.name.includes('.zip')) &&
!asset.name.includes('.blockmap')) ||
(asset.name.includes('latest-mac') && asset.name.endsWith('.yml')),
);
const linuxAssets = release.data.assets.filter(
(asset) =>
(asset.name.includes('.AppImage') && !asset.name.includes('.blockmap')) ||
(asset.name.includes('latest-linux') && asset.name.endsWith('.yml')),
);
const winAssets = release.data.assets.filter(
(asset) =>
(asset.name.includes('.exe') && !asset.name.includes('.blockmap')) ||
(asset.name.includes('latest-win') && asset.name.endsWith('.yml')),
);
// Generate combined download table
let assetTable = '| Platform | File | Size |\n| --- | --- | --- |\n';
const linuxAssets = release.data.assets.filter(
(asset) =>
(asset.name.includes('.AppImage') && !asset.name.includes('.blockmap')) ||
(asset.name.includes('latest-linux') && asset.name.endsWith('.yml')),
);
// Add macOS assets
macAssets.forEach((asset) => {
const sizeInMB = (asset.size / (1024 * 1024)).toFixed(2);
assetTable += `| macOS | [${asset.name}](${asset.browser_download_url}) | ${sizeInMB} MB |\n`;
});
// Generate combined download table
let assetTable = '| Platform | File | Size |\n| --- | --- | --- |\n';
// Add Windows assets
winAssets.forEach((asset) => {
const sizeInMB = (asset.size / (1024 * 1024)).toFixed(2);
assetTable += `| Windows | [${asset.name}](${asset.browser_download_url}) | ${sizeInMB} MB |\n`;
});
// Add macOS assets
macAssets.forEach((asset) => {
const sizeInMB = (asset.size / (1024 * 1024)).toFixed(2);
assetTable += `| macOS | [${asset.name}](${asset.browser_download_url}) | ${sizeInMB} MB |\n`;
});
// Add Linux assets
linuxAssets.forEach((asset) => {
const sizeInMB = (asset.size / (1024 * 1024)).toFixed(2);
assetTable += `| Linux | [${asset.name}](${asset.browser_download_url}) | ${sizeInMB} MB |\n`;
});
// Add Windows assets
winAssets.forEach((asset) => {
const sizeInMB = (asset.size / (1024 * 1024)).toFixed(2);
assetTable += `| Windows | [${asset.name}](${asset.browser_download_url}) | ${sizeInMB} MB |\n`;
});
return `### 🚀 Desktop App Build Completed!
// Add Linux assets
linuxAssets.forEach((asset) => {
const sizeInMB = (asset.size / (1024 * 1024)).toFixed(2);
assetTable += `| Linux | [${asset.name}](${asset.browser_download_url}) | ${sizeInMB} MB |\n`;
});
return `${COMMENT_IDENTIFIER}
### 🚀 Desktop App Build Completed!
**Version**: \`${version}\`
**Build Time**: \`${new Date().toISOString()}\`
📦 [View All Build Artifacts](${releaseUrl})
@@ -65,17 +75,62 @@ ${assetTable}
> [!Warning]
>
> Note: This is a temporary build for testing purposes only.`;
} catch (error) {
console.error('Error generating PR comment:', error);
// Fallback to a simple comment if error occurs
return `
} catch (error) {
console.error('Error generating PR comment:', error);
// Fallback to a simple comment if error occurs
return `${COMMENT_IDENTIFIER}
### 🚀 Desktop App Build Completed!
**Version**: \`${version}\`
**Build Time**: \`${new Date().toISOString()}\`
## 📦 [View All Build Artifacts](${releaseUrl})
> Note: This is a temporary build for testing purposes only.
`;
}
`;
}
};
/**
* 查找并更新或创建PR评论
*/
const updateOrCreateComment = async () => {
// 生成评论内容
const body = await generateCommentBody();
// 查找我们之前可能创建的评论
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
// 查找包含我们标识符的评论
const buildComment = comments.find((comment) => comment.body.includes(COMMENT_IDENTIFIER));
if (buildComment) {
// 如果找到现有评论,则更新它
await github.rest.issues.updateComment({
comment_id: buildComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
console.log(`已更新现有评论 ID: ${buildComment.id}`);
return { updated: true, id: buildComment.id };
} else {
// 如果没有找到现有评论,则创建新评论
const result = await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
console.log(`已创建新评论 ID: ${result.data.id}`);
return { updated: false, id: result.data.id };
}
};
// 执行评论更新或创建
return await updateOrCreateComment();
};

2
.gitignore vendored
View File

@@ -68,4 +68,4 @@ public/swe-worker*
*.patch
*.pdf
vertex-ai-key.json
.pnpm-store
.pnpm-store

View File

@@ -71,6 +71,37 @@ Further reading:
- [\[RFC\] 022 - Default Assistant Parameters Configuration via Environment Variables](https://github.com/lobehub/lobe-chat/discussions/913)
### `SYSTEM_AGENT`
- Type: Optional
- Description: Used to configure models and providers for LobeChat system agents (such as topic generation, translation, etc.).
- Default value: `-`
- Example: `default=ollama/deepseek-v3` or `topic=openai/gpt-4,translation=anthropic/claude-1`
The `SYSTEM_AGENT` environment variable supports two configuration methods:
1. Use `default=provider/model` to set the same default configuration for all system agents
2. Configure specific system agents individually using the format `agent-name=provider/model`
Configuration details:
| Config Type | Format | Explanation |
| ------------------- | ----------------------------------------------- | ---------------------------------------------------------------------- |
| Default setting | `default=ollama/deepseek-v3` | Set deepseek-v3 from ollama as the default model for all system agents |
| Specific setting | `topic=openai/gpt-4` | Set a specific provider and model for topic generation |
| Mixed configuration | `default=ollama/deepseek-v3,topic=openai/gpt-4` | First set default values for all agents, then override specific agents |
Available system agents and their functions:
| System Agent | Key Name | Function Description |
| ------------------- | ----------------- | --------------------------------------------------------------------------------------------------- |
| Topic Generation | `topic` | Automatically generates topic names and summaries based on chat content |
| Translation | `translation` | Handles text translation between multiple languages |
| Metadata Generation | `agentMeta` | Generates descriptive information and metadata for assistants |
| History Compression | `historyCompress` | Compresses and organizes history for long conversations, optimizing context management |
| Query Rewrite | `queryRewrite` | Rewrites follow-up questions as standalone questions with context, improving conversation coherence |
| Thread Management | `thread` | Handles the creation and management of conversation threads |
### `FEATURE_FLAGS`
- Type: Optional

View File

@@ -67,6 +67,37 @@ LobeChat 在部署时提供了一些额外的配置项,你可以使用环境
- [\[RFC\] 022 - 环境变量配置默认助手参数](https://github.com/lobehub/lobe-chat/discussions/913)
### `SYSTEM_AGENT`
- 类型:可选
- 描述:用于配置 LobeChat 系统助手(如主题生成、翻译等功能)的模型和供应商。
- 默认值:`-`
- 示例:`default=ollama/deepseek-v3` 或 `topic=openai/gpt-4,translation=anthropic/claude-1`
`SYSTEM_AGENT` 环境变量支持两种配置方式:
1. 使用 `default=供应商/模型` 为所有系统助手设置相同的默认配置
2. 针对特定的系统助手进行单独配置,格式为 `助手名称=供应商/模型`
配置项说明:
| 配置项 | 格式 | 解释 |
| ---- | ----------------------------------------------- | ----------------------------------- |
| 默认设置 | `default=ollama/deepseek-v3` | 为所有系统助手设置默认模型为 ollama 的 deepseek-v3 |
| 特定设置 | `topic=openai/gpt-4` | 为主题生成设置特定的供应商和模型 |
| 混合配置 | `default=ollama/deepseek-v3,topic=openai/gpt-4` | 先为所有助手设置默认值,然后针对特定助手进行覆盖 |
可配置的系统助手及其作用:
| 系统助手 | 键名 | 作用描述 |
| ------- | ----------------- | --------------------------- |
| 主题生成 | `topic` | 根据聊天内容自动生成主题名称和摘要 |
| 翻译 | `translation` | 文本翻译使用的助手 |
| 元数据生成 | `agentMeta` | 为助手生成描述性信息和元数据 |
| 历史记录压缩 | `historyCompress` | 压缩和整理长对话的历史记录,优化上下文管理 |
| 知识库查询重写 | `queryRewrite` | 将后续问题改写为包含上下文的独立问题,提升对话的连贯性 |
| 分支对话 | `thread` | 自定生成分支对话的标题 |
### `FEATURE_FLAGS`
- 类型:可选

View File

@@ -12,6 +12,7 @@ dotenv.config();
const migrationsFolder = join(__dirname, '../../src/database/migrations');
const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
const runMigrations = async () => {
if (process.env.DATABASE_DRIVER === 'node') {
await nodeMigrate(serverDB, { migrationsFolder });
@@ -27,7 +28,7 @@ const runMigrations = async () => {
let connectionString = process.env.DATABASE_URL;
// only migrate database if the connection string is available
if (connectionString) {
if (!isDesktop && connectionString) {
// eslint-disable-next-line unicorn/prefer-top-level-await
runMigrations().catch((err) => {
console.error('❌ Database migrate failed:', err);
@@ -44,5 +45,5 @@ if (connectionString) {
process.exit(1);
});
} else {
console.log('🟢 not find database env, migration skipped');
console.log('🟢 not find database env or in desktop mode, migration skipped');
}

View File

@@ -16,7 +16,7 @@ import { z } from 'zod';
import { FormInput, FormPassword } from '@/components/FormInput';
import { FORM_STYLE } from '@/const/layoutTokens';
import { AES_GCM_URL, BASE_PROVIDER_DOC_URL } from '@/const/url';
import { isServerMode } from '@/const/version';
import { isDesktop, isServerMode } from '@/const/version';
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
import {
AiProviderDetailItem,
@@ -244,12 +244,14 @@ const ProviderConfig = memo<ProviderConfigProps>(
/*
* Conditions to show Client Fetch Switch
* 0. is not desktop app
* 1. provider is not disabled browser request
* 2. provider show browser request by default
* 3. Provider allow to edit endpoint and the value of endpoint is not empty
* 4. There is an apikey provided by user
*/
const showClientFetch =
!isDesktop &&
!disableBrowserRequest &&
(defaultShowBrowserRequest ||
(showEndpoint && isProviderEndpointNotEmpty) ||

View File

@@ -30,44 +30,43 @@ const DevTools = memo(() => {
const [tab, setTab] = useState<string>(items[0].key);
return (
<Flexbox
height={'100%'}
horizontal
style={{ background: theme.colorBgLayout, overflow: 'hidden', position: 'relative' }}
width={'100%'}
>
<SideNav
bottomActions={[]}
style={{
background: 'transparent',
paddingBlock: 32,
width: 48,
}}
topActions={items.map((item) => (
<ActionIcon
active={tab === item.key}
key={item.key}
onClick={() => setTab(item.key)}
placement={'right'}
title={item.key}
>
{item.icon}
</ActionIcon>
))}
/>
<Flexbox height={'100%'} style={{ overflow: 'hidden', position: 'relative' }} width={'100%'}>
<Flexbox
align={'center'}
className={cx(`panel-drag-handle`, styles.header, electronStylish.draggable)}
horizontal
justify={'center'}
>
<Flexbox align={'baseline'} gap={6} horizontal>
<b>{BRANDING_NAME} Dev Tools</b>
<span style={{ color: theme.colorTextDescription }}>/</span>
<span style={{ color: theme.colorTextDescription }}>{tab}</span>
</Flexbox>
<Flexbox height={'100%'} style={{ overflow: 'hidden', position: 'relative' }} width={'100%'}>
<Flexbox
align={'center'}
className={cx(`panel-drag-handle`, styles.header, electronStylish.draggable)}
horizontal
justify={'center'}
>
<Flexbox align={'baseline'} gap={6} horizontal>
<b>{BRANDING_NAME} Dev Tools</b>
<span style={{ color: theme.colorTextDescription }}>/</span>
<span style={{ color: theme.colorTextDescription }}>{tab}</span>
</Flexbox>
</Flexbox>
<Flexbox
height={'100%'}
horizontal
style={{ background: theme.colorBgLayout, overflow: 'hidden', position: 'relative' }}
width={'100%'}
>
<SideNav
bottomActions={[]}
style={{
background: 'transparent',
width: 48,
}}
topActions={items.map((item) => (
<ActionIcon
active={tab === item.key}
key={item.key}
onClick={() => setTab(item.key)}
placement={'right'}
title={item.key}
>
{item.icon}
</ActionIcon>
))}
/>
{items.map((item) => (
<Flexbox
flex={1}

View File

@@ -1,15 +1,6 @@
import { AIChatModelCard } from '@/types/aiModel';
const ollamaChatModels: AIChatModelCard[] = [
{
contextWindowTokens: 65_536,
description:
'DeepSeek-V3 是一个强大的专家混合MoE语言模型总参数量为 671B每个 Token 激活 37B 参数。该模型采用多头潜在注意力MLA和 DeepSeekMoE 架构,实现了高效推理和经济训练,并在前代 DeepSeek-V3 的基础上显著提升了性能。',
displayName: 'DeepSeek V3',
enabled: true,
id: 'deepseek-v3',
type: 'chat',
},
{
abilities: {
reasoning: true,
@@ -22,6 +13,14 @@ const ollamaChatModels: AIChatModelCard[] = [
id: 'deepseek-r1',
type: 'chat',
},
{
contextWindowTokens: 65_536,
description:
'DeepSeek-V3 是一个强大的专家混合MoE语言模型总参数量为 671B每个 Token 激活 37B 参数。该模型采用多头潜在注意力MLA和 DeepSeekMoE 架构,实现了高效推理和经济训练,并在前代 DeepSeek-V3 的基础上显著提升了性能。',
displayName: 'DeepSeek V3 671B',
id: 'deepseek-v3',
type: 'chat',
},
{
abilities: {
functionCall: true,
@@ -87,8 +86,10 @@ const ollamaChatModels: AIChatModelCard[] = [
reasoning: true,
},
contextWindowTokens: 128_000,
description: 'QwQ 是一个实验研究模型,专注于提高 AI 推理能力。',
description:
'QwQ 是 Qwen 系列的推理模型。与传统的指令调优模型相比QwQ 具备思考和推理的能力能够在下游任务中尤其是困难问题上显著提升性能。QwQ-32B 是中型推理模型,能够在与最先进的推理模型(如 DeepSeek-R1、o1-mini竞争时取得可观的表现。',
displayName: 'QwQ 32B',
enabled: true,
id: 'qwq',
releasedAt: '2024-11-28',
type: 'chat',
@@ -114,6 +115,7 @@ const ollamaChatModels: AIChatModelCard[] = [
contextWindowTokens: 128_000,
description: 'Qwen2.5 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
displayName: 'Qwen2.5 7B',
enabled: true,
id: 'qwen2.5',
type: 'chat',
},

View File

@@ -129,7 +129,7 @@ const SchemaPanel = ({ onTableSelect, selectedTable }: SchemaPanelProps) => {
};
return (
<DraggablePanel placement={'left'}>
<DraggablePanel minWidth={264} placement={'left'}>
<Flexbox height={'100%'} style={{ overflow: 'hidden', position: 'relative' }}>
<Flexbox
align={'center'}

View File

@@ -13,7 +13,7 @@ import { useNewVersion } from './useNewVersion';
const useStyles = createStyles(({ css }) => {
return {
popover: css`
inset-block-start: ${isDesktop ? 24 : 8}px !important;
inset-block-start: ${isDesktop ? 32 : 8}px !important;
inset-inline-start: 8px !important;
`,
};

View File

@@ -63,6 +63,7 @@ vi.mock('../DataStatistics', () => ({
vi.mock('@/const/version', () => ({
isDeprecatedEdition: false,
isDesktop: false,
}));
// 定义一个变量来存储 enableAuth 的值

View File

@@ -7,6 +7,9 @@
* @link https://trpc.io/docs/v11/router
* @link https://trpc.io/docs/v11/procedures
*/
import { DESKTOP_USER_ID } from '@/const/desktop';
import { isDesktop } from '@/const/version';
import { trpc } from './init';
import { jwtPayloadChecker } from './middleware/jwtPayload';
import { userAuth } from './middleware/userAuth';
@@ -21,7 +24,11 @@ export const router = trpc.router;
* Create an unprotected procedure
* @link https://trpc.io/docs/v11/procedures
**/
export const publicProcedure = trpc.procedure;
export const publicProcedure = trpc.procedure.use(({ next }) => {
return next({
ctx: { userId: isDesktop ? DESKTOP_USER_ID : null },
});
});
// procedure that asserts that the user is logged in
export const authedProcedure = trpc.procedure.use(userAuth);

View File

@@ -9,11 +9,10 @@ import { trpc } from '../init';
export const userAuth = trpc.middleware(async (opts) => {
const { ctx } = opts;
// 桌面端模式下,跳过默认鉴权逻辑
if (isDesktop) {
return opts.next({
ctx: {
userId: DESKTOP_USER_ID,
},
ctx: { userId: DESKTOP_USER_ID },
});
}
// `ctx.user` is nullable

View File

@@ -92,4 +92,60 @@ describe('parseSystemAgent', () => {
expect(parseSystemAgent(envValue)).toEqual(expected);
});
it('should apply default setting to all system agents when default is specified', () => {
const envValue = 'default=ollama/deepseek-v3';
const result = parseSystemAgent(envValue);
expect(result.topic).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.translation).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.agentMeta).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.historyCompress).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.thread).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.queryRewrite).toEqual({
provider: 'ollama',
model: 'deepseek-v3',
enabled: true,
});
});
it('should override default setting with specific settings', () => {
const envValue = 'default=ollama/deepseek-v3,topic=openai/gpt-4';
const result = parseSystemAgent(envValue);
expect(result.topic).toEqual({ provider: 'openai', model: 'gpt-4' });
expect(result.translation).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.agentMeta).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.historyCompress).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.thread).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.queryRewrite).toEqual({
provider: 'ollama',
model: 'deepseek-v3',
enabled: true,
});
});
it('should properly handle priority when topic appears before default in the string', () => {
// 即使 topic 在 default 之前出现topic 的设置仍然应该优先
const envValue = 'topic=openai/gpt-4,default=ollama/deepseek-v3';
const result = parseSystemAgent(envValue);
// topic 应该保持自己的设置而不被 default 覆盖
expect(result.topic).toEqual({ provider: 'openai', model: 'gpt-4' });
// 其他系统智能体应该使用默认配置
expect(result.translation).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.agentMeta).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.historyCompress).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.thread).toEqual({ provider: 'ollama', model: 'deepseek-v3' });
expect(result.queryRewrite).toEqual({
provider: 'ollama',
model: 'deepseek-v3',
enabled: true,
});
});
});

View File

@@ -13,6 +13,9 @@ export const parseSystemAgent = (envString: string = ''): Partial<UserSystemAgen
const pairs = envValue.split(',');
// 用于存储默认设置,如果有 default=provider/model 的情况
let defaultSetting: { model: string; provider: string } | undefined;
for (const pair of pairs) {
const [key, value] = pair.split('=').map((s) => s.trim());
@@ -24,6 +27,15 @@ export const parseSystemAgent = (envString: string = ''): Partial<UserSystemAgen
throw new Error('Missing model or provider value');
}
// 如果是 default 键,保存默认设置
if (key === 'default') {
defaultSetting = {
model: model.trim(),
provider: provider.trim(),
};
continue;
}
if (protectedKeys.includes(key)) {
config[key as keyof UserSystemAgentConfig] = {
enabled: key === 'queryRewrite' ? true : undefined,
@@ -36,5 +48,18 @@ export const parseSystemAgent = (envString: string = ''): Partial<UserSystemAgen
}
}
// 如果有默认设置,应用到所有未设置的系统智能体
if (defaultSetting) {
for (const key of protectedKeys) {
if (!config[key as keyof UserSystemAgentConfig]) {
config[key as keyof UserSystemAgentConfig] = {
enabled: key === 'queryRewrite' ? true : undefined,
model: defaultSetting.model,
provider: defaultSetting.provider,
} as any;
}
}
}
return config;
};

View File

@@ -79,6 +79,7 @@ describe('ChangelogService', () => {
describe('getChangelogIndex', () => {
it('should fetch and merge changelog data', async () => {
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
cloud: [{ id: 'cloud1', date: '2023-01-01', versionRange: ['1.0.0'] }],
community: [{ id: 'community1', date: '2023-01-02', versionRange: ['1.1.0'] }],
@@ -104,6 +105,7 @@ describe('ChangelogService', () => {
it('should return only community items when config type is community', async () => {
service.config.type = 'community';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
cloud: [{ id: 'cloud1', date: '2023-01-01', versionRange: ['1.0.0'] }],
community: [{ id: 'community1', date: '2023-01-02', versionRange: ['1.1.0'] }],

View File

@@ -55,9 +55,13 @@ export class ChangelogService {
next: { revalidate: 3600, tags: [FetchCacheTag.Changelog] },
});
const data = await res.json();
if (res.ok) {
const data = await res.json();
return this.mergeChangelogs(data.cloud, data.community).slice(0, 5);
return this.mergeChangelogs(data.cloud, data.community).slice(0, 5);
}
return [];
} catch (e) {
const cause = (e as Error).cause as { code: string };
if (cause?.code.includes('ETIMEDOUT')) {

View File

@@ -1,4 +1,4 @@
import { dispatch } from '@/utils/electron/dispatch';
import { dispatch } from '@lobechat/electron-client-ipc';
class DevtoolsService {
async openDevtools(): Promise<void> {

View File

@@ -1,10 +0,0 @@
import { DispatchInvoke } from '@lobechat/electron-client-ipc';
/**
* client 端请求 sketch 端 event 数据的方法
*/
export const dispatch: DispatchInvoke = async (event, ...data) => {
if (!window.electronAPI) throw new Error('electronAPI not found');
return window.electronAPI.invoke(event, ...data);
};