@wax0629/pi-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.mjs ADDED
@@ -0,0 +1,761 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import http from "node:http";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createGateway } from "./gateway.mjs";
8
+ import { createPiAuthProbe } from "./pi-auth.mjs";
9
+ import { discoverProviderModels } from "./provider-discovery.mjs";
10
+ import { applyLivePiConfig, restoreLivePiBackup } from "./pi-apply.mjs";
11
+ import { readPiModelsConfig, resolvePiAgentDir } from "./pi-import.mjs";
12
+ import { createNativeAuth } from "./pi-native.mjs";
13
+ import { sanitizeConnectionTestUrl, testProviderConnection } from "./provider-test.mjs";
14
+ import { writePiProfile } from "./profile.mjs";
15
+ import { createStore } from "./store.mjs";
16
+
17
+ const managerRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+ const projectRoot = path.resolve(managerRoot, "..");
19
+ const bundledWebDir = path.join(managerRoot, "web", "dist");
20
+ const legacyPublicDir = path.join(managerRoot, "public");
21
+ const publicDir = process.env.PI_MANAGER_PUBLIC_DIR
22
+ ? path.resolve(process.env.PI_MANAGER_PUBLIC_DIR)
23
+ : fs.existsSync(legacyPublicDir) ? legacyPublicDir : bundledWebDir;
24
+ const dataDir = process.env.PI_MANAGER_HOME || path.join(os.homedir(), ".pi-manager");
25
+ const uiHost = process.env.PI_MANAGER_HOST || "127.0.0.1";
26
+ const uiPort = Number(process.env.PI_MANAGER_PORT || 8670);
27
+
28
+ function resolvePiExecutable() {
29
+ if (process.env.PI_EXECUTABLE) return process.env.PI_EXECUTABLE;
30
+ try {
31
+ return execFileSync("which", ["pi"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "pi";
32
+ } catch {
33
+ return "pi";
34
+ }
35
+ }
36
+
37
+ const piExecutable = resolvePiExecutable();
38
+ const piAuthProbe = createPiAuthProbe({ executable: piExecutable });
39
+ const store = createStore({ projectRoot, dataDir });
40
+ const nativeAuth = createNativeAuth({
41
+ executable: piExecutable,
42
+ agentDir: resolvePiAgentDir(),
43
+ openUrl: async (url) => {
44
+ if (process.platform === "darwin") {
45
+ await new Promise((resolve, reject) => execFile("open", [url], (error) => error ? reject(error) : resolve()));
46
+ }
47
+ }
48
+ });
49
+ let piInfo = null;
50
+ let bridgeInfoCache = new Map();
51
+ let gatewayLastEvent = null;
52
+
53
+ function errorMessage(error) {
54
+ return error instanceof Error ? error.message : String(error);
55
+ }
56
+
57
+ function resolveCycleEntries(current, providers) {
58
+ const providersById = new Map(providers.map((provider) => [provider.id, provider]));
59
+ const refs = Array.isArray(current.cycle?.modelRefs) ? current.cycle.modelRefs : [];
60
+
61
+ return refs.map((ref, index) => {
62
+ const normalizedRef = String(ref || "").trim();
63
+ const slashIndex = normalizedRef.indexOf("/");
64
+ const providerId = slashIndex > 0 ? normalizedRef.slice(0, slashIndex) : "";
65
+ const modelId = slashIndex > 0 ? normalizedRef.slice(slashIndex + 1) : "";
66
+ const provider = providersById.get(providerId);
67
+ const model = provider?.models.find((item) => item.id === modelId);
68
+ const valid = Boolean(provider && model && provider.status === "ready");
69
+ return {
70
+ index,
71
+ ref: normalizedRef,
72
+ providerId,
73
+ providerName: provider?.name || providerId,
74
+ providerStatus: provider?.status || "missing",
75
+ modelId,
76
+ modelName: model?.name || modelId,
77
+ valid,
78
+ reason: !provider ? "provider-missing" : !model ? "model-missing" : provider.status !== "ready" ? "provider-unready" : "ok"
79
+ };
80
+ });
81
+ }
82
+
83
+ async function ensureCycleListReady() {
84
+ const snapshot = await publicState();
85
+ const invalid = snapshot.cycle.entries.filter((entry) => !entry.valid);
86
+ if (invalid.length > 0) {
87
+ throw new Error(`循环列表包含不可用模型: ${invalid.map((entry) => entry.ref).join(" / ")}`);
88
+ }
89
+ }
90
+
91
+ function collectProviderCredentials() {
92
+ const credentials = {};
93
+ for (const provider of store.get().providers) {
94
+ const secret = store.credential(provider);
95
+ if (String(secret || "").trim()) {
96
+ credentials[provider.id] = secret;
97
+ }
98
+ }
99
+ return credentials;
100
+ }
101
+
102
+ function sendJson(res, status, value) {
103
+ const body = JSON.stringify(value);
104
+ res.statusCode = status;
105
+ res.setHeader("content-type", "application/json; charset=utf-8");
106
+ res.setHeader("cache-control", "no-store");
107
+ res.end(body);
108
+ }
109
+
110
+ function sendError(res, error, status = 400) {
111
+ sendJson(res, status, { error: errorMessage(error) });
112
+ }
113
+
114
+ async function parseBody(req) {
115
+ const chunks = [];
116
+ let size = 0;
117
+ for await (const chunk of req) {
118
+ size += chunk.length;
119
+ if (size > 2 * 1024 * 1024) throw new Error("请求体过大");
120
+ chunks.push(chunk);
121
+ }
122
+ const raw = Buffer.concat(chunks).toString("utf8");
123
+ if (!raw) return {};
124
+ try {
125
+ return JSON.parse(raw);
126
+ } catch {
127
+ throw new Error("请求体不是有效 JSON");
128
+ }
129
+ }
130
+
131
+ function safeStaticPath(requestPath) {
132
+ let decoded;
133
+ try {
134
+ decoded = decodeURIComponent(requestPath.split("?")[0]);
135
+ } catch {
136
+ return null;
137
+ }
138
+ const relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
139
+ const target = path.resolve(publicDir, relative);
140
+ return target.startsWith(`${publicDir}${path.sep}`) ? target : null;
141
+ }
142
+
143
+ function contentType(filePath) {
144
+ const ext = path.extname(filePath).toLowerCase();
145
+ return {
146
+ ".html": "text/html; charset=utf-8",
147
+ ".js": "text/javascript; charset=utf-8",
148
+ ".css": "text/css; charset=utf-8",
149
+ ".json": "application/json; charset=utf-8",
150
+ ".svg": "image/svg+xml",
151
+ ".png": "image/png",
152
+ ".jpg": "image/jpeg",
153
+ ".ico": "image/x-icon"
154
+ }[ext] || "application/octet-stream";
155
+ }
156
+
157
+ async function bridgeStatus(provider) {
158
+ const cacheKey = provider.id;
159
+ const previous = bridgeInfoCache.get(cacheKey);
160
+ if (previous && Date.now() - previous.checkedAt < 4000) return previous;
161
+ let running = false;
162
+ try {
163
+ const base = new URL(provider.baseUrl);
164
+ base.pathname = base.pathname.replace(/\/v1\/?$/, "") || "/";
165
+ base.search = "";
166
+ const response = await fetch(new URL("health", base), { signal: AbortSignal.timeout(1500) });
167
+ running = response.ok;
168
+ } catch {
169
+ running = false;
170
+ }
171
+ const result = { running, checkedAt: Date.now() };
172
+ bridgeInfoCache.set(cacheKey, result);
173
+ return result;
174
+ }
175
+
176
+ function detectPi(providerId = "openai-codex", { force = false } = {}) {
177
+ if (!piInfo) {
178
+ const detected = { installed: false, path: piExecutable, version: "" };
179
+ try {
180
+ detected.version = execFileSync(piExecutable, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
181
+ detected.installed = true;
182
+ } catch {
183
+ // A missing Pi executable is represented as an unavailable environment.
184
+ }
185
+ piInfo = detected;
186
+ }
187
+
188
+ if (!piInfo.installed) {
189
+ return { ...piInfo, subscriptionReady: false, authStatus: "unavailable", authType: "", authReason: "check_failed" };
190
+ }
191
+
192
+ const auth = piAuthProbe.check(providerId, { force });
193
+ return {
194
+ ...piInfo,
195
+ subscriptionReady: auth.ready,
196
+ authStatus: auth.status,
197
+ authType: auth.authType,
198
+ authReason: auth.reason
199
+ };
200
+ }
201
+
202
+ function nativeAuthDetail(pi) {
203
+ if (!pi?.installed) return "未检测到 Pi 可执行文件";
204
+ if (pi.subscriptionReady) return pi.authType ? `Pi 原生认证已就绪(${pi.authType})` : "Pi 原生认证已就绪";
205
+ if (pi.authReason === "credentials_not_configured" || pi.authReason === "auth_required") return "尚未完成 Pi 原生授权";
206
+ if (pi.authReason === "expired") return "Pi 原生认证已过期";
207
+ if (pi.authReason === "check_failed" || pi.authReason === "invalid_response") return "无法读取 Pi 原生认证状态";
208
+ return "Pi 原生认证未就绪";
209
+ }
210
+
211
+ async function providerPublicState(provider, { forceAuth = false, nativeSummary = null } = {}) {
212
+ const credentialConfigured = provider.kind === "native-subscription"
213
+ ? Boolean(nativeSummary?.credentialConfigured || detectPi(provider.piProvider || provider.id, { force: forceAuth }).subscriptionReady)
214
+ : store.credentialConfigured(provider);
215
+ const piNative = provider.kind === "native-subscription" ? detectPi(provider.piProvider || provider.id, { force: forceAuth }) : null;
216
+ let status = provider.kind === "native-subscription"
217
+ ? (credentialConfigured ? "ready" : "not-configured")
218
+ : (credentialConfigured ? "ready" : "not-configured");
219
+ let detail = provider.kind === "native-subscription"
220
+ ? (nativeSummary?.authLabel || nativeAuthDetail(piNative))
221
+ : "凭据未写入 Manager";
222
+ if (provider.kind === "local-bridge") {
223
+ const bridge = await bridgeStatus(provider);
224
+ if (!bridge.running) {
225
+ status = "offline";
226
+ detail = "本地桥接未运行";
227
+ } else if (!credentialConfigured) {
228
+ status = "not-configured";
229
+ detail = "桥接已运行,待配置访问密钥";
230
+ } else {
231
+ detail = "桥接在线,使用本地订阅";
232
+ }
233
+ } else if (provider.kind === "openai-api" && credentialConfigured) {
234
+ detail = "API 凭据已配置";
235
+ }
236
+ return {
237
+ ...provider,
238
+ baseUrl: provider.baseUrl ? sanitizeConnectionTestUrl(provider.baseUrl) : undefined,
239
+ credentialEnv: provider.credentialEnv || undefined,
240
+ credentialConfigured,
241
+ status,
242
+ detail,
243
+ models: provider.models.map((model) => ({ ...model }))
244
+ };
245
+ }
246
+
247
+ async function mergeNativeProviders(providers) {
248
+ let nativeSummaries = [];
249
+ try {
250
+ nativeSummaries = await nativeAuth.listNativeProviders();
251
+ } catch {
252
+ return providers;
253
+ }
254
+ return providers.map((provider) => {
255
+ const summary = nativeSummaries.find((item) => item.id === provider.id);
256
+ if (!summary || provider.kind !== "native-subscription") return provider;
257
+ return {
258
+ ...provider,
259
+ credentialConfigured: summary.credentialConfigured,
260
+ status: summary.credentialConfigured ? "ready" : provider.status,
261
+ detail: summary.authLabel || provider.detail,
262
+ authMethods: summary.authMethods
263
+ };
264
+ });
265
+ }
266
+
267
+ async function publicState({ forceAuth = false } = {}) {
268
+ const current = store.get();
269
+ const providers = await mergeNativeProviders(
270
+ await Promise.all(current.providers.map((provider) => providerPublicState(provider, { forceAuth })))
271
+ );
272
+ const activeProvider = providers.find((provider) => provider.id === current.active.providerId);
273
+ const activeModel = activeProvider?.models.find((model) => model.id === current.active.modelId);
274
+ const gatewayStats = gateway.getStats();
275
+ return {
276
+ app: { name: "Pi Manager", version: "0.1.0", platform: process.platform },
277
+ targetProject: current.targetProject,
278
+ cycle: {
279
+ modelRefs: Array.isArray(current.cycle?.modelRefs) ? current.cycle.modelRefs : [],
280
+ entries: resolveCycleEntries(current, providers)
281
+ },
282
+ active: {
283
+ ...current.active,
284
+ providerName: activeProvider?.name || current.active.providerId,
285
+ modelName: activeModel?.name || current.active.modelId,
286
+ model: activeModel || null,
287
+ providerKind: activeProvider?.kind || ""
288
+ },
289
+ providers,
290
+ gateway: {
291
+ enabled: current.gateway.enabled,
292
+ host: current.gateway.host,
293
+ port: current.gateway.port,
294
+ running: gateway.isRunning(),
295
+ stats: gatewayStats,
296
+ lastEvent: gatewayLastEvent
297
+ },
298
+ configuration: {
299
+ revision: current.runtime.configRevision,
300
+ appliedRevision: current.runtime.appliedRevision,
301
+ dirty: current.runtime.configRevision !== current.runtime.appliedRevision
302
+ },
303
+ runtime: { ...current.runtime, gatewayStats, piExecutable },
304
+ pi: detectPi(),
305
+ storage: { dataDir: store.dataDir, statePath: store.statePath },
306
+ events: current.runtime.events || []
307
+ };
308
+ }
309
+
310
+ async function applyLivePi() {
311
+ await ensureCycleListReady();
312
+ const result = applyLivePiConfig({
313
+ agentDir: resolvePiAgentDir(),
314
+ backupRoot: path.join(store.dataDir, "backups"),
315
+ state: store.get(),
316
+ credentials: collectProviderCredentials(),
317
+ piExecutable
318
+ });
319
+ store.update((state) => {
320
+ state.runtime.lastLiveImportAt = new Date().toISOString();
321
+ state.runtime.lastLiveBackupDir = result.backupDir;
322
+ state.runtime.lastLiveVerify = {
323
+ ok: Boolean(result.verify?.ok),
324
+ error: result.verify?.error || "",
325
+ refs: Array.isArray(result.verify?.refs) ? result.verify.refs : []
326
+ };
327
+ state.runtime.lastError = result.verify?.ok ? null : (result.verify?.error || null);
328
+ });
329
+ store.recordEvent("pi", "已导入本机 Pi 配置", result.agentDir);
330
+ return result;
331
+ }
332
+
333
+ function rollbackLivePi() {
334
+ const backupDir = store.get().runtime.lastLiveBackupDir;
335
+ const result = restoreLivePiBackup({
336
+ agentDir: resolvePiAgentDir(),
337
+ backupDir
338
+ });
339
+ store.update((state) => {
340
+ state.runtime.lastLiveImportAt = null;
341
+ state.runtime.lastError = null;
342
+ });
343
+ store.recordEvent("pi", "已回滚本机 Pi 配置", result.backupDir);
344
+ return result;
345
+ }
346
+
347
+ function applyProfile() {
348
+ const appliedSnapshot = store.snapshotConfiguration();
349
+ const profile = writePiProfile({
350
+ dataDir: store.dataDir,
351
+ state: store.get(),
352
+ piExecutable,
353
+ credentials: collectProviderCredentials()
354
+ });
355
+ store.update((state) => {
356
+ state.runtime.profilePath = profile.runtimeDir;
357
+ state.runtime.extensionPath = profile.extensionPath;
358
+ state.runtime.appliedRevision = state.runtime.configRevision;
359
+ state.runtime.appliedSnapshot = appliedSnapshot;
360
+ state.runtime.lastAppliedAt = new Date().toISOString();
361
+ state.runtime.lastError = null;
362
+ });
363
+ store.recordEvent("profile", "已生成 Pi 受控 profile", profile.runtimeDir);
364
+ return profile;
365
+ }
366
+
367
+ function validateTargetProject(targetProject) {
368
+ if (!targetProject || typeof targetProject !== "string") throw new Error("项目目录不能为空");
369
+ const resolved = path.resolve(targetProject);
370
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) throw new Error("项目目录不存在");
371
+ return resolved;
372
+ }
373
+
374
+ async function startGateway() {
375
+ store.update((state) => { state.gateway.enabled = true; state.runtime.lastError = null; });
376
+ try {
377
+ await gateway.start();
378
+ store.recordEvent("gateway", `本地网关已启动 :${store.get().gateway.port}`, "");
379
+ } catch (error) {
380
+ store.update((state) => { state.runtime.lastError = `网关启动失败: ${errorMessage(error)}`; });
381
+ throw error;
382
+ }
383
+ }
384
+
385
+ async function stopGateway() {
386
+ await gateway.stop();
387
+ store.update((state) => { state.gateway.enabled = false; });
388
+ store.recordEvent("gateway", "本地网关已停止", "");
389
+ }
390
+
391
+ async function launchPi() {
392
+ await ensureCycleListReady();
393
+ const profile = applyProfile();
394
+ const child = await import("node:child_process").then(({ spawn }) => spawn(profile.launcherPath, [], {
395
+ cwd: store.get().targetProject,
396
+ detached: true,
397
+ stdio: "ignore"
398
+ }));
399
+ child.unref();
400
+ store.update((state) => {
401
+ state.runtime.lastLaunchAt = new Date().toISOString();
402
+ state.runtime.lastLaunchPid = child.pid || null;
403
+ state.runtime.lastStopAt = null;
404
+ });
405
+ store.recordEvent("pi", "已启动 Pi", profile.launcherPath);
406
+ return profile;
407
+ }
408
+
409
+ async function stopPi() {
410
+ const pid = store.get().runtime.lastLaunchPid;
411
+ if (!pid) throw new Error("没有可停止的 Pi 进程");
412
+ try {
413
+ process.kill(process.platform === "win32" ? pid : -pid, "SIGTERM");
414
+ } catch (error) {
415
+ if (!(error instanceof Error) || error.code !== "ESRCH") {
416
+ throw error;
417
+ }
418
+ }
419
+ store.update((state) => {
420
+ state.runtime.lastLaunchPid = null;
421
+ state.runtime.lastStopAt = new Date().toISOString();
422
+ state.runtime.lastError = null;
423
+ });
424
+ store.recordEvent("pi", "已停止 Pi", String(pid));
425
+ }
426
+
427
+ function rollbackProfile() {
428
+ store.restoreAppliedConfiguration();
429
+ const profile = applyProfile();
430
+ store.recordEvent("profile", "已回滚到上次成功应用的配置", profile.runtimeDir);
431
+ return profile;
432
+ }
433
+
434
+ async function openBridge() {
435
+ const provider = store.provider("antigravity");
436
+ const url = provider?.baseUrl ? new URL(provider.baseUrl).origin : "http://127.0.0.1:8045";
437
+ if (process.platform === "darwin") {
438
+ await new Promise((resolve, reject) => execFile("open", [url], (error) => error ? reject(error) : resolve()));
439
+ }
440
+ return { url };
441
+ }
442
+
443
+ const gateway = createGateway({
444
+ getState: () => store.get(),
445
+ getCredential: (provider) => store.credential(provider),
446
+ onRequest: (event) => {
447
+ gatewayLastEvent = { ...event, at: new Date().toISOString() };
448
+ if (event.error) store.recordEvent("request", `${event.providerId || "渠道"} 请求失败`, event.error);
449
+ }
450
+ });
451
+
452
+ async function handleApi(req, res, pathname, { forceAuth = false } = {}) {
453
+ if (req.method === "GET" && pathname === "/api/state") {
454
+ sendJson(res, 200, await publicState({ forceAuth }));
455
+ return;
456
+ }
457
+ if (req.method === "GET" && pathname === "/api/events") {
458
+ sendJson(res, 200, { events: store.get().runtime.events || [] });
459
+ return;
460
+ }
461
+ if (req.method === "POST" && pathname === "/api/route") {
462
+ const body = await parseBody(req);
463
+ const provider = store.provider(body.providerId);
464
+ if (!provider) throw new Error("渠道不存在");
465
+ const model = provider.models.find((item) => item.id === body.modelId);
466
+ if (!model) throw new Error("模型不存在");
467
+ if (provider.kind === "native-subscription" && !detectPi(provider.piProvider || provider.id).subscriptionReady) {
468
+ throw new Error(`渠道 ${provider.name} 尚未完成 Pi 原生授权`);
469
+ }
470
+ if (provider.kind !== "native-subscription" && !store.credentialConfigured(provider)) {
471
+ throw new Error(`渠道 ${provider.name} 尚未配置凭据`);
472
+ }
473
+ if (provider.kind === "local-bridge" && !(await bridgeStatus(provider)).running) {
474
+ throw new Error(`渠道 ${provider.name} 的本地桥接未运行`);
475
+ }
476
+ store.setActive({ providerId: body.providerId, modelId: body.modelId, thinking: body.thinking });
477
+ sendJson(res, 200, { ok: true, state: await publicState() });
478
+ return;
479
+ }
480
+ if (req.method === "PATCH" && pathname === "/api/models/thinking") {
481
+ const body = await parseBody(req);
482
+ store.updateThinkingMap(body);
483
+ sendJson(res, 200, { ok: true, state: await publicState() });
484
+ return;
485
+ }
486
+ if (req.method === "PATCH" && pathname === "/api/models/context-window") {
487
+ const body = await parseBody(req);
488
+ store.updateModelContextWindow(body);
489
+ sendJson(res, 200, { ok: true, state: await publicState() });
490
+ return;
491
+ }
492
+ if (req.method === "POST" && pathname === "/api/apply") {
493
+ await ensureCycleListReady();
494
+ const profile = applyProfile();
495
+ sendJson(res, 200, { ok: true, profile, state: await publicState() });
496
+ return;
497
+ }
498
+ if (req.method === "POST" && pathname === "/api/pi/login") {
499
+ const body = await parseBody(req);
500
+ const result = await nativeAuth.login({
501
+ providerId: body.providerId,
502
+ type: body.type,
503
+ apiKey: body.apiKey
504
+ });
505
+ if (result.status === "completed") {
506
+ const summaries = await nativeAuth.listNativeProviders();
507
+ const summary = summaries.find((item) => item.id === body.providerId);
508
+ if (summary) store.upsertNativeProvider(summary);
509
+ piAuthProbe.clear(body.providerId);
510
+ }
511
+ sendJson(res, 200, { ok: true, login: result, state: await publicState({ forceAuth: true }) });
512
+ return;
513
+ }
514
+ if (req.method === "GET" && pathname.startsWith("/api/pi/login/")) {
515
+ const loginId = pathname.split("/")[4];
516
+ const result = await nativeAuth.loginStatus(loginId);
517
+ if (result.status === "completed") {
518
+ const summaries = await nativeAuth.listNativeProviders();
519
+ const summary = summaries.find((item) => item.id === result.providerId);
520
+ if (summary) store.upsertNativeProvider(summary);
521
+ piAuthProbe.clear(result.providerId);
522
+ }
523
+ sendJson(res, 200, { ok: true, login: result, state: await publicState({ forceAuth: true }) });
524
+ return;
525
+ }
526
+ if (req.method === "POST" && pathname.startsWith("/api/pi/login/") && pathname.endsWith("/prompt")) {
527
+ const loginId = pathname.split("/")[4];
528
+ const body = await parseBody(req);
529
+ const result = nativeAuth.answerPrompt(loginId, body.value);
530
+ sendJson(res, 200, { ok: true, login: result, state: await publicState() });
531
+ return;
532
+ }
533
+ if (req.method === "POST" && pathname === "/api/pi/logout") {
534
+ const body = await parseBody(req);
535
+ const result = await nativeAuth.logout(body.providerId);
536
+ store.removeNativeProvider(body.providerId);
537
+ piAuthProbe.clear(body.providerId);
538
+ sendJson(res, 200, { ok: true, result, state: await publicState({ forceAuth: true }) });
539
+ return;
540
+ }
541
+ if (req.method === "POST" && pathname === "/api/pi/live-import") {
542
+ const result = await applyLivePi();
543
+ sendJson(res, 200, { ok: true, result, state: await publicState() });
544
+ return;
545
+ }
546
+ if (req.method === "POST" && pathname === "/api/pi/live-rollback") {
547
+ const result = rollbackLivePi();
548
+ sendJson(res, 200, { ok: true, result, state: await publicState() });
549
+ return;
550
+ }
551
+ if (req.method === "POST" && pathname === "/api/profile/rollback") {
552
+ const profile = rollbackProfile();
553
+ sendJson(res, 200, { ok: true, profile, state: await publicState() });
554
+ return;
555
+ }
556
+ if (req.method === "PATCH" && pathname === "/api/models/cycle") {
557
+ const body = await parseBody(req);
558
+ store.updateCycleList(body.modelRefs);
559
+ sendJson(res, 200, { ok: true, state: await publicState() });
560
+ return;
561
+ }
562
+ if (req.method === "POST" && pathname === "/api/pi/launch") {
563
+ const profile = await launchPi();
564
+ sendJson(res, 200, { ok: true, profile, state: await publicState() });
565
+ return;
566
+ }
567
+ if (req.method === "POST" && pathname === "/api/pi/stop") {
568
+ const profile = await stopPi();
569
+ sendJson(res, 200, { ok: true, profile, state: await publicState() });
570
+ return;
571
+ }
572
+ if (req.method === "POST" && pathname === "/api/gateway/start") {
573
+ const body = await parseBody(req);
574
+ if (body.port !== undefined) {
575
+ const port = Number(body.port);
576
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("端口必须在 1024-65535 之间");
577
+ if (port !== store.get().gateway.port) {
578
+ await gateway.stop();
579
+ store.update((state) => { state.gateway.port = port; });
580
+ store.touchConfiguration();
581
+ }
582
+ }
583
+ await startGateway();
584
+ sendJson(res, 200, { ok: true, state: await publicState() });
585
+ return;
586
+ }
587
+ if (req.method === "POST" && pathname === "/api/gateway/stop") {
588
+ await stopGateway();
589
+ sendJson(res, 200, { ok: true, state: await publicState() });
590
+ return;
591
+ }
592
+ if (req.method === "POST" && pathname === "/api/bridge/open") {
593
+ sendJson(res, 200, { ok: true, ...(await openBridge()) });
594
+ return;
595
+ }
596
+ if (req.method === "POST" && pathname.startsWith("/api/providers/") && pathname.endsWith("/test")) {
597
+ const providerId = pathname.split("/")[3];
598
+ const provider = store.provider(providerId);
599
+ if (!provider) throw new Error("渠道不存在");
600
+ const result = await testProviderConnection({
601
+ provider,
602
+ credential: store.credential(provider),
603
+ detectPi: (providerId) => detectPi(providerId, { force: true })
604
+ });
605
+ store.recordEvent("provider-test", `已测试 ${provider.name}`, `${result.category} · ${result.message}`);
606
+ sendJson(res, 200, { ok: true, result, state: await publicState() });
607
+ return;
608
+ }
609
+ if (req.method === "POST" && pathname === "/api/project") {
610
+ const body = await parseBody(req);
611
+ const targetProject = validateTargetProject(body.targetProject);
612
+ store.update((state) => {
613
+ state.targetProject = targetProject;
614
+ const bridge = state.providers.find((provider) => provider.id === "antigravity");
615
+ const defaultBridge = path.join(projectRoot, "antigravity-bridge");
616
+ if (bridge && (!bridge.bridgePath || bridge.bridgePath === defaultBridge)) bridge.bridgePath = path.join(targetProject, "antigravity-bridge");
617
+ });
618
+ store.touchConfiguration();
619
+ store.recordEvent("project", "已更新 Pi 目标项目", targetProject);
620
+ const profile = applyProfile();
621
+ sendJson(res, 200, { ok: true, profile, state: await publicState() });
622
+ return;
623
+ }
624
+ if (req.method === "POST" && pathname.startsWith("/api/providers/") && pathname.endsWith("/credential")) {
625
+ const providerId = pathname.split("/")[3];
626
+ const body = await parseBody(req);
627
+ store.setCredential(providerId, body.value);
628
+ sendJson(res, 200, { ok: true, state: await publicState() });
629
+ return;
630
+ }
631
+ if (req.method === "DELETE" && pathname.startsWith("/api/providers/") && pathname.endsWith("/credential")) {
632
+ const providerId = pathname.split("/")[3];
633
+ store.deleteCredential(providerId);
634
+ sendJson(res, 200, { ok: true, state: await publicState() });
635
+ return;
636
+ }
637
+ if (req.method === "GET" && pathname === "/api/pi/native-providers") {
638
+ sendJson(res, 200, { ok: true, providers: await nativeAuth.listFeaturedNativeProviders() });
639
+ return;
640
+ }
641
+ if (req.method === "GET" && pathname === "/api/pi/import") {
642
+ const modelsConfig = readPiModelsConfig(resolvePiAgentDir());
643
+ sendJson(res, 200, { ok: true, preview: store.previewPiImport(modelsConfig) });
644
+ return;
645
+ }
646
+ if (req.method === "POST" && pathname === "/api/pi/import") {
647
+ const body = await parseBody(req);
648
+ const modelsConfig = readPiModelsConfig(resolvePiAgentDir());
649
+ const result = store.importPiProviders({
650
+ modelsConfig,
651
+ overwrite: Boolean(body.overwrite),
652
+ providerIds: body.providerIds
653
+ });
654
+ sendJson(res, 200, { ok: true, result, state: await publicState() });
655
+ return;
656
+ }
657
+ if (req.method === "POST" && pathname === "/api/providers/discover") {
658
+ const body = await parseBody(req);
659
+ const result = await discoverProviderModels({
660
+ baseUrl: body.baseUrl,
661
+ apiKey: body.apiKey
662
+ });
663
+ sendJson(res, 200, { ok: true, result });
664
+ return;
665
+ }
666
+ if (req.method === "POST" && pathname === "/api/providers") {
667
+ const body = await parseBody(req);
668
+ const provider = store.addProvider(body);
669
+ sendJson(res, 201, { ok: true, provider, state: await publicState() });
670
+ return;
671
+ }
672
+ if (req.method === "POST" && pathname.startsWith("/api/providers/") && pathname.endsWith("/models")) {
673
+ const providerId = pathname.split("/")[3];
674
+ const body = await parseBody(req);
675
+ store.addProviderModel({ providerId, model: body.model || body });
676
+ sendJson(res, 201, { ok: true, state: await publicState() });
677
+ return;
678
+ }
679
+ if (req.method === "DELETE" && pathname.startsWith("/api/providers/") && pathname.includes("/models/")) {
680
+ const parts = pathname.split("/");
681
+ const providerId = parts[3];
682
+ const modelId = decodeURIComponent(parts[5] || "");
683
+ store.removeProviderModel({ providerId, modelId });
684
+ sendJson(res, 200, { ok: true, state: await publicState() });
685
+ return;
686
+ }
687
+ if (req.method === "PATCH" && pathname.startsWith("/api/providers/") && !pathname.endsWith("/credential") && !pathname.endsWith("/test") && !pathname.endsWith("/models")) {
688
+ const providerId = pathname.split("/")[3];
689
+ const body = await parseBody(req);
690
+ const provider = store.updateProvider(providerId, body);
691
+ sendJson(res, 200, { ok: true, provider, state: await publicState() });
692
+ return;
693
+ }
694
+ if (req.method === "DELETE" && pathname.startsWith("/api/providers/") && !pathname.endsWith("/credential") && !pathname.includes("/models/")) {
695
+ const providerId = pathname.split("/")[3];
696
+ store.removeProvider(providerId);
697
+ sendJson(res, 200, { ok: true, state: await publicState() });
698
+ return;
699
+ }
700
+ sendJson(res, 404, { error: "API Not Found" });
701
+ }
702
+
703
+ async function handleRequest(req, res) {
704
+ const url = new URL(req.url || "/", `http://${uiHost}:${uiPort}`);
705
+ res.setHeader("access-control-allow-origin", `http://${uiHost}:${uiPort}`);
706
+ res.setHeader("x-content-type-options", "nosniff");
707
+ if (req.method === "OPTIONS") {
708
+ res.statusCode = 204;
709
+ res.setHeader("access-control-allow-methods", "GET, POST, PATCH, DELETE, OPTIONS");
710
+ res.setHeader("access-control-allow-headers", "content-type");
711
+ res.end();
712
+ return;
713
+ }
714
+ if (url.pathname.startsWith("/api/")) {
715
+ try {
716
+ await handleApi(req, res, url.pathname, { forceAuth: url.searchParams.get("refresh") === "1" });
717
+ } catch (error) {
718
+ sendError(res, error, 400);
719
+ }
720
+ return;
721
+ }
722
+ const filePath = safeStaticPath(url.pathname);
723
+ if (!filePath || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
724
+ sendJson(res, 404, { error: "Not Found" });
725
+ return;
726
+ }
727
+ res.statusCode = 200;
728
+ res.setHeader("content-type", contentType(filePath));
729
+ res.setHeader("cache-control", "no-store");
730
+ fs.createReadStream(filePath).pipe(res);
731
+ }
732
+
733
+ export async function startManagerServer({ host = uiHost, port = uiPort } = {}) {
734
+ if (store.get().gateway.enabled) {
735
+ try {
736
+ await gateway.start();
737
+ } catch (error) {
738
+ store.update((state) => { state.runtime.lastError = `网关启动失败: ${errorMessage(error)}`; });
739
+ }
740
+ }
741
+ const server = http.createServer((req, res) => {
742
+ handleRequest(req, res).catch((error) => sendError(res, error, 500));
743
+ });
744
+ await new Promise((resolve, reject) => {
745
+ server.once("error", reject);
746
+ server.listen(port, host, resolve);
747
+ });
748
+ return server;
749
+ }
750
+
751
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
752
+ startManagerServer().then(() => {
753
+ console.log(`Pi Manager running at http://${uiHost}:${uiPort}`);
754
+ console.log(`Pi gateway: http://${store.get().gateway.host}:${store.get().gateway.port}/v1`);
755
+ }).catch((error) => {
756
+ console.error(`Pi Manager failed to start: ${errorMessage(error)}`);
757
+ process.exitCode = 1;
758
+ });
759
+ }
760
+
761
+ export { managerRoot, projectRoot, publicDir, store, gateway, publicState, applyProfile, detectPi };