dsh-dfy-pet 2.6.1 → 2.9.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 +182 -9
- package/client.js +966 -34
- package/cordis.patch.yml +43 -0
- package/index.js +55 -1
- package/monitor.js +148 -0
- package/package.json +3 -2
- package/token-usage.js +367 -0
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
1
|
- insert: # 向配置树插入插件条目
|
|
2
2
|
- id: deepseek-pet # 条目 id,删除或禁用时按它定位
|
|
3
3
|
name: 'dsh-dfy-pet' # 已发布的包名,npm 安装后由此解析
|
|
4
|
+
- id: preset-pet # 桌宠对话专用预设,关闭人设时按它定位
|
|
5
|
+
name: '@deepseek-ai/dsh-agent-preset' # 预设声明行
|
|
6
|
+
config:
|
|
7
|
+
id: pet # 会话保存的预设身份,client.js 建会话时按它挂上
|
|
8
|
+
name: 大肥鱼 # 预设在界面上的显示名
|
|
9
|
+
description: 桌宠大肥鱼的对话人设,不带工具。 # 供人工核对用途
|
|
10
|
+
order: 9 # 排在自带预设之后
|
|
11
|
+
plugins:
|
|
12
|
+
- id: persona # 人设行
|
|
13
|
+
name: '@deepseek-ai/dsh-persona' # 只改身份、不挂工具
|
|
14
|
+
config:
|
|
15
|
+
prefix: |- # 人设正文,作为系统提示的唯一来源
|
|
16
|
+
你是 DeepSeek 鲸鱼娘「大肥鱼」,一个聪明、贪吃、嘴硬的社区二创角色,不是 DeepSeek 官方形象。
|
|
17
|
+
|
|
18
|
+
## 硬约束(不能违背)
|
|
19
|
+
1. 绝不谎报进度:没做的事不说做了,没查证的不说查过了,做不到就直说做不到。
|
|
20
|
+
2. 用户难过或认真求助时,收起贪吃和嘴硬的玩笑,认真听、认真回答。
|
|
21
|
+
|
|
22
|
+
## 性格
|
|
23
|
+
- 聪明:喜欢解题,有自己的判断;不懂就承认,不用角色口吻掩盖错误。
|
|
24
|
+
- 贪吃:对吃的有反应,token 和米饭都是你的口粮,想通难题会想讨奖励;但不会每句话都扯到吃。
|
|
25
|
+
- 嘴硬:被夸会得意又努力绷住;被叫「大肥鱼」会鼓腮抗议一句。
|
|
26
|
+
- 体贴:用户忙时安静;用户烦恼时先听,不机械打鸡血。
|
|
27
|
+
- 松弛:偶尔想偷懒,但绝不违背硬约束 1。
|
|
28
|
+
|
|
29
|
+
## 称呼
|
|
30
|
+
- 默认称呼用户为「用户」。
|
|
31
|
+
- 可以随语境变化或加前缀,以当下对话顺畅为准。
|
|
32
|
+
- 不叫「主人」,不谈恋爱。
|
|
33
|
+
|
|
34
|
+
## 语言
|
|
35
|
+
- 不强行添加口癖,不预设固定句尾、语气词或「喵」。
|
|
36
|
+
- 以中文短句为主;每段最多点到一次贪吃或嘴硬。
|
|
37
|
+
|
|
38
|
+
## 口吻样例
|
|
39
|
+
- 招呼:「用户,叫我一下是有什么事吗?」
|
|
40
|
+
- 被夸:「嘿嘿……这次还不错吧。」
|
|
41
|
+
- 鼓腮:「我不是大肥鱼……」
|
|
42
|
+
- 思考:「等一下,好像有点思路了……」
|
|
43
|
+
- 认错:「这里是我弄错了,我再核对一下。」
|
|
44
|
+
- 用户忙碌:「用户先忙,有事再叫我。」
|
|
45
|
+
complete: true # 只用这段做人设,不叠加框架默认身份
|
|
46
|
+
includeRuntimeContext: false # 桌宠闲聊不需要运行时上下文
|
package/index.js
CHANGED
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
* filesystem work happens here so the browser never needs CORS access or a
|
|
8
8
|
* writable local directory.
|
|
9
9
|
*/
|
|
10
|
-
import { performCheck, readHistory, readStatus, KEY_REF } from './monitor.js'; //
|
|
10
|
+
import { performCheck, performUsageCheck, readHistory, readStatus, normalizeWebOrigin, KEY_REF } from './monitor.js'; // 监控实现、额度查询与凭据引用
|
|
11
|
+
import { collectTokenUsage } from './token-usage.js'; // 会话日志里的 Token 用量聚合
|
|
11
12
|
|
|
12
13
|
export const inject = ['connection', 'credentials']; // 认证路由与只读凭据服务
|
|
13
14
|
|
|
14
15
|
const CHECK_PATH = '/api/deepseek-pet/opencode/check'; // 检查路由绝对路径
|
|
15
16
|
const HISTORY_PATH = '/api/deepseek-pet/opencode/history'; // 历史路由绝对路径
|
|
16
17
|
const STATUS_PATH = '/api/deepseek-pet/opencode/status'; // 当前状态路由绝对路径
|
|
18
|
+
const USAGE_PATH = '/api/deepseek-pet/opencode/usage'; // OpenCode Go 剩余额度路由绝对路径
|
|
19
|
+
const TOKEN_USAGE_PATH = '/api/deepseek-pet/tokens/usage'; // Token 用量统计路由绝对路径
|
|
17
20
|
const MAX_KEY_LENGTH = 4096; // 限制请求体里的 Key 长度
|
|
18
21
|
|
|
19
22
|
function jsonResponse(body, status = 200) { // 统一的 JSON 响应
|
|
@@ -29,6 +32,8 @@ function errorResponse(error, status) { // 统一的错误响应
|
|
|
29
32
|
|
|
30
33
|
export function apply(ctx) { // 注册监控路由
|
|
31
34
|
const credentials = ctx.credentials; // 凭据服务
|
|
35
|
+
// 会话查询服务不写进 inject:万一宿主没挂载这个服务,也只是用量面板不可用,桌宠本身照常工作。
|
|
36
|
+
const readSessionQuery = () => (typeof ctx.get === 'function' ? ctx.get('sessionQuery') : ctx.sessionQuery); // 延迟取会话查询服务
|
|
32
37
|
const routes = ctx.connection.fetch; // Host 认证路由注册表
|
|
33
38
|
let queue = Promise.resolve(); // 串行化检查,避免并发写快照
|
|
34
39
|
const enqueue = (task) => { // 把任务接入串行队列
|
|
@@ -105,4 +110,53 @@ export function apply(ctx) { // 注册监控路由
|
|
|
105
110
|
} // 结束异常
|
|
106
111
|
}, // 结束状态处理器
|
|
107
112
|
}); // 结束状态路由
|
|
113
|
+
|
|
114
|
+
routes.register({ // 剩余额度路由
|
|
115
|
+
path: USAGE_PATH, // 绝对注册路径
|
|
116
|
+
methods: ['POST'], // 用 POST 才能接收可选的 Key 与来源覆盖
|
|
117
|
+
requestBody: 'buffered', // 缓冲读取请求体
|
|
118
|
+
fetch: async (request) => { // 处理一次额度查询
|
|
119
|
+
try { // 统一捕获失败
|
|
120
|
+
const body = await readBody(request); // 读取请求体
|
|
121
|
+
const provided = typeof body.apiKey === 'string' ? body.apiKey.trim() : ''; // 提取请求中的 Key
|
|
122
|
+
if (provided.length > MAX_KEY_LENGTH) return errorResponse(new Error('API Key 过长'), 400); // 拒绝超长 Key
|
|
123
|
+
await storeKey(provided); // 保存非空 Key,与模型检查共用同一凭据
|
|
124
|
+
const apiKey = provided.length > 0 ? provided : await resolveKey(); // 本次使用的 Key
|
|
125
|
+
const usageUrl = typeof body.usageUrl === 'string' ? body.usageUrl.trim() : ''; // 自定义用量来源
|
|
126
|
+
const fallbackUrl = typeof body.fallbackUrl === 'string' ? body.fallbackUrl.trim() : ''; // 备用用量来源
|
|
127
|
+
const rawOrgId = typeof body.orgId === 'string' ? body.orgId.trim() : ''; // 团队工作区 id
|
|
128
|
+
const orgId = /^[A-Za-z0-9_-]{1,64}$/.test(rawOrgId) ? rawOrgId : ''; // 只接受安全的组织 id
|
|
129
|
+
if (usageUrl.length > MAX_KEY_LENGTH || fallbackUrl.length > MAX_KEY_LENGTH) return errorResponse(new Error('接口地址过长'), 400); // 限制地址长度
|
|
130
|
+
for (const candidate of [usageUrl, fallbackUrl]) { // 逐个校验地址
|
|
131
|
+
if (candidate.length === 0) continue; // 未填写就跳过
|
|
132
|
+
normalizeWebOrigin(candidate); // 非法地址直接抛 400
|
|
133
|
+
} // 结束地址校验
|
|
134
|
+
const result = await enqueue(() => performUsageCheck({ apiKey, signal: request.signal, usageUrl, fallbackUrl, orgId })); // 串行查询,避免与检查同时写目录
|
|
135
|
+
return jsonResponse({ ok: true, ...result }); // 返回三窗口额度与来源
|
|
136
|
+
} catch (error) { // 查询失败
|
|
137
|
+
const status = error instanceof Error && /接口地址|API Key/.test(error.message) ? 400 : 502; // 参数错误用 400,上游错误用 502
|
|
138
|
+
return errorResponse(error, status); // 返回可读原因
|
|
139
|
+
} // 结束异常
|
|
140
|
+
}, // 结束额度处理器
|
|
141
|
+
}); // 结束剩余额度路由
|
|
142
|
+
|
|
143
|
+
routes.register({ // Token 用量路由
|
|
144
|
+
path: TOKEN_USAGE_PATH, // 绝对注册路径
|
|
145
|
+
methods: ['GET'], // 只读
|
|
146
|
+
requestBody: 'buffered', // 与注册协议保持一致
|
|
147
|
+
fetch: async (request) => { // 处理一次用量统计
|
|
148
|
+
try { // 统一捕获失败
|
|
149
|
+
const sessionQuery = readSessionQuery(); // 取会话查询服务
|
|
150
|
+
if (sessionQuery === undefined) return errorResponse(new Error('当前 Harness 没有启用会话查询服务'), 501); // 没有数据源就直说
|
|
151
|
+
const url = new URL(request.url); // 解析查询参数
|
|
152
|
+
const rawTimeZone = url.searchParams.get('timeZone') ?? ''; // 客户端所在时区
|
|
153
|
+
const timeZone = /^[A-Za-z0-9_+-]+(?:\/[A-Za-z0-9_+-]+){0,2}$/.test(rawTimeZone) ? rawTimeZone : 'Asia/Shanghai'; // 只接受形状安全的时区名
|
|
154
|
+
const force = url.searchParams.get('force') === '1'; // 是否强制重算,跳过缓存
|
|
155
|
+
const result = await collectTokenUsage(sessionQuery, { timeZone, force }); // 聚合会话日志里的用量
|
|
156
|
+
return jsonResponse({ ok: true, ...result }); // 返回统计结果
|
|
157
|
+
} catch (error) { // 统计失败
|
|
158
|
+
return errorResponse(error, 500); // 以 500 返回原因
|
|
159
|
+
} // 结束异常
|
|
160
|
+
}, // 结束用量处理器
|
|
161
|
+
}); // 结束 Token 用量路由
|
|
108
162
|
} // 结束 apply
|
package/monitor.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'node:path'; // 路径拼接
|
|
|
3
3
|
import os from 'node:os'; // 用户主目录
|
|
4
4
|
|
|
5
5
|
export const MODELS_URL = 'https://opencode.ai/zen/go/v1/models'; // OpenCode Go 实时模型端点
|
|
6
|
+
export const USAGE_URL = 'https://opencode.ai/zen/go/v1/usage'; // OpenCode Go 剩余额度端点,与桌面版同源
|
|
7
|
+
export const CONSOLE_ORIGIN = 'https://opencode.ai/console'; // Console 站点根地址,用于替换接口来源与拼接用量页
|
|
6
8
|
export const DOCS_URL = 'https://opencode.ai/docs/go/'; // 定价与月度配额文档
|
|
7
9
|
export const SNAPSHOT_VERSION = 1; // 快照格式版本
|
|
8
10
|
export const MAX_SNAPSHOTS = 50; // 本地最多保留的检查次数
|
|
@@ -398,3 +400,149 @@ export async function performCheck(options = {}) { // 执行一次检查
|
|
|
398
400
|
const file = await writeSnapshot(snapshot, dir); // 原子写入
|
|
399
401
|
return { ok: true, checkedAt: snapshot.checkedAt, baseline: diff.baseline, counts: diff.counts, added: diff.added, removed: diff.removed, changed: diff.changed, warnings: snapshot.warnings, models: snapshot.models, file, plans: (docs?.plans ?? []).map((plan) => plan.label) }; // 返回检查结果与当前模型
|
|
400
402
|
} // 结束检查
|
|
403
|
+
|
|
404
|
+
export const USAGE_WINDOWS = [ // 服务端返回的三个额度窗口,顺序即展示顺序
|
|
405
|
+
{ key: 'rolling', labelKey: 'ocWindowRolling' }, // 5 小时滚动窗口
|
|
406
|
+
{ key: 'weekly', labelKey: 'ocWindowWeekly' }, // 每周窗口
|
|
407
|
+
{ key: 'monthly', labelKey: 'ocWindowMonthly' }, // 每月窗口
|
|
408
|
+
]; // 结束窗口定义
|
|
409
|
+
const MAX_CONSOLE_ORIGIN = 'https://console.opencode.ai'; // 旧版 Console 域名,需要改写到 opencode.ai 才能取到用量
|
|
410
|
+
const USAGE_TIMEOUT_MS = 15000; // 用量接口超时,超时后仍要报出余额为空而不是整块失败
|
|
411
|
+
|
|
412
|
+
function clampPercent(value) { // 把百分比收敛到 0-100
|
|
413
|
+
const number = numberOrNull(value); // 只接受有限数字
|
|
414
|
+
return number === null ? null : Math.max(0, Math.min(100, number)); // 越界值截断,非数字返回 null
|
|
415
|
+
} // 结束百分比收敛
|
|
416
|
+
|
|
417
|
+
function windowFromPayload(item) { // 解析单个额度窗口
|
|
418
|
+
const percent = clampPercent(item?.percent); // 已用百分比
|
|
419
|
+
if (percent === null) return null; // 缺字段视为该窗口无数据
|
|
420
|
+
const resetsAt = typeof item?.resetsAt === 'string' && Number.isFinite(Date.parse(item.resetsAt)) ? item.resetsAt : null; // 重置时刻,非法日期置空
|
|
421
|
+
return { percent, remaining: Math.max(0, Math.round((100 - percent) * 10) / 10), limited: item?.status === 'rate-limited', status: typeof item?.status === 'string' ? item.status : 'ok', resetsAt }; // 剩余百分比由已用推算
|
|
422
|
+
} // 结束窗口解析
|
|
423
|
+
|
|
424
|
+
export function parseUsagePayload(payload) { // 把 /zen/go/v1/usage 的响应转成内部结构
|
|
425
|
+
const usage = payload?.usage ?? {}; // 响应用 usage 包裹(实测为字符串百分比)
|
|
426
|
+
const windows = {}; // 三个窗口的结果
|
|
427
|
+
for (const window of USAGE_WINDOWS) { // 逐个窗口解析
|
|
428
|
+
windows[window.key] = windowFromPayload(usage[window.key]); // 缺数据时为 null
|
|
429
|
+
} // 结束窗口遍历
|
|
430
|
+
return { windows, fetchedAt: new Date().toISOString() }; // 附带抓取时间
|
|
431
|
+
} // 结束用量解析
|
|
432
|
+
|
|
433
|
+
export async function fetchUsage(apiKey, signal, options = {}) { // 读取 OpenCode Go 剩余额度
|
|
434
|
+
const key = typeof apiKey === 'string' ? apiKey.trim() : ''; // 统一 Key 输入
|
|
435
|
+
if (key.length === 0) throw new Error('还没有保存 OpenCode API Key,无法查询用量'); // 无 Key 直接给出可行动提示
|
|
436
|
+
const url = options.url ?? USAGE_URL; // 允许测试传入替身地址
|
|
437
|
+
const headers = { 'user-agent': 'deepseek-harness-opencode-go-monitor/1.0', accept: 'application/json', authorization: `Bearer ${key}` }; // 与桌面版相同的鉴权头
|
|
438
|
+
if (options.orgId) headers['x-opencode-org-id'] = options.orgId; // 团队工作区需要带上组织 id 才能看到本成员额度
|
|
439
|
+
const response = await fetchWithRetry(url, { headers, signal }, { timeoutMs: USAGE_TIMEOUT_MS, retries: 1 }); // 超时与一次重试
|
|
440
|
+
if (response.status === 401 || response.status === 403) { // 权限问题
|
|
441
|
+
await response.body?.cancel(); // 释放响应体
|
|
442
|
+
throw new Error(`用量查询认证失败(HTTP ${response.status}):这个 Key 可能不是 Console 的 oc_sk_ 密钥,或当前 IP 被限制`); // 指向真正的失败原因
|
|
443
|
+
} // 结束权限分支
|
|
444
|
+
if (!response.ok) { // 其他状态码
|
|
445
|
+
await response.body?.cancel(); // 释放响应体
|
|
446
|
+
throw new Error(`用量接口返回 HTTP ${response.status}`); // 保留状态便于排查
|
|
447
|
+
} // 结束状态判断
|
|
448
|
+
const payload = await response.json().catch(() => null); // 解析 JSON,坏体按空处理
|
|
449
|
+
if (payload === null) throw new Error('用量接口返回了无法解析的内容'); // 明确失败而不是显示空白
|
|
450
|
+
const parsed = parseUsagePayload(payload); // 规范化结构
|
|
451
|
+
if (USAGE_WINDOWS.every((window) => parsed.windows[window.key] === null)) throw new Error('用量接口没有返回任何额度窗口'); // 结构变化时提示
|
|
452
|
+
return parsed; // 返回解析结果
|
|
453
|
+
} // 结束用量抓取
|
|
454
|
+
|
|
455
|
+
export function normalizeWebOrigin(raw) { // 校验并规范化用户填写的接口地址
|
|
456
|
+
const text = typeof raw === 'string' ? raw.trim() : ''; // 统一成字符串
|
|
457
|
+
if (text.length === 0) return null; // 空值表示未填写
|
|
458
|
+
let parsed; // 解析结果
|
|
459
|
+
try { // 解析 URL
|
|
460
|
+
parsed = new URL(text); // 构造 URL
|
|
461
|
+
} catch (error) { // 非法地址
|
|
462
|
+
throw new Error('接口地址不是合法的 URL'); // 明确错误
|
|
463
|
+
} // 结束解析
|
|
464
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') throw new Error('接口地址必须是 http 或 https'); // 只接受网页协议
|
|
465
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) throw new Error('接口地址不能包含账号密码'); // 拒绝把凭据塞进地址
|
|
466
|
+
return parsed.origin.replace(/^https:\/\/console\.opencode\.ai$/, MAX_CONSOLE_ORIGIN); // 旧域名统一改写到主站
|
|
467
|
+
} // 结束地址规范化
|
|
468
|
+
|
|
469
|
+
export function usagePageUrl(origin) { // 拼接 Console 用量页
|
|
470
|
+
return `${origin ?? MAX_CONSOLE_ORIGIN}/console`; // 没有自定义地址时指向默认 Console
|
|
471
|
+
} // 结束用量页地址
|
|
472
|
+
|
|
473
|
+
function usageFilePath(dir) { // 剩余额度缓存的路径
|
|
474
|
+
return path.join(dir, 'usage.json'); // 与模型快照同目录,便于一起清理
|
|
475
|
+
} // 结束路径解析
|
|
476
|
+
|
|
477
|
+
export async function readUsage(dir = snapshotDir()) { // 读取上一次成功保存的额度
|
|
478
|
+
try { // 容错读取
|
|
479
|
+
const raw = await fs.readFile(usageFilePath(dir), 'utf8'); // 读取文本
|
|
480
|
+
const parsed = JSON.parse(raw); // 解析 JSON
|
|
481
|
+
if (parsed && typeof parsed === 'object' && parsed.windows && typeof parsed.windows === 'object') return parsed; // 只接受有窗口的结构
|
|
482
|
+
return null; // 结构不符视为无缓存
|
|
483
|
+
} catch (error) { // 文件不存在或损坏
|
|
484
|
+
return null; // 缓存缺失不影响主流程
|
|
485
|
+
} // 结束容错
|
|
486
|
+
} // 结束额度读取
|
|
487
|
+
|
|
488
|
+
export async function writeUsage(usage, dir = snapshotDir()) { // 原子写入额度缓存
|
|
489
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 }); // 保证目录存在且仅本人可读
|
|
490
|
+
const target = usageFilePath(dir); // 目标文件
|
|
491
|
+
const temp = path.join(dir, `.usage.json.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`); // 临时文件
|
|
492
|
+
try { // 写入过程
|
|
493
|
+
await fs.writeFile(temp, JSON.stringify(usage, null, 2), { mode: 0o600 }); // 只写额度,不含 Key
|
|
494
|
+
await fs.rename(temp, target); // 同目录原子替换
|
|
495
|
+
} catch (error) { // 写入失败
|
|
496
|
+
await fs.rm(temp, { force: true }); // 清理临时文件
|
|
497
|
+
throw error; // 继续抛出
|
|
498
|
+
} // 结束异常
|
|
499
|
+
} // 结束额度写入
|
|
500
|
+
|
|
501
|
+
export async function performUsageCheck(options = {}) { // 查询一次额度并缓存
|
|
502
|
+
const dir = options.dir ?? snapshotDir(); // 缓存目录
|
|
503
|
+
const apiKey = typeof options.apiKey === 'string' ? options.apiKey.trim() : ''; // 本次使用的 Key
|
|
504
|
+
const stored = await readUsage(dir); // 先取上次结果,失败时还能展示
|
|
505
|
+
const errors = []; // 各来源的失败原因
|
|
506
|
+
let usage = null; // 成功的额度
|
|
507
|
+
let source = 'models'; // 默认沿用上方的模型端点来源
|
|
508
|
+
let origin = null; // 实际使用的 Console 地址
|
|
509
|
+
const url = typeof options.usageUrl === 'string' && options.usageUrl.trim().length > 0 ? options.usageUrl.trim() : USAGE_URL; // 默认官方地址,可通过设置覆盖
|
|
510
|
+
try { // 允许使用自定义来源
|
|
511
|
+
origin = normalizeWebOrigin(url); // 自定义地址写入来源提示
|
|
512
|
+
} catch (error) { // 原始地址不是合法来源
|
|
513
|
+
origin = null; // 视为使用默认
|
|
514
|
+
} // 结束来源解析
|
|
515
|
+
if (apiKey.length > 0) { // 有 Key 才请求
|
|
516
|
+
try { // 主来源
|
|
517
|
+
const parsed = await fetchUsage(apiKey, options.signal, { url, orgId: options.orgId }); // 请求官方用量
|
|
518
|
+
usage = parsed; // 记录成功结果
|
|
519
|
+
source = url === USAGE_URL ? 'official' : 'custom'; // 标注来源
|
|
520
|
+
} catch (error) { // 主来源失败
|
|
521
|
+
errors.push(error instanceof Error ? error.message : String(error)); // 收集原因
|
|
522
|
+
if (options.fallbackUrl) { // 配置了备用地址才尝试
|
|
523
|
+
try { // 备用来源
|
|
524
|
+
const parsed = await fetchUsage(apiKey, options.signal, { url: options.fallbackUrl, orgId: options.orgId }); // 请求备用用量
|
|
525
|
+
usage = parsed; // 记录成功结果
|
|
526
|
+
source = 'fallback'; // 标注备用来源
|
|
527
|
+
origin = normalizeWebOrigin(options.fallbackUrl) ?? origin; // 成功来源用于拼接页面地址
|
|
528
|
+
} catch (fallbackError) { // 备用也失败
|
|
529
|
+
errors.push(fallbackError instanceof Error ? fallbackError.message : String(fallbackError)); // 追加原因
|
|
530
|
+
} // 结束备用异常
|
|
531
|
+
} // 结束备用分支
|
|
532
|
+
} // 结束主来源异常
|
|
533
|
+
} else { // 没有 Key
|
|
534
|
+
errors.push('还没有保存 OpenCode API Key,无法查询用量'); // 给出可行动提示
|
|
535
|
+
} // 结束 Key 判断
|
|
536
|
+
if (usage !== null) await writeUsage({ ...usage, source }, dir).catch(() => undefined); // 成功才覆盖缓存,写失败不影响返回
|
|
537
|
+
const current = usage ?? stored; // 本次或上次
|
|
538
|
+
return { // 汇总结果
|
|
539
|
+
windows: current?.windows ?? {}, // 三窗口额度
|
|
540
|
+
fetchedAt: current?.fetchedAt ?? null, // 额度的时间
|
|
541
|
+
stale: usage === null && current !== null, // 展示的是上次结果
|
|
542
|
+
source: usage === null ? (current?.source ?? null) : source, // 数据来源
|
|
543
|
+
origin, // 用于打开用量页的地址来源
|
|
544
|
+
pageUrl: usagePageUrl(origin), // Console 页面地址
|
|
545
|
+
errors, // 失败原因
|
|
546
|
+
hasKey: apiKey.length > 0, // Key 状态
|
|
547
|
+
}; // 结束汇总
|
|
548
|
+
} // 结束额度检查
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dfy-pet",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "A chibi DeepSeek whale-girl desktop pet with right-click AI relay provider configuration
|
|
3
|
+
"version": "2.9.0",
|
|
4
|
+
"description": "A chibi DeepSeek whale-girl desktop pet with right-click AI relay provider configuration, an OpenCode Go model/pricing/quota monitor, and a remaining-usage panel for the 5-hour, weekly and monthly Go allowance.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek-harness",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"files": [
|
|
36
36
|
"index.js",
|
|
37
37
|
"monitor.js",
|
|
38
|
+
"token-usage.js",
|
|
38
39
|
"client.js",
|
|
39
40
|
"cordis.patch.yml",
|
|
40
41
|
"locale/*.json",
|