diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d2cea7e1f..2765dc703 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -103,19 +103,12 @@ jobs:
- platform: macos-latest
target: aarch64-apple-darwin
name: macOS-arm64
- build_cli: true
- platform: macos-15-intel
target: x86_64-apple-darwin
name: macOS-x64
- build_cli: true
- platform: windows-2022
target: x86_64-pc-windows-msvc
name: Windows-x64
- build_cli: true
- - platform: ubuntu-22.04
- target: x86_64-unknown-linux-gnu
- name: Linux-x64-cli
- build_cli: true
runs-on: ${{ matrix.platform }}
env:
@@ -751,66 +744,6 @@ jobs:
printf 'Staged macOS assets under %s:\n' "$STAGING_DIR"
find "$STAGING_DIR" -maxdepth 1 -type f | sort
- - name: Build lime-cli release binary
- if: matrix.build_cli && (matrix.platform == 'windows-2022' || matrix.platform == 'ubuntu-22.04' || (startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success')))
- shell: bash
- env:
- CARGO_PROFILE_RELEASE_LTO: "off"
- CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32
- CARGO_INCREMENTAL: 0
- CARGO_TARGET_DIR: src-tauri/target
- SCCACHE_GHA_ENABLED: "true"
- RUSTC_WRAPPER: sccache
- run: |
- set -euxo pipefail
- cargo build --manifest-path src-tauri/Cargo.toml -p lime-cli --release --target "${{ matrix.target }}"
-
- - name: Package lime-cli release asset
- id: package_lime_cli
- if: matrix.build_cli && (matrix.platform == 'windows-2022' || matrix.platform == 'ubuntu-22.04' || (startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success')))
- shell: bash
- run: |
- set -euxo pipefail
- VERSION="${{ github.event.inputs.version || github.ref_name }}"
- VERSION="${VERSION#v}"
-
- metadata="$(
- node packages/lime-cli-npm/scripts/build-release.js \
- --target-triple "${{ matrix.target }}" \
- --version "$VERSION" \
- --out-dir "packages/lime-cli-npm/dist" \
- --json
- )"
-
- echo "$metadata"
- asset_path="$(node -e 'const data = JSON.parse(process.argv[1]); process.stdout.write(data.archivePath);' "$metadata")"
- echo "asset_path=$asset_path" >> "$GITHUB_OUTPUT"
-
- - name: Stage lime-cli release asset
- if: matrix.build_cli && (matrix.platform == 'windows-2022' || matrix.platform == 'ubuntu-22.04' || (startsWith(matrix.platform, 'macos') && (steps.build_macos_primary.outcome == 'success' || steps.build_macos_retry.outcome == 'success')))
- shell: bash
- run: |
- set -euxo pipefail
- ASSET_PATH="${{ steps.package_lime_cli.outputs.asset_path }}"
- STAGING_DIR="release-assets/${{ matrix.target }}"
-
- if [ -z "$ASSET_PATH" ] || [ ! -f "$ASSET_PATH" ]; then
- echo "lime-cli asset missing: $ASSET_PATH" >&2
- exit 1
- fi
-
- node -e "
- const fs = require('fs');
- const path = require('path');
- const src = process.argv[1];
- const destDir = process.argv[2];
- fs.mkdirSync(destDir, { recursive: true });
- const dest = path.join(destDir, path.basename(src));
- fs.copyFileSync(src, dest);
- process.stdout.write(dest);
- " "$ASSET_PATH" "$STAGING_DIR"
- echo
-
- name: Upload staged release assets artifact
if: always()
uses: actions/upload-artifact@v4
@@ -935,6 +868,18 @@ jobs:
printf 'Generated updater manifest:\n'
cat release-updater/latest.json
+ - name: Prepare GitHub release upload assets
+ env:
+ RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ node scripts/prepare-github-release-assets.mjs \
+ --assets-dir release-assets \
+ --out-dir release-github-assets \
+ --version "$RELEASE_TAG" \
+ --extra-asset release-updater/latest.json
+
- name: Upload staged release assets to GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -944,24 +889,114 @@ jobs:
TAG="${{ github.event.inputs.version || github.ref_name }}"
assets=()
- while IFS= read -r asset; do
- [ -n "$asset" ] || continue
+ while IFS= read -r -d '' asset; do
assets+=("$asset")
- done < <(find "release-assets" -type f ! -name "latest*.json" | sort)
- assets+=("release-updater/latest.json")
+ done < <(find "release-github-assets" -type f -print0 | sort -z)
if [ "${#assets[@]}" -eq 0 ]; then
- echo "No staged release assets found" >&2
+ echo "No prepared GitHub release assets found" >&2
exit 1
fi
- printf 'Uploading staged release assets:\n'
+ printf 'Uploading GitHub release assets:\n'
printf ' - %s\n' "${assets[@]}"
gh release upload "$TAG" "${assets[@]}" \
--repo "$GITHUB_REPOSITORY" \
--clobber
+ - name: Publish GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ gh release edit "$RELEASE_TAG" \
+ --repo "$GITHUB_REPOSITORY" \
+ --draft=false \
+ --latest
+
+ publish_updater_assets_r2:
+ name: Publish updater assets to Cloudflare R2
+ needs: publish_release_assets
+ if: needs.publish_release_assets.result == 'success'
+ continue-on-error: true
+ runs-on: ubuntu-22.04
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: ${{ github.event.inputs.source_ref || github.ref }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+
+ - name: Download staged release assets
+ uses: actions/download-artifact@v4
+ with:
+ pattern: release-assets-*
+ path: release-assets
+ merge-multiple: true
+
+ - name: Prepare updater release notes
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ NOTES_FILE="$RUNNER_TEMP/release-notes.md"
+
+ if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json body -q .body > "$NOTES_FILE" && [ -s "$NOTES_FILE" ]; then
+ echo "Loaded release notes from GitHub Release"
+ exit 0
+ fi
+
+ if [ -f RELEASE_NOTES.md ]; then
+ cp RELEASE_NOTES.md "$NOTES_FILE"
+ echo "Loaded release notes from RELEASE_NOTES.md"
+ exit 0
+ fi
+
+ PREV_TAG="$(git tag --sort=-v:refname | grep -v "^${RELEASE_TAG}$" | head -n 1)"
+ if [ -z "$PREV_TAG" ]; then
+ CHANGELOG="$(git log --pretty=format:"- %s (%h)" "${RELEASE_TAG}" 2>/dev/null || git log --pretty=format:"- %s (%h)")"
+ else
+ CHANGELOG="$(git log --pretty=format:"- %s (%h)" "${PREV_TAG}..${RELEASE_TAG}" 2>/dev/null || git log --pretty=format:"- %s (%h)" "${PREV_TAG}..HEAD")"
+ fi
+
+ {
+ echo "## Lime ${RELEASE_TAG}"
+ echo
+ echo "### Changes"
+ echo "${CHANGELOG}"
+ } > "$NOTES_FILE"
+ echo "Generated release notes from git history"
+
+ - name: Generate stable updater manifest
+ env:
+ LIME_UPDATES_BASE_URL: ${{ secrets.LIME_UPDATES_BASE_URL || vars.LIME_UPDATES_BASE_URL }}
+ RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ BASE_URL="${LIME_UPDATES_BASE_URL:-https://updates.limecloud.com}"
+ node scripts/release-updater-manifest.mjs \
+ --assets-dir release-assets \
+ --out-dir release-updater \
+ --version "$RELEASE_TAG" \
+ --base-url "$BASE_URL" \
+ --notes-file "$RUNNER_TEMP/release-notes.md" \
+ --channel stable
+
+ printf 'Generated updater manifest for R2:\n'
+ cat release-updater/latest.json
+
- name: Upload updater assets to Cloudflare R2
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -979,6 +1014,7 @@ jobs:
const args = [
'npx wrangler@latest r2 object put',
JSON.stringify(`${process.env.BUCKET}/${item.key}`),
+ '--remote',
'--file',
JSON.stringify(item.file),
'--content-type',
@@ -1006,7 +1042,13 @@ jobs:
BUCKET="${LIME_RELEASES_R2_BUCKET:-lime-releases}"
KEEP="${LIME_R2_KEEP_RELEASES:-3}"
+ if ! npx wrangler@latest r2 object --help | grep -q "object list"; then
+ echo "::warning::wrangler r2 object list is unavailable; skipping old R2 updater cleanup"
+ exit 0
+ fi
+
npx wrangler@latest r2 object list "$BUCKET" \
+ --remote \
--prefix "lime/stable/v" \
--json > "$RUNNER_TEMP/r2-objects.json"
@@ -1027,5 +1069,112 @@ jobs:
cat "$RUNNER_TEMP/r2-delete-keys.txt"
while IFS= read -r key; do
[ -n "$key" ] || continue
- npx wrangler@latest r2 object delete "${BUCKET}/${key}"
+ npx wrangler@latest r2 object delete "${BUCKET}/${key}" --remote
done < "$RUNNER_TEMP/r2-delete-keys.txt"
+
+ publish_lime_cli_assets:
+ name: Publish lime-cli release assets
+ needs: publish_release_assets
+ if: needs.publish_release_assets.result == 'success'
+ continue-on-error: true
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: macos-latest
+ target: aarch64-apple-darwin
+ name: macOS-arm64-cli
+ - platform: macos-15-intel
+ target: x86_64-apple-darwin
+ name: macOS-x64-cli
+ - platform: windows-2022
+ target: x86_64-pc-windows-msvc
+ name: Windows-x64-cli
+ - platform: ubuntu-22.04
+ target: x86_64-unknown-linux-gnu
+ name: Linux-x64-cli
+
+ runs-on: ${{ matrix.platform }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: ${{ github.event.inputs.source_ref || github.ref }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ matrix.target }}
+
+ - name: Setup sccache
+ uses: mozilla-actions/sccache-action@v0.0.9
+
+ - name: Setup Rust cache
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: src-tauri
+ shared-key: "lime-cli-${{ matrix.target }}"
+ cache-on-failure: true
+ cache-all-crates: true
+ save-if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
+
+ - name: Build lime-cli release binary
+ shell: bash
+ env:
+ CARGO_PROFILE_RELEASE_LTO: "off"
+ CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32
+ CARGO_INCREMENTAL: 0
+ CARGO_TARGET_DIR: src-tauri/target
+ SCCACHE_GHA_ENABLED: "true"
+ RUSTC_WRAPPER: sccache
+ run: |
+ set -euxo pipefail
+ cargo build --manifest-path src-tauri/Cargo.toml -p lime-cli --release --target "${{ matrix.target }}"
+
+ - name: Package lime-cli release asset
+ id: package_lime_cli
+ shell: bash
+ run: |
+ set -euxo pipefail
+ VERSION="${{ github.event.inputs.version || github.ref_name }}"
+ VERSION="${VERSION#v}"
+
+ metadata="$(
+ node packages/lime-cli-npm/scripts/build-release.js \
+ --target-triple "${{ matrix.target }}" \
+ --version "$VERSION" \
+ --out-dir "packages/lime-cli-npm/dist" \
+ --json
+ )"
+
+ echo "$metadata"
+ asset_path="$(node -e 'const data = JSON.parse(process.argv[1]); process.stdout.write(data.archivePath);' "$metadata")"
+ echo "asset_path=$asset_path" >> "$GITHUB_OUTPUT"
+
+ - name: Upload lime-cli asset to GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ RELEASE_TAG: ${{ github.event.inputs.version || github.ref_name }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ ASSET_PATH="${{ steps.package_lime_cli.outputs.asset_path }}"
+ if [ -z "$ASSET_PATH" ] || [ ! -f "$ASSET_PATH" ]; then
+ echo "lime-cli asset missing: $ASSET_PATH" >&2
+ exit 1
+ fi
+
+ gh release upload "$RELEASE_TAG" "$ASSET_PATH" \
+ --repo "$GITHUB_REPOSITORY" \
+ --clobber
+
+ - name: Show sccache stats
+ if: always()
+ run: sccache --show-stats
diff --git a/README.md b/README.md
index 69c9acbdb..5e60631ff 100644
--- a/README.md
+++ b/README.md
@@ -86,7 +86,7 @@ Lime 是一个基于 Tauri 的桌面应用,面向创作者、内容团队与
- 基于 Aster Agent Runtime
- 支持会话、流式执行、技能调用、子任务接力与长期运行
-- 底层保留多 Provider 接入、凭证池、路由与协议兼容能力
+- 底层保留多 Provider API Key 接入、路由与协议兼容能力
---
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 6a0689fb8..d985fee6f 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,109 +1,96 @@
-## Lime v1.21.0
+## Lime v1.22.0
-发布日期:`2026-04-28`
+发布日期:`2026-04-29`
### 发布概览
-- 本次发布目标 tag 为 `v1.21.0`。
-- 本次发布聚焦稳定版自动更新与 R2 分发链路、OEM 云端商业闭环、工作台首页与侧栏体验、资源管理器、Provider 模型管理收口、主题外观与设置页更新。
-- 本轮待递交内容覆盖 Rust 后端、Tauri update command、DevBridge / mock、发布工作流、前端 Workspace / Settings / Provider Pool / Resource Manager、测试覆盖、版本锁文件与临时产物清理。
+- 本次发布目标 tag 为 `v1.22.0`。
+- 本次发布聚焦稳定版 GitHub Release / R2 分发链路收口、lime-cli 独立产物发布、Provider / Credential 旧路径清退、云端用户中心商业边界收口,以及 Agent 会话恢复与模型选择体验稳定性。
+- 本轮待递交内容覆盖 Rust 后端、Tauri 配置、发布工作流、release asset 脚本、Provider / Model / Credential 治理、前端 Workspace / Settings / Provider API Key 主路径、测试覆盖、版本锁文件与执行计划文档。
### 重点更新
-#### 1. 稳定版更新与 R2 发布链路
+#### 1. 版本号同步到 v1.22.0
-- `.github/workflows/release.yml` 补齐稳定版 updater 发布门禁,要求签名密钥与更新地址就绪后再生成 updater artifacts。
-- 发布流程会规范化 Tauri updater 公钥,并在仅产出 sidecar `.sig` 时由发布脚本生成稳定版 `latest.json`。
-- 新增 `scripts/release-updater-manifest.mjs`,聚合各平台 `latest.json`,生成统一 `latest.json`、版本化清单、R2 上传计划与 manifest metadata。
-- 新增 `scripts/plan-r2-release-cleanup.mjs`,按稳定版本窗口规划旧 R2 updater 产物清理,避免发布桶无限增长。
-- `scripts/release-updater-manifest.test.mjs` 覆盖平台缺失、版本不匹配、同名跨平台 artifact 与旧版本清理保护逻辑。
-- `src-tauri/src/commands/update_cmd.rs` 与 `src-tauri/crates/services/src/update_check_service.rs` 切到静态清单检查 + Tauri updater 安装主链,并补齐 semver 比较与缓存兜底。
-
-#### 2. 版本号同步到 v1.21.0
-
-- 应用版本已同步为 `1.21.0`:
+- 应用版本已同步为 `1.22.0`:
- `package.json`
- `package-lock.json`
- `src-tauri/Cargo.toml`
- `src-tauri/Cargo.lock`
- `src-tauri/tauri.conf.json`
- `src-tauri/tauri.conf.headless.json`
-- `packages/lime-cli-npm/package.json` 与 `packages/lime-cli-npm/README.md` 已同步到 `1.21.0`,保持 CLI wrapper 与桌面 release 版本一致。
-- 浏览器模式默认 mock 的 update current version 已同步为 `1.21.0`。
+- `packages/lime-cli-npm/package.json` 与 `packages/lime-cli-npm/README.md` 已同步到 `1.22.0`,保持 CLI wrapper 与桌面 release 版本一致。
+- 浏览器模式默认 mock 的 update current version 已同步为 `1.22.0`。
+- GitHub release asset staging 测试中的当前发布样例已同步到 `v1.22.0`。
-#### 3. OEM 云端商业闭环
+#### 2. 稳定版发布与 R2 分发链路
-- 新增 `src/lib/oemCloudPaymentReturn.ts`,统一生成、解析、暂存并分发 `lime://payment/return` 支付回跳事件。
-- `useDeepLink` 识别支付回跳 deep link,直接分发云端商业刷新事件,不再走旧 `handle_deep_link` 命令分支。
-- `useOemCloudAccess` 接入云端激活、支付配置、套餐订单、充值订单、账本、积分余额与访问令牌刷新主链。
-- 套餐购买和积分充值 checkout 支持 HTTPS bridge 回跳 URL,支付完成后自动刷新云端权益、积分余额与订单 watcher。
-- `docs/exec-plans/oem-cloud-commerce-loop-progress.md` 记录当前阶段、已清退的旧支付配置入口与下一轮真实渠道沙箱验证计划。
+- `.github/workflows/release.yml` 将桌面应用构建、GitHub Release 资产发布、R2 updater 发布和 lime-cli 资产发布拆成更清晰的阶段。
+- 新增 `scripts/prepare-github-release-assets.mjs`,在上传 GitHub Release 前统一整理资产名,避免 macOS `Lime.app.tar.gz` / `.sig` 同名跨架构冲突。
+- GitHub Release 上传改为使用 `release-github-assets` 暂存目录,并在资产上传后显式发布 release、标记 latest。
+- R2 updater 发布改为独立 job,从 GitHub Release 或 `RELEASE_NOTES.md` 准备 updater release notes,再生成稳定版 manifest。
+- Cloudflare R2 上传 / 列表 / 删除命令补齐 `--remote`,并在 wrangler 不支持 `r2 object list` 时跳过旧版本清理而不是阻塞发布。
+- lime-cli release binary 与 npm wrapper 资产改为独立矩阵 job 发布,保留 macOS / Windows / Linux CLI 产物,不再耦合桌面安装包矩阵。
-#### 4. 工作台首页、侧栏与导航体验
+#### 3. Provider / Credential 旧路径清退
-- `AppSidebar` 增加最近对话 / 归档会话架、分页加载、归档切换、外观切换、账户菜单与折叠态细节。
-- 新增 `src/components/app-sidebar/AppSidebarConversationShelf.tsx`,把会话架从侧栏主体中拆出,降低侧栏单体复杂度。
-- 工作台首页空态升级为“先开始这一轮 / 继续这轮 / 直接开工”入口,强化任务起手、推荐模板与继续上下文。
-- Workspace / Task Center / ChatNavbar / Inputbar / EmptyState / Team Workspace 等主路径继续收口视觉状态、运行时状态与回归断言。
-- `src/lib/windowControls.ts`、窗口 chrome 与主窗口启动链路继续补齐 macOS / headless 场景下的窗口控制一致性。
+- 清退旧 Provider Pool 页面、凭证卡片、Credential 表单、OAuth / Kiro / Antigravity / Claude OAuth / usage 等旧命令与服务路径。
+- Rust 后端删除旧 credential crate、provider pool DAO / service、Kiro credential handler、旧 provider converter / translator / fingerprint 模型等 dead surface。
+- 前端保留当前 API Key Provider 设置主路径,并继续收口模型启用、模型能力、Prompt Cache 与 companion provider 概览口径。
+- `agentCommandCatalog`、`legacySurfaceCatalog`、DevBridge mock 与相关测试同步更新,避免已删除命令继续作为 current surface 出现。
+- 模型资源索引删除旧 Antigravity / Kiro / Codex alias/provider 静态入口,减少 provider 真相源分叉。
-#### 5. 资源管理器
+#### 4. Agent 会话恢复与工作台稳定性
-- 新增 `src/features/resource-manager/`,提供资源管理器页面、侧栏、工具栏、预览面板、Inspector、搜索与导航意图。
-- 支持图片、文本、Markdown、PDF、Office、音视频、数据文件、压缩包与系统委托类型的分层预览渲染。
-- 支持资源下载、复制、系统打开、Finder 揭示、聊天位置与项目资源上下文回跳。
-- 补齐 `ResourceManagerPage`、资源预览搜索、会话状态和导航意图测试。
+- 会话切换 / 恢复详情默认按 `historyLimit: 40` 拉取近期历史,完整历史加载仍通过显式 `historyLimit: 0` 入口完成。
+- `useAsterAgentChat` 回归断言已同步新的 session detail 拉取参数,覆盖 stop refresh、timeline cache hydrate、workspace guard 与 stale 快照刷新路径。
+- 工作台消息流、模型选择、Provider selector、Team Workspace、artifact / saved content 展示继续保持与 runtime execution metadata 对齐。
+- `ModelSelector`、`useConfiguredProviders`、`useProviderModels`、Prompt Cache 支持判断与 companion provider overview 补齐回归覆盖。
-#### 6. Provider Pool 与设置页收口
+#### 5. 云端用户中心与商业边界
-- Provider 模型管理改为“启用的模型”左侧列表 + 添加模型面板,删除旧 API Key 列表 / Provider 表单 / 模型列表拆分组件。
-- 新增 `ModelProviderList`、`ModelAddPanel`、`providerConfigUtils` 与连接测试类型,统一 Provider 配置工具函数与 UI 入口。
-- 设置页 Provider、About、Developer、Experimental、Appearance、Channels 与 Automation 页面继续收口布局、状态展示和回归断言。
-- Prompt Cache 与 Anthropic-compatible 能力口径更新,避免把显式 `cache_control` 能力误显示为自动缓存。
+- 新增 `docs/exec-plans/cloud-commerce-user-center-boundary.md`,明确套餐购买、支付、账单、用量明细统一收敛到 `limecore` 用户中心网页。
+- Lime 客户端云端服务设置面继续收口为会话状态、当前套餐、积分余额、待支付提醒与用户中心跳转入口。
+- 客户端移除直接创建套餐 / 充值订单的旧处理面,避免本地商业工作台与用户中心形成双轨。
+- `useOemCloudAccess` 与 OEM cloud / LimeHub provider 同步测试继续覆盖登录态、权益摘要、API Key 与回跳刷新路径。
-#### 7. App Update 前端与 mock
+#### 6. 文档、治理与回归
-- `src/lib/api/appUpdate.ts` 扩展 release notes URL / pubDate / 错误信息字段。
-- `src/lib/tauri-mock/core.ts` 补齐 `check_update`、`check_for_updates` 与下载无更新态 mock,浏览器模式不再落入 unknown command。
-- About 设置页更新检查、下载失败、诊断错误和版本展示补齐测试覆盖。
-
-#### 8. 文档、治理与临时产物
-
-- `README.md` 更新产品定位文案:从“本地优先的 AI API Proxy 桌面应用”收敛为 AI Agent 创作工作台。
-- `src/lib/governance/legacySurfaceCatalog.json` 与测试补充新的 legacy surface 口径。
-- 删除根目录临时调试产物:`monitor.sh`、`network-before.md`、`post-hmr-state.png`、`tmp-e2e-home.png`、`knip.governance.json`。
-- 新增 `theme-scope-messages-ocean.png` 作为本轮主题视觉验证产物。
+- `docs/aiprompts/` 下 Provider、Credential Pool、Services、Hooks、Components、Overview 等导航文档同步当前 provider / credential / model registry 事实源。
+- `docs/content/03.providers/1.overview.md` 与 `src/components/provider-pool/api-key/README.md` 更新当前 Provider 配置入口说明。
+- `scripts/release-updater-manifest.test.mjs` 增加 GitHub release asset staging 覆盖,保护 macOS 同名 updater bundle 重命名逻辑。
+- `src-tauri/proptest-regressions/` 已纳入本轮待递交范围,保留 property test 回归种子。
### 待递交范围确认
-- 版本与发布:版本文件、lockfile、CLI wrapper、release workflow、R2 updater manifest / cleanup 脚本与测试。
-- Rust 主链:update command、update service、window chrome、runner/app 模块、DevBridge dispatcher、tray 事件与菜单处理。
-- 前端主链:AppSidebar、Workspace、Task Center、EmptyState、Inputbar、Team Workspace、Settings、Provider Pool、Resource Manager、MCP、Memory、SceneApps、Resources。
-- 商业闭环:OEM cloud control plane API、支付回跳 deep link、权益 / 积分 / 账本刷新、订单 watcher 与执行计划文档。
-- 验证与治理:新增/更新测试、legacy catalog、release updater contract、删除临时调试文件与旧 Provider Pool 组件。
+- 版本与发布:版本文件、lockfile、Tauri 配置、CLI wrapper、release workflow、GitHub release asset staging 脚本与测试。
+- Rust 主链:Provider / Credential / Server / Services / Agent / DevBridge / model registry / router / websocket 相关 current surface 收口。
+- 前端主链:Agent Chat Workspace、MessageList、ModelSelector、Settings Provider、API Key Provider、Provider hooks、mock 与治理目录册。
+- 商业边界:云端用户中心执行计划、OEM cloud access / LimeHub provider sync、设置页云端服务入口。
+- 验证与治理:新增/更新测试、legacy catalog、release updater contract、删除旧 Provider Pool / credential / Kiro / Antigravity 等 dead surface。
### 校验状态
- 已通过:
- `npm run verify:app-version`
- `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all`
- - `cargo test --manifest-path "src-tauri/Cargo.toml"` — 1112 passed / 0 failed / 2 ignored
+ - `cargo test --manifest-path "src-tauri/Cargo.toml"` — 1070 passed / 0 failed / 2 ignored
- `cargo clippy --manifest-path "src-tauri/Cargo.toml" --all-targets --all-features`
- `npm run lint`
- - `npm test` — 44 个 Vitest smart 批次通过
+ - `npx vitest run "src/components/agent/chat/hooks/useAsterAgentChat.test.tsx"`
+ - `npm test` — 43 个 Vitest smart 批次通过
- `npm run test:contracts`
- - `npm run smoke:agent-runtime-tool-surface`
- - `npm run smoke:agent-runtime-tool-surface-page`
- `git diff --check`
- `cargo test` 通过,当前存在 1 条预存 warning:
- `write_auxiliary_runtime_projection_fixture` 的 `dead_code`
-- `cargo clippy` 通过,当前存在 4 条预存 warning:
+- `cargo clippy` 通过,当前存在 6 条 warning:
- `crates/services/src/aster_session_store.rs` 的 `manual_repeat_n`
- - `crates/skills/src/lime_llm_provider.rs` 的 `too_many_arguments`
+ - `crates/skills/src/lime_llm_provider.rs` 的 2 处 `too_many_arguments`
+ - `crates/agent/src/request_tool_policy.rs` 的 `too_many_arguments`
- `crates/agent/src/session_execution_runtime.rs` 的 `needless_lifetimes`
- `src/services/runtime_evidence_pack_service.rs` 的 `dead_code`
-- GUI 主路径补充复测已通过:`smoke:agent-runtime-tool-surface` 与 `smoke:agent-runtime-tool-surface-page` 均确认 Harness 入口在执行态可见,修复此前等待 Harness 按钮超时的问题。
+- GUI 主路径未额外执行 `npm run verify:gui-smoke`;本轮发布收口以版本、发布链路、Provider / Credential 治理和前端 / Rust 回归为主要风险覆盖。
---
-**完整变更**: `v1.20.0` -> `v1.21.0`
+**完整变更**: `v1.21.0` -> `v1.22.0`
diff --git a/docs/aiprompts/README.md b/docs/aiprompts/README.md
index 985f8b07f..8198dfab4 100644
--- a/docs/aiprompts/README.md
+++ b/docs/aiprompts/README.md
@@ -55,7 +55,7 @@
### Provider 与数据
- `providers.md` - Provider 接入与认证方式
-- `credential-pool.md` - 凭证池、运行时路径、调试入口
+- `credential-pool.md` - 凭证池退役边界、启动清理与守卫
- `converter.md` - 协议转换与兼容层
- `database.md` - 数据库层与持久化
diff --git a/docs/aiprompts/aster-integration.md b/docs/aiprompts/aster-integration.md
index 154aca4c1..242167b4f 100644
--- a/docs/aiprompts/aster-integration.md
+++ b/docs/aiprompts/aster-integration.md
@@ -2,7 +2,7 @@
## 集成状态 ✅
-Lime 已完整集成 aster-rust 框架,包括凭证池桥接。
+Lime 已完整集成 aster-rust 框架。Provider 配置桥接已收敛到 API Key Provider。
## 当前事实源
@@ -18,13 +18,12 @@ Lime 已完整集成 aster-rust 框架,包括凭证池桥接。
- `aster_state.rs` - Agent 状态管理
- `aster_agent.rs` - Agent 包装器
- `event_converter.rs` - 事件转换器
-- `credential_bridge.rs` - 凭证池桥接
+- `credential_bridge.rs` - API Key Provider 桥接
**Tauri 命令** (`src-tauri/src/commands/aster_agent_cmd.rs`):
- `aster_agent_init` - 初始化 Agent
- `aster_agent_configure_provider` - 手动配置 Provider
-- `aster_agent_configure_from_pool` - 从凭证池配置 Provider(推荐)
- `aster_agent_status` - 获取状态
- `agent_runtime_submit_turn` - 统一提交 turn
- `agent_runtime_interrupt_turn` - 统一中断 turn
@@ -59,9 +58,9 @@ Lime 已完整集成 aster-rust 框架,包括凭证池桥接。
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────┐ │
-│ │ Lime 凭证池 │ │
-│ │ - ProviderPoolService │ │
+│ │ Lime Provider 配置 │ │
│ │ - ApiKeyProviderService │ │
+│ │ - ModelRegistryService │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
@@ -75,7 +74,7 @@ Lime 已完整集成 aster-rust 框架,包括凭证池桥接。
└─────────────────────────────────────────────────────────────────┘
```
-## 凭证池桥接
+## Provider 桥接
### 支持的凭证类型映射
@@ -83,12 +82,11 @@ Lime 已完整集成 aster-rust 框架,包括凭证池桥接。
| -------------------------- | -------------- |
| OpenAIKey | openai |
| ClaudeKey / AnthropicKey | anthropic |
-| KiroOAuth | bedrock |
-| GeminiOAuth / GeminiApiKey | google |
+| GeminiApiKey | google |
| VertexKey | gcpvertexai |
-| CodexOAuth | codex |
-| ClaudeOAuth | anthropic |
-| AntigravityOAuth | google |
+| Codex API Key | codex |
+
+Kiro / Gemini OAuth / Codex OAuth / Claude OAuth / Antigravity OAuth 均已退役,不再作为 Aster Provider 配置来源。
### 使用方式
@@ -127,4 +125,4 @@ await submitAgentRuntimeTurn({
- [overview.md](overview.md) - 项目架构
- [providers.md](providers.md) - Provider 系统
-- [credential-pool.md](credential-pool.md) - 凭证池管理
+- [credential-pool.md](credential-pool.md) - 凭证池退役说明
diff --git a/docs/aiprompts/commands.md b/docs/aiprompts/commands.md
index b0adadc86..dc5dde429 100644
--- a/docs/aiprompts/commands.md
+++ b/docs/aiprompts/commands.md
@@ -324,7 +324,7 @@ Companion 桌宠链路同样遵循这条路径。当前主入口为 `src/lib/api
Lime 主应用会在本地维护 `ws://127.0.0.1:45554/companion/pet` 的桌宠 companion 入口。前端如需感知桌宠连接状态,应继续通过 `companion-pet-status` 事件监听统一状态,不要在页面或 Hook 里自行直连本地 `WebSocket`。
-如果 companion 协议继续扩展,也应优先延续“Lime 做宿主、桌宠只收脱敏派生状态”的边界。例如 provider 凭证池相关能力,允许 Lime 通过 `companion_send_pet_command` 下发诸如 `pet.provider_overview` 这类脱敏摘要,并允许桌宠通过 `pet.open_provider_settings` 请求 Lime 聚焦主窗口并跳到 `设置 -> AI 服务商`,或通过 `pet.request_provider_overview_sync` 请求 Lime 立即重发最新的脱敏摘要;桌宠交互增强能力也应继续走这条主链,例如双击 / 三击桌宠后发出 `pet.request_pet_cheer`、`pet.request_pet_next_step`,或通过 `pet.request_chat_reply` 携带用户输入文本,请求 Lime 代为调用当前可聊天模型,再统一回写 `pet.show_bubble`;但不允许桌宠直接读取凭证文件、数据库或内部 `/v1/credentials/*` 完整凭证接口。
+如果 companion 协议继续扩展,也应优先延续“Lime 做宿主、桌宠只收脱敏派生状态”的边界。例如 provider 概览相关能力,允许 Lime 通过 `companion_send_pet_command` 下发诸如 `pet.provider_overview` 这类脱敏摘要,并允许桌宠通过 `pet.open_provider_settings` 请求 Lime 聚焦主窗口并跳到 `设置 -> AI 服务商`,或通过 `pet.request_provider_overview_sync` 请求 Lime 立即重发最新的脱敏摘要;桌宠交互增强能力也应继续走这条主链,例如双击 / 三击桌宠后发出 `pet.request_pet_cheer`、`pet.request_pet_next_step`,或通过 `pet.request_chat_reply` 携带用户输入文本,请求 Lime 代为调用当前可聊天模型,再统一回写 `pet.show_bubble`;但不允许桌宠直接读取凭证文件、数据库或内部 `/v1/credentials/*` 完整凭证接口。
## 命令契约的五个事实源
diff --git a/docs/aiprompts/components.md b/docs/aiprompts/components.md
index 6c2df71a4..1bfffdd88 100644
--- a/docs/aiprompts/components.md
+++ b/docs/aiprompts/components.md
@@ -9,7 +9,7 @@ React 组件层,使用 TailwindCSS 和 shadcn/ui。
```
src/components/
├── ui/ # 基础 UI 组件 (shadcn/ui)
-├── provider-pool/ # 凭证池管理
+├── provider-pool/api-key/ # API Key Provider 设置(目录名保留历史路径)
├── flow-monitor/ # 流量监控
├── general-chat/ # 兼容画布桥接(非对话主入口)
├── terminal/ # 已移除的历史终端页面路径(不要恢复)
@@ -39,24 +39,17 @@ export function AppSidebar() {
}
```
-### ProviderPool
+### ApiKeyProviderSection
-凭证池管理组件。
+API Key Provider 设置组件,是当前设置页的 Provider 配置入口。
```tsx
-// src/components/provider-pool/ProviderPoolPanel.tsx
-export function ProviderPoolPanel() {
- const { credentials, addCredential, removeCredential } = useProviderPool();
-
- return (
-
- );
-}
+// src/components/provider-pool/api-key/ApiKeyProviderSection.tsx
+import { ApiKeyProviderSection } from "@/components/provider-pool/api-key";
```
+旧 `ProviderPoolPage`、凭证卡片、OAuth 表单与 `useProviderPool` 已退役,不允许重新作为设置入口。
+
### FlowMonitor
流量监控组件。
diff --git a/docs/aiprompts/credential-pool.md b/docs/aiprompts/credential-pool.md
index a44676110..98440d874 100644
--- a/docs/aiprompts/credential-pool.md
+++ b/docs/aiprompts/credential-pool.md
@@ -1,229 +1,64 @@
-# 凭证池管理
+# 凭证池退役说明
-## 概述
+## 当前状态
-凭证池管理系统实现多凭证轮询负载均衡、健康检查和自动 Token 刷新。
+凭证池管理系统已退役,分类为 `dead`。
-## 核心组件
+后续 Provider 凭证与模型选择只允许收敛到以下 `current` 主路径:
-```
-src-tauri/src/
-├── credential/ # 凭证池核心
-│ ├── mod.rs
-│ ├── pool.rs # 凭证池实现
-│ └── health.rs # 健康检查
-└── services/
- ├── provider_pool_service.rs # 池服务
- └── token_cache_service.rs # Token 缓存
+- API Key Provider:应用内 Provider 配置、连接测试、模型发现与默认模型选择
+- configured providers:用户已配置 Provider 的事实源
+- 模型注册表:Provider / model 目录与协议能力事实源
+- 协议转换器:请求协议适配能力,例如 coding plan 仍可使用 `openai_to_antigravity`
+
+旧凭证池不再提供多凭证轮询、OAuth 登录、本地 CLI 凭证导入、健康检查、Token 自动刷新、使用量统计或管理页面。
+
+## 已下线范围
+
+- 前端凭证池管理页、旧凭证卡片、OAuth / 本地凭证表单、`useProviderPool` 与 `providerPool` API 网关
+- Rust `ProviderPoolService`、`TokenCacheService`、Kiro 事件服务、OAuth 命令与旧 provider pool 命令
+- Kiro / Qwen / Antigravity / Codex OAuth / Claude OAuth / Gemini OAuth 这类登录型或本地 CLI 凭证运行时
+- `provider_pool_credentials` 的运行时读取、健康写回与 fallback 选择
+
+## 数据处理
+
+启动期迁移会清理 Lime 管理的旧凭证池数据:
+
+- 清空 `provider_pool_credentials`
+- 删除 Lime 应用数据目录下托管的 `credentials/` 副本
+
+这只处理 Lime 自己复制和管理过的数据,不删除用户外部 CLI 原始目录,例如 `~/.codex`、`~/.gemini` 或其它第三方工具目录。
+
+## 保留边界
+
+以下内容不是凭证池功能,仍可继续演进:
+
+- API Key Provider 中的 OpenAI、Anthropic、Gemini API Key、OpenRouter、GitHub、Azure 等配置
+- 模型名或模型系列中出现的 `codex`、`gemini`、`qwen` 等字符串
+- 协议转换器,尤其是 `src-tauri/crates/providers/src/converter/openai_to_antigravity.rs`
+- server 内部短期用于桥接 API Key Provider 的兼容 DTO;它只能承载 current API Key Provider 数据,不代表凭证池恢复
+
+## 守卫
+
+旧 UI / Hook / API 文件路径已登记到 `src/lib/governance/legacySurfaceCatalog.json`,不允许重新接回前端入口。
+
+涉及 Provider 或命令边界时,至少执行:
+
+```bash
+npm run test:contracts
+npm run governance:legacy-report
```
-## 凭证池架构
+如果改动影响设置页或 Agent 运行主路径,再补:
-```
-┌─────────────────────────────────────────────────────────────────┐
-│ ProviderPoolService │
-│ ┌─────────────────────────────────────────────────────────────┐│
-│ │ Credential Pool ││
-│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││
-│ │ │ Cred 1 │ │ Cred 2 │ │ Cred 3 │ │ Cred N │ ││
-│ │ │ Healthy │ │ Healthy │ │ Expired │ │ Healthy │ ││
-│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ││
-│ │ │ │ │ │ ││
-│ │ └────────────┴────────────┴────────────┘ ││
-│ │ │ ││
-│ │ Round Robin ││
-│ └─────────────────────────┼───────────────────────────────────┘│
-│ │ │
-│ ┌─────────────────────────┼───────────────────────────────────┐│
-│ │ Health Checker (定时任务) ││
-│ │ - Token 过期检查 ││
-│ │ - 自动刷新 ││
-│ │ - 不健康凭证剔除 ││
-│ └─────────────────────────────────────────────────────────────┘│
-└─────────────────────────────────────────────────────────────────┘
-```
-
-## 负载均衡策略
-
-### Round Robin (轮询)
-
-```rust
-pub struct RoundRobinPool {
- credentials: Vec,
- current_index: AtomicUsize,
-}
-
-impl RoundRobinPool {
- pub fn next(&self) -> Option<&CredentialEntry> {
- let healthy: Vec<_> = self.credentials
- .iter()
- .filter(|c| c.is_healthy())
- .collect();
-
- if healthy.is_empty() {
- return None;
- }
-
- let index = self.current_index
- .fetch_add(1, Ordering::Relaxed) % healthy.len();
- Some(healthy[index])
- }
-}
-```
-
-### 权重轮询 (可选)
-
-```rust
-pub struct WeightedPool {
- credentials: Vec<(CredentialEntry, u32)>, // (凭证, 权重)
-}
-```
-
-## 健康检查
-
-### 检查项目
-
-| 检查项 | 说明 | 频率 |
-| ---------- | ------------------ | ------------ |
-| Token 过期 | 检查 expires_at | 每次请求前 |
-| Token 刷新 | 尝试刷新过期 Token | Token 过期时 |
-| API 可用性 | 发送测试请求 | 定时 (5分钟) |
-
-### 健康状态
-
-```rust
-pub enum HealthStatus {
- Healthy, // 健康
- TokenExpired, // Token 过期
- TokenRefreshing, // 正在刷新
- RefreshFailed(String), // 刷新失败
- Unhealthy(String), // 不健康
- Disabled, // 已禁用
-}
-```
-
-### 自动恢复
-
-```rust
-// 健康检查任务
-async fn health_check_task(pool: Arc) {
- loop {
- for credential in pool.credentials() {
- match credential.health_status() {
- HealthStatus::TokenExpired => {
- // 尝试刷新
- if let Err(e) = pool.refresh_token(&credential).await {
- credential.set_status(HealthStatus::RefreshFailed(e));
- }
- }
- HealthStatus::RefreshFailed(_) => {
- // 重试刷新 (最多 3 次)
- if credential.retry_count() < 3 {
- pool.retry_refresh(&credential).await;
- }
- }
- _ => {}
- }
- }
-
- tokio::time::sleep(Duration::from_secs(300)).await;
- }
-}
-```
-
-## Token 缓存
-
-### 缓存策略
-
-```rust
-pub struct TokenCacheService {
- cache: DashMap,
-}
-
-struct CachedToken {
- access_token: String,
- expires_at: i64,
- refresh_token: String,
-}
-
-impl TokenCacheService {
- pub async fn get_or_refresh(&self, credential_id: &str) -> Result {
- if let Some(cached) = self.cache.get(credential_id) {
- if !cached.is_expired() {
- return Ok(cached.access_token.clone());
- }
- }
-
- // 刷新并缓存
- let new_token = self.refresh(credential_id).await?;
- self.cache.insert(credential_id.to_string(), new_token.clone());
- Ok(new_token.access_token)
- }
-}
-```
-
-### 数据库持久化
-
-```sql
-CREATE TABLE token_cache (
- credential_id TEXT PRIMARY KEY,
- access_token TEXT NOT NULL,
- refresh_token TEXT NOT NULL,
- expires_at INTEGER NOT NULL,
- updated_at INTEGER NOT NULL
-);
-```
-
-## 凭证生命周期
-
-```
-┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
-│ 上传 │ ──▶ │ 验证 │ ──▶ │ 激活 │ ──▶ │ 使用中 │
-└─────────┘ └─────────┘ └─────────┘ └────┬────┘
- │
- ┌────────────────────────────────┘
- │
- ▼
- ┌─────────┐ ┌─────────┐ ┌─────────┐
- │ 过期 │ ──▶ │ 刷新 │ ──▶ │ 恢复 │
- └─────────┘ └────┬────┘ └─────────┘
- │
- ▼ (失败)
- ┌─────────┐
- │ 禁用 │
- └─────────┘
-```
-
-## API 接口
-
-### Tauri Commands
-
-```rust
-#[tauri::command]
-async fn add_credential(provider: String, path: String) -> Result<()>;
-
-#[tauri::command]
-async fn remove_credential(id: String) -> Result<()>;
-
-#[tauri::command]
-async fn list_credentials() -> Result>;
-
-#[tauri::command]
-async fn refresh_credential(id: String) -> Result<()>;
-
-#[tauri::command]
-async fn get_pool_status() -> Result;
+```bash
+npm run verify:local
+npm run verify:gui-smoke
```
## 相关文档
-- [providers.md](providers.md) - Provider 系统
-- [services.md](services.md) - 业务服务
-- [database.md](database.md) - 数据库层
-
-## 运行时路径与调试
-
-- 凭证文件默认存放在应用数据目录下的 `lime/credentials/`
-- `~/Library/Application Support/lime/credentials/` 只作为 macOS 示例,Windows 必须使用对应的应用数据目录
-- `request_logs`、日志目录等运行时路径也应通过统一 `app_paths` / 系统目录 API 获取,不要在实现里写死
-- 需要排查 Kiro 凭证加载时,可使用 `debug_kiro_credentials` 对应命令进行诊断;具体命令边界以 `docs/aiprompts/commands.md` 和 Rust 注册表为准
+- [providers.md](providers.md) - Provider current 主路径
+- [commands.md](commands.md) - Tauri 命令边界
+- [database.md](database.md) - 数据库层与启动迁移
+- [converter.md](converter.md) - 协议转换
diff --git a/docs/aiprompts/database.md b/docs/aiprompts/database.md
index 1adc44be0..73a3f2cce 100644
--- a/docs/aiprompts/database.md
+++ b/docs/aiprompts/database.md
@@ -103,4 +103,4 @@ pub fn run_migrations(conn: &Connection) -> Result<()> {
## 相关文档
- [services.md](services.md) - 业务服务
-- [credential-pool.md](credential-pool.md) - 凭证池管理
+- [credential-pool.md](credential-pool.md) - 凭证池退役说明
diff --git a/docs/aiprompts/hooks.md b/docs/aiprompts/hooks.md
index 02786951b..6c9090657 100644
--- a/docs/aiprompts/hooks.md
+++ b/docs/aiprompts/hooks.md
@@ -9,8 +9,7 @@
```
src/hooks/
├── index.ts # 导出入口
-├── useProviderPool.ts # 凭证池管理
-├── useOAuthCredentials.ts # OAuth 凭证
+├── useConfiguredProviders.ts # 已配置 Provider 读取
├── useFlowEvents.ts # 流量事件
├── useMcpServers.ts # MCP 服务器
├── useDeepLink.ts # Deep Link 处理
@@ -23,6 +22,7 @@ src/hooks/
- 历史 `useTauri.ts` 兼容聚合层已删除,不要重新引入新的“大一统 API Hook”。
- Agent 工作台统一走 `src/components/agent/chat/hooks/index.ts` 暴露的 `useAgentChatUnified`,底层实现委托 `useAsterAgentChat`。
- 历史 `@/hooks/useUnifiedChat` 与 `src/lib/api/unified-chat.ts` 已删除,不要重建 compat Hook / API。
+- 凭证池 Hook `useProviderPool` 已删除;Provider 配置只允许走 API Key Provider / configured providers 主路径。
## 核心 Hooks
@@ -40,37 +40,9 @@ src/hooks/
- Hook 实现:`src/components/agent/chat/hooks/useAsterAgentChat.ts`
- API 封装:`src/lib/api/agentRuntime.ts`
-### useProviderPool
+### useConfiguredProviders
-```typescript
-export function useProviderPool() {
- const [credentials, setCredentials] = useState([]);
- const [loading, setLoading] = useState(false);
-
- const refresh = async () => {
- setLoading(true);
- const list = await invoke("list_credentials");
- setCredentials(list);
- setLoading(false);
- };
-
- const addCredential = async (provider: string, path: string) => {
- await invoke("add_credential", { provider, filePath: path });
- await refresh();
- };
-
- const removeCredential = async (id: string) => {
- await invoke("remove_credential", { id });
- await refresh();
- };
-
- useEffect(() => {
- refresh();
- }, []);
-
- return { credentials, loading, addCredential, removeCredential, refresh };
-}
-```
+Provider 列表与默认模型读取应优先复用 `src/lib/api/modelRegistry.ts`、`src/lib/api/appConfigTypes.ts` 和现有 configured provider Hook,不要重新创建凭证池 Hook 或 OAuth Hook。
### useFlowEvents
@@ -117,7 +89,7 @@ export function useDeepLink() {
### 命名约定
- 以 `use` 开头
-- 描述功能: `useProviderPool`, `useFlowEvents`
+- 描述功能: `useConfiguredProviders`, `useFlowEvents`
### 返回值
diff --git a/docs/aiprompts/overview.md b/docs/aiprompts/overview.md
index 548c3edf4..749fdbce2 100644
--- a/docs/aiprompts/overview.md
+++ b/docs/aiprompts/overview.md
@@ -8,7 +8,7 @@ Lime 是一个以创作为中心的本地优先 AI Agent 交互工作台,基
1. **产品层**:Workspace、通用工作区 / Harness、Agent 对话、Skills、Artifact/Canvas、记忆与风格
2. **能力层**:MCP、浏览器运行时、终端、插件、批量/心跳、Claw 渠道
-3. **基础设施层**:Aster Agent、Provider 凭证池、协议兼容、路由、服务器、数据库与监控
+3. **基础设施层**:Aster Agent、API Key Provider、模型注册表、协议兼容、路由、服务器、数据库与监控
其中,Provider 接入、协议兼容与运行时服务共同构成底层能力底座。
@@ -90,11 +90,11 @@ lime/
| 模块 | 说明 |
|------|------|
| `src-tauri/src/agent/` | Aster Agent 集成、会话历史、线程读模型、工具注册与流式桥接 |
-| `providers/` | LLM Provider 认证和 API 实现 |
+| `providers/` | LLM Provider API 实现与协议兼容 |
| `services/` | 业务服务层 |
| `converter/` | 协议转换与兼容层 |
| `server/` | HTTP API 服务器 |
-| `credential/` | 凭证池管理 |
+| `api_key_provider` | API Key Provider 配置与凭证选择 |
| `flow_monitor/` | 流量监控 |
| `database/` | 数据持久化与 DAO |
@@ -169,10 +169,10 @@ lime/
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
-│ Provider Pool Service │
+│ API Key Provider / Model Registry │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
-│ │ 凭证轮询 │ │ 健康检查 │ │ Token 刷新 │ │
-│ │ (负载均衡) │ │ (自动剔除) │ │ (OAuth) │ │
+│ │ Provider │ │ 模型目录 │ │ 协议能力 │ │
+│ │ 配置选择 │ │ 解析 │ │ 判断 │ │
│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘ │
└─────────┼────────────────┼─────────────────────┼────────────────┘
│ │ │
@@ -219,9 +219,10 @@ lime/
- `write_file`、画布联动与主题工作流负责把过程沉淀成交付物
### 7. 多 Provider 与兼容层
-- OAuth 与 API Key Provider 并存
-- 凭证池、模型路由、协议兼容与 HTTP Server 作为底层支撑
+- Provider 配置统一走 API Key Provider / configured providers
+- 模型注册表、协议兼容与 HTTP Server 作为底层支撑
- Prompt Cache 等运行时能力按 ProviderType 判断;`anthropic-compatible` 只表示 Anthropic wire format 兼容,不等于自动 Prompt Cache 能力
+- 旧凭证池、OAuth 与本地 CLI credential runtime 已退役;Antigravity 仅保留协议转换器能力
### 8. 本地优先与可扩展
- 桌面应用、本地工作区、插件与外部工具扩展
@@ -241,7 +242,7 @@ lime/
### 基础设施
- [providers.md](providers.md) - Provider 系统
-- [credential-pool.md](credential-pool.md) - 凭证池管理
+- [credential-pool.md](credential-pool.md) - 凭证池退役说明
- [converter.md](converter.md) - 协议转换
- [server.md](server.md) - HTTP 服务器
diff --git a/docs/aiprompts/providers.md b/docs/aiprompts/providers.md
index 48d16fe63..04a28a7aa 100644
--- a/docs/aiprompts/providers.md
+++ b/docs/aiprompts/providers.md
@@ -2,7 +2,7 @@
## 概述
-Provider 系统负责与各 LLM 服务商的认证和 API 交互。支持 OAuth 和 API Key 两种认证方式。
+Provider 系统负责与各 LLM 服务商的 API 交互。当前认证事实源是 API Key Provider / configured providers;旧 OAuth 与本地 CLI 凭证池运行时已退役。
如果需求同时涉及“候选模型解析、OEM 与本地 provider 协同、自动与设置平衡、成本/限额事件”,继续补读:
@@ -16,15 +16,11 @@ src-tauri/src/providers/
├── mod.rs # 模块入口和 Provider 枚举
├── traits.rs # Provider trait 定义
├── error.rs # 错误类型
-├── kiro.rs # Kiro/CodeWhisperer OAuth
-├── gemini.rs # Gemini OAuth
-├── qwen.rs # Qwen OAuth
-├── antigravity.rs # Antigravity OAuth
-├── claude_oauth.rs # Claude OAuth
+├── gemini.rs # Gemini API Key 请求支持
+├── antigravity.rs # Antigravity 协议兼容支持
├── claude_custom.rs # Claude API Key
├── openai_custom.rs # OpenAI API Key
-├── codex.rs # Codex Provider
-├── iflow.rs # iFlow Provider
+├── codex.rs # OpenAI Responses / Codex 兼容请求支持
├── vertex.rs # Vertex AI Provider
└── tests.rs # 单元测试
```
@@ -33,15 +29,10 @@ src-tauri/src/providers/
```rust
pub enum ProviderType {
- Kiro, // Kiro/CodeWhisperer OAuth
- Gemini, // Google Gemini OAuth
- Qwen, // 通义千问 OAuth
- Antigravity, // Antigravity (Gemini CLI) OAuth
- ClaudeOAuth, // Claude OAuth
ClaudeCustom, // Claude API Key
OpenAICustom, // OpenAI API Key
- Codex, // Codex
- IFlow, // iFlow
+ GeminiApiKey, // Gemini API Key
+ Codex, // OpenAI Responses / Codex 兼容 API Key
Vertex, // Vertex AI
}
```
@@ -67,40 +58,11 @@ pub trait Provider: Send + Sync {
}
```
-## OAuth Provider 实现
+## 已退役 Provider
-### Kiro Provider
+Kiro / Qwen / Antigravity OAuth / Codex OAuth / Claude OAuth / Gemini OAuth 都属于旧凭证池功能,分类为 `dead`。不得重新接回设置页、Tauri 命令、Token 刷新任务或运行时 fallback。
-```rust
-// 凭证文件结构
-struct KiroCredential {
- access_token: String,
- refresh_token: String,
- expires_at: i64,
- client_id: Option, // 从 clientIdHash 合并
- client_secret: Option, // 从 clientIdHash 合并
-}
-
-// Token 刷新流程
-1. 检查 expires_at 是否过期
-2. 使用 refresh_token 请求新 token
-3. 更新凭证文件
-```
-
-### Gemini Provider
-
-```rust
-// OAuth 端点
-const AUTH_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth";
-const TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
-
-// 凭证文件结构
-struct GeminiCredential {
- access_token: String,
- refresh_token: String,
- expires_at: i64,
-}
-```
+Antigravity 的协议转换能力不是凭证池功能,`openai_to_antigravity` converter 仍可用于 coding plan。
## API Key Provider 实现
@@ -163,64 +125,16 @@ Lime 当前把 Prompt Cache 能力视为 **Provider 显式声明优先、类型
2. 上游服务是否真的声明支持 Anthropic Automatic Prompt Caching
3. 响应 usage 中是否存在 `cache_creation_input_tokens` / `cache_read_input_tokens` / `cached_input_tokens`
-## 凭证管理策略
-
-### 方案 B: 独立副本策略
-
-```
-原始凭证文件 (用户上传)
- │
- ▼
-┌─────────────────────────────────────┐
-│ 合并 clientIdHash 中的 │
-│ client_id / client_secret │
-└─────────────────────────────────────┘
- │
- ▼
-副本凭证文件 (credentials/ 目录)
- │
- ▼
-独立刷新和管理
-```
-
-优点:
-- 每个副本完全独立
-- 支持多账号场景
-- 不影响原始文件
-
-## 健康检查
-
-```rust
-// 健康检查逻辑
-async fn health_check(&self, credential: &CredentialData) -> HealthStatus {
- // 1. 检查 Token 是否过期
- if self.is_token_expired(credential) {
- return HealthStatus::TokenExpired;
- }
-
- // 2. 尝试刷新 Token
- if let Err(e) = self.refresh_token(credential).await {
- return HealthStatus::RefreshFailed(e);
- }
-
- // 3. 发送测试请求
- match self.send_test_request(credential).await {
- Ok(_) => HealthStatus::Healthy,
- Err(e) => HealthStatus::Unhealthy(e),
- }
-}
-```
-
## 添加新 Provider
1. 在 `providers/` 创建新模块文件
2. 实现 `Provider` trait
3. 在 `ProviderType` 枚举添加新类型
-4. 在 `ProviderPoolService` 注册健康检查
-5. 更新前端 Provider 选择器
+4. 同步 API Key Provider schema、模型注册表与连接测试
+5. 更新前端 Provider 选择器与文档
## 相关文档
-- [credential-pool.md](credential-pool.md) - 凭证池管理
+- [credential-pool.md](credential-pool.md) - 凭证池退役说明
- [converter.md](converter.md) - 协议转换
- [server.md](server.md) - HTTP 服务器
diff --git a/docs/aiprompts/services.md b/docs/aiprompts/services.md
index b2d33045f..23933c0ae 100644
--- a/docs/aiprompts/services.md
+++ b/docs/aiprompts/services.md
@@ -9,8 +9,8 @@
```
src-tauri/src/services/
├── mod.rs # 模块入口
-├── provider_pool_service.rs # 凭证池服务
-├── token_cache_service.rs # Token 缓存
+├── api_key_provider_service.rs # API Key Provider 服务
+├── model_registry_service.rs # 模型注册表服务
├── mcp_service.rs # MCP 服务器管理
├── prompt_service.rs # Prompt 管理
├── skill_service.rs # 技能管理
@@ -23,45 +23,14 @@ src-tauri/src/services/
> 注意:`general_chat/` 兼容壳已删除。
> 新功能与新治理都应直接落到 `agent_runtime_*` 与现役 `agent/chat` 体系,不要重新引回旧入口。
-> `ProviderPoolService::select_credential_with_fallback_legacy` 也已删除,凭证选择统一走现役 `select_credential_with_fallback`。
+> 旧 `ProviderPoolService`、`TokenCacheService` 与 OAuth/local CLI credential runtime 已退役。凭证选择统一走 `ApiKeyProviderService`。
-### ProviderPoolService
+### ApiKeyProviderService
```rust
-pub struct ProviderPoolService {
- pools: HashMap,
- health_checker: HealthChecker,
-}
-
-impl ProviderPoolService {
- /// 获取下一个可用凭证
- pub async fn next_credential(&self, provider: ProviderType) -> Option;
-
- /// 添加凭证到池
- pub async fn add_credential(&self, credential: Credential) -> Result<()>;
-
- /// 移除凭证
- pub async fn remove_credential(&self, id: &str) -> Result<()>;
-
- /// 启动健康检查
- pub fn start_health_check(&self);
-}
-```
-
-### TokenCacheService
-
-```rust
-pub struct TokenCacheService {
- cache: DashMap,
- db: Arc,
-}
-
-impl TokenCacheService {
- /// 获取或刷新 Token
- pub async fn get_or_refresh(&self, credential_id: &str) -> Result;
-
- /// 使 Token 失效
- pub async fn invalidate(&self, credential_id: &str);
+impl ApiKeyProviderService {
+ /// 选择当前 Provider 可用的 API Key 配置
+ pub async fn select_credential_for_provider(&self, provider_id: &str) -> Result;
}
```
@@ -87,24 +56,10 @@ impl McpService {
## 服务注入
```rust
-// 在 main.rs 中初始化
-let pool_service = Arc::new(ProviderPoolService::new());
-let token_cache = Arc::new(TokenCacheService::new(db.clone()));
-
-app.manage(pool_service);
-app.manage(token_cache);
-
-// 在命令中使用
-#[tauri::command]
-async fn add_credential(
- pool: State<'_, Arc>,
- // ...
-) -> Result<(), String> {
- pool.add_credential(credential).await
-}
+// 服务由 bootstrap / state 注入,命令层不再管理凭证池服务。
```
## 相关文档
- [commands.md](commands.md) - Tauri 命令
-- [credential-pool.md](credential-pool.md) - 凭证池管理
+- [credential-pool.md](credential-pool.md) - 凭证池退役说明
diff --git a/docs/content/03.providers/1.overview.md b/docs/content/03.providers/1.overview.md
index e8532cfab..d999a1f81 100644
--- a/docs/content/03.providers/1.overview.md
+++ b/docs/content/03.providers/1.overview.md
@@ -22,17 +22,17 @@ Provider 文档正在按 LimeNext V2 current 连接入口重建。
先补可用 API Key,再读取真实模型目录、做连接验证,并校准默认模型与兼容协议
- `中转服务`
对应 Lime Connect。
- 浏览已验证中转商,获取 API Key 后通过 `lime://connect` 深链一键带入凭证池
+ 浏览已验证中转商,获取 API Key 后通过 `lime://connect` 深链接入 API Key Provider
- `语音服务`
单独管理语音相关 Provider
-- `OAuth 凭证`
- 管理 Kiro、Gemini、Antigravity、Codex、Claude OAuth 等登录型凭证
+- `协议转换`
+ 保留请求协议适配能力,例如 coding plan 可继续使用 Antigravity 协议转换器
## 当前建议
1. 默认先从 `服务商` 分类补可用 API Key
-2. 只有需要登录型凭证时,再进入 `OAuth 凭证`
-3. 需要浏览中转商时,再进入 `中转服务`
+2. 需要浏览中转商时,再进入 `中转服务`
+3. 不再配置 Kiro、Gemini OAuth、Antigravity OAuth、Codex OAuth、Claude OAuth 等登录型凭证
4. 不再以旧 YAML 配置文档作为日常配置入口
## 后续说明
diff --git a/docs/exec-plans/README.md b/docs/exec-plans/README.md
index fa3d06f7a..5ec2059ca 100644
--- a/docs/exec-plans/README.md
+++ b/docs/exec-plans/README.md
@@ -27,6 +27,7 @@
- 参考运行时主链总计划:`docs/exec-plans/upstream-runtime-alignment-plan.md`
- 参考运行时主链进度日志:`docs/exec-plans/upstream-runtime-alignment-progress.md`
- Provider 模型能力 taxonomy 进度日志:`docs/exec-plans/provider-model-taxonomy-progress.md`
+- 云端套餐与支付边界收口计划:`docs/exec-plans/cloud-commerce-user-center-boundary.md`
- `@` 命令本地执行纠偏计划:`docs/exec-plans/at-command-local-execution-alignment-plan.md`
- LimeNext 总实施计划(`legacy current reference`,当前主规划已切到 `docs/roadmap/limenextv2/README.md`):`docs/exec-plans/limenext-plan.md`
- LimeNext 推进日志:`docs/exec-plans/limenext-progress.md`
diff --git a/docs/exec-plans/cloud-commerce-user-center-boundary.md b/docs/exec-plans/cloud-commerce-user-center-boundary.md
new file mode 100644
index 000000000..589bc65aa
--- /dev/null
+++ b/docs/exec-plans/cloud-commerce-user-center-boundary.md
@@ -0,0 +1,25 @@
+# 云端套餐与支付边界收口执行计划
+
+## 主目标
+
+把套餐购买、支付、账单、用量明细统一收敛到 `limecore` 用户中心网页;Lime 客户端只保留会话状态、当前套餐、积分余额、待支付提醒和跳转入口。
+
+## 事实源
+
+- `current`:`limecore` control-plane 与 `apps/user-center-web` 的 `/pricing`、`/billing`、`/subscription`、`/credits`
+- `current`:Lime 客户端 `useOemCloudAccess` 负责登录态、权益摘要、API Key、支付回跳同步
+- `dead`:Lime 客户端内置套餐卡、充值包卡、用量图、账单表、直接创建购买订单的本地商业工作台
+
+## 本轮进度
+
+- 已将 Lime 设置页云端服务面收口为摘要卡 + 用户中心入口。
+- 已从客户端设置页移除本地套餐购买、积分充值、用量明细和账单表渲染。
+- 已从 `useOemCloudAccess` 返回面移除客户端直接创建套餐/充值订单的处理器。
+- 已在 `limecore` 用户中心 `/billing` 补齐用量与账单 tab,使用真实 usage / credits / billing dashboard 和真实 checkout。
+- 已将 `limecore` 用户中心 `/pricing` 文案从“模型价格”收敛为“套餐与价格”。
+- 已让 Lime 客户端入口直达 `/billing?tab=usage` 与 `/billing?tab=billing`,并让用户中心 tab 与 URL 查询参数同步。
+- 已从用户中心主导航移除独立“账单管理”菜单,保留 `/subscription` 直接路由作为旧链接可达页面,主导航收敛到 `/billing`。
+
+## 下一刀
+
+继续验证两个仓库的定向类型检查与回归测试;如果后续发现客户端仍存在套餐购买 UI 文案或旧 testid,应继续按 `dead` 删除,不再迁回客户端。
diff --git a/docs/ops.md b/docs/ops.md
index 06963ed98..50d5b0cdf 100644
--- a/docs/ops.md
+++ b/docs/ops.md
@@ -25,7 +25,7 @@
## 数据与日志位置
- SQLite 数据库:`~/.lime/lime.db`
-- 凭证池副本目录:macOS `~/Library/Application Support/lime/credentials/`,Linux `~/.local/share/lime/credentials/`,Windows `%APPDATA%\\lime\\credentials\\`
+- 旧凭证池副本目录:macOS `~/Library/Application Support/lime/credentials/`,Linux `~/.local/share/lime/credentials/`,Windows `%APPDATA%\\lime\\credentials\\`;启动期会清理 Lime 管理的副本
- OAuth/Token 目录(默认):`~/.lime/auth/`
- 日志目录:`~/.lime/logs/`
- 请求日志目录:`~/.lime/request_logs/`
@@ -41,7 +41,7 @@
- 复制以下路径:
- 配置文件(macOS: `~/Library/Application Support/lime/config.yaml`,Linux: `~/.config/lime/config.yaml`,Windows: `%APPDATA%\\lime\\config.yaml`)
- 配置备份文件:`config.yaml.backup`
- - 凭证池副本目录(macOS: `~/Library/Application Support/lime/credentials/`,Linux: `~/.local/share/lime/credentials/`,Windows: `%APPDATA%\\lime\\credentials\\`)
+ - 旧凭证池副本目录(macOS: `~/Library/Application Support/lime/credentials/`,Linux: `~/.local/share/lime/credentials/`,Windows: `%APPDATA%\\lime\\credentials\\`)
- `~/.lime/lime.db`
- `~/.lime/auth/`(如需要保留 OAuth/Token)
- `~/.lime/logs/`、`~/.lime/request_logs/`(如需保留日志)
diff --git a/homebrew/Casks/lime.rb b/homebrew/Casks/lime.rb
index a6b302271..13dad443b 100644
--- a/homebrew/Casks/lime.rb
+++ b/homebrew/Casks/lime.rb
@@ -12,7 +12,7 @@ cask "lime" do
end
name "Lime"
- desc "AI 代理服务桌面应用 - 多 Provider 凭证池管理"
+ desc "AI 代理服务桌面应用 - 多 Provider API Key 管理"
homepage "https://github.com/aiclientproxy/lime"
livecheck do
diff --git a/package-lock.json b/package-lock.json
index ac9a8356e..2c0f4ae2d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "lime",
- "version": "1.21.0",
+ "version": "1.22.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lime",
- "version": "1.21.0",
+ "version": "1.22.0",
"dependencies": {
"@babel/standalone": "^7.29.0",
"@fabianlars/tauri-plugin-oauth": "^2",
diff --git a/package.json b/package.json
index 87d283c79..30ba6c241 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "lime",
"private": true,
- "version": "1.21.0",
+ "version": "1.22.0",
"type": "module",
"engines": {
"node": ">=22.0.0"
diff --git a/packages/lime-cli-npm/README.md b/packages/lime-cli-npm/README.md
index 13209c7f4..54057c5c2 100644
--- a/packages/lime-cli-npm/README.md
+++ b/packages/lime-cli-npm/README.md
@@ -112,7 +112,7 @@ npm run build:release -- \
```bash
npm run build:release -- \
--target-triple "aarch64-apple-darwin" \
- --version "1.21.0" \
+ --version "1.22.0" \
--out-dir "./dist"
```
diff --git a/packages/lime-cli-npm/package.json b/packages/lime-cli-npm/package.json
index d0141c84e..5b44bc88f 100644
--- a/packages/lime-cli-npm/package.json
+++ b/packages/lime-cli-npm/package.json
@@ -1,6 +1,6 @@
{
"name": "@limecloud/lime-cli",
- "version": "1.21.0",
+ "version": "1.22.0",
"description": "Lime 官方任务 CLI",
"bin": {
"lime": "scripts/run.js"
diff --git a/scripts/prepare-github-release-assets.mjs b/scripts/prepare-github-release-assets.mjs
new file mode 100644
index 000000000..fb24e783c
--- /dev/null
+++ b/scripts/prepare-github-release-assets.mjs
@@ -0,0 +1,213 @@
+#!/usr/bin/env node
+
+import fs from "node:fs";
+import path from "node:path";
+import process from "node:process";
+import { fileURLToPath } from "node:url";
+
+function parseArgs(argv) {
+ const args = {
+ extraAsset: [],
+ };
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const item = argv[index];
+ if (!item.startsWith("--")) {
+ continue;
+ }
+
+ const key = item.slice(2);
+ const next = argv[index + 1];
+ const value = !next || next.startsWith("--") ? "true" : next;
+ if (value !== "true") {
+ index += 1;
+ }
+
+ if (key === "extra-asset") {
+ args.extraAsset.push(value);
+ } else {
+ args[key] = value;
+ }
+ }
+
+ return args;
+}
+
+function normalizeVersion(value) {
+ return String(value || "")
+ .trim()
+ .replace(/^v/, "");
+}
+
+function listFilesRecursive(root) {
+ if (!fs.existsSync(root)) {
+ return [];
+ }
+
+ return fs
+ .readdirSync(root, { withFileTypes: true })
+ .flatMap((entry) => {
+ const filePath = path.join(root, entry.name);
+ if (entry.isDirectory()) {
+ return listFilesRecursive(filePath);
+ }
+ return entry.isFile() ? [filePath] : [];
+ })
+ .sort();
+}
+
+function isPathInside(parent, child) {
+ const relative = path.relative(parent, child);
+ return (
+ Boolean(relative) &&
+ !relative.startsWith("..") &&
+ !path.isAbsolute(relative)
+ );
+}
+
+function targetFromAssetPath(assetsDir, filePath) {
+ if (!isPathInside(assetsDir, filePath)) {
+ return "";
+ }
+
+ return path.relative(assetsDir, filePath).split(path.sep)[0] || "";
+}
+
+function duplicateTargetLabel(target) {
+ if (target === "aarch64-apple-darwin") {
+ return "macos-arm64";
+ }
+ if (target === "x86_64-apple-darwin") {
+ return "macos-x64";
+ }
+ if (target === "x86_64-pc-windows-msvc") {
+ return "windows-x64";
+ }
+ if (target === "x86_64-unknown-linux-gnu") {
+ return "linux-x64";
+ }
+ return target.replace(/[^A-Za-z0-9._-]+/g, "-") || "asset";
+}
+
+function macUpdaterAssetName(basename, target, version) {
+ let arch = "";
+ if (target === "aarch64-apple-darwin") {
+ arch = "aarch64";
+ } else if (target === "x86_64-apple-darwin") {
+ arch = "x64";
+ }
+
+ if (!arch) {
+ return "";
+ }
+ if (basename === "Lime.app.tar.gz") {
+ return `Lime_${version}_${arch}.app.tar.gz`;
+ }
+ if (basename === "Lime.app.tar.gz.sig") {
+ return `Lime_${version}_${arch}.app.tar.gz.sig`;
+ }
+ return "";
+}
+
+function githubAssetName(filePath, context) {
+ const basename = path.basename(filePath);
+ const target = targetFromAssetPath(context.assetsDir, filePath);
+ const duplicateCount = context.basenameCounts.get(basename) || 0;
+
+ if (duplicateCount <= 1) {
+ return basename;
+ }
+
+ const macName = macUpdaterAssetName(basename, target, context.version);
+ if (macName) {
+ return macName;
+ }
+
+ return `${duplicateTargetLabel(target)}-${basename}`;
+}
+
+function prepareGitHubReleaseAssets(options) {
+ const assetsDir = path.resolve(options.assetsDir || "release-assets");
+ const outDir = path.resolve(options.outDir || "release-github-assets");
+ const version = normalizeVersion(options.version);
+ const extraAssets = (options.extraAssets || []).map((item) =>
+ path.resolve(item),
+ );
+
+ if (!version) {
+ throw new Error("version is required");
+ }
+ if (!fs.existsSync(assetsDir)) {
+ throw new Error(`assets directory is missing: ${assetsDir}`);
+ }
+
+ const releaseAssetFiles = listFilesRecursive(assetsDir).filter(
+ (filePath) => !/^latest.*\.json$/i.test(path.basename(filePath)),
+ );
+ const inputFiles = [...releaseAssetFiles, ...extraAssets].sort();
+
+ for (const filePath of inputFiles) {
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
+ throw new Error(`release asset is missing: ${filePath}`);
+ }
+ }
+
+ const basenameCounts = new Map();
+ for (const filePath of inputFiles) {
+ const basename = path.basename(filePath);
+ basenameCounts.set(basename, (basenameCounts.get(basename) || 0) + 1);
+ }
+
+ fs.rmSync(outDir, { recursive: true, force: true });
+ fs.mkdirSync(outDir, { recursive: true });
+
+ const usedNames = new Set();
+ const copied = [];
+ for (const filePath of inputFiles) {
+ const name = githubAssetName(filePath, {
+ assetsDir,
+ basenameCounts,
+ version,
+ });
+ if (usedNames.has(name)) {
+ throw new Error(`duplicate GitHub release asset name: ${name}`);
+ }
+ usedNames.add(name);
+
+ const destination = path.join(outDir, name);
+ fs.copyFileSync(filePath, destination);
+ copied.push({
+ name,
+ source: filePath,
+ destination,
+ });
+ }
+
+ return copied.sort((left, right) => left.name.localeCompare(right.name));
+}
+
+function main() {
+ const args = parseArgs(process.argv.slice(2));
+ const copied = prepareGitHubReleaseAssets({
+ assetsDir: args["assets-dir"],
+ extraAssets: args.extraAsset,
+ outDir: args["out-dir"],
+ version:
+ args.version || process.env.RELEASE_TAG || process.env.GITHUB_REF_NAME,
+ });
+
+ console.log("Prepared GitHub release upload assets:");
+ for (const item of copied) {
+ console.log(` - ${path.relative(process.cwd(), item.destination)}`);
+ }
+}
+
+const isCli =
+ process.argv[1] &&
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
+
+if (isCli) {
+ main();
+}
+
+export { prepareGitHubReleaseAssets };
diff --git a/scripts/release-updater-manifest.test.mjs b/scripts/release-updater-manifest.test.mjs
index a14dbd67b..40d0b2d5f 100644
--- a/scripts/release-updater-manifest.test.mjs
+++ b/scripts/release-updater-manifest.test.mjs
@@ -4,6 +4,7 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import { planR2ReleaseCleanup } from "./plan-r2-release-cleanup.mjs";
+import { prepareGitHubReleaseAssets } from "./prepare-github-release-assets.mjs";
import {
collectUpdaterManifest,
writeOutputs,
@@ -215,9 +216,7 @@ describe("release updater manifest", () => {
path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz.sig"),
encodedSignature,
);
- writeFile(
- path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz"),
- );
+ writeFile(path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz"));
writeFile(
path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz.sig"),
rawSignature,
@@ -243,11 +242,7 @@ describe("release updater manifest", () => {
baseUrl: "https://updates.limecloud.com",
channel: "stable",
notes: "notes",
- requiredPlatforms: [
- "darwin-aarch64",
- "darwin-x86_64",
- "windows-x86_64",
- ],
+ requiredPlatforms: ["darwin-aarch64", "darwin-x86_64", "windows-x86_64"],
version: "v1.20.0",
});
@@ -292,3 +287,63 @@ describe("R2 release cleanup", () => {
expect(plan.protectedVersions).toContain("1.16.0");
});
});
+
+describe("GitHub release asset staging", () => {
+ it("同名 macOS updater 资产上传 GitHub Release 前应重命名", () => {
+ const root = fs.mkdtempSync(
+ path.join(os.tmpdir(), "lime-github-release-assets-"),
+ );
+ const assetsDir = path.join(root, "release-assets");
+ const outDir = path.join(root, "release-github-assets");
+ const latestPath = path.join(root, "release-updater", "latest.json");
+
+ writeFile(path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"));
+ writeFile(
+ path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz.sig"),
+ "arm-sig",
+ );
+ writeFile(
+ path.join(assetsDir, "aarch64-apple-darwin", "Lime_1.22.0_aarch64.dmg"),
+ );
+ writeFile(path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz"));
+ writeFile(
+ path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz.sig"),
+ "x64-sig",
+ );
+ writeFile(
+ path.join(assetsDir, "x86_64-apple-darwin", "Lime_1.22.0_x64.dmg"),
+ );
+ writeFile(latestPath, "{}");
+
+ const copied = prepareGitHubReleaseAssets({
+ assetsDir,
+ extraAssets: [latestPath],
+ outDir,
+ version: "v1.22.0",
+ });
+
+ expect(copied.map((item) => item.name).sort()).toEqual(
+ [
+ "Lime_1.22.0_aarch64.app.tar.gz",
+ "Lime_1.22.0_aarch64.app.tar.gz.sig",
+ "Lime_1.22.0_aarch64.dmg",
+ "Lime_1.22.0_x64.app.tar.gz",
+ "Lime_1.22.0_x64.app.tar.gz.sig",
+ "Lime_1.22.0_x64.dmg",
+ "latest.json",
+ ].sort(),
+ );
+ expect(
+ fs.readFileSync(
+ path.join(outDir, "Lime_1.22.0_aarch64.app.tar.gz.sig"),
+ "utf8",
+ ),
+ ).toBe("arm-sig");
+ expect(
+ fs.readFileSync(
+ path.join(outDir, "Lime_1.22.0_x64.app.tar.gz.sig"),
+ "utf8",
+ ),
+ ).toBe("x64-sig");
+ });
+});
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 8d012b36f..f2402f870 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -165,16 +165,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
-[[package]]
-name = "aead"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
-dependencies = [
- "crypto-common",
- "generic-array",
-]
-
[[package]]
name = "aes"
version = "0.8.4"
@@ -1862,30 +1852,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
-[[package]]
-name = "chacha20"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
-dependencies = [
- "cfg-if",
- "cipher",
- "cpufeatures",
-]
-
-[[package]]
-name = "chacha20poly1305"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
-dependencies = [
- "aead",
- "chacha20",
- "cipher",
- "poly1305",
- "zeroize",
-]
-
[[package]]
name = "chrono"
version = "0.4.43"
@@ -1918,7 +1884,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
- "zeroize",
]
[[package]]
@@ -2403,7 +2368,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
- "rand_core 0.6.4",
"typenum",
]
@@ -5101,7 +5065,7 @@ dependencies = [
[[package]]
name = "lime"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"anyhow",
"arboard",
@@ -5132,7 +5096,6 @@ dependencies = [
"lime-browser-runtime",
"lime-config",
"lime-core",
- "lime-credential",
"lime-embedding",
"lime-gateway",
"lime-infra",
@@ -5205,7 +5168,7 @@ dependencies = [
[[package]]
name = "lime-agent"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"anyhow",
"aster-core",
@@ -5234,7 +5197,7 @@ dependencies = [
[[package]]
name = "lime-browser-runtime"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"chrono",
"futures",
@@ -5251,7 +5214,7 @@ dependencies = [
[[package]]
name = "lime-cli"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"clap",
"lime-core",
@@ -5263,7 +5226,7 @@ dependencies = [
[[package]]
name = "lime-config"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"async-trait",
"lime-core",
@@ -5279,7 +5242,7 @@ dependencies = [
[[package]]
name = "lime-core"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"aster-models",
"async-trait",
@@ -5317,28 +5280,6 @@ dependencies = [
"zip 0.6.6",
]
-[[package]]
-name = "lime-credential"
-version = "1.21.0"
-dependencies = [
- "axum 0.7.9",
- "base64 0.22.1",
- "chacha20poly1305",
- "chrono",
- "dashmap 5.5.3",
- "lime-core",
- "lime-infra",
- "proptest",
- "rand 0.8.5",
- "reqwest 0.12.28",
- "serde",
- "serde_json",
- "sha2",
- "tempfile",
- "tokio",
- "tracing",
-]
-
[[package]]
name = "lime-embedding"
version = "0.1.0"
@@ -5354,7 +5295,7 @@ dependencies = [
[[package]]
name = "lime-gateway"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"aes",
"axum 0.7.9",
@@ -5384,7 +5325,7 @@ dependencies = [
[[package]]
name = "lime-infra"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"chrono",
"dashmap 5.5.3",
@@ -5404,7 +5345,7 @@ dependencies = [
[[package]]
name = "lime-mcp"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"async-trait",
"dirs 5.0.1",
@@ -5420,7 +5361,7 @@ dependencies = [
[[package]]
name = "lime-media-runtime"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -5451,7 +5392,7 @@ dependencies = [
[[package]]
name = "lime-processor"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"async-trait",
"lime-core",
@@ -5470,7 +5411,7 @@ dependencies = [
[[package]]
name = "lime-providers"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"anyhow",
"async-stream",
@@ -5525,7 +5466,7 @@ dependencies = [
[[package]]
name = "lime-server"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"aster-core",
"async-stream",
@@ -5540,7 +5481,6 @@ dependencies = [
"lime-agent",
"lime-config",
"lime-core",
- "lime-credential",
"lime-infra",
"lime-processor",
"lime-providers",
@@ -5570,7 +5510,7 @@ dependencies = [
[[package]]
name = "lime-server-utils"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"axum 0.7.9",
"futures",
@@ -5585,7 +5525,7 @@ dependencies = [
[[package]]
name = "lime-services"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"anyhow",
"aster-core",
@@ -5628,7 +5568,7 @@ dependencies = [
[[package]]
name = "lime-skills"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"async-trait",
"dirs 5.0.1",
@@ -5646,7 +5586,7 @@ dependencies = [
[[package]]
name = "lime-websocket"
-version = "1.21.0"
+version = "1.22.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -6715,12 +6655,6 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
-[[package]]
-name = "opaque-debug"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
-
[[package]]
name = "open"
version = "5.3.3"
@@ -7399,17 +7333,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "poly1305"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
-dependencies = [
- "cpufeatures",
- "opaque-debug",
- "universal-hash",
-]
-
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -11016,16 +10939,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
-[[package]]
-name = "universal-hash"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
-dependencies = [
- "crypto-common",
- "subtle",
-]
-
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index c4cdfab94..42c87fa89 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,10 +1,15 @@
[workspace]
members = ["crates/*"]
-exclude = ["crates/aster", "crates/aster-models", "crates/aster-rust"]
+exclude = [
+ "crates/aster",
+ "crates/aster-models",
+ "crates/aster-rust",
+ "crates/credential",
+]
resolver = "2"
[workspace.package]
-version = "1.21.0"
+version = "1.22.0"
edition = "2021"
authors = ["coso"]
repository = "https://github.com/aiclientproxy/lime"
@@ -17,7 +22,6 @@ lime-config = { path = "crates/config" }
lime-infra = { path = "crates/infra" }
lime-providers = { path = "crates/providers" }
lime-services = { path = "crates/services" }
-lime-credential = { path = "crates/credential" }
lime-websocket = { path = "crates/websocket" }
lime-processor = { path = "crates/processor" }
lime-server-utils = { path = "crates/server-utils" }
@@ -189,7 +193,7 @@ version = "2.4"
[package]
name = "lime"
-version = "1.21.0"
+version = "1.22.0"
description = "AI API Proxy Desktop App"
authors = ["you"]
edition = "2021"
@@ -211,7 +215,6 @@ lime-config.workspace = true
lime-infra.workspace = true
lime-providers.workspace = true
lime-services.workspace = true
-lime-credential.workspace = true
lime-websocket.workspace = true
lime-processor.workspace = true
lime-server-utils.workspace = true
diff --git a/src-tauri/crates/agent/src/aster_state.rs b/src-tauri/crates/agent/src/aster_state.rs
index e7e7aeb4a..5ac4effa4 100644
--- a/src-tauri/crates/agent/src/aster_state.rs
+++ b/src-tauri/crates/agent/src/aster_state.rs
@@ -2,7 +2,7 @@
//!
//! 管理 Aster Agent 实例和相关状态
//! 提供 Tauri 应用与 Aster 框架的桥接
-//! 支持从 Lime 凭证池自动选择凭证
+//! 支持从 Lime API Key Provider 自动选择凭证
//!
//! ## 重要:SessionStore 注入
//!
@@ -116,7 +116,7 @@ pub struct RuntimeInterruptMarker {
pub struct ProviderConfig {
/// Provider 名称 (openai, anthropic, google, ollama 等)
pub provider_name: String,
- /// Provider 选择器(优先保留前端 provider_id / pool provider_type)
+ /// Provider 选择器(优先保留前端 provider_id / API Key Provider 类型)
pub provider_selector: Option,
/// 模型名称
pub model_name: String,
@@ -124,12 +124,10 @@ pub struct ProviderConfig {
pub api_key: Option,
/// Base URL (可选,用于自定义端点)
pub base_url: Option,
- /// 凭证 UUID(来自凭证池,用于记录使用和健康状态)
+ /// 凭证 UUID(来自 API Key Provider,用于记录使用和健康状态)
pub credential_uuid: Option,
/// 是否强制 OpenAI provider 使用 Responses API
pub force_responses_api: bool,
- /// OAuth/本地 Provider 需要的凭证文件路径
- pub credential_path: Option,
/// 当前回合是否需要用 toolshim 兼容无原生 tools 的模型
pub toolshim: bool,
/// toolshim 解释器模型(可与实际回复模型不同)
@@ -306,7 +304,6 @@ impl AsterAgentState {
.clone()
.unwrap_or_else(|| format!("manual:{session_id}")),
force_responses_api: config.force_responses_api,
- credential_path: config.credential_path.clone(),
toolshim: config.toolshim,
toolshim_model: config.toolshim_model.clone(),
})
@@ -339,13 +336,13 @@ impl AsterAgentState {
Ok(())
}
- /// 从凭证池配置 Provider
+ /// 从 API Key Provider 配置 Provider
///
- /// 自动从 Lime 凭证池选择可用凭证并配置 Aster Provider
+ /// 自动从 Lime API Key Provider 选择可用凭证并配置 Aster Provider
///
/// # 参数
/// - `db`: 数据库连接
- /// - `provider_type`: Provider 类型 (openai, anthropic, kiro 等)
+ /// - `provider_type`: Provider 类型 (openai, anthropic, google 等)
/// - `model`: 模型名称
/// - `session_id`: 会话 ID
pub async fn configure_provider_from_pool(
@@ -358,12 +355,12 @@ impl AsterAgentState {
// 确保 Agent 已初始化(使用带数据库的版本)
self.init_agent_with_db(db).await?;
- // 从凭证池选择凭证并获取配置
+ // 从 API Key Provider 选择凭证并获取配置
let aster_config = self
.credential_bridge
.select_and_configure(db, provider_type, model)
.await
- .map_err(|e| format!("从凭证池选择凭证失败: {e}"))?;
+ .map_err(|e| format!("从 API Key Provider 选择凭证失败: {e}"))?;
// 创建 Provider
let provider = create_aster_provider(&aster_config)
@@ -388,7 +385,6 @@ impl AsterAgentState {
base_url: aster_config.base_url.clone(),
credential_uuid: Some(aster_config.credential_uuid.clone()),
force_responses_api: aster_config.force_responses_api,
- credential_path: aster_config.credential_path.clone(),
toolshim: aster_config.toolshim,
toolshim_model: aster_config.toolshim_model.clone(),
};
@@ -408,7 +404,7 @@ impl AsterAgentState {
}
tracing::info!(
- "[AsterAgent] 从凭证池配置 Provider 成功: {} / {} (凭证: {})",
+ "[AsterAgent] 从 API Key Provider 配置 Provider 成功: {} / {} (凭证: {})",
aster_config.provider_name,
aster_config.model_name,
aster_config.credential_uuid
@@ -450,7 +446,7 @@ impl AsterAgentState {
/// 清除当前 Provider 配置
///
- /// 用于切换凭证后重置状态,下次对话时会重新从凭证池选择凭证
+ /// 用于切换凭证后重置状态,下次对话时会重新从 API Key Provider 选择凭证
pub async fn clear_provider_config(&self) {
let mut config_guard = self.current_provider_config.write().await;
*config_guard = None;
@@ -853,7 +849,6 @@ mod tests {
base_url: None,
credential_uuid: None,
force_responses_api: false,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -878,7 +873,6 @@ mod tests {
base_url: None,
credential_uuid: None,
force_responses_api: true,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -894,7 +888,7 @@ mod tests {
}
#[test]
- fn test_provider_config_detects_kiro_provider_session_token_capability() {
+ fn test_provider_config_treats_retired_kiro_as_history_replay_only() {
let config = ProviderConfig {
provider_name: "kiro".to_string(),
provider_selector: Some("kiro".to_string()),
@@ -903,14 +897,13 @@ mod tests {
base_url: None,
credential_uuid: None,
force_responses_api: false,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
assert_eq!(
config.provider_continuation_capability(),
- ProviderContinuationCapability::ProviderSessionToken
+ ProviderContinuationCapability::HistoryReplayOnly
);
assert_eq!(
config.provider_continuation_state(),
diff --git a/src-tauri/crates/agent/src/credential_bridge.rs b/src-tauri/crates/agent/src/credential_bridge.rs
index 263de8c0b..9275ac211 100644
--- a/src-tauri/crates/agent/src/credential_bridge.rs
+++ b/src-tauri/crates/agent/src/credential_bridge.rs
@@ -1,28 +1,21 @@
-//! 凭证池桥接模块
+//! API Key Provider 桥接模块
//!
-//! 将 Lime 凭证池与 Aster Provider 系统连接
-//! 支持从凭证池自动选择凭证并配置 Aster Provider
+//! 将 Lime API Key Provider 主路径与 Aster Provider 系统连接。
//!
//! ## 功能
-//! - 从凭证池选择可用凭证
+//! - 从 API Key Provider 选择可用凭证
//! - 将凭证转换为 Aster Provider 配置
-//! - 支持 OAuth 和 API Key 两种凭证类型
-//! - 自动刷新过期的 OAuth Token
//! - 智能拆分 base_url 为 host + path,避免路径重复(如智谱 /v4/v1 问题)
use aster::model::ModelConfig;
use aster::providers::base::Provider;
use lime_core::database::dao::api_key_provider::{infer_managed_runtime_spec, ApiProviderType};
use lime_core::database::DbConnection;
-use lime_core::models::provider_pool_model::{
- CredentialData, PoolProviderType, ProviderCredential,
-};
+use lime_core::models::provider_pool_model::{CredentialData, ProviderCredential};
use lime_core::models::provider_type::is_custom_provider_id;
use lime_services::api_key_provider_service::ApiKeyProviderService;
-use lime_services::provider_pool_service::ProviderPoolService;
use std::sync::Arc;
-use crate::kiro_provider_adapter::LimeKiroProvider;
use crate::provider_safety::wrap_provider_with_safety;
/// 凭证桥接错误
@@ -71,19 +64,16 @@ pub struct AsterProviderConfig {
pub credential_uuid: String,
/// 是否强制 OpenAI provider 使用 Responses API(用于 Codex 等兼容链路)
pub force_responses_api: bool,
- /// OAuth/本地 Provider 需要的凭证文件路径
- pub credential_path: Option,
/// 当前回合是否启用 toolshim
pub toolshim: bool,
/// toolshim 解释器模型
pub toolshim_model: Option,
}
-/// 凭证池桥接器
+/// Provider 凭证桥接器
///
-/// 负责从 Lime 凭证池选择凭证并转换为 Aster Provider 配置
+/// 负责从 Lime API Key Provider 选择凭证并转换为 Aster Provider 配置
pub struct CredentialBridge {
- pool_service: ProviderPoolService,
api_key_service: ApiKeyProviderService,
}
@@ -96,7 +86,6 @@ impl Default for CredentialBridge {
impl CredentialBridge {
pub fn new() -> Self {
Self {
- pool_service: ProviderPoolService::new(),
api_key_service: ApiKeyProviderService::new(),
}
}
@@ -106,7 +95,7 @@ impl CredentialBridge {
.filter(|value| !value.is_empty())
}
- /// 从凭证池选择凭证并创建 Aster Provider 配置
+ /// 从 API Key Provider 选择凭证并创建 Aster Provider 配置
///
/// # 参数
/// - `db`: 数据库连接
@@ -121,19 +110,9 @@ impl CredentialBridge {
provider_type: &str,
model: &str,
) -> Result {
- // 1. 从凭证池选择凭证
- // 将 provider_type 同时作为 provider_id_hint 传递,支持 60+ API Key Provider
- // 例如 "deepseek", "moonshot", "qwen" 等
let credential = self
- .pool_service
- .select_credential_with_fallback(
- db,
- &self.api_key_service,
- provider_type,
- Some(model),
- Some(provider_type), // 传递 provider_id_hint 支持智能降级
- None,
- )
+ .api_key_service
+ .select_credential_for_provider(db, provider_type, Some(provider_type), None)
.await
.map_err(CredentialBridgeError::DatabaseError)?
.ok_or_else(|| {
@@ -223,22 +202,6 @@ impl CredentialBridge {
false,
),
- // Kiro OAuth - 需要获取 access_token
- CredentialData::KiroOAuth { creds_file_path } => {
- let token = self
- .get_kiro_token(creds_file_path, db, &credential.uuid)
- .await?;
- ("kiro".to_string(), Some(token), None, false)
- }
-
- // Gemini OAuth
- CredentialData::GeminiOAuth {
- creds_file_path, ..
- } => {
- let token = self.get_oauth_token(creds_file_path).await?;
- ("google".to_string(), Some(token), None, false)
- }
-
// Gemini API Key
CredentialData::GeminiApiKey {
api_key, base_url, ..
@@ -259,33 +222,11 @@ impl CredentialBridge {
false,
),
- // Codex OAuth
- CredentialData::CodexOAuth {
- creds_file_path,
- api_base_url,
- } => {
- let token = self.get_codex_token(creds_file_path).await?;
- (
- // 统一走 OpenAI provider,保证 tools/stream 事件链路一致
- "openai".to_string(),
- Some(token),
- api_base_url.clone(),
- true,
- )
- }
-
- // Claude OAuth
- CredentialData::ClaudeOAuth { creds_file_path } => {
- let token = self.get_oauth_token(creds_file_path).await?;
- ("anthropic".to_string(), Some(token), None, false)
- }
-
- // Antigravity OAuth
- CredentialData::AntigravityOAuth {
- creds_file_path, ..
- } => {
- let token = self.get_oauth_token(creds_file_path).await?;
- ("google".to_string(), Some(token), None, false)
+ unsupported => {
+ return Err(CredentialBridgeError::UnsupportedCredentialType(format!(
+ "凭证池/OAuth 凭证已退役,当前只支持 API Key Provider 凭证: {:?}",
+ unsupported
+ )));
}
};
@@ -297,88 +238,11 @@ impl CredentialBridge {
base_url,
credential_uuid: credential.uuid.clone(),
force_responses_api,
- credential_path: match &credential.credential {
- CredentialData::KiroOAuth { creds_file_path } => Some(creds_file_path.clone()),
- _ => None,
- },
toolshim: false,
toolshim_model: None,
})
}
- /// 获取 Kiro OAuth Token
- async fn get_kiro_token(
- &self,
- creds_path: &str,
- _db: &DbConnection,
- _uuid: &str,
- ) -> Result {
- use lime_providers::providers::KiroProvider;
-
- let mut provider = KiroProvider::new();
- provider
- .load_credentials_from_path(creds_path)
- .await
- .map_err(|e| {
- CredentialBridgeError::TokenRefreshFailed(format!("加载 Kiro 凭证失败: {e}"))
- })?;
-
- // 检查 token 是否过期,如果过期则刷新
- if provider.is_token_expired() {
- tracing::info!("[CredentialBridge] Kiro token 已过期,尝试刷新");
- self.pool_service
- .refresh_kiro_token(creds_path)
- .await
- .map_err(CredentialBridgeError::TokenRefreshFailed)?;
-
- // 重新加载凭证
- provider
- .load_credentials_from_path(creds_path)
- .await
- .map_err(|e| {
- CredentialBridgeError::TokenRefreshFailed(format!("重新加载凭证失败: {e}"))
- })?;
- }
-
- provider.credentials.access_token.ok_or_else(|| {
- CredentialBridgeError::TokenRefreshFailed("缺少 access_token".to_string())
- })
- }
-
- /// 获取通用 OAuth Token
- async fn get_oauth_token(&self, creds_path: &str) -> Result {
- let content = std::fs::read_to_string(creds_path).map_err(|e| {
- CredentialBridgeError::TokenRefreshFailed(format!("读取凭证文件失败: {e}"))
- })?;
-
- let creds: serde_json::Value = serde_json::from_str(&content)
- .map_err(|e| CredentialBridgeError::TokenRefreshFailed(format!("解析凭证失败: {e}")))?;
-
- creds["access_token"]
- .as_str()
- .map(String::from)
- .ok_or_else(|| {
- CredentialBridgeError::TokenRefreshFailed("凭证中缺少 access_token".to_string())
- })
- }
-
- /// 获取 Codex OAuth Token
- async fn get_codex_token(&self, creds_path: &str) -> Result {
- use lime_providers::providers::CodexProvider;
-
- let mut provider = CodexProvider::new();
- provider
- .load_credentials_from_path(creds_path)
- .await
- .map_err(|e| {
- CredentialBridgeError::TokenRefreshFailed(format!("加载 Codex 凭证失败: {e}"))
- })?;
-
- provider.ensure_valid_token().await.map_err(|e| {
- CredentialBridgeError::TokenRefreshFailed(format!("获取 Codex token 失败: {e}"))
- })
- }
-
/// 记录凭证使用
pub fn record_usage(&self, db: &DbConnection, uuid: &str) -> Result<(), CredentialBridgeError> {
if let Some(api_key_id) = self.resolve_fallback_api_key_id(uuid) {
@@ -388,9 +252,8 @@ impl CredentialBridge {
.map_err(CredentialBridgeError::DatabaseError);
}
- self.pool_service
- .record_usage(db, uuid)
- .map_err(CredentialBridgeError::DatabaseError)
+ tracing::debug!("[CredentialBridge] 忽略已退役的凭证池使用记录: {}", uuid);
+ Ok(())
}
/// 标记凭证为健康
@@ -404,9 +267,9 @@ impl CredentialBridge {
return Ok(());
}
- self.pool_service
- .mark_healthy(db, uuid, model)
- .map_err(CredentialBridgeError::DatabaseError)
+ let _ = (db, model);
+ tracing::debug!("[CredentialBridge] 忽略已退役的凭证池健康标记: {}", uuid);
+ Ok(())
}
/// 标记凭证为不健康
@@ -420,9 +283,9 @@ impl CredentialBridge {
return Ok(());
}
- self.pool_service
- .mark_unhealthy(db, uuid, error)
- .map_err(CredentialBridgeError::DatabaseError)
+ let _ = (db, error);
+ tracing::debug!("[CredentialBridge] 忽略已退役的凭证池失败标记: {}", uuid);
+ Ok(())
}
}
@@ -443,28 +306,6 @@ pub async fn create_aster_provider(
);
}
- if config.provider_name == "kiro" {
- let model_config = build_provider_model_config(config)?;
-
- let credential_path = config.credential_path.clone().ok_or_else(|| {
- CredentialBridgeError::ProviderCreationFailed(
- "Kiro provider 缺少 credential_path".to_string(),
- )
- })?;
-
- let provider = LimeKiroProvider::new(credential_path, model_config).map_err(|error| {
- CredentialBridgeError::ProviderCreationFailed(format!(
- "创建 Kiro Provider 失败: {}",
- error
- ))
- })?;
-
- return Ok(wrap_provider_with_safety(
- Arc::new(provider),
- disable_default_fast_model,
- ));
- }
-
// 设置环境变量
set_provider_env_vars(config);
@@ -515,7 +356,7 @@ fn is_first_party_openai_base_url(base_url: &str) -> bool {
}
fn is_first_party_anthropic_selector(selector: &str) -> bool {
- matches!(selector, "anthropic" | "claude" | "claude_oauth")
+ matches!(selector, "anthropic" | "claude")
}
fn is_first_party_anthropic_base_url(base_url: &str) -> bool {
@@ -714,28 +555,6 @@ fn set_provider_env_vars(config: &AsterProviderConfig) {
}
}
-/// Provider 类型映射
-///
-/// 将 Lime PoolProviderType 映射到 Aster Provider 名称
-pub fn map_pool_type_to_aster(pool_type: &PoolProviderType) -> &'static str {
- match pool_type {
- PoolProviderType::Kiro => "kiro",
- PoolProviderType::Gemini => "google",
- PoolProviderType::Antigravity => "google",
- PoolProviderType::OpenAI => "openai",
- PoolProviderType::Claude => "anthropic",
- PoolProviderType::Anthropic => "anthropic",
- PoolProviderType::AnthropicCompatible => "anthropic",
- PoolProviderType::Vertex => "gcpvertexai",
- PoolProviderType::GeminiApiKey => "google",
- PoolProviderType::Codex => "codex",
- PoolProviderType::ClaudeOAuth => "anthropic",
- PoolProviderType::AzureOpenai => "azure",
- PoolProviderType::AwsBedrock => "bedrock",
- PoolProviderType::Ollama => "ollama",
- }
-}
-
/// 将 provider_type 字符串映射到 Aster Provider 名称
///
/// 支持 60+ API Key Provider,包括 deepseek, moonshot, qwen 等
@@ -750,9 +569,8 @@ fn map_provider_type_to_aster(provider_type: &str) -> &'static str {
"anthropic" | "claude" => "anthropic",
"google" | "gemini" => "google",
"bedrock" => "bedrock",
- "kiro" | "codewhisperer" => "kiro",
"gcpvertexai" | "vertex" => "gcpvertexai",
- "codex" => "codex",
+ "codex" => "openai",
"azure" | "azure-openai" => "azure",
"ollama" => "ollama",
@@ -789,17 +607,6 @@ fn map_provider_type_to_aster_with_api_type(
mod tests {
use super::*;
- #[test]
- fn test_map_pool_type_to_aster() {
- assert_eq!(map_pool_type_to_aster(&PoolProviderType::OpenAI), "openai");
- assert_eq!(
- map_pool_type_to_aster(&PoolProviderType::Claude),
- "anthropic"
- );
- assert_eq!(map_pool_type_to_aster(&PoolProviderType::Gemini), "google");
- assert_eq!(map_pool_type_to_aster(&PoolProviderType::Kiro), "kiro");
- }
-
#[test]
fn test_map_provider_type_to_aster_with_api_type() {
assert_eq!(
@@ -836,7 +643,6 @@ mod tests {
base_url: Some("https://example.com/openai".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: true,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -871,7 +677,6 @@ mod tests {
base_url: Some("https://api.openai.com/v1".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: true,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -908,7 +713,6 @@ mod tests {
base_url: Some("https://open.bigmodel.cn/api/paas/v4".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: false,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -926,7 +730,6 @@ mod tests {
base_url: Some("https://api.openai.com/v1".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: false,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -944,7 +747,6 @@ mod tests {
base_url: Some("https://example.com/openai".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: true,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -962,7 +764,6 @@ mod tests {
base_url: Some("https://token-plan-cn.xiaomimimo.com/anthropic".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: false,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -980,7 +781,6 @@ mod tests {
base_url: Some("https://api.anthropic.com".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: false,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -1029,7 +829,6 @@ mod tests {
base_url: Some("https://open.bigmodel.cn/api/anthropic".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: false,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -1064,7 +863,6 @@ mod tests {
base_url: Some("https://api.anthropic.com".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: false,
- credential_path: None,
toolshim: false,
toolshim_model: None,
};
@@ -1088,7 +886,6 @@ mod tests {
base_url: Some("http://127.0.0.1:11434".to_string()),
credential_uuid: "test-uuid".to_string(),
force_responses_api: false,
- credential_path: None,
toolshim: true,
toolshim_model: Some("glm-5.1:cloud".to_string()),
};
diff --git a/src-tauri/crates/agent/src/kiro_provider_adapter.rs b/src-tauri/crates/agent/src/kiro_provider_adapter.rs
deleted file mode 100644
index bba1be1e3..000000000
--- a/src-tauri/crates/agent/src/kiro_provider_adapter.rs
+++ /dev/null
@@ -1,360 +0,0 @@
-use anyhow::anyhow;
-use aster::conversation::message::{Message, MessageContent};
-use aster::model::ModelConfig;
-use aster::providers::base::{
- ConfigKey, MessageStream, ModelInfo, Provider, ProviderMetadata, ProviderUsage, Usage,
-};
-use aster::providers::errors::ProviderError;
-use aster::providers::formats::openai::{
- format_messages, format_tools, response_to_streaming_message,
-};
-use aster::providers::utils::ImageFormat;
-use aster::session_context::current_turn_context;
-use async_stream::try_stream;
-use async_trait::async_trait;
-use futures::{pin_mut, StreamExt};
-use lime_core::models::openai::ChatCompletionRequest;
-use lime_providers::providers::{KiroProvider, TokenManager};
-use lime_providers::streaming::converter::{StreamConverter, StreamFormat as LimeStreamFormat};
-use rmcp::model::{Role, Tool};
-use serde_json::json;
-use uuid::Uuid;
-
-const KIRO_PROVIDER_NAME: &str = "kiro";
-
-pub(crate) struct LimeKiroProvider {
- credential_path: String,
- model: ModelConfig,
- name: String,
-}
-
-impl LimeKiroProvider {
- pub(crate) fn new(
- credential_path: impl Into,
- model: ModelConfig,
- ) -> Result {
- let credential_path = credential_path.into();
- if credential_path.trim().is_empty() {
- return Err(ProviderError::ExecutionError(
- "Kiro provider 缺少 credential_path".to_string(),
- ));
- }
-
- Ok(Self {
- credential_path,
- model,
- name: KIRO_PROVIDER_NAME.to_string(),
- })
- }
-
- async fn load_provider(&self) -> Result {
- let mut provider = KiroProvider::new();
- provider
- .load_credentials_from_path(&self.credential_path)
- .await
- .map_err(|error| {
- ProviderError::Authentication(format!("加载 Kiro 凭证失败: {}", error))
- })?;
-
- provider.ensure_valid_token().await.map_err(|error| {
- ProviderError::Authentication(format!("刷新 Kiro Token 失败: {}", error))
- })?;
-
- Ok(provider)
- }
-
- fn normalize_optional_text(value: Option<&str>) -> Option {
- value
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(str::to_string)
- }
-
- fn resolve_conversation_id_from_turn_context() -> Option {
- let turn_context = current_turn_context()?;
- let provider_continuation = turn_context
- .metadata
- .get("provider_continuation")?
- .as_object()?;
-
- if provider_continuation
- .get("enabled")
- .and_then(serde_json::Value::as_bool)
- != Some(true)
- {
- return None;
- }
-
- if provider_continuation
- .get("kind")
- .and_then(serde_json::Value::as_str)
- .map(str::trim)
- != Some("provider_session_token")
- {
- return None;
- }
-
- for key in [
- "session_token",
- "sessionToken",
- "provider_session_token",
- "providerSessionToken",
- "conversation_id",
- "conversationId",
- ] {
- if let Some(value) = Self::normalize_optional_text(
- provider_continuation
- .get(key)
- .and_then(serde_json::Value::as_str),
- ) {
- return Some(value);
- }
- }
-
- None
- }
-
- fn resolve_or_create_conversation_id() -> String {
- Self::resolve_conversation_id_from_turn_context()
- .unwrap_or_else(|| Uuid::new_v4().to_string())
- }
-
- fn build_chat_request(
- model_config: &ModelConfig,
- system: &str,
- messages: &[Message],
- tools: &[Tool],
- stream: bool,
- ) -> Result {
- let mut openai_messages = vec![json!({
- "role": "system",
- "content": system,
- })];
- openai_messages.extend(format_messages(messages, &ImageFormat::OpenAi));
-
- let tools_payload = format_tools(tools)
- .map_err(|error| ProviderError::ExecutionError(error.to_string()))?;
-
- let mut payload = json!({
- "model": model_config.model_name,
- "messages": openai_messages,
- "stream": stream,
- });
-
- if !tools_payload.is_empty() {
- payload["tools"] = json!(tools_payload);
- }
-
- serde_json::from_value(payload).map_err(|error| {
- ProviderError::ExecutionError(format!(
- "构造 Kiro ChatCompletionRequest 失败: {}",
- error
- ))
- })
- }
-
- fn attach_conversation_id(mut message: Message, conversation_id: &str) -> Message {
- if message.role == Role::Assistant {
- message.id = Some(conversation_id.to_string());
- }
- message
- }
-
- fn push_or_merge_content(target: &mut Vec, content: MessageContent) {
- match (target.last_mut(), &content) {
- (Some(MessageContent::Text(existing)), MessageContent::Text(incoming)) => {
- existing.text.push_str(&incoming.text);
- }
- (Some(MessageContent::Thinking(existing)), MessageContent::Thinking(incoming)) => {
- existing.thinking.push_str(&incoming.thinking);
- }
- _ => target.push(content),
- }
- }
-
- fn merge_message_chunk(target: &mut Message, chunk: Message) {
- if target.id.is_none() {
- target.id = chunk.id.clone();
- }
-
- for content in chunk.content {
- Self::push_or_merge_content(&mut target.content, content);
- }
- }
-
- async fn stream_with_model_and_conversation(
- &self,
- model_config: &ModelConfig,
- system: &str,
- messages: &[Message],
- tools: &[Tool],
- conversation_id: String,
- ) -> Result {
- let request = Self::build_chat_request(model_config, system, messages, tools, true)?;
- let provider = self.load_provider().await?;
- let source_stream = provider
- .call_api_stream_with_conversation_id(&request, Some(&conversation_id))
- .await
- .map_err(Self::map_lime_provider_error)?;
-
- let model_name = model_config.model_name.clone();
- let openai_line_stream = Box::pin(try_stream! {
- let mut source_stream = source_stream;
- let mut converter = StreamConverter::with_model(
- LimeStreamFormat::AwsEventStream,
- LimeStreamFormat::OpenAiSse,
- &model_name,
- );
-
- while let Some(chunk) = source_stream.next().await {
- let chunk = chunk.map_err(|error| anyhow!(error.to_string()))?;
- for event in converter.convert(&chunk) {
- for line in event.lines() {
- let line = line.trim_end_matches('\r');
- if !line.is_empty() {
- yield line.to_string();
- }
- }
- }
- }
-
- for event in converter.finish() {
- for line in event.lines() {
- let line = line.trim_end_matches('\r');
- if !line.is_empty() {
- yield line.to_string();
- }
- }
- }
- });
-
- Ok(Box::pin(try_stream! {
- let message_stream = response_to_streaming_message(openai_line_stream);
- pin_mut!(message_stream);
-
- while let Some(item) = message_stream.next().await {
- let (message, usage) = item.map_err(|error| {
- ProviderError::RequestFailed(format!("解析 Kiro 流式响应失败: {}", error))
- })?;
-
- let message = message.map(|message| {
- Self::attach_conversation_id(message, &conversation_id)
- });
-
- yield (message, usage);
- }
- }))
- }
-
- fn map_lime_provider_error(error: lime_providers::providers::ProviderError) -> ProviderError {
- match error {
- lime_providers::providers::ProviderError::AuthenticationError(details) => {
- ProviderError::Authentication(details)
- }
- lime_providers::providers::ProviderError::RateLimitError(details) => {
- ProviderError::RateLimitExceeded {
- details,
- retry_delay: None,
- }
- }
- lime_providers::providers::ProviderError::ServerError(details) => {
- ProviderError::ServerError(details)
- }
- lime_providers::providers::ProviderError::RequestError(details) => {
- ProviderError::RequestFailed(details)
- }
- lime_providers::providers::ProviderError::ParseError(details)
- | lime_providers::providers::ProviderError::ConfigurationError(details)
- | lime_providers::providers::ProviderError::Unknown(details)
- | lime_providers::providers::ProviderError::TokenExpired(details)
- | lime_providers::providers::ProviderError::NetworkError(details) => {
- ProviderError::ExecutionError(details)
- }
- }
- }
-}
-
-#[async_trait]
-impl Provider for LimeKiroProvider {
- fn metadata() -> ProviderMetadata
- where
- Self: Sized,
- {
- ProviderMetadata::with_models(
- KIRO_PROVIDER_NAME,
- "Kiro",
- "Lime 本地 Kiro/CodeWhisperer Provider 适配器",
- "claude-sonnet-4-5",
- vec![ModelInfo::new("claude-sonnet-4-5", 200_000)],
- "",
- vec![ConfigKey::new("KIRO_CREDENTIAL_PATH", true, true, None)],
- )
- }
-
- fn get_name(&self) -> &str {
- &self.name
- }
-
- async fn complete_with_model(
- &self,
- model_config: &ModelConfig,
- system: &str,
- messages: &[Message],
- tools: &[Tool],
- ) -> Result<(Message, ProviderUsage), ProviderError> {
- let conversation_id = Self::resolve_or_create_conversation_id();
- let mut stream = self
- .stream_with_model_and_conversation(
- model_config,
- system,
- messages,
- tools,
- conversation_id.clone(),
- )
- .await?;
-
- let mut final_message = Message::assistant().with_id(conversation_id.clone());
- let mut final_usage = None;
-
- while let Some(item) = stream.next().await {
- let (message, usage) = item?;
- if let Some(message) = message {
- Self::merge_message_chunk(&mut final_message, message);
- }
- if usage.is_some() {
- final_usage = usage;
- }
- }
-
- let usage = final_usage.unwrap_or_else(|| {
- ProviderUsage::new(model_config.model_name.clone(), Usage::default())
- });
-
- Ok((final_message, usage))
- }
-
- fn get_model_config(&self) -> ModelConfig {
- self.model.clone()
- }
-
- async fn stream(
- &self,
- system: &str,
- messages: &[Message],
- tools: &[Tool],
- ) -> Result {
- let conversation_id = Self::resolve_or_create_conversation_id();
- self.stream_with_model_and_conversation(
- &self.model,
- system,
- messages,
- tools,
- conversation_id,
- )
- .await
- }
-
- fn supports_streaming(&self) -> bool {
- true
- }
-}
diff --git a/src-tauri/crates/agent/src/lib.rs b/src-tauri/crates/agent/src/lib.rs
index d2cd2cd3f..e11ec0b4a 100644
--- a/src-tauri/crates/agent/src/lib.rs
+++ b/src-tauri/crates/agent/src/lib.rs
@@ -21,7 +21,6 @@ pub mod durable_memory_fs;
pub mod event_converter;
pub mod filesystem_event_protocol;
pub mod hooks;
-mod kiro_provider_adapter;
pub mod lsp_bridge;
pub mod mcp_bridge;
pub mod prompt;
diff --git a/src-tauri/crates/agent/src/provider_continuation_state.rs b/src-tauri/crates/agent/src/provider_continuation_state.rs
index 0c1035829..88e0700a6 100644
--- a/src-tauri/crates/agent/src/provider_continuation_state.rs
+++ b/src-tauri/crates/agent/src/provider_continuation_state.rs
@@ -19,11 +19,6 @@ fn is_openai_responses_model(model_name: &str) -> bool {
normalized.starts_with("gpt-5") && normalized.contains("codex")
}
-fn is_kiro_session_provider(candidate: &str) -> bool {
- let normalized = candidate.trim().to_ascii_lowercase();
- normalized.contains("kiro") || normalized.contains("codewhisperer")
-}
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProviderContinuationCapability {
@@ -64,14 +59,6 @@ pub fn resolve_provider_continuation_capability(
return ProviderContinuationCapability::PreviousResponseId;
}
- if provider_candidates
- .iter()
- .flatten()
- .any(|candidate| is_kiro_session_provider(candidate))
- {
- return ProviderContinuationCapability::ProviderSessionToken;
- }
-
ProviderContinuationCapability::HistoryReplayOnly
}
@@ -224,10 +211,10 @@ mod tests {
}
#[test]
- fn test_resolve_provider_continuation_capability_detects_kiro_provider_session_token() {
+ fn test_resolve_provider_continuation_capability_treats_retired_kiro_as_history_replay_only() {
assert_eq!(
resolve_provider_continuation_capability("kiro", Some("kiro"), "claude-3.7", false),
- ProviderContinuationCapability::ProviderSessionToken
+ ProviderContinuationCapability::HistoryReplayOnly
);
assert_eq!(
resolve_provider_continuation_capability(
@@ -236,7 +223,7 @@ mod tests {
"claude-3.7",
false
),
- ProviderContinuationCapability::ProviderSessionToken
+ ProviderContinuationCapability::HistoryReplayOnly
);
}
diff --git a/src-tauri/crates/agent/src/request_tool_policy.rs b/src-tauri/crates/agent/src/request_tool_policy.rs
index 9b40034e9..5a5d6b75a 100644
--- a/src-tauri/crates/agent/src/request_tool_policy.rs
+++ b/src-tauri/crates/agent/src/request_tool_policy.rs
@@ -1621,6 +1621,7 @@ pub async fn execute_web_search_preflight_if_needed(
message_text: &str,
working_directory: Option<&Path>,
cancel_token: Option,
+ turn_context: Option,
policy: &RequestToolPolicy,
tracker: &mut WebSearchExecutionTracker,
) -> Result {
@@ -1687,6 +1688,7 @@ pub async fn execute_web_search_preflight_if_needed(
let session_id = session_id.to_string();
let working_directory = working_directory.clone();
let cancel_token = cancel_token.clone();
+ let turn_context = turn_context.clone();
async move {
let query = planned.query.clone();
let params = serde_json::json!({ "query": query });
@@ -1694,12 +1696,13 @@ pub async fn execute_web_search_preflight_if_needed(
if let Some(token) = cancel_token {
context = context.with_cancellation_token(token);
}
- let result = {
+ let result = aster::session_context::with_turn_context(turn_context, async {
let registry = registry_arc.read().await;
registry
.execute(&preflight_tool_name, params, &context, None)
.await
- };
+ })
+ .await;
match result {
Ok(tool_result) => PreflightSearchOutcome {
index: planned.index,
@@ -1825,6 +1828,7 @@ where
&message_text,
working_directory,
cancel_token.clone(),
+ session_config.turn_context.clone(),
request_tool_policy,
&mut web_search_tracker,
)
@@ -1839,6 +1843,7 @@ where
&message_text,
working_directory,
cancel_token.clone(),
+ session_config.turn_context.clone(),
request_tool_policy,
&mut optional_preflight_tracker,
),
@@ -2192,12 +2197,76 @@ mod tests {
use aster::providers::base::{Provider, ProviderMetadata, ProviderUsage, Usage};
use aster::providers::errors::ProviderError;
use aster::session::{SessionManager, SessionType, TurnContextOverride, TurnStatus};
+ use aster::tools::{PermissionCheckResult, Tool, ToolError, ToolResult};
use async_trait::async_trait;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
+ struct TurnContextGatedWebSearchTool;
+
+ #[async_trait]
+ impl Tool for TurnContextGatedWebSearchTool {
+ fn name(&self) -> &str {
+ "WebSearch"
+ }
+
+ fn description(&self) -> &str {
+ "测试用 WebSearch 工具"
+ }
+
+ fn input_schema(&self) -> serde_json::Value {
+ serde_json::json!({
+ "type": "object",
+ "properties": {
+ "query": { "type": "string" }
+ },
+ "required": ["query"]
+ })
+ }
+
+ async fn check_permissions(
+ &self,
+ _params: &serde_json::Value,
+ _context: &ToolContext,
+ ) -> PermissionCheckResult {
+ let allowed = aster::session_context::current_turn_context()
+ .as_ref()
+ .is_some_and(|turn_context| {
+ ["web_search_enabled", "webSearchEnabled"]
+ .iter()
+ .any(|key| {
+ turn_context
+ .metadata
+ .get(*key)
+ .and_then(serde_json::Value::as_bool)
+ .unwrap_or(false)
+ })
+ });
+
+ if allowed {
+ PermissionCheckResult::allow()
+ } else {
+ PermissionCheckResult::ask("WebSearch 需要联网确认。")
+ }
+ }
+
+ async fn execute(
+ &self,
+ params: serde_json::Value,
+ _context: &ToolContext,
+ ) -> Result {
+ let query = params
+ .get("query")
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or_default();
+ Ok(ToolResult::success(format!(
+ "预检索测试结果:https://example.com/search?q={query}"
+ )))
+ }
+ }
+
struct ContextLengthExceededProvider;
#[async_trait]
@@ -2533,6 +2602,53 @@ mod tests {
);
}
+ #[tokio::test]
+ async fn web_search_preflight_uses_turn_context_for_permission_check() {
+ let agent = Agent::new();
+ {
+ let registry_arc = agent.tool_registry().clone();
+ let mut registry = registry_arc.write().await;
+ registry.register(Box::new(TurnContextGatedWebSearchTool));
+ }
+
+ let policy = resolve_request_tool_policy_with_mode(
+ Some(true),
+ Some(RequestToolPolicyMode::Required),
+ false,
+ );
+ let mut metadata = HashMap::new();
+ metadata.insert("webSearchEnabled".to_string(), serde_json::json!(true));
+ let turn_context = TurnContextOverride {
+ metadata,
+ ..TurnContextOverride::default()
+ };
+ let mut tracker = WebSearchExecutionTracker::default();
+
+ let execution = execute_web_search_preflight_if_needed(
+ &agent,
+ "session-web-preflight-permission",
+ "继续",
+ None,
+ None,
+ Some(turn_context),
+ &policy,
+ &mut tracker,
+ )
+ .await
+ .expect("预调用应继承 turn context 并免确认执行");
+
+ assert!(execution
+ .system_prompt_appendix
+ .as_deref()
+ .unwrap_or_default()
+ .contains("预检索测试结果"));
+ assert!(execution.events.iter().any(|event| matches!(
+ event,
+ RuntimeAgentEvent::ToolEnd { result, .. } if result.success
+ )));
+ assert!(tracker.validate_web_search_requirement(&policy).is_ok());
+ }
+
#[test]
fn discarded_optional_preflight_attempt_should_not_force_synthesis_retry() {
let diagnostics = StreamEventDiagnostics::default();
diff --git a/src-tauri/crates/agent/src/runtime_queue.rs b/src-tauri/crates/agent/src/runtime_queue.rs
index 169a30478..0d06900a9 100644
--- a/src-tauri/crates/agent/src/runtime_queue.rs
+++ b/src-tauri/crates/agent/src/runtime_queue.rs
@@ -30,6 +30,7 @@ fn emit_runtime_queue_event(
fn spawn_runtime_turn_task(
session_id: String,
+ event_name: String,
context: C,
executor: RuntimeQueueExecutor,
emitter: RuntimeQueueEventEmitter,
@@ -38,6 +39,8 @@ fn spawn_runtime_turn_task(
C: Clone + Send + Sync + 'static,
{
let thread_name = format!("lime-runtime-turn-{}", session_id);
+ let event_name_for_thread = event_name.clone();
+ let emitter_for_thread = emitter.clone();
let spawn_result = std::thread::Builder::new()
.name(thread_name)
.stack_size(RUNTIME_TURN_THREAD_STACK_SIZE)
@@ -52,9 +55,12 @@ fn spawn_runtime_turn_task(
{
Ok(runtime) => runtime,
Err(error) => {
- tracing::error!(
- "[AsterAgent][Queue] 创建 runtime turn 专用运行时失败: {}",
- error
+ let message = format!("创建 runtime turn 专用运行时失败: {error}");
+ tracing::error!("[AsterAgent][Queue] {}", message);
+ emit_runtime_queue_event(
+ &emitter_for_thread,
+ &event_name_for_thread,
+ RuntimeAgentEvent::Error { message },
);
return;
}
@@ -62,27 +68,31 @@ fn spawn_runtime_turn_task(
runtime.block_on(async move {
let result = executor(context.clone(), payload).await;
+ if let Err(error) = result {
+ tracing::warn!("[AsterAgent][Queue] 队列任务执行失败: {}", error);
+ emit_runtime_queue_event(
+ &emitter_for_thread,
+ &event_name_for_thread,
+ RuntimeAgentEvent::Error { message: error },
+ );
+ }
if let Err(error) = continue_runtime_queue_after_turn(
session_id,
context.clone(),
executor.clone(),
- emitter.clone(),
+ emitter_for_thread.clone(),
)
.await
{
tracing::warn!("[AsterAgent][Queue] 调度下一条排队 turn 失败: {}", error);
}
- if let Err(error) = result {
- tracing::warn!("[AsterAgent][Queue] 队列任务执行失败: {}", error);
- }
});
});
if let Err(error) = spawn_result {
- tracing::error!(
- "[AsterAgent][Queue] 启动 runtime turn 专用线程失败: {}",
- error
- );
+ let message = format!("启动 runtime turn 专用线程失败: {error}");
+ tracing::error!("[AsterAgent][Queue] {}", message);
+ emit_runtime_queue_event(&emitter, &event_name, RuntimeAgentEvent::Error { message });
}
}
@@ -138,6 +148,7 @@ where
spawn_runtime_turn_task(
session_id,
+ event_name,
context,
executor,
emitter,
@@ -192,7 +203,14 @@ where
.map_err(|error| format!("提交 runtime queue turn 失败: {error}"))?
{
RuntimeQueueSubmitResult::StartNow => {
- spawn_runtime_turn_task(session_id, context, executor, emitter, queued_task.payload);
+ spawn_runtime_turn_task(
+ session_id,
+ queued_task.event_name,
+ context,
+ executor,
+ emitter,
+ queued_task.payload,
+ );
Ok(())
}
RuntimeQueueSubmitResult::Busy => Err("当前会话仍在生成,无法立即开始执行".to_string()),
diff --git a/src-tauri/crates/core/src/agent/types.rs b/src-tauri/crates/core/src/agent/types.rs
index 6bc14db5b..f1518527e 100644
--- a/src-tauri/crates/core/src/agent/types.rs
+++ b/src-tauri/crates/core/src/agent/types.rs
@@ -29,8 +29,6 @@ pub enum ProviderType {
Qwen,
/// Codex (OpenAI 兼容)
Codex,
- /// Antigravity (OpenAI 兼容)
- Antigravity,
/// iFlow (OpenAI 兼容)
IFlow,
}
@@ -47,7 +45,6 @@ impl ProviderType {
"openai" => Self::OpenAI,
"qwen" => Self::Qwen,
"codex" => Self::Codex,
- "antigravity" => Self::Antigravity,
"iflow" => Self::IFlow,
_ => Self::OpenAI, // 默认使用 OpenAI 协议
}
@@ -116,7 +113,7 @@ impl ProviderType {
pub fn is_openai_compatible(&self) -> bool {
matches!(
self,
- Self::OpenAI | Self::Qwen | Self::Codex | Self::Antigravity | Self::IFlow | Self::Kiro
+ Self::OpenAI | Self::Qwen | Self::Codex | Self::IFlow | Self::Kiro
)
}
}
diff --git a/src-tauri/crates/core/src/config/tests.rs b/src-tauri/crates/core/src/config/tests.rs
index b31a85ad8..5a5f823ee 100644
--- a/src-tauri/crates/core/src/config/tests.rs
+++ b/src-tauri/crates/core/src/config/tests.rs
@@ -2507,7 +2507,6 @@ fn arb_valid_provider_type() -> impl Strategy {
Just("gemini".to_string()),
Just("openai".to_string()),
Just("claude".to_string()),
- Just("antigravity".to_string()),
Just("vertex".to_string()),
Just("gemini_api_key".to_string()),
Just("codex".to_string()),
diff --git a/src-tauri/crates/core/src/config/types.rs b/src-tauri/crates/core/src/config/types.rs
index 3a1fc7c1b..db5f57f1d 100644
--- a/src-tauri/crates/core/src/config/types.rs
+++ b/src-tauri/crates/core/src/config/types.rs
@@ -1577,7 +1577,7 @@ pub struct RoutingConfig {
}
fn default_provider() -> String {
- "kiro".to_string()
+ "openai".to_string()
}
impl Default for RoutingConfig {
@@ -1982,51 +1982,6 @@ impl Default for ModelsConfig {
},
);
- // Antigravity
- providers.insert(
- "antigravity".to_string(),
- ProviderModelsConfig {
- label: "Antigravity".to_string(),
- models: vec![
- ModelInfo {
- id: "gemini-3-pro-preview".to_string(),
- name: None,
- enabled: true,
- },
- ModelInfo {
- id: "gemini-3-pro-image-preview".to_string(),
- name: None,
- enabled: true,
- },
- ModelInfo {
- id: "gemini-3-flash-preview".to_string(),
- name: None,
- enabled: true,
- },
- ModelInfo {
- id: "gemini-2.5-computer-use-preview-10-2025".to_string(),
- name: None,
- enabled: true,
- },
- ModelInfo {
- id: "gemini-claude-sonnet-4-5".to_string(),
- name: None,
- enabled: true,
- },
- ModelInfo {
- id: "gemini-claude-sonnet-4-5-thinking".to_string(),
- name: None,
- enabled: true,
- },
- ModelInfo {
- id: "gemini-claude-opus-4-5-thinking".to_string(),
- name: None,
- enabled: true,
- },
- ],
- },
- );
-
// Submodel
providers.insert(
"submodel".to_string(),
diff --git a/src-tauri/crates/core/src/connect/README.md b/src-tauri/crates/core/src/connect/README.md
index 0b5cf826b..4b69fc19b 100644
--- a/src-tauri/crates/core/src/connect/README.md
+++ b/src-tauri/crates/core/src/connect/README.md
@@ -6,7 +6,7 @@
Lime Connect 模块,实现中转商生态合作方案。
通过 Deep Link 协议实现一键配置功能,支持中转商品牌展示。
-API Key 直接集成到凭证池系统,无需单独存储。
+API Key 直接集成到 API Key Provider 主路径,不再进入旧凭证池系统。
## 功能概述
@@ -43,7 +43,7 @@ API Key 直接集成到凭证池系统,无需单独存储。
- Requirements 1.x - Deep Link 协议处理
- Requirements 2.x - 中转商注册表管理
-- Requirements 4.x - API Key 存储(已集成到凭证池系统)
+- Requirements 4.x - API Key 存储(已集成到 API Key Provider)
- Requirements 5.3 - 统计回调(Webhook)
## 更新提醒
diff --git a/src-tauri/crates/core/src/connect/mod.rs b/src-tauri/crates/core/src/connect/mod.rs
index 7571cde15..3c49baf9a 100644
--- a/src-tauri/crates/core/src/connect/mod.rs
+++ b/src-tauri/crates/core/src/connect/mod.rs
@@ -20,7 +20,7 @@
//! let registry = RelayRegistry::new(cache_path);
//! let relay_info = registry.get(&payload.relay);
//!
-//! // API Key 直接保存到凭证池系统(通过 ProviderPoolService)
+//! // API Key 通过 API Key Provider 主路径保存
//! ```
// 子模块声明
diff --git a/src-tauri/crates/core/src/credential/health.rs b/src-tauri/crates/core/src/credential/health.rs
deleted file mode 100644
index 64669d172..000000000
--- a/src-tauri/crates/core/src/credential/health.rs
+++ /dev/null
@@ -1,426 +0,0 @@
-//! 凭证健康检查器实现
-//!
-//! 提供凭证健康状态检查和自动更新功能
-
-use super::pool::{CredentialPool, PoolError};
-use super::types::{Credential, CredentialStatus};
-use chrono::{DateTime, Utc};
-use serde::{Deserialize, Serialize};
-use std::time::Duration;
-
-/// 健康状态
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub enum HealthStatus {
- /// 健康
- Healthy,
- /// 不健康
- Unhealthy {
- /// 不健康原因
- reason: String,
- /// 连续失败次数
- consecutive_failures: u32,
- },
- /// 未知(未检查过)
- Unknown,
-}
-
-/// 健康检查结果
-#[derive(Debug, Clone)]
-pub struct HealthCheckResult {
- /// 凭证 ID
- pub credential_id: String,
- /// 健康状态
- pub status: HealthStatus,
- /// 检查时间
- pub checked_at: DateTime,
- /// 检查延迟(毫秒)
- pub latency_ms: Option,
-}
-
-/// 健康检查配置
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct HealthCheckConfig {
- /// 检查间隔
- pub check_interval: Duration,
- /// 连续失败阈值(达到此值标记为不健康)
- pub failure_threshold: u32,
- /// 恢复阈值(连续成功此次数后恢复为健康)
- pub recovery_threshold: u32,
-}
-
-impl Default for HealthCheckConfig {
- fn default() -> Self {
- Self {
- check_interval: Duration::from_secs(60),
- failure_threshold: 3,
- recovery_threshold: 1,
- }
- }
-}
-
-/// 健康检查器 - 管理凭证健康状态
-pub struct HealthChecker {
- /// 配置
- config: HealthCheckConfig,
-}
-
-impl HealthChecker {
- /// 创建新的健康检查器
- pub fn new(config: HealthCheckConfig) -> Self {
- Self { config }
- }
-
- /// 使用默认配置创建健康检查器
- pub fn with_defaults() -> Self {
- Self::new(HealthCheckConfig::default())
- }
-
- /// 获取配置
- pub fn config(&self) -> &HealthCheckConfig {
- &self.config
- }
-
- /// 获取失败阈值
- pub fn failure_threshold(&self) -> u32 {
- self.config.failure_threshold
- }
-
- /// 获取恢复阈值
- pub fn recovery_threshold(&self) -> u32 {
- self.config.recovery_threshold
- }
-
- /// 检查单个凭证的健康状态
- ///
- /// 根据凭证的统计信息判断健康状态
- pub fn check(&self, credential: &Credential) -> HealthCheckResult {
- let status = self.evaluate_health(credential);
-
- HealthCheckResult {
- credential_id: credential.id.clone(),
- status,
- checked_at: Utc::now(),
- latency_ms: if credential.stats.successful_requests > 0 {
- Some(credential.stats.avg_latency_ms as u64)
- } else {
- None
- },
- }
- }
-
- /// 评估凭证健康状态
- fn evaluate_health(&self, credential: &Credential) -> HealthStatus {
- // 如果已经被标记为不健康,返回当前状态
- if let CredentialStatus::Unhealthy { reason } = &credential.status {
- return HealthStatus::Unhealthy {
- reason: reason.clone(),
- consecutive_failures: credential.stats.consecutive_failures,
- };
- }
-
- // 检查连续失败次数
- if credential.stats.consecutive_failures >= self.config.failure_threshold {
- return HealthStatus::Unhealthy {
- reason: format!(
- "连续失败 {} 次(阈值: {})",
- credential.stats.consecutive_failures, self.config.failure_threshold
- ),
- consecutive_failures: credential.stats.consecutive_failures,
- };
- }
-
- // 如果没有请求记录,状态未知
- if credential.stats.total_requests == 0 {
- return HealthStatus::Unknown;
- }
-
- HealthStatus::Healthy
- }
-
- /// 记录凭证使用失败并更新健康状态
- ///
- /// 如果连续失败次数达到阈值,自动标记为不健康
- ///
- /// # 返回
- /// - `true` 如果凭证被标记为不健康
- /// - `false` 如果凭证仍然健康
- pub fn record_failure(
- &self,
- pool: &CredentialPool,
- credential_id: &str,
- ) -> Result {
- // 记录失败
- pool.record_failure(credential_id)?;
-
- // 获取更新后的凭证
- let credential = pool
- .get(credential_id)
- .ok_or_else(|| PoolError::CredentialNotFound(credential_id.to_string()))?;
-
- // 检查是否需要标记为不健康
- if credential.stats.consecutive_failures >= self.config.failure_threshold {
- let reason = format!("连续认证失败 {} 次", credential.stats.consecutive_failures);
- pool.mark_unhealthy(credential_id, reason)?;
- return Ok(true);
- }
-
- Ok(false)
- }
-
- /// 记录凭证使用成功并更新健康状态
- ///
- /// 如果凭证之前不健康,成功后会恢复为健康状态
- ///
- /// # 返回
- /// - `true` 如果凭证从不健康恢复为健康
- /// - `false` 如果凭证状态未改变
- pub fn record_success(
- &self,
- pool: &CredentialPool,
- credential_id: &str,
- latency_ms: u64,
- ) -> Result {
- // 获取当前状态
- let was_unhealthy = pool
- .get(credential_id)
- .map(|c| matches!(c.status, CredentialStatus::Unhealthy { .. }))
- .unwrap_or(false);
-
- // 记录成功
- pool.record_success(credential_id, latency_ms)?;
-
- // 如果之前不健康,恢复为健康
- if was_unhealthy {
- pool.mark_active(credential_id)?;
- return Ok(true);
- }
-
- Ok(false)
- }
-
- /// 批量检查凭证池中所有凭证的健康状态
- pub fn check_all(&self, pool: &CredentialPool) -> Vec {
- pool.all().iter().map(|cred| self.check(cred)).collect()
- }
-
- /// 获取池中不健康的凭证数量
- pub fn unhealthy_count(&self, pool: &CredentialPool) -> usize {
- pool.all()
- .iter()
- .filter(|cred| matches!(self.check(cred).status, HealthStatus::Unhealthy { .. }))
- .count()
- }
-
- /// 检查凭证是否应该被标记为不健康
- ///
- /// 这是一个纯函数,不会修改任何状态
- pub fn should_mark_unhealthy(&self, consecutive_failures: u32) -> bool {
- consecutive_failures >= self.config.failure_threshold
- }
-
- /// 尝试恢复不健康的凭证
- ///
- /// 将所有不健康的凭证恢复为活跃状态(用于手动恢复)
- pub fn recover_all(&self, pool: &CredentialPool) -> Vec {
- let mut recovered = Vec::new();
-
- for cred in pool.all() {
- if matches!(cred.status, CredentialStatus::Unhealthy { .. })
- && pool.mark_active(&cred.id).is_ok()
- {
- recovered.push(cred.id.clone());
- }
- }
-
- recovered
- }
-}
-
-impl Default for HealthChecker {
- fn default() -> Self {
- Self::with_defaults()
- }
-}
-
-#[cfg(test)]
-mod health_tests {
- use super::*;
- use crate::credential::CredentialData;
- use crate::ProviderType;
-
- fn create_test_credential(id: &str) -> Credential {
- Credential::new(
- id.to_string(),
- ProviderType::Kiro,
- CredentialData::ApiKey {
- key: format!("key-{id}"),
- base_url: None,
- },
- )
- }
-
- #[test]
- fn test_health_checker_new() {
- let checker = HealthChecker::with_defaults();
- assert_eq!(checker.failure_threshold(), 3);
- assert_eq!(checker.recovery_threshold(), 1);
- }
-
- #[test]
- fn test_health_checker_custom_config() {
- let config = HealthCheckConfig {
- check_interval: Duration::from_secs(30),
- failure_threshold: 5,
- recovery_threshold: 2,
- };
- let checker = HealthChecker::new(config);
- assert_eq!(checker.failure_threshold(), 5);
- assert_eq!(checker.recovery_threshold(), 2);
- }
-
- #[test]
- fn test_check_healthy_credential() {
- let checker = HealthChecker::with_defaults();
- let mut cred = create_test_credential("test-1");
-
- // 记录一些成功请求
- cred.stats.record_success(100);
- cred.stats.record_success(150);
-
- let result = checker.check(&cred);
- assert_eq!(result.credential_id, "test-1");
- assert!(matches!(result.status, HealthStatus::Healthy));
- }
-
- #[test]
- fn test_check_unknown_credential() {
- let checker = HealthChecker::with_defaults();
- let cred = create_test_credential("test-1");
-
- // 没有任何请求记录
- let result = checker.check(&cred);
- assert!(matches!(result.status, HealthStatus::Unknown));
- }
-
- #[test]
- fn test_check_unhealthy_credential() {
- let checker = HealthChecker::with_defaults();
- let mut cred = create_test_credential("test-1");
-
- // 记录 3 次连续失败
- cred.stats.record_failure();
- cred.stats.record_failure();
- cred.stats.record_failure();
-
- let result = checker.check(&cred);
- assert!(matches!(result.status, HealthStatus::Unhealthy { .. }));
-
- if let HealthStatus::Unhealthy {
- consecutive_failures,
- ..
- } = result.status
- {
- assert_eq!(consecutive_failures, 3);
- }
- }
-
- #[test]
- fn test_record_failure_marks_unhealthy() {
- let checker = HealthChecker::with_defaults();
- let pool = CredentialPool::new(ProviderType::Kiro);
- pool.add(create_test_credential("test-1")).unwrap();
-
- // 前两次失败不应标记为不健康
- assert!(!checker.record_failure(&pool, "test-1").unwrap());
- assert!(!checker.record_failure(&pool, "test-1").unwrap());
-
- // 第三次失败应标记为不健康
- assert!(checker.record_failure(&pool, "test-1").unwrap());
-
- // 验证状态
- let cred = pool.get("test-1").unwrap();
- assert!(matches!(cred.status, CredentialStatus::Unhealthy { .. }));
- }
-
- #[test]
- fn test_record_success_recovers_unhealthy() {
- let checker = HealthChecker::with_defaults();
- let pool = CredentialPool::new(ProviderType::Kiro);
- pool.add(create_test_credential("test-1")).unwrap();
-
- // 标记为不健康
- pool.mark_unhealthy("test-1", "test reason".to_string())
- .unwrap();
-
- // 记录成功应恢复
- let recovered = checker.record_success(&pool, "test-1", 100).unwrap();
- assert!(recovered);
-
- // 验证状态
- let cred = pool.get("test-1").unwrap();
- assert!(matches!(cred.status, CredentialStatus::Active));
- }
-
- #[test]
- fn test_check_all() {
- let checker = HealthChecker::with_defaults();
- let pool = CredentialPool::new(ProviderType::Kiro);
-
- pool.add(create_test_credential("cred-1")).unwrap();
- pool.add(create_test_credential("cred-2")).unwrap();
- pool.add(create_test_credential("cred-3")).unwrap();
-
- let results = checker.check_all(&pool);
- assert_eq!(results.len(), 3);
- }
-
- #[test]
- fn test_unhealthy_count() {
- let checker = HealthChecker::with_defaults();
- let pool = CredentialPool::new(ProviderType::Kiro);
-
- pool.add(create_test_credential("cred-1")).unwrap();
- pool.add(create_test_credential("cred-2")).unwrap();
- pool.add(create_test_credential("cred-3")).unwrap();
-
- // 标记一个为不健康
- pool.mark_unhealthy("cred-2", "test".to_string()).unwrap();
-
- assert_eq!(checker.unhealthy_count(&pool), 1);
- }
-
- #[test]
- fn test_should_mark_unhealthy() {
- let checker = HealthChecker::with_defaults();
-
- assert!(!checker.should_mark_unhealthy(0));
- assert!(!checker.should_mark_unhealthy(1));
- assert!(!checker.should_mark_unhealthy(2));
- assert!(checker.should_mark_unhealthy(3));
- assert!(checker.should_mark_unhealthy(4));
- }
-
- #[test]
- fn test_recover_all() {
- let checker = HealthChecker::with_defaults();
- let pool = CredentialPool::new(ProviderType::Kiro);
-
- pool.add(create_test_credential("cred-1")).unwrap();
- pool.add(create_test_credential("cred-2")).unwrap();
- pool.add(create_test_credential("cred-3")).unwrap();
-
- // 标记两个为不健康
- pool.mark_unhealthy("cred-1", "test".to_string()).unwrap();
- pool.mark_unhealthy("cred-3", "test".to_string()).unwrap();
-
- let recovered = checker.recover_all(&pool);
- assert_eq!(recovered.len(), 2);
- assert!(recovered.contains(&"cred-1".to_string()));
- assert!(recovered.contains(&"cred-3".to_string()));
-
- // 验证所有凭证都是活跃状态
- for cred in pool.all() {
- assert!(matches!(cred.status, CredentialStatus::Active));
- }
- }
-}
diff --git a/src-tauri/crates/core/src/credential/mod.rs b/src-tauri/crates/core/src/credential/mod.rs
deleted file mode 100644
index 8e5cc8eb0..000000000
--- a/src-tauri/crates/core/src/credential/mod.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-//! 凭证池核心类型和独立逻辑
-//!
-//! 包含凭证类型定义、凭证池管理、健康检查和风控模块。
-//! 负载均衡器(balancer)、配额管理(quota)和同步服务(sync)
-//! 因依赖 infra crate 保留在主 crate 中。
-
-pub mod health;
-pub mod pool;
-pub mod risk;
-pub mod types;
-
-pub use health::{HealthCheckConfig, HealthCheckResult, HealthChecker, HealthStatus};
-pub use pool::{CredentialPool, PoolError, PoolStatus};
-pub use risk::{CooldownConfig, RateLimitEvent, RateLimitStats, RiskController, RiskLevel};
-pub use types::{Credential, CredentialData, CredentialStats, CredentialStatus};
diff --git a/src-tauri/crates/core/src/credential/pool.rs b/src-tauri/crates/core/src/credential/pool.rs
deleted file mode 100644
index b45066b47..000000000
--- a/src-tauri/crates/core/src/credential/pool.rs
+++ /dev/null
@@ -1,416 +0,0 @@
-//! 凭证池实现
-//!
-//! 使用 DashMap 实现线程安全的凭证池管理
-
-use super::types::{Credential, CredentialStatus};
-use crate::ProviderType;
-use chrono::{DateTime, Duration, Utc};
-use dashmap::DashMap;
-use serde::{Deserialize, Serialize};
-use std::sync::atomic::{AtomicUsize, Ordering};
-
-/// 凭证池 - 管理同一 Provider 的多个凭证
-pub struct CredentialPool {
- /// 所属 Provider 类型
- provider: ProviderType,
- /// 凭证存储(id -> Credential)
- pub credentials: DashMap,
- /// 轮询索引(用于负载均衡)
- round_robin_index: AtomicUsize,
-}
-
-/// 凭证池状态
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct PoolStatus {
- /// Provider 类型
- pub provider: ProviderType,
- /// 总凭证数
- pub total: usize,
- /// 活跃凭证数
- pub active: usize,
- /// 冷却中凭证数
- pub cooldown: usize,
- /// 不健康凭证数
- pub unhealthy: usize,
- /// 已禁用凭证数
- pub disabled: usize,
-}
-
-/// 凭证池错误
-#[derive(Debug, Clone, PartialEq)]
-pub enum PoolError {
- /// 凭证已存在
- CredentialExists(String),
- /// 凭证不存在
- CredentialNotFound(String),
- /// 凭证池为空
- EmptyPool,
- /// 所有凭证不可用
- NoAvailableCredential,
-}
-
-impl std::fmt::Display for PoolError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- PoolError::CredentialExists(id) => write!(f, "凭证已存在: {id}"),
- PoolError::CredentialNotFound(id) => write!(f, "凭证不存在: {id}"),
- PoolError::EmptyPool => write!(f, "凭证池为空"),
- PoolError::NoAvailableCredential => write!(f, "没有可用的凭证"),
- }
- }
-}
-
-impl std::error::Error for PoolError {}
-
-impl CredentialPool {
- /// 创建新的凭证池
- pub fn new(provider: ProviderType) -> Self {
- Self {
- provider,
- credentials: DashMap::new(),
- round_robin_index: AtomicUsize::new(0),
- }
- }
-
- /// 获取 Provider 类型
- pub fn provider(&self) -> ProviderType {
- self.provider
- }
-
- /// 获取凭证池大小
- pub fn len(&self) -> usize {
- self.credentials.len()
- }
-
- /// 检查凭证池是否为空
- pub fn is_empty(&self) -> bool {
- self.credentials.is_empty()
- }
-
- /// 添加凭证到池中
- ///
- /// # 错误
- /// - 如果凭证 ID 已存在,返回 `PoolError::CredentialExists`
- pub fn add(&self, credential: Credential) -> Result<(), PoolError> {
- if self.credentials.contains_key(&credential.id) {
- return Err(PoolError::CredentialExists(credential.id.clone()));
- }
- self.credentials.insert(credential.id.clone(), credential);
- Ok(())
- }
-
- /// 从池中移除凭证
- ///
- /// # 错误
- /// - 如果凭证不存在,返回 `PoolError::CredentialNotFound`
- pub fn remove(&self, id: &str) -> Result {
- self.credentials
- .remove(id)
- .map(|(_, cred)| cred)
- .ok_or_else(|| PoolError::CredentialNotFound(id.to_string()))
- }
-
- /// 获取凭证(只读)
- pub fn get(&self, id: &str) -> Option {
- self.credentials.get(id).map(|r| r.value().clone())
- }
-
- /// 获取所有凭证 ID
- pub fn ids(&self) -> Vec {
- self.credentials.iter().map(|r| r.key().clone()).collect()
- }
-
- /// 获取所有凭证
- pub fn all(&self) -> Vec {
- self.credentials.iter().map(|r| r.value().clone()).collect()
- }
-
- /// 检查凭证是否存在
- pub fn contains(&self, id: &str) -> bool {
- self.credentials.contains_key(id)
- }
-
- /// 获取池状态
- pub fn status(&self) -> PoolStatus {
- let mut active = 0;
- let mut cooldown = 0;
- let mut unhealthy = 0;
- let mut disabled = 0;
-
- for entry in self.credentials.iter() {
- match &entry.value().status {
- CredentialStatus::Active => active += 1,
- CredentialStatus::Cooldown { .. } => cooldown += 1,
- CredentialStatus::Unhealthy { .. } => unhealthy += 1,
- CredentialStatus::Disabled => disabled += 1,
- }
- }
-
- PoolStatus {
- provider: self.provider,
- total: self.credentials.len(),
- active,
- cooldown,
- unhealthy,
- disabled,
- }
- }
-
- /// 获取活跃凭证数量
- pub fn active_count(&self) -> usize {
- self.credentials
- .iter()
- .filter(|r| r.value().is_available())
- .count()
- }
-
- /// 标记凭证为冷却状态
- pub fn mark_cooldown(&self, id: &str, duration: Duration) -> Result<(), PoolError> {
- let mut entry = self
- .credentials
- .get_mut(id)
- .ok_or_else(|| PoolError::CredentialNotFound(id.to_string()))?;
-
- entry.status = CredentialStatus::Cooldown {
- until: Utc::now() + duration,
- };
- Ok(())
- }
-
- /// 标记凭证为不健康状态
- pub fn mark_unhealthy(&self, id: &str, reason: String) -> Result<(), PoolError> {
- let mut entry = self
- .credentials
- .get_mut(id)
- .ok_or_else(|| PoolError::CredentialNotFound(id.to_string()))?;
-
- entry.status = CredentialStatus::Unhealthy { reason };
- Ok(())
- }
-
- /// 恢复凭证为活跃状态
- pub fn mark_active(&self, id: &str) -> Result<(), PoolError> {
- let mut entry = self
- .credentials
- .get_mut(id)
- .ok_or_else(|| PoolError::CredentialNotFound(id.to_string()))?;
-
- entry.status = CredentialStatus::Active;
- Ok(())
- }
-
- /// 更新过期的冷却状态
- /// 将冷却期已过的凭证恢复为活跃状态
- pub fn refresh_cooldowns(&self) {
- let now = Utc::now();
- for mut entry in self.credentials.iter_mut() {
- if let CredentialStatus::Cooldown { until } = &entry.status {
- if *until <= now {
- entry.status = CredentialStatus::Active;
- }
- }
- }
- }
-
- /// 获取下一个可用凭证(轮询策略)
- ///
- /// # 错误
- /// - 如果池为空,返回 `PoolError::EmptyPool`
- /// - 如果没有可用凭证,返回 `PoolError::NoAvailableCredential`
- pub fn next_available(&self) -> Result {
- if self.credentials.is_empty() {
- return Err(PoolError::EmptyPool);
- }
-
- // 先刷新冷却状态
- self.refresh_cooldowns();
-
- // 收集所有活跃凭证
- let active_creds: Vec<_> = self
- .credentials
- .iter()
- .filter(|r| r.value().is_available())
- .map(|r| r.value().clone())
- .collect();
-
- if active_creds.is_empty() {
- return Err(PoolError::NoAvailableCredential);
- }
-
- // 轮询选择
- let index = self.round_robin_index.fetch_add(1, Ordering::SeqCst) % active_creds.len();
- Ok(active_creds[index].clone())
- }
-
- /// 获取最早恢复时间(当所有凭证都在冷却时)
- pub fn earliest_recovery(&self) -> Option> {
- self.credentials
- .iter()
- .filter_map(|r| {
- if let CredentialStatus::Cooldown { until } = &r.value().status {
- Some(*until)
- } else {
- None
- }
- })
- .min()
- }
-
- /// 记录凭证使用成功
- pub fn record_success(&self, id: &str, latency_ms: u64) -> Result<(), PoolError> {
- let mut entry = self
- .credentials
- .get_mut(id)
- .ok_or_else(|| PoolError::CredentialNotFound(id.to_string()))?;
-
- entry.mark_used();
- entry.stats.record_success(latency_ms);
- Ok(())
- }
-
- /// 记录凭证使用失败
- pub fn record_failure(&self, id: &str) -> Result<(), PoolError> {
- let mut entry = self
- .credentials
- .get_mut(id)
- .ok_or_else(|| PoolError::CredentialNotFound(id.to_string()))?;
-
- entry.mark_used();
- entry.stats.record_failure();
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod pool_tests {
- use super::*;
- use crate::credential::CredentialData;
-
- fn create_test_credential(id: &str) -> Credential {
- Credential::new(
- id.to_string(),
- ProviderType::Kiro,
- CredentialData::ApiKey {
- key: format!("key-{id}"),
- base_url: None,
- },
- )
- }
-
- #[test]
- fn test_pool_new() {
- let pool = CredentialPool::new(ProviderType::Kiro);
- assert_eq!(pool.provider(), ProviderType::Kiro);
- assert!(pool.is_empty());
- assert_eq!(pool.len(), 0);
- }
-
- #[test]
- fn test_pool_add() {
- let pool = CredentialPool::new(ProviderType::Kiro);
- let cred = create_test_credential("test-1");
-
- assert!(pool.add(cred.clone()).is_ok());
- assert_eq!(pool.len(), 1);
- assert!(pool.contains("test-1"));
-
- // 重复添加应失败
- let result = pool.add(cred);
- assert!(matches!(result, Err(PoolError::CredentialExists(_))));
- }
-
- #[test]
- fn test_pool_remove() {
- let pool = CredentialPool::new(ProviderType::Kiro);
- let cred = create_test_credential("test-1");
-
- pool.add(cred).unwrap();
- assert_eq!(pool.len(), 1);
-
- let removed = pool.remove("test-1").unwrap();
- assert_eq!(removed.id, "test-1");
- assert!(pool.is_empty());
-
- // 移除不存在的凭证应失败
- let result = pool.remove("test-1");
- assert!(matches!(result, Err(PoolError::CredentialNotFound(_))));
- }
-
- #[test]
- fn test_pool_get() {
- let pool = CredentialPool::new(ProviderType::Kiro);
- let cred = create_test_credential("test-1");
-
- pool.add(cred).unwrap();
-
- let retrieved = pool.get("test-1");
- assert!(retrieved.is_some());
- assert_eq!(retrieved.unwrap().id, "test-1");
-
- assert!(pool.get("nonexistent").is_none());
- }
-
- #[test]
- fn test_pool_status() {
- let pool = CredentialPool::new(ProviderType::Kiro);
-
- pool.add(create_test_credential("active-1")).unwrap();
- pool.add(create_test_credential("active-2")).unwrap();
- pool.add(create_test_credential("cooldown-1")).unwrap();
- pool.add(create_test_credential("unhealthy-1")).unwrap();
-
- pool.mark_cooldown("cooldown-1", Duration::hours(1))
- .unwrap();
- pool.mark_unhealthy("unhealthy-1", "test reason".to_string())
- .unwrap();
-
- let status = pool.status();
- assert_eq!(status.total, 4);
- assert_eq!(status.active, 2);
- assert_eq!(status.cooldown, 1);
- assert_eq!(status.unhealthy, 1);
- assert_eq!(status.disabled, 0);
- }
-
- #[test]
- fn test_pool_next_available_empty() {
- let pool = CredentialPool::new(ProviderType::Kiro);
- let result = pool.next_available();
- assert!(matches!(result, Err(PoolError::EmptyPool)));
- }
-
- #[test]
- fn test_pool_next_available_all_cooldown() {
- let pool = CredentialPool::new(ProviderType::Kiro);
- pool.add(create_test_credential("cred-1")).unwrap();
- pool.mark_cooldown("cred-1", Duration::hours(1)).unwrap();
-
- let result = pool.next_available();
- assert!(matches!(result, Err(PoolError::NoAvailableCredential)));
- }
-
- #[test]
- fn test_pool_record_success() {
- let pool = CredentialPool::new(ProviderType::Kiro);
- pool.add(create_test_credential("test-1")).unwrap();
-
- pool.record_success("test-1", 100).unwrap();
-
- let cred = pool.get("test-1").unwrap();
- assert_eq!(cred.stats.total_requests, 1);
- assert_eq!(cred.stats.successful_requests, 1);
- assert!(cred.last_used.is_some());
- }
-
- #[test]
- fn test_pool_record_failure() {
- let pool = CredentialPool::new(ProviderType::Kiro);
- pool.add(create_test_credential("test-1")).unwrap();
-
- pool.record_failure("test-1").unwrap();
-
- let cred = pool.get("test-1").unwrap();
- assert_eq!(cred.stats.total_requests, 1);
- assert_eq!(cred.stats.consecutive_failures, 1);
- }
-}
diff --git a/src-tauri/crates/core/src/credential/risk.rs b/src-tauri/crates/core/src/credential/risk.rs
deleted file mode 100644
index c5e433dbe..000000000
--- a/src-tauri/crates/core/src/credential/risk.rs
+++ /dev/null
@@ -1,586 +0,0 @@
-//! 风控模块
-//!
-//! 提供限流检测、冷却期管理和风险评估功能。
-//!
-//! ## 功能
-//!
-//! - **限流检测**: 检测 API 返回的限流错误(429、rate limit)
-//! - **冷却期管理**: 自动计算和管理凭证冷却时间
-//! - **风险评估**: 根据历史数据评估凭证风险等级
-
-use chrono::{DateTime, Duration, Utc};
-use dashmap::DashMap;
-use serde::{Deserialize, Serialize};
-use std::collections::VecDeque;
-use std::sync::atomic::{AtomicU64, Ordering};
-
-/// 风险等级
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum RiskLevel {
- /// 低风险 - 正常使用
- Low,
- /// 中风险 - 接近限流阈值
- Medium,
- /// 高风险 - 频繁触发限流
- High,
- /// 危险 - 需要立即冷却
- Critical,
-}
-
-impl RiskLevel {
- /// 获取风险等级对应的冷却时间倍数
- pub fn cooldown_multiplier(&self) -> f64 {
- match self {
- RiskLevel::Low => 1.0,
- RiskLevel::Medium => 1.5,
- RiskLevel::High => 2.0,
- RiskLevel::Critical => 3.0,
- }
- }
-
- /// 获取风险等级描述
- pub fn description(&self) -> &'static str {
- match self {
- RiskLevel::Low => "正常",
- RiskLevel::Medium => "接近限流",
- RiskLevel::High => "频繁限流",
- RiskLevel::Critical => "需要冷却",
- }
- }
-}
-
-/// 限流事件
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RateLimitEvent {
- /// 凭证 ID
- pub credential_id: String,
- /// 事件时间
- pub timestamp: DateTime,
- /// HTTP 状态码
- pub status_code: Option,
- /// 错误消息
- pub error_message: Option,
- /// 建议的重试时间(秒)
- pub retry_after_secs: Option,
-}
-
-impl RateLimitEvent {
- /// 创建新的限流事件
- pub fn new(credential_id: String) -> Self {
- Self {
- credential_id,
- timestamp: Utc::now(),
- status_code: None,
- error_message: None,
- retry_after_secs: None,
- }
- }
-
- /// 设置状态码
- pub fn with_status_code(mut self, code: u16) -> Self {
- self.status_code = Some(code);
- self
- }
-
- /// 设置错误消息
- pub fn with_error_message(mut self, message: String) -> Self {
- self.error_message = Some(message);
- self
- }
-
- /// 设置重试时间
- pub fn with_retry_after(mut self, secs: u64) -> Self {
- self.retry_after_secs = Some(secs);
- self
- }
-}
-
-/// 冷却配置
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct CooldownConfig {
- /// 基础冷却时间(秒)
- pub base_cooldown_secs: u64,
- /// 最大冷却时间(秒)
- pub max_cooldown_secs: u64,
- /// 冷却时间增长因子(指数退避)
- pub backoff_factor: f64,
- /// 限流事件窗口大小(保留最近 N 个事件)
- pub event_window_size: usize,
- /// 限流事件时间窗口(秒)- 只统计此时间内的事件
- pub event_time_window_secs: u64,
- /// 触发中风险的限流次数阈值
- pub medium_risk_threshold: u32,
- /// 触发高风险的限流次数阈值
- pub high_risk_threshold: u32,
- /// 触发危险的限流次数阈值
- pub critical_risk_threshold: u32,
-}
-
-impl Default for CooldownConfig {
- fn default() -> Self {
- Self {
- base_cooldown_secs: 60, // 1 分钟
- max_cooldown_secs: 3600, // 1 小时
- backoff_factor: 2.0, // 指数退避因子
- event_window_size: 100, // 保留最近 100 个事件
- event_time_window_secs: 3600, // 1 小时内的事件
- medium_risk_threshold: 3, // 3 次限流 -> 中风险
- high_risk_threshold: 5, // 5 次限流 -> 高风险
- critical_risk_threshold: 10, // 10 次限流 -> 危险
- }
- }
-}
-
-/// 凭证风控状态
-#[derive(Debug)]
-struct CredentialRiskState {
- /// 限流事件历史
- events: VecDeque,
- /// 连续限流次数
- consecutive_rate_limits: AtomicU64,
- /// 当前冷却结束时间
- cooldown_until: Option>,
- /// 上次限流时间
- last_rate_limit: Option>,
-}
-
-impl CredentialRiskState {
- fn new() -> Self {
- Self {
- events: VecDeque::new(),
- consecutive_rate_limits: AtomicU64::new(0),
- cooldown_until: None,
- last_rate_limit: None,
- }
- }
-}
-
-/// 风控控制器
-///
-/// 管理凭证的限流检测和冷却期
-pub struct RiskController {
- /// 配置
- config: CooldownConfig,
- /// 各凭证的风控状态
- states: DashMap,
-}
-
-impl RiskController {
- /// 创建新的风控控制器
- pub fn new(config: CooldownConfig) -> Self {
- Self {
- config,
- states: DashMap::new(),
- }
- }
-
- /// 使用默认配置创建
- pub fn with_defaults() -> Self {
- Self::new(CooldownConfig::default())
- }
-
- /// 获取配置
- pub fn config(&self) -> &CooldownConfig {
- &self.config
- }
-
- /// 记录限流事件
- ///
- /// # 返回
- /// 建议的冷却时间(秒)
- pub fn record_rate_limit(&self, event: RateLimitEvent) -> u64 {
- let credential_id = event.credential_id.clone();
- let retry_after = event.retry_after_secs;
-
- let mut state = self
- .states
- .entry(credential_id.clone())
- .or_insert_with(CredentialRiskState::new);
-
- // 更新连续限流次数
- state.consecutive_rate_limits.fetch_add(1, Ordering::SeqCst);
- state.last_rate_limit = Some(Utc::now());
-
- // 添加事件到历史
- state.events.push_back(event);
-
- // 清理过期事件
- self.cleanup_old_events(&mut state);
-
- // 计算冷却时间
- let cooldown_secs = self.calculate_cooldown(&state, retry_after);
-
- // 设置冷却结束时间
- state.cooldown_until = Some(Utc::now() + Duration::seconds(cooldown_secs as i64));
-
- cooldown_secs
- }
-
- /// 记录成功请求(重置连续限流计数)
- pub fn record_success(&self, credential_id: &str) {
- if let Some(state) = self.states.get_mut(credential_id) {
- state.consecutive_rate_limits.store(0, Ordering::SeqCst);
- }
- }
-
- /// 获取凭证的风险等级
- pub fn get_risk_level(&self, credential_id: &str) -> RiskLevel {
- let state = match self.states.get(credential_id) {
- Some(s) => s,
- None => return RiskLevel::Low,
- };
-
- let recent_count = self.count_recent_events(&state);
-
- if recent_count >= self.config.critical_risk_threshold {
- RiskLevel::Critical
- } else if recent_count >= self.config.high_risk_threshold {
- RiskLevel::High
- } else if recent_count >= self.config.medium_risk_threshold {
- RiskLevel::Medium
- } else {
- RiskLevel::Low
- }
- }
-
- /// 检查凭证是否在冷却中
- pub fn is_in_cooldown(&self, credential_id: &str) -> bool {
- self.states
- .get(credential_id)
- .and_then(|state| state.cooldown_until)
- .map(|until| Utc::now() < until)
- .unwrap_or(false)
- }
-
- /// 获取凭证的冷却结束时间
- pub fn get_cooldown_until(&self, credential_id: &str) -> Option> {
- self.states
- .get(credential_id)
- .and_then(|state| state.cooldown_until)
- .filter(|until| Utc::now() < *until)
- }
-
- /// 获取凭证的剩余冷却时间(秒)
- pub fn get_remaining_cooldown_secs(&self, credential_id: &str) -> Option {
- self.get_cooldown_until(credential_id).map(|until| {
- let remaining = until - Utc::now();
- remaining.num_seconds().max(0) as u64
- })
- }
-
- /// 手动清除凭证的冷却状态
- pub fn clear_cooldown(&self, credential_id: &str) {
- if let Some(mut state) = self.states.get_mut(credential_id) {
- state.cooldown_until = None;
- state.consecutive_rate_limits.store(0, Ordering::SeqCst);
- }
- }
-
- /// 获取所有处于冷却中的凭证 ID
- pub fn get_cooling_credentials(&self) -> Vec {
- let now = Utc::now();
- self.states
- .iter()
- .filter(|entry| {
- entry
- .value()
- .cooldown_until
- .map(|until| now < until)
- .unwrap_or(false)
- })
- .map(|entry| entry.key().clone())
- .collect()
- }
-
- /// 获取凭证的限流事件统计
- pub fn get_event_stats(&self, credential_id: &str) -> Option {
- self.states.get(credential_id).map(|state| {
- let recent_count = self.count_recent_events(&state);
- let consecutive = state.consecutive_rate_limits.load(Ordering::SeqCst);
-
- RateLimitStats {
- total_events: state.events.len(),
- recent_events: recent_count as usize,
- consecutive_rate_limits: consecutive,
- last_rate_limit: state.last_rate_limit,
- cooldown_until: state.cooldown_until,
- risk_level: self.get_risk_level(credential_id),
- }
- })
- }
-
- /// 检测响应是否为限流错误
- pub fn is_rate_limit_error(status_code: u16, body: Option<&str>) -> bool {
- // HTTP 429 Too Many Requests
- if status_code == 429 {
- return true;
- }
-
- // 检查响应体中的限流关键词
- if let Some(body) = body {
- let body_lower = body.to_lowercase();
- if body_lower.contains("rate limit")
- || body_lower.contains("rate_limit")
- || body_lower.contains("ratelimit")
- || body_lower.contains("too many requests")
- || body_lower.contains("quota exceeded")
- || body_lower.contains("resource_exhausted")
- {
- return true;
- }
- }
-
- false
- }
-
- /// 从响应头解析 Retry-After
- pub fn parse_retry_after(header_value: &str) -> Option {
- // 尝试解析为秒数
- if let Ok(secs) = header_value.parse::() {
- return Some(secs);
- }
-
- // 尝试解析为 HTTP 日期格式
- if let Ok(date) = DateTime::parse_from_rfc2822(header_value) {
- let until = date.with_timezone(&Utc);
- let now = Utc::now();
- if until > now {
- return Some((until - now).num_seconds() as u64);
- }
- }
-
- None
- }
-
- /// 清理过期事件
- fn cleanup_old_events(&self, state: &mut CredentialRiskState) {
- let cutoff = Utc::now() - Duration::seconds(self.config.event_time_window_secs as i64);
-
- // 移除过期事件
- while let Some(front) = state.events.front() {
- if front.timestamp < cutoff {
- state.events.pop_front();
- } else {
- break;
- }
- }
-
- // 限制事件数量
- while state.events.len() > self.config.event_window_size {
- state.events.pop_front();
- }
- }
-
- /// 统计最近的限流事件数
- fn count_recent_events(&self, state: &CredentialRiskState) -> u32 {
- let cutoff = Utc::now() - Duration::seconds(self.config.event_time_window_secs as i64);
- state
- .events
- .iter()
- .filter(|e| e.timestamp >= cutoff)
- .count() as u32
- }
-
- /// 计算冷却时间
- fn calculate_cooldown(&self, state: &CredentialRiskState, retry_after: Option) -> u64 {
- // 如果有 Retry-After,优先使用
- if let Some(retry) = retry_after {
- return retry.min(self.config.max_cooldown_secs);
- }
-
- // 使用指数退避计算冷却时间
- let consecutive = state.consecutive_rate_limits.load(Ordering::SeqCst);
- let base = self.config.base_cooldown_secs as f64;
- let factor = self.config.backoff_factor;
-
- // cooldown = base * factor^(consecutive - 1)
- let cooldown = if consecutive > 0 {
- base * factor.powi((consecutive - 1) as i32)
- } else {
- base
- };
-
- // 根据风险等级调整
- let risk_level = self.get_risk_level_from_state(state);
- let adjusted = cooldown * risk_level.cooldown_multiplier();
-
- // 限制在最大值内
- (adjusted as u64).min(self.config.max_cooldown_secs)
- }
-
- /// 从状态计算风险等级
- fn get_risk_level_from_state(&self, state: &CredentialRiskState) -> RiskLevel {
- let recent_count = self.count_recent_events(state);
-
- if recent_count >= self.config.critical_risk_threshold {
- RiskLevel::Critical
- } else if recent_count >= self.config.high_risk_threshold {
- RiskLevel::High
- } else if recent_count >= self.config.medium_risk_threshold {
- RiskLevel::Medium
- } else {
- RiskLevel::Low
- }
- }
-}
-
-impl Default for RiskController {
- fn default() -> Self {
- Self::with_defaults()
- }
-}
-
-/// 限流事件统计
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RateLimitStats {
- /// 总事件数
- pub total_events: usize,
- /// 最近事件数(时间窗口内)
- pub recent_events: usize,
- /// 连续限流次数
- pub consecutive_rate_limits: u64,
- /// 上次限流时间
- pub last_rate_limit: Option>,
- /// 冷却结束时间
- pub cooldown_until: Option>,
- /// 风险等级
- pub risk_level: RiskLevel,
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_risk_controller_new() {
- let controller = RiskController::with_defaults();
- assert_eq!(controller.config().base_cooldown_secs, 60);
- }
-
- #[test]
- fn test_record_rate_limit() {
- let controller = RiskController::with_defaults();
- let event = RateLimitEvent::new("cred-1".to_string()).with_status_code(429);
-
- let cooldown = controller.record_rate_limit(event);
- assert!(cooldown >= 60); // 至少是基础冷却时间
-
- assert!(controller.is_in_cooldown("cred-1"));
- assert_eq!(controller.get_risk_level("cred-1"), RiskLevel::Low);
- }
-
- #[test]
- fn test_risk_level_escalation() {
- let controller = RiskController::with_defaults();
-
- // 记录多次限流事件
- for i in 0..5 {
- let event = RateLimitEvent::new("cred-1".to_string())
- .with_status_code(429)
- .with_error_message(format!("Rate limit {i}"));
- controller.record_rate_limit(event);
- }
-
- // 应该达到高风险
- assert_eq!(controller.get_risk_level("cred-1"), RiskLevel::High);
- }
-
- #[test]
- fn test_record_success_resets_consecutive() {
- let controller = RiskController::with_defaults();
-
- // 记录限流
- let event = RateLimitEvent::new("cred-1".to_string());
- controller.record_rate_limit(event);
-
- // 记录成功
- controller.record_success("cred-1");
-
- // 连续计数应该重置
- let stats = controller.get_event_stats("cred-1").unwrap();
- assert_eq!(stats.consecutive_rate_limits, 0);
- }
-
- #[test]
- fn test_clear_cooldown() {
- let controller = RiskController::with_defaults();
-
- let event = RateLimitEvent::new("cred-1".to_string());
- controller.record_rate_limit(event);
-
- assert!(controller.is_in_cooldown("cred-1"));
-
- controller.clear_cooldown("cred-1");
-
- assert!(!controller.is_in_cooldown("cred-1"));
- }
-
- #[test]
- fn test_is_rate_limit_error() {
- assert!(RiskController::is_rate_limit_error(429, None));
- assert!(RiskController::is_rate_limit_error(
- 200,
- Some("rate limit exceeded")
- ));
- assert!(RiskController::is_rate_limit_error(
- 500,
- Some("RESOURCE_EXHAUSTED")
- ));
- assert!(!RiskController::is_rate_limit_error(200, Some("success")));
- }
-
- #[test]
- fn test_parse_retry_after() {
- assert_eq!(RiskController::parse_retry_after("60"), Some(60));
- assert_eq!(RiskController::parse_retry_after("3600"), Some(3600));
- assert!(RiskController::parse_retry_after("invalid").is_none());
- }
-
- #[test]
- fn test_retry_after_priority() {
- let controller = RiskController::with_defaults();
-
- // 使用 retry_after 的事件
- let event = RateLimitEvent::new("cred-1".to_string()).with_retry_after(120);
-
- let cooldown = controller.record_rate_limit(event);
- assert_eq!(cooldown, 120); // 应该使用 retry_after 的值
- }
-
- #[test]
- fn test_exponential_backoff() {
- let controller = RiskController::with_defaults();
-
- // 第一次限流
- let event1 = RateLimitEvent::new("cred-1".to_string());
- let cooldown1 = controller.record_rate_limit(event1);
-
- // 第二次限流(应该更长)
- let event2 = RateLimitEvent::new("cred-1".to_string());
- let cooldown2 = controller.record_rate_limit(event2);
-
- assert!(cooldown2 > cooldown1);
- }
-
- #[test]
- fn test_get_cooling_credentials() {
- let controller = RiskController::with_defaults();
-
- controller.record_rate_limit(RateLimitEvent::new("cred-1".to_string()));
- controller.record_rate_limit(RateLimitEvent::new("cred-2".to_string()));
-
- let cooling = controller.get_cooling_credentials();
- assert_eq!(cooling.len(), 2);
- assert!(cooling.contains(&"cred-1".to_string()));
- assert!(cooling.contains(&"cred-2".to_string()));
- }
-
- #[test]
- fn test_risk_level_cooldown_multiplier() {
- assert_eq!(RiskLevel::Low.cooldown_multiplier(), 1.0);
- assert_eq!(RiskLevel::Medium.cooldown_multiplier(), 1.5);
- assert_eq!(RiskLevel::High.cooldown_multiplier(), 2.0);
- assert_eq!(RiskLevel::Critical.cooldown_multiplier(), 3.0);
- }
-}
diff --git a/src-tauri/crates/core/src/credential/types.rs b/src-tauri/crates/core/src/credential/types.rs
deleted file mode 100644
index c7c49f15d..000000000
--- a/src-tauri/crates/core/src/credential/types.rs
+++ /dev/null
@@ -1,243 +0,0 @@
-//! 凭证相关类型定义
-//!
-//! 定义凭证、凭证数据、凭证状态等核心类型
-
-use crate::ProviderType;
-use chrono::{DateTime, Utc};
-use serde::{Deserialize, Serialize};
-
-/// 凭证 - 表示单个 API 凭证
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct Credential {
- /// 唯一标识符
- pub id: String,
- /// 所属 Provider 类型
- pub provider: ProviderType,
- /// 凭证数据
- pub data: CredentialData,
- /// 创建时间
- pub created_at: DateTime,
- /// 最后使用时间
- pub last_used: Option>,
- /// 当前状态
- pub status: CredentialStatus,
- /// 统计信息
- pub stats: CredentialStats,
- /// Per-Key 代理 URL(覆盖全局代理)
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub proxy_url: Option,
-}
-
-impl Credential {
- /// 创建新凭证
- pub fn new(id: String, provider: ProviderType, data: CredentialData) -> Self {
- Self {
- id,
- provider,
- data,
- created_at: Utc::now(),
- last_used: None,
- status: CredentialStatus::Active,
- stats: CredentialStats::default(),
- proxy_url: None,
- }
- }
-
- /// 创建带代理的凭证
- pub fn with_proxy(mut self, proxy_url: Option) -> Self {
- self.proxy_url = proxy_url;
- self
- }
-
- /// 设置代理 URL
- pub fn set_proxy_url(&mut self, proxy_url: Option) {
- self.proxy_url = proxy_url;
- }
-
- /// 获取代理 URL
- pub fn proxy_url(&self) -> Option<&str> {
- self.proxy_url.as_deref()
- }
-
- /// 检查凭证是否可用(活跃状态)
- pub fn is_available(&self) -> bool {
- matches!(self.status, CredentialStatus::Active)
- }
-
- /// 更新最后使用时间
- pub fn mark_used(&mut self) {
- self.last_used = Some(Utc::now());
- }
-}
-
-/// 凭证数据 - 不同 Provider 有不同的凭证格式
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-#[serde(tag = "type", rename_all = "snake_case")]
-pub enum CredentialData {
- /// OAuth 凭证(用于 Kiro、Gemini、Qwen 等)
- OAuth {
- access_token: String,
- refresh_token: Option,
- expires_at: Option>,
- },
- /// API Key 凭证(用于 OpenAI、Claude 等)
- ApiKey {
- key: String,
- base_url: Option,
- },
-}
-
-/// 凭证状态
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-#[serde(tag = "status", rename_all = "snake_case")]
-pub enum CredentialStatus {
- /// 活跃可用
- Active,
- /// 冷却中(配额超限等)
- Cooldown {
- /// 冷却结束时间
- until: DateTime,
- },
- /// 不健康(连续失败)
- Unhealthy {
- /// 不健康原因
- reason: String,
- },
- /// 已禁用
- Disabled,
-}
-
-/// 凭证统计信息
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
-pub struct CredentialStats {
- /// 总请求数
- pub total_requests: u64,
- /// 成功请求数
- pub successful_requests: u64,
- /// 连续失败次数
- pub consecutive_failures: u32,
- /// 平均延迟(毫秒)
- pub avg_latency_ms: f64,
-}
-
-impl CredentialStats {
- /// 记录成功请求
- pub fn record_success(&mut self, latency_ms: u64) {
- self.total_requests += 1;
- self.successful_requests += 1;
- self.consecutive_failures = 0;
-
- // 更新平均延迟(移动平均)
- let n = self.successful_requests as f64;
- self.avg_latency_ms = self.avg_latency_ms * (n - 1.0) / n + latency_ms as f64 / n;
- }
-
- /// 记录失败请求
- pub fn record_failure(&mut self) {
- self.total_requests += 1;
- self.consecutive_failures += 1;
- }
-
- /// 获取成功率
- pub fn success_rate(&self) -> f64 {
- if self.total_requests == 0 {
- 1.0
- } else {
- self.successful_requests as f64 / self.total_requests as f64
- }
- }
-}
-
-#[cfg(test)]
-mod type_tests {
- use super::*;
-
- #[test]
- fn test_credential_new() {
- let cred = Credential::new(
- "test-id".to_string(),
- ProviderType::Kiro,
- CredentialData::ApiKey {
- key: "test-key".to_string(),
- base_url: None,
- },
- );
-
- assert_eq!(cred.id, "test-id");
- assert_eq!(cred.provider, ProviderType::Kiro);
- assert!(cred.is_available());
- assert!(cred.last_used.is_none());
- }
-
- #[test]
- fn test_credential_is_available() {
- let mut cred = Credential::new(
- "test".to_string(),
- ProviderType::Gemini,
- CredentialData::ApiKey {
- key: "key".to_string(),
- base_url: None,
- },
- );
-
- assert!(cred.is_available());
-
- cred.status = CredentialStatus::Cooldown {
- until: Utc::now() + chrono::Duration::hours(1),
- };
- assert!(!cred.is_available());
-
- cred.status = CredentialStatus::Unhealthy {
- reason: "test".to_string(),
- };
- assert!(!cred.is_available());
-
- cred.status = CredentialStatus::Disabled;
- assert!(!cred.is_available());
- }
-
- #[test]
- fn test_credential_stats_success() {
- let mut stats = CredentialStats::default();
-
- stats.record_success(100);
- assert_eq!(stats.total_requests, 1);
- assert_eq!(stats.successful_requests, 1);
- assert_eq!(stats.consecutive_failures, 0);
- assert!((stats.avg_latency_ms - 100.0).abs() < 0.001);
-
- stats.record_success(200);
- assert_eq!(stats.total_requests, 2);
- assert!((stats.avg_latency_ms - 150.0).abs() < 0.001);
- }
-
- #[test]
- fn test_credential_stats_failure() {
- let mut stats = CredentialStats::default();
-
- stats.record_failure();
- assert_eq!(stats.total_requests, 1);
- assert_eq!(stats.successful_requests, 0);
- assert_eq!(stats.consecutive_failures, 1);
-
- stats.record_failure();
- assert_eq!(stats.consecutive_failures, 2);
-
- stats.record_success(100);
- assert_eq!(stats.consecutive_failures, 0);
- }
-
- #[test]
- fn test_credential_stats_success_rate() {
- let mut stats = CredentialStats::default();
-
- // 空统计应返回 1.0
- assert!((stats.success_rate() - 1.0).abs() < 0.001);
-
- stats.record_success(100);
- assert!((stats.success_rate() - 1.0).abs() < 0.001);
-
- stats.record_failure();
- assert!((stats.success_rate() - 0.5).abs() < 0.001);
- }
-}
diff --git a/src-tauri/crates/core/src/database/README.md b/src-tauri/crates/core/src/database/README.md
index 9a502d818..2b1ca2e02 100644
--- a/src-tauri/crates/core/src/database/README.md
+++ b/src-tauri/crates/core/src/database/README.md
@@ -18,8 +18,8 @@
### 核心表
- `api_key_providers` - API Key Provider 配置
-- `api_keys` - API Key 条目(已迁移到 provider_pool_credentials)
-- `provider_pool_credentials` - 凭证池(统一管理所有凭证)
+- `api_key_providers` - API Key Provider 配置主表
+- `provider_pool_credentials` - 旧凭证池表;启动期清空,仅保留历史迁移边界
- `providers` - Provider 配置
- `settings` - 应用设置
@@ -43,7 +43,7 @@
| `dao/api_key_provider.rs` | API Key Provider DAO |
| `dao/mcp.rs` | MCP 服务器 DAO |
| `dao/prompts.rs` | 提示词 DAO |
-| `dao/provider_pool.rs` | 凭证池 DAO |
+| `dao/provider_pool.rs` | 旧凭证池 DAO;不得作为运行时选择入口 |
| `dao/providers.rs` | Provider DAO |
| `dao/skills.rs` | 技能 DAO |
@@ -51,7 +51,7 @@
### API Keys 迁移
-`migrate_api_keys_to_pool()` 函数将 `api_keys` 表中的数据迁移到 `provider_pool_credentials` 表:
+旧 `migrate_api_keys_to_pool()` 只属于历史迁移链。当前启动期会清理 `provider_pool_credentials`,运行时不再读取该表选择凭证。
- 根据 provider_type 自动转换为对应的 CredentialData 类型
- 保留使用统计和错误计数
diff --git a/src-tauri/crates/core/src/database/dao/mod.rs b/src-tauri/crates/core/src/database/dao/mod.rs
index 9fbdf3981..3fd879e48 100644
--- a/src-tauri/crates/core/src/database/dao/mod.rs
+++ b/src-tauri/crates/core/src/database/dao/mod.rs
@@ -16,7 +16,6 @@ pub mod mcp;
pub mod orchestrator;
pub mod persona_dao;
pub mod prompts;
-pub mod provider_pool;
pub mod providers;
pub mod publish_config_dao;
pub mod skills;
diff --git a/src-tauri/crates/core/src/database/dao/provider_pool.rs b/src-tauri/crates/core/src/database/dao/provider_pool.rs
deleted file mode 100644
index f0a820e58..000000000
--- a/src-tauri/crates/core/src/database/dao/provider_pool.rs
+++ /dev/null
@@ -1,504 +0,0 @@
-//! Provider Pool 数据访问对象
-//!
-//! 提供凭证池的 CRUD 操作。
-
-use crate::models::provider_pool_model::{
- CachedTokenInfo, CredentialData, CredentialSource, PoolProviderType, ProviderCredential,
- ProviderPools,
-};
-use chrono::{DateTime, TimeZone, Utc};
-use rusqlite::{params, Connection};
-
-pub struct ProviderPoolDao;
-
-impl ProviderPoolDao {
- /// 获取所有凭证
- pub fn get_all(conn: &Connection) -> Result, rusqlite::Error> {
- let mut stmt = conn.prepare(
- "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled,
- check_health, check_model_name, not_supported_models, supported_models, usage_count, error_count,
- last_used, last_error_time, last_error_message, last_health_check_time,
- last_health_check_model, created_at, updated_at, source, proxy_url
- FROM provider_pool_credentials
- ORDER BY provider_type, created_at ASC",
- )?;
-
- let rows = stmt.query_map([], Self::row_to_credential)?;
-
- let mut credentials = Vec::new();
- for cred in rows.flatten() {
- credentials.push(cred);
- }
- Ok(credentials)
- }
-
- /// 获取指定类型的凭证
- pub fn get_by_type(
- conn: &Connection,
- provider_type: &PoolProviderType,
- ) -> Result, rusqlite::Error> {
- let mut stmt = conn.prepare(
- "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled,
- check_health, check_model_name, not_supported_models, supported_models, usage_count, error_count,
- last_used, last_error_time, last_error_message, last_health_check_time,
- last_health_check_model, created_at, updated_at, source, proxy_url
- FROM provider_pool_credentials
- WHERE provider_type = ?1
- ORDER BY created_at ASC",
- )?;
-
- let rows = stmt.query_map([provider_type.to_string()], |row| {
- Self::row_to_credential(row)
- })?;
-
- let mut credentials = Vec::new();
- for cred in rows.flatten() {
- credentials.push(cred);
- }
- Ok(credentials)
- }
-
- /// 获取指定 UUID 的凭证
- pub fn get_by_uuid(
- conn: &Connection,
- uuid: &str,
- ) -> Result