codex-endpoint-switcher 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/bin/codex-switcher.js +27 -0
- package/package.json +2 -1
- package/src/main/cloud-sync-client.js +328 -0
- package/src/main/main.js +33 -0
- package/src/main/preload.js +24 -0
- package/src/main/profile-manager.js +248 -0
- package/src/renderer/index.html +71 -0
- package/src/renderer/renderer.js +187 -1
- package/src/renderer/styles.css +41 -2
- package/src/web/cloud-sync-server.js +419 -0
- package/src/web/server.js +57 -0
|
@@ -11,6 +11,7 @@ const endpointStorePath = path.join(codexRoot, "endpoint-presets.json");
|
|
|
11
11
|
const endpointStatePath = path.join(codexRoot, "endpoint-switcher-state.json");
|
|
12
12
|
const proxyPort = 3187;
|
|
13
13
|
const proxyBaseUrl = `http://127.0.0.1:${proxyPort}/`;
|
|
14
|
+
const syncCodePrefix = "ces1:";
|
|
14
15
|
|
|
15
16
|
function escapeRegExp(value) {
|
|
16
17
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -79,6 +80,123 @@ function validateEndpointPayload(payload) {
|
|
|
79
80
|
return { note, url, key };
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
function normalizeTimestamp(value, fallbackValue) {
|
|
84
|
+
const raw = String(value || "").trim();
|
|
85
|
+
if (!raw) {
|
|
86
|
+
return fallbackValue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const parsed = new Date(raw);
|
|
90
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
91
|
+
return fallbackValue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return parsed.toISOString();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function encodeSyncCode(payload) {
|
|
98
|
+
return `${syncCodePrefix}${Buffer.from(JSON.stringify(payload), "utf8").toString("base64url")}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function decodeSyncCode(syncCode) {
|
|
102
|
+
const input = String(syncCode || "").trim();
|
|
103
|
+
if (!input) {
|
|
104
|
+
throw new Error("同步码不能为空。");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (input.startsWith("{")) {
|
|
108
|
+
return JSON.parse(input);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (input.startsWith(syncCodePrefix)) {
|
|
112
|
+
const encoded = input.slice(syncCodePrefix.length).trim();
|
|
113
|
+
if (!encoded) {
|
|
114
|
+
throw new Error("同步码格式不正确。");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error("同步码格式不正确。请使用系统生成的同步码,或粘贴 JSON 内容。");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeSyncPayload(payload) {
|
|
124
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
125
|
+
throw new Error("同步数据格式不正确。");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const schemaVersion = Number(payload.schemaVersion || payload.version || 0);
|
|
129
|
+
if (schemaVersion !== 1) {
|
|
130
|
+
throw new Error("当前只支持导入 v1 同步数据。");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const rawEndpoints = Array.isArray(payload.endpoints) ? payload.endpoints : [];
|
|
134
|
+
if (rawEndpoints.length === 0) {
|
|
135
|
+
throw new Error("同步数据里没有可导入的连接。");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const usedIds = new Set();
|
|
139
|
+
const endpoints = rawEndpoints.map((item) => {
|
|
140
|
+
const normalized = validateEndpointPayload(item);
|
|
141
|
+
const fallbackTimestamp = new Date().toISOString();
|
|
142
|
+
let id = String(item.id || "").trim();
|
|
143
|
+
|
|
144
|
+
if (!id || usedIds.has(id)) {
|
|
145
|
+
id = randomUUID();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
usedIds.add(id);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
id,
|
|
152
|
+
note: normalized.note,
|
|
153
|
+
url: normalized.url,
|
|
154
|
+
key: normalized.key,
|
|
155
|
+
createdAt: normalizeTimestamp(item.createdAt, fallbackTimestamp),
|
|
156
|
+
updatedAt: normalizeTimestamp(item.updatedAt, fallbackTimestamp),
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
schemaVersion,
|
|
162
|
+
exportedAt: normalizeTimestamp(payload.exportedAt, new Date().toISOString()),
|
|
163
|
+
activeEndpointId: String(payload.activeEndpointId || "").trim(),
|
|
164
|
+
endpoints,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildSyncPayload(endpoints, activeEndpointId) {
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
exportedAt: new Date().toISOString(),
|
|
172
|
+
source: "codex-endpoint-switcher",
|
|
173
|
+
activeEndpointId: String(activeEndpointId || "").trim(),
|
|
174
|
+
endpoints: endpoints.map((endpoint) => ({
|
|
175
|
+
id: endpoint.id,
|
|
176
|
+
note: endpoint.note,
|
|
177
|
+
url: endpoint.url,
|
|
178
|
+
key: endpoint.key,
|
|
179
|
+
createdAt: endpoint.createdAt,
|
|
180
|
+
updatedAt: endpoint.updatedAt,
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function findEndpointMatchIndex(endpoints, importedEndpoint) {
|
|
186
|
+
const byId = endpoints.findIndex((item) => item.id === importedEndpoint.id);
|
|
187
|
+
if (byId !== -1) {
|
|
188
|
+
return byId;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return endpoints.findIndex(
|
|
192
|
+
(item) => item.note === importedEndpoint.note && item.url === importedEndpoint.url,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveImportedEndpointId(endpoints, importedId) {
|
|
197
|
+
return endpoints.find((item) => item.id === importedId)?.id || "";
|
|
198
|
+
}
|
|
199
|
+
|
|
82
200
|
async function ensureDirectory(targetPath) {
|
|
83
201
|
await fs.mkdir(targetPath, { recursive: true });
|
|
84
202
|
}
|
|
@@ -302,6 +420,26 @@ async function backupCurrentState() {
|
|
|
302
420
|
};
|
|
303
421
|
}
|
|
304
422
|
|
|
423
|
+
async function backupEndpointData() {
|
|
424
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
425
|
+
const endpointStoreBackupPath = path.join(
|
|
426
|
+
backupDir,
|
|
427
|
+
`endpoint-presets.json.sync-${timestamp}.bak`,
|
|
428
|
+
);
|
|
429
|
+
const endpointStateBackupPath = path.join(
|
|
430
|
+
backupDir,
|
|
431
|
+
`endpoint-switcher-state.json.sync-${timestamp}.bak`,
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
await fs.copyFile(endpointStorePath, endpointStoreBackupPath);
|
|
435
|
+
await fs.copyFile(endpointStatePath, endpointStateBackupPath);
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
endpointStoreBackupPath,
|
|
439
|
+
endpointStateBackupPath,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
305
443
|
function buildEndpointResponse(endpoint, activeId) {
|
|
306
444
|
return {
|
|
307
445
|
id: endpoint.id,
|
|
@@ -582,6 +720,114 @@ async function enableProxyMode() {
|
|
|
582
720
|
};
|
|
583
721
|
}
|
|
584
722
|
|
|
723
|
+
/**
|
|
724
|
+
* 导出当前保存的连接为可跨机器粘贴传输的同步码。
|
|
725
|
+
*/
|
|
726
|
+
async function exportSyncPackage() {
|
|
727
|
+
const context = await resolveActiveEndpointContext();
|
|
728
|
+
const payload = buildSyncPayload(context.endpoints, context.activeEndpointId);
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
syncCode: encodeSyncCode(payload),
|
|
732
|
+
exportedAt: payload.exportedAt,
|
|
733
|
+
endpointCount: payload.endpoints.length,
|
|
734
|
+
activeEndpointId: payload.activeEndpointId,
|
|
735
|
+
endpoints: context.endpoints.map((item) => ({
|
|
736
|
+
id: item.id,
|
|
737
|
+
note: item.note,
|
|
738
|
+
url: item.url,
|
|
739
|
+
maskedKey: maskKey(item.key),
|
|
740
|
+
isActive: item.id === context.activeEndpointId,
|
|
741
|
+
})),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* 导入同步码,支持合并现有连接或直接覆盖本机连接列表。
|
|
747
|
+
*/
|
|
748
|
+
async function importSyncPackage(syncCode, mode = "merge") {
|
|
749
|
+
const safeMode = String(mode || "merge").trim().toLowerCase();
|
|
750
|
+
if (!["merge", "replace"].includes(safeMode)) {
|
|
751
|
+
throw new Error("导入模式不正确,只支持 merge 或 replace。");
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const importedPayload = normalizeSyncPayload(decodeSyncCode(syncCode));
|
|
755
|
+
const [existingEndpoints, existingState] = await Promise.all([
|
|
756
|
+
readEndpointStore(),
|
|
757
|
+
readEndpointState(),
|
|
758
|
+
]);
|
|
759
|
+
|
|
760
|
+
const workingEndpoints = safeMode === "replace" ? [] : existingEndpoints.slice();
|
|
761
|
+
const importIdMapping = new Map();
|
|
762
|
+
let addedCount = 0;
|
|
763
|
+
let updatedCount = 0;
|
|
764
|
+
|
|
765
|
+
for (const importedEndpoint of importedPayload.endpoints) {
|
|
766
|
+
const matchIndex = findEndpointMatchIndex(workingEndpoints, importedEndpoint);
|
|
767
|
+
|
|
768
|
+
if (matchIndex !== -1) {
|
|
769
|
+
const currentEndpoint = workingEndpoints[matchIndex];
|
|
770
|
+
workingEndpoints[matchIndex] = {
|
|
771
|
+
...currentEndpoint,
|
|
772
|
+
note: importedEndpoint.note,
|
|
773
|
+
url: importedEndpoint.url,
|
|
774
|
+
key: importedEndpoint.key,
|
|
775
|
+
createdAt: currentEndpoint.createdAt || importedEndpoint.createdAt,
|
|
776
|
+
updatedAt: importedEndpoint.updatedAt,
|
|
777
|
+
};
|
|
778
|
+
importIdMapping.set(importedEndpoint.id, workingEndpoints[matchIndex].id);
|
|
779
|
+
updatedCount += 1;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let nextId = importedEndpoint.id;
|
|
784
|
+
while (resolveImportedEndpointId(workingEndpoints, nextId)) {
|
|
785
|
+
nextId = randomUUID();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const nextEndpoint = {
|
|
789
|
+
...importedEndpoint,
|
|
790
|
+
id: nextId,
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
workingEndpoints.push(nextEndpoint);
|
|
794
|
+
importIdMapping.set(importedEndpoint.id, nextEndpoint.id);
|
|
795
|
+
addedCount += 1;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const currentActiveStillExists = workingEndpoints.some(
|
|
799
|
+
(item) => item.id === existingState.activeEndpointId,
|
|
800
|
+
);
|
|
801
|
+
const importedActiveId = importIdMapping.get(importedPayload.activeEndpointId) || "";
|
|
802
|
+
const nextActiveId = currentActiveStillExists
|
|
803
|
+
? existingState.activeEndpointId
|
|
804
|
+
: importedActiveId || (workingEndpoints[0] ? workingEndpoints[0].id : "");
|
|
805
|
+
const activeEndpoint = workingEndpoints.find((item) => item.id === nextActiveId) || null;
|
|
806
|
+
|
|
807
|
+
const backupPaths = await backupEndpointData();
|
|
808
|
+
await writeEndpointStore(workingEndpoints);
|
|
809
|
+
await writeEndpointState({
|
|
810
|
+
activeEndpointId: nextActiveId,
|
|
811
|
+
proxyModeEnabled: existingState.proxyModeEnabled,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
...backupPaths,
|
|
816
|
+
mode: safeMode,
|
|
817
|
+
importedCount: importedPayload.endpoints.length,
|
|
818
|
+
addedCount,
|
|
819
|
+
updatedCount,
|
|
820
|
+
totalEndpoints: workingEndpoints.length,
|
|
821
|
+
activeEndpointId: nextActiveId,
|
|
822
|
+
activeEndpointNote: activeEndpoint ? activeEndpoint.note : "",
|
|
823
|
+
importedActiveApplied: Boolean(importedActiveId && importedActiveId === nextActiveId),
|
|
824
|
+
message:
|
|
825
|
+
safeMode === "merge"
|
|
826
|
+
? "已合并导入同步数据。当前连接不会被强制切换,除非原当前连接已不存在。"
|
|
827
|
+
: "已覆盖本机连接列表。若原当前连接已不存在,则会切到同步数据里的当前连接。",
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
585
831
|
function getManagedPaths() {
|
|
586
832
|
return {
|
|
587
833
|
codexRoot,
|
|
@@ -616,9 +862,11 @@ module.exports = {
|
|
|
616
862
|
createEndpoint,
|
|
617
863
|
deleteEndpoint,
|
|
618
864
|
enableProxyMode,
|
|
865
|
+
exportSyncPackage,
|
|
619
866
|
getCurrentEndpointSummary,
|
|
620
867
|
getManagedPaths,
|
|
621
868
|
getProxyTarget,
|
|
869
|
+
importSyncPackage,
|
|
622
870
|
listEndpoints,
|
|
623
871
|
proxyBaseUrl,
|
|
624
872
|
proxyPort,
|
package/src/renderer/index.html
CHANGED
|
@@ -122,6 +122,77 @@
|
|
|
122
122
|
<button id="clearFormButton" type="button" class="ghost-button">清空表单</button>
|
|
123
123
|
</div>
|
|
124
124
|
</form>
|
|
125
|
+
|
|
126
|
+
<div class="stack-form stack-form-alt">
|
|
127
|
+
<div>
|
|
128
|
+
<p class="panel-kicker">Cloud Sync</p>
|
|
129
|
+
<h3>账号配置同步</h3>
|
|
130
|
+
</div>
|
|
131
|
+
<label class="field">
|
|
132
|
+
<span>同步服务器</span>
|
|
133
|
+
<input
|
|
134
|
+
id="cloudServerUrl"
|
|
135
|
+
type="text"
|
|
136
|
+
placeholder="例如:http://127.0.0.1:3190"
|
|
137
|
+
autocomplete="off"
|
|
138
|
+
/>
|
|
139
|
+
</label>
|
|
140
|
+
<label class="field">
|
|
141
|
+
<span>账号</span>
|
|
142
|
+
<input
|
|
143
|
+
id="cloudUsername"
|
|
144
|
+
type="text"
|
|
145
|
+
placeholder="例如:yourname"
|
|
146
|
+
autocomplete="off"
|
|
147
|
+
/>
|
|
148
|
+
</label>
|
|
149
|
+
<label class="field">
|
|
150
|
+
<span>密码</span>
|
|
151
|
+
<input
|
|
152
|
+
id="cloudPassword"
|
|
153
|
+
type="password"
|
|
154
|
+
placeholder="输入同步账号密码"
|
|
155
|
+
autocomplete="off"
|
|
156
|
+
/>
|
|
157
|
+
</label>
|
|
158
|
+
<p class="field-hint">
|
|
159
|
+
账号空间里保存的是你的连接配置。推送是把本机连接上传到账号,拉取是把账号里的连接同步回本机。
|
|
160
|
+
</p>
|
|
161
|
+
<div class="sync-status-grid">
|
|
162
|
+
<article class="sync-status-card">
|
|
163
|
+
<span class="metric-label">登录状态</span>
|
|
164
|
+
<strong id="cloudAuthStatus">未登录</strong>
|
|
165
|
+
</article>
|
|
166
|
+
<article class="sync-status-card">
|
|
167
|
+
<span class="metric-label">服务器</span>
|
|
168
|
+
<strong id="cloudRemoteStatus">未设置</strong>
|
|
169
|
+
</article>
|
|
170
|
+
<article class="sync-status-card sync-status-card-wide">
|
|
171
|
+
<span class="metric-label">同步记录</span>
|
|
172
|
+
<strong id="cloudLastSyncStatus">还没有推送或拉取记录</strong>
|
|
173
|
+
</article>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="action-row">
|
|
176
|
+
<button id="cloudRegisterButton" type="button" class="secondary-button">
|
|
177
|
+
注册账号
|
|
178
|
+
</button>
|
|
179
|
+
<button id="cloudLoginButton" type="button" class="primary-button">
|
|
180
|
+
登录账号
|
|
181
|
+
</button>
|
|
182
|
+
<button id="cloudLogoutButton" type="button" class="ghost-button">退出登录</button>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="action-row">
|
|
185
|
+
<button id="cloudPushButton" type="button" class="secondary-button">
|
|
186
|
+
推送当前连接
|
|
187
|
+
</button>
|
|
188
|
+
<button id="cloudPullMergeButton" type="button" class="primary-button">
|
|
189
|
+
合并拉取
|
|
190
|
+
</button>
|
|
191
|
+
<button id="cloudPullReplaceButton" type="button" class="danger-button">
|
|
192
|
+
覆盖拉取
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
125
196
|
</section>
|
|
126
197
|
|
|
127
198
|
<section class="panel profiles-panel">
|
package/src/renderer/renderer.js
CHANGED
|
@@ -2,6 +2,7 @@ const state = {
|
|
|
2
2
|
current: null,
|
|
3
3
|
endpoints: [],
|
|
4
4
|
paths: null,
|
|
5
|
+
cloud: null,
|
|
5
6
|
};
|
|
6
7
|
|
|
7
8
|
function createWebBridge() {
|
|
@@ -56,6 +57,37 @@ function createWebBridge() {
|
|
|
56
57
|
method: "POST",
|
|
57
58
|
});
|
|
58
59
|
},
|
|
60
|
+
getCloudSyncStatus() {
|
|
61
|
+
return request("/api/cloud/status");
|
|
62
|
+
},
|
|
63
|
+
registerCloudAccount(payload) {
|
|
64
|
+
return request("/api/cloud/register", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify(payload),
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
loginCloudAccount(payload) {
|
|
70
|
+
return request("/api/cloud/login", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify(payload),
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
logoutCloudAccount() {
|
|
76
|
+
return request("/api/cloud/logout", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
pushCloudConfig() {
|
|
81
|
+
return request("/api/cloud/push", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
pullCloudConfig(payload) {
|
|
86
|
+
return request("/api/cloud/pull", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: JSON.stringify(payload),
|
|
89
|
+
});
|
|
90
|
+
},
|
|
59
91
|
getPaths() {
|
|
60
92
|
return request("/api/paths");
|
|
61
93
|
},
|
|
@@ -96,6 +128,19 @@ function setStatus(message, type = "info") {
|
|
|
96
128
|
statusBox.className = `status-box ${type}`;
|
|
97
129
|
}
|
|
98
130
|
|
|
131
|
+
function syncFieldValue(selector, value) {
|
|
132
|
+
const input = $(selector);
|
|
133
|
+
if (!input) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (document.activeElement === input && String(input.value || "").trim()) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
input.value = value || "";
|
|
142
|
+
}
|
|
143
|
+
|
|
99
144
|
function resetForm() {
|
|
100
145
|
$("#endpointId").value = "";
|
|
101
146
|
$("#endpointNote").value = "";
|
|
@@ -140,6 +185,49 @@ function renderCurrent() {
|
|
|
140
185
|
: "当前未匹配到已保存连接";
|
|
141
186
|
}
|
|
142
187
|
|
|
188
|
+
function renderCloudStatus() {
|
|
189
|
+
const cloud = state.cloud;
|
|
190
|
+
if (!cloud) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
syncFieldValue("#cloudServerUrl", cloud.serverUrl);
|
|
195
|
+
syncFieldValue("#cloudUsername", cloud.username);
|
|
196
|
+
|
|
197
|
+
const authText = !cloud.serverUrl
|
|
198
|
+
? "未配置同步服务器"
|
|
199
|
+
: cloud.loggedIn
|
|
200
|
+
? `已登录:${cloud.remoteUser || cloud.username}`
|
|
201
|
+
: cloud.hasToken
|
|
202
|
+
? "本地已保存登录态,但远端校验未通过"
|
|
203
|
+
: "未登录";
|
|
204
|
+
|
|
205
|
+
const remoteText = cloud.serverUrl
|
|
206
|
+
? cloud.lastError
|
|
207
|
+
? `${cloud.serverUrl} · ${cloud.lastError}`
|
|
208
|
+
: cloud.serverUrl
|
|
209
|
+
: "未设置";
|
|
210
|
+
|
|
211
|
+
const syncStatusParts = [];
|
|
212
|
+
if (cloud.lastPushAt) {
|
|
213
|
+
syncStatusParts.push(`最近推送:${formatDateTime(cloud.lastPushAt)}`);
|
|
214
|
+
}
|
|
215
|
+
if (cloud.lastPullAt) {
|
|
216
|
+
syncStatusParts.push(`最近拉取:${formatDateTime(cloud.lastPullAt)}`);
|
|
217
|
+
}
|
|
218
|
+
if (!syncStatusParts.length) {
|
|
219
|
+
syncStatusParts.push("还没有推送或拉取记录");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
$("#cloudAuthStatus").textContent = authText;
|
|
223
|
+
$("#cloudRemoteStatus").textContent = remoteText;
|
|
224
|
+
$("#cloudLastSyncStatus").textContent = syncStatusParts.join(" / ");
|
|
225
|
+
$("#cloudLogoutButton").disabled = !cloud.hasToken;
|
|
226
|
+
$("#cloudPushButton").disabled = !cloud.loggedIn;
|
|
227
|
+
$("#cloudPullMergeButton").disabled = !cloud.loggedIn;
|
|
228
|
+
$("#cloudPullReplaceButton").disabled = !cloud.loggedIn;
|
|
229
|
+
}
|
|
230
|
+
|
|
143
231
|
function createEndpointCard(endpoint) {
|
|
144
232
|
const card = document.createElement("article");
|
|
145
233
|
card.className = `profile-card ${endpoint.isActive ? "active" : ""}`;
|
|
@@ -230,18 +318,21 @@ function renderEndpoints() {
|
|
|
230
318
|
|
|
231
319
|
async function refreshDashboard(showMessage = true) {
|
|
232
320
|
try {
|
|
233
|
-
const [current, endpoints, paths] = await Promise.all([
|
|
321
|
+
const [current, endpoints, paths, cloud] = await Promise.all([
|
|
234
322
|
bridge.getCurrentConfig(),
|
|
235
323
|
bridge.listEndpoints(),
|
|
236
324
|
bridge.getPaths(),
|
|
325
|
+
bridge.getCloudSyncStatus(),
|
|
237
326
|
]);
|
|
238
327
|
|
|
239
328
|
state.current = current;
|
|
240
329
|
state.endpoints = endpoints;
|
|
241
330
|
state.paths = paths;
|
|
331
|
+
state.cloud = cloud;
|
|
242
332
|
|
|
243
333
|
renderCurrent();
|
|
244
334
|
renderEndpoints();
|
|
335
|
+
renderCloudStatus();
|
|
245
336
|
|
|
246
337
|
if (showMessage) {
|
|
247
338
|
setStatus("已刷新当前连接状态。", "success");
|
|
@@ -289,6 +380,91 @@ async function handleEnableProxyMode() {
|
|
|
289
380
|
}
|
|
290
381
|
}
|
|
291
382
|
|
|
383
|
+
function getCloudFormPayload() {
|
|
384
|
+
return {
|
|
385
|
+
serverUrl: $("#cloudServerUrl").value.trim(),
|
|
386
|
+
username: $("#cloudUsername").value.trim(),
|
|
387
|
+
password: $("#cloudPassword").value,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function handleCloudRegister() {
|
|
392
|
+
try {
|
|
393
|
+
setStatus("正在注册同步账号 ...", "info");
|
|
394
|
+
const payload = getCloudFormPayload();
|
|
395
|
+
const result = await bridge.registerCloudAccount(payload);
|
|
396
|
+
$("#cloudPassword").value = "";
|
|
397
|
+
await refreshDashboard(false);
|
|
398
|
+
setStatus(result.message || "账号注册成功。", "success");
|
|
399
|
+
} catch (error) {
|
|
400
|
+
setStatus(`注册失败:${error.message}`, "error");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function handleCloudLogin() {
|
|
405
|
+
try {
|
|
406
|
+
setStatus("正在登录同步账号 ...", "info");
|
|
407
|
+
const payload = getCloudFormPayload();
|
|
408
|
+
const result = await bridge.loginCloudAccount(payload);
|
|
409
|
+
$("#cloudPassword").value = "";
|
|
410
|
+
await refreshDashboard(false);
|
|
411
|
+
setStatus(result.message || "账号登录成功。", "success");
|
|
412
|
+
} catch (error) {
|
|
413
|
+
setStatus(`登录失败:${error.message}`, "error");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function handleCloudLogout() {
|
|
418
|
+
try {
|
|
419
|
+
setStatus("正在退出同步账号 ...", "info");
|
|
420
|
+
const result = await bridge.logoutCloudAccount();
|
|
421
|
+
await refreshDashboard(false);
|
|
422
|
+
setStatus(result.message || "已退出同步账号。", "success");
|
|
423
|
+
} catch (error) {
|
|
424
|
+
setStatus(`退出失败:${error.message}`, "error");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function handleCloudPush() {
|
|
429
|
+
try {
|
|
430
|
+
setStatus("正在把当前连接推送到账号空间 ...", "info");
|
|
431
|
+
const result = await bridge.pushCloudConfig();
|
|
432
|
+
await refreshDashboard(false);
|
|
433
|
+
setStatus(
|
|
434
|
+
result.message || `已推送 ${result.endpointCount || 0} 条连接到账号空间。`,
|
|
435
|
+
"success",
|
|
436
|
+
);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
setStatus(`推送失败:${error.message}`, "error");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function handleCloudPull(mode) {
|
|
443
|
+
if (mode === "replace") {
|
|
444
|
+
const confirmed = window.confirm(
|
|
445
|
+
"覆盖拉取会直接替换本机保存的连接列表。拉取前会自动备份,确定继续吗?",
|
|
446
|
+
);
|
|
447
|
+
if (!confirmed) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
setStatus(mode === "merge" ? "正在从账号空间合并拉取 ..." : "正在从账号空间覆盖拉取 ...", "info");
|
|
454
|
+
const result = await bridge.pullCloudConfig({ mode });
|
|
455
|
+
await refreshDashboard(false);
|
|
456
|
+
const activeHint = result.activeEndpointNote
|
|
457
|
+
? `当前生效连接记录:${result.activeEndpointNote}。`
|
|
458
|
+
: "当前没有匹配到生效连接记录。";
|
|
459
|
+
setStatus(
|
|
460
|
+
`${result.message || "拉取完成。"} 新增 ${result.addedCount} 条,更新 ${result.updatedCount} 条。${activeHint}`,
|
|
461
|
+
"success",
|
|
462
|
+
);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
setStatus(`拉取失败:${error.message}`, "error");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
292
468
|
async function handleSubmitEndpoint(event) {
|
|
293
469
|
event.preventDefault();
|
|
294
470
|
const id = $("#endpointId").value.trim();
|
|
@@ -358,6 +534,16 @@ function bindEvents() {
|
|
|
358
534
|
setStatus("已清空表单。", "info");
|
|
359
535
|
});
|
|
360
536
|
$("#enableProxyModeButton").addEventListener("click", handleEnableProxyMode);
|
|
537
|
+
$("#cloudRegisterButton").addEventListener("click", handleCloudRegister);
|
|
538
|
+
$("#cloudLoginButton").addEventListener("click", handleCloudLogin);
|
|
539
|
+
$("#cloudLogoutButton").addEventListener("click", handleCloudLogout);
|
|
540
|
+
$("#cloudPushButton").addEventListener("click", handleCloudPush);
|
|
541
|
+
$("#cloudPullMergeButton").addEventListener("click", () => {
|
|
542
|
+
handleCloudPull("merge");
|
|
543
|
+
});
|
|
544
|
+
$("#cloudPullReplaceButton").addEventListener("click", () => {
|
|
545
|
+
handleCloudPull("replace");
|
|
546
|
+
});
|
|
361
547
|
$("#openCodexRootButton").addEventListener("click", () => {
|
|
362
548
|
if (state.paths?.codexRoot) {
|
|
363
549
|
handleOpenPath(state.paths.codexRoot, " Codex 目录");
|
package/src/renderer/styles.css
CHANGED
|
@@ -33,7 +33,8 @@ body {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
button,
|
|
36
|
-
input
|
|
36
|
+
input,
|
|
37
|
+
textarea {
|
|
37
38
|
font: inherit;
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -259,7 +260,8 @@ code {
|
|
|
259
260
|
|
|
260
261
|
input[type="text"],
|
|
261
262
|
input[type="password"],
|
|
262
|
-
input[type="file"]
|
|
263
|
+
input[type="file"],
|
|
264
|
+
textarea {
|
|
263
265
|
width: 100%;
|
|
264
266
|
min-height: 46px;
|
|
265
267
|
padding: 10px 14px;
|
|
@@ -269,6 +271,12 @@ input[type="file"] {
|
|
|
269
271
|
color: var(--text);
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
textarea {
|
|
275
|
+
min-height: 168px;
|
|
276
|
+
resize: vertical;
|
|
277
|
+
line-height: 1.6;
|
|
278
|
+
}
|
|
279
|
+
|
|
272
280
|
input[readonly] {
|
|
273
281
|
color: var(--muted);
|
|
274
282
|
background: rgba(249, 244, 238, 0.95);
|
|
@@ -300,6 +308,29 @@ input[readonly] {
|
|
|
300
308
|
gap: 10px;
|
|
301
309
|
}
|
|
302
310
|
|
|
311
|
+
.sync-status-grid {
|
|
312
|
+
display: grid;
|
|
313
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
314
|
+
gap: 10px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.sync-status-card {
|
|
318
|
+
padding: 14px;
|
|
319
|
+
border-radius: 14px;
|
|
320
|
+
background: rgba(255, 255, 255, 0.72);
|
|
321
|
+
border: 1px solid rgba(47, 36, 30, 0.08);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.sync-status-card strong {
|
|
325
|
+
display: block;
|
|
326
|
+
margin-top: 8px;
|
|
327
|
+
word-break: break-all;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.sync-status-card-wide {
|
|
331
|
+
grid-column: span 2;
|
|
332
|
+
}
|
|
333
|
+
|
|
303
334
|
.metric-actions {
|
|
304
335
|
margin-top: 12px;
|
|
305
336
|
}
|
|
@@ -508,4 +539,12 @@ input[readonly] {
|
|
|
508
539
|
.hero-side {
|
|
509
540
|
min-height: 180px;
|
|
510
541
|
}
|
|
542
|
+
|
|
543
|
+
.sync-status-grid {
|
|
544
|
+
grid-template-columns: 1fr;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.sync-status-card-wide {
|
|
548
|
+
grid-column: auto;
|
|
549
|
+
}
|
|
511
550
|
}
|