mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-21 06:29:59 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2aa2d927a | |||
| 5e1ff5842e | |||
| 695f504c8b | |||
| 4d08c967c8 | |||
| 094f9a6dc7 | |||
| 0554093a8d | |||
| 51305a9566 | |||
| 39c5bd4081 | |||
| c199a85693 | |||
| c5833c232f | |||
| fb931674f6 | |||
| e6422793f0 | |||
| 794642fc57 | |||
| edb576fdd6 | |||
| 27eca4e649 | |||
| 40ea09f764 | |||
| 613dd23594 | |||
| b40caee32c | |||
| 5897d9e106 | |||
| cbfb4660cc | |||
| ffd0dbc7f5 | |||
| 3a52f5cf97 | |||
| 253521883d | |||
| 72734686e2 | |||
| 8969716168 | |||
| 666b2b0f0c | |||
| 0f7af4b898 | |||
| 11b6467f36 | |||
| 5c71db6c4e | |||
| 481cab0515 | |||
| 7ae17b62d3 | |||
| 4309730cc8 | |||
| eb5545bd7f | |||
| 1d526c2f7c | |||
| f831d8641c | |||
| 7c18071d21 |
@@ -0,0 +1,260 @@
|
||||
name: Release Desktop
|
||||
|
||||
on:
|
||||
workflow_dispatch: # 手动触发构建
|
||||
release:
|
||||
types: [published] # 发布 release 时触发构建
|
||||
pull_request:
|
||||
types: [synchronize, labeled, unlabeled] # PR 更新或标签变化时触发
|
||||
|
||||
# 确保同一时间只运行一个相同的 workflow,取消正在进行的旧的运行
|
||||
concurrency:
|
||||
group: ${{ github.ref }}-${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
PR_TAG_PREFIX: pr- # PR 构建版本的前缀标识
|
||||
|
||||
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'
|
||||
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: 8
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
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'
|
||||
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")
|
||||
|
||||
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})"
|
||||
|
||||
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
|
||||
|
||||
# 输出版本信息总结,方便在 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:
|
||||
os: [macos-latest, windows-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: 8
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install
|
||||
|
||||
- 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 }}
|
||||
|
||||
# macOS 构建处理
|
||||
- name: Build artifact on macOS
|
||||
if: runner.os == 'macOS'
|
||||
run: npm run desktop:build
|
||||
env:
|
||||
APP_URL: http://localhost:3010
|
||||
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 平台构建处理
|
||||
- name: Build artifact on other platforms
|
||||
if: runner.os != 'macOS'
|
||||
run: npm run desktop:build
|
||||
env:
|
||||
APP_URL: http://localhost:3010
|
||||
DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres'
|
||||
KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE='
|
||||
|
||||
# 上传构建产物,移除了 zip 相关部分
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-${{ matrix.os }}
|
||||
path: |
|
||||
apps/desktop/release/latest*
|
||||
apps/desktop/release/lobehub*.dmg*
|
||||
apps/desktop/release/lobehub*.exe*
|
||||
apps/desktop/release/lobehub*.AppImage
|
||||
retention-days: 5
|
||||
echo "🔄 Is PR Build: ${{ needs.version.outputs.is_pr_build }}"
|
||||
|
||||
merge:
|
||||
needs: [build, version]
|
||||
name: Merge Artifacts
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 下载所有平台的构建产物
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release
|
||||
pattern: release-*
|
||||
merge-multiple: true
|
||||
|
||||
# 列出所有构建产物
|
||||
- name: List artifacts
|
||||
run: ls -R release
|
||||
|
||||
publish:
|
||||
# 只有非 PR 构建且没有 [skip ci] 标记的提交才执行发布
|
||||
if: |
|
||||
needs.version.outputs.is_pr_build != 'true' &&
|
||||
!contains(github.event.head_commit.message, '[skip ci]')
|
||||
needs: [merge, 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: release
|
||||
pattern: release-*
|
||||
merge-multiple: true
|
||||
|
||||
# 列出所有构建产物
|
||||
- name: List artifacts
|
||||
run: ls -R release
|
||||
|
||||
# 对于非 release 触发的构建,创建为 draft 状态的 GitHub Release
|
||||
- name: Create Draft Release
|
||||
if: github.event_name != 'release'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Desktop v${{ needs.version.outputs.version }}
|
||||
tag_name: v${{ needs.version.outputs.version }}
|
||||
draft: true # A draft release
|
||||
prerelease: false
|
||||
files: |
|
||||
release/latest*
|
||||
release/umi*.dmg*
|
||||
release/umi*.exe*
|
||||
release/umi*.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# 对于 release 触发的构建,将构建产物上传到现有 release
|
||||
- name: Upload to existing Release
|
||||
if: github.event_name == 'release'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ github.event.release.tag_name }}
|
||||
files: |
|
||||
release/latest*
|
||||
release/umi*.dmg*
|
||||
release/umi*.exe*
|
||||
release/umi*.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+2
-1
@@ -68,4 +68,5 @@ public/swe-worker*
|
||||
*.patch
|
||||
*.pdf
|
||||
vertex-ai-key.json
|
||||
.pnpm-store
|
||||
.pnpm-store
|
||||
lobechat-db
|
||||
|
||||
+100
@@ -2,6 +2,106 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
### [Version 1.77.6](https://github.com/lobehub/lobe-chat/compare/v1.77.5...v1.77.6)
|
||||
|
||||
<sup>Released on **2025-04-01**</sup>
|
||||
|
||||
#### ♻ Code Refactoring
|
||||
|
||||
- **misc**: Refactor the db to context inject mode.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Code refactoring
|
||||
|
||||
- **misc**: Refactor the db to context inject mode, closes [#7255](https://github.com/lobehub/lobe-chat/issues/7255) ([ffd0dbc](https://github.com/lobehub/lobe-chat/commit/ffd0dbc))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.77.5](https://github.com/lobehub/lobe-chat/compare/v1.77.4...v1.77.5)
|
||||
|
||||
<sup>Released on **2025-04-01**</sup>
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.77.4](https://github.com/lobehub/lobe-chat/compare/v1.77.3...v1.77.4)
|
||||
|
||||
<sup>Released on **2025-03-31**</sup>
|
||||
|
||||
#### ♻ Code Refactoring
|
||||
|
||||
- **misc**: Refactor db core.
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Update branding.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Code refactoring
|
||||
|
||||
- **misc**: Refactor db core, closes [#7245](https://github.com/lobehub/lobe-chat/issues/7245) ([5c71db6](https://github.com/lobehub/lobe-chat/commit/5c71db6))
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Update branding, closes [#7224](https://github.com/lobehub/lobe-chat/issues/7224) ([481cab0](https://github.com/lobehub/lobe-chat/commit/481cab0))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.77.3](https://github.com/lobehub/lobe-chat/compare/v1.77.2...v1.77.3)
|
||||
|
||||
<sup>Released on **2025-03-29**</sup>
|
||||
|
||||
#### ♻ Code Refactoring
|
||||
|
||||
- **misc**: Move general db models to database folder.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Code refactoring
|
||||
|
||||
- **misc**: Move general db models to database folder, closes [#7222](https://github.com/lobehub/lobe-chat/issues/7222) ([f831d86](https://github.com/lobehub/lobe-chat/commit/f831d86))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.77.2](https://github.com/lobehub/lobe-chat/compare/v1.77.1...v1.77.2)
|
||||
|
||||
<sup>Released on **2025-03-29**</sup>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
*.log*
|
||||
standalone
|
||||
release
|
||||
@@ -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/
|
||||
@@ -0,0 +1,4 @@
|
||||
构建路径:
|
||||
|
||||
- dist: 构建产物路径
|
||||
- release: 发布产物路径
|
||||
Binary file not shown.
@@ -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: 161 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
@@ -0,0 +1,3 @@
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
updaterCacheDirName: electron-app-updater
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @type {import('electron-builder').Configuration}
|
||||
* @see https://www.electron.build/configuration
|
||||
*/
|
||||
const config = {
|
||||
appId: 'com.lobehub.lobehub-desktop',
|
||||
appImage: {
|
||||
artifactName: '${productName}-${version}.${ext}',
|
||||
},
|
||||
asar: false,
|
||||
// TODO: 研究下怎么样可以做成 asar 的模式
|
||||
// asar: { smartUnpack: false },
|
||||
// asarUnpack: ['dist/next'],
|
||||
directories: {
|
||||
buildResources: 'build',
|
||||
output: 'release',
|
||||
},
|
||||
dmg: {
|
||||
artifactName: '${productName}-${version}.${ext}',
|
||||
},
|
||||
electronDownload: {
|
||||
mirror: 'https://npmmirror.com/mirrors/electron/',
|
||||
},
|
||||
files: [
|
||||
'dist',
|
||||
'resources',
|
||||
'!dist/next/docs',
|
||||
'!dist/next/packages',
|
||||
'!dist/next/.next/server/app/sitemap',
|
||||
// '!dist/next/.next/static/media',
|
||||
],
|
||||
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.",
|
||||
},
|
||||
],
|
||||
notarize: false,
|
||||
},
|
||||
npmRebuild: true,
|
||||
nsis: {
|
||||
artifactName: '${productName}-${version}-setup.${ext}',
|
||||
createDesktopShortcut: 'always',
|
||||
shortcutName: '${productName}',
|
||||
uninstallDisplayName: '${productName}',
|
||||
},
|
||||
productName: 'LobeHub',
|
||||
publish: {
|
||||
provider: 'generic',
|
||||
url: 'https://example.com/auto-updates',
|
||||
},
|
||||
win: {
|
||||
executableName: 'electron-app',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
build: {
|
||||
outDir: 'dist/main',
|
||||
},
|
||||
plugins: [externalizeDepsPlugin({})],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src/main'),
|
||||
},
|
||||
},
|
||||
},
|
||||
preload: {
|
||||
build: {
|
||||
outDir: 'dist/preload',
|
||||
},
|
||||
plugins: [externalizeDepsPlugin({})],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "lobehub-desktop",
|
||||
"version": "0.0.2-nightly",
|
||||
"description": "A minimal Electron application with TypeScript",
|
||||
"homepage": "https://lobehub.com",
|
||||
"author": "arvinxx",
|
||||
"main": "./dist/main/index.js",
|
||||
"scripts": {
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"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:unpack": "npm run build && electron-builder --dir --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 ",
|
||||
"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-toolkit/utils": "^4.0.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-port-please": "^3.1.2",
|
||||
"next-electron-rsc": "^0.2.3"
|
||||
},
|
||||
"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",
|
||||
"@lobechat/electron-client-ipc": "workspace:*",
|
||||
"@lobechat/electron-server-ipc": "workspace:*",
|
||||
"electron": "35.1.1",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-is": "^3.0.0",
|
||||
"electron-vite": "^3.0.0",
|
||||
"pglite-server": "^0.1.4",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.2.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
- '../../packages/electron-server-ipc'
|
||||
- '../../packages/electron-client-ipc'
|
||||
- '.'
|
||||
@@ -0,0 +1,124 @@
|
||||
<!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 server, please wait...';
|
||||
|
||||
// 调用主进程的重试逻辑
|
||||
if (window.electron && window.electron.ipcRenderer) {
|
||||
window.electron.ipcRenderer.send('retry-connection');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { BrowserWindowOpts } from './core/Browser';
|
||||
|
||||
export const chat: BrowserWindowOpts = {
|
||||
autoHideMenuBar: true,
|
||||
height: 800,
|
||||
identifier: 'chat',
|
||||
keepAlive: true,
|
||||
minWidth: 400,
|
||||
path: '/chat',
|
||||
titleBarStyle: 'hidden',
|
||||
vibrancy: 'under-window',
|
||||
width: 1200,
|
||||
};
|
||||
|
||||
export const devtools: BrowserWindowOpts = {
|
||||
autoHideMenuBar: true,
|
||||
fullscreenable: false,
|
||||
height: 600,
|
||||
identifier: 'devtools',
|
||||
maximizable: false,
|
||||
minWidth: 400,
|
||||
path: '/desktop/devtools',
|
||||
titleBarStyle: 'hiddenInset',
|
||||
vibrancy: 'under-window',
|
||||
width: 1000,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
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');
|
||||
@@ -0,0 +1 @@
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
@@ -0,0 +1,9 @@
|
||||
import { devtools } from '../appBrowsers';
|
||||
import { ControllerModule } from './index';
|
||||
|
||||
export default class DevtoolsCtr extends ControllerModule {
|
||||
// @event('openDevtools')
|
||||
async openDevtools() {
|
||||
this.app.browserManager.retrieveOrInitialize(devtools);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ClientDispatchEvents } from '@lobechat/electron-client-ipc';
|
||||
|
||||
import type { App } from '../core/App';
|
||||
import { IoCContainer } from '../core/IoCContainer';
|
||||
|
||||
const baseDecorator =
|
||||
(name: string, showLog = true) =>
|
||||
(target: any, methodName: string, descriptor?: any) => {
|
||||
const actions = IoCContainer.controllers.get(target.constructor) || [];
|
||||
actions.push({
|
||||
methodName,
|
||||
name,
|
||||
showLog,
|
||||
});
|
||||
IoCContainer.controllers.set(target.constructor, actions);
|
||||
return descriptor;
|
||||
};
|
||||
|
||||
/**
|
||||
* service 用的 event 装饰器
|
||||
*/
|
||||
export const ipcClientEvent = (method: keyof ClientDispatchEvents) => baseDecorator(method);
|
||||
|
||||
export class ControllerModule {
|
||||
constructor(public app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
}
|
||||
|
||||
export type IControlModule = typeof ControllerModule;
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Session, app, ipcMain, protocol } from 'electron';
|
||||
import { macOS, windows } from 'electron-is';
|
||||
import { createHandler } from 'next-electron-rsc';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import * as appBrowsers from '../appBrowsers';
|
||||
import { buildDir, nextStandaloneDir } from '../const/dir';
|
||||
import { isDev } from '../const/env';
|
||||
import { IControlModule } from '../controllers';
|
||||
import BrowserManager from './BrowserManager';
|
||||
import { initIPCServer } from './IPCServer';
|
||||
import { IoCContainer } from './IoCContainer';
|
||||
|
||||
export type IPCClientEventMap = Map<string, any>;
|
||||
|
||||
const importAll = (r: any) => Object.values(r).map((v: any) => v.default);
|
||||
|
||||
export class App {
|
||||
/**
|
||||
* all controllers in app
|
||||
*/
|
||||
private controllers = new WeakMap();
|
||||
nextServerUrl = 'http://localhost:3010';
|
||||
|
||||
/**
|
||||
* 承接 webview fetch 的事件表
|
||||
*/
|
||||
private ipcClientEventMap: IPCClientEventMap = new Map();
|
||||
browserManager: BrowserManager;
|
||||
nextInterceptor: ({ session }: { session: Session }) => () => void;
|
||||
|
||||
constructor() {
|
||||
// load controllers
|
||||
const controllers: IControlModule[] = importAll(
|
||||
// @ts-ignore
|
||||
import.meta.glob('../controllers/*Ctr.ts', { eager: true }),
|
||||
);
|
||||
|
||||
controllers.forEach((service) => this.addController(service));
|
||||
|
||||
// 批量注册 controller 中 event 事件 供 render 端消费
|
||||
this.ipcClientEventMap.forEach((serviceInfo, key) => {
|
||||
// 获取相应方法
|
||||
const { service, methodName } = serviceInfo;
|
||||
|
||||
ipcMain.handle(key, async (e, ...data) => {
|
||||
try {
|
||||
return await service[methodName](...data);
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.browserManager = new BrowserManager(this);
|
||||
}
|
||||
|
||||
private onActivate = () => {
|
||||
this.browserManager.showMainWindow();
|
||||
};
|
||||
|
||||
bootstrap = async () => {
|
||||
// make single instance
|
||||
const isSingle = app.requestSingleInstanceLock();
|
||||
if (!isSingle) app.exit(0);
|
||||
|
||||
this.initDevBranding();
|
||||
|
||||
// ==============
|
||||
await initIPCServer();
|
||||
|
||||
// register the schema to interceptor url
|
||||
// it should register before app ready
|
||||
this.registerNextHandler();
|
||||
|
||||
await app.whenReady();
|
||||
|
||||
app.on('ready', async () => {
|
||||
this.initBrowsers();
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (windows()) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', this.onActivate);
|
||||
};
|
||||
|
||||
private addController = (ControllerClass: IControlModule) => {
|
||||
const service = new ControllerClass(this);
|
||||
this.controllers.set(ControllerClass, service);
|
||||
|
||||
IoCContainer.controllers.get(ControllerClass)?.forEach((event) => {
|
||||
// 将 event 装饰器中的对象全部存到 ipcClientEventMap 中
|
||||
this.ipcClientEventMap.set(event.name, {
|
||||
methodName: event.methodName,
|
||||
service,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private initDevBranding = () => {
|
||||
if (!isDev) return;
|
||||
|
||||
app.setName('LobeHub Dev');
|
||||
if (macOS()) {
|
||||
app.dock!.setIcon(join(buildDir, 'icon-dev.png'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加窗口
|
||||
|
||||
*/
|
||||
private initBrowsers() {
|
||||
Object.values(appBrowsers).forEach((item) => {
|
||||
this.browserManager.retrieveOrInitialize(item);
|
||||
});
|
||||
}
|
||||
|
||||
private registerNextHandler() {
|
||||
if (isDev) return;
|
||||
|
||||
const handler = createHandler({
|
||||
debug: true,
|
||||
localhostUrl: this.nextServerUrl,
|
||||
protocol,
|
||||
standaloneDir: nextStandaloneDir,
|
||||
});
|
||||
console.log(
|
||||
`[APP] Server Debugging Enabled, ${this.nextServerUrl} will be intercepted to ${nextStandaloneDir}`,
|
||||
);
|
||||
|
||||
this.nextInterceptor = handler.createInterceptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain } from 'electron';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { preloadDir, resourcesDir } from '../const/dir';
|
||||
import { isDev } from '../const/env';
|
||||
import type { App } from './App';
|
||||
|
||||
export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
|
||||
devTools?: boolean;
|
||||
height?: number;
|
||||
/**
|
||||
* URL
|
||||
*/
|
||||
identifier: string;
|
||||
keepAlive?: boolean;
|
||||
path: string;
|
||||
title?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export default class Browser {
|
||||
private app: App;
|
||||
|
||||
/**
|
||||
* 内部的 electron 窗口
|
||||
*/
|
||||
private _browserWindow?: BrowserWindow;
|
||||
|
||||
private stopInterceptHandler;
|
||||
/**
|
||||
* 标识符
|
||||
*/
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 生成时的选项
|
||||
*/
|
||||
options: BrowserWindowOpts;
|
||||
|
||||
/**
|
||||
* 对外暴露的获取窗口的方法
|
||||
*/
|
||||
get browserWindow() {
|
||||
return this.retrieveOrInitialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 BrowserWindows 对象的方法
|
||||
* @param options
|
||||
* @param application
|
||||
*/
|
||||
constructor(options: BrowserWindowOpts, application: App) {
|
||||
this.app = application;
|
||||
this.identifier = options.identifier;
|
||||
this.options = options;
|
||||
|
||||
// 初始化
|
||||
this.retrieveOrInitialize();
|
||||
}
|
||||
|
||||
loadUrl = async (path: string) => {
|
||||
const initUrl = this.app.nextServerUrl + path;
|
||||
|
||||
try {
|
||||
await this._browserWindow.loadURL(initUrl);
|
||||
console.log('[APP] Loaded', initUrl);
|
||||
} catch (error) {
|
||||
console.error('[APP] Failed to load URL:', error);
|
||||
|
||||
// 加载本地错误页面
|
||||
await this._browserWindow.loadFile(join(resourcesDir, 'error.html'));
|
||||
|
||||
// 设置简单的重试逻辑
|
||||
ipcMain.on('retry-connection', async () => {
|
||||
try {
|
||||
await this._browserWindow?.loadURL(initUrl);
|
||||
console.log('[APP] Reconnected successfully');
|
||||
} catch (err) {
|
||||
console.error('[APP] Retry failed:', err);
|
||||
// 重新加载错误页面,重置状态
|
||||
this._browserWindow?.loadFile(join(resourcesDir, 'error.html'));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadPlaceholder = async () => {
|
||||
// 首先加载一个本地的HTML加载页面
|
||||
await this._browserWindow.loadFile(join(resourcesDir, 'splash.html'));
|
||||
};
|
||||
|
||||
show() {
|
||||
this.browserWindow.show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.browserWindow.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁实例
|
||||
*/
|
||||
destroy() {
|
||||
this.stopInterceptHandler?.();
|
||||
this._browserWindow = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
retrieveOrInitialize() {
|
||||
// 当有这个窗口 且这个窗口没有被注销时
|
||||
if (this._browserWindow && !this._browserWindow.isDestroyed()) {
|
||||
return this._browserWindow;
|
||||
}
|
||||
|
||||
const { path, title, width, height, devTools, ...res } = this.options;
|
||||
|
||||
const browserWindow = new BrowserWindow({
|
||||
...res,
|
||||
height,
|
||||
show: false,
|
||||
title,
|
||||
webPreferences: {
|
||||
// 上下文隔离环境
|
||||
// https://www.electronjs.org/docs/tutorial/context-isolation
|
||||
contextIsolation: true,
|
||||
preload: join(preloadDir, 'index.js'),
|
||||
// devTools: isDev,
|
||||
},
|
||||
width,
|
||||
});
|
||||
|
||||
this._browserWindow = browserWindow;
|
||||
if (!isDev) {
|
||||
this.stopInterceptHandler = this.app.nextInterceptor({
|
||||
session: browserWindow.webContents.session,
|
||||
});
|
||||
}
|
||||
|
||||
// Windows 11 可以使用这个新 API
|
||||
if (process.platform === 'win32' && browserWindow.setBackgroundMaterial) {
|
||||
browserWindow.setBackgroundMaterial('acrylic');
|
||||
}
|
||||
|
||||
this.loadPlaceholder().then(() => {
|
||||
this.loadUrl(path).catch((e) => {
|
||||
console.error(`load url error, ${path}`, e);
|
||||
});
|
||||
});
|
||||
|
||||
// 显示 devtools 就打开
|
||||
if (devTools) {
|
||||
browserWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
browserWindow.once('ready-to-show', () => {
|
||||
browserWindow?.show();
|
||||
});
|
||||
|
||||
browserWindow.on('close', () => {
|
||||
// the ones who need keepAlive won't be destroyed
|
||||
this.stopInterceptHandler();
|
||||
if (this.options.keepAlive) {
|
||||
console.log('needto');
|
||||
// e.preventDefault();
|
||||
// browserWindow.hide();
|
||||
}
|
||||
});
|
||||
|
||||
return browserWindow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { chat } from '../appBrowsers';
|
||||
import type { App } from './App';
|
||||
import type { BrowserWindowOpts } from './Browser';
|
||||
import Browser from './Browser';
|
||||
|
||||
export default class BrowserManager {
|
||||
app: App;
|
||||
|
||||
browsers: Map<string, Browser | null> = new Map();
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动或初始化
|
||||
* @param options
|
||||
*/
|
||||
retrieveOrInitialize(options: BrowserWindowOpts) {
|
||||
let browser = this.browsers.get(options.identifier);
|
||||
if (browser) {
|
||||
return browser;
|
||||
}
|
||||
|
||||
browser = new Browser(options, this.app);
|
||||
|
||||
this.browsers.set(options.identifier, browser);
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
showMainWindow() {
|
||||
const window = this.retrieveOrInitialize(chat);
|
||||
window.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ElectronIPCServer } from '@lobechat/electron-server-ipc';
|
||||
|
||||
import { ipcEvent } from '../ipcServer';
|
||||
|
||||
const ipcServer = new ElectronIPCServer(ipcEvent);
|
||||
|
||||
export const initIPCServer = async (): Promise<ElectronIPCServer> => {
|
||||
await ipcServer.start();
|
||||
|
||||
return ipcServer;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 存储插件中的 service
|
||||
*/
|
||||
export class IoCContainer {
|
||||
static controllers: WeakMap<any, { methodName: string; name: string; showLog?: boolean }[]> =
|
||||
new WeakMap();
|
||||
|
||||
init() {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { App } from './core/App';
|
||||
|
||||
const app = new App();
|
||||
|
||||
app.bootstrap();
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IpcDispatchEvent } from '@lobechat/electron-server-ipc';
|
||||
import { app } from 'electron';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const userDataPath = app.getPath('userData');
|
||||
|
||||
const DB_SCHEMA_HASH_PATH = path.join(userDataPath, 'lobehub-local-db-schema-hash');
|
||||
|
||||
export const ipcEvent: IpcDispatchEvent = {
|
||||
getDatabasePath: async () => {
|
||||
return path.join(userDataPath, 'lobehub-local-db');
|
||||
},
|
||||
getDatabaseSchemaHash: async () => {
|
||||
try {
|
||||
return readFileSync(DB_SCHEMA_HASH_PATH, 'utf8');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
getUserDataPath: async () => {
|
||||
return userDataPath;
|
||||
},
|
||||
|
||||
setDatabaseSchemaHash: async (hash: string) => {
|
||||
writeFileSync(DB_SCHEMA_HASH_PATH, hash, 'utf8');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload';
|
||||
import { ClientDispatchEventKey, DispatchInvoke } from '@lobechat/electron-client-ipc';
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {};
|
||||
|
||||
// 添加 IPC 通信接口
|
||||
const ipcApi = {
|
||||
receive: (channel: string, callback: (...args: any[]) => void) => {
|
||||
// 包装回调函数,确保安全性
|
||||
const subscription = (_event: any, ...args: any[]) => callback(...args);
|
||||
ipcRenderer.on(channel, subscription);
|
||||
|
||||
// 返回取消订阅的函数
|
||||
return () => {
|
||||
ipcRenderer.removeListener(channel, subscription);
|
||||
};
|
||||
},
|
||||
send: (channel: string, ...args: any[]) => {
|
||||
console.log('channel', channel);
|
||||
ipcRenderer.send(channel, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('api', api);
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
...electronAPI,
|
||||
ipcRenderer: ipcApi,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* client 端请求 electron main 端方法
|
||||
*/
|
||||
const invoke: DispatchInvoke = async <T extends ClientDispatchEventKey>(event: T, ...data: any[]) =>
|
||||
ipcRenderer.invoke(event, ...data);
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', { invoke });
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"target": "ESNext",
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src/main/**/*", "src/preload/**/*", "electron-builder.js"]
|
||||
}
|
||||
@@ -1,4 +1,37 @@
|
||||
[
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Refactor the db to context inject mode."]
|
||||
},
|
||||
"date": "2025-04-01",
|
||||
"version": "1.77.6"
|
||||
},
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-04-01",
|
||||
"version": "1.77.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update branding."]
|
||||
},
|
||||
"date": "2025-03-31",
|
||||
"version": "1.77.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Move general db models to database folder."]
|
||||
},
|
||||
"date": "2025-03-29",
|
||||
"version": "1.77.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix decrypt error with imported pg data."]
|
||||
},
|
||||
"date": "2025-03-29",
|
||||
"version": "1.77.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix export button and clean orphan agent."]
|
||||
|
||||
@@ -578,7 +578,7 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
|
||||
- Type: Optional
|
||||
- Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `model_name->deploymentName=display_name` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
|
||||
- Default: `-`
|
||||
- Example: `-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-241226,+doubao-1.5-pro-256k->doubao-1-5-pro-256k-250115,+doubao-1.5-pro-32k->doubao-1-5-pro-32k-250115,+doubao-1.5-lite-32k->doubao-1-5-lite-32k-250115`
|
||||
- Example: `-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-250324,+doubao-1.5-pro-256k->doubao-1-5-pro-256k-250115,+doubao-1.5-pro-32k->doubao-1-5-pro-32k-250115,+doubao-1.5-lite-32k->doubao-1-5-lite-32k-250115`
|
||||
|
||||
### `VOLCENGINE_PROXY_URL`
|
||||
|
||||
|
||||
@@ -577,7 +577,7 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
|
||||
- 类型:可选
|
||||
- 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名->部署名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则见 [模型列表][model-list]
|
||||
- 默认值:`-`
|
||||
- 示例:`-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-241226,+doubao-1.5-pro-256k->doubao-1-5-pro-256k-250115,+doubao-1.5-pro-32k->doubao-1-5-pro-32k-250115,+doubao-1.5-lite-32k->doubao-1-5-lite-32k-250115`
|
||||
- 示例:`-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-250324,+doubao-1.5-pro-256k->doubao-1-5-pro-256k-250115,+doubao-1.5-pro-32k->doubao-1-5-pro-32k-250115,+doubao-1.5-lite-32k->doubao-1-5-lite-32k-250115`
|
||||
|
||||
### `VOLCENGINE_PROXY_URL`
|
||||
|
||||
|
||||
+42
-11
@@ -6,14 +6,48 @@ import ReactComponentName from 'react-scan/react-component-name/webpack';
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
const buildWithDocker = process.env.DOCKER === 'true';
|
||||
const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
|
||||
const enableReactScan = !!process.env.REACT_SCAN_MONITOR_API_KEY;
|
||||
const isUsePglite = process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite';
|
||||
|
||||
// if you need to proxy the api endpoint to remote server
|
||||
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH;
|
||||
const isStandaloneMode = buildWithDocker || isDesktop;
|
||||
|
||||
// 创建需要排除的特性映射
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
// const partialBuildPages = [
|
||||
// // {
|
||||
// // name: 'changelog',
|
||||
// // disabled: isDesktop,
|
||||
// // paths: ['src/app/[variants]/@modal/(.)changelog', 'src/app/[variants]/(main)/changelog'],
|
||||
// // },
|
||||
// {
|
||||
// name: 'desktop-devtools',
|
||||
// disabled: isDesktop,
|
||||
// paths: ['src/app/desktop'],
|
||||
// },
|
||||
// // {
|
||||
// // name: 'auth',
|
||||
// // disabled: isDesktop,
|
||||
// // paths: ['src/app/[variants]/(auth)'],
|
||||
// // },
|
||||
// {
|
||||
// name: 'desktop-trpc',
|
||||
// disabled: isDesktop,
|
||||
// paths: ['src/app/(backend)/trpc/desktop'],
|
||||
// },
|
||||
// ];
|
||||
/* eslint-enable */
|
||||
|
||||
const standaloneConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
outputFileTracingIncludes: { '*': ['public/**/*', '.next/static/**/*'] },
|
||||
};
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
...(isStandaloneMode ? standaloneConfig : {}),
|
||||
basePath,
|
||||
compress: isProd,
|
||||
experimental: {
|
||||
@@ -110,10 +144,6 @@ const nextConfig: NextConfig = {
|
||||
hmrRefreshes: true,
|
||||
},
|
||||
},
|
||||
output: buildWithDocker ? 'standalone' : undefined,
|
||||
outputFileTracingIncludes: buildWithDocker
|
||||
? { '*': ['public/**/*', '.next/static/**/*'] }
|
||||
: undefined,
|
||||
reactStrictMode: true,
|
||||
redirects: async () => [
|
||||
{
|
||||
@@ -231,13 +261,14 @@ const noWrapper = (config: NextConfig) => config;
|
||||
|
||||
const withBundleAnalyzer = process.env.ANALYZE === 'true' ? analyzer() : noWrapper;
|
||||
|
||||
const withPWA = isProd
|
||||
? withSerwistInit({
|
||||
register: false,
|
||||
swDest: 'public/sw.js',
|
||||
swSrc: 'src/app/sw.ts',
|
||||
})
|
||||
: noWrapper;
|
||||
const withPWA =
|
||||
isProd && !isDesktop
|
||||
? withSerwistInit({
|
||||
register: false,
|
||||
swDest: 'public/sw.js',
|
||||
swSrc: 'src/app/sw.ts',
|
||||
})
|
||||
: noWrapper;
|
||||
|
||||
const hasSentry = !!process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
const withSentry =
|
||||
|
||||
+13
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/chat",
|
||||
"version": "1.77.2",
|
||||
"version": "1.77.6",
|
||||
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
@@ -35,6 +35,7 @@
|
||||
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
||||
"build:analyze": "ANALYZE=true next build",
|
||||
"build:docker": "DOCKER=true next build && npm run build-sitemap",
|
||||
"build:electron": "cross-env NODE_OPTIONS=--max-old-space-size=6144 NEXT_PUBLIC_IS_DESKTOP_APP=1 NEXT_PUBLIC_SERVICE_MODE=server next build",
|
||||
"db:generate": "drizzle-kit generate && npm run db:generate-client && npm run workflow:dbml",
|
||||
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
|
||||
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
|
||||
@@ -43,6 +44,10 @@
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:visualize": "dbdocs build docs/developer/database-schema.dbml --project lobe-chat",
|
||||
"db:z-pull": "drizzle-kit introspect",
|
||||
"desktop:build": "npm run desktop:build-next && npm run desktop:prepare-dist && npm run desktop:build-electron",
|
||||
"desktop:build-electron": "tsx scripts/electronWorkflow/buildElectron.ts",
|
||||
"desktop:build-next": "npm run build:electron",
|
||||
"desktop:prepare-dist": "tsx scripts/electronWorkflow/moveNextStandalone.ts",
|
||||
"dev": "next dev --turbopack -p 3010",
|
||||
"docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx",
|
||||
"docs:seo": "lobe-seo && npm run lint:mdx",
|
||||
@@ -77,7 +82,8 @@
|
||||
"workflow:docs": "tsx ./scripts/docsWorkflow/index.ts",
|
||||
"workflow:i18n": "tsx ./scripts/i18nWorkflow/index.ts",
|
||||
"workflow:mdx": "tsx ./scripts/mdxWorkflow/index.ts",
|
||||
"workflow:readme": "tsx ./scripts/readmeWorkflow/index.ts"
|
||||
"workflow:readme": "tsx ./scripts/readmeWorkflow/index.ts",
|
||||
"workflow:set-desktop-version": "tsx ./scripts/electronWorkflow/setDesktopVersion.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.md": [
|
||||
@@ -129,6 +135,8 @@
|
||||
"@icons-pack/react-simple-icons": "9.6.0",
|
||||
"@khmyznikov/pwa-install": "0.3.9",
|
||||
"@langchain/community": "^0.3.37",
|
||||
"@lobechat/electron-client-ipc": "workspace:*",
|
||||
"@lobechat/electron-server-ipc": "workspace:*",
|
||||
"@lobechat/web-crawler": "workspace:*",
|
||||
"@lobehub/charts": "^1.12.0",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
@@ -136,7 +144,7 @@
|
||||
"@lobehub/icons": "^1.73.1",
|
||||
"@lobehub/tts": "^1.28.0",
|
||||
"@lobehub/ui": "^1.169.2",
|
||||
"@neondatabase/serverless": "^0.10.4",
|
||||
"@neondatabase/serverless": "^1.0.0",
|
||||
"@next/third-parties": "15.2.3",
|
||||
"@react-spring/web": "^9.7.5",
|
||||
"@sentry/nextjs": "^7.120.2",
|
||||
@@ -182,7 +190,7 @@
|
||||
"langfuse": "3.29.1",
|
||||
"langfuse-core": "3.29.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.483.0",
|
||||
"lucide-react": "^0.485.0",
|
||||
"mammoth": "^1.9.0",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"modern-screenshot": "^4.5.5",
|
||||
@@ -290,6 +298,7 @@
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"commitlint": "^19.6.1",
|
||||
"consola": "^3.3.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dbdocs": "^0.14.3",
|
||||
"dotenv": "^16.4.7",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# @lobechat/electron-client-ipc
|
||||
|
||||
这个包是 LobeChat 在 Electron 环境中用于处理 IPC(进程间通信)的客户端工具包。
|
||||
|
||||
## 介绍
|
||||
|
||||
在 Electron 应用中,IPC(进程间通信)是连接主进程(Main Process)、渲染进程(Renderer Process)以及 NextJS 进程的桥梁。为了更好地组织和管理这些通信,我们将 IPC 相关的代码分成了两个包:
|
||||
|
||||
- `@lobechat/electron-client-ipc`:**客户端 IPC 包**
|
||||
- `@lobechat/electron-server-ipc`:**服务端 IPC 包**
|
||||
|
||||
## 主要区别
|
||||
|
||||
### electron-client-ipc(本包)
|
||||
|
||||
- 运行环境:在渲染进程(Renderer Process)中运行
|
||||
- 主要职责:
|
||||
- 提供渲染进程调用主进程方法的接口定义
|
||||
- 封装 `ipcRenderer.invoke` 相关方法
|
||||
- 处理与主进程的通信请求
|
||||
|
||||
### electron-server-ipc
|
||||
|
||||
- 运行环境:在 Electron 主进程和 Next.js 服务端进程中运行
|
||||
- 主要职责:
|
||||
- 提供基于 Socket 的 IPC 通信机制
|
||||
- 实现服务端(ElectronIPCServer)和客户端(ElectronIpcClient)通信组件
|
||||
- 处理跨进程的请求和响应
|
||||
- 提供自动重连和错误处理机制
|
||||
- 确保类型安全的 API 调用
|
||||
|
||||
## 使用场景
|
||||
|
||||
当渲染进程需要:
|
||||
|
||||
- 访问系统 API
|
||||
- 进行文件操作
|
||||
- 调用主进程特定功能
|
||||
|
||||
时,都需要通过 `electron-client-ipc` 包提供的方法来发起请求。
|
||||
|
||||
## 技术说明
|
||||
|
||||
这种分包设计遵循了关注点分离原则,使得:
|
||||
|
||||
- IPC 通信接口清晰可维护
|
||||
- 客户端和服务端代码解耦
|
||||
- TypeScript 类型定义共享,确保类型安全
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@lobechat/electron-client-ipc",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface DevtoolsDispatchEvents {
|
||||
/**
|
||||
* open the LobeHub Devtools
|
||||
*/
|
||||
openDevtools: () => void;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { DevtoolsDispatchEvents } from './devtools';
|
||||
|
||||
/**
|
||||
* renderer -> main dispatch events
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ClientDispatchEvents extends DevtoolsDispatchEvents {}
|
||||
|
||||
export type ClientDispatchEventKey = keyof ClientDispatchEvents;
|
||||
|
||||
export type ClientEventReturnType<T extends ClientDispatchEventKey> = ReturnType<
|
||||
ClientDispatchEvents[T]
|
||||
>;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './events';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,10 @@
|
||||
import type {
|
||||
ClientDispatchEventKey,
|
||||
ClientDispatchEvents,
|
||||
ClientEventReturnType,
|
||||
} from '../events';
|
||||
|
||||
export type DispatchInvoke = <T extends ClientDispatchEventKey>(
|
||||
event: T,
|
||||
...data: Parameters<ClientDispatchEvents[T]>
|
||||
) => Promise<ClientEventReturnType<T>>;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './dispatch';
|
||||
@@ -0,0 +1,54 @@
|
||||
# @lobechat/electron-server-ipc
|
||||
|
||||
LobeHub 的 Electron 应用与服务端之间的 IPC(进程间通信)模块,提供可靠的跨进程通信能力。
|
||||
|
||||
## 📝 简介
|
||||
|
||||
`@lobechat/electron-server-ipc` 是 LobeHub 桌面应用的核心组件,负责处理 Electron 主进程与 nextjs 服务端之间的通信。它提供了一套简单而健壮的 API,用于在不同进程间传递数据和执行远程方法调用。
|
||||
|
||||
## 🛠️ 核心功能
|
||||
|
||||
- **可靠的 IPC 通信**: 基于 Socket 的通信机制,确保跨进程通信的稳定性和可靠性
|
||||
- **自动重连机制**: 客户端具备断线重连功能,提高应用稳定性
|
||||
- **类型安全**: 使用 TypeScript 提供完整的类型定义,确保 API 调用的类型安全
|
||||
- **跨平台支持**: 同时支持 Windows、macOS 和 Linux 平台
|
||||
|
||||
## 🧩 核心组件
|
||||
|
||||
### IPC 服务端 (ElectronIPCServer)
|
||||
|
||||
负责监听客户端请求并响应,通常运行在 Electron 的主进程中:
|
||||
|
||||
```typescript
|
||||
import { ElectronIPCEventHandler, ElectronIPCServer } from '@lobechat/electron-server-ipc';
|
||||
|
||||
// 定义处理函数
|
||||
const eventHandler: ElectronIPCEventHandler = {
|
||||
getDatabasePath: async () => {
|
||||
return '/path/to/database';
|
||||
},
|
||||
// 其他处理函数...
|
||||
};
|
||||
|
||||
// 创建并启动服务器
|
||||
const server = new ElectronIPCServer(eventHandler);
|
||||
server.start();
|
||||
```
|
||||
|
||||
### IPC 客户端 (ElectronIpcClient)
|
||||
|
||||
负责连接到服务端并发送请求,通常在服务端(如 Next.js 服务)中使用:
|
||||
|
||||
```typescript
|
||||
import { ElectronIPCMethods, ElectronIpcClient } from '@lobechat/electron-server-ipc';
|
||||
|
||||
// 创建客户端
|
||||
const client = new ElectronIpcClient();
|
||||
|
||||
// 发送请求
|
||||
const dbPath = await client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
||||
```
|
||||
|
||||
## 📌 说明
|
||||
|
||||
这是 LobeHub 的内部模块 (`"private": true`),专为 LobeHub 桌面应用设计,不作为独立包发布。
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@lobechat/electron-server-ipc",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const SOCK_FILE = 'lobehub-electron-ipc.sock';
|
||||
|
||||
export const SOCK_INFO_FILE = 'lobehub-electron-ipc-info.json';
|
||||
|
||||
export const WINDOW_PIPE_FILE = '\\\\.\\pipe\\lobehub-electron-ipc';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './ipcClient';
|
||||
export * from './ipcServer';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,211 @@
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ElectronIpcClient } from './ipcClient';
|
||||
import { ElectronIPCMethods } from './types';
|
||||
|
||||
// Mock node modules
|
||||
vi.mock('node:fs');
|
||||
vi.mock('node:net');
|
||||
vi.mock('node:os');
|
||||
vi.mock('node:path');
|
||||
|
||||
describe('ElectronIpcClient', () => {
|
||||
// Mock data
|
||||
const mockTempDir = '/mock/temp/dir';
|
||||
const mockSocketInfoPath = '/mock/temp/dir/lobehub-electron-ipc-info.json';
|
||||
const mockSocketInfo = { socketPath: '/mock/socket/path' };
|
||||
|
||||
// Mock socket
|
||||
const mockSocket = {
|
||||
on: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Use fake timers
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Reset all mocks
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Setup common mocks
|
||||
vi.mocked(os.tmpdir).mockReturnValue(mockTempDir);
|
||||
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
|
||||
vi.mocked(net.createConnection).mockReturnValue(mockSocket as unknown as net.Socket);
|
||||
|
||||
// Mock console methods
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with socket path from info file if it exists', () => {
|
||||
// Setup
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
|
||||
|
||||
// Execute
|
||||
new ElectronIpcClient();
|
||||
|
||||
// Verify
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(mockSocketInfoPath);
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(mockSocketInfoPath, 'utf8');
|
||||
});
|
||||
|
||||
it('should initialize with default socket path if info file does not exist', () => {
|
||||
// Setup
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
// Execute
|
||||
new ElectronIpcClient();
|
||||
|
||||
// Verify
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(mockSocketInfoPath);
|
||||
expect(fs.readFileSync).not.toHaveBeenCalled();
|
||||
|
||||
// Test platform-specific behavior
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
new ElectronIpcClient();
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
it('should handle initialization errors gracefully', () => {
|
||||
// Setup - Mock the error
|
||||
vi.mocked(fs.existsSync).mockImplementation(() => {
|
||||
throw new Error('Mock file system error');
|
||||
});
|
||||
|
||||
// Execute
|
||||
new ElectronIpcClient();
|
||||
|
||||
// Verify
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Failed to initialize IPC client:',
|
||||
expect.objectContaining({ message: 'Mock file system error' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connection and request handling', () => {
|
||||
let client: ElectronIpcClient;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup a client with a known socket path
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
|
||||
client = new ElectronIpcClient();
|
||||
|
||||
// Reset socket mocks for each test
|
||||
mockSocket.on.mockReset();
|
||||
mockSocket.write.mockReset();
|
||||
|
||||
// Default implementation for socket.on
|
||||
mockSocket.on.mockImplementation((event, callback) => {
|
||||
return mockSocket;
|
||||
});
|
||||
|
||||
// Default implementation for socket.write
|
||||
mockSocket.write.mockImplementation((data, callback) => {
|
||||
if (callback) callback();
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle connection errors', async () => {
|
||||
// Start request - but don't await it yet
|
||||
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
||||
|
||||
// Find the error event handler
|
||||
const errorCallArgs = mockSocket.on.mock.calls.find((call) => call[0] === 'error');
|
||||
if (errorCallArgs && typeof errorCallArgs[1] === 'function') {
|
||||
const errorHandler = errorCallArgs[1];
|
||||
|
||||
// Trigger the error handler
|
||||
errorHandler(new Error('Connection error'));
|
||||
}
|
||||
|
||||
// Now await the promise
|
||||
await expect(requestPromise).rejects.toThrow('Connection error');
|
||||
});
|
||||
|
||||
it('should handle write errors', async () => {
|
||||
// Setup connection callback
|
||||
let connectionCallback: Function | undefined;
|
||||
vi.mocked(net.createConnection).mockImplementation((path, callback) => {
|
||||
connectionCallback = callback as Function;
|
||||
return mockSocket as unknown as net.Socket;
|
||||
});
|
||||
|
||||
// Setup write to fail
|
||||
mockSocket.write.mockImplementation((data, callback) => {
|
||||
if (callback) callback(new Error('Write error'));
|
||||
return true;
|
||||
});
|
||||
|
||||
// Start request
|
||||
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
||||
|
||||
// Simulate connection established
|
||||
if (connectionCallback) connectionCallback();
|
||||
|
||||
// Now await the promise
|
||||
await expect(requestPromise).rejects.toThrow('Write error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('close method', () => {
|
||||
let client: ElectronIpcClient;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup a client with a known socket path
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
|
||||
client = new ElectronIpcClient();
|
||||
|
||||
// Setup socket.on
|
||||
mockSocket.on.mockImplementation((event, callback) => {
|
||||
return mockSocket;
|
||||
});
|
||||
});
|
||||
|
||||
it('should close the socket connection', async () => {
|
||||
// Setup connection callback
|
||||
let connectionCallback: Function | undefined;
|
||||
vi.mocked(net.createConnection).mockImplementation((path, callback) => {
|
||||
connectionCallback = callback as Function;
|
||||
return mockSocket as unknown as net.Socket;
|
||||
});
|
||||
|
||||
// Start a request to establish connection (but don't wait for it)
|
||||
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath).catch(() => {}); // Ignore any errors
|
||||
|
||||
// Simulate connection
|
||||
if (connectionCallback) connectionCallback();
|
||||
|
||||
// Close the connection
|
||||
client.close();
|
||||
|
||||
// Verify
|
||||
expect(mockSocket.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle close when not connected', () => {
|
||||
// Close without connecting
|
||||
client.close();
|
||||
|
||||
// Verify no errors
|
||||
expect(mockSocket.end).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
||||
import { IElectronIPCMethods } from './types';
|
||||
|
||||
export class ElectronIpcClient {
|
||||
private socketPath: string | null = null;
|
||||
private connected: boolean = false;
|
||||
private socket: net.Socket | null = null;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
private requestQueue: Map<string, { reject: Function; resolve: Function }> = new Map();
|
||||
// eslint-disable-next-line no-undef
|
||||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
private connectionAttempts: number = 0;
|
||||
private maxConnectionAttempts: number = 5;
|
||||
|
||||
constructor() {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
// 初始化客户端
|
||||
private initialize() {
|
||||
try {
|
||||
// 从临时文件读取套接字路径
|
||||
const tempDir = os.tmpdir();
|
||||
const socketInfoPath = path.join(tempDir, SOCK_INFO_FILE);
|
||||
|
||||
if (fs.existsSync(socketInfoPath)) {
|
||||
const socketInfo = JSON.parse(fs.readFileSync(socketInfoPath, 'utf8'));
|
||||
this.socketPath = socketInfo.socketPath;
|
||||
} else {
|
||||
// 如果找不到套接字信息,使用默认路径
|
||||
this.socketPath =
|
||||
process.platform === 'win32' ? WINDOW_PIPE_FILE : path.join(os.tmpdir(), SOCK_FILE);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize IPC client:', err);
|
||||
this.socketPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 连接到 Electron IPC 服务器
|
||||
private connect(): Promise<void> {
|
||||
if (this.connected || !this.socketPath) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.socket = net.createConnection(this.socketPath!, () => {
|
||||
this.connected = true;
|
||||
this.connectionAttempts = 0;
|
||||
console.log('Connected to Electron IPC server');
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.socket.on('data', (data) => {
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
const { id, result, error } = response;
|
||||
|
||||
const pending = this.requestQueue.get(id);
|
||||
if (pending) {
|
||||
this.requestQueue.delete(id);
|
||||
if (error) {
|
||||
pending.reject(new Error(error));
|
||||
} else {
|
||||
pending.resolve(result);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to parse response:', err);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
this.connected = false;
|
||||
this.handleDisconnect();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.socket.on('close', () => {
|
||||
console.log('Socket closed');
|
||||
this.connected = false;
|
||||
this.handleDisconnect();
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to connect to IPC server:', err);
|
||||
this.handleDisconnect();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理断开连接
|
||||
private handleDisconnect() {
|
||||
// 清除重连定时器
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
// 拒绝所有待处理的请求
|
||||
for (const [, { reject }] of this.requestQueue) {
|
||||
reject(new Error('Connection to Electron IPC server lost'));
|
||||
}
|
||||
this.requestQueue.clear();
|
||||
|
||||
// 尝试重新连接
|
||||
if (this.connectionAttempts < this.maxConnectionAttempts) {
|
||||
this.connectionAttempts++;
|
||||
const delay = Math.min(1000 * Math.pow(2, this.connectionAttempts - 1), 30_000);
|
||||
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.connect().catch((err) => {
|
||||
console.error(`Reconnection attempt ${this.connectionAttempts} failed:`, err);
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求到 Electron IPC 服务器
|
||||
public async sendRequest<T>(method: IElectronIPCMethods, params: any = {}): Promise<T> {
|
||||
if (!this.socketPath) {
|
||||
throw new Error('Electron IPC connection not available');
|
||||
}
|
||||
|
||||
// 如果未连接,先连接
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
try {
|
||||
const id = Math.random().toString(36).slice(2, 15);
|
||||
const request = { id, method, params };
|
||||
|
||||
// 将请求添加到队列
|
||||
this.requestQueue.set(id, { reject, resolve });
|
||||
|
||||
// 设置超时
|
||||
const timeout = setTimeout(() => {
|
||||
this.requestQueue.delete(id);
|
||||
reject(new Error(`Request ${method} timed out`));
|
||||
}, 10_000);
|
||||
|
||||
// 发送请求
|
||||
this.socket!.write(JSON.stringify(request), (err) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
this.requestQueue.delete(id);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
public close() {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.end();
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
||||
import { IElectronIPCMethods } from './types';
|
||||
|
||||
export type IPCEventMethod = (
|
||||
params: any,
|
||||
context: { id: string; method: string; socket: net.Socket },
|
||||
) => Promise<any>;
|
||||
|
||||
export type ElectronIPCEventHandler = {
|
||||
[key in IElectronIPCMethods]: IPCEventMethod;
|
||||
};
|
||||
|
||||
export class ElectronIPCServer {
|
||||
private server: net.Server;
|
||||
private socketPath: string;
|
||||
private eventHandler: ElectronIPCEventHandler;
|
||||
|
||||
constructor(eventHandler: ElectronIPCEventHandler) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
// 创建唯一的套接字路径,避免冲突
|
||||
this.socketPath = isWindows ? WINDOW_PIPE_FILE : path.join(os.tmpdir(), SOCK_FILE);
|
||||
|
||||
// 如果是 Unix 套接字,确保文件不存在
|
||||
if (!isWindows && fs.existsSync(this.socketPath)) {
|
||||
fs.unlinkSync(this.socketPath);
|
||||
}
|
||||
|
||||
// 创建服务器
|
||||
this.server = net.createServer(this.handleConnection.bind(this));
|
||||
|
||||
this.eventHandler = eventHandler;
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
public start(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.on('error', (err) => {
|
||||
console.error('IPC Server error:', err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.server.listen(this.socketPath, () => {
|
||||
console.log(`Electron IPC server listening on ${this.socketPath}`);
|
||||
|
||||
// 将套接字路径写入临时文件,供 Next.js 服务端读取
|
||||
const tempDir = os.tmpdir();
|
||||
const socketInfoPath = path.join(tempDir, SOCK_INFO_FILE);
|
||||
fs.writeFileSync(socketInfoPath, JSON.stringify({ socketPath: this.socketPath }), 'utf8');
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 处理客户端连接
|
||||
private handleConnection(socket: net.Socket): void {
|
||||
let dataBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
dataBuffer += data.toString();
|
||||
|
||||
try {
|
||||
// 尝试解析 JSON 消息
|
||||
const message = JSON.parse(dataBuffer);
|
||||
dataBuffer = ''; // 重置缓冲区
|
||||
|
||||
// 处理请求
|
||||
this.handleRequest(socket, message);
|
||||
} catch {
|
||||
// 如果不是有效的 JSON,可能是消息不完整,继续等待
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理客户端请求
|
||||
private handleRequest = async (socket: net.Socket, request: any) => {
|
||||
const { id, method, params } = request;
|
||||
|
||||
// 根据请求方法执行相应的操作
|
||||
const eventHandler = this.eventHandler[method as IElectronIPCMethods];
|
||||
if (!eventHandler) return;
|
||||
|
||||
try {
|
||||
const data = await eventHandler(params, { id, method, socket });
|
||||
|
||||
this.sendResult(socket, id, data);
|
||||
} catch (err) {
|
||||
this.sendError(socket, id, `Failed to handle method(${method}): ${(err as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 发送结果
|
||||
private sendResult(socket: net.Socket, id: string, result: any): void {
|
||||
socket.write(JSON.stringify({ id, result }));
|
||||
}
|
||||
|
||||
// 发送错误
|
||||
private sendError(socket: net.Socket, id: string, error: string): void {
|
||||
socket.write(JSON.stringify({ error, id }));
|
||||
}
|
||||
|
||||
// 关闭服务器
|
||||
public close(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.server.close(() => {
|
||||
console.log('Electron IPC server closed');
|
||||
|
||||
// 删除套接字文件(Unix 平台)
|
||||
if (process.platform !== 'win32' && fs.existsSync(this.socketPath)) {
|
||||
fs.unlinkSync(this.socketPath);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable typescript-sort-keys/interface, sort-keys-fix/sort-keys-fix */
|
||||
export const ElectronIPCMethods = {
|
||||
getDatabasePath: 'getDatabasePath',
|
||||
getUserDataPath: 'getUserDataPath',
|
||||
|
||||
getDatabaseSchemaHash: 'getDatabaseSchemaHash',
|
||||
setDatabaseSchemaHash: 'setDatabaseSchemaHash',
|
||||
} as const;
|
||||
|
||||
export type IElectronIPCMethods = keyof typeof ElectronIPCMethods;
|
||||
|
||||
export interface IpcDispatchEvent {
|
||||
getDatabasePath: () => Promise<string>;
|
||||
getUserDataPath: () => Promise<string>;
|
||||
|
||||
getDatabaseSchemaHash: () => Promise<string | undefined>;
|
||||
setDatabaseSchemaHash: (hash: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './event';
|
||||
@@ -1,3 +1,4 @@
|
||||
packages:
|
||||
- 'packages/**'
|
||||
- '.'
|
||||
- '!apps/**'
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable unicorn/no-process-exit */
|
||||
import { execSync } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
|
||||
/**
|
||||
* Build desktop application based on current operating system platform
|
||||
*/
|
||||
const buildElectron = () => {
|
||||
const platform = os.platform();
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log(`🔨 Starting to build desktop app for ${platform} platform...`);
|
||||
|
||||
try {
|
||||
let buildCommand = '';
|
||||
|
||||
// Determine build command based on platform
|
||||
switch (platform) {
|
||||
case 'darwin': {
|
||||
buildCommand = 'npm run build:mac --prefix=./apps/desktop';
|
||||
console.log('📦 Building macOS desktop application...');
|
||||
break;
|
||||
}
|
||||
case 'win32': {
|
||||
buildCommand = 'npm run build:win --prefix=./apps/desktop';
|
||||
console.log('📦 Building Windows desktop application...');
|
||||
break;
|
||||
}
|
||||
case 'linux': {
|
||||
buildCommand = 'npm run build:linux --prefix=./apps/desktop';
|
||||
console.log('📦 Building Linux desktop application...');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute build command
|
||||
execSync(buildCommand, { stdio: 'inherit' });
|
||||
|
||||
const endTime = Date.now();
|
||||
const buildTime = ((endTime - startTime) / 1000).toFixed(2);
|
||||
console.log(`✅ Desktop application build completed! (${buildTime}s)`);
|
||||
} catch (error) {
|
||||
console.error('❌ Build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute build
|
||||
buildElectron();
|
||||
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable unicorn/no-process-exit */
|
||||
import fs from 'fs-extra';
|
||||
import { execSync } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
|
||||
// 定义源目录和目标目录
|
||||
const sourceDir: string = path.join(rootDir, '.next/standalone');
|
||||
const targetDir: string = path.join(rootDir, 'apps/desktop/dist/next');
|
||||
|
||||
// 确保目标目录的父目录存在
|
||||
fs.ensureDirSync(path.dirname(targetDir));
|
||||
|
||||
// 如果目标目录已存在,先删除它
|
||||
if (fs.existsSync(targetDir)) {
|
||||
console.log(`🗑️ Target directory ${targetDir} already exists, deleting...`);
|
||||
try {
|
||||
fs.removeSync(targetDir);
|
||||
console.log(`✅ Old target directory removed successfully`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to delete target directory: ${error}`);
|
||||
console.log('🔄 Trying to delete using system command...');
|
||||
try {
|
||||
if (os.platform() === 'win32') {
|
||||
execSync(`rmdir /S /Q "${targetDir}"`, { stdio: 'inherit' });
|
||||
} else {
|
||||
execSync(`rm -rf "${targetDir}"`, { stdio: 'inherit' });
|
||||
}
|
||||
console.log('✅ Successfully deleted old target directory');
|
||||
} catch (cmdError) {
|
||||
console.error(`❌ Unable to delete target directory, might need manual cleanup: ${cmdError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🚚 Moving ${sourceDir} to ${targetDir}...`);
|
||||
|
||||
try {
|
||||
// 使用 fs-extra 的 move 方法
|
||||
fs.moveSync(sourceDir, targetDir, { overwrite: true });
|
||||
console.log(`✅ Directory moved successfully!`);
|
||||
} catch (error) {
|
||||
console.error('❌ fs-extra move failed:', error);
|
||||
console.log('🔄 Trying to move using system command...');
|
||||
|
||||
try {
|
||||
// 使用系统命令进行移动
|
||||
if (os.platform() === 'win32') {
|
||||
execSync(`move "${sourceDir}" "${targetDir}"`, { stdio: 'inherit' });
|
||||
} else {
|
||||
execSync(`mv "${sourceDir}" "${targetDir}"`, { stdio: 'inherit' });
|
||||
}
|
||||
console.log('✅ System command move completed successfully!');
|
||||
} catch (mvError) {
|
||||
console.error('❌ Failed to move directory:', mvError);
|
||||
console.log('💡 Try running manually: sudo mv ' + sourceDir + ' ' + targetDir);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🎉 Move completed!`);
|
||||
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable unicorn/no-process-exit */
|
||||
import fs from 'fs-extra';
|
||||
import path from 'node:path';
|
||||
|
||||
// 获取脚本的命令行参数
|
||||
const version = process.argv[2];
|
||||
|
||||
if (!version) {
|
||||
console.error('Missing version parameter, usage: bun run setDesktopVersion.ts <version>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 获取根目录
|
||||
const rootDir = path.resolve(__dirname, '../..');
|
||||
|
||||
// 桌面应用 package.json 的路径
|
||||
const desktopPackageJsonPath = path.join(rootDir, 'apps/desktop/package.json');
|
||||
|
||||
function updateVersion() {
|
||||
try {
|
||||
// 确保文件存在
|
||||
if (!fs.existsSync(desktopPackageJsonPath)) {
|
||||
console.error(`Error: File not found ${desktopPackageJsonPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 读取 package.json 文件
|
||||
const packageJson = fs.readJSONSync(desktopPackageJsonPath);
|
||||
|
||||
// 更新版本号
|
||||
packageJson.version = version;
|
||||
|
||||
// 写回文件
|
||||
fs.writeJsonSync(desktopPackageJsonPath, packageJson, { spaces: 2 });
|
||||
|
||||
console.log(`Desktop app version updated to: ${version}`);
|
||||
} catch (error) {
|
||||
console.error('Error updating version:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
updateVersion();
|
||||
@@ -3,7 +3,7 @@ import { migrate as neonMigrate } from 'drizzle-orm/neon-serverless/migrator';
|
||||
import { migrate as nodeMigrate } from 'drizzle-orm/node-postgres/migrator';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { serverDB } from '../../src/database/server/core/db';
|
||||
import { serverDB } from '../../src/database/server';
|
||||
import { DB_FAIL_INIT_HINT, PGVECTOR_HINT } from './errorHint';
|
||||
|
||||
// Read the `.env` file if it exists, or a file specified by the
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { pino } from '@/libs/logger';
|
||||
import { createContext } from '@/server/context';
|
||||
import { desktopRouter } from '@/server/routers/desktop';
|
||||
|
||||
const handler = (req: NextRequest) =>
|
||||
fetchRequestHandler({
|
||||
/**
|
||||
* @link https://trpc.io/docs/v11/context
|
||||
*/
|
||||
createContext: () => createContext(req),
|
||||
|
||||
endpoint: '/trpc/desktop',
|
||||
|
||||
onError: ({ error, path, type }) => {
|
||||
pino.info(`Error in tRPC handler (tools) on path: ${path}, type: ${type}`);
|
||||
console.error(error);
|
||||
},
|
||||
|
||||
req,
|
||||
router: desktopRouter,
|
||||
});
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -0,0 +1,12 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
|
||||
const Layout = ({ children }: PropsWithChildren) => {
|
||||
if (isDesktop) return notFound();
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { Center } from 'react-layout-kit';
|
||||
|
||||
import BrandWatermark from '@/components/BrandWatermark';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { metadataModule } from '@/server/metadata';
|
||||
import { translation } from '@/server/translation';
|
||||
import { DynamicLayoutProps } from '@/types/next';
|
||||
@@ -20,6 +21,7 @@ export const generateMetadata = async (props: DynamicLayoutProps) => {
|
||||
};
|
||||
|
||||
const Page = async (props: DynamicLayoutProps) => {
|
||||
if (isDesktop) return notFound();
|
||||
const isMobile = await RouteVariants.getIsMobile(props);
|
||||
|
||||
if (!isMobile) return redirect('/chat');
|
||||
|
||||
@@ -4,10 +4,12 @@ import { SideNav } from '@lobehub/ui';
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs';
|
||||
import { Suspense, memo } from 'react';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { useActiveTabKey } from '@/hooks/useActiveTabKey';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { electronStylish } from '@/styles/electron';
|
||||
|
||||
import Avatar from './Avatar';
|
||||
import BottomActions from './BottomActions';
|
||||
@@ -28,13 +30,28 @@ const Nav = memo(() => {
|
||||
return (
|
||||
!inZenMode && (
|
||||
<SideNav
|
||||
avatar={<Avatar />}
|
||||
bottomActions={<BottomActions />}
|
||||
style={{ height: '100%', zIndex: 100 }}
|
||||
avatar={
|
||||
<div className={electronStylish.nodrag}>
|
||||
<Avatar />
|
||||
</div>
|
||||
}
|
||||
bottomActions={
|
||||
<div className={electronStylish.nodrag}>
|
||||
<BottomActions />
|
||||
</div>
|
||||
}
|
||||
className={electronStylish.draggable}
|
||||
style={{
|
||||
height: '100%',
|
||||
zIndex: 100,
|
||||
...(isDesktop ? { background: 'transparent', paddingTop: 24 } : {}),
|
||||
}}
|
||||
topActions={
|
||||
<Suspense>
|
||||
<Top />
|
||||
{showPinList && <PinList />}
|
||||
<div className={electronStylish.nodrag}>
|
||||
<Top />
|
||||
{showPinList && <PinList />}
|
||||
</div>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import ServerLayout from '@/components/server/ServerLayout';
|
||||
import { isDesktop } from '@/const/version';
|
||||
|
||||
import Desktop from './_layout/Desktop';
|
||||
import Mobile from './_layout/Mobile';
|
||||
|
||||
const MainLayout = ServerLayout({ Desktop, Mobile });
|
||||
const Layout = ServerLayout({ Desktop, Mobile });
|
||||
|
||||
const MainLayout = (props: { children: ReactNode }) => {
|
||||
if (isDesktop) return notFound();
|
||||
|
||||
return <Layout {...props} />;
|
||||
};
|
||||
|
||||
MainLayout.displayName = 'ChangelogLayout';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ChatHeader } from '@lobehub/ui/chat';
|
||||
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { electronStylish } from '@/styles/electron';
|
||||
|
||||
import HeaderAction from './HeaderAction';
|
||||
import Main from './Main';
|
||||
@@ -14,8 +15,17 @@ const Header = () => {
|
||||
return (
|
||||
showHeader && (
|
||||
<ChatHeader
|
||||
left={<Main />}
|
||||
right={<HeaderAction />}
|
||||
className={electronStylish.draggable}
|
||||
left={
|
||||
<div className={electronStylish.nodrag}>
|
||||
<Main />
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
<div className={electronStylish.nodrag}>
|
||||
<HeaderAction />
|
||||
</div>
|
||||
}
|
||||
style={{ height: 48, minHeight: 48, paddingInline: 8, position: 'initial', zIndex: 11 }}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Suspense } from 'react';
|
||||
import StructuredData from '@/components/StructuredData';
|
||||
import { serverFeatureFlags } from '@/config/featureFlags';
|
||||
import { BRANDING_NAME } from '@/const/branding';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { ldModule } from '@/server/ld';
|
||||
import { metadataModule } from '@/server/metadata';
|
||||
import { translation } from '@/server/translation';
|
||||
@@ -38,7 +39,7 @@ const Page = async (props: DynamicLayoutProps) => {
|
||||
<StructuredData ld={ld} />
|
||||
<PageTitle />
|
||||
<TelemetryNotification mobile={isMobile} />
|
||||
{showChangelog && !hideDocs && !isMobile && (
|
||||
{!isDesktop && showChangelog && !hideDocs && !isMobile && (
|
||||
<Suspense>
|
||||
<Changelog />
|
||||
</Suspense>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { KnowledgeBaseModel } from '@/database/models/knowledgeBase';
|
||||
import { serverDB } from '@/database/server';
|
||||
import { KnowledgeBaseModel } from '@/database/server/models/knowledgeBase';
|
||||
|
||||
import Head from './Head';
|
||||
import Menu from './Menu';
|
||||
@@ -11,9 +11,10 @@ interface Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type Props = { params: Params };
|
||||
type Props = { params: Promise<Params> };
|
||||
|
||||
const MenuPage = async ({ params }: Props) => {
|
||||
const MenuPage = async (props: Props) => {
|
||||
const params = await props.params;
|
||||
const id = params.id;
|
||||
const item = await KnowledgeBaseModel.findById(serverDB, params.id);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { KnowledgeBaseModel } from '@/database/models/knowledgeBase';
|
||||
import { serverDB } from '@/database/server';
|
||||
import { KnowledgeBaseModel } from '@/database/server/models/knowledgeBase';
|
||||
import FileManager from '@/features/FileManager';
|
||||
import { PagePropsWithId } from '@/types/next';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { DEFAULT_MODEL_PROVIDER_LIST } from '@/config/modelProviders';
|
||||
import { isServerMode } from '@/const/version';
|
||||
import { AiInfraRepos } from '@/database/repositories/aiInfra';
|
||||
import { serverDB } from '@/database/server';
|
||||
import { AiInfraRepos } from '@/database/repositories/aiInfra';
|
||||
import { getServerGlobalConfig } from '@/server/globalConfig';
|
||||
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
import { PagePropsWithId } from '@/types/next';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { serverFeatureFlags } from '@/config/featureFlags';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { ChangelogService } from '@/server/services/changelog';
|
||||
import { DynamicLayoutProps } from '@/types/next';
|
||||
import { RouteVariants } from '@/utils/server/routeVariants';
|
||||
@@ -11,6 +12,8 @@ import UpdateChangelogStatus from './features/UpdateChangelogStatus';
|
||||
import Loading from './loading';
|
||||
|
||||
const Page = async (props: DynamicLayoutProps) => {
|
||||
if (isDesktop) return notFound();
|
||||
|
||||
const hideDocs = serverFeatureFlags().hideDocs;
|
||||
if (hideDocs) return notFound();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { isRtlLang } from 'rtl-detect';
|
||||
|
||||
import Analytics from '@/components/Analytics';
|
||||
import { DEFAULT_LANG } from '@/const/locale';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import PWAInstall from '@/features/PWAInstall';
|
||||
import AuthProvider from '@/layout/AuthProvider';
|
||||
import GlobalProvider from '@/layout/GlobalProvider';
|
||||
@@ -77,6 +78,8 @@ export const generateViewport = async (props: DynamicLayoutProps): ResolvingView
|
||||
};
|
||||
|
||||
export const generateStaticParams = () => {
|
||||
if (isDesktop) return [{ variants: 'desktop' }];
|
||||
|
||||
const themes: ThemeAppearance[] = ['dark', 'light'];
|
||||
const mobileOptions = [true, false];
|
||||
// only static for serveral page, other go to dynamtic
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon, FluentEmoji, SideNav } from '@lobehub/ui';
|
||||
import { Cog, DatabaseIcon } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { BRANDING_NAME } from '@/const/branding';
|
||||
import PostgresViewer from '@/features/DevPanel/PostgresViewer';
|
||||
import SystemInspector from '@/features/DevPanel/SystemInspector';
|
||||
import { useStyles } from '@/features/DevPanel/features/FloatPanel';
|
||||
import { electronStylish } from '@/styles/electron';
|
||||
|
||||
const DevTools = memo(() => {
|
||||
const { styles, theme, cx } = useStyles();
|
||||
|
||||
const items = [
|
||||
{
|
||||
children: <PostgresViewer />,
|
||||
icon: <DatabaseIcon size={16} />,
|
||||
key: 'Postgres Viewer',
|
||||
},
|
||||
{
|
||||
children: <SystemInspector />,
|
||||
icon: <Cog size={16} />,
|
||||
key: 'System Status',
|
||||
},
|
||||
];
|
||||
|
||||
const [tab, setTab] = useState<string>(items[0].key);
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
height={'100%'}
|
||||
horizontal
|
||||
style={{ overflow: 'hidden', position: 'relative' }}
|
||||
width={'100%'}
|
||||
>
|
||||
<SideNav
|
||||
avatar={<FluentEmoji emoji={'🧰'} size={24} />}
|
||||
bottomActions={[]}
|
||||
style={{
|
||||
paddingBlock: 32,
|
||||
width: 48,
|
||||
}}
|
||||
topActions={items.map((item) => (
|
||||
<ActionIcon
|
||||
active={tab === item.key}
|
||||
key={item.key}
|
||||
onClick={() => setTab(item.key)}
|
||||
placement={'right'}
|
||||
title={item.key}
|
||||
>
|
||||
{item.icon}
|
||||
</ActionIcon>
|
||||
))}
|
||||
/>
|
||||
<Flexbox height={'100%'} style={{ overflow: 'hidden', position: 'relative' }} width={'100%'}>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(`panel-drag-handle`, styles.header, electronStylish.draggable)}
|
||||
horizontal
|
||||
justify={'center'}
|
||||
>
|
||||
<Flexbox align={'baseline'} gap={6} horizontal>
|
||||
<b>{BRANDING_NAME} Dev Tools</b>
|
||||
<span style={{ color: theme.colorTextDescription }}>/</span>
|
||||
<span style={{ color: theme.colorTextDescription }}>{tab}</span>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
{items.map((item) => (
|
||||
<Flexbox
|
||||
flex={1}
|
||||
height={'100%'}
|
||||
key={item.key}
|
||||
style={{
|
||||
display: tab === item.key ? 'flex' : 'none',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{item.children}
|
||||
</Flexbox>
|
||||
))}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default DevTools;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { NuqsAdapter } from 'nuqs/adapters/next/app';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import GlobalLayout from '@/layout/GlobalProvider';
|
||||
import { ServerConfigStoreProvider } from '@/store/serverConfig/Provider';
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const RootLayout = async ({ children }: RootLayoutProps) => {
|
||||
if (!isDesktop) return notFound();
|
||||
|
||||
return (
|
||||
<html dir="ltr" suppressHydrationWarning>
|
||||
<body>
|
||||
<NuqsAdapter>
|
||||
<ServerConfigStoreProvider>
|
||||
<GlobalLayout appearance={'auto'} isMobile={false} locale={''}>
|
||||
{children}
|
||||
</GlobalLayout>
|
||||
</ServerConfigStoreProvider>
|
||||
</NuqsAdapter>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
const Layout = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from '@/components/404';
|
||||
@@ -1,14 +1,14 @@
|
||||
import { LobeChat, LobeChatProps } from '@lobehub/ui/brand';
|
||||
import { LobeHub, LobeHubProps } from '@lobehub/ui/brand';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { isCustomBranding } from '@/const/version';
|
||||
|
||||
import CustomLogo from './Custom';
|
||||
|
||||
export const ProductLogo = memo<LobeChatProps>((props) => {
|
||||
export const ProductLogo = memo<LobeHubProps>((props) => {
|
||||
if (isCustomBranding) {
|
||||
return <CustomLogo {...props} />;
|
||||
}
|
||||
|
||||
return <LobeChat {...props} />;
|
||||
return <LobeHub {...props} />;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrandLoading, LobeChatText } from '@lobehub/ui/brand';
|
||||
import { BrandLoading, LobeHubText } from '@lobehub/ui/brand';
|
||||
import { Center } from 'react-layout-kit';
|
||||
|
||||
import { isCustomBranding } from '@/const/version';
|
||||
@@ -10,7 +10,7 @@ export default () => {
|
||||
|
||||
return (
|
||||
<Center height={'100%'} width={'100%'}>
|
||||
<BrandLoading size={40} style={{ opacity: 0.6 }} text={LobeChatText} />
|
||||
<BrandLoading size={40} style={{ opacity: 0.6 }} text={LobeHubText} />
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const DESKTOP_USER_ID = 'DEFAULT_DESKTOP_USER';
|
||||
@@ -7,6 +7,8 @@ export const CURRENT_VERSION = pkg.version;
|
||||
export const isServerMode = process.env.NEXT_PUBLIC_SERVICE_MODE === 'server';
|
||||
export const isUsePgliteDB = process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite';
|
||||
|
||||
export const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
|
||||
|
||||
export const isDeprecatedEdition = !isServerMode && !isUsePgliteDB;
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
@@ -167,17 +167,10 @@ export class DatabaseManager {
|
||||
// if hash is the same, no need to migrate
|
||||
if (hash === cacheHash) {
|
||||
try {
|
||||
// 检查数据库中是否存在表
|
||||
// 这里使用 pg_tables 系统表查询用户表数量
|
||||
const tablesResult = await this.db.execute(
|
||||
sql`
|
||||
SELECT COUNT(*) as table_count
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
`,
|
||||
);
|
||||
const drizzleMigration = new DrizzleMigrationModel(this.db as any);
|
||||
|
||||
const tableCount = parseInt((tablesResult.rows[0] as any).table_count || '0', 10);
|
||||
// 检查数据库中是否存在表
|
||||
const tableCount = await drizzleMigration.getTableCounts();
|
||||
|
||||
// 如果表数量大于0,则认为数据库已正确初始化
|
||||
if (tableCount > 0) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { getDBInstance } from '@/database/core/web-server';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { getPgliteInstance } from './electron';
|
||||
|
||||
/**
|
||||
* 懒加载数据库实例
|
||||
* 避免每次模块导入时都初始化数据库
|
||||
*/
|
||||
let cachedDB: LobeChatDatabase | null = null;
|
||||
|
||||
export const getServerDB = async (): Promise<LobeChatDatabase> => {
|
||||
// 如果已经有缓存的实例,直接返回
|
||||
if (cachedDB) return cachedDB;
|
||||
|
||||
try {
|
||||
// 根据环境选择合适的数据库实例
|
||||
cachedDB = isDesktop ? await getPgliteInstance() : getDBInstance();
|
||||
return cachedDB;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize database:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const serverDB = getDBInstance();
|
||||
@@ -9,9 +9,9 @@ import ws from 'ws';
|
||||
|
||||
import { serverDBEnv } from '@/config/db';
|
||||
|
||||
import * as schema from '../../schemas';
|
||||
import * as schema from '../schemas';
|
||||
|
||||
const migrationsFolder = join(__dirname, '../../migrations');
|
||||
const migrationsFolder = join(__dirname, '../migrations');
|
||||
|
||||
export const getTestDBInstance = async () => {
|
||||
let connectionString = serverDBEnv.DATABASE_TEST_URL;
|
||||
@@ -0,0 +1,312 @@
|
||||
import { PGlite } from '@electric-sql/pglite';
|
||||
import { vector } from '@electric-sql/pglite/vector';
|
||||
import { drizzle as pgliteDrizzle } from 'drizzle-orm/pglite';
|
||||
import fs from 'node:fs';
|
||||
import { Md5 } from 'ts-md5';
|
||||
|
||||
import { DrizzleMigrationModel } from '@/database/models/drizzleMigration';
|
||||
import * as schema from '@/database/schemas';
|
||||
import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
|
||||
import { MigrationTableItem } from '@/types/clientDB';
|
||||
|
||||
import migrations from '../client/migrations.json';
|
||||
import { LobeChatDatabase } from '../type';
|
||||
|
||||
// 用于实例管理的全局对象
|
||||
interface LobeGlobal {
|
||||
pgDB?: LobeChatDatabase;
|
||||
pgDBInitPromise?: Promise<LobeChatDatabase>;
|
||||
pgDBLock?: {
|
||||
acquired: boolean;
|
||||
lockPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 确保 globalThis 有我们的命名空间
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __LOBE__: LobeGlobal;
|
||||
}
|
||||
|
||||
if (!globalThis.__LOBE__) {
|
||||
globalThis.__LOBE__ = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试创建一个文件锁来确保单例模式
|
||||
* 返回 true 表示成功获取锁,false 表示已有其他实例正在运行
|
||||
*/
|
||||
const acquireLock = async (dbPath: string): Promise<boolean> => {
|
||||
try {
|
||||
// 数据库锁文件路径
|
||||
const lockPath = `${dbPath}.lock`;
|
||||
|
||||
// 尝试创建锁文件
|
||||
if (!fs.existsSync(lockPath)) {
|
||||
// 创建锁文件并写入当前进程 ID
|
||||
fs.writeFileSync(lockPath, process.pid.toString(), 'utf8');
|
||||
|
||||
// 保存锁信息到全局对象
|
||||
if (!globalThis.__LOBE__.pgDBLock) {
|
||||
globalThis.__LOBE__.pgDBLock = {
|
||||
acquired: true,
|
||||
lockPath,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`✅ Successfully acquired database lock: ${lockPath}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查锁文件是否过期(超过5分钟未更新)
|
||||
const stats = fs.statSync(lockPath);
|
||||
const currentTime = Date.now();
|
||||
const modifiedTime = stats.mtime.getTime();
|
||||
|
||||
// 如果锁文件超过5分钟未更新,视为过期锁
|
||||
if (currentTime - modifiedTime > 5 * 60 * 1000) {
|
||||
// 删除过期锁文件
|
||||
fs.unlinkSync(lockPath);
|
||||
// 重新创建锁文件
|
||||
fs.writeFileSync(lockPath, process.pid.toString(), 'utf8');
|
||||
|
||||
// 保存锁信息到全局对象
|
||||
if (!globalThis.__LOBE__.pgDBLock) {
|
||||
globalThis.__LOBE__.pgDBLock = {
|
||||
acquired: true,
|
||||
lockPath,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`✅ Removed stale lock and acquired new lock: ${lockPath}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.warn(`⚠️ Another process has already locked the database: ${lockPath}`);
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to acquire database lock:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 释放文件锁
|
||||
*/
|
||||
const releaseLock = () => {
|
||||
if (globalThis.__LOBE__.pgDBLock?.acquired && globalThis.__LOBE__.pgDBLock.lockPath) {
|
||||
try {
|
||||
fs.unlinkSync(globalThis.__LOBE__.pgDBLock.lockPath);
|
||||
globalThis.__LOBE__.pgDBLock.acquired = false;
|
||||
console.log(`✅ Released database lock: ${globalThis.__LOBE__.pgDBLock.lockPath}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to release database lock:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 在进程退出时释放锁
|
||||
process.on('exit', releaseLock);
|
||||
process.on('SIGINT', () => {
|
||||
releaseLock();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
releaseLock();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const migrateDatabase = async (db: LobeChatDatabase): Promise<void> => {
|
||||
try {
|
||||
let hash: string | undefined;
|
||||
const cacheHash = await electronIpcClient.getDatabaseSchemaHash();
|
||||
|
||||
hash = Md5.hashStr(JSON.stringify(migrations));
|
||||
|
||||
console.log('schemaHash:', hash);
|
||||
|
||||
// 如果哈希值相同,看下表是否全了
|
||||
if (hash === cacheHash) {
|
||||
try {
|
||||
const drizzleMigration = new DrizzleMigrationModel(db);
|
||||
|
||||
// 检查数据库中是否存在表
|
||||
const tableCount = await drizzleMigration.getTableCounts();
|
||||
|
||||
// 如果表数量大于0,则认为数据库已正确初始化
|
||||
if (tableCount > 0) {
|
||||
console.log('✅ Electron DB schema already synced');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error checking table existence, proceeding with migration:');
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
console.log('🚀 Starting Electron DB migration...');
|
||||
|
||||
try {
|
||||
// 执行迁移
|
||||
// @ts-expect-error
|
||||
await db.dialect.migrate(migrations, db.session, {});
|
||||
|
||||
await electronIpcClient.setDatabaseSchemaHash(hash);
|
||||
|
||||
console.info(`✅ Electron DB migration success, took ${Date.now() - start}ms`);
|
||||
} catch (error) {
|
||||
console.error('❌ Electron database schema migration failed', error);
|
||||
|
||||
// 尝试查询迁移表数据
|
||||
let migrationsTableData: MigrationTableItem[] = [];
|
||||
try {
|
||||
// 尝试查询迁移表
|
||||
const drizzleMigration = new DrizzleMigrationModel(db);
|
||||
migrationsTableData = await drizzleMigration.getMigrationList();
|
||||
} catch (queryError) {
|
||||
console.error('Failed to query migrations table:', queryError);
|
||||
}
|
||||
|
||||
throw {
|
||||
error: error as Error,
|
||||
migrationTableItems: migrationsTableData,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Electron database migration failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查当前是否有活跃的数据库实例,如果有则尝试关闭它
|
||||
*/
|
||||
const checkAndCleanupExistingInstance = async () => {
|
||||
if (globalThis.__LOBE__.pgDB) {
|
||||
try {
|
||||
// 尝试关闭现有的 PGlite 实例 (如果客户端有 close 方法)
|
||||
// @ts-expect-error
|
||||
const client = globalThis.__LOBE__.pgDB?.dialect?.client;
|
||||
|
||||
if (client && typeof client.close === 'function') {
|
||||
await client.close();
|
||||
console.log('✅ Successfully closed previous PGlite instance');
|
||||
}
|
||||
|
||||
// 重置全局引用
|
||||
globalThis.__LOBE__.pgDB = undefined;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to close previous PGlite instance:', error);
|
||||
// 继续执行,创建新实例
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let isInitializing = false;
|
||||
|
||||
export const getPgliteInstance = async (): Promise<LobeChatDatabase> => {
|
||||
try {
|
||||
console.log(
|
||||
'Getting PGlite instance, state:',
|
||||
JSON.stringify({
|
||||
hasExistingDB: !!globalThis.__LOBE__.pgDB,
|
||||
hasPromise: !!globalThis.__LOBE__.pgDBInitPromise,
|
||||
isInitializing,
|
||||
}),
|
||||
);
|
||||
|
||||
// 已经初始化完成,直接返回实例
|
||||
if (globalThis.__LOBE__.pgDB) return globalThis.__LOBE__.pgDB;
|
||||
|
||||
// 有初始化进行中的Promise,等待它完成
|
||||
if (globalThis.__LOBE__.pgDBInitPromise) {
|
||||
console.log('Waiting for existing initialization promise to complete');
|
||||
return globalThis.__LOBE__.pgDBInitPromise;
|
||||
}
|
||||
|
||||
// 防止多次调用引起的竞态条件
|
||||
if (isInitializing) {
|
||||
console.log('Already initializing, waiting for result');
|
||||
// 创建新的 Promise 等待初始化完成
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
if (globalThis.__LOBE__.pgDB) {
|
||||
clearInterval(checkInterval);
|
||||
resolve(globalThis.__LOBE__.pgDB);
|
||||
} else if (!isInitializing) {
|
||||
clearInterval(checkInterval);
|
||||
reject(new Error('Initialization failed or was canceled'));
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
isInitializing = true;
|
||||
|
||||
// 创建初始化Promise并保存
|
||||
globalThis.__LOBE__.pgDBInitPromise = (async () => {
|
||||
// 再次检查,以防在等待过程中已有其他调用初始化成功
|
||||
if (globalThis.__LOBE__.pgDB) return globalThis.__LOBE__.pgDB;
|
||||
|
||||
// 先获取数据库路径
|
||||
let dbPath: string = '';
|
||||
try {
|
||||
dbPath = await electronIpcClient.getDatabasePath();
|
||||
} catch {}
|
||||
|
||||
console.log('Database path:', dbPath);
|
||||
try {
|
||||
// 尝试获取数据库锁
|
||||
const lockAcquired = await acquireLock(dbPath);
|
||||
if (!lockAcquired) {
|
||||
throw new Error('Cannot acquire database lock. Another instance might be using it.');
|
||||
}
|
||||
|
||||
// 检查并清理可能存在的旧实例
|
||||
await checkAndCleanupExistingInstance();
|
||||
|
||||
// 创建新的 PGlite 实例
|
||||
console.log('Creating new PGlite instance');
|
||||
const client = new PGlite(dbPath, {
|
||||
extensions: { vector },
|
||||
// 增加选项以提高稳定性
|
||||
relaxedDurability: true,
|
||||
});
|
||||
|
||||
// 等待数据库就绪
|
||||
await client.waitReady;
|
||||
console.log('PGlite state:', client.ready);
|
||||
|
||||
// 创建 Drizzle 数据库实例
|
||||
const db = pgliteDrizzle({ client, schema }) as unknown as LobeChatDatabase;
|
||||
|
||||
// 执行迁移
|
||||
await migrateDatabase(db);
|
||||
|
||||
// 保存实例引用
|
||||
globalThis.__LOBE__.pgDB = db;
|
||||
|
||||
console.log('✅ PGlite instance successfully initialized');
|
||||
|
||||
return db;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize PGlite instance:', error);
|
||||
// 清空初始化Promise,允许下次重试
|
||||
globalThis.__LOBE__.pgDBInitPromise = undefined;
|
||||
// 释放可能已获取的锁
|
||||
releaseLock();
|
||||
throw error;
|
||||
} finally {
|
||||
isInitializing = false;
|
||||
}
|
||||
})();
|
||||
|
||||
return globalThis.__LOBE__.pgDBInitPromise;
|
||||
} catch (error) {
|
||||
console.error('❌ Unexpected error in getPgliteInstance:', error);
|
||||
isInitializing = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { getServerDB } from './db-adaptor';
|
||||
|
||||
/**
|
||||
* 初始化数据库
|
||||
* 在应用启动时调用此函数,确保数据库在首次请求到达前已初始化
|
||||
*/
|
||||
export const initializeDatabase = async (): Promise<void> => {
|
||||
try {
|
||||
console.log('🚀 Initializing database during application startup...');
|
||||
await getServerDB();
|
||||
console.log('✅ Database initialized successfully during startup');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize database during startup:', error);
|
||||
// 不抛出错误,允许应用继续启动
|
||||
// 后续请求会再次尝试初始化数据库
|
||||
}
|
||||
};
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Pool as NeonPool, neonConfig } from '@neondatabase/serverless';
|
||||
import { NeonDatabase, drizzle as neonDrizzle } from 'drizzle-orm/neon-serverless';
|
||||
import { drizzle as neonDrizzle } from 'drizzle-orm/neon-serverless';
|
||||
import { drizzle as nodeDrizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Pool as NodePool } from 'pg';
|
||||
import ws from 'ws';
|
||||
|
||||
import { serverDBEnv } from '@/config/db';
|
||||
import { isServerMode } from '@/const/version';
|
||||
import * as schema from '@/database/schemas';
|
||||
|
||||
import * as schema from '../../schemas';
|
||||
import { LobeChatDatabase } from '../type';
|
||||
|
||||
export const getDBInstance = (): NeonDatabase<typeof schema> => {
|
||||
export const getDBInstance = (): LobeChatDatabase => {
|
||||
if (!isServerMode) return {} as any;
|
||||
|
||||
if (!serverDBEnv.KEY_VAULTS_SECRET) {
|
||||
@@ -40,5 +41,3 @@ If you don't have it, please run \`openssl rand -base64 32\` to create one.
|
||||
const client = new NeonPool({ connectionString });
|
||||
return neonDrizzle(client, { schema });
|
||||
};
|
||||
|
||||
export const serverDB = getDBInstance();
|
||||
@@ -2,12 +2,13 @@
|
||||
import { eq } from 'drizzle-orm/expressions';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { sessionGroups, users } from '../../schemas';
|
||||
import { SessionGroupModel } from '../../server/models/sessionGroup';
|
||||
import { SessionGroupModel } from '../sessionGroup';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
let serverDB = await getTestDBInstance();
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
const userId = 'session-group-model-test-user-id';
|
||||
const sessionGroupModel = new SessionGroupModel(serverDB, userId);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { clientDB, initializeDB } from '@/database/client/db';
|
||||
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
||||
import { getTestDBInstance } from '@/database/core/dbForTest';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
export const isServerDBMode = process.env.TEST_SERVER_DB === '1';
|
||||
const isServerDBMode = process.env.TEST_SERVER_DB === '1';
|
||||
|
||||
export const getTestDB = async () => {
|
||||
if (isServerDBMode) return await getTestDBInstance();
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
sessions,
|
||||
users,
|
||||
} from '../../schemas';
|
||||
import { AgentModel } from '../../server/models/agent';
|
||||
import { AgentModel } from '../agent';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LobeChatDatabase } from '@/database/type';
|
||||
import { AiProviderModelListItem } from '@/types/aiModel';
|
||||
|
||||
import { AiModelSelectItem, NewAiModelItem, aiModels, users } from '../../schemas';
|
||||
import { AiModelModel } from '../../server/models/aiModel';
|
||||
import { AiModelModel } from '../aiModel';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -4,9 +4,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
import { ModelProvider } from '@/libs/agent-runtime';
|
||||
import { sleep } from '@/utils/sleep';
|
||||
|
||||
import { aiProviders, users } from '../../schemas';
|
||||
import { AiProviderModel } from '../../server/models/aiProvider';
|
||||
import { AiProviderModel } from '../aiProvider';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
@@ -96,6 +97,7 @@ describe('AiProviderModel', () => {
|
||||
describe('query', () => {
|
||||
it('should query ai providers for the user', async () => {
|
||||
await aiProviderModel.create({ name: 'AiHubMix', source: 'custom', id: 'aihubmix' });
|
||||
await sleep(10);
|
||||
await aiProviderModel.create({ name: 'AiHubMix', source: 'custom', id: 'aihubmix-2' });
|
||||
|
||||
const userGroups = await aiProviderModel.query();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LobeChatDatabase } from '@/database/type';
|
||||
import { AsyncTaskStatus, AsyncTaskType } from '@/types/asyncTask';
|
||||
|
||||
import { asyncTasks, users } from '../../schemas';
|
||||
import { ASYNC_TASK_TIMEOUT, AsyncTaskModel } from '../../server/models/asyncTask';
|
||||
import { ASYNC_TASK_TIMEOUT, AsyncTaskModel } from '../asyncTask';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LobeChatDatabase } from '@/database/type';
|
||||
import { uuid } from '@/utils/uuid';
|
||||
|
||||
import { chunks, embeddings, fileChunks, files, unstructuredChunks, users } from '../../schemas';
|
||||
import { ChunkModel } from '../../server/models/chunk';
|
||||
import { ChunkModel } from '../chunk';
|
||||
import { getTestDB } from './_util';
|
||||
import { codeEmbedding, designThinkingQuery, designThinkingQuery2 } from './fixtures/embedding';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LobeChatDatabase } from '@/database/type';
|
||||
import { FilesTabs, SortType } from '@/types/files';
|
||||
|
||||
import { files, globalFiles, knowledgeBaseFiles, knowledgeBases, users } from '../../schemas';
|
||||
import { FileModel } from '../../server/models/file';
|
||||
import { FileModel } from '../file';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
knowledgeBases,
|
||||
users,
|
||||
} from '../../schemas';
|
||||
import { KnowledgeBaseModel } from '../../server/models/knowledgeBase';
|
||||
import { KnowledgeBaseModel } from '../knowledgeBase';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
topics,
|
||||
users,
|
||||
} from '../../schemas';
|
||||
import { MessageModel } from '../../server/models/message';
|
||||
import { MessageModel } from '../message';
|
||||
import { codeEmbedding } from './fixtures/embedding';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { NewInstalledPlugin, userInstalledPlugins, users } from '../../schemas';
|
||||
import { PluginModel } from '../../server/models/plugin';
|
||||
import { PluginModel } from '../plugin';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -2,7 +2,6 @@ import { and, eq, inArray } from 'drizzle-orm/expressions';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
||||
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
import { idGenerator } from '@/database/utils/idGenerator';
|
||||
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
topics,
|
||||
users,
|
||||
} from '../../schemas';
|
||||
import { SessionModel } from '../../server/models/session';
|
||||
import { SessionModel } from '../session';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { sessionGroups, users } from '../../schemas';
|
||||
import { SessionGroupModel } from '../../server/models/sessionGroup';
|
||||
import { SessionGroupModel } from '../sessionGroup';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { messages, sessions, topics, users } from '../../schemas';
|
||||
import { CreateTopicParams, TopicModel } from '../../server/models/topic';
|
||||
import { CreateTopicParams, TopicModel } from '../topic';
|
||||
import { getTestDB } from './_util';
|
||||
|
||||
const serverDB: LobeChatDatabase = await getTestDB();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { and, desc, eq } from 'drizzle-orm/expressions';
|
||||
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { NewSessionGroup, SessionGroupItem, sessionGroups } from '../../schemas';
|
||||
import { NewSessionGroup, SessionGroupItem, sessionGroups } from '../schemas';
|
||||
|
||||
export class TemplateModel {
|
||||
private userId: string;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user