@oversk7/pi-web-dsh-style 1.0.1 → 1.1.1
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 +21 -0
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/attachment-parser.ts +210 -0
- package/attachment-store.ts +67 -24
- package/background-jobs.ts +52 -0
- package/extension-settings.ts +50 -0
- package/file-preview.ts +7 -1
- package/package.json +21 -3
- package/pi-rpc.ts +7 -5
- package/server.ts +251 -83
- package/sse-channel.ts +37 -0
- package/terminal-proxy.ts +50 -0
- package/transcript.ts +13 -0
- package/web/app.css +187 -55
- package/web/app.js +5187 -4619
- package/web/assets/markdown.LICENSE.txt +200 -0
- package/web/assets/markdown.js +25 -0
- package/web/assets/themes/sakura.webp +0 -0
- package/web/assets/themes/starlight.webp +0 -0
- package/web/index.html +5 -1
- package/web/markdown.ts +118 -0
- package/web/slash-commands.d.ts +11 -0
- package/web/slash-commands.js +46 -0
- package/web/themes.css +158 -0
package/server.ts
CHANGED
|
@@ -16,20 +16,25 @@ import {
|
|
|
16
16
|
type RpcQueueKind,
|
|
17
17
|
type RpcQueueSnapshot,
|
|
18
18
|
} from "./abort-session.ts";
|
|
19
|
-
import {
|
|
19
|
+
import { AttachmentStore, detectImageMimeType } from "./attachment-store.ts";
|
|
20
|
+
import { builtinCommandError, parseSlashCommand } from "./web/slash-commands.js";
|
|
21
|
+
import { extensionSettings } from "./extension-settings.ts";
|
|
20
22
|
import { isLocalRequest, WebAccess } from "./network-access.ts";
|
|
21
23
|
import { FrpProcess } from "./frp-process.ts";
|
|
24
|
+
import { loadTerminalProxy, saveTerminalProxy, validateTerminalProxy, type TerminalProxySettings } from "./terminal-proxy.ts";
|
|
22
25
|
import { frpcConfig, loadNetworkSettings, proxyForNetwork, publicNetworkSettings, relayServerFiles, resolveFrpc, saveNetworkSettings, updateNetworkSettings, type NetworkMode, type NetworkSettings } from "./network-settings.ts";
|
|
23
|
-
import { closeSseClient, createSseClient, sendSseEvent, type SseClient } from "./sse-channel.ts";
|
|
26
|
+
import { closeSseClient, createSseClient, createSnapshotScheduler, sendSseEvent, type SseClient } from "./sse-channel.ts";
|
|
24
27
|
import { closeAllPiRpcs, spawnPiRpc, type Json, type PiRpc } from "./pi-rpc.ts";
|
|
25
28
|
import { loadLocalSession } from "./local-session.ts";
|
|
26
29
|
import { loadJsonWithLegacyMigration, writeJsonAtomically, writeTextAtomically } from "./state-store.ts";
|
|
27
30
|
import { buildFilePreview } from "./file-preview.ts";
|
|
31
|
+
import { collectBackgroundJobs, readBackgroundJobLog } from "./background-jobs.ts";
|
|
28
32
|
import { readClipboardImage, readClipboardText } from "./windows-clipboard.ts";
|
|
29
33
|
import {
|
|
30
34
|
applyAssistantDelta,
|
|
31
35
|
applyToolExecution,
|
|
32
36
|
attachToolResults,
|
|
37
|
+
browserMessages,
|
|
33
38
|
findLast,
|
|
34
39
|
renderAssistantMessage,
|
|
35
40
|
renderCompaction,
|
|
@@ -49,6 +54,7 @@ const STATE_FILE = resolve(process.env.PI_WEB_STATE_FILE || join(STATE_DIR, "sta
|
|
|
49
54
|
const LEGACY_STATE_FILE = resolve(process.env.PI_WEB_LEGACY_STATE_FILE || join(here, "state.json"));
|
|
50
55
|
const AUTH_FILE = join(dirname(STATE_FILE), "auth.json");
|
|
51
56
|
const NETWORK_FILE = join(dirname(STATE_FILE), "network.json");
|
|
57
|
+
const TERMINAL_PROXY_FILE = join(dirname(STATE_FILE), "terminal-proxy.json");
|
|
52
58
|
|
|
53
59
|
interface Workspace {
|
|
54
60
|
id: string;
|
|
@@ -186,6 +192,7 @@ interface RuntimeSession {
|
|
|
186
192
|
operationChain?: Promise<void>;
|
|
187
193
|
pendingTreeNavigation?: PendingTreeNavigation;
|
|
188
194
|
compactionNotice?: RenderedMessage;
|
|
195
|
+
compactionNoticeIndex?: number;
|
|
189
196
|
pendingQueue?: RpcQueueSnapshot;
|
|
190
197
|
}
|
|
191
198
|
|
|
@@ -212,6 +219,8 @@ let serverUrl: string | undefined;
|
|
|
212
219
|
let serverInstanceId: string | undefined;
|
|
213
220
|
let webAccess = new WebAccess(false);
|
|
214
221
|
let networkSettings: NetworkSettings = { mode: "local" };
|
|
222
|
+
let terminalProxy: TerminalProxySettings = { mode: "inherit", url: "http://127.0.0.1:7987" };
|
|
223
|
+
let terminalProxySaving = false;
|
|
215
224
|
let networkApplying = false;
|
|
216
225
|
let networkChange: Promise<void> = Promise.resolve();
|
|
217
226
|
let networkError: string | undefined;
|
|
@@ -219,6 +228,8 @@ let frpRuntimeDir: string | undefined;
|
|
|
219
228
|
const frpProcess = new FrpProcess(() => broadcastEvent({ type: "network_changed" }));
|
|
220
229
|
let updateExtensions: (() => Promise<MaintenanceCommandResult>) | undefined;
|
|
221
230
|
let reloadRuntime: (() => Promise<void>) | undefined;
|
|
231
|
+
let extensionSettingsCwd = process.cwd();
|
|
232
|
+
let extensionSettingsSaving = false;
|
|
222
233
|
let maintenanceRunning = false;
|
|
223
234
|
let sessions = new Map<string, RuntimeSession>();
|
|
224
235
|
let sseClients = new Set<SseClient>();
|
|
@@ -233,7 +244,7 @@ const modelInfoCache = new Map<string, ModelInfo>();
|
|
|
233
244
|
const modelCatalogRefreshes = new Map<string, Promise<ModelCatalog>>();
|
|
234
245
|
const commandCache = new Map<string, SlashCommandInfo[]>();
|
|
235
246
|
const exportDownloads = new Map<string, ExportDownload>();
|
|
236
|
-
const
|
|
247
|
+
const attachments = new AttachmentStore();
|
|
237
248
|
const ABORT_RPC_TIMEOUT_MS = 3_000;
|
|
238
249
|
const INTERNAL_TREE_COMMAND = "pi-web-navigate-tree";
|
|
239
250
|
const INTERNAL_RELOAD_COMMAND = "pi-web-reload-runtime";
|
|
@@ -252,8 +263,10 @@ const MIME: Record<string, string> = {
|
|
|
252
263
|
".woff2": "font/woff2",
|
|
253
264
|
".ttf": "font/ttf",
|
|
254
265
|
".png": "image/png",
|
|
266
|
+
".webp": "image/webp",
|
|
255
267
|
};
|
|
256
268
|
const MAX_FILE_VIEW_BYTES = 2 * 1024 * 1024;
|
|
269
|
+
const MAX_IMAGE_VIEW_BYTES = 10 * 1024 * 1024;
|
|
257
270
|
|
|
258
271
|
function nowIso(): string {
|
|
259
272
|
return new Date().toISOString();
|
|
@@ -409,7 +422,7 @@ function historyEditorText(record: RuntimeSession, entryId: string): string | un
|
|
|
409
422
|
async function pasteSystemClipboard(): Promise<Json> {
|
|
410
423
|
const image = await readClipboardImage();
|
|
411
424
|
if (image) {
|
|
412
|
-
const attachment = await
|
|
425
|
+
const attachment = await attachments.create(image.bytes);
|
|
413
426
|
return {
|
|
414
427
|
kind: "image",
|
|
415
428
|
attachment: { ...attachment, url: `/api/attachments/${attachment.id}` },
|
|
@@ -630,7 +643,9 @@ function snapshotMessages(record: RuntimeSession): RenderedMessage[] {
|
|
|
630
643
|
return merged;
|
|
631
644
|
}
|
|
632
645
|
}
|
|
633
|
-
|
|
646
|
+
const messages = [...record.messages];
|
|
647
|
+
messages.splice(Math.min(record.compactionNoticeIndex ?? messages.length, messages.length), 0, notice);
|
|
648
|
+
return messages;
|
|
634
649
|
}
|
|
635
650
|
|
|
636
651
|
function pendingQueueSnapshot(record: RuntimeSession): RpcQueueSnapshot {
|
|
@@ -651,7 +666,14 @@ function setPendingQueue(record: RuntimeSession, queue: RpcQueueSnapshot): void
|
|
|
651
666
|
};
|
|
652
667
|
}
|
|
653
668
|
|
|
669
|
+
const snapshotScheduler = createSnapshotScheduler<RuntimeSession>(emitSessionSnapshot);
|
|
670
|
+
|
|
654
671
|
function broadcastSessionSnapshot(record: RuntimeSession): void {
|
|
672
|
+
if (shuttingDown) return;
|
|
673
|
+
snapshotScheduler.schedule(record.id, record, !record.streaming || record.dialogs.length > 0);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function emitSessionSnapshot(record: RuntimeSession): void {
|
|
655
677
|
broadcastEvent({
|
|
656
678
|
type: "snapshot",
|
|
657
679
|
sessionId: record.id,
|
|
@@ -663,7 +685,7 @@ function broadcastSessionSnapshot(record: RuntimeSession): void {
|
|
|
663
685
|
thinkingLevelsByModel: thinkingLevelsByModel(record.models),
|
|
664
686
|
commands: record.commands,
|
|
665
687
|
extensionStatuses: record.extensionStatuses,
|
|
666
|
-
messages: snapshotMessages(record),
|
|
688
|
+
messages: browserMessages(snapshotMessages(record)),
|
|
667
689
|
pendingQueue: pendingQueueSnapshot(record),
|
|
668
690
|
dialogs: record.dialogs,
|
|
669
691
|
});
|
|
@@ -876,7 +898,7 @@ async function discoverFreshModelCatalog(cwd: string): Promise<ModelCatalog> {
|
|
|
876
898
|
const existing = modelCatalogRefreshes.get(key);
|
|
877
899
|
if (existing) return existing;
|
|
878
900
|
const task = (async () => {
|
|
879
|
-
const probe = spawnPiRpc({ cwd: key, noSession: true });
|
|
901
|
+
const probe = spawnPiRpc({ cwd: key, noSession: true, proxy: terminalProxy });
|
|
880
902
|
try {
|
|
881
903
|
const [modelsResponse, stateResponse] = await Promise.all([
|
|
882
904
|
probe.send<Json>({ type: "get_available_models" }),
|
|
@@ -914,7 +936,7 @@ function applyModelInfo(record: RuntimeSession, info: ModelInfo): void {
|
|
|
914
936
|
|
|
915
937
|
async function refreshModelsFresh(record: RuntimeSession): Promise<void> {
|
|
916
938
|
if (record.starting) await record.starting;
|
|
917
|
-
if (record.streaming) {
|
|
939
|
+
if (record.streaming || hasRunningBackgroundJobs(record)) {
|
|
918
940
|
await refreshModels(record);
|
|
919
941
|
return;
|
|
920
942
|
}
|
|
@@ -925,10 +947,15 @@ async function refreshModelsFresh(record: RuntimeSession): Promise<void> {
|
|
|
925
947
|
const runtimeModels = (runtimeResponse.data?.models ?? []) as unknown[];
|
|
926
948
|
if (!sameModelCatalog(runtimeModels, catalog.models)) {
|
|
927
949
|
await Promise.allSettled([refreshState(record), refreshMessages(record)]);
|
|
950
|
+
if (record.streaming || hasRunningBackgroundJobs(record)) {
|
|
951
|
+
await refreshModels(record);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
928
954
|
await stopSessionProcess(record);
|
|
929
955
|
}
|
|
930
956
|
} catch (error) {
|
|
931
957
|
console.error("[pi-web] runtime model refresh failed:", error);
|
|
958
|
+
if (record.streaming || hasRunningBackgroundJobs(record)) return;
|
|
932
959
|
await stopSessionProcess(record);
|
|
933
960
|
}
|
|
934
961
|
}
|
|
@@ -1232,6 +1259,7 @@ function bindSessionEvents(record: RuntimeSession, proc: PiRpc): void {
|
|
|
1232
1259
|
} else if (event.type === "compaction_start") {
|
|
1233
1260
|
const reason = typeof event.reason === "string" ? event.reason : undefined;
|
|
1234
1261
|
record.state = { ...(record.state ?? {}), isCompacting: true };
|
|
1262
|
+
record.compactionNoticeIndex = record.messages.length;
|
|
1235
1263
|
record.compactionNotice = {
|
|
1236
1264
|
kind: "compaction",
|
|
1237
1265
|
id: `compaction-running:${Date.now()}`,
|
|
@@ -1263,6 +1291,7 @@ function bindSessionEvents(record: RuntimeSession, proc: PiRpc): void {
|
|
|
1263
1291
|
timestamp: Date.now(),
|
|
1264
1292
|
};
|
|
1265
1293
|
record.compactionNotice = notice;
|
|
1294
|
+
record.compactionNoticeIndex = record.messages.length;
|
|
1266
1295
|
broadcastSessionSnapshot(record);
|
|
1267
1296
|
if (result) {
|
|
1268
1297
|
Promise.allSettled([refreshMessages(record), refreshStats(record)]).then(() => {
|
|
@@ -1308,10 +1337,15 @@ function bindSessionEvents(record: RuntimeSession, proc: PiRpc): void {
|
|
|
1308
1337
|
// session lifecycle
|
|
1309
1338
|
// ---------------------------------------------------------------------------
|
|
1310
1339
|
|
|
1340
|
+
function hasRunningBackgroundJobs(record: RuntimeSession): boolean {
|
|
1341
|
+
// pi-pwsh-notify clears this live status only after its last background job exits.
|
|
1342
|
+
return Boolean(record.extensionStatuses["pwsh-bg"]);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1311
1345
|
function evictIdleProcesses(): void {
|
|
1312
1346
|
const idle: RuntimeSession[] = [];
|
|
1313
1347
|
for (const record of sessions.values()) {
|
|
1314
|
-
if (record.proc && !record.streaming && record.status !== "running" && record.status !== "starting" && record.dialogs.length === 0 && record.id !== state.currentSessionId) {
|
|
1348
|
+
if (record.proc && !record.streaming && !hasRunningBackgroundJobs(record) && record.status !== "running" && record.status !== "starting" && record.dialogs.length === 0 && record.id !== state.currentSessionId) {
|
|
1315
1349
|
idle.push(record);
|
|
1316
1350
|
}
|
|
1317
1351
|
}
|
|
@@ -1334,8 +1368,7 @@ async function applyPendingSettings(record: RuntimeSession): Promise<void> {
|
|
|
1334
1368
|
await proc.send({ type: "set_model", provider: pending.provider, modelId: pending.modelId });
|
|
1335
1369
|
record.pendingModel = undefined;
|
|
1336
1370
|
} catch (error) {
|
|
1337
|
-
|
|
1338
|
-
record.pendingModel = undefined;
|
|
1371
|
+
throw new Error(`无法使用模型 ${pending.provider}/${pending.modelId},请在模型菜单中选择当前可用的模型:${error instanceof Error ? error.message : String(error)}`);
|
|
1339
1372
|
}
|
|
1340
1373
|
}
|
|
1341
1374
|
if (record.pendingThinkingLevel) {
|
|
@@ -1373,6 +1406,7 @@ async function ensureProcess(record: RuntimeSession): Promise<void> {
|
|
|
1373
1406
|
const task = (async () => {
|
|
1374
1407
|
const proc = spawnPiRpc({
|
|
1375
1408
|
cwd: record.cwd,
|
|
1409
|
+
proxy: terminalProxy,
|
|
1376
1410
|
sessionId: record.sessionFile ? undefined : record.id,
|
|
1377
1411
|
sessionFile: record.sessionFile,
|
|
1378
1412
|
name: record.title === "新会话" ? undefined : record.title,
|
|
@@ -1392,9 +1426,16 @@ async function ensureProcess(record: RuntimeSession): Promise<void> {
|
|
|
1392
1426
|
broadcastWorkspaces();
|
|
1393
1427
|
evictIdleProcesses();
|
|
1394
1428
|
} catch (error) {
|
|
1429
|
+
if (record.proc === proc) record.proc = undefined;
|
|
1430
|
+
try {
|
|
1431
|
+
await proc.close(2500);
|
|
1432
|
+
} catch (closeError) {
|
|
1433
|
+
console.error("[pi-web] closing failed session process:", closeError);
|
|
1434
|
+
}
|
|
1395
1435
|
record.status = "error";
|
|
1396
1436
|
record.error = error instanceof Error ? error.message : String(error);
|
|
1397
1437
|
broadcastSessionSnapshot(record);
|
|
1438
|
+
throw error;
|
|
1398
1439
|
} finally {
|
|
1399
1440
|
record.starting = undefined;
|
|
1400
1441
|
}
|
|
@@ -1535,6 +1576,7 @@ async function deleteWorkspaceSession(workspace: Workspace, sessionId: string):
|
|
|
1535
1576
|
candidate.updatedAt = updatedAt;
|
|
1536
1577
|
}
|
|
1537
1578
|
state.sessions = state.sessions.filter((session) => session.id !== sessionId);
|
|
1579
|
+
snapshotScheduler.cancel(sessionId);
|
|
1538
1580
|
sessions.delete(sessionId);
|
|
1539
1581
|
if (state.currentSessionId === sessionId) state.currentSessionId = undefined;
|
|
1540
1582
|
scheduleSave();
|
|
@@ -1548,14 +1590,23 @@ async function createSession(
|
|
|
1548
1590
|
): Promise<RuntimeSession> {
|
|
1549
1591
|
const ws = state.workspaces.find((w) => w.id === workspaceId);
|
|
1550
1592
|
if (!ws) throw new Error("workspace not found");
|
|
1593
|
+
const info = modelInfoFromCatalog(
|
|
1594
|
+
await discoverFreshModelCatalog(ws.path),
|
|
1595
|
+
preferences.model,
|
|
1596
|
+
preferences.thinkingLevel,
|
|
1597
|
+
);
|
|
1598
|
+
const selected = info.defaultModel as Json | undefined;
|
|
1599
|
+
const model = selected && typeof selected.provider === "string" && typeof selected.id === "string"
|
|
1600
|
+
? { provider: selected.provider, modelId: selected.id }
|
|
1601
|
+
: undefined;
|
|
1551
1602
|
const id = randomUUID();
|
|
1552
1603
|
const cleanTitle = (title ?? "").trim().replace(/\s+/g, " ").slice(0, 60) || "新会话";
|
|
1553
1604
|
const stored: StoredSession = {
|
|
1554
1605
|
id,
|
|
1555
1606
|
workspaceId,
|
|
1556
1607
|
title: cleanTitle,
|
|
1557
|
-
model
|
|
1558
|
-
thinkingLevel:
|
|
1608
|
+
model,
|
|
1609
|
+
thinkingLevel: info.thinkingLevel,
|
|
1559
1610
|
needsAttention: false,
|
|
1560
1611
|
createdAt: nowIso(),
|
|
1561
1612
|
updatedAt: nowIso(),
|
|
@@ -1580,29 +1631,11 @@ async function createSession(
|
|
|
1580
1631
|
streaming: false,
|
|
1581
1632
|
needsAttention: false,
|
|
1582
1633
|
lastUsed: Date.now(),
|
|
1583
|
-
pendingModel:
|
|
1584
|
-
pendingThinkingLevel:
|
|
1634
|
+
pendingModel: model,
|
|
1635
|
+
pendingThinkingLevel: info.thinkingLevel,
|
|
1585
1636
|
};
|
|
1586
|
-
|
|
1637
|
+
applyModelInfo(record, info);
|
|
1587
1638
|
applyCachedCommands(record);
|
|
1588
|
-
if (preferences.model) {
|
|
1589
|
-
const full = (record.models as Json[]).find(
|
|
1590
|
-
(model) => model?.provider === preferences.model?.provider && model.id === preferences.model?.modelId,
|
|
1591
|
-
);
|
|
1592
|
-
record.state = {
|
|
1593
|
-
...(record.state ?? {}),
|
|
1594
|
-
model: full ?? { provider: preferences.model.provider, id: preferences.model.modelId, name: preferences.model.modelId },
|
|
1595
|
-
};
|
|
1596
|
-
if (full) record.thinkingLevels = supportedThinkingLevels(full);
|
|
1597
|
-
}
|
|
1598
|
-
if (preferences.thinkingLevel) {
|
|
1599
|
-
const effectiveLevel = record.thinkingLevels.includes(preferences.thinkingLevel)
|
|
1600
|
-
? preferences.thinkingLevel
|
|
1601
|
-
: record.thinkingLevels.includes("off") ? "off" : record.thinkingLevels[0] ?? "off";
|
|
1602
|
-
record.pendingThinkingLevel = effectiveLevel;
|
|
1603
|
-
stored.thinkingLevel = effectiveLevel;
|
|
1604
|
-
record.state = { ...(record.state ?? {}), thinkingLevel: effectiveLevel };
|
|
1605
|
-
}
|
|
1606
1639
|
sessions.set(id, record);
|
|
1607
1640
|
scheduleSave();
|
|
1608
1641
|
broadcastWorkspaces();
|
|
@@ -2112,6 +2145,46 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2112
2145
|
return sendJson(res, 200, { ok: true });
|
|
2113
2146
|
}
|
|
2114
2147
|
|
|
2148
|
+
if (path === "/api/extensions" && (method === "GET" || method === "POST")) {
|
|
2149
|
+
if (!isLocalRequest(req)) return sendJson(res, 403, { error: "请在电脑本机网页管理扩展" });
|
|
2150
|
+
if (extensionSettingsSaving || maintenanceRunning) return sendJson(res, 409, { error: "扩展配置或维护操作正在执行,请稍后重试" });
|
|
2151
|
+
extensionSettingsSaving = true;
|
|
2152
|
+
try {
|
|
2153
|
+
let change: { scope: "global" | "project"; source: string; enabled: boolean } | undefined;
|
|
2154
|
+
if (method === "POST") {
|
|
2155
|
+
const body = await readJsonBody(req, 16 * 1024);
|
|
2156
|
+
if ((body.scope !== "global" && body.scope !== "project") || typeof body.source !== "string" || !body.source || typeof body.enabled !== "boolean") {
|
|
2157
|
+
return sendJson(res, 400, { error: "扩展配置参数无效" });
|
|
2158
|
+
}
|
|
2159
|
+
change = { scope: body.scope, source: body.source, enabled: body.enabled };
|
|
2160
|
+
}
|
|
2161
|
+
return sendJson(res, 200, await extensionSettings(extensionSettingsCwd, change));
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
return sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
2164
|
+
} finally {
|
|
2165
|
+
extensionSettingsSaving = false;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
if (path === "/api/terminal-proxy") {
|
|
2170
|
+
if (!isLocalRequest(req)) return sendJson(res, 403, { error: "请在电脑本机网页配置终端代理" });
|
|
2171
|
+
if (method === "GET") return sendJson(res, 200, { settings: terminalProxy });
|
|
2172
|
+
if (method === "POST") {
|
|
2173
|
+
if (terminalProxySaving) return sendJson(res, 409, { error: "终端代理正在保存,请稍后重试" });
|
|
2174
|
+
terminalProxySaving = true;
|
|
2175
|
+
try {
|
|
2176
|
+
const next = validateTerminalProxy(await readJsonBody(req, 4096));
|
|
2177
|
+
await saveTerminalProxy(TERMINAL_PROXY_FILE, next);
|
|
2178
|
+
terminalProxy = next;
|
|
2179
|
+
return sendJson(res, 200, { settings: terminalProxy });
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
return sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
2182
|
+
} finally {
|
|
2183
|
+
terminalProxySaving = false;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2115
2188
|
if (path === "/api/network" || path === "/api/network/server-files") {
|
|
2116
2189
|
if (!isLocalRequest(req)) return sendJson(res, 403, { error: "请在电脑本机网页配置网络访问" });
|
|
2117
2190
|
if (method === "GET" && path === "/api/network/server-files") {
|
|
@@ -2162,7 +2235,7 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2162
2235
|
}
|
|
2163
2236
|
|
|
2164
2237
|
if (method === "POST" && (path === "/api/maintenance/update-and-reload" || path === "/api/maintenance/reload")) {
|
|
2165
|
-
if (maintenanceRunning) return sendJson(res, 409, { error: "
|
|
2238
|
+
if (maintenanceRunning || extensionSettingsSaving) return sendJson(res, 409, { error: "维护操作或扩展配置正在执行" });
|
|
2166
2239
|
if (!reloadRuntime || (path.endsWith("update-and-reload") && !updateExtensions)) {
|
|
2167
2240
|
return sendJson(res, 503, { error: "当前 Pi 运行时不支持 Web 维护操作,请先执行一次 /reload" });
|
|
2168
2241
|
}
|
|
@@ -2202,10 +2275,10 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2202
2275
|
if (method === "POST" && path === "/api/attachments") {
|
|
2203
2276
|
const body = await readJsonBody(req, 21 * 1024 * 1024);
|
|
2204
2277
|
if (typeof body.data !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(body.data)) {
|
|
2205
|
-
return sendJson(res, 400, { error: "
|
|
2278
|
+
return sendJson(res, 400, { error: "文件数据无效" });
|
|
2206
2279
|
}
|
|
2207
2280
|
try {
|
|
2208
|
-
const attachment = await
|
|
2281
|
+
const attachment = await attachments.create(Buffer.from(body.data, "base64"), typeof body.name === "string" ? body.name : undefined);
|
|
2209
2282
|
return sendJson(res, 200, { attachment: { ...attachment, url: `/api/attachments/${attachment.id}` } });
|
|
2210
2283
|
} catch (error) {
|
|
2211
2284
|
return sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -2215,9 +2288,10 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2215
2288
|
const attachmentMatch = /^\/api\/attachments\/([a-f0-9-]+)$/.exec(path);
|
|
2216
2289
|
if (attachmentMatch && (method === "GET" || method === "HEAD")) {
|
|
2217
2290
|
try {
|
|
2218
|
-
const { meta, bytes } = await
|
|
2291
|
+
const { meta, bytes } = await attachments.read(attachmentMatch[1]);
|
|
2219
2292
|
res.writeHead(200, {
|
|
2220
2293
|
"content-type": meta.mimeType,
|
|
2294
|
+
...(meta.kind === "document" ? { "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(meta.name)}` } : {}),
|
|
2221
2295
|
"content-length": bytes.byteLength,
|
|
2222
2296
|
"cache-control": "private, no-store",
|
|
2223
2297
|
"x-content-type-options": "nosniff",
|
|
@@ -2230,7 +2304,7 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2230
2304
|
}
|
|
2231
2305
|
|
|
2232
2306
|
if (attachmentMatch && method === "DELETE") {
|
|
2233
|
-
const removed = await
|
|
2307
|
+
const removed = await attachments.remove(attachmentMatch[1]);
|
|
2234
2308
|
return sendJson(res, removed ? 200 : 404, removed ? { ok: true } : { error: "attachment not found" });
|
|
2235
2309
|
}
|
|
2236
2310
|
|
|
@@ -2266,8 +2340,20 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2266
2340
|
const target = normalizePath(input);
|
|
2267
2341
|
const info = await stat(target);
|
|
2268
2342
|
if (!info.isFile()) return sendJson(res, 400, { error: "path is not a file" });
|
|
2269
|
-
|
|
2343
|
+
const image = url.searchParams.get("format") === "image";
|
|
2344
|
+
if (info.size > (image ? MAX_IMAGE_VIEW_BYTES : MAX_FILE_VIEW_BYTES)) return sendJson(res, 413, { error: "file is too large to preview" });
|
|
2270
2345
|
const data = await readFile(target);
|
|
2346
|
+
if (image) {
|
|
2347
|
+
const mimeType = await detectImageMimeType(target, data);
|
|
2348
|
+
if (!mimeType) return sendJson(res, 415, { error: "file is not a supported image" });
|
|
2349
|
+
res.writeHead(200, {
|
|
2350
|
+
"content-type": mimeType,
|
|
2351
|
+
"cache-control": "private, no-store",
|
|
2352
|
+
"x-content-type-options": "nosniff",
|
|
2353
|
+
});
|
|
2354
|
+
res.end(data);
|
|
2355
|
+
return true;
|
|
2356
|
+
}
|
|
2271
2357
|
if (data.includes(0)) return sendJson(res, 415, { error: "binary files cannot be previewed" });
|
|
2272
2358
|
if (url.searchParams.get("format") === "json") {
|
|
2273
2359
|
return sendJson(res, 200, buildFilePreview(target, data));
|
|
@@ -2440,14 +2526,19 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2440
2526
|
&& typeof requestedModel.modelId === "string"
|
|
2441
2527
|
? { provider: requestedModel.provider, modelId: requestedModel.modelId }
|
|
2442
2528
|
: undefined;
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2529
|
+
let record: RuntimeSession;
|
|
2530
|
+
try {
|
|
2531
|
+
record = await createSession(
|
|
2532
|
+
ws.id,
|
|
2533
|
+
typeof body.title === "string" ? body.title : undefined,
|
|
2534
|
+
{
|
|
2535
|
+
model,
|
|
2536
|
+
thinkingLevel: typeof body.thinkingLevel === "string" ? body.thinkingLevel : undefined,
|
|
2537
|
+
},
|
|
2538
|
+
);
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
return sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2541
|
+
}
|
|
2451
2542
|
state.currentWorkspaceId = ws.id;
|
|
2452
2543
|
state.currentSessionId = record.id;
|
|
2453
2544
|
scheduleSave();
|
|
@@ -2455,7 +2546,7 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2455
2546
|
return sendJson(res, 200, {
|
|
2456
2547
|
session: sessionSummary(record),
|
|
2457
2548
|
state: record.state,
|
|
2458
|
-
messages: record.messages,
|
|
2549
|
+
messages: browserMessages(record.messages),
|
|
2459
2550
|
pendingQueue: pendingQueueSnapshot(record),
|
|
2460
2551
|
models: record.models,
|
|
2461
2552
|
thinkingLevels: record.thinkingLevels,
|
|
@@ -2480,6 +2571,21 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2480
2571
|
const match = /^\/api\/sessions\/([^/]+)(?:\/([^/]+))?$/.exec(path);
|
|
2481
2572
|
const action = match?.[2] ?? "";
|
|
2482
2573
|
|
|
2574
|
+
if (method === "GET" && action === "background-jobs") {
|
|
2575
|
+
const jobs = collectBackgroundJobs(session.messages);
|
|
2576
|
+
const requested = url.searchParams.get("job");
|
|
2577
|
+
const job = requested ? jobs.find((item) => item.id === requested) : jobs[0];
|
|
2578
|
+
if (requested && !job) return sendJson(res, 404, { error: "当前会话中找不到此后台任务" });
|
|
2579
|
+
const visible = ({ logPath: _logPath, ...item }: typeof jobs[number]) => item;
|
|
2580
|
+
const data = { jobs: jobs.map(visible), job: job ? visible(job) : null };
|
|
2581
|
+
if (!job) return sendJson(res, 200, { ...data, output: "", truncated: false });
|
|
2582
|
+
try {
|
|
2583
|
+
return sendJson(res, 200, { ...data, ...await readBackgroundJobLog(job.logPath) });
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
return sendJson(res, 200, { ...data, output: "", truncated: false, error: (error as NodeJS.ErrnoException).code === "ENOENT" ? "日志文件已被清理或尚未创建" : "无法读取任务日志" });
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2483
2589
|
if (method === "POST" && action === "models") {
|
|
2484
2590
|
try {
|
|
2485
2591
|
await refreshModelsFresh(session);
|
|
@@ -2500,7 +2606,7 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2500
2606
|
return sendJson(res, 200, {
|
|
2501
2607
|
session: sessionSummary(session),
|
|
2502
2608
|
state: session.state,
|
|
2503
|
-
messages: snapshotMessages(session),
|
|
2609
|
+
messages: browserMessages(snapshotMessages(session)),
|
|
2504
2610
|
pendingQueue: pendingQueueSnapshot(session),
|
|
2505
2611
|
models: session.models,
|
|
2506
2612
|
thinkingLevels: session.thinkingLevels,
|
|
@@ -2580,7 +2686,7 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2580
2686
|
history: nextHistory,
|
|
2581
2687
|
session: sessionSummary(session),
|
|
2582
2688
|
state: session.state,
|
|
2583
|
-
messages: snapshotMessages(session),
|
|
2689
|
+
messages: browserMessages(snapshotMessages(session)),
|
|
2584
2690
|
pendingQueue: pendingQueueSnapshot(session),
|
|
2585
2691
|
stats: session.stats,
|
|
2586
2692
|
});
|
|
@@ -2590,6 +2696,40 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2590
2696
|
}
|
|
2591
2697
|
}
|
|
2592
2698
|
|
|
2699
|
+
if (method === "POST" && action === "reload") {
|
|
2700
|
+
try {
|
|
2701
|
+
return await withSessionOperation(session, async () => {
|
|
2702
|
+
await ensureProcess(session);
|
|
2703
|
+
if (session.streaming || session.state?.isCompacting || session.dialogs.length > 0) {
|
|
2704
|
+
return sendJson(res, 409, { error: "请等待当前会话运行结束并处理完交互后再重载" });
|
|
2705
|
+
}
|
|
2706
|
+
const proc = session.proc!;
|
|
2707
|
+
const commandsResponse = await proc.send<Json>({ type: "get_commands" });
|
|
2708
|
+
const commands = commandsResponse.data?.commands;
|
|
2709
|
+
if (!Array.isArray(commands) || !commands.some((command) => command?.name === INTERNAL_RELOAD_COMMAND)) {
|
|
2710
|
+
return sendJson(res, 409, { error: "当前会话未加载 Web 重载命令,请重启 Pi Web 后重试" });
|
|
2711
|
+
}
|
|
2712
|
+
let commandError: string | undefined;
|
|
2713
|
+
const unsubscribe = proc.onEvent((event) => {
|
|
2714
|
+
if (event.type === "extension_error" && event.extensionPath === `command:${INTERNAL_RELOAD_COMMAND}`) {
|
|
2715
|
+
commandError = String(event.error || "会话重载失败");
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
try {
|
|
2719
|
+
await proc.send({ type: "prompt", message: `/${INTERNAL_RELOAD_COMMAND}` });
|
|
2720
|
+
if (commandError) throw new Error(commandError);
|
|
2721
|
+
} finally {
|
|
2722
|
+
unsubscribe();
|
|
2723
|
+
}
|
|
2724
|
+
await Promise.all([refreshState(session), refreshCommands(session), refreshModels(session)]);
|
|
2725
|
+
broadcastSessionSnapshot(session);
|
|
2726
|
+
return sendJson(res, 200, { ok: true, reloaded: true });
|
|
2727
|
+
});
|
|
2728
|
+
} catch (error) {
|
|
2729
|
+
return sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2593
2733
|
if (method === "POST" && action === "prompt") {
|
|
2594
2734
|
const body = await readJsonBody(req);
|
|
2595
2735
|
const message = typeof body.message === "string" ? body.message : "";
|
|
@@ -2597,35 +2737,47 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2597
2737
|
? body.attachmentIds.filter((id): id is string => typeof id === "string")
|
|
2598
2738
|
: [];
|
|
2599
2739
|
if (!message.trim() && attachmentIds.length === 0) {
|
|
2600
|
-
return sendJson(res, 400, { error: "
|
|
2740
|
+
return sendJson(res, 400, { error: "请输入消息或添加文件附件" });
|
|
2601
2741
|
}
|
|
2742
|
+
const parsedCommand = parseSlashCommand(message);
|
|
2743
|
+
const builtinError = parsedCommand ? builtinCommandError(parsedCommand.name) : null;
|
|
2744
|
+
if (builtinError) return sendJson(res, 400, { error: builtinError });
|
|
2602
2745
|
try {
|
|
2603
2746
|
return await withSessionOperation(session, async () => {
|
|
2604
2747
|
await ensureProcess(session);
|
|
2605
2748
|
const wasStreaming = session.streaming;
|
|
2606
2749
|
if (attachmentIds.length > 0 && wasStreaming) {
|
|
2607
|
-
return sendJson(res, 409, { error: "
|
|
2750
|
+
return sendJson(res, 409, { error: "生成过程中暂不支持排队附件,请停止当前生成后再发送" });
|
|
2608
2751
|
}
|
|
2609
2752
|
if (attachmentIds.length > 0 && message.trimStart().startsWith("/")) {
|
|
2610
|
-
return sendJson(res, 400, { error: "
|
|
2753
|
+
return sendJson(res, 400, { error: "斜杠命令不能附带附件" });
|
|
2611
2754
|
}
|
|
2612
2755
|
const model = session.state?.model;
|
|
2613
2756
|
const modelInput = model && typeof model === "object" && Array.isArray((model as Json).input)
|
|
2614
2757
|
? (model as Json).input as unknown[]
|
|
2615
2758
|
: [];
|
|
2616
|
-
|
|
2617
|
-
return sendJson(res, 400, { error: "当前模型不支持图片输入,请切换视觉模型" });
|
|
2618
|
-
}
|
|
2619
|
-
let images: Array<{ type: "image"; data: string; mimeType: string }> = [];
|
|
2759
|
+
let prepared: Awaited<ReturnType<AttachmentStore["promptForRpc"]>>;
|
|
2620
2760
|
try {
|
|
2621
|
-
|
|
2761
|
+
prepared = await attachments.promptForRpc(attachmentIds, message);
|
|
2622
2762
|
} catch (error) {
|
|
2623
2763
|
return sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
2624
2764
|
}
|
|
2625
|
-
const
|
|
2626
|
-
if (
|
|
2627
|
-
|
|
2628
|
-
|
|
2765
|
+
const { images, message: promptMessage } = prepared;
|
|
2766
|
+
if (images.length > 0 && !modelInput.includes("image")) {
|
|
2767
|
+
return sendJson(res, 400, { error: "当前模型不支持图片输入,请切换视觉模型" });
|
|
2768
|
+
}
|
|
2769
|
+
const slash = /^\/([^\s]+)(?:\s|$)/.exec(promptMessage.trim());
|
|
2770
|
+
if (promptMessage.trimStart().startsWith("/")) {
|
|
2771
|
+
const response = await session.proc!.send<Json>({ type: "get_commands" });
|
|
2772
|
+
session.commands = normalizeCommands(response.data?.commands);
|
|
2773
|
+
commandCache.set(session.cwd, session.commands);
|
|
2774
|
+
if (!slash || !session.commands.some((command) => command.name === slash[1])) {
|
|
2775
|
+
return sendJson(res, 400, { error: `未知或不可用的命令:${slash ? `/${slash[1]}` : "/"}。请输入 / 查看可用命令;普通消息请勿以 / 开头。` });
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
const immediateCommand = isImmediateExtensionCommand(session, promptMessage);
|
|
2779
|
+
const optimisticStreaming = !wasStreaming && !immediateCommand;
|
|
2780
|
+
const command: Json = { type: "prompt", message: slash ? promptMessage.trim() : promptMessage };
|
|
2629
2781
|
if (images.length > 0) command.images = images;
|
|
2630
2782
|
if (wasStreaming) command.streamingBehavior = "steer";
|
|
2631
2783
|
if (optimisticStreaming) {
|
|
@@ -2635,15 +2787,15 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2635
2787
|
}
|
|
2636
2788
|
try {
|
|
2637
2789
|
const response = await session.proc!.send(command);
|
|
2638
|
-
await Promise.all(attachmentIds.map((id) =>
|
|
2639
|
-
if (!session.title || session.title === "新会话") {
|
|
2640
|
-
const nextTitle = message.trim().replace(/\s+/g, " ").slice(0, 60) || "
|
|
2790
|
+
await Promise.all(attachmentIds.map((id) => attachments.remove(id)));
|
|
2791
|
+
if (!immediateCommand && (!session.title || session.title === "新会话")) {
|
|
2792
|
+
const nextTitle = message.trim().replace(/\s+/g, " ").slice(0, 60) || "文件会话";
|
|
2641
2793
|
session.title = nextTitle;
|
|
2642
2794
|
updateStoredSession(session);
|
|
2643
2795
|
session.proc?.send({ type: "set_session_name", name: nextTitle }).catch(() => {});
|
|
2644
2796
|
broadcastWorkspaces();
|
|
2645
2797
|
}
|
|
2646
|
-
return sendJson(res, 200, { ok: true, response, queued: wasStreaming });
|
|
2798
|
+
return sendJson(res, 200, { ok: true, response, queued: wasStreaming && !immediateCommand });
|
|
2647
2799
|
} catch (error) {
|
|
2648
2800
|
if (optimisticStreaming) {
|
|
2649
2801
|
session.streaming = false;
|
|
@@ -2884,24 +3036,37 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2884
3036
|
const body = await readJsonBody(req);
|
|
2885
3037
|
const name = typeof body.name === "string" ? body.name.trim().replace(/\s+/g, " ").slice(0, 60) : "";
|
|
2886
3038
|
if (!name) return sendJson(res, 400, { error: "name is required" });
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
3039
|
+
try {
|
|
3040
|
+
if (session.proc) {
|
|
3041
|
+
await session.proc.send({ type: "set_session_name", name });
|
|
3042
|
+
session.pendingName = undefined;
|
|
3043
|
+
} else {
|
|
3044
|
+
session.pendingName = name;
|
|
3045
|
+
}
|
|
3046
|
+
session.title = name;
|
|
3047
|
+
updateStoredSession(session);
|
|
3048
|
+
broadcastWorkspaces();
|
|
3049
|
+
broadcastSessionSnapshot(session);
|
|
3050
|
+
return sendJson(res, 200, { ok: true });
|
|
3051
|
+
} catch (error) {
|
|
3052
|
+
return sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2894
3053
|
}
|
|
2895
|
-
broadcastWorkspaces();
|
|
2896
|
-
broadcastSessionSnapshot(session);
|
|
2897
|
-
return sendJson(res, 200, { ok: true });
|
|
2898
3054
|
}
|
|
2899
3055
|
|
|
2900
3056
|
if (method === "POST" && action === "compact") {
|
|
2901
|
-
await
|
|
3057
|
+
const body = await readJsonBody(req);
|
|
3058
|
+
const customInstructions = typeof body.customInstructions === "string" ? body.customInstructions.trim() : "";
|
|
2902
3059
|
try {
|
|
2903
|
-
|
|
2904
|
-
|
|
3060
|
+
return await withSessionOperation(session, async () => {
|
|
3061
|
+
await ensureProcess(session);
|
|
3062
|
+
if (session.streaming || session.state?.isCompacting || session.dialogs.length > 0) {
|
|
3063
|
+
return sendJson(res, 409, { error: "请等待当前会话运行结束并处理完交互后再压缩" });
|
|
3064
|
+
}
|
|
3065
|
+
const response = await session.proc!.send({ type: "compact", ...(customInstructions ? { customInstructions } : {}) });
|
|
3066
|
+
await Promise.all([refreshState(session), refreshMessages(session), refreshStats(session)]);
|
|
3067
|
+
broadcastSessionSnapshot(session);
|
|
3068
|
+
return sendJson(res, 200, { ok: true, response });
|
|
3069
|
+
});
|
|
2905
3070
|
} catch (error) {
|
|
2906
3071
|
return sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
2907
3072
|
}
|
|
@@ -2970,7 +3135,7 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
|
|
|
2970
3135
|
return sendJson(res, 200, {
|
|
2971
3136
|
session: sessionSummary(session),
|
|
2972
3137
|
state: session.state,
|
|
2973
|
-
messages: snapshotMessages(session),
|
|
3138
|
+
messages: browserMessages(snapshotMessages(session)),
|
|
2974
3139
|
pendingQueue: pendingQueueSnapshot(session),
|
|
2975
3140
|
models: session.models,
|
|
2976
3141
|
thinkingLevels: session.thinkingLevels,
|
|
@@ -3091,7 +3256,7 @@ function exitHook(): void {
|
|
|
3091
3256
|
// ignore
|
|
3092
3257
|
}
|
|
3093
3258
|
}
|
|
3094
|
-
void
|
|
3259
|
+
void attachments.close();
|
|
3095
3260
|
}
|
|
3096
3261
|
|
|
3097
3262
|
function sigintHook(): void {
|
|
@@ -3106,6 +3271,7 @@ function shutdown(): Promise<void> {
|
|
|
3106
3271
|
}
|
|
3107
3272
|
|
|
3108
3273
|
async function shutdownServer(): Promise<void> {
|
|
3274
|
+
snapshotScheduler.clear();
|
|
3109
3275
|
await networkChange;
|
|
3110
3276
|
await stopRelay();
|
|
3111
3277
|
for (const client of [...sseClients]) {
|
|
@@ -3121,7 +3287,7 @@ async function shutdownServer(): Promise<void> {
|
|
|
3121
3287
|
await closeAllPiRpcs();
|
|
3122
3288
|
for (const download of exportDownloads.values()) unlink(download.path).catch(() => {});
|
|
3123
3289
|
exportDownloads.clear();
|
|
3124
|
-
await
|
|
3290
|
+
await attachments.close();
|
|
3125
3291
|
if (heartbeat) clearInterval(heartbeat);
|
|
3126
3292
|
heartbeat = undefined;
|
|
3127
3293
|
await saveStateNow().catch(() => {});
|
|
@@ -3155,6 +3321,7 @@ export async function startWebServer(options: {
|
|
|
3155
3321
|
} = {}): Promise<string> {
|
|
3156
3322
|
if (shutdownPromise) await shutdownPromise;
|
|
3157
3323
|
const initialCwd = normalizePath(options.defaultCwd ?? process.cwd());
|
|
3324
|
+
extensionSettingsCwd = initialCwd;
|
|
3158
3325
|
updateExtensions = options.updateExtensions;
|
|
3159
3326
|
reloadRuntime = options.reloadRuntime;
|
|
3160
3327
|
const mode = options.mode ?? (options.lan === undefined ? undefined : options.lan ? "lan" : "local");
|
|
@@ -3188,6 +3355,7 @@ export async function startWebServer(options: {
|
|
|
3188
3355
|
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
3189
3356
|
throw new Error("端口必须是 0–65535 之间的整数");
|
|
3190
3357
|
}
|
|
3358
|
+
terminalProxy = await loadTerminalProxy(TERMINAL_PROXY_FILE);
|
|
3191
3359
|
networkSettings = await loadNetworkSettings(NETWORK_FILE);
|
|
3192
3360
|
if (mode !== undefined) {
|
|
3193
3361
|
networkSettings = updateNetworkSettings({ mode }, networkSettings);
|