@whyshu/ba 0.1.0 → 0.1.1
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 +9 -0
- package/dist/client.js +24 -13
- package/package.json +7 -2
- package/src/compat.mjs +305 -0
- package/src/index.js +37 -6
package/README.md
CHANGED
|
@@ -31,6 +31,15 @@ install_bundle target = /Users/Chester/Whyshu/whyshu-ba
|
|
|
31
31
|
|
|
32
32
|
安装后左侧栏出现图标;点击进入面板。首次打开若未配置 Key,会显示配置引导。
|
|
33
33
|
|
|
34
|
+
## dsh 兼容范围(双层门禁)
|
|
35
|
+
|
|
36
|
+
支持 `dsh >=0.2.0-0 <0.3.0-0`(`package.json#engines.dsh`,含 0.2.x 的 rc 预发布)。
|
|
37
|
+
|
|
38
|
+
1. **宿主侧**:dsh 在导入插件代码前校验 `engines.dsh`;不匹配的 bundle 会被 skip(宿主与客户端 bundle 都不会挂载),只在 stderr 打一行原因,不阻断 `dsh web` 启动。
|
|
39
|
+
2. **插件侧(兜底)**:`apply()` 顶部再自行评估版本;未适配时只打一行
|
|
40
|
+
`[@whyshu/ba] 插件已自我禁用(dsh x.y.z 未适配)` 后 `return`,**不注册任何宿主 API、不抛**。
|
|
41
|
+
排障可用环境变量 `WHYSHU_BA_DSH_VERSION` 显式指定宿主版本。
|
|
42
|
+
|
|
34
43
|
## 配置 API Key
|
|
35
44
|
|
|
36
45
|
1. 币安 → 账户 → **API 管理** → 新建 API,权限只勾 **「启用读取」**(务必禁用提现、交易)。
|
package/dist/client.js
CHANGED
|
@@ -513,33 +513,44 @@ window.__ModuleLoader__.load({
|
|
|
513
513
|
const name = 'whyshu-ba-client'
|
|
514
514
|
const inject = ['slots', 'locale']
|
|
515
515
|
|
|
516
|
-
|
|
516
|
+
/** 安全执行一次注册:任何异常只记日志,绝不让客户端 apply 抛出(抛了会空白槽位/页面)。 */
|
|
517
|
+
function safe(label, fn) {
|
|
517
518
|
try {
|
|
518
|
-
|
|
519
|
+
return fn()
|
|
519
520
|
} catch (e) {
|
|
520
|
-
|
|
521
|
+
try {
|
|
522
|
+
console.warn('[whyshu-ba] ' + label + ' 失败(已忽略,不影响 dsh 页面):', (e && e.message) || e)
|
|
523
|
+
} catch { /* 日志本身失败也不能抛 */ }
|
|
524
|
+
return undefined
|
|
521
525
|
}
|
|
522
|
-
|
|
523
|
-
injectCss()
|
|
524
|
-
// 自检信标:宿主 /health 会回报 clientPingAt ⇒ 能区分"bundle 没加载"与"加载了但没显示"
|
|
525
|
-
try { fetch(API + '/ping?from=client').catch(() => {}) } catch { /* noop */ }
|
|
526
|
+
}
|
|
526
527
|
|
|
527
|
-
|
|
528
|
+
function apply(ctx) {
|
|
529
|
+
// 整体 try/catch:客户端 apply 抛异常会让整个页面/槽位崩溃。
|
|
528
530
|
try {
|
|
529
|
-
|
|
531
|
+
safe('语言字典', () => ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'whyshu-ba: dictionaries'))
|
|
532
|
+
const t = safe('locale.bind', () => ctx.locale.bind(NS)) || ((key) => key)
|
|
533
|
+
safe('样式注入', injectCss)
|
|
534
|
+
// 自检信标:宿主 /health 会回报 clientPingAt ⇒ 能区分"bundle 没加载"与"加载了但没显示"
|
|
535
|
+
safe('ping', () => { fetch(API + '/ping?from=client').catch(() => {}) })
|
|
536
|
+
|
|
537
|
+
// ⛔ 永不抛出:注册失败只影响这个面板,不能让 web boot 报 "entry did not activate"
|
|
538
|
+
safe('main 面板', () => ctx.slots.inject('main', () => ctx.slots.register({
|
|
530
539
|
name: 'main',
|
|
531
540
|
key: PANEL_ID,
|
|
532
541
|
locale: NS,
|
|
533
|
-
}, BinancePanelSafe))
|
|
534
|
-
ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
|
542
|
+
}, BinancePanelSafe)))
|
|
543
|
+
safe('侧栏入口', () => ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
|
535
544
|
name: 'sidebar.panellist',
|
|
536
545
|
id: PANEL_ID,
|
|
537
546
|
order: 20,
|
|
538
547
|
locale: NS,
|
|
539
548
|
label: () => t('panel'),
|
|
540
|
-
}, BinanceIcon))
|
|
549
|
+
}, BinanceIcon)))
|
|
541
550
|
} catch (e) {
|
|
542
|
-
|
|
551
|
+
try {
|
|
552
|
+
console.warn('[whyshu-ba] 客户端 apply 异常已忽略(不影响 dsh 页面):', (e && e.message) || e)
|
|
553
|
+
} catch { /* noop */ }
|
|
543
554
|
}
|
|
544
555
|
}
|
|
545
556
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whyshu/ba",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "DSH 左侧栏币安面板:主视图展示现货资产、合约持仓与行情看板(只读,API Key 仅宿主侧)",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"engines": {
|
|
7
|
-
"node": ">=24.2.0"
|
|
8
|
+
"node": ">=24.2.0",
|
|
9
|
+
"dsh": ">=0.2.0-0 <0.3.0-0"
|
|
8
10
|
},
|
|
9
11
|
"keywords": [
|
|
10
12
|
"dsh",
|
|
@@ -30,6 +32,9 @@
|
|
|
30
32
|
"cordis.patch.yml",
|
|
31
33
|
"README.md"
|
|
32
34
|
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test test/*.test.mjs"
|
|
37
|
+
},
|
|
33
38
|
"publishConfig": {
|
|
34
39
|
"access": "public"
|
|
35
40
|
},
|
package/src/compat.mjs
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh 宿主兼容闸(@whyshu/ba)
|
|
3
|
+
*
|
|
4
|
+
* 目标:**dsh 升级到未适配版本时,插件自己降级/关闭,但绝不能让 dsh web 启动失败或卡住。**
|
|
5
|
+
*
|
|
6
|
+
* 双层防线(本文件是第二层,第一层在 package.json#engines.dsh):
|
|
7
|
+
* ① **宿主侧(首选)**:`package.json#engines.dsh` 声明支持范围。dsh(0.2.x 起)在**导入插件代码之前**
|
|
8
|
+
* 用 `evaluatePluginCompatibility()` 检查 manifest:不匹配的 bundle 被 skip、不匹配的 entry 被
|
|
9
|
+
* disable,只在 stderr 打一行原因,**不阻断 boot**。
|
|
10
|
+
* ⇒ 宿主升级到未适配版本时,插件(含其客户端 bundle)根本不会被挂载。
|
|
11
|
+
* ② **插件侧(本文件,兜底)**:宿主不认 manifest 门禁(更老版本 / 手工 patch 挂载 / 已授豁免)时,
|
|
12
|
+
* `apply()` 顶部自行评估版本:不支持 ⇒ 只打一条明确日志后 return,**不注册任何宿主 API**。
|
|
13
|
+
*
|
|
14
|
+
* 纯函数边界:本文件**不 import 任何宿主模块**(`@deepseek-ai/*`、`cordis`、`dsh-*`);
|
|
15
|
+
* 只用 node 内置模块(fs/module/path/url)做只读探测,且所有解析失败都返回 null、绝不抛。
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ 范围语义与 dsh 自己的门禁保持一致:dsh 用
|
|
18
|
+
* `semver.satisfies(runtimeVersion, range, { includePrerelease: true })`。dsh 日常发 rc 预发布,
|
|
19
|
+
* 所以下界写 `0.2.0-0` 才能把 `0.2.0-rc.2` 算进来;上界 `0.3.0-0` 把 0.3.0 的所有预发布也挡在外面。
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
22
|
+
import { createRequire } from 'node:module'
|
|
23
|
+
import { join, resolve as resolvePath } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
/** 支持范围(唯一真源;package.json#engines.dsh 必须与它逐字一致,有用例守着)。 */
|
|
27
|
+
export const SUPPORTED_DSH_RANGE = '>=0.2.0-0 <0.3.0-0'
|
|
28
|
+
/** 下界(含)。`-0` 是"该版本线最早的预发布"哨兵,不是笔误。 */
|
|
29
|
+
export const SUPPORTED_DSH_MIN = '0.2.0-0'
|
|
30
|
+
/** 上界(不含)。`-0` 让 0.3.0-rc.x 也算"高于上界" ⇒ 未适配。 */
|
|
31
|
+
export const SUPPORTED_DSH_MAX = '0.3.0-0'
|
|
32
|
+
|
|
33
|
+
/** 可显式覆盖版本探测的环境变量(排障/离线验证/宿主未暴露版本时用;顺序即优先级)。 */
|
|
34
|
+
export const DSH_VERSION_ENV_KEYS = ['WHYSHU_BA_DSH_VERSION', 'DSH_VERSION']
|
|
35
|
+
|
|
36
|
+
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
|
|
37
|
+
const NUMERIC_RE = /^\d+$/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 解析 dsh 版本号(semver;允许 `v` 前缀与 `+build` 元数据)。
|
|
41
|
+
* @param {unknown} value
|
|
42
|
+
* @returns {{raw:string, version:string, major:number, minor:number, patch:number, prerelease:string[]}|null}
|
|
43
|
+
* 解析不了返回 null(**绝不抛** —— 未知版本必须走"降级"而不是炸 boot)。
|
|
44
|
+
*/
|
|
45
|
+
export function parseDshVersion(value) {
|
|
46
|
+
if (typeof value !== 'string') return null
|
|
47
|
+
const raw = value.trim()
|
|
48
|
+
if (!raw) return null
|
|
49
|
+
const m = SEMVER_RE.exec(raw)
|
|
50
|
+
if (!m) return null
|
|
51
|
+
const major = Number(m[1])
|
|
52
|
+
const minor = Number(m[2])
|
|
53
|
+
const patch = Number(m[3])
|
|
54
|
+
if (![major, minor, patch].every(Number.isSafeInteger)) return null
|
|
55
|
+
const prerelease = m[4] ? m[4].split('.') : []
|
|
56
|
+
return {
|
|
57
|
+
raw,
|
|
58
|
+
version: `${major}.${minor}.${patch}${m[4] ? `-${m[4]}` : ''}`,
|
|
59
|
+
major,
|
|
60
|
+
minor,
|
|
61
|
+
patch,
|
|
62
|
+
prerelease,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** semver 预发布段比较(数字段 < 非数字段;数字按数值比;前缀短的小)。 */
|
|
67
|
+
function comparePrerelease(a, b) {
|
|
68
|
+
if (a.length === 0 && b.length === 0) return 0
|
|
69
|
+
if (a.length === 0) return 1 // 正式版 > 预发布
|
|
70
|
+
if (b.length === 0) return -1
|
|
71
|
+
const n = Math.max(a.length, b.length)
|
|
72
|
+
for (let i = 0; i < n; i += 1) {
|
|
73
|
+
if (i >= a.length) return -1
|
|
74
|
+
if (i >= b.length) return 1
|
|
75
|
+
const x = a[i]
|
|
76
|
+
const y = b[i]
|
|
77
|
+
const xn = NUMERIC_RE.test(x)
|
|
78
|
+
const yn = NUMERIC_RE.test(y)
|
|
79
|
+
if (xn && yn) {
|
|
80
|
+
const d = Number(x) - Number(y)
|
|
81
|
+
if (d !== 0) return d < 0 ? -1 : 1
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if (xn) return -1
|
|
85
|
+
if (yn) return 1
|
|
86
|
+
if (x !== y) return x < y ? -1 : 1
|
|
87
|
+
}
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 两个已解析版本的 semver 优先级比较(build 元数据不参与)。 */
|
|
92
|
+
function compareParsed(a, b) {
|
|
93
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
94
|
+
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1
|
|
95
|
+
}
|
|
96
|
+
return comparePrerelease(a.prerelease, b.prerelease)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* semver 优先级比较(导出给用例/排障用)。
|
|
101
|
+
* @returns {number|null} -1 / 0 / 1;任一侧无法解析返回 null(不抛)
|
|
102
|
+
*/
|
|
103
|
+
export function compareDshVersions(a, b) {
|
|
104
|
+
const x = parseDshVersion(a)
|
|
105
|
+
const y = parseDshVersion(b)
|
|
106
|
+
if (!x || !y) return null
|
|
107
|
+
return compareParsed(x, y)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const MIN_BOUND = parseDshVersion(SUPPORTED_DSH_MIN)
|
|
111
|
+
const MAX_BOUND = parseDshVersion(SUPPORTED_DSH_MAX)
|
|
112
|
+
|
|
113
|
+
/** 版本是否落在支持范围内(等价于 `evaluateCompatibility(version).supported`)。 */
|
|
114
|
+
export function isVersionSupported(version) {
|
|
115
|
+
const parsed = parseDshVersion(typeof version === 'string' ? version : '')
|
|
116
|
+
if (!parsed) return false
|
|
117
|
+
return compareParsed(parsed, MIN_BOUND) >= 0 && compareParsed(parsed, MAX_BOUND) < 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 日志里的版本串截断(防御超长 env 值刷屏)。 */
|
|
121
|
+
const show = (v) => {
|
|
122
|
+
const s = String(v == null ? '' : v).trim()
|
|
123
|
+
return s.length > 40 ? `${s.slice(0, 39)}…` : s
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 评估宿主兼容性。**永不抛**:任何拿不准的情况都按 `supported:false`(降级)处理。
|
|
128
|
+
* @param {unknown} version dsh 版本号;null/''/无法解析 = 未知 = 不支持
|
|
129
|
+
* @returns {{supported:boolean, version:string|null, range:string, severity:'ok'|'warn', reason?:string}}
|
|
130
|
+
*/
|
|
131
|
+
export function evaluateCompatibility(version) {
|
|
132
|
+
const input = version == null ? '' : String(version).trim()
|
|
133
|
+
if (!input) {
|
|
134
|
+
return {
|
|
135
|
+
supported: false,
|
|
136
|
+
version: null,
|
|
137
|
+
range: SUPPORTED_DSH_RANGE,
|
|
138
|
+
severity: 'warn',
|
|
139
|
+
reason: '未能识别 dsh 版本(视为未适配,插件自动禁用)',
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const parsed = parseDshVersion(input)
|
|
143
|
+
if (!parsed) {
|
|
144
|
+
return {
|
|
145
|
+
supported: false,
|
|
146
|
+
version: input,
|
|
147
|
+
range: SUPPORTED_DSH_RANGE,
|
|
148
|
+
severity: 'warn',
|
|
149
|
+
reason: `dsh 版本号无法解析(${show(input)}),视为未适配,插件自动禁用`,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (compareParsed(parsed, MIN_BOUND) < 0) {
|
|
153
|
+
return {
|
|
154
|
+
supported: false,
|
|
155
|
+
version: parsed.raw,
|
|
156
|
+
range: SUPPORTED_DSH_RANGE,
|
|
157
|
+
severity: 'warn',
|
|
158
|
+
reason: `dsh ${parsed.raw} 低于适配下限(要求 ${SUPPORTED_DSH_RANGE})`,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (compareParsed(parsed, MAX_BOUND) >= 0) {
|
|
162
|
+
return {
|
|
163
|
+
supported: false,
|
|
164
|
+
version: parsed.raw,
|
|
165
|
+
range: SUPPORTED_DSH_RANGE,
|
|
166
|
+
severity: 'warn',
|
|
167
|
+
reason: `dsh ${parsed.raw} 高于适配上限(要求 ${SUPPORTED_DSH_RANGE})`,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { supported: true, version: parsed.raw, range: SUPPORTED_DSH_RANGE, severity: 'ok' }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 读 package.json 的 version;读不到/不是字符串返回 ''。 */
|
|
174
|
+
function readPackageVersion(packageJsonPath) {
|
|
175
|
+
try {
|
|
176
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
177
|
+
return typeof pkg?.version === 'string' ? pkg.version.trim() : ''
|
|
178
|
+
} catch {
|
|
179
|
+
return ''
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 安全 realpath(路径可能不存在/是虚拟路径)。 */
|
|
184
|
+
function safeRealpath(p) {
|
|
185
|
+
try { return realpathSync(p) } catch { return '' }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 直接从"路径里含 `node_modules/@deepseek-ai/dsh/`"的位置推出包目录并读 package.json。
|
|
190
|
+
* 不依赖包的 `exports` 是否导出 `./package.json`(npx 缓存 / 全局安装 / 工作区三种布局都能命中)。
|
|
191
|
+
* @returns {string} 版本号;不是 dsh 路径或读不到返回 ''
|
|
192
|
+
*/
|
|
193
|
+
function versionFromNodeModulesPath(p) {
|
|
194
|
+
if (typeof p !== 'string' || !p) return ''
|
|
195
|
+
for (const marker of ['/node_modules/@deepseek-ai/dsh/', '/node_modules/@deepseek-ai/dsh-app-boot/']) {
|
|
196
|
+
const at = p.indexOf(marker)
|
|
197
|
+
if (at < 0) continue
|
|
198
|
+
const dir = p.slice(0, at + marker.length - 1) // 含 .../@deepseek-ai/dsh
|
|
199
|
+
const version = readPackageVersion(join(dir, 'package.json'))
|
|
200
|
+
if (version) return version
|
|
201
|
+
}
|
|
202
|
+
return ''
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 默认的"从已安装包解析 dsh 版本"实现。
|
|
207
|
+
* 候选锚点(按顺序,全部 try/catch):process.argv 里的脚本路径 → 本插件模块路径 → cwd。
|
|
208
|
+
* 每个锚点两条路:`require.resolve('<包>/package.json')`(依赖 exports)+ 路径直推(不依赖 exports)。
|
|
209
|
+
* @returns {string} 版本号;解析不到返回 ''
|
|
210
|
+
*/
|
|
211
|
+
function resolveDshPackageVersion({ argv = process.argv, cwd = process.cwd() } = {}) {
|
|
212
|
+
const candidates = []
|
|
213
|
+
for (const a of Array.isArray(argv) ? argv : []) {
|
|
214
|
+
if (typeof a !== 'string' || !a.trim()) continue
|
|
215
|
+
if (a === process.execPath) continue
|
|
216
|
+
if (a.startsWith('-')) continue
|
|
217
|
+
if (!(a.startsWith('/') || a.startsWith('.') || a.includes('/') || a.includes('\\'))) continue
|
|
218
|
+
try {
|
|
219
|
+
candidates.push(a.startsWith('/') ? a : resolvePath(a))
|
|
220
|
+
} catch { /* 非法路径跳过 */ }
|
|
221
|
+
}
|
|
222
|
+
candidates.push(fileURLToPath(import.meta.url), join(cwd, 'package.json'))
|
|
223
|
+
|
|
224
|
+
for (const base of candidates) {
|
|
225
|
+
for (const name of ['@deepseek-ai/dsh/package.json', '@deepseek-ai/dsh-app-boot/package.json']) {
|
|
226
|
+
try {
|
|
227
|
+
const req = createRequire(base)
|
|
228
|
+
const version = readPackageVersion(req.resolve(name))
|
|
229
|
+
if (version) return version
|
|
230
|
+
} catch { /* 下一个锚点/包名 */ }
|
|
231
|
+
}
|
|
232
|
+
for (const p of [base, safeRealpath(base)]) {
|
|
233
|
+
const version = versionFromNodeModulesPath(p)
|
|
234
|
+
if (version) return version
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return ''
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 尽力识别当前 dsh 版本。**永不抛**,失败返回 null。
|
|
242
|
+
*
|
|
243
|
+
* 优先级:
|
|
244
|
+
* ① env `WHYSHU_BA_DSH_VERSION` / `DSH_VERSION`(显式覆盖;离线验证/宿主未暴露版本时用);
|
|
245
|
+
* ② env `npm_package_name === '@deepseek-ai/dsh'` 时的 `npm_package_version`;
|
|
246
|
+
* ③ argv 里字面出现的 `@deepseek-ai/dsh@x.y.z`(`npm exec` 形态会留下);
|
|
247
|
+
* ④ 包解析(见 resolveDshPackageVersion)。
|
|
248
|
+
*
|
|
249
|
+
* @param {{env?:Record<string,string|undefined>, argv?:string[], resolvePackageVersion?:Function}} [options]
|
|
250
|
+
* 仅供测试注入;生产调用不传参。
|
|
251
|
+
* @returns {string|null}
|
|
252
|
+
*/
|
|
253
|
+
export function readDshVersion(options = {}) {
|
|
254
|
+
try {
|
|
255
|
+
const env = (options.env && typeof options.env === 'object') ? options.env : process.env
|
|
256
|
+
const argv = Array.isArray(options.argv) ? options.argv : process.argv
|
|
257
|
+
|
|
258
|
+
for (const key of DSH_VERSION_ENV_KEYS) {
|
|
259
|
+
const v = env && typeof env[key] === 'string' ? env[key].trim() : ''
|
|
260
|
+
if (v) return v
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (env && env.npm_package_name === '@deepseek-ai/dsh' && typeof env.npm_package_version === 'string') {
|
|
264
|
+
const v = env.npm_package_version.trim()
|
|
265
|
+
if (v) return v
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const literal = /@deepseek-ai\/dsh@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/
|
|
269
|
+
for (const a of argv) {
|
|
270
|
+
if (typeof a !== 'string') continue
|
|
271
|
+
const m = literal.exec(a)
|
|
272
|
+
if (m) return m[1]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const resolvePackageVersion = typeof options.resolvePackageVersion === 'function'
|
|
276
|
+
? options.resolvePackageVersion
|
|
277
|
+
: resolveDshPackageVersion
|
|
278
|
+
const resolved = resolvePackageVersion({ argv })
|
|
279
|
+
if (typeof resolved === 'string' && resolved.trim()) return resolved.trim()
|
|
280
|
+
return null
|
|
281
|
+
} catch {
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 兼容闸的降级动作:**只打日志,不碰任何宿主 API**(不注册端点、不监听事件、不加载子插件)。
|
|
288
|
+
*
|
|
289
|
+
* @param {object|null} ctx 宿主 context;当前刻意不使用(降级路径零宿主调用这一点有用例断言)
|
|
290
|
+
* @param {{version?:string|null, reason?:string, log?:Console}} [options]
|
|
291
|
+
* @returns {{supported:false, degraded:true, version:string|null, reason:string}}
|
|
292
|
+
*/
|
|
293
|
+
export function installCompatGuard(ctx, { version = null, reason = '', log = console } = {}) {
|
|
294
|
+
const shown = version ? show(version) : '未知版本'
|
|
295
|
+
const detail = String(reason || '').trim() || `不在支持范围 ${SUPPORTED_DSH_RANGE} 内`
|
|
296
|
+
const line = `[@whyshu/ba] 插件已自我禁用(dsh ${shown} 未适配):${detail}。`
|
|
297
|
+
+ 'dsh 可继续启动;升级 @whyshu/ba 到适配版本后会自动恢复。'
|
|
298
|
+
+ `(排障:可设 ${DSH_VERSION_ENV_KEYS[0]} 显式指定宿主版本)`
|
|
299
|
+
try {
|
|
300
|
+
if (log && typeof log.warn === 'function') log.warn(line)
|
|
301
|
+
else if (typeof log === 'function') log(line)
|
|
302
|
+
else console.warn(line)
|
|
303
|
+
} catch { /* 日志本身失败也绝不能影响宿主启动 */ }
|
|
304
|
+
return { supported: false, degraded: true, version: version || null, reason: detail }
|
|
305
|
+
}
|
package/src/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { dirname, join } from 'node:path'
|
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
14
|
|
|
15
15
|
import { ENV_FILE, fetchOverview, hasCredentials, loadSettings } from './binance.mjs'
|
|
16
|
+
import { evaluateCompatibility, installCompatGuard, readDshVersion } from './compat.mjs'
|
|
16
17
|
|
|
17
18
|
/** 每次都读盘:重装后 version 仍能反映当前磁盘版本 */
|
|
18
19
|
function pkgVersion() {
|
|
@@ -69,16 +70,46 @@ async function handle(req, res) {
|
|
|
69
70
|
export const name = 'whyshu-ba'
|
|
70
71
|
export const inject = ['webServer']
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 安全执行一次注册:任何异常只记日志,绝不让 apply 抛出。
|
|
75
|
+
* (apply 抛异常 = dsh 报 "entry did not activate",严重时整个 web boot 失败。)
|
|
76
|
+
* @param {string} label 注册项描述(日志用)
|
|
77
|
+
* @param {() => unknown} fn 注册动作
|
|
78
|
+
* @returns {unknown} fn 的返回值;失败返回 undefined
|
|
79
|
+
*/
|
|
80
|
+
function safeRegister(label, fn) {
|
|
81
|
+
try {
|
|
82
|
+
return fn()
|
|
83
|
+
} catch (e) {
|
|
84
|
+
try {
|
|
85
|
+
console.warn(`[whyshu-ba] ${label} 注册失败(不影响 dsh 继续启动):`, (e && e.message) || e)
|
|
86
|
+
} catch { /* 日志本身失败也不能抛 */ }
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
export function apply(ctx) {
|
|
73
|
-
//
|
|
92
|
+
// 整体 try/catch:apply 抛异常会变成 dsh 的 "entry did not activate"(甚至 boot 失败)。
|
|
74
93
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
// ① 宿主兼容闸:未适配版本(如 dsh 0.3.x)⇒ 只打一行日志后直接 return,零宿主调用。
|
|
95
|
+
const version = readDshVersion()
|
|
96
|
+
const compat = evaluateCompatibility(version)
|
|
97
|
+
if (!compat.supported) {
|
|
98
|
+
installCompatGuard(ctx, { version, reason: compat.reason })
|
|
77
99
|
return
|
|
78
100
|
}
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
|
|
102
|
+
// ② 只读端点注册(safe 包裹;拿不到 webServer 时降级为"面板 404,dsh 正常启动")
|
|
103
|
+
safeRegister('webServer', () => {
|
|
104
|
+
if (!ctx || !ctx.webServer || typeof ctx.webServer.register !== 'function') {
|
|
105
|
+
throw new Error('未拿到 webServer 服务,跳过端点注册')
|
|
106
|
+
}
|
|
107
|
+
ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/api/whyshu-ba', handler: handle }), 'whyshu-ba: read-only binance endpoints')
|
|
108
|
+
})
|
|
109
|
+
console.log(`[whyshu-ba] 已注册 /api/whyshu-ba/*(v${pkgVersion()},dsh ${compat.version},只读,币安 Key 仅在宿主侧使用)`)
|
|
81
110
|
} catch (e) {
|
|
82
|
-
|
|
111
|
+
try {
|
|
112
|
+
console.error('[whyshu-ba] apply 异常已忽略(不影响 dsh 启动):', (e && e.message) || e)
|
|
113
|
+
} catch { /* noop */ }
|
|
83
114
|
}
|
|
84
115
|
}
|