mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 19:47:10 +08:00
refactor: UI
This commit is contained in:
+79
-115
@@ -12,7 +12,6 @@ import {
|
||||
loadUserConfig,
|
||||
openLogsDirectory,
|
||||
openModelConfig,
|
||||
openModelEditor,
|
||||
saveUserConfig,
|
||||
startProxyService,
|
||||
stopProxyService,
|
||||
@@ -248,6 +247,7 @@ function normalizeModelAdapterTestResults(source) {
|
||||
export function createEmptyModelAdapter() {
|
||||
return {
|
||||
id: "",
|
||||
sort: 0,
|
||||
displayName: "",
|
||||
type: "openai",
|
||||
baseURL: "",
|
||||
@@ -355,6 +355,7 @@ export function normalizeModelAdapter(source) {
|
||||
: "";
|
||||
return {
|
||||
id: asString(raw.id),
|
||||
sort: asPositiveInteger(raw.sort),
|
||||
displayName: asString(raw.displayName || raw.name),
|
||||
type: SUPPORTED_MODEL_ADAPTER_TYPES.has(normalizedType) ? normalizedType : "",
|
||||
baseURL: normalizeBaseURL(raw.baseURL || raw.url),
|
||||
@@ -392,7 +393,29 @@ export function normalizeModelAdapter(source) {
|
||||
}
|
||||
|
||||
export function normalizeModelAdapters(source) {
|
||||
return asArray(source).map((item) => normalizeModelAdapter(item));
|
||||
return asArray(source)
|
||||
.map((item, sourceIndex) => ({
|
||||
adapter: normalizeModelAdapter(item),
|
||||
sourceIndex,
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const leftSort = left.adapter.sort;
|
||||
const rightSort = right.adapter.sort;
|
||||
if (leftSort <= 0 && rightSort <= 0) {
|
||||
return left.sourceIndex - right.sourceIndex;
|
||||
}
|
||||
if (leftSort <= 0) {
|
||||
return 1;
|
||||
}
|
||||
if (rightSort <= 0) {
|
||||
return -1;
|
||||
}
|
||||
return leftSort - rightSort || left.sourceIndex - right.sourceIndex;
|
||||
})
|
||||
.map(({ adapter }, index) => ({
|
||||
...adapter,
|
||||
sort: index + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
export function validateModelAdapters(source) {
|
||||
@@ -400,9 +423,6 @@ export function validateModelAdapters(source) {
|
||||
const seenIdentityKeys = new Set();
|
||||
for (const [index, adapter] of adapters.entries()) {
|
||||
const prefix = `模型 ${index + 1}`;
|
||||
if (!adapter.displayName) {
|
||||
return `${prefix} 的显示名称不能为空`;
|
||||
}
|
||||
if (!SUPPORTED_MODEL_ADAPTER_TYPES.has(adapter.type)) {
|
||||
return `${prefix} 的类型仅支持 OpenAI 或 Anthropic`;
|
||||
}
|
||||
@@ -412,15 +432,27 @@ export function validateModelAdapters(source) {
|
||||
if (!adapter.apiKey) {
|
||||
return `${prefix} 的访问密钥不能为空`;
|
||||
}
|
||||
if (!adapter.tooltipData) {
|
||||
return `${prefix} 的悬停提示不能为空`;
|
||||
if (!adapter.displayName) {
|
||||
return `${prefix} 的显示名称不能为空`;
|
||||
}
|
||||
if (!adapter.modelID) {
|
||||
return `${prefix} 的模型标识不能为空`;
|
||||
}
|
||||
if (adapter.contextWindowTokens && (!Number.isInteger(adapter.contextWindowTokens) || adapter.contextWindowTokens <= 0)) {
|
||||
return `${prefix} 的上下文窗口必须为正整数`;
|
||||
}
|
||||
if (adapter.type === "openai" && !SUPPORTED_REASONING_EFFORTS.has(adapter.reasoningEffort)) {
|
||||
return `${prefix} 的推理强度仅支持 low、medium、high、xhigh、max`;
|
||||
}
|
||||
if (adapter.type === "anthropic" && adapter.anthropicMaxTokens && (!Number.isInteger(adapter.anthropicMaxTokens) || adapter.anthropicMaxTokens <= 0)) {
|
||||
return `${prefix} 的最大输出 Token 必须为正整数`;
|
||||
}
|
||||
if (adapter.type === "anthropic" && !SUPPORTED_ANTHROPIC_THINKING_EFFORTS.has(adapter.anthropicThinkingEffort)) {
|
||||
return `${prefix} 的 Anthropic 思考强度仅支持 low、medium、high、xhigh、max`;
|
||||
}
|
||||
if (adapter.type === "openai" && adapter.maxCompletionTokens && (!Number.isInteger(adapter.maxCompletionTokens) || adapter.maxCompletionTokens <= 0)) {
|
||||
return `${prefix} 的最大输出 Token 必须为正整数`;
|
||||
}
|
||||
if (adapter.type === "openai" && !isValidOpenAIEndpoint(adapter.openAIEndpoint)) {
|
||||
return `${prefix} 的 OpenAI 端点仅支持 /v1/responses、/v1/chat/completions 或以 / 开头的自定义路径`;
|
||||
}
|
||||
@@ -430,29 +462,20 @@ export function validateModelAdapters(source) {
|
||||
return `${prefix} 的 ${extraParamsError}`;
|
||||
}
|
||||
}
|
||||
if (adapter.customHeadersEnabled) {
|
||||
const customHeadersError = validateHeadersJSON(adapter.customHeadersJSON);
|
||||
if (customHeadersError) {
|
||||
return `${prefix} 的 ${customHeadersError}`;
|
||||
}
|
||||
}
|
||||
if (adapter.type === "anthropic" && adapter.anthropicExtraParamsEnabled) {
|
||||
const extraParamsError = validateAnthropicExtraParamsJSON(adapter.anthropicExtraParamsJSON);
|
||||
if (extraParamsError) {
|
||||
return `${prefix} 的 ${extraParamsError}`;
|
||||
}
|
||||
}
|
||||
if (adapter.type === "anthropic" && !SUPPORTED_ANTHROPIC_THINKING_EFFORTS.has(adapter.anthropicThinkingEffort)) {
|
||||
return `${prefix} 的 Anthropic 思考强度仅支持 low、medium、high、xhigh、max`;
|
||||
if (adapter.customHeadersEnabled) {
|
||||
const customHeadersError = validateHeadersJSON(adapter.customHeadersJSON);
|
||||
if (customHeadersError) {
|
||||
return `${prefix} 的 ${customHeadersError}`;
|
||||
}
|
||||
}
|
||||
if (adapter.contextWindowTokens && (!Number.isInteger(adapter.contextWindowTokens) || adapter.contextWindowTokens <= 0)) {
|
||||
return `${prefix} 的上下文窗口必须为正整数`;
|
||||
}
|
||||
if (adapter.maxCompletionTokens && (!Number.isInteger(adapter.maxCompletionTokens) || adapter.maxCompletionTokens <= 0)) {
|
||||
return `${prefix} 的最大输出 Token 必须为正整数`;
|
||||
}
|
||||
if (adapter.anthropicMaxTokens && (!Number.isInteger(adapter.anthropicMaxTokens) || adapter.anthropicMaxTokens <= 0)) {
|
||||
return `${prefix} 的最大输出 Token 必须为正整数`;
|
||||
if (!adapter.tooltipData) {
|
||||
return `${prefix} 的悬停提示不能为空`;
|
||||
}
|
||||
if (adapter.thinkingBudgetTokens && (!Number.isInteger(adapter.thinkingBudgetTokens) || adapter.thinkingBudgetTokens <= 0)) {
|
||||
return `${prefix} 的思考预算 Token 必须为正整数`;
|
||||
@@ -1151,93 +1174,6 @@ export async function fetchAvailableModelIDs(payload) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildPrefixedModelDisplayName(prefix, modelID) {
|
||||
const normalizedPrefix = asString(prefix) || "模型";
|
||||
return `${normalizedPrefix}-${asString(modelID)}`;
|
||||
}
|
||||
|
||||
export function buildModelAdaptersFromModelIDs(source, modelIDs, prefix) {
|
||||
const base = normalizeModelAdapter(source);
|
||||
const seen = new Set();
|
||||
return asArray(modelIDs)
|
||||
.map((item) => asString(item))
|
||||
.filter((modelID) => {
|
||||
if (!modelID || seen.has(modelID)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(modelID);
|
||||
return true;
|
||||
})
|
||||
.map((modelID) => normalizeModelAdapter({
|
||||
...base,
|
||||
id: "",
|
||||
modelID,
|
||||
displayName: buildPrefixedModelDisplayName(prefix, modelID),
|
||||
tooltipData: base.tooltipData || "备注",
|
||||
}));
|
||||
}
|
||||
|
||||
function findModelAdapterUpsertIndex(adapters, target) {
|
||||
return adapters.findIndex((adapter) => {
|
||||
const current = normalizeModelAdapter(adapter);
|
||||
return current.type === target.type
|
||||
&& normalizeBaseURL(current.baseURL) === normalizeBaseURL(target.baseURL)
|
||||
&& current.apiKey === target.apiKey
|
||||
&& current.modelID === target.modelID
|
||||
&& current.displayName === target.displayName
|
||||
&& (current.type !== "openai" || current.openAIEndpoint === target.openAIEndpoint);
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveModelAdaptersFromModelIDs(source, modelIDs, prefix, selectedModelID = "") {
|
||||
const generatedAdapters = buildModelAdaptersFromModelIDs(source, modelIDs, prefix);
|
||||
if (generatedAdapters.length === 0) {
|
||||
return { ok: false, error: "没有可保存的模型" };
|
||||
}
|
||||
|
||||
const generatedError = validateModelAdapters(generatedAdapters);
|
||||
if (generatedError) {
|
||||
return { ok: false, error: generatedError };
|
||||
}
|
||||
|
||||
const currentConfig = await loadPersistedUserConfig();
|
||||
const nextAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
||||
const targetModelID = asString(selectedModelID) || generatedAdapters[0]?.modelID || "";
|
||||
let selectedIndex = -1;
|
||||
|
||||
for (const adapter of generatedAdapters) {
|
||||
const index = findModelAdapterUpsertIndex(nextAdapters, adapter);
|
||||
if (index >= 0) {
|
||||
nextAdapters.splice(index, 1, adapter);
|
||||
if (selectedIndex < 0 && adapter.modelID === targetModelID) {
|
||||
selectedIndex = index;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
nextAdapters.push(adapter);
|
||||
if (selectedIndex < 0 && adapter.modelID === targetModelID) {
|
||||
selectedIndex = nextAdapters.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await persistConfigPayload(
|
||||
{
|
||||
...currentConfig,
|
||||
modelAdapters: nextAdapters,
|
||||
},
|
||||
{ modelAdaptersOnly: true },
|
||||
);
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
index: selectedIndex,
|
||||
adapter: selectedIndex >= 0 ? appState.modelAdapters[selectedIndex] ?? null : null,
|
||||
count: generatedAdapters.length,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteModelAdapterAt(index) {
|
||||
const currentConfig = await loadPersistedUserConfig();
|
||||
const nextAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
||||
@@ -1260,6 +1196,39 @@ export async function deleteModelAdapterAt(index) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveModelAdapterOrder(adapterIDs) {
|
||||
const orderedIDs = asArray(adapterIDs)
|
||||
.map((item) => asString(item))
|
||||
.filter(Boolean);
|
||||
const currentConfig = await loadPersistedUserConfig();
|
||||
const currentAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
||||
const adaptersByID = new Map(currentAdapters.map((adapter) => [adapter.id, adapter]));
|
||||
const uniqueIDs = new Set(orderedIDs);
|
||||
|
||||
if (
|
||||
orderedIDs.length !== currentAdapters.length
|
||||
|| uniqueIDs.size !== currentAdapters.length
|
||||
|| orderedIDs.some((id) => !adaptersByID.has(id))
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "模型配置已发生变化,请刷新后重试",
|
||||
};
|
||||
}
|
||||
|
||||
const nextAdapters = orderedIDs.map((id, index) => ({
|
||||
...adaptersByID.get(id),
|
||||
sort: index + 1,
|
||||
}));
|
||||
return persistConfigPayload(
|
||||
{
|
||||
...currentConfig,
|
||||
modelAdapters: nextAdapters,
|
||||
},
|
||||
{ modelAdaptersOnly: true },
|
||||
);
|
||||
}
|
||||
|
||||
function splitDisplayNameSeed(value) {
|
||||
const text = asString(value);
|
||||
const match = text.match(/^(.*?)(?:\s*[-+](\d+))?$/);
|
||||
@@ -1403,11 +1372,6 @@ export async function openModelConfigWindow() {
|
||||
await openModelConfig();
|
||||
}
|
||||
|
||||
export async function openModelEditorWindow(index, adapter) {
|
||||
const adapterJSON = JSON.stringify(normalizeModelAdapter(adapter));
|
||||
await openModelEditor(index, adapterJSON);
|
||||
}
|
||||
|
||||
export async function checkForAppUpdates() {
|
||||
await checkForUpdates();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user