@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/dist/cli/index.js CHANGED
@@ -551,9 +551,25 @@ var init_wire = __esm({
551
551
 
552
552
  // src/cli/index.ts
553
553
  init_esm_shims();
554
+
555
+ // src/cli/quietDeprecations.ts
556
+ init_esm_shims();
557
+ var IGNORED_WARNING_CODES = /* @__PURE__ */ new Set(["DEP0040"]);
558
+ function shouldSilenceWarning(warning) {
559
+ const code = warning?.code;
560
+ return typeof code === "string" && IGNORED_WARNING_CODES.has(code);
561
+ }
562
+ var listeners = process.listeners("warning");
563
+ process.removeAllListeners("warning");
564
+ process.on("warning", (warning) => {
565
+ if (shouldSilenceWarning(warning)) return;
566
+ for (const listener of listeners) listener(warning);
567
+ });
568
+
569
+ // src/cli/index.ts
554
570
  import "dotenv/config";
555
571
  import * as fs34 from "fs";
556
- import * as readline3 from "readline";
572
+ import * as readline4 from "readline";
557
573
  import * as path44 from "path";
558
574
  import { exec } from "child_process";
559
575
  import { promisify } from "util";
@@ -714,15 +730,27 @@ function networkErrorHints(message, errorType = "") {
714
730
  hints.push("\u2464 IPv6 \u6296\u52A8\u53EF\u8BD5\uFF1ANODE_OPTIONS=--dns-result-order=ipv4first");
715
731
  return hints;
716
732
  }
733
+ function authErrorHints(message, code = "") {
734
+ const text = `${message} ${code}`;
735
+ if (!/401|403|unauthorized|forbidden|invalid[_\s]*api[_\s]*key|api[\s_-]*key\s*(?:无效|invalid)|管理令牌无效/i.test(text)) {
736
+ return [];
737
+ }
738
+ return [
739
+ "\u51ED\u636E\u65E0\u6548\uFF1A\u82E5\u7528\u7684\u662F\u300C\u5FD7\u524D\u5927\u6A21\u578B\uFF08\u5185\u7F6E\uFF09\u300D\uFF0C\u5728\u8282\u70B9\u8BBE\u7F6E\u7684\u6A21\u578B\u9875\u70B9\u300C\u5237\u65B0\u5185\u7F6E\u6A21\u578B\u300D\u91CD\u65B0\u9886\u53D6 Key\uFF08\u5728\u63A7\u5236\u53F0\u91CD\u7F6E\u8FC7 Key \u540E\u9700\u8981\u8FD9\u4E00\u6B65\uFF09\uFF1B\u82E5\u662F\u81EA\u586B\u7684\u4F9B\u5E94\u5546\uFF0C\u8BF7\u68C0\u67E5 Key \u662F\u5426\u6B63\u786E\u6216\u5DF2\u8FC7\u671F"
740
+ ];
741
+ }
717
742
  function paramErrorHints(message, code = "") {
718
743
  const text = `${message} ${code}`;
719
744
  const hints = [];
720
- if (/not[_\s]*(activated|enabled|purchased)|arrearage|quota|subscription|insufficient|未开通|欠费|额度/i.test(
745
+ if (/not[_\s]*(activated|enabled|purchased)|arrearage|quota|subscription|insufficient|未开通|欠费|额度|余额不足/i.test(
721
746
  text
722
747
  )) {
723
748
  hints.push(
724
749
  "\u8BE5\u6A21\u578B\u5728\u6B64 **API Key/\u8D26\u53F7** \u4E0B\u672A\u5F00\u901A\uFF08\u6216\u8D26\u53F7\u6B20\u8D39\u3001\u989D\u5EA6\u7528\u5C3D\uFF09\uFF1A\u5230\u7F51\u5173\u63A7\u5236\u53F0\u5F00\u901A\u8BE5\u6A21\u578B\uFF0C\u6216\u6362\u4E00\u4E2A\u5DF2\u5F00\u901A\u7684\u6A21\u578B"
725
750
  );
751
+ hints.push(
752
+ "\u5982\u679C\u4F60\u7528\u7684\u662F\u300C\u5FD7\u524D\u5927\u6A21\u578B\uFF08\u5185\u7F6E\uFF09\u300D\uFF1A\u5230 https://hub.cd-zq.com \u67E5\u770B\u4F59\u989D\u5E76\u5145\u503C\uFF1B\u4E5F\u53EF\u4EE5\u5148\u5207\u6362\u5230\u4F60\u81EA\u5DF1\u914D\u7F6E\u7684\u5176\u5B83\u6A21\u578B\u7EE7\u7EED\uFF08\u8282\u70B9\u8BBE\u7F6E \u2192 \u6A21\u578B \u2192 \u8BBE\u4E3A\u9ED8\u8BA4\uFF09"
753
+ );
726
754
  }
727
755
  if (/invalid[_\s]*parameter|invalidparameter|required[_\s]+parameter|missing[_\s]+parameter|invalid[_\s]*model|model[_\s]*not[_\s]*found|not[_\s]*support|unsupported/i.test(
728
756
  text
@@ -1080,13 +1108,13 @@ function textOf(content) {
1080
1108
  }
1081
1109
  return content != null ? String(content) : "";
1082
1110
  }
1083
- function extractUserMessages(span, clip = USER_MESSAGE_CLIP) {
1111
+ function extractUserMessages(span, clip2 = USER_MESSAGE_CLIP) {
1084
1112
  const out = [];
1085
1113
  for (const m of span) {
1086
1114
  if (m.role !== "user") continue;
1087
1115
  const text = textOf(m.content).replace(/\s+/g, " ").trim();
1088
1116
  if (!text) continue;
1089
- out.push(text.length > clip ? text.slice(0, clip - 1) + "\u2026" : text);
1117
+ out.push(text.length > clip2 ? text.slice(0, clip2 - 1) + "\u2026" : text);
1090
1118
  }
1091
1119
  return out;
1092
1120
  }
@@ -1920,6 +1948,21 @@ var TurnEngine = class _TurnEngine {
1920
1948
  queueSteering(text) {
1921
1949
  this._steering.push(text);
1922
1950
  }
1951
+ /**
1952
+ * 撤销一条尚未注入的 steering。
1953
+ *
1954
+ * 按文本删第一条匹配项:调用方(SessionManager)已按索引删掉了自己那份队列,
1955
+ * 这里只需保证引擎不再注入同样的内容。用文本而非索引,是因为引擎会在迭代
1956
+ * 间隙批量清空 `_steering`,索引可能已经漂移;文本相同则删哪条都等价。
1957
+ *
1958
+ * @returns 是否真的删掉了(已注入的项返回 false,调用方据此提示用户)
1959
+ */
1960
+ removeSteering(text) {
1961
+ const i = this._steering.indexOf(text);
1962
+ if (i < 0) return false;
1963
+ this._steering.splice(i, 1);
1964
+ return true;
1965
+ }
1923
1966
  // ===================== 主入口 =====================
1924
1967
  /**
1925
1968
  * 运行一轮 Turn
@@ -4950,6 +4993,9 @@ var ConsoleHook = class {
4950
4993
  for (const hint of networkErrorHints(error2, errorType)) {
4951
4994
  cliLog(` \u{1F4A1} ${hint}`);
4952
4995
  }
4996
+ for (const hint of authErrorHints(error2, errorType)) {
4997
+ cliLog(` \u{1F4A1} ${hint}`);
4998
+ }
4953
4999
  for (const hint of paramErrorHints(error2)) {
4954
5000
  cliLog(` \u{1F4A1} ${hint}`);
4955
5001
  }
@@ -9943,6 +9989,15 @@ function commonLegacyFiles() {
9943
9989
  return [path18.join(stateDir(), "data", "node-llm.json")];
9944
9990
  }
9945
9991
 
9992
+ // src/server/llmIds.ts
9993
+ init_esm_shims();
9994
+ var GLOBAL_PROVIDER_ID = "__env__";
9995
+ var BUILTIN_PROVIDER_ID = "__zqai__";
9996
+ function isBuiltinProvider(id) {
9997
+ return id === BUILTIN_PROVIDER_ID;
9998
+ }
9999
+ var FALLBACK_MODEL = "gpt-4o";
10000
+
9946
10001
  // src/cli/connection.ts
9947
10002
  var PROVIDERS = Object.fromEntries(
9948
10003
  LLM_PRESETS.map((p) => [
@@ -10057,6 +10112,7 @@ function useConnection(id) {
10057
10112
  return true;
10058
10113
  }
10059
10114
  function removeConnection(id) {
10115
+ if (id === BUILTIN_PROVIDER_ID) return false;
10060
10116
  const f = read();
10061
10117
  const next = f.providers.filter((p) => p.id !== id);
10062
10118
  if (next.length === f.providers.length) return false;
@@ -10340,7 +10396,7 @@ var GLYPHS = {
10340
10396
  T: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588", " \u2588\u2588 ", " \u2588\u2588 ", " \u2588\u2588 ", " \u2588\u2588 ", " "]
10341
10397
  };
10342
10398
  var GLYPH_ROWS = 6;
10343
- var GLYPH_COLS = 9;
10399
+ var GLYPH_COLS = 11;
10344
10400
  var GRADIENT = [
10345
10401
  [34, 211, 238],
10346
10402
  [99, 102, 241],
@@ -10497,13 +10553,14 @@ function renderTerminalTutorial() {
10497
10553
  // src/cli/link.ts
10498
10554
  init_esm_shims();
10499
10555
  import * as path28 from "path";
10500
- import * as readline2 from "readline";
10556
+ import * as readline3 from "readline";
10501
10557
 
10502
10558
  // src/server/store.ts
10503
10559
  init_esm_shims();
10504
10560
  import * as fs17 from "fs";
10505
10561
  import * as fsp from "fs/promises";
10506
10562
  import * as path20 from "path";
10563
+ import * as readline from "readline";
10507
10564
  import { randomUUID } from "crypto";
10508
10565
 
10509
10566
  // src/server/dbgTrace.ts
@@ -10723,6 +10780,170 @@ var Store = class {
10723
10780
  }
10724
10781
  }
10725
10782
  }
10783
+ /**
10784
+ * 读取事件「一页」:从文件末尾向前,按字节预算取最近的事件。
10785
+ *
10786
+ * - `before` 省略:取最新的 maxBytes 字节
10787
+ * - `before` 指定:只取 seq < before 的事件(向前翻页)
10788
+ *
10789
+ * 为什么不能复用 loadEventsTail:后者只能从文件末尾读固定窗口,
10790
+ * 无法「向前翻页」,而控制台需要滚到顶部继续加载更早的对话。
10791
+ * 这里按块从后往前扫,读到预算或文件头即停;翻了多页之后每次
10792
+ * before 都紧邻上一页,实际只需读 1~2 块。
10793
+ *
10794
+ * 返回按 seq 升序;`hasMore` 表示本页最早一条之前还有更早的事件。
10795
+ */
10796
+ loadEventPage(tenantId, sessionId, opts) {
10797
+ const p = path20.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
10798
+ const { before, maxBytes } = opts;
10799
+ const __t0 = dbgNow();
10800
+ let fd = null;
10801
+ try {
10802
+ const size = fs17.statSync(p).size;
10803
+ if (size === 0) return { events: [], hasMore: false };
10804
+ fd = fs17.openSync(p, "r");
10805
+ const CHUNK = 256 * 1024;
10806
+ let pos = size;
10807
+ const kept = [];
10808
+ let bytes = 0;
10809
+ let hasMore = false;
10810
+ let done = false;
10811
+ while (pos > 0 && !done) {
10812
+ const start = Math.max(0, pos - CHUNK);
10813
+ const len = pos - start;
10814
+ const buf = Buffer.allocUnsafe(len);
10815
+ const read2 = fs17.readSync(fd, buf, 0, len, start);
10816
+ const rows = buf.subarray(0, read2).toString("utf-8").split("\n");
10817
+ if (start > 0) rows.shift();
10818
+ for (let i = rows.length - 1; i >= 0; i--) {
10819
+ const line = rows[i];
10820
+ if (!line) continue;
10821
+ let entry;
10822
+ try {
10823
+ entry = JSON.parse(line);
10824
+ } catch {
10825
+ continue;
10826
+ }
10827
+ if (!Number.isFinite(Number(entry.seq))) continue;
10828
+ if (before !== void 0 && Number(entry.seq) >= before) continue;
10829
+ const sz = Buffer.byteLength(line, "utf-8") + 1;
10830
+ if (bytes > 0 && bytes + sz > maxBytes) {
10831
+ hasMore = true;
10832
+ done = true;
10833
+ break;
10834
+ }
10835
+ bytes += sz;
10836
+ kept.unshift(entry);
10837
+ }
10838
+ pos = start;
10839
+ }
10840
+ dbgMs(
10841
+ "store.loadEventPage",
10842
+ __t0,
10843
+ // [dbg]
10844
+ `size=${size} before=${before ?? "-"} out=${kept.length} bytes\u2248${bytes} more=${hasMore}`
10845
+ );
10846
+ return { events: kept, hasMore };
10847
+ } catch {
10848
+ return { events: [], hasMore: false };
10849
+ } finally {
10850
+ if (fd !== null) {
10851
+ try {
10852
+ fs17.closeSync(fd);
10853
+ } catch {
10854
+ }
10855
+ }
10856
+ }
10857
+ }
10858
+ // ===================== 轮次索引 =====================
10859
+ /** 轮次索引文件路径 */
10860
+ turnsFile(tenantId, sessionId) {
10861
+ return path20.join(this.sessionDir(tenantId, sessionId), "turns.ndjson");
10862
+ }
10863
+ /**
10864
+ * 追加一轮的最新快照(同一 startSeq 后写者胜出)。
10865
+ *
10866
+ * 走与 session.json 相同的每文件写链:索引按「开轮 → 补充消息 → 收轮」
10867
+ * 顺序追加,乱序会让列表读到旧的 running 版本。
10868
+ */
10869
+ appendTurnIndex(tenantId, sessionId, entry) {
10870
+ const dir = this.sessionDir(tenantId, sessionId);
10871
+ fs17.mkdirSync(dir, { recursive: true });
10872
+ const file = this.turnsFile(tenantId, sessionId);
10873
+ const line = JSON.stringify(entry) + "\n";
10874
+ const prev = this.writeChains.get(file) ?? Promise.resolve();
10875
+ const next = prev.then(() => fsp.appendFile(file, line, "utf-8")).catch((err) => {
10876
+ console.error(`[store] appendTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
10877
+ });
10878
+ this.writeChains.set(file, next);
10879
+ return next;
10880
+ }
10881
+ /**
10882
+ * 读取轮次索引(已按 startSeq 去重取最后一版,升序)。
10883
+ *
10884
+ * 文件很小(一轮约 0.5KB,1000 轮 ≈ 500KB),同步读即可,
10885
+ * 会话详情打开路径不会因历史长度变慢。
10886
+ */
10887
+ loadTurnIndex(tenantId, sessionId) {
10888
+ let raw;
10889
+ try {
10890
+ raw = fs17.readFileSync(this.turnsFile(tenantId, sessionId), "utf-8");
10891
+ } catch {
10892
+ return [];
10893
+ }
10894
+ const byStart = /* @__PURE__ */ new Map();
10895
+ for (const line of raw.split("\n")) {
10896
+ if (!line) continue;
10897
+ try {
10898
+ const e = JSON.parse(line);
10899
+ if (!Number.isFinite(Number(e?.startSeq))) continue;
10900
+ byStart.set(Number(e.startSeq), e);
10901
+ } catch {
10902
+ }
10903
+ }
10904
+ return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
10905
+ }
10906
+ /** 整文件替换轮次索引(重建用,tmp + rename 原子写) */
10907
+ saveTurnIndex(tenantId, sessionId, entries) {
10908
+ const dir = this.sessionDir(tenantId, sessionId);
10909
+ fs17.mkdirSync(dir, { recursive: true });
10910
+ const file = this.turnsFile(tenantId, sessionId);
10911
+ const snapshot = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
10912
+ const tmp = `${file}.${randomUUID()}.tmp`;
10913
+ const prev = this.writeChains.get(file) ?? Promise.resolve();
10914
+ const next = prev.then(() => fsp.writeFile(tmp, snapshot, "utf-8")).then(() => fsp.rename(tmp, file)).catch((err) => {
10915
+ console.error(`[store] saveTurnIndex \u5931\u8D25 ${sessionId}: ${String(err)}`);
10916
+ void fsp.rm(tmp, { force: true });
10917
+ });
10918
+ this.writeChains.set(file, next);
10919
+ return next;
10920
+ }
10921
+ /**
10922
+ * 流式读取整份事件日志(重建索引用)。
10923
+ *
10924
+ * 不能用 loadEvents(readFileSync + 逐行 JSON.parse):大会话几十 MB,
10925
+ * 会长时间阻塞事件循环,导致 SSE 心跳 / MQTT 保活断开。
10926
+ */
10927
+ async readEventsAsync(tenantId, sessionId) {
10928
+ const p = path20.join(this.sessionDir(tenantId, sessionId), "events.jsonl");
10929
+ const out = [];
10930
+ try {
10931
+ const rl2 = readline.createInterface({
10932
+ input: fs17.createReadStream(p, { encoding: "utf-8" }),
10933
+ crlfDelay: Infinity
10934
+ });
10935
+ for await (const line of rl2) {
10936
+ if (!line) continue;
10937
+ try {
10938
+ out.push(JSON.parse(line));
10939
+ } catch {
10940
+ }
10941
+ }
10942
+ } catch {
10943
+ return out;
10944
+ }
10945
+ return out;
10946
+ }
10726
10947
  // ===================== 节点身份 & 管理员密钥 =====================
10727
10948
  /** 节点稳定标识:{root}/node.json(首次启动生成后长期固定) */
10728
10949
  loadNodeId() {
@@ -10781,7 +11002,23 @@ var Store = class {
10781
11002
  legacyFiles: [path20.join(this.root, "node-llm.json")]
10782
11003
  });
10783
11004
  const old = new Map(cur.providers.map((p) => [p.id, p]));
10784
- const providers = cfg.providers.map((p) => {
11005
+ const src = [...cfg.providers];
11006
+ const prevBuiltin = cur.providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
11007
+ if (prevBuiltin && !src.some((p) => p.id === BUILTIN_PROVIDER_ID)) {
11008
+ src.unshift({
11009
+ id: prevBuiltin.id,
11010
+ name: prevBuiltin.name,
11011
+ type: prevBuiltin.type,
11012
+ baseURL: prevBuiltin.baseURL,
11013
+ model: prevBuiltin.model,
11014
+ apiKey: decryptKey(prevBuiltin.apiKeyEnc, file) ?? void 0,
11015
+ reasoningEffort: prevBuiltin.reasoningEffort ?? null,
11016
+ ...prevBuiltin.models ? { models: prevBuiltin.models } : {},
11017
+ ...prevBuiltin.nonChat ? { nonChat: prevBuiltin.nonChat } : {},
11018
+ ...prevBuiltin.modelMeta ? { modelMeta: prevBuiltin.modelMeta } : {}
11019
+ });
11020
+ }
11021
+ const providers = src.map((p) => {
10785
11022
  const prev = old.get(p.id);
10786
11023
  const apiKeyEnc = p.apiKey ? encryptKey(p.apiKey, file) : prev?.apiKeyEnc ?? "";
10787
11024
  return {
@@ -10922,7 +11159,7 @@ function defaultNodeLabel() {
10922
11159
  // src/cli/account-enroll.ts
10923
11160
  init_esm_shims();
10924
11161
  import * as path25 from "path";
10925
- import * as readline from "readline/promises";
11162
+ import * as readline2 from "readline/promises";
10926
11163
 
10927
11164
  // src/server/projects.ts
10928
11165
  init_esm_shims();
@@ -10972,7 +11209,6 @@ function loadServerMcpConfigs(tenant) {
10972
11209
 
10973
11210
  // src/server/nodeLlm.ts
10974
11211
  init_esm_shims();
10975
- var GLOBAL_PROVIDER_ID = "__env__";
10976
11212
  function envProvider() {
10977
11213
  const apiKey = process.env.OPENAI_API_KEY;
10978
11214
  if (!apiKey) return null;
@@ -11015,19 +11251,26 @@ function nodeLlmView(cfg) {
11015
11251
  global: true
11016
11252
  }
11017
11253
  ] : [],
11018
- ...cfg.providers.map((p) => ({
11019
- id: p.id,
11020
- name: p.name,
11021
- type: p.type ?? "",
11022
- baseURL: p.baseURL,
11023
- model: p.model,
11024
- hasApiKey: Boolean(p.apiKey),
11025
- models: p.models ?? [],
11026
- nonChat: p.nonChat ?? [],
11027
- modelMeta: p.modelMeta ?? [],
11028
- reasoningEffort: p.reasoningEffort ?? null,
11029
- global: false
11030
- }))
11254
+ ...cfg.providers.map((p) => {
11255
+ const builtin = isBuiltinProvider(p.id);
11256
+ return {
11257
+ id: p.id,
11258
+ name: p.name,
11259
+ type: p.type ?? "",
11260
+ baseURL: p.baseURL,
11261
+ model: p.model,
11262
+ hasApiKey: Boolean(p.apiKey),
11263
+ models: p.models ?? [],
11264
+ nonChat: p.nonChat ?? [],
11265
+ modelMeta: p.modelMeta ?? [],
11266
+ reasoningEffort: p.reasoningEffort ?? null,
11267
+ global: false,
11268
+ /** 平台内置(接入 Hub 后自动下发,用户不用关心 Key) */
11269
+ builtin,
11270
+ /** 不可编辑的字段:控制台据此把 baseURL / key 显示为只读 */
11271
+ ...builtin ? { locked: ["baseURL", "apiKey"] } : {}
11272
+ };
11273
+ })
11031
11274
  ];
11032
11275
  return {
11033
11276
  defaultProviderId: cfg.defaultProviderId ?? null,
@@ -11040,27 +11283,42 @@ function applyNodeLlmPatch(cfg, raw) {
11040
11283
  const next = { providers: cfg.providers, defaultProviderId: cfg.defaultProviderId };
11041
11284
  if (Array.isArray(raw?.providers)) {
11042
11285
  const old = new Map(cfg.providers.map((p) => [p.id, p]));
11043
- next.providers = raw.providers.filter((x) => Boolean(x) && typeof x === "object").filter((x) => x.global !== true && String(x.id ?? "") !== GLOBAL_PROVIDER_ID).map((x) => {
11044
- const id = String(x.id ?? "").trim();
11045
- if (!id) throw new ApiError(400, "bad_request", "\u4F9B\u5E94\u5546 id \u4E0D\u80FD\u4E3A\u7A7A");
11046
- const prev = old.get(id);
11047
- const apiKey = String(x.apiKey ?? "").trim();
11048
- const models = normalizeModelList(x.models) ?? prev?.models;
11049
- const nonChat = normalizeModelList(x.nonChat) ?? prev?.nonChat;
11050
- const modelMeta = normalizeModelMeta(x.modelMeta) ?? prev?.modelMeta;
11051
- return {
11052
- id,
11053
- name: String(x.name ?? prev?.name ?? id).trim(),
11054
- type: x.type == null ? prev?.type : String(x.type),
11055
- baseURL: String(x.baseURL ?? prev?.baseURL ?? "").trim(),
11056
- model: String(x.model ?? prev?.model ?? "").trim(),
11057
- apiKey: apiKey || prev?.apiKey,
11058
- reasoningEffort: x.reasoningEffort === void 0 ? prev?.reasoningEffort ?? null : normReasoningEffort(x.reasoningEffort),
11059
- ...models ? { models } : {},
11060
- ...nonChat ? { nonChat } : {},
11061
- ...modelMeta ? { modelMeta } : {}
11062
- };
11063
- });
11286
+ const incoming = raw.providers.filter((x) => Boolean(x) && typeof x === "object").filter((x) => x.global !== true && String(x.id ?? "") !== GLOBAL_PROVIDER_ID);
11287
+ const prevBuiltin = cfg.providers.find((p) => isBuiltinProvider(p.id));
11288
+ const builtinPatch = incoming.find((x) => isBuiltinProvider(String(x.id ?? "")));
11289
+ const keptBuiltin = prevBuiltin ? [
11290
+ {
11291
+ ...prevBuiltin,
11292
+ ...builtinPatch?.model != null ? { model: String(builtinPatch.model).trim() || prevBuiltin.model } : {},
11293
+ ...normalizeModelList(builtinPatch?.models) ? { models: normalizeModelList(builtinPatch?.models) } : {},
11294
+ ...builtinPatch?.reasoningEffort !== void 0 ? { reasoningEffort: normReasoningEffort(builtinPatch.reasoningEffort) } : {},
11295
+ ...typeof builtinPatch?.name === "string" && builtinPatch.name.trim() ? { name: builtinPatch.name.trim() } : {}
11296
+ }
11297
+ ] : [];
11298
+ next.providers = [
11299
+ ...keptBuiltin,
11300
+ ...incoming.filter((x) => !isBuiltinProvider(String(x.id ?? ""))).map((x) => {
11301
+ const id = String(x.id ?? "").trim();
11302
+ if (!id) throw new ApiError(400, "bad_request", "\u4F9B\u5E94\u5546 id \u4E0D\u80FD\u4E3A\u7A7A");
11303
+ const prev = old.get(id);
11304
+ const apiKey = String(x.apiKey ?? "").trim();
11305
+ const models = normalizeModelList(x.models) ?? prev?.models;
11306
+ const nonChat = normalizeModelList(x.nonChat) ?? prev?.nonChat;
11307
+ const modelMeta = normalizeModelMeta(x.modelMeta) ?? prev?.modelMeta;
11308
+ return {
11309
+ id,
11310
+ name: String(x.name ?? prev?.name ?? id).trim(),
11311
+ type: x.type == null ? prev?.type : String(x.type),
11312
+ baseURL: String(x.baseURL ?? prev?.baseURL ?? "").trim(),
11313
+ model: String(x.model ?? prev?.model ?? "").trim(),
11314
+ apiKey: apiKey || prev?.apiKey,
11315
+ reasoningEffort: x.reasoningEffort === void 0 ? prev?.reasoningEffort ?? null : normReasoningEffort(x.reasoningEffort),
11316
+ ...models ? { models } : {},
11317
+ ...nonChat ? { nonChat } : {},
11318
+ ...modelMeta ? { modelMeta } : {}
11319
+ };
11320
+ })
11321
+ ];
11064
11322
  }
11065
11323
  if (typeof raw?.defaultProviderId === "string") {
11066
11324
  const id = raw.defaultProviderId.trim();
@@ -11080,6 +11338,9 @@ function nodeDefaultProvider(store) {
11080
11338
  if (cfg.providers.length === 0) return null;
11081
11339
  return cfg.providers.find((p) => p.id === cfg.defaultProviderId) ?? cfg.providers[0] ?? null;
11082
11340
  }
11341
+ function defaultTenantModel(store) {
11342
+ return process.env.OPENAI_MODEL || process.env.MODEL || nodeDefaultProvider(store)?.model || FALLBACK_MODEL;
11343
+ }
11083
11344
  function resolveSessionProvider(store, tenant, providerId) {
11084
11345
  const own = tenant.provider ?? {};
11085
11346
  if (providerId) {
@@ -11109,6 +11370,208 @@ function resolveSessionProvider(store, tenant, providerId) {
11109
11370
  return { ...own, model: tenant.model };
11110
11371
  }
11111
11372
 
11373
+ // src/server/builtinLlm.ts
11374
+ init_esm_shims();
11375
+ var FETCH_TIMEOUT_MS = 8e3;
11376
+ var RETRY_AFTER_MS = 5 * 6e4;
11377
+ function hubBase(cred) {
11378
+ return (cred.hubUrl ?? "").replace(/\/+$/, "");
11379
+ }
11380
+ function asProvider(grant) {
11381
+ const baseURL = String(grant.baseURL ?? "").trim();
11382
+ const apiKey = String(grant.apiKey ?? "").trim();
11383
+ const model = String(grant.model ?? "").trim();
11384
+ if (!baseURL || !apiKey) return null;
11385
+ return {
11386
+ id: BUILTIN_PROVIDER_ID,
11387
+ name: String(grant.name ?? "").trim() || "\u5FD7\u524D\u5927\u6A21\u578B\uFF08\u5185\u7F6E\uFF09",
11388
+ type: "builtin",
11389
+ baseURL,
11390
+ model,
11391
+ apiKey
11392
+ };
11393
+ }
11394
+ async function enrichModels(p) {
11395
+ try {
11396
+ const r = await Promise.race([
11397
+ fetchModels(p.baseURL, p.apiKey ?? "", p.model),
11398
+ new Promise((resolve25) => {
11399
+ const t = setTimeout(() => resolve25(null), FETCH_TIMEOUT_MS);
11400
+ t.unref?.();
11401
+ })
11402
+ ]);
11403
+ if (!r?.models?.length) return p;
11404
+ return {
11405
+ ...p,
11406
+ // 控制面没给默认型号时,取供应商返回的第一个,避免模型为空让用户卡住
11407
+ model: p.model || r.models[0],
11408
+ models: r.models,
11409
+ ...r.nonChat?.length ? { nonChat: r.nonChat } : {},
11410
+ ...r.modelMeta?.length ? { modelMeta: r.modelMeta } : {}
11411
+ };
11412
+ } catch {
11413
+ return p;
11414
+ }
11415
+ }
11416
+ function saveBuiltin(store, provider) {
11417
+ const cfg = store.loadNodeLlm();
11418
+ const others = cfg.providers.filter((p) => p.id !== BUILTIN_PROVIDER_ID);
11419
+ const prev = cfg.providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
11420
+ const merged = {
11421
+ ...provider,
11422
+ // 用户选过的型号/候选列表/推理强度/显示名都保留;只有托管字段(baseURL/key)强制以控制面为准
11423
+ ...prev?.model ? { model: prev.model } : {},
11424
+ ...prev?.models?.length ? { models: prev.models } : {},
11425
+ ...prev?.reasoningEffort != null ? { reasoningEffort: prev.reasoningEffort } : {},
11426
+ ...prev?.name ? { name: prev.name } : {}
11427
+ };
11428
+ store.saveNodeLlm({
11429
+ providers: [merged, ...others],
11430
+ // 之前没有默认且用户也没指定时,让内置模型成为默认——「开箱即用」
11431
+ defaultProviderId: cfg.defaultProviderId ?? BUILTIN_PROVIDER_ID
11432
+ });
11433
+ adoptBuiltinModelForTenants(store, merged.model);
11434
+ }
11435
+ var LEGACY_DEFAULT_MODELS = /* @__PURE__ */ new Set([FALLBACK_MODEL, "deepseek-v4-flash"]);
11436
+ function adoptBuiltinModelForTenants(store, model) {
11437
+ if (!model) return;
11438
+ for (const id of store.listTenants()) {
11439
+ const tenant = store.loadTenant(id);
11440
+ if (!tenant || !LEGACY_DEFAULT_MODELS.has(tenant.model)) continue;
11441
+ if (tenant.provider?.apiKey) continue;
11442
+ tenant.model = model;
11443
+ store.saveTenant(tenant);
11444
+ }
11445
+ }
11446
+ async function ensureBuiltinProvider(store, opts = {}) {
11447
+ const cred = store.loadFleetCredential();
11448
+ if (!cred?.fleetToken || !cred.fleetSecret) {
11449
+ return { status: "disabled", reason: "\u8282\u70B9\u672A\u63A5\u5165 Hub" };
11450
+ }
11451
+ const hub = hubBase(cred);
11452
+ if (!hub) return { status: "disabled", reason: "\u7F3A\u5C11 Hub \u5730\u5740" };
11453
+ const local = store.loadNodeLlm().providers.find((p) => p.id === BUILTIN_PROVIDER_ID);
11454
+ if (local && !opts.force) {
11455
+ adoptBuiltinModelForTenants(store, local.model);
11456
+ return { status: "ready", provider: local, refreshed: false };
11457
+ }
11458
+ const cached3 = cred.builtinLlm ? asProvider(cred.builtinLlm) : null;
11459
+ let res;
11460
+ try {
11461
+ res = await fetch(`${hub}/api/hub/node/builtin-llm`, {
11462
+ headers: {
11463
+ Authorization: `Bearer ${cred.fleetToken}`,
11464
+ "X-Fleet-Secret": cred.fleetSecret,
11465
+ "X-Node-Id": cred.nodeId
11466
+ },
11467
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
11468
+ });
11469
+ } catch (e) {
11470
+ if (cached3) {
11471
+ const provider2 = await enrichModels(cached3);
11472
+ saveBuiltin(store, provider2);
11473
+ return { status: "ready", provider: provider2, refreshed: true };
11474
+ }
11475
+ return { status: "error", reason: `\u8FDE\u63A5 Hub \u5931\u8D25\uFF1A${e.message}` };
11476
+ }
11477
+ let body = null;
11478
+ try {
11479
+ body = await res.json();
11480
+ } catch {
11481
+ }
11482
+ if (!res.ok) {
11483
+ const msg = body?.error?.message ?? `HTTP ${res.status}`;
11484
+ if (res.status === 401 || res.status === 403) {
11485
+ return { status: "unavailable", reason: msg };
11486
+ }
11487
+ if (cached3) {
11488
+ const provider2 = await enrichModels(cached3);
11489
+ saveBuiltin(store, provider2);
11490
+ return { status: "ready", provider: provider2, refreshed: true };
11491
+ }
11492
+ return { status: "error", reason: msg };
11493
+ }
11494
+ const grant = body?.data?.builtinLlm;
11495
+ if (!grant) {
11496
+ const why = String(body?.data?.reason ?? "").trim();
11497
+ return { status: "unavailable", reason: why || "\u63A7\u5236\u9762\u672A\u4E0B\u53D1\u5185\u7F6E\u6A21\u578B" };
11498
+ }
11499
+ const parsed = asProvider(grant);
11500
+ if (!parsed) return { status: "unavailable", reason: "\u5185\u7F6E\u6A21\u578B\u914D\u7F6E\u4E0D\u5B8C\u6574" };
11501
+ const provider = await enrichModels(parsed);
11502
+ saveBuiltin(store, provider);
11503
+ return { status: "ready", provider, refreshed: true };
11504
+ }
11505
+ async function fetchLlmBalance(store) {
11506
+ const cred = store.loadFleetCredential();
11507
+ const hub = cred ? hubBase(cred) : "";
11508
+ if (!cred?.fleetToken || !cred.fleetSecret || !hub) {
11509
+ return { available: null, currency: "CNY", reason: "\u8282\u70B9\u672A\u63A5\u5165 Hub" };
11510
+ }
11511
+ try {
11512
+ const res = await fetch(`${hub}/api/hub/node/balance`, {
11513
+ headers: {
11514
+ Authorization: `Bearer ${cred.fleetToken}`,
11515
+ "X-Fleet-Secret": cred.fleetSecret,
11516
+ "X-Node-Id": cred.nodeId
11517
+ },
11518
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
11519
+ });
11520
+ const body = await res.json().catch(() => null);
11521
+ if (!res.ok) {
11522
+ return {
11523
+ available: null,
11524
+ currency: "CNY",
11525
+ reason: body?.data ? "" : `HTTP ${res.status}`
11526
+ };
11527
+ }
11528
+ const d = body?.data ?? {};
11529
+ const available = typeof d.available === "number" ? d.available : null;
11530
+ return {
11531
+ available,
11532
+ currency: typeof d.currency === "string" && d.currency ? d.currency : "CNY",
11533
+ ...typeof d.reserved === "number" ? { reserved: d.reserved } : {},
11534
+ // Hub 没配 HUB_PUBLIC_URL 时退回用接入时的 hubUrl
11535
+ topupUrl: typeof d.topupUrl === "string" && d.topupUrl ? d.topupUrl : `${hub}/topup`,
11536
+ ...available === null && typeof d.reason === "string" ? { reason: d.reason } : {}
11537
+ };
11538
+ } catch (e) {
11539
+ return {
11540
+ available: null,
11541
+ currency: "CNY",
11542
+ reason: `\u8FDE\u63A5 Hub \u5931\u8D25\uFF1A${e.message}`
11543
+ };
11544
+ }
11545
+ }
11546
+ var AUTH_RETRY_THROTTLE_MS = 6e4;
11547
+ var lastAuthRetryAt = 0;
11548
+ function healBuiltinOnAuthFailure(store, message) {
11549
+ if (!/401|unauthorized|invalid[_\s]*api[_\s]*key|api[\s_-]*key\s*(?:无效|invalid)/i.test(message)) return;
11550
+ if (!store.loadFleetCredential()) return;
11551
+ if (!store.loadNodeLlm().providers.some((p) => p.id === BUILTIN_PROVIDER_ID)) return;
11552
+ if (Date.now() - lastAuthRetryAt < AUTH_RETRY_THROTTLE_MS) return;
11553
+ lastAuthRetryAt = Date.now();
11554
+ void ensureBuiltinProvider(store, { force: true }).catch(() => {
11555
+ });
11556
+ }
11557
+ var lastFailureAt = 0;
11558
+ function scheduleBuiltinProvider(store, log = () => {
11559
+ }) {
11560
+ if (Date.now() - lastFailureAt < RETRY_AFTER_MS) return;
11561
+ void ensureBuiltinProvider(store).then((r) => {
11562
+ if (r.status === "ready") {
11563
+ if (r.refreshed) log(`[builtin-llm] \u5DF2\u4E0B\u53D1\u5185\u7F6E\u6A21\u578B\uFF1A${r.provider.name}\uFF08${r.provider.model}\uFF09`);
11564
+ return;
11565
+ }
11566
+ if (r.status === "disabled") return;
11567
+ lastFailureAt = Date.now();
11568
+ if (r.status === "unavailable") log(`[builtin-llm] \u6682\u65E0\u5185\u7F6E\u6A21\u578B\uFF1A${r.reason}`);
11569
+ else log(`[builtin-llm] \u62C9\u53D6\u5185\u7F6E\u6A21\u578B\u5931\u8D25\uFF1A${r.reason}`);
11570
+ }).catch(() => {
11571
+ lastFailureAt = Date.now();
11572
+ });
11573
+ }
11574
+
11112
11575
  // src/server/usage.ts
11113
11576
  init_esm_shims();
11114
11577
  import * as fs19 from "fs";
@@ -11180,6 +11643,19 @@ function addAgg(into, tokens, costUsd) {
11180
11643
  function utcDay(iso) {
11181
11644
  return iso.slice(0, 10);
11182
11645
  }
11646
+ function sumAgg(values) {
11647
+ const out = zeroAgg();
11648
+ for (const v of values) {
11649
+ out.calls += v.calls;
11650
+ out.input += v.input;
11651
+ out.output += v.output;
11652
+ out.cacheRead += v.cacheRead;
11653
+ out.cacheWrite += v.cacheWrite;
11654
+ out.costUsd += v.costUsd;
11655
+ out.unpriced += v.unpriced;
11656
+ }
11657
+ return out;
11658
+ }
11183
11659
  function monthOf(iso) {
11184
11660
  return iso.slice(0, 7);
11185
11661
  }
@@ -11455,6 +11931,23 @@ var UsageLedger = class {
11455
11931
  }
11456
11932
  return all;
11457
11933
  }
11934
+ /**
11935
+ * 按「项目 × 天」聚合(供控制面把消耗拆到每个项目)。
11936
+ *
11937
+ * 汇总文件里只存了按项目、按天两个独立维度,拿不到交叉项,因此这里从
11938
+ * 账本明细现算——调用方(节点上报)只取最近 40 天,读的是 2 个月文件,
11939
+ * 5 分钟一次的成本可忽略。
11940
+ */
11941
+ projectByDay(opts) {
11942
+ const out = {};
11943
+ for (const rec of this.allRecords({ from: opts.from, to: opts.to })) {
11944
+ const day = utcDay(rec.ts);
11945
+ const days = out[rec.projectId] ?? (out[rec.projectId] = {});
11946
+ const agg = days[day] ?? (days[day] = zeroAgg());
11947
+ addAgg(agg, rec, rec.costUsd);
11948
+ }
11949
+ return out;
11950
+ }
11458
11951
  summary(opts) {
11459
11952
  this.ensureInit();
11460
11953
  const currency = (opts.currency ?? "USD").toUpperCase();
@@ -11754,23 +12247,158 @@ var EVENT_BUFFER_MAX = 2e3;
11754
12247
  var EVENTS_ACK_MAX_BYTES = 512 * 1024;
11755
12248
  var APPROVAL_TIMEOUT_MS = (Number(process.env.APPROVAL_TIMEOUT_SECONDS) || 15 * 60) * 1e3;
11756
12249
  var LAST_USER_MESSAGE_MAX = 200;
11757
- function isAffirmative(answer) {
11758
- const a = answer.trim().toLowerCase();
11759
- if (!a) return false;
11760
- if (["y", "yes", "approve", "approved", "ok", "true", "a", "1"].includes(a)) return true;
11761
- return /^(yes|approve|同意|批准|允许|确定|可以|好的|确认)/.test(a);
12250
+ var TURN_TEXT_MAX = 500;
12251
+ var TURN_REPLY_MAX_BYTES = 256 * 1024;
12252
+ var TURN_REPLY_MAX_TEXT = 256 * 1024;
12253
+ var TURNS_DEFAULT_LIMIT = 20;
12254
+ function clip(text, max = LAST_USER_MESSAGE_MAX) {
12255
+ const s = text ?? "";
12256
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
12257
+ }
12258
+ function asNum(v) {
12259
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
12260
+ }
12261
+ function newTurnEntry(ev, input) {
12262
+ return {
12263
+ v: 1,
12264
+ startSeq: ev.seq,
12265
+ endSeq: ev.seq,
12266
+ startedAt: ev.ts,
12267
+ endedAt: null,
12268
+ status: "running",
12269
+ inputs: [{ seq: ev.seq, ts: ev.ts, text: clip(input, TURN_TEXT_MAX) }],
12270
+ replyPreview: "",
12271
+ producedFiles: [],
12272
+ llmCalls: 0,
12273
+ toolCalls: 0,
12274
+ tokensIn: 0,
12275
+ tokensOut: 0
12276
+ };
11762
12277
  }
11763
- function defer() {
11764
- let resolve25;
11765
- const promise = new Promise((r) => {
11766
- resolve25 = r;
11767
- });
11768
- return { promise, resolve: resolve25 };
12278
+ function applyTurnEvent(turn, ev, toolSeen) {
12279
+ const d = ev.data ?? {};
12280
+ turn.endSeq = ev.seq;
12281
+ switch (ev.type) {
12282
+ case "queue_steering": {
12283
+ turn.inputs.push({
12284
+ seq: ev.seq,
12285
+ ts: ev.ts,
12286
+ text: clip(String(d["text"] ?? ""), TURN_TEXT_MAX)
12287
+ });
12288
+ break;
12289
+ }
12290
+ case "assistant_message": {
12291
+ turn.llmCalls += 1;
12292
+ const text = String(d["text"] ?? "");
12293
+ if (text) {
12294
+ turn.replyPreview = clip(text);
12295
+ turn.replySeq = ev.seq;
12296
+ }
12297
+ const u = d["usage"];
12298
+ if (u && typeof u === "object") {
12299
+ const usage = u;
12300
+ turn.tokensIn += asNum(usage["input"]);
12301
+ turn.tokensOut += asNum(usage["output"]);
12302
+ }
12303
+ break;
12304
+ }
12305
+ case "tool_started":
12306
+ case "tool_finished": {
12307
+ const key = String(d["reqId"] ?? "") || `${String(d["name"] ?? "")}:${ev.seq}`;
12308
+ if (!toolSeen.has(key)) {
12309
+ toolSeen.add(key);
12310
+ turn.toolCalls += 1;
12311
+ }
12312
+ break;
12313
+ }
12314
+ case "error": {
12315
+ turn.error = clip(String(d["message"] ?? d["error"] ?? ""));
12316
+ break;
12317
+ }
12318
+ default:
12319
+ break;
12320
+ }
11769
12321
  }
11770
- var SessionManager = class {
11771
- constructor(store, approvalTimeoutMs = APPROVAL_TIMEOUT_MS, opts = {}) {
11772
- this.store = store;
11773
- this.approvalTimeoutMs = approvalTimeoutMs;
12322
+ function closeTurnEntry(turn, ev) {
12323
+ const d = ev.data ?? {};
12324
+ turn.endSeq = ev.seq;
12325
+ turn.endedAt = ev.ts;
12326
+ const status = String(d["status"] ?? "");
12327
+ turn.status = ev.type === "interrupted" ? status || "interrupted" : status || "completed";
12328
+ const files = d["produced_files"];
12329
+ if (Array.isArray(files)) {
12330
+ turn.producedFiles = files.map((x) => String(x ?? "")).filter(Boolean);
12331
+ }
12332
+ if (typeof d["iterations"] === "number") turn.iterations = d["iterations"];
12333
+ const startMs = Date.parse(turn.startedAt);
12334
+ const endMs = Date.parse(ev.ts);
12335
+ if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
12336
+ turn.durationMs = endMs - startMs;
12337
+ }
12338
+ }
12339
+ function buildTurnIndex(events) {
12340
+ const out = [];
12341
+ let cur = null;
12342
+ let toolSeen = /* @__PURE__ */ new Set();
12343
+ for (const ev of events) {
12344
+ if (ev.type === "turn_start") {
12345
+ cur = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
12346
+ toolSeen = /* @__PURE__ */ new Set();
12347
+ out.push(cur);
12348
+ continue;
12349
+ }
12350
+ if (!cur) continue;
12351
+ if (ev.type === "turn_end" || ev.type === "interrupted") {
12352
+ closeTurnEntry(cur, ev);
12353
+ cur = null;
12354
+ continue;
12355
+ }
12356
+ applyTurnEvent(cur, ev, toolSeen);
12357
+ }
12358
+ for (const t of out) {
12359
+ if (!t.endedAt && t.error) t.status = "error";
12360
+ }
12361
+ return out;
12362
+ }
12363
+ function summaryFromEntry(t) {
12364
+ return {
12365
+ startSeq: t.startSeq,
12366
+ endSeq: t.endSeq,
12367
+ startedAt: t.startedAt,
12368
+ endedAt: t.endedAt,
12369
+ status: t.status,
12370
+ input: t.inputs[0]?.text ?? "",
12371
+ reply: t.replyPreview,
12372
+ llmCalls: t.llmCalls,
12373
+ toolCalls: t.toolCalls,
12374
+ tokensIn: t.tokensIn,
12375
+ tokensOut: t.tokensOut,
12376
+ iterations: t.iterations,
12377
+ durationMs: t.durationMs,
12378
+ error: t.error,
12379
+ inputs: t.inputs,
12380
+ userCount: t.inputs.length,
12381
+ replySeq: t.replySeq,
12382
+ producedFiles: t.producedFiles
12383
+ };
12384
+ }
12385
+ function isAffirmative(answer) {
12386
+ const a = answer.trim().toLowerCase();
12387
+ if (!a) return false;
12388
+ if (["y", "yes", "approve", "approved", "ok", "true", "a", "1"].includes(a)) return true;
12389
+ return /^(yes|approve|同意|批准|允许|确定|可以|好的|确认)/.test(a);
12390
+ }
12391
+ function defer() {
12392
+ let resolve25;
12393
+ const promise = new Promise((r) => {
12394
+ resolve25 = r;
12395
+ });
12396
+ return { promise, resolve: resolve25 };
12397
+ }
12398
+ var SessionManager = class {
12399
+ constructor(store, approvalTimeoutMs = APPROVAL_TIMEOUT_MS, opts = {}) {
12400
+ this.store = store;
12401
+ this.approvalTimeoutMs = approvalTimeoutMs;
11774
12402
  this.opts = opts;
11775
12403
  }
11776
12404
  store;
@@ -11778,6 +12406,10 @@ var SessionManager = class {
11778
12406
  opts;
11779
12407
  runtimes = /* @__PURE__ */ new Map();
11780
12408
  // sessionId → runtime
12409
+ /** 轮次索引的内存副本(运行中的轮比磁盘索引新;重启后为空) */
12410
+ turnIndexCache = /* @__PURE__ */ new Map();
12411
+ /** 进行中的索引重建任务(并发请求合并到同一个) */
12412
+ turnIndexBuilds = /* @__PURE__ */ new Map();
11781
12413
  /** 全局通知 SSE 订阅者(控制台通知中心) */
11782
12414
  notificationSubs = /* @__PURE__ */ new Set();
11783
12415
  /**
@@ -11832,6 +12464,17 @@ var SessionManager = class {
11832
12464
  rt.status = status;
11833
12465
  this._emitNotify(rt, "session_status", this._statusData(rt));
11834
12466
  }
12467
+ /**
12468
+ * 通知里的「最近一条用户消息」:截断到 LAST_USER_MESSAGE_MAX。
12469
+ *
12470
+ * 三处共用(session_status / turn_completed / turn_stopped),避免各自写一遍
12471
+ * 截断规则后长度不一致——控制台弹窗与任务列表会显示同一句话的两个版本。
12472
+ */
12473
+ _lastMessage(rt) {
12474
+ const text = (rt.lastInput ?? "").trim();
12475
+ if (!text) return null;
12476
+ return text.length > LAST_USER_MESSAGE_MAX ? `${text.slice(0, LAST_USER_MESSAGE_MAX)}\u2026` : text;
12477
+ }
11835
12478
  /**
11836
12479
  * session_status 通知载荷:状态 + 最近一条用户消息(截断)。
11837
12480
  *
@@ -11840,10 +12483,24 @@ var SessionManager = class {
11840
12483
  */
11841
12484
  _statusData(rt) {
11842
12485
  const data = { status: rt.status };
11843
- const text = (rt.lastInput ?? "").trim();
11844
- if (text) {
11845
- data.lastUserMessage = text.length > LAST_USER_MESSAGE_MAX ? `${text.slice(0, LAST_USER_MESSAGE_MAX)}\u2026` : text;
11846
- }
12486
+ if (rt.lastStartedAt) data.lastStartedAt = rt.lastStartedAt;
12487
+ const msg = this._lastMessage(rt);
12488
+ if (msg) data.lastUserMessage = msg;
12489
+ return data;
12490
+ }
12491
+ /**
12492
+ * 任务结束类通知(完成 / 达到迭代上限)的载荷:在 turn_end 数据之外补上
12493
+ * 「会话名 + 最后一条用户消息」。
12494
+ *
12495
+ * 控制台右下角的完成弹窗要说明「哪个会话、用户让它做了什么」。这些字段
12496
+ * 以节点侧为准随通知一起下发,前端就不必依赖快照(nodeInfo / session_status)
12497
+ * 是否已经到达——刚打开页面或通知乱序时才不会退化成只剩一句「任务已完成」。
12498
+ */
12499
+ _turnEndData(rt, eventData, extra) {
12500
+ const data = { ...eventData, ...extra };
12501
+ if (rt.name) data.sessionName = rt.name;
12502
+ const msg = this._lastMessage(rt);
12503
+ if (msg) data.lastUserMessage = msg;
11847
12504
  return data;
11848
12505
  }
11849
12506
  /** 触发通知回调(无 handler 时为空操作,异常隔离不冒泡) */
@@ -12000,6 +12657,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
12000
12657
  this.runtimes.delete(sessionId);
12001
12658
  }
12002
12659
  this.store.deleteSession(tenantId, sessionId);
12660
+ this.turnIndexCache.delete(sessionId);
12003
12661
  this._publishNotify(tenantId, {
12004
12662
  kind: "session_status",
12005
12663
  sessionId,
@@ -12043,6 +12701,7 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
12043
12701
  throw new ApiError(409, "busy", "\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u4E2D");
12044
12702
  }
12045
12703
  rt.engine.queueSteering(input);
12704
+ if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
12046
12705
  rt.pendingSteers.push(input);
12047
12706
  this._recordLastInput(rt, input);
12048
12707
  return "steered";
@@ -12075,9 +12734,42 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
12075
12734
  const rt = this._getOrLoadRuntime(tenant, sessionId);
12076
12735
  if (!rt.engine) throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
12077
12736
  rt.engine.queueSteering(text);
12737
+ if (rt.pendingSteers.length === 0) rt.steerBaseInput = rt.lastInput;
12078
12738
  rt.pendingSteers.push(text);
12079
12739
  this._recordLastInput(rt, text);
12080
12740
  }
12741
+ /**
12742
+ * 撤销一条尚未注入的补充信息(用户反悔)。
12743
+ *
12744
+ * 队列里的项还没进聊天流,此时只能靠中断整个任务来「收回」代价太大。
12745
+ * index 取自 GET /state 的 pendingSteers(与前端展示顺序一致),两侧同时删:
12746
+ * 只删前端那份,引擎下一迭代仍会把它注入对话;只删引擎那份,队列里会残留
12747
+ * 一条永远不注入的条目。
12748
+ *
12749
+ * 若撤的正是「最近用户指令」,把它还原成队列里的上一条、或本轮被覆盖前的
12750
+ * 指令——否则工作台还会显示这条已经被取消的内容。
12751
+ *
12752
+ * @returns 剩余待注入队列(前端据此刷新提示区)
12753
+ */
12754
+ unsteer(tenant, sessionId, index) {
12755
+ const rt = this._getOrLoadRuntime(tenant, sessionId);
12756
+ if (!rt.engine || !rt.runTask) {
12757
+ throw new ApiError(409, "not_running", "\u4F1A\u8BDD\u672A\u5728\u8FD0\u884C\u4E2D");
12758
+ }
12759
+ if (!Number.isInteger(index) || index < 0 || index >= rt.pendingSteers.length) {
12760
+ throw new ApiError(409, "already_injected", "\u8FD9\u6761\u8865\u5145\u4FE1\u606F\u5DF2\u8FDB\u5165\u5BF9\u8BDD\uFF0C\u65E0\u6CD5\u64A4\u9500");
12761
+ }
12762
+ const [removed] = rt.pendingSteers.splice(index, 1);
12763
+ if (removed !== void 0) {
12764
+ rt.engine.removeSteering(removed);
12765
+ if (removed === rt.lastInput) {
12766
+ rt.lastInput = rt.pendingSteers.length > 0 ? rt.pendingSteers[rt.pendingSteers.length - 1] : rt.steerBaseInput;
12767
+ this._persist(rt);
12768
+ this._emitNotify(rt, "session_status", this._statusData(rt));
12769
+ }
12770
+ }
12771
+ return [...rt.pendingSteers];
12772
+ }
12081
12773
  /**
12082
12774
  * 已提交、尚未注入当前任务的补充信息。
12083
12775
  *
@@ -12510,44 +13202,214 @@ data: ${JSON.stringify({ sessionId, seq: entry.seq, ts: entry.ts, data: entry.da
12510
13202
  status: rt.status
12511
13203
  };
12512
13204
  }
12513
- /** 增量事件(since 游标;无内存运行时则从磁盘读全量) */
12514
- eventsSince(tenantId, sessionId, since) {
13205
+ /**
13206
+ * 事件分页(向前 / 向后两种方向)。
13207
+ *
13208
+ * - `since`:取 seq > since(实时增量 / 重连补齐)
13209
+ * - `before`:取 seq < before(控制台滚到顶部加载更早的对话)
13210
+ * - `maxBytes`:单次回执的字节预算(默认沿用 EVENTS_ACK_MAX_BYTES)
13211
+ *
13212
+ * 两者同时给出时按 `since`(向后)处理。`hasMore` 只对向前翻页有意义:
13213
+ * 表示本页最早一条之前还有更早的事件。
13214
+ *
13215
+ * 向前翻页一律走磁盘:内存缓冲只保留最近 EVENT_BUFFER_MAX 条,
13216
+ * 更早的事件只在 events.jsonl 里。
13217
+ */
13218
+ eventsPage(tenantId, sessionId, opts = {}) {
12515
13219
  const __t0 = dbgNow();
13220
+ const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : EVENTS_ACK_MAX_BYTES;
12516
13221
  const rt = this.runtimes.get(sessionId);
13222
+ if (opts.before !== void 0) {
13223
+ const since2 = opts.since ?? 0;
13224
+ const page = this.store.loadEventPage(tenantId, sessionId, {
13225
+ before: opts.before,
13226
+ maxBytes
13227
+ });
13228
+ const ev2 = since2 > 0 ? page.events.filter((e) => e.seq > since2) : page.events;
13229
+ dbgMs(
13230
+ "session.eventsPage(before/\u78C1\u76D8)",
13231
+ __t0,
13232
+ // [dbg]
13233
+ `sid=${sessionId.slice(0, 8)} since=${since2} before=${opts.before} out=${ev2.length} more=${page.hasMore}`
13234
+ );
13235
+ return {
13236
+ events: ev2,
13237
+ latestSeq: rt?.seq ?? page.events.at(-1)?.seq ?? 0,
13238
+ earliestSeq: ev2[0]?.seq ?? 0,
13239
+ hasMore: page.hasMore
13240
+ };
13241
+ }
13242
+ const since = opts.since ?? 0;
12517
13243
  if (rt) {
12518
- const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since));
13244
+ const ev2 = this._boundedTail(rt.events.filter((e) => e.seq > since), maxBytes);
12519
13245
  dbgMs(
12520
- "session.eventsSince(\u5185\u5B58)",
13246
+ "session.eventsPage(\u5185\u5B58)",
12521
13247
  __t0,
12522
13248
  // [dbg]
12523
13249
  `sid=${sessionId.slice(0, 8)} since=${since} in=${rt.events.length} out=${ev2.length}`
12524
13250
  );
12525
- return { events: ev2, latestSeq: rt.seq };
13251
+ return {
13252
+ events: ev2,
13253
+ latestSeq: rt.seq,
13254
+ earliestSeq: ev2[0]?.seq ?? 0,
13255
+ // 内存缓冲被截断(或 since 早于缓冲起点)时,磁盘上可能还有更早的
13256
+ hasMore: !!ev2.length && ev2[0].seq > (rt.events[0]?.seq ?? 0)
13257
+ };
12526
13258
  }
12527
13259
  const all = this.store.loadEventsTail(tenantId, sessionId, EVENTS_ACK_MAX_BYTES * 2);
12528
- const ev = this._boundedTail(all.filter((e) => e.seq > since));
13260
+ const ev = this._boundedTail(
13261
+ all.filter((e) => e.seq > since),
13262
+ maxBytes
13263
+ );
12529
13264
  dbgMs(
12530
- "session.eventsSince(\u78C1\u76D8)",
13265
+ "session.eventsPage(\u78C1\u76D8)",
12531
13266
  __t0,
12532
13267
  // [dbg]
12533
13268
  `sid=${sessionId.slice(0, 8)} since=${since} loaded=${all.length} out=${ev.length}`
12534
13269
  );
12535
13270
  return {
12536
13271
  events: ev,
12537
- latestSeq: all.at(-1)?.seq ?? 0
13272
+ latestSeq: all.at(-1)?.seq ?? 0,
13273
+ earliestSeq: ev[0]?.seq ?? 0,
13274
+ // 读的是文件尾部窗口:窗口已满说明更早的事件还在文件里
13275
+ hasMore: all.length > 0 && ev.length > 0 && ev[0].seq > all[0].seq
13276
+ };
13277
+ }
13278
+ /**
13279
+ * 兼容旧调用:只按 since 取增量。
13280
+ * @deprecated 新代码用 eventsPage(支持 before 向前翻页)
13281
+ */
13282
+ eventsSince(tenantId, sessionId, since) {
13283
+ const r = this.eventsPage(tenantId, sessionId, { since });
13284
+ return { events: r.events, latestSeq: r.latestSeq };
13285
+ }
13286
+ /**
13287
+ * 「按 turn 汇总」的轻量列表:只回标题与统计,点开某一轮再单独拉该轮回复。
13288
+ *
13289
+ * 数据来自 turns.ndjson 索引(派生自事件流),读取成本与会话历史长度无关:
13290
+ * 老会话首次访问时后台/本请求内重建一次,之后每页只读几 KB。
13291
+ *
13292
+ * - `before`:只返回 startSeq < before 的轮次(向前翻页)
13293
+ * - `turns` 按时间升序;`hasMore` 表示更早还有轮次
13294
+ */
13295
+ async sessionTurns(tenantId, sessionId, opts = {}) {
13296
+ const limit = Math.min(Math.max(opts.limit ?? TURNS_DEFAULT_LIMIT, 1), 100);
13297
+ const entries = await this._ensureTurnIndex(tenantId, sessionId);
13298
+ const before = opts.before;
13299
+ const endIdx = before === void 0 ? entries.length : entries.findIndex((t) => t.startSeq >= before);
13300
+ const end = endIdx < 0 ? entries.length : endIdx;
13301
+ const start = Math.max(0, end - limit);
13302
+ const picked = entries.slice(start, end);
13303
+ return {
13304
+ turns: picked.map(summaryFromEntry),
13305
+ hasMore: start > 0,
13306
+ earliestSeq: picked[0]?.startSeq ?? 0,
13307
+ total: entries.length,
13308
+ userTotal: entries.reduce((n, t) => n + t.inputs.length, 0)
12538
13309
  };
12539
13310
  }
13311
+ /**
13312
+ * 某一轮的用户消息与最终回复(含产出文件)。
13313
+ *
13314
+ * 只读两条索引记录 + 尾部一个事件页,不会因该轮工具输出很大而变慢:
13315
+ * 回复正文紧邻 turn_end,按 replySeq 精确读即可。
13316
+ */
13317
+ async sessionTurnReply(tenantId, sessionId, startSeq) {
13318
+ const entries = await this._ensureTurnIndex(tenantId, sessionId);
13319
+ const turn = entries.find((t) => t.startSeq === startSeq);
13320
+ if (!turn) throw new ApiError(404, "not_found", "\u8F6E\u6B21\u4E0D\u5B58\u5728");
13321
+ let reply = null;
13322
+ let truncated = false;
13323
+ if (turn.replySeq !== void 0) {
13324
+ const page = this.store.loadEventPage(tenantId, sessionId, {
13325
+ before: turn.replySeq + 1,
13326
+ maxBytes: TURN_REPLY_MAX_BYTES
13327
+ });
13328
+ const ev = page.events.find((e) => e.seq === turn.replySeq);
13329
+ if (ev && ev.type === "assistant_message") {
13330
+ const text = String(ev.data?.["text"] ?? "");
13331
+ if (text.length > TURN_REPLY_MAX_TEXT) {
13332
+ reply = { seq: ev.seq, ts: ev.ts, text: text.slice(0, TURN_REPLY_MAX_TEXT) };
13333
+ truncated = true;
13334
+ } else {
13335
+ reply = { seq: ev.seq, ts: ev.ts, text };
13336
+ }
13337
+ }
13338
+ }
13339
+ return {
13340
+ startSeq: turn.startSeq,
13341
+ endSeq: turn.endSeq,
13342
+ status: turn.status,
13343
+ startedAt: turn.startedAt,
13344
+ endedAt: turn.endedAt,
13345
+ userMessages: turn.inputs,
13346
+ reply,
13347
+ truncated,
13348
+ producedFiles: turn.producedFiles
13349
+ };
13350
+ }
13351
+ /**
13352
+ * 取轮次索引;缺失时从事件流重建(同一会话的并发请求共用一个重建任务)。
13353
+ *
13354
+ * 索引是派生数据:损坏/落后都只需重建,不影响会话本身。
13355
+ */
13356
+ /**
13357
+ * 取轮次索引;缺失/落后时从事件流重建(同一会话的并发请求共用一个重建任务)。
13358
+ *
13359
+ * 索引是派生数据:损坏或落后都只需重建,不影响会话本身。
13360
+ */
13361
+ async _ensureTurnIndex(tenantId, sessionId) {
13362
+ let entries = this._mergeTurnIndex(tenantId, sessionId);
13363
+ if (entries.length > 0 && !this.runtimes.has(sessionId)) {
13364
+ const last = entries[entries.length - 1];
13365
+ const tail = this.store.loadEventsTail(tenantId, sessionId, 64 * 1024);
13366
+ const newTurnStarted = tail.some((e) => e.type === "turn_start" && e.seq > last.endSeq);
13367
+ if (newTurnStarted || last.status === "running") entries = [];
13368
+ }
13369
+ if (entries.length > 0) return entries;
13370
+ const pending = this.turnIndexBuilds.get(sessionId);
13371
+ if (pending) return pending;
13372
+ const task = this._rebuildTurnIndex(tenantId, sessionId).finally(() => {
13373
+ this.turnIndexBuilds.delete(sessionId);
13374
+ });
13375
+ this.turnIndexBuilds.set(sessionId, task);
13376
+ return task;
13377
+ }
13378
+ /** 合并磁盘索引与内存索引(内存版本更新,同一 startSeq 以内存为准) */
13379
+ _mergeTurnIndex(tenantId, sessionId) {
13380
+ const byStart = /* @__PURE__ */ new Map();
13381
+ for (const e of this.store.loadTurnIndex(tenantId, sessionId)) byStart.set(e.startSeq, e);
13382
+ for (const e of this.turnIndexCache.get(sessionId) ?? []) byStart.set(e.startSeq, e);
13383
+ return [...byStart.values()].sort((a, b) => a.startSeq - b.startSeq);
13384
+ }
13385
+ /** 从 events.jsonl 顺序流式重建轮次索引(一次性,之后增量维护) */
13386
+ async _rebuildTurnIndex(tenantId, sessionId) {
13387
+ const __t0 = dbgNow();
13388
+ const events = await this.store.readEventsAsync(tenantId, sessionId);
13389
+ const entries = buildTurnIndex(events);
13390
+ if (entries.length > 0) {
13391
+ await this.store.saveTurnIndex(tenantId, sessionId, entries);
13392
+ if (this.runtimes.has(sessionId)) this.turnIndexCache.set(sessionId, entries);
13393
+ }
13394
+ dbgMs(
13395
+ "session._rebuildTurnIndex",
13396
+ __t0,
13397
+ // [dbg]
13398
+ `sid=${sessionId.slice(0, 8)} events=${events.length} turns=${entries.length}`
13399
+ );
13400
+ return entries;
13401
+ }
12540
13402
  /**
12541
13403
  * 按字节预算截取事件「尾部」(至少保留最后一条),确保 MQTT 回执远小于单包上限。
12542
13404
  * 供 get_events 使用:会话历史可任意大,但 MQTT 链路不能承载大包。
12543
13405
  */
12544
- _boundedTail(events) {
13406
+ _boundedTail(events, maxBytes = EVENTS_ACK_MAX_BYTES) {
12545
13407
  const __t0 = dbgNow();
12546
13408
  let total = 0;
12547
13409
  let i = events.length;
12548
13410
  while (i > 0) {
12549
13411
  const size = Buffer.byteLength(JSON.stringify(events[i - 1]));
12550
- if (total > 0 && total + size > EVENTS_ACK_MAX_BYTES) break;
13412
+ if (total > 0 && total + size > maxBytes) break;
12551
13413
  total += size;
12552
13414
  i -= 1;
12553
13415
  }
@@ -12667,6 +13529,7 @@ data: ${JSON.stringify(entry.data)}
12667
13529
  nameSource: rec.nameSource,
12668
13530
  taskCount: rec.taskCount ?? 0,
12669
13531
  lastCompletedAt: rec.lastCompletedAt,
13532
+ lastStartedAt: rec.lastStartedAt,
12670
13533
  naming: false,
12671
13534
  turnStartedAt: 0
12672
13535
  };
@@ -12684,10 +13547,11 @@ data: ${JSON.stringify(entry.data)}
12684
13547
  }
12685
13548
  /** 装配引擎并启动一轮 Turn(供 submitTurn / submitOrSteer 复用) */
12686
13549
  _startTurn(rt, input) {
13550
+ const engine = this._buildEngine(rt.tenant, rt, true);
12687
13551
  rt.lastInput = input;
12688
- this._setStatus(rt, "running");
12689
13552
  rt.turnStartedAt = Date.now();
12690
- const engine = this._buildEngine(rt.tenant, rt, true);
13553
+ rt.lastStartedAt = new Date(rt.turnStartedAt).toISOString();
13554
+ this._setStatus(rt, "running");
12691
13555
  rt.engine = engine;
12692
13556
  this._persist(rt);
12693
13557
  rt.runTask = this._drive(rt, engine.run(input));
@@ -12746,18 +13610,18 @@ data: ${JSON.stringify(entry.data)}
12746
13610
  this._setStatus(rt, "completed");
12747
13611
  rt.taskCount += 1;
12748
13612
  rt.lastCompletedAt = (/* @__PURE__ */ new Date()).toISOString();
12749
- this._emitNotify(rt, "turn_completed", {
12750
- ...event.data,
12751
- summary,
12752
- durationMs
12753
- });
13613
+ this._emitNotify(
13614
+ rt,
13615
+ "turn_completed",
13616
+ this._turnEndData(rt, event.data, { summary, durationMs })
13617
+ );
12754
13618
  } else if (status === "max_iterations_exceeded") {
12755
13619
  this._setStatus(rt, "stopped");
12756
- this._emitNotify(rt, "turn_stopped", {
12757
- ...event.data,
12758
- summary,
12759
- durationMs
12760
- });
13620
+ this._emitNotify(
13621
+ rt,
13622
+ "turn_stopped",
13623
+ this._turnEndData(rt, event.data, { summary, durationMs })
13624
+ );
12761
13625
  }
12762
13626
  this._persist(rt);
12763
13627
  if (status === "completed" && engine && shouldAutoName(rt.taskCount, rt.nameSource)) {
@@ -12768,7 +13632,8 @@ data: ${JSON.stringify(entry.data)}
12768
13632
  case "error" /* ERROR */:
12769
13633
  this._setStatus(rt, "error");
12770
13634
  this._persist(rt);
12771
- this._emitNotify(rt, "error", event.data);
13635
+ this._emitNotify(rt, "error", this._turnEndData(rt, event.data, {}));
13636
+ healBuiltinOnAuthFailure(this.store, String(event.data["error"] ?? ""));
12772
13637
  break;
12773
13638
  case "interrupted" /* INTERRUPTED */:
12774
13639
  this._setStatus(rt, "interrupted");
@@ -12794,9 +13659,17 @@ data: ${JSON.stringify(entry.data)}
12794
13659
  this._setStatus(rt, "error");
12795
13660
  this._persist(rt);
12796
13661
  } finally {
13662
+ if (rt.turnIndex) {
13663
+ const turn = rt.turnIndex;
13664
+ if (!turn.endedAt && turn.error) turn.status = "error";
13665
+ this._appendTurnIndex(rt, turn);
13666
+ rt.turnIndex = void 0;
13667
+ rt.turnToolKeys = void 0;
13668
+ }
12797
13669
  rt.runTask = null;
12798
13670
  rt.engine = null;
12799
13671
  rt.pendingSteers = [];
13672
+ rt.steerBaseInput = void 0;
12800
13673
  }
12801
13674
  }
12802
13675
  _buffer(rt, event) {
@@ -12809,6 +13682,7 @@ data: ${JSON.stringify(entry.data)}
12809
13682
  rt.events.push(entry);
12810
13683
  if (rt.events.length > EVENT_BUFFER_MAX) rt.events.shift();
12811
13684
  void this.store.appendEvent(rt.tenantId, rt.sessionId, entry);
13685
+ this._trackTurnIndex(rt, entry);
12812
13686
  for (const res of rt.subscribers) this._sseSend(res, entry);
12813
13687
  this._broadcastSessionEvent(rt.tenantId, rt.sessionId, entry);
12814
13688
  try {
@@ -12816,6 +13690,42 @@ data: ${JSON.stringify(entry.data)}
12816
13690
  } catch {
12817
13691
  }
12818
13692
  }
13693
+ /**
13694
+ * 维护运行中轮次的索引(in-memory 记账 + 关键节点落盘)。
13695
+ *
13696
+ * 落盘只在「开轮 / 补充消息 / 收轮」发生,delta 事件仅推进 endSeq 与统计,
13697
+ * 因此索引写入量是每轮 2~3 行,与会话事件量无关。
13698
+ */
13699
+ _trackTurnIndex(rt, ev) {
13700
+ if (ev.type === "turn_start") {
13701
+ const turn2 = newTurnEntry(ev, String(ev.data?.["input"] ?? ""));
13702
+ rt.turnIndex = turn2;
13703
+ rt.turnToolKeys = /* @__PURE__ */ new Set();
13704
+ this._appendTurnIndex(rt, turn2);
13705
+ return;
13706
+ }
13707
+ const turn = rt.turnIndex;
13708
+ if (!turn) return;
13709
+ if (ev.type === "turn_end" || ev.type === "interrupted") {
13710
+ closeTurnEntry(turn, ev);
13711
+ this._appendTurnIndex(rt, turn);
13712
+ rt.turnIndex = void 0;
13713
+ rt.turnToolKeys = void 0;
13714
+ return;
13715
+ }
13716
+ if (!rt.turnToolKeys) rt.turnToolKeys = /* @__PURE__ */ new Set();
13717
+ const inputsBefore = turn.inputs.length;
13718
+ applyTurnEvent(turn, ev, rt.turnToolKeys);
13719
+ if (turn.inputs.length !== inputsBefore) this._appendTurnIndex(rt, turn);
13720
+ }
13721
+ /** 写入一版轮次索引(内存副本 + 异步落盘) */
13722
+ _appendTurnIndex(rt, turn) {
13723
+ const cached3 = this.turnIndexCache.get(rt.sessionId) ?? [];
13724
+ const i = cached3.findIndex((t) => t.startSeq === turn.startSeq);
13725
+ const next = i >= 0 ? [...cached3.slice(0, i), turn, ...cached3.slice(i + 1)] : [...cached3, turn];
13726
+ this.turnIndexCache.set(rt.sessionId, next);
13727
+ void this.store.appendTurnIndex(rt.tenantId, rt.sessionId, turn);
13728
+ }
12819
13729
  /** 回收内存 run 任务(释放引用交给 GC;磁盘状态保持不动) */
12820
13730
  _recycleRunTask(rt) {
12821
13731
  rt.runTask = null;
@@ -12853,6 +13763,7 @@ data: ${JSON.stringify(entry.data)}
12853
13763
  nameSource: rt.nameSource,
12854
13764
  taskCount: rt.taskCount,
12855
13765
  lastCompletedAt: rt.lastCompletedAt,
13766
+ lastStartedAt: rt.lastStartedAt,
12856
13767
  modelOverridden: rt.modelOverridden,
12857
13768
  providerId: rt.providerId,
12858
13769
  permissionMode: rt.permissionMode,
@@ -13146,7 +14057,6 @@ var ApiError = class extends Error {
13146
14057
  };
13147
14058
 
13148
14059
  // src/server/projects.ts
13149
- var DEFAULT_MODEL = () => process.env.OPENAI_MODEL || process.env.MODEL || "gpt-4o";
13150
14060
  function toProjectSummary(store, tenant) {
13151
14061
  return {
13152
14062
  id: tenant.id,
@@ -13256,6 +14166,7 @@ function allSessionStates(store, manager) {
13256
14166
  name: s.name ?? null,
13257
14167
  status: s.status,
13258
14168
  lastUserMessage: lastUserMessage(s),
14169
+ lastStartedAt: s.lastStartedAt ?? null,
13259
14170
  lastCompletedAt: lastCompletedAt(s)
13260
14171
  });
13261
14172
  }
@@ -13285,7 +14196,7 @@ function createProject(store, input) {
13285
14196
  id,
13286
14197
  name: explicitName || defaultName,
13287
14198
  apiKey: generateApiKey(),
13288
- model: typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : DEFAULT_MODEL(),
14199
+ model: typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : defaultTenantModel(store),
13289
14200
  provider: {
13290
14201
  apiKey: typeof raw.providerApiKey === "string" && raw.providerApiKey.trim() ? raw.providerApiKey.trim() : void 0,
13291
14202
  baseURL: typeof raw.baseURL === "string" && raw.baseURL.trim() ? raw.baseURL.trim() : void 0
@@ -13310,7 +14221,7 @@ function buildNodeInfo(store, opts) {
13310
14221
  nodeId: opts.nodeId ?? ensureNodeId(store),
13311
14222
  platform: process.platform,
13312
14223
  mode: process.env.NODE_MODE ?? "standalone",
13313
- version: "0.2.15",
14224
+ version: "0.2.20",
13314
14225
  admin: opts.admin,
13315
14226
  projects: opts.projects
13316
14227
  };
@@ -13432,7 +14343,7 @@ async function maybeAccountEnroll(opts = {}) {
13432
14343
  cliErr(`\u63A5\u5165\u68C0\u67E5\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
13433
14344
  return;
13434
14345
  }
13435
- const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
14346
+ const rl2 = readline2.createInterface({ input: process.stdin, output: process.stdout });
13436
14347
  try {
13437
14348
  await runWizard(rl2, store, opts);
13438
14349
  } catch (e) {
@@ -13526,7 +14437,7 @@ import * as path26 from "path";
13526
14437
  import { spawnSync as spawnSync2 } from "child_process";
13527
14438
  import { createRequire as createRequire3 } from "module";
13528
14439
  function currentVersion() {
13529
- const injected = "0.2.15"?.trim();
14440
+ const injected = "0.2.20"?.trim();
13530
14441
  if (injected) return injected;
13531
14442
  try {
13532
14443
  const req2 = createRequire3(import.meta.url);
@@ -14242,7 +15153,7 @@ async function runLinkCommand(argv, ctx = {}) {
14242
15153
  const getRl = () => {
14243
15154
  if (ctx.rl) return asPrompter(ctx.rl);
14244
15155
  if (!interactive) return null;
14245
- ownedRl = readline2.createInterface({ input: process.stdin, output: process.stdout });
15156
+ ownedRl = readline3.createInterface({ input: process.stdin, output: process.stdout });
14246
15157
  return asPrompter(ownedRl);
14247
15158
  };
14248
15159
  const closeOwnedRl = () => {
@@ -14862,9 +15773,133 @@ function makeDirIn(parentInput, name) {
14862
15773
  init_esm_shims();
14863
15774
  import * as fs23 from "fs";
14864
15775
  import * as path31 from "path";
15776
+
15777
+ // src/server/zipArchive.ts
15778
+ init_esm_shims();
15779
+ import * as zlib from "zlib";
15780
+ var MAX_ZIP_ENTRIES = 2e4;
15781
+ var LOCAL_SIG = 67324752;
15782
+ var CENTRAL_SIG = 33639248;
15783
+ var EOCD_SIG = 101010256;
15784
+ var FLAG_UTF8 = 2048;
15785
+ var METHOD_STORE = 0;
15786
+ var METHOD_DEFLATE = 8;
15787
+ var CRC_TABLE = (() => {
15788
+ const table = new Int32Array(256);
15789
+ for (let n = 0; n < 256; n++) {
15790
+ let c = n;
15791
+ for (let k = 0; k < 8; k++) {
15792
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
15793
+ }
15794
+ table[n] = c;
15795
+ }
15796
+ return table;
15797
+ })();
15798
+ function crc32(buf) {
15799
+ let c = -1;
15800
+ for (let i = 0; i < buf.length; i++) {
15801
+ c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
15802
+ }
15803
+ return (c ^ -1) >>> 0;
15804
+ }
15805
+ function dosDateTime(d) {
15806
+ const year = Math.max(1980, d.getFullYear());
15807
+ return {
15808
+ time: d.getHours() << 11 | d.getMinutes() << 5 | d.getSeconds() >> 1,
15809
+ date: year - 1980 << 9 | d.getMonth() + 1 << 5 | d.getDate()
15810
+ };
15811
+ }
15812
+ function buildZip(entries) {
15813
+ if (entries.length > MAX_ZIP_ENTRIES) {
15814
+ throw new Error(`\u5F52\u6863\u6761\u76EE\u8FC7\u591A\uFF08${entries.length} > ${MAX_ZIP_ENTRIES}\uFF09`);
15815
+ }
15816
+ const chunks = [];
15817
+ const central = [];
15818
+ let offset = 0;
15819
+ for (const e of entries) {
15820
+ const name = Buffer.from(e.name, "utf-8");
15821
+ const isDir = e.name.endsWith("/");
15822
+ const raw = isDir ? Buffer.alloc(0) : e.data ?? Buffer.alloc(0);
15823
+ let method = METHOD_STORE;
15824
+ let body = raw;
15825
+ if (!isDir && raw.length > 0) {
15826
+ const deflated = zlib.deflateRawSync(raw, { level: 6 });
15827
+ if (deflated.length < raw.length) {
15828
+ method = METHOD_DEFLATE;
15829
+ body = deflated;
15830
+ }
15831
+ }
15832
+ const crc = isDir ? 0 : crc32(raw);
15833
+ const { time, date } = dosDateTime(e.mtime ?? /* @__PURE__ */ new Date());
15834
+ const local = Buffer.alloc(30);
15835
+ local.writeUInt32LE(LOCAL_SIG, 0);
15836
+ local.writeUInt16LE(20, 4);
15837
+ local.writeUInt16LE(FLAG_UTF8, 6);
15838
+ local.writeUInt16LE(method, 8);
15839
+ local.writeUInt16LE(time, 10);
15840
+ local.writeUInt16LE(date, 12);
15841
+ local.writeUInt32LE(crc, 14);
15842
+ local.writeUInt32LE(body.length, 18);
15843
+ local.writeUInt32LE(raw.length, 22);
15844
+ local.writeUInt16LE(name.length, 26);
15845
+ local.writeUInt16LE(0, 28);
15846
+ chunks.push(local, name);
15847
+ if (body.length > 0) chunks.push(body);
15848
+ central.push({
15849
+ name,
15850
+ method,
15851
+ crc,
15852
+ csize: body.length,
15853
+ usize: raw.length,
15854
+ offset,
15855
+ time,
15856
+ date,
15857
+ isDir
15858
+ });
15859
+ offset += local.length + name.length + body.length;
15860
+ }
15861
+ const cdStart = offset;
15862
+ for (const c of central) {
15863
+ const head = Buffer.alloc(46);
15864
+ head.writeUInt32LE(CENTRAL_SIG, 0);
15865
+ head.writeUInt16LE(20, 4);
15866
+ head.writeUInt16LE(20, 6);
15867
+ head.writeUInt16LE(FLAG_UTF8, 8);
15868
+ head.writeUInt16LE(c.method, 10);
15869
+ head.writeUInt16LE(c.time, 12);
15870
+ head.writeUInt16LE(c.date, 14);
15871
+ head.writeUInt32LE(c.crc, 16);
15872
+ head.writeUInt32LE(c.csize, 20);
15873
+ head.writeUInt32LE(c.usize, 24);
15874
+ head.writeUInt16LE(c.name.length, 28);
15875
+ head.writeUInt16LE(0, 30);
15876
+ head.writeUInt16LE(0, 32);
15877
+ head.writeUInt16LE(0, 34);
15878
+ head.writeUInt16LE(0, 36);
15879
+ head.writeUInt32LE(c.isDir ? 16 : 0, 38);
15880
+ head.writeUInt32LE(c.offset, 42);
15881
+ chunks.push(head, c.name);
15882
+ offset += head.length + c.name.length;
15883
+ }
15884
+ const eocd = Buffer.alloc(22);
15885
+ eocd.writeUInt32LE(EOCD_SIG, 0);
15886
+ eocd.writeUInt16LE(0, 4);
15887
+ eocd.writeUInt16LE(0, 6);
15888
+ eocd.writeUInt16LE(central.length, 8);
15889
+ eocd.writeUInt16LE(central.length, 10);
15890
+ eocd.writeUInt32LE(offset - cdStart, 12);
15891
+ eocd.writeUInt32LE(cdStart, 16);
15892
+ eocd.writeUInt16LE(0, 20);
15893
+ chunks.push(eocd);
15894
+ return Buffer.concat(chunks);
15895
+ }
15896
+
15897
+ // src/server/projectFiles.ts
14865
15898
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
14866
15899
  var MAX_DOWNLOAD_BYTES = 16 * 1024 * 1024;
15900
+ var MAX_BINARY_BYTES = 2.5 * 1024 * 1024;
14867
15901
  var MAX_ENTRIES2 = 2e3;
15902
+ var MAX_ZIP_BYTES = 100 * 1024 * 1024;
14868
15903
  function toPosix(p) {
14869
15904
  return p.split(path31.sep).join("/");
14870
15905
  }
@@ -15011,6 +16046,26 @@ function writeProjectFile(workspaceRoot, relPath, content) {
15011
16046
  }
15012
16047
  return { path: toPosix(path31.relative(base, file)), size: buf.length };
15013
16048
  }
16049
+ function writeProjectFileBase64(workspaceRoot, relPath, base64) {
16050
+ const base = projectRoot(workspaceRoot);
16051
+ const rel = (relPath ?? "").trim();
16052
+ if (!rel) throw new Error("\u6587\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
16053
+ const file = resolveWithin(base, rel);
16054
+ const b64 = String(base64 ?? "").replace(/\s+/g, "");
16055
+ if (!b64) throw new Error("\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
16056
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) throw new Error("\u5185\u5BB9\u4E0D\u662F\u5408\u6CD5\u7684 base64");
16057
+ if (Math.floor(b64.length * 3 / 4) > MAX_BINARY_BYTES) {
16058
+ throw new Error(`\u5185\u5BB9\u8D85\u8FC7 ${MAX_BINARY_BYTES / (1024 * 1024)}MB \u4E0A\u9650\uFF0C\u65E0\u6CD5\u4FDD\u5B58`);
16059
+ }
16060
+ const buf = Buffer.from(b64, "base64");
16061
+ try {
16062
+ fs23.mkdirSync(path31.dirname(file), { recursive: true });
16063
+ fs23.writeFileSync(file, buf);
16064
+ } catch (e) {
16065
+ throw new Error(`\u5199\u5165\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
16066
+ }
16067
+ return { path: toPosix(path31.relative(base, file)), size: buf.length };
16068
+ }
15014
16069
  function createProjectDir(workspaceRoot, relPath) {
15015
16070
  const base = projectRoot(workspaceRoot);
15016
16071
  const rel = (relPath ?? "").trim();
@@ -15084,6 +16139,170 @@ function searchProjectFiles(workspaceRoot, query) {
15084
16139
  walk(base, "");
15085
16140
  return out;
15086
16141
  }
16142
+ function deleteProjectPaths(workspaceRoot, relPaths) {
16143
+ const base = projectRoot(workspaceRoot);
16144
+ const out = { deleted: [], failed: [] };
16145
+ const seen = /* @__PURE__ */ new Set();
16146
+ for (const raw of relPaths ?? []) {
16147
+ const rel = (raw ?? "").trim().replace(/^[/\\]+/, "");
16148
+ if (!rel) {
16149
+ out.failed.push({ path: String(raw ?? ""), error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
16150
+ continue;
16151
+ }
16152
+ if (seen.has(rel)) continue;
16153
+ seen.add(rel);
16154
+ let abs;
16155
+ try {
16156
+ abs = resolveWithin(base, rel);
16157
+ } catch (e) {
16158
+ out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
16159
+ continue;
16160
+ }
16161
+ if (path31.resolve(abs) === path31.resolve(base)) {
16162
+ out.failed.push({ path: rel, error: "\u4E0D\u80FD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55" });
16163
+ continue;
16164
+ }
16165
+ if (!fs23.existsSync(abs)) {
16166
+ out.failed.push({ path: rel, error: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
16167
+ continue;
16168
+ }
16169
+ try {
16170
+ fs23.rmSync(abs, { recursive: true, force: true });
16171
+ out.deleted.push(toPosix(path31.relative(base, abs)));
16172
+ } catch (e) {
16173
+ out.failed.push({ path: rel, error: e instanceof Error ? e.message : String(e) });
16174
+ }
16175
+ }
16176
+ return out;
16177
+ }
16178
+ function formatBytes(n) {
16179
+ if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
16180
+ if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
16181
+ return `${n} B`;
16182
+ }
16183
+ function renameProjectPath(workspaceRoot, relPath, newName) {
16184
+ const base = projectRoot(workspaceRoot);
16185
+ const rel = (relPath ?? "").trim().replace(/^[/\\]+/, "");
16186
+ if (!rel) throw new Error("\u4E0D\u80FD\u91CD\u547D\u540D\u9879\u76EE\u6839\u76EE\u5F55");
16187
+ const abs = resolveWithin(base, rel);
16188
+ const name = (newName ?? "").trim();
16189
+ if (!name) throw new Error("\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
16190
+ if (name === "." || name === "..") throw new Error("\u540D\u79F0\u975E\u6CD5");
16191
+ if (/[/\\]/.test(name)) throw new Error("\u540D\u79F0\u4E0D\u80FD\u5305\u542B\u8DEF\u5F84\u5206\u9694\u7B26");
16192
+ if (!fs23.existsSync(abs)) throw new Error("\u6587\u4EF6\u4E0D\u5B58\u5728");
16193
+ const parent = path31.dirname(abs);
16194
+ const target = path31.join(parent, name);
16195
+ if (!isWithinRoot(base, target)) throw new Error("\u8DEF\u5F84\u8D8A\u754C\uFF08\u8D85\u51FA\u9879\u76EE\u5DE5\u4F5C\u533A\uFF09");
16196
+ if (fs23.existsSync(target)) {
16197
+ let same = false;
16198
+ try {
16199
+ const a = fs23.statSync(target);
16200
+ const b = fs23.statSync(abs);
16201
+ same = a.ino === b.ino && a.dev === b.dev;
16202
+ } catch {
16203
+ same = false;
16204
+ }
16205
+ if (!same) throw new Error(`\u5DF2\u5B58\u5728\u540C\u540D\u6587\u4EF6\u6216\u76EE\u5F55: ${name}`);
16206
+ }
16207
+ if (path31.resolve(target) === path31.resolve(abs)) {
16208
+ return { from: toPosix(path31.relative(base, abs)), path: toPosix(path31.relative(base, abs)) };
16209
+ }
16210
+ try {
16211
+ fs23.renameSync(abs, target);
16212
+ } catch (e) {
16213
+ throw new Error(`\u91CD\u547D\u540D\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
16214
+ }
16215
+ return { from: toPosix(path31.relative(base, abs)), path: toPosix(path31.relative(base, target)) };
16216
+ }
16217
+ function zipProjectPaths(workspaceRoot, relPaths, opts = {}) {
16218
+ const base = projectRoot(workspaceRoot);
16219
+ const maxBytes = opts.maxBytes && opts.maxBytes > 0 ? opts.maxBytes : MAX_ZIP_BYTES;
16220
+ const rels = (relPaths ?? []).map((x) => (x ?? "").trim().replace(/^[/\\]+/, ""));
16221
+ if (rels.length === 0) throw new Error("\u8BF7\u9009\u62E9\u8981\u538B\u7F29\u7684\u6587\u4EF6\u6216\u76EE\u5F55");
16222
+ if (rels.some((r) => !r)) throw new Error("\u4E0D\u80FD\u538B\u7F29\u9879\u76EE\u6839\u76EE\u5F55");
16223
+ const targets = rels.map((rel) => ({ rel, abs: resolveWithin(base, rel) }));
16224
+ const single = targets.length === 1 ? targets[0].abs : "";
16225
+ const items = [];
16226
+ let total = 0;
16227
+ const overLimit = () => new Error(
16228
+ `\u5171 ${formatBytes(total)}\uFF0C\u8D85\u8FC7 ${formatBytes(maxBytes)} \u4E0A\u9650\uFF0C\u4E0D\u652F\u6301\u538B\u7F29\u4E0B\u8F7D`
16229
+ );
16230
+ const walk = (absDir) => {
16231
+ let dirents;
16232
+ try {
16233
+ dirents = fs23.readdirSync(absDir, { withFileTypes: true });
16234
+ } catch {
16235
+ return 0;
16236
+ }
16237
+ dirents.sort((a, b) => a.name.localeCompare(b.name));
16238
+ let files = 0;
16239
+ for (const d of dirents) {
16240
+ if (d.isSymbolicLink()) continue;
16241
+ const child = path31.join(absDir, d.name);
16242
+ if (d.isDirectory()) {
16243
+ files += walk(child);
16244
+ continue;
16245
+ }
16246
+ if (!d.isFile()) continue;
16247
+ let st;
16248
+ try {
16249
+ st = fs23.statSync(child);
16250
+ } catch {
16251
+ continue;
16252
+ }
16253
+ total += st.size;
16254
+ if (total > maxBytes) throw overLimit();
16255
+ if (items.length >= MAX_ZIP_ENTRIES) {
16256
+ 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`);
16257
+ }
16258
+ items.push({ abs: child, dir: false, size: st.size, mtime: st.mtime });
16259
+ files++;
16260
+ }
16261
+ if (files === 0) {
16262
+ let mtime = /* @__PURE__ */ new Date();
16263
+ try {
16264
+ mtime = fs23.statSync(absDir).mtime;
16265
+ } catch {
16266
+ }
16267
+ items.push({ abs: absDir, dir: true, size: 0, mtime });
16268
+ }
16269
+ return files;
16270
+ };
16271
+ for (const t of targets) {
16272
+ let st;
16273
+ try {
16274
+ st = fs23.statSync(t.abs);
16275
+ } catch {
16276
+ throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${t.rel}`);
16277
+ }
16278
+ if (st.isDirectory()) {
16279
+ walk(t.abs);
16280
+ continue;
16281
+ }
16282
+ total += st.size;
16283
+ if (total > maxBytes) throw overLimit();
16284
+ items.push({ abs: t.abs, dir: false, size: st.size, mtime: st.mtime });
16285
+ }
16286
+ const nameOf = (abs) => {
16287
+ const rel = toPosix(path31.relative(single || base, abs));
16288
+ return rel || path31.posix.basename(toPosix(abs));
16289
+ };
16290
+ const zipEntries = items.map((it) => {
16291
+ const name2 = nameOf(it.abs);
16292
+ if (it.dir) return { name: `${name2}/`, mtime: it.mtime };
16293
+ return { name: name2, data: fs23.readFileSync(it.abs), mtime: it.mtime };
16294
+ });
16295
+ const buf = buildZip(zipEntries);
16296
+ const first = targets[0].rel;
16297
+ const name = single ? `${path31.posix.basename(toPosix(first))}.zip` : "archive.zip";
16298
+ return {
16299
+ name,
16300
+ size: buf.length,
16301
+ total,
16302
+ entries: zipEntries.length,
16303
+ base64: buf.toString("base64")
16304
+ };
16305
+ }
15087
16306
 
15088
16307
  // src/server/projectOps.ts
15089
16308
  init_esm_shims();
@@ -15413,6 +16632,7 @@ function projectStats(manager, tenantId) {
15413
16632
 
15414
16633
  // src/server/nodeHost.ts
15415
16634
  init_esm_shims();
16635
+ import { spawnSync as spawnSync4 } from "child_process";
15416
16636
  import * as fs24 from "fs";
15417
16637
  import * as os9 from "os";
15418
16638
  function diskInfo(pathStr) {
@@ -15427,6 +16647,64 @@ function diskInfo(pathStr) {
15427
16647
  return null;
15428
16648
  }
15429
16649
  }
16650
+ 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)$/;
16651
+ function isInternalMount(mount) {
16652
+ if (mount === "/") return false;
16653
+ const prefixes = ["/proc", "/sys", "/run", "/snap"];
16654
+ if (process.platform === "darwin") prefixes.push("/System/Volumes");
16655
+ return prefixes.some((p) => mount === p || mount.startsWith(`${p}/`));
16656
+ }
16657
+ function readOnlyVolumes() {
16658
+ const out = /* @__PURE__ */ new Set();
16659
+ const res = spawnSync4("mount", [], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
16660
+ if (res.error || res.status !== 0 || !res.stdout) return out;
16661
+ for (const line of res.stdout.split("\n")) {
16662
+ const m = line.match(/^.+? on (.+?) \(([^)]*)\)$/);
16663
+ if (!m || m[1] === "/") continue;
16664
+ if (m[2].split(",").some((opt) => opt.trim() === "read-only")) out.add(m[1]);
16665
+ }
16666
+ return out;
16667
+ }
16668
+ function diskList(workspacePath) {
16669
+ const out = [];
16670
+ const seen = /* @__PURE__ */ new Set();
16671
+ const push = (path45, total, free) => {
16672
+ if (!path45 || seen.has(path45) || !Number.isFinite(total) || total <= 0) return;
16673
+ seen.add(path45);
16674
+ out.push({ path: path45, total, free });
16675
+ };
16676
+ if (process.platform === "win32") {
16677
+ for (const letter of "CDEFGHIJKLMNOPQRSTUVWXYZ") {
16678
+ const root = `${letter}:\\`;
16679
+ const d = diskInfo(root);
16680
+ if (d) push(root, d.total, d.free);
16681
+ }
16682
+ } else {
16683
+ const readonly = process.platform === "darwin" ? readOnlyVolumes() : /* @__PURE__ */ new Set();
16684
+ const res = spawnSync4("df", ["-kP"], {
16685
+ encoding: "utf-8",
16686
+ stdio: ["ignore", "pipe", "ignore"]
16687
+ });
16688
+ if (!res.error && res.status === 0 && res.stdout) {
16689
+ for (const line of res.stdout.split("\n").slice(1)) {
16690
+ const m = line.trim().match(/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\d+%\s+(.+)$/);
16691
+ if (!m || PSEUDO_FS.test(m[1])) continue;
16692
+ if (/^\/dev\/loop\d+$/.test(m[1])) continue;
16693
+ if (isInternalMount(m[5]) || readonly.has(m[5])) continue;
16694
+ push(m[5], Number(m[2]) * 1024, Number(m[4]) * 1024);
16695
+ }
16696
+ }
16697
+ }
16698
+ const primary = diskInfo(workspacePath);
16699
+ if (primary) {
16700
+ const covered = out.some((d) => {
16701
+ const root = d.path.endsWith("/") || d.path.endsWith("\\") ? d.path : `${d.path}/`;
16702
+ return workspacePath === d.path || workspacePath.startsWith(root);
16703
+ });
16704
+ if (!covered) push(primary.path, primary.total, primary.free);
16705
+ }
16706
+ return out.sort((a, b) => b.total - a.total);
16707
+ }
15430
16708
  function networkInfo() {
15431
16709
  const out = [];
15432
16710
  const ifaces = os9.networkInterfaces();
@@ -15482,6 +16760,7 @@ function overview(store, manager) {
15482
16760
  function buildNodeHost(store, manager, diskPath) {
15483
16761
  const cpus2 = os9.cpus();
15484
16762
  const ov = overview(store, manager);
16763
+ const disks = diskList(diskPath || process.cwd());
15485
16764
  return {
15486
16765
  nodeId: store.loadNodeId() ?? "",
15487
16766
  hostname: os9.hostname(),
@@ -15489,7 +16768,7 @@ function buildNodeHost(store, manager, diskPath) {
15489
16768
  arch: process.arch,
15490
16769
  release: os9.release(),
15491
16770
  uptimeSec: Math.round(os9.uptime()),
15492
- version: "0.2.15",
16771
+ version: "0.2.20",
15493
16772
  mode: process.env.NODE_MODE ?? "standalone",
15494
16773
  cpu: {
15495
16774
  count: cpus2.length,
@@ -15499,6 +16778,7 @@ function buildNodeHost(store, manager, diskPath) {
15499
16778
  memory: { total: os9.totalmem(), free: os9.freemem() },
15500
16779
  network: networkInfo(),
15501
16780
  disk: diskInfo(diskPath || process.cwd()),
16781
+ disks,
15502
16782
  ...ov
15503
16783
  };
15504
16784
  }
@@ -15632,6 +16912,7 @@ function parseApprovalDecision(raw) {
15632
16912
  }
15633
16913
  function createApp(store, manager, opts = {}) {
15634
16914
  setupServerHandlers(manager);
16915
+ const updater = opts.updater ?? null;
15635
16916
  const routes = [
15636
16917
  // ---------- 会话管理 ----------
15637
16918
  {
@@ -15773,6 +17054,18 @@ function createApp(store, manager, opts = {}) {
15773
17054
  ok(res, { accepted: true });
15774
17055
  }
15775
17056
  },
17057
+ {
17058
+ method: "POST",
17059
+ pattern: ["sessions", ":id", "steer", "remove"],
17060
+ // 撤销未注入的补充信息
17061
+ fn: (_req, res, { tenant, params, body }) => {
17062
+ const index = Number(body?.index);
17063
+ if (!Number.isInteger(index))
17064
+ throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
17065
+ const pendingSteers = manager.unsteer(tenant, params["id"], index);
17066
+ ok(res, { accepted: true, pendingSteers });
17067
+ }
17068
+ },
15776
17069
  {
15777
17070
  method: "POST",
15778
17071
  pattern: ["sessions", ":id", "interrupt"],
@@ -16303,6 +17596,30 @@ function createApp(store, manager, opts = {}) {
16303
17596
  }
16304
17597
  }
16305
17598
  },
17599
+ {
17600
+ // 二进制写入(图片编辑保存):body = { path, base64 }
17601
+ method: "POST",
17602
+ pattern: ["files", "binary"],
17603
+ fn: (_req, res, { tenant, body }) => {
17604
+ const raw = body ?? {};
17605
+ try {
17606
+ ok(
17607
+ res,
17608
+ writeProjectFileBase64(
17609
+ tenant.workspaceRoot ?? null,
17610
+ String(raw.path ?? ""),
17611
+ String(raw.base64 ?? "")
17612
+ )
17613
+ );
17614
+ } catch (e) {
17615
+ throw new ApiError(
17616
+ 400,
17617
+ "bad_request",
17618
+ e instanceof Error ? e.message : String(e)
17619
+ );
17620
+ }
17621
+ }
17622
+ },
16306
17623
  // ---------- 项目终端(在工作区执行命令) ----------
16307
17624
  {
16308
17625
  method: "POST",
@@ -16653,6 +17970,21 @@ function createApp(store, manager, opts = {}) {
16653
17970
  }
16654
17971
  }
16655
17972
  },
17973
+ {
17974
+ method: "POST",
17975
+ pattern: ["files", "delete"],
17976
+ // 删除文件 / 目录(目录递归);逐条返回失败原因
17977
+ fn: (_req, res, { tenant, body }) => {
17978
+ const raw = body ?? {};
17979
+ const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
17980
+ if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
17981
+ try {
17982
+ ok(res, deleteProjectPaths(tenant.workspaceRoot ?? null, paths));
17983
+ } catch (e) {
17984
+ throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
17985
+ }
17986
+ }
17987
+ },
16656
17988
  {
16657
17989
  method: "POST",
16658
17990
  pattern: ["files", "create"],
@@ -16672,6 +18004,41 @@ function createApp(store, manager, opts = {}) {
16672
18004
  }
16673
18005
  }
16674
18006
  },
18007
+ {
18008
+ method: "POST",
18009
+ pattern: ["files", "rename"],
18010
+ // 重命名(只改同目录下的名字)
18011
+ fn: (_req, res, { tenant, body }) => {
18012
+ const raw = body ?? {};
18013
+ try {
18014
+ ok(
18015
+ res,
18016
+ renameProjectPath(
18017
+ tenant.workspaceRoot ?? null,
18018
+ String(raw.path ?? ""),
18019
+ String(raw.name ?? "")
18020
+ )
18021
+ );
18022
+ } catch (e) {
18023
+ throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
18024
+ }
18025
+ }
18026
+ },
18027
+ {
18028
+ method: "POST",
18029
+ pattern: ["files", "zip"],
18030
+ // 压缩下载(多选一起打包;超上限由实现拒绝)
18031
+ fn: (_req, res, { tenant, body }) => {
18032
+ const raw = body ?? {};
18033
+ const paths = Array.isArray(raw.paths) ? raw.paths.map((x) => String(x)) : [];
18034
+ if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
18035
+ try {
18036
+ ok(res, zipProjectPaths(tenant.workspaceRoot ?? null, paths));
18037
+ } catch (e) {
18038
+ throw new ApiError(400, "bad_request", e instanceof Error ? e.message : String(e));
18039
+ }
18040
+ }
18041
+ },
16675
18042
  // ---------- 项目(= 租户)管理:控制台多项目视图 ----------
16676
18043
  // 仅管理员密钥可用;项目级与会话级接口仍用项目自己的 tenant API Key
16677
18044
  // (或管理员密钥 + X-Project-Id 头),互不暴露明文密钥。
@@ -16730,6 +18097,29 @@ function createApp(store, manager, opts = {}) {
16730
18097
  ok(res, nodeLlmView(next));
16731
18098
  }
16732
18099
  },
18100
+ // 大模型账户余额(控制台「余额不足 → 查看余额并充值」用;由节点向 Hub 代查)
18101
+ {
18102
+ method: "GET",
18103
+ pattern: ["node", "llm", "balance"],
18104
+ adminOnly: true,
18105
+ fn: async (_req, res) => {
18106
+ ok(res, await fetchLlmBalance(store));
18107
+ }
18108
+ },
18109
+ // 重新拉取平台内置模型凭据(用户点了「刷新内置模型」,或 Key 在控制台被重置)
18110
+ {
18111
+ method: "POST",
18112
+ pattern: ["node", "llm", "builtin-refresh"],
18113
+ adminOnly: true,
18114
+ fn: async (_req, res) => {
18115
+ const r = await ensureBuiltinProvider(store, { force: true });
18116
+ if (r.status !== "ready") {
18117
+ ok(res, { ok: false, reason: r.reason });
18118
+ return;
18119
+ }
18120
+ ok(res, { ok: true, view: nodeLlmView(store.loadNodeLlm()) });
18121
+ }
18122
+ },
16733
18123
  // ---------- 用量(账本 + 汇总 + 价格) ----------
16734
18124
  {
16735
18125
  method: "GET",
@@ -16881,6 +18271,34 @@ function createApp(store, manager, opts = {}) {
16881
18271
  }
16882
18272
  }
16883
18273
  },
18274
+ // ---------- 节点自更新(状态 / 检查 / 安装) ----------
18275
+ // action=install 先回执、后台安装并退出进程(由服务管理器拉起新版本),
18276
+ // 因此回执成功只代表「已受理」,是否升级成功要用 action=status 再确认。
18277
+ {
18278
+ method: "POST",
18279
+ pattern: ["node", "self-update"],
18280
+ projectOptional: true,
18281
+ fn: async (req2, res, { body }) => {
18282
+ const b = body ?? {};
18283
+ const action = String(b.action ?? "status");
18284
+ if (!updater) {
18285
+ throw new ApiError(400, "bad_request", "\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A\u8282\u70B9\u672A\u4EE5 serve \u6A21\u5F0F\u8FD0\u884C");
18286
+ }
18287
+ if (action === "status") {
18288
+ ok(res, updater.status());
18289
+ return;
18290
+ }
18291
+ if (action === "check") {
18292
+ ok(res, await updater.inspect());
18293
+ return;
18294
+ }
18295
+ if (action === "install") {
18296
+ ok(res, await updater.requestInstall(b.force === true));
18297
+ return;
18298
+ }
18299
+ throw new ApiError(400, "bad_request", `\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
18300
+ }
18301
+ },
16884
18302
  // ---------- 节点级会话状态(跨项目;只含标识与 status) ----------
16885
18303
  // 供节点首页 / 工作台建立初始视图,后续变化由 session_status 通知增量推送。
16886
18304
  {
@@ -17043,7 +18461,7 @@ function createApp(store, manager, opts = {}) {
17043
18461
  // 阻止反向代理(nginx 等)缓冲 SSE 流
17044
18462
  "X-Accel-Buffering": "no"
17045
18463
  });
17046
- const sinceRaw = new URL(
18464
+ const sinceRaw2 = new URL(
17047
18465
  req2.url,
17048
18466
  "http://localhost"
17049
18467
  ).searchParams.get("since");
@@ -17051,19 +18469,51 @@ function createApp(store, manager, opts = {}) {
17051
18469
  tenant.id,
17052
18470
  params["id"],
17053
18471
  res,
17054
- sinceRaw ? Number(sinceRaw) : 0
18472
+ sinceRaw2 ? Number(sinceRaw2) : 0
17055
18473
  );
17056
18474
  return;
17057
18475
  }
17058
- const since = Number(
17059
- new URL(req2.url, "http://localhost").searchParams.get("since") ?? 0
18476
+ const q = new URL(req2.url, "http://localhost").searchParams;
18477
+ const sinceRaw = q.get("since");
18478
+ const beforeRaw = q.get("before");
18479
+ const maxBytesRaw = q.get("maxBytes");
18480
+ ok(
18481
+ res,
18482
+ manager.eventsPage(tenant.id, params["id"], {
18483
+ since: sinceRaw === null ? void 0 : Number(sinceRaw) || 0,
18484
+ before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
18485
+ maxBytes: maxBytesRaw === null ? void 0 : Number(maxBytesRaw) || void 0
18486
+ })
17060
18487
  );
17061
- const { events, latestSeq } = manager.eventsSince(
17062
- tenant.id,
17063
- params["id"],
17064
- since
18488
+ }
18489
+ },
18490
+ {
18491
+ method: "GET",
18492
+ pattern: ["sessions", ":id", "turns"],
18493
+ // 轮次汇总(会话详情的列表)
18494
+ fn: async (req2, res, { tenant, params }) => {
18495
+ const q = new URL(req2.url, "http://localhost").searchParams;
18496
+ const beforeRaw = q.get("before");
18497
+ const limitRaw = q.get("limit");
18498
+ ok(
18499
+ res,
18500
+ await manager.sessionTurns(tenant.id, params["id"], {
18501
+ before: beforeRaw === null ? void 0 : Number(beforeRaw) || 0,
18502
+ limit: limitRaw === null ? void 0 : Number(limitRaw) || void 0
18503
+ })
17065
18504
  );
17066
- ok(res, { events, latestSeq });
18505
+ }
18506
+ },
18507
+ {
18508
+ method: "GET",
18509
+ // 单轮详情:用户消息(含补充) + 最终回复 + 产出文件
18510
+ pattern: ["sessions", ":id", "turns", ":seq", "reply"],
18511
+ fn: async (req2, res, { tenant, params }) => {
18512
+ const startSeq = Number(params["seq"]);
18513
+ if (!Number.isFinite(startSeq) || startSeq <= 0) {
18514
+ throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
18515
+ }
18516
+ ok(res, await manager.sessionTurnReply(tenant.id, params["id"], startSeq));
17067
18517
  }
17068
18518
  }
17069
18519
  ];
@@ -17096,7 +18546,7 @@ function createApp(store, manager, opts = {}) {
17096
18546
  json(res, 200, {
17097
18547
  status: "ok",
17098
18548
  // 版本让运维/探针能确认线上跑的是哪个构建(更新与回滚据此判断)
17099
- version: "0.2.15",
18549
+ version: "0.2.20",
17100
18550
  features: { mcp: hasServerMcpConfig() }
17101
18551
  });
17102
18552
  return;
@@ -17369,6 +18819,24 @@ async function dispatchFleetCommand(deps, env3) {
17369
18819
  switch (env3.type) {
17370
18820
  case "ping":
17371
18821
  return ok2({ pong: true, ts: Date.now() });
18822
+ case "self_update": {
18823
+ const updater = deps.getUpdater?.() ?? null;
18824
+ if (!updater) {
18825
+ 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");
18826
+ }
18827
+ const action = String(payload.action ?? "status");
18828
+ if (action === "status") {
18829
+ return ok2(updater.status());
18830
+ }
18831
+ if (action === "check") {
18832
+ return ok2(await updater.inspect());
18833
+ }
18834
+ if (action === "install") {
18835
+ const started = await updater.requestInstall(payload.force === true);
18836
+ return ok2(started);
18837
+ }
18838
+ return fail(`\u672A\u77E5\u7684 action\uFF1A${action}\uFF08\u53EF\u9009 status / check / install\uFF09`);
18839
+ }
17372
18840
  case "get_node_info":
17373
18841
  return ok2(
17374
18842
  buildNodeInfo(store, {
@@ -17418,6 +18886,22 @@ async function dispatchFleetCommand(deps, env3) {
17418
18886
  store.saveNodeLlm(next);
17419
18887
  return ok2(nodeLlmView(next));
17420
18888
  }
18889
+ /*
18890
+ * 重新拉取平台内置模型(志前 API)。
18891
+ *
18892
+ * 用途:用户在控制台点了「刷新」、或内置模型的 Key 在控制台被重置后需要换新。
18893
+ * 走写链串行——它会改写 connections.json,不能与 update_node_llm 并发。
18894
+ */
18895
+ // 余额查询:只读、幂等,可进只读并发池
18896
+ case "get_llm_balance":
18897
+ return ok2(await fetchLlmBalance(store));
18898
+ case "refresh_builtin_llm": {
18899
+ const r = await ensureBuiltinProvider(store, { force: true });
18900
+ if (r.status !== "ready") {
18901
+ return ok2({ ok: false, reason: r.reason });
18902
+ }
18903
+ return ok2({ ok: true, view: nodeLlmView(store.loadNodeLlm()) });
18904
+ }
17421
18905
  case "get_usage_summary": {
17422
18906
  const ledger = manager.usageLedger();
17423
18907
  if (!ledger) return ok2({ total: null, groups: [] });
@@ -17491,8 +18975,22 @@ async function dispatchFleetCommand(deps, env3) {
17491
18975
  );
17492
18976
  }
17493
18977
  case "get_events": {
17494
- const since = Number(payload.since ?? 0) || 0;
17495
- return ok2(manager.eventsSince(project().id, sessionId(), since));
18978
+ const since = payload.since === void 0 ? void 0 : Number(payload.since) || 0;
18979
+ const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
18980
+ const maxBytes = Number(payload.maxBytes) || void 0;
18981
+ return ok2(
18982
+ manager.eventsPage(project().id, sessionId(), { since, before, maxBytes })
18983
+ );
18984
+ }
18985
+ case "get_session_turns": {
18986
+ const before = payload.before === void 0 ? void 0 : Number(payload.before) || 0;
18987
+ const limit = Number(payload.limit) || void 0;
18988
+ return ok2(await manager.sessionTurns(project().id, sessionId(), { before, limit }));
18989
+ }
18990
+ case "get_session_turn_reply": {
18991
+ const startSeq = Number(payload.startSeq) || 0;
18992
+ if (startSeq <= 0) throw new ApiError(400, "bad_request", "\u65E0\u6548\u7684\u8F6E\u6B21\u5E8F\u53F7");
18993
+ return ok2(await manager.sessionTurnReply(project().id, sessionId(), startSeq));
17496
18994
  }
17497
18995
  case "get_config":
17498
18996
  return ok2(configView2(project()));
@@ -17553,6 +19051,16 @@ async function dispatchFleetCommand(deps, env3) {
17553
19051
  )
17554
19052
  );
17555
19053
  }
19054
+ case "write_file_base64": {
19055
+ const t = project();
19056
+ return ok2(
19057
+ writeProjectFileBase64(
19058
+ t.workspaceRoot ?? null,
19059
+ String(payload.path ?? ""),
19060
+ String(payload.base64 ?? "")
19061
+ )
19062
+ );
19063
+ }
17556
19064
  case "create_dir": {
17557
19065
  const t = project();
17558
19066
  return ok2(createProjectDir(t.workspaceRoot ?? null, String(payload.path ?? "")));
@@ -17567,10 +19075,35 @@ async function dispatchFleetCommand(deps, env3) {
17567
19075
  )
17568
19076
  );
17569
19077
  }
19078
+ case "delete_files": {
19079
+ const t = project();
19080
+ const raw = payload.paths;
19081
+ const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
19082
+ if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
19083
+ return ok2(deleteProjectPaths(t.workspaceRoot ?? null, paths));
19084
+ }
17570
19085
  case "search_files": {
17571
19086
  const t = project();
17572
19087
  return ok2(searchProjectFiles(t.workspaceRoot ?? null, String(payload.query ?? "")));
17573
19088
  }
19089
+ case "rename_path": {
19090
+ const t = project();
19091
+ return ok2(
19092
+ renameProjectPath(
19093
+ t.workspaceRoot ?? null,
19094
+ String(payload.path ?? ""),
19095
+ String(payload.name ?? "")
19096
+ )
19097
+ );
19098
+ }
19099
+ case "zip_download": {
19100
+ const t = project();
19101
+ const raw = payload.paths;
19102
+ const paths = Array.isArray(raw) ? raw.map((x) => String(x)) : [];
19103
+ if (paths.length === 0) throw new ApiError(400, "bad_request", "paths \u4E0D\u80FD\u4E3A\u7A7A");
19104
+ const maxBytes = typeof payload.maxBytes === "number" ? payload.maxBytes : void 0;
19105
+ return ok2(zipProjectPaths(t.workspaceRoot ?? null, paths, { maxBytes }));
19106
+ }
17574
19107
  case "list_tools": {
17575
19108
  const t = project();
17576
19109
  return ok2(buildToolCatalog(t.workspaceRoot ?? process.cwd()));
@@ -17793,6 +19326,13 @@ async function dispatchFleetCommand(deps, env3) {
17793
19326
  manager.steer(project(), sessionId(), text);
17794
19327
  return ok2({ accepted: true });
17795
19328
  }
19329
+ case "unsteer": {
19330
+ const index = Number(payload.index);
19331
+ if (!Number.isInteger(index)) {
19332
+ throw new ApiError(400, "bad_request", "index \u5FC5\u987B\u662F\u6574\u6570");
19333
+ }
19334
+ return ok2({ accepted: true, pendingSteers: manager.unsteer(project(), sessionId(), index) });
19335
+ }
17796
19336
  case "interrupt":
17797
19337
  manager.interrupt(project(), sessionId());
17798
19338
  return ok2({ accepted: true });
@@ -17916,6 +19456,22 @@ function toWireAck(ack) {
17916
19456
  data: ack.data === void 0 ? void 0 : utf8Encode(JSON.stringify(ack.data))
17917
19457
  };
17918
19458
  }
19459
+ var ACK_JSON_MAX_BYTES = MAX_FRAME_BYTES - 64 * 1024;
19460
+ function mb(bytes) {
19461
+ return (bytes / (1024 * 1024)).toFixed(1);
19462
+ }
19463
+ function guardAckSize(ack, json2) {
19464
+ if (!ack.ok || ack.data === void 0) return ack;
19465
+ const bytes = Buffer.byteLength(json2, "utf8");
19466
+ if (bytes <= ACK_JSON_MAX_BYTES) return ack;
19467
+ return {
19468
+ reqId: ack.reqId,
19469
+ type: ack.type,
19470
+ ok: false,
19471
+ // 带上实际大小:用户据此能判断「差一点」还是「差很多」,而不是只看到「失败」
19472
+ 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`
19473
+ };
19474
+ }
17919
19475
  function fromWireCommand(env3) {
17920
19476
  let payload = {};
17921
19477
  if (env3.payload && env3.payload.length > 0) {
@@ -18321,6 +19877,7 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
18321
19877
  "get_node_llm",
18322
19878
  "list_node_llm_models",
18323
19879
  "get_usage_summary",
19880
+ "get_llm_balance",
18324
19881
  "get_usage_records",
18325
19882
  "get_usage_pricing",
18326
19883
  "list_projects",
@@ -18328,6 +19885,8 @@ var READONLY_CMD_TYPES = /* @__PURE__ */ new Set([
18328
19885
  "get_session_states",
18329
19886
  "get_state",
18330
19887
  "get_events",
19888
+ "get_session_turns",
19889
+ "get_session_turn_reply",
18331
19890
  "get_config",
18332
19891
  "list_models",
18333
19892
  "list_dirs",
@@ -18577,6 +20136,10 @@ var FleetNodeBridge = class {
18577
20136
  _dispatchEnvelope(env3) {
18578
20137
  const run = () => this._dispatch(env3);
18579
20138
  dbgMark(`recv:${env3.type}`, `reqId=${env3.reqId}`);
20139
+ if (env3.type === "self_update" && String(env3.payload?.action ?? "status") !== "install") {
20140
+ this._runReadonly(run);
20141
+ return;
20142
+ }
18580
20143
  if (READONLY_CMD_TYPES.has(env3.type) || this._isReadonlyBatch(env3)) {
18581
20144
  this._runReadonly(run);
18582
20145
  return;
@@ -18603,7 +20166,7 @@ var FleetNodeBridge = class {
18603
20166
  _invalidateSessionCache() {
18604
20167
  this.writeEpoch += 1;
18605
20168
  for (const key of [...this.cmdResultCache.keys()]) {
18606
- if (/^(get_state|get_events|list_sessions|get_session_states)\|/.test(key)) {
20169
+ if (/^(get_state|get_events|get_session_turns|get_session_turn_reply|list_sessions|get_session_states)\|/.test(key)) {
18607
20170
  this.cmdResultCache.delete(key);
18608
20171
  }
18609
20172
  }
@@ -18739,11 +20302,17 @@ var FleetNodeBridge = class {
18739
20302
  label: this.opts.deps.store.loadNodeLabel() ?? void 0,
18740
20303
  platform: process.platform,
18741
20304
  mode: process.env.NODE_MODE ?? "standalone",
18742
- version: "0.2.15",
20305
+ version: "0.2.20",
18743
20306
  ip: primaryIp()
18744
20307
  };
18745
20308
  }
18746
- /** 用量汇总(供控制面账户级聚合):总量 + 最近 40 天按天 */
20309
+ /**
20310
+ * 用量汇总(供控制面账户级聚合):总量 + 最近 40 天按天 + 最近 40 天按项目。
20311
+ *
20312
+ * 按项目这一维是为控制台「今日 / 最近 30 天消耗拆到每个项目」服务的:
20313
+ * 账本里的汇总只存了按天、按项目两个独立维度,交叉项只能现算,因此这里
20314
+ * 从明细聚合后随 info 一起上报(Hub 按 (节点, 项目, 天) 覆盖写入,天然幂等)。
20315
+ */
18747
20316
  _usagePayload() {
18748
20317
  const ledger = this.opts.deps.manager.usageLedger();
18749
20318
  if (!ledger) return void 0;
@@ -18751,11 +20320,22 @@ var FleetNodeBridge = class {
18751
20320
  const days = {};
18752
20321
  const keys = Object.keys(r.byDay).sort().slice(-40);
18753
20322
  for (const k of keys) days[k] = r.byDay[k];
20323
+ const from = keys[0] ?? "";
20324
+ const to = keys[keys.length - 1] ?? "";
20325
+ const projects = {};
20326
+ if (from && to) {
20327
+ for (const [projectId, byDay] of Object.entries(
20328
+ ledger.projectByDay({ from, to })
20329
+ )) {
20330
+ projects[projectId] = { total: sumAgg(Object.values(byDay)), byDay };
20331
+ }
20332
+ }
18754
20333
  return {
18755
20334
  since: r.since,
18756
20335
  total: r.total,
18757
20336
  legacy: r.legacy ?? null,
18758
- byDay: days
20337
+ byDay: days,
20338
+ projects
18759
20339
  };
18760
20340
  }
18761
20341
  /**
@@ -18790,6 +20370,8 @@ var FleetNodeBridge = class {
18790
20370
  name: s.name ?? null,
18791
20371
  status: s.status,
18792
20372
  lastUserMessage: lastUserMessage(s),
20373
+ // 控制面按它排「最近在跑什么」,运行中的长任务才不会被排到后面
20374
+ lastStartedAt: s.lastStartedAt ?? null,
18793
20375
  // 控制面的聚合邮件靠它判断「这台机器最近有没有在干活」;
18794
20376
  // 状态跃迁可能被 info 节流跳过,完成时间不会。
18795
20377
  lastCompletedAt: s.lastCompletedAt ?? null
@@ -18818,7 +20400,7 @@ var FleetNodeBridge = class {
18818
20400
  label: this.opts.deps.store.loadNodeLabel() ?? void 0,
18819
20401
  platform: process.platform,
18820
20402
  mode: process.env.NODE_MODE ?? "standalone",
18821
- version: "0.2.15",
20403
+ version: "0.2.20",
18822
20404
  ip: primaryIp(),
18823
20405
  projects,
18824
20406
  totals,
@@ -18980,7 +20562,13 @@ var FleetNodeBridge = class {
18980
20562
  const stamped = { ...ack, reqId: env3.reqId, type: env3.type };
18981
20563
  const json2 = JSON.stringify(stamped);
18982
20564
  const __t1 = dbgNow();
18983
- this._send({ kind: "ack", clientId: env3.clientId, ack: stamped });
20565
+ const payload = guardAckSize(stamped, json2);
20566
+ if (payload !== stamped) {
20567
+ console.warn(
20568
+ `[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`
20569
+ );
20570
+ }
20571
+ this._send({ kind: "ack", clientId: env3.clientId, ack: payload });
18984
20572
  dbgMs(
18985
20573
  "publishAck",
18986
20574
  __t0,
@@ -20216,7 +21804,7 @@ import { createHash as createHash3 } from "crypto";
20216
21804
  import * as fs27 from "fs";
20217
21805
  import { Readable as Readable2 } from "stream";
20218
21806
  import { pipeline as pipeline2 } from "stream/promises";
20219
- import { spawnSync as spawnSync4 } from "child_process";
21807
+ import { spawnSync as spawnSync5 } from "child_process";
20220
21808
  import * as path36 from "path";
20221
21809
 
20222
21810
  // src/update/manifest.ts
@@ -20359,7 +21947,7 @@ function verifySignature(data, signatureBase64, publicKey) {
20359
21947
  }
20360
21948
 
20361
21949
  // src/update/downloader.ts
20362
- var MAX_BINARY_BYTES = 400 * 1024 * 1024;
21950
+ var MAX_BINARY_BYTES2 = 400 * 1024 * 1024;
20363
21951
  var MANIFEST_TIMEOUT_MS = 15e3;
20364
21952
  var DOWNLOAD_TIMEOUT_MS2 = 10 * 6e4;
20365
21953
  async function fetchManifest(url, publicKey, opts = {}) {
@@ -20405,7 +21993,7 @@ async function downloadBinary(bin, destPath, log = () => {
20405
21993
  if (!res.ok) return { error: `\u4E0B\u8F7D\u5931\u8D25 HTTP ${res.status}` };
20406
21994
  if (!res.body) return { error: "\u4E0B\u8F7D\u54CD\u5E94\u6CA1\u6709 body" };
20407
21995
  const expected = bin.size && bin.size > 0 ? bin.size : void 0;
20408
- const limit = expected ? Math.max(expected * 2, 1024 * 1024) : MAX_BINARY_BYTES;
21996
+ const limit = expected ? Math.max(expected * 2, 1024 * 1024) : MAX_BINARY_BYTES2;
20409
21997
  const hash = createHash3("sha256");
20410
21998
  let bytes = 0;
20411
21999
  try {
@@ -20443,7 +22031,7 @@ async function downloadBinary(bin, destPath, log = () => {
20443
22031
  }
20444
22032
  function selfCheck(binPath, expectedVersion) {
20445
22033
  try {
20446
- const r = spawnSync4(binPath, ["--version"], {
22034
+ const r = spawnSync5(binPath, ["--version"], {
20447
22035
  timeout: 2e4,
20448
22036
  encoding: "utf-8",
20449
22037
  env: { ...process.env, ZQAGENT_AUTO_UPDATE: "0" }
@@ -20612,6 +22200,31 @@ function nextIntervalMs() {
20612
22200
  const jitter = base * 0.15 * (Math.random() * 2 - 1);
20613
22201
  return Math.max(6e4, Math.round(base + jitter));
20614
22202
  }
22203
+ function evaluateManifest(base, manifest) {
22204
+ if (!parseVersion(base.currentVersion) || compareVersions(manifest.version, base.currentVersion) <= 0) {
22205
+ return {
22206
+ ...base,
22207
+ updateAvailable: false,
22208
+ latestVersion: manifest.version,
22209
+ reason: `\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF08v${base.currentVersion}\uFF09`
22210
+ };
22211
+ }
22212
+ if (manifest.minimumVersion && compareVersions(base.currentVersion, manifest.minimumVersion) < 0) {
22213
+ return {
22214
+ ...base,
22215
+ updateAvailable: false,
22216
+ latestVersion: manifest.version,
22217
+ manualUpgradeRequired: manifest.minimumVersion,
22218
+ reason: `\u5F53\u524D\u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u624B\u52A8\u5347\u7EA7\u5230 >= ${manifest.minimumVersion}`
22219
+ };
22220
+ }
22221
+ return {
22222
+ ...base,
22223
+ updateAvailable: true,
22224
+ latestVersion: manifest.version,
22225
+ ...manifest.notes ? { notes: manifest.notes } : {}
22226
+ };
22227
+ }
20615
22228
  var UpdateController = class {
20616
22229
  constructor(opts, currentVersion2 = currentVersion()) {
20617
22230
  this.opts = opts;
@@ -20700,6 +22313,77 @@ var UpdateController = class {
20700
22313
  }
20701
22314
  this.schedule(nextIntervalMs());
20702
22315
  }
22316
+ /**
22317
+ * 只读检查:拉清单比对版本,不下载、不改 state。
22318
+ *
22319
+ * 供远端 `self_update --action check` 与 UI 展示用;与定时器 `check()` 不同,
22320
+ * 这里不加 `checking` 锁、不写 `lastCheckAt`,因此不会干扰自动更新流程。
22321
+ */
22322
+ async inspect() {
22323
+ const base = {
22324
+ currentVersion: this.currentVersion,
22325
+ channel: updaterChannel(),
22326
+ manifestUrl: this.manifestUrl()
22327
+ };
22328
+ const key = this.publicKey;
22329
+ const cap = selfUpdateCapability(Boolean(key));
22330
+ if (!cap.ok) {
22331
+ return { ...base, updateAvailable: false, reason: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
22332
+ }
22333
+ const res = await fetchManifest(base.manifestUrl, key, {
22334
+ allowInsecureUrl: env("ZQAGENT_UPDATE_INSECURE") === "1"
22335
+ });
22336
+ if (!res.manifest) {
22337
+ return {
22338
+ ...base,
22339
+ updateAvailable: false,
22340
+ reason: `\u66F4\u65B0\u68C0\u67E5\u5931\u8D25\uFF1A${res.error ?? "\u6E05\u5355\u6821\u9A8C\u5931\u8D25"}`
22341
+ };
22342
+ }
22343
+ return evaluateManifest(base, res.manifest);
22344
+ }
22345
+ /**
22346
+ * 远端触发安装:先同步给出结论(受理/无需更新),安装本身在后台跑。
22347
+ *
22348
+ * 必须「先回执、后安装」:安装成功后进程会 `exit(70)` 交给 systemd 拉起新版本,
22349
+ * 如果等到安装完成再回执,回执永远发不出去(表现为网关超时)。
22350
+ *
22351
+ * 复用定时器同一条 `check()` 路径:验签、下载、自检、失败计数、安装后自检与
22352
+ * 回滚保护全都一致,只是把 `installMode` 设为 `force` 时不等待空闲。
22353
+ */
22354
+ async requestInstall(force = false) {
22355
+ const fromVersion = this.currentVersion;
22356
+ const cap = selfUpdateCapability(Boolean(this.publicKey));
22357
+ if (!cap.ok) {
22358
+ return { accepted: false, fromVersion, message: `\u81EA\u66F4\u65B0\u4E0D\u53EF\u7528\uFF1A${cap.reason}` };
22359
+ }
22360
+ const info = await this.inspect();
22361
+ if (!info.updateAvailable) {
22362
+ return {
22363
+ accepted: false,
22364
+ fromVersion,
22365
+ ...info.latestVersion ? { toVersion: info.latestVersion } : {},
22366
+ message: info.reason ?? "\u5DF2\u662F\u6700\u65B0\u7248\u672C"
22367
+ };
22368
+ }
22369
+ if (this.checking || this.installing) {
22370
+ return {
22371
+ accepted: true,
22372
+ fromVersion,
22373
+ toVersion: info.latestVersion,
22374
+ 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`
22375
+ };
22376
+ }
22377
+ void this.check(force, false, true).catch((err) => {
22378
+ this.recordFailure(info.latestVersion, `\u5B89\u88C5\u5931\u8D25\uFF1A${err.message}`);
22379
+ });
22380
+ return {
22381
+ accepted: true,
22382
+ fromVersion,
22383
+ toVersion: info.latestVersion,
22384
+ 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`
22385
+ };
22386
+ }
20703
22387
  /**
20704
22388
  * 检查并准备更新;`installMode` 为 true 时下载后立即安装(不再等空闲)。
20705
22389
  * 返回可读结论,供 CLI `update --check` 使用。
@@ -20928,7 +22612,9 @@ function seedDemoTenant(store) {
20928
22612
  // 未显式指定时随机生成:固定默认密钥一旦随 HOST=0.0.0.0 暴露到公网,等于公开凭据。
20929
22613
  // 只在首次创建时生成并落盘,后续启动沿用磁盘上的值(启动日志会打印完整密钥)。
20930
22614
  apiKey: env2("TENANT_API_KEY") ?? generateApiKey(),
20931
- model: env2("MODEL") ?? env2("OPENAI_MODEL") ?? "deepseek-v4-flash",
22615
+ // 不再硬编码具体厂商型号:志前 API 只认 deepseek-flash,写死 deepseek-v4-flash
22616
+ // 会让新装节点的第一次请求 404。默认值统一走 defaultTenantModel(见 nodeLlm.ts)。
22617
+ model: defaultTenantModel(store),
20932
22618
  provider: {
20933
22619
  apiKey: env2("OPENAI_API_KEY"),
20934
22620
  baseURL: env2("OPENAI_BASE_URL")
@@ -21068,6 +22754,8 @@ function startServer(opts = {}) {
21068
22754
  const store = new Store(dataDir);
21069
22755
  const demo = seedDemoTenant(store);
21070
22756
  syncTenantModels(store);
22757
+ const nodeModel = nodeDefaultProvider(store)?.model;
22758
+ if (nodeModel) adoptBuiltinModelForTenants(store, nodeModel);
21071
22759
  const usageLedger = new UsageLedger(dataDir);
21072
22760
  usageLedger.init();
21073
22761
  const manager = new SessionManager(store, void 0, { usage: usageLedger });
@@ -21099,6 +22787,7 @@ function startServer(opts = {}) {
21099
22787
  const notificationHub = createNotificationHub();
21100
22788
  let fleet = null;
21101
22789
  let fleetCred = null;
22790
+ let updater = null;
21102
22791
  function connectFleet(cred) {
21103
22792
  const localId = ensureNodeId(store);
21104
22793
  const nodeId = (cred.nodeId ?? "").trim() || localId;
@@ -21116,6 +22805,8 @@ function startServer(opts = {}) {
21116
22805
  deps: {
21117
22806
  store,
21118
22807
  manager,
22808
+ // 闭包推迟求值:桥先于 UpdateController 创建,指令到达时 updater 已就绪
22809
+ getUpdater: () => updater,
21119
22810
  applyFleetCred: async (cred2) => {
21120
22811
  store.saveFleetCredential(cred2);
21121
22812
  setTimeout(() => connectFleet(cred2), 300);
@@ -21208,8 +22899,12 @@ function startServer(opts = {}) {
21208
22899
  if (initialCred) {
21209
22900
  connectFleet(initialCred);
21210
22901
  } else if (opts.autoEnroll) {
21211
- void fleetController.enroll(opts.autoEnroll).then((st) => console.log(`[fleet] \u5DF2\u81EA\u52A8\u63A5\u5165\uFF0C\u8282\u70B9 ${st.nodeId}\uFF08${st.gatewayUrl}\uFF09`)).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}`));
22902
+ void fleetController.enroll(opts.autoEnroll).then((st) => {
22903
+ console.log(`[fleet] \u5DF2\u81EA\u52A8\u63A5\u5165\uFF0C\u8282\u70B9 ${st.nodeId}\uFF08${st.gatewayUrl}\uFF09`);
22904
+ scheduleBuiltinProvider(store, (m) => console.log(m));
22905
+ }).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}`));
21212
22906
  }
22907
+ if (initialCred) scheduleBuiltinProvider(store, (m) => console.log(m));
21213
22908
  const pairingCodes = new PairingCodeStore();
21214
22909
  const qq = new QqManager({
21215
22910
  store,
@@ -21266,16 +22961,21 @@ function startServer(opts = {}) {
21266
22961
  scheduleFleetInfoRefresh();
21267
22962
  }
21268
22963
  });
22964
+ updater = new UpdateController({
22965
+ dataDir,
22966
+ isIdle: () => manager.isIdle(),
22967
+ prepareExit: closeServer
22968
+ });
21269
22969
  const server = createApp(store, manager, {
21270
22970
  secretBox,
21271
22971
  notificationHub,
21272
22972
  qq,
21273
22973
  adminKey,
21274
22974
  fleet: fleetController,
21275
- host
22975
+ host,
22976
+ updater
21276
22977
  });
21277
22978
  let closed = false;
21278
- let updater = null;
21279
22979
  function closeServer() {
21280
22980
  if (closed) return;
21281
22981
  closed = true;
@@ -21308,13 +23008,6 @@ function startServer(opts = {}) {
21308
23008
  }
21309
23009
  process.on("SIGINT", () => shutdown("SIGINT"));
21310
23010
  process.on("SIGTERM", () => shutdown("SIGTERM"));
21311
- if (opts.autoUpdate) {
21312
- updater = new UpdateController({
21313
- dataDir,
21314
- isIdle: () => manager.isIdle(),
21315
- prepareExit: closeServer
21316
- });
21317
- }
21318
23011
  server.timeout = 0;
21319
23012
  server.keepAliveTimeout = 65e3;
21320
23013
  server.headersTimeout = 66e3;
@@ -21334,7 +23027,7 @@ function startServer(opts = {}) {
21334
23027
  } else {
21335
23028
  printReady(info, qq);
21336
23029
  }
21337
- updater?.start();
23030
+ if (opts.autoUpdate) updater?.start();
21338
23031
  resolve25(info);
21339
23032
  }, reject);
21340
23033
  });
@@ -22019,7 +23712,7 @@ function cycleMode(engine) {
22019
23712
  engine.permissions.mode = next;
22020
23713
  return next;
22021
23714
  }
22022
- var rl = readline3.createInterface({
23715
+ var rl = readline4.createInterface({
22023
23716
  input: process.stdin,
22024
23717
  output: process.stdout
22025
23718
  });
@@ -22265,7 +23958,10 @@ async function connectWizard() {
22265
23958
  break;
22266
23959
  }
22267
23960
  cliLog(`\u274C \u81EA\u68C0\u5931\u8D25: ${probe.error}${probe.code ? ` | ${probe.code}` : ""}`);
22268
- for (const hint of paramErrorHints(probe.error ?? "", probe.code ?? "")) {
23961
+ for (const hint of [
23962
+ ...authErrorHints(probe.error ?? "", probe.code ?? ""),
23963
+ ...paramErrorHints(probe.error ?? "", probe.code ?? "")
23964
+ ]) {
22269
23965
  cliLog(` \u{1F4A1} ${hint}`);
22270
23966
  }
22271
23967
  const keep = (await question("\u91CD\u65B0\u9009\u62E9\u6A21\u578B (\u56DE\u8F66\u91CD\u9009) / \u8F93\u5165 y \u4ECD\u4F7F\u7528\u8BE5\u6A21\u578B: ")).trim().toLowerCase();
@@ -22499,7 +24195,7 @@ async function offerTerminalSetup(engine, canUseAgent) {
22499
24195
  }
22500
24196
  var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["link", "serve", "update", "rename"]);
22501
24197
  function cliVersion() {
22502
- const env3 = "0.2.15"?.trim();
24198
+ const env3 = "0.2.20"?.trim();
22503
24199
  if (env3) return env3;
22504
24200
  try {
22505
24201
  const req2 = createRequire4(import.meta.url);
@@ -22762,8 +24458,8 @@ async function main() {
22762
24458
 
22763
24459
  \u{1F4CB} \u4F1A\u8BDDID: ${SESSION_ID}`);
22764
24460
  cliLog(`\u{1F4BE} \u72B6\u6001\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u901A\u8FC7\u4EE5\u4E0B\u547D\u4EE4\u6062\u590D:`);
22765
- cliLog(` tsx src/cli/index.ts --resume ${SESSION_ID}`);
22766
- cliLog(` \u6216 tsx src/cli/index.ts --r ${SESSION_ID}
24461
+ cliLog(` zqagent --resume ${SESSION_ID}`);
24462
+ cliLog(` \u6216 zqagent --r ${SESSION_ID}
22767
24463
  `);
22768
24464
  rl.close();
22769
24465
  process.exit(0);
@@ -23012,6 +24708,12 @@ async function main() {
23012
24708
  cliLog(`\u274C \u672A\u627E\u5230\u4F9B\u5E94\u5546: ${arg}`);
23013
24709
  break;
23014
24710
  }
24711
+ if (target.id === BUILTIN_PROVIDER_ID) {
24712
+ cliLog(
24713
+ "\u274C \u5E73\u53F0\u5185\u7F6E\u6A21\u578B\u4E0D\u53EF\u5220\u9664\uFF08\u63A5\u5165 Hub \u540E\u81EA\u52A8\u914D\u7F6E\uFF0CKey \u7531\u5E73\u53F0\u6258\u7BA1\uFF09\u3002\n \u5982\u9700\u6539\u7528\u5176\u5B83\u6A21\u578B\uFF1A/connect \u9009\u62E9\u6216\u6DFB\u52A0\u4F9B\u5E94\u5546\u540E\u8BBE\u4E3A\u9ED8\u8BA4\u3002"
24714
+ );
24715
+ break;
24716
+ }
23015
24717
  if (removeConnection(target.id)) {
23016
24718
  currentConnection = loadConnection();
23017
24719
  engine = buildEngine(SESSION_ID);