dsh-bottom-info-bar 1.16.2 → 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 +25 -13
- package/README.zh-CN.md +25 -13
- package/docs/INSTALL.md +1 -1
- package/lib/client.js +20 -2
- package/lib/index.js +20 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,9 +20,27 @@ A DeepSeek Harness plugin that replaces the stats row under the composer with on
|
|
|
20
20
|
| Spend | this session (including subagents), today, last 30 days, all time |
|
|
21
21
|
| Extras | main time, world time, custom text |
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
The bar has two densities — click it to switch. **Full** keeps DSH's native stats row above the plugin row; **compact** folds everything into one line. Both follow DSH's light or dark theme, and the context ring always stays on the same line as the text before it.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
## Compact mode
|
|
26
|
+
|
|
27
|
+
Four real states — light and dark, balance and subscription quota.
|
|
28
|
+
|
|
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
|
+

|
|
26
44
|
|
|
27
45
|
## Three billing modes
|
|
28
46
|
|
|
@@ -32,8 +50,6 @@ The bar follows the active session and picks the mode from the provider. The thr
|
|
|
32
50
|
|
|
33
51
|
Shows the real balance from the provider's own API. It refetches when the bar opens, the page refreshes or the provider changes, then polls every 60 seconds; a failed refresh keeps the last known figure on screen. Below 20 (in the account's currency) the amount and a **Low** label turn red.
|
|
34
52
|
|
|
35
|
-

|
|
36
|
-
|
|
37
53
|
### Subscription quota
|
|
38
54
|
|
|
39
55
|
Shows the remaining quota per window (5-hour / weekly / monthly) and a countdown to the next reset — both always come from the same window, so they can never disagree. Windows show **remaining** percent by default and can be switched to **used** in settings; the low-quota warning always follows the remaining ≤ 20% rule. Compact mode prefers the shortest available window (5-hour → weekly → monthly).
|
|
@@ -42,12 +58,6 @@ Shows the remaining quota per window (5-hour / weekly / monthly) and a countdown
|
|
|
42
58
|
|
|
43
59
|
Shows this month's real spend from the provider's official billing API, for example `Together | This month $12.34` or `AWS Bedrock | This month $45.60 · Budget 46%`. Cloudflare also shows the daily free-quota remainder with a UTC-midnight reset countdown when the API actually reports a free allowance.
|
|
44
60
|
|
|
45
|
-
## Dark mode
|
|
46
|
-
|
|
47
|
-
Colors adapt to DSH's theme — there is no separate setting.
|
|
48
|
-
|
|
49
|
-

|
|
50
|
-
|
|
51
61
|
## Install
|
|
52
62
|
|
|
53
63
|
Requires DeepSeek Harness with the web interface (`dsh web`) and pnpm.
|
|
@@ -72,6 +82,8 @@ cd dsh-bottom-info-bar
|
|
|
72
82
|
./install.sh
|
|
73
83
|
```
|
|
74
84
|
|
|
85
|
+
If you installed from a checkout before the package moved to the repository root, your profile points at `<repo>/plugin`. That path still resolves — the repository keeps `plugin/` as symlinks to the package root — so nothing needs reinstalling. If it does not resolve (a ZIP download, for instance, delivers those symlinks as plain files), remove the plugin and install again with one of the commands above.
|
|
86
|
+
|
|
75
87
|
Then **restart `dsh web`** — plugins are composed when the host starts, so a page refresh is not enough. The plugin shows up in the Plugins list, enabled:
|
|
76
88
|
|
|
77
89
|

|
|
@@ -103,7 +115,7 @@ Everything lives on the plugin page — **Plugins → bottom-info-bar**. Changes
|
|
|
103
115
|
|
|
104
116
|
**Custom text** — up to 64 characters, shown in the bar.
|
|
105
117
|
|
|
106
|
-

|
|
107
119
|
|
|
108
120
|
**Billing data** — export the ledger as CSV or JSON, or clear it after confirmation. Settings and sign-in information stay untouched.
|
|
109
121
|
|
|
@@ -117,7 +129,7 @@ The bar detects the provider from DSH's current model — no configuration. Set
|
|
|
117
129
|
|---|---|---|
|
|
118
130
|
| deepseek / deepseek-official | DeepSeek | `DEEPSEEK_API_KEY` |
|
|
119
131
|
| openai | OpenAI | `OPENAI_API_KEY` — estimated from your spending rate; there is no public balance API |
|
|
120
|
-
| moonshotai / moonshotai-cn / kimi-coding | Kimi | `MOONSHOT_API_KEY`
|
|
132
|
+
| moonshotai / moonshotai-cn / kimi-coding | Kimi | `MOONSHOT_API_KEY` |
|
|
121
133
|
| openrouter | OpenRouter | `OPENROUTER_API_KEY` |
|
|
122
134
|
| stepfun | StepFun | `STEPFUN_API_KEY` |
|
|
123
135
|
| xiaomi | Xiaomi MiMo | `XIAOMI_API_KEY` |
|
|
@@ -150,7 +162,7 @@ Every model response is recorded (usage × unit price) and aggregated four ways:
|
|
|
150
162
|
|
|
151
163
|
## Updating
|
|
152
164
|
|
|
153
|
-
|
|
165
|
+
The plugin checks npm for a newer version at startup and re-checks at most every 15 minutes. When one exists, a red **Update available** label appears in the bar: click it to copy the update command, run it in a terminal, then restart `dsh web`.
|
|
154
166
|
|
|
155
167
|
| Installed via | Command |
|
|
156
168
|
|---|---|
|
package/README.zh-CN.md
CHANGED
|
@@ -20,9 +20,27 @@ DeepSeek Harness 插件:把输入框下方那行统计栏换成一行信息栏
|
|
|
20
20
|
| 花费 | 本会话(含子代理)、今天、近 30 天、累计 |
|
|
21
21
|
| 其它 | 主时间、世界时间、自定义文字 |
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
信息栏有两种密度,点击即可切换:**完整**保留 DSH 原生统计行,插件信息另起一行;**简洁**把全部内容收成一行。两种密度都跟随 DSH 的深色/浅色主题,上下文圆环始终与前面的文字同一行,不会单独折行。
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
## 简洁模式
|
|
26
|
+
|
|
27
|
+
四种真实状态:浅色与深色、余额制与订阅额度制。
|
|
28
|
+
|
|
29
|
+
**浅色 · 余额制**
|
|
30
|
+
|
|
31
|
+

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

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

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

|
|
26
44
|
|
|
27
45
|
## 三种计费模式
|
|
28
46
|
|
|
@@ -32,8 +50,6 @@ DeepSeek Harness 插件:把输入框下方那行统计栏换成一行信息栏
|
|
|
32
50
|
|
|
33
51
|
显示服务商官方接口返回的真实余额。信息栏打开、页面刷新或切换服务商时立即重查,之后每 60 秒轮询一次;刷新失败时保留上一次的数值。余额低于 20(按账户币种)时,金额与**低**标记转红。
|
|
34
52
|
|
|
35
|
-

|
|
36
|
-
|
|
37
53
|
### 订阅额度制
|
|
38
54
|
|
|
39
55
|
显示各窗口剩余额度(5 小时 / 周 / 月)与距下次重置的倒计时——两者始终取自同一个窗口,不会互相矛盾。窗口默认按**剩余**显示百分比,可在设置里切换为**已用**;低额度告警始终按剩余 ≤ 20% 判定。简洁模式优先显示时长最短的可用窗口(5 小时 → 周 → 月)。
|
|
@@ -42,12 +58,6 @@ DeepSeek Harness 插件:把输入框下方那行统计栏换成一行信息栏
|
|
|
42
58
|
|
|
43
59
|
显示官方账单接口返回的本月真实花费,例如 `Together | 本月 $12.34`、`AWS Bedrock | 本月 $45.60 · 预算 46%`。Cloudflare 还会在接口确实返回免费额度时,显示每日免费额度剩余与 UTC 零点重置倒计时。
|
|
44
60
|
|
|
45
|
-
## 深色模式
|
|
46
|
-
|
|
47
|
-
配色跟随 DSH 主题,无需单独设置。
|
|
48
|
-
|
|
49
|
-

|
|
50
|
-
|
|
51
61
|
## 安装
|
|
52
62
|
|
|
53
63
|
需要带 Web 界面的 DeepSeek Harness(`dsh web`)与 pnpm。
|
|
@@ -72,6 +82,8 @@ cd dsh-bottom-info-bar
|
|
|
72
82
|
./install.sh
|
|
73
83
|
```
|
|
74
84
|
|
|
85
|
+
如果你在「包移到仓库根」之前用本地代码装过,profile 里的路径末尾是 `/plugin`。这条路径依然有效——仓库里保留了 `plugin/` 软链指向仓库根,所以不必重装。万一它不生效(例如用 ZIP 下载仓库时,软链会变成普通文件),先卸载,再用上面的任一方式装一次。
|
|
86
|
+
|
|
75
87
|
然后**重启 `dsh web`** —— 插件在宿主启动时组合,刷新页面不够。重启后插件出现在插件列表并处于启用状态:
|
|
76
88
|
|
|
77
89
|

|
|
@@ -103,7 +115,7 @@ cd dsh-bottom-info-bar
|
|
|
103
115
|
|
|
104
116
|
**自定义文字** —— 最多 64 个字符,显示在信息栏中。
|
|
105
117
|
|
|
106
|
-

|
|
107
119
|
|
|
108
120
|
**账单数据** —— 导出 CSV / JSON,或确认后清除记录。设置与登录信息不受影响。
|
|
109
121
|
|
|
@@ -117,7 +129,7 @@ cd dsh-bottom-info-bar
|
|
|
117
129
|
|---|---|---|
|
|
118
130
|
| deepseek / deepseek-official | DeepSeek | `DEEPSEEK_API_KEY` |
|
|
119
131
|
| openai | OpenAI | `OPENAI_API_KEY` —— 按消耗速度估算;官方没有公开余额接口 |
|
|
120
|
-
| moonshotai / moonshotai-cn / kimi-coding | Kimi | `MOONSHOT_API_KEY
|
|
132
|
+
| moonshotai / moonshotai-cn / kimi-coding | Kimi | `MOONSHOT_API_KEY` |
|
|
121
133
|
| openrouter | OpenRouter | `OPENROUTER_API_KEY` |
|
|
122
134
|
| stepfun | StepFun | `STEPFUN_API_KEY` |
|
|
123
135
|
| xiaomi | 小米 MiMo | `XIAOMI_API_KEY` |
|
|
@@ -150,7 +162,7 @@ cd dsh-bottom-info-bar
|
|
|
150
162
|
|
|
151
163
|
## 更新
|
|
152
164
|
|
|
153
|
-
|
|
165
|
+
插件在 DSH 启动时检查一次 npm 上的新版本,之后最多每 15 分钟复查一次。有新版时,信息栏出现红色**新版本提醒**标签:点击即可复制更新命令,到终端执行后重启 `dsh web`。
|
|
154
166
|
|
|
155
167
|
| 安装方式 | 命令 |
|
|
156
168
|
|---|---|
|
package/docs/INSTALL.md
CHANGED
|
@@ -47,7 +47,7 @@ node scripts/build.mjs
|
|
|
47
47
|
dsh plugin --profile web add /path/to/dsh-bottom-info-bar
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
> 1.15.0 及更早的版本里,插件包放在仓库的 `plugin/`
|
|
50
|
+
> 1.15.0 及更早的版本里,插件包放在仓库的 `plugin/` 子目录,安装路径末尾写的是 `/plugin`。仓库里保留了 `plugin/` 软链指向仓库根,所以旧路径依然可用、不必重装;万一不生效(例如用 ZIP 下载仓库时软链会变成普通文件),先 `dsh plugin --profile web remove dsh-bottom-info-bar`,再用上面的命令装一次。
|
|
51
51
|
|
|
52
52
|
### 安装原理
|
|
53
53
|
|
package/lib/client.js
CHANGED
|
@@ -586,6 +586,18 @@ function assembleInfoBarRow(groups, trailingErrorGroups, createElement) {
|
|
|
586
586
|
return nodes;
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
// 上下文圆环的落位(2026-09-24 用户报「圆环被单独挤到下一行并居中」):
|
|
590
|
+
// 圆环原本作为主行的独立 flex 子项追加在末尾 —— 行满换行时它会独占一行,居中后非常难看。
|
|
591
|
+
// 改为与「最后一个内容节点」一起包进同一个 nowrap 尾巴(.bi-tail):要换行两者一起走,
|
|
592
|
+
// 圆环永远不会孤零零占一行;没有内容节点时(只显示圆环)保持原样。
|
|
593
|
+
function attachContextMeter(nodes, contextNode, createElement) {
|
|
594
|
+
if (!contextNode) return nodes.slice();
|
|
595
|
+
if (nodes.length === 0) return [contextNode];
|
|
596
|
+
const out = nodes.slice();
|
|
597
|
+
out[out.length - 1] = createElement('span', { key: 'tail', className: 'bi-tail' }, out[out.length - 1], contextNode);
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
|
|
589
601
|
// D6 用户拍板:全部字段隐藏 = 底栏彻底移除。此判定为纯函数供单测:
|
|
590
602
|
// 注册表内没有任何可见字段,或渲染结果(原生行/主行/错误组)全空 → 信息栏整体不渲染,
|
|
591
603
|
// 不留空行、占位高度或悬空分隔符;density 点击因无 DOM 而天然无副作用。
|
|
@@ -630,6 +642,8 @@ function installStyles() {
|
|
|
630
642
|
/* 整条信息栏始终作为一个居中的内容组;不会超过上方对话框的内容宽度。 */
|
|
631
643
|
.bi-root > .bi-row2 { flex: none; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; width: 100%; }
|
|
632
644
|
.bi-native-row > span, .bi-row2 > span { white-space: nowrap; }
|
|
645
|
+
/* 圆环与最后一个内容节点同组:换行时一起走,绝不单独占一行(见 attachContextMeter) */
|
|
646
|
+
.bi-tail { display: inline-flex; align-items: center; flex: 0 0 auto; max-width: 100%; white-space: nowrap; }
|
|
633
647
|
/* 只有模型组可在窄宽度折行;服务商与模型详情仍成组,不会让圆点落在行尾。 */
|
|
634
648
|
.bi-row2 > .bi-model-group { white-space: normal; }
|
|
635
649
|
/* 组间 6px、模型内部圆点 4px:保留分组层级,同时避免 12px 信息栏被过大留白拉散。 */
|
|
@@ -681,7 +695,11 @@ function installStyles() {
|
|
|
681
695
|
.bi-ctx-track { fill: none; stroke: var(--dsw-alias-border-l3, rgba(128, 128, 128, 0.35)); stroke-width: 2px; }
|
|
682
696
|
.bi-ctx-fill { fill: none; stroke: currentColor; stroke-width: 2px; stroke-linecap: round; }
|
|
683
697
|
/* 构成明细面板:与原生 ContextMeter 面板同构(264px 宽 / 12px 内边距 / 12px 圆角 / 4px 构成条)。 */
|
|
684
|
-
|
|
698
|
+
/* 面板底色必须不透明(2026-09-24 用户报「面板透明、文字和底下的信息栏叠在一起看不清」):
|
|
699
|
+
宿主 --dsw-specific-menu 是带 alpha 的色(浅 #f8f9fa94 / 深 #30313680),且宿主菜单另有毛玻璃层,
|
|
700
|
+
本面板悬在信息栏文字之上,直接用它必然透字。改用不透明的层级底色(浅 #fff / 深 #353638,随主题走),
|
|
701
|
+
并保留宿主阴影;不引用任何可能带 alpha 的 token。 */
|
|
702
|
+
.bi-ctx-panel { z-index: 1100; box-sizing: border-box; width: min(264px, calc(100vw - 24px)); padding: 12px; border: 0.5px solid var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.28)); border-radius: 12px; background: var(--dsw-alias-bg-layer-3, #fff); color: var(--dsw-alias-label-secondary, #5a6169); box-shadow: var(--dsw-elevation-prominent, 0 8px 24px rgba(0, 0, 0, 0.18)); font-size: 12px; line-height: 20px; cursor: default; position: fixed; }
|
|
685
703
|
.bi-ctx-panel-header { display: flex; align-items: center; gap: 6px; }
|
|
686
704
|
.bi-ctx-panel-headline { color: var(--dsw-alias-label-tertiary, #8a9099); }
|
|
687
705
|
.bi-ctx-panel-headline:empty { display: none; }
|
|
@@ -3490,7 +3508,7 @@ module.exports = {
|
|
|
3490
3508
|
context: contextInfo,
|
|
3491
3509
|
breakdown: breakdownProj,
|
|
3492
3510
|
});
|
|
3493
|
-
const row2 = React.createElement('div', { id: 'dsh-bottom-info-bar-primary', className: 'bi-row2' }, ...nodes, contextNode);
|
|
3511
|
+
const row2 = React.createElement('div', { id: 'dsh-bottom-info-bar-primary', className: 'bi-row2' }, ...attachContextMeter(nodes, contextNode, React.createElement));
|
|
3494
3512
|
|
|
3495
3513
|
let row1 = null;
|
|
3496
3514
|
if (statsProj) {
|
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
|
},
|