Compare commits
18
Commits
v0.6.2
...
b9ea6426d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9ea6426d8 | ||
|
|
0889eeba24 | ||
|
|
f35924e15c | ||
|
|
19f58c3a2b | ||
|
|
8f0729ddfc | ||
|
|
0dbe2ba88c | ||
|
|
6a8b09ef35 | ||
|
|
4adf5cafbe | ||
|
|
90ab9f8a97 | ||
|
|
06aad52e0f | ||
|
|
548cdabe99 | ||
|
|
7b41518d6e | ||
|
|
63ae664e72 | ||
|
|
00322fb503 | ||
|
|
084883a54a | ||
|
|
fb530c85ca | ||
|
|
919b9e49f5 | ||
|
|
549c8b5b13 |
No files matched your search
@@ -0,0 +1,87 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
name: Version consistency
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Verify tag matches package.json / Cargo.toml / tauri.conf.json
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
chmod +x scripts/check-version.sh
|
||||
./scripts/check-version.sh "$TAG"
|
||||
|
||||
build:
|
||||
name: Build (${{ matrix.platform }})
|
||||
needs: version-check
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: ubuntu-latest
|
||||
args: --bundles deb,appimage,rpm
|
||||
- platform: macos-latest
|
||||
args: --bundles app,dmg
|
||||
- platform: windows-latest
|
||||
args: --bundles msi
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- uses: actions/setup-rust@v1
|
||||
with:
|
||||
cache: true
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev
|
||||
|
||||
- name: Build and upload release
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: ${{ github.ref_name }}
|
||||
releaseBody: |
|
||||
## ${{ github.ref_name }}
|
||||
|
||||
### Downloads by platform
|
||||
|
||||
- **Windows**: `.msi` installer
|
||||
- **macOS**: `.dmg` (unsigned — first launch needs right-click → Open) + `.app`
|
||||
- **Linux**: `.deb` / `.AppImage` / `.rpm`
|
||||
|
||||
Full changelog: see commit history.
|
||||
releaseDraft: true
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }}
|
||||
args: ${{ matrix.args }}
|
||||
@@ -13,3 +13,4 @@ Snipaste_*.png
|
||||
|
||||
# 内部设计规范与实施计划,仅供本地查阅,不入版本库
|
||||
docs/superpowers/
|
||||
.release*
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Build Kimi Switch Linux packages (deb / AppImage / rpm) inside a clean
|
||||
# Ubuntu 22.04 container. Used both locally (`docker compose run`) and as a
|
||||
# reference for GH Actions' ubuntu-latest job.
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV CARGO_TERM_COLOR=always
|
||||
ENV RUSTUP_HOME=/usr/local/rustup
|
||||
ENV CARGO_HOME=/usr/local/cargo
|
||||
ENV PATH=/usr/local/cargo/bin:$PATH
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libsoup-3.0-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
patchelf \
|
||||
file \
|
||||
nodejs \
|
||||
npm \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||
|
||||
RUN cargo install tauri-cli --version "^2.0" --locked
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -8,8 +8,9 @@
|
||||
[](https://react.dev)
|
||||
[](https://www.typescriptlang.org)
|
||||
[](https://www.rust-lang.org)
|
||||
[](https://git.codingplan.site/admin/KimiCodeSwitch/releases)
|
||||
[](https://github.com/billowliu2/KimiSwitch/releases/tag/v0.6.0)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/billowliu2/KimiSwitch/releases/latest)
|
||||
|
||||
---
|
||||
|
||||
@@ -51,6 +52,16 @@
|
||||
- **主题/语言切换**:设置面板中切换深色/浅色/跟随系统,简体中文/English
|
||||
- **Windows 友好**:单实例;最小化保留任务栏按钮;关闭按钮隐藏到托盘
|
||||
|
||||
## 下载
|
||||
|
||||
- **Windows**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.msi`
|
||||
- **macOS**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.dmg`(**未签名** —— 首次启动请右键 → 打开绕过 Gatekeeper)
|
||||
- **Linux**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.deb` / `.AppImage` / `.rpm`
|
||||
|
||||
历史版本(v0.5.x 及之前)请前往 [git.codingplan.site 仓库](https://git.codingplan.site/admin/KimiCodeSwitch/releases)。
|
||||
|
||||
构建说明请见 [`docs/BUILD.md`](./docs/BUILD.md)。
|
||||
|
||||
## 核心特性
|
||||
|
||||
| 分类 | 功能 |
|
||||
@@ -498,38 +509,21 @@ Windows 安装包(MSI),含 WebView2 bootstrapper 自动下载。`nsis` 已
|
||||
|
||||
## 发布历史
|
||||
|
||||
### v0.5.1(最新)
|
||||
### v0.6.0(最新)
|
||||
|
||||
- 窗口/托盘:最小化保留任务栏;关闭 X 隐藏到托盘;托盘左键始终显示并聚焦
|
||||
- 模型用量表:模型列宽度自适应,长模型名完整显示不再截断
|
||||
- 模型能力编辑:只保留"思考",其余能力仍由 models.dev 自动推导
|
||||
- 模型映射表格:行分隔线改为主题感知,修复浅色模式突兀问题
|
||||
- **15 条主流供应商预设**:Kimi Coding / Moonshot / Anthropic / DeepSeek / 智谱 GLM / z.ai / 阿里百炼 / MiniMax / StepFun / SiliconFlow / Novita / OpenRouter / OpenAI / Google AI Studio / 火山方舟,通过 `PresetPickerModal` 一键填表
|
||||
- **供应商余额 / 套餐查询**(卡片底部 `UsageFooter`):余额类(DeepSeek / SiliconFlow / OpenRouter / StepFun / Novita)+ 套餐类(Kimi For Coding / 智谱 GLM / MiniMax),5min stale TTL + `force_refresh` + 并发 ≤3
|
||||
- **Rust 端 `services/` 模块**(`balance` / `coding_plan` / `usage_types`),参考 cc-switch(MIT,© Jason Young)实现
|
||||
- `query_provider_usage` 命令:Rust 端从 config 加载 key,不经 IPC 序列化 API key
|
||||
- `usageKinds` 持久化到 SQLite settings(JSON 数组),load 时合并回 config,导出 `config.toml` 时不写
|
||||
- `detect_provider` 启发式:旧用户升级后自动获得账单支持
|
||||
- save-time 校验:未完成供应商(缺 api_key / base_url / 模型)保存时弹 confirm 列出原因;新增未提交 back 时静默 drop
|
||||
- 修复:`handleDuplicateProvider` 的 `alias.slice` 漏洞;`handleSelectPreset` 不再 auto-save;`handleSwitchProvider` 校验取消时自动 refresh 回滚
|
||||
- 新增文档:[`docs/PROPOSAL-presets-and-usage.md`](./docs/PROPOSAL-presets-and-usage.md)(v0.4-draft 实施档)、[`docs/VERIFICATION-CHECKLIST.md`](./docs/VERIFICATION-CHECKLIST.md)(70+ 项手动验证清单)
|
||||
|
||||
### v0.5.0
|
||||
### 历史版本
|
||||
|
||||
- 供应商操作图标化(lucide-react):启用 / 编辑 / 复制 / 测试连通 / 删除
|
||||
- 重复供应商(深拷贝供应商 + 全部模型)
|
||||
- 连通性测试(绿/橙/红气泡 + 6 秒自动消失)
|
||||
- 每日趋势柱状图锚定到面板底部
|
||||
|
||||
### v0.4.1
|
||||
|
||||
- 模型发现分页(OpenAI / Anthropic / Google),减少大列表命中限制
|
||||
|
||||
### v0.4.0
|
||||
|
||||
- 完善的图标系统(移植自 cc-switch 图标库)
|
||||
- 设置面板(主题、语言)
|
||||
- 启动版本检查
|
||||
|
||||
### v0.3.0
|
||||
|
||||
[移植自 kimicode-dashboard](https://github.com/JochenYang/kimicode-dashboard):
|
||||
|
||||
- 用量仪表盘:8 KPI、每日趋势、全年热力图、最近请求翻页
|
||||
- 会话管理:按工作区浏览、预览、归档、批量删除
|
||||
- 时区修复:`today` / 热力图 / `day_key` 改用本地日历日
|
||||
- Pi 选项隐藏
|
||||
v0.5.x 及之前版本(含 v0.5.1 / v0.5.0 / v0.4.1 / v0.4.0 / v0.3.0)请前往 [git.codingplan.site 仓库](https://git.codingplan.site/admin/KimiCodeSwitch/releases) 查看。GitHub 仓库仅作为 v0.6.0 及以后版本的发布渠道。
|
||||
|
||||
## 常见问题
|
||||
|
||||
|
||||
+27
-30
@@ -8,8 +8,9 @@
|
||||
[](https://react.dev)
|
||||
[](https://www.typescriptlang.org)
|
||||
[](https://www.rust-lang.org)
|
||||
[](https://git.codingplan.site/admin/KimiCodeSwitch/releases)
|
||||
[](https://github.com/billowliu2/KimiSwitch/releases/tag/v0.6.0)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/billowliu2/KimiSwitch/releases/latest)
|
||||
|
||||
---
|
||||
|
||||
@@ -51,6 +52,16 @@
|
||||
- **Theme / language** — dark / light / follow-system themes; Simplified Chinese / English
|
||||
- **Windows polish** — single instance; minimize keeps the taskbar button; close hides to tray
|
||||
|
||||
## Downloads
|
||||
|
||||
- **Windows**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.msi`
|
||||
- **macOS**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.dmg` (**unsigned** — first launch needs right-click → Open to bypass Gatekeeper)
|
||||
- **Linux**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.deb` / `.AppImage` / `.rpm`
|
||||
|
||||
Historical versions (v0.5.x and earlier) live on the [git.codingplan.site repository](https://git.codingplan.site/admin/KimiCodeSwitch/releases).
|
||||
|
||||
Build instructions: [`docs/BUILD.md`](./docs/BUILD.md).
|
||||
|
||||
## Key Features
|
||||
|
||||
| Category | Feature |
|
||||
@@ -494,38 +505,24 @@ The first run downloads:
|
||||
|
||||
## Release History
|
||||
|
||||
### v0.5.1 (latest)
|
||||
### v0.6.0 (latest)
|
||||
|
||||
- Window / tray: minimize keeps the taskbar button; close X hides to tray; tray left-click always shows + focuses
|
||||
- Usage table: model column auto-width, long names no longer truncated
|
||||
- Capability editor: only `thinking` exposed; other capabilities still auto-derived from models.dev
|
||||
- Model mapping table: theme-aware row divider, no more harsh line in light mode
|
||||
- **15 mainstream provider presets**: Kimi Coding / Moonshot / Anthropic / DeepSeek / Zhipu GLM / z.ai / Alibaba Bailian / MiniMax / StepFun / SiliconFlow / Novita / OpenRouter / OpenAI / Google AI Studio / Volcengine Ark — one-click form fill via `PresetPickerModal`
|
||||
- **Provider balance / plan usage query** (per-card `UsageFooter`):
|
||||
- Balance: DeepSeek / SiliconFlow / OpenRouter / StepFun / Novita
|
||||
- Plan: Kimi For Coding / Zhipu GLM / MiniMax
|
||||
- 5-min stale TTL + `force_refresh` + concurrency ≤ 3
|
||||
- **Rust `services/` module** (`balance` / `coding_plan` / `usage_types`), adapted from cc-switch (MIT, © Jason Young)
|
||||
- `query_provider_usage` command: loads API key from config in Rust, never serialized over IPC
|
||||
- `usageKinds` persisted in SQLite `settings` (JSON array), merged back into config on load, omitted from `config.toml` export
|
||||
- `detect_provider` heuristic: existing users automatically gain balance/plan support after upgrade
|
||||
- Save-time validation: incomplete providers (missing `api_key` / `base_url` / models) trigger a confirm dialog listing the reasons; uncommitted `add` is silently dropped on back
|
||||
- Fixes: `handleDuplicateProvider` `alias.slice` bug; `handleSelectPreset` no longer auto-saves; `handleSwitchProvider` auto-refresh rollback when validation is cancelled
|
||||
- New docs: [`docs/PROPOSAL-presets-and-usage.md`](./docs/PROPOSAL-presets-and-usage.md) (v0.4-draft implementation), [`docs/VERIFICATION-CHECKLIST.md`](./docs/VERIFICATION-CHECKLIST.md) (70+ manual checks)
|
||||
|
||||
### v0.5.0
|
||||
### Historical versions
|
||||
|
||||
- Iconified actions (lucide-react): activate / edit / duplicate / test / delete
|
||||
- Duplicate provider (deep-copy a provider + all its models)
|
||||
- Connectivity test (green / orange / red bubble + 6s auto-dismiss)
|
||||
- Daily trend bars anchored to the panel bottom
|
||||
|
||||
### v0.4.1
|
||||
|
||||
- Model discovery pagination (OpenAI / Anthropic / Google), fewer large-list limits hit
|
||||
|
||||
### v0.4.0
|
||||
|
||||
- Polished icon system (library adapted from cc-switch)
|
||||
- Settings modal (theme / language)
|
||||
- Launch-time update check
|
||||
|
||||
### v0.3.0
|
||||
|
||||
Ported from [kimicode-dashboard](https://github.com/JochenYang/kimicode-dashboard):
|
||||
|
||||
- Dashboard: 8 KPIs, daily trend, full-year heatmap, paginated recent requests
|
||||
- Session manager: per-workspace browsing, preview, archive, bulk delete
|
||||
- Timezone fix: `today` / heatmap / `day_key` now use local calendar day
|
||||
- Pi option hidden from the UI
|
||||
Versions v0.5.x and earlier (v0.5.1 / v0.5.0 / v0.4.1 / v0.4.0 / v0.3.0) live on the [git.codingplan.site repository](https://git.codingplan.site/admin/KimiCodeSwitch/releases). The GitHub repo is the release channel from v0.6.0 onward.
|
||||
|
||||
## FAQ
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
13
|
||||
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
build-linux:
|
||||
build: .
|
||||
container_name: kimiswitch-build-linux
|
||||
volumes:
|
||||
- .:/workspace
|
||||
- cargo-cache:/usr/local/cargo/registry
|
||||
- target-cache:/workspace/src-tauri/target
|
||||
working_dir: /workspace
|
||||
|
||||
volumes:
|
||||
cargo-cache:
|
||||
target-cache:
|
||||
@@ -0,0 +1,86 @@
|
||||
# Build
|
||||
|
||||
Docker (Linux), native (any platform), and CI (multi-platform) build instructions.
|
||||
|
||||
## Local builds by host
|
||||
|
||||
### Windows (current primary dev environment)
|
||||
|
||||
Prerequisites: Rust stable, Node 20, Microsoft C++ Build Tools, WebView2.
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run tauri build -- --bundles msi
|
||||
```
|
||||
|
||||
Output: `src-tauri/target/release/bundle/msi/`.
|
||||
|
||||
### macOS
|
||||
|
||||
Prerequisites: Xcode command-line tools, Rust stable, Node 20.
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run tauri build -- --bundles app,dmg
|
||||
```
|
||||
|
||||
Output: `src-tauri/target/release/bundle/{macos,dmg}/`.
|
||||
|
||||
Unsigned: first launch requires right-click → Open to bypass Gatekeeper.
|
||||
|
||||
### Linux (native)
|
||||
|
||||
Prerequisites: see `Dockerfile` for the full apt-get list (webkit2gtk-4.1, librsvg2, etc.).
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run tauri build -- --bundles deb,appimage,rpm
|
||||
```
|
||||
|
||||
Output: `src-tauri/target/release/bundle/{deb,appimage,rpm}/`.
|
||||
|
||||
### Linux via Docker (works on Windows / macOS too)
|
||||
|
||||
```bash
|
||||
docker compose build build-linux
|
||||
docker compose run --rm build-linux bash -c '
|
||||
npm ci &&
|
||||
npm run tauri build -- --bundles deb,appimage,rpm
|
||||
'
|
||||
```
|
||||
|
||||
Output is mounted back to `src-tauri/target/release/bundle/` on the host.
|
||||
|
||||
## CI: GitHub Actions
|
||||
|
||||
`.github/workflows/release.yml` runs on `git tag v* && git push origin v*`.
|
||||
|
||||
1. `version-check` runs `scripts/check-version.sh` to ensure tag matches `package.json`, `Cargo.toml`, `tauri.conf.json`.
|
||||
2. Three parallel jobs build for `ubuntu-latest`, `macos-latest`, `windows-latest` using `tauri-action`.
|
||||
3. Artifacts are uploaded to the GitHub Release (auto-draft for non-prerelease tags).
|
||||
|
||||
To cut a release:
|
||||
|
||||
```bash
|
||||
# 1. Bump version in three places
|
||||
# package.json: "version": "0.7.0"
|
||||
# src-tauri/Cargo.toml: version = "0.7.0"
|
||||
# src-tauri/tauri.conf.json: "version": "0.7.0"
|
||||
# 2. Commit
|
||||
git commit -am "chore(release): bump to v0.7.0"
|
||||
# 3. Tag and push
|
||||
git tag v0.7.0
|
||||
git push origin v0.7.0
|
||||
```
|
||||
|
||||
## Update checker asset selection
|
||||
|
||||
`src-tauri/src/commands.rs::pick_asset_for_current_os` picks the right asset by file extension:
|
||||
|
||||
| OS | Extension |
|
||||
|---------|------------|
|
||||
| Windows | `.msi` |
|
||||
| macOS | `.dmg` |
|
||||
| Linux | `.AppImage` (preferred; deb / rpm downloadable manually from the release page) |
|
||||
|
||||
If the preferred extension is missing, the UI falls back to opening the release page.
|
||||
@@ -0,0 +1,144 @@
|
||||
# 用户自定义预设方案(User Presets)
|
||||
|
||||
## 动机
|
||||
|
||||
当前内置预设(`providerPresets.ts`)硬编码在源码中,用户无法自行添加自定义供应商预设或修改已有预设的 `apiKeyUrl`(获取 Key / 推广链接)。每次新增或修改都需要修改源码、重新编译、重新打包,缺乏灵活性。
|
||||
|
||||
## 目标
|
||||
|
||||
用户可通过外部配置文件**零代码**添加/修改预设供应商,包含:
|
||||
- 自定义供应商预设(名称、Base URL、API 格式、图标、套餐类型)
|
||||
- 自定义获取 API Key 入口 / 推广链接(`apiKeyUrl`,可直接放带 ref 的链接)
|
||||
- 自定义模型列表(模型 ID、显示名称、上下文大小、能力)
|
||||
- 覆盖内置预设(同一 `id` 时用户预设优先)
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 1. 配置文件位置
|
||||
|
||||
```
|
||||
Windows: %APPDATA%/KimiSwitch/user-presets.json
|
||||
macOS: ~/Library/Application Support/KimiSwitch/user-presets.json
|
||||
Linux: ~/.config/KimiSwitch/user-presets.json
|
||||
```
|
||||
|
||||
统一使用应用数据目录(与 `app_config_dir` 一致),不扫描项目根目录——发行版是编译后的二进制,终端用户没有"项目根目录"。
|
||||
|
||||
首次启动时若文件不存在,自动创建一个空 `[]` 模板文件,方便用户直接编辑。
|
||||
PresetPickerModal / 设置页提供一个"打开预设文件夹"按钮,让用户能找到文件位置。
|
||||
|
||||
### 2. 文件格式
|
||||
|
||||
> 支持 `//` 行注释(jsonc 风格):加载时先剥离 `//` 注释再 `JSON.parse`,方便用户写备注。
|
||||
|
||||
```jsonc
|
||||
[
|
||||
{
|
||||
// 唯一标识,与内置预设 id 重名则覆盖内置预设
|
||||
"id": "my-custom-vendor",
|
||||
// 显示名称(fallback 英文)
|
||||
"name": "My Custom Vendor",
|
||||
// i18n key(可选,有则优先用 t(nameKey))
|
||||
"nameKey": "presetNameMyCustom",
|
||||
// 官网链接
|
||||
"websiteUrl": "https://example.com",
|
||||
// 获取 API Key 入口 / 推广链接(可直接放带 ref 的链接,如 ?ref=CODE)
|
||||
"apiKeyUrl": "https://example.com?ref=MYCODE",
|
||||
// 分类:official | cn_official | third_party | aggregator | custom
|
||||
"category": "third_party",
|
||||
// API 格式:openai | anthropic | google-genai | vertexai | kimi | openai_responses
|
||||
"providerType": "openai",
|
||||
// Base URL(null 表示使用默认)
|
||||
"baseUrl": "https://api.example.com/v1",
|
||||
// 图标 key(从 src/icons/extracted 中选,或空字符串用首字母)
|
||||
"icon": "",
|
||||
// 图标颜色(可选,hex 格式)
|
||||
"iconColor": "",
|
||||
// 计费模式:subscription | pay_as_you_go
|
||||
"billingMode": "pay_as_you_go",
|
||||
// 用量查询类型(可选,参考内置预设)
|
||||
"usageKinds": [],
|
||||
// 模型列表
|
||||
"models": [
|
||||
{
|
||||
"model": "my-model-1",
|
||||
"displayName": "My Model 1",
|
||||
"maxContextSize": 128000,
|
||||
"capabilities": ["thinking"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 3. 加载逻辑(伪代码)
|
||||
|
||||
```
|
||||
function loadAllPresets(): ProviderPreset[] {
|
||||
const builtin = providerPresets; // 内置预设
|
||||
const user = readUserPresetsFile(); // 读取用户预设文件
|
||||
if (!user || user.length === 0) return builtin;
|
||||
|
||||
const merged = new Map<string, ProviderPreset>();
|
||||
for (const p of builtin) merged.set(p.id, p); // 内置先进
|
||||
for (const p of user) merged.set(p.id, p); // 用户覆盖
|
||||
return Array.from(merged.values());
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 需要修改的代码文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `src/config/providerPresets.ts` | 导出 `loadAllPresets()` 函数替代直接使用 `providerPresets` 数组 |
|
||||
| `src/config/userPresets.ts`(新建) | 实现 `readUserPresetsFile()`:通过 Tauri 命令(Rust 侧 `read_text_file`)读取 + 剥离注释 + `JSON.parse` + 校验结构。用 Rust 命令而非前端 FS 插件,避免 scope 配置且跨平台路径由 Rust `app_config_dir` 统一解析 |
|
||||
| `src/components/PresetPickerModal.tsx` | 将 `providerPresets` 引用改为 `loadAllPresets()` |
|
||||
| `src/config/providerPresets.ts`(`findPresetForProvider`) | 同样需要从合并后的预设中查找 |
|
||||
|
||||
### 5. 校验逻辑
|
||||
|
||||
用户 JSON 文件可能写错,需要做基础校验:
|
||||
|
||||
```typescript
|
||||
interface UserPresetValidation {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
presets: ProviderPreset[];
|
||||
}
|
||||
|
||||
function validateUserPreset(raw: unknown): UserPresetValidation {
|
||||
// 1. 必须是数组
|
||||
// 2. 每项必须有 id、name、providerType、billingMode
|
||||
// 3. providerType 必须是合法值之一(openai | openai_responses | anthropic | google-genai | vertexai | kimi)
|
||||
// 4. billingMode 必须是 "subscription" | "pay_as_you_go"
|
||||
// 5. models 数组每项必须有 model 字段
|
||||
// 6. usageKinds 若提供,每项必须在 SUPPORTED_USAGE_KINDS 内,否则丢弃并警告
|
||||
// (Rust 侧运行时也会静默丢弃未知 kind,这里提前拦截给用户明确反馈)
|
||||
// 7. 忽略未知字段不报错(仅警告)
|
||||
// 8. 单条严重错误时跳过该条,不阻塞其他预设
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 用户交互
|
||||
|
||||
- PresetPickerModal 中用户预设顶部显示,与内置预设分栏展示(或加"自定义"标签)
|
||||
- 若 JSON 文件解析失败,界面上显示一个黄色警告条 + 错误详情
|
||||
- 提供一个"刷新用户预设"按钮,不必重启应用
|
||||
|
||||
### 7. 安全问题
|
||||
|
||||
- 不读取系统敏感路径,仅限于应用数据目录
|
||||
- 不执行 JSON 中的任何代码;`JSON.parse` 产出的是纯对象,无原型污染风险,校验只需关注字段类型与取值
|
||||
- 对 URL 字段(websiteUrl / apiKeyUrl)做基本 `http(s)://` 协议校验,`openUrl` 调用前过滤非法 scheme
|
||||
|
||||
### 8. 向后兼容
|
||||
|
||||
- 没有 `user-presets.json` 时行为完全不变
|
||||
- 用户预设只影响 PresetPickerModal 中的选项,不影响已保存的 Provider
|
||||
- 已保存的 Provider 不受用户预设删除影响
|
||||
|
||||
## 未解决的问题
|
||||
|
||||
1. **图标来源**:用户用内置图标集不存在的 key(含空字符串/null)时,统一降级为首字母显示。是否支持用户自定义 SVG 图标?—— 当前首字母降级足够,先不做。
|
||||
2. **热加载**:文件修改后是否需要自动检测并刷新?—— 初期手动刷新按钮即可,后续可加 `fs::watch`。
|
||||
3. **i18n**:用户预设的 `nameKey` 如果对应内置 i18n key 则显示翻译,否则 fallback 到 `name`。用户不能添加自定义 i18n key(涉及翻译文件修改,违背零代码原则)。
|
||||
@@ -0,0 +1,867 @@
|
||||
# Kimi Switch Multi-Platform Packaging Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add macOS + Linux packaging support on top of existing Windows MSI; release driven by GitHub Actions tag-push workflow.
|
||||
|
||||
**Architecture:** Source-level changes are minimal (Tauri config tweaks + platform-conditional Rust + asset picker). Build infrastructure is the heavy lifting: one GH Actions workflow (version-check + 3-platform matrix using `tauri-action`) and a local Docker setup for Linux dev iteration.
|
||||
|
||||
**Tech Stack:** Tauri 2.x, Rust 2021, React 18 + TypeScript 5.5, GitHub Actions (`tauri-apps/tauri-action`), Docker (Ubuntu 22.04 + webkit2gtk-4.1), bash.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- macOS assets: `.app` + `.dmg`, **unsigned** (no Apple Developer ID provided)
|
||||
- Linux assets: `.deb` + `.AppImage` + `.rpm` (all three)
|
||||
- Windows assets: `.msi` (existing)
|
||||
- Bundle target string in `tauri.conf.json`: `"all"` (allow per-platform build)
|
||||
- macOS minimum system version: `10.15`
|
||||
- Update checker picks asset by file extension: `.msi` / `.dmg` / `.AppImage`
|
||||
- Temp filename for downloaded installer: `KimiSwitch_update.<ext>` (ext from OS)
|
||||
- Version consistency: `git tag` must equal `package.json:version` = `Cargo.toml:version` = `tauri.conf.json:version`
|
||||
- Cargo behavior: keep `tauri = "2.0.0"`, `tauri-plugin-opener = "2.0.0"`, `tauri-plugin-single-instance = "2.0.0"`
|
||||
- No new lint failures; no new dependencies beyond what's already in `Cargo.toml`
|
||||
- Spec location: `docs/superpowers/specs/2026-07-29-multi-platform-packaging-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Tauri config — bundle targets and macOS section
|
||||
|
||||
**Files:**
|
||||
- Modify: `src-tauri/tauri.conf.json`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: existing Windows-only config
|
||||
- Produces: `bundle.targets = "all"`, `bundle.macOS.minimumSystemVersion`, expanded icon list
|
||||
|
||||
- [ ] **Step 1: Update `tauri.conf.json`**
|
||||
|
||||
Replace the existing `bundle` block with:
|
||||
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.ico",
|
||||
"icons/icon.icns"
|
||||
],
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"type": "downloadBootstrapper"
|
||||
},
|
||||
"nsis": null
|
||||
},
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "10.15"
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"appimage": {
|
||||
"bundleMediaFramework": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify JSON is valid**
|
||||
|
||||
Run: `node -e "JSON.parse(require('fs').readFileSync('src-tauri/tauri.conf.json','utf8'))"`
|
||||
Expected: no output (silent success)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src-tauri/tauri.conf.json
|
||||
git commit -m "feat(bundle): enable all-platform targets + macOS/Linux config"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Platform-conditional `windows_subsystem` in main.rs
|
||||
|
||||
**Files:**
|
||||
- Modify: `src-tauri/src/main.rs:1`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: existing `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]`
|
||||
- Produces: Windows-only windows_subsystem; macOS/Linux keep console subsystem so logs are visible
|
||||
|
||||
- [ ] **Step 1: Replace the directive at line 1**
|
||||
|
||||
Replace:
|
||||
```rust
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
```
|
||||
|
||||
With:
|
||||
```rust
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify Windows build still compiles**
|
||||
|
||||
Run: `cd src-tauri && cargo check --message-format=short 2>&1 | tail -5`
|
||||
Expected: `Finished \`dev\` profile [unoptimized + debuginfo] target(s)` with no errors
|
||||
|
||||
- [ ] **Step 3: Verify Linux target compiles (cross-check)**
|
||||
|
||||
Run: `cd src-tauri && cargo check --target x86_64-unknown-linux-gnu --message-format=short 2>&1 | tail -5`
|
||||
Expected: finishes clean (may need `rustup target add x86_64-unknown-linux-gnu` first; if rustup target is missing, run that and retry)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src-tauri/src/main.rs
|
||||
git commit -m "fix(build): only set windows_subsystem on Windows targets"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: TDD — platform-aware asset picker
|
||||
|
||||
**Files:**
|
||||
- Modify: `src-tauri/src/commands.rs` (append at end)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces (public to module): `fn pick_asset_for_current_os(assets: &[serde_json::Value]) -> Option<String>`
|
||||
- Pure function, no side effects; testable in isolation
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
Add to the end of `src-tauri/src/commands.rs`:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod asset_picker_tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn assets(names: &[&str]) -> Vec<serde_json::Value> {
|
||||
names
|
||||
.iter()
|
||||
.map(|n| {
|
||||
json!({
|
||||
"name": n,
|
||||
"browser_download_url": format!("https://example.com/{n}"),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_picks_msi() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||
let url = pick_asset_for_current_os(&assets);
|
||||
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_x64_en-US.msi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_picks_dmg() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||
let url = pick_asset_for_current_os(&assets);
|
||||
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_aarch64.dmg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_picks_appimage() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||
let url = pick_asset_for_current_os(&assets);
|
||||
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_amd64.AppImage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_target_returns_none() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi"]);
|
||||
let url = pick_asset_for_current_os(&assets);
|
||||
assert_eq!(url, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_assets_returns_none() {
|
||||
let url = pick_asset_for_current_os(&[]);
|
||||
assert_eq!(url, None);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd src-tauri && cargo test asset_picker_tests --message-format=short 2>&1 | tail -10`
|
||||
Expected: compile error `function \`pick_asset_for_current_os\` not found`
|
||||
|
||||
- [ ] **Step 3: Implement the function**
|
||||
|
||||
Add to `src-tauri/src/commands.rs` (above the test module):
|
||||
|
||||
```rust
|
||||
/// Pick the GitHub release asset matching the current OS by file extension.
|
||||
/// Preference for Linux is AppImage (portable, no install). If the preferred
|
||||
/// extension is not present, returns None so the UI can fall back to
|
||||
/// "open release page" instead of guessing a less-preferred format.
|
||||
fn pick_asset_for_current_os(assets: &[serde_json::Value]) -> Option<String> {
|
||||
let target_ext = match std::env::consts::OS {
|
||||
"macos" => "dmg",
|
||||
"linux" => "AppImage",
|
||||
"windows" => "msi",
|
||||
_ => return None,
|
||||
};
|
||||
assets
|
||||
.iter()
|
||||
.find(|a| {
|
||||
a.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|n| n.ends_with(target_ext))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.and_then(|a| a.get("browser_download_url"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// Build the platform-specific temp filename for the downloaded installer.
|
||||
fn update_temp_filename() -> String {
|
||||
let ext = match std::env::consts::OS {
|
||||
"macos" => "dmg",
|
||||
"linux" => "AppImage",
|
||||
"windows" => "msi",
|
||||
_ => "bin",
|
||||
};
|
||||
format!("KimiSwitch_update.{ext}")
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd src-tauri && cargo test asset_picker_tests --message-format=short 2>&1 | tail -10`
|
||||
Expected: `test result: ok. 5 passed; 0 failed`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src-tauri/src/commands.rs
|
||||
git commit -m "feat(update): TDD platform-aware asset picker + tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Wire asset picker into `check_for_update` and `download_update`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src-tauri/src/commands.rs` (the `check_for_update` and `download_update` functions)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `pick_asset_for_current_os` (Task 3) and `update_temp_filename` (Task 3)
|
||||
- Produces: `check_for_update` returns `download_url` for the current OS only; `download_update` writes to a platform-specific filename
|
||||
|
||||
- [ ] **Step 1: Replace the asset-selection block in `check_for_update`**
|
||||
|
||||
In `check_for_update`, find the existing block:
|
||||
|
||||
```rust
|
||||
let download_url = first
|
||||
.get("assets")
|
||||
.and_then(|a| a.as_array())
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|a| a.get("browser_download_url"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```rust
|
||||
let download_url = first
|
||||
.get("assets")
|
||||
.and_then(|a| a.as_array())
|
||||
.and_then(|a| pick_asset_for_current_os(a));
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the temp filename in `download_update`**
|
||||
|
||||
In `download_update`, find the existing line:
|
||||
|
||||
```rust
|
||||
let file_path = temp_dir.join("KimiSwitch_update.msi");
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```rust
|
||||
let file_path = temp_dir.join(update_temp_filename());
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify on Windows**
|
||||
|
||||
Run: `cd src-tauri && cargo check --message-format=short 2>&1 | tail -5`
|
||||
Expected: finishes clean
|
||||
|
||||
- [ ] **Step 4: Run the unit tests**
|
||||
|
||||
Run: `cd src-tauri && cargo test asset_picker_tests --message-format=short 2>&1 | tail -5`
|
||||
Expected: `5 passed; 0 failed`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src-tauri/src/commands.rs
|
||||
git commit -m "feat(update): use platform-aware asset and filename in update flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Version consistency check script
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/check-version.sh`
|
||||
|
||||
**Interfaces:**
|
||||
- Inputs: `expected_version` from CLI arg; reads `package.json`, `src-tauri/Cargo.toml`, `src-tauri/tauri.conf.json`
|
||||
- Output: exit 0 if all match, exit 1 otherwise (with diff to stderr)
|
||||
|
||||
- [ ] **Step 1: Write the script**
|
||||
|
||||
Create `scripts/check-version.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Verify that the version across the project matches the expected version.
|
||||
# Usage: check-version.sh <expected>
|
||||
# Reads:
|
||||
# package.json : root
|
||||
# src-tauri/Cargo.toml : version field
|
||||
# src-tauri/tauri.conf.json : version field
|
||||
set -euo pipefail
|
||||
|
||||
EXPECTED="${1:-}"
|
||||
|
||||
if [ -z "$EXPECTED" ]; then
|
||||
echo "usage: check-version.sh <expected>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Strip leading 'v' for comparison (we expect semver without v)
|
||||
EXPECTED_BARE="${EXPECTED#v}"
|
||||
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
CARGO_VERSION=$(grep -E '^version\s*=' src-tauri/Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
TAURI_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")
|
||||
|
||||
fail=0
|
||||
check() {
|
||||
local label="$1"
|
||||
local actual="$2"
|
||||
if [ "$actual" != "$EXPECTED_BARE" ]; then
|
||||
echo "FAIL: $label = $actual (expected $EXPECTED_BARE)" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
check "package.json" "$PKG_VERSION"
|
||||
check "src-tauri/Cargo.toml" "$CARGO_VERSION"
|
||||
check "src-tauri/tauri.conf.json" "$TAURI_VERSION"
|
||||
|
||||
if [ $fail -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: all versions = $EXPECTED_BARE"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Make it executable**
|
||||
|
||||
Run: `chmod +x scripts/check-version.sh`
|
||||
Expected: no output
|
||||
|
||||
- [ ] **Step 3: Test happy path**
|
||||
|
||||
Run: `./scripts/check-version.sh v0.6.0`
|
||||
Expected: `OK: all versions = 0.6.0`
|
||||
|
||||
- [ ] **Step 4: Test mismatch**
|
||||
|
||||
Run: `./scripts/check-version.sh v9.9.9`
|
||||
Expected: 3 lines starting with `FAIL:` and exit code 1
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/check-version.sh
|
||||
git commit -m "ci: add version-consistency check script"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: GitHub Actions release workflow
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/workflows/release.yml`
|
||||
|
||||
**Interfaces:**
|
||||
- Trigger: `push: tags: ['v*']`
|
||||
- Two jobs: `version-check` (sequential) and `build` (matrix ubuntu/macos/windows)
|
||||
|
||||
- [ ] **Step 1: Create the workflow file**
|
||||
|
||||
Create `.github/workflows/release.yml`:
|
||||
|
||||
```yaml
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
name: Version consistency
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Verify tag matches package.json / Cargo.toml / tauri.conf.json
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
chmod +x scripts/check-version.sh
|
||||
./scripts/check-version.sh "$TAG"
|
||||
|
||||
build:
|
||||
name: Build (${{ matrix.platform }})
|
||||
needs: version-check
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: ubuntu-latest
|
||||
args: --bundles deb,appimage,rpm
|
||||
- platform: macos-latest
|
||||
args: --bundles app,dmg
|
||||
- platform: windows-latest
|
||||
args: --bundles msi
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- uses: actions/setup-rust@v1
|
||||
with:
|
||||
cache: true
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libappindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev
|
||||
|
||||
- name: Build and upload release
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: ${{ github.ref_name }}
|
||||
releaseBody: |
|
||||
## ${{ github.ref_name }}
|
||||
|
||||
### Downloads by platform
|
||||
|
||||
- **Windows**: `.msi` installer
|
||||
- **macOS**: `.dmg` (unsigned — first launch needs right-click → Open) + `.app`
|
||||
- **Linux**: `.deb` / `.AppImage` / `.rpm`
|
||||
|
||||
Full changelog: see commit history.
|
||||
releaseDraft: true
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }}
|
||||
args: ${{ matrix.args }}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Sanity-check the file is in place**
|
||||
|
||||
Run: `ls -la .github/workflows/release.yml && head -5 .github/workflows/release.yml`
|
||||
Expected: file exists and the first line is `name: Release` (GitHub Actions will catch any YAML syntax errors at runtime)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .github/workflows/release.yml
|
||||
git commit -m "ci: add multi-platform release workflow on tag push"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Local Docker for Linux builds
|
||||
|
||||
**Files:**
|
||||
- Create: `Dockerfile`
|
||||
- Create: `docker-compose.yml`
|
||||
|
||||
**Interfaces:**
|
||||
- Input: source tree at `/workspace`
|
||||
- Output: built packages at `/workspace/src-tauri/target/release/bundle/{deb,appimage,rpm}/`
|
||||
|
||||
- [ ] **Step 1: Create the Dockerfile**
|
||||
|
||||
Create `Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
# Build Kimi Switch Linux packages (deb / AppImage / rpm) inside a clean
|
||||
# Ubuntu 22.04 container. Used both locally (`docker compose run`) and as a
|
||||
# reference for GH Actions' ubuntu-latest job.
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV CARGO_TERM_COLOR=always
|
||||
ENV RUSTUP_HOME=/usr/local/rustup
|
||||
ENV CARGO_HOME=/usr/local/cargo
|
||||
ENV PATH=/usr/local/cargo/bin:$PATH
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libsoup-3.0-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
patchelf \
|
||||
file \
|
||||
nodejs \
|
||||
npm \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||
|
||||
RUN cargo install tauri-cli --version "^2.0" --locked
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create docker-compose.yml**
|
||||
|
||||
Create `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
build-linux:
|
||||
build: .
|
||||
container_name: kimiswitch-build-linux
|
||||
volumes:
|
||||
- .:/workspace
|
||||
- cargo-cache:/usr/local/cargo/registry
|
||||
- target-cache:/workspace/src-tauri/target
|
||||
working_dir: /workspace
|
||||
|
||||
volumes:
|
||||
cargo-cache:
|
||||
target-cache:
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build the Docker image**
|
||||
|
||||
Run: `docker compose build build-linux 2>&1 | tail -10`
|
||||
Expected: ends with `naming to docker.io/library/...build-linux done` (or similar successful build output)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add Dockerfile docker-compose.yml
|
||||
git commit -m "ci: add Linux Docker build environment"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Build documentation
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/BUILD.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Audience: contributors who want to build the project locally
|
||||
- Output: instructions for Windows / macOS / Linux local builds + Docker Linux build
|
||||
|
||||
- [ ] **Step 1: Write the doc**
|
||||
|
||||
Create `docs/BUILD.md`:
|
||||
|
||||
````markdown
|
||||
# Build
|
||||
|
||||
Docker (Linux), native (any platform), and CI (multi-platform) build instructions.
|
||||
|
||||
## Local builds by host
|
||||
|
||||
### Windows (current primary dev environment)
|
||||
|
||||
Prerequisites: Rust stable, Node 20, Microsoft C++ Build Tools, WebView2.
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run tauri build -- --bundles msi
|
||||
```
|
||||
|
||||
Output: `src-tauri/target/release/bundle/msi/`.
|
||||
|
||||
### macOS
|
||||
|
||||
Prerequisites: Xcode command-line tools, Rust stable, Node 20.
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run tauri build -- --bundles app,dmg
|
||||
```
|
||||
|
||||
Output: `src-tauri/target/release/bundle/{macos,dmg}/`.
|
||||
|
||||
Unsigned: first launch requires right-click → Open to bypass Gatekeeper.
|
||||
|
||||
### Linux (native)
|
||||
|
||||
Prerequisites: see `Dockerfile` for the full apt-get list (webkit2gtk-4.1, librsvg2, etc.).
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run tauri build -- --bundles deb,appimage,rpm
|
||||
```
|
||||
|
||||
Output: `src-tauri/target/release/bundle/{deb,appimage,rpm}/`.
|
||||
|
||||
### Linux via Docker (works on Windows / macOS too)
|
||||
|
||||
```bash
|
||||
docker compose build build-linux
|
||||
docker compose run --rm build-linux bash -c '
|
||||
npm ci &&
|
||||
npm run tauri build -- --bundles deb,appimage,rpm
|
||||
'
|
||||
```
|
||||
|
||||
Output is mounted back to `src-tauri/target/release/bundle/` on the host.
|
||||
|
||||
## CI: GitHub Actions
|
||||
|
||||
`.github/workflows/release.yml` runs on `git tag v* && git push origin v*`.
|
||||
|
||||
1. `version-check` runs `scripts/check-version.sh` to ensure tag matches `package.json`, `Cargo.toml`, `tauri.conf.json`.
|
||||
2. Three parallel jobs build for `ubuntu-latest`, `macos-latest`, `windows-latest` using `tauri-action`.
|
||||
3. Artifacts are uploaded to the GitHub Release (auto-draft for non-prerelease tags).
|
||||
|
||||
To cut a release:
|
||||
|
||||
```bash
|
||||
# 1. Bump version in three places
|
||||
# package.json: "version": "0.7.0"
|
||||
# src-tauri/Cargo.toml: version = "0.7.0"
|
||||
# src-tauri/tauri.conf.json: "version": "0.7.0"
|
||||
# 2. Commit
|
||||
git commit -am "chore(release): bump to v0.7.0"
|
||||
# 3. Tag and push
|
||||
git tag v0.7.0
|
||||
git push origin v0.7.0
|
||||
```
|
||||
|
||||
## Update checker asset selection
|
||||
|
||||
`src-tauri/src/commands.rs::pick_asset_for_current_os` picks the right asset by file extension:
|
||||
|
||||
| OS | Extension |
|
||||
|---------|------------|
|
||||
| Windows | `.msi` |
|
||||
| macOS | `.dmg` |
|
||||
| Linux | `.AppImage` (preferred; deb / rpm downloadable manually from the release page) |
|
||||
|
||||
If the preferred extension is missing, the UI falls back to opening the release page.
|
||||
````
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/BUILD.md
|
||||
git commit -m "docs: build instructions for all platforms + Docker + CI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: README — multi-platform download links + workflow badge
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `README_EN.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Adds a "下载" / "Downloads" section with three platform bullets
|
||||
- Adds a build-from-source link to `docs/BUILD.md`
|
||||
- Adds a GitHub Actions badge
|
||||
|
||||
- [ ] **Step 1: Add the badges (after the existing license badge)**
|
||||
|
||||
In `README.md`, locate the line:
|
||||
|
||||
```markdown
|
||||
[](./LICENSE)
|
||||
```
|
||||
|
||||
Add two new badges below it:
|
||||
|
||||
```markdown
|
||||
[](https://github.com/billowliu2/KimiSwitch/releases/latest)
|
||||
```
|
||||
|
||||
(Do the same in `README_EN.md`.)
|
||||
|
||||
- [ ] **Step 2: Add a "下载" section right after "这是什么"**
|
||||
|
||||
In `README.md`, after the paragraph immediately above `## 核心特性`, insert:
|
||||
|
||||
```markdown
|
||||
## 下载
|
||||
|
||||
- **Windows**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.msi`
|
||||
- **macOS**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.dmg`(**未签名** —— 首次启动请右键 → 打开绕过 Gatekeeper)
|
||||
- **Linux**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.deb` / `.AppImage` / `.rpm`
|
||||
|
||||
历史版本(v0.5.x 及之前)请前往 [git.codingplan.site 仓库](https://git.codingplan.site/admin/KimiCodeSwitch/releases)。
|
||||
|
||||
构建说明请见 [`docs/BUILD.md`](./docs/BUILD.md)。
|
||||
```
|
||||
|
||||
In `README_EN.md`, insert the English equivalent:
|
||||
|
||||
```markdown
|
||||
## Downloads
|
||||
|
||||
- **Windows**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.msi`
|
||||
- **macOS**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.dmg` (**unsigned** — first launch needs right-click → Open to bypass Gatekeeper)
|
||||
- **Linux**: [GitHub Releases](https://github.com/billowliu2/KimiSwitch/releases/latest) → `.deb` / `.AppImage` / `.rpm`
|
||||
|
||||
Historical versions (v0.5.x and earlier) live on the [git.codingplan.site repository](https://git.codingplan.site/admin/KimiCodeSwitch/releases).
|
||||
|
||||
Build instructions: [`docs/BUILD.md`](./docs/BUILD.md).
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md README_EN.md
|
||||
git commit -m "docs(readme): multi-platform downloads + workflow badge"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: End-to-end test with a pre-release tag
|
||||
|
||||
**Files:**
|
||||
- (No file changes — pure verification)
|
||||
|
||||
**Goal:** Confirm the GH Actions workflow actually produces all 6 assets. Use a pre-release tag so it doesn't pollute the main `v0.6.0` release.
|
||||
|
||||
- [ ] **Step 1: Confirm current version is in sync**
|
||||
|
||||
Run: `./scripts/check-version.sh v0.6.0`
|
||||
Expected: `OK: all versions = 0.6.0`
|
||||
|
||||
- [ ] **Step 2: Push all un-pushed commits**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Tag a pre-release on the current commit**
|
||||
|
||||
```bash
|
||||
git tag v0.6.0-rc1
|
||||
git push origin v0.6.0-rc1
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Watch the workflow run**
|
||||
|
||||
Open: https://github.com/billowliu2/KimiSwitch/actions
|
||||
|
||||
Expected: `version-check` job passes, then 3 `build` jobs run in parallel. Total time roughly 10-15 minutes per platform.
|
||||
|
||||
- [ ] **Step 5: Verify the pre-release**
|
||||
|
||||
Open: https://github.com/billowliu2/KimiSwitch/releases/tag/v0.6.0-rc1
|
||||
|
||||
Expected: 6 assets attached (`.msi`, `.dmg`, `.app`, `.deb`, `.AppImage`, `.rpm`); release marked as "Pre-release".
|
||||
|
||||
- [ ] **Step 6: Verify version mismatch fails the workflow**
|
||||
|
||||
On a clean branch, change only `package.json` version to `0.9.0`, commit, then:
|
||||
|
||||
```bash
|
||||
git tag v0.6.0-rc2
|
||||
git push origin v0.6.0-rc2
|
||||
```
|
||||
|
||||
Open the new workflow run. Expected: `version-check` job fails with `FAIL: package.json = 0.9.0 (expected 0.6.0)`. Build jobs are skipped.
|
||||
|
||||
Then revert the package.json change and delete the rc2 tag:
|
||||
|
||||
```bash
|
||||
git checkout main -- package.json
|
||||
git tag -d v0.6.0-rc2
|
||||
git push origin :refs/tags/v0.6.0-rc2
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Clean up the rc1 tag**
|
||||
|
||||
After verifying the rc1 assets are good, delete the pre-release to keep the release list clean:
|
||||
|
||||
```bash
|
||||
git tag -d v0.6.0-rc1
|
||||
git push origin :refs/tags/v0.6.0-rc1
|
||||
# Then delete the GitHub Release via the web UI (or leave it as a historical rc)
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Final commit — none if Tasks 1-9 are clean**
|
||||
|
||||
If no edits were needed, no commit. Otherwise, commit any fixups.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
After all 10 tasks, the project ships with:
|
||||
- macOS + Linux + Windows builds produced from a single `git tag v0.7.0` push
|
||||
- Local Docker for Linux dev iteration
|
||||
- Per-platform asset picker in the update checker
|
||||
- README + BUILD docs covering all three platforms
|
||||
- Version-consistency gate preventing drift between tag and the three version files
|
||||
@@ -0,0 +1,248 @@
|
||||
# Kimi Switch 多平台打包 — 设计
|
||||
|
||||
**日期**:2026-07-29
|
||||
**状态**:待用户审查
|
||||
**关联 issue / 任务**:新增 macOS + Linux 打包支持(gh Actions 为主 + Docker 本地调 Linux)
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
Kimi Switch 是 Tauri 2.x 桌面配置管理器,目前仅支持 Windows MSI(`tauri.conf.json` 中 `bundle.targets: ["msi"]`)。本次改动让同一份代码可同时构建 Windows / macOS / Linux 三平台,整理出能在 GitHub Releases 上统一发布的工作流。
|
||||
|
||||
成功标准:
|
||||
- `git tag v0.7.0 && git push origin v0.7.0` → GitHub Actions 自动产出 6 个资产(`.msi` + `.dmg` + `.app` + `.deb` + `.AppImage` + `.rpm`),全部挂到 v0.7.0 Release
|
||||
- 开发者本机 Windows + Docker 可本地调 Linux 三种格式
|
||||
- 应用内"检查更新"按 OS 自动选对应 asset 下载
|
||||
- 不回归现有 Windows 用户的体验
|
||||
|
||||
## 2. 决策(已与用户确认)
|
||||
|
||||
| 决策点 | 选定 |
|
||||
|--------|------|
|
||||
| 构建策略 | GH Actions 为主(tauri-action)+ Docker 本地调 Linux |
|
||||
| macOS 签名 | 不签名 / 跳过公证(用户在 README 中被告知右键 → 打开绕过 Gatekeeper) |
|
||||
| Linux 格式 | deb + AppImage + rpm(全部覆盖) |
|
||||
| Release 触发 | tag 推送触发 + tag / package.json / Cargo.toml / tauri.conf.json 四处版本一致性检查 |
|
||||
| Update checker | 按 OS + 扩展名自动选 asset(Windows: .msi,macOS: .dmg,Linux: .AppImage) |
|
||||
|
||||
## 3. 架构
|
||||
|
||||
### 3.1 发布流
|
||||
|
||||
```
|
||||
git tag v0.7.0 && git push origin v0.7.0
|
||||
│
|
||||
▼
|
||||
GH Actions: version-check job
|
||||
│ (tag == package.json == Cargo.toml == tauri.conf.json ?)
|
||||
▼
|
||||
3 个并行 jobs 同时 build:
|
||||
ubuntu-latest → .deb / .AppImage / .rpm
|
||||
macos-latest → .app + .dmg(无签名)
|
||||
windows-latest → .msi
|
||||
│
|
||||
▼
|
||||
tauri-action 自动收集 artifacts → 上传到 GitHub Release v0.7.0
|
||||
```
|
||||
|
||||
### 3.2 本地调试流
|
||||
|
||||
```
|
||||
Developer on Windows
|
||||
│
|
||||
▼
|
||||
docker compose run --rm build-linux
|
||||
│
|
||||
▼
|
||||
ubuntu 22.04 容器内
|
||||
apt-get install webkit2gtk-4.1 librsvg2-dev
|
||||
cargo install tauri-cli
|
||||
npm ci && npm run build
|
||||
cargo tauri build --bundles deb,appimage,rpm
|
||||
│
|
||||
▼
|
||||
src-tauri/target/release/bundle/
|
||||
├── deb/KimiSwitch_0.7.0_amd64.deb
|
||||
├── appimage/KimiSwitch_0.7.0_amd64.AppImage
|
||||
└── rpm/KimiSwitch-0.7.0-1.x86_64.rpm
|
||||
```
|
||||
|
||||
### 3.3 用户更新流
|
||||
|
||||
```
|
||||
App 启动 / 每 8h / 手动
|
||||
│
|
||||
▼
|
||||
GET https://api.github.com/repos/billowliu2/KimiSwitch/releases?per_page=1
|
||||
│
|
||||
▼
|
||||
解析 JSON,按 OS 过滤 assets
|
||||
│
|
||||
▼
|
||||
? update_available → footer 弹提示
|
||||
│
|
||||
▼
|
||||
用户点"下载" → 后端流式下载到 temp_dir()/KimiSwitch_update.<ext>
|
||||
│
|
||||
▼
|
||||
open_installer(path) → 系统默认 handler 打开
|
||||
Windows: 启动 MSI 安装
|
||||
macOS: 挂载 .dmg,用户拖入 Applications
|
||||
Linux: AppImage 直接 chmod +x 跑;deb/rpm 走系统包管理器
|
||||
```
|
||||
|
||||
## 4. 文件 / 代码变更
|
||||
|
||||
### 4.1 新增
|
||||
|
||||
| 路径 | 目的 |
|
||||
|------|------|
|
||||
| `.github/workflows/release.yml` | GH Actions 工作流(version-check + 3 平台矩阵) |
|
||||
| `Dockerfile` | Ubuntu 22.04 + webkit2gtk-4.1 + librsvg2 + cargo + node,构建工具链 |
|
||||
| `docker-compose.yml` | `docker compose run --rm build-linux` 一行起 |
|
||||
| `scripts/check-version.sh` | 校验 tag / package.json / Cargo.toml / tauri.conf.json 四处版本号一致 |
|
||||
| `docs/BUILD.md` | 本地 Docker 构建、平台差异、签名状态说明 |
|
||||
|
||||
### 4.2 修改
|
||||
|
||||
| 路径 | 改动 |
|
||||
|------|------|
|
||||
| `src-tauri/tauri.conf.json` | `bundle.targets: ["msi"]` → `"all"`;icon 列表追加 macOS 专用 `.icns`;加 `macOS.minimumSystemVersion: "10.15"` |
|
||||
| `src-tauri/src/main.rs` | `windows_subsystem = "windows"` 改为仅 Windows 启用(macOS / Linux 仍 console subsystem) |
|
||||
| `src-tauri/src/commands.rs` | `download_update` 临时文件名按 OS 走;`check_for_update` 按 OS 过滤 asset |
|
||||
| `README.md` | 三平台下载链接;Docker 调试一节;GH Actions 状态徽章 |
|
||||
| `README_EN.md` | 同上英文版 |
|
||||
|
||||
### 4.3 核心 Rust 实现
|
||||
|
||||
```rust
|
||||
// src-tauri/src/commands.rs
|
||||
|
||||
/// Pick the GitHub release asset matching the current OS by file extension.
|
||||
/// Preference for Linux is AppImage (portable, no install). If the preferred
|
||||
/// extension is not present, returns None so the UI can fall back to
|
||||
/// "open release page" instead of guessing a less-preferred format.
|
||||
fn pick_asset_for_current_os(assets: &[serde_json::Value]) -> Option<String> {
|
||||
let target_ext = match std::env::consts::OS {
|
||||
"macos" => "dmg",
|
||||
"linux" => "AppImage",
|
||||
"windows" => "msi",
|
||||
_ => return None,
|
||||
};
|
||||
assets.iter()
|
||||
.find(|a| a.get("name").and_then(|n| n.as_str())
|
||||
.map(|n| n.ends_with(target_ext)).unwrap_or(false))
|
||||
.and_then(|a| a.get("browser_download_url"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// Build the platform-ext-specific temp filename for downloaded installer.
|
||||
fn update_temp_filename() -> String {
|
||||
let ext = match std::env::consts::OS {
|
||||
"macos" => "dmg",
|
||||
"linux" => "AppImage",
|
||||
"windows" => "msi",
|
||||
_ => "bin",
|
||||
};
|
||||
format!("KimiSwitch_update.{ext}")
|
||||
}
|
||||
```
|
||||
|
||||
`main.rs` 调整:
|
||||
|
||||
```rust
|
||||
// 旧
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
// 新
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
```
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
| 场景 | 处理 |
|
||||
|------|------|
|
||||
| tag 与 Cargo.toml 版本不一致 | workflow 立即 fail,输出 diff;不进入 build |
|
||||
| GitHub Release 已有同名 asset | tauri-action 自动覆盖(默认行为) |
|
||||
| Linux runner 缺 webkit2gtk 等系统包 | tauri-action 文档明确会自动 `apt-get install`;失败 bubble up |
|
||||
| macOS 构建失败 | 失败 job 标红,其他平台不受影响;release 仍上传成功的资产 |
|
||||
| 用户端 update check 网络失败 | 静默失败(`useUpdateCheck.ts` 已有 try/catch) |
|
||||
| 本地 Docker 构建失败 | 多阶段 Dockerfile,失败 → 容器退出码 + 完整日志 |
|
||||
| macOS 未签名 .dmg 被 Gatekeeper 拦 | README 明确"右键 → 打开"绕过;远期可加签名(独立设计) |
|
||||
|
||||
质量门:
|
||||
- `cargo check --target x86_64-unknown-linux-gnu`(Docker 内)
|
||||
- `npm run build`(前端)
|
||||
- 不引入新的 lint 失败
|
||||
- macOS unsigned 状态在 release body 里显式标注
|
||||
|
||||
## 6. 测试验证
|
||||
|
||||
### 6.1 本地构建验证
|
||||
|
||||
1. **单测 / lint**
|
||||
- `npm run lint`
|
||||
- `cargo check`
|
||||
- `cargo check --target x86_64-unknown-linux-gnu`(Docker 内)
|
||||
|
||||
2. **本地 Docker Linux 构建**
|
||||
- `docker compose build build-linux` → 容器构建成功
|
||||
- `docker compose run --rm build-linux` → 产出 3 个 Linux 包
|
||||
- `dpkg --info kimiswitch_0.7.0_amd64.deb` 确认 metadata
|
||||
- `file kimiswitch_0.7.0_amd64.AppImage` 确认 ELF 可执行
|
||||
|
||||
3. **Rust 单元测**
|
||||
- `pick_asset_for_current_os`:
|
||||
- `windows` + assets `[msi, dmg, AppImage]` → 选 msi
|
||||
- `macos` + assets `[msi, dmg, AppImage]` → 选 dmg
|
||||
- `linux` + assets `[msi, dmg, AppImage]` → 选 AppImage
|
||||
- 都不匹配 → 返回 None
|
||||
|
||||
### 6.2 GH Actions 端到端
|
||||
|
||||
- 推 test tag `v0.6.0-rc1`(prerelease,**不污染正式 v0.6.0**)
|
||||
- 验证 3 个 jobs 都绿
|
||||
- 验证 assets 上传到 GitHub Release
|
||||
|
||||
### 6.3 运行时手测(自动化跳过,仅文档化)
|
||||
|
||||
- macOS: 挂载 .dmg,拖入 Applications;启动确认(首次会被 Gatekeeper 拦,文档说明)
|
||||
- Linux: AppImage `chmod +x && ./KimiSwitch_0.7.0_amd64.AppImage`;deb `sudo dpkg -i`;rpm `sudo rpm -i`
|
||||
- Windows: 已有 MSI 流程,不回归
|
||||
|
||||
### 6.4 Update checker 联调
|
||||
|
||||
- App 内"检查更新" → 后端 fetch GH releases → 按 OS 选对 asset → 下载正确文件
|
||||
- Linux 上 AppImage 装完后启动 app,update check 选 AppImage 而非其他格式
|
||||
|
||||
### 6.5 验收
|
||||
|
||||
- ✅ 三个平台都跑出可安装产物
|
||||
- ✅ GH Actions 总耗时 < 30 分钟
|
||||
- ✅ 版本不一致时 workflow 立即 fail
|
||||
- ✅ Update checker 跨平台行为正确
|
||||
- ✅ README 三平台下载链接齐全
|
||||
|
||||
## 7. 范围外(本次不做)
|
||||
|
||||
- macOS 代码签名 + notarization(用户已确认本期跳过;独立设计未来再加)
|
||||
- 自动更新检查替换为 Tauri 官方 `tauri-plugin-updater`(本期仍用自定义 `check_for_update`)
|
||||
- iOS / Android(已有 icon 资源,但本期不构建)
|
||||
- 自动 changelog 生成(用 commit log + 手动整理)
|
||||
- Linux 平台 snap / flatpak
|
||||
- Linux 32 位 / ARM 构建(先 x86_64 就够了)
|
||||
|
||||
## 8. 风险与回退
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|------|------|
|
||||
| macOS unsigned 限制分发 | README 引导用户绕过;后续可补签名 |
|
||||
| GH Actions 30 分钟耗时不够 | 用 `tauri-action` 自带缓存;3 平台并行通常 10-15 分钟 |
|
||||
| Docker 镜像构建慢 | 用 prebuilt `tauri-apps/tauri:2-jammy` 风格基础镜像(自维护一份);接下来 PR 可替换 |
|
||||
| 单元测 build 时间拖慢 | `pick_asset_for_current_os` 是纯函数,单测几百 ms;不会显著拖慢 CI |
|
||||
| 用户升级后旧版检查更新路径被破坏 | 新逻辑兼容旧 release(只有一个 msi asset 时返回 None,UI 显示"请手动下载") |
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kimiswitch",
|
||||
"private": true,
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify that the version across the project matches the expected version.
|
||||
# Usage: check-version.sh <expected>
|
||||
# Reads:
|
||||
# package.json : root
|
||||
# src-tauri/Cargo.toml : version field
|
||||
# src-tauri/tauri.conf.json : version field
|
||||
set -euo pipefail
|
||||
|
||||
EXPECTED="${1:-}"
|
||||
|
||||
if [ -z "$EXPECTED" ]; then
|
||||
echo "usage: check-version.sh <expected>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Strip leading 'v' for comparison (we expect semver without v)
|
||||
EXPECTED_BARE="${EXPECTED#v}"
|
||||
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
CARGO_VERSION=$(grep -E '^version\s*=' src-tauri/Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
TAURI_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")
|
||||
|
||||
fail=0
|
||||
check() {
|
||||
local label="$1"
|
||||
local actual="$2"
|
||||
if [ "$actual" != "$EXPECTED_BARE" ]; then
|
||||
echo "FAIL: $label = $actual (expected $EXPECTED_BARE)" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
check "package.json" "$PKG_VERSION"
|
||||
check "src-tauri/Cargo.toml" "$CARGO_VERSION"
|
||||
check "src-tauri/tauri.conf.json" "$TAURI_VERSION"
|
||||
|
||||
if [ $fail -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: all versions = $EXPECTED_BARE"
|
||||
Generated
+1
-1
@@ -1958,7 +1958,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kimiswitch"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kimiswitch"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
description = "Kimi Switch - model config manager"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
+124
-7
@@ -740,9 +740,21 @@ fn version_lt(current: &str, latest: &str) -> bool {
|
||||
#[tauri::command]
|
||||
pub async fn check_for_update() -> Result<UpdateInfo, String> {
|
||||
let current = env!("CARGO_PKG_VERSION").to_string();
|
||||
let url = "https://git.codingplan.site/api/v1/repos/admin/KimiCodeSwitch/releases?limit=1";
|
||||
// GitHub Releases API for billowliu2/KimiSwitch. Historical versions
|
||||
// (v0.5.x and earlier) live on git.codingplan.site and are not exposed
|
||||
// here — only v0.6.0+ is published via GitHub.
|
||||
let url = "https://api.github.com/repos/billowliu2/KimiSwitch/releases?per_page=1";
|
||||
|
||||
let resp = reqwest::get(url)
|
||||
// GitHub returns 403 without a User-Agent header. Bundle the app version
|
||||
// so the source is identifiable in any rate-limit / abuse reports.
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(concat!("KimiSwitch/", env!("CARGO_PKG_VERSION")))
|
||||
.build()
|
||||
.map_err(|e| format!("client build failed: {e}"))?;
|
||||
|
||||
let resp = client
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("request failed: {e}"))?;
|
||||
|
||||
@@ -771,10 +783,7 @@ pub async fn check_for_update() -> Result<UpdateInfo, String> {
|
||||
let download_url = first
|
||||
.get("assets")
|
||||
.and_then(|a| a.as_array())
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|a| a.get("browser_download_url"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
.and_then(|a| pick_asset_for_current_os(a));
|
||||
|
||||
let update_available = version_lt(¤t, &latest);
|
||||
|
||||
@@ -807,7 +816,7 @@ pub async fn download_update(
|
||||
let total = resp.content_length().unwrap_or(0);
|
||||
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let file_path = temp_dir.join("KimiSwitch_update.msi");
|
||||
let file_path = temp_dir.join(update_temp_filename());
|
||||
|
||||
let mut file = std::fs::File::create(&file_path)
|
||||
.map_err(|e| format!("create temp file failed: {e}"))?;
|
||||
@@ -856,4 +865,112 @@ pub fn open_installer(app: tauri::AppHandle, path: String) -> Result<(), String>
|
||||
app.opener()
|
||||
.open_path(&path, None::<&str>)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
/// Preference for Linux is AppImage (portable, no install). If the preferred
|
||||
/// extension is not present, returns None so the UI can fall back to
|
||||
/// "open release page" instead of guessing a less-preferred format.
|
||||
fn pick_asset_for_os(assets: &[serde_json::Value], os: &str) -> Option<String> {
|
||||
let target_ext = match os {
|
||||
"macos" => "dmg",
|
||||
"linux" => "AppImage",
|
||||
"windows" => "msi",
|
||||
_ => return None,
|
||||
};
|
||||
assets
|
||||
.iter()
|
||||
.find(|a| {
|
||||
a.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|n| n.ends_with(target_ext))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.and_then(|a| a.get("browser_download_url"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// Pick the GitHub release asset for the current OS (runtime target).
|
||||
fn pick_asset_for_current_os(assets: &[serde_json::Value]) -> Option<String> {
|
||||
pick_asset_for_os(assets, std::env::consts::OS)
|
||||
}
|
||||
|
||||
/// Build the platform-specific temp filename for the downloaded installer.
|
||||
fn update_temp_filename() -> String {
|
||||
let ext = match std::env::consts::OS {
|
||||
"macos" => "dmg",
|
||||
"linux" => "AppImage",
|
||||
"windows" => "msi",
|
||||
_ => "bin",
|
||||
};
|
||||
format!("KimiSwitch_update.{ext}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod asset_picker_tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn assets(names: &[&str]) -> Vec<serde_json::Value> {
|
||||
names
|
||||
.iter()
|
||||
.map(|n| {
|
||||
json!({
|
||||
"name": n,
|
||||
"browser_download_url": format!("https://example.com/{n}"),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_picks_msi() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||
let url = pick_asset_for_os(&assets, "windows");
|
||||
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_x64_en-US.msi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_picks_dmg() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||
let url = pick_asset_for_os(&assets, "macos");
|
||||
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_aarch64.dmg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_picks_appimage() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi", "KimiSwitch_0.7.0_aarch64.dmg", "KimiSwitch_0.7.0_amd64.AppImage"]);
|
||||
let url = pick_asset_for_os(&assets, "linux");
|
||||
assert_eq!(url.as_deref(), Some("https://example.com/KimiSwitch_0.7.0_amd64.AppImage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_target_returns_none() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi"]);
|
||||
let url = pick_asset_for_os(&assets, "macos");
|
||||
assert_eq!(url, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_assets_returns_none() {
|
||||
let url = pick_asset_for_os(&[], "linux");
|
||||
assert_eq!(url, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_os_returns_none() {
|
||||
let assets = assets(&["KimiSwitch_0.7.0_x64_en-US.msi"]);
|
||||
let url = pick_asset_for_os(&assets, "freebsd");
|
||||
assert_eq!(url, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Open an external https/http URL in the system default browser.
|
||||
/// Uses the Rust opener directly (bypassing the JS plugin scope) so it
|
||||
/// works regardless of capability scope configuration.
|
||||
#[tauri::command]
|
||||
pub fn open_external_url(app: tauri::AppHandle, url: String) -> Result<(), String> {
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
app.opener()
|
||||
.open_url(url, None::<&str>)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -104,6 +104,7 @@ pub fn run() {
|
||||
commands::check_for_update,
|
||||
commands::download_update,
|
||||
commands::open_installer,
|
||||
commands::open_external_url,
|
||||
dashboard::get_paths,
|
||||
dashboard::get_prices,
|
||||
dashboard::get_summary,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
fn main() {
|
||||
kimiswitch_lib::run();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"productName": "Kimi Switch",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.3",
|
||||
"identifier": "com.kimiswitch.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@@ -26,18 +26,30 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["msi"],
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.ico",
|
||||
"icons/icon.icns"
|
||||
],
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"type": "downloadBootstrapper"
|
||||
},
|
||||
"nsis": null
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "10.15"
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"appimage": {
|
||||
"bundleMediaFramework": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createPortal } from "react-dom";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useTranslation } from "../i18n";
|
||||
import type { TranslationKey } from "../i18n/zh";
|
||||
import { findPresetForProvider } from "../config/providerPresets";
|
||||
import { getDefaultMaxContextSize } from "../lib/model-defaults";
|
||||
import { capabilitiesFromRef, getModelRef } from "../lib/models-dev";
|
||||
import { getIconMetadata } from "../icons/extracted/metadata";
|
||||
@@ -117,6 +118,10 @@ export function ProviderEdit({
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||
|
||||
// Preset this provider was created from (if any) — provides the
|
||||
// "Get API Key" / referral links shown under the key input.
|
||||
const preset = findPresetForProvider(provider);
|
||||
|
||||
useEffect(() => {
|
||||
const def = defaultBaseUrl(agent, provider.provider_type);
|
||||
if (!def) return;
|
||||
@@ -215,7 +220,7 @@ export function ProviderEdit({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor={nameId} className="block text-sm text-content-muted mb-1.5">
|
||||
{t("providerName")}
|
||||
{t("providerName")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id={nameId}
|
||||
@@ -296,7 +301,7 @@ export function ProviderEdit({
|
||||
{!provider.managed && (
|
||||
<div>
|
||||
<label htmlFor={apiKeyId} className="block text-sm text-content-muted mb-1.5">
|
||||
{t("apiKey")}
|
||||
{t("apiKey")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
@@ -316,12 +321,29 @@ export function ProviderEdit({
|
||||
{showApiKey ? t("hide") : t("show")}
|
||||
</button>
|
||||
</div>
|
||||
{(() => {
|
||||
const linkUrl = preset?.apiKeyUrl ?? provider.official_url ?? null;
|
||||
if (!linkUrl) return null;
|
||||
return (
|
||||
<div className="mt-1.5 flex items-center gap-4 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
invoke("open_external_url", { url: linkUrl }).catch(() => {})
|
||||
}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
{preset?.apiKeyUrl ? t("getApiKeyLink") : t("officialUrl")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor={baseUrlId} className="block text-sm text-content-muted mb-1.5">
|
||||
{t("requestUrl")}
|
||||
{t("requestUrl")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id={baseUrlId}
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface ProviderPreset {
|
||||
nameKey?: string;
|
||||
/** Official website / console URL. */
|
||||
websiteUrl?: string;
|
||||
/** Where to create / fetch an API key; shown as a link under the key input. */
|
||||
apiKeyUrl?: string;
|
||||
/** Category: sorting & badge. */
|
||||
category: PresetCategory;
|
||||
@@ -111,6 +112,7 @@ export const providerPresets: ProviderPreset[] = [
|
||||
name: "Kimi For Coding",
|
||||
nameKey: "presetNameKimiCoding",
|
||||
websiteUrl: "https://www.kimi.com/code",
|
||||
apiKeyUrl: "https://kimi-bot.com/activities/zh-cn/invite/share?scenario=invite&from=share_poster&invitation_code=6UJX7J",
|
||||
category: "official",
|
||||
providerType: "kimi",
|
||||
baseUrl: "https://api.kimi.com/coding/v1",
|
||||
@@ -248,6 +250,7 @@ export const providerPresets: ProviderPreset[] = [
|
||||
name: "MiniMax Token Plan",
|
||||
nameKey: "presetNameMinimaxTokenPlan",
|
||||
websiteUrl: "https://platform.minimaxi.com",
|
||||
apiKeyUrl: "https://platform.minimaxi.com/subscribe/token-plan?code=GmmZA629b5&source=link",
|
||||
category: "cn_official",
|
||||
providerType: "openai",
|
||||
// Token Plan is sold against the same platform endpoint as the
|
||||
@@ -274,6 +277,21 @@ export const providerPresets: ProviderPreset[] = [
|
||||
billingMode: "pay_as_you_go",
|
||||
usageKinds: ["balance:stepfun"],
|
||||
},
|
||||
{
|
||||
id: "stepfun-plan",
|
||||
name: "StepFun Plan",
|
||||
nameKey: "presetNameStepfunPlan",
|
||||
websiteUrl: "https://platform.stepfun.com/step-plan",
|
||||
apiKeyUrl: "https://platform.stepfun.com/?invite_code=HSIGHTPS",
|
||||
category: "cn_official",
|
||||
providerType: "openai",
|
||||
// Step Plan endpoint — same host, different path prefix than the
|
||||
// pay-as-you-go API (/v1). Serves both OpenAI and Anthropic protocols.
|
||||
baseUrl: "https://api.stepfun.com/step_plan/v1",
|
||||
icon: "stepfun",
|
||||
models: [{ model: "step-3.7-flash" }, { model: "step-3.5-flash" }],
|
||||
billingMode: "subscription",
|
||||
},
|
||||
{
|
||||
id: "siliconflow",
|
||||
name: "SiliconFlow",
|
||||
@@ -401,7 +419,7 @@ export const providerPresets: ProviderPreset[] = [
|
||||
nameKey: "presetNameOpencodeGo",
|
||||
// Referral link — supports the OpenCode project
|
||||
websiteUrl: "https://opencode.ai/go?ref=DFCNADQCEM",
|
||||
apiKeyUrl: "https://opencode.ai/zen/",
|
||||
apiKeyUrl: "https://opencode.ai/go?ref=DFCNADQCEM",
|
||||
category: "third_party",
|
||||
providerType: "openai",
|
||||
baseUrl: "https://opencode.ai/zen/go/v1",
|
||||
@@ -521,6 +539,21 @@ export function presetToProviderAndModels(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort reverse lookup: which preset (if any) a Provider was created
|
||||
* from. Matches the provider name with any "-2"/"-3" dedup suffix stripped,
|
||||
* falling back to an exact official_url match. Used to surface preset-only
|
||||
* metadata (apiKeyUrl / referralUrl) in the edit form without persisting
|
||||
* extra fields on Provider.
|
||||
*/
|
||||
export function findPresetForProvider(provider: Provider): ProviderPreset | undefined {
|
||||
const base = provider.name.replace(/-\d+$/, "");
|
||||
return (
|
||||
providerPresets.find((p) => p.id === base) ??
|
||||
providerPresets.find((p) => p.websiteUrl != null && p.websiteUrl === provider.official_url)
|
||||
);
|
||||
}
|
||||
|
||||
// Dev-only cross-end drift check: assert that every usage kind referenced by
|
||||
// any preset is one the Rust `UsageKind` enum knows how to handle. The
|
||||
// runtime filter in `query_provider_usage` silently drops unknown kinds,
|
||||
|
||||
@@ -60,6 +60,7 @@ export const enTranslations: Record<TranslationKey, string> = {
|
||||
apiFormat: "API Format",
|
||||
authField: "Auth Field",
|
||||
apiKey: "API Key",
|
||||
getApiKeyLink: "Get API Key",
|
||||
show: "Show",
|
||||
hide: "Hide",
|
||||
requestUrl: "Request URL",
|
||||
@@ -349,6 +350,7 @@ export const enTranslations: Record<TranslationKey, string> = {
|
||||
presetNameMinimax: "MiniMax",
|
||||
presetNameMinimaxTokenPlan: "MiniMax Token Plan",
|
||||
presetNameStepfun: "StepFun",
|
||||
presetNameStepfunPlan: "StepFun Plan",
|
||||
presetNameSiliconflow: "SiliconFlow",
|
||||
presetNameNovita: "Novita AI",
|
||||
presetNameOpenrouter: "OpenRouter",
|
||||
|
||||
@@ -58,6 +58,7 @@ export const zhTranslations = {
|
||||
apiFormat: "API 格式",
|
||||
authField: "认证字段",
|
||||
apiKey: "API Key",
|
||||
getApiKeyLink: "获取 API Key",
|
||||
show: "显示",
|
||||
hide: "隐藏",
|
||||
requestUrl: "请求地址",
|
||||
@@ -344,6 +345,7 @@ export const zhTranslations = {
|
||||
presetNameMinimax: "MiniMax",
|
||||
presetNameMinimaxTokenPlan: "MiniMax Token Plan",
|
||||
presetNameStepfun: "阶跃星辰",
|
||||
presetNameStepfunPlan: "阶跃星辰 Step Plan",
|
||||
presetNameSiliconflow: "硅基流动",
|
||||
presetNameNovita: "Novita AI",
|
||||
presetNameOpenrouter: "OpenRouter",
|
||||
|
||||
Reference in new issue
Block a user