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.
- package/package.json +64 -64
- package/src/compression/active-summary-cache.ts +50 -50
- package/src/compression/adopt.ts +57 -57
- package/src/compression/auto.ts +372 -372
- package/src/compression/babel.ts +85 -85
- package/src/compression/block.ts +25 -25
- package/src/compression/cache-history.ts +75 -75
- package/src/compression/compaction.ts +189 -189
- package/src/compression/decompress-tool.ts +74 -74
- package/src/compression/decompress.ts +292 -292
- package/src/compression/deleted-sessions.ts +24 -24
- package/src/compression/distill.ts +220 -220
- package/src/compression/events.ts +275 -275
- package/src/compression/markdown.ts +53 -53
- package/src/compression/normalize.ts +114 -114
- package/src/compression/render.ts +70 -70
- package/src/compression/schema.ts +84 -84
- package/src/compression/select.ts +123 -123
- package/src/compression/session-message.ts +112 -112
- package/src/compression/settings-rpc.ts +111 -111
- package/src/compression/settings.ts +175 -175
- package/src/compression/state.ts +248 -248
- package/src/compression/summarize.ts +595 -595
- package/src/compression/summary.ts +73 -73
- package/src/compression/tool.ts +142 -142
- package/src/compression/transcript-store.ts +258 -258
- package/src/compression/transcript.ts +63 -63
- package/src/compression/validate.ts +81 -81
- package/src/i18n/en.ts +93 -93
- package/src/i18n/index.ts +28 -28
- package/src/i18n/types.ts +89 -89
- package/src/i18n/zh.ts +93 -93
- package/src/index.ts +108 -108
- package/src/shared/blocks.ts +17 -17
- package/src/shared/cache.ts +199 -199
- package/src/shared/format.ts +23 -23
- package/src/shared/media.ts +184 -184
- package/src/shared/parse.ts +25 -25
- package/src/shared/parts.ts +72 -72
- package/src/shared/rpc.ts +145 -145
- package/src/shared/scan.ts +32 -32
- package/src/shared/settings.ts +471 -471
- package/src/shared/stats.ts +142 -142
- package/src/shared/tokens.ts +259 -259
- package/src/tui/chart/cache-chart.ts +237 -237
- package/src/tui/chart/color.ts +58 -58
- package/src/tui/index.tsx +111 -111
- package/src/tui/panel/cache.tsx +203 -203
- package/src/tui/panel/data.ts +105 -105
- package/src/tui/panel/index.tsx +176 -176
- package/src/tui/panel/summaries.tsx +192 -192
- package/src/tui/settings/dialogs.tsx +215 -215
- package/src/tui/settings/options.ts +238 -238
- package/src/tui/settings.tsx +418 -418
- package/src/tui/types.ts +4 -4
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode/plugin"
|
|
2
|
-
|
|
3
|
-
import type { SummaryBlock } from "../shared/blocks.ts"
|
|
4
|
-
import { ContextRpc } from "../shared/rpc.ts"
|
|
5
|
-
import { loadCacheHistory, recordCacheHistory } from "./cache-history.ts"
|
|
6
|
-
import { activeBlocks, renderSummary } from "./render.ts"
|
|
7
|
-
import { isBabelSummary, isMarkdownSummary } from "./schema.ts"
|
|
8
|
-
import { loadState } from "./state.ts"
|
|
9
|
-
import {
|
|
10
|
-
RESOLVED_WINDOW_KEY,
|
|
11
|
-
loadServerSettings,
|
|
12
|
-
parseServerSettings,
|
|
13
|
-
saveServerSettings,
|
|
14
|
-
windowKey,
|
|
15
|
-
} from "./settings.ts"
|
|
16
|
-
|
|
17
|
-
/** 注册句柄的最小结构。 */
|
|
18
|
-
export interface Disposable {
|
|
19
|
-
dispose(): Promise<void>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 输入:服务端插件上下文。
|
|
24
|
-
* 输出:注册 settingsGet / settingsSet / blocksGet / cacheHistoryGet,并返回注销句柄。
|
|
25
|
-
* 预期:TUI 推来的设置存进 ctx.storage;非法值返回 ok:false;读取时带上最近解析到的原生窗口;
|
|
26
|
-
* blocksGet 只返回 active 链,cacheHistoryGet 读取前先补写当前上下文。
|
|
27
|
-
*/
|
|
28
|
-
export async function registerSettingsRpc(ctx: Plugin.Context): Promise<Disposable> {
|
|
29
|
-
return ctx.rpc.register(ContextRpc, {
|
|
30
|
-
settingsGet: async (input) => {
|
|
31
|
-
const settings = await loadServerSettings(ctx.storage)
|
|
32
|
-
const requested = (input as { readonly sessionID?: unknown }).sessionID
|
|
33
|
-
const sessionID = typeof requested === "string" && requested.length > 0 ? requested : undefined
|
|
34
|
-
// 优先返回当前会话模型自己的窗口;拿不到再退回最近一次解析值。
|
|
35
|
-
let resolvedWindow: number | undefined
|
|
36
|
-
if (sessionID !== undefined) {
|
|
37
|
-
try {
|
|
38
|
-
const session = await ctx.session.get({ sessionID })
|
|
39
|
-
const model = session.model
|
|
40
|
-
if (model !== undefined) {
|
|
41
|
-
const stored = await ctx.storage.get(windowKey(model.providerID, model.id))
|
|
42
|
-
if (typeof stored === "number" && stored > 0) resolvedWindow = stored
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// 会话不存在或模型未知:回退到最近一次解析值
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (resolvedWindow === undefined) {
|
|
49
|
-
const stored = await ctx.storage.get(RESOLVED_WINDOW_KEY)
|
|
50
|
-
if (typeof stored === "number" && stored > 0) resolvedWindow = stored
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
contextLimit: settings.contextLimit,
|
|
54
|
-
triggerPercent: settings.triggerPercent,
|
|
55
|
-
nudgeGrowthTokens: settings.nudgeGrowthTokens,
|
|
56
|
-
summaryBlockPercent: settings.summaryBlockPercent,
|
|
57
|
-
summaryTotalPercent: settings.summaryTotalPercent,
|
|
58
|
-
summaryLanguage: settings.summaryLanguage,
|
|
59
|
-
summaryFormat: settings.summaryFormat,
|
|
60
|
-
autoCompress: settings.autoCompress,
|
|
61
|
-
enableCompressTool: settings.enableCompressTool,
|
|
62
|
-
enableDecompressTool: settings.enableDecompressTool,
|
|
63
|
-
showTimeline: settings.showTimeline,
|
|
64
|
-
showBlockTokens: settings.showBlockTokens,
|
|
65
|
-
decompressInlineMax: settings.decompressInlineMax,
|
|
66
|
-
decompressTempCap: settings.decompressTempCap,
|
|
67
|
-
decompressShowTokens: settings.decompressShowTokens,
|
|
68
|
-
...(resolvedWindow === undefined ? {} : { resolvedWindow }),
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
settingsSet: async (input) => {
|
|
72
|
-
const current = await loadServerSettings(ctx.storage)
|
|
73
|
-
const parsed = parseServerSettings({ ...current, ...(input as Record<string, unknown>) })
|
|
74
|
-
if (!parsed) return { ok: false }
|
|
75
|
-
await saveServerSettings(ctx.storage, parsed)
|
|
76
|
-
return { ok: true }
|
|
77
|
-
},
|
|
78
|
-
blocksGet: async (input) => {
|
|
79
|
-
const sessionID = (input as { readonly sessionID: string }).sessionID
|
|
80
|
-
const state = await loadState(ctx.storage, sessionID)
|
|
81
|
-
// 只返回模型实际看到的 active 链;被合并掉的源块不展示。
|
|
82
|
-
const blocks: SummaryBlock[] = activeBlocks(state.blocks, state.pending).map((block) => ({
|
|
83
|
-
id: block.id,
|
|
84
|
-
tier: block.tier,
|
|
85
|
-
format: isBabelSummary(block.summary)
|
|
86
|
-
? "babel"
|
|
87
|
-
: isMarkdownSummary(block.summary)
|
|
88
|
-
? "markdown"
|
|
89
|
-
: "json",
|
|
90
|
-
title: block.summary.title,
|
|
91
|
-
at: block.at,
|
|
92
|
-
tokensBefore: block.tokensBefore,
|
|
93
|
-
tokensAfter: block.tokensAfter,
|
|
94
|
-
...(block.sourceBlockIds === undefined ? {} : { sourceBlockIds: [...block.sourceBlockIds] }),
|
|
95
|
-
body: renderSummary(block.summary),
|
|
96
|
-
}))
|
|
97
|
-
return { blocks }
|
|
98
|
-
},
|
|
99
|
-
cacheHistoryGet: async (input) => {
|
|
100
|
-
const sessionID = (input as { readonly sessionID: string }).sessionID
|
|
101
|
-
const history = await loadCacheHistory(ctx.storage, sessionID)
|
|
102
|
-
// 只在历史为空时补写一次;已有历史时只读,避免每次 TUI 刷新都拉全文。
|
|
103
|
-
if (history.samples.length === 0) {
|
|
104
|
-
await recordCacheHistory(ctx, sessionID)
|
|
105
|
-
const bootstrapped = await loadCacheHistory(ctx.storage, sessionID)
|
|
106
|
-
return { samples: bootstrapped.samples }
|
|
107
|
-
}
|
|
108
|
-
return { samples: history.samples }
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
}
|
|
1
|
+
import type { Plugin } from "@opencode/plugin"
|
|
2
|
+
|
|
3
|
+
import type { SummaryBlock } from "../shared/blocks.ts"
|
|
4
|
+
import { ContextRpc } from "../shared/rpc.ts"
|
|
5
|
+
import { loadCacheHistory, recordCacheHistory } from "./cache-history.ts"
|
|
6
|
+
import { activeBlocks, renderSummary } from "./render.ts"
|
|
7
|
+
import { isBabelSummary, isMarkdownSummary } from "./schema.ts"
|
|
8
|
+
import { loadState } from "./state.ts"
|
|
9
|
+
import {
|
|
10
|
+
RESOLVED_WINDOW_KEY,
|
|
11
|
+
loadServerSettings,
|
|
12
|
+
parseServerSettings,
|
|
13
|
+
saveServerSettings,
|
|
14
|
+
windowKey,
|
|
15
|
+
} from "./settings.ts"
|
|
16
|
+
|
|
17
|
+
/** 注册句柄的最小结构。 */
|
|
18
|
+
export interface Disposable {
|
|
19
|
+
dispose(): Promise<void>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 输入:服务端插件上下文。
|
|
24
|
+
* 输出:注册 settingsGet / settingsSet / blocksGet / cacheHistoryGet,并返回注销句柄。
|
|
25
|
+
* 预期:TUI 推来的设置存进 ctx.storage;非法值返回 ok:false;读取时带上最近解析到的原生窗口;
|
|
26
|
+
* blocksGet 只返回 active 链,cacheHistoryGet 读取前先补写当前上下文。
|
|
27
|
+
*/
|
|
28
|
+
export async function registerSettingsRpc(ctx: Plugin.Context): Promise<Disposable> {
|
|
29
|
+
return ctx.rpc.register(ContextRpc, {
|
|
30
|
+
settingsGet: async (input) => {
|
|
31
|
+
const settings = await loadServerSettings(ctx.storage)
|
|
32
|
+
const requested = (input as { readonly sessionID?: unknown }).sessionID
|
|
33
|
+
const sessionID = typeof requested === "string" && requested.length > 0 ? requested : undefined
|
|
34
|
+
// 优先返回当前会话模型自己的窗口;拿不到再退回最近一次解析值。
|
|
35
|
+
let resolvedWindow: number | undefined
|
|
36
|
+
if (sessionID !== undefined) {
|
|
37
|
+
try {
|
|
38
|
+
const session = await ctx.session.get({ sessionID })
|
|
39
|
+
const model = session.model
|
|
40
|
+
if (model !== undefined) {
|
|
41
|
+
const stored = await ctx.storage.get(windowKey(model.providerID, model.id))
|
|
42
|
+
if (typeof stored === "number" && stored > 0) resolvedWindow = stored
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// 会话不存在或模型未知:回退到最近一次解析值
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (resolvedWindow === undefined) {
|
|
49
|
+
const stored = await ctx.storage.get(RESOLVED_WINDOW_KEY)
|
|
50
|
+
if (typeof stored === "number" && stored > 0) resolvedWindow = stored
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
contextLimit: settings.contextLimit,
|
|
54
|
+
triggerPercent: settings.triggerPercent,
|
|
55
|
+
nudgeGrowthTokens: settings.nudgeGrowthTokens,
|
|
56
|
+
summaryBlockPercent: settings.summaryBlockPercent,
|
|
57
|
+
summaryTotalPercent: settings.summaryTotalPercent,
|
|
58
|
+
summaryLanguage: settings.summaryLanguage,
|
|
59
|
+
summaryFormat: settings.summaryFormat,
|
|
60
|
+
autoCompress: settings.autoCompress,
|
|
61
|
+
enableCompressTool: settings.enableCompressTool,
|
|
62
|
+
enableDecompressTool: settings.enableDecompressTool,
|
|
63
|
+
showTimeline: settings.showTimeline,
|
|
64
|
+
showBlockTokens: settings.showBlockTokens,
|
|
65
|
+
decompressInlineMax: settings.decompressInlineMax,
|
|
66
|
+
decompressTempCap: settings.decompressTempCap,
|
|
67
|
+
decompressShowTokens: settings.decompressShowTokens,
|
|
68
|
+
...(resolvedWindow === undefined ? {} : { resolvedWindow }),
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
settingsSet: async (input) => {
|
|
72
|
+
const current = await loadServerSettings(ctx.storage)
|
|
73
|
+
const parsed = parseServerSettings({ ...current, ...(input as Record<string, unknown>) })
|
|
74
|
+
if (!parsed) return { ok: false }
|
|
75
|
+
await saveServerSettings(ctx.storage, parsed)
|
|
76
|
+
return { ok: true }
|
|
77
|
+
},
|
|
78
|
+
blocksGet: async (input) => {
|
|
79
|
+
const sessionID = (input as { readonly sessionID: string }).sessionID
|
|
80
|
+
const state = await loadState(ctx.storage, sessionID)
|
|
81
|
+
// 只返回模型实际看到的 active 链;被合并掉的源块不展示。
|
|
82
|
+
const blocks: SummaryBlock[] = activeBlocks(state.blocks, state.pending).map((block) => ({
|
|
83
|
+
id: block.id,
|
|
84
|
+
tier: block.tier,
|
|
85
|
+
format: isBabelSummary(block.summary)
|
|
86
|
+
? "babel"
|
|
87
|
+
: isMarkdownSummary(block.summary)
|
|
88
|
+
? "markdown"
|
|
89
|
+
: "json",
|
|
90
|
+
title: block.summary.title,
|
|
91
|
+
at: block.at,
|
|
92
|
+
tokensBefore: block.tokensBefore,
|
|
93
|
+
tokensAfter: block.tokensAfter,
|
|
94
|
+
...(block.sourceBlockIds === undefined ? {} : { sourceBlockIds: [...block.sourceBlockIds] }),
|
|
95
|
+
body: renderSummary(block.summary),
|
|
96
|
+
}))
|
|
97
|
+
return { blocks }
|
|
98
|
+
},
|
|
99
|
+
cacheHistoryGet: async (input) => {
|
|
100
|
+
const sessionID = (input as { readonly sessionID: string }).sessionID
|
|
101
|
+
const history = await loadCacheHistory(ctx.storage, sessionID)
|
|
102
|
+
// 只在历史为空时补写一次;已有历史时只读,避免每次 TUI 刷新都拉全文。
|
|
103
|
+
if (history.samples.length === 0) {
|
|
104
|
+
await recordCacheHistory(ctx, sessionID)
|
|
105
|
+
const bootstrapped = await loadCacheHistory(ctx.storage, sessionID)
|
|
106
|
+
return { samples: bootstrapped.samples }
|
|
107
|
+
}
|
|
108
|
+
return { samples: history.samples }
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
}
|
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
import type { Schema } from "effect"
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_AUTO_COMPRESS,
|
|
5
|
-
DEFAULT_DECOMPRESS_INLINE_MAX,
|
|
6
|
-
DEFAULT_DECOMPRESS_SHOW_TOKENS,
|
|
7
|
-
DEFAULT_DECOMPRESS_TEMP_CAP,
|
|
8
|
-
DEFAULT_ENABLE_COMPRESS_TOOL,
|
|
9
|
-
DEFAULT_ENABLE_DECOMPRESS_TOOL,
|
|
10
|
-
DEFAULT_NUDGE_GROWTH_TOKENS,
|
|
11
|
-
DEFAULT_SHOW_BLOCK_TOKENS,
|
|
12
|
-
DEFAULT_SHOW_TIMELINE,
|
|
13
|
-
DEFAULT_SUMMARY_BLOCK_PERCENT,
|
|
14
|
-
DEFAULT_SUMMARY_FORMAT,
|
|
15
|
-
DEFAULT_SUMMARY_TOTAL_PERCENT,
|
|
16
|
-
MAX_DECOMPRESS_INLINE_MAX,
|
|
17
|
-
MAX_DECOMPRESS_TEMP_CAP,
|
|
18
|
-
MAX_CONTEXT_LIMIT,
|
|
19
|
-
MAX_SUMMARY_PERCENT,
|
|
20
|
-
MAX_TRIGGER_PERCENT,
|
|
21
|
-
MIN_CONTEXT_LIMIT,
|
|
22
|
-
MIN_DECOMPRESS_INLINE_MAX,
|
|
23
|
-
MIN_DECOMPRESS_TEMP_CAP,
|
|
24
|
-
MIN_SUMMARY_PERCENT,
|
|
25
|
-
MIN_TRIGGER_PERCENT,
|
|
26
|
-
isSummaryLanguage,
|
|
27
|
-
isSummaryFormat,
|
|
28
|
-
type CompressionSettings,
|
|
29
|
-
} from "../shared/settings.ts"
|
|
30
|
-
import { readOptionalInt, readStrictBoolean } from "../shared/parse.ts"
|
|
31
|
-
import type { StorageLike } from "./state.ts"
|
|
32
|
-
|
|
33
|
-
/** 服务端从 TUI 收到的设置;字段与 shared 的 CompressionSettings 一致。 */
|
|
34
|
-
export type ServerSettings = CompressionSettings
|
|
35
|
-
|
|
36
|
-
export const SETTINGS_KEY = "settings"
|
|
37
|
-
/** 插件最近一次从模型解析到的原生窗口;TUI 用它把绝对值换算成百分比。 */
|
|
38
|
-
export const RESOLVED_WINDOW_KEY = "window:last"
|
|
39
|
-
|
|
40
|
-
/** 最近一次解析到的「生效窗口」= min(settings.contextLimit, 原生窗口);自动压缩用它折算阈值。 */
|
|
41
|
-
export const EFFECTIVE_WINDOW_KEY = "window:effective"
|
|
42
|
-
|
|
43
|
-
/** 输入:模型标识;输出:该模型原生窗口在 storage 里的 key。 */
|
|
44
|
-
export function windowKey(providerID: string, modelID: string): string {
|
|
45
|
-
return `window:${providerID}/${modelID}`
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const DEFAULT_SERVER_SETTINGS: ServerSettings = {
|
|
49
|
-
contextLimit: "auto",
|
|
50
|
-
triggerPercent: 80,
|
|
51
|
-
nudgeGrowthTokens: DEFAULT_NUDGE_GROWTH_TOKENS,
|
|
52
|
-
summaryBlockPercent: DEFAULT_SUMMARY_BLOCK_PERCENT,
|
|
53
|
-
summaryTotalPercent: DEFAULT_SUMMARY_TOTAL_PERCENT,
|
|
54
|
-
summaryLanguage: "auto",
|
|
55
|
-
summaryFormat: DEFAULT_SUMMARY_FORMAT,
|
|
56
|
-
autoCompress: DEFAULT_AUTO_COMPRESS,
|
|
57
|
-
enableCompressTool: DEFAULT_ENABLE_COMPRESS_TOOL,
|
|
58
|
-
enableDecompressTool: DEFAULT_ENABLE_DECOMPRESS_TOOL,
|
|
59
|
-
showTimeline: DEFAULT_SHOW_TIMELINE,
|
|
60
|
-
showBlockTokens: DEFAULT_SHOW_BLOCK_TOKENS,
|
|
61
|
-
decompressInlineMax: DEFAULT_DECOMPRESS_INLINE_MAX,
|
|
62
|
-
decompressTempCap: DEFAULT_DECOMPRESS_TEMP_CAP,
|
|
63
|
-
decompressShowTokens: DEFAULT_DECOMPRESS_SHOW_TOKENS,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 输入:storage 里的原始值。
|
|
68
|
-
* 输出:合法设置;非法返回 undefined。
|
|
69
|
-
* 预期:范围外的值一律拒绝,不静默修正。
|
|
70
|
-
*/
|
|
71
|
-
export function parseServerSettings(value: unknown): ServerSettings | undefined {
|
|
72
|
-
if (typeof value !== "object" || value === null) return undefined
|
|
73
|
-
const record = value as Record<string, unknown>
|
|
74
|
-
const limit = record["contextLimit"]
|
|
75
|
-
const percent = record["triggerPercent"]
|
|
76
|
-
const nudge = record["nudgeGrowthTokens"] ?? DEFAULT_NUDGE_GROWTH_TOKENS
|
|
77
|
-
const blockPercent = record["summaryBlockPercent"] ?? DEFAULT_SUMMARY_BLOCK_PERCENT
|
|
78
|
-
const totalPercent = record["summaryTotalPercent"] ?? DEFAULT_SUMMARY_TOTAL_PERCENT
|
|
79
|
-
const language = record["summaryLanguage"] ?? "auto"
|
|
80
|
-
const summaryFormat = record["summaryFormat"] ?? DEFAULT_SUMMARY_FORMAT
|
|
81
|
-
if (
|
|
82
|
-
limit !== "auto" &&
|
|
83
|
-
(typeof limit !== "number" ||
|
|
84
|
-
!Number.isFinite(limit) ||
|
|
85
|
-
limit < MIN_CONTEXT_LIMIT ||
|
|
86
|
-
limit > MAX_CONTEXT_LIMIT)
|
|
87
|
-
) {
|
|
88
|
-
return undefined
|
|
89
|
-
}
|
|
90
|
-
if (
|
|
91
|
-
typeof percent !== "number" ||
|
|
92
|
-
!Number.isFinite(percent) ||
|
|
93
|
-
percent < MIN_TRIGGER_PERCENT ||
|
|
94
|
-
percent > MAX_TRIGGER_PERCENT
|
|
95
|
-
) {
|
|
96
|
-
return undefined
|
|
97
|
-
}
|
|
98
|
-
if (
|
|
99
|
-
typeof nudge !== "number" ||
|
|
100
|
-
!Number.isFinite(nudge) ||
|
|
101
|
-
(nudge !== 0 && (nudge < 1_000 || nudge > 200_000))
|
|
102
|
-
) {
|
|
103
|
-
return undefined
|
|
104
|
-
}
|
|
105
|
-
if (
|
|
106
|
-
typeof blockPercent !== "number" ||
|
|
107
|
-
!Number.isFinite(blockPercent) ||
|
|
108
|
-
blockPercent < MIN_SUMMARY_PERCENT ||
|
|
109
|
-
blockPercent > MAX_SUMMARY_PERCENT
|
|
110
|
-
) {
|
|
111
|
-
return undefined
|
|
112
|
-
}
|
|
113
|
-
if (
|
|
114
|
-
typeof totalPercent !== "number" ||
|
|
115
|
-
!Number.isFinite(totalPercent) ||
|
|
116
|
-
totalPercent < MIN_SUMMARY_PERCENT ||
|
|
117
|
-
totalPercent > MAX_SUMMARY_PERCENT
|
|
118
|
-
) {
|
|
119
|
-
return undefined
|
|
120
|
-
}
|
|
121
|
-
if (!isSummaryLanguage(language)) return undefined
|
|
122
|
-
if (!isSummaryFormat(summaryFormat)) return undefined
|
|
123
|
-
const autoCompress = readStrictBoolean(record["autoCompress"], DEFAULT_AUTO_COMPRESS)
|
|
124
|
-
const enableCompressTool = readStrictBoolean(record["enableCompressTool"], DEFAULT_ENABLE_COMPRESS_TOOL)
|
|
125
|
-
const enableDecompressTool = readStrictBoolean(record["enableDecompressTool"], DEFAULT_ENABLE_DECOMPRESS_TOOL)
|
|
126
|
-
const showTimeline = readStrictBoolean(record["showTimeline"], DEFAULT_SHOW_TIMELINE)
|
|
127
|
-
const showBlockTokens = readStrictBoolean(record["showBlockTokens"], DEFAULT_SHOW_BLOCK_TOKENS)
|
|
128
|
-
const decompressShowTokens = readStrictBoolean(record["decompressShowTokens"], DEFAULT_DECOMPRESS_SHOW_TOKENS)
|
|
129
|
-
if (
|
|
130
|
-
autoCompress === undefined ||
|
|
131
|
-
enableCompressTool === undefined ||
|
|
132
|
-
enableDecompressTool === undefined ||
|
|
133
|
-
showTimeline === undefined ||
|
|
134
|
-
showBlockTokens === undefined ||
|
|
135
|
-
decompressShowTokens === undefined
|
|
136
|
-
) {
|
|
137
|
-
return undefined
|
|
138
|
-
}
|
|
139
|
-
const decompressInlineMax =
|
|
140
|
-
record["decompressInlineMax"] === undefined
|
|
141
|
-
? DEFAULT_DECOMPRESS_INLINE_MAX
|
|
142
|
-
: readOptionalInt(record["decompressInlineMax"], MIN_DECOMPRESS_INLINE_MAX, MAX_DECOMPRESS_INLINE_MAX)
|
|
143
|
-
const decompressTempCap =
|
|
144
|
-
record["decompressTempCap"] === undefined
|
|
145
|
-
? DEFAULT_DECOMPRESS_TEMP_CAP
|
|
146
|
-
: readOptionalInt(record["decompressTempCap"], MIN_DECOMPRESS_TEMP_CAP, MAX_DECOMPRESS_TEMP_CAP)
|
|
147
|
-
if (decompressInlineMax === undefined || decompressTempCap === undefined) return undefined
|
|
148
|
-
return {
|
|
149
|
-
contextLimit: limit,
|
|
150
|
-
triggerPercent: Math.round(percent),
|
|
151
|
-
nudgeGrowthTokens: Math.round(nudge),
|
|
152
|
-
summaryBlockPercent: Math.round(blockPercent * 10) / 10,
|
|
153
|
-
summaryTotalPercent: Math.round(totalPercent * 10) / 10,
|
|
154
|
-
summaryLanguage: language,
|
|
155
|
-
summaryFormat,
|
|
156
|
-
autoCompress,
|
|
157
|
-
enableCompressTool,
|
|
158
|
-
enableDecompressTool,
|
|
159
|
-
showTimeline,
|
|
160
|
-
showBlockTokens,
|
|
161
|
-
decompressInlineMax,
|
|
162
|
-
decompressTempCap,
|
|
163
|
-
decompressShowTokens,
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** 输入:storage;输出:设置(缺失或损坏时用默认值)。 */
|
|
168
|
-
export async function loadServerSettings(storage: StorageLike): Promise<ServerSettings> {
|
|
169
|
-
return parseServerSettings(await storage.get(SETTINGS_KEY)) ?? DEFAULT_SERVER_SETTINGS
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** 输入:storage + 设置;输出:无。 */
|
|
173
|
-
export async function saveServerSettings(storage: StorageLike, settings: ServerSettings): Promise<void> {
|
|
174
|
-
await storage.set(SETTINGS_KEY, settings as unknown as Schema.Json)
|
|
175
|
-
}
|
|
1
|
+
import type { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_AUTO_COMPRESS,
|
|
5
|
+
DEFAULT_DECOMPRESS_INLINE_MAX,
|
|
6
|
+
DEFAULT_DECOMPRESS_SHOW_TOKENS,
|
|
7
|
+
DEFAULT_DECOMPRESS_TEMP_CAP,
|
|
8
|
+
DEFAULT_ENABLE_COMPRESS_TOOL,
|
|
9
|
+
DEFAULT_ENABLE_DECOMPRESS_TOOL,
|
|
10
|
+
DEFAULT_NUDGE_GROWTH_TOKENS,
|
|
11
|
+
DEFAULT_SHOW_BLOCK_TOKENS,
|
|
12
|
+
DEFAULT_SHOW_TIMELINE,
|
|
13
|
+
DEFAULT_SUMMARY_BLOCK_PERCENT,
|
|
14
|
+
DEFAULT_SUMMARY_FORMAT,
|
|
15
|
+
DEFAULT_SUMMARY_TOTAL_PERCENT,
|
|
16
|
+
MAX_DECOMPRESS_INLINE_MAX,
|
|
17
|
+
MAX_DECOMPRESS_TEMP_CAP,
|
|
18
|
+
MAX_CONTEXT_LIMIT,
|
|
19
|
+
MAX_SUMMARY_PERCENT,
|
|
20
|
+
MAX_TRIGGER_PERCENT,
|
|
21
|
+
MIN_CONTEXT_LIMIT,
|
|
22
|
+
MIN_DECOMPRESS_INLINE_MAX,
|
|
23
|
+
MIN_DECOMPRESS_TEMP_CAP,
|
|
24
|
+
MIN_SUMMARY_PERCENT,
|
|
25
|
+
MIN_TRIGGER_PERCENT,
|
|
26
|
+
isSummaryLanguage,
|
|
27
|
+
isSummaryFormat,
|
|
28
|
+
type CompressionSettings,
|
|
29
|
+
} from "../shared/settings.ts"
|
|
30
|
+
import { readOptionalInt, readStrictBoolean } from "../shared/parse.ts"
|
|
31
|
+
import type { StorageLike } from "./state.ts"
|
|
32
|
+
|
|
33
|
+
/** 服务端从 TUI 收到的设置;字段与 shared 的 CompressionSettings 一致。 */
|
|
34
|
+
export type ServerSettings = CompressionSettings
|
|
35
|
+
|
|
36
|
+
export const SETTINGS_KEY = "settings"
|
|
37
|
+
/** 插件最近一次从模型解析到的原生窗口;TUI 用它把绝对值换算成百分比。 */
|
|
38
|
+
export const RESOLVED_WINDOW_KEY = "window:last"
|
|
39
|
+
|
|
40
|
+
/** 最近一次解析到的「生效窗口」= min(settings.contextLimit, 原生窗口);自动压缩用它折算阈值。 */
|
|
41
|
+
export const EFFECTIVE_WINDOW_KEY = "window:effective"
|
|
42
|
+
|
|
43
|
+
/** 输入:模型标识;输出:该模型原生窗口在 storage 里的 key。 */
|
|
44
|
+
export function windowKey(providerID: string, modelID: string): string {
|
|
45
|
+
return `window:${providerID}/${modelID}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_SERVER_SETTINGS: ServerSettings = {
|
|
49
|
+
contextLimit: "auto",
|
|
50
|
+
triggerPercent: 80,
|
|
51
|
+
nudgeGrowthTokens: DEFAULT_NUDGE_GROWTH_TOKENS,
|
|
52
|
+
summaryBlockPercent: DEFAULT_SUMMARY_BLOCK_PERCENT,
|
|
53
|
+
summaryTotalPercent: DEFAULT_SUMMARY_TOTAL_PERCENT,
|
|
54
|
+
summaryLanguage: "auto",
|
|
55
|
+
summaryFormat: DEFAULT_SUMMARY_FORMAT,
|
|
56
|
+
autoCompress: DEFAULT_AUTO_COMPRESS,
|
|
57
|
+
enableCompressTool: DEFAULT_ENABLE_COMPRESS_TOOL,
|
|
58
|
+
enableDecompressTool: DEFAULT_ENABLE_DECOMPRESS_TOOL,
|
|
59
|
+
showTimeline: DEFAULT_SHOW_TIMELINE,
|
|
60
|
+
showBlockTokens: DEFAULT_SHOW_BLOCK_TOKENS,
|
|
61
|
+
decompressInlineMax: DEFAULT_DECOMPRESS_INLINE_MAX,
|
|
62
|
+
decompressTempCap: DEFAULT_DECOMPRESS_TEMP_CAP,
|
|
63
|
+
decompressShowTokens: DEFAULT_DECOMPRESS_SHOW_TOKENS,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 输入:storage 里的原始值。
|
|
68
|
+
* 输出:合法设置;非法返回 undefined。
|
|
69
|
+
* 预期:范围外的值一律拒绝,不静默修正。
|
|
70
|
+
*/
|
|
71
|
+
export function parseServerSettings(value: unknown): ServerSettings | undefined {
|
|
72
|
+
if (typeof value !== "object" || value === null) return undefined
|
|
73
|
+
const record = value as Record<string, unknown>
|
|
74
|
+
const limit = record["contextLimit"]
|
|
75
|
+
const percent = record["triggerPercent"]
|
|
76
|
+
const nudge = record["nudgeGrowthTokens"] ?? DEFAULT_NUDGE_GROWTH_TOKENS
|
|
77
|
+
const blockPercent = record["summaryBlockPercent"] ?? DEFAULT_SUMMARY_BLOCK_PERCENT
|
|
78
|
+
const totalPercent = record["summaryTotalPercent"] ?? DEFAULT_SUMMARY_TOTAL_PERCENT
|
|
79
|
+
const language = record["summaryLanguage"] ?? "auto"
|
|
80
|
+
const summaryFormat = record["summaryFormat"] ?? DEFAULT_SUMMARY_FORMAT
|
|
81
|
+
if (
|
|
82
|
+
limit !== "auto" &&
|
|
83
|
+
(typeof limit !== "number" ||
|
|
84
|
+
!Number.isFinite(limit) ||
|
|
85
|
+
limit < MIN_CONTEXT_LIMIT ||
|
|
86
|
+
limit > MAX_CONTEXT_LIMIT)
|
|
87
|
+
) {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
typeof percent !== "number" ||
|
|
92
|
+
!Number.isFinite(percent) ||
|
|
93
|
+
percent < MIN_TRIGGER_PERCENT ||
|
|
94
|
+
percent > MAX_TRIGGER_PERCENT
|
|
95
|
+
) {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
typeof nudge !== "number" ||
|
|
100
|
+
!Number.isFinite(nudge) ||
|
|
101
|
+
(nudge !== 0 && (nudge < 1_000 || nudge > 200_000))
|
|
102
|
+
) {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
typeof blockPercent !== "number" ||
|
|
107
|
+
!Number.isFinite(blockPercent) ||
|
|
108
|
+
blockPercent < MIN_SUMMARY_PERCENT ||
|
|
109
|
+
blockPercent > MAX_SUMMARY_PERCENT
|
|
110
|
+
) {
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
typeof totalPercent !== "number" ||
|
|
115
|
+
!Number.isFinite(totalPercent) ||
|
|
116
|
+
totalPercent < MIN_SUMMARY_PERCENT ||
|
|
117
|
+
totalPercent > MAX_SUMMARY_PERCENT
|
|
118
|
+
) {
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
121
|
+
if (!isSummaryLanguage(language)) return undefined
|
|
122
|
+
if (!isSummaryFormat(summaryFormat)) return undefined
|
|
123
|
+
const autoCompress = readStrictBoolean(record["autoCompress"], DEFAULT_AUTO_COMPRESS)
|
|
124
|
+
const enableCompressTool = readStrictBoolean(record["enableCompressTool"], DEFAULT_ENABLE_COMPRESS_TOOL)
|
|
125
|
+
const enableDecompressTool = readStrictBoolean(record["enableDecompressTool"], DEFAULT_ENABLE_DECOMPRESS_TOOL)
|
|
126
|
+
const showTimeline = readStrictBoolean(record["showTimeline"], DEFAULT_SHOW_TIMELINE)
|
|
127
|
+
const showBlockTokens = readStrictBoolean(record["showBlockTokens"], DEFAULT_SHOW_BLOCK_TOKENS)
|
|
128
|
+
const decompressShowTokens = readStrictBoolean(record["decompressShowTokens"], DEFAULT_DECOMPRESS_SHOW_TOKENS)
|
|
129
|
+
if (
|
|
130
|
+
autoCompress === undefined ||
|
|
131
|
+
enableCompressTool === undefined ||
|
|
132
|
+
enableDecompressTool === undefined ||
|
|
133
|
+
showTimeline === undefined ||
|
|
134
|
+
showBlockTokens === undefined ||
|
|
135
|
+
decompressShowTokens === undefined
|
|
136
|
+
) {
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
const decompressInlineMax =
|
|
140
|
+
record["decompressInlineMax"] === undefined
|
|
141
|
+
? DEFAULT_DECOMPRESS_INLINE_MAX
|
|
142
|
+
: readOptionalInt(record["decompressInlineMax"], MIN_DECOMPRESS_INLINE_MAX, MAX_DECOMPRESS_INLINE_MAX)
|
|
143
|
+
const decompressTempCap =
|
|
144
|
+
record["decompressTempCap"] === undefined
|
|
145
|
+
? DEFAULT_DECOMPRESS_TEMP_CAP
|
|
146
|
+
: readOptionalInt(record["decompressTempCap"], MIN_DECOMPRESS_TEMP_CAP, MAX_DECOMPRESS_TEMP_CAP)
|
|
147
|
+
if (decompressInlineMax === undefined || decompressTempCap === undefined) return undefined
|
|
148
|
+
return {
|
|
149
|
+
contextLimit: limit,
|
|
150
|
+
triggerPercent: Math.round(percent),
|
|
151
|
+
nudgeGrowthTokens: Math.round(nudge),
|
|
152
|
+
summaryBlockPercent: Math.round(blockPercent * 10) / 10,
|
|
153
|
+
summaryTotalPercent: Math.round(totalPercent * 10) / 10,
|
|
154
|
+
summaryLanguage: language,
|
|
155
|
+
summaryFormat,
|
|
156
|
+
autoCompress,
|
|
157
|
+
enableCompressTool,
|
|
158
|
+
enableDecompressTool,
|
|
159
|
+
showTimeline,
|
|
160
|
+
showBlockTokens,
|
|
161
|
+
decompressInlineMax,
|
|
162
|
+
decompressTempCap,
|
|
163
|
+
decompressShowTokens,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 输入:storage;输出:设置(缺失或损坏时用默认值)。 */
|
|
168
|
+
export async function loadServerSettings(storage: StorageLike): Promise<ServerSettings> {
|
|
169
|
+
return parseServerSettings(await storage.get(SETTINGS_KEY)) ?? DEFAULT_SERVER_SETTINGS
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 输入:storage + 设置;输出:无。 */
|
|
173
|
+
export async function saveServerSettings(storage: StorageLike, settings: ServerSettings): Promise<void> {
|
|
174
|
+
await storage.set(SETTINGS_KEY, settings as unknown as Schema.Json)
|
|
175
|
+
}
|