Compare commits

...
1 Commits
Author SHA1 Message Date
KimiSwitch Dev 2464500ef5 feat: 优化更新检查逻辑 + 修复按量余额显示(v0.6.5)
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
2026-08-01 10:51:54 +08:00
12 changed files with 133 additions and 62 deletions

No files matched your search

+7
View File
@@ -11,6 +11,13 @@
- GitHub Actions Release workflow 已触发(三平台构建 → Release draft 自动上传 msi/dmg/AppImage/deb/rpm)
- 本地 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 预设补推荐链接
**工作区未提交改动**——用户硬约束:**实现完成不提交,等用户逐批测试确认后才提交**。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "kimiswitch",
"private": true,
"version": "0.6.4",
"version": "0.6.5",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -1958,7 +1958,7 @@ dependencies = [
[[package]]
name = "kimiswitch"
version = "0.6.4"
version = "0.6.5"
dependencies = [
"anyhow",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "kimiswitch"
version = "0.6.4"
version = "0.6.5"
description = "Kimi Switch - model config manager"
authors = ["you"]
edition = "2021"
+41 -19
View File
@@ -801,25 +801,55 @@ fn version_lt(current: &str, latest: &str) -> bool {
#[tauri::command]
pub async fn check_for_update() -> Result<UpdateInfo, String> {
let current = env!("CARGO_PKG_VERSION").to_string();
// GitHub Releases API for billowliu2/KimiSwitch. Historical versions
// (v0.5.x and earlier) live on git.codingplan.site and are not exposed
// here — only v0.6.0+ is published via GitHub.
let url = "https://api.github.com/repos/billowliu2/KimiSwitch/releases?per_page=1";
// Prefer the private Gitea repo (primary release channel, all versions
// since v0.1.0). Fall back to GitHub Releases when it is unreachable
// (e.g. off the LAN/VPN) — v0.6.0+ is mirrored there.
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
// so the source is identifiable in any rate-limit / abuse reports.
// Both hosts require a User-Agent header (GitHub returns 403 without one).
// 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()
.user_agent(concat!("KimiSwitch/", env!("CARGO_PKG_VERSION")))
.timeout(std::time::Duration::from_secs(8))
.build()
.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(&current, &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)
.send()
.await
.map_err(|e| format!("request failed: {e}"))?;
let releases: serde_json::Value = resp
.map_err(|e| format!("request failed: {e}"))?
.json()
.await
.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| pick_asset_for_current_os(a));
let update_available = version_lt(&current, &latest);
Ok(UpdateInfo {
current,
latest,
update_available,
release_url,
download_url,
})
Ok((latest, release_url, download_url))
}
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -1,6 +1,6 @@
{
"productName": "Kimi Switch",
"version": "0.6.4",
"version": "0.6.5",
"identifier": "com.kimiswitch.app",
"build": {
"beforeDevCommand": "npm run dev",
+65 -9
View File
@@ -110,6 +110,15 @@ export function SettingsModal({
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;
const themeOptions: { value: Theme; icon: ReactNode; label: string }[] = [
@@ -253,26 +262,66 @@ export function SettingsModal({
type="button"
onClick={onCheckUpdate}
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")}
</button>
{updateInfo?.updateAvailable && downloadProgress === null && !downloadedPath && (
<button
type="button"
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>
)}
{downloadedPath && (
<button
type="button"
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>
)}
</div>
@@ -323,18 +372,25 @@ export function SettingsModal({
<div className="font-medium text-content-primary">Kimi Switch</div>
<div>{t("appDescription")}</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>
<button
type="button"
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"
>
kimicode-dashboard
cc-switch
</button>
<span className="text-content-muted"> (MIT, © JochenYang)</span>
<span className="text-content-muted"> (MIT, © Jason Young)</span>
</div>
</div>
</div>
</section>
+13 -2
View File
@@ -245,7 +245,13 @@ export function UsageFooter({
const compactSummary = (): { icon: string; text: string; color: string } | null => {
const d = data[0];
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) {
return { icon: "⚡", text: t("usageInvalidKey"), color: "text-red-500 dark:text-red-400" };
}
@@ -360,7 +366,12 @@ export function UsageFooter({
}`}
>
{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 color =
pct == null
+1 -13
View File
@@ -1,5 +1,4 @@
import { useState, type ReactNode } from "react";
import { openUrl } from "@tauri-apps/plugin-opener";
import { useDashboard, type DashboardRange } from "../../hooks/useDashboard";
import { useTranslation } from "../../i18n";
import { fmtInt, fmtPct, fmtTime, fmtTokens, fmtUsd } from "../../lib/dashboard-format";
@@ -420,23 +419,12 @@ export function DashboardPage() {
)}
</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>
{t("footerScanned", { n: data.meta.filesScanned, m: fmtInt(data.meta.recordCount) })} ·{" "}
{data.meta.home}
</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>
);
-11
View File
@@ -1,5 +1,4 @@
import { useMemo, useState } from "react";
import { openUrl } from "@tauri-apps/plugin-opener";
import { useSessions } from "../../hooks/useSessions";
import { fmtInt, fmtTime } from "../../lib/dashboard-format";
import { useTranslation } from "../../i18n";
@@ -547,16 +546,6 @@ export function SessionsPage() {
{/* Attribution */}
<div className="text-xs text-content-muted pb-2">
{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>
{/* Confirm dialog */}
+1 -2
View File
@@ -318,8 +318,7 @@ export const enTranslations: Record<TranslationKey, string> = {
// Dashboard — footer / attribution
footerScanned: "Scanned {n} files · {m} records",
dashboardAttribution: "Dashboard ported from",
sessionsAttribution: "Session manager ported from",
attributionPorted: "Dashboard & session manager ported from",
attributionSuffix: "(MIT, © JochenYang)",
// Sessions — preview
+1 -2
View File
@@ -313,8 +313,7 @@ export const zhTranslations = {
// Dashboard — footer / attribution
footerScanned: "扫描 {n} 个文件 · {m} 条记录",
dashboardAttribution: "仪表盘功能基于",
sessionsAttribution: "会话管理功能基于",
attributionPorted: "仪表盘与会话管理功能基于",
attributionSuffix: "(MIT,© JochenYang)移植",
// Sessions — preview