compressed-context 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +62 -4
- package/src/compression/active-summary-cache.ts +50 -0
- package/src/compression/adopt.ts +57 -0
- package/src/compression/auto.ts +372 -0
- package/src/compression/babel.ts +85 -0
- package/src/compression/block.ts +25 -0
- package/src/compression/cache-history.ts +75 -0
- package/src/compression/compaction.ts +189 -0
- package/src/compression/decompress-tool.ts +74 -0
- package/src/compression/decompress.ts +292 -0
- package/src/compression/deleted-sessions.ts +24 -0
- package/src/compression/distill.ts +220 -0
- package/src/compression/events.ts +275 -0
- package/src/compression/markdown.ts +53 -0
- package/src/compression/normalize.ts +114 -0
- package/src/compression/render.ts +70 -0
- package/src/compression/schema.ts +84 -0
- package/src/compression/select.ts +123 -0
- package/src/compression/session-message.ts +112 -0
- package/src/compression/settings-rpc.ts +111 -0
- package/src/compression/settings.ts +175 -0
- package/src/compression/state.ts +248 -0
- package/src/compression/summarize.ts +595 -0
- package/src/compression/summary.ts +73 -0
- package/src/compression/tool.ts +142 -0
- package/src/compression/transcript-store.ts +258 -0
- package/src/compression/transcript.ts +63 -0
- package/src/compression/validate.ts +81 -0
- package/src/i18n/en.ts +93 -0
- package/src/i18n/index.ts +28 -0
- package/src/i18n/types.ts +89 -0
- package/src/i18n/zh.ts +93 -0
- package/src/index.ts +108 -0
- package/src/shared/blocks.ts +17 -0
- package/src/shared/cache.ts +199 -0
- package/src/shared/format.ts +23 -0
- package/src/shared/media.ts +184 -0
- package/src/shared/parse.ts +25 -0
- package/src/shared/parts.ts +72 -0
- package/src/shared/rpc.ts +145 -0
- package/src/shared/scan.ts +32 -0
- package/src/shared/settings.ts +471 -0
- package/src/shared/stats.ts +142 -0
- package/src/shared/tokens.ts +259 -0
- package/src/tui/chart/cache-chart.ts +237 -0
- package/src/tui/chart/color.ts +58 -0
- package/src/tui/index.tsx +111 -0
- package/src/tui/panel/cache.tsx +203 -0
- package/src/tui/panel/data.ts +105 -0
- package/src/tui/panel/index.tsx +176 -0
- package/src/tui/panel/summaries.tsx +192 -0
- package/src/tui/settings/dialogs.tsx +215 -0
- package/src/tui/settings/options.ts +238 -0
- package/src/tui/settings.tsx +418 -0
- package/src/tui/types.ts +4 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文统计的跨进程载体。
|
|
3
|
+
*
|
|
4
|
+
* 服务端插件把估算写进 session metadata,TUI 插件读出来按真实 usage 缩放。
|
|
5
|
+
* 本模块只放纯函数:构造、校验、节流判断、metadata 合并、缩放。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { totalCategories, type TokenCategories } from "./tokens.ts"
|
|
9
|
+
|
|
10
|
+
/** metadata 里存放统计的键名,避免和其他插件撞车。 */
|
|
11
|
+
export const STATS_KEY = "compressed-context"
|
|
12
|
+
|
|
13
|
+
/** metadata 里存放终止原因的键名;TUI 面板据此显示警告。 */
|
|
14
|
+
export const TERMINAL_KEY = "compressed-context-terminal"
|
|
15
|
+
|
|
16
|
+
export const CATEGORY_KEYS = ["system", "tools", "tool", "text", "recent", "summaries", "media"] as const
|
|
17
|
+
|
|
18
|
+
export type CategoryKey = (typeof CATEGORY_KEYS)[number]
|
|
19
|
+
|
|
20
|
+
export interface ContextStats extends TokenCategories {
|
|
21
|
+
readonly estimatedTotal: number
|
|
22
|
+
readonly at: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 两次写入之间至少要有这么多 token 的变化,避免 durable 事件被刷屏。 */
|
|
26
|
+
export const PUBLISH_MIN_DELTA = 2_000
|
|
27
|
+
|
|
28
|
+
/** 输入:分类估算 + 时间戳;输出:带合计的统计对象。 */
|
|
29
|
+
export function createStats(categories: TokenCategories, now: number): ContextStats {
|
|
30
|
+
return { ...categories, estimatedTotal: totalCategories(categories), at: now }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 输入:任意 metadata 值;输出:合法数字或 undefined。 */
|
|
34
|
+
function readNumber(value: unknown): number | undefined {
|
|
35
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 输入:metadata 里的原始 JSON。
|
|
40
|
+
* 输出:校验通过的统计对象;缺字段、类型不对或负数返回 undefined。
|
|
41
|
+
* 预期:metadata 可能被别的版本或别的插件改写,解析必须防御。
|
|
42
|
+
*/
|
|
43
|
+
export function parseStats(value: unknown): ContextStats | undefined {
|
|
44
|
+
if (typeof value !== "object" || value === null) return undefined
|
|
45
|
+
const record = value as Record<string, unknown>
|
|
46
|
+
const system = readNumber(record["system"])
|
|
47
|
+
const tools = readNumber(record["tools"])
|
|
48
|
+
const tool = readNumber(record["tool"])
|
|
49
|
+
const text = readNumber(record["text"])
|
|
50
|
+
// 旧版本没有 recent 字段;缺省按 0 处理,避免统计整块失效。
|
|
51
|
+
const recent = record["recent"] === undefined ? 0 : readNumber(record["recent"])
|
|
52
|
+
const summaries = readNumber(record["summaries"])
|
|
53
|
+
const media = readNumber(record["media"])
|
|
54
|
+
const estimatedTotal = readNumber(record["estimatedTotal"])
|
|
55
|
+
const at = readNumber(record["at"])
|
|
56
|
+
if (
|
|
57
|
+
system === undefined ||
|
|
58
|
+
tools === undefined ||
|
|
59
|
+
tool === undefined ||
|
|
60
|
+
text === undefined ||
|
|
61
|
+
recent === undefined ||
|
|
62
|
+
summaries === undefined ||
|
|
63
|
+
media === undefined ||
|
|
64
|
+
estimatedTotal === undefined ||
|
|
65
|
+
at === undefined
|
|
66
|
+
) {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
return { system, tools, tool, text, recent, summaries, media, estimatedTotal, at }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 输入:上一次发布的统计(可能没有)+ 本次统计。
|
|
74
|
+
* 输出:是否值得写一次 metadata。
|
|
75
|
+
* 预期:合计或任一分类变化达到阈值才写,减少 durable 事件。
|
|
76
|
+
*/
|
|
77
|
+
export function shouldPublish(previous: ContextStats | undefined, next: ContextStats): boolean {
|
|
78
|
+
if (previous === undefined) return true
|
|
79
|
+
if (Math.abs(next.estimatedTotal - previous.estimatedTotal) >= PUBLISH_MIN_DELTA) return true
|
|
80
|
+
return CATEGORY_KEYS.some((key) => Math.abs(next[key] - previous[key]) >= PUBLISH_MIN_DELTA)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 输入:已有 metadata + 本次统计。
|
|
87
|
+
* 输出:保留其他键、替换 STATS_KEY 的新对象。
|
|
88
|
+
* 预期:metadata 是整体替换语义,必须先合并再写。
|
|
89
|
+
*/
|
|
90
|
+
export function mergeMetadata(
|
|
91
|
+
metadata: Readonly<Record<string, JsonValue>> | undefined,
|
|
92
|
+
stats: ContextStats,
|
|
93
|
+
): Record<string, JsonValue> {
|
|
94
|
+
return {
|
|
95
|
+
...(metadata ?? {}),
|
|
96
|
+
[STATS_KEY]: {
|
|
97
|
+
system: stats.system,
|
|
98
|
+
tools: stats.tools,
|
|
99
|
+
tool: stats.tool,
|
|
100
|
+
text: stats.text,
|
|
101
|
+
recent: stats.recent,
|
|
102
|
+
summaries: stats.summaries,
|
|
103
|
+
media: stats.media,
|
|
104
|
+
estimatedTotal: stats.estimatedTotal,
|
|
105
|
+
at: stats.at,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 输入:已有 metadata + 终止原因。
|
|
112
|
+
* 输出:保留其他键、写入终止原因的新对象。
|
|
113
|
+
* 预期:TUI 面板读 TERMINAL_KEY 显示警告;写入失败不影响压缩状态。
|
|
114
|
+
*/
|
|
115
|
+
export function mergeTerminalMetadata(
|
|
116
|
+
metadata: Readonly<Record<string, JsonValue>> | undefined,
|
|
117
|
+
message: string,
|
|
118
|
+
): Record<string, JsonValue> {
|
|
119
|
+
return { ...(metadata ?? {}), [TERMINAL_KEY]: message }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ScaledCategory {
|
|
123
|
+
readonly key: CategoryKey
|
|
124
|
+
readonly value: number
|
|
125
|
+
readonly pct: number
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 输入:估算统计 + 真实总量(0 表示上游没给)。
|
|
130
|
+
* 输出:按真实总量缩放后的各类数值与百分比。
|
|
131
|
+
* 预期:真实总量缺失时按原值展示;百分比分母是缩放后的合计。
|
|
132
|
+
*/
|
|
133
|
+
export function scaleCategories(stats: ContextStats, realTotal: number): readonly ScaledCategory[] {
|
|
134
|
+
const scale = realTotal > 0 && stats.estimatedTotal > 0 ? realTotal / stats.estimatedTotal : 1
|
|
135
|
+
const scaled = CATEGORY_KEYS.map((key) => ({ key, value: Math.round(stats[key] * scale) }))
|
|
136
|
+
const total = scaled.reduce((sum, item) => sum + item.value, 0)
|
|
137
|
+
return scaled.map((item) => ({
|
|
138
|
+
key: item.key,
|
|
139
|
+
value: item.value,
|
|
140
|
+
pct: total > 0 ? Math.round((item.value / total) * 100) : 0,
|
|
141
|
+
}))
|
|
142
|
+
}
|