Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/atom112_aio.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
13 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "atom112/aio", "files": {"src/core/utils/models.ts": {"e77a3a145e375788b9e2f3e2ddd5606713ff185e": {"t": 1786067560.6301908, "n": 12026, "h": "a096d7cb4205538d", "c": "/**\n * 模型目录加载与查询工具\n * @description 包装 @aio/models-data 包,提供 catalog 加载、模型查询、能力补全\n * @data-source D:/base/aio-models-data (每周日 04:00 UTC 自动同步)\n */\n\nimport { invoke } from '@tauri-apps/api/core';\nimport type { ModelMeta, ProviderMeta, Catalog } from '@aio/models-data';\nimport {\n formatDateTime as formatLocaleDateTime,\n formatRelativeTime as formatLocaleRelativeTime,\n t,\n} from '../i18n';\n\nexport type { ModelMeta, ProviderMeta, Catalog } from '@aio/models-data';\n\n/** 从 provider API 拉取并持久化的模型条目(仿 LobeHub 列表行) */\nexport interface FetchedModel {\n id: string;\n ownedBy: string;\n /** 厂商返回的展示名(Google `displayName` / Anthropic `display_name`),OpenAI/Ollama 无 */\n displayName?: string;\n /** 发布日期 YYYY-MM-DD,OpenAI `created` 转 / Anthropic `created_at` 原样 */\n releasedAt?: string;\n}\n\nexport interface ProviderConfig {\n id: string;\n enabled: boolean;\n displayName: string;\n apiUrl: string;\n apiKey: string;\n enabledModels: string[];\n isCustom: boolean;\n customModelIds: string[];\n /** per-provider HTTP/HTTPS 代理 (例如 `http://127.0.0.1:7890`),用于解决国内访问 OpenAI/Google 的网络问题 */\n proxyUrl?: string;\n /** 从 API 持久化拉取的模型列表,用于仿 LobeHub 风格的双段 toggle 列表 */\n fetchedModels?: FetchedModel[];\n /** 本地引擎专属:模型文件路径映射 { modelId -> localPath } */\n localModelPaths?: Record<string, string>;\n}\n\nexport interface ProviderConfigFile {\n version: number;\n updatedAt: string;\n providers: Record<string, ProviderConfig>;\n}\n\n// ==================== 本地推理引擎供应商定义 ====================\n\n/** 内置本地推理引擎供应商定义 */\nexport interface LocalEngineProvider {\n id: string; // provider identifier, e.g. 'local-llamacpp'\n name: string; // display name, e.g. 'llama.cpp'\n defaultApiUrl: string; // pre-filled URL, e.g. 'http://127.0.0.1:8080/v1'\n engineType: string; // 'llama_cpp' | 'vllm' | 'ollama'\n fileExtensions: string[]; // ['gguf'] for file picker, [] for no file picker\n allowModelFileSelection: boolean;\n engineInstallUrl?: string; // docs URL for manual install (not AIO auto-install)\n /** 是否支持从 API 拉取模型列表(Ollama 的 /api/tags;llama.cpp/vLLM 启动时固定模型,无需拉取) */\n supportsFetchModels?: boolean;\n}\n\nexport const LOCAL_ENGINE_PROVIDERS: LocalEngineProvider[] = [\n {\n id: 'local-llamacpp',\n name: 'llama.cpp',\n defaultApiUrl: 'http://127.0.0.1:8080/v1',\n engineType: 'llama_cpp',\n fileExtensions: ['gguf'],\n allowModelFileSelection: true,\n engineInstallUrl: 'https://github.com/ggml-org/llama.cpp',\n supportsFetchModels: false,\n },\n {\n id: 'local-vllm',\n name: 'vLLM',\n defaultApiUrl: 'http://127.0.0.1:8000/v1',\n engineType: 'vllm',\n fileExtensions: [],\n allowModelFileSelection: false,\n engineInstallUrl: 'https://docs.vllm.ai',\n supportsFetchModels: false,\n },\n {\n id: 'ollama',\n name: 'Ollama',\n defaultApiUrl: 'http://localhost:11434',\n engineType: 'ollama',\n fileExtensions: ['gguf'],\n allowModelFileSelection: true,\n engineInstallUrl: 'https://ollama.ai',\n supportsFetchModels: true,\n },\n];\n\n/** 判断 provider id 是否是本地引擎 provider */\nexport const isLocalEngineProvider = (id: string): boolean =>\n LOCAL_ENGINE_PROVIDERS.some((p) => p.id === id);\n\n/** 根据 provider id 取 LocalEngineProvider 定义 */\nexport const getLocalEngineProvider = (id: string): LocalEngineProvider | undefined =>\n LOCAL_ENGINE_PROVIDERS.find((p) => p.id === id);\n\nexport interface TestConnectionResult {\n success: boolean;\n modelCount: number;\n sampleModelIds: string[];\n error: string | null;\n elapsedMs: number;\n}\n\nexport interface FetchLiveModelsResult {\n success: boolean;\n models: Array<{ id: string; owned_by: string; display_name?: string; released_at?: string }>;\n error: string | null;\n elapsedMs: number;\n}\n\nexport type CatalogSourceTag = 'appdata' | 'bundled' | 'dev_fallback' | 'empty';\n\nexport interface CatalogResponse {\n source: CatalogSourceTag;\n json: string;\n path: string | null;\n version: string | null;\n generatedAt: string | null;\n}\n\nexport interface UpdateResult {\n success: boolean;\n modelCount: number;\n providerCount: number;\n version: string;\n cachedPath: string;\n error: string | null;\n bytes: number;\n elapsedMs: number;\n}\n\nlet cachedCatalog: Catalog | null = null;\nlet cachedSource: CatalogSourceTag = 'empty';\nlet cachedPath: string | null = null;\nlet cachedVersion: string | null = null;\nlet cachedGeneratedAt: string | null = null;\nlet loadPromise: Promise<Catalog> | null = null;\n\nconst EMPTY_CATALOG: Catalog = {\n version: '0.0.0',\n generatedAt: '1970-01-01T00:00:00.000Z',\n source: '',\n providerCount: 0,\n modelCount: 0,\n providers: [],\n models: [],\n};\n\nfunction applyResponse(resp: CatalogResponse): Catalog {\n const cat = JSON.parse(resp.json) as Catalog;\n cachedCatalog = cat;\n cachedSource = resp.source;\n cachedPath = resp.path;\n cachedVersion = resp.version;\n cachedGeneratedAt = resp.generatedAt;\n return cat;\n}\n\nasync function loadCatalogFromBackend(): Promise<Catalog> {\n try {\n const resp = await invoke<CatalogResponse>('load_models_catalog_full');\n return applyResponse(resp);\n } catch (e) {\n console.warn('[models] 从后端加载 catalog 失败:', e);\n return EMPTY_CATALOG;\n }\n}\n\nexport async function loadModelsCatalog(): Promise<Catalog> {\n if (cachedCatalog) return cachedCatalog;\n if (loadPromise) return loadPromise;\n loadPromise = loadCatalogFromBackend().then((c) => {\n loadPromise = null;\n return c;\n });\n return loadPromise;\n}\n\nexport function getCachedCatalog(): Catalog | null {\n return cachedCatalog;\n}\n\nexport function getCatalogMeta(): {\n source: CatalogSourceTag;\n path: string | null;\n version: string | null;\n generatedAt: string | null;\n} {\n return {\n source: cachedSource,\n path: cachedPath,\n version: cachedVersion,\n generatedAt: cachedGeneratedAt,\n };\n}\n\nexport async function refreshModelsCatalog(): Promise<Catalog> {\n loadPromise = null;\n return loadModelsCatalog();\n}\n\nexport async function updateModelsCatalog(url?: string): Promise<UpdateResult> {\n const result = await invoke<UpdateResult>('update_models_catalog', { url: url ?? null });\n if (result.success) {\n await refreshModelsCatalog();\n }\n return result;\n}\n\nexport async function getCatalogUrl(): Promise<string> {\n return await invoke<string>('get_catalog_url');\n}\n\nexport function findModel(catalog: Catalog, provider: string, id: string): ModelMeta | null {\n const prov = (provider || '').toLowerCase();\n const target = (id || '').toLowerCase();\n\n const direct = catalog.models.find(\n (m: ModelMeta) => m.provider.toLowerCase() === prov && m.id.toLowerCase() === target,\n );\n if (direct) return direct;\n\n return (\n catalog.models.find(\n (m: ModelMeta) =>\n m.provider.toLowerCase() === prov &&\n m.aliases.some((a: string) => a.toLowerCase() === target),\n ) ?? null\n );\n}\n\nexport function listProvidersForApiUrl(catalog: Catalog, apiUrl: string): ProviderMeta[] {\n if (!apiUrl) return catalog.providers.slice(0, 50);\n const u = apiUrl.toLowerCase();\n const knownHosts: Record<string, string> = {\n 'api.openai.com': 'openai',\n 'api.anthropic.com': 'anthropic',\n 'generativelanguage.googleapis.com': 'google',\n 'api.deepseek.com': 'deepseek',\n 'api.groq.com': 'groq',\n 'api.mistral.ai': 'mistral',\n 'api.x.ai': 'xai',\n 'openrouter.ai': 'openrouter',\n 'api.cohere.ai': 'cohere',\n };\n for (const [host, prov] of Object.entries(knownHosts)) {\n if (u.includes(host)) {\n return catalog.providers.filter((p: ProviderMeta) => p.id === prov);\n }\n }\n return catalog.providers.filter((p: ProviderMeta) => !p.isAggregator).slice(0, 50);\n}\n\nexport function listCatalogModels(catalog: Catalog, providerId?: string): ModelMeta[] {\n if (!providerId || providerId === 'all') return catalog.models;\n return catalog.models.filter((m: ModelMeta) => m.provider === providerId);\n}\n\n/** 格式化上下文窗口(1234567 → \"1.2M\") */\nexport function formatContextWindow(tokens: number): string {\n if (tokens >= 1_000_000)\n return `${(tokens / 1_000_000).toFixed(tokens % 1_000_000 === 0 ? 0 : 1)}M`;\n if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(0)}K`;\n return String(tokens);\n}\n\n/** 格式化单条价格(per 1M tokens) */\nexport function formatPricing(input: number, output: number): string {\n if (input === 0 && output === 0) return t('common.free');\n return `$${input}/${output}/1M`;\n}\n\n/** 把 ISO 时间格式化为「X 天前」之类的相对描述 */\nexport function formatRelativeTime(iso: string | null): string {\n if (!iso) return t('common.neverUpdated');\n try {\n const d = new Date(iso);\n const now = Date.now();\n const diff = now - d.getTime();\n const minutes = Math.floor(diff / 60_000);\n if (minutes < 1) return formatLocaleRelativeTime(0, 'minute');\n if (minutes < 60) return formatLocaleRelativeTime(-minutes, 'minute');\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return formatLocaleRelativeTime(-hours, 'hour');\n const days = Math.floor(hours / 24);\n if (days < 30) return formatLocaleRelativeTime(-days, 'day');\n const months = Math.floor(days / 30);\n if (months < 12) return formatLocaleRelativeTime(-months, 'month');\n return formatLocaleRelativeTime(-Math.floor(months / 12), 'year');\n } catch {\n return iso;\n }\n}\n\n/** 格式化 releaseDate(\"2025-01-15\" → \"2025-01\")只取年月 */\nexport function formatReleaseDate(iso: string | null | undefined): string {\n if (!iso) return '';\n return formatLocaleDateTime(`${iso.slice(0, 10)}T00:00:00`, { year: 'numeric', month: 'short' });\n}\n\n// ==================== v2 工具函数 ====================\n\n/**\n * 合并 catalog 模型元数据 + 用户启用状态, 拆分为三段\n * - enabled: 已启用的 (按 enabledIds 原顺序)\n * - available: catalog 中有但未启用的 (按 releaseDate desc, 同日期按 id 字母序)\n * - orphans: enabledModels 中有但 catalog 没有的 (遗留 custom / 老 config)\n */\nexport function listProviderModels(\n catalog: Catalog,\n providerId: string,\n enabledIds: readonly string[],\n): {\n enabled: ModelMeta[];\n available: ModelMeta[];\n orphans: string[];\n} {\n const catalogModels = catalog.models.filter((m: ModelMeta) => m.provider === providerId);\n const enabledSet = new Set(enabledIds);\n const enabledOrder = new Map(enabledIds.map((id, i) => [id, i]));\n\n const enabled: ModelMeta[] = [];\n const available: ModelMeta[] = [];\n const seen = new Set<string>();\n\n for (const m of catalogModels) {\n if (enabledSet.has(m.id)) {\n enabled.push(m);\n seen.add(m.id);\n }\n }\n // 按 enabledIds 顺序排序 enabled\n enabled.sort((a, b) => (enabledOrder.get(a.id) ?? 0) - (enabledOrder.get(b.id) ?? 0));\n\n for (const m of catalogModels) {\n if (!seen.has(m.id)) {\n available.push(m);\n }\n }\n // 按 releaseDate desc, 同日期按 id 字母序\n available.sort((a, b) => {\n const ra = a.releaseDate ?? '';\n const rb = b.releaseDate ?? '';\n if (ra !== rb) return rb.localeCompare(ra); // desc\n return a.id.localeCompare(b.id);\n });\n\n // orphans: enabledIds 中有但 catalog 没有的\n const catalogIds = new Set(catalogModels.map((m: ModelMeta) => m.id));\n const orphans = enabledIds.filter((id) => !catalogIds.has(id));\n\n return { enabled, available, orphans };\n}\n\n/**\n * 通过 host 反查 provider id (统一入口, 替代三处重复实现)\n * 优先匹配 catalog 中 ProviderMeta 的 api 字段, 兜底用 9 个常见 host 映射\n */\nconst KNOWN_HOSTS: ReadonlyArray<readonly [string, string]> = [\n ['api.openai.com', 'openai'],\n ['api.anthropic.com', 'anthropic'],\n ['generativelanguage.googleapis.com', 'google'],\n ['api.deepseek.com', 'deepseek'],\n ['api.groq.com', 'groq'],\n ['api.mistral.ai', 'mistral'],\n ['api.x.ai', 'xai'],\n ['api.cohere.ai', 'cohere'],\n ['openrouter.ai', 'openrouter'],\n];\n\nexport function detectProviderByUrl(url: string): string | null {\n if (!url) return null;\n const u = url.toLowerCase();\n for (const [host, pid] of KNOWN_HOSTS) {\n if (u.includes(host)) return pid;\n }\n return null;\n}\n\n/** 列表页搜索: 仅匹配 provider id / name, 不含模型名 */\nexport function searchProviders(catalog: Catalog, query: string): ProviderMeta[] {\n if (!query.trim()) return catalog.providers;\n const q = query.toLowerCase();\n return catalog.providers.filter(\n (p: ProviderMeta) => p.id.includes(q) || p.name.toLowerCase().includes(q),\n );\n}\n"}}}}