@wenbin_wb/dsh-bridge 2.9.0 → 2.10.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/CHANGELOG.md +320 -295
- package/client/client.js +370 -124
- package/client/index.js +4244 -4073
- package/client/unlock-manager.js +142 -0
- package/docs/release-process.md +40 -0
- package/lib/auth/manager.js +545 -532
- package/lib/bridge-rpc.js +455 -436
- package/lib/index.js +1831 -1765
- package/package.json +106 -106
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// 统一管理员解锁管理器(client/unlock-manager.js)
|
|
2
|
+
//
|
|
3
|
+
// 背景:多个 RPC 封装(BridgePanel 的 authRpcCall、目录选择器的 authRpc)都会遇到
|
|
4
|
+
// "需要管理员权限" 的拦截,此前各自处理、目录选择器甚至没有解锁入口导致流程卡死。
|
|
5
|
+
// 本模块把"token 管理 + 权限拦截检测 + 解锁动作 + 被拦截操作自动重放"统一收敛。
|
|
6
|
+
//
|
|
7
|
+
// 设计要点:
|
|
8
|
+
// - token 与 sessionStorage 同步,是本插件唯一的 adminToken 缓存来源
|
|
9
|
+
// - 权限失败 -> 解锁成功 -> 自动重放被拦截的 RPC(pending 队列)
|
|
10
|
+
// - 与具体 UI 框架解耦,纯 JS 单例
|
|
11
|
+
|
|
12
|
+
const SESSION_KEY = 'dsh_admin_token';
|
|
13
|
+
|
|
14
|
+
let _token = '';
|
|
15
|
+
let _pendingOps = []; // 被拦截待重放的操作队列(解锁后按序重放)
|
|
16
|
+
let _onUnlocked = null; // 解锁成功后的全局回调(可多个)
|
|
17
|
+
|
|
18
|
+
// 读写 sessionStorage
|
|
19
|
+
function _readStorage() {
|
|
20
|
+
try { return typeof window !== 'undefined' ? window.sessionStorage.getItem(SESSION_KEY) : ''; } catch { return ''; }
|
|
21
|
+
}
|
|
22
|
+
function _writeStorage(t) {
|
|
23
|
+
try {
|
|
24
|
+
if (typeof window === 'undefined') return;
|
|
25
|
+
if (t) window.sessionStorage.setItem(SESSION_KEY, t);
|
|
26
|
+
else window.sessionStorage.removeItem(SESSION_KEY);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getAdminToken() {
|
|
31
|
+
if (_token) return _token;
|
|
32
|
+
const saved = _readStorage();
|
|
33
|
+
if (saved) { _token = saved; return saved; }
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function setAdminToken(t) {
|
|
38
|
+
_token = t || '';
|
|
39
|
+
_writeStorage(_token);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function clearAdminToken() {
|
|
43
|
+
setAdminToken('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 本机(回环)场景:从多候选 URL 获取 loopback adminToken(免输密码直通管理)。
|
|
48
|
+
* 支持 3080 原生端口与 3082 代理端口;不信任 sessionStorage 里的旧 token
|
|
49
|
+
* ——DSH 重启后 adminSessions 清空,旧 token 失效,因此强制每次获取新的。
|
|
50
|
+
* 返回 null 表示获取失败(非本机 / 端点不可达)。
|
|
51
|
+
* @param {boolean} [force] true 时忽略已缓存 token 强制重取(默认 true)
|
|
52
|
+
*/
|
|
53
|
+
export async function fetchLoopbackTokenOnce(force = true) {
|
|
54
|
+
if (typeof window === 'undefined') return null;
|
|
55
|
+
if (!force) {
|
|
56
|
+
const existing = getAdminToken();
|
|
57
|
+
if (existing) return existing;
|
|
58
|
+
}
|
|
59
|
+
const candidates = [
|
|
60
|
+
'/__dsh_bridge__/loopback-token',
|
|
61
|
+
'http://127.0.0.1:3082/__dsh_bridge__/loopback-token',
|
|
62
|
+
'http://localhost:3082/__dsh_bridge__/loopback-token',
|
|
63
|
+
];
|
|
64
|
+
for (const url of [...new Set(candidates)]) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(url, { method: 'POST' });
|
|
67
|
+
if (res.ok) {
|
|
68
|
+
const data = await res.json();
|
|
69
|
+
if (data?.ok && data.adminToken) {
|
|
70
|
+
setAdminToken(data.adminToken);
|
|
71
|
+
return data.adminToken;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 暂存被拦截的操作,解锁成功后自动重放(队列:多个被拦操作按序重放)。
|
|
81
|
+
* @param {function} retry 返回 Promise 的重试函数(带新 token 重新发起)
|
|
82
|
+
*/
|
|
83
|
+
export function queuePendingOperation(retry) {
|
|
84
|
+
_pendingOps.push(retry);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 解锁成功后重放所有 pending 操作(若有) */
|
|
88
|
+
export async function replayPending() {
|
|
89
|
+
if (_pendingOps.length === 0) return;
|
|
90
|
+
const ops = _pendingOps;
|
|
91
|
+
_pendingOps = [];
|
|
92
|
+
for (const retry of ops) {
|
|
93
|
+
try { await retry(); } catch {}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 监听解锁成功(可多个);返回注销函数 */
|
|
98
|
+
export function onUnlocked(cb) {
|
|
99
|
+
if (!_onUnlocked) _onUnlocked = [];
|
|
100
|
+
_onUnlocked.push(cb);
|
|
101
|
+
return () => { _onUnlocked = _onUnlocked.filter((f) => f !== cb); };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 内部:解锁成功后触发 */
|
|
105
|
+
function _emitUnlocked() {
|
|
106
|
+
if (_onUnlocked) {
|
|
107
|
+
for (const cb of _onUnlocked) { try { cb(); } catch {} }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 统一解锁动作:调 authAdminUnlock,成功存 token 并触发回调/重放。
|
|
113
|
+
* @param {function} rpcCall 底层 rpcCall(用于调 authAdminUnlock)
|
|
114
|
+
* @param {string} password 管理密码
|
|
115
|
+
* @returns {Promise<{ok:boolean, error?:string}>}
|
|
116
|
+
*/
|
|
117
|
+
export async function unlockAdmin(rpcCall, password) {
|
|
118
|
+
try {
|
|
119
|
+
const res = await rpcCall('authAdminUnlock', { password });
|
|
120
|
+
if (res?.ok) {
|
|
121
|
+
const token = res.value?.adminToken || '';
|
|
122
|
+
setAdminToken(token);
|
|
123
|
+
_emitUnlocked();
|
|
124
|
+
await replayPending();
|
|
125
|
+
return { ok: true };
|
|
126
|
+
}
|
|
127
|
+
return { ok: false, error: res?.error?.message || '管理员密码错误' };
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// 访问会话失效(HTTP 401):authAdminUnlock 即使豁免,主面板后续请求也会 401,
|
|
130
|
+
// 解锁成功也进不去。此时应整页重载回绿色登录页重新获取访问会话。
|
|
131
|
+
const msg = String(err?.message || err || '');
|
|
132
|
+
if (msg.includes('401') || msg.includes('transport failure') || msg.includes('unauthorized')) {
|
|
133
|
+
if (typeof window !== 'undefined' && typeof window.location?.reload === 'function') {
|
|
134
|
+
try {
|
|
135
|
+
window.sessionStorage.setItem('dsh_access_expired_reloaded', '1');
|
|
136
|
+
} catch {}
|
|
137
|
+
window.location.reload();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { ok: false, error: err?.message || '解锁请求失败' };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 发布流程规则(RELEASE PROCESS)
|
|
2
|
+
|
|
3
|
+
> 本文件是**发布操作的红线规则**,任何一次对外发布(npm / GitHub Release)都必须遵守。
|
|
4
|
+
|
|
5
|
+
## 铁律:发布前必须人工确认
|
|
6
|
+
|
|
7
|
+
**任何版本(正式版 / 补丁版)在推 tag、npm publish、创建 GitHub Release 之前,必须先向仓库所有者(wenbin-wb)确认,得到明确同意后才可执行。**
|
|
8
|
+
|
|
9
|
+
原因(2026-09-01 教训):v2.10.0 发布时未先确认就自行完成了 合并 → 打 tag → 推送 → npm publish 全流程,随后发现发布说明措辞需要修订,而 npm 不允许覆盖已发布版本,只能再补 v2.10.1 修正,造成不必要的版本噪音。
|
|
10
|
+
|
|
11
|
+
### 确认清单(发布前发给所有者)
|
|
12
|
+
|
|
13
|
+
1. **版本号**:下一个版本号(如 `v2.10.1`)是否认可?
|
|
14
|
+
2. **发布说明(releaseNotes / CHANGELOG)**:内容是否准确?是否只包含用户可感知的变更(新功能、真实修复),不包含开发过程细节?
|
|
15
|
+
3. **发布范围**:npm 发布 + GitHub Release + tag,是否全部执行?
|
|
16
|
+
|
|
17
|
+
得到「可以发布」的明确回复后,才允许执行发布动作。
|
|
18
|
+
|
|
19
|
+
## 发布流程
|
|
20
|
+
|
|
21
|
+
1. **改版本**:`package.json` 的 `version` + `releaseNotes`
|
|
22
|
+
2. **写 CHANGELOG**:`CHANGELOG.md` 顶部新增对应版本条目
|
|
23
|
+
3. **构建与测试**:`npm run build:lark` + `npm run build:client` + `npm run build:banner` + `npm test`(全量 166 项)
|
|
24
|
+
4. **提交**:合并到 `main`(fast-forward),提交信息 `release: vX.Y.Z — ...`
|
|
25
|
+
5. **打 tag**:`git tag vX.Y.Z && git push origin vX.Y.Z`(触发 GitHub Actions 自动创建 Release)
|
|
26
|
+
6. **npm 发布**:`npm publish`(prepack 自动构建产物)
|
|
27
|
+
7. **验证**:npm 线上版本 + GitHub Release body 是否正确
|
|
28
|
+
|
|
29
|
+
## 发布说明写作规范
|
|
30
|
+
|
|
31
|
+
- ✅ 只写**用户可感知**的变更:新功能、真实修复(用户报告的问题)
|
|
32
|
+
- ❌ 不写开发过程中自己引入又修复的内部 bug
|
|
33
|
+
- ❌ 不罗列内部重构/清理细节(一句话概括即可)
|
|
34
|
+
- ✅ 修复类条目建议**合并成一条概括**,不逐条展开技术实现
|
|
35
|
+
|
|
36
|
+
## 注意事项
|
|
37
|
+
|
|
38
|
+
- npm **不允许覆盖已发布版本**(403)。发布后发现说明要改,只能 bump 补丁版本——所以发布前确认说明尤其重要。
|
|
39
|
+
- `release.yml` workflow 只创建 GitHub Release,npm 发布是手动步骤,不要漏。
|
|
40
|
+
- 发布后如发现问题,走补丁版本(`vX.Y.Z+1`),不要尝试覆盖。
|