Compare commits
19 Commits
feat/ssot
...
feat/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed9cf47df | ||
|
|
a3582f54e9 | ||
|
|
9ff7516c51 | ||
|
|
a1a16be2aa | ||
|
|
df7d818514 | ||
|
|
c0d9d0296d | ||
|
|
77a65aaad8 | ||
|
|
3ce847d2e0 | ||
|
|
1482dc9e66 | ||
|
|
fa2b11fcc2 | ||
|
|
02bfc97ee6 | ||
|
|
77bdeb02fb | ||
|
|
48bd37a74b | ||
|
|
7346fcde2c | ||
|
|
5af476d376 | ||
|
|
07b870488d | ||
|
|
74ab14f572 | ||
|
|
a14f7ef9b2 | ||
|
|
07dd70570e |
38
.gitattributes
vendored
Normal file
38
.gitattributes
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Explicitly declare text files you want to always be normalized and converted
|
||||||
|
# to native line endings on checkout.
|
||||||
|
*.rs text eol=lf
|
||||||
|
*.toml text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.txt text eol=lf
|
||||||
|
|
||||||
|
# TypeScript/JavaScript files
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
|
||||||
|
# HTML/CSS files
|
||||||
|
*.html text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.scss text eol=lf
|
||||||
|
|
||||||
|
# Shell scripts
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Denote all files that are truly binary and should not be modified.
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.exe binary
|
||||||
|
*.dll binary
|
||||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -85,14 +85,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Tauri App (macOS)
|
- name: Build Tauri App (macOS)
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
|
env:
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
run: pnpm tauri build --target universal-apple-darwin
|
run: pnpm tauri build --target universal-apple-darwin
|
||||||
|
|
||||||
- name: Build Tauri App (Windows)
|
- name: Build Tauri App (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
|
env:
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
run: pnpm tauri build
|
run: pnpm tauri build
|
||||||
|
|
||||||
- name: Build Tauri App (Linux)
|
- name: Build Tauri App (Linux)
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
|
env:
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
run: pnpm tauri build
|
run: pnpm tauri build
|
||||||
|
|
||||||
- name: Prepare macOS Assets
|
- name: Prepare macOS Assets
|
||||||
@@ -180,6 +186,16 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
ls -la release-assets || true
|
ls -la release-assets || true
|
||||||
|
|
||||||
|
- name: Collect Signatures
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# 查找并复制签名文件到 release-assets
|
||||||
|
find src-tauri/target -name "*.sig" -type f 2>/dev/null | while read sig; do
|
||||||
|
cp "$sig" release-assets/ || true
|
||||||
|
done
|
||||||
|
echo "Collected signatures:"
|
||||||
|
ls -la release-assets/*.sig || echo "No signatures found"
|
||||||
|
|
||||||
- name: Upload Release Assets
|
- name: Upload Release Assets
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
91
docs/updater-plan.md
Normal file
91
docs/updater-plan.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# 更新功能开发计划(Tauri v2 Updater)
|
||||||
|
|
||||||
|
> 目标:基于 Tauri v2 官方 Updater,完成“检查更新 → 下载 → 安装 → 重启”的完整闭环;提供清晰的前后端接口、配置与测试/发布流程。
|
||||||
|
|
||||||
|
## 范围与目标
|
||||||
|
- 能力:静态 JSON 与动态接口两种更新源;可选稳定/测试通道;进度反馈与错误处理。
|
||||||
|
- 平台:macOS `.app` 优先;Windows 使用安装器(NSIS/MSI)。
|
||||||
|
- 安全:启用 Ed25519 更新签名校验;上线前建议平台代码签名与公证。
|
||||||
|
|
||||||
|
## 架构与依赖
|
||||||
|
- 插件:`tauri-plugin-updater`(更新)、`@tauri-apps/plugin-updater`(JS);`tauri-plugin-process` 与 `@tauri-apps/plugin-process`(重启)。
|
||||||
|
- 签名与构建:`tauri signer generate` 生成密钥;CI/本机注入 `TAURI_SIGNING_PRIVATE_KEY`;`bundle.createUpdaterArtifacts: true` 生成签名制品。
|
||||||
|
- 权限:在 `src-tauri/capabilities/default.json` 启用 `updater:default` 与 `process:allow-restart`。
|
||||||
|
- 配置(`src-tauri/tauri.conf.json`):
|
||||||
|
- `plugins.updater.pubkey: "<PUBLICKEY.PEM>"`
|
||||||
|
- `plugins.updater.endpoints: ["<更新源 URL 列表>"]`
|
||||||
|
- Windows(可选):`plugins.updater.windows.installMode: "passive|basicUi|quiet"`
|
||||||
|
|
||||||
|
## 前端接口设计(TypeScript)
|
||||||
|
- 类型
|
||||||
|
- `type UpdateChannel = 'stable' | 'beta'`
|
||||||
|
- `type UpdaterPhase = 'idle' | 'checking' | 'available' | 'downloading' | 'installing' | 'restarting' | 'upToDate' | 'error'`
|
||||||
|
- `type UpdateInfo = { currentVersion: string; availableVersion: string; notes?: string; pubDate?: string }`
|
||||||
|
- `type UpdateProgressEvent = { event: 'Started' | 'Progress' | 'Finished'; total?: number; downloaded?: number }`
|
||||||
|
- `type UpdateError = { code: string; message: string; cause?: unknown }`
|
||||||
|
- `type CheckOptions = { timeout?: number; channel?: UpdateChannel }`
|
||||||
|
- API(`src/lib/updater.ts`)
|
||||||
|
- `getCurrentVersion(): Promise<string>` 读取当前版本。
|
||||||
|
- `checkForUpdate(opts?: CheckOptions)` → `up-to-date` 或 `{ status: 'available', info, update }`。
|
||||||
|
- `downloadAndInstall(update, onProgress?)` 下载并安装,进度回调映射 Started/Progress/Finished。
|
||||||
|
- `relaunchApp()` 调用 `@tauri-apps/plugin-process.relaunch()`。
|
||||||
|
- `runUpdateFlow(opts?)` 编排:检查 → 下载安装 → 重启;错误统一抛出 `UpdateError`。
|
||||||
|
- `setUpdateChannel(channel)` 前端记录偏好;实际端点切换见“端点动态化”。
|
||||||
|
- Hook(可选 `useUpdater()`)
|
||||||
|
- 返回 `{ phase, info?, progress?, error?, actions: { check, startUpdate, relaunch } }`。
|
||||||
|
- UI(组件建议)
|
||||||
|
- `UpdateBanner`:发现新版本时展示;`UpdaterDialog`:显示说明、进度与错误/重试。
|
||||||
|
|
||||||
|
## Rust 集成与权限
|
||||||
|
- 插件注册(`src-tauri/src/main.rs`):
|
||||||
|
- `app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;`
|
||||||
|
- `.plugin(tauri_plugin_process::init())` 用于重启。
|
||||||
|
- Windows 清理钩子(可选):`UpdaterExt::on_before_exit(app.cleanup_before_exit)`,避免安装器启动前文件占用。
|
||||||
|
- 端点动态化(可选):在 `setup` 根据配置/环境切换 `endpoints`、超时、代理或 headers。
|
||||||
|
|
||||||
|
## 更新源与格式
|
||||||
|
- 静态 JSON(latest.json):字段 `version`、`platforms[target].url`、`platforms[target].signature`(`.sig` 内容);可选 `notes`、`pub_date`。
|
||||||
|
- 动态接口:
|
||||||
|
- 无更新:HTTP 204
|
||||||
|
- 有更新:HTTP 200 → `{ version, url, signature, notes?, pub_date? }`
|
||||||
|
- 通道组织:`/stable/latest.json`、`/beta/latest.json`;CDN 缓存需可控,回滚可强制刷新。
|
||||||
|
|
||||||
|
## 用户流程与 UX
|
||||||
|
- 流程:检查 → 展示版本/日志 → 下载进度(累计/百分比)→ 安装 → 提示并重启。
|
||||||
|
- 错误:网络异常(超时/断网/证书)、签名不匹配、权限/文件占用(Win)。提供“重试/稍后更新”。
|
||||||
|
- 平台提示:
|
||||||
|
- macOS:建议安装在 `~/Applications`,避免 `/Applications` 提权导致失败。
|
||||||
|
- Windows:优先安装器分发,并选择合适 `installMode`。
|
||||||
|
|
||||||
|
## 测试计划
|
||||||
|
- 功能:有更新/无更新(204)/下载中断/重试/安装后重启成功与版本号提升。
|
||||||
|
- 安全:签名不匹配必须拒绝更新;端点不可用/被劫持有清晰提示。
|
||||||
|
- 网络:超时/断网/代理场景提示与恢复。
|
||||||
|
- 平台:
|
||||||
|
- macOS:`/Applications` 与 `~/Applications` 的权限差异。
|
||||||
|
- Windows:`passive|basicUi|quiet` 行为差异与成功率。
|
||||||
|
- 本地自测:以 v1.0.0 运行,构建 v1.0.1 制品+`.sig`,本地 HTTP 托管 `latest.json`,验证全链路。
|
||||||
|
|
||||||
|
## 发布与回滚
|
||||||
|
- 发布(CI 推荐):注入 `TAURI_SIGNING_PRIVATE_KEY` → 构建生成各平台制品+签名 → 上传产物与 `latest.json` 至 Releases/CDN。
|
||||||
|
- 回滚:撤下问题版本或将 `latest.json` 指回上一个稳定版本;如需降级,Rust 侧可定制版本比较策略(可选)。
|
||||||
|
|
||||||
|
## 里程碑与验收
|
||||||
|
- D1:密钥与基础集成(插件/配置/权限)。
|
||||||
|
- D2:前端入口与进度 UI,静态 JSON 自测通过。
|
||||||
|
- D3:Releases/CDN 端到端验证,平台专项测试。
|
||||||
|
- D4:文档完善、回滚与异常流程演练。
|
||||||
|
- 验收:两平台完成“发现→下载→安装→重启→版本提升”;签名校验生效;异常有明确提示与可行恢复。
|
||||||
|
|
||||||
|
## 待确认
|
||||||
|
- 更新源托管(GitHub Releases 还是自有 CDN)。
|
||||||
|
- 是否需要 beta 通道与运行时切换。
|
||||||
|
- Windows 是否仅支持安装器分发;便携版兼容策略是否需要明确说明。
|
||||||
|
- UI 文案与样式偏好。
|
||||||
|
|
||||||
|
## 落地步骤(实施顺序)
|
||||||
|
1) 生成 Ed25519 密钥,将公钥写入 `plugins.updater.pubkey`,在构建环境配置 `TAURI_SIGNING_PRIVATE_KEY`。
|
||||||
|
2) `src-tauri` 注册 `tauri-plugin-updater` 与 `tauri-plugin-process`,补齐 `capabilities/default.json` 与 `tauri.conf.json`。
|
||||||
|
3) 前端新增 `src/lib/updater.ts` 封装与 `UpdateBanner`/`UpdaterDialog` 组件,接入入口按钮。
|
||||||
|
4) 本地静态 `latest.json` 自测全链路;完善错误与进度提示。
|
||||||
|
5) 配置 CI 发布产物与 `latest.json`;编写发布/回滚操作手册。
|
||||||
12
package.json
12
package.json
@@ -26,8 +26,18 @@
|
|||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@codemirror/view": "^6.38.2",
|
||||||
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@tauri-apps/api": "^2.8.0",
|
"@tauri-apps/api": "^2.8.0",
|
||||||
|
"@tauri-apps/plugin-process": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"lucide-react": "^0.542.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"tailwindcss": "^4.1.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
591
pnpm-lock.yaml
generated
591
pnpm-lock.yaml
generated
@@ -8,15 +8,45 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@codemirror/lang-json':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
'@codemirror/state':
|
||||||
|
specifier: ^6.5.2
|
||||||
|
version: 6.5.2
|
||||||
|
'@codemirror/theme-one-dark':
|
||||||
|
specifier: ^6.1.3
|
||||||
|
version: 6.1.3
|
||||||
|
'@codemirror/view':
|
||||||
|
specifier: ^6.38.2
|
||||||
|
version: 6.38.2
|
||||||
|
'@tailwindcss/vite':
|
||||||
|
specifier: ^4.1.13
|
||||||
|
version: 4.1.13(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2.8.0
|
specifier: ^2.8.0
|
||||||
version: 2.8.0
|
version: 2.8.0
|
||||||
|
'@tauri-apps/plugin-process':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.3.0
|
||||||
|
'@tauri-apps/plugin-updater':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.9.0
|
||||||
|
codemirror:
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
lucide-react:
|
||||||
|
specifier: ^0.542.0
|
||||||
|
version: 0.542.0(react@18.3.1)
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
|
tailwindcss:
|
||||||
|
specifier: ^4.1.13
|
||||||
|
version: 4.1.13
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: ^2.8.0
|
specifier: ^2.8.0
|
||||||
@@ -32,7 +62,7 @@ importers:
|
|||||||
version: 18.3.7(@types/react@18.3.23)
|
version: 18.3.7(@types/react@18.3.23)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.7.0(vite@5.4.19(@types/node@20.19.9))
|
version: 4.7.0(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.6.2
|
specifier: ^3.6.2
|
||||||
version: 3.6.2
|
version: 3.6.2
|
||||||
@@ -41,7 +71,7 @@ importers:
|
|||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.4.19(@types/node@20.19.9)
|
version: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -132,6 +162,33 @@ packages:
|
|||||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@codemirror/autocomplete@6.18.7':
|
||||||
|
resolution: {integrity: sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==}
|
||||||
|
|
||||||
|
'@codemirror/commands@6.8.1':
|
||||||
|
resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==}
|
||||||
|
|
||||||
|
'@codemirror/lang-json@6.0.2':
|
||||||
|
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
|
||||||
|
|
||||||
|
'@codemirror/language@6.11.3':
|
||||||
|
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
|
||||||
|
|
||||||
|
'@codemirror/lint@6.8.5':
|
||||||
|
resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==}
|
||||||
|
|
||||||
|
'@codemirror/search@6.5.11':
|
||||||
|
resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
|
||||||
|
|
||||||
|
'@codemirror/state@6.5.2':
|
||||||
|
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
|
||||||
|
|
||||||
|
'@codemirror/theme-one-dark@6.1.3':
|
||||||
|
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
|
||||||
|
|
||||||
|
'@codemirror/view@6.38.2':
|
||||||
|
resolution: {integrity: sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.21.5':
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -270,9 +327,16 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
|
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.12':
|
'@jridgewell/gen-mapping@0.3.12':
|
||||||
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
|
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
|
||||||
|
|
||||||
|
'@jridgewell/remapping@2.3.5':
|
||||||
|
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2':
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -280,9 +344,27 @@ packages:
|
|||||||
'@jridgewell/sourcemap-codec@1.5.4':
|
'@jridgewell/sourcemap-codec@1.5.4':
|
||||||
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
|
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.29':
|
'@jridgewell/trace-mapping@0.3.29':
|
||||||
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
||||||
|
|
||||||
|
'@lezer/common@1.2.3':
|
||||||
|
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
||||||
|
|
||||||
|
'@lezer/highlight@1.2.1':
|
||||||
|
resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
|
||||||
|
|
||||||
|
'@lezer/json@1.0.3':
|
||||||
|
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
|
||||||
|
|
||||||
|
'@lezer/lr@1.4.2':
|
||||||
|
resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
|
||||||
|
|
||||||
|
'@marijn/find-cluster-break@1.0.2':
|
||||||
|
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||||
|
|
||||||
@@ -386,6 +468,96 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@tailwindcss/node@4.1.13':
|
||||||
|
resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==}
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-android-arm64@4.1.13':
|
||||||
|
resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-arm64@4.1.13':
|
||||||
|
resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-x64@4.1.13':
|
||||||
|
resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-freebsd-x64@4.1.13':
|
||||||
|
resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13':
|
||||||
|
resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.13':
|
||||||
|
resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.13':
|
||||||
|
resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.13':
|
||||||
|
resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl@4.1.13':
|
||||||
|
resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi@4.1.13':
|
||||||
|
resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
cpu: [wasm32]
|
||||||
|
bundledDependencies:
|
||||||
|
- '@napi-rs/wasm-runtime'
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
|
- '@tybys/wasm-util'
|
||||||
|
- '@emnapi/wasi-threads'
|
||||||
|
- tslib
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc@4.1.13':
|
||||||
|
resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc@4.1.13':
|
||||||
|
resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide@4.1.13':
|
||||||
|
resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
'@tailwindcss/vite@4.1.13':
|
||||||
|
resolution: {integrity: sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vite: ^5.2.0 || ^6 || ^7
|
||||||
|
|
||||||
'@tauri-apps/api@2.8.0':
|
'@tauri-apps/api@2.8.0':
|
||||||
resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==}
|
resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==}
|
||||||
|
|
||||||
@@ -460,6 +632,12 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-process@2.3.0':
|
||||||
|
resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-updater@2.9.0':
|
||||||
|
resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==}
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
|
|
||||||
@@ -503,9 +681,19 @@ packages:
|
|||||||
caniuse-lite@1.0.30001731:
|
caniuse-lite@1.0.30001731:
|
||||||
resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==}
|
resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==}
|
||||||
|
|
||||||
|
chownr@3.0.0:
|
||||||
|
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
codemirror@6.0.2:
|
||||||
|
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
|
||||||
|
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
crelt@1.0.6:
|
||||||
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
@@ -518,9 +706,17 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
detect-libc@2.0.4:
|
||||||
|
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
electron-to-chromium@1.5.197:
|
electron-to-chromium@1.5.197:
|
||||||
resolution: {integrity: sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==}
|
resolution: {integrity: sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==}
|
||||||
|
|
||||||
|
enhanced-resolve@5.18.3:
|
||||||
|
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
esbuild@0.21.5:
|
esbuild@0.21.5:
|
||||||
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
|
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -539,6 +735,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
graceful-fs@4.2.11:
|
||||||
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
jiti@2.5.1:
|
||||||
|
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -552,6 +755,70 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
|
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.30.1:
|
||||||
|
resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.1:
|
||||||
|
resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||||
|
resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.1:
|
||||||
|
resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.1:
|
||||||
|
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.1:
|
||||||
|
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.1:
|
||||||
|
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.1:
|
||||||
|
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.1:
|
||||||
|
resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss@1.30.1:
|
||||||
|
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
loose-envify@1.4.0:
|
||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -559,6 +826,27 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
|
lucide-react@0.542.0:
|
||||||
|
resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
magic-string@0.30.18:
|
||||||
|
resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
|
||||||
|
|
||||||
|
minipass@7.1.2:
|
||||||
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
minizlib@3.0.2:
|
||||||
|
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
mkdirp@3.0.1:
|
||||||
|
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -611,6 +899,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
style-mod@4.1.2:
|
||||||
|
resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
|
||||||
|
|
||||||
|
tailwindcss@4.1.13:
|
||||||
|
resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==}
|
||||||
|
|
||||||
|
tapable@2.2.3:
|
||||||
|
resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tar@7.4.3:
|
||||||
|
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
typescript@5.9.2:
|
typescript@5.9.2:
|
||||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -656,9 +958,16 @@ packages:
|
|||||||
terser:
|
terser:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
w3c-keyname@2.2.8:
|
||||||
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
|
yallist@5.0.0:
|
||||||
|
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
@@ -778,6 +1087,64 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.27.1
|
'@babel/helper-validator-identifier': 7.27.1
|
||||||
|
|
||||||
|
'@codemirror/autocomplete@6.18.7':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.11.3
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.38.2
|
||||||
|
'@lezer/common': 1.2.3
|
||||||
|
|
||||||
|
'@codemirror/commands@6.8.1':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.11.3
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.38.2
|
||||||
|
'@lezer/common': 1.2.3
|
||||||
|
|
||||||
|
'@codemirror/lang-json@6.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.11.3
|
||||||
|
'@lezer/json': 1.0.3
|
||||||
|
|
||||||
|
'@codemirror/language@6.11.3':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.38.2
|
||||||
|
'@lezer/common': 1.2.3
|
||||||
|
'@lezer/highlight': 1.2.1
|
||||||
|
'@lezer/lr': 1.4.2
|
||||||
|
style-mod: 4.1.2
|
||||||
|
|
||||||
|
'@codemirror/lint@6.8.5':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.38.2
|
||||||
|
crelt: 1.0.6
|
||||||
|
|
||||||
|
'@codemirror/search@6.5.11':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.38.2
|
||||||
|
crelt: 1.0.6
|
||||||
|
|
||||||
|
'@codemirror/state@6.5.2':
|
||||||
|
dependencies:
|
||||||
|
'@marijn/find-cluster-break': 1.0.2
|
||||||
|
|
||||||
|
'@codemirror/theme-one-dark@6.1.3':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.11.3
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.38.2
|
||||||
|
'@lezer/highlight': 1.2.1
|
||||||
|
|
||||||
|
'@codemirror/view@6.38.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
crelt: 1.0.6
|
||||||
|
style-mod: 4.1.2
|
||||||
|
w3c-keyname: 2.2.8
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.21.5':
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -847,20 +1214,49 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.21.5':
|
'@esbuild/win32-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
minipass: 7.1.2
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.12':
|
'@jridgewell/gen-mapping@0.3.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.4
|
'@jridgewell/sourcemap-codec': 1.5.4
|
||||||
'@jridgewell/trace-mapping': 0.3.29
|
'@jridgewell/trace-mapping': 0.3.29
|
||||||
|
|
||||||
|
'@jridgewell/remapping@2.3.5':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/gen-mapping': 0.3.12
|
||||||
|
'@jridgewell/trace-mapping': 0.3.29
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2': {}
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.4': {}
|
'@jridgewell/sourcemap-codec@1.5.4': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.29':
|
'@jridgewell/trace-mapping@0.3.29':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.4
|
'@jridgewell/sourcemap-codec': 1.5.4
|
||||||
|
|
||||||
|
'@lezer/common@1.2.3': {}
|
||||||
|
|
||||||
|
'@lezer/highlight@1.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.2.3
|
||||||
|
|
||||||
|
'@lezer/json@1.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.2.3
|
||||||
|
'@lezer/highlight': 1.2.1
|
||||||
|
'@lezer/lr': 1.4.2
|
||||||
|
|
||||||
|
'@lezer/lr@1.4.2':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.2.3
|
||||||
|
|
||||||
|
'@marijn/find-cluster-break@1.0.2': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.46.2':
|
'@rollup/rollup-android-arm-eabi@4.46.2':
|
||||||
@@ -923,6 +1319,77 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.46.2':
|
'@rollup/rollup-win32-x64-msvc@4.46.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/node@4.1.13':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/remapping': 2.3.5
|
||||||
|
enhanced-resolve: 5.18.3
|
||||||
|
jiti: 2.5.1
|
||||||
|
lightningcss: 1.30.1
|
||||||
|
magic-string: 0.30.18
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
tailwindcss: 4.1.13
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-android-arm64@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-arm64@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-x64@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-freebsd-x64@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc@4.1.13':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide@4.1.13':
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.0.4
|
||||||
|
tar: 7.4.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@tailwindcss/oxide-android-arm64': 4.1.13
|
||||||
|
'@tailwindcss/oxide-darwin-arm64': 4.1.13
|
||||||
|
'@tailwindcss/oxide-darwin-x64': 4.1.13
|
||||||
|
'@tailwindcss/oxide-freebsd-x64': 4.1.13
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.13
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl': 4.1.13
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu': 4.1.13
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl': 4.1.13
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi': 4.1.13
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.13
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc': 4.1.13
|
||||||
|
|
||||||
|
'@tailwindcss/vite@4.1.13(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))':
|
||||||
|
dependencies:
|
||||||
|
'@tailwindcss/node': 4.1.13
|
||||||
|
'@tailwindcss/oxide': 4.1.13
|
||||||
|
tailwindcss: 4.1.13
|
||||||
|
vite: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)
|
||||||
|
|
||||||
'@tauri-apps/api@2.8.0': {}
|
'@tauri-apps/api@2.8.0': {}
|
||||||
|
|
||||||
'@tauri-apps/cli-darwin-arm64@2.8.1':
|
'@tauri-apps/cli-darwin-arm64@2.8.1':
|
||||||
@@ -972,6 +1439,14 @@ snapshots:
|
|||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.8.1
|
'@tauri-apps/cli-win32-ia32-msvc': 2.8.1
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 2.8.1
|
'@tauri-apps/cli-win32-x64-msvc': 2.8.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-process@2.3.0':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.8.0
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-updater@2.9.0':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.8.0
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.0
|
'@babel/parser': 7.28.0
|
||||||
@@ -1010,7 +1485,7 @@ snapshots:
|
|||||||
'@types/prop-types': 15.7.15
|
'@types/prop-types': 15.7.15
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0(vite@5.4.19(@types/node@20.19.9))':
|
'@vitejs/plugin-react@4.7.0(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.0
|
'@babel/core': 7.28.0
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
|
||||||
@@ -1018,7 +1493,7 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
vite: 5.4.19(@types/node@20.19.9)
|
vite: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -1031,16 +1506,37 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001731: {}
|
caniuse-lite@1.0.30001731: {}
|
||||||
|
|
||||||
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
|
codemirror@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.18.7
|
||||||
|
'@codemirror/commands': 6.8.1
|
||||||
|
'@codemirror/language': 6.11.3
|
||||||
|
'@codemirror/lint': 6.8.5
|
||||||
|
'@codemirror/search': 6.5.11
|
||||||
|
'@codemirror/state': 6.5.2
|
||||||
|
'@codemirror/view': 6.38.2
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
detect-libc@2.0.4: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.197: {}
|
electron-to-chromium@1.5.197: {}
|
||||||
|
|
||||||
|
enhanced-resolve@5.18.3:
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
tapable: 2.2.3
|
||||||
|
|
||||||
esbuild@0.21.5:
|
esbuild@0.21.5:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.21.5
|
'@esbuild/aix-ppc64': 0.21.5
|
||||||
@@ -1074,12 +1570,61 @@ snapshots:
|
|||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
jiti@2.5.1: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss@1.30.1:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.0.4
|
||||||
|
optionalDependencies:
|
||||||
|
lightningcss-darwin-arm64: 1.30.1
|
||||||
|
lightningcss-darwin-x64: 1.30.1
|
||||||
|
lightningcss-freebsd-x64: 1.30.1
|
||||||
|
lightningcss-linux-arm-gnueabihf: 1.30.1
|
||||||
|
lightningcss-linux-arm64-gnu: 1.30.1
|
||||||
|
lightningcss-linux-arm64-musl: 1.30.1
|
||||||
|
lightningcss-linux-x64-gnu: 1.30.1
|
||||||
|
lightningcss-linux-x64-musl: 1.30.1
|
||||||
|
lightningcss-win32-arm64-msvc: 1.30.1
|
||||||
|
lightningcss-win32-x64-msvc: 1.30.1
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
loose-envify@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
@@ -1088,6 +1633,22 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
lucide-react@0.542.0(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
|
magic-string@0.30.18:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
|
minizlib@3.0.2:
|
||||||
|
dependencies:
|
||||||
|
minipass: 7.1.2
|
||||||
|
|
||||||
|
mkdirp@3.0.1: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
@@ -1150,6 +1711,21 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
style-mod@4.1.2: {}
|
||||||
|
|
||||||
|
tailwindcss@4.1.13: {}
|
||||||
|
|
||||||
|
tapable@2.2.3: {}
|
||||||
|
|
||||||
|
tar@7.4.3:
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/fs-minipass': 4.0.1
|
||||||
|
chownr: 3.0.0
|
||||||
|
minipass: 7.1.2
|
||||||
|
minizlib: 3.0.2
|
||||||
|
mkdirp: 3.0.1
|
||||||
|
yallist: 5.0.0
|
||||||
|
|
||||||
typescript@5.9.2: {}
|
typescript@5.9.2: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
@@ -1160,7 +1736,7 @@ snapshots:
|
|||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
vite@5.4.19(@types/node@20.19.9):
|
vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@@ -1168,5 +1744,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 20.19.9
|
'@types/node': 20.19.9
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
lightningcss: 1.30.1
|
||||||
|
|
||||||
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
|
yallist@5.0.0: {}
|
||||||
|
|||||||
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@tailwindcss/oxide'
|
||||||
361
src-tauri/Cargo.lock
generated
361
src-tauri/Cargo.lock
generated
@@ -90,6 +90,15 @@ version = "1.0.99"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@@ -562,6 +571,8 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-process",
|
||||||
|
"tauri-plugin-updater",
|
||||||
"toml 0.8.2",
|
"toml 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -834,6 +845,17 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.20"
|
version = "0.99.20"
|
||||||
@@ -1123,6 +1145,18 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -1412,8 +1446,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1423,9 +1459,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasi 0.14.2+wasi-0.2.4",
|
"wasi 0.14.2+wasi-0.2.4",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1694,6 +1732,23 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
@@ -2102,6 +2157,7 @@ checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.3",
|
"bitflags 2.9.3",
|
||||||
"libc",
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2135,6 +2191,12 @@ dependencies = [
|
|||||||
"value-bag",
|
"value-bag",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-slab"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2193,6 +2255,12 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minisign-verify"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -2549,6 +2617,18 @@ dependencies = [
|
|||||||
"objc2-foundation 0.2.2",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-osa-kit"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.3",
|
||||||
|
"objc2 0.6.2",
|
||||||
|
"objc2-app-kit 0.3.1",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-quartz-core"
|
name = "objc2-quartz-core"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -2655,6 +2735,20 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "osakit"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||||
|
dependencies = [
|
||||||
|
"objc2 0.6.2",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
|
"objc2-osa-kit",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.16",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -3042,6 +3136,61 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases 0.2.1",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.16",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"lru-slab",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.16",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases 0.2.1",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@@ -3088,6 +3237,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -3108,6 +3267,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -3126,6 +3295,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_hc"
|
name = "rand_hc"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3253,16 +3431,21 @@ dependencies = [
|
|||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
@@ -3272,6 +3455,21 @@ dependencies = [
|
|||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3325,6 +3523,12 @@ version = "0.1.26"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -3347,6 +3551,41 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||||
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -3791,6 +4030,12 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -3926,6 +4171,17 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tar"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||||
|
dependencies = [
|
||||||
|
"filetime",
|
||||||
|
"libc",
|
||||||
|
"xattr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.12.16"
|
||||||
@@ -4108,6 +4364,48 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-process"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab"
|
||||||
|
dependencies = [
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-updater"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"dirs 6.0.0",
|
||||||
|
"flate2",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"infer",
|
||||||
|
"log",
|
||||||
|
"minisign-verify",
|
||||||
|
"osakit",
|
||||||
|
"percent-encoding",
|
||||||
|
"reqwest",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tar",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.16",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@@ -4347,6 +4645,16 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.16"
|
version = "0.7.16"
|
||||||
@@ -4624,6 +4932,12 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.7"
|
version = "2.5.7"
|
||||||
@@ -4850,6 +5164,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webkit2gtk"
|
name = "webkit2gtk"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -4894,6 +5218,15 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -5563,6 +5896,16 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xattr"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xdg-home"
|
name = "xdg-home"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -5702,6 +6045,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -5735,6 +6084,18 @@ dependencies = [
|
|||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"indexmap 2.11.0",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zvariant"
|
name = "zvariant"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ tauri-build = { version = "2.4.0", features = [] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tauri = { version = "2.8.2", features = [] }
|
tauri = { version = "2.8.2", features = ["tray-icon"] }
|
||||||
tauri-plugin-log = "2"
|
tauri-plugin-log = "2"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-process = "2"
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default"
|
"opener:default",
|
||||||
|
"updater:default",
|
||||||
|
"process:allow-restart"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ use std::path::PathBuf;
|
|||||||
use crate::config::{
|
use crate::config::{
|
||||||
atomic_write, delete_file, sanitize_provider_name, write_json_file, write_text_file,
|
atomic_write, delete_file, sanitize_provider_name, write_json_file, write_text_file,
|
||||||
};
|
};
|
||||||
|
use serde_json::Value;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
/// 获取 Codex 配置目录路径
|
/// 获取 Codex 配置目录路径
|
||||||
pub fn get_codex_config_dir() -> PathBuf {
|
pub fn get_codex_config_dir() -> PathBuf {
|
||||||
@@ -77,7 +77,8 @@ pub fn write_codex_live_atomic(auth: &Value, config_text_opt: Option<&str>) -> R
|
|||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
if !cfg_text.trim().is_empty() {
|
if !cfg_text.trim().is_empty() {
|
||||||
toml::from_str::<toml::Table>(&cfg_text).map_err(|e| format!("config.toml 格式错误: {}", e))?;
|
toml::from_str::<toml::Table>(&cfg_text)
|
||||||
|
.map_err(|e| format!("config.toml 格式错误: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第一步:写 auth.json
|
// 第一步:写 auth.json
|
||||||
@@ -121,7 +122,9 @@ pub fn validate_config_toml(text: &str) -> Result<(), String> {
|
|||||||
if text.trim().is_empty() {
|
if text.trim().is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
toml::from_str::<toml::Table>(text).map(|_| ()).map_err(|e| format!("config.toml 语法错误: {}", e))
|
toml::from_str::<toml::Table>(text)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| format!("config.toml 语法错误: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读取并校验 `~/.codex/config.toml`,返回文本(可能为空)
|
/// 读取并校验 `~/.codex/config.toml`,返回文本(可能为空)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use tauri_plugin_opener::OpenerExt;
|
|||||||
|
|
||||||
use crate::app_config::AppType;
|
use crate::app_config::AppType;
|
||||||
use crate::codex_config;
|
use crate::codex_config;
|
||||||
use crate::config::{ConfigStatus, get_claude_settings_path};
|
use crate::config::{get_claude_settings_path, ConfigStatus};
|
||||||
use crate::provider::Provider;
|
use crate::provider::Provider;
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
|
|
||||||
@@ -116,7 +116,9 @@ pub async fn add_provider(
|
|||||||
let manager = config
|
let manager = config
|
||||||
.get_manager_mut(&app_type)
|
.get_manager_mut(&app_type)
|
||||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
manager.providers.insert(provider.id.clone(), provider.clone());
|
manager
|
||||||
|
.providers
|
||||||
|
.insert(provider.id.clone(), provider.clone());
|
||||||
}
|
}
|
||||||
state.save()?;
|
state.save()?;
|
||||||
|
|
||||||
@@ -146,7 +148,10 @@ pub async fn update_provider(
|
|||||||
let manager = config
|
let manager = config
|
||||||
.get_manager(&app_type)
|
.get_manager(&app_type)
|
||||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
(manager.providers.contains_key(&provider.id), manager.current == provider.id)
|
(
|
||||||
|
manager.providers.contains_key(&provider.id),
|
||||||
|
manager.current == provider.id,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
if !exists {
|
if !exists {
|
||||||
return Err(format!("供应商不存在: {}", provider.id));
|
return Err(format!("供应商不存在: {}", provider.id));
|
||||||
@@ -182,7 +187,9 @@ pub async fn update_provider(
|
|||||||
let manager = config
|
let manager = config
|
||||||
.get_manager_mut(&app_type)
|
.get_manager_mut(&app_type)
|
||||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||||
manager.providers.insert(provider.id.clone(), provider.clone());
|
manager
|
||||||
|
.providers
|
||||||
|
.insert(provider.id.clone(), provider.clone());
|
||||||
}
|
}
|
||||||
state.save()?;
|
state.save()?;
|
||||||
|
|
||||||
@@ -390,7 +397,8 @@ pub async fn import_default_config(
|
|||||||
if !auth_path.exists() {
|
if !auth_path.exists() {
|
||||||
return Err("Codex 配置文件不存在".to_string());
|
return Err("Codex 配置文件不存在".to_string());
|
||||||
}
|
}
|
||||||
let auth: serde_json::Value = crate::config::read_json_file::<serde_json::Value>(&auth_path)?;
|
let auth: serde_json::Value =
|
||||||
|
crate::config::read_json_file::<serde_json::Value>(&auth_path)?;
|
||||||
let config_str = match crate::codex_config::read_and_validate_codex_config_text() {
|
let config_str = match crate::codex_config::read_and_validate_codex_config_text() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
@@ -500,7 +508,8 @@ pub async fn open_config_folder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用 opener 插件打开文件夹
|
// 使用 opener 插件打开文件夹
|
||||||
handle.opener()
|
handle
|
||||||
|
.opener()
|
||||||
.open_path(config_dir.to_string_lossy().to_string(), None::<String>)
|
.open_path(config_dir.to_string_lossy().to_string(), None::<String>)
|
||||||
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
||||||
|
|
||||||
@@ -524,3 +533,68 @@ pub async fn open_external(app: tauri::AppHandle, url: String) -> Result<bool, S
|
|||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取应用配置文件路径
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_app_config_path() -> Result<String, String> {
|
||||||
|
use crate::config::get_app_config_path;
|
||||||
|
|
||||||
|
let config_path = get_app_config_path();
|
||||||
|
Ok(config_path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开应用配置文件夹
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_app_config_folder(handle: tauri::AppHandle) -> Result<bool, String> {
|
||||||
|
use crate::config::get_app_config_dir;
|
||||||
|
|
||||||
|
let config_dir = get_app_config_dir();
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if !config_dir.exists() {
|
||||||
|
std::fs::create_dir_all(&config_dir).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 opener 插件打开文件夹
|
||||||
|
handle
|
||||||
|
.opener()
|
||||||
|
.open_path(config_dir.to_string_lossy().to_string(), None::<String>)
|
||||||
|
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取设置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_settings(_state: State<'_, AppState>) -> Result<serde_json::Value, String> {
|
||||||
|
// 暂时返回默认设置
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"showInDock": true
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存设置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_settings(
|
||||||
|
_state: State<'_, AppState>,
|
||||||
|
settings: serde_json::Value,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
// TODO: 实现设置保存逻辑
|
||||||
|
log::info!("保存设置: {:?}", settings);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查更新
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_for_updates(handle: tauri::AppHandle) -> Result<bool, String> {
|
||||||
|
// 打开 GitHub releases 页面
|
||||||
|
handle
|
||||||
|
.opener()
|
||||||
|
.open_url(
|
||||||
|
"https://github.com/farion1231/cc-switch/releases",
|
||||||
|
None::<String>,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("打开更新页面失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ pub fn archive_file(ts: u64, category: &str, src: &Path) -> Result<Option<PathBu
|
|||||||
Ok(Some(dest))
|
Ok(Some(dest))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 清理供应商名称,确保文件名安全
|
/// 清理供应商名称,确保文件名安全
|
||||||
pub fn sanitize_provider_name(name: &str) -> String {
|
pub fn sanitize_provider_name(name: &str) -> String {
|
||||||
name.chars()
|
name.chars()
|
||||||
|
|||||||
@@ -2,18 +2,238 @@ mod app_config;
|
|||||||
mod codex_config;
|
mod codex_config;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod migration;
|
||||||
mod provider;
|
mod provider;
|
||||||
mod store;
|
mod store;
|
||||||
mod migration;
|
|
||||||
|
|
||||||
use store::AppState;
|
use store::AppState;
|
||||||
use tauri::Manager;
|
use tauri::{
|
||||||
|
menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem},
|
||||||
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
|
};
|
||||||
|
use tauri::{Emitter, Manager};
|
||||||
|
|
||||||
|
/// 创建动态托盘菜单
|
||||||
|
fn create_tray_menu(
|
||||||
|
app: &tauri::AppHandle,
|
||||||
|
app_state: &AppState,
|
||||||
|
) -> Result<Menu<tauri::Wry>, String> {
|
||||||
|
let config = app_state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
let mut menu_builder = MenuBuilder::new(app);
|
||||||
|
|
||||||
|
// 直接添加所有供应商到主菜单(扁平化结构,更简单可靠)
|
||||||
|
if let Some(claude_manager) = config.get_manager(&crate::app_config::AppType::Claude) {
|
||||||
|
// 添加Claude标题(禁用状态,仅作为分组标识)
|
||||||
|
let claude_header =
|
||||||
|
MenuItem::with_id(app, "claude_header", "─── Claude ───", false, None::<&str>)
|
||||||
|
.map_err(|e| format!("创建Claude标题失败: {}", e))?;
|
||||||
|
menu_builder = menu_builder.item(&claude_header);
|
||||||
|
|
||||||
|
if !claude_manager.providers.is_empty() {
|
||||||
|
for (id, provider) in &claude_manager.providers {
|
||||||
|
let is_current = claude_manager.current == *id;
|
||||||
|
let item = CheckMenuItem::with_id(
|
||||||
|
app,
|
||||||
|
format!("claude_{}", id),
|
||||||
|
&provider.name,
|
||||||
|
true,
|
||||||
|
is_current,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("创建菜单项失败: {}", e))?;
|
||||||
|
menu_builder = menu_builder.item(&item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有供应商时显示提示
|
||||||
|
let empty_hint = MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"claude_empty",
|
||||||
|
" (无供应商,请在主界面添加)",
|
||||||
|
false,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("创建Claude空提示失败: {}", e))?;
|
||||||
|
menu_builder = menu_builder.item(&empty_hint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(codex_manager) = config.get_manager(&crate::app_config::AppType::Codex) {
|
||||||
|
// 添加Codex标题(禁用状态,仅作为分组标识)
|
||||||
|
let codex_header =
|
||||||
|
MenuItem::with_id(app, "codex_header", "─── Codex ───", false, None::<&str>)
|
||||||
|
.map_err(|e| format!("创建Codex标题失败: {}", e))?;
|
||||||
|
menu_builder = menu_builder.item(&codex_header);
|
||||||
|
|
||||||
|
if !codex_manager.providers.is_empty() {
|
||||||
|
for (id, provider) in &codex_manager.providers {
|
||||||
|
let is_current = codex_manager.current == *id;
|
||||||
|
let item = CheckMenuItem::with_id(
|
||||||
|
app,
|
||||||
|
format!("codex_{}", id),
|
||||||
|
&provider.name,
|
||||||
|
true,
|
||||||
|
is_current,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("创建菜单项失败: {}", e))?;
|
||||||
|
menu_builder = menu_builder.item(&item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有供应商时显示提示
|
||||||
|
let empty_hint = MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"codex_empty",
|
||||||
|
" (无供应商,请在主界面添加)",
|
||||||
|
false,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("创建Codex空提示失败: {}", e))?;
|
||||||
|
menu_builder = menu_builder.item(&empty_hint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分隔符和退出菜单
|
||||||
|
let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)
|
||||||
|
.map_err(|e| format!("创建退出菜单失败: {}", e))?;
|
||||||
|
|
||||||
|
menu_builder = menu_builder.separator().item(&quit_item);
|
||||||
|
|
||||||
|
menu_builder
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("构建菜单失败: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理托盘菜单事件
|
||||||
|
fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
|
||||||
|
println!("处理托盘菜单事件: {}", event_id);
|
||||||
|
|
||||||
|
match event_id {
|
||||||
|
"quit" => {
|
||||||
|
println!("退出应用");
|
||||||
|
app.exit(0);
|
||||||
|
}
|
||||||
|
id if id.starts_with("claude_") => {
|
||||||
|
let provider_id = id.strip_prefix("claude_").unwrap();
|
||||||
|
println!("切换到Claude供应商: {}", provider_id);
|
||||||
|
|
||||||
|
// 执行切换
|
||||||
|
let app_handle = app.clone();
|
||||||
|
let provider_id = provider_id.to_string();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if let Err(e) = switch_provider_internal(
|
||||||
|
&app_handle,
|
||||||
|
crate::app_config::AppType::Claude,
|
||||||
|
provider_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("切换Claude供应商失败: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
id if id.starts_with("codex_") => {
|
||||||
|
let provider_id = id.strip_prefix("codex_").unwrap();
|
||||||
|
println!("切换到Codex供应商: {}", provider_id);
|
||||||
|
|
||||||
|
// 执行切换
|
||||||
|
let app_handle = app.clone();
|
||||||
|
let provider_id = provider_id.to_string();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if let Err(e) = switch_provider_internal(
|
||||||
|
&app_handle,
|
||||||
|
crate::app_config::AppType::Codex,
|
||||||
|
provider_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("切换Codex供应商失败: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("未处理的菜单事件: {}", event_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内部切换供应商函数
|
||||||
|
async fn switch_provider_internal(
|
||||||
|
app: &tauri::AppHandle,
|
||||||
|
app_type: crate::app_config::AppType,
|
||||||
|
provider_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(app_state) = app.try_state::<AppState>() {
|
||||||
|
// 在使用前先保存需要的值
|
||||||
|
let app_type_str = app_type.as_str().to_string();
|
||||||
|
let provider_id_clone = provider_id.clone();
|
||||||
|
|
||||||
|
crate::commands::switch_provider(
|
||||||
|
app_state.clone().into(),
|
||||||
|
Some(app_type),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
provider_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 切换成功后重新创建托盘菜单
|
||||||
|
if let Ok(new_menu) = create_tray_menu(app, app_state.inner()) {
|
||||||
|
if let Some(tray) = app.tray_by_id("main") {
|
||||||
|
if let Err(e) = tray.set_menu(Some(new_menu)) {
|
||||||
|
eprintln!("更新托盘菜单失败: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发射事件到前端,通知供应商已切换
|
||||||
|
let event_data = serde_json::json!({
|
||||||
|
"appType": app_type_str,
|
||||||
|
"providerId": provider_id_clone
|
||||||
|
});
|
||||||
|
if let Err(e) = app.emit("provider-switched", event_data) {
|
||||||
|
eprintln!("发射供应商切换事件失败: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新托盘菜单的Tauri命令
|
||||||
|
#[tauri::command]
|
||||||
|
async fn update_tray_menu(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
state: tauri::State<'_, AppState>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
if let Ok(new_menu) = create_tray_menu(&app, state.inner()) {
|
||||||
|
if let Some(tray) = app.tray_by_id("main") {
|
||||||
|
tray.set_menu(Some(new_menu))
|
||||||
|
.map_err(|e| format!("更新托盘菜单失败: {}", e))?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
// 注册 Updater 插件(桌面端)
|
||||||
|
#[cfg(desktop)]
|
||||||
|
{
|
||||||
|
if let Err(e) = app
|
||||||
|
.handle()
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
|
{
|
||||||
|
// 若配置不完整(如缺少 pubkey),跳过 Updater 而不中断应用
|
||||||
|
log::warn!("初始化 Updater 插件失败,已跳过:{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
// 设置 macOS 标题栏背景色为主界面蓝色
|
// 设置 macOS 标题栏背景色为主界面蓝色
|
||||||
@@ -71,6 +291,36 @@ pub fn run() {
|
|||||||
// 保存配置
|
// 保存配置
|
||||||
let _ = app_state.save();
|
let _ = app_state.save();
|
||||||
|
|
||||||
|
// 创建动态托盘菜单
|
||||||
|
let menu = create_tray_menu(&app.handle(), &app_state)?;
|
||||||
|
|
||||||
|
let _tray = TrayIconBuilder::with_id("main")
|
||||||
|
.on_tray_icon_event(|tray, event| match event {
|
||||||
|
TrayIconEvent::Click {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
button_state: MouseButtonState::Up,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
println!("left click pressed and released");
|
||||||
|
// 在这个例子中,当点击托盘图标时,将展示并聚焦于主窗口
|
||||||
|
let app = tray.app_handle();
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("unhandled event {event:?}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.menu(&menu)
|
||||||
|
.on_menu_event(|app, event| {
|
||||||
|
handle_tray_menu_event(app, &event.id.0);
|
||||||
|
})
|
||||||
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
|
.show_menu_on_left_click(true)
|
||||||
|
.build(app)?;
|
||||||
// 将同一个实例注入到全局状态,避免重复创建导致的不一致
|
// 将同一个实例注入到全局状态,避免重复创建导致的不一致
|
||||||
app.manage(app_state);
|
app.manage(app_state);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -88,6 +338,12 @@ pub fn run() {
|
|||||||
commands::get_claude_code_config_path,
|
commands::get_claude_code_config_path,
|
||||||
commands::open_config_folder,
|
commands::open_config_folder,
|
||||||
commands::open_external,
|
commands::open_external,
|
||||||
|
commands::get_app_config_path,
|
||||||
|
commands::open_app_config_folder,
|
||||||
|
commands::get_settings,
|
||||||
|
commands::save_settings,
|
||||||
|
commands::check_for_updates,
|
||||||
|
update_tray_menu,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ fn extract_claude_api_key(value: &Value) -> Option<String> {
|
|||||||
fn extract_codex_api_key(value: &Value) -> Option<String> {
|
fn extract_codex_api_key(value: &Value) -> Option<String> {
|
||||||
value
|
value
|
||||||
.get("auth")
|
.get("auth")
|
||||||
.and_then(|auth| auth.get("OPENAI_API_KEY").or_else(|| auth.get("openai_api_key")))
|
.and_then(|auth| {
|
||||||
|
auth.get("OPENAI_API_KEY")
|
||||||
|
.or_else(|| auth.get("openai_api_key"))
|
||||||
|
})
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
}
|
}
|
||||||
@@ -77,7 +80,9 @@ fn scan_claude_copies() -> Vec<(String, PathBuf, Value)> {
|
|||||||
if !fname.starts_with("settings-") || !fname.ends_with(".json") {
|
if !fname.starts_with("settings-") || !fname.ends_with(".json") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let name = fname.trim_start_matches("settings-").trim_end_matches(".json");
|
let name = fname
|
||||||
|
.trim_start_matches("settings-")
|
||||||
|
.trim_end_matches(".json");
|
||||||
if let Ok(val) = crate::config::read_json_file::<Value>(&p) {
|
if let Ok(val) = crate::config::read_json_file::<Value>(&p) {
|
||||||
items.push((name.to_string(), p, val));
|
items.push((name.to_string(), p, val));
|
||||||
}
|
}
|
||||||
@@ -104,7 +109,9 @@ fn scan_codex_copies() -> Vec<(String, Option<PathBuf>, Option<PathBuf>, Value)>
|
|||||||
let entry = by_name.entry(name.to_string()).or_default();
|
let entry = by_name.entry(name.to_string()).or_default();
|
||||||
entry.0 = Some(p);
|
entry.0 = Some(p);
|
||||||
} else if fname.starts_with("config-") && fname.ends_with(".toml") {
|
} else if fname.starts_with("config-") && fname.ends_with(".toml") {
|
||||||
let name = fname.trim_start_matches("config-").trim_end_matches(".toml");
|
let name = fname
|
||||||
|
.trim_start_matches("config-")
|
||||||
|
.trim_end_matches(".toml");
|
||||||
let entry = by_name.entry(name.to_string()).or_default();
|
let entry = by_name.entry(name.to_string()).or_default();
|
||||||
entry.1 = Some(p);
|
entry.1 = Some(p);
|
||||||
}
|
}
|
||||||
@@ -183,17 +190,14 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
|||||||
|
|
||||||
if let Some((name, value)) = &live_claude {
|
if let Some((name, value)) = &live_claude {
|
||||||
let cand_key = extract_claude_api_key(value);
|
let cand_key = extract_claude_api_key(value);
|
||||||
let exist_id = manager
|
let exist_id = manager.providers.iter().find_map(|(id, p)| {
|
||||||
.providers
|
let pk = extract_claude_api_key(&p.settings_config);
|
||||||
.iter()
|
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||||
.find_map(|(id, p)| {
|
Some(id.clone())
|
||||||
let pk = extract_claude_api_key(&p.settings_config);
|
} else {
|
||||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
None
|
||||||
Some(id.clone())
|
}
|
||||||
} else {
|
});
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Some(exist_id) = exist_id {
|
if let Some(exist_id) = exist_id {
|
||||||
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
||||||
log::info!("合并到已存在 Claude 供应商 '{}' (by name+key)", name);
|
log::info!("合并到已存在 Claude 供应商 '{}' (by name+key)", name);
|
||||||
@@ -203,43 +207,36 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
|||||||
} else {
|
} else {
|
||||||
let id = next_unique_id(&ids, name);
|
let id = next_unique_id(&ids, name);
|
||||||
ids.insert(id.clone());
|
ids.insert(id.clone());
|
||||||
let provider = crate::provider::Provider::with_id(
|
let provider =
|
||||||
id.clone(),
|
crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None);
|
||||||
name.clone(),
|
|
||||||
value.clone(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
manager.providers.insert(provider.id.clone(), provider);
|
manager.providers.insert(provider.id.clone(), provider);
|
||||||
live_claude_id = Some(id);
|
live_claude_id = Some(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (name, path, value) in claude_items.iter() {
|
for (name, path, value) in claude_items.iter() {
|
||||||
let cand_key = extract_claude_api_key(value);
|
let cand_key = extract_claude_api_key(value);
|
||||||
let exist_id = manager
|
let exist_id = manager.providers.iter().find_map(|(id, p)| {
|
||||||
.providers
|
let pk = extract_claude_api_key(&p.settings_config);
|
||||||
.iter()
|
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||||
.find_map(|(id, p)| {
|
Some(id.clone())
|
||||||
let pk = extract_claude_api_key(&p.settings_config);
|
} else {
|
||||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
None
|
||||||
Some(id.clone())
|
}
|
||||||
} else {
|
});
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Some(exist_id) = exist_id {
|
if let Some(exist_id) = exist_id {
|
||||||
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
||||||
log::info!("覆盖 Claude 供应商 '{}' 来自 {} (by name+key)", name, path.display());
|
log::info!(
|
||||||
|
"覆盖 Claude 供应商 '{}' 来自 {} (by name+key)",
|
||||||
|
name,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
prov.settings_config = value.clone();
|
prov.settings_config = value.clone();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let id = next_unique_id(&ids, name);
|
let id = next_unique_id(&ids, name);
|
||||||
ids.insert(id.clone());
|
ids.insert(id.clone());
|
||||||
let provider = crate::provider::Provider::with_id(
|
let provider =
|
||||||
id.clone(),
|
crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None);
|
||||||
name.clone(),
|
|
||||||
value.clone(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
manager.providers.insert(provider.id.clone(), provider);
|
manager.providers.insert(provider.id.clone(), provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +254,10 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
|||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Some(("default".to_string(), serde_json::json!({"auth": auth, "config": cfg})))
|
Some((
|
||||||
|
"default".to_string(),
|
||||||
|
serde_json::json!({"auth": auth, "config": cfg}),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("读取 Codex live auth.json 失败: {}", e);
|
log::warn!("读取 Codex live auth.json 失败: {}", e);
|
||||||
@@ -277,17 +277,14 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
|||||||
|
|
||||||
if let Some((name, value)) = &live_codex {
|
if let Some((name, value)) = &live_codex {
|
||||||
let cand_key = extract_codex_api_key(value);
|
let cand_key = extract_codex_api_key(value);
|
||||||
let exist_id = manager
|
let exist_id = manager.providers.iter().find_map(|(id, p)| {
|
||||||
.providers
|
let pk = extract_codex_api_key(&p.settings_config);
|
||||||
.iter()
|
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||||
.find_map(|(id, p)| {
|
Some(id.clone())
|
||||||
let pk = extract_codex_api_key(&p.settings_config);
|
} else {
|
||||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
None
|
||||||
Some(id.clone())
|
}
|
||||||
} else {
|
});
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Some(exist_id) = exist_id {
|
if let Some(exist_id) = exist_id {
|
||||||
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
||||||
log::info!("合并到已存在 Codex 供应商 '{}' (by name+key)", name);
|
log::info!("合并到已存在 Codex 供应商 '{}' (by name+key)", name);
|
||||||
@@ -297,43 +294,37 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
|||||||
} else {
|
} else {
|
||||||
let id = next_unique_id(&ids, name);
|
let id = next_unique_id(&ids, name);
|
||||||
ids.insert(id.clone());
|
ids.insert(id.clone());
|
||||||
let provider = crate::provider::Provider::with_id(
|
let provider =
|
||||||
id.clone(),
|
crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None);
|
||||||
name.clone(),
|
|
||||||
value.clone(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
manager.providers.insert(provider.id.clone(), provider);
|
manager.providers.insert(provider.id.clone(), provider);
|
||||||
live_codex_id = Some(id);
|
live_codex_id = Some(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (name, authp, cfgp, value) in codex_items.iter() {
|
for (name, authp, cfgp, value) in codex_items.iter() {
|
||||||
let cand_key = extract_codex_api_key(value);
|
let cand_key = extract_codex_api_key(value);
|
||||||
let exist_id = manager
|
let exist_id = manager.providers.iter().find_map(|(id, p)| {
|
||||||
.providers
|
let pk = extract_codex_api_key(&p.settings_config);
|
||||||
.iter()
|
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||||
.find_map(|(id, p)| {
|
Some(id.clone())
|
||||||
let pk = extract_codex_api_key(&p.settings_config);
|
} else {
|
||||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
None
|
||||||
Some(id.clone())
|
}
|
||||||
} else {
|
});
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Some(exist_id) = exist_id {
|
if let Some(exist_id) = exist_id {
|
||||||
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
||||||
log::info!("覆盖 Codex 供应商 '{}' 来自 {:?}/{:?} (by name+key)", name, authp, cfgp);
|
log::info!(
|
||||||
|
"覆盖 Codex 供应商 '{}' 来自 {:?}/{:?} (by name+key)",
|
||||||
|
name,
|
||||||
|
authp,
|
||||||
|
cfgp
|
||||||
|
);
|
||||||
prov.settings_config = value.clone();
|
prov.settings_config = value.clone();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let id = next_unique_id(&ids, name);
|
let id = next_unique_id(&ids, name);
|
||||||
ids.insert(id.clone());
|
ids.insert(id.clone());
|
||||||
let provider = crate::provider::Provider::with_id(
|
let provider =
|
||||||
id.clone(),
|
crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None);
|
||||||
name.clone(),
|
|
||||||
value.clone(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
manager.providers.insert(provider.id.clone(), provider);
|
manager.providers.insert(provider.id.clone(), provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,13 +361,17 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
|||||||
for (_, ap, cp, _) in codex_items.into_iter() {
|
for (_, ap, cp, _) in codex_items.into_iter() {
|
||||||
if let Some(ap) = ap {
|
if let Some(ap) = ap {
|
||||||
match archive_file(ts, "codex", &ap) {
|
match archive_file(ts, "codex", &ap) {
|
||||||
Ok(Some(_)) => { let _ = delete_file(&ap); }
|
Ok(Some(_)) => {
|
||||||
|
let _ = delete_file(&ap);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(cp) = cp {
|
if let Some(cp) = cp {
|
||||||
match archive_file(ts, "codex", &cp) {
|
match archive_file(ts, "codex", &cp) {
|
||||||
Ok(Some(_)) => { let _ = delete_file(&cp); }
|
Ok(Some(_)) => {
|
||||||
|
let _ = delete_file(&cp);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,7 +399,11 @@ pub fn dedupe_config(config: &mut MultiAppConfig) -> usize {
|
|||||||
let mut keep: Map<String, String> = Map::new(); // key -> id 保留
|
let mut keep: Map<String, String> = Map::new(); // key -> id 保留
|
||||||
let mut remove: Vec<String> = Vec::new();
|
let mut remove: Vec<String> = Vec::new();
|
||||||
for (id, p) in mgr.providers.iter() {
|
for (id, p) in mgr.providers.iter() {
|
||||||
let k = format!("{}|{}", norm_name(&p.name), extract_key(&p.settings_config).unwrap_or_default());
|
let k = format!(
|
||||||
|
"{}|{}",
|
||||||
|
norm_name(&p.name),
|
||||||
|
extract_key(&p.settings_config).unwrap_or_default()
|
||||||
|
);
|
||||||
if let Some(exist_id) = keep.get(&k) {
|
if let Some(exist_id) = keep.get(&k) {
|
||||||
// 若当前是正在使用的,则用当前替换之前的,反之丢弃当前
|
// 若当前是正在使用的,则用当前替换之前的,反之丢弃当前
|
||||||
if *id == mgr.current {
|
if *id == mgr.current {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
@@ -38,4 +39,13 @@
|
|||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDRERTRCNEUxQUE3MDA4QTYKUldTbUNIQ3E0YlRrVFF2cnFVVE1jczlNZFlmemxXd0h6cTdibXRJWjBDSytQODdZOTYvR3d3d2oK",
|
||||||
|
"endpoints": [
|
||||||
|
"https://github.com/jasonyoung/cc-switch/releases/latest/download/latest.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
242
src/App.css
242
src/App.css
@@ -1,242 +0,0 @@
|
|||||||
.app {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header {
|
|
||||||
background: linear-gradient(180deg, #3498db 0%, #2d89c7 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 0.35rem 2rem 0.45rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto 1fr;
|
|
||||||
grid-template-rows: auto auto;
|
|
||||||
grid-template-areas:
|
|
||||||
". title ."
|
|
||||||
"tabs . actions";
|
|
||||||
align-items: center;
|
|
||||||
row-gap: 0.6rem;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-tabs {
|
|
||||||
grid-area: tabs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Segmented control */
|
|
||||||
.segmented {
|
|
||||||
--seg-bg: rgba(255, 255, 255, 0.16);
|
|
||||||
--seg-thumb: #ffffff;
|
|
||||||
--seg-color: rgba(255, 255, 255, 0.85);
|
|
||||||
--seg-active: #2d89c7;
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
width: 280px;
|
|
||||||
background: var(--seg-bg);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 4px;
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
|
|
||||||
backdrop-filter: saturate(140%) blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented-thumb {
|
|
||||||
position: absolute;
|
|
||||||
top: 4px;
|
|
||||||
left: 4px;
|
|
||||||
width: calc(50% - 4px);
|
|
||||||
height: calc(100% - 8px);
|
|
||||||
background: var(--seg-thumb);
|
|
||||||
border-radius: 999px;
|
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
|
||||||
transition:
|
|
||||||
transform 220ms ease,
|
|
||||||
width 220ms ease;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented-item {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 6px 16px; /* 更紧凑的高度 */
|
|
||||||
color: var(--seg-color);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.2px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented-item.active {
|
|
||||||
color: var(--seg-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
.segmented-item:focus-visible {
|
|
||||||
outline: 2px solid rgba(255, 255, 255, 0.8);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin: 0;
|
|
||||||
grid-area: title;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
grid-area: actions;
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn,
|
|
||||||
.add-btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
background: #3498db;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:hover:not(:disabled) {
|
|
||||||
background: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.import-btn {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
color: white;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.import-btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.import-btn:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn {
|
|
||||||
background: #27ae60;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn:hover {
|
|
||||||
background: #229954;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-main {
|
|
||||||
flex: 1;
|
|
||||||
padding: 2rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-path {
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #ecf0f1;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #7f8c8d;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browse-btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #3498db;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browse-btn:hover {
|
|
||||||
background: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 供应商列表区域 - 相对定位容器 */
|
|
||||||
.provider-section {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 浮动通知 - 绝对定位,不占据空间 */
|
|
||||||
.notification-floating {
|
|
||||||
position: absolute;
|
|
||||||
top: -10px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
padding: 0.75rem 1.25rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
width: fit-content;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in {
|
|
||||||
animation: fadeIn 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-out {
|
|
||||||
animation: fadeOut 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-success {
|
|
||||||
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-error {
|
|
||||||
background: linear-gradient(135deg, #e74c3c 0%, #ec7063 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
143
src/App.tsx
143
src/App.tsx
@@ -6,17 +6,17 @@ import AddProviderModal from "./components/AddProviderModal";
|
|||||||
import EditProviderModal from "./components/EditProviderModal";
|
import EditProviderModal from "./components/EditProviderModal";
|
||||||
import { ConfirmDialog } from "./components/ConfirmDialog";
|
import { ConfirmDialog } from "./components/ConfirmDialog";
|
||||||
import { AppSwitcher } from "./components/AppSwitcher";
|
import { AppSwitcher } from "./components/AppSwitcher";
|
||||||
import "./App.css";
|
import SettingsModal from "./components/SettingsModal";
|
||||||
|
import { Plus, Settings, Moon, Sun } from "lucide-react";
|
||||||
|
import { buttonStyles } from "./lib/styles";
|
||||||
|
import { useDarkMode } from "./hooks/useDarkMode";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||||
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
||||||
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
||||||
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [configStatus, setConfigStatus] = useState<{
|
|
||||||
exists: boolean;
|
|
||||||
path: string;
|
|
||||||
} | null>(null);
|
|
||||||
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -31,6 +31,7 @@ function App() {
|
|||||||
message: string;
|
message: string;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
// 设置通知的辅助函数
|
// 设置通知的辅助函数
|
||||||
@@ -62,7 +63,6 @@ function App() {
|
|||||||
// 加载供应商列表
|
// 加载供应商列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProviders();
|
loadProviders();
|
||||||
loadConfigStatus();
|
|
||||||
}, [activeApp]); // 当切换应用时重新加载
|
}, [activeApp]); // 当切换应用时重新加载
|
||||||
|
|
||||||
// 清理定时器
|
// 清理定时器
|
||||||
@@ -74,6 +74,35 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 监听托盘切换事件
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: (() => void) | null = null;
|
||||||
|
|
||||||
|
const setupListener = async () => {
|
||||||
|
try {
|
||||||
|
unlisten = await window.api.onProviderSwitched(async (data) => {
|
||||||
|
console.log("收到供应商切换事件:", data);
|
||||||
|
|
||||||
|
// 如果当前应用类型匹配,则重新加载数据
|
||||||
|
if (data.appType === activeApp) {
|
||||||
|
await loadProviders();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("设置供应商切换监听器失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setupListener();
|
||||||
|
|
||||||
|
// 清理监听器
|
||||||
|
return () => {
|
||||||
|
if (unlisten) {
|
||||||
|
unlisten();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [activeApp]); // 依赖activeApp,切换应用时重新设置监听器
|
||||||
|
|
||||||
const loadProviders = async () => {
|
const loadProviders = async () => {
|
||||||
const loadedProviders = await window.api.getProviders(activeApp);
|
const loadedProviders = await window.api.getProviders(activeApp);
|
||||||
const currentId = await window.api.getCurrentProvider(activeApp);
|
const currentId = await window.api.getCurrentProvider(activeApp);
|
||||||
@@ -86,13 +115,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadConfigStatus = async () => {
|
|
||||||
const status = await window.api.getConfigStatus(activeApp);
|
|
||||||
setConfigStatus({
|
|
||||||
exists: Boolean(status?.exists),
|
|
||||||
path: String(status?.path || ""),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
const generateId = () => {
|
const generateId = () => {
|
||||||
@@ -103,10 +125,13 @@ function App() {
|
|||||||
const newProvider: Provider = {
|
const newProvider: Provider = {
|
||||||
...provider,
|
...provider,
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
|
createdAt: Date.now(), // 添加创建时间戳
|
||||||
};
|
};
|
||||||
await window.api.addProvider(newProvider, activeApp);
|
await window.api.addProvider(newProvider, activeApp);
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditProvider = async (provider: Provider) => {
|
const handleEditProvider = async (provider: Provider) => {
|
||||||
@@ -116,6 +141,8 @@ function App() {
|
|||||||
setEditingProviderId(null);
|
setEditingProviderId(null);
|
||||||
// 显示编辑成功提示
|
// 显示编辑成功提示
|
||||||
showNotification("供应商配置已保存", "success", 2000);
|
showNotification("供应商配置已保存", "success", 2000);
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("更新供应商失败:", error);
|
console.error("更新供应商失败:", error);
|
||||||
setEditingProviderId(null);
|
setEditingProviderId(null);
|
||||||
@@ -134,6 +161,8 @@ function App() {
|
|||||||
await loadProviders();
|
await loadProviders();
|
||||||
setConfirmDialog(null);
|
setConfirmDialog(null);
|
||||||
showNotification("供应商删除成功", "success");
|
showNotification("供应商删除成功", "success");
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -149,6 +178,8 @@ function App() {
|
|||||||
"success",
|
"success",
|
||||||
2000,
|
2000,
|
||||||
);
|
);
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
} else {
|
} else {
|
||||||
showNotification("切换失败,请检查配置", "error");
|
showNotification("切换失败,请检查配置", "error");
|
||||||
}
|
}
|
||||||
@@ -162,6 +193,8 @@ function App() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
showNotification("已从现有配置创建默认供应商", "success", 3000);
|
showNotification("已从现有配置创建默认供应商", "success", 3000);
|
||||||
|
// 更新托盘菜单
|
||||||
|
await window.api.updateTrayMenu();
|
||||||
}
|
}
|
||||||
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
|
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -170,34 +203,57 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenConfigFolder = async () => {
|
|
||||||
await window.api.openConfigFolder(activeApp);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-950">
|
||||||
<header className="app-header">
|
{/* Linear 风格的顶部导航 */}
|
||||||
<h1>CC Switch</h1>
|
<header className="bg-white border-b border-gray-200 dark:bg-gray-900 dark:border-gray-800 px-6 py-4">
|
||||||
<div className="app-tabs">
|
<div className="flex items-center justify-between">
|
||||||
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<h1 className="text-xl font-semibold text-blue-500 dark:text-blue-400">
|
||||||
<div className="header-actions">
|
CC Switch
|
||||||
<button className="add-btn" onClick={() => setIsAddModalOpen(true)}>
|
</h1>
|
||||||
添加供应商
|
<button
|
||||||
</button>
|
onClick={toggleDarkMode}
|
||||||
|
className={buttonStyles.icon}
|
||||||
|
title={isDarkMode ? "切换到亮色模式" : "切换到暗色模式"}
|
||||||
|
>
|
||||||
|
{isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
className={buttonStyles.icon}
|
||||||
|
title="设置"
|
||||||
|
>
|
||||||
|
<Settings size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setIsAddModalOpen(true)}
|
||||||
|
className={`inline-flex items-center gap-2 ${buttonStyles.primary}`}
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
添加供应商
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="app-main">
|
{/* 主内容区域 */}
|
||||||
<div className="provider-section">
|
<main className="flex-1 p-6">
|
||||||
{/* 浮动通知组件 */}
|
<div className="max-w-4xl mx-auto">
|
||||||
|
{/* 通知组件 */}
|
||||||
{notification && (
|
{notification && (
|
||||||
<div
|
<div
|
||||||
className={`notification-floating ${
|
className={`fixed top-6 left-1/2 transform -translate-x-1/2 z-50 px-4 py-3 rounded-lg shadow-lg transition-all duration-300 ${
|
||||||
notification.type === "error"
|
notification.type === "error"
|
||||||
? "notification-error"
|
? "bg-red-500 text-white"
|
||||||
: "notification-success"
|
: "bg-green-500 text-white"
|
||||||
} ${isNotificationVisible ? "fade-in" : "fade-out"}`}
|
} ${isNotificationVisible ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2"}`}
|
||||||
>
|
>
|
||||||
{notification.message}
|
{notification.message}
|
||||||
</div>
|
</div>
|
||||||
@@ -210,23 +266,8 @@ function App() {
|
|||||||
onDelete={handleDeleteProvider}
|
onDelete={handleDeleteProvider}
|
||||||
onEdit={setEditingProviderId}
|
onEdit={setEditingProviderId}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{configStatus && (
|
</div>
|
||||||
<div className="config-path">
|
|
||||||
<span>
|
|
||||||
配置文件位置: {configStatus.path}
|
|
||||||
{!configStatus.exists ? "(未创建,切换或保存时会自动创建)" : ""}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className="browse-btn"
|
|
||||||
onClick={handleOpenConfigFolder}
|
|
||||||
title="打开配置文件夹"
|
|
||||||
>
|
|
||||||
打开
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{isAddModalOpen && (
|
{isAddModalOpen && (
|
||||||
@@ -255,6 +296,10 @@ function App() {
|
|||||||
onCancel={() => setConfirmDialog(null)}
|
onCancel={() => setConfirmDialog(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isSettingsOpen && (
|
||||||
|
<SettingsModal onClose={() => setIsSettingsOpen(false)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,268 +0,0 @@
|
|||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 640px;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow: hidden; /* 由 body 滚动,标题栏固定 */
|
|
||||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1001;
|
|
||||||
display: flex; /* 纵向布局,便于底栏固定 */
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 模拟窗口标题栏 */
|
|
||||||
.modal-titlebar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 3rem; /* 与主窗口标题栏一致 */
|
|
||||||
padding: 0 12px; /* 接近主头部的水平留白 */
|
|
||||||
background: #3498db; /* 与 .app-header 相同 */
|
|
||||||
color: #fff;
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 左侧占位以保证标题居中(与右侧关闭按钮宽度相当) */
|
|
||||||
.modal-spacer {
|
|
||||||
width: 32px;
|
|
||||||
flex: 0 0 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close-btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.18);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-form {
|
|
||||||
/* 表单外层包裹 body + footer */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 0; /* 允许子元素正确计算高度 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 1.25rem 1.5rem 1.5rem;
|
|
||||||
overflow: auto; /* 仅内容区滚动 */
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: #fee;
|
|
||||||
color: #c33;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border: 1px solid #fcc;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presets {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
border-bottom: 1px solid #ecf0f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presets label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #555;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-btn {
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
border: 1px solid #3498db;
|
|
||||||
background: white;
|
|
||||||
color: #3498db;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-btn:hover,
|
|
||||||
.preset-btn.selected {
|
|
||||||
background: #3498db;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 官方按钮橙色主题(Anthropic 风格) */
|
|
||||||
.preset-btn.official {
|
|
||||||
border: 1px solid #d97706;
|
|
||||||
color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset-btn.official:hover,
|
|
||||||
.preset-btn.official.selected {
|
|
||||||
background: #d97706;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #555;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* API Key 输入框容器 - 预留空间避免抖动 */
|
|
||||||
.form-group.api-key-group {
|
|
||||||
min-height: 88px; /* 固定高度:label + input + 间距 */
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group.api-key-group.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.625rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
background: white;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus,
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3498db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
/* 固定在弹窗底部(非滚动区) */
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-top: 1px solid #ecf0f1;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn,
|
|
||||||
.submit-btn {
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
background: #ecf0f1;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn:hover {
|
|
||||||
background: #bdc3c7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
background: #27ae60;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:hover {
|
|
||||||
background: #229954;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-hint {
|
|
||||||
display: block;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加标签和选择框的样式 */
|
|
||||||
.label-with-checkbox {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: baseline;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-with-checkbox label:first-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #666;
|
|
||||||
font-weight: normal;
|
|
||||||
margin-bottom: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
margin: 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/* 药丸式切换按钮 */
|
|
||||||
.switcher-pills {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 50px;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switcher-pill {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50px;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switcher-pill:hover:not(.active) {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switcher-pill.active {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
color: white;
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px 3px rgba(0, 0, 0, 0.1),
|
|
||||||
0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: currentColor;
|
|
||||||
opacity: 0.4;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switcher-pill.active .pill-dot {
|
|
||||||
opacity: 1;
|
|
||||||
box-shadow: 0 0 8px currentColor;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pills-divider {
|
|
||||||
width: 1px;
|
|
||||||
height: 20px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.2);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AppType } from "../lib/tauri-api";
|
import { AppType } from "../lib/tauri-api";
|
||||||
import "./AppSwitcher.css";
|
import { Terminal, Code2 } from "lucide-react";
|
||||||
|
|
||||||
interface AppSwitcherProps {
|
interface AppSwitcherProps {
|
||||||
activeApp: AppType;
|
activeApp: AppType;
|
||||||
@@ -13,22 +13,30 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="switcher-pills">
|
<div className="inline-flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 gap-1 border border-transparent dark:border-gray-700">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`switcher-pill ${activeApp === "claude" ? "active" : ""}`}
|
|
||||||
onClick={() => handleSwitch("claude")}
|
onClick={() => handleSwitch("claude")}
|
||||||
|
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||||
|
activeApp === "claude"
|
||||||
|
? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100 dark:shadow-none"
|
||||||
|
: "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span className="pill-dot" />
|
<Code2 size={16} />
|
||||||
<span>Claude Code</span>
|
<span>Claude Code</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="pills-divider" />
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`switcher-pill ${activeApp === "codex" ? "active" : ""}`}
|
|
||||||
onClick={() => handleSwitch("codex")}
|
onClick={() => handleSwitch("codex")}
|
||||||
|
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||||
|
activeApp === "codex"
|
||||||
|
? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100 dark:shadow-none"
|
||||||
|
: "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span className="pill-dot" />
|
<Terminal size={16} />
|
||||||
<span>Codex</span>
|
<span>Codex</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
.confirm-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-dialog {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
||||||
min-width: 300px;
|
|
||||||
max-width: 400px;
|
|
||||||
animation: confirmSlideIn 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes confirmSlideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9) translateY(-20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-header {
|
|
||||||
padding: 1.5rem 1.5rem 1rem;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-content {
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-content p {
|
|
||||||
margin: 0;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 1rem 1.5rem 1.5rem;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition:
|
|
||||||
background-color 0.2s,
|
|
||||||
transform 0.1s;
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
background: #f8f9fa;
|
|
||||||
color: #6c757d;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn:hover {
|
|
||||||
background: #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn-primary {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn-primary:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn:focus {
|
|
||||||
outline: 2px solid #007bff;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import "./ConfirmDialog.css";
|
import { AlertTriangle, X } from "lucide-react";
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -23,25 +23,52 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="confirm-overlay">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<div className="confirm-dialog">
|
{/* Backdrop */}
|
||||||
<div className="confirm-header">
|
<div
|
||||||
<h3>{title}</h3>
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||||
</div>
|
onClick={onCancel}
|
||||||
<div className="confirm-content">
|
/>
|
||||||
<p>{message}</p>
|
|
||||||
</div>
|
{/* Dialog */}
|
||||||
<div className="confirm-actions">
|
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-md w-full mx-4 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-red-100 dark:bg-red-500/10 rounded-full flex items-center justify-center">
|
||||||
|
<AlertTriangle size={20} className="text-red-500" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className="confirm-btn cancel-btn"
|
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
|
className="p-1 text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 leading-relaxed">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-900">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-900 hover:bg-white dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||||
autoFocus
|
autoFocus
|
||||||
>
|
>
|
||||||
{cancelText}
|
{cancelText}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="confirm-btn confirm-btn-primary"
|
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
|
className="px-4 py-2 text-sm font-medium bg-red-500 text-white hover:bg-red-500/90 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
{confirmText}
|
{confirmText}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
97
src/components/JsonEditor.tsx
Normal file
97
src/components/JsonEditor.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import React, { useRef, useEffect } from "react";
|
||||||
|
import { EditorView, basicSetup } from "codemirror";
|
||||||
|
import { json } from "@codemirror/lang-json";
|
||||||
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
|
import { EditorState } from "@codemirror/state";
|
||||||
|
import { placeholder } from "@codemirror/view";
|
||||||
|
|
||||||
|
interface JsonEditorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
darkMode?: boolean;
|
||||||
|
rows?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder: placeholderText = "",
|
||||||
|
darkMode = false,
|
||||||
|
rows = 12,
|
||||||
|
}) => {
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const viewRef = useRef<EditorView | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
|
||||||
|
// 创建编辑器扩展
|
||||||
|
const minHeightPx = Math.max(1, rows) * 18; // 降低最小高度以减少抖动
|
||||||
|
const sizingTheme = EditorView.theme({
|
||||||
|
"&": { minHeight: `${minHeightPx}px` },
|
||||||
|
".cm-scroller": { overflow: "auto" },
|
||||||
|
".cm-content": {
|
||||||
|
fontFamily:
|
||||||
|
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||||
|
fontSize: "14px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
basicSetup,
|
||||||
|
json(),
|
||||||
|
placeholder(placeholderText || ""),
|
||||||
|
sizingTheme,
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
const newValue = update.state.doc.toString();
|
||||||
|
onChange(newValue);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 如果启用深色模式,添加深色主题
|
||||||
|
if (darkMode) {
|
||||||
|
extensions.push(oneDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建初始状态
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc: value,
|
||||||
|
extensions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建编辑器视图
|
||||||
|
const view = new EditorView({
|
||||||
|
state,
|
||||||
|
parent: editorRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
|
viewRef.current = view;
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
view.destroy();
|
||||||
|
viewRef.current = null;
|
||||||
|
};
|
||||||
|
}, [darkMode, rows]); // 依赖项中不包含 onChange 和 placeholder,避免不必要的重建
|
||||||
|
|
||||||
|
// 当 value 从外部改变时更新编辑器内容
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewRef.current && viewRef.current.state.doc.toString() !== value) {
|
||||||
|
const transaction = viewRef.current.state.update({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: viewRef.current.state.doc.length,
|
||||||
|
insert: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
viewRef.current.dispatch(transaction);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return <div ref={editorRef} style={{ width: "100%" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JsonEditor;
|
||||||
@@ -4,14 +4,18 @@ import { AppType } from "../lib/tauri-api";
|
|||||||
import {
|
import {
|
||||||
updateCoAuthoredSetting,
|
updateCoAuthoredSetting,
|
||||||
checkCoAuthoredSetting,
|
checkCoAuthoredSetting,
|
||||||
extractWebsiteUrl,
|
|
||||||
getApiKeyFromConfig,
|
getApiKeyFromConfig,
|
||||||
hasApiKeyField,
|
hasApiKeyField,
|
||||||
setApiKeyInConfig,
|
setApiKeyInConfig,
|
||||||
} from "../utils/providerConfigUtils";
|
} from "../utils/providerConfigUtils";
|
||||||
import { providerPresets } from "../config/providerPresets";
|
import { providerPresets } from "../config/providerPresets";
|
||||||
import { codexProviderPresets } from "../config/codexProviderPresets";
|
import { codexProviderPresets } from "../config/codexProviderPresets";
|
||||||
import "./AddProviderModal.css";
|
import PresetSelector from "./ProviderForm/PresetSelector";
|
||||||
|
import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
||||||
|
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
||||||
|
import CodexConfigEditor from "./ProviderForm/CodexConfigEditor";
|
||||||
|
import KimiModelSelector from "./ProviderForm/KimiModelSelector";
|
||||||
|
import { X, AlertCircle, Save } from "lucide-react";
|
||||||
|
|
||||||
interface ProviderFormProps {
|
interface ProviderFormProps {
|
||||||
appType?: AppType;
|
appType?: AppType;
|
||||||
@@ -70,6 +74,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isCodex, initialData]);
|
}, [isCodex, initialData]);
|
||||||
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
@@ -78,12 +83,33 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
);
|
);
|
||||||
const [apiKey, setApiKey] = useState("");
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
|
||||||
|
// Kimi 模型选择状态
|
||||||
|
const [kimiAnthropicModel, setKimiAnthropicModel] = useState("");
|
||||||
|
const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] =
|
||||||
|
useState("");
|
||||||
|
|
||||||
// 初始化时检查禁用签名状态
|
// 初始化时检查禁用签名状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
|
|
||||||
|
// 初始化 Kimi 模型选择(编辑模式)
|
||||||
|
if (
|
||||||
|
initialData.settingsConfig &&
|
||||||
|
typeof initialData.settingsConfig === "object"
|
||||||
|
) {
|
||||||
|
const config = initialData.settingsConfig as {
|
||||||
|
env?: Record<string, any>;
|
||||||
|
};
|
||||||
|
if (config.env) {
|
||||||
|
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
|
setKimiAnthropicSmallFastModel(
|
||||||
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
@@ -160,9 +186,6 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
||||||
if (name === "settingsConfig") {
|
if (name === "settingsConfig") {
|
||||||
// 当用户修改配置时,尝试自动提取官网地址
|
|
||||||
const extractedWebsiteUrl = extractWebsiteUrl(value);
|
|
||||||
|
|
||||||
// 同时检查并同步选择框状态
|
// 同时检查并同步选择框状态
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(value);
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(value);
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
@@ -171,12 +194,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const parsedKey = getApiKeyFromConfig(value);
|
const parsedKey = getApiKeyFromConfig(value);
|
||||||
setApiKey(parsedKey);
|
setApiKey(parsedKey);
|
||||||
|
|
||||||
setFormData({
|
// 不再从 JSON 自动提取或覆盖官网地址,只更新配置内容
|
||||||
...formData,
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
// 只有在官网地址为空时才自动填入
|
}));
|
||||||
websiteUrl: formData.websiteUrl || extractedWebsiteUrl,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
@@ -218,6 +240,24 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 同步选择框状态
|
// 同步选择框状态
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
|
|
||||||
|
// 如果是 Kimi 预设,初始化模型选择
|
||||||
|
if (
|
||||||
|
preset.name?.includes("Kimi") &&
|
||||||
|
preset.settingsConfig &&
|
||||||
|
typeof preset.settingsConfig === "object"
|
||||||
|
) {
|
||||||
|
const config = preset.settingsConfig as { env?: Record<string, any> };
|
||||||
|
if (config.env) {
|
||||||
|
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
|
setKimiAnthropicSmallFastModel(
|
||||||
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setKimiAnthropicModel("");
|
||||||
|
setKimiAnthropicSmallFastModel("");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理点击自定义按钮
|
// 处理点击自定义按钮
|
||||||
@@ -230,6 +270,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
});
|
});
|
||||||
setApiKey("");
|
setApiKey("");
|
||||||
setDisableCoAuthored(false);
|
setDisableCoAuthored(false);
|
||||||
|
setKimiAnthropicModel("");
|
||||||
|
setKimiAnthropicSmallFastModel("");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Codex: 应用预设
|
// Codex: 应用预设
|
||||||
@@ -241,11 +283,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setCodexAuth(authString);
|
setCodexAuth(authString);
|
||||||
setCodexConfig(preset.config || "");
|
setCodexConfig(preset.config || "");
|
||||||
|
|
||||||
setFormData({
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
websiteUrl: preset.websiteUrl,
|
websiteUrl: preset.websiteUrl,
|
||||||
settingsConfig: formData.settingsConfig,
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedCodexPreset(index);
|
setSelectedCodexPreset(index);
|
||||||
|
|
||||||
@@ -311,6 +353,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
selectedPreset >= 0 &&
|
selectedPreset >= 0 &&
|
||||||
providerPresets[selectedPreset]?.isOfficial === true;
|
providerPresets[selectedPreset]?.isOfficial === true;
|
||||||
|
|
||||||
|
// 判断当前选中的预设是否是 Kimi
|
||||||
|
const isKimiPreset =
|
||||||
|
selectedPreset !== null &&
|
||||||
|
selectedPreset >= 0 &&
|
||||||
|
providerPresets[selectedPreset]?.name?.includes("Kimi");
|
||||||
|
|
||||||
|
// 判断当前编辑的是否是 Kimi 提供商(通过名称或配置判断)
|
||||||
|
const isEditingKimi =
|
||||||
|
initialData &&
|
||||||
|
(formData.name.includes("Kimi") ||
|
||||||
|
formData.name.includes("kimi") ||
|
||||||
|
(formData.settingsConfig.includes("api.moonshot.cn") &&
|
||||||
|
formData.settingsConfig.includes("ANTHROPIC_MODEL")));
|
||||||
|
|
||||||
|
// 综合判断是否应该显示 Kimi 模型选择器
|
||||||
|
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
||||||
|
|
||||||
// Codex: 控制显示 API Key 与官方标记
|
// Codex: 控制显示 API Key 与官方标记
|
||||||
const getCodexAuthApiKey = (authString: string): string => {
|
const getCodexAuthApiKey = (authString: string): string => {
|
||||||
try {
|
try {
|
||||||
@@ -320,15 +379,44 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自定义模式(-1)不显示独立的 API Key 输入框
|
// 自定义模式(-1)不显示独立的 API Key 输入框
|
||||||
const showCodexApiKey =
|
const showCodexApiKey =
|
||||||
(selectedCodexPreset !== null && selectedCodexPreset !== -1) ||
|
(selectedCodexPreset !== null && selectedCodexPreset !== -1) ||
|
||||||
(!showPresets && getCodexAuthApiKey(codexAuth) !== "");
|
(!showPresets && getCodexAuthApiKey(codexAuth) !== "");
|
||||||
|
|
||||||
const isCodexOfficialPreset =
|
const isCodexOfficialPreset =
|
||||||
selectedCodexPreset !== null &&
|
selectedCodexPreset !== null &&
|
||||||
selectedCodexPreset >= 0 &&
|
selectedCodexPreset >= 0 &&
|
||||||
codexProviderPresets[selectedCodexPreset]?.isOfficial === true;
|
codexProviderPresets[selectedCodexPreset]?.isOfficial === true;
|
||||||
|
|
||||||
|
// Kimi 模型选择处理函数
|
||||||
|
const handleKimiModelChange = (
|
||||||
|
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
if (field === "ANTHROPIC_MODEL") {
|
||||||
|
setKimiAnthropicModel(value);
|
||||||
|
} else {
|
||||||
|
setKimiAnthropicSmallFastModel(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置 JSON
|
||||||
|
try {
|
||||||
|
const currentConfig = JSON.parse(formData.settingsConfig || "{}");
|
||||||
|
if (!currentConfig.env) currentConfig.env = {};
|
||||||
|
currentConfig.env[field] = value;
|
||||||
|
|
||||||
|
const updatedConfigString = JSON.stringify(currentConfig, null, 2);
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
settingsConfig: updatedConfigString,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("更新 Kimi 模型配置失败:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始时从配置中同步 API Key(编辑模式)
|
// 初始时从配置中同步 API Key(编辑模式)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
@@ -354,130 +442,74 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal-overlay"
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
if (e.target === e.currentTarget) onClose();
|
if (e.target === e.currentTarget) onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="modal-content">
|
{/* Backdrop */}
|
||||||
<div className="modal-titlebar">
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
||||||
<div className="modal-spacer" />
|
|
||||||
<div className="modal-title" title={title}>
|
{/* Modal */}
|
||||||
|
<div className="relative bg-white rounded-xl shadow-lg max-w-3xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="modal-close-btn"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
title="关闭"
|
className="p-1 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-md transition-colors"
|
||||||
|
aria-label="关闭"
|
||||||
>
|
>
|
||||||
×
|
<X size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="modal-form">
|
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
||||||
<div className="modal-body">
|
<div className="flex-1 overflow-auto p-6 space-y-6">
|
||||||
{error && <div className="error-message">{error}</div>}
|
{error && (
|
||||||
|
<div className="flex items-center gap-3 p-4 bg-red-100 border border-red-500/20 rounded-lg">
|
||||||
|
<AlertCircle
|
||||||
|
size={20}
|
||||||
|
className="text-red-500 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<p className="text-red-500 text-sm font-medium">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showPresets && !isCodex && (
|
{showPresets && !isCodex && (
|
||||||
<div className="presets">
|
<PresetSelector
|
||||||
<label>选择配置类型</label>
|
presets={providerPresets}
|
||||||
<div className="preset-buttons">
|
selectedIndex={selectedPreset}
|
||||||
<button
|
onSelectPreset={(index) =>
|
||||||
type="button"
|
applyPreset(providerPresets[index], index)
|
||||||
className={`preset-btn ${
|
}
|
||||||
selectedPreset === -1 ? "selected" : ""
|
onCustomClick={handleCustomClick}
|
||||||
}`}
|
/>
|
||||||
onClick={handleCustomClick}
|
|
||||||
>
|
|
||||||
自定义
|
|
||||||
</button>
|
|
||||||
{providerPresets.map((preset, index) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
type="button"
|
|
||||||
className={`preset-btn ${
|
|
||||||
selectedPreset === index ? "selected" : ""
|
|
||||||
} ${preset.isOfficial ? "official" : ""}`}
|
|
||||||
onClick={() => applyPreset(preset, index)}
|
|
||||||
>
|
|
||||||
{preset.name}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{selectedPreset === -1 && (
|
|
||||||
<small
|
|
||||||
className="field-hint"
|
|
||||||
style={{ marginTop: "8px", display: "block" }}
|
|
||||||
>
|
|
||||||
手动配置供应商,需要填写完整的配置信息
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{selectedPreset !== -1 && selectedPreset !== null && (
|
|
||||||
<small
|
|
||||||
className="field-hint"
|
|
||||||
style={{ marginTop: "8px", display: "block" }}
|
|
||||||
>
|
|
||||||
{isOfficialPreset
|
|
||||||
? "Claude 官方登录,不需要填写 API Key"
|
|
||||||
: "使用预设配置,只需填写 API Key"}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showPresets && isCodex && (
|
{showPresets && isCodex && (
|
||||||
<div className="presets">
|
<PresetSelector
|
||||||
<label>选择配置类型</label>
|
presets={codexProviderPresets}
|
||||||
<div className="preset-buttons">
|
selectedIndex={selectedCodexPreset}
|
||||||
<button
|
onSelectPreset={(index) =>
|
||||||
type="button"
|
applyCodexPreset(codexProviderPresets[index], index)
|
||||||
className={`preset-btn ${
|
}
|
||||||
selectedCodexPreset === -1 ? "selected" : ""
|
onCustomClick={handleCodexCustomClick}
|
||||||
}`}
|
/>
|
||||||
onClick={handleCodexCustomClick}
|
|
||||||
>
|
|
||||||
自定义
|
|
||||||
</button>
|
|
||||||
{codexProviderPresets.map((preset, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
type="button"
|
|
||||||
className={`preset-btn ${
|
|
||||||
selectedCodexPreset === index ? "selected" : ""
|
|
||||||
} ${preset.isOfficial ? "official" : ""}`}
|
|
||||||
onClick={() => applyCodexPreset(preset, index)}
|
|
||||||
>
|
|
||||||
{preset.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{selectedCodexPreset === -1 && (
|
|
||||||
<small
|
|
||||||
className="field-hint"
|
|
||||||
style={{ marginTop: "8px", display: "block" }}
|
|
||||||
>
|
|
||||||
手动配置供应商,需要填写完整的配置信息
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{selectedCodexPreset !== -1 && selectedCodexPreset !== null && (
|
|
||||||
<small
|
|
||||||
className="field-hint"
|
|
||||||
style={{ marginTop: "8px", display: "block" }}
|
|
||||||
>
|
|
||||||
{isCodexOfficialPreset
|
|
||||||
? "Codex 官方登录,不需要填写 API Key"
|
|
||||||
: "使用预设配置,只需填写 API Key"}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="space-y-2">
|
||||||
<label htmlFor="name">供应商名称 *</label>
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
供应商名称 *
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
@@ -487,76 +519,62 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
placeholder="例如:Anthropic 官方"
|
placeholder="例如:Anthropic 官方"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isCodex && (
|
{!isCodex && showApiKey && (
|
||||||
<div
|
<ApiKeyInput
|
||||||
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
|
value={apiKey}
|
||||||
>
|
onChange={handleApiKeyChange}
|
||||||
<label htmlFor="apiKey">API Key *</label>
|
placeholder={
|
||||||
<input
|
isOfficialPreset
|
||||||
type="text"
|
? "官方登录无需填写 API Key,直接保存即可"
|
||||||
id="apiKey"
|
: shouldShowKimiSelector
|
||||||
value={apiKey}
|
? "sk-xxx-api-key-here (填写后可获取模型列表)"
|
||||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
|
||||||
placeholder={
|
|
||||||
isOfficialPreset
|
|
||||||
? "官方登录无需填写 API Key,直接保存即可"
|
|
||||||
: "只需要填这里,下方配置会自动填充"
|
: "只需要填这里,下方配置会自动填充"
|
||||||
}
|
}
|
||||||
disabled={isOfficialPreset}
|
disabled={isOfficialPreset}
|
||||||
autoComplete="off"
|
/>
|
||||||
style={
|
|
||||||
isOfficialPreset
|
|
||||||
? {
|
|
||||||
backgroundColor: "#f5f5f5",
|
|
||||||
cursor: "not-allowed",
|
|
||||||
color: "#999",
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCodex && (
|
{!isCodex && shouldShowKimiSelector && apiKey.trim() && (
|
||||||
<div
|
<KimiModelSelector
|
||||||
className={`form-group api-key-group ${!showCodexApiKey ? "hidden" : ""}`}
|
apiKey={apiKey}
|
||||||
|
anthropicModel={kimiAnthropicModel}
|
||||||
|
anthropicSmallFastModel={kimiAnthropicSmallFastModel}
|
||||||
|
onModelChange={handleKimiModelChange}
|
||||||
|
disabled={isOfficialPreset}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isCodex && showCodexApiKey && (
|
||||||
|
<ApiKeyInput
|
||||||
|
id="codexApiKey"
|
||||||
|
label="API Key"
|
||||||
|
value={codexApiKey}
|
||||||
|
onChange={handleCodexApiKeyChange}
|
||||||
|
placeholder={
|
||||||
|
isCodexOfficialPreset
|
||||||
|
? "官方无需填写 API Key,直接保存即可"
|
||||||
|
: "只需要填这里,下方 auth.json 会自动填充"
|
||||||
|
}
|
||||||
|
disabled={isCodexOfficialPreset}
|
||||||
|
required={
|
||||||
|
selectedCodexPreset !== null &&
|
||||||
|
selectedCodexPreset >= 0 &&
|
||||||
|
!isCodexOfficialPreset
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="websiteUrl"
|
||||||
|
className="block text-sm font-medium text-gray-900"
|
||||||
>
|
>
|
||||||
<label htmlFor="codexApiKey">API Key *</label>
|
官网地址
|
||||||
<input
|
</label>
|
||||||
type="text"
|
|
||||||
id="codexApiKey"
|
|
||||||
value={codexApiKey}
|
|
||||||
onChange={(e) => handleCodexApiKeyChange(e.target.value)}
|
|
||||||
placeholder={
|
|
||||||
isCodexOfficialPreset
|
|
||||||
? "官方无需填写 API Key,直接保存即可"
|
|
||||||
: "只需要填这里,下方 auth.json 会自动填充"
|
|
||||||
}
|
|
||||||
disabled={isCodexOfficialPreset}
|
|
||||||
required={
|
|
||||||
selectedCodexPreset !== null &&
|
|
||||||
selectedCodexPreset >= 0 &&
|
|
||||||
!isCodexOfficialPreset
|
|
||||||
}
|
|
||||||
autoComplete="off"
|
|
||||||
style={
|
|
||||||
isCodexOfficialPreset
|
|
||||||
? {
|
|
||||||
backgroundColor: "#f5f5f5",
|
|
||||||
cursor: "not-allowed",
|
|
||||||
color: "#999",
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor="websiteUrl">官网地址</label>
|
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
id="websiteUrl"
|
id="websiteUrl"
|
||||||
@@ -565,100 +583,58 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="https://example.com(可选)"
|
placeholder="https://example.com(可选)"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Claude 或 Codex 的配置部分 */}
|
{/* Claude 或 Codex 的配置部分 */}
|
||||||
{isCodex ? (
|
{isCodex ? (
|
||||||
// Codex: 双编辑器
|
<CodexConfigEditor
|
||||||
<>
|
authValue={codexAuth}
|
||||||
<div className="form-group">
|
configValue={codexConfig}
|
||||||
<label htmlFor="codexAuth">auth.json (JSON) *</label>
|
onAuthChange={setCodexAuth}
|
||||||
<textarea
|
onConfigChange={setCodexConfig}
|
||||||
id="codexAuth"
|
onAuthBlur={() => {
|
||||||
value={codexAuth}
|
try {
|
||||||
onChange={(e) => {
|
const auth = JSON.parse(codexAuth || "{}");
|
||||||
const value = e.target.value;
|
const key =
|
||||||
setCodexAuth(value);
|
typeof auth.OPENAI_API_KEY === "string"
|
||||||
try {
|
? auth.OPENAI_API_KEY
|
||||||
const auth = JSON.parse(value || "{}");
|
: "";
|
||||||
const key =
|
setCodexApiKey(key);
|
||||||
typeof auth.OPENAI_API_KEY === "string"
|
} catch {
|
||||||
? auth.OPENAI_API_KEY
|
// ignore
|
||||||
: "";
|
}
|
||||||
setCodexApiKey(key);
|
}}
|
||||||
} catch {
|
/>
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={`{
|
|
||||||
"OPENAI_API_KEY": "sk-your-api-key-here"
|
|
||||||
}`}
|
|
||||||
rows={6}
|
|
||||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<small className="field-hint">Codex auth.json 配置内容</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor="codexConfig">config.toml (TOML)</label>
|
|
||||||
<textarea
|
|
||||||
id="codexConfig"
|
|
||||||
value={codexConfig}
|
|
||||||
onChange={(e) => setCodexConfig(e.target.value)}
|
|
||||||
placeholder={``}
|
|
||||||
rows={8}
|
|
||||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
|
||||||
/>
|
|
||||||
<small className="field-hint">
|
|
||||||
Codex config.toml 配置内容
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
// Claude: 原有的单编辑器
|
<ClaudeConfigEditor
|
||||||
<div className="form-group">
|
value={formData.settingsConfig}
|
||||||
<div className="label-with-checkbox">
|
onChange={(value) =>
|
||||||
<label htmlFor="settingsConfig">
|
handleChange({
|
||||||
Claude Code 配置 (JSON) *
|
target: { name: "settingsConfig", value },
|
||||||
</label>
|
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||||
<label className="checkbox-label">
|
}
|
||||||
<input
|
disableCoAuthored={disableCoAuthored}
|
||||||
type="checkbox"
|
onCoAuthoredToggle={handleCoAuthoredToggle}
|
||||||
checked={disableCoAuthored}
|
/>
|
||||||
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
|
|
||||||
/>
|
|
||||||
禁止 Claude Code 签名
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
id="settingsConfig"
|
|
||||||
name="settingsConfig"
|
|
||||||
value={formData.settingsConfig}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder={`{
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
rows={12}
|
|
||||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<small className="field-hint">
|
|
||||||
完整的 Claude Code settings.json 配置内容
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-footer">
|
{/* Footer */}
|
||||||
<button type="button" className="cancel-btn" onClick={onClose}>
|
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 bg-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-900 hover:bg-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="submit-btn">
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<Save size={16} />
|
||||||
{submitText}
|
{submitText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
70
src/components/ProviderForm/ApiKeyInput.tsx
Normal file
70
src/components/ProviderForm/ApiKeyInput.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
|
||||||
|
interface ApiKeyInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "请输入API Key",
|
||||||
|
disabled = false,
|
||||||
|
required = false,
|
||||||
|
label = "API Key",
|
||||||
|
id = "apiKey",
|
||||||
|
}) => {
|
||||||
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
|
||||||
|
const toggleShowKey = () => {
|
||||||
|
setShowKey(!showKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputClass = `w-full px-3 py-2 pr-10 border rounded-lg text-sm transition-colors ${
|
||||||
|
disabled
|
||||||
|
? "bg-gray-100 border-gray-200 text-gray-400 cursor-not-allowed"
|
||||||
|
: "border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="block text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{label} {required && "*"}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showKey ? "text" : "password"}
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
|
autoComplete="off"
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
{!disabled && value && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleShowKey}
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 hover:text-gray-900 transition-colors"
|
||||||
|
aria-label={showKey ? "隐藏API Key" : "显示API Key"}
|
||||||
|
>
|
||||||
|
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiKeyInput;
|
||||||
54
src/components/ProviderForm/ClaudeConfigEditor.tsx
Normal file
54
src/components/ProviderForm/ClaudeConfigEditor.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import JsonEditor from "../JsonEditor";
|
||||||
|
|
||||||
|
interface ClaudeConfigEditorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
disableCoAuthored: boolean;
|
||||||
|
onCoAuthoredToggle: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disableCoAuthored,
|
||||||
|
onCoAuthoredToggle,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
htmlFor="settingsConfig"
|
||||||
|
className="block text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
Claude Code 配置 (JSON) *
|
||||||
|
</label>
|
||||||
|
<label className="inline-flex items-center gap-2 text-sm text-gray-500 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={disableCoAuthored}
|
||||||
|
onChange={(e) => onCoAuthoredToggle(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-500 bg-white border-gray-200 rounded focus:ring-blue-500 focus:ring-2"
|
||||||
|
/>
|
||||||
|
禁止 Claude Code 签名
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<JsonEditor
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={`{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
rows={12}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
完整的 Claude Code settings.json 配置内容
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClaudeConfigEditor;
|
||||||
67
src/components/ProviderForm/CodexConfigEditor.tsx
Normal file
67
src/components/ProviderForm/CodexConfigEditor.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface CodexConfigEditorProps {
|
||||||
|
authValue: string;
|
||||||
|
configValue: string;
|
||||||
|
onAuthChange: (value: string) => void;
|
||||||
|
onConfigChange: (value: string) => void;
|
||||||
|
onAuthBlur?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||||
|
authValue,
|
||||||
|
configValue,
|
||||||
|
onAuthChange,
|
||||||
|
onConfigChange,
|
||||||
|
onAuthBlur,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="codexAuth"
|
||||||
|
className="block text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
auth.json (JSON) *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="codexAuth"
|
||||||
|
value={authValue}
|
||||||
|
onChange={(e) => onAuthChange(e.target.value)}
|
||||||
|
onBlur={onAuthBlur}
|
||||||
|
placeholder={`{
|
||||||
|
"OPENAI_API_KEY": "sk-your-api-key-here"
|
||||||
|
}`}
|
||||||
|
rows={6}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors resize-y min-h-[8rem]"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Codex auth.json 配置内容
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="codexConfig"
|
||||||
|
className="block text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
config.toml (TOML)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="codexConfig"
|
||||||
|
value={configValue}
|
||||||
|
onChange={(e) => onConfigChange(e.target.value)}
|
||||||
|
placeholder=""
|
||||||
|
rows={8}
|
||||||
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors resize-y min-h-[10rem]"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Codex config.toml 配置内容
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodexConfigEditor;
|
||||||
185
src/components/ProviderForm/KimiModelSelector.tsx
Normal file
185
src/components/ProviderForm/KimiModelSelector.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { ChevronDown, RefreshCw, AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
|
interface KimiModel {
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
created: number;
|
||||||
|
owned_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KimiModelSelectorProps {
|
||||||
|
apiKey: string;
|
||||||
|
anthropicModel: string;
|
||||||
|
anthropicSmallFastModel: string;
|
||||||
|
onModelChange: (
|
||||||
|
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
|
value: string,
|
||||||
|
) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||||
|
apiKey,
|
||||||
|
anthropicModel,
|
||||||
|
anthropicSmallFastModel,
|
||||||
|
onModelChange,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const [models, setModels] = useState<KimiModel[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [debouncedKey, setDebouncedKey] = useState("");
|
||||||
|
|
||||||
|
// 获取模型列表
|
||||||
|
const fetchModelsWithKey = async (key: string) => {
|
||||||
|
if (!key) {
|
||||||
|
setError("请先填写 API Key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.moonshot.cn/v1/models", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.data && Array.isArray(data.data)) {
|
||||||
|
setModels(data.data);
|
||||||
|
} else {
|
||||||
|
throw new Error("返回数据格式错误");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取模型列表失败:", err);
|
||||||
|
setError(err instanceof Error ? err.message : "获取模型列表失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 500ms 防抖 API Key
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedKey(apiKey.trim());
|
||||||
|
}, 500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [apiKey]);
|
||||||
|
|
||||||
|
// 当防抖后的 Key 改变时自动获取模型列表
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedKey) {
|
||||||
|
fetchModelsWithKey(debouncedKey);
|
||||||
|
} else {
|
||||||
|
setModels([]);
|
||||||
|
setError("");
|
||||||
|
}
|
||||||
|
}, [debouncedKey]);
|
||||||
|
|
||||||
|
const selectClass = `w-full px-3 py-2 border rounded-lg text-sm transition-colors appearance-none bg-white ${
|
||||||
|
disabled
|
||||||
|
? "bg-gray-100 border-gray-200 text-gray-400 cursor-not-allowed"
|
||||||
|
: "border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const ModelSelect: React.FC<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}> = ({ label, value, onChange }) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-900">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
disabled={disabled || loading || models.length === 0}
|
||||||
|
className={selectClass}
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
{loading
|
||||||
|
? "加载中..."
|
||||||
|
: models.length === 0
|
||||||
|
? "暂无模型"
|
||||||
|
: "请选择模型"}
|
||||||
|
</option>
|
||||||
|
{models.map((model) => (
|
||||||
|
<option key={model.id} value={model.id}>
|
||||||
|
{model.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 pointer-events-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900">
|
||||||
|
模型配置
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => debouncedKey && fetchModelsWithKey(debouncedKey)}
|
||||||
|
disabled={disabled || loading || !debouncedKey}
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-1 text-xs text-gray-500 hover:text-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
|
||||||
|
刷新模型列表
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-center gap-2 p-3 bg-red-100 border border-red-500/20 rounded-lg">
|
||||||
|
<AlertCircle
|
||||||
|
size={16}
|
||||||
|
className="text-red-500 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<p className="text-red-500 text-xs">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<ModelSelect
|
||||||
|
label="主模型 (ANTHROPIC_MODEL)"
|
||||||
|
value={anthropicModel}
|
||||||
|
onChange={(value) => onModelChange("ANTHROPIC_MODEL", value)}
|
||||||
|
/>
|
||||||
|
<ModelSelect
|
||||||
|
label="快速模型 (ANTHROPIC_SMALL_FAST_MODEL)"
|
||||||
|
value={anthropicSmallFastModel}
|
||||||
|
onChange={(value) =>
|
||||||
|
onModelChange("ANTHROPIC_SMALL_FAST_MODEL", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!apiKey.trim() && (
|
||||||
|
<div className="p-3 bg-gray-100 border border-gray-200 rounded-lg">
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
📝 请先填写 API Key(格式:sk-xxx-api-key-here)以获取可用模型列表
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KimiModelSelector;
|
||||||
91
src/components/ProviderForm/PresetSelector.tsx
Normal file
91
src/components/ProviderForm/PresetSelector.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Zap } from "lucide-react";
|
||||||
|
|
||||||
|
interface Preset {
|
||||||
|
name: string;
|
||||||
|
isOfficial?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PresetSelectorProps {
|
||||||
|
title?: string;
|
||||||
|
presets: Preset[];
|
||||||
|
selectedIndex: number | null;
|
||||||
|
onSelectPreset: (index: number) => void;
|
||||||
|
onCustomClick: () => void;
|
||||||
|
customLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||||
|
title = "选择配置类型",
|
||||||
|
presets,
|
||||||
|
selectedIndex,
|
||||||
|
onSelectPreset,
|
||||||
|
onCustomClick,
|
||||||
|
customLabel = "自定义",
|
||||||
|
}) => {
|
||||||
|
const getButtonClass = (index: number, isOfficial?: boolean) => {
|
||||||
|
const isSelected = selectedIndex === index;
|
||||||
|
const baseClass =
|
||||||
|
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
return isOfficial
|
||||||
|
? `${baseClass} bg-amber-500 text-white`
|
||||||
|
: `${baseClass} bg-blue-500 text-white`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseClass} bg-gray-100 text-gray-500 hover:bg-gray-200`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDescription = () => {
|
||||||
|
if (selectedIndex === -1) {
|
||||||
|
return "手动配置供应商,需要填写完整的配置信息";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIndex !== null && selectedIndex >= 0) {
|
||||||
|
const preset = presets[selectedIndex];
|
||||||
|
return preset?.isOfficial
|
||||||
|
? "Claude 官方登录,不需要填写 API Key"
|
||||||
|
: "使用预设配置,只需填写 API Key";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-900 mb-3">
|
||||||
|
{title}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${getButtonClass(-1)} ${selectedIndex === -1 ? '' : ''}`}
|
||||||
|
onClick={onCustomClick}
|
||||||
|
>
|
||||||
|
{customLabel}
|
||||||
|
</button>
|
||||||
|
{presets.map((preset, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
className={getButtonClass(index, preset.isOfficial)}
|
||||||
|
onClick={() => onSelectPreset(index)}
|
||||||
|
>
|
||||||
|
{preset.isOfficial && <Zap size={14} />}
|
||||||
|
{preset.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{getDescription() && (
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{getDescription()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PresetSelector;
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
.provider-list {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem 1rem;
|
|
||||||
color: #7f8c8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p:first-child {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-items {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 2px solid #ecf0f1;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-item:hover {
|
|
||||||
border-color: #3498db;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-item.current {
|
|
||||||
border-color: #27ae60;
|
|
||||||
background: #f0fdf4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-name input[type="radio"] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-name input[type="radio"]:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-badge {
|
|
||||||
background: #27ae60;
|
|
||||||
color: white;
|
|
||||||
padding: 0.125rem 0.5rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-url {
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-link {
|
|
||||||
color: #3498db;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url-link:hover {
|
|
||||||
color: #2980b9;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-url {
|
|
||||||
color: #7f8c8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-right: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
color: #555;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.response-time {
|
|
||||||
color: #3498db;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-btn {
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
border: 1px solid #f39c12;
|
|
||||||
background: white;
|
|
||||||
color: #f39c12;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-btn:hover:not(:disabled) {
|
|
||||||
background: #f39c12;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.enable-btn {
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
border: 1px solid #27ae60;
|
|
||||||
background: white;
|
|
||||||
color: #27ae60;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.enable-btn:hover:not(:disabled) {
|
|
||||||
background: #27ae60;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.enable-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-btn {
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
border: 1px solid #3498db;
|
|
||||||
background: white;
|
|
||||||
color: #3498db;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-btn:hover:not(:disabled) {
|
|
||||||
background: #3498db;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn {
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
border: 1px solid #e74c3c;
|
|
||||||
background: white;
|
|
||||||
color: #e74c3c;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn:hover:not(:disabled) {
|
|
||||||
background: #e74c3c;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Provider } from "../types";
|
import { Provider } from "../types";
|
||||||
import "./ProviderList.css";
|
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
||||||
|
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
||||||
|
|
||||||
interface ProviderListProps {
|
interface ProviderListProps {
|
||||||
providers: Record<string, Provider>;
|
providers: Record<string, Provider>;
|
||||||
@@ -17,14 +18,20 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
}) => {
|
}) => {
|
||||||
// 提取API地址
|
// 提取API地址(兼容不同供应商配置:Claude env / Codex TOML)
|
||||||
const getApiUrl = (provider: Provider): string => {
|
const getApiUrl = (provider: Provider): string => {
|
||||||
try {
|
try {
|
||||||
const config = provider.settingsConfig;
|
const cfg = provider.settingsConfig;
|
||||||
if (config?.env?.ANTHROPIC_BASE_URL) {
|
// Claude/Anthropic: 从 env 中读取
|
||||||
return config.env.ANTHROPIC_BASE_URL;
|
if (cfg?.env?.ANTHROPIC_BASE_URL) {
|
||||||
|
return cfg.env.ANTHROPIC_BASE_URL;
|
||||||
}
|
}
|
||||||
return "未设置";
|
// Codex: 从 TOML 配置中解析 base_url
|
||||||
|
if (typeof cfg?.config === "string" && cfg.config.includes("base_url")) {
|
||||||
|
const match = cfg.config.match(/base_url\s*=\s*"([^"]+)"/);
|
||||||
|
if (match && match[1]) return match[1];
|
||||||
|
}
|
||||||
|
return "未配置官网地址";
|
||||||
} catch {
|
} catch {
|
||||||
return "配置错误";
|
return "配置错误";
|
||||||
}
|
}
|
||||||
@@ -38,72 +45,128 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 对供应商列表进行排序
|
||||||
|
const sortedProviders = Object.values(providers).sort((a, b) => {
|
||||||
|
// 按添加时间排序
|
||||||
|
// 没有时间戳的视为最早添加的(排在最前面)
|
||||||
|
// 有时间戳的按时间升序排列
|
||||||
|
const timeA = a.createdAt || 0;
|
||||||
|
const timeB = b.createdAt || 0;
|
||||||
|
|
||||||
|
// 如果都没有时间戳,按名称排序
|
||||||
|
if (timeA === 0 && timeB === 0) {
|
||||||
|
return a.name.localeCompare(b.name, 'zh-CN');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果只有一个没有时间戳,没有时间戳的排在前面
|
||||||
|
if (timeA === 0) return -1;
|
||||||
|
if (timeB === 0) return 1;
|
||||||
|
|
||||||
|
// 都有时间戳,按时间升序
|
||||||
|
return timeA - timeB;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="provider-list">
|
<div className="space-y-4">
|
||||||
{Object.values(providers).length === 0 ? (
|
{sortedProviders.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="text-center py-12">
|
||||||
<p>还没有添加任何供应商</p>
|
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
<p>点击右上角的"添加供应商"按钮开始</p>
|
<Users size={24} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
还没有添加任何供应商
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
点击右上角的"添加供应商"按钮开始配置您的第一个API供应商
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="provider-items">
|
<div className="space-y-3">
|
||||||
{Object.values(providers).map((provider) => {
|
{sortedProviders.map((provider) => {
|
||||||
const isCurrent = provider.id === currentProviderId;
|
const isCurrent = provider.id === currentProviderId;
|
||||||
|
const apiUrl = getApiUrl(provider);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
className={`provider-item ${isCurrent ? "current" : ""}`}
|
className={cn(
|
||||||
|
isCurrent ? cardStyles.selected : cardStyles.interactive
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="provider-info">
|
<div className="flex items-start justify-between">
|
||||||
<div className="provider-name">
|
<div className="flex-1">
|
||||||
<span>{provider.name}</span>
|
<div className="flex items-center gap-3 mb-2">
|
||||||
{isCurrent && (
|
<h3 className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
<span className="current-badge">当前使用</span>
|
{provider.name}
|
||||||
)}
|
</h3>
|
||||||
</div>
|
{isCurrent && (
|
||||||
<div className="provider-url">
|
<div className={badgeStyles.success}>
|
||||||
{provider.websiteUrl ? (
|
<CheckCircle2 size={12} />
|
||||||
<a
|
当前使用
|
||||||
href="#"
|
</div>
|
||||||
onClick={(e) => {
|
)}
|
||||||
e.preventDefault();
|
</div>
|
||||||
handleUrlClick(provider.websiteUrl!);
|
|
||||||
}}
|
|
||||||
className="url-link"
|
|
||||||
title={`访问 ${provider.websiteUrl}`}
|
|
||||||
>
|
|
||||||
{provider.websiteUrl}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="api-url" title={getApiUrl(provider)}>
|
|
||||||
{getApiUrl(provider)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="provider-actions">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<button
|
{provider.websiteUrl ? (
|
||||||
className="enable-btn"
|
<button
|
||||||
onClick={() => onSwitch(provider.id)}
|
onClick={(e) => {
|
||||||
disabled={isCurrent}
|
e.preventDefault();
|
||||||
>
|
handleUrlClick(provider.websiteUrl!);
|
||||||
启用
|
}}
|
||||||
</button>
|
className="inline-flex items-center gap-1 text-blue-500 dark:text-blue-400 hover:opacity-90 transition-colors"
|
||||||
<button
|
title={`访问 ${provider.websiteUrl}`}
|
||||||
className="edit-btn"
|
>
|
||||||
onClick={() => onEdit(provider.id)}
|
{provider.websiteUrl}
|
||||||
>
|
</button>
|
||||||
编辑
|
) : (
|
||||||
</button>
|
<span
|
||||||
<button
|
className="text-gray-500 dark:text-gray-400"
|
||||||
className="delete-btn"
|
title={apiUrl}
|
||||||
onClick={() => onDelete(provider.id)}
|
>
|
||||||
disabled={isCurrent}
|
{apiUrl}
|
||||||
>
|
</span>
|
||||||
删除
|
)}
|
||||||
</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 ml-4">
|
||||||
|
<button
|
||||||
|
onClick={() => onSwitch(provider.id)}
|
||||||
|
disabled={isCurrent}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
|
||||||
|
isCurrent
|
||||||
|
? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed"
|
||||||
|
: "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Play size={14} />
|
||||||
|
{isCurrent ? "使用中" : "启用"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onEdit(provider.id)}
|
||||||
|
className={buttonStyles.icon}
|
||||||
|
title="编辑供应商"
|
||||||
|
>
|
||||||
|
<Edit3 size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onDelete(provider.id)}
|
||||||
|
disabled={isCurrent}
|
||||||
|
className={cn(
|
||||||
|
buttonStyles.icon,
|
||||||
|
isCurrent
|
||||||
|
? "text-gray-400 cursor-not-allowed"
|
||||||
|
: "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10"
|
||||||
|
)}
|
||||||
|
title="删除供应商"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
212
src/components/SettingsModal.tsx
Normal file
212
src/components/SettingsModal.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { X, Info, RefreshCw, FolderOpen } from "lucide-react";
|
||||||
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
import "../lib/tauri-api";
|
||||||
|
import { runUpdateFlow } from "../lib/updater";
|
||||||
|
import type { Settings } from "../types";
|
||||||
|
|
||||||
|
interface SettingsModalProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||||
|
const [settings, setSettings] = useState<Settings>({
|
||||||
|
showInDock: true,
|
||||||
|
});
|
||||||
|
const [configPath, setConfigPath] = useState<string>("");
|
||||||
|
const [version, setVersion] = useState<string>("");
|
||||||
|
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadSettings();
|
||||||
|
loadConfigPath();
|
||||||
|
loadVersion();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadVersion = async () => {
|
||||||
|
try {
|
||||||
|
const appVersion = await getVersion();
|
||||||
|
setVersion(appVersion);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取版本信息失败:", error);
|
||||||
|
setVersion("3.1.1"); // 降级使用默认版本
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const loadedSettings = await window.api.getSettings();
|
||||||
|
if (loadedSettings?.showInDock !== undefined) {
|
||||||
|
setSettings({ showInDock: loadedSettings.showInDock });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载设置失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadConfigPath = async () => {
|
||||||
|
try {
|
||||||
|
const path = await window.api.getAppConfigPath();
|
||||||
|
if (path) {
|
||||||
|
setConfigPath(path);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取配置路径失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
try {
|
||||||
|
await window.api.saveSettings(settings);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("保存设置失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckUpdate = async () => {
|
||||||
|
setIsCheckingUpdate(true);
|
||||||
|
try {
|
||||||
|
// 优先使用 Tauri Updater 流程;失败时回退到打开 Releases 页面
|
||||||
|
await runUpdateFlow({ timeout: 30000 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("检查更新失败,回退到 Releases 页面:", error);
|
||||||
|
await window.api.checkForUpdates();
|
||||||
|
} finally {
|
||||||
|
setIsCheckingUpdate(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenConfigFolder = async () => {
|
||||||
|
try {
|
||||||
|
await window.api.openAppConfigFolder();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("打开配置文件夹失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-[500px] overflow-hidden">
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<h2 className="text-lg font-semibold text-blue-500 dark:text-blue-400">
|
||||||
|
设置
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
<X size={20} className="text-gray-500 dark:text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 设置内容 */}
|
||||||
|
<div className="px-6 py-4 space-y-6">
|
||||||
|
{/* 显示设置 - 功能还未实现 */}
|
||||||
|
{/* <div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
显示设置
|
||||||
|
</h3>
|
||||||
|
<label className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
在 Dock 中显示(macOS)
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.showInDock}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({ ...settings, showInDock: e.target.checked })
|
||||||
|
}
|
||||||
|
className="w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* 配置文件位置 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
配置文件位置
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 px-3 py-2 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<span className="text-xs font-mono text-gray-500 dark:text-gray-400">
|
||||||
|
{configPath || "加载中..."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleOpenConfigFolder}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||||
|
title="打开文件夹"
|
||||||
|
>
|
||||||
|
<FolderOpen
|
||||||
|
size={18}
|
||||||
|
className="text-gray-500 dark:text-gray-400"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 关于 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
关于
|
||||||
|
</h3>
|
||||||
|
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Info
|
||||||
|
size={18}
|
||||||
|
className="text-gray-500 mt-0.5"
|
||||||
|
/>
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
CC Switch
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-gray-500 dark:text-gray-400">
|
||||||
|
版本 {version}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCheckUpdate}
|
||||||
|
disabled={isCheckingUpdate}
|
||||||
|
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-all ${
|
||||||
|
isCheckingUpdate
|
||||||
|
? "bg-white dark:bg-gray-700 text-gray-400 dark:text-gray-500"
|
||||||
|
: "bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 text-blue-500 dark:text-blue-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isCheckingUpdate ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<RefreshCw size={12} className="animate-spin" />
|
||||||
|
检查中...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
"检查更新"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部按钮 */}
|
||||||
|
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveSettings}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "deepseek-chat",
|
ANTHROPIC_MODEL: "deepseek-chat",
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "deepseek-chat",
|
ANTHROPIC_SMALL_FAST_MODEL: "deepseek-chat",
|
||||||
},
|
},
|
||||||
@@ -35,7 +35,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -46,7 +46,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL:
|
ANTHROPIC_BASE_URL:
|
||||||
"https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
|
"https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -56,19 +56,31 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "kimi-k2-turbo-preview",
|
ANTHROPIC_MODEL: "kimi-k2-turbo-preview",
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "kimi-k2-turbo-preview",
|
ANTHROPIC_SMALL_FAST_MODEL: "kimi-k2-turbo-preview",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "魔搭",
|
||||||
|
websiteUrl: "https://modelscope.cn",
|
||||||
|
settingsConfig: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_AUTH_TOKEN: "ms-your-api-key",
|
||||||
|
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
||||||
|
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.5",
|
||||||
|
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "PackyCode",
|
name: "PackyCode",
|
||||||
websiteUrl: "https://www.packycode.com",
|
websiteUrl: "https://www.packycode.com",
|
||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.packycode.com",
|
ANTHROPIC_BASE_URL: "https://api.packycode.com",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
80
src/hooks/useDarkMode.ts
Normal file
80
src/hooks/useDarkMode.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useDarkMode() {
|
||||||
|
// 初始设为 false,挂载后在 useEffect 中加载真实值
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
|
// 组件挂载后加载初始值(兼容 Tauri 环境)
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试读取已保存的偏好
|
||||||
|
const saved = localStorage.getItem('darkMode');
|
||||||
|
if (saved !== null) {
|
||||||
|
const savedBool = saved === 'true';
|
||||||
|
setIsDarkMode(savedBool);
|
||||||
|
console.log('[DarkMode] Loaded from localStorage:', savedBool);
|
||||||
|
} else {
|
||||||
|
// 回退到系统偏好
|
||||||
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
setIsDarkMode(prefersDark);
|
||||||
|
console.log('[DarkMode] Using system preference:', prefersDark);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DarkMode] Error loading preference:', error);
|
||||||
|
setIsDarkMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsInitialized(true);
|
||||||
|
}, []); // 仅在首次挂载时运行
|
||||||
|
|
||||||
|
// 将 dark 类应用到文档根节点
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInitialized) return;
|
||||||
|
|
||||||
|
// 添加短暂延迟以确保 Tauri 中 DOM 已就绪
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
if (isDarkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
console.log('[DarkMode] Added dark class to document');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
console.log('[DarkMode] Removed dark class from document');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查类名是否已成功应用
|
||||||
|
const hasClass = document.documentElement.classList.contains('dark');
|
||||||
|
console.log('[DarkMode] Document has dark class:', hasClass);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DarkMode] Error applying dark class:', error);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isDarkMode, isInitialized]);
|
||||||
|
|
||||||
|
// 将偏好保存到 localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInitialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('darkMode', isDarkMode.toString());
|
||||||
|
console.log('[DarkMode] Saved to localStorage:', isDarkMode);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DarkMode] Error saving preference:', error);
|
||||||
|
}
|
||||||
|
}, [isDarkMode, isInitialized]);
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
setIsDarkMode(prev => {
|
||||||
|
const newValue = !prev;
|
||||||
|
console.log('[DarkMode] Toggling from', prev, 'to', newValue);
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { isDarkMode, toggleDarkMode };
|
||||||
|
}
|
||||||
@@ -1,30 +1,46 @@
|
|||||||
|
/* 引入 Tailwind v4 内建样式与工具 */
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* 覆盖 Tailwind v4 默认的 dark 变体为“类选择器”模式 */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
/* 全局基础样式 */
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply font-sans antialiased;
|
||||||
|
line-height: 1.5;
|
||||||
|
/* 让原生控件与滚动条随主题切换配色 */
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family:
|
@apply m-0 p-0 bg-gray-50 text-gray-900 text-sm dark:bg-gray-950 dark:text-gray-100;
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
|
||||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
/* 暗色模式下启用暗色原生控件/滚动条配色 */
|
||||||
height: 100vh;
|
html.dark { color-scheme: dark; }
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
@apply w-1.5 h-1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 仅在 macOS 下为顶部预留交通灯空间 */
|
::-webkit-scrollbar-track {
|
||||||
/* 保持 mac 下与内容区域左对齐(不额外偏移) */
|
@apply bg-gray-100 dark:bg-gray-800;
|
||||||
|
}
|
||||||
/* 在 macOS 下稍微增加 banner 高度,拉开与交通灯的垂直距离 */
|
|
||||||
body.is-mac .app-header {
|
::-webkit-scrollbar-thumb {
|
||||||
padding-top: 1.4rem;
|
@apply bg-gray-300 rounded dark:bg-gray-600;
|
||||||
padding-bottom: 1.4rem;
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
@apply bg-gray-400 dark:bg-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 焦点样式 */
|
||||||
|
*:focus-visible {
|
||||||
|
@apply outline-2 outline-blue-500 outline-offset-2;
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/lib/styles.ts
Normal file
68
src/lib/styles.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 复用的 Tailwind 样式组合,覆盖常见 UI 模式
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 按钮样式
|
||||||
|
export const buttonStyles = {
|
||||||
|
// 主按钮:蓝底白字
|
||||||
|
primary: "px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium",
|
||||||
|
|
||||||
|
// 次按钮:灰背景,深色文本
|
||||||
|
secondary: "px-4 py-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium",
|
||||||
|
|
||||||
|
// 危险按钮:用于不可撤销/破坏性操作
|
||||||
|
danger: "px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700 transition-colors text-sm font-medium",
|
||||||
|
|
||||||
|
// 幽灵按钮:无背景,仅悬浮反馈
|
||||||
|
ghost: "px-4 py-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors text-sm font-medium",
|
||||||
|
|
||||||
|
// 图标按钮:小尺寸,仅图标
|
||||||
|
icon: "p-1.5 text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors",
|
||||||
|
|
||||||
|
// 禁用态:可与其他样式组合
|
||||||
|
disabled: "opacity-50 cursor-not-allowed pointer-events-none",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 卡片样式
|
||||||
|
export const cardStyles = {
|
||||||
|
// 基础卡片容器
|
||||||
|
base: "bg-white rounded-lg border border-gray-200 p-4 dark:bg-gray-900 dark:border-gray-700",
|
||||||
|
|
||||||
|
// 带悬浮效果的卡片
|
||||||
|
interactive: "bg-white rounded-lg border border-gray-200 p-4 hover:border-gray-300 hover:shadow-sm dark:bg-gray-900 dark:border-gray-700 dark:hover:border-gray-600 transition-all duration-200",
|
||||||
|
|
||||||
|
// 选中/激活态卡片
|
||||||
|
selected: "bg-white rounded-lg border border-blue-500 ring-1 ring-blue-500/20 bg-blue-500/5 p-4 dark:bg-gray-900 dark:border-blue-400 dark:ring-blue-400/20 dark:bg-blue-400/10",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 输入控件样式
|
||||||
|
export const inputStyles = {
|
||||||
|
// 文本输入框
|
||||||
|
text: "w-full px-3 py-2 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 outline-none dark:bg-gray-900 dark:border-gray-700 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-400/20 transition-colors",
|
||||||
|
|
||||||
|
// 下拉选择框
|
||||||
|
select: "w-full px-3 py-2 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 outline-none bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-400/20 transition-colors",
|
||||||
|
|
||||||
|
// 复选框
|
||||||
|
checkbox: "w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20 border-gray-300 dark:border-gray-600 dark:bg-gray-800",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 徽标(Badge)样式
|
||||||
|
export const badgeStyles = {
|
||||||
|
// 成功徽标
|
||||||
|
success: "inline-flex items-center gap-1 px-2 py-1 bg-green-500/10 text-green-500 rounded-md text-xs font-medium",
|
||||||
|
|
||||||
|
// 信息徽标
|
||||||
|
info: "inline-flex items-center gap-1 px-2 py-1 bg-blue-500/10 text-blue-500 rounded-md text-xs font-medium",
|
||||||
|
|
||||||
|
// 警告徽标
|
||||||
|
warning: "inline-flex items-center gap-1 px-2 py-1 bg-amber-500/10 text-amber-500 rounded-md text-xs font-medium",
|
||||||
|
|
||||||
|
// 错误徽标
|
||||||
|
error: "inline-flex items-center gap-1 px-2 py-1 bg-red-500/10 text-red-500 rounded-md text-xs font-medium",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 组合类名的工具函数
|
||||||
|
export function cn(...classes: (string | undefined | false)[]) {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { Provider } from "../types";
|
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
import { Provider, Settings } from "../types";
|
||||||
|
|
||||||
// 应用类型
|
// 应用类型
|
||||||
export type AppType = "claude" | "codex";
|
export type AppType = "claude" | "codex";
|
||||||
@@ -167,6 +168,25 @@ export const tauriAPI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 更新托盘菜单
|
||||||
|
updateTrayMenu: async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke("update_tray_menu");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("更新托盘菜单失败:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 监听供应商切换事件
|
||||||
|
onProviderSwitched: async (
|
||||||
|
callback: (data: { appType: string; providerId: string }) => void,
|
||||||
|
): Promise<UnlistenFn> => {
|
||||||
|
return await listen("provider-switched", (event) => {
|
||||||
|
callback(event.payload as { appType: string; providerId: string });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// (保留空位,取消迁移提示)
|
// (保留空位,取消迁移提示)
|
||||||
|
|
||||||
// 选择配置文件(Tauri 暂不实现,保留接口兼容性)
|
// 选择配置文件(Tauri 暂不实现,保留接口兼容性)
|
||||||
@@ -174,6 +194,54 @@ export const tauriAPI = {
|
|||||||
console.warn("selectConfigFile 在 Tauri 版本中暂不支持");
|
console.warn("selectConfigFile 在 Tauri 版本中暂不支持");
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取设置
|
||||||
|
getSettings: async (): Promise<Settings> => {
|
||||||
|
try {
|
||||||
|
return await invoke("get_settings");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取设置失败:", error);
|
||||||
|
return { showInDock: true };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
saveSettings: async (settings: Settings): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke("save_settings", { settings });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("保存设置失败:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
checkForUpdates: async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await invoke("check_for_updates");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("检查更新失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取应用配置文件路径
|
||||||
|
getAppConfigPath: async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
return await invoke("get_app_config_path");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取应用配置路径失败:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 打开应用配置文件夹
|
||||||
|
openAppConfigFolder: async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await invoke("open_app_config_folder");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("打开应用配置文件夹失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建全局 API 对象,兼容现有代码
|
// 创建全局 API 对象,兼容现有代码
|
||||||
|
|||||||
146
src/lib/updater.ts
Normal file
146
src/lib/updater.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
|
||||||
|
// 可选导入:在未注册插件或非 Tauri 环境下,调用时会抛错,外层需做兜底
|
||||||
|
// 我们按需加载并在运行时捕获错误,避免构建期类型问题
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
|
import type { Update } from "@tauri-apps/plugin-updater";
|
||||||
|
|
||||||
|
export type UpdateChannel = "stable" | "beta";
|
||||||
|
|
||||||
|
export type UpdaterPhase =
|
||||||
|
| "idle"
|
||||||
|
| "checking"
|
||||||
|
| "available"
|
||||||
|
| "downloading"
|
||||||
|
| "installing"
|
||||||
|
| "restarting"
|
||||||
|
| "upToDate"
|
||||||
|
| "error";
|
||||||
|
|
||||||
|
export interface UpdateInfo {
|
||||||
|
currentVersion: string;
|
||||||
|
availableVersion: string;
|
||||||
|
notes?: string;
|
||||||
|
pubDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProgressEvent {
|
||||||
|
event: "Started" | "Progress" | "Finished";
|
||||||
|
total?: number;
|
||||||
|
downloaded?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateHandle {
|
||||||
|
version: string;
|
||||||
|
notes?: string;
|
||||||
|
date?: string;
|
||||||
|
downloadAndInstall: (
|
||||||
|
onProgress?: (e: UpdateProgressEvent) => void,
|
||||||
|
) => Promise<void>;
|
||||||
|
download?: () => Promise<void>;
|
||||||
|
install?: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckOptions {
|
||||||
|
timeout?: number;
|
||||||
|
channel?: UpdateChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapUpdateHandle(raw: Update): UpdateHandle {
|
||||||
|
return {
|
||||||
|
version: (raw as any).version ?? "",
|
||||||
|
notes: (raw as any).notes,
|
||||||
|
date: (raw as any).date,
|
||||||
|
async downloadAndInstall(onProgress?: (e: UpdateProgressEvent) => void) {
|
||||||
|
await (raw as any).downloadAndInstall((evt: any) => {
|
||||||
|
if (!onProgress) return;
|
||||||
|
const mapped: UpdateProgressEvent = {
|
||||||
|
event: evt?.event,
|
||||||
|
};
|
||||||
|
if (evt?.event === "Started") {
|
||||||
|
mapped.total = evt?.data?.contentLength ?? 0;
|
||||||
|
mapped.downloaded = 0;
|
||||||
|
} else if (evt?.event === "Progress") {
|
||||||
|
mapped.downloaded = evt?.data?.chunkLength ?? 0; // 累积由调用方完成
|
||||||
|
}
|
||||||
|
onProgress(mapped);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 透传可选 API(若插件版本支持)
|
||||||
|
download: (raw as any).download
|
||||||
|
? async () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
await (raw as any).download();
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
install: (raw as any).install
|
||||||
|
? async () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
await (raw as any).install();
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentVersion(): Promise<string> {
|
||||||
|
try {
|
||||||
|
return await getVersion();
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkForUpdate(
|
||||||
|
opts: CheckOptions = {},
|
||||||
|
): Promise<
|
||||||
|
| { status: "up-to-date" }
|
||||||
|
| { status: "available"; info: UpdateInfo; update: UpdateHandle }
|
||||||
|
> {
|
||||||
|
// 动态引入,避免在未安装插件时导致打包期问题
|
||||||
|
const { check } = await import("@tauri-apps/plugin-updater");
|
||||||
|
|
||||||
|
const currentVersion = await getCurrentVersion();
|
||||||
|
const update = await check({ timeout: opts.timeout ?? 30000 } as any);
|
||||||
|
|
||||||
|
if (!update) {
|
||||||
|
return { status: "up-to-date" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = mapUpdateHandle(update);
|
||||||
|
const info: UpdateInfo = {
|
||||||
|
currentVersion,
|
||||||
|
availableVersion: mapped.version,
|
||||||
|
notes: mapped.notes,
|
||||||
|
pubDate: mapped.date,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { status: "available", info, update: mapped };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function relaunchApp(): Promise<void> {
|
||||||
|
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||||
|
await relaunch();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runUpdateFlow(
|
||||||
|
opts: CheckOptions = {},
|
||||||
|
): Promise<{ status: "up-to-date" | "done" }> {
|
||||||
|
const result = await checkForUpdate(opts);
|
||||||
|
if (result.status === "up-to-date") return result;
|
||||||
|
|
||||||
|
let downloaded = 0;
|
||||||
|
let total = 0;
|
||||||
|
await result.update.downloadAndInstall((e) => {
|
||||||
|
if (e.event === "Started") {
|
||||||
|
total = e.total ?? 0;
|
||||||
|
downloaded = 0;
|
||||||
|
} else if (e.event === "Progress") {
|
||||||
|
downloaded += e.downloaded ?? 0;
|
||||||
|
// 调用方可监听此处并更新 UI(目前设置页仅显示加载态)
|
||||||
|
console.debug("update progress", { downloaded, total });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await relaunchApp();
|
||||||
|
return { status: "done" };
|
||||||
|
}
|
||||||
@@ -3,9 +3,15 @@ export interface Provider {
|
|||||||
name: string;
|
name: string;
|
||||||
settingsConfig: Record<string, any>; // 应用配置对象:Claude 为 settings.json;Codex 为 { auth, config }
|
settingsConfig: Record<string, any>; // 应用配置对象:Claude 为 settings.json;Codex 为 { auth, config }
|
||||||
websiteUrl?: string;
|
websiteUrl?: string;
|
||||||
|
createdAt?: number; // 添加时间戳(毫秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
providers: Record<string, Provider>;
|
providers: Record<string, Provider>;
|
||||||
current: string;
|
current: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 应用设置类型(用于 SettingsModal 与 Tauri API)
|
||||||
|
export interface Settings {
|
||||||
|
showInDock: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,22 +33,6 @@ export const checkCoAuthoredSetting = (jsonString: string): boolean => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从JSON配置中提取并处理官网地址
|
|
||||||
export const extractWebsiteUrl = (jsonString: string): string => {
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(jsonString);
|
|
||||||
const baseUrl = config?.env?.ANTHROPIC_BASE_URL;
|
|
||||||
|
|
||||||
if (baseUrl && typeof baseUrl === "string") {
|
|
||||||
// 去掉 "api." 前缀
|
|
||||||
return baseUrl.replace(/^https?:\/\/api\./, "https://");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// 忽略JSON解析错误
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
// 读取配置中的 API Key(env.ANTHROPIC_AUTH_TOKEN)
|
// 读取配置中的 API Key(env.ANTHROPIC_AUTH_TOKEN)
|
||||||
export const getApiKeyFromConfig = (jsonString: string): string => {
|
export const getApiKeyFromConfig = (jsonString: string): string => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
12
src/vite-env.d.ts
vendored
12
src/vite-env.d.ts
vendored
@@ -1,7 +1,8 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
import { Provider } from "./types";
|
import { Provider, Settings } from "./types";
|
||||||
import { AppType } from "./lib/tauri-api";
|
import { AppType } from "./lib/tauri-api";
|
||||||
|
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
interface ImportResult {
|
interface ImportResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -30,6 +31,15 @@ declare global {
|
|||||||
selectConfigFile: () => Promise<string | null>;
|
selectConfigFile: () => Promise<string | null>;
|
||||||
openConfigFolder: (app?: AppType) => Promise<void>;
|
openConfigFolder: (app?: AppType) => Promise<void>;
|
||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
|
updateTrayMenu: () => Promise<boolean>;
|
||||||
|
onProviderSwitched: (
|
||||||
|
callback: (data: { appType: string; providerId: string }) => void,
|
||||||
|
) => Promise<UnlistenFn>;
|
||||||
|
getSettings: () => Promise<Settings>;
|
||||||
|
saveSettings: (settings: Settings) => Promise<boolean>;
|
||||||
|
checkForUpdates: () => Promise<void>;
|
||||||
|
getAppConfigPath: () => Promise<string>;
|
||||||
|
openAppConfigFolder: () => Promise<void>;
|
||||||
};
|
};
|
||||||
platform: {
|
platform: {
|
||||||
isMac: boolean;
|
isMac: boolean;
|
||||||
|
|||||||
62
tailwind.config.js
Normal file
62
tailwind.config.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./src/index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
// 扩展蓝色系列以匹配 Linear 风格
|
||||||
|
blue: {
|
||||||
|
500: '#3498db',
|
||||||
|
600: '#2980b9',
|
||||||
|
400: '#5dade2',
|
||||||
|
},
|
||||||
|
// 自定义灰色系列
|
||||||
|
gray: {
|
||||||
|
50: '#fafafa', // bg-primary
|
||||||
|
100: '#f4f4f5', // bg-tertiary
|
||||||
|
200: '#e4e4e7', // border
|
||||||
|
300: '#d4d4d8', // border-hover
|
||||||
|
400: '#a1a1aa', // text-tertiary
|
||||||
|
500: '#71717a', // text-secondary
|
||||||
|
600: '#52525b', // text-secondary-dark
|
||||||
|
700: '#3f3f46', // bg-tertiary-dark
|
||||||
|
800: '#27272a', // bg-secondary-dark
|
||||||
|
900: '#18181b', // text-primary
|
||||||
|
950: '#0a0a0b', // bg-primary-dark
|
||||||
|
},
|
||||||
|
// 状态颜色
|
||||||
|
green: {
|
||||||
|
500: '#10b981',
|
||||||
|
100: '#d1fae5',
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
500: '#ef4444',
|
||||||
|
100: '#fee2e2',
|
||||||
|
},
|
||||||
|
amber: {
|
||||||
|
500: '#f59e0b',
|
||||||
|
100: '#fef3c7',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'sm': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
||||||
|
'md': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||||
|
'lg': '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
'sm': '0.375rem',
|
||||||
|
'md': '0.5rem',
|
||||||
|
'lg': '0.75rem',
|
||||||
|
'xl': '0.875rem',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'],
|
||||||
|
mono: ['ui-monospace', 'SFMono-Regular', '"SF Mono"', 'Consolas', '"Liberation Mono"', 'Menlo', 'monospace'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"module": "CommonJS",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@@ -11,5 +11,5 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.mts"]
|
||||||
}
|
}
|
||||||
19
vite.config.mts
Normal file
19
vite.config.mts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: "src",
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
base: "./",
|
||||||
|
build: {
|
||||||
|
outDir: "../dist",
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
clearScreen: false,
|
||||||
|
envPrefix: ["VITE_", "TAURI_"],
|
||||||
|
});
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
root: 'src',
|
|
||||||
plugins: [react()],
|
|
||||||
base: './',
|
|
||||||
build: {
|
|
||||||
outDir: '../dist',
|
|
||||||
emptyOutDir: true
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 3000,
|
|
||||||
strictPort: true
|
|
||||||
},
|
|
||||||
clearScreen: false,
|
|
||||||
envPrefix: ['VITE_', 'TAURI_']
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user