@zqagent/core 0.2.15 → 0.2.20
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/README.oss.en.md +13 -0
- package/README.oss.md +11 -0
- package/README.oss.zh-TW.md +11 -0
- package/dist/cli/index.cjs +1832 -130
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1833 -131
- 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 +31 -1
- 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 +31 -1
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +1766 -105
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +1772 -111
- 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,8 +580,18 @@ 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
|
|
|
586
|
+
// src/server/llmIds.ts
|
|
587
|
+
init_cjs_shims();
|
|
588
|
+
var GLOBAL_PROVIDER_ID = "__env__";
|
|
589
|
+
var BUILTIN_PROVIDER_ID = "__zqai__";
|
|
590
|
+
function isBuiltinProvider(id) {
|
|
591
|
+
return id === BUILTIN_PROVIDER_ID;
|
|
592
|
+
}
|
|
593
|
+
var FALLBACK_MODEL = "gpt-4o";
|
|
594
|
+
|
|
585
595
|
// src/server/dbgTrace.ts
|
|
586
596
|
init_cjs_shims();
|
|
587
597
|
var DBG_FLEET = (() => {
|
|
@@ -1497,6 +1507,170 @@ var Store = class {
|
|
|
1497
1507
|
}
|
|
1498
1508
|
}
|
|
1499
1509
|
}
|
|
1510
|
+
/**
|
|
1511
|
+
* 读取事件「一页」:从文件末尾向前,按字节预算取最近的事件。
|
|
1512
|
+
*
|
|
1513
|
+
* - `before` 省略:取最新的 maxBytes 字节
|
|
1514
|
+
* - `before` 指定:只取 seq < before 的事件(向前翻页)
|
|
1515
|
+
*
|
|
1516
|
+
* 为什么不能复用 loadEventsTail:后者只能从文件末尾读固定窗口,
|
|
1517
|
+
* 无法「向前翻页」,而控制台需要滚到顶部继续加载更早的对话。
|
|
1518
|
+
* 这里按块从后往前扫,读到预算或文件头即停;翻了多页之后每次
|
|
1519
|
+
* before 都紧邻上一页,实际只需读 1~2 块。
|
|
1520
|
+
*
|
|
1521
|
+
* 返回按 seq 升序;`hasMore` 表示本页最早一条之前还有更早的事件。
|
|
1522
|
+
*/
|
|
1523
|
+
loadEventPage(tenantId, sessionId, opts) {
|
|
1524
|
+
const p = path4.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
1525
|
+
const { before, maxBytes } = opts;
|
|
1526
|
+
const __t0 = dbgNow();
|
|
1527
|
+
let fd = null;
|
|
1528
|
+
try {
|
|
1529
|
+
const size = fs3.statSync(p).size;
|
|
1530
|
+
if (size === 0) return { events: [], hasMore: false };
|
|
1531
|
+
fd = fs3.openSync(p, "r");
|
|
1532
|
+
const CHUNK = 256 * 1024;
|
|
1533
|
+
let pos = size;
|
|
1534
|
+
const kept = [];
|
|
1535
|
+
let bytes = 0;
|
|
1536
|
+
let hasMore = false;
|
|
1537
|
+
let done = false;
|
|
1538
|
+
while (pos > 0 && !done) {
|
|
1539
|
+
const start = Math.max(0, pos - CHUNK);
|
|
1540
|
+
const len = pos - start;
|
|
1541
|
+
const buf = Buffer.allocUnsafe(len);
|
|
1542
|
+
const read = fs3.readSync(fd, buf, 0, len, start);
|
|
1543
|
+
const rows = buf.subarray(0, read).toString("utf-8").split("\n");
|
|
1544
|
+
if (start > 0) rows.shift();
|
|
1545
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
1546
|
+
const line = rows[i];
|
|
1547
|
+
if (!line) continue;
|
|
1548
|
+
let entry;
|
|
1549
|
+
try {
|
|
1550
|
+
entry = JSON.parse(line);
|
|
1551
|
+
} catch {
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
if (!Number.isFinite(Number(entry.seq))) continue;
|
|
1555
|
+
if (before !== void 0 && Number(entry.seq) >= before) continue;
|
|
1556
|
+
const sz = Buffer.byteLength(line, "utf-8") + 1;
|
|
1557
|
+
if (bytes > 0 && bytes + sz > maxBytes) {
|
|
1558
|
+
hasMore = true;
|
|
1559
|
+
done = true;
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
bytes += sz;
|
|
1563
|
+
kept.unshift(entry);
|
|
1564
|
+
}
|
|
1565
|
+
pos = start;
|
|
1566
|
+
}
|
|
1567
|
+
dbgMs(
|
|
1568
|
+
"store.loadEventPage",
|
|
1569
|
+
__t0,
|
|
1570
|
+
// [dbg]
|
|
1571
|
+
`size=${size} before=${before ?? "-"} out=${kept.length} bytes\u2248${bytes} more=${hasMore}`
|
|
1572
|
+
);
|
|
1573
|
+
return { events: kept, hasMore };
|
|
1574
|
+
} catch {
|
|
1575
|
+
return { events: [], hasMore: false };
|
|
1576
|
+
} finally {
|
|
1577
|
+
if (fd !== null) {
|
|
1578
|
+
try {
|
|
1579
|
+
fs3.closeSync(fd);
|
|
1580
|
+
} catch {
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
// ===================== 轮次索引 =====================
|
|
1586
|
+
/** 轮次索引文件路径 */
|
|
1587
|
+
turnsFile(tenantId, sessionId) {
|
|
1588
|
+
return path4.join(this.sessionDir(tenantId, sessionId), "turns.ndjson");
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* 追加一轮的最新快照(同一 startSeq 后写者胜出)。
|
|
1592
|
+
*
|
|
1593
|
+
* 走与 session.json 相同的每文件写链:索引按「开轮 → 补充消息 → 收轮」
|
|
1594
|
+
* 顺序追加,乱序会让列表读到旧的 running 版本。
|
|
1595
|
+
*/
|
|
1596
|
+
appendTurnIndex(tenantId, sessionId, entry) {
|
|
1597
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
1598
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1599
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
1600
|
+
const line = JSON.stringify(entry) + "\n";
|
|
1601
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
1602
|
+
const next = prev.then(() => fsp.appendFile(file, line, "utf-8")).catch((err) => {
|
|
1603
|
+
console.error(`[store] appendTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
1604
|
+
});
|
|
1605
|
+
this.writeChains.set(file, next);
|
|
1606
|
+
return next;
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* 读取轮次索引(已按 startSeq 去重取最后一版,升序)。
|
|
1610
|
+
*
|
|
1611
|
+
* 文件很小(一轮约 0.5KB,1000 轮 ≈ 500KB),同步读即可,
|
|
1612
|
+
* 会话详情打开路径不会因历史长度变慢。
|
|
1613
|
+
*/
|
|
1614
|
+
loadTurnIndex(tenantId, sessionId) {
|
|
1615
|
+
let raw;
|
|
1616
|
+
try {
|
|
1617
|
+
raw = fs3.readFileSync(this.turnsFile(tenantId, sessionId), "utf-8");
|
|
1618
|
+
} catch {
|
|
1619
|
+
return [];
|
|
1620
|
+
}
|
|
1621
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
1622
|
+
for (const line of raw.split("\n")) {
|
|
1623
|
+
if (!line) continue;
|
|
1624
|
+
try {
|
|
1625
|
+
const e = JSON.parse(line);
|
|
1626
|
+
if (!Number.isFinite(Number(e?.startSeq))) continue;
|
|
1627
|
+
byStart.set(Number(e.startSeq), e);
|
|
1628
|
+
} catch {
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
1632
|
+
}
|
|
1633
|
+
/** 整文件替换轮次索引(重建用,tmp + rename 原子写) */
|
|
1634
|
+
saveTurnIndex(tenantId, sessionId, entries) {
|
|
1635
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
1636
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1637
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
1638
|
+
const snapshot = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
1639
|
+
const tmp = `${file}.${(0, import_crypto.randomUUID)()}.tmp`;
|
|
1640
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
1641
|
+
const next = prev.then(() => fsp.writeFile(tmp, snapshot, "utf-8")).then(() => fsp.rename(tmp, file)).catch((err) => {
|
|
1642
|
+
console.error(`[store] saveTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
1643
|
+
void fsp.rm(tmp, { force: true });
|
|
1644
|
+
});
|
|
1645
|
+
this.writeChains.set(file, next);
|
|
1646
|
+
return next;
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* 流式读取整份事件日志(重建索引用)。
|
|
1650
|
+
*
|
|
1651
|
+
* 不能用 loadEvents(readFileSync + 逐行 JSON.parse):大会话几十 MB,
|
|
1652
|
+
* 会长时间阻塞事件循环,导致 SSE 心跳 / MQTT 保活断开。
|
|
1653
|
+
*/
|
|
1654
|
+
async readEventsAsync(tenantId, sessionId) {
|
|
1655
|
+
const p = path4.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
1656
|
+
const out = [];
|
|
1657
|
+
try {
|
|
1658
|
+
const rl = readline.createInterface({
|
|
1659
|
+
input: fs3.createReadStream(p, { encoding: "utf-8" }),
|
|
1660
|
+
crlfDelay: Infinity
|
|
1661
|
+
});
|
|
1662
|
+
for await (const line of rl) {
|
|
1663
|
+
if (!line) continue;
|
|
1664
|
+
try {
|
|
1665
|
+
out.push(JSON.parse(line));
|
|
1666
|
+
} catch {
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
} catch {
|
|
1670
|
+
return out;
|
|
1671
|
+
}
|
|
1672
|
+
return out;
|
|
1673
|
+
}
|
|
1500
1674
|
// ===================== 节点身份 & 管理员密钥 =====================
|
|
1501
1675
|
/** 节点稳定标识:{root}/node.json(首次启动生成后长期固定) */
|
|
1502
1676
|
loadNodeId() {
|
|
@@ -1555,7 +1729,23 @@ var Store = class {
|
|
|
1555
1729
|
legacyFiles: [path4.join(this.root, "node-llm.json")]
|
|
1556
1730
|
});
|
|
1557
1731
|
const old = new Map(cur.providers.map((p) => [p.id, p]));
|
|
1558
|
-
const
|
|
1732
|
+
const src = [...cfg.providers];
|
|
1733
|
+
const prevBuiltin = cur.providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
|
|
1734
|
+
if (prevBuiltin && !src.some((p) => p.id === BUILTIN_PROVIDER_ID)) {
|
|
1735
|
+
src.unshift({
|
|
1736
|
+
id: prevBuiltin.id,
|
|
1737
|
+
name: prevBuiltin.name,
|
|
1738
|
+
type: prevBuiltin.type,
|
|
1739
|
+
baseURL: prevBuiltin.baseURL,
|
|
1740
|
+
model: prevBuiltin.model,
|
|
1741
|
+
apiKey: decryptKey(prevBuiltin.apiKeyEnc, file) ?? void 0,
|
|
1742
|
+
reasoningEffort: prevBuiltin.reasoningEffort ?? null,
|
|
1743
|
+
...prevBuiltin.models ? { models: prevBuiltin.models } : {},
|
|
1744
|
+
...prevBuiltin.nonChat ? { nonChat: prevBuiltin.nonChat } : {},
|
|
1745
|
+
...prevBuiltin.modelMeta ? { modelMeta: prevBuiltin.modelMeta } : {}
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
const providers = src.map((p) => {
|
|
1559
1749
|
const prev = old.get(p.id);
|
|
1560
1750
|
const apiKeyEnc = p.apiKey ? encryptKey(p.apiKey, file) : prev?.apiKeyEnc ?? "";
|
|
1561
1751
|
return {
|
|
@@ -1917,13 +2107,13 @@ function textOf(content) {
|
|
|
1917
2107
|
}
|
|
1918
2108
|
return content != null ? String(content) : "";
|
|
1919
2109
|
}
|
|
1920
|
-
function extractUserMessages(span,
|
|
2110
|
+
function extractUserMessages(span, clip2 = USER_MESSAGE_CLIP) {
|
|
1921
2111
|
const out = [];
|
|
1922
2112
|
for (const m of span) {
|
|
1923
2113
|
if (m.role !== "user") continue;
|
|
1924
2114
|
const text = textOf(m.content).replace(/\s+/g, " ").trim();
|
|
1925
2115
|
if (!text) continue;
|
|
1926
|
-
out.push(text.length >
|
|
2116
|
+
out.push(text.length > clip2 ? text.slice(0, clip2 - 1) + "\u2026" : text);
|
|
1927
2117
|
}
|
|
1928
2118
|
return out;
|
|
1929
2119
|
}
|
|
@@ -2735,6 +2925,21 @@ var TurnEngine = class _TurnEngine {
|
|
|
2735
2925
|
queueSteering(text) {
|
|
2736
2926
|
this._steering.push(text);
|
|
2737
2927
|
}
|
|
2928
|
+
/**
|
|
2929
|
+
* 撤销一条尚未注入的 steering。
|
|
2930
|
+
*
|
|
2931
|
+
* 按文本删第一条匹配项:调用方(SessionManager)已按索引删掉了自己那份队列,
|
|
2932
|
+
* 这里只需保证引擎不再注入同样的内容。用文本而非索引,是因为引擎会在迭代
|
|
2933
|
+
* 间隙批量清空 `_steering`,索引可能已经漂移;文本相同则删哪条都等价。
|
|
2934
|
+
*
|
|
2935
|
+
* @returns 是否真的删掉了(已注入的项返回 false,调用方据此提示用户)
|
|
2936
|
+
*/
|
|
2937
|
+
removeSteering(text) {
|
|
2938
|
+
const i = this._steering.indexOf(text);
|
|
2939
|
+
if (i < 0) return false;
|
|
2940
|
+
this._steering.splice(i, 1);
|
|
2941
|
+
return true;
|
|
2942
|
+
}
|
|
2738
2943
|
// ===================== 主入口 =====================
|
|
2739
2944
|
/**
|
|
2740
2945
|
* 运行一轮 Turn
|
|
@@ -9068,7 +9273,6 @@ function registerMediaTool(registry, workspace, opts = {}) {
|
|
|
9068
9273
|
|
|
9069
9274
|
// src/server/nodeLlm.ts
|
|
9070
9275
|
init_cjs_shims();
|
|
9071
|
-
var GLOBAL_PROVIDER_ID = "__env__";
|
|
9072
9276
|
function envProvider() {
|
|
9073
9277
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
9074
9278
|
if (!apiKey) return null;
|
|
@@ -9111,19 +9315,26 @@ function nodeLlmView(cfg) {
|
|
|
9111
9315
|
global: true
|
|
9112
9316
|
}
|
|
9113
9317
|
] : [],
|
|
9114
|
-
...cfg.providers.map((p) =>
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9318
|
+
...cfg.providers.map((p) => {
|
|
9319
|
+
const builtin = isBuiltinProvider(p.id);
|
|
9320
|
+
return {
|
|
9321
|
+
id: p.id,
|
|
9322
|
+
name: p.name,
|
|
9323
|
+
type: p.type ?? "",
|
|
9324
|
+
baseURL: p.baseURL,
|
|
9325
|
+
model: p.model,
|
|
9326
|
+
hasApiKey: Boolean(p.apiKey),
|
|
9327
|
+
models: p.models ?? [],
|
|
9328
|
+
nonChat: p.nonChat ?? [],
|
|
9329
|
+
modelMeta: p.modelMeta ?? [],
|
|
9330
|
+
reasoningEffort: p.reasoningEffort ?? null,
|
|
9331
|
+
global: false,
|
|
9332
|
+
/** 平台内置(接入 Hub 后自动下发,用户不用关心 Key) */
|
|
9333
|
+
builtin,
|
|
9334
|
+
/** 不可编辑的字段:控制台据此把 baseURL / key 显示为只读 */
|
|
9335
|
+
...builtin ? { locked: ["baseURL", "apiKey"] } : {}
|
|
9336
|
+
};
|
|
9337
|
+
})
|
|
9127
9338
|
];
|
|
9128
9339
|
return {
|
|
9129
9340
|
defaultProviderId: cfg.defaultProviderId ?? null,
|
|
@@ -9136,27 +9347,42 @@ function applyNodeLlmPatch(cfg, raw) {
|
|
|
9136
9347
|
const next = { providers: cfg.providers, defaultProviderId: cfg.defaultProviderId };
|
|
9137
9348
|
if (Array.isArray(raw?.providers)) {
|
|
9138
9349
|
const old = new Map(cfg.providers.map((p) => [p.id, p]));
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9350
|
+
const incoming = raw.providers.filter((x) => Boolean(x) && typeof x === "object").filter((x) => x.global !== true && String(x.id ?? "") !== GLOBAL_PROVIDER_ID);
|
|
9351
|
+
const prevBuiltin = cfg.providers.find((p) => isBuiltinProvider(p.id));
|
|
9352
|
+
const builtinPatch = incoming.find((x) => isBuiltinProvider(String(x.id ?? "")));
|
|
9353
|
+
const keptBuiltin = prevBuiltin ? [
|
|
9354
|
+
{
|
|
9355
|
+
...prevBuiltin,
|
|
9356
|
+
...builtinPatch?.model != null ? { model: String(builtinPatch.model).trim() || prevBuiltin.model } : {},
|
|
9357
|
+
...normalizeModelList(builtinPatch?.models) ? { models: normalizeModelList(builtinPatch?.models) } : {},
|
|
9358
|
+
...builtinPatch?.reasoningEffort !== void 0 ? { reasoningEffort: normReasoningEffort(builtinPatch.reasoningEffort) } : {},
|
|
9359
|
+
...typeof builtinPatch?.name === "string" && builtinPatch.name.trim() ? { name: builtinPatch.name.trim() } : {}
|
|
9360
|
+
}
|
|
9361
|
+
] : [];
|
|
9362
|
+
next.providers = [
|
|
9363
|
+
...keptBuiltin,
|
|
9364
|
+
...incoming.filter((x) => !isBuiltinProvider(String(x.id ?? ""))).map((x) => {
|
|
9365
|
+
const id = String(x.id ?? "").trim();
|
|
9366
|
+
if (!id) throw new ApiError(400, "bad_request", "\u4F9B\u5E94\u5546 id \u4E0D\u80FD\u4E3A\u7A7A");
|
|
9367
|
+
const prev = old.get(id);
|
|
9368
|
+
const apiKey = String(x.apiKey ?? "").trim();
|
|
9369
|
+
const models = normalizeModelList(x.models) ?? prev?.models;
|
|
9370
|
+
const nonChat = normalizeModelList(x.nonChat) ?? prev?.nonChat;
|
|
9371
|
+
const modelMeta = normalizeModelMeta(x.modelMeta) ?? prev?.modelMeta;
|
|
9372
|
+
return {
|
|
9373
|
+
id,
|
|
9374
|
+
name: String(x.name ?? prev?.name ?? id).trim(),
|
|
9375
|
+
type: x.type == null ? prev?.type : String(x.type),
|
|
9376
|
+
baseURL: String(x.baseURL ?? prev?.baseURL ?? "").trim(),
|
|
9377
|
+
model: String(x.model ?? prev?.model ?? "").trim(),
|
|
9378
|
+
apiKey: apiKey || prev?.apiKey,
|
|
9379
|
+
reasoningEffort: x.reasoningEffort === void 0 ? prev?.reasoningEffort ?? null : normReasoningEffort(x.reasoningEffort),
|
|
9380
|
+
...models ? { models } : {},
|
|
9381
|
+
...nonChat ? { nonChat } : {},
|
|
9382
|
+
...modelMeta ? { modelMeta } : {}
|
|
9383
|
+
};
|
|
9384
|
+
})
|
|
9385
|
+
];
|
|
9160
9386
|
}
|
|
9161
9387
|
if (typeof raw?.defaultProviderId === "string") {
|
|
9162
9388
|
const id = raw.defaultProviderId.trim();
|
|
@@ -9176,6 +9402,9 @@ function nodeDefaultProvider(store) {
|
|
|
9176
9402
|
if (cfg.providers.length === 0) return null;
|
|
9177
9403
|
return cfg.providers.find((p) => p.id === cfg.defaultProviderId) ?? cfg.providers[0] ?? null;
|
|
9178
9404
|
}
|
|
9405
|
+
function defaultTenantModel(store) {
|
|
9406
|
+
return process.env.OPENAI_MODEL || process.env.MODEL || nodeDefaultProvider(store)?.model || FALLBACK_MODEL;
|
|
9407
|
+
}
|
|
9179
9408
|
function resolveSessionProvider(store, tenant, providerId) {
|
|
9180
9409
|
const own = tenant.provider ?? {};
|
|
9181
9410
|
if (providerId) {
|
|
@@ -9205,6 +9434,208 @@ function resolveSessionProvider(store, tenant, providerId) {
|
|
|
9205
9434
|
return { ...own, model: tenant.model };
|
|
9206
9435
|
}
|
|
9207
9436
|
|
|
9437
|
+
// src/server/builtinLlm.ts
|
|
9438
|
+
init_cjs_shims();
|
|
9439
|
+
var FETCH_TIMEOUT_MS = 8e3;
|
|
9440
|
+
var RETRY_AFTER_MS = 5 * 6e4;
|
|
9441
|
+
function hubBase(cred) {
|
|
9442
|
+
return (cred.hubUrl ?? "").replace(/\/+$/, "");
|
|
9443
|
+
}
|
|
9444
|
+
function asProvider(grant) {
|
|
9445
|
+
const baseURL = String(grant.baseURL ?? "").trim();
|
|
9446
|
+
const apiKey = String(grant.apiKey ?? "").trim();
|
|
9447
|
+
const model = String(grant.model ?? "").trim();
|
|
9448
|
+
if (!baseURL || !apiKey) return null;
|
|
9449
|
+
return {
|
|
9450
|
+
id: BUILTIN_PROVIDER_ID,
|
|
9451
|
+
name: String(grant.name ?? "").trim() || "\u5FD7\u524D\u5927\u6A21\u578B\uFF08\u5185\u7F6E\uFF09",
|
|
9452
|
+
type: "builtin",
|
|
9453
|
+
baseURL,
|
|
9454
|
+
model,
|
|
9455
|
+
apiKey
|
|
9456
|
+
};
|
|
9457
|
+
}
|
|
9458
|
+
async function enrichModels(p) {
|
|
9459
|
+
try {
|
|
9460
|
+
const r = await Promise.race([
|
|
9461
|
+
fetchModels(p.baseURL, p.apiKey ?? "", p.model),
|
|
9462
|
+
new Promise((resolve20) => {
|
|
9463
|
+
const t = setTimeout(() => resolve20(null), FETCH_TIMEOUT_MS);
|
|
9464
|
+
t.unref?.();
|
|
9465
|
+
})
|
|
9466
|
+
]);
|
|
9467
|
+
if (!r?.models?.length) return p;
|
|
9468
|
+
return {
|
|
9469
|
+
...p,
|
|
9470
|
+
// 控制面没给默认型号时,取供应商返回的第一个,避免模型为空让用户卡住
|
|
9471
|
+
model: p.model || r.models[0],
|
|
9472
|
+
models: r.models,
|
|
9473
|
+
...r.nonChat?.length ? { nonChat: r.nonChat } : {},
|
|
9474
|
+
...r.modelMeta?.length ? { modelMeta: r.modelMeta } : {}
|
|
9475
|
+
};
|
|
9476
|
+
} catch {
|
|
9477
|
+
return p;
|
|
9478
|
+
}
|
|
9479
|
+
}
|
|
9480
|
+
function saveBuiltin(store, provider) {
|
|
9481
|
+
const cfg = store.loadNodeLlm();
|
|
9482
|
+
const others = cfg.providers.filter((p) => p.id !== BUILTIN_PROVIDER_ID);
|
|
9483
|
+
const prev = cfg.providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
|
|
9484
|
+
const merged = {
|
|
9485
|
+
...provider,
|
|
9486
|
+
// 用户选过的型号/候选列表/推理强度/显示名都保留;只有托管字段(baseURL/key)强制以控制面为准
|
|
9487
|
+
...prev?.model ? { model: prev.model } : {},
|
|
9488
|
+
...prev?.models?.length ? { models: prev.models } : {},
|
|
9489
|
+
...prev?.reasoningEffort != null ? { reasoningEffort: prev.reasoningEffort } : {},
|
|
9490
|
+
...prev?.name ? { name: prev.name } : {}
|
|
9491
|
+
};
|
|
9492
|
+
store.saveNodeLlm({
|
|
9493
|
+
providers: [merged, ...others],
|
|
9494
|
+
// 之前没有默认且用户也没指定时,让内置模型成为默认——「开箱即用」
|
|
9495
|
+
defaultProviderId: cfg.defaultProviderId ?? BUILTIN_PROVIDER_ID
|
|
9496
|
+
});
|
|
9497
|
+
adoptBuiltinModelForTenants(store, merged.model);
|
|
9498
|
+
}
|
|
9499
|
+
var LEGACY_DEFAULT_MODELS = /* @__PURE__ */ new Set([FALLBACK_MODEL, "deepseek-v4-flash"]);
|
|
9500
|
+
function adoptBuiltinModelForTenants(store, model) {
|
|
9501
|
+
if (!model) return;
|
|
9502
|
+
for (const id of store.listTenants()) {
|
|
9503
|
+
const tenant = store.loadTenant(id);
|
|
9504
|
+
if (!tenant || !LEGACY_DEFAULT_MODELS.has(tenant.model)) continue;
|
|
9505
|
+
if (tenant.provider?.apiKey) continue;
|
|
9506
|
+
tenant.model = model;
|
|
9507
|
+
store.saveTenant(tenant);
|
|
9508
|
+
}
|
|
9509
|
+
}
|
|
9510
|
+
async function ensureBuiltinProvider(store, opts = {}) {
|
|
9511
|
+
const cred = store.loadFleetCredential();
|
|
9512
|
+
if (!cred?.fleetToken || !cred.fleetSecret) {
|
|
9513
|
+
return { status: "disabled", reason: "\u8282\u70B9\u672A\u63A5\u5165 Hub" };
|
|
9514
|
+
}
|
|
9515
|
+
const hub = hubBase(cred);
|
|
9516
|
+
if (!hub) return { status: "disabled", reason: "\u7F3A\u5C11 Hub \u5730\u5740" };
|
|
9517
|
+
const local = store.loadNodeLlm().providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
|
|
9518
|
+
if (local && !opts.force) {
|
|
9519
|
+
adoptBuiltinModelForTenants(store, local.model);
|
|
9520
|
+
return { status: "ready", provider: local, refreshed: false };
|
|
9521
|
+
}
|
|
9522
|
+
const cached3 = cred.builtinLlm ? asProvider(cred.builtinLlm) : null;
|
|
9523
|
+
let res;
|
|
9524
|
+
try {
|
|
9525
|
+
res = await fetch(`${hub}/api/hub/node/builtin-llm`, {
|
|
9526
|
+
headers: {
|
|
9527
|
+
Authorization: `Bearer ${cred.fleetToken}`,
|
|
9528
|
+
"X-Fleet-Secret": cred.fleetSecret,
|
|
9529
|
+
"X-Node-Id": cred.nodeId
|
|
9530
|
+
},
|
|
9531
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
9532
|
+
});
|
|
9533
|
+
} catch (e) {
|
|
9534
|
+
if (cached3) {
|
|
9535
|
+
const provider2 = await enrichModels(cached3);
|
|
9536
|
+
saveBuiltin(store, provider2);
|
|
9537
|
+
return { status: "ready", provider: provider2, refreshed: true };
|
|
9538
|
+
}
|
|
9539
|
+
return { status: "error", reason: `\u8FDE\u63A5 Hub \u5931\u8D25\uFF1A${e.message}` };
|
|
9540
|
+
}
|
|
9541
|
+
let body = null;
|
|
9542
|
+
try {
|
|
9543
|
+
body = await res.json();
|
|
9544
|
+
} catch {
|
|
9545
|
+
}
|
|
9546
|
+
if (!res.ok) {
|
|
9547
|
+
const msg = body?.error?.message ?? `HTTP ${res.status}`;
|
|
9548
|
+
if (res.status === 401 || res.status === 403) {
|
|
9549
|
+
return { status: "unavailable", reason: msg };
|
|
9550
|
+
}
|
|
9551
|
+
if (cached3) {
|
|
9552
|
+
const provider2 = await enrichModels(cached3);
|
|
9553
|
+
saveBuiltin(store, provider2);
|
|
9554
|
+
return { status: "ready", provider: provider2, refreshed: true };
|
|
9555
|
+
}
|
|
9556
|
+
return { status: "error", reason: msg };
|
|
9557
|
+
}
|
|
9558
|
+
const grant = body?.data?.builtinLlm;
|
|
9559
|
+
if (!grant) {
|
|
9560
|
+
const why = String(body?.data?.reason ?? "").trim();
|
|
9561
|
+
return { status: "unavailable", reason: why || "\u63A7\u5236\u9762\u672A\u4E0B\u53D1\u5185\u7F6E\u6A21\u578B" };
|
|
9562
|
+
}
|
|
9563
|
+
const parsed = asProvider(grant);
|
|
9564
|
+
if (!parsed) return { status: "unavailable", reason: "\u5185\u7F6E\u6A21\u578B\u914D\u7F6E\u4E0D\u5B8C\u6574" };
|
|
9565
|
+
const provider = await enrichModels(parsed);
|
|
9566
|
+
saveBuiltin(store, provider);
|
|
9567
|
+
return { status: "ready", provider, refreshed: true };
|
|
9568
|
+
}
|
|
9569
|
+
async function fetchLlmBalance(store) {
|
|
9570
|
+
const cred = store.loadFleetCredential();
|
|
9571
|
+
const hub = cred ? hubBase(cred) : "";
|
|
9572
|
+
if (!cred?.fleetToken || !cred.fleetSecret || !hub) {
|
|
9573
|
+
return { available: null, currency: "CNY", reason: "\u8282\u70B9\u672A\u63A5\u5165 Hub" };
|
|
9574
|
+
}
|
|
9575
|
+
try {
|
|
9576
|
+
const res = await fetch(`${hub}/api/hub/node/balance`, {
|
|
9577
|
+
headers: {
|
|
9578
|
+
Authorization: `Bearer ${cred.fleetToken}`,
|
|
9579
|
+
"X-Fleet-Secret": cred.fleetSecret,
|
|
9580
|
+
"X-Node-Id": cred.nodeId
|
|
9581
|
+
},
|
|
9582
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
9583
|
+
});
|
|
9584
|
+
const body = await res.json().catch(() => null);
|
|
9585
|
+
if (!res.ok) {
|
|
9586
|
+
return {
|
|
9587
|
+
available: null,
|
|
9588
|
+
currency: "CNY",
|
|
9589
|
+
reason: body?.data ? "" : `HTTP ${res.status}`
|
|
9590
|
+
};
|
|
9591
|
+
}
|
|
9592
|
+
const d = body?.data ?? {};
|
|
9593
|
+
const available = typeof d.available === "number" ? d.available : null;
|
|
9594
|
+
return {
|
|
9595
|
+
available,
|
|
9596
|
+
currency: typeof d.currency === "string" && d.currency ? d.currency : "CNY",
|
|
9597
|
+
...typeof d.reserved === "number" ? { reserved: d.reserved } : {},
|
|
9598
|
+
// Hub 没配 HUB_PUBLIC_URL 时退回用接入时的 hubUrl
|
|
9599
|
+
topupUrl: typeof d.topupUrl === "string" && d.topupUrl ? d.topupUrl : `${hub}/topup`,
|
|
9600
|
+
...available === null && typeof d.reason === "string" ? { reason: d.reason } : {}
|
|
9601
|
+
};
|
|
9602
|
+
} catch (e) {
|
|
9603
|
+
return {
|
|
9604
|
+
available: null,
|
|
9605
|
+
currency: "CNY",
|
|
9606
|
+
reason: `\u8FDE\u63A5 Hub \u5931\u8D25\uFF1A${e.message}`
|
|
9607
|
+
};
|
|
9608
|
+
}
|
|
9609
|
+
}
|
|
9610
|
+
var AUTH_RETRY_THROTTLE_MS = 6e4;
|
|
9611
|
+
var lastAuthRetryAt = 0;
|
|
9612
|
+
function healBuiltinOnAuthFailure(store, message) {
|
|
9613
|
+
if (!/401|unauthorized|invalid[_\s]*api[_\s]*key|api[\s_-]*key\s*(?:无效|invalid)/i.test(message)) return;
|
|
9614
|
+
if (!store.loadFleetCredential()) return;
|
|
9615
|
+
if (!store.loadNodeLlm().providers.some((p) => p.id === BUILTIN_PROVIDER_ID)) return;
|
|
9616
|
+
if (Date.now() - lastAuthRetryAt < AUTH_RETRY_THROTTLE_MS) return;
|
|
9617
|
+
lastAuthRetryAt = Date.now();
|
|
9618
|
+
void ensureBuiltinProvider(store, { force: true }).catch(() => {
|
|
9619
|
+
});
|
|
9620
|
+
}
|
|
9621
|
+
var lastFailureAt = 0;
|
|
9622
|
+
function scheduleBuiltinProvider(store, log = () => {
|
|
9623
|
+
}) {
|
|
9624
|
+
if (Date.now() - lastFailureAt < RETRY_AFTER_MS) return;
|
|
9625
|
+
void ensureBuiltinProvider(store).then((r) => {
|
|
9626
|
+
if (r.status === "ready") {
|
|
9627
|
+
if (r.refreshed) log(`[builtin-llm] \u5DF2\u4E0B\u53D1\u5185\u7F6E\u6A21\u578B\uFF1A${r.provider.name}\uFF08${r.provider.model}\uFF09`);
|
|
9628
|
+
return;
|
|
9629
|
+
}
|
|
9630
|
+
if (r.status === "disabled") return;
|
|
9631
|
+
lastFailureAt = Date.now();
|
|
9632
|
+
if (r.status === "unavailable") log(`[builtin-llm] \u6682\u65E0\u5185\u7F6E\u6A21\u578B\uFF1A${r.reason}`);
|
|
9633
|
+
else log(`[builtin-llm] \u62C9\u53D6\u5185\u7F6E\u6A21\u578B\u5931\u8D25\uFF1A${r.reason}`);
|
|
9634
|
+
}).catch(() => {
|
|
9635
|
+
lastFailureAt = Date.now();
|
|
9636
|
+
});
|
|
9637
|
+
}
|
|
9638
|
+
|
|
9208
9639
|
// src/server/usage.ts
|
|
9209
9640
|
init_cjs_shims();
|
|
9210
9641
|
var fs18 = __toESM(require("fs"), 1);
|
|
@@ -9276,6 +9707,19 @@ function addAgg(into, tokens, costUsd) {
|
|
|
9276
9707
|
function utcDay(iso) {
|
|
9277
9708
|
return iso.slice(0, 10);
|
|
9278
9709
|
}
|
|
9710
|
+
function sumAgg(values) {
|
|
9711
|
+
const out = zeroAgg();
|
|
9712
|
+
for (const v of values) {
|
|
9713
|
+
out.calls += v.calls;
|
|
9714
|
+
out.input += v.input;
|
|
9715
|
+
out.output += v.output;
|
|
9716
|
+
out.cacheRead += v.cacheRead;
|
|
9717
|
+
out.cacheWrite += v.cacheWrite;
|
|
9718
|
+
out.costUsd += v.costUsd;
|
|
9719
|
+
out.unpriced += v.unpriced;
|
|
9720
|
+
}
|
|
9721
|
+
return out;
|
|
9722
|
+
}
|
|
9279
9723
|
function monthOf(iso) {
|
|
9280
9724
|
return iso.slice(0, 7);
|
|
9281
9725
|
}
|
|
@@ -9551,6 +9995,23 @@ var UsageLedger = class {
|
|
|
9551
9995
|
}
|
|
9552
9996
|
return all;
|
|
9553
9997
|
}
|
|
9998
|
+
/**
|
|
9999
|
+
* 按「项目 × 天」聚合(供控制面把消耗拆到每个项目)。
|
|
10000
|
+
*
|
|
10001
|
+
* 汇总文件里只存了按项目、按天两个独立维度,拿不到交叉项,因此这里从
|
|
10002
|
+
* 账本明细现算——调用方(节点上报)只取最近 40 天,读的是 2 个月文件,
|
|
10003
|
+
* 5 分钟一次的成本可忽略。
|
|
10004
|
+
*/
|
|
10005
|
+
projectByDay(opts) {
|
|
10006
|
+
const out = {};
|
|
10007
|
+
for (const rec of this.allRecords({ from: opts.from, to: opts.to })) {
|
|
10008
|
+
const day = utcDay(rec.ts);
|
|
10009
|
+
const days = out[rec.projectId] ?? (out[rec.projectId] = {});
|
|
10010
|
+
const agg = days[day] ?? (days[day] = zeroAgg());
|
|
10011
|
+
addAgg(agg, rec, rec.costUsd);
|
|
10012
|
+
}
|
|
10013
|
+
return out;
|
|
10014
|
+
}
|
|
9554
10015
|
summary(opts) {
|
|
9555
10016
|
this.ensureInit();
|
|
9556
10017
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
@@ -9850,6 +10311,141 @@ var EVENT_BUFFER_MAX = 2e3;
|
|
|
9850
10311
|
var EVENTS_ACK_MAX_BYTES = 512 * 1024;
|
|
9851
10312
|
var APPROVAL_TIMEOUT_MS = (Number(process.env.APPROVAL_TIMEOUT_SECONDS) || 15 * 60) * 1e3;
|
|
9852
10313
|
var LAST_USER_MESSAGE_MAX = 200;
|
|
10314
|
+
var TURN_TEXT_MAX = 500;
|
|
10315
|
+
var TURN_REPLY_MAX_BYTES = 256 * 1024;
|
|
10316
|
+
var TURN_REPLY_MAX_TEXT = 256 * 1024;
|
|
10317
|
+
var TURNS_DEFAULT_LIMIT = 20;
|
|
10318
|
+
function clip(text, max = LAST_USER_MESSAGE_MAX) {
|
|
10319
|
+
const s = text ?? "";
|
|
10320
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
10321
|
+
}
|
|
10322
|
+
function asNum(v) {
|
|
10323
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
10324
|
+
}
|
|
10325
|
+
function newTurnEntry(ev, input) {
|
|
10326
|
+
return {
|
|
10327
|
+
v: 1,
|
|
10328
|
+
startSeq: ev.seq,
|
|
10329
|
+
endSeq: ev.seq,
|
|
10330
|
+
startedAt: ev.ts,
|
|
10331
|
+
endedAt: null,
|
|
10332
|
+
status: "running",
|
|
10333
|
+
inputs: [{ seq: ev.seq, ts: ev.ts, text: clip(input, TURN_TEXT_MAX) }],
|
|
10334
|
+
replyPreview: "",
|
|
10335
|
+
producedFiles: [],
|
|
10336
|
+
llmCalls: 0,
|
|
10337
|
+
toolCalls: 0,
|
|
10338
|
+
tokensIn: 0,
|
|
10339
|
+
tokensOut: 0
|
|
10340
|
+
};
|
|
10341
|
+
}
|
|
10342
|
+
function applyTurnEvent(turn, ev, toolSeen) {
|
|
10343
|
+
const d = ev.data ?? {};
|
|
10344
|
+
turn.endSeq = ev.seq;
|
|
10345
|
+
switch (ev.type) {
|
|
10346
|
+
case "queue_steering": {
|
|
10347
|
+
turn.inputs.push({
|
|
10348
|
+
seq: ev.seq,
|
|
10349
|
+
ts: ev.ts,
|
|
10350
|
+
text: clip(String(d["text"] ?? ""), TURN_TEXT_MAX)
|
|
10351
|
+
});
|
|
10352
|
+
break;
|
|
10353
|
+
}
|
|
10354
|
+
case "assistant_message": {
|
|
10355
|
+
turn.llmCalls += 1;
|
|
10356
|
+
const text = String(d["text"] ?? "");
|
|
10357
|
+
if (text) {
|
|
10358
|
+
turn.replyPreview = clip(text);
|
|
10359
|
+
turn.replySeq = ev.seq;
|
|
10360
|
+
}
|
|
10361
|
+
const u = d["usage"];
|
|
10362
|
+
if (u && typeof u === "object") {
|
|
10363
|
+
const usage = u;
|
|
10364
|
+
turn.tokensIn += asNum(usage["input"]);
|
|
10365
|
+
turn.tokensOut += asNum(usage["output"]);
|
|
10366
|
+
}
|
|
10367
|
+
break;
|
|
10368
|
+
}
|
|
10369
|
+
case "tool_started":
|
|
10370
|
+
case "tool_finished": {
|
|
10371
|
+
const key = String(d["reqId"] ?? "") || `${String(d["name"] ?? "")}:${ev.seq}`;
|
|
10372
|
+
if (!toolSeen.has(key)) {
|
|
10373
|
+
toolSeen.add(key);
|
|
10374
|
+
turn.toolCalls += 1;
|
|
10375
|
+
}
|
|
10376
|
+
break;
|
|
10377
|
+
}
|
|
10378
|
+
case "error": {
|
|
10379
|
+
turn.error = clip(String(d["message"] ?? d["error"] ?? ""));
|
|
10380
|
+
break;
|
|
10381
|
+
}
|
|
10382
|
+
default:
|
|
10383
|
+
break;
|
|
10384
|
+
}
|
|
10385
|
+
}
|
|
10386
|
+
function closeTurnEntry(turn, ev) {
|
|
10387
|
+
const d = ev.data ?? {};
|
|
10388
|
+
turn.endSeq = ev.seq;
|
|
10389
|
+
turn.endedAt = ev.ts;
|
|
10390
|
+
const status = String(d["status"] ?? "");
|
|
10391
|
+
turn.status = ev.type === "interrupted" ? status || "interrupted" : status || "completed";
|
|
10392
|
+
const files = d["produced_files"];
|
|
10393
|
+
if (Array.isArray(files)) {
|
|
10394
|
+
turn.producedFiles = files.map((x) => String(x ?? "")).filter(Boolean);
|
|
10395
|
+
}
|
|
10396
|
+
if (typeof d["iterations"] === "number") turn.iterations = d["iterations"];
|
|
10397
|
+
const startMs = Date.parse(turn.startedAt);
|
|
10398
|
+
const endMs = Date.parse(ev.ts);
|
|
10399
|
+
if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
|
|
10400
|
+
turn.durationMs = endMs - startMs;
|
|
10401
|
+
}
|
|
10402
|
+
}
|
|
10403
|
+
function buildTurnIndex(events) {
|
|
10404
|
+
const out = [];
|
|
10405
|
+
let cur = null;
|
|
10406
|
+
let toolSeen = /* @__PURE__ */ new Set();
|
|
10407
|
+
for (const ev of events) {
|
|
10408
|
+
if (ev.type === "turn_start") {
|
|
10409
|
+
cur = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
10410
|
+
toolSeen = /* @__PURE__ */ new Set();
|
|
10411
|
+
out.push(cur);
|
|
10412
|
+
continue;
|
|
10413
|
+
}
|
|
10414
|
+
if (!cur) continue;
|
|
10415
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
10416
|
+
closeTurnEntry(cur, ev);
|
|
10417
|
+
cur = null;
|
|
10418
|
+
continue;
|
|
10419
|
+
}
|
|
10420
|
+
applyTurnEvent(cur, ev, toolSeen);
|
|
10421
|
+
}
|
|
10422
|
+
for (const t of out) {
|
|
10423
|
+
if (!t.endedAt && t.error) t.status = "error";
|
|
10424
|
+
}
|
|
10425
|
+
return out;
|
|
10426
|
+
}
|
|
10427
|
+
function summaryFromEntry(t) {
|
|
10428
|
+
return {
|
|
10429
|
+
startSeq: t.startSeq,
|
|
10430
|
+
endSeq: t.endSeq,
|
|
10431
|
+
startedAt: t.startedAt,
|
|
10432
|
+
endedAt: t.endedAt,
|
|
10433
|
+
status: t.status,
|
|
10434
|
+
input: t.inputs[0]?.text ?? "",
|
|
10435
|
+
reply: t.replyPreview,
|
|
10436
|
+
llmCalls: t.llmCalls,
|
|
10437
|
+
toolCalls: t.toolCalls,
|
|
10438
|
+
tokensIn: t.tokensIn,
|
|
10439
|
+
tokensOut: t.tokensOut,
|
|
10440
|
+
iterations: t.iterations,
|
|
10441
|
+
durationMs: t.durationMs,
|
|
10442
|
+
error: t.error,
|
|
10443
|
+
inputs: t.inputs,
|
|
10444
|
+
userCount: t.inputs.length,
|
|
10445
|
+
replySeq: t.replySeq,
|
|
10446
|
+
producedFiles: t.producedFiles
|
|
10447
|
+
};
|
|
10448
|
+
}
|
|
9853
10449
|
function isAffirmative(answer) {
|
|
9854
10450
|
const a = answer.trim().toLowerCase();
|
|
9855
10451
|
if (!a) return false;
|
|
@@ -9874,6 +10470,10 @@ var SessionManager = class {
|
|
|
9874
10470
|
opts;
|
|
9875
10471
|
runtimes = /* @__PURE__ */ new Map();
|
|
9876
10472
|
// sessionId → runtime
|
|
10473
|
+
/** 轮次索引的内存副本(运行中的轮比磁盘索引新;重启后为空) */
|
|
10474
|
+
turnIndexCache = /* @__PURE__ */ new Map();
|
|
10475
|
+
/** 进行中的索引重建任务(并发请求合并到同一个) */
|
|
10476
|
+
turnIndexBuilds = /* @__PURE__ */ new Map();
|
|
9877
10477
|
/** 全局通知 SSE 订阅者(控制台通知中心) */
|
|
9878
10478
|
notificationSubs = /* @__PURE__ */ new Set();
|
|
9879
10479
|
/**
|
|
@@ -9928,6 +10528,17 @@ var SessionManager = class {
|
|
|
9928
10528
|
rt.status = status;
|
|
9929
10529
|
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
9930
10530
|
}
|
|
10531
|
+
/**
|
|
10532
|
+
* 通知里的「最近一条用户消息」:截断到 LAST_USER_MESSAGE_MAX。
|
|
10533
|
+
*
|
|
10534
|
+
* 三处共用(session_status / turn_completed / turn_stopped),避免各自写一遍
|
|
10535
|
+
* 截断规则后长度不一致——控制台弹窗与任务列表会显示同一句话的两个版本。
|
|
10536
|
+
*/
|
|
10537
|
+
_lastMessage(rt) {
|
|
10538
|
+
const text = (rt.lastInput ?? "").trim();
|
|
10539
|
+
if (!text) return null;
|
|
10540
|
+
return text.length > LAST_USER_MESSAGE_MAX ? `${text.slice(0, LAST_USER_MESSAGE_MAX)}\u2026` : text;
|
|
10541
|
+
}
|
|
9931
10542
|
/**
|
|
9932
10543
|
* session_status 通知载荷:状态 + 最近一条用户消息(截断)。
|
|
9933
10544
|
*
|
|
@@ -9936,10 +10547,24 @@ var SessionManager = class {
|
|
|
9936
10547
|
*/
|
|
9937
10548
|
_statusData(rt) {
|
|
9938
10549
|
const data = { status: rt.status };
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
10550
|
+
if (rt.lastStartedAt) data.lastStartedAt = rt.lastStartedAt;
|
|
10551
|
+
const msg = this._lastMessage(rt);
|
|
10552
|
+
if (msg) data.lastUserMessage = msg;
|
|
10553
|
+
return data;
|
|
10554
|
+
}
|
|
10555
|
+
/**
|
|
10556
|
+
* 任务结束类通知(完成 / 达到迭代上限)的载荷:在 turn_end 数据之外补上
|
|
10557
|
+
* 「会话名 + 最后一条用户消息」。
|
|
10558
|
+
*
|
|
10559
|
+
* 控制台右下角的完成弹窗要说明「哪个会话、用户让它做了什么」。这些字段
|
|
10560
|
+
* 以节点侧为准随通知一起下发,前端就不必依赖快照(nodeInfo / session_status)
|
|
10561
|
+
* 是否已经到达——刚打开页面或通知乱序时才不会退化成只剩一句「任务已完成」。
|
|
10562
|
+
*/
|
|
10563
|
+
_turnEndData(rt, eventData, extra) {
|
|
10564
|
+
const data = { ...eventData, ...extra };
|
|
10565
|
+
if (rt.name) data.sessionName = rt.name;
|
|
10566
|
+
const msg = this._lastMessage(rt);
|
|
10567
|
+
if (msg) data.lastUserMessage = msg;
|
|
9943
10568
|
return data;
|
|
9944
10569
|
}
|
|
9945
10570
|
/** 触发通知回调(无 handler 时为空操作,异常隔离不冒泡) */
|
|
@@ -10096,6 +10721,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10096
10721
|
this.runtimes.delete(sessionId);
|
|
10097
10722
|
}
|
|
10098
10723
|
this.store.deleteSession(tenantId, sessionId);
|
|
10724
|
+
this.turnIndexCache.delete(sessionId);
|
|
10099
10725
|
this._publishNotify(tenantId, {
|
|
10100
10726
|
kind: "session_status",
|
|
10101
10727
|
sessionId,
|
|
@@ -10139,6 +10765,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10139
10765
|
throw new ApiError(409, "busy", "\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D");
|
|
10140
10766
|
}
|
|
10141
10767
|
rt.engine.queueSteering(input);
|
|
10768
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
10142
10769
|
rt.pendingSteers.push(input);
|
|
10143
10770
|
this._recordLastInput(rt, input);
|
|
10144
10771
|
return "steered";
|
|
@@ -10171,9 +10798,42 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10171
10798
|
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
10172
10799
|
if (!rt.engine) throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
10173
10800
|
rt.engine.queueSteering(text);
|
|
10801
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
10174
10802
|
rt.pendingSteers.push(text);
|
|
10175
10803
|
this._recordLastInput(rt, text);
|
|
10176
10804
|
}
|
|
10805
|
+
/**
|
|
10806
|
+
* 撤销一条尚未注入的补充信息(用户反悔)。
|
|
10807
|
+
*
|
|
10808
|
+
* 队列里的项还没进聊天流,此时只能靠中断整个任务来「收回」代价太大。
|
|
10809
|
+
* index 取自 GET /state 的 pendingSteers(与前端展示顺序一致),两侧同时删:
|
|
10810
|
+
* 只删前端那份,引擎下一迭代仍会把它注入对话;只删引擎那份,队列里会残留
|
|
10811
|
+
* 一条永远不注入的条目。
|
|
10812
|
+
*
|
|
10813
|
+
* 若撤的正是「最近用户指令」,把它还原成队列里的上一条、或本轮被覆盖前的
|
|
10814
|
+
* 指令——否则工作台还会显示这条已经被取消的内容。
|
|
10815
|
+
*
|
|
10816
|
+
* @returns 剩余待注入队列(前端据此刷新提示区)
|
|
10817
|
+
*/
|
|
10818
|
+
unsteer(tenant, sessionId, index) {
|
|
10819
|
+
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
10820
|
+
if (!rt.engine || !rt.runTask) {
|
|
10821
|
+
throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
10822
|
+
}
|
|
10823
|
+
if (!Number.isInteger(index) || index < 0 || index >= rt.pendingSteers.length) {
|
|
10824
|
+
throw new ApiError(409, "already_injected", "\u8FD9\u6761\u8865\u5145\u4FE1\u606F\u5DF2\u8FDB\u5165\u5BF9\u8BDD\uFF0C\u65E0\u6CD5\u64A4\u9500");
|
|
10825
|
+
}
|
|
10826
|
+
const [removed] = rt.pendingSteers.splice(index, 1);
|
|
10827
|
+
if (removed !== void 0) {
|
|
10828
|
+
rt.engine.removeSteering(removed);
|
|
10829
|
+
if (removed === rt.lastInput) {
|
|
10830
|
+
rt.lastInput = rt.pendingSteers.length > 0 ? rt.pendingSteers[rt.pendingSteers.length - 1] : rt.steerBaseInput;
|
|
10831
|
+
this._persist(rt);
|
|
10832
|
+
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
return [...rt.pendingSteers];
|
|
10836
|
+
}
|
|
10177
10837
|
/**
|
|
10178
10838
|
* 已提交、尚未注入当前任务的补充信息。
|
|
10179
10839
|
*
|
|
@@ -10606,44 +11266,214 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10606
11266
|
status: rt.status
|
|
10607
11267
|
};
|
|
10608
11268
|
}
|
|
10609
|
-
/**
|
|
10610
|
-
|
|
11269
|
+
/**
|
|
11270
|
+
* 事件分页(向前 / 向后两种方向)。
|
|
11271
|
+
*
|
|
11272
|
+
* - `since`:取 seq > since(实时增量 / 重连补齐)
|
|
11273
|
+
* - `before`:取 seq < before(控制台滚到顶部加载更早的对话)
|
|
11274
|
+
* - `maxBytes`:单次回执的字节预算(默认沿用 EVENTS_ACK_MAX_BYTES)
|
|
11275
|
+
*
|
|
11276
|
+
* 两者同时给出时按 `since`(向后)处理。`hasMore` 只对向前翻页有意义:
|
|
11277
|
+
* 表示本页最早一条之前还有更早的事件。
|
|
11278
|
+
*
|
|
11279
|
+
* 向前翻页一律走磁盘:内存缓冲只保留最近 EVENT_BUFFER_MAX 条,
|
|
11280
|
+
* 更早的事件只在 events.jsonl 里。
|
|
11281
|
+
*/
|
|
11282
|
+
eventsPage(tenantId, sessionId, opts = {}) {
|
|
10611
11283
|
const __t0 = dbgNow();
|
|
11284
|
+
const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : EVENTS_ACK_MAX_BYTES;
|
|
10612
11285
|
const rt = this.runtimes.get(sessionId);
|
|
11286
|
+
if (opts.before !== void 0) {
|
|
11287
|
+
const since2 = opts.since ?? 0;
|
|
11288
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
11289
|
+
before: opts.before,
|
|
11290
|
+
maxBytes
|
|
11291
|
+
});
|
|
11292
|
+
const ev2 = since2 > 0 ? page.events.filter((e) => e.seq > since2) : page.events;
|
|
11293
|
+
dbgMs(
|
|
11294
|
+
"session.eventsPage(before/\u78C1\u76D8)",
|
|
11295
|
+
__t0,
|
|
11296
|
+
// [dbg]
|
|
11297
|
+
`sid=${sessionId.slice(0, 8)} since=${since2} before=${opts.before} out=${ev2.length} more=${page.hasMore}`
|
|
11298
|
+
);
|
|
11299
|
+
return {
|
|
11300
|
+
events: ev2,
|
|
11301
|
+
latestSeq: rt?.seq ?? page.events.at(-1)?.seq ?? 0,
|
|
11302
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
11303
|
+
hasMore: page.hasMore
|
|
11304
|
+
};
|
|
11305
|
+
}
|
|
11306
|
+
const since = opts.since ?? 0;
|
|
10613
11307
|
if (rt) {
|
|
10614
|
-
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since));
|
|
11308
|
+
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since), maxBytes);
|
|
10615
11309
|
dbgMs(
|
|
10616
|
-
"session.
|
|
11310
|
+
"session.eventsPage(\u5185\u5B58)",
|
|
10617
11311
|
__t0,
|
|
10618
11312
|
// [dbg]
|
|
10619
11313
|
`sid=${sessionId.slice(0, 8)} since=${since} in=${rt.events.length} out=${ev2.length}`
|
|
10620
11314
|
);
|
|
10621
|
-
return {
|
|
11315
|
+
return {
|
|
11316
|
+
events: ev2,
|
|
11317
|
+
latestSeq: rt.seq,
|
|
11318
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
11319
|
+
// 内存缓冲被截断(或 since 早于缓冲起点)时,磁盘上可能还有更早的
|
|
11320
|
+
hasMore: !!ev2.length && ev2[0].seq > (rt.events[0]?.seq ?? 0)
|
|
11321
|
+
};
|
|
10622
11322
|
}
|
|
10623
11323
|
const all = this.store.loadEventsTail(tenantId, sessionId, EVENTS_ACK_MAX_BYTES * 2);
|
|
10624
|
-
const ev = this._boundedTail(
|
|
11324
|
+
const ev = this._boundedTail(
|
|
11325
|
+
all.filter((e) => e.seq > since),
|
|
11326
|
+
maxBytes
|
|
11327
|
+
);
|
|
10625
11328
|
dbgMs(
|
|
10626
|
-
"session.
|
|
11329
|
+
"session.eventsPage(\u78C1\u76D8)",
|
|
10627
11330
|
__t0,
|
|
10628
11331
|
// [dbg]
|
|
10629
11332
|
`sid=${sessionId.slice(0, 8)} since=${since} loaded=${all.length} out=${ev.length}`
|
|
10630
11333
|
);
|
|
10631
11334
|
return {
|
|
10632
11335
|
events: ev,
|
|
10633
|
-
latestSeq: all.at(-1)?.seq ?? 0
|
|
11336
|
+
latestSeq: all.at(-1)?.seq ?? 0,
|
|
11337
|
+
earliestSeq: ev[0]?.seq ?? 0,
|
|
11338
|
+
// 读的是文件尾部窗口:窗口已满说明更早的事件还在文件里
|
|
11339
|
+
hasMore: all.length > 0 && ev.length > 0 && ev[0].seq > all[0].seq
|
|
11340
|
+
};
|
|
11341
|
+
}
|
|
11342
|
+
/**
|
|
11343
|
+
* 兼容旧调用:只按 since 取增量。
|
|
11344
|
+
* @deprecated 新代码用 eventsPage(支持 before 向前翻页)
|
|
11345
|
+
*/
|
|
11346
|
+
eventsSince(tenantId, sessionId, since) {
|
|
11347
|
+
const r = this.eventsPage(tenantId, sessionId, { since });
|
|
11348
|
+
return { events: r.events, latestSeq: r.latestSeq };
|
|
11349
|
+
}
|
|
11350
|
+
/**
|
|
11351
|
+
* 「按 turn 汇总」的轻量列表:只回标题与统计,点开某一轮再单独拉该轮回复。
|
|
11352
|
+
*
|
|
11353
|
+
* 数据来自 turns.ndjson 索引(派生自事件流),读取成本与会话历史长度无关:
|
|
11354
|
+
* 老会话首次访问时后台/本请求内重建一次,之后每页只读几 KB。
|
|
11355
|
+
*
|
|
11356
|
+
* - `before`:只返回 startSeq < before 的轮次(向前翻页)
|
|
11357
|
+
* - `turns` 按时间升序;`hasMore` 表示更早还有轮次
|
|
11358
|
+
*/
|
|
11359
|
+
async sessionTurns(tenantId, sessionId, opts = {}) {
|
|
11360
|
+
const limit = Math.min(Math.max(opts.limit ?? TURNS_DEFAULT_LIMIT, 1), 100);
|
|
11361
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
11362
|
+
const before = opts.before;
|
|
11363
|
+
const endIdx = before === void 0 ? entries.length : entries.findIndex((t) => t.startSeq >= before);
|
|
11364
|
+
const end = endIdx < 0 ? entries.length : endIdx;
|
|
11365
|
+
const start = Math.max(0, end - limit);
|
|
11366
|
+
const picked = entries.slice(start, end);
|
|
11367
|
+
return {
|
|
11368
|
+
turns: picked.map(summaryFromEntry),
|
|
11369
|
+
hasMore: start > 0,
|
|
11370
|
+
earliestSeq: picked[0]?.startSeq ?? 0,
|
|
11371
|
+
total: entries.length,
|
|
11372
|
+
userTotal: entries.reduce((n, t) => n + t.inputs.length, 0)
|
|
11373
|
+
};
|
|
11374
|
+
}
|
|
11375
|
+
/**
|
|
11376
|
+
* 某一轮的用户消息与最终回复(含产出文件)。
|
|
11377
|
+
*
|
|
11378
|
+
* 只读两条索引记录 + 尾部一个事件页,不会因该轮工具输出很大而变慢:
|
|
11379
|
+
* 回复正文紧邻 turn_end,按 replySeq 精确读即可。
|
|
11380
|
+
*/
|
|
11381
|
+
async sessionTurnReply(tenantId, sessionId, startSeq) {
|
|
11382
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
11383
|
+
const turn = entries.find((t) => t.startSeq === startSeq);
|
|
11384
|
+
if (!turn) throw new ApiError(404, "not_found", "\u8F6E\u6B21\u4E0D\u5B58\u5728");
|
|
11385
|
+
let reply = null;
|
|
11386
|
+
let truncated = false;
|
|
11387
|
+
if (turn.replySeq !== void 0) {
|
|
11388
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
11389
|
+
before: turn.replySeq + 1,
|
|
11390
|
+
maxBytes: TURN_REPLY_MAX_BYTES
|
|
11391
|
+
});
|
|
11392
|
+
const ev = page.events.find((e) => e.seq === turn.replySeq);
|
|
11393
|
+
if (ev && ev.type === "assistant_message") {
|
|
11394
|
+
const text = String(ev.data?.["text"] ?? "");
|
|
11395
|
+
if (text.length > TURN_REPLY_MAX_TEXT) {
|
|
11396
|
+
reply = { seq: ev.seq, ts: ev.ts, text: text.slice(0, TURN_REPLY_MAX_TEXT) };
|
|
11397
|
+
truncated = true;
|
|
11398
|
+
} else {
|
|
11399
|
+
reply = { seq: ev.seq, ts: ev.ts, text };
|
|
11400
|
+
}
|
|
11401
|
+
}
|
|
11402
|
+
}
|
|
11403
|
+
return {
|
|
11404
|
+
startSeq: turn.startSeq,
|
|
11405
|
+
endSeq: turn.endSeq,
|
|
11406
|
+
status: turn.status,
|
|
11407
|
+
startedAt: turn.startedAt,
|
|
11408
|
+
endedAt: turn.endedAt,
|
|
11409
|
+
userMessages: turn.inputs,
|
|
11410
|
+
reply,
|
|
11411
|
+
truncated,
|
|
11412
|
+
producedFiles: turn.producedFiles
|
|
10634
11413
|
};
|
|
10635
11414
|
}
|
|
11415
|
+
/**
|
|
11416
|
+
* 取轮次索引;缺失时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
11417
|
+
*
|
|
11418
|
+
* 索引是派生数据:损坏/落后都只需重建,不影响会话本身。
|
|
11419
|
+
*/
|
|
11420
|
+
/**
|
|
11421
|
+
* 取轮次索引;缺失/落后时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
11422
|
+
*
|
|
11423
|
+
* 索引是派生数据:损坏或落后都只需重建,不影响会话本身。
|
|
11424
|
+
*/
|
|
11425
|
+
async _ensureTurnIndex(tenantId, sessionId) {
|
|
11426
|
+
let entries = this._mergeTurnIndex(tenantId, sessionId);
|
|
11427
|
+
if (entries.length > 0 && !this.runtimes.has(sessionId)) {
|
|
11428
|
+
const last = entries[entries.length - 1];
|
|
11429
|
+
const tail = this.store.loadEventsTail(tenantId, sessionId, 64 * 1024);
|
|
11430
|
+
const newTurnStarted = tail.some((e) => e.type === "turn_start" && e.seq > last.endSeq);
|
|
11431
|
+
if (newTurnStarted || last.status === "running") entries = [];
|
|
11432
|
+
}
|
|
11433
|
+
if (entries.length > 0) return entries;
|
|
11434
|
+
const pending = this.turnIndexBuilds.get(sessionId);
|
|
11435
|
+
if (pending) return pending;
|
|
11436
|
+
const task = this._rebuildTurnIndex(tenantId, sessionId).finally(() => {
|
|
11437
|
+
this.turnIndexBuilds.delete(sessionId);
|
|
11438
|
+
});
|
|
11439
|
+
this.turnIndexBuilds.set(sessionId, task);
|
|
11440
|
+
return task;
|
|
11441
|
+
}
|
|
11442
|
+
/** 合并磁盘索引与内存索引(内存版本更新,同一 startSeq 以内存为准) */
|
|
11443
|
+
_mergeTurnIndex(tenantId, sessionId) {
|
|
11444
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
11445
|
+
for (const e of this.store.loadTurnIndex(tenantId, sessionId)) byStart.set(e.startSeq, e);
|
|
11446
|
+
for (const e of this.turnIndexCache.get(sessionId) ?? []) byStart.set(e.startSeq, e);
|
|
11447
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
11448
|
+
}
|
|
11449
|
+
/** 从 events.jsonl 顺序流式重建轮次索引(一次性,之后增量维护) */
|
|
11450
|
+
async _rebuildTurnIndex(tenantId, sessionId) {
|
|
11451
|
+
const __t0 = dbgNow();
|
|
11452
|
+
const events = await this.store.readEventsAsync(tenantId, sessionId);
|
|
11453
|
+
const entries = buildTurnIndex(events);
|
|
11454
|
+
if (entries.length > 0) {
|
|
11455
|
+
await this.store.saveTurnIndex(tenantId, sessionId, entries);
|
|
11456
|
+
if (this.runtimes.has(sessionId)) this.turnIndexCache.set(sessionId, entries);
|
|
11457
|
+
}
|
|
11458
|
+
dbgMs(
|
|
11459
|
+
"session._rebuildTurnIndex",
|
|
11460
|
+
__t0,
|
|
11461
|
+
// [dbg]
|
|
11462
|
+
`sid=${sessionId.slice(0, 8)} events=${events.length} turns=${entries.length}`
|
|
11463
|
+
);
|
|
11464
|
+
return entries;
|
|
11465
|
+
}
|
|
10636
11466
|
/**
|
|
10637
11467
|
* 按字节预算截取事件「尾部」(至少保留最后一条),确保 MQTT 回执远小于单包上限。
|
|
10638
11468
|
* 供 get_events 使用:会话历史可任意大,但 MQTT 链路不能承载大包。
|
|
10639
11469
|
*/
|
|
10640
|
-
_boundedTail(events) {
|
|
11470
|
+
_boundedTail(events, maxBytes = EVENTS_ACK_MAX_BYTES) {
|
|
10641
11471
|
const __t0 = dbgNow();
|
|
10642
11472
|
let total = 0;
|
|
10643
11473
|
let i = events.length;
|
|
10644
11474
|
while (i > 0) {
|
|
10645
11475
|
const size = Buffer.byteLength(JSON.stringify(events[i - 1]));
|
|
10646
|
-
if (total > 0 && total + size >
|
|
11476
|
+
if (total > 0 && total + size > maxBytes) break;
|
|
10647
11477
|
total += size;
|
|
10648
11478
|
i -= 1;
|
|
10649
11479
|
}
|
|
@@ -10763,6 +11593,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10763
11593
|
nameSource: rec.nameSource,
|
|
10764
11594
|
taskCount: rec.taskCount ?? 0,
|
|
10765
11595
|
lastCompletedAt: rec.lastCompletedAt,
|
|
11596
|
+
lastStartedAt: rec.lastStartedAt,
|
|
10766
11597
|
naming: false,
|
|
10767
11598
|
turnStartedAt: 0
|
|
10768
11599
|
};
|
|
@@ -10780,10 +11611,11 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10780
11611
|
}
|
|
10781
11612
|
/** 装配引擎并启动一轮 Turn(供 submitTurn / submitOrSteer 复用) */
|
|
10782
11613
|
_startTurn(rt, input) {
|
|
11614
|
+
const engine = this._buildEngine(rt.tenant, rt, true);
|
|
10783
11615
|
rt.lastInput = input;
|
|
10784
|
-
this._setStatus(rt, "running");
|
|
10785
11616
|
rt.turnStartedAt = Date.now();
|
|
10786
|
-
|
|
11617
|
+
rt.lastStartedAt = new Date(rt.turnStartedAt).toISOString();
|
|
11618
|
+
this._setStatus(rt, "running");
|
|
10787
11619
|
rt.engine = engine;
|
|
10788
11620
|
this._persist(rt);
|
|
10789
11621
|
rt.runTask = this._drive(rt, engine.run(input));
|
|
@@ -10842,18 +11674,18 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10842
11674
|
this._setStatus(rt, "completed");
|
|
10843
11675
|
rt.taskCount += 1;
|
|
10844
11676
|
rt.lastCompletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10845
|
-
this._emitNotify(
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
durationMs
|
|
10849
|
-
|
|
11677
|
+
this._emitNotify(
|
|
11678
|
+
rt,
|
|
11679
|
+
"turn_completed",
|
|
11680
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
11681
|
+
);
|
|
10850
11682
|
} else if (status === "max_iterations_exceeded") {
|
|
10851
11683
|
this._setStatus(rt, "stopped");
|
|
10852
|
-
this._emitNotify(
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
durationMs
|
|
10856
|
-
|
|
11684
|
+
this._emitNotify(
|
|
11685
|
+
rt,
|
|
11686
|
+
"turn_stopped",
|
|
11687
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
11688
|
+
);
|
|
10857
11689
|
}
|
|
10858
11690
|
this._persist(rt);
|
|
10859
11691
|
if (status === "completed" && engine && shouldAutoName(rt.taskCount, rt.nameSource)) {
|
|
@@ -10864,7 +11696,8 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10864
11696
|
case "error" /* ERROR */:
|
|
10865
11697
|
this._setStatus(rt, "error");
|
|
10866
11698
|
this._persist(rt);
|
|
10867
|
-
this._emitNotify(rt, "error", event.data);
|
|
11699
|
+
this._emitNotify(rt, "error", this._turnEndData(rt, event.data, {}));
|
|
11700
|
+
healBuiltinOnAuthFailure(this.store, String(event.data["error"] ?? ""));
|
|
10868
11701
|
break;
|
|
10869
11702
|
case "interrupted" /* INTERRUPTED */:
|
|
10870
11703
|
this._setStatus(rt, "interrupted");
|
|
@@ -10890,9 +11723,17 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10890
11723
|
this._setStatus(rt, "error");
|
|
10891
11724
|
this._persist(rt);
|
|
10892
11725
|
} finally {
|
|
11726
|
+
if (rt.turnIndex) {
|
|
11727
|
+
const turn = rt.turnIndex;
|
|
11728
|
+
if (!turn.endedAt && turn.error) turn.status = "error";
|
|
11729
|
+
this._appendTurnIndex(rt, turn);
|
|
11730
|
+
rt.turnIndex = void 0;
|
|
11731
|
+
rt.turnToolKeys = void 0;
|
|
11732
|
+
}
|
|
10893
11733
|
rt.runTask = null;
|
|
10894
11734
|
rt.engine = null;
|
|
10895
11735
|
rt.pendingSteers = [];
|
|
11736
|
+
rt.steerBaseInput = void 0;
|
|
10896
11737
|
}
|
|
10897
11738
|
}
|
|
10898
11739
|
_buffer(rt, event) {
|
|
@@ -10905,6 +11746,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10905
11746
|
rt.events.push(entry);
|
|
10906
11747
|
if (rt.events.length > EVENT_BUFFER_MAX) rt.events.shift();
|
|
10907
11748
|
void this.store.appendEvent(rt.tenantId, rt.sessionId, entry);
|
|
11749
|
+
this._trackTurnIndex(rt, entry);
|
|
10908
11750
|
for (const res of rt.subscribers) this._sseSend(res, entry);
|
|
10909
11751
|
this._broadcastSessionEvent(rt.tenantId, rt.sessionId, entry);
|
|
10910
11752
|
try {
|
|
@@ -10912,6 +11754,42 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10912
11754
|
} catch {
|
|
10913
11755
|
}
|
|
10914
11756
|
}
|
|
11757
|
+
/**
|
|
11758
|
+
* 维护运行中轮次的索引(in-memory 记账 + 关键节点落盘)。
|
|
11759
|
+
*
|
|
11760
|
+
* 落盘只在「开轮 / 补充消息 / 收轮」发生,delta 事件仅推进 endSeq 与统计,
|
|
11761
|
+
* 因此索引写入量是每轮 2~3 行,与会话事件量无关。
|
|
11762
|
+
*/
|
|
11763
|
+
_trackTurnIndex(rt, ev) {
|
|
11764
|
+
if (ev.type === "turn_start") {
|
|
11765
|
+
const turn2 = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
11766
|
+
rt.turnIndex = turn2;
|
|
11767
|
+
rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
11768
|
+
this._appendTurnIndex(rt, turn2);
|
|
11769
|
+
return;
|
|
11770
|
+
}
|
|
11771
|
+
const turn = rt.turnIndex;
|
|
11772
|
+
if (!turn) return;
|
|
11773
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
11774
|
+
closeTurnEntry(turn, ev);
|
|
11775
|
+
this._appendTurnIndex(rt, turn);
|
|
11776
|
+
rt.turnIndex = void 0;
|
|
11777
|
+
rt.turnToolKeys = void 0;
|
|
11778
|
+
return;
|
|
11779
|
+
}
|
|
11780
|
+
if (!rt.turnToolKeys) rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
11781
|
+
const inputsBefore = turn.inputs.length;
|
|
11782
|
+
applyTurnEvent(turn, ev, rt.turnToolKeys);
|
|
11783
|
+
if (turn.inputs.length !== inputsBefore) this._appendTurnIndex(rt, turn);
|
|
11784
|
+
}
|
|
11785
|
+
/** 写入一版轮次索引(内存副本 + 异步落盘) */
|
|
11786
|
+
_appendTurnIndex(rt, turn) {
|
|
11787
|
+
const cached3 = this.turnIndexCache.get(rt.sessionId) ?? [];
|
|
11788
|
+
const i = cached3.findIndex((t) => t.startSeq === turn.startSeq);
|
|
11789
|
+
const next = i >= 0 ? [...cached3.slice(0, i), turn, ...cached3.slice(i + 1)] : [...cached3, turn];
|
|
11790
|
+
this.turnIndexCache.set(rt.sessionId, next);
|
|
11791
|
+
void this.store.appendTurnIndex(rt.tenantId, rt.sessionId, turn);
|
|
11792
|
+
}
|
|
10915
11793
|
/** 回收内存 run 任务(释放引用交给 GC;磁盘状态保持不动) */
|
|
10916
11794
|
_recycleRunTask(rt) {
|
|
10917
11795
|
rt.runTask = null;
|
|
@@ -10949,6 +11827,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10949
11827
|
nameSource: rt.nameSource,
|
|
10950
11828
|
taskCount: rt.taskCount,
|
|
10951
11829
|
lastCompletedAt: rt.lastCompletedAt,
|
|
11830
|
+
lastStartedAt: rt.lastStartedAt,
|
|
10952
11831
|
modelOverridden: rt.modelOverridden,
|
|
10953
11832
|
providerId: rt.providerId,
|
|
10954
11833
|
permissionMode: rt.permissionMode,
|
|
@@ -11249,7 +12128,6 @@ var path25 = __toESM(require("path"), 1);
|
|
|
11249
12128
|
// src/server/projects.ts
|
|
11250
12129
|
init_cjs_shims();
|
|
11251
12130
|
var path21 = __toESM(require("path"), 1);
|
|
11252
|
-
var DEFAULT_MODEL = () => process.env.OPENAI_MODEL || process.env.MODEL || "gpt-4o";
|
|
11253
12131
|
function toProjectSummary(store, tenant) {
|
|
11254
12132
|
return {
|
|
11255
12133
|
id: tenant.id,
|
|
@@ -11359,6 +12237,7 @@ function allSessionStates(store, manager) {
|
|
|
11359
12237
|
name: s.name ?? null,
|
|
11360
12238
|
status: s.status,
|
|
11361
12239
|
lastUserMessage: lastUserMessage(s),
|
|
12240
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
11362
12241
|
lastCompletedAt: lastCompletedAt(s)
|
|
11363
12242
|
});
|
|
11364
12243
|
}
|
|
@@ -11388,7 +12267,7 @@ function createProject(store, input) {
|
|
|
11388
12267
|
id,
|
|
11389
12268
|
name: explicitName || defaultName,
|
|
11390
12269
|
apiKey: generateApiKey(),
|
|
11391
|
-
model: typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() :
|
|
12270
|
+
model: typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : defaultTenantModel(store),
|
|
11392
12271
|
provider: {
|
|
11393
12272
|
apiKey: typeof raw.providerApiKey === "string" && raw.providerApiKey.trim() ? raw.providerApiKey.trim() : void 0,
|
|
11394
12273
|
baseURL: typeof raw.baseURL === "string" && raw.baseURL.trim() ? raw.baseURL.trim() : void 0
|
|
@@ -11413,7 +12292,7 @@ function buildNodeInfo(store, opts) {
|
|
|
11413
12292
|
nodeId: opts.nodeId ?? ensureNodeId(store),
|
|
11414
12293
|
platform: process.platform,
|
|
11415
12294
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
11416
|
-
version: "0.2.
|
|
12295
|
+
version: "0.2.20",
|
|
11417
12296
|
admin: opts.admin,
|
|
11418
12297
|
projects: opts.projects
|
|
11419
12298
|
};
|
|
@@ -11779,9 +12658,133 @@ function makeDirIn(parentInput, name) {
|
|
|
11779
12658
|
init_cjs_shims();
|
|
11780
12659
|
var fs20 = __toESM(require("fs"), 1);
|
|
11781
12660
|
var path23 = __toESM(require("path"), 1);
|
|
12661
|
+
|
|
12662
|
+
// src/server/zipArchive.ts
|
|
12663
|
+
init_cjs_shims();
|
|
12664
|
+
var zlib = __toESM(require("zlib"), 1);
|
|
12665
|
+
var MAX_ZIP_ENTRIES = 2e4;
|
|
12666
|
+
var LOCAL_SIG = 67324752;
|
|
12667
|
+
var CENTRAL_SIG = 33639248;
|
|
12668
|
+
var EOCD_SIG = 101010256;
|
|
12669
|
+
var FLAG_UTF8 = 2048;
|
|
12670
|
+
var METHOD_STORE = 0;
|
|
12671
|
+
var METHOD_DEFLATE = 8;
|
|
12672
|
+
var CRC_TABLE = (() => {
|
|
12673
|
+
const table = new Int32Array(256);
|
|
12674
|
+
for (let n = 0; n < 256; n++) {
|
|
12675
|
+
let c = n;
|
|
12676
|
+
for (let k = 0; k < 8; k++) {
|
|
12677
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
12678
|
+
}
|
|
12679
|
+
table[n] = c;
|
|
12680
|
+
}
|
|
12681
|
+
return table;
|
|
12682
|
+
})();
|
|
12683
|
+
function crc32(buf) {
|
|
12684
|
+
let c = -1;
|
|
12685
|
+
for (let i = 0; i < buf.length; i++) {
|
|
12686
|
+
c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
|
|
12687
|
+
}
|
|
12688
|
+
return (c ^ -1) >>> 0;
|
|
12689
|
+
}
|
|
12690
|
+
function dosDateTime(d) {
|
|
12691
|
+
const year = Math.max(1980, d.getFullYear());
|
|
12692
|
+
return {
|
|
12693
|
+
time: d.getHours() << 11 | d.getMinutes() << 5 | d.getSeconds() >> 1,
|
|
12694
|
+
date: year - 1980 << 9 | d.getMonth() + 1 << 5 | d.getDate()
|
|
12695
|
+
};
|
|
12696
|
+
}
|
|
12697
|
+
function buildZip(entries) {
|
|
12698
|
+
if (entries.length > MAX_ZIP_ENTRIES) {
|
|
12699
|
+
throw new Error(`\u5F52\u6863\u6761\u76EE\u8FC7\u591A\uFF08${entries.length} > ${MAX_ZIP_ENTRIES}\uFF09`);
|
|
12700
|
+
}
|
|
12701
|
+
const chunks = [];
|
|
12702
|
+
const central = [];
|
|
12703
|
+
let offset = 0;
|
|
12704
|
+
for (const e of entries) {
|
|
12705
|
+
const name = Buffer.from(e.name, "utf-8");
|
|
12706
|
+
const isDir = e.name.endsWith("/");
|
|
12707
|
+
const raw = isDir ? Buffer.alloc(0) : e.data ?? Buffer.alloc(0);
|
|
12708
|
+
let method = METHOD_STORE;
|
|
12709
|
+
let body = raw;
|
|
12710
|
+
if (!isDir && raw.length > 0) {
|
|
12711
|
+
const deflated = zlib.deflateRawSync(raw, { level: 6 });
|
|
12712
|
+
if (deflated.length < raw.length) {
|
|
12713
|
+
method = METHOD_DEFLATE;
|
|
12714
|
+
body = deflated;
|
|
12715
|
+
}
|
|
12716
|
+
}
|
|
12717
|
+
const crc = isDir ? 0 : crc32(raw);
|
|
12718
|
+
const { time, date } = dosDateTime(e.mtime ?? /* @__PURE__ */ new Date());
|
|
12719
|
+
const local = Buffer.alloc(30);
|
|
12720
|
+
local.writeUInt32LE(LOCAL_SIG, 0);
|
|
12721
|
+
local.writeUInt16LE(20, 4);
|
|
12722
|
+
local.writeUInt16LE(FLAG_UTF8, 6);
|
|
12723
|
+
local.writeUInt16LE(method, 8);
|
|
12724
|
+
local.writeUInt16LE(time, 10);
|
|
12725
|
+
local.writeUInt16LE(date, 12);
|
|
12726
|
+
local.writeUInt32LE(crc, 14);
|
|
12727
|
+
local.writeUInt32LE(body.length, 18);
|
|
12728
|
+
local.writeUInt32LE(raw.length, 22);
|
|
12729
|
+
local.writeUInt16LE(name.length, 26);
|
|
12730
|
+
local.writeUInt16LE(0, 28);
|
|
12731
|
+
chunks.push(local, name);
|
|
12732
|
+
if (body.length > 0) chunks.push(body);
|
|
12733
|
+
central.push({
|
|
12734
|
+
name,
|
|
12735
|
+
method,
|
|
12736
|
+
crc,
|
|
12737
|
+
csize: body.length,
|
|
12738
|
+
usize: raw.length,
|
|
12739
|
+
offset,
|
|
12740
|
+
time,
|
|
12741
|
+
date,
|
|
12742
|
+
isDir
|
|
12743
|
+
});
|
|
12744
|
+
offset += local.length + name.length + body.length;
|
|
12745
|
+
}
|
|
12746
|
+
const cdStart = offset;
|
|
12747
|
+
for (const c of central) {
|
|
12748
|
+
const head = Buffer.alloc(46);
|
|
12749
|
+
head.writeUInt32LE(CENTRAL_SIG, 0);
|
|
12750
|
+
head.writeUInt16LE(20, 4);
|
|
12751
|
+
head.writeUInt16LE(20, 6);
|
|
12752
|
+
head.writeUInt16LE(FLAG_UTF8, 8);
|
|
12753
|
+
head.writeUInt16LE(c.method, 10);
|
|
12754
|
+
head.writeUInt16LE(c.time, 12);
|
|
12755
|
+
head.writeUInt16LE(c.date, 14);
|
|
12756
|
+
head.writeUInt32LE(c.crc, 16);
|
|
12757
|
+
head.writeUInt32LE(c.csize, 20);
|
|
12758
|
+
head.writeUInt32LE(c.usize, 24);
|
|
12759
|
+
head.writeUInt16LE(c.name.length, 28);
|
|
12760
|
+
head.writeUInt16LE(0, 30);
|
|
12761
|
+
head.writeUInt16LE(0, 32);
|
|
12762
|
+
head.writeUInt16LE(0, 34);
|
|
12763
|
+
head.writeUInt16LE(0, 36);
|
|
12764
|
+
head.writeUInt32LE(c.isDir ? 16 : 0, 38);
|
|
12765
|
+
head.writeUInt32LE(c.offset, 42);
|
|
12766
|
+
chunks.push(head, c.name);
|
|
12767
|
+
offset += head.length + c.name.length;
|
|
12768
|
+
}
|
|
12769
|
+
const eocd = Buffer.alloc(22);
|
|
12770
|
+
eocd.writeUInt32LE(EOCD_SIG, 0);
|
|
12771
|
+
eocd.writeUInt16LE(0, 4);
|
|
12772
|
+
eocd.writeUInt16LE(0, 6);
|
|
12773
|
+
eocd.writeUInt16LE(central.length, 8);
|
|
12774
|
+
eocd.writeUInt16LE(central.length, 10);
|
|
12775
|
+
eocd.writeUInt32LE(offset - cdStart, 12);
|
|
12776
|
+
eocd.writeUInt32LE(cdStart, 16);
|
|
12777
|
+
eocd.writeUInt16LE(0, 20);
|
|
12778
|
+
chunks.push(eocd);
|
|
12779
|
+
return Buffer.concat(chunks);
|
|
12780
|
+
}
|
|
12781
|
+
|
|
12782
|
+
// src/server/projectFiles.ts
|
|
11782
12783
|
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11783
12784
|
var MAX_DOWNLOAD_BYTES = 16 * 1024 * 1024;
|
|
12785
|
+
var MAX_BINARY_BYTES = 2.5 * 1024 * 1024;
|
|
11784
12786
|
var MAX_ENTRIES2 = 2e3;
|
|
12787
|
+
var MAX_ZIP_BYTES = 100 * 1024 * 1024;
|
|
11785
12788
|
function toPosix(p) {
|
|
11786
12789
|
return p.split(path23.sep).join("/");
|
|
11787
12790
|
}
|
|
@@ -11928,6 +12931,26 @@ function writeProjectFile(workspaceRoot, relPath, content) {
|
|
|
11928
12931
|
}
|
|
11929
12932
|
return { path: toPosix(path23.relative(base, file)), size: buf.length };
|
|
11930
12933
|
}
|
|
12934
|
+
function writeProjectFileBase64(workspaceRoot, relPath, base64) {
|
|
12935
|
+
const base = projectRoot(workspaceRoot);
|
|
12936
|
+
const rel = (relPath ?? "").trim();
|
|
12937
|
+
if (!rel) throw new Error("\u6587\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
|
|
12938
|
+
const file = resolveWithin(base, rel);
|
|
12939
|
+
const b64 = String(base64 ?? "").replace(/\s+/g, "");
|
|
12940
|
+
if (!b64) throw new Error("\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
|
|
12941
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) throw new Error("\u5185\u5BB9\u4E0D\u662F\u5408\u6CD5\u7684 base64");
|
|
12942
|
+
if (Math.floor(b64.length * 3 / 4) > MAX_BINARY_BYTES) {
|
|
12943
|
+
throw new Error(`\u5185\u5BB9\u8D85\u8FC7 ${MAX_BINARY_BYTES / (1024 * 1024)}MB \u4E0A\u9650\uFF0C\u65E0\u6CD5\u4FDD\u5B58`);
|
|
12944
|
+
}
|
|
12945
|
+
const buf = Buffer.from(b64, "base64");
|
|
12946
|
+
try {
|
|
12947
|
+
fs20.mkdirSync(path23.dirname(file), { recursive: true });
|
|
12948
|
+
fs20.writeFileSync(file, buf);
|
|
12949
|
+
} catch (e) {
|
|
12950
|
+
throw new Error(`\u5199\u5165\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
12951
|
+
}
|
|
12952
|
+
return { path: toPosix(path23.relative(base, file)), size: buf.length };
|
|
12953
|
+
}
|
|
11931
12954
|
function createProjectDir(workspaceRoot, relPath) {
|
|
11932
12955
|
const base = projectRoot(workspaceRoot);
|
|
11933
12956
|
const rel = (relPath ?? "").trim();
|
|
@@ -12001,6 +13024,170 @@ function searchProjectFiles(workspaceRoot, query) {
|
|
|
12001
13024
|
walk(base, "");
|
|
12002
13025
|
return out;
|
|
12003
13026
|
}
|
|
13027
|
+
function deleteProjectPaths(workspaceRoot, relPaths) {
|
|
13028
|
+
const base = projectRoot(workspaceRoot);
|
|
13029
|
+
const out = { deleted: [], failed: [] };
|
|
13030
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13031
|
+
for (const raw of relPaths ?? []) {
|
|
13032
|
+
const rel = (raw ?? "").trim().replace(/^[/\\]+/, "");
|
|
13033
|
+
if (!rel) {
|
|
13034
|
+
out.failed.push({ path: String(raw ?? ""), error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
13035
|
+
continue;
|
|
13036
|
+
}
|
|
13037
|
+
if (seen.has(rel)) continue;
|
|
13038
|
+
seen.add(rel);
|
|
13039
|
+
let abs;
|
|
13040
|
+
try {
|
|
13041
|
+
abs = resolveWithin(base, rel);
|
|
13042
|
+
} catch (e) {
|
|
13043
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
13044
|
+
continue;
|
|
13045
|
+
}
|
|
13046
|
+
if (path23.resolve(abs) === path23.resolve(base)) {
|
|
13047
|
+
out.failed.push({ path: rel, error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
13048
|
+
continue;
|
|
13049
|
+
}
|
|
13050
|
+
if (!fs20.existsSync(abs)) {
|
|
13051
|
+
out.failed.push({ path: rel, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
|
|
13052
|
+
continue;
|
|
13053
|
+
}
|
|
13054
|
+
try {
|
|
13055
|
+
fs20.rmSync(abs, { recursive: true, force: true });
|
|
13056
|
+
out.deleted.push(toPosix(path23.relative(base, abs)));
|
|
13057
|
+
} catch (e) {
|
|
13058
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
13059
|
+
}
|
|
13060
|
+
}
|
|
13061
|
+
return out;
|
|
13062
|
+
}
|
|
13063
|
+
function formatBytes(n) {
|
|
13064
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
13065
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
13066
|
+
return `${n} B`;
|
|
13067
|
+
}
|
|
13068
|
+
function renameProjectPath(workspaceRoot, relPath, newName) {
|
|
13069
|
+
const base = projectRoot(workspaceRoot);
|
|
13070
|
+
const rel = (relPath ?? "").trim().replace(/^[/\\]+/, "");
|
|
13071
|
+
if (!rel) throw new Error("\u4E0D\u80FD\u91CD\u547D\u540D\u9879\u76EE\u6839\u76EE\u5F55");
|
|
13072
|
+
const abs = resolveWithin(base, rel);
|
|
13073
|
+
const name = (newName ?? "").trim();
|
|
13074
|
+
if (!name) throw new Error("\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
|
|
13075
|
+
if (name === "." || name === "..") throw new Error("\u540D\u79F0\u975E\u6CD5");
|
|
13076
|
+
if (/[/\\]/.test(name)) throw new Error("\u540D\u79F0\u4E0D\u80FD\u5305\u542B\u8DEF\u5F84\u5206\u9694\u7B26");
|
|
13077
|
+
if (!fs20.existsSync(abs)) throw new Error("\u6587\u4EF6\u4E0D\u5B58\u5728");
|
|
13078
|
+
const parent = path23.dirname(abs);
|
|
13079
|
+
const target = path23.join(parent, name);
|
|
13080
|
+
if (!isWithinRoot(base, target)) throw new Error("\u8DEF\u5F84\u8D8A\u754C\uFF08\u8D85\u51FA\u9879\u76EE\u5DE5\u4F5C\u533A\uFF09");
|
|
13081
|
+
if (fs20.existsSync(target)) {
|
|
13082
|
+
let same = false;
|
|
13083
|
+
try {
|
|
13084
|
+
const a = fs20.statSync(target);
|
|
13085
|
+
const b = fs20.statSync(abs);
|
|
13086
|
+
same = a.ino === b.ino && a.dev === b.dev;
|
|
13087
|
+
} catch {
|
|
13088
|
+
same = false;
|
|
13089
|
+
}
|
|
13090
|
+
if (!same) throw new Error(`\u5DF2\u5B58\u5728\u540C\u540D\u6587\u4EF6\u6216\u76EE\u5F55: ${name}`);
|
|
13091
|
+
}
|
|
13092
|
+
if (path23.resolve(target) === path23.resolve(abs)) {
|
|
13093
|
+
return { from: toPosix(path23.relative(base, abs)), path: toPosix(path23.relative(base, abs)) };
|
|
13094
|
+
}
|
|
13095
|
+
try {
|
|
13096
|
+
fs20.renameSync(abs, target);
|
|
13097
|
+
} catch (e) {
|
|
13098
|
+
throw new Error(`\u91CD\u547D\u540D\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
13099
|
+
}
|
|
13100
|
+
return { from: toPosix(path23.relative(base, abs)), path: toPosix(path23.relative(base, target)) };
|
|
13101
|
+
}
|
|
13102
|
+
function zipProjectPaths(workspaceRoot, relPaths, opts = {}) {
|
|
13103
|
+
const base = projectRoot(workspaceRoot);
|
|
13104
|
+
const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : MAX_ZIP_BYTES;
|
|
13105
|
+
const rels = (relPaths ?? []).map((x) => (x ?? "").trim().replace(/^[/\\]+/, ""));
|
|
13106
|
+
if (rels.length === 0) throw new Error("\u8BF7\u9009\u62E9\u8981\u538B\u7F29\u7684\u6587\u4EF6\u6216\u76EE\u5F55");
|
|
13107
|
+
if (rels.some((r) => !r)) throw new Error("\u4E0D\u80FD\u538B\u7F29\u9879\u76EE\u6839\u76EE\u5F55");
|
|
13108
|
+
const targets = rels.map((rel) => ({ rel, abs: resolveWithin(base, rel) }));
|
|
13109
|
+
const single = targets.length === 1 ? targets[0].abs : "";
|
|
13110
|
+
const items = [];
|
|
13111
|
+
let total = 0;
|
|
13112
|
+
const overLimit = () => new Error(
|
|
13113
|
+
`\u5171 ${formatBytes(total)}\uFF0C\u8D85\u8FC7 ${formatBytes(maxBytes)} \u4E0A\u9650\uFF0C\u4E0D\u652F\u6301\u538B\u7F29\u4E0B\u8F7D`
|
|
13114
|
+
);
|
|
13115
|
+
const walk = (absDir) => {
|
|
13116
|
+
let dirents;
|
|
13117
|
+
try {
|
|
13118
|
+
dirents = fs20.readdirSync(absDir, { withFileTypes: true });
|
|
13119
|
+
} catch {
|
|
13120
|
+
return 0;
|
|
13121
|
+
}
|
|
13122
|
+
dirents.sort((a, b) => a.name.localeCompare(b.name));
|
|
13123
|
+
let files = 0;
|
|
13124
|
+
for (const d of dirents) {
|
|
13125
|
+
if (d.isSymbolicLink()) continue;
|
|
13126
|
+
const child = path23.join(absDir, d.name);
|
|
13127
|
+
if (d.isDirectory()) {
|
|
13128
|
+
files += walk(child);
|
|
13129
|
+
continue;
|
|
13130
|
+
}
|
|
13131
|
+
if (!d.isFile()) continue;
|
|
13132
|
+
let st;
|
|
13133
|
+
try {
|
|
13134
|
+
st = fs20.statSync(child);
|
|
13135
|
+
} catch {
|
|
13136
|
+
continue;
|
|
13137
|
+
}
|
|
13138
|
+
total += st.size;
|
|
13139
|
+
if (total > maxBytes) throw overLimit();
|
|
13140
|
+
if (items.length >= MAX_ZIP_ENTRIES) {
|
|
13141
|
+
throw new Error(`\u5F52\u6863\u6761\u76EE\u8FC7\u591A\uFF08\u8D85\u8FC7 ${MAX_ZIP_ENTRIES} \u9879\uFF09\uFF0C\u8BF7\u7F29\u5C0F\u8303\u56F4\u540E\u518D\u8BD5`);
|
|
13142
|
+
}
|
|
13143
|
+
items.push({ abs: child, dir: false, size: st.size, mtime: st.mtime });
|
|
13144
|
+
files++;
|
|
13145
|
+
}
|
|
13146
|
+
if (files === 0) {
|
|
13147
|
+
let mtime = /* @__PURE__ */ new Date();
|
|
13148
|
+
try {
|
|
13149
|
+
mtime = fs20.statSync(absDir).mtime;
|
|
13150
|
+
} catch {
|
|
13151
|
+
}
|
|
13152
|
+
items.push({ abs: absDir, dir: true, size: 0, mtime });
|
|
13153
|
+
}
|
|
13154
|
+
return files;
|
|
13155
|
+
};
|
|
13156
|
+
for (const t of targets) {
|
|
13157
|
+
let st;
|
|
13158
|
+
try {
|
|
13159
|
+
st = fs20.statSync(t.abs);
|
|
13160
|
+
} catch {
|
|
13161
|
+
throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${t.rel}`);
|
|
13162
|
+
}
|
|
13163
|
+
if (st.isDirectory()) {
|
|
13164
|
+
walk(t.abs);
|
|
13165
|
+
continue;
|
|
13166
|
+
}
|
|
13167
|
+
total += st.size;
|
|
13168
|
+
if (total > maxBytes) throw overLimit();
|
|
13169
|
+
items.push({ abs: t.abs, dir: false, size: st.size, mtime: st.mtime });
|
|
13170
|
+
}
|
|
13171
|
+
const nameOf = (abs) => {
|
|
13172
|
+
const rel = toPosix(path23.relative(single || base, abs));
|
|
13173
|
+
return rel || path23.posix.basename(toPosix(abs));
|
|
13174
|
+
};
|
|
13175
|
+
const zipEntries = items.map((it) => {
|
|
13176
|
+
const name2 = nameOf(it.abs);
|
|
13177
|
+
if (it.dir) return { name: `${name2}/`, mtime: it.mtime };
|
|
13178
|
+
return { name: name2, data: fs20.readFileSync(it.abs), mtime: it.mtime };
|
|
13179
|
+
});
|
|
13180
|
+
const buf = buildZip(zipEntries);
|
|
13181
|
+
const first = targets[0].rel;
|
|
13182
|
+
const name = single ? `${path23.posix.basename(toPosix(first))}.zip` : "archive.zip";
|
|
13183
|
+
return {
|
|
13184
|
+
name,
|
|
13185
|
+
size: buf.length,
|
|
13186
|
+
total,
|
|
13187
|
+
entries: zipEntries.length,
|
|
13188
|
+
base64: buf.toString("base64")
|
|
13189
|
+
};
|
|
13190
|
+
}
|
|
12004
13191
|
|
|
12005
13192
|
// src/server/projectOps.ts
|
|
12006
13193
|
init_cjs_shims();
|
|
@@ -12330,6 +13517,7 @@ function projectStats(manager, tenantId) {
|
|
|
12330
13517
|
|
|
12331
13518
|
// src/server/nodeHost.ts
|
|
12332
13519
|
init_cjs_shims();
|
|
13520
|
+
var import_child_process = require("child_process");
|
|
12333
13521
|
var fs21 = __toESM(require("fs"), 1);
|
|
12334
13522
|
var os6 = __toESM(require("os"), 1);
|
|
12335
13523
|
function diskInfo(pathStr) {
|
|
@@ -12344,6 +13532,64 @@ function diskInfo(pathStr) {
|
|
|
12344
13532
|
return null;
|
|
12345
13533
|
}
|
|
12346
13534
|
}
|
|
13535
|
+
var PSEUDO_FS = /^(devfs|tmpfs|devtmpfs|proc|procfs|sysfs|none|udev|shm|mqueue|devpts|hugetlbfs|debugfs|tracefs|securityfs|pstore|bpf|configfs|fusectl|nsfs|autofs|efivarfs|ramfs|map)$/;
|
|
13536
|
+
function isInternalMount(mount) {
|
|
13537
|
+
if (mount === "/") return false;
|
|
13538
|
+
const prefixes = ["/proc", "/sys", "/run", "/snap"];
|
|
13539
|
+
if (process.platform === "darwin") prefixes.push("/System/Volumes");
|
|
13540
|
+
return prefixes.some((p) => mount === p || mount.startsWith(`${p}/`));
|
|
13541
|
+
}
|
|
13542
|
+
function readOnlyVolumes() {
|
|
13543
|
+
const out = /* @__PURE__ */ new Set();
|
|
13544
|
+
const res = (0, import_child_process.spawnSync)("mount", [], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
13545
|
+
if (res.error || res.status !== 0 || !res.stdout) return out;
|
|
13546
|
+
for (const line of res.stdout.split("\n")) {
|
|
13547
|
+
const m = line.match(/^.+? on (.+?) \(([^)]*)\)$/);
|
|
13548
|
+
if (!m || m[1] === "/") continue;
|
|
13549
|
+
if (m[2].split(",").some((opt) => opt.trim() === "read-only")) out.add(m[1]);
|
|
13550
|
+
}
|
|
13551
|
+
return out;
|
|
13552
|
+
}
|
|
13553
|
+
function diskList(workspacePath) {
|
|
13554
|
+
const out = [];
|
|
13555
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13556
|
+
const push = (path36, total, free) => {
|
|
13557
|
+
if (!path36 || seen.has(path36) || !Number.isFinite(total) || total <= 0) return;
|
|
13558
|
+
seen.add(path36);
|
|
13559
|
+
out.push({ path: path36, total, free });
|
|
13560
|
+
};
|
|
13561
|
+
if (process.platform === "win32") {
|
|
13562
|
+
for (const letter of "CDEFGHIJKLMNOPQRSTUVWXYZ") {
|
|
13563
|
+
const root = `${letter}:\\`;
|
|
13564
|
+
const d = diskInfo(root);
|
|
13565
|
+
if (d) push(root, d.total, d.free);
|
|
13566
|
+
}
|
|
13567
|
+
} else {
|
|
13568
|
+
const readonly = process.platform === "darwin" ? readOnlyVolumes() : /* @__PURE__ */ new Set();
|
|
13569
|
+
const res = (0, import_child_process.spawnSync)("df", ["-kP"], {
|
|
13570
|
+
encoding: "utf-8",
|
|
13571
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
13572
|
+
});
|
|
13573
|
+
if (!res.error && res.status === 0 && res.stdout) {
|
|
13574
|
+
for (const line of res.stdout.split("\n").slice(1)) {
|
|
13575
|
+
const m = line.trim().match(/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\d+%\s+(.+)$/);
|
|
13576
|
+
if (!m || PSEUDO_FS.test(m[1])) continue;
|
|
13577
|
+
if (/^\/dev\/loop\d+$/.test(m[1])) continue;
|
|
13578
|
+
if (isInternalMount(m[5]) || readonly.has(m[5])) continue;
|
|
13579
|
+
push(m[5], Number(m[2]) * 1024, Number(m[4]) * 1024);
|
|
13580
|
+
}
|
|
13581
|
+
}
|
|
13582
|
+
}
|
|
13583
|
+
const primary = diskInfo(workspacePath);
|
|
13584
|
+
if (primary) {
|
|
13585
|
+
const covered = out.some((d) => {
|
|
13586
|
+
const root = d.path.endsWith("/") || d.path.endsWith("\\") ? d.path : `${d.path}/`;
|
|
13587
|
+
return workspacePath === d.path || workspacePath.startsWith(root);
|
|
13588
|
+
});
|
|
13589
|
+
if (!covered) push(primary.path, primary.total, primary.free);
|
|
13590
|
+
}
|
|
13591
|
+
return out.sort((a, b) => b.total - a.total);
|
|
13592
|
+
}
|
|
12347
13593
|
function networkInfo() {
|
|
12348
13594
|
const out = [];
|
|
12349
13595
|
const ifaces = os6.networkInterfaces();
|
|
@@ -12399,6 +13645,7 @@ function overview(store, manager) {
|
|
|
12399
13645
|
function buildNodeHost(store, manager, diskPath) {
|
|
12400
13646
|
const cpus2 = os6.cpus();
|
|
12401
13647
|
const ov = overview(store, manager);
|
|
13648
|
+
const disks = diskList(diskPath || process.cwd());
|
|
12402
13649
|
return {
|
|
12403
13650
|
nodeId: store.loadNodeId() ?? "",
|
|
12404
13651
|
hostname: os6.hostname(),
|
|
@@ -12406,7 +13653,7 @@ function buildNodeHost(store, manager, diskPath) {
|
|
|
12406
13653
|
arch: process.arch,
|
|
12407
13654
|
release: os6.release(),
|
|
12408
13655
|
uptimeSec: Math.round(os6.uptime()),
|
|
12409
|
-
version: "0.2.
|
|
13656
|
+
version: "0.2.20",
|
|
12410
13657
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
12411
13658
|
cpu: {
|
|
12412
13659
|
count: cpus2.length,
|
|
@@ -12416,6 +13663,7 @@ function buildNodeHost(store, manager, diskPath) {
|
|
|
12416
13663
|
memory: { total: os6.totalmem(), free: os6.freemem() },
|
|
12417
13664
|
network: networkInfo(),
|
|
12418
13665
|
disk: diskInfo(diskPath || process.cwd()),
|
|
13666
|
+
disks,
|
|
12419
13667
|
...ov
|
|
12420
13668
|
};
|
|
12421
13669
|
}
|
|
@@ -12549,6 +13797,7 @@ function parseApprovalDecision(raw) {
|
|
|
12549
13797
|
}
|
|
12550
13798
|
function createApp(store, manager, opts = {}) {
|
|
12551
13799
|
setupServerHandlers(manager);
|
|
13800
|
+
const updater = opts.updater ?? null;
|
|
12552
13801
|
const routes = [
|
|
12553
13802
|
// ---------- 会话管理 ----------
|
|
12554
13803
|
{
|
|
@@ -12690,6 +13939,18 @@ function createApp(store, manager, opts = {}) {
|
|
|
12690
13939
|
ok(res, { accepted: true });
|
|
12691
13940
|
}
|
|
12692
13941
|
},
|
|
13942
|
+
{
|
|
13943
|
+
method: "POST",
|
|
13944
|
+
pattern: ["sessions", ":id", "steer", "remove"],
|
|
13945
|
+
// 撤销未注入的补充信息
|
|
13946
|
+
fn: (_req, res, { tenant, params, body }) => {
|
|
13947
|
+
const index = Number(body?.index);
|
|
13948
|
+
if (!Number.isInteger(index))
|
|
13949
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
13950
|
+
const pendingSteers = manager.unsteer(tenant, params["id"], index);
|
|
13951
|
+
ok(res, { accepted: true, pendingSteers });
|
|
13952
|
+
}
|
|
13953
|
+
},
|
|
12693
13954
|
{
|
|
12694
13955
|
method: "POST",
|
|
12695
13956
|
pattern: ["sessions", ":id", "interrupt"],
|
|
@@ -13220,6 +14481,30 @@ function createApp(store, manager, opts = {}) {
|
|
|
13220
14481
|
}
|
|
13221
14482
|
}
|
|
13222
14483
|
},
|
|
14484
|
+
{
|
|
14485
|
+
// 二进制写入(图片编辑保存):body = { path, base64 }
|
|
14486
|
+
method: "POST",
|
|
14487
|
+
pattern: ["files", "binary"],
|
|
14488
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14489
|
+
const raw = body ?? {};
|
|
14490
|
+
try {
|
|
14491
|
+
ok(
|
|
14492
|
+
res,
|
|
14493
|
+
writeProjectFileBase64(
|
|
14494
|
+
tenant.workspaceRoot ?? null,
|
|
14495
|
+
String(raw.path ?? ""),
|
|
14496
|
+
String(raw.base64 ?? "")
|
|
14497
|
+
)
|
|
14498
|
+
);
|
|
14499
|
+
} catch (e) {
|
|
14500
|
+
throw new ApiError(
|
|
14501
|
+
400,
|
|
14502
|
+
"bad_request",
|
|
14503
|
+
e instanceof Error ? e.message : String(e)
|
|
14504
|
+
);
|
|
14505
|
+
}
|
|
14506
|
+
}
|
|
14507
|
+
},
|
|
13223
14508
|
// ---------- 项目终端(在工作区执行命令) ----------
|
|
13224
14509
|
{
|
|
13225
14510
|
method: "POST",
|
|
@@ -13570,6 +14855,21 @@ function createApp(store, manager, opts = {}) {
|
|
|
13570
14855
|
}
|
|
13571
14856
|
}
|
|
13572
14857
|
},
|
|
14858
|
+
{
|
|
14859
|
+
method: "POST",
|
|
14860
|
+
pattern: ["files", "delete"],
|
|
14861
|
+
// 删除文件 / 目录(目录递归);逐条返回失败原因
|
|
14862
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14863
|
+
const raw = body ?? {};
|
|
14864
|
+
const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
|
|
14865
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
14866
|
+
try {
|
|
14867
|
+
ok(res, deleteProjectPaths(tenant.workspaceRoot ?? null, paths));
|
|
14868
|
+
} catch (e) {
|
|
14869
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
14870
|
+
}
|
|
14871
|
+
}
|
|
14872
|
+
},
|
|
13573
14873
|
{
|
|
13574
14874
|
method: "POST",
|
|
13575
14875
|
pattern: ["files", "create"],
|
|
@@ -13589,6 +14889,41 @@ function createApp(store, manager, opts = {}) {
|
|
|
13589
14889
|
}
|
|
13590
14890
|
}
|
|
13591
14891
|
},
|
|
14892
|
+
{
|
|
14893
|
+
method: "POST",
|
|
14894
|
+
pattern: ["files", "rename"],
|
|
14895
|
+
// 重命名(只改同目录下的名字)
|
|
14896
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14897
|
+
const raw = body ?? {};
|
|
14898
|
+
try {
|
|
14899
|
+
ok(
|
|
14900
|
+
res,
|
|
14901
|
+
renameProjectPath(
|
|
14902
|
+
tenant.workspaceRoot ?? null,
|
|
14903
|
+
String(raw.path ?? ""),
|
|
14904
|
+
String(raw.name ?? "")
|
|
14905
|
+
)
|
|
14906
|
+
);
|
|
14907
|
+
} catch (e) {
|
|
14908
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
14909
|
+
}
|
|
14910
|
+
}
|
|
14911
|
+
},
|
|
14912
|
+
{
|
|
14913
|
+
method: "POST",
|
|
14914
|
+
pattern: ["files", "zip"],
|
|
14915
|
+
// 压缩下载(多选一起打包;超上限由实现拒绝)
|
|
14916
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14917
|
+
const raw = body ?? {};
|
|
14918
|
+
const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
|
|
14919
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
14920
|
+
try {
|
|
14921
|
+
ok(res, zipProjectPaths(tenant.workspaceRoot ?? null, paths));
|
|
14922
|
+
} catch (e) {
|
|
14923
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
14924
|
+
}
|
|
14925
|
+
}
|
|
14926
|
+
},
|
|
13592
14927
|
// ---------- 项目(= 租户)管理:控制台多项目视图 ----------
|
|
13593
14928
|
// 仅管理员密钥可用;项目级与会话级接口仍用项目自己的 tenant API Key
|
|
13594
14929
|
// (或管理员密钥 + X-Project-Id 头),互不暴露明文密钥。
|
|
@@ -13647,6 +14982,29 @@ function createApp(store, manager, opts = {}) {
|
|
|
13647
14982
|
ok(res, nodeLlmView(next));
|
|
13648
14983
|
}
|
|
13649
14984
|
},
|
|
14985
|
+
// 大模型账户余额(控制台「余额不足 → 查看余额并充值」用;由节点向 Hub 代查)
|
|
14986
|
+
{
|
|
14987
|
+
method: "GET",
|
|
14988
|
+
pattern: ["node", "llm", "balance"],
|
|
14989
|
+
adminOnly: true,
|
|
14990
|
+
fn: async (_req, res) => {
|
|
14991
|
+
ok(res, await fetchLlmBalance(store));
|
|
14992
|
+
}
|
|
14993
|
+
},
|
|
14994
|
+
// 重新拉取平台内置模型凭据(用户点了「刷新内置模型」,或 Key 在控制台被重置)
|
|
14995
|
+
{
|
|
14996
|
+
method: "POST",
|
|
14997
|
+
pattern: ["node", "llm", "builtin-refresh"],
|
|
14998
|
+
adminOnly: true,
|
|
14999
|
+
fn: async (_req, res) => {
|
|
15000
|
+
const r = await ensureBuiltinProvider(store, { force: true });
|
|
15001
|
+
if (r.status !== "ready") {
|
|
15002
|
+
ok(res, { ok: false, reason: r.reason });
|
|
15003
|
+
return;
|
|
15004
|
+
}
|
|
15005
|
+
ok(res, { ok: true, view: nodeLlmView(store.loadNodeLlm()) });
|
|
15006
|
+
}
|
|
15007
|
+
},
|
|
13650
15008
|
// ---------- 用量(账本 + 汇总 + 价格) ----------
|
|
13651
15009
|
{
|
|
13652
15010
|
method: "GET",
|
|
@@ -13798,6 +15156,34 @@ function createApp(store, manager, opts = {}) {
|
|
|
13798
15156
|
}
|
|
13799
15157
|
}
|
|
13800
15158
|
},
|
|
15159
|
+
// ---------- 节点自更新(状态 / 检查 / 安装) ----------
|
|
15160
|
+
// action=install 先回执、后台安装并退出进程(由服务管理器拉起新版本),
|
|
15161
|
+
// 因此回执成功只代表「已受理」,是否升级成功要用 action=status 再确认。
|
|
15162
|
+
{
|
|
15163
|
+
method: "POST",
|
|
15164
|
+
pattern: ["node", "self-update"],
|
|
15165
|
+
projectOptional: true,
|
|
15166
|
+
fn: async (req2, res, { body }) => {
|
|
15167
|
+
const b = body ?? {};
|
|
15168
|
+
const action = String(b.action ?? "status");
|
|
15169
|
+
if (!updater) {
|
|
15170
|
+
throw new ApiError(400, "bad_request", "\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A\u8282\u70B9\u672A\u4EE5 serve \u6A21\u5F0F\u8FD0\u884C");
|
|
15171
|
+
}
|
|
15172
|
+
if (action === "status") {
|
|
15173
|
+
ok(res, updater.status());
|
|
15174
|
+
return;
|
|
15175
|
+
}
|
|
15176
|
+
if (action === "check") {
|
|
15177
|
+
ok(res, await updater.inspect());
|
|
15178
|
+
return;
|
|
15179
|
+
}
|
|
15180
|
+
if (action === "install") {
|
|
15181
|
+
ok(res, await updater.requestInstall(b.force === true));
|
|
15182
|
+
return;
|
|
15183
|
+
}
|
|
15184
|
+
throw new ApiError(400, "bad_request", `\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
15185
|
+
}
|
|
15186
|
+
},
|
|
13801
15187
|
// ---------- 节点级会话状态(跨项目;只含标识与 status) ----------
|
|
13802
15188
|
// 供节点首页 / 工作台建立初始视图,后续变化由 session_status 通知增量推送。
|
|
13803
15189
|
{
|
|
@@ -13960,7 +15346,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
13960
15346
|
// 阻止反向代理(nginx 等)缓冲 SSE 流
|
|
13961
15347
|
"X-Accel-Buffering": "no"
|
|
13962
15348
|
});
|
|
13963
|
-
const
|
|
15349
|
+
const sinceRaw2 = new URL(
|
|
13964
15350
|
req2.url,
|
|
13965
15351
|
"http://localhost"
|
|
13966
15352
|
).searchParams.get("since");
|
|
@@ -13968,19 +15354,51 @@ function createApp(store, manager, opts = {}) {
|
|
|
13968
15354
|
tenant.id,
|
|
13969
15355
|
params["id"],
|
|
13970
15356
|
res,
|
|
13971
|
-
|
|
15357
|
+
sinceRaw2 ? Number(sinceRaw2) : 0
|
|
13972
15358
|
);
|
|
13973
15359
|
return;
|
|
13974
15360
|
}
|
|
13975
|
-
const
|
|
13976
|
-
|
|
15361
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
15362
|
+
const sinceRaw = q.get("since");
|
|
15363
|
+
const beforeRaw = q.get("before");
|
|
15364
|
+
const maxBytesRaw = q.get("maxBytes");
|
|
15365
|
+
ok(
|
|
15366
|
+
res,
|
|
15367
|
+
manager.eventsPage(tenant.id, params["id"], {
|
|
15368
|
+
since: sinceRaw === null ? void 0 : Number(sinceRaw) || 0,
|
|
15369
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
15370
|
+
maxBytes: maxBytesRaw === null ? void 0 : Number(maxBytesRaw) || void 0
|
|
15371
|
+
})
|
|
13977
15372
|
);
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
15373
|
+
}
|
|
15374
|
+
},
|
|
15375
|
+
{
|
|
15376
|
+
method: "GET",
|
|
15377
|
+
pattern: ["sessions", ":id", "turns"],
|
|
15378
|
+
// 轮次汇总(会话详情的列表)
|
|
15379
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
15380
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
15381
|
+
const beforeRaw = q.get("before");
|
|
15382
|
+
const limitRaw = q.get("limit");
|
|
15383
|
+
ok(
|
|
15384
|
+
res,
|
|
15385
|
+
await manager.sessionTurns(tenant.id, params["id"], {
|
|
15386
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
15387
|
+
limit: limitRaw === null ? void 0 : Number(limitRaw) || void 0
|
|
15388
|
+
})
|
|
13982
15389
|
);
|
|
13983
|
-
|
|
15390
|
+
}
|
|
15391
|
+
},
|
|
15392
|
+
{
|
|
15393
|
+
method: "GET",
|
|
15394
|
+
// 单轮详情:用户消息(含补充) + 最终回复 + 产出文件
|
|
15395
|
+
pattern: ["sessions", ":id", "turns", ":seq", "reply"],
|
|
15396
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
15397
|
+
const startSeq = Number(params["seq"]);
|
|
15398
|
+
if (!Number.isFinite(startSeq) || startSeq <= 0) {
|
|
15399
|
+
throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
15400
|
+
}
|
|
15401
|
+
ok(res, await manager.sessionTurnReply(tenant.id, params["id"], startSeq));
|
|
13984
15402
|
}
|
|
13985
15403
|
}
|
|
13986
15404
|
];
|
|
@@ -14013,7 +15431,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
14013
15431
|
json(res, 200, {
|
|
14014
15432
|
status: "ok",
|
|
14015
15433
|
// 版本让运维/探针能确认线上跑的是哪个构建(更新与回滚据此判断)
|
|
14016
|
-
version: "0.2.
|
|
15434
|
+
version: "0.2.20",
|
|
14017
15435
|
features: { mcp: hasServerMcpConfig() }
|
|
14018
15436
|
});
|
|
14019
15437
|
return;
|
|
@@ -14286,6 +15704,24 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14286
15704
|
switch (env3.type) {
|
|
14287
15705
|
case "ping":
|
|
14288
15706
|
return ok2({ pong: true, ts: Date.now() });
|
|
15707
|
+
case "self_update": {
|
|
15708
|
+
const updater = deps.getUpdater?.() ?? null;
|
|
15709
|
+
if (!updater) {
|
|
15710
|
+
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");
|
|
15711
|
+
}
|
|
15712
|
+
const action = String(payload.action ?? "status");
|
|
15713
|
+
if (action === "status") {
|
|
15714
|
+
return ok2(updater.status());
|
|
15715
|
+
}
|
|
15716
|
+
if (action === "check") {
|
|
15717
|
+
return ok2(await updater.inspect());
|
|
15718
|
+
}
|
|
15719
|
+
if (action === "install") {
|
|
15720
|
+
const started = await updater.requestInstall(payload.force === true);
|
|
15721
|
+
return ok2(started);
|
|
15722
|
+
}
|
|
15723
|
+
return fail(`\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
15724
|
+
}
|
|
14289
15725
|
case "get_node_info":
|
|
14290
15726
|
return ok2(
|
|
14291
15727
|
buildNodeInfo(store, {
|
|
@@ -14335,6 +15771,22 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14335
15771
|
store.saveNodeLlm(next);
|
|
14336
15772
|
return ok2(nodeLlmView(next));
|
|
14337
15773
|
}
|
|
15774
|
+
/*
|
|
15775
|
+
* 重新拉取平台内置模型(志前 API)。
|
|
15776
|
+
*
|
|
15777
|
+
* 用途:用户在控制台点了「刷新」、或内置模型的 Key 在控制台被重置后需要换新。
|
|
15778
|
+
* 走写链串行——它会改写 connections.json,不能与 update_node_llm 并发。
|
|
15779
|
+
*/
|
|
15780
|
+
// 余额查询:只读、幂等,可进只读并发池
|
|
15781
|
+
case "get_llm_balance":
|
|
15782
|
+
return ok2(await fetchLlmBalance(store));
|
|
15783
|
+
case "refresh_builtin_llm": {
|
|
15784
|
+
const r = await ensureBuiltinProvider(store, { force: true });
|
|
15785
|
+
if (r.status !== "ready") {
|
|
15786
|
+
return ok2({ ok: false, reason: r.reason });
|
|
15787
|
+
}
|
|
15788
|
+
return ok2({ ok: true, view: nodeLlmView(store.loadNodeLlm()) });
|
|
15789
|
+
}
|
|
14338
15790
|
case "get_usage_summary": {
|
|
14339
15791
|
const ledger = manager.usageLedger();
|
|
14340
15792
|
if (!ledger) return ok2({ total: null, groups: [] });
|
|
@@ -14408,8 +15860,22 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14408
15860
|
);
|
|
14409
15861
|
}
|
|
14410
15862
|
case "get_events": {
|
|
14411
|
-
const since =
|
|
14412
|
-
|
|
15863
|
+
const since = payload.since === void 0 ? void 0 : Number(payload.since) || 0;
|
|
15864
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
15865
|
+
const maxBytes = Number(payload.maxBytes) || void 0;
|
|
15866
|
+
return ok2(
|
|
15867
|
+
manager.eventsPage(project().id, sessionId(), { since, before, maxBytes })
|
|
15868
|
+
);
|
|
15869
|
+
}
|
|
15870
|
+
case "get_session_turns": {
|
|
15871
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
15872
|
+
const limit = Number(payload.limit) || void 0;
|
|
15873
|
+
return ok2(await manager.sessionTurns(project().id, sessionId(), { before, limit }));
|
|
15874
|
+
}
|
|
15875
|
+
case "get_session_turn_reply": {
|
|
15876
|
+
const startSeq = Number(payload.startSeq) || 0;
|
|
15877
|
+
if (startSeq <= 0) throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
15878
|
+
return ok2(await manager.sessionTurnReply(project().id, sessionId(), startSeq));
|
|
14413
15879
|
}
|
|
14414
15880
|
case "get_config":
|
|
14415
15881
|
return ok2(configView2(project()));
|
|
@@ -14470,6 +15936,16 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14470
15936
|
)
|
|
14471
15937
|
);
|
|
14472
15938
|
}
|
|
15939
|
+
case "write_file_base64": {
|
|
15940
|
+
const t = project();
|
|
15941
|
+
return ok2(
|
|
15942
|
+
writeProjectFileBase64(
|
|
15943
|
+
t.workspaceRoot ?? null,
|
|
15944
|
+
String(payload.path ?? ""),
|
|
15945
|
+
String(payload.base64 ?? "")
|
|
15946
|
+
)
|
|
15947
|
+
);
|
|
15948
|
+
}
|
|
14473
15949
|
case "create_dir": {
|
|
14474
15950
|
const t = project();
|
|
14475
15951
|
return ok2(createProjectDir(t.workspaceRoot ?? null, String(payload.path ?? "")));
|
|
@@ -14484,10 +15960,35 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14484
15960
|
)
|
|
14485
15961
|
);
|
|
14486
15962
|
}
|
|
15963
|
+
case "delete_files": {
|
|
15964
|
+
const t = project();
|
|
15965
|
+
const raw = payload.paths;
|
|
15966
|
+
const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
|
|
15967
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
15968
|
+
return ok2(deleteProjectPaths(t.workspaceRoot ?? null, paths));
|
|
15969
|
+
}
|
|
14487
15970
|
case "search_files": {
|
|
14488
15971
|
const t = project();
|
|
14489
15972
|
return ok2(searchProjectFiles(t.workspaceRoot ?? null, String(payload.query ?? "")));
|
|
14490
15973
|
}
|
|
15974
|
+
case "rename_path": {
|
|
15975
|
+
const t = project();
|
|
15976
|
+
return ok2(
|
|
15977
|
+
renameProjectPath(
|
|
15978
|
+
t.workspaceRoot ?? null,
|
|
15979
|
+
String(payload.path ?? ""),
|
|
15980
|
+
String(payload.name ?? "")
|
|
15981
|
+
)
|
|
15982
|
+
);
|
|
15983
|
+
}
|
|
15984
|
+
case "zip_download": {
|
|
15985
|
+
const t = project();
|
|
15986
|
+
const raw = payload.paths;
|
|
15987
|
+
const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
|
|
15988
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
15989
|
+
const maxBytes = typeof payload.maxBytes === "number" ? payload.maxBytes : void 0;
|
|
15990
|
+
return ok2(zipProjectPaths(t.workspaceRoot ?? null, paths, { maxBytes }));
|
|
15991
|
+
}
|
|
14491
15992
|
case "list_tools": {
|
|
14492
15993
|
const t = project();
|
|
14493
15994
|
return ok2(buildToolCatalog(t.workspaceRoot ?? process.cwd()));
|
|
@@ -14710,6 +16211,13 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14710
16211
|
manager.steer(project(), sessionId(), text);
|
|
14711
16212
|
return ok2({ accepted: true });
|
|
14712
16213
|
}
|
|
16214
|
+
case "unsteer": {
|
|
16215
|
+
const index = Number(payload.index);
|
|
16216
|
+
if (!Number.isInteger(index)) {
|
|
16217
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
16218
|
+
}
|
|
16219
|
+
return ok2({ accepted: true, pendingSteers: manager.unsteer(project(), sessionId(), index) });
|
|
16220
|
+
}
|
|
14713
16221
|
case "interrupt":
|
|
14714
16222
|
manager.interrupt(project(), sessionId());
|
|
14715
16223
|
return ok2({ accepted: true });
|
|
@@ -14833,6 +16341,22 @@ function toWireAck(ack) {
|
|
|
14833
16341
|
data: ack.data === void 0 ? void 0 : utf8Encode(JSON.stringify(ack.data))
|
|
14834
16342
|
};
|
|
14835
16343
|
}
|
|
16344
|
+
var ACK_JSON_MAX_BYTES = MAX_FRAME_BYTES - 64 * 1024;
|
|
16345
|
+
function mb(bytes) {
|
|
16346
|
+
return (bytes / (1024 * 1024)).toFixed(1);
|
|
16347
|
+
}
|
|
16348
|
+
function guardAckSize(ack, json2) {
|
|
16349
|
+
if (!ack.ok || ack.data === void 0) return ack;
|
|
16350
|
+
const bytes = Buffer.byteLength(json2, "utf8");
|
|
16351
|
+
if (bytes <= ACK_JSON_MAX_BYTES) return ack;
|
|
16352
|
+
return {
|
|
16353
|
+
reqId: ack.reqId,
|
|
16354
|
+
type: ack.type,
|
|
16355
|
+
ok: false,
|
|
16356
|
+
// 带上实际大小:用户据此能判断「差一点」还是「差很多」,而不是只看到「失败」
|
|
16357
|
+
error: `\u7ED3\u679C\u8FC7\u5927\uFF08${mb(bytes)} MB\uFF09\uFF0C\u8D85\u8FC7\u5355\u5E27\u4F20\u8F93\u4E0A\u9650\uFF08${mb(ACK_JSON_MAX_BYTES)} MB\uFF09\uFF0C\u8BF7\u7F29\u5C0F\u8303\u56F4\u540E\u91CD\u8BD5`
|
|
16358
|
+
};
|
|
16359
|
+
}
|
|
14836
16360
|
function fromWireCommand(env3) {
|
|
14837
16361
|
let payload = {};
|
|
14838
16362
|
if (env3.payload && env3.payload.length > 0) {
|
|
@@ -15303,6 +16827,7 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
|
|
|
15303
16827
|
"get_node_llm",
|
|
15304
16828
|
"list_node_llm_models",
|
|
15305
16829
|
"get_usage_summary",
|
|
16830
|
+
"get_llm_balance",
|
|
15306
16831
|
"get_usage_records",
|
|
15307
16832
|
"get_usage_pricing",
|
|
15308
16833
|
"list_projects",
|
|
@@ -15310,6 +16835,8 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
|
|
|
15310
16835
|
"get_session_states",
|
|
15311
16836
|
"get_state",
|
|
15312
16837
|
"get_events",
|
|
16838
|
+
"get_session_turns",
|
|
16839
|
+
"get_session_turn_reply",
|
|
15313
16840
|
"get_config",
|
|
15314
16841
|
"list_models",
|
|
15315
16842
|
"list_dirs",
|
|
@@ -15559,6 +17086,10 @@ var FleetNodeBridge = class {
|
|
|
15559
17086
|
_dispatchEnvelope(env3) {
|
|
15560
17087
|
const run = () => this._dispatch(env3);
|
|
15561
17088
|
dbgMark(`recv:${env3.type}`, `reqId=${env3.reqId}`);
|
|
17089
|
+
if (env3.type === "self_update" && String(env3.payload?.action ?? "status") !== "install") {
|
|
17090
|
+
this._runReadonly(run);
|
|
17091
|
+
return;
|
|
17092
|
+
}
|
|
15562
17093
|
if (READONLY_CMD_TYPES.has(env3.type) || this._isReadonlyBatch(env3)) {
|
|
15563
17094
|
this._runReadonly(run);
|
|
15564
17095
|
return;
|
|
@@ -15585,7 +17116,7 @@ var FleetNodeBridge = class {
|
|
|
15585
17116
|
_invalidateSessionCache() {
|
|
15586
17117
|
this.writeEpoch += 1;
|
|
15587
17118
|
for (const key of [...this.cmdResultCache.keys()]) {
|
|
15588
|
-
if (/^(get_state|get_events|list_sessions|get_session_states)\|/.test(key)) {
|
|
17119
|
+
if (/^(get_state|get_events|get_session_turns|get_session_turn_reply|list_sessions|get_session_states)\|/.test(key)) {
|
|
15589
17120
|
this.cmdResultCache.delete(key);
|
|
15590
17121
|
}
|
|
15591
17122
|
}
|
|
@@ -15721,11 +17252,17 @@ var FleetNodeBridge = class {
|
|
|
15721
17252
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
15722
17253
|
platform: process.platform,
|
|
15723
17254
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15724
|
-
version: "0.2.
|
|
17255
|
+
version: "0.2.20",
|
|
15725
17256
|
ip: primaryIp()
|
|
15726
17257
|
};
|
|
15727
17258
|
}
|
|
15728
|
-
/**
|
|
17259
|
+
/**
|
|
17260
|
+
* 用量汇总(供控制面账户级聚合):总量 + 最近 40 天按天 + 最近 40 天按项目。
|
|
17261
|
+
*
|
|
17262
|
+
* 按项目这一维是为控制台「今日 / 最近 30 天消耗拆到每个项目」服务的:
|
|
17263
|
+
* 账本里的汇总只存了按天、按项目两个独立维度,交叉项只能现算,因此这里
|
|
17264
|
+
* 从明细聚合后随 info 一起上报(Hub 按 (节点, 项目, 天) 覆盖写入,天然幂等)。
|
|
17265
|
+
*/
|
|
15729
17266
|
_usagePayload() {
|
|
15730
17267
|
const ledger = this.opts.deps.manager.usageLedger();
|
|
15731
17268
|
if (!ledger) return void 0;
|
|
@@ -15733,11 +17270,22 @@ var FleetNodeBridge = class {
|
|
|
15733
17270
|
const days = {};
|
|
15734
17271
|
const keys = Object.keys(r.byDay).sort().slice(-40);
|
|
15735
17272
|
for (const k of keys) days[k] = r.byDay[k];
|
|
17273
|
+
const from = keys[0] ?? "";
|
|
17274
|
+
const to = keys[keys.length - 1] ?? "";
|
|
17275
|
+
const projects = {};
|
|
17276
|
+
if (from && to) {
|
|
17277
|
+
for (const [projectId, byDay] of Object.entries(
|
|
17278
|
+
ledger.projectByDay({ from, to })
|
|
17279
|
+
)) {
|
|
17280
|
+
projects[projectId] = { total: sumAgg(Object.values(byDay)), byDay };
|
|
17281
|
+
}
|
|
17282
|
+
}
|
|
15736
17283
|
return {
|
|
15737
17284
|
since: r.since,
|
|
15738
17285
|
total: r.total,
|
|
15739
17286
|
legacy: r.legacy ?? null,
|
|
15740
|
-
byDay: days
|
|
17287
|
+
byDay: days,
|
|
17288
|
+
projects
|
|
15741
17289
|
};
|
|
15742
17290
|
}
|
|
15743
17291
|
/**
|
|
@@ -15772,6 +17320,8 @@ var FleetNodeBridge = class {
|
|
|
15772
17320
|
name: s.name ?? null,
|
|
15773
17321
|
status: s.status,
|
|
15774
17322
|
lastUserMessage: lastUserMessage(s),
|
|
17323
|
+
// 控制面按它排「最近在跑什么」,运行中的长任务才不会被排到后面
|
|
17324
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
15775
17325
|
// 控制面的聚合邮件靠它判断「这台机器最近有没有在干活」;
|
|
15776
17326
|
// 状态跃迁可能被 info 节流跳过,完成时间不会。
|
|
15777
17327
|
lastCompletedAt: s.lastCompletedAt ?? null
|
|
@@ -15800,7 +17350,7 @@ var FleetNodeBridge = class {
|
|
|
15800
17350
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
15801
17351
|
platform: process.platform,
|
|
15802
17352
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15803
|
-
version: "0.2.
|
|
17353
|
+
version: "0.2.20",
|
|
15804
17354
|
ip: primaryIp(),
|
|
15805
17355
|
projects,
|
|
15806
17356
|
totals,
|
|
@@ -15962,7 +17512,13 @@ var FleetNodeBridge = class {
|
|
|
15962
17512
|
const stamped = { ...ack, reqId: env3.reqId, type: env3.type };
|
|
15963
17513
|
const json2 = JSON.stringify(stamped);
|
|
15964
17514
|
const __t1 = dbgNow();
|
|
15965
|
-
|
|
17515
|
+
const payload = guardAckSize(stamped, json2);
|
|
17516
|
+
if (payload !== stamped) {
|
|
17517
|
+
console.warn(
|
|
17518
|
+
`[fleet] ${env3.type} \u56DE\u6267 ${(json2.length / 1024 / 1024).toFixed(1)}MB \u8D85\u8FC7\u5355\u5E27\u4E0A\u9650\uFF0C\u5DF2\u964D\u7EA7\u4E3A\u9519\u8BEF\u56DE\u6267`
|
|
17519
|
+
);
|
|
17520
|
+
}
|
|
17521
|
+
this._send({ kind: "ack", clientId: env3.clientId, ack: payload });
|
|
15966
17522
|
dbgMs(
|
|
15967
17523
|
"publishAck",
|
|
15968
17524
|
__t0,
|
|
@@ -17429,7 +18985,7 @@ function verifySignature(data, signatureBase64, publicKey) {
|
|
|
17429
18985
|
}
|
|
17430
18986
|
|
|
17431
18987
|
// src/update/downloader.ts
|
|
17432
|
-
var
|
|
18988
|
+
var MAX_BINARY_BYTES2 = 400 * 1024 * 1024;
|
|
17433
18989
|
var MANIFEST_TIMEOUT_MS = 15e3;
|
|
17434
18990
|
var DOWNLOAD_TIMEOUT_MS2 = 10 * 6e4;
|
|
17435
18991
|
async function fetchManifest(url, publicKey, opts = {}) {
|
|
@@ -17475,7 +19031,7 @@ async function downloadBinary(bin, destPath, log = () => {
|
|
|
17475
19031
|
if (!res.ok) return { error: `\u4E0B\u8F7D\u5931\u8D25 HTTP ${res.status}` };
|
|
17476
19032
|
if (!res.body) return { error: "\u4E0B\u8F7D\u54CD\u5E94\u6CA1\u6709 body" };
|
|
17477
19033
|
const expected = bin.size && bin.size > 0 ? bin.size : void 0;
|
|
17478
|
-
const limit = expected ? Math.max(expected * 2, 1024 * 1024) :
|
|
19034
|
+
const limit = expected ? Math.max(expected * 2, 1024 * 1024) : MAX_BINARY_BYTES2;
|
|
17479
19035
|
const hash = (0, import_node_crypto5.createHash)("sha256");
|
|
17480
19036
|
let bytes = 0;
|
|
17481
19037
|
try {
|
|
@@ -17538,7 +19094,7 @@ var path29 = __toESM(require("path"), 1);
|
|
|
17538
19094
|
var import_node_child_process5 = require("child_process");
|
|
17539
19095
|
var import_node_module3 = require("module");
|
|
17540
19096
|
function currentVersion() {
|
|
17541
|
-
const injected = "0.2.
|
|
19097
|
+
const injected = "0.2.20"?.trim();
|
|
17542
19098
|
if (injected) return injected;
|
|
17543
19099
|
try {
|
|
17544
19100
|
const req2 = (0, import_node_module3.createRequire)(importMetaUrl);
|
|
@@ -17852,6 +19408,31 @@ function nextIntervalMs() {
|
|
|
17852
19408
|
const jitter = base * 0.15 * (Math.random() * 2 - 1);
|
|
17853
19409
|
return Math.max(6e4, Math.round(base + jitter));
|
|
17854
19410
|
}
|
|
19411
|
+
function evaluateManifest(base, manifest) {
|
|
19412
|
+
if (!parseVersion(base.currentVersion) || compareVersions(manifest.version, base.currentVersion) <= 0) {
|
|
19413
|
+
return {
|
|
19414
|
+
...base,
|
|
19415
|
+
updateAvailable: false,
|
|
19416
|
+
latestVersion: manifest.version,
|
|
19417
|
+
reason: `\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF08v${base.currentVersion}\uFF09`
|
|
19418
|
+
};
|
|
19419
|
+
}
|
|
19420
|
+
if (manifest.minimumVersion && compareVersions(base.currentVersion, manifest.minimumVersion) < 0) {
|
|
19421
|
+
return {
|
|
19422
|
+
...base,
|
|
19423
|
+
updateAvailable: false,
|
|
19424
|
+
latestVersion: manifest.version,
|
|
19425
|
+
manualUpgradeRequired: manifest.minimumVersion,
|
|
19426
|
+
reason: `\u5F53\u524D\u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u624B\u52A8\u5347\u7EA7\u5230 >= ${manifest.minimumVersion}`
|
|
19427
|
+
};
|
|
19428
|
+
}
|
|
19429
|
+
return {
|
|
19430
|
+
...base,
|
|
19431
|
+
updateAvailable: true,
|
|
19432
|
+
latestVersion: manifest.version,
|
|
19433
|
+
...manifest.notes ? { notes: manifest.notes } : {}
|
|
19434
|
+
};
|
|
19435
|
+
}
|
|
17855
19436
|
var UpdateController = class {
|
|
17856
19437
|
constructor(opts, currentVersion2 = currentVersion()) {
|
|
17857
19438
|
this.opts = opts;
|
|
@@ -17940,6 +19521,77 @@ var UpdateController = class {
|
|
|
17940
19521
|
}
|
|
17941
19522
|
this.schedule(nextIntervalMs());
|
|
17942
19523
|
}
|
|
19524
|
+
/**
|
|
19525
|
+
* 只读检查:拉清单比对版本,不下载、不改 state。
|
|
19526
|
+
*
|
|
19527
|
+
* 供远端 `self_update --action check` 与 UI 展示用;与定时器 `check()` 不同,
|
|
19528
|
+
* 这里不加 `checking` 锁、不写 `lastCheckAt`,因此不会干扰自动更新流程。
|
|
19529
|
+
*/
|
|
19530
|
+
async inspect() {
|
|
19531
|
+
const base = {
|
|
19532
|
+
currentVersion: this.currentVersion,
|
|
19533
|
+
channel: updaterChannel(),
|
|
19534
|
+
manifestUrl: this.manifestUrl()
|
|
19535
|
+
};
|
|
19536
|
+
const key = this.publicKey;
|
|
19537
|
+
const cap = selfUpdateCapability(Boolean(key));
|
|
19538
|
+
if (!cap.ok) {
|
|
19539
|
+
return { ...base, updateAvailable: false, reason: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
19540
|
+
}
|
|
19541
|
+
const res = await fetchManifest(base.manifestUrl, key, {
|
|
19542
|
+
allowInsecureUrl: env("ZQAGENT_UPDATE_INSECURE") === "1"
|
|
19543
|
+
});
|
|
19544
|
+
if (!res.manifest) {
|
|
19545
|
+
return {
|
|
19546
|
+
...base,
|
|
19547
|
+
updateAvailable: false,
|
|
19548
|
+
reason: `\u66F4\u65B0\u68C0\u67E5\u5931\u8D25\uFF1A${res.error ?? "\u6E05\u5355\u6821\u9A8C\u5931\u8D25"}`
|
|
19549
|
+
};
|
|
19550
|
+
}
|
|
19551
|
+
return evaluateManifest(base, res.manifest);
|
|
19552
|
+
}
|
|
19553
|
+
/**
|
|
19554
|
+
* 远端触发安装:先同步给出结论(受理/无需更新),安装本身在后台跑。
|
|
19555
|
+
*
|
|
19556
|
+
* 必须「先回执、后安装」:安装成功后进程会 `exit(70)` 交给 systemd 拉起新版本,
|
|
19557
|
+
* 如果等到安装完成再回执,回执永远发不出去(表现为网关超时)。
|
|
19558
|
+
*
|
|
19559
|
+
* 复用定时器同一条 `check()` 路径:验签、下载、自检、失败计数、安装后自检与
|
|
19560
|
+
* 回滚保护全都一致,只是把 `installMode` 设为 `force` 时不等待空闲。
|
|
19561
|
+
*/
|
|
19562
|
+
async requestInstall(force = false) {
|
|
19563
|
+
const fromVersion = this.currentVersion;
|
|
19564
|
+
const cap = selfUpdateCapability(Boolean(this.publicKey));
|
|
19565
|
+
if (!cap.ok) {
|
|
19566
|
+
return { accepted: false, fromVersion, message: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
19567
|
+
}
|
|
19568
|
+
const info = await this.inspect();
|
|
19569
|
+
if (!info.updateAvailable) {
|
|
19570
|
+
return {
|
|
19571
|
+
accepted: false,
|
|
19572
|
+
fromVersion,
|
|
19573
|
+
...info.latestVersion ? { toVersion: info.latestVersion } : {},
|
|
19574
|
+
message: info.reason ?? "\u5DF2\u662F\u6700\u65B0\u7248\u672C"
|
|
19575
|
+
};
|
|
19576
|
+
}
|
|
19577
|
+
if (this.checking || this.installing) {
|
|
19578
|
+
return {
|
|
19579
|
+
accepted: true,
|
|
19580
|
+
fromVersion,
|
|
19581
|
+
toVersion: info.latestVersion,
|
|
19582
|
+
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`
|
|
19583
|
+
};
|
|
19584
|
+
}
|
|
19585
|
+
void this.check(force, false, true).catch((err) => {
|
|
19586
|
+
this.recordFailure(info.latestVersion, `\u5B89\u88C5\u5931\u8D25\uFF1A${err.message}`);
|
|
19587
|
+
});
|
|
19588
|
+
return {
|
|
19589
|
+
accepted: true,
|
|
19590
|
+
fromVersion,
|
|
19591
|
+
toVersion: info.latestVersion,
|
|
19592
|
+
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`
|
|
19593
|
+
};
|
|
19594
|
+
}
|
|
17943
19595
|
/**
|
|
17944
19596
|
* 检查并准备更新;`installMode` 为 true 时下载后立即安装(不再等空闲)。
|
|
17945
19597
|
* 返回可读结论,供 CLI `update --check` 使用。
|
|
@@ -18168,7 +19820,9 @@ function seedDemoTenant(store) {
|
|
|
18168
19820
|
// 未显式指定时随机生成:固定默认密钥一旦随 HOST=0.0.0.0 暴露到公网,等于公开凭据。
|
|
18169
19821
|
// 只在首次创建时生成并落盘,后续启动沿用磁盘上的值(启动日志会打印完整密钥)。
|
|
18170
19822
|
apiKey: env2("TENANT_API_KEY") ?? generateApiKey(),
|
|
18171
|
-
|
|
19823
|
+
// 不再硬编码具体厂商型号:志前 API 只认 deepseek-flash,写死 deepseek-v4-flash
|
|
19824
|
+
// 会让新装节点的第一次请求 404。默认值统一走 defaultTenantModel(见 nodeLlm.ts)。
|
|
19825
|
+
model: defaultTenantModel(store),
|
|
18172
19826
|
provider: {
|
|
18173
19827
|
apiKey: env2("OPENAI_API_KEY"),
|
|
18174
19828
|
baseURL: env2("OPENAI_BASE_URL")
|
|
@@ -18308,6 +19962,8 @@ function startServer(opts = {}) {
|
|
|
18308
19962
|
const store = new Store(dataDir);
|
|
18309
19963
|
const demo = seedDemoTenant(store);
|
|
18310
19964
|
syncTenantModels(store);
|
|
19965
|
+
const nodeModel = nodeDefaultProvider(store)?.model;
|
|
19966
|
+
if (nodeModel) adoptBuiltinModelForTenants(store, nodeModel);
|
|
18311
19967
|
const usageLedger = new UsageLedger(dataDir);
|
|
18312
19968
|
usageLedger.init();
|
|
18313
19969
|
const manager = new SessionManager(store, void 0, { usage: usageLedger });
|
|
@@ -18339,6 +19995,7 @@ function startServer(opts = {}) {
|
|
|
18339
19995
|
const notificationHub = createNotificationHub();
|
|
18340
19996
|
let fleet = null;
|
|
18341
19997
|
let fleetCred = null;
|
|
19998
|
+
let updater = null;
|
|
18342
19999
|
function connectFleet(cred) {
|
|
18343
20000
|
const localId = ensureNodeId(store);
|
|
18344
20001
|
const nodeId = (cred.nodeId ?? "").trim() || localId;
|
|
@@ -18356,6 +20013,8 @@ function startServer(opts = {}) {
|
|
|
18356
20013
|
deps: {
|
|
18357
20014
|
store,
|
|
18358
20015
|
manager,
|
|
20016
|
+
// 闭包推迟求值:桥先于 UpdateController 创建,指令到达时 updater 已就绪
|
|
20017
|
+
getUpdater: () => updater,
|
|
18359
20018
|
applyFleetCred: async (cred2) => {
|
|
18360
20019
|
store.saveFleetCredential(cred2);
|
|
18361
20020
|
setTimeout(() => connectFleet(cred2), 300);
|
|
@@ -18448,8 +20107,12 @@ function startServer(opts = {}) {
|
|
|
18448
20107
|
if (initialCred) {
|
|
18449
20108
|
connectFleet(initialCred);
|
|
18450
20109
|
} else if (opts.autoEnroll) {
|
|
18451
|
-
void fleetController.enroll(opts.autoEnroll).then((st) =>
|
|
20110
|
+
void fleetController.enroll(opts.autoEnroll).then((st) => {
|
|
20111
|
+
console.log(`[fleet] \u5DF2\u81EA\u52A8\u63A5\u5165\uFF0C\u8282\u70B9 ${st.nodeId}\uFF08${st.gatewayUrl}\uFF09`);
|
|
20112
|
+
scheduleBuiltinProvider(store, (m) => console.log(m));
|
|
20113
|
+
}).catch((e) => console.warn(`[fleet] \u81EA\u52A8\u63A5\u5165\u5931\u8D25\uFF08\u53EF\u7A0D\u540E /link \u91CD\u8BD5\uFF09: ${e instanceof Error ? e.message : e}`));
|
|
18452
20114
|
}
|
|
20115
|
+
if (initialCred) scheduleBuiltinProvider(store, (m) => console.log(m));
|
|
18453
20116
|
const pairingCodes = new PairingCodeStore();
|
|
18454
20117
|
const qq = new QqManager({
|
|
18455
20118
|
store,
|
|
@@ -18506,16 +20169,21 @@ function startServer(opts = {}) {
|
|
|
18506
20169
|
scheduleFleetInfoRefresh();
|
|
18507
20170
|
}
|
|
18508
20171
|
});
|
|
20172
|
+
updater = new UpdateController({
|
|
20173
|
+
dataDir,
|
|
20174
|
+
isIdle: () => manager.isIdle(),
|
|
20175
|
+
prepareExit: closeServer
|
|
20176
|
+
});
|
|
18509
20177
|
const server = createApp(store, manager, {
|
|
18510
20178
|
secretBox,
|
|
18511
20179
|
notificationHub,
|
|
18512
20180
|
qq,
|
|
18513
20181
|
adminKey,
|
|
18514
20182
|
fleet: fleetController,
|
|
18515
|
-
host
|
|
20183
|
+
host,
|
|
20184
|
+
updater
|
|
18516
20185
|
});
|
|
18517
20186
|
let closed = false;
|
|
18518
|
-
let updater = null;
|
|
18519
20187
|
function closeServer() {
|
|
18520
20188
|
if (closed) return;
|
|
18521
20189
|
closed = true;
|
|
@@ -18548,13 +20216,6 @@ function startServer(opts = {}) {
|
|
|
18548
20216
|
}
|
|
18549
20217
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
18550
20218
|
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
20219
|
server.timeout = 0;
|
|
18559
20220
|
server.keepAliveTimeout = 65e3;
|
|
18560
20221
|
server.headersTimeout = 66e3;
|
|
@@ -18574,7 +20235,7 @@ function startServer(opts = {}) {
|
|
|
18574
20235
|
} else {
|
|
18575
20236
|
printReady(info, qq);
|
|
18576
20237
|
}
|
|
18577
|
-
updater?.start();
|
|
20238
|
+
if (opts.autoUpdate) updater?.start();
|
|
18578
20239
|
resolve20(info);
|
|
18579
20240
|
}, reject);
|
|
18580
20241
|
});
|
|
@@ -18615,7 +20276,7 @@ function printReady(info, qq) {
|
|
|
18615
20276
|
// src/cli/account-enroll.ts
|
|
18616
20277
|
init_cjs_shims();
|
|
18617
20278
|
var path34 = __toESM(require("path"), 1);
|
|
18618
|
-
var
|
|
20279
|
+
var readline2 = __toESM(require("readline/promises"), 1);
|
|
18619
20280
|
var SKIP_ANSWERS = /* @__PURE__ */ new Set(["-", "skip", "none", "no"]);
|
|
18620
20281
|
function resolveDefaultHub(hubUrl2, env3 = process.env) {
|
|
18621
20282
|
return (hubUrl2 ?? env3.HUB_URL ?? DEFAULT_HUB_URL).trim().replace(/\/$/, "");
|
|
@@ -18706,7 +20367,7 @@ async function maybeAccountEnroll(opts = {}) {
|
|
|
18706
20367
|
cliErr(`\u63A5\u5165\u68C0\u67E5\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
18707
20368
|
return;
|
|
18708
20369
|
}
|
|
18709
|
-
const rl =
|
|
20370
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
18710
20371
|
try {
|
|
18711
20372
|
await runWizard(rl, store, opts);
|
|
18712
20373
|
} catch (e) {
|
|
@@ -18787,7 +20448,7 @@ async function askHidden(rl, prompt) {
|
|
|
18787
20448
|
|
|
18788
20449
|
// src/cli/link.ts
|
|
18789
20450
|
init_cjs_shims();
|
|
18790
|
-
var
|
|
20451
|
+
var readline3 = __toESM(require("readline"), 1);
|
|
18791
20452
|
|
|
18792
20453
|
// src/cli/serve-instances.ts
|
|
18793
20454
|
init_cjs_shims();
|