@evandu/super-pi 0.1.135 → 0.1.137
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/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +5 -5
- package/dist/client/assets/{CodeMirrorEditor-D6awRUZO.js → CodeMirrorEditor-CkA_5D5r.js} +2 -2
- package/dist/client/assets/{dist-BGgidsaF.js → dist-9Fm8XKu4.js} +1 -1
- package/dist/client/assets/{dist-spB2Q2CG.js → dist-B02Erd0t.js} +1 -1
- package/dist/client/assets/{dist-DoDmoWTn.js → dist-B3Mfuv1D.js} +1 -1
- package/dist/client/assets/{dist-yTTCht0J.js → dist-BfEmujEC.js} +1 -1
- package/dist/client/assets/{dist-WkVUFX28.js → dist-BqGjijI6.js} +1 -1
- package/dist/client/assets/{dist-DOwrSiuY.js → dist-BsqzrZ5R.js} +1 -1
- package/dist/client/assets/{dist-BYMYC_2I.js → dist-BySjislr.js} +1 -1
- package/dist/client/assets/{dist-Cwb-Lb5-.js → dist-C2tSd-AY.js} +1 -1
- package/dist/client/assets/{dist-DCiuVvGU.js → dist-CrxmpY93.js} +1 -1
- package/dist/client/assets/{dist-1jSyapdW.js → dist-CsNytPug.js} +1 -1
- package/dist/client/assets/{dist-mPR8uuzk.js → dist-D-LtG92p.js} +1 -1
- package/dist/client/assets/{dist-DeZsNbFx.js → dist-DGhdkud4.js} +1 -1
- package/dist/client/assets/{dist-BkVaPA_q.js → dist-DL4Q05ts.js} +1 -1
- package/dist/client/assets/{dist-CNuKauOq.js → dist-D_umYm0o.js} +1 -1
- package/dist/client/assets/{dist-CubYGjAk.js → dist-DbovsYeH.js} +1 -1
- package/dist/client/assets/{dist-C46474ys.js → dist-DcdzfM-n.js} +1 -1
- package/dist/client/assets/{dist-BoeM5xW7.js → dist-DdVs8Y0X.js} +1 -1
- package/dist/client/assets/{dist-DHBWUQhM.js → dist-MC2XuZgq.js} +1 -1
- package/dist/client/assets/{dist-D0Iw5XQA.js → dist-b9CfAgzG.js} +1 -1
- package/dist/client/assets/{dist-lZUWPQbT.js → dist-j9X7j381.js} +1 -1
- package/dist/client/assets/{dist-CCkdCH6a.js → dist-k6bOz9RS.js} +1 -1
- package/dist/client/assets/{dist-Dk3e6RfY.js → dist-ufRHdQ8G.js} +1 -1
- package/dist/client/assets/{index-Dyta-HkQ.js → index-DVmE8H4Y.js} +12 -12
- package/dist/client/assets/index-FxTrUapG.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/active-memory.d.ts +17 -2
- package/dist/server/active-memory.js +76 -8
- package/dist/server/application/catalog-service.js +8 -6
- package/dist/server/application/conversation-service.js +4 -2
- package/dist/server/application/scheduled-task-service.d.ts +7 -0
- package/dist/server/application/scheduled-task-service.js +14 -5
- package/dist/server/files.js +11 -1
- package/dist/server/http-server.d.ts +0 -3
- package/dist/server/http-server.js +131 -87
- package/dist/server/model-filter.d.ts +13 -3
- package/dist/server/model-filter.js +7 -5
- package/dist/server/store.d.ts +27 -0
- package/dist/server/store.js +44 -0
- package/dist/server/subagents.js +10 -2
- package/package.json +9 -1
- package/dist/client/assets/index-V93Kzott.css +0 -1
|
@@ -230,6 +230,47 @@ function abortedAs(request) {
|
|
|
230
230
|
/**
|
|
231
231
|
* Creates the HTTP server providing static client assets and all `/api/*` endpoints.
|
|
232
232
|
*/
|
|
233
|
+
/**
|
|
234
|
+
* A REQUEST BODY BEING READ AS TEXT, decoded across chunk boundaries.
|
|
235
|
+
*
|
|
236
|
+
* The counterpart of `readRequestText` for the sites that need to inspect the body WHILE it streams
|
|
237
|
+
* (the file uploads, which refuse an oversized body without reading the rest of it).
|
|
238
|
+
*/
|
|
239
|
+
function textBodyReader() {
|
|
240
|
+
const decoder = new TextDecoder("utf-8");
|
|
241
|
+
let text = "";
|
|
242
|
+
return {
|
|
243
|
+
push: (chunk) => { text += decoder.decode(chunk, { stream: true }); return text; },
|
|
244
|
+
finish: () => text + decoder.decode(),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* THE REQUEST BODY AS TEXT, DECODED ACROSS CHUNK BOUNDARIES.
|
|
249
|
+
*
|
|
250
|
+
* `raw += chunk` decodes EVERY CHUNK ON ITS OWN: the chunk is a Buffer, and concatenating it into a
|
|
251
|
+
* string calls `toString("utf8")` on it. A UTF-8 character that straddles two chunks is then two
|
|
252
|
+
* half-characters, and each half decodes to U+FFFD - the replacement character. So a Chinese
|
|
253
|
+
* sentence that happens to be split in the wrong place is silently corrupted, and the corrupted text
|
|
254
|
+
* is what the endpoint PERSISTS (a knowledge note, a memory, a scheduled task). The corruption is
|
|
255
|
+
* invisible - the JSON still parses - and permanent.
|
|
256
|
+
*
|
|
257
|
+
* A `TextDecoder` in streaming mode keeps the incomplete tail of a chunk and completes it with the
|
|
258
|
+
* next one, which is what "the body is one UTF-8 document" means. The size guards count BYTES, which
|
|
259
|
+
* is what a body limit is about anyway.
|
|
260
|
+
*/
|
|
261
|
+
async function readRequestText(request, limitBytes) {
|
|
262
|
+
const decoder = new TextDecoder("utf-8");
|
|
263
|
+
let text = "";
|
|
264
|
+
let bytes = 0;
|
|
265
|
+
for await (const chunk of request) {
|
|
266
|
+
const piece = chunk;
|
|
267
|
+
bytes += piece.length;
|
|
268
|
+
if (bytes > limitBytes)
|
|
269
|
+
return { text: "", tooLarge: true };
|
|
270
|
+
text += decoder.decode(piece, { stream: true });
|
|
271
|
+
}
|
|
272
|
+
return { text: text + decoder.decode(), tooLarge: false };
|
|
273
|
+
}
|
|
233
274
|
export function createHttpServer(options) {
|
|
234
275
|
const { context, onShutdown } = options;
|
|
235
276
|
const { store, paths, accessToken, broadcaster, runService, catalogService, execService, toolRegistry, toolRunner } = context;
|
|
@@ -320,16 +361,12 @@ export function createHttpServer(options) {
|
|
|
320
361
|
// BYTES, NOT CHARACTERS, AND FROM THE REGISTRY'S OWN CONSTANT. The old cap counted UTF-16 code
|
|
321
362
|
// units at 256,000, below the registry's own 512,000-byte asset limit, so JSON escaping decided
|
|
322
363
|
// where the real ceiling was and an exported bundle could be refused on the way back in.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (bytes > MAX_TOOL_BODY_BYTES) {
|
|
328
|
-
json(413, { error: "导入的内容太大,超过了大小上限。" });
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
raw += piece.toString("utf8");
|
|
364
|
+
const imported = await readRequestText(request, MAX_TOOL_BODY_BYTES);
|
|
365
|
+
if (imported.tooLarge) {
|
|
366
|
+
json(413, { error: "导入的内容太大,超过了大小上限。" });
|
|
367
|
+
return;
|
|
332
368
|
}
|
|
369
|
+
const raw = imported.text;
|
|
333
370
|
const body = JSON.parse(raw);
|
|
334
371
|
if (!isAuthorized(request, accessToken)) {
|
|
335
372
|
if (!body.conversationId) {
|
|
@@ -404,14 +441,12 @@ export function createHttpServer(options) {
|
|
|
404
441
|
toolApi(405, { error: "method" });
|
|
405
442
|
return;
|
|
406
443
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
toolApi(413, { error: "request too large" });
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
444
|
+
const executed = await readRequestText(request, 64_000);
|
|
445
|
+
if (executed.tooLarge) {
|
|
446
|
+
toolApi(413, { error: "request too large" });
|
|
447
|
+
return;
|
|
414
448
|
}
|
|
449
|
+
const raw = executed.text;
|
|
415
450
|
try {
|
|
416
451
|
const body = JSON.parse(raw);
|
|
417
452
|
const resolved = await toolRegistry.resolve(id);
|
|
@@ -501,14 +536,12 @@ export function createHttpServer(options) {
|
|
|
501
536
|
toolApi(405, { error: "method" });
|
|
502
537
|
return;
|
|
503
538
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
toolApi(413, { error: "request too large" });
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
539
|
+
const asked = await readRequestText(request, 64_000);
|
|
540
|
+
if (asked.tooLarge) {
|
|
541
|
+
toolApi(413, { error: "request too large" });
|
|
542
|
+
return;
|
|
511
543
|
}
|
|
544
|
+
const raw = asked.text;
|
|
512
545
|
try {
|
|
513
546
|
const body = JSON.parse(raw);
|
|
514
547
|
toolApi(202, context.toolLlm.start({ toolId: id, prompt: body.prompt ?? "", system: body.system, timeoutMs: body.timeoutMs }));
|
|
@@ -598,14 +631,10 @@ export function createHttpServer(options) {
|
|
|
598
631
|
return;
|
|
599
632
|
}
|
|
600
633
|
try {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (totalBytes > 512000)
|
|
606
|
-
throw new Error("记忆请求过大");
|
|
607
|
-
raw += chunk;
|
|
608
|
-
}
|
|
634
|
+
const remembered = await readRequestText(request, 512000);
|
|
635
|
+
if (remembered.tooLarge)
|
|
636
|
+
throw new Error("记忆请求过大");
|
|
637
|
+
const raw = remembered.text;
|
|
609
638
|
const body = raw ? JSON.parse(raw) : {};
|
|
610
639
|
const result = url.pathname.endsWith("/tool")
|
|
611
640
|
? await context.memory.tool(String(request.headers["x-memory-capability"] ?? ""), body)
|
|
@@ -634,9 +663,10 @@ export function createHttpServer(options) {
|
|
|
634
663
|
let name = url.searchParams.get("name") ?? url.searchParams.get("skillId") ?? "";
|
|
635
664
|
if (request.method === "POST") {
|
|
636
665
|
try {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
666
|
+
const asked = await readRequestText(request, 512000);
|
|
667
|
+
if (asked.tooLarge)
|
|
668
|
+
throw new Error("记忆请求过大");
|
|
669
|
+
const raw = asked.text;
|
|
640
670
|
if (raw) {
|
|
641
671
|
const body = JSON.parse(raw);
|
|
642
672
|
if (body.workspaceId)
|
|
@@ -673,9 +703,12 @@ export function createHttpServer(options) {
|
|
|
673
703
|
return;
|
|
674
704
|
}
|
|
675
705
|
try {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
706
|
+
const asked = await readRequestText(request, 512000);
|
|
707
|
+
if (asked.tooLarge) {
|
|
708
|
+
json(413, { error: "请求过大" });
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const raw = asked.text;
|
|
679
712
|
const body = raw ? JSON.parse(raw) : {};
|
|
680
713
|
const models = await context.providerService.fetchModels(body);
|
|
681
714
|
json(200, { ok: true, models });
|
|
@@ -704,14 +737,12 @@ export function createHttpServer(options) {
|
|
|
704
737
|
let totalBytes = 0;
|
|
705
738
|
// A pasted document rides in the body, so the cap is the document cap - past it the request is
|
|
706
739
|
// refused outright rather than truncated into a document that silently lost its tail.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
raw += chunk;
|
|
740
|
+
const known = await readRequestText(request, MAX_KNOWLEDGE_REQUEST_BYTES);
|
|
741
|
+
if (known.tooLarge) {
|
|
742
|
+
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "知识库请求过大" }));
|
|
743
|
+
return;
|
|
714
744
|
}
|
|
745
|
+
raw = known.text;
|
|
715
746
|
const body = raw ? JSON.parse(raw) : {};
|
|
716
747
|
const result = url.pathname.endsWith("/tool")
|
|
717
748
|
? await context.knowledge.tool(String(request.headers["x-knowledge-capability"] ?? ""), body, abortedAs(request))
|
|
@@ -743,14 +774,12 @@ export function createHttpServer(options) {
|
|
|
743
774
|
// A DOCUMENT'S TEXT RIDES IN THE REQUEST BODY, so the cap is the document cap: a request
|
|
744
775
|
// past it is refused outright rather than truncated into a document that silently lost its
|
|
745
776
|
// tail.
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
raw += chunk;
|
|
777
|
+
const documented = await readRequestText(request, MAX_DOCUMENT_REQUEST_BYTES);
|
|
778
|
+
if (documented.tooLarge) {
|
|
779
|
+
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "文档请求过大" }));
|
|
780
|
+
return;
|
|
753
781
|
}
|
|
782
|
+
raw = documented.text;
|
|
754
783
|
const body = raw ? JSON.parse(raw) : {};
|
|
755
784
|
// The capability says WHICH CONVERSATION is asking, and the workspace is derived from it
|
|
756
785
|
// rather than taken from the request: a request that named its own workspace would be a
|
|
@@ -787,14 +816,12 @@ export function createHttpServer(options) {
|
|
|
787
816
|
}
|
|
788
817
|
try {
|
|
789
818
|
let raw = "", totalBytes = 0;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
raw += chunk;
|
|
819
|
+
const asked = await readRequestText(request, MAX_WEB_REQUEST_BYTES);
|
|
820
|
+
if (asked.tooLarge) {
|
|
821
|
+
response.writeHead(413).end();
|
|
822
|
+
return;
|
|
797
823
|
}
|
|
824
|
+
raw = asked.text;
|
|
798
825
|
const conversationId = String(request.headers["x-web-conversation"] ?? "");
|
|
799
826
|
const verdict = verifyCapability(conversationId, request.headers["x-web-capability"], { running: context.runService.isRunning(conversationId) });
|
|
800
827
|
if (!verdict.ok) {
|
|
@@ -831,14 +858,12 @@ export function createHttpServer(options) {
|
|
|
831
858
|
try {
|
|
832
859
|
let raw = "", totalBytes = 0;
|
|
833
860
|
// The Markdown of a document rides in the request body, so the cap is the document cap.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
raw += chunk;
|
|
861
|
+
const asked = await readRequestText(request, MAX_FEISHU_REQUEST_BYTES);
|
|
862
|
+
if (asked.tooLarge) {
|
|
863
|
+
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "飞书请求过大" }));
|
|
864
|
+
return;
|
|
841
865
|
}
|
|
866
|
+
raw = asked.text;
|
|
842
867
|
const conversationId = String(request.headers["x-feishu-conversation"] ?? "");
|
|
843
868
|
// THE SAME PROOF THE DOCUMENT TOOLS USE: the request must carry the session's own
|
|
844
869
|
// capability, and the session must actually be running a turn. The Feishu credential is a
|
|
@@ -1050,9 +1075,12 @@ export function createHttpServer(options) {
|
|
|
1050
1075
|
json(403, { error: "Git 写操作只允许来自本机或局域网的请求。" });
|
|
1051
1076
|
return;
|
|
1052
1077
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1078
|
+
const asked = await readRequestText(request, 512000);
|
|
1079
|
+
if (asked.tooLarge) {
|
|
1080
|
+
json(413, { error: "请求过大" });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const raw = asked.text;
|
|
1056
1084
|
const body = raw ? JSON.parse(raw) : {};
|
|
1057
1085
|
json(200, await gitMutate(workspace.path, op, body.path, body.message));
|
|
1058
1086
|
return;
|
|
@@ -1113,15 +1141,18 @@ export function createHttpServer(options) {
|
|
|
1113
1141
|
}
|
|
1114
1142
|
let raw = "";
|
|
1115
1143
|
let totalBytes = 0;
|
|
1144
|
+
const reader = textBodyReader();
|
|
1116
1145
|
for await (const chunk of request) {
|
|
1117
|
-
|
|
1146
|
+
const piece = chunk;
|
|
1147
|
+
totalBytes += piece.length;
|
|
1118
1148
|
if (totalBytes > MAX_FILE_BYTES * 2) {
|
|
1119
1149
|
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "请求体过大", code: "too_large" }));
|
|
1120
1150
|
request.destroy();
|
|
1121
1151
|
return;
|
|
1122
1152
|
}
|
|
1123
|
-
raw
|
|
1153
|
+
raw = reader.push(piece);
|
|
1124
1154
|
}
|
|
1155
|
+
raw = reader.finish();
|
|
1125
1156
|
const json = (status, body) => {
|
|
1126
1157
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
1127
1158
|
};
|
|
@@ -1151,15 +1182,18 @@ export function createHttpServer(options) {
|
|
|
1151
1182
|
}
|
|
1152
1183
|
let raw = "";
|
|
1153
1184
|
let totalBytes = 0;
|
|
1185
|
+
const reader = textBodyReader();
|
|
1154
1186
|
for await (const chunk of request) {
|
|
1155
|
-
|
|
1187
|
+
const piece = chunk;
|
|
1188
|
+
totalBytes += piece.length;
|
|
1156
1189
|
if (totalBytes > MAX_FILE_BYTES * 2) {
|
|
1157
1190
|
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "请求体过大", code: "too_large" }));
|
|
1158
1191
|
request.destroy();
|
|
1159
1192
|
return;
|
|
1160
1193
|
}
|
|
1161
|
-
raw
|
|
1194
|
+
raw = reader.push(piece);
|
|
1162
1195
|
}
|
|
1196
|
+
raw = reader.finish();
|
|
1163
1197
|
const json = (status, body) => {
|
|
1164
1198
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
1165
1199
|
};
|
|
@@ -1234,15 +1268,18 @@ export function createHttpServer(options) {
|
|
|
1234
1268
|
}
|
|
1235
1269
|
let raw = "";
|
|
1236
1270
|
let totalBytes = 0;
|
|
1271
|
+
const reader = textBodyReader();
|
|
1237
1272
|
for await (const chunk of request) {
|
|
1238
|
-
|
|
1273
|
+
const piece = chunk;
|
|
1274
|
+
totalBytes += piece.length;
|
|
1239
1275
|
if (totalBytes > MAX_FILE_BYTES * 2) {
|
|
1240
1276
|
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "请求体过大", code: "too_large" }));
|
|
1241
1277
|
request.destroy();
|
|
1242
1278
|
return;
|
|
1243
1279
|
}
|
|
1244
|
-
raw
|
|
1280
|
+
raw = reader.push(piece);
|
|
1245
1281
|
}
|
|
1282
|
+
raw = reader.finish();
|
|
1246
1283
|
const json = (status, body) => {
|
|
1247
1284
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
1248
1285
|
};
|
|
@@ -1262,15 +1299,18 @@ export function createHttpServer(options) {
|
|
|
1262
1299
|
}
|
|
1263
1300
|
let raw = "";
|
|
1264
1301
|
let totalBytes = 0;
|
|
1302
|
+
const reader = textBodyReader();
|
|
1265
1303
|
for await (const chunk of request) {
|
|
1266
|
-
|
|
1304
|
+
const piece = chunk;
|
|
1305
|
+
totalBytes += piece.length;
|
|
1267
1306
|
if (totalBytes > MAX_FILE_BYTES * 2) {
|
|
1268
1307
|
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "请求体过大", code: "too_large" }));
|
|
1269
1308
|
request.destroy();
|
|
1270
1309
|
return;
|
|
1271
1310
|
}
|
|
1272
|
-
raw
|
|
1311
|
+
raw = reader.push(piece);
|
|
1273
1312
|
}
|
|
1313
|
+
raw = reader.finish();
|
|
1274
1314
|
const json = (status, body) => {
|
|
1275
1315
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
1276
1316
|
};
|
|
@@ -1303,15 +1343,18 @@ export function createHttpServer(options) {
|
|
|
1303
1343
|
}
|
|
1304
1344
|
let raw = "";
|
|
1305
1345
|
let totalBytes = 0;
|
|
1346
|
+
const reader = textBodyReader();
|
|
1306
1347
|
for await (const chunk of request) {
|
|
1307
|
-
|
|
1348
|
+
const piece = chunk;
|
|
1349
|
+
totalBytes += piece.length;
|
|
1308
1350
|
if (totalBytes > MAX_FILE_BYTES * 2) {
|
|
1309
1351
|
response.writeHead(413, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "请求体过大", code: "too_large" }));
|
|
1310
1352
|
request.destroy();
|
|
1311
1353
|
return;
|
|
1312
1354
|
}
|
|
1313
|
-
raw
|
|
1355
|
+
raw = reader.push(piece);
|
|
1314
1356
|
}
|
|
1357
|
+
raw = reader.finish();
|
|
1315
1358
|
const json = (status, body) => {
|
|
1316
1359
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify(body));
|
|
1317
1360
|
};
|
|
@@ -1407,14 +1450,12 @@ export function createHttpServer(options) {
|
|
|
1407
1450
|
// body cost its full size in memory before the 403 said no. A question is a few hundred bytes;
|
|
1408
1451
|
// 64KB is orders of magnitude above any honest one and far below what a hostile body could use,
|
|
1409
1452
|
// and the shape is the same ceiling the `/api/tools` endpoints already enforce.
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
response.writeHead(413, { "content-type": "application/json" }).end(JSON.stringify({ error: "question request too large" }));
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1453
|
+
const asked = await readRequestText(request, 64_000);
|
|
1454
|
+
if (asked.tooLarge) {
|
|
1455
|
+
response.writeHead(413, { "content-type": "application/json" }).end(JSON.stringify({ error: "question request too large" }));
|
|
1456
|
+
return;
|
|
1417
1457
|
}
|
|
1458
|
+
const raw = asked.text;
|
|
1418
1459
|
try {
|
|
1419
1460
|
const body = JSON.parse(raw);
|
|
1420
1461
|
if (!body.conversationId || !body.question || !Array.isArray(body.options))
|
|
@@ -1633,15 +1674,18 @@ export function createHttpServer(options) {
|
|
|
1633
1674
|
}
|
|
1634
1675
|
let raw = "";
|
|
1635
1676
|
let totalBytes = 0;
|
|
1677
|
+
const reader = textBodyReader();
|
|
1636
1678
|
for await (const chunk of request) {
|
|
1637
|
-
|
|
1679
|
+
const piece = chunk;
|
|
1680
|
+
totalBytes += piece.length;
|
|
1638
1681
|
if (totalBytes > MAX_FILE_BYTES * 2) {
|
|
1639
1682
|
json(413, { error: "请求体过大", code: "too_large" });
|
|
1640
1683
|
request.destroy();
|
|
1641
1684
|
return;
|
|
1642
1685
|
}
|
|
1643
|
-
raw
|
|
1686
|
+
raw = reader.push(piece);
|
|
1644
1687
|
}
|
|
1688
|
+
raw = reader.finish();
|
|
1645
1689
|
const declared = Number(request.headers["content-length"] ?? "0");
|
|
1646
1690
|
if (Number.isFinite(declared) && declared > MAX_FILE_BYTES * 2) {
|
|
1647
1691
|
json(413, { error: "请求体过大", code: "too_large" });
|
|
@@ -6,8 +6,18 @@ export declare function applicableRules(rules: ModelFilterRule[] | undefined, wo
|
|
|
6
6
|
export declare function assertModelAllowed(rules: ModelFilterRule[] | undefined, workspace: Workspace, model: ModelSelection | undefined): void;
|
|
7
7
|
export declare function conversationWorkspace(state: AppState, conversationId: string): Workspace;
|
|
8
8
|
/** Resolve Pi's actual global default when no model was selected; never guess a permitted replacement. */
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* THE STORE READS THIS NEEDS, AND NOTHING ELSE.
|
|
11
|
+
*
|
|
12
|
+
* The check wants two things: the rules, and the workspace the conversation is in. Both now have a
|
|
13
|
+
* narrow reader on the store (`modelFilterRules()` / `workspaceForConversation()`), so this path
|
|
14
|
+
* clones NOTHING - it used to deep-clone the whole application three times over, on the way to
|
|
15
|
+
* sending a message.
|
|
16
|
+
*/
|
|
17
|
+
export interface ModelFilterStore {
|
|
18
|
+
modelFilterRules(): import("./types.js").ModelFilterRule[];
|
|
19
|
+
workspaceForConversation(conversationId: string): Workspace | undefined;
|
|
20
|
+
}
|
|
21
|
+
export declare function validateConversationModel(store: ModelFilterStore, runtime: {
|
|
12
22
|
catalog(selection?: ModelSelection): Promise<import("./types.js").ModelCatalog>;
|
|
13
23
|
}, conversationId: string, selection?: ModelSelection): Promise<void>;
|
|
@@ -29,18 +29,20 @@ export function conversationWorkspace(state, conversationId) {
|
|
|
29
29
|
throw new Error("会话所属工作区不存在");
|
|
30
30
|
return workspace;
|
|
31
31
|
}
|
|
32
|
-
/** Resolve Pi's actual global default when no model was selected; never guess a permitted replacement. */
|
|
33
32
|
export async function validateConversationModel(store, runtime, conversationId, selection) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const workspace = store.workspaceForConversation(conversationId);
|
|
34
|
+
if (!workspace)
|
|
35
|
+
throw new Error("会话所属工作区不存在");
|
|
36
|
+
if (!applicableRules(store.modelFilterRules(), workspace).length)
|
|
37
37
|
return;
|
|
38
38
|
const startedAt = performance.now();
|
|
39
39
|
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
40
40
|
console.warn(`[super-pi latency] conversation=${conversationId} phase=model.validate.begin selection=${selection ? `${selection.provider}/${selection.modelId}` : "default"}`);
|
|
41
41
|
const catalog = await runtime.catalog();
|
|
42
42
|
const model = selection ? catalog.models.find(item => item.provider === selection.provider && item.modelId === selection.modelId) ?? selection : catalog.defaultModel;
|
|
43
|
-
|
|
43
|
+
// READ AGAIN, NOT CACHED: the catalog fetch above is an await, and a rule edited during it must
|
|
44
|
+
// still be enforced for this message. The narrow readers make that re-read cheap.
|
|
45
|
+
assertModelAllowed(store.modelFilterRules(), store.workspaceForConversation(conversationId), model);
|
|
44
46
|
if (process.env.SUPER_PI_TRACE_LATENCY === "1")
|
|
45
47
|
console.warn(`[super-pi latency] conversation=${conversationId} phase=model.validate.end elapsedMs=${Math.round(performance.now() - startedAt)}`);
|
|
46
48
|
}
|
package/dist/server/store.d.ts
CHANGED
|
@@ -96,6 +96,33 @@ export declare class Store {
|
|
|
96
96
|
static open(filePath: string): Promise<Store>;
|
|
97
97
|
/** Opens an injected repository, retaining domain reconciliation here. */
|
|
98
98
|
static openRepository(repository: AppStateRepository): Promise<Store>;
|
|
99
|
+
/**
|
|
100
|
+
* THE MODEL FILTER RULES, WITHOUT CLONING THE STATE TO READ THEM.
|
|
101
|
+
*
|
|
102
|
+
* `snapshot()` deep-clones the WHOLE application - every workspace, every conversation, every
|
|
103
|
+
* message of every conversation - and the model filter check needs ONE small array. Reading that
|
|
104
|
+
* array through a full clone is the difference between a few hundred bytes and the entire
|
|
105
|
+
* transcript of the app, on the path that runs every time a message is sent.
|
|
106
|
+
*
|
|
107
|
+
* So the rules get their own narrow reader. It returns a COPY of the array (the caller may keep
|
|
108
|
+
* it, and the store's own state must not change under it), and the individual rules are treated
|
|
109
|
+
* as read-only - which is what the filter does with them.
|
|
110
|
+
*/
|
|
111
|
+
modelFilterRules(): import("./types.js").ModelFilterRule[];
|
|
112
|
+
/**
|
|
113
|
+
* THE WORKSPACE A CONVERSATION BELONGS TO, WITHOUT CLONING THE STATE TO FIND ONE ROW.
|
|
114
|
+
*
|
|
115
|
+
* The same argument as the rules above: the filter needs to know WHICH workspace the conversation
|
|
116
|
+
* is in (the rules are per workspace), and the only way to know that today is to clone everything
|
|
117
|
+
* and look. This returns a shallow copy of the one workspace - cheap, and detached enough that a
|
|
118
|
+
* caller cannot reach into the store's state through it.
|
|
119
|
+
*/
|
|
120
|
+
workspaceForConversation(conversationId: string): Workspace | undefined;
|
|
121
|
+
workspace(workspaceId: string): Workspace | undefined;
|
|
122
|
+
/** Small detached rows used when refreshing each conversation's scoped model catalog. */
|
|
123
|
+
catalogConversationTargets(): Array<Pick<Conversation, "id" | "archivedAt" | "model">>;
|
|
124
|
+
/** Clone only one workspace's transcripts, never unrelated workspaces or app state. */
|
|
125
|
+
conversationsForWorkspace(workspaceId: string): Conversation[];
|
|
99
126
|
setModelFilterRules(rules: import("./types.js").ModelFilterRule[]): Promise<void>;
|
|
100
127
|
/**
|
|
101
128
|
* Sets (or clears) the app-wide default model.
|
package/dist/server/store.js
CHANGED
|
@@ -689,6 +689,50 @@ export class Store {
|
|
|
689
689
|
return new Store(repository, { workspaces: [], conversations: [], subagentRuns: [], backgroundTasks: [] });
|
|
690
690
|
}
|
|
691
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* THE MODEL FILTER RULES, WITHOUT CLONING THE STATE TO READ THEM.
|
|
694
|
+
*
|
|
695
|
+
* `snapshot()` deep-clones the WHOLE application - every workspace, every conversation, every
|
|
696
|
+
* message of every conversation - and the model filter check needs ONE small array. Reading that
|
|
697
|
+
* array through a full clone is the difference between a few hundred bytes and the entire
|
|
698
|
+
* transcript of the app, on the path that runs every time a message is sent.
|
|
699
|
+
*
|
|
700
|
+
* So the rules get their own narrow reader. It returns a COPY of the array (the caller may keep
|
|
701
|
+
* it, and the store's own state must not change under it), and the individual rules are treated
|
|
702
|
+
* as read-only - which is what the filter does with them.
|
|
703
|
+
*/
|
|
704
|
+
modelFilterRules() {
|
|
705
|
+
return this.state.modelFilterRules ? [...this.state.modelFilterRules] : [];
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* THE WORKSPACE A CONVERSATION BELONGS TO, WITHOUT CLONING THE STATE TO FIND ONE ROW.
|
|
709
|
+
*
|
|
710
|
+
* The same argument as the rules above: the filter needs to know WHICH workspace the conversation
|
|
711
|
+
* is in (the rules are per workspace), and the only way to know that today is to clone everything
|
|
712
|
+
* and look. This returns a shallow copy of the one workspace - cheap, and detached enough that a
|
|
713
|
+
* caller cannot reach into the store's state through it.
|
|
714
|
+
*/
|
|
715
|
+
workspaceForConversation(conversationId) {
|
|
716
|
+
const conversation = this.state.conversations.find((item) => item.id === conversationId);
|
|
717
|
+
const workspace = this.state.workspaces.find((item) => item.id === conversation?.workspaceId);
|
|
718
|
+
return workspace ? { ...workspace } : undefined;
|
|
719
|
+
}
|
|
720
|
+
workspace(workspaceId) {
|
|
721
|
+
const workspace = this.state.workspaces.find((item) => item.id === workspaceId);
|
|
722
|
+
return workspace ? { ...workspace } : undefined;
|
|
723
|
+
}
|
|
724
|
+
/** Small detached rows used when refreshing each conversation's scoped model catalog. */
|
|
725
|
+
catalogConversationTargets() {
|
|
726
|
+
return this.state.conversations.map(({ id, archivedAt, model }) => ({
|
|
727
|
+
id,
|
|
728
|
+
archivedAt,
|
|
729
|
+
model: model ? { ...model } : undefined,
|
|
730
|
+
}));
|
|
731
|
+
}
|
|
732
|
+
/** Clone only one workspace's transcripts, never unrelated workspaces or app state. */
|
|
733
|
+
conversationsForWorkspace(workspaceId) {
|
|
734
|
+
return structuredClone(this.state.conversations.filter((item) => item.workspaceId === workspaceId));
|
|
735
|
+
}
|
|
692
736
|
async setModelFilterRules(rules) {
|
|
693
737
|
if (!Array.isArray(rules) || rules.some(rule => !rule || typeof rule.id !== "string" || typeof rule.workspacePattern !== "string" || typeof rule.modelPattern !== "string" || (rule.label !== undefined && typeof rule.label !== "string")))
|
|
694
738
|
throw new Error("模型过滤规则格式不合法");
|
package/dist/server/subagents.js
CHANGED
|
@@ -421,8 +421,16 @@ export class SubagentSupervisor {
|
|
|
421
421
|
}
|
|
422
422
|
if (!record)
|
|
423
423
|
break;
|
|
424
|
-
|
|
425
|
-
|
|
424
|
+
// THE RECEIPT FOR OUR OWN PROMPT IS A KNOWN RECORD, NOT AN UNKNOWN ONE. Pi answers every
|
|
425
|
+
// command with `type:"response"`, so the successful receipt for the prompt we just wrote is
|
|
426
|
+
// part of the protocol - and treating it as unrecognised made every ordinary sub-agent run
|
|
427
|
+
// log a false alarm ("unhandled child record response - consumed without action"). A FAILED
|
|
428
|
+
// receipt is the run's failure and is thrown; a successful one is simply consumed.
|
|
429
|
+
if (record.type === "response" && record.id === "prompt") {
|
|
430
|
+
if (record.success === false)
|
|
431
|
+
throw new Error(`Pi rejected the sub-agent task: ${record.error ?? "no reason given"}`);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
426
434
|
if (record.type === "message_update") {
|
|
427
435
|
const event = record.assistantMessageEvent;
|
|
428
436
|
// `text_delta` is the only inner event that is output. `start` and `done` are the boundaries
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evandu/super-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.137",
|
|
4
4
|
"description": "Web control plane and companion for Pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"scripts/workspace-cwd.test.mjs",
|
|
77
77
|
"scripts/subagents.test.mjs",
|
|
78
78
|
"scripts/subagent-noise.test.mjs",
|
|
79
|
+
"scripts/subagent-response-receipt.test.mjs",
|
|
79
80
|
"scripts/subagent-runs.test.mjs",
|
|
80
81
|
"scripts/conversation-reconcile.test.mjs",
|
|
81
82
|
"scripts/subagent-dispatch.test.mjs",
|
|
@@ -110,6 +111,8 @@
|
|
|
110
111
|
"scripts/todo-tool.test.mjs",
|
|
111
112
|
"scripts/tool-calls.test.mjs",
|
|
112
113
|
"scripts/files-api.test.mjs",
|
|
114
|
+
"scripts/http-body-utf8-chunking.test.mjs",
|
|
115
|
+
"scripts/files-eol-roundtrip.test.mjs",
|
|
113
116
|
"scripts/files-missing-path.test.mjs",
|
|
114
117
|
"scripts/file-changes.test.mjs",
|
|
115
118
|
"scripts/file-index.test.mjs",
|
|
@@ -138,6 +141,7 @@
|
|
|
138
141
|
"scripts/provider-pi-reads.test.mjs",
|
|
139
142
|
"scripts/session-extension-credentials.test.mjs",
|
|
140
143
|
"scripts/active-memory.test.mjs",
|
|
144
|
+
"scripts/active-memory-index-content.test.mjs",
|
|
141
145
|
"scripts/knowledge-base.test.mjs",
|
|
142
146
|
"scripts/knowledge-import-path.test.mjs",
|
|
143
147
|
"scripts/knowledge-import-identity.test.mjs",
|
|
@@ -164,6 +168,7 @@
|
|
|
164
168
|
"scripts/document-write-overwrite.test.mjs",
|
|
165
169
|
"scripts/feishu.test.mjs",
|
|
166
170
|
"scripts/scheduled-tasks.test.mjs",
|
|
171
|
+
"scripts/scheduled-task-model-clear.test.mjs",
|
|
167
172
|
"scripts/scheduled-task-scope.test.mjs",
|
|
168
173
|
"scripts/scheduled-task-tool.test.mjs",
|
|
169
174
|
"scripts/run-state-sync.test.mjs",
|
|
@@ -182,6 +187,9 @@
|
|
|
182
187
|
"scripts/conversation-reconcile-browser.test.mjs",
|
|
183
188
|
"scripts/visual-defects-browser.test.mjs",
|
|
184
189
|
"scripts/session-state-animation-browser.test.mjs",
|
|
190
|
+
"scripts/session-state-delivery-browser.test.mjs",
|
|
191
|
+
"scripts/question-awaiting-signal-browser.test.mjs",
|
|
192
|
+
"scripts/withdraw-then-send-browser.test.mjs",
|
|
185
193
|
"scripts/agent-vis-card-browser.test.mjs",
|
|
186
194
|
"scripts/queue-dock.test.mjs",
|
|
187
195
|
"scripts/scheduled-tasks-browser.test.mjs",
|