@zqagent/core 0.2.14 → 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/server/index.js
CHANGED
|
@@ -563,6 +563,7 @@ init_esm_shims();
|
|
|
563
563
|
import * as fs3 from "fs";
|
|
564
564
|
import * as fsp from "fs/promises";
|
|
565
565
|
import * as path5 from "path";
|
|
566
|
+
import * as readline from "readline";
|
|
566
567
|
import { randomUUID } from "crypto";
|
|
567
568
|
|
|
568
569
|
// src/server/dbgTrace.ts
|
|
@@ -1480,6 +1481,170 @@ var Store = class {
|
|
|
1480
1481
|
}
|
|
1481
1482
|
}
|
|
1482
1483
|
}
|
|
1484
|
+
/**
|
|
1485
|
+
* 读取事件「一页」:从文件末尾向前,按字节预算取最近的事件。
|
|
1486
|
+
*
|
|
1487
|
+
* - `before` 省略:取最新的 maxBytes 字节
|
|
1488
|
+
* - `before` 指定:只取 seq < before 的事件(向前翻页)
|
|
1489
|
+
*
|
|
1490
|
+
* 为什么不能复用 loadEventsTail:后者只能从文件末尾读固定窗口,
|
|
1491
|
+
* 无法「向前翻页」,而控制台需要滚到顶部继续加载更早的对话。
|
|
1492
|
+
* 这里按块从后往前扫,读到预算或文件头即停;翻了多页之后每次
|
|
1493
|
+
* before 都紧邻上一页,实际只需读 1~2 块。
|
|
1494
|
+
*
|
|
1495
|
+
* 返回按 seq 升序;`hasMore` 表示本页最早一条之前还有更早的事件。
|
|
1496
|
+
*/
|
|
1497
|
+
loadEventPage(tenantId, sessionId, opts) {
|
|
1498
|
+
const p = path5.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
1499
|
+
const { before, maxBytes } = opts;
|
|
1500
|
+
const __t0 = dbgNow();
|
|
1501
|
+
let fd = null;
|
|
1502
|
+
try {
|
|
1503
|
+
const size = fs3.statSync(p).size;
|
|
1504
|
+
if (size === 0) return { events: [], hasMore: false };
|
|
1505
|
+
fd = fs3.openSync(p, "r");
|
|
1506
|
+
const CHUNK = 256 * 1024;
|
|
1507
|
+
let pos = size;
|
|
1508
|
+
const kept = [];
|
|
1509
|
+
let bytes = 0;
|
|
1510
|
+
let hasMore = false;
|
|
1511
|
+
let done = false;
|
|
1512
|
+
while (pos > 0 && !done) {
|
|
1513
|
+
const start = Math.max(0, pos - CHUNK);
|
|
1514
|
+
const len = pos - start;
|
|
1515
|
+
const buf = Buffer.allocUnsafe(len);
|
|
1516
|
+
const read = fs3.readSync(fd, buf, 0, len, start);
|
|
1517
|
+
const rows = buf.subarray(0, read).toString("utf-8").split("\n");
|
|
1518
|
+
if (start > 0) rows.shift();
|
|
1519
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
1520
|
+
const line = rows[i];
|
|
1521
|
+
if (!line) continue;
|
|
1522
|
+
let entry;
|
|
1523
|
+
try {
|
|
1524
|
+
entry = JSON.parse(line);
|
|
1525
|
+
} catch {
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
if (!Number.isFinite(Number(entry.seq))) continue;
|
|
1529
|
+
if (before !== void 0 && Number(entry.seq) >= before) continue;
|
|
1530
|
+
const sz = Buffer.byteLength(line, "utf-8") + 1;
|
|
1531
|
+
if (bytes > 0 && bytes + sz > maxBytes) {
|
|
1532
|
+
hasMore = true;
|
|
1533
|
+
done = true;
|
|
1534
|
+
break;
|
|
1535
|
+
}
|
|
1536
|
+
bytes += sz;
|
|
1537
|
+
kept.unshift(entry);
|
|
1538
|
+
}
|
|
1539
|
+
pos = start;
|
|
1540
|
+
}
|
|
1541
|
+
dbgMs(
|
|
1542
|
+
"store.loadEventPage",
|
|
1543
|
+
__t0,
|
|
1544
|
+
// [dbg]
|
|
1545
|
+
`size=${size} before=${before ?? "-"} out=${kept.length} bytes\u2248${bytes} more=${hasMore}`
|
|
1546
|
+
);
|
|
1547
|
+
return { events: kept, hasMore };
|
|
1548
|
+
} catch {
|
|
1549
|
+
return { events: [], hasMore: false };
|
|
1550
|
+
} finally {
|
|
1551
|
+
if (fd !== null) {
|
|
1552
|
+
try {
|
|
1553
|
+
fs3.closeSync(fd);
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
// ===================== 轮次索引 =====================
|
|
1560
|
+
/** 轮次索引文件路径 */
|
|
1561
|
+
turnsFile(tenantId, sessionId) {
|
|
1562
|
+
return path5.join(this.sessionDir(tenantId, sessionId), "turns.ndjson");
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* 追加一轮的最新快照(同一 startSeq 后写者胜出)。
|
|
1566
|
+
*
|
|
1567
|
+
* 走与 session.json 相同的每文件写链:索引按「开轮 → 补充消息 → 收轮」
|
|
1568
|
+
* 顺序追加,乱序会让列表读到旧的 running 版本。
|
|
1569
|
+
*/
|
|
1570
|
+
appendTurnIndex(tenantId, sessionId, entry) {
|
|
1571
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
1572
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1573
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
1574
|
+
const line = JSON.stringify(entry) + "\n";
|
|
1575
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
1576
|
+
const next = prev.then(() => fsp.appendFile(file, line, "utf-8")).catch((err) => {
|
|
1577
|
+
console.error(`[store] appendTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
1578
|
+
});
|
|
1579
|
+
this.writeChains.set(file, next);
|
|
1580
|
+
return next;
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* 读取轮次索引(已按 startSeq 去重取最后一版,升序)。
|
|
1584
|
+
*
|
|
1585
|
+
* 文件很小(一轮约 0.5KB,1000 轮 ≈ 500KB),同步读即可,
|
|
1586
|
+
* 会话详情打开路径不会因历史长度变慢。
|
|
1587
|
+
*/
|
|
1588
|
+
loadTurnIndex(tenantId, sessionId) {
|
|
1589
|
+
let raw;
|
|
1590
|
+
try {
|
|
1591
|
+
raw = fs3.readFileSync(this.turnsFile(tenantId, sessionId), "utf-8");
|
|
1592
|
+
} catch {
|
|
1593
|
+
return [];
|
|
1594
|
+
}
|
|
1595
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
1596
|
+
for (const line of raw.split("\n")) {
|
|
1597
|
+
if (!line) continue;
|
|
1598
|
+
try {
|
|
1599
|
+
const e = JSON.parse(line);
|
|
1600
|
+
if (!Number.isFinite(Number(e?.startSeq))) continue;
|
|
1601
|
+
byStart.set(Number(e.startSeq), e);
|
|
1602
|
+
} catch {
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
1606
|
+
}
|
|
1607
|
+
/** 整文件替换轮次索引(重建用,tmp + rename 原子写) */
|
|
1608
|
+
saveTurnIndex(tenantId, sessionId, entries) {
|
|
1609
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
1610
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1611
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
1612
|
+
const snapshot = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
1613
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
1614
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
1615
|
+
const next = prev.then(() => fsp.writeFile(tmp, snapshot, "utf-8")).then(() => fsp.rename(tmp, file)).catch((err) => {
|
|
1616
|
+
console.error(`[store] saveTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
1617
|
+
void fsp.rm(tmp, { force: true });
|
|
1618
|
+
});
|
|
1619
|
+
this.writeChains.set(file, next);
|
|
1620
|
+
return next;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* 流式读取整份事件日志(重建索引用)。
|
|
1624
|
+
*
|
|
1625
|
+
* 不能用 loadEvents(readFileSync + 逐行 JSON.parse):大会话几十 MB,
|
|
1626
|
+
* 会长时间阻塞事件循环,导致 SSE 心跳 / MQTT 保活断开。
|
|
1627
|
+
*/
|
|
1628
|
+
async readEventsAsync(tenantId, sessionId) {
|
|
1629
|
+
const p = path5.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
1630
|
+
const out = [];
|
|
1631
|
+
try {
|
|
1632
|
+
const rl = readline.createInterface({
|
|
1633
|
+
input: fs3.createReadStream(p, { encoding: "utf-8" }),
|
|
1634
|
+
crlfDelay: Infinity
|
|
1635
|
+
});
|
|
1636
|
+
for await (const line of rl) {
|
|
1637
|
+
if (!line) continue;
|
|
1638
|
+
try {
|
|
1639
|
+
out.push(JSON.parse(line));
|
|
1640
|
+
} catch {
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
} catch {
|
|
1644
|
+
return out;
|
|
1645
|
+
}
|
|
1646
|
+
return out;
|
|
1647
|
+
}
|
|
1483
1648
|
// ===================== 节点身份 & 管理员密钥 =====================
|
|
1484
1649
|
/** 节点稳定标识:{root}/node.json(首次启动生成后长期固定) */
|
|
1485
1650
|
loadNodeId() {
|
|
@@ -1900,13 +2065,13 @@ function textOf(content) {
|
|
|
1900
2065
|
}
|
|
1901
2066
|
return content != null ? String(content) : "";
|
|
1902
2067
|
}
|
|
1903
|
-
function extractUserMessages(span,
|
|
2068
|
+
function extractUserMessages(span, clip2 = USER_MESSAGE_CLIP) {
|
|
1904
2069
|
const out = [];
|
|
1905
2070
|
for (const m of span) {
|
|
1906
2071
|
if (m.role !== "user") continue;
|
|
1907
2072
|
const text = textOf(m.content).replace(/\s+/g, " ").trim();
|
|
1908
2073
|
if (!text) continue;
|
|
1909
|
-
out.push(text.length >
|
|
2074
|
+
out.push(text.length > clip2 ? text.slice(0, clip2 - 1) + "\u2026" : text);
|
|
1910
2075
|
}
|
|
1911
2076
|
return out;
|
|
1912
2077
|
}
|
|
@@ -2718,6 +2883,21 @@ var TurnEngine = class _TurnEngine {
|
|
|
2718
2883
|
queueSteering(text) {
|
|
2719
2884
|
this._steering.push(text);
|
|
2720
2885
|
}
|
|
2886
|
+
/**
|
|
2887
|
+
* 撤销一条尚未注入的 steering。
|
|
2888
|
+
*
|
|
2889
|
+
* 按文本删第一条匹配项:调用方(SessionManager)已按索引删掉了自己那份队列,
|
|
2890
|
+
* 这里只需保证引擎不再注入同样的内容。用文本而非索引,是因为引擎会在迭代
|
|
2891
|
+
* 间隙批量清空 `_steering`,索引可能已经漂移;文本相同则删哪条都等价。
|
|
2892
|
+
*
|
|
2893
|
+
* @returns 是否真的删掉了(已注入的项返回 false,调用方据此提示用户)
|
|
2894
|
+
*/
|
|
2895
|
+
removeSteering(text) {
|
|
2896
|
+
const i = this._steering.indexOf(text);
|
|
2897
|
+
if (i < 0) return false;
|
|
2898
|
+
this._steering.splice(i, 1);
|
|
2899
|
+
return true;
|
|
2900
|
+
}
|
|
2721
2901
|
// ===================== 主入口 =====================
|
|
2722
2902
|
/**
|
|
2723
2903
|
* 运行一轮 Turn
|
|
@@ -9259,6 +9439,19 @@ function addAgg(into, tokens, costUsd) {
|
|
|
9259
9439
|
function utcDay(iso) {
|
|
9260
9440
|
return iso.slice(0, 10);
|
|
9261
9441
|
}
|
|
9442
|
+
function sumAgg(values) {
|
|
9443
|
+
const out = zeroAgg();
|
|
9444
|
+
for (const v of values) {
|
|
9445
|
+
out.calls += v.calls;
|
|
9446
|
+
out.input += v.input;
|
|
9447
|
+
out.output += v.output;
|
|
9448
|
+
out.cacheRead += v.cacheRead;
|
|
9449
|
+
out.cacheWrite += v.cacheWrite;
|
|
9450
|
+
out.costUsd += v.costUsd;
|
|
9451
|
+
out.unpriced += v.unpriced;
|
|
9452
|
+
}
|
|
9453
|
+
return out;
|
|
9454
|
+
}
|
|
9262
9455
|
function monthOf(iso) {
|
|
9263
9456
|
return iso.slice(0, 7);
|
|
9264
9457
|
}
|
|
@@ -9534,6 +9727,23 @@ var UsageLedger = class {
|
|
|
9534
9727
|
}
|
|
9535
9728
|
return all;
|
|
9536
9729
|
}
|
|
9730
|
+
/**
|
|
9731
|
+
* 按「项目 × 天」聚合(供控制面把消耗拆到每个项目)。
|
|
9732
|
+
*
|
|
9733
|
+
* 汇总文件里只存了按项目、按天两个独立维度,拿不到交叉项,因此这里从
|
|
9734
|
+
* 账本明细现算——调用方(节点上报)只取最近 40 天,读的是 2 个月文件,
|
|
9735
|
+
* 5 分钟一次的成本可忽略。
|
|
9736
|
+
*/
|
|
9737
|
+
projectByDay(opts) {
|
|
9738
|
+
const out = {};
|
|
9739
|
+
for (const rec of this.allRecords({ from: opts.from, to: opts.to })) {
|
|
9740
|
+
const day = utcDay(rec.ts);
|
|
9741
|
+
const days = out[rec.projectId] ?? (out[rec.projectId] = {});
|
|
9742
|
+
const agg = days[day] ?? (days[day] = zeroAgg());
|
|
9743
|
+
addAgg(agg, rec, rec.costUsd);
|
|
9744
|
+
}
|
|
9745
|
+
return out;
|
|
9746
|
+
}
|
|
9537
9747
|
summary(opts) {
|
|
9538
9748
|
this.ensureInit();
|
|
9539
9749
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
@@ -9833,6 +10043,141 @@ var EVENT_BUFFER_MAX = 2e3;
|
|
|
9833
10043
|
var EVENTS_ACK_MAX_BYTES = 512 * 1024;
|
|
9834
10044
|
var APPROVAL_TIMEOUT_MS = (Number(process.env.APPROVAL_TIMEOUT_SECONDS) || 15 * 60) * 1e3;
|
|
9835
10045
|
var LAST_USER_MESSAGE_MAX = 200;
|
|
10046
|
+
var TURN_TEXT_MAX = 500;
|
|
10047
|
+
var TURN_REPLY_MAX_BYTES = 256 * 1024;
|
|
10048
|
+
var TURN_REPLY_MAX_TEXT = 256 * 1024;
|
|
10049
|
+
var TURNS_DEFAULT_LIMIT = 20;
|
|
10050
|
+
function clip(text, max = LAST_USER_MESSAGE_MAX) {
|
|
10051
|
+
const s = text ?? "";
|
|
10052
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
10053
|
+
}
|
|
10054
|
+
function asNum(v) {
|
|
10055
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
10056
|
+
}
|
|
10057
|
+
function newTurnEntry(ev, input) {
|
|
10058
|
+
return {
|
|
10059
|
+
v: 1,
|
|
10060
|
+
startSeq: ev.seq,
|
|
10061
|
+
endSeq: ev.seq,
|
|
10062
|
+
startedAt: ev.ts,
|
|
10063
|
+
endedAt: null,
|
|
10064
|
+
status: "running",
|
|
10065
|
+
inputs: [{ seq: ev.seq, ts: ev.ts, text: clip(input, TURN_TEXT_MAX) }],
|
|
10066
|
+
replyPreview: "",
|
|
10067
|
+
producedFiles: [],
|
|
10068
|
+
llmCalls: 0,
|
|
10069
|
+
toolCalls: 0,
|
|
10070
|
+
tokensIn: 0,
|
|
10071
|
+
tokensOut: 0
|
|
10072
|
+
};
|
|
10073
|
+
}
|
|
10074
|
+
function applyTurnEvent(turn, ev, toolSeen) {
|
|
10075
|
+
const d = ev.data ?? {};
|
|
10076
|
+
turn.endSeq = ev.seq;
|
|
10077
|
+
switch (ev.type) {
|
|
10078
|
+
case "queue_steering": {
|
|
10079
|
+
turn.inputs.push({
|
|
10080
|
+
seq: ev.seq,
|
|
10081
|
+
ts: ev.ts,
|
|
10082
|
+
text: clip(String(d["text"] ?? ""), TURN_TEXT_MAX)
|
|
10083
|
+
});
|
|
10084
|
+
break;
|
|
10085
|
+
}
|
|
10086
|
+
case "assistant_message": {
|
|
10087
|
+
turn.llmCalls += 1;
|
|
10088
|
+
const text = String(d["text"] ?? "");
|
|
10089
|
+
if (text) {
|
|
10090
|
+
turn.replyPreview = clip(text);
|
|
10091
|
+
turn.replySeq = ev.seq;
|
|
10092
|
+
}
|
|
10093
|
+
const u = d["usage"];
|
|
10094
|
+
if (u && typeof u === "object") {
|
|
10095
|
+
const usage = u;
|
|
10096
|
+
turn.tokensIn += asNum(usage["input"]);
|
|
10097
|
+
turn.tokensOut += asNum(usage["output"]);
|
|
10098
|
+
}
|
|
10099
|
+
break;
|
|
10100
|
+
}
|
|
10101
|
+
case "tool_started":
|
|
10102
|
+
case "tool_finished": {
|
|
10103
|
+
const key = String(d["reqId"] ?? "") || `${String(d["name"] ?? "")}:${ev.seq}`;
|
|
10104
|
+
if (!toolSeen.has(key)) {
|
|
10105
|
+
toolSeen.add(key);
|
|
10106
|
+
turn.toolCalls += 1;
|
|
10107
|
+
}
|
|
10108
|
+
break;
|
|
10109
|
+
}
|
|
10110
|
+
case "error": {
|
|
10111
|
+
turn.error = clip(String(d["message"] ?? d["error"] ?? ""));
|
|
10112
|
+
break;
|
|
10113
|
+
}
|
|
10114
|
+
default:
|
|
10115
|
+
break;
|
|
10116
|
+
}
|
|
10117
|
+
}
|
|
10118
|
+
function closeTurnEntry(turn, ev) {
|
|
10119
|
+
const d = ev.data ?? {};
|
|
10120
|
+
turn.endSeq = ev.seq;
|
|
10121
|
+
turn.endedAt = ev.ts;
|
|
10122
|
+
const status = String(d["status"] ?? "");
|
|
10123
|
+
turn.status = ev.type === "interrupted" ? status || "interrupted" : status || "completed";
|
|
10124
|
+
const files = d["produced_files"];
|
|
10125
|
+
if (Array.isArray(files)) {
|
|
10126
|
+
turn.producedFiles = files.map((x) => String(x ?? "")).filter(Boolean);
|
|
10127
|
+
}
|
|
10128
|
+
if (typeof d["iterations"] === "number") turn.iterations = d["iterations"];
|
|
10129
|
+
const startMs = Date.parse(turn.startedAt);
|
|
10130
|
+
const endMs = Date.parse(ev.ts);
|
|
10131
|
+
if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
|
|
10132
|
+
turn.durationMs = endMs - startMs;
|
|
10133
|
+
}
|
|
10134
|
+
}
|
|
10135
|
+
function buildTurnIndex(events) {
|
|
10136
|
+
const out = [];
|
|
10137
|
+
let cur = null;
|
|
10138
|
+
let toolSeen = /* @__PURE__ */ new Set();
|
|
10139
|
+
for (const ev of events) {
|
|
10140
|
+
if (ev.type === "turn_start") {
|
|
10141
|
+
cur = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
10142
|
+
toolSeen = /* @__PURE__ */ new Set();
|
|
10143
|
+
out.push(cur);
|
|
10144
|
+
continue;
|
|
10145
|
+
}
|
|
10146
|
+
if (!cur) continue;
|
|
10147
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
10148
|
+
closeTurnEntry(cur, ev);
|
|
10149
|
+
cur = null;
|
|
10150
|
+
continue;
|
|
10151
|
+
}
|
|
10152
|
+
applyTurnEvent(cur, ev, toolSeen);
|
|
10153
|
+
}
|
|
10154
|
+
for (const t of out) {
|
|
10155
|
+
if (!t.endedAt && t.error) t.status = "error";
|
|
10156
|
+
}
|
|
10157
|
+
return out;
|
|
10158
|
+
}
|
|
10159
|
+
function summaryFromEntry(t) {
|
|
10160
|
+
return {
|
|
10161
|
+
startSeq: t.startSeq,
|
|
10162
|
+
endSeq: t.endSeq,
|
|
10163
|
+
startedAt: t.startedAt,
|
|
10164
|
+
endedAt: t.endedAt,
|
|
10165
|
+
status: t.status,
|
|
10166
|
+
input: t.inputs[0]?.text ?? "",
|
|
10167
|
+
reply: t.replyPreview,
|
|
10168
|
+
llmCalls: t.llmCalls,
|
|
10169
|
+
toolCalls: t.toolCalls,
|
|
10170
|
+
tokensIn: t.tokensIn,
|
|
10171
|
+
tokensOut: t.tokensOut,
|
|
10172
|
+
iterations: t.iterations,
|
|
10173
|
+
durationMs: t.durationMs,
|
|
10174
|
+
error: t.error,
|
|
10175
|
+
inputs: t.inputs,
|
|
10176
|
+
userCount: t.inputs.length,
|
|
10177
|
+
replySeq: t.replySeq,
|
|
10178
|
+
producedFiles: t.producedFiles
|
|
10179
|
+
};
|
|
10180
|
+
}
|
|
9836
10181
|
function isAffirmative(answer) {
|
|
9837
10182
|
const a = answer.trim().toLowerCase();
|
|
9838
10183
|
if (!a) return false;
|
|
@@ -9857,6 +10202,10 @@ var SessionManager = class {
|
|
|
9857
10202
|
opts;
|
|
9858
10203
|
runtimes = /* @__PURE__ */ new Map();
|
|
9859
10204
|
// sessionId → runtime
|
|
10205
|
+
/** 轮次索引的内存副本(运行中的轮比磁盘索引新;重启后为空) */
|
|
10206
|
+
turnIndexCache = /* @__PURE__ */ new Map();
|
|
10207
|
+
/** 进行中的索引重建任务(并发请求合并到同一个) */
|
|
10208
|
+
turnIndexBuilds = /* @__PURE__ */ new Map();
|
|
9860
10209
|
/** 全局通知 SSE 订阅者(控制台通知中心) */
|
|
9861
10210
|
notificationSubs = /* @__PURE__ */ new Set();
|
|
9862
10211
|
/**
|
|
@@ -9911,6 +10260,17 @@ var SessionManager = class {
|
|
|
9911
10260
|
rt.status = status;
|
|
9912
10261
|
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
9913
10262
|
}
|
|
10263
|
+
/**
|
|
10264
|
+
* 通知里的「最近一条用户消息」:截断到 LAST_USER_MESSAGE_MAX。
|
|
10265
|
+
*
|
|
10266
|
+
* 三处共用(session_status / turn_completed / turn_stopped),避免各自写一遍
|
|
10267
|
+
* 截断规则后长度不一致——控制台弹窗与任务列表会显示同一句话的两个版本。
|
|
10268
|
+
*/
|
|
10269
|
+
_lastMessage(rt) {
|
|
10270
|
+
const text = (rt.lastInput ?? "").trim();
|
|
10271
|
+
if (!text) return null;
|
|
10272
|
+
return text.length > LAST_USER_MESSAGE_MAX ? `${text.slice(0, LAST_USER_MESSAGE_MAX)}\u2026` : text;
|
|
10273
|
+
}
|
|
9914
10274
|
/**
|
|
9915
10275
|
* session_status 通知载荷:状态 + 最近一条用户消息(截断)。
|
|
9916
10276
|
*
|
|
@@ -9919,10 +10279,24 @@ var SessionManager = class {
|
|
|
9919
10279
|
*/
|
|
9920
10280
|
_statusData(rt) {
|
|
9921
10281
|
const data = { status: rt.status };
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
10282
|
+
if (rt.lastStartedAt) data.lastStartedAt = rt.lastStartedAt;
|
|
10283
|
+
const msg = this._lastMessage(rt);
|
|
10284
|
+
if (msg) data.lastUserMessage = msg;
|
|
10285
|
+
return data;
|
|
10286
|
+
}
|
|
10287
|
+
/**
|
|
10288
|
+
* 任务结束类通知(完成 / 达到迭代上限)的载荷:在 turn_end 数据之外补上
|
|
10289
|
+
* 「会话名 + 最后一条用户消息」。
|
|
10290
|
+
*
|
|
10291
|
+
* 控制台右下角的完成弹窗要说明「哪个会话、用户让它做了什么」。这些字段
|
|
10292
|
+
* 以节点侧为准随通知一起下发,前端就不必依赖快照(nodeInfo / session_status)
|
|
10293
|
+
* 是否已经到达——刚打开页面或通知乱序时才不会退化成只剩一句「任务已完成」。
|
|
10294
|
+
*/
|
|
10295
|
+
_turnEndData(rt, eventData, extra) {
|
|
10296
|
+
const data = { ...eventData, ...extra };
|
|
10297
|
+
if (rt.name) data.sessionName = rt.name;
|
|
10298
|
+
const msg = this._lastMessage(rt);
|
|
10299
|
+
if (msg) data.lastUserMessage = msg;
|
|
9926
10300
|
return data;
|
|
9927
10301
|
}
|
|
9928
10302
|
/** 触发通知回调(无 handler 时为空操作,异常隔离不冒泡) */
|
|
@@ -10079,6 +10453,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10079
10453
|
this.runtimes.delete(sessionId);
|
|
10080
10454
|
}
|
|
10081
10455
|
this.store.deleteSession(tenantId, sessionId);
|
|
10456
|
+
this.turnIndexCache.delete(sessionId);
|
|
10082
10457
|
this._publishNotify(tenantId, {
|
|
10083
10458
|
kind: "session_status",
|
|
10084
10459
|
sessionId,
|
|
@@ -10122,6 +10497,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10122
10497
|
throw new ApiError(409, "busy", "\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D");
|
|
10123
10498
|
}
|
|
10124
10499
|
rt.engine.queueSteering(input);
|
|
10500
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
10125
10501
|
rt.pendingSteers.push(input);
|
|
10126
10502
|
this._recordLastInput(rt, input);
|
|
10127
10503
|
return "steered";
|
|
@@ -10154,9 +10530,42 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10154
10530
|
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
10155
10531
|
if (!rt.engine) throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
10156
10532
|
rt.engine.queueSteering(text);
|
|
10533
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
10157
10534
|
rt.pendingSteers.push(text);
|
|
10158
10535
|
this._recordLastInput(rt, text);
|
|
10159
10536
|
}
|
|
10537
|
+
/**
|
|
10538
|
+
* 撤销一条尚未注入的补充信息(用户反悔)。
|
|
10539
|
+
*
|
|
10540
|
+
* 队列里的项还没进聊天流,此时只能靠中断整个任务来「收回」代价太大。
|
|
10541
|
+
* index 取自 GET /state 的 pendingSteers(与前端展示顺序一致),两侧同时删:
|
|
10542
|
+
* 只删前端那份,引擎下一迭代仍会把它注入对话;只删引擎那份,队列里会残留
|
|
10543
|
+
* 一条永远不注入的条目。
|
|
10544
|
+
*
|
|
10545
|
+
* 若撤的正是「最近用户指令」,把它还原成队列里的上一条、或本轮被覆盖前的
|
|
10546
|
+
* 指令——否则工作台还会显示这条已经被取消的内容。
|
|
10547
|
+
*
|
|
10548
|
+
* @returns 剩余待注入队列(前端据此刷新提示区)
|
|
10549
|
+
*/
|
|
10550
|
+
unsteer(tenant, sessionId, index) {
|
|
10551
|
+
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
10552
|
+
if (!rt.engine || !rt.runTask) {
|
|
10553
|
+
throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
10554
|
+
}
|
|
10555
|
+
if (!Number.isInteger(index) || index < 0 || index >= rt.pendingSteers.length) {
|
|
10556
|
+
throw new ApiError(409, "already_injected", "\u8FD9\u6761\u8865\u5145\u4FE1\u606F\u5DF2\u8FDB\u5165\u5BF9\u8BDD\uFF0C\u65E0\u6CD5\u64A4\u9500");
|
|
10557
|
+
}
|
|
10558
|
+
const [removed] = rt.pendingSteers.splice(index, 1);
|
|
10559
|
+
if (removed !== void 0) {
|
|
10560
|
+
rt.engine.removeSteering(removed);
|
|
10561
|
+
if (removed === rt.lastInput) {
|
|
10562
|
+
rt.lastInput = rt.pendingSteers.length > 0 ? rt.pendingSteers[rt.pendingSteers.length - 1] : rt.steerBaseInput;
|
|
10563
|
+
this._persist(rt);
|
|
10564
|
+
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
10565
|
+
}
|
|
10566
|
+
}
|
|
10567
|
+
return [...rt.pendingSteers];
|
|
10568
|
+
}
|
|
10160
10569
|
/**
|
|
10161
10570
|
* 已提交、尚未注入当前任务的补充信息。
|
|
10162
10571
|
*
|
|
@@ -10589,44 +10998,214 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10589
10998
|
status: rt.status
|
|
10590
10999
|
};
|
|
10591
11000
|
}
|
|
10592
|
-
/**
|
|
10593
|
-
|
|
11001
|
+
/**
|
|
11002
|
+
* 事件分页(向前 / 向后两种方向)。
|
|
11003
|
+
*
|
|
11004
|
+
* - `since`:取 seq > since(实时增量 / 重连补齐)
|
|
11005
|
+
* - `before`:取 seq < before(控制台滚到顶部加载更早的对话)
|
|
11006
|
+
* - `maxBytes`:单次回执的字节预算(默认沿用 EVENTS_ACK_MAX_BYTES)
|
|
11007
|
+
*
|
|
11008
|
+
* 两者同时给出时按 `since`(向后)处理。`hasMore` 只对向前翻页有意义:
|
|
11009
|
+
* 表示本页最早一条之前还有更早的事件。
|
|
11010
|
+
*
|
|
11011
|
+
* 向前翻页一律走磁盘:内存缓冲只保留最近 EVENT_BUFFER_MAX 条,
|
|
11012
|
+
* 更早的事件只在 events.jsonl 里。
|
|
11013
|
+
*/
|
|
11014
|
+
eventsPage(tenantId, sessionId, opts = {}) {
|
|
10594
11015
|
const __t0 = dbgNow();
|
|
11016
|
+
const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : EVENTS_ACK_MAX_BYTES;
|
|
10595
11017
|
const rt = this.runtimes.get(sessionId);
|
|
11018
|
+
if (opts.before !== void 0) {
|
|
11019
|
+
const since2 = opts.since ?? 0;
|
|
11020
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
11021
|
+
before: opts.before,
|
|
11022
|
+
maxBytes
|
|
11023
|
+
});
|
|
11024
|
+
const ev2 = since2 > 0 ? page.events.filter((e) => e.seq > since2) : page.events;
|
|
11025
|
+
dbgMs(
|
|
11026
|
+
"session.eventsPage(before/\u78C1\u76D8)",
|
|
11027
|
+
__t0,
|
|
11028
|
+
// [dbg]
|
|
11029
|
+
`sid=${sessionId.slice(0, 8)} since=${since2} before=${opts.before} out=${ev2.length} more=${page.hasMore}`
|
|
11030
|
+
);
|
|
11031
|
+
return {
|
|
11032
|
+
events: ev2,
|
|
11033
|
+
latestSeq: rt?.seq ?? page.events.at(-1)?.seq ?? 0,
|
|
11034
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
11035
|
+
hasMore: page.hasMore
|
|
11036
|
+
};
|
|
11037
|
+
}
|
|
11038
|
+
const since = opts.since ?? 0;
|
|
10596
11039
|
if (rt) {
|
|
10597
|
-
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since));
|
|
11040
|
+
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since), maxBytes);
|
|
10598
11041
|
dbgMs(
|
|
10599
|
-
"session.
|
|
11042
|
+
"session.eventsPage(\u5185\u5B58)",
|
|
10600
11043
|
__t0,
|
|
10601
11044
|
// [dbg]
|
|
10602
11045
|
`sid=${sessionId.slice(0, 8)} since=${since} in=${rt.events.length} out=${ev2.length}`
|
|
10603
11046
|
);
|
|
10604
|
-
return {
|
|
11047
|
+
return {
|
|
11048
|
+
events: ev2,
|
|
11049
|
+
latestSeq: rt.seq,
|
|
11050
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
11051
|
+
// 内存缓冲被截断(或 since 早于缓冲起点)时,磁盘上可能还有更早的
|
|
11052
|
+
hasMore: !!ev2.length && ev2[0].seq > (rt.events[0]?.seq ?? 0)
|
|
11053
|
+
};
|
|
10605
11054
|
}
|
|
10606
11055
|
const all = this.store.loadEventsTail(tenantId, sessionId, EVENTS_ACK_MAX_BYTES * 2);
|
|
10607
|
-
const ev = this._boundedTail(
|
|
11056
|
+
const ev = this._boundedTail(
|
|
11057
|
+
all.filter((e) => e.seq > since),
|
|
11058
|
+
maxBytes
|
|
11059
|
+
);
|
|
10608
11060
|
dbgMs(
|
|
10609
|
-
"session.
|
|
11061
|
+
"session.eventsPage(\u78C1\u76D8)",
|
|
10610
11062
|
__t0,
|
|
10611
11063
|
// [dbg]
|
|
10612
11064
|
`sid=${sessionId.slice(0, 8)} since=${since} loaded=${all.length} out=${ev.length}`
|
|
10613
11065
|
);
|
|
10614
11066
|
return {
|
|
10615
11067
|
events: ev,
|
|
10616
|
-
latestSeq: all.at(-1)?.seq ?? 0
|
|
11068
|
+
latestSeq: all.at(-1)?.seq ?? 0,
|
|
11069
|
+
earliestSeq: ev[0]?.seq ?? 0,
|
|
11070
|
+
// 读的是文件尾部窗口:窗口已满说明更早的事件还在文件里
|
|
11071
|
+
hasMore: all.length > 0 && ev.length > 0 && ev[0].seq > all[0].seq
|
|
10617
11072
|
};
|
|
10618
11073
|
}
|
|
11074
|
+
/**
|
|
11075
|
+
* 兼容旧调用:只按 since 取增量。
|
|
11076
|
+
* @deprecated 新代码用 eventsPage(支持 before 向前翻页)
|
|
11077
|
+
*/
|
|
11078
|
+
eventsSince(tenantId, sessionId, since) {
|
|
11079
|
+
const r = this.eventsPage(tenantId, sessionId, { since });
|
|
11080
|
+
return { events: r.events, latestSeq: r.latestSeq };
|
|
11081
|
+
}
|
|
11082
|
+
/**
|
|
11083
|
+
* 「按 turn 汇总」的轻量列表:只回标题与统计,点开某一轮再单独拉该轮回复。
|
|
11084
|
+
*
|
|
11085
|
+
* 数据来自 turns.ndjson 索引(派生自事件流),读取成本与会话历史长度无关:
|
|
11086
|
+
* 老会话首次访问时后台/本请求内重建一次,之后每页只读几 KB。
|
|
11087
|
+
*
|
|
11088
|
+
* - `before`:只返回 startSeq < before 的轮次(向前翻页)
|
|
11089
|
+
* - `turns` 按时间升序;`hasMore` 表示更早还有轮次
|
|
11090
|
+
*/
|
|
11091
|
+
async sessionTurns(tenantId, sessionId, opts = {}) {
|
|
11092
|
+
const limit = Math.min(Math.max(opts.limit ?? TURNS_DEFAULT_LIMIT, 1), 100);
|
|
11093
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
11094
|
+
const before = opts.before;
|
|
11095
|
+
const endIdx = before === void 0 ? entries.length : entries.findIndex((t) => t.startSeq >= before);
|
|
11096
|
+
const end = endIdx < 0 ? entries.length : endIdx;
|
|
11097
|
+
const start = Math.max(0, end - limit);
|
|
11098
|
+
const picked = entries.slice(start, end);
|
|
11099
|
+
return {
|
|
11100
|
+
turns: picked.map(summaryFromEntry),
|
|
11101
|
+
hasMore: start > 0,
|
|
11102
|
+
earliestSeq: picked[0]?.startSeq ?? 0,
|
|
11103
|
+
total: entries.length,
|
|
11104
|
+
userTotal: entries.reduce((n, t) => n + t.inputs.length, 0)
|
|
11105
|
+
};
|
|
11106
|
+
}
|
|
11107
|
+
/**
|
|
11108
|
+
* 某一轮的用户消息与最终回复(含产出文件)。
|
|
11109
|
+
*
|
|
11110
|
+
* 只读两条索引记录 + 尾部一个事件页,不会因该轮工具输出很大而变慢:
|
|
11111
|
+
* 回复正文紧邻 turn_end,按 replySeq 精确读即可。
|
|
11112
|
+
*/
|
|
11113
|
+
async sessionTurnReply(tenantId, sessionId, startSeq) {
|
|
11114
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
11115
|
+
const turn = entries.find((t) => t.startSeq === startSeq);
|
|
11116
|
+
if (!turn) throw new ApiError(404, "not_found", "\u8F6E\u6B21\u4E0D\u5B58\u5728");
|
|
11117
|
+
let reply = null;
|
|
11118
|
+
let truncated = false;
|
|
11119
|
+
if (turn.replySeq !== void 0) {
|
|
11120
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
11121
|
+
before: turn.replySeq + 1,
|
|
11122
|
+
maxBytes: TURN_REPLY_MAX_BYTES
|
|
11123
|
+
});
|
|
11124
|
+
const ev = page.events.find((e) => e.seq === turn.replySeq);
|
|
11125
|
+
if (ev && ev.type === "assistant_message") {
|
|
11126
|
+
const text = String(ev.data?.["text"] ?? "");
|
|
11127
|
+
if (text.length > TURN_REPLY_MAX_TEXT) {
|
|
11128
|
+
reply = { seq: ev.seq, ts: ev.ts, text: text.slice(0, TURN_REPLY_MAX_TEXT) };
|
|
11129
|
+
truncated = true;
|
|
11130
|
+
} else {
|
|
11131
|
+
reply = { seq: ev.seq, ts: ev.ts, text };
|
|
11132
|
+
}
|
|
11133
|
+
}
|
|
11134
|
+
}
|
|
11135
|
+
return {
|
|
11136
|
+
startSeq: turn.startSeq,
|
|
11137
|
+
endSeq: turn.endSeq,
|
|
11138
|
+
status: turn.status,
|
|
11139
|
+
startedAt: turn.startedAt,
|
|
11140
|
+
endedAt: turn.endedAt,
|
|
11141
|
+
userMessages: turn.inputs,
|
|
11142
|
+
reply,
|
|
11143
|
+
truncated,
|
|
11144
|
+
producedFiles: turn.producedFiles
|
|
11145
|
+
};
|
|
11146
|
+
}
|
|
11147
|
+
/**
|
|
11148
|
+
* 取轮次索引;缺失时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
11149
|
+
*
|
|
11150
|
+
* 索引是派生数据:损坏/落后都只需重建,不影响会话本身。
|
|
11151
|
+
*/
|
|
11152
|
+
/**
|
|
11153
|
+
* 取轮次索引;缺失/落后时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
11154
|
+
*
|
|
11155
|
+
* 索引是派生数据:损坏或落后都只需重建,不影响会话本身。
|
|
11156
|
+
*/
|
|
11157
|
+
async _ensureTurnIndex(tenantId, sessionId) {
|
|
11158
|
+
let entries = this._mergeTurnIndex(tenantId, sessionId);
|
|
11159
|
+
if (entries.length > 0 && !this.runtimes.has(sessionId)) {
|
|
11160
|
+
const last = entries[entries.length - 1];
|
|
11161
|
+
const tail = this.store.loadEventsTail(tenantId, sessionId, 64 * 1024);
|
|
11162
|
+
const newTurnStarted = tail.some((e) => e.type === "turn_start" && e.seq > last.endSeq);
|
|
11163
|
+
if (newTurnStarted || last.status === "running") entries = [];
|
|
11164
|
+
}
|
|
11165
|
+
if (entries.length > 0) return entries;
|
|
11166
|
+
const pending = this.turnIndexBuilds.get(sessionId);
|
|
11167
|
+
if (pending) return pending;
|
|
11168
|
+
const task = this._rebuildTurnIndex(tenantId, sessionId).finally(() => {
|
|
11169
|
+
this.turnIndexBuilds.delete(sessionId);
|
|
11170
|
+
});
|
|
11171
|
+
this.turnIndexBuilds.set(sessionId, task);
|
|
11172
|
+
return task;
|
|
11173
|
+
}
|
|
11174
|
+
/** 合并磁盘索引与内存索引(内存版本更新,同一 startSeq 以内存为准) */
|
|
11175
|
+
_mergeTurnIndex(tenantId, sessionId) {
|
|
11176
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
11177
|
+
for (const e of this.store.loadTurnIndex(tenantId, sessionId)) byStart.set(e.startSeq, e);
|
|
11178
|
+
for (const e of this.turnIndexCache.get(sessionId) ?? []) byStart.set(e.startSeq, e);
|
|
11179
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
11180
|
+
}
|
|
11181
|
+
/** 从 events.jsonl 顺序流式重建轮次索引(一次性,之后增量维护) */
|
|
11182
|
+
async _rebuildTurnIndex(tenantId, sessionId) {
|
|
11183
|
+
const __t0 = dbgNow();
|
|
11184
|
+
const events = await this.store.readEventsAsync(tenantId, sessionId);
|
|
11185
|
+
const entries = buildTurnIndex(events);
|
|
11186
|
+
if (entries.length > 0) {
|
|
11187
|
+
await this.store.saveTurnIndex(tenantId, sessionId, entries);
|
|
11188
|
+
if (this.runtimes.has(sessionId)) this.turnIndexCache.set(sessionId, entries);
|
|
11189
|
+
}
|
|
11190
|
+
dbgMs(
|
|
11191
|
+
"session._rebuildTurnIndex",
|
|
11192
|
+
__t0,
|
|
11193
|
+
// [dbg]
|
|
11194
|
+
`sid=${sessionId.slice(0, 8)} events=${events.length} turns=${entries.length}`
|
|
11195
|
+
);
|
|
11196
|
+
return entries;
|
|
11197
|
+
}
|
|
10619
11198
|
/**
|
|
10620
11199
|
* 按字节预算截取事件「尾部」(至少保留最后一条),确保 MQTT 回执远小于单包上限。
|
|
10621
11200
|
* 供 get_events 使用:会话历史可任意大,但 MQTT 链路不能承载大包。
|
|
10622
11201
|
*/
|
|
10623
|
-
_boundedTail(events) {
|
|
11202
|
+
_boundedTail(events, maxBytes = EVENTS_ACK_MAX_BYTES) {
|
|
10624
11203
|
const __t0 = dbgNow();
|
|
10625
11204
|
let total = 0;
|
|
10626
11205
|
let i = events.length;
|
|
10627
11206
|
while (i > 0) {
|
|
10628
11207
|
const size = Buffer.byteLength(JSON.stringify(events[i - 1]));
|
|
10629
|
-
if (total > 0 && total + size >
|
|
11208
|
+
if (total > 0 && total + size > maxBytes) break;
|
|
10630
11209
|
total += size;
|
|
10631
11210
|
i -= 1;
|
|
10632
11211
|
}
|
|
@@ -10746,6 +11325,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10746
11325
|
nameSource: rec.nameSource,
|
|
10747
11326
|
taskCount: rec.taskCount ?? 0,
|
|
10748
11327
|
lastCompletedAt: rec.lastCompletedAt,
|
|
11328
|
+
lastStartedAt: rec.lastStartedAt,
|
|
10749
11329
|
naming: false,
|
|
10750
11330
|
turnStartedAt: 0
|
|
10751
11331
|
};
|
|
@@ -10763,10 +11343,11 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10763
11343
|
}
|
|
10764
11344
|
/** 装配引擎并启动一轮 Turn(供 submitTurn / submitOrSteer 复用) */
|
|
10765
11345
|
_startTurn(rt, input) {
|
|
11346
|
+
const engine = this._buildEngine(rt.tenant, rt, true);
|
|
10766
11347
|
rt.lastInput = input;
|
|
10767
|
-
this._setStatus(rt, "running");
|
|
10768
11348
|
rt.turnStartedAt = Date.now();
|
|
10769
|
-
|
|
11349
|
+
rt.lastStartedAt = new Date(rt.turnStartedAt).toISOString();
|
|
11350
|
+
this._setStatus(rt, "running");
|
|
10770
11351
|
rt.engine = engine;
|
|
10771
11352
|
this._persist(rt);
|
|
10772
11353
|
rt.runTask = this._drive(rt, engine.run(input));
|
|
@@ -10825,18 +11406,18 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10825
11406
|
this._setStatus(rt, "completed");
|
|
10826
11407
|
rt.taskCount += 1;
|
|
10827
11408
|
rt.lastCompletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10828
|
-
this._emitNotify(
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
durationMs
|
|
10832
|
-
|
|
11409
|
+
this._emitNotify(
|
|
11410
|
+
rt,
|
|
11411
|
+
"turn_completed",
|
|
11412
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
11413
|
+
);
|
|
10833
11414
|
} else if (status === "max_iterations_exceeded") {
|
|
10834
11415
|
this._setStatus(rt, "stopped");
|
|
10835
|
-
this._emitNotify(
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
durationMs
|
|
10839
|
-
|
|
11416
|
+
this._emitNotify(
|
|
11417
|
+
rt,
|
|
11418
|
+
"turn_stopped",
|
|
11419
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
11420
|
+
);
|
|
10840
11421
|
}
|
|
10841
11422
|
this._persist(rt);
|
|
10842
11423
|
if (status === "completed" && engine && shouldAutoName(rt.taskCount, rt.nameSource)) {
|
|
@@ -10847,7 +11428,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10847
11428
|
case "error" /* ERROR */:
|
|
10848
11429
|
this._setStatus(rt, "error");
|
|
10849
11430
|
this._persist(rt);
|
|
10850
|
-
this._emitNotify(rt, "error", event.data);
|
|
11431
|
+
this._emitNotify(rt, "error", this._turnEndData(rt, event.data, {}));
|
|
10851
11432
|
break;
|
|
10852
11433
|
case "interrupted" /* INTERRUPTED */:
|
|
10853
11434
|
this._setStatus(rt, "interrupted");
|
|
@@ -10873,9 +11454,17 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10873
11454
|
this._setStatus(rt, "error");
|
|
10874
11455
|
this._persist(rt);
|
|
10875
11456
|
} finally {
|
|
11457
|
+
if (rt.turnIndex) {
|
|
11458
|
+
const turn = rt.turnIndex;
|
|
11459
|
+
if (!turn.endedAt && turn.error) turn.status = "error";
|
|
11460
|
+
this._appendTurnIndex(rt, turn);
|
|
11461
|
+
rt.turnIndex = void 0;
|
|
11462
|
+
rt.turnToolKeys = void 0;
|
|
11463
|
+
}
|
|
10876
11464
|
rt.runTask = null;
|
|
10877
11465
|
rt.engine = null;
|
|
10878
11466
|
rt.pendingSteers = [];
|
|
11467
|
+
rt.steerBaseInput = void 0;
|
|
10879
11468
|
}
|
|
10880
11469
|
}
|
|
10881
11470
|
_buffer(rt, event) {
|
|
@@ -10888,6 +11477,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10888
11477
|
rt.events.push(entry);
|
|
10889
11478
|
if (rt.events.length > EVENT_BUFFER_MAX) rt.events.shift();
|
|
10890
11479
|
void this.store.appendEvent(rt.tenantId, rt.sessionId, entry);
|
|
11480
|
+
this._trackTurnIndex(rt, entry);
|
|
10891
11481
|
for (const res of rt.subscribers) this._sseSend(res, entry);
|
|
10892
11482
|
this._broadcastSessionEvent(rt.tenantId, rt.sessionId, entry);
|
|
10893
11483
|
try {
|
|
@@ -10895,6 +11485,42 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10895
11485
|
} catch {
|
|
10896
11486
|
}
|
|
10897
11487
|
}
|
|
11488
|
+
/**
|
|
11489
|
+
* 维护运行中轮次的索引(in-memory 记账 + 关键节点落盘)。
|
|
11490
|
+
*
|
|
11491
|
+
* 落盘只在「开轮 / 补充消息 / 收轮」发生,delta 事件仅推进 endSeq 与统计,
|
|
11492
|
+
* 因此索引写入量是每轮 2~3 行,与会话事件量无关。
|
|
11493
|
+
*/
|
|
11494
|
+
_trackTurnIndex(rt, ev) {
|
|
11495
|
+
if (ev.type === "turn_start") {
|
|
11496
|
+
const turn2 = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
11497
|
+
rt.turnIndex = turn2;
|
|
11498
|
+
rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
11499
|
+
this._appendTurnIndex(rt, turn2);
|
|
11500
|
+
return;
|
|
11501
|
+
}
|
|
11502
|
+
const turn = rt.turnIndex;
|
|
11503
|
+
if (!turn) return;
|
|
11504
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
11505
|
+
closeTurnEntry(turn, ev);
|
|
11506
|
+
this._appendTurnIndex(rt, turn);
|
|
11507
|
+
rt.turnIndex = void 0;
|
|
11508
|
+
rt.turnToolKeys = void 0;
|
|
11509
|
+
return;
|
|
11510
|
+
}
|
|
11511
|
+
if (!rt.turnToolKeys) rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
11512
|
+
const inputsBefore = turn.inputs.length;
|
|
11513
|
+
applyTurnEvent(turn, ev, rt.turnToolKeys);
|
|
11514
|
+
if (turn.inputs.length !== inputsBefore) this._appendTurnIndex(rt, turn);
|
|
11515
|
+
}
|
|
11516
|
+
/** 写入一版轮次索引(内存副本 + 异步落盘) */
|
|
11517
|
+
_appendTurnIndex(rt, turn) {
|
|
11518
|
+
const cached3 = this.turnIndexCache.get(rt.sessionId) ?? [];
|
|
11519
|
+
const i = cached3.findIndex((t) => t.startSeq === turn.startSeq);
|
|
11520
|
+
const next = i >= 0 ? [...cached3.slice(0, i), turn, ...cached3.slice(i + 1)] : [...cached3, turn];
|
|
11521
|
+
this.turnIndexCache.set(rt.sessionId, next);
|
|
11522
|
+
void this.store.appendTurnIndex(rt.tenantId, rt.sessionId, turn);
|
|
11523
|
+
}
|
|
10898
11524
|
/** 回收内存 run 任务(释放引用交给 GC;磁盘状态保持不动) */
|
|
10899
11525
|
_recycleRunTask(rt) {
|
|
10900
11526
|
rt.runTask = null;
|
|
@@ -10932,6 +11558,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10932
11558
|
nameSource: rt.nameSource,
|
|
10933
11559
|
taskCount: rt.taskCount,
|
|
10934
11560
|
lastCompletedAt: rt.lastCompletedAt,
|
|
11561
|
+
lastStartedAt: rt.lastStartedAt,
|
|
10935
11562
|
modelOverridden: rt.modelOverridden,
|
|
10936
11563
|
providerId: rt.providerId,
|
|
10937
11564
|
permissionMode: rt.permissionMode,
|
|
@@ -11342,6 +11969,7 @@ function allSessionStates(store, manager) {
|
|
|
11342
11969
|
name: s.name ?? null,
|
|
11343
11970
|
status: s.status,
|
|
11344
11971
|
lastUserMessage: lastUserMessage(s),
|
|
11972
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
11345
11973
|
lastCompletedAt: lastCompletedAt(s)
|
|
11346
11974
|
});
|
|
11347
11975
|
}
|
|
@@ -11396,7 +12024,7 @@ function buildNodeInfo(store, opts) {
|
|
|
11396
12024
|
nodeId: opts.nodeId ?? ensureNodeId(store),
|
|
11397
12025
|
platform: process.platform,
|
|
11398
12026
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
11399
|
-
version: "0.2.
|
|
12027
|
+
version: "0.2.16",
|
|
11400
12028
|
admin: opts.admin,
|
|
11401
12029
|
projects: opts.projects
|
|
11402
12030
|
};
|
|
@@ -11764,6 +12392,7 @@ import * as fs20 from "fs";
|
|
|
11764
12392
|
import * as path24 from "path";
|
|
11765
12393
|
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11766
12394
|
var MAX_DOWNLOAD_BYTES = 16 * 1024 * 1024;
|
|
12395
|
+
var MAX_BINARY_BYTES = 2.5 * 1024 * 1024;
|
|
11767
12396
|
var MAX_ENTRIES2 = 2e3;
|
|
11768
12397
|
function toPosix(p) {
|
|
11769
12398
|
return p.split(path24.sep).join("/");
|
|
@@ -11911,6 +12540,26 @@ function writeProjectFile(workspaceRoot, relPath, content) {
|
|
|
11911
12540
|
}
|
|
11912
12541
|
return { path: toPosix(path24.relative(base, file)), size: buf.length };
|
|
11913
12542
|
}
|
|
12543
|
+
function writeProjectFileBase64(workspaceRoot, relPath, base64) {
|
|
12544
|
+
const base = projectRoot(workspaceRoot);
|
|
12545
|
+
const rel = (relPath ?? "").trim();
|
|
12546
|
+
if (!rel) throw new Error("\u6587\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
|
|
12547
|
+
const file = resolveWithin(base, rel);
|
|
12548
|
+
const b64 = String(base64 ?? "").replace(/\s+/g, "");
|
|
12549
|
+
if (!b64) throw new Error("\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
|
|
12550
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) throw new Error("\u5185\u5BB9\u4E0D\u662F\u5408\u6CD5\u7684 base64");
|
|
12551
|
+
if (Math.floor(b64.length * 3 / 4) > MAX_BINARY_BYTES) {
|
|
12552
|
+
throw new Error(`\u5185\u5BB9\u8D85\u8FC7 ${MAX_BINARY_BYTES / (1024 * 1024)}MB \u4E0A\u9650\uFF0C\u65E0\u6CD5\u4FDD\u5B58`);
|
|
12553
|
+
}
|
|
12554
|
+
const buf = Buffer.from(b64, "base64");
|
|
12555
|
+
try {
|
|
12556
|
+
fs20.mkdirSync(path24.dirname(file), { recursive: true });
|
|
12557
|
+
fs20.writeFileSync(file, buf);
|
|
12558
|
+
} catch (e) {
|
|
12559
|
+
throw new Error(`\u5199\u5165\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
12560
|
+
}
|
|
12561
|
+
return { path: toPosix(path24.relative(base, file)), size: buf.length };
|
|
12562
|
+
}
|
|
11914
12563
|
function createProjectDir(workspaceRoot, relPath) {
|
|
11915
12564
|
const base = projectRoot(workspaceRoot);
|
|
11916
12565
|
const rel = (relPath ?? "").trim();
|
|
@@ -11984,6 +12633,42 @@ function searchProjectFiles(workspaceRoot, query) {
|
|
|
11984
12633
|
walk(base, "");
|
|
11985
12634
|
return out;
|
|
11986
12635
|
}
|
|
12636
|
+
function deleteProjectPaths(workspaceRoot, relPaths) {
|
|
12637
|
+
const base = projectRoot(workspaceRoot);
|
|
12638
|
+
const out = { deleted: [], failed: [] };
|
|
12639
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12640
|
+
for (const raw of relPaths ?? []) {
|
|
12641
|
+
const rel = (raw ?? "").trim().replace(/^[/\\]+/, "");
|
|
12642
|
+
if (!rel) {
|
|
12643
|
+
out.failed.push({ path: String(raw ?? ""), error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
12644
|
+
continue;
|
|
12645
|
+
}
|
|
12646
|
+
if (seen.has(rel)) continue;
|
|
12647
|
+
seen.add(rel);
|
|
12648
|
+
let abs;
|
|
12649
|
+
try {
|
|
12650
|
+
abs = resolveWithin(base, rel);
|
|
12651
|
+
} catch (e) {
|
|
12652
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
12653
|
+
continue;
|
|
12654
|
+
}
|
|
12655
|
+
if (path24.resolve(abs) === path24.resolve(base)) {
|
|
12656
|
+
out.failed.push({ path: rel, error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
12657
|
+
continue;
|
|
12658
|
+
}
|
|
12659
|
+
if (!fs20.existsSync(abs)) {
|
|
12660
|
+
out.failed.push({ path: rel, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
|
|
12661
|
+
continue;
|
|
12662
|
+
}
|
|
12663
|
+
try {
|
|
12664
|
+
fs20.rmSync(abs, { recursive: true, force: true });
|
|
12665
|
+
out.deleted.push(toPosix(path24.relative(base, abs)));
|
|
12666
|
+
} catch (e) {
|
|
12667
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
12668
|
+
}
|
|
12669
|
+
}
|
|
12670
|
+
return out;
|
|
12671
|
+
}
|
|
11987
12672
|
|
|
11988
12673
|
// src/server/projectOps.ts
|
|
11989
12674
|
init_esm_shims();
|
|
@@ -12389,7 +13074,7 @@ function buildNodeHost(store, manager, diskPath) {
|
|
|
12389
13074
|
arch: process.arch,
|
|
12390
13075
|
release: os6.release(),
|
|
12391
13076
|
uptimeSec: Math.round(os6.uptime()),
|
|
12392
|
-
version: "0.2.
|
|
13077
|
+
version: "0.2.16",
|
|
12393
13078
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
12394
13079
|
cpu: {
|
|
12395
13080
|
count: cpus2.length,
|
|
@@ -12532,6 +13217,7 @@ function parseApprovalDecision(raw) {
|
|
|
12532
13217
|
}
|
|
12533
13218
|
function createApp(store, manager, opts = {}) {
|
|
12534
13219
|
setupServerHandlers(manager);
|
|
13220
|
+
const updater = opts.updater ?? null;
|
|
12535
13221
|
const routes = [
|
|
12536
13222
|
// ---------- 会话管理 ----------
|
|
12537
13223
|
{
|
|
@@ -12673,6 +13359,18 @@ function createApp(store, manager, opts = {}) {
|
|
|
12673
13359
|
ok(res, { accepted: true });
|
|
12674
13360
|
}
|
|
12675
13361
|
},
|
|
13362
|
+
{
|
|
13363
|
+
method: "POST",
|
|
13364
|
+
pattern: ["sessions", ":id", "steer", "remove"],
|
|
13365
|
+
// 撤销未注入的补充信息
|
|
13366
|
+
fn: (_req, res, { tenant, params, body }) => {
|
|
13367
|
+
const index = Number(body?.index);
|
|
13368
|
+
if (!Number.isInteger(index))
|
|
13369
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
13370
|
+
const pendingSteers = manager.unsteer(tenant, params["id"], index);
|
|
13371
|
+
ok(res, { accepted: true, pendingSteers });
|
|
13372
|
+
}
|
|
13373
|
+
},
|
|
12676
13374
|
{
|
|
12677
13375
|
method: "POST",
|
|
12678
13376
|
pattern: ["sessions", ":id", "interrupt"],
|
|
@@ -13203,6 +13901,30 @@ function createApp(store, manager, opts = {}) {
|
|
|
13203
13901
|
}
|
|
13204
13902
|
}
|
|
13205
13903
|
},
|
|
13904
|
+
{
|
|
13905
|
+
// 二进制写入(图片编辑保存):body = { path, base64 }
|
|
13906
|
+
method: "POST",
|
|
13907
|
+
pattern: ["files", "binary"],
|
|
13908
|
+
fn: (_req, res, { tenant, body }) => {
|
|
13909
|
+
const raw = body ?? {};
|
|
13910
|
+
try {
|
|
13911
|
+
ok(
|
|
13912
|
+
res,
|
|
13913
|
+
writeProjectFileBase64(
|
|
13914
|
+
tenant.workspaceRoot ?? null,
|
|
13915
|
+
String(raw.path ?? ""),
|
|
13916
|
+
String(raw.base64 ?? "")
|
|
13917
|
+
)
|
|
13918
|
+
);
|
|
13919
|
+
} catch (e) {
|
|
13920
|
+
throw new ApiError(
|
|
13921
|
+
400,
|
|
13922
|
+
"bad_request",
|
|
13923
|
+
e instanceof Error ? e.message : String(e)
|
|
13924
|
+
);
|
|
13925
|
+
}
|
|
13926
|
+
}
|
|
13927
|
+
},
|
|
13206
13928
|
// ---------- 项目终端(在工作区执行命令) ----------
|
|
13207
13929
|
{
|
|
13208
13930
|
method: "POST",
|
|
@@ -13553,6 +14275,21 @@ function createApp(store, manager, opts = {}) {
|
|
|
13553
14275
|
}
|
|
13554
14276
|
}
|
|
13555
14277
|
},
|
|
14278
|
+
{
|
|
14279
|
+
method: "POST",
|
|
14280
|
+
pattern: ["files", "delete"],
|
|
14281
|
+
// 删除文件 / 目录(目录递归);逐条返回失败原因
|
|
14282
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14283
|
+
const raw = body ?? {};
|
|
14284
|
+
const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
|
|
14285
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
14286
|
+
try {
|
|
14287
|
+
ok(res, deleteProjectPaths(tenant.workspaceRoot ?? null, paths));
|
|
14288
|
+
} catch (e) {
|
|
14289
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
14290
|
+
}
|
|
14291
|
+
}
|
|
14292
|
+
},
|
|
13556
14293
|
{
|
|
13557
14294
|
method: "POST",
|
|
13558
14295
|
pattern: ["files", "create"],
|
|
@@ -13781,6 +14518,34 @@ function createApp(store, manager, opts = {}) {
|
|
|
13781
14518
|
}
|
|
13782
14519
|
}
|
|
13783
14520
|
},
|
|
14521
|
+
// ---------- 节点自更新(状态 / 检查 / 安装) ----------
|
|
14522
|
+
// action=install 先回执、后台安装并退出进程(由服务管理器拉起新版本),
|
|
14523
|
+
// 因此回执成功只代表「已受理」,是否升级成功要用 action=status 再确认。
|
|
14524
|
+
{
|
|
14525
|
+
method: "POST",
|
|
14526
|
+
pattern: ["node", "self-update"],
|
|
14527
|
+
projectOptional: true,
|
|
14528
|
+
fn: async (req2, res, { body }) => {
|
|
14529
|
+
const b = body ?? {};
|
|
14530
|
+
const action = String(b.action ?? "status");
|
|
14531
|
+
if (!updater) {
|
|
14532
|
+
throw new ApiError(400, "bad_request", "\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A\u8282\u70B9\u672A\u4EE5 serve \u6A21\u5F0F\u8FD0\u884C");
|
|
14533
|
+
}
|
|
14534
|
+
if (action === "status") {
|
|
14535
|
+
ok(res, updater.status());
|
|
14536
|
+
return;
|
|
14537
|
+
}
|
|
14538
|
+
if (action === "check") {
|
|
14539
|
+
ok(res, await updater.inspect());
|
|
14540
|
+
return;
|
|
14541
|
+
}
|
|
14542
|
+
if (action === "install") {
|
|
14543
|
+
ok(res, await updater.requestInstall(b.force === true));
|
|
14544
|
+
return;
|
|
14545
|
+
}
|
|
14546
|
+
throw new ApiError(400, "bad_request", `\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
14547
|
+
}
|
|
14548
|
+
},
|
|
13784
14549
|
// ---------- 节点级会话状态(跨项目;只含标识与 status) ----------
|
|
13785
14550
|
// 供节点首页 / 工作台建立初始视图,后续变化由 session_status 通知增量推送。
|
|
13786
14551
|
{
|
|
@@ -13943,7 +14708,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
13943
14708
|
// 阻止反向代理(nginx 等)缓冲 SSE 流
|
|
13944
14709
|
"X-Accel-Buffering": "no"
|
|
13945
14710
|
});
|
|
13946
|
-
const
|
|
14711
|
+
const sinceRaw2 = new URL(
|
|
13947
14712
|
req2.url,
|
|
13948
14713
|
"http://localhost"
|
|
13949
14714
|
).searchParams.get("since");
|
|
@@ -13951,19 +14716,51 @@ function createApp(store, manager, opts = {}) {
|
|
|
13951
14716
|
tenant.id,
|
|
13952
14717
|
params["id"],
|
|
13953
14718
|
res,
|
|
13954
|
-
|
|
14719
|
+
sinceRaw2 ? Number(sinceRaw2) : 0
|
|
13955
14720
|
);
|
|
13956
14721
|
return;
|
|
13957
14722
|
}
|
|
13958
|
-
const
|
|
13959
|
-
|
|
14723
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
14724
|
+
const sinceRaw = q.get("since");
|
|
14725
|
+
const beforeRaw = q.get("before");
|
|
14726
|
+
const maxBytesRaw = q.get("maxBytes");
|
|
14727
|
+
ok(
|
|
14728
|
+
res,
|
|
14729
|
+
manager.eventsPage(tenant.id, params["id"], {
|
|
14730
|
+
since: sinceRaw === null ? void 0 : Number(sinceRaw) || 0,
|
|
14731
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
14732
|
+
maxBytes: maxBytesRaw === null ? void 0 : Number(maxBytesRaw) || void 0
|
|
14733
|
+
})
|
|
13960
14734
|
);
|
|
13961
|
-
|
|
13962
|
-
|
|
13963
|
-
|
|
13964
|
-
|
|
14735
|
+
}
|
|
14736
|
+
},
|
|
14737
|
+
{
|
|
14738
|
+
method: "GET",
|
|
14739
|
+
pattern: ["sessions", ":id", "turns"],
|
|
14740
|
+
// 轮次汇总(会话详情的列表)
|
|
14741
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
14742
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
14743
|
+
const beforeRaw = q.get("before");
|
|
14744
|
+
const limitRaw = q.get("limit");
|
|
14745
|
+
ok(
|
|
14746
|
+
res,
|
|
14747
|
+
await manager.sessionTurns(tenant.id, params["id"], {
|
|
14748
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
14749
|
+
limit: limitRaw === null ? void 0 : Number(limitRaw) || void 0
|
|
14750
|
+
})
|
|
13965
14751
|
);
|
|
13966
|
-
|
|
14752
|
+
}
|
|
14753
|
+
},
|
|
14754
|
+
{
|
|
14755
|
+
method: "GET",
|
|
14756
|
+
// 单轮详情:用户消息(含补充) + 最终回复 + 产出文件
|
|
14757
|
+
pattern: ["sessions", ":id", "turns", ":seq", "reply"],
|
|
14758
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
14759
|
+
const startSeq = Number(params["seq"]);
|
|
14760
|
+
if (!Number.isFinite(startSeq) || startSeq <= 0) {
|
|
14761
|
+
throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
14762
|
+
}
|
|
14763
|
+
ok(res, await manager.sessionTurnReply(tenant.id, params["id"], startSeq));
|
|
13967
14764
|
}
|
|
13968
14765
|
}
|
|
13969
14766
|
];
|
|
@@ -13996,7 +14793,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
13996
14793
|
json(res, 200, {
|
|
13997
14794
|
status: "ok",
|
|
13998
14795
|
// 版本让运维/探针能确认线上跑的是哪个构建(更新与回滚据此判断)
|
|
13999
|
-
version: "0.2.
|
|
14796
|
+
version: "0.2.16",
|
|
14000
14797
|
features: { mcp: hasServerMcpConfig() }
|
|
14001
14798
|
});
|
|
14002
14799
|
return;
|
|
@@ -14269,6 +15066,24 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14269
15066
|
switch (env3.type) {
|
|
14270
15067
|
case "ping":
|
|
14271
15068
|
return ok2({ pong: true, ts: Date.now() });
|
|
15069
|
+
case "self_update": {
|
|
15070
|
+
const updater = deps.getUpdater?.() ?? null;
|
|
15071
|
+
if (!updater) {
|
|
15072
|
+
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");
|
|
15073
|
+
}
|
|
15074
|
+
const action = String(payload.action ?? "status");
|
|
15075
|
+
if (action === "status") {
|
|
15076
|
+
return ok2(updater.status());
|
|
15077
|
+
}
|
|
15078
|
+
if (action === "check") {
|
|
15079
|
+
return ok2(await updater.inspect());
|
|
15080
|
+
}
|
|
15081
|
+
if (action === "install") {
|
|
15082
|
+
const started = await updater.requestInstall(payload.force === true);
|
|
15083
|
+
return ok2(started);
|
|
15084
|
+
}
|
|
15085
|
+
return fail(`\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
15086
|
+
}
|
|
14272
15087
|
case "get_node_info":
|
|
14273
15088
|
return ok2(
|
|
14274
15089
|
buildNodeInfo(store, {
|
|
@@ -14391,8 +15206,22 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14391
15206
|
);
|
|
14392
15207
|
}
|
|
14393
15208
|
case "get_events": {
|
|
14394
|
-
const since =
|
|
14395
|
-
|
|
15209
|
+
const since = payload.since === void 0 ? void 0 : Number(payload.since) || 0;
|
|
15210
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
15211
|
+
const maxBytes = Number(payload.maxBytes) || void 0;
|
|
15212
|
+
return ok2(
|
|
15213
|
+
manager.eventsPage(project().id, sessionId(), { since, before, maxBytes })
|
|
15214
|
+
);
|
|
15215
|
+
}
|
|
15216
|
+
case "get_session_turns": {
|
|
15217
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
15218
|
+
const limit = Number(payload.limit) || void 0;
|
|
15219
|
+
return ok2(await manager.sessionTurns(project().id, sessionId(), { before, limit }));
|
|
15220
|
+
}
|
|
15221
|
+
case "get_session_turn_reply": {
|
|
15222
|
+
const startSeq = Number(payload.startSeq) || 0;
|
|
15223
|
+
if (startSeq <= 0) throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
15224
|
+
return ok2(await manager.sessionTurnReply(project().id, sessionId(), startSeq));
|
|
14396
15225
|
}
|
|
14397
15226
|
case "get_config":
|
|
14398
15227
|
return ok2(configView2(project()));
|
|
@@ -14453,6 +15282,16 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14453
15282
|
)
|
|
14454
15283
|
);
|
|
14455
15284
|
}
|
|
15285
|
+
case "write_file_base64": {
|
|
15286
|
+
const t = project();
|
|
15287
|
+
return ok2(
|
|
15288
|
+
writeProjectFileBase64(
|
|
15289
|
+
t.workspaceRoot ?? null,
|
|
15290
|
+
String(payload.path ?? ""),
|
|
15291
|
+
String(payload.base64 ?? "")
|
|
15292
|
+
)
|
|
15293
|
+
);
|
|
15294
|
+
}
|
|
14456
15295
|
case "create_dir": {
|
|
14457
15296
|
const t = project();
|
|
14458
15297
|
return ok2(createProjectDir(t.workspaceRoot ?? null, String(payload.path ?? "")));
|
|
@@ -14467,6 +15306,13 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14467
15306
|
)
|
|
14468
15307
|
);
|
|
14469
15308
|
}
|
|
15309
|
+
case "delete_files": {
|
|
15310
|
+
const t = project();
|
|
15311
|
+
const raw = payload.paths;
|
|
15312
|
+
const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
|
|
15313
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
15314
|
+
return ok2(deleteProjectPaths(t.workspaceRoot ?? null, paths));
|
|
15315
|
+
}
|
|
14470
15316
|
case "search_files": {
|
|
14471
15317
|
const t = project();
|
|
14472
15318
|
return ok2(searchProjectFiles(t.workspaceRoot ?? null, String(payload.query ?? "")));
|
|
@@ -14693,6 +15539,13 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14693
15539
|
manager.steer(project(), sessionId(), text);
|
|
14694
15540
|
return ok2({ accepted: true });
|
|
14695
15541
|
}
|
|
15542
|
+
case "unsteer": {
|
|
15543
|
+
const index = Number(payload.index);
|
|
15544
|
+
if (!Number.isInteger(index)) {
|
|
15545
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
15546
|
+
}
|
|
15547
|
+
return ok2({ accepted: true, pendingSteers: manager.unsteer(project(), sessionId(), index) });
|
|
15548
|
+
}
|
|
14696
15549
|
case "interrupt":
|
|
14697
15550
|
manager.interrupt(project(), sessionId());
|
|
14698
15551
|
return ok2({ accepted: true });
|
|
@@ -15293,6 +16146,8 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
|
|
|
15293
16146
|
"get_session_states",
|
|
15294
16147
|
"get_state",
|
|
15295
16148
|
"get_events",
|
|
16149
|
+
"get_session_turns",
|
|
16150
|
+
"get_session_turn_reply",
|
|
15296
16151
|
"get_config",
|
|
15297
16152
|
"list_models",
|
|
15298
16153
|
"list_dirs",
|
|
@@ -15542,6 +16397,10 @@ var FleetNodeBridge = class {
|
|
|
15542
16397
|
_dispatchEnvelope(env3) {
|
|
15543
16398
|
const run = () => this._dispatch(env3);
|
|
15544
16399
|
dbgMark(`recv:${env3.type}`, `reqId=${env3.reqId}`);
|
|
16400
|
+
if (env3.type === "self_update" && String(env3.payload?.action ?? "status") !== "install") {
|
|
16401
|
+
this._runReadonly(run);
|
|
16402
|
+
return;
|
|
16403
|
+
}
|
|
15545
16404
|
if (READONLY_CMD_TYPES.has(env3.type) || this._isReadonlyBatch(env3)) {
|
|
15546
16405
|
this._runReadonly(run);
|
|
15547
16406
|
return;
|
|
@@ -15568,7 +16427,7 @@ var FleetNodeBridge = class {
|
|
|
15568
16427
|
_invalidateSessionCache() {
|
|
15569
16428
|
this.writeEpoch += 1;
|
|
15570
16429
|
for (const key of [...this.cmdResultCache.keys()]) {
|
|
15571
|
-
if (/^(get_state|get_events|list_sessions|get_session_states)\|/.test(key)) {
|
|
16430
|
+
if (/^(get_state|get_events|get_session_turns|get_session_turn_reply|list_sessions|get_session_states)\|/.test(key)) {
|
|
15572
16431
|
this.cmdResultCache.delete(key);
|
|
15573
16432
|
}
|
|
15574
16433
|
}
|
|
@@ -15704,11 +16563,17 @@ var FleetNodeBridge = class {
|
|
|
15704
16563
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
15705
16564
|
platform: process.platform,
|
|
15706
16565
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15707
|
-
version: "0.2.
|
|
16566
|
+
version: "0.2.16",
|
|
15708
16567
|
ip: primaryIp()
|
|
15709
16568
|
};
|
|
15710
16569
|
}
|
|
15711
|
-
/**
|
|
16570
|
+
/**
|
|
16571
|
+
* 用量汇总(供控制面账户级聚合):总量 + 最近 40 天按天 + 最近 40 天按项目。
|
|
16572
|
+
*
|
|
16573
|
+
* 按项目这一维是为控制台「今日 / 最近 30 天消耗拆到每个项目」服务的:
|
|
16574
|
+
* 账本里的汇总只存了按天、按项目两个独立维度,交叉项只能现算,因此这里
|
|
16575
|
+
* 从明细聚合后随 info 一起上报(Hub 按 (节点, 项目, 天) 覆盖写入,天然幂等)。
|
|
16576
|
+
*/
|
|
15712
16577
|
_usagePayload() {
|
|
15713
16578
|
const ledger = this.opts.deps.manager.usageLedger();
|
|
15714
16579
|
if (!ledger) return void 0;
|
|
@@ -15716,11 +16581,22 @@ var FleetNodeBridge = class {
|
|
|
15716
16581
|
const days = {};
|
|
15717
16582
|
const keys = Object.keys(r.byDay).sort().slice(-40);
|
|
15718
16583
|
for (const k of keys) days[k] = r.byDay[k];
|
|
16584
|
+
const from = keys[0] ?? "";
|
|
16585
|
+
const to = keys[keys.length - 1] ?? "";
|
|
16586
|
+
const projects = {};
|
|
16587
|
+
if (from && to) {
|
|
16588
|
+
for (const [projectId, byDay] of Object.entries(
|
|
16589
|
+
ledger.projectByDay({ from, to })
|
|
16590
|
+
)) {
|
|
16591
|
+
projects[projectId] = { total: sumAgg(Object.values(byDay)), byDay };
|
|
16592
|
+
}
|
|
16593
|
+
}
|
|
15719
16594
|
return {
|
|
15720
16595
|
since: r.since,
|
|
15721
16596
|
total: r.total,
|
|
15722
16597
|
legacy: r.legacy ?? null,
|
|
15723
|
-
byDay: days
|
|
16598
|
+
byDay: days,
|
|
16599
|
+
projects
|
|
15724
16600
|
};
|
|
15725
16601
|
}
|
|
15726
16602
|
/**
|
|
@@ -15755,6 +16631,8 @@ var FleetNodeBridge = class {
|
|
|
15755
16631
|
name: s.name ?? null,
|
|
15756
16632
|
status: s.status,
|
|
15757
16633
|
lastUserMessage: lastUserMessage(s),
|
|
16634
|
+
// 控制面按它排「最近在跑什么」,运行中的长任务才不会被排到后面
|
|
16635
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
15758
16636
|
// 控制面的聚合邮件靠它判断「这台机器最近有没有在干活」;
|
|
15759
16637
|
// 状态跃迁可能被 info 节流跳过,完成时间不会。
|
|
15760
16638
|
lastCompletedAt: s.lastCompletedAt ?? null
|
|
@@ -15783,7 +16661,7 @@ var FleetNodeBridge = class {
|
|
|
15783
16661
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
15784
16662
|
platform: process.platform,
|
|
15785
16663
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15786
|
-
version: "0.2.
|
|
16664
|
+
version: "0.2.16",
|
|
15787
16665
|
ip: primaryIp(),
|
|
15788
16666
|
projects,
|
|
15789
16667
|
totals,
|
|
@@ -17412,7 +18290,7 @@ function verifySignature(data, signatureBase64, publicKey) {
|
|
|
17412
18290
|
}
|
|
17413
18291
|
|
|
17414
18292
|
// src/update/downloader.ts
|
|
17415
|
-
var
|
|
18293
|
+
var MAX_BINARY_BYTES2 = 400 * 1024 * 1024;
|
|
17416
18294
|
var MANIFEST_TIMEOUT_MS = 15e3;
|
|
17417
18295
|
var DOWNLOAD_TIMEOUT_MS2 = 10 * 6e4;
|
|
17418
18296
|
async function fetchManifest(url, publicKey, opts = {}) {
|
|
@@ -17458,7 +18336,7 @@ async function downloadBinary(bin, destPath, log = () => {
|
|
|
17458
18336
|
if (!res.ok) return { error: `\u4E0B\u8F7D\u5931\u8D25 HTTP ${res.status}` };
|
|
17459
18337
|
if (!res.body) return { error: "\u4E0B\u8F7D\u54CD\u5E94\u6CA1\u6709 body" };
|
|
17460
18338
|
const expected = bin.size && bin.size > 0 ? bin.size : void 0;
|
|
17461
|
-
const limit = expected ? Math.max(expected * 2, 1024 * 1024) :
|
|
18339
|
+
const limit = expected ? Math.max(expected * 2, 1024 * 1024) : MAX_BINARY_BYTES2;
|
|
17462
18340
|
const hash = createHash3("sha256");
|
|
17463
18341
|
let bytes = 0;
|
|
17464
18342
|
try {
|
|
@@ -17521,7 +18399,7 @@ import * as path30 from "path";
|
|
|
17521
18399
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
17522
18400
|
import { createRequire as createRequire3 } from "module";
|
|
17523
18401
|
function currentVersion() {
|
|
17524
|
-
const injected = "0.2.
|
|
18402
|
+
const injected = "0.2.16"?.trim();
|
|
17525
18403
|
if (injected) return injected;
|
|
17526
18404
|
try {
|
|
17527
18405
|
const req2 = createRequire3(import.meta.url);
|
|
@@ -17835,6 +18713,31 @@ function nextIntervalMs() {
|
|
|
17835
18713
|
const jitter = base * 0.15 * (Math.random() * 2 - 1);
|
|
17836
18714
|
return Math.max(6e4, Math.round(base + jitter));
|
|
17837
18715
|
}
|
|
18716
|
+
function evaluateManifest(base, manifest) {
|
|
18717
|
+
if (!parseVersion(base.currentVersion) || compareVersions(manifest.version, base.currentVersion) <= 0) {
|
|
18718
|
+
return {
|
|
18719
|
+
...base,
|
|
18720
|
+
updateAvailable: false,
|
|
18721
|
+
latestVersion: manifest.version,
|
|
18722
|
+
reason: `\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF08v${base.currentVersion}\uFF09`
|
|
18723
|
+
};
|
|
18724
|
+
}
|
|
18725
|
+
if (manifest.minimumVersion && compareVersions(base.currentVersion, manifest.minimumVersion) < 0) {
|
|
18726
|
+
return {
|
|
18727
|
+
...base,
|
|
18728
|
+
updateAvailable: false,
|
|
18729
|
+
latestVersion: manifest.version,
|
|
18730
|
+
manualUpgradeRequired: manifest.minimumVersion,
|
|
18731
|
+
reason: `\u5F53\u524D\u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u624B\u52A8\u5347\u7EA7\u5230 >= ${manifest.minimumVersion}`
|
|
18732
|
+
};
|
|
18733
|
+
}
|
|
18734
|
+
return {
|
|
18735
|
+
...base,
|
|
18736
|
+
updateAvailable: true,
|
|
18737
|
+
latestVersion: manifest.version,
|
|
18738
|
+
...manifest.notes ? { notes: manifest.notes } : {}
|
|
18739
|
+
};
|
|
18740
|
+
}
|
|
17838
18741
|
var UpdateController = class {
|
|
17839
18742
|
constructor(opts, currentVersion2 = currentVersion()) {
|
|
17840
18743
|
this.opts = opts;
|
|
@@ -17923,6 +18826,77 @@ var UpdateController = class {
|
|
|
17923
18826
|
}
|
|
17924
18827
|
this.schedule(nextIntervalMs());
|
|
17925
18828
|
}
|
|
18829
|
+
/**
|
|
18830
|
+
* 只读检查:拉清单比对版本,不下载、不改 state。
|
|
18831
|
+
*
|
|
18832
|
+
* 供远端 `self_update --action check` 与 UI 展示用;与定时器 `check()` 不同,
|
|
18833
|
+
* 这里不加 `checking` 锁、不写 `lastCheckAt`,因此不会干扰自动更新流程。
|
|
18834
|
+
*/
|
|
18835
|
+
async inspect() {
|
|
18836
|
+
const base = {
|
|
18837
|
+
currentVersion: this.currentVersion,
|
|
18838
|
+
channel: updaterChannel(),
|
|
18839
|
+
manifestUrl: this.manifestUrl()
|
|
18840
|
+
};
|
|
18841
|
+
const key = this.publicKey;
|
|
18842
|
+
const cap = selfUpdateCapability(Boolean(key));
|
|
18843
|
+
if (!cap.ok) {
|
|
18844
|
+
return { ...base, updateAvailable: false, reason: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
18845
|
+
}
|
|
18846
|
+
const res = await fetchManifest(base.manifestUrl, key, {
|
|
18847
|
+
allowInsecureUrl: env("ZQAGENT_UPDATE_INSECURE") === "1"
|
|
18848
|
+
});
|
|
18849
|
+
if (!res.manifest) {
|
|
18850
|
+
return {
|
|
18851
|
+
...base,
|
|
18852
|
+
updateAvailable: false,
|
|
18853
|
+
reason: `\u66F4\u65B0\u68C0\u67E5\u5931\u8D25\uFF1A${res.error ?? "\u6E05\u5355\u6821\u9A8C\u5931\u8D25"}`
|
|
18854
|
+
};
|
|
18855
|
+
}
|
|
18856
|
+
return evaluateManifest(base, res.manifest);
|
|
18857
|
+
}
|
|
18858
|
+
/**
|
|
18859
|
+
* 远端触发安装:先同步给出结论(受理/无需更新),安装本身在后台跑。
|
|
18860
|
+
*
|
|
18861
|
+
* 必须「先回执、后安装」:安装成功后进程会 `exit(70)` 交给 systemd 拉起新版本,
|
|
18862
|
+
* 如果等到安装完成再回执,回执永远发不出去(表现为网关超时)。
|
|
18863
|
+
*
|
|
18864
|
+
* 复用定时器同一条 `check()` 路径:验签、下载、自检、失败计数、安装后自检与
|
|
18865
|
+
* 回滚保护全都一致,只是把 `installMode` 设为 `force` 时不等待空闲。
|
|
18866
|
+
*/
|
|
18867
|
+
async requestInstall(force = false) {
|
|
18868
|
+
const fromVersion = this.currentVersion;
|
|
18869
|
+
const cap = selfUpdateCapability(Boolean(this.publicKey));
|
|
18870
|
+
if (!cap.ok) {
|
|
18871
|
+
return { accepted: false, fromVersion, message: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
18872
|
+
}
|
|
18873
|
+
const info = await this.inspect();
|
|
18874
|
+
if (!info.updateAvailable) {
|
|
18875
|
+
return {
|
|
18876
|
+
accepted: false,
|
|
18877
|
+
fromVersion,
|
|
18878
|
+
...info.latestVersion ? { toVersion: info.latestVersion } : {},
|
|
18879
|
+
message: info.reason ?? "\u5DF2\u662F\u6700\u65B0\u7248\u672C"
|
|
18880
|
+
};
|
|
18881
|
+
}
|
|
18882
|
+
if (this.checking || this.installing) {
|
|
18883
|
+
return {
|
|
18884
|
+
accepted: true,
|
|
18885
|
+
fromVersion,
|
|
18886
|
+
toVersion: info.latestVersion,
|
|
18887
|
+
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`
|
|
18888
|
+
};
|
|
18889
|
+
}
|
|
18890
|
+
void this.check(force, false, true).catch((err) => {
|
|
18891
|
+
this.recordFailure(info.latestVersion, `\u5B89\u88C5\u5931\u8D25\uFF1A${err.message}`);
|
|
18892
|
+
});
|
|
18893
|
+
return {
|
|
18894
|
+
accepted: true,
|
|
18895
|
+
fromVersion,
|
|
18896
|
+
toVersion: info.latestVersion,
|
|
18897
|
+
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`
|
|
18898
|
+
};
|
|
18899
|
+
}
|
|
17926
18900
|
/**
|
|
17927
18901
|
* 检查并准备更新;`installMode` 为 true 时下载后立即安装(不再等空闲)。
|
|
17928
18902
|
* 返回可读结论,供 CLI `update --check` 使用。
|
|
@@ -18322,6 +19296,7 @@ function startServer(opts = {}) {
|
|
|
18322
19296
|
const notificationHub = createNotificationHub();
|
|
18323
19297
|
let fleet = null;
|
|
18324
19298
|
let fleetCred = null;
|
|
19299
|
+
let updater = null;
|
|
18325
19300
|
function connectFleet(cred) {
|
|
18326
19301
|
const localId = ensureNodeId(store);
|
|
18327
19302
|
const nodeId = (cred.nodeId ?? "").trim() || localId;
|
|
@@ -18339,6 +19314,8 @@ function startServer(opts = {}) {
|
|
|
18339
19314
|
deps: {
|
|
18340
19315
|
store,
|
|
18341
19316
|
manager,
|
|
19317
|
+
// 闭包推迟求值:桥先于 UpdateController 创建,指令到达时 updater 已就绪
|
|
19318
|
+
getUpdater: () => updater,
|
|
18342
19319
|
applyFleetCred: async (cred2) => {
|
|
18343
19320
|
store.saveFleetCredential(cred2);
|
|
18344
19321
|
setTimeout(() => connectFleet(cred2), 300);
|
|
@@ -18489,16 +19466,21 @@ function startServer(opts = {}) {
|
|
|
18489
19466
|
scheduleFleetInfoRefresh();
|
|
18490
19467
|
}
|
|
18491
19468
|
});
|
|
19469
|
+
updater = new UpdateController({
|
|
19470
|
+
dataDir,
|
|
19471
|
+
isIdle: () => manager.isIdle(),
|
|
19472
|
+
prepareExit: closeServer
|
|
19473
|
+
});
|
|
18492
19474
|
const server = createApp(store, manager, {
|
|
18493
19475
|
secretBox,
|
|
18494
19476
|
notificationHub,
|
|
18495
19477
|
qq,
|
|
18496
19478
|
adminKey,
|
|
18497
19479
|
fleet: fleetController,
|
|
18498
|
-
host
|
|
19480
|
+
host,
|
|
19481
|
+
updater
|
|
18499
19482
|
});
|
|
18500
19483
|
let closed = false;
|
|
18501
|
-
let updater = null;
|
|
18502
19484
|
function closeServer() {
|
|
18503
19485
|
if (closed) return;
|
|
18504
19486
|
closed = true;
|
|
@@ -18531,13 +19513,6 @@ function startServer(opts = {}) {
|
|
|
18531
19513
|
}
|
|
18532
19514
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
18533
19515
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
18534
|
-
if (opts.autoUpdate) {
|
|
18535
|
-
updater = new UpdateController({
|
|
18536
|
-
dataDir,
|
|
18537
|
-
isIdle: () => manager.isIdle(),
|
|
18538
|
-
prepareExit: closeServer
|
|
18539
|
-
});
|
|
18540
|
-
}
|
|
18541
19516
|
server.timeout = 0;
|
|
18542
19517
|
server.keepAliveTimeout = 65e3;
|
|
18543
19518
|
server.headersTimeout = 66e3;
|
|
@@ -18557,7 +19532,7 @@ function startServer(opts = {}) {
|
|
|
18557
19532
|
} else {
|
|
18558
19533
|
printReady(info, qq);
|
|
18559
19534
|
}
|
|
18560
|
-
updater?.start();
|
|
19535
|
+
if (opts.autoUpdate) updater?.start();
|
|
18561
19536
|
resolve20(info);
|
|
18562
19537
|
}, reject);
|
|
18563
19538
|
});
|
|
@@ -18598,7 +19573,7 @@ function printReady(info, qq) {
|
|
|
18598
19573
|
// src/cli/account-enroll.ts
|
|
18599
19574
|
init_esm_shims();
|
|
18600
19575
|
import * as path35 from "path";
|
|
18601
|
-
import * as
|
|
19576
|
+
import * as readline2 from "readline/promises";
|
|
18602
19577
|
var SKIP_ANSWERS = /* @__PURE__ */ new Set(["-", "skip", "none", "no"]);
|
|
18603
19578
|
function resolveDefaultHub(hubUrl2, env3 = process.env) {
|
|
18604
19579
|
return (hubUrl2 ?? env3.HUB_URL ?? DEFAULT_HUB_URL).trim().replace(/\/$/, "");
|
|
@@ -18689,7 +19664,7 @@ async function maybeAccountEnroll(opts = {}) {
|
|
|
18689
19664
|
cliErr(`\u63A5\u5165\u68C0\u67E5\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
18690
19665
|
return;
|
|
18691
19666
|
}
|
|
18692
|
-
const rl =
|
|
19667
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
18693
19668
|
try {
|
|
18694
19669
|
await runWizard(rl, store, opts);
|
|
18695
19670
|
} catch (e) {
|
|
@@ -18770,7 +19745,7 @@ async function askHidden(rl, prompt) {
|
|
|
18770
19745
|
|
|
18771
19746
|
// src/cli/link.ts
|
|
18772
19747
|
init_esm_shims();
|
|
18773
|
-
import * as
|
|
19748
|
+
import * as readline3 from "readline";
|
|
18774
19749
|
|
|
18775
19750
|
// src/cli/serve-instances.ts
|
|
18776
19751
|
init_esm_shims();
|