@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.cjs
CHANGED
|
@@ -580,6 +580,7 @@ init_cjs_shims();
|
|
|
580
580
|
var fs3 = __toESM(require("fs"), 1);
|
|
581
581
|
var fsp = __toESM(require("fs/promises"), 1);
|
|
582
582
|
var path4 = __toESM(require("path"), 1);
|
|
583
|
+
var readline = __toESM(require("readline"), 1);
|
|
583
584
|
var import_crypto = require("crypto");
|
|
584
585
|
|
|
585
586
|
// src/server/dbgTrace.ts
|
|
@@ -1497,6 +1498,170 @@ var Store = class {
|
|
|
1497
1498
|
}
|
|
1498
1499
|
}
|
|
1499
1500
|
}
|
|
1501
|
+
/**
|
|
1502
|
+
* 读取事件「一页」:从文件末尾向前,按字节预算取最近的事件。
|
|
1503
|
+
*
|
|
1504
|
+
* - `before` 省略:取最新的 maxBytes 字节
|
|
1505
|
+
* - `before` 指定:只取 seq < before 的事件(向前翻页)
|
|
1506
|
+
*
|
|
1507
|
+
* 为什么不能复用 loadEventsTail:后者只能从文件末尾读固定窗口,
|
|
1508
|
+
* 无法「向前翻页」,而控制台需要滚到顶部继续加载更早的对话。
|
|
1509
|
+
* 这里按块从后往前扫,读到预算或文件头即停;翻了多页之后每次
|
|
1510
|
+
* before 都紧邻上一页,实际只需读 1~2 块。
|
|
1511
|
+
*
|
|
1512
|
+
* 返回按 seq 升序;`hasMore` 表示本页最早一条之前还有更早的事件。
|
|
1513
|
+
*/
|
|
1514
|
+
loadEventPage(tenantId, sessionId, opts) {
|
|
1515
|
+
const p = path4.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
1516
|
+
const { before, maxBytes } = opts;
|
|
1517
|
+
const __t0 = dbgNow();
|
|
1518
|
+
let fd = null;
|
|
1519
|
+
try {
|
|
1520
|
+
const size = fs3.statSync(p).size;
|
|
1521
|
+
if (size === 0) return { events: [], hasMore: false };
|
|
1522
|
+
fd = fs3.openSync(p, "r");
|
|
1523
|
+
const CHUNK = 256 * 1024;
|
|
1524
|
+
let pos = size;
|
|
1525
|
+
const kept = [];
|
|
1526
|
+
let bytes = 0;
|
|
1527
|
+
let hasMore = false;
|
|
1528
|
+
let done = false;
|
|
1529
|
+
while (pos > 0 && !done) {
|
|
1530
|
+
const start = Math.max(0, pos - CHUNK);
|
|
1531
|
+
const len = pos - start;
|
|
1532
|
+
const buf = Buffer.allocUnsafe(len);
|
|
1533
|
+
const read = fs3.readSync(fd, buf, 0, len, start);
|
|
1534
|
+
const rows = buf.subarray(0, read).toString("utf-8").split("\n");
|
|
1535
|
+
if (start > 0) rows.shift();
|
|
1536
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
1537
|
+
const line = rows[i];
|
|
1538
|
+
if (!line) continue;
|
|
1539
|
+
let entry;
|
|
1540
|
+
try {
|
|
1541
|
+
entry = JSON.parse(line);
|
|
1542
|
+
} catch {
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (!Number.isFinite(Number(entry.seq))) continue;
|
|
1546
|
+
if (before !== void 0 && Number(entry.seq) >= before) continue;
|
|
1547
|
+
const sz = Buffer.byteLength(line, "utf-8") + 1;
|
|
1548
|
+
if (bytes > 0 && bytes + sz > maxBytes) {
|
|
1549
|
+
hasMore = true;
|
|
1550
|
+
done = true;
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
bytes += sz;
|
|
1554
|
+
kept.unshift(entry);
|
|
1555
|
+
}
|
|
1556
|
+
pos = start;
|
|
1557
|
+
}
|
|
1558
|
+
dbgMs(
|
|
1559
|
+
"store.loadEventPage",
|
|
1560
|
+
__t0,
|
|
1561
|
+
// [dbg]
|
|
1562
|
+
`size=${size} before=${before ?? "-"} out=${kept.length} bytes\u2248${bytes} more=${hasMore}`
|
|
1563
|
+
);
|
|
1564
|
+
return { events: kept, hasMore };
|
|
1565
|
+
} catch {
|
|
1566
|
+
return { events: [], hasMore: false };
|
|
1567
|
+
} finally {
|
|
1568
|
+
if (fd !== null) {
|
|
1569
|
+
try {
|
|
1570
|
+
fs3.closeSync(fd);
|
|
1571
|
+
} catch {
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
// ===================== 轮次索引 =====================
|
|
1577
|
+
/** 轮次索引文件路径 */
|
|
1578
|
+
turnsFile(tenantId, sessionId) {
|
|
1579
|
+
return path4.join(this.sessionDir(tenantId, sessionId), "turns.ndjson");
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* 追加一轮的最新快照(同一 startSeq 后写者胜出)。
|
|
1583
|
+
*
|
|
1584
|
+
* 走与 session.json 相同的每文件写链:索引按「开轮 → 补充消息 → 收轮」
|
|
1585
|
+
* 顺序追加,乱序会让列表读到旧的 running 版本。
|
|
1586
|
+
*/
|
|
1587
|
+
appendTurnIndex(tenantId, sessionId, entry) {
|
|
1588
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
1589
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1590
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
1591
|
+
const line = JSON.stringify(entry) + "\n";
|
|
1592
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
1593
|
+
const next = prev.then(() => fsp.appendFile(file, line, "utf-8")).catch((err) => {
|
|
1594
|
+
console.error(`[store] appendTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
1595
|
+
});
|
|
1596
|
+
this.writeChains.set(file, next);
|
|
1597
|
+
return next;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* 读取轮次索引(已按 startSeq 去重取最后一版,升序)。
|
|
1601
|
+
*
|
|
1602
|
+
* 文件很小(一轮约 0.5KB,1000 轮 ≈ 500KB),同步读即可,
|
|
1603
|
+
* 会话详情打开路径不会因历史长度变慢。
|
|
1604
|
+
*/
|
|
1605
|
+
loadTurnIndex(tenantId, sessionId) {
|
|
1606
|
+
let raw;
|
|
1607
|
+
try {
|
|
1608
|
+
raw = fs3.readFileSync(this.turnsFile(tenantId, sessionId), "utf-8");
|
|
1609
|
+
} catch {
|
|
1610
|
+
return [];
|
|
1611
|
+
}
|
|
1612
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
1613
|
+
for (const line of raw.split("\n")) {
|
|
1614
|
+
if (!line) continue;
|
|
1615
|
+
try {
|
|
1616
|
+
const e = JSON.parse(line);
|
|
1617
|
+
if (!Number.isFinite(Number(e?.startSeq))) continue;
|
|
1618
|
+
byStart.set(Number(e.startSeq), e);
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
1623
|
+
}
|
|
1624
|
+
/** 整文件替换轮次索引(重建用,tmp + rename 原子写) */
|
|
1625
|
+
saveTurnIndex(tenantId, sessionId, entries) {
|
|
1626
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
1627
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1628
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
1629
|
+
const snapshot = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
1630
|
+
const tmp = `${file}.${(0, import_crypto.randomUUID)()}.tmp`;
|
|
1631
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
1632
|
+
const next = prev.then(() => fsp.writeFile(tmp, snapshot, "utf-8")).then(() => fsp.rename(tmp, file)).catch((err) => {
|
|
1633
|
+
console.error(`[store] saveTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
1634
|
+
void fsp.rm(tmp, { force: true });
|
|
1635
|
+
});
|
|
1636
|
+
this.writeChains.set(file, next);
|
|
1637
|
+
return next;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* 流式读取整份事件日志(重建索引用)。
|
|
1641
|
+
*
|
|
1642
|
+
* 不能用 loadEvents(readFileSync + 逐行 JSON.parse):大会话几十 MB,
|
|
1643
|
+
* 会长时间阻塞事件循环,导致 SSE 心跳 / MQTT 保活断开。
|
|
1644
|
+
*/
|
|
1645
|
+
async readEventsAsync(tenantId, sessionId) {
|
|
1646
|
+
const p = path4.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
1647
|
+
const out = [];
|
|
1648
|
+
try {
|
|
1649
|
+
const rl = readline.createInterface({
|
|
1650
|
+
input: fs3.createReadStream(p, { encoding: "utf-8" }),
|
|
1651
|
+
crlfDelay: Infinity
|
|
1652
|
+
});
|
|
1653
|
+
for await (const line of rl) {
|
|
1654
|
+
if (!line) continue;
|
|
1655
|
+
try {
|
|
1656
|
+
out.push(JSON.parse(line));
|
|
1657
|
+
} catch {
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
} catch {
|
|
1661
|
+
return out;
|
|
1662
|
+
}
|
|
1663
|
+
return out;
|
|
1664
|
+
}
|
|
1500
1665
|
// ===================== 节点身份 & 管理员密钥 =====================
|
|
1501
1666
|
/** 节点稳定标识:{root}/node.json(首次启动生成后长期固定) */
|
|
1502
1667
|
loadNodeId() {
|
|
@@ -1917,13 +2082,13 @@ function textOf(content) {
|
|
|
1917
2082
|
}
|
|
1918
2083
|
return content != null ? String(content) : "";
|
|
1919
2084
|
}
|
|
1920
|
-
function extractUserMessages(span,
|
|
2085
|
+
function extractUserMessages(span, clip2 = USER_MESSAGE_CLIP) {
|
|
1921
2086
|
const out = [];
|
|
1922
2087
|
for (const m of span) {
|
|
1923
2088
|
if (m.role !== "user") continue;
|
|
1924
2089
|
const text = textOf(m.content).replace(/\s+/g, " ").trim();
|
|
1925
2090
|
if (!text) continue;
|
|
1926
|
-
out.push(text.length >
|
|
2091
|
+
out.push(text.length > clip2 ? text.slice(0, clip2 - 1) + "\u2026" : text);
|
|
1927
2092
|
}
|
|
1928
2093
|
return out;
|
|
1929
2094
|
}
|
|
@@ -2735,6 +2900,21 @@ var TurnEngine = class _TurnEngine {
|
|
|
2735
2900
|
queueSteering(text) {
|
|
2736
2901
|
this._steering.push(text);
|
|
2737
2902
|
}
|
|
2903
|
+
/**
|
|
2904
|
+
* 撤销一条尚未注入的 steering。
|
|
2905
|
+
*
|
|
2906
|
+
* 按文本删第一条匹配项:调用方(SessionManager)已按索引删掉了自己那份队列,
|
|
2907
|
+
* 这里只需保证引擎不再注入同样的内容。用文本而非索引,是因为引擎会在迭代
|
|
2908
|
+
* 间隙批量清空 `_steering`,索引可能已经漂移;文本相同则删哪条都等价。
|
|
2909
|
+
*
|
|
2910
|
+
* @returns 是否真的删掉了(已注入的项返回 false,调用方据此提示用户)
|
|
2911
|
+
*/
|
|
2912
|
+
removeSteering(text) {
|
|
2913
|
+
const i = this._steering.indexOf(text);
|
|
2914
|
+
if (i < 0) return false;
|
|
2915
|
+
this._steering.splice(i, 1);
|
|
2916
|
+
return true;
|
|
2917
|
+
}
|
|
2738
2918
|
// ===================== 主入口 =====================
|
|
2739
2919
|
/**
|
|
2740
2920
|
* 运行一轮 Turn
|
|
@@ -9276,6 +9456,19 @@ function addAgg(into, tokens, costUsd) {
|
|
|
9276
9456
|
function utcDay(iso) {
|
|
9277
9457
|
return iso.slice(0, 10);
|
|
9278
9458
|
}
|
|
9459
|
+
function sumAgg(values) {
|
|
9460
|
+
const out = zeroAgg();
|
|
9461
|
+
for (const v of values) {
|
|
9462
|
+
out.calls += v.calls;
|
|
9463
|
+
out.input += v.input;
|
|
9464
|
+
out.output += v.output;
|
|
9465
|
+
out.cacheRead += v.cacheRead;
|
|
9466
|
+
out.cacheWrite += v.cacheWrite;
|
|
9467
|
+
out.costUsd += v.costUsd;
|
|
9468
|
+
out.unpriced += v.unpriced;
|
|
9469
|
+
}
|
|
9470
|
+
return out;
|
|
9471
|
+
}
|
|
9279
9472
|
function monthOf(iso) {
|
|
9280
9473
|
return iso.slice(0, 7);
|
|
9281
9474
|
}
|
|
@@ -9551,6 +9744,23 @@ var UsageLedger = class {
|
|
|
9551
9744
|
}
|
|
9552
9745
|
return all;
|
|
9553
9746
|
}
|
|
9747
|
+
/**
|
|
9748
|
+
* 按「项目 × 天」聚合(供控制面把消耗拆到每个项目)。
|
|
9749
|
+
*
|
|
9750
|
+
* 汇总文件里只存了按项目、按天两个独立维度,拿不到交叉项,因此这里从
|
|
9751
|
+
* 账本明细现算——调用方(节点上报)只取最近 40 天,读的是 2 个月文件,
|
|
9752
|
+
* 5 分钟一次的成本可忽略。
|
|
9753
|
+
*/
|
|
9754
|
+
projectByDay(opts) {
|
|
9755
|
+
const out = {};
|
|
9756
|
+
for (const rec of this.allRecords({ from: opts.from, to: opts.to })) {
|
|
9757
|
+
const day = utcDay(rec.ts);
|
|
9758
|
+
const days = out[rec.projectId] ?? (out[rec.projectId] = {});
|
|
9759
|
+
const agg = days[day] ?? (days[day] = zeroAgg());
|
|
9760
|
+
addAgg(agg, rec, rec.costUsd);
|
|
9761
|
+
}
|
|
9762
|
+
return out;
|
|
9763
|
+
}
|
|
9554
9764
|
summary(opts) {
|
|
9555
9765
|
this.ensureInit();
|
|
9556
9766
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
@@ -9850,6 +10060,141 @@ var EVENT_BUFFER_MAX = 2e3;
|
|
|
9850
10060
|
var EVENTS_ACK_MAX_BYTES = 512 * 1024;
|
|
9851
10061
|
var APPROVAL_TIMEOUT_MS = (Number(process.env.APPROVAL_TIMEOUT_SECONDS) || 15 * 60) * 1e3;
|
|
9852
10062
|
var LAST_USER_MESSAGE_MAX = 200;
|
|
10063
|
+
var TURN_TEXT_MAX = 500;
|
|
10064
|
+
var TURN_REPLY_MAX_BYTES = 256 * 1024;
|
|
10065
|
+
var TURN_REPLY_MAX_TEXT = 256 * 1024;
|
|
10066
|
+
var TURNS_DEFAULT_LIMIT = 20;
|
|
10067
|
+
function clip(text, max = LAST_USER_MESSAGE_MAX) {
|
|
10068
|
+
const s = text ?? "";
|
|
10069
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
10070
|
+
}
|
|
10071
|
+
function asNum(v) {
|
|
10072
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
10073
|
+
}
|
|
10074
|
+
function newTurnEntry(ev, input) {
|
|
10075
|
+
return {
|
|
10076
|
+
v: 1,
|
|
10077
|
+
startSeq: ev.seq,
|
|
10078
|
+
endSeq: ev.seq,
|
|
10079
|
+
startedAt: ev.ts,
|
|
10080
|
+
endedAt: null,
|
|
10081
|
+
status: "running",
|
|
10082
|
+
inputs: [{ seq: ev.seq, ts: ev.ts, text: clip(input, TURN_TEXT_MAX) }],
|
|
10083
|
+
replyPreview: "",
|
|
10084
|
+
producedFiles: [],
|
|
10085
|
+
llmCalls: 0,
|
|
10086
|
+
toolCalls: 0,
|
|
10087
|
+
tokensIn: 0,
|
|
10088
|
+
tokensOut: 0
|
|
10089
|
+
};
|
|
10090
|
+
}
|
|
10091
|
+
function applyTurnEvent(turn, ev, toolSeen) {
|
|
10092
|
+
const d = ev.data ?? {};
|
|
10093
|
+
turn.endSeq = ev.seq;
|
|
10094
|
+
switch (ev.type) {
|
|
10095
|
+
case "queue_steering": {
|
|
10096
|
+
turn.inputs.push({
|
|
10097
|
+
seq: ev.seq,
|
|
10098
|
+
ts: ev.ts,
|
|
10099
|
+
text: clip(String(d["text"] ?? ""), TURN_TEXT_MAX)
|
|
10100
|
+
});
|
|
10101
|
+
break;
|
|
10102
|
+
}
|
|
10103
|
+
case "assistant_message": {
|
|
10104
|
+
turn.llmCalls += 1;
|
|
10105
|
+
const text = String(d["text"] ?? "");
|
|
10106
|
+
if (text) {
|
|
10107
|
+
turn.replyPreview = clip(text);
|
|
10108
|
+
turn.replySeq = ev.seq;
|
|
10109
|
+
}
|
|
10110
|
+
const u = d["usage"];
|
|
10111
|
+
if (u && typeof u === "object") {
|
|
10112
|
+
const usage = u;
|
|
10113
|
+
turn.tokensIn += asNum(usage["input"]);
|
|
10114
|
+
turn.tokensOut += asNum(usage["output"]);
|
|
10115
|
+
}
|
|
10116
|
+
break;
|
|
10117
|
+
}
|
|
10118
|
+
case "tool_started":
|
|
10119
|
+
case "tool_finished": {
|
|
10120
|
+
const key = String(d["reqId"] ?? "") || `${String(d["name"] ?? "")}:${ev.seq}`;
|
|
10121
|
+
if (!toolSeen.has(key)) {
|
|
10122
|
+
toolSeen.add(key);
|
|
10123
|
+
turn.toolCalls += 1;
|
|
10124
|
+
}
|
|
10125
|
+
break;
|
|
10126
|
+
}
|
|
10127
|
+
case "error": {
|
|
10128
|
+
turn.error = clip(String(d["message"] ?? d["error"] ?? ""));
|
|
10129
|
+
break;
|
|
10130
|
+
}
|
|
10131
|
+
default:
|
|
10132
|
+
break;
|
|
10133
|
+
}
|
|
10134
|
+
}
|
|
10135
|
+
function closeTurnEntry(turn, ev) {
|
|
10136
|
+
const d = ev.data ?? {};
|
|
10137
|
+
turn.endSeq = ev.seq;
|
|
10138
|
+
turn.endedAt = ev.ts;
|
|
10139
|
+
const status = String(d["status"] ?? "");
|
|
10140
|
+
turn.status = ev.type === "interrupted" ? status || "interrupted" : status || "completed";
|
|
10141
|
+
const files = d["produced_files"];
|
|
10142
|
+
if (Array.isArray(files)) {
|
|
10143
|
+
turn.producedFiles = files.map((x) => String(x ?? "")).filter(Boolean);
|
|
10144
|
+
}
|
|
10145
|
+
if (typeof d["iterations"] === "number") turn.iterations = d["iterations"];
|
|
10146
|
+
const startMs = Date.parse(turn.startedAt);
|
|
10147
|
+
const endMs = Date.parse(ev.ts);
|
|
10148
|
+
if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
|
|
10149
|
+
turn.durationMs = endMs - startMs;
|
|
10150
|
+
}
|
|
10151
|
+
}
|
|
10152
|
+
function buildTurnIndex(events) {
|
|
10153
|
+
const out = [];
|
|
10154
|
+
let cur = null;
|
|
10155
|
+
let toolSeen = /* @__PURE__ */ new Set();
|
|
10156
|
+
for (const ev of events) {
|
|
10157
|
+
if (ev.type === "turn_start") {
|
|
10158
|
+
cur = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
10159
|
+
toolSeen = /* @__PURE__ */ new Set();
|
|
10160
|
+
out.push(cur);
|
|
10161
|
+
continue;
|
|
10162
|
+
}
|
|
10163
|
+
if (!cur) continue;
|
|
10164
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
10165
|
+
closeTurnEntry(cur, ev);
|
|
10166
|
+
cur = null;
|
|
10167
|
+
continue;
|
|
10168
|
+
}
|
|
10169
|
+
applyTurnEvent(cur, ev, toolSeen);
|
|
10170
|
+
}
|
|
10171
|
+
for (const t of out) {
|
|
10172
|
+
if (!t.endedAt && t.error) t.status = "error";
|
|
10173
|
+
}
|
|
10174
|
+
return out;
|
|
10175
|
+
}
|
|
10176
|
+
function summaryFromEntry(t) {
|
|
10177
|
+
return {
|
|
10178
|
+
startSeq: t.startSeq,
|
|
10179
|
+
endSeq: t.endSeq,
|
|
10180
|
+
startedAt: t.startedAt,
|
|
10181
|
+
endedAt: t.endedAt,
|
|
10182
|
+
status: t.status,
|
|
10183
|
+
input: t.inputs[0]?.text ?? "",
|
|
10184
|
+
reply: t.replyPreview,
|
|
10185
|
+
llmCalls: t.llmCalls,
|
|
10186
|
+
toolCalls: t.toolCalls,
|
|
10187
|
+
tokensIn: t.tokensIn,
|
|
10188
|
+
tokensOut: t.tokensOut,
|
|
10189
|
+
iterations: t.iterations,
|
|
10190
|
+
durationMs: t.durationMs,
|
|
10191
|
+
error: t.error,
|
|
10192
|
+
inputs: t.inputs,
|
|
10193
|
+
userCount: t.inputs.length,
|
|
10194
|
+
replySeq: t.replySeq,
|
|
10195
|
+
producedFiles: t.producedFiles
|
|
10196
|
+
};
|
|
10197
|
+
}
|
|
9853
10198
|
function isAffirmative(answer) {
|
|
9854
10199
|
const a = answer.trim().toLowerCase();
|
|
9855
10200
|
if (!a) return false;
|
|
@@ -9874,6 +10219,10 @@ var SessionManager = class {
|
|
|
9874
10219
|
opts;
|
|
9875
10220
|
runtimes = /* @__PURE__ */ new Map();
|
|
9876
10221
|
// sessionId → runtime
|
|
10222
|
+
/** 轮次索引的内存副本(运行中的轮比磁盘索引新;重启后为空) */
|
|
10223
|
+
turnIndexCache = /* @__PURE__ */ new Map();
|
|
10224
|
+
/** 进行中的索引重建任务(并发请求合并到同一个) */
|
|
10225
|
+
turnIndexBuilds = /* @__PURE__ */ new Map();
|
|
9877
10226
|
/** 全局通知 SSE 订阅者(控制台通知中心) */
|
|
9878
10227
|
notificationSubs = /* @__PURE__ */ new Set();
|
|
9879
10228
|
/**
|
|
@@ -9928,6 +10277,17 @@ var SessionManager = class {
|
|
|
9928
10277
|
rt.status = status;
|
|
9929
10278
|
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
9930
10279
|
}
|
|
10280
|
+
/**
|
|
10281
|
+
* 通知里的「最近一条用户消息」:截断到 LAST_USER_MESSAGE_MAX。
|
|
10282
|
+
*
|
|
10283
|
+
* 三处共用(session_status / turn_completed / turn_stopped),避免各自写一遍
|
|
10284
|
+
* 截断规则后长度不一致——控制台弹窗与任务列表会显示同一句话的两个版本。
|
|
10285
|
+
*/
|
|
10286
|
+
_lastMessage(rt) {
|
|
10287
|
+
const text = (rt.lastInput ?? "").trim();
|
|
10288
|
+
if (!text) return null;
|
|
10289
|
+
return text.length > LAST_USER_MESSAGE_MAX ? `${text.slice(0, LAST_USER_MESSAGE_MAX)}\u2026` : text;
|
|
10290
|
+
}
|
|
9931
10291
|
/**
|
|
9932
10292
|
* session_status 通知载荷:状态 + 最近一条用户消息(截断)。
|
|
9933
10293
|
*
|
|
@@ -9936,10 +10296,24 @@ var SessionManager = class {
|
|
|
9936
10296
|
*/
|
|
9937
10297
|
_statusData(rt) {
|
|
9938
10298
|
const data = { status: rt.status };
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
10299
|
+
if (rt.lastStartedAt) data.lastStartedAt = rt.lastStartedAt;
|
|
10300
|
+
const msg = this._lastMessage(rt);
|
|
10301
|
+
if (msg) data.lastUserMessage = msg;
|
|
10302
|
+
return data;
|
|
10303
|
+
}
|
|
10304
|
+
/**
|
|
10305
|
+
* 任务结束类通知(完成 / 达到迭代上限)的载荷:在 turn_end 数据之外补上
|
|
10306
|
+
* 「会话名 + 最后一条用户消息」。
|
|
10307
|
+
*
|
|
10308
|
+
* 控制台右下角的完成弹窗要说明「哪个会话、用户让它做了什么」。这些字段
|
|
10309
|
+
* 以节点侧为准随通知一起下发,前端就不必依赖快照(nodeInfo / session_status)
|
|
10310
|
+
* 是否已经到达——刚打开页面或通知乱序时才不会退化成只剩一句「任务已完成」。
|
|
10311
|
+
*/
|
|
10312
|
+
_turnEndData(rt, eventData, extra) {
|
|
10313
|
+
const data = { ...eventData, ...extra };
|
|
10314
|
+
if (rt.name) data.sessionName = rt.name;
|
|
10315
|
+
const msg = this._lastMessage(rt);
|
|
10316
|
+
if (msg) data.lastUserMessage = msg;
|
|
9943
10317
|
return data;
|
|
9944
10318
|
}
|
|
9945
10319
|
/** 触发通知回调(无 handler 时为空操作,异常隔离不冒泡) */
|
|
@@ -10096,6 +10470,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10096
10470
|
this.runtimes.delete(sessionId);
|
|
10097
10471
|
}
|
|
10098
10472
|
this.store.deleteSession(tenantId, sessionId);
|
|
10473
|
+
this.turnIndexCache.delete(sessionId);
|
|
10099
10474
|
this._publishNotify(tenantId, {
|
|
10100
10475
|
kind: "session_status",
|
|
10101
10476
|
sessionId,
|
|
@@ -10139,6 +10514,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10139
10514
|
throw new ApiError(409, "busy", "\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D");
|
|
10140
10515
|
}
|
|
10141
10516
|
rt.engine.queueSteering(input);
|
|
10517
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
10142
10518
|
rt.pendingSteers.push(input);
|
|
10143
10519
|
this._recordLastInput(rt, input);
|
|
10144
10520
|
return "steered";
|
|
@@ -10171,9 +10547,42 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10171
10547
|
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
10172
10548
|
if (!rt.engine) throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
10173
10549
|
rt.engine.queueSteering(text);
|
|
10550
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
10174
10551
|
rt.pendingSteers.push(text);
|
|
10175
10552
|
this._recordLastInput(rt, text);
|
|
10176
10553
|
}
|
|
10554
|
+
/**
|
|
10555
|
+
* 撤销一条尚未注入的补充信息(用户反悔)。
|
|
10556
|
+
*
|
|
10557
|
+
* 队列里的项还没进聊天流,此时只能靠中断整个任务来「收回」代价太大。
|
|
10558
|
+
* index 取自 GET /state 的 pendingSteers(与前端展示顺序一致),两侧同时删:
|
|
10559
|
+
* 只删前端那份,引擎下一迭代仍会把它注入对话;只删引擎那份,队列里会残留
|
|
10560
|
+
* 一条永远不注入的条目。
|
|
10561
|
+
*
|
|
10562
|
+
* 若撤的正是「最近用户指令」,把它还原成队列里的上一条、或本轮被覆盖前的
|
|
10563
|
+
* 指令——否则工作台还会显示这条已经被取消的内容。
|
|
10564
|
+
*
|
|
10565
|
+
* @returns 剩余待注入队列(前端据此刷新提示区)
|
|
10566
|
+
*/
|
|
10567
|
+
unsteer(tenant, sessionId, index) {
|
|
10568
|
+
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
10569
|
+
if (!rt.engine || !rt.runTask) {
|
|
10570
|
+
throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
10571
|
+
}
|
|
10572
|
+
if (!Number.isInteger(index) || index < 0 || index >= rt.pendingSteers.length) {
|
|
10573
|
+
throw new ApiError(409, "already_injected", "\u8FD9\u6761\u8865\u5145\u4FE1\u606F\u5DF2\u8FDB\u5165\u5BF9\u8BDD\uFF0C\u65E0\u6CD5\u64A4\u9500");
|
|
10574
|
+
}
|
|
10575
|
+
const [removed] = rt.pendingSteers.splice(index, 1);
|
|
10576
|
+
if (removed !== void 0) {
|
|
10577
|
+
rt.engine.removeSteering(removed);
|
|
10578
|
+
if (removed === rt.lastInput) {
|
|
10579
|
+
rt.lastInput = rt.pendingSteers.length > 0 ? rt.pendingSteers[rt.pendingSteers.length - 1] : rt.steerBaseInput;
|
|
10580
|
+
this._persist(rt);
|
|
10581
|
+
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
10582
|
+
}
|
|
10583
|
+
}
|
|
10584
|
+
return [...rt.pendingSteers];
|
|
10585
|
+
}
|
|
10177
10586
|
/**
|
|
10178
10587
|
* 已提交、尚未注入当前任务的补充信息。
|
|
10179
10588
|
*
|
|
@@ -10606,44 +11015,214 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10606
11015
|
status: rt.status
|
|
10607
11016
|
};
|
|
10608
11017
|
}
|
|
10609
|
-
/**
|
|
10610
|
-
|
|
11018
|
+
/**
|
|
11019
|
+
* 事件分页(向前 / 向后两种方向)。
|
|
11020
|
+
*
|
|
11021
|
+
* - `since`:取 seq > since(实时增量 / 重连补齐)
|
|
11022
|
+
* - `before`:取 seq < before(控制台滚到顶部加载更早的对话)
|
|
11023
|
+
* - `maxBytes`:单次回执的字节预算(默认沿用 EVENTS_ACK_MAX_BYTES)
|
|
11024
|
+
*
|
|
11025
|
+
* 两者同时给出时按 `since`(向后)处理。`hasMore` 只对向前翻页有意义:
|
|
11026
|
+
* 表示本页最早一条之前还有更早的事件。
|
|
11027
|
+
*
|
|
11028
|
+
* 向前翻页一律走磁盘:内存缓冲只保留最近 EVENT_BUFFER_MAX 条,
|
|
11029
|
+
* 更早的事件只在 events.jsonl 里。
|
|
11030
|
+
*/
|
|
11031
|
+
eventsPage(tenantId, sessionId, opts = {}) {
|
|
10611
11032
|
const __t0 = dbgNow();
|
|
11033
|
+
const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : EVENTS_ACK_MAX_BYTES;
|
|
10612
11034
|
const rt = this.runtimes.get(sessionId);
|
|
11035
|
+
if (opts.before !== void 0) {
|
|
11036
|
+
const since2 = opts.since ?? 0;
|
|
11037
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
11038
|
+
before: opts.before,
|
|
11039
|
+
maxBytes
|
|
11040
|
+
});
|
|
11041
|
+
const ev2 = since2 > 0 ? page.events.filter((e) => e.seq > since2) : page.events;
|
|
11042
|
+
dbgMs(
|
|
11043
|
+
"session.eventsPage(before/\u78C1\u76D8)",
|
|
11044
|
+
__t0,
|
|
11045
|
+
// [dbg]
|
|
11046
|
+
`sid=${sessionId.slice(0, 8)} since=${since2} before=${opts.before} out=${ev2.length} more=${page.hasMore}`
|
|
11047
|
+
);
|
|
11048
|
+
return {
|
|
11049
|
+
events: ev2,
|
|
11050
|
+
latestSeq: rt?.seq ?? page.events.at(-1)?.seq ?? 0,
|
|
11051
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
11052
|
+
hasMore: page.hasMore
|
|
11053
|
+
};
|
|
11054
|
+
}
|
|
11055
|
+
const since = opts.since ?? 0;
|
|
10613
11056
|
if (rt) {
|
|
10614
|
-
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since));
|
|
11057
|
+
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since), maxBytes);
|
|
10615
11058
|
dbgMs(
|
|
10616
|
-
"session.
|
|
11059
|
+
"session.eventsPage(\u5185\u5B58)",
|
|
10617
11060
|
__t0,
|
|
10618
11061
|
// [dbg]
|
|
10619
11062
|
`sid=${sessionId.slice(0, 8)} since=${since} in=${rt.events.length} out=${ev2.length}`
|
|
10620
11063
|
);
|
|
10621
|
-
return {
|
|
11064
|
+
return {
|
|
11065
|
+
events: ev2,
|
|
11066
|
+
latestSeq: rt.seq,
|
|
11067
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
11068
|
+
// 内存缓冲被截断(或 since 早于缓冲起点)时,磁盘上可能还有更早的
|
|
11069
|
+
hasMore: !!ev2.length && ev2[0].seq > (rt.events[0]?.seq ?? 0)
|
|
11070
|
+
};
|
|
10622
11071
|
}
|
|
10623
11072
|
const all = this.store.loadEventsTail(tenantId, sessionId, EVENTS_ACK_MAX_BYTES * 2);
|
|
10624
|
-
const ev = this._boundedTail(
|
|
11073
|
+
const ev = this._boundedTail(
|
|
11074
|
+
all.filter((e) => e.seq > since),
|
|
11075
|
+
maxBytes
|
|
11076
|
+
);
|
|
10625
11077
|
dbgMs(
|
|
10626
|
-
"session.
|
|
11078
|
+
"session.eventsPage(\u78C1\u76D8)",
|
|
10627
11079
|
__t0,
|
|
10628
11080
|
// [dbg]
|
|
10629
11081
|
`sid=${sessionId.slice(0, 8)} since=${since} loaded=${all.length} out=${ev.length}`
|
|
10630
11082
|
);
|
|
10631
11083
|
return {
|
|
10632
11084
|
events: ev,
|
|
10633
|
-
latestSeq: all.at(-1)?.seq ?? 0
|
|
11085
|
+
latestSeq: all.at(-1)?.seq ?? 0,
|
|
11086
|
+
earliestSeq: ev[0]?.seq ?? 0,
|
|
11087
|
+
// 读的是文件尾部窗口:窗口已满说明更早的事件还在文件里
|
|
11088
|
+
hasMore: all.length > 0 && ev.length > 0 && ev[0].seq > all[0].seq
|
|
10634
11089
|
};
|
|
10635
11090
|
}
|
|
11091
|
+
/**
|
|
11092
|
+
* 兼容旧调用:只按 since 取增量。
|
|
11093
|
+
* @deprecated 新代码用 eventsPage(支持 before 向前翻页)
|
|
11094
|
+
*/
|
|
11095
|
+
eventsSince(tenantId, sessionId, since) {
|
|
11096
|
+
const r = this.eventsPage(tenantId, sessionId, { since });
|
|
11097
|
+
return { events: r.events, latestSeq: r.latestSeq };
|
|
11098
|
+
}
|
|
11099
|
+
/**
|
|
11100
|
+
* 「按 turn 汇总」的轻量列表:只回标题与统计,点开某一轮再单独拉该轮回复。
|
|
11101
|
+
*
|
|
11102
|
+
* 数据来自 turns.ndjson 索引(派生自事件流),读取成本与会话历史长度无关:
|
|
11103
|
+
* 老会话首次访问时后台/本请求内重建一次,之后每页只读几 KB。
|
|
11104
|
+
*
|
|
11105
|
+
* - `before`:只返回 startSeq < before 的轮次(向前翻页)
|
|
11106
|
+
* - `turns` 按时间升序;`hasMore` 表示更早还有轮次
|
|
11107
|
+
*/
|
|
11108
|
+
async sessionTurns(tenantId, sessionId, opts = {}) {
|
|
11109
|
+
const limit = Math.min(Math.max(opts.limit ?? TURNS_DEFAULT_LIMIT, 1), 100);
|
|
11110
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
11111
|
+
const before = opts.before;
|
|
11112
|
+
const endIdx = before === void 0 ? entries.length : entries.findIndex((t) => t.startSeq >= before);
|
|
11113
|
+
const end = endIdx < 0 ? entries.length : endIdx;
|
|
11114
|
+
const start = Math.max(0, end - limit);
|
|
11115
|
+
const picked = entries.slice(start, end);
|
|
11116
|
+
return {
|
|
11117
|
+
turns: picked.map(summaryFromEntry),
|
|
11118
|
+
hasMore: start > 0,
|
|
11119
|
+
earliestSeq: picked[0]?.startSeq ?? 0,
|
|
11120
|
+
total: entries.length,
|
|
11121
|
+
userTotal: entries.reduce((n, t) => n + t.inputs.length, 0)
|
|
11122
|
+
};
|
|
11123
|
+
}
|
|
11124
|
+
/**
|
|
11125
|
+
* 某一轮的用户消息与最终回复(含产出文件)。
|
|
11126
|
+
*
|
|
11127
|
+
* 只读两条索引记录 + 尾部一个事件页,不会因该轮工具输出很大而变慢:
|
|
11128
|
+
* 回复正文紧邻 turn_end,按 replySeq 精确读即可。
|
|
11129
|
+
*/
|
|
11130
|
+
async sessionTurnReply(tenantId, sessionId, startSeq) {
|
|
11131
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
11132
|
+
const turn = entries.find((t) => t.startSeq === startSeq);
|
|
11133
|
+
if (!turn) throw new ApiError(404, "not_found", "\u8F6E\u6B21\u4E0D\u5B58\u5728");
|
|
11134
|
+
let reply = null;
|
|
11135
|
+
let truncated = false;
|
|
11136
|
+
if (turn.replySeq !== void 0) {
|
|
11137
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
11138
|
+
before: turn.replySeq + 1,
|
|
11139
|
+
maxBytes: TURN_REPLY_MAX_BYTES
|
|
11140
|
+
});
|
|
11141
|
+
const ev = page.events.find((e) => e.seq === turn.replySeq);
|
|
11142
|
+
if (ev && ev.type === "assistant_message") {
|
|
11143
|
+
const text = String(ev.data?.["text"] ?? "");
|
|
11144
|
+
if (text.length > TURN_REPLY_MAX_TEXT) {
|
|
11145
|
+
reply = { seq: ev.seq, ts: ev.ts, text: text.slice(0, TURN_REPLY_MAX_TEXT) };
|
|
11146
|
+
truncated = true;
|
|
11147
|
+
} else {
|
|
11148
|
+
reply = { seq: ev.seq, ts: ev.ts, text };
|
|
11149
|
+
}
|
|
11150
|
+
}
|
|
11151
|
+
}
|
|
11152
|
+
return {
|
|
11153
|
+
startSeq: turn.startSeq,
|
|
11154
|
+
endSeq: turn.endSeq,
|
|
11155
|
+
status: turn.status,
|
|
11156
|
+
startedAt: turn.startedAt,
|
|
11157
|
+
endedAt: turn.endedAt,
|
|
11158
|
+
userMessages: turn.inputs,
|
|
11159
|
+
reply,
|
|
11160
|
+
truncated,
|
|
11161
|
+
producedFiles: turn.producedFiles
|
|
11162
|
+
};
|
|
11163
|
+
}
|
|
11164
|
+
/**
|
|
11165
|
+
* 取轮次索引;缺失时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
11166
|
+
*
|
|
11167
|
+
* 索引是派生数据:损坏/落后都只需重建,不影响会话本身。
|
|
11168
|
+
*/
|
|
11169
|
+
/**
|
|
11170
|
+
* 取轮次索引;缺失/落后时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
11171
|
+
*
|
|
11172
|
+
* 索引是派生数据:损坏或落后都只需重建,不影响会话本身。
|
|
11173
|
+
*/
|
|
11174
|
+
async _ensureTurnIndex(tenantId, sessionId) {
|
|
11175
|
+
let entries = this._mergeTurnIndex(tenantId, sessionId);
|
|
11176
|
+
if (entries.length > 0 && !this.runtimes.has(sessionId)) {
|
|
11177
|
+
const last = entries[entries.length - 1];
|
|
11178
|
+
const tail = this.store.loadEventsTail(tenantId, sessionId, 64 * 1024);
|
|
11179
|
+
const newTurnStarted = tail.some((e) => e.type === "turn_start" && e.seq > last.endSeq);
|
|
11180
|
+
if (newTurnStarted || last.status === "running") entries = [];
|
|
11181
|
+
}
|
|
11182
|
+
if (entries.length > 0) return entries;
|
|
11183
|
+
const pending = this.turnIndexBuilds.get(sessionId);
|
|
11184
|
+
if (pending) return pending;
|
|
11185
|
+
const task = this._rebuildTurnIndex(tenantId, sessionId).finally(() => {
|
|
11186
|
+
this.turnIndexBuilds.delete(sessionId);
|
|
11187
|
+
});
|
|
11188
|
+
this.turnIndexBuilds.set(sessionId, task);
|
|
11189
|
+
return task;
|
|
11190
|
+
}
|
|
11191
|
+
/** 合并磁盘索引与内存索引(内存版本更新,同一 startSeq 以内存为准) */
|
|
11192
|
+
_mergeTurnIndex(tenantId, sessionId) {
|
|
11193
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
11194
|
+
for (const e of this.store.loadTurnIndex(tenantId, sessionId)) byStart.set(e.startSeq, e);
|
|
11195
|
+
for (const e of this.turnIndexCache.get(sessionId) ?? []) byStart.set(e.startSeq, e);
|
|
11196
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
11197
|
+
}
|
|
11198
|
+
/** 从 events.jsonl 顺序流式重建轮次索引(一次性,之后增量维护) */
|
|
11199
|
+
async _rebuildTurnIndex(tenantId, sessionId) {
|
|
11200
|
+
const __t0 = dbgNow();
|
|
11201
|
+
const events = await this.store.readEventsAsync(tenantId, sessionId);
|
|
11202
|
+
const entries = buildTurnIndex(events);
|
|
11203
|
+
if (entries.length > 0) {
|
|
11204
|
+
await this.store.saveTurnIndex(tenantId, sessionId, entries);
|
|
11205
|
+
if (this.runtimes.has(sessionId)) this.turnIndexCache.set(sessionId, entries);
|
|
11206
|
+
}
|
|
11207
|
+
dbgMs(
|
|
11208
|
+
"session._rebuildTurnIndex",
|
|
11209
|
+
__t0,
|
|
11210
|
+
// [dbg]
|
|
11211
|
+
`sid=${sessionId.slice(0, 8)} events=${events.length} turns=${entries.length}`
|
|
11212
|
+
);
|
|
11213
|
+
return entries;
|
|
11214
|
+
}
|
|
10636
11215
|
/**
|
|
10637
11216
|
* 按字节预算截取事件「尾部」(至少保留最后一条),确保 MQTT 回执远小于单包上限。
|
|
10638
11217
|
* 供 get_events 使用:会话历史可任意大,但 MQTT 链路不能承载大包。
|
|
10639
11218
|
*/
|
|
10640
|
-
_boundedTail(events) {
|
|
11219
|
+
_boundedTail(events, maxBytes = EVENTS_ACK_MAX_BYTES) {
|
|
10641
11220
|
const __t0 = dbgNow();
|
|
10642
11221
|
let total = 0;
|
|
10643
11222
|
let i = events.length;
|
|
10644
11223
|
while (i > 0) {
|
|
10645
11224
|
const size = Buffer.byteLength(JSON.stringify(events[i - 1]));
|
|
10646
|
-
if (total > 0 && total + size >
|
|
11225
|
+
if (total > 0 && total + size > maxBytes) break;
|
|
10647
11226
|
total += size;
|
|
10648
11227
|
i -= 1;
|
|
10649
11228
|
}
|
|
@@ -10763,6 +11342,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10763
11342
|
nameSource: rec.nameSource,
|
|
10764
11343
|
taskCount: rec.taskCount ?? 0,
|
|
10765
11344
|
lastCompletedAt: rec.lastCompletedAt,
|
|
11345
|
+
lastStartedAt: rec.lastStartedAt,
|
|
10766
11346
|
naming: false,
|
|
10767
11347
|
turnStartedAt: 0
|
|
10768
11348
|
};
|
|
@@ -10780,10 +11360,11 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10780
11360
|
}
|
|
10781
11361
|
/** 装配引擎并启动一轮 Turn(供 submitTurn / submitOrSteer 复用) */
|
|
10782
11362
|
_startTurn(rt, input) {
|
|
11363
|
+
const engine = this._buildEngine(rt.tenant, rt, true);
|
|
10783
11364
|
rt.lastInput = input;
|
|
10784
|
-
this._setStatus(rt, "running");
|
|
10785
11365
|
rt.turnStartedAt = Date.now();
|
|
10786
|
-
|
|
11366
|
+
rt.lastStartedAt = new Date(rt.turnStartedAt).toISOString();
|
|
11367
|
+
this._setStatus(rt, "running");
|
|
10787
11368
|
rt.engine = engine;
|
|
10788
11369
|
this._persist(rt);
|
|
10789
11370
|
rt.runTask = this._drive(rt, engine.run(input));
|
|
@@ -10842,18 +11423,18 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10842
11423
|
this._setStatus(rt, "completed");
|
|
10843
11424
|
rt.taskCount += 1;
|
|
10844
11425
|
rt.lastCompletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10845
|
-
this._emitNotify(
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
durationMs
|
|
10849
|
-
|
|
11426
|
+
this._emitNotify(
|
|
11427
|
+
rt,
|
|
11428
|
+
"turn_completed",
|
|
11429
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
11430
|
+
);
|
|
10850
11431
|
} else if (status === "max_iterations_exceeded") {
|
|
10851
11432
|
this._setStatus(rt, "stopped");
|
|
10852
|
-
this._emitNotify(
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
durationMs
|
|
10856
|
-
|
|
11433
|
+
this._emitNotify(
|
|
11434
|
+
rt,
|
|
11435
|
+
"turn_stopped",
|
|
11436
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
11437
|
+
);
|
|
10857
11438
|
}
|
|
10858
11439
|
this._persist(rt);
|
|
10859
11440
|
if (status === "completed" && engine && shouldAutoName(rt.taskCount, rt.nameSource)) {
|
|
@@ -10864,7 +11445,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10864
11445
|
case "error" /* ERROR */:
|
|
10865
11446
|
this._setStatus(rt, "error");
|
|
10866
11447
|
this._persist(rt);
|
|
10867
|
-
this._emitNotify(rt, "error", event.data);
|
|
11448
|
+
this._emitNotify(rt, "error", this._turnEndData(rt, event.data, {}));
|
|
10868
11449
|
break;
|
|
10869
11450
|
case "interrupted" /* INTERRUPTED */:
|
|
10870
11451
|
this._setStatus(rt, "interrupted");
|
|
@@ -10890,9 +11471,17 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10890
11471
|
this._setStatus(rt, "error");
|
|
10891
11472
|
this._persist(rt);
|
|
10892
11473
|
} finally {
|
|
11474
|
+
if (rt.turnIndex) {
|
|
11475
|
+
const turn = rt.turnIndex;
|
|
11476
|
+
if (!turn.endedAt && turn.error) turn.status = "error";
|
|
11477
|
+
this._appendTurnIndex(rt, turn);
|
|
11478
|
+
rt.turnIndex = void 0;
|
|
11479
|
+
rt.turnToolKeys = void 0;
|
|
11480
|
+
}
|
|
10893
11481
|
rt.runTask = null;
|
|
10894
11482
|
rt.engine = null;
|
|
10895
11483
|
rt.pendingSteers = [];
|
|
11484
|
+
rt.steerBaseInput = void 0;
|
|
10896
11485
|
}
|
|
10897
11486
|
}
|
|
10898
11487
|
_buffer(rt, event) {
|
|
@@ -10905,6 +11494,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10905
11494
|
rt.events.push(entry);
|
|
10906
11495
|
if (rt.events.length > EVENT_BUFFER_MAX) rt.events.shift();
|
|
10907
11496
|
void this.store.appendEvent(rt.tenantId, rt.sessionId, entry);
|
|
11497
|
+
this._trackTurnIndex(rt, entry);
|
|
10908
11498
|
for (const res of rt.subscribers) this._sseSend(res, entry);
|
|
10909
11499
|
this._broadcastSessionEvent(rt.tenantId, rt.sessionId, entry);
|
|
10910
11500
|
try {
|
|
@@ -10912,6 +11502,42 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10912
11502
|
} catch {
|
|
10913
11503
|
}
|
|
10914
11504
|
}
|
|
11505
|
+
/**
|
|
11506
|
+
* 维护运行中轮次的索引(in-memory 记账 + 关键节点落盘)。
|
|
11507
|
+
*
|
|
11508
|
+
* 落盘只在「开轮 / 补充消息 / 收轮」发生,delta 事件仅推进 endSeq 与统计,
|
|
11509
|
+
* 因此索引写入量是每轮 2~3 行,与会话事件量无关。
|
|
11510
|
+
*/
|
|
11511
|
+
_trackTurnIndex(rt, ev) {
|
|
11512
|
+
if (ev.type === "turn_start") {
|
|
11513
|
+
const turn2 = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
11514
|
+
rt.turnIndex = turn2;
|
|
11515
|
+
rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
11516
|
+
this._appendTurnIndex(rt, turn2);
|
|
11517
|
+
return;
|
|
11518
|
+
}
|
|
11519
|
+
const turn = rt.turnIndex;
|
|
11520
|
+
if (!turn) return;
|
|
11521
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
11522
|
+
closeTurnEntry(turn, ev);
|
|
11523
|
+
this._appendTurnIndex(rt, turn);
|
|
11524
|
+
rt.turnIndex = void 0;
|
|
11525
|
+
rt.turnToolKeys = void 0;
|
|
11526
|
+
return;
|
|
11527
|
+
}
|
|
11528
|
+
if (!rt.turnToolKeys) rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
11529
|
+
const inputsBefore = turn.inputs.length;
|
|
11530
|
+
applyTurnEvent(turn, ev, rt.turnToolKeys);
|
|
11531
|
+
if (turn.inputs.length !== inputsBefore) this._appendTurnIndex(rt, turn);
|
|
11532
|
+
}
|
|
11533
|
+
/** 写入一版轮次索引(内存副本 + 异步落盘) */
|
|
11534
|
+
_appendTurnIndex(rt, turn) {
|
|
11535
|
+
const cached3 = this.turnIndexCache.get(rt.sessionId) ?? [];
|
|
11536
|
+
const i = cached3.findIndex((t) => t.startSeq === turn.startSeq);
|
|
11537
|
+
const next = i >= 0 ? [...cached3.slice(0, i), turn, ...cached3.slice(i + 1)] : [...cached3, turn];
|
|
11538
|
+
this.turnIndexCache.set(rt.sessionId, next);
|
|
11539
|
+
void this.store.appendTurnIndex(rt.tenantId, rt.sessionId, turn);
|
|
11540
|
+
}
|
|
10915
11541
|
/** 回收内存 run 任务(释放引用交给 GC;磁盘状态保持不动) */
|
|
10916
11542
|
_recycleRunTask(rt) {
|
|
10917
11543
|
rt.runTask = null;
|
|
@@ -10949,6 +11575,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10949
11575
|
nameSource: rt.nameSource,
|
|
10950
11576
|
taskCount: rt.taskCount,
|
|
10951
11577
|
lastCompletedAt: rt.lastCompletedAt,
|
|
11578
|
+
lastStartedAt: rt.lastStartedAt,
|
|
10952
11579
|
modelOverridden: rt.modelOverridden,
|
|
10953
11580
|
providerId: rt.providerId,
|
|
10954
11581
|
permissionMode: rt.permissionMode,
|
|
@@ -11359,6 +11986,7 @@ function allSessionStates(store, manager) {
|
|
|
11359
11986
|
name: s.name ?? null,
|
|
11360
11987
|
status: s.status,
|
|
11361
11988
|
lastUserMessage: lastUserMessage(s),
|
|
11989
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
11362
11990
|
lastCompletedAt: lastCompletedAt(s)
|
|
11363
11991
|
});
|
|
11364
11992
|
}
|
|
@@ -11413,7 +12041,7 @@ function buildNodeInfo(store, opts) {
|
|
|
11413
12041
|
nodeId: opts.nodeId ?? ensureNodeId(store),
|
|
11414
12042
|
platform: process.platform,
|
|
11415
12043
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
11416
|
-
version: "0.2.
|
|
12044
|
+
version: "0.2.16",
|
|
11417
12045
|
admin: opts.admin,
|
|
11418
12046
|
projects: opts.projects
|
|
11419
12047
|
};
|
|
@@ -11781,6 +12409,7 @@ var fs20 = __toESM(require("fs"), 1);
|
|
|
11781
12409
|
var path23 = __toESM(require("path"), 1);
|
|
11782
12410
|
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11783
12411
|
var MAX_DOWNLOAD_BYTES = 16 * 1024 * 1024;
|
|
12412
|
+
var MAX_BINARY_BYTES = 2.5 * 1024 * 1024;
|
|
11784
12413
|
var MAX_ENTRIES2 = 2e3;
|
|
11785
12414
|
function toPosix(p) {
|
|
11786
12415
|
return p.split(path23.sep).join("/");
|
|
@@ -11928,6 +12557,26 @@ function writeProjectFile(workspaceRoot, relPath, content) {
|
|
|
11928
12557
|
}
|
|
11929
12558
|
return { path: toPosix(path23.relative(base, file)), size: buf.length };
|
|
11930
12559
|
}
|
|
12560
|
+
function writeProjectFileBase64(workspaceRoot, relPath, base64) {
|
|
12561
|
+
const base = projectRoot(workspaceRoot);
|
|
12562
|
+
const rel = (relPath ?? "").trim();
|
|
12563
|
+
if (!rel) throw new Error("\u6587\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
|
|
12564
|
+
const file = resolveWithin(base, rel);
|
|
12565
|
+
const b64 = String(base64 ?? "").replace(/\s+/g, "");
|
|
12566
|
+
if (!b64) throw new Error("\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
|
|
12567
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) throw new Error("\u5185\u5BB9\u4E0D\u662F\u5408\u6CD5\u7684 base64");
|
|
12568
|
+
if (Math.floor(b64.length * 3 / 4) > MAX_BINARY_BYTES) {
|
|
12569
|
+
throw new Error(`\u5185\u5BB9\u8D85\u8FC7 ${MAX_BINARY_BYTES / (1024 * 1024)}MB \u4E0A\u9650\uFF0C\u65E0\u6CD5\u4FDD\u5B58`);
|
|
12570
|
+
}
|
|
12571
|
+
const buf = Buffer.from(b64, "base64");
|
|
12572
|
+
try {
|
|
12573
|
+
fs20.mkdirSync(path23.dirname(file), { recursive: true });
|
|
12574
|
+
fs20.writeFileSync(file, buf);
|
|
12575
|
+
} catch (e) {
|
|
12576
|
+
throw new Error(`\u5199\u5165\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
12577
|
+
}
|
|
12578
|
+
return { path: toPosix(path23.relative(base, file)), size: buf.length };
|
|
12579
|
+
}
|
|
11931
12580
|
function createProjectDir(workspaceRoot, relPath) {
|
|
11932
12581
|
const base = projectRoot(workspaceRoot);
|
|
11933
12582
|
const rel = (relPath ?? "").trim();
|
|
@@ -12001,6 +12650,42 @@ function searchProjectFiles(workspaceRoot, query) {
|
|
|
12001
12650
|
walk(base, "");
|
|
12002
12651
|
return out;
|
|
12003
12652
|
}
|
|
12653
|
+
function deleteProjectPaths(workspaceRoot, relPaths) {
|
|
12654
|
+
const base = projectRoot(workspaceRoot);
|
|
12655
|
+
const out = { deleted: [], failed: [] };
|
|
12656
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12657
|
+
for (const raw of relPaths ?? []) {
|
|
12658
|
+
const rel = (raw ?? "").trim().replace(/^[/\\]+/, "");
|
|
12659
|
+
if (!rel) {
|
|
12660
|
+
out.failed.push({ path: String(raw ?? ""), error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
12661
|
+
continue;
|
|
12662
|
+
}
|
|
12663
|
+
if (seen.has(rel)) continue;
|
|
12664
|
+
seen.add(rel);
|
|
12665
|
+
let abs;
|
|
12666
|
+
try {
|
|
12667
|
+
abs = resolveWithin(base, rel);
|
|
12668
|
+
} catch (e) {
|
|
12669
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
12670
|
+
continue;
|
|
12671
|
+
}
|
|
12672
|
+
if (path23.resolve(abs) === path23.resolve(base)) {
|
|
12673
|
+
out.failed.push({ path: rel, error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
12674
|
+
continue;
|
|
12675
|
+
}
|
|
12676
|
+
if (!fs20.existsSync(abs)) {
|
|
12677
|
+
out.failed.push({ path: rel, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
|
|
12678
|
+
continue;
|
|
12679
|
+
}
|
|
12680
|
+
try {
|
|
12681
|
+
fs20.rmSync(abs, { recursive: true, force: true });
|
|
12682
|
+
out.deleted.push(toPosix(path23.relative(base, abs)));
|
|
12683
|
+
} catch (e) {
|
|
12684
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
12685
|
+
}
|
|
12686
|
+
}
|
|
12687
|
+
return out;
|
|
12688
|
+
}
|
|
12004
12689
|
|
|
12005
12690
|
// src/server/projectOps.ts
|
|
12006
12691
|
init_cjs_shims();
|
|
@@ -12406,7 +13091,7 @@ function buildNodeHost(store, manager, diskPath) {
|
|
|
12406
13091
|
arch: process.arch,
|
|
12407
13092
|
release: os6.release(),
|
|
12408
13093
|
uptimeSec: Math.round(os6.uptime()),
|
|
12409
|
-
version: "0.2.
|
|
13094
|
+
version: "0.2.16",
|
|
12410
13095
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
12411
13096
|
cpu: {
|
|
12412
13097
|
count: cpus2.length,
|
|
@@ -12549,6 +13234,7 @@ function parseApprovalDecision(raw) {
|
|
|
12549
13234
|
}
|
|
12550
13235
|
function createApp(store, manager, opts = {}) {
|
|
12551
13236
|
setupServerHandlers(manager);
|
|
13237
|
+
const updater = opts.updater ?? null;
|
|
12552
13238
|
const routes = [
|
|
12553
13239
|
// ---------- 会话管理 ----------
|
|
12554
13240
|
{
|
|
@@ -12690,6 +13376,18 @@ function createApp(store, manager, opts = {}) {
|
|
|
12690
13376
|
ok(res, { accepted: true });
|
|
12691
13377
|
}
|
|
12692
13378
|
},
|
|
13379
|
+
{
|
|
13380
|
+
method: "POST",
|
|
13381
|
+
pattern: ["sessions", ":id", "steer", "remove"],
|
|
13382
|
+
// 撤销未注入的补充信息
|
|
13383
|
+
fn: (_req, res, { tenant, params, body }) => {
|
|
13384
|
+
const index = Number(body?.index);
|
|
13385
|
+
if (!Number.isInteger(index))
|
|
13386
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
13387
|
+
const pendingSteers = manager.unsteer(tenant, params["id"], index);
|
|
13388
|
+
ok(res, { accepted: true, pendingSteers });
|
|
13389
|
+
}
|
|
13390
|
+
},
|
|
12693
13391
|
{
|
|
12694
13392
|
method: "POST",
|
|
12695
13393
|
pattern: ["sessions", ":id", "interrupt"],
|
|
@@ -13220,6 +13918,30 @@ function createApp(store, manager, opts = {}) {
|
|
|
13220
13918
|
}
|
|
13221
13919
|
}
|
|
13222
13920
|
},
|
|
13921
|
+
{
|
|
13922
|
+
// 二进制写入(图片编辑保存):body = { path, base64 }
|
|
13923
|
+
method: "POST",
|
|
13924
|
+
pattern: ["files", "binary"],
|
|
13925
|
+
fn: (_req, res, { tenant, body }) => {
|
|
13926
|
+
const raw = body ?? {};
|
|
13927
|
+
try {
|
|
13928
|
+
ok(
|
|
13929
|
+
res,
|
|
13930
|
+
writeProjectFileBase64(
|
|
13931
|
+
tenant.workspaceRoot ?? null,
|
|
13932
|
+
String(raw.path ?? ""),
|
|
13933
|
+
String(raw.base64 ?? "")
|
|
13934
|
+
)
|
|
13935
|
+
);
|
|
13936
|
+
} catch (e) {
|
|
13937
|
+
throw new ApiError(
|
|
13938
|
+
400,
|
|
13939
|
+
"bad_request",
|
|
13940
|
+
e instanceof Error ? e.message : String(e)
|
|
13941
|
+
);
|
|
13942
|
+
}
|
|
13943
|
+
}
|
|
13944
|
+
},
|
|
13223
13945
|
// ---------- 项目终端(在工作区执行命令) ----------
|
|
13224
13946
|
{
|
|
13225
13947
|
method: "POST",
|
|
@@ -13570,6 +14292,21 @@ function createApp(store, manager, opts = {}) {
|
|
|
13570
14292
|
}
|
|
13571
14293
|
}
|
|
13572
14294
|
},
|
|
14295
|
+
{
|
|
14296
|
+
method: "POST",
|
|
14297
|
+
pattern: ["files", "delete"],
|
|
14298
|
+
// 删除文件 / 目录(目录递归);逐条返回失败原因
|
|
14299
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14300
|
+
const raw = body ?? {};
|
|
14301
|
+
const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
|
|
14302
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
14303
|
+
try {
|
|
14304
|
+
ok(res, deleteProjectPaths(tenant.workspaceRoot ?? null, paths));
|
|
14305
|
+
} catch (e) {
|
|
14306
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
14307
|
+
}
|
|
14308
|
+
}
|
|
14309
|
+
},
|
|
13573
14310
|
{
|
|
13574
14311
|
method: "POST",
|
|
13575
14312
|
pattern: ["files", "create"],
|
|
@@ -13798,6 +14535,34 @@ function createApp(store, manager, opts = {}) {
|
|
|
13798
14535
|
}
|
|
13799
14536
|
}
|
|
13800
14537
|
},
|
|
14538
|
+
// ---------- 节点自更新(状态 / 检查 / 安装) ----------
|
|
14539
|
+
// action=install 先回执、后台安装并退出进程(由服务管理器拉起新版本),
|
|
14540
|
+
// 因此回执成功只代表「已受理」,是否升级成功要用 action=status 再确认。
|
|
14541
|
+
{
|
|
14542
|
+
method: "POST",
|
|
14543
|
+
pattern: ["node", "self-update"],
|
|
14544
|
+
projectOptional: true,
|
|
14545
|
+
fn: async (req2, res, { body }) => {
|
|
14546
|
+
const b = body ?? {};
|
|
14547
|
+
const action = String(b.action ?? "status");
|
|
14548
|
+
if (!updater) {
|
|
14549
|
+
throw new ApiError(400, "bad_request", "\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A\u8282\u70B9\u672A\u4EE5 serve \u6A21\u5F0F\u8FD0\u884C");
|
|
14550
|
+
}
|
|
14551
|
+
if (action === "status") {
|
|
14552
|
+
ok(res, updater.status());
|
|
14553
|
+
return;
|
|
14554
|
+
}
|
|
14555
|
+
if (action === "check") {
|
|
14556
|
+
ok(res, await updater.inspect());
|
|
14557
|
+
return;
|
|
14558
|
+
}
|
|
14559
|
+
if (action === "install") {
|
|
14560
|
+
ok(res, await updater.requestInstall(b.force === true));
|
|
14561
|
+
return;
|
|
14562
|
+
}
|
|
14563
|
+
throw new ApiError(400, "bad_request", `\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
14564
|
+
}
|
|
14565
|
+
},
|
|
13801
14566
|
// ---------- 节点级会话状态(跨项目;只含标识与 status) ----------
|
|
13802
14567
|
// 供节点首页 / 工作台建立初始视图,后续变化由 session_status 通知增量推送。
|
|
13803
14568
|
{
|
|
@@ -13960,7 +14725,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
13960
14725
|
// 阻止反向代理(nginx 等)缓冲 SSE 流
|
|
13961
14726
|
"X-Accel-Buffering": "no"
|
|
13962
14727
|
});
|
|
13963
|
-
const
|
|
14728
|
+
const sinceRaw2 = new URL(
|
|
13964
14729
|
req2.url,
|
|
13965
14730
|
"http://localhost"
|
|
13966
14731
|
).searchParams.get("since");
|
|
@@ -13968,19 +14733,51 @@ function createApp(store, manager, opts = {}) {
|
|
|
13968
14733
|
tenant.id,
|
|
13969
14734
|
params["id"],
|
|
13970
14735
|
res,
|
|
13971
|
-
|
|
14736
|
+
sinceRaw2 ? Number(sinceRaw2) : 0
|
|
13972
14737
|
);
|
|
13973
14738
|
return;
|
|
13974
14739
|
}
|
|
13975
|
-
const
|
|
13976
|
-
|
|
14740
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
14741
|
+
const sinceRaw = q.get("since");
|
|
14742
|
+
const beforeRaw = q.get("before");
|
|
14743
|
+
const maxBytesRaw = q.get("maxBytes");
|
|
14744
|
+
ok(
|
|
14745
|
+
res,
|
|
14746
|
+
manager.eventsPage(tenant.id, params["id"], {
|
|
14747
|
+
since: sinceRaw === null ? void 0 : Number(sinceRaw) || 0,
|
|
14748
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
14749
|
+
maxBytes: maxBytesRaw === null ? void 0 : Number(maxBytesRaw) || void 0
|
|
14750
|
+
})
|
|
13977
14751
|
);
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
14752
|
+
}
|
|
14753
|
+
},
|
|
14754
|
+
{
|
|
14755
|
+
method: "GET",
|
|
14756
|
+
pattern: ["sessions", ":id", "turns"],
|
|
14757
|
+
// 轮次汇总(会话详情的列表)
|
|
14758
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
14759
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
14760
|
+
const beforeRaw = q.get("before");
|
|
14761
|
+
const limitRaw = q.get("limit");
|
|
14762
|
+
ok(
|
|
14763
|
+
res,
|
|
14764
|
+
await manager.sessionTurns(tenant.id, params["id"], {
|
|
14765
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
14766
|
+
limit: limitRaw === null ? void 0 : Number(limitRaw) || void 0
|
|
14767
|
+
})
|
|
13982
14768
|
);
|
|
13983
|
-
|
|
14769
|
+
}
|
|
14770
|
+
},
|
|
14771
|
+
{
|
|
14772
|
+
method: "GET",
|
|
14773
|
+
// 单轮详情:用户消息(含补充) + 最终回复 + 产出文件
|
|
14774
|
+
pattern: ["sessions", ":id", "turns", ":seq", "reply"],
|
|
14775
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
14776
|
+
const startSeq = Number(params["seq"]);
|
|
14777
|
+
if (!Number.isFinite(startSeq) || startSeq <= 0) {
|
|
14778
|
+
throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
14779
|
+
}
|
|
14780
|
+
ok(res, await manager.sessionTurnReply(tenant.id, params["id"], startSeq));
|
|
13984
14781
|
}
|
|
13985
14782
|
}
|
|
13986
14783
|
];
|
|
@@ -14013,7 +14810,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
14013
14810
|
json(res, 200, {
|
|
14014
14811
|
status: "ok",
|
|
14015
14812
|
// 版本让运维/探针能确认线上跑的是哪个构建(更新与回滚据此判断)
|
|
14016
|
-
version: "0.2.
|
|
14813
|
+
version: "0.2.16",
|
|
14017
14814
|
features: { mcp: hasServerMcpConfig() }
|
|
14018
14815
|
});
|
|
14019
14816
|
return;
|
|
@@ -14286,6 +15083,24 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14286
15083
|
switch (env3.type) {
|
|
14287
15084
|
case "ping":
|
|
14288
15085
|
return ok2({ pong: true, ts: Date.now() });
|
|
15086
|
+
case "self_update": {
|
|
15087
|
+
const updater = deps.getUpdater?.() ?? null;
|
|
15088
|
+
if (!updater) {
|
|
15089
|
+
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");
|
|
15090
|
+
}
|
|
15091
|
+
const action = String(payload.action ?? "status");
|
|
15092
|
+
if (action === "status") {
|
|
15093
|
+
return ok2(updater.status());
|
|
15094
|
+
}
|
|
15095
|
+
if (action === "check") {
|
|
15096
|
+
return ok2(await updater.inspect());
|
|
15097
|
+
}
|
|
15098
|
+
if (action === "install") {
|
|
15099
|
+
const started = await updater.requestInstall(payload.force === true);
|
|
15100
|
+
return ok2(started);
|
|
15101
|
+
}
|
|
15102
|
+
return fail(`\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
15103
|
+
}
|
|
14289
15104
|
case "get_node_info":
|
|
14290
15105
|
return ok2(
|
|
14291
15106
|
buildNodeInfo(store, {
|
|
@@ -14408,8 +15223,22 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14408
15223
|
);
|
|
14409
15224
|
}
|
|
14410
15225
|
case "get_events": {
|
|
14411
|
-
const since =
|
|
14412
|
-
|
|
15226
|
+
const since = payload.since === void 0 ? void 0 : Number(payload.since) || 0;
|
|
15227
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
15228
|
+
const maxBytes = Number(payload.maxBytes) || void 0;
|
|
15229
|
+
return ok2(
|
|
15230
|
+
manager.eventsPage(project().id, sessionId(), { since, before, maxBytes })
|
|
15231
|
+
);
|
|
15232
|
+
}
|
|
15233
|
+
case "get_session_turns": {
|
|
15234
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
15235
|
+
const limit = Number(payload.limit) || void 0;
|
|
15236
|
+
return ok2(await manager.sessionTurns(project().id, sessionId(), { before, limit }));
|
|
15237
|
+
}
|
|
15238
|
+
case "get_session_turn_reply": {
|
|
15239
|
+
const startSeq = Number(payload.startSeq) || 0;
|
|
15240
|
+
if (startSeq <= 0) throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
15241
|
+
return ok2(await manager.sessionTurnReply(project().id, sessionId(), startSeq));
|
|
14413
15242
|
}
|
|
14414
15243
|
case "get_config":
|
|
14415
15244
|
return ok2(configView2(project()));
|
|
@@ -14470,6 +15299,16 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14470
15299
|
)
|
|
14471
15300
|
);
|
|
14472
15301
|
}
|
|
15302
|
+
case "write_file_base64": {
|
|
15303
|
+
const t = project();
|
|
15304
|
+
return ok2(
|
|
15305
|
+
writeProjectFileBase64(
|
|
15306
|
+
t.workspaceRoot ?? null,
|
|
15307
|
+
String(payload.path ?? ""),
|
|
15308
|
+
String(payload.base64 ?? "")
|
|
15309
|
+
)
|
|
15310
|
+
);
|
|
15311
|
+
}
|
|
14473
15312
|
case "create_dir": {
|
|
14474
15313
|
const t = project();
|
|
14475
15314
|
return ok2(createProjectDir(t.workspaceRoot ?? null, String(payload.path ?? "")));
|
|
@@ -14484,6 +15323,13 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14484
15323
|
)
|
|
14485
15324
|
);
|
|
14486
15325
|
}
|
|
15326
|
+
case "delete_files": {
|
|
15327
|
+
const t = project();
|
|
15328
|
+
const raw = payload.paths;
|
|
15329
|
+
const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
|
|
15330
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
15331
|
+
return ok2(deleteProjectPaths(t.workspaceRoot ?? null, paths));
|
|
15332
|
+
}
|
|
14487
15333
|
case "search_files": {
|
|
14488
15334
|
const t = project();
|
|
14489
15335
|
return ok2(searchProjectFiles(t.workspaceRoot ?? null, String(payload.query ?? "")));
|
|
@@ -14710,6 +15556,13 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14710
15556
|
manager.steer(project(), sessionId(), text);
|
|
14711
15557
|
return ok2({ accepted: true });
|
|
14712
15558
|
}
|
|
15559
|
+
case "unsteer": {
|
|
15560
|
+
const index = Number(payload.index);
|
|
15561
|
+
if (!Number.isInteger(index)) {
|
|
15562
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
15563
|
+
}
|
|
15564
|
+
return ok2({ accepted: true, pendingSteers: manager.unsteer(project(), sessionId(), index) });
|
|
15565
|
+
}
|
|
14713
15566
|
case "interrupt":
|
|
14714
15567
|
manager.interrupt(project(), sessionId());
|
|
14715
15568
|
return ok2({ accepted: true });
|
|
@@ -15310,6 +16163,8 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
|
|
|
15310
16163
|
"get_session_states",
|
|
15311
16164
|
"get_state",
|
|
15312
16165
|
"get_events",
|
|
16166
|
+
"get_session_turns",
|
|
16167
|
+
"get_session_turn_reply",
|
|
15313
16168
|
"get_config",
|
|
15314
16169
|
"list_models",
|
|
15315
16170
|
"list_dirs",
|
|
@@ -15559,6 +16414,10 @@ var FleetNodeBridge = class {
|
|
|
15559
16414
|
_dispatchEnvelope(env3) {
|
|
15560
16415
|
const run = () => this._dispatch(env3);
|
|
15561
16416
|
dbgMark(`recv:${env3.type}`, `reqId=${env3.reqId}`);
|
|
16417
|
+
if (env3.type === "self_update" && String(env3.payload?.action ?? "status") !== "install") {
|
|
16418
|
+
this._runReadonly(run);
|
|
16419
|
+
return;
|
|
16420
|
+
}
|
|
15562
16421
|
if (READONLY_CMD_TYPES.has(env3.type) || this._isReadonlyBatch(env3)) {
|
|
15563
16422
|
this._runReadonly(run);
|
|
15564
16423
|
return;
|
|
@@ -15585,7 +16444,7 @@ var FleetNodeBridge = class {
|
|
|
15585
16444
|
_invalidateSessionCache() {
|
|
15586
16445
|
this.writeEpoch += 1;
|
|
15587
16446
|
for (const key of [...this.cmdResultCache.keys()]) {
|
|
15588
|
-
if (/^(get_state|get_events|list_sessions|get_session_states)\|/.test(key)) {
|
|
16447
|
+
if (/^(get_state|get_events|get_session_turns|get_session_turn_reply|list_sessions|get_session_states)\|/.test(key)) {
|
|
15589
16448
|
this.cmdResultCache.delete(key);
|
|
15590
16449
|
}
|
|
15591
16450
|
}
|
|
@@ -15721,11 +16580,17 @@ var FleetNodeBridge = class {
|
|
|
15721
16580
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
15722
16581
|
platform: process.platform,
|
|
15723
16582
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15724
|
-
version: "0.2.
|
|
16583
|
+
version: "0.2.16",
|
|
15725
16584
|
ip: primaryIp()
|
|
15726
16585
|
};
|
|
15727
16586
|
}
|
|
15728
|
-
/**
|
|
16587
|
+
/**
|
|
16588
|
+
* 用量汇总(供控制面账户级聚合):总量 + 最近 40 天按天 + 最近 40 天按项目。
|
|
16589
|
+
*
|
|
16590
|
+
* 按项目这一维是为控制台「今日 / 最近 30 天消耗拆到每个项目」服务的:
|
|
16591
|
+
* 账本里的汇总只存了按天、按项目两个独立维度,交叉项只能现算,因此这里
|
|
16592
|
+
* 从明细聚合后随 info 一起上报(Hub 按 (节点, 项目, 天) 覆盖写入,天然幂等)。
|
|
16593
|
+
*/
|
|
15729
16594
|
_usagePayload() {
|
|
15730
16595
|
const ledger = this.opts.deps.manager.usageLedger();
|
|
15731
16596
|
if (!ledger) return void 0;
|
|
@@ -15733,11 +16598,22 @@ var FleetNodeBridge = class {
|
|
|
15733
16598
|
const days = {};
|
|
15734
16599
|
const keys = Object.keys(r.byDay).sort().slice(-40);
|
|
15735
16600
|
for (const k of keys) days[k] = r.byDay[k];
|
|
16601
|
+
const from = keys[0] ?? "";
|
|
16602
|
+
const to = keys[keys.length - 1] ?? "";
|
|
16603
|
+
const projects = {};
|
|
16604
|
+
if (from && to) {
|
|
16605
|
+
for (const [projectId, byDay] of Object.entries(
|
|
16606
|
+
ledger.projectByDay({ from, to })
|
|
16607
|
+
)) {
|
|
16608
|
+
projects[projectId] = { total: sumAgg(Object.values(byDay)), byDay };
|
|
16609
|
+
}
|
|
16610
|
+
}
|
|
15736
16611
|
return {
|
|
15737
16612
|
since: r.since,
|
|
15738
16613
|
total: r.total,
|
|
15739
16614
|
legacy: r.legacy ?? null,
|
|
15740
|
-
byDay: days
|
|
16615
|
+
byDay: days,
|
|
16616
|
+
projects
|
|
15741
16617
|
};
|
|
15742
16618
|
}
|
|
15743
16619
|
/**
|
|
@@ -15772,6 +16648,8 @@ var FleetNodeBridge = class {
|
|
|
15772
16648
|
name: s.name ?? null,
|
|
15773
16649
|
status: s.status,
|
|
15774
16650
|
lastUserMessage: lastUserMessage(s),
|
|
16651
|
+
// 控制面按它排「最近在跑什么」,运行中的长任务才不会被排到后面
|
|
16652
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
15775
16653
|
// 控制面的聚合邮件靠它判断「这台机器最近有没有在干活」;
|
|
15776
16654
|
// 状态跃迁可能被 info 节流跳过,完成时间不会。
|
|
15777
16655
|
lastCompletedAt: s.lastCompletedAt ?? null
|
|
@@ -15800,7 +16678,7 @@ var FleetNodeBridge = class {
|
|
|
15800
16678
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
15801
16679
|
platform: process.platform,
|
|
15802
16680
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15803
|
-
version: "0.2.
|
|
16681
|
+
version: "0.2.16",
|
|
15804
16682
|
ip: primaryIp(),
|
|
15805
16683
|
projects,
|
|
15806
16684
|
totals,
|
|
@@ -17429,7 +18307,7 @@ function verifySignature(data, signatureBase64, publicKey) {
|
|
|
17429
18307
|
}
|
|
17430
18308
|
|
|
17431
18309
|
// src/update/downloader.ts
|
|
17432
|
-
var
|
|
18310
|
+
var MAX_BINARY_BYTES2 = 400 * 1024 * 1024;
|
|
17433
18311
|
var MANIFEST_TIMEOUT_MS = 15e3;
|
|
17434
18312
|
var DOWNLOAD_TIMEOUT_MS2 = 10 * 6e4;
|
|
17435
18313
|
async function fetchManifest(url, publicKey, opts = {}) {
|
|
@@ -17475,7 +18353,7 @@ async function downloadBinary(bin, destPath, log = () => {
|
|
|
17475
18353
|
if (!res.ok) return { error: `\u4E0B\u8F7D\u5931\u8D25 HTTP ${res.status}` };
|
|
17476
18354
|
if (!res.body) return { error: "\u4E0B\u8F7D\u54CD\u5E94\u6CA1\u6709 body" };
|
|
17477
18355
|
const expected = bin.size && bin.size > 0 ? bin.size : void 0;
|
|
17478
|
-
const limit = expected ? Math.max(expected * 2, 1024 * 1024) :
|
|
18356
|
+
const limit = expected ? Math.max(expected * 2, 1024 * 1024) : MAX_BINARY_BYTES2;
|
|
17479
18357
|
const hash = (0, import_node_crypto5.createHash)("sha256");
|
|
17480
18358
|
let bytes = 0;
|
|
17481
18359
|
try {
|
|
@@ -17538,7 +18416,7 @@ var path29 = __toESM(require("path"), 1);
|
|
|
17538
18416
|
var import_node_child_process5 = require("child_process");
|
|
17539
18417
|
var import_node_module3 = require("module");
|
|
17540
18418
|
function currentVersion() {
|
|
17541
|
-
const injected = "0.2.
|
|
18419
|
+
const injected = "0.2.16"?.trim();
|
|
17542
18420
|
if (injected) return injected;
|
|
17543
18421
|
try {
|
|
17544
18422
|
const req2 = (0, import_node_module3.createRequire)(importMetaUrl);
|
|
@@ -17852,6 +18730,31 @@ function nextIntervalMs() {
|
|
|
17852
18730
|
const jitter = base * 0.15 * (Math.random() * 2 - 1);
|
|
17853
18731
|
return Math.max(6e4, Math.round(base + jitter));
|
|
17854
18732
|
}
|
|
18733
|
+
function evaluateManifest(base, manifest) {
|
|
18734
|
+
if (!parseVersion(base.currentVersion) || compareVersions(manifest.version, base.currentVersion) <= 0) {
|
|
18735
|
+
return {
|
|
18736
|
+
...base,
|
|
18737
|
+
updateAvailable: false,
|
|
18738
|
+
latestVersion: manifest.version,
|
|
18739
|
+
reason: `\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF08v${base.currentVersion}\uFF09`
|
|
18740
|
+
};
|
|
18741
|
+
}
|
|
18742
|
+
if (manifest.minimumVersion && compareVersions(base.currentVersion, manifest.minimumVersion) < 0) {
|
|
18743
|
+
return {
|
|
18744
|
+
...base,
|
|
18745
|
+
updateAvailable: false,
|
|
18746
|
+
latestVersion: manifest.version,
|
|
18747
|
+
manualUpgradeRequired: manifest.minimumVersion,
|
|
18748
|
+
reason: `\u5F53\u524D\u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u624B\u52A8\u5347\u7EA7\u5230 >= ${manifest.minimumVersion}`
|
|
18749
|
+
};
|
|
18750
|
+
}
|
|
18751
|
+
return {
|
|
18752
|
+
...base,
|
|
18753
|
+
updateAvailable: true,
|
|
18754
|
+
latestVersion: manifest.version,
|
|
18755
|
+
...manifest.notes ? { notes: manifest.notes } : {}
|
|
18756
|
+
};
|
|
18757
|
+
}
|
|
17855
18758
|
var UpdateController = class {
|
|
17856
18759
|
constructor(opts, currentVersion2 = currentVersion()) {
|
|
17857
18760
|
this.opts = opts;
|
|
@@ -17940,6 +18843,77 @@ var UpdateController = class {
|
|
|
17940
18843
|
}
|
|
17941
18844
|
this.schedule(nextIntervalMs());
|
|
17942
18845
|
}
|
|
18846
|
+
/**
|
|
18847
|
+
* 只读检查:拉清单比对版本,不下载、不改 state。
|
|
18848
|
+
*
|
|
18849
|
+
* 供远端 `self_update --action check` 与 UI 展示用;与定时器 `check()` 不同,
|
|
18850
|
+
* 这里不加 `checking` 锁、不写 `lastCheckAt`,因此不会干扰自动更新流程。
|
|
18851
|
+
*/
|
|
18852
|
+
async inspect() {
|
|
18853
|
+
const base = {
|
|
18854
|
+
currentVersion: this.currentVersion,
|
|
18855
|
+
channel: updaterChannel(),
|
|
18856
|
+
manifestUrl: this.manifestUrl()
|
|
18857
|
+
};
|
|
18858
|
+
const key = this.publicKey;
|
|
18859
|
+
const cap = selfUpdateCapability(Boolean(key));
|
|
18860
|
+
if (!cap.ok) {
|
|
18861
|
+
return { ...base, updateAvailable: false, reason: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
18862
|
+
}
|
|
18863
|
+
const res = await fetchManifest(base.manifestUrl, key, {
|
|
18864
|
+
allowInsecureUrl: env("ZQAGENT_UPDATE_INSECURE") === "1"
|
|
18865
|
+
});
|
|
18866
|
+
if (!res.manifest) {
|
|
18867
|
+
return {
|
|
18868
|
+
...base,
|
|
18869
|
+
updateAvailable: false,
|
|
18870
|
+
reason: `\u66F4\u65B0\u68C0\u67E5\u5931\u8D25\uFF1A${res.error ?? "\u6E05\u5355\u6821\u9A8C\u5931\u8D25"}`
|
|
18871
|
+
};
|
|
18872
|
+
}
|
|
18873
|
+
return evaluateManifest(base, res.manifest);
|
|
18874
|
+
}
|
|
18875
|
+
/**
|
|
18876
|
+
* 远端触发安装:先同步给出结论(受理/无需更新),安装本身在后台跑。
|
|
18877
|
+
*
|
|
18878
|
+
* 必须「先回执、后安装」:安装成功后进程会 `exit(70)` 交给 systemd 拉起新版本,
|
|
18879
|
+
* 如果等到安装完成再回执,回执永远发不出去(表现为网关超时)。
|
|
18880
|
+
*
|
|
18881
|
+
* 复用定时器同一条 `check()` 路径:验签、下载、自检、失败计数、安装后自检与
|
|
18882
|
+
* 回滚保护全都一致,只是把 `installMode` 设为 `force` 时不等待空闲。
|
|
18883
|
+
*/
|
|
18884
|
+
async requestInstall(force = false) {
|
|
18885
|
+
const fromVersion = this.currentVersion;
|
|
18886
|
+
const cap = selfUpdateCapability(Boolean(this.publicKey));
|
|
18887
|
+
if (!cap.ok) {
|
|
18888
|
+
return { accepted: false, fromVersion, message: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
18889
|
+
}
|
|
18890
|
+
const info = await this.inspect();
|
|
18891
|
+
if (!info.updateAvailable) {
|
|
18892
|
+
return {
|
|
18893
|
+
accepted: false,
|
|
18894
|
+
fromVersion,
|
|
18895
|
+
...info.latestVersion ? { toVersion: info.latestVersion } : {},
|
|
18896
|
+
message: info.reason ?? "\u5DF2\u662F\u6700\u65B0\u7248\u672C"
|
|
18897
|
+
};
|
|
18898
|
+
}
|
|
18899
|
+
if (this.checking || this.installing) {
|
|
18900
|
+
return {
|
|
18901
|
+
accepted: true,
|
|
18902
|
+
fromVersion,
|
|
18903
|
+
toVersion: info.latestVersion,
|
|
18904
|
+
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`
|
|
18905
|
+
};
|
|
18906
|
+
}
|
|
18907
|
+
void this.check(force, false, true).catch((err) => {
|
|
18908
|
+
this.recordFailure(info.latestVersion, `\u5B89\u88C5\u5931\u8D25\uFF1A${err.message}`);
|
|
18909
|
+
});
|
|
18910
|
+
return {
|
|
18911
|
+
accepted: true,
|
|
18912
|
+
fromVersion,
|
|
18913
|
+
toVersion: info.latestVersion,
|
|
18914
|
+
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`
|
|
18915
|
+
};
|
|
18916
|
+
}
|
|
17943
18917
|
/**
|
|
17944
18918
|
* 检查并准备更新;`installMode` 为 true 时下载后立即安装(不再等空闲)。
|
|
17945
18919
|
* 返回可读结论,供 CLI `update --check` 使用。
|
|
@@ -18339,6 +19313,7 @@ function startServer(opts = {}) {
|
|
|
18339
19313
|
const notificationHub = createNotificationHub();
|
|
18340
19314
|
let fleet = null;
|
|
18341
19315
|
let fleetCred = null;
|
|
19316
|
+
let updater = null;
|
|
18342
19317
|
function connectFleet(cred) {
|
|
18343
19318
|
const localId = ensureNodeId(store);
|
|
18344
19319
|
const nodeId = (cred.nodeId ?? "").trim() || localId;
|
|
@@ -18356,6 +19331,8 @@ function startServer(opts = {}) {
|
|
|
18356
19331
|
deps: {
|
|
18357
19332
|
store,
|
|
18358
19333
|
manager,
|
|
19334
|
+
// 闭包推迟求值:桥先于 UpdateController 创建,指令到达时 updater 已就绪
|
|
19335
|
+
getUpdater: () => updater,
|
|
18359
19336
|
applyFleetCred: async (cred2) => {
|
|
18360
19337
|
store.saveFleetCredential(cred2);
|
|
18361
19338
|
setTimeout(() => connectFleet(cred2), 300);
|
|
@@ -18506,16 +19483,21 @@ function startServer(opts = {}) {
|
|
|
18506
19483
|
scheduleFleetInfoRefresh();
|
|
18507
19484
|
}
|
|
18508
19485
|
});
|
|
19486
|
+
updater = new UpdateController({
|
|
19487
|
+
dataDir,
|
|
19488
|
+
isIdle: () => manager.isIdle(),
|
|
19489
|
+
prepareExit: closeServer
|
|
19490
|
+
});
|
|
18509
19491
|
const server = createApp(store, manager, {
|
|
18510
19492
|
secretBox,
|
|
18511
19493
|
notificationHub,
|
|
18512
19494
|
qq,
|
|
18513
19495
|
adminKey,
|
|
18514
19496
|
fleet: fleetController,
|
|
18515
|
-
host
|
|
19497
|
+
host,
|
|
19498
|
+
updater
|
|
18516
19499
|
});
|
|
18517
19500
|
let closed = false;
|
|
18518
|
-
let updater = null;
|
|
18519
19501
|
function closeServer() {
|
|
18520
19502
|
if (closed) return;
|
|
18521
19503
|
closed = true;
|
|
@@ -18548,13 +19530,6 @@ function startServer(opts = {}) {
|
|
|
18548
19530
|
}
|
|
18549
19531
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
18550
19532
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
18551
|
-
if (opts.autoUpdate) {
|
|
18552
|
-
updater = new UpdateController({
|
|
18553
|
-
dataDir,
|
|
18554
|
-
isIdle: () => manager.isIdle(),
|
|
18555
|
-
prepareExit: closeServer
|
|
18556
|
-
});
|
|
18557
|
-
}
|
|
18558
19533
|
server.timeout = 0;
|
|
18559
19534
|
server.keepAliveTimeout = 65e3;
|
|
18560
19535
|
server.headersTimeout = 66e3;
|
|
@@ -18574,7 +19549,7 @@ function startServer(opts = {}) {
|
|
|
18574
19549
|
} else {
|
|
18575
19550
|
printReady(info, qq);
|
|
18576
19551
|
}
|
|
18577
|
-
updater?.start();
|
|
19552
|
+
if (opts.autoUpdate) updater?.start();
|
|
18578
19553
|
resolve20(info);
|
|
18579
19554
|
}, reject);
|
|
18580
19555
|
});
|
|
@@ -18615,7 +19590,7 @@ function printReady(info, qq) {
|
|
|
18615
19590
|
// src/cli/account-enroll.ts
|
|
18616
19591
|
init_cjs_shims();
|
|
18617
19592
|
var path34 = __toESM(require("path"), 1);
|
|
18618
|
-
var
|
|
19593
|
+
var readline2 = __toESM(require("readline/promises"), 1);
|
|
18619
19594
|
var SKIP_ANSWERS = /* @__PURE__ */ new Set(["-", "skip", "none", "no"]);
|
|
18620
19595
|
function resolveDefaultHub(hubUrl2, env3 = process.env) {
|
|
18621
19596
|
return (hubUrl2 ?? env3.HUB_URL ?? DEFAULT_HUB_URL).trim().replace(/\/$/, "");
|
|
@@ -18706,7 +19681,7 @@ async function maybeAccountEnroll(opts = {}) {
|
|
|
18706
19681
|
cliErr(`\u63A5\u5165\u68C0\u67E5\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
18707
19682
|
return;
|
|
18708
19683
|
}
|
|
18709
|
-
const rl =
|
|
19684
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
18710
19685
|
try {
|
|
18711
19686
|
await runWizard(rl, store, opts);
|
|
18712
19687
|
} catch (e) {
|
|
@@ -18787,7 +19762,7 @@ async function askHidden(rl, prompt) {
|
|
|
18787
19762
|
|
|
18788
19763
|
// src/cli/link.ts
|
|
18789
19764
|
init_cjs_shims();
|
|
18790
|
-
var
|
|
19765
|
+
var readline3 = __toESM(require("readline"), 1);
|
|
18791
19766
|
|
|
18792
19767
|
// src/cli/serve-instances.ts
|
|
18793
19768
|
init_cjs_shims();
|