дȫÏîĿÉó²éÎÊÌⲢ·¢²¼ v1.1.9
¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档 д Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£ Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£ Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢ Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£ Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { apiLogEntry } from "@/utils/configSafety.js";
|
||||
import {
|
||||
GetState,
|
||||
LoadUserConfig,
|
||||
@@ -32,19 +33,8 @@ import { Call } from "@wailsio/runtime";
|
||||
const API_LOG_PREFIX = "[clientApi]";
|
||||
const PROXY_SERVICE_NAME = "cursor/internal/bridge.ProxyService";
|
||||
|
||||
function logSuccess(name, payload, result) {
|
||||
console.log(`${API_LOG_PREFIX} ${name} response`, {
|
||||
payload,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
function logError(name, payload, error) {
|
||||
console.error(`${API_LOG_PREFIX} ${name} error`, {
|
||||
payload,
|
||||
error,
|
||||
});
|
||||
}
|
||||
function logSuccess(name) { console.info(API_LOG_PREFIX, apiLogEntry(name)); }
|
||||
function logError(name) { console.error(API_LOG_PREFIX, apiLogEntry(name, true)); }
|
||||
|
||||
function withApiLogging(name, payload, runner) {
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { safePreferences, editorConflict } from "@/utils/configSafety.js";
|
||||
import { computed, reactive, watchSyncEffect } from "vue";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import dayjs from "dayjs";
|
||||
@@ -580,7 +581,9 @@ function loadCachedState() {
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return {};
|
||||
}
|
||||
return parsed;
|
||||
const safe = safePreferences(parsed);
|
||||
window.localStorage.setItem(APP_STATE_STORAGE_KEY, JSON.stringify(safe));
|
||||
return safe;
|
||||
} catch (_error) {
|
||||
return {};
|
||||
}
|
||||
@@ -591,6 +594,8 @@ function normalizeConfig(source) {
|
||||
const routing = raw.routing && typeof raw.routing === "object" ? raw.routing : {};
|
||||
const homeMetrics = raw.homeMetrics && typeof raw.homeMetrics === "object" ? raw.homeMetrics : {};
|
||||
return {
|
||||
revision: asString(raw.revision || raw.configRevision),
|
||||
cliHttpsListenAddr: asString(raw.cliHttpsListenAddr || raw.configCLIHTTPSListenAddr),
|
||||
log: asBoolean(raw.log),
|
||||
providerStreamIdleTimeout: asPositiveInteger(raw.providerStreamIdleTimeout),
|
||||
backendListenAddr: asString(raw.configBackendListenAddr) || asString(raw.backendListenAddr),
|
||||
@@ -649,6 +654,8 @@ function applyHomeMetrics(raw) {
|
||||
function buildConfigPayload(source = appState) {
|
||||
const normalized = normalizeConfig(source);
|
||||
return {
|
||||
revision: normalized.revision,
|
||||
cliHttpsListenAddr: normalized.cliHttpsListenAddr,
|
||||
log: normalized.log,
|
||||
providerStreamIdleTimeout: normalized.providerStreamIdleTimeout,
|
||||
backendListenAddr: normalized.backendListenAddr,
|
||||
@@ -664,19 +671,12 @@ function buildConfigPayload(source = appState) {
|
||||
|
||||
// sanitizePayloadForCache 剥离敏感字段(apiKey/tabServerToken),避免 localStorage 明文落盘。
|
||||
// 缓存仅用于启动瞬间兜底展示,真实配置始终以后端返回为准。
|
||||
function sanitizePayloadForCache(payload) {
|
||||
return {
|
||||
...payload,
|
||||
modelAdapters: (payload.modelAdapters || []).map(({ apiKey, ...adapter }) => ({
|
||||
...adapter,
|
||||
apiKey: "",
|
||||
})),
|
||||
tabServerToken: "",
|
||||
};
|
||||
}
|
||||
function sanitizePayloadForCache(payload) { return safePreferences(payload); }
|
||||
|
||||
function applyConfigToState(config, { modelAdaptersOnly = false } = {}) {
|
||||
const normalized = normalizeConfig(config);
|
||||
appState.configRevision = normalized.revision;
|
||||
appState.configCLIHTTPSListenAddr = normalized.cliHttpsListenAddr;
|
||||
if (modelAdaptersOnly) {
|
||||
appState.modelAdapters = normalized.modelAdapters;
|
||||
return normalized;
|
||||
@@ -802,6 +802,8 @@ const cachedConfig = normalizeConfig(cachedState);
|
||||
|
||||
export const appState = reactive({
|
||||
appVersion: "",
|
||||
configRevision: "",
|
||||
configCLIHTTPSListenAddr: "",
|
||||
modelAdapters: cachedConfig.modelAdapters,
|
||||
modelAdapterTestResults: {},
|
||||
configBackendListenAddr: cachedConfig.backendListenAddr,
|
||||
@@ -845,29 +847,7 @@ watchSyncEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
APP_STATE_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
...sanitizePayloadForCache(buildConfigPayload()),
|
||||
serviceRunning: appState.serviceRunning,
|
||||
backendRunning: appState.backendRunning,
|
||||
proxyRunning: appState.proxyRunning,
|
||||
serviceLastError: appState.serviceLastError,
|
||||
serviceListenAddr: appState.serviceListenAddr,
|
||||
configBackendListenAddr: appState.configBackendListenAddr,
|
||||
configProxyListenAddr: appState.configProxyListenAddr,
|
||||
backendListenAddr: appState.backendListenAddr,
|
||||
proxyListenAddr: appState.proxyListenAddr,
|
||||
tabServerBaseURL: appState.tabServerBaseURL,
|
||||
tabServerToken: "",
|
||||
cursorSettingsApplied: appState.cursorSettingsApplied,
|
||||
netProxySource: appState.netProxySource,
|
||||
netProxyActive: appState.netProxyActive,
|
||||
netProxyUsingSystem: appState.netProxyUsingSystem,
|
||||
netProxyUsingEnv: appState.netProxyUsingEnv,
|
||||
netProxyHttp: appState.netProxyHttp,
|
||||
netProxyHttps: appState.netProxyHttps,
|
||||
netProxyPacIgnored: appState.netProxyPacIgnored,
|
||||
netProxyDescription: appState.netProxyDescription,
|
||||
}),
|
||||
JSON.stringify(sanitizePayloadForCache(buildConfigPayload())),
|
||||
);
|
||||
} catch (_error) {
|
||||
// ignore local persistence failures
|
||||
@@ -978,7 +958,9 @@ export async function runModelAdapterTest(adapter) {
|
||||
}
|
||||
|
||||
export async function persistUserConfig() {
|
||||
const expected = appState.configRevision;
|
||||
const currentConfig = await loadPersistedUserConfig();
|
||||
if (currentConfig.revision !== expected) return {ok:false,error:"CONFIG_CONFLICT: 配置已更改,请重新加载;草稿已保留"};
|
||||
return persistConfigPayload({
|
||||
...currentConfig,
|
||||
modelAdapters: normalizeModelAdapters(appState.modelAdapters),
|
||||
@@ -1077,8 +1059,9 @@ export async function reloadUserConfig(options = {}) {
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function saveModelAdapterAt(index, adapter) {
|
||||
export async function saveModelAdapterAt(index, adapter, expectedRevision, originalID) {
|
||||
const currentConfig = await loadPersistedUserConfig();
|
||||
if (editorConflict(currentConfig, expectedRevision, index, originalID)) return { ok:false, error:"CONFIG_CONFLICT: 配置已在其他窗口更改,请刷新版本后再保存;草稿已保留" };
|
||||
const nextAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
||||
const nextAdapter = normalizeModelAdapter(adapter);
|
||||
const targetIndex = index >= 0 && index < nextAdapters.length ? index : nextAdapters.length;
|
||||
@@ -1311,7 +1294,7 @@ export async function openModelConfigWindow() {
|
||||
}
|
||||
|
||||
export async function openModelEditorWindow(index, adapter) {
|
||||
const adapterJSON = JSON.stringify(normalizeModelAdapter(adapter));
|
||||
const adapterJSON = JSON.stringify({ ...normalizeModelAdapter(adapter), __revision: appState.configRevision, __originalID: adapter?.id || "" });
|
||||
await openModelEditor(index, adapterJSON);
|
||||
}
|
||||
|
||||
|
||||
21
frontend/src/utils/configSafety.js
Normal file
21
frontend/src/utils/configSafety.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// Cache an explicit preference allowlist; model and authentication data stay in memory.
|
||||
export function safePreferences(payload = {}) {
|
||||
const metrics = payload.homeMetrics || {};
|
||||
return {
|
||||
homeMetrics: {
|
||||
includeCacheWriteInHitRate: metrics.includeCacheWriteInHitRate === true,
|
||||
persistentMode: metrics.persistentMode !== false,
|
||||
refreshIntervalSeconds: Number(metrics.refreshIntervalSeconds) || 60,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function apiLogEntry(operation, failed = false) {
|
||||
return { operation, status: failed ? "failed" : "completed" };
|
||||
}
|
||||
|
||||
export function editorConflict(current, expectedRevision, index, originalID) {
|
||||
if (expectedRevision !== undefined && current.revision !== expectedRevision) return true;
|
||||
if (index >= 0 && (!current.modelAdapters[index] || (originalID && current.modelAdapters[index].id !== originalID))) return true;
|
||||
return false;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
OPENAI_ENDPOINT_RESPONSES,
|
||||
OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
|
||||
runModelAdapterTest,
|
||||
reloadUserConfig,
|
||||
saveModelAdapterAt,
|
||||
toUserError,
|
||||
validateModelAdapters,
|
||||
@@ -53,6 +54,8 @@ const openAIEndpointOptions = [
|
||||
];
|
||||
|
||||
const editorIndex = ref(-1);
|
||||
const editorRevision = ref("");
|
||||
const originalID = ref("");
|
||||
const draft = reactive(createEmptyModelAdapter());
|
||||
const errorMessage = ref("");
|
||||
const loading = ref(true);
|
||||
@@ -142,6 +145,8 @@ async function loadContext() {
|
||||
const ctx = await getModelEditorContext();
|
||||
editorIndex.value = typeof ctx.index === "number" ? ctx.index : -1;
|
||||
const parsed = JSON.parse(ctx.adapterJSON || "{}");
|
||||
editorRevision.value = parsed.__revision || appState.configRevision;
|
||||
originalID.value = parsed.__originalID || parsed.id || "";
|
||||
Object.assign(draft, normalizeModelAdapter(parsed));
|
||||
if (!draft.type) {
|
||||
draft.type = "openai";
|
||||
@@ -163,7 +168,7 @@ async function persistDraft() {
|
||||
return { ok: false, error: singleCheck, adapter: null };
|
||||
}
|
||||
|
||||
const result = await saveModelAdapterAt(editorIndex.value, adapter);
|
||||
const result = await saveModelAdapterAt(editorIndex.value, adapter, editorRevision.value, originalID.value);
|
||||
if (!result.ok) {
|
||||
errorMessage.value = result.error;
|
||||
return { ok: false, error: result.error, adapter: null };
|
||||
@@ -175,6 +180,8 @@ async function persistDraft() {
|
||||
if (result.adapter) {
|
||||
Object.assign(draft, normalizeModelAdapter(result.adapter));
|
||||
}
|
||||
editorRevision.value = appState.configRevision;
|
||||
originalID.value = result.adapter?.id || originalID.value;
|
||||
errorMessage.value = "";
|
||||
return {
|
||||
ok: true,
|
||||
@@ -183,6 +190,19 @@ async function persistDraft() {
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshEditorRevision() {
|
||||
try {
|
||||
const latest = await reloadUserConfig();
|
||||
if (editorIndex.value >= 0) {
|
||||
const index = latest.modelAdapters.findIndex(item => item.id === originalID.value);
|
||||
if (index < 0) { errorMessage.value = "原模型已被修改或删除,草稿仍保留。请先核对其他窗口中的修改。"; return; }
|
||||
editorIndex.value = index;
|
||||
}
|
||||
editorRevision.value = latest.revision;
|
||||
errorMessage.value = "版本已刷新,草稿保持不变。请核对后保存。";
|
||||
} catch (error) { errorMessage.value = toUserError(error); }
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const result = await persistDraft();
|
||||
if (!result.ok) {
|
||||
@@ -542,6 +562,7 @@ onMounted(async () => {
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<Button v-if="errorMessage.includes('CONFIG_CONFLICT')" @click="refreshEditorRevision">刷新版本,保留草稿</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user