feat: 自动更新

This commit is contained in:
npc0-hue
2026-07-15 19:43:06 +08:00
parent f64eb0831f
commit f3b14b7945
54 changed files with 3207 additions and 589 deletions
+164 -12
View File
@@ -17,6 +17,108 @@ export interface AiProviderFormState {
redactionPolicy: string;
}
export interface AiProviderKindDefaults {
id: string;
name: string;
kind: AiProviderKind;
baseUrl: string;
apiKeyRef: string;
modelsText: string;
defaultModel: string;
relayMode: AiRelayMode;
timeoutMs: string;
redactionPolicy: string;
requirement: string;
advancedNote: string;
}
export const aiProviderKindDefaults: Record<AiProviderKind, AiProviderKindDefaults> = {
"openai-compatible": {
id: "ai.openai-compatible",
name: "OpenAI Compatible",
kind: "openai-compatible",
baseUrl: "https://relay.example.test/v1",
apiKeyRef: "secret://providers/openai-compatible",
modelsText: "gpt-5.6-luna",
defaultModel: "gpt-5.6-luna",
relayMode: "relay",
timeoutMs: "30000",
redactionPolicy: "default",
requirement: "需要平台侧 API key / secret 引用;Base URL 可按网关修改。",
advancedNote: "OpenAI 兼容网关通常只差 Base URL,模型可以保存后再发现。"
},
openai: {
id: "ai.openai",
name: "OpenAI",
kind: "openai",
baseUrl: "https://api.openai.com/v1",
apiKeyRef: "secret://providers/openai",
modelsText: "gpt-5.6-terra, gpt-5.6-luna",
defaultModel: "gpt-5.6-terra",
relayMode: "relay",
timeoutMs: "30000",
redactionPolicy: "default",
requirement: "官方 SDK/API 使用 API key;平台保存 secret 引用,不在页面保存真实密钥。",
advancedNote: "官方 OpenAI API 使用 Bearer API key 与 /v1 endpoint;模型列表可按账号权限调整。"
},
claude: {
id: "ai.claude",
name: "Claude",
kind: "claude",
baseUrl: "https://api.anthropic.com/v1",
apiKeyRef: "secret://providers/anthropic",
modelsText: "claude-sonnet-5, claude-haiku-4-5-20251001",
defaultModel: "claude-sonnet-5",
relayMode: "relay",
timeoutMs: "30000",
redactionPolicy: "default",
requirement: "Anthropic 请求使用 API key;版本头由平台适配层处理。",
advancedNote: "保持平台中介调用,避免插件或前端接触 Anthropic key。"
},
gemini: {
id: "ai.gemini",
name: "Gemini",
kind: "gemini",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
apiKeyRef: "secret://providers/gemini",
modelsText: "gemini-3.5-flash, gemini-2.5-flash",
defaultModel: "gemini-3.5-flash",
relayMode: "relay",
timeoutMs: "30000",
redactionPolicy: "default",
requirement: "Google Gemini 入门使用 API key;平台保存 secret 引用。",
advancedNote: "Base URL 使用 Google Generative Language API;模型名按项目可用模型调整。"
},
ollama: {
id: "ai.ollama",
name: "Ollama Local",
kind: "ollama",
baseUrl: "http://127.0.0.1:11434/v1",
apiKeyRef: "",
modelsText: "gpt-oss:20b",
defaultModel: "gpt-oss:20b",
relayMode: "local",
timeoutMs: "30000",
redactionPolicy: "default",
requirement: "本地 Ollama 默认不需要 API key;重点是本机/内网 Base URL 和模型名。",
advancedNote: "Ollama OpenAI 兼容接口会忽略 dummy key;平台本地模式不要求用户填写密钥引用。"
},
custom: {
id: "ai.custom",
name: "Custom Provider",
kind: "custom",
baseUrl: "https://provider.example.test/v1",
apiKeyRef: "secret://providers/custom",
modelsText: "custom-model",
defaultModel: "custom-model",
relayMode: "relay",
timeoutMs: "30000",
redactionPolicy: "default",
requirement: "自定义服务至少需要平台 secret 引用、Base URL 和一个模型名。",
advancedNote: "用于非标准协议或私有网关;保存前请确认服务兼容平台适配层。"
}
};
export interface AiProviderMetrics {
total: number;
active: number;
@@ -41,18 +143,7 @@ export interface AiProviderPageInitialState {
}
export function emptyAiProviderForm(): AiProviderFormState {
return {
id: "",
name: "",
kind: "openai-compatible",
baseUrl: "",
apiKeyRef: "secret://providers/",
modelsText: "",
defaultModel: "",
relayMode: "relay",
timeoutMs: "30000",
redactionPolicy: "default"
};
return aiProviderFormFromDefaults("openai");
}
export function aiProviderToForm(provider?: AiProviderResponse): AiProviderFormState {
@@ -73,6 +164,67 @@ export function aiProviderToForm(provider?: AiProviderResponse): AiProviderFormS
};
}
export function aiProviderFormFromDefaults(kind: AiProviderKind): AiProviderFormState {
const defaults = aiProviderKindDefaults[kind];
return {
id: "",
name: defaults.name,
kind: defaults.kind,
baseUrl: defaults.baseUrl,
apiKeyRef: defaults.apiKeyRef,
modelsText: defaults.modelsText,
defaultModel: defaults.defaultModel,
relayMode: defaults.relayMode,
timeoutMs: defaults.timeoutMs,
redactionPolicy: defaults.redactionPolicy
};
}
export function applyAiProviderKindDefaults(current: AiProviderFormState, kind: AiProviderKind): AiProviderFormState {
const defaults = aiProviderKindDefaults[kind];
return {
...current,
id: current.id,
name: defaults.name,
kind: defaults.kind,
baseUrl: defaults.baseUrl,
apiKeyRef: defaults.apiKeyRef,
modelsText: defaults.modelsText,
defaultModel: defaults.defaultModel,
relayMode: defaults.relayMode,
timeoutMs: defaults.timeoutMs,
redactionPolicy: defaults.redactionPolicy
};
}
export function completeAiProviderForm(form: AiProviderFormState): AiProviderFormState {
const defaults = aiProviderKindDefaults[form.kind];
const modelsText = form.modelsText.trim() || defaults.modelsText;
const models = modelsText
.split(",")
.map((model) => model.trim())
.filter(Boolean);
return {
...form,
id: generatedAiProviderId(form),
name: form.name.trim() || defaults.name,
baseUrl: form.baseUrl.trim() || defaults.baseUrl,
apiKeyRef: form.apiKeyRef.trim() || defaults.apiKeyRef,
modelsText,
defaultModel: form.defaultModel.trim() || models[0] || defaults.defaultModel,
relayMode: form.relayMode || defaults.relayMode,
timeoutMs: form.timeoutMs.trim() || defaults.timeoutMs,
redactionPolicy: form.redactionPolicy.trim() || defaults.redactionPolicy
};
}
export function generatedAiProviderId(form: AiProviderFormState): string {
if (form.id.trim()) {
return form.id.trim();
}
return aiProviderKindDefaults[form.kind].id;
}
export function summarizeAiProviders(providers: AiProviderResponse[]): AiProviderMetrics {
return providers.reduce<AiProviderMetrics>(
(metrics, provider) => ({