feat(qoder): fetch latest model + nút import model trên dashboard

Merge PR #1642 (decolua/9router) — chỉ lấy code + i18n, bỏ test/docs/package.json.

- qoder.js: bỏ guard QODER_MODEL_MAP cứng, resolve model_config qua dynamic API (hỗ trợ qmodel_latest không cần sửa code)
- dashboard page: thêm nút "Fetch Qoder Models" tự import model list vào aliases
- i18n zh-CN: thêm key cho nút fetch

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhangweihong
2026-06-06 10:22:03 +07:00
committed by decolua
parent 44d8de288d
commit 12c97ad46f
3 changed files with 83 additions and 4 deletions

View File

@@ -8,6 +8,7 @@ import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthW
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { translate } from "@/i18n/runtime";
import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher";
import ModelRow from "./ModelRow";
import PassthroughModelsSection from "./PassthroughModelsSection";
@@ -62,6 +63,7 @@ export default function ProviderDetailPage() {
const [oneByOneResults, setOneByOneResults] = useState({});
const [oneByOneSummary, setOneByOneSummary] = useState(null);
const stopOneByOneRef = useRef(false);
const [importingQoderModels, setImportingQoderModels] = useState(false);
const { copied, copy } = useCopyToClipboard();
const AG_RISK_STORAGE_KEY = "ag_risk_confirmed";
@@ -409,6 +411,66 @@ export default function ProviderDetailPage() {
}
};
// Fetch Qoder model list and automatically add to available models
const handleImportQoderModels = async () => {
if (importingQoderModels) return;
const activeConnection = connections.find((conn) => conn.isActive !== false);
if (!activeConnection) {
alert(translate("Please add an active Qoder connection first"));
return;
}
setImportingQoderModels(true);
try {
const res = await fetch(`/api/providers/${activeConnection.id}/models`);
const data = await res.json();
if (!res.ok) {
alert(data.error || translate("Failed to fetch models"));
return;
}
const models = data.models || [];
if (models.length === 0) {
alert(translate("No models returned"));
return;
}
let importedCount = 0;
for (const model of models) {
const modelId = model.id || model.name;
if (!modelId) continue;
// Qoder model ID format may be "qoder/auto" or "auto", need to remove prefix
const cleanModelId = modelId.replace(/^qoder\//, "");
const fullModel = `${providerStorageAlias}/${cleanModelId}`;
// Check if already exists
if (Object.values(modelAliases).includes(fullModel)) {
continue;
}
// Use model ID as alias
const alias = cleanModelId;
if (modelAliases[alias]) {
continue;
}
await handleSetAlias(cleanModelId, alias, providerStorageAlias);
importedCount += 1;
}
if (importedCount === 0) {
alert(translate("All models already exist, no new models added"));
} else {
alert(translate("Successfully added") + ` ${importedCount} ` + translate("models"));
}
} catch (error) {
console.log("Error importing Qoder models:", error);
alert(translate("Error fetching models") + ": " + error.message);
} finally {
setImportingQoderModels(false);
}
};
const handleRunOneByOneTest = async () => {
if (oneByOneRunning || connections.length === 0) return;
@@ -926,6 +988,20 @@ export default function ProviderDetailPage() {
Add Model
</button>
{/* Import Qoder models button — only show for qoder provider */}
{providerId === "qoder" && connections.some((conn) => conn.isActive !== false) && (
<button
onClick={handleImportQoderModels}
disabled={importingQoderModels}
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-blue-500/40 px-3 py-2 text-xs text-blue-600 dark:text-blue-400 transition-colors hover:border-blue-500 hover:bg-blue-500/5 sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-sm" style={importingQoderModels ? { animation: "spin 1s linear infinite" } : undefined}>
{importingQoderModels ? "progress_activity" : "download"}
</span>
{importingQoderModels ? translate("Fetching...") : translate("Fetch Qoder Models")}
</button>
)}
{/* Suggested models from provider API — show only models not yet added */}
{suggestedModels.length > 0 && (() => {
const addedFullModels = new Set(Object.values(modelAliases));