compressed-context 0.1.0 → 0.1.1

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 (55) hide show
  1. package/package.json +64 -64
  2. package/src/compression/active-summary-cache.ts +50 -50
  3. package/src/compression/adopt.ts +57 -57
  4. package/src/compression/auto.ts +372 -372
  5. package/src/compression/babel.ts +85 -85
  6. package/src/compression/block.ts +25 -25
  7. package/src/compression/cache-history.ts +75 -75
  8. package/src/compression/compaction.ts +189 -189
  9. package/src/compression/decompress-tool.ts +74 -74
  10. package/src/compression/decompress.ts +292 -292
  11. package/src/compression/deleted-sessions.ts +24 -24
  12. package/src/compression/distill.ts +220 -220
  13. package/src/compression/events.ts +275 -275
  14. package/src/compression/markdown.ts +53 -53
  15. package/src/compression/normalize.ts +114 -114
  16. package/src/compression/render.ts +70 -70
  17. package/src/compression/schema.ts +84 -84
  18. package/src/compression/select.ts +123 -123
  19. package/src/compression/session-message.ts +112 -112
  20. package/src/compression/settings-rpc.ts +111 -111
  21. package/src/compression/settings.ts +175 -175
  22. package/src/compression/state.ts +248 -248
  23. package/src/compression/summarize.ts +595 -595
  24. package/src/compression/summary.ts +73 -73
  25. package/src/compression/tool.ts +142 -142
  26. package/src/compression/transcript-store.ts +258 -258
  27. package/src/compression/transcript.ts +63 -63
  28. package/src/compression/validate.ts +81 -81
  29. package/src/i18n/en.ts +93 -93
  30. package/src/i18n/index.ts +28 -28
  31. package/src/i18n/types.ts +89 -89
  32. package/src/i18n/zh.ts +93 -93
  33. package/src/index.ts +108 -108
  34. package/src/shared/blocks.ts +17 -17
  35. package/src/shared/cache.ts +199 -199
  36. package/src/shared/format.ts +23 -23
  37. package/src/shared/media.ts +184 -184
  38. package/src/shared/parse.ts +25 -25
  39. package/src/shared/parts.ts +72 -72
  40. package/src/shared/rpc.ts +145 -145
  41. package/src/shared/scan.ts +32 -32
  42. package/src/shared/settings.ts +471 -471
  43. package/src/shared/stats.ts +142 -142
  44. package/src/shared/tokens.ts +259 -259
  45. package/src/tui/chart/cache-chart.ts +237 -237
  46. package/src/tui/chart/color.ts +58 -58
  47. package/src/tui/index.tsx +111 -111
  48. package/src/tui/panel/cache.tsx +203 -203
  49. package/src/tui/panel/data.ts +105 -105
  50. package/src/tui/panel/index.tsx +176 -176
  51. package/src/tui/panel/summaries.tsx +192 -192
  52. package/src/tui/settings/dialogs.tsx +215 -215
  53. package/src/tui/settings/options.ts +238 -238
  54. package/src/tui/settings.tsx +418 -418
  55. package/src/tui/types.ts +4 -4
@@ -1,471 +1,471 @@
1
- import { readInt } from "./parse.ts"
2
-
3
- /**
4
- * TUI 面板设置。
5
- *
6
- * 存在 TUI 自己的 storage 里(`context.storage.store`),用户不用改 config.json。
7
- * 优先级:UI 改过的值 > 插件 options 给的初始值 > 内置默认。
8
- *
9
- * 摘要上限按「窗口百分比」存储:换模型、换窗口后自动缩放,不会因为旧的绝对值放不下。
10
- */
11
-
12
- export const LOCALE_SETTINGS = ["auto", "zh", "en"] as const
13
-
14
- export type LocaleSetting = (typeof LOCALE_SETTINGS)[number]
15
-
16
- /** 摘要语言;auto 跟随源对话。 */
17
- export const SUMMARY_LANGUAGES = ["auto", "zh", "en"] as const
18
-
19
- export type SummaryLanguage = (typeof SUMMARY_LANGUAGES)[number]
20
-
21
- /** 摘要格式:JSON 结构化、自由 Markdown,或模型间高密度 Babel。 */
22
- export const SUMMARY_FORMATS = ["json", "markdown", "babel"] as const
23
-
24
- export type SummaryFormat = (typeof SUMMARY_FORMATS)[number]
25
-
26
- export const DEFAULT_SUMMARY_FORMAT: SummaryFormat = "markdown"
27
-
28
- /** 输入:任意值;输出:是否是合法摘要格式。 */
29
- export function isSummaryFormat(value: unknown): value is SummaryFormat {
30
- return typeof value === "string" && (SUMMARY_FORMATS as readonly string[]).includes(value)
31
- }
32
-
33
- /** 单条摘要的最小字符数;只用于把绝对值输入换算成百分比时的下界。 */
34
- export const MIN_SUMMARY_BLOCK_CHARS = 500
35
-
36
- /** 窗口未知时(还没拿到模型信息)退回的绝对值上限。 */
37
- export const DEFAULT_SUMMARY_BLOCK_CHARS = 20_000
38
- export const DEFAULT_SUMMARY_TOTAL_CHARS = 100_000
39
-
40
- /** 摘要上限允许的窗口百分比范围。 */
41
- export const MIN_SUMMARY_PERCENT = 1
42
- export const MAX_SUMMARY_PERCENT = 90
43
- export const DEFAULT_SUMMARY_BLOCK_PERCENT = 10
44
- export const DEFAULT_SUMMARY_TOTAL_PERCENT = 50
45
-
46
- /**
47
- * 上下文窗口里预留给 system/tools/recent 的 token 数。
48
- * 摘要上限按 1 字符 ≈ 1 token 的最坏情况折算,保证 CJK 摘要也不会撑爆窗口。
49
- */
50
- export const SUMMARY_WINDOW_RESERVE = 10_000
51
-
52
- /** 上下文窗口上限:auto 复用模型原生值,数字表示自定义 token 数。 */
53
- export type ContextLimit = "auto" | number
54
-
55
- export const MIN_TRIGGER_PERCENT = 60
56
- export const MAX_TRIGGER_PERCENT = 95
57
- export const DEFAULT_TRIGGER_PERCENT = 80
58
-
59
- /** 主动压缩提醒的默认增长量;0 表示关闭。 */
60
- export const DEFAULT_NUDGE_GROWTH_TOKENS = 50_000
61
- export const MIN_NUDGE_GROWTH_TOKENS = 0
62
- export const MAX_NUDGE_GROWTH_TOKENS = 200_000
63
-
64
- /** 可用窗口下限:低于它连最小摘要都放不下。 */
65
- export const MIN_CONTEXT_LIMIT = 50_000
66
- export const MAX_CONTEXT_LIMIT = 10_000_000
67
-
68
- /** 自动压缩总开关:关掉后只保留手动 /compact 和 compress 工具。 */
69
- export const DEFAULT_AUTO_COMPRESS = true
70
-
71
- /** 工具开关:关掉后工具仍注册,但调用会返回明确错误。 */
72
- export const DEFAULT_ENABLE_COMPRESS_TOOL = true
73
- export const DEFAULT_ENABLE_DECOMPRESS_TOOL = true
74
-
75
- /** 块头是否显示时间线 / 估算 token;默认关闭,避免多占 token。 */
76
- export const DEFAULT_SHOW_TIMELINE = false
77
- export const DEFAULT_SHOW_BLOCK_TOKENS = false
78
-
79
- /** 面板显示项;都是 TUI 本地状态,不发给服务端。 */
80
- export const DEFAULT_CACHE_EXPANDED = true
81
- export const DEFAULT_SHOW_CACHE_CHART = true
82
- export const DEFAULT_CACHE_CHART_EXPANDED = true
83
- export const DEFAULT_SHOW_CACHE_LOG = true
84
- export const DEFAULT_CACHE_LOG_EXPANDED = false
85
- export const DEFAULT_SHOW_SUMMARIES = true
86
- export const DEFAULT_SUMMARIES_EXPANDED = true
87
-
88
- /** decompress 返回分级与临时文件上限。 */
89
- export const DEFAULT_DECOMPRESS_INLINE_MAX = 10_000
90
- export const DEFAULT_DECOMPRESS_TEMP_CAP = 50
91
- export const DEFAULT_DECOMPRESS_SHOW_TOKENS = true
92
- export const MIN_DECOMPRESS_INLINE_MAX = 1_000
93
- export const MAX_DECOMPRESS_INLINE_MAX = 1_000_000
94
- export const MIN_DECOMPRESS_TEMP_CAP = 1
95
- export const MAX_DECOMPRESS_TEMP_CAP = 1_000
96
-
97
- /** 服务端关心的压缩设置;TUI 的 Settings 在此基础上加显示项。 */
98
- export interface CompressionSettings {
99
- /** 上下文窗口上限;数字是上限,实际取「设置值」和「模型原生窗口」中较小的那个 */
100
- contextLimit: ContextLimit
101
- /** 强制压缩阈值(占用百分比) */
102
- triggerPercent: number
103
- /** 每增长多少 token 提醒模型可以主动压缩;0 关闭 */
104
- nudgeGrowthTokens: number
105
- /** 单条摘要最多占窗口的百分比 */
106
- summaryBlockPercent: number
107
- /** 全部摘要加起来最多占窗口的百分比 */
108
- summaryTotalPercent: number
109
- /** 摘要语言;只影响提示词 */
110
- summaryLanguage: SummaryLanguage
111
- /** 摘要格式:JSON 结构化 / Markdown 自然语言 / Babel 模型间高密度 */
112
- summaryFormat: SummaryFormat
113
- /** 自动压缩总开关 */
114
- autoCompress: boolean
115
- /** compress 工具开关 */
116
- enableCompressTool: boolean
117
- /** decompress 工具开关 */
118
- enableDecompressTool: boolean
119
- /** 块头显示时间线 */
120
- showTimeline: boolean
121
- /** 块头显示估算 token */
122
- showBlockTokens: boolean
123
- /** decompress 内联上限(字符) */
124
- decompressInlineMax: number
125
- /** decompress 临时文件上限 */
126
- decompressTempCap: number
127
- /** decompress 返回是否带估算 token */
128
- decompressShowTokens: boolean
129
- }
130
-
131
- export interface Settings extends CompressionSettings {
132
- /** 是否显示整块上下文面板 */
133
- enabled: boolean
134
- /** 面板是否展开(折叠后只剩标题行) */
135
- expanded: boolean
136
- /** 界面语言;auto 跟随系统 */
137
- locale: LocaleSetting
138
- /** 缓存记录区是否展开 */
139
- cacheExpanded: boolean
140
- /** 显示缓存折线图 */
141
- showCacheChart: boolean
142
- /** 折线图是否展开 */
143
- cacheChartExpanded: boolean
144
- /** 显示缓存日志 */
145
- showCacheLog: boolean
146
- /** 日志是否展开 */
147
- cacheLogExpanded: boolean
148
- /** 显示摘要块 */
149
- showSummaries: boolean
150
- /** 摘要区是否展开 */
151
- summariesExpanded: boolean
152
- /** 当前展开的摘要块 id;undefined 表示全部收起 */
153
- expandedBlockId?: string
154
- }
155
-
156
- export const DEFAULT_SETTINGS: Settings = {
157
- enabled: true,
158
- expanded: true,
159
- locale: "auto",
160
- contextLimit: "auto",
161
- triggerPercent: DEFAULT_TRIGGER_PERCENT,
162
- nudgeGrowthTokens: DEFAULT_NUDGE_GROWTH_TOKENS,
163
- summaryBlockPercent: DEFAULT_SUMMARY_BLOCK_PERCENT,
164
- summaryTotalPercent: DEFAULT_SUMMARY_TOTAL_PERCENT,
165
- summaryLanguage: "auto",
166
- summaryFormat: DEFAULT_SUMMARY_FORMAT,
167
- autoCompress: DEFAULT_AUTO_COMPRESS,
168
- enableCompressTool: DEFAULT_ENABLE_COMPRESS_TOOL,
169
- enableDecompressTool: DEFAULT_ENABLE_DECOMPRESS_TOOL,
170
- showTimeline: DEFAULT_SHOW_TIMELINE,
171
- showBlockTokens: DEFAULT_SHOW_BLOCK_TOKENS,
172
- cacheExpanded: DEFAULT_CACHE_EXPANDED,
173
- showCacheChart: DEFAULT_SHOW_CACHE_CHART,
174
- cacheChartExpanded: DEFAULT_CACHE_CHART_EXPANDED,
175
- showCacheLog: DEFAULT_SHOW_CACHE_LOG,
176
- cacheLogExpanded: DEFAULT_CACHE_LOG_EXPANDED,
177
- showSummaries: DEFAULT_SHOW_SUMMARIES,
178
- summariesExpanded: DEFAULT_SUMMARIES_EXPANDED,
179
- decompressInlineMax: DEFAULT_DECOMPRESS_INLINE_MAX,
180
- decompressTempCap: DEFAULT_DECOMPRESS_TEMP_CAP,
181
- decompressShowTokens: DEFAULT_DECOMPRESS_SHOW_TOKENS,
182
- }
183
-
184
- /** 输入:任意值;输出:是否是合法语言设置。 */
185
- export function isLocaleSetting(value: unknown): value is LocaleSetting {
186
- return typeof value === "string" && (LOCALE_SETTINGS as readonly string[]).includes(value)
187
- }
188
-
189
- /** 输入:任意值;输出:是否是合法摘要语言。 */
190
- export function isSummaryLanguage(value: unknown): value is SummaryLanguage {
191
- return typeof value === "string" && (SUMMARY_LANGUAGES as readonly string[]).includes(value)
192
- }
193
-
194
- /** 输入:任意值;输出:合法的主动压缩提醒增长量;0 表示关闭。 */
195
- export function readNudgeGrowthTokens(value: unknown, fallback: number): number {
196
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback
197
- const rounded = Math.round(value)
198
- if (rounded === 0) return 0
199
- if (rounded < 1_000 || rounded > MAX_NUDGE_GROWTH_TOKENS) return fallback
200
- return rounded
201
- }
202
-
203
- /** 输入:任意值;输出:合法的窗口百分比(保留一位小数)。 */
204
- export function readSummaryPercent(value: unknown, fallback: number): number {
205
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback
206
- const rounded = Math.round(value * 10) / 10
207
- if (rounded < MIN_SUMMARY_PERCENT || rounded > MAX_SUMMARY_PERCENT) return fallback
208
- return rounded
209
- }
210
-
211
- /**
212
- * 输入:用户输入的摘要上限文本(`10%` / `20k` / `20000`)+ 当前窗口。
213
- * 输出:窗口百分比;非法或无法换算返回 undefined。
214
- * 预期:绝对值按窗口换算;窗口未知时只接受百分比写法。
215
- */
216
- export function parseSummaryPercent(text: string, windowTokens: number | undefined): number | undefined {
217
- const trimmed = text.trim()
218
- if (trimmed.endsWith("%")) {
219
- const value = Number(trimmed.slice(0, -1))
220
- if (!Number.isFinite(value)) return undefined
221
- const percent = Math.round(value * 10) / 10
222
- if (percent < MIN_SUMMARY_PERCENT || percent > MAX_SUMMARY_PERCENT) return undefined
223
- return percent
224
- }
225
- if (windowTokens === undefined || !Number.isFinite(windowTokens) || windowTokens <= 0) return undefined
226
- const tokens = parseTokenCount(trimmed, MIN_SUMMARY_BLOCK_CHARS, Math.floor(windowTokens))
227
- if (tokens === undefined) return undefined
228
- const percent = Math.round((tokens / windowTokens) * 1000) / 10
229
- if (percent < MIN_SUMMARY_PERCENT || percent > MAX_SUMMARY_PERCENT) return undefined
230
- return percent
231
- }
232
-
233
- /** 输入:百分比 + 窗口;输出:折算后的字符上限;窗口未知返回 undefined。 */
234
- export function percentToChars(percent: number, windowTokens: number | undefined): number | undefined {
235
- if (windowTokens === undefined || !Number.isFinite(windowTokens) || windowTokens <= 0) return undefined
236
- return Math.round((windowTokens * percent) / 100)
237
- }
238
-
239
- /**
240
- * 输入:窗口 token 数。
241
- * 输出:摘要百分比允许的最大值(扣掉预留)。
242
- * 预期:窗口未知时返回全局上限;窗口太小放不下最小摘要时返回 MIN。
243
- */
244
- export function maxSummaryPercent(windowTokens: number | undefined): number {
245
- if (windowTokens === undefined || !Number.isFinite(windowTokens) || windowTokens <= 0) {
246
- return MAX_SUMMARY_PERCENT
247
- }
248
- const physical = ((windowTokens - SUMMARY_WINDOW_RESERVE) / windowTokens) * 100
249
- return Math.max(MIN_SUMMARY_PERCENT, Math.min(MAX_SUMMARY_PERCENT, Math.floor(physical)))
250
- }
251
-
252
- /**
253
- * 输入:用户输入的 token 文本(支持 `200k` / `1m` / `200000`)+ 范围。
254
- * 输出:token 数;非法或越界返回 undefined。
255
- * 预期:大小写不敏感,允许小数(如 `1.5m`)。
256
- */
257
- export function parseTokenCount(text: string, min: number, max: number): number | undefined {
258
- const match = /^(\d+(?:\.\d+)?)\s*([km])?$/i.exec(text.trim())
259
- if (match === null) return undefined
260
- const value = Number(match[1])
261
- if (!Number.isFinite(value) || value <= 0) return undefined
262
- const unit = match[2]?.toLowerCase()
263
- const multiplier = unit === "k" ? 1_000 : unit === "m" ? 1_000_000 : 1
264
- const tokens = Math.round(value * multiplier)
265
- if (tokens < min || tokens > max) return undefined
266
- return tokens
267
- }
268
-
269
- /** 输入:用户输入的上下文上限文本;输出:token 数;非法或越界返回 undefined。 */
270
- export function parseTokenLimit(text: string): number | undefined {
271
- return parseTokenCount(text, MIN_CONTEXT_LIMIT, MAX_CONTEXT_LIMIT)
272
- }
273
-
274
- /** 输入:任意输入文本;输出:只保留数字 + 结尾单个 k/m 的合法形态。 */
275
- export function sanitizeLimitInput(value: string): string {
276
- const cleaned = value.toLowerCase().replace(/[^0-9km]/g, "")
277
- const match = /^(\d*)([km]?)/.exec(cleaned)
278
- return `${match?.[1] ?? ""}${match?.[2] ?? ""}`
279
- }
280
-
281
- /** 输入:任意值;输出:是否是合法上下文上限。 */
282
- export function isContextLimit(value: unknown): value is ContextLimit {
283
- if (value === "auto") return true
284
- return (
285
- typeof value === "number" &&
286
- Number.isFinite(value) &&
287
- value >= MIN_CONTEXT_LIMIT &&
288
- value <= MAX_CONTEXT_LIMIT
289
- )
290
- }
291
-
292
- /** 输入:任意值;输出:合法的上下文上限设置(支持 `200k` 这类字符串)。 */
293
- function readContextLimit(value: unknown): ContextLimit {
294
- if (isContextLimit(value)) return value
295
- if (typeof value === "string") return parseTokenLimit(value) ?? DEFAULT_SETTINGS.contextLimit
296
- return DEFAULT_SETTINGS.contextLimit
297
- }
298
-
299
- /**
300
- * 输入:任意设置对象(可能来自旧版本、缺字段或类型不对)。
301
- * 输出:补齐后的合法设置。
302
- * 预期:用于启动时迁移 storage 里的旧数据,绝不抛错;旧的绝对值字段不再读取,统一回到百分比默认。
303
- */
304
- export function normalizeSettings(value: Readonly<Record<string, unknown>>): Settings {
305
- const percent = value["triggerPercent"]
306
- const blockPercent = readSummaryPercent(value["summaryBlockPercent"], DEFAULT_SUMMARY_BLOCK_PERCENT)
307
- const totalPercent = readSummaryPercent(value["summaryTotalPercent"], DEFAULT_SUMMARY_TOTAL_PERCENT)
308
- return {
309
- enabled: typeof value["enabled"] === "boolean" ? value["enabled"] : DEFAULT_SETTINGS.enabled,
310
- expanded: typeof value["expanded"] === "boolean" ? value["expanded"] : DEFAULT_SETTINGS.expanded,
311
- locale: isLocaleSetting(value["locale"]) ? value["locale"] : DEFAULT_SETTINGS.locale,
312
- contextLimit: readContextLimit(value["contextLimit"]),
313
- triggerPercent:
314
- typeof percent === "number" && Number.isFinite(percent) && percent >= MIN_TRIGGER_PERCENT && percent <= MAX_TRIGGER_PERCENT
315
- ? Math.round(percent)
316
- : DEFAULT_SETTINGS.triggerPercent,
317
- nudgeGrowthTokens: readNudgeGrowthTokens(value["nudgeGrowthTokens"], DEFAULT_NUDGE_GROWTH_TOKENS),
318
- summaryBlockPercent: blockPercent,
319
- summaryTotalPercent: totalPercent,
320
- summaryLanguage: isSummaryLanguage(value["summaryLanguage"]) ? value["summaryLanguage"] : "auto",
321
- summaryFormat: isSummaryFormat(value["summaryFormat"]) ? value["summaryFormat"] : DEFAULT_SUMMARY_FORMAT,
322
- autoCompress: typeof value["autoCompress"] === "boolean" ? value["autoCompress"] : DEFAULT_AUTO_COMPRESS,
323
- enableCompressTool:
324
- typeof value["enableCompressTool"] === "boolean" ? value["enableCompressTool"] : DEFAULT_ENABLE_COMPRESS_TOOL,
325
- enableDecompressTool:
326
- typeof value["enableDecompressTool"] === "boolean" ? value["enableDecompressTool"] : DEFAULT_ENABLE_DECOMPRESS_TOOL,
327
- showTimeline: typeof value["showTimeline"] === "boolean" ? value["showTimeline"] : DEFAULT_SHOW_TIMELINE,
328
- showBlockTokens: typeof value["showBlockTokens"] === "boolean" ? value["showBlockTokens"] : DEFAULT_SHOW_BLOCK_TOKENS,
329
- cacheExpanded: typeof value["cacheExpanded"] === "boolean" ? value["cacheExpanded"] : DEFAULT_CACHE_EXPANDED,
330
- showCacheChart: typeof value["showCacheChart"] === "boolean" ? value["showCacheChart"] : DEFAULT_SHOW_CACHE_CHART,
331
- cacheChartExpanded:
332
- typeof value["cacheChartExpanded"] === "boolean" ? value["cacheChartExpanded"] : DEFAULT_CACHE_CHART_EXPANDED,
333
- showCacheLog: typeof value["showCacheLog"] === "boolean" ? value["showCacheLog"] : DEFAULT_SHOW_CACHE_LOG,
334
- cacheLogExpanded:
335
- typeof value["cacheLogExpanded"] === "boolean" ? value["cacheLogExpanded"] : DEFAULT_CACHE_LOG_EXPANDED,
336
- showSummaries: typeof value["showSummaries"] === "boolean" ? value["showSummaries"] : DEFAULT_SHOW_SUMMARIES,
337
- summariesExpanded:
338
- typeof value["summariesExpanded"] === "boolean" ? value["summariesExpanded"] : DEFAULT_SUMMARIES_EXPANDED,
339
- ...(typeof value["expandedBlockId"] === "string" && value["expandedBlockId"].length > 0
340
- ? { expandedBlockId: value["expandedBlockId"] }
341
- : {}),
342
- decompressInlineMax: readInt(
343
- value["decompressInlineMax"],
344
- MIN_DECOMPRESS_INLINE_MAX,
345
- MAX_DECOMPRESS_INLINE_MAX,
346
- DEFAULT_DECOMPRESS_INLINE_MAX,
347
- ),
348
- decompressTempCap: readInt(
349
- value["decompressTempCap"],
350
- MIN_DECOMPRESS_TEMP_CAP,
351
- MAX_DECOMPRESS_TEMP_CAP,
352
- DEFAULT_DECOMPRESS_TEMP_CAP,
353
- ),
354
- decompressShowTokens:
355
- typeof value["decompressShowTokens"] === "boolean" ? value["decompressShowTokens"] : DEFAULT_DECOMPRESS_SHOW_TOKENS,
356
- }
357
- }
358
-
359
- /**
360
- * 输入:插件 options 的原始值(可能缺失或类型不对)。
361
- * 输出:合法的初始设置。
362
- * 预期:非法值回退默认值,绝不抛错。
363
- */
364
- export function initialSettings(options: Readonly<Record<string, unknown>>): Settings {
365
- return normalizeSettings(options)
366
- }
367
-
368
- /** 输入:设置对象(可能是热重载前的旧对象,字段缺失)。输出:合法的单条摘要百分比。 */
369
- export function effectiveSummaryBlockPercent(settings: Settings): number {
370
- return readSummaryPercent(settings.summaryBlockPercent, DEFAULT_SUMMARY_BLOCK_PERCENT)
371
- }
372
-
373
- /** 输入:设置对象。输出:合法的全部摘要百分比。 */
374
- export function effectiveSummaryTotalPercent(settings: Settings): number {
375
- return readSummaryPercent(settings.summaryTotalPercent, DEFAULT_SUMMARY_TOTAL_PERCENT)
376
- }
377
-
378
- /** 输入:设置;输出:合法的摘要语言。 */
379
- export function effectiveSummaryLanguage(settings: Settings): SummaryLanguage {
380
- return isSummaryLanguage(settings.summaryLanguage) ? settings.summaryLanguage : "auto"
381
- }
382
-
383
- /** 输入:设置;输出:合法的 decompress 内联上限。 */
384
- export function effectiveDecompressInlineMax(settings: { readonly decompressInlineMax?: unknown }): number {
385
- return readInt(
386
- settings.decompressInlineMax,
387
- MIN_DECOMPRESS_INLINE_MAX,
388
- MAX_DECOMPRESS_INLINE_MAX,
389
- DEFAULT_DECOMPRESS_INLINE_MAX,
390
- )
391
- }
392
-
393
- /** 输入:设置;输出:合法的 decompress 临时文件上限。 */
394
- export function effectiveDecompressTempCap(settings: { readonly decompressTempCap?: unknown }): number {
395
- return readInt(
396
- settings.decompressTempCap,
397
- MIN_DECOMPRESS_TEMP_CAP,
398
- MAX_DECOMPRESS_TEMP_CAP,
399
- DEFAULT_DECOMPRESS_TEMP_CAP,
400
- )
401
- }
402
-
403
- /** 输入:TUI 设置;输出:发给服务端的压缩设置子集;预期:字段列表只在这里维护。 */
404
- export function serverSettingsPayload(settings: Settings): CompressionSettings {
405
- return {
406
- contextLimit: settings.contextLimit,
407
- triggerPercent: settings.triggerPercent,
408
- nudgeGrowthTokens: settings.nudgeGrowthTokens,
409
- summaryBlockPercent: effectiveSummaryBlockPercent(settings),
410
- summaryTotalPercent: effectiveSummaryTotalPercent(settings),
411
- summaryLanguage: effectiveSummaryLanguage(settings),
412
- summaryFormat: settings.summaryFormat,
413
- autoCompress: settings.autoCompress,
414
- enableCompressTool: settings.enableCompressTool,
415
- enableDecompressTool: settings.enableDecompressTool,
416
- showTimeline: settings.showTimeline,
417
- showBlockTokens: settings.showBlockTokens,
418
- decompressInlineMax: effectiveDecompressInlineMax(settings),
419
- decompressTempCap: effectiveDecompressTempCap(settings),
420
- decompressShowTokens: settings.decompressShowTokens,
421
- }
422
- }
423
-
424
- /**
425
- * 输入:设置(结构同 Settings,允许缺字段)+ 已知窗口 token 数。
426
- * 输出:折算后的字符上限与合并预算。
427
- * 预期:窗口未知时退回默认绝对值;窗口已知时按百分比折算,并扣除 system/tools/recent 预留。
428
- */
429
- export function resolveSummaryLimits(
430
- settings: {
431
- readonly summaryBlockPercent?: unknown
432
- readonly summaryTotalPercent?: unknown
433
- },
434
- windowTokens: number | undefined,
435
- ): { blockChars: number; totalChars: number; budgetTokens: number } {
436
- const blockPercent = effectiveSummaryBlockPercent(settings as Settings)
437
- const totalPercent = effectiveSummaryTotalPercent(settings as Settings)
438
- if (windowTokens === undefined || !Number.isFinite(windowTokens) || windowTokens <= 0) {
439
- return {
440
- blockChars: DEFAULT_SUMMARY_BLOCK_CHARS,
441
- totalChars: DEFAULT_SUMMARY_TOTAL_CHARS,
442
- budgetTokens: DEFAULT_SUMMARY_TOTAL_CHARS,
443
- }
444
- }
445
- const physical = Math.max(0, Math.floor(windowTokens) - SUMMARY_WINDOW_RESERVE)
446
- const blockChars = Math.min(physical, Math.max(1, Math.round((windowTokens * blockPercent) / 100)))
447
- const totalChars = Math.min(physical, Math.max(1, Math.round((windowTokens * totalPercent) / 100)))
448
- return { blockChars, totalChars, budgetTokens: totalChars }
449
- }
450
-
451
- /**
452
- * 输入:语言设置。
453
- * 输出:传给 resolveMessages 的 locale。
454
- * 预期:auto 返回 undefined(交给系统检测),其余原样返回。
455
- */
456
- export function localeArgument(locale: LocaleSetting): string | undefined {
457
- return locale === "auto" ? undefined : locale
458
- }
459
-
460
- /**
461
- * 输入:设置 + 模型原生上下文长度。
462
- * 输出:实际使用的上下文上限;未知时返回 undefined。
463
- * 预期:数字设置是上限,不能超过模型原生窗口;auto 直接用原生值。
464
- */
465
- export function effectiveLimit(settings: Settings, native: number): number | undefined {
466
- const value: unknown = settings.contextLimit
467
- if (isContextLimit(value) && value !== "auto") {
468
- return native > 0 ? Math.min(value, native) : value
469
- }
470
- return native > 0 ? native : undefined
471
- }
1
+ import { readInt } from "./parse.ts"
2
+
3
+ /**
4
+ * TUI 面板设置。
5
+ *
6
+ * 存在 TUI 自己的 storage 里(`context.storage.store`),用户不用改 config.json。
7
+ * 优先级:UI 改过的值 > 插件 options 给的初始值 > 内置默认。
8
+ *
9
+ * 摘要上限按「窗口百分比」存储:换模型、换窗口后自动缩放,不会因为旧的绝对值放不下。
10
+ */
11
+
12
+ export const LOCALE_SETTINGS = ["auto", "zh", "en"] as const
13
+
14
+ export type LocaleSetting = (typeof LOCALE_SETTINGS)[number]
15
+
16
+ /** 摘要语言;auto 跟随源对话。 */
17
+ export const SUMMARY_LANGUAGES = ["auto", "zh", "en"] as const
18
+
19
+ export type SummaryLanguage = (typeof SUMMARY_LANGUAGES)[number]
20
+
21
+ /** 摘要格式:JSON 结构化、自由 Markdown,或模型间高密度 Babel。 */
22
+ export const SUMMARY_FORMATS = ["json", "markdown", "babel"] as const
23
+
24
+ export type SummaryFormat = (typeof SUMMARY_FORMATS)[number]
25
+
26
+ export const DEFAULT_SUMMARY_FORMAT: SummaryFormat = "markdown"
27
+
28
+ /** 输入:任意值;输出:是否是合法摘要格式。 */
29
+ export function isSummaryFormat(value: unknown): value is SummaryFormat {
30
+ return typeof value === "string" && (SUMMARY_FORMATS as readonly string[]).includes(value)
31
+ }
32
+
33
+ /** 单条摘要的最小字符数;只用于把绝对值输入换算成百分比时的下界。 */
34
+ export const MIN_SUMMARY_BLOCK_CHARS = 500
35
+
36
+ /** 窗口未知时(还没拿到模型信息)退回的绝对值上限。 */
37
+ export const DEFAULT_SUMMARY_BLOCK_CHARS = 20_000
38
+ export const DEFAULT_SUMMARY_TOTAL_CHARS = 100_000
39
+
40
+ /** 摘要上限允许的窗口百分比范围。 */
41
+ export const MIN_SUMMARY_PERCENT = 1
42
+ export const MAX_SUMMARY_PERCENT = 90
43
+ export const DEFAULT_SUMMARY_BLOCK_PERCENT = 10
44
+ export const DEFAULT_SUMMARY_TOTAL_PERCENT = 50
45
+
46
+ /**
47
+ * 上下文窗口里预留给 system/tools/recent 的 token 数。
48
+ * 摘要上限按 1 字符 ≈ 1 token 的最坏情况折算,保证 CJK 摘要也不会撑爆窗口。
49
+ */
50
+ export const SUMMARY_WINDOW_RESERVE = 10_000
51
+
52
+ /** 上下文窗口上限:auto 复用模型原生值,数字表示自定义 token 数。 */
53
+ export type ContextLimit = "auto" | number
54
+
55
+ export const MIN_TRIGGER_PERCENT = 60
56
+ export const MAX_TRIGGER_PERCENT = 95
57
+ export const DEFAULT_TRIGGER_PERCENT = 80
58
+
59
+ /** 主动压缩提醒的默认增长量;0 表示关闭。 */
60
+ export const DEFAULT_NUDGE_GROWTH_TOKENS = 50_000
61
+ export const MIN_NUDGE_GROWTH_TOKENS = 0
62
+ export const MAX_NUDGE_GROWTH_TOKENS = 200_000
63
+
64
+ /** 可用窗口下限:低于它连最小摘要都放不下。 */
65
+ export const MIN_CONTEXT_LIMIT = 50_000
66
+ export const MAX_CONTEXT_LIMIT = 10_000_000
67
+
68
+ /** 自动压缩总开关:关掉后只保留手动 /compact 和 compress 工具。 */
69
+ export const DEFAULT_AUTO_COMPRESS = true
70
+
71
+ /** 工具开关:关掉后工具仍注册,但调用会返回明确错误。 */
72
+ export const DEFAULT_ENABLE_COMPRESS_TOOL = true
73
+ export const DEFAULT_ENABLE_DECOMPRESS_TOOL = true
74
+
75
+ /** 块头是否显示时间线 / 估算 token;默认关闭,避免多占 token。 */
76
+ export const DEFAULT_SHOW_TIMELINE = false
77
+ export const DEFAULT_SHOW_BLOCK_TOKENS = false
78
+
79
+ /** 面板显示项;都是 TUI 本地状态,不发给服务端。 */
80
+ export const DEFAULT_CACHE_EXPANDED = true
81
+ export const DEFAULT_SHOW_CACHE_CHART = true
82
+ export const DEFAULT_CACHE_CHART_EXPANDED = true
83
+ export const DEFAULT_SHOW_CACHE_LOG = true
84
+ export const DEFAULT_CACHE_LOG_EXPANDED = false
85
+ export const DEFAULT_SHOW_SUMMARIES = true
86
+ export const DEFAULT_SUMMARIES_EXPANDED = true
87
+
88
+ /** decompress 返回分级与临时文件上限。 */
89
+ export const DEFAULT_DECOMPRESS_INLINE_MAX = 10_000
90
+ export const DEFAULT_DECOMPRESS_TEMP_CAP = 50
91
+ export const DEFAULT_DECOMPRESS_SHOW_TOKENS = true
92
+ export const MIN_DECOMPRESS_INLINE_MAX = 1_000
93
+ export const MAX_DECOMPRESS_INLINE_MAX = 1_000_000
94
+ export const MIN_DECOMPRESS_TEMP_CAP = 1
95
+ export const MAX_DECOMPRESS_TEMP_CAP = 1_000
96
+
97
+ /** 服务端关心的压缩设置;TUI 的 Settings 在此基础上加显示项。 */
98
+ export interface CompressionSettings {
99
+ /** 上下文窗口上限;数字是上限,实际取「设置值」和「模型原生窗口」中较小的那个 */
100
+ contextLimit: ContextLimit
101
+ /** 强制压缩阈值(占用百分比) */
102
+ triggerPercent: number
103
+ /** 每增长多少 token 提醒模型可以主动压缩;0 关闭 */
104
+ nudgeGrowthTokens: number
105
+ /** 单条摘要最多占窗口的百分比 */
106
+ summaryBlockPercent: number
107
+ /** 全部摘要加起来最多占窗口的百分比 */
108
+ summaryTotalPercent: number
109
+ /** 摘要语言;只影响提示词 */
110
+ summaryLanguage: SummaryLanguage
111
+ /** 摘要格式:JSON 结构化 / Markdown 自然语言 / Babel 模型间高密度 */
112
+ summaryFormat: SummaryFormat
113
+ /** 自动压缩总开关 */
114
+ autoCompress: boolean
115
+ /** compress 工具开关 */
116
+ enableCompressTool: boolean
117
+ /** decompress 工具开关 */
118
+ enableDecompressTool: boolean
119
+ /** 块头显示时间线 */
120
+ showTimeline: boolean
121
+ /** 块头显示估算 token */
122
+ showBlockTokens: boolean
123
+ /** decompress 内联上限(字符) */
124
+ decompressInlineMax: number
125
+ /** decompress 临时文件上限 */
126
+ decompressTempCap: number
127
+ /** decompress 返回是否带估算 token */
128
+ decompressShowTokens: boolean
129
+ }
130
+
131
+ export interface Settings extends CompressionSettings {
132
+ /** 是否显示整块上下文面板 */
133
+ enabled: boolean
134
+ /** 面板是否展开(折叠后只剩标题行) */
135
+ expanded: boolean
136
+ /** 界面语言;auto 跟随系统 */
137
+ locale: LocaleSetting
138
+ /** 缓存记录区是否展开 */
139
+ cacheExpanded: boolean
140
+ /** 显示缓存折线图 */
141
+ showCacheChart: boolean
142
+ /** 折线图是否展开 */
143
+ cacheChartExpanded: boolean
144
+ /** 显示缓存日志 */
145
+ showCacheLog: boolean
146
+ /** 日志是否展开 */
147
+ cacheLogExpanded: boolean
148
+ /** 显示摘要块 */
149
+ showSummaries: boolean
150
+ /** 摘要区是否展开 */
151
+ summariesExpanded: boolean
152
+ /** 当前展开的摘要块 id;undefined 表示全部收起 */
153
+ expandedBlockId?: string
154
+ }
155
+
156
+ export const DEFAULT_SETTINGS: Settings = {
157
+ enabled: true,
158
+ expanded: true,
159
+ locale: "auto",
160
+ contextLimit: "auto",
161
+ triggerPercent: DEFAULT_TRIGGER_PERCENT,
162
+ nudgeGrowthTokens: DEFAULT_NUDGE_GROWTH_TOKENS,
163
+ summaryBlockPercent: DEFAULT_SUMMARY_BLOCK_PERCENT,
164
+ summaryTotalPercent: DEFAULT_SUMMARY_TOTAL_PERCENT,
165
+ summaryLanguage: "auto",
166
+ summaryFormat: DEFAULT_SUMMARY_FORMAT,
167
+ autoCompress: DEFAULT_AUTO_COMPRESS,
168
+ enableCompressTool: DEFAULT_ENABLE_COMPRESS_TOOL,
169
+ enableDecompressTool: DEFAULT_ENABLE_DECOMPRESS_TOOL,
170
+ showTimeline: DEFAULT_SHOW_TIMELINE,
171
+ showBlockTokens: DEFAULT_SHOW_BLOCK_TOKENS,
172
+ cacheExpanded: DEFAULT_CACHE_EXPANDED,
173
+ showCacheChart: DEFAULT_SHOW_CACHE_CHART,
174
+ cacheChartExpanded: DEFAULT_CACHE_CHART_EXPANDED,
175
+ showCacheLog: DEFAULT_SHOW_CACHE_LOG,
176
+ cacheLogExpanded: DEFAULT_CACHE_LOG_EXPANDED,
177
+ showSummaries: DEFAULT_SHOW_SUMMARIES,
178
+ summariesExpanded: DEFAULT_SUMMARIES_EXPANDED,
179
+ decompressInlineMax: DEFAULT_DECOMPRESS_INLINE_MAX,
180
+ decompressTempCap: DEFAULT_DECOMPRESS_TEMP_CAP,
181
+ decompressShowTokens: DEFAULT_DECOMPRESS_SHOW_TOKENS,
182
+ }
183
+
184
+ /** 输入:任意值;输出:是否是合法语言设置。 */
185
+ export function isLocaleSetting(value: unknown): value is LocaleSetting {
186
+ return typeof value === "string" && (LOCALE_SETTINGS as readonly string[]).includes(value)
187
+ }
188
+
189
+ /** 输入:任意值;输出:是否是合法摘要语言。 */
190
+ export function isSummaryLanguage(value: unknown): value is SummaryLanguage {
191
+ return typeof value === "string" && (SUMMARY_LANGUAGES as readonly string[]).includes(value)
192
+ }
193
+
194
+ /** 输入:任意值;输出:合法的主动压缩提醒增长量;0 表示关闭。 */
195
+ export function readNudgeGrowthTokens(value: unknown, fallback: number): number {
196
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback
197
+ const rounded = Math.round(value)
198
+ if (rounded === 0) return 0
199
+ if (rounded < 1_000 || rounded > MAX_NUDGE_GROWTH_TOKENS) return fallback
200
+ return rounded
201
+ }
202
+
203
+ /** 输入:任意值;输出:合法的窗口百分比(保留一位小数)。 */
204
+ export function readSummaryPercent(value: unknown, fallback: number): number {
205
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback
206
+ const rounded = Math.round(value * 10) / 10
207
+ if (rounded < MIN_SUMMARY_PERCENT || rounded > MAX_SUMMARY_PERCENT) return fallback
208
+ return rounded
209
+ }
210
+
211
+ /**
212
+ * 输入:用户输入的摘要上限文本(`10%` / `20k` / `20000`)+ 当前窗口。
213
+ * 输出:窗口百分比;非法或无法换算返回 undefined。
214
+ * 预期:绝对值按窗口换算;窗口未知时只接受百分比写法。
215
+ */
216
+ export function parseSummaryPercent(text: string, windowTokens: number | undefined): number | undefined {
217
+ const trimmed = text.trim()
218
+ if (trimmed.endsWith("%")) {
219
+ const value = Number(trimmed.slice(0, -1))
220
+ if (!Number.isFinite(value)) return undefined
221
+ const percent = Math.round(value * 10) / 10
222
+ if (percent < MIN_SUMMARY_PERCENT || percent > MAX_SUMMARY_PERCENT) return undefined
223
+ return percent
224
+ }
225
+ if (windowTokens === undefined || !Number.isFinite(windowTokens) || windowTokens <= 0) return undefined
226
+ const tokens = parseTokenCount(trimmed, MIN_SUMMARY_BLOCK_CHARS, Math.floor(windowTokens))
227
+ if (tokens === undefined) return undefined
228
+ const percent = Math.round((tokens / windowTokens) * 1000) / 10
229
+ if (percent < MIN_SUMMARY_PERCENT || percent > MAX_SUMMARY_PERCENT) return undefined
230
+ return percent
231
+ }
232
+
233
+ /** 输入:百分比 + 窗口;输出:折算后的字符上限;窗口未知返回 undefined。 */
234
+ export function percentToChars(percent: number, windowTokens: number | undefined): number | undefined {
235
+ if (windowTokens === undefined || !Number.isFinite(windowTokens) || windowTokens <= 0) return undefined
236
+ return Math.round((windowTokens * percent) / 100)
237
+ }
238
+
239
+ /**
240
+ * 输入:窗口 token 数。
241
+ * 输出:摘要百分比允许的最大值(扣掉预留)。
242
+ * 预期:窗口未知时返回全局上限;窗口太小放不下最小摘要时返回 MIN。
243
+ */
244
+ export function maxSummaryPercent(windowTokens: number | undefined): number {
245
+ if (windowTokens === undefined || !Number.isFinite(windowTokens) || windowTokens <= 0) {
246
+ return MAX_SUMMARY_PERCENT
247
+ }
248
+ const physical = ((windowTokens - SUMMARY_WINDOW_RESERVE) / windowTokens) * 100
249
+ return Math.max(MIN_SUMMARY_PERCENT, Math.min(MAX_SUMMARY_PERCENT, Math.floor(physical)))
250
+ }
251
+
252
+ /**
253
+ * 输入:用户输入的 token 文本(支持 `200k` / `1m` / `200000`)+ 范围。
254
+ * 输出:token 数;非法或越界返回 undefined。
255
+ * 预期:大小写不敏感,允许小数(如 `1.5m`)。
256
+ */
257
+ export function parseTokenCount(text: string, min: number, max: number): number | undefined {
258
+ const match = /^(\d+(?:\.\d+)?)\s*([km])?$/i.exec(text.trim())
259
+ if (match === null) return undefined
260
+ const value = Number(match[1])
261
+ if (!Number.isFinite(value) || value <= 0) return undefined
262
+ const unit = match[2]?.toLowerCase()
263
+ const multiplier = unit === "k" ? 1_000 : unit === "m" ? 1_000_000 : 1
264
+ const tokens = Math.round(value * multiplier)
265
+ if (tokens < min || tokens > max) return undefined
266
+ return tokens
267
+ }
268
+
269
+ /** 输入:用户输入的上下文上限文本;输出:token 数;非法或越界返回 undefined。 */
270
+ export function parseTokenLimit(text: string): number | undefined {
271
+ return parseTokenCount(text, MIN_CONTEXT_LIMIT, MAX_CONTEXT_LIMIT)
272
+ }
273
+
274
+ /** 输入:任意输入文本;输出:只保留数字 + 结尾单个 k/m 的合法形态。 */
275
+ export function sanitizeLimitInput(value: string): string {
276
+ const cleaned = value.toLowerCase().replace(/[^0-9km]/g, "")
277
+ const match = /^(\d*)([km]?)/.exec(cleaned)
278
+ return `${match?.[1] ?? ""}${match?.[2] ?? ""}`
279
+ }
280
+
281
+ /** 输入:任意值;输出:是否是合法上下文上限。 */
282
+ export function isContextLimit(value: unknown): value is ContextLimit {
283
+ if (value === "auto") return true
284
+ return (
285
+ typeof value === "number" &&
286
+ Number.isFinite(value) &&
287
+ value >= MIN_CONTEXT_LIMIT &&
288
+ value <= MAX_CONTEXT_LIMIT
289
+ )
290
+ }
291
+
292
+ /** 输入:任意值;输出:合法的上下文上限设置(支持 `200k` 这类字符串)。 */
293
+ function readContextLimit(value: unknown): ContextLimit {
294
+ if (isContextLimit(value)) return value
295
+ if (typeof value === "string") return parseTokenLimit(value) ?? DEFAULT_SETTINGS.contextLimit
296
+ return DEFAULT_SETTINGS.contextLimit
297
+ }
298
+
299
+ /**
300
+ * 输入:任意设置对象(可能来自旧版本、缺字段或类型不对)。
301
+ * 输出:补齐后的合法设置。
302
+ * 预期:用于启动时迁移 storage 里的旧数据,绝不抛错;旧的绝对值字段不再读取,统一回到百分比默认。
303
+ */
304
+ export function normalizeSettings(value: Readonly<Record<string, unknown>>): Settings {
305
+ const percent = value["triggerPercent"]
306
+ const blockPercent = readSummaryPercent(value["summaryBlockPercent"], DEFAULT_SUMMARY_BLOCK_PERCENT)
307
+ const totalPercent = readSummaryPercent(value["summaryTotalPercent"], DEFAULT_SUMMARY_TOTAL_PERCENT)
308
+ return {
309
+ enabled: typeof value["enabled"] === "boolean" ? value["enabled"] : DEFAULT_SETTINGS.enabled,
310
+ expanded: typeof value["expanded"] === "boolean" ? value["expanded"] : DEFAULT_SETTINGS.expanded,
311
+ locale: isLocaleSetting(value["locale"]) ? value["locale"] : DEFAULT_SETTINGS.locale,
312
+ contextLimit: readContextLimit(value["contextLimit"]),
313
+ triggerPercent:
314
+ typeof percent === "number" && Number.isFinite(percent) && percent >= MIN_TRIGGER_PERCENT && percent <= MAX_TRIGGER_PERCENT
315
+ ? Math.round(percent)
316
+ : DEFAULT_SETTINGS.triggerPercent,
317
+ nudgeGrowthTokens: readNudgeGrowthTokens(value["nudgeGrowthTokens"], DEFAULT_NUDGE_GROWTH_TOKENS),
318
+ summaryBlockPercent: blockPercent,
319
+ summaryTotalPercent: totalPercent,
320
+ summaryLanguage: isSummaryLanguage(value["summaryLanguage"]) ? value["summaryLanguage"] : "auto",
321
+ summaryFormat: isSummaryFormat(value["summaryFormat"]) ? value["summaryFormat"] : DEFAULT_SUMMARY_FORMAT,
322
+ autoCompress: typeof value["autoCompress"] === "boolean" ? value["autoCompress"] : DEFAULT_AUTO_COMPRESS,
323
+ enableCompressTool:
324
+ typeof value["enableCompressTool"] === "boolean" ? value["enableCompressTool"] : DEFAULT_ENABLE_COMPRESS_TOOL,
325
+ enableDecompressTool:
326
+ typeof value["enableDecompressTool"] === "boolean" ? value["enableDecompressTool"] : DEFAULT_ENABLE_DECOMPRESS_TOOL,
327
+ showTimeline: typeof value["showTimeline"] === "boolean" ? value["showTimeline"] : DEFAULT_SHOW_TIMELINE,
328
+ showBlockTokens: typeof value["showBlockTokens"] === "boolean" ? value["showBlockTokens"] : DEFAULT_SHOW_BLOCK_TOKENS,
329
+ cacheExpanded: typeof value["cacheExpanded"] === "boolean" ? value["cacheExpanded"] : DEFAULT_CACHE_EXPANDED,
330
+ showCacheChart: typeof value["showCacheChart"] === "boolean" ? value["showCacheChart"] : DEFAULT_SHOW_CACHE_CHART,
331
+ cacheChartExpanded:
332
+ typeof value["cacheChartExpanded"] === "boolean" ? value["cacheChartExpanded"] : DEFAULT_CACHE_CHART_EXPANDED,
333
+ showCacheLog: typeof value["showCacheLog"] === "boolean" ? value["showCacheLog"] : DEFAULT_SHOW_CACHE_LOG,
334
+ cacheLogExpanded:
335
+ typeof value["cacheLogExpanded"] === "boolean" ? value["cacheLogExpanded"] : DEFAULT_CACHE_LOG_EXPANDED,
336
+ showSummaries: typeof value["showSummaries"] === "boolean" ? value["showSummaries"] : DEFAULT_SHOW_SUMMARIES,
337
+ summariesExpanded:
338
+ typeof value["summariesExpanded"] === "boolean" ? value["summariesExpanded"] : DEFAULT_SUMMARIES_EXPANDED,
339
+ ...(typeof value["expandedBlockId"] === "string" && value["expandedBlockId"].length > 0
340
+ ? { expandedBlockId: value["expandedBlockId"] }
341
+ : {}),
342
+ decompressInlineMax: readInt(
343
+ value["decompressInlineMax"],
344
+ MIN_DECOMPRESS_INLINE_MAX,
345
+ MAX_DECOMPRESS_INLINE_MAX,
346
+ DEFAULT_DECOMPRESS_INLINE_MAX,
347
+ ),
348
+ decompressTempCap: readInt(
349
+ value["decompressTempCap"],
350
+ MIN_DECOMPRESS_TEMP_CAP,
351
+ MAX_DECOMPRESS_TEMP_CAP,
352
+ DEFAULT_DECOMPRESS_TEMP_CAP,
353
+ ),
354
+ decompressShowTokens:
355
+ typeof value["decompressShowTokens"] === "boolean" ? value["decompressShowTokens"] : DEFAULT_DECOMPRESS_SHOW_TOKENS,
356
+ }
357
+ }
358
+
359
+ /**
360
+ * 输入:插件 options 的原始值(可能缺失或类型不对)。
361
+ * 输出:合法的初始设置。
362
+ * 预期:非法值回退默认值,绝不抛错。
363
+ */
364
+ export function initialSettings(options: Readonly<Record<string, unknown>>): Settings {
365
+ return normalizeSettings(options)
366
+ }
367
+
368
+ /** 输入:设置对象(可能是热重载前的旧对象,字段缺失)。输出:合法的单条摘要百分比。 */
369
+ export function effectiveSummaryBlockPercent(settings: Settings): number {
370
+ return readSummaryPercent(settings.summaryBlockPercent, DEFAULT_SUMMARY_BLOCK_PERCENT)
371
+ }
372
+
373
+ /** 输入:设置对象。输出:合法的全部摘要百分比。 */
374
+ export function effectiveSummaryTotalPercent(settings: Settings): number {
375
+ return readSummaryPercent(settings.summaryTotalPercent, DEFAULT_SUMMARY_TOTAL_PERCENT)
376
+ }
377
+
378
+ /** 输入:设置;输出:合法的摘要语言。 */
379
+ export function effectiveSummaryLanguage(settings: Settings): SummaryLanguage {
380
+ return isSummaryLanguage(settings.summaryLanguage) ? settings.summaryLanguage : "auto"
381
+ }
382
+
383
+ /** 输入:设置;输出:合法的 decompress 内联上限。 */
384
+ export function effectiveDecompressInlineMax(settings: { readonly decompressInlineMax?: unknown }): number {
385
+ return readInt(
386
+ settings.decompressInlineMax,
387
+ MIN_DECOMPRESS_INLINE_MAX,
388
+ MAX_DECOMPRESS_INLINE_MAX,
389
+ DEFAULT_DECOMPRESS_INLINE_MAX,
390
+ )
391
+ }
392
+
393
+ /** 输入:设置;输出:合法的 decompress 临时文件上限。 */
394
+ export function effectiveDecompressTempCap(settings: { readonly decompressTempCap?: unknown }): number {
395
+ return readInt(
396
+ settings.decompressTempCap,
397
+ MIN_DECOMPRESS_TEMP_CAP,
398
+ MAX_DECOMPRESS_TEMP_CAP,
399
+ DEFAULT_DECOMPRESS_TEMP_CAP,
400
+ )
401
+ }
402
+
403
+ /** 输入:TUI 设置;输出:发给服务端的压缩设置子集;预期:字段列表只在这里维护。 */
404
+ export function serverSettingsPayload(settings: Settings): CompressionSettings {
405
+ return {
406
+ contextLimit: settings.contextLimit,
407
+ triggerPercent: settings.triggerPercent,
408
+ nudgeGrowthTokens: settings.nudgeGrowthTokens,
409
+ summaryBlockPercent: effectiveSummaryBlockPercent(settings),
410
+ summaryTotalPercent: effectiveSummaryTotalPercent(settings),
411
+ summaryLanguage: effectiveSummaryLanguage(settings),
412
+ summaryFormat: settings.summaryFormat,
413
+ autoCompress: settings.autoCompress,
414
+ enableCompressTool: settings.enableCompressTool,
415
+ enableDecompressTool: settings.enableDecompressTool,
416
+ showTimeline: settings.showTimeline,
417
+ showBlockTokens: settings.showBlockTokens,
418
+ decompressInlineMax: effectiveDecompressInlineMax(settings),
419
+ decompressTempCap: effectiveDecompressTempCap(settings),
420
+ decompressShowTokens: settings.decompressShowTokens,
421
+ }
422
+ }
423
+
424
+ /**
425
+ * 输入:设置(结构同 Settings,允许缺字段)+ 已知窗口 token 数。
426
+ * 输出:折算后的字符上限与合并预算。
427
+ * 预期:窗口未知时退回默认绝对值;窗口已知时按百分比折算,并扣除 system/tools/recent 预留。
428
+ */
429
+ export function resolveSummaryLimits(
430
+ settings: {
431
+ readonly summaryBlockPercent?: unknown
432
+ readonly summaryTotalPercent?: unknown
433
+ },
434
+ windowTokens: number | undefined,
435
+ ): { blockChars: number; totalChars: number; budgetTokens: number } {
436
+ const blockPercent = effectiveSummaryBlockPercent(settings as Settings)
437
+ const totalPercent = effectiveSummaryTotalPercent(settings as Settings)
438
+ if (windowTokens === undefined || !Number.isFinite(windowTokens) || windowTokens <= 0) {
439
+ return {
440
+ blockChars: DEFAULT_SUMMARY_BLOCK_CHARS,
441
+ totalChars: DEFAULT_SUMMARY_TOTAL_CHARS,
442
+ budgetTokens: DEFAULT_SUMMARY_TOTAL_CHARS,
443
+ }
444
+ }
445
+ const physical = Math.max(0, Math.floor(windowTokens) - SUMMARY_WINDOW_RESERVE)
446
+ const blockChars = Math.min(physical, Math.max(1, Math.round((windowTokens * blockPercent) / 100)))
447
+ const totalChars = Math.min(physical, Math.max(1, Math.round((windowTokens * totalPercent) / 100)))
448
+ return { blockChars, totalChars, budgetTokens: totalChars }
449
+ }
450
+
451
+ /**
452
+ * 输入:语言设置。
453
+ * 输出:传给 resolveMessages 的 locale。
454
+ * 预期:auto 返回 undefined(交给系统检测),其余原样返回。
455
+ */
456
+ export function localeArgument(locale: LocaleSetting): string | undefined {
457
+ return locale === "auto" ? undefined : locale
458
+ }
459
+
460
+ /**
461
+ * 输入:设置 + 模型原生上下文长度。
462
+ * 输出:实际使用的上下文上限;未知时返回 undefined。
463
+ * 预期:数字设置是上限,不能超过模型原生窗口;auto 直接用原生值。
464
+ */
465
+ export function effectiveLimit(settings: Settings, native: number): number | undefined {
466
+ const value: unknown = settings.contextLimit
467
+ if (isContextLimit(value) && value !== "auto") {
468
+ return native > 0 ? Math.min(value, native) : value
469
+ }
470
+ return native > 0 ? native : undefined
471
+ }