@evandu/super-pi 0.1.116 → 0.1.118

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.
Files changed (39) hide show
  1. package/dist/.super-pi-check-inputs.json +2 -2
  2. package/dist/client/.super-pi-client-sources.json +14 -13
  3. package/dist/client/assets/{CodeMirrorEditor-Clwe70xp.js → CodeMirrorEditor-D9UWxggU.js} +2 -2
  4. package/dist/client/assets/{dist-BrMKdZVk.js → dist-Adu9a_hi.js} +1 -1
  5. package/dist/client/assets/{dist-C5tXtmY1.js → dist-BIjlwIeL.js} +1 -1
  6. package/dist/client/assets/{dist-BYeP-GQ3.js → dist-BSBqp9tS.js} +1 -1
  7. package/dist/client/assets/{dist-j7xPxyi5.js → dist-BUJwJ5eB.js} +1 -1
  8. package/dist/client/assets/{dist-BHW4MmlG.js → dist-BWxNjbza.js} +1 -1
  9. package/dist/client/assets/{dist-XNKM2ykU.js → dist-BewFlCBY.js} +1 -1
  10. package/dist/client/assets/{dist-CLmxejh8.js → dist-Boep2KRH.js} +1 -1
  11. package/dist/client/assets/{dist-BDLUOAZX.js → dist-C7waPnE-.js} +1 -1
  12. package/dist/client/assets/{dist-BgyXimSG.js → dist-CDI2Ko-l.js} +1 -1
  13. package/dist/client/assets/{dist-D75ywTCb.js → dist-CMRuQ4se.js} +1 -1
  14. package/dist/client/assets/{dist-BIbiS-sH.js → dist-COGr0kGo.js} +1 -1
  15. package/dist/client/assets/{dist-BsfoRXPT.js → dist-C_eAtLLb.js} +1 -1
  16. package/dist/client/assets/{dist-DPk76fqB.js → dist-ClX_xXLe.js} +1 -1
  17. package/dist/client/assets/{dist-DUJafXOC.js → dist-D9571BVm.js} +1 -1
  18. package/dist/client/assets/{dist-B8LZmhch.js → dist-DncjdwQz.js} +1 -1
  19. package/dist/client/assets/{dist-D9kUpM9y.js → dist-ce3zYUZG.js} +1 -1
  20. package/dist/client/assets/index-CF45rxGL.js +99 -0
  21. package/dist/client/assets/index-DGPYJzbU.css +1 -0
  22. package/dist/client/index.html +2 -2
  23. package/dist/server/application/conversation-service.js +4 -4
  24. package/dist/server/application/provider-service.d.ts +11 -1
  25. package/dist/server/application/provider-service.js +27 -6
  26. package/dist/server/application/run-service.js +2 -61
  27. package/dist/server/application/workspace-service.js +1 -1
  28. package/dist/server/http-server.js +46 -28
  29. package/dist/server/providers.d.ts +10 -1
  30. package/dist/server/providers.js +95 -0
  31. package/dist/server/store.js +18 -18
  32. package/dist/server/terminal.js +3 -1
  33. package/dist/server/tool-registry.js +16 -16
  34. package/dist/server/turn-failure.d.ts +22 -0
  35. package/dist/server/turn-failure.js +82 -0
  36. package/dist/server/types.d.ts +8 -0
  37. package/package.json +5 -2
  38. package/dist/client/assets/index-D6_D2f3F.js +0 -99
  39. package/dist/client/assets/index-Duvz_o9d.css +0 -1
@@ -5,69 +5,10 @@ import { readAgents } from "../agents.js";
5
5
  import { verifyCapability } from "../capability.js";
6
6
  import { validateWorkflowPlan, workflowSummary } from "../workflow.js";
7
7
  import { ensureNameIndex } from "../file-name-index.js";
8
+ import { formatTurnFailure } from "../turn-failure.js";
8
9
  import { execFile } from "node:child_process";
9
10
  import { promisify } from "node:util";
10
11
  const execFileAsync = promisify(execFile);
11
- /**
12
- * Translates a thrown error from the turn loop into a sentence a person reads.
13
- *
14
- * The turn loop throws for very different reasons: a model-provider rate limit, a 5xx that Pi's RPC
15
- * layer chose to surface as a child exit, an OOM that took the whole process down, a path that
16
- * resolved to a directory the workspace contract forbids, an `imageInput: false` mismatch. The
17
- * throw message carries the technical fact; what the reader needs is the action it implies - retry,
18
- * change model, check the key, change workspace - and that is the level this function writes at.
19
- *
20
- * The reason the shape is match-on-substring rather than match-on-error-class is that the upstream
21
- * error is whatever Pi wrote to its own log: a string, not an exception class, and the host has
22
- * nothing to pattern-match against other than the message itself. The literals below are
23
- * deliberately permissive on the LLM side ("quota", "rate limit", "usage limit", "too many requests",
24
- * "5-hour", "5h") because the underlying model APIs name this limit in several ways, and the
25
- * reader cares about the bucket, not the exact wording.
26
- */
27
- function formatTurnFailure(error) {
28
- if (controllerAborted(error))
29
- return "本轮任务已停止。";
30
- const raw = error instanceof Error ? error.message : typeof error === "string" ? error : "";
31
- const lower = raw.toLowerCase();
32
- // PROVIDER QUOTA / RATE LIMIT - the only reason the user is in this branch is because Pi was
33
- // answering them a moment ago and now the model will not let it. The action is "wait or change
34
- // model"; the wording has to make that obvious from the banner alone, without a tooltip.
35
- if (/\b429\b|quota|rate limit|usage limit|too many requests|5-hour|5h|resource_exhausted|resourc exhaust|hit your limit|usage cap|usage restrict/.test(lower)) {
36
- return "模型额度已用完(5 小时限额或类似上限),Pi 已停止回答。可以等待重置后重试,或在会话里换一个模型再发。";
37
- }
38
- // RPC CHANNEL DEAD - Pi died without producing a `settled` event. Anything in the turn is now
39
- // unanswerable, but the deltas the user already saw are still on screen, so the message has to
40
- // say "we noticed and stopped" rather than "your input was lost".
41
- if (lower.includes("pi rpc channel closed") || lower.includes("agent rpc") || lower.includes("exited before settling")) {
42
- return "Pi 进程异常退出,未完成本轮回答。已停止运行,可以直接重发任务继续。";
43
- }
44
- // NO PI - the launch path failed (binary missing, executable bit, Windows shim). The reader's
45
- // best move is a `which pi` / reinstall, and the banner should point there.
46
- if (lower.includes("could not find") && lower.includes("pi") || lower.includes("pi command") || lower.startsWith("pi ")) {
47
- return `无法启动 Pi:${raw || "未知原因"}。请确认 Pi 已正确安装并可在命令行运行。`;
48
- }
49
- // WORKSPACE PATH - turn refused on directory the contract forbids. Refusal happens before
50
- // anything is on screen, so the banner is also the only place the reason lives.
51
- if (lower.includes("workspace directory") || lower.includes("does not exist or is not a directory")) {
52
- return `工作区目录无效:${raw}`;
53
- }
54
- // IMAGE MISMATCH - explicit by the caller, no need to translate.
55
- if (lower.includes("不支持图片输入")) {
56
- return raw;
57
- }
58
- // GENERIC - the literal message is more useful than a paraphrase, but it has to be the LAST
59
- // branch and the message has to be non-empty (a thrown `new Error()` is empty and renders
60
- // nothing, which is exactly the silence this whole change exists to break).
61
- return raw || "Pi 未能完成本轮回答,已标记为失败。可以重发任务继续。";
62
- }
63
- function controllerAborted(error) {
64
- if (!error)
65
- return false;
66
- if (error instanceof Error && error.name === "AbortError")
67
- return true;
68
- const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "";
69
- return /abort|cancel|aborted/i.test(msg);
70
- }
71
12
  /** Expands composer references into explicit context for the model. */
72
13
  async function expandAtReferences(store, workspace, content) {
73
14
  const refs = [...content.matchAll(/@(worktree|session):([^\s]+)/gi)];
@@ -753,7 +694,7 @@ export class RunService {
753
694
  async openTurn(conversationId, messageId) {
754
695
  const conversation = this.store.snapshot().conversations.find(item => item.id === conversationId);
755
696
  if (!conversation)
756
- throw new Error("Conversation not found");
697
+ throw new Error("找不到这个会话。");
757
698
  const agent = await this.runtime.agentFor?.(conversation);
758
699
  const identity = agent ? { id: agent.id, name: agent.name } : { name: "Pi" };
759
700
  const opened = await this.store.openTurnOnMessage(conversationId, messageId, identity);
@@ -64,7 +64,7 @@ export class WorkspaceService {
64
64
  async updateSettings(workspaceId, settings) {
65
65
  const workspace = this.store.snapshot().workspaces.find((item) => item.id === workspaceId);
66
66
  if (!workspace)
67
- throw new Error("Workspace not found");
67
+ throw new Error("找不到这个工作区。");
68
68
  let selection;
69
69
  const requested = settings.model;
70
70
  if (requested) {
@@ -253,12 +253,12 @@ export function createHttpServer(options) {
253
253
  try {
254
254
  const id = url.searchParams.get("id");
255
255
  if (!id) {
256
- json(400, { error: "id required" });
256
+ json(400, { error: "缺少工具 id。" });
257
257
  return;
258
258
  }
259
259
  const exported = await toolRegistry.exportBundle(id);
260
260
  if (!exported) {
261
- json(404, { error: "tool not found" });
261
+ json(404, { error: "找不到这个工具。" });
262
262
  return;
263
263
  }
264
264
  json(200, exported);
@@ -281,7 +281,7 @@ export function createHttpServer(options) {
281
281
  if (request.method === "DELETE") {
282
282
  const id = url.searchParams.get("id");
283
283
  if (!id) {
284
- json(400, { error: "id required" });
284
+ json(400, { error: "缺少工具 id。" });
285
285
  return;
286
286
  }
287
287
  const removed = await toolRegistry.remove(id);
@@ -296,7 +296,7 @@ export function createHttpServer(options) {
296
296
  return;
297
297
  }
298
298
  if (request.method !== "POST") {
299
- json(405, { error: "method" });
299
+ json(405, { error: "这个接口只接受 POST 请求。" });
300
300
  return;
301
301
  }
302
302
  // BYTES, NOT CHARACTERS, AND FROM THE REGISTRY'S OWN CONSTANT. The old cap counted UTF-16 code
@@ -307,7 +307,7 @@ export function createHttpServer(options) {
307
307
  const piece = chunk;
308
308
  bytes += piece.length;
309
309
  if (bytes > MAX_TOOL_BODY_BYTES) {
310
- json(413, { error: "request too large" });
310
+ json(413, { error: "导入的内容太大,超过了大小上限。" });
311
311
  return;
312
312
  }
313
313
  raw += piece.toString("utf8");
@@ -315,7 +315,7 @@ export function createHttpServer(options) {
315
315
  const body = JSON.parse(raw);
316
316
  if (!isAuthorized(request, accessToken)) {
317
317
  if (!body.conversationId) {
318
- json(401, { error: "authentication or agent capability required" });
318
+ json(401, { error: "需要访问凭据,或正在运行的会话凭据。" });
319
319
  return;
320
320
  }
321
321
  const verdict = runService.verifyPrimaryCapability(body.conversationId, body.capability ?? request.headers["x-super-pi-capability"]);
@@ -325,7 +325,7 @@ export function createHttpServer(options) {
325
325
  }
326
326
  }
327
327
  if (!body.slug || !body.name || !Array.isArray(body.files)) {
328
- json(400, { error: "slug, name and files are required" });
328
+ json(400, { error: "导入需要 slug、名称和文件列表。" });
329
329
  return;
330
330
  }
331
331
  // AN IMPORTED BUNDLE KEEPS THE EXPORTER'S FINGERPRINT. It describes where the tool was written
@@ -649,6 +649,24 @@ export function createHttpServer(options) {
649
649
  }
650
650
  return;
651
651
  }
652
+ if (url.pathname === "/api/providers/fetch-models") {
653
+ if (request.method !== "POST") {
654
+ response.writeHead(405).end();
655
+ return;
656
+ }
657
+ try {
658
+ let raw = "";
659
+ for await (const chunk of request)
660
+ raw += chunk;
661
+ const body = raw ? JSON.parse(raw) : {};
662
+ const models = await context.providerService.fetchModels(body);
663
+ json(200, { ok: true, models });
664
+ }
665
+ catch (error) {
666
+ json(400, { ok: false, error: error instanceof Error ? error.message : String(error) });
667
+ }
668
+ return;
669
+ }
652
670
  if (url.pathname === "/api/knowledge/tool" || url.pathname === "/api/knowledge") {
653
671
  if (!context.knowledge) {
654
672
  response.writeHead(503).end();
@@ -849,7 +867,7 @@ export function createHttpServer(options) {
849
867
  return;
850
868
  }
851
869
  if (!execService) {
852
- response.writeHead(503, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "exec service not available", code: "unavailable" }));
870
+ response.writeHead(503, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "后台命令服务没有启用。", code: "unavailable" }));
853
871
  return;
854
872
  }
855
873
  let raw = "";
@@ -871,12 +889,12 @@ export function createHttpServer(options) {
871
889
  body = JSON.parse(raw);
872
890
  }
873
891
  catch {
874
- json(400, { error: "invalid JSON" });
892
+ json(400, { error: "请求体不是合法的 JSON。" });
875
893
  return;
876
894
  }
877
895
  const { cmd, cwd, conversationId, capability, toolCallId } = body;
878
896
  if (!cmd || !cwd || !conversationId) {
879
- json(400, { error: "cmd, cwd and conversationId are required" });
897
+ json(400, { error: "需要 cmd、cwd 和 conversationId。" });
880
898
  return;
881
899
  }
882
900
  // Verify both the credential and RunService's authoritative active-turn state.
@@ -1029,7 +1047,7 @@ export function createHttpServer(options) {
1029
1047
  const workspaceId = url.searchParams.get("workspaceId")?.trim();
1030
1048
  const workspace = store.snapshot().workspaces.find((item) => item.id === workspaceId);
1031
1049
  if (!workspaceId || !workspace) {
1032
- json(404, { error: `Workspace ${workspaceId ?? "(missing)"} not found`, code: "no_workspace" });
1050
+ json(404, { error: `找不到工作区「${workspaceId ?? "未指定"}」——它可能已经被删除。`, code: "no_workspace" });
1033
1051
  return;
1034
1052
  }
1035
1053
  const path = url.searchParams.get("path")?.trim() ?? "";
@@ -1060,19 +1078,19 @@ export function createHttpServer(options) {
1060
1078
  const json = (status, body) => { response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body)); };
1061
1079
  try {
1062
1080
  if (request.method !== "GET") {
1063
- json(405, { error: "Content search requires GET", code: "method" });
1081
+ json(405, { error: "内容搜索需要 GET 请求。", code: "method" });
1064
1082
  return;
1065
1083
  }
1066
1084
  const workspaceId = url.searchParams.get("workspaceId")?.trim();
1067
1085
  const query = url.searchParams.get("q")?.trim() ?? "";
1068
1086
  const workspace = store.snapshot().workspaces.find((item) => item.id === workspaceId);
1069
1087
  if (!workspaceId || !workspace) {
1070
- json(404, { error: "workspace not found", code: "no_workspace" });
1088
+ json(404, { error: "找不到这个工作区——它可能已经被删除。", code: "no_workspace" });
1071
1089
  return;
1072
1090
  }
1073
1091
  await resolveInsideRoot(workspace.path, "");
1074
1092
  if (query.length > 256 || /[\r\n]/.test(query)) {
1075
- json(400, { error: "Search query must be one line, at most 256 characters", code: "bad_query" });
1093
+ json(400, { error: "搜索词只能是一行,且不超过 256 个字符。", code: "bad_query" });
1076
1094
  return;
1077
1095
  }
1078
1096
  if (!query) {
@@ -1105,13 +1123,13 @@ export function createHttpServer(options) {
1105
1123
  const json = (status, body) => { response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body)); };
1106
1124
  try {
1107
1125
  if (request.method !== "GET") {
1108
- json(405, { error: "File index requires GET", code: "method" });
1126
+ json(405, { error: "文件索引需要 GET 请求。", code: "method" });
1109
1127
  return;
1110
1128
  }
1111
1129
  const workspaceId = url.searchParams.get("workspaceId")?.trim();
1112
1130
  const workspace = store.snapshot().workspaces.find((item) => item.id === workspaceId);
1113
1131
  if (!workspaceId || !workspace) {
1114
- json(404, { error: `Workspace ${workspaceId ?? "(missing)"} not found`, code: "no_workspace" });
1132
+ json(404, { error: `找不到工作区「${workspaceId ?? "未指定"}」——它可能已经被删除。`, code: "no_workspace" });
1115
1133
  return;
1116
1134
  }
1117
1135
  const indexes = context.fileIndexes ?? new Map();
@@ -1146,13 +1164,13 @@ export function createHttpServer(options) {
1146
1164
  const workspaceId = url.searchParams.get("workspaceId")?.trim();
1147
1165
  const workspace = store.snapshot().workspaces.find((item) => item.id === workspaceId);
1148
1166
  if (!workspaceId || !workspace) {
1149
- json(404, { error: `Workspace ${workspaceId ?? "(missing)"} not found`, code: "no_workspace" });
1167
+ json(404, { error: `找不到工作区「${workspaceId ?? "未指定"}」——它可能已经被删除。`, code: "no_workspace" });
1150
1168
  return;
1151
1169
  }
1152
1170
  const root = workspace.path;
1153
1171
  if (url.pathname === "/api/files") {
1154
1172
  if (request.method !== "GET") {
1155
- json(405, { error: "Directory listing requires GET", code: "method" });
1173
+ json(405, { error: "目录列表需要 GET 请求。", code: "method" });
1156
1174
  return;
1157
1175
  }
1158
1176
  const query = url.searchParams.get("q");
@@ -1217,7 +1235,7 @@ export function createHttpServer(options) {
1217
1235
  if (request.method === "PUT") {
1218
1236
  const remoteAddr = request.socket.remoteAddress ?? "";
1219
1237
  if (!isLocalOrPrivateAddress(remoteAddr)) {
1220
- json(403, { error: "File writes require a loopback or private network connection", code: "not_loopback" });
1238
+ json(403, { error: "保存文件需要从本机或局域网发起。", code: "not_loopback" });
1221
1239
  return;
1222
1240
  }
1223
1241
  let raw = "";
@@ -1250,7 +1268,7 @@ export function createHttpServer(options) {
1250
1268
  json(200, outcome);
1251
1269
  return;
1252
1270
  }
1253
- json(405, { error: `${request.method} is not supported on /api/file`, code: "method" });
1271
+ json(405, { error: `/api/file 不支持 ${request.method} 请求。`, code: "method" });
1254
1272
  }
1255
1273
  catch (error) {
1256
1274
  const described = describeFilesError(error);
@@ -1329,7 +1347,7 @@ export function createHttpServer(options) {
1329
1347
  }
1330
1348
  if (url.pathname === "/api/upload") {
1331
1349
  if (request.method !== "POST" || !isLocalOrPrivateAddress(remote)) {
1332
- response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "Upload requires a loopback or private network POST" }));
1350
+ response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "上传需要从本机或局域网发起。" }));
1333
1351
  return;
1334
1352
  }
1335
1353
  const json = (status, body) => {
@@ -1341,7 +1359,7 @@ export function createHttpServer(options) {
1341
1359
  for await (const chunk of request) {
1342
1360
  totalBytes += chunk.length;
1343
1361
  if (totalBytes > MAX_UPLOAD_BODY_BYTES) {
1344
- json(413, { error: "Upload payload exceeds size limit" });
1362
+ json(413, { error: "图片超过了大小上限。" });
1345
1363
  request.destroy();
1346
1364
  return;
1347
1365
  }
@@ -1351,20 +1369,20 @@ export function createHttpServer(options) {
1351
1369
  const body = JSON.parse(raw);
1352
1370
  const conversationId = body.conversationId?.trim();
1353
1371
  if (!conversationId) {
1354
- json(400, { error: "conversationId is required" });
1372
+ json(400, { error: "缺少会话 ID。" });
1355
1373
  return;
1356
1374
  }
1357
1375
  const conversation = store.snapshot().conversations.find((c) => c.id === conversationId);
1358
1376
  if (!conversation) {
1359
- json(404, { error: `Conversation not found: ${conversationId}` });
1377
+ json(404, { error: "找不到这个会话——它可能已经被删除。" });
1360
1378
  return;
1361
1379
  }
1362
1380
  if (!body.mimeType || typeof body.mimeType !== "string") {
1363
- json(400, { error: "mimeType is required" });
1381
+ json(400, { error: "缺少图片类型。" });
1364
1382
  return;
1365
1383
  }
1366
1384
  if (!body.data || typeof body.data !== "string") {
1367
- json(400, { error: "data (base64) is required" });
1385
+ json(400, { error: "缺少图片数据。" });
1368
1386
  return;
1369
1387
  }
1370
1388
  const catalog = await catalogService.getCatalog(conversation.model);
@@ -1384,12 +1402,12 @@ export function createHttpServer(options) {
1384
1402
  }
1385
1403
  if (url.pathname.startsWith("/api/upload/")) {
1386
1404
  if (request.method !== "GET" || !isLocalOrPrivateAddress(remote)) {
1387
- response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "Upload access requires a loopback or private network GET" }));
1405
+ response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "读取上传的图片需要从本机或局域网发起。" }));
1388
1406
  return;
1389
1407
  }
1390
1408
  const pathParts = url.pathname.slice("/api/upload/".length).split("/").filter(Boolean);
1391
1409
  if (!pathParts.length || pathParts.length > 2) {
1392
- response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "Invalid upload path" }));
1410
+ response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "上传路径不合法。" }));
1393
1411
  return;
1394
1412
  }
1395
1413
  const convId = pathParts.length === 2 ? pathParts[0] : undefined;
@@ -1,4 +1,4 @@
1
- import type { PublicProvider } from "./types.js";
1
+ import type { DiscoveredModel, PublicProvider } from "./types.js";
2
2
  /**
3
3
  * THE PROVIDERS A PERSON ADDED, and the two files that make Pi know about them.
4
4
  *
@@ -158,3 +158,12 @@ export declare function publicProviders(providers: ProviderDefinition[]): Public
158
158
  * server cannot ask a follow-up question, so a refusal has to name the field and the reason.
159
159
  */
160
160
  export declare function validateProvider(input: unknown): ProviderDefinition;
161
+ /**
162
+ * Probes the provider's remote endpoint (OpenAI /models or Ollama /api/tags)
163
+ * and returns the list of available models with inferred metadata.
164
+ */
165
+ export declare function fetchRemoteModels(input: {
166
+ baseUrl: string;
167
+ apiKey?: string;
168
+ api?: string;
169
+ }): Promise<DiscoveredModel[]>;
@@ -394,3 +394,98 @@ export function validateProvider(input) {
394
394
  models: checked,
395
395
  };
396
396
  }
397
+ /**
398
+ * Probes the provider's remote endpoint (OpenAI /models or Ollama /api/tags)
399
+ * and returns the list of available models with inferred metadata.
400
+ */
401
+ export async function fetchRemoteModels(input) {
402
+ const rawBase = (input.baseUrl || "").trim();
403
+ if (!rawBase)
404
+ throw new Error("API 地址不能为空");
405
+ let parsed;
406
+ try {
407
+ parsed = new URL(rawBase);
408
+ }
409
+ catch {
410
+ throw new Error(`API 地址格式不正确:${rawBase}`);
411
+ }
412
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
413
+ throw new Error("API 地址必须以 http:// 或 https:// 开头");
414
+ }
415
+ const cleanBase = rawBase.replace(/\/+$/, "");
416
+ const headers = {
417
+ accept: "application/json",
418
+ };
419
+ if (input.apiKey && input.apiKey.trim()) {
420
+ headers.authorization = `Bearer ${input.apiKey.trim()}`;
421
+ }
422
+ const isOllama = input.api === "ollama" || cleanBase.includes(":11434");
423
+ const candidates = [];
424
+ if (isOllama) {
425
+ candidates.push(`${cleanBase}/api/tags`, `${cleanBase}/v1/models`, `${cleanBase}/models`);
426
+ }
427
+ else if (cleanBase.endsWith("/v1")) {
428
+ candidates.push(`${cleanBase}/models`);
429
+ }
430
+ else {
431
+ candidates.push(`${cleanBase}/v1/models`, `${cleanBase}/models`);
432
+ }
433
+ let lastError = "";
434
+ let payload = null;
435
+ for (const candidate of candidates) {
436
+ try {
437
+ const response = await fetch(candidate, {
438
+ headers,
439
+ signal: AbortSignal.timeout(10000),
440
+ });
441
+ if (!response.ok) {
442
+ lastError = `HTTP ${response.status} ${response.statusText}`;
443
+ continue;
444
+ }
445
+ payload = await response.json();
446
+ if (payload)
447
+ break;
448
+ }
449
+ catch (error) {
450
+ lastError = error instanceof Error ? error.message : String(error);
451
+ }
452
+ }
453
+ if (!payload) {
454
+ throw new Error(`无法从远程服务获取模型列表 (${lastError || "请求超时或无法连接"})`);
455
+ }
456
+ const rawList = Array.isArray(payload)
457
+ ? payload
458
+ : Array.isArray(payload.data)
459
+ ? payload.data
460
+ : Array.isArray(payload.models)
461
+ ? payload.models
462
+ : [];
463
+ if (rawList.length === 0) {
464
+ throw new Error("远程接口返回的模型列表为空");
465
+ }
466
+ const seen = new Set();
467
+ const discovered = [];
468
+ for (const item of rawList) {
469
+ if (!item || typeof item !== "object")
470
+ continue;
471
+ const rec = item;
472
+ const id = typeof rec.id === "string" ? rec.id.trim() : typeof rec.name === "string" ? rec.name.trim() : typeof rec.model === "string" ? rec.model.trim() : "";
473
+ if (!id || seen.has(id))
474
+ continue;
475
+ seen.add(id);
476
+ const lower = id.toLowerCase();
477
+ const reasoning = /r1|reason|o1|o3|thinking|qwq|deepseek-r1/i.test(lower);
478
+ const imageInput = /vision|vl|4o|gemini|claude-3|flash|pixtral/i.test(lower);
479
+ const contextWindow = typeof rec.context_window === "number" ? rec.context_window : DEFAULT_CONTEXT_WINDOW;
480
+ const maxTokens = reasoning ? 32768 : DEFAULT_MAX_TOKENS;
481
+ discovered.push({
482
+ id,
483
+ name: id,
484
+ reasoning,
485
+ imageInput,
486
+ contextWindow,
487
+ maxTokens,
488
+ });
489
+ }
490
+ return discovered.sort((a, b) => a.id.localeCompare(b.id));
491
+ }
@@ -436,7 +436,7 @@ function planRevert(state, conversationId, messageId, withdraw = false) {
436
436
  if (!withdraw && !boundary?.piEntryId)
437
437
  throw new Error("这条消息没有对应的 Pi 记录,不能作为回退位置。请在它之后的任意一条消息上回退。");
438
438
  if (withdraw && boundary.role !== "user")
439
- throw new Error("Only user messages can be withdrawn");
439
+ throw new Error("只能撤回你自己发送的消息。");
440
440
  const removed = conversation.messages.slice(boundaryIndex + (withdraw ? 0 : 1));
441
441
  // Unparseable stamps are compared as 0, i.e. "older than everything", which keeps a corrupted run
442
442
  // record ALIVE rather than deleting a child that may still be the user's most recent work. Deleting on a
@@ -719,10 +719,10 @@ export class Store {
719
719
  async createWorkspace(name, path) {
720
720
  const cleanName = name.trim();
721
721
  if (!cleanName || cleanName.length > 80)
722
- throw new Error("Workspace name must be 1-80 characters");
722
+ throw new Error("工作区名称需要 1-80 个字符。");
723
723
  const cleanPath = path?.trim() || process.cwd();
724
724
  if (cleanPath.length > 500)
725
- throw new Error("Workspace path is too long");
725
+ throw new Error("工作区路径太长了。");
726
726
  const workspace = { id: id("workspace"), name: cleanName, path: cleanPath, updatedAt: now() };
727
727
  this.state.workspaces.push(workspace);
728
728
  await this.persist();
@@ -731,10 +731,10 @@ export class Store {
731
731
  async renameWorkspace(workspaceId, name) {
732
732
  const cleanName = name.trim();
733
733
  if (!cleanName || cleanName.length > 80)
734
- throw new Error("Workspace name must be 1-80 characters");
734
+ throw new Error("工作区名称需要 1-80 个字符。");
735
735
  const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
736
736
  if (!workspace)
737
- throw new Error("Workspace not found");
737
+ throw new Error("找不到这个工作区。");
738
738
  workspace.name = cleanName;
739
739
  workspace.updatedAt = now();
740
740
  await this.persist();
@@ -761,7 +761,7 @@ export class Store {
761
761
  async setWorkspaceDefaults(workspaceId, defaults) {
762
762
  const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
763
763
  if (!workspace)
764
- throw new Error("Workspace not found");
764
+ throw new Error("找不到这个工作区。");
765
765
  if (defaults.model === undefined)
766
766
  delete workspace.defaultModel;
767
767
  else
@@ -781,9 +781,9 @@ export class Store {
781
781
  }
782
782
  async deleteWorkspace(workspaceId) {
783
783
  if (!this.state.workspaces.some((item) => item.id === workspaceId))
784
- throw new Error("Workspace not found");
784
+ throw new Error("找不到这个工作区。");
785
785
  if (this.state.workspaces.length <= 1)
786
- throw new Error("Cannot delete the last workspace");
786
+ throw new Error("至少要保留一个工作区,不能把最后一个删掉。");
787
787
  // Cascade: workspaces and conversations live in one state.json, so this single atomic persist removes
788
788
  // both together. A waiting message needs no sweep of its own any more - it is a message inside one of
789
789
  // the conversations being removed, so it goes with its container, which is the whole reason it is a
@@ -799,7 +799,7 @@ export class Store {
799
799
  async createConversation(workspaceId, configuration) {
800
800
  const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
801
801
  if (!workspace)
802
- throw new Error("Workspace not found");
802
+ throw new Error("找不到这个工作区。");
803
803
  const conversation = { id: id("conversation"), title: DEFAULT_CONVERSATION_TITLE, workspaceId, updatedAt: now(), status: "idle", messages: [] };
804
804
  // INHERITANCE, AS A COPY AT BIRTH. The workspace's defaults are stamped onto the new conversation
805
805
  // here and never consulted again: the session picker edits the conversation's own fields, so an
@@ -952,7 +952,7 @@ export class Store {
952
952
  async renameConversation(conversationId, name) {
953
953
  const cleanName = name.trim();
954
954
  if (!cleanName || cleanName.length > 80)
955
- throw new Error("Conversation name must be 1-80 characters");
955
+ throw new Error("会话名称需要 1-80 个字符。");
956
956
  const conversation = this.requireConversation(conversationId);
957
957
  conversation.title = cleanName;
958
958
  conversation.titleLocked = true;
@@ -1167,7 +1167,7 @@ export class Store {
1167
1167
  const conversation = this.requireConversation(conversationId);
1168
1168
  const message = conversation.messages.find((item) => item.id === messageId);
1169
1169
  if (!message)
1170
- throw new Error("Assistant message not found");
1170
+ throw new Error("找不到对应的助手消息。");
1171
1171
  message.content += delta;
1172
1172
  conversation.updatedAt = now();
1173
1173
  }
@@ -1189,7 +1189,7 @@ export class Store {
1189
1189
  const conversation = this.requireConversation(conversationId);
1190
1190
  const message = conversation.messages.find((item) => item.id === messageId);
1191
1191
  if (!message)
1192
- throw new Error("Assistant message not found");
1192
+ throw new Error("找不到对应的助手消息。");
1193
1193
  message.thinking = `${message.thinking ?? ""}${delta}`;
1194
1194
  const blocks = (message.thinkingBlocks ??= []);
1195
1195
  const last = blocks.at(-1);
@@ -1213,7 +1213,7 @@ export class Store {
1213
1213
  const conversation = this.requireConversation(conversationId);
1214
1214
  const message = conversation.messages.find((item) => item.id === messageId);
1215
1215
  if (!message)
1216
- throw new Error("Assistant message not found");
1216
+ throw new Error("找不到对应的助手消息。");
1217
1217
  await this.persistTransaction({ kind: "message.replace", conversationId, message: structuredClone(message), updatedAt: conversation.updatedAt });
1218
1218
  }
1219
1219
  /**
@@ -1240,7 +1240,7 @@ export class Store {
1240
1240
  const conversation = this.requireConversation(conversationId);
1241
1241
  const message = conversation.messages.find((item) => item.id === messageId);
1242
1242
  if (!message)
1243
- throw new Error("Assistant message not found");
1243
+ throw new Error("找不到对应的助手消息。");
1244
1244
  const calls = (message.toolCalls ??= []);
1245
1245
  // WHERE IN THE ANSWER THIS CALL HAPPENED, stamped here because this is the only place that knows both
1246
1246
  // halves: the call arrived, and the message already holds every character streamed before it. Without
@@ -1287,7 +1287,7 @@ export class Store {
1287
1287
  const conversation = this.requireConversation(conversationId);
1288
1288
  const message = conversation.messages.find((item) => item.id === messageId);
1289
1289
  if (!message)
1290
- throw new Error("Assistant message not found");
1290
+ throw new Error("找不到对应的助手消息。");
1291
1291
  const calls = message.toolCalls;
1292
1292
  const call = calls?.[index];
1293
1293
  if (!calls || !call)
@@ -1323,7 +1323,7 @@ export class Store {
1323
1323
  const conversation = this.requireConversation(conversationId);
1324
1324
  const message = conversation.messages.find((item) => item.id === messageId);
1325
1325
  if (!message)
1326
- throw new Error("Assistant message not found");
1326
+ throw new Error("找不到对应的助手消息。");
1327
1327
  const calls = message.toolCalls;
1328
1328
  const call = calls?.[index];
1329
1329
  if (!calls || !call)
@@ -1389,7 +1389,7 @@ export class Store {
1389
1389
  const conversation = this.requireConversation(conversationId);
1390
1390
  const message = conversation.messages.find((item) => item.id === messageId);
1391
1391
  if (!message)
1392
- throw new Error("Assistant message not found");
1392
+ throw new Error("找不到对应的助手消息。");
1393
1393
  const calls = message.toolCalls;
1394
1394
  if (!calls)
1395
1395
  return [];
@@ -1491,7 +1491,7 @@ export class Store {
1491
1491
  return plan;
1492
1492
  }
1493
1493
  requireConversation(conversationId) { const conversation = this.state.conversations.find((item) => item.id === conversationId); if (!conversation)
1494
- throw new Error("Conversation not found"); return conversation; }
1494
+ throw new Error("找不到这个会话。"); return conversation; }
1495
1495
  // ------------------------------------------------------------- Waiting messages
1496
1496
  //
1497
1497
  // A message the user sent while the agent was working WAITS HERE, and "here" is a field on the message
@@ -81,6 +81,7 @@ function loadPty() {
81
81
  }
82
82
  catch (error) {
83
83
  ptyLoadFailure = error instanceof Error ? error.message : String(error);
84
+ console.warn(`Super Pi: 终端组件 node-pty 加载失败,右侧栏终端将不可用:${ptyLoadFailure}`);
84
85
  return undefined;
85
86
  }
86
87
  }
@@ -172,8 +173,9 @@ export function isTerminalEnabled(env = process.env) {
172
173
  }
173
174
  /** The sentence a browser shows when the terminal is missing. Says WHICH of the three gates closed. */
174
175
  export function terminalDisabledReason(_env = process.env) {
176
+ // The raw native error is logged when it happens (loadPty); the browser gets a sentence it can act on.
175
177
  if (ptyLoadFailure)
176
- return "node-pty unavailable: " + ptyLoadFailure;
178
+ return "终端组件(node-pty)没有加载成功,请重新安装依赖或重启 Super Pi。";
177
179
  return "";
178
180
  }
179
181
  export class TerminalService {