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
package/src/shared/media.ts
CHANGED
|
@@ -1,184 +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
|
-
}
|
|
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
|
+
}
|
package/src/shared/parse.ts
CHANGED
|
@@ -1,25 +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
|
-
}
|
|
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
|
+
}
|
package/src/shared/parts.ts
CHANGED
|
@@ -1,72 +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
|
-
}
|
|
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
|
+
}
|