@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.js
CHANGED
|
@@ -563,8 +563,18 @@ init_esm_shims();
|
|
|
563
563
|
import * as fs3 from "fs";
|
|
564
564
|
import * as fsp from "fs/promises";
|
|
565
565
|
import * as path5 from "path";
|
|
566
|
+
import * as readline from "readline";
|
|
566
567
|
import { randomUUID } from "crypto";
|
|
567
568
|
|
|
569
|
+
// src/server/llmIds.ts
|
|
570
|
+
init_esm_shims();
|
|
571
|
+
var GLOBAL_PROVIDER_ID = "__env__";
|
|
572
|
+
var BUILTIN_PROVIDER_ID = "__zqai__";
|
|
573
|
+
function isBuiltinProvider(id) {
|
|
574
|
+
return id === BUILTIN_PROVIDER_ID;
|
|
575
|
+
}
|
|
576
|
+
var FALLBACK_MODEL = "gpt-4o";
|
|
577
|
+
|
|
568
578
|
// src/server/dbgTrace.ts
|
|
569
579
|
init_esm_shims();
|
|
570
580
|
var DBG_FLEET = (() => {
|
|
@@ -1480,6 +1490,170 @@ var Store = class {
|
|
|
1480
1490
|
}
|
|
1481
1491
|
}
|
|
1482
1492
|
}
|
|
1493
|
+
/**
|
|
1494
|
+
* 读取事件「一页」:从文件末尾向前,按字节预算取最近的事件。
|
|
1495
|
+
*
|
|
1496
|
+
* - `before` 省略:取最新的 maxBytes 字节
|
|
1497
|
+
* - `before` 指定:只取 seq < before 的事件(向前翻页)
|
|
1498
|
+
*
|
|
1499
|
+
* 为什么不能复用 loadEventsTail:后者只能从文件末尾读固定窗口,
|
|
1500
|
+
* 无法「向前翻页」,而控制台需要滚到顶部继续加载更早的对话。
|
|
1501
|
+
* 这里按块从后往前扫,读到预算或文件头即停;翻了多页之后每次
|
|
1502
|
+
* before 都紧邻上一页,实际只需读 1~2 块。
|
|
1503
|
+
*
|
|
1504
|
+
* 返回按 seq 升序;`hasMore` 表示本页最早一条之前还有更早的事件。
|
|
1505
|
+
*/
|
|
1506
|
+
loadEventPage(tenantId, sessionId, opts) {
|
|
1507
|
+
const p = path5.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
1508
|
+
const { before, maxBytes } = opts;
|
|
1509
|
+
const __t0 = dbgNow();
|
|
1510
|
+
let fd = null;
|
|
1511
|
+
try {
|
|
1512
|
+
const size = fs3.statSync(p).size;
|
|
1513
|
+
if (size === 0) return { events: [], hasMore: false };
|
|
1514
|
+
fd = fs3.openSync(p, "r");
|
|
1515
|
+
const CHUNK = 256 * 1024;
|
|
1516
|
+
let pos = size;
|
|
1517
|
+
const kept = [];
|
|
1518
|
+
let bytes = 0;
|
|
1519
|
+
let hasMore = false;
|
|
1520
|
+
let done = false;
|
|
1521
|
+
while (pos > 0 && !done) {
|
|
1522
|
+
const start = Math.max(0, pos - CHUNK);
|
|
1523
|
+
const len = pos - start;
|
|
1524
|
+
const buf = Buffer.allocUnsafe(len);
|
|
1525
|
+
const read = fs3.readSync(fd, buf, 0, len, start);
|
|
1526
|
+
const rows = buf.subarray(0, read).toString("utf-8").split("\n");
|
|
1527
|
+
if (start > 0) rows.shift();
|
|
1528
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
1529
|
+
const line = rows[i];
|
|
1530
|
+
if (!line) continue;
|
|
1531
|
+
let entry;
|
|
1532
|
+
try {
|
|
1533
|
+
entry = JSON.parse(line);
|
|
1534
|
+
} catch {
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (!Number.isFinite(Number(entry.seq))) continue;
|
|
1538
|
+
if (before !== void 0 && Number(entry.seq) >= before) continue;
|
|
1539
|
+
const sz = Buffer.byteLength(line, "utf-8") + 1;
|
|
1540
|
+
if (bytes > 0 && bytes + sz > maxBytes) {
|
|
1541
|
+
hasMore = true;
|
|
1542
|
+
done = true;
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
bytes += sz;
|
|
1546
|
+
kept.unshift(entry);
|
|
1547
|
+
}
|
|
1548
|
+
pos = start;
|
|
1549
|
+
}
|
|
1550
|
+
dbgMs(
|
|
1551
|
+
"store.loadEventPage",
|
|
1552
|
+
__t0,
|
|
1553
|
+
// [dbg]
|
|
1554
|
+
`size=${size} before=${before ?? "-"} out=${kept.length} bytes\u2248${bytes} more=${hasMore}`
|
|
1555
|
+
);
|
|
1556
|
+
return { events: kept, hasMore };
|
|
1557
|
+
} catch {
|
|
1558
|
+
return { events: [], hasMore: false };
|
|
1559
|
+
} finally {
|
|
1560
|
+
if (fd !== null) {
|
|
1561
|
+
try {
|
|
1562
|
+
fs3.closeSync(fd);
|
|
1563
|
+
} catch {
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
// ===================== 轮次索引 =====================
|
|
1569
|
+
/** 轮次索引文件路径 */
|
|
1570
|
+
turnsFile(tenantId, sessionId) {
|
|
1571
|
+
return path5.join(this.sessionDir(tenantId, sessionId), "turns.ndjson");
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* 追加一轮的最新快照(同一 startSeq 后写者胜出)。
|
|
1575
|
+
*
|
|
1576
|
+
* 走与 session.json 相同的每文件写链:索引按「开轮 → 补充消息 → 收轮」
|
|
1577
|
+
* 顺序追加,乱序会让列表读到旧的 running 版本。
|
|
1578
|
+
*/
|
|
1579
|
+
appendTurnIndex(tenantId, sessionId, entry) {
|
|
1580
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
1581
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1582
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
1583
|
+
const line = JSON.stringify(entry) + "\n";
|
|
1584
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
1585
|
+
const next = prev.then(() => fsp.appendFile(file, line, "utf-8")).catch((err) => {
|
|
1586
|
+
console.error(`[store] appendTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
1587
|
+
});
|
|
1588
|
+
this.writeChains.set(file, next);
|
|
1589
|
+
return next;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* 读取轮次索引(已按 startSeq 去重取最后一版,升序)。
|
|
1593
|
+
*
|
|
1594
|
+
* 文件很小(一轮约 0.5KB,1000 轮 ≈ 500KB),同步读即可,
|
|
1595
|
+
* 会话详情打开路径不会因历史长度变慢。
|
|
1596
|
+
*/
|
|
1597
|
+
loadTurnIndex(tenantId, sessionId) {
|
|
1598
|
+
let raw;
|
|
1599
|
+
try {
|
|
1600
|
+
raw = fs3.readFileSync(this.turnsFile(tenantId, sessionId), "utf-8");
|
|
1601
|
+
} catch {
|
|
1602
|
+
return [];
|
|
1603
|
+
}
|
|
1604
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
1605
|
+
for (const line of raw.split("\n")) {
|
|
1606
|
+
if (!line) continue;
|
|
1607
|
+
try {
|
|
1608
|
+
const e = JSON.parse(line);
|
|
1609
|
+
if (!Number.isFinite(Number(e?.startSeq))) continue;
|
|
1610
|
+
byStart.set(Number(e.startSeq), e);
|
|
1611
|
+
} catch {
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
1615
|
+
}
|
|
1616
|
+
/** 整文件替换轮次索引(重建用,tmp + rename 原子写) */
|
|
1617
|
+
saveTurnIndex(tenantId, sessionId, entries) {
|
|
1618
|
+
const dir = this.sessionDir(tenantId, sessionId);
|
|
1619
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1620
|
+
const file = this.turnsFile(tenantId, sessionId);
|
|
1621
|
+
const snapshot = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
|
|
1622
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
1623
|
+
const prev = this.writeChains.get(file) ?? Promise.resolve();
|
|
1624
|
+
const next = prev.then(() => fsp.writeFile(tmp, snapshot, "utf-8")).then(() => fsp.rename(tmp, file)).catch((err) => {
|
|
1625
|
+
console.error(`[store] saveTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
|
|
1626
|
+
void fsp.rm(tmp, { force: true });
|
|
1627
|
+
});
|
|
1628
|
+
this.writeChains.set(file, next);
|
|
1629
|
+
return next;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* 流式读取整份事件日志(重建索引用)。
|
|
1633
|
+
*
|
|
1634
|
+
* 不能用 loadEvents(readFileSync + 逐行 JSON.parse):大会话几十 MB,
|
|
1635
|
+
* 会长时间阻塞事件循环,导致 SSE 心跳 / MQTT 保活断开。
|
|
1636
|
+
*/
|
|
1637
|
+
async readEventsAsync(tenantId, sessionId) {
|
|
1638
|
+
const p = path5.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
|
|
1639
|
+
const out = [];
|
|
1640
|
+
try {
|
|
1641
|
+
const rl = readline.createInterface({
|
|
1642
|
+
input: fs3.createReadStream(p, { encoding: "utf-8" }),
|
|
1643
|
+
crlfDelay: Infinity
|
|
1644
|
+
});
|
|
1645
|
+
for await (const line of rl) {
|
|
1646
|
+
if (!line) continue;
|
|
1647
|
+
try {
|
|
1648
|
+
out.push(JSON.parse(line));
|
|
1649
|
+
} catch {
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
} catch {
|
|
1653
|
+
return out;
|
|
1654
|
+
}
|
|
1655
|
+
return out;
|
|
1656
|
+
}
|
|
1483
1657
|
// ===================== 节点身份 & 管理员密钥 =====================
|
|
1484
1658
|
/** 节点稳定标识:{root}/node.json(首次启动生成后长期固定) */
|
|
1485
1659
|
loadNodeId() {
|
|
@@ -1538,7 +1712,23 @@ var Store = class {
|
|
|
1538
1712
|
legacyFiles: [path5.join(this.root, "node-llm.json")]
|
|
1539
1713
|
});
|
|
1540
1714
|
const old = new Map(cur.providers.map((p) => [p.id, p]));
|
|
1541
|
-
const
|
|
1715
|
+
const src = [...cfg.providers];
|
|
1716
|
+
const prevBuiltin = cur.providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
|
|
1717
|
+
if (prevBuiltin && !src.some((p) => p.id === BUILTIN_PROVIDER_ID)) {
|
|
1718
|
+
src.unshift({
|
|
1719
|
+
id: prevBuiltin.id,
|
|
1720
|
+
name: prevBuiltin.name,
|
|
1721
|
+
type: prevBuiltin.type,
|
|
1722
|
+
baseURL: prevBuiltin.baseURL,
|
|
1723
|
+
model: prevBuiltin.model,
|
|
1724
|
+
apiKey: decryptKey(prevBuiltin.apiKeyEnc, file) ?? void 0,
|
|
1725
|
+
reasoningEffort: prevBuiltin.reasoningEffort ?? null,
|
|
1726
|
+
...prevBuiltin.models ? { models: prevBuiltin.models } : {},
|
|
1727
|
+
...prevBuiltin.nonChat ? { nonChat: prevBuiltin.nonChat } : {},
|
|
1728
|
+
...prevBuiltin.modelMeta ? { modelMeta: prevBuiltin.modelMeta } : {}
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
const providers = src.map((p) => {
|
|
1542
1732
|
const prev = old.get(p.id);
|
|
1543
1733
|
const apiKeyEnc = p.apiKey ? encryptKey(p.apiKey, file) : prev?.apiKeyEnc ?? "";
|
|
1544
1734
|
return {
|
|
@@ -1900,13 +2090,13 @@ function textOf(content) {
|
|
|
1900
2090
|
}
|
|
1901
2091
|
return content != null ? String(content) : "";
|
|
1902
2092
|
}
|
|
1903
|
-
function extractUserMessages(span,
|
|
2093
|
+
function extractUserMessages(span, clip2 = USER_MESSAGE_CLIP) {
|
|
1904
2094
|
const out = [];
|
|
1905
2095
|
for (const m of span) {
|
|
1906
2096
|
if (m.role !== "user") continue;
|
|
1907
2097
|
const text = textOf(m.content).replace(/\s+/g, " ").trim();
|
|
1908
2098
|
if (!text) continue;
|
|
1909
|
-
out.push(text.length >
|
|
2099
|
+
out.push(text.length > clip2 ? text.slice(0, clip2 - 1) + "\u2026" : text);
|
|
1910
2100
|
}
|
|
1911
2101
|
return out;
|
|
1912
2102
|
}
|
|
@@ -2718,6 +2908,21 @@ var TurnEngine = class _TurnEngine {
|
|
|
2718
2908
|
queueSteering(text) {
|
|
2719
2909
|
this._steering.push(text);
|
|
2720
2910
|
}
|
|
2911
|
+
/**
|
|
2912
|
+
* 撤销一条尚未注入的 steering。
|
|
2913
|
+
*
|
|
2914
|
+
* 按文本删第一条匹配项:调用方(SessionManager)已按索引删掉了自己那份队列,
|
|
2915
|
+
* 这里只需保证引擎不再注入同样的内容。用文本而非索引,是因为引擎会在迭代
|
|
2916
|
+
* 间隙批量清空 `_steering`,索引可能已经漂移;文本相同则删哪条都等价。
|
|
2917
|
+
*
|
|
2918
|
+
* @returns 是否真的删掉了(已注入的项返回 false,调用方据此提示用户)
|
|
2919
|
+
*/
|
|
2920
|
+
removeSteering(text) {
|
|
2921
|
+
const i = this._steering.indexOf(text);
|
|
2922
|
+
if (i < 0) return false;
|
|
2923
|
+
this._steering.splice(i, 1);
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2721
2926
|
// ===================== 主入口 =====================
|
|
2722
2927
|
/**
|
|
2723
2928
|
* 运行一轮 Turn
|
|
@@ -9051,7 +9256,6 @@ function registerMediaTool(registry, workspace, opts = {}) {
|
|
|
9051
9256
|
|
|
9052
9257
|
// src/server/nodeLlm.ts
|
|
9053
9258
|
init_esm_shims();
|
|
9054
|
-
var GLOBAL_PROVIDER_ID = "__env__";
|
|
9055
9259
|
function envProvider() {
|
|
9056
9260
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
9057
9261
|
if (!apiKey) return null;
|
|
@@ -9094,19 +9298,26 @@ function nodeLlmView(cfg) {
|
|
|
9094
9298
|
global: true
|
|
9095
9299
|
}
|
|
9096
9300
|
] : [],
|
|
9097
|
-
...cfg.providers.map((p) =>
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9301
|
+
...cfg.providers.map((p) => {
|
|
9302
|
+
const builtin = isBuiltinProvider(p.id);
|
|
9303
|
+
return {
|
|
9304
|
+
id: p.id,
|
|
9305
|
+
name: p.name,
|
|
9306
|
+
type: p.type ?? "",
|
|
9307
|
+
baseURL: p.baseURL,
|
|
9308
|
+
model: p.model,
|
|
9309
|
+
hasApiKey: Boolean(p.apiKey),
|
|
9310
|
+
models: p.models ?? [],
|
|
9311
|
+
nonChat: p.nonChat ?? [],
|
|
9312
|
+
modelMeta: p.modelMeta ?? [],
|
|
9313
|
+
reasoningEffort: p.reasoningEffort ?? null,
|
|
9314
|
+
global: false,
|
|
9315
|
+
/** 平台内置(接入 Hub 后自动下发,用户不用关心 Key) */
|
|
9316
|
+
builtin,
|
|
9317
|
+
/** 不可编辑的字段:控制台据此把 baseURL / key 显示为只读 */
|
|
9318
|
+
...builtin ? { locked: ["baseURL", "apiKey"] } : {}
|
|
9319
|
+
};
|
|
9320
|
+
})
|
|
9110
9321
|
];
|
|
9111
9322
|
return {
|
|
9112
9323
|
defaultProviderId: cfg.defaultProviderId ?? null,
|
|
@@ -9119,27 +9330,42 @@ function applyNodeLlmPatch(cfg, raw) {
|
|
|
9119
9330
|
const next = { providers: cfg.providers, defaultProviderId: cfg.defaultProviderId };
|
|
9120
9331
|
if (Array.isArray(raw?.providers)) {
|
|
9121
9332
|
const old = new Map(cfg.providers.map((p) => [p.id, p]));
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9333
|
+
const incoming = raw.providers.filter((x) => Boolean(x) && typeof x === "object").filter((x) => x.global !== true && String(x.id ?? "") !== GLOBAL_PROVIDER_ID);
|
|
9334
|
+
const prevBuiltin = cfg.providers.find((p) => isBuiltinProvider(p.id));
|
|
9335
|
+
const builtinPatch = incoming.find((x) => isBuiltinProvider(String(x.id ?? "")));
|
|
9336
|
+
const keptBuiltin = prevBuiltin ? [
|
|
9337
|
+
{
|
|
9338
|
+
...prevBuiltin,
|
|
9339
|
+
...builtinPatch?.model != null ? { model: String(builtinPatch.model).trim() || prevBuiltin.model } : {},
|
|
9340
|
+
...normalizeModelList(builtinPatch?.models) ? { models: normalizeModelList(builtinPatch?.models) } : {},
|
|
9341
|
+
...builtinPatch?.reasoningEffort !== void 0 ? { reasoningEffort: normReasoningEffort(builtinPatch.reasoningEffort) } : {},
|
|
9342
|
+
...typeof builtinPatch?.name === "string" && builtinPatch.name.trim() ? { name: builtinPatch.name.trim() } : {}
|
|
9343
|
+
}
|
|
9344
|
+
] : [];
|
|
9345
|
+
next.providers = [
|
|
9346
|
+
...keptBuiltin,
|
|
9347
|
+
...incoming.filter((x) => !isBuiltinProvider(String(x.id ?? ""))).map((x) => {
|
|
9348
|
+
const id = String(x.id ?? "").trim();
|
|
9349
|
+
if (!id) throw new ApiError(400, "bad_request", "\u4F9B\u5E94\u5546 id \u4E0D\u80FD\u4E3A\u7A7A");
|
|
9350
|
+
const prev = old.get(id);
|
|
9351
|
+
const apiKey = String(x.apiKey ?? "").trim();
|
|
9352
|
+
const models = normalizeModelList(x.models) ?? prev?.models;
|
|
9353
|
+
const nonChat = normalizeModelList(x.nonChat) ?? prev?.nonChat;
|
|
9354
|
+
const modelMeta = normalizeModelMeta(x.modelMeta) ?? prev?.modelMeta;
|
|
9355
|
+
return {
|
|
9356
|
+
id,
|
|
9357
|
+
name: String(x.name ?? prev?.name ?? id).trim(),
|
|
9358
|
+
type: x.type == null ? prev?.type : String(x.type),
|
|
9359
|
+
baseURL: String(x.baseURL ?? prev?.baseURL ?? "").trim(),
|
|
9360
|
+
model: String(x.model ?? prev?.model ?? "").trim(),
|
|
9361
|
+
apiKey: apiKey || prev?.apiKey,
|
|
9362
|
+
reasoningEffort: x.reasoningEffort === void 0 ? prev?.reasoningEffort ?? null : normReasoningEffort(x.reasoningEffort),
|
|
9363
|
+
...models ? { models } : {},
|
|
9364
|
+
...nonChat ? { nonChat } : {},
|
|
9365
|
+
...modelMeta ? { modelMeta } : {}
|
|
9366
|
+
};
|
|
9367
|
+
})
|
|
9368
|
+
];
|
|
9143
9369
|
}
|
|
9144
9370
|
if (typeof raw?.defaultProviderId === "string") {
|
|
9145
9371
|
const id = raw.defaultProviderId.trim();
|
|
@@ -9159,6 +9385,9 @@ function nodeDefaultProvider(store) {
|
|
|
9159
9385
|
if (cfg.providers.length === 0) return null;
|
|
9160
9386
|
return cfg.providers.find((p) => p.id === cfg.defaultProviderId) ?? cfg.providers[0] ?? null;
|
|
9161
9387
|
}
|
|
9388
|
+
function defaultTenantModel(store) {
|
|
9389
|
+
return process.env.OPENAI_MODEL || process.env.MODEL || nodeDefaultProvider(store)?.model || FALLBACK_MODEL;
|
|
9390
|
+
}
|
|
9162
9391
|
function resolveSessionProvider(store, tenant, providerId) {
|
|
9163
9392
|
const own = tenant.provider ?? {};
|
|
9164
9393
|
if (providerId) {
|
|
@@ -9188,6 +9417,208 @@ function resolveSessionProvider(store, tenant, providerId) {
|
|
|
9188
9417
|
return { ...own, model: tenant.model };
|
|
9189
9418
|
}
|
|
9190
9419
|
|
|
9420
|
+
// src/server/builtinLlm.ts
|
|
9421
|
+
init_esm_shims();
|
|
9422
|
+
var FETCH_TIMEOUT_MS = 8e3;
|
|
9423
|
+
var RETRY_AFTER_MS = 5 * 6e4;
|
|
9424
|
+
function hubBase(cred) {
|
|
9425
|
+
return (cred.hubUrl ?? "").replace(/\/+$/, "");
|
|
9426
|
+
}
|
|
9427
|
+
function asProvider(grant) {
|
|
9428
|
+
const baseURL = String(grant.baseURL ?? "").trim();
|
|
9429
|
+
const apiKey = String(grant.apiKey ?? "").trim();
|
|
9430
|
+
const model = String(grant.model ?? "").trim();
|
|
9431
|
+
if (!baseURL || !apiKey) return null;
|
|
9432
|
+
return {
|
|
9433
|
+
id: BUILTIN_PROVIDER_ID,
|
|
9434
|
+
name: String(grant.name ?? "").trim() || "\u5FD7\u524D\u5927\u6A21\u578B\uFF08\u5185\u7F6E\uFF09",
|
|
9435
|
+
type: "builtin",
|
|
9436
|
+
baseURL,
|
|
9437
|
+
model,
|
|
9438
|
+
apiKey
|
|
9439
|
+
};
|
|
9440
|
+
}
|
|
9441
|
+
async function enrichModels(p) {
|
|
9442
|
+
try {
|
|
9443
|
+
const r = await Promise.race([
|
|
9444
|
+
fetchModels(p.baseURL, p.apiKey ?? "", p.model),
|
|
9445
|
+
new Promise((resolve20) => {
|
|
9446
|
+
const t = setTimeout(() => resolve20(null), FETCH_TIMEOUT_MS);
|
|
9447
|
+
t.unref?.();
|
|
9448
|
+
})
|
|
9449
|
+
]);
|
|
9450
|
+
if (!r?.models?.length) return p;
|
|
9451
|
+
return {
|
|
9452
|
+
...p,
|
|
9453
|
+
// 控制面没给默认型号时,取供应商返回的第一个,避免模型为空让用户卡住
|
|
9454
|
+
model: p.model || r.models[0],
|
|
9455
|
+
models: r.models,
|
|
9456
|
+
...r.nonChat?.length ? { nonChat: r.nonChat } : {},
|
|
9457
|
+
...r.modelMeta?.length ? { modelMeta: r.modelMeta } : {}
|
|
9458
|
+
};
|
|
9459
|
+
} catch {
|
|
9460
|
+
return p;
|
|
9461
|
+
}
|
|
9462
|
+
}
|
|
9463
|
+
function saveBuiltin(store, provider) {
|
|
9464
|
+
const cfg = store.loadNodeLlm();
|
|
9465
|
+
const others = cfg.providers.filter((p) => p.id !== BUILTIN_PROVIDER_ID);
|
|
9466
|
+
const prev = cfg.providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
|
|
9467
|
+
const merged = {
|
|
9468
|
+
...provider,
|
|
9469
|
+
// 用户选过的型号/候选列表/推理强度/显示名都保留;只有托管字段(baseURL/key)强制以控制面为准
|
|
9470
|
+
...prev?.model ? { model: prev.model } : {},
|
|
9471
|
+
...prev?.models?.length ? { models: prev.models } : {},
|
|
9472
|
+
...prev?.reasoningEffort != null ? { reasoningEffort: prev.reasoningEffort } : {},
|
|
9473
|
+
...prev?.name ? { name: prev.name } : {}
|
|
9474
|
+
};
|
|
9475
|
+
store.saveNodeLlm({
|
|
9476
|
+
providers: [merged, ...others],
|
|
9477
|
+
// 之前没有默认且用户也没指定时,让内置模型成为默认——「开箱即用」
|
|
9478
|
+
defaultProviderId: cfg.defaultProviderId ?? BUILTIN_PROVIDER_ID
|
|
9479
|
+
});
|
|
9480
|
+
adoptBuiltinModelForTenants(store, merged.model);
|
|
9481
|
+
}
|
|
9482
|
+
var LEGACY_DEFAULT_MODELS = /* @__PURE__ */ new Set([FALLBACK_MODEL, "deepseek-v4-flash"]);
|
|
9483
|
+
function adoptBuiltinModelForTenants(store, model) {
|
|
9484
|
+
if (!model) return;
|
|
9485
|
+
for (const id of store.listTenants()) {
|
|
9486
|
+
const tenant = store.loadTenant(id);
|
|
9487
|
+
if (!tenant || !LEGACY_DEFAULT_MODELS.has(tenant.model)) continue;
|
|
9488
|
+
if (tenant.provider?.apiKey) continue;
|
|
9489
|
+
tenant.model = model;
|
|
9490
|
+
store.saveTenant(tenant);
|
|
9491
|
+
}
|
|
9492
|
+
}
|
|
9493
|
+
async function ensureBuiltinProvider(store, opts = {}) {
|
|
9494
|
+
const cred = store.loadFleetCredential();
|
|
9495
|
+
if (!cred?.fleetToken || !cred.fleetSecret) {
|
|
9496
|
+
return { status: "disabled", reason: "\u8282\u70B9\u672A\u63A5\u5165 Hub" };
|
|
9497
|
+
}
|
|
9498
|
+
const hub = hubBase(cred);
|
|
9499
|
+
if (!hub) return { status: "disabled", reason: "\u7F3A\u5C11 Hub \u5730\u5740" };
|
|
9500
|
+
const local = store.loadNodeLlm().providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
|
|
9501
|
+
if (local && !opts.force) {
|
|
9502
|
+
adoptBuiltinModelForTenants(store, local.model);
|
|
9503
|
+
return { status: "ready", provider: local, refreshed: false };
|
|
9504
|
+
}
|
|
9505
|
+
const cached3 = cred.builtinLlm ? asProvider(cred.builtinLlm) : null;
|
|
9506
|
+
let res;
|
|
9507
|
+
try {
|
|
9508
|
+
res = await fetch(`${hub}/api/hub/node/builtin-llm`, {
|
|
9509
|
+
headers: {
|
|
9510
|
+
Authorization: `Bearer ${cred.fleetToken}`,
|
|
9511
|
+
"X-Fleet-Secret": cred.fleetSecret,
|
|
9512
|
+
"X-Node-Id": cred.nodeId
|
|
9513
|
+
},
|
|
9514
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
9515
|
+
});
|
|
9516
|
+
} catch (e) {
|
|
9517
|
+
if (cached3) {
|
|
9518
|
+
const provider2 = await enrichModels(cached3);
|
|
9519
|
+
saveBuiltin(store, provider2);
|
|
9520
|
+
return { status: "ready", provider: provider2, refreshed: true };
|
|
9521
|
+
}
|
|
9522
|
+
return { status: "error", reason: `\u8FDE\u63A5 Hub \u5931\u8D25\uFF1A${e.message}` };
|
|
9523
|
+
}
|
|
9524
|
+
let body = null;
|
|
9525
|
+
try {
|
|
9526
|
+
body = await res.json();
|
|
9527
|
+
} catch {
|
|
9528
|
+
}
|
|
9529
|
+
if (!res.ok) {
|
|
9530
|
+
const msg = body?.error?.message ?? `HTTP ${res.status}`;
|
|
9531
|
+
if (res.status === 401 || res.status === 403) {
|
|
9532
|
+
return { status: "unavailable", reason: msg };
|
|
9533
|
+
}
|
|
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: msg };
|
|
9540
|
+
}
|
|
9541
|
+
const grant = body?.data?.builtinLlm;
|
|
9542
|
+
if (!grant) {
|
|
9543
|
+
const why = String(body?.data?.reason ?? "").trim();
|
|
9544
|
+
return { status: "unavailable", reason: why || "\u63A7\u5236\u9762\u672A\u4E0B\u53D1\u5185\u7F6E\u6A21\u578B" };
|
|
9545
|
+
}
|
|
9546
|
+
const parsed = asProvider(grant);
|
|
9547
|
+
if (!parsed) return { status: "unavailable", reason: "\u5185\u7F6E\u6A21\u578B\u914D\u7F6E\u4E0D\u5B8C\u6574" };
|
|
9548
|
+
const provider = await enrichModels(parsed);
|
|
9549
|
+
saveBuiltin(store, provider);
|
|
9550
|
+
return { status: "ready", provider, refreshed: true };
|
|
9551
|
+
}
|
|
9552
|
+
async function fetchLlmBalance(store) {
|
|
9553
|
+
const cred = store.loadFleetCredential();
|
|
9554
|
+
const hub = cred ? hubBase(cred) : "";
|
|
9555
|
+
if (!cred?.fleetToken || !cred.fleetSecret || !hub) {
|
|
9556
|
+
return { available: null, currency: "CNY", reason: "\u8282\u70B9\u672A\u63A5\u5165 Hub" };
|
|
9557
|
+
}
|
|
9558
|
+
try {
|
|
9559
|
+
const res = await fetch(`${hub}/api/hub/node/balance`, {
|
|
9560
|
+
headers: {
|
|
9561
|
+
Authorization: `Bearer ${cred.fleetToken}`,
|
|
9562
|
+
"X-Fleet-Secret": cred.fleetSecret,
|
|
9563
|
+
"X-Node-Id": cred.nodeId
|
|
9564
|
+
},
|
|
9565
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
9566
|
+
});
|
|
9567
|
+
const body = await res.json().catch(() => null);
|
|
9568
|
+
if (!res.ok) {
|
|
9569
|
+
return {
|
|
9570
|
+
available: null,
|
|
9571
|
+
currency: "CNY",
|
|
9572
|
+
reason: body?.data ? "" : `HTTP ${res.status}`
|
|
9573
|
+
};
|
|
9574
|
+
}
|
|
9575
|
+
const d = body?.data ?? {};
|
|
9576
|
+
const available = typeof d.available === "number" ? d.available : null;
|
|
9577
|
+
return {
|
|
9578
|
+
available,
|
|
9579
|
+
currency: typeof d.currency === "string" && d.currency ? d.currency : "CNY",
|
|
9580
|
+
...typeof d.reserved === "number" ? { reserved: d.reserved } : {},
|
|
9581
|
+
// Hub 没配 HUB_PUBLIC_URL 时退回用接入时的 hubUrl
|
|
9582
|
+
topupUrl: typeof d.topupUrl === "string" && d.topupUrl ? d.topupUrl : `${hub}/topup`,
|
|
9583
|
+
...available === null && typeof d.reason === "string" ? { reason: d.reason } : {}
|
|
9584
|
+
};
|
|
9585
|
+
} catch (e) {
|
|
9586
|
+
return {
|
|
9587
|
+
available: null,
|
|
9588
|
+
currency: "CNY",
|
|
9589
|
+
reason: `\u8FDE\u63A5 Hub \u5931\u8D25\uFF1A${e.message}`
|
|
9590
|
+
};
|
|
9591
|
+
}
|
|
9592
|
+
}
|
|
9593
|
+
var AUTH_RETRY_THROTTLE_MS = 6e4;
|
|
9594
|
+
var lastAuthRetryAt = 0;
|
|
9595
|
+
function healBuiltinOnAuthFailure(store, message) {
|
|
9596
|
+
if (!/401|unauthorized|invalid[_\s]*api[_\s]*key|api[\s_-]*key\s*(?:无效|invalid)/i.test(message)) return;
|
|
9597
|
+
if (!store.loadFleetCredential()) return;
|
|
9598
|
+
if (!store.loadNodeLlm().providers.some((p) => p.id === BUILTIN_PROVIDER_ID)) return;
|
|
9599
|
+
if (Date.now() - lastAuthRetryAt < AUTH_RETRY_THROTTLE_MS) return;
|
|
9600
|
+
lastAuthRetryAt = Date.now();
|
|
9601
|
+
void ensureBuiltinProvider(store, { force: true }).catch(() => {
|
|
9602
|
+
});
|
|
9603
|
+
}
|
|
9604
|
+
var lastFailureAt = 0;
|
|
9605
|
+
function scheduleBuiltinProvider(store, log = () => {
|
|
9606
|
+
}) {
|
|
9607
|
+
if (Date.now() - lastFailureAt < RETRY_AFTER_MS) return;
|
|
9608
|
+
void ensureBuiltinProvider(store).then((r) => {
|
|
9609
|
+
if (r.status === "ready") {
|
|
9610
|
+
if (r.refreshed) log(`[builtin-llm] \u5DF2\u4E0B\u53D1\u5185\u7F6E\u6A21\u578B\uFF1A${r.provider.name}\uFF08${r.provider.model}\uFF09`);
|
|
9611
|
+
return;
|
|
9612
|
+
}
|
|
9613
|
+
if (r.status === "disabled") return;
|
|
9614
|
+
lastFailureAt = Date.now();
|
|
9615
|
+
if (r.status === "unavailable") log(`[builtin-llm] \u6682\u65E0\u5185\u7F6E\u6A21\u578B\uFF1A${r.reason}`);
|
|
9616
|
+
else log(`[builtin-llm] \u62C9\u53D6\u5185\u7F6E\u6A21\u578B\u5931\u8D25\uFF1A${r.reason}`);
|
|
9617
|
+
}).catch(() => {
|
|
9618
|
+
lastFailureAt = Date.now();
|
|
9619
|
+
});
|
|
9620
|
+
}
|
|
9621
|
+
|
|
9191
9622
|
// src/server/usage.ts
|
|
9192
9623
|
init_esm_shims();
|
|
9193
9624
|
import * as fs18 from "fs";
|
|
@@ -9259,6 +9690,19 @@ function addAgg(into, tokens, costUsd) {
|
|
|
9259
9690
|
function utcDay(iso) {
|
|
9260
9691
|
return iso.slice(0, 10);
|
|
9261
9692
|
}
|
|
9693
|
+
function sumAgg(values) {
|
|
9694
|
+
const out = zeroAgg();
|
|
9695
|
+
for (const v of values) {
|
|
9696
|
+
out.calls += v.calls;
|
|
9697
|
+
out.input += v.input;
|
|
9698
|
+
out.output += v.output;
|
|
9699
|
+
out.cacheRead += v.cacheRead;
|
|
9700
|
+
out.cacheWrite += v.cacheWrite;
|
|
9701
|
+
out.costUsd += v.costUsd;
|
|
9702
|
+
out.unpriced += v.unpriced;
|
|
9703
|
+
}
|
|
9704
|
+
return out;
|
|
9705
|
+
}
|
|
9262
9706
|
function monthOf(iso) {
|
|
9263
9707
|
return iso.slice(0, 7);
|
|
9264
9708
|
}
|
|
@@ -9534,6 +9978,23 @@ var UsageLedger = class {
|
|
|
9534
9978
|
}
|
|
9535
9979
|
return all;
|
|
9536
9980
|
}
|
|
9981
|
+
/**
|
|
9982
|
+
* 按「项目 × 天」聚合(供控制面把消耗拆到每个项目)。
|
|
9983
|
+
*
|
|
9984
|
+
* 汇总文件里只存了按项目、按天两个独立维度,拿不到交叉项,因此这里从
|
|
9985
|
+
* 账本明细现算——调用方(节点上报)只取最近 40 天,读的是 2 个月文件,
|
|
9986
|
+
* 5 分钟一次的成本可忽略。
|
|
9987
|
+
*/
|
|
9988
|
+
projectByDay(opts) {
|
|
9989
|
+
const out = {};
|
|
9990
|
+
for (const rec of this.allRecords({ from: opts.from, to: opts.to })) {
|
|
9991
|
+
const day = utcDay(rec.ts);
|
|
9992
|
+
const days = out[rec.projectId] ?? (out[rec.projectId] = {});
|
|
9993
|
+
const agg = days[day] ?? (days[day] = zeroAgg());
|
|
9994
|
+
addAgg(agg, rec, rec.costUsd);
|
|
9995
|
+
}
|
|
9996
|
+
return out;
|
|
9997
|
+
}
|
|
9537
9998
|
summary(opts) {
|
|
9538
9999
|
this.ensureInit();
|
|
9539
10000
|
const currency = (opts.currency ?? "USD").toUpperCase();
|
|
@@ -9833,6 +10294,141 @@ var EVENT_BUFFER_MAX = 2e3;
|
|
|
9833
10294
|
var EVENTS_ACK_MAX_BYTES = 512 * 1024;
|
|
9834
10295
|
var APPROVAL_TIMEOUT_MS = (Number(process.env.APPROVAL_TIMEOUT_SECONDS) || 15 * 60) * 1e3;
|
|
9835
10296
|
var LAST_USER_MESSAGE_MAX = 200;
|
|
10297
|
+
var TURN_TEXT_MAX = 500;
|
|
10298
|
+
var TURN_REPLY_MAX_BYTES = 256 * 1024;
|
|
10299
|
+
var TURN_REPLY_MAX_TEXT = 256 * 1024;
|
|
10300
|
+
var TURNS_DEFAULT_LIMIT = 20;
|
|
10301
|
+
function clip(text, max = LAST_USER_MESSAGE_MAX) {
|
|
10302
|
+
const s = text ?? "";
|
|
10303
|
+
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
10304
|
+
}
|
|
10305
|
+
function asNum(v) {
|
|
10306
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
10307
|
+
}
|
|
10308
|
+
function newTurnEntry(ev, input) {
|
|
10309
|
+
return {
|
|
10310
|
+
v: 1,
|
|
10311
|
+
startSeq: ev.seq,
|
|
10312
|
+
endSeq: ev.seq,
|
|
10313
|
+
startedAt: ev.ts,
|
|
10314
|
+
endedAt: null,
|
|
10315
|
+
status: "running",
|
|
10316
|
+
inputs: [{ seq: ev.seq, ts: ev.ts, text: clip(input, TURN_TEXT_MAX) }],
|
|
10317
|
+
replyPreview: "",
|
|
10318
|
+
producedFiles: [],
|
|
10319
|
+
llmCalls: 0,
|
|
10320
|
+
toolCalls: 0,
|
|
10321
|
+
tokensIn: 0,
|
|
10322
|
+
tokensOut: 0
|
|
10323
|
+
};
|
|
10324
|
+
}
|
|
10325
|
+
function applyTurnEvent(turn, ev, toolSeen) {
|
|
10326
|
+
const d = ev.data ?? {};
|
|
10327
|
+
turn.endSeq = ev.seq;
|
|
10328
|
+
switch (ev.type) {
|
|
10329
|
+
case "queue_steering": {
|
|
10330
|
+
turn.inputs.push({
|
|
10331
|
+
seq: ev.seq,
|
|
10332
|
+
ts: ev.ts,
|
|
10333
|
+
text: clip(String(d["text"] ?? ""), TURN_TEXT_MAX)
|
|
10334
|
+
});
|
|
10335
|
+
break;
|
|
10336
|
+
}
|
|
10337
|
+
case "assistant_message": {
|
|
10338
|
+
turn.llmCalls += 1;
|
|
10339
|
+
const text = String(d["text"] ?? "");
|
|
10340
|
+
if (text) {
|
|
10341
|
+
turn.replyPreview = clip(text);
|
|
10342
|
+
turn.replySeq = ev.seq;
|
|
10343
|
+
}
|
|
10344
|
+
const u = d["usage"];
|
|
10345
|
+
if (u && typeof u === "object") {
|
|
10346
|
+
const usage = u;
|
|
10347
|
+
turn.tokensIn += asNum(usage["input"]);
|
|
10348
|
+
turn.tokensOut += asNum(usage["output"]);
|
|
10349
|
+
}
|
|
10350
|
+
break;
|
|
10351
|
+
}
|
|
10352
|
+
case "tool_started":
|
|
10353
|
+
case "tool_finished": {
|
|
10354
|
+
const key = String(d["reqId"] ?? "") || `${String(d["name"] ?? "")}:${ev.seq}`;
|
|
10355
|
+
if (!toolSeen.has(key)) {
|
|
10356
|
+
toolSeen.add(key);
|
|
10357
|
+
turn.toolCalls += 1;
|
|
10358
|
+
}
|
|
10359
|
+
break;
|
|
10360
|
+
}
|
|
10361
|
+
case "error": {
|
|
10362
|
+
turn.error = clip(String(d["message"] ?? d["error"] ?? ""));
|
|
10363
|
+
break;
|
|
10364
|
+
}
|
|
10365
|
+
default:
|
|
10366
|
+
break;
|
|
10367
|
+
}
|
|
10368
|
+
}
|
|
10369
|
+
function closeTurnEntry(turn, ev) {
|
|
10370
|
+
const d = ev.data ?? {};
|
|
10371
|
+
turn.endSeq = ev.seq;
|
|
10372
|
+
turn.endedAt = ev.ts;
|
|
10373
|
+
const status = String(d["status"] ?? "");
|
|
10374
|
+
turn.status = ev.type === "interrupted" ? status || "interrupted" : status || "completed";
|
|
10375
|
+
const files = d["produced_files"];
|
|
10376
|
+
if (Array.isArray(files)) {
|
|
10377
|
+
turn.producedFiles = files.map((x) => String(x ?? "")).filter(Boolean);
|
|
10378
|
+
}
|
|
10379
|
+
if (typeof d["iterations"] === "number") turn.iterations = d["iterations"];
|
|
10380
|
+
const startMs = Date.parse(turn.startedAt);
|
|
10381
|
+
const endMs = Date.parse(ev.ts);
|
|
10382
|
+
if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
|
|
10383
|
+
turn.durationMs = endMs - startMs;
|
|
10384
|
+
}
|
|
10385
|
+
}
|
|
10386
|
+
function buildTurnIndex(events) {
|
|
10387
|
+
const out = [];
|
|
10388
|
+
let cur = null;
|
|
10389
|
+
let toolSeen = /* @__PURE__ */ new Set();
|
|
10390
|
+
for (const ev of events) {
|
|
10391
|
+
if (ev.type === "turn_start") {
|
|
10392
|
+
cur = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
10393
|
+
toolSeen = /* @__PURE__ */ new Set();
|
|
10394
|
+
out.push(cur);
|
|
10395
|
+
continue;
|
|
10396
|
+
}
|
|
10397
|
+
if (!cur) continue;
|
|
10398
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
10399
|
+
closeTurnEntry(cur, ev);
|
|
10400
|
+
cur = null;
|
|
10401
|
+
continue;
|
|
10402
|
+
}
|
|
10403
|
+
applyTurnEvent(cur, ev, toolSeen);
|
|
10404
|
+
}
|
|
10405
|
+
for (const t of out) {
|
|
10406
|
+
if (!t.endedAt && t.error) t.status = "error";
|
|
10407
|
+
}
|
|
10408
|
+
return out;
|
|
10409
|
+
}
|
|
10410
|
+
function summaryFromEntry(t) {
|
|
10411
|
+
return {
|
|
10412
|
+
startSeq: t.startSeq,
|
|
10413
|
+
endSeq: t.endSeq,
|
|
10414
|
+
startedAt: t.startedAt,
|
|
10415
|
+
endedAt: t.endedAt,
|
|
10416
|
+
status: t.status,
|
|
10417
|
+
input: t.inputs[0]?.text ?? "",
|
|
10418
|
+
reply: t.replyPreview,
|
|
10419
|
+
llmCalls: t.llmCalls,
|
|
10420
|
+
toolCalls: t.toolCalls,
|
|
10421
|
+
tokensIn: t.tokensIn,
|
|
10422
|
+
tokensOut: t.tokensOut,
|
|
10423
|
+
iterations: t.iterations,
|
|
10424
|
+
durationMs: t.durationMs,
|
|
10425
|
+
error: t.error,
|
|
10426
|
+
inputs: t.inputs,
|
|
10427
|
+
userCount: t.inputs.length,
|
|
10428
|
+
replySeq: t.replySeq,
|
|
10429
|
+
producedFiles: t.producedFiles
|
|
10430
|
+
};
|
|
10431
|
+
}
|
|
9836
10432
|
function isAffirmative(answer) {
|
|
9837
10433
|
const a = answer.trim().toLowerCase();
|
|
9838
10434
|
if (!a) return false;
|
|
@@ -9857,6 +10453,10 @@ var SessionManager = class {
|
|
|
9857
10453
|
opts;
|
|
9858
10454
|
runtimes = /* @__PURE__ */ new Map();
|
|
9859
10455
|
// sessionId → runtime
|
|
10456
|
+
/** 轮次索引的内存副本(运行中的轮比磁盘索引新;重启后为空) */
|
|
10457
|
+
turnIndexCache = /* @__PURE__ */ new Map();
|
|
10458
|
+
/** 进行中的索引重建任务(并发请求合并到同一个) */
|
|
10459
|
+
turnIndexBuilds = /* @__PURE__ */ new Map();
|
|
9860
10460
|
/** 全局通知 SSE 订阅者(控制台通知中心) */
|
|
9861
10461
|
notificationSubs = /* @__PURE__ */ new Set();
|
|
9862
10462
|
/**
|
|
@@ -9911,6 +10511,17 @@ var SessionManager = class {
|
|
|
9911
10511
|
rt.status = status;
|
|
9912
10512
|
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
9913
10513
|
}
|
|
10514
|
+
/**
|
|
10515
|
+
* 通知里的「最近一条用户消息」:截断到 LAST_USER_MESSAGE_MAX。
|
|
10516
|
+
*
|
|
10517
|
+
* 三处共用(session_status / turn_completed / turn_stopped),避免各自写一遍
|
|
10518
|
+
* 截断规则后长度不一致——控制台弹窗与任务列表会显示同一句话的两个版本。
|
|
10519
|
+
*/
|
|
10520
|
+
_lastMessage(rt) {
|
|
10521
|
+
const text = (rt.lastInput ?? "").trim();
|
|
10522
|
+
if (!text) return null;
|
|
10523
|
+
return text.length > LAST_USER_MESSAGE_MAX ? `${text.slice(0, LAST_USER_MESSAGE_MAX)}\u2026` : text;
|
|
10524
|
+
}
|
|
9914
10525
|
/**
|
|
9915
10526
|
* session_status 通知载荷:状态 + 最近一条用户消息(截断)。
|
|
9916
10527
|
*
|
|
@@ -9919,10 +10530,24 @@ var SessionManager = class {
|
|
|
9919
10530
|
*/
|
|
9920
10531
|
_statusData(rt) {
|
|
9921
10532
|
const data = { status: rt.status };
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
10533
|
+
if (rt.lastStartedAt) data.lastStartedAt = rt.lastStartedAt;
|
|
10534
|
+
const msg = this._lastMessage(rt);
|
|
10535
|
+
if (msg) data.lastUserMessage = msg;
|
|
10536
|
+
return data;
|
|
10537
|
+
}
|
|
10538
|
+
/**
|
|
10539
|
+
* 任务结束类通知(完成 / 达到迭代上限)的载荷:在 turn_end 数据之外补上
|
|
10540
|
+
* 「会话名 + 最后一条用户消息」。
|
|
10541
|
+
*
|
|
10542
|
+
* 控制台右下角的完成弹窗要说明「哪个会话、用户让它做了什么」。这些字段
|
|
10543
|
+
* 以节点侧为准随通知一起下发,前端就不必依赖快照(nodeInfo / session_status)
|
|
10544
|
+
* 是否已经到达——刚打开页面或通知乱序时才不会退化成只剩一句「任务已完成」。
|
|
10545
|
+
*/
|
|
10546
|
+
_turnEndData(rt, eventData, extra) {
|
|
10547
|
+
const data = { ...eventData, ...extra };
|
|
10548
|
+
if (rt.name) data.sessionName = rt.name;
|
|
10549
|
+
const msg = this._lastMessage(rt);
|
|
10550
|
+
if (msg) data.lastUserMessage = msg;
|
|
9926
10551
|
return data;
|
|
9927
10552
|
}
|
|
9928
10553
|
/** 触发通知回调(无 handler 时为空操作,异常隔离不冒泡) */
|
|
@@ -10079,6 +10704,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10079
10704
|
this.runtimes.delete(sessionId);
|
|
10080
10705
|
}
|
|
10081
10706
|
this.store.deleteSession(tenantId, sessionId);
|
|
10707
|
+
this.turnIndexCache.delete(sessionId);
|
|
10082
10708
|
this._publishNotify(tenantId, {
|
|
10083
10709
|
kind: "session_status",
|
|
10084
10710
|
sessionId,
|
|
@@ -10122,6 +10748,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10122
10748
|
throw new ApiError(409, "busy", "\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D");
|
|
10123
10749
|
}
|
|
10124
10750
|
rt.engine.queueSteering(input);
|
|
10751
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
10125
10752
|
rt.pendingSteers.push(input);
|
|
10126
10753
|
this._recordLastInput(rt, input);
|
|
10127
10754
|
return "steered";
|
|
@@ -10154,9 +10781,42 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10154
10781
|
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
10155
10782
|
if (!rt.engine) throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
10156
10783
|
rt.engine.queueSteering(text);
|
|
10784
|
+
if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
|
|
10157
10785
|
rt.pendingSteers.push(text);
|
|
10158
10786
|
this._recordLastInput(rt, text);
|
|
10159
10787
|
}
|
|
10788
|
+
/**
|
|
10789
|
+
* 撤销一条尚未注入的补充信息(用户反悔)。
|
|
10790
|
+
*
|
|
10791
|
+
* 队列里的项还没进聊天流,此时只能靠中断整个任务来「收回」代价太大。
|
|
10792
|
+
* index 取自 GET /state 的 pendingSteers(与前端展示顺序一致),两侧同时删:
|
|
10793
|
+
* 只删前端那份,引擎下一迭代仍会把它注入对话;只删引擎那份,队列里会残留
|
|
10794
|
+
* 一条永远不注入的条目。
|
|
10795
|
+
*
|
|
10796
|
+
* 若撤的正是「最近用户指令」,把它还原成队列里的上一条、或本轮被覆盖前的
|
|
10797
|
+
* 指令——否则工作台还会显示这条已经被取消的内容。
|
|
10798
|
+
*
|
|
10799
|
+
* @returns 剩余待注入队列(前端据此刷新提示区)
|
|
10800
|
+
*/
|
|
10801
|
+
unsteer(tenant, sessionId, index) {
|
|
10802
|
+
const rt = this._getOrLoadRuntime(tenant, sessionId);
|
|
10803
|
+
if (!rt.engine || !rt.runTask) {
|
|
10804
|
+
throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
|
|
10805
|
+
}
|
|
10806
|
+
if (!Number.isInteger(index) || index < 0 || index >= rt.pendingSteers.length) {
|
|
10807
|
+
throw new ApiError(409, "already_injected", "\u8FD9\u6761\u8865\u5145\u4FE1\u606F\u5DF2\u8FDB\u5165\u5BF9\u8BDD\uFF0C\u65E0\u6CD5\u64A4\u9500");
|
|
10808
|
+
}
|
|
10809
|
+
const [removed] = rt.pendingSteers.splice(index, 1);
|
|
10810
|
+
if (removed !== void 0) {
|
|
10811
|
+
rt.engine.removeSteering(removed);
|
|
10812
|
+
if (removed === rt.lastInput) {
|
|
10813
|
+
rt.lastInput = rt.pendingSteers.length > 0 ? rt.pendingSteers[rt.pendingSteers.length - 1] : rt.steerBaseInput;
|
|
10814
|
+
this._persist(rt);
|
|
10815
|
+
this._emitNotify(rt, "session_status", this._statusData(rt));
|
|
10816
|
+
}
|
|
10817
|
+
}
|
|
10818
|
+
return [...rt.pendingSteers];
|
|
10819
|
+
}
|
|
10160
10820
|
/**
|
|
10161
10821
|
* 已提交、尚未注入当前任务的补充信息。
|
|
10162
10822
|
*
|
|
@@ -10589,44 +11249,214 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
|
|
|
10589
11249
|
status: rt.status
|
|
10590
11250
|
};
|
|
10591
11251
|
}
|
|
10592
|
-
/**
|
|
10593
|
-
|
|
11252
|
+
/**
|
|
11253
|
+
* 事件分页(向前 / 向后两种方向)。
|
|
11254
|
+
*
|
|
11255
|
+
* - `since`:取 seq > since(实时增量 / 重连补齐)
|
|
11256
|
+
* - `before`:取 seq < before(控制台滚到顶部加载更早的对话)
|
|
11257
|
+
* - `maxBytes`:单次回执的字节预算(默认沿用 EVENTS_ACK_MAX_BYTES)
|
|
11258
|
+
*
|
|
11259
|
+
* 两者同时给出时按 `since`(向后)处理。`hasMore` 只对向前翻页有意义:
|
|
11260
|
+
* 表示本页最早一条之前还有更早的事件。
|
|
11261
|
+
*
|
|
11262
|
+
* 向前翻页一律走磁盘:内存缓冲只保留最近 EVENT_BUFFER_MAX 条,
|
|
11263
|
+
* 更早的事件只在 events.jsonl 里。
|
|
11264
|
+
*/
|
|
11265
|
+
eventsPage(tenantId, sessionId, opts = {}) {
|
|
10594
11266
|
const __t0 = dbgNow();
|
|
11267
|
+
const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : EVENTS_ACK_MAX_BYTES;
|
|
10595
11268
|
const rt = this.runtimes.get(sessionId);
|
|
11269
|
+
if (opts.before !== void 0) {
|
|
11270
|
+
const since2 = opts.since ?? 0;
|
|
11271
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
11272
|
+
before: opts.before,
|
|
11273
|
+
maxBytes
|
|
11274
|
+
});
|
|
11275
|
+
const ev2 = since2 > 0 ? page.events.filter((e) => e.seq > since2) : page.events;
|
|
11276
|
+
dbgMs(
|
|
11277
|
+
"session.eventsPage(before/\u78C1\u76D8)",
|
|
11278
|
+
__t0,
|
|
11279
|
+
// [dbg]
|
|
11280
|
+
`sid=${sessionId.slice(0, 8)} since=${since2} before=${opts.before} out=${ev2.length} more=${page.hasMore}`
|
|
11281
|
+
);
|
|
11282
|
+
return {
|
|
11283
|
+
events: ev2,
|
|
11284
|
+
latestSeq: rt?.seq ?? page.events.at(-1)?.seq ?? 0,
|
|
11285
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
11286
|
+
hasMore: page.hasMore
|
|
11287
|
+
};
|
|
11288
|
+
}
|
|
11289
|
+
const since = opts.since ?? 0;
|
|
10596
11290
|
if (rt) {
|
|
10597
|
-
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since));
|
|
11291
|
+
const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since), maxBytes);
|
|
10598
11292
|
dbgMs(
|
|
10599
|
-
"session.
|
|
11293
|
+
"session.eventsPage(\u5185\u5B58)",
|
|
10600
11294
|
__t0,
|
|
10601
11295
|
// [dbg]
|
|
10602
11296
|
`sid=${sessionId.slice(0, 8)} since=${since} in=${rt.events.length} out=${ev2.length}`
|
|
10603
11297
|
);
|
|
10604
|
-
return {
|
|
11298
|
+
return {
|
|
11299
|
+
events: ev2,
|
|
11300
|
+
latestSeq: rt.seq,
|
|
11301
|
+
earliestSeq: ev2[0]?.seq ?? 0,
|
|
11302
|
+
// 内存缓冲被截断(或 since 早于缓冲起点)时,磁盘上可能还有更早的
|
|
11303
|
+
hasMore: !!ev2.length && ev2[0].seq > (rt.events[0]?.seq ?? 0)
|
|
11304
|
+
};
|
|
10605
11305
|
}
|
|
10606
11306
|
const all = this.store.loadEventsTail(tenantId, sessionId, EVENTS_ACK_MAX_BYTES * 2);
|
|
10607
|
-
const ev = this._boundedTail(
|
|
11307
|
+
const ev = this._boundedTail(
|
|
11308
|
+
all.filter((e) => e.seq > since),
|
|
11309
|
+
maxBytes
|
|
11310
|
+
);
|
|
10608
11311
|
dbgMs(
|
|
10609
|
-
"session.
|
|
11312
|
+
"session.eventsPage(\u78C1\u76D8)",
|
|
10610
11313
|
__t0,
|
|
10611
11314
|
// [dbg]
|
|
10612
11315
|
`sid=${sessionId.slice(0, 8)} since=${since} loaded=${all.length} out=${ev.length}`
|
|
10613
11316
|
);
|
|
10614
11317
|
return {
|
|
10615
11318
|
events: ev,
|
|
10616
|
-
latestSeq: all.at(-1)?.seq ?? 0
|
|
11319
|
+
latestSeq: all.at(-1)?.seq ?? 0,
|
|
11320
|
+
earliestSeq: ev[0]?.seq ?? 0,
|
|
11321
|
+
// 读的是文件尾部窗口:窗口已满说明更早的事件还在文件里
|
|
11322
|
+
hasMore: all.length > 0 && ev.length > 0 && ev[0].seq > all[0].seq
|
|
10617
11323
|
};
|
|
10618
11324
|
}
|
|
11325
|
+
/**
|
|
11326
|
+
* 兼容旧调用:只按 since 取增量。
|
|
11327
|
+
* @deprecated 新代码用 eventsPage(支持 before 向前翻页)
|
|
11328
|
+
*/
|
|
11329
|
+
eventsSince(tenantId, sessionId, since) {
|
|
11330
|
+
const r = this.eventsPage(tenantId, sessionId, { since });
|
|
11331
|
+
return { events: r.events, latestSeq: r.latestSeq };
|
|
11332
|
+
}
|
|
11333
|
+
/**
|
|
11334
|
+
* 「按 turn 汇总」的轻量列表:只回标题与统计,点开某一轮再单独拉该轮回复。
|
|
11335
|
+
*
|
|
11336
|
+
* 数据来自 turns.ndjson 索引(派生自事件流),读取成本与会话历史长度无关:
|
|
11337
|
+
* 老会话首次访问时后台/本请求内重建一次,之后每页只读几 KB。
|
|
11338
|
+
*
|
|
11339
|
+
* - `before`:只返回 startSeq < before 的轮次(向前翻页)
|
|
11340
|
+
* - `turns` 按时间升序;`hasMore` 表示更早还有轮次
|
|
11341
|
+
*/
|
|
11342
|
+
async sessionTurns(tenantId, sessionId, opts = {}) {
|
|
11343
|
+
const limit = Math.min(Math.max(opts.limit ?? TURNS_DEFAULT_LIMIT, 1), 100);
|
|
11344
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
11345
|
+
const before = opts.before;
|
|
11346
|
+
const endIdx = before === void 0 ? entries.length : entries.findIndex((t) => t.startSeq >= before);
|
|
11347
|
+
const end = endIdx < 0 ? entries.length : endIdx;
|
|
11348
|
+
const start = Math.max(0, end - limit);
|
|
11349
|
+
const picked = entries.slice(start, end);
|
|
11350
|
+
return {
|
|
11351
|
+
turns: picked.map(summaryFromEntry),
|
|
11352
|
+
hasMore: start > 0,
|
|
11353
|
+
earliestSeq: picked[0]?.startSeq ?? 0,
|
|
11354
|
+
total: entries.length,
|
|
11355
|
+
userTotal: entries.reduce((n, t) => n + t.inputs.length, 0)
|
|
11356
|
+
};
|
|
11357
|
+
}
|
|
11358
|
+
/**
|
|
11359
|
+
* 某一轮的用户消息与最终回复(含产出文件)。
|
|
11360
|
+
*
|
|
11361
|
+
* 只读两条索引记录 + 尾部一个事件页,不会因该轮工具输出很大而变慢:
|
|
11362
|
+
* 回复正文紧邻 turn_end,按 replySeq 精确读即可。
|
|
11363
|
+
*/
|
|
11364
|
+
async sessionTurnReply(tenantId, sessionId, startSeq) {
|
|
11365
|
+
const entries = await this._ensureTurnIndex(tenantId, sessionId);
|
|
11366
|
+
const turn = entries.find((t) => t.startSeq === startSeq);
|
|
11367
|
+
if (!turn) throw new ApiError(404, "not_found", "\u8F6E\u6B21\u4E0D\u5B58\u5728");
|
|
11368
|
+
let reply = null;
|
|
11369
|
+
let truncated = false;
|
|
11370
|
+
if (turn.replySeq !== void 0) {
|
|
11371
|
+
const page = this.store.loadEventPage(tenantId, sessionId, {
|
|
11372
|
+
before: turn.replySeq + 1,
|
|
11373
|
+
maxBytes: TURN_REPLY_MAX_BYTES
|
|
11374
|
+
});
|
|
11375
|
+
const ev = page.events.find((e) => e.seq === turn.replySeq);
|
|
11376
|
+
if (ev && ev.type === "assistant_message") {
|
|
11377
|
+
const text = String(ev.data?.["text"] ?? "");
|
|
11378
|
+
if (text.length > TURN_REPLY_MAX_TEXT) {
|
|
11379
|
+
reply = { seq: ev.seq, ts: ev.ts, text: text.slice(0, TURN_REPLY_MAX_TEXT) };
|
|
11380
|
+
truncated = true;
|
|
11381
|
+
} else {
|
|
11382
|
+
reply = { seq: ev.seq, ts: ev.ts, text };
|
|
11383
|
+
}
|
|
11384
|
+
}
|
|
11385
|
+
}
|
|
11386
|
+
return {
|
|
11387
|
+
startSeq: turn.startSeq,
|
|
11388
|
+
endSeq: turn.endSeq,
|
|
11389
|
+
status: turn.status,
|
|
11390
|
+
startedAt: turn.startedAt,
|
|
11391
|
+
endedAt: turn.endedAt,
|
|
11392
|
+
userMessages: turn.inputs,
|
|
11393
|
+
reply,
|
|
11394
|
+
truncated,
|
|
11395
|
+
producedFiles: turn.producedFiles
|
|
11396
|
+
};
|
|
11397
|
+
}
|
|
11398
|
+
/**
|
|
11399
|
+
* 取轮次索引;缺失时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
11400
|
+
*
|
|
11401
|
+
* 索引是派生数据:损坏/落后都只需重建,不影响会话本身。
|
|
11402
|
+
*/
|
|
11403
|
+
/**
|
|
11404
|
+
* 取轮次索引;缺失/落后时从事件流重建(同一会话的并发请求共用一个重建任务)。
|
|
11405
|
+
*
|
|
11406
|
+
* 索引是派生数据:损坏或落后都只需重建,不影响会话本身。
|
|
11407
|
+
*/
|
|
11408
|
+
async _ensureTurnIndex(tenantId, sessionId) {
|
|
11409
|
+
let entries = this._mergeTurnIndex(tenantId, sessionId);
|
|
11410
|
+
if (entries.length > 0 && !this.runtimes.has(sessionId)) {
|
|
11411
|
+
const last = entries[entries.length - 1];
|
|
11412
|
+
const tail = this.store.loadEventsTail(tenantId, sessionId, 64 * 1024);
|
|
11413
|
+
const newTurnStarted = tail.some((e) => e.type === "turn_start" && e.seq > last.endSeq);
|
|
11414
|
+
if (newTurnStarted || last.status === "running") entries = [];
|
|
11415
|
+
}
|
|
11416
|
+
if (entries.length > 0) return entries;
|
|
11417
|
+
const pending = this.turnIndexBuilds.get(sessionId);
|
|
11418
|
+
if (pending) return pending;
|
|
11419
|
+
const task = this._rebuildTurnIndex(tenantId, sessionId).finally(() => {
|
|
11420
|
+
this.turnIndexBuilds.delete(sessionId);
|
|
11421
|
+
});
|
|
11422
|
+
this.turnIndexBuilds.set(sessionId, task);
|
|
11423
|
+
return task;
|
|
11424
|
+
}
|
|
11425
|
+
/** 合并磁盘索引与内存索引(内存版本更新,同一 startSeq 以内存为准) */
|
|
11426
|
+
_mergeTurnIndex(tenantId, sessionId) {
|
|
11427
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
11428
|
+
for (const e of this.store.loadTurnIndex(tenantId, sessionId)) byStart.set(e.startSeq, e);
|
|
11429
|
+
for (const e of this.turnIndexCache.get(sessionId) ?? []) byStart.set(e.startSeq, e);
|
|
11430
|
+
return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
|
|
11431
|
+
}
|
|
11432
|
+
/** 从 events.jsonl 顺序流式重建轮次索引(一次性,之后增量维护) */
|
|
11433
|
+
async _rebuildTurnIndex(tenantId, sessionId) {
|
|
11434
|
+
const __t0 = dbgNow();
|
|
11435
|
+
const events = await this.store.readEventsAsync(tenantId, sessionId);
|
|
11436
|
+
const entries = buildTurnIndex(events);
|
|
11437
|
+
if (entries.length > 0) {
|
|
11438
|
+
await this.store.saveTurnIndex(tenantId, sessionId, entries);
|
|
11439
|
+
if (this.runtimes.has(sessionId)) this.turnIndexCache.set(sessionId, entries);
|
|
11440
|
+
}
|
|
11441
|
+
dbgMs(
|
|
11442
|
+
"session._rebuildTurnIndex",
|
|
11443
|
+
__t0,
|
|
11444
|
+
// [dbg]
|
|
11445
|
+
`sid=${sessionId.slice(0, 8)} events=${events.length} turns=${entries.length}`
|
|
11446
|
+
);
|
|
11447
|
+
return entries;
|
|
11448
|
+
}
|
|
10619
11449
|
/**
|
|
10620
11450
|
* 按字节预算截取事件「尾部」(至少保留最后一条),确保 MQTT 回执远小于单包上限。
|
|
10621
11451
|
* 供 get_events 使用:会话历史可任意大,但 MQTT 链路不能承载大包。
|
|
10622
11452
|
*/
|
|
10623
|
-
_boundedTail(events) {
|
|
11453
|
+
_boundedTail(events, maxBytes = EVENTS_ACK_MAX_BYTES) {
|
|
10624
11454
|
const __t0 = dbgNow();
|
|
10625
11455
|
let total = 0;
|
|
10626
11456
|
let i = events.length;
|
|
10627
11457
|
while (i > 0) {
|
|
10628
11458
|
const size = Buffer.byteLength(JSON.stringify(events[i - 1]));
|
|
10629
|
-
if (total > 0 && total + size >
|
|
11459
|
+
if (total > 0 && total + size > maxBytes) break;
|
|
10630
11460
|
total += size;
|
|
10631
11461
|
i -= 1;
|
|
10632
11462
|
}
|
|
@@ -10746,6 +11576,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10746
11576
|
nameSource: rec.nameSource,
|
|
10747
11577
|
taskCount: rec.taskCount ?? 0,
|
|
10748
11578
|
lastCompletedAt: rec.lastCompletedAt,
|
|
11579
|
+
lastStartedAt: rec.lastStartedAt,
|
|
10749
11580
|
naming: false,
|
|
10750
11581
|
turnStartedAt: 0
|
|
10751
11582
|
};
|
|
@@ -10763,10 +11594,11 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10763
11594
|
}
|
|
10764
11595
|
/** 装配引擎并启动一轮 Turn(供 submitTurn / submitOrSteer 复用) */
|
|
10765
11596
|
_startTurn(rt, input) {
|
|
11597
|
+
const engine = this._buildEngine(rt.tenant, rt, true);
|
|
10766
11598
|
rt.lastInput = input;
|
|
10767
|
-
this._setStatus(rt, "running");
|
|
10768
11599
|
rt.turnStartedAt = Date.now();
|
|
10769
|
-
|
|
11600
|
+
rt.lastStartedAt = new Date(rt.turnStartedAt).toISOString();
|
|
11601
|
+
this._setStatus(rt, "running");
|
|
10770
11602
|
rt.engine = engine;
|
|
10771
11603
|
this._persist(rt);
|
|
10772
11604
|
rt.runTask = this._drive(rt, engine.run(input));
|
|
@@ -10825,18 +11657,18 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10825
11657
|
this._setStatus(rt, "completed");
|
|
10826
11658
|
rt.taskCount += 1;
|
|
10827
11659
|
rt.lastCompletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10828
|
-
this._emitNotify(
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
durationMs
|
|
10832
|
-
|
|
11660
|
+
this._emitNotify(
|
|
11661
|
+
rt,
|
|
11662
|
+
"turn_completed",
|
|
11663
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
11664
|
+
);
|
|
10833
11665
|
} else if (status === "max_iterations_exceeded") {
|
|
10834
11666
|
this._setStatus(rt, "stopped");
|
|
10835
|
-
this._emitNotify(
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
durationMs
|
|
10839
|
-
|
|
11667
|
+
this._emitNotify(
|
|
11668
|
+
rt,
|
|
11669
|
+
"turn_stopped",
|
|
11670
|
+
this._turnEndData(rt, event.data, { summary, durationMs })
|
|
11671
|
+
);
|
|
10840
11672
|
}
|
|
10841
11673
|
this._persist(rt);
|
|
10842
11674
|
if (status === "completed" && engine && shouldAutoName(rt.taskCount, rt.nameSource)) {
|
|
@@ -10847,7 +11679,8 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10847
11679
|
case "error" /* ERROR */:
|
|
10848
11680
|
this._setStatus(rt, "error");
|
|
10849
11681
|
this._persist(rt);
|
|
10850
|
-
this._emitNotify(rt, "error", event.data);
|
|
11682
|
+
this._emitNotify(rt, "error", this._turnEndData(rt, event.data, {}));
|
|
11683
|
+
healBuiltinOnAuthFailure(this.store, String(event.data["error"] ?? ""));
|
|
10851
11684
|
break;
|
|
10852
11685
|
case "interrupted" /* INTERRUPTED */:
|
|
10853
11686
|
this._setStatus(rt, "interrupted");
|
|
@@ -10873,9 +11706,17 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10873
11706
|
this._setStatus(rt, "error");
|
|
10874
11707
|
this._persist(rt);
|
|
10875
11708
|
} finally {
|
|
11709
|
+
if (rt.turnIndex) {
|
|
11710
|
+
const turn = rt.turnIndex;
|
|
11711
|
+
if (!turn.endedAt && turn.error) turn.status = "error";
|
|
11712
|
+
this._appendTurnIndex(rt, turn);
|
|
11713
|
+
rt.turnIndex = void 0;
|
|
11714
|
+
rt.turnToolKeys = void 0;
|
|
11715
|
+
}
|
|
10876
11716
|
rt.runTask = null;
|
|
10877
11717
|
rt.engine = null;
|
|
10878
11718
|
rt.pendingSteers = [];
|
|
11719
|
+
rt.steerBaseInput = void 0;
|
|
10879
11720
|
}
|
|
10880
11721
|
}
|
|
10881
11722
|
_buffer(rt, event) {
|
|
@@ -10888,6 +11729,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10888
11729
|
rt.events.push(entry);
|
|
10889
11730
|
if (rt.events.length > EVENT_BUFFER_MAX) rt.events.shift();
|
|
10890
11731
|
void this.store.appendEvent(rt.tenantId, rt.sessionId, entry);
|
|
11732
|
+
this._trackTurnIndex(rt, entry);
|
|
10891
11733
|
for (const res of rt.subscribers) this._sseSend(res, entry);
|
|
10892
11734
|
this._broadcastSessionEvent(rt.tenantId, rt.sessionId, entry);
|
|
10893
11735
|
try {
|
|
@@ -10895,6 +11737,42 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10895
11737
|
} catch {
|
|
10896
11738
|
}
|
|
10897
11739
|
}
|
|
11740
|
+
/**
|
|
11741
|
+
* 维护运行中轮次的索引(in-memory 记账 + 关键节点落盘)。
|
|
11742
|
+
*
|
|
11743
|
+
* 落盘只在「开轮 / 补充消息 / 收轮」发生,delta 事件仅推进 endSeq 与统计,
|
|
11744
|
+
* 因此索引写入量是每轮 2~3 行,与会话事件量无关。
|
|
11745
|
+
*/
|
|
11746
|
+
_trackTurnIndex(rt, ev) {
|
|
11747
|
+
if (ev.type === "turn_start") {
|
|
11748
|
+
const turn2 = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
|
|
11749
|
+
rt.turnIndex = turn2;
|
|
11750
|
+
rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
11751
|
+
this._appendTurnIndex(rt, turn2);
|
|
11752
|
+
return;
|
|
11753
|
+
}
|
|
11754
|
+
const turn = rt.turnIndex;
|
|
11755
|
+
if (!turn) return;
|
|
11756
|
+
if (ev.type === "turn_end" || ev.type === "interrupted") {
|
|
11757
|
+
closeTurnEntry(turn, ev);
|
|
11758
|
+
this._appendTurnIndex(rt, turn);
|
|
11759
|
+
rt.turnIndex = void 0;
|
|
11760
|
+
rt.turnToolKeys = void 0;
|
|
11761
|
+
return;
|
|
11762
|
+
}
|
|
11763
|
+
if (!rt.turnToolKeys) rt.turnToolKeys = /* @__PURE__ */ new Set();
|
|
11764
|
+
const inputsBefore = turn.inputs.length;
|
|
11765
|
+
applyTurnEvent(turn, ev, rt.turnToolKeys);
|
|
11766
|
+
if (turn.inputs.length !== inputsBefore) this._appendTurnIndex(rt, turn);
|
|
11767
|
+
}
|
|
11768
|
+
/** 写入一版轮次索引(内存副本 + 异步落盘) */
|
|
11769
|
+
_appendTurnIndex(rt, turn) {
|
|
11770
|
+
const cached3 = this.turnIndexCache.get(rt.sessionId) ?? [];
|
|
11771
|
+
const i = cached3.findIndex((t) => t.startSeq === turn.startSeq);
|
|
11772
|
+
const next = i >= 0 ? [...cached3.slice(0, i), turn, ...cached3.slice(i + 1)] : [...cached3, turn];
|
|
11773
|
+
this.turnIndexCache.set(rt.sessionId, next);
|
|
11774
|
+
void this.store.appendTurnIndex(rt.tenantId, rt.sessionId, turn);
|
|
11775
|
+
}
|
|
10898
11776
|
/** 回收内存 run 任务(释放引用交给 GC;磁盘状态保持不动) */
|
|
10899
11777
|
_recycleRunTask(rt) {
|
|
10900
11778
|
rt.runTask = null;
|
|
@@ -10932,6 +11810,7 @@ data: ${JSON.stringify(entry.data)}
|
|
|
10932
11810
|
nameSource: rt.nameSource,
|
|
10933
11811
|
taskCount: rt.taskCount,
|
|
10934
11812
|
lastCompletedAt: rt.lastCompletedAt,
|
|
11813
|
+
lastStartedAt: rt.lastStartedAt,
|
|
10935
11814
|
modelOverridden: rt.modelOverridden,
|
|
10936
11815
|
providerId: rt.providerId,
|
|
10937
11816
|
permissionMode: rt.permissionMode,
|
|
@@ -11232,7 +12111,6 @@ import * as path26 from "path";
|
|
|
11232
12111
|
// src/server/projects.ts
|
|
11233
12112
|
init_esm_shims();
|
|
11234
12113
|
import * as path22 from "path";
|
|
11235
|
-
var DEFAULT_MODEL = () => process.env.OPENAI_MODEL || process.env.MODEL || "gpt-4o";
|
|
11236
12114
|
function toProjectSummary(store, tenant) {
|
|
11237
12115
|
return {
|
|
11238
12116
|
id: tenant.id,
|
|
@@ -11342,6 +12220,7 @@ function allSessionStates(store, manager) {
|
|
|
11342
12220
|
name: s.name ?? null,
|
|
11343
12221
|
status: s.status,
|
|
11344
12222
|
lastUserMessage: lastUserMessage(s),
|
|
12223
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
11345
12224
|
lastCompletedAt: lastCompletedAt(s)
|
|
11346
12225
|
});
|
|
11347
12226
|
}
|
|
@@ -11371,7 +12250,7 @@ function createProject(store, input) {
|
|
|
11371
12250
|
id,
|
|
11372
12251
|
name: explicitName || defaultName,
|
|
11373
12252
|
apiKey: generateApiKey(),
|
|
11374
|
-
model: typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() :
|
|
12253
|
+
model: typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : defaultTenantModel(store),
|
|
11375
12254
|
provider: {
|
|
11376
12255
|
apiKey: typeof raw.providerApiKey === "string" && raw.providerApiKey.trim() ? raw.providerApiKey.trim() : void 0,
|
|
11377
12256
|
baseURL: typeof raw.baseURL === "string" && raw.baseURL.trim() ? raw.baseURL.trim() : void 0
|
|
@@ -11396,7 +12275,7 @@ function buildNodeInfo(store, opts) {
|
|
|
11396
12275
|
nodeId: opts.nodeId ?? ensureNodeId(store),
|
|
11397
12276
|
platform: process.platform,
|
|
11398
12277
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
11399
|
-
version: "0.2.
|
|
12278
|
+
version: "0.2.20",
|
|
11400
12279
|
admin: opts.admin,
|
|
11401
12280
|
projects: opts.projects
|
|
11402
12281
|
};
|
|
@@ -11762,9 +12641,133 @@ function makeDirIn(parentInput, name) {
|
|
|
11762
12641
|
init_esm_shims();
|
|
11763
12642
|
import * as fs20 from "fs";
|
|
11764
12643
|
import * as path24 from "path";
|
|
12644
|
+
|
|
12645
|
+
// src/server/zipArchive.ts
|
|
12646
|
+
init_esm_shims();
|
|
12647
|
+
import * as zlib from "zlib";
|
|
12648
|
+
var MAX_ZIP_ENTRIES = 2e4;
|
|
12649
|
+
var LOCAL_SIG = 67324752;
|
|
12650
|
+
var CENTRAL_SIG = 33639248;
|
|
12651
|
+
var EOCD_SIG = 101010256;
|
|
12652
|
+
var FLAG_UTF8 = 2048;
|
|
12653
|
+
var METHOD_STORE = 0;
|
|
12654
|
+
var METHOD_DEFLATE = 8;
|
|
12655
|
+
var CRC_TABLE = (() => {
|
|
12656
|
+
const table = new Int32Array(256);
|
|
12657
|
+
for (let n = 0; n < 256; n++) {
|
|
12658
|
+
let c = n;
|
|
12659
|
+
for (let k = 0; k < 8; k++) {
|
|
12660
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
12661
|
+
}
|
|
12662
|
+
table[n] = c;
|
|
12663
|
+
}
|
|
12664
|
+
return table;
|
|
12665
|
+
})();
|
|
12666
|
+
function crc32(buf) {
|
|
12667
|
+
let c = -1;
|
|
12668
|
+
for (let i = 0; i < buf.length; i++) {
|
|
12669
|
+
c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
|
|
12670
|
+
}
|
|
12671
|
+
return (c ^ -1) >>> 0;
|
|
12672
|
+
}
|
|
12673
|
+
function dosDateTime(d) {
|
|
12674
|
+
const year = Math.max(1980, d.getFullYear());
|
|
12675
|
+
return {
|
|
12676
|
+
time: d.getHours() << 11 | d.getMinutes() << 5 | d.getSeconds() >> 1,
|
|
12677
|
+
date: year - 1980 << 9 | d.getMonth() + 1 << 5 | d.getDate()
|
|
12678
|
+
};
|
|
12679
|
+
}
|
|
12680
|
+
function buildZip(entries) {
|
|
12681
|
+
if (entries.length > MAX_ZIP_ENTRIES) {
|
|
12682
|
+
throw new Error(`\u5F52\u6863\u6761\u76EE\u8FC7\u591A\uFF08${entries.length} > ${MAX_ZIP_ENTRIES}\uFF09`);
|
|
12683
|
+
}
|
|
12684
|
+
const chunks = [];
|
|
12685
|
+
const central = [];
|
|
12686
|
+
let offset = 0;
|
|
12687
|
+
for (const e of entries) {
|
|
12688
|
+
const name = Buffer.from(e.name, "utf-8");
|
|
12689
|
+
const isDir = e.name.endsWith("/");
|
|
12690
|
+
const raw = isDir ? Buffer.alloc(0) : e.data ?? Buffer.alloc(0);
|
|
12691
|
+
let method = METHOD_STORE;
|
|
12692
|
+
let body = raw;
|
|
12693
|
+
if (!isDir && raw.length > 0) {
|
|
12694
|
+
const deflated = zlib.deflateRawSync(raw, { level: 6 });
|
|
12695
|
+
if (deflated.length < raw.length) {
|
|
12696
|
+
method = METHOD_DEFLATE;
|
|
12697
|
+
body = deflated;
|
|
12698
|
+
}
|
|
12699
|
+
}
|
|
12700
|
+
const crc = isDir ? 0 : crc32(raw);
|
|
12701
|
+
const { time, date } = dosDateTime(e.mtime ?? /* @__PURE__ */ new Date());
|
|
12702
|
+
const local = Buffer.alloc(30);
|
|
12703
|
+
local.writeUInt32LE(LOCAL_SIG, 0);
|
|
12704
|
+
local.writeUInt16LE(20, 4);
|
|
12705
|
+
local.writeUInt16LE(FLAG_UTF8, 6);
|
|
12706
|
+
local.writeUInt16LE(method, 8);
|
|
12707
|
+
local.writeUInt16LE(time, 10);
|
|
12708
|
+
local.writeUInt16LE(date, 12);
|
|
12709
|
+
local.writeUInt32LE(crc, 14);
|
|
12710
|
+
local.writeUInt32LE(body.length, 18);
|
|
12711
|
+
local.writeUInt32LE(raw.length, 22);
|
|
12712
|
+
local.writeUInt16LE(name.length, 26);
|
|
12713
|
+
local.writeUInt16LE(0, 28);
|
|
12714
|
+
chunks.push(local, name);
|
|
12715
|
+
if (body.length > 0) chunks.push(body);
|
|
12716
|
+
central.push({
|
|
12717
|
+
name,
|
|
12718
|
+
method,
|
|
12719
|
+
crc,
|
|
12720
|
+
csize: body.length,
|
|
12721
|
+
usize: raw.length,
|
|
12722
|
+
offset,
|
|
12723
|
+
time,
|
|
12724
|
+
date,
|
|
12725
|
+
isDir
|
|
12726
|
+
});
|
|
12727
|
+
offset += local.length + name.length + body.length;
|
|
12728
|
+
}
|
|
12729
|
+
const cdStart = offset;
|
|
12730
|
+
for (const c of central) {
|
|
12731
|
+
const head = Buffer.alloc(46);
|
|
12732
|
+
head.writeUInt32LE(CENTRAL_SIG, 0);
|
|
12733
|
+
head.writeUInt16LE(20, 4);
|
|
12734
|
+
head.writeUInt16LE(20, 6);
|
|
12735
|
+
head.writeUInt16LE(FLAG_UTF8, 8);
|
|
12736
|
+
head.writeUInt16LE(c.method, 10);
|
|
12737
|
+
head.writeUInt16LE(c.time, 12);
|
|
12738
|
+
head.writeUInt16LE(c.date, 14);
|
|
12739
|
+
head.writeUInt32LE(c.crc, 16);
|
|
12740
|
+
head.writeUInt32LE(c.csize, 20);
|
|
12741
|
+
head.writeUInt32LE(c.usize, 24);
|
|
12742
|
+
head.writeUInt16LE(c.name.length, 28);
|
|
12743
|
+
head.writeUInt16LE(0, 30);
|
|
12744
|
+
head.writeUInt16LE(0, 32);
|
|
12745
|
+
head.writeUInt16LE(0, 34);
|
|
12746
|
+
head.writeUInt16LE(0, 36);
|
|
12747
|
+
head.writeUInt32LE(c.isDir ? 16 : 0, 38);
|
|
12748
|
+
head.writeUInt32LE(c.offset, 42);
|
|
12749
|
+
chunks.push(head, c.name);
|
|
12750
|
+
offset += head.length + c.name.length;
|
|
12751
|
+
}
|
|
12752
|
+
const eocd = Buffer.alloc(22);
|
|
12753
|
+
eocd.writeUInt32LE(EOCD_SIG, 0);
|
|
12754
|
+
eocd.writeUInt16LE(0, 4);
|
|
12755
|
+
eocd.writeUInt16LE(0, 6);
|
|
12756
|
+
eocd.writeUInt16LE(central.length, 8);
|
|
12757
|
+
eocd.writeUInt16LE(central.length, 10);
|
|
12758
|
+
eocd.writeUInt32LE(offset - cdStart, 12);
|
|
12759
|
+
eocd.writeUInt32LE(cdStart, 16);
|
|
12760
|
+
eocd.writeUInt16LE(0, 20);
|
|
12761
|
+
chunks.push(eocd);
|
|
12762
|
+
return Buffer.concat(chunks);
|
|
12763
|
+
}
|
|
12764
|
+
|
|
12765
|
+
// src/server/projectFiles.ts
|
|
11765
12766
|
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11766
12767
|
var MAX_DOWNLOAD_BYTES = 16 * 1024 * 1024;
|
|
12768
|
+
var MAX_BINARY_BYTES = 2.5 * 1024 * 1024;
|
|
11767
12769
|
var MAX_ENTRIES2 = 2e3;
|
|
12770
|
+
var MAX_ZIP_BYTES = 100 * 1024 * 1024;
|
|
11768
12771
|
function toPosix(p) {
|
|
11769
12772
|
return p.split(path24.sep).join("/");
|
|
11770
12773
|
}
|
|
@@ -11911,6 +12914,26 @@ function writeProjectFile(workspaceRoot, relPath, content) {
|
|
|
11911
12914
|
}
|
|
11912
12915
|
return { path: toPosix(path24.relative(base, file)), size: buf.length };
|
|
11913
12916
|
}
|
|
12917
|
+
function writeProjectFileBase64(workspaceRoot, relPath, base64) {
|
|
12918
|
+
const base = projectRoot(workspaceRoot);
|
|
12919
|
+
const rel = (relPath ?? "").trim();
|
|
12920
|
+
if (!rel) throw new Error("\u6587\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
|
|
12921
|
+
const file = resolveWithin(base, rel);
|
|
12922
|
+
const b64 = String(base64 ?? "").replace(/\s+/g, "");
|
|
12923
|
+
if (!b64) throw new Error("\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
|
|
12924
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) throw new Error("\u5185\u5BB9\u4E0D\u662F\u5408\u6CD5\u7684 base64");
|
|
12925
|
+
if (Math.floor(b64.length * 3 / 4) > MAX_BINARY_BYTES) {
|
|
12926
|
+
throw new Error(`\u5185\u5BB9\u8D85\u8FC7 ${MAX_BINARY_BYTES / (1024 * 1024)}MB \u4E0A\u9650\uFF0C\u65E0\u6CD5\u4FDD\u5B58`);
|
|
12927
|
+
}
|
|
12928
|
+
const buf = Buffer.from(b64, "base64");
|
|
12929
|
+
try {
|
|
12930
|
+
fs20.mkdirSync(path24.dirname(file), { recursive: true });
|
|
12931
|
+
fs20.writeFileSync(file, buf);
|
|
12932
|
+
} catch (e) {
|
|
12933
|
+
throw new Error(`\u5199\u5165\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
12934
|
+
}
|
|
12935
|
+
return { path: toPosix(path24.relative(base, file)), size: buf.length };
|
|
12936
|
+
}
|
|
11914
12937
|
function createProjectDir(workspaceRoot, relPath) {
|
|
11915
12938
|
const base = projectRoot(workspaceRoot);
|
|
11916
12939
|
const rel = (relPath ?? "").trim();
|
|
@@ -11984,6 +13007,170 @@ function searchProjectFiles(workspaceRoot, query) {
|
|
|
11984
13007
|
walk(base, "");
|
|
11985
13008
|
return out;
|
|
11986
13009
|
}
|
|
13010
|
+
function deleteProjectPaths(workspaceRoot, relPaths) {
|
|
13011
|
+
const base = projectRoot(workspaceRoot);
|
|
13012
|
+
const out = { deleted: [], failed: [] };
|
|
13013
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13014
|
+
for (const raw of relPaths ?? []) {
|
|
13015
|
+
const rel = (raw ?? "").trim().replace(/^[/\\]+/, "");
|
|
13016
|
+
if (!rel) {
|
|
13017
|
+
out.failed.push({ path: String(raw ?? ""), error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
13018
|
+
continue;
|
|
13019
|
+
}
|
|
13020
|
+
if (seen.has(rel)) continue;
|
|
13021
|
+
seen.add(rel);
|
|
13022
|
+
let abs;
|
|
13023
|
+
try {
|
|
13024
|
+
abs = resolveWithin(base, rel);
|
|
13025
|
+
} catch (e) {
|
|
13026
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
13027
|
+
continue;
|
|
13028
|
+
}
|
|
13029
|
+
if (path24.resolve(abs) === path24.resolve(base)) {
|
|
13030
|
+
out.failed.push({ path: rel, error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
|
|
13031
|
+
continue;
|
|
13032
|
+
}
|
|
13033
|
+
if (!fs20.existsSync(abs)) {
|
|
13034
|
+
out.failed.push({ path: rel, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
|
|
13035
|
+
continue;
|
|
13036
|
+
}
|
|
13037
|
+
try {
|
|
13038
|
+
fs20.rmSync(abs, { recursive: true, force: true });
|
|
13039
|
+
out.deleted.push(toPosix(path24.relative(base, abs)));
|
|
13040
|
+
} catch (e) {
|
|
13041
|
+
out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
|
|
13042
|
+
}
|
|
13043
|
+
}
|
|
13044
|
+
return out;
|
|
13045
|
+
}
|
|
13046
|
+
function formatBytes(n) {
|
|
13047
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
13048
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
13049
|
+
return `${n} B`;
|
|
13050
|
+
}
|
|
13051
|
+
function renameProjectPath(workspaceRoot, relPath, newName) {
|
|
13052
|
+
const base = projectRoot(workspaceRoot);
|
|
13053
|
+
const rel = (relPath ?? "").trim().replace(/^[/\\]+/, "");
|
|
13054
|
+
if (!rel) throw new Error("\u4E0D\u80FD\u91CD\u547D\u540D\u9879\u76EE\u6839\u76EE\u5F55");
|
|
13055
|
+
const abs = resolveWithin(base, rel);
|
|
13056
|
+
const name = (newName ?? "").trim();
|
|
13057
|
+
if (!name) throw new Error("\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
|
|
13058
|
+
if (name === "." || name === "..") throw new Error("\u540D\u79F0\u975E\u6CD5");
|
|
13059
|
+
if (/[/\\]/.test(name)) throw new Error("\u540D\u79F0\u4E0D\u80FD\u5305\u542B\u8DEF\u5F84\u5206\u9694\u7B26");
|
|
13060
|
+
if (!fs20.existsSync(abs)) throw new Error("\u6587\u4EF6\u4E0D\u5B58\u5728");
|
|
13061
|
+
const parent = path24.dirname(abs);
|
|
13062
|
+
const target = path24.join(parent, name);
|
|
13063
|
+
if (!isWithinRoot(base, target)) throw new Error("\u8DEF\u5F84\u8D8A\u754C\uFF08\u8D85\u51FA\u9879\u76EE\u5DE5\u4F5C\u533A\uFF09");
|
|
13064
|
+
if (fs20.existsSync(target)) {
|
|
13065
|
+
let same = false;
|
|
13066
|
+
try {
|
|
13067
|
+
const a = fs20.statSync(target);
|
|
13068
|
+
const b = fs20.statSync(abs);
|
|
13069
|
+
same = a.ino === b.ino && a.dev === b.dev;
|
|
13070
|
+
} catch {
|
|
13071
|
+
same = false;
|
|
13072
|
+
}
|
|
13073
|
+
if (!same) throw new Error(`\u5DF2\u5B58\u5728\u540C\u540D\u6587\u4EF6\u6216\u76EE\u5F55: ${name}`);
|
|
13074
|
+
}
|
|
13075
|
+
if (path24.resolve(target) === path24.resolve(abs)) {
|
|
13076
|
+
return { from: toPosix(path24.relative(base, abs)), path: toPosix(path24.relative(base, abs)) };
|
|
13077
|
+
}
|
|
13078
|
+
try {
|
|
13079
|
+
fs20.renameSync(abs, target);
|
|
13080
|
+
} catch (e) {
|
|
13081
|
+
throw new Error(`\u91CD\u547D\u540D\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
13082
|
+
}
|
|
13083
|
+
return { from: toPosix(path24.relative(base, abs)), path: toPosix(path24.relative(base, target)) };
|
|
13084
|
+
}
|
|
13085
|
+
function zipProjectPaths(workspaceRoot, relPaths, opts = {}) {
|
|
13086
|
+
const base = projectRoot(workspaceRoot);
|
|
13087
|
+
const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : MAX_ZIP_BYTES;
|
|
13088
|
+
const rels = (relPaths ?? []).map((x) => (x ?? "").trim().replace(/^[/\\]+/, ""));
|
|
13089
|
+
if (rels.length === 0) throw new Error("\u8BF7\u9009\u62E9\u8981\u538B\u7F29\u7684\u6587\u4EF6\u6216\u76EE\u5F55");
|
|
13090
|
+
if (rels.some((r) => !r)) throw new Error("\u4E0D\u80FD\u538B\u7F29\u9879\u76EE\u6839\u76EE\u5F55");
|
|
13091
|
+
const targets = rels.map((rel) => ({ rel, abs: resolveWithin(base, rel) }));
|
|
13092
|
+
const single = targets.length === 1 ? targets[0].abs : "";
|
|
13093
|
+
const items = [];
|
|
13094
|
+
let total = 0;
|
|
13095
|
+
const overLimit = () => new Error(
|
|
13096
|
+
`\u5171 ${formatBytes(total)}\uFF0C\u8D85\u8FC7 ${formatBytes(maxBytes)} \u4E0A\u9650\uFF0C\u4E0D\u652F\u6301\u538B\u7F29\u4E0B\u8F7D`
|
|
13097
|
+
);
|
|
13098
|
+
const walk = (absDir) => {
|
|
13099
|
+
let dirents;
|
|
13100
|
+
try {
|
|
13101
|
+
dirents = fs20.readdirSync(absDir, { withFileTypes: true });
|
|
13102
|
+
} catch {
|
|
13103
|
+
return 0;
|
|
13104
|
+
}
|
|
13105
|
+
dirents.sort((a, b) => a.name.localeCompare(b.name));
|
|
13106
|
+
let files = 0;
|
|
13107
|
+
for (const d of dirents) {
|
|
13108
|
+
if (d.isSymbolicLink()) continue;
|
|
13109
|
+
const child = path24.join(absDir, d.name);
|
|
13110
|
+
if (d.isDirectory()) {
|
|
13111
|
+
files += walk(child);
|
|
13112
|
+
continue;
|
|
13113
|
+
}
|
|
13114
|
+
if (!d.isFile()) continue;
|
|
13115
|
+
let st;
|
|
13116
|
+
try {
|
|
13117
|
+
st = fs20.statSync(child);
|
|
13118
|
+
} catch {
|
|
13119
|
+
continue;
|
|
13120
|
+
}
|
|
13121
|
+
total += st.size;
|
|
13122
|
+
if (total > maxBytes) throw overLimit();
|
|
13123
|
+
if (items.length >= MAX_ZIP_ENTRIES) {
|
|
13124
|
+
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`);
|
|
13125
|
+
}
|
|
13126
|
+
items.push({ abs: child, dir: false, size: st.size, mtime: st.mtime });
|
|
13127
|
+
files++;
|
|
13128
|
+
}
|
|
13129
|
+
if (files === 0) {
|
|
13130
|
+
let mtime = /* @__PURE__ */ new Date();
|
|
13131
|
+
try {
|
|
13132
|
+
mtime = fs20.statSync(absDir).mtime;
|
|
13133
|
+
} catch {
|
|
13134
|
+
}
|
|
13135
|
+
items.push({ abs: absDir, dir: true, size: 0, mtime });
|
|
13136
|
+
}
|
|
13137
|
+
return files;
|
|
13138
|
+
};
|
|
13139
|
+
for (const t of targets) {
|
|
13140
|
+
let st;
|
|
13141
|
+
try {
|
|
13142
|
+
st = fs20.statSync(t.abs);
|
|
13143
|
+
} catch {
|
|
13144
|
+
throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${t.rel}`);
|
|
13145
|
+
}
|
|
13146
|
+
if (st.isDirectory()) {
|
|
13147
|
+
walk(t.abs);
|
|
13148
|
+
continue;
|
|
13149
|
+
}
|
|
13150
|
+
total += st.size;
|
|
13151
|
+
if (total > maxBytes) throw overLimit();
|
|
13152
|
+
items.push({ abs: t.abs, dir: false, size: st.size, mtime: st.mtime });
|
|
13153
|
+
}
|
|
13154
|
+
const nameOf = (abs) => {
|
|
13155
|
+
const rel = toPosix(path24.relative(single || base, abs));
|
|
13156
|
+
return rel || path24.posix.basename(toPosix(abs));
|
|
13157
|
+
};
|
|
13158
|
+
const zipEntries = items.map((it) => {
|
|
13159
|
+
const name2 = nameOf(it.abs);
|
|
13160
|
+
if (it.dir) return { name: `${name2}/`, mtime: it.mtime };
|
|
13161
|
+
return { name: name2, data: fs20.readFileSync(it.abs), mtime: it.mtime };
|
|
13162
|
+
});
|
|
13163
|
+
const buf = buildZip(zipEntries);
|
|
13164
|
+
const first = targets[0].rel;
|
|
13165
|
+
const name = single ? `${path24.posix.basename(toPosix(first))}.zip` : "archive.zip";
|
|
13166
|
+
return {
|
|
13167
|
+
name,
|
|
13168
|
+
size: buf.length,
|
|
13169
|
+
total,
|
|
13170
|
+
entries: zipEntries.length,
|
|
13171
|
+
base64: buf.toString("base64")
|
|
13172
|
+
};
|
|
13173
|
+
}
|
|
11987
13174
|
|
|
11988
13175
|
// src/server/projectOps.ts
|
|
11989
13176
|
init_esm_shims();
|
|
@@ -12313,6 +13500,7 @@ function projectStats(manager, tenantId) {
|
|
|
12313
13500
|
|
|
12314
13501
|
// src/server/nodeHost.ts
|
|
12315
13502
|
init_esm_shims();
|
|
13503
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
12316
13504
|
import * as fs21 from "fs";
|
|
12317
13505
|
import * as os6 from "os";
|
|
12318
13506
|
function diskInfo(pathStr) {
|
|
@@ -12327,6 +13515,64 @@ function diskInfo(pathStr) {
|
|
|
12327
13515
|
return null;
|
|
12328
13516
|
}
|
|
12329
13517
|
}
|
|
13518
|
+
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)$/;
|
|
13519
|
+
function isInternalMount(mount) {
|
|
13520
|
+
if (mount === "/") return false;
|
|
13521
|
+
const prefixes = ["/proc", "/sys", "/run", "/snap"];
|
|
13522
|
+
if (process.platform === "darwin") prefixes.push("/System/Volumes");
|
|
13523
|
+
return prefixes.some((p) => mount === p || mount.startsWith(`${p}/`));
|
|
13524
|
+
}
|
|
13525
|
+
function readOnlyVolumes() {
|
|
13526
|
+
const out = /* @__PURE__ */ new Set();
|
|
13527
|
+
const res = spawnSync2("mount", [], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
13528
|
+
if (res.error || res.status !== 0 || !res.stdout) return out;
|
|
13529
|
+
for (const line of res.stdout.split("\n")) {
|
|
13530
|
+
const m = line.match(/^.+? on (.+?) \(([^)]*)\)$/);
|
|
13531
|
+
if (!m || m[1] === "/") continue;
|
|
13532
|
+
if (m[2].split(",").some((opt) => opt.trim() === "read-only")) out.add(m[1]);
|
|
13533
|
+
}
|
|
13534
|
+
return out;
|
|
13535
|
+
}
|
|
13536
|
+
function diskList(workspacePath) {
|
|
13537
|
+
const out = [];
|
|
13538
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13539
|
+
const push = (path37, total, free) => {
|
|
13540
|
+
if (!path37 || seen.has(path37) || !Number.isFinite(total) || total <= 0) return;
|
|
13541
|
+
seen.add(path37);
|
|
13542
|
+
out.push({ path: path37, total, free });
|
|
13543
|
+
};
|
|
13544
|
+
if (process.platform === "win32") {
|
|
13545
|
+
for (const letter of "CDEFGHIJKLMNOPQRSTUVWXYZ") {
|
|
13546
|
+
const root = `${letter}:\\`;
|
|
13547
|
+
const d = diskInfo(root);
|
|
13548
|
+
if (d) push(root, d.total, d.free);
|
|
13549
|
+
}
|
|
13550
|
+
} else {
|
|
13551
|
+
const readonly = process.platform === "darwin" ? readOnlyVolumes() : /* @__PURE__ */ new Set();
|
|
13552
|
+
const res = spawnSync2("df", ["-kP"], {
|
|
13553
|
+
encoding: "utf-8",
|
|
13554
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
13555
|
+
});
|
|
13556
|
+
if (!res.error && res.status === 0 && res.stdout) {
|
|
13557
|
+
for (const line of res.stdout.split("\n").slice(1)) {
|
|
13558
|
+
const m = line.trim().match(/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\d+%\s+(.+)$/);
|
|
13559
|
+
if (!m || PSEUDO_FS.test(m[1])) continue;
|
|
13560
|
+
if (/^\/dev\/loop\d+$/.test(m[1])) continue;
|
|
13561
|
+
if (isInternalMount(m[5]) || readonly.has(m[5])) continue;
|
|
13562
|
+
push(m[5], Number(m[2]) * 1024, Number(m[4]) * 1024);
|
|
13563
|
+
}
|
|
13564
|
+
}
|
|
13565
|
+
}
|
|
13566
|
+
const primary = diskInfo(workspacePath);
|
|
13567
|
+
if (primary) {
|
|
13568
|
+
const covered = out.some((d) => {
|
|
13569
|
+
const root = d.path.endsWith("/") || d.path.endsWith("\\") ? d.path : `${d.path}/`;
|
|
13570
|
+
return workspacePath === d.path || workspacePath.startsWith(root);
|
|
13571
|
+
});
|
|
13572
|
+
if (!covered) push(primary.path, primary.total, primary.free);
|
|
13573
|
+
}
|
|
13574
|
+
return out.sort((a, b) => b.total - a.total);
|
|
13575
|
+
}
|
|
12330
13576
|
function networkInfo() {
|
|
12331
13577
|
const out = [];
|
|
12332
13578
|
const ifaces = os6.networkInterfaces();
|
|
@@ -12382,6 +13628,7 @@ function overview(store, manager) {
|
|
|
12382
13628
|
function buildNodeHost(store, manager, diskPath) {
|
|
12383
13629
|
const cpus2 = os6.cpus();
|
|
12384
13630
|
const ov = overview(store, manager);
|
|
13631
|
+
const disks = diskList(diskPath || process.cwd());
|
|
12385
13632
|
return {
|
|
12386
13633
|
nodeId: store.loadNodeId() ?? "",
|
|
12387
13634
|
hostname: os6.hostname(),
|
|
@@ -12389,7 +13636,7 @@ function buildNodeHost(store, manager, diskPath) {
|
|
|
12389
13636
|
arch: process.arch,
|
|
12390
13637
|
release: os6.release(),
|
|
12391
13638
|
uptimeSec: Math.round(os6.uptime()),
|
|
12392
|
-
version: "0.2.
|
|
13639
|
+
version: "0.2.20",
|
|
12393
13640
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
12394
13641
|
cpu: {
|
|
12395
13642
|
count: cpus2.length,
|
|
@@ -12399,6 +13646,7 @@ function buildNodeHost(store, manager, diskPath) {
|
|
|
12399
13646
|
memory: { total: os6.totalmem(), free: os6.freemem() },
|
|
12400
13647
|
network: networkInfo(),
|
|
12401
13648
|
disk: diskInfo(diskPath || process.cwd()),
|
|
13649
|
+
disks,
|
|
12402
13650
|
...ov
|
|
12403
13651
|
};
|
|
12404
13652
|
}
|
|
@@ -12532,6 +13780,7 @@ function parseApprovalDecision(raw) {
|
|
|
12532
13780
|
}
|
|
12533
13781
|
function createApp(store, manager, opts = {}) {
|
|
12534
13782
|
setupServerHandlers(manager);
|
|
13783
|
+
const updater = opts.updater ?? null;
|
|
12535
13784
|
const routes = [
|
|
12536
13785
|
// ---------- 会话管理 ----------
|
|
12537
13786
|
{
|
|
@@ -12673,6 +13922,18 @@ function createApp(store, manager, opts = {}) {
|
|
|
12673
13922
|
ok(res, { accepted: true });
|
|
12674
13923
|
}
|
|
12675
13924
|
},
|
|
13925
|
+
{
|
|
13926
|
+
method: "POST",
|
|
13927
|
+
pattern: ["sessions", ":id", "steer", "remove"],
|
|
13928
|
+
// 撤销未注入的补充信息
|
|
13929
|
+
fn: (_req, res, { tenant, params, body }) => {
|
|
13930
|
+
const index = Number(body?.index);
|
|
13931
|
+
if (!Number.isInteger(index))
|
|
13932
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
13933
|
+
const pendingSteers = manager.unsteer(tenant, params["id"], index);
|
|
13934
|
+
ok(res, { accepted: true, pendingSteers });
|
|
13935
|
+
}
|
|
13936
|
+
},
|
|
12676
13937
|
{
|
|
12677
13938
|
method: "POST",
|
|
12678
13939
|
pattern: ["sessions", ":id", "interrupt"],
|
|
@@ -13203,6 +14464,30 @@ function createApp(store, manager, opts = {}) {
|
|
|
13203
14464
|
}
|
|
13204
14465
|
}
|
|
13205
14466
|
},
|
|
14467
|
+
{
|
|
14468
|
+
// 二进制写入(图片编辑保存):body = { path, base64 }
|
|
14469
|
+
method: "POST",
|
|
14470
|
+
pattern: ["files", "binary"],
|
|
14471
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14472
|
+
const raw = body ?? {};
|
|
14473
|
+
try {
|
|
14474
|
+
ok(
|
|
14475
|
+
res,
|
|
14476
|
+
writeProjectFileBase64(
|
|
14477
|
+
tenant.workspaceRoot ?? null,
|
|
14478
|
+
String(raw.path ?? ""),
|
|
14479
|
+
String(raw.base64 ?? "")
|
|
14480
|
+
)
|
|
14481
|
+
);
|
|
14482
|
+
} catch (e) {
|
|
14483
|
+
throw new ApiError(
|
|
14484
|
+
400,
|
|
14485
|
+
"bad_request",
|
|
14486
|
+
e instanceof Error ? e.message : String(e)
|
|
14487
|
+
);
|
|
14488
|
+
}
|
|
14489
|
+
}
|
|
14490
|
+
},
|
|
13206
14491
|
// ---------- 项目终端(在工作区执行命令) ----------
|
|
13207
14492
|
{
|
|
13208
14493
|
method: "POST",
|
|
@@ -13553,6 +14838,21 @@ function createApp(store, manager, opts = {}) {
|
|
|
13553
14838
|
}
|
|
13554
14839
|
}
|
|
13555
14840
|
},
|
|
14841
|
+
{
|
|
14842
|
+
method: "POST",
|
|
14843
|
+
pattern: ["files", "delete"],
|
|
14844
|
+
// 删除文件 / 目录(目录递归);逐条返回失败原因
|
|
14845
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14846
|
+
const raw = body ?? {};
|
|
14847
|
+
const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
|
|
14848
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
14849
|
+
try {
|
|
14850
|
+
ok(res, deleteProjectPaths(tenant.workspaceRoot ?? null, paths));
|
|
14851
|
+
} catch (e) {
|
|
14852
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
14853
|
+
}
|
|
14854
|
+
}
|
|
14855
|
+
},
|
|
13556
14856
|
{
|
|
13557
14857
|
method: "POST",
|
|
13558
14858
|
pattern: ["files", "create"],
|
|
@@ -13572,6 +14872,41 @@ function createApp(store, manager, opts = {}) {
|
|
|
13572
14872
|
}
|
|
13573
14873
|
}
|
|
13574
14874
|
},
|
|
14875
|
+
{
|
|
14876
|
+
method: "POST",
|
|
14877
|
+
pattern: ["files", "rename"],
|
|
14878
|
+
// 重命名(只改同目录下的名字)
|
|
14879
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14880
|
+
const raw = body ?? {};
|
|
14881
|
+
try {
|
|
14882
|
+
ok(
|
|
14883
|
+
res,
|
|
14884
|
+
renameProjectPath(
|
|
14885
|
+
tenant.workspaceRoot ?? null,
|
|
14886
|
+
String(raw.path ?? ""),
|
|
14887
|
+
String(raw.name ?? "")
|
|
14888
|
+
)
|
|
14889
|
+
);
|
|
14890
|
+
} catch (e) {
|
|
14891
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
14892
|
+
}
|
|
14893
|
+
}
|
|
14894
|
+
},
|
|
14895
|
+
{
|
|
14896
|
+
method: "POST",
|
|
14897
|
+
pattern: ["files", "zip"],
|
|
14898
|
+
// 压缩下载(多选一起打包;超上限由实现拒绝)
|
|
14899
|
+
fn: (_req, res, { tenant, body }) => {
|
|
14900
|
+
const raw = body ?? {};
|
|
14901
|
+
const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
|
|
14902
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
14903
|
+
try {
|
|
14904
|
+
ok(res, zipProjectPaths(tenant.workspaceRoot ?? null, paths));
|
|
14905
|
+
} catch (e) {
|
|
14906
|
+
throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
|
|
14907
|
+
}
|
|
14908
|
+
}
|
|
14909
|
+
},
|
|
13575
14910
|
// ---------- 项目(= 租户)管理:控制台多项目视图 ----------
|
|
13576
14911
|
// 仅管理员密钥可用;项目级与会话级接口仍用项目自己的 tenant API Key
|
|
13577
14912
|
// (或管理员密钥 + X-Project-Id 头),互不暴露明文密钥。
|
|
@@ -13630,6 +14965,29 @@ function createApp(store, manager, opts = {}) {
|
|
|
13630
14965
|
ok(res, nodeLlmView(next));
|
|
13631
14966
|
}
|
|
13632
14967
|
},
|
|
14968
|
+
// 大模型账户余额(控制台「余额不足 → 查看余额并充值」用;由节点向 Hub 代查)
|
|
14969
|
+
{
|
|
14970
|
+
method: "GET",
|
|
14971
|
+
pattern: ["node", "llm", "balance"],
|
|
14972
|
+
adminOnly: true,
|
|
14973
|
+
fn: async (_req, res) => {
|
|
14974
|
+
ok(res, await fetchLlmBalance(store));
|
|
14975
|
+
}
|
|
14976
|
+
},
|
|
14977
|
+
// 重新拉取平台内置模型凭据(用户点了「刷新内置模型」,或 Key 在控制台被重置)
|
|
14978
|
+
{
|
|
14979
|
+
method: "POST",
|
|
14980
|
+
pattern: ["node", "llm", "builtin-refresh"],
|
|
14981
|
+
adminOnly: true,
|
|
14982
|
+
fn: async (_req, res) => {
|
|
14983
|
+
const r = await ensureBuiltinProvider(store, { force: true });
|
|
14984
|
+
if (r.status !== "ready") {
|
|
14985
|
+
ok(res, { ok: false, reason: r.reason });
|
|
14986
|
+
return;
|
|
14987
|
+
}
|
|
14988
|
+
ok(res, { ok: true, view: nodeLlmView(store.loadNodeLlm()) });
|
|
14989
|
+
}
|
|
14990
|
+
},
|
|
13633
14991
|
// ---------- 用量(账本 + 汇总 + 价格) ----------
|
|
13634
14992
|
{
|
|
13635
14993
|
method: "GET",
|
|
@@ -13781,6 +15139,34 @@ function createApp(store, manager, opts = {}) {
|
|
|
13781
15139
|
}
|
|
13782
15140
|
}
|
|
13783
15141
|
},
|
|
15142
|
+
// ---------- 节点自更新(状态 / 检查 / 安装) ----------
|
|
15143
|
+
// action=install 先回执、后台安装并退出进程(由服务管理器拉起新版本),
|
|
15144
|
+
// 因此回执成功只代表「已受理」,是否升级成功要用 action=status 再确认。
|
|
15145
|
+
{
|
|
15146
|
+
method: "POST",
|
|
15147
|
+
pattern: ["node", "self-update"],
|
|
15148
|
+
projectOptional: true,
|
|
15149
|
+
fn: async (req2, res, { body }) => {
|
|
15150
|
+
const b = body ?? {};
|
|
15151
|
+
const action = String(b.action ?? "status");
|
|
15152
|
+
if (!updater) {
|
|
15153
|
+
throw new ApiError(400, "bad_request", "\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A\u8282\u70B9\u672A\u4EE5 serve \u6A21\u5F0F\u8FD0\u884C");
|
|
15154
|
+
}
|
|
15155
|
+
if (action === "status") {
|
|
15156
|
+
ok(res, updater.status());
|
|
15157
|
+
return;
|
|
15158
|
+
}
|
|
15159
|
+
if (action === "check") {
|
|
15160
|
+
ok(res, await updater.inspect());
|
|
15161
|
+
return;
|
|
15162
|
+
}
|
|
15163
|
+
if (action === "install") {
|
|
15164
|
+
ok(res, await updater.requestInstall(b.force === true));
|
|
15165
|
+
return;
|
|
15166
|
+
}
|
|
15167
|
+
throw new ApiError(400, "bad_request", `\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
15168
|
+
}
|
|
15169
|
+
},
|
|
13784
15170
|
// ---------- 节点级会话状态(跨项目;只含标识与 status) ----------
|
|
13785
15171
|
// 供节点首页 / 工作台建立初始视图,后续变化由 session_status 通知增量推送。
|
|
13786
15172
|
{
|
|
@@ -13943,7 +15329,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
13943
15329
|
// 阻止反向代理(nginx 等)缓冲 SSE 流
|
|
13944
15330
|
"X-Accel-Buffering": "no"
|
|
13945
15331
|
});
|
|
13946
|
-
const
|
|
15332
|
+
const sinceRaw2 = new URL(
|
|
13947
15333
|
req2.url,
|
|
13948
15334
|
"http://localhost"
|
|
13949
15335
|
).searchParams.get("since");
|
|
@@ -13951,19 +15337,51 @@ function createApp(store, manager, opts = {}) {
|
|
|
13951
15337
|
tenant.id,
|
|
13952
15338
|
params["id"],
|
|
13953
15339
|
res,
|
|
13954
|
-
|
|
15340
|
+
sinceRaw2 ? Number(sinceRaw2) : 0
|
|
13955
15341
|
);
|
|
13956
15342
|
return;
|
|
13957
15343
|
}
|
|
13958
|
-
const
|
|
13959
|
-
|
|
15344
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
15345
|
+
const sinceRaw = q.get("since");
|
|
15346
|
+
const beforeRaw = q.get("before");
|
|
15347
|
+
const maxBytesRaw = q.get("maxBytes");
|
|
15348
|
+
ok(
|
|
15349
|
+
res,
|
|
15350
|
+
manager.eventsPage(tenant.id, params["id"], {
|
|
15351
|
+
since: sinceRaw === null ? void 0 : Number(sinceRaw) || 0,
|
|
15352
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
15353
|
+
maxBytes: maxBytesRaw === null ? void 0 : Number(maxBytesRaw) || void 0
|
|
15354
|
+
})
|
|
13960
15355
|
);
|
|
13961
|
-
|
|
13962
|
-
|
|
13963
|
-
|
|
13964
|
-
|
|
15356
|
+
}
|
|
15357
|
+
},
|
|
15358
|
+
{
|
|
15359
|
+
method: "GET",
|
|
15360
|
+
pattern: ["sessions", ":id", "turns"],
|
|
15361
|
+
// 轮次汇总(会话详情的列表)
|
|
15362
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
15363
|
+
const q = new URL(req2.url, "http://localhost").searchParams;
|
|
15364
|
+
const beforeRaw = q.get("before");
|
|
15365
|
+
const limitRaw = q.get("limit");
|
|
15366
|
+
ok(
|
|
15367
|
+
res,
|
|
15368
|
+
await manager.sessionTurns(tenant.id, params["id"], {
|
|
15369
|
+
before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
|
|
15370
|
+
limit: limitRaw === null ? void 0 : Number(limitRaw) || void 0
|
|
15371
|
+
})
|
|
13965
15372
|
);
|
|
13966
|
-
|
|
15373
|
+
}
|
|
15374
|
+
},
|
|
15375
|
+
{
|
|
15376
|
+
method: "GET",
|
|
15377
|
+
// 单轮详情:用户消息(含补充) + 最终回复 + 产出文件
|
|
15378
|
+
pattern: ["sessions", ":id", "turns", ":seq", "reply"],
|
|
15379
|
+
fn: async (req2, res, { tenant, params }) => {
|
|
15380
|
+
const startSeq = Number(params["seq"]);
|
|
15381
|
+
if (!Number.isFinite(startSeq) || startSeq <= 0) {
|
|
15382
|
+
throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
15383
|
+
}
|
|
15384
|
+
ok(res, await manager.sessionTurnReply(tenant.id, params["id"], startSeq));
|
|
13967
15385
|
}
|
|
13968
15386
|
}
|
|
13969
15387
|
];
|
|
@@ -13996,7 +15414,7 @@ function createApp(store, manager, opts = {}) {
|
|
|
13996
15414
|
json(res, 200, {
|
|
13997
15415
|
status: "ok",
|
|
13998
15416
|
// 版本让运维/探针能确认线上跑的是哪个构建(更新与回滚据此判断)
|
|
13999
|
-
version: "0.2.
|
|
15417
|
+
version: "0.2.20",
|
|
14000
15418
|
features: { mcp: hasServerMcpConfig() }
|
|
14001
15419
|
});
|
|
14002
15420
|
return;
|
|
@@ -14269,6 +15687,24 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14269
15687
|
switch (env3.type) {
|
|
14270
15688
|
case "ping":
|
|
14271
15689
|
return ok2({ pong: true, ts: Date.now() });
|
|
15690
|
+
case "self_update": {
|
|
15691
|
+
const updater = deps.getUpdater?.() ?? null;
|
|
15692
|
+
if (!updater) {
|
|
15693
|
+
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");
|
|
15694
|
+
}
|
|
15695
|
+
const action = String(payload.action ?? "status");
|
|
15696
|
+
if (action === "status") {
|
|
15697
|
+
return ok2(updater.status());
|
|
15698
|
+
}
|
|
15699
|
+
if (action === "check") {
|
|
15700
|
+
return ok2(await updater.inspect());
|
|
15701
|
+
}
|
|
15702
|
+
if (action === "install") {
|
|
15703
|
+
const started = await updater.requestInstall(payload.force === true);
|
|
15704
|
+
return ok2(started);
|
|
15705
|
+
}
|
|
15706
|
+
return fail(`\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
|
|
15707
|
+
}
|
|
14272
15708
|
case "get_node_info":
|
|
14273
15709
|
return ok2(
|
|
14274
15710
|
buildNodeInfo(store, {
|
|
@@ -14318,6 +15754,22 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14318
15754
|
store.saveNodeLlm(next);
|
|
14319
15755
|
return ok2(nodeLlmView(next));
|
|
14320
15756
|
}
|
|
15757
|
+
/*
|
|
15758
|
+
* 重新拉取平台内置模型(志前 API)。
|
|
15759
|
+
*
|
|
15760
|
+
* 用途:用户在控制台点了「刷新」、或内置模型的 Key 在控制台被重置后需要换新。
|
|
15761
|
+
* 走写链串行——它会改写 connections.json,不能与 update_node_llm 并发。
|
|
15762
|
+
*/
|
|
15763
|
+
// 余额查询:只读、幂等,可进只读并发池
|
|
15764
|
+
case "get_llm_balance":
|
|
15765
|
+
return ok2(await fetchLlmBalance(store));
|
|
15766
|
+
case "refresh_builtin_llm": {
|
|
15767
|
+
const r = await ensureBuiltinProvider(store, { force: true });
|
|
15768
|
+
if (r.status !== "ready") {
|
|
15769
|
+
return ok2({ ok: false, reason: r.reason });
|
|
15770
|
+
}
|
|
15771
|
+
return ok2({ ok: true, view: nodeLlmView(store.loadNodeLlm()) });
|
|
15772
|
+
}
|
|
14321
15773
|
case "get_usage_summary": {
|
|
14322
15774
|
const ledger = manager.usageLedger();
|
|
14323
15775
|
if (!ledger) return ok2({ total: null, groups: [] });
|
|
@@ -14391,8 +15843,22 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14391
15843
|
);
|
|
14392
15844
|
}
|
|
14393
15845
|
case "get_events": {
|
|
14394
|
-
const since =
|
|
14395
|
-
|
|
15846
|
+
const since = payload.since === void 0 ? void 0 : Number(payload.since) || 0;
|
|
15847
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
15848
|
+
const maxBytes = Number(payload.maxBytes) || void 0;
|
|
15849
|
+
return ok2(
|
|
15850
|
+
manager.eventsPage(project().id, sessionId(), { since, before, maxBytes })
|
|
15851
|
+
);
|
|
15852
|
+
}
|
|
15853
|
+
case "get_session_turns": {
|
|
15854
|
+
const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
|
|
15855
|
+
const limit = Number(payload.limit) || void 0;
|
|
15856
|
+
return ok2(await manager.sessionTurns(project().id, sessionId(), { before, limit }));
|
|
15857
|
+
}
|
|
15858
|
+
case "get_session_turn_reply": {
|
|
15859
|
+
const startSeq = Number(payload.startSeq) || 0;
|
|
15860
|
+
if (startSeq <= 0) throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
|
|
15861
|
+
return ok2(await manager.sessionTurnReply(project().id, sessionId(), startSeq));
|
|
14396
15862
|
}
|
|
14397
15863
|
case "get_config":
|
|
14398
15864
|
return ok2(configView2(project()));
|
|
@@ -14453,6 +15919,16 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14453
15919
|
)
|
|
14454
15920
|
);
|
|
14455
15921
|
}
|
|
15922
|
+
case "write_file_base64": {
|
|
15923
|
+
const t = project();
|
|
15924
|
+
return ok2(
|
|
15925
|
+
writeProjectFileBase64(
|
|
15926
|
+
t.workspaceRoot ?? null,
|
|
15927
|
+
String(payload.path ?? ""),
|
|
15928
|
+
String(payload.base64 ?? "")
|
|
15929
|
+
)
|
|
15930
|
+
);
|
|
15931
|
+
}
|
|
14456
15932
|
case "create_dir": {
|
|
14457
15933
|
const t = project();
|
|
14458
15934
|
return ok2(createProjectDir(t.workspaceRoot ?? null, String(payload.path ?? "")));
|
|
@@ -14467,10 +15943,35 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14467
15943
|
)
|
|
14468
15944
|
);
|
|
14469
15945
|
}
|
|
15946
|
+
case "delete_files": {
|
|
15947
|
+
const t = project();
|
|
15948
|
+
const raw = payload.paths;
|
|
15949
|
+
const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
|
|
15950
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
15951
|
+
return ok2(deleteProjectPaths(t.workspaceRoot ?? null, paths));
|
|
15952
|
+
}
|
|
14470
15953
|
case "search_files": {
|
|
14471
15954
|
const t = project();
|
|
14472
15955
|
return ok2(searchProjectFiles(t.workspaceRoot ?? null, String(payload.query ?? "")));
|
|
14473
15956
|
}
|
|
15957
|
+
case "rename_path": {
|
|
15958
|
+
const t = project();
|
|
15959
|
+
return ok2(
|
|
15960
|
+
renameProjectPath(
|
|
15961
|
+
t.workspaceRoot ?? null,
|
|
15962
|
+
String(payload.path ?? ""),
|
|
15963
|
+
String(payload.name ?? "")
|
|
15964
|
+
)
|
|
15965
|
+
);
|
|
15966
|
+
}
|
|
15967
|
+
case "zip_download": {
|
|
15968
|
+
const t = project();
|
|
15969
|
+
const raw = payload.paths;
|
|
15970
|
+
const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
|
|
15971
|
+
if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
|
|
15972
|
+
const maxBytes = typeof payload.maxBytes === "number" ? payload.maxBytes : void 0;
|
|
15973
|
+
return ok2(zipProjectPaths(t.workspaceRoot ?? null, paths, { maxBytes }));
|
|
15974
|
+
}
|
|
14474
15975
|
case "list_tools": {
|
|
14475
15976
|
const t = project();
|
|
14476
15977
|
return ok2(buildToolCatalog(t.workspaceRoot ?? process.cwd()));
|
|
@@ -14693,6 +16194,13 @@ async function dispatchFleetCommand(deps, env3) {
|
|
|
14693
16194
|
manager.steer(project(), sessionId(), text);
|
|
14694
16195
|
return ok2({ accepted: true });
|
|
14695
16196
|
}
|
|
16197
|
+
case "unsteer": {
|
|
16198
|
+
const index = Number(payload.index);
|
|
16199
|
+
if (!Number.isInteger(index)) {
|
|
16200
|
+
throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
|
|
16201
|
+
}
|
|
16202
|
+
return ok2({ accepted: true, pendingSteers: manager.unsteer(project(), sessionId(), index) });
|
|
16203
|
+
}
|
|
14696
16204
|
case "interrupt":
|
|
14697
16205
|
manager.interrupt(project(), sessionId());
|
|
14698
16206
|
return ok2({ accepted: true });
|
|
@@ -14816,6 +16324,22 @@ function toWireAck(ack) {
|
|
|
14816
16324
|
data: ack.data === void 0 ? void 0 : utf8Encode(JSON.stringify(ack.data))
|
|
14817
16325
|
};
|
|
14818
16326
|
}
|
|
16327
|
+
var ACK_JSON_MAX_BYTES = MAX_FRAME_BYTES - 64 * 1024;
|
|
16328
|
+
function mb(bytes) {
|
|
16329
|
+
return (bytes / (1024 * 1024)).toFixed(1);
|
|
16330
|
+
}
|
|
16331
|
+
function guardAckSize(ack, json2) {
|
|
16332
|
+
if (!ack.ok || ack.data === void 0) return ack;
|
|
16333
|
+
const bytes = Buffer.byteLength(json2, "utf8");
|
|
16334
|
+
if (bytes <= ACK_JSON_MAX_BYTES) return ack;
|
|
16335
|
+
return {
|
|
16336
|
+
reqId: ack.reqId,
|
|
16337
|
+
type: ack.type,
|
|
16338
|
+
ok: false,
|
|
16339
|
+
// 带上实际大小:用户据此能判断「差一点」还是「差很多」,而不是只看到「失败」
|
|
16340
|
+
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`
|
|
16341
|
+
};
|
|
16342
|
+
}
|
|
14819
16343
|
function fromWireCommand(env3) {
|
|
14820
16344
|
let payload = {};
|
|
14821
16345
|
if (env3.payload && env3.payload.length > 0) {
|
|
@@ -15286,6 +16810,7 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
|
|
|
15286
16810
|
"get_node_llm",
|
|
15287
16811
|
"list_node_llm_models",
|
|
15288
16812
|
"get_usage_summary",
|
|
16813
|
+
"get_llm_balance",
|
|
15289
16814
|
"get_usage_records",
|
|
15290
16815
|
"get_usage_pricing",
|
|
15291
16816
|
"list_projects",
|
|
@@ -15293,6 +16818,8 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
|
|
|
15293
16818
|
"get_session_states",
|
|
15294
16819
|
"get_state",
|
|
15295
16820
|
"get_events",
|
|
16821
|
+
"get_session_turns",
|
|
16822
|
+
"get_session_turn_reply",
|
|
15296
16823
|
"get_config",
|
|
15297
16824
|
"list_models",
|
|
15298
16825
|
"list_dirs",
|
|
@@ -15542,6 +17069,10 @@ var FleetNodeBridge = class {
|
|
|
15542
17069
|
_dispatchEnvelope(env3) {
|
|
15543
17070
|
const run = () => this._dispatch(env3);
|
|
15544
17071
|
dbgMark(`recv:${env3.type}`, `reqId=${env3.reqId}`);
|
|
17072
|
+
if (env3.type === "self_update" && String(env3.payload?.action ?? "status") !== "install") {
|
|
17073
|
+
this._runReadonly(run);
|
|
17074
|
+
return;
|
|
17075
|
+
}
|
|
15545
17076
|
if (READONLY_CMD_TYPES.has(env3.type) || this._isReadonlyBatch(env3)) {
|
|
15546
17077
|
this._runReadonly(run);
|
|
15547
17078
|
return;
|
|
@@ -15568,7 +17099,7 @@ var FleetNodeBridge = class {
|
|
|
15568
17099
|
_invalidateSessionCache() {
|
|
15569
17100
|
this.writeEpoch += 1;
|
|
15570
17101
|
for (const key of [...this.cmdResultCache.keys()]) {
|
|
15571
|
-
if (/^(get_state|get_events|list_sessions|get_session_states)\|/.test(key)) {
|
|
17102
|
+
if (/^(get_state|get_events|get_session_turns|get_session_turn_reply|list_sessions|get_session_states)\|/.test(key)) {
|
|
15572
17103
|
this.cmdResultCache.delete(key);
|
|
15573
17104
|
}
|
|
15574
17105
|
}
|
|
@@ -15704,11 +17235,17 @@ var FleetNodeBridge = class {
|
|
|
15704
17235
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
15705
17236
|
platform: process.platform,
|
|
15706
17237
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15707
|
-
version: "0.2.
|
|
17238
|
+
version: "0.2.20",
|
|
15708
17239
|
ip: primaryIp()
|
|
15709
17240
|
};
|
|
15710
17241
|
}
|
|
15711
|
-
/**
|
|
17242
|
+
/**
|
|
17243
|
+
* 用量汇总(供控制面账户级聚合):总量 + 最近 40 天按天 + 最近 40 天按项目。
|
|
17244
|
+
*
|
|
17245
|
+
* 按项目这一维是为控制台「今日 / 最近 30 天消耗拆到每个项目」服务的:
|
|
17246
|
+
* 账本里的汇总只存了按天、按项目两个独立维度,交叉项只能现算,因此这里
|
|
17247
|
+
* 从明细聚合后随 info 一起上报(Hub 按 (节点, 项目, 天) 覆盖写入,天然幂等)。
|
|
17248
|
+
*/
|
|
15712
17249
|
_usagePayload() {
|
|
15713
17250
|
const ledger = this.opts.deps.manager.usageLedger();
|
|
15714
17251
|
if (!ledger) return void 0;
|
|
@@ -15716,11 +17253,22 @@ var FleetNodeBridge = class {
|
|
|
15716
17253
|
const days = {};
|
|
15717
17254
|
const keys = Object.keys(r.byDay).sort().slice(-40);
|
|
15718
17255
|
for (const k of keys) days[k] = r.byDay[k];
|
|
17256
|
+
const from = keys[0] ?? "";
|
|
17257
|
+
const to = keys[keys.length - 1] ?? "";
|
|
17258
|
+
const projects = {};
|
|
17259
|
+
if (from && to) {
|
|
17260
|
+
for (const [projectId, byDay] of Object.entries(
|
|
17261
|
+
ledger.projectByDay({ from, to })
|
|
17262
|
+
)) {
|
|
17263
|
+
projects[projectId] = { total: sumAgg(Object.values(byDay)), byDay };
|
|
17264
|
+
}
|
|
17265
|
+
}
|
|
15719
17266
|
return {
|
|
15720
17267
|
since: r.since,
|
|
15721
17268
|
total: r.total,
|
|
15722
17269
|
legacy: r.legacy ?? null,
|
|
15723
|
-
byDay: days
|
|
17270
|
+
byDay: days,
|
|
17271
|
+
projects
|
|
15724
17272
|
};
|
|
15725
17273
|
}
|
|
15726
17274
|
/**
|
|
@@ -15755,6 +17303,8 @@ var FleetNodeBridge = class {
|
|
|
15755
17303
|
name: s.name ?? null,
|
|
15756
17304
|
status: s.status,
|
|
15757
17305
|
lastUserMessage: lastUserMessage(s),
|
|
17306
|
+
// 控制面按它排「最近在跑什么」,运行中的长任务才不会被排到后面
|
|
17307
|
+
lastStartedAt: s.lastStartedAt ?? null,
|
|
15758
17308
|
// 控制面的聚合邮件靠它判断「这台机器最近有没有在干活」;
|
|
15759
17309
|
// 状态跃迁可能被 info 节流跳过,完成时间不会。
|
|
15760
17310
|
lastCompletedAt: s.lastCompletedAt ?? null
|
|
@@ -15783,7 +17333,7 @@ var FleetNodeBridge = class {
|
|
|
15783
17333
|
label: this.opts.deps.store.loadNodeLabel() ?? void 0,
|
|
15784
17334
|
platform: process.platform,
|
|
15785
17335
|
mode: process.env.NODE_MODE ?? "standalone",
|
|
15786
|
-
version: "0.2.
|
|
17336
|
+
version: "0.2.20",
|
|
15787
17337
|
ip: primaryIp(),
|
|
15788
17338
|
projects,
|
|
15789
17339
|
totals,
|
|
@@ -15945,7 +17495,13 @@ var FleetNodeBridge = class {
|
|
|
15945
17495
|
const stamped = { ...ack, reqId: env3.reqId, type: env3.type };
|
|
15946
17496
|
const json2 = JSON.stringify(stamped);
|
|
15947
17497
|
const __t1 = dbgNow();
|
|
15948
|
-
|
|
17498
|
+
const payload = guardAckSize(stamped, json2);
|
|
17499
|
+
if (payload !== stamped) {
|
|
17500
|
+
console.warn(
|
|
17501
|
+
`[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`
|
|
17502
|
+
);
|
|
17503
|
+
}
|
|
17504
|
+
this._send({ kind: "ack", clientId: env3.clientId, ack: payload });
|
|
15949
17505
|
dbgMs(
|
|
15950
17506
|
"publishAck",
|
|
15951
17507
|
__t0,
|
|
@@ -17269,7 +18825,7 @@ import { createHash as createHash3 } from "crypto";
|
|
|
17269
18825
|
import * as fs24 from "fs";
|
|
17270
18826
|
import { Readable as Readable2 } from "stream";
|
|
17271
18827
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
17272
|
-
import { spawnSync as
|
|
18828
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
17273
18829
|
import * as path29 from "path";
|
|
17274
18830
|
|
|
17275
18831
|
// src/update/manifest.ts
|
|
@@ -17412,7 +18968,7 @@ function verifySignature(data, signatureBase64, publicKey) {
|
|
|
17412
18968
|
}
|
|
17413
18969
|
|
|
17414
18970
|
// src/update/downloader.ts
|
|
17415
|
-
var
|
|
18971
|
+
var MAX_BINARY_BYTES2 = 400 * 1024 * 1024;
|
|
17416
18972
|
var MANIFEST_TIMEOUT_MS = 15e3;
|
|
17417
18973
|
var DOWNLOAD_TIMEOUT_MS2 = 10 * 6e4;
|
|
17418
18974
|
async function fetchManifest(url, publicKey, opts = {}) {
|
|
@@ -17458,7 +19014,7 @@ async function downloadBinary(bin, destPath, log = () => {
|
|
|
17458
19014
|
if (!res.ok) return { error: `\u4E0B\u8F7D\u5931\u8D25 HTTP ${res.status}` };
|
|
17459
19015
|
if (!res.body) return { error: "\u4E0B\u8F7D\u54CD\u5E94\u6CA1\u6709 body" };
|
|
17460
19016
|
const expected = bin.size && bin.size > 0 ? bin.size : void 0;
|
|
17461
|
-
const limit = expected ? Math.max(expected * 2, 1024 * 1024) :
|
|
19017
|
+
const limit = expected ? Math.max(expected * 2, 1024 * 1024) : MAX_BINARY_BYTES2;
|
|
17462
19018
|
const hash = createHash3("sha256");
|
|
17463
19019
|
let bytes = 0;
|
|
17464
19020
|
try {
|
|
@@ -17496,7 +19052,7 @@ async function downloadBinary(bin, destPath, log = () => {
|
|
|
17496
19052
|
}
|
|
17497
19053
|
function selfCheck(binPath, expectedVersion) {
|
|
17498
19054
|
try {
|
|
17499
|
-
const r =
|
|
19055
|
+
const r = spawnSync3(binPath, ["--version"], {
|
|
17500
19056
|
timeout: 2e4,
|
|
17501
19057
|
encoding: "utf-8",
|
|
17502
19058
|
env: { ...process.env, ZQAGENT_AUTO_UPDATE: "0" }
|
|
@@ -17518,10 +19074,10 @@ init_esm_shims();
|
|
|
17518
19074
|
import * as fs25 from "fs";
|
|
17519
19075
|
import * as os9 from "os";
|
|
17520
19076
|
import * as path30 from "path";
|
|
17521
|
-
import { spawnSync as
|
|
19077
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
17522
19078
|
import { createRequire as createRequire3 } from "module";
|
|
17523
19079
|
function currentVersion() {
|
|
17524
|
-
const injected = "0.2.
|
|
19080
|
+
const injected = "0.2.20"?.trim();
|
|
17525
19081
|
if (injected) return injected;
|
|
17526
19082
|
try {
|
|
17527
19083
|
const req2 = createRequire3(import.meta.url);
|
|
@@ -17601,7 +19157,7 @@ function detectServiceManager() {
|
|
|
17601
19157
|
}
|
|
17602
19158
|
function detectLaunchd() {
|
|
17603
19159
|
try {
|
|
17604
|
-
const res =
|
|
19160
|
+
const res = spawnSync4(
|
|
17605
19161
|
"launchctl",
|
|
17606
19162
|
["print", `${launchdDomain()}/${launchdLabel()}`],
|
|
17607
19163
|
{ encoding: "utf8", timeout: 3e3 }
|
|
@@ -17634,7 +19190,7 @@ var cachedServiceEnv = null;
|
|
|
17634
19190
|
function readServiceEnvironment() {
|
|
17635
19191
|
const serviceName = process.env.ZQAGENT_SERVICE_NAME?.trim() || DEFAULT_SERVICE_NAME;
|
|
17636
19192
|
try {
|
|
17637
|
-
const res =
|
|
19193
|
+
const res = spawnSync4(
|
|
17638
19194
|
"systemctl",
|
|
17639
19195
|
["show", serviceName, "--property=Environment", "--value", "--no-pager"],
|
|
17640
19196
|
{ encoding: "utf8", timeout: 3e3 }
|
|
@@ -17835,6 +19391,31 @@ function nextIntervalMs() {
|
|
|
17835
19391
|
const jitter = base * 0.15 * (Math.random() * 2 - 1);
|
|
17836
19392
|
return Math.max(6e4, Math.round(base + jitter));
|
|
17837
19393
|
}
|
|
19394
|
+
function evaluateManifest(base, manifest) {
|
|
19395
|
+
if (!parseVersion(base.currentVersion) || compareVersions(manifest.version, base.currentVersion) <= 0) {
|
|
19396
|
+
return {
|
|
19397
|
+
...base,
|
|
19398
|
+
updateAvailable: false,
|
|
19399
|
+
latestVersion: manifest.version,
|
|
19400
|
+
reason: `\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF08v${base.currentVersion}\uFF09`
|
|
19401
|
+
};
|
|
19402
|
+
}
|
|
19403
|
+
if (manifest.minimumVersion && compareVersions(base.currentVersion, manifest.minimumVersion) < 0) {
|
|
19404
|
+
return {
|
|
19405
|
+
...base,
|
|
19406
|
+
updateAvailable: false,
|
|
19407
|
+
latestVersion: manifest.version,
|
|
19408
|
+
manualUpgradeRequired: manifest.minimumVersion,
|
|
19409
|
+
reason: `\u5F53\u524D\u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u624B\u52A8\u5347\u7EA7\u5230 >= ${manifest.minimumVersion}`
|
|
19410
|
+
};
|
|
19411
|
+
}
|
|
19412
|
+
return {
|
|
19413
|
+
...base,
|
|
19414
|
+
updateAvailable: true,
|
|
19415
|
+
latestVersion: manifest.version,
|
|
19416
|
+
...manifest.notes ? { notes: manifest.notes } : {}
|
|
19417
|
+
};
|
|
19418
|
+
}
|
|
17838
19419
|
var UpdateController = class {
|
|
17839
19420
|
constructor(opts, currentVersion2 = currentVersion()) {
|
|
17840
19421
|
this.opts = opts;
|
|
@@ -17923,6 +19504,77 @@ var UpdateController = class {
|
|
|
17923
19504
|
}
|
|
17924
19505
|
this.schedule(nextIntervalMs());
|
|
17925
19506
|
}
|
|
19507
|
+
/**
|
|
19508
|
+
* 只读检查:拉清单比对版本,不下载、不改 state。
|
|
19509
|
+
*
|
|
19510
|
+
* 供远端 `self_update --action check` 与 UI 展示用;与定时器 `check()` 不同,
|
|
19511
|
+
* 这里不加 `checking` 锁、不写 `lastCheckAt`,因此不会干扰自动更新流程。
|
|
19512
|
+
*/
|
|
19513
|
+
async inspect() {
|
|
19514
|
+
const base = {
|
|
19515
|
+
currentVersion: this.currentVersion,
|
|
19516
|
+
channel: updaterChannel(),
|
|
19517
|
+
manifestUrl: this.manifestUrl()
|
|
19518
|
+
};
|
|
19519
|
+
const key = this.publicKey;
|
|
19520
|
+
const cap = selfUpdateCapability(Boolean(key));
|
|
19521
|
+
if (!cap.ok) {
|
|
19522
|
+
return { ...base, updateAvailable: false, reason: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
19523
|
+
}
|
|
19524
|
+
const res = await fetchManifest(base.manifestUrl, key, {
|
|
19525
|
+
allowInsecureUrl: env("ZQAGENT_UPDATE_INSECURE") === "1"
|
|
19526
|
+
});
|
|
19527
|
+
if (!res.manifest) {
|
|
19528
|
+
return {
|
|
19529
|
+
...base,
|
|
19530
|
+
updateAvailable: false,
|
|
19531
|
+
reason: `\u66F4\u65B0\u68C0\u67E5\u5931\u8D25\uFF1A${res.error ?? "\u6E05\u5355\u6821\u9A8C\u5931\u8D25"}`
|
|
19532
|
+
};
|
|
19533
|
+
}
|
|
19534
|
+
return evaluateManifest(base, res.manifest);
|
|
19535
|
+
}
|
|
19536
|
+
/**
|
|
19537
|
+
* 远端触发安装:先同步给出结论(受理/无需更新),安装本身在后台跑。
|
|
19538
|
+
*
|
|
19539
|
+
* 必须「先回执、后安装」:安装成功后进程会 `exit(70)` 交给 systemd 拉起新版本,
|
|
19540
|
+
* 如果等到安装完成再回执,回执永远发不出去(表现为网关超时)。
|
|
19541
|
+
*
|
|
19542
|
+
* 复用定时器同一条 `check()` 路径:验签、下载、自检、失败计数、安装后自检与
|
|
19543
|
+
* 回滚保护全都一致,只是把 `installMode` 设为 `force` 时不等待空闲。
|
|
19544
|
+
*/
|
|
19545
|
+
async requestInstall(force = false) {
|
|
19546
|
+
const fromVersion = this.currentVersion;
|
|
19547
|
+
const cap = selfUpdateCapability(Boolean(this.publicKey));
|
|
19548
|
+
if (!cap.ok) {
|
|
19549
|
+
return { accepted: false, fromVersion, message: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
|
|
19550
|
+
}
|
|
19551
|
+
const info = await this.inspect();
|
|
19552
|
+
if (!info.updateAvailable) {
|
|
19553
|
+
return {
|
|
19554
|
+
accepted: false,
|
|
19555
|
+
fromVersion,
|
|
19556
|
+
...info.latestVersion ? { toVersion: info.latestVersion } : {},
|
|
19557
|
+
message: info.reason ?? "\u5DF2\u662F\u6700\u65B0\u7248\u672C"
|
|
19558
|
+
};
|
|
19559
|
+
}
|
|
19560
|
+
if (this.checking || this.installing) {
|
|
19561
|
+
return {
|
|
19562
|
+
accepted: true,
|
|
19563
|
+
fromVersion,
|
|
19564
|
+
toVersion: info.latestVersion,
|
|
19565
|
+
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`
|
|
19566
|
+
};
|
|
19567
|
+
}
|
|
19568
|
+
void this.check(force, false, true).catch((err) => {
|
|
19569
|
+
this.recordFailure(info.latestVersion, `\u5B89\u88C5\u5931\u8D25\uFF1A${err.message}`);
|
|
19570
|
+
});
|
|
19571
|
+
return {
|
|
19572
|
+
accepted: true,
|
|
19573
|
+
fromVersion,
|
|
19574
|
+
toVersion: info.latestVersion,
|
|
19575
|
+
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`
|
|
19576
|
+
};
|
|
19577
|
+
}
|
|
17926
19578
|
/**
|
|
17927
19579
|
* 检查并准备更新;`installMode` 为 true 时下载后立即安装(不再等空闲)。
|
|
17928
19580
|
* 返回可读结论,供 CLI `update --check` 使用。
|
|
@@ -18151,7 +19803,9 @@ function seedDemoTenant(store) {
|
|
|
18151
19803
|
// 未显式指定时随机生成:固定默认密钥一旦随 HOST=0.0.0.0 暴露到公网,等于公开凭据。
|
|
18152
19804
|
// 只在首次创建时生成并落盘,后续启动沿用磁盘上的值(启动日志会打印完整密钥)。
|
|
18153
19805
|
apiKey: env2("TENANT_API_KEY") ?? generateApiKey(),
|
|
18154
|
-
|
|
19806
|
+
// 不再硬编码具体厂商型号:志前 API 只认 deepseek-flash,写死 deepseek-v4-flash
|
|
19807
|
+
// 会让新装节点的第一次请求 404。默认值统一走 defaultTenantModel(见 nodeLlm.ts)。
|
|
19808
|
+
model: defaultTenantModel(store),
|
|
18155
19809
|
provider: {
|
|
18156
19810
|
apiKey: env2("OPENAI_API_KEY"),
|
|
18157
19811
|
baseURL: env2("OPENAI_BASE_URL")
|
|
@@ -18291,6 +19945,8 @@ function startServer(opts = {}) {
|
|
|
18291
19945
|
const store = new Store(dataDir);
|
|
18292
19946
|
const demo = seedDemoTenant(store);
|
|
18293
19947
|
syncTenantModels(store);
|
|
19948
|
+
const nodeModel = nodeDefaultProvider(store)?.model;
|
|
19949
|
+
if (nodeModel) adoptBuiltinModelForTenants(store, nodeModel);
|
|
18294
19950
|
const usageLedger = new UsageLedger(dataDir);
|
|
18295
19951
|
usageLedger.init();
|
|
18296
19952
|
const manager = new SessionManager(store, void 0, { usage: usageLedger });
|
|
@@ -18322,6 +19978,7 @@ function startServer(opts = {}) {
|
|
|
18322
19978
|
const notificationHub = createNotificationHub();
|
|
18323
19979
|
let fleet = null;
|
|
18324
19980
|
let fleetCred = null;
|
|
19981
|
+
let updater = null;
|
|
18325
19982
|
function connectFleet(cred) {
|
|
18326
19983
|
const localId = ensureNodeId(store);
|
|
18327
19984
|
const nodeId = (cred.nodeId ?? "").trim() || localId;
|
|
@@ -18339,6 +19996,8 @@ function startServer(opts = {}) {
|
|
|
18339
19996
|
deps: {
|
|
18340
19997
|
store,
|
|
18341
19998
|
manager,
|
|
19999
|
+
// 闭包推迟求值:桥先于 UpdateController 创建,指令到达时 updater 已就绪
|
|
20000
|
+
getUpdater: () => updater,
|
|
18342
20001
|
applyFleetCred: async (cred2) => {
|
|
18343
20002
|
store.saveFleetCredential(cred2);
|
|
18344
20003
|
setTimeout(() => connectFleet(cred2), 300);
|
|
@@ -18431,8 +20090,12 @@ function startServer(opts = {}) {
|
|
|
18431
20090
|
if (initialCred) {
|
|
18432
20091
|
connectFleet(initialCred);
|
|
18433
20092
|
} else if (opts.autoEnroll) {
|
|
18434
|
-
void fleetController.enroll(opts.autoEnroll).then((st) =>
|
|
20093
|
+
void fleetController.enroll(opts.autoEnroll).then((st) => {
|
|
20094
|
+
console.log(`[fleet] \u5DF2\u81EA\u52A8\u63A5\u5165\uFF0C\u8282\u70B9 ${st.nodeId}\uFF08${st.gatewayUrl}\uFF09`);
|
|
20095
|
+
scheduleBuiltinProvider(store, (m) => console.log(m));
|
|
20096
|
+
}).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}`));
|
|
18435
20097
|
}
|
|
20098
|
+
if (initialCred) scheduleBuiltinProvider(store, (m) => console.log(m));
|
|
18436
20099
|
const pairingCodes = new PairingCodeStore();
|
|
18437
20100
|
const qq = new QqManager({
|
|
18438
20101
|
store,
|
|
@@ -18489,16 +20152,21 @@ function startServer(opts = {}) {
|
|
|
18489
20152
|
scheduleFleetInfoRefresh();
|
|
18490
20153
|
}
|
|
18491
20154
|
});
|
|
20155
|
+
updater = new UpdateController({
|
|
20156
|
+
dataDir,
|
|
20157
|
+
isIdle: () => manager.isIdle(),
|
|
20158
|
+
prepareExit: closeServer
|
|
20159
|
+
});
|
|
18492
20160
|
const server = createApp(store, manager, {
|
|
18493
20161
|
secretBox,
|
|
18494
20162
|
notificationHub,
|
|
18495
20163
|
qq,
|
|
18496
20164
|
adminKey,
|
|
18497
20165
|
fleet: fleetController,
|
|
18498
|
-
host
|
|
20166
|
+
host,
|
|
20167
|
+
updater
|
|
18499
20168
|
});
|
|
18500
20169
|
let closed = false;
|
|
18501
|
-
let updater = null;
|
|
18502
20170
|
function closeServer() {
|
|
18503
20171
|
if (closed) return;
|
|
18504
20172
|
closed = true;
|
|
@@ -18531,13 +20199,6 @@ function startServer(opts = {}) {
|
|
|
18531
20199
|
}
|
|
18532
20200
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
18533
20201
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
18534
|
-
if (opts.autoUpdate) {
|
|
18535
|
-
updater = new UpdateController({
|
|
18536
|
-
dataDir,
|
|
18537
|
-
isIdle: () => manager.isIdle(),
|
|
18538
|
-
prepareExit: closeServer
|
|
18539
|
-
});
|
|
18540
|
-
}
|
|
18541
20202
|
server.timeout = 0;
|
|
18542
20203
|
server.keepAliveTimeout = 65e3;
|
|
18543
20204
|
server.headersTimeout = 66e3;
|
|
@@ -18557,7 +20218,7 @@ function startServer(opts = {}) {
|
|
|
18557
20218
|
} else {
|
|
18558
20219
|
printReady(info, qq);
|
|
18559
20220
|
}
|
|
18560
|
-
updater?.start();
|
|
20221
|
+
if (opts.autoUpdate) updater?.start();
|
|
18561
20222
|
resolve20(info);
|
|
18562
20223
|
}, reject);
|
|
18563
20224
|
});
|
|
@@ -18598,7 +20259,7 @@ function printReady(info, qq) {
|
|
|
18598
20259
|
// src/cli/account-enroll.ts
|
|
18599
20260
|
init_esm_shims();
|
|
18600
20261
|
import * as path35 from "path";
|
|
18601
|
-
import * as
|
|
20262
|
+
import * as readline2 from "readline/promises";
|
|
18602
20263
|
var SKIP_ANSWERS = /* @__PURE__ */ new Set(["-", "skip", "none", "no"]);
|
|
18603
20264
|
function resolveDefaultHub(hubUrl2, env3 = process.env) {
|
|
18604
20265
|
return (hubUrl2 ?? env3.HUB_URL ?? DEFAULT_HUB_URL).trim().replace(/\/$/, "");
|
|
@@ -18689,7 +20350,7 @@ async function maybeAccountEnroll(opts = {}) {
|
|
|
18689
20350
|
cliErr(`\u63A5\u5165\u68C0\u67E5\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
18690
20351
|
return;
|
|
18691
20352
|
}
|
|
18692
|
-
const rl =
|
|
20353
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
18693
20354
|
try {
|
|
18694
20355
|
await runWizard(rl, store, opts);
|
|
18695
20356
|
} catch (e) {
|
|
@@ -18770,11 +20431,11 @@ async function askHidden(rl, prompt) {
|
|
|
18770
20431
|
|
|
18771
20432
|
// src/cli/link.ts
|
|
18772
20433
|
init_esm_shims();
|
|
18773
|
-
import * as
|
|
20434
|
+
import * as readline3 from "readline";
|
|
18774
20435
|
|
|
18775
20436
|
// src/cli/serve-instances.ts
|
|
18776
20437
|
init_esm_shims();
|
|
18777
|
-
import { spawnSync as
|
|
20438
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
18778
20439
|
import * as fs30 from "fs";
|
|
18779
20440
|
import * as os10 from "os";
|
|
18780
20441
|
import * as path36 from "path";
|