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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 多模态附件 token 估算。
|
|
3
|
+
*
|
|
4
|
+
* 输入:mime、base64/字节数据、可选时长。
|
|
5
|
+
* 输出:粗略 token 数。
|
|
6
|
+
* 预期:拿不到宽高/页数/时长时使用保守固定值;这里只做估算,不伪装成精确 tokenizer。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 图片拿不到尺寸时的固定估算。 */
|
|
10
|
+
export const IMAGE_FALLBACK_TOKENS = 1_500
|
|
11
|
+
/** PDF 拿不到页数时的固定估算。 */
|
|
12
|
+
export const PDF_FALLBACK_TOKENS = 2_000
|
|
13
|
+
/** PDF 每页按一张图估算。 */
|
|
14
|
+
export const PDF_PAGE_TOKENS = 1_500
|
|
15
|
+
/** 音频每秒估算。 */
|
|
16
|
+
export const AUDIO_TOKENS_PER_SECOND = 32
|
|
17
|
+
/** 音频拿不到时长时的固定估算。 */
|
|
18
|
+
export const AUDIO_FALLBACK_TOKENS = 1_000
|
|
19
|
+
/** 视频每秒估算。 */
|
|
20
|
+
export const VIDEO_TOKENS_PER_SECOND = 263
|
|
21
|
+
/** 视频拿不到时长时的固定估算。 */
|
|
22
|
+
export const VIDEO_FALLBACK_TOKENS = 10_000
|
|
23
|
+
/** 未知二进制文件固定估算。 */
|
|
24
|
+
export const FILE_FALLBACK_TOKENS = 2_000
|
|
25
|
+
|
|
26
|
+
export interface AttachmentInput {
|
|
27
|
+
readonly mime?: string | undefined
|
|
28
|
+
readonly data?: string | Uint8Array | undefined
|
|
29
|
+
readonly width?: number | undefined
|
|
30
|
+
readonly height?: number | undefined
|
|
31
|
+
readonly durationSeconds?: number | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ImageSize {
|
|
35
|
+
readonly width: number
|
|
36
|
+
readonly height: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 输入:base64 或字节;输出:字节;失败返回 undefined。 */
|
|
40
|
+
function toBytes(data: string | Uint8Array | undefined): Uint8Array | undefined {
|
|
41
|
+
if (data === undefined) return undefined
|
|
42
|
+
if (data instanceof Uint8Array) return data
|
|
43
|
+
try {
|
|
44
|
+
return Uint8Array.from(Buffer.from(data, "base64"))
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 输入:字节 + 偏移;输出:大端 u32;越界返回 0。 */
|
|
51
|
+
function u32be(bytes: Uint8Array, offset: number): number {
|
|
52
|
+
const a = bytes[offset] ?? 0
|
|
53
|
+
const b = bytes[offset + 1] ?? 0
|
|
54
|
+
const c = bytes[offset + 2] ?? 0
|
|
55
|
+
const d = bytes[offset + 3] ?? 0
|
|
56
|
+
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 输入:字节 + 偏移;输出:小端 u16;越界返回 0。 */
|
|
60
|
+
function u16le(bytes: Uint8Array, offset: number): number {
|
|
61
|
+
return (bytes[offset] ?? 0) | ((bytes[offset + 1] ?? 0) << 8)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 输入:字节 + 偏移;输出:大端 u16;越界返回 0。 */
|
|
65
|
+
function u16be(bytes: Uint8Array, offset: number): number {
|
|
66
|
+
return ((bytes[offset] ?? 0) << 8) | (bytes[offset + 1] ?? 0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 输入:字节 + 偏移;输出:小端 u24;越界返回 0。 */
|
|
70
|
+
function u24le(bytes: Uint8Array, offset: number): number {
|
|
71
|
+
return (bytes[offset] ?? 0) | ((bytes[offset + 1] ?? 0) << 8) | ((bytes[offset + 2] ?? 0) << 16)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 输入:PNG 字节;输出:尺寸;不是 PNG 返回 undefined。 */
|
|
75
|
+
function pngSize(bytes: Uint8Array): ImageSize | undefined {
|
|
76
|
+
if (bytes.length < 24) return undefined
|
|
77
|
+
const signature = [137, 80, 78, 71, 13, 10, 26, 10]
|
|
78
|
+
if (!signature.every((value, index) => bytes[index] === value)) return undefined
|
|
79
|
+
const width = u32be(bytes, 16)
|
|
80
|
+
const height = u32be(bytes, 20)
|
|
81
|
+
return width > 0 && height > 0 ? { width, height } : undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 输入:GIF 字节;输出:尺寸;不是 GIF 返回 undefined。 */
|
|
85
|
+
function gifSize(bytes: Uint8Array): ImageSize | undefined {
|
|
86
|
+
if (bytes.length < 10) return undefined
|
|
87
|
+
const header = String.fromCharCode(...bytes.slice(0, 6))
|
|
88
|
+
if (header !== "GIF87a" && header !== "GIF89a") return undefined
|
|
89
|
+
const width = u16le(bytes, 6)
|
|
90
|
+
const height = u16le(bytes, 8)
|
|
91
|
+
return width > 0 && height > 0 ? { width, height } : undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 输入:JPEG 字节;输出:尺寸;找不到 SOF 返回 undefined。 */
|
|
95
|
+
function jpegSize(bytes: Uint8Array): ImageSize | undefined {
|
|
96
|
+
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return undefined
|
|
97
|
+
let offset = 2
|
|
98
|
+
while (offset + 9 < bytes.length) {
|
|
99
|
+
if (bytes[offset] !== 0xff) {
|
|
100
|
+
offset += 1
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const marker = bytes[offset + 1] ?? 0
|
|
104
|
+
const length = u16be(bytes, offset + 2)
|
|
105
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
106
|
+
const height = u16be(bytes, offset + 5)
|
|
107
|
+
const width = u16be(bytes, offset + 7)
|
|
108
|
+
return width > 0 && height > 0 ? { width, height } : undefined
|
|
109
|
+
}
|
|
110
|
+
if (length < 2) return undefined
|
|
111
|
+
offset += 2 + length
|
|
112
|
+
}
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 输入:WebP 字节;输出:尺寸;VP8X 之外的编码回退 undefined。 */
|
|
117
|
+
function webpSize(bytes: Uint8Array): ImageSize | undefined {
|
|
118
|
+
if (bytes.length < 30) return undefined
|
|
119
|
+
const riff = String.fromCharCode(...bytes.slice(0, 4))
|
|
120
|
+
const webp = String.fromCharCode(...bytes.slice(8, 12))
|
|
121
|
+
const chunk = String.fromCharCode(...bytes.slice(12, 16))
|
|
122
|
+
if (riff !== "RIFF" || webp !== "WEBP" || chunk !== "VP8X") return undefined
|
|
123
|
+
const width = 1 + u24le(bytes, 24)
|
|
124
|
+
const height = 1 + u24le(bytes, 27)
|
|
125
|
+
return width > 0 && height > 0 ? { width, height } : undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 输入:附件数据;输出:图片尺寸;不是图片或解析失败返回 undefined。 */
|
|
129
|
+
export function imageSize(data: string | Uint8Array | undefined): ImageSize | undefined {
|
|
130
|
+
const bytes = toBytes(data)
|
|
131
|
+
if (bytes === undefined) return undefined
|
|
132
|
+
return pngSize(bytes) ?? gifSize(bytes) ?? jpegSize(bytes) ?? webpSize(bytes)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 输入:图片宽高;输出:通用经验公式估算。 */
|
|
136
|
+
export function imageTokens(width: number, height: number): number {
|
|
137
|
+
if (width <= 0 || height <= 0) return IMAGE_FALLBACK_TOKENS
|
|
138
|
+
return Math.max(258, Math.ceil((width * height) / 750))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 输入:PDF 字节;输出:页数;解析失败返回 undefined。 */
|
|
142
|
+
export function pdfPages(data: string | Uint8Array | undefined): number | undefined {
|
|
143
|
+
const bytes = toBytes(data)
|
|
144
|
+
if (bytes === undefined) return undefined
|
|
145
|
+
const text = Buffer.from(bytes).toString("latin1")
|
|
146
|
+
const pages = text.match(/\/Type\s*\/Page\b/g)
|
|
147
|
+
return pages === null ? undefined : pages.length
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 输入:文本类文件字节;输出:粗略 token。 */
|
|
151
|
+
function textFileTokens(bytes: Uint8Array): number {
|
|
152
|
+
return Math.max(1, Math.ceil(bytes.length / 4))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 输入:附件;输出:通用经验估算。 */
|
|
156
|
+
export function attachmentTokens(input: AttachmentInput): number {
|
|
157
|
+
const mime = (input.mime ?? "").toLowerCase()
|
|
158
|
+
const bytes = toBytes(input.data)
|
|
159
|
+
if (mime.startsWith("image/")) {
|
|
160
|
+
if (input.width !== undefined && input.height !== undefined) {
|
|
161
|
+
return imageTokens(input.width, input.height)
|
|
162
|
+
}
|
|
163
|
+
const size = imageSize(input.data)
|
|
164
|
+
return size === undefined ? IMAGE_FALLBACK_TOKENS : imageTokens(size.width, size.height)
|
|
165
|
+
}
|
|
166
|
+
if (mime === "application/pdf") {
|
|
167
|
+
const pages = pdfPages(input.data)
|
|
168
|
+
return pages === undefined ? PDF_FALLBACK_TOKENS : pages * PDF_PAGE_TOKENS
|
|
169
|
+
}
|
|
170
|
+
if (mime.startsWith("audio/")) {
|
|
171
|
+
return input.durationSeconds === undefined
|
|
172
|
+
? AUDIO_FALLBACK_TOKENS
|
|
173
|
+
: Math.max(1, Math.ceil(input.durationSeconds * AUDIO_TOKENS_PER_SECOND))
|
|
174
|
+
}
|
|
175
|
+
if (mime.startsWith("video/")) {
|
|
176
|
+
return input.durationSeconds === undefined
|
|
177
|
+
? VIDEO_FALLBACK_TOKENS
|
|
178
|
+
: Math.max(1, Math.ceil(input.durationSeconds * VIDEO_TOKENS_PER_SECOND))
|
|
179
|
+
}
|
|
180
|
+
if (mime.startsWith("text/") || mime === "application/json") {
|
|
181
|
+
return bytes === undefined ? FILE_FALLBACK_TOKENS : textFileTokens(bytes)
|
|
182
|
+
}
|
|
183
|
+
return FILE_FALLBACK_TOKENS
|
|
184
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** 输入:任意值 + 范围;输出:合法整数;非法返回 fallback。 */
|
|
2
|
+
export function readInt(value: unknown, min: number, max: number, fallback: number): number {
|
|
3
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback
|
|
4
|
+
const rounded = Math.round(value)
|
|
5
|
+
if (rounded < min || rounded > max) return fallback
|
|
6
|
+
return rounded
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** 输入:任意值 + 范围;输出:合法整数;非法返回 undefined。 */
|
|
10
|
+
export function readOptionalInt(value: unknown, min: number, max: number): number | undefined {
|
|
11
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined
|
|
12
|
+
const rounded = Math.round(value)
|
|
13
|
+
if (rounded < min || rounded > max) return undefined
|
|
14
|
+
return rounded
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 输入:任意值 + 默认值。
|
|
19
|
+
* 输出:布尔;缺省返回 fallback,类型不对返回 undefined。
|
|
20
|
+
* 预期:服务端设置对错误类型严格拒绝;TUI 迁移用宽松版本。
|
|
21
|
+
*/
|
|
22
|
+
export function readStrictBoolean(value: unknown, fallback: boolean): boolean | undefined {
|
|
23
|
+
if (value === undefined) return fallback
|
|
24
|
+
return typeof value === "boolean" ? value : undefined
|
|
25
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 消息 part 的兼容层。
|
|
3
|
+
*
|
|
4
|
+
* OpenCode v2 的工具 part 是 `type: "tool"`,输入/输出在 `state.input` /
|
|
5
|
+
* `state.content`;旧测试和部分 provider 仍用 `tool-call` / `tool-result`。
|
|
6
|
+
* 这里只做形状归一,避免 tokens / transcript / select 各自漏一种形状。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 工具 part 的 state;只保留计数和转录需要的字段。 */
|
|
10
|
+
export interface ToolStateLike {
|
|
11
|
+
readonly input?: unknown
|
|
12
|
+
readonly content?: unknown
|
|
13
|
+
readonly output?: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 工具 part 的最小形状。 */
|
|
17
|
+
export interface ToolPartLike {
|
|
18
|
+
readonly name?: string | undefined
|
|
19
|
+
readonly input?: unknown
|
|
20
|
+
readonly result?: unknown
|
|
21
|
+
readonly state?: ToolStateLike | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 输入:任意值;输出:可计数文本;不可序列化时返回空串。 */
|
|
25
|
+
export function stringify(value: unknown): string {
|
|
26
|
+
if (typeof value === "string") return value
|
|
27
|
+
if (value === undefined) return ""
|
|
28
|
+
try {
|
|
29
|
+
const encoded: unknown = JSON.stringify(value)
|
|
30
|
+
return typeof encoded === "string" ? encoded : ""
|
|
31
|
+
} catch {
|
|
32
|
+
return ""
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 输入:工具 part;输出:工具调用参数。 */
|
|
37
|
+
export function toolInput(part: ToolPartLike): unknown {
|
|
38
|
+
return part.input ?? part.state?.input
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 输入:工具 part;输出:工具结果。 */
|
|
42
|
+
export function toolResult(part: ToolPartLike): unknown {
|
|
43
|
+
return part.result ?? part.state?.content ?? part.state?.output
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 输入:工具结果(v2 state.content 数组、字符串、对象或旧 result.value)。
|
|
48
|
+
* 输出:可读文本;数组只拼 text 项,其余项退回 JSON。
|
|
49
|
+
* 预期:不丢工具输出,同时避免把媒体 base64 当普通文本展开。
|
|
50
|
+
*/
|
|
51
|
+
export function toolResultText(value: unknown): string {
|
|
52
|
+
if (typeof value === "string") return value
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
return value
|
|
55
|
+
.map((item) => {
|
|
56
|
+
if (typeof item === "object" && item !== null) {
|
|
57
|
+
const record = item as { readonly type?: unknown; readonly text?: unknown }
|
|
58
|
+
if (record.type === "text" && typeof record.text === "string") return record.text
|
|
59
|
+
}
|
|
60
|
+
return stringify(item)
|
|
61
|
+
})
|
|
62
|
+
.filter((text) => text.length > 0)
|
|
63
|
+
.join("\n")
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "object" && value !== null) {
|
|
66
|
+
const record = value as { readonly content?: unknown; readonly output?: unknown; readonly value?: unknown }
|
|
67
|
+
if (record.content !== undefined) return toolResultText(record.content)
|
|
68
|
+
if (record.output !== undefined) return toolResultText(record.output)
|
|
69
|
+
if (record.value !== undefined) return toolResultText(record.value)
|
|
70
|
+
}
|
|
71
|
+
return stringify(value)
|
|
72
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Rpc } from "@opencode/plugin/rpc"
|
|
2
|
+
|
|
3
|
+
import type { SummaryBlock } from "./blocks.ts"
|
|
4
|
+
import type { CacheSample } from "./cache.ts"
|
|
5
|
+
import type { CompressionSettings } from "./settings.ts"
|
|
6
|
+
|
|
7
|
+
const SettingsGetInputJson = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: { sessionID: { type: "string" } },
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
const SettingsProperties = {
|
|
14
|
+
contextLimit: {
|
|
15
|
+
anyOf: [{ type: "string", enum: ["auto"] }, { type: "number" }],
|
|
16
|
+
},
|
|
17
|
+
triggerPercent: { type: "number" },
|
|
18
|
+
nudgeGrowthTokens: { type: "number" },
|
|
19
|
+
summaryBlockPercent: { type: "number" },
|
|
20
|
+
summaryTotalPercent: { type: "number" },
|
|
21
|
+
summaryLanguage: { type: "string", enum: ["auto", "zh", "en"] },
|
|
22
|
+
summaryFormat: { type: "string", enum: ["json", "markdown", "babel"] },
|
|
23
|
+
autoCompress: { type: "boolean" },
|
|
24
|
+
enableCompressTool: { type: "boolean" },
|
|
25
|
+
enableDecompressTool: { type: "boolean" },
|
|
26
|
+
showTimeline: { type: "boolean" },
|
|
27
|
+
showBlockTokens: { type: "boolean" },
|
|
28
|
+
decompressInlineMax: { type: "number" },
|
|
29
|
+
decompressTempCap: { type: "number" },
|
|
30
|
+
decompressShowTokens: { type: "boolean" },
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
const SettingsInputJson = {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: SettingsProperties,
|
|
36
|
+
required: ["contextLimit", "triggerPercent"],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
} as const
|
|
39
|
+
|
|
40
|
+
/** 读取结果比写入多一个 resolvedWindow:auto 时从模型原生窗口解析出来的值。 */
|
|
41
|
+
const SettingsOutputJson = {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
...SettingsProperties,
|
|
45
|
+
resolvedWindow: { type: "number" },
|
|
46
|
+
},
|
|
47
|
+
required: ["contextLimit", "triggerPercent"],
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
} as const
|
|
50
|
+
|
|
51
|
+
const OkJson = {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: { ok: { type: "boolean" } },
|
|
54
|
+
required: ["ok"],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
} as const
|
|
57
|
+
|
|
58
|
+
/** 需要明确指定会话的只读查询。 */
|
|
59
|
+
const SessionInputJson = {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: { sessionID: { type: "string" } },
|
|
62
|
+
required: ["sessionID"],
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
} as const
|
|
65
|
+
|
|
66
|
+
/** 摘要块:服务端返回 active 链,body 是渲染好的 markdown。 */
|
|
67
|
+
const BlockJson = {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
id: { type: "string" },
|
|
71
|
+
tier: { type: "number", enum: [1, 2, 3] },
|
|
72
|
+
format: { type: "string", enum: ["json", "markdown", "babel"] },
|
|
73
|
+
title: { type: "string" },
|
|
74
|
+
at: { type: "number" },
|
|
75
|
+
tokensBefore: { type: "number" },
|
|
76
|
+
tokensAfter: { type: "number" },
|
|
77
|
+
sourceBlockIds: { type: "array", items: { type: "string" } },
|
|
78
|
+
body: { type: "string" },
|
|
79
|
+
},
|
|
80
|
+
required: ["id", "tier", "format", "title", "at", "tokensBefore", "tokensAfter", "body"],
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
} as const
|
|
83
|
+
|
|
84
|
+
const BlocksOutputJson = {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: { blocks: { type: "array", items: BlockJson } },
|
|
87
|
+
required: ["blocks"],
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
} as const
|
|
90
|
+
|
|
91
|
+
/** 一条缓存命中样本。 */
|
|
92
|
+
const CacheSampleJson = {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
messageID: { type: "string" },
|
|
96
|
+
at: { type: "number" },
|
|
97
|
+
input: { type: "number" },
|
|
98
|
+
output: { type: "number" },
|
|
99
|
+
cached: { type: "number" },
|
|
100
|
+
hitRate: { type: "number" },
|
|
101
|
+
},
|
|
102
|
+
required: ["messageID", "at", "input", "output", "cached", "hitRate"],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
} as const
|
|
105
|
+
|
|
106
|
+
const CacheHistoryOutputJson = {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: { samples: { type: "array", items: CacheSampleJson } },
|
|
109
|
+
required: ["samples"],
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
} as const
|
|
112
|
+
|
|
113
|
+
/** TUI 与服务端共用的设置 / 面板 RPC。 */
|
|
114
|
+
export const ContextRpc: Rpc.PortableDefinition = Rpc.define({
|
|
115
|
+
id: "compressed-context",
|
|
116
|
+
methods: {
|
|
117
|
+
settingsGet: { input: SettingsGetInputJson, output: SettingsOutputJson },
|
|
118
|
+
settingsSet: { input: SettingsInputJson, output: OkJson },
|
|
119
|
+
blocksGet: { input: SessionInputJson, output: BlocksOutputJson },
|
|
120
|
+
cacheHistoryGet: { input: SessionInputJson, output: CacheHistoryOutputJson },
|
|
121
|
+
},
|
|
122
|
+
events: {},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/** 服务端设置读取结果。 */
|
|
126
|
+
export interface ContextSettingsResult extends CompressionSettings {
|
|
127
|
+
readonly resolvedWindow?: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** TUI 侧对 ContextRpc 的类型化视图。 */
|
|
131
|
+
export interface ContextRpcClient {
|
|
132
|
+
settingsGet(input: { readonly sessionID?: string }): Promise<ContextSettingsResult>
|
|
133
|
+
settingsSet(input: CompressionSettings): Promise<{ readonly ok: boolean }>
|
|
134
|
+
blocksGet(input: { readonly sessionID: string }): Promise<{ readonly blocks: readonly SummaryBlock[] }>
|
|
135
|
+
cacheHistoryGet(input: {
|
|
136
|
+
readonly sessionID: string
|
|
137
|
+
}): Promise<{ readonly samples: readonly CacheSample[] }>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 输入:带 rpc 方法的 TUI client;输出:类型化 RPC 客户端。 */
|
|
141
|
+
export function contextRpc(client: {
|
|
142
|
+
rpc(definition: Rpc.PortableDefinition): unknown
|
|
143
|
+
}): ContextRpcClient {
|
|
144
|
+
return client.rpc(ContextRpc) as ContextRpcClient
|
|
145
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 输入:具备 scan 能力的 storage + 前缀 + 每条目的处理函数。
|
|
3
|
+
* 输出:无。
|
|
4
|
+
* 预期:按 next 游标分页遍历全部匹配条目,逐条串行 await,避免一次性把 key 全读进内存。
|
|
5
|
+
*/
|
|
6
|
+
export interface ScanStorageLike {
|
|
7
|
+
readonly scan?: (options: { prefix: string; limit: number; after?: string }) => Promise<{
|
|
8
|
+
entries: readonly { key: string; value: unknown }[]
|
|
9
|
+
next?: string
|
|
10
|
+
}>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 输入:storage + 前缀 + 访问函数 + 每页条数;输出:无;预期:storage 无 scan 时直接返回。 */
|
|
14
|
+
export async function forEachScannedEntry(
|
|
15
|
+
storage: ScanStorageLike,
|
|
16
|
+
prefix: string,
|
|
17
|
+
visit: (entry: { key: string; value: unknown }) => Promise<void>,
|
|
18
|
+
limit = 100,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
if (storage.scan === undefined) return
|
|
21
|
+
let after: string | undefined
|
|
22
|
+
for (;;) {
|
|
23
|
+
const page = await storage.scan({
|
|
24
|
+
prefix,
|
|
25
|
+
limit,
|
|
26
|
+
...(after === undefined ? {} : { after }),
|
|
27
|
+
})
|
|
28
|
+
for (const entry of page.entries) await visit(entry)
|
|
29
|
+
if (page.next === undefined) return
|
|
30
|
+
after = page.next
|
|
31
|
+
}
|
|
32
|
+
}
|