magpie-gemini-web 0.5.2 → 0.5.3
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.en.md +7 -2
- package/README.md +7 -2
- package/lib/studio-native.mjs +268 -37
- package/lib/studio.mjs +72 -18
- package/package.json +1 -1
- package/scripts/studio.mjs +17 -26
package/README.en.md
CHANGED
|
@@ -82,12 +82,17 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
82
82
|
| `stripCitations` | `true` | `gemini-web` only | Strip `[cite: 1]` and `[cite_start]` markers from replies and tool arguments (not supported on AI Studio) |
|
|
83
83
|
| `baseURL` | `http://127.0.0.1:8083/v1` | `gemini-web` only | Default Gemini Web backend URL |
|
|
84
84
|
| `contextLimit` / `outputLimit` | `1000000` / `8192` | `gemini-web` only | Context and output limits reported to clients |
|
|
85
|
-
| `studio.proxy` | none | `ai-studio` only | Proxy for reaching Google (`http` / `https` / `socks5`), or set `AI_STUDIO_PROXY` env var |
|
|
85
|
+
| `studio.proxy` | none | `ai-studio` only | Proxy for reaching Google (`http` / `https` / `socks5`), or set `AI_STUDIO_PROXY` env var; when set, it takes precedence over the proxy saved in the admin page. An empty value counts as unset |
|
|
86
86
|
| `studio.baseURL` | `http://127.0.0.1:2048/v1` | `ai-studio` only | Default URL when connecting an existing AIStudio2API instance |
|
|
87
87
|
| `studio.contextLimit` / `studio.outputLimit` | Model catalog defaults | `ai-studio` only | Limits reported to clients (can only lower the catalog defaults) |
|
|
88
88
|
|
|
89
89
|
**Using a proxy to reach Google:**
|
|
90
|
-
- **AI Studio:**
|
|
90
|
+
- **AI Studio:** Preferably set it in the local admin page. Open the address on the usage card's "管理页" (admin page) line (default `http://127.0.0.1:2048`) and sign in as `magpie` with `adminPassword` from `config.json` in the data directory. Save the proxy, then restart the service from the admin page. The plugin keeps the saved value when it restarts the backend.
|
|
91
|
+
- Alternatively, `studio.proxy` or `AI_STUDIO_PROXY` take precedence over the saved proxy. A change applies after the local service restarts (`npm run studio:stop`; the next request starts it again).
|
|
92
|
+
- With neither set, the backend falls back to the `HTTPS_PROXY` environment variable or the system proxy (Windows proxy settings).
|
|
93
|
+
- **The provider proxy set for `ai-studio` in Magpie is not passed to the local backend**; use one of the options above.
|
|
94
|
+
- The usage card's "代理出口" (proxy) line, the Google sign-in instructions and failure messages, and `npm run studio:status` show the proxy the backend actually uses, where it comes from, and whether a new setting still needs a restart.
|
|
95
|
+
- The listen address, API key and admin credentials are managed by the plugin; changing them in the admin page has no effect.
|
|
91
96
|
- **Gemini Web:** The backend ignores `HTTP_PROXY`. Add your proxy to the proxy pool in the local admin panel at `http://127.0.0.1:8083/admin` (password is `adminToken` in `secrets.json` inside the data directory).
|
|
92
97
|
- *Note:* When using a proxy pool, send one request with a newly added account so the backend binds it to a proxy, then re-add it via "Add an existing Google account from the local backend" to load its email and usage (SOCKS proxies are not supported for usage reads).
|
|
93
98
|
|
package/README.md
CHANGED
|
@@ -82,12 +82,17 @@ magpie plugin options magpie-gemini-web '{"studio":{"proxy":"http://127.0.0.1:78
|
|
|
82
82
|
| `stripCitations` | `true` | 仅 `gemini-web` | 剥离回复与工具参数中的 `[cite: 1]`、`[cite_start]` 标记(AI Studio 不支持剥离) |
|
|
83
83
|
| `baseURL` | `http://127.0.0.1:8083/v1` | 仅 `gemini-web` | Gemini Web 默认后端地址 |
|
|
84
84
|
| `contextLimit` / `outputLimit` | `1000000` / `8192` | 仅 `gemini-web` | 向客户端声明的上下文 / 输出上限 |
|
|
85
|
-
| `studio.proxy` | 无 | 仅 `ai-studio` | 访问 Google 的代理(`http` / `https` / `socks5`),也可用环境变量 `AI_STUDIO_PROXY
|
|
85
|
+
| `studio.proxy` | 无 | 仅 `ai-studio` | 访问 Google 的代理(`http` / `https` / `socks5`),也可用环境变量 `AI_STUDIO_PROXY`;设置后优先于管理页里保存的代理,留空等于不设置 |
|
|
86
86
|
| `studio.baseURL` | `http://127.0.0.1:2048/v1` | 仅 `ai-studio` | 连接已有 AIStudio2API 时的默认地址 |
|
|
87
87
|
| `studio.contextLimit` / `studio.outputLimit` | 模型目录默认值 | 仅 `ai-studio` | 向客户端声明的上限(仅支持在目录值基础上调低) |
|
|
88
88
|
|
|
89
89
|
**配置代理访问 Google:**
|
|
90
|
-
- **AI Studio
|
|
90
|
+
- **AI Studio**:推荐在本地管理页设置。打开额度卡片「管理页」一行里的地址(默认 `http://127.0.0.1:2048`),用户名 `magpie`,密码是数据目录 `config.json` 里的 `adminPassword`;保存代理后在管理页重启服务即可生效,之后插件重启后端也不会覆盖它。
|
|
91
|
+
- 也可以用上表的 `studio.proxy` 或环境变量 `AI_STUDIO_PROXY`,它们优先于管理页里保存的代理。改了要重启本地服务才生效(`npm run studio:stop`,下一次请求会自动启动)。
|
|
92
|
+
- 都没设置时,后端自动使用 `HTTPS_PROXY` 环境变量或系统代理(Windows 的「代理」设置)。
|
|
93
|
+
- **Magpie 里给 `ai-studio` 设置的供应商代理不会传给本地后端**,请用上面两种方式之一设置。
|
|
94
|
+
- 额度卡片的「代理出口」一行会显示后端当前实际使用的代理、它来自哪里,以及新设置是否还要重启才生效;登录 Google 时的说明、登录失败的提示和 `npm run studio:status` 也会显示。
|
|
95
|
+
- 监听地址、API Key、管理员账号等由插件管理,在管理页修改不会生效。
|
|
91
96
|
- **Gemini Web**:后端不读取 `HTTP_PROXY`,请打开本地管理面板 `http://127.0.0.1:8083/admin` 在代理池中添加(登录口令为数据目录 `secrets.json` 中的 `adminToken`)。
|
|
92
97
|
- *注意*:配置代理池后,新添加的账号需先发送一次请求以绑定代理,再通过「添加本地后端里已有的 Google 账号」重新添加,才能正常读取邮箱和用量(不支持通过 SOCKS 代理读取用量)。
|
|
93
98
|
|
package/lib/studio-native.mjs
CHANGED
|
@@ -109,12 +109,36 @@ export function validateProxy(proxy) {
|
|
|
109
109
|
return trimmed;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
112
|
+
// pluginProxy is the proxy this plugin sets for the service over the one saved in its .env: the
|
|
113
|
+
// one given (studio.proxy, or a CLI --proxy), else AI_STUDIO_PROXY, with where it comes from. An
|
|
114
|
+
// empty one names none, so the saved one applies; null when neither names one.
|
|
115
|
+
export function pluginProxy(explicit) {
|
|
116
|
+
const given = validateProxy(explicit);
|
|
117
|
+
if (given) return { proxy: given, source: 'option' };
|
|
118
|
+
const env = validateProxy(process.env.AI_STUDIO_PROXY);
|
|
119
|
+
if (env) return { proxy: env, source: 'env' };
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// downloadProxy is the proxy an install downloads through: the one given (an empty one turns it
|
|
124
|
+
// off), else AI_STUDIO_PROXY, else the service's saved one, else the one an older config.json kept.
|
|
125
|
+
function downloadProxy(dataDir, explicit, savedConfig) {
|
|
115
126
|
if (explicit !== undefined) return validateProxy(explicit);
|
|
116
127
|
if (process.env.AI_STUDIO_PROXY !== undefined) return validateProxy(process.env.AI_STUDIO_PROXY);
|
|
117
|
-
|
|
128
|
+
let saved;
|
|
129
|
+
try {
|
|
130
|
+
saved = readStudioEnv(dataDir).PROXY;
|
|
131
|
+
} catch {}
|
|
132
|
+
return saved !== undefined ? firstProxy(saved) : validateProxy(savedConfig?.proxy);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// firstProxy is the first of a PROXY list (upstream takes several, comma-separated, as a pool).
|
|
136
|
+
function firstProxy(value) {
|
|
137
|
+
return proxyList(value)[0] ?? '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function proxyList(value) {
|
|
141
|
+
return [...new Set(String(value ?? '').split(',').map(p => p.trim()).filter(Boolean))];
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
export function validateConfig(config) {
|
|
@@ -148,9 +172,34 @@ export function validateConfig(config) {
|
|
|
148
172
|
};
|
|
149
173
|
}
|
|
150
174
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
// ENV_KEYS are the settings upstream reads from .env, in the order it writes them (config.configKeys).
|
|
176
|
+
const ENV_KEYS = [
|
|
177
|
+
'AISTUDIO_AUTH_STATES', 'LISTEN_ADDR', 'PROXY_API_KEY', 'PROXY', 'INIT_TIMEOUT', 'REQUEST_TIMEOUT',
|
|
178
|
+
'WARM_WORKER_LIMIT', 'MAX_ACTIVE_WORKERS', 'WARM_STARTUP_CONCURRENCY', 'PER_ACCOUNT_CONCURRENCY',
|
|
179
|
+
'ROUTING_STRATEGY', 'UPSTREAM_CHANNELS', 'MODEL_MAPPING', 'TEMPORARY_CHAT', 'WAA_BACKEND', 'AUTO_START',
|
|
180
|
+
'REQUEST_BODY_LOG', 'LOG_FILE', 'LOG_MAX_TOTAL_MB', 'ADMIN_AUTH_ENABLED', 'ADMIN_USERNAME', 'ADMIN_PASSWORD',
|
|
181
|
+
'BUILD_NATIVE_NONSTREAM'
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
// SETTING_DEFAULTS are the settings the admin page owns, as a new setup has them. They are written
|
|
185
|
+
// to .env when it lacks them and left as the admin page saves them after that.
|
|
186
|
+
const SETTING_DEFAULTS = {
|
|
187
|
+
REQUEST_BODY_LOG: 'false',
|
|
188
|
+
LOG_FILE: 'false',
|
|
189
|
+
TEMPORARY_CHAT: 'true',
|
|
190
|
+
PER_ACCOUNT_CONCURRENCY: '1',
|
|
191
|
+
WARM_WORKER_LIMIT: '1',
|
|
192
|
+
MAX_ACTIVE_WORKERS: '1',
|
|
193
|
+
WARM_STARTUP_CONCURRENCY: '1',
|
|
194
|
+
ROUTING_STRATEGY: 'fill-first',
|
|
195
|
+
REQUEST_TIMEOUT: '30m',
|
|
196
|
+
INIT_TIMEOUT: '2m'
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// pluginEnv is the settings this plugin depends on to reach, trust and drive the service. The
|
|
200
|
+
// service is started with them in its environment, which upstream reads over .env, so saving
|
|
201
|
+
// others in the admin page can't break them.
|
|
202
|
+
function pluginEnv(dataDir, config) {
|
|
154
203
|
const authDir = path.resolve(dataDir, 'auth');
|
|
155
204
|
if (/[\r\n]/.test(authDir) || /[\r\n]/.test(config.apiKey) || /[\r\n]/.test(config.adminUsername) || /[\r\n]/.test(config.adminPassword)) {
|
|
156
205
|
throw new Error('Configuration paths or credentials must not contain newlines');
|
|
@@ -163,29 +212,103 @@ function serviceEnv(dataDir, config, options = {}) {
|
|
|
163
212
|
ADMIN_USERNAME: config.adminUsername,
|
|
164
213
|
ADMIN_PASSWORD: config.adminPassword,
|
|
165
214
|
AUTO_START: 'true',
|
|
166
|
-
REQUEST_BODY_LOG: 'false',
|
|
167
|
-
LOG_FILE: 'false',
|
|
168
215
|
WAA_BACKEND: 'go',
|
|
169
216
|
UPSTREAM_CHANNELS: 'playground',
|
|
170
|
-
BUILD_NATIVE_NONSTREAM: 'false'
|
|
171
|
-
TEMPORARY_CHAT: 'true',
|
|
172
|
-
PER_ACCOUNT_CONCURRENCY: '1',
|
|
173
|
-
WARM_WORKER_LIMIT: '1',
|
|
174
|
-
MAX_ACTIVE_WORKERS: '1',
|
|
175
|
-
WARM_STARTUP_CONCURRENCY: '1',
|
|
176
|
-
ROUTING_STRATEGY: 'fill-first',
|
|
177
|
-
REQUEST_TIMEOUT: '30m',
|
|
178
|
-
INIT_TIMEOUT: '2m',
|
|
179
|
-
PROXY: effectiveProxy(options.proxy, config.proxy)
|
|
217
|
+
BUILD_NATIVE_NONSTREAM: 'false'
|
|
180
218
|
};
|
|
181
219
|
}
|
|
182
220
|
|
|
183
|
-
|
|
184
|
-
|
|
221
|
+
// serviceEnv is the environment the service is started with: this plugin's settings, and PROXY
|
|
222
|
+
// only when the plugin sets one (see pluginProxy). Without it the service takes the PROXY saved
|
|
223
|
+
// in .env, which the admin page changes.
|
|
224
|
+
export function serviceEnv(dataDir, config, options = {}) {
|
|
225
|
+
const env = pluginEnv(dataDir, config);
|
|
226
|
+
const owned = pluginProxy(options.proxy);
|
|
227
|
+
if (owned) env.PROXY = owned.proxy;
|
|
228
|
+
return env;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// parseEnvValue and formatEnvValue read and write a .env value as upstream does.
|
|
232
|
+
function parseEnvValue(raw, where) {
|
|
233
|
+
if (!raw) return '';
|
|
234
|
+
if (raw[0] === "'") {
|
|
235
|
+
if (raw.length < 2 || !raw.endsWith("'")) throw new Error(`${where}: unterminated single quote`);
|
|
236
|
+
return raw.slice(1, -1);
|
|
237
|
+
}
|
|
238
|
+
if (raw[0] === '"') {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(raw);
|
|
241
|
+
} catch {
|
|
242
|
+
throw new Error(`${where}: invalid double-quoted value`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const comment = raw.indexOf(' #');
|
|
246
|
+
return comment >= 0 ? raw.slice(0, comment).trim() : raw;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function formatEnvValue(value) {
|
|
250
|
+
return /[ \t\r\n#"']/.test(value) ? JSON.stringify(value) : value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// readStudioEnv is the settings saved in the service's .env (none when it has no .env).
|
|
254
|
+
export function readStudioEnv(dataDir) {
|
|
255
|
+
const envPath = path.join(dataDir, '.env');
|
|
256
|
+
let text;
|
|
257
|
+
try {
|
|
258
|
+
text = fs.readFileSync(envPath, 'utf8');
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err.code === 'ENOENT') return {};
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
const values = {};
|
|
264
|
+
text.split(/\r?\n/).forEach((line, i) => {
|
|
265
|
+
line = line.trim();
|
|
266
|
+
if (!line || line.startsWith('#')) return;
|
|
267
|
+
const eq = line.indexOf('=');
|
|
268
|
+
if (eq < 0) throw new Error(`${envPath}:${i + 1}: missing "="`);
|
|
269
|
+
const key = line.slice(0, eq).trim();
|
|
270
|
+
if (ENV_KEYS.includes(key)) values[key] = parseEnvValue(line.slice(eq + 1).trim(), `${envPath}:${i + 1}`);
|
|
271
|
+
});
|
|
272
|
+
return values;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function writeStudioEnv(dataDir, values) {
|
|
276
|
+
const envPath = path.join(dataDir, '.env');
|
|
277
|
+
const content = ENV_KEYS.filter(k => values[k] !== undefined).map(k => `${k}=${formatEnvValue(String(values[k]))}\n`).join('');
|
|
278
|
+
let current;
|
|
279
|
+
try {
|
|
280
|
+
current = fs.readFileSync(envPath, 'utf8');
|
|
281
|
+
} catch {}
|
|
282
|
+
if (current !== content) {
|
|
283
|
+
const tmpEnv = `${envPath}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
284
|
+
fs.writeFileSync(tmpEnv, content, { mode: 0o600 });
|
|
285
|
+
fs.renameSync(tmpEnv, envPath);
|
|
286
|
+
}
|
|
287
|
+
fs.chmodSync(envPath, 0o600);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// syncStudioEnv brings .env up to date with config.json, keeping what the admin page saved: this
|
|
291
|
+
// plugin's settings are written over, the others only when missing. A missing PROXY is the one
|
|
292
|
+
// given, else the one an older config.json kept (which nothing reads after that).
|
|
293
|
+
function syncStudioEnv(dataDir, config, seedProxy) {
|
|
294
|
+
const values = readStudioEnv(dataDir);
|
|
295
|
+
Object.assign(values, pluginEnv(dataDir, config));
|
|
296
|
+
for (const [key, value] of Object.entries(SETTING_DEFAULTS)) values[key] ??= value;
|
|
297
|
+
values.PROXY ??= validateProxy(seedProxy) || config.proxy;
|
|
298
|
+
writeStudioEnv(dataDir, values);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// saveStudioProxy saves the service's proxy in its .env, as the admin page does. The service
|
|
302
|
+
// takes it the next time its generation service starts.
|
|
303
|
+
export function saveStudioProxy(dataDir, proxy) {
|
|
304
|
+
const values = readStudioEnv(dataDir);
|
|
305
|
+
values.PROXY = validateProxy(proxy);
|
|
306
|
+
writeStudioEnv(dataDir, values);
|
|
185
307
|
}
|
|
186
308
|
|
|
187
309
|
// ensureStudioConfig reads config.json, writing a new one if there is none: on options.port when
|
|
188
|
-
// given (see pickStudioPort), else on DEFAULT_STUDIO_PORT.
|
|
310
|
+
// given (see pickStudioPort), else on DEFAULT_STUDIO_PORT. It then brings .env up to date (see
|
|
311
|
+
// syncStudioEnv), with explicitProxy as the proxy to save if .env has none yet.
|
|
189
312
|
export function ensureStudioConfig(dataDir, explicitProxy = undefined, options = {}) {
|
|
190
313
|
fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
191
314
|
fs.chmodSync(dataDir, 0o700);
|
|
@@ -204,29 +327,24 @@ export function ensureStudioConfig(dataDir, explicitProxy = undefined, options =
|
|
|
204
327
|
fs.chmodSync(configPath, 0o600);
|
|
205
328
|
} catch (err) {
|
|
206
329
|
if (err.code === 'ENOENT') {
|
|
207
|
-
|
|
330
|
+
const created = {
|
|
208
331
|
version: 1,
|
|
209
332
|
port: options.port ?? DEFAULT_STUDIO_PORT,
|
|
210
333
|
apiKey: 'as_' + crypto.randomBytes(32).toString('hex'),
|
|
211
334
|
adminUsername: 'magpie',
|
|
212
|
-
adminPassword: crypto.randomBytes(32).toString('hex')
|
|
213
|
-
proxy: validateProxy(explicitProxy ?? '')
|
|
335
|
+
adminPassword: crypto.randomBytes(32).toString('hex')
|
|
214
336
|
};
|
|
215
337
|
const tmp = `${configPath}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
216
|
-
fs.writeFileSync(tmp, JSON.stringify(
|
|
338
|
+
fs.writeFileSync(tmp, JSON.stringify(created, null, 2) + '\n', { mode: 0o600 });
|
|
217
339
|
fs.renameSync(tmp, configPath);
|
|
218
340
|
fs.chmodSync(configPath, 0o600);
|
|
341
|
+
config = validateConfig(created);
|
|
219
342
|
} else {
|
|
220
343
|
throw err;
|
|
221
344
|
}
|
|
222
345
|
}
|
|
223
346
|
|
|
224
|
-
|
|
225
|
-
const envContent = generateEnv(dataDir, config, { proxy: explicitProxy });
|
|
226
|
-
const tmpEnv = `${envPath}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
227
|
-
fs.writeFileSync(tmpEnv, envContent, { mode: 0o600 });
|
|
228
|
-
fs.renameSync(tmpEnv, envPath);
|
|
229
|
-
fs.chmodSync(envPath, 0o600);
|
|
347
|
+
syncStudioEnv(dataDir, config, explicitProxy);
|
|
230
348
|
|
|
231
349
|
const authDir = path.join(dataDir, 'auth');
|
|
232
350
|
fs.mkdirSync(authDir, { recursive: true, mode: 0o700 });
|
|
@@ -425,6 +543,114 @@ export async function studioAccounts(options = {}) {
|
|
|
425
543
|
return accounts.filter(a => a.enabled !== false);
|
|
426
544
|
}
|
|
427
545
|
|
|
546
|
+
// PROXY_SOURCES names where the proxy the service goes out through comes from.
|
|
547
|
+
export const PROXY_SOURCES = {
|
|
548
|
+
option: '插件选项 studio.proxy',
|
|
549
|
+
env: '环境变量 AI_STUDIO_PROXY',
|
|
550
|
+
saved: '管理页',
|
|
551
|
+
startup: '服务启动时的设置',
|
|
552
|
+
unknown: '来源待确认',
|
|
553
|
+
system: '自动检测:HTTPS_PROXY 环境变量或系统代理',
|
|
554
|
+
direct: '直连'
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// redactProxy hides the passwords in a PROXY list.
|
|
558
|
+
export function redactProxy(value) {
|
|
559
|
+
return proxyList(value).map(p => {
|
|
560
|
+
try {
|
|
561
|
+
const u = new URL(p);
|
|
562
|
+
if (u.password) u.password = '***';
|
|
563
|
+
return u.toString().replace(/\/$/, '');
|
|
564
|
+
} catch {
|
|
565
|
+
return p;
|
|
566
|
+
}
|
|
567
|
+
}).join(',');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// LAUNCH_RECORD says, for the service process this plugin last started, whether it was started
|
|
571
|
+
// with a PROXY of the plugin's over the saved one, and whose: 'option' or 'env' (see pluginProxy),
|
|
572
|
+
// null for none. A caller that doesn't know the plugin's options (the CLI) reads it from there.
|
|
573
|
+
const LAUNCH_RECORD = 'backend.launch.json';
|
|
574
|
+
|
|
575
|
+
function writeLaunchRecord(dataDir, pid, proxy) {
|
|
576
|
+
const record = { pid, proxy: pluginProxy(proxy)?.source ?? null };
|
|
577
|
+
try {
|
|
578
|
+
fs.writeFileSync(path.join(dataDir, LAUNCH_RECORD), JSON.stringify(record) + '\n', { mode: 0o600 });
|
|
579
|
+
} catch {}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// launchRecord is the record of the running service's start, or null when the running service
|
|
583
|
+
// isn't the one it was written for (or an older plugin started it, which wrote none).
|
|
584
|
+
function launchRecord(dataDir) {
|
|
585
|
+
try {
|
|
586
|
+
const record = JSON.parse(fs.readFileSync(path.join(dataDir, LAUNCH_RECORD), 'utf8'));
|
|
587
|
+
if (record?.pid !== recordedPid(dataDir) || !(record.proxy === null || record.proxy in PROXY_SOURCES)) return null;
|
|
588
|
+
return record;
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// studioProxy is how the running service reaches Google:
|
|
595
|
+
// active the proxy it goes out through now ('' for none), and source where that comes from (a
|
|
596
|
+
// PROXY_SOURCES key; 'unknown' when the service can't tell it from a change not yet applied);
|
|
597
|
+
// saved the proxy saved in .env, as the admin page shows it;
|
|
598
|
+
// overridden, when another one is used over a saved one: whose it is ('option' or 'env'), or
|
|
599
|
+
// 'unknown' when the service was started with another one in its environment and
|
|
600
|
+
// nothing tells whose;
|
|
601
|
+
// pending what it takes for the one set (wanted, from wantedSource) to apply: 'restart', the
|
|
602
|
+
// service must start again; 'generation', its generation service must; null, nothing.
|
|
603
|
+
// A standalone caller (the CLI) doesn't know the plugin's options, so it can't tell what the
|
|
604
|
+
// service will be started with next: it reads what it was started with from the launch record.
|
|
605
|
+
// Passwords are hidden.
|
|
606
|
+
export async function studioProxy(local, options = {}) {
|
|
607
|
+
const saved = proxyList(readStudioEnv(local.dataDir).PROXY).join(',');
|
|
608
|
+
const override = options.standalone ? null : pluginProxy(options.proxy);
|
|
609
|
+
const cfg = await studioAdmin(local, 'GET', '/api/config', null, { timeoutMs: 3000, fetchFn: options.fetchFn });
|
|
610
|
+
// What the service reads as its PROXY now: .env, or the environment it was started with over it
|
|
611
|
+
const loaded = proxyList(cfg?.proxy).join(',');
|
|
612
|
+
const active = proxyList(cfg?.active_proxy).join(',');
|
|
613
|
+
|
|
614
|
+
let want = override ? override.proxy : saved;
|
|
615
|
+
let wantedSource = override ? override.source : 'saved';
|
|
616
|
+
let overridden = override && saved && saved !== override.proxy ? override.source : null;
|
|
617
|
+
if (options.standalone && loaded !== saved) {
|
|
618
|
+
// Started with another proxy in its environment: one of the plugin's, which applies as long as
|
|
619
|
+
// it is set, or, with no record of whose, one a restart may or may not set again
|
|
620
|
+
const launched = launchRecord(local.dataDir)?.proxy;
|
|
621
|
+
overridden = launched || 'unknown';
|
|
622
|
+
if (launched) {
|
|
623
|
+
want = loaded;
|
|
624
|
+
wantedSource = launched;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let pending = null;
|
|
629
|
+
if (loaded !== want) pending = 'restart';
|
|
630
|
+
else if (want && active !== want) pending = 'generation';
|
|
631
|
+
// Saved none, going out through one: the system's, or one saved before that the generation
|
|
632
|
+
// service still has. A change not yet applied leaves which it is open.
|
|
633
|
+
const unsure = !want && active && cfg?.service_restart_required === true;
|
|
634
|
+
if (unsure) pending = 'generation';
|
|
635
|
+
|
|
636
|
+
let source;
|
|
637
|
+
if (!active) source = 'direct';
|
|
638
|
+
else if (unsure) source = 'unknown';
|
|
639
|
+
else if (pending) source = 'startup';
|
|
640
|
+
else if (!loaded) source = 'system';
|
|
641
|
+
else source = wantedSource;
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
active: redactProxy(active),
|
|
645
|
+
source,
|
|
646
|
+
saved: redactProxy(saved),
|
|
647
|
+
overridden,
|
|
648
|
+
wanted: redactProxy(want),
|
|
649
|
+
wantedSource,
|
|
650
|
+
pending
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
428
654
|
export function isConnectRefused(err) {
|
|
429
655
|
if (!err) return false;
|
|
430
656
|
const visited = new Set();
|
|
@@ -799,7 +1025,7 @@ export async function installStudio(options = {}) {
|
|
|
799
1025
|
} else {
|
|
800
1026
|
const downloadUrl = `https://github.com/${release.repository}/releases/download/${release.tag}/${pin.asset}`;
|
|
801
1027
|
archivePath = path.join(stageDir, pin.asset);
|
|
802
|
-
const proxy =
|
|
1028
|
+
const proxy = downloadProxy(dataDir, options.proxy, savedConfig);
|
|
803
1029
|
|
|
804
1030
|
if (options.fetchFn && options.fetchFn !== globalThis.fetch) {
|
|
805
1031
|
const res = await options.fetchFn(downloadUrl, { signal: AbortSignal.timeout(300000) });
|
|
@@ -1036,7 +1262,9 @@ function shared(map, key, fn) {
|
|
|
1036
1262
|
}
|
|
1037
1263
|
|
|
1038
1264
|
// ensureStudio makes sure the service is installed and running, and, unless managementOnly,
|
|
1039
|
-
// that it takes generation requests.
|
|
1265
|
+
// that it takes generation requests. options.proxy is a proxy it is started with over the saved
|
|
1266
|
+
// one (studio.proxy); options.savedProxy is one saved as the service's own before it starts, as
|
|
1267
|
+
// the admin page saves one. Callers share one start of the service and one wait for
|
|
1040
1268
|
// it to take requests; a managementOnly caller doesn't wait for the latter.
|
|
1041
1269
|
export async function ensureStudio(options = {}) {
|
|
1042
1270
|
const dataDir = options.dataDir || studioDataDir();
|
|
@@ -1113,10 +1341,11 @@ async function launchStudio(options) {
|
|
|
1113
1341
|
}
|
|
1114
1342
|
|
|
1115
1343
|
if (!manifestValid) {
|
|
1116
|
-
await installStudio({ dataDir, platform, arch, fetchFn, archivePath: options.archivePath, proxy: options.proxy, preferredPort: options.preferredPort });
|
|
1344
|
+
await installStudio({ dataDir, platform, arch, fetchFn, archivePath: options.archivePath, proxy: options.proxy ?? options.savedProxy, preferredPort: options.preferredPort });
|
|
1117
1345
|
}
|
|
1118
1346
|
|
|
1119
1347
|
const config = ensureStudioConfig(dataDir, options.proxy, { port: await newConfigPort(dataDir, options) });
|
|
1348
|
+
if (options.savedProxy !== undefined) saveStudioProxy(dataDir, options.savedProxy);
|
|
1120
1349
|
local = localStudio({ dataDir, platform });
|
|
1121
1350
|
if (!local) throw new Error(`Invalid AI Studio configuration in ${dataDir}`);
|
|
1122
1351
|
|
|
@@ -1128,10 +1357,11 @@ async function launchStudio(options) {
|
|
|
1128
1357
|
assertStudioExited(dataDir, platform);
|
|
1129
1358
|
protectPackage(dataDir, platform);
|
|
1130
1359
|
|
|
1131
|
-
// Clean environment: no
|
|
1360
|
+
// Clean environment: no host AISTUDIO_* or WAA_* vars, nor any of upstream's settings, which
|
|
1361
|
+
// would hide the ones saved in .env
|
|
1132
1362
|
const cleanEnv = {};
|
|
1133
1363
|
for (const [k, v] of Object.entries(process.env)) {
|
|
1134
|
-
if (!k.startsWith('AISTUDIO_') && !k.startsWith('WAA_')) {
|
|
1364
|
+
if (!k.startsWith('AISTUDIO_') && !k.startsWith('WAA_') && !ENV_KEYS.includes(k.toUpperCase())) {
|
|
1135
1365
|
cleanEnv[k] = v;
|
|
1136
1366
|
}
|
|
1137
1367
|
}
|
|
@@ -1161,6 +1391,7 @@ async function launchStudio(options) {
|
|
|
1161
1391
|
});
|
|
1162
1392
|
if (child.pid) {
|
|
1163
1393
|
fs.writeFileSync(path.join(dataDir, 'backend.pid'), `${child.pid}\n`, { mode: 0o600 });
|
|
1394
|
+
writeLaunchRecord(dataDir, child.pid, options.proxy);
|
|
1164
1395
|
}
|
|
1165
1396
|
child.unref?.();
|
|
1166
1397
|
} finally {
|
package/lib/studio.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeBaseURL, credentials, forward, makeOpenAIError, UNREACHABLE_HEADER } from './backend.mjs';
|
|
2
|
-
import
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { localStudio, ensureStudio, studioAdmin, studioAccounts, studioProxy, studioCommand, validateProxy, isConnectRefused, PROXY_SOURCES } from './studio-native.mjs';
|
|
3
4
|
|
|
4
5
|
export const STUDIO_ID = 'ai-studio';
|
|
5
6
|
export const STUDIO_BASE = 'http://127.0.0.1:2048/v1';
|
|
@@ -454,6 +455,43 @@ export function poolUsage(accounts, cooldowns, now = Date.now()) {
|
|
|
454
455
|
return { user, windows: cooldownWindows(cooldowns, accounts, now) };
|
|
455
456
|
}
|
|
456
457
|
|
|
458
|
+
// proxyLine says which proxy the local service goes out through, where that comes from, a saved
|
|
459
|
+
// one set aside for another, and what it takes for the one set to apply (see studioProxy).
|
|
460
|
+
export function proxyLine(proxy, local) {
|
|
461
|
+
const named = p => p || '不走代理';
|
|
462
|
+
const restart = `重启本地服务(${studioCommand('stop', { dataDir: local.dataDir })},下一次请求会自动启动)`;
|
|
463
|
+
let line = `${named(proxy.active)}(${PROXY_SOURCES[proxy.source]})`;
|
|
464
|
+
if (proxy.overridden === 'unknown') {
|
|
465
|
+
line += `;管理页保存的是 ${named(proxy.saved)},但服务启动时的环境变量盖住了它(旧版插件启动的服务会这样)。${restart}后应按管理页设置生效;如果由 Magpie 再次启动后仍被盖住,检查插件选项 studio.proxy 和环境变量 AI_STUDIO_PROXY`;
|
|
466
|
+
return line;
|
|
467
|
+
}
|
|
468
|
+
if (proxy.pending === 'restart') {
|
|
469
|
+
line += `;${PROXY_SOURCES[proxy.wantedSource]}设置的是 ${named(proxy.wanted)},${restart}后生效`;
|
|
470
|
+
} else if (proxy.pending === 'generation') {
|
|
471
|
+
line += proxy.source === 'unknown'
|
|
472
|
+
? ';管理页有改动还没生效,在管理页重启服务后生效'
|
|
473
|
+
: `;管理页保存的 ${named(proxy.wanted)} 要在管理页重启服务后生效`;
|
|
474
|
+
}
|
|
475
|
+
if (proxy.overridden) {
|
|
476
|
+
const by = PROXY_SOURCES[proxy.overridden];
|
|
477
|
+
line += `;管理页保存的 ${proxy.saved} 被 ${by} 覆盖,不会生效。要改用它,先去掉 ${by},再${restart}`;
|
|
478
|
+
}
|
|
479
|
+
return line;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// serviceWindows are the usage card's lines about the local service itself: the proxy it goes out
|
|
483
|
+
// through (when it could be read), and where its admin page is.
|
|
484
|
+
function serviceWindows(local, proxy) {
|
|
485
|
+
const windows = [];
|
|
486
|
+
if (proxy) windows.push({ name: '代理出口', used: 0, display: proxyLine(proxy, local) });
|
|
487
|
+
windows.push({
|
|
488
|
+
name: '管理页',
|
|
489
|
+
used: 0,
|
|
490
|
+
display: `${local.origin}(用户名 ${local.adminUsername},密码是 ${path.join(local.dataDir, 'config.json')} 里的 adminPassword)`
|
|
491
|
+
});
|
|
492
|
+
return windows;
|
|
493
|
+
}
|
|
494
|
+
|
|
457
495
|
export const AIStudioPlugin = async (_input, options = {}) => {
|
|
458
496
|
const opts = studioSettings(options);
|
|
459
497
|
|
|
@@ -473,14 +511,26 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
473
511
|
return accountSignIn(local, account);
|
|
474
512
|
}
|
|
475
513
|
|
|
514
|
+
// proxyNote is a line on the proxy the local service goes out through, for a sign-in to show
|
|
515
|
+
// before its login window opens and when it fails: a wrong proxy is the usual reason the window
|
|
516
|
+
// can't reach Google, and nothing else shows it before an account is signed in.
|
|
517
|
+
async function proxyNote(local) {
|
|
518
|
+
if (!local) return '';
|
|
519
|
+
try {
|
|
520
|
+
return `\n本地服务的代理出口:${proxyLine(await studioProxy(local, opts), local)}`;
|
|
521
|
+
} catch {
|
|
522
|
+
return '';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
476
526
|
// addAccount signs a new Google account in through the service's own login window, setting the
|
|
477
527
|
// service up first if this is the first.
|
|
478
528
|
async function addAccount() {
|
|
479
|
-
await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
529
|
+
const ready = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
480
530
|
return {
|
|
481
531
|
url: '',
|
|
482
532
|
method: 'auto',
|
|
483
|
-
instructions: LOGIN_STEPS,
|
|
533
|
+
instructions: LOGIN_STEPS + await proxyNote(ready),
|
|
484
534
|
async callback() {
|
|
485
535
|
let local, account;
|
|
486
536
|
try {
|
|
@@ -491,7 +541,7 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
491
541
|
throw new Error('AIStudio2API did not return an enabled account');
|
|
492
542
|
}
|
|
493
543
|
} catch (err) {
|
|
494
|
-
return { type: 'failed', error: `Google 登录没有完成:${err.message}` };
|
|
544
|
+
return { type: 'failed', error: `Google 登录没有完成:${err.message}${await proxyNote(local)}` };
|
|
495
545
|
}
|
|
496
546
|
return afterLogin(local, account);
|
|
497
547
|
}
|
|
@@ -499,11 +549,11 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
499
549
|
}
|
|
500
550
|
|
|
501
551
|
// signInAgain signs an account whose Google sign-in expired in again, in a login window.
|
|
502
|
-
function signInAgain(chosen) {
|
|
552
|
+
async function signInAgain(ready, chosen) {
|
|
503
553
|
return {
|
|
504
554
|
url: '',
|
|
505
555
|
method: 'auto',
|
|
506
|
-
instructions: RELOGIN_STEPS,
|
|
556
|
+
instructions: RELOGIN_STEPS + await proxyNote(ready),
|
|
507
557
|
async callback() {
|
|
508
558
|
let local;
|
|
509
559
|
try {
|
|
@@ -511,7 +561,7 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
511
561
|
const res = await studioAdmin(local, 'POST', `/api/accounts/${encodeURIComponent(chosen.id)}/login`, {}, { timeoutMs: LOGIN_TIMEOUT_MS });
|
|
512
562
|
if (res?.account?.state === 'auth_required') throw new Error(res.account.message || '账号仍然需要重新登录');
|
|
513
563
|
} catch (err) {
|
|
514
|
-
return { type: 'failed', error: `${labelOf(chosen)} 没有重新登录:${err.message}` };
|
|
564
|
+
return { type: 'failed', error: `${labelOf(chosen)} 没有重新登录:${err.message}${await proxyNote(local)}` };
|
|
515
565
|
}
|
|
516
566
|
return afterLogin(local, chosen);
|
|
517
567
|
}
|
|
@@ -564,7 +614,7 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
564
614
|
const local = await ensureStudio({ proxy: opts.proxy, managementOnly: true });
|
|
565
615
|
const chosen = (await studioAccounts(opts)).find(a => a.id === id);
|
|
566
616
|
if (!chosen) throw new Error(`本地 Google AI Studio 里没有启用的账号 ${id}`);
|
|
567
|
-
if (chosen.state === 'auth_required') return signInAgain(chosen);
|
|
617
|
+
if (chosen.state === 'auth_required') return signInAgain(local, chosen);
|
|
568
618
|
return { url: '', method: 'auto', instructions: '', callback: async () => accountSignIn(local, chosen) };
|
|
569
619
|
}
|
|
570
620
|
},
|
|
@@ -631,12 +681,13 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
631
681
|
return { error: '本地 AI Studio 配置不匹配,请重新登录', windows: [], signIn: 'expired' };
|
|
632
682
|
}
|
|
633
683
|
|
|
634
|
-
let accounts, cooldowns;
|
|
684
|
+
let accounts, cooldowns, proxy;
|
|
635
685
|
try {
|
|
636
|
-
[accounts, cooldowns] = await Promise.all([
|
|
686
|
+
[accounts, cooldowns, proxy] = await Promise.all([
|
|
637
687
|
studioAdmin(local, 'GET', '/api/accounts', null, { timeoutMs: 3000 }),
|
|
638
|
-
// Without its cooldowns the card still says whose accounts these are.
|
|
639
|
-
studioAdmin(local, 'GET', '/api/cooldowns', null, { timeoutMs: 3000 }).catch(() => null)
|
|
688
|
+
// Without its cooldowns or proxy the card still says whose accounts these are.
|
|
689
|
+
studioAdmin(local, 'GET', '/api/cooldowns', null, { timeoutMs: 3000 }).catch(() => null),
|
|
690
|
+
studioProxy(local, opts).catch(() => null)
|
|
640
691
|
]);
|
|
641
692
|
} catch (err) {
|
|
642
693
|
if (isConnectRefused(err)) {
|
|
@@ -647,15 +698,18 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
647
698
|
const enabled = (Array.isArray(accounts?.accounts) ? accounts.accounts : []).filter(a => a && a.enabled !== false);
|
|
648
699
|
const cooling = Array.isArray(cooldowns?.cooldowns) ? cooldowns.cooldowns : [];
|
|
649
700
|
const id = auth.metadata.account;
|
|
701
|
+
const service = serviceWindows(local, proxy);
|
|
702
|
+
let card;
|
|
650
703
|
if (typeof id !== 'string' || !id) {
|
|
651
704
|
// A sign-in for the whole pool, as 0.5.0 made them
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
705
|
+
card = poolUsage(enabled, cooling);
|
|
706
|
+
} else {
|
|
707
|
+
const account = enabled.find(a => a.id === id);
|
|
708
|
+
card = account
|
|
709
|
+
? accountUsage(account, cooling)
|
|
710
|
+
: { error: `本地 AI Studio 里已经没有这个账号(${auth.metadata.email || id}),可能被停用或删除了`, windows: [] };
|
|
657
711
|
}
|
|
658
|
-
return { ...
|
|
712
|
+
return { ...card, windows: [...card.windows, ...service], signIn: 'kept' };
|
|
659
713
|
}
|
|
660
714
|
},
|
|
661
715
|
provider: {
|
package/package.json
CHANGED
package/scripts/studio.mjs
CHANGED
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
studioAccounts,
|
|
12
12
|
stopStudio,
|
|
13
13
|
studioStatus,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
generateEnv,
|
|
14
|
+
studioProxy,
|
|
15
|
+
saveStudioProxy,
|
|
17
16
|
studioCommand
|
|
18
17
|
} from '../lib/studio-native.mjs';
|
|
18
|
+
import { proxyLine } from '../lib/studio.mjs';
|
|
19
19
|
|
|
20
20
|
const LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
|
|
21
21
|
|
|
@@ -63,22 +63,10 @@ function parseArgs(args) {
|
|
|
63
63
|
return flags;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
config.proxy = validateProxy(proxy);
|
|
71
|
-
const tmp = `${configPath}.tmp.${Date.now()}.${crypto.randomBytes(4).toString('hex')}`;
|
|
72
|
-
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
73
|
-
fs.renameSync(tmp, configPath);
|
|
74
|
-
fs.chmodSync(configPath, 0o600);
|
|
75
|
-
|
|
76
|
-
const envPath = path.join(dataDir, '.env');
|
|
77
|
-
const envContent = generateEnv(dataDir, config, { proxy: config.proxy });
|
|
78
|
-
const tmpEnv = `${envPath}.tmp.${Date.now()}.${crypto.randomBytes(4).toString('hex')}`;
|
|
79
|
-
fs.writeFileSync(tmpEnv, envContent, { mode: 0o600 });
|
|
80
|
-
fs.renameSync(tmpEnv, envPath);
|
|
81
|
-
fs.chmodSync(envPath, 0o600);
|
|
66
|
+
// saveProxy saves the proxy given with --proxy as the service's own, as its admin page does,
|
|
67
|
+
// once the data directory is set up.
|
|
68
|
+
function saveProxy(dataDir, proxy) {
|
|
69
|
+
if (proxy !== undefined && fs.existsSync(path.join(dataDir, 'config.json'))) saveStudioProxy(dataDir, proxy);
|
|
82
70
|
}
|
|
83
71
|
|
|
84
72
|
function validateStorageState(filePath) {
|
|
@@ -128,15 +116,13 @@ async function main() {
|
|
|
128
116
|
if (status.status === 'running' || status.status === 'outdated' || status.status === 'unresponsive') {
|
|
129
117
|
throw new Error('Cannot install or update AI Studio while service is running. Stop the service first.');
|
|
130
118
|
}
|
|
131
|
-
if (flags.proxy !== undefined) {
|
|
132
|
-
await updateProxyConfig(dataDir, flags.proxy);
|
|
133
|
-
}
|
|
134
119
|
console.log(`Installing AIStudio2API into ${dataDir}...`);
|
|
135
120
|
const local = await installStudio({
|
|
136
121
|
dataDir,
|
|
137
122
|
archivePath: flags.archive,
|
|
138
123
|
proxy: flags.proxy
|
|
139
124
|
});
|
|
125
|
+
saveProxy(dataDir, flags.proxy);
|
|
140
126
|
console.log(`AIStudio2API installed successfully.`);
|
|
141
127
|
console.log(`Listen origin: ${local.origin}`);
|
|
142
128
|
console.log(`Data directory: ${local.dataDir}`);
|
|
@@ -155,14 +141,12 @@ async function main() {
|
|
|
155
141
|
console.log(`Management URL: ${local.origin}`);
|
|
156
142
|
return;
|
|
157
143
|
}
|
|
158
|
-
if (flags.proxy !== undefined) {
|
|
159
|
-
await updateProxyConfig(dataDir, flags.proxy);
|
|
160
|
-
}
|
|
161
144
|
if (status.status === 'outdated') {
|
|
162
145
|
console.log(`Replacing AIStudio2API ${status.version} from an earlier install with ${status.expected}...`);
|
|
163
146
|
}
|
|
164
147
|
console.log(`Starting AIStudio2API service in ${dataDir}...`);
|
|
165
|
-
|
|
148
|
+
// --proxy is saved as the service's own, as its admin page saves one, not set over it
|
|
149
|
+
const local = await ensureStudio({ dataDir, savedProxy: flags.proxy });
|
|
166
150
|
console.log(`AIStudio2API service is running.`);
|
|
167
151
|
console.log(`API Base URL: ${local.baseURL}`);
|
|
168
152
|
console.log(`Management URL: ${local.origin}`);
|
|
@@ -175,6 +159,13 @@ async function main() {
|
|
|
175
159
|
console.log(`State: ${status.state || 'RUNNING'}`);
|
|
176
160
|
console.log(`Listen port: ${status.port}`);
|
|
177
161
|
console.log(`Accounts: ready=${status.accounts?.ready ?? 0}, total=${status.accounts?.total ?? 0}`);
|
|
162
|
+
const local = localStudio({ dataDir });
|
|
163
|
+
console.log(`Management URL: ${local.origin} (user ${local.adminUsername}, password: adminPassword in ${path.join(dataDir, 'config.json')})`);
|
|
164
|
+
try {
|
|
165
|
+
console.log(`Proxy: ${proxyLine(await studioProxy(local, { standalone: true }), local)}`);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.log(`Proxy: could not be read (${err.message})`);
|
|
168
|
+
}
|
|
178
169
|
const accounts = await studioAccounts({ dataDir });
|
|
179
170
|
if (accounts.length > 0) {
|
|
180
171
|
console.log(`Enabled accounts:`);
|