@zqagent/core 0.2.15 → 0.2.16
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/cli/index.cjs +1061 -70
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1061 -70
- package/dist/cli/index.js.map +1 -1
- package/dist/{explore-COqHKDoK.d.cts → explore-BgRhj6SP.d.cts} +10 -0
- package/dist/{explore-B_bIPzmB.d.ts → explore-RtZtb-V6.d.ts} +10 -0
- package/dist/index.cjs +15 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +1039 -64
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +1039 -64
- package/dist/server/index.js.map +1 -1
- package/dist/tools/explore.cjs +15 -0
- package/dist/tools/explore.cjs.map +1 -1
- package/dist/tools/explore.d.cts +1 -1
- package/dist/tools/explore.d.ts +1 -1
- package/dist/tools/explore.js +15 -0
- package/dist/tools/explore.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -551,9 +551,25 @@ var init_wire = __esm({
|
|
|
551
551
|
|
|
552
552
|
// src/cli/index.ts
|
|
553
553
|
init_esm_shims();
|
|
554
|
+
|
|
555
|
+
// src/cli/quietDeprecations.ts
|
|
556
|
+
init_esm_shims();
|
|
557
|
+
var IGNORED_WARNING_CODES = /* @__PURE__ */ new Set(["DEP0040"]);
|
|
558
|
+
function shouldSilenceWarning(warning) {
|
|
559
|
+
const code = warning?.code;
|
|
560
|
+
return typeof code === "string" && IGNORED_WARNING_CODES.has(code);
|
|
561
|
+
}
|
|
562
|
+
var listeners = process.listeners("warning");
|
|
563
|
+
process.removeAllListeners("warning");
|
|
564
|
+
process.on("warning", (warning) => {
|
|
565
|
+
if (shouldSilenceWarning(warning)) return;
|
|
566
|
+
for (const listener of listeners) listener(warning);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// src/cli/index.ts
|
|
554
570
|
import "dotenv/config";
|
|
555
571
|
import * as fs34 from "fs";
|
|
556
|
-
import * as
|
|
572
|
+
import * as readline4 from "readline";
|
|
557
573
|
import * as path44 from "path";
|
|
558
574
|
import { exec } from "child_process";
|
|
559
575
|
import { promisify } from "util";
|
|
@@ -1080,13 +1096,13 @@ function textOf(content) {
|
|
|
1080
1096
|
}
|
|
1081
1097
|
return content != null ? String(content) : "";
|
|
1082
1098
|
}
|
|
1083
|
-
function extractUserMessages(span,
|
|
1099
|
+
function extractUserMessages(span, clip2 = USER_MESSAGE_CLIP) {
|
|
1084
1100
|
const out = [];
|
|
1085
1101
|
for (const m of span) {
|
|
1086
1102
|
if (m.role !== "user") continue;
|
|
1087
1103
|
const text = textOf(m.content).replace(/\s+/g, " ").trim();
|
|
1088
1104
|
if (!text) continue;
|
|
1089
|
-
out.push(text.length >
|
|
1105
|
+
out.push(text.length > clip2 ? text.slice(0, clip2 - 1) + "\u2026" : text);
|
|
1090
1106
|
}
|
|
1091
1107
|
return out;
|
|
1092
1108
|
}
|
|
@@ -1920,6 +1936,21 @@ var TurnEngine = class _TurnEngine {
|
|
|
1920
1936
|
queueSteering(text) {
|
|
1921
1937
|
this._steering.push(text);
|
|
1922
1938
|
}
|
|
1939
|
+
/**
|
|
1940
|
+
* 撤销一条尚未注入的 steering。
|
|
1941
|
+
*
|
|
1942
|
+
* 按文本删第一条匹配项:调用方(SessionManager)已按索引删掉了自己那份队列,
|
|
1943
|
+
* 这里只需保证引擎不再注入同样的内容。用文本而非索引,是因为引擎会在迭代
|
|
1944
|
+
* 间隙批量清空 `_steering`,索引可能已经漂移;文本相同则删哪条都等价。
|
|
1945
|
+
*
|
|
1946
|
+
* @returns 是否真的删掉了(已注入的项返回 false,调用方据此提示用户)
|
|
1947
|
+
*/
|
|
1948
|
+
removeSteering(text) {
|
|
1949
|
+
const i = this._steering.indexOf(text);
|
|
1950
|
+
if (i < 0) return false;
|
|
1951
|
+
this._steering.splice(i, 1);
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1923
1954
|
// ===================== 主入口 =====================
|
|
1924
1955
|
/**
|
|
1925
1956
|
* 运行一轮 Turn
|
|
@@ -10497,13 +10528,14 @@ function renderTerminalTutorial() {
|
|
|
10497
10528
|
// src/cli/link.ts
|
|
10498
10529
|
init_esm_shims();
|
|
10499
10530
|
import * as path28 from "path";
|
|
10500
|
-
import * as
|
|
10531
|
+
import * as readline3 from "readline";
|
|
10501
10532
|
|
|
10502
10533
|
// src/server/store.ts
|
|
10503
10534
|
init_esm_shims();
|
|
10504
10535
|
import * as fs17 from "fs";
|
|
10505
10536
|
import * as fsp from "fs/promises";
|
|
10506
10537
|
import * as path20 from "path";
|
|
10538
|
+
import * as readline from "readline";
|
|
10507
10539
|
import { randomUUID } from "crypto";
|
|
10508
10540
|
|
|
10509
10541
|
// src/server/dbgTrace.ts
|
|
@@ -10723,6 +10755,170 @@ var Store = class {
|
|
|
10723
10755
|
}
|
|
10724
10756
|
}
|
|
10725
10757
|
}
|
|
10758
|
+
/**
|
|
10759
|
+
* 读取事件「一页」:从文件末尾向前,按字节预算取最近的事件。
|
|
10760
|
+
*
|
|
10761
|
+
* - `before` 省略:取最新的 maxBytes 字节
|
|
10762
|
+
* - `before` 指定:只取 seq < before 的事件(向前翻页)
|
|
10763
|
+
*
|
|
10764
|
+
* 为什么不能复用 loadEventsTail:后者只能从文件末尾读固定窗口,
|
|
10765
|
+
* 无法「向前翻页」,而控制台需要滚到顶部继续加载更早的对话。
|
|
10766
|
+
* 这里按块从后往前扫,读到预算或文件头即停;翻了多页之后每次
|
|
10767
|
+
* before 都紧邻上一页,实际只需读 1~2 块。
|
|
10768
|
+
*
|
|
10769
|
+
* 返回按 seq 升序;`hasMore` 表示本页最早一条之前还有更早的事件。
|
|
10770
|
+
*/
|
|
10771
|
+
loadEventPage(tenantId, sessionId, opts) {
|
|
10772
|
+
const p = path20.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
10773
|
+
const { before, maxBytes } = opts;
|
|
10774
|
+
const __t0 = dbgNow();
|
|
10775
|
+
let fd = null;
|
|
10776
|
+
try {
|
|
10777
|
+
const size = fs17.statSync(p).size;
|
|
10778
|
+
if (size === 0) return { events: [], hasMore: false };
|
|
10779
|
+
fd = fs17.openSync(p, "r");
|
|
10780
|
+
const CHUNK = 256 * 1024;
|
|
10781
|
+
let pos = size;
|
|
10782
|
+
const kept = [];
|
|
10783
|
+
let bytes = 0;
|
|
10784
|
+
let hasMore = false;
|
|
10785
|
+
let done = false;
|
|
10786
|
+
while (pos > 0 && !done) {
|
|
10787
|
+
const start = Math.max(0, pos - CHUNK);
|
|
10788
|
+
const len = pos - start;
|
|
10789
|
+
const buf = Buffer.allocUnsafe(len);
|
|
10790
|
+
const read2 = fs17.readSync(fd, buf, 0, len, start);
|
|
10791
|
+
const rows = buf.subarray(0, read2).toString("utf-8").split("\n");
|
|
10792
|
+
if (start > 0) rows.shift();
|
|
10793
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
10794
|
+
const line = rows[i];
|
|
10795
|
+
if (!line) continue;
|
|
10796
|
+
let entry;
|
|
10797
|
+
try {
|
|
10798
|
+
entry = JSON.parse(line);
|
|
10799
|
+
} catch {
|
|
10800
|
+
continue;
|
|
10801
|
+
}
|
|
10802
|
+
if (!Number.isFinite(Number(entry.seq))) continue;
|
|
10803
|
+
if (before !== void 0 && Number(entry.seq) >= before) continue;
|
|
10804
|
+
const sz = Buffer.byteLength(line, "utf-8") + 1;
|
|
10805
|
+
if (bytes > 0 && bytes + sz > maxBytes) {
|
|
10806
|
+
hasMore = true;
|
|
10807
|
+
done = true;
|
|
10808
|
+
break;
|
|
10809
|
+
}
|
|
10810
|
+
bytes += sz;
|
|
10811
|
+
kept.unshift(entry);
|
|
10812
|
+
}
|
|
10813
|
+
pos = start;
|
|
10814
|
+
}
|
|
10815
|
+
dbgMs(
|
|
10816
|
+
"store.loadEventPage",
|
|
10817
|
+
__t0,
|
|
10818
|
+
// [dbg]
|
|
10819
|
+
`size=${size} before=${before ?? "-"} out=${kept.length} bytes\u2248${bytes} more=${hasMore}`
|
|
10820
|
+
);
|
|
10821
|
+
return { events: kept, hasMore };
|
|
10822
|
+
} catch {
|
|
10823
|
+
return { events: [], hasMore: false };
|
|
10824
|
+
} finally {
|
|
10825
|
+
if (fd !== null) {
|
|
10826
|
+
try {
|
|
10827
|
+
fs17.closeSync(fd);
|
|
10828
|
+
} catch {
|
|
10829
|
+
}
|
|
10830
|
+
}
|
|
10831
|
+
}
|
|
10832
|
+
}
|
|
10833
|
+
// ===================== 轮次索引 =====================
|
|
10834
|
+
/** 轮次索引文件路径 */
|
|
10835
|
+
turnsFile(tenantId, sessionId) {
|
|
10836
|
+
return path20.join(this.sessionDir(tenantId, sessionId), "turns.ndjson");
|
|
10837
|
+
}
|
|
10838
|
+
/**
|
|
10839
|
+
* 追加一轮的最新快照(同一 startSeq 后写者胜出)。
|
|
10840
|
+
*
|
|
10841
|
+
* 走与 session.json 相同的每文件写链:索引按「开轮 → 补充消息 → 收轮」
|
|
10842
|
+
* 顺序追加,乱序会让列表读到旧的 running 版本。
|
|
10843
|
+
*/
|
|
10844
|
+
appendTurnIndex(tenantId, sessionId, entry) {
|
|
10845
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
10846
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
10847
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
10848
|
+
const line = JSON.stringify(entry) + "\n";
|
|
10849
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
10850
|
+
const next = prev.then(() => fsp.appendFile(file, line, "utf-8")).catch((err) => {
|
|
10851
|
+
console.error(`[store] appendTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
10852
|
+
});
|
|
10853
|
+
this.writeChains.set(file, next);
|
|
10854
|
+
return next;
|
|
10855
|
+
}
|
|
10856
|
+
/**
|
|
10857
|
+
* 读取轮次索引(已按 startSeq 去重取最后一版,升序)。
|
|
10858
|
+
*
|
|
10859
|
+
* 文件很小(一轮约 0.5KB,1000 轮 ≈ 500KB),同步读即可,
|
|
10860
|
+
* 会话详情打开路径不会因历史长度变慢。
|
|
10861
|
+
*/
|
|
10862
|
+
loadTurnIndex(tenantId, sessionId) {
|
|
10863
|
+
let raw;
|
|
10864
|
+
try {
|
|
10865
|
+
raw = fs17.readFileSync(this.turnsFile(tenantId, sessionId), "utf-8");
|
|
10866
|
+
} catch {
|
|
10867
|
+
return [];
|
|
10868
|
+
}
|
|
10869
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
10870
|
+
for (const line of raw.split("\n")) {
|
|
10871
|
+
if (!line) continue;
|
|
10872
|
+
try {
|
|
10873
|
+
const e = JSON.parse(line);
|
|
10874
|
+
if (!Number.isFinite(Number(e?.startSeq))) continue;
|
|
10875
|
+
byStart.set(Number(e.startSeq), e);
|
|
10876
|
+
} catch {
|
|
10877
|
+
}
|
|
10878
|
+
}
|
|
10879
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
10880
|
+
}
|
|
10881
|
+
/** 整文件替换轮次索引(重建用,tmp + rename 原子写) */
|
|
10882
|
+
saveTurnIndex(tenantId, sessionId, entries) {
|
|
10883
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
10884
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
10885
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
10886
|
+
const snapshot = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
10887
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
10888
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
10889
|
+
const next = prev.then(() => fsp.writeFile(tmp, snapshot, "utf-8")).then(() => fsp.rename(tmp, file)).catch((err) => {
|
|
10890
|
+
console.error(`[store] saveTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
10891
|
+
void fsp.rm(tmp, { force: true });
|
|
10892
|
+
});
|
|
10893
|
+
this.writeChains.set(file, next);
|
|
10894
|
+
return next;
|
|
10895
|
+
}
|
|
10896
|
+
/**
|
|
10897
|
+
* 流式读取整份事件日志(重建索引用)。
|
|
10898
|
+
*
|
|
10899
|
+
* 不能用 loadEvents(readFileSync + 逐行 JSON.parse):大会话几十 MB,
|
|
10900
|
+
* 会长时间阻塞事件循环,导致 SSE 心跳 / MQTT 保活断开。
|
|
10901
|
+
*/
|
|
10902
|
+
async readEventsAsync(tenantId, sessionId) {
|
|
10903
|
+
const p = path20.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
10904
|
+
const out = [];
|
|
10905
|
+
try {
|
|
10906
|
+
const rl2 = readline.createInterface({
|
|
10907
|
+
input: fs17.createReadStream(p, { encoding: "utf-8" }),
|
|
10908
|
+
crlfDelay: Infinity
|
|
10909
|
+
});
|
|
10910
|
+
for await (const line of rl2) {
|
|
10911
|
+
if (!line) continue;
|
|
10912
|
+
try {
|
|
10913
|
+
out.push(JSON.parse(line));
|
|
10914
|
+
} catch {
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
} catch {
|
|
10918
|
+
return out;
|
|
10919
|
+
}
|
|
10920
|
+
return out;
|
|
10921
|
+
}
|
|
10726
10922
|
// ===================== 节点身份 & 管理员密钥 =====================
|
|
10727
10923
|
/** 节点稳定标识:{root}/node.json(首次启动生成后长期固定) */
|
|
10728
10924
|
loadNodeId() {
|
|
@@ -10922,7 +11118,7 @@ function defaultNodeLabel() {
|
|
|
10922
11118
|
// src/cli/account-enroll.ts
|
|
10923
11119
|
init_esm_shims();
|
|
10924
11120
|
import * as path25 from "path";
|
|
10925
|
-
import * as
|
|
11121
|
+
import * as readline2 from "readline/promises";
|
|
10926
11122
|
|
|
10927
11123
|
// src/server/projects.ts
|
|
10928
11124
|
init_esm_shims();
|
|
@@ -11180,6 +11376,19 @@ function addAgg(into, tokens, costUsd) {
|
|
|
11180
11376
|
function utcDay(iso) {
|
|
11181
11377
|
return iso.slice(0, 10);
|
|
11182
11378
|
}
|
|
11379
|
+
function sumAgg(values) {
|
|
11380
|
+
const out = zeroAgg();
|
|
11381
|
+
for (const v of values) {
|
|
11382
|
+
out.calls += v.calls;
|
|
11383
|
+
out.input += v.input;
|
|
11384
|
+
out.output += v.output;
|
|
11385
|
+
out.cacheRead += v.cacheRead;
|
|
11386
|
+
out.cacheWrite += v.cacheWrite;
|
|
11387
|
+
out.costUsd += v.costUsd;
|
|
11388
|
+
out.unpriced += v.unpriced;
|
|
11389
|
+
}
|
|
11390
|
+
return out;
|
|
11391
|
+
}
|
|
11183
11392
|
function monthOf(iso) {
|
|
11184
11393
|
return iso.slice(0, 7);
|
|
11185
11394
|
}
|
|
@@ -11455,6 +11664,23 @@ var UsageLedger = class {
|
|
|
11455
11664
|
}
|
|
11456
11665
|
return all;
|
|
11457
11666
|
}
|
|
11667
|
+
/**
|
|
11668
|
+
* 按「项目 × 天」聚合(供控制面把消耗拆到每个项目)。
|
|
11669
|
+
*
|
|
11670
|
+
* 汇总文件里只存了按项目、按天两个独立维度,拿不到交叉项,因此这里从
|
|
11671
|
+
* 账本明细现算——调用方(节点上报)只取最近 40 天,读的是 2 个月文件,
|
|
11672
|
+
* 5 分钟一次的成本可忽略。
|
|
11673
|
+
*/
|
|
11674
|
+
projectByDay(opts) {
|
|
11675
|
+
const out = {};
|
|
11676
|
+
for (const rec of this.allRecords({ from: opts.from, to: opts.to })) {
|
|
11677
|
+
const day = utcDay(rec.ts);
|
|
11678
|
+
const days = out[rec.projectId] ?? (out[rec.projectId] = {});
|
|
11679
|
+
const agg = days[day] ?? (days[day] = zeroAgg());
|
|
11680
|
+
addAgg(agg, rec, rec.costUsd);
|
|
11681
|
+
}
|
|
11682
|
+
return out;
|
|
11683
|
+
}
|
|
11458
11684
|
summary(opts) {
|
|
11459
11685
|
this.ensureInit();
|
|
11460
11686
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
@@ -11754,6 +11980,141 @@ var EVENT_BUFFER_MAX = 2e3;
|
|
|
11754
11980
|
var EVENTS_ACK_MAX_BYTES = 512 * 1024;
|
|
11755
11981
|
var APPROVAL_TIMEOUT_MS = (Number(process.env.APPROVAL_TIMEOUT_SECONDS) || 15 * 60) * 1e3;
|
|
11756
11982
|
var LAST_USER_MESSAGE_MAX = 200;
|
|
11983
|
+
var TURN_TEXT_MAX = 500;
|
|
11984
|
+
var TURN_REPLY_MAX_BYTES = 256 * 1024;
|
|
11985
|
+
var TURN_REPLY_MAX_TEXT = 256 * 1024;
|
|
11986
|
+
var TURNS_DEFAULT_LIMIT = 20;
|
|
11987
|
+
function clip(text, max = LAST_USER_MESSAGE_MAX) {
|
|
11988
|
+
const s = text ?? "";
|
|
11989
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
11990
|
+
}
|
|
11991
|
+
function asNum(v) {
|
|
11992
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
11993
|
+
}
|
|
11994
|
+
function newTurnEntry(ev, input) {
|
|
11995
|
+
return {
|
|
11996
|
+
v: 1,
|
|
11997
|
+
startSeq: ev.seq,
|
|
11998
|
+
endSeq: ev.seq,
|
|
11999
|
+
startedAt: ev.ts,
|
|
12000
|
+
endedAt: null,
|
|
12001
|
+
status: "running",
|
|
12002
|
+
inputs: [{ seq: ev.seq, ts: ev.ts, text: clip(input, TURN_TEXT_MAX) }],
|
|
12003
|
+
replyPreview: "",
|
|
12004
|
+
producedFiles: [],
|
|
12005
|
+
llmCalls: 0,
|
|
12006
|
+
toolCalls: 0,
|
|
12007
|
+
tokensIn: 0,
|
|
12008
|
+
tokensOut: 0
|
|
12009
|
+
};
|
|
12010
|
+
}
|
|
12011
|
+
function applyTurnEvent(turn, ev, toolSeen) {
|
|
12012
|
+
const d = ev.data ?? {};
|
|
12013
|
+
turn.endSeq = ev.seq;
|
|
12014
|
+
switch (ev.type) {
|
|
12015
|
+
case "queue_steering": {
|
|
12016
|
+
turn.inputs.push({
|
|
12017
|
+
seq: ev.seq,
|
|
12018
|
+
ts: ev.ts,
|
|
12019
|
+
text: clip(String(d["text"] ?? ""), TURN_TEXT_MAX)
|
|
12020
|
+
});
|
|
12021
|
+
break;
|
|
12022
|
+
}
|
|
12023
|
+
case "assistant_message": {
|
|
12024
|
+
turn.llmCalls += 1;
|
|
12025
|
+
const text = String(d["text"] ?? "");
|
|
12026
|
+
if (text) {
|
|
12027
|
+
turn.replyPreview = clip(text);
|
|
12028
|
+
turn.replySeq = ev.seq;
|
|
12029
|
+
}
|
|
12030
|
+
const u = d["usage"];
|
|
12031
|
+
if (u && typeof u === "object") {
|
|
12032
|
+
const usage = u;
|
|
12033
|
+
turn.tokensIn += asNum(usage["input"]);
|
|
12034
|
+
turn.tokensOut += asNum(usage["output"]);
|
|
12035
|
+
}
|
|
12036
|
+
break;
|
|
12037
|
+
}
|
|
12038
|
+
case "tool_started":
|
|
12039
|
+
case "tool_finished": {
|
|
12040
|
+
const key = String(d["reqId"] ?? "") || `${String(d["name"] ?? "")}:${ev.seq}`;
|
|
12041
|
+
if (!toolSeen.has(key)) {
|
|
12042
|
+
toolSeen.add(key);
|
|
12043
|
+
turn.toolCalls += 1;
|
|
12044
|
+
}
|
|
12045
|
+
break;
|
|
12046
|
+
}
|
|
12047
|
+
case "error": {
|
|
12048
|
+
turn.error = clip(String(d["message"] ?? d["error"] ?? ""));
|
|
12049
|
+
break;
|
|
12050
|
+
}
|
|
12051
|
+
default:
|
|
12052
|
+
break;
|
|
12053
|
+
}
|
|
12054
|
+
}
|
|
12055
|
+
function closeTurnEntry(turn, ev) {
|
|
12056
|
+
const d = ev.data ?? {};
|
|
12057
|
+
turn.endSeq = ev.seq;
|
|
12058
|
+
turn.endedAt = ev.ts;
|
|
12059
|
+
const status = String(d["status"] ?? "");
|
|
12060
|
+
turn.status = ev.type === "interrupted" ? status || "interrupted" : status || "completed";
|
|
12061
|
+
const files = d["produced_files"];
|
|
12062
|
+
if (Array.isArray(files)) {
|
|
12063
|
+
turn.producedFiles = files.map((x) => String(x ?? "")).filter(Boolean);
|
|
12064
|
+
}
|
|
12065
|
+
if (typeof d["iterations"] === "number") turn.iterations = d["iterations"];
|
|
12066
|
+
const startMs = Date.parse(turn.startedAt);
|
|
12067
|
+
const endMs = Date.parse(ev.ts);
|
|
12068
|
+
if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
|
|
12069
|
+
turn.durationMs = endMs - startMs;
|
|
12070
|
+
}
|
|
12071
|
+
}
|
|
12072
|
+
function buildTurnIndex(events) {
|
|
12073
|
+
const out = [];
|
|
12074
|
+
let cur = null;
|
|
12075
|
+
let toolSeen = /* @__PURE__ */ new Set();
|
|
12076
|
+
for (const ev of events) {
|
|
12077
|
+
if (ev.type === "turn_start") {
|
|
12078
|
+
cur = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
12079
|
+
toolSeen = /* @__PURE__ */ new Set();
|
|
12080
|
+
out.push(cur);
|
|
12081
|
+
continue;
|
|
12082
|
+
}
|
|
12083
|
+
if (!cur) continue;
|
|
12084
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
12085
|
+
closeTurnEntry(cur, ev);
|
|
12086
|
+
cur = null;
|
|
12087
|
+
continue;
|
|
12088
|
+
}
|
|
12089
|
+
applyTurnEvent(cur, ev, toolSeen);
|
|
12090
|
+
}
|
|
12091
|
+
for (const t of out) {
|
|
12092
|
+
if (!t.endedAt && t.error) t.status = "error";
|
|
12093
|
+
}
|
|
12094
|
+
return out;
|
|
12095
|
+
}
|
|
12096
|
+
function summaryFromEntry(t) {
|
|
12097
|
+
return {
|
|
12098
|
+
startSeq: t.startSeq,
|
|
12099
|
+
endSeq: t.endSeq,
|
|
12100
|
+
startedAt: t.startedAt,
|
|
12101
|
+
endedAt: t.endedAt,
|
|
12102
|
+
status: t.status,
|
|
12103
|
+
input: t.inputs[0]?.text ?? "",
|
|
12104
|
+
reply: t.replyPreview,
|
|
12105
|
+
llmCalls: t.llmCalls,
|
|
12106
|
+
toolCalls: t.toolCalls,
|
|
12107
|
+
tokensIn: t.tokensIn,
|
|
12108
|
+
tokensOut: t.tokensOut,
|
|
12109
|
+
iterations: t.iterations,
|
|
12110
|
+
durationMs: t.durationMs,
|
|
12111
|
+
error: t.error,
|
|
12112
|
+
inputs: t.inputs,
|
|
12113
|
+
userCount: t.inputs.length,
|
|
12114
|
+
replySeq: t.replySeq,
|
|
12115
|
+
producedFiles: t.producedFiles
|
|
12116
|
+
};
|
|
12117
|
+
}
|
|
11757
12118
|
function isAffirmative(answer) {
|
|
11758
12119
|
const a = answer.trim().toLowerCase();
|
|
11759
12120
|
if (!a) return false;
|
|
@@ -11778,6 +12139,10 @@ var SessionManager = class {
|
|
|
11778
12139
|
opts;
|
|
11779
12140
|
runtimes = /* @__PURE__ */ new Map();
|
|
11780
12141
|
// sessionId → runtime
|
|
12142
|
+
/** 轮次索引的内存副本(运行中的轮比磁盘索引新;重启后为空) */
|
|
12143
|
+
turnIndexCache = /* @__PURE__ */ new Map();
|
|
12144
|
+
/** 进行中的索引重建任务(并发请求合并到同一个) */
|
|
12145
|
+
turnIndexBuilds = /* @__PURE__ */ new Map();
|
|
11781
12146
|
/** 全局通知 SSE 订阅者(控制台通知中心) */
|
|
11782
12147
|
notificationSubs = /* @__PURE__ */ new Set();
|
|
11783
12148
|
/**
|
|
@@ -11832,6 +12197,17 @@ var SessionManager = class {
|
|
|
11832
12197
|
rt.status = status;
|
|
11833
12198
|
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
11834
12199
|
}
|
|
12200
|
+
/**
|
|
12201
|
+
* 通知里的「最近一条用户消息」:截断到 LAST_USER_MESSAGE_MAX。
|
|
12202
|
+
*
|
|
12203
|
+
* 三处共用(session_status / turn_completed / turn_stopped),避免各自写一遍
|
|
12204
|
+
* 截断规则后长度不一致——控制台弹窗与任务列表会显示同一句话的两个版本。
|
|
12205
|
+
*/
|
|
12206
|
+
_lastMessage(rt) {
|
|
12207
|
+
const text = (rt.lastInput ?? "").trim();
|
|
12208
|
+
if (!text) return null;
|
|
12209
|
+
return text.length > LAST_USER_MESSAGE_MAX ? `${text.slice(0, LAST_USER_MESSAGE_MAX)}\u2026` : text;
|
|
12210
|
+
}
|
|
11835
12211
|
/**
|
|
11836
12212
|
* session_status 通知载荷:状态 + 最近一条用户消息(截断)。
|
|
11837
12213
|
*
|
|
@@ -11840,10 +12216,24 @@ var SessionManager = class {
|
|
|
11840
12216
|
*/
|
|
11841
12217
|
_statusData(rt) {
|
|
11842
12218
|
const data = { status: rt.status };
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
12219
|
+
if (rt.lastStartedAt) data.lastStartedAt = rt.lastStartedAt;
|
|
12220
|
+
const msg = this._lastMessage(rt);
|
|
12221
|
+
if (msg) data.lastUserMessage = msg;
|
|
12222
|
+
return data;
|
|
12223
|
+
}
|
|
12224
|
+
/**
|
|
12225
|
+
* 任务结束类通知(完成 / 达到迭代上限)的载荷:在 turn_end 数据之外补上
|
|
12226
|
+
* 「会话名 + 最后一条用户消息」。
|
|
12227
|
+
*
|
|
12228
|
+
* 控制台右下角的完成弹窗要说明「哪个会话、用户让它做了什么」。这些字段
|
|
12229
|
+
* 以节点侧为准随通知一起下发,前端就不必依赖快照(nodeInfo / session_status)
|
|
12230
|
+
* 是否已经到达——刚打开页面或通知乱序时才不会退化成只剩一句「任务已完成」。
|
|
12231
|
+
*/
|
|
12232
|
+
_turnEndData(rt, eventData, extra) {
|
|
12233
|
+
const data = { ...eventData, ...extra };
|
|
12234
|
+
if (rt.name) data.sessionName = rt.name;
|
|
12235
|
+
const msg = this._lastMessage(rt);
|
|
12236
|
+
if (msg) data.lastUserMessage = msg;
|
|
11847
12237
|
return data;
|
|
11848
12238
|
}
|
|
11849
12239
|
/** 触发通知回调(无 handler 时为空操作,异常隔离不冒泡) */
|
|
@@ -12000,6 +12390,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
12000
12390
|
this.runtimes.delete(sessionId);
|
|
12001
12391
|
}
|
|
12002
12392
|
this.store.deleteSession(tenantId, sessionId);
|
|
12393
|
+
this.turnIndexCache.delete(sessionId);
|
|
12003
12394
|
this._publishNotify(tenantId, {
|
|
12004
12395
|
kind: "session_status",
|
|
12005
12396
|
sessionId,
|
|
@@ -12043,6 +12434,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
12043
12434
|
throw new ApiError(409, "busy", "\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D");
|
|
12044
12435
|
}
|
|
12045
12436
|
rt.engine.queueSteering(input);
|
|
12437
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
12046
12438
|
rt.pendingSteers.push(input);
|
|
12047
12439
|
this._recordLastInput(rt, input);
|
|
12048
12440
|
return "steered";
|
|
@@ -12075,9 +12467,42 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
12075
12467
|
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
12076
12468
|
if (!rt.engine) throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
12077
12469
|
rt.engine.queueSteering(text);
|
|
12470
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
12078
12471
|
rt.pendingSteers.push(text);
|
|
12079
12472
|
this._recordLastInput(rt, text);
|
|
12080
12473
|
}
|
|
12474
|
+
/**
|
|
12475
|
+
* 撤销一条尚未注入的补充信息(用户反悔)。
|
|
12476
|
+
*
|
|
12477
|
+
* 队列里的项还没进聊天流,此时只能靠中断整个任务来「收回」代价太大。
|
|
12478
|
+
* index 取自 GET /state 的 pendingSteers(与前端展示顺序一致),两侧同时删:
|
|
12479
|
+
* 只删前端那份,引擎下一迭代仍会把它注入对话;只删引擎那份,队列里会残留
|
|
12480
|
+
* 一条永远不注入的条目。
|
|
12481
|
+
*
|
|
12482
|
+
* 若撤的正是「最近用户指令」,把它还原成队列里的上一条、或本轮被覆盖前的
|
|
12483
|
+
* 指令——否则工作台还会显示这条已经被取消的内容。
|
|
12484
|
+
*
|
|
12485
|
+
* @returns 剩余待注入队列(前端据此刷新提示区)
|
|
12486
|
+
*/
|
|
12487
|
+
unsteer(tenant, sessionId, index) {
|
|
12488
|
+
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
12489
|
+
if (!rt.engine || !rt.runTask) {
|
|
12490
|
+
throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
12491
|
+
}
|
|
12492
|
+
if (!Number.isInteger(index) || index < 0 || index >= rt.pendingSteers.length) {
|
|
12493
|
+
throw new ApiError(409, "already_injected", "\u8FD9\u6761\u8865\u5145\u4FE1\u606F\u5DF2\u8FDB\u5165\u5BF9\u8BDD\uFF0C\u65E0\u6CD5\u64A4\u9500");
|
|
12494
|
+
}
|
|
12495
|
+
const [removed] = rt.pendingSteers.splice(index, 1);
|
|
12496
|
+
if (removed !== void 0) {
|
|
12497
|
+
rt.engine.removeSteering(removed);
|
|
12498
|
+
if (removed === rt.lastInput) {
|
|
12499
|
+
rt.lastInput = rt.pendingSteers.length > 0 ? rt.pendingSteers[rt.pendingSteers.length - 1] : rt.steerBaseInput;
|
|
12500
|
+
this._persist(rt);
|
|
12501
|
+
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
12502
|
+
}
|
|
12503
|
+
}
|
|
12504
|
+
return [...rt.pendingSteers];
|
|
12505
|
+
}
|
|
12081
12506
|
/**
|
|
12082
12507
|
* 已提交、尚未注入当前任务的补充信息。
|
|
12083
12508
|
*
|
|
@@ -12510,44 +12935,214 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
12510
12935
|
status: rt.status
|
|
12511
12936
|
};
|
|
12512
12937
|
}
|
|
12513
|
-
/**
|
|
12514
|
-
|
|
12938
|
+
/**
|
|
12939
|
+
* 事件分页(向前 / 向后两种方向)。
|
|
12940
|
+
*
|
|
12941
|
+
* - `since`:取 seq > since(实时增量 / 重连补齐)
|
|
12942
|
+
* - `before`:取 seq < before(控制台滚到顶部加载更早的对话)
|
|
12943
|
+
* - `maxBytes`:单次回执的字节预算(默认沿用 EVENTS_ACK_MAX_BYTES)
|
|
12944
|
+
*
|
|
12945
|
+
* 两者同时给出时按 `since`(向后)处理。`hasMore` 只对向前翻页有意义:
|
|
12946
|
+
* 表示本页最早一条之前还有更早的事件。
|
|
12947
|
+
*
|
|
12948
|
+
* 向前翻页一律走磁盘:内存缓冲只保留最近 EVENT_BUFFER_MAX 条,
|
|
12949
|
+
* 更早的事件只在 events.jsonl 里。
|
|
12950
|
+
*/
|
|
12951
|
+
eventsPage(tenantId, sessionId, opts = {}) {
|
|
12515
12952
|
const __t0 = dbgNow();
|
|
12953
|
+
const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : EVENTS_ACK_MAX_BYTES;
|
|
12516
12954
|
const rt = this.runtimes.get(sessionId);
|
|
12955
|
+
if (opts.before !== void 0) {
|
|
12956
|
+
const since2 = opts.since ?? 0;
|
|
12957
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
12958
|
+
before: opts.before,
|
|
12959
|
+
maxBytes
|
|
12960
|
+
});
|
|
12961
|
+
const ev2 = since2 > 0 ? page.events.filter((e) => e.seq > since2) : page.events;
|
|
12962
|
+
dbgMs(
|
|
12963
|
+
"session.eventsPage(before/\u78C1\u76D8)",
|
|
12964
|
+
__t0,
|
|
12965
|
+
// [dbg]
|
|
12966
|
+
`sid=${sessionId.slice(0, 8)} since=${since2} before=${opts.before} out=${ev2.length} more=${page.hasMore}`
|
|
12967
|
+
);
|
|
12968
|
+
return {
|
|
12969
|
+
events: ev2,
|
|
12970
|
+
latestSeq: rt?.seq ?? page.events.at(-1)?.seq ?? 0,
|
|
12971
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
12972
|
+
hasMore: page.hasMore
|
|
12973
|
+
};
|
|
12974
|
+
}
|
|
12975
|
+
const since = opts.since ?? 0;
|
|
12517
12976
|
if (rt) {
|
|
12518
|
-
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since));
|
|
12977
|
+
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since), maxBytes);
|
|
12519
12978
|
dbgMs(
|
|
12520
|
-
"session.
|
|
12979
|
+
"session.eventsPage(\u5185\u5B58)",
|
|
12521
12980
|
__t0,
|
|
12522
12981
|
// [dbg]
|
|
12523
12982
|
`sid=${sessionId.slice(0, 8)} since=${since} in=${rt.events.length} out=${ev2.length}`
|
|
12524
12983
|
);
|
|
12525
|
-
return {
|
|
12984
|
+
return {
|
|
12985
|
+
events: ev2,
|
|
12986
|
+
latestSeq: rt.seq,
|
|
12987
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
12988
|
+
// 内存缓冲被截断(或 since 早于缓冲起点)时,磁盘上可能还有更早的
|
|
12989
|
+
hasMore: !!ev2.length && ev2[0].seq > (rt.events[0]?.seq ?? 0)
|
|
12990
|
+
};
|
|
12526
12991
|
}
|
|
12527
12992
|
const all = this.store.loadEventsTail(tenantId, sessionId, EVENTS_ACK_MAX_BYTES * 2);
|
|
12528
|
-
const ev = this._boundedTail(
|
|
12993
|
+
const ev = this._boundedTail(
|
|
12994
|
+
all.filter((e) => e.seq > since),
|
|
12995
|
+
maxBytes
|
|
12996
|
+
);
|
|
12529
12997
|
dbgMs(
|
|
12530
|
-
"session.
|
|
12998
|
+
"session.eventsPage(\u78C1\u76D8)",
|
|
12531
12999
|
__t0,
|
|
12532
13000
|
// [dbg]
|
|
12533
13001
|
`sid=${sessionId.slice(0, 8)} since=${since} loaded=${all.length} out=${ev.length}`
|
|
12534
13002
|
);
|
|
12535
13003
|
return {
|
|
12536
13004
|
events: ev,
|
|
12537
|
-
latestSeq: all.at(-1)?.seq ?? 0
|
|
13005
|
+
latestSeq: all.at(-1)?.seq ?? 0,
|
|
13006
|
+
earliestSeq: ev[0]?.seq ?? 0,
|
|
13007
|
+
// 读的是文件尾部窗口:窗口已满说明更早的事件还在文件里
|
|
13008
|
+
hasMore: all.length > 0 && ev.length > 0 && ev[0].seq > all[0].seq
|
|
13009
|
+
};
|
|
13010
|
+
}
|
|
13011
|
+
/**
|
|
13012
|
+
* 兼容旧调用:只按 since 取增量。
|
|
13013
|
+
* @deprecated 新代码用 eventsPage(支持 before 向前翻页)
|
|
13014
|
+
*/
|
|
13015
|
+
eventsSince(tenantId, sessionId, since) {
|
|
13016
|
+
const r = this.eventsPage(tenantId, sessionId, { since });
|
|
13017
|
+
return { events: r.events, latestSeq: r.latestSeq };
|
|
13018
|
+
}
|
|
13019
|
+
/**
|
|
13020
|
+
* 「按 turn 汇总」的轻量列表:只回标题与统计,点开某一轮再单独拉该轮回复。
|
|
13021
|
+
*
|
|
13022
|
+
* 数据来自 turns.ndjson 索引(派生自事件流),读取成本与会话历史长度无关:
|
|
13023
|
+
* 老会话首次访问时后台/本请求内重建一次,之后每页只读几 KB。
|
|
13024
|
+
*
|
|
13025
|
+
* - `before`:只返回 startSeq < before 的轮次(向前翻页)
|
|
13026
|
+
* - `turns` 按时间升序;`hasMore` 表示更早还有轮次
|
|
13027
|
+
*/
|
|
13028
|
+
async sessionTurns(tenantId, sessionId, opts = {}) {
|
|
13029
|
+
const limit = Math.min(Math.max(opts.limit ?? TURNS_DEFAULT_LIMIT, 1), 100);
|
|
13030
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
13031
|
+
const before = opts.before;
|
|
13032
|
+
const endIdx = before === void 0 ? entries.length : entries.findIndex((t) => t.startSeq >= before);
|
|
13033
|
+
const end = endIdx < 0 ? entries.length : endIdx;
|
|
13034
|
+
const start = Math.max(0, end - limit);
|
|
13035
|
+
const picked = entries.slice(start, end);
|
|
13036
|
+
return {
|
|
13037
|
+
turns: picked.map(summaryFromEntry),
|
|
13038
|
+
hasMore: start > 0,
|
|
13039
|
+
earliestSeq: picked[0]?.startSeq ?? 0,
|
|
13040
|
+
total: entries.length,
|
|
13041
|
+
userTotal: entries.reduce((n, t) => n + t.inputs.length, 0)
|
|
13042
|
+
};
|
|
13043
|
+
}
|
|
13044
|
+
/**
|
|
13045
|
+
* 某一轮的用户消息与最终回复(含产出文件)。
|
|
13046
|
+
*
|
|
13047
|
+
* 只读两条索引记录 + 尾部一个事件页,不会因该轮工具输出很大而变慢:
|
|
13048
|
+
* 回复正文紧邻 turn_end,按 replySeq 精确读即可。
|
|
13049
|
+
*/
|
|
13050
|
+
async sessionTurnReply(tenantId, sessionId, startSeq) {
|
|
13051
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
13052
|
+
const turn = entries.find((t) => t.startSeq === startSeq);
|
|
13053
|
+
if (!turn) throw new ApiError(404, "not_found", "\u8F6E\u6B21\u4E0D\u5B58\u5728");
|
|
13054
|
+
let reply = null;
|
|
13055
|
+
let truncated = false;
|
|
13056
|
+
if (turn.replySeq !== void 0) {
|
|
13057
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
13058
|
+
before: turn.replySeq + 1,
|
|
13059
|
+
maxBytes: TURN_REPLY_MAX_BYTES
|
|
13060
|
+
});
|
|
13061
|
+
const ev = page.events.find((e) => e.seq === turn.replySeq);
|
|
13062
|
+
if (ev && ev.type === "assistant_message") {
|
|
13063
|
+
const text = String(ev.data?.["text"] ?? "");
|
|
13064
|
+
if (text.length > TURN_REPLY_MAX_TEXT) {
|
|
13065
|
+
reply = { seq: ev.seq, ts: ev.ts, text: text.slice(0, TURN_REPLY_MAX_TEXT) };
|
|
13066
|
+
truncated = true;
|
|
13067
|
+
} else {
|
|
13068
|
+
reply = { seq: ev.seq, ts: ev.ts, text };
|
|
13069
|
+
}
|
|
13070
|
+
}
|
|
13071
|
+
}
|
|
13072
|
+
return {
|
|
13073
|
+
startSeq: turn.startSeq,
|
|
13074
|
+
endSeq: turn.endSeq,
|
|
13075
|
+
status: turn.status,
|
|
13076
|
+
startedAt: turn.startedAt,
|
|
13077
|
+
endedAt: turn.endedAt,
|
|
13078
|
+
userMessages: turn.inputs,
|
|
13079
|
+
reply,
|
|
13080
|
+
truncated,
|
|
13081
|
+
producedFiles: turn.producedFiles
|
|
12538
13082
|
};
|
|
12539
13083
|
}
|
|
13084
|
+
/**
|
|
13085
|
+
* 取轮次索引;缺失时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
13086
|
+
*
|
|
13087
|
+
* 索引是派生数据:损坏/落后都只需重建,不影响会话本身。
|
|
13088
|
+
*/
|
|
13089
|
+
/**
|
|
13090
|
+
* 取轮次索引;缺失/落后时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
13091
|
+
*
|
|
13092
|
+
* 索引是派生数据:损坏或落后都只需重建,不影响会话本身。
|
|
13093
|
+
*/
|
|
13094
|
+
async _ensureTurnIndex(tenantId, sessionId) {
|
|
13095
|
+
let entries = this._mergeTurnIndex(tenantId, sessionId);
|
|
13096
|
+
if (entries.length > 0 && !this.runtimes.has(sessionId)) {
|
|
13097
|
+
const last = entries[entries.length - 1];
|
|
13098
|
+
const tail = this.store.loadEventsTail(tenantId, sessionId, 64 * 1024);
|
|
13099
|
+
const newTurnStarted = tail.some((e) => e.type === "turn_start" && e.seq > last.endSeq);
|
|
13100
|
+
if (newTurnStarted || last.status === "running") entries = [];
|
|
13101
|
+
}
|
|
13102
|
+
if (entries.length > 0) return entries;
|
|
13103
|
+
const pending = this.turnIndexBuilds.get(sessionId);
|
|
13104
|
+
if (pending) return pending;
|
|
13105
|
+
const task = this._rebuildTurnIndex(tenantId, sessionId).finally(() => {
|
|
13106
|
+
this.turnIndexBuilds.delete(sessionId);
|
|
13107
|
+
});
|
|
13108
|
+
this.turnIndexBuilds.set(sessionId, task);
|
|
13109
|
+
return task;
|
|
13110
|
+
}
|
|
13111
|
+
/** 合并磁盘索引与内存索引(内存版本更新,同一 startSeq 以内存为准) */
|
|
13112
|
+
_mergeTurnIndex(tenantId, sessionId) {
|
|
13113
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
13114
|
+
for (const e of this.store.loadTurnIndex(tenantId, sessionId)) byStart.set(e.startSeq, e);
|
|
13115
|
+
for (const e of this.turnIndexCache.get(sessionId) ?? []) byStart.set(e.startSeq, e);
|
|
13116
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
13117
|
+
}
|
|
13118
|
+
/** 从 events.jsonl 顺序流式重建轮次索引(一次性,之后增量维护) */
|
|
13119
|
+
async _rebuildTurnIndex(tenantId, sessionId) {
|
|
13120
|
+
const __t0 = dbgNow();
|
|
13121
|
+
const events = await this.store.readEventsAsync(tenantId, sessionId);
|
|
13122
|
+
const entries = buildTurnIndex(events);
|
|
13123
|
+
if (entries.length > 0) {
|
|
13124
|
+
await this.store.saveTurnIndex(tenantId, sessionId, entries);
|
|
13125
|
+
if (this.runtimes.has(sessionId)) this.turnIndexCache.set(sessionId, entries);
|
|
13126
|
+
}
|
|
13127
|
+
dbgMs(
|
|
13128
|
+
"session._rebuildTurnIndex",
|
|
13129
|
+
__t0,
|
|
13130
|
+
// [dbg]
|
|
13131
|
+
`sid=${sessionId.slice(0, 8)} events=${events.length} turns=${entries.length}`
|
|
13132
|
+
);
|
|
13133
|
+
return entries;
|
|
13134
|
+
}
|
|
12540
13135
|
/**
|
|
12541
13136
|
* 按字节预算截取事件「尾部」(至少保留最后一条),确保 MQTT 回执远小于单包上限。
|
|
12542
13137
|
* 供 get_events 使用:会话历史可任意大,但 MQTT 链路不能承载大包。
|
|
12543
13138
|
*/
|
|
12544
|
-
_boundedTail(events) {
|
|
13139
|
+
_boundedTail(events, maxBytes = EVENTS_ACK_MAX_BYTES) {
|
|
12545
13140
|
const __t0 = dbgNow();
|
|
12546
13141
|
let total = 0;
|
|
12547
13142
|
let i = events.length;
|
|
12548
13143
|
while (i > 0) {
|
|
12549
13144
|
const size = Buffer.byteLength(JSON.stringify(events[i - 1]));
|
|
12550
|
-
if (total > 0 && total + size >
|
|
13145
|
+
if (total > 0 && total + size > maxBytes) break;
|
|
12551
13146
|
total += size;
|
|
12552
13147
|
i -= 1;
|
|
12553
13148
|
}
|
|
@@ -12667,6 +13262,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
12667
13262
|
nameSource: rec.nameSource,
|
|
12668
13263
|
taskCount: rec.taskCount ?? 0,
|
|
12669
13264
|
lastCompletedAt: rec.lastCompletedAt,
|
|
13265
|
+
lastStartedAt: rec.lastStartedAt,
|
|
12670
13266
|
naming: false,
|
|
12671
13267
|
turnStartedAt: 0
|
|
12672
13268
|
};
|
|
@@ -12684,10 +13280,11 @@ data: ${JSON.stringify(entry.data)}
|
|
|
12684
13280
|
}
|
|
12685
13281
|
/** 装配引擎并启动一轮 Turn(供 submitTurn / submitOrSteer 复用) */
|
|
12686
13282
|
_startTurn(rt, input) {
|
|
13283
|
+
const engine = this._buildEngine(rt.tenant, rt, true);
|
|
12687
13284
|
rt.lastInput = input;
|
|
12688
|
-
this._setStatus(rt, "running");
|
|
12689
13285
|
rt.turnStartedAt = Date.now();
|
|
12690
|
-
|
|
13286
|
+
rt.lastStartedAt = new Date(rt.turnStartedAt).toISOString();
|
|
13287
|
+
this._setStatus(rt, "running");
|
|
12691
13288
|
rt.engine = engine;
|
|
12692
13289
|
this._persist(rt);
|
|
12693
13290
|
rt.runTask = this._drive(rt, engine.run(input));
|
|
@@ -12746,18 +13343,18 @@ data: ${JSON.stringify(entry.data)}
|
|
|
12746
13343
|
this._setStatus(rt, "completed");
|
|
12747
13344
|
rt.taskCount += 1;
|
|
12748
13345
|
rt.lastCompletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12749
|
-
this._emitNotify(
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
durationMs
|
|
12753
|
-
|
|
13346
|
+
this._emitNotify(
|
|
13347
|
+
rt,
|
|
13348
|
+
"turn_completed",
|
|
13349
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
13350
|
+
);
|
|
12754
13351
|
} else if (status === "max_iterations_exceeded") {
|
|
12755
13352
|
this._setStatus(rt, "stopped");
|
|
12756
|
-
this._emitNotify(
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
durationMs
|
|
12760
|
-
|
|
13353
|
+
this._emitNotify(
|
|
13354
|
+
rt,
|
|
13355
|
+
"turn_stopped",
|
|
13356
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
13357
|
+
);
|
|
12761
13358
|
}
|
|
12762
13359
|
this._persist(rt);
|
|
12763
13360
|
if (status === "completed" && engine && shouldAutoName(rt.taskCount, rt.nameSource)) {
|
|
@@ -12768,7 +13365,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
12768
13365
|
case "error" /* ERROR */:
|
|
12769
13366
|
this._setStatus(rt, "error");
|
|
12770
13367
|
this._persist(rt);
|
|
12771
|
-
this._emitNotify(rt, "error", event.data);
|
|
13368
|
+
this._emitNotify(rt, "error", this._turnEndData(rt, event.data, {}));
|
|
12772
13369
|
break;
|
|
12773
13370
|
case "interrupted" /* INTERRUPTED */:
|
|
12774
13371
|
this._setStatus(rt, "interrupted");
|
|
@@ -12794,9 +13391,17 @@ data: ${JSON.stringify(entry.data)}
|
|
|
12794
13391
|
this._setStatus(rt, "error");
|
|
12795
13392
|
this._persist(rt);
|
|
12796
13393
|
} finally {
|
|
13394
|
+
if (rt.turnIndex) {
|
|
13395
|
+
const turn = rt.turnIndex;
|
|
13396
|
+
if (!turn.endedAt && turn.error) turn.status = "error";
|
|
13397
|
+
this._appendTurnIndex(rt, turn);
|
|
13398
|
+
rt.turnIndex = void 0;
|
|
13399
|
+
rt.turnToolKeys = void 0;
|
|
13400
|
+
}
|
|
12797
13401
|
rt.runTask = null;
|
|
12798
13402
|
rt.engine = null;
|
|
12799
13403
|
rt.pendingSteers = [];
|
|
13404
|
+
rt.steerBaseInput = void 0;
|
|
12800
13405
|
}
|
|
12801
13406
|
}
|
|
12802
13407
|
_buffer(rt, event) {
|
|
@@ -12809,6 +13414,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
12809
13414
|
rt.events.push(entry);
|
|
12810
13415
|
if (rt.events.length > EVENT_BUFFER_MAX) rt.events.shift();
|
|
12811
13416
|
void this.store.appendEvent(rt.tenantId, rt.sessionId, entry);
|
|
13417
|
+
this._trackTurnIndex(rt, entry);
|
|
12812
13418
|
for (const res of rt.subscribers) this._sseSend(res, entry);
|
|
12813
13419
|
this._broadcastSessionEvent(rt.tenantId, rt.sessionId, entry);
|
|
12814
13420
|
try {
|
|
@@ -12816,6 +13422,42 @@ data: ${JSON.stringify(entry.data)}
|
|
|
12816
13422
|
} catch {
|
|
12817
13423
|
}
|
|
12818
13424
|
}
|
|
13425
|
+
/**
|
|
13426
|
+
* 维护运行中轮次的索引(in-memory 记账 + 关键节点落盘)。
|
|
13427
|
+
*
|
|
13428
|
+
* 落盘只在「开轮 / 补充消息 / 收轮」发生,delta 事件仅推进 endSeq 与统计,
|
|
13429
|
+
* 因此索引写入量是每轮 2~3 行,与会话事件量无关。
|
|
13430
|
+
*/
|
|
13431
|
+
_trackTurnIndex(rt, ev) {
|
|
13432
|
+
if (ev.type === "turn_start") {
|
|
13433
|
+
const turn2 = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
13434
|
+
rt.turnIndex = turn2;
|
|
13435
|
+
rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
13436
|
+
this._appendTurnIndex(rt, turn2);
|
|
13437
|
+
return;
|
|
13438
|
+
}
|
|
13439
|
+
const turn = rt.turnIndex;
|
|
13440
|
+
if (!turn) return;
|
|
13441
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
13442
|
+
closeTurnEntry(turn, ev);
|
|
13443
|
+
this._appendTurnIndex(rt, turn);
|
|
13444
|
+
rt.turnIndex = void 0;
|
|
13445
|
+
rt.turnToolKeys = void 0;
|
|
13446
|
+
return;
|
|
13447
|
+
}
|
|
13448
|
+
if (!rt.turnToolKeys) rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
13449
|
+
const inputsBefore = turn.inputs.length;
|
|
13450
|
+
applyTurnEvent(turn, ev, rt.turnToolKeys);
|
|
13451
|
+
if (turn.inputs.length !== inputsBefore) this._appendTurnIndex(rt, turn);
|
|
13452
|
+
}
|
|
13453
|
+
/** 写入一版轮次索引(内存副本 + 异步落盘) */
|
|
13454
|
+
_appendTurnIndex(rt, turn) {
|
|
13455
|
+
const cached3 = this.turnIndexCache.get(rt.sessionId) ?? [];
|
|
13456
|
+
const i = cached3.findIndex((t) => t.startSeq === turn.startSeq);
|
|
13457
|
+
const next = i >= 0 ? [...cached3.slice(0, i), turn, ...cached3.slice(i + 1)] : [...cached3, turn];
|
|
13458
|
+
this.turnIndexCache.set(rt.sessionId, next);
|
|
13459
|
+
void this.store.appendTurnIndex(rt.tenantId, rt.sessionId, turn);
|
|
13460
|
+
}
|
|
12819
13461
|
/** 回收内存 run 任务(释放引用交给 GC;磁盘状态保持不动) */
|
|
12820
13462
|
_recycleRunTask(rt) {
|
|
12821
13463
|
rt.runTask = null;
|
|
@@ -12853,6 +13495,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
12853
13495
|
nameSource: rt.nameSource,
|
|
12854
13496
|
taskCount: rt.taskCount,
|
|
12855
13497
|
lastCompletedAt: rt.lastCompletedAt,
|
|
13498
|
+
lastStartedAt: rt.lastStartedAt,
|
|
12856
13499
|
modelOverridden: rt.modelOverridden,
|
|
12857
13500
|
providerId: rt.providerId,
|
|
12858
13501
|
permissionMode: rt.permissionMode,
|
|
@@ -13256,6 +13899,7 @@ function allSessionStates(store, manager) {
|
|
|
13256
13899
|
name: s.name ?? null,
|
|
13257
13900
|
status: s.status,
|
|
13258
13901
|
lastUserMessage: lastUserMessage(s),
|
|
13902
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
13259
13903
|
lastCompletedAt: lastCompletedAt(s)
|
|
13260
13904
|
});
|
|
13261
13905
|
}
|
|
@@ -13310,7 +13954,7 @@ function buildNodeInfo(store, opts) {
|
|
|
13310
13954
|
nodeId: opts.nodeId ?? ensureNodeId(store),
|
|
13311
13955
|
platform: process.platform,
|
|
13312
13956
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
13313
|
-
version: "0.2.
|
|
13957
|
+
version: "0.2.16",
|
|
13314
13958
|
admin: opts.admin,
|
|
13315
13959
|
projects: opts.projects
|
|
13316
13960
|
};
|
|
@@ -13432,7 +14076,7 @@ async function maybeAccountEnroll(opts = {}) {
|
|
|
13432
14076
|
cliErr(`\u63A5\u5165\u68C0\u67E5\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
13433
14077
|
return;
|
|
13434
14078
|
}
|
|
13435
|
-
const rl2 =
|
|
14079
|
+
const rl2 = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
13436
14080
|
try {
|
|
13437
14081
|
await runWizard(rl2, store, opts);
|
|
13438
14082
|
} catch (e) {
|
|
@@ -13526,7 +14170,7 @@ import * as path26 from "path";
|
|
|
13526
14170
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
13527
14171
|
import { createRequire as createRequire3 } from "module";
|
|
13528
14172
|
function currentVersion() {
|
|
13529
|
-
const injected = "0.2.
|
|
14173
|
+
const injected = "0.2.16"?.trim();
|
|
13530
14174
|
if (injected) return injected;
|
|
13531
14175
|
try {
|
|
13532
14176
|
const req2 = createRequire3(import.meta.url);
|
|
@@ -14242,7 +14886,7 @@ async function runLinkCommand(argv, ctx = {}) {
|
|
|
14242
14886
|
const getRl = () => {
|
|
14243
14887
|
if (ctx.rl) return asPrompter(ctx.rl);
|
|
14244
14888
|
if (!interactive) return null;
|
|
14245
|
-
ownedRl =
|
|
14889
|
+
ownedRl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
14246
14890
|
return asPrompter(ownedRl);
|
|
14247
14891
|
};
|
|
14248
14892
|
const closeOwnedRl = () => {
|
|
@@ -14864,6 +15508,7 @@ import * as fs23 from "fs";
|
|
|
14864
15508
|
import * as path31 from "path";
|
|
14865
15509
|
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
14866
15510
|
var MAX_DOWNLOAD_BYTES = 16 * 1024 * 1024;
|
|
15511
|
+
var MAX_BINARY_BYTES = 2.5 * 1024 * 1024;
|
|
14867
15512
|
var MAX_ENTRIES2 = 2e3;
|
|
14868
15513
|
function toPosix(p) {
|
|
14869
15514
|
return p.split(path31.sep).join("/");
|
|
@@ -15011,6 +15656,26 @@ function writeProjectFile(workspaceRoot, relPath, content) {
|
|
|
15011
15656
|
}
|
|
15012
15657
|
return { path: toPosix(path31.relative(base, file)), size: buf.length };
|
|
15013
15658
|
}
|
|
15659
|
+
function writeProjectFileBase64(workspaceRoot, relPath, base64) {
|
|
15660
|
+
const base = projectRoot(workspaceRoot);
|
|
15661
|
+
const rel = (relPath ?? "").trim();
|
|
15662
|
+
if (!rel) throw new Error("\u6587\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
|
|
15663
|
+
const file = resolveWithin(base, rel);
|
|
15664
|
+
const b64 = String(base64 ?? "").replace(/\s+/g, "");
|
|
15665
|
+
if (!b64) throw new Error("\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
|
|
15666
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) throw new Error("\u5185\u5BB9\u4E0D\u662F\u5408\u6CD5\u7684 base64");
|
|
15667
|
+
if (Math.floor(b64.length * 3 / 4) > MAX_BINARY_BYTES) {
|
|
15668
|
+
throw new Error(`\u5185\u5BB9\u8D85\u8FC7 ${MAX_BINARY_BYTES / (1024 * 1024)}MB \u4E0A\u9650\uFF0C\u65E0\u6CD5\u4FDD\u5B58`);
|
|
15669
|
+
}
|
|
15670
|
+
const buf = Buffer.from(b64, "base64");
|
|
15671
|
+
try {
|
|
15672
|
+
fs23.mkdirSync(path31.dirname(file), { recursive: true });
|
|
15673
|
+
fs23.writeFileSync(file, buf);
|
|
15674
|
+
} catch (e) {
|
|
15675
|
+
throw new Error(`\u5199\u5165\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
15676
|
+
}
|
|
15677
|
+
return { path: toPosix(path31.relative(base, file)), size: buf.length };
|
|
15678
|
+
}
|
|
15014
15679
|
function createProjectDir(workspaceRoot, relPath) {
|
|
15015
15680
|
const base = projectRoot(workspaceRoot);
|
|
15016
15681
|
const rel = (relPath ?? "").trim();
|
|
@@ -15084,6 +15749,42 @@ function searchProjectFiles(workspaceRoot, query) {
|
|
|
15084
15749
|
walk(base, "");
|
|
15085
15750
|
return out;
|
|
15086
15751
|
}
|
|
15752
|
+
function deleteProjectPaths(workspaceRoot, relPaths) {
|
|
15753
|
+
const base = projectRoot(workspaceRoot);
|
|
15754
|
+
const out = { deleted: [], failed: [] };
|
|
15755
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15756
|
+
for (const raw of relPaths ?? []) {
|
|
15757
|
+
const rel = (raw ?? "").trim().replace(/^[/\\]+/, "");
|
|
15758
|
+
if (!rel) {
|
|
15759
|
+
out.failed.push({ path: String(raw ?? ""), error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
15760
|
+
continue;
|
|
15761
|
+
}
|
|
15762
|
+
if (seen.has(rel)) continue;
|
|
15763
|
+
seen.add(rel);
|
|
15764
|
+
let abs;
|
|
15765
|
+
try {
|
|
15766
|
+
abs = resolveWithin(base, rel);
|
|
15767
|
+
} catch (e) {
|
|
15768
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
15769
|
+
continue;
|
|
15770
|
+
}
|
|
15771
|
+
if (path31.resolve(abs) === path31.resolve(base)) {
|
|
15772
|
+
out.failed.push({ path: rel, error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
15773
|
+
continue;
|
|
15774
|
+
}
|
|
15775
|
+
if (!fs23.existsSync(abs)) {
|
|
15776
|
+
out.failed.push({ path: rel, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
|
|
15777
|
+
continue;
|
|
15778
|
+
}
|
|
15779
|
+
try {
|
|
15780
|
+
fs23.rmSync(abs, { recursive: true, force: true });
|
|
15781
|
+
out.deleted.push(toPosix(path31.relative(base, abs)));
|
|
15782
|
+
} catch (e) {
|
|
15783
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
15784
|
+
}
|
|
15785
|
+
}
|
|
15786
|
+
return out;
|
|
15787
|
+
}
|
|
15087
15788
|
|
|
15088
15789
|
// src/server/projectOps.ts
|
|
15089
15790
|
init_esm_shims();
|
|
@@ -15489,7 +16190,7 @@ function buildNodeHost(store, manager, diskPath) {
|
|
|
15489
16190
|
arch: process.arch,
|
|
15490
16191
|
release: os9.release(),
|
|
15491
16192
|
uptimeSec: Math.round(os9.uptime()),
|
|
15492
|
-
version: "0.2.
|
|
16193
|
+
version: "0.2.16",
|
|
15493
16194
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15494
16195
|
cpu: {
|
|
15495
16196
|
count: cpus2.length,
|
|
@@ -15632,6 +16333,7 @@ function parseApprovalDecision(raw) {
|
|
|
15632
16333
|
}
|
|
15633
16334
|
function createApp(store, manager, opts = {}) {
|
|
15634
16335
|
setupServerHandlers(manager);
|
|
16336
|
+
const updater = opts.updater ?? null;
|
|
15635
16337
|
const routes = [
|
|
15636
16338
|
// ---------- 会话管理 ----------
|
|
15637
16339
|
{
|
|
@@ -15773,6 +16475,18 @@ function createApp(store, manager, opts = {}) {
|
|
|
15773
16475
|
ok(res, { accepted: true });
|
|
15774
16476
|
}
|
|
15775
16477
|
},
|
|
16478
|
+
{
|
|
16479
|
+
method: "POST",
|
|
16480
|
+
pattern: ["sessions", ":id", "steer", "remove"],
|
|
16481
|
+
// 撤销未注入的补充信息
|
|
16482
|
+
fn: (_req, res, { tenant, params, body }) => {
|
|
16483
|
+
const index = Number(body?.index);
|
|
16484
|
+
if (!Number.isInteger(index))
|
|
16485
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
16486
|
+
const pendingSteers = manager.unsteer(tenant, params["id"], index);
|
|
16487
|
+
ok(res, { accepted: true, pendingSteers });
|
|
16488
|
+
}
|
|
16489
|
+
},
|
|
15776
16490
|
{
|
|
15777
16491
|
method: "POST",
|
|
15778
16492
|
pattern: ["sessions", ":id", "interrupt"],
|
|
@@ -16303,6 +17017,30 @@ function createApp(store, manager, opts = {}) {
|
|
|
16303
17017
|
}
|
|
16304
17018
|
}
|
|
16305
17019
|
},
|
|
17020
|
+
{
|
|
17021
|
+
// 二进制写入(图片编辑保存):body = { path, base64 }
|
|
17022
|
+
method: "POST",
|
|
17023
|
+
pattern: ["files", "binary"],
|
|
17024
|
+
fn: (_req, res, { tenant, body }) => {
|
|
17025
|
+
const raw = body ?? {};
|
|
17026
|
+
try {
|
|
17027
|
+
ok(
|
|
17028
|
+
res,
|
|
17029
|
+
writeProjectFileBase64(
|
|
17030
|
+
tenant.workspaceRoot ?? null,
|
|
17031
|
+
String(raw.path ?? ""),
|
|
17032
|
+
String(raw.base64 ?? "")
|
|
17033
|
+
)
|
|
17034
|
+
);
|
|
17035
|
+
} catch (e) {
|
|
17036
|
+
throw new ApiError(
|
|
17037
|
+
400,
|
|
17038
|
+
"bad_request",
|
|
17039
|
+
e instanceof Error ? e.message : String(e)
|
|
17040
|
+
);
|
|
17041
|
+
}
|
|
17042
|
+
}
|
|
17043
|
+
},
|
|
16306
17044
|
// ---------- 项目终端(在工作区执行命令) ----------
|
|
16307
17045
|
{
|
|
16308
17046
|
method: "POST",
|
|
@@ -16653,6 +17391,21 @@ function createApp(store, manager, opts = {}) {
|
|
|
16653
17391
|
}
|
|
16654
17392
|
}
|
|
16655
17393
|
},
|
|
17394
|
+
{
|
|
17395
|
+
method: "POST",
|
|
17396
|
+
pattern: ["files", "delete"],
|
|
17397
|
+
// 删除文件 / 目录(目录递归);逐条返回失败原因
|
|
17398
|
+
fn: (_req, res, { tenant, body }) => {
|
|
17399
|
+
const raw = body ?? {};
|
|
17400
|
+
const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
|
|
17401
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
17402
|
+
try {
|
|
17403
|
+
ok(res, deleteProjectPaths(tenant.workspaceRoot ?? null, paths));
|
|
17404
|
+
} catch (e) {
|
|
17405
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
17406
|
+
}
|
|
17407
|
+
}
|
|
17408
|
+
},
|
|
16656
17409
|
{
|
|
16657
17410
|
method: "POST",
|
|
16658
17411
|
pattern: ["files", "create"],
|
|
@@ -16881,6 +17634,34 @@ function createApp(store, manager, opts = {}) {
|
|
|
16881
17634
|
}
|
|
16882
17635
|
}
|
|
16883
17636
|
},
|
|
17637
|
+
// ---------- 节点自更新(状态 / 检查 / 安装) ----------
|
|
17638
|
+
// action=install 先回执、后台安装并退出进程(由服务管理器拉起新版本),
|
|
17639
|
+
// 因此回执成功只代表「已受理」,是否升级成功要用 action=status 再确认。
|
|
17640
|
+
{
|
|
17641
|
+
method: "POST",
|
|
17642
|
+
pattern: ["node", "self-update"],
|
|
17643
|
+
projectOptional: true,
|
|
17644
|
+
fn: async (req2, res, { body }) => {
|
|
17645
|
+
const b = body ?? {};
|
|
17646
|
+
const action = String(b.action ?? "status");
|
|
17647
|
+
if (!updater) {
|
|
17648
|
+
throw new ApiError(400, "bad_request", "\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A\u8282\u70B9\u672A\u4EE5 serve \u6A21\u5F0F\u8FD0\u884C");
|
|
17649
|
+
}
|
|
17650
|
+
if (action === "status") {
|
|
17651
|
+
ok(res, updater.status());
|
|
17652
|
+
return;
|
|
17653
|
+
}
|
|
17654
|
+
if (action === "check") {
|
|
17655
|
+
ok(res, await updater.inspect());
|
|
17656
|
+
return;
|
|
17657
|
+
}
|
|
17658
|
+
if (action === "install") {
|
|
17659
|
+
ok(res, await updater.requestInstall(b.force === true));
|
|
17660
|
+
return;
|
|
17661
|
+
}
|
|
17662
|
+
throw new ApiError(400, "bad_request", `\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
17663
|
+
}
|
|
17664
|
+
},
|
|
16884
17665
|
// ---------- 节点级会话状态(跨项目;只含标识与 status) ----------
|
|
16885
17666
|
// 供节点首页 / 工作台建立初始视图,后续变化由 session_status 通知增量推送。
|
|
16886
17667
|
{
|
|
@@ -17043,7 +17824,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
17043
17824
|
// 阻止反向代理(nginx 等)缓冲 SSE 流
|
|
17044
17825
|
"X-Accel-Buffering": "no"
|
|
17045
17826
|
});
|
|
17046
|
-
const
|
|
17827
|
+
const sinceRaw2 = new URL(
|
|
17047
17828
|
req2.url,
|
|
17048
17829
|
"http://localhost"
|
|
17049
17830
|
).searchParams.get("since");
|
|
@@ -17051,19 +17832,51 @@ function createApp(store, manager, opts = {}) {
|
|
|
17051
17832
|
tenant.id,
|
|
17052
17833
|
params["id"],
|
|
17053
17834
|
res,
|
|
17054
|
-
|
|
17835
|
+
sinceRaw2 ? Number(sinceRaw2) : 0
|
|
17055
17836
|
);
|
|
17056
17837
|
return;
|
|
17057
17838
|
}
|
|
17058
|
-
const
|
|
17059
|
-
|
|
17839
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
17840
|
+
const sinceRaw = q.get("since");
|
|
17841
|
+
const beforeRaw = q.get("before");
|
|
17842
|
+
const maxBytesRaw = q.get("maxBytes");
|
|
17843
|
+
ok(
|
|
17844
|
+
res,
|
|
17845
|
+
manager.eventsPage(tenant.id, params["id"], {
|
|
17846
|
+
since: sinceRaw === null ? void 0 : Number(sinceRaw) || 0,
|
|
17847
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
17848
|
+
maxBytes: maxBytesRaw === null ? void 0 : Number(maxBytesRaw) || void 0
|
|
17849
|
+
})
|
|
17060
17850
|
);
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
17851
|
+
}
|
|
17852
|
+
},
|
|
17853
|
+
{
|
|
17854
|
+
method: "GET",
|
|
17855
|
+
pattern: ["sessions", ":id", "turns"],
|
|
17856
|
+
// 轮次汇总(会话详情的列表)
|
|
17857
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
17858
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
17859
|
+
const beforeRaw = q.get("before");
|
|
17860
|
+
const limitRaw = q.get("limit");
|
|
17861
|
+
ok(
|
|
17862
|
+
res,
|
|
17863
|
+
await manager.sessionTurns(tenant.id, params["id"], {
|
|
17864
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
17865
|
+
limit: limitRaw === null ? void 0 : Number(limitRaw) || void 0
|
|
17866
|
+
})
|
|
17065
17867
|
);
|
|
17066
|
-
|
|
17868
|
+
}
|
|
17869
|
+
},
|
|
17870
|
+
{
|
|
17871
|
+
method: "GET",
|
|
17872
|
+
// 单轮详情:用户消息(含补充) + 最终回复 + 产出文件
|
|
17873
|
+
pattern: ["sessions", ":id", "turns", ":seq", "reply"],
|
|
17874
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
17875
|
+
const startSeq = Number(params["seq"]);
|
|
17876
|
+
if (!Number.isFinite(startSeq) || startSeq <= 0) {
|
|
17877
|
+
throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
17878
|
+
}
|
|
17879
|
+
ok(res, await manager.sessionTurnReply(tenant.id, params["id"], startSeq));
|
|
17067
17880
|
}
|
|
17068
17881
|
}
|
|
17069
17882
|
];
|
|
@@ -17096,7 +17909,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
17096
17909
|
json(res, 200, {
|
|
17097
17910
|
status: "ok",
|
|
17098
17911
|
// 版本让运维/探针能确认线上跑的是哪个构建(更新与回滚据此判断)
|
|
17099
|
-
version: "0.2.
|
|
17912
|
+
version: "0.2.16",
|
|
17100
17913
|
features: { mcp: hasServerMcpConfig() }
|
|
17101
17914
|
});
|
|
17102
17915
|
return;
|
|
@@ -17369,6 +18182,24 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
17369
18182
|
switch (env3.type) {
|
|
17370
18183
|
case "ping":
|
|
17371
18184
|
return ok2({ pong: true, ts: Date.now() });
|
|
18185
|
+
case "self_update": {
|
|
18186
|
+
const updater = deps.getUpdater?.() ?? null;
|
|
18187
|
+
if (!updater) {
|
|
18188
|
+
return fail("\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A\u8282\u70B9\u672A\u4EE5 serve \u6A21\u5F0F\u8FD0\u884C\uFF08\u7F3A\u5C11\u66F4\u65B0\u63A7\u5236\u5668\uFF09");
|
|
18189
|
+
}
|
|
18190
|
+
const action = String(payload.action ?? "status");
|
|
18191
|
+
if (action === "status") {
|
|
18192
|
+
return ok2(updater.status());
|
|
18193
|
+
}
|
|
18194
|
+
if (action === "check") {
|
|
18195
|
+
return ok2(await updater.inspect());
|
|
18196
|
+
}
|
|
18197
|
+
if (action === "install") {
|
|
18198
|
+
const started = await updater.requestInstall(payload.force === true);
|
|
18199
|
+
return ok2(started);
|
|
18200
|
+
}
|
|
18201
|
+
return fail(`\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
18202
|
+
}
|
|
17372
18203
|
case "get_node_info":
|
|
17373
18204
|
return ok2(
|
|
17374
18205
|
buildNodeInfo(store, {
|
|
@@ -17491,8 +18322,22 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
17491
18322
|
);
|
|
17492
18323
|
}
|
|
17493
18324
|
case "get_events": {
|
|
17494
|
-
const since =
|
|
17495
|
-
|
|
18325
|
+
const since = payload.since === void 0 ? void 0 : Number(payload.since) || 0;
|
|
18326
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
18327
|
+
const maxBytes = Number(payload.maxBytes) || void 0;
|
|
18328
|
+
return ok2(
|
|
18329
|
+
manager.eventsPage(project().id, sessionId(), { since, before, maxBytes })
|
|
18330
|
+
);
|
|
18331
|
+
}
|
|
18332
|
+
case "get_session_turns": {
|
|
18333
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
18334
|
+
const limit = Number(payload.limit) || void 0;
|
|
18335
|
+
return ok2(await manager.sessionTurns(project().id, sessionId(), { before, limit }));
|
|
18336
|
+
}
|
|
18337
|
+
case "get_session_turn_reply": {
|
|
18338
|
+
const startSeq = Number(payload.startSeq) || 0;
|
|
18339
|
+
if (startSeq <= 0) throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
18340
|
+
return ok2(await manager.sessionTurnReply(project().id, sessionId(), startSeq));
|
|
17496
18341
|
}
|
|
17497
18342
|
case "get_config":
|
|
17498
18343
|
return ok2(configView2(project()));
|
|
@@ -17553,6 +18398,16 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
17553
18398
|
)
|
|
17554
18399
|
);
|
|
17555
18400
|
}
|
|
18401
|
+
case "write_file_base64": {
|
|
18402
|
+
const t = project();
|
|
18403
|
+
return ok2(
|
|
18404
|
+
writeProjectFileBase64(
|
|
18405
|
+
t.workspaceRoot ?? null,
|
|
18406
|
+
String(payload.path ?? ""),
|
|
18407
|
+
String(payload.base64 ?? "")
|
|
18408
|
+
)
|
|
18409
|
+
);
|
|
18410
|
+
}
|
|
17556
18411
|
case "create_dir": {
|
|
17557
18412
|
const t = project();
|
|
17558
18413
|
return ok2(createProjectDir(t.workspaceRoot ?? null, String(payload.path ?? "")));
|
|
@@ -17567,6 +18422,13 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
17567
18422
|
)
|
|
17568
18423
|
);
|
|
17569
18424
|
}
|
|
18425
|
+
case "delete_files": {
|
|
18426
|
+
const t = project();
|
|
18427
|
+
const raw = payload.paths;
|
|
18428
|
+
const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
|
|
18429
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
18430
|
+
return ok2(deleteProjectPaths(t.workspaceRoot ?? null, paths));
|
|
18431
|
+
}
|
|
17570
18432
|
case "search_files": {
|
|
17571
18433
|
const t = project();
|
|
17572
18434
|
return ok2(searchProjectFiles(t.workspaceRoot ?? null, String(payload.query ?? "")));
|
|
@@ -17793,6 +18655,13 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
17793
18655
|
manager.steer(project(), sessionId(), text);
|
|
17794
18656
|
return ok2({ accepted: true });
|
|
17795
18657
|
}
|
|
18658
|
+
case "unsteer": {
|
|
18659
|
+
const index = Number(payload.index);
|
|
18660
|
+
if (!Number.isInteger(index)) {
|
|
18661
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
18662
|
+
}
|
|
18663
|
+
return ok2({ accepted: true, pendingSteers: manager.unsteer(project(), sessionId(), index) });
|
|
18664
|
+
}
|
|
17796
18665
|
case "interrupt":
|
|
17797
18666
|
manager.interrupt(project(), sessionId());
|
|
17798
18667
|
return ok2({ accepted: true });
|
|
@@ -18328,6 +19197,8 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
|
|
|
18328
19197
|
"get_session_states",
|
|
18329
19198
|
"get_state",
|
|
18330
19199
|
"get_events",
|
|
19200
|
+
"get_session_turns",
|
|
19201
|
+
"get_session_turn_reply",
|
|
18331
19202
|
"get_config",
|
|
18332
19203
|
"list_models",
|
|
18333
19204
|
"list_dirs",
|
|
@@ -18577,6 +19448,10 @@ var FleetNodeBridge = class {
|
|
|
18577
19448
|
_dispatchEnvelope(env3) {
|
|
18578
19449
|
const run = () => this._dispatch(env3);
|
|
18579
19450
|
dbgMark(`recv:${env3.type}`, `reqId=${env3.reqId}`);
|
|
19451
|
+
if (env3.type === "self_update" && String(env3.payload?.action ?? "status") !== "install") {
|
|
19452
|
+
this._runReadonly(run);
|
|
19453
|
+
return;
|
|
19454
|
+
}
|
|
18580
19455
|
if (READONLY_CMD_TYPES.has(env3.type) || this._isReadonlyBatch(env3)) {
|
|
18581
19456
|
this._runReadonly(run);
|
|
18582
19457
|
return;
|
|
@@ -18603,7 +19478,7 @@ var FleetNodeBridge = class {
|
|
|
18603
19478
|
_invalidateSessionCache() {
|
|
18604
19479
|
this.writeEpoch += 1;
|
|
18605
19480
|
for (const key of [...this.cmdResultCache.keys()]) {
|
|
18606
|
-
if (/^(get_state|get_events|list_sessions|get_session_states)\|/.test(key)) {
|
|
19481
|
+
if (/^(get_state|get_events|get_session_turns|get_session_turn_reply|list_sessions|get_session_states)\|/.test(key)) {
|
|
18607
19482
|
this.cmdResultCache.delete(key);
|
|
18608
19483
|
}
|
|
18609
19484
|
}
|
|
@@ -18739,11 +19614,17 @@ var FleetNodeBridge = class {
|
|
|
18739
19614
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
18740
19615
|
platform: process.platform,
|
|
18741
19616
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
18742
|
-
version: "0.2.
|
|
19617
|
+
version: "0.2.16",
|
|
18743
19618
|
ip: primaryIp()
|
|
18744
19619
|
};
|
|
18745
19620
|
}
|
|
18746
|
-
/**
|
|
19621
|
+
/**
|
|
19622
|
+
* 用量汇总(供控制面账户级聚合):总量 + 最近 40 天按天 + 最近 40 天按项目。
|
|
19623
|
+
*
|
|
19624
|
+
* 按项目这一维是为控制台「今日 / 最近 30 天消耗拆到每个项目」服务的:
|
|
19625
|
+
* 账本里的汇总只存了按天、按项目两个独立维度,交叉项只能现算,因此这里
|
|
19626
|
+
* 从明细聚合后随 info 一起上报(Hub 按 (节点, 项目, 天) 覆盖写入,天然幂等)。
|
|
19627
|
+
*/
|
|
18747
19628
|
_usagePayload() {
|
|
18748
19629
|
const ledger = this.opts.deps.manager.usageLedger();
|
|
18749
19630
|
if (!ledger) return void 0;
|
|
@@ -18751,11 +19632,22 @@ var FleetNodeBridge = class {
|
|
|
18751
19632
|
const days = {};
|
|
18752
19633
|
const keys = Object.keys(r.byDay).sort().slice(-40);
|
|
18753
19634
|
for (const k of keys) days[k] = r.byDay[k];
|
|
19635
|
+
const from = keys[0] ?? "";
|
|
19636
|
+
const to = keys[keys.length - 1] ?? "";
|
|
19637
|
+
const projects = {};
|
|
19638
|
+
if (from && to) {
|
|
19639
|
+
for (const [projectId, byDay] of Object.entries(
|
|
19640
|
+
ledger.projectByDay({ from, to })
|
|
19641
|
+
)) {
|
|
19642
|
+
projects[projectId] = { total: sumAgg(Object.values(byDay)), byDay };
|
|
19643
|
+
}
|
|
19644
|
+
}
|
|
18754
19645
|
return {
|
|
18755
19646
|
since: r.since,
|
|
18756
19647
|
total: r.total,
|
|
18757
19648
|
legacy: r.legacy ?? null,
|
|
18758
|
-
byDay: days
|
|
19649
|
+
byDay: days,
|
|
19650
|
+
projects
|
|
18759
19651
|
};
|
|
18760
19652
|
}
|
|
18761
19653
|
/**
|
|
@@ -18790,6 +19682,8 @@ var FleetNodeBridge = class {
|
|
|
18790
19682
|
name: s.name ?? null,
|
|
18791
19683
|
status: s.status,
|
|
18792
19684
|
lastUserMessage: lastUserMessage(s),
|
|
19685
|
+
// 控制面按它排「最近在跑什么」,运行中的长任务才不会被排到后面
|
|
19686
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
18793
19687
|
// 控制面的聚合邮件靠它判断「这台机器最近有没有在干活」;
|
|
18794
19688
|
// 状态跃迁可能被 info 节流跳过,完成时间不会。
|
|
18795
19689
|
lastCompletedAt: s.lastCompletedAt ?? null
|
|
@@ -18818,7 +19712,7 @@ var FleetNodeBridge = class {
|
|
|
18818
19712
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
18819
19713
|
platform: process.platform,
|
|
18820
19714
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
18821
|
-
version: "0.2.
|
|
19715
|
+
version: "0.2.16",
|
|
18822
19716
|
ip: primaryIp(),
|
|
18823
19717
|
projects,
|
|
18824
19718
|
totals,
|
|
@@ -20359,7 +21253,7 @@ function verifySignature(data, signatureBase64, publicKey) {
|
|
|
20359
21253
|
}
|
|
20360
21254
|
|
|
20361
21255
|
// src/update/downloader.ts
|
|
20362
|
-
var
|
|
21256
|
+
var MAX_BINARY_BYTES2 = 400 * 1024 * 1024;
|
|
20363
21257
|
var MANIFEST_TIMEOUT_MS = 15e3;
|
|
20364
21258
|
var DOWNLOAD_TIMEOUT_MS2 = 10 * 6e4;
|
|
20365
21259
|
async function fetchManifest(url, publicKey, opts = {}) {
|
|
@@ -20405,7 +21299,7 @@ async function downloadBinary(bin, destPath, log = () => {
|
|
|
20405
21299
|
if (!res.ok) return { error: `\u4E0B\u8F7D\u5931\u8D25 HTTP ${res.status}` };
|
|
20406
21300
|
if (!res.body) return { error: "\u4E0B\u8F7D\u54CD\u5E94\u6CA1\u6709 body" };
|
|
20407
21301
|
const expected = bin.size && bin.size > 0 ? bin.size : void 0;
|
|
20408
|
-
const limit = expected ? Math.max(expected * 2, 1024 * 1024) :
|
|
21302
|
+
const limit = expected ? Math.max(expected * 2, 1024 * 1024) : MAX_BINARY_BYTES2;
|
|
20409
21303
|
const hash = createHash3("sha256");
|
|
20410
21304
|
let bytes = 0;
|
|
20411
21305
|
try {
|
|
@@ -20612,6 +21506,31 @@ function nextIntervalMs() {
|
|
|
20612
21506
|
const jitter = base * 0.15 * (Math.random() * 2 - 1);
|
|
20613
21507
|
return Math.max(6e4, Math.round(base + jitter));
|
|
20614
21508
|
}
|
|
21509
|
+
function evaluateManifest(base, manifest) {
|
|
21510
|
+
if (!parseVersion(base.currentVersion) || compareVersions(manifest.version, base.currentVersion) <= 0) {
|
|
21511
|
+
return {
|
|
21512
|
+
...base,
|
|
21513
|
+
updateAvailable: false,
|
|
21514
|
+
latestVersion: manifest.version,
|
|
21515
|
+
reason: `\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF08v${base.currentVersion}\uFF09`
|
|
21516
|
+
};
|
|
21517
|
+
}
|
|
21518
|
+
if (manifest.minimumVersion && compareVersions(base.currentVersion, manifest.minimumVersion) < 0) {
|
|
21519
|
+
return {
|
|
21520
|
+
...base,
|
|
21521
|
+
updateAvailable: false,
|
|
21522
|
+
latestVersion: manifest.version,
|
|
21523
|
+
manualUpgradeRequired: manifest.minimumVersion,
|
|
21524
|
+
reason: `\u5F53\u524D\u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u624B\u52A8\u5347\u7EA7\u5230 >= ${manifest.minimumVersion}`
|
|
21525
|
+
};
|
|
21526
|
+
}
|
|
21527
|
+
return {
|
|
21528
|
+
...base,
|
|
21529
|
+
updateAvailable: true,
|
|
21530
|
+
latestVersion: manifest.version,
|
|
21531
|
+
...manifest.notes ? { notes: manifest.notes } : {}
|
|
21532
|
+
};
|
|
21533
|
+
}
|
|
20615
21534
|
var UpdateController = class {
|
|
20616
21535
|
constructor(opts, currentVersion2 = currentVersion()) {
|
|
20617
21536
|
this.opts = opts;
|
|
@@ -20700,6 +21619,77 @@ var UpdateController = class {
|
|
|
20700
21619
|
}
|
|
20701
21620
|
this.schedule(nextIntervalMs());
|
|
20702
21621
|
}
|
|
21622
|
+
/**
|
|
21623
|
+
* 只读检查:拉清单比对版本,不下载、不改 state。
|
|
21624
|
+
*
|
|
21625
|
+
* 供远端 `self_update --action check` 与 UI 展示用;与定时器 `check()` 不同,
|
|
21626
|
+
* 这里不加 `checking` 锁、不写 `lastCheckAt`,因此不会干扰自动更新流程。
|
|
21627
|
+
*/
|
|
21628
|
+
async inspect() {
|
|
21629
|
+
const base = {
|
|
21630
|
+
currentVersion: this.currentVersion,
|
|
21631
|
+
channel: updaterChannel(),
|
|
21632
|
+
manifestUrl: this.manifestUrl()
|
|
21633
|
+
};
|
|
21634
|
+
const key = this.publicKey;
|
|
21635
|
+
const cap = selfUpdateCapability(Boolean(key));
|
|
21636
|
+
if (!cap.ok) {
|
|
21637
|
+
return { ...base, updateAvailable: false, reason: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
21638
|
+
}
|
|
21639
|
+
const res = await fetchManifest(base.manifestUrl, key, {
|
|
21640
|
+
allowInsecureUrl: env("ZQAGENT_UPDATE_INSECURE") === "1"
|
|
21641
|
+
});
|
|
21642
|
+
if (!res.manifest) {
|
|
21643
|
+
return {
|
|
21644
|
+
...base,
|
|
21645
|
+
updateAvailable: false,
|
|
21646
|
+
reason: `\u66F4\u65B0\u68C0\u67E5\u5931\u8D25\uFF1A${res.error ?? "\u6E05\u5355\u6821\u9A8C\u5931\u8D25"}`
|
|
21647
|
+
};
|
|
21648
|
+
}
|
|
21649
|
+
return evaluateManifest(base, res.manifest);
|
|
21650
|
+
}
|
|
21651
|
+
/**
|
|
21652
|
+
* 远端触发安装:先同步给出结论(受理/无需更新),安装本身在后台跑。
|
|
21653
|
+
*
|
|
21654
|
+
* 必须「先回执、后安装」:安装成功后进程会 `exit(70)` 交给 systemd 拉起新版本,
|
|
21655
|
+
* 如果等到安装完成再回执,回执永远发不出去(表现为网关超时)。
|
|
21656
|
+
*
|
|
21657
|
+
* 复用定时器同一条 `check()` 路径:验签、下载、自检、失败计数、安装后自检与
|
|
21658
|
+
* 回滚保护全都一致,只是把 `installMode` 设为 `force` 时不等待空闲。
|
|
21659
|
+
*/
|
|
21660
|
+
async requestInstall(force = false) {
|
|
21661
|
+
const fromVersion = this.currentVersion;
|
|
21662
|
+
const cap = selfUpdateCapability(Boolean(this.publicKey));
|
|
21663
|
+
if (!cap.ok) {
|
|
21664
|
+
return { accepted: false, fromVersion, message: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
21665
|
+
}
|
|
21666
|
+
const info = await this.inspect();
|
|
21667
|
+
if (!info.updateAvailable) {
|
|
21668
|
+
return {
|
|
21669
|
+
accepted: false,
|
|
21670
|
+
fromVersion,
|
|
21671
|
+
...info.latestVersion ? { toVersion: info.latestVersion } : {},
|
|
21672
|
+
message: info.reason ?? "\u5DF2\u662F\u6700\u65B0\u7248\u672C"
|
|
21673
|
+
};
|
|
21674
|
+
}
|
|
21675
|
+
if (this.checking || this.installing) {
|
|
21676
|
+
return {
|
|
21677
|
+
accepted: true,
|
|
21678
|
+
fromVersion,
|
|
21679
|
+
toVersion: info.latestVersion,
|
|
21680
|
+
message: `\u5DF2\u6709\u66F4\u65B0\u4EFB\u52A1\u5728\u8FDB\u884C\u4E2D\uFF08\u76EE\u6807 v${info.latestVersion}\uFF09\uFF0C\u53EF\u5728\u7A0D\u540E\u7528 status \u67E5\u770B\u7ED3\u679C`
|
|
21681
|
+
};
|
|
21682
|
+
}
|
|
21683
|
+
void this.check(force, false, true).catch((err) => {
|
|
21684
|
+
this.recordFailure(info.latestVersion, `\u5B89\u88C5\u5931\u8D25\uFF1A${err.message}`);
|
|
21685
|
+
});
|
|
21686
|
+
return {
|
|
21687
|
+
accepted: true,
|
|
21688
|
+
fromVersion,
|
|
21689
|
+
toVersion: info.latestVersion,
|
|
21690
|
+
message: force ? `\u5F00\u59CB\u5B89\u88C5 v${info.latestVersion}\uFF08\u7ACB\u5373\u91CD\u542F\uFF09` : `\u5F00\u59CB\u5B89\u88C5 v${info.latestVersion}\uFF08\u7B49\u5F85\u8282\u70B9\u7A7A\u95F2\u540E\u91CD\u542F\uFF09`
|
|
21691
|
+
};
|
|
21692
|
+
}
|
|
20703
21693
|
/**
|
|
20704
21694
|
* 检查并准备更新;`installMode` 为 true 时下载后立即安装(不再等空闲)。
|
|
20705
21695
|
* 返回可读结论,供 CLI `update --check` 使用。
|
|
@@ -21099,6 +22089,7 @@ function startServer(opts = {}) {
|
|
|
21099
22089
|
const notificationHub = createNotificationHub();
|
|
21100
22090
|
let fleet = null;
|
|
21101
22091
|
let fleetCred = null;
|
|
22092
|
+
let updater = null;
|
|
21102
22093
|
function connectFleet(cred) {
|
|
21103
22094
|
const localId = ensureNodeId(store);
|
|
21104
22095
|
const nodeId = (cred.nodeId ?? "").trim() || localId;
|
|
@@ -21116,6 +22107,8 @@ function startServer(opts = {}) {
|
|
|
21116
22107
|
deps: {
|
|
21117
22108
|
store,
|
|
21118
22109
|
manager,
|
|
22110
|
+
// 闭包推迟求值:桥先于 UpdateController 创建,指令到达时 updater 已就绪
|
|
22111
|
+
getUpdater: () => updater,
|
|
21119
22112
|
applyFleetCred: async (cred2) => {
|
|
21120
22113
|
store.saveFleetCredential(cred2);
|
|
21121
22114
|
setTimeout(() => connectFleet(cred2), 300);
|
|
@@ -21266,16 +22259,21 @@ function startServer(opts = {}) {
|
|
|
21266
22259
|
scheduleFleetInfoRefresh();
|
|
21267
22260
|
}
|
|
21268
22261
|
});
|
|
22262
|
+
updater = new UpdateController({
|
|
22263
|
+
dataDir,
|
|
22264
|
+
isIdle: () => manager.isIdle(),
|
|
22265
|
+
prepareExit: closeServer
|
|
22266
|
+
});
|
|
21269
22267
|
const server = createApp(store, manager, {
|
|
21270
22268
|
secretBox,
|
|
21271
22269
|
notificationHub,
|
|
21272
22270
|
qq,
|
|
21273
22271
|
adminKey,
|
|
21274
22272
|
fleet: fleetController,
|
|
21275
|
-
host
|
|
22273
|
+
host,
|
|
22274
|
+
updater
|
|
21276
22275
|
});
|
|
21277
22276
|
let closed = false;
|
|
21278
|
-
let updater = null;
|
|
21279
22277
|
function closeServer() {
|
|
21280
22278
|
if (closed) return;
|
|
21281
22279
|
closed = true;
|
|
@@ -21308,13 +22306,6 @@ function startServer(opts = {}) {
|
|
|
21308
22306
|
}
|
|
21309
22307
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
21310
22308
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
21311
|
-
if (opts.autoUpdate) {
|
|
21312
|
-
updater = new UpdateController({
|
|
21313
|
-
dataDir,
|
|
21314
|
-
isIdle: () => manager.isIdle(),
|
|
21315
|
-
prepareExit: closeServer
|
|
21316
|
-
});
|
|
21317
|
-
}
|
|
21318
22309
|
server.timeout = 0;
|
|
21319
22310
|
server.keepAliveTimeout = 65e3;
|
|
21320
22311
|
server.headersTimeout = 66e3;
|
|
@@ -21334,7 +22325,7 @@ function startServer(opts = {}) {
|
|
|
21334
22325
|
} else {
|
|
21335
22326
|
printReady(info, qq);
|
|
21336
22327
|
}
|
|
21337
|
-
updater?.start();
|
|
22328
|
+
if (opts.autoUpdate) updater?.start();
|
|
21338
22329
|
resolve25(info);
|
|
21339
22330
|
}, reject);
|
|
21340
22331
|
});
|
|
@@ -22019,7 +23010,7 @@ function cycleMode(engine) {
|
|
|
22019
23010
|
engine.permissions.mode = next;
|
|
22020
23011
|
return next;
|
|
22021
23012
|
}
|
|
22022
|
-
var rl =
|
|
23013
|
+
var rl = readline4.createInterface({
|
|
22023
23014
|
input: process.stdin,
|
|
22024
23015
|
output: process.stdout
|
|
22025
23016
|
});
|
|
@@ -22499,7 +23490,7 @@ async function offerTerminalSetup(engine, canUseAgent) {
|
|
|
22499
23490
|
}
|
|
22500
23491
|
var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["link", "serve", "update", "rename"]);
|
|
22501
23492
|
function cliVersion() {
|
|
22502
|
-
const env3 = "0.2.
|
|
23493
|
+
const env3 = "0.2.16"?.trim();
|
|
22503
23494
|
if (env3) return env3;
|
|
22504
23495
|
try {
|
|
22505
23496
|
const req2 = createRequire4(import.meta.url);
|
|
@@ -22762,8 +23753,8 @@ async function main() {
|
|
|
22762
23753
|
|
|
22763
23754
|
\u{1F4CB} \u4F1A\u8BDDID: ${SESSION_ID}`);
|
|
22764
23755
|
cliLog(`\u{1F4BE} \u72B6\u6001\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u901A\u8FC7\u4EE5\u4E0B\u547D\u4EE4\u6062\u590D:`);
|
|
22765
|
-
cliLog(`
|
|
22766
|
-
cliLog(` \u6216
|
|
23756
|
+
cliLog(` zqagent --resume ${SESSION_ID}`);
|
|
23757
|
+
cliLog(` \u6216 zqagent --r ${SESSION_ID}
|
|
22767
23758
|
`);
|
|
22768
23759
|
rl.close();
|
|
22769
23760
|
process.exit(0);
|