deepseek-harness-wallet 0.3.8 → 0.3.9
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 +7 -0
- package/README.md +6 -6
- package/index.js +244 -30
- package/lib/client.js +178 -12
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.3.9 - 2026-09-04
|
|
6
|
+
|
|
7
|
+
- 第三方 API 自定义价格新增分时计价(Issue #37):每个精确 Provider/模型可在基础价之上配置多个 IANA 时区、适用星期和起止时间规则,并为输入、缓存读、缓存写、输出分别定价;支持跨午夜时段,当前命中的价格段会在设置和钱包详情中显示。 / Added time-of-use custom third-party API pricing (Issue #37): each exact Provider/model route can define multiple IANA-timezone, weekday, and start/end windows over a base rate, with separate input, cache-read, cache-write, and output prices; cross-midnight windows are supported and the active price segment is shown in Settings and wallet details.
|
|
8
|
+
- 分时规则会拒绝无效时区、日期、时间和重叠范围;365 天账本与当前会话按每条保留用量的发生时间重新估算,缺少时间戳的旧聚合或已清除历史安全回退基础价,且 v0.3.8 固定价格规则无需迁移配置。 / Time-window validation rejects invalid timezones, weekdays, times, and overlaps; the 365-day ledger and live session reprice each retained usage event by occurrence time, while legacy aggregates or cleared history without timestamps safely use the base rate and existing v0.3.8 fixed-price rules remain compatible.
|
|
9
|
+
- 将峰谷时钟渲染测试固定到明确的工作日时间,避免测试结果随执行当天是否周末而波动。 / Pinned the peak-clock rendering test to an explicit weekday so its result no longer depends on the day the suite runs.
|
|
10
|
+
- Vision Toolkit 透明包装的 Z.ai 路由会还原为对应套餐 Provider,模型切换后继续显示 5 小时 Token 与月度 MCP 摘要,而不会误显示为普通第三方价格;DeepSeek 与其他包装路由的既有分桶规则保持不变。 / Z.ai routes transparently wrapped by Vision Toolkit now resolve back to their plan provider so model switches retain five-hour Token and monthly MCP summaries instead of looking like ordinary third-party pricing; existing DeepSeek and other wrapper classification remains unchanged.
|
|
11
|
+
|
|
5
12
|
## 0.3.8 - 2026-09-02
|
|
6
13
|
|
|
7
14
|
- 新增第三方 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.
|
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.
|
|
17
|
+
> **Version:** v0.3.9 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
|
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
- **v4 peak/off-peak ring clock** — a resident 24-hour sidebar footer widget for `v4-flash`, `v4-pro`, and `v4-flash-vision-exp`. Weekday peak windows are 09:00–12:00 and 14:00–18:00 Beijing time. After Friday 18:00 the card previews “weekend all-day off-peak”; Saturday and Sunday name the current all-day off-peak rule; Monday before 09:00 shows the time remaining to enter peak. Optional notifications treat Friday 18:00 through Monday 09:00 as one continuous off-peak period.
|
|
30
30
|
- **Official pricing sync** — periodically checks the official DeepSeek pricing page and applies only a fully validated table. Network failures retain the last validated rule (or the built-in rule before the first successful sync); an unrecognized page structure is marked for review instead of silently changing billing.
|
|
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
|
-
- **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.
|
|
32
|
+
- **Provider-aware composer surfaces** — the chip and sidebar clock follow the session's selected provider/model. Z.ai—including transparent `vision-toolkit-` variants—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
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
|
|
35
|
+
- **Custom third-party pricing** — enter an exact Provider/model route, currency, and base per-million input/cache-read/cache-write/output rates, then optionally add multiple IANA-timezone, weekday, and cross-midnight pricing windows. Current-session and 365-day ledger costs are recalculated locally by occurrence time and clearly labeled as user-defined estimates, never as provider balances or invoices.
|
|
36
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.
|
|
37
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).
|
|
38
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.
|
|
@@ -74,7 +74,7 @@ Details: [compatibility](#browser-desktop-and-os-compatibility) · [data and tru
|
|
|
74
74
|
|
|
75
75
|
## Install
|
|
76
76
|
|
|
77
|
-
From npm (published stable v0.3.
|
|
77
|
+
From npm (published stable v0.3.9):
|
|
78
78
|
|
|
79
79
|
```sh
|
|
80
80
|
dsh plugin --profile web add deepseek-harness-wallet
|
|
@@ -150,7 +150,7 @@ For buildable DSH hosts, the npm package and repository include a versioned [Age
|
|
|
150
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. |
|
|
151
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. |
|
|
152
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. |
|
|
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. |
|
|
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 and each retained event's occurrence time; aggregate usage without a retained timestamp safely falls back to the base rate. |
|
|
154
154
|
| Local settings | Layout, scale, visibility, reminder, and panel settings stay in browser-compatible local storage. |
|
|
155
155
|
| Permanent deletion | Opt-in and host-gated. The wallet never advertises the action unless the host implements the matching session deletion path. |
|
|
156
156
|
| Model surface | No tools registered, no prompt injection, zero token cost. |
|
|
@@ -174,7 +174,7 @@ Historical deepseek-chat and deepseek-reasoner records retain their original fla
|
|
|
174
174
|
|
|
175
175
|
- [x] 365-day Token heatmap and rebuildable local usage ledger
|
|
176
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
|
|
177
|
+
- [x] User-defined third-party Provider/model base and time-of-use pricing
|
|
178
178
|
- [ ] Additional provider price/balance adapters only after real-account validation
|
|
179
179
|
|
|
180
180
|
## License
|
package/index.js
CHANGED
|
@@ -28,12 +28,13 @@ export const name = 'wallet'
|
|
|
28
28
|
export const inject = ['webServer', 'credentials']
|
|
29
29
|
|
|
30
30
|
const OFFICIAL_PROVIDER = 'deepseek-official'
|
|
31
|
+
const TRANSPARENT_PROVIDER_PREFIX = 'vision-toolkit-'
|
|
31
32
|
// Subscription-plan routes are independent providers. They may contribute to
|
|
32
33
|
// the generic third-party token ledger, but they must never be promoted into
|
|
33
34
|
// DeepSeek's official paid-API bucket by the wrapper-provider alias control.
|
|
34
35
|
const PLAN_PROVIDER_IDS = new Set(PLAN_ADAPTERS.map((adapter) => adapter.provider))
|
|
35
36
|
const RECHARGE_URL = 'https://platform.deepseek.com/top_up'
|
|
36
|
-
const PLUGIN_VERSION = '0.3.
|
|
37
|
+
const PLUGIN_VERSION = '0.3.9'
|
|
37
38
|
const PRICING_SOURCE_URL = 'https://api-docs.deepseek.com/zh-cn/quick_start/pricing/'
|
|
38
39
|
const PRICING_SYNC_INTERVAL_MS = 6 * 60 * 60_000
|
|
39
40
|
const PRICING_SYNC_TIMEOUT_MS = 8_000
|
|
@@ -49,14 +50,21 @@ const ACCOUNTS_CRYPTO_VERSION = 1
|
|
|
49
50
|
const CREDENTIAL_REF = 'DEEPSEEK_API_KEY'
|
|
50
51
|
const DEFAULT_THRESHOLD = 5
|
|
51
52
|
const BALANCE_REFRESH_MS = 60_000
|
|
52
|
-
const STORE_VERSION =
|
|
53
|
+
const STORE_VERSION = 7
|
|
53
54
|
const HISTORY_VERSION = 1
|
|
54
55
|
const HISTORY_RETENTION_DAYS = 365
|
|
55
56
|
const HISTORY_MAX_EVENTS = 20_000
|
|
56
57
|
const CUSTOM_PRICE_MAX_RULES = 100
|
|
57
58
|
const CUSTOM_PRICE_MAX_RATE = 1_000_000
|
|
59
|
+
const CUSTOM_PRICE_MAX_WINDOWS = 24
|
|
58
60
|
const HISTORY_TIMEZONE = 'Asia/Shanghai'
|
|
59
61
|
const DAY_MS = 86_400_000
|
|
62
|
+
const MINUTES_PER_DAY = 1_440
|
|
63
|
+
const MINUTES_PER_WEEK = 10_080
|
|
64
|
+
const CUSTOM_WEEKDAY_ORDER = [1, 2, 3, 4, 5, 6, 0]
|
|
65
|
+
const WEEKDAY_NUMBER = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 }
|
|
66
|
+
const customTimeFormatters = new Map()
|
|
67
|
+
const customTimezoneNames = new Map()
|
|
60
68
|
const PLAN_REFRESH_MS = 5 * 60_000
|
|
61
69
|
const PLAN_STALE_MS = 15 * 60_000
|
|
62
70
|
const PLAN_REFRESH_TIMEOUT_MS = 12_000
|
|
@@ -797,7 +805,8 @@ function publicHistoryAggregate(aggregate, hasOfficial = true) {
|
|
|
797
805
|
}
|
|
798
806
|
}
|
|
799
807
|
|
|
800
|
-
function historyAggregate(events) {
|
|
808
|
+
function historyAggregate(events, customRules = null) {
|
|
809
|
+
const ruleMap = customRules || customPriceRuleMap(store.customPrices)
|
|
801
810
|
const official = emptyHistoryAggregate()
|
|
802
811
|
const third = emptyHistoryAggregate()
|
|
803
812
|
const models = new Map()
|
|
@@ -811,10 +820,18 @@ function historyAggregate(events) {
|
|
|
811
820
|
const key = event.provider + '\u0000' + event.model
|
|
812
821
|
let model = models.get(key)
|
|
813
822
|
if (model === undefined) {
|
|
814
|
-
model = { provider: event.provider, model: event.model, official: event.official, ...emptyHistoryAggregate() }
|
|
823
|
+
model = { provider: event.provider, model: event.model, official: event.official, customCost: null, ...emptyHistoryAggregate() }
|
|
815
824
|
models.set(key, model)
|
|
816
825
|
}
|
|
817
826
|
addHistoryAggregate(model, event)
|
|
827
|
+
if (!event.official) {
|
|
828
|
+
const rule = ruleMap.get(event.provider + '\u0000' + event.model)
|
|
829
|
+
const priced = rule === undefined ? null : customCostWithRule(rule, event.usage, event.occurredAt)
|
|
830
|
+
if (priced !== null) {
|
|
831
|
+
if (model.customCost === null) model.customCost = { cost: 0, currency: priced.currency }
|
|
832
|
+
model.customCost.cost += priced.cost
|
|
833
|
+
}
|
|
834
|
+
}
|
|
818
835
|
}
|
|
819
836
|
const total = emptyHistoryAggregate()
|
|
820
837
|
for (const source of [official, third]) {
|
|
@@ -825,13 +842,12 @@ function historyAggregate(events) {
|
|
|
825
842
|
total.priced = official.priced
|
|
826
843
|
const publicModels = [...models.values()].map((model) => {
|
|
827
844
|
const aggregate = publicHistoryAggregate(model, model.official)
|
|
828
|
-
const priced = model.official ? null : customCostOf(model.provider, model.model, model.tokens, store.customPrices)
|
|
829
845
|
return {
|
|
830
846
|
provider: model.provider,
|
|
831
847
|
model: model.model,
|
|
832
848
|
official: model.official,
|
|
833
849
|
...aggregate,
|
|
834
|
-
customCost:
|
|
850
|
+
customCost: model.official || model.customCost === null ? null : { ...model.customCost },
|
|
835
851
|
}
|
|
836
852
|
})
|
|
837
853
|
const customCosts = customCostTotals(publicModels)
|
|
@@ -848,6 +864,7 @@ export function historyView({ days = HISTORY_RETENTION_DAYS, date = null, sessio
|
|
|
848
864
|
const window = historyWindow(atMs, days)
|
|
849
865
|
const sourceEvents = store.history && store.history.events ? Object.values(store.history.events) : []
|
|
850
866
|
const filtered = sourceEvents.filter((event) => event.day >= window.from && event.day <= window.to && (sessionId === null || event.sessionId === sessionId))
|
|
867
|
+
const customRules = customPriceRuleMap(store.customPrices)
|
|
851
868
|
const byDay = new Map()
|
|
852
869
|
for (const event of filtered) {
|
|
853
870
|
if (!byDay.has(event.day)) byDay.set(event.day, [])
|
|
@@ -857,7 +874,7 @@ export function historyView({ days = HISTORY_RETENTION_DAYS, date = null, sessio
|
|
|
857
874
|
for (let index = 0; index < window.days; index += 1) {
|
|
858
875
|
const day = shiftHistoryDay(window.from, index)
|
|
859
876
|
const events = byDay.get(day) || []
|
|
860
|
-
const aggregate = historyAggregate(events)
|
|
877
|
+
const aggregate = historyAggregate(events, customRules)
|
|
861
878
|
const dayStart = historyDayStartMs(day)
|
|
862
879
|
daysOut.push({
|
|
863
880
|
date: day,
|
|
@@ -868,12 +885,12 @@ export function historyView({ days = HISTORY_RETENTION_DAYS, date = null, sessio
|
|
|
868
885
|
cacheHitRate: aggregate.cacheHitRate,
|
|
869
886
|
})
|
|
870
887
|
}
|
|
871
|
-
const total = historyAggregate(filtered)
|
|
888
|
+
const total = historyAggregate(filtered, customRules)
|
|
872
889
|
const todayEvents = byDay.get(window.to) || []
|
|
873
890
|
const monthPrefix = window.to.slice(0, 7)
|
|
874
891
|
const monthEvents = filtered.filter((event) => event.day.startsWith(monthPrefix))
|
|
875
892
|
const selectedEvents = typeof date === 'string' ? (byDay.get(date) || []) : []
|
|
876
|
-
const selectedAggregate = typeof date === 'string' && date >= window.from && date <= window.to ? historyAggregate(selectedEvents) : null
|
|
893
|
+
const selectedAggregate = typeof date === 'string' && date >= window.from && date <= window.to ? historyAggregate(selectedEvents, customRules) : null
|
|
877
894
|
return {
|
|
878
895
|
ok: true,
|
|
879
896
|
timezone: HISTORY_TIMEZONE,
|
|
@@ -882,8 +899,8 @@ export function historyView({ days = HISTORY_RETENTION_DAYS, date = null, sessio
|
|
|
882
899
|
window,
|
|
883
900
|
summary: {
|
|
884
901
|
total: total.total,
|
|
885
|
-
today: historyAggregate(todayEvents).total,
|
|
886
|
-
month: historyAggregate(monthEvents).total,
|
|
902
|
+
today: historyAggregate(todayEvents, customRules).total,
|
|
903
|
+
month: historyAggregate(monthEvents, customRules).total,
|
|
887
904
|
cacheHitRate: total.cacheHitRate,
|
|
888
905
|
},
|
|
889
906
|
days: daysOut,
|
|
@@ -912,7 +929,9 @@ export function normalizeProviderList(value) {
|
|
|
912
929
|
}
|
|
913
930
|
|
|
914
931
|
export function isPlanProvider(provider) {
|
|
915
|
-
|
|
932
|
+
if (typeof provider !== 'string') return false
|
|
933
|
+
const upstream = provider.startsWith(TRANSPARENT_PROVIDER_PREFIX) ? provider.slice(TRANSPARENT_PROVIDER_PREFIX.length) : provider
|
|
934
|
+
return PLAN_PROVIDER_IDS.has(upstream)
|
|
916
935
|
}
|
|
917
936
|
|
|
918
937
|
export function isOfficialProvider(provider, extraProviders) {
|
|
@@ -926,6 +945,107 @@ function customPriceRate(value) {
|
|
|
926
945
|
return Math.round(value * 1_000_000) / 1_000_000
|
|
927
946
|
}
|
|
928
947
|
|
|
948
|
+
function normalizeCustomTimezone(value) {
|
|
949
|
+
if (typeof value !== 'string' || value === '' || value.length > 80 || /[\u0000-\u001f\u007f]/.test(value)) return null
|
|
950
|
+
if (customTimezoneNames.has(value)) return customTimezoneNames.get(value)
|
|
951
|
+
try {
|
|
952
|
+
const timezone = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone
|
|
953
|
+
if (customTimezoneNames.size >= 200) customTimezoneNames.clear()
|
|
954
|
+
customTimezoneNames.set(value, timezone)
|
|
955
|
+
return timezone
|
|
956
|
+
} catch {
|
|
957
|
+
return null
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function customTimeMinute(value) {
|
|
962
|
+
if (typeof value !== 'string') return null
|
|
963
|
+
const match = value.match(/^([01]\d|2[0-3]):([0-5]\d)$/)
|
|
964
|
+
return match === null ? null : Number(match[1]) * 60 + Number(match[2])
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function normalizeCustomPriceWindow(value) {
|
|
968
|
+
const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
969
|
+
const label = boundedString(source.label, 40)
|
|
970
|
+
const start = customTimeMinute(source.start)
|
|
971
|
+
const end = customTimeMinute(source.end)
|
|
972
|
+
const rawDays = Array.isArray(source.days) ? source.days : []
|
|
973
|
+
const days = CUSTOM_WEEKDAY_ORDER.filter((day) => rawDays.includes(day))
|
|
974
|
+
const input = customPriceRate(source.input)
|
|
975
|
+
const cacheRead = customPriceRate(source.cacheRead)
|
|
976
|
+
const cacheWrite = customPriceRate(source.cacheWrite)
|
|
977
|
+
const output = customPriceRate(source.output)
|
|
978
|
+
if (label === null || start === null || end === null || start === end || days.length === 0) return null
|
|
979
|
+
if (rawDays.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) return null
|
|
980
|
+
if ([input, cacheRead, cacheWrite, output].some((rate) => rate === null)) return null
|
|
981
|
+
return { label, days, start: source.start, end: source.end, input, cacheRead, cacheWrite, output }
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function normalizeCustomPriceWindows(value) {
|
|
985
|
+
if (value === undefined) return []
|
|
986
|
+
if (!Array.isArray(value) || value.length > CUSTOM_PRICE_MAX_WINDOWS) return null
|
|
987
|
+
const windows = []
|
|
988
|
+
const intervals = []
|
|
989
|
+
for (const candidate of value) {
|
|
990
|
+
const window = normalizeCustomPriceWindow(candidate)
|
|
991
|
+
if (window === null) return null
|
|
992
|
+
const startMinute = customTimeMinute(window.start)
|
|
993
|
+
const endMinute = customTimeMinute(window.end)
|
|
994
|
+
const duration = (endMinute - startMinute + MINUTES_PER_DAY) % MINUTES_PER_DAY
|
|
995
|
+
for (const day of window.days) {
|
|
996
|
+
const start = day * MINUTES_PER_DAY + startMinute
|
|
997
|
+
const end = start + duration
|
|
998
|
+
if (end <= MINUTES_PER_WEEK) intervals.push({ start, end })
|
|
999
|
+
else {
|
|
1000
|
+
intervals.push({ start, end: MINUTES_PER_WEEK })
|
|
1001
|
+
intervals.push({ start: 0, end: end - MINUTES_PER_WEEK })
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
windows.push(window)
|
|
1005
|
+
}
|
|
1006
|
+
intervals.sort((left, right) => left.start - right.start || left.end - right.end)
|
|
1007
|
+
for (let index = 1; index < intervals.length; index += 1) {
|
|
1008
|
+
if (intervals[index].start < intervals[index - 1].end) return null
|
|
1009
|
+
}
|
|
1010
|
+
return windows
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function customWallTime(atMs, timezone) {
|
|
1014
|
+
if (!Number.isFinite(atMs)) return null
|
|
1015
|
+
try {
|
|
1016
|
+
let formatter = customTimeFormatters.get(timezone)
|
|
1017
|
+
if (formatter === undefined) {
|
|
1018
|
+
formatter = new Intl.DateTimeFormat('en-US-u-ca-gregory', {
|
|
1019
|
+
timeZone: timezone,
|
|
1020
|
+
weekday: 'short',
|
|
1021
|
+
hour: '2-digit',
|
|
1022
|
+
minute: '2-digit',
|
|
1023
|
+
hourCycle: 'h23',
|
|
1024
|
+
})
|
|
1025
|
+
if (customTimeFormatters.size >= 200) customTimeFormatters.clear()
|
|
1026
|
+
customTimeFormatters.set(timezone, formatter)
|
|
1027
|
+
}
|
|
1028
|
+
const parts = Object.fromEntries(formatter.formatToParts(new Date(atMs)).map((part) => [part.type, part.value]))
|
|
1029
|
+
const weekday = WEEKDAY_NUMBER[parts.weekday]
|
|
1030
|
+
const hour = Number(parts.hour)
|
|
1031
|
+
const minute = Number(parts.minute)
|
|
1032
|
+
return Number.isInteger(weekday) && Number.isInteger(hour) && Number.isInteger(minute)
|
|
1033
|
+
? { weekday, minute: hour * 60 + minute }
|
|
1034
|
+
: null
|
|
1035
|
+
} catch {
|
|
1036
|
+
return null
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function customWindowMatches(window, wall) {
|
|
1041
|
+
const start = customTimeMinute(window.start)
|
|
1042
|
+
const end = customTimeMinute(window.end)
|
|
1043
|
+
if (start < end) return window.days.includes(wall.weekday) && wall.minute >= start && wall.minute < end
|
|
1044
|
+
if (wall.minute >= start) return window.days.includes(wall.weekday)
|
|
1045
|
+
const previousDay = (wall.weekday + 6) % 7
|
|
1046
|
+
return wall.minute < end && window.days.includes(previousDay)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
929
1049
|
/** Normalize one exact third-party provider/model price rule (rates are per 1M tokens). */
|
|
930
1050
|
export function normalizeCustomPriceRule(value) {
|
|
931
1051
|
const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
@@ -941,7 +1061,12 @@ export function normalizeCustomPriceRule(value) {
|
|
|
941
1061
|
if (provider === '__proto__' || provider === 'prototype' || provider === 'constructor') return null
|
|
942
1062
|
if (model === '__proto__' || model === 'prototype' || model === 'constructor') return null
|
|
943
1063
|
if ([input, cacheRead, cacheWrite, output].some((rate) => rate === null)) return null
|
|
944
|
-
|
|
1064
|
+
const windows = normalizeCustomPriceWindows(source.windows)
|
|
1065
|
+
if (windows === null) return null
|
|
1066
|
+
const timezone = normalizeCustomTimezone(source.timezone === undefined ? HISTORY_TIMEZONE : source.timezone)
|
|
1067
|
+
if (timezone === null) return null
|
|
1068
|
+
const rule = { provider, model, currency, input, cacheRead, cacheWrite, output }
|
|
1069
|
+
return windows.length === 0 ? rule : { ...rule, timezone, windows }
|
|
945
1070
|
}
|
|
946
1071
|
|
|
947
1072
|
/** Normalize, deduplicate, and bound persisted custom third-party price rules. */
|
|
@@ -959,19 +1084,105 @@ export function normalizeCustomPrices(value) {
|
|
|
959
1084
|
return [...rules.values()]
|
|
960
1085
|
}
|
|
961
1086
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1087
|
+
function customPriceRuleMap(rules) {
|
|
1088
|
+
return new Map(normalizeCustomPrices(rules).map((rule) => [rule.provider + '\u0000' + rule.model, rule]))
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function customRuleFor(provider, model, rules) {
|
|
1092
|
+
if (typeof provider !== 'string' || typeof model !== 'string' || !Array.isArray(rules)) return null
|
|
1093
|
+
for (let index = rules.length - 1; index >= 0; index -= 1) {
|
|
1094
|
+
const candidate = rules[index]
|
|
1095
|
+
if (candidate && candidate.provider === provider && candidate.model === model) return normalizeCustomPriceRule(candidate)
|
|
1096
|
+
}
|
|
1097
|
+
return null
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function customRateSelection(rule, atMs) {
|
|
1101
|
+
if (Array.isArray(rule.windows) && rule.windows.length > 0 && Number.isFinite(atMs)) {
|
|
1102
|
+
const wall = customWallTime(atMs, rule.timezone)
|
|
1103
|
+
const window = wall === null ? null : rule.windows.find((candidate) => customWindowMatches(candidate, wall))
|
|
1104
|
+
if (window !== undefined && window !== null) {
|
|
1105
|
+
return {
|
|
1106
|
+
kind: 'window',
|
|
1107
|
+
label: window.label,
|
|
1108
|
+
rates: { input: window.input, cacheRead: window.cacheRead, cacheWrite: window.cacheWrite, output: window.output },
|
|
1109
|
+
window,
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
kind: 'base',
|
|
1115
|
+
label: '基础价',
|
|
1116
|
+
rates: { input: rule.input, cacheRead: rule.cacheRead, cacheWrite: rule.cacheWrite, output: rule.output },
|
|
1117
|
+
window: null,
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export function customPriceStatus(value, atMs = Date.now()) {
|
|
1122
|
+
const rule = normalizeCustomPriceRule(value)
|
|
1123
|
+
if (rule === null) return null
|
|
1124
|
+
const selected = customRateSelection(rule, atMs)
|
|
1125
|
+
return {
|
|
1126
|
+
kind: selected.kind,
|
|
1127
|
+
label: selected.label,
|
|
1128
|
+
timezone: rule.timezone || null,
|
|
1129
|
+
rates: selected.rates,
|
|
1130
|
+
window: selected.window === null
|
|
1131
|
+
? null
|
|
1132
|
+
: { label: selected.window.label, days: [...selected.window.days], start: selected.window.start, end: selected.window.end },
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function customCostWithRule(rule, usage, atMs) {
|
|
1137
|
+
const selected = customRateSelection(rule, atMs)
|
|
967
1138
|
const counters = historyUsageCounters(usage)
|
|
968
1139
|
const cost = (
|
|
969
|
-
counters.input *
|
|
970
|
-
+ counters.cacheRead *
|
|
971
|
-
+ counters.cacheWrite *
|
|
972
|
-
+ counters.output *
|
|
1140
|
+
counters.input * selected.rates.input
|
|
1141
|
+
+ counters.cacheRead * selected.rates.cacheRead
|
|
1142
|
+
+ counters.cacheWrite * selected.rates.cacheWrite
|
|
1143
|
+
+ counters.output * selected.rates.output
|
|
973
1144
|
) / 1e6
|
|
974
|
-
|
|
1145
|
+
const result = { cost, currency: rule.currency, rule }
|
|
1146
|
+
if (Array.isArray(rule.windows) && rule.windows.length > 0) result.active = customPriceStatus(rule, atMs)
|
|
1147
|
+
return result
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** Price one exact third-party route from user-entered per-million-token rates. */
|
|
1151
|
+
export function customCostOf(provider, model, usage, rules, atMs = null) {
|
|
1152
|
+
const rule = customRuleFor(provider, model, rules)
|
|
1153
|
+
return rule === null ? null : customCostWithRule(rule, usage, atMs)
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function customSessionCost(provider, model, usage, sessionId, rules) {
|
|
1157
|
+
const rule = customRuleFor(provider, model, rules)
|
|
1158
|
+
if (rule === null) return null
|
|
1159
|
+
const remaining = historyUsageCounters(usage)
|
|
1160
|
+
let cost = 0
|
|
1161
|
+
if (typeof sessionId === 'string') {
|
|
1162
|
+
const events = Object.values(store.history && store.history.events ? store.history.events : {})
|
|
1163
|
+
.filter((event) => event && !event.official && event.sessionId === sessionId && event.provider === provider && event.model === model)
|
|
1164
|
+
.sort((left, right) => right.occurredAt - left.occurredAt)
|
|
1165
|
+
for (const event of events) {
|
|
1166
|
+
const counters = historyUsageCounters(event.usage)
|
|
1167
|
+
// Clearing a session intentionally leaves historical events in the
|
|
1168
|
+
// ledger. Walk newest-first and only price events that still fit inside
|
|
1169
|
+
// the live aggregate, so pre-clear usage is not resurrected here.
|
|
1170
|
+
if (!['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning'].every((key) => counters[key] <= remaining[key])) continue
|
|
1171
|
+
for (const key of ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning']) remaining[key] -= counters[key]
|
|
1172
|
+
cost += customCostWithRule(rule, event.usage, event.occurredAt).cost
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// Session totals can outlive the 365-day event ledger or survive an explicit
|
|
1176
|
+
// history clear. Without an occurrence timestamp, charge only the configured
|
|
1177
|
+
// base rate instead of pretending the current time window applied.
|
|
1178
|
+
cost += customCostWithRule(rule, remaining, null).cost
|
|
1179
|
+
const result = { cost, currency: rule.currency, rule }
|
|
1180
|
+
if (Array.isArray(rule.windows) && rule.windows.length > 0) result.active = customPriceStatus(rule, Date.now())
|
|
1181
|
+
return result
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function customPriceRulesView(atMs = Date.now()) {
|
|
1185
|
+
return store.customPrices.map((rule) => ({ ...rule, active: customPriceStatus(rule, atMs) }))
|
|
975
1186
|
}
|
|
976
1187
|
|
|
977
1188
|
function normalizeThirdPartyRoutes(value) {
|
|
@@ -1947,19 +2158,22 @@ function balanceTotal() {
|
|
|
1947
2158
|
return sumBalances(balance.balances)
|
|
1948
2159
|
}
|
|
1949
2160
|
|
|
1950
|
-
function thirdPartyRouteView(bucket) {
|
|
2161
|
+
function thirdPartyRouteView(bucket, sessionId = null) {
|
|
1951
2162
|
const routes = []
|
|
1952
2163
|
const source = normalizeThirdPartyRoutes(bucket && bucket.routes)
|
|
1953
2164
|
for (const [provider, providerState] of Object.entries(source)) {
|
|
1954
2165
|
if (isPlanProvider(provider) || isOfficialProvider(provider, store.officialProviders)) continue
|
|
1955
2166
|
for (const [model, counters] of Object.entries(providerState.models)) {
|
|
1956
|
-
const priced =
|
|
2167
|
+
const priced = sessionId === null
|
|
2168
|
+
? customCostOf(provider, model, counters, store.customPrices)
|
|
2169
|
+
: customSessionCost(provider, model, counters, sessionId, store.customPrices)
|
|
1957
2170
|
routes.push({
|
|
1958
2171
|
provider,
|
|
1959
2172
|
model,
|
|
1960
2173
|
tokens: counters,
|
|
1961
2174
|
totalTokens: historyTokenTotal(counters),
|
|
1962
2175
|
customCost: priced === null ? null : { cost: priced.cost, currency: priced.currency },
|
|
2176
|
+
activePrice: priced && priced.active ? priced.active : null,
|
|
1963
2177
|
})
|
|
1964
2178
|
}
|
|
1965
2179
|
}
|
|
@@ -1999,7 +2213,7 @@ function sessionView(sessionId) {
|
|
|
1999
2213
|
}
|
|
2000
2214
|
const official = bucketTotals(session.official)
|
|
2001
2215
|
const third = bucketTotals(session.third)
|
|
2002
|
-
const thirdRoutes = thirdPartyRouteView(session.third)
|
|
2216
|
+
const thirdRoutes = thirdPartyRouteView(session.third, sessionId)
|
|
2003
2217
|
return {
|
|
2004
2218
|
official: {
|
|
2005
2219
|
tokens: official,
|
|
@@ -2060,7 +2274,7 @@ function snapshotView(sessionId) {
|
|
|
2060
2274
|
builtinOfficial: OFFICIAL_PROVIDER,
|
|
2061
2275
|
official: [...store.officialProviders],
|
|
2062
2276
|
known: store.knownProviders.filter((p) => p !== OFFICIAL_PROVIDER && !store.officialProviders.includes(p)),
|
|
2063
|
-
customPrices:
|
|
2277
|
+
customPrices: customPriceRulesView(now),
|
|
2064
2278
|
knownRoutes: knownThirdPartyRoutes(),
|
|
2065
2279
|
},
|
|
2066
2280
|
plans: planView(now),
|
|
@@ -2359,7 +2573,7 @@ historyEventCount = store.history && store.history.events ? Object.keys(store.hi
|
|
|
2359
2573
|
path: '/api/wallet/custom-prices',
|
|
2360
2574
|
handler: async (req, res) => {
|
|
2361
2575
|
if (req.method === 'GET') {
|
|
2362
|
-
return json(res, 200, { ok: true, rules:
|
|
2576
|
+
return json(res, 200, { ok: true, rules: customPriceRulesView(), knownRoutes: knownThirdPartyRoutes() })
|
|
2363
2577
|
}
|
|
2364
2578
|
if (usageStorageLocked) return json(res, 423, { ok: false, error: 'usage-storage-locked' })
|
|
2365
2579
|
if (req.method !== 'POST' && req.method !== 'DELETE') {
|
|
@@ -2378,14 +2592,14 @@ historyEventCount = store.history && store.history.events ? Object.keys(store.hi
|
|
|
2378
2592
|
rule,
|
|
2379
2593
|
])
|
|
2380
2594
|
scheduleSave(ctx.logger)
|
|
2381
|
-
return json(res, 200, { ok: true, rules:
|
|
2595
|
+
return json(res, 200, { ok: true, rules: customPriceRulesView(), knownRoutes: knownThirdPartyRoutes() })
|
|
2382
2596
|
}
|
|
2383
2597
|
const provider = boundedString(body.provider, 100)
|
|
2384
2598
|
const model = boundedString(body.model, 160)
|
|
2385
2599
|
if (provider === null || model === null) return json(res, 400, { ok: false, error: 'provider-and-model-required' })
|
|
2386
2600
|
store.customPrices = store.customPrices.filter((entry) => entry.provider !== provider || entry.model !== model)
|
|
2387
2601
|
scheduleSave(ctx.logger)
|
|
2388
|
-
return json(res, 200, { ok: true, rules:
|
|
2602
|
+
return json(res, 200, { ok: true, rules: customPriceRulesView(), knownRoutes: knownThirdPartyRoutes() })
|
|
2389
2603
|
},
|
|
2390
2604
|
})
|
|
2391
2605
|
const disposeAccounts = ctx.webServer.register({
|
package/lib/client.js
CHANGED
|
@@ -23,7 +23,11 @@ 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.
|
|
26
|
+
var WALLET_VERSION = '0.3.9'
|
|
27
|
+
var CUSTOM_PRICE_WEEKDAYS = [
|
|
28
|
+
{ value: 1, label: '一' }, { value: 2, label: '二' }, { value: 3, label: '三' },
|
|
29
|
+
{ value: 4, label: '四' }, { value: 5, label: '五' }, { value: 6, label: '六' }, { value: 0, label: '日' }
|
|
30
|
+
]
|
|
27
31
|
var CONFIRM_KEY = 'dsh-wallet-recharge-confirmed'
|
|
28
32
|
var CHIP_LAYOUT_KEY = 'dshw-chip-layout-v4'
|
|
29
33
|
var PANEL_POS_KEY = 'dshw-panel-pos-v1'
|
|
@@ -610,15 +614,22 @@ window.__ModuleLoader__.load({
|
|
|
610
614
|
'.dshw_settingChoiceCopy{min-width:0;display:flex;flex-direction:column;gap:1px}',
|
|
611
615
|
'.dshw_settingChoiceCopy strong{font-size:11px;font-weight:550;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit)}',
|
|
612
616
|
'.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:
|
|
617
|
+
'.dshw_priceForm{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;align-items:end;padding:10px 12px}',
|
|
614
618
|
'.dshw_priceField{display:grid;gap:4px;min-width:0}',
|
|
615
619
|
'.dshw_priceField>span{font-size:9.5px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary,#667085))}',
|
|
616
620
|
'.dshw_priceField .dshw_input,.dshw_priceField .dshw_select{box-sizing:border-box;width:100%;height:28px}',
|
|
621
|
+
'.dshw_priceWindowEditor{display:grid;gap:7px;margin:0 12px 10px;padding:9px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.15));border-radius:9px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1,rgba(127,127,127,.035)))}',
|
|
622
|
+
'.dshw_priceWindowToolbar{display:flex;align-items:end;justify-content:space-between;gap:8px;flex-wrap:wrap}.dshw_priceWindowToolbar .dshw_priceField{min-width:180px;flex:1}',
|
|
623
|
+
'.dshw_priceWindowList{display:grid;gap:7px}.dshw_priceWindow{display:grid;gap:7px;padding:8px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.14));border-radius:8px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,rgba(127,127,127,.03)))}',
|
|
624
|
+
'.dshw_priceWindowHead{display:flex;align-items:end;gap:7px}.dshw_priceWindowHead .dshw_priceField{flex:1}',
|
|
625
|
+
'.dshw_priceDays{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.dshw_priceDays>span{margin-right:2px;font-size:9.5px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary,#667085))}',
|
|
626
|
+
'.dshw_priceDay{width:25px;height:24px;padding:0;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18));border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary,inherit);font-size:10px;cursor:pointer}.dshw_priceDay[aria-pressed="true"]{border-color:var(--dsw-alias-brand-primary,#4aa3ff);background:var(--dsw-alias-brand-soft,rgba(74,163,255,.12));color:var(--dsw-alias-label-primary,inherit)}',
|
|
627
|
+
'.dshw_priceWindowRates{display:grid;grid-template-columns:repeat(6,minmax(72px,1fr));gap:7px}',
|
|
617
628
|
'.dshw_priceRules{display:grid;gap:6px;padding:0 12px 11px}',
|
|
618
629
|
'.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
630
|
'.dshw_priceRule strong,.dshw_priceRule span{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
620
631
|
'.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:
|
|
632
|
+
'@media(max-width:760px){.dshw_priceForm{grid-template-columns:repeat(2,minmax(0,1fr))}.dshw_priceForm>.dshw_btn{min-height:28px}.dshw_priceWindowRates{grid-template-columns:repeat(2,minmax(0,1fr))}}',
|
|
622
633
|
'.dshw_switch{position:relative;flex:none;width:34px;height:20px;display:inline-flex;cursor:pointer}',
|
|
623
634
|
'.dshw_switch>input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}',
|
|
624
635
|
'.dshw_track{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l3,rgba(127,127,127,.3));transition:background-color .15s}',
|
|
@@ -1597,7 +1608,7 @@ window.__ModuleLoader__.load({
|
|
|
1597
1608
|
React.createElement('div', null, React.createElement('span', null, '今天'), React.createElement('strong', null, fmtTokens(today.totalTokens))),
|
|
1598
1609
|
React.createElement('div', null, React.createElement('span', null, '本月'), React.createElement('strong', null, fmtTokens(month.totalTokens))),
|
|
1599
1610
|
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' }, '
|
|
1611
|
+
customCostsText(total.customCosts) ? React.createElement('div', { className: 'dshw_historyLegend' }, '第三方按当前自定义规则估算:' + customCostsText(total.customCosts)) : null,
|
|
1601
1612
|
React.createElement('div', { className: 'dshw_historyMetricToggle', role: 'group', 'aria-label': '历史热力图指标' },
|
|
1602
1613
|
React.createElement('button', { type: 'button', className: metric === 'tokens' ? 'dshw_historyMetricActive' : '', 'aria-pressed': metric === 'tokens', onClick: function () { setMetric('tokens') } }, 'Token'),
|
|
1603
1614
|
React.createElement('button', { type: 'button', className: metric === 'cost' ? 'dshw_historyMetricActive' : '', 'aria-pressed': metric === 'cost', onClick: function () { setMetric('cost') } }, '费用')),
|
|
@@ -1709,7 +1720,8 @@ window.__ModuleLoader__.load({
|
|
|
1709
1720
|
if (!selection || typeof selection.provider !== 'string') return { kind: 'loading', provider: null, model: null }
|
|
1710
1721
|
var provider = selection.provider
|
|
1711
1722
|
var model = typeof selection.model === 'string' ? selection.model : null
|
|
1712
|
-
|
|
1723
|
+
var planProvider = provider.indexOf('vision-toolkit-') === 0 ? provider.slice('vision-toolkit-'.length) : provider
|
|
1724
|
+
if (planProvider === 'zai' || planProvider === 'zai-coding-cn') return { kind: 'zai', provider: planProvider, routeProvider: provider, model: model }
|
|
1713
1725
|
var providers = snapshot && snapshot.providers ? snapshot.providers : {}
|
|
1714
1726
|
var aliases = Array.isArray(providers.official) ? providers.official : []
|
|
1715
1727
|
if (provider === (providers.builtinOfficial || 'deepseek-official') || aliases.indexOf(provider) >= 0) {
|
|
@@ -1893,7 +1905,8 @@ window.__ModuleLoader__.load({
|
|
|
1893
1905
|
var [knownPriceRoutes, setKnownPriceRoutes] = React.useState([])
|
|
1894
1906
|
var [priceNotice, setPriceNotice] = React.useState(null)
|
|
1895
1907
|
var [priceDraft, setPriceDraft] = React.useState({
|
|
1896
|
-
provider: '', model: '', currency: 'CNY', input: '', cacheRead: '', cacheWrite: '', output: ''
|
|
1908
|
+
provider: '', model: '', currency: 'CNY', input: '', cacheRead: '', cacheWrite: '', output: '',
|
|
1909
|
+
timezone: 'Asia/Shanghai', windows: []
|
|
1897
1910
|
})
|
|
1898
1911
|
var [visibility, setVisibility] = React.useState(function () {
|
|
1899
1912
|
try {
|
|
@@ -2207,6 +2220,29 @@ window.__ModuleLoader__.load({
|
|
|
2207
2220
|
setPriceNotice('请填写 Provider、模型及四项非负价格')
|
|
2208
2221
|
return
|
|
2209
2222
|
}
|
|
2223
|
+
var windows = (Array.isArray(priceDraft.windows) ? priceDraft.windows : []).map(function (window) {
|
|
2224
|
+
var rates = ['input', 'cacheRead', 'cacheWrite', 'output'].reduce(function (out, key) {
|
|
2225
|
+
var value = Number.parseFloat(window[key])
|
|
2226
|
+
out[key] = Number.isFinite(value) && value >= 0 ? value : null
|
|
2227
|
+
return out
|
|
2228
|
+
}, {})
|
|
2229
|
+
return {
|
|
2230
|
+
label: String(window.label || '').trim(),
|
|
2231
|
+
days: Array.isArray(window.days) ? window.days.slice() : [],
|
|
2232
|
+
start: window.start,
|
|
2233
|
+
end: window.end,
|
|
2234
|
+
input: rates.input,
|
|
2235
|
+
cacheRead: rates.cacheRead,
|
|
2236
|
+
cacheWrite: rates.cacheWrite,
|
|
2237
|
+
output: rates.output
|
|
2238
|
+
}
|
|
2239
|
+
})
|
|
2240
|
+
if (windows.some(function (window) {
|
|
2241
|
+
return !window.label || window.days.length === 0 || !/^([01]\d|2[0-3]):[0-5]\d$/.test(window.start || '') || !/^([01]\d|2[0-3]):[0-5]\d$/.test(window.end || '') || window.start === window.end || ['input', 'cacheRead', 'cacheWrite', 'output'].some(function (key) { return window[key] === null })
|
|
2242
|
+
})) {
|
|
2243
|
+
setPriceNotice('请完整填写分时段名称、星期、起止时间及四项价格')
|
|
2244
|
+
return
|
|
2245
|
+
}
|
|
2210
2246
|
setPriceNotice('正在保存…')
|
|
2211
2247
|
fetch('/api/wallet/custom-prices', {
|
|
2212
2248
|
method: 'POST',
|
|
@@ -2218,13 +2254,15 @@ window.__ModuleLoader__.load({
|
|
|
2218
2254
|
input: numeric.input,
|
|
2219
2255
|
cacheRead: numeric.cacheRead,
|
|
2220
2256
|
cacheWrite: numeric.cacheWrite,
|
|
2221
|
-
output: numeric.output
|
|
2257
|
+
output: numeric.output,
|
|
2258
|
+
timezone: String(priceDraft.timezone || '').trim(),
|
|
2259
|
+
windows: windows
|
|
2222
2260
|
} })
|
|
2223
2261
|
}).then(function (resp) { return resp.json().then(function (json) { return { ok: resp.ok, json: json } }) }).then(function (result) {
|
|
2224
2262
|
if (!result.ok || !applyCustomPriceResponse(result.json)) throw new Error(result.json && result.json.error ? result.json.error : 'save-failed')
|
|
2225
|
-
setPriceNotice('
|
|
2263
|
+
setPriceNotice('已保存;本会话与历史账本按当前自定义规则重新估算')
|
|
2226
2264
|
}).catch(function (error) {
|
|
2227
|
-
setPriceNotice(error && error.message === 'official-provider-not-allowed' ? '官方计费 Provider 不能设置第三方价格' : '
|
|
2265
|
+
setPriceNotice(error && error.message === 'official-provider-not-allowed' ? '官方计费 Provider 不能设置第三方价格' : '保存失败,请检查时区、时段是否有效或重叠')
|
|
2228
2266
|
})
|
|
2229
2267
|
}
|
|
2230
2268
|
|
|
@@ -2249,11 +2287,69 @@ window.__ModuleLoader__.load({
|
|
|
2249
2287
|
input: String(rule.input),
|
|
2250
2288
|
cacheRead: String(rule.cacheRead),
|
|
2251
2289
|
cacheWrite: String(rule.cacheWrite),
|
|
2252
|
-
output: String(rule.output)
|
|
2290
|
+
output: String(rule.output),
|
|
2291
|
+
timezone: rule.timezone || 'Asia/Shanghai',
|
|
2292
|
+
windows: (Array.isArray(rule.windows) ? rule.windows : []).map(function (window) {
|
|
2293
|
+
return {
|
|
2294
|
+
label: window.label,
|
|
2295
|
+
days: Array.isArray(window.days) ? window.days.slice() : [],
|
|
2296
|
+
start: window.start,
|
|
2297
|
+
end: window.end,
|
|
2298
|
+
input: String(window.input),
|
|
2299
|
+
cacheRead: String(window.cacheRead),
|
|
2300
|
+
cacheWrite: String(window.cacheWrite),
|
|
2301
|
+
output: String(window.output)
|
|
2302
|
+
}
|
|
2303
|
+
})
|
|
2253
2304
|
})
|
|
2254
2305
|
setPriceNotice('已载入,可修改后保存')
|
|
2255
2306
|
}
|
|
2256
2307
|
|
|
2308
|
+
function addCustomPriceWindow() {
|
|
2309
|
+
setPriceDraft(function (current) {
|
|
2310
|
+
var windows = Array.isArray(current.windows) ? current.windows.slice() : []
|
|
2311
|
+
var fallback = function (key) { return current[key] === '' ? '' : String(current[key]) }
|
|
2312
|
+
windows.push({
|
|
2313
|
+
label: '分时段 ' + (windows.length + 1),
|
|
2314
|
+
days: [1, 2, 3, 4, 5],
|
|
2315
|
+
start: '00:00',
|
|
2316
|
+
end: '09:00',
|
|
2317
|
+
input: fallback('input'),
|
|
2318
|
+
cacheRead: fallback('cacheRead'),
|
|
2319
|
+
cacheWrite: fallback('cacheWrite'),
|
|
2320
|
+
output: fallback('output')
|
|
2321
|
+
})
|
|
2322
|
+
return Object.assign({}, current, { windows: windows })
|
|
2323
|
+
})
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function updateCustomPriceWindow(index, key, value) {
|
|
2327
|
+
setPriceDraft(function (current) {
|
|
2328
|
+
var windows = (Array.isArray(current.windows) ? current.windows : []).map(function (window, candidate) {
|
|
2329
|
+
return candidate === index ? Object.assign({}, window, (function () { var patch = {}; patch[key] = value; return patch })()) : window
|
|
2330
|
+
})
|
|
2331
|
+
return Object.assign({}, current, { windows: windows })
|
|
2332
|
+
})
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
function toggleCustomPriceDay(index, day) {
|
|
2336
|
+
setPriceDraft(function (current) {
|
|
2337
|
+
var windows = (Array.isArray(current.windows) ? current.windows : []).map(function (window, candidate) {
|
|
2338
|
+
if (candidate !== index) return window
|
|
2339
|
+
var days = Array.isArray(window.days) ? window.days.slice() : []
|
|
2340
|
+
days = days.includes(day) ? days.filter(function (value) { return value !== day }) : days.concat([day])
|
|
2341
|
+
return Object.assign({}, window, { days: days })
|
|
2342
|
+
})
|
|
2343
|
+
return Object.assign({}, current, { windows: windows })
|
|
2344
|
+
})
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
function removeCustomPriceWindow(index) {
|
|
2348
|
+
setPriceDraft(function (current) {
|
|
2349
|
+
return Object.assign({}, current, { windows: (Array.isArray(current.windows) ? current.windows : []).filter(function (_, candidate) { return candidate !== index }) })
|
|
2350
|
+
})
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2257
2353
|
function useCurrentRouteForPrice() {
|
|
2258
2354
|
if (!settingsModelSelection || typeof settingsModelSelection.provider !== 'string' || typeof settingsModelSelection.model !== 'string') {
|
|
2259
2355
|
setPriceNotice('当前会话尚未选择可识别的模型')
|
|
@@ -2748,17 +2844,78 @@ window.__ModuleLoader__.load({
|
|
|
2748
2844
|
React.createElement('button', { type: 'button', className: 'dshw_btn dshw_btnPrimary', disabled: usageLocked, onClick: saveCustomPrice }, '保存'),
|
|
2749
2845
|
React.createElement('datalist', { id: 'dshw-price-providers' }, providerOptions.map(function (provider) { return React.createElement('option', { key: provider, value: provider }) })),
|
|
2750
2846
|
React.createElement('datalist', { id: 'dshw-price-models' }, modelOptions.map(function (model) { return React.createElement('option', { key: model, value: model }) })))
|
|
2847
|
+
function customWindowField(window, index, key, label, type) {
|
|
2848
|
+
return React.createElement('label', { key: key, className: 'dshw_priceField' },
|
|
2849
|
+
React.createElement('span', null, label),
|
|
2850
|
+
React.createElement('input', {
|
|
2851
|
+
className: 'dshw_input',
|
|
2852
|
+
type: type,
|
|
2853
|
+
min: type === 'number' ? '0' : undefined,
|
|
2854
|
+
max: type === 'number' ? '1000000' : undefined,
|
|
2855
|
+
step: type === 'number' ? '0.000001' : undefined,
|
|
2856
|
+
value: window[key],
|
|
2857
|
+
'aria-label': '分时段 ' + (index + 1) + ' ' + label,
|
|
2858
|
+
onChange: function (event) { updateCustomPriceWindow(index, key, event.target.value) }
|
|
2859
|
+
}))
|
|
2860
|
+
}
|
|
2861
|
+
var draftPriceWindows = Array.isArray(priceDraft.windows) ? priceDraft.windows : []
|
|
2862
|
+
var priceWindowRows = draftPriceWindows.map(function (window, index) {
|
|
2863
|
+
return React.createElement('div', { key: index, className: 'dshw_priceWindow' },
|
|
2864
|
+
React.createElement('div', { className: 'dshw_priceWindowHead' },
|
|
2865
|
+
customWindowField(window, index, 'label', '时段名称', 'text'),
|
|
2866
|
+
React.createElement('button', { type: 'button', className: 'dshw_btn', disabled: usageLocked, 'aria-label': '删除分时段 ' + (index + 1), onClick: function () { removeCustomPriceWindow(index) } }, '删除时段')),
|
|
2867
|
+
React.createElement('div', { className: 'dshw_priceDays', role: 'group', 'aria-label': '分时段 ' + (index + 1) + ' 适用星期' },
|
|
2868
|
+
React.createElement('span', null, '星期'),
|
|
2869
|
+
CUSTOM_PRICE_WEEKDAYS.map(function (day) {
|
|
2870
|
+
var selected = Array.isArray(window.days) && window.days.includes(day.value)
|
|
2871
|
+
return React.createElement('button', {
|
|
2872
|
+
key: day.value,
|
|
2873
|
+
type: 'button',
|
|
2874
|
+
className: 'dshw_priceDay',
|
|
2875
|
+
'aria-label': '星期' + day.label,
|
|
2876
|
+
'aria-pressed': selected,
|
|
2877
|
+
onClick: function () { toggleCustomPriceDay(index, day.value) }
|
|
2878
|
+
}, day.label)
|
|
2879
|
+
})),
|
|
2880
|
+
React.createElement('div', { className: 'dshw_priceWindowRates' },
|
|
2881
|
+
customWindowField(window, index, 'start', '开始', 'time'),
|
|
2882
|
+
customWindowField(window, index, 'end', '结束', 'time'),
|
|
2883
|
+
customWindowField(window, index, 'input', '输入', 'number'),
|
|
2884
|
+
customWindowField(window, index, 'cacheRead', '缓存读', 'number'),
|
|
2885
|
+
customWindowField(window, index, 'cacheWrite', '缓存写', 'number'),
|
|
2886
|
+
customWindowField(window, index, 'output', '输出', 'number')))
|
|
2887
|
+
})
|
|
2888
|
+
var priceWindowEditor = React.createElement('div', { className: 'dshw_priceWindowEditor' },
|
|
2889
|
+
React.createElement('div', { className: 'dshw_priceWindowToolbar' },
|
|
2890
|
+
React.createElement('label', { className: 'dshw_priceField' },
|
|
2891
|
+
React.createElement('span', null, '计价时区(IANA)'),
|
|
2892
|
+
React.createElement('input', {
|
|
2893
|
+
className: 'dshw_input', type: 'text', list: 'dshw-price-timezones', value: priceDraft.timezone,
|
|
2894
|
+
'aria-label': '第三方计价时区', onChange: function (event) { updatePriceDraft('timezone', event.target.value) }
|
|
2895
|
+
}),
|
|
2896
|
+
React.createElement('datalist', { id: 'dshw-price-timezones' }, ['Asia/Shanghai', 'UTC', 'America/New_York', 'Europe/London'].map(function (timezone) {
|
|
2897
|
+
return React.createElement('option', { key: timezone, value: timezone }, timezone)
|
|
2898
|
+
}))),
|
|
2899
|
+
React.createElement('button', { type: 'button', className: 'dshw_btn', disabled: usageLocked || draftPriceWindows.length >= 24, onClick: addCustomPriceWindow }, '+ 添加分时段')),
|
|
2900
|
+
priceWindowRows.length > 0
|
|
2901
|
+
? React.createElement('div', { className: 'dshw_priceWindowList' }, priceWindowRows)
|
|
2902
|
+
: React.createElement('div', { className: 'dshw_muted' }, '未添加分时段时始终使用上方基础价格。'),
|
|
2903
|
+
React.createElement('div', { className: 'dshw_muted' }, '分时段可跨午夜;同一规则的时段不能重叠。'))
|
|
2751
2904
|
var priceRuleRows = priceRules.map(function (rule) {
|
|
2905
|
+
var windows = Array.isArray(rule.windows) ? rule.windows : []
|
|
2906
|
+
var active = rule.active && windows.length > 0 ? rule.active : null
|
|
2752
2907
|
return React.createElement('div', { key: rule.provider + ':' + rule.model, className: 'dshw_priceRule' },
|
|
2753
2908
|
React.createElement('span', null,
|
|
2754
2909
|
React.createElement('strong', { title: rule.provider + ' · ' + rule.model }, rule.provider + ' · ' + rule.model),
|
|
2755
|
-
React.createElement('span', null, rule.currency + '/1M ·
|
|
2910
|
+
React.createElement('span', null, rule.currency + '/1M · 基础:入 ' + rule.input + ' · 缓读 ' + rule.cacheRead + ' · 缓写 ' + rule.cacheWrite + ' · 出 ' + rule.output),
|
|
2911
|
+
windows.length > 0 ? React.createElement('span', null, '分时 ' + windows.length + ' 段 · ' + (rule.timezone || 'Asia/Shanghai') + ' · 当前:' + (active ? active.label : '基础价')) : null),
|
|
2756
2912
|
React.createElement('span', { className: 'dshw_settingsInline' },
|
|
2757
2913
|
React.createElement('button', { type: 'button', className: 'dshw_btn', onClick: function () { editCustomPrice(rule) } }, '编辑'),
|
|
2758
2914
|
React.createElement('button', { type: 'button', className: 'dshw_btn', disabled: usageLocked, onClick: function () { removeCustomPrice(rule) } }, '删除')))
|
|
2759
2915
|
})
|
|
2760
2916
|
rows.push(React.createElement('div', { key: 'cp-card', className: 'dshw_setCard wide', style: { display: 'block' } },
|
|
2761
2917
|
priceForm,
|
|
2918
|
+
priceWindowEditor,
|
|
2762
2919
|
priceNotice ? React.createElement('div', { className: 'dshw_muted', role: 'status', style: { padding: '0 12px 8px' } }, priceNotice) : null,
|
|
2763
2920
|
priceRuleRows.length > 0
|
|
2764
2921
|
? React.createElement('div', { className: 'dshw_priceRules' }, priceRuleRows)
|
|
@@ -4917,7 +5074,16 @@ window.__ModuleLoader__.load({
|
|
|
4917
5074
|
if (thirdCustomText) {
|
|
4918
5075
|
thirdRows.push(React.createElement('div', { key: 't-c', className: 'dshw_row' },
|
|
4919
5076
|
React.createElement('span', { className: 'dshw_muted' }, '自定义估算'),
|
|
4920
|
-
React.createElement('span', { title: '
|
|
5077
|
+
React.createElement('span', { title: '按当前自定义规则及用量发生时间计算,不代表第三方账单' }, thirdCustomText)))
|
|
5078
|
+
}
|
|
5079
|
+
if (activeThirdRoute && activeThirdRoute.activePrice) {
|
|
5080
|
+
var activeCustomPrice = activeThirdRoute.activePrice
|
|
5081
|
+
var activeWindow = activeCustomPrice.window
|
|
5082
|
+
thirdRows.push(React.createElement('div', { key: 't-price-window', className: 'dshw_row' },
|
|
5083
|
+
React.createElement('span', { className: 'dshw_muted' }, '当前计价'),
|
|
5084
|
+
React.createElement('span', {
|
|
5085
|
+
title: activeWindow ? ('星期按所选日期 · ' + activeWindow.start + '–' + activeWindow.end + ' · ' + activeCustomPrice.timezone) : ('未命中分时段 · ' + activeCustomPrice.timezone)
|
|
5086
|
+
}, activeCustomPrice.label)))
|
|
4921
5087
|
}
|
|
4922
5088
|
|
|
4923
5089
|
var thresholdInput = React.createElement('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, 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.
|
|
3
|
+
"description": "Local-first DeepSeek Harness control center for balance monitoring, usage accounting, time-aware 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.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|