feat(models-dev): 高级设置新增 models.dev 在线同步 — 上下文/能力/单价即时生效 + 一键恢复内置;修复 public 快照构建漂移;确认适配 kimi-code 2.1.0 — bump to v0.7.21
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
Release / Attach macOS install script (push) Canceled after 0s
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
Release / Attach macOS install script (push) Canceled after 0s
This commit is contained in:
1 parent
26e783c2ba
commit
11286419b1
17 files changed
+282395
-267694
No files matched your search
@@ -24,6 +24,15 @@ import {
|
||||
type ValidationErrorKey,
|
||||
} from "../lib/subagent-settings";
|
||||
import { Card, Toggle } from "./ui/controls";
|
||||
import { applyModelsDevSnapshot, reloadModelsDev } from "../lib/models-dev";
|
||||
|
||||
/** Mirror of models_dev::ModelsDevStatus (camelCase over IPC). */
|
||||
interface ModelsDevStatus {
|
||||
source: "synced" | "builtin";
|
||||
lastUpdated: string;
|
||||
modelCount: number;
|
||||
providerCount: number;
|
||||
}
|
||||
|
||||
interface SubagentSettingsPageProps {
|
||||
/** config.raw_other — hosts the `[experimental]` and `[secondary_model]` sections. */
|
||||
@@ -155,13 +164,65 @@ export function SubagentSettingsPage({
|
||||
const [addSelection, setAddSelection] = useState("");
|
||||
/** WebUI-open button in flight; disables both buttons while non-null. */
|
||||
const [webuiBusy, setWebuiBusy] = useState<"embedded" | "browser" | null>(null);
|
||||
/** models.dev snapshot provenance (synced copy vs bundled asset). */
|
||||
const [modelsDevStatus, setModelsDevStatus] = useState<ModelsDevStatus | null>(null);
|
||||
/** Sync/restore button in flight; blocks the card's buttons. */
|
||||
const [modelsDevBusy, setModelsDevBusy] = useState(false);
|
||||
/** Last sync error (inline) and success flag (auto-cleared on next action). */
|
||||
const [syncError, setSyncError] = useState<string | null>(null);
|
||||
const [syncOk, setSyncOk] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
invoke<ExperimentalEnvStatus>("get_experimental_env_status")
|
||||
.then(setEnv)
|
||||
.catch(() => setEnv({}));
|
||||
invoke<ModelsDevStatus>("get_models_dev_status")
|
||||
.then(setModelsDevStatus)
|
||||
.catch(() => setModelsDevStatus(null));
|
||||
}, []);
|
||||
|
||||
const refreshModelsDevStatus = () => {
|
||||
invoke<ModelsDevStatus>("get_models_dev_status")
|
||||
.then(setModelsDevStatus)
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const handleModelsDevSync = async () => {
|
||||
setModelsDevBusy(true);
|
||||
setSyncError(null);
|
||||
setSyncOk(false);
|
||||
try {
|
||||
const st = await invoke<ModelsDevStatus>("sync_models_dev");
|
||||
setModelsDevStatus(st);
|
||||
// Hot-swap the frontend index; the dashboard price index rebuilds on
|
||||
// its next query (mtime-triggered on the Rust side).
|
||||
const raw = await invoke<string | null>("get_models_dev_snapshot");
|
||||
if (raw) applyModelsDevSnapshot(JSON.parse(raw) as Record<string, unknown>);
|
||||
setSyncOk(true);
|
||||
} catch (err) {
|
||||
setSyncError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setModelsDevBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelsDevRestore = async () => {
|
||||
if (!confirm(t("modelsDataRestoreConfirm"))) return;
|
||||
setModelsDevBusy(true);
|
||||
setSyncError(null);
|
||||
setSyncOk(false);
|
||||
try {
|
||||
const st = await invoke<ModelsDevStatus>("reset_models_dev");
|
||||
setModelsDevStatus(st);
|
||||
await reloadModelsDev();
|
||||
} catch (err) {
|
||||
setSyncError(err instanceof Error ? err.message : String(err));
|
||||
refreshModelsDevStatus();
|
||||
} finally {
|
||||
setModelsDevBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const flags = getExperimentalFlags(rawOther);
|
||||
const pool = getSubagentModelPool(rawOther);
|
||||
const poolKeys = pool ? Object.keys(pool.models) : [];
|
||||
@@ -398,6 +459,66 @@ export function SubagentSettingsPage({
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* models.dev reference data: online sync / restore bundled */}
|
||||
<Card title={t("modelsDataSection")}>
|
||||
<p className="text-xs text-content-muted">{t("modelsDataDesc")}</p>
|
||||
{modelsDevStatus && (
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span
|
||||
className={`shrink-0 rounded border px-1.5 py-0.5 text-xs ${
|
||||
modelsDevStatus.source === "synced"
|
||||
? "border-blue-500/30 text-blue-600 dark:text-blue-400"
|
||||
: "border-border text-content-muted"
|
||||
}`}
|
||||
>
|
||||
{modelsDevStatus.source === "synced"
|
||||
? t("modelsDataSourceSynced")
|
||||
: t("modelsDataSourceBuiltin")}
|
||||
</span>
|
||||
<span className="text-xs text-content-muted">
|
||||
{t("modelsDataStats", {
|
||||
date: modelsDevStatus.lastUpdated,
|
||||
models: modelsDevStatus.modelCount.toLocaleString(),
|
||||
providers: modelsDevStatus.providerCount.toLocaleString(),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{syncOk && (
|
||||
<div className="bg-green-100 dark:bg-green-900/20 border border-green-300 dark:border-green-500/30 rounded-lg px-3 py-2 text-xs text-green-700 dark:text-green-400">
|
||||
{t("modelsDataSyncOk")}
|
||||
</div>
|
||||
)}
|
||||
{syncError && (
|
||||
<div
|
||||
role="alert"
|
||||
className="bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-500/30 rounded-lg px-3 py-2 text-xs text-red-700 dark:text-red-400"
|
||||
>
|
||||
{t("modelsDataSyncFailed")}: {syncError}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={modelsDevBusy}
|
||||
onClick={handleModelsDevSync}
|
||||
className="px-3 py-1.5 text-sm rounded bg-blue-600 text-white hover:bg-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{modelsDevBusy ? t("modelsDataSyncing") : t("modelsDataSyncNow")}
|
||||
</button>
|
||||
{modelsDevStatus?.source === "synced" && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={modelsDevBusy}
|
||||
onClick={handleModelsDevRestore}
|
||||
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t("modelsDataRestore")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Subagent: secondary model picker / pool + inherited settings */}
|
||||
<Card title={t("secondaryModelSection")}>
|
||||
{isPoolView && pool ? (
|
||||
|
||||
@@ -594,6 +594,21 @@ export const enTranslations: Record<TranslationKey, string> = {
|
||||
openWebUIBrowser: "Open in Browser",
|
||||
webuiOpening: "Opening...",
|
||||
|
||||
// models.dev reference data (context / capabilities / pricing)
|
||||
modelsDataSection: "Model Reference Data (models.dev)",
|
||||
modelsDataDesc:
|
||||
"Model context limits, capability flags, and pricing come from an online models.dev snapshot. Syncing applies new models immediately — no app update needed.",
|
||||
modelsDataSourceSynced: "Online sync",
|
||||
modelsDataSourceBuiltin: "Bundled with release",
|
||||
modelsDataStats: "{date} · {models} models / {providers} providers",
|
||||
modelsDataSyncNow: "Sync Now",
|
||||
modelsDataSyncing: "Syncing...",
|
||||
modelsDataSyncOk: "Synced — model parameters and pricing refreshed",
|
||||
modelsDataSyncFailed: "Sync failed",
|
||||
modelsDataRestore: "Restore Built-in Data",
|
||||
modelsDataRestoreConfirm:
|
||||
"Delete the local synced copy and fall back to the snapshot bundled with this release? You can sync again at any time.",
|
||||
|
||||
// Plugin marketplace
|
||||
pluginMarketplace: "Plugin Marketplace",
|
||||
pluginMarketplaceSubtitle:
|
||||
|
||||
@@ -583,6 +583,21 @@ export const zhTranslations = {
|
||||
openWebUIBrowser: "在浏览器打开",
|
||||
webuiOpening: "打开中...",
|
||||
|
||||
// models.dev reference data (context / capabilities / pricing)
|
||||
modelsDataSection: "模型参考数据(models.dev)",
|
||||
modelsDataDesc:
|
||||
"模型上下文长度、能力标记与单价参考来自 models.dev 在线快照。同步后新模型的参数与价格立即生效,无需等待新版本。",
|
||||
modelsDataSourceSynced: "在线同步",
|
||||
modelsDataSourceBuiltin: "随版本内置",
|
||||
modelsDataStats: "{date} · {models} 模型 / {providers} 供应商",
|
||||
modelsDataSyncNow: "立即同步",
|
||||
modelsDataSyncing: "同步中...",
|
||||
modelsDataSyncOk: "已同步,模型参数与单价已刷新",
|
||||
modelsDataSyncFailed: "同步失败",
|
||||
modelsDataRestore: "恢复内置数据",
|
||||
modelsDataRestoreConfirm:
|
||||
"删除本地同步副本并回退到随版本内置的快照数据?之后可随时重新同步。",
|
||||
|
||||
// Plugin marketplace
|
||||
pluginMarketplace: "插件市场",
|
||||
pluginMarketplaceSubtitle:
|
||||
|
||||
+104236
-100240
File diff suppressed because it is too large.
Load diff
+88653
-85246
File diff suppressed because it is too large.
Load diff
+60
-23
@@ -3,13 +3,18 @@
|
||||
* (see scripts/fetch-models-dev.mjs).
|
||||
*
|
||||
* The snapshot (~1.3 MB JSON) is NOT bundled into the main chunk anymore:
|
||||
* parsing a 1.3 MB JSON literal at startup blocks first paint. It is served
|
||||
* as a static asset (`/models-dev.json`) and loaded once in the background.
|
||||
* `getModelRef` stays synchronous and returns undefined until the index is
|
||||
* ready — callers already fall back to defaults — and `modelsDevReady()`
|
||||
* lets the app re-render once the index arrives.
|
||||
* parsing a 1.3 MB JSON literal at startup blocks first paint. Loading order
|
||||
* (see modelsDevReady): the runtime-synced copy under ~/.kimi-switch/
|
||||
* (via models_dev.rs, written by the 高级设置 sync button) when present,
|
||||
* otherwise the bundled static asset (`/models-dev.json`). `getModelRef`
|
||||
* stays synchronous and returns undefined until the index is ready — callers
|
||||
* already fall back to defaults — and `modelsDevReady()` lets the app
|
||||
* re-render once the index arrives. `applyModelsDevSnapshot` hot-swaps the
|
||||
* index after an online sync without a restart.
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface ModelCost {
|
||||
input?: number;
|
||||
output?: number;
|
||||
@@ -49,32 +54,64 @@ function buildIndex(raw: Record<string, unknown>) {
|
||||
return lower;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap in a new snapshot (initial load, online sync, or restore-to-builtin)
|
||||
* and re-notify listeners so context/capability/price columns re-render.
|
||||
* Listeners stay registered across reloads by design.
|
||||
*/
|
||||
function applySnapshot(raw: Record<string, unknown>) {
|
||||
byLowerKey = buildIndex(raw);
|
||||
for (const cb of readyListeners) cb();
|
||||
}
|
||||
|
||||
async function fetchBundled(): Promise<Record<string, unknown>> {
|
||||
const r = await fetch(`${import.meta.env.BASE_URL}models-dev.json`);
|
||||
if (!r.ok) throw new Error(`models-dev.json HTTP ${r.status}`);
|
||||
return (await r.json()) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
|
||||
export function modelsDevReady(): Promise<void> {
|
||||
if (!loadPromise) {
|
||||
loadPromise = fetch(`${import.meta.env.BASE_URL}models-dev.json`)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`models-dev.json HTTP ${r.status}`);
|
||||
return r.json() as Promise<Record<string, unknown>>;
|
||||
})
|
||||
.then((raw) => {
|
||||
byLowerKey = buildIndex(raw);
|
||||
const listeners = readyListeners;
|
||||
readyListeners = [];
|
||||
for (const cb of listeners) cb();
|
||||
})
|
||||
.catch((err) => {
|
||||
// A failed load is permanent for this session: fall back to defaults.
|
||||
byLowerKey = {};
|
||||
loadPromise = null; // allow one retry next time
|
||||
console.warn("models-dev.json load failed:", err);
|
||||
});
|
||||
// Prefer the runtime-synced copy (~/.kimi-switch/models-dev.json, via
|
||||
// models_dev.rs); outside Tauri (or without one) fall back to the bundled
|
||||
// static asset. A failed load is permanent for this session.
|
||||
loadPromise = (async () => {
|
||||
let raw: Record<string, unknown> | null = null;
|
||||
try {
|
||||
const synced = await invoke<string | null>("get_models_dev_snapshot");
|
||||
if (synced) raw = JSON.parse(synced) as Record<string, unknown>;
|
||||
} catch {
|
||||
/* not running inside Tauri — use the bundled asset */
|
||||
}
|
||||
applySnapshot(raw ?? (await fetchBundled()));
|
||||
})().catch((err) => {
|
||||
byLowerKey = {};
|
||||
loadPromise = null; // allow one retry next time
|
||||
console.warn("models-dev.json load failed:", err);
|
||||
});
|
||||
}
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
/** Register a callback invoked once the models.dev index is ready. */
|
||||
/**
|
||||
* Apply a freshly synced snapshot (already fetched by the caller) and notify
|
||||
* listeners so the UI refreshes without a restart.
|
||||
*/
|
||||
export function applyModelsDevSnapshot(raw: Record<string, unknown>): void {
|
||||
applySnapshot(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload from the bundled static asset (after "restore built-in data" —
|
||||
* the synced copy was already deleted on the Rust side).
|
||||
*/
|
||||
export function reloadModelsDev(): Promise<void> {
|
||||
return fetchBundled().then(applySnapshot);
|
||||
}
|
||||
|
||||
/** Register a callback invoked whenever the models.dev index changes. */
|
||||
export function onModelsDevReady(cb: () => void): void {
|
||||
if (byLowerKey) {
|
||||
cb();
|
||||
|
||||
Reference in new issue
Block a user