@evandu/super-pi 0.1.136 → 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-CWN8JIx4.js → CodeMirrorEditor-CkA_5D5r.js} +2 -2
- package/dist/client/assets/{dist-ZvUavnTX.js → dist-9Fm8XKu4.js} +1 -1
- package/dist/client/assets/{dist-DMDxxHpj.js → dist-B02Erd0t.js} +1 -1
- package/dist/client/assets/{dist-yOveRczL.js → dist-B3Mfuv1D.js} +1 -1
- package/dist/client/assets/{dist-CV9QN-qU.js → dist-BfEmujEC.js} +1 -1
- package/dist/client/assets/{dist-Qc0xt1y7.js → dist-BqGjijI6.js} +1 -1
- package/dist/client/assets/{dist-BQjtlfpY.js → dist-BsqzrZ5R.js} +1 -1
- package/dist/client/assets/{dist-DErmxG4Q.js → dist-BySjislr.js} +1 -1
- package/dist/client/assets/{dist-OKarkelj.js → dist-C2tSd-AY.js} +1 -1
- package/dist/client/assets/{dist-BSOBu3lQ.js → dist-CrxmpY93.js} +1 -1
- package/dist/client/assets/{dist-CYLUz40X.js → dist-CsNytPug.js} +1 -1
- package/dist/client/assets/{dist-CTXz1R9C.js → dist-D-LtG92p.js} +1 -1
- package/dist/client/assets/{dist-BA6BeFaf.js → dist-DGhdkud4.js} +1 -1
- package/dist/client/assets/{dist-BtNKV30Z.js → dist-DL4Q05ts.js} +1 -1
- package/dist/client/assets/{dist-DHTyhm7w.js → dist-D_umYm0o.js} +1 -1
- package/dist/client/assets/{dist-DCuAT0n-.js → dist-DbovsYeH.js} +1 -1
- package/dist/client/assets/{dist-0vYGgCC5.js → dist-DcdzfM-n.js} +1 -1
- package/dist/client/assets/{dist-WkcafMsD.js → dist-DdVs8Y0X.js} +1 -1
- package/dist/client/assets/{dist-Dalb5yHq.js → dist-MC2XuZgq.js} +1 -1
- package/dist/client/assets/{dist-B0vHKLg1.js → dist-b9CfAgzG.js} +1 -1
- package/dist/client/assets/{dist-t0oHfWqq.js → dist-j9X7j381.js} +1 -1
- package/dist/client/assets/{dist-q1T0uV1c.js → dist-k6bOz9RS.js} +1 -1
- package/dist/client/assets/{dist-CwNZuQ4i.js → dist-ufRHdQ8G.js} +1 -1
- package/dist/client/assets/{index-oDRgSKPX.js → index-DVmE8H4Y.js} +12 -12
- package/dist/client/assets/{index-r-L_XCYs.css → index-FxTrUapG.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/active-memory.js +9 -2
- package/dist/server/application/catalog-service.js +8 -6
- package/dist/server/http-server.d.ts +0 -3
- package/dist/server/http-server.js +131 -87
- package/dist/server/store.d.ts +5 -0
- package/dist/server/store.js +16 -0
- package/package.json +4 -1
package/dist/client/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="alternate icon" href="/favicon.ico" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/favicon.svg" />
|
|
10
10
|
<title>Super Pi</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DVmE8H4Y.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-FxTrUapG.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -139,7 +139,11 @@ export class ActiveMemory {
|
|
|
139
139
|
if (this.indexedWorkspaces.has(workspaceId))
|
|
140
140
|
return;
|
|
141
141
|
const index = new Map();
|
|
142
|
-
|
|
142
|
+
const store = this.store;
|
|
143
|
+
const conversations = store.conversationsForWorkspace
|
|
144
|
+
? store.conversationsForWorkspace(workspaceId)
|
|
145
|
+
: store.snapshot().conversations.filter(c => c.workspaceId === workspaceId);
|
|
146
|
+
for (const conversation of conversations)
|
|
143
147
|
for (const message of conversation.messages) {
|
|
144
148
|
if (message.role !== "user" && message.role !== "assistant")
|
|
145
149
|
continue;
|
|
@@ -362,7 +366,10 @@ export class ActiveMemory {
|
|
|
362
366
|
const settings = await this.configuration();
|
|
363
367
|
if (!settings.enabled || !settings.reviewInterval)
|
|
364
368
|
return;
|
|
365
|
-
const
|
|
369
|
+
const store = this.store;
|
|
370
|
+
const workspace = store.workspace
|
|
371
|
+
? store.workspace(snapshot.workspaceId)
|
|
372
|
+
: store.snapshot().workspaces.find(w => w.id === snapshot.workspaceId);
|
|
366
373
|
if (!workspace)
|
|
367
374
|
return;
|
|
368
375
|
const agent = await this.runtime.agentFor?.(snapshot);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertModelAllowed
|
|
1
|
+
import { assertModelAllowed } from "../model-filter.js";
|
|
2
2
|
import { readAgents, resolveAgentDirectories } from "../agents.js";
|
|
3
3
|
import { loadPromptTemplateSummaries } from "../pi-session.js";
|
|
4
4
|
function summarize(agent) {
|
|
@@ -75,10 +75,12 @@ export class CatalogService {
|
|
|
75
75
|
const catalog = await this.runtime.catalog(selection);
|
|
76
76
|
if (!conversationId || !this.store)
|
|
77
77
|
return catalog;
|
|
78
|
-
const
|
|
79
|
-
|
|
78
|
+
const workspace = this.store.workspaceForConversation(conversationId);
|
|
79
|
+
if (!workspace)
|
|
80
|
+
throw new Error("会话所属工作区不存在");
|
|
81
|
+
const rules = this.store.modelFilterRules();
|
|
80
82
|
return { ...catalog, models: catalog.models.filter(model => { try {
|
|
81
|
-
assertModelAllowed(
|
|
83
|
+
assertModelAllowed(rules, workspace, model);
|
|
82
84
|
return true;
|
|
83
85
|
}
|
|
84
86
|
catch {
|
|
@@ -108,7 +110,7 @@ export class CatalogService {
|
|
|
108
110
|
throw new Error("Store unavailable");
|
|
109
111
|
await this.store.setModelFilterRules(rules);
|
|
110
112
|
this.broadcaster.broadcastState();
|
|
111
|
-
for (const conversation of this.store.
|
|
113
|
+
for (const conversation of this.store.catalogConversationTargets()) {
|
|
112
114
|
if (!conversation.archivedAt) {
|
|
113
115
|
try {
|
|
114
116
|
await this.broadcastCatalog(conversation.model, conversation.id);
|
|
@@ -182,7 +184,7 @@ export class CatalogService {
|
|
|
182
184
|
const cached = this.promptCatalogs.get(workspaceId);
|
|
183
185
|
if (cached && cached.expiresAt > Date.now())
|
|
184
186
|
return cached.catalog;
|
|
185
|
-
const workspace = this.store?.
|
|
187
|
+
const workspace = this.store?.workspace(workspaceId);
|
|
186
188
|
if (!workspace)
|
|
187
189
|
return { source: "unavailable", prompts: [], workspaceId, fetchedAt: new Date().toISOString(), error: "workspace not found" };
|
|
188
190
|
try {
|
|
@@ -30,7 +30,4 @@ export interface HttpServerOptions {
|
|
|
30
30
|
context: ServerContext;
|
|
31
31
|
onShutdown: (reason: string) => Promise<void>;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Creates the HTTP server providing static client assets and all `/api/*` endpoints.
|
|
35
|
-
*/
|
|
36
33
|
export declare function createHttpServer(options: HttpServerOptions): NodeHttpServer;
|
|
@@ -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" });
|
package/dist/server/store.d.ts
CHANGED
|
@@ -118,6 +118,11 @@ export declare class Store {
|
|
|
118
118
|
* caller cannot reach into the store's state through it.
|
|
119
119
|
*/
|
|
120
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[];
|
|
121
126
|
setModelFilterRules(rules: import("./types.js").ModelFilterRule[]): Promise<void>;
|
|
122
127
|
/**
|
|
123
128
|
* Sets (or clears) the app-wide default model.
|
package/dist/server/store.js
CHANGED
|
@@ -717,6 +717,22 @@ export class Store {
|
|
|
717
717
|
const workspace = this.state.workspaces.find((item) => item.id === conversation?.workspaceId);
|
|
718
718
|
return workspace ? { ...workspace } : undefined;
|
|
719
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
|
+
}
|
|
720
736
|
async setModelFilterRules(rules) {
|
|
721
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")))
|
|
722
738
|
throw new Error("模型过滤规则格式不合法");
|
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",
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"scripts/todo-tool.test.mjs",
|
|
112
112
|
"scripts/tool-calls.test.mjs",
|
|
113
113
|
"scripts/files-api.test.mjs",
|
|
114
|
+
"scripts/http-body-utf8-chunking.test.mjs",
|
|
114
115
|
"scripts/files-eol-roundtrip.test.mjs",
|
|
115
116
|
"scripts/files-missing-path.test.mjs",
|
|
116
117
|
"scripts/file-changes.test.mjs",
|
|
@@ -187,6 +188,8 @@
|
|
|
187
188
|
"scripts/visual-defects-browser.test.mjs",
|
|
188
189
|
"scripts/session-state-animation-browser.test.mjs",
|
|
189
190
|
"scripts/session-state-delivery-browser.test.mjs",
|
|
191
|
+
"scripts/question-awaiting-signal-browser.test.mjs",
|
|
192
|
+
"scripts/withdraw-then-send-browser.test.mjs",
|
|
190
193
|
"scripts/agent-vis-card-browser.test.mjs",
|
|
191
194
|
"scripts/queue-dock.test.mjs",
|
|
192
195
|
"scripts/scheduled-tasks-browser.test.mjs",
|