dsh-bottom-info-bar 1.16.3 → 1.16.4
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 +16 -5
- package/README.zh-CN.md +16 -5
- package/lib/index.js +20 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,12 +24,23 @@ The bar has two densities — click it to switch. **Full** keeps DSH's native st
|
|
|
24
24
|
|
|
25
25
|
## Compact mode
|
|
26
26
|
|
|
27
|
-
Four real states — light and dark, balance and subscription quota
|
|
27
|
+
Four real states — light and dark, balance and subscription quota.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
**Light · balance**
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
**Dark · balance**
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
**Light · subscription quota**
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
**Dark · subscription quota**
|
|
42
|
+
|
|
43
|
+

|
|
33
44
|
|
|
34
45
|
## Three billing modes
|
|
35
46
|
|
package/README.zh-CN.md
CHANGED
|
@@ -24,12 +24,23 @@ DeepSeek Harness 插件:把输入框下方那行统计栏换成一行信息栏
|
|
|
24
24
|
|
|
25
25
|
## 简洁模式
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
四种真实状态:浅色与深色、余额制与订阅额度制。
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
**浅色 · 余额制**
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
**深色 · 余额制**
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
**浅色 · 订阅额度制**
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
**深色 · 订阅额度制**
|
|
42
|
+
|
|
43
|
+

|
|
33
44
|
|
|
34
45
|
## 三种计费模式
|
|
35
46
|
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Generated by scripts/build.mjs from src/host.js — do not edit directly.
|
|
2
2
|
// Bottom Info Bar(底部信息栏插件)— host half(静态 bundle 形态)
|
|
3
3
|
// 业务:余额真实 API / 峰谷定价 / llm/stream 记账 / 会话聚合 / 显示名识别 / 订阅额度显示
|
|
4
|
-
// RPC:webServer HTTP 路由(GET/POST /_dsh/dsh-bottom-info-bar/<method>,JSON
|
|
4
|
+
// RPC:webServer HTTP 路由(GET/POST /_dsh/dsh-bottom-info-bar/<method>,JSON 进出,宿主认证边界)
|
|
5
5
|
// 依赖:inject ['credentials', 'timer'];可选服务 webServer / sessionController(按能力读取)
|
|
6
6
|
// 记账持久化:追加账本 + 可恢复快照落盘 ~/.dsh/dsh-bottom-info-bar/(可用环境变量
|
|
7
7
|
// DSH_BOTTOM_INFO_BAR_DATA_DIR 覆盖目录),重启/中断后真实累计花费不丢失。
|
|
@@ -17,9 +17,6 @@ import * as hostLocale from './host-locale.js'
|
|
|
17
17
|
const t = hostLocale.createHostTranslator()
|
|
18
18
|
|
|
19
19
|
const DATA_DIR = process.env.DSH_BOTTOM_INFO_BAR_DATA_DIR || join(homedir(), '.dsh', 'dsh-bottom-info-bar')
|
|
20
|
-
// DSH_BOTTOM_INFO_BAR_PROFILE_ROOT 仅供测试隔离(指向临时 profiles 目录),运行期不设置。
|
|
21
|
-
const PROFILE_ROOT = process.env.DSH_BOTTOM_INFO_BAR_PROFILE_ROOT || join(homedir(), '.dsh', 'profiles')
|
|
22
|
-
const BUNDLE_NAME = 'dsh-bottom-info-bar'
|
|
23
20
|
const DATA_FILE = join(DATA_DIR, 'usage-records.json')
|
|
24
21
|
const DATA_BACKUP_FILE = DATA_FILE + '.bak'
|
|
25
22
|
const DATA_TEMP_FILE = DATA_FILE + '.tmp'
|
|
@@ -1630,64 +1627,10 @@ export const __settingsInternals = {
|
|
|
1630
1627
|
DEFAULT_TIME_ZONES: DEFAULT_TIME_ZONES,
|
|
1631
1628
|
}
|
|
1632
1629
|
|
|
1633
|
-
// ----------
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1636
|
-
//
|
|
1637
|
-
// 与 dependencies 都查);停用只改 cordis.patch.yml 的 disabled,重启什么都不改,两者
|
|
1638
|
-
// 都仍能在 package.json 里查到本 bundle。
|
|
1639
|
-
// 安全底线:任何读取失败 / 无法判定的情况一律返回 true(按「还在装」处理,绝不删数据)。
|
|
1640
|
-
function bundleStillReferenced() {
|
|
1641
|
-
try {
|
|
1642
|
-
// 保险①:数据与 profile 必须同属一个 DSH home。数据目录被指到别处(自定义部署)时,
|
|
1643
|
-
// profile 未必在默认位置,两者对不上就判定不了——一律按「还在装」处理。
|
|
1644
|
-
if (dirname(DATA_DIR) !== dirname(PROFILE_ROOT)) return true
|
|
1645
|
-
if (!existsSync(PROFILE_ROOT)) return true
|
|
1646
|
-
let profileDirs = 0
|
|
1647
|
-
let readableManifests = 0
|
|
1648
|
-
const entries = readdirSync(PROFILE_ROOT, { withFileTypes: true })
|
|
1649
|
-
for (const entry of entries) {
|
|
1650
|
-
if (!entry.isDirectory()) continue
|
|
1651
|
-
profileDirs += 1
|
|
1652
|
-
const manifestPath = join(PROFILE_ROOT, entry.name, 'package.json')
|
|
1653
|
-
if (!existsSync(manifestPath)) continue
|
|
1654
|
-
let parsed = null
|
|
1655
|
-
try {
|
|
1656
|
-
parsed = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
1657
|
-
} catch (err) {
|
|
1658
|
-
continue
|
|
1659
|
-
}
|
|
1660
|
-
if (!parsed || typeof parsed !== 'object') continue
|
|
1661
|
-
readableManifests += 1
|
|
1662
|
-
const profile = parsed.dsh && parsed.dsh.profile
|
|
1663
|
-
const bundles = profile && Array.isArray(profile.bundles) ? profile.bundles : []
|
|
1664
|
-
if (bundles.indexOf(BUNDLE_NAME) !== -1) return true
|
|
1665
|
-
for (const key of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
1666
|
-
const deps = parsed[key]
|
|
1667
|
-
if (deps && typeof deps === 'object' && Object.hasOwn(deps, BUNDLE_NAME)) return true
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
// 保险②:一个 profile 目录都没有 / 一份 manifest 都没读出来 = 根本没判定成功,
|
|
1671
|
-
// 按「还在装」处理,绝不删数据。
|
|
1672
|
-
if (profileDirs === 0 || readableManifests === 0) return true
|
|
1673
|
-
} catch (err) {
|
|
1674
|
-
return true
|
|
1675
|
-
}
|
|
1676
|
-
return false
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
// 真卸载:连目录一起清掉(账本 + 设置 + 备份),不留残留。
|
|
1680
|
-
// 只在 bundleStillReferenced() 为 false 时调用;调用点必须已经确认「不再被任何 profile 引用」。
|
|
1681
|
-
function clearPluginData() {
|
|
1682
|
-
try {
|
|
1683
|
-
if (!existsSync(DATA_DIR)) return true
|
|
1684
|
-
rmSync(DATA_DIR, { recursive: true, force: true })
|
|
1685
|
-
return true
|
|
1686
|
-
} catch (err) {
|
|
1687
|
-
console.warn('[dsh-bottom-info-bar] 卸载清理数据目录失败:' + String((err && err.message) || err))
|
|
1688
|
-
return false
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1630
|
+
// ---------- 卸载数据边界 ----------
|
|
1631
|
+
// 账本和设置属于用户,不属于可替换的插件代码。插件更新、停用、移除或宿主重启时都只冲刷
|
|
1632
|
+
// 尚未落盘的记录;真正清除只能由设置页的 clearUsageRecords 明确发起。这样桌面端在更新
|
|
1633
|
+
// bundle 时无需以卸载为代价,也不会丢失历史账单。
|
|
1691
1634
|
|
|
1692
1635
|
export default {
|
|
1693
1636
|
inject: ['credentials', 'timer'],
|
|
@@ -4813,8 +4756,8 @@ export default {
|
|
|
4813
4756
|
return settingsPayload(persistError);
|
|
4814
4757
|
},
|
|
4815
4758
|
};
|
|
4816
|
-
//
|
|
4817
|
-
//
|
|
4759
|
+
// 会写入或触发网络请求的方法必须使用 POST;浏览器和桌面端的认证/防重绑定由宿主
|
|
4760
|
+
// connection 服务统一完成,插件不能根据 Origin/Host 自行判断。
|
|
4818
4761
|
const MUTATING = { getBalanceSnapshot: true, setDisplayMode: true, setInfoDensity: true, getSubscriptionSnapshot: true, getBillingStatus: true, setFieldConfig: true, resetFieldConfig: true, resetFieldColors: true, clearUsageRecords: true };
|
|
4819
4762
|
function invalidArgument(message) {
|
|
4820
4763
|
const err = new Error(message);
|
|
@@ -4822,21 +4765,6 @@ export default {
|
|
|
4822
4765
|
return err;
|
|
4823
4766
|
}
|
|
4824
4767
|
|
|
4825
|
-
function sameOrigin(req) {
|
|
4826
|
-
const fetchSite = req.headers['sec-fetch-site'];
|
|
4827
|
-
if (fetchSite === 'cross-site') return false;
|
|
4828
|
-
const origin = req.headers.origin;
|
|
4829
|
-
if (origin === undefined) return fetchSite === 'same-origin' || fetchSite === 'same-site';
|
|
4830
|
-
const host = req.headers.host;
|
|
4831
|
-
if (host === undefined) return false;
|
|
4832
|
-
try {
|
|
4833
|
-
const parsed = new URL(origin);
|
|
4834
|
-
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host === host;
|
|
4835
|
-
} catch {
|
|
4836
|
-
return false;
|
|
4837
|
-
}
|
|
4838
|
-
}
|
|
4839
|
-
|
|
4840
4768
|
function readBody(req, maxBytes) {
|
|
4841
4769
|
return new Promise(function (resolve, reject) {
|
|
4842
4770
|
const chunks = [];
|
|
@@ -4867,7 +4795,11 @@ export default {
|
|
|
4867
4795
|
res.end(body);
|
|
4868
4796
|
}
|
|
4869
4797
|
|
|
4870
|
-
|
|
4798
|
+
// Desktop validates dsh-app://app before forwarding, then deliberately removes renderer
|
|
4799
|
+
// Origin/Host headers and adds its private Host cookie. requestRejection() is the shared
|
|
4800
|
+
// boundary for both Web and Desktop; reproducing the browser-header check here rejects
|
|
4801
|
+
// legitimate desktop requests.
|
|
4802
|
+
ctx.inject(['connection', 'webServer'], function (webCtx) {
|
|
4871
4803
|
webCtx.effect(function () {
|
|
4872
4804
|
const dispose = webCtx.webServer.register({
|
|
4873
4805
|
kind: 'prefix',
|
|
@@ -4875,6 +4807,11 @@ export default {
|
|
|
4875
4807
|
handler: async function (req, res) {
|
|
4876
4808
|
let method = '';
|
|
4877
4809
|
try {
|
|
4810
|
+
const rejection = webCtx.connection.requestRejection(req);
|
|
4811
|
+
if (rejection !== undefined) {
|
|
4812
|
+
respond(res, rejection, { error: rejection === 401 ? 'authentication required' : 'request rejected' });
|
|
4813
|
+
return;
|
|
4814
|
+
}
|
|
4878
4815
|
const url = new URL(req.url || '/', 'http://localhost');
|
|
4879
4816
|
const path = url.pathname;
|
|
4880
4817
|
if (!path.startsWith(ROUTE_PREFIX + '/')) {
|
|
@@ -4887,8 +4824,8 @@ export default {
|
|
|
4887
4824
|
respond(res, 404, { error: 'unknown method: ' + method });
|
|
4888
4825
|
return;
|
|
4889
4826
|
}
|
|
4890
|
-
if (Object.hasOwn(MUTATING, method) &&
|
|
4891
|
-
respond(res,
|
|
4827
|
+
if (Object.hasOwn(MUTATING, method) && req.method !== 'POST') {
|
|
4828
|
+
respond(res, 405, { error: 'mutating methods require POST' });
|
|
4892
4829
|
return;
|
|
4893
4830
|
}
|
|
4894
4831
|
let args = {};
|
|
@@ -4932,14 +4869,8 @@ export default {
|
|
|
4932
4869
|
ctx.interval(refreshActiveSubscriptions, 60000);
|
|
4933
4870
|
ctx.interval(refreshActiveBilling, 60000); // v1.7
|
|
4934
4871
|
|
|
4935
|
-
//
|
|
4872
|
+
// 生命周期结束时只冲刷未落盘的账本;数据的删除必须经过设置页的显式操作。
|
|
4936
4873
|
return function () {
|
|
4937
|
-
// 真卸载(profile 的 package.json 里已无本 bundle):连数据目录一起清空,不留残留。
|
|
4938
|
-
// 只是停用那一排、或 DSH 正常重启时,走下面的冲刷分支,一个字都不删。
|
|
4939
|
-
if (!bundleStillReferenced()) {
|
|
4940
|
-
clearPluginData();
|
|
4941
|
-
return;
|
|
4942
|
-
}
|
|
4943
4874
|
if (dirty || summariesDirty) flushSave();
|
|
4944
4875
|
};
|
|
4945
4876
|
},
|