@eddyskywalker/dsh-chatgpt-subscription 0.44.0 → 0.45.0

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/client.js +13 -0
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2100 -10
  5. package/lib/types/compat.d.ts +1 -1
  6. package/lib/types/host/cache-warmup/budget.d.ts +64 -0
  7. package/lib/types/host/cache-warmup/budget.d.ts.map +1 -0
  8. package/lib/types/host/cache-warmup/contracts.d.ts +267 -0
  9. package/lib/types/host/cache-warmup/contracts.d.ts.map +1 -0
  10. package/lib/types/host/cache-warmup/index.d.ts +21 -0
  11. package/lib/types/host/cache-warmup/index.d.ts.map +1 -0
  12. package/lib/types/host/cache-warmup/install.d.ts +130 -0
  13. package/lib/types/host/cache-warmup/install.d.ts.map +1 -0
  14. package/lib/types/host/cache-warmup/logging.d.ts +120 -0
  15. package/lib/types/host/cache-warmup/logging.d.ts.map +1 -0
  16. package/lib/types/host/cache-warmup/projection.d.ts +29 -0
  17. package/lib/types/host/cache-warmup/projection.d.ts.map +1 -0
  18. package/lib/types/host/cache-warmup/result-tool.d.ts +87 -0
  19. package/lib/types/host/cache-warmup/result-tool.d.ts.map +1 -0
  20. package/lib/types/host/cache-warmup/settings.d.ts +73 -0
  21. package/lib/types/host/cache-warmup/settings.d.ts.map +1 -0
  22. package/lib/types/host/cache-warmup/store.d.ts +108 -0
  23. package/lib/types/host/cache-warmup/store.d.ts.map +1 -0
  24. package/lib/types/host/cache-warmup/usage.d.ts +86 -0
  25. package/lib/types/host/cache-warmup/usage.d.ts.map +1 -0
  26. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  27. package/lib/types/index.d.ts +11 -0
  28. package/lib/types/index.d.ts.map +1 -1
  29. package/lib/types/shared/model-catalog.d.ts +12 -1
  30. package/lib/types/shared/model-catalog.d.ts.map +1 -1
  31. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -141,7 +141,7 @@ const ROUTE_PREFIX$4 = "/api/dsh-chatgpt-subscription";
141
141
  * `test/responses-client.test.ts` asserts the two agree, so a release that
142
142
  * forgets this constant fails the suite instead of shipping quietly.
143
143
  */
144
- const PLUGIN_VERSION = "0.44.0";
144
+ const PLUGIN_VERSION = "0.45.0";
145
145
  const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
146
146
  const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
147
147
  const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
@@ -221,6 +221,18 @@ const CODEX_MODEL_CATALOG = [
221
221
  supportsOutputVerbosity: true,
222
222
  defaultOutputVerbosity: "low"
223
223
  },
224
+ {
225
+ id: "gpt-6.1-terra",
226
+ name: "6.1 Terra",
227
+ contextWindow: GPT_6_DEFAULT_CONTEXT_WINDOW,
228
+ inputModalities: ["text", "image"],
229
+ defaultReasoningEffort: "medium",
230
+ reasoningProfile: "gpt-6",
231
+ supportsReasoningSummary: true,
232
+ maxTokens: GPT_6_MAX_TOKENS,
233
+ supportsOutputVerbosity: true,
234
+ defaultOutputVerbosity: "low"
235
+ },
224
236
  {
225
237
  id: "gpt-6-sol",
226
238
  name: "6 Sol",
@@ -312,6 +324,7 @@ const CODEX_MODEL_CATALOG = [
312
324
  const DEFAULT_VISIBLE_CODEX_MODEL_IDS = [
313
325
  "gpt-6-astra",
314
326
  "gpt-6.1-sol",
327
+ "gpt-6.1-terra",
315
328
  "gpt-6-sol",
316
329
  "gpt-6-luna",
317
330
  "gpt-5.6-sol",
@@ -24670,6 +24683,1327 @@ var OllamaAccountPool = class extends AccountPoolCore {
24670
24683
  }
24671
24684
  };
24672
24685
  //#endregion
24686
+ //#region src/host/cache-warmup/contracts.ts
24687
+ /**
24688
+ * 缓存预热子包(cache-warmup)的共享契约:类型、默认值与错误码。
24689
+ *
24690
+ * 这一层刻意不 import 任何 `node:` 内置模块:投影与预算的一半逻辑是纯字符串/数字运算,
24691
+ * 保持零依赖才能让消费者(宿主适配层、设置路由、测试)只为了一个类型就拖进文件系统实现。
24692
+ * 唯一的例外是 `parseStructuredResultRows` —— 它是对**已存储原文**的解析器,
24693
+ * 存储分页与投影预览必须共用同一份判定,否则「能分页但预览判断不同」会立刻变成幻觉。
24694
+ *
24695
+ * @module dsh-chatgpt-subscription/host/cache-warmup/contracts
24696
+ */
24697
+ /**
24698
+ * 子包默认值。
24699
+ *
24700
+ * 预热期的字符上限是**按 token 反推的保守字符预算**:中文/代码混排大约每 token 1.6-2.0
24701
+ * 字符,1200 字符落在 512-1024 token 区间的中段,宁可少给也不要在预热期用一个
24702
+ * 还没被验证过的更大预算去撞缓存键。命中率与连续次数见约束 4。
24703
+ */
24704
+ const CACHE_WARMUP_DEFAULTS = {
24705
+ /** 预热期单次预览的字符预算。约 600-750 tokens。 */
24706
+ warmupMaxChars: 1200,
24707
+ /** 稳态(连续两次高命中率且无 drift 之后)的字符预算。 */
24708
+ steadyStateMaxChars: 8e3,
24709
+ /** 视为命中的最低命中率。 */
24710
+ hitRatioThreshold: .97,
24711
+ /** 退出预热期需要连续命中的报告条数。 */
24712
+ consecutiveHitsRequired: 2,
24713
+ /** 单结果原文上限(字节)。 */
24714
+ maxResultBytes: 8 * 1024 * 1024,
24715
+ /** 每 session 原文总量上限(字节)。 */
24716
+ maxSessionBytes: 64 * 1024 * 1024,
24717
+ /** 分页 limit 默认值(行)。 */
24718
+ pageLimitDefault: 64,
24719
+ /** 分页 limit 上限(行)。 */
24720
+ pageLimitMax: 512,
24721
+ /** 分页 maxBytes 默认值(字节)。 */
24722
+ pageMaxBytesDefault: 16 * 1024,
24723
+ /** 分页 maxBytes 上限(字节)。 */
24724
+ pageMaxBytesMax: 64 * 1024,
24725
+ /** usage registry 的 session 数上限。 */
24726
+ maxUsageSessions: 256,
24727
+ /** 每 session 保留的用量报告条数上限。 */
24728
+ maxUsageReportsPerSession: 32
24729
+ };
24730
+ /**
24731
+ * 存储层错误。信息里只描述**请求本身**的问题,绝不回显其他 session 的内容或存在性。
24732
+ */
24733
+ var CacheWarmupStoreError = class extends Error {
24734
+ code;
24735
+ constructor(message, code) {
24736
+ super(message);
24737
+ this.code = code;
24738
+ this.name = "CacheWarmupStoreError";
24739
+ }
24740
+ };
24741
+ /** 一段文本被拆开的一列。 */
24742
+ function splitDelimitedLine(line, delimiter) {
24743
+ const cells = [];
24744
+ let current = "";
24745
+ let quoted = false;
24746
+ for (let index = 0; index < line.length; index += 1) {
24747
+ const char = line[index];
24748
+ if (char === "\"") {
24749
+ if (quoted && line[index + 1] === "\"") {
24750
+ current += "\"";
24751
+ index += 1;
24752
+ } else quoted = !quoted;
24753
+ continue;
24754
+ }
24755
+ if (!quoted && char === delimiter) {
24756
+ cells.push(current);
24757
+ current = "";
24758
+ continue;
24759
+ }
24760
+ current += char;
24761
+ }
24762
+ cells.push(current);
24763
+ return cells;
24764
+ }
24765
+ /** 把任意 JSON 值压成单元格文本;对象/数组保持可读的 JSON。 */
24766
+ function cellOf(value) {
24767
+ if (value === null || value === void 0) return "";
24768
+ if (typeof value === "string") return value;
24769
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
24770
+ try {
24771
+ return JSON.stringify(value) ?? "";
24772
+ } catch {
24773
+ return "";
24774
+ }
24775
+ }
24776
+ /** JSON Lines:每一行都是一个对象,且至少两行。 */
24777
+ function parseJsonLines(lines) {
24778
+ if (lines.length < 2) return void 0;
24779
+ const fields = [];
24780
+ const seen = /* @__PURE__ */ new Set();
24781
+ const rows = [];
24782
+ for (const line of lines) {
24783
+ let parsed;
24784
+ try {
24785
+ parsed = JSON.parse(line);
24786
+ } catch {
24787
+ return;
24788
+ }
24789
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
24790
+ const record = parsed;
24791
+ const row = {};
24792
+ for (const [key, value] of Object.entries(record)) {
24793
+ if (!seen.has(key)) {
24794
+ seen.add(key);
24795
+ fields.push(key);
24796
+ }
24797
+ row[key] = cellOf(value);
24798
+ }
24799
+ rows.push(row);
24800
+ }
24801
+ if (fields.length === 0) return void 0;
24802
+ return {
24803
+ fields,
24804
+ rows
24805
+ };
24806
+ }
24807
+ /** 分隔符表格:首行表头 + 至少两行同列数的数据。 */
24808
+ function parseDelimitedRows(lines) {
24809
+ if (lines.length < 3) return void 0;
24810
+ const header = lines[0];
24811
+ const delimiter = header.includes(" ") ? " " : header.includes(",") ? "," : void 0;
24812
+ if (delimiter === void 0) return void 0;
24813
+ const fields = splitDelimitedLine(header, delimiter).map((cell) => cell.trim());
24814
+ if (fields.length < 2 || fields.some((field) => field === "")) return void 0;
24815
+ if (new Set(fields).size !== fields.length) return void 0;
24816
+ const rows = [];
24817
+ for (const line of lines.slice(1)) {
24818
+ const cells = splitDelimitedLine(line, delimiter);
24819
+ if (cells.length !== fields.length) return void 0;
24820
+ const row = {};
24821
+ fields.forEach((field, index) => {
24822
+ row[field] = cells[index];
24823
+ });
24824
+ rows.push(row);
24825
+ }
24826
+ return rows.length === 0 ? void 0 : {
24827
+ fields,
24828
+ rows
24829
+ };
24830
+ }
24831
+ /**
24832
+ * 把一段文本可靠地解析成结构化行;解析不可靠就返回 undefined。
24833
+ *
24834
+ * 判定刻意保守:宁可返回 undefined 让调用方保持原文,也不要猜出一张表,
24835
+ * 因为下游会把「猜出来的表」当成完整事实。
24836
+ */
24837
+ function parseStructuredResultRows(text) {
24838
+ const lines = text.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
24839
+ if (lines.length < 2) return void 0;
24840
+ return parseJsonLines(lines) ?? parseDelimitedRows(lines);
24841
+ }
24842
+ //#endregion
24843
+ //#region src/host/cache-warmup/store.ts
24844
+ /**
24845
+ * 结果引用存储:内容寻址、原子写入、按字节安全分页。
24846
+ *
24847
+ * 三条不变量:
24848
+ *
24849
+ * 1. **原文逐字保存**。文件名是原文 UTF-8 字节的 sha256,所以同一份内容重复入库是幂等的,
24850
+ * 也不可能有一条引用悄悄指向别的字节。
24851
+ * 2. **历史 preview 与存储生命周期无关**。过期/淘汰/释放只影响「还能不能读到原文」,
24852
+ * 已经写进历史的那段预览文本是常量 —— 本模块从不接触历史。
24853
+ * 3. **只删具体文件**。淘汰按 mtime 逐个 `rm` 文件,绝不递归删除任何目录。
24854
+ *
24855
+ * @module dsh-chatgpt-subscription/host/cache-warmup/store
24856
+ */
24857
+ /** 会话 id 的合法形状:单个路径段,不允许分隔符与 `..`。 */
24858
+ const SESSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
24859
+ /** `ref://<session>/<sha256>`。 */
24860
+ const REFERENCE_PATTERN = /^ref:\/\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})\/([0-9a-f]{64})$/;
24861
+ /** 引用前缀。 */
24862
+ const RESULT_REFERENCE_PREFIX = "ref://";
24863
+ /** 原文文件后缀。 */
24864
+ const RESULT_SUFFIX = ".txt";
24865
+ /** 临时文件标记(原子写入的中转,永不被分页读到)。 */
24866
+ const TMP_MARKER = ".tmp.";
24867
+ /**
24868
+ * 拼一条引用。digest 必须是 64 位小写十六进制,否则分页时会拒绝解析。
24869
+ * @param sessionId - 会话 id。
24870
+ * @param digest - 原文 sha256。
24871
+ * @returns `ref://<session>/<digest>`。
24872
+ */
24873
+ function formatResultRefId(sessionId, digest) {
24874
+ return `${RESULT_REFERENCE_PREFIX}${sessionId}/${digest}`;
24875
+ }
24876
+ /**
24877
+ * 解析一条引用。
24878
+ * @param refId - 待解析的引用。
24879
+ * @returns 会话与 digest;形状不合法时 undefined。
24880
+ */
24881
+ function parseResultRefId(refId) {
24882
+ if (typeof refId !== "string") return void 0;
24883
+ const match = REFERENCE_PATTERN.exec(refId);
24884
+ if (match === null) return void 0;
24885
+ return {
24886
+ sessionId: match[1],
24887
+ digest: match[2]
24888
+ };
24889
+ }
24890
+ /**
24891
+ * 默认根目录。
24892
+ *
24893
+ * 每次调用都重新解析 `DSH_HOME`,所以构造之后再改环境变量(测试、多 profile)
24894
+ * 仍然生效;这也是零参构造能安全地只拿一个惰性默认值的原因。
24895
+ * @returns 默认根目录的绝对路径。
24896
+ */
24897
+ function defaultResultStoreRootDir() {
24898
+ return path.join(dshHomeDir(), "storages", "cache-warmup");
24899
+ }
24900
+ /** 校验会话 id 形状,非法直接拒绝(路径穿越在这里被挡住)。 */
24901
+ function assertSessionId(sessionId) {
24902
+ if (typeof sessionId !== "string" || !SESSION_PATTERN.test(sessionId)) throw new CacheWarmupStoreError("session id is not a single safe path segment", "SESSION_INVALID");
24903
+ return sessionId;
24904
+ }
24905
+ /** 把 limit 收进 [1, 512]。 */
24906
+ function clampLimit(limit) {
24907
+ if (limit === void 0 || !Number.isFinite(limit)) return CACHE_WARMUP_DEFAULTS.pageLimitDefault;
24908
+ const rounded = Math.floor(limit);
24909
+ if (rounded < 1) return 1;
24910
+ return Math.min(rounded, CACHE_WARMUP_DEFAULTS.pageLimitMax);
24911
+ }
24912
+ /** 把 maxBytes 收进 [1, 65536]。 */
24913
+ function clampMaxBytes(maxBytes) {
24914
+ if (maxBytes === void 0 || !Number.isFinite(maxBytes)) return CACHE_WARMUP_DEFAULTS.pageMaxBytesDefault;
24915
+ const rounded = Math.floor(maxBytes);
24916
+ if (rounded < 1) return 1;
24917
+ return Math.min(rounded, CACHE_WARMUP_DEFAULTS.pageMaxBytesMax);
24918
+ }
24919
+ /** 一段文本的行数(末尾没有换行也算一行)。 */
24920
+ function countLines(text) {
24921
+ if (text === "") return 0;
24922
+ let lines = 0;
24923
+ for (const char of text) if (char === "\n") lines += 1;
24924
+ return text.endsWith("\n") ? lines : lines + 1;
24925
+ }
24926
+ /**
24927
+ * 按 UTF-8 字节安全分页。
24928
+ *
24929
+ * `offset` 是字节偏移,必须落在字符边界上(上一页的 `nextOffset` 天然满足);
24930
+ * 每页最多 `limit` 行且不超过 `maxBytes` 字节,任何一页都不会把一个字符切成两半。
24931
+ * 两个上限谁先到就在谁那里收手,`nextOffset` 精确指回同一位置,所以
24932
+ * 「两次翻页拼起来等于原文」是构造出来的性质,不是巧合。
24933
+ */
24934
+ function pageText(args) {
24935
+ const { sessionId, refId, text, buffer, offset, limit, maxBytes } = args;
24936
+ const totalBytes = buffer.length;
24937
+ if (!Number.isInteger(offset) || offset < 0) throw new CacheWarmupStoreError("offset must be a non-negative integer byte offset", "OFFSET_OUT_OF_RANGE");
24938
+ if (offset > totalBytes) throw new CacheWarmupStoreError(`offset ${offset} is past the ${totalBytes}-byte result`, "OFFSET_OUT_OF_RANGE");
24939
+ if (offset < totalBytes && (buffer[offset] & 192) === 128) throw new CacheWarmupStoreError("offset is not on a UTF-8 character boundary", "OFFSET_INVALID");
24940
+ const rest = offset === 0 ? text : new TextDecoder("utf-8").decode(buffer.subarray(offset));
24941
+ let content = "";
24942
+ let contentBytes = 0;
24943
+ let lines = 0;
24944
+ let stoppedByBytes = false;
24945
+ let stoppedByLines = false;
24946
+ for (const char of rest) {
24947
+ const charBytes = Buffer.byteLength(char, "utf8");
24948
+ if (lines >= limit) {
24949
+ stoppedByLines = true;
24950
+ break;
24951
+ }
24952
+ if (contentBytes + charBytes > maxBytes) {
24953
+ if (content === "") {
24954
+ content = char;
24955
+ contentBytes = charBytes;
24956
+ }
24957
+ stoppedByBytes = true;
24958
+ break;
24959
+ }
24960
+ content += char;
24961
+ contentBytes += charBytes;
24962
+ if (char === "\n") lines += 1;
24963
+ }
24964
+ const nextOffset = offset + Buffer.byteLength(content, "utf8");
24965
+ const complete = nextOffset >= totalBytes;
24966
+ const remaining = rest.slice(content.length);
24967
+ return {
24968
+ sessionId,
24969
+ refId,
24970
+ mode: "text",
24971
+ content,
24972
+ offset,
24973
+ nextOffset: complete ? null : nextOffset,
24974
+ complete,
24975
+ omitted: {
24976
+ reason: complete ? "none" : stoppedByBytes ? "bytes" : stoppedByLines ? "lines" : "bytes",
24977
+ chars: remaining.length,
24978
+ lines: countLines(remaining),
24979
+ rows: 0,
24980
+ bytes: Buffer.byteLength(remaining, "utf8")
24981
+ },
24982
+ totalChars: text.length,
24983
+ totalBytes,
24984
+ limit,
24985
+ maxBytes
24986
+ };
24987
+ }
24988
+ /**
24989
+ * 结构化行分页:先可靠解析,再按 `fields`/`equals` 过滤,最后按行取一页。
24990
+ *
24991
+ * 解析不可信就抛 `NOT_STRUCTURED`,而不是退回文本模式 —— 调用方要的是「按字段取」,
24992
+ * 悄悄给回一段原文等于让它以为字段过滤生效了。
24993
+ */
24994
+ function pageStructured(args) {
24995
+ const { sessionId, refId, text, buffer, offset, limit, maxBytes } = args;
24996
+ const parsed = parseStructuredResultRows(text);
24997
+ if (parsed === void 0) throw new CacheWarmupStoreError("stored result is not reliably structured rows", "NOT_STRUCTURED");
24998
+ const requested = args.fields === void 0 ? [...parsed.fields] : [...args.fields];
24999
+ for (const field of requested) if (!parsed.fields.includes(field)) throw new CacheWarmupStoreError(`unknown field "${field}"`, "FIELD_UNKNOWN");
25000
+ const equals = args.equals ?? {};
25001
+ for (const key of Object.keys(equals)) if (!parsed.fields.includes(key)) throw new CacheWarmupStoreError(`unknown field "${key}"`, "FIELD_UNKNOWN");
25002
+ const matched = parsed.rows.filter((row) => Object.entries(equals).every(([key, value]) => (row[key] ?? "") === value));
25003
+ if (!Number.isInteger(offset) || offset < 0) throw new CacheWarmupStoreError("offset must be a non-negative integer row index", "OFFSET_OUT_OF_RANGE");
25004
+ if (offset > matched.length) throw new CacheWarmupStoreError(`offset ${offset} is past the ${matched.length}-row result`, "OFFSET_OUT_OF_RANGE");
25005
+ const project = (row) => {
25006
+ const out = {};
25007
+ for (const field of requested) out[field] = row[field] ?? "";
25008
+ return out;
25009
+ };
25010
+ const serialized = (row) => JSON.stringify(project(row));
25011
+ const selected = [];
25012
+ let bytes = 0;
25013
+ let index = offset;
25014
+ let hitByteCap = false;
25015
+ while (index < matched.length && selected.length < limit) {
25016
+ const line = serialized(matched[index]);
25017
+ const lineBytes = Buffer.byteLength(line, "utf8") + (selected.length === 0 ? 0 : 1);
25018
+ if (bytes + lineBytes > maxBytes && selected.length > 0) {
25019
+ hitByteCap = true;
25020
+ break;
25021
+ }
25022
+ if (lineBytes > maxBytes) hitByteCap = true;
25023
+ selected.push(line);
25024
+ bytes += lineBytes;
25025
+ index += 1;
25026
+ }
25027
+ const remainingRows = matched.slice(index);
25028
+ const remainingText = remainingRows.map(serialized).join("\n");
25029
+ const complete = index >= matched.length;
25030
+ return {
25031
+ sessionId,
25032
+ refId,
25033
+ mode: "structured",
25034
+ content: selected.join("\n"),
25035
+ offset,
25036
+ nextOffset: complete ? null : index,
25037
+ complete,
25038
+ omitted: {
25039
+ reason: complete ? "none" : hitByteCap ? "bytes" : "rows",
25040
+ chars: remainingText.length,
25041
+ lines: remainingRows.length,
25042
+ rows: remainingRows.length,
25043
+ bytes: Buffer.byteLength(remainingText, "utf8")
25044
+ },
25045
+ totalChars: text.length,
25046
+ totalBytes: buffer.length,
25047
+ limit,
25048
+ maxBytes,
25049
+ fields: requested,
25050
+ totalRows: parsed.rows.length,
25051
+ matchedRows: matched.length
25052
+ };
25053
+ }
25054
+ /**
25055
+ * 文件系统结果存储。
25056
+ *
25057
+ * 目录布局:`<rootDir>/<sessionId>/<sha256>.txt`。会话 id 已经是单个安全路径段,
25058
+ * 所以「另一个 session 的引用」在路径上就构造不出来;即便如此 page 仍然先做归属校验,
25059
+ * 免得将来换布局时丢掉这道闸。
25060
+ */
25061
+ var FileResultReferenceStore = class {
25062
+ options;
25063
+ resolvedRootDir;
25064
+ /** @param options - 根目录与上限;全部可选。 */
25065
+ constructor(options = {}) {
25066
+ this.options = { ...options };
25067
+ }
25068
+ /** 当前生效的根目录。 */
25069
+ rootDir() {
25070
+ const configured = this.options.rootDir;
25071
+ if (configured !== void 0 && configured.trim() !== "") return path.resolve(configured);
25072
+ if (this.resolvedRootDir === void 0) this.resolvedRootDir = path.resolve(defaultResultStoreRootDir());
25073
+ return this.resolvedRootDir;
25074
+ }
25075
+ /** 单结果字节上限。 */
25076
+ maxResultBytes() {
25077
+ const value = this.options.maxResultBytes;
25078
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : CACHE_WARMUP_DEFAULTS.maxResultBytes;
25079
+ }
25080
+ /** 每 session 字节总量上限。 */
25081
+ maxSessionBytes() {
25082
+ const value = this.options.maxSessionBytes;
25083
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : CACHE_WARMUP_DEFAULTS.maxSessionBytes;
25084
+ }
25085
+ /** 当前时钟。 */
25086
+ now() {
25087
+ return this.options.now === void 0 ? Date.now() : this.options.now();
25088
+ }
25089
+ /** 一个会话的目录。 */
25090
+ sessionDir(sessionId) {
25091
+ return path.join(this.rootDir(), sessionId);
25092
+ }
25093
+ /** 一个会话里一个 digest 的文件路径。 */
25094
+ fileFor(sessionId, digest) {
25095
+ return path.join(this.sessionDir(sessionId), digest + RESULT_SUFFIX);
25096
+ }
25097
+ /**
25098
+ * 保存原文。
25099
+ *
25100
+ * 幂等:文件已存在就直接返回同一个 refId,不重写、不改 mtime。
25101
+ * 原子:先写同目录的临时文件再 rename,读到一半的引用不可能存在。
25102
+ * 权限:写 0600(Windows 上是尽力而为,测试按平台跳过权限断言)。
25103
+ */
25104
+ async put(input) {
25105
+ const sessionId = assertSessionId(input.sessionId);
25106
+ if (typeof input.content !== "string") throw new CacheWarmupStoreError("content must be a string", "CONTENT_INVALID");
25107
+ const bytes = Buffer.from(input.content, "utf8");
25108
+ const maxResultBytes = this.maxResultBytes();
25109
+ if (bytes.length > maxResultBytes) throw new CacheWarmupStoreError(`result is ${bytes.length} bytes, above the ${maxResultBytes}-byte single-result limit`, "RESULT_TOO_LARGE");
25110
+ const digest = createHash("sha256").update(bytes).digest("hex");
25111
+ const refId = formatResultRefId(sessionId, digest);
25112
+ const at = input.at ?? this.now();
25113
+ const target = this.fileFor(sessionId, digest);
25114
+ const existing = await fs.stat(target).catch(() => void 0);
25115
+ if (existing !== void 0 && existing.isFile()) return {
25116
+ refId,
25117
+ sessionId,
25118
+ digest,
25119
+ bytes: existing.size,
25120
+ chars: input.content.length,
25121
+ at,
25122
+ created: false
25123
+ };
25124
+ await fs.mkdir(this.sessionDir(sessionId), {
25125
+ recursive: true,
25126
+ mode: 448
25127
+ });
25128
+ await this.enforceSessionBudget(this.sessionDir(sessionId), bytes.length, target);
25129
+ const tmp = path.join(this.sessionDir(sessionId), `${digest}.${process.pid}.${Date.now()}${TMP_MARKER}`);
25130
+ await fs.writeFile(tmp, bytes, { mode: 384 });
25131
+ await fs.chmod(tmp, 384).catch(() => void 0);
25132
+ await fs.rename(tmp, target);
25133
+ await fs.chmod(target, 384).catch(() => void 0);
25134
+ return {
25135
+ refId,
25136
+ sessionId,
25137
+ digest,
25138
+ bytes: bytes.length,
25139
+ chars: input.content.length,
25140
+ at,
25141
+ created: true
25142
+ };
25143
+ }
25144
+ /**
25145
+ * 按 session 预算淘汰最旧的原文。
25146
+ *
25147
+ * 只按 mtime 从旧到新单个删除;删的是具体文件,路径来自 readdir,不含目录递归。
25148
+ * 淘汰永远不改任何历史 preview —— 那是调用方已经拿走的文本。
25149
+ */
25150
+ async enforceSessionBudget(dir, incoming, keep) {
25151
+ const budget = this.maxSessionBytes();
25152
+ if (incoming > budget) throw new CacheWarmupStoreError(`result is ${incoming} bytes, above the ${budget}-byte per-session limit`, "STORE_FULL");
25153
+ let entries;
25154
+ try {
25155
+ entries = await fs.readdir(dir, { withFileTypes: true });
25156
+ } catch {
25157
+ return;
25158
+ }
25159
+ const files = [];
25160
+ for (const entry of entries) {
25161
+ if (!entry.isFile() || !entry.name.endsWith(RESULT_SUFFIX)) continue;
25162
+ const file = path.join(dir, entry.name);
25163
+ if (file === keep) continue;
25164
+ const stats = await fs.stat(file).catch(() => void 0);
25165
+ if (stats === void 0) continue;
25166
+ files.push({
25167
+ file,
25168
+ size: stats.size,
25169
+ mtime: stats.mtimeMs
25170
+ });
25171
+ }
25172
+ let total = files.reduce((sum, file) => sum + file.size, 0);
25173
+ if (total + incoming <= budget) return;
25174
+ files.sort((left, right) => left.mtime - right.mtime);
25175
+ for (const file of files) {
25176
+ if (total + incoming <= budget) break;
25177
+ await fs.rm(file.file, { force: true }).catch(() => void 0);
25178
+ total -= file.size;
25179
+ }
25180
+ }
25181
+ /**
25182
+ * 读一页。
25183
+ *
25184
+ * 归属校验在碰文件系统之前完成:别人的引用不会告诉你它存在不存在。
25185
+ */
25186
+ async page(input) {
25187
+ const sessionId = assertSessionId(input.sessionId);
25188
+ const parsed = parseResultRefId(input.refId);
25189
+ if (parsed === void 0) throw new CacheWarmupStoreError("reference is not one this store issues", "REFERENCE_INVALID");
25190
+ if (parsed.sessionId !== sessionId) throw new CacheWarmupStoreError("reference does not belong to this session", "SESSION_MISMATCH");
25191
+ const file = this.fileFor(sessionId, parsed.digest);
25192
+ const stats = await fs.stat(file).catch(() => void 0);
25193
+ if (stats === void 0 || !stats.isFile()) throw new CacheWarmupStoreError("stored result is missing; it was released or evicted", "RESULT_NOT_FOUND");
25194
+ const ttlMs = this.options.ttlMs;
25195
+ if (ttlMs !== void 0 && ttlMs > 0 && this.now() - stats.mtimeMs > ttlMs) throw new CacheWarmupStoreError("stored result expired", "RESULT_EXPIRED");
25196
+ const buffer = await fs.readFile(file);
25197
+ const text = new TextDecoder("utf-8").decode(buffer);
25198
+ const limit = clampLimit(input.limit);
25199
+ const maxBytes = clampMaxBytes(input.maxBytes);
25200
+ const offset = input.offset ?? 0;
25201
+ if (input.fields !== void 0 || input.equals !== void 0) return pageStructured({
25202
+ sessionId,
25203
+ refId: input.refId,
25204
+ text,
25205
+ buffer,
25206
+ offset,
25207
+ limit,
25208
+ maxBytes,
25209
+ fields: input.fields,
25210
+ equals: input.equals
25211
+ });
25212
+ return pageText({
25213
+ sessionId,
25214
+ refId: input.refId,
25215
+ text,
25216
+ buffer,
25217
+ offset,
25218
+ limit,
25219
+ maxBytes
25220
+ });
25221
+ }
25222
+ /**
25223
+ * 释放一条引用:只删这一个具体文件,缺了就当已经释放(返回 false)。
25224
+ * 清理路径上不抛文件系统错误,免得在 finally 里把原始异常顶掉。
25225
+ */
25226
+ async release(input) {
25227
+ const sessionId = assertSessionId(input.sessionId);
25228
+ const parsed = parseResultRefId(input.refId);
25229
+ if (parsed === void 0) throw new CacheWarmupStoreError("reference is not one this store issues", "REFERENCE_INVALID");
25230
+ if (parsed.sessionId !== sessionId) throw new CacheWarmupStoreError("reference does not belong to this session", "SESSION_MISMATCH");
25231
+ const file = this.fileFor(sessionId, parsed.digest);
25232
+ const exists = await fs.stat(file).catch(() => void 0);
25233
+ if (exists === void 0 || !exists.isFile()) return false;
25234
+ try {
25235
+ await fs.rm(file, { force: true });
25236
+ return true;
25237
+ } catch {
25238
+ return false;
25239
+ }
25240
+ }
25241
+ };
25242
+ //#endregion
25243
+ //#region src/host/cache-warmup/usage.ts
25244
+ /**
25245
+ * 缓存用量登记表 —— 有界、只写不读消息、永不抛出。
25246
+ *
25247
+ * 为什么「永不抛出」是硬性契约:Kimi adapter 在 finally 路径里调用
25248
+ * `recordCacheWarmupUsage`。那个位置抛出的任何东西都会顶掉原始异常、把一次失败
25249
+ * 伪装成另一次失败,或者让重试/账号轮换逻辑读到半个状态。所以整个函数体被
25250
+ * 一层 catch 包住,坏输入、被污染的属性访问、内部容器异常一律降级成中性返回值。
25251
+ *
25252
+ * 有界:最多 {@link CACHE_WARMUP_DEFAULTS.maxUsageSessions} 个会话,
25253
+ * 每个会话最多 {@link CACHE_WARMUP_DEFAULTS.maxUsageReportsPerSession} 条报告。
25254
+ * 超出时淘汰最久未活动的会话 / 最旧的报告 —— 内存里只留「最近发生了什么」。
25255
+ *
25256
+ * @module dsh-chatgpt-subscription/host/cache-warmup/usage
25257
+ */
25258
+ /** 有界登记表:Map 的插入顺序同时是 LRU 顺序(命中即重新插入到队尾)。 */
25259
+ const registry = /* @__PURE__ */ new Map();
25260
+ /** 一个中性返回值,异常路径复用(不含任何容器操作)。 */
25261
+ function neutralState(sessionId) {
25262
+ return {
25263
+ sessionId,
25264
+ reports: 0,
25265
+ consecutiveHits: 0,
25266
+ hitRatio: 0
25267
+ };
25268
+ }
25269
+ /** 非负有限数;其他一律 0。 */
25270
+ function finiteCount(value) {
25271
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
25272
+ }
25273
+ /**
25274
+ * 一次请求的缓存命中率 = 命中缓存 token / (未命中 token + 命中 token)。
25275
+ *
25276
+ * 这里的两个字段是**互斥的两半**,不是整体与部分:Kimi/DSH 上报的
25277
+ * `inputTokens` 是这次真正送去计算的未命中部分,`cacheReadTokens` 是命中缓存、
25278
+ * 没有重算的部分。所以分母必须是两者之和。
25279
+ *
25280
+ * 用 `cached / inputTokens` 是错的,而且错得很隐蔽:只要命中数接近或超过未命中数,
25281
+ * 比值就会 ≥ 1 并被钳成 1,于是「连续两次 ≥ 0.97」的判据在真实流量的**第一次**请求上
25282
+ * 就成立,预热期形同不存在 —— 这正好是这套预算想防的那件事。
25283
+ *
25284
+ * 也不允许拿 `totalTokens` 当分母:那个字段的构成由上层决定(可能含输出 token、
25285
+ * 也可能已经包含缓存部分),用它会让命中率随无关字段漂移。
25286
+ *
25287
+ * @param usage - 至少含 inputTokens(未命中)与 cacheReadTokens(命中)的用量。
25288
+ * @returns 0-1;`input=0` 且 `cached>0` 时为 1;两者都为 0 或非法时为 0。
25289
+ */
25290
+ function cacheWarmupHitRatio(usage) {
25291
+ try {
25292
+ const input = finiteCount(usage.inputTokens);
25293
+ const cached = finiteCount(usage.cacheReadTokens);
25294
+ if (cached <= 0) return 0;
25295
+ const total = input + cached;
25296
+ if (total <= 0) return 0;
25297
+ return Math.min(1, cached / total);
25298
+ } catch {
25299
+ return 0;
25300
+ }
25301
+ }
25302
+ /**
25303
+ * 从最新一条往回数「连续高命中率」条数。
25304
+ *
25305
+ * 停止条件(任一):命中率低于阈值、该条带 driftCause、报告到头。
25306
+ * 「连续」是从最近往回数的,所以中间任何一次低命中或漂移都会把计数截断到 0。
25307
+ * @param reports - 按时间从旧到新排列的报告。
25308
+ * @returns 连续命中条数。
25309
+ */
25310
+ /** Kimi 对每个请求都会报告的生命周期状态;只有其余值才表示真实前缀漂移。 */
25311
+ const BENIGN_LIFECYCLE_CAUSES = /* @__PURE__ */ new Set(["stable", "first-request"]);
25312
+ function consecutiveWarmupHits(reports) {
25313
+ try {
25314
+ if (!Array.isArray(reports)) return 0;
25315
+ let hits = 0;
25316
+ for (let index = reports.length - 1; index >= 0; index -= 1) {
25317
+ const report = reports[index];
25318
+ if (report === void 0 || report === null) break;
25319
+ const drift = report.driftCause;
25320
+ if (typeof drift === "string" && drift.trim() !== "" && !BENIGN_LIFECYCLE_CAUSES.has(drift)) break;
25321
+ if (cacheWarmupHitRatio(report) < CACHE_WARMUP_DEFAULTS.hitRatioThreshold) break;
25322
+ hits += 1;
25323
+ }
25324
+ return hits;
25325
+ } catch {
25326
+ return 0;
25327
+ }
25328
+ }
25329
+ /** 把任意输入归一成一条用量报告;形状不对返回 undefined(绝不抛)。 */
25330
+ function normalizeUsage(input) {
25331
+ if (input === null || input === void 0 || typeof input !== "object") return void 0;
25332
+ const sessionId = input.sessionId;
25333
+ if (typeof sessionId !== "string" || sessionId.trim() === "") return void 0;
25334
+ const driftCause = typeof input.driftCause === "string" && input.driftCause.trim() !== "" ? input.driftCause : void 0;
25335
+ const at = typeof input.at === "number" && Number.isFinite(input.at) ? input.at : Date.now();
25336
+ const normalized = {
25337
+ sessionId,
25338
+ inputTokens: finiteCount(input.inputTokens),
25339
+ cacheReadTokens: finiteCount(input.cacheReadTokens),
25340
+ outputTokens: finiteCount(input.outputTokens),
25341
+ totalTokens: finiteCount(input.totalTokens),
25342
+ at
25343
+ };
25344
+ if (driftCause !== void 0) normalized.driftCause = driftCause;
25345
+ return normalized;
25346
+ }
25347
+ /** 由一份报告列表算出状态摘要。 */
25348
+ function stateOf(sessionId, reports) {
25349
+ const last = reports[reports.length - 1];
25350
+ return {
25351
+ sessionId,
25352
+ reports: reports.length,
25353
+ consecutiveHits: consecutiveWarmupHits(reports),
25354
+ hitRatio: last === void 0 ? 0 : cacheWarmupHitRatio(last),
25355
+ lastAt: last?.at
25356
+ };
25357
+ }
25358
+ /**
25359
+ * 登记一次用量报告。
25360
+ *
25361
+ * **total function**:任何输入、任何内部异常都不抛出,最差返回一份中性状态。
25362
+ * 有副作用(更新有界登记表),但不触碰消息、不发起请求、不改变请求参数。
25363
+ * @param input - 用量报告。
25364
+ * @returns 该会话登记之后的状态摘要。
25365
+ */
25366
+ function recordCacheWarmupUsage(input) {
25367
+ let sessionId = "";
25368
+ try {
25369
+ const usage = normalizeUsage(input);
25370
+ if (usage === void 0) return neutralState("");
25371
+ sessionId = usage.sessionId;
25372
+ const previous = registry.get(usage.sessionId);
25373
+ const kept = previous === void 0 ? [] : [...previous];
25374
+ kept.push(Object.freeze({ ...usage }));
25375
+ while (kept.length > CACHE_WARMUP_DEFAULTS.maxUsageReportsPerSession) kept.shift();
25376
+ if (previous !== void 0) registry.delete(usage.sessionId);
25377
+ registry.set(usage.sessionId, Object.freeze(kept));
25378
+ while (registry.size > CACHE_WARMUP_DEFAULTS.maxUsageSessions) {
25379
+ const oldest = registry.keys().next();
25380
+ if (oldest.done === true) break;
25381
+ registry.delete(oldest.value);
25382
+ }
25383
+ return stateOf(usage.sessionId, kept);
25384
+ } catch {
25385
+ try {
25386
+ if (sessionId !== "") registry.delete(sessionId);
25387
+ } catch {}
25388
+ return neutralState(sessionId);
25389
+ }
25390
+ }
25391
+ /**
25392
+ * 读一个会话的报告(从旧到新),不抛出。
25393
+ * @param sessionId - 会话 id。
25394
+ * @returns 报告副本;没有或读不到时为空数组。
25395
+ */
25396
+ function cacheWarmupUsageReports(sessionId) {
25397
+ try {
25398
+ if (typeof sessionId !== "string" || sessionId === "") return [];
25399
+ const reports = registry.get(sessionId);
25400
+ return reports === void 0 ? [] : [...reports];
25401
+ } catch {
25402
+ return [];
25403
+ }
25404
+ }
25405
+ /**
25406
+ * 列出所有有报告的会话,按最近活动倒序;不抛出。
25407
+ * @returns 会话摘要数组。
25408
+ */
25409
+ function cacheWarmupUsageSessions() {
25410
+ try {
25411
+ const summaries = [];
25412
+ for (const [sessionId, reports] of registry.entries()) {
25413
+ if (reports.length === 0) continue;
25414
+ const last = reports[reports.length - 1];
25415
+ summaries.push({
25416
+ sessionId,
25417
+ reports: reports.length,
25418
+ lastAt: last.at,
25419
+ hitRatio: cacheWarmupHitRatio(last)
25420
+ });
25421
+ }
25422
+ summaries.sort((left, right) => right.lastAt - left.lastAt);
25423
+ return summaries;
25424
+ } catch {
25425
+ return [];
25426
+ }
25427
+ }
25428
+ //#endregion
25429
+ //#region src/host/cache-warmup/budget.ts
25430
+ /**
25431
+ * 预热预算:预热期保守、连续命中后放大、漂移立刻收回。
25432
+ *
25433
+ * 控制器是**纯函数**:不发起请求、不改消息、不改登记表。它只回答一个问题 ——
25434
+ * 「这一次预览最多能占多少字符」。把决策与执行分开,才能保证预算收紧或放开
25435
+ * 都不会悄悄改写已经进入历史的文本(历史里的 preview 是常量)。
25436
+ *
25437
+ * @module dsh-chatgpt-subscription/host/cache-warmup/budget
25438
+ */
25439
+ /** 预热期的保守预算(构造一次,避免每次读模块常量)。 */
25440
+ const WARMUP_BUDGET = Object.freeze({
25441
+ maxChars: CACHE_WARMUP_DEFAULTS.warmupMaxChars,
25442
+ warmup: true,
25443
+ consecutiveHits: 0,
25444
+ hitRatio: 0
25445
+ });
25446
+ /**
25447
+ * 当前会话的预览预算。
25448
+ *
25449
+ * 规则(约束 4):预热期 `maxChars = 1200`(约 600-750 tokens);连续
25450
+ * `consecutiveHitsRequired`(2) 次命中率 ≥ 0.97 且期间没有 drift,才进入稳态
25451
+ * `maxChars = 8000`。任何一次 drift 都清零连续计数,于是下一条请求立刻回落到预热预算。
25452
+ *
25453
+ * **不抛出**:没有会话、没有报告、登记表异常,一律返回预热期的保守预算。
25454
+ * @param sessionId - 会话 id;不传则按最保守的预热预算处理。
25455
+ * @returns 本次投影可用的预算。
25456
+ */
25457
+ function cacheWarmupBudget(sessionId) {
25458
+ try {
25459
+ if (typeof sessionId !== "string" || sessionId.trim() === "") return { ...WARMUP_BUDGET };
25460
+ const reports = cacheWarmupUsageReports(sessionId);
25461
+ if (reports.length === 0) return { ...WARMUP_BUDGET };
25462
+ const consecutiveHits = consecutiveWarmupHits(reports);
25463
+ const last = reports[reports.length - 1];
25464
+ const hitRatio = last === void 0 ? 0 : cacheWarmupHitRatio(last);
25465
+ const warmup = consecutiveHits < CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired;
25466
+ return {
25467
+ maxChars: warmup ? CACHE_WARMUP_DEFAULTS.warmupMaxChars : CACHE_WARMUP_DEFAULTS.steadyStateMaxChars,
25468
+ warmup,
25469
+ consecutiveHits,
25470
+ hitRatio
25471
+ };
25472
+ } catch {
25473
+ return { ...WARMUP_BUDGET };
25474
+ }
25475
+ }
25476
+ /**
25477
+ * 一份可读快照。**不抛出**,且完全无副作用(只读登记表)。
25478
+ * @param sessionId - 会话 id;不传则给全局汇总。
25479
+ * @returns 快照。
25480
+ */
25481
+ function cacheWarmupSnapshot(sessionId) {
25482
+ const fallback = {
25483
+ budget: { ...WARMUP_BUDGET },
25484
+ defaults: CACHE_WARMUP_DEFAULTS,
25485
+ usage: {
25486
+ reports: 0,
25487
+ hitRatio: 0,
25488
+ totals: {
25489
+ inputTokens: 0,
25490
+ cacheReadTokens: 0,
25491
+ outputTokens: 0,
25492
+ totalTokens: 0
25493
+ },
25494
+ driftCauses: []
25495
+ },
25496
+ sessions: 0
25497
+ };
25498
+ try {
25499
+ const budget = cacheWarmupBudget(sessionId);
25500
+ const reports = typeof sessionId === "string" && sessionId.trim() !== "" ? cacheWarmupUsageReports(sessionId) : [];
25501
+ const totals = {
25502
+ inputTokens: 0,
25503
+ cacheReadTokens: 0,
25504
+ outputTokens: 0,
25505
+ totalTokens: 0
25506
+ };
25507
+ const driftCauses = [];
25508
+ for (const report of reports) {
25509
+ totals.inputTokens += report.inputTokens;
25510
+ totals.cacheReadTokens += report.cacheReadTokens;
25511
+ totals.outputTokens += report.outputTokens;
25512
+ totals.totalTokens += report.totalTokens;
25513
+ const drift = report.driftCause;
25514
+ if (typeof drift === "string" && drift !== "" && !driftCauses.includes(drift)) driftCauses.push(drift);
25515
+ }
25516
+ const last = reports[reports.length - 1];
25517
+ const snapshot = {
25518
+ budget,
25519
+ defaults: CACHE_WARMUP_DEFAULTS,
25520
+ usage: {
25521
+ reports: reports.length,
25522
+ hitRatio: last === void 0 ? 0 : cacheWarmupHitRatio(last),
25523
+ totals,
25524
+ driftCauses
25525
+ },
25526
+ sessions: cacheWarmupUsageSessions().length
25527
+ };
25528
+ if (last !== void 0) snapshot.usage.lastAt = last.at;
25529
+ if (typeof sessionId === "string" && sessionId.trim() !== "") snapshot.sessionId = sessionId;
25530
+ return snapshot;
25531
+ } catch {
25532
+ return fallback;
25533
+ }
25534
+ }
25535
+ //#endregion
25536
+ //#region src/host/cache-warmup/projection.ts
25537
+ /**
25538
+ * 保守结构化投影:只有**可证明**的结构才换成预览,其余一律原样。
25539
+ *
25540
+ * 默认保守的四条线:
25541
+ *
25542
+ * - 小结果原样(不超过预算就不动),因为换来换去只会扰乱缓存前缀。
25543
+ * - 无法可靠解析的大文本原样;未知大文本原样;关键源码、接口/类型定义、子代理报告原样。
25544
+ * - 失败结果只允许换成「完整失败集」型预览(测试失败清单、shell 错误行),
25545
+ * 永远不会被压成一句计数 —— 计数看起来像结论,实际会把下一步的调试带偏。
25546
+ * - 预览必须自述完整度:带 ref、带省略字符数,读全文的路径写在预览里。
25547
+ *
25548
+ * 另一个不变量:本模块**不修改历史**。它返回一份新内容,调用方决定要不要写进新结果;
25549
+ * 已经进入历史的 preview 与其 refId 是常量,存储过期也不会回头改它。
25550
+ *
25551
+ * @module dsh-chatgpt-subscription/host/cache-warmup/projection
25552
+ */
25553
+ /** 子代理报告:一律原样,摘要会丢掉调用方真正要的判断依据。 */
25554
+ const TOOL_SUBAGENT = /(?:^|[-_/])(?:subagent|agent|task|delegate|ralph|teammate)(?:[-_/]|$)/i;
25555
+ /** 源码读取/写入类工具:一律原样。 */
25556
+ const TOOL_SOURCE = /(?:^|[-_/])(?:read|view|cat|open|edit|write|patch|apply_patch|notebook|document|diff)(?:[-_/]|$)/i;
25557
+ /** 一行文本的裁剪(超长行才裁剪,并留下省略号)。 */
25558
+ function clip(text, max) {
25559
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
25560
+ }
25561
+ /**
25562
+ * 按预算装配预览正文。
25563
+ *
25564
+ * 必需段落要么整段进来,要么整体装不下就返回 undefined(调用方据此保持原文);
25565
+ * 可选段落逐段尝试,装不下就整段跳过。这样任何输出都不会出现「半截失败清单」。
25566
+ */
25567
+ function fitSections(sections, maxChars) {
25568
+ const lines = [];
25569
+ let used = 0;
25570
+ for (const section of sections) {
25571
+ if (section.optional === true) continue;
25572
+ for (const line of section.lines) {
25573
+ used += line.length + 1;
25574
+ lines.push(line);
25575
+ }
25576
+ }
25577
+ if (used > maxChars + 1) return void 0;
25578
+ for (const section of sections) {
25579
+ if (section.optional !== true) continue;
25580
+ const size = section.lines.reduce((sum, line) => sum + line.length + 1, 0);
25581
+ if (used + size > maxChars + 1) continue;
25582
+ for (const line of section.lines) lines.push(line);
25583
+ used += size;
25584
+ }
25585
+ return lines.join("\n");
25586
+ }
25587
+ /** 从测试输出里抽取「完整失败集」与汇总行;抽不全就整体放弃。 */
25588
+ function extractTestEvidence(text) {
25589
+ const summary = [];
25590
+ const failures = [];
25591
+ for (const raw of text.split("\n")) {
25592
+ const trimmed = raw.trim();
25593
+ if (trimmed === "") continue;
25594
+ if (/^(?:FAIL|FAILED|✗|✕|×)\s+\S/.test(trimmed) || /^not ok \d+ - /.test(trimmed) || /^●\s+\S/.test(trimmed) || /^--- FAIL:\s/.test(trimmed)) {
25595
+ failures.push(clip(trimmed, 240));
25596
+ continue;
25597
+ }
25598
+ if (/^Test Files\s+\d/.test(trimmed) || /^Tests\s+\d/.test(trimmed) || /^Test Suites:\s/.test(trimmed) || /^Snapshots:\s/.test(trimmed) || /^Time:\s/.test(trimmed) || /^={2,}.*\b\d+ (?:failed|passed|error)/.test(trimmed) || /^\d+ (?:failed|passed), \d+ (?:passed|failed)/.test(trimmed)) summary.push(clip(trimmed, 200));
25599
+ }
25600
+ if (summary.length === 0 && failures.length === 0) return void 0;
25601
+ if (summary.length === 0 && !/\b(?:test|tests|spec|suite|assert)\b/i.test(text.slice(0, 4e3))) return void 0;
25602
+ return {
25603
+ summary,
25604
+ failures
25605
+ };
25606
+ }
25607
+ /** 测试预览:失败集必须完整,汇总行紧随其后。 */
25608
+ function buildTestSections(evidence) {
25609
+ const sections = [{ lines: [`failures: ${evidence.failures.length} (complete set)`] }];
25610
+ if (evidence.failures.length > 0) sections.push({ lines: evidence.failures.map((line) => ` ${line}`) });
25611
+ if (evidence.summary.length > 0) sections.push({ lines: ["summary:", ...evidence.summary.map((line) => ` ${line}`)] });
25612
+ return sections;
25613
+ }
25614
+ /** 一行 shell 错误。用于「完整错误集」,所以匹配宁可宽一点。 */
25615
+ const SHELL_ERROR_LINE = /(?:error|fatal|panic|traceback|exception|EACCES|EPERM|ENOENT|EEXIST|ELIFECYCLE|ERR!|FAILED|permission denied|cannot |could not |not found)/i;
25616
+ /** 一行 glob 结果的判定:无空白、像路径。 */
25617
+ function looksLikeGlobResult(text) {
25618
+ const lines = text.split("\n").filter((line) => line.trim() !== "");
25619
+ if (lines.length < 10) return false;
25620
+ let paths = 0;
25621
+ for (const line of lines) {
25622
+ const trimmed = line.trim();
25623
+ if (trimmed.length === 0 || trimmed.length > 400) continue;
25624
+ if (/\s/.test(trimmed)) continue;
25625
+ if (/^[-*+]$/.test(trimmed)) continue;
25626
+ paths += 1;
25627
+ }
25628
+ return paths / lines.length >= .9;
25629
+ }
25630
+ /** shell 输出判定:有退出码标记,或至少一行错误。 */
25631
+ function looksLikeShellOutput(text) {
25632
+ if (/(?:exit code|exited with code|process exited with code)[:=\s]+-?\d+/i.test(text)) return true;
25633
+ if (/\[exit code: -?\d+\]/i.test(text)) return true;
25634
+ if (/^Traceback \(most recent call last\):/m.test(text)) return true;
25635
+ const lines = text.split("\n");
25636
+ let errors = 0;
25637
+ for (const line of lines) {
25638
+ if (line.trim() === "") continue;
25639
+ if (/^\s*(?:0|no)\s+(?:errors?|failures?|failed)\b/i.test(line.trim())) continue;
25640
+ if (SHELL_ERROR_LINE.test(line)) errors += 1;
25641
+ if (errors >= 1) return true;
25642
+ }
25643
+ return false;
25644
+ }
25645
+ /** shell 预览:退出码 + 完整错误集(必需),首尾片段(可选)。 */
25646
+ function buildShellSections(text) {
25647
+ const lines = text.split("\n");
25648
+ const errors = [];
25649
+ for (const raw of lines) {
25650
+ const trimmed = raw.trim();
25651
+ if (trimmed === "") continue;
25652
+ if (/^\[exit code: -?\d+\]/i.test(trimmed)) continue;
25653
+ if (/^\s*(?:0|no)\s+(?:errors?|failures?|failed)\b/i.test(trimmed)) continue;
25654
+ if (SHELL_ERROR_LINE.test(trimmed)) errors.push(` ${clip(trimmed, 240)}`);
25655
+ }
25656
+ if (errors.length > 300) return void 0;
25657
+ const exitMatch = /(?:exit code|exited with code|process exited with code)[:=\s]+(-?\d+)/i.exec(text) ?? /\[exit code: (-?\d+)\]/i.exec(text);
25658
+ const sections = [];
25659
+ if (exitMatch !== null) sections.push({ lines: [`exit code: ${exitMatch[1]}`] });
25660
+ sections.push({ lines: [`errors: ${errors.length} (complete set)`] });
25661
+ if (errors.length > 0) sections.push({ lines: errors });
25662
+ const nonEmpty = lines.map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
25663
+ const head = nonEmpty.slice(0, 5).map((line) => ` ${clip(line.trim(), 160)}`);
25664
+ const tail = nonEmpty.slice(-5).map((line) => ` ${clip(line.trim(), 160)}`);
25665
+ if (head.length > 0) sections.push({
25666
+ lines: ["head:", ...head],
25667
+ optional: true
25668
+ });
25669
+ if (tail.length > 0) sections.push({
25670
+ lines: ["tail:", ...tail],
25671
+ optional: true
25672
+ });
25673
+ return sections;
25674
+ }
25675
+ /** 一行 grep 结果:`<path>:<line>[:<column>]:<text>`。 */
25676
+ const GREP_LINE = /^(.+?):(\d+):(?:\d+:)?(.*)$/;
25677
+ /** 抽取 grep 证据;命中线占比不到 80% 就当它不是 grep 输出。 */
25678
+ function extractGrepEvidence(text) {
25679
+ const lines = text.split("\n").filter((line) => line.trim() !== "");
25680
+ if (lines.length < 10) return void 0;
25681
+ const files = /* @__PURE__ */ new Map();
25682
+ let matched = 0;
25683
+ for (const line of lines) {
25684
+ const match = GREP_LINE.exec(line);
25685
+ if (match === null) continue;
25686
+ const file = match[1];
25687
+ if (!/[\\/.]/.test(file) || file.length > 400) continue;
25688
+ matched += 1;
25689
+ const entry = files.get(file);
25690
+ if (entry === void 0) files.set(file, {
25691
+ count: 1,
25692
+ first: match[3]
25693
+ });
25694
+ else entry.count += 1;
25695
+ }
25696
+ if (matched < 10 || matched / lines.length < .8) return void 0;
25697
+ return {
25698
+ files,
25699
+ total: matched
25700
+ };
25701
+ }
25702
+ /** grep 预览:命中总数与每文件计数(最多列 30 个文件,其余显式说明)。 */
25703
+ function buildGrepSections(evidence) {
25704
+ const ranked = [...evidence.files.entries()].sort((left, right) => right[1].count - left[1].count);
25705
+ const shown = ranked.slice(0, 30);
25706
+ const sections = [{ lines: [`matches: ${evidence.total} across ${ranked.length} files`] }, { lines: ["by file:", ...shown.map(([file, entry]) => ` ${clip(file, 120)}: ${entry.count}`)] }];
25707
+ if (ranked.length > shown.length) sections.push({ lines: [` … ${ranked.length - shown.length} more files (full list via ref)`] });
25708
+ sections.push({
25709
+ lines: ["first matches:", ...shown.slice(0, 8).map(([file, entry]) => ` ${clip(file, 80)}: ${clip(entry.first, 120)}`)],
25710
+ optional: true
25711
+ });
25712
+ return sections;
25713
+ }
25714
+ /** glob 预览:总数 + 扩展名分布(必需),样本路径(可选)。 */
25715
+ function buildGlobSections(text) {
25716
+ const paths = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
25717
+ const byExtension = /* @__PURE__ */ new Map();
25718
+ for (const entry of paths) {
25719
+ const dot = entry.lastIndexOf(".");
25720
+ const extension = dot > Math.max(entry.lastIndexOf("/"), entry.lastIndexOf("\\")) ? entry.slice(dot).toLowerCase() : "(none)";
25721
+ byExtension.set(extension, (byExtension.get(extension) ?? 0) + 1);
25722
+ }
25723
+ const top = [...byExtension.entries()].sort((left, right) => right[1] - left[1]).slice(0, 6);
25724
+ const sample = paths.slice(0, 10).map((entry) => ` ${clip(entry, 140)}`);
25725
+ if (paths.length > 10) sample.push(` … ${paths.length - 10} more`);
25726
+ return [
25727
+ { lines: [`paths: ${paths.length}`] },
25728
+ { lines: ["by extension:", ...top.map(([extension, count]) => ` ${extension}: ${count}`)] },
25729
+ {
25730
+ lines: ["sample:", ...sample],
25731
+ optional: true
25732
+ }
25733
+ ];
25734
+ }
25735
+ /** 结构化预览:行数与字段(必需),样本行(可选)。 */
25736
+ function buildStructuredSections(text) {
25737
+ const parsed = parseStructuredResultRows(text);
25738
+ if (parsed === void 0) return void 0;
25739
+ const sample = parsed.rows.slice(0, 3).map((row) => ` ${clip(JSON.stringify(row), 240)}`);
25740
+ return [{ lines: [`rows: ${parsed.rows.length}`, `fields: ${parsed.fields.join(", ")}`] }, {
25741
+ lines: ["sample:", ...sample],
25742
+ optional: true
25743
+ }];
25744
+ }
25745
+ /** 契约/类型定义:命中两行以上声明就当它是契约,保持原样。 */
25746
+ function looksLikeContract(text) {
25747
+ let declarations = 0;
25748
+ for (const line of text.split("\n").slice(0, 800)) {
25749
+ if (/^\s*(?:export\s+)?(?:declare\s+)?(?:interface|type)\s+[A-Za-z_$][\w$]*/.test(line)) declarations += 1;
25750
+ if (declarations >= 2) return true;
25751
+ }
25752
+ return false;
25753
+ }
25754
+ /** 源码判定:代码行占比过半(或含多处 markdown 代码围栏)。 */
25755
+ function looksLikeSourceCode(text) {
25756
+ let fences = 0;
25757
+ for (const line of text.split("\n").slice(0, 600)) if (line.trim().startsWith("```")) fences += 1;
25758
+ if (fences >= 4) return true;
25759
+ let code = 0;
25760
+ let nonEmpty = 0;
25761
+ for (const raw of text.split("\n").slice(0, 600)) {
25762
+ const trimmed = raw.trim();
25763
+ if (trimmed === "") continue;
25764
+ nonEmpty += 1;
25765
+ if (/^(?:import|export|from|const|let|var|function|class|interface|type|enum|async|await|return|if|for|while|switch|case|break|continue|try|catch|finally|throw|new|public|private|protected|static|readonly|def|using|namespace|package|#include|#pragma)\b/.test(trimmed) || /^[})\];]/.test(trimmed) || /^(?:\/\/|\/\*|\*)/.test(trimmed)) code += 1;
25766
+ }
25767
+ return nonEmpty >= 10 && code / nonEmpty >= .45;
25768
+ }
25769
+ /** 完整 JSON 文档或被截断的 JSON 对象:可能是批量源码/混合证据,不按裸日志猜测。 */
25770
+ function isJsonDocument(text) {
25771
+ const trimmed = text.trim();
25772
+ if (trimmed.length < 2) return false;
25773
+ const first = trimmed[0];
25774
+ const last = trimmed[trimmed.length - 1];
25775
+ if (first !== "{" && first !== "[") return false;
25776
+ if (first === "{" && last === "}" || first === "[" && last === "]") try {
25777
+ const value = JSON.parse(trimmed);
25778
+ return typeof value === "object" && value !== null;
25779
+ } catch {}
25780
+ if (first === "{") {
25781
+ let fields = 0;
25782
+ for (const line of trimmed.split("\n").slice(0, 24)) if (/^\s*"[^"]+"\s*:/.test(line)) fields += 1;
25783
+ return fields >= 2;
25784
+ }
25785
+ return false;
25786
+ }
25787
+ /**
25788
+ * 决定这一段文本该怎么处理。
25789
+ *
25790
+ * 顺序就是「可证明性」的顺序:子代理报告/源码工具/契约先被判成原样,
25791
+ * 然后是测试失败集、grep、结构化、glob、shell 这些有明确形状的,
25792
+ * 最后落到「无法解析 → 原样」。
25793
+ */
25794
+ function planProjection(text, toolName) {
25795
+ if (TOOL_SUBAGENT.test(toolName)) return {
25796
+ kind: "verbatim",
25797
+ strategy: "verbatim-subagent-report"
25798
+ };
25799
+ if (TOOL_SOURCE.test(toolName)) return {
25800
+ kind: "verbatim",
25801
+ strategy: "verbatim-source-code"
25802
+ };
25803
+ if (looksLikeContract(text)) return {
25804
+ kind: "verbatim",
25805
+ strategy: "verbatim-contract"
25806
+ };
25807
+ if (isJsonDocument(text)) return {
25808
+ kind: "verbatim",
25809
+ strategy: "verbatim-unparsed"
25810
+ };
25811
+ if (looksLikeSourceCode(text)) return {
25812
+ kind: "verbatim",
25813
+ strategy: "verbatim-source-code"
25814
+ };
25815
+ const test = extractTestEvidence(text);
25816
+ if (test !== void 0) return {
25817
+ kind: "preview",
25818
+ strategy: "test-failure",
25819
+ sections: buildTestSections(test)
25820
+ };
25821
+ const grep = extractGrepEvidence(text);
25822
+ if (grep !== void 0) return {
25823
+ kind: "preview",
25824
+ strategy: "grep-results",
25825
+ sections: buildGrepSections(grep)
25826
+ };
25827
+ const structured = buildStructuredSections(text);
25828
+ if (structured !== void 0) return {
25829
+ kind: "preview",
25830
+ strategy: "structured-rows",
25831
+ sections: structured
25832
+ };
25833
+ if (looksLikeGlobResult(text)) return {
25834
+ kind: "preview",
25835
+ strategy: "glob-results",
25836
+ sections: buildGlobSections(text)
25837
+ };
25838
+ if (looksLikeShellOutput(text)) {
25839
+ const sections = buildShellSections(text);
25840
+ if (sections === void 0) return {
25841
+ kind: "verbatim",
25842
+ strategy: "verbatim-unparsed"
25843
+ };
25844
+ return {
25845
+ kind: "preview",
25846
+ strategy: "shell-output",
25847
+ sections
25848
+ };
25849
+ }
25850
+ return {
25851
+ kind: "verbatim",
25852
+ strategy: "verbatim-unparsed"
25853
+ };
25854
+ }
25855
+ /** 预览尾部:把「怎么读全文」写死在预览里,模型不用猜。 */
25856
+ function footerFor(refId) {
25857
+ return `[full result: ${refId} · page with offset/limit(default 64 lines)/maxBytes(default 16384 bytes)]`;
25858
+ }
25859
+ /**
25860
+ * 核心决策:预算 → 计划 → 装配 → 入库。
25861
+ *
25862
+ * 入库放在最后:预览装不进预算就不写文件,也不会留下一条没人引用的引用。
25863
+ * 入库失败一律降级为「保持原样」—— 没有 ref 的预览等于把原文丢了。
25864
+ */
25865
+ async function decide(args) {
25866
+ const { text, input, budget } = args;
25867
+ if (text.length <= budget.maxChars) return { reason: "verbatim-small" };
25868
+ const plan = planProjection(text, input.toolName);
25869
+ if (plan.kind === "verbatim") return { reason: plan.strategy };
25870
+ if (input.isError === true && plan.strategy !== "test-failure" && plan.strategy !== "shell-output") return { reason: "verbatim-unparsed" };
25871
+ const header = `[cache-warmup preview] strategy=${plan.strategy} input=${text.length}ch`;
25872
+ const footerBudget = footerFor(`ref://${input.sessionId}/${"0".repeat(64)}`).length;
25873
+ const bodyBudget = budget.maxChars - header.length - footerBudget - 2;
25874
+ if (bodyBudget <= 0) return { reason: "verbatim-budget" };
25875
+ const body = fitSections(plan.sections, bodyBudget);
25876
+ if (body === void 0) return { reason: "verbatim-budget" };
25877
+ const store = input.store;
25878
+ if (store === void 0) return { reason: "store-unavailable" };
25879
+ let reference;
25880
+ try {
25881
+ reference = await store.put({
25882
+ sessionId: input.sessionId,
25883
+ content: text,
25884
+ toolName: input.toolName,
25885
+ at: args.at
25886
+ });
25887
+ } catch {
25888
+ return { reason: "store-unavailable" };
25889
+ }
25890
+ const preview = `${header}\n${body}\n${footerFor(reference.refId)}`;
25891
+ if (preview.length > budget.maxChars) return { reason: "verbatim-budget" };
25892
+ return {
25893
+ preview,
25894
+ refId: reference.refId,
25895
+ strategy: plan.strategy
25896
+ };
25897
+ }
25898
+ /** 块数组里文本字符数的合计。 */
25899
+ function textCharsOf(blocks) {
25900
+ let total = 0;
25901
+ for (const block of blocks) if (typeof block.text === "string") total += block.text.length;
25902
+ return total;
25903
+ }
25904
+ /**
25905
+ * 投影一条工具结果。
25906
+ *
25907
+ * 纯函数式语义:不改入参、不写日志、不碰历史;返回新内容 + 度量。
25908
+ * 只有 `changed: true` 时才有 `refId`/`preview`。
25909
+ * @param input - 工具结果与预算。
25910
+ * @returns 投影结果。
25911
+ */
25912
+ async function projectToolResult(input) {
25913
+ const at = input.at ?? Date.now();
25914
+ const budget = input.budget ?? cacheWarmupBudget(input.sessionId);
25915
+ const base = {
25916
+ toolName: input.toolName,
25917
+ sessionId: input.sessionId,
25918
+ provider: input.provider,
25919
+ at
25920
+ };
25921
+ if (typeof input.content === "string") {
25922
+ const text = input.content;
25923
+ const decision = await decide({
25924
+ text,
25925
+ input,
25926
+ budget,
25927
+ at
25928
+ });
25929
+ if ("reason" in decision) return {
25930
+ ...base,
25931
+ content: input.content,
25932
+ changed: false,
25933
+ metrics: {
25934
+ inputChars: text.length,
25935
+ outputChars: text.length,
25936
+ omittedChars: 0,
25937
+ strategy: decision.reason
25938
+ }
25939
+ };
25940
+ return {
25941
+ ...base,
25942
+ content: decision.preview,
25943
+ changed: true,
25944
+ refId: decision.refId,
25945
+ preview: decision.preview,
25946
+ metrics: {
25947
+ inputChars: text.length,
25948
+ outputChars: decision.preview.length,
25949
+ omittedChars: Math.max(0, text.length - decision.preview.length),
25950
+ strategy: decision.strategy
25951
+ }
25952
+ };
25953
+ }
25954
+ const blocks = input.content;
25955
+ if (!(blocks.length === 1 && blocks[0]?.type === "text" && typeof blocks[0]?.text === "string")) {
25956
+ const chars = textCharsOf(blocks);
25957
+ const hasNonText = blocks.some((block) => block.type !== "text");
25958
+ return {
25959
+ ...base,
25960
+ content: input.content,
25961
+ changed: false,
25962
+ metrics: {
25963
+ inputChars: chars,
25964
+ outputChars: chars,
25965
+ omittedChars: 0,
25966
+ strategy: hasNonText ? "verbatim-non-text" : "verbatim-unparsed"
25967
+ }
25968
+ };
25969
+ }
25970
+ const text = blocks[0].text;
25971
+ const decision = await decide({
25972
+ text,
25973
+ input,
25974
+ budget,
25975
+ at
25976
+ });
25977
+ if ("reason" in decision) return {
25978
+ ...base,
25979
+ content: input.content,
25980
+ changed: false,
25981
+ metrics: {
25982
+ inputChars: text.length,
25983
+ outputChars: text.length,
25984
+ omittedChars: 0,
25985
+ strategy: decision.reason
25986
+ }
25987
+ };
25988
+ const rebuilt = {
25989
+ ...blocks[0],
25990
+ text: decision.preview
25991
+ };
25992
+ return {
25993
+ ...base,
25994
+ content: [rebuilt],
25995
+ changed: true,
25996
+ refId: decision.refId,
25997
+ preview: decision.preview,
25998
+ metrics: {
25999
+ inputChars: text.length,
26000
+ outputChars: decision.preview.length,
26001
+ omittedChars: Math.max(0, text.length - decision.preview.length),
26002
+ strategy: decision.strategy
26003
+ }
26004
+ };
26005
+ }
26006
+ //#endregion
24673
26007
  //#region src/host/common/request-images.ts
24674
26008
  function isRecord$41(value) {
24675
26009
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -31526,6 +32860,15 @@ function noteSessionCacheState(sessionId, state) {
31526
32860
  ...lastDrift === void 0 ? {} : { lastDrift },
31527
32861
  updatedAt: now
31528
32862
  });
32863
+ recordCacheWarmupUsage({
32864
+ sessionId,
32865
+ inputTokens: state.inputTokens,
32866
+ cacheReadTokens: state.cacheReadTokens,
32867
+ outputTokens: state.outputTokens,
32868
+ totalTokens: state.cacheReadTokens + state.inputTokens,
32869
+ at: now,
32870
+ ...lastDrift === void 0 ? {} : { driftCause: lastDrift }
32871
+ });
31529
32872
  }
31530
32873
  var KimiCodeAdapter = class extends LlmAdapter {
31531
32874
  store;
@@ -53701,7 +55044,7 @@ function withTimeout$3(signal, ms) {
53701
55044
  return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
53702
55045
  }
53703
55046
  /** 一句可以直接展示给用户的失败原因;正文只截前 200 字,且不含任何请求头。 */
53704
- function failure(detail) {
55047
+ function failure$1(detail) {
53705
55048
  return "GLM(ZCode 账号) " + detail;
53706
55049
  }
53707
55050
  /** 响应正文的一句话摘要(去换行、截断),用于把上游原因带给调用方。 */
@@ -53728,7 +55071,7 @@ function glmOpenAiProbeHeaders(codingPlanKey) {
53728
55071
  */
53729
55072
  function quotaRequestHeaders$1(codingPlanKey) {
53730
55073
  const key = codingPlanKey.trim();
53731
- if (key === "") throw new Error(failure("的配额接口需要一把 Coding Plan Key(裸值,不加 Bearer);空凭据不会变成一个能通过鉴权的请求。"));
55074
+ if (key === "") throw new Error(failure$1("的配额接口需要一把 Coding Plan Key(裸值,不加 Bearer);空凭据不会变成一个能通过鉴权的请求。"));
53732
55075
  return {
53733
55076
  "content-type": "application/json",
53734
55077
  accept: "application/json",
@@ -53810,13 +55153,13 @@ async function fetchCatalog(region, codingPlanKey, options = {}) {
53810
55153
  const text = await response.text().catch(() => "");
53811
55154
  if (!response.ok) {
53812
55155
  const detail = summarize(text);
53813
- throw new Error(failure("读取模型目录失败(HTTP " + response.status + "): " + url) + (detail === "" ? "" : ":" + detail));
55156
+ throw new Error(failure$1("读取模型目录失败(HTTP " + response.status + "): " + url) + (detail === "" ? "" : ":" + detail));
53814
55157
  }
53815
55158
  let payload;
53816
55159
  try {
53817
55160
  payload = JSON.parse(text);
53818
55161
  } catch {
53819
- throw new Error(failure("返回的模型目录不是可读的 JSON:" + url));
55162
+ throw new Error(failure$1("返回的模型目录不是可读的 JSON:" + url));
53820
55163
  }
53821
55164
  return parseCatalogListing(payload, region);
53822
55165
  }
@@ -54071,7 +55414,7 @@ async function fetchSubscriptionPlan(region, codingPlanKey, options = {}) {
54071
55414
  * 所以它同步抛出,与「上游读失败返回 `error`」那条路径分开。
54072
55415
  */
54073
55416
  async function fetchGlmQuota(region, codingPlanKey, options = {}) {
54074
- if (codingPlanKey.trim() === "") throw new Error(failure("的配额读取需要一把 Coding Plan Key(裸值,不加 Bearer)。"));
55417
+ if (codingPlanKey.trim() === "") throw new Error(failure$1("的配额读取需要一把 Coding Plan Key(裸值,不加 Bearer)。"));
54075
55418
  const accountId = options.accountId;
54076
55419
  const cached = quotaEntryFor(accountId);
54077
55420
  if (options.force !== true && cached !== null && Date.now() - cached.at < 12e4) return {
@@ -54097,7 +55440,7 @@ async function fetchGlmQuota(region, codingPlanKey, options = {}) {
54097
55440
  } catch (error) {
54098
55441
  return {
54099
55442
  snapshot: null,
54100
- error: failure("配额接口不可达:" + (error instanceof Error ? error.message : String(error))),
55443
+ error: failure$1("配额接口不可达:" + (error instanceof Error ? error.message : String(error))),
54101
55444
  fromCache: false
54102
55445
  };
54103
55446
  }
@@ -54106,7 +55449,7 @@ async function fetchGlmQuota(region, codingPlanKey, options = {}) {
54106
55449
  const detail = summarize(text);
54107
55450
  return {
54108
55451
  snapshot: null,
54109
- error: failure("配额查询失败(HTTP " + response.status + ")") + (detail === "" ? "" : ":" + detail),
55452
+ error: failure$1("配额查询失败(HTTP " + response.status + ")") + (detail === "" ? "" : ":" + detail),
54110
55453
  fromCache: false
54111
55454
  };
54112
55455
  }
@@ -54116,7 +55459,7 @@ async function fetchGlmQuota(region, codingPlanKey, options = {}) {
54116
55459
  } catch {
54117
55460
  return {
54118
55461
  snapshot: null,
54119
- error: failure("配额查询返回的不是 JSON。"),
55462
+ error: failure$1("配额查询返回的不是 JSON。"),
54120
55463
  fromCache: false
54121
55464
  };
54122
55465
  }
@@ -54129,7 +55472,7 @@ async function fetchGlmQuota(region, codingPlanKey, options = {}) {
54129
55472
  });
54130
55473
  if (snapshot === null) return {
54131
55474
  snapshot: null,
54132
- error: failure("上游没有返回可显示的额度窗口。"),
55475
+ error: failure$1("上游没有返回可显示的额度窗口。"),
54133
55476
  fromCache: false
54134
55477
  };
54135
55478
  return {
@@ -80962,6 +82305,751 @@ function releaseListener(handle) {
80962
82305
  handle?.dispose?.();
80963
82306
  }
80964
82307
  //#endregion
82308
+ //#region src/host/cache-warmup/logging.ts
82309
+ /**
82310
+ * 把一条事件序列化成日志行。
82311
+ *
82312
+ * 形状写死在这里,而不是让调用方自己拼字符串:任何一次「顺手把 content 也带进
82313
+ * 去」的改动都只会发生在这个函数里,而它有测试盯着(断言日志里不含原文)。
82314
+ * 值为 `undefined` 的字段直接不出现在行里,保持每一行只描述它确实测到的指标。
82315
+ * @param event - 指标事件。
82316
+ * @returns 一行日志文本(带上本模块前缀)。
82317
+ */
82318
+ function formatCacheWarmupLog(event) {
82319
+ const record = {};
82320
+ for (const [key, value] of Object.entries(event)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") record[key] = value;
82321
+ return "[cache-warmup] " + JSON.stringify(record);
82322
+ }
82323
+ /**
82324
+ * 把一个异常压成**稳定的身份标签**,只用于宿主日志。
82325
+ *
82326
+ * 只取两样东西:`name`(构造器名;缺失或不是非空字符串时退成 `'Error'`)和
82327
+ * `code`(有就带上——`CacheWarmupStoreError` 与 Node 的系统错误都靠它区分)。
82328
+ *
82329
+ * **`message` 一律不进日志**,这是本函数存在的全部理由:异常消息里常常带着绝对
82330
+ * 路径、命令行片段、凭据,或者被读取结果的一段原文。一条日志就够把原文复制一份,
82331
+ * 或者把主机的目录结构写进部署方的日志管道。想知道"哪一类错误、来自哪一层",
82332
+ * name/code 就够了;想知道"具体哪一条",该去会话本身看,不该从日志里捞。
82333
+ * @param error - 捕获到的任意值。
82334
+ * @returns 形如 `Error`、`RangeError`、`CacheWarmupStoreError/SESSION_MISMATCH`
82335
+ * 的标签(`code` 是非空字符串时以 `/` 连接)。
82336
+ */
82337
+ function cacheWarmupErrorLabel(error) {
82338
+ if (typeof error !== "object" || error === null) return "Error";
82339
+ const record = error;
82340
+ const name = typeof record.name === "string" && record.name !== "" ? record.name : "Error";
82341
+ const code = typeof record.code === "string" && record.code !== "" ? record.code : void 0;
82342
+ return code === void 0 ? name : name + "/" + code;
82343
+ }
82344
+ /**
82345
+ * 发一条指标日志。
82346
+ *
82347
+ * logger 缺席时**安静跳过**:日志不是功能,装不上日志服务的宿主不该因此装不上
82348
+ * 这个模块。写入失败同样吞掉——一条日志把工具调用搞挂是最不划算的失败。
82349
+ * @param logger - 宿主 logger(可选)。
82350
+ * @param event - 指标事件。
82351
+ * @returns 写出的那一行(调用方/测试可以拿去断言);没写出去时 undefined。
82352
+ */
82353
+ function logCacheWarmup(logger, event) {
82354
+ const line = formatCacheWarmupLog(event);
82355
+ const info = logger?.info;
82356
+ if (typeof info !== "function") return void 0;
82357
+ try {
82358
+ info.call(logger, line);
82359
+ } catch {
82360
+ return;
82361
+ }
82362
+ return line;
82363
+ }
82364
+ //#endregion
82365
+ //#region src/host/cache-warmup/result-tool.ts
82366
+ /** 注册的工具名。冻结契约的一部分,不要改动。 */
82367
+ const CACHE_WARMUP_RESULT_TOOL = "cache_warmup_result";
82368
+ /** 是不是一个可用的非负整数。 */
82369
+ function isCount(value) {
82370
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
82371
+ }
82372
+ /** 读一个未知值为有限数字(`null` 不算:契约用 `null` 表示"没有下一页")。 */
82373
+ function numberOf$1(value) {
82374
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
82375
+ }
82376
+ /** 读一个未知值为布尔。 */
82377
+ function booleanOf$1(value) {
82378
+ return typeof value === "boolean" ? value : void 0;
82379
+ }
82380
+ /** 读一个未知值为非空字符串。 */
82381
+ function stringOf$1(value) {
82382
+ return typeof value === "string" && value !== "" ? value : void 0;
82383
+ }
82384
+ /**
82385
+ * 从存储交回来的 `omitted` 里读出省略字符数。
82386
+ *
82387
+ * 契约给的是一个结构化对象(`{ reason, chars, lines, rows, bytes }`,`reason: 'none'`
82388
+ * 表示这一页就是全部);这里同时容忍一个裸数字,因为"省略了多少字符"是唯一
82389
+ * 模型真正需要的那个数,而形状变化不该让整页读取失败。
82390
+ * @param value - 存储返回的 `omitted`。
82391
+ * @returns 省略的字符数;读不到时 undefined。
82392
+ */
82393
+ function omittedCharsOf(value) {
82394
+ const direct = numberOf$1(value);
82395
+ if (direct !== void 0) return direct;
82396
+ if (typeof value !== "object" || value === null) return void 0;
82397
+ return numberOf$1(value.chars);
82398
+ }
82399
+ /**
82400
+ * 造一个失败值。
82401
+ * @param action - 回显的动作。
82402
+ * @param refId - 被拒绝的引用。
82403
+ * @param sessionId - 当前会话(拿不到时为空串)。
82404
+ * @param code - 稳定错误码。
82405
+ * @param message - 一句话说明。
82406
+ * @returns 规范值。
82407
+ */
82408
+ function failure(action, refId, sessionId, code, message) {
82409
+ return {
82410
+ ok: false,
82411
+ action,
82412
+ refId,
82413
+ sessionId,
82414
+ error: {
82415
+ code,
82416
+ message
82417
+ }
82418
+ };
82419
+ }
82420
+ /**
82421
+ * 把未知异常压成一句不含原文的话。
82422
+ *
82423
+ * 异常消息里可能带着被读取内容的片段(解析错误最常见),所以只取消息的第一行,
82424
+ * 长度也截断——工具输出不该成为原文的第二条出口。
82425
+ * @param error - 捕获到的值。
82426
+ * @returns 一句话描述。
82427
+ */
82428
+ function describe(error) {
82429
+ const firstLine = (error instanceof Error ? error.message : String(error)).split("\n", 1)[0] ?? "";
82430
+ return firstLine.length > 160 ? firstLine.slice(0, 157) + "..." : firstLine;
82431
+ }
82432
+ /**
82433
+ * 取结构化错误码:存储自己的码优先,否则用本工具给的兜底码。
82434
+ * @param error - 捕获到的值。
82435
+ * @param fallback - 兜底码。
82436
+ * @returns 错误码。
82437
+ */
82438
+ function errorCodeOf(error, fallback) {
82439
+ const code = error?.code;
82440
+ return typeof code === "string" && code !== "" ? code : fallback;
82441
+ }
82442
+ /**
82443
+ * 把共享存储交回来的页读成规范值。
82444
+ *
82445
+ * 字段一律按「读得到就用、读不到就不出现」处理:核心侧多给字段不会让这里报错,
82446
+ * 少给字段也不会伪造一个 `complete:true`——「不确定是否读完」必须表现成
82447
+ * `complete:false`,把 partial 伪装成完整是这条路线上最贵的一种错。
82448
+ * @param raw - 存储的返回值(`ResultPage`,按共享约定结构化读取)。
82449
+ * @param refId - 引用。
82450
+ * @param sessionId - 会话。
82451
+ * @returns 规范值。
82452
+ */
82453
+ function pageValue(raw, refId, sessionId) {
82454
+ const page = typeof raw === "object" && raw !== null ? raw : {};
82455
+ const text = stringOf$1(page.content) ?? stringOf$1(page.text);
82456
+ const complete = booleanOf$1(page.complete) === true;
82457
+ const offset = numberOf$1(page.offset);
82458
+ const nextOffset = complete ? void 0 : numberOf$1(page.nextOffset);
82459
+ const omitted = omittedCharsOf(page.omitted);
82460
+ const totalChars = numberOf$1(page.totalChars);
82461
+ return {
82462
+ ok: true,
82463
+ action: "page",
82464
+ refId,
82465
+ sessionId,
82466
+ complete,
82467
+ ...offset === void 0 ? {} : { offset },
82468
+ ...nextOffset === void 0 ? {} : { nextOffset },
82469
+ ...omitted === void 0 ? {} : { omitted },
82470
+ ...totalChars === void 0 ? {} : { totalChars },
82471
+ ...text === void 0 ? {} : {
82472
+ text,
82473
+ chars: text.length
82474
+ }
82475
+ };
82476
+ }
82477
+ /**
82478
+ * 造 `cache_warmup_result` 工具。
82479
+ * @param deps - 依赖:线路校验、存储读取、指标日志。
82480
+ * @returns 可注册的工具定义。
82481
+ */
82482
+ function createCacheWarmupResultTool(deps) {
82483
+ /** 发一条日志;安装方没接线时什么也不做。 */
82484
+ const log = (event) => {
82485
+ try {
82486
+ deps.log?.(event);
82487
+ } catch {}
82488
+ };
82489
+ return defineTool({
82490
+ name: CACHE_WARMUP_RESULT_TOOL,
82491
+ description: [
82492
+ "Read back a result that was replaced by a preview, using the `ref://<session>/<sha256>` handle written in that preview.",
82493
+ "Use `action: \"page\"` to read a slice: pass `offset`, and optionally `limit`, `maxBytes`, `fields` and `equals`.",
82494
+ "Every page reports `complete`, `nextOffset`, `omitted` and `totalChars`, so you can see whether more remains instead of assuming a slice is the whole thing.",
82495
+ "Use `action: \"release\"` when the stored copy is no longer needed."
82496
+ ].join(" "),
82497
+ parameters: {
82498
+ action: {
82499
+ type: "string",
82500
+ required: true,
82501
+ description: "page reads a slice of the stored result; release drops the stored copy.",
82502
+ enum: ["page", "release"]
82503
+ },
82504
+ refId: {
82505
+ type: "string",
82506
+ required: true,
82507
+ description: "The `ref://<session>/<sha256>` handle exactly as written in the preview."
82508
+ },
82509
+ offset: {
82510
+ type: "integer",
82511
+ description: "Where to start: a UTF-8 byte offset for text results, a row index for structured ones."
82512
+ },
82513
+ limit: {
82514
+ type: "integer",
82515
+ description: "Maximum number of rows for a structured result."
82516
+ },
82517
+ maxBytes: {
82518
+ type: "integer",
82519
+ description: "Byte budget for this page; the store returns less when the budget is smaller."
82520
+ },
82521
+ fields: {
82522
+ type: "array",
82523
+ items: { type: "string" },
82524
+ description: "Restrict a structured result to these fields."
82525
+ },
82526
+ equals: {
82527
+ type: "object",
82528
+ additionalProperties: true,
82529
+ description: "Keep only structured rows whose named field equals the given string."
82530
+ }
82531
+ },
82532
+ output: {
82533
+ schema: {
82534
+ type: "object",
82535
+ additionalProperties: false,
82536
+ properties: {
82537
+ ok: {
82538
+ type: "boolean",
82539
+ description: "Whether the call succeeded."
82540
+ },
82541
+ action: {
82542
+ type: "string",
82543
+ enum: ["page", "release"],
82544
+ description: "The action that ran."
82545
+ },
82546
+ refId: {
82547
+ type: "string",
82548
+ description: "The handle this call addressed."
82549
+ },
82550
+ sessionId: {
82551
+ type: "string",
82552
+ description: "The session that owns the handle."
82553
+ },
82554
+ complete: {
82555
+ type: "boolean",
82556
+ description: "Whether this page reached the end of the stored result."
82557
+ },
82558
+ offset: {
82559
+ type: "number",
82560
+ description: "Where this page actually started."
82561
+ },
82562
+ nextOffset: {
82563
+ type: "number",
82564
+ description: "Where the next page should start."
82565
+ },
82566
+ omitted: {
82567
+ type: "number",
82568
+ description: "Characters still not returned after this page."
82569
+ },
82570
+ chars: {
82571
+ type: "number",
82572
+ description: "Characters in this page."
82573
+ },
82574
+ totalChars: {
82575
+ type: "number",
82576
+ description: "Characters in the whole stored result."
82577
+ },
82578
+ released: {
82579
+ type: "boolean",
82580
+ description: "Whether a stored copy was actually released."
82581
+ },
82582
+ text: {
82583
+ type: "string",
82584
+ description: "This page of the stored result."
82585
+ },
82586
+ error: {
82587
+ type: "object",
82588
+ additionalProperties: false,
82589
+ properties: {
82590
+ code: {
82591
+ type: "string",
82592
+ description: "Stable error code."
82593
+ },
82594
+ message: {
82595
+ type: "string",
82596
+ description: "One-line explanation."
82597
+ }
82598
+ }
82599
+ }
82600
+ }
82601
+ },
82602
+ render(_args, value) {
82603
+ if (value.ok !== true) {
82604
+ const code = value.error?.code ?? "FAILED";
82605
+ const message = value.error?.message ?? "the request was refused";
82606
+ return [{
82607
+ type: "text",
82608
+ text: "cache_warmup_result " + value.action + " refused (" + code + "): " + message
82609
+ }];
82610
+ }
82611
+ if (value.action === "release") return [{
82612
+ type: "text",
82613
+ text: value.released === true ? "Released " + value.refId + ". The preview in your history is unchanged; it can no longer be read back." : "Nothing stored for " + value.refId + "; nothing to release."
82614
+ }];
82615
+ const at = value.offset ?? 0;
82616
+ const span = typeof value.chars === "number" ? value.chars + " chars" : "no content";
82617
+ const total = typeof value.totalChars === "number" ? " of " + value.totalChars : "";
82618
+ const tail = value.complete === true ? "complete" : "more remains" + (typeof value.nextOffset === "number" ? ", continue at offset " + value.nextOffset : "") + (typeof value.omitted === "number" ? ", " + value.omitted + " chars omitted" : "");
82619
+ const body = typeof value.text === "string" && value.text !== "" ? value.text : "(empty page)";
82620
+ return [{
82621
+ type: "text",
82622
+ text: value.refId + " from offset " + at + " — " + span + total + ", " + tail + "\n\n" + body
82623
+ }];
82624
+ }
82625
+ },
82626
+ async execute(args, exec) {
82627
+ const route = deps.routeOf(exec);
82628
+ const refId = args.refId;
82629
+ const action = args.action;
82630
+ if (route.provider !== deps.provider) return failure(action, refId, route.sessionId ?? "", "WRONG_PROVIDER", "ref handles belong to the " + deps.provider + " line; this session is routed to \"" + (route.provider ?? "unknown") + "\"");
82631
+ const sessionId = route.sessionId;
82632
+ if (sessionId === void 0) return failure(action, refId, "", "NO_SESSION", "the current session could not be resolved, so the handle cannot be checked");
82633
+ const store = deps.store();
82634
+ if (store === void 0) return failure(action, refId, sessionId, "NO_STORE", "the result store is unavailable on this host");
82635
+ if (action === "release") {
82636
+ let released = false;
82637
+ try {
82638
+ const raw = await store.release({
82639
+ sessionId,
82640
+ refId
82641
+ });
82642
+ released = booleanOf$1(raw) ?? booleanOf$1(raw?.released) ?? false;
82643
+ } catch (error) {
82644
+ return failure(action, refId, sessionId, errorCodeOf(error, "STORE_FAILED"), "releasing the stored copy failed: " + describe(error));
82645
+ }
82646
+ log({
82647
+ event: "ref-release",
82648
+ session: sessionId,
82649
+ refId,
82650
+ released
82651
+ });
82652
+ return {
82653
+ ok: true,
82654
+ action,
82655
+ refId,
82656
+ sessionId,
82657
+ released
82658
+ };
82659
+ }
82660
+ for (const [name, value] of [
82661
+ ["offset", args.offset],
82662
+ ["limit", args.limit],
82663
+ ["maxBytes", args.maxBytes]
82664
+ ]) if (value !== void 0 && !isCount(value)) return failure(action, refId, sessionId, "BAD_ARGUMENT", name + " must be a non-negative integer");
82665
+ const request = {
82666
+ sessionId,
82667
+ refId
82668
+ };
82669
+ if (args.offset !== void 0) request.offset = args.offset;
82670
+ if (args.limit !== void 0) request.limit = args.limit;
82671
+ if (args.maxBytes !== void 0) request.maxBytes = args.maxBytes;
82672
+ if (args.fields !== void 0) request.fields = args.fields;
82673
+ if (args.equals !== void 0) request.equals = args.equals;
82674
+ let raw;
82675
+ try {
82676
+ raw = await store.page(request);
82677
+ } catch (error) {
82678
+ return failure(action, refId, sessionId, errorCodeOf(error, "STORE_FAILED"), "reading the stored copy failed: " + describe(error));
82679
+ }
82680
+ const value = pageValue(raw, refId, sessionId);
82681
+ log({
82682
+ event: "ref-page",
82683
+ session: sessionId,
82684
+ refId,
82685
+ offset: value.offset ?? args.offset ?? 0,
82686
+ returnedChars: value.chars ?? 0,
82687
+ complete: value.complete === true,
82688
+ ...value.nextOffset === void 0 ? {} : { nextOffset: value.nextOffset }
82689
+ });
82690
+ return value;
82691
+ }
82692
+ });
82693
+ }
82694
+ //#endregion
82695
+ //#region src/host/cache-warmup/install.ts
82696
+ /**
82697
+ * 本模块服务的线路。
82698
+ *
82699
+ * 结果投影的正确性依赖这条线路的缓存语义(内容前缀命中),别的线路上把结果换成
82700
+ * 预览只有坏处没有好处,所以这里写死一个 provider 名而不是做成开关。
82701
+ */
82702
+ const CACHE_WARMUP_PROVIDER = "kimi-code";
82703
+ /** 提示词分节名,全局唯一(重复注册会抛错)。 */
82704
+ const CACHE_WARMUP_SECTION_NAME = "plugin:cache-warmup";
82705
+ /**
82706
+ * 一段**恒定**的引导词:告诉模型预览是什么、怎么补读。
82707
+ *
82708
+ * 文本与开关无关,这是刻意的——系统提示是缓存前缀的第一段,会话中途改一个字就
82709
+ * 让整段前缀失效、整段重新计费。开关的语义只交给 post-execute 与工具。
82710
+ */
82711
+ const CACHE_WARMUP_GUIDANCE = [
82712
+ "## Result previews",
82713
+ "",
82714
+ "On large `run_code` results this line may replace the output with a preview: a short description plus one or more `ref://<session>/<sha256>` handles.",
82715
+ "",
82716
+ "- A preview is **not** the full result. It states `complete`, `omitted` and `nextOffset` explicitly; treat an incomplete preview as incomplete data, not as the whole output.",
82717
+ "- To read the original, call `cache_warmup_result` with `action: \"page\"` and that `refId`, then follow `nextOffset` until `complete` is true.",
82718
+ "- `action: \"release\"` drops the stored copy. The preview already in the conversation stays identical; it simply can no longer be read back.",
82719
+ "- Source code, type and contract definitions, mixed evidence and anything that could not be parsed reliably are never replaced: they arrive in full."
82720
+ ].join("\n");
82721
+ /**
82722
+ * 静态引导词的文本提供者。
82723
+ * @returns 一个永远返回同一段文本的函数(系统提示的稳定前缀)。
82724
+ */
82725
+ function cacheWarmupSectionText() {
82726
+ return () => CACHE_WARMUP_GUIDANCE;
82727
+ }
82728
+ /**
82729
+ * 读一次工具调用所属的线路。
82730
+ *
82731
+ * provider 先问会话的 request header(那是本轮请求真正走的那条线),再退回
82732
+ * agent 自己的 options。两处都没有就返回空对象——调用方据此放手,而不是猜一个。
82733
+ * @param agent - 事件里的调用方 agent。
82734
+ * @returns 线路信息;读不到的字段省略。
82735
+ */
82736
+ function cacheWarmupRouteOf(agent) {
82737
+ const record = typeof agent === "object" && agent !== null ? agent : void 0;
82738
+ const session = record?.session;
82739
+ let provider;
82740
+ try {
82741
+ provider = stringOf(session?.requestHeader?.()?.config?.provider);
82742
+ } catch {
82743
+ provider = void 0;
82744
+ }
82745
+ if (provider === void 0) provider = stringOf(record?.options?.provider);
82746
+ const sessionId = stringOf(session?.header?.id) ?? stringOf(record?.id);
82747
+ return {
82748
+ ...provider === void 0 ? {} : { provider },
82749
+ ...sessionId === void 0 ? {} : { sessionId }
82750
+ };
82751
+ }
82752
+ /**
82753
+ * 预热窗口结束时,本次算「稳住了」还是「掉了」。
82754
+ *
82755
+ * 阈值取核心契约里的 `consecutiveHitsRequired`(连续高命中率报告条数),而不是在
82756
+ * 这里另编一个命中率——判定标准只有核心一份,日志只如实记录它当时的结论。
82757
+ * @param consecutiveHits - 退出时的连续命中条数;读不到时 undefined。
82758
+ * @returns 稳定的退出原因。
82759
+ */
82760
+ function exitReasonOf(consecutiveHits) {
82761
+ return consecutiveHits !== void 0 && consecutiveHits >= CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired ? "settled" : "dropped";
82762
+ }
82763
+ /** 单实例登记表,走一个 global symbol(与进展更新模块共用一个表)。 */
82764
+ const MOUNTED = Symbol.for("dsh-chatgpt-subscription.mounted-modules");
82765
+ /** 本模块在登记表里的名字。 */
82766
+ const MODULE_ID = "cache-warmup";
82767
+ /**
82768
+ * 读一个未知值为非空字符串。
82769
+ * @param value - 候选值。
82770
+ * @returns 非空字符串,其它类型的 undefined。
82771
+ */
82772
+ function stringOf(value) {
82773
+ return typeof value === "string" && value !== "" ? value : void 0;
82774
+ }
82775
+ /**
82776
+ * 读一个未知值为有限数字。
82777
+ * @param value - 候选值。
82778
+ * @returns 有限数字,其它类型的 undefined。
82779
+ */
82780
+ function numberOf(value) {
82781
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
82782
+ }
82783
+ /**
82784
+ * 读一个未知值为布尔。
82785
+ * @param value - 候选值。
82786
+ * @returns 布尔,其它类型的 undefined。
82787
+ */
82788
+ function booleanOf(value) {
82789
+ return typeof value === "boolean" ? value : void 0;
82790
+ }
82791
+ /**
82792
+ * 单实例守卫:本模块已挂载时放弃本次安装。
82793
+ * @param ctx - 宿主上下文。
82794
+ * @returns 释放函数(交出挂载权);已经挂载过时返回 undefined。
82795
+ */
82796
+ function claimMount(ctx) {
82797
+ const registry = globalThis;
82798
+ const existing = registry[MOUNTED];
82799
+ const mounted = existing instanceof Set ? existing : /* @__PURE__ */ new Set();
82800
+ if (existing !== mounted) registry[MOUNTED] = mounted;
82801
+ if (mounted.has(MODULE_ID)) return void 0;
82802
+ mounted.add(MODULE_ID);
82803
+ const release = () => {
82804
+ mounted.delete(MODULE_ID);
82805
+ };
82806
+ if (typeof ctx.effect === "function") ctx.effect(() => release, "dsh-chatgpt-subscription: cache warmup mount");
82807
+ return release;
82808
+ }
82809
+ /**
82810
+ * 读一次会话级指标快照。
82811
+ *
82812
+ * 形状按共享约定**结构化**推断,任何字段缺失都不影响日志;读不出来就返回
82813
+ * undefined,让调用方退回预算对象上的那一份。
82814
+ * @param sessionId - 会话标识。
82815
+ * @returns 快照,或读不到时的 undefined。
82816
+ */
82817
+ function readCacheWarmupSnapshot(sessionId) {
82818
+ try {
82819
+ const raw = cacheWarmupSnapshot(sessionId);
82820
+ return typeof raw === "object" && raw !== null ? raw : void 0;
82821
+ } catch {
82822
+ return;
82823
+ }
82824
+ }
82825
+ /**
82826
+ * 挂载「缓存预热」的宿主半边。
82827
+ * @param ctx - 宿主插件上下文。
82828
+ * @param options - 挂载选项(设置读取器、可选的存储工厂)。
82829
+ * @returns 卸载函数;重复挂载时返回一个空操作。
82830
+ */
82831
+ function installCacheWarmup(ctx, options) {
82832
+ const release = claimMount(ctx);
82833
+ if (release === void 0) return () => void 0;
82834
+ /** 每次调用重新读服务:宿主树可能在本模块挂载之后才 settle,读一次存下来会漏。 */
82835
+ const serviceOf = (name) => {
82836
+ const read = ctx.get;
82837
+ if (typeof read !== "function") return void 0;
82838
+ try {
82839
+ return read.call(ctx, name);
82840
+ } catch {
82841
+ return;
82842
+ }
82843
+ };
82844
+ /** 登记一个副作用;上下文没有 effect 时立即执行(测试用假上下文)。 */
82845
+ const track = (execute, label) => {
82846
+ if (typeof ctx.effect === "function") ctx.effect(execute, label);
82847
+ else execute();
82848
+ };
82849
+ /** 日志出口(每次现读,logger 可能晚于本模块挂载)。 */
82850
+ const loggerOf = () => ctx.logger ?? serviceOf("logger");
82851
+ /** 发一条指标日志。 */
82852
+ const log = (event) => {
82853
+ logCacheWarmup(loggerOf(), event);
82854
+ };
82855
+ /** 惰性构造出来的存储(注入优先)。 */
82856
+ let ownedStore;
82857
+ /**
82858
+ * 取结果引用存储。
82859
+ *
82860
+ * 构造失败只意味着「这台主机上没有引用存储」:此时投影不生效(原样返回),
82861
+ * `cache_warmup_result` 返回结构化错误。都不是一次安装失败。
82862
+ * @returns 存储,或拿不到时的 undefined。
82863
+ */
82864
+ const storeOf = () => {
82865
+ if (options.store !== void 0) try {
82866
+ return options.store();
82867
+ } catch (error) {
82868
+ loggerOf()?.warn?.("[dsh-chatgpt-subscription] cache warmup store injection failed: " + cacheWarmupErrorLabel(error));
82869
+ return;
82870
+ }
82871
+ if (ownedStore !== void 0) return ownedStore;
82872
+ try {
82873
+ ownedStore = new FileResultReferenceStore();
82874
+ } catch (error) {
82875
+ loggerOf()?.warn?.("[dsh-chatgpt-subscription] cache warmup store unavailable: " + cacheWarmupErrorLabel(error));
82876
+ return;
82877
+ }
82878
+ return ownedStore;
82879
+ };
82880
+ track(() => {
82881
+ const tools = serviceOf("tools");
82882
+ if (tools === void 0) return () => void 0;
82883
+ return tools.register(createCacheWarmupResultTool({
82884
+ provider: CACHE_WARMUP_PROVIDER,
82885
+ routeOf: (exec) => cacheWarmupRouteOf(exec?.agent),
82886
+ store: storeOf,
82887
+ log
82888
+ }));
82889
+ }, "dsh-chatgpt-subscription: cache warmup result tool");
82890
+ track(() => {
82891
+ const systemPrompt = serviceOf("systemPrompt");
82892
+ if (systemPrompt === void 0) return () => void 0;
82893
+ return systemPrompt.section({
82894
+ name: CACHE_WARMUP_SECTION_NAME,
82895
+ order: 225,
82896
+ text: cacheWarmupSectionText()
82897
+ });
82898
+ }, "dsh-chatgpt-subscription: cache warmup prompt section");
82899
+ /** 上一轮看到的预热状态,用来在窗口结束时发一条 `warmup-exit`。 */
82900
+ const warmupState = /* @__PURE__ */ new Map();
82901
+ /**
82902
+ * 记账并(必要时)发一条 `warmup-exit`。
82903
+ * @param sessionId - 会话标识。
82904
+ * @param budget - 本轮读到的预算对象。
82905
+ */
82906
+ const trackWarmup = (sessionId, budget) => {
82907
+ const snapshot = readCacheWarmupSnapshot(sessionId);
82908
+ const warmup = booleanOf(snapshot?.warmup) ?? booleanOf(budget.warmup);
82909
+ const consecutiveHits = numberOf(snapshot?.consecutiveHits) ?? numberOf(budget.consecutiveHits);
82910
+ const hitRatio = numberOf(snapshot?.hitRatio) ?? numberOf(budget.hitRatio);
82911
+ if (warmupState.get(sessionId) === true && warmup === false) {
82912
+ const reason = exitReasonOf(consecutiveHits);
82913
+ log({
82914
+ event: "warmup-exit",
82915
+ session: sessionId,
82916
+ reason,
82917
+ ...consecutiveHits === void 0 ? {} : { consecutiveHits },
82918
+ ...hitRatio === void 0 ? {} : { hitRatio }
82919
+ });
82920
+ }
82921
+ if (warmup !== void 0) warmupState.set(sessionId, warmup);
82922
+ return {
82923
+ ...warmup === void 0 ? {} : { warmup },
82924
+ ...consecutiveHits === void 0 ? {} : { consecutiveHits },
82925
+ ...hitRatio === void 0 ? {} : { hitRatio }
82926
+ };
82927
+ };
82928
+ /**
82929
+ * 一次工具结果的投影。
82930
+ *
82931
+ * 决策取舍写在代码里,因为它全是「什么时候**不动**」:
82932
+ * - 不是 `accept`(`block` 的 feedback 是别人的东西);
82933
+ * - 已经带 `value`(执行本地的 canonical 值,replay 与结构化消费方读它,
82934
+ * 替换掉等于把一份 JSON 换成字符串);
82935
+ * - 工具名不是 `run_code`、线路不是本线、会话拿不到;
82936
+ * - 没有存储、内容为空。
82937
+ * 上面任何一条成立就原样返回同一个决策对象。
82938
+ * @param exec - 工具执行上下文。
82939
+ * @param result - 归一化后的执行结果。
82940
+ * @param decision - `next()` 交回来的决策。
82941
+ * @returns 投影后的决策(形状不变),或原决策。
82942
+ */
82943
+ const projectDecision = async (exec, result, decision) => {
82944
+ if (decision.kind !== "accept") return decision;
82945
+ if (decision.value !== void 0) return decision;
82946
+ if (exec.name !== "run_code") return decision;
82947
+ if (!options.settings().enabled) return decision;
82948
+ const route = cacheWarmupRouteOf(exec.agent);
82949
+ if (route.provider !== "kimi-code") return decision;
82950
+ const sessionId = route.sessionId;
82951
+ if (sessionId === void 0) return decision;
82952
+ const store = storeOf();
82953
+ if (store === void 0) return decision;
82954
+ const blocks = decision.content ?? result.content;
82955
+ if (blocks.length === 0) return decision;
82956
+ const content = blocks;
82957
+ const budget = cacheWarmupBudget(sessionId);
82958
+ const projected = await projectToolResult({
82959
+ toolName: exec.name,
82960
+ provider: route.provider,
82961
+ sessionId,
82962
+ content,
82963
+ isError: result.isError === true,
82964
+ budget,
82965
+ store
82966
+ }).catch((error) => {
82967
+ loggerOf()?.warn?.("[dsh-chatgpt-subscription] cache warmup projection skipped: " + cacheWarmupErrorLabel(error));
82968
+ });
82969
+ if (projected === void 0) return decision;
82970
+ const metrics = trackWarmup(sessionId, budget);
82971
+ log({
82972
+ event: "projection",
82973
+ session: sessionId,
82974
+ strategy: String(projected.metrics.strategy),
82975
+ changed: projected.changed === true,
82976
+ inputChars: projected.metrics.inputChars,
82977
+ outputChars: projected.metrics.outputChars,
82978
+ omittedChars: projected.metrics.omittedChars,
82979
+ ...metrics.warmup === void 0 ? {} : { warmup: metrics.warmup }
82980
+ });
82981
+ if (projected.changed !== true) return decision;
82982
+ return {
82983
+ kind: "accept",
82984
+ content: blocksOf(projected.content),
82985
+ ...decision.additionalContexts === void 0 ? {} : { additionalContexts: decision.additionalContexts }
82986
+ };
82987
+ };
82988
+ const disposeListener = ctx.on?.("tools/post-execute", (async (exec, result, next) => {
82989
+ const decision = await next();
82990
+ return projectDecision(exec, result, decision);
82991
+ }));
82992
+ return () => {
82993
+ disposeListener?.();
82994
+ warmupState.clear();
82995
+ release();
82996
+ };
82997
+ }
82998
+ /**
82999
+ * 把投影结果的内容转回宿主的内容块数组。
83000
+ *
83001
+ * 核心的 `ProjectionResult.content` 允许纯文本(预览就是一段文本),也允许块数组
83002
+ * (只替换了其中一个文本块,非文本块原样保留)。宿主的 `PostToolDecision.content`
83003
+ * 只认块数组,所以纯文本在这里包成一个 text 块——包一层,不改字。
83004
+ * @param content - 核心交回来的内容。
83005
+ * @returns 宿主的块数组。
83006
+ */
83007
+ function blocksOf(content) {
83008
+ if (typeof content === "string") return [{
83009
+ type: "text",
83010
+ text: content
83011
+ }];
83012
+ return content;
83013
+ }
83014
+ /**
83015
+ * 本文件刻意**不保留**任何"把异常压成一句话"的辅助函数。
83016
+ *
83017
+ * 这里曾经有一个 `describe(error)`,把 `error.message` 截断后拼进宿主日志。它看上去
83018
+ * 无害(只取第一行、还截了长度),实际上是一条原文出口:异常消息里常常带着绝对
83019
+ * 路径、命令行片段、凭据,或者被读取结果的一段正文。三处降级日志现在统一走
83020
+ * {@link cacheWarmupErrorLabel}——只输出稳定的 name/code。
83021
+ */
83022
+ //#endregion
83023
+ //#region src/host/cache-warmup/settings.ts
83024
+ /**
83025
+ * 设置卡上这一行的说明文字。
83026
+ *
83027
+ * schema(`src/index.ts` 的 `Config`)与设置卡共用同一份文案:两处各写一份,
83028
+ * 迟早变成「卡片说只影响新结果、schema 说立刻生效」这种自相矛盾。
83029
+ */
83030
+ const CACHE_WARMUP_SETTING_DESCRIPTION = "启用缓存预热的结果投影:Kimi 线路上可以可靠结构化提取的超大工具结果会被替换成带 ref:// 引用的预览,需要原文时由 cache_warmup_result 精确补读。关闭只影响之后产生的新结果,已进入历史的内容不变。";
83031
+ /**
83032
+ * 读取一个配置字段,沿着 volatile 活引用取当前值。
83033
+ * @param field - 字段本身。
83034
+ * @returns 当前值,或字段缺失时的 undefined。
83035
+ */
83036
+ function readCacheWarmupConfigField(field) {
83037
+ if (field === void 0 || field === null) return void 0;
83038
+ if (typeof field === "object" && typeof field.get === "function") return field.get();
83039
+ return field;
83040
+ }
83041
+ /**
83042
+ * 解析一次读取的有效设置。
83043
+ *
83044
+ * 每次调用都重读活引用,因此设置保存后立刻生效(对**之后**的新结果生效;历史
83045
+ * 结果不重新投影,见模块说明)。
83046
+ * @param config - 主入口收到的插件配置(活引用或普通值)。
83047
+ * @returns 生效的开关。
83048
+ */
83049
+ function resolveCacheWarmupSettings(config) {
83050
+ return { enabled: readCacheWarmupConfigField(config?.cacheWarmup) !== false };
83051
+ }
83052
+ //#endregion
80965
83053
  //#region src/index.ts
80966
83054
  const Config = z.object({
80967
83055
  fetchProvider: z.union([
@@ -80985,6 +83073,7 @@ const Config = z.object({
80985
83073
  z.const("zh"),
80986
83074
  z.const("en")
80987
83075
  ]).default("auto").volatile().description("进展更新的输出语言:auto 跟随对话语言,zh 中文,en 英文。"),
83076
+ cacheWarmup: z.boolean().default(true).volatile().description(CACHE_WARMUP_SETTING_DESCRIPTION),
80988
83077
  codexStreamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).step(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS$2),
80989
83078
  poolStorage: z.union([z.const("keyring"), z.const("credentials")]).default("keyring").description("号池存储后端:keyring(默认,桌面平台的系统密钥环)或 credentials(DSH 凭据库,服务器推荐)。")
80990
83079
  });
@@ -81102,6 +83191,7 @@ function apply(ctx, pluginConfig = {}) {
81102
83191
  return dispose;
81103
83192
  }, "dsh-chatgpt-subscription: no-answer resume");
81104
83193
  }
83194
+ ctx.effect(() => installCacheWarmup(ctx, { settings: () => resolveCacheWarmupSettings(pluginConfig) }), "dsh-chatgpt-subscription: cache warmup");
81105
83195
  const probeEnvFile = relayProbeEnvFile();
81106
83196
  if (relayProbeEnabled(process.env, { envFile: probeEnvFile })) {
81107
83197
  const probePath = relayProbeLogPath(process.env, { envFile: probeEnvFile });