feat: support desktop release framework and workflow (#6474)

* add desktop

fix build

update release desktop ci

improve desktop build for pr workflow

update desktop build workflow

test auto updater

fix

fix release nightly channel

support shortcut framework

improve nightly version rule

add zip release

only add mac publish

fix static file relative issue

support delete files

fix lint

enable asar

add setting open in editor in menu

add electron store framework and locale update flow

fix default searchFCModel

refactor the electron server ipc to stable mode

improve electron dev workflow

improve electron build workflow

make qwen2.5b default

improve comment workflow

fix types

refactor code

improve window size of settings/provider

路由拦截器v3.5

fix RouteIntercept issue

improve log

use productName in package.json

update

add pin list for feature flag

update

sure settings update

make ollama as default provider in desktop

fix desktop close page issue

fix desktop default variants

improve to reduce bundle

improve to reduce bundle again

improve set desktop version workflow

add nightly icons

add prebuild scripts to reduce package size

add to test prebuild

fix workflow

try to add sign and notarize for mac in workflow

try to add sign and notarize

add i18n for menu and main

update menu i18n

add i18n framework

add menu implement and setting

improve layout design for desktop

update Author

fix failed register protocol

fix prod building

fix tests

fix open error of mac and windows
improve lint

update pr comment

add service framework

add fileSearchService

improve

fix release workflow

add header

improve pr workflow fetch

improve client fetch

add linux upload workflow

improve workflow and implement

fix build electron in ci

build the desktop framework

fix build electron in ci

update tsconfig

fix desktop build workflow

finish desktop build workflow

fix workflow build steps

update workflow

test release workflow

refactor

update

update

improve loading state

refactor the 404 error

* 重构存储路径,统一到一个 lobehub-storage 下,方便未来用户自定义存储路径

* fix lint

* update

* try to fix windows open issue

* rename

* fix storage

* refactor the remote server sync

* refactor the request method

* 完成服务端同步实现逻辑

* fix lint

* save size

* refactor to make sure different instance of ipc channel

* clean log

* fix refresh

* fix tools calling

* fix auth callback issue

* update workflow

* add window ico

* push

* update

* add beta release

* fix update issue

* 完成官方实例链接

* fix

* fix stdio
This commit is contained in:
Arvin Xu
2025-04-27 11:57:06 +08:00
committed by GitHub
parent 99b45f3245
commit ed97363255
195 changed files with 10918 additions and 381 deletions

View File

@@ -3,6 +3,5 @@ APP_URL=http://localhost:3015
FEATURE_FLAGS=-check_updates,+pin_list
KEY_VAULTS_SECRET=oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=
DATABASE_URL=postgresql://postgres@localhost:5432/postgres
DEFAULT_AGENT_CONFIG="model=qwen2.5;provider=ollama;chatConfig.searchFCModel.provider=ollama;chatConfig.searchFCModel.model=qwen2.5"
SYSTEM_AGENT="default=ollama/qwen2.5"
SEARCH_PROVIDERS=search1api
NEXT_PUBLIC_SERVICE_MODE='server'

View File

@@ -1,9 +1,6 @@
name: Release Desktop
name: Desktop PR Build
on:
# uncomment when official desktop version released
# release:
# types: [published] # 发布 release 时触发构建
pull_request:
types: [synchronize, labeled, unlabeled] # PR 更新或标签变化时触发
@@ -12,6 +9,9 @@ concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
# Add default permissions
permissions: read-all
env:
PR_TAG_PREFIX: pr- # PR 构建版本的前缀标识
@@ -19,10 +19,7 @@ jobs:
test:
name: Code quality check
# 添加 PR label 触发条件,只有添加了 Build Desktop 标签的 PR 才会触发构建
if: |
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'Build Desktop')) ||
github.event_name != 'pull_request'
if: contains(github.event.pull_request.labels.*.name, 'Build Desktop')
runs-on: ubuntu-latest # 只在 ubuntu 上运行一次检查
steps:
- name: Checkout base
@@ -38,7 +35,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
version: 9
- name: Install deps
run: pnpm install
@@ -50,21 +47,14 @@ jobs:
env:
NODE_OPTIONS: --max-old-space-size=6144
# - name: Test
# run: pnpm run test
version:
name: Determine version
# 与 test job 相同的触发条件
if: |
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'Build Desktop')) ||
github.event_name != 'pull_request'
if: contains(github.event.pull_request.labels.*.name, 'Build Desktop')
runs-on: ubuntu-latest
outputs:
# 输出版本信息,供后续 job 使用
version: ${{ steps.set_version.outputs.version }}
is_pr_build: ${{ steps.set_version.outputs.is_pr_build }}
steps:
- uses: actions/checkout@v4
with:
@@ -82,32 +72,13 @@ jobs:
# 从 apps/desktop/package.json 读取基础版本号
base_version=$(node -p "require('./apps/desktop/package.json').version")
if [ "${{ github.event_name }}" == "pull_request" ]; then
# PR 构建:在基础版本号上添加 PR 信息
branch_name="${{ github.head_ref }}"
# 清理分支名,移除非法字符
sanitized_branch=$(echo "${branch_name}" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g')
# 创建特殊的 PR 版本号:基础版本号-PR前缀-分支名-提交哈希
version="${base_version}-${{ env.PR_TAG_PREFIX }}${sanitized_branch}-$(git rev-parse --short HEAD)"
echo "version=${version}" >> $GITHUB_OUTPUT
echo "is_pr_build=true" >> $GITHUB_OUTPUT
echo "📦 Release Version: ${version} (based on base version ${base_version})"
# PR 构建:在基础版本号上添加 PR 信息
pr_number="${{ github.event.pull_request.number }}"
ci_build_number="${{ github.run_number }}" # CI 构建编号
version="${base_version}-nightly.pr${pr_number}.${ci_build_number}"
echo "version=${version}" >> $GITHUB_OUTPUT
echo "📦 Release Version: ${version} (based on base version ${base_version})"
elif [ "${{ github.event_name }}" == "release" ]; then
# Release 事件直接使用 release tag 作为版本号,去掉可能的 v 前缀
version="${{ github.event.release.tag_name }}"
version="${version#v}"
echo "version=${version}" >> $GITHUB_OUTPUT
echo "is_pr_build=false" >> $GITHUB_OUTPUT
echo "📦 Release Version: ${version}"
else
# 其他情况(如手动触发)使用 apps/desktop/package.json 的版本号
version="${base_version}"
echo "version=${version}" >> $GITHUB_OUTPUT
echo "is_pr_build=false" >> $GITHUB_OUTPUT
echo "📦 Release Version: ${version}"
fi
env:
NODE_OPTIONS: --max-old-space-size=6144
@@ -115,7 +86,6 @@ jobs:
- name: Version Summary
run: |
echo "🚦 Release Version: ${{ steps.set_version.outputs.version }}"
echo "🔄 Is PR Build: ${{ steps.set_version.outputs.is_pr_build }}"
build:
needs: [version, test]
@@ -137,43 +107,57 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
version: 9
# node-linker=hoisted 模式将可以确保 asar 压缩可用
- name: Install deps
run: pnpm install
run: pnpm install --node-linker=hoisted
- name: Install deps on Desktop
run: npm run install-isolated --prefix=./apps/desktop
# 设置 package.json 的版本号
- name: Set package version
run: npm run workflow:set-desktop-version ${{ needs.version.outputs.version }}
run: npm run workflow:set-desktop-version ${{ needs.version.outputs.version }} nightly
# macOS 构建处理
- name: Build artifact on macOS
if: runner.os == 'macOS'
run: npm run desktop:build
env:
APP_URL: http://localhost:3010
# 设置更新通道PR构建为nightly否则为stable
UPDATE_CHANNEL: 'nightly'
APP_URL: http://localhost:3015
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
# 默认添加一个加密 SECRET
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
# 公证部分将来再加回
# CSC_LINK: ./build/developer-id-app-certs.p12
# CSC_KEY_PASSWORD: ${{ secrets.APPLE_APP_CERTS_PASSWORD }}
# APPLE_ID: ${{ secrets.APPLE_ID }}
# APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
# macOS 签名和公证配置
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }}
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }}
# allow provisionally
CSC_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# 非 macOS 平台构建处理
- name: Build artifact on other platforms
if: runner.os != 'macOS'
run: npm run desktop:build
env:
APP_URL: http://localhost:3010
# 设置更新通道PR构建为nightly否则为stable
UPDATE_CHANNEL: 'nightly'
APP_URL: http://localhost:3015
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }}
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }}
# 上传构建产物,移除了 zip 相关部分
# 上传构建产物
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
@@ -186,18 +170,21 @@ jobs:
apps/desktop/release/*.AppImage
retention-days: 5
- name: Log build info
run: |
echo "🔄 Is PR Build: ${{ needs.version.outputs.is_pr_build }}"
# 将原本的 merge job 调整,作为所有构建产物的准备步骤
prepare-artifacts:
publish-pr:
needs: [build, version]
name: Prepare Artifacts
name: Publish PR Build
runs-on: ubuntu-latest
# Grant write permissions for creating release and commenting on PR
permissions:
contents: write
pull-requests: write
outputs:
artifact_path: ${{ steps.set_path.outputs.path }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# 下载所有平台的构建产物
- name: Download artifacts
uses: actions/download-artifact@v4
@@ -210,66 +197,6 @@ jobs:
- name: List artifacts
run: ls -R release
# 设置构建产物路径,供后续 job 使用
- name: Set artifact path
id: set_path
run: echo "path=release" >> $GITHUB_OUTPUT
# 正式版发布 job - 只处理 release 触发的场景
publish-release:
# 只在 release 事件触发且不是 PR 构建时执行
if: |
github.event_name == 'release' &&
needs.version.outputs.is_pr_build != 'true'
needs: [prepare-artifacts, version]
name: Publish Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# 下载构建产物
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: ${{ needs.prepare-artifacts.outputs.artifact_path }}
pattern: release-*
merge-multiple: true
# 将构建产物上传到现有 release
- name: Upload to Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.event.release.tag_name }}
files: |
${{ needs.prepare-artifacts.outputs.artifact_path }}/latest*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.dmg*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.zip*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.exe*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# PR 构建的处理步骤
publish-pr:
if: needs.version.outputs.is_pr_build == 'true'
needs: [prepare-artifacts, version]
name: Publish PR Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# 下载构建产物
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: ${{ needs.prepare-artifacts.outputs.artifact_path }}
pattern: release-*
merge-multiple: true
# 生成PR发布描述
- name: Generate PR Release Body
id: pr_release_body
@@ -287,22 +214,22 @@ jobs:
return body;
# 为构建产物创建一个临时发布
- name: Create Temporary Release for PR
id: create_release
uses: softprops/action-gh-release@v1
with:
name: PR Build v${{ needs.version.outputs.version }}
tag_name: pr-build-${{ github.event.pull_request.number }}-${{ github.sha }}
tag_name: v${{ needs.version.outputs.version }}
# tag_name: pr-build-${{ github.event.pull_request.number }}-${{ github.sha }}
body: ${{ steps.pr_release_body.outputs.result }}
draft: false
prerelease: true
files: |
${{ needs.prepare-artifacts.outputs.artifact_path }}/latest*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.dmg*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.zip*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.exe*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.AppImage
release/latest*
release/*.dmg*
release/*.zip*
release/*.exe*
release/*.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -315,17 +242,12 @@ jobs:
const releaseUrl = "${{ steps.create_release.outputs.url }}";
const prCommentGenerator = require('${{ github.workspace }}/.github/scripts/pr-comment.js');
const body = await prCommentGenerator({
const result = await prCommentGenerator({
github,
context,
releaseUrl,
version: "${{ needs.version.outputs.version }}",
tag: "pr-build-${{ github.event.pull_request.number }}-${{ github.sha }}"
tag: "v${{ needs.version.outputs.version }}"
});
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
console.log(`评论状态: ${result.updated ? '已更新' : '已创建'}, ID: ${result.id}`);

View File

@@ -0,0 +1,196 @@
name: Release Desktop
on:
release:
types: [published] # 发布 release 时触发构建
# 确保同一时间只运行一个相同的 workflow取消正在进行的旧的运行
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
# Add default permissions
permissions: read-all
jobs:
test:
name: Code quality check
# 添加 PR label 触发条件,只有添加了 Build Desktop 标签的 PR 才会触发构建
runs-on: ubuntu-latest # 只在 ubuntu 上运行一次检查
steps:
- name: Checkout base
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install deps
run: pnpm install
- name: Lint
run: pnpm run lint
version:
name: Determine version
runs-on: ubuntu-latest
outputs:
# 输出版本信息,供后续 job 使用
version: ${{ steps.set_version.outputs.version }}
is_pr_build: ${{ steps.set_version.outputs.is_pr_build }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
# 主要逻辑:确定构建版本号
- name: Set version
id: set_version
run: |
# 从 apps/desktop/package.json 读取基础版本号
base_version=$(node -p "require('./apps/desktop/package.json').version")
# Release 事件直接使用 release tag 作为版本号,去掉可能的 v 前缀
version="${{ github.event.release.tag_name }}"
version="${version#v}"
echo "version=${version}" >> $GITHUB_OUTPUT
echo "is_pr_build=false" >> $GITHUB_OUTPUT
echo "📦 Release Version: ${version}"
# 输出版本信息总结,方便在 GitHub Actions 界面查看
- name: Version Summary
run: |
echo "🚦 Release Version: ${{ steps.set_version.outputs.version }}"
echo "🔄 Is PR Build: ${{ steps.set_version.outputs.is_pr_build }}"
build:
needs: [version, test]
name: Build Desktop App
runs-on: ${{ matrix.os }}
strategy:
matrix:
# 暂时先支持 macOS
os: [macos-latest]
# os: [macos-latest, windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
# node-linker=hoisted 模式将可以确保 asar 压缩可用
- name: Install deps
run: pnpm install --node-linker=hoisted
- name: Install deps on Desktop
run: npm run install-isolated --prefix=./apps/desktop
# 设置 package.json 的版本号
- name: Set package version
run: npm run workflow:set-desktop-version ${{ needs.version.outputs.version }} beta
# macOS 构建处理
- name: Build artifact on macOS
if: runner.os == 'macOS'
run: npm run desktop:build
env:
UPDATE_CHANNEL: 'stable'
APP_URL: http://localhost:3015
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
# 默认添加一个加密 SECRET
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
# macOS 签名和公证配置
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
# allow provisionally
CSC_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }}
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }}
# 非 macOS 平台构建处理
- name: Build artifact on other platforms
if: runner.os != 'macOS'
run: npm run desktop:build
env:
UPDATE_CHANNEL: 'stable'
APP_URL: http://localhost:3015
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }}
NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }}
# 上传构建产物,移除了 zip 相关部分
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.os }}
path: |
apps/desktop/release/latest*
apps/desktop/release/*.dmg*
apps/desktop/release/*.zip*
apps/desktop/release/*.exe*
apps/desktop/release/*.AppImage
retention-days: 5
# 正式版发布 job
publish-release:
needs: [build, version]
name: Prepare Artifacts
runs-on: ubuntu-latest
# Grant write permission to contents for uploading release assets
permissions:
contents: write
outputs:
artifact_path: ${{ steps.set_path.outputs.path }}
steps:
# 下载所有平台的构建产物
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: release
pattern: release-*
merge-multiple: true
# 列出所有构建产物
- name: List artifacts
run: ls -R release
# 将构建产物上传到现有 release
- name: Upload to Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.event.release.tag_name }}
files: |
${{ needs.prepare-artifacts.outputs.artifact_path }}/latest*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.dmg*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.zip*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.exe*
${{ needs.prepare-artifacts.outputs.artifact_path }}/*.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

8
apps/desktop/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules
dist
out
.DS_Store
.eslintcache
*.log*
standalone
release

31
apps/desktop/.i18nrc.js Normal file
View File

@@ -0,0 +1,31 @@
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
entry: 'resources/locales/zh-CN',
entryLocale: 'zh-CN',
output: 'resources/locales',
outputLocales: [
'ar',
'bg-BG',
'zh-TW',
'en-US',
'ru-RU',
'ja-JP',
'ko-KR',
'fr-FR',
'tr-TR',
'es-ES',
'pt-BR',
'de-DE',
'it-IT',
'nl-NL',
'pl-PL',
'vi-VN',
'fa-IR',
],
temperature: 0,
modelName: 'gpt-4o-mini',
experimental: {
jsonMode: true,
},
});

4
apps/desktop/.npmrc Normal file
View File

@@ -0,0 +1,4 @@
lockfile=false
shamefully-hoist=true
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

View File

@@ -0,0 +1,47 @@
## Menu 实现框架
```
apps/desktop/src/main/
├── core/
│ ├── App.ts // 应用核心类
│ ├── BrowserManager.ts // 浏览器窗口管理
│ └── MenuManager.ts // 新增:菜单管理核心类,负责选择和协调平台实现
├── menus/ // 新增:菜单实现目录
│ ├── index.ts // 导出平台实现和接口
│ ├── types.ts // 定义菜单平台接口 IMenuPlatform
│ └── impl/ // 平台特定实现目录
│ ├── BaseMenuPlatform.ts // 基础平台类注入App
│ ├── DarwinMenu.ts // macOS 充血模型实现
│ ├── WindowsMenu.ts // Windows 充血模型实现
│ └── LinuxMenu.ts // Linux 充血模型实现
├── controllers/
│ └── MenuCtr.ts // 菜单控制器,处理渲染进程调用
```
## i18n
src/main/
├── core/
│ ├── I18nManager.ts //i18n 管理器
│ └── App.ts // 应用主类,集成 i18n
├── locales/
│ ├── index.ts // 导出 i18n 相关功能
│ ├── resources.ts // 资源加载逻辑
│ └── default/ // 默认中文翻译源文件
│ ├── index.ts // 导出所有翻译
│ ├── menu.ts // 菜单翻译
│ ├── dialog.ts // 对话框翻译
│ └── common.ts // 通用翻译
主进程 i18n 国际化管理
使用方式:
1. 直接导入 i18nManager 实例:
import i18nManager from '@/locales';
2. 使用翻译函数:
import {t} from '@/locales';
const translated = t ('key');
3. 添加新翻译:
在 locales/default/ 目录下添加翻译源文件

6
apps/desktop/README.md Normal file
View File

@@ -0,0 +1,6 @@
# LobeHub Desktop
构建路径:
- dist: 构建产物路径
- release: 发布产物路径

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
apps/desktop/build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
apps/desktop/build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

View File

@@ -0,0 +1,6 @@
provider: github
owner: lobehub
repo: lobe-chat
updaterCacheDirName: electron-app-updater
allowPrerelease: true
channel: nightly

View File

@@ -0,0 +1,92 @@
const dotenv = require('dotenv');
dotenv.config();
const packageJSON = require('./package.json');
const channel = process.env.UPDATE_CHANNEL || 'stable';
console.log(`🚄 Build Version ${packageJSON.version}, Channel: ${channel}`);
const isNightly = channel === 'nightly';
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration
*/
const config = {
appId: isNightly ? 'com.lobehub.lobehub-desktop-nightly' : 'com.lobehub.lobehub-desktop',
appImage: {
artifactName: '${productName}-${version}.${ext}',
},
asar: true,
detectUpdateChannel: true,
directories: {
buildResources: 'build',
output: 'release',
},
dmg: {
artifactName: '${productName}-${version}-${arch}.${ext}',
},
electronDownload: {
mirror: 'https://npmmirror.com/mirrors/electron/',
},
files: [
'dist',
'resources',
'!resources/locales',
'!dist/next/docs',
'!dist/next/packages',
'!dist/next/.next/server/app/sitemap',
'!dist/next/.next/static/media',
],
generateUpdatesFilesForAllChannels: true,
linux: {
category: 'Utility',
maintainer: 'electronjs.org',
target: ['AppImage', 'snap', 'deb'],
},
mac: {
compression: 'maximum',
entitlementsInherit: 'build/entitlements.mac.plist',
extendInfo: [
{ NSCameraUsageDescription: "Application requests access to the device's camera." },
{ NSMicrophoneUsageDescription: "Application requests access to the device's microphone." },
{
NSDocumentsFolderUsageDescription:
"Application requests access to the user's Documents folder.",
},
{
NSDownloadsFolderUsageDescription:
"Application requests access to the user's Downloads folder.",
},
],
gatekeeperAssess: false,
hardenedRuntime: true,
notarize: true,
target: [
{ arch: ['x64', 'arm64'], target: 'dmg' },
{ arch: ['x64', 'arm64'], target: 'zip' },
],
},
npmRebuild: true,
nsis: {
artifactName: '${productName}-${version}-setup.${ext}',
createDesktopShortcut: 'always',
// allowToChangeInstallationDirectory: true,
// oneClick: false,
shortcutName: '${productName}',
uninstallDisplayName: '${productName}',
},
publish: [
{
owner: 'lobehub',
provider: 'github',
repo: 'lobe-chat',
},
],
win: {
executableName: 'LobeHub',
},
};
module.exports = config;

View File

@@ -0,0 +1,40 @@
import dotenv from 'dotenv';
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import { resolve } from 'node:path';
dotenv.config();
const updateChannel = process.env.UPDATE_CHANNEL || 'stable';
console.log(`[electron-vite.config.ts] Detected UPDATE_CHANNEL: ${updateChannel}`); // 添加日志确认
export default defineConfig({
main: {
build: {
outDir: 'dist/main',
},
// 这里是关键:在构建时进行文本替换
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.OFFICIAL_CLOUD_SERVER': JSON.stringify(process.env.OFFICIAL_CLOUD_SERVER),
'process.env.UPDATE_CHANNEL': JSON.stringify(process.env.UPDATE_CHANNEL),
},
plugins: [externalizeDepsPlugin({})],
resolve: {
alias: {
'@': resolve(__dirname, 'src/main'),
'~common': resolve(__dirname, 'src/common'),
},
},
},
preload: {
build: {
outDir: 'dist/preload',
},
plugins: [externalizeDepsPlugin({})],
resolve: {
alias: {
'~common': resolve(__dirname, 'src/common'),
},
},
},
});

72
apps/desktop/package.json Normal file
View File

@@ -0,0 +1,72 @@
{
"name": "lobehub-desktop-dev",
"version": "0.0.10",
"description": "LobeHub Desktop Application",
"homepage": "https://lobehub.com",
"repository": {
"type": "git",
"url": "https://github.com/lobehub/lobe-chat.git"
},
"author": "LobeHub",
"main": "./dist/main/index.js",
"scripts": {
"build": "npm run typecheck && electron-vite build",
"build-local": "npm run build && electron-builder --dir --config electron-builder.js --c.mac.notarize=false -c.mac.identity=null --c.asar=false",
"build:linux": "npm run build && electron-builder --linux --config electron-builder.js",
"build:mac": "npm run build && electron-builder --mac --config electron-builder.js",
"build:win": "npm run build && electron-builder --win --config electron-builder.js",
"electron:dev": "electron-vite dev",
"electron:run-unpack": "electron .",
"format": "prettier --write ",
"i18n": "bun run scripts/i18nWorkflow/index.ts && lobe-i18n",
"postinstall": "electron-builder install-app-deps",
"install-isolated": "pnpm install",
"lint": "eslint --cache ",
"pg-server": "bun run scripts/pglite-server.ts",
"start": "electron-vite preview",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"electron-updater": "^6.6.2",
"get-port-please": "^3.1.2",
"pdfjs-dist": "4.8.69"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron-toolkit/utils": "^4.0.0",
"@lobechat/electron-client-ipc": "workspace:*",
"@lobechat/electron-server-ipc": "workspace:*",
"@lobechat/file-loaders": "workspace:*",
"@lobehub/i18n-cli": "^1.20.3",
"@types/lodash": "^4.17.0",
"@types/resolve": "^1.20.6",
"@types/semver": "^7.7.0",
"@types/set-cookie-parser": "^2.4.10",
"consola": "^3.1.0",
"cookie": "^1.0.2",
"electron": "^35.2.0",
"electron-builder": "^26.0.12",
"electron-is": "^3.0.0",
"electron-log": "^5.3.3",
"electron-store": "^8.2.0",
"electron-vite": "^3.0.0",
"execa": "^9.5.2",
"just-diff": "^6.0.2",
"lodash": "^4.17.21",
"pglite-server": "^0.1.4",
"resolve": "^1.22.8",
"semver": "^7.5.4",
"set-cookie-parser": "^2.7.1",
"tsx": "^4.19.3",
"typescript": "^5.7.3",
"vite": "^6.2.5"
},
"pnpm": {
"onlyBuiltDependencies": [
"electron"
]
}
}

View File

@@ -0,0 +1,5 @@
packages:
- '../../packages/electron-server-ipc'
- '../../packages/electron-client-ipc'
- '../../packages/file-loaders'
- '.'

View File

@@ -0,0 +1,136 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LobeHub - 连接错误</title>
<style>
body {
-webkit-app-region: drag;
margin: 0;
padding: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: #1f1f1f;
overflow: hidden;
}
/* 添加暗色模式支持 */
@media (prefers-color-scheme: dark) {
body {
color: #f5f5f5;
background-color: #121212;
}
.error-message {
color: #f5f5f5;
}
.retry-button {
background-color: #2a2a2a;
color: #f5f5f5;
border: 1px solid #3a3a3a;
}
.retry-button:hover {
background-color: #3a3a3a;
}
}
.container {
text-align: center;
padding: 2rem;
max-width: 500px;
}
.lobe-brand {
width: 120px;
height: auto;
margin-bottom: 2rem;
}
.lobe-brand path {
fill: currentcolor;
}
.error-icon {
font-size: 3rem;
margin-bottom: 1rem;
color: #ff4d4f;
}
.error-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
}
.error-message {
margin-bottom: 2rem;
line-height: 1.5;
color: #666;
}
.retry-button {
-webkit-app-region: no-drag;
padding: 0.75rem 1.5rem;
background-color: #f5f5f5;
color: #1f1f1f;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.retry-button:hover {
background-color: #e9e9e9;
}
</style>
</head>
<body>
<div class="container">
<div class="error-icon">⚠️</div>
<h1 class="error-title">Connection Error</h1>
<p class="error-message">
Unable to connect to the application, please check your network connection or confirm if the
development server is running.
</p>
<button id="retry-button" class="retry-button">Retry</button>
</div>
<script>
// 当按钮被点击时,通知主进程重试连接
const retryButton = document.getElementById('retry-button');
const errorMessage = document.querySelector('.error-message');
if (retryButton) {
retryButton.addEventListener('click', () => {
// 更新UI状态
retryButton.disabled = true;
retryButton.textContent = 'Retrying...';
errorMessage.textContent = 'Attempting to reconnect to the next server, please wait...';
// 调用主进程的重试逻辑
if (window.electron && window.electron.ipcRenderer) {
window.electron.ipcRenderer.invoke('retry-connection')
.then((result) => {
if (result && result.success) {
// 连接成功,无需额外操作,页面会自动导航
} else {
// 连接失败,重置按钮状态
setTimeout(() => {
retryButton.disabled = false;
retryButton.textContent = 'Retry';
errorMessage.textContent = 'Unable to connect to the application, please check your network connection or confirm if the development server is running.';
}, 1000);
}
});
}
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "إضافة",
"back": "عودة",
"cancel": "إلغاء",
"close": "إغلاق",
"confirm": "تأكيد",
"delete": "حذف",
"edit": "تعديل",
"more": "المزيد",
"next": "التالي",
"ok": "حسناً",
"previous": "السابق",
"refresh": "تحديث",
"remove": "إزالة",
"retry": "إعادة المحاولة",
"save": "حفظ",
"search": "بحث",
"submit": "إرسال"
},
"app": {
"description": "منصة تعاون مساعدك الذكي",
"name": "LobeHub"
},
"status": {
"error": "خطأ",
"info": "معلومات",
"loading": "جارٍ التحميل",
"success": "نجاح",
"warning": "تحذير"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "تأكيد",
"detail": "تطبيق دردشة يعتمد على نموذج لغة كبير",
"message": "{{appName}} {{appVersion}}",
"title": "حول"
},
"confirm": {
"cancel": "إلغاء",
"no": "لا",
"title": "تأكيد",
"yes": "نعم"
},
"error": {
"button": "تأكيد",
"detail": "حدث خطأ أثناء العملية، يرجى المحاولة لاحقًا",
"message": "حدث خطأ",
"title": "خطأ"
},
"update": {
"downloadAndInstall": "تنزيل وتثبيت",
"downloadComplete": "اكتمل التنزيل",
"downloadCompleteMessage": "تم تنزيل حزمة التحديث، هل ترغب في التثبيت الآن؟",
"installLater": "تثبيت لاحقًا",
"installNow": "تثبيت الآن",
"later": "تذكير لاحقًا",
"newVersion": "تم اكتشاف إصدار جديد",
"newVersionAvailable": "تم اكتشاف إصدار جديد: {{version}}",
"skipThisVersion": "تخطي هذا الإصدار"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "التحقق من التحديثات..."
},
"dev": {
"devPanel": "لوحة المطور",
"devTools": "أدوات المطور",
"forceReload": "إعادة تحميل قسري",
"openStore": "فتح ملف التخزين",
"refreshMenu": "تحديث القائمة",
"reload": "إعادة تحميل",
"title": "تطوير"
},
"edit": {
"copy": "نسخ",
"cut": "قص",
"paste": "لصق",
"redo": "إعادة",
"selectAll": "تحديد الكل",
"speech": "صوت",
"startSpeaking": "بدء القراءة",
"stopSpeaking": "إيقاف القراءة",
"title": "تحرير",
"undo": "تراجع"
},
"file": {
"preferences": "التفضيلات",
"quit": "خروج",
"title": "ملف"
},
"help": {
"about": "حول",
"githubRepo": "مستودع GitHub",
"reportIssue": "الإبلاغ عن مشكلة",
"title": "مساعدة",
"visitWebsite": "زيارة الموقع الرسمي"
},
"macOS": {
"about": "حول {{appName}}",
"devTools": "أدوات مطور LobeHub",
"hide": "إخفاء {{appName}}",
"hideOthers": "إخفاء الآخرين",
"preferences": "إعدادات مفضلة...",
"services": "خدمات",
"unhide": "إظهار الكل"
},
"tray": {
"open": "فتح {{appName}}",
"quit": "خروج",
"show": "عرض {{appName}}"
},
"view": {
"forceReload": "إعادة تحميل قسري",
"reload": "إعادة تحميل",
"resetZoom": "إعادة تعيين التكبير",
"title": "عرض",
"toggleFullscreen": "تبديل وضع ملء الشاشة",
"zoomIn": "تكبير",
"zoomOut": "تصغير"
},
"window": {
"bringAllToFront": "إحضار جميع النوافذ إلى الأمام",
"close": "إغلاق",
"front": "إحضار جميع النوافذ إلى الأمام",
"minimize": "تصغير",
"title": "نافذة",
"toggleFullscreen": "تبديل وضع ملء الشاشة",
"zoom": "تكبير"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Добави",
"back": "Назад",
"cancel": "Отмени",
"close": "Затвори",
"confirm": "Потвърди",
"delete": "Изтрий",
"edit": "Редактирай",
"more": "Повече",
"next": "Следващ",
"ok": "Добре",
"previous": "Предишен",
"refresh": "Освежи",
"remove": "Премахни",
"retry": "Опитай отново",
"save": "Запази",
"search": "Търси",
"submit": "Изпрати"
},
"app": {
"description": "Твоята платформа за сътрудничество с AI асистент",
"name": "LobeHub"
},
"status": {
"error": "Грешка",
"info": "Информация",
"loading": "Зареждане",
"success": "Успех",
"warning": "Предупреждение"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Потвърди",
"detail": "Приложение за чат, базирано на голям езиков модел",
"message": "{{appName}} {{appVersion}}",
"title": "За нас"
},
"confirm": {
"cancel": "Отказ",
"no": "Не",
"title": "Потвърждение",
"yes": "Да"
},
"error": {
"button": "Потвърди",
"detail": "Възникна грешка по време на операцията, моля опитайте отново по-късно",
"message": "Възникна грешка",
"title": "Грешка"
},
"update": {
"downloadAndInstall": "Изтегли и инсталирай",
"downloadComplete": "Изтеглянето е завършено",
"downloadCompleteMessage": "Актуализационният пакет е изтеглен, желаете ли да го инсталирате веднага?",
"installLater": "Инсталирай по-късно",
"installNow": "Инсталирай сега",
"later": "Напомни по-късно",
"newVersion": "Открита нова версия",
"newVersionAvailable": "Открита нова версия: {{version}}",
"skipThisVersion": "Пропусни тази версия"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Проверка за актуализации..."
},
"dev": {
"devPanel": "Панел на разработчика",
"devTools": "Инструменти за разработчици",
"forceReload": "Принудително презареждане",
"openStore": "Отворете файла за съхранение",
"refreshMenu": "Освежаване на менюто",
"reload": "Презареждане",
"title": "Разработка"
},
"edit": {
"copy": "Копиране",
"cut": "Изрязване",
"paste": "Поставяне",
"redo": "Повторно",
"selectAll": "Избери всичко",
"speech": "Глас",
"startSpeaking": "Започни четене",
"stopSpeaking": "Спри четенето",
"title": "Редактиране",
"undo": "Отмяна"
},
"file": {
"preferences": "Предпочитания",
"quit": "Изход",
"title": "Файл"
},
"help": {
"about": "За",
"githubRepo": "GitHub хранилище",
"reportIssue": "Докладвай проблем",
"title": "Помощ",
"visitWebsite": "Посети уебсайта"
},
"macOS": {
"about": "За {{appName}}",
"devTools": "Инструменти за разработчици на LobeHub",
"hide": "Скрий {{appName}}",
"hideOthers": "Скрий другите",
"preferences": "Настройки...",
"services": "Услуги",
"unhide": "Покажи всичко"
},
"tray": {
"open": "Отвори {{appName}}",
"quit": "Изход",
"show": "Покажи {{appName}}"
},
"view": {
"forceReload": "Принудително презареждане",
"reload": "Презареждане",
"resetZoom": "Нулиране на мащаба",
"title": "Изглед",
"toggleFullscreen": "Превключи на цял екран",
"zoomIn": "Увеличи",
"zoomOut": "Намали"
},
"window": {
"bringAllToFront": "Премести всички прозорци напред",
"close": "Затвори",
"front": "Премести всички прозорци напред",
"minimize": "Минимизирай",
"title": "Прозорец",
"toggleFullscreen": "Превключи на цял екран",
"zoom": "Мащаб"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Hinzufügen",
"back": "Zurück",
"cancel": "Abbrechen",
"close": "Schließen",
"confirm": "Bestätigen",
"delete": "Löschen",
"edit": "Bearbeiten",
"more": "Mehr",
"next": "Weiter",
"ok": "OK",
"previous": "Zurück",
"refresh": "Aktualisieren",
"remove": "Entfernen",
"retry": "Erneut versuchen",
"save": "Speichern",
"search": "Suchen",
"submit": "Einreichen"
},
"app": {
"description": "Ihre KI-Assistenten-Kollaborationsplattform",
"name": "LobeHub"
},
"status": {
"error": "Fehler",
"info": "Information",
"loading": "Lädt",
"success": "Erfolg",
"warning": "Warnung"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Bestätigen",
"detail": "Eine Chat-Anwendung, die auf einem großen Sprachmodell basiert",
"message": "{{appName}} {{appVersion}}",
"title": "Über"
},
"confirm": {
"cancel": "Abbrechen",
"no": "Nein",
"title": "Bestätigung",
"yes": "Ja"
},
"error": {
"button": "Bestätigen",
"detail": "Während der Operation ist ein Fehler aufgetreten, bitte versuchen Sie es später erneut",
"message": "Ein Fehler ist aufgetreten",
"title": "Fehler"
},
"update": {
"downloadAndInstall": "Herunterladen und installieren",
"downloadComplete": "Download abgeschlossen",
"downloadCompleteMessage": "Das Update-Paket wurde heruntergeladen, möchten Sie es jetzt installieren?",
"installLater": "Später installieren",
"installNow": "Jetzt installieren",
"later": "Später erinnern",
"newVersion": "Neue Version gefunden",
"newVersionAvailable": "Neue Version verfügbar: {{version}}",
"skipThisVersion": "Diese Version überspringen"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Überprüfen Sie auf Updates..."
},
"dev": {
"devPanel": "Entwicklerpanel",
"devTools": "Entwicklerwerkzeuge",
"forceReload": "Erzwinge Neuladen",
"openStore": "Speicherdatei öffnen",
"refreshMenu": "Menü aktualisieren",
"reload": "Neuladen",
"title": "Entwicklung"
},
"edit": {
"copy": "Kopieren",
"cut": "Ausschneiden",
"paste": "Einfügen",
"redo": "Wiederherstellen",
"selectAll": "Alles auswählen",
"speech": "Sprache",
"startSpeaking": "Beginne zu sprechen",
"stopSpeaking": "Stoppe das Sprechen",
"title": "Bearbeiten",
"undo": "Rückgängig"
},
"file": {
"preferences": "Einstellungen",
"quit": "Beenden",
"title": "Datei"
},
"help": {
"about": "Über",
"githubRepo": "GitHub-Repository",
"reportIssue": "Problem melden",
"title": "Hilfe",
"visitWebsite": "Besuche die Website"
},
"macOS": {
"about": "Über {{appName}}",
"devTools": "LobeHub Entwicklerwerkzeuge",
"hide": "{{appName}} ausblenden",
"hideOthers": "Andere ausblenden",
"preferences": "Einstellungen...",
"services": "Dienste",
"unhide": "Alle anzeigen"
},
"tray": {
"open": "{{appName}} öffnen",
"quit": "Beenden",
"show": "{{appName}} anzeigen"
},
"view": {
"forceReload": "Erzwinge Neuladen",
"reload": "Neuladen",
"resetZoom": "Zoom zurücksetzen",
"title": "Ansicht",
"toggleFullscreen": "Vollbild umschalten",
"zoomIn": "Vergrößern",
"zoomOut": "Verkleinern"
},
"window": {
"bringAllToFront": "Alle Fenster in den Vordergrund bringen",
"close": "Schließen",
"front": "Alle Fenster in den Vordergrund bringen",
"minimize": "Minimieren",
"title": "Fenster",
"toggleFullscreen": "Vollbild umschalten",
"zoom": "Zoom"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Add",
"back": "Back",
"cancel": "Cancel",
"close": "Close",
"confirm": "Confirm",
"delete": "Delete",
"edit": "Edit",
"more": "More",
"next": "Next",
"ok": "OK",
"previous": "Previous",
"refresh": "Refresh",
"remove": "Remove",
"retry": "Retry",
"save": "Save",
"search": "Search",
"submit": "Submit"
},
"app": {
"description": "Your AI Assistant Collaboration Platform",
"name": "LobeHub"
},
"status": {
"error": "Error",
"info": "Information",
"loading": "Loading",
"success": "Success",
"warning": "Warning"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "OK",
"detail": "A chat application based on a large language model",
"message": "{{appName}} {{appVersion}}",
"title": "About"
},
"confirm": {
"cancel": "Cancel",
"no": "No",
"title": "Confirm",
"yes": "Yes"
},
"error": {
"button": "OK",
"detail": "An error occurred during the operation, please try again later",
"message": "An error occurred",
"title": "Error"
},
"update": {
"downloadAndInstall": "Download and Install",
"downloadComplete": "Download Complete",
"downloadCompleteMessage": "The update package has been downloaded, would you like to install it now?",
"installLater": "Install Later",
"installNow": "Install Now",
"later": "Remind Me Later",
"newVersion": "New Version Found",
"newVersionAvailable": "New version available: {{version}}",
"skipThisVersion": "Skip This Version"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Checking for updates..."
},
"dev": {
"devPanel": "Developer Panel",
"devTools": "Developer Tools",
"forceReload": "Force Reload",
"openStore": "Open Storage File",
"refreshMenu": "Refresh menu",
"reload": "Reload",
"title": "Development"
},
"edit": {
"copy": "Copy",
"cut": "Cut",
"paste": "Paste",
"redo": "Redo",
"selectAll": "Select All",
"speech": "Speech",
"startSpeaking": "Start Speaking",
"stopSpeaking": "Stop Speaking",
"title": "Edit",
"undo": "Undo"
},
"file": {
"preferences": "Preferences",
"quit": "Quit",
"title": "File"
},
"help": {
"about": "About",
"githubRepo": "GitHub Repository",
"reportIssue": "Report Issue",
"title": "Help",
"visitWebsite": "Visit Website"
},
"macOS": {
"about": "About {{appName}}",
"devTools": "LobeHub Developer Tools",
"hide": "Hide {{appName}}",
"hideOthers": "Hide Others",
"preferences": "Preferences...",
"services": "Services",
"unhide": "Show All"
},
"tray": {
"open": "Open {{appName}}",
"quit": "Quit",
"show": "Show {{appName}}"
},
"view": {
"forceReload": "Force Reload",
"reload": "Reload",
"resetZoom": "Reset Zoom",
"title": "View",
"toggleFullscreen": "Toggle Fullscreen",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out"
},
"window": {
"bringAllToFront": "Bring All Windows to Front",
"close": "Close",
"front": "Bring All Windows to Front",
"minimize": "Minimize",
"title": "Window",
"toggleFullscreen": "Toggle Fullscreen",
"zoom": "Zoom"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Agregar",
"back": "Volver",
"cancel": "Cancelar",
"close": "Cerrar",
"confirm": "Confirmar",
"delete": "Eliminar",
"edit": "Editar",
"more": "Más",
"next": "Siguiente",
"ok": "Aceptar",
"previous": "Anterior",
"refresh": "Actualizar",
"remove": "Eliminar",
"retry": "Reintentar",
"save": "Guardar",
"search": "Buscar",
"submit": "Enviar"
},
"app": {
"description": "Tu plataforma de colaboración con el asistente de IA",
"name": "LobeHub"
},
"status": {
"error": "Error",
"info": "Información",
"loading": "Cargando",
"success": "Éxito",
"warning": "Advertencia"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Aceptar",
"detail": "Una aplicación de chat basada en un modelo de lenguaje grande",
"message": "{{appName}} {{appVersion}}",
"title": "Acerca de"
},
"confirm": {
"cancel": "Cancelar",
"no": "No",
"title": "Confirmar",
"yes": "Sí"
},
"error": {
"button": "Aceptar",
"detail": "Se produjo un error durante la operación, por favor intente de nuevo más tarde",
"message": "Se produjo un error",
"title": "Error"
},
"update": {
"downloadAndInstall": "Descargar e instalar",
"downloadComplete": "Descarga completada",
"downloadCompleteMessage": "El paquete de actualización se ha descargado, ¿desea instalarlo ahora?",
"installLater": "Instalar más tarde",
"installNow": "Instalar ahora",
"later": "Recordar más tarde",
"newVersion": "Nueva versión disponible",
"newVersionAvailable": "Nueva versión encontrada: {{version}}",
"skipThisVersion": "Saltar esta versión"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Comprobando actualizaciones..."
},
"dev": {
"devPanel": "Panel de desarrollador",
"devTools": "Herramientas de desarrollador",
"forceReload": "Recargar forzosamente",
"openStore": "Abrir archivo de almacenamiento",
"refreshMenu": "Actualizar menú",
"reload": "Recargar",
"title": "Desarrollo"
},
"edit": {
"copy": "Copiar",
"cut": "Cortar",
"paste": "Pegar",
"redo": "Rehacer",
"selectAll": "Seleccionar todo",
"speech": "Voz",
"startSpeaking": "Comenzar a leer en voz alta",
"stopSpeaking": "Detener lectura en voz alta",
"title": "Editar",
"undo": "Deshacer"
},
"file": {
"preferences": "Preferencias",
"quit": "Salir",
"title": "Archivo"
},
"help": {
"about": "Acerca de",
"githubRepo": "Repositorio de GitHub",
"reportIssue": "Reportar un problema",
"title": "Ayuda",
"visitWebsite": "Visitar el sitio web"
},
"macOS": {
"about": "Acerca de {{appName}}",
"devTools": "Herramientas de desarrollador de LobeHub",
"hide": "Ocultar {{appName}}",
"hideOthers": "Ocultar otros",
"preferences": "Configuración...",
"services": "Servicios",
"unhide": "Mostrar todo"
},
"tray": {
"open": "Abrir {{appName}}",
"quit": "Salir",
"show": "Mostrar {{appName}}"
},
"view": {
"forceReload": "Recargar forzosamente",
"reload": "Recargar",
"resetZoom": "Restablecer zoom",
"title": "Vista",
"toggleFullscreen": "Alternar pantalla completa",
"zoomIn": "Acercar",
"zoomOut": "Alejar"
},
"window": {
"bringAllToFront": "Traer todas las ventanas al frente",
"close": "Cerrar",
"front": "Traer todas las ventanas al frente",
"minimize": "Minimizar",
"title": "Ventana",
"toggleFullscreen": "Alternar pantalla completa",
"zoom": "Zoom"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "افزودن",
"back": "بازگشت",
"cancel": "لغو",
"close": "بستن",
"confirm": "تأیید",
"delete": "حذف",
"edit": "ویرایش",
"more": "بیشتر",
"next": "مرحله بعد",
"ok": "تأیید",
"previous": "مرحله قبل",
"refresh": "به‌روزرسانی",
"remove": "حذف",
"retry": "تلاش مجدد",
"save": "ذخیره",
"search": "جستجو",
"submit": "ارسال"
},
"app": {
"description": "پلتفرم همکاری دستیار هوش مصنوعی شما",
"name": "LobeHub"
},
"status": {
"error": "خطا",
"info": "اطلاعات",
"loading": "در حال بارگذاری",
"success": "موفق",
"warning": "هشدار"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "تأیید",
"detail": "یک برنامه چت مبتنی بر مدل‌های زبانی بزرگ",
"message": "{{appName}} {{appVersion}}",
"title": "درباره"
},
"confirm": {
"cancel": "لغو",
"no": "خیر",
"title": "تأیید",
"yes": "بله"
},
"error": {
"button": "تأیید",
"detail": "در حین انجام عملیات خطایی رخ داده است، لطفاً بعداً دوباره تلاش کنید",
"message": "خطا رخ داده است",
"title": "خطا"
},
"update": {
"downloadAndInstall": "دانلود و نصب",
"downloadComplete": "دانلود کامل شد",
"downloadCompleteMessage": "بسته به‌روزرسانی دانلود شده است، آیا می‌خواهید بلافاصله نصب کنید؟",
"installLater": "نصب بعداً",
"installNow": "نصب اکنون",
"later": "یادآوری بعداً",
"newVersion": "نسخه جدیدی پیدا شد",
"newVersionAvailable": "نسخه جدید پیدا شد: {{version}}",
"skipThisVersion": "این نسخه را نادیده بگیرید"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "بررسی به‌روزرسانی..."
},
"dev": {
"devPanel": "پنل توسعه‌دهنده",
"devTools": "ابزارهای توسعه‌دهنده",
"forceReload": "بارگذاری اجباری",
"openStore": "باز کردن فایل‌های ذخیره شده",
"refreshMenu": "به‌روزرسانی منو",
"reload": "بارگذاری مجدد",
"title": "توسعه"
},
"edit": {
"copy": "کپی",
"cut": "برش",
"paste": "چسباندن",
"redo": "انجام مجدد",
"selectAll": "انتخاب همه",
"speech": "گفتار",
"startSpeaking": "شروع به خواندن",
"stopSpeaking": "متوقف کردن خواندن",
"title": "ویرایش",
"undo": "بازگشت"
},
"file": {
"preferences": "تنظیمات",
"quit": "خروج",
"title": "فایل"
},
"help": {
"about": "درباره",
"githubRepo": "مخزن GitHub",
"reportIssue": "گزارش مشکل",
"title": "کمک",
"visitWebsite": "بازدید از وب‌سایت"
},
"macOS": {
"about": "درباره {{appName}}",
"devTools": "ابزارهای توسعه‌دهنده LobeHub",
"hide": "پنهان کردن {{appName}}",
"hideOthers": "پنهان کردن دیگران",
"preferences": "تنظیمات...",
"services": "خدمات",
"unhide": "نمایش همه"
},
"tray": {
"open": "باز کردن {{appName}}",
"quit": "خروج",
"show": "نمایش {{appName}}"
},
"view": {
"forceReload": "بارگذاری اجباری",
"reload": "بارگذاری مجدد",
"resetZoom": "تنظیم زوم به حالت اولیه",
"title": "نمایش",
"toggleFullscreen": "تغییر به حالت تمام صفحه",
"zoomIn": "بزرگ‌نمایی",
"zoomOut": "کوچک‌نمایی"
},
"window": {
"bringAllToFront": "همه پنجره‌ها را به جلو بیاورید",
"close": "بستن",
"front": "همه پنجره‌ها را به جلو بیاورید",
"minimize": "کوچک کردن",
"title": "پنجره",
"toggleFullscreen": "تغییر به حالت تمام صفحه",
"zoom": "زوم"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Ajouter",
"back": "Retour",
"cancel": "Annuler",
"close": "Fermer",
"confirm": "Confirmer",
"delete": "Supprimer",
"edit": "Éditer",
"more": "Plus",
"next": "Suivant",
"ok": "D'accord",
"previous": "Précédent",
"refresh": "Rafraîchir",
"remove": "Retirer",
"retry": "Réessayer",
"save": "Enregistrer",
"search": "Rechercher",
"submit": "Soumettre"
},
"app": {
"description": "Votre plateforme de collaboration avec l'assistant IA",
"name": "LobeHub"
},
"status": {
"error": "Erreur",
"info": "Information",
"loading": "Chargement",
"success": "Succès",
"warning": "Avertissement"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "D'accord",
"detail": "Une application de chat basée sur un grand modèle de langage",
"message": "{{appName}} {{appVersion}}",
"title": "À propos"
},
"confirm": {
"cancel": "Annuler",
"no": "Non",
"title": "Confirmer",
"yes": "Oui"
},
"error": {
"button": "D'accord",
"detail": "Une erreur s'est produite lors de l'opération, veuillez réessayer plus tard",
"message": "Une erreur s'est produite",
"title": "Erreur"
},
"update": {
"downloadAndInstall": "Télécharger et installer",
"downloadComplete": "Téléchargement terminé",
"downloadCompleteMessage": "Le paquet de mise à jour a été téléchargé, souhaitez-vous l'installer maintenant ?",
"installLater": "Installer plus tard",
"installNow": "Installer maintenant",
"later": "Rappeler plus tard",
"newVersion": "Nouvelle version détectée",
"newVersionAvailable": "Nouvelle version disponible : {{version}}",
"skipThisVersion": "Ignorer cette version"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Vérifier les mises à jour..."
},
"dev": {
"devPanel": "Panneau de développement",
"devTools": "Outils de développement",
"forceReload": "Recharger de force",
"openStore": "Ouvrir le fichier de stockage",
"refreshMenu": "Rafraîchir le menu",
"reload": "Recharger",
"title": "Développement"
},
"edit": {
"copy": "Copier",
"cut": "Couper",
"paste": "Coller",
"redo": "Rétablir",
"selectAll": "Tout sélectionner",
"speech": "Voix",
"startSpeaking": "Commencer à lire",
"stopSpeaking": "Arrêter de lire",
"title": "Édition",
"undo": "Annuler"
},
"file": {
"preferences": "Préférences",
"quit": "Quitter",
"title": "Fichier"
},
"help": {
"about": "À propos",
"githubRepo": "Dépôt GitHub",
"reportIssue": "Signaler un problème",
"title": "Aide",
"visitWebsite": "Visiter le site officiel"
},
"macOS": {
"about": "À propos de {{appName}}",
"devTools": "Outils de développement LobeHub",
"hide": "Masquer {{appName}}",
"hideOthers": "Masquer les autres",
"preferences": "Préférences...",
"services": "Services",
"unhide": "Tout afficher"
},
"tray": {
"open": "Ouvrir {{appName}}",
"quit": "Quitter",
"show": "Afficher {{appName}}"
},
"view": {
"forceReload": "Recharger de force",
"reload": "Recharger",
"resetZoom": "Réinitialiser le zoom",
"title": "Affichage",
"toggleFullscreen": "Basculer en plein écran",
"zoomIn": "Zoomer",
"zoomOut": "Dézoomer"
},
"window": {
"bringAllToFront": "Mettre toutes les fenêtres au premier plan",
"close": "Fermer",
"front": "Mettre toutes les fenêtres au premier plan",
"minimize": "Réduire",
"title": "Fenêtre",
"toggleFullscreen": "Basculer en plein écran",
"zoom": "Zoom"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Aggiungi",
"back": "Indietro",
"cancel": "Annulla",
"close": "Chiudi",
"confirm": "Conferma",
"delete": "Elimina",
"edit": "Modifica",
"more": "Di più",
"next": "Avanti",
"ok": "OK",
"previous": "Indietro",
"refresh": "Aggiorna",
"remove": "Rimuovi",
"retry": "Riprova",
"save": "Salva",
"search": "Cerca",
"submit": "Invia"
},
"app": {
"description": "La tua piattaforma di collaborazione con assistente AI",
"name": "LobeHub"
},
"status": {
"error": "Errore",
"info": "Informazioni",
"loading": "Caricamento in corso",
"success": "Successo",
"warning": "Avviso"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Conferma",
"detail": "Un'app di chat basata su un grande modello linguistico",
"message": "{{appName}} {{appVersion}}",
"title": "Informazioni"
},
"confirm": {
"cancel": "Annulla",
"no": "No",
"title": "Conferma",
"yes": "Sì"
},
"error": {
"button": "Conferma",
"detail": "Si è verificato un errore durante l'operazione, riprovare più tardi",
"message": "Si è verificato un errore",
"title": "Errore"
},
"update": {
"downloadAndInstall": "Scarica e installa",
"downloadComplete": "Download completato",
"downloadCompleteMessage": "Il pacchetto di aggiornamento è stato scaricato, vuoi installarlo subito?",
"installLater": "Installa più tardi",
"installNow": "Installa ora",
"later": "Promemoria più tardi",
"newVersion": "Nuova versione disponibile",
"newVersionAvailable": "Nuova versione trovata: {{version}}",
"skipThisVersion": "Salta questa versione"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Controlla aggiornamenti..."
},
"dev": {
"devPanel": "Pannello sviluppatore",
"devTools": "Strumenti per sviluppatori",
"forceReload": "Ricarica forzata",
"openStore": "Apri il file di archiviazione",
"refreshMenu": "Aggiorna menu",
"reload": "Ricarica",
"title": "Sviluppo"
},
"edit": {
"copy": "Copia",
"cut": "Taglia",
"paste": "Incolla",
"redo": "Ripeti",
"selectAll": "Seleziona tutto",
"speech": "Voce",
"startSpeaking": "Inizia a leggere",
"stopSpeaking": "Ferma la lettura",
"title": "Modifica",
"undo": "Annulla"
},
"file": {
"preferences": "Preferenze",
"quit": "Esci",
"title": "File"
},
"help": {
"about": "Informazioni",
"githubRepo": "Repository GitHub",
"reportIssue": "Segnala un problema",
"title": "Aiuto",
"visitWebsite": "Visita il sito ufficiale"
},
"macOS": {
"about": "Informazioni su {{appName}}",
"devTools": "Strumenti per sviluppatori LobeHub",
"hide": "Nascondi {{appName}}",
"hideOthers": "Nascondi altri",
"preferences": "Impostazioni...",
"services": "Servizi",
"unhide": "Mostra tutto"
},
"tray": {
"open": "Apri {{appName}}",
"quit": "Esci",
"show": "Mostra {{appName}}"
},
"view": {
"forceReload": "Ricarica forzata",
"reload": "Ricarica",
"resetZoom": "Reimposta zoom",
"title": "Visualizza",
"toggleFullscreen": "Attiva/disattiva schermo intero",
"zoomIn": "Ingrandisci",
"zoomOut": "Riduci"
},
"window": {
"bringAllToFront": "Porta tutte le finestre in primo piano",
"close": "Chiudi",
"front": "Porta tutte le finestre in primo piano",
"minimize": "Minimizza",
"title": "Finestra",
"toggleFullscreen": "Attiva/disattiva schermo intero",
"zoom": "Zoom"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "追加",
"back": "戻る",
"cancel": "キャンセル",
"close": "閉じる",
"confirm": "確認",
"delete": "削除",
"edit": "編集",
"more": "もっと見る",
"next": "次へ",
"ok": "OK",
"previous": "前へ",
"refresh": "更新",
"remove": "削除",
"retry": "再試行",
"save": "保存",
"search": "検索",
"submit": "送信"
},
"app": {
"description": "あなたのAIアシスタント協力プラットフォーム",
"name": "LobeHub"
},
"status": {
"error": "エラー",
"info": "情報",
"loading": "読み込み中",
"success": "成功",
"warning": "警告"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "確定",
"detail": "大規模言語モデルに基づくチャットアプリ",
"message": "{{appName}} {{appVersion}}",
"title": "について"
},
"confirm": {
"cancel": "キャンセル",
"no": "いいえ",
"title": "確認",
"yes": "はい"
},
"error": {
"button": "確定",
"detail": "操作中にエラーが発生しました。後で再試行してください。",
"message": "エラーが発生しました",
"title": "エラー"
},
"update": {
"downloadAndInstall": "ダウンロードしてインストール",
"downloadComplete": "ダウンロード完了",
"downloadCompleteMessage": "更新パッケージのダウンロードが完了しました。今すぐインストールしますか?",
"installLater": "後でインストール",
"installNow": "今すぐインストール",
"later": "後でリマインド",
"newVersion": "新しいバージョンが見つかりました",
"newVersionAvailable": "新しいバージョンが見つかりました: {{version}}",
"skipThisVersion": "このバージョンをスキップ"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "更新を確認しています..."
},
"dev": {
"devPanel": "開発者パネル",
"devTools": "開発者ツール",
"forceReload": "強制再読み込み",
"openStore": "ストレージファイルを開く",
"refreshMenu": "メニューを更新",
"reload": "再読み込み",
"title": "開発"
},
"edit": {
"copy": "コピー",
"cut": "切り取り",
"paste": "貼り付け",
"redo": "やり直し",
"selectAll": "すべて選択",
"speech": "音声",
"startSpeaking": "読み上げ開始",
"stopSpeaking": "読み上げ停止",
"title": "編集",
"undo": "元に戻す"
},
"file": {
"preferences": "設定",
"quit": "終了",
"title": "ファイル"
},
"help": {
"about": "について",
"githubRepo": "GitHub リポジトリ",
"reportIssue": "問題を報告",
"title": "ヘルプ",
"visitWebsite": "公式ウェブサイトを訪問"
},
"macOS": {
"about": "{{appName}} について",
"devTools": "LobeHub 開発者ツール",
"hide": "{{appName}} を隠す",
"hideOthers": "他を隠す",
"preferences": "環境設定...",
"services": "サービス",
"unhide": "すべて表示"
},
"tray": {
"open": "{{appName}} を開く",
"quit": "終了",
"show": "{{appName}} を表示"
},
"view": {
"forceReload": "強制再読み込み",
"reload": "再読み込み",
"resetZoom": "ズームをリセット",
"title": "ビュー",
"toggleFullscreen": "フルスクリーン切替",
"zoomIn": "ズームイン",
"zoomOut": "ズームアウト"
},
"window": {
"bringAllToFront": "すべてのウィンドウを前面に",
"close": "閉じる",
"front": "すべてのウィンドウを前面に",
"minimize": "最小化",
"title": "ウィンドウ",
"toggleFullscreen": "フルスクリーン切替",
"zoom": "ズーム"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "추가",
"back": "뒤로",
"cancel": "취소",
"close": "닫기",
"confirm": "확인",
"delete": "삭제",
"edit": "편집",
"more": "더보기",
"next": "다음",
"ok": "확인",
"previous": "이전",
"refresh": "새로 고침",
"remove": "제거",
"retry": "다시 시도",
"save": "저장",
"search": "검색",
"submit": "제출"
},
"app": {
"description": "당신의 AI 비서 협업 플랫폼",
"name": "LobeHub"
},
"status": {
"error": "오류",
"info": "정보",
"loading": "로딩 중",
"success": "성공",
"warning": "경고"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "확인",
"detail": "대형 언어 모델 기반의 채팅 애플리케이션",
"message": "{{appName}} {{appVersion}}",
"title": "정보"
},
"confirm": {
"cancel": "취소",
"no": "아니요",
"title": "확인",
"yes": "예"
},
"error": {
"button": "확인",
"detail": "작업 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
"message": "오류 발생",
"title": "오류"
},
"update": {
"downloadAndInstall": "다운로드 및 설치",
"downloadComplete": "다운로드 완료",
"downloadCompleteMessage": "업데이트 패키지가 다운로드 완료되었습니다. 지금 설치하시겠습니까?",
"installLater": "나중에 설치",
"installNow": "지금 설치",
"later": "나중에 알림",
"newVersion": "새 버전 발견",
"newVersionAvailable": "새 버전 발견: {{version}}",
"skipThisVersion": "이 버전 건너뛰기"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "업데이트 확인 중..."
},
"dev": {
"devPanel": "개발자 패널",
"devTools": "개발자 도구",
"forceReload": "강제 새로 고침",
"openStore": "저장 파일 열기",
"refreshMenu": "메뉴 새로 고침",
"reload": "새로 고침",
"title": "개발"
},
"edit": {
"copy": "복사",
"cut": "잘라내기",
"paste": "붙여넣기",
"redo": "다시 실행",
"selectAll": "모두 선택",
"speech": "음성",
"startSpeaking": "읽기 시작",
"stopSpeaking": "읽기 중지",
"title": "편집",
"undo": "실행 취소"
},
"file": {
"preferences": "환경 설정",
"quit": "종료",
"title": "파일"
},
"help": {
"about": "정보",
"githubRepo": "GitHub 저장소",
"reportIssue": "문제 보고",
"title": "도움말",
"visitWebsite": "웹사이트 방문"
},
"macOS": {
"about": "{{appName}} 정보",
"devTools": "LobeHub 개발자 도구",
"hide": "{{appName}} 숨기기",
"hideOthers": "다른 것 숨기기",
"preferences": "환경 설정...",
"services": "서비스",
"unhide": "모두 표시"
},
"tray": {
"open": "{{appName}} 열기",
"quit": "종료",
"show": "{{appName}} 표시"
},
"view": {
"forceReload": "강제 새로 고침",
"reload": "새로 고침",
"resetZoom": "줌 초기화",
"title": "보기",
"toggleFullscreen": "전체 화면 전환",
"zoomIn": "확대",
"zoomOut": "축소"
},
"window": {
"bringAllToFront": "모든 창 앞으로 가져오기",
"close": "닫기",
"front": "모든 창 앞으로 가져오기",
"minimize": "최소화",
"title": "창",
"toggleFullscreen": "전체 화면 전환",
"zoom": "줌"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Toevoegen",
"back": "Terug",
"cancel": "Annuleren",
"close": "Sluiten",
"confirm": "Bevestigen",
"delete": "Verwijderen",
"edit": "Bewerken",
"more": "Meer",
"next": "Volgende stap",
"ok": "OK",
"previous": "Vorige stap",
"refresh": "Vernieuwen",
"remove": "Verwijderen",
"retry": "Opnieuw proberen",
"save": "Opslaan",
"search": "Zoeken",
"submit": "Indienen"
},
"app": {
"description": "Jouw AI-assistent samenwerkingsplatform",
"name": "LobeHub"
},
"status": {
"error": "Fout",
"info": "Informatie",
"loading": "Laden",
"success": "Succes",
"warning": "Waarschuwing"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Bevestigen",
"detail": "Een chatapplicatie gebaseerd op een groot taalmodel",
"message": "{{appName}} {{appVersion}}",
"title": "Over"
},
"confirm": {
"cancel": "Annuleren",
"no": "Nee",
"title": "Bevestigen",
"yes": "Ja"
},
"error": {
"button": "Bevestigen",
"detail": "Er is een fout opgetreden tijdens de operatie, probeer het later opnieuw",
"message": "Er is een fout opgetreden",
"title": "Fout"
},
"update": {
"downloadAndInstall": "Downloaden en installeren",
"downloadComplete": "Download voltooid",
"downloadCompleteMessage": "Het updatepakket is gedownload, wilt u het nu installeren?",
"installLater": "Later installeren",
"installNow": "Nu installeren",
"later": "Later herinneren",
"newVersion": "Nieuwe versie gevonden",
"newVersionAvailable": "Nieuwe versie beschikbaar: {{version}}",
"skipThisVersion": "Deze versie overslaan"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Updates controleren..."
},
"dev": {
"devPanel": "Ontwikkelaarspaneel",
"devTools": "Ontwikkelaarstools",
"forceReload": "Forceer herladen",
"openStore": "Open opslagbestand",
"refreshMenu": "Menu verversen",
"reload": "Herladen",
"title": "Ontwikkeling"
},
"edit": {
"copy": "Kopiëren",
"cut": "Knippen",
"paste": "Plakken",
"redo": "Opnieuw doen",
"selectAll": "Alles selecteren",
"speech": "Spraak",
"startSpeaking": "Begin met voorlezen",
"stopSpeaking": "Stop met voorlezen",
"title": "Bewerken",
"undo": "Ongedaan maken"
},
"file": {
"preferences": "Voorkeuren",
"quit": "Afsluiten",
"title": "Bestand"
},
"help": {
"about": "Over",
"githubRepo": "GitHub-repo",
"reportIssue": "Probleem melden",
"title": "Hulp",
"visitWebsite": "Bezoek de website"
},
"macOS": {
"about": "Over {{appName}}",
"devTools": "LobeHub Ontwikkelaarstools",
"hide": "Verberg {{appName}}",
"hideOthers": "Verberg anderen",
"preferences": "Voorkeuren...",
"services": "Diensten",
"unhide": "Toon alles"
},
"tray": {
"open": "Open {{appName}}",
"quit": "Afsluiten",
"show": "Toon {{appName}}"
},
"view": {
"forceReload": "Forceer herladen",
"reload": "Herladen",
"resetZoom": "Zoom resetten",
"title": "Weergave",
"toggleFullscreen": "Schakel volledig scherm in/uit",
"zoomIn": "Inzoomen",
"zoomOut": "Uitzoomen"
},
"window": {
"bringAllToFront": "Breng alle vensters naar voren",
"close": "Sluiten",
"front": "Breng alle vensters naar voren",
"minimize": "Minimaliseren",
"title": "Venster",
"toggleFullscreen": "Schakel volledig scherm in/uit",
"zoom": "Inzoomen"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Dodaj",
"back": "Wstecz",
"cancel": "Anuluj",
"close": "Zamknij",
"confirm": "Potwierdź",
"delete": "Usuń",
"edit": "Edytuj",
"more": "Więcej",
"next": "Dalej",
"ok": "OK",
"previous": "Cofnij",
"refresh": "Odśwież",
"remove": "Usuń",
"retry": "Spróbuj ponownie",
"save": "Zapisz",
"search": "Szukaj",
"submit": "Wyślij"
},
"app": {
"description": "Twoja platforma współpracy z asystentem AI",
"name": "LobeHub"
},
"status": {
"error": "Błąd",
"info": "Informacja",
"loading": "Ładowanie",
"success": "Sukces",
"warning": "Ostrzeżenie"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "OK",
"detail": "Aplikacja czatu oparta na dużym modelu językowym",
"message": "{{appName}} {{appVersion}}",
"title": "O aplikacji"
},
"confirm": {
"cancel": "Anuluj",
"no": "Nie",
"title": "Potwierdzenie",
"yes": "Tak"
},
"error": {
"button": "OK",
"detail": "Wystąpił błąd podczas operacji, spróbuj ponownie później",
"message": "Wystąpił błąd",
"title": "Błąd"
},
"update": {
"downloadAndInstall": "Pobierz i zainstaluj",
"downloadComplete": "Pobieranie zakończone",
"downloadCompleteMessage": "Pakiet aktualizacji został pobrany, czy chcesz go teraz zainstalować?",
"installLater": "Zainstaluj później",
"installNow": "Zainstaluj teraz",
"later": "Przypomnij później",
"newVersion": "Nowa wersja dostępna",
"newVersionAvailable": "Znaleziono nową wersję: {{version}}",
"skipThisVersion": "Pomiń tę wersję"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Sprawdzanie aktualizacji..."
},
"dev": {
"devPanel": "Panel dewelopera",
"devTools": "Narzędzia dewelopera",
"forceReload": "Wymuś ponowne załadowanie",
"openStore": "Otwórz plik magazynu",
"refreshMenu": "Odśwież menu",
"reload": "Przeładuj",
"title": "Rozwój"
},
"edit": {
"copy": "Kopiuj",
"cut": "Wytnij",
"paste": "Wklej",
"redo": "Ponów",
"selectAll": "Zaznacz wszystko",
"speech": "Mowa",
"startSpeaking": "Rozpocznij czytanie",
"stopSpeaking": "Zatrzymaj czytanie",
"title": "Edycja",
"undo": "Cofnij"
},
"file": {
"preferences": "Preferencje",
"quit": "Zakończ",
"title": "Plik"
},
"help": {
"about": "O",
"githubRepo": "Repozytorium GitHub",
"reportIssue": "Zgłoś problem",
"title": "Pomoc",
"visitWebsite": "Odwiedź stronę internetową"
},
"macOS": {
"about": "O {{appName}}",
"devTools": "Narzędzia dewelopera LobeHub",
"hide": "Ukryj {{appName}}",
"hideOthers": "Ukryj inne",
"preferences": "Ustawienia...",
"services": "Usługi",
"unhide": "Pokaż wszystko"
},
"tray": {
"open": "Otwórz {{appName}}",
"quit": "Zakończ",
"show": "Pokaż {{appName}}"
},
"view": {
"forceReload": "Wymuś ponowne załadowanie",
"reload": "Przeładuj",
"resetZoom": "Zresetuj powiększenie",
"title": "Widok",
"toggleFullscreen": "Przełącz tryb pełnoekranowy",
"zoomIn": "Powiększ",
"zoomOut": "Pomniejsz"
},
"window": {
"bringAllToFront": "Przenieś wszystkie okna na wierzch",
"close": "Zamknij",
"front": "Przenieś wszystkie okna na wierzch",
"minimize": "Zminimalizuj",
"title": "Okno",
"toggleFullscreen": "Przełącz tryb pełnoekranowy",
"zoom": "Powiększenie"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Adicionar",
"back": "Voltar",
"cancel": "Cancelar",
"close": "Fechar",
"confirm": "Confirmar",
"delete": "Excluir",
"edit": "Editar",
"more": "Mais",
"next": "Próximo",
"ok": "OK",
"previous": "Anterior",
"refresh": "Atualizar",
"remove": "Remover",
"retry": "Tentar novamente",
"save": "Salvar",
"search": "Pesquisar",
"submit": "Enviar"
},
"app": {
"description": "Sua plataforma de colaboração com assistente de IA",
"name": "LobeHub"
},
"status": {
"error": "Erro",
"info": "Informação",
"loading": "Carregando",
"success": "Sucesso",
"warning": "Aviso"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Confirmar",
"detail": "Um aplicativo de chat baseado em um grande modelo de linguagem",
"message": "{{appName}} {{appVersion}}",
"title": "Sobre"
},
"confirm": {
"cancel": "Cancelar",
"no": "Não",
"title": "Confirmar",
"yes": "Sim"
},
"error": {
"button": "Confirmar",
"detail": "Ocorreu um erro durante a operação, por favor tente novamente mais tarde",
"message": "Ocorreu um erro",
"title": "Erro"
},
"update": {
"downloadAndInstall": "Baixar e instalar",
"downloadComplete": "Download completo",
"downloadCompleteMessage": "O pacote de atualização foi baixado com sucesso, deseja instalá-lo agora?",
"installLater": "Instalar depois",
"installNow": "Instalar agora",
"later": "Lembrar mais tarde",
"newVersion": "Nova versão disponível",
"newVersionAvailable": "Nova versão encontrada: {{version}}",
"skipThisVersion": "Ignorar esta versão"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Verificando atualizações..."
},
"dev": {
"devPanel": "Painel do Desenvolvedor",
"devTools": "Ferramentas do Desenvolvedor",
"forceReload": "Recarregar Forçadamente",
"openStore": "Abrir arquivo de armazenamento",
"refreshMenu": "Atualizar menu",
"reload": "Recarregar",
"title": "Desenvolvimento"
},
"edit": {
"copy": "Copiar",
"cut": "Cortar",
"paste": "Colar",
"redo": "Refazer",
"selectAll": "Selecionar Tudo",
"speech": "Fala",
"startSpeaking": "Começar a Ler",
"stopSpeaking": "Parar de Ler",
"title": "Edição",
"undo": "Desfazer"
},
"file": {
"preferences": "Preferências",
"quit": "Sair",
"title": "Arquivo"
},
"help": {
"about": "Sobre",
"githubRepo": "Repositório do GitHub",
"reportIssue": "Reportar Problema",
"title": "Ajuda",
"visitWebsite": "Visitar o Site"
},
"macOS": {
"about": "Sobre {{appName}}",
"devTools": "Ferramentas do Desenvolvedor LobeHub",
"hide": "Ocultar {{appName}}",
"hideOthers": "Ocultar Outros",
"preferences": "Configurações...",
"services": "Serviços",
"unhide": "Mostrar Todos"
},
"tray": {
"open": "Abrir {{appName}}",
"quit": "Sair",
"show": "Mostrar {{appName}}"
},
"view": {
"forceReload": "Recarregar Forçadamente",
"reload": "Recarregar",
"resetZoom": "Redefinir Zoom",
"title": "Visualização",
"toggleFullscreen": "Alternar Tela Cheia",
"zoomIn": "Aumentar",
"zoomOut": "Diminuir"
},
"window": {
"bringAllToFront": "Trazer Todas as Janelas para Frente",
"close": "Fechar",
"front": "Trazer Todas as Janelas para Frente",
"minimize": "Minimizar",
"title": "Janela",
"toggleFullscreen": "Alternar Tela Cheia",
"zoom": "Zoom"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Добавить",
"back": "Назад",
"cancel": "Отмена",
"close": "Закрыть",
"confirm": "Подтвердить",
"delete": "Удалить",
"edit": "Редактировать",
"more": "Больше",
"next": "Далее",
"ok": "ОК",
"previous": "Назад",
"refresh": "Обновить",
"remove": "Удалить",
"retry": "Повторить",
"save": "Сохранить",
"search": "Поиск",
"submit": "Отправить"
},
"app": {
"description": "Ваша платформа для совместной работы с ИИ",
"name": "LobeHub"
},
"status": {
"error": "Ошибка",
"info": "Информация",
"loading": "Загрузка",
"success": "Успех",
"warning": "Предупреждение"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Подтвердить",
"detail": "Приложение для чата на основе большой языковой модели",
"message": "{{appName}} {{appVersion}}",
"title": "О приложении"
},
"confirm": {
"cancel": "Отмена",
"no": "Нет",
"title": "Подтверждение",
"yes": "Да"
},
"error": {
"button": "Подтвердить",
"detail": "Произошла ошибка во время операции, пожалуйста, попробуйте позже",
"message": "Произошла ошибка",
"title": "Ошибка"
},
"update": {
"downloadAndInstall": "Скачать и установить",
"downloadComplete": "Скачивание завершено",
"downloadCompleteMessage": "Обновление загружено, хотите установить сейчас?",
"installLater": "Установить позже",
"installNow": "Установить сейчас",
"later": "Напомнить позже",
"newVersion": "Обнаружена новая версия",
"newVersionAvailable": "Обнаружена новая версия: {{version}}",
"skipThisVersion": "Пропустить эту версию"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Проверка обновлений..."
},
"dev": {
"devPanel": "Панель разработчика",
"devTools": "Инструменты разработчика",
"forceReload": "Принудительная перезагрузка",
"openStore": "Открыть файл хранилища",
"refreshMenu": "Обновить меню",
"reload": "Перезагрузить",
"title": "Разработка"
},
"edit": {
"copy": "Копировать",
"cut": "Вырезать",
"paste": "Вставить",
"redo": "Повторить",
"selectAll": "Выбрать все",
"speech": "Речь",
"startSpeaking": "Начать чтение",
"stopSpeaking": "Остановить чтение",
"title": "Редактирование",
"undo": "Отменить"
},
"file": {
"preferences": "Настройки",
"quit": "Выйти",
"title": "Файл"
},
"help": {
"about": "О программе",
"githubRepo": "Репозиторий GitHub",
"reportIssue": "Сообщить о проблеме",
"title": "Помощь",
"visitWebsite": "Посетить сайт"
},
"macOS": {
"about": "О {{appName}}",
"devTools": "Инструменты разработчика LobeHub",
"hide": "Скрыть {{appName}}",
"hideOthers": "Скрыть другие",
"preferences": "Настройки...",
"services": "Сервисы",
"unhide": "Показать все"
},
"tray": {
"open": "Открыть {{appName}}",
"quit": "Выйти",
"show": "Показать {{appName}}"
},
"view": {
"forceReload": "Принудительная перезагрузка",
"reload": "Перезагрузить",
"resetZoom": "Сбросить масштаб",
"title": "Вид",
"toggleFullscreen": "Переключить полноэкранный режим",
"zoomIn": "Увеличить",
"zoomOut": "Уменьшить"
},
"window": {
"bringAllToFront": "Вывести все окна на передний план",
"close": "Закрыть",
"front": "Вывести все окна на передний план",
"minimize": "Свернуть",
"title": "Окно",
"toggleFullscreen": "Переключить полноэкранный режим",
"zoom": "Масштаб"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Ekle",
"back": "Geri",
"cancel": "İptal",
"close": "Kapat",
"confirm": "Onayla",
"delete": "Sil",
"edit": "Düzenle",
"more": "Daha Fazla",
"next": "Sonraki",
"ok": "Tamam",
"previous": "Önceki",
"refresh": "Yenile",
"remove": "Kaldır",
"retry": "Yeniden Dene",
"save": "Kaydet",
"search": "Ara",
"submit": "Gönder"
},
"app": {
"description": "AI asistanınız için işbirliği platformu",
"name": "LobeHub"
},
"status": {
"error": "Hata",
"info": "Bilgi",
"loading": "Yükleniyor",
"success": "Başarılı",
"warning": "Uyarı"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Tamam",
"detail": "Büyük dil modeli tabanlı bir sohbet uygulaması",
"message": "{{appName}} {{appVersion}}",
"title": "Hakkında"
},
"confirm": {
"cancel": "İptal",
"no": "Hayır",
"title": "Onay",
"yes": "Evet"
},
"error": {
"button": "Tamam",
"detail": "İşlem sırasında bir hata oluştu, lütfen daha sonra tekrar deneyin",
"message": "Hata oluştu",
"title": "Hata"
},
"update": {
"downloadAndInstall": "İndir ve Yükle",
"downloadComplete": "İndirme tamamlandı",
"downloadCompleteMessage": "Güncelleme paketi indirildi, hemen yüklemek ister misiniz?",
"installLater": "Sonra yükle",
"installNow": "Şimdi yükle",
"later": "Sonra hatırlat",
"newVersion": "Yeni sürüm bulundu",
"newVersionAvailable": "Yeni sürüm bulundu: {{version}}",
"skipThisVersion": "Bu sürümü atla"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Güncellemeleri kontrol et..."
},
"dev": {
"devPanel": "Geliştirici Paneli",
"devTools": "Geliştirici Araçları",
"forceReload": "Zorla Yenile",
"openStore": "Depolama dosyasını aç",
"refreshMenu": "Menüyü yenile",
"reload": "Yenile",
"title": "Geliştir"
},
"edit": {
"copy": "Kopyala",
"cut": "Kes",
"paste": "Yapıştır",
"redo": "Yinele",
"selectAll": "Tümünü Seç",
"speech": "Ses",
"startSpeaking": "Okumaya Başla",
"stopSpeaking": "Okumayı Durdur",
"title": "Düzenle",
"undo": "Geri Al"
},
"file": {
"preferences": "Tercihler",
"quit": ık",
"title": "Dosya"
},
"help": {
"about": "Hakkında",
"githubRepo": "GitHub Deposu",
"reportIssue": "Sorun Bildir",
"title": "Yardım",
"visitWebsite": "Resmi Web Sitesini Ziyaret Et"
},
"macOS": {
"about": "{{appName}} Hakkında",
"devTools": "LobeHub Geliştirici Araçları",
"hide": "{{appName}}'i Gizle",
"hideOthers": "Diğerlerini Gizle",
"preferences": "Tercihler...",
"services": "Hizmetler",
"unhide": "Hepsini Göster"
},
"tray": {
"open": "{{appName}}'i Aç",
"quit": ık",
"show": "{{appName}}'i Göster"
},
"view": {
"forceReload": "Zorla Yenile",
"reload": "Yenile",
"resetZoom": "Yakınlaştırmayı Sıfırla",
"title": "Görünüm",
"toggleFullscreen": "Tam Ekrana Geç",
"zoomIn": "Büyüt",
"zoomOut": "Küçült"
},
"window": {
"bringAllToFront": "Tüm Pencereleri Öne Getir",
"close": "Kapat",
"front": "Tüm Pencereleri Öne Getir",
"minimize": "Küçült",
"title": "Pencere",
"toggleFullscreen": "Tam Ekrana Geç",
"zoom": "Yakınlaştır"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "Thêm",
"back": "Quay lại",
"cancel": "Hủy",
"close": "Đóng",
"confirm": "Xác nhận",
"delete": "Xóa",
"edit": "Chỉnh sửa",
"more": "Thêm nữa",
"next": "Tiếp theo",
"ok": "Đồng ý",
"previous": "Quay lại",
"refresh": "Tải lại",
"remove": "Gỡ bỏ",
"retry": "Thử lại",
"save": "Lưu",
"search": "Tìm kiếm",
"submit": "Gửi"
},
"app": {
"description": "Nền tảng hợp tác trợ lý AI của bạn",
"name": "LobeHub"
},
"status": {
"error": "Lỗi",
"info": "Thông tin",
"loading": "Đang tải",
"success": "Thành công",
"warning": "Cảnh báo"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "Xác nhận",
"detail": "Một ứng dụng trò chuyện dựa trên mô hình ngôn ngữ lớn",
"message": "{{appName}} {{appVersion}}",
"title": "Về"
},
"confirm": {
"cancel": "Hủy",
"no": "Không",
"title": "Xác nhận",
"yes": "Có"
},
"error": {
"button": "Xác nhận",
"detail": "Đã xảy ra lỗi trong quá trình thực hiện, vui lòng thử lại sau",
"message": "Đã xảy ra lỗi",
"title": "Lỗi"
},
"update": {
"downloadAndInstall": "Tải xuống và cài đặt",
"downloadComplete": "Tải xuống hoàn tất",
"downloadCompleteMessage": "Gói cập nhật đã tải xuống hoàn tất, có muốn cài đặt ngay không?",
"installLater": "Cài đặt sau",
"installNow": "Cài đặt ngay",
"later": "Nhắc nhở sau",
"newVersion": "Phát hiện phiên bản mới",
"newVersionAvailable": "Phát hiện phiên bản mới: {{version}}",
"skipThisVersion": "Bỏ qua phiên bản này"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "Kiểm tra cập nhật..."
},
"dev": {
"devPanel": "Bảng điều khiển nhà phát triển",
"devTools": "Công cụ phát triển",
"forceReload": "Tải lại cưỡng bức",
"openStore": "Mở tệp lưu trữ",
"refreshMenu": "Làm mới menu",
"reload": "Tải lại",
"title": "Phát triển"
},
"edit": {
"copy": "Sao chép",
"cut": "Cắt",
"paste": "Dán",
"redo": "Làm lại",
"selectAll": "Chọn tất cả",
"speech": "Giọng nói",
"startSpeaking": "Bắt đầu đọc",
"stopSpeaking": "Dừng đọc",
"title": "Chỉnh sửa",
"undo": "Hoàn tác"
},
"file": {
"preferences": "Tùy chọn",
"quit": "Thoát",
"title": "Tập tin"
},
"help": {
"about": "Về",
"githubRepo": "Kho lưu trữ GitHub",
"reportIssue": "Báo cáo sự cố",
"title": "Trợ giúp",
"visitWebsite": "Truy cập trang web"
},
"macOS": {
"about": "Về {{appName}}",
"devTools": "Công cụ phát triển LobeHub",
"hide": "Ẩn {{appName}}",
"hideOthers": "Ẩn khác",
"preferences": "Cài đặt ưu tiên...",
"services": "Dịch vụ",
"unhide": "Hiện tất cả"
},
"tray": {
"open": "Mở {{appName}}",
"quit": "Thoát",
"show": "Hiện {{appName}}"
},
"view": {
"forceReload": "Tải lại cưỡng bức",
"reload": "Tải lại",
"resetZoom": "Đặt lại thu phóng",
"title": "Xem",
"toggleFullscreen": "Chuyển đổi toàn màn hình",
"zoomIn": "Phóng to",
"zoomOut": "Thu nhỏ"
},
"window": {
"bringAllToFront": "Đưa tất cả cửa sổ lên trước",
"close": "Đóng",
"front": "Đưa tất cả cửa sổ lên trước",
"minimize": "Thu nhỏ",
"title": "Cửa sổ",
"toggleFullscreen": "Chuyển đổi toàn màn hình",
"zoom": "Thu phóng"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "添加",
"back": "返回",
"cancel": "取消",
"close": "关闭",
"confirm": "确认",
"delete": "删除",
"edit": "编辑",
"more": "更多",
"next": "下一步",
"ok": "确定",
"previous": "上一步",
"refresh": "刷新",
"remove": "移除",
"retry": "重试",
"save": "保存",
"search": "搜索",
"submit": "提交"
},
"app": {
"description": "你的 AI 助手协作平台",
"name": "LobeHub"
},
"status": {
"error": "错误",
"info": "信息",
"loading": "加载中",
"success": "成功",
"warning": "警告"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "确定",
"detail": "一个基于大语言模型的聊天应用",
"message": "{{appName}} {{appVersion}}",
"title": "关于"
},
"confirm": {
"cancel": "取消",
"no": "否",
"title": "确认",
"yes": "是"
},
"error": {
"button": "确定",
"detail": "操作过程中发生错误,请稍后重试",
"message": "发生错误",
"title": "错误"
},
"update": {
"downloadAndInstall": "下载并安装",
"downloadComplete": "下载完成",
"downloadCompleteMessage": "更新包已下载完成,是否立即安装?",
"installLater": "稍后安装",
"installNow": "立即安装",
"later": "稍后提醒",
"newVersion": "发现新版本",
"newVersionAvailable": "发现新版本: {{version}}",
"skipThisVersion": "跳过此版本"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "检查更新..."
},
"dev": {
"devPanel": "开发者面板",
"devTools": "开发者工具",
"forceReload": "强制重新加载",
"openStore": "打开存储文件",
"refreshMenu": "刷新菜单",
"reload": "重新加载",
"title": "开发"
},
"edit": {
"copy": "复制",
"cut": "剪切",
"paste": "粘贴",
"redo": "重做",
"selectAll": "全选",
"speech": "语音",
"startSpeaking": "开始朗读",
"stopSpeaking": "停止朗读",
"title": "编辑",
"undo": "撤销"
},
"file": {
"preferences": "首选项",
"quit": "退出",
"title": "文件"
},
"help": {
"about": "关于",
"githubRepo": "GitHub 仓库",
"reportIssue": "报告问题",
"title": "帮助",
"visitWebsite": "访问官网"
},
"macOS": {
"about": "关于 {{appName}}",
"devTools": "LobeHub 开发者工具",
"hide": "隐藏 {{appName}}",
"hideOthers": "隐藏其他",
"preferences": "偏好设置...",
"services": "服务",
"unhide": "全部显示"
},
"tray": {
"open": "打开 {{appName}}",
"quit": "退出",
"show": "显示 {{appName}}"
},
"view": {
"forceReload": "强制重新加载",
"reload": "重新加载",
"resetZoom": "重置缩放",
"title": "视图",
"toggleFullscreen": "切换全屏",
"zoomIn": "放大",
"zoomOut": "缩小"
},
"window": {
"bringAllToFront": "前置所有窗口",
"close": "关闭",
"front": "前置所有窗口",
"minimize": "最小化",
"title": "窗口",
"toggleFullscreen": "切换全屏",
"zoom": "缩放"
}
}

View File

@@ -0,0 +1,32 @@
{
"actions": {
"add": "新增",
"back": "返回",
"cancel": "取消",
"close": "關閉",
"confirm": "確認",
"delete": "刪除",
"edit": "編輯",
"more": "更多",
"next": "下一步",
"ok": "確定",
"previous": "上一步",
"refresh": "刷新",
"remove": "移除",
"retry": "重試",
"save": "儲存",
"search": "搜尋",
"submit": "提交"
},
"app": {
"description": "你的 AI 助手協作平台",
"name": "LobeHub"
},
"status": {
"error": "錯誤",
"info": "資訊",
"loading": "載入中",
"success": "成功",
"warning": "警告"
}
}

View File

@@ -0,0 +1,31 @@
{
"about": {
"button": "確定",
"detail": "一個基於大語言模型的聊天應用",
"message": "{{appName}} {{appVersion}}",
"title": "關於"
},
"confirm": {
"cancel": "取消",
"no": "否",
"title": "確認",
"yes": "是"
},
"error": {
"button": "確定",
"detail": "操作過程中發生錯誤,請稍後重試",
"message": "發生錯誤",
"title": "錯誤"
},
"update": {
"downloadAndInstall": "下載並安裝",
"downloadComplete": "下載完成",
"downloadCompleteMessage": "更新包已下載完成,是否立即安裝?",
"installLater": "稍後安裝",
"installNow": "立即安裝",
"later": "稍後提醒",
"newVersion": "發現新版本",
"newVersionAvailable": "發現新版本: {{version}}",
"skipThisVersion": "跳過此版本"
}
}

View File

@@ -0,0 +1,70 @@
{
"common": {
"checkUpdates": "檢查更新..."
},
"dev": {
"devPanel": "開發者面板",
"devTools": "開發者工具",
"forceReload": "強制重新載入",
"openStore": "打開儲存檔案",
"refreshMenu": "刷新選單",
"reload": "重新載入",
"title": "開發"
},
"edit": {
"copy": "複製",
"cut": "剪下",
"paste": "貼上",
"redo": "重做",
"selectAll": "全選",
"speech": "語音",
"startSpeaking": "開始朗讀",
"stopSpeaking": "停止朗讀",
"title": "編輯",
"undo": "撤銷"
},
"file": {
"preferences": "偏好設定",
"quit": "退出",
"title": "檔案"
},
"help": {
"about": "關於",
"githubRepo": "GitHub 倉庫",
"reportIssue": "報告問題",
"title": "幫助",
"visitWebsite": "訪問網站"
},
"macOS": {
"about": "關於 {{appName}}",
"devTools": "LobeHub 開發者工具",
"hide": "隱藏 {{appName}}",
"hideOthers": "隱藏其他",
"preferences": "偏好設定...",
"services": "服務",
"unhide": "全部顯示"
},
"tray": {
"open": "打開 {{appName}}",
"quit": "退出",
"show": "顯示 {{appName}}"
},
"view": {
"forceReload": "強制重新載入",
"reload": "重新載入",
"resetZoom": "重置縮放",
"title": "視圖",
"toggleFullscreen": "切換全螢幕",
"zoomIn": "放大",
"zoomOut": "縮小"
},
"window": {
"bringAllToFront": "前置所有視窗",
"close": "關閉",
"front": "前置所有視窗",
"minimize": "最小化",
"title": "視窗",
"toggleFullscreen": "切換全螢幕",
"zoom": "縮放"
}
}

View File

@@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LobeHub</title>
<style>
body {
-webkit-app-region: drag;
margin: 0;
padding: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: #1f1f1f;
overflow: hidden;
}
/* 添加暗色模式支持 */
@media (prefers-color-scheme: dark) {
body {
color: #f5f5f5;
}
}
.container {
text-align: center;
}
.lobe-brand-loading {
width: 120px;
height: auto;
}
.lobe-brand-loading path {
fill: currentcolor;
fill-opacity: 0%;
stroke: currentcolor;
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
stroke-width: 0.25em;
animation:
draw 2s cubic-bezier(0.4, 0, 0.2, 1) infinite,
fill 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes draw {
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: 0;
}
}
@keyframes fill {
30% {
fill-opacity: 5%;
}
100% {
fill-opacity: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<svg
class="lobe-brand-loading"
fill="currentColor"
fillRule="evenodd"
viewBox="0 0 940 320"
xmlns="http://www.w3.org/2000/svg"
>
<title>LobeHub</title>
<path
d="M15 240.035V87.172h39.24V205.75h66.192v34.285H15zM183.731 242c-11.759 0-22.196-2.621-31.313-7.862-9.116-5.241-16.317-12.447-21.601-21.619-5.153-9.317-7.729-19.945-7.729-31.883 0-11.937 2.576-22.492 7.729-31.664 5.164-8.963 12.159-15.98 20.982-21.05l.619-.351c9.117-5.241 19.554-7.861 31.313-7.861s22.196 2.62 31.313 7.861c9.248 5.096 16.449 12.229 21.601 21.401 5.153 9.172 7.729 19.727 7.729 31.664 0 11.938-2.576 22.566-7.729 31.883-5.152 9.172-12.353 16.378-21.601 21.619-9.117 5.241-19.554 7.862-31.313 7.862zm0-32.975c4.36 0 8.191-1.092 11.494-3.275 3.436-2.184 6.144-5.387 8.126-9.609 1.982-4.367 2.973-9.536 2.973-15.505 0-5.968-.991-10.991-2.973-15.067-1.906-4.06-4.483-7.177-7.733-9.352l-.393-.257c-3.303-2.184-7.134-3.276-11.494-3.276-4.228 0-8.059 1.092-11.495 3.276-3.303 2.184-6.011 5.387-8.125 9.609-1.982 4.076-2.973 9.099-2.973 15.067 0 5.969.991 11.138 2.973 15.505 2.114 4.222 4.822 7.425 8.125 9.609 3.436 2.183 7.267 3.275 11.495 3.275zM295.508 78l-.001 54.042a34.071 34.071 0 016.541-5.781c6.474-4.367 14.269-6.551 23.385-6.551 9.777 0 18.629 2.475 26.557 7.424 7.872 4.835 14.105 11.684 18.7 20.546l.325.637c4.756 9.026 7.135 19.799 7.135 32.319 0 12.666-2.379 23.585-7.135 32.757-4.624 9.026-10.966 16.087-19.025 21.182-7.928 4.95-16.78 7.425-26.557 7.425-9.644 0-17.704-2.184-24.178-6.551-2.825-1.946-5.336-4.355-7.532-7.226l.001 11.812h-35.87V78h37.654zm21.998 74.684c-4.228 0-8.059 1.092-11.494 3.276-3.303 2.184-6.012 5.387-8.126 9.609-1.982 4.076-2.972 9.099-2.972 15.067 0 5.969.99 11.138 2.972 15.505 2.114 4.222 4.823 7.425 8.126 9.609 3.435 2.183 7.266 3.275 11.494 3.275s7.994-1.092 11.297-3.275c3.435-2.184 6.143-5.387 8.125-9.609 2.114-4.367 3.171-9.536 3.171-15.505 0-5.968-1.057-10.991-3.171-15.067-1.906-4.06-4.483-7.177-7.732-9.352l-.393-.257c-3.303-2.184-7.069-3.276-11.297-3.276zm105.335 38.653l.084.337a27.857 27.857 0 002.057 5.559c2.246 4.222 5.417 7.498 9.513 9.827 4.096 2.184 8.984 3.276 14.665 3.276 5.285 0 9.777-.801 13.477-2.403 3.579-1.632 7.1-4.025 10.564-7.182l.732-.679 19.818 22.711c-5.153 6.26-11.494 11.064-19.025 14.413-7.531 3.203-16.449 4.804-26.755 4.804-12.683 0-23.782-2.621-33.294-7.862-9.381-5.386-16.713-12.665-21.998-21.837-5.153-9.317-7.729-19.872-7.729-31.665 0-11.792 2.51-22.274 7.53-31.446 5.036-9.105 11.902-16.195 20.596-21.268l.61-.351c8.984-5.241 19.091-7.861 30.322-7.861 10.311 0 19.743 2.286 28.294 6.859l.64.347c8.72 4.659 15.656 11.574 20.809 20.746 5.153 9.172 7.729 20.309 7.729 33.411 0 1.294-.052 2.761-.156 4.4l-.042.623-.17 2.353c-.075 1.01-.151 1.973-.227 2.888h-78.044zm21.365-42.147c-4.492 0-8.456 1.092-11.891 3.276-3.303 2.184-5.879 5.314-7.729 9.39a26.04 26.04 0 00-1.117 2.79 30.164 30.164 0 00-1.121 4.499l-.058.354h43.96l-.015-.106c-.401-2.638-1.122-5.055-2.163-7.252l-.246-.503c-1.776-3.774-4.282-6.742-7.519-8.906l-.409-.266c-3.303-2.184-7.2-3.276-11.692-3.276zm111.695-62.018l-.001 57.432h53.51V87.172h39.24v152.863h-39.24v-59.617H555.9l.001 59.617h-39.24V87.172h39.24zM715.766 242c-8.72 0-16.581-1.893-23.583-5.678-6.87-3.785-12.287-9.681-16.251-17.688-3.832-8.153-5.747-18.417-5.747-30.791v-66.168h37.654v59.398c0 9.172 1.519 15.723 4.558 19.654 3.171 3.931 7.597 5.896 13.278 5.896 3.7 0 7.069-.946 10.108-2.839 3.038-1.892 5.483-4.877 7.332-8.953 1.85-4.222 2.775-9.609 2.775-16.16v-56.996h37.654v118.36h-35.871l.004-12.38c-2.642 3.197-5.682 5.868-9.12 8.012-7.002 4.222-14.599 6.333-22.791 6.333zM841.489 78l-.001 54.041a34.1 34.1 0 016.541-5.78c6.474-4.367 14.269-6.551 23.385-6.551 9.777 0 18.629 2.475 26.556 7.424 7.873 4.835 14.106 11.684 18.701 20.546l.325.637c4.756 9.026 7.134 19.799 7.134 32.319 0 12.666-2.378 23.585-7.134 32.757-4.624 9.026-10.966 16.087-19.026 21.182-7.927 4.95-16.779 7.425-26.556 7.425-9.645 0-17.704-2.184-24.178-6.551-2.825-1.946-5.336-4.354-7.531-7.224v11.81h-35.87V78h37.654zm21.998 74.684c-4.228 0-8.059 1.092-11.495 3.276-3.303 2.184-6.011 5.387-8.125 9.609-1.982 4.076-2.973 9.099-2.973 15.067 0 5.969.991 11.138 2.973 15.505 2.114 4.222 4.822 7.425 8.125 9.609 3.436 2.183 7.267 3.275 11.495 3.275 4.228 0 7.993-1.092 11.296-3.275 3.435-2.184 6.144-5.387 8.126-9.609 2.114-4.367 3.171-9.536 3.171-15.505 0-5.968-1.057-10.991-3.171-15.067-1.906-4.06-4.484-7.177-7.733-9.352l-.393-.257c-3.303-2.184-7.068-3.276-11.296-3.276z"
/>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,18 @@
import { readdirSync } from 'node:fs';
import { resolve } from 'node:path';
import i18nConfig from '../../.i18nrc';
export const root = resolve(__dirname, '../..');
export const localesDir = resolve(root, i18nConfig.output);
export const localeDir = (locale: string) => resolve(localesDir, locale);
export const localeDirJsonList = (locale: string) =>
readdirSync(localeDir(locale)).filter((name) => name.includes('.json'));
export const srcLocalesDir = resolve(root, './src/main/locales');
export const entryLocaleJsonFilepath = (file: string) =>
resolve(localesDir, i18nConfig.entryLocale, file);
export const outputLocaleJsonFilepath = (locale: string, file: string) =>
resolve(localesDir, locale, file);
export const srcDefaultLocales = resolve(root, srcLocalesDir, 'default');
export { default as i18nConfig } from '../../.i18nrc';

View File

@@ -0,0 +1,35 @@
import { consola } from 'consola';
import { colors } from 'consola/utils';
import { existsSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { entryLocaleJsonFilepath, i18nConfig, localeDir, srcDefaultLocales } from './const';
import { tagWhite, writeJSON } from './utils';
export const genDefaultLocale = () => {
consola.info(`默认语言为 ${i18nConfig.entryLocale}...`);
// 确保入口语言目录存在
const entryLocaleDir = localeDir(i18nConfig.entryLocale);
if (!existsSync(entryLocaleDir)) {
mkdirSync(entryLocaleDir, { recursive: true });
consola.info(`创建目录:${entryLocaleDir}`);
}
const resources = require(srcDefaultLocales);
const data = Object.entries(resources.default);
consola.start(`生成默认语言 JSON 文件,发现 ${data.length} 个命名空间...`);
for (const [ns, value] of data) {
const filepath = entryLocaleJsonFilepath(`${ns}.json`);
// 确保目录存在
const dir = dirname(filepath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeJSON(filepath, value);
consola.success(tagWhite(ns), colors.gray(filepath));
}
};

View File

@@ -0,0 +1,57 @@
import { consola } from 'consola';
import { colors } from 'consola/utils';
import { diff } from 'just-diff';
import { unset } from 'lodash';
import { existsSync } from 'node:fs';
import {
entryLocaleJsonFilepath,
i18nConfig,
outputLocaleJsonFilepath,
srcDefaultLocales,
} from './const';
import { readJSON, tagWhite, writeJSON } from './utils';
export const genDiff = () => {
consola.start(`对比开发与生产环境中的本地化文件...`);
const resources = require(srcDefaultLocales);
const data = Object.entries(resources.default);
for (const [ns, devJSON] of data) {
const filepath = entryLocaleJsonFilepath(`${ns}.json`);
if (!existsSync(filepath)) {
consola.info(`文件不存在,跳过:${filepath}`);
continue;
}
const prodJSON = readJSON(filepath);
const diffResult = diff(prodJSON, devJSON as any);
const remove = diffResult.filter((item) => item.op === 'remove');
if (remove.length === 0) {
consola.success(tagWhite(ns), colors.gray(filepath));
continue;
}
const clearLocals = [];
for (const locale of [i18nConfig.entryLocale, ...i18nConfig.outputLocales]) {
const localeFilepath = outputLocaleJsonFilepath(locale, `${ns}.json`);
if (!existsSync(localeFilepath)) continue;
const localeJSON = readJSON(localeFilepath);
for (const item of remove) {
unset(localeJSON, item.path);
}
writeJSON(localeFilepath, localeJSON);
clearLocals.push(locale);
}
if (clearLocals.length > 0) {
consola.info('清理了以下语言的过期项目:', clearLocals.join(', '));
}
consola.success(tagWhite(ns), colors.gray(filepath));
}
};

View File

@@ -0,0 +1,35 @@
import { existsSync, mkdirSync } from 'node:fs';
import { i18nConfig, localeDir } from './const';
import { genDefaultLocale } from './genDefaultLocale';
import { genDiff } from './genDiff';
import { split } from './utils';
// 确保所有语言目录存在
const ensureLocalesDirs = () => {
[i18nConfig.entryLocale, ...i18nConfig.outputLocales].forEach((locale) => {
const dir = localeDir(locale);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
});
};
// 运行工作流
const run = async () => {
// 确保目录存在
ensureLocalesDirs();
// 差异分析
split('差异分析');
genDiff();
// 生成默认语言文件
split('生成默认语言文件');
genDefaultLocale();
// 生成国际化文件
split('生成国际化文件');
};
run();

View File

@@ -0,0 +1,54 @@
import { consola } from 'consola';
import { colors } from 'consola/utils';
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import i18nConfig from '../../.i18nrc';
export const readJSON = (filePath: string) => {
const data = readFileSync(filePath, 'utf8');
return JSON.parse(data);
};
export const writeJSON = (filePath: string, data: any) => {
const jsonStr = JSON.stringify(data, null, 2);
writeFileSync(filePath, jsonStr, 'utf8');
};
export const genResourcesContent = (locales: string[]) => {
let index = '';
let indexObj = '';
for (const locale of locales) {
index += `import ${locale} from "./${locale}";\n`;
indexObj += ` "${locale.replace('_', '-')}": ${locale},\n`;
}
return `${index}
const resources = {
${indexObj}} as const;
export default resources;
export const defaultResources = ${i18nConfig.entryLocale};
export type Resources = typeof resources;
export type DefaultResources = typeof defaultResources;
export type Namespaces = keyof DefaultResources;
export type Locales = keyof Resources;
`;
};
export const genNamespaceList = (files: string[], locale: string) => {
return files.map((file) => ({
name: file.replace('.json', ''),
path: resolve(i18nConfig.output, locale, file),
}));
};
export const tagBlue = (text: string) => colors.bgBlueBright(colors.black(` ${text} `));
export const tagYellow = (text: string) => colors.bgYellowBright(colors.black(` ${text} `));
export const tagGreen = (text: string) => colors.bgGreenBright(colors.black(` ${text} `));
export const tagWhite = (text: string) => colors.bgWhiteBright(colors.black(` ${text} `));
export const split = (name: string) => {
consola.log('');
consola.log(colors.gray(`========================== ${name} ==============================`));
};

View File

@@ -0,0 +1,14 @@
import { PGlite } from "@electric-sql/pglite";
import { createServer } from "pglite-server";
// 创建或连接到您现有的 PGlite 数据库
const db = new PGlite("/Users/arvinxx/Library/Application Support/lobehub-desktop/lobehub-local-db");
await db.waitReady;
// 创建服务器并监听端口
const PORT = 6543;
const pgServer = createServer(db);
pgServer.listen(PORT, () => {
console.log(`PGlite 服务器已启动,监听端口 ${PORT}`);
});

View File

@@ -0,0 +1,78 @@
/**
* 路由拦截类型,描述拦截路由和目标窗口的映射关系
*/
export interface RouteInterceptConfig {
/**
* 是否始终在新窗口中打开,即使目标窗口已经存在
*/
alwaysOpenNew?: boolean;
/**
* 描述
*/
description: string;
/**
* 是否启用拦截
*/
enabled: boolean;
/**
* 路由模式前缀,例如 '/settings'
*/
pathPrefix: string;
/**
* 目标窗口标识符
*/
targetWindow: string;
}
/**
* 拦截路由配置列表
* 定义了所有需要特殊处理的路由
*/
export const interceptRoutes: RouteInterceptConfig[] = [
{
description: '设置页面',
enabled: true,
pathPrefix: '/settings',
targetWindow: 'settings',
},
{
description: '开发者工具',
enabled: true,
pathPrefix: '/desktop/devtools',
targetWindow: 'devtools',
},
// 未来可能的其他路由
// {
// description: '帮助中心',
// enabled: true,
// pathPrefix: '/help',
// targetWindow: 'help',
// },
];
/**
* 通过路径查找匹配的路由拦截配置
* @param path 需要检查的路径
* @returns 匹配的拦截配置,如果没有匹配则返回 undefined
*/
export const findMatchingRoute = (path: string): RouteInterceptConfig | undefined => {
return interceptRoutes.find((route) => route.enabled && path.startsWith(route.pathPrefix));
};
/**
* 从完整路径中提取子路径
* @param fullPath 完整路径,如 '/settings/agent'
* @param pathPrefix 路径前缀,如 '/settings'
* @returns 子路径,如 'agent'
*/
export const extractSubPath = (fullPath: string, pathPrefix: string): string | undefined => {
if (fullPath.length <= pathPrefix.length) return undefined;
// 去除前导斜杠
const subPath = fullPath.slice(Math.max(0, pathPrefix.length + 1));
return subPath || undefined;
};

View File

@@ -0,0 +1,47 @@
import type { BrowserWindowOpts } from './core/Browser';
export const BrowsersIdentifiers = {
chat: 'chat',
devtools: 'devtools',
settings: 'settings',
};
export const appBrowsers = {
chat: {
autoHideMenuBar: true,
height: 800,
identifier: 'chat',
keepAlive: true,
minWidth: 400,
path: '/chat',
showOnInit: true,
titleBarStyle: 'hidden',
vibrancy: 'under-window',
width: 1200,
},
devtools: {
autoHideMenuBar: true,
fullscreenable: false,
height: 600,
identifier: 'devtools',
maximizable: false,
minWidth: 400,
path: '/desktop/devtools',
titleBarStyle: 'hiddenInset',
vibrancy: 'under-window',
width: 1000,
},
settings: {
autoHideMenuBar: true,
height: 800,
identifier: 'settings',
keepAlive: true,
minWidth: 600,
path: '/settings',
titleBarStyle: 'hidden',
vibrancy: 'under-window',
width: 1000,
},
} satisfies Record<string, BrowserWindowOpts>;
export type AppBrowsersIdentifiers = keyof typeof appBrowsers;

View File

@@ -0,0 +1,29 @@
import { app } from 'electron';
import { join } from 'node:path';
export const mainDir = join(__dirname);
export const preloadDir = join(mainDir, '../preload');
export const resourcesDir = join(mainDir, '../../resources');
export const buildDir = join(mainDir, '../../build');
const appPath = app.getAppPath();
export const nextStandaloneDir = join(appPath, 'dist', 'next');
export const userDataDir = app.getPath('userData');
export const appStorageDir = join(userDataDir, 'lobehub-storage');
// ------ Application storage directory ---- //
// db schema hash
export const DB_SCHEMA_HASH_FILENAME = 'lobehub-local-db-schema-hash';
// pglite database dir
export const LOCAL_DATABASE_DIR = 'lobehub-local-db';
// 本地存储文件(模拟 S3
export const FILE_STORAGE_DIR = 'file-storage';
// Plugin 安装目录
export const INSTALL_PLUGINS_DIR = 'plugins';

View File

@@ -0,0 +1,3 @@
export const isDev = process.env.NODE_ENV === 'development';
export const OFFICIAL_CLOUD_SERVER = process.env.OFFICIAL_CLOUD_SERVER || 'https://lobechat.com';

View File

@@ -0,0 +1,22 @@
/**
* 应用设置存储相关常量
*/
import { appStorageDir } from '@/const/dir';
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
import { ElectronMainStore } from '@/types/store';
/**
* 存储名称
*/
export const STORE_NAME = 'lobehub-settings';
/**
* 存储默认值
*/
export const STORE_DEFAULTS: ElectronMainStore = {
dataSyncConfig: { storageMode: 'local' },
encryptedTokens: {},
locale: 'auto',
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
storagePath: appStorageDir,
};

View File

@@ -0,0 +1,390 @@
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
import { BrowserWindow, app, shell } from 'electron';
import crypto from 'node:crypto';
import querystring from 'node:querystring';
import { URL } from 'node:url';
import { name } from '@/../../package.json';
import { createLogger } from '@/utils/logger';
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
import { ControllerModule, ipcClientEvent } from './index';
// Create logger
const logger = createLogger('controllers:AuthCtr');
const protocolPrefix = `com.lobehub.${name}`;
/**
* Authentication Controller
* Used to implement the OAuth authorization flow
*/
export default class AuthCtr extends ControllerModule {
/**
* 远程服务器配置控制器
*/
private get remoteServerConfigCtr() {
return this.app.getController(RemoteServerConfigCtr);
}
/**
* 当前的 PKCE 参数
*/
private codeVerifier: string | null = null;
private authRequestState: string | null = null;
beforeAppReady = () => {
this.registerProtocolHandler();
};
/**
* Request OAuth authorization
*/
@ipcClientEvent('requestAuthorization')
async requestAuthorization(config: DataSyncConfig) {
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl(config);
logger.info(
`Requesting OAuth authorization, storageMode:${config.storageMode} server URL: ${remoteUrl}`,
);
try {
// Generate PKCE parameters
logger.debug('Generating PKCE parameters');
const codeVerifier = this.generateCodeVerifier();
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
this.codeVerifier = codeVerifier;
// Generate state parameter to prevent CSRF attacks
this.authRequestState = crypto.randomBytes(16).toString('hex');
logger.debug(`Generated state parameter: ${this.authRequestState}`);
// Construct authorization URL
const authUrl = new URL('/oidc/auth', remoteUrl);
// Add query parameters
authUrl.search = querystring.stringify({
client_id: 'lobehub-desktop',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
prompt: 'consent',
redirect_uri: `${protocolPrefix}://auth/callback`,
response_type: 'code',
scope: 'profile email offline_access',
state: this.authRequestState,
});
logger.info(`Constructed authorization URL: ${authUrl.toString()}`);
// Open authorization URL in the default browser
await shell.openExternal(authUrl.toString());
logger.debug('Opening authorization URL in default browser');
return { success: true };
} catch (error) {
logger.error('Authorization request failed:', error);
return { error: error.message, success: false };
}
}
/**
* Handle authorization callback
* This method is called when the browser redirects to our custom protocol
*/
async handleAuthCallback(callbackUrl: string) {
logger.info(`Handling authorization callback: ${callbackUrl}`);
try {
const url = new URL(callbackUrl);
const params = new URLSearchParams(url.search);
// Get authorization code
const code = params.get('code');
const state = params.get('state');
logger.debug(`Got parameters from callback URL: code=${code}, state=${state}`);
// Validate state parameter to prevent CSRF attacks
if (state !== this.authRequestState) {
logger.error(
`Invalid state parameter: expected ${this.authRequestState}, received ${state}`,
);
throw new Error('Invalid state parameter');
}
logger.debug('State parameter validation passed');
if (!code) {
logger.error('No authorization code received');
throw new Error('No authorization code received');
}
// Get configuration information
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
logger.debug(`Getting remote server configuration: url=${config.remoteServerUrl}`);
if (!config.remoteServerUrl) {
logger.error('Server URL not configured');
throw new Error('No server URL configured');
}
// Get the previously saved code_verifier
const codeVerifier = this.codeVerifier;
if (!codeVerifier) {
logger.error('Code verifier not found');
throw new Error('No code verifier found');
}
logger.debug('Found code verifier');
// Exchange authorization code for token
logger.debug('Starting to exchange authorization code for token');
const result = await this.exchangeCodeForToken(code, codeVerifier);
if (result.success) {
logger.info('Authorization successful');
// Notify render process of successful authorization
this.broadcastAuthorizationSuccessful();
} else {
logger.warn(`Authorization failed: ${result.error || 'Unknown error'}`);
// Notify render process of failed authorization
this.broadcastAuthorizationFailed(result.error || 'Unknown error');
}
return result;
} catch (error) {
logger.error('Handling authorization callback failed:', error);
// Notify render process of failed authorization
this.broadcastAuthorizationFailed(error.message);
return { error: error.message, success: false };
} finally {
// Clear authorization request state
logger.debug('Clearing authorization request state');
this.authRequestState = null;
this.codeVerifier = null;
}
}
/**
* Refresh access token
*/
@ipcClientEvent('refreshAccessToken')
async refreshAccessToken() {
logger.info('Starting to refresh access token');
try {
// Call the centralized refresh logic in RemoteServerConfigCtr
const result = await this.remoteServerConfigCtr.refreshAccessToken();
if (result.success) {
logger.info('Token refresh successful via AuthCtr call.');
// Notify render process that token has been refreshed
this.broadcastTokenRefreshed();
return { success: true };
} else {
// Throw an error to be caught by the catch block below
// This maintains the existing behavior of clearing tokens on failure
logger.error(`Token refresh failed via AuthCtr call: ${result.error}`);
throw new Error(result.error || 'Token refresh failed');
}
} catch (error) {
// Keep the existing logic to clear tokens and require re-auth on failure
logger.error('Token refresh operation failed via AuthCtr, initiating cleanup:', error);
// Refresh failed, clear tokens and disable remote server
logger.warn('Refresh failed, clearing tokens and disabling remote server');
await this.remoteServerConfigCtr.clearTokens();
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
// Notify render process that re-authorization is required
this.broadcastAuthorizationRequired();
return { error: error.message, success: false };
}
}
/**
* Register custom protocol handler
*/
private registerProtocolHandler() {
logger.info(`Registering custom protocol handler ${protocolPrefix}://`);
app.setAsDefaultProtocolClient(protocolPrefix);
// Register custom protocol handler
if (process.platform === 'darwin') {
// Handle open-url event on macOS
logger.debug('Registering open-url event handler for macOS');
app.on('open-url', (event, url) => {
event.preventDefault();
logger.info(`Received open-url event: ${url}`);
this.handleAuthCallback(url);
});
} else {
// Handle protocol callback via second-instance event on Windows and Linux
logger.debug('Registering second-instance event handler for Windows/Linux');
app.on('second-instance', (event, commandLine) => {
// Find the URL from command line arguments
const url = commandLine.find((arg) => arg.startsWith(`${protocolPrefix}://`));
if (url) {
logger.info(`Found URL from second-instance command line arguments: ${url}`);
this.handleAuthCallback(url);
} else {
logger.warn('Protocol URL not found in second-instance command line arguments');
}
});
}
logger.info(`Registered ${protocolPrefix}:// custom protocol handler`);
}
/**
* Exchange authorization code for token
*/
private async exchangeCodeForToken(code: string, codeVerifier: string) {
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl();
logger.info('Starting to exchange authorization code for token');
try {
const tokenUrl = new URL('/oidc/token', remoteUrl);
logger.debug(`Constructed token exchange URL: ${tokenUrl.toString()}`);
// Construct request body
const body = querystring.stringify({
client_id: 'lobehub-desktop',
code,
code_verifier: codeVerifier,
grant_type: 'authorization_code',
redirect_uri: `${protocolPrefix}://auth/callback`,
});
logger.debug('Sending token exchange request');
// Send request to get token
const response = await fetch(tokenUrl.toString(), {
body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
if (!response.ok) {
// Try parsing the error response
const errorData = await response.json().catch(() => ({}));
const errorMessage = `Failed to get token: ${response.status} ${response.statusText} ${errorData.error_description || errorData.error || ''}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
// Parse response
const data = await response.json();
logger.debug('Successfully received token exchange response');
// console.log(data); // Keep original log for debugging, or remove/change to logger.debug as needed
// Ensure response contains necessary fields
if (!data.access_token || !data.refresh_token) {
logger.error('Invalid token response: missing access_token or refresh_token');
throw new Error('Invalid token response: missing required fields');
}
// Save tokens
logger.debug('Starting to save exchanged tokens');
await this.remoteServerConfigCtr.saveTokens(data.access_token, data.refresh_token);
logger.info('Successfully saved exchanged tokens');
// Set server to active state
logger.debug(`Setting remote server to active state: ${remoteUrl}`);
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: true });
return { success: true };
} catch (error) {
logger.error('Exchanging authorization code failed:', error);
return { error: error.message, success: false };
}
}
/**
* Broadcast token refreshed event
*/
private broadcastTokenRefreshed() {
logger.debug('Broadcasting tokenRefreshed event to all windows');
const allWindows = BrowserWindow.getAllWindows();
for (const win of allWindows) {
if (!win.isDestroyed()) {
win.webContents.send('tokenRefreshed');
}
}
}
/**
* Broadcast authorization successful event
*/
private broadcastAuthorizationSuccessful() {
logger.debug('Broadcasting authorizationSuccessful event to all windows');
const allWindows = BrowserWindow.getAllWindows();
for (const win of allWindows) {
if (!win.isDestroyed()) {
win.webContents.send('authorizationSuccessful');
}
}
}
/**
* Broadcast authorization failed event
*/
private broadcastAuthorizationFailed(error: string) {
logger.debug(`Broadcasting authorizationFailed event to all windows, error: ${error}`);
const allWindows = BrowserWindow.getAllWindows();
for (const win of allWindows) {
if (!win.isDestroyed()) {
win.webContents.send('authorizationFailed', { error });
}
}
}
/**
* Broadcast authorization required event
*/
private broadcastAuthorizationRequired() {
logger.debug('Broadcasting authorizationRequired event to all windows');
const allWindows = BrowserWindow.getAllWindows();
for (const win of allWindows) {
if (!win.isDestroyed()) {
win.webContents.send('authorizationRequired');
}
}
}
/**
* Generate PKCE codeVerifier
*/
private generateCodeVerifier(): string {
logger.debug('Generating PKCE code verifier');
// Generate a random string of at least 43 characters
const verifier = crypto
.randomBytes(32)
.toString('base64')
.replaceAll('+', '-')
.replaceAll('/', '_')
.replace(/=+$/, '');
logger.debug('Generated code verifier (partial): ' + verifier.slice(0, 10) + '...'); // Avoid logging full sensitive info
return verifier;
}
/**
* Generate codeChallenge from codeVerifier (S256 method)
*/
private async generateCodeChallenge(codeVerifier: string): Promise<string> {
logger.debug('Generating PKCE code challenge (S256)');
// Hash codeVerifier using SHA-256
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
// Convert hash result to base64url encoding
const challenge = Buffer.from(digest)
.toString('base64')
.replaceAll('+', '-')
.replaceAll('/', '_')
.replace(/=+$/, '');
logger.debug('Generated code challenge (partial): ' + challenge.slice(0, 10) + '...'); // Avoid logging full sensitive info
return challenge;
}
}

View File

@@ -0,0 +1,95 @@
import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
import { extractSubPath, findMatchingRoute } from '~common/routes';
import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
import { ControllerModule, ipcClientEvent, shortcut } from './index';
export default class BrowserWindowsCtr extends ControllerModule {
@shortcut('toggleMainWindow')
async toggleMainWindow() {
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.toggleVisible();
}
@ipcClientEvent('openSettingsWindow')
async openSettingsWindow(tab?: string) {
console.log('[BrowserWindowsCtr] Received request to open settings window', tab);
try {
await this.app.browserManager.showSettingsWindowWithTab(tab);
return { success: true };
} catch (error) {
console.error('[BrowserWindowsCtr] Failed to open settings window:', error);
return { error: error.message, success: false };
}
}
/**
* Handle route interception requests
* Responsible for handling route interception requests from the renderer process
*/
@ipcClientEvent('interceptRoute')
async interceptRoute(params: InterceptRouteParams) {
const { path, source } = params;
console.log(
`[BrowserWindowsCtr] Received route interception request: ${path}, source: ${source}`,
);
// Find matching route configuration
const matchedRoute = findMatchingRoute(path);
// If no matching route found, return not intercepted
if (!matchedRoute) {
console.log(`[BrowserWindowsCtr] No matching route configuration found: ${path}`);
return { intercepted: false, path, source };
}
console.log(
`[BrowserWindowsCtr] Intercepted route: ${path}, target window: ${matchedRoute.targetWindow}`,
);
try {
if (matchedRoute.targetWindow === BrowsersIdentifiers.settings) {
const subPath = extractSubPath(path, matchedRoute.pathPrefix);
await this.app.browserManager.showSettingsWindowWithTab(subPath);
return {
intercepted: true,
path,
source,
subPath,
targetWindow: matchedRoute.targetWindow,
};
} else {
await this.openTargetWindow(matchedRoute.targetWindow as AppBrowsersIdentifiers);
return {
intercepted: true,
path,
source,
targetWindow: matchedRoute.targetWindow,
};
}
} catch (error) {
console.error('[BrowserWindowsCtr] Error while processing route interception:', error);
return {
error: error.message,
intercepted: false,
path,
source,
};
}
}
/**
* Open target window and navigate to specified sub-path
*/
private async openTargetWindow(targetWindow: AppBrowsersIdentifiers) {
// Ensure the window can always be created or reopened
const browser = this.app.browserManager.retrieveByIdentifier(targetWindow);
browser.show();
}
}

View File

@@ -0,0 +1,9 @@
import { ControllerModule, ipcClientEvent } from './index';
export default class DevtoolsCtr extends ControllerModule {
@ipcClientEvent('openDevtools')
async openDevtools() {
const devtoolsBrowser = this.app.browserManager.retrieveByIdentifier('devtools');
devtoolsBrowser.show();
}
}

View File

@@ -0,0 +1,380 @@
import {
ListLocalFileParams,
LocalMoveFilesResultItem,
LocalReadFileParams,
LocalReadFileResult,
LocalReadFilesParams,
LocalSearchFilesParams,
MoveLocalFilesParams,
OpenLocalFileParams,
OpenLocalFolderParams,
RenameLocalFileResult,
} from '@lobechat/electron-client-ipc';
import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
import { shell } from 'electron';
import * as fs from 'node:fs';
import { rename as renamePromise } from 'node:fs/promises';
import * as path from 'node:path';
import { promisify } from 'node:util';
import FileSearchService from '@/services/fileSearchSrv';
import { FileResult, SearchOptions } from '@/types/fileSearch';
import { makeSureDirExist } from '@/utils/file-system';
import { ControllerModule, ipcClientEvent } from './index';
const statPromise = promisify(fs.stat);
const readdirPromise = promisify(fs.readdir);
const renamePromiseFs = promisify(fs.rename);
const accessPromise = promisify(fs.access);
export default class LocalFileCtr extends ControllerModule {
private get searchService() {
return this.app.getService(FileSearchService);
}
/**
* Handle IPC event for local file search
*/
@ipcClientEvent('searchLocalFiles')
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
const options: Omit<SearchOptions, 'keywords'> = {
limit: 30,
};
return this.searchService.search(params.keywords, options);
}
@ipcClientEvent('openLocalFile')
async handleOpenLocalFile({ path: filePath }: OpenLocalFileParams): Promise<{
error?: string;
success: boolean;
}> {
try {
await shell.openPath(filePath);
return { success: true };
} catch (error) {
console.error(`Failed to open file ${filePath}:`, error);
return { error: (error as Error).message, success: false };
}
}
@ipcClientEvent('openLocalFolder')
async handleOpenLocalFolder({ path: targetPath, isDirectory }: OpenLocalFolderParams): Promise<{
error?: string;
success: boolean;
}> {
try {
const folderPath = isDirectory ? targetPath : path.dirname(targetPath);
await shell.openPath(folderPath);
return { success: true };
} catch (error) {
console.error(`Failed to open folder for path ${targetPath}:`, error);
return { error: (error as Error).message, success: false };
}
}
@ipcClientEvent('readLocalFiles')
async readFiles({ paths }: LocalReadFilesParams): Promise<LocalReadFileResult[]> {
const results: LocalReadFileResult[] = [];
for (const filePath of paths) {
// 初始化结果对象
const result = await this.readFile({ path: filePath });
results.push(result);
}
return results;
}
@ipcClientEvent('readLocalFile')
async readFile({ path: filePath, loc }: LocalReadFileParams): Promise<LocalReadFileResult> {
try {
const effectiveLoc = loc ?? [0, 200];
const fileDocument = await loadFile(filePath);
const [startLine, endLine] = effectiveLoc;
const lines = fileDocument.content.split('\n');
const totalLineCount = lines.length;
const totalCharCount = fileDocument.content.length;
// Adjust slice indices to be 0-based and inclusive/exclusive
const selectedLines = lines.slice(startLine, endLine);
const content = selectedLines.join('\n');
const charCount = content.length;
const lineCount = selectedLines.length;
const result: LocalReadFileResult = {
// Char count for the selected range
charCount,
// Content for the selected range
content,
createdTime: fileDocument.createdTime,
fileType: fileDocument.fileType,
filename: fileDocument.filename,
lineCount,
loc: effectiveLoc,
// Line count for the selected range
modifiedTime: fileDocument.modifiedTime,
// Total char count of the file
totalCharCount,
// Total line count of the file
totalLineCount,
};
try {
const stats = await statPromise(filePath);
if (stats.isDirectory()) {
result.content = 'This is a directory and cannot be read as plain text.';
result.charCount = 0;
result.lineCount = 0;
// Keep total counts for directory as 0 as well, or decide if they should reflect metadata size
result.totalCharCount = 0;
result.totalLineCount = 0;
}
} catch (statError) {
console.error(`Stat failed for ${filePath} after loadFile:`, statError);
}
return result;
} catch (error) {
console.error(`Error processing file ${filePath}:`, error);
const errorMessage = (error as Error).message;
return {
charCount: 0,
content: `Error accessing or processing file: ${errorMessage}`,
createdTime: new Date(),
fileType: path.extname(filePath).toLowerCase().replace('.', '') || 'unknown',
filename: path.basename(filePath),
lineCount: 0,
loc: [0, 0],
modifiedTime: new Date(),
totalCharCount: 0, // Add total counts to error result
totalLineCount: 0,
};
}
}
@ipcClientEvent('listLocalFiles')
async listLocalFiles({ path: dirPath }: ListLocalFileParams): Promise<FileResult[]> {
const results: FileResult[] = [];
try {
const entries = await readdirPromise(dirPath);
for (const entry of entries) {
// Skip specific system files based on the ignore list
if (SYSTEM_FILES_TO_IGNORE.includes(entry)) {
continue;
}
const fullPath = path.join(dirPath, entry);
try {
const stats = await statPromise(fullPath);
const isDirectory = stats.isDirectory();
results.push({
createdTime: stats.birthtime,
isDirectory,
lastAccessTime: stats.atime,
modifiedTime: stats.mtime,
name: entry,
path: fullPath,
size: stats.size,
type: isDirectory ? 'directory' : path.extname(entry).toLowerCase().replace('.', ''),
});
} catch (statError) {
// Silently ignore files we can't stat (e.g. permissions)
console.error(`Failed to stat ${fullPath}:`, statError);
}
}
// Sort entries: folders first, then by name
results.sort((a, b) => {
if (a.isDirectory !== b.isDirectory) {
return a.isDirectory ? -1 : 1; // Directories first
}
// Add null/undefined checks for robustness if needed, though names should exist
return (a.name || '').localeCompare(b.name || ''); // Then sort by name
});
return results;
} catch (error) {
console.error(`Failed to list directory ${dirPath}:`, error);
// Rethrow or return an empty array/error object depending on desired behavior
// For now, returning empty array on error listing directory itself
return [];
}
}
@ipcClientEvent('moveLocalFiles')
async handleMoveFiles({ items }: MoveLocalFilesParams): Promise<LocalMoveFilesResultItem[]> {
const results: LocalMoveFilesResultItem[] = [];
if (!items || items.length === 0) {
console.warn('moveLocalFiles called with empty items array.');
return [];
}
// 逐个处理移动请求
for (const item of items) {
const { oldPath: sourcePath, newPath } = item;
const resultItem: LocalMoveFilesResultItem = {
newPath: undefined,
sourcePath,
success: false,
};
// 基本验证
if (!sourcePath || !newPath) {
resultItem.error = 'Both oldPath and newPath are required for each item.';
results.push(resultItem);
continue;
}
try {
// 检查源是否存在
try {
await accessPromise(sourcePath, fs.constants.F_OK);
} catch (accessError: any) {
if (accessError.code === 'ENOENT') {
throw new Error(`Source path not found: ${sourcePath}`);
} else {
throw new Error(
`Permission denied accessing source path: ${sourcePath}. ${accessError.message}`,
);
}
}
// 检查目标路径是否与源路径相同
if (path.normalize(sourcePath) === path.normalize(newPath)) {
console.log(`Skipping move: source and target path are identical: ${sourcePath}`);
resultItem.success = true;
resultItem.newPath = newPath; // 即使未移动,也报告目标路径
results.push(resultItem);
continue;
}
// LBYL: 确保目标目录存在
const targetDir = path.dirname(newPath);
makeSureDirExist(targetDir);
// 执行移动 (rename)
await renamePromiseFs(sourcePath, newPath);
resultItem.success = true;
resultItem.newPath = newPath;
console.log(`Successfully moved ${sourcePath} to ${newPath}`);
} catch (error) {
console.error(`Error moving ${sourcePath} to ${newPath}:`, error);
// 使用与 handleMoveFile 类似的错误处理逻辑
let errorMessage = (error as Error).message;
if ((error as any).code === 'ENOENT')
errorMessage = `Source path not found: ${sourcePath}.`;
else if ((error as any).code === 'EPERM' || (error as any).code === 'EACCES')
errorMessage = `Permission denied to move the item at ${sourcePath}. Check file/folder permissions.`;
else if ((error as any).code === 'EBUSY')
errorMessage = `The file or directory at ${sourcePath} or ${newPath} is busy or locked by another process.`;
else if ((error as any).code === 'EXDEV')
errorMessage = `Cannot move across different file systems or drives. Source: ${sourcePath}, Target: ${newPath}.`;
else if ((error as any).code === 'EISDIR')
errorMessage = `Cannot overwrite a directory with a file, or vice versa. Source: ${sourcePath}, Target: ${newPath}.`;
else if ((error as any).code === 'ENOTEMPTY')
errorMessage = `The target directory ${newPath} is not empty (relevant on some systems if target exists and is a directory).`;
else if ((error as any).code === 'EEXIST')
errorMessage = `An item already exists at the target path: ${newPath}.`;
// 保留来自访问检查或目录检查的更具体错误
else if (
!errorMessage.startsWith('Source path not found') &&
!errorMessage.startsWith('Permission denied accessing source path') &&
!errorMessage.includes('Target directory')
) {
// Keep the original error message if none of the specific codes match
}
resultItem.error = errorMessage;
}
results.push(resultItem);
}
return results;
}
@ipcClientEvent('renameLocalFile')
async handleRenameFile({
path: currentPath,
newName,
}: {
newName: string;
path: string;
}): Promise<RenameLocalFileResult> {
// Basic validation (can also be done in frontend action)
if (!currentPath || !newName) {
return { error: 'Both path and newName are required.', newPath: '', success: false };
}
// Prevent path traversal or using invalid characters/names
if (
newName.includes('/') ||
newName.includes('\\') ||
newName === '.' ||
newName === '..' ||
/["*/:<>?\\|]/.test(newName) // Check for typical invalid filename characters
) {
return {
error:
'Invalid new name. It cannot contain path separators (/, \\), be "." or "..", or include characters like < > : " / \\ | ? *.',
newPath: '',
success: false,
};
}
let newPath: string;
try {
const dir = path.dirname(currentPath);
newPath = path.join(dir, newName);
// Check if paths are identical after calculation
if (path.normalize(currentPath) === path.normalize(newPath)) {
console.log(
`Skipping rename: oldPath and calculated newPath are identical: ${currentPath}`,
);
// Consider success as no change is needed, but maybe inform the user?
// Return success for now.
return { newPath, success: true };
}
} catch (error) {
console.error(`Error calculating new path for rename ${currentPath} to ${newName}:`, error);
return {
error: `Internal error calculating the new path: ${(error as Error).message}`,
newPath: '',
success: false,
};
}
// Perform the rename operation using fs.promises.rename directly
try {
await renamePromise(currentPath, newPath);
console.log(`Successfully renamed ${currentPath} to ${newPath}`);
// Optionally return the newPath if frontend needs it
// return { success: true, newPath: newPath };
return { newPath, success: true };
} catch (error) {
console.error(`Error renaming ${currentPath} to ${newPath}:`, error);
let errorMessage = (error as Error).message;
// Provide more specific error messages based on common codes
if ((error as any).code === 'ENOENT') {
errorMessage = `File or directory not found at the original path: ${currentPath}.`;
} else if ((error as any).code === 'EPERM' || (error as any).code === 'EACCES') {
errorMessage = `Permission denied to rename the item at ${currentPath}. Check file/folder permissions.`;
} else if ((error as any).code === 'EBUSY') {
errorMessage = `The file or directory at ${currentPath} or ${newPath} is busy or locked by another process.`;
} else if ((error as any).code === 'EISDIR' || (error as any).code === 'ENOTDIR') {
errorMessage = `Cannot rename - conflict between file and directory. Source: ${currentPath}, Target: ${newPath}.`;
} else if ((error as any).code === 'EEXIST') {
// Target already exists
errorMessage = `Cannot rename: an item with the name '${newName}' already exists at this location.`;
}
// Add more specific checks as needed
return { error: errorMessage, newPath: '', success: false };
}
}
}

View File

@@ -0,0 +1,29 @@
import { ControllerModule, ipcClientEvent } from './index';
export default class MenuController extends ControllerModule {
/**
* 刷新菜单
*/
@ipcClientEvent('refreshAppMenu')
refreshAppMenu() {
// 注意:可能需要根据具体情况决定是否允许渲染进程刷新所有菜单
return this.app.menuManager.refreshMenus();
}
/**
* 显示上下文菜单
*/
@ipcClientEvent('showContextMenu')
showContextMenu(type: string, data?: any) {
return this.app.menuManager.showContextMenu(type, data);
}
/**
* 设置开发菜单可见性
*/
@ipcClientEvent('setDevMenuVisibility')
setDevMenuVisibility(visible: boolean) {
// 调用 MenuManager 的方法来重建应用菜单
return this.app.menuManager.rebuildAppMenu({ showDevItems: visible });
}
}

View File

@@ -0,0 +1,335 @@
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
import { safeStorage } from 'electron';
import querystring from 'node:querystring';
import { URL } from 'node:url';
import { OFFICIAL_CLOUD_SERVER } from '@/const/env';
import { createLogger } from '@/utils/logger';
import { ControllerModule, ipcClientEvent } from './index';
// Create logger
const logger = createLogger('controllers:RemoteServerConfigCtr');
/**
* Remote Server Configuration Controller
* Used to manage custom remote LobeChat server configuration
*/
export default class RemoteServerConfigCtr extends ControllerModule {
/**
* Key used to store encrypted tokens in electron-store.
*/
private readonly encryptedTokensKey = 'encryptedTokens';
/**
* Get remote server configuration
*/
@ipcClientEvent('getRemoteServerConfig')
async getRemoteServerConfig() {
logger.debug('Getting remote server configuration');
const { storeManager } = this.app;
const config: DataSyncConfig = storeManager.get('dataSyncConfig');
logger.debug(
`Remote server config: active=${config.active}, storageMode=${config.storageMode}, url=${config.remoteServerUrl}`,
);
return config;
}
/**
* Set remote server configuration
*/
@ipcClientEvent('setRemoteServerConfig')
async setRemoteServerConfig(config: Partial<DataSyncConfig>) {
logger.info(
`Setting remote server storageMode: active=${config.active}, storageMode=${config.storageMode}, url=${config.remoteServerUrl}`,
);
const { storeManager } = this.app;
const prev: DataSyncConfig = storeManager.get('dataSyncConfig');
// Save configuration
storeManager.set('dataSyncConfig', { ...prev, ...config });
return true;
}
/**
* Clear remote server configuration
*/
@ipcClientEvent('clearRemoteServerConfig')
async clearRemoteServerConfig() {
logger.info('Clearing remote server configuration');
const { storeManager } = this.app;
// Clear instance configuration
storeManager.set('dataSyncConfig', { storageMode: 'local' });
// Clear tokens (if any)
await this.clearTokens();
return true;
}
/**
* Encrypted tokens
* Stored in memory for quick access, loaded from persistent storage on init.
*/
private encryptedAccessToken?: string;
private encryptedRefreshToken?: string;
/**
* Promise representing the ongoing token refresh operation.
* Used to prevent concurrent refreshes and allow callers to wait.
*/
private refreshPromise: Promise<{ error?: string; success: boolean }> | null = null;
/**
* Encrypt and store tokens
* @param accessToken Access token
* @param refreshToken Refresh token
*/
async saveTokens(accessToken: string, refreshToken: string) {
logger.info('Saving encrypted tokens');
// If platform doesn't support secure storage, store raw tokens
if (!safeStorage.isEncryptionAvailable()) {
logger.warn('Safe storage not available, storing tokens unencrypted');
this.encryptedAccessToken = accessToken;
this.encryptedRefreshToken = refreshToken;
// Persist unencrypted tokens (consider security implications)
this.app.storeManager.set(this.encryptedTokensKey, {
accessToken: this.encryptedAccessToken,
refreshToken: this.encryptedRefreshToken,
});
return;
}
// Encrypt tokens
logger.debug('Encrypting tokens using safe storage');
this.encryptedAccessToken = Buffer.from(safeStorage.encryptString(accessToken)).toString(
'base64',
);
this.encryptedRefreshToken = Buffer.from(safeStorage.encryptString(refreshToken)).toString(
'base64',
);
// Persist encrypted tokens
logger.debug(`Persisting encrypted tokens to store key: ${this.encryptedTokensKey}`);
this.app.storeManager.set(this.encryptedTokensKey, {
accessToken: this.encryptedAccessToken,
refreshToken: this.encryptedRefreshToken,
});
}
/**
* Get decrypted access token
*/
async getAccessToken(): Promise<string | null> {
// Try loading from memory first
if (!this.encryptedAccessToken) {
logger.debug('Access token not in memory, trying to load from store...');
this.loadTokensFromStore(); // Attempt to load from persistent storage
}
if (!this.encryptedAccessToken) {
logger.debug('No access token found in memory or store.');
return null;
}
// If platform doesn't support secure storage, return stored token
if (!safeStorage.isEncryptionAvailable()) {
logger.debug(
'Safe storage not available, returning potentially unencrypted token from memory/store',
);
return this.encryptedAccessToken;
}
try {
// Decrypt token
logger.debug('Decrypting access token');
const encryptedData = Buffer.from(this.encryptedAccessToken, 'base64');
return safeStorage.decryptString(encryptedData);
} catch (error) {
logger.error('Failed to decrypt access token:', error);
return null;
}
}
/**
* Get decrypted refresh token
*/
async getRefreshToken(): Promise<string | null> {
// Try loading from memory first
if (!this.encryptedRefreshToken) {
logger.debug('Refresh token not in memory, trying to load from store...');
this.loadTokensFromStore(); // Attempt to load from persistent storage
}
if (!this.encryptedRefreshToken) {
logger.debug('No refresh token found in memory or store.');
return null;
}
// If platform doesn't support secure storage, return stored token
if (!safeStorage.isEncryptionAvailable()) {
logger.debug(
'Safe storage not available, returning potentially unencrypted token from memory/store',
);
return this.encryptedRefreshToken;
}
try {
// Decrypt token
logger.debug('Decrypting refresh token');
const encryptedData = Buffer.from(this.encryptedRefreshToken, 'base64');
return safeStorage.decryptString(encryptedData);
} catch (error) {
logger.error('Failed to decrypt refresh token:', error);
return null;
}
}
/**
* Clear tokens
*/
async clearTokens() {
logger.info('Clearing access and refresh tokens');
this.encryptedAccessToken = undefined;
this.encryptedRefreshToken = undefined;
// Also clear from persistent storage
logger.debug(`Deleting tokens from store key: ${this.encryptedTokensKey}`);
this.app.storeManager.delete(this.encryptedTokensKey);
}
/**
* 刷新访问令牌
* 使用存储的刷新令牌获取新的访问令牌
* Handles concurrent requests by returning the existing refresh promise if one is in progress.
*/
@ipcClientEvent('refreshAccessToken')
async refreshAccessToken(): Promise<{ error?: string; success: boolean }> {
// If a refresh is already in progress, return the existing promise
if (this.refreshPromise) {
logger.debug('Token refresh already in progress, returning existing promise.');
return this.refreshPromise;
}
// Start a new refresh operation
logger.info('Initiating new token refresh operation.');
this.refreshPromise = this.performTokenRefresh();
// Return the promise so callers can wait
return this.refreshPromise;
}
/**
* Performs the actual token refresh logic.
* This method is called by refreshAccessToken and wrapped in a promise.
*/
private async performTokenRefresh(): Promise<{ error?: string; success: boolean }> {
try {
// 获取配置信息
const config = await this.getRemoteServerConfig();
if (!config.remoteServerUrl || !config.active) {
logger.warn('Remote server not active or configured, skipping refresh.');
return { error: '远程服务器未激活或未配置', success: false };
}
// 获取刷新令牌
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
logger.error('No refresh token available for refresh operation.');
return { error: '没有可用的刷新令牌', success: false };
}
// 构造刷新请求
const remoteUrl = await this.getRemoteServerUrl(config);
const tokenUrl = new URL('/oidc/token', remoteUrl);
// 构造请求体
const body = querystring.stringify({
client_id: 'lobehub-desktop',
grant_type: 'refresh_token',
refresh_token: refreshToken,
});
logger.debug(`Sending token refresh request to ${tokenUrl.toString()}`);
// 发送请求
const response = await fetch(tokenUrl.toString(), {
body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
if (!response.ok) {
// 尝试解析错误响应
const errorData = await response.json().catch(() => ({}));
const errorMessage = `刷新令牌失败: ${response.status} ${response.statusText} ${
errorData.error_description || errorData.error || ''
}`.trim();
logger.error(errorMessage, errorData);
return { error: errorMessage, success: false };
}
// 解析响应
const data = await response.json();
// 检查响应中是否包含必要令牌
if (!data.access_token || !data.refresh_token) {
logger.error('Refresh response missing access_token or refresh_token', data);
return { error: '刷新响应中缺少令牌', success: false };
}
// 保存新令牌
logger.info('Token refresh successful, saving new tokens.');
await this.saveTokens(data.access_token, data.refresh_token);
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Exception during token refresh operation:', errorMessage, error);
return { error: `刷新令牌时发生异常: ${errorMessage}`, success: false };
} finally {
// Ensure the promise reference is cleared once the operation completes
logger.debug('Clearing the refresh promise reference.');
this.refreshPromise = null;
}
}
/**
* Load encrypted tokens from persistent storage (electron-store) into memory.
* This should be called during initialization or if memory tokens are missing.
*/
private loadTokensFromStore() {
logger.debug(`Attempting to load tokens from store key: ${this.encryptedTokensKey}`);
const storedTokens = this.app.storeManager.get(this.encryptedTokensKey);
if (storedTokens && storedTokens.accessToken && storedTokens.refreshToken) {
logger.info('Successfully loaded tokens from store into memory.');
this.encryptedAccessToken = storedTokens.accessToken;
this.encryptedRefreshToken = storedTokens.refreshToken;
} else {
logger.debug('No valid tokens found in store.');
}
}
// Initialize by loading tokens from store when the controller is ready
// We might need a dedicated lifecycle method if constructor is too early for storeManager
afterAppReady() {
this.loadTokensFromStore();
}
async getRemoteServerUrl(config?: DataSyncConfig) {
const dataConfig = config ? config : await this.getRemoteServerConfig();
return dataConfig.storageMode === 'cloud' ? OFFICIAL_CLOUD_SERVER : dataConfig.remoteServerUrl;
}
}

View File

@@ -0,0 +1,321 @@
import {
ProxyTRPCRequestParams,
ProxyTRPCRequestResult,
} from '@lobechat/electron-client-ipc/src/types/proxyTRPCRequest';
import { Buffer } from 'node:buffer';
import http, { IncomingMessage, OutgoingHttpHeaders } from 'node:http';
import https from 'node:https';
import { URL } from 'node:url';
import { createLogger } from '@/utils/logger';
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
import { ControllerModule, ipcClientEvent } from './index';
// Create logger
const logger = createLogger('controllers:RemoteServerSyncCtr');
/**
* Remote Server Sync Controller
* For handling data synchronization with remote servers via IPC.
*/
export default class RemoteServerSyncCtr extends ControllerModule {
/**
* Cached instance of RemoteServerConfigCtr
*/
private _remoteServerConfigCtrInstance: RemoteServerConfigCtr | null = null;
/**
* Remote server configuration controller
*/
private get remoteServerConfigCtr() {
if (!this._remoteServerConfigCtrInstance) {
this._remoteServerConfigCtrInstance = this.app.getController(RemoteServerConfigCtr);
}
return this._remoteServerConfigCtrInstance;
}
/**
* Controller initialization - No specific logic needed here now for request handling
*/
afterAppReady() {
logger.info('RemoteServerSyncCtr initialized (IPC based)');
// No need to register protocol handler anymore
}
/**
* Helper function to perform the actual request forwarding to the remote server.
* Accepts arguments from IPC and returns response details.
*/
private async forwardRequest(args: {
accessToken: string | null;
body?: string | ArrayBuffer;
headers: Record<string, string>;
method: string;
remoteServerUrl: string;
urlPath: string; // Pass the base URL
}): Promise<{
// Node headers type
body: Buffer;
headers: Record<string, string | string[] | undefined>;
status: number;
statusText: string; // Return body as Buffer
}> {
const {
urlPath,
method,
headers: originalHeaders,
body: requestBody,
accessToken,
remoteServerUrl,
} = args;
const logPrefix = `[ForwardRequest ${method} ${urlPath}]`; // Add prefix for easier correlation
if (!accessToken) {
logger.error(`${logPrefix} No access token provided`); // Enhanced log
return {
body: Buffer.from(''),
headers: {},
status: 401,
statusText: 'Authentication required, missing token',
};
}
// 1. Determine target URL and prepare request options
const targetUrl = new URL(urlPath, remoteServerUrl); // Combine base URL and path
logger.debug(`${logPrefix} Forwarding to ${targetUrl.toString()}`); // Enhanced log
// Prepare headers, cloning and adding Authorization
const requestHeaders: OutgoingHttpHeaders = { ...originalHeaders }; // Use OutgoingHttpHeaders
requestHeaders['Authorization'] = `Bearer ${accessToken}`;
// Let node handle Host, Content-Length etc. Remove potentially problematic headers
delete requestHeaders['host'];
delete requestHeaders['connection']; // Often causes issues
// delete requestHeaders['content-length']; // Let node handle it based on body
const requestOptions: https.RequestOptions | http.RequestOptions = {
// Use union type
headers: requestHeaders,
hostname: targetUrl.hostname,
method: method,
path: targetUrl.pathname + targetUrl.search,
port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
protocol: targetUrl.protocol,
// agent: false, // Consider for keep-alive issues if they arise
};
const requester = targetUrl.protocol === 'https:' ? https : http;
// 2. Make the request and capture response
return new Promise((resolve) => {
const clientReq = requester.request(requestOptions, (clientRes: IncomingMessage) => {
const chunks: Buffer[] = [];
clientRes.on('data', (chunk) => {
chunks.push(chunk);
});
clientRes.on('end', () => {
const responseBody = Buffer.concat(chunks);
logger.debug(
`${logPrefix} Received response from ${targetUrl.toString()}: ${clientRes.statusCode}`,
); // Enhanced log
resolve({
// These are IncomingHttpHeaders
body: responseBody,
headers: clientRes.headers,
status: clientRes.statusCode || 500,
statusText: clientRes.statusMessage || 'Unknown Status',
});
});
clientRes.on('error', (error) => {
// Error during response streaming
logger.error(
`${logPrefix} Error reading response stream from ${targetUrl.toString()}:`,
error,
); // Enhanced log
// Rejecting might be better, but we need to resolve the outer promise for proxyTRPCRequest
resolve({
body: Buffer.from(`Error reading response stream: ${error.message}`),
headers: {},
status: 502,
// Bad Gateway
statusText: 'Error reading response stream',
});
});
});
clientReq.on('error', (error) => {
logger.error(`${logPrefix} Error forwarding request to ${targetUrl.toString()}:`, error); // Enhanced log
// Reject or resolve with error status for the outer promise
resolve({
body: Buffer.from(`Error forwarding request: ${error.message}`),
headers: {},
status: 502,
// Bad Gateway
statusText: 'Error forwarding request',
});
});
// 3. Send request body if present
if (requestBody) {
if (typeof requestBody === 'string') {
clientReq.write(requestBody, 'utf8'); // Specify encoding for strings
} else if (requestBody instanceof ArrayBuffer) {
clientReq.write(Buffer.from(requestBody)); // Convert ArrayBuffer to Buffer
} else {
// Should not happen based on type, but handle defensively
logger.warn(`${logPrefix} Unsupported request body type received:`, typeof requestBody); // Enhanced log
}
}
clientReq.end(); // Finalize the request
});
}
/**
* Handles the 'proxy-trpc-request' IPC call from the renderer process.
* This method should be invoked by the ipcMain.handle setup in your main process entry point.
*/
@ipcClientEvent('proxyTRPCRequest')
public async proxyTRPCRequest(args: ProxyTRPCRequestParams): Promise<ProxyTRPCRequestResult> {
logger.debug('Received proxyTRPCRequest IPC call:', {
headers: args.headers,
method: args.method,
urlPath: args.urlPath, // Log headers too for context
});
const logPrefix = `[ProxyTRPC ${args.method} ${args.urlPath}]`; // Prefix for this specific request
try {
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
if (!config.active || (config.storageMode === 'selfHost' && !config.remoteServerUrl)) {
logger.warn(
`${logPrefix} Remote server sync not active or configured. Rejecting proxy request.`,
); // Enhanced log
return {
body: Buffer.from('Remote server sync not active or configured').buffer,
headers: {},
status: 503,
// Service Unavailable
statusText: 'Remote server sync not active or configured', // Return ArrayBuffer
};
}
const remoteServerUrl = await this.remoteServerConfigCtr.getRemoteServerUrl();
// Get initial token
let token = await this.remoteServerConfigCtr.getAccessToken();
logger.debug(
`${logPrefix} Initial token check: ${token ? 'Token exists' : 'No token found'}`,
); // Added log
logger.info(`${logPrefix} Attempting to forward request...`); // Added log
let response = await this.forwardRequest({ ...args, accessToken: token, remoteServerUrl });
// Handle 401: Refresh token and retry if necessary
if (response.status === 401) {
logger.info(`${logPrefix} Received 401 from forwarded request. Attempting token refresh.`); // Enhanced log
const refreshed = await this.refreshTokenIfNeeded(logPrefix); // Pass prefix for context
if (refreshed) {
const newToken = await this.remoteServerConfigCtr.getAccessToken();
if (newToken) {
logger.info(`${logPrefix} Token refreshed successfully, retrying the request.`); // Enhanced log
response = await this.forwardRequest({
...args,
accessToken: newToken,
remoteServerUrl,
});
} else {
logger.error(
`${logPrefix} Token refresh reported success, but failed to retrieve new token. Keeping original 401 response.`,
); // Enhanced log
// Keep the original 401 response
}
} else {
logger.error(`${logPrefix} Token refresh failed. Keeping original 401 response.`); // Enhanced log
// Keep the original 401 response
}
}
// Convert headers and body to format defined in IPC event
const responseHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(response.headers)) {
if (value !== undefined) {
responseHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;
}
}
// Return the final response, ensuring body is serializable (string or ArrayBuffer)
const responseBody = response.body; // Buffer
// IMPORTANT: Check IPC limits. Large bodies might fail. Consider chunking if needed.
// Convert Buffer to ArrayBuffer for IPC
const finalBody = responseBody.buffer.slice(
responseBody.byteOffset,
responseBody.byteOffset + responseBody.byteLength,
);
logger.debug(`${logPrefix} Forwarding successful. Status: ${response.status}`); // Added log
return {
body: finalBody as ArrayBuffer,
headers: responseHeaders,
status: response.status,
statusText: response.statusText, // Return ArrayBuffer
};
} catch (error) {
logger.error(`${logPrefix} Unhandled error processing proxyTRPCRequest:`, error); // Enhanced log
// Ensure a serializable error response is returned
return {
body: Buffer.from(
`Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
).buffer,
headers: {},
status: 500,
statusText: 'Internal Server Error during proxy', // Return ArrayBuffer
};
}
}
/**
* Attempts to refresh the access token by calling the RemoteServerConfigCtr.
* @returns Whether token refresh was successful
*/
private async refreshTokenIfNeeded(callerLogPrefix: string = '[RefreshToken]'): Promise<boolean> {
// Added prefix parameter
const logPrefix = `${callerLogPrefix} [RefreshTrigger]`; // Updated prefix
logger.debug(`${logPrefix} Entered refreshTokenIfNeeded.`);
try {
logger.info(`${logPrefix} Triggering refreshAccessToken in RemoteServerConfigCtr.`);
const result = await this.remoteServerConfigCtr.refreshAccessToken();
if (result.success) {
logger.info(`${logPrefix} refreshAccessToken call completed successfully.`);
return true;
} else {
logger.error(`${logPrefix} refreshAccessToken call failed: ${result.error}`);
return false;
}
} catch (error) {
logger.error(`${logPrefix} Exception occurred while calling refreshAccessToken:`, error);
return false;
}
}
/**
* Clean up resources - No protocol handler to unregister anymore
*/
destroy() {
logger.info('Destroying RemoteServerSyncCtr');
// Nothing specific to clean up here regarding request handling now
}
}

View File

@@ -0,0 +1,19 @@
import { ControllerModule, ipcClientEvent } from '.';
export default class ShortcutController extends ControllerModule {
/**
* 获取所有快捷键配置
*/
@ipcClientEvent('getShortcutsConfig')
getShortcutsConfig() {
return this.app.shortcutManager.getShortcutsConfig();
}
/**
* 更新单个快捷键配置
*/
@ipcClientEvent('updateShortcutConfig')
updateShortcutConfig(id: string, accelerator: string): boolean {
return this.app.shortcutManager.updateShortcutConfig(id, accelerator);
}
}

View File

@@ -0,0 +1,93 @@
import { ElectronAppState } from '@lobechat/electron-client-ipc';
import { app, systemPreferences } from 'electron';
import { macOS } from 'electron-is';
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import process from 'node:process';
import { DB_SCHEMA_HASH_FILENAME, LOCAL_DATABASE_DIR, userDataDir } from '@/const/dir';
import { ControllerModule, ipcClientEvent, ipcServerEvent } from './index';
export default class SystemController extends ControllerModule {
/**
* Handles the 'getDesktopAppState' IPC request.
* Gathers essential application and system information.
*/
@ipcClientEvent('getDesktopAppState')
async getAppState(): Promise<ElectronAppState> {
const platform = process.platform;
const arch = process.arch;
return {
// System Info
arch,
isLinux: platform === 'linux',
isMac: platform === 'darwin',
isWindows: platform === 'win32',
platform: platform as 'darwin' | 'win32' | 'linux',
userPath: {
// User Paths (ensure keys match UserPathData / DesktopAppState interface)
desktop: app.getPath('desktop'),
documents: app.getPath('documents'),
downloads: app.getPath('downloads'),
home: app.getPath('home'),
music: app.getPath('music'),
pictures: app.getPath('pictures'),
userData: app.getPath('userData'),
videos: app.getPath('videos'),
},
};
}
/**
* 检查可用性
*/
@ipcClientEvent('checkSystemAccessibility')
checkAccessibilityForMacOS() {
if (!macOS()) return;
return systemPreferences.isTrustedAccessibilityClient(true);
}
/**
* 更新应用语言设置
*/
@ipcClientEvent('updateLocale')
async updateLocale(locale: string) {
// 保存语言设置
this.app.storeManager.set('locale', locale);
// 更新i18n实例的语言
await this.app.i18n.changeLanguage(locale === 'auto' ? app.getLocale() : locale);
return { success: true };
}
@ipcServerEvent('getDatabasePath')
async getDatabasePath() {
return join(this.app.appStoragePath, LOCAL_DATABASE_DIR);
}
@ipcServerEvent('getDatabaseSchemaHash')
async getDatabaseSchemaHash() {
try {
return readFileSync(this.DB_SCHEMA_HASH_PATH, 'utf8');
} catch {
return undefined;
}
}
@ipcServerEvent('getUserDataPath')
async getUserDataPath() {
return userDataDir;
}
@ipcServerEvent('setDatabaseSchemaHash')
async setDatabaseSchemaHash(hash: string) {
writeFileSync(this.DB_SCHEMA_HASH_PATH, hash, 'utf8');
}
private get DB_SCHEMA_HASH_PATH() {
return join(this.app.appStoragePath, DB_SCHEMA_HASH_FILENAME);
}
}

Some files were not shown because too many files have changed in this diff Show More