neoctl 0.2.50 → 0.2.52
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/core/query-engine.d.ts +1 -0
- package/dist/core/query-engine.js +6 -0
- package/dist/core/query-engine.js.map +1 -1
- package/dist/web/index.d.ts +31 -2
- package/dist/web/index.js +202 -20
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
package/dist/web/index.js
CHANGED
|
@@ -478,6 +478,66 @@ function parseResumeFlag(value) {
|
|
|
478
478
|
}
|
|
479
479
|
export class WebRuntimeRouter {
|
|
480
480
|
repls = new Map();
|
|
481
|
+
deletedSessions = new Set();
|
|
482
|
+
deletingSessions = new Set();
|
|
483
|
+
assertSessionAvailable(sessionId) {
|
|
484
|
+
if (sessionId && (this.deletedSessions.has(sessionId) || this.deletingSessions.has(sessionId))) {
|
|
485
|
+
throw new Error("会话已删除或正在删除");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async deleteSession(scope, sessionId) {
|
|
489
|
+
if (!sessionId)
|
|
490
|
+
return actionFailure("INVALID_REQUEST", "sessionId is required");
|
|
491
|
+
if (this.deletingSessions.has(sessionId))
|
|
492
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
493
|
+
if (this.deletedSessions.has(sessionId))
|
|
494
|
+
return actionFailure("SESSION_NOT_FOUND", "会话已删除");
|
|
495
|
+
this.deletingSessions.add(sessionId);
|
|
496
|
+
try {
|
|
497
|
+
const entries = await Promise.all([...this.repls.entries()].map(async ([key, promise]) => ({ key, promise, repl: await promise.catch(() => undefined) })));
|
|
498
|
+
const owners = [...new Set(entries.filter(entry => entry.repl?.currentSessionId() === sessionId).map(entry => entry.repl))];
|
|
499
|
+
// Shared hubs should have one owner. Never orphan a legacy second writer.
|
|
500
|
+
if (owners.length > 1)
|
|
501
|
+
return actionFailure("SESSION_IN_USE", "会话被多个运行实例占用,请先关闭其它窗口");
|
|
502
|
+
// A cold current-session request must restore its owner without passing the deletion gate.
|
|
503
|
+
if (!owners.length && scope.sessionId === sessionId) {
|
|
504
|
+
const key = `session:${sessionId}`;
|
|
505
|
+
const promise = this.createRuntime({ sessionId, resume: true }).then(runtime => this.createRepl(runtime));
|
|
506
|
+
this.repls.set(key, promise);
|
|
507
|
+
try {
|
|
508
|
+
const repl = await promise;
|
|
509
|
+
entries.push({ key, promise, repl });
|
|
510
|
+
owners.push(repl);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
this.repls.delete(key);
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const repl = owners[0] || await this.get(scope);
|
|
518
|
+
if (entries.some(entry => entry.repl?.runningSessionIds().includes(sessionId)))
|
|
519
|
+
return actionFailure("SESSION_RUNNING", "运行中的会话不可删除");
|
|
520
|
+
const result = await repl.deleteSession(sessionId);
|
|
521
|
+
if (!result.ok)
|
|
522
|
+
return result;
|
|
523
|
+
this.deletedSessions.add(sessionId);
|
|
524
|
+
// Retire every cache alias of the deleted writer, then publish only the replacement key.
|
|
525
|
+
if (owners.length) {
|
|
526
|
+
for (const entry of entries)
|
|
527
|
+
if (entry.repl === repl && this.repls.get(entry.key) === entry.promise)
|
|
528
|
+
this.repls.delete(entry.key);
|
|
529
|
+
if (result.replacementSessionId)
|
|
530
|
+
this.repls.set(`session:${result.replacementSessionId}`, Promise.resolve(repl));
|
|
531
|
+
}
|
|
532
|
+
// Publish only after the replacement is routable; SSE clients may reconnect immediately.
|
|
533
|
+
if (owners.length)
|
|
534
|
+
repl.publishSessionDeletion(sessionId, result.replacementSessionId);
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
this.deletingSessions.delete(sessionId);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
481
541
|
createRuntime;
|
|
482
542
|
createRepl;
|
|
483
543
|
clientRevision = process.env.NEO_CLIENT_REVISION?.trim() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -487,6 +547,7 @@ export class WebRuntimeRouter {
|
|
|
487
547
|
this.createRepl = options.createRepl ?? ((runtime) => new WebRepl(runtime));
|
|
488
548
|
}
|
|
489
549
|
get(scope = {}) {
|
|
550
|
+
this.assertSessionAvailable(scope.sessionId);
|
|
490
551
|
const key = webRuntimeScopeKey(scope);
|
|
491
552
|
let repl = this.repls.get(key);
|
|
492
553
|
if (!repl) {
|
|
@@ -499,6 +560,11 @@ export class WebRuntimeRouter {
|
|
|
499
560
|
async snapshot(scope = {}, includeCatalog = true) {
|
|
500
561
|
return (await this.get(scope)).snapshot(includeCatalog);
|
|
501
562
|
}
|
|
563
|
+
async runningSessionIds() {
|
|
564
|
+
// Inspect only existing runtime instances: never restore sessions or read transcripts.
|
|
565
|
+
const entries = await Promise.allSettled([...this.repls.values()]);
|
|
566
|
+
return [...new Set(entries.flatMap(entry => entry.status === "fulfilled" ? entry.value.runningSessionIds() : []))];
|
|
567
|
+
}
|
|
502
568
|
async reloadModelConfig() {
|
|
503
569
|
const repls = await Promise.all([...this.repls.values()]);
|
|
504
570
|
await Promise.all(repls.map(repl => repl.reloadModelConfig()));
|
|
@@ -546,6 +612,9 @@ function webRuntimeScopeKey(scope) {
|
|
|
546
612
|
export class WebRepl {
|
|
547
613
|
runtime;
|
|
548
614
|
demoOptions;
|
|
615
|
+
currentSessionId() { return this.runtime.engine.snapshot().session?.sessionId; }
|
|
616
|
+
deletingSession = false;
|
|
617
|
+
sessionMutations = 0;
|
|
549
618
|
pendingContinuation;
|
|
550
619
|
runControl() {
|
|
551
620
|
const execution = this.runtime.engine.getExecutionState?.();
|
|
@@ -750,6 +819,14 @@ export class WebRepl {
|
|
|
750
819
|
// Cursor addresses stored bytes, not the possibly additionally redacted response length.
|
|
751
820
|
return page.ok ? this.runtime.engine.redactDisplayValue({ sessionId, runId, ...page.value }) : undefined;
|
|
752
821
|
}
|
|
822
|
+
runningSessionIds() {
|
|
823
|
+
const id = this.runtime.engine.snapshot().session?.sessionId;
|
|
824
|
+
return [...new Set([...this.backgroundSessionRuns.keys(), ...(this.busy && !this.runControl().canResume && id ? [id] : [])])];
|
|
825
|
+
}
|
|
826
|
+
queuedImage(imageId) {
|
|
827
|
+
const attachment = this.queuedAttachments?.find(item => item.kind === "image" && item.imageId === imageId);
|
|
828
|
+
return attachment?.kind === "image" ? imagePayloadForBlock({ type: "image", ...attachment }) : undefined;
|
|
829
|
+
}
|
|
753
830
|
snapshot(includeCatalog = false) {
|
|
754
831
|
const backgroundTasks = this.backgroundTasks();
|
|
755
832
|
const engineSnapshot = this.runtime.engine.snapshot();
|
|
@@ -766,12 +843,16 @@ export class WebRepl {
|
|
|
766
843
|
runControl: this.runControl(),
|
|
767
844
|
demoMode: this.isDemo(),
|
|
768
845
|
queuedInput: this.queuedInput,
|
|
846
|
+
// Preview metadata only. Base64 image contents must not inflate every status snapshot.
|
|
847
|
+
queuedAttachments: (this.queuedAttachments ?? []).map(item => item.kind === "image"
|
|
848
|
+
? { kind: "image", imageId: item.imageId, previewUrl: `/api/queue/images/${encodeURIComponent(item.imageId)}` }
|
|
849
|
+
: { kind: "file", name: item.name }),
|
|
769
850
|
backgroundTaskCount: backgroundTasks.length,
|
|
770
851
|
backgroundTasks,
|
|
771
852
|
terminalTaskHistory: this.terminalTaskHistory(),
|
|
772
853
|
agentTaskHistory: sessionAgentTasks(this.runtime).filter((task) => this.runtime.taskStore.isTerminal(task)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((task) => this.agentTaskSnapshot(task)),
|
|
773
854
|
backgroundSessionRunCount: this.backgroundSessionRuns.size,
|
|
774
|
-
runningSessionIds:
|
|
855
|
+
runningSessionIds: this.runningSessionIds(),
|
|
775
856
|
session: engineSnapshot.session,
|
|
776
857
|
modelSettings: { model: engineSnapshot.model, reasoning: engineSnapshot.reasoning },
|
|
777
858
|
pendingSessionSettings,
|
|
@@ -879,6 +960,8 @@ export class WebRepl {
|
|
|
879
960
|
await this.refreshToolConfiguration();
|
|
880
961
|
}
|
|
881
962
|
async setSessionTools(value) {
|
|
963
|
+
if (this.deletingSession)
|
|
964
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
882
965
|
if (this.busy)
|
|
883
966
|
return actionFailure("TOOL_UPDATE_BLOCKED", "cannot change session tools while a response is running");
|
|
884
967
|
if (!isRecord(value))
|
|
@@ -889,6 +972,7 @@ export class WebRepl {
|
|
|
889
972
|
const normalized = this.parseToolModes(value, support);
|
|
890
973
|
if (!normalized.ok)
|
|
891
974
|
return normalized;
|
|
975
|
+
this.sessionMutations++;
|
|
892
976
|
try {
|
|
893
977
|
await this.applySessionToolOverrides(normalized.value, true);
|
|
894
978
|
return { ok: true, state: this.sessionTools() };
|
|
@@ -896,6 +980,9 @@ export class WebRepl {
|
|
|
896
980
|
catch (error) {
|
|
897
981
|
return actionFailure("TOOL_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
898
982
|
}
|
|
983
|
+
finally {
|
|
984
|
+
this.sessionMutations--;
|
|
985
|
+
}
|
|
899
986
|
}
|
|
900
987
|
async loadSessionTools(sessionId) {
|
|
901
988
|
const support = this.runtime.toolSupport;
|
|
@@ -904,6 +991,8 @@ export class WebRepl {
|
|
|
904
991
|
await this.applySessionToolOverrides(await support.resolveSession(sessionId), false);
|
|
905
992
|
}
|
|
906
993
|
async setSessionPlugins(value) {
|
|
994
|
+
if (this.deletingSession)
|
|
995
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
907
996
|
if (!isRecord(value))
|
|
908
997
|
return actionFailure("INVALID_REQUEST", "plugin overrides must be an object");
|
|
909
998
|
const support = this.runtime.pluginSupport;
|
|
@@ -921,6 +1010,7 @@ export class WebRepl {
|
|
|
921
1010
|
else if (mode !== "inherit" && mode !== null && mode !== undefined)
|
|
922
1011
|
return actionFailure("PLUGIN_INVALID", `invalid plugin mode: ${id}`);
|
|
923
1012
|
}
|
|
1013
|
+
this.sessionMutations++;
|
|
924
1014
|
try {
|
|
925
1015
|
await this.applySessionPluginOverrides(overrides, true);
|
|
926
1016
|
return { ok: true, state: this.sessionPlugins() };
|
|
@@ -928,6 +1018,9 @@ export class WebRepl {
|
|
|
928
1018
|
catch (error) {
|
|
929
1019
|
return actionFailure("PLUGIN_UPDATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
930
1020
|
}
|
|
1021
|
+
finally {
|
|
1022
|
+
this.sessionMutations--;
|
|
1023
|
+
}
|
|
931
1024
|
}
|
|
932
1025
|
async loadSessionPlugins(sessionId) {
|
|
933
1026
|
const support = this.runtime.pluginSupport;
|
|
@@ -1018,6 +1111,8 @@ export class WebRepl {
|
|
|
1018
1111
|
this.publishRuntimeContext();
|
|
1019
1112
|
}
|
|
1020
1113
|
async submit(text, attachments = []) {
|
|
1114
|
+
if (this.deletingSession)
|
|
1115
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1021
1116
|
if (this.runControl().canResume && (!text.trimStart().startsWith("/") || !text.trim()))
|
|
1022
1117
|
return this.continueRun(text, attachments);
|
|
1023
1118
|
if (this.pendingContinuation)
|
|
@@ -1046,6 +1141,8 @@ export class WebRepl {
|
|
|
1046
1141
|
return { ok: true };
|
|
1047
1142
|
}
|
|
1048
1143
|
async submitImmediately(text, attachments = []) {
|
|
1144
|
+
if (this.deletingSession)
|
|
1145
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1049
1146
|
if (this.runControl().canResume) {
|
|
1050
1147
|
const result = this.continueRun(text, attachments);
|
|
1051
1148
|
return result.ok ? { ok: true, interrupted: false } : result;
|
|
@@ -1066,6 +1163,8 @@ export class WebRepl {
|
|
|
1066
1163
|
return this.sendQueuedNow();
|
|
1067
1164
|
}
|
|
1068
1165
|
sendQueuedNow() {
|
|
1166
|
+
if (this.deletingSession)
|
|
1167
|
+
return Promise.reject(new Error("会话正在删除"));
|
|
1069
1168
|
if (this.immediateSend)
|
|
1070
1169
|
return this.immediateSend;
|
|
1071
1170
|
const queuedText = this.queuedInput;
|
|
@@ -1100,12 +1199,15 @@ export class WebRepl {
|
|
|
1100
1199
|
}
|
|
1101
1200
|
async listSessions(options) {
|
|
1102
1201
|
const sessions = await this.runtime.engine.listSessions(Number.POSITIVE_INFINITY);
|
|
1103
|
-
const runningSessionIds =
|
|
1202
|
+
const runningSessionIds = this.runningSessionIds();
|
|
1104
1203
|
return { ...(options ? paginateSessions(sessions, options) : { sessions }), runningSessionIds };
|
|
1105
1204
|
}
|
|
1106
1205
|
async resumeSession(sessionId) {
|
|
1206
|
+
if (this.deletingSession)
|
|
1207
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1107
1208
|
if (!sessionId)
|
|
1108
1209
|
return actionFailure("INVALID_REQUEST", "sessionId is required");
|
|
1210
|
+
this.sessionMutations++;
|
|
1109
1211
|
try {
|
|
1110
1212
|
const running = this.backgroundSessionRuns.get(sessionId);
|
|
1111
1213
|
if (running) {
|
|
@@ -1127,8 +1229,14 @@ export class WebRepl {
|
|
|
1127
1229
|
const message = error instanceof Error ? error.message : String(error);
|
|
1128
1230
|
return actionFailure("SESSION_RESUME_FAILED", message);
|
|
1129
1231
|
}
|
|
1232
|
+
finally {
|
|
1233
|
+
this.sessionMutations--;
|
|
1234
|
+
}
|
|
1130
1235
|
}
|
|
1131
1236
|
async newSession() {
|
|
1237
|
+
if (this.deletingSession)
|
|
1238
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1239
|
+
this.sessionMutations++;
|
|
1132
1240
|
try {
|
|
1133
1241
|
await this.detachRunningForeground("new session");
|
|
1134
1242
|
this.runtime.engine = this.runtime.engine.forkForSession(undefined, false);
|
|
@@ -1145,8 +1253,11 @@ export class WebRepl {
|
|
|
1145
1253
|
const message = error instanceof Error ? error.message : String(error);
|
|
1146
1254
|
return actionFailure("SESSION_CREATE_FAILED", message);
|
|
1147
1255
|
}
|
|
1256
|
+
finally {
|
|
1257
|
+
this.sessionMutations--;
|
|
1258
|
+
}
|
|
1148
1259
|
}
|
|
1149
|
-
async refreshSessionView() {
|
|
1260
|
+
async refreshSessionView(publish = true) {
|
|
1150
1261
|
const metrics = await this.runtime.engine.contextMetrics();
|
|
1151
1262
|
this.runtime.usage.reset();
|
|
1152
1263
|
this.status = initialStatus(this.runtime, metrics);
|
|
@@ -1158,27 +1269,75 @@ export class WebRepl {
|
|
|
1158
1269
|
this.thinkingLineId = undefined;
|
|
1159
1270
|
this.finalizedThinkingLineId = undefined;
|
|
1160
1271
|
this.liveToolLineIds.clear();
|
|
1161
|
-
|
|
1162
|
-
|
|
1272
|
+
if (publish) {
|
|
1273
|
+
this.broadcastSync();
|
|
1274
|
+
this.publishRuntimeContext();
|
|
1275
|
+
}
|
|
1163
1276
|
}
|
|
1164
1277
|
async deleteSession(sessionId) {
|
|
1165
1278
|
if (!sessionId)
|
|
1166
1279
|
return actionFailure("INVALID_REQUEST", "sessionId is required");
|
|
1167
|
-
if (this.
|
|
1168
|
-
return actionFailure("
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1280
|
+
if (this.deletingSession)
|
|
1281
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1282
|
+
if (this.sessionMutations)
|
|
1283
|
+
return actionFailure("SESSION_IN_USE", "会话正在保存或切换,请稍后重试");
|
|
1284
|
+
const active = this.runtime.engine.snapshot().session?.sessionId === sessionId;
|
|
1285
|
+
if (this.backgroundSessionRuns.has(sessionId) || (active && (this.busy || this.foregroundRun || this.compactingSession || this.pendingContinuation || this.immediateSend || this.backgroundTaskCount))) {
|
|
1286
|
+
return actionFailure("SESSION_RUNNING", "运行中的会话不可删除");
|
|
1287
|
+
}
|
|
1288
|
+
this.deletingSession = true;
|
|
1289
|
+
const previous = this.runtime.engine;
|
|
1290
|
+
let deleted = false;
|
|
1173
1291
|
try {
|
|
1174
|
-
|
|
1175
|
-
|
|
1292
|
+
if (active) {
|
|
1293
|
+
// Retire the writer before removing files; an uninitialized fork owns no directory.
|
|
1294
|
+
await this.runtimeContextPublishQueue;
|
|
1295
|
+
await previous.settleSessionWrites();
|
|
1296
|
+
await this.demoMessageQueues.get(previous);
|
|
1297
|
+
this.queuedInput = undefined;
|
|
1298
|
+
this.queuedAttachments = undefined;
|
|
1299
|
+
this.pendingContinuation = undefined;
|
|
1300
|
+
this.runtime.engine = previous.forkForSession(undefined, false);
|
|
1301
|
+
activateSession(this.runtime);
|
|
1302
|
+
}
|
|
1303
|
+
deleted = await this.runtime.engine.deleteSession(sessionId);
|
|
1304
|
+
if (!deleted) {
|
|
1305
|
+
if (active) {
|
|
1306
|
+
this.runtime.engine = previous;
|
|
1307
|
+
activateSession(this.runtime);
|
|
1308
|
+
}
|
|
1176
1309
|
return actionFailure("SESSION_NOT_FOUND", `session not found: ${sessionId}`);
|
|
1177
|
-
|
|
1310
|
+
}
|
|
1311
|
+
if (!active)
|
|
1312
|
+
return { ok: true };
|
|
1313
|
+
await this.runtime.engine.initialize();
|
|
1314
|
+
activateSession(this.runtime);
|
|
1315
|
+
const replacementSessionId = this.runtime.engine.snapshot().session?.sessionId;
|
|
1316
|
+
if (!replacementSessionId)
|
|
1317
|
+
throw new Error("新会话初始化失败");
|
|
1318
|
+
await Promise.all([this.loadSessionPlugins(replacementSessionId), this.loadSessionTools(replacementSessionId)]);
|
|
1319
|
+
await this.refreshSessionView(false);
|
|
1320
|
+
return { ok: true, replacementSessionId };
|
|
1178
1321
|
}
|
|
1179
1322
|
catch (error) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1323
|
+
if (active && !deleted) {
|
|
1324
|
+
this.runtime.engine = previous;
|
|
1325
|
+
activateSession(this.runtime);
|
|
1326
|
+
}
|
|
1327
|
+
if (active && deleted) {
|
|
1328
|
+
// A deletion cannot be rolled back; never restore the deleted writer.
|
|
1329
|
+
return { ok: true };
|
|
1330
|
+
}
|
|
1331
|
+
return actionFailure("SESSION_DELETE_FAILED", error instanceof Error ? error.message : String(error));
|
|
1332
|
+
}
|
|
1333
|
+
finally {
|
|
1334
|
+
this.deletingSession = false;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
publishSessionDeletion(sessionId, replacementSessionId) {
|
|
1338
|
+
for (const subscriber of this.subscribers) {
|
|
1339
|
+
this.send(subscriber, replacementSessionId ? "session.replaced" : "session.deleted", replacementSessionId
|
|
1340
|
+
? { deletedSessionId: sessionId, sessionId: replacementSessionId } : { sessionId });
|
|
1182
1341
|
}
|
|
1183
1342
|
}
|
|
1184
1343
|
appPrompt() {
|
|
@@ -1257,6 +1416,8 @@ export class WebRepl {
|
|
|
1257
1416
|
this.setStatus({ ...this.status, metrics, activityTick: this.status.activityTick + 1 });
|
|
1258
1417
|
}
|
|
1259
1418
|
async setFastMode(enabled) {
|
|
1419
|
+
if (this.deletingSession)
|
|
1420
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1260
1421
|
const engine = this.runtime.engine;
|
|
1261
1422
|
try {
|
|
1262
1423
|
const { deferred } = await engine.updateSessionSettings({ fastMode: enabled });
|
|
@@ -1269,6 +1430,8 @@ export class WebRepl {
|
|
|
1269
1430
|
}
|
|
1270
1431
|
}
|
|
1271
1432
|
async setContextWindowK(value) {
|
|
1433
|
+
if (this.deletingSession)
|
|
1434
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1272
1435
|
if (typeof value !== "string" || !/^\d+$/u.test(value) || Number(value) <= 0 || !Number.isSafeInteger(Number(value) * 1000)) {
|
|
1273
1436
|
return actionFailure("CONTEXT_WINDOW_INVALID", "context window must be a positive integer in k");
|
|
1274
1437
|
}
|
|
@@ -1288,6 +1451,8 @@ export class WebRepl {
|
|
|
1288
1451
|
}
|
|
1289
1452
|
}
|
|
1290
1453
|
async setSessionModel(model, reasoning) {
|
|
1454
|
+
if (this.deletingSession)
|
|
1455
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1291
1456
|
if (typeof model !== "string" || !model.trim() || model.length > 256 || /[\s\x00-\x1f]/u.test(model.trim())) {
|
|
1292
1457
|
return actionFailure("MODEL_INVALID", "invalid model");
|
|
1293
1458
|
}
|
|
@@ -1320,6 +1485,8 @@ export class WebRepl {
|
|
|
1320
1485
|
return readSessionPrompt(this.runtime.engine);
|
|
1321
1486
|
}
|
|
1322
1487
|
async saveSessionPrompt(body) {
|
|
1488
|
+
if (this.deletingSession)
|
|
1489
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1323
1490
|
const engine = this.runtime.engine;
|
|
1324
1491
|
const result = await updateSessionPrompt(engine, body);
|
|
1325
1492
|
if (this.runtime.engine === engine) {
|
|
@@ -1331,6 +1498,8 @@ export class WebRepl {
|
|
|
1331
1498
|
return result;
|
|
1332
1499
|
}
|
|
1333
1500
|
async compactSession() {
|
|
1501
|
+
if (this.deletingSession)
|
|
1502
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1334
1503
|
const engine = this.runtime.engine;
|
|
1335
1504
|
if (this.isDemo())
|
|
1336
1505
|
return actionFailure("DEMO_SCRIPTED", "演示会话由后台安排压缩");
|
|
@@ -1467,6 +1636,8 @@ export class WebRepl {
|
|
|
1467
1636
|
return { ok: true };
|
|
1468
1637
|
}
|
|
1469
1638
|
continueRun(text = "", attachments = []) {
|
|
1639
|
+
if (this.deletingSession)
|
|
1640
|
+
return actionFailure("SESSION_DELETING", "会话正在删除");
|
|
1470
1641
|
if (!this.runControl().canResume)
|
|
1471
1642
|
return actionFailure("INVALID_REQUEST", "没有可恢复的执行,或续接请求已经提交");
|
|
1472
1643
|
const useQueue = !text.trim() && attachments.length === 0 && this.queuedInput !== undefined;
|
|
@@ -2423,6 +2594,8 @@ export class WebRepl {
|
|
|
2423
2594
|
});
|
|
2424
2595
|
}
|
|
2425
2596
|
publishRuntimeContext(targets) {
|
|
2597
|
+
if (this.deletingSession)
|
|
2598
|
+
return;
|
|
2426
2599
|
this.runtimeContextPublishQueue = this.runtimeContextPublishQueue.then(async () => {
|
|
2427
2600
|
const revision = ++this.runtimeContextRevision;
|
|
2428
2601
|
const payload = await this.buildRuntimeContext(revision);
|
|
@@ -2511,6 +2684,8 @@ export class WebRepl {
|
|
|
2511
2684
|
this.send(subscriber, "terminal.output", { updates });
|
|
2512
2685
|
}
|
|
2513
2686
|
broadcastSync() {
|
|
2687
|
+
if (this.deletingSession)
|
|
2688
|
+
return;
|
|
2514
2689
|
this.discardPendingDeltaOperations();
|
|
2515
2690
|
const payload = this.snapshot(false);
|
|
2516
2691
|
for (const subscriber of this.subscribers)
|
|
@@ -2570,7 +2745,13 @@ export async function handleWebRequest(req, res, router) {
|
|
|
2570
2745
|
if (["/api/tool-call-detail", "/api/terminal-output", "/api/agent-content"].includes(url.pathname) && !allowToolDetailRequest(req.headers.origin, req.headers.host, req.headers["sec-fetch-site"])) {
|
|
2571
2746
|
return sendJson(res, { error: "详情访问被拒绝" }, 403);
|
|
2572
2747
|
}
|
|
2748
|
+
if (req.method === "GET" && url.pathname === "/api/sessions/running")
|
|
2749
|
+
return sendJson(res, { runningSessionIds: await router.runningSessionIds() });
|
|
2573
2750
|
const scope = webRuntimeScopeFromUrl(url);
|
|
2751
|
+
if (req.method === "POST" && url.pathname === "/api/sessions/delete") {
|
|
2752
|
+
const body = await readJsonBody(req);
|
|
2753
|
+
return sendJson(res, await router.deleteSession(scope, String(body.sessionId ?? "")));
|
|
2754
|
+
}
|
|
2574
2755
|
const repl = await router.get(scope);
|
|
2575
2756
|
if (req.method === "GET" && url.pathname === "/events")
|
|
2576
2757
|
return repl.subscribe(res, router.clientInfo());
|
|
@@ -2599,6 +2780,11 @@ export async function handleWebRequest(req, res, router) {
|
|
|
2599
2780
|
const page = repl.terminalOutput(url.searchParams.get("sessionId") ?? "", url.searchParams.get("runId") ?? "", url.searchParams.get("stream") ?? "stdout", Number(url.searchParams.get("offset") ?? 0), Number(url.searchParams.get("limitBytes") ?? 65536), url.searchParams.get("tail") === "true");
|
|
2600
2781
|
return page ? sendJson(res, page) : sendJson(res, { error: "终端输出不可获取或无权访问" }, 404);
|
|
2601
2782
|
}
|
|
2783
|
+
const queuedImageMatch = /^\/api\/queue\/images\/(image_[0-9a-f-]{36})$/iu.exec(url.pathname);
|
|
2784
|
+
if (req.method === "GET" && queuedImageMatch) {
|
|
2785
|
+
const image = repl.queuedImage(queuedImageMatch[1]);
|
|
2786
|
+
return image ? sendImage(res, image, req) : sendJson(res, { error: "图片不可用" }, 404);
|
|
2787
|
+
}
|
|
2602
2788
|
if (req.method === "GET" && url.pathname === "/api/state")
|
|
2603
2789
|
return sendJson(res, repl.snapshot(true));
|
|
2604
2790
|
if (req.method === "GET" && url.pathname === "/api/runtime-context")
|
|
@@ -2776,10 +2962,6 @@ export async function handleWebRequest(req, res, router) {
|
|
|
2776
2962
|
}
|
|
2777
2963
|
if (req.method === "POST" && url.pathname === "/api/sessions/new")
|
|
2778
2964
|
return sendJson(res, await repl.newSession());
|
|
2779
|
-
if (req.method === "POST" && url.pathname === "/api/sessions/delete") {
|
|
2780
|
-
const body = await readJsonBody(req);
|
|
2781
|
-
return sendJson(res, await repl.deleteSession(String(body.sessionId ?? "")));
|
|
2782
|
-
}
|
|
2783
2965
|
if (req.method === "GET" && url.pathname === "/api/login")
|
|
2784
2966
|
return sendJson(res, repl.loginForm(url.searchParams.get("provider") ?? undefined));
|
|
2785
2967
|
if (req.method === "POST" && url.pathname === "/api/login") {
|