deepseek-harness-wallet 0.3.6 → 0.3.8

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.3.8 - 2026-09-02
6
+
7
+ - 新增第三方 API 自定义价格(Issue #36):可按精确 Provider/模型设置币种以及每百万 Token 的输入、缓存读、缓存写和输出价格;本会话与 365 天账本会按当前规则重新估算,并始终标注为用户自定义估算。 / Added custom third-party API pricing (Issue #36) with exact Provider/model matching, currency, and per-million input/cache-read/cache-write/output rates; current-session and 365-day ledger totals are recalculated from the active user rule and always labeled as estimates.
8
+ - 自定义价格保存在本机 `wallet.json`,支持修改、删除、观察到的路由建议、输入边界与官方/套餐 Provider 隔离;不会保存 API Key,也不会更改历史 Token。 / Custom prices persist locally in `wallet.json`, support editing, deletion, observed-route suggestions, bounded inputs, and strict separation from official/plan providers; no API keys or historical token counts are changed.
9
+
10
+ ## 0.3.7 - 2026-09-01
11
+
12
+ - 适配 DeepSeek Harness `0.1.2-alpha.3` 的新客户端包结构:移除已被官方删除的 `@deepseek-ai/dsh-client-runtime` 清单边,继续通过 Cordis `sessions` 服务和现存 UI slot 合约工作;同时保留 `0.1.1-rc.2` 兼容。 / Adapted to the new DeepSeek Harness `0.1.2-alpha.3` client package layout by removing the retired `@deepseek-ai/dsh-client-runtime` manifest edge while continuing to consume the Cordis `sessions` service and surviving UI-slot contracts; `0.1.1-rc.2` compatibility is retained.
13
+ - 在隔离的 alpha.3 构建和数据目录中完成 npm 安装、完整 Harness 构建、Web 启动、插件后端、设置页、365 天账本、背景选项和健康检查验证,未向测试环境写入真实凭据。 / Verified npm installation, a full Harness build, Web startup, plugin host routes, settings UI, 365-day ledger, background controls, and health checks in isolated alpha.3 source/data directories without copying real credentials into the test environment.
14
+
5
15
  ## 0.3.6 - 2026-08-28
6
16
 
7
17
  - 峰谷时钟新增明确的「透明 / 实色」背景选择,不加入自动模式;透明模式常态不铺底,悬停时使用主题实色,实色模式保持原有卡片背景。 / Added an explicit peak-clock background choice between transparent and solid, without an automatic mode; transparent mode removes the normal fill and uses the themed solid hover surface, while solid mode keeps the existing card background.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  > A local-first companion that keeps account status, per-conversation usage, completion reminders, official recharge, flexible layout, and host-gated session controls beside the DSH composer.
16
16
 
17
- > **Version:** v0.3.6 is the current local patch candidate; v0.3.5 remains the published stable release on GitHub and npm until release approval.
17
+ > **Version:** v0.3.8 is the current release on GitHub and npm.
18
18
 
19
19
  > If DeepSeek Harness Control Center helps you, please consider leaving a ⭐ Star. Thank you!
20
20
 
@@ -31,7 +31,8 @@
31
31
  - **Z.ai Coding Plan quotas** — a generic official-plan adapter monitors configured Global and China plans without exposing credentials. It separates the 5-hour model-token window from monthly MCP-tool usage, leads with quota remaining from 100% down while keeping usage as secondary context, retains the last successful snapshot on failure, and never converts subscription quota into CNY balance.
32
32
  - **Provider-aware composer surfaces** — the chip and sidebar clock follow the session's selected provider/model. Z.ai replaces DeepSeek balance, recharge, and peak pricing with plan-window summaries; unrelated providers show only their own session tokens, and DeepSeek V4 restores the wallet and peak clock.
33
33
  - **365-day local usage ledger** — Wallet settings keeps the heatmap visible, while compact wallet panels keep it collapsible. Stable request identities are deduplicated, official cost is locked at usage time, official and third-party data stay separate, and prompts or responses are never stored. Collection begins after upgrading to v0.3.2; older aggregate counters have no trustworthy dates and are not backfilled.
34
- - **Third-party total** — current-session tokens (input / cache read / output). No balance guessing, no cost math, zero configuration.
34
+ - **Third-party total** — current-session tokens (input / cache read / output) remain available with zero configuration.
35
+ - **Custom third-party pricing** — enter an exact Provider/model route, currency, and per-million input/cache-read/cache-write/output rates. Current-session and 365-day ledger costs are recalculated locally from the saved rule and clearly labeled as user-defined estimates, never as provider balances or invoices.
35
36
  - **Provider classification** — observed wrapper routes appear in the settings page; opted-in routes join the official token/cost bucket for subsequent calls and are priced with the official table. Existing history is not retroactively reclassified.
36
37
  - **Click the chip** to open the detail panel: correctly formatted per-currency balances, cost and token splits, a freely editable low-balance threshold for the active account and currency (two decimals, persisted per account; alerts never mix currencies), manual refresh, and a jump to the official recharge page (first click shows the domain for confirmation — anti-phishing).
37
38
  - **Move, dock, and scale** — drag the chip freely, preview nearby snap targets, use compact horizontal or vertical layouts, adjust its scale from the control panel, and show official or third-party data independently. The peak clock background can be explicitly set to transparent (solid on hover) or solid; there is no automatic mode. The choices are remembered locally.
@@ -73,13 +74,13 @@ Details: [compatibility](#browser-desktop-and-os-compatibility) · [data and tru
73
74
 
74
75
  ## Install
75
76
 
76
- From npm (published stable v0.3.5; v0.3.6 is the local patch candidate until release approval):
77
+ From npm (published stable v0.3.8):
77
78
 
78
79
  ```sh
79
80
  dsh plugin --profile web add deepseek-harness-wallet
80
81
  ```
81
82
 
82
- or from GitHub `main` (published v0.3.5 source until release approval):
83
+ or from GitHub `main`:
83
84
 
84
85
  ```sh
85
86
  dsh plugin --profile web add github:feibi-mochi/deepseek-harness-control-center
@@ -116,6 +117,7 @@ The client contains no operating-system-specific feature branch; it checks the W
116
117
  | Verification level | Coverage |
117
118
  | --- | --- |
118
119
  | Real environment checked for this release | Windows + current Edge + DSH Web |
120
+ | Verified Harness versions | `0.1.1-rc.2` and isolated `0.1.2-alpha.3` Web profiles; the alpha profile was built from its official tag and started with no real credentials |
119
121
  | Automated compatibility checks | Browser notification failure, in-page fallback, cross-tab fallback, storage fallback, CSS-scale fallback, and synchronous/asynchronous desktop adapters |
120
122
  | Capability-compatible targets | Current Chrome, Edge, and Firefox on Windows/macOS/Linux; Safari on macOS; Electron/Tauri-style DSH wrappers that provide the requirements below |
121
123
 
@@ -148,7 +150,7 @@ For buildable DSH hosts, the npm package and repository include a versioned [Age
148
150
  | Token accounting | Listens to the `llm/stream` event and buckets per session and provider: `deepseek-official` plus explicitly opted-in wrapper routes use the official bucket; other providers stay third-party; each usage event also locks its contemporaneous official price, so multiple sessions and pricing windows never mix. |
149
151
  | Balance | The wallet plugin itself sends the active key directly only to the official `/user/balance` endpoint. When multi-account switching is enabled, the selected key is also written into the DSH credentials seam; DSH may then use it for subsequent model requests. |
150
152
  | Accounts | Keys live encrypted in `$DSH_HOME/storages/accounts.json`, with an encrypted `accounts.json.bak` fallback for a missing, corrupt, or undecryptable primary. Windows uses current-user DPAPI; other platforms use an owner-only AES-GCM key file, so move `accounts.json`, `.bak`, and `.key` together. If neither copy can be read, account writes fail closed. |
151
- | Usage ledger | Local events live in `$DSH_HOME/storages/wallet.json` with a `wallet.json.bak` recovery copy. Missing/corrupt primaries recover automatically; if neither copy is readable, wallet writes fail closed. Up to 365 days of session/provider/model/token metadata and locked cost are kept—never prompts, tool arguments, or response bodies. |
153
+ | Usage ledger | Local events and custom third-party price rules live in `$DSH_HOME/storages/wallet.json` with a `wallet.json.bak` recovery copy. Missing/corrupt primaries recover automatically; if neither copy is readable, wallet writes fail closed. Up to 365 days of session/provider/model/token metadata and official locked cost are kept—never prompts, tool arguments, response bodies, or API keys. Third-party estimates are recalculated from the current custom rule. |
152
154
  | Local settings | Layout, scale, visibility, reminder, and panel settings stay in browser-compatible local storage. |
153
155
  | Permanent deletion | Opt-in and host-gated. The wallet never advertises the action unless the host implements the matching session deletion path. |
154
156
  | Model surface | No tools registered, no prompt injection, zero token cost. |
@@ -172,6 +174,7 @@ Historical deepseek-chat and deepseek-reasoner records retain their original fla
172
174
 
173
175
  - [x] 365-day Token heatmap and rebuildable local usage ledger
174
176
  - [x] Z.ai Coding Plan Global/China monitoring on a generic official-plan adapter contract
177
+ - [x] User-defined third-party Provider/model pricing
175
178
  - [ ] Additional provider price/balance adapters only after real-account validation
176
179
 
177
180
  ## License
package/index.js CHANGED
@@ -33,7 +33,7 @@ const OFFICIAL_PROVIDER = 'deepseek-official'
33
33
  // DeepSeek's official paid-API bucket by the wrapper-provider alias control.
34
34
  const PLAN_PROVIDER_IDS = new Set(PLAN_ADAPTERS.map((adapter) => adapter.provider))
35
35
  const RECHARGE_URL = 'https://platform.deepseek.com/top_up'
36
- const PLUGIN_VERSION = '0.3.6'
36
+ const PLUGIN_VERSION = '0.3.8'
37
37
  const PRICING_SOURCE_URL = 'https://api-docs.deepseek.com/zh-cn/quick_start/pricing/'
38
38
  const PRICING_SYNC_INTERVAL_MS = 6 * 60 * 60_000
39
39
  const PRICING_SYNC_TIMEOUT_MS = 8_000
@@ -49,10 +49,12 @@ const ACCOUNTS_CRYPTO_VERSION = 1
49
49
  const CREDENTIAL_REF = 'DEEPSEEK_API_KEY'
50
50
  const DEFAULT_THRESHOLD = 5
51
51
  const BALANCE_REFRESH_MS = 60_000
52
- const STORE_VERSION = 5
52
+ const STORE_VERSION = 6
53
53
  const HISTORY_VERSION = 1
54
54
  const HISTORY_RETENTION_DAYS = 365
55
55
  const HISTORY_MAX_EVENTS = 20_000
56
+ const CUSTOM_PRICE_MAX_RULES = 100
57
+ const CUSTOM_PRICE_MAX_RATE = 1_000_000
56
58
  const HISTORY_TIMEZONE = 'Asia/Shanghai'
57
59
  const DAY_MS = 86_400_000
58
60
  const PLAN_REFRESH_MS = 5 * 60_000
@@ -733,6 +735,15 @@ function removeSessionContribution(event) {
733
735
  bucket.unpriced = Math.max(0, currentUnpriced - 1)
734
736
  bucket.priced = bucket.unpriced === 0
735
737
  }
738
+ } else if (bucket.routes && bucket.routes[event.provider] && bucket.routes[event.provider].models) {
739
+ const routeCounters = bucket.routes[event.provider].models[event.model]
740
+ if (routeCounters !== null && typeof routeCounters === 'object') {
741
+ subtractUsage(routeCounters, event.usage)
742
+ if (Object.values(routeCounters).every((value) => finiteCounter(value) === 0)) {
743
+ delete bucket.routes[event.provider].models[event.model]
744
+ }
745
+ if (Object.keys(bucket.routes[event.provider].models).length === 0) delete bucket.routes[event.provider]
746
+ }
736
747
  }
737
748
  }
738
749
 
@@ -812,17 +823,24 @@ function historyAggregate(events) {
812
823
  }
813
824
  total.cost = official.cost
814
825
  total.priced = official.priced
815
- return {
816
- official: publicHistoryAggregate(official, true),
817
- third: publicHistoryAggregate(third, false),
818
- total: publicHistoryAggregate(total, official.calls > 0),
819
- cacheHitRate: cacheInput + cacheRead > 0 ? Math.round(cacheRead / (cacheInput + cacheRead) * 1000) / 10 : null,
820
- models: [...models.values()].map((model) => ({
826
+ const publicModels = [...models.values()].map((model) => {
827
+ const aggregate = publicHistoryAggregate(model, model.official)
828
+ const priced = model.official ? null : customCostOf(model.provider, model.model, model.tokens, store.customPrices)
829
+ return {
821
830
  provider: model.provider,
822
831
  model: model.model,
823
832
  official: model.official,
824
- ...publicHistoryAggregate(model, model.official),
825
- })),
833
+ ...aggregate,
834
+ customCost: priced === null ? null : { cost: priced.cost, currency: priced.currency },
835
+ }
836
+ })
837
+ const customCosts = customCostTotals(publicModels)
838
+ return {
839
+ official: publicHistoryAggregate(official, true),
840
+ third: { ...publicHistoryAggregate(third, false), customCosts },
841
+ total: { ...publicHistoryAggregate(total, official.calls > 0), customCosts },
842
+ cacheHitRate: cacheInput + cacheRead > 0 ? Math.round(cacheRead / (cacheInput + cacheRead) * 1000) / 10 : null,
843
+ models: publicModels,
826
844
  }
827
845
  }
828
846
 
@@ -903,6 +921,90 @@ export function isOfficialProvider(provider, extraProviders) {
903
921
  return Array.isArray(extraProviders) && extraProviders.includes(provider)
904
922
  }
905
923
 
924
+ function customPriceRate(value) {
925
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > CUSTOM_PRICE_MAX_RATE) return null
926
+ return Math.round(value * 1_000_000) / 1_000_000
927
+ }
928
+
929
+ /** Normalize one exact third-party provider/model price rule (rates are per 1M tokens). */
930
+ export function normalizeCustomPriceRule(value) {
931
+ const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
932
+ const provider = boundedString(source.provider, 100)
933
+ const model = boundedString(source.model, 160)
934
+ const currency = typeof source.currency === 'string' ? source.currency.toUpperCase() : ''
935
+ const input = customPriceRate(source.input)
936
+ const cacheRead = customPriceRate(source.cacheRead)
937
+ const cacheWrite = customPriceRate(source.cacheWrite)
938
+ const output = customPriceRate(source.output)
939
+ if (provider === null || model === null || !/^[A-Z]{3}$/.test(currency)) return null
940
+ if (provider === OFFICIAL_PROVIDER || isPlanProvider(provider)) return null
941
+ if (provider === '__proto__' || provider === 'prototype' || provider === 'constructor') return null
942
+ if (model === '__proto__' || model === 'prototype' || model === 'constructor') return null
943
+ if ([input, cacheRead, cacheWrite, output].some((rate) => rate === null)) return null
944
+ return { provider, model, currency, input, cacheRead, cacheWrite, output }
945
+ }
946
+
947
+ /** Normalize, deduplicate, and bound persisted custom third-party price rules. */
948
+ export function normalizeCustomPrices(value) {
949
+ if (!Array.isArray(value)) return []
950
+ const rules = new Map()
951
+ for (const candidate of value) {
952
+ const rule = normalizeCustomPriceRule(candidate)
953
+ if (rule === null) continue
954
+ const key = rule.provider + '\u0000' + rule.model
955
+ if (rules.has(key)) rules.delete(key)
956
+ rules.set(key, rule)
957
+ if (rules.size > CUSTOM_PRICE_MAX_RULES) rules.delete(rules.keys().next().value)
958
+ }
959
+ return [...rules.values()]
960
+ }
961
+
962
+ /** Price one exact third-party route from user-entered per-million-token rates. */
963
+ export function customCostOf(provider, model, usage, rules) {
964
+ if (typeof provider !== 'string' || typeof model !== 'string') return null
965
+ const rule = normalizeCustomPrices(rules).find((entry) => entry.provider === provider && entry.model === model)
966
+ if (rule === undefined) return null
967
+ const counters = historyUsageCounters(usage)
968
+ const cost = (
969
+ counters.input * rule.input
970
+ + counters.cacheRead * rule.cacheRead
971
+ + counters.cacheWrite * rule.cacheWrite
972
+ + counters.output * rule.output
973
+ ) / 1e6
974
+ return { cost, currency: rule.currency, rule }
975
+ }
976
+
977
+ function normalizeThirdPartyRoutes(value) {
978
+ const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
979
+ const routes = {}
980
+ let providers = 0
981
+ let models = 0
982
+ for (const [provider, rawProvider] of Object.entries(source)) {
983
+ if (boundedString(provider, 100) === null || provider === OFFICIAL_PROVIDER || isPlanProvider(provider)) continue
984
+ if (provider === '__proto__' || provider === 'prototype' || provider === 'constructor') continue
985
+ const rawModels = rawProvider !== null && typeof rawProvider === 'object' && !Array.isArray(rawProvider)
986
+ ? rawProvider.models
987
+ : null
988
+ const normalizedModels = normalizeModels(rawModels)
989
+ if (Object.keys(normalizedModels).length === 0) continue
990
+ routes[provider] = { models: normalizedModels }
991
+ providers += 1
992
+ models += Object.keys(normalizedModels).length
993
+ if (providers >= 100 || models >= 1_000) break
994
+ }
995
+ return routes
996
+ }
997
+
998
+ function customCostTotals(rows) {
999
+ const totals = {}
1000
+ for (const row of rows) {
1001
+ if (row === null || typeof row !== 'object' || !row.customCost) continue
1002
+ const currency = row.customCost.currency
1003
+ totals[currency] = (totals[currency] || 0) + row.customCost.cost
1004
+ }
1005
+ return totals
1006
+ }
1007
+
906
1008
  export function normalizeUiPreferences(value) {
907
1009
  const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
908
1010
  const normalized = {}
@@ -964,7 +1066,10 @@ export function normalizeStoreData(value, atMs = Date.now()) {
964
1066
  const rawSession = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
965
1067
  sessions[sessionId] = {
966
1068
  official: migratedOfficialBucket(rawSession.official, atMs),
967
- third: { models: normalizeModels(rawSession.third && rawSession.third.models) },
1069
+ third: {
1070
+ models: normalizeModels(rawSession.third && rawSession.third.models),
1071
+ routes: normalizeThirdPartyRoutes(rawSession.third && rawSession.third.routes),
1072
+ },
968
1073
  }
969
1074
  sessionCount += 1
970
1075
  if (sessionCount >= 5_000) break
@@ -976,6 +1081,7 @@ export function normalizeStoreData(value, atMs = Date.now()) {
976
1081
  sessions,
977
1082
  officialProviders: normalizeProviderList(source.officialProviders),
978
1083
  knownProviders: normalizeProviderList(source.knownProviders),
1084
+ customPrices: normalizeCustomPrices(source.customPrices),
979
1085
  plans: normalizePlanSnapshotCache(source.plans, atMs),
980
1086
  preferences: normalizeUiPreferences(source.preferences),
981
1087
  }
@@ -993,7 +1099,7 @@ let usageStorageRecovered = false
993
1099
  let usageStoreSkipBackupOnce = false
994
1100
 
995
1101
  function emptyStoreData() {
996
- return { version: STORE_VERSION, thresholds: { CNY: DEFAULT_THRESHOLD }, accountThresholds: {}, sessions: {}, officialProviders: [], knownProviders: [], plans: {}, preferences: {}, history: emptyHistory() }
1102
+ return { version: STORE_VERSION, thresholds: { CNY: DEFAULT_THRESHOLD }, accountThresholds: {}, sessions: {}, officialProviders: [], knownProviders: [], customPrices: [], plans: {}, preferences: {}, history: emptyHistory() }
997
1103
  }
998
1104
 
999
1105
  function readStoreFile(path) {
@@ -1703,7 +1809,7 @@ function recordUsage(options, usage, atMs = Date.now()) {
1703
1809
  store.knownProviders = normalizeProviderList([...store.knownProviders, provider])
1704
1810
  }
1705
1811
  if (!Object.hasOwn(store.sessions, sessionId)) {
1706
- store.sessions[sessionId] = { official: emptyBucket(true), third: emptyBucket(false) }
1812
+ store.sessions[sessionId] = { official: emptyBucket(true), third: { models: {}, routes: {} } }
1707
1813
  }
1708
1814
  const event = createHistoryEvent(request, usage, atMs, store.officialProviders)
1709
1815
  const previousEvent = event !== null && store.history && store.history.events ? store.history.events[event.eventId] : undefined
@@ -1716,6 +1822,10 @@ function recordUsage(options, usage, atMs = Date.now()) {
1716
1822
  else {
1717
1823
  if (!Object.hasOwn(bucket.models, model)) bucket.models[model] = emptyCounters()
1718
1824
  addUsage(bucket.models[model], usage)
1825
+ if (bucket.routes === null || typeof bucket.routes !== 'object' || Array.isArray(bucket.routes)) bucket.routes = {}
1826
+ if (!Object.hasOwn(bucket.routes, provider)) bucket.routes[provider] = { models: {} }
1827
+ if (!Object.hasOwn(bucket.routes[provider].models, model)) bucket.routes[provider].models[model] = emptyCounters()
1828
+ addUsage(bucket.routes[provider].models[model], usage)
1719
1829
  }
1720
1830
  recordHistoryEvent(event)
1721
1831
  }
@@ -1837,6 +1947,42 @@ function balanceTotal() {
1837
1947
  return sumBalances(balance.balances)
1838
1948
  }
1839
1949
 
1950
+ function thirdPartyRouteView(bucket) {
1951
+ const routes = []
1952
+ const source = normalizeThirdPartyRoutes(bucket && bucket.routes)
1953
+ for (const [provider, providerState] of Object.entries(source)) {
1954
+ if (isPlanProvider(provider) || isOfficialProvider(provider, store.officialProviders)) continue
1955
+ for (const [model, counters] of Object.entries(providerState.models)) {
1956
+ const priced = customCostOf(provider, model, counters, store.customPrices)
1957
+ routes.push({
1958
+ provider,
1959
+ model,
1960
+ tokens: counters,
1961
+ totalTokens: historyTokenTotal(counters),
1962
+ customCost: priced === null ? null : { cost: priced.cost, currency: priced.currency },
1963
+ })
1964
+ }
1965
+ }
1966
+ return routes
1967
+ }
1968
+
1969
+ function knownThirdPartyRoutes() {
1970
+ const found = new Map()
1971
+ for (const session of Object.values(store.sessions)) {
1972
+ for (const route of thirdPartyRouteView(session && session.third)) {
1973
+ found.set(route.provider + '\u0000' + route.model, { provider: route.provider, model: route.model })
1974
+ }
1975
+ }
1976
+ for (const event of Object.values(store.history && store.history.events ? store.history.events : {})) {
1977
+ if (!event || event.official || isPlanProvider(event.provider) || isOfficialProvider(event.provider, store.officialProviders)) continue
1978
+ const provider = boundedString(event.provider, 100)
1979
+ const model = boundedString(event.model, 160)
1980
+ if (provider !== null && model !== null) found.set(provider + '\u0000' + model, { provider, model })
1981
+ if (found.size >= 500) break
1982
+ }
1983
+ return [...found.values()].slice(0, 500)
1984
+ }
1985
+
1840
1986
  function sessionView(sessionId) {
1841
1987
  if (sessionId === undefined || sessionId === '') return { official: null, third: null }
1842
1988
  // Own-property lookup only: session ids like `__proto__`, `constructor` or
@@ -1853,6 +1999,7 @@ function sessionView(sessionId) {
1853
1999
  }
1854
2000
  const official = bucketTotals(session.official)
1855
2001
  const third = bucketTotals(session.third)
2002
+ const thirdRoutes = thirdPartyRouteView(session.third)
1856
2003
  return {
1857
2004
  official: {
1858
2005
  tokens: official,
@@ -1860,7 +2007,12 @@ function sessionView(sessionId) {
1860
2007
  priced: session.official.priced === true,
1861
2008
  models: session.official.models,
1862
2009
  },
1863
- third: { tokens: third, models: session.third.models },
2010
+ third: {
2011
+ tokens: third,
2012
+ models: session.third.models,
2013
+ routes: thirdRoutes,
2014
+ customCosts: customCostTotals(thirdRoutes),
2015
+ },
1864
2016
  }
1865
2017
  }
1866
2018
 
@@ -1908,6 +2060,8 @@ function snapshotView(sessionId) {
1908
2060
  builtinOfficial: OFFICIAL_PROVIDER,
1909
2061
  official: [...store.officialProviders],
1910
2062
  known: store.knownProviders.filter((p) => p !== OFFICIAL_PROVIDER && !store.officialProviders.includes(p)),
2063
+ customPrices: [...store.customPrices],
2064
+ knownRoutes: knownThirdPartyRoutes(),
1911
2065
  },
1912
2066
  plans: planView(now),
1913
2067
  }
@@ -2191,6 +2345,7 @@ historyEventCount = store.history && store.history.events ? Object.keys(store.hi
2191
2345
  const body = await readBody(req)
2192
2346
  if (body === null || !Array.isArray(body.providers)) return json(res, 400, { ok: false, error: 'providers array is required' })
2193
2347
  store.officialProviders = normalizeProviderList(body.providers)
2348
+ store.customPrices = store.customPrices.filter((rule) => !store.officialProviders.includes(rule.provider))
2194
2349
  scheduleSave(ctx.logger)
2195
2350
  return json(res, 200, {
2196
2351
  ok: true,
@@ -2199,6 +2354,40 @@ historyEventCount = store.history && store.history.events ? Object.keys(store.hi
2199
2354
  })
2200
2355
  },
2201
2356
  })
2357
+ const disposeCustomPrices = ctx.webServer.register({
2358
+ kind: 'exact',
2359
+ path: '/api/wallet/custom-prices',
2360
+ handler: async (req, res) => {
2361
+ if (req.method === 'GET') {
2362
+ return json(res, 200, { ok: true, rules: [...store.customPrices], knownRoutes: knownThirdPartyRoutes() })
2363
+ }
2364
+ if (usageStorageLocked) return json(res, 423, { ok: false, error: 'usage-storage-locked' })
2365
+ if (req.method !== 'POST' && req.method !== 'DELETE') {
2366
+ return json(res, 405, { ok: false, error: 'method-not-allowed' })
2367
+ }
2368
+ const body = await readBody(req, 16_384)
2369
+ if (body === null) return json(res, 400, { ok: false, error: 'invalid-body' })
2370
+ if (req.method === 'POST') {
2371
+ const rule = normalizeCustomPriceRule(body.rule)
2372
+ if (rule === null) return json(res, 400, { ok: false, error: 'invalid-custom-price' })
2373
+ if (isOfficialProvider(rule.provider, store.officialProviders)) {
2374
+ return json(res, 400, { ok: false, error: 'official-provider-not-allowed' })
2375
+ }
2376
+ store.customPrices = normalizeCustomPrices([
2377
+ ...store.customPrices.filter((entry) => entry.provider !== rule.provider || entry.model !== rule.model),
2378
+ rule,
2379
+ ])
2380
+ scheduleSave(ctx.logger)
2381
+ return json(res, 200, { ok: true, rules: [...store.customPrices], knownRoutes: knownThirdPartyRoutes() })
2382
+ }
2383
+ const provider = boundedString(body.provider, 100)
2384
+ const model = boundedString(body.model, 160)
2385
+ if (provider === null || model === null) return json(res, 400, { ok: false, error: 'provider-and-model-required' })
2386
+ store.customPrices = store.customPrices.filter((entry) => entry.provider !== provider || entry.model !== model)
2387
+ scheduleSave(ctx.logger)
2388
+ return json(res, 200, { ok: true, rules: [...store.customPrices], knownRoutes: knownThirdPartyRoutes() })
2389
+ },
2390
+ })
2202
2391
  const disposeAccounts = ctx.webServer.register({
2203
2392
  kind: 'exact',
2204
2393
  path: '/api/wallet/accounts',
@@ -2285,6 +2474,7 @@ historyEventCount = store.history && store.history.events ? Object.keys(store.hi
2285
2474
  disposePlans()
2286
2475
  disposeAccounts()
2287
2476
  disposeProviders()
2477
+ disposeCustomPrices()
2288
2478
  disposeActivate()
2289
2479
  disposeRemove()
2290
2480
  clearInterval(balanceTimer)
package/lib/client.js CHANGED
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
23
23
 
24
24
  var POLL_MS = 15000
25
25
  // Keep in lockstep with package.json; a test enforces the sync.
26
- var WALLET_VERSION = '0.3.6'
26
+ var WALLET_VERSION = '0.3.8'
27
27
  var CONFIRM_KEY = 'dsh-wallet-recharge-confirmed'
28
28
  var CHIP_LAYOUT_KEY = 'dshw-chip-layout-v4'
29
29
  var PANEL_POS_KEY = 'dshw-panel-pos-v1'
@@ -610,6 +610,15 @@ window.__ModuleLoader__.load({
610
610
  '.dshw_settingChoiceCopy{min-width:0;display:flex;flex-direction:column;gap:1px}',
611
611
  '.dshw_settingChoiceCopy strong{font-size:11px;font-weight:550;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit)}',
612
612
  '.dshw_settingChoiceCopy span{font-size:9.5px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary,#667085));white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
613
+ '.dshw_priceForm{display:grid;grid-template-columns:minmax(120px,1.25fr) minmax(140px,1.5fr) 72px repeat(4,minmax(74px,.7fr)) auto;gap:7px;align-items:end;padding:10px 12px}',
614
+ '.dshw_priceField{display:grid;gap:4px;min-width:0}',
615
+ '.dshw_priceField>span{font-size:9.5px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary,#667085))}',
616
+ '.dshw_priceField .dshw_input,.dshw_priceField .dshw_select{box-sizing:border-box;width:100%;height:28px}',
617
+ '.dshw_priceRules{display:grid;gap:6px;padding:0 12px 11px}',
618
+ '.dshw_priceRule{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;padding:7px 9px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.15));border-radius:8px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,rgba(127,127,127,.04)))}',
619
+ '.dshw_priceRule strong,.dshw_priceRule span{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
620
+ '.dshw_priceRule strong{font-size:11px}.dshw_priceRule span{font-size:9.5px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary,#667085))}',
621
+ '@media(max-width:980px){.dshw_priceForm{grid-template-columns:repeat(2,minmax(0,1fr))}.dshw_priceForm>.dshw_btn{min-height:28px}}',
613
622
  '.dshw_switch{position:relative;flex:none;width:34px;height:20px;display:inline-flex;cursor:pointer}',
614
623
  '.dshw_switch>input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}',
615
624
  '.dshw_track{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l3,rgba(127,127,127,.3));transition:background-color .15s}',
@@ -908,6 +917,15 @@ window.__ModuleLoader__.load({
908
917
  }
909
918
  }
910
919
 
920
+ function customCostsText(costs) {
921
+ if (!costs || typeof costs !== 'object') return ''
922
+ return Object.keys(costs).sort().filter(function (currency) {
923
+ return Number.isFinite(costs[currency])
924
+ }).map(function (currency) {
925
+ return fmtCurrency(costs[currency], currency)
926
+ }).join(' + ')
927
+ }
928
+
911
929
  function selectBalanceInfo(balance) {
912
930
  if (!balance || !Array.isArray(balance.balances) || balance.balances.length === 0) return null
913
931
  var currency = typeof balance.currency === 'string' ? balance.currency.toUpperCase() : null
@@ -1400,7 +1418,8 @@ window.__ModuleLoader__.load({
1400
1418
 
1401
1419
  function historyHeatValue(day, metric) {
1402
1420
  if (!day) return 0
1403
- if (metric === 'cost') return Number.isFinite(day.cost) ? day.cost : 0
1421
+ if (metric === 'cost') return (Number.isFinite(day.cost) ? day.cost : 0)
1422
+ + (day.customCosts && Number.isFinite(day.customCosts.CNY) ? day.customCosts.CNY : 0)
1404
1423
  return Number.isFinite(day.totalTokens) ? day.totalTokens : 0
1405
1424
  }
1406
1425
 
@@ -1552,8 +1571,8 @@ window.__ModuleLoader__.load({
1552
1571
  tabIndex: day.calls > 0 || isSelected || isToday ? 0 : -1,
1553
1572
  className: 'dshw_historyCell dshw_historyCell' + level + (isToday ? ' dshw_historyCellToday' : '') + (isSelected ? ' dshw_historyCellSelected' : ''),
1554
1573
  style: { gridColumn: String(Math.floor((index + firstWeekday) / 7) + 1), gridRow: String(weekday + 1) },
1555
- title: day.date + ' · ' + fmtTokens(day.totalTokens) + ' token · ' + (day.cost === null ? '未定价' : historyMoney(day.cost)) + ' · ' + day.calls + ' 次调用',
1556
- 'aria-label': day.date + ',' + (metric === 'cost' ? (day.cost === null ? '未定价' : historyMoney(day.cost)) : fmtTokens(day.totalTokens) + ' token') + ',' + day.calls + ' 次调用',
1574
+ title: day.date + ' · ' + fmtTokens(day.totalTokens) + ' token · 官方 ' + (day.cost === null ? '未定价' : historyMoney(day.cost)) + (customCostsText(day.customCosts) ? ' · 三方估 ' + customCostsText(day.customCosts) : '') + ' · ' + day.calls + ' 次调用',
1575
+ 'aria-label': day.date + ',' + (metric === 'cost' ? ((day.cost === null ? '官方未定价' : historyMoney(day.cost)) + (customCostsText(day.customCosts) ? ',第三方估算 ' + customCostsText(day.customCosts) : '')) : fmtTokens(day.totalTokens) + ' token') + ',' + day.calls + ' 次调用',
1557
1576
  'aria-selected': isSelected,
1558
1577
  'aria-current': isToday ? 'date' : undefined,
1559
1578
  onClick: function () { loadHistory(day.date) }
@@ -1570,7 +1589,7 @@ window.__ModuleLoader__.load({
1570
1589
  : breakdown.map(function (row) {
1571
1590
  return React.createElement('div', { key: row.provider + ':' + row.model, className: 'dshw_historyDetailRow' },
1572
1591
  React.createElement('span', { className: 'dshw_historyDetailName', title: row.provider + ' · ' + row.model }, row.model),
1573
- React.createElement('span', null, fmtTokens(row.totalTokens) + ' · ' + row.calls + ' 次' + (row.cost === null ? '' : ' · ' + historyMoney(row.cost))))
1592
+ React.createElement('span', null, fmtTokens(row.totalTokens) + ' · ' + row.calls + ' 次' + (row.cost === null ? '' : ' · ' + historyMoney(row.cost)) + (row.customCost ? ' · 估 ' + fmtCurrency(row.customCost.cost, row.customCost.currency) : '')))
1574
1593
  })
1575
1594
  body = React.createElement(React.Fragment, null,
1576
1595
  React.createElement('div', { className: 'dshw_historySummary' },
@@ -1578,6 +1597,7 @@ window.__ModuleLoader__.load({
1578
1597
  React.createElement('div', null, React.createElement('span', null, '今天'), React.createElement('strong', null, fmtTokens(today.totalTokens))),
1579
1598
  React.createElement('div', null, React.createElement('span', null, '本月'), React.createElement('strong', null, fmtTokens(month.totalTokens))),
1580
1599
  React.createElement('div', null, React.createElement('span', null, '缓存命中'), React.createElement('strong', null, history.summary.cacheHitRate === null || history.summary.cacheHitRate === undefined ? '--' : history.summary.cacheHitRate + '%'))),
1600
+ customCostsText(total.customCosts) ? React.createElement('div', { className: 'dshw_historyLegend' }, '第三方按当前自定义价估算:' + customCostsText(total.customCosts)) : null,
1581
1601
  React.createElement('div', { className: 'dshw_historyMetricToggle', role: 'group', 'aria-label': '历史热力图指标' },
1582
1602
  React.createElement('button', { type: 'button', className: metric === 'tokens' ? 'dshw_historyMetricActive' : '', 'aria-pressed': metric === 'tokens', onClick: function () { setMetric('tokens') } }, 'Token'),
1583
1603
  React.createElement('button', { type: 'button', className: metric === 'cost' ? 'dshw_historyMetricActive' : '', 'aria-pressed': metric === 'cost', onClick: function () { setMetric('cost') } }, '费用')),
@@ -1869,6 +1889,12 @@ window.__ModuleLoader__.load({
1869
1889
  var [nameDraft, setNameDraft] = React.useState('')
1870
1890
  var [keyDraft, setKeyDraft] = React.useState('')
1871
1891
  var [switchingId, setSwitchingId] = React.useState(null)
1892
+ var [priceRules, setPriceRules] = React.useState([])
1893
+ var [knownPriceRoutes, setKnownPriceRoutes] = React.useState([])
1894
+ var [priceNotice, setPriceNotice] = React.useState(null)
1895
+ var [priceDraft, setPriceDraft] = React.useState({
1896
+ provider: '', model: '', currency: 'CNY', input: '', cacheRead: '', cacheWrite: '', output: ''
1897
+ })
1872
1898
  var [visibility, setVisibility] = React.useState(function () {
1873
1899
  try {
1874
1900
  var saved = compatibility.storage.getItem(DATA_VISIBILITY_KEY)
@@ -1974,6 +2000,9 @@ window.__ModuleLoader__.load({
1974
2000
  fetch('/api/wallet/snapshot').then(function (resp) { return resp.json() }).then(function (json) {
1975
2001
  if (!stopped && json && json.ok) {
1976
2002
  setSnapshot(json)
2003
+ var providerState = json.providers || {}
2004
+ setPriceRules(Array.isArray(providerState.customPrices) ? providerState.customPrices : [])
2005
+ setKnownPriceRoutes(Array.isArray(providerState.knownRoutes) ? providerState.knownRoutes : [])
1977
2006
  setThresholdDraft(json.threshold !== undefined && json.threshold !== null ? json.threshold.toFixed(2) : '')
1978
2007
  }
1979
2008
  }).catch(function () { /* ignore */ })
@@ -2160,6 +2189,85 @@ window.__ModuleLoader__.load({
2160
2189
  }).catch(function () { setThresholdNotice('\u4fdd\u5b58\u5931\u8d25') })
2161
2190
  }
2162
2191
 
2192
+ function applyCustomPriceResponse(json) {
2193
+ if (!json || json.ok !== true) return false
2194
+ setPriceRules(Array.isArray(json.rules) ? json.rules : [])
2195
+ setKnownPriceRoutes(Array.isArray(json.knownRoutes) ? json.knownRoutes : [])
2196
+ return true
2197
+ }
2198
+
2199
+ function saveCustomPrice() {
2200
+ if (usageLocked) { setPriceNotice('用量账本已锁定,无法保存'); return }
2201
+ var numeric = ['input', 'cacheRead', 'cacheWrite', 'output'].reduce(function (out, key) {
2202
+ var value = Number.parseFloat(priceDraft[key])
2203
+ out[key] = Number.isFinite(value) && value >= 0 ? value : null
2204
+ return out
2205
+ }, {})
2206
+ if (!priceDraft.provider.trim() || !priceDraft.model.trim() || Object.values(numeric).some(function (value) { return value === null })) {
2207
+ setPriceNotice('请填写 Provider、模型及四项非负价格')
2208
+ return
2209
+ }
2210
+ setPriceNotice('正在保存…')
2211
+ fetch('/api/wallet/custom-prices', {
2212
+ method: 'POST',
2213
+ headers: { 'content-type': 'application/json' },
2214
+ body: JSON.stringify({ rule: {
2215
+ provider: priceDraft.provider.trim(),
2216
+ model: priceDraft.model.trim(),
2217
+ currency: priceDraft.currency,
2218
+ input: numeric.input,
2219
+ cacheRead: numeric.cacheRead,
2220
+ cacheWrite: numeric.cacheWrite,
2221
+ output: numeric.output
2222
+ } })
2223
+ }).then(function (resp) { return resp.json().then(function (json) { return { ok: resp.ok, json: json } }) }).then(function (result) {
2224
+ if (!result.ok || !applyCustomPriceResponse(result.json)) throw new Error(result.json && result.json.error ? result.json.error : 'save-failed')
2225
+ setPriceNotice('已保存;本会话与历史账本按当前自定义价重新估算')
2226
+ }).catch(function (error) {
2227
+ setPriceNotice(error && error.message === 'official-provider-not-allowed' ? '官方计费 Provider 不能设置第三方价格' : '保存失败,请检查填写内容')
2228
+ })
2229
+ }
2230
+
2231
+ function removeCustomPrice(rule) {
2232
+ if (usageLocked) return
2233
+ setPriceNotice('正在删除…')
2234
+ fetch('/api/wallet/custom-prices', {
2235
+ method: 'DELETE',
2236
+ headers: { 'content-type': 'application/json' },
2237
+ body: JSON.stringify({ provider: rule.provider, model: rule.model })
2238
+ }).then(function (resp) { return resp.json().then(function (json) { return { ok: resp.ok, json: json } }) }).then(function (result) {
2239
+ if (!result.ok || !applyCustomPriceResponse(result.json)) throw new Error('delete-failed')
2240
+ setPriceNotice('已删除价格规则')
2241
+ }).catch(function () { setPriceNotice('删除失败') })
2242
+ }
2243
+
2244
+ function editCustomPrice(rule) {
2245
+ setPriceDraft({
2246
+ provider: rule.provider,
2247
+ model: rule.model,
2248
+ currency: rule.currency,
2249
+ input: String(rule.input),
2250
+ cacheRead: String(rule.cacheRead),
2251
+ cacheWrite: String(rule.cacheWrite),
2252
+ output: String(rule.output)
2253
+ })
2254
+ setPriceNotice('已载入,可修改后保存')
2255
+ }
2256
+
2257
+ function useCurrentRouteForPrice() {
2258
+ if (!settingsModelSelection || typeof settingsModelSelection.provider !== 'string' || typeof settingsModelSelection.model !== 'string') {
2259
+ setPriceNotice('当前会话尚未选择可识别的模型')
2260
+ return
2261
+ }
2262
+ setPriceDraft(function (current) {
2263
+ return Object.assign({}, current, {
2264
+ provider: settingsModelSelection.provider,
2265
+ model: settingsModelSelection.model
2266
+ })
2267
+ })
2268
+ setPriceNotice('已填入当前会话的 Provider 与模型')
2269
+ }
2270
+
2163
2271
  function reloadAccounts() {
2164
2272
  fetch('/api/wallet/accounts').then(function (resp) { return resp.json() }).then(function (json) {
2165
2273
  if (json && json.ok) {
@@ -2597,6 +2705,65 @@ window.__ModuleLoader__.load({
2597
2705
  rows.push(React.createElement('div', { key: 'pr-card', className: 'dshw_setCard', style: { display: 'block' } }, providerRows))
2598
2706
  }
2599
2707
 
2708
+ // —— 第三方 API 自定义价格(Issue #36) ——
2709
+ rows.push(React.createElement('div', { key: 'cp-head', className: 'dshw_accountHeader', style: { marginTop: '8px' } },
2710
+ React.createElement('span', null,
2711
+ React.createElement('span', { className: 'dshw_title' }, '第三方 API 自定义价格'),
2712
+ React.createElement('span', { className: 'dshw_muted', style: { display: 'block', marginTop: '2px', fontSize: '10px' } }, '每 100 万 Token;仅作本机估算,不代表第三方账单')),
2713
+ React.createElement('button', {
2714
+ type: 'button', className: 'dshw_btn',
2715
+ disabled: settingsProviderMode.kind !== 'third',
2716
+ title: settingsProviderMode.kind === 'third' ? '填入当前会话选择的 Provider 与模型' : '请先切换到第三方模型',
2717
+ onClick: useCurrentRouteForPrice
2718
+ }, '使用当前模型')))
2719
+ var providerOptions = Array.from(new Set(knownPriceRoutes.map(function (route) { return route && route.provider }).filter(Boolean)))
2720
+ var modelOptions = Array.from(new Set(knownPriceRoutes.filter(function (route) {
2721
+ return route && (!priceDraft.provider || route.provider === priceDraft.provider)
2722
+ }).map(function (route) { return route.model }).filter(Boolean)))
2723
+ function updatePriceDraft(key, value) {
2724
+ setPriceDraft(function (current) { var next = Object.assign({}, current); next[key] = key === 'currency' ? String(value).toUpperCase().slice(0, 3) : value; return next })
2725
+ }
2726
+ function priceField(key, label, type, options) {
2727
+ return React.createElement('label', { key: key, className: 'dshw_priceField' },
2728
+ React.createElement('span', null, label),
2729
+ type === 'select'
2730
+ ? React.createElement('select', { className: 'dshw_select', value: priceDraft[key], 'aria-label': label, onChange: function (event) { updatePriceDraft(key, event.target.value) } },
2731
+ options.map(function (option) { return React.createElement('option', { key: option, value: option }, option) }))
2732
+ : React.createElement('input', {
2733
+ className: 'dshw_input', type: type, min: type === 'number' ? '0' : undefined,
2734
+ max: type === 'number' ? '1000000' : undefined, step: type === 'number' ? '0.000001' : undefined,
2735
+ list: key === 'provider' ? 'dshw-price-providers' : key === 'model' ? 'dshw-price-models' : undefined,
2736
+ value: priceDraft[key], 'aria-label': label,
2737
+ onChange: function (event) { updatePriceDraft(key, event.target.value) }
2738
+ }))
2739
+ }
2740
+ var priceForm = React.createElement('div', { className: 'dshw_priceForm' },
2741
+ priceField('provider', 'Provider ID', 'text'),
2742
+ priceField('model', '模型 ID', 'text'),
2743
+ priceField('currency', '币种', 'text'),
2744
+ priceField('input', '输入', 'number'),
2745
+ priceField('cacheRead', '缓存读', 'number'),
2746
+ priceField('cacheWrite', '缓存写', 'number'),
2747
+ priceField('output', '输出', 'number'),
2748
+ React.createElement('button', { type: 'button', className: 'dshw_btn dshw_btnPrimary', disabled: usageLocked, onClick: saveCustomPrice }, '保存'),
2749
+ React.createElement('datalist', { id: 'dshw-price-providers' }, providerOptions.map(function (provider) { return React.createElement('option', { key: provider, value: provider }) })),
2750
+ React.createElement('datalist', { id: 'dshw-price-models' }, modelOptions.map(function (model) { return React.createElement('option', { key: model, value: model }) })))
2751
+ var priceRuleRows = priceRules.map(function (rule) {
2752
+ return React.createElement('div', { key: rule.provider + ':' + rule.model, className: 'dshw_priceRule' },
2753
+ React.createElement('span', null,
2754
+ React.createElement('strong', { title: rule.provider + ' · ' + rule.model }, rule.provider + ' · ' + rule.model),
2755
+ React.createElement('span', null, rule.currency + '/1M · 入 ' + rule.input + ' · 缓读 ' + rule.cacheRead + ' · 缓写 ' + rule.cacheWrite + ' · 出 ' + rule.output)),
2756
+ React.createElement('span', { className: 'dshw_settingsInline' },
2757
+ React.createElement('button', { type: 'button', className: 'dshw_btn', onClick: function () { editCustomPrice(rule) } }, '编辑'),
2758
+ React.createElement('button', { type: 'button', className: 'dshw_btn', disabled: usageLocked, onClick: function () { removeCustomPrice(rule) } }, '删除')))
2759
+ })
2760
+ rows.push(React.createElement('div', { key: 'cp-card', className: 'dshw_setCard wide', style: { display: 'block' } },
2761
+ priceForm,
2762
+ priceNotice ? React.createElement('div', { className: 'dshw_muted', role: 'status', style: { padding: '0 12px 8px' } }, priceNotice) : null,
2763
+ priceRuleRows.length > 0
2764
+ ? React.createElement('div', { className: 'dshw_priceRules' }, priceRuleRows)
2765
+ : React.createElement('div', { className: 'dshw_muted', style: { padding: '0 12px 11px' } }, '暂无规则;填写后即可看到第三方费用估算')))
2766
+
2600
2767
  rows.push(React.createElement(PlanUsagePanel, { key: 'plans', compact: false }))
2601
2768
  rows.push(React.createElement(UsageHistoryPanel, { key: 'history', sessionId: null, alwaysOpen: true }))
2602
2769
 
@@ -4541,6 +4708,13 @@ window.__ModuleLoader__.load({
4541
4708
  var third = session.third || {}
4542
4709
  var officialTokens = official.tokens ? totalTokens(official.tokens) : 0
4543
4710
  var thirdTokens = third.tokens ? totalTokens(third.tokens) : 0
4711
+ var thirdCustomText = customCostsText(third.customCosts)
4712
+ var activeThirdRoute = activeProviderMode.kind === 'third' && Array.isArray(third.routes)
4713
+ ? third.routes.find(function (route) { return route.provider === activeProviderMode.provider && route.model === activeProviderMode.model })
4714
+ : null
4715
+ var activeThirdCostText = activeThirdRoute && activeThirdRoute.customCost
4716
+ ? fmtCurrency(activeThirdRoute.customCost.cost, activeThirdRoute.customCost.currency)
4717
+ : ''
4544
4718
  var activePlanSource = activeProviderMode.kind === 'zai'
4545
4719
  ? planSourceForProvider(snapshot, activeProviderMode.provider)
4546
4720
  : null
@@ -4589,10 +4763,10 @@ window.__ModuleLoader__.load({
4589
4763
  React.createElement('span', { className: 'dshw_homePrimaryLabel' }, '5h 剩 '),
4590
4764
  React.createElement('span', { className: 'dshw_homePrimaryValue' }, planTokenRemaining)))
4591
4765
  } else if (activeProviderMode.kind === 'third') {
4592
- if (chipVertical) chipTextParts.push(verticalMetric('third', '本场', fmtTokens(thirdTokens)))
4766
+ if (chipVertical) chipTextParts.push(verticalMetric('third', activeThirdCostText ? '本估' : '本场', activeThirdCostText || fmtTokens(thirdTokens)))
4593
4767
  else chipTextParts.push(React.createElement('span', { key: 'third', className: 'dshw_homePrimary' },
4594
- React.createElement('span', { className: 'dshw_homePrimaryLabel' }, '本场 '),
4595
- React.createElement('span', { className: 'dshw_homePrimaryValue' }, fmtTokens(thirdTokens))))
4768
+ React.createElement('span', { className: 'dshw_homePrimaryLabel' }, activeThirdCostText ? '本估 ' : '本场 '),
4769
+ React.createElement('span', { className: 'dshw_homePrimaryValue' }, activeThirdCostText || fmtTokens(thirdTokens))))
4596
4770
  } else {
4597
4771
  if (chipVertical) chipTextParts.push(verticalMetric('bal', '余额', balanceText))
4598
4772
  else chipTextParts.push(React.createElement('span', { key: 'bal', className: 'dshw_balanceText dshw_homePrimary' },
@@ -4617,10 +4791,12 @@ window.__ModuleLoader__.load({
4617
4791
  if (chipVertical) {
4618
4792
  chipTextParts.push(verticalMetric('provider', '模型', thirdProviderName))
4619
4793
  chipTextParts.push(verticalMetric('third', '本场', fmtTokens(thirdTokens)))
4794
+ if (activeThirdCostText) chipTextParts.push(verticalMetric('third-cost', '估算', activeThirdCostText))
4620
4795
  } else {
4621
4796
  chipTextParts.push(React.createElement('span', { key: 'provider', className: 'dshw_homePrimary' },
4622
4797
  React.createElement('span', { className: 'dshw_homePrimaryValue' }, thirdProviderName)))
4623
4798
  chipTextParts.push(React.createElement('span', { key: 'third' }, '本场 ' + fmtTokens(thirdTokens)))
4799
+ if (activeThirdCostText) chipTextParts.push(React.createElement('span', { key: 'third-cost' }, '估 ' + activeThirdCostText))
4624
4800
  }
4625
4801
  } else if (chipVertical) {
4626
4802
  if (showDeepSeek) {
@@ -4738,6 +4914,11 @@ window.__ModuleLoader__.load({
4738
4914
  thirdRows.push(React.createElement('div', { key: 't-t', className: 'dshw_row' },
4739
4915
  React.createElement('span', { className: 'dshw_muted' }, '\u7b2c\u4e09\u65b9 token'),
4740
4916
  React.createElement('span', null, '\u8f93\u5165 ' + fmtTokens(third.tokens && third.tokens.input) + ' \u00b7 \u7f13\u5b58\u8bfb ' + fmtTokens(third.tokens && third.tokens.cacheRead) + ' \u00b7 \u8f93\u51fa ' + fmtTokens(third.tokens && third.tokens.output))))
4917
+ if (thirdCustomText) {
4918
+ thirdRows.push(React.createElement('div', { key: 't-c', className: 'dshw_row' },
4919
+ React.createElement('span', { className: 'dshw_muted' }, '自定义估算'),
4920
+ React.createElement('span', { title: '按当前自定义价格计算,不代表第三方账单' }, thirdCustomText)))
4921
+ }
4741
4922
 
4742
4923
  var thresholdInput = React.createElement('input', {
4743
4924
  className: 'dshw_input',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "deepseek-harness-wallet",
3
- "description": "Local-first DeepSeek Harness control center for balance monitoring, usage accounting, a 365-day local token ledger, Z.ai Coding Plan quota monitoring, official pricing sync, peak/off-peak status, encrypted multi-account switching, health checks, completion reminders, recharge, and host-gated session controls.",
4
- "version": "0.3.6",
3
+ "description": "Local-first DeepSeek Harness control center for balance monitoring, usage accounting, custom third-party API pricing, a 365-day local token ledger, Z.ai Coding Plan quotas, official pricing sync, encrypted multi-account switching, reminders, recharge, and host-gated session controls.",
4
+ "version": "0.3.8",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -56,7 +56,6 @@
56
56
  "client": {
57
57
  "platform": "web",
58
58
  "inject": [
59
- "@deepseek-ai/dsh-client-runtime",
60
59
  "@deepseek-ai/dsh-client-ui-slots",
61
60
  "@deepseek-ai/dsh-client-ui-conversation",
62
61
  "@deepseek-ai/dsh-client-ui-model-selection"