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/cache.ts
CHANGED
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 缓存命中历史的跨进程载体。
|
|
3
|
-
*
|
|
4
|
-
* 服务端插件在 session.idle 时把最近 10 条 assistant 消息的 usage 追加到
|
|
5
|
-
* storage(`cache:history:<sessionID>`);TUI 通过 RPC 读取。
|
|
6
|
-
* 本模块只放纯函数:采样、追加、校验、排序,方便单测。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** 每个会话一份,key 里带 sessionID,父/子会话互不影响。 */
|
|
10
|
-
export const CACHE_HISTORY_PREFIX = "cache:history:"
|
|
11
|
-
|
|
12
|
-
/** 只保留最近 10 次请求。 */
|
|
13
|
-
export const CACHE_HISTORY_LIMIT = 10
|
|
14
|
-
|
|
15
|
-
/** 一条缓存命中样本;hitRate 是 0..100,向下截断到两位小数。 */
|
|
16
|
-
export interface CacheSample {
|
|
17
|
-
readonly messageID: string
|
|
18
|
-
readonly at: number
|
|
19
|
-
readonly input: number
|
|
20
|
-
readonly output: number
|
|
21
|
-
readonly cached: number
|
|
22
|
-
readonly hitRate: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** storage 里的持久化结构。 */
|
|
26
|
-
export interface CacheHistory {
|
|
27
|
-
readonly v: 1
|
|
28
|
-
readonly samples: readonly CacheSample[]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const EMPTY_CACHE_HISTORY: CacheHistory = { v: 1, samples: [] }
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 输入:总输入 token + 命中 token。
|
|
35
|
-
* 输出:0..100 的百分比,向下截断到两位小数。
|
|
36
|
-
* 预期:不四舍五入;99.999% 显示 99.99,不会变成 100.00。
|
|
37
|
-
*/
|
|
38
|
-
export function hitRatePercent(input: number, cached: number): number {
|
|
39
|
-
if (input <= 0) return 0
|
|
40
|
-
return Math.floor(Math.min(1, cached / input) * 10_000) / 100
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** 输入:命中率;输出:固定两位小数的百分比文本(不含 `%`)。 */
|
|
44
|
-
export function formatHitRate(hitRate: number): string {
|
|
45
|
-
return hitRate.toFixed(2)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** 输入:sessionID;输出:该会话的 storage key。 */
|
|
49
|
-
export function cacheHistoryKey(sessionID: string): string {
|
|
50
|
-
return CACHE_HISTORY_PREFIX + sessionID
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 输入:消息(结构最小化,便于测试替身)。
|
|
55
|
-
* 输出:assistant 且带 tokens 时返回样本,否则 undefined。
|
|
56
|
-
*/
|
|
57
|
-
export interface CacheMessage {
|
|
58
|
-
readonly id: string
|
|
59
|
-
readonly type: string
|
|
60
|
-
readonly time?: { readonly created?: number }
|
|
61
|
-
readonly tokens?: {
|
|
62
|
-
readonly input: number
|
|
63
|
-
readonly output: number
|
|
64
|
-
readonly reasoning: number
|
|
65
|
-
readonly cache: { readonly read: number; readonly write: number }
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 输入:消息 + 缺时间时的回退值;输出:样本或 undefined。 */
|
|
70
|
-
export function sampleFromMessage(message: CacheMessage, fallbackAt: number): CacheSample | undefined {
|
|
71
|
-
if (message.type !== "assistant") return undefined
|
|
72
|
-
const tokens = message.tokens
|
|
73
|
-
if (tokens === undefined) return undefined
|
|
74
|
-
const input = tokens.input + tokens.cache.read + tokens.cache.write
|
|
75
|
-
const output = tokens.output + tokens.reasoning
|
|
76
|
-
const cached = tokens.cache.read
|
|
77
|
-
return {
|
|
78
|
-
messageID: message.id,
|
|
79
|
-
at: message.time?.created ?? fallbackAt,
|
|
80
|
-
input,
|
|
81
|
-
output,
|
|
82
|
-
cached,
|
|
83
|
-
hitRate: hitRatePercent(input, cached),
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** 输入:样本列表;输出:按时间升序、去重、只留最新 10 条。 */
|
|
88
|
-
function normalizeSamples(samples: readonly CacheSample[]): readonly CacheSample[] {
|
|
89
|
-
const unique = new Map<string, CacheSample>()
|
|
90
|
-
for (const sample of samples) {
|
|
91
|
-
// 同一条消息重复出现时保留第一次记录,避免重试把同一条刷成多条。
|
|
92
|
-
if (!unique.has(sample.messageID)) unique.set(sample.messageID, sample)
|
|
93
|
-
}
|
|
94
|
-
return [...unique.values()]
|
|
95
|
-
.sort((left, right) => left.at - right.at || left.messageID.localeCompare(right.messageID))
|
|
96
|
-
.slice(-CACHE_HISTORY_LIMIT)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 输入:已有历史 + 当前可见消息。
|
|
101
|
-
* 输出:追加未记录过的 assistant 样本后的新历史。
|
|
102
|
-
* 预期:按 messageID 去重;按时间排序;只保留最新 10 条。
|
|
103
|
-
*/
|
|
104
|
-
export function appendCacheSamples(
|
|
105
|
-
existing: readonly CacheSample[],
|
|
106
|
-
messages: readonly CacheMessage[],
|
|
107
|
-
now: number = Date.now(),
|
|
108
|
-
): readonly CacheSample[] {
|
|
109
|
-
const known = new Set(existing.map((sample) => sample.messageID))
|
|
110
|
-
const added: CacheSample[] = []
|
|
111
|
-
for (const message of messages) {
|
|
112
|
-
if (known.has(message.id)) continue
|
|
113
|
-
const sample = sampleFromMessage(message, now)
|
|
114
|
-
if (sample === undefined) continue
|
|
115
|
-
known.add(sample.messageID)
|
|
116
|
-
added.push(sample)
|
|
117
|
-
}
|
|
118
|
-
return normalizeSamples([...existing, ...added])
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* 输入:已有历史 + 单个 step 的 assistant usage。
|
|
123
|
-
* 输出:追加该 step 样本后的新历史。
|
|
124
|
-
* 预期:tool loop 里 context/usage.updated 拿不到最新 step usage,
|
|
125
|
-
* `session.step.ended` 事件直接携带 tokens,这里按 messageID 去重落一条。
|
|
126
|
-
*/
|
|
127
|
-
export function appendCacheSampleFromStep(
|
|
128
|
-
existing: readonly CacheSample[],
|
|
129
|
-
messageID: string,
|
|
130
|
-
tokens: {
|
|
131
|
-
readonly input: number
|
|
132
|
-
readonly output: number
|
|
133
|
-
readonly reasoning: number
|
|
134
|
-
readonly cache: { readonly read: number; readonly write: number }
|
|
135
|
-
},
|
|
136
|
-
fallbackAt: number = Date.now(),
|
|
137
|
-
): readonly CacheSample[] {
|
|
138
|
-
return appendCacheSamples(existing, [{ id: messageID, type: "assistant", tokens }], fallbackAt)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** 输入:任意 storage 值;输出:合法历史;损坏时返回空历史,绝不抛错。hitRate 按 input/cached 重算。 */
|
|
142
|
-
export function parseCacheHistory(value: unknown): CacheHistory {
|
|
143
|
-
if (typeof value !== "object" || value === null) return EMPTY_CACHE_HISTORY
|
|
144
|
-
const record = value as Record<string, unknown>
|
|
145
|
-
if (record["v"] !== 1 || !Array.isArray(record["samples"])) return EMPTY_CACHE_HISTORY
|
|
146
|
-
const samples: CacheSample[] = []
|
|
147
|
-
for (const item of record["samples"]) {
|
|
148
|
-
if (typeof item !== "object" || item === null) continue
|
|
149
|
-
const sample = item as Record<string, unknown>
|
|
150
|
-
const messageID = sample["messageID"]
|
|
151
|
-
const at = sample["at"]
|
|
152
|
-
const input = sample["input"]
|
|
153
|
-
const output = sample["output"]
|
|
154
|
-
const cached = sample["cached"]
|
|
155
|
-
if (typeof messageID !== "string" || messageID.length === 0) continue
|
|
156
|
-
if (
|
|
157
|
-
typeof at !== "number" ||
|
|
158
|
-
!Number.isFinite(at) ||
|
|
159
|
-
at < 0 ||
|
|
160
|
-
typeof input !== "number" ||
|
|
161
|
-
!Number.isFinite(input) ||
|
|
162
|
-
input < 0 ||
|
|
163
|
-
typeof output !== "number" ||
|
|
164
|
-
!Number.isFinite(output) ||
|
|
165
|
-
output < 0 ||
|
|
166
|
-
typeof cached !== "number" ||
|
|
167
|
-
!Number.isFinite(cached) ||
|
|
168
|
-
cached < 0
|
|
169
|
-
) {
|
|
170
|
-
continue
|
|
171
|
-
}
|
|
172
|
-
samples.push({
|
|
173
|
-
messageID,
|
|
174
|
-
at,
|
|
175
|
-
input,
|
|
176
|
-
output,
|
|
177
|
-
cached,
|
|
178
|
-
hitRate: hitRatePercent(input, cached),
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
return { v: 1, samples: normalizeSamples(samples) }
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** 输入:两条历史;输出:样本是否完全一致;用于避免无变化的重复写盘。 */
|
|
185
|
-
export function cacheHistoryEquals(left: readonly CacheSample[], right: readonly CacheSample[]): boolean {
|
|
186
|
-
if (left.length !== right.length) return false
|
|
187
|
-
return left.every((sample, index) => {
|
|
188
|
-
const other = right[index]
|
|
189
|
-
if (other === undefined) return false
|
|
190
|
-
return (
|
|
191
|
-
sample.messageID === other.messageID &&
|
|
192
|
-
sample.at === other.at &&
|
|
193
|
-
sample.input === other.input &&
|
|
194
|
-
sample.output === other.output &&
|
|
195
|
-
sample.cached === other.cached &&
|
|
196
|
-
sample.hitRate === other.hitRate
|
|
197
|
-
)
|
|
198
|
-
})
|
|
199
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 缓存命中历史的跨进程载体。
|
|
3
|
+
*
|
|
4
|
+
* 服务端插件在 session.idle 时把最近 10 条 assistant 消息的 usage 追加到
|
|
5
|
+
* storage(`cache:history:<sessionID>`);TUI 通过 RPC 读取。
|
|
6
|
+
* 本模块只放纯函数:采样、追加、校验、排序,方便单测。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 每个会话一份,key 里带 sessionID,父/子会话互不影响。 */
|
|
10
|
+
export const CACHE_HISTORY_PREFIX = "cache:history:"
|
|
11
|
+
|
|
12
|
+
/** 只保留最近 10 次请求。 */
|
|
13
|
+
export const CACHE_HISTORY_LIMIT = 10
|
|
14
|
+
|
|
15
|
+
/** 一条缓存命中样本;hitRate 是 0..100,向下截断到两位小数。 */
|
|
16
|
+
export interface CacheSample {
|
|
17
|
+
readonly messageID: string
|
|
18
|
+
readonly at: number
|
|
19
|
+
readonly input: number
|
|
20
|
+
readonly output: number
|
|
21
|
+
readonly cached: number
|
|
22
|
+
readonly hitRate: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** storage 里的持久化结构。 */
|
|
26
|
+
export interface CacheHistory {
|
|
27
|
+
readonly v: 1
|
|
28
|
+
readonly samples: readonly CacheSample[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const EMPTY_CACHE_HISTORY: CacheHistory = { v: 1, samples: [] }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 输入:总输入 token + 命中 token。
|
|
35
|
+
* 输出:0..100 的百分比,向下截断到两位小数。
|
|
36
|
+
* 预期:不四舍五入;99.999% 显示 99.99,不会变成 100.00。
|
|
37
|
+
*/
|
|
38
|
+
export function hitRatePercent(input: number, cached: number): number {
|
|
39
|
+
if (input <= 0) return 0
|
|
40
|
+
return Math.floor(Math.min(1, cached / input) * 10_000) / 100
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 输入:命中率;输出:固定两位小数的百分比文本(不含 `%`)。 */
|
|
44
|
+
export function formatHitRate(hitRate: number): string {
|
|
45
|
+
return hitRate.toFixed(2)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 输入:sessionID;输出:该会话的 storage key。 */
|
|
49
|
+
export function cacheHistoryKey(sessionID: string): string {
|
|
50
|
+
return CACHE_HISTORY_PREFIX + sessionID
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 输入:消息(结构最小化,便于测试替身)。
|
|
55
|
+
* 输出:assistant 且带 tokens 时返回样本,否则 undefined。
|
|
56
|
+
*/
|
|
57
|
+
export interface CacheMessage {
|
|
58
|
+
readonly id: string
|
|
59
|
+
readonly type: string
|
|
60
|
+
readonly time?: { readonly created?: number }
|
|
61
|
+
readonly tokens?: {
|
|
62
|
+
readonly input: number
|
|
63
|
+
readonly output: number
|
|
64
|
+
readonly reasoning: number
|
|
65
|
+
readonly cache: { readonly read: number; readonly write: number }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 输入:消息 + 缺时间时的回退值;输出:样本或 undefined。 */
|
|
70
|
+
export function sampleFromMessage(message: CacheMessage, fallbackAt: number): CacheSample | undefined {
|
|
71
|
+
if (message.type !== "assistant") return undefined
|
|
72
|
+
const tokens = message.tokens
|
|
73
|
+
if (tokens === undefined) return undefined
|
|
74
|
+
const input = tokens.input + tokens.cache.read + tokens.cache.write
|
|
75
|
+
const output = tokens.output + tokens.reasoning
|
|
76
|
+
const cached = tokens.cache.read
|
|
77
|
+
return {
|
|
78
|
+
messageID: message.id,
|
|
79
|
+
at: message.time?.created ?? fallbackAt,
|
|
80
|
+
input,
|
|
81
|
+
output,
|
|
82
|
+
cached,
|
|
83
|
+
hitRate: hitRatePercent(input, cached),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 输入:样本列表;输出:按时间升序、去重、只留最新 10 条。 */
|
|
88
|
+
function normalizeSamples(samples: readonly CacheSample[]): readonly CacheSample[] {
|
|
89
|
+
const unique = new Map<string, CacheSample>()
|
|
90
|
+
for (const sample of samples) {
|
|
91
|
+
// 同一条消息重复出现时保留第一次记录,避免重试把同一条刷成多条。
|
|
92
|
+
if (!unique.has(sample.messageID)) unique.set(sample.messageID, sample)
|
|
93
|
+
}
|
|
94
|
+
return [...unique.values()]
|
|
95
|
+
.sort((left, right) => left.at - right.at || left.messageID.localeCompare(right.messageID))
|
|
96
|
+
.slice(-CACHE_HISTORY_LIMIT)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 输入:已有历史 + 当前可见消息。
|
|
101
|
+
* 输出:追加未记录过的 assistant 样本后的新历史。
|
|
102
|
+
* 预期:按 messageID 去重;按时间排序;只保留最新 10 条。
|
|
103
|
+
*/
|
|
104
|
+
export function appendCacheSamples(
|
|
105
|
+
existing: readonly CacheSample[],
|
|
106
|
+
messages: readonly CacheMessage[],
|
|
107
|
+
now: number = Date.now(),
|
|
108
|
+
): readonly CacheSample[] {
|
|
109
|
+
const known = new Set(existing.map((sample) => sample.messageID))
|
|
110
|
+
const added: CacheSample[] = []
|
|
111
|
+
for (const message of messages) {
|
|
112
|
+
if (known.has(message.id)) continue
|
|
113
|
+
const sample = sampleFromMessage(message, now)
|
|
114
|
+
if (sample === undefined) continue
|
|
115
|
+
known.add(sample.messageID)
|
|
116
|
+
added.push(sample)
|
|
117
|
+
}
|
|
118
|
+
return normalizeSamples([...existing, ...added])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 输入:已有历史 + 单个 step 的 assistant usage。
|
|
123
|
+
* 输出:追加该 step 样本后的新历史。
|
|
124
|
+
* 预期:tool loop 里 context/usage.updated 拿不到最新 step usage,
|
|
125
|
+
* `session.step.ended` 事件直接携带 tokens,这里按 messageID 去重落一条。
|
|
126
|
+
*/
|
|
127
|
+
export function appendCacheSampleFromStep(
|
|
128
|
+
existing: readonly CacheSample[],
|
|
129
|
+
messageID: string,
|
|
130
|
+
tokens: {
|
|
131
|
+
readonly input: number
|
|
132
|
+
readonly output: number
|
|
133
|
+
readonly reasoning: number
|
|
134
|
+
readonly cache: { readonly read: number; readonly write: number }
|
|
135
|
+
},
|
|
136
|
+
fallbackAt: number = Date.now(),
|
|
137
|
+
): readonly CacheSample[] {
|
|
138
|
+
return appendCacheSamples(existing, [{ id: messageID, type: "assistant", tokens }], fallbackAt)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 输入:任意 storage 值;输出:合法历史;损坏时返回空历史,绝不抛错。hitRate 按 input/cached 重算。 */
|
|
142
|
+
export function parseCacheHistory(value: unknown): CacheHistory {
|
|
143
|
+
if (typeof value !== "object" || value === null) return EMPTY_CACHE_HISTORY
|
|
144
|
+
const record = value as Record<string, unknown>
|
|
145
|
+
if (record["v"] !== 1 || !Array.isArray(record["samples"])) return EMPTY_CACHE_HISTORY
|
|
146
|
+
const samples: CacheSample[] = []
|
|
147
|
+
for (const item of record["samples"]) {
|
|
148
|
+
if (typeof item !== "object" || item === null) continue
|
|
149
|
+
const sample = item as Record<string, unknown>
|
|
150
|
+
const messageID = sample["messageID"]
|
|
151
|
+
const at = sample["at"]
|
|
152
|
+
const input = sample["input"]
|
|
153
|
+
const output = sample["output"]
|
|
154
|
+
const cached = sample["cached"]
|
|
155
|
+
if (typeof messageID !== "string" || messageID.length === 0) continue
|
|
156
|
+
if (
|
|
157
|
+
typeof at !== "number" ||
|
|
158
|
+
!Number.isFinite(at) ||
|
|
159
|
+
at < 0 ||
|
|
160
|
+
typeof input !== "number" ||
|
|
161
|
+
!Number.isFinite(input) ||
|
|
162
|
+
input < 0 ||
|
|
163
|
+
typeof output !== "number" ||
|
|
164
|
+
!Number.isFinite(output) ||
|
|
165
|
+
output < 0 ||
|
|
166
|
+
typeof cached !== "number" ||
|
|
167
|
+
!Number.isFinite(cached) ||
|
|
168
|
+
cached < 0
|
|
169
|
+
) {
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
samples.push({
|
|
173
|
+
messageID,
|
|
174
|
+
at,
|
|
175
|
+
input,
|
|
176
|
+
output,
|
|
177
|
+
cached,
|
|
178
|
+
hitRate: hitRatePercent(input, cached),
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
return { v: 1, samples: normalizeSamples(samples) }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 输入:两条历史;输出:样本是否完全一致;用于避免无变化的重复写盘。 */
|
|
185
|
+
export function cacheHistoryEquals(left: readonly CacheSample[], right: readonly CacheSample[]): boolean {
|
|
186
|
+
if (left.length !== right.length) return false
|
|
187
|
+
return left.every((sample, index) => {
|
|
188
|
+
const other = right[index]
|
|
189
|
+
if (other === undefined) return false
|
|
190
|
+
return (
|
|
191
|
+
sample.messageID === other.messageID &&
|
|
192
|
+
sample.at === other.at &&
|
|
193
|
+
sample.input === other.input &&
|
|
194
|
+
sample.output === other.output &&
|
|
195
|
+
sample.cached === other.cached &&
|
|
196
|
+
sample.hitRate === other.hitRate
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
}
|
package/src/shared/format.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 面板数字格式化。
|
|
3
|
-
*
|
|
4
|
-
* 输入:token 数量(非负整数)。
|
|
5
|
-
* 输出:紧凑字符串:<1000 原样、<10000 一位小数 k、<1e6 取整 k、
|
|
6
|
-
* <1e7 一位小数 M、更大取整 M。
|
|
7
|
-
* 预期:侧栏一行内放得下。
|
|
8
|
-
*/
|
|
9
|
-
export function formatTokens(count: number): string {
|
|
10
|
-
if (!Number.isFinite(count) || count <= 0) return "0"
|
|
11
|
-
if (count < 1_000) return count.toString()
|
|
12
|
-
if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`
|
|
13
|
-
if (count < 1_000_000) return `${Math.round(count / 1_000).toString()}k`
|
|
14
|
-
if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`
|
|
15
|
-
return `${Math.round(count / 1_000_000).toString()}M`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** 输入:毫秒时间戳;输出:本地时间 `YYYY/MM/DD HH:mm`;预期:固定格式,不依赖 locale。 */
|
|
19
|
-
export function formatTimestamp(at: number): string {
|
|
20
|
-
const date = new Date(at)
|
|
21
|
-
const pad = (value: number): string => value.toString().padStart(2, "0")
|
|
22
|
-
return `${date.getFullYear().toString()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
23
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 面板数字格式化。
|
|
3
|
+
*
|
|
4
|
+
* 输入:token 数量(非负整数)。
|
|
5
|
+
* 输出:紧凑字符串:<1000 原样、<10000 一位小数 k、<1e6 取整 k、
|
|
6
|
+
* <1e7 一位小数 M、更大取整 M。
|
|
7
|
+
* 预期:侧栏一行内放得下。
|
|
8
|
+
*/
|
|
9
|
+
export function formatTokens(count: number): string {
|
|
10
|
+
if (!Number.isFinite(count) || count <= 0) return "0"
|
|
11
|
+
if (count < 1_000) return count.toString()
|
|
12
|
+
if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`
|
|
13
|
+
if (count < 1_000_000) return `${Math.round(count / 1_000).toString()}k`
|
|
14
|
+
if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`
|
|
15
|
+
return `${Math.round(count / 1_000_000).toString()}M`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 输入:毫秒时间戳;输出:本地时间 `YYYY/MM/DD HH:mm`;预期:固定格式,不依赖 locale。 */
|
|
19
|
+
export function formatTimestamp(at: number): string {
|
|
20
|
+
const date = new Date(at)
|
|
21
|
+
const pad = (value: number): string => value.toString().padStart(2, "0")
|
|
22
|
+
return `${date.getFullYear().toString()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
23
|
+
}
|