Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions apps/src/app/models/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
} from "@/components/ui/dialog";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/components/ui/empty";
Expand Down Expand Up @@ -132,13 +131,15 @@ export default function ModelsPage() {
isLoading,
isServiceReady,
refreshRemote,
pruneStaleRemoteModels,
saveModel,
deleteModel,
deleteModels,
exportCodexCache,
routing,
canExportCodexCache,
isRefreshing,
isPruningStaleRemote,
isSaving,
isDeleting,
isExporting,
Expand Down Expand Up @@ -612,13 +613,25 @@ export default function ModelsPage() {
<Button
variant="outline"
onClick={() => void refreshRemote()}
disabled={isRefreshing}
disabled={isRefreshing || isPruningStaleRemote}
>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("远端并入")}
</Button>
<Button
variant="outline"
className="border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive"
onClick={() => void pruneStaleRemoteModels()}
disabled={isRefreshing || isPruningStaleRemote}
title={t("仅删除未本地覆写且不再出现在远端目录中的远端模型,不会删除自定义模型。")}
>
<Trash2
className={`mr-2 h-4 w-4 ${isPruningStaleRemote ? "animate-pulse" : ""}`}
/>
{t("清理远端旧模型")}
</Button>
{canExportCodexCache ? (
<Button
variant="outline"
Expand Down
22 changes: 22 additions & 0 deletions apps/src/hooks/useManagedModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,23 @@ export function useManagedModels() {
},
});

const pruneStaleRemoteMutation = useMutation({
mutationFn: () => accountClient.pruneStaleRemoteManagedModels(),
onSuccess: async (catalog) => {
queryClient.setQueryData(MANAGED_MODEL_QUERY_KEY, catalog);
const cacheSyncError = await syncCatalogToCodexCache(catalog);
await invalidateAll();
if (cacheSyncError) {
toast.error(`${t("远端旧模型已清理,但同步 Codex 模型缓存失败")}: ${cacheSyncError}`);
} else {
toast.success(t("远端旧模型已清理"));
}
},
onError: (error: unknown) => {
toast.error(`${t("清理远端旧模型失败")}: ${getAppErrorMessage(error)}`);
},
});

const saveMutation = useMutation({
mutationFn: (params: ManagedModelPayload) => accountClient.saveManagedModel(params),
onSuccess: async () => {
Expand Down Expand Up @@ -401,6 +418,10 @@ export function useManagedModels() {
if (!ensureServiceReady("读取模型")) return null;
return refreshMutation.mutateAsync(false);
},
pruneStaleRemoteModels: async () => {
if (!ensureServiceReady("清理远端旧模型")) return null;
return pruneStaleRemoteMutation.mutateAsync();
},
saveModel: async (params: ManagedModelPayload) => {
if (!ensureServiceReady("保存模型")) return null;
return saveMutation.mutateAsync(params);
Expand Down Expand Up @@ -439,6 +460,7 @@ export function useManagedModels() {
return true;
},
isRefreshing: refreshMutation.isPending,
isPruningStaleRemote: pruneStaleRemoteMutation.isPending,
isSaving: saveMutation.isPending,
isDeleting: deleteMutation.isPending || batchDeleteMutation.isPending,
isExporting: exportMutation.isPending,
Expand Down
7 changes: 7 additions & 0 deletions apps/src/lib/api/account-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,13 @@ export const accountClient = {
},
deleteManagedModel: (slug: string) =>
invoke("service_model_catalog_delete", withAddr({ slug })),
async pruneStaleRemoteManagedModels(): Promise<ManagedModelCatalog> {
const result = await invoke<unknown>(
"service_model_catalog_prune_stale_remote",
withAddr()
);
return normalizeManagedModelCatalog(result);
},
async readApiKeySecret(keyId: string): Promise<string> {
const result = await invoke<unknown>(
"service_apikey_read_secret",
Expand Down
7 changes: 5 additions & 2 deletions apps/src/lib/api/transport-web-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,11 @@ export function createWebCommandMap(
rpcMethod: "apikey/modelCatalogSave",
mapParams: (params) => asRecord(asRecord(params)?.payload) ?? {},
},
service_model_catalog_delete: { rpcMethod: "apikey/modelCatalogDelete" },
service_model_routing: { rpcMethod: "apikey/modelRouting" },
service_model_catalog_delete: { rpcMethod: "apikey/modelCatalogDelete" },
service_model_catalog_prune_stale_remote: {
rpcMethod: "apikey/modelCatalogPruneStaleRemote",
},
service_model_routing: { rpcMethod: "apikey/modelRouting" },
service_model_source_sync: {
rpcMethod: "apikey/modelSourceSync",
mapParams: (params) => asRecord(asRecord(params)?.payload) ?? {},
Expand Down
7 changes: 7 additions & 0 deletions apps/src/lib/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,13 @@ export const EN_MESSAGES: MessageCatalog = {
"按 slug、显示名称或描述快速定位,并结合来源与覆写状态查看当前目录。":
"Quickly locate entries by slug, display name, or description, and review the current catalog together with source and override status.",
"远端并入": "Merge remote",
"清理远端旧模型": "Prune stale remote models",
"远端旧模型已清理": "Stale remote models pruned",
"清理远端旧模型失败": "Failed to prune stale remote models",
"远端旧模型已清理,但同步 Codex 模型缓存失败":
"Stale remote models were pruned, but syncing the Codex model cache failed",
"仅删除未本地覆写且不再出现在远端目录中的远端模型,不会删除自定义模型。":
"Only deletes remote models that have no local override and no longer appear in the remote catalog. Custom models are not deleted.",
"新增自定义模型": "Add custom model",
"模型总数": "Total models",
"API 可用": "API available",
Expand Down
29 changes: 29 additions & 0 deletions crates/core/src/storage/model_groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ impl Storage {
AND TRIM(slug) <> ''",
params![DEFAULT_MODEL_GROUP_ID, now],
)?;
self.prune_default_model_group_models_not_in_catalog()?;
self.conn.execute(
"INSERT OR IGNORE INTO user_model_groups (
user_id, group_id, status, expires_at, created_at, updated_at
Expand All @@ -125,6 +126,34 @@ impl Storage {
Ok(())
}

pub fn prune_default_model_group_models_not_in_catalog(&self) -> Result<()> {
self.conn.execute(
"DELETE FROM model_group_models
WHERE group_id IN (SELECT id FROM model_groups WHERE is_default = 1)
AND platform_model_slug NOT IN (
SELECT slug
FROM model_catalog_models
WHERE scope = 'default'
AND COALESCE(supported_in_api, 1) = 1
AND TRIM(slug) <> ''
)",
[],
)?;
Ok(())
}
Comment on lines +129 to +143

pub fn delete_model_group_model_references(&self, platform_model_slug: &str) -> Result<()> {
let slug = platform_model_slug.trim();
if slug.is_empty() {
return Ok(());
}
self.conn.execute(
"DELETE FROM model_group_models WHERE platform_model_slug = ?1",
params![slug],
)?;
Ok(())
}

pub fn default_model_group_id(&self) -> Result<Option<String>> {
self.conn
.query_row(
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/storage/model_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ impl Storage {
return Ok(());
}

self.prune_default_model_group_models_not_in_catalog()?;
let now = now_ts();
self.conn.execute(
"INSERT OR IGNORE INTO model_group_models (
Expand Down
20 changes: 20 additions & 0 deletions crates/core/src/storage/model_sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,24 @@ impl Storage {
)?;
Ok(())
}

pub fn delete_model_source_routes_for_platform_model(
&self,
platform_model_slug: &str,
) -> Result<()> {
let slug = normalize_text(platform_model_slug);
if slug.is_empty() {
return Ok(());
}
self.conn.execute(
"DELETE FROM model_source_mappings
WHERE platform_model_slug = ?1 OR upstream_model = ?1",
params![&slug],
)?;
self.conn.execute(
"DELETE FROM model_source_models WHERE upstream_model = ?1",
params![&slug],
)?;
Ok(())
}
Comment on lines +372 to +390
}
Loading