codex-slot 0.1.28 → 0.1.29
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 +32 -0
- package/dist/app/status-service.js +30 -0
- package/dist/cli.js +62 -0
- package/dist/config.js +12 -2
- package/dist/model-proxy-dispatcher.js +67 -0
- package/dist/relay-commands.js +166 -0
- package/dist/relay-proxy-service.js +160 -0
- package/dist/relay-store.js +138 -0
- package/dist/server.js +2 -2
- package/dist/state.js +50 -0
- package/dist/status-command.js +141 -19
- package/dist/status.js +72 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- Manage multiple accounts or workspaces as separate slots
|
|
11
11
|
- Refresh and cache the latest usage from the official usage endpoint
|
|
12
12
|
- Expose a local provider endpoint for Codex
|
|
13
|
+
- Optionally pin model requests to an OpenAI-compatible relay slot
|
|
13
14
|
- Proxy ChatGPT backend plugin requests through the selected cslot account
|
|
14
15
|
- Apply local block rules for temporary, 5-hour, and weekly limits
|
|
15
16
|
- Automatically switch `~/.codex/config.toml` to the `cslot` provider while the local proxy is running (and restore it on stop)
|
|
@@ -89,12 +90,21 @@ codex-slot import <name> [HOME]
|
|
|
89
90
|
codex-slot status
|
|
90
91
|
codex-slot start [--port <port>]
|
|
91
92
|
codex-slot stop
|
|
93
|
+
codex-slot relay add <name> --base-url <url> --api-key <key>
|
|
94
|
+
codex-slot relay list
|
|
95
|
+
codex-slot use relay <name>
|
|
96
|
+
codex-slot use auth
|
|
97
|
+
codex-slot current
|
|
92
98
|
```
|
|
93
99
|
|
|
94
100
|
Common patterns:
|
|
95
101
|
|
|
96
102
|
- `cslot import work ~/workspace-home`
|
|
97
103
|
- `cslot rename work work-main`
|
|
104
|
+
- `cslot relay add third --base-url https://relay.example.com/v1 --api-key sk-...`
|
|
105
|
+
- `cslot status`
|
|
106
|
+
- `cslot use relay third`
|
|
107
|
+
- `cslot use auth`
|
|
98
108
|
- `cslot start`
|
|
99
109
|
|
|
100
110
|
## Architecture
|
|
@@ -108,6 +118,7 @@ The project is intentionally split by responsibility:
|
|
|
108
118
|
- `src/status-command.ts`: usage refresh output and interactive toggle UI
|
|
109
119
|
- `src/codex-config.ts`: managed `~/.codex/config.toml` apply/restore logic
|
|
110
120
|
- `src/backend-proxy-service.ts`: ChatGPT backend proxy for Codex plugin/runtime requests
|
|
121
|
+
- `src/relay-proxy-service.ts`, `src/model-proxy-dispatcher.ts`, `src/relay-store.ts`: optional OpenAI-compatible relay slots and `/v1/*` model route dispatch
|
|
111
122
|
- `src/account-store.ts`, `src/usage-sync.ts`, `src/scheduler.ts`, `src/status.ts`: core domain and runtime logic
|
|
112
123
|
- `src/text.ts`: shared bilingual text and locale-independent formatting helpers
|
|
113
124
|
|
|
@@ -146,6 +157,27 @@ Behavior:
|
|
|
146
157
|
- `requires_openai_auth = true` keeps Codex App treating the local cslot provider as a ChatGPT-authenticated provider, so plugin navigation and trusted plugin runtimes are not disabled as API-key/custom-provider mode
|
|
147
158
|
- `/backend-api/*` requests are forwarded to ChatGPT backend with the current selected account's upstream token; client `Authorization` headers are not forwarded upstream
|
|
148
159
|
|
|
160
|
+
## OpenAI-compatible Relay Slots
|
|
161
|
+
|
|
162
|
+
Relay slots are optional model-only exits. They do not replace the official Codex / ChatGPT login state.
|
|
163
|
+
Use `codex-slot relay add` to register a relay slot, then use `codex-slot status` for daily enable/disable and model-route selection.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
codex-slot relay add third --base-url https://relay.example.com/v1 --api-key sk-...
|
|
167
|
+
codex-slot use relay third
|
|
168
|
+
codex-slot current
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Behavior:
|
|
172
|
+
|
|
173
|
+
- `/v1/*` model requests are fixed to the selected relay slot
|
|
174
|
+
- Relay requests use the relay slot API key; client `Authorization` headers are not forwarded upstream
|
|
175
|
+
- Relay failures are returned directly and do not fall back to official cslot accounts
|
|
176
|
+
- `/backend-api/*` plugin/runtime requests still use the selected official Codex / ChatGPT login state
|
|
177
|
+
- Usage refresh only applies to official accounts, not relay slots
|
|
178
|
+
- In interactive `status`, press `Space` to enable/disable the selected account or relay slot, and press `m` to select the model route
|
|
179
|
+
- Run `codex-slot use auth` to restore `/v1/*` model requests to the official account scheduler
|
|
180
|
+
|
|
149
181
|
## Codex App Plugins
|
|
150
182
|
|
|
151
183
|
`codex-slot start` also switches the main `~/.codex` login state to the selected managed account while cslot is running.
|
|
@@ -3,11 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.refreshStatusSnapshot = refreshStatusSnapshot;
|
|
4
4
|
exports.getStatusSnapshot = getStatusSnapshot;
|
|
5
5
|
exports.persistAccountEnabledState = persistAccountEnabledState;
|
|
6
|
+
exports.persistRelayEnabledState = persistRelayEnabledState;
|
|
6
7
|
const config_1 = require("../config");
|
|
7
8
|
const scheduler_1 = require("../scheduler");
|
|
8
9
|
const status_1 = require("../status");
|
|
9
10
|
const state_1 = require("../state");
|
|
11
|
+
const relay_store_1 = require("../relay-store");
|
|
12
|
+
const state_2 = require("../state");
|
|
10
13
|
const usage_sync_1 = require("../usage-sync");
|
|
14
|
+
function formatModelRouteLabel(route) {
|
|
15
|
+
if (route.mode === "relay_slot") {
|
|
16
|
+
return `relay:${route.relay_slot_id}`;
|
|
17
|
+
}
|
|
18
|
+
return "auth_pool";
|
|
19
|
+
}
|
|
11
20
|
/**
|
|
12
21
|
* 刷新全部账号额度并返回最新状态快照。
|
|
13
22
|
*
|
|
@@ -27,8 +36,12 @@ async function refreshStatusSnapshot() {
|
|
|
27
36
|
function getStatusSnapshot() {
|
|
28
37
|
const statuses = (0, status_1.collectAccountStatuses)();
|
|
29
38
|
const selected = (0, scheduler_1.pickBestAccount)();
|
|
39
|
+
const modelRoute = (0, state_2.getSelectedModelRoute)();
|
|
30
40
|
return {
|
|
31
41
|
statuses,
|
|
42
|
+
relaySlots: (0, relay_store_1.listRelaySlots)(),
|
|
43
|
+
modelRoute,
|
|
44
|
+
modelRouteLabel: formatModelRouteLabel(modelRoute),
|
|
32
45
|
selectedName: selected?.account.name ?? null,
|
|
33
46
|
codexAuthAccountId: (0, state_1.getSelectedCodexAuthAccountId)(),
|
|
34
47
|
summary: (0, status_1.summarizeAccountStatuses)(statuses)
|
|
@@ -51,3 +64,20 @@ function persistAccountEnabledState(accounts) {
|
|
|
51
64
|
}
|
|
52
65
|
(0, config_1.saveConfig)(latest);
|
|
53
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* 将交互式界面中的 relay 启用状态修改写回配置文件。
|
|
69
|
+
*
|
|
70
|
+
* @param slots 用户在交互界面中调整后的 relay slot 数组。
|
|
71
|
+
* @returns 无返回值。
|
|
72
|
+
* @throws 当配置写入失败时抛出异常。
|
|
73
|
+
*/
|
|
74
|
+
function persistRelayEnabledState(slots) {
|
|
75
|
+
const latest = (0, config_1.loadConfig)();
|
|
76
|
+
for (const slot of slots) {
|
|
77
|
+
const index = latest.relay_slots.findIndex((item) => item.id === slot.id);
|
|
78
|
+
if (index >= 0) {
|
|
79
|
+
latest.relay_slots[index].enabled = slot.enabled;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
(0, config_1.saveConfig)(latest);
|
|
83
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const account_commands_1 = require("./account-commands");
|
|
6
6
|
const cli_helpers_1 = require("./cli-helpers");
|
|
7
7
|
const config_1 = require("./config");
|
|
8
|
+
const relay_commands_1 = require("./relay-commands");
|
|
8
9
|
const service_control_1 = require("./service-control");
|
|
9
10
|
const status_command_1 = require("./status-command");
|
|
10
11
|
const text_1 = require("./text");
|
|
@@ -71,6 +72,66 @@ function registerAccountCommands(program) {
|
|
|
71
72
|
.argument("<newName>", (0, text_1.bi)("新账号或工作空间标识", "New managed slot name"))
|
|
72
73
|
.action(account_commands_1.handleAccountRename);
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 注册 relay 与模型出口选择相关子命令。
|
|
77
|
+
*
|
|
78
|
+
* @param program Commander 程序实例。
|
|
79
|
+
* @returns 无返回值。
|
|
80
|
+
* @throws 无显式抛出。
|
|
81
|
+
*/
|
|
82
|
+
function registerRelayCommands(program) {
|
|
83
|
+
const relay = program
|
|
84
|
+
.command("relay")
|
|
85
|
+
.description((0, text_1.bi)("管理 OpenAI-compatible 中转槽位", "Manage OpenAI-compatible relay slots"));
|
|
86
|
+
relay
|
|
87
|
+
.command("add")
|
|
88
|
+
.description((0, text_1.bi)("新增一个中转槽位", "Add a relay slot"))
|
|
89
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
90
|
+
.requiredOption("--base-url <url>", (0, text_1.bi)("OpenAI-compatible base_url,通常以 /v1 结尾", "OpenAI-compatible base_url, usually ending with /v1"))
|
|
91
|
+
.requiredOption("--api-key <key>", (0, text_1.bi)("中转 API key", "Relay API key"))
|
|
92
|
+
.action(relay_commands_1.handleRelayAdd);
|
|
93
|
+
relay
|
|
94
|
+
.command("list")
|
|
95
|
+
.description((0, text_1.bi)("列出中转槽位", "List relay slots"))
|
|
96
|
+
.action(relay_commands_1.handleRelayList);
|
|
97
|
+
relay
|
|
98
|
+
.command("del")
|
|
99
|
+
.description((0, text_1.bi)("删除一个中转槽位", "Remove a relay slot"))
|
|
100
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
101
|
+
.action(relay_commands_1.handleRelayRemove);
|
|
102
|
+
relay
|
|
103
|
+
.command("rename")
|
|
104
|
+
.description((0, text_1.bi)("重命名一个中转槽位", "Rename a relay slot"))
|
|
105
|
+
.argument("<oldName>", (0, text_1.bi)("原中转槽位名", "Old relay slot name"))
|
|
106
|
+
.argument("<newName>", (0, text_1.bi)("新中转槽位名", "New relay slot name"))
|
|
107
|
+
.action(relay_commands_1.handleRelayRename);
|
|
108
|
+
relay
|
|
109
|
+
.command("enable")
|
|
110
|
+
.description((0, text_1.bi)("启用一个中转槽位", "Enable a relay slot"))
|
|
111
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
112
|
+
.action(relay_commands_1.handleRelayEnable);
|
|
113
|
+
relay
|
|
114
|
+
.command("disable")
|
|
115
|
+
.description((0, text_1.bi)("禁用一个中转槽位", "Disable a relay slot"))
|
|
116
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
117
|
+
.action(relay_commands_1.handleRelayDisable);
|
|
118
|
+
const use = program
|
|
119
|
+
.command("use")
|
|
120
|
+
.description((0, text_1.bi)("切换模型请求出口", "Switch model route"));
|
|
121
|
+
use
|
|
122
|
+
.command("relay")
|
|
123
|
+
.description((0, text_1.bi)("固定模型请求到指定中转槽位", "Fix model route to a relay slot"))
|
|
124
|
+
.argument("<name>", (0, text_1.bi)("中转槽位名", "Relay slot name"))
|
|
125
|
+
.action(relay_commands_1.handleUseRelay);
|
|
126
|
+
use
|
|
127
|
+
.command("auth")
|
|
128
|
+
.description((0, text_1.bi)("恢复模型请求到官方账号池", "Restore model route to the official auth pool"))
|
|
129
|
+
.action(relay_commands_1.handleUseAuthPool);
|
|
130
|
+
program
|
|
131
|
+
.command("current")
|
|
132
|
+
.description((0, text_1.bi)("查看当前模型出口与 Codex App 登录态选择", "Show current model route and Codex App auth selection"))
|
|
133
|
+
.action(relay_commands_1.handleCurrent);
|
|
134
|
+
}
|
|
74
135
|
/**
|
|
75
136
|
* 注册配置与状态相关子命令。
|
|
76
137
|
*
|
|
@@ -117,6 +178,7 @@ async function main() {
|
|
|
117
178
|
(0, config_1.loadConfig)();
|
|
118
179
|
configureRootProgram(program);
|
|
119
180
|
registerAccountCommands(program);
|
|
181
|
+
registerRelayCommands(program);
|
|
120
182
|
registerRuntimeCommands(program);
|
|
121
183
|
await program.parseAsync(process.argv);
|
|
122
184
|
}
|
package/dist/config.js
CHANGED
|
@@ -26,6 +26,14 @@ const managedAccountSchema = zod_1.z.object({
|
|
|
26
26
|
enabled: zod_1.z.boolean().default(true),
|
|
27
27
|
imported_at: zod_1.z.string().optional()
|
|
28
28
|
});
|
|
29
|
+
const relaySlotSchema = zod_1.z.object({
|
|
30
|
+
id: zod_1.z.string().min(1),
|
|
31
|
+
name: zod_1.z.string().min(1),
|
|
32
|
+
base_url: zod_1.z.string().url(),
|
|
33
|
+
api_key: zod_1.z.string().min(1),
|
|
34
|
+
enabled: zod_1.z.boolean().default(true),
|
|
35
|
+
imported_at: zod_1.z.string().optional()
|
|
36
|
+
});
|
|
29
37
|
const configSchema = zod_1.z.object({
|
|
30
38
|
version: zod_1.z.number().int().default(1),
|
|
31
39
|
server: zod_1.z
|
|
@@ -52,7 +60,8 @@ const configSchema = zod_1.z.object({
|
|
|
52
60
|
auth_base_url: "https://auth.openai.com",
|
|
53
61
|
oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
54
62
|
}),
|
|
55
|
-
accounts: zod_1.z.array(managedAccountSchema).default([])
|
|
63
|
+
accounts: zod_1.z.array(managedAccountSchema).default([]),
|
|
64
|
+
relay_slots: zod_1.z.array(relaySlotSchema).default([])
|
|
56
65
|
});
|
|
57
66
|
/**
|
|
58
67
|
* 解析当前进程应使用的用户 HOME 目录,兼容 Windows 缺少 `HOME` 的场景。
|
|
@@ -138,7 +147,8 @@ function loadConfig() {
|
|
|
138
147
|
auth_base_url: "https://auth.openai.com",
|
|
139
148
|
oauth_client_id: "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
140
149
|
},
|
|
141
|
-
accounts: []
|
|
150
|
+
accounts: [],
|
|
151
|
+
relay_slots: []
|
|
142
152
|
};
|
|
143
153
|
saveConfig(defaultConfig);
|
|
144
154
|
return defaultConfig;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.proxyModelWithRoute = void 0;
|
|
4
|
+
exports.createModelProxyDispatcher = createModelProxyDispatcher;
|
|
5
|
+
const proxy_retry_service_1 = require("./proxy-retry-service");
|
|
6
|
+
const relay_proxy_service_1 = require("./relay-proxy-service");
|
|
7
|
+
const relay_store_1 = require("./relay-store");
|
|
8
|
+
const state_1 = require("./state");
|
|
9
|
+
function buildSendResult(statusCode, payload, headers) {
|
|
10
|
+
return {
|
|
11
|
+
type: "send",
|
|
12
|
+
statusCode,
|
|
13
|
+
payload,
|
|
14
|
+
headers
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 创建 `/v1/*` 模型请求分发器。
|
|
19
|
+
*
|
|
20
|
+
* 业务含义:
|
|
21
|
+
* 1. 默认 `auth_pool` 模式保持原有官方账号自动调度行为。
|
|
22
|
+
* 2. 手动选择 `relay_slot` 后,模型请求固定走该 relay slot。
|
|
23
|
+
* 3. relay 模式不影响 `/backend-api/*` 插件链路,也不会失败回退官方账号。
|
|
24
|
+
*
|
|
25
|
+
* @param overrides 可选依赖覆盖项。
|
|
26
|
+
* @returns 模型请求分发器实例。
|
|
27
|
+
* @throws 无显式抛出。
|
|
28
|
+
*/
|
|
29
|
+
function createModelProxyDispatcher(overrides) {
|
|
30
|
+
const dependencies = {
|
|
31
|
+
getSelectedModelRoute: state_1.getSelectedModelRoute,
|
|
32
|
+
findRelaySlot: relay_store_1.findRelaySlot,
|
|
33
|
+
proxyCodexWithRetry: proxy_retry_service_1.proxyCodexWithRetry,
|
|
34
|
+
proxyRelaySlot: relay_proxy_service_1.proxyRelaySlot,
|
|
35
|
+
...overrides
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
async proxyModelWithRoute(request) {
|
|
39
|
+
const route = dependencies.getSelectedModelRoute();
|
|
40
|
+
if (route.mode !== "relay_slot") {
|
|
41
|
+
return await dependencies.proxyCodexWithRetry(request);
|
|
42
|
+
}
|
|
43
|
+
const slot = dependencies.findRelaySlot(route.relay_slot_id);
|
|
44
|
+
if (!slot) {
|
|
45
|
+
return buildSendResult(503, {
|
|
46
|
+
error: {
|
|
47
|
+
message: `Relay slot not found: ${route.relay_slot_id}`,
|
|
48
|
+
type: "relay_slot_not_found"
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!slot.enabled) {
|
|
53
|
+
return buildSendResult(503, {
|
|
54
|
+
error: {
|
|
55
|
+
message: `Relay slot is disabled: ${route.relay_slot_id}`,
|
|
56
|
+
type: "relay_slot_disabled"
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return await dependencies.proxyRelaySlot({
|
|
61
|
+
slot,
|
|
62
|
+
request
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
exports.proxyModelWithRoute = createModelProxyDispatcher().proxyModelWithRoute;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleRelayAdd = handleRelayAdd;
|
|
4
|
+
exports.handleRelayList = handleRelayList;
|
|
5
|
+
exports.handleRelayRemove = handleRelayRemove;
|
|
6
|
+
exports.handleRelayRename = handleRelayRename;
|
|
7
|
+
exports.handleRelayEnable = handleRelayEnable;
|
|
8
|
+
exports.handleRelayDisable = handleRelayDisable;
|
|
9
|
+
exports.handleUseRelay = handleUseRelay;
|
|
10
|
+
exports.handleUseAuthPool = handleUseAuthPool;
|
|
11
|
+
exports.handleCurrent = handleCurrent;
|
|
12
|
+
const relay_store_1 = require("./relay-store");
|
|
13
|
+
const state_1 = require("./state");
|
|
14
|
+
const text_1 = require("./text");
|
|
15
|
+
function maskApiKey(apiKey) {
|
|
16
|
+
if (apiKey.length <= 8) {
|
|
17
|
+
return "****";
|
|
18
|
+
}
|
|
19
|
+
return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`;
|
|
20
|
+
}
|
|
21
|
+
function formatModelRoute() {
|
|
22
|
+
const route = (0, state_1.getSelectedModelRoute)();
|
|
23
|
+
if (route.mode === "relay_slot") {
|
|
24
|
+
return `relay:${route.relay_slot_id}`;
|
|
25
|
+
}
|
|
26
|
+
return "auth_pool";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 新增 OpenAI-compatible relay slot。
|
|
30
|
+
*
|
|
31
|
+
* @param name relay slot 名称。
|
|
32
|
+
* @param options relay 参数,必须包含 baseUrl 与 apiKey。
|
|
33
|
+
* @returns 无返回值。
|
|
34
|
+
* @throws 当 slot 已存在、URL 非法或配置写入失败时抛出异常。
|
|
35
|
+
*/
|
|
36
|
+
function handleRelayAdd(name, options) {
|
|
37
|
+
const slot = (0, relay_store_1.addRelaySlot)({
|
|
38
|
+
name,
|
|
39
|
+
baseUrl: options.baseUrl,
|
|
40
|
+
apiKey: options.apiKey
|
|
41
|
+
});
|
|
42
|
+
console.log((0, text_1.bi)(`已添加中转槽位: ${slot.name}`, `Relay slot added: ${slot.name}`));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 列出 OpenAI-compatible relay slot。
|
|
46
|
+
*
|
|
47
|
+
* @returns 无返回值。
|
|
48
|
+
* @throws 当配置读取失败时抛出异常。
|
|
49
|
+
*/
|
|
50
|
+
function handleRelayList() {
|
|
51
|
+
const slots = (0, relay_store_1.listRelaySlots)();
|
|
52
|
+
const route = (0, state_1.getSelectedModelRoute)();
|
|
53
|
+
if (slots.length === 0) {
|
|
54
|
+
console.log((0, text_1.bi)("当前没有中转槽位。", "No relay slots found."));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const slot of slots) {
|
|
58
|
+
const selected = route.mode === "relay_slot" && route.relay_slot_id === slot.id ? "*" : " ";
|
|
59
|
+
const enabled = slot.enabled ? "enabled" : "disabled";
|
|
60
|
+
console.log(`${selected} ${slot.name} ${enabled} ${slot.base_url} key=${maskApiKey(slot.api_key)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 删除 OpenAI-compatible relay slot。
|
|
65
|
+
*
|
|
66
|
+
* @param name relay slot 名称。
|
|
67
|
+
* @returns 无返回值。
|
|
68
|
+
* @throws 当配置写入失败时抛出异常。
|
|
69
|
+
*/
|
|
70
|
+
function handleRelayRemove(name) {
|
|
71
|
+
const removed = (0, relay_store_1.removeRelaySlot)(name);
|
|
72
|
+
if (!removed) {
|
|
73
|
+
console.log((0, text_1.bi)(`未找到中转槽位: ${name}`, `Relay slot not found: ${name}`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log((0, text_1.bi)(`已删除中转槽位: ${name}`, `Relay slot removed: ${name}`));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 重命名 OpenAI-compatible relay slot。
|
|
80
|
+
*
|
|
81
|
+
* @param oldName 原 slot 名称。
|
|
82
|
+
* @param newName 新 slot 名称。
|
|
83
|
+
* @returns 无返回值。
|
|
84
|
+
* @throws 当旧 slot 不存在、新 slot 已存在或写入失败时抛出异常。
|
|
85
|
+
*/
|
|
86
|
+
function handleRelayRename(oldName, newName) {
|
|
87
|
+
const slot = (0, relay_store_1.renameRelaySlot)(oldName, newName);
|
|
88
|
+
console.log((0, text_1.bi)(`已重命名中转槽位: ${oldName} -> ${slot.name}`, `Relay slot renamed: ${oldName} -> ${slot.name}`));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 启用或禁用 OpenAI-compatible relay slot。
|
|
92
|
+
*
|
|
93
|
+
* @param name relay slot 名称。
|
|
94
|
+
* @param enabled 是否启用。
|
|
95
|
+
* @returns 无返回值。
|
|
96
|
+
* @throws 当 slot 不存在或写入失败时抛出异常。
|
|
97
|
+
*/
|
|
98
|
+
function handleRelayEnabled(name, enabled) {
|
|
99
|
+
const slot = (0, relay_store_1.setRelaySlotEnabled)(name, enabled);
|
|
100
|
+
console.log(enabled
|
|
101
|
+
? (0, text_1.bi)(`已启用中转槽位: ${slot.name}`, `Relay slot enabled: ${slot.name}`)
|
|
102
|
+
: (0, text_1.bi)(`已禁用中转槽位: ${slot.name}`, `Relay slot disabled: ${slot.name}`));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 启用 OpenAI-compatible relay slot。
|
|
106
|
+
*
|
|
107
|
+
* @param name relay slot 名称。
|
|
108
|
+
* @returns 无返回值。
|
|
109
|
+
* @throws 当 slot 不存在或写入失败时抛出异常。
|
|
110
|
+
*/
|
|
111
|
+
function handleRelayEnable(name) {
|
|
112
|
+
handleRelayEnabled(name, true);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 禁用 OpenAI-compatible relay slot。
|
|
116
|
+
*
|
|
117
|
+
* @param name relay slot 名称。
|
|
118
|
+
* @returns 无返回值。
|
|
119
|
+
* @throws 当 slot 不存在或写入失败时抛出异常。
|
|
120
|
+
*/
|
|
121
|
+
function handleRelayDisable(name) {
|
|
122
|
+
handleRelayEnabled(name, false);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 将模型请求固定到指定 relay slot。
|
|
126
|
+
*
|
|
127
|
+
* @param name relay slot 名称。
|
|
128
|
+
* @returns 无返回值。
|
|
129
|
+
* @throws 当 relay slot 不存在或状态写入失败时抛出异常。
|
|
130
|
+
*/
|
|
131
|
+
function handleUseRelay(name) {
|
|
132
|
+
const slot = (0, relay_store_1.findRelaySlot)(name);
|
|
133
|
+
if (!slot) {
|
|
134
|
+
throw new Error((0, text_1.bi)(`未找到中转槽位 ${name}`, `Relay slot not found: ${name}`));
|
|
135
|
+
}
|
|
136
|
+
if (!slot.enabled) {
|
|
137
|
+
throw new Error((0, text_1.bi)(`中转槽位 ${name} 已禁用`, `Relay slot is disabled: ${name}`));
|
|
138
|
+
}
|
|
139
|
+
(0, state_1.setSelectedModelRoute)({
|
|
140
|
+
mode: "relay_slot",
|
|
141
|
+
relay_slot_id: slot.id
|
|
142
|
+
});
|
|
143
|
+
console.log((0, text_1.bi)(`模型请求已固定到中转槽位: ${slot.name}`, `Model route fixed to relay slot: ${slot.name}`));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 恢复模型请求到官方账号自动调度池。
|
|
147
|
+
*
|
|
148
|
+
* @returns 无返回值。
|
|
149
|
+
* @throws 当状态写入失败时抛出异常。
|
|
150
|
+
*/
|
|
151
|
+
function handleUseAuthPool() {
|
|
152
|
+
(0, state_1.setSelectedModelRoute)({
|
|
153
|
+
mode: "auth_pool"
|
|
154
|
+
});
|
|
155
|
+
console.log((0, text_1.bi)("模型请求已恢复官方账号池。", "Model route restored to auth pool."));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 输出当前模型出口与 Codex App 登录态选择。
|
|
159
|
+
*
|
|
160
|
+
* @returns 无返回值。
|
|
161
|
+
* @throws 当状态读取失败时抛出异常。
|
|
162
|
+
*/
|
|
163
|
+
function handleCurrent() {
|
|
164
|
+
console.log(`model_route=${formatModelRoute()}`);
|
|
165
|
+
console.log(`codex_auth=${(0, state_1.getSelectedCodexAuthAccountId)() ?? "none"}`);
|
|
166
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.proxyRelaySlot = void 0;
|
|
4
|
+
exports.buildRelayHeaders = buildRelayHeaders;
|
|
5
|
+
exports.sendRelayRequest = sendRelayRequest;
|
|
6
|
+
exports.createRelayProxyService = createRelayProxyService;
|
|
7
|
+
const undici_1 = require("undici");
|
|
8
|
+
/**
|
|
9
|
+
* 构造 OpenAI-compatible relay 上游请求头。
|
|
10
|
+
*
|
|
11
|
+
* 业务含义:
|
|
12
|
+
* 1. 客户端发给本地 cslot 的 Authorization 不代表 relay 凭据,不能透传。
|
|
13
|
+
* 2. relay 模式只使用 relay slot 自己的 api_key 鉴权,并保留其余兼容请求头。
|
|
14
|
+
*
|
|
15
|
+
* @param requestHeaders 客户端发到本地服务的原始请求头。
|
|
16
|
+
* @param apiKey relay slot 的 API key。
|
|
17
|
+
* @param bodyLength 请求体字节长度;无 body 时不写 content-length。
|
|
18
|
+
* @returns 可发往 relay 上游的请求头。
|
|
19
|
+
* @throws 无显式抛出。
|
|
20
|
+
*/
|
|
21
|
+
function buildRelayHeaders(requestHeaders, apiKey, bodyLength) {
|
|
22
|
+
const headers = {};
|
|
23
|
+
for (const [headerName, headerValue] of Object.entries(requestHeaders)) {
|
|
24
|
+
const normalizedName = headerName.toLowerCase();
|
|
25
|
+
if (headerValue == null ||
|
|
26
|
+
normalizedName === "authorization" ||
|
|
27
|
+
normalizedName === "host" ||
|
|
28
|
+
normalizedName === "connection" ||
|
|
29
|
+
normalizedName === "content-length") {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
headers[normalizedName] = Array.isArray(headerValue)
|
|
33
|
+
? headerValue.join(", ")
|
|
34
|
+
: headerValue;
|
|
35
|
+
}
|
|
36
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
37
|
+
if (typeof bodyLength === "number") {
|
|
38
|
+
headers["content-length"] = String(bodyLength);
|
|
39
|
+
}
|
|
40
|
+
headers["user-agent"] = "codex-slot/0.1.1";
|
|
41
|
+
return headers;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 将本地 `/v1/*` 请求解析成 relay base_url 后的相对路径。
|
|
45
|
+
*
|
|
46
|
+
* @param url 本地代理收到的 URL。
|
|
47
|
+
* @returns relay 上游相对 path 与 query;不属于 `/v1/*` 时返回 `null`。
|
|
48
|
+
* @throws URL 解析失败时透传异常。
|
|
49
|
+
*/
|
|
50
|
+
function resolveRelayPath(url) {
|
|
51
|
+
const parsedUrl = new URL(url, "http://127.0.0.1");
|
|
52
|
+
const openAiPrefix = "/v1";
|
|
53
|
+
if (!parsedUrl.pathname.startsWith(`${openAiPrefix}/`)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return `${parsedUrl.pathname.slice(openAiPrefix.length)}${parsedUrl.search}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 提取上游响应中允许透传给客户端的响应头。
|
|
60
|
+
*
|
|
61
|
+
* @param headers 上游响应头对象。
|
|
62
|
+
* @returns 可透传响应头。
|
|
63
|
+
* @throws 无显式抛出。
|
|
64
|
+
*/
|
|
65
|
+
function pickResponseHeaders(headers) {
|
|
66
|
+
const picked = {};
|
|
67
|
+
const contentType = headers["content-type"];
|
|
68
|
+
const cacheControl = headers["cache-control"];
|
|
69
|
+
if (typeof contentType === "string") {
|
|
70
|
+
picked["content-type"] = contentType;
|
|
71
|
+
}
|
|
72
|
+
if (typeof cacheControl === "string") {
|
|
73
|
+
picked["cache-control"] = cacheControl;
|
|
74
|
+
}
|
|
75
|
+
return picked;
|
|
76
|
+
}
|
|
77
|
+
function buildSendResult(statusCode, payload, headers) {
|
|
78
|
+
return {
|
|
79
|
+
type: "send",
|
|
80
|
+
statusCode,
|
|
81
|
+
payload,
|
|
82
|
+
headers
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 向 OpenAI-compatible relay 上游发送一次请求。
|
|
87
|
+
*
|
|
88
|
+
* @param options relay 请求参数。
|
|
89
|
+
* @returns undici 上游响应对象。
|
|
90
|
+
* @throws 当网络层或 undici 请求失败时透传底层异常。
|
|
91
|
+
*/
|
|
92
|
+
async function sendRelayRequest(options) {
|
|
93
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
94
|
+
const pathWithQuery = options.pathWithQuery.startsWith("/")
|
|
95
|
+
? options.pathWithQuery
|
|
96
|
+
: `/${options.pathWithQuery}`;
|
|
97
|
+
return await (0, undici_1.request)(`${baseUrl}${pathWithQuery}`, {
|
|
98
|
+
method: options.method,
|
|
99
|
+
headers: options.headers,
|
|
100
|
+
body: options.body && options.body.length > 0 ? options.body : undefined
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 创建 relay 模型代理服务。
|
|
105
|
+
*
|
|
106
|
+
* 业务含义:
|
|
107
|
+
* 1. relay slot 是手动固定的模型出口,不参与官方账号自动调度。
|
|
108
|
+
* 2. relay 上游返回 401/429/5xx 时原样返回给客户端,不回退到官方账号。
|
|
109
|
+
*
|
|
110
|
+
* @param overrides 可选依赖覆盖项。
|
|
111
|
+
* @returns relay 代理服务实例。
|
|
112
|
+
* @throws 无显式抛出。
|
|
113
|
+
*/
|
|
114
|
+
function createRelayProxyService(overrides) {
|
|
115
|
+
const dependencies = {
|
|
116
|
+
sendRelayRequest,
|
|
117
|
+
...overrides
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
async proxyRelaySlot(options) {
|
|
121
|
+
const pathWithQuery = resolveRelayPath(options.request.url);
|
|
122
|
+
if (!pathWithQuery) {
|
|
123
|
+
return buildSendResult(404, {
|
|
124
|
+
error: {
|
|
125
|
+
message: "Unsupported relay proxy path",
|
|
126
|
+
type: "unsupported_relay_proxy_path"
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const bodyLength = options.request.body && options.request.body.length > 0
|
|
131
|
+
? options.request.body.length
|
|
132
|
+
: undefined;
|
|
133
|
+
let upstream;
|
|
134
|
+
try {
|
|
135
|
+
upstream = await dependencies.sendRelayRequest({
|
|
136
|
+
baseUrl: options.slot.base_url,
|
|
137
|
+
method: options.request.method.toUpperCase(),
|
|
138
|
+
pathWithQuery,
|
|
139
|
+
headers: buildRelayHeaders(options.request.headers, options.slot.api_key, bodyLength),
|
|
140
|
+
body: options.request.body
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return buildSendResult(502, {
|
|
145
|
+
error: {
|
|
146
|
+
message: error instanceof Error ? error.message : String(error),
|
|
147
|
+
type: "relay_request_failed"
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
type: "proxy",
|
|
153
|
+
statusCode: upstream.statusCode,
|
|
154
|
+
headers: pickResponseHeaders(upstream.headers),
|
|
155
|
+
body: upstream.body
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
exports.proxyRelaySlot = createRelayProxyService().proxyRelaySlot;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listRelaySlots = listRelaySlots;
|
|
4
|
+
exports.findRelaySlot = findRelaySlot;
|
|
5
|
+
exports.addRelaySlot = addRelaySlot;
|
|
6
|
+
exports.removeRelaySlot = removeRelaySlot;
|
|
7
|
+
exports.renameRelaySlot = renameRelaySlot;
|
|
8
|
+
exports.setRelaySlotEnabled = setRelaySlotEnabled;
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
const state_1 = require("./state");
|
|
11
|
+
const text_1 = require("./text");
|
|
12
|
+
/**
|
|
13
|
+
* 列出所有 OpenAI-compatible relay 槽位。
|
|
14
|
+
*
|
|
15
|
+
* @returns 当前配置中的 relay slot 列表。
|
|
16
|
+
* @throws 当配置读取失败时抛出异常。
|
|
17
|
+
*/
|
|
18
|
+
function listRelaySlots() {
|
|
19
|
+
return (0, config_1.loadConfig)().relay_slots;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 查找指定 relay slot。
|
|
23
|
+
*
|
|
24
|
+
* @param slotId relay slot 标识。
|
|
25
|
+
* @returns 命中时返回 slot;不存在时返回 `null`。
|
|
26
|
+
* @throws 当配置读取失败时抛出异常。
|
|
27
|
+
*/
|
|
28
|
+
function findRelaySlot(slotId) {
|
|
29
|
+
return listRelaySlots().find((item) => item.id === slotId) ?? null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 新增一个 OpenAI-compatible relay slot。
|
|
33
|
+
*
|
|
34
|
+
* @param input relay slot 配置。
|
|
35
|
+
* @returns 新增后的 relay slot。
|
|
36
|
+
* @throws 当同名 slot 已存在或配置写入失败时抛出异常。
|
|
37
|
+
*/
|
|
38
|
+
function addRelaySlot(input) {
|
|
39
|
+
const config = (0, config_1.loadConfig)();
|
|
40
|
+
if (config.relay_slots.some((item) => item.id === input.name)) {
|
|
41
|
+
throw new Error((0, text_1.bi)(`中转槽位 ${input.name} 已存在`, `Relay slot already exists: ${input.name}`));
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
new URL(input.baseUrl);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error((0, text_1.bi)(`中转 base_url 非法: ${input.baseUrl}`, `Invalid relay base_url: ${input.baseUrl}`));
|
|
48
|
+
}
|
|
49
|
+
const slot = {
|
|
50
|
+
id: input.name,
|
|
51
|
+
name: input.name,
|
|
52
|
+
base_url: input.baseUrl,
|
|
53
|
+
api_key: input.apiKey,
|
|
54
|
+
enabled: true,
|
|
55
|
+
imported_at: new Date().toISOString()
|
|
56
|
+
};
|
|
57
|
+
config.relay_slots.push(slot);
|
|
58
|
+
(0, config_1.saveConfig)(config);
|
|
59
|
+
return slot;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 删除指定 relay slot,并在当前模型出口指向它时恢复官方账号池。
|
|
63
|
+
*
|
|
64
|
+
* @param slotId relay slot 标识。
|
|
65
|
+
* @returns 被删除的 slot;不存在时返回 `null`。
|
|
66
|
+
* @throws 当配置或状态写入失败时抛出异常。
|
|
67
|
+
*/
|
|
68
|
+
function removeRelaySlot(slotId) {
|
|
69
|
+
const config = (0, config_1.loadConfig)();
|
|
70
|
+
const index = config.relay_slots.findIndex((item) => item.id === slotId);
|
|
71
|
+
if (index < 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const [removed] = config.relay_slots.splice(index, 1);
|
|
75
|
+
(0, config_1.saveConfig)(config);
|
|
76
|
+
const route = (0, state_1.getSelectedModelRoute)();
|
|
77
|
+
if (route.mode === "relay_slot" && route.relay_slot_id === slotId) {
|
|
78
|
+
(0, state_1.setSelectedModelRoute)({ mode: "auth_pool" });
|
|
79
|
+
}
|
|
80
|
+
return removed;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 重命名 relay slot,并迁移当前模型出口选择。
|
|
84
|
+
*
|
|
85
|
+
* @param oldName 原 slot 标识。
|
|
86
|
+
* @param newName 新 slot 标识。
|
|
87
|
+
* @returns 重命名后的 relay slot。
|
|
88
|
+
* @throws 当旧 slot 不存在、新 slot 已存在或写入失败时抛出异常。
|
|
89
|
+
*/
|
|
90
|
+
function renameRelaySlot(oldName, newName) {
|
|
91
|
+
const config = (0, config_1.loadConfig)();
|
|
92
|
+
const index = config.relay_slots.findIndex((item) => item.id === oldName);
|
|
93
|
+
if (index < 0) {
|
|
94
|
+
throw new Error((0, text_1.bi)(`未找到中转槽位 ${oldName}`, `Relay slot not found: ${oldName}`));
|
|
95
|
+
}
|
|
96
|
+
if (config.relay_slots.some((item) => item.id === newName)) {
|
|
97
|
+
throw new Error((0, text_1.bi)(`中转槽位 ${newName} 已存在`, `Relay slot already exists: ${newName}`));
|
|
98
|
+
}
|
|
99
|
+
const renamed = {
|
|
100
|
+
...config.relay_slots[index],
|
|
101
|
+
id: newName,
|
|
102
|
+
name: newName
|
|
103
|
+
};
|
|
104
|
+
config.relay_slots[index] = renamed;
|
|
105
|
+
(0, config_1.saveConfig)(config);
|
|
106
|
+
(0, state_1.updateState)((state) => {
|
|
107
|
+
if (state.selected_model_route?.mode === "relay_slot" &&
|
|
108
|
+
state.selected_model_route.relay_slot_id === oldName) {
|
|
109
|
+
state.selected_model_route = {
|
|
110
|
+
mode: "relay_slot",
|
|
111
|
+
relay_slot_id: newName
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return renamed;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 更新 relay slot 启用状态。
|
|
119
|
+
*
|
|
120
|
+
* @param slotId relay slot 标识。
|
|
121
|
+
* @param enabled 是否启用。
|
|
122
|
+
* @returns 更新后的 relay slot。
|
|
123
|
+
* @throws 当 slot 不存在或配置写入失败时抛出异常。
|
|
124
|
+
*/
|
|
125
|
+
function setRelaySlotEnabled(slotId, enabled) {
|
|
126
|
+
const config = (0, config_1.loadConfig)();
|
|
127
|
+
const index = config.relay_slots.findIndex((item) => item.id === slotId);
|
|
128
|
+
if (index < 0) {
|
|
129
|
+
throw new Error((0, text_1.bi)(`未找到中转槽位 ${slotId}`, `Relay slot not found: ${slotId}`));
|
|
130
|
+
}
|
|
131
|
+
const updated = {
|
|
132
|
+
...config.relay_slots[index],
|
|
133
|
+
enabled
|
|
134
|
+
};
|
|
135
|
+
config.relay_slots[index] = updated;
|
|
136
|
+
(0, config_1.saveConfig)(config);
|
|
137
|
+
return updated;
|
|
138
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.startServer = startServer;
|
|
|
7
7
|
const fastify_1 = __importDefault(require("fastify"));
|
|
8
8
|
const config_1 = require("./config");
|
|
9
9
|
const backend_proxy_service_1 = require("./backend-proxy-service");
|
|
10
|
-
const
|
|
10
|
+
const model_proxy_dispatcher_1 = require("./model-proxy-dispatcher");
|
|
11
11
|
const status_1 = require("./status");
|
|
12
12
|
const scheduler_1 = require("./scheduler");
|
|
13
13
|
const usage_sync_1 = require("./usage-sync");
|
|
@@ -91,7 +91,7 @@ async function startServer(port) {
|
|
|
91
91
|
});
|
|
92
92
|
const codexProxyHandler = async (request, reply) => {
|
|
93
93
|
const requestBody = await readRawRequestBody(request.body);
|
|
94
|
-
const result = await (0,
|
|
94
|
+
const result = await (0, model_proxy_dispatcher_1.proxyModelWithRoute)({
|
|
95
95
|
method: request.method,
|
|
96
96
|
url: request.url,
|
|
97
97
|
headers: request.headers,
|
package/dist/state.js
CHANGED
|
@@ -17,6 +17,8 @@ exports.clearUsageRefreshError = clearUsageRefreshError;
|
|
|
17
17
|
exports.getUsageRefreshError = getUsageRefreshError;
|
|
18
18
|
exports.getSelectedCodexAuthAccountId = getSelectedCodexAuthAccountId;
|
|
19
19
|
exports.setSelectedCodexAuthAccountId = setSelectedCodexAuthAccountId;
|
|
20
|
+
exports.getSelectedModelRoute = getSelectedModelRoute;
|
|
21
|
+
exports.setSelectedModelRoute = setSelectedModelRoute;
|
|
20
22
|
exports.getManagedCodexConfigState = getManagedCodexConfigState;
|
|
21
23
|
exports.getManagedCodexAuthState = getManagedCodexAuthState;
|
|
22
24
|
exports.setManagedCodexConfigState = setManagedCodexConfigState;
|
|
@@ -44,6 +46,9 @@ function createDefaultState() {
|
|
|
44
46
|
return {
|
|
45
47
|
state_version: STATE_SCHEMA_VERSION,
|
|
46
48
|
selected_codex_auth_account_id: null,
|
|
49
|
+
selected_model_route: {
|
|
50
|
+
mode: "auth_pool"
|
|
51
|
+
},
|
|
47
52
|
account_blocks: {},
|
|
48
53
|
usage_cache: {},
|
|
49
54
|
usage_refresh_errors: {},
|
|
@@ -64,6 +69,7 @@ function normalizeState(parsed) {
|
|
|
64
69
|
return {
|
|
65
70
|
state_version: STATE_SCHEMA_VERSION,
|
|
66
71
|
selected_codex_auth_account_id: parsed?.selected_codex_auth_account_id ?? defaults.selected_codex_auth_account_id,
|
|
72
|
+
selected_model_route: normalizeModelRouteSelection(parsed?.selected_model_route ?? defaults.selected_model_route),
|
|
67
73
|
account_blocks: parsed?.account_blocks ?? defaults.account_blocks,
|
|
68
74
|
usage_cache: parsed?.usage_cache ?? defaults.usage_cache,
|
|
69
75
|
usage_refresh_errors: parsed?.usage_refresh_errors ?? defaults.usage_refresh_errors,
|
|
@@ -72,6 +78,24 @@ function normalizeState(parsed) {
|
|
|
72
78
|
managed_codex_config: parsed?.managed_codex_config ?? defaults.managed_codex_config
|
|
73
79
|
};
|
|
74
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* 归一化当前模型出口选择,避免历史 state 或手工编辑内容让代理入口进入未知模式。
|
|
83
|
+
*
|
|
84
|
+
* @param value state 文件中记录的模型出口选择。
|
|
85
|
+
* @returns 可安全使用的模型出口选择;非法内容回退到官方账号池。
|
|
86
|
+
* @throws 无显式抛出。
|
|
87
|
+
*/
|
|
88
|
+
function normalizeModelRouteSelection(value) {
|
|
89
|
+
if (value?.mode === "relay_slot" && value.relay_slot_id) {
|
|
90
|
+
return {
|
|
91
|
+
mode: "relay_slot",
|
|
92
|
+
relay_slot_id: value.relay_slot_id
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
mode: "auth_pool"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
75
99
|
/**
|
|
76
100
|
* 读取 cslot 的本地运行状态;文件不存在时返回默认空状态。
|
|
77
101
|
*
|
|
@@ -261,6 +285,32 @@ function setSelectedCodexAuthAccountId(accountId) {
|
|
|
261
285
|
state.selected_codex_auth_account_id = accountId;
|
|
262
286
|
});
|
|
263
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* 读取当前模型请求出口选择。
|
|
290
|
+
*
|
|
291
|
+
* 业务含义:
|
|
292
|
+
* 1. `auth_pool` 表示 `/v1/*` 继续走官方账号自动调度。
|
|
293
|
+
* 2. `relay_slot` 表示 `/v1/*` 固定走指定 OpenAI-compatible 中转槽位。
|
|
294
|
+
* 3. 该选择不影响 Codex App 主登录态与 `/backend-api/*` 插件链路。
|
|
295
|
+
*
|
|
296
|
+
* @returns 当前模型出口选择。
|
|
297
|
+
* @throws 当 state 文件读取或解析失败时透传底层异常。
|
|
298
|
+
*/
|
|
299
|
+
function getSelectedModelRoute() {
|
|
300
|
+
return normalizeModelRouteSelection(loadState().selected_model_route);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 保存当前模型请求出口选择。
|
|
304
|
+
*
|
|
305
|
+
* @param selection 模型出口选择;`null` 表示恢复官方账号池。
|
|
306
|
+
* @returns 无返回值。
|
|
307
|
+
* @throws 当 state 文件写入失败时透传底层异常。
|
|
308
|
+
*/
|
|
309
|
+
function setSelectedModelRoute(selection) {
|
|
310
|
+
updateState((state) => {
|
|
311
|
+
state.selected_model_route = normalizeModelRouteSelection(selection);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
264
314
|
/**
|
|
265
315
|
* 读取当前记录的 Codex `config.toml` 接管快照。
|
|
266
316
|
*
|
package/dist/status-command.js
CHANGED
|
@@ -245,6 +245,12 @@ function resolveInitialCursorIndex(accounts, statuses, selectedAuthAccountId) {
|
|
|
245
245
|
}
|
|
246
246
|
return 0;
|
|
247
247
|
}
|
|
248
|
+
function buildInteractiveItems(accounts, relaySlots) {
|
|
249
|
+
return [
|
|
250
|
+
...accounts.map((account) => ({ type: "account", id: account.id })),
|
|
251
|
+
...relaySlots.map((slot) => ({ type: "relay", id: slot.id }))
|
|
252
|
+
];
|
|
253
|
+
}
|
|
248
254
|
/**
|
|
249
255
|
* 将状态面板中选中的账号立即应用为 Codex App 主登录态。
|
|
250
256
|
*
|
|
@@ -286,15 +292,27 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
286
292
|
node_readline_1.default.emitKeypressEvents(stdin);
|
|
287
293
|
stdin.setRawMode?.(true);
|
|
288
294
|
const accountsFromConfig = (0, account_service_1.listAccounts)();
|
|
289
|
-
|
|
290
|
-
|
|
295
|
+
const latestSnapshotForRelays = (0, status_service_1.getStatusSnapshot)();
|
|
296
|
+
const relaySlotsFromConfig = latestSnapshotForRelays.relaySlots;
|
|
297
|
+
if (accountsFromConfig.length === 0 && relaySlotsFromConfig.length === 0) {
|
|
298
|
+
console.log((0, text_1.bi)("当前没有已录入账号或中转槽位。", "No managed accounts or relay slots found."));
|
|
291
299
|
stdin.setRawMode?.(false);
|
|
292
300
|
return;
|
|
293
301
|
}
|
|
294
302
|
const accounts = [...accountsFromConfig].sort((left, right) => left.name.localeCompare(right.name));
|
|
303
|
+
const relaySlots = [...relaySlotsFromConfig].sort((left, right) => left.name.localeCompare(right.name));
|
|
295
304
|
let selectedAuthAccountId = (0, state_1.getSelectedCodexAuthAccountId)();
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
const initialItems = buildInteractiveItems(accounts, relaySlots);
|
|
306
|
+
const selectedModelRoute = (0, state_1.getSelectedModelRoute)();
|
|
307
|
+
const initialAccountCursor = resolveInitialCursorIndex(accounts, initialStatuses ?? (0, status_1.collectAccountStatuses)(), selectedAuthAccountId);
|
|
308
|
+
const selectedRelayIndex = selectedModelRoute.mode === "relay_slot"
|
|
309
|
+
? relaySlots.findIndex((slot) => slot.id === selectedModelRoute.relay_slot_id)
|
|
310
|
+
: -1;
|
|
311
|
+
let cursor = selectedRelayIndex >= 0
|
|
312
|
+
? accounts.length + selectedRelayIndex
|
|
313
|
+
: Math.min(initialAccountCursor, Math.max(0, initialItems.length - 1));
|
|
314
|
+
let accountChanged = false;
|
|
315
|
+
let relayChanged = false;
|
|
298
316
|
enterInteractiveScreen();
|
|
299
317
|
return await new Promise((resolve) => {
|
|
300
318
|
let closed = false;
|
|
@@ -304,7 +322,9 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
304
322
|
const screenWidth = process.stdout.columns ?? 80;
|
|
305
323
|
const styled = shouldUseAnsiStyle();
|
|
306
324
|
const latestSnapshot = (0, status_service_1.getStatusSnapshot)();
|
|
307
|
-
const
|
|
325
|
+
const items = buildInteractiveItems(accounts, relaySlots);
|
|
326
|
+
const currentSelection = items[cursor] ?? null;
|
|
327
|
+
const statusSource = accountChanged ? latestSnapshot.statuses : (initialStatuses ?? latestSnapshot.statuses);
|
|
308
328
|
const statusById = new Map(statusSource.map((item) => [item.id, item]));
|
|
309
329
|
const autoSelectedId = (0, scheduler_1.pickBestAccount)()?.account.id ?? null;
|
|
310
330
|
const summary = (0, status_1.summarizeAccountStatuses)(statusSource);
|
|
@@ -321,7 +341,20 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
321
341
|
};
|
|
322
342
|
})
|
|
323
343
|
.filter((item) => item !== null);
|
|
324
|
-
const
|
|
344
|
+
const displayRelays = relaySlots.map((slot) => {
|
|
345
|
+
const selected = latestSnapshot.modelRoute.mode === "relay_slot" &&
|
|
346
|
+
latestSnapshot.modelRoute.relay_slot_id === slot.id;
|
|
347
|
+
return {
|
|
348
|
+
...slot,
|
|
349
|
+
name: selected ? `${slot.name}*` : slot.name
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
const currentAccount = currentSelection?.type === "account"
|
|
353
|
+
? displayStatuses.find((item) => item.id === currentSelection.id) ?? null
|
|
354
|
+
: null;
|
|
355
|
+
const currentRelay = currentSelection?.type === "relay"
|
|
356
|
+
? displayRelays.find((item) => item.id === currentSelection.id) ?? null
|
|
357
|
+
: null;
|
|
325
358
|
const wideLayout = screenWidth >= 104;
|
|
326
359
|
const leftWidth = wideLayout ? Math.max(68, Math.floor(screenWidth * 0.64)) : screenWidth;
|
|
327
360
|
const rightWidth = wideLayout ? Math.max(28, screenWidth - leftWidth - 3) : screenWidth;
|
|
@@ -333,41 +366,71 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
333
366
|
styled,
|
|
334
367
|
selectorColumn: {
|
|
335
368
|
enabledById: Object.fromEntries(accounts.map((account) => [account.id, account.enabled])),
|
|
336
|
-
cursorAccountId:
|
|
369
|
+
cursorAccountId: currentSelection?.type === "account" ? currentSelection.id : null
|
|
337
370
|
}
|
|
338
371
|
}).split("\n")
|
|
339
372
|
];
|
|
373
|
+
const relayLines = [
|
|
374
|
+
renderSectionHeader("relays", leftWidth, styled),
|
|
375
|
+
...(displayRelays.length > 0
|
|
376
|
+
? (0, status_1.renderRelayStatusTable)(displayRelays, {
|
|
377
|
+
compact: true,
|
|
378
|
+
maxWidth: leftWidth,
|
|
379
|
+
styled,
|
|
380
|
+
selectorColumn: {
|
|
381
|
+
enabledById: Object.fromEntries(relaySlots.map((slot) => [slot.id, slot.enabled])),
|
|
382
|
+
cursorRelayId: currentSelection?.type === "relay" ? currentSelection.id : null
|
|
383
|
+
}
|
|
384
|
+
}).split("\n")
|
|
385
|
+
: ["-"])
|
|
386
|
+
];
|
|
387
|
+
const currentDetails = currentSelection?.type === "relay"
|
|
388
|
+
? (0, status_1.renderRelayStatusDetails)(currentRelay, { maxWidth: rightWidth, header: false }).split("\n")
|
|
389
|
+
: (0, status_1.renderStatusDetails)(currentAccount, { maxWidth: rightWidth, header: false }).split("\n");
|
|
340
390
|
const sideLines = [
|
|
341
391
|
renderSectionHeader("current", rightWidth, styled),
|
|
342
|
-
...
|
|
392
|
+
...currentDetails,
|
|
343
393
|
"",
|
|
344
394
|
renderSectionHeader("summary", rightWidth, styled),
|
|
345
395
|
renderSummaryLine(summary, rightWidth < 42, styled),
|
|
396
|
+
`model_route=${latestSnapshot.modelRouteLabel}`,
|
|
346
397
|
`scheduler=${latestSnapshot.selectedName ?? "none"}`,
|
|
347
398
|
`codex_auth=${selectedAuthAccountId ?? "none"}`,
|
|
399
|
+
`relay_slots=${latestSnapshot.relaySlots.length}`,
|
|
348
400
|
...(refreshStatusText ? [`refresh=${refreshStatusText}`] : []),
|
|
349
401
|
"",
|
|
350
402
|
renderSectionHeader("help", rightWidth, styled),
|
|
351
|
-
"↑/↓ move Space toggle a app-auth c clear r refresh Enter/q exit"
|
|
403
|
+
"↑/↓ move Space toggle a app-auth m model-route c clear r refresh Enter/q exit"
|
|
404
|
+
];
|
|
405
|
+
const leftLines = [
|
|
406
|
+
...accountLines,
|
|
407
|
+
"",
|
|
408
|
+
...relayLines
|
|
352
409
|
];
|
|
353
410
|
if (wideLayout) {
|
|
354
|
-
renderInteractiveScreen(renderColumns(
|
|
411
|
+
renderInteractiveScreen(renderColumns(leftLines, sideLines, 3));
|
|
355
412
|
return;
|
|
356
413
|
}
|
|
357
414
|
renderInteractiveScreen([
|
|
358
|
-
...
|
|
415
|
+
...leftLines,
|
|
359
416
|
"",
|
|
360
417
|
renderDivider(screenWidth, styled),
|
|
361
418
|
...sideLines
|
|
362
419
|
]);
|
|
363
420
|
};
|
|
364
421
|
const applyChanges = () => {
|
|
365
|
-
if (!
|
|
422
|
+
if (!accountChanged && !relayChanged) {
|
|
366
423
|
return;
|
|
367
424
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
425
|
+
if (accountChanged) {
|
|
426
|
+
(0, status_service_1.persistAccountEnabledState)(accounts);
|
|
427
|
+
accountChanged = false;
|
|
428
|
+
initialStatuses = (0, status_1.collectAccountStatuses)();
|
|
429
|
+
}
|
|
430
|
+
if (relayChanged) {
|
|
431
|
+
(0, status_service_1.persistRelayEnabledState)(relaySlots);
|
|
432
|
+
relayChanged = false;
|
|
433
|
+
}
|
|
371
434
|
};
|
|
372
435
|
const exitInteractive = () => {
|
|
373
436
|
if (closed) {
|
|
@@ -392,7 +455,7 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
392
455
|
return;
|
|
393
456
|
}
|
|
394
457
|
if (key.name === "down") {
|
|
395
|
-
const nextCursor = Math.min(accounts.length - 1, cursor + 1);
|
|
458
|
+
const nextCursor = Math.min(buildInteractiveItems(accounts, relaySlots).length - 1, cursor + 1);
|
|
396
459
|
if (nextCursor !== cursor) {
|
|
397
460
|
cursor = nextCursor;
|
|
398
461
|
render();
|
|
@@ -400,14 +463,33 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
400
463
|
return;
|
|
401
464
|
}
|
|
402
465
|
if (key.name === "space") {
|
|
403
|
-
|
|
404
|
-
|
|
466
|
+
const item = buildInteractiveItems(accounts, relaySlots)[cursor];
|
|
467
|
+
if (item?.type === "account") {
|
|
468
|
+
const account = accounts.find((candidate) => candidate.id === item.id);
|
|
469
|
+
if (account) {
|
|
470
|
+
account.enabled = !account.enabled;
|
|
471
|
+
accountChanged = true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else if (item?.type === "relay") {
|
|
475
|
+
const slot = relaySlots.find((candidate) => candidate.id === item.id);
|
|
476
|
+
if (slot) {
|
|
477
|
+
slot.enabled = !slot.enabled;
|
|
478
|
+
relayChanged = true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
405
481
|
applyChanges();
|
|
406
482
|
render();
|
|
407
483
|
return;
|
|
408
484
|
}
|
|
409
485
|
if (key.name === "a") {
|
|
410
|
-
const
|
|
486
|
+
const item = buildInteractiveItems(accounts, relaySlots)[cursor];
|
|
487
|
+
if (item?.type !== "account") {
|
|
488
|
+
refreshStatusText = "app-auth requires account";
|
|
489
|
+
render();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const account = accounts.find((candidate) => candidate.id === item.id);
|
|
411
493
|
if (!account) {
|
|
412
494
|
return;
|
|
413
495
|
}
|
|
@@ -422,6 +504,33 @@ async function handleInteractiveToggle(initialStatuses) {
|
|
|
422
504
|
render();
|
|
423
505
|
return;
|
|
424
506
|
}
|
|
507
|
+
if (key.name === "m") {
|
|
508
|
+
const item = buildInteractiveItems(accounts, relaySlots)[cursor];
|
|
509
|
+
if (item?.type === "relay") {
|
|
510
|
+
const slot = relaySlots.find((candidate) => candidate.id === item.id);
|
|
511
|
+
if (!slot) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!slot.enabled) {
|
|
515
|
+
refreshStatusText = `relay_disabled=${slot.id}`;
|
|
516
|
+
render();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
(0, state_1.setSelectedModelRoute)({
|
|
520
|
+
mode: "relay_slot",
|
|
521
|
+
relay_slot_id: slot.id
|
|
522
|
+
});
|
|
523
|
+
refreshStatusText = `model_route=relay:${slot.id}`;
|
|
524
|
+
render();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
(0, state_1.setSelectedModelRoute)({
|
|
528
|
+
mode: "auth_pool"
|
|
529
|
+
});
|
|
530
|
+
refreshStatusText = "model_route=auth_pool";
|
|
531
|
+
render();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
425
534
|
if (key.name === "c") {
|
|
426
535
|
selectedAuthAccountId = null;
|
|
427
536
|
(0, state_1.setSelectedCodexAuthAccountId)(null);
|
|
@@ -485,8 +594,21 @@ async function handleStatus(options) {
|
|
|
485
594
|
name: `${item.name}${item.id === (0, scheduler_1.pickBestAccount)()?.account.id ? "*" : ""}${item.id === snapshot.codexAuthAccountId ? "@" : ""}`
|
|
486
595
|
}));
|
|
487
596
|
console.log((0, status_1.renderStatusTable)(displayStatuses));
|
|
597
|
+
if (snapshot.relaySlots.length > 0) {
|
|
598
|
+
const displayRelays = snapshot.relaySlots.map((slot) => ({
|
|
599
|
+
...slot,
|
|
600
|
+
name: snapshot.modelRoute.mode === "relay_slot" &&
|
|
601
|
+
snapshot.modelRoute.relay_slot_id === slot.id
|
|
602
|
+
? `${slot.name}*`
|
|
603
|
+
: slot.name
|
|
604
|
+
}));
|
|
605
|
+
console.log("");
|
|
606
|
+
console.log((0, status_1.renderRelayStatusTable)(displayRelays));
|
|
607
|
+
}
|
|
488
608
|
console.log("");
|
|
489
609
|
console.log(`available=${snapshot.summary.available} 5h_limited=${snapshot.summary.fiveHourLimited} weekly_limited=${snapshot.summary.weeklyLimited}`);
|
|
610
|
+
console.log(`model_route=${snapshot.modelRouteLabel}`);
|
|
490
611
|
console.log(`scheduler=${snapshot.selectedName ?? "none"}`);
|
|
491
612
|
console.log(`codex_auth=${snapshot.codexAuthAccountId ?? "none"}`);
|
|
613
|
+
console.log(`relay_slots=${snapshot.relaySlots.length}`);
|
|
492
614
|
}
|
package/dist/status.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.collectAccountStatuses = collectAccountStatuses;
|
|
|
4
4
|
exports.summarizeAccountStatuses = summarizeAccountStatuses;
|
|
5
5
|
exports.renderStatusTable = renderStatusTable;
|
|
6
6
|
exports.renderStatusDetails = renderStatusDetails;
|
|
7
|
+
exports.renderRelayStatusTable = renderRelayStatusTable;
|
|
8
|
+
exports.renderRelayStatusDetails = renderRelayStatusDetails;
|
|
7
9
|
const config_1 = require("./config");
|
|
8
10
|
const account_store_1 = require("./account-store");
|
|
9
11
|
const state_1 = require("./state");
|
|
@@ -554,3 +556,73 @@ function renderStatusDetails(item, options) {
|
|
|
554
556
|
}
|
|
555
557
|
return lines.join("\n");
|
|
556
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* 将 relay slot 状态渲染为适合终端输出的表格文本。
|
|
561
|
+
*
|
|
562
|
+
* @param slots 待展示的 relay slot 列表。
|
|
563
|
+
* @param options 渲染选项;交互模式下可传入选择列配置。
|
|
564
|
+
* @returns 可直接打印到终端的表格字符串。
|
|
565
|
+
* @throws 无显式抛出。
|
|
566
|
+
*/
|
|
567
|
+
function renderRelayStatusTable(slots, options) {
|
|
568
|
+
const selectorColumn = options?.selectorColumn;
|
|
569
|
+
const compact = options?.compact ?? false;
|
|
570
|
+
const maxWidth = options?.maxWidth ?? Number.POSITIVE_INFINITY;
|
|
571
|
+
const compactHeader = maxWidth < 68;
|
|
572
|
+
const relayHeader = compactHeader ? "ID" : "RELAY";
|
|
573
|
+
const statusHeader = compactHeader ? "ST" : "STATUS";
|
|
574
|
+
const relayWidth = Math.max(getDisplayWidth(relayHeader), ...slots.map((item) => getDisplayWidth(item.name)));
|
|
575
|
+
const statusWidth = compactHeader ? 8 : 10;
|
|
576
|
+
const fixedWidth = (selectorColumn ? 4 + 2 : 0) + relayWidth + 2 + statusWidth + 2;
|
|
577
|
+
const baseUrlWidth = Number.isFinite(maxWidth)
|
|
578
|
+
? Math.max(12, Math.floor(maxWidth) - fixedWidth)
|
|
579
|
+
: Math.max(getDisplayWidth("BASE_URL"), ...slots.map((item) => getDisplayWidth(item.base_url)));
|
|
580
|
+
const rows = [
|
|
581
|
+
[
|
|
582
|
+
...(selectorColumn ? [" "] : []),
|
|
583
|
+
relayHeader,
|
|
584
|
+
statusHeader,
|
|
585
|
+
"BASE_URL"
|
|
586
|
+
]
|
|
587
|
+
];
|
|
588
|
+
for (const slot of slots) {
|
|
589
|
+
const selectorCell = selectorColumn
|
|
590
|
+
? `${selectorColumn.cursorRelayId === slot.id ? ">" : " "}[${selectorColumn.enabledById[slot.id] ? "x" : " "}]`
|
|
591
|
+
: null;
|
|
592
|
+
const status = slot.enabled ? "enabled" : "disabled";
|
|
593
|
+
rows.push([
|
|
594
|
+
...(selectorCell ? [selectorCell] : []),
|
|
595
|
+
styleNameCell(truncateCell(slot.name, relayWidth), options?.styled ?? false),
|
|
596
|
+
truncateCell(status, statusWidth),
|
|
597
|
+
truncateCell(slot.base_url, baseUrlWidth)
|
|
598
|
+
]);
|
|
599
|
+
}
|
|
600
|
+
const widths = rows[0].map((_, columnIndex) => Math.max(...rows.map((row) => getDisplayWidth(row[columnIndex]))));
|
|
601
|
+
return rows
|
|
602
|
+
.map((row) => row.map((cell, index) => padCell(cell, widths[index])).join(" "))
|
|
603
|
+
.join("\n");
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* 将当前选中 relay slot 渲染为紧凑详情区。
|
|
607
|
+
*
|
|
608
|
+
* @param slot 当前选中的 relay slot;为空时返回占位提示。
|
|
609
|
+
* @param options 详情区渲染选项。
|
|
610
|
+
* @returns 适合直接打印的详情区文本。
|
|
611
|
+
* @throws 无显式抛出。
|
|
612
|
+
*/
|
|
613
|
+
function renderRelayStatusDetails(slot, options) {
|
|
614
|
+
const includeHeader = options?.header ?? true;
|
|
615
|
+
if (!slot) {
|
|
616
|
+
return [includeHeader ? "[ relay ]" : "slot -", includeHeader ? "slot -" : ""]
|
|
617
|
+
.filter((line) => line.length > 0)
|
|
618
|
+
.join("\n");
|
|
619
|
+
}
|
|
620
|
+
const maxWidth = options?.maxWidth ?? Number.POSITIVE_INFINITY;
|
|
621
|
+
return [
|
|
622
|
+
...(includeHeader ? ["[ relay ]"] : []),
|
|
623
|
+
formatDetailLine("slot", slot.name, maxWidth),
|
|
624
|
+
formatDetailLine("status", slot.enabled ? "enabled" : "disabled", maxWidth),
|
|
625
|
+
formatDetailLine("base", slot.base_url, maxWidth),
|
|
626
|
+
formatDetailLine("key", "********", maxWidth)
|
|
627
|
+
].join("\n");
|
|
628
|
+
}
|