@qthinker/opencode-deepseek-search 0.2.0 → 0.3.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.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -21
- package/README.md +26 -10
- package/gui-experimental.cjs +1 -1
- package/package.json +88 -87
- package/src/deepseek.ts +65 -47
- package/src/gui/index.tsx +14 -7
- package/src/gui/panel.tsx +13 -8
- package/src/i18n/en.ts +151 -0
- package/src/i18n/index.ts +87 -0
- package/src/i18n/ja.ts +138 -0
- package/src/i18n/zh.ts +133 -0
- package/src/index.ts +58 -41
- package/src/rpc.ts +34 -2
- package/src/status-view.ts +59 -51
- package/src/tui/card.ts +4 -1
- package/src/tui/main.ts +16 -8
- package/src/usage.ts +32 -9
package/src/status-view.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
// Pure view model for the sidebar card. No renderer, no runtime dependency:
|
|
2
2
|
// the card and the tests both derive their text from here.
|
|
3
|
-
import
|
|
3
|
+
import { format, pack, t } from "./i18n/index.ts"
|
|
4
|
+
import type { SearchStatus, UsageCounts, Language } from "./rpc.ts"
|
|
4
5
|
|
|
5
6
|
export interface SidebarInput {
|
|
6
7
|
readonly status: SearchStatus | undefined
|
|
7
|
-
|
|
8
|
+
/** The last read failure, if any; undefined while the first read is in flight. */
|
|
9
|
+
readonly error?: string | undefined
|
|
8
10
|
/**
|
|
9
11
|
* Columns one row may occupy. Defaults to the narrow sidebar budget; a host
|
|
10
12
|
* that has measured its width may pass it, and the desktop panel passes
|
|
11
13
|
* `Infinity` because it has room for the verbose copy.
|
|
12
14
|
*/
|
|
13
15
|
readonly maxColumns?: number
|
|
16
|
+
/** Resolved by the server and carried in `status`; both sidebars read it there. */
|
|
17
|
+
readonly language: Language
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export interface SidebarRows {
|
|
@@ -58,7 +62,11 @@ export const DEFAULT_COLUMNS = 24
|
|
|
58
62
|
|
|
59
63
|
const PERIODS = ["today", "month", "total"] as const
|
|
60
64
|
type Period = (typeof PERIODS)[number]
|
|
61
|
-
const
|
|
65
|
+
const LABEL_KEY = {
|
|
66
|
+
today: "usage.label.today",
|
|
67
|
+
month: "usage.label.month",
|
|
68
|
+
total: "usage.label.total",
|
|
69
|
+
} as const
|
|
62
70
|
|
|
63
71
|
type Counts = NonNullable<SearchStatus["counts"]>
|
|
64
72
|
|
|
@@ -106,9 +114,9 @@ function clip(text: string, maxColumns: number): string {
|
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
/** One short body line: collapse whitespace, then clip to the available columns. */
|
|
109
|
-
function shortMessage(message: string, maxColumns: number): string {
|
|
117
|
+
function shortMessage(message: string, maxColumns: number, language: Language): string {
|
|
110
118
|
const text = message.replace(/\s+/g, " ").trim()
|
|
111
|
-
return clip(text || "
|
|
119
|
+
return clip(text || t(language, "state.failed"), maxColumns)
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
/** Micro-currency as `¥0.015`: three decimals, trailing zeros trimmed to cents. */
|
|
@@ -142,46 +150,41 @@ function usageLayout(counts: Counts): UsageLayout {
|
|
|
142
150
|
|
|
143
151
|
/**
|
|
144
152
|
* One usage row, e.g. `今日 Search 0 Fetch 3 ¥0.015` when the surface is
|
|
145
|
-
* wide, otherwise shorter forms down to `今日`. The
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* the three rows read as a table.
|
|
153
|
+
* wide, otherwise shorter forms down to `今日`. The ladder is per language:
|
|
154
|
+
* each locale owns the shapes that read well at its widths, so no language is
|
|
155
|
+
* squeezed into another one's budget. Counts are right-aligned in their shared
|
|
156
|
+
* width and every row picks the same form, so the three rows read as a table.
|
|
150
157
|
*/
|
|
151
158
|
function compactUsageRow(
|
|
152
159
|
period: Period,
|
|
153
160
|
counts: UsageCounts,
|
|
154
161
|
layout: UsageLayout,
|
|
155
162
|
maxColumns: number,
|
|
163
|
+
language: Language,
|
|
156
164
|
): string {
|
|
157
|
-
const label =
|
|
165
|
+
const label = t(language, LABEL_KEY[period])
|
|
158
166
|
const search = String(counts.search).padStart(layout.search, " ")
|
|
159
167
|
const fetch = String(counts.fetch).padStart(layout.fetch, " ")
|
|
160
168
|
const cost = money(counts.costMicro).padStart(layout.cost, " ")
|
|
169
|
+
const params = { label, search, fetch, cost }
|
|
161
170
|
// Each candidate is a complete row, so nothing is ever left dangling and no
|
|
162
171
|
// number is ever cut from its unit.
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
`${label} Search ${search} Fetch ${fetch} ${cost}`,
|
|
166
|
-
`${label} Search ${search} Fetch ${fetch}`,
|
|
167
|
-
`${label} Search ${search} Fetch ${fetch}`,
|
|
168
|
-
`${label} ${search}S ${fetch}F ${cost}`,
|
|
169
|
-
`${label} ${search}S ${fetch}F`,
|
|
170
|
-
`${label} ${search}S`,
|
|
171
|
-
]
|
|
172
|
-
for (const row of candidates) {
|
|
172
|
+
for (const form of pack(language).usageForms) {
|
|
173
|
+
const row = format(form, params)
|
|
173
174
|
if (textColumns(row) <= maxColumns) return row
|
|
174
175
|
}
|
|
175
176
|
return textColumns(label) <= maxColumns ? label : clip(label, maxColumns)
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
/**
|
|
179
|
-
* An older server only sends pre-formatted strings
|
|
180
|
-
*
|
|
181
|
-
*
|
|
180
|
+
* An older server only sends pre-formatted strings, and those are always the
|
|
181
|
+
* Chinese ones this plugin used to send: the filter below is deliberately
|
|
182
|
+
* Chinese-only and stays correct while the UI language changes. Keep the full
|
|
183
|
+
* string when it fits, otherwise drop its trailing segments whole: the amount
|
|
184
|
+
* and the diagnostic counters go first, then the fetch count.
|
|
182
185
|
*/
|
|
183
|
-
function legacyUsageRow(period: Period, text: string, maxColumns: number): string {
|
|
184
|
-
const label =
|
|
186
|
+
function legacyUsageRow(period: Period, text: string, maxColumns: number, language: Language): string {
|
|
187
|
+
const label = t(language, LABEL_KEY[period])
|
|
185
188
|
const full = `${label} ${text}`
|
|
186
189
|
if (textColumns(full) <= maxColumns) return full
|
|
187
190
|
const stats = text.split(" · ").filter((part) => !part.startsWith("¥") && !part.startsWith("空/失"))
|
|
@@ -196,18 +199,22 @@ function legacyUsageRow(period: Period, text: string, maxColumns: number): strin
|
|
|
196
199
|
return clip(label, maxColumns)
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
function usageLines(status: SearchStatus, maxColumns: number): readonly string[] {
|
|
202
|
+
function usageLines(status: SearchStatus, maxColumns: number, language: Language): readonly string[] {
|
|
200
203
|
// The desktop panel lays the original verbose copy out as ` · ` chunks of its
|
|
201
204
|
// own table; only the terminal needs the compact form.
|
|
202
205
|
if (maxColumns === Number.POSITIVE_INFINITY) {
|
|
203
|
-
return [
|
|
206
|
+
return [
|
|
207
|
+
`${t(language, "usage.label.today")} ${status.today}`,
|
|
208
|
+
`${t(language, "usage.label.month")} ${status.month}`,
|
|
209
|
+
`${t(language, "usage.label.total")} ${status.total}`,
|
|
210
|
+
]
|
|
204
211
|
}
|
|
205
212
|
const counts = status.counts
|
|
206
213
|
if (counts) {
|
|
207
214
|
const layout = usageLayout(counts)
|
|
208
|
-
return PERIODS.map((period) => compactUsageRow(period, counts[period], layout, maxColumns))
|
|
215
|
+
return PERIODS.map((period) => compactUsageRow(period, counts[period], layout, maxColumns, language))
|
|
209
216
|
}
|
|
210
|
-
return PERIODS.map((period) => legacyUsageRow(period, status[period], maxColumns))
|
|
217
|
+
return PERIODS.map((period) => legacyUsageRow(period, status[period], maxColumns, language))
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
/**
|
|
@@ -275,18 +282,13 @@ function fitTitle(
|
|
|
275
282
|
}
|
|
276
283
|
|
|
277
284
|
/** The fix hint for a missing key; it gets its own row because no click can fix it. */
|
|
278
|
-
function fitHint(maxColumns: number): string {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"设置 KEY",
|
|
284
|
-
]
|
|
285
|
-
if (maxColumns === Number.POSITIVE_INFINITY) return candidates[0]
|
|
286
|
-
for (const candidate of candidates) {
|
|
287
|
-
if (textColumns(candidate) <= maxColumns) return candidate
|
|
285
|
+
function fitHint(maxColumns: number, language: Language): string {
|
|
286
|
+
const forms = pack(language).hintForms
|
|
287
|
+
if (maxColumns === Number.POSITIVE_INFINITY) return forms[0] ?? ""
|
|
288
|
+
for (const form of forms) {
|
|
289
|
+
if (textColumns(form) <= maxColumns) return form
|
|
288
290
|
}
|
|
289
|
-
return clip(
|
|
291
|
+
return clip(forms[forms.length - 1] ?? "", maxColumns)
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
interface ActionRows extends TitleFit {
|
|
@@ -300,33 +302,34 @@ function actionRows(state: string, shortState: string, verb: string, maxColumns:
|
|
|
300
302
|
}
|
|
301
303
|
|
|
302
304
|
export function sidebarRows(input: SidebarInput): SidebarRows {
|
|
303
|
-
const { status, error } = input
|
|
305
|
+
const { status, error, language } = input
|
|
304
306
|
const maxColumns = input.maxColumns ?? DEFAULT_COLUMNS
|
|
305
|
-
const lines = status === undefined ? [] : usageLines(status, maxColumns)
|
|
307
|
+
const lines = status === undefined ? [] : usageLines(status, maxColumns, language)
|
|
306
308
|
|
|
307
309
|
// No status yet: the body has no numbers to show, only the reason (or that
|
|
308
310
|
// the first read is still in flight).
|
|
309
311
|
if (status === undefined) {
|
|
312
|
+
const retry = t(language, "action.retry")
|
|
310
313
|
return error === undefined || error.trim() === ""
|
|
311
314
|
? {
|
|
312
|
-
...actionRows(
|
|
315
|
+
...actionRows(t(language, "state.loading"), t(language, "state.loadingShort"), retry, maxColumns),
|
|
313
316
|
hint: undefined,
|
|
314
317
|
clickable: true,
|
|
315
318
|
lines,
|
|
316
319
|
tone: "idle",
|
|
317
320
|
}
|
|
318
321
|
: {
|
|
319
|
-
...actionRows(
|
|
322
|
+
...actionRows(t(language, "state.failed"), t(language, "state.failedShort"), retry, maxColumns),
|
|
320
323
|
hint: undefined,
|
|
321
324
|
clickable: true,
|
|
322
|
-
lines: [shortMessage(error, maxColumns)],
|
|
325
|
+
lines: [shortMessage(error, maxColumns, language)],
|
|
323
326
|
tone: "error",
|
|
324
327
|
}
|
|
325
328
|
}
|
|
326
329
|
|
|
327
330
|
if (status.patched && status.hasKey) {
|
|
328
331
|
return {
|
|
329
|
-
...actionRows("
|
|
332
|
+
...actionRows(t(language, "state.enabled"), t(language, "state.enabledShort"), t(language, "action.disable"), maxColumns),
|
|
330
333
|
hint: undefined,
|
|
331
334
|
clickable: true,
|
|
332
335
|
lines,
|
|
@@ -337,7 +340,12 @@ export function sidebarRows(input: SidebarInput): SidebarRows {
|
|
|
337
340
|
// DeepSeek was requested but the built-in tool was not found: nothing is
|
|
338
341
|
// actually taken over, so the only honest action is to release the mode.
|
|
339
342
|
return {
|
|
340
|
-
...actionRows(
|
|
343
|
+
...actionRows(
|
|
344
|
+
t(language, "state.unpatched"),
|
|
345
|
+
t(language, "state.unpatchedShort"),
|
|
346
|
+
t(language, "action.release"),
|
|
347
|
+
maxColumns,
|
|
348
|
+
),
|
|
341
349
|
hint: undefined,
|
|
342
350
|
clickable: true,
|
|
343
351
|
lines,
|
|
@@ -346,7 +354,7 @@ export function sidebarRows(input: SidebarInput): SidebarRows {
|
|
|
346
354
|
}
|
|
347
355
|
if (status.mode === "native") {
|
|
348
356
|
return {
|
|
349
|
-
...actionRows("
|
|
357
|
+
...actionRows(t(language, "state.disabled"), t(language, "state.disabledShort"), t(language, "action.enable"), maxColumns),
|
|
350
358
|
hint: undefined,
|
|
351
359
|
clickable: true,
|
|
352
360
|
lines,
|
|
@@ -356,9 +364,9 @@ export function sidebarRows(input: SidebarInput): SidebarRows {
|
|
|
356
364
|
// A missing key is not something a click can fix, so the title row stands
|
|
357
365
|
// alone and the hint gets its own row directly under it. The env var name
|
|
358
366
|
// only survives on surfaces wide enough for it.
|
|
359
|
-
const hint = fitHint(maxColumns)
|
|
367
|
+
const hint = fitHint(maxColumns, language)
|
|
360
368
|
return {
|
|
361
|
-
...fitTitle(
|
|
369
|
+
...fitTitle(t(language, "state.missingKey"), t(language, "state.missingKeyShort"), maxColumns, undefined),
|
|
362
370
|
action: hint,
|
|
363
371
|
hint,
|
|
364
372
|
clickable: false,
|
package/src/tui/card.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { JSX } from "@opentui/solid"
|
|
4
4
|
import type { ResolvedTheme } from "@opencode/theme/tui"
|
|
5
5
|
import { createElement, insert, setProp } from "@opentui/solid"
|
|
6
|
-
import type { SearchStatus } from "../rpc.ts"
|
|
6
|
+
import type { Language, SearchStatus } from "../rpc.ts"
|
|
7
7
|
import { DEFAULT_COLUMNS, sidebarRows } from "../status-view.ts"
|
|
8
8
|
import type { SidebarRows } from "../status-view.ts"
|
|
9
9
|
|
|
@@ -72,6 +72,8 @@ function element(tag: string, props: Props | undefined, children: readonly Child
|
|
|
72
72
|
export function sidebarCard(input: {
|
|
73
73
|
readonly status: SearchStatus | undefined
|
|
74
74
|
readonly error: string | undefined
|
|
75
|
+
/** UI language resolved by the server; the card never picks one itself. */
|
|
76
|
+
readonly language: Language
|
|
75
77
|
readonly theme: ResolvedTheme
|
|
76
78
|
readonly onToggle: () => void
|
|
77
79
|
/**
|
|
@@ -87,6 +89,7 @@ export function sidebarCard(input: {
|
|
|
87
89
|
status: input.status,
|
|
88
90
|
error: input.error,
|
|
89
91
|
maxColumns: cardColumns(input.maxColumns),
|
|
92
|
+
language: input.language,
|
|
90
93
|
})
|
|
91
94
|
const theme = input.theme
|
|
92
95
|
const state = toneColor(theme, rows.tone)
|
package/src/tui/main.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { Plugin } from "@opencode/plugin/tui"
|
|
|
9
9
|
import { createElement, insert, setProp, type JSX } from "@opentui/solid"
|
|
10
10
|
import type { Renderable } from "@opentui/core"
|
|
11
11
|
import { createSignal } from "solid-js"
|
|
12
|
-
import { DeepSeekRpc, readModeResult, readStatus, type SearchStatus } from "../rpc.ts"
|
|
12
|
+
import { DeepSeekRpc, readModeResult, readStatus, type Language, type SearchStatus } from "../rpc.ts"
|
|
13
|
+
import { t } from "../i18n/index.ts"
|
|
13
14
|
import { sidebarCard } from "./card.ts"
|
|
14
15
|
|
|
15
16
|
const REFRESH_MS = 10_000
|
|
@@ -25,6 +26,12 @@ export default Plugin.define({
|
|
|
25
26
|
|
|
26
27
|
const describeError = (e: unknown) => (e instanceof Error ? e.message : String(e)).slice(0, ERROR_MAX)
|
|
27
28
|
|
|
29
|
+
// One language for every string this half renders. Before the first status
|
|
30
|
+
// arrives there is nothing to read it from, so the first frame is English;
|
|
31
|
+
// the reactive card and the host-re-run keymap factory pick up the real one
|
|
32
|
+
// as soon as the server reports it.
|
|
33
|
+
const language = (): Language => status()?.language ?? "en"
|
|
34
|
+
|
|
28
35
|
// `status` takes a required first argument: plain JSON Schema degrades the
|
|
29
36
|
// input type to `unknown`, and the host validates the payload against the
|
|
30
37
|
// method's object schema, so an omitted input is rejected as invalid.
|
|
@@ -35,7 +42,7 @@ export default Plugin.define({
|
|
|
35
42
|
setStatus(next)
|
|
36
43
|
setError(undefined)
|
|
37
44
|
} else {
|
|
38
|
-
setError("
|
|
45
|
+
setError(t(language(), "toast.statusMalformed"))
|
|
39
46
|
}
|
|
40
47
|
} catch (e) {
|
|
41
48
|
setError(describeError(e))
|
|
@@ -46,7 +53,7 @@ export default Plugin.define({
|
|
|
46
53
|
async function applyMode(mode: "deepseek" | "native") {
|
|
47
54
|
try {
|
|
48
55
|
const result = readModeResult(await rpc.setMode({ mode }))
|
|
49
|
-
if (!result) ctx.ui.toast.show({ message: "
|
|
56
|
+
if (!result) ctx.ui.toast.show({ message: t(language(), "toast.modeMalformed"), variant: "error" })
|
|
50
57
|
else if (!result.ok) ctx.ui.toast.show({ message: result.message, variant: "error" })
|
|
51
58
|
} catch (e) {
|
|
52
59
|
ctx.ui.toast.show({ message: describeError(e), variant: "error" })
|
|
@@ -72,11 +79,11 @@ export default Plugin.define({
|
|
|
72
79
|
// entry point that always works (keymap command + `/deepseek` slash).
|
|
73
80
|
async function openMenu() {
|
|
74
81
|
const choice = await ctx.ui.dialog.select<"deepseek" | "native" | "refresh">({
|
|
75
|
-
title:
|
|
82
|
+
title: t(language(), "dialog.title"),
|
|
76
83
|
options: [
|
|
77
|
-
{ title: "
|
|
78
|
-
{ title: "
|
|
79
|
-
{ title: "
|
|
84
|
+
{ title: t(language(), "dialog.enable"), value: "deepseek", description: t(language(), "dialog.enableDesc") },
|
|
85
|
+
{ title: t(language(), "dialog.disable"), value: "native", description: t(language(), "dialog.disableDesc") },
|
|
86
|
+
{ title: t(language(), "dialog.refresh"), value: "refresh", description: t(language(), "dialog.refreshDesc") },
|
|
80
87
|
],
|
|
81
88
|
current: status()?.mode,
|
|
82
89
|
})
|
|
@@ -106,6 +113,7 @@ export default Plugin.define({
|
|
|
106
113
|
sidebarCard({
|
|
107
114
|
status: status(),
|
|
108
115
|
error: error(),
|
|
116
|
+
language: language(),
|
|
109
117
|
theme: ctx.theme,
|
|
110
118
|
onToggle: toggle,
|
|
111
119
|
maxColumns: width(),
|
|
@@ -121,7 +129,7 @@ export default Plugin.define({
|
|
|
121
129
|
commands: [
|
|
122
130
|
{
|
|
123
131
|
id: "deepseek-search.toggle",
|
|
124
|
-
title:
|
|
132
|
+
title: t(language(), "command.toggle"),
|
|
125
133
|
slash: { name: "deepseek" },
|
|
126
134
|
run: () => void openMenu(),
|
|
127
135
|
},
|
package/src/usage.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Everything here is pure; persistence is the caller's job (ctx.storage).
|
|
8
8
|
|
|
9
|
+
import { t } from "./i18n/index.ts"
|
|
10
|
+
import type { Language } from "./rpc.ts"
|
|
11
|
+
|
|
9
12
|
export const USAGE_KEY = "usage/v1"
|
|
10
13
|
export const KEEP_DAYS = 60
|
|
11
14
|
export const MICRO = 1_000_000
|
|
@@ -102,31 +105,51 @@ export function formatYuan(costMicro: number): string {
|
|
|
102
105
|
return `¥${(costMicro / MICRO).toFixed(4)}`
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
export function formatUsage(
|
|
108
|
+
export function formatUsage(
|
|
109
|
+
ledger: UsageLedger,
|
|
110
|
+
pricePerThousand: number,
|
|
111
|
+
mode: string,
|
|
112
|
+
language: Language,
|
|
113
|
+
today = dayKey(),
|
|
114
|
+
): string {
|
|
106
115
|
const month = today.slice(0, 7)
|
|
107
116
|
const line = (label: string, d: UsageDay) =>
|
|
108
|
-
|
|
117
|
+
t(language, "usage.report.line", {
|
|
118
|
+
label,
|
|
119
|
+
search: d.search,
|
|
120
|
+
fetch: d.fetch,
|
|
121
|
+
skipped: d.empty + d.failed,
|
|
122
|
+
empty: d.empty,
|
|
123
|
+
failed: d.failed,
|
|
124
|
+
cost: formatYuan(d.costMicro),
|
|
125
|
+
})
|
|
109
126
|
return [
|
|
110
|
-
|
|
111
|
-
line(
|
|
112
|
-
line(
|
|
113
|
-
line("
|
|
114
|
-
|
|
127
|
+
t(language, "usage.report.title", { mode, price: pricePerThousand }),
|
|
128
|
+
line(`${t(language, "usage.label.today")} ${today}`, sumDays(ledger, (k) => k === today)),
|
|
129
|
+
line(`${t(language, "usage.label.month")} ${month}`, sumDays(ledger, (k) => k.startsWith(month))),
|
|
130
|
+
line(t(language, "usage.label.total"), sumDays(ledger, () => true)),
|
|
131
|
+
t(language, "usage.report.note"),
|
|
115
132
|
].join("\n")
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
/**
|
|
119
136
|
* Compact one-line summaries for a status display, keyed by period. Callers add
|
|
120
|
-
* the
|
|
137
|
+
* the localized period label themselves. Costs are read from the ledger, which
|
|
121
138
|
* already accumulated them at tally time.
|
|
122
139
|
*/
|
|
123
140
|
export function usageSummaryLines(
|
|
124
141
|
ledger: UsageLedger,
|
|
142
|
+
language: Language,
|
|
125
143
|
today = dayKey(),
|
|
126
144
|
): { today: string; month: string; total: string } {
|
|
127
145
|
const month = today.slice(0, 7)
|
|
128
146
|
const line = (d: UsageDay) =>
|
|
129
|
-
|
|
147
|
+
t(language, "usage.summary.line", {
|
|
148
|
+
search: d.search,
|
|
149
|
+
fetch: d.fetch,
|
|
150
|
+
skipped: d.empty + d.failed,
|
|
151
|
+
cost: formatYuan(d.costMicro),
|
|
152
|
+
})
|
|
130
153
|
return {
|
|
131
154
|
today: line(sumDays(ledger, (k) => k === today)),
|
|
132
155
|
month: line(sumDays(ledger, (k) => k.startsWith(month))),
|