codex-endpoint-switcher 1.0.0 → 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 +103 -0
- package/bin/codex-switcher.js +27 -0
- package/package.json +6 -1
- package/src/main/cloud-sync-client.js +328 -0
- package/src/main/main.js +37 -0
- package/src/main/preload.js +27 -0
- package/src/main/profile-manager.js +478 -42
- package/src/renderer/index.html +83 -0
- package/src/renderer/renderer.js +230 -6
- package/src/renderer/styles.css +45 -2
- package/src/web/cloud-sync-server.js +419 -0
- package/src/web/launcher.js +6 -1
- package/src/web/proxy-server.js +69 -0
- package/src/web/server.js +100 -2
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ codex-switcher open
|
|
|
48
48
|
codex-switcher start
|
|
49
49
|
codex-switcher status
|
|
50
50
|
codex-switcher restart
|
|
51
|
+
codex-switcher sync-server
|
|
51
52
|
codex-switcher install-access
|
|
52
53
|
codex-switcher remove-access
|
|
53
54
|
```
|
|
@@ -58,6 +59,7 @@ codex-switcher remove-access
|
|
|
58
59
|
- `start`:仅在后台启动本地网页服务
|
|
59
60
|
- `status`:查看当前服务状态
|
|
60
61
|
- `restart`:重启本地网页服务
|
|
62
|
+
- `sync-server`:启动账号同步服务,服务端使用 SQLite 单文件数据库
|
|
61
63
|
- `install-access`:创建桌面快捷方式和开机启动项
|
|
62
64
|
- `remove-access`:移除桌面快捷方式和开机启动项
|
|
63
65
|
|
|
@@ -68,3 +70,104 @@ codex-switcher remove-access
|
|
|
68
70
|
```text
|
|
69
71
|
http://localhost:3186
|
|
70
72
|
```
|
|
73
|
+
|
|
74
|
+
## 账号配置同步
|
|
75
|
+
|
|
76
|
+
网页控制台现在支持“服务器地址 + 账号 + 密码”的同步方式。
|
|
77
|
+
|
|
78
|
+
### 1. 启动同步服务
|
|
79
|
+
|
|
80
|
+
在准备当服务端的机器上执行:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
codex-switcher sync-server
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
或者:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm run start:sync-server
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
默认会监听:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
http://127.0.0.1:3190
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
默认数据库文件:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
~/.codex-cloud-sync/cloud-sync.db
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
如果你要让局域网或公网机器访问,可以先设置:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
set SYNC_SERVER_HOST=0.0.0.0
|
|
108
|
+
set SYNC_SERVER_PORT=3190
|
|
109
|
+
codex-switcher sync-server
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 2. 在网页控制台登录同步账号
|
|
113
|
+
|
|
114
|
+
在客户端网页控制台里填写:
|
|
115
|
+
|
|
116
|
+
- 同步服务器地址
|
|
117
|
+
- 账号
|
|
118
|
+
- 密码
|
|
119
|
+
|
|
120
|
+
然后:
|
|
121
|
+
|
|
122
|
+
- 第一次使用点 `注册账号`
|
|
123
|
+
- 后续使用点 `登录账号`
|
|
124
|
+
|
|
125
|
+
### 3. 推送和拉取
|
|
126
|
+
|
|
127
|
+
- `推送当前连接`:把本机保存的 URL / Key / 备注 上传到账号空间
|
|
128
|
+
- `合并拉取`:把账号空间里的连接合并到本机
|
|
129
|
+
- `覆盖拉取`:直接用账号空间里的连接替换本机连接列表
|
|
130
|
+
|
|
131
|
+
说明:
|
|
132
|
+
|
|
133
|
+
- 拉取前会自动备份本机 `~/.codex/endpoint-presets.json` 和 `~/.codex/endpoint-switcher-state.json`
|
|
134
|
+
- 服务端数据库是 SQLite,部署只需要一个 Node 进程和一个 `.db` 文件
|
|
135
|
+
|
|
136
|
+
## 更新 npm 包
|
|
137
|
+
|
|
138
|
+
先进入项目目录:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd C:\Users\33825\Desktop\codex-profile-desktop
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
发布前先检查这次会打进 npm 包里的文件:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm run release:check
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
如果只是小修复,直接发补丁版本:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm run release:patch
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
如果是新增功能但兼容旧版:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm run release:minor
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
如果有破坏性改动:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npm run release:major
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
说明:
|
|
169
|
+
|
|
170
|
+
- `release:patch` 会自动把版本从 `1.0.0` 升到 `1.0.1` 这一类补丁版本,然后执行 `npm publish`
|
|
171
|
+
- `release:minor` 会自动把版本从 `1.0.0` 升到 `1.1.0` 这一类功能版本,然后执行 `npm publish`
|
|
172
|
+
- `release:major` 会自动把版本从 `1.0.0` 升到 `2.0.0` 这一类大版本,然后执行 `npm publish`
|
|
173
|
+
- 如果 npm 开启了 2FA,发布时仍然会要求输入验证码
|
package/bin/codex-switcher.js
CHANGED
|
@@ -7,6 +7,7 @@ const { handleCommand } = require("../src/web/launcher");
|
|
|
7
7
|
const projectRoot = path.resolve(__dirname, "..");
|
|
8
8
|
const installScriptPath = path.join(projectRoot, "install-web-access.ps1");
|
|
9
9
|
const removeScriptPath = path.join(projectRoot, "remove-web-access.ps1");
|
|
10
|
+
const cloudSyncServerPath = path.join(projectRoot, "src/web/cloud-sync-server.js");
|
|
10
11
|
|
|
11
12
|
function printUsage() {
|
|
12
13
|
console.log(`
|
|
@@ -15,6 +16,7 @@ function printUsage() {
|
|
|
15
16
|
codex-switcher start
|
|
16
17
|
codex-switcher status
|
|
17
18
|
codex-switcher restart
|
|
19
|
+
codex-switcher sync-server
|
|
18
20
|
codex-switcher install-access
|
|
19
21
|
codex-switcher remove-access
|
|
20
22
|
|
|
@@ -23,6 +25,7 @@ function printUsage() {
|
|
|
23
25
|
start 仅在后台启动本地网页服务
|
|
24
26
|
status 查看服务状态
|
|
25
27
|
restart 重启本地网页服务
|
|
28
|
+
sync-server 启动账号同步服务(SQLite)
|
|
26
29
|
install-access 创建桌面快捷方式与开机启动项
|
|
27
30
|
remove-access 删除桌面快捷方式与开机启动项
|
|
28
31
|
`);
|
|
@@ -52,6 +55,26 @@ function runPowerShellScript(scriptPath) {
|
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
function runNodeScript(scriptPath, extraArgs = []) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const child = spawn(process.execPath, [...extraArgs, scriptPath], {
|
|
61
|
+
cwd: projectRoot,
|
|
62
|
+
stdio: "inherit",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
child.on("exit", (code) => {
|
|
66
|
+
if (code === 0) {
|
|
67
|
+
resolve();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
reject(new Error(`脚本执行失败,退出码:${code}`));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
child.on("error", reject);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
async function main() {
|
|
56
79
|
const command = process.argv[2] || "open";
|
|
57
80
|
|
|
@@ -63,6 +86,10 @@ async function main() {
|
|
|
63
86
|
await handleCommand(command === "start" ? "ensure" : command);
|
|
64
87
|
return;
|
|
65
88
|
}
|
|
89
|
+
case "sync-server": {
|
|
90
|
+
await runNodeScript(cloudSyncServerPath, ["--experimental-sqlite"]);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
66
93
|
case "install-access": {
|
|
67
94
|
await runPowerShellScript(installScriptPath);
|
|
68
95
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-endpoint-switcher",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "用于切换 Codex URL 和 Key 的本地网页控制台与 npm CLI",
|
|
5
5
|
"main": "src/main/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,13 @@
|
|
|
21
21
|
"dev": "electron .",
|
|
22
22
|
"start:web": "node src/web/server.js",
|
|
23
23
|
"dev:web": "node src/web/server.js",
|
|
24
|
+
"start:sync-server": "node --experimental-sqlite src/web/cloud-sync-server.js",
|
|
24
25
|
"cli": "node bin/codex-switcher.js",
|
|
25
26
|
"pack:npm": "npm pack",
|
|
27
|
+
"release:check": "npm pack --dry-run",
|
|
28
|
+
"release:patch": "npm version patch --no-git-tag-version && npm publish",
|
|
29
|
+
"release:minor": "npm version minor --no-git-tag-version && npm publish",
|
|
30
|
+
"release:major": "npm version major --no-git-tag-version && npm publish",
|
|
26
31
|
"build:portable": "electron-builder --win portable",
|
|
27
32
|
"build:installer": "electron-builder --win nsis"
|
|
28
33
|
},
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const profileManager = require("./profile-manager");
|
|
5
|
+
|
|
6
|
+
const codexRoot = path.join(os.homedir(), ".codex");
|
|
7
|
+
const cloudSyncConfigPath = path.join(codexRoot, "cloud-sync-config.json");
|
|
8
|
+
|
|
9
|
+
function normalizeServerUrl(value) {
|
|
10
|
+
const raw = String(value || "").trim();
|
|
11
|
+
if (!raw) {
|
|
12
|
+
throw new Error("同步服务器地址不能为空。");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let parsedUrl;
|
|
16
|
+
try {
|
|
17
|
+
parsedUrl = new URL(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error("同步服务器地址格式不正确。");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
23
|
+
throw new Error("同步服务器地址仅支持 http 或 https。");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
parsedUrl.hash = "";
|
|
27
|
+
parsedUrl.search = "";
|
|
28
|
+
|
|
29
|
+
return parsedUrl.toString().replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeUsername(value) {
|
|
33
|
+
const username = String(value || "").trim();
|
|
34
|
+
if (!username) {
|
|
35
|
+
throw new Error("账号不能为空。");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (username.length < 3 || username.length > 64) {
|
|
39
|
+
throw new Error("账号长度需要在 3 到 64 个字符之间。");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (/\s/.test(username)) {
|
|
43
|
+
throw new Error("账号不能包含空白字符。");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return username;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizePassword(value) {
|
|
50
|
+
const password = String(value || "");
|
|
51
|
+
if (!password.trim()) {
|
|
52
|
+
throw new Error("密码不能为空。");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (password.length < 6) {
|
|
56
|
+
throw new Error("密码至少需要 6 位。");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return password;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function ensureStorage() {
|
|
63
|
+
await fs.mkdir(codexRoot, { recursive: true });
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(cloudSyncConfigPath);
|
|
67
|
+
} catch {
|
|
68
|
+
await fs.writeFile(
|
|
69
|
+
cloudSyncConfigPath,
|
|
70
|
+
`${JSON.stringify(
|
|
71
|
+
{
|
|
72
|
+
serverUrl: "",
|
|
73
|
+
username: "",
|
|
74
|
+
authToken: "",
|
|
75
|
+
tokenExpiresAt: "",
|
|
76
|
+
lastPushAt: "",
|
|
77
|
+
lastPullAt: "",
|
|
78
|
+
},
|
|
79
|
+
null,
|
|
80
|
+
2,
|
|
81
|
+
)}\n`,
|
|
82
|
+
"utf8",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readConfig() {
|
|
88
|
+
await ensureStorage();
|
|
89
|
+
const content = await fs.readFile(cloudSyncConfigPath, "utf8");
|
|
90
|
+
const config = JSON.parse(content);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
serverUrl: String(config.serverUrl || "").trim(),
|
|
94
|
+
username: String(config.username || "").trim(),
|
|
95
|
+
authToken: String(config.authToken || "").trim(),
|
|
96
|
+
tokenExpiresAt: String(config.tokenExpiresAt || "").trim(),
|
|
97
|
+
lastPushAt: String(config.lastPushAt || "").trim(),
|
|
98
|
+
lastPullAt: String(config.lastPullAt || "").trim(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function writeConfig(config) {
|
|
103
|
+
await ensureStorage();
|
|
104
|
+
await fs.writeFile(cloudSyncConfigPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function requestJson(serverUrl, pathname, options = {}) {
|
|
108
|
+
const targetUrl = new URL(pathname, `${serverUrl}/`);
|
|
109
|
+
const response = await fetch(targetUrl, {
|
|
110
|
+
method: options.method || "GET",
|
|
111
|
+
headers: {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
...(options.authToken
|
|
114
|
+
? {
|
|
115
|
+
Authorization: `Bearer ${options.authToken}`,
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
118
|
+
},
|
|
119
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
120
|
+
signal: AbortSignal.timeout(options.timeoutMs || 5000),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const rawText = await response.text();
|
|
124
|
+
let payload = {};
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
payload = rawText ? JSON.parse(rawText) : {};
|
|
128
|
+
} catch {
|
|
129
|
+
payload = {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!response.ok || payload.ok === false) {
|
|
133
|
+
throw new Error(payload.error || `请求同步服务失败:${response.status}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return payload.data;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function getCloudSyncStatus() {
|
|
140
|
+
const config = await readConfig();
|
|
141
|
+
const result = {
|
|
142
|
+
configPath: cloudSyncConfigPath,
|
|
143
|
+
serverUrl: config.serverUrl,
|
|
144
|
+
username: config.username,
|
|
145
|
+
hasToken: Boolean(config.authToken),
|
|
146
|
+
loggedIn: false,
|
|
147
|
+
tokenExpiresAt: config.tokenExpiresAt,
|
|
148
|
+
lastPushAt: config.lastPushAt,
|
|
149
|
+
lastPullAt: config.lastPullAt,
|
|
150
|
+
remoteUser: "",
|
|
151
|
+
remoteProfileUpdatedAt: "",
|
|
152
|
+
lastError: "",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (!config.serverUrl || !config.authToken) {
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const data = await requestJson(config.serverUrl, "/api/auth/me", {
|
|
161
|
+
authToken: config.authToken,
|
|
162
|
+
timeoutMs: 3000,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...result,
|
|
167
|
+
loggedIn: true,
|
|
168
|
+
username: data.user.username,
|
|
169
|
+
remoteUser: data.user.username,
|
|
170
|
+
tokenExpiresAt: data.session.expiresAt,
|
|
171
|
+
remoteProfileUpdatedAt: data.syncProfileUpdatedAt || "",
|
|
172
|
+
lastError: "",
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
...result,
|
|
177
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function authenticate(mode, payload) {
|
|
183
|
+
const serverUrl = normalizeServerUrl(payload.serverUrl);
|
|
184
|
+
const username = normalizeUsername(payload.username);
|
|
185
|
+
const password = normalizePassword(payload.password);
|
|
186
|
+
const endpointPath = mode === "register" ? "/api/auth/register" : "/api/auth/login";
|
|
187
|
+
const response = await requestJson(serverUrl, endpointPath, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
body: {
|
|
190
|
+
username,
|
|
191
|
+
password,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await writeConfig({
|
|
196
|
+
serverUrl,
|
|
197
|
+
username: response.user.username,
|
|
198
|
+
authToken: String(response.token || "").trim(),
|
|
199
|
+
tokenExpiresAt: String(response.expiresAt || "").trim(),
|
|
200
|
+
lastPushAt: "",
|
|
201
|
+
lastPullAt: "",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
...(await getCloudSyncStatus()),
|
|
206
|
+
message: mode === "register" ? "账号注册并登录成功。" : "账号登录成功。",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function registerAccount(payload) {
|
|
211
|
+
return authenticate("register", payload);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function loginAccount(payload) {
|
|
215
|
+
return authenticate("login", payload);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function logoutAccount() {
|
|
219
|
+
const config = await readConfig();
|
|
220
|
+
|
|
221
|
+
if (config.serverUrl && config.authToken) {
|
|
222
|
+
try {
|
|
223
|
+
await requestJson(config.serverUrl, "/api/auth/logout", {
|
|
224
|
+
method: "POST",
|
|
225
|
+
authToken: config.authToken,
|
|
226
|
+
timeoutMs: 3000,
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
// 服务端不可达时,本地仍然允许清理登录态,避免用户被卡住。
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await writeConfig({
|
|
234
|
+
...config,
|
|
235
|
+
authToken: "",
|
|
236
|
+
tokenExpiresAt: "",
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
...(await getCloudSyncStatus()),
|
|
241
|
+
message: "已退出账号同步登录。",
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function requireLoggedInConfig() {
|
|
246
|
+
const config = await readConfig();
|
|
247
|
+
if (!config.serverUrl) {
|
|
248
|
+
throw new Error("请先填写并登录同步服务器。");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!config.authToken) {
|
|
252
|
+
throw new Error("当前还没有登录同步账号。");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return config;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 把当前本机保存的连接配置推送到远端账号空间。
|
|
260
|
+
*/
|
|
261
|
+
async function pushCurrentConfig() {
|
|
262
|
+
const config = await requireLoggedInConfig();
|
|
263
|
+
const exported = await profileManager.exportSyncPackage();
|
|
264
|
+
const response = await requestJson(config.serverUrl, "/api/sync/config", {
|
|
265
|
+
method: "PUT",
|
|
266
|
+
authToken: config.authToken,
|
|
267
|
+
body: {
|
|
268
|
+
syncCode: exported.syncCode,
|
|
269
|
+
endpointCount: exported.endpointCount,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const nextConfig = {
|
|
274
|
+
...config,
|
|
275
|
+
lastPushAt: String(response.updatedAt || new Date().toISOString()).trim(),
|
|
276
|
+
};
|
|
277
|
+
await writeConfig(nextConfig);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
...(await getCloudSyncStatus()),
|
|
281
|
+
endpointCount: exported.endpointCount,
|
|
282
|
+
remoteUpdatedAt: response.updatedAt,
|
|
283
|
+
message: `已把 ${exported.endpointCount} 条连接推送到账号空间。`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 从远端账号空间拉取连接配置,再按指定策略导入到本机。
|
|
289
|
+
*/
|
|
290
|
+
async function pullRemoteConfig(mode = "merge") {
|
|
291
|
+
const safeMode = String(mode || "merge").trim().toLowerCase();
|
|
292
|
+
if (!["merge", "replace"].includes(safeMode)) {
|
|
293
|
+
throw new Error("拉取模式不正确,只支持 merge 或 replace。");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const config = await requireLoggedInConfig();
|
|
297
|
+
const response = await requestJson(config.serverUrl, "/api/sync/config", {
|
|
298
|
+
method: "GET",
|
|
299
|
+
authToken: config.authToken,
|
|
300
|
+
});
|
|
301
|
+
const importResult = await profileManager.importSyncPackage(response.syncCode, safeMode);
|
|
302
|
+
const nextConfig = {
|
|
303
|
+
...config,
|
|
304
|
+
lastPullAt: new Date().toISOString(),
|
|
305
|
+
};
|
|
306
|
+
await writeConfig(nextConfig);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
...(await getCloudSyncStatus()),
|
|
310
|
+
...importResult,
|
|
311
|
+
remoteUpdatedAt: response.updatedAt,
|
|
312
|
+
endpointCount: response.endpointCount,
|
|
313
|
+
message:
|
|
314
|
+
safeMode === "merge"
|
|
315
|
+
? "已从账号空间拉取并合并到本机。"
|
|
316
|
+
: "已从账号空间拉取并覆盖本机连接列表。",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = {
|
|
321
|
+
cloudSyncConfigPath,
|
|
322
|
+
getCloudSyncStatus,
|
|
323
|
+
loginAccount,
|
|
324
|
+
logoutAccount,
|
|
325
|
+
pullRemoteConfig,
|
|
326
|
+
pushCurrentConfig,
|
|
327
|
+
registerAccount,
|
|
328
|
+
};
|
package/src/main/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require("node:path");
|
|
2
2
|
const { app, BrowserWindow, ipcMain, shell } = require("electron");
|
|
3
3
|
const profileManager = require("./profile-manager");
|
|
4
|
+
const cloudSyncClient = require("./cloud-sync-client");
|
|
4
5
|
|
|
5
6
|
function createWindow() {
|
|
6
7
|
const mainWindow = new BrowserWindow({
|
|
@@ -46,6 +47,42 @@ function registerHandlers() {
|
|
|
46
47
|
return profileManager.switchEndpoint(payload.id);
|
|
47
48
|
});
|
|
48
49
|
|
|
50
|
+
ipcMain.handle("proxy:enable", async () => {
|
|
51
|
+
return profileManager.enableProxyMode();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
ipcMain.handle("sync:export", async () => {
|
|
55
|
+
return profileManager.exportSyncPackage();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
ipcMain.handle("sync:import", async (_event, payload) => {
|
|
59
|
+
return profileManager.importSyncPackage(payload.syncCode, payload.mode);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
ipcMain.handle("cloud:status", async () => {
|
|
63
|
+
return cloudSyncClient.getCloudSyncStatus();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
ipcMain.handle("cloud:register", async (_event, payload) => {
|
|
67
|
+
return cloudSyncClient.registerAccount(payload);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
ipcMain.handle("cloud:login", async (_event, payload) => {
|
|
71
|
+
return cloudSyncClient.loginAccount(payload);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
ipcMain.handle("cloud:logout", async () => {
|
|
75
|
+
return cloudSyncClient.logoutAccount();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
ipcMain.handle("cloud:push", async () => {
|
|
79
|
+
return cloudSyncClient.pushCurrentConfig();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
ipcMain.handle("cloud:pull", async (_event, payload) => {
|
|
83
|
+
return cloudSyncClient.pullRemoteConfig(payload.mode);
|
|
84
|
+
});
|
|
85
|
+
|
|
49
86
|
ipcMain.handle("paths:get", async () => {
|
|
50
87
|
return profileManager.getManagedPaths();
|
|
51
88
|
});
|
package/src/main/preload.js
CHANGED
|
@@ -19,6 +19,33 @@ contextBridge.exposeInMainWorld("codexDesktop", {
|
|
|
19
19
|
switchEndpoint(payload) {
|
|
20
20
|
return ipcRenderer.invoke("endpoints:switch", payload);
|
|
21
21
|
},
|
|
22
|
+
enableProxyMode() {
|
|
23
|
+
return ipcRenderer.invoke("proxy:enable");
|
|
24
|
+
},
|
|
25
|
+
exportSyncCode() {
|
|
26
|
+
return ipcRenderer.invoke("sync:export");
|
|
27
|
+
},
|
|
28
|
+
importSyncCode(payload) {
|
|
29
|
+
return ipcRenderer.invoke("sync:import", payload);
|
|
30
|
+
},
|
|
31
|
+
getCloudSyncStatus() {
|
|
32
|
+
return ipcRenderer.invoke("cloud:status");
|
|
33
|
+
},
|
|
34
|
+
registerCloudAccount(payload) {
|
|
35
|
+
return ipcRenderer.invoke("cloud:register", payload);
|
|
36
|
+
},
|
|
37
|
+
loginCloudAccount(payload) {
|
|
38
|
+
return ipcRenderer.invoke("cloud:login", payload);
|
|
39
|
+
},
|
|
40
|
+
logoutCloudAccount() {
|
|
41
|
+
return ipcRenderer.invoke("cloud:logout");
|
|
42
|
+
},
|
|
43
|
+
pushCloudConfig() {
|
|
44
|
+
return ipcRenderer.invoke("cloud:push");
|
|
45
|
+
},
|
|
46
|
+
pullCloudConfig(payload) {
|
|
47
|
+
return ipcRenderer.invoke("cloud:pull", payload);
|
|
48
|
+
},
|
|
22
49
|
getPaths() {
|
|
23
50
|
return ipcRenderer.invoke("paths:get");
|
|
24
51
|
},
|