compressed-context 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +59 -51
  2. package/package.json +64 -64
  3. package/src/compression/active-summary-cache.ts +50 -50
  4. package/src/compression/adopt.ts +57 -57
  5. package/src/compression/auto.ts +386 -372
  6. package/src/compression/babel.ts +85 -85
  7. package/src/compression/block.ts +25 -25
  8. package/src/compression/cache-history.ts +75 -75
  9. package/src/compression/compaction.ts +189 -189
  10. package/src/compression/decompress-tool.ts +74 -74
  11. package/src/compression/decompress.ts +292 -292
  12. package/src/compression/deleted-sessions.ts +24 -24
  13. package/src/compression/distill.ts +220 -220
  14. package/src/compression/events.ts +275 -275
  15. package/src/compression/markdown.ts +53 -53
  16. package/src/compression/normalize.ts +114 -114
  17. package/src/compression/render.ts +70 -70
  18. package/src/compression/schema.ts +131 -84
  19. package/src/compression/select.ts +123 -123
  20. package/src/compression/session-message.ts +112 -112
  21. package/src/compression/settings-rpc.ts +123 -111
  22. package/src/compression/settings.ts +175 -175
  23. package/src/compression/shape.ts +75 -0
  24. package/src/compression/state.ts +248 -248
  25. package/src/compression/summarize.ts +534 -595
  26. package/src/compression/summary.ts +73 -73
  27. package/src/compression/tool.ts +184 -142
  28. package/src/compression/transcript-store.ts +258 -258
  29. package/src/compression/transcript.ts +63 -63
  30. package/src/compression/validate.ts +81 -81
  31. package/src/i18n/en.ts +93 -93
  32. package/src/i18n/index.ts +28 -28
  33. package/src/i18n/types.ts +89 -89
  34. package/src/i18n/zh.ts +93 -93
  35. package/src/index.ts +116 -108
  36. package/src/shared/blocks.ts +17 -17
  37. package/src/shared/cache.ts +199 -199
  38. package/src/shared/format.ts +23 -23
  39. package/src/shared/media.ts +184 -184
  40. package/src/shared/parse.ts +25 -25
  41. package/src/shared/parts.ts +72 -72
  42. package/src/shared/rpc.ts +145 -145
  43. package/src/shared/scan.ts +32 -32
  44. package/src/shared/settings.ts +466 -471
  45. package/src/shared/stats.ts +142 -142
  46. package/src/shared/tokens.ts +259 -259
  47. package/src/tui/chart/cache-chart.ts +237 -237
  48. package/src/tui/chart/color.ts +58 -58
  49. package/src/tui/index.tsx +109 -111
  50. package/src/tui/panel/cache.tsx +203 -203
  51. package/src/tui/panel/data.ts +105 -105
  52. package/src/tui/panel/index.tsx +176 -176
  53. package/src/tui/panel/summaries.tsx +203 -192
  54. package/src/tui/settings/dialogs.tsx +215 -215
  55. package/src/tui/settings/options.ts +238 -238
  56. package/src/tui/settings.tsx +418 -418
  57. package/src/tui/types.ts +4 -4
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }