dsh-dock 0.13.0 → 0.15.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 +27 -8
- package/client.js +1235 -610
- package/features/animation/view.jsx +2 -2
- package/features/balance/view.js +1 -1
- package/features/games/view.jsx +1 -1
- package/features/mobile/host.js +306 -0
- package/features/mobile/view.js +359 -154
- package/features/mobile-relay/host.js +5 -245
- package/features/mobile-relay/view.jsx +9 -330
- package/features/modelconfig/host.js +195 -1
- package/features/modelconfig/view.js +762 -92
- package/features/notify/view.jsx +2 -2
- package/features/runstate/host.js +10 -0
- package/features/runstate/view.jsx +2 -2
- package/features/tokenlog/view.jsx +44 -14
- package/index.js +15 -1
- package/package.json +9 -3
- package/screenshots/dock-modelconfig-provider-create.png +0 -0
- package/screenshots/dock-modelconfig-provider-edit.png +0 -0
- package/screenshots/dock-modelconfig-providers.png +0 -0
- package/screenshots/dock-modelconfig.png +0 -0
- package/src/client.jsx +1 -1
- package/src/shared.js +6 -3
- package/src/welcome-ack.js +69 -0
package/features/notify/view.jsx
CHANGED
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
// features/runstate/,本模块只管通知(页内卡片 / 提示音 / 系统通知 / 钉钉飞书群机器人推送)。
|
|
11
11
|
// 模块启停 = 功能坞里的开关(独立菜单项),本页不再有第二个总开关。
|
|
12
12
|
//
|
|
13
|
-
// Host 通信:fetch('
|
|
13
|
+
// Host 通信:fetch('dsh-dock/notify/<method>')(见 features/notify/host.js)。
|
|
14
14
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
15
15
|
|
|
16
16
|
// ---------- Host RPC 桥接 ----------
|
|
17
17
|
function rpcCall(method, args) {
|
|
18
|
-
return fetch("
|
|
18
|
+
return fetch("dsh-dock/notify/" + method, {
|
|
19
19
|
method: "POST",
|
|
20
20
|
headers: { "content-type": "application/json" },
|
|
21
21
|
body: JSON.stringify(args === undefined ? {} : args),
|
|
@@ -20,6 +20,8 @@ export const feature = {
|
|
|
20
20
|
setup(ctx) {
|
|
21
21
|
const disposers = []
|
|
22
22
|
const dispose = () => {
|
|
23
|
+
// 撤掉「本宿主注册了 runstate 路由」旗标(见下方注册处的说明)
|
|
24
|
+
try { if (typeof window !== 'undefined' && window.__dshDockRunstate) delete window.__dshDockRunstate } catch { /* ignore */ }
|
|
23
25
|
while (disposers.length > 0) {
|
|
24
26
|
const fn = disposers.pop()
|
|
25
27
|
try { if (typeof fn === 'function') fn() } catch { /* 停用清理失败不阻断 */ }
|
|
@@ -33,6 +35,14 @@ export const feature = {
|
|
|
33
35
|
|
|
34
36
|
// ===== RPC 路由 =====
|
|
35
37
|
disposers.push(ctx.inject(['webServer'], (wsCtx) => {
|
|
38
|
+
// 路由注册成功后打旗标:【手机适配】的汉堡角标据此决定要不要轮询 status。
|
|
39
|
+
// 没有它的话,功能停用时角标会先打一次必然失败的请求 → 控制台一条 405/404,
|
|
40
|
+
// 与【用量记录】chip 当年同一个毛病(见 features/tokenlog/view.jsx 的 routeGone 闩锁)。
|
|
41
|
+
// 旗标与 __dshDockMobileDrawer / __dshDockWelcomeAck 同风格:宿主侧唯一事实,客户端只读。
|
|
42
|
+
wsCtx.effect(() => {
|
|
43
|
+
if (typeof window !== 'undefined') window.__dshDockRunstate = true
|
|
44
|
+
return () => { try { if (typeof window !== 'undefined' && window.__dshDockRunstate) delete window.__dshDockRunstate } catch { /* ignore */ } }
|
|
45
|
+
})
|
|
36
46
|
wsCtx.effect(() => wsCtx.webServer.register({
|
|
37
47
|
kind: 'prefix',
|
|
38
48
|
path: '/dsh-dock/runstate',
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
// 共用同一份实例),本模块不写任何配置——动效在 features/animation/、结束与确认提醒在
|
|
9
9
|
// features/notify/。没有全局浮层,因此只在面板打开时轮询,关闭面板即零开销。
|
|
10
10
|
//
|
|
11
|
-
// Host 通信:fetch('
|
|
11
|
+
// Host 通信:fetch('dsh-dock/runstate/status')(见 features/runstate/host.js)。
|
|
12
12
|
import { useState, useEffect } from "react";
|
|
13
13
|
|
|
14
14
|
// ---------- Host RPC 桥接 ----------
|
|
15
15
|
function rpcCall(method, args) {
|
|
16
|
-
return fetch("
|
|
16
|
+
return fetch("dsh-dock/runstate/" + method, {
|
|
17
17
|
method: "POST",
|
|
18
18
|
headers: { "content-type": "application/json" },
|
|
19
19
|
body: JSON.stringify(args === undefined ? {} : args),
|
|
@@ -8,13 +8,27 @@
|
|
|
8
8
|
// - 挂载即扫描历史+按暂存条件查询;挂载期间每 5s 静默自动刷新
|
|
9
9
|
// - 查询等待动画:点「查询」后按钮转圈扫光 + 数据区顶部「记账小队清点」banner(轮换俏皮文案,
|
|
10
10
|
// 等久了换语气),旧数据压暗禁点;结果到位时闪一次「✓ 数据已更新」并让 KPI 卡回弹
|
|
11
|
-
// Host 通信:fetch('
|
|
11
|
+
// Host 通信:fetch('dsh-dock/tokenlog/<method>')(见 features/tokenlog/host.js)。
|
|
12
12
|
import react, { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
13
13
|
import { openPanel } from "../../src/shared.js";
|
|
14
14
|
|
|
15
15
|
// ---------- Host RPC 桥接 ----------
|
|
16
|
+
// 路由不可用(宿主没注册用量路由)的两种成因必须区分开,否则提示会误导人:
|
|
17
|
+
// - 404/405:宿主进程是旧版本,**或**本功能当前在宿主侧是停用的(多设备开关不一致、
|
|
18
|
+
// localStorage 与宿主 settings 不同步都会这样)。此时路由就是不存在,**重试无用**。
|
|
19
|
+
// - 其余状态码:网络/服务端错误,重试有意义。
|
|
20
|
+
// 因此 404/405 会置 routeGone 闩锁:所有调用方据此**停止轮询**,既不再刷控制台报错,
|
|
21
|
+
// 也不会每 10s 打一次无效请求(chip 原本会一直重试到页面关闭)。
|
|
22
|
+
let routeGone = false;
|
|
23
|
+
/** 路由是否已确认不可用(宿主未注册)。一旦为真,本次页面生命周期内不再重试。 */
|
|
24
|
+
export function isTokenlogRouteGone() { return routeGone; }
|
|
25
|
+
/** 把 404/405 的错误翻译成人能看懂、且不误导的提示。 */
|
|
26
|
+
function routeGoneMessage() {
|
|
27
|
+
return "宿主未启用「用量记录」路由(或宿主进程是旧版本),请在功能坞里启用该功能或重启 dsh web";
|
|
28
|
+
}
|
|
16
29
|
function rpcCall(method, args) {
|
|
17
|
-
|
|
30
|
+
if (routeGone) return Promise.reject(new Error(routeGoneMessage()));
|
|
31
|
+
return fetch("dsh-dock/tokenlog/" + method, {
|
|
18
32
|
method: "POST",
|
|
19
33
|
headers: { "content-type": "application/json" },
|
|
20
34
|
body: JSON.stringify(args === undefined ? {} : args),
|
|
@@ -22,9 +36,13 @@ function rpcCall(method, args) {
|
|
|
22
36
|
.then(async (res) => {
|
|
23
37
|
const data = await res.json().catch(() => ({}));
|
|
24
38
|
if (res.ok && data && data.ok === true) return data.data;
|
|
25
|
-
//
|
|
39
|
+
// 宿主没注册用量路由(405/404):置闩锁并给出可操作提示而非裸状态码。
|
|
40
|
+
// 注意这既可能是「宿主旧版本」,也可能是「功能在宿主侧被停用」——
|
|
41
|
+
// 实测后者更常见(多设备开关/localStorage 与宿主 settings 不一致),
|
|
42
|
+
// 所以文案两种都提到,且置闩锁后不再无谓重试。
|
|
26
43
|
if (res.status === 405 || res.status === 404) {
|
|
27
|
-
|
|
44
|
+
routeGone = true;
|
|
45
|
+
throw new Error(routeGoneMessage());
|
|
28
46
|
}
|
|
29
47
|
if (data && data.ok === false) throw new Error((data.error && data.error.message) || ("HTTP " + res.status));
|
|
30
48
|
throw new Error("HTTP " + res.status + (data && data.error && data.error.message ? ": " + data.error.message : ""));
|
|
@@ -1377,27 +1395,39 @@ export function TokenLogChip(props) {
|
|
|
1377
1395
|
useEffect(() => {
|
|
1378
1396
|
if (!sid) return;
|
|
1379
1397
|
let cancel = false;
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1398
|
+
let timer = null;
|
|
1399
|
+
const load = () => {
|
|
1400
|
+
// 路由不可用是**终态**(宿主没注册该路由):直接早退,不再每 10s 打一次
|
|
1401
|
+
// 无效请求、不再刷控制台报错。此前会一直重试到页面关闭(实测控制台连续 405)。
|
|
1402
|
+
if (cancel || isTokenlogRouteGone()) return null;
|
|
1403
|
+
return rpcCall("query", { sessionId: sid })
|
|
1404
|
+
.then((d) => { if (!cancel) setSnap({ totals: d && d.totals, rate: (d && d.rateUsdCny) || 7.2, err: "" }); })
|
|
1405
|
+
.catch((e) => { if (!cancel) setSnap((s) => ({ totals: s.totals, rate: s.rate, err: String((e && e.message) || e) })); });
|
|
1406
|
+
};
|
|
1383
1407
|
load();
|
|
1384
|
-
|
|
1385
|
-
return () => { cancel = true; clearInterval(timer); };
|
|
1408
|
+
timer = setInterval(load, 10000);
|
|
1409
|
+
return () => { cancel = true; if (timer) clearInterval(timer); };
|
|
1386
1410
|
}, [sid]);
|
|
1387
1411
|
if (!sid) return null;
|
|
1388
1412
|
const t = snap.totals;
|
|
1413
|
+
const gone = isTokenlogRouteGone();
|
|
1389
1414
|
// chip 文案用紧凑人民币金额(2 位小数),避免撑爆输入卡工具行;
|
|
1390
1415
|
// 完整金额在 title 悬浮提示里。
|
|
1391
|
-
const label =
|
|
1392
|
-
?
|
|
1416
|
+
const label = gone
|
|
1417
|
+
? "用量·未启用"
|
|
1418
|
+
: snap.err && !t
|
|
1419
|
+
? (snap.err.includes("重启") || snap.err.includes("启用") ? "用量·未启用" : "用量·失败")
|
|
1393
1420
|
: (t ? "⛁ " + fmtCompact(t.totalTokens) + (t.cost > 0 ? " · " + fmtCostCny(t.cost, snap.rate) : "") : "⛁ …");
|
|
1394
|
-
const title =
|
|
1421
|
+
const title = gone
|
|
1422
|
+
? routeGoneMessage() + "\n(可在功能坞里启用「用量记录」)"
|
|
1423
|
+
: t
|
|
1395
1424
|
? "本会话 " + fmtNum(t.calls) + " 次调用 · " + fmtNum(t.totalTokens) + " Token · " + fmtCostCny(t.cost, snap.rate) + "(估算)\n点击在功能坞查看用量记录"
|
|
1396
1425
|
: (snap.err ? snap.err + "\n" : "") + "点击在功能坞查看用量记录";
|
|
1426
|
+
// 路由不可用时不做成报错态(红色),它不是错误,是「这个功能当前没开」。
|
|
1397
1427
|
return (
|
|
1398
|
-
<button type="button" className={"dockchip" + (snap.err && !t ? " err" : "")} title={title} aria-label="会话用量"
|
|
1428
|
+
<button type="button" className={"dockchip" + (snap.err && !t && !gone ? " err" : "")} title={title} aria-label="会话用量"
|
|
1399
1429
|
onClick={() => openPanel("tokenlog", { sessionId: sid, navAt: Date.now() })}>
|
|
1400
|
-
<span className="dockchip-dot" style={{ background: "var(--dk-warn)" }} />
|
|
1430
|
+
<span className="dockchip-dot" style={{ background: gone ? "var(--dsw-alias-label-tertiary,#8b95ab)" : "var(--dk-warn)" }} />
|
|
1401
1431
|
<span>{label}</span>
|
|
1402
1432
|
</button>
|
|
1403
1433
|
);
|
package/index.js
CHANGED
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
// - animation 任务动画(v0.5.0):会话任务追踪 + 动效配置持久化(参照 @wycto/dsh-task-pulse)
|
|
16
16
|
// - notify 任务通知:完成/异常/需确认通知 + 提示音/系统通知/钉钉飞书推送(从任务动画拆出)
|
|
17
17
|
// - runstate 运行状态:进行中任务与最近完成一览(从任务动画拆出;只读,无配置段)
|
|
18
|
-
// - mobile
|
|
18
|
+
// - mobile 手机适配:窄屏排版/Tab 栏/抽屉/遮罩/键盘(宿主经 tapIndex 注入 index.html)
|
|
19
|
+
// - mobile-relay 远程访问:账号密码登录的远程入口(网关 + 账号 + 浏览模式目录选择器)
|
|
19
20
|
import { DockConfig, sendJson, readBody, migrateNotifyConfig, migrateModelsFeatureId, migrateImportedFeatures, migrateImportedSections, bindDockConfig, readDockRoot, mutateDockSection } from './src/host-core.js'
|
|
21
|
+
import { injectWelcomeAck } from './src/welcome-ack.js'
|
|
20
22
|
import { feature as fModels } from './features/modelconfig/host.js'
|
|
21
23
|
import { feature as fVisionProxy } from './features/visionproxy/host.js'
|
|
22
24
|
import { feature as fBalance } from './features/balance/host.js'
|
|
@@ -25,6 +27,7 @@ import { feature as fAnimation } from './features/animation/host.js'
|
|
|
25
27
|
import { feature as fNotify } from './features/notify/host.js'
|
|
26
28
|
import { feature as fRunState } from './features/runstate/host.js'
|
|
27
29
|
import { feature as fMobileRelay } from './features/mobile-relay/host.js'
|
|
30
|
+
import { feature as fMobile } from './features/mobile/host.js'
|
|
28
31
|
|
|
29
32
|
export const name = 'dsh-dock'
|
|
30
33
|
|
|
@@ -66,6 +69,7 @@ export function apply(ctx, config) {
|
|
|
66
69
|
fNotify,
|
|
67
70
|
fRunState,
|
|
68
71
|
fMobileRelay,
|
|
72
|
+
fMobile,
|
|
69
73
|
]
|
|
70
74
|
|
|
71
75
|
// 绑定 Config 镜像(替代已移除的 settings.get);volatile 字段热更时重绑。
|
|
@@ -170,6 +174,16 @@ export function apply(ctx, config) {
|
|
|
170
174
|
})
|
|
171
175
|
})
|
|
172
176
|
|
|
177
|
+
// 插件级兼容注入(不随功能开关启停):远程(非回环)浏览器的「预览版说明」自动确认。
|
|
178
|
+
// 根因与归属见 src/welcome-ack.js 文件头——关掉远程访问、主实例直接绑 0.0.0.0 的
|
|
179
|
+
// 直连形态同样每次重弹,所以不能挂在【远程访问】名下;回环地址脚本自我短路。
|
|
180
|
+
{
|
|
181
|
+
const webServer = ctx.get('webServer')
|
|
182
|
+
if (webServer && typeof webServer.tapIndex === 'function') {
|
|
183
|
+
ctx.effect(() => webServer.tapIndex(injectWelcomeAck), 'dsh-dock: welcome-ack index tap')
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
173
187
|
// 功能开关管理路由(无条件注册):GET 查询各功能宿主侧状态;POST 同步开关——
|
|
174
188
|
// 浏览器面板 toggle 经此写入 settings(跨浏览器/重启保持)并即时 setup/dispose 宿主半部。
|
|
175
189
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "DeepSeek Harness 功能坞插件(dsh-dock):模块化功能中枢——每个功能一个 features/ 模块(可单独提取成包发布,又可经 dockBridge 装回面板)。含用量记录(LLM 调用记账/费用统计,支持各模型自定义单价、多段分时价与周末/节假日谷价)、模型设置、图片理解代理、模型余额、任务动画(19 种运行动画 + 桌面伙伴 3D 场景)、任务通知(完成/异常/需确认通知、提示音、系统通知、钉钉/飞书推送)、运行状态(进行中任务与最近完成一览);侧栏入口 + 可拖拽弹窗(最大化/最小化/缩放)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build:client": "node scripts/build-client.mjs",
|
|
45
45
|
"test:client": "node scripts/test-client-views.mjs",
|
|
46
|
-
"test:host": "node scripts/test-task-host.mjs && node scripts/test-tokenlog-host.mjs && node scripts/test-legacy-import.mjs && node scripts/test-mobile-relay-host.mjs && node scripts/test-mobile-relay.mjs",
|
|
46
|
+
"test:host": "node scripts/test-task-host.mjs && node scripts/test-tokenlog-host.mjs && node scripts/test-legacy-import.mjs && node scripts/test-mobile-host.mjs && node scripts/test-mobile-relay-host.mjs && node scripts/test-mobile-relay.mjs && node scripts/test-welcome-ack.mjs",
|
|
47
47
|
"dev:client": "node scripts/watch-client.mjs",
|
|
48
48
|
"dev:link": "node scripts/dev-link-deps.mjs"
|
|
49
49
|
},
|
|
@@ -64,5 +64,11 @@
|
|
|
64
64
|
"@deepseek-ai/dsh-llm": "*",
|
|
65
65
|
"@deepseek-ai/dsh-settings": "*",
|
|
66
66
|
"@deepseek-ai/schemastery": "*"
|
|
67
|
+
},
|
|
68
|
+
"allowScripts": {
|
|
69
|
+
"file:../esbuild@0.28.1/node_modules/esbuild": true,
|
|
70
|
+
"file:../../esbuild@0.28.1/node_modules/esbuild": true,
|
|
71
|
+
"file:../../esbuild@0.25.12/node_modules/esbuild": true,
|
|
72
|
+
"file:../../esbuild@0.21.5/node_modules/esbuild": true
|
|
67
73
|
}
|
|
68
|
-
}
|
|
74
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/client.jsx
CHANGED
|
@@ -31,7 +31,7 @@ import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
|
|
|
31
31
|
import { feature as fMobile } from "../features/mobile/view.js";
|
|
32
32
|
|
|
33
33
|
const name = "dsh-dock";
|
|
34
|
-
const DOCK_VERSION = "0.
|
|
34
|
+
const DOCK_VERSION = "0.15.0";
|
|
35
35
|
|
|
36
36
|
// ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
|
|
37
37
|
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fNotify, fRunState, fGames, fMobileRelay, fMobile];
|
package/src/shared.js
CHANGED
|
@@ -68,11 +68,14 @@ export function initFeatureState(defs) {
|
|
|
68
68
|
syncFeatureStateFromHost(defs);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/** 从宿主
|
|
71
|
+
/** 从宿主 dsh-dock/features 拉取持久化开关,覆盖尚未本地表态的功能默认值。
|
|
72
|
+
* 路由一律用文档相对路径(不带前导斜杠):DSH 的 index.html 带 <base href="./">,
|
|
73
|
+
* 子路径反代(如 /ui/)下自动解析为 /ui/dsh-dock/...,根挂载下即 dsh-dock/...。
|
|
74
|
+
* 写死前导斜杠会绕过反代挂载前缀,请求被 302 打回应用入口,宿主接口整片失败。
|
|
72
75
|
* 拉取失败(宿主旧版本无此路由)静默降级——沿用本地默认。 */
|
|
73
76
|
function syncFeatureStateFromHost(defs) {
|
|
74
77
|
if (typeof fetch !== "function") return;
|
|
75
|
-
fetch("
|
|
78
|
+
fetch("dsh-dock/features").then(async (res) => {
|
|
76
79
|
if (!res.ok) return;
|
|
77
80
|
const data = await res.json().catch(() => null);
|
|
78
81
|
const persisted = data && data.ok && data.data && data.data.persisted;
|
|
@@ -108,7 +111,7 @@ function syncFeatureStateFromHost(defs) {
|
|
|
108
111
|
* 开关永远 404 而这里毫无声息,排查了两个版本才发现。) */
|
|
109
112
|
function pushFeatureEnabledToHost(id, enabled) {
|
|
110
113
|
if (typeof fetch !== "function") return;
|
|
111
|
-
fetch("
|
|
114
|
+
fetch("dsh-dock/features", {
|
|
112
115
|
method: "POST",
|
|
113
116
|
headers: { "content-type": "application/json" },
|
|
114
117
|
body: JSON.stringify({ id, enabled }),
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// dsh-dock · 插件级兼容注入:远程(非回环)浏览器的「预览版说明」自动确认
|
|
2
|
+
//
|
|
3
|
+
// 根因(dsh 0.2.0 ui-settings-models/src/client/welcome-store.ts,文件头注释原文):
|
|
4
|
+
// 回环浏览器的确认走宿主持久化,一次确认永久生效;**远程(非回环)浏览器的设置 scope
|
|
5
|
+
// 是 memory 模式,确认只存页面进程内存,刷新即丢**——所以从局域网/组网 IP 打开时
|
|
6
|
+
// 「预览版说明」每次刷新都重弹,profile patch 里钉 welcomeNoticeVersion 也救不了
|
|
7
|
+
// (memory 分支根本不读持久值)。
|
|
8
|
+
//
|
|
9
|
+
// 归属:这是「非回环浏览器」这一访问形态的兼容问题,与任何一个功能模块的职责都不重合——
|
|
10
|
+
// 【远程访问】只保留远程入口本身(用户要求),而关掉远程访问、主实例经 patch 层绑 0.0.0.0
|
|
11
|
+
// 的直连形态(公司部署)同样会每次重弹。所以挂在插件级(index.js 注册 tapIndex),
|
|
12
|
+
// 不随任何功能开关启停;回环地址自我短路,让 dsh 原生的一次性持久确认自然工作。
|
|
13
|
+
//
|
|
14
|
+
// 与【远程访问】网关的关系:网关代理的页面由网关对连接 bundle 做回环自判改写,
|
|
15
|
+
// 确认可持久化、弹窗首次确认后不再出现,本脚本找不到弹窗即零副作用;直连形态下
|
|
16
|
+
// 本脚本每次页面加载最多替用户点一次「继续」。dsh 未来若让远程确认可持久化,
|
|
17
|
+
// 弹窗不再出现,本脚本自动空转,无需随版本跟进。
|
|
18
|
+
//
|
|
19
|
+
// 注入点在 <head> 之后,body 尚未解析:观察器一律推迟到 DOMContentLoaded 再挂。
|
|
20
|
+
const WELCOME_ACK_JS = [
|
|
21
|
+
';(function(){',
|
|
22
|
+
'if (window.__dshDockWelcomeAck) return;',
|
|
23
|
+
'window.__dshDockWelcomeAck = "host";',
|
|
24
|
+
'var host = (window.location && window.location.hostname) || "";',
|
|
25
|
+
'if (host === "127.0.0.1" || host === "localhost" || host === "::1" || host.slice(-10) === ".localhost") return;',
|
|
26
|
+
'if (typeof MutationObserver === "undefined") return;',
|
|
27
|
+
'var done = false;',
|
|
28
|
+
'var observer = null;',
|
|
29
|
+
'function tryDismiss() {',
|
|
30
|
+
' if (done || !document.body) return;',
|
|
31
|
+
' var dialogs = document.querySelectorAll(\'[role="dialog"]\');',
|
|
32
|
+
' for (var i = 0; i < dialogs.length; i++) {',
|
|
33
|
+
' if (!/预览版说明|Preview Notice/.test(dialogs[i].textContent || "")) continue;',
|
|
34
|
+
' var buttons = dialogs[i].querySelectorAll("button");',
|
|
35
|
+
' for (var j = 0; j < buttons.length; j++) {',
|
|
36
|
+
' var label = (buttons[j].textContent || "").replace(/^\\s+|\\s+$/g, "");',
|
|
37
|
+
' if (label === "继续" || label === "Continue") {',
|
|
38
|
+
' done = true;',
|
|
39
|
+
' buttons[j].click();',
|
|
40
|
+
' if (observer) observer.disconnect();',
|
|
41
|
+
' return;',
|
|
42
|
+
' }',
|
|
43
|
+
' }',
|
|
44
|
+
' break;',
|
|
45
|
+
' }',
|
|
46
|
+
'}',
|
|
47
|
+
'function start() {',
|
|
48
|
+
' observer = new MutationObserver(tryDismiss);',
|
|
49
|
+
' observer.observe(document.body, { childList: true, subtree: true });',
|
|
50
|
+
' tryDismiss();',
|
|
51
|
+
// 兜底停止:说明若 60 秒内没出现(未来版本改版/被官方移除),观察器自动退场
|
|
52
|
+
' setTimeout(function () { if (observer) observer.disconnect(); }, 60000);',
|
|
53
|
+
'}',
|
|
54
|
+
'if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);',
|
|
55
|
+
'else start();',
|
|
56
|
+
'})();',
|
|
57
|
+
].join('\n')
|
|
58
|
+
const WELCOME_ACK_MARKER = 'data-dsh-dock-welcome-ack'
|
|
59
|
+
export { WELCOME_ACK_MARKER }
|
|
60
|
+
|
|
61
|
+
/** 把「预览版说明」自动确认脚本内联注入 index.html 的 <head> 之后(幂等)。 */
|
|
62
|
+
export function injectWelcomeAck(html) {
|
|
63
|
+
if (html.includes(WELCOME_ACK_MARKER)) return html
|
|
64
|
+
const match = /<head(?:\s[^>]*)?>/i.exec(html)
|
|
65
|
+
const script = `<script ${WELCOME_ACK_MARKER}>${WELCOME_ACK_JS}</script>`
|
|
66
|
+
if (!match) return script + html
|
|
67
|
+
const at = match.index + match[0].length
|
|
68
|
+
return html.slice(0, at) + script + html.slice(at)
|
|
69
|
+
}
|