feat: 优化更新检查逻辑 + 修复按量余额显示(v0.6.5)
This commit is contained in:
1 parent
f60305fbeb
commit
2464500ef5
12 files changed
+133
-62
No files matched your search
@@ -11,6 +11,13 @@
|
|||||||
- GitHub Actions Release workflow 已触发(三平台构建 → Release draft 自动上传 msi/dmg/AppImage/deb/rpm)
|
- GitHub Actions Release workflow 已触发(三平台构建 → Release draft 自动上传 msi/dmg/AppImage/deb/rpm)
|
||||||
- 本地 0.6.4 安装包:`src-tauri/target/release/bundle/{nsis,msi}/`
|
- 本地 0.6.4 安装包:`src-tauri/target/release/bundle/{nsis,msi}/`
|
||||||
|
|
||||||
|
**已提交并发布 v0.6.5**:
|
||||||
|
- `feat: 优化更新检查逻辑 + 修复按量余额显示(v0.6.5)`
|
||||||
|
- 检查更新优先私有库(git.codingplan.site),8s 超时自动回退 GitHub
|
||||||
|
- 设置-关于:合并移植声明为一句、新增 cc-switch 链接(openUrl + window.open 兜底)、移除仪表盘/会话页脚重复声明
|
||||||
|
- 修复按量余额不显示:UsageFooter isPlan 判定增加配额字段(total/used)条件——余额条目(DeepSeek/StepFun/SiliconFlow/Novita/Kimi 开放平台)现在正确显示 `💰 ¥x.xx`,套餐类(有 total/used)不受影响。实测 DeepSeek 官方接口返回 200 + balance_infos
|
||||||
|
- 两仓库 main + Dev_20260801 + tag v0.6.5 已同步
|
||||||
|
|
||||||
**历史提交**:`12dcb17` — docs: 账单查询适配调研 + OpenCode Zen 预设补推荐链接
|
**历史提交**:`12dcb17` — docs: 账单查询适配调研 + OpenCode Zen 预设补推荐链接
|
||||||
|
|
||||||
**工作区未提交改动**——用户硬约束:**实现完成不提交,等用户逐批测试确认后才提交**。
|
**工作区未提交改动**——用户硬约束:**实现完成不提交,等用户逐批测试确认后才提交**。
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kimiswitch",
|
"name": "kimiswitch",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.6.4",
|
"version": "0.6.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1958,7 +1958,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.6.4"
|
version = "0.6.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.6.4"
|
version = "0.6.5"
|
||||||
description = "Kimi Switch - model config manager"
|
description = "Kimi Switch - model config manager"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
+41
-19
@@ -801,25 +801,55 @@ fn version_lt(current: &str, latest: &str) -> bool {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn check_for_update() -> Result<UpdateInfo, String> {
|
pub async fn check_for_update() -> Result<UpdateInfo, String> {
|
||||||
let current = env!("CARGO_PKG_VERSION").to_string();
|
let current = env!("CARGO_PKG_VERSION").to_string();
|
||||||
// GitHub Releases API for billowliu2/KimiSwitch. Historical versions
|
// Prefer the private Gitea repo (primary release channel, all versions
|
||||||
// (v0.5.x and earlier) live on git.codingplan.site and are not exposed
|
// since v0.1.0). Fall back to GitHub Releases when it is unreachable
|
||||||
// here — only v0.6.0+ is published via GitHub.
|
// (e.g. off the LAN/VPN) — v0.6.0+ is mirrored there.
|
||||||
let url = "https://api.github.com/repos/billowliu2/KimiSwitch/releases?per_page=1";
|
let sources = [
|
||||||
|
"https://git.codingplan.site/api/v1/repos/admin/KimiCodeSwitch/releases?limit=1",
|
||||||
|
"https://api.github.com/repos/billowliu2/KimiSwitch/releases?per_page=1",
|
||||||
|
];
|
||||||
|
|
||||||
// GitHub returns 403 without a User-Agent header. Bundle the app version
|
// Both hosts require a User-Agent header (GitHub returns 403 without one).
|
||||||
// so the source is identifiable in any rate-limit / abuse reports.
|
// Bundle the app version so the source is identifiable in any rate-limit
|
||||||
|
// / abuse reports. A short timeout keeps the fallback responsive when the
|
||||||
|
// private server is unreachable.
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.user_agent(concat!("KimiSwitch/", env!("CARGO_PKG_VERSION")))
|
.user_agent(concat!("KimiSwitch/", env!("CARGO_PKG_VERSION")))
|
||||||
|
.timeout(std::time::Duration::from_secs(8))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| format!("client build failed: {e}"))?;
|
.map_err(|e| format!("client build failed: {e}"))?;
|
||||||
|
|
||||||
let resp = client
|
let mut errors: Vec<String> = Vec::new();
|
||||||
|
for url in sources {
|
||||||
|
match fetch_latest_release(&client, url).await {
|
||||||
|
Ok((latest, release_url, download_url)) => {
|
||||||
|
let update_available = version_lt(¤t, &latest);
|
||||||
|
return Ok(UpdateInfo {
|
||||||
|
current,
|
||||||
|
latest,
|
||||||
|
update_available,
|
||||||
|
release_url,
|
||||||
|
download_url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => errors.push(format!("{url}: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("all update sources failed: {}", errors.join("; ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET a releases API (Gitea or GitHub, same array shape) and extract the
|
||||||
|
/// latest tag, release page URL and the installer asset for the current OS.
|
||||||
|
async fn fetch_latest_release(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<(String, String, Option<String>), String> {
|
||||||
|
let releases: serde_json::Value = client
|
||||||
.get(url)
|
.get(url)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("request failed: {e}"))?;
|
.map_err(|e| format!("request failed: {e}"))?
|
||||||
|
|
||||||
let releases: serde_json::Value = resp
|
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("parse failed: {e}"))?;
|
.map_err(|e| format!("parse failed: {e}"))?;
|
||||||
@@ -846,15 +876,7 @@ pub async fn check_for_update() -> Result<UpdateInfo, String> {
|
|||||||
.and_then(|a| a.as_array())
|
.and_then(|a| a.as_array())
|
||||||
.and_then(|a| pick_asset_for_current_os(a));
|
.and_then(|a| pick_asset_for_current_os(a));
|
||||||
|
|
||||||
let update_available = version_lt(¤t, &latest);
|
Ok((latest, release_url, download_url))
|
||||||
|
|
||||||
Ok(UpdateInfo {
|
|
||||||
current,
|
|
||||||
latest,
|
|
||||||
update_available,
|
|
||||||
release_url,
|
|
||||||
download_url,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "Kimi Switch",
|
"productName": "Kimi Switch",
|
||||||
"version": "0.6.4",
|
"version": "0.6.5",
|
||||||
"identifier": "com.kimiswitch.app",
|
"identifier": "com.kimiswitch.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -110,6 +110,15 @@ export function SettingsModal({
|
|||||||
invoke("open_installer", { path: downloadedPath }).catch(() => {});
|
invoke("open_installer", { path: downloadedPath }).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Open an external link in the system browser. Falls back to a new tab
|
||||||
|
// when the opener plugin call fails, so a broken click never stays silent.
|
||||||
|
const openExternal = (url: string) => {
|
||||||
|
openUrl(url).catch(() => {
|
||||||
|
const w = window.open(url, "_blank");
|
||||||
|
if (!w) console.error(`failed to open ${url}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
const themeOptions: { value: Theme; icon: ReactNode; label: string }[] = [
|
const themeOptions: { value: Theme; icon: ReactNode; label: string }[] = [
|
||||||
@@ -253,26 +262,66 @@ export function SettingsModal({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onCheckUpdate}
|
onClick={onCheckUpdate}
|
||||||
disabled={checking}
|
disabled={checking}
|
||||||
className="px-3 py-1.5 text-sm rounded border border-border hover:bg-hover-2 disabled:opacity-50 transition-colors"
|
className="px-3 py-1.5 text-sm rounded border border-border hover:bg-hover-2 disabled:opacity-50 transition-colors flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={checking ? "animate-spin" : ""}
|
||||||
|
>
|
||||||
|
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
|
||||||
|
<polyline points="21 3 21 9 15 9" />
|
||||||
|
</svg>
|
||||||
{checking ? t("checking") : t("checkUpdate")}
|
{checking ? t("checking") : t("checkUpdate")}
|
||||||
</button>
|
</button>
|
||||||
{updateInfo?.updateAvailable && downloadProgress === null && !downloadedPath && (
|
{updateInfo?.updateAvailable && downloadProgress === null && !downloadedPath && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
className="px-3 py-1.5 text-sm rounded bg-green-600 text-white hover:bg-green-500 transition-colors"
|
className="px-3 py-1.5 text-sm rounded bg-green-600 text-white hover:bg-green-500 transition-colors flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{t("downloadUpdate")} ↓
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
|
<polyline points="7 10 12 15 17 10" />
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3" />
|
||||||
|
</svg>
|
||||||
|
{t("downloadUpdate")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{downloadedPath && (
|
{downloadedPath && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleInstall}
|
onClick={handleInstall}
|
||||||
className="px-3 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-500 transition-colors"
|
className="px-3 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-500 transition-colors flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{t("install")} ⏎
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polygon points="6 3 20 12 6 21 6 3" />
|
||||||
|
</svg>
|
||||||
|
{t("install")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -323,18 +372,25 @@ export function SettingsModal({
|
|||||||
<div className="font-medium text-content-primary">Kimi Switch</div>
|
<div className="font-medium text-content-primary">Kimi Switch</div>
|
||||||
<div>{t("appDescription")}</div>
|
<div>{t("appDescription")}</div>
|
||||||
<div className="text-xs text-content-muted">v{appVersion} · MIT License</div>
|
<div className="text-xs text-content-muted">v{appVersion} · MIT License</div>
|
||||||
<div className="pt-2">
|
<div className="pt-2 space-y-1">
|
||||||
|
<div>
|
||||||
|
{t("attributionPorted")}{" "}
|
||||||
|
<span className="font-medium text-content-primary">kimicode-dashboard</span>
|
||||||
|
<span className="text-content-muted"> {t("attributionSuffix")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-1">
|
||||||
<span className="text-content-muted">{t("referenceProject")}: </span>
|
<span className="text-content-muted">{t("referenceProject")}: </span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openUrl("https://github.com/JochenYang/kimicode-dashboard")
|
openExternal("https://github.com/farion1231/cc-switch")
|
||||||
}
|
}
|
||||||
className="text-blue-500 hover:text-blue-400 underline bg-transparent p-0 border-0 cursor-pointer"
|
className="text-blue-500 hover:text-blue-400 underline bg-transparent p-0 border-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
kimicode-dashboard
|
cc-switch
|
||||||
</button>
|
</button>
|
||||||
<span className="text-content-muted"> (MIT, © JochenYang)</span>
|
<span className="text-content-muted"> (MIT, © Jason Young)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -245,7 +245,13 @@ export function UsageFooter({
|
|||||||
const compactSummary = (): { icon: string; text: string; color: string } | null => {
|
const compactSummary = (): { icon: string; text: string; color: string } | null => {
|
||||||
const d = data[0];
|
const d = data[0];
|
||||||
if (!d) return null;
|
if (!d) return null;
|
||||||
const isPlan = !!d.planName && d.planName !== "NewAPI";
|
// A "plan" is a quota tier (used/total, e.g. 5h / weekly limit). Balance
|
||||||
|
// entries carry only remaining + unit — their plan_name is a currency /
|
||||||
|
// brand label and must render as an amount, not a percentage.
|
||||||
|
const isPlan =
|
||||||
|
!!d.planName &&
|
||||||
|
d.planName !== "NewAPI" &&
|
||||||
|
(d.total != null || d.used != null);
|
||||||
if (d.isValid === false) {
|
if (d.isValid === false) {
|
||||||
return { icon: "⚡", text: t("usageInvalidKey"), color: "text-red-500 dark:text-red-400" };
|
return { icon: "⚡", text: t("usageInvalidKey"), color: "text-red-500 dark:text-red-400" };
|
||||||
}
|
}
|
||||||
@@ -360,7 +366,12 @@ export function UsageFooter({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{data.map((d, i) => {
|
{data.map((d, i) => {
|
||||||
const isPlan = !!d.planName;
|
// Same plan-vs-balance rule as compactSummary: balance rows have no
|
||||||
|
// quota fields (total/used), so they render as an amount.
|
||||||
|
const isPlan =
|
||||||
|
!!d.planName &&
|
||||||
|
d.planName !== "NewAPI" &&
|
||||||
|
(d.total != null || d.used != null);
|
||||||
const pct = isPlan ? percentOf(d) : null;
|
const pct = isPlan ? percentOf(d) : null;
|
||||||
const color =
|
const color =
|
||||||
pct == null
|
pct == null
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import { useDashboard, type DashboardRange } from "../../hooks/useDashboard";
|
import { useDashboard, type DashboardRange } from "../../hooks/useDashboard";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { fmtInt, fmtPct, fmtTime, fmtTokens, fmtUsd } from "../../lib/dashboard-format";
|
import { fmtInt, fmtPct, fmtTime, fmtTokens, fmtUsd } from "../../lib/dashboard-format";
|
||||||
@@ -420,23 +419,12 @@ export function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Footer — meta + attribution at the end of the content flow */}
|
{/* Footer — meta at the end of the content flow */}
|
||||||
<div className="text-xs text-content-muted pb-2">
|
<div className="text-xs text-content-muted pb-2">
|
||||||
<div>
|
<div>
|
||||||
{t("footerScanned", { n: data.meta.filesScanned, m: fmtInt(data.meta.recordCount) })} ·{" "}
|
{t("footerScanned", { n: data.meta.filesScanned, m: fmtInt(data.meta.recordCount) })} ·{" "}
|
||||||
{data.meta.home}
|
{data.meta.home}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5">
|
|
||||||
{t("dashboardAttribution")}{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openUrl("https://github.com/JochenYang/kimicode-dashboard")}
|
|
||||||
className="text-blue-500 hover:text-blue-400 underline bg-transparent p-0 border-0 cursor-pointer"
|
|
||||||
>
|
|
||||||
kimicode-dashboard
|
|
||||||
</button>{" "}
|
|
||||||
{t("attributionSuffix")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import { useSessions } from "../../hooks/useSessions";
|
import { useSessions } from "../../hooks/useSessions";
|
||||||
import { fmtInt, fmtTime } from "../../lib/dashboard-format";
|
import { fmtInt, fmtTime } from "../../lib/dashboard-format";
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
@@ -547,16 +546,6 @@ export function SessionsPage() {
|
|||||||
{/* Attribution */}
|
{/* Attribution */}
|
||||||
<div className="text-xs text-content-muted pb-2">
|
<div className="text-xs text-content-muted pb-2">
|
||||||
{data?.home && <>{data.home}</>}
|
{data?.home && <>{data.home}</>}
|
||||||
<br />
|
|
||||||
{t("sessionsAttribution")}{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openUrl("https://github.com/JochenYang/kimicode-dashboard")}
|
|
||||||
className="text-blue-500 hover:text-blue-400 underline bg-transparent p-0 border-0 cursor-pointer"
|
|
||||||
>
|
|
||||||
kimicode-dashboard
|
|
||||||
</button>{" "}
|
|
||||||
{t("attributionSuffix")}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Confirm dialog */}
|
{/* Confirm dialog */}
|
||||||
|
|||||||
+1
-2
@@ -318,8 +318,7 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
|
|
||||||
// Dashboard — footer / attribution
|
// Dashboard — footer / attribution
|
||||||
footerScanned: "Scanned {n} files · {m} records",
|
footerScanned: "Scanned {n} files · {m} records",
|
||||||
dashboardAttribution: "Dashboard ported from",
|
attributionPorted: "Dashboard & session manager ported from",
|
||||||
sessionsAttribution: "Session manager ported from",
|
|
||||||
attributionSuffix: "(MIT, © JochenYang)",
|
attributionSuffix: "(MIT, © JochenYang)",
|
||||||
|
|
||||||
// Sessions — preview
|
// Sessions — preview
|
||||||
|
|||||||
+1
-2
@@ -313,8 +313,7 @@ export const zhTranslations = {
|
|||||||
|
|
||||||
// Dashboard — footer / attribution
|
// Dashboard — footer / attribution
|
||||||
footerScanned: "扫描 {n} 个文件 · {m} 条记录",
|
footerScanned: "扫描 {n} 个文件 · {m} 条记录",
|
||||||
dashboardAttribution: "仪表盘功能基于",
|
attributionPorted: "仪表盘与会话管理功能基于",
|
||||||
sessionsAttribution: "会话管理功能基于",
|
|
||||||
attributionSuffix: "(MIT,© JochenYang)移植",
|
attributionSuffix: "(MIT,© JochenYang)移植",
|
||||||
|
|
||||||
// Sessions — preview
|
// Sessions — preview
|
||||||
|
|||||||
Reference in new issue
Block a user