codefree-proxy 0.1.2 → 0.1.4
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 +56 -75
- package/dist/auth/auth-manager.js +107 -27
- package/dist/auth/codefree-o.js +109 -12
- package/dist/auth/config-store.js +13 -3
- package/dist/cli.d.ts +33 -1
- package/dist/cli.js +347 -32
- package/dist/cli.js.map +1 -1
- package/dist/proxy/image-sanitize.js +126 -0
- package/dist/proxy/model-map.js +58 -4
- package/dist/proxy/openai-routes.js +524 -26
- package/dist/proxy/upstream-client.js +173 -5
- package/dist/proxy/upstream-retry.js +97 -0
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/auth/browser-login.d.ts +0 -30
- package/dist/auth/browser-login.js +0 -181
- package/dist/auth/browser-login.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,109 +1,90 @@
|
|
|
1
1
|
# codefree-proxy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把本机 **CodeFree-O** 的登录态转换成一个本地 OpenAI 兼容接口,任何支持 OpenAI API 的客户端(Cherry Studio、Open WebUI、Continue、opencode 等)都能直接用研发云的模型。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
只监听 `127.0.0.1`,不需要填 API Key。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 前置条件
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Or install globally:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install -g codefree-proxy
|
|
17
|
-
codefree-proxy --port 8788 --host 127.0.0.1
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
The server prints the local base URL and endpoints after it starts.
|
|
21
|
-
|
|
22
|
-
```text
|
|
23
|
-
Server running at: http://127.0.0.1:8788
|
|
24
|
-
OpenAI Base URL: http://127.0.0.1:8788/v1
|
|
25
|
-
Chat endpoint: POST http://127.0.0.1:8788/v1/chat/completions
|
|
26
|
-
Models endpoint: GET http://127.0.0.1:8788/v1/models
|
|
27
|
-
Health check: GET http://127.0.0.1:8788/health
|
|
28
|
-
Stop command: codefree-proxy stop --port 8788
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Use this OpenAI-compatible base URL in clients:
|
|
32
|
-
|
|
33
|
-
```text
|
|
34
|
-
http://127.0.0.1:8788/v1
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Use any API key value in OpenAI-compatible clients, or omit it if the client allows.
|
|
9
|
+
1. **Node.js >= 18**(`node -v` 检查)
|
|
10
|
+
2. **本机已安装并登录 CodeFree-O**:先跑一次 `codefree-o login`,确认能正常对话
|
|
38
11
|
|
|
39
|
-
|
|
12
|
+
代理不保存密码,也不需要你提供任何密钥:它读取 CodeFree-O 在本机的登录态(`~/.codefree-o/.../codefree.json`,AES-256-GCM 加密),用其中的 refresh token 换取访问令牌。因此 **CodeFree-O 必须先登录过**。
|
|
40
13
|
|
|
41
|
-
|
|
14
|
+
## 快速开始
|
|
42
15
|
|
|
43
16
|
```bash
|
|
44
|
-
npx codefree-proxy
|
|
17
|
+
npx codefree-proxy doctor # 1. 自检:Node、登录态、端口、网络
|
|
18
|
+
npx codefree-proxy start # 2. 后台启动
|
|
45
19
|
```
|
|
46
20
|
|
|
47
|
-
|
|
21
|
+
启动后把客户端指向:
|
|
48
22
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
npx codefree-proxy status
|
|
23
|
+
```text
|
|
24
|
+
Base URL: http://127.0.0.1:8788/v1
|
|
25
|
+
API Key: 随意填或留空
|
|
53
26
|
```
|
|
54
27
|
|
|
55
|
-
|
|
28
|
+
停止与查看:
|
|
56
29
|
|
|
57
30
|
```bash
|
|
58
|
-
npx codefree-proxy
|
|
31
|
+
npx codefree-proxy status # 登录态(只读,不会触发登录)
|
|
32
|
+
npx codefree-proxy stop # 停止 8788 上的代理
|
|
59
33
|
```
|
|
60
34
|
|
|
61
|
-
##
|
|
35
|
+
## 可用模型
|
|
62
36
|
|
|
63
|
-
|
|
37
|
+
- `DeepSeek-V4-Flash-0731`
|
|
38
|
+
- `DeepSeek-V4.1-Flash-1017`(支持图片)
|
|
39
|
+
- `GLM-5.2`
|
|
40
|
+
- `GLM-5.3`
|
|
41
|
+
- `Qwen3.8-27B`(支持图片)
|
|
64
42
|
|
|
65
|
-
|
|
66
|
-
npx codefree-proxy stop
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Custom port:
|
|
43
|
+
写错模型名会直接返回 400 并列出可用模型,不会悄悄换成别的模型。
|
|
70
44
|
|
|
71
|
-
|
|
72
|
-
npx codefree-proxy stop --port 8788
|
|
73
|
-
```
|
|
45
|
+
## 命令
|
|
74
46
|
|
|
75
|
-
|
|
47
|
+
| 命令 | 说明 |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `doctor` | 环境自检,输出 ✓/✗ 与结论 |
|
|
50
|
+
| `serve` | 前台启动,日志打在终端 |
|
|
51
|
+
| `start` | 后台启动,并确认端口真的监听成功 |
|
|
52
|
+
| `login` | 从 CodeFree-O 导入登录态 |
|
|
53
|
+
| `status` | 查看登录态(不写文件、不联网) |
|
|
54
|
+
| `logout` | 删除本代理保存的登录态 |
|
|
55
|
+
| `stop` | 停止代理(只停自己,端口被别的程序占用会拒绝) |
|
|
76
56
|
|
|
77
|
-
|
|
78
|
-
codefree-proxy --port 8788 --host 127.0.0.1
|
|
79
|
-
codefree-proxy login
|
|
80
|
-
codefree-proxy logout
|
|
81
|
-
codefree-proxy status
|
|
82
|
-
codefree-proxy stop
|
|
83
|
-
codefree-proxy stop --port 8788
|
|
84
|
-
```
|
|
57
|
+
通用选项:`--host`(默认 127.0.0.1)、`--port`(默认 8788)、`--timeout`(秒,默认 120)、`--no-mirror`、`--help`。
|
|
85
58
|
|
|
86
|
-
##
|
|
59
|
+
## 接口
|
|
87
60
|
|
|
88
|
-
- `POST /v1/chat/completions`
|
|
61
|
+
- `POST /v1/chat/completions`(支持 `stream`)
|
|
62
|
+
- `POST /v1/messages`(Anthropic 兼容)
|
|
89
63
|
- `GET /v1/models`
|
|
90
64
|
- `GET /health`
|
|
91
|
-
- `GET /auth/status`
|
|
92
|
-
- `POST /auth/refresh`
|
|
93
65
|
|
|
94
|
-
##
|
|
66
|
+
## 常见问题
|
|
67
|
+
|
|
68
|
+
**提示"未找到可用的 CodeFree-O 登录态"**
|
|
69
|
+
运行 `codefree-proxy login`:它会自动调用 `codefree-o login` 打开研发云登录页,登录成功后把登录态导入,再 `codefree-proxy start` 即可。
|
|
95
70
|
|
|
96
|
-
- `
|
|
97
|
-
|
|
98
|
-
- `Qwen3.5-122B-A10B`
|
|
71
|
+
**`codefree-proxy login` 是什么行为**
|
|
72
|
+
有可导入的登录态就直接导入;没有就代跑一次原生 `codefree-o login`(会弹浏览器,需要你完成登录)。`start` / `serve` 不会自动弹浏览器,只在未登录时立刻报错并提示上一步。
|
|
99
73
|
|
|
100
|
-
|
|
74
|
+
**提示"登录态已失效,无法续期"**
|
|
75
|
+
CodeFree-O 的 refresh token 过期了,重新 `codefree-o login` 后重启代理。
|
|
101
76
|
|
|
102
|
-
|
|
77
|
+
**端口被占用**
|
|
78
|
+
`codefree-proxy start --port 8789`,或按提示确认占用进程。
|
|
103
79
|
|
|
104
|
-
|
|
80
|
+
**不想让代理动 CodeFree-O 的登录态**
|
|
81
|
+
默认会把轮换后的 token 回写到 CodeFree-O,让两边都能继续用。若你同时在跑原生 CLI,可加 `--no-mirror` 关闭回写(此时原生 CLI 可能需要重新登录)。
|
|
105
82
|
|
|
106
|
-
|
|
107
|
-
-
|
|
83
|
+
**日志**
|
|
84
|
+
`~/.codefree-proxy/logs/proxy.log`;登录态缓存在 `~/.codefree-proxy/config.json`(权限 0600)。
|
|
108
85
|
|
|
109
|
-
|
|
86
|
+
## 更新
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm i -g codefree-proxy@latest
|
|
90
|
+
```
|
|
@@ -1,61 +1,141 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { deleteCredentials, loadCredentials, saveCredentials } from './config-store.js';
|
|
2
|
-
import { loadCodeFreeOCredentials } from './codefree-o.js';
|
|
3
|
-
|
|
4
|
+
import { inspectCodeFreeOStore, loadCodeFreeOCredentials } from './codefree-o.js';
|
|
5
|
+
|
|
6
|
+
const CACHE_TTL_MS = 30_000;
|
|
7
|
+
const NATIVE_KEEP_FIELDS = ['userId', 'refreshCfToken', 'refreshCfTokenExpiresAt', 'baseUrlSnapshot', 'clientId'];
|
|
8
|
+
|
|
9
|
+
export function nativeCredentialPaths(homeDir = homedir()) {
|
|
10
|
+
return [
|
|
11
|
+
join(homeDir, '.codefree-o', '.local', 'share', 'codefree.json'),
|
|
12
|
+
join(homeDir, '.codefree-o', '.config', 'codefree.json'),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class CredentialsError extends Error {
|
|
17
|
+
constructor(message, detail = {}) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'CredentialsError';
|
|
20
|
+
this.code = 'CODEFREE_NO_CREDENTIALS';
|
|
21
|
+
this.detail = detail;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const LOGIN_HINT = '请运行 codefree-proxy login 完成登录(它会调用 codefree-o login)。';
|
|
26
|
+
|
|
4
27
|
export class AuthManager {
|
|
5
28
|
cache = null;
|
|
29
|
+
cacheAt = 0;
|
|
6
30
|
source;
|
|
31
|
+
nativeState = null;
|
|
7
32
|
deps;
|
|
8
33
|
constructor(deps = {}) {
|
|
9
34
|
this.deps = {
|
|
10
35
|
loadConfig: deps.loadConfig ?? loadCredentials,
|
|
11
36
|
saveConfig: deps.saveConfig ?? saveCredentials,
|
|
12
37
|
deleteConfig: deps.deleteConfig ?? deleteCredentials,
|
|
13
|
-
loadCodeFreeO: deps.loadCodeFreeO ?? loadCodeFreeOCredentials,
|
|
14
|
-
|
|
38
|
+
loadCodeFreeO: deps.loadCodeFreeO ?? ((path) => loadCodeFreeOCredentials(path)),
|
|
39
|
+
paths: deps.paths ?? nativeCredentialPaths()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Read every known CodeFree-O store; first usable one wins. */
|
|
43
|
+
async readNative() {
|
|
44
|
+
const checked = [];
|
|
45
|
+
for (const path of this.deps.paths) {
|
|
46
|
+
const info = await inspectCodeFreeOStore(path);
|
|
47
|
+
checked.push(info);
|
|
48
|
+
if (info.ok) {
|
|
49
|
+
const creds = await this.deps.loadCodeFreeO(path);
|
|
50
|
+
if (creds) {
|
|
51
|
+
this.nativeState = { ok: true, path, checked, userId: creds.userId };
|
|
52
|
+
return { creds, checked };
|
|
53
|
+
}
|
|
54
|
+
info.ok = false;
|
|
55
|
+
info.reason = '登录态无法读取,请重新运行 codefree-o login';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const credentialFile = checked.find((c) => c.exists && c.credentialFile);
|
|
59
|
+
this.nativeState = {
|
|
60
|
+
ok: false,
|
|
61
|
+
checked,
|
|
62
|
+
reason: credentialFile?.reason ?? `未找到 CodeFree-O 登录态文件(已检查 ${checked.map((c) => c.path).join('、')})`
|
|
15
63
|
};
|
|
64
|
+
return { creds: null, checked };
|
|
16
65
|
}
|
|
17
66
|
async getCredentials() {
|
|
18
|
-
if (this.cache)
|
|
67
|
+
if (this.cache && Date.now() - this.cacheAt < CACHE_TTL_MS)
|
|
19
68
|
return this.cache;
|
|
20
69
|
const config = await this.deps.loadConfig();
|
|
21
|
-
|
|
70
|
+
const { creds: native, checked } = await this.readNative();
|
|
71
|
+
if (native?.refreshCfToken) {
|
|
72
|
+
const preserved = {};
|
|
73
|
+
for (const field of NATIVE_KEEP_FIELDS) {
|
|
74
|
+
if (native[field] !== undefined)
|
|
75
|
+
preserved[field] = native[field];
|
|
76
|
+
}
|
|
77
|
+
this.cache = { ...(config ?? {}), ...native, ...preserved };
|
|
78
|
+
this.cacheAt = Date.now();
|
|
79
|
+
this.source = config ? 'config+codefree-o' : 'codefree-o';
|
|
80
|
+
await this.deps.saveConfig(this.cache);
|
|
81
|
+
return this.cache;
|
|
82
|
+
}
|
|
83
|
+
if (config?.refreshCfToken) {
|
|
22
84
|
this.cache = config;
|
|
85
|
+
this.cacheAt = Date.now();
|
|
23
86
|
this.source = 'config';
|
|
24
87
|
return config;
|
|
25
88
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.cache = codefreeO;
|
|
29
|
-
this.source = 'codefree-o';
|
|
30
|
-
await this.deps.saveConfig(codefreeO);
|
|
31
|
-
return codefreeO;
|
|
89
|
+
if (config?.apiKey) {
|
|
90
|
+
throw new CredentialsError(`本机保存的是旧版 CodeFree-O 登录态(只有 apiKey,无法续期)。${LOGIN_HINT}`, { checked });
|
|
32
91
|
}
|
|
33
|
-
|
|
92
|
+
const reason = this.nativeState?.reason ? `${this.nativeState.reason}。` : '';
|
|
93
|
+
const hint = reason.includes('codefree-proxy login') ? '' : LOGIN_HINT;
|
|
94
|
+
throw new CredentialsError(`未找到可用的 CodeFree-O 登录态。${reason}${hint}`, { checked });
|
|
34
95
|
}
|
|
35
|
-
async
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
await this.deps.saveConfig(credentials);
|
|
40
|
-
return credentials;
|
|
96
|
+
async invalidate() {
|
|
97
|
+
this.cache = null;
|
|
98
|
+
this.cacheAt = 0;
|
|
99
|
+
return true;
|
|
41
100
|
}
|
|
42
101
|
async logout() {
|
|
43
102
|
this.cache = null;
|
|
103
|
+
this.cacheAt = 0;
|
|
44
104
|
this.source = undefined;
|
|
45
105
|
await this.deps.deleteConfig();
|
|
46
106
|
}
|
|
47
107
|
async refresh() {
|
|
48
|
-
this.
|
|
108
|
+
this.invalidate();
|
|
49
109
|
return this.getCredentials();
|
|
50
110
|
}
|
|
111
|
+
/** Read-only: never logs in, never writes, never touches the network. */
|
|
51
112
|
async status() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
113
|
+
const config = await this.deps.loadConfig().catch(() => null);
|
|
114
|
+
const { checked } = await this.readNative();
|
|
115
|
+
const usable = Boolean(config?.refreshCfToken) || Boolean(this.nativeState?.ok);
|
|
116
|
+
return {
|
|
117
|
+
status: usable ? 'ok' : 'missing',
|
|
118
|
+
source: this.nativeState?.ok ? 'codefree-o' : (config?.refreshCfToken ? 'config' : null),
|
|
119
|
+
userId: this.nativeState?.userId ?? config?.userId ?? null,
|
|
120
|
+
codefreeO: this.nativeState?.ok
|
|
121
|
+
? { ok: true, path: this.nativeState.path }
|
|
122
|
+
: { ok: false, reason: this.nativeState?.reason ?? '未找到登录态文件', checked: checked.map((c) => ({ path: c.path, exists: c.exists, ok: c.ok, reason: c.reason })) },
|
|
123
|
+
proxyConfig: config
|
|
124
|
+
? { ok: Boolean(config.refreshCfToken), userId: config.userId ?? null, updatedAt: config.updatedAt ?? null }
|
|
125
|
+
: { ok: false },
|
|
126
|
+
...(usable ? {} : { hint: LOGIN_HINT })
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Explicit import of the native CodeFree-O store (used by `login`). */
|
|
130
|
+
async login() {
|
|
131
|
+
const { creds, checked } = await this.readNative();
|
|
132
|
+
if (!creds?.refreshCfToken) {
|
|
133
|
+
throw new CredentialsError(`未找到可导入的 CodeFree-O 登录态。${LOGIN_HINT}`, { checked });
|
|
58
134
|
}
|
|
135
|
+
this.cache = creds;
|
|
136
|
+
this.cacheAt = Date.now();
|
|
137
|
+
this.source = 'codefree-o';
|
|
138
|
+
await this.deps.saveConfig(creds);
|
|
139
|
+
return creds;
|
|
59
140
|
}
|
|
60
141
|
}
|
|
61
|
-
//# sourceMappingURL=auth-manager.js.map
|
package/dist/auth/codefree-o.js
CHANGED
|
@@ -1,25 +1,123 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { createDecipheriv } from 'node:crypto';
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { createDecipheriv, createCipheriv, createHash, randomBytes } from 'node:crypto';
|
|
5
|
+
// CodeFree-O >= 0.3 stores its refresh token as v1:<iv>:<ciphertext>:<tag>,
|
|
6
|
+
// AES-256-GCM with a fixed local key derived from a constant string. The key is
|
|
7
|
+
// part of the on-disk format, so every CodeFree-O build derives the same one.
|
|
8
|
+
const MODERN_KEY = createHash('sha256')
|
|
9
|
+
.update('codefree-o-refresh-token-local-encryption-key-v1', 'utf8')
|
|
10
|
+
.digest();
|
|
7
11
|
export function getCodeFreeOCredentialPath(homeDir = homedir()) {
|
|
8
12
|
return join(homeDir, '.codefree-o', '.local', 'share', 'codefree.json');
|
|
9
13
|
}
|
|
10
|
-
export function
|
|
11
|
-
const
|
|
12
|
-
|
|
14
|
+
export function decryptRefreshCfToken(encrypted) {
|
|
15
|
+
const parts = String(encrypted).split(':');
|
|
16
|
+
if (parts.length !== 4 || parts[0] !== 'v1')
|
|
17
|
+
throw new Error('unsupported token format');
|
|
18
|
+
const decipher = createDecipheriv('aes-256-gcm', MODERN_KEY, Buffer.from(parts[1], 'base64'));
|
|
19
|
+
decipher.setAuthTag(Buffer.from(parts[3], 'base64'));
|
|
20
|
+
return Buffer.concat([decipher.update(Buffer.from(parts[2], 'base64')), decipher.final()]).toString('utf8');
|
|
21
|
+
}
|
|
22
|
+
export function encryptRefreshCfToken(refreshCfToken) {
|
|
23
|
+
const iv = randomBytes(12);
|
|
24
|
+
const cipher = createCipheriv('aes-256-gcm', MODERN_KEY, iv);
|
|
25
|
+
const ciphertext = Buffer.concat([cipher.update(String(refreshCfToken), 'utf8'), cipher.final()]);
|
|
26
|
+
const tag = cipher.getAuthTag();
|
|
27
|
+
return `v1:${iv.toString('base64')}:${ciphertext.toString('base64')}:${tag.toString('base64')}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Persist a rotated refresh token back into the native CodeFree-O store so the
|
|
31
|
+
* native CLI keeps working after the proxy refreshes. Merges into the existing
|
|
32
|
+
* file (mode 0600) instead of replacing it, so userId/version survive.
|
|
33
|
+
*/
|
|
34
|
+
export async function saveCodeFreeORefreshToken(refreshCfToken, refreshCfTokenExpiresAt, path = getCodeFreeOCredentialPath()) {
|
|
35
|
+
let existing = {};
|
|
36
|
+
try {
|
|
37
|
+
existing = JSON.parse(await readFile(path, 'utf8'));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const next = {
|
|
43
|
+
...existing,
|
|
44
|
+
encryptedRefreshCfToken: encryptRefreshCfToken(refreshCfToken),
|
|
45
|
+
refreshCfTokenExpiresAt: Number(refreshCfTokenExpiresAt) || existing.refreshCfTokenExpiresAt || 0
|
|
46
|
+
};
|
|
47
|
+
delete next.refreshCfToken;
|
|
48
|
+
await writeFile(path, JSON.stringify(next), { mode: 0o600 });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read-only inspection for `doctor` / `status`. Never throws; reports why a
|
|
53
|
+
* store cannot be used so the CLI can fail fast with an actionable message.
|
|
54
|
+
*/
|
|
55
|
+
export async function inspectCodeFreeOStore(path = getCodeFreeOCredentialPath()) {
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await readFile(path, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const code = error?.code;
|
|
62
|
+
return {
|
|
63
|
+
path,
|
|
64
|
+
exists: false,
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: code === 'ENOENT'
|
|
67
|
+
? '未找到 CodeFree-O 登录态文件,请运行 codefree-proxy login 完成登录'
|
|
68
|
+
: `读取 CodeFree-O 登录态文件失败(${code ?? 'unknown'})`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(raw);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { path, exists: true, ok: false, reason: 'CodeFree-O 登录态文件不是合法 JSON,请运行 codefree-proxy login 重新登录' };
|
|
77
|
+
}
|
|
78
|
+
const encrypted = parsed.encryptedRefreshCfToken;
|
|
79
|
+
const userId = parsed.userId ? String(parsed.userId) : undefined;
|
|
80
|
+
if (!encrypted) {
|
|
81
|
+
return { path, exists: true, ok: false, userId, credentialFile: Boolean(parsed.encryptedApiKey || parsed.refreshCfToken),
|
|
82
|
+
reason: parsed.encryptedApiKey || parsed.refreshCfToken
|
|
83
|
+
? '登录态里没有 refreshCfToken(CodeFree-O 版本过旧),请升级 CodeFree-O 后重新登录'
|
|
84
|
+
: '该文件不是 CodeFree-O 登录态(缺少 refreshCfToken)' };
|
|
85
|
+
}
|
|
86
|
+
let token;
|
|
87
|
+
try {
|
|
88
|
+
token = decryptRefreshCfToken(encrypted);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return { path, exists: true, ok: false, userId, credentialFile: true,
|
|
92
|
+
reason: `登录态无法解密(${error?.message ?? '格式不支持'}),请运行 codefree-proxy login 重新登录` };
|
|
93
|
+
}
|
|
94
|
+
if (!token) {
|
|
95
|
+
return { path, exists: true, ok: false, userId, credentialFile: true, reason: '登录态里的 refreshCfToken 为空,请运行 codefree-proxy login 重新登录' };
|
|
96
|
+
}
|
|
97
|
+
const expiresAt = Number(parsed.refreshCfTokenExpiresAt) || 0;
|
|
98
|
+
return {
|
|
99
|
+
path,
|
|
100
|
+
exists: true,
|
|
101
|
+
ok: true,
|
|
102
|
+
userId,
|
|
103
|
+
expiresAt,
|
|
104
|
+
expiresAtIso: expiresAt > 0 ? new Date(expiresAt).toISOString() : undefined
|
|
105
|
+
};
|
|
13
106
|
}
|
|
14
107
|
export async function loadCodeFreeOCredentials(path = getCodeFreeOCredentialPath()) {
|
|
15
108
|
try {
|
|
16
109
|
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
17
|
-
if (!parsed.userId || !parsed.
|
|
110
|
+
if (!parsed.userId || !parsed.encryptedRefreshCfToken)
|
|
111
|
+
return null;
|
|
112
|
+
const refreshCfToken = decryptRefreshCfToken(parsed.encryptedRefreshCfToken);
|
|
113
|
+
if (typeof refreshCfToken !== 'string' || refreshCfToken.length === 0)
|
|
18
114
|
return null;
|
|
19
115
|
return {
|
|
20
|
-
userId: parsed.userId,
|
|
21
|
-
|
|
22
|
-
|
|
116
|
+
userId: String(parsed.userId),
|
|
117
|
+
refreshCfToken,
|
|
118
|
+
refreshCfTokenExpiresAt: Number(parsed.refreshCfTokenExpiresAt) || 0,
|
|
119
|
+
...(parsed.baseUrlSnapshot ? { baseUrlSnapshot: parsed.baseUrlSnapshot } : {}),
|
|
120
|
+
...(parsed.clientId ? { clientId: parsed.clientId } : {}),
|
|
23
121
|
updatedAt: Date.now()
|
|
24
122
|
};
|
|
25
123
|
}
|
|
@@ -27,4 +125,3 @@ export async function loadCodeFreeOCredentials(path = getCodeFreeOCredentialPath
|
|
|
27
125
|
return null;
|
|
28
126
|
}
|
|
29
127
|
}
|
|
30
|
-
//# sourceMappingURL=codefree-o.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
function isStoredCredentials(value) {
|
|
@@ -27,8 +27,18 @@ export async function loadCredentials(path = getConfigPath()) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export async function saveCredentials(credentials, path = getConfigPath()) {
|
|
30
|
-
await mkdir(dirname(path), { recursive: true });
|
|
31
|
-
|
|
30
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
31
|
+
let previous = {};
|
|
32
|
+
try {
|
|
33
|
+
previous = JSON.parse(await readFile(path, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// A first login has no prior record; write the new credentials below.
|
|
37
|
+
}
|
|
38
|
+
const merged = { ...previous, ...credentials };
|
|
39
|
+
// Credentials file holds cfToken / refreshCfToken: keep it owner-only.
|
|
40
|
+
await writeFile(path, `${JSON.stringify(merged, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
41
|
+
await chmod(path, 0o600).catch(() => { });
|
|
32
42
|
}
|
|
33
43
|
export async function deleteCredentials(path = getConfigPath()) {
|
|
34
44
|
await rm(path, { force: true });
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
type Platform = NodeJS.Platform;
|
|
3
|
-
export type CliCommand = 'serve' | 'login' | 'logout' | 'status' | 'stop';
|
|
3
|
+
export type CliCommand = 'serve' | 'start' | 'login' | 'logout' | 'status' | 'stop';
|
|
4
4
|
export type CommandRunner = (command: string, args: string[]) => Promise<string>;
|
|
5
|
+
type DaemonSpawnOptions = {
|
|
6
|
+
detached: true;
|
|
7
|
+
stdio: ['ignore', number, number];
|
|
8
|
+
};
|
|
9
|
+
type DaemonChild = {
|
|
10
|
+
pid?: number;
|
|
11
|
+
unref(): void;
|
|
12
|
+
};
|
|
13
|
+
export interface DaemonPaths {
|
|
14
|
+
dir: string;
|
|
15
|
+
logDir: string;
|
|
16
|
+
logFile: string;
|
|
17
|
+
pidFile: string;
|
|
18
|
+
}
|
|
19
|
+
export interface StartResult {
|
|
20
|
+
pid: number;
|
|
21
|
+
pidFile: string;
|
|
22
|
+
logFile: string;
|
|
23
|
+
}
|
|
24
|
+
export interface StartDaemonDeps {
|
|
25
|
+
nodePath?: string;
|
|
26
|
+
entryPath?: string;
|
|
27
|
+
homeDir?: string;
|
|
28
|
+
spawnDaemon?: (command: string, args: string[], options: DaemonSpawnOptions) => DaemonChild;
|
|
29
|
+
mkdirp?: (path: string) => void;
|
|
30
|
+
writeFile?: (path: string, value: string) => void;
|
|
31
|
+
openLog?: (path: string) => number;
|
|
32
|
+
closeLog?: (fd: number) => void;
|
|
33
|
+
}
|
|
5
34
|
export interface CliOptions {
|
|
6
35
|
command: CliCommand;
|
|
7
36
|
host: string;
|
|
@@ -19,7 +48,10 @@ export interface StartupInfoOptions {
|
|
|
19
48
|
userId: string;
|
|
20
49
|
models: readonly string[];
|
|
21
50
|
}
|
|
51
|
+
export declare function daemonPaths(homeDir?: string): DaemonPaths;
|
|
52
|
+
export declare function startDaemon(options: CliOptions, deps?: StartDaemonDeps): Promise<StartResult>;
|
|
22
53
|
export declare function formatStartupInfo(options: StartupInfoOptions): string;
|
|
54
|
+
export declare function formatDaemonStartedInfo(options: StartupInfoOptions, result: StartResult): string;
|
|
23
55
|
export declare function parseArgs(argv: string[]): CliOptions;
|
|
24
56
|
export declare function findListeningPids(port: number, platform?: Platform, run?: CommandRunner): Promise<number[]>;
|
|
25
57
|
export declare function stopByPort(port: number, platform?: Platform, run?: CommandRunner): Promise<StopResult>;
|