dsh-plugin-billing-card 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -14
- package/billing-core.js +5 -4
- package/client.js +129 -9
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
默认内置 DeepSeek 官方价格(含**峰谷分时计价**),并保留完整的扩展能力:价格表、峰谷规则、
|
|
5
5
|
其他模型/提供商都可以在插件配置里追加,不需要改代码。0.2.0 起同时统计 **DeepSeek 官方网页搜索 /
|
|
6
6
|
抓取 API** 的调用次数与按次费用(Search 服务端调用免费至 2026-11-01 00:00,之后按每 1000 次
|
|
7
|
-
5
|
|
7
|
+
5 元估算);0.3.0 起卡片按当前**实际生效的 provider** 标注——服务端搜索(`deepseek-official`)、
|
|
8
|
+
直连 Search / Fetch API(`deepseek-search-api` / `deepseek-fetch-api`)、本地 HTTP 抓取(`http`)
|
|
9
|
+
各按自己的计费口径显示,不会再把本地免费抓取或直连 API 标成「免费期内」。
|
|
8
10
|
|
|
9
|
-
> npm:[`dsh-plugin-billing-card@0.
|
|
10
|
-
> · `tofu-box@1.2.
|
|
11
|
+
> npm:[`dsh-plugin-billing-card@0.3.0`](https://www.npmjs.com/package/dsh-plugin-billing-card)
|
|
12
|
+
> · `tofu-box@1.2.2` 起包含本版本。
|
|
13
|
+
>
|
|
14
|
+
> 兼容性:`engines.dsh` 声明为 `>=0.2.0-rc.2 <0.3.0-0`,已实测运行于 dsh **0.2.1-rc.3**。
|
|
11
15
|
|
|
12
16
|
- **Host 半**:注册一个客户端可见的 Session 投影单元 `billingUsage`,在提交事件时折叠
|
|
13
17
|
`assistant/message` / `assistant/attempt` 中模型上报的 `usage`(按 `request/header` 归属模型路由),
|
|
@@ -31,13 +35,14 @@
|
|
|
31
35
|
- 按模型分项:模型名、token 数、估算费用;没有配置价格的模型显示「未配置价格」,
|
|
32
36
|
token 仍计入合计,但不计入费用。
|
|
33
37
|
- 联网搜索小节(本会话有搜索/抓取调用时出现):搜索调用次数、provider 请求数、预计计费请求数、
|
|
34
|
-
|
|
38
|
+
搜索费用、抓取次数;标注随当前 provider 变化——服务端搜索免费期内显示「免费期内(11/1 起 …)」,
|
|
39
|
+
直连 API 直接显示按次费用,本地 HTTP 抓取显示「免费」。
|
|
35
40
|
|
|
36
41
|
会话还没有任何已结算的 token 用量时,卡片不显示(不会占位、不会报错)。
|
|
37
42
|
|
|
38
43
|
## 安装
|
|
39
44
|
|
|
40
|
-
已发布 npm:`dsh-plugin-billing-card@0.
|
|
45
|
+
已发布 npm:`dsh-plugin-billing-card@0.3.0`,同时也是 `tofu-box` 伞包(1.2.2 起)的成员。
|
|
41
46
|
插件行 id 为 `billing-card`,Client 半是手写浏览器模块,无需构建步骤。
|
|
42
47
|
|
|
43
48
|
```bash
|
|
@@ -103,22 +108,31 @@ plugin_manager install_bundle target = D:\program\dsh-plugins\dsh-plugin-billing
|
|
|
103
108
|
> 价格来源:DeepSeek 官方「模型 & 价格」页(中文 / 英文),核对时间 2026-10-17。
|
|
104
109
|
> 官方保留调价权利,请以官网与你的实际结算为准;调价后覆盖本插件配置即可。
|
|
105
110
|
|
|
106
|
-
## 联网搜索 / 抓取计费(0.
|
|
111
|
+
## 联网搜索 / 抓取计费(0.3.0)
|
|
107
112
|
|
|
108
113
|
官方口径(Search / Fetch API,2026-10-17 上线):
|
|
109
114
|
|
|
110
115
|
- 按**成功请求次数**计费:每 1,000 次成功请求 **5 元 / $0.75**;失败、空结果、超时不计费,
|
|
111
116
|
`count` 只控制返回条数、不影响单价。
|
|
112
117
|
- 在 DeepSeek Harness / Claude Code / Codex 中由**服务端工具调用**触发的 Search / Fetch 目前免费,
|
|
113
|
-
**北京时间 2026-11-01 00:00
|
|
114
|
-
|
|
115
|
-
不属于服务端工具调用,按官方规则自上线起计费。
|
|
118
|
+
**北京时间 2026-11-01 00:00 起**按次收费;而直连 API(`web.searchProvider: deepseek-search-api` /
|
|
119
|
+
`web.fetchProvider: deepseek-fetch-api`)自上线起就按次计费,没有免费期。
|
|
116
120
|
- 本插件从会话日志统计(不额外发请求):
|
|
117
121
|
- `web_search`:每次工具调用按**去重后的 query 数**计为 provider 请求数(工具本身就是每个
|
|
118
122
|
query 发一个请求);一次调用成功且结果源非空时,这些 query 计为「预计计费请求」。
|
|
119
|
-
- `web_fetch`:按 HTTP 200
|
|
123
|
+
- `web_fetch`:按 HTTP 200 的成功抓取计为一次抓取请求。
|
|
120
124
|
- 费用 = 预计计费请求数 × `search.perRequest`(默认 0.005 元/次,美元 0.00075/次)。
|
|
121
|
-
-
|
|
125
|
+
- 卡片读取当前 `web` 行的 `searchProvider` / `fetchProvider`(「设置 → 插件 → Web 搜索」里的
|
|
126
|
+
「启用新版 Web 搜索 API」开关写的就是这两个字段),据此标注:
|
|
127
|
+
|
|
128
|
+
| 生效 provider | 搜索费用标签 | 抓取费用标签 |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| 服务端 Messages 搜索 `deepseek-official` | 免费期内显示「免费期内(11/1 起 ¥…)」 | —(默认抓取是 `http`) |
|
|
131
|
+
| 直连 Search / Fetch API | 直接显示按次费用,并注明没有服务端免费期 | 显示 `次数 · 费用` |
|
|
132
|
+
| 本地 HTTP 抓取 `http` | — | 显示 `次数 · 免费`,并注明不计费 |
|
|
133
|
+
| 其他 provider(Exa / Perplexity 等) | 只显示按次估算,不声称免费期 | 同上 |
|
|
134
|
+
|
|
135
|
+
读不到设置时按出厂默认(`deepseek-official` + `http`)标注,不会因此隐藏卡片。
|
|
122
136
|
|
|
123
137
|
已知限制(估算口径):
|
|
124
138
|
|
|
@@ -127,9 +141,9 @@ plugin_manager install_bundle target = D:\program\dsh-plugins\dsh-plugin-billing
|
|
|
127
141
|
- 多 query 批量搜索失败时,已成功的那部分可能已被官方计费,但这里整单失败、不计费;
|
|
128
142
|
空结果的成功调用不计费(与官方一致)。
|
|
129
143
|
- 服务端搜索附带的辅助模型 token 费用不写入会话日志,本卡片不统计。
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
- 计数只按工具名(`web_search` / `web_fetch`),不区分 provider;换用 Exa / Perplexity 等
|
|
145
|
+
第三方 provider 时次数仍会累计,卡片只会显示按 `search.perRequest` 的估算而不声称官方口径。
|
|
146
|
+
这类部署若不想看到按次费用,把 `search.enabled` 设为 `false`。
|
|
133
147
|
|
|
134
148
|
## 配置
|
|
135
149
|
|
package/billing-core.js
CHANGED
|
@@ -1081,10 +1081,11 @@ function counterValue(value) {
|
|
|
1081
1081
|
|
|
1082
1082
|
/**
|
|
1083
1083
|
* Build the wire view of the search / fetch cost section. Counts are always
|
|
1084
|
-
* present; costs are estimates from the resolved per-request price.
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1084
|
+
* present; costs are estimates from the resolved per-request price. Both costs
|
|
1085
|
+
* assume the DeepSeek APIs charge per request, so the search cost is the
|
|
1086
|
+
* post-free-window estimate and the fetch cost the direct-API estimate; the
|
|
1087
|
+
* client decides how to label each one from the `web` row's provider selection,
|
|
1088
|
+
* which this replayable fold deliberately does not read.
|
|
1088
1089
|
* @param {{ search?: Record<string, any> }} state - fold state.
|
|
1089
1090
|
* @param {ReturnType<typeof resolveBillingConfig>} resolvedConfig - resolved config.
|
|
1090
1091
|
* @returns {Record<string, any>} the `search` wire section.
|
package/client.js
CHANGED
|
@@ -51,9 +51,12 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
'search.cost': '搜索费用',
|
|
52
52
|
'search.freeValue': '免费期内(11/1 起 {cost})',
|
|
53
53
|
'search.freeNote': '服务端搜索调用免费至 2026-11-01 00:00,之后按官方每 1000 次价格估算。',
|
|
54
|
+
'search.directNote': '当前使用直连 DeepSeek Search API,按次计费,没有服务端免费期。',
|
|
54
55
|
'search.fetch': '网页抓取',
|
|
55
56
|
'search.fetchValue': '{count} 次 · {cost}',
|
|
57
|
+
'search.fetchLocalValue': '{count} 次 · 免费',
|
|
56
58
|
'search.fetchNote': '网页抓取为直连 DeepSeek Fetch API,按次计费。',
|
|
59
|
+
'search.fetchLocalNote': '网页抓取走本地 HTTP,不产生按次费用。',
|
|
57
60
|
'footer.estimate': '费用为按配置价格表估算的预估值,非实际账单。',
|
|
58
61
|
'footer.config': '价格与峰谷时段可在插件配置中调整。',
|
|
59
62
|
'pill.aria': '预估费用 {cost}',
|
|
@@ -81,9 +84,12 @@ window.__ModuleLoader__.load({
|
|
|
81
84
|
'search.cost': 'Search cost',
|
|
82
85
|
'search.freeValue': 'Free until Nov 1 (then {cost})',
|
|
83
86
|
'search.freeNote': 'Server-side search calls are free until 2026-11-01 00:00 Beijing; afterward priced at the official per-1,000-request rate.',
|
|
87
|
+
'search.directNote': 'The direct DeepSeek Search API is billed per request with no server-side free window.',
|
|
84
88
|
'search.fetch': 'Web fetch',
|
|
85
89
|
'search.fetchValue': '{count} requests · {cost}',
|
|
90
|
+
'search.fetchLocalValue': '{count} requests · free',
|
|
86
91
|
'search.fetchNote': 'Web fetch calls the DeepSeek Fetch API directly and is billed per request.',
|
|
92
|
+
'search.fetchLocalNote': 'Web fetch uses local HTTP and incurs no per-request charge.',
|
|
87
93
|
'footer.estimate': 'Costs are estimates from the configured price table, not an actual bill.',
|
|
88
94
|
'footer.config': 'Pricing and peak windows are configurable in the plugin config.',
|
|
89
95
|
'pill.aria': 'Estimated cost {cost}',
|
|
@@ -196,6 +202,94 @@ window.__ModuleLoader__.load({
|
|
|
196
202
|
return Math.round(cacheRead / denominator * 100)
|
|
197
203
|
}
|
|
198
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Provider ids the `web` row selects. Both its fields are volatile, so the
|
|
207
|
+
* card follows the live selection instead of the composition's startup value.
|
|
208
|
+
*/
|
|
209
|
+
const DIRECT_SEARCH_PROVIDER = 'deepseek-search-api'
|
|
210
|
+
const DIRECT_FETCH_PROVIDER = 'deepseek-fetch-api'
|
|
211
|
+
/** The shipped `web` selection; also the fallback when settings are unreadable. */
|
|
212
|
+
const MESSAGES_SEARCH_PROVIDER = 'deepseek-official'
|
|
213
|
+
const HTTP_FETCH_PROVIDER = 'http'
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Classify the selected web providers into the per-request billing each one
|
|
217
|
+
* carries. Only these two DeepSeek deployments are known: server-side search
|
|
218
|
+
* has the free window, the direct Search and Fetch APIs bill from launch, and
|
|
219
|
+
* the local HTTP fetch bills nothing. Any other provider id stays `other`, so
|
|
220
|
+
* the card reports the resolved cost without claiming a DeepSeek rate.
|
|
221
|
+
* @param webValue - the `web` settings namespace value, or `null` when unread.
|
|
222
|
+
* @returns `{ search, fetch }` modes: `side`/`direct`/`other`, `local`/`api`/`other`.
|
|
223
|
+
*/
|
|
224
|
+
function searchBilling(webValue) {
|
|
225
|
+
const value = webValue !== null && typeof webValue === 'object' ? webValue : null
|
|
226
|
+
const providerOf = (field, fallback) => {
|
|
227
|
+
const id = value === null ? undefined : value[field]
|
|
228
|
+
return typeof id === 'string' && id !== '' ? id : fallback
|
|
229
|
+
}
|
|
230
|
+
const searchProvider = providerOf('searchProvider', MESSAGES_SEARCH_PROVIDER)
|
|
231
|
+
const fetchProvider = providerOf('fetchProvider', HTTP_FETCH_PROVIDER)
|
|
232
|
+
return {
|
|
233
|
+
search: searchProvider === DIRECT_SEARCH_PROVIDER
|
|
234
|
+
? 'direct'
|
|
235
|
+
: searchProvider === MESSAGES_SEARCH_PROVIDER ? 'side' : 'other',
|
|
236
|
+
fetch: fetchProvider === DIRECT_FETCH_PROVIDER
|
|
237
|
+
? 'api'
|
|
238
|
+
: fetchProvider === HTTP_FETCH_PROVIDER ? 'local' : 'other',
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Modes of the shipped selection, used until (or unless) settings resolve. */
|
|
243
|
+
const SHIPPED_SEARCH_BILLING = Object.freeze(searchBilling(null))
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Bare observable over the `web` row's provider selection for the
|
|
247
|
+
* registration's `hooks` compartment.
|
|
248
|
+
*
|
|
249
|
+
* The settings form is resolved lazily because the settings service may
|
|
250
|
+
* activate after this plugin, and an absent service must not remove the
|
|
251
|
+
* card: the shipped selection is the answer until one resolves. The
|
|
252
|
+
* snapshot keeps one identity until the form itself changes, because the
|
|
253
|
+
* renderer compares snapshots by reference.
|
|
254
|
+
* @param ctx - the plugin's client context.
|
|
255
|
+
* @returns `{ getSnapshot, subscribe }`.
|
|
256
|
+
*/
|
|
257
|
+
function webSearchBillingSource(ctx) {
|
|
258
|
+
let form
|
|
259
|
+
/** The `web` form, or `undefined` while the settings service is absent. */
|
|
260
|
+
const formOf = () => {
|
|
261
|
+
if (form !== undefined) return form
|
|
262
|
+
const forms = typeof ctx.get === 'function' ? ctx.get('configForms') : undefined
|
|
263
|
+
if (forms === undefined || forms === null || typeof forms.get !== 'function') return undefined
|
|
264
|
+
try {
|
|
265
|
+
form = forms.get('web')
|
|
266
|
+
} catch {
|
|
267
|
+
return undefined
|
|
268
|
+
}
|
|
269
|
+
return form
|
|
270
|
+
}
|
|
271
|
+
let cachedSnapshot
|
|
272
|
+
let cachedModes = SHIPPED_SEARCH_BILLING
|
|
273
|
+
return {
|
|
274
|
+
getSnapshot() {
|
|
275
|
+
const current = formOf()
|
|
276
|
+
if (current === undefined || typeof current.getSnapshot !== 'function') return SHIPPED_SEARCH_BILLING
|
|
277
|
+
const snapshot = current.getSnapshot()
|
|
278
|
+
if (snapshot !== cachedSnapshot) {
|
|
279
|
+
cachedSnapshot = snapshot
|
|
280
|
+
cachedModes = Object.freeze(searchBilling(snapshot !== null && typeof snapshot === 'object' ? snapshot.value : null))
|
|
281
|
+
}
|
|
282
|
+
return cachedModes
|
|
283
|
+
},
|
|
284
|
+
subscribe(listener) {
|
|
285
|
+
const current = formOf()
|
|
286
|
+
return current === undefined || typeof current.subscribe !== 'function'
|
|
287
|
+
? () => {}
|
|
288
|
+
: current.subscribe(listener)
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
199
293
|
/**
|
|
200
294
|
* Validate and normalize the Host wire view. Any value that does not match
|
|
201
295
|
* the version-1 shape with a positive token total yields null, so a
|
|
@@ -251,11 +345,18 @@ window.__ModuleLoader__.load({
|
|
|
251
345
|
/**
|
|
252
346
|
* The dock entry. Renders the collapsed pill always (when usage exists)
|
|
253
347
|
* and portals the expanded card above it while open.
|
|
254
|
-
* @param props - framework standard props
|
|
348
|
+
* @param props - framework standard props, the plugin's `locale` service,
|
|
349
|
+
* and the provider-mode hook bound from the registration's inject face.
|
|
255
350
|
*/
|
|
256
|
-
function BillingCard({ useProjection, locale }) {
|
|
351
|
+
function BillingCard({ useProjection, useWebSearchBilling, locale }) {
|
|
257
352
|
const t = useTranslate(locale)
|
|
258
353
|
const data = renderableView(useProjection(PROJECTION_KEY))
|
|
354
|
+
// A registration always supplies the hook; the shipped selection keeps a
|
|
355
|
+
// hand-mounted component rendering instead of throwing. The renderer's
|
|
356
|
+
// selector hook requires a selector, so pass identity.
|
|
357
|
+
const billing = typeof useWebSearchBilling === 'function'
|
|
358
|
+
? useWebSearchBilling(value => value)
|
|
359
|
+
: SHIPPED_SEARCH_BILLING
|
|
259
360
|
const visible = data !== null
|
|
260
361
|
const [open, setOpen] = React.useState(false)
|
|
261
362
|
const [position, setPosition] = React.useState(null)
|
|
@@ -393,8 +494,11 @@ window.__ModuleLoader__.load({
|
|
|
393
494
|
const searchRows = []
|
|
394
495
|
const searchNotes = []
|
|
395
496
|
if (showSearch) {
|
|
497
|
+
// The free window is a DeepSeek server-side fact: it holds only while the
|
|
498
|
+
// deployment still searches through Messages. The direct Search API has
|
|
499
|
+
// none, and an unknown provider is not claimed to have one.
|
|
396
500
|
const freeUntil = typeof search.searchFreeUntil === 'number' ? search.searchFreeUntil : null
|
|
397
|
-
const freePeriod = freeUntil !== null && Date.now() < freeUntil
|
|
501
|
+
const freePeriod = billing.search === 'side' && freeUntil !== null && Date.now() < freeUntil
|
|
398
502
|
const pushSearchRow = (key, label, value) => {
|
|
399
503
|
searchRows.push(h('dt', { key: `search-${key}-label`, className: 'bc-row-label' }, label))
|
|
400
504
|
searchRows.push(h('dd', { key: `search-${key}-value`, className: 'bc-row-value' }, value))
|
|
@@ -411,13 +515,19 @@ window.__ModuleLoader__.load({
|
|
|
411
515
|
? t('search.freeValue', { cost: searchCostText })
|
|
412
516
|
: searchCostText)
|
|
413
517
|
if (fetchCalls > 0) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
518
|
+
const fetchCount = formatCount(finite(search.fetchBillable))
|
|
519
|
+
// The projection counts every answered fetch; only the direct Fetch API
|
|
520
|
+
// charges for one, so the local HTTP provider reports no cost.
|
|
521
|
+
pushSearchRow('fetch', t('search.fetch'), billing.fetch === 'local'
|
|
522
|
+
? t('search.fetchLocalValue', { count: fetchCount })
|
|
523
|
+
: t('search.fetchValue', { count: fetchCount, cost: formatCost(finite(search.fetchCost), currency) }))
|
|
418
524
|
}
|
|
419
525
|
if (freePeriod) searchNotes.push(t('search.freeNote'))
|
|
420
|
-
if (
|
|
526
|
+
if (billing.search === 'direct') searchNotes.push(t('search.directNote'))
|
|
527
|
+
if (fetchCalls > 0) {
|
|
528
|
+
if (billing.fetch === 'api') searchNotes.push(t('search.fetchNote'))
|
|
529
|
+
else if (billing.fetch === 'local') searchNotes.push(t('search.fetchLocalNote'))
|
|
530
|
+
}
|
|
421
531
|
}
|
|
422
532
|
|
|
423
533
|
const panel = open ? ReactDOM.createPortal( h('div', {
|
|
@@ -519,8 +629,18 @@ window.__ModuleLoader__.load({
|
|
|
519
629
|
|
|
520
630
|
ctx.effect(() => registerDictionaries(ctx.locale))
|
|
521
631
|
|
|
632
|
+
// The `web` row's provider selection decides which Search / Fetch
|
|
633
|
+
// deployment this card is labelling; it is read from the settings
|
|
634
|
+
// domain rather than the projection so the fold stays replayable.
|
|
635
|
+
const webSearchBilling = webSearchBillingSource(ctx)
|
|
636
|
+
|
|
522
637
|
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register(
|
|
523
|
-
{
|
|
638
|
+
{
|
|
639
|
+
name: 'conversation.composer.dock',
|
|
640
|
+
id: 'billing-card',
|
|
641
|
+
order: 2,
|
|
642
|
+
inject: () => ({ hooks: { webSearchBilling } }),
|
|
643
|
+
},
|
|
524
644
|
props => h(BillingCard, { ...props, locale: ctx.locale })))
|
|
525
645
|
},
|
|
526
646
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-billing-card",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"engines": { "dsh": ">=0.2.0-rc.2 <0.3.0-0" },
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"keywords": ["dsh-plugin", "deepseek-harness", "dsh", "billing", "token", "usage", "cost", "deepseek"],
|
|
6
7
|
"publishConfig": { "registry": "https://registry.npmjs.org" },
|