dsh-cost-meter 1.7.21 → 1.7.23

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.
@@ -9,16 +9,9 @@ import { randomUUID } from 'node:crypto'
9
9
  import { readFileSync, writeFileSync, renameSync, mkdirSync, statSync } from 'node:fs'
10
10
  import { dirname } from 'node:path'
11
11
  import { gunzipSync, inflateSync, brotliDecompressSync } from 'node:zlib'
12
+ import { recordNativeSearchUsage } from './native-search-history.js'
13
+ export { NATIVE_SEARCH_USAGE_EVENT, isNativeSearchUsageEvent } from './native-search-events.js'
12
14
 
13
- export const NATIVE_SEARCH_USAGE_EVENT = 'cost-meter/native-search-usage'
14
- export function isNativeSearchUsageEvent(event) {
15
- const data = event?.data
16
- return event?.type === NATIVE_SEARCH_USAGE_EVENT && data?.provider === 'deepseek-official'
17
- && typeof data.model === 'string' && /^deepseek-[a-z\d._:-]{1,120}$/i.test(data.model)
18
- && typeof data.requestId === 'string' && /^[a-f\d]{8}(?:-[a-f\d]{4}){3}-[a-f\d]{12}$/i.test(data.requestId)
19
- && Number.isFinite(data.startedAtMs) && data.startedAtMs > 0
20
- && ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens'].every(key => Number.isSafeInteger(data.usage?.[key]) && data.usage[key] >= 0)
21
- }
22
15
  const MAX_BODY_BYTES = 4 * 1024 * 1024
23
16
  const monitors = new WeakMap()
24
17
 
@@ -253,7 +246,9 @@ export function installNativeSearchBilling(ctx, ledger) {
253
246
  const usage = event.usage
254
247
  ledger.account({ input: usage.inputTokens, output: usage.outputTokens, cacheRead: usage.cacheReadTokens, cacheWrite: usage.cacheWriteTokens, reasoning: usage.reasoningTokens }, event.model, session?.id, event.startedAtMs, event.provider)
255
248
  },
256
- record(session, event) { session.append(NATIVE_SEARCH_USAGE_EVENT, event) },
249
+ // Never infer an append contract from function source. Host logs remain read-only;
250
+ // the plugin journal retains exact usage for imports and currency repricing.
251
+ record(session, event) { recordNativeSearchUsage(ledger.path, session.id, event) },
257
252
  uncovered: (event) => coverage.add(event),
258
253
  })
259
254
  monitors.set(ledger, { coverage })
@@ -0,0 +1,11 @@
1
+ /** Shared, host-independent validation for the plugin's own metering records. */
2
+ export const NATIVE_SEARCH_USAGE_EVENT = 'cost-meter/native-search-usage'
3
+
4
+ export function isNativeSearchUsageEvent(event) {
5
+ const data = event?.data
6
+ return event?.type === NATIVE_SEARCH_USAGE_EVENT && data?.provider === 'deepseek-official'
7
+ && typeof data.model === 'string' && /^deepseek-[a-z\d._:-]{1,120}$/i.test(data.model)
8
+ && typeof data.requestId === 'string' && /^[a-f\d]{8}(?:-[a-f\d]{4}){3}-[a-f\d]{12}$/i.test(data.requestId)
9
+ && Number.isFinite(data.startedAtMs) && data.startedAtMs > 0
10
+ && ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens'].every(key => Number.isSafeInteger(data.usage?.[key]) && data.usage[key] >= 0)
11
+ }
@@ -0,0 +1,79 @@
1
+ /** Native-search usage belongs to the plugin, never to the host's event vocabulary. */
2
+ import { createHash } from 'node:crypto'
3
+ import { mkdirSync, openSync, writeSync, fsyncSync, closeSync, lstatSync, readdirSync, createReadStream } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { StringDecoder } from 'node:string_decoder'
6
+ import { NATIVE_SEARCH_USAGE_EVENT, isNativeSearchUsageEvent } from './native-search-events.js'
7
+
8
+ const validId = id => typeof id === 'string' && id.length > 0 && id.length <= 512
9
+ export const nativeSearchHistoryRoot = ledgerPath => ledgerPath ? `${ledgerPath}.native-search` : ''
10
+ const filename = id => `${createHash('sha256').update(id).digest('hex')}.jsonl`
11
+
12
+ export function recordNativeSearchUsage(ledgerPath, sessionId, data) {
13
+ if (!ledgerPath || !validId(sessionId) || !isNativeSearchUsageEvent({ type: NATIVE_SEARCH_USAGE_EVENT, data })) throw new Error('Invalid native-search history record')
14
+ const root = nativeSearchHistoryRoot(ledgerPath)
15
+ mkdirSync(root, { recursive: true, mode: 0o700 })
16
+ if (!lstatSync(root).isDirectory() || lstatSync(root).isSymbolicLink()) throw new Error('Unsafe native-search history directory')
17
+ const path = join(root, filename(sessionId))
18
+ try { if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink()) throw new Error('Unsafe native-search history file') } catch (error) { if (error.code !== 'ENOENT') throw error }
19
+ // Whitelist fields: no prompt, response body, credential, or Session object is persisted.
20
+ const event = { type: NATIVE_SEARCH_USAGE_EVENT, sessionId, time: data.startedAtMs, data: {
21
+ model: data.model, provider: data.provider, startedAtMs: data.startedAtMs, requestId: data.requestId,
22
+ usage: Object.fromEntries(['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens'].map(key => [key, data.usage[key]])),
23
+ } }
24
+ const fd = openSync(path, 'a', 0o600)
25
+ try {
26
+ // A leading newline isolates any torn final row from the next successful append.
27
+ const bytes = Buffer.from(`\n${JSON.stringify(event)}\n`)
28
+ let offset = 0
29
+ while (offset < bytes.length) offset += writeSync(fd, bytes, offset, bytes.length - offset)
30
+ fsyncSync(fd)
31
+ } finally { closeSync(fd) }
32
+ }
33
+
34
+ export function listNativeSearchHistory(ledgerPath, excludeIds = new Set(), onlyIds = null) {
35
+ const root = nativeSearchHistoryRoot(ledgerPath)
36
+ if (!root) return []
37
+ try {
38
+ if (lstatSync(root).isSymbolicLink()) return []
39
+ const excluded = new Set([...excludeIds].map(filename))
40
+ const included = onlyIds === null ? null : new Set([...onlyIds].map(filename))
41
+ return readdirSync(root, { withFileTypes: true }).filter(e => e.isFile() && /^[a-f\d]{64}\.jsonl$/.test(e.name) && !excluded.has(e.name) && (included === null || included.has(e.name))).map(e => join(root, e.name))
42
+ } catch { return [] }
43
+ }
44
+
45
+ /** Small, bounded lines; invalid/torn rows are ignored without joining adjacent calls. */
46
+ export async function readNativeSearchHistory(path, expectedId) {
47
+ const records = [], ids = new Set()
48
+ const info = lstatSync(path)
49
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 256 * 1024 * 1024) throw new Error('Invalid native-search history file')
50
+ const decoder = new StringDecoder('utf8')
51
+ let pending = '', dropping = false, sessionId = expectedId
52
+ const consume = text => {
53
+ const lines = text.split('\n')
54
+ lines[0] = pending + lines[0]
55
+ pending = lines.pop()
56
+ for (const line of lines) {
57
+ if (dropping) { dropping = false; continue }
58
+ if (line.length > 4096) continue
59
+ let row
60
+ try { row = JSON.parse(line) } catch { continue }
61
+ if (!validId(row?.sessionId) || !isNativeSearchUsageEvent(row) || !Number.isFinite(row.time) || row.time <= 0 || !path.endsWith(filename(row.sessionId))) continue
62
+ if (sessionId === undefined) sessionId = row.sessionId
63
+ if (row.sessionId !== sessionId || ids.has(row.data.requestId)) continue
64
+ ids.add(row.data.requestId)
65
+ records.push(row)
66
+ }
67
+ if (pending.length > 4096) { pending = ''; dropping = true }
68
+ }
69
+ for await (const bytes of createReadStream(path, { highWaterMark: 64 * 1024 })) consume(decoder.write(bytes))
70
+ consume(decoder.end())
71
+ // No newline means the append may have been interrupted: never infer its completion.
72
+ return { sessionId, records }
73
+ }
74
+
75
+ export async function nativeSearchRecordsFor(ledgerPath, sessionId) {
76
+ if (!ledgerPath || !validId(sessionId)) return []
77
+ try { return (await readNativeSearchHistory(join(nativeSearchHistoryRoot(ledgerPath), filename(sessionId)), sessionId)).records }
78
+ catch (error) { if (error.code === 'ENOENT') return []; throw error }
79
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util'
3
+ import { createRequire } from 'node:module'
4
+ import { pathToFileURL } from 'node:url'
5
+ import { join, resolve } from 'node:path'
6
+ import { lstat } from 'node:fs/promises'
7
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
8
+ import { listSessionLogFiles } from './session-log-files.js'
9
+ import { repairSessionLog } from './session-log-repair.js'
10
+
11
+ async function main() {
12
+ const { values } = parseArgs({ options: {
13
+ write: { type: 'boolean', default: false },
14
+ 'sessions-root': { type: 'string' }, 'host-modules': { type: 'string' },
15
+ help: { type: 'boolean', short: 'h' },
16
+ } })
17
+ if (values.help) {
18
+ console.log('dsh-cost-meter-repair-sessions [--sessions-root <目录>] [--write] [--host-modules <node_modules>]\n默认只检查。退出所有 DSH 后加 --write 修复;每份受影响日志先保留原样备份。\n写入需要同次 npx 安装 @deepseek-ai/dsh,或用 --host-modules 指向现有宿主的 node_modules。')
19
+ return
20
+ }
21
+ const root = resolve(values['sessions-root'] ?? join(resolveDshHome(), 'sessions'))
22
+ if (!(await lstat(root)).isDirectory()) throw new Error('会话根目录不存在或不是目录')
23
+ let acquireLease
24
+ if (values.write) {
25
+ const require = createRequire(values['host-modules'] ? join(resolve(values['host-modules']), '__cost_meter_repair__.cjs') : import.meta.url)
26
+ let Host
27
+ try { Host = (await import(pathToFileURL(require.resolve('@deepseek-ai/dsh-session-persistence-jsonl')).href)).default }
28
+ catch { throw new Error('未找到可用宿主:请在 npx 命令中同时指定 --package=@deepseek-ai/dsh@0.1.5-rc.2,或传 --host-modules') }
29
+ if (typeof Host?.prototype?.acquireLease !== 'function') throw new Error('该宿主没有兼容的会话锁接口,未写入任何日志')
30
+ acquireLease = (directory, id) => Host.prototype.acquireLease.call(null, id, undefined, directory)
31
+ }
32
+ const totals = { scanned: 0, affected: 0, events: 0, repaired: 0, failed: 0 }
33
+ console.log(`${values.write ? '修复' : '只读检查'}:${root}`)
34
+ for (const path of listSessionLogFiles(root, null, true)) {
35
+ totals.scanned++
36
+ try {
37
+ const result = await repairSessionLog(path, { write: values.write, acquireLease })
38
+ if (!result.changedEvents) continue
39
+ totals.affected++; totals.events += result.changedEvents
40
+ if (result.written) totals.repaired++
41
+ console.log(JSON.stringify({ path, events: result.changedEvents, written: result.written, backup: result.backup }))
42
+ } catch (error) {
43
+ totals.failed++
44
+ console.error(JSON.stringify({ path, error: error.message }))
45
+ }
46
+ }
47
+ console.log(JSON.stringify(totals))
48
+ if (totals.failed) process.exitCode = 1
49
+ }
50
+
51
+ main().catch(error => { console.error(error.message); process.exitCode = 1 })
@@ -0,0 +1,30 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ /** Canonical committed artifacts only: exclude backups, temporary files and aliases. */
5
+ export function sessionLogGeneration(name) {
6
+ const match = /^session(?:\.v([1-9]\d*))?\.jsonl(\.zstd)?$/.exec(name)
7
+ if (!match) return null
8
+ const version = Number(match[1] ?? 0)
9
+ return Number.isSafeInteger(version) ? { version, compressed: Boolean(match[2]) } : null
10
+ }
11
+
12
+ export function listSessionLogFiles(root, onlySessionIds = null, allGenerations = false) {
13
+ const entries = path => { try { return readdirSync(path, { withFileTypes: true }) } catch { return [] } }
14
+ const paths = []
15
+ for (const project of entries(root)) {
16
+ if (!project.isDirectory()) continue
17
+ for (const session of entries(join(root, project.name))) {
18
+ if (!session.isDirectory() || (onlySessionIds !== null && !onlySessionIds.has(session.name))) continue
19
+ const directory = join(root, project.name, session.name)
20
+ const candidates = entries(directory).filter(entry => entry.isFile()).flatMap(entry => {
21
+ const generation = sessionLogGeneration(entry.name)
22
+ return generation ? [{ name: entry.name, ...generation }] : []
23
+ }).sort((a, b) => b.version - a.version || Number(b.compressed) - Number(a.compressed))
24
+ // DSH keeps older generations after migration; they share the same event prefix.
25
+ // Billing uses exactly one generation per session, with numeric v10 > v3 ordering.
26
+ for (const entry of allGenerations ? candidates : candidates.slice(0, 1)) paths.push(join(directory, entry.name))
27
+ }
28
+ }
29
+ return paths
30
+ }
@@ -0,0 +1,175 @@
1
+ /** Offline repair for v1.7.21's unknown native-search events. Never called at startup. */
2
+ import { open, lstat, realpath, copyFile, rename, unlink } from 'node:fs/promises'
3
+ import { createReadStream, constants } from 'node:fs'
4
+ import { createHash, randomUUID } from 'node:crypto'
5
+ import { dirname, basename, resolve } from 'node:path'
6
+ import * as zlib from 'node:zlib'
7
+ import { scanZstdFrames } from './backfill.js'
8
+ import { NATIVE_SEARCH_USAGE_EVENT, isNativeSearchUsageEvent } from './native-search-events.js'
9
+ import { sessionLogGeneration } from './session-log-files.js'
10
+
11
+ const MAX_FRAME = 64 * 1024 * 1024
12
+ const MAX_TOTAL = 4 * 1024 * 1024 * 1024
13
+ const identity = info => [info.dev, info.ino, info.size, info.mtimeNs, info.ctimeNs].join(':')
14
+
15
+ async function canonicalLogPath(path) {
16
+ // realpath also expands Windows 8.3 names (e.g. RUNNER~1), which are ordinary
17
+ // aliases, not symbolic links. Check links explicitly, then use one physical
18
+ // identity for validation. The host lock can still depend on the supplied spelling.
19
+ for (let current = path; ; current = dirname(current)) {
20
+ if ((await lstat(current)).isSymbolicLink()) throw new Error('不修复符号链接或目录链接中的日志')
21
+ if (dirname(current) === current) break
22
+ }
23
+ return realpath(path)
24
+ }
25
+
26
+ async function hashFile(path) {
27
+ const hash = createHash('sha256')
28
+ for await (const bytes of createReadStream(path)) hash.update(bytes)
29
+ return hash.digest('hex')
30
+ }
31
+
32
+ /** Copy untouched compressed frames byte for byte; join only frames that split a row. */
33
+ async function* logGroups(path, compressed) {
34
+ let tail = Buffer.alloc(0), rawParts = [], plainParts = [], groupSize = 0, rawSize = 0, total = 0
35
+ const group = (raw, plain) => {
36
+ total += plain.length
37
+ groupSize += plain.length
38
+ rawSize += raw.length
39
+ if (groupSize > MAX_FRAME || rawSize > MAX_FRAME || total > MAX_TOTAL) throw new Error('会话日志超过修复内存预算,原文件未改动')
40
+ rawParts.push(raw); plainParts.push(plain)
41
+ if (plain.length === 0 || plain.at(-1) !== 10) return null
42
+ const out = { raw: Buffer.concat(rawParts, rawSize), plain: Buffer.concat(plainParts, groupSize) }
43
+ rawParts = []; plainParts = []; groupSize = 0; rawSize = 0
44
+ return out
45
+ }
46
+ for await (const bytes of createReadStream(path, { highWaterMark: 1024 * 1024 })) {
47
+ const data = tail.length ? Buffer.concat([tail, bytes]) : bytes
48
+ if (!compressed) {
49
+ const end = data.lastIndexOf(10) + 1
50
+ if (end > MAX_FRAME) throw new Error('会话日志行过大,原文件未改动')
51
+ if (end) yield { raw: data.subarray(0, end), plain: data.subarray(0, end) }
52
+ tail = Buffer.from(data.subarray(end))
53
+ total += end
54
+ if (total > MAX_TOTAL || tail.length > MAX_FRAME) throw new Error('会话日志超过修复内存预算,原文件未改动')
55
+ continue
56
+ }
57
+ const frames = scanZstdFrames(data)
58
+ for (const frame of frames) {
59
+ const raw = data.subarray(frame.start, frame.end)
60
+ const plain = zlib.zstdDecompressSync(raw, { maxOutputLength: MAX_FRAME })
61
+ const out = group(raw, plain)
62
+ if (out) yield out
63
+ }
64
+ tail = Buffer.from(data.subarray(frames.at(-1)?.end ?? 0))
65
+ if (tail.length > MAX_FRAME) throw new Error('Zstandard 帧损坏或过大,原文件未改动')
66
+ }
67
+ if (compressed && tail.length) throw new Error('Zstandard 尾帧不完整或损坏,原文件未改动')
68
+ if (plainParts.length || tail.length) throw new Error('会话日志末行未完整落盘,原文件未改动')
69
+ }
70
+
71
+ function repairGroup(bytes, state) {
72
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
73
+ const lines = text.split('\n')
74
+ let changes = 0
75
+ for (let i = 0; i < lines.length - 1; i++) {
76
+ const line = lines[i]
77
+ if (state.rows++ === 0) {
78
+ const header = JSON.parse(line)
79
+ if (header?.type !== 'session' || typeof header.id !== 'string' || !Number.isSafeInteger(header.createdAt)) throw new Error('会话日志缺少合法头记录,原文件未改动')
80
+ state.sessionId = header.id
81
+ continue
82
+ }
83
+ if (!line.includes(NATIVE_SEARCH_USAGE_EVENT)) continue
84
+ const row = JSON.parse(line)
85
+ if (row?.type !== NATIVE_SEARCH_USAGE_EVENT || row.ignorable === true) continue
86
+ if (!isNativeSearchUsageEvent(row) || !Number.isSafeInteger(row.seq) || row.seq < 0 || !Number.isSafeInteger(row.time) || row.time <= 0) throw new Error('原生搜索记录形态异常,未自动修改')
87
+ if (Object.hasOwn(row, 'ignorable')) lines[i] = JSON.stringify({ ...row, ignorable: true })
88
+ else {
89
+ // Preserve every existing byte in the affected row when adding the missing marker.
90
+ const end = line.lastIndexOf('}')
91
+ lines[i] = `${line.slice(0, end)},"ignorable":true${line.slice(end)}`
92
+ }
93
+ changes++
94
+ }
95
+ state.changedEvents += changes
96
+ return changes ? Buffer.from(lines.join('\n')) : null
97
+ }
98
+
99
+ async function writeAll(handle, bytes) {
100
+ let offset = 0
101
+ while (offset < bytes.length) offset += (await handle.write(bytes, offset, bytes.length - offset)).bytesWritten
102
+ }
103
+
104
+ /**
105
+ * Default is read-only. Writes require the actual host's kernel Session lease.
106
+ * Each replacement keeps an exclusive byte-identical backup and verifies source identity.
107
+ */
108
+ export async function repairSessionLog(path, { write = false, acquireLease } = {}) {
109
+ path = resolve(path)
110
+ const generation = sessionLogGeneration(basename(path))
111
+ if (!generation) throw new Error('只接受规范的 session[.vN].jsonl[.zstd] 文件')
112
+ if (generation.compressed && (typeof zlib.zstdDecompressSync !== 'function' || typeof zlib.zstdCompressSync !== 'function')) throw new Error('修复 Zstandard 日志需要 Node 22.15+ 或 Node 24')
113
+ const supplied = await lstat(path, { bigint: true })
114
+ if (!supplied.isFile() || supplied.isSymbolicLink()) throw new Error('不修复符号链接或非普通日志文件')
115
+ const physicalPath = await canonicalLogPath(path)
116
+ const initial = await lstat(physicalPath, { bigint: true })
117
+ if (identity(initial) !== identity(supplied)) throw new Error('日志在路径解析期间已变化,未自动修改')
118
+ const state = { path, rows: 0, sessionId: '', changedEvents: 0, written: false, backup: null }
119
+ const leases = []
120
+ let output, temporary
121
+ try {
122
+ if (write) {
123
+ if (typeof acquireLease !== 'function') throw new Error('写入修复需要宿主会话锁,请使用带 @deepseek-ai/dsh 的修复命令')
124
+ // Windows host semaphores hash resolve(path), without expanding 8.3 aliases.
125
+ // Hold both supplied and physical names so either host spelling is protected.
126
+ const directories = [dirname(path)]
127
+ if (process.platform === 'win32' && dirname(path).toLowerCase() !== dirname(physicalPath).toLowerCase()) directories.push(dirname(physicalPath))
128
+ for (const directory of directories) {
129
+ const lease = await acquireLease(directory, basename(dirname(path)))
130
+ leases.push(lease)
131
+ if (typeof lease?.release !== 'function') throw new Error('宿主未提供可释放的会话锁,停止修复')
132
+ }
133
+ temporary = `${path}.cost-meter-repair-${randomUUID()}.tmp`
134
+ output = await open(temporary, 'wx', Number(initial.mode & 0o777n))
135
+ }
136
+ const before = await lstat(path, { bigint: true })
137
+ if (identity(before) !== identity(initial)) throw new Error('日志在取得锁之前已变化,请退出宿主后重试')
138
+ const sourceHash = createHash('sha256')
139
+ for await (const group of logGroups(path, generation.compressed)) {
140
+ sourceHash.update(group.raw)
141
+ const updated = repairGroup(group.plain, state)
142
+ if (output) {
143
+ const bytes = !updated ? group.raw : generation.compressed ? zlib.zstdCompressSync(updated, {
144
+ params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 },
145
+ }) : updated
146
+ await writeAll(output, bytes)
147
+ }
148
+ }
149
+ if (state.rows === 0) throw new Error('空日志,未自动修改')
150
+ const expectedHash = sourceHash.digest('hex')
151
+ if (identity(await lstat(path, { bigint: true })) !== identity(before)) throw new Error('日志在扫描期间已变化,请退出宿主后重试')
152
+ if (output && state.changedEvents) {
153
+ await output.sync()
154
+ await output.close(); output = null
155
+ const backup = `${path}.cost-meter-backup-${Date.now()}-${randomUUID()}`
156
+ await copyFile(path, backup, constants.COPYFILE_EXCL)
157
+ state.backup = backup
158
+ const backupHandle = await open(backup, 'r+')
159
+ try { await backupHandle.sync() } finally { await backupHandle.close() }
160
+ if (await hashFile(backup) !== expectedHash || await hashFile(path) !== expectedHash || identity(await lstat(path, { bigint: true })) !== identity(before)) throw new Error('日志或备份在写入前发生变化,原文件未替换')
161
+ await rename(temporary, path)
162
+ temporary = null
163
+ state.written = true
164
+ if (process.platform !== 'win32') {
165
+ const directory = await open(dirname(path), 'r')
166
+ try { await directory.sync() } finally { await directory.close() }
167
+ }
168
+ }
169
+ return state
170
+ } finally {
171
+ if (output) await output.close()
172
+ if (temporary) await unlink(temporary).catch(() => {})
173
+ for (const lease of leases.reverse()) await lease?.release?.()
174
+ }
175
+ }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.7.21",
3
+ "version": "1.7.23",
4
4
  "description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(9 家,含 SCNet Token Plan 本地 Credits 计量与火山方舟 Volcano Ark Coding Plan AK/SK 签名查询)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条、峰谷计价时段显示与切换前弹窗/系统通知提醒(位置/提前量/类型可配),界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 9 vendors (incl. local credits metering for the SCNet Token Plan and Volcano Ark Coding Plan via AK/SK signature), plan/API dual-track billing with per-1% & full-window Token Plan usage estimates, custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, plus peak/off-peak pricing display with pre-switch popup & system-notification alerts (position / lead time / type configurable), in a bilingual (Chinese/English) UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
+ "bin": { "dsh-cost-meter-repair-sessions": "./lib/repair-sessions-cli.js" },
7
8
  "exports": {
8
9
  ".": {
9
10
  "default": "./lib/index.js"