opencode-visual-cache 1.2.16 → 1.4.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/README.md +4 -1
- package/README_EN.md +4 -1
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +343 -9
- package/dist/tui.js +735 -223
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +417 -3
package/package.json
CHANGED
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.
|
|
2
|
+
export const PLUGIN_VERSION="1.4.0";
|
package/src/index.tsx
CHANGED
|
@@ -113,6 +113,14 @@ const ZH_T = {
|
|
|
113
113
|
secDetail: "明细",
|
|
114
114
|
secModel: "模型",
|
|
115
115
|
secSkills: "已加载技能",
|
|
116
|
+
balTotal: "总余额:",
|
|
117
|
+
balNoKey: "未配置 API Key",
|
|
118
|
+
balLoading: "查询中...",
|
|
119
|
+
balError: "查询失败",
|
|
120
|
+
balErr401: "API Key 无效",
|
|
121
|
+
balErr403: "余额查询被拒绝",
|
|
122
|
+
balErrEmpty:"未获取到余额数据",
|
|
123
|
+
balErrTimeout: "查询超时",
|
|
116
124
|
} as const
|
|
117
125
|
|
|
118
126
|
const EN_T = {
|
|
@@ -145,6 +153,14 @@ const EN_T = {
|
|
|
145
153
|
secDetail: "Detail",
|
|
146
154
|
secModel: "Model",
|
|
147
155
|
secSkills: "Loaded Skills",
|
|
156
|
+
balTotal: "Total:",
|
|
157
|
+
balNoKey: "No API Key set",
|
|
158
|
+
balLoading: "Fetching...",
|
|
159
|
+
balError: "Fetch failed",
|
|
160
|
+
balErr401: "Invalid API Key",
|
|
161
|
+
balErr403: "Balance request rejected",
|
|
162
|
+
balErrEmpty:"No balance data",
|
|
163
|
+
balErrTimeout: "Request timed out",
|
|
148
164
|
} as const
|
|
149
165
|
|
|
150
166
|
// ── color helpers ────────────────────────────────────────────────
|
|
@@ -223,6 +239,16 @@ function desaturateTo(raw: unknown, maxSat: number, fallback: string): string {
|
|
|
223
239
|
return "#" + [nr, ng, nb].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
|
|
224
240
|
}
|
|
225
241
|
|
|
242
|
+
/** Darken a hex colour by multiplying each channel by `factor` (0–1). */
|
|
243
|
+
function dimColor(hex: string, factor = 0.5): string {
|
|
244
|
+
const c = rgb(hex)
|
|
245
|
+
if (!c) return hex
|
|
246
|
+
const r = Math.round(c.r * factor)
|
|
247
|
+
const g = Math.round(c.g * factor)
|
|
248
|
+
const b = Math.round(c.b * factor)
|
|
249
|
+
return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
|
|
250
|
+
}
|
|
251
|
+
|
|
226
252
|
// Morandi fallbacks — used when a theme colour cannot be resolved
|
|
227
253
|
const FALLBACK = {
|
|
228
254
|
primary: "#8B9DAF",
|
|
@@ -321,6 +347,64 @@ interface TokenDist {
|
|
|
321
347
|
stepCost: number
|
|
322
348
|
}
|
|
323
349
|
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// DeepSeek Balance
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
interface DeepSeekBalance {
|
|
355
|
+
currency: string
|
|
356
|
+
total: string
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
interface BalanceState {
|
|
360
|
+
status: "idle" | "loading" | "ok" | "error"
|
|
361
|
+
data: DeepSeekBalance[] | null
|
|
362
|
+
lastFetch: number
|
|
363
|
+
error?: string
|
|
364
|
+
key?: string // 上次成功/尝试查询所用的 key,用于检测 key 是否更换
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const BALANCE_POLL_MS = 5 * 60 * 1000 // 5 minutes
|
|
368
|
+
|
|
369
|
+
async function fetchDeepSeekBalance(apiKey: string, signal?: AbortSignal): Promise<DeepSeekBalance[]> {
|
|
370
|
+
const res = await fetch("https://api.deepseek.com/user/balance", {
|
|
371
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
372
|
+
signal,
|
|
373
|
+
})
|
|
374
|
+
if (!res.ok) {
|
|
375
|
+
if (res.status === 401) throw new Error("401")
|
|
376
|
+
if (res.status === 402 || res.status === 403) throw new Error("403")
|
|
377
|
+
throw new Error(String(res.status))
|
|
378
|
+
}
|
|
379
|
+
const json = await res.json() as {
|
|
380
|
+
is_available?: boolean
|
|
381
|
+
balance_infos?: { currency: string; total_balance: string; granted_balance: string; topped_up_balance: string }[]
|
|
382
|
+
}
|
|
383
|
+
const infos = json.balance_infos ?? []
|
|
384
|
+
if (infos.length === 0) throw new Error("EMPTY")
|
|
385
|
+
return infos.map((info) => ({
|
|
386
|
+
currency: info.currency ?? "CNY",
|
|
387
|
+
total: info.total_balance ?? "0",
|
|
388
|
+
}))
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 将余额从来源币种换算为目标币种。
|
|
393
|
+
* DEFAULT_RATES 以 USD=1 为基准:先折算为 USD,再换算到目标币种。
|
|
394
|
+
*/
|
|
395
|
+
function convertBalance(target: string, targetRate: number, amount: number, from: string): number {
|
|
396
|
+
if (from === target) return amount
|
|
397
|
+
const fromRate = DEFAULT_RATES[from] ?? 1
|
|
398
|
+
const usd = from === "USD" ? amount : amount / fromRate
|
|
399
|
+
return target === "USD" ? usd : usd * targetRate
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** 货币符号:优先取 /cache-currency 内置映射,未知币种回退为代码。 */
|
|
403
|
+
function balanceSymbol(currency: string): string {
|
|
404
|
+
const sym = CURRENCIES[currency]
|
|
405
|
+
return sym ?? currency + " "
|
|
406
|
+
}
|
|
407
|
+
|
|
324
408
|
// ---------------------------------------------------------------------------
|
|
325
409
|
// Sidebar component
|
|
326
410
|
// ---------------------------------------------------------------------------
|
|
@@ -343,8 +427,19 @@ interface PanelSignals {
|
|
|
343
427
|
setSectionDist: (v: boolean) => void
|
|
344
428
|
sectionSkills: () => boolean
|
|
345
429
|
setSectionSkills: (v: boolean) => void
|
|
430
|
+
sectionBalance: () => boolean
|
|
431
|
+
setSectionBalance: (v: boolean) => void
|
|
432
|
+
/** Increment to force a DeepSeek balance re-fetch. */
|
|
433
|
+
balanceRefresh: () => number
|
|
434
|
+
setBalanceRefresh: (v: number) => void
|
|
435
|
+
/** Preferred currency code for balance display (CNY / USD / …). Empty = first entry. */
|
|
436
|
+
balanceCurrency: () => string
|
|
437
|
+
setBalanceCurrency: (v: string) => void
|
|
346
438
|
borderVisible: () => boolean
|
|
347
439
|
setBorderVisible: (v: boolean) => void
|
|
440
|
+
/** When set, the panel renders stats for this session instead of the main one. */
|
|
441
|
+
overrideSessionId: () => string | undefined
|
|
442
|
+
setOverrideSessionId: (v: string | undefined) => void
|
|
348
443
|
}
|
|
349
444
|
|
|
350
445
|
const CURRENCIES: Record<string, string> = {
|
|
@@ -391,6 +486,9 @@ function TokenCachePanel(props: {
|
|
|
391
486
|
sectionModel, setSectionModel,
|
|
392
487
|
sectionDist, setSectionDist,
|
|
393
488
|
sectionSkills, setSectionSkills,
|
|
489
|
+
sectionBalance, setSectionBalance,
|
|
490
|
+
balanceRefresh,
|
|
491
|
+
balanceCurrency, setBalanceCurrency,
|
|
394
492
|
borderVisible, setBorderVisible,
|
|
395
493
|
} = props.signals
|
|
396
494
|
|
|
@@ -424,8 +522,63 @@ function TokenCachePanel(props: {
|
|
|
424
522
|
})
|
|
425
523
|
const [refreshTick, setRefreshTick] = createSignal(0)
|
|
426
524
|
|
|
525
|
+
// ── balance state + polling ──────────────────────────────────
|
|
526
|
+
const [balanceState, setBalanceState] = createSignal<BalanceState>({
|
|
527
|
+
status: "idle", data: null, lastFetch: 0,
|
|
528
|
+
})
|
|
529
|
+
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
530
|
+
let balanceSeq = 0
|
|
531
|
+
|
|
532
|
+
const pollBalance = async () => {
|
|
533
|
+
const key = props.api.kv.get<string>(`${KV_PREFIX}.ds_key`, "")
|
|
534
|
+
if (!key) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
535
|
+
const now = Date.now()
|
|
536
|
+
const prev = balanceState()
|
|
537
|
+
// key 已更换(重新输入)→ 强制重新查询,绕过缓存
|
|
538
|
+
if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS) return // cache still fresh
|
|
539
|
+
const seq = ++balanceSeq
|
|
540
|
+
setBalanceState({ ...prev, status: "loading", error: undefined, key })
|
|
541
|
+
const controller = new AbortController()
|
|
542
|
+
let timedOut = false
|
|
543
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort() }, 10_000)
|
|
544
|
+
try {
|
|
545
|
+
const data = await fetchDeepSeekBalance(key, controller.signal)
|
|
546
|
+
clearTimeout(timer)
|
|
547
|
+
if (seq !== balanceSeq) return // 已被更新的请求取代,丢弃过期结果
|
|
548
|
+
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key })
|
|
549
|
+
} catch (err) {
|
|
550
|
+
clearTimeout(timer)
|
|
551
|
+
if (seq !== balanceSeq) return
|
|
552
|
+
const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "")
|
|
553
|
+
// 失败时清空旧数据,避免显示过期余额
|
|
554
|
+
setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key })
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
559
|
+
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
560
|
+
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
561
|
+
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
562
|
+
createEffect(() => {
|
|
563
|
+
void balanceRefresh()
|
|
564
|
+
untrack(() => { void pollBalance() })
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
// ── auto-clear override when the user navigates to a different main session ──
|
|
568
|
+
let lastMainSid = props.sessionId
|
|
427
569
|
createEffect(() => {
|
|
428
570
|
const sid = props.sessionId
|
|
571
|
+
if (sid !== lastMainSid) {
|
|
572
|
+
lastMainSid = sid
|
|
573
|
+
if (props.signals.overrideSessionId()) {
|
|
574
|
+
props.signals.setOverrideSessionId(undefined)
|
|
575
|
+
props.api.kv.set(`${KV_PREFIX}.session`, "")
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
createEffect(() => {
|
|
581
|
+
const sid = props.signals.overrideSessionId() ?? props.sessionId
|
|
429
582
|
void refreshTick()
|
|
430
583
|
void partVersion()
|
|
431
584
|
|
|
@@ -608,10 +761,13 @@ function TokenCachePanel(props: {
|
|
|
608
761
|
const rate = props.api.kv.get<number>(`${KV_PREFIX}.rate`)
|
|
609
762
|
if (typeof sym === "string") setCurrencySymbol(sym)
|
|
610
763
|
if (typeof rate === "number" && rate > 0) setExchangeRate(rate)
|
|
764
|
+
const balCur = props.api.kv.get<string>(`${KV_PREFIX}.balance_currency`)
|
|
765
|
+
if (typeof balCur === "string") setBalanceCurrency(balCur)
|
|
611
766
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
|
|
612
767
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
|
|
613
768
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
614
769
|
setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)))
|
|
770
|
+
setSectionBalance(Boolean(props.api.kv.get(`${KV_PREFIX}.section.balance`, true)))
|
|
615
771
|
const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
616
772
|
setBorderVisible(bv !== false)
|
|
617
773
|
// Restore language preference
|
|
@@ -666,7 +822,8 @@ function TokenCachePanel(props: {
|
|
|
666
822
|
const unsubMsg = props.api.event.on("message.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1) })
|
|
667
823
|
const unsubSession = props.api.event.on("session.updated", () => { setRefreshTick(v => v + 1) })
|
|
668
824
|
setRefreshTick(v => v + 1)
|
|
669
|
-
|
|
825
|
+
const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS)
|
|
826
|
+
onCleanup(() => { clearTimeout(partTimer); clearInterval(balanceTimer); unsubPart(); unsubMsg(); unsubSession() })
|
|
670
827
|
})
|
|
671
828
|
|
|
672
829
|
// ── colours ──
|
|
@@ -751,7 +908,7 @@ function TokenCachePanel(props: {
|
|
|
751
908
|
<span style={{ fg: pal().primary }}>
|
|
752
909
|
<b>{t().title}</b>
|
|
753
910
|
<Show when={open()}>
|
|
754
|
-
<span style={{ fg: pal().muted }}>
|
|
911
|
+
<span style={{ fg: dimColor(pal().muted, 0.75) }}> v{PLUGIN_VERSION}</span>
|
|
755
912
|
</Show>
|
|
756
913
|
</span>
|
|
757
914
|
<Show when={!open() && data().hasData}>
|
|
@@ -774,6 +931,18 @@ function TokenCachePanel(props: {
|
|
|
774
931
|
</text>
|
|
775
932
|
|
|
776
933
|
<Show when={open()}>
|
|
934
|
+
<Show when={props.signals.overrideSessionId()}>
|
|
935
|
+
{(() => {
|
|
936
|
+
const prefix = " \u21b3 " + (langZH() ? "\u5B50\u4EE3\u7406: " : "Sub: ")
|
|
937
|
+
const maxSidW = Math.max(6, panelWidth() - visualWidth(prefix))
|
|
938
|
+
return (
|
|
939
|
+
<text>
|
|
940
|
+
<span style={{ fg: pal().muted }}>{prefix}</span>
|
|
941
|
+
<span style={{ fg: pal().text }}>{truncateVisual(props.signals.overrideSessionId()!, maxSidW)}</span>
|
|
942
|
+
</text>
|
|
943
|
+
)
|
|
944
|
+
})()}
|
|
945
|
+
</Show>
|
|
777
946
|
<Show when={data().hasData} fallback={
|
|
778
947
|
<>
|
|
779
948
|
<text fg={pal().muted}>{sep()}</text>
|
|
@@ -939,6 +1108,65 @@ function TokenCachePanel(props: {
|
|
|
939
1108
|
</Show>
|
|
940
1109
|
</Show>
|
|
941
1110
|
</Show>
|
|
1111
|
+
|
|
1112
|
+
{/* ── DeepSeek balance (single line) ── */}
|
|
1113
|
+
<Show when={sectionBalance()}>
|
|
1114
|
+
<text fg={pal().muted}>{sep()}</text>
|
|
1115
|
+
<Show when={balanceState().status === "idle"}>
|
|
1116
|
+
<text fg={pal().muted}>
|
|
1117
|
+
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1118
|
+
<span>{t().balNoKey}</span>
|
|
1119
|
+
</text>
|
|
1120
|
+
</Show>
|
|
1121
|
+
<Show when={balanceState().status === "loading"}>
|
|
1122
|
+
<text fg={pal().muted}>
|
|
1123
|
+
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1124
|
+
<span>{t().balLoading}</span>
|
|
1125
|
+
</text>
|
|
1126
|
+
</Show>
|
|
1127
|
+
<Show when={balanceState().status === "error"}>
|
|
1128
|
+
<text fg={pal().error}>
|
|
1129
|
+
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1130
|
+
<span>{(() => {
|
|
1131
|
+
const code = balanceState().error
|
|
1132
|
+
if (code === "401") return t().balErr401
|
|
1133
|
+
if (code === "403") return t().balErr403
|
|
1134
|
+
if (code === "EMPTY") return t().balErrEmpty
|
|
1135
|
+
if (code === "TIMEOUT") return t().balErrTimeout
|
|
1136
|
+
return t().balError + (code ? ` (${code})` : "")
|
|
1137
|
+
})()}</span>
|
|
1138
|
+
</text>
|
|
1139
|
+
</Show>
|
|
1140
|
+
<Show when={balanceState().status === "ok" && balanceState().data}>
|
|
1141
|
+
{(() => {
|
|
1142
|
+
const list = balanceState().data!
|
|
1143
|
+
const pref = balanceCurrency()
|
|
1144
|
+
// 偏好币种是 DeepSeek 原生返回的(CNY/USD)→ 直接显示
|
|
1145
|
+
const native = pref ? list.find(x => x.currency === pref) : undefined
|
|
1146
|
+
if (native) {
|
|
1147
|
+
return (
|
|
1148
|
+
<text fg={pal().text}>
|
|
1149
|
+
{justify(t().balTotal, balanceSymbol(native.currency) + native.total)}
|
|
1150
|
+
</text>
|
|
1151
|
+
)
|
|
1152
|
+
}
|
|
1153
|
+
// 非原生币种(EUR/JPY/GBP/KRW…)→ 取第一条余额按汇率换算
|
|
1154
|
+
const base = list[0]
|
|
1155
|
+
const baseAmt = parseFloat(base.total)
|
|
1156
|
+
const converted = Number.isFinite(baseAmt)
|
|
1157
|
+
? convertBalance(pref || base.currency, exchangeRate(), baseAmt, base.currency)
|
|
1158
|
+
: baseAmt
|
|
1159
|
+
const shown = pref && base.currency !== pref
|
|
1160
|
+
? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
|
|
1161
|
+
: base.total
|
|
1162
|
+
return (
|
|
1163
|
+
<text fg={pal().text}>
|
|
1164
|
+
{justify(t().balTotal, balanceSymbol(pref || base.currency) + shown)}
|
|
1165
|
+
</text>
|
|
1166
|
+
)
|
|
1167
|
+
})()}
|
|
1168
|
+
</Show>
|
|
1169
|
+
</Show>
|
|
942
1170
|
</Show>
|
|
943
1171
|
</Show>
|
|
944
1172
|
</box>
|
|
@@ -950,10 +1178,19 @@ function TokenCachePanel(props: {
|
|
|
950
1178
|
// ---------------------------------------------------------------------------
|
|
951
1179
|
|
|
952
1180
|
function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin {
|
|
1181
|
+
let lastSlotSid = ""
|
|
953
1182
|
return {
|
|
954
1183
|
order: 55,
|
|
955
1184
|
slots: {
|
|
956
1185
|
sidebar_content(ctx: TuiSlotContext, input: { session_id: string }): JSX.Element {
|
|
1186
|
+
// ── auto-clear override when the user navigates to a different main session ──
|
|
1187
|
+
if (input.session_id !== lastSlotSid) {
|
|
1188
|
+
lastSlotSid = input.session_id
|
|
1189
|
+
if (signals.overrideSessionId()) {
|
|
1190
|
+
signals.setOverrideSessionId(undefined)
|
|
1191
|
+
api.kv.set("cache_panel.session", "")
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
957
1194
|
return (
|
|
958
1195
|
<TokenCachePanel
|
|
959
1196
|
theme={ctx.theme.current}
|
|
@@ -975,8 +1212,12 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
975
1212
|
const [sectionModel, setSectionModel] = createSignal(true)
|
|
976
1213
|
const [sectionDist, setSectionDist] = createSignal(true)
|
|
977
1214
|
const [sectionSkills, setSectionSkills] = createSignal(true)
|
|
1215
|
+
const [sectionBalance, setSectionBalance] = createSignal(true)
|
|
1216
|
+
const [balanceRefresh, setBalanceRefresh] = createSignal(0)
|
|
1217
|
+
const [balanceCurrency, setBalanceCurrency] = createSignal("")
|
|
978
1218
|
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
979
1219
|
const [langZH, setLangZH] = createSignal(LANG_ZH)
|
|
1220
|
+
const [overrideSessionId, setOverrideSessionId] = createSignal<string | undefined>(undefined)
|
|
980
1221
|
|
|
981
1222
|
const signals: PanelSignals = {
|
|
982
1223
|
currencySymbol, setCurrencySymbol,
|
|
@@ -986,7 +1227,11 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
986
1227
|
sectionModel, setSectionModel,
|
|
987
1228
|
sectionDist, setSectionDist,
|
|
988
1229
|
sectionSkills, setSectionSkills,
|
|
1230
|
+
sectionBalance, setSectionBalance,
|
|
1231
|
+
balanceRefresh, setBalanceRefresh,
|
|
1232
|
+
balanceCurrency, setBalanceCurrency,
|
|
989
1233
|
borderVisible, setBorderVisible,
|
|
1234
|
+
overrideSessionId, setOverrideSessionId,
|
|
990
1235
|
}
|
|
991
1236
|
|
|
992
1237
|
api.slots.register(createSidebarSlot(api, signals))
|
|
@@ -1012,6 +1257,9 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1012
1257
|
const defRate = DEFAULT_RATES[opt.value] ?? 1
|
|
1013
1258
|
api.kv.set(`${KV_PREFIX}.currency`, sym)
|
|
1014
1259
|
api.kv.set(`${KV_PREFIX}.rate`, defRate)
|
|
1260
|
+
// 同步余额显示币种偏好:CNY/USD 原生直显,其余币种按汇率换算
|
|
1261
|
+
api.kv.set(`${KV_PREFIX}.balance_currency`, opt.value)
|
|
1262
|
+
signals.setBalanceCurrency(opt.value)
|
|
1015
1263
|
signals.setCurrencySymbol(sym)
|
|
1016
1264
|
signals.setExchangeRate(defRate)
|
|
1017
1265
|
api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` })
|
|
@@ -1056,6 +1304,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1056
1304
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
1057
1305
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1058
1306
|
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
1307
|
+
const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
1059
1308
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
1060
1309
|
dialog?.replace(() => (
|
|
1061
1310
|
<api.ui.DialogSelect
|
|
@@ -1065,6 +1314,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1065
1314
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
1066
1315
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
1067
1316
|
{ title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
|
|
1317
|
+
{ title: `DS Balance [${balanceOn ? "ON" : "OFF"}]`, value: "balance" },
|
|
1068
1318
|
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
1069
1319
|
]}
|
|
1070
1320
|
onSelect={(opt) => {
|
|
@@ -1081,6 +1331,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1081
1331
|
if (opt.value === "model") signals.setSectionModel(!cur)
|
|
1082
1332
|
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
1083
1333
|
if (opt.value === "skills") signals.setSectionSkills(!cur)
|
|
1334
|
+
if (opt.value === "balance") signals.setSectionBalance(!cur)
|
|
1084
1335
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
|
|
1085
1336
|
}
|
|
1086
1337
|
dialog?.clear()
|
|
@@ -1101,9 +1352,10 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1101
1352
|
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
1102
1353
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1103
1354
|
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
1355
|
+
const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
1104
1356
|
api.ui.toast({
|
|
1105
1357
|
title: "Cache Panel Config",
|
|
1106
|
-
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"}`,
|
|
1358
|
+
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"} | Balance: ${balance ? "ON" : "OFF"}`,
|
|
1107
1359
|
duration: 8000,
|
|
1108
1360
|
})
|
|
1109
1361
|
dialog?.clear()
|
|
@@ -1134,6 +1386,54 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1134
1386
|
))
|
|
1135
1387
|
},
|
|
1136
1388
|
},
|
|
1389
|
+
{
|
|
1390
|
+
title: "Cache: Set DeepSeek API Key",
|
|
1391
|
+
value: "cache.balance.key",
|
|
1392
|
+
description: "Set or update the DeepSeek API key for balance display",
|
|
1393
|
+
slash: { name: "cache-balance-key" },
|
|
1394
|
+
onSelect: (dialog) => {
|
|
1395
|
+
const zh = langZH()
|
|
1396
|
+
const current = api.kv.get<string>(`${KV_PREFIX}.ds_key`, "")
|
|
1397
|
+
// 已保存的 key 以脱敏形式预填:保留 "sk-" 前缀 + 头 5 尾 5 字符,中间用 * 填充
|
|
1398
|
+
const maskKey = (k: string): string => {
|
|
1399
|
+
if (!k) return ""
|
|
1400
|
+
const prefix = k.startsWith("sk-") ? "sk-" : ""
|
|
1401
|
+
const body = prefix ? k.slice(3) : k
|
|
1402
|
+
if (body.length <= 10) return prefix + body.slice(0, 5) + "*".repeat(Math.max(3, body.length - 5))
|
|
1403
|
+
return prefix + body.slice(0, 5) + "*".repeat(Math.max(3, body.length - 10)) + body.slice(-5)
|
|
1404
|
+
}
|
|
1405
|
+
const masked = maskKey(current)
|
|
1406
|
+
dialog?.replace(() => (
|
|
1407
|
+
<api.ui.DialogPrompt
|
|
1408
|
+
title={zh ? "DeepSeek API Key" : "DeepSeek API Key"}
|
|
1409
|
+
description={() => <text>{zh ? "输入 DeepSeek API Key 以显示账户余额(留空清除)" : "Enter your DeepSeek API key to show account balance (leave empty to clear)"}</text>}
|
|
1410
|
+
placeholder="sk-..."
|
|
1411
|
+
value={masked}
|
|
1412
|
+
onConfirm={(val) => {
|
|
1413
|
+
const input = val.trim()
|
|
1414
|
+
// 空 → 清除;含 * (脱敏占位符残留)→ 视为未修改,保留原 key;否则为新 key
|
|
1415
|
+
let key: string
|
|
1416
|
+
if (input === "") {
|
|
1417
|
+
key = ""
|
|
1418
|
+
} else if (input.includes("*")) {
|
|
1419
|
+
key = current
|
|
1420
|
+
} else {
|
|
1421
|
+
key = input
|
|
1422
|
+
}
|
|
1423
|
+
api.kv.set(`${KV_PREFIX}.ds_key`, key)
|
|
1424
|
+
setBalanceRefresh(v => v + 1)
|
|
1425
|
+
if (key) {
|
|
1426
|
+
api.ui.toast({ message: zh ? "API Key 已保存,正在查询余额..." : "API Key saved, fetching balance..." })
|
|
1427
|
+
} else {
|
|
1428
|
+
api.ui.toast({ message: zh ? "API Key 已清除" : "API Key cleared" })
|
|
1429
|
+
}
|
|
1430
|
+
dialog?.clear()
|
|
1431
|
+
}}
|
|
1432
|
+
onCancel={() => dialog?.clear()}
|
|
1433
|
+
/>
|
|
1434
|
+
))
|
|
1435
|
+
},
|
|
1436
|
+
},
|
|
1137
1437
|
{
|
|
1138
1438
|
title: "Cache: Debug Skills Detection",
|
|
1139
1439
|
value: "cache.debug-skills",
|
|
@@ -1174,6 +1474,120 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1174
1474
|
})
|
|
1175
1475
|
},
|
|
1176
1476
|
},
|
|
1477
|
+
{
|
|
1478
|
+
title: "Cache: Sub-Agent Stats",
|
|
1479
|
+
value: "cache.session",
|
|
1480
|
+
description: "View token cache statistics for a sub-agent by session ID",
|
|
1481
|
+
slash: { name: "cache-session" },
|
|
1482
|
+
onSelect: (dialog) => {
|
|
1483
|
+
// ── 扫描当前主 session 的子代理 session ID 列表 ──
|
|
1484
|
+
const rt = api.route.current
|
|
1485
|
+
const parentSid = rt.name === "session" && rt.params ? String(rt.params.sessionID) : ""
|
|
1486
|
+
const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
|
|
1487
|
+
|
|
1488
|
+
interface ChildEntry { title: string; value: string; description: string }
|
|
1489
|
+
const children: ChildEntry[] = []
|
|
1490
|
+
if (parentSid) {
|
|
1491
|
+
try {
|
|
1492
|
+
const msgs = api.state.session.messages(parentSid)
|
|
1493
|
+
for (const msg of msgs) {
|
|
1494
|
+
if (msg.role !== "assistant") continue
|
|
1495
|
+
let parts: readonly Part[] = []
|
|
1496
|
+
try { parts = api.state.part(msg.id) } catch {}
|
|
1497
|
+
for (const p of parts) {
|
|
1498
|
+
if (p.type !== "tool") continue
|
|
1499
|
+
const tool = String((p as ToolPart).tool ?? "")
|
|
1500
|
+
if (!SUBAGENT_TOOLS.has(tool)) continue
|
|
1501
|
+
const st = (p as any).state as Record<string, unknown> | undefined
|
|
1502
|
+
const stMeta = st?.metadata as Record<string, unknown> | undefined
|
|
1503
|
+
const subSid = stMeta?.session_id ?? stMeta?.sessionId
|
|
1504
|
+
if (!subSid) continue
|
|
1505
|
+
const sidStr = String(subSid)
|
|
1506
|
+
const input = st?.input as Record<string, unknown> | undefined
|
|
1507
|
+
const agent = String((p as any).subagent_type ?? input?.subagent_type ?? input?.category ?? tool)
|
|
1508
|
+
const prompt = String(input?.prompt ?? "")
|
|
1509
|
+
const desc = input?.description ? String(input.description) : ""
|
|
1510
|
+
const title = desc || prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 40) || agent
|
|
1511
|
+
children.push({ title, value: sidStr, description: `${agent} · ${sidStr.slice(0, 24)}…` })
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
} catch {}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// 去重
|
|
1518
|
+
const seen = new Set<string>()
|
|
1519
|
+
const unique = children.filter(c => { if (seen.has(c.value)) return false; seen.add(c.value); return true })
|
|
1520
|
+
|
|
1521
|
+
if (unique.length > 0) {
|
|
1522
|
+
// ── 有子代理 → DialogSelect 列表选择 ──
|
|
1523
|
+
const zh = langZH()
|
|
1524
|
+
const currentSid = signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "")
|
|
1525
|
+
const options = unique.map((c, i) => ({
|
|
1526
|
+
title: `${i + 1}. ${c.title}`,
|
|
1527
|
+
value: c.value,
|
|
1528
|
+
description: c.description,
|
|
1529
|
+
}))
|
|
1530
|
+
// 首尾各放一个"回到主会话",长列表时顶部底部均可直达
|
|
1531
|
+
const backValue = "__main__"
|
|
1532
|
+
const backTitle = `\u2500 ${zh ? "\u56DE\u5230\u4E3B\u4F1A\u8BDD" : "Back to Main"}`
|
|
1533
|
+
options.unshift({ title: backTitle, value: backValue, description: "" })
|
|
1534
|
+
options.push({ title: backTitle, value: backValue, description: "" })
|
|
1535
|
+
const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1
|
|
1536
|
+
dialog?.replace(() => (
|
|
1537
|
+
<api.ui.DialogSelect
|
|
1538
|
+
title={zh ? "选择子代理" : "Select Sub-Agent"}
|
|
1539
|
+
options={options}
|
|
1540
|
+
current={currentIdx >= 0 ? options[currentIdx].value : undefined}
|
|
1541
|
+
onSelect={(opt) => {
|
|
1542
|
+
if (opt.value === backValue) {
|
|
1543
|
+
signals.setOverrideSessionId(undefined)
|
|
1544
|
+
api.kv.set(`${KV_PREFIX}.session`, "")
|
|
1545
|
+
api.ui.toast({ message: zh ? "已切回主会话" : "Switched to main session" })
|
|
1546
|
+
} else {
|
|
1547
|
+
signals.setOverrideSessionId(opt.value)
|
|
1548
|
+
api.kv.set(`${KV_PREFIX}.session`, opt.value)
|
|
1549
|
+
api.ui.toast({ message: (zh ? "已切换至子代理: " : "Showing sub-agent: ") + opt.value.slice(0, 24) + "\u2026" })
|
|
1550
|
+
}
|
|
1551
|
+
dialog?.clear()
|
|
1552
|
+
}}
|
|
1553
|
+
/>
|
|
1554
|
+
))
|
|
1555
|
+
} else {
|
|
1556
|
+
// ── 无子代理 → DialogPrompt 手动粘贴 ──
|
|
1557
|
+
const zh = langZH()
|
|
1558
|
+
dialog?.replace(() => (
|
|
1559
|
+
<api.ui.DialogPrompt
|
|
1560
|
+
title={signals.overrideSessionId() ? zh ? "切换子代理" : "Switch Sub" : zh ? "查看子代理缓存" : "View Sub Cache"}
|
|
1561
|
+
description={() => <text>{zh ? "未找到子代理,请手动粘贴 Session ID" : "No sub-agents found. Paste a Session ID manually"}</text>}
|
|
1562
|
+
placeholder="ses_..."
|
|
1563
|
+
value={signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "") ?? ""}
|
|
1564
|
+
onConfirm={(val) => {
|
|
1565
|
+
const sid = val.trim()
|
|
1566
|
+
if (sid) {
|
|
1567
|
+
signals.setOverrideSessionId(sid)
|
|
1568
|
+
api.kv.set(`${KV_PREFIX}.session`, sid)
|
|
1569
|
+
api.ui.toast({ message: (langZH() ? "已切换至子代理: " : "Showing sub-agent: ") + sid.slice(0, 24) + "\u2026" })
|
|
1570
|
+
}
|
|
1571
|
+
dialog?.clear()
|
|
1572
|
+
}}
|
|
1573
|
+
onCancel={() => dialog?.clear()}
|
|
1574
|
+
/>
|
|
1575
|
+
))
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
title: "Cache: Back to Main",
|
|
1581
|
+
value: "cache.session.back",
|
|
1582
|
+
description: "Return to main session stats",
|
|
1583
|
+
slash: { name: "cache-session-back" },
|
|
1584
|
+
onSelect: (dialog) => {
|
|
1585
|
+
signals.setOverrideSessionId(undefined)
|
|
1586
|
+
api.kv.set(`${KV_PREFIX}.session`, "")
|
|
1587
|
+
api.ui.toast({ message: langZH() ? "已切回主会话" : "Switched to main session" })
|
|
1588
|
+
dialog?.clear()
|
|
1589
|
+
},
|
|
1590
|
+
},
|
|
1177
1591
|
])
|
|
1178
1592
|
}
|
|
1179
1593
|
|