@mrrisega/dsh-remote 0.7.6-beta.1 → 0.7.6-beta.10
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/clients/dsh-remote/dsh-bridge.mjs +678 -50
- package/clients/dsh-remote/e2ee-client.mjs +256 -0
- package/clients/dsh-remote/mobile-adapter.mjs +261 -0
- package/clients/dsh-remote/test/bridge-control-priority.test.mjs +214 -0
- package/clients/dsh-remote/test/bridge-heartbeat-adaptive.test.mjs +110 -0
- package/clients/dsh-remote/test/bridge-upstream-refresh.test.mjs +26 -8
- package/clients/dsh-remote/test/bridge-ws-open-fast-ack.test.mjs +213 -0
- package/clients/dsh-remote/test/e2ee-session-store.test.mjs +251 -0
- package/clients/dsh-remote/test/mobile-adapter-single-page.test.mjs +328 -0
- package/clients/dsh-remote/test/pending-plugin-update.test.mjs +96 -0
- package/dsh-setup.mjs +109 -10
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +257 -13
- package/packages/dsh-remote-web/lib/index.js +172 -2
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +678 -50
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +256 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +261 -0
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +109 -10
- package/packages/dsh-remote-web/test/community-invite-proxy.test.mjs +343 -0
- package/packages/dsh-remote-web/test/community-invite-ui.test.mjs +375 -0
- package/packages/dsh-remote-web/test/desktop-compat.test.mjs +7 -5
- package/packages/dsh-remote-web/test/purchase-agreement-proxy.test.mjs +198 -0
- package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +336 -0
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +4 -2
- package/packages/dsh-remote-web/test/setup-output.test.mjs +17 -8
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +2 -2
package/dsh-setup.mjs
CHANGED
|
@@ -1127,7 +1127,15 @@ const UPSTREAM_FILE = ".dsh-upstream";
|
|
|
1127
1127
|
* 本次安装要**钉住**的上游(null = 不钉,运行时按 env → .dsh-upstream → 探测 取值)。
|
|
1128
1128
|
* 见 desktopAppPort() 的注释:插件装进多个 profile 后,各宿主会互相覆盖 `.dsh-upstream`。
|
|
1129
1129
|
*/
|
|
1130
|
-
|
|
1130
|
+
// 🔴 2026-10-25 事故:以前把安装进程里的 DSH_BRIDGE_UPSTREAM(一个**当时的动态端口**)写进
|
|
1131
|
+
// launchd plist,之后 dsh 每次重启换端口,plist 里的旧端口就成了永久生效的"显式指定",
|
|
1132
|
+
// 守护/桥永远连不上 → 设备掉线。现在默认**不钉**(运行时按 插件写的 .dsh-upstream → 探测 取值);
|
|
1133
|
+
// 仅当部署方显式要求(DSH_BRIDGE_PIN_UPSTREAM=1)时才沿用旧行为。
|
|
1134
|
+
let PINNED_UPSTREAM = (() => {
|
|
1135
|
+
const pinFlag = String(process.env.DSH_BRIDGE_PIN_UPSTREAM || "").trim().toLowerCase();
|
|
1136
|
+
if (pinFlag !== "1" && pinFlag !== "true" && pinFlag !== "on") return null;
|
|
1137
|
+
return String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "") || null;
|
|
1138
|
+
})();
|
|
1131
1139
|
|
|
1132
1140
|
/**
|
|
1133
1141
|
* 探测桌面端(Electron 应用)当前监听的**回环端口**。
|
|
@@ -1223,6 +1231,18 @@ async function upstreamReachable(url = upstreamUrl(), timeoutMs = 1200) {
|
|
|
1223
1231
|
*/
|
|
1224
1232
|
async function isDshWebUp(timeoutMs = 1200) {
|
|
1225
1233
|
if (await upstreamReachable(upstreamUrl(), timeoutMs)) return true;
|
|
1234
|
+
// 🔴 2026-10-25(设备掉线实测):Electron 每次启动可能换端口。端口文件由 dsh web 内的插件
|
|
1235
|
+
// 在每次启动时重写,比进程内的 discoveredUpstream 缓存新 —— 先直接试文件,别被旧缓存挡住。
|
|
1236
|
+
try {
|
|
1237
|
+
const fromFile = String(fs.readFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), "utf8")).trim();
|
|
1238
|
+
if (/^https?:\/\/[^\s]+$/i.test(fromFile)) {
|
|
1239
|
+
const normalized = fromFile.replace(/\/+$/, "");
|
|
1240
|
+
if (normalized !== upstreamUrl() && (await upstreamReachable(normalized, timeoutMs))) {
|
|
1241
|
+
discoveredUpstream = normalized;
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
} catch { /* 没有端口文件:继续走动态发现 */ }
|
|
1226
1246
|
const before = upstreamUrl();
|
|
1227
1247
|
const after = await refreshUpstreamDiscovery(true);
|
|
1228
1248
|
if (after === before) return false;
|
|
@@ -1659,7 +1679,22 @@ function releaseSpawnLock() {
|
|
|
1659
1679
|
}
|
|
1660
1680
|
|
|
1661
1681
|
// ---------- run:前台跑 bridge(带配置 + 自启动 watcher) ----------
|
|
1682
|
+
/** 以插件写入的 .dsh-upstream 为准校正进程内的旧上游环境变量(dsh 换端口后不再连旧端口)。 */
|
|
1683
|
+
function reconcileUpstreamEnvFromFile() {
|
|
1684
|
+
try {
|
|
1685
|
+
const fileUp = String(fs.readFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), "utf8")).trim().replace(/\/+$/, "");
|
|
1686
|
+
const envUp = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "");
|
|
1687
|
+
if (envUp && /^https?:\/\/[^\s]+$/i.test(fileUp) && fileUp !== envUp) {
|
|
1688
|
+
console.log(`[dsh-remote] 忽略旧的上游地址 ${envUp} → 使用插件写入的最新地址 ${fileUp}(防端口漂移掉线)`);
|
|
1689
|
+
process.env.DSH_BRIDGE_UPSTREAM = fileUp;
|
|
1690
|
+
discoveredUpstream = fileUp;
|
|
1691
|
+
}
|
|
1692
|
+
} catch { /* 无文件:保持原行为 */ }
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1662
1695
|
async function runBridge() {
|
|
1696
|
+
// 启动/每次拉起前都按插件写入的端口校正继承来的旧 env(旧 plist 可能钉着安装时的端口)
|
|
1697
|
+
reconcileUpstreamEnvFromFile();
|
|
1663
1698
|
// 去重(所有平台):自启动服务、登录任务、插件半的脱离进程兜底都可能拉起 watcher,
|
|
1664
1699
|
// 两个 bridge 会抢同一个设备登记 → 以 pid 文件为准。
|
|
1665
1700
|
//
|
|
@@ -1856,9 +1891,13 @@ async function runBridge() {
|
|
|
1856
1891
|
}
|
|
1857
1892
|
warnedDuplicateBridge = false;
|
|
1858
1893
|
|
|
1894
|
+
reconcileUpstreamEnvFromFile(); // dsh 可能刚重启并换了端口:拉起前再以最新端口文件为准
|
|
1859
1895
|
const alive = await checkUpstream();
|
|
1860
1896
|
if (alive && childStopped(bridgeProc)) {
|
|
1861
1897
|
starting = true;
|
|
1898
|
+
// dsh 重启可能换端口(Electron 每次启动端口可能变):拉起前**强制重新发现**,
|
|
1899
|
+
// 否则 discoveredUpstream 缓存旧端口 → bridge 连不上上游 → 设备掉线(2026-10-25 实测)。
|
|
1900
|
+
await refreshUpstreamDiscovery(true);
|
|
1862
1901
|
console.log("[dsh-remote] dsh web 在线,启动 bridge...");
|
|
1863
1902
|
// 清除代理环境变量(bridge 需直连 relay,不受本机代理影响)
|
|
1864
1903
|
const childEnv = {
|
|
@@ -1900,6 +1939,17 @@ async function runBridge() {
|
|
|
1900
1939
|
// 另一个 bridge 把本连接顶掉)后会以"重复实例"退出码优雅退出(见 dsh-bridge.mjs)。
|
|
1901
1940
|
// 这种退出**绝不能**照常 10s 重拉:本机已经有另一个实例在跑,重拉只会继续乒乓互踢。
|
|
1902
1941
|
if (code === DUPLICATE_INSTANCE_EXIT_CODE) {
|
|
1942
|
+
// 🔴 2026-10-25:升级/重启后常见「上一代 bridge 变成孤儿继续占着单实例锁」,
|
|
1943
|
+
// 新 bridge 因此退出 code=4。旧逻辑一律暂停 5 分钟 → 设备在线要等 5 分钟(用户实测掉线)。
|
|
1944
|
+
// 现在先**清扫同运行时的孤儿 bridge**;扫到了说明冲突已解除,3 秒后立即重试;
|
|
1945
|
+
// 扫不到(多半真有另一个 watcher 的活实例)才按 5 分钟退避,避免乒乓互踢。
|
|
1946
|
+
let swept = [];
|
|
1947
|
+
try { swept = sweepStrayBridges(); } catch { /* 清扫失败不致命 */ }
|
|
1948
|
+
if (swept.length > 0) {
|
|
1949
|
+
duplicateBridgeBackoffUntil = Date.now() + 3000;
|
|
1950
|
+
console.log(`[dsh-remote] bridge 因同实例冲突退出(code=${code}),已清扫 ${swept.length} 个孤儿 bridge(pid ${swept.join(", ")})→ 3 秒后重试。`);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1903
1953
|
duplicateBridgeBackoffUntil = Date.now() + DUPLICATE_BRIDGE_BACKOFF_MS;
|
|
1904
1954
|
console.log(`[dsh-remote] bridge 检测到同 deviceId 的另一个实例并退出(code=${code})——本守护暂停 ${Math.round(DUPLICATE_BRIDGE_BACKOFF_MS / 60000)} 分钟后再试,避免两个实例反复互踢。`);
|
|
1905
1955
|
return;
|
|
@@ -1913,6 +1963,8 @@ async function runBridge() {
|
|
|
1913
1963
|
setTimeout(() => { starting = false; }, 5000);
|
|
1914
1964
|
} else if (!alive && bridgeProc && bridgeProc.exitCode === null) {
|
|
1915
1965
|
console.log("[dsh-remote] dsh web 离线,停止 bridge...");
|
|
1966
|
+
// dsh 重启后端口可能变化:清掉进程内缓存,下一轮优先用插件新写的端口文件探测
|
|
1967
|
+
discoveredUpstream = "";
|
|
1916
1968
|
await stopChildGracefully(bridgeProc); // 同上:Windows 上 kill() 跑不到退出钩子
|
|
1917
1969
|
}
|
|
1918
1970
|
};
|
|
@@ -2024,9 +2076,11 @@ async function setup(argv) {
|
|
|
2024
2076
|
if (!PINNED_UPSTREAM && pluginTargets.some((t) => t.name === "desktop")) {
|
|
2025
2077
|
const p = desktopAppPort();
|
|
2026
2078
|
if (p) {
|
|
2027
|
-
PINNED_UPSTREAM
|
|
2028
|
-
|
|
2029
|
-
|
|
2079
|
+
// 🔴 2026-10-25:这里只能写运行时端口文件;**不能**回填 PINNED_UPSTREAM ——
|
|
2080
|
+
// 它会被写进 launchd plist 变成永久显式指定,dsh 换端口后守护/桥永远连旧端口
|
|
2081
|
+
// (用户实测 upstream_unreachable)。
|
|
2082
|
+
try { fs.writeFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), `http://127.0.0.1:${p}` + "\n"); } catch { /* 忽略 */ }
|
|
2083
|
+
console.log(`ℹ 桌面端在 127.0.0.1:${p} 监听 → 已写入运行时上游端口(不再钉进自启动配置)`);
|
|
2030
2084
|
}
|
|
2031
2085
|
}
|
|
2032
2086
|
} catch { /* 探测失败不阻断安装 */ }
|
|
@@ -2583,6 +2637,45 @@ function missingPluginFiles(dir) {
|
|
|
2583
2637
|
*
|
|
2584
2638
|
* 另外清掉上次被强杀留下的 `.staging-*` / `.backup-*`,不留垃圾。
|
|
2585
2639
|
*/
|
|
2640
|
+
// ── 桌面端「暫存安装」:应用运行中不能改写它的 profile(会把它弄退出/重启),
|
|
2641
|
+
// 但用户点「一键更新」必须能装到桌面端。做法:先整份拷到 ~/.dsh-remote/pending-plugin/ 并写 marker,
|
|
2642
|
+
// 由常驻的 bridge 在检测到应用退出后自动原子替换 profile 内的插件目录(见 dsh-bridge.mjs)。
|
|
2643
|
+
const PENDING_PLUGIN_MARKER = path.join(CONFIG_DIR, ".plugin-update-pending.json");
|
|
2644
|
+
const PENDING_PLUGIN_DIR = path.join(CONFIG_DIR, "pending-plugin");
|
|
2645
|
+
|
|
2646
|
+
/** 暂存一份插件更新(供 bridge 在应用退出后安装)。返回 marker 对象。 */
|
|
2647
|
+
function stagePluginUpdateForProfile(profileDir, pluginDir) {
|
|
2648
|
+
fs.rmSync(PENDING_PLUGIN_DIR, { recursive: true, force: true });
|
|
2649
|
+
fs.mkdirSync(PENDING_PLUGIN_DIR, { recursive: true });
|
|
2650
|
+
const cpOpts = {
|
|
2651
|
+
recursive: true,
|
|
2652
|
+
filter: (src) => {
|
|
2653
|
+
if (src === pluginDir) return true;
|
|
2654
|
+
const rel = String(path.relative(pluginDir, src));
|
|
2655
|
+
const segs = rel.split(path.sep);
|
|
2656
|
+
if (segs.some((seg) => seg === ".git" || seg === ".DS_Store")) return false;
|
|
2657
|
+
const inBundledRuntime = segs[0] === "runtime";
|
|
2658
|
+
if (!inBundledRuntime && segs.some((seg) => seg === "node_modules")) return false;
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
fs.cpSync(pluginDir, PENDING_PLUGIN_DIR, cpOpts);
|
|
2663
|
+
for (const rel of PLUGIN_REQUIRED_FILES) {
|
|
2664
|
+
const f = path.join(PENDING_PLUGIN_DIR, rel);
|
|
2665
|
+
if (!fs.existsSync(f)) throw new Error(`暂存校验失败:缺少 ${rel}`);
|
|
2666
|
+
}
|
|
2667
|
+
const version = JSON.parse(fs.readFileSync(path.join(pluginDir, "package.json"), "utf8")).version;
|
|
2668
|
+
const marker = {
|
|
2669
|
+
version,
|
|
2670
|
+
profileDir,
|
|
2671
|
+
pluginDirName: PLUGIN_LOCAL_DIR,
|
|
2672
|
+
stageDir: PENDING_PLUGIN_DIR,
|
|
2673
|
+
stagedAt: Date.now()
|
|
2674
|
+
};
|
|
2675
|
+
fs.writeFileSync(PENDING_PLUGIN_MARKER, JSON.stringify(marker, null, 2) + "\n");
|
|
2676
|
+
return marker;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2586
2679
|
function copyPluginIntoProfile(profileDir, pluginDir) {
|
|
2587
2680
|
// 清理历史名残留目录(≤0.4.9 的 dsh-remote-ui-plugin),避免与新版并存
|
|
2588
2681
|
for (const id of PLUGIN_LEGACY_IDS) {
|
|
@@ -3031,16 +3124,21 @@ async function pluginCmd(argv) {
|
|
|
3031
3124
|
let last = null;
|
|
3032
3125
|
let ok = 0;
|
|
3033
3126
|
let skippedRunning = 0;
|
|
3127
|
+
let stagedRunning = 0;
|
|
3034
3128
|
const force = hasFlag(argv, "--force");
|
|
3035
3129
|
for (const t of targets) {
|
|
3036
3130
|
// ⚠️ 桌面应用在运行时**绝不改写它的 profile**:应用会因此退出/重启(2026-10-11 实测故障)
|
|
3037
3131
|
if (t.name === "desktop" && !force && dshDesktopAppRunning()) {
|
|
3038
|
-
console.log(`⚠️
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3132
|
+
console.log(`⚠️ DeepSeek Harness 应用正在运行 → 不直接改写它的 profile(会把它弄退出/重启)。`);
|
|
3133
|
+
try {
|
|
3134
|
+
const staged = stagePluginUpdateForProfile(t.dir, path.join(THIS_DIR, "packages", PLUGIN_ID));
|
|
3135
|
+
stagedRunning += 1;
|
|
3136
|
+
console.log(` ✅ 新版本 ${staged.version} 已暂存:**退出 DeepSeek Harness 后会自动安装**,重新打开应用即生效(无需终端)。`);
|
|
3137
|
+
} catch (e) {
|
|
3138
|
+
console.log(` ⚠️ 暂存失败:${e && e.message ? e.message : e}`);
|
|
3139
|
+
console.log(` 请完全退出应用后重跑本命令,或在应用内「插件」页安装/更新 dsh-remote-web。`);
|
|
3140
|
+
skippedRunning += 1;
|
|
3141
|
+
}
|
|
3044
3142
|
continue;
|
|
3045
3143
|
}
|
|
3046
3144
|
if (multi) console.log(`\n── profile: ${t.name}(${t.dir})`);
|
|
@@ -3050,6 +3148,7 @@ async function pluginCmd(argv) {
|
|
|
3050
3148
|
if (r !== undefined && r !== null) last = r;
|
|
3051
3149
|
}
|
|
3052
3150
|
if (ok === 0 && skippedRunning === 0) process.exit(1);
|
|
3151
|
+
if (stagedRunning > 0) console.log(`ℹ 桌面端插件已暂存(${stagedRunning} 个):退出 DeepSeek Harness 后由后台服务自动安装,重开应用生效。`);
|
|
3053
3152
|
if (skippedRunning > 0) console.log(`ℹ 本次跳过了 ${skippedRunning} 个 profile(应用在运行);桥与运行环境不受影响。`);
|
|
3054
3153
|
return last;
|
|
3055
3154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.7.6-beta.
|
|
3
|
+
"version": "0.7.6-beta.10",
|
|
4
4
|
"description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -98,7 +98,7 @@ window.__ModuleLoader__.load({
|
|
|
98
98
|
// 绝不写 :root / body —— 否则会污染整个 GUI。JS 解析出主题后往根节点写
|
|
99
99
|
// data-dru-theme="dark|light",下面按属性切换;末一条 `:not([data-dru-theme])`
|
|
100
100
|
// 是 JS 尚未执行时的纯 CSS 兜底(一旦 JS 写过属性就不再参与,不会打架)。
|
|
101
|
-
".dru-settings-section,.dru-popup,.dru-nav-remote{",
|
|
101
|
+
".dru-settings-section,.dru-popup,.dru-nav-remote,.dru-standalone-card{",
|
|
102
102
|
" color-scheme:light;",
|
|
103
103
|
" --dru-surface:#f6f8fa;", // 卡片底
|
|
104
104
|
" --dru-surface-2:#ffffff;", // 抬升面(输入框/行/弹窗卡)
|
|
@@ -149,10 +149,11 @@ window.__ModuleLoader__.load({
|
|
|
149
149
|
// 深色:JS 写的 data-dru-theme 优先;宿主属性作 JS 未执行时的兜底。
|
|
150
150
|
// 取值跟着宿主的深色基色走(body 实测 #151517 / 层 1 #232324 / 层 2 #2c2c2e / 层 3 #353638),
|
|
151
151
|
// 语义色**在深底上重新取值**,不是沿用亮色值(亮色的 #1a7f37 / #cf222e 在深底对比度不足)。
|
|
152
|
-
".dru-settings-section[data-dru-theme=\"dark\"],.dru-popup[data-dru-theme=\"dark\"],.dru-nav-remote[data-dru-theme=\"dark\"],",
|
|
152
|
+
".dru-settings-section[data-dru-theme=\"dark\"],.dru-popup[data-dru-theme=\"dark\"],.dru-nav-remote[data-dru-theme=\"dark\"],.dru-standalone-card[data-dru-theme=\"dark\"],",
|
|
153
153
|
"body[data-ds-dark-theme] .dru-settings-section:not([data-dru-theme]),",
|
|
154
154
|
"body[data-ds-dark-theme] .dru-popup:not([data-dru-theme]),",
|
|
155
|
-
"body[data-ds-dark-theme] .dru-nav-remote:not([data-dru-theme])
|
|
155
|
+
"body[data-ds-dark-theme] .dru-nav-remote:not([data-dru-theme]),",
|
|
156
|
+
"body[data-ds-dark-theme] .dru-standalone-card:not([data-dru-theme]){",
|
|
156
157
|
" color-scheme:dark;",
|
|
157
158
|
" --dru-surface:#232324;",
|
|
158
159
|
" --dru-surface-2:#2c2c2e;",
|
|
@@ -202,6 +203,15 @@ window.__ModuleLoader__.load({
|
|
|
202
203
|
"}",
|
|
203
204
|
// 设置页栏目容器(nav 选中后渲染在 settings.section 内容区)
|
|
204
205
|
".dru-settings-section{max-width:720px;display:flex;flex-direction:column;gap:14px;padding-top:2px;color:var(--dru-fg)}",
|
|
206
|
+
// 📱 独立「远程访问」弹层(2026-10-25):入口点击不再去"猜"官方设置面板的 DOM,
|
|
207
|
+
// 直接弹我们自己的一份设置页 —— 宿主改版也影响不到它。
|
|
208
|
+
".dru-standalone-overlay{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dru-overlay)}",
|
|
209
|
+
".dru-standalone-card{width:min(760px,100%);max-height:88vh;overflow:auto;box-sizing:border-box;padding:18px 20px 22px;border:1px solid var(--dru-border);border-radius:14px;background:var(--dru-surface-2);color:var(--dru-fg);box-shadow:0 18px 60px var(--dru-shadow)}",
|
|
210
|
+
".dru-standalone-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}",
|
|
211
|
+
".dru-standalone-title{font-size:16px;font-weight:700}",
|
|
212
|
+
".dru-standalone-close{appearance:none;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dru-fg-muted);width:30px;height:30px;border-radius:8px;font-size:18px;line-height:1;cursor:pointer}",
|
|
213
|
+
".dru-standalone-close:hover{background:var(--dru-hover);color:var(--dru-fg)}",
|
|
214
|
+
"@media (max-width:640px){.dru-standalone-overlay{padding:8px}.dru-standalone-card{max-height:94vh;border-radius:12px;padding:14px 14px 18px}}",
|
|
205
215
|
".dru-settings-head{display:flex;align-items:center;gap:10px;padding:6px 2px 2px}",
|
|
206
216
|
".dru-settings-icon{font-size:24px;line-height:1;flex:none}",
|
|
207
217
|
// 标题/副标题优先沿用宿主语义令牌(自带深浅两套),取不到再退回本插件令牌
|
|
@@ -897,7 +907,10 @@ window.__ModuleLoader__.load({
|
|
|
897
907
|
for (var i = 0; i < els.length; i++) {
|
|
898
908
|
var el = els[i];
|
|
899
909
|
if (!el || el.id === NAV_ENTRY_ID) continue; // 排除注入按钮自身(防递归)
|
|
900
|
-
|
|
910
|
+
// 2026-10-25:新宿主把设置面板左栏的 navCell class 换掉了 → 只认 navCellLike 会漏掉
|
|
911
|
+
// 「远程访问」栏目,导致入口点击静默失败(用户实测:只能从更多→设置里手动点)。
|
|
912
|
+
// 在**设置弹层内**时放宽为「可见、文案精确匹配」,不再依赖 class 名。
|
|
913
|
+
if (!navCellLike(el) && !(insideDialog === true && navInDialog(el))) continue;
|
|
901
914
|
if (NAV_BAD_RE.test(String(el.className || ""))) continue;
|
|
902
915
|
var inDlg = navInDialog(el);
|
|
903
916
|
if (insideDialog === true && !inDlg) continue;
|
|
@@ -928,6 +941,9 @@ window.__ModuleLoader__.load({
|
|
|
928
941
|
if (!el.getBoundingClientRect || el.getBoundingClientRect().width === 0) continue;
|
|
929
942
|
var t = String(el.textContent || "");
|
|
930
943
|
if (t.indexOf("通用设置") !== -1 || t.indexOf("General") !== -1) return true;
|
|
944
|
+
// 2026-10-25:新版桌面端设置面板里是「远程访问 / 通用设置…」;若宿主把「通用设置」
|
|
945
|
+
// 改成了别的文案,只看它就会误判「设置没打开」→ 入口点击后一路走兜底/超时。
|
|
946
|
+
if (t.indexOf("远程访问") !== -1) return true;
|
|
931
947
|
}
|
|
932
948
|
} catch (e) { /* 忽略 */ }
|
|
933
949
|
return false;
|
|
@@ -957,6 +973,15 @@ window.__ModuleLoader__.load({
|
|
|
957
973
|
if (!el || el.id === NAV_ENTRY_ID) continue;
|
|
958
974
|
var aria = String((el.getAttribute && el.getAttribute("aria-label")) || "");
|
|
959
975
|
if (/账号菜单|Account menu/i.test(aria)) return el;
|
|
976
|
+
// 2026-10-25(用户反馈「只能从更多→设置里打开」):新宿主把设置收在**更多**菜单里,
|
|
977
|
+
// 这里补充「更多」形态的候选(aria-label / title / 文本 / ⋯ 字形)。
|
|
978
|
+
var title = String((el.getAttribute && (el.getAttribute("title") || "")) || "");
|
|
979
|
+
var txt = String((el.textContent || "")).replace(/\s+/g, "");
|
|
980
|
+
if (/更多|More|菜单|Menu/i.test(aria + " " + title)) {
|
|
981
|
+
var rMore = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
982
|
+
if (!rMore || (rMore.width > 0 && rMore.height > 0)) return el;
|
|
983
|
+
}
|
|
984
|
+
if (/^(更多|More|⋯|\.\.\.)$/i.test(txt) && el.getAttribute && el.getAttribute("aria-haspopup")) return el;
|
|
960
985
|
var r = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
961
986
|
if (!best && r && r.width > 0 && r.left < 360 && r.top > 300
|
|
962
987
|
&& String((el.getAttribute && el.getAttribute("aria-haspopup")) || "") === "menu") best = el;
|
|
@@ -1102,6 +1127,61 @@ window.__ModuleLoader__.load({
|
|
|
1102
1127
|
0.2 实测该槽就在侧栏底部、且**当前是空的**)—— 位置、宽度、折叠态全由宿主负责。
|
|
1103
1128
|
所以这里不再注入任何 DOM,直接注册进槽位。id 仍保留 `dru-nav-remote`:
|
|
1104
1129
|
公告徽标 / 首次红点那套按 id 取节点的既有逻辑因此不用改。 */
|
|
1130
|
+
/* 📱 独立「远程访问」弹层(2026-10-25 用户拍板):
|
|
1131
|
+
桌面端点击侧栏入口时,不再去"点官方设置/找栏目"(宿主一改版就点错或没反应,
|
|
1132
|
+
实测会点到左上角「更多→浏览插件」),而是弹出我们自己的一份设置页。
|
|
1133
|
+
「设置 → 远程访问」栏目保留,正常从官方设置页进入的用户仍走那条路。
|
|
1134
|
+
实现上用 shell.overlay 槽(宿主最上层,不受侧栏容器裁剪),组件常驻、按事件开关。 */
|
|
1135
|
+
var standaloneOverlayMounted = false;
|
|
1136
|
+
function openRemoteStandalonePanel() {
|
|
1137
|
+
try { window.dispatchEvent(new CustomEvent("dsh-remote:open-standalone")); } catch (e) { /* 忽略 */ }
|
|
1138
|
+
}
|
|
1139
|
+
function RemoteStandaloneOverlay() {
|
|
1140
|
+
var st = useState(false);
|
|
1141
|
+
var open = st[0];
|
|
1142
|
+
var setOpen = st[1];
|
|
1143
|
+
useEffect(function () {
|
|
1144
|
+
standaloneOverlayMounted = true;
|
|
1145
|
+
function onOpen() { try { setOpen(true); } catch (e) { /* 忽略 */ } }
|
|
1146
|
+
function onKey(e) {
|
|
1147
|
+
if (!e) return;
|
|
1148
|
+
if (e.key === "Escape" || e.keyCode === 27) { try { setOpen(false); } catch (e2) { /* 忽略 */ } }
|
|
1149
|
+
}
|
|
1150
|
+
try {
|
|
1151
|
+
window.addEventListener("dsh-remote:open-standalone", onOpen);
|
|
1152
|
+
window.addEventListener("keydown", onKey);
|
|
1153
|
+
} catch (e3) { /* 忽略 */ }
|
|
1154
|
+
return function () {
|
|
1155
|
+
standaloneOverlayMounted = false;
|
|
1156
|
+
try {
|
|
1157
|
+
window.removeEventListener("dsh-remote:open-standalone", onOpen);
|
|
1158
|
+
window.removeEventListener("keydown", onKey);
|
|
1159
|
+
} catch (e4) { /* 忽略 */ }
|
|
1160
|
+
};
|
|
1161
|
+
}, []);
|
|
1162
|
+
if (!open) return null;
|
|
1163
|
+
var dark = false;
|
|
1164
|
+
try { dark = !!document.body && document.body.hasAttribute("data-ds-dark-theme"); } catch (e5) { /* 忽略 */ }
|
|
1165
|
+
return h("div", {
|
|
1166
|
+
className: "dru-standalone-overlay",
|
|
1167
|
+
role: "dialog",
|
|
1168
|
+
"aria-modal": "true",
|
|
1169
|
+
"aria-label": "远程访问",
|
|
1170
|
+
onClick: function (e) { if (e && e.target === e.currentTarget) setOpen(false); }
|
|
1171
|
+
},
|
|
1172
|
+
h("div", { className: "dru-standalone-card", "data-dru-theme": dark ? "dark" : "light" },
|
|
1173
|
+
h("div", { className: "dru-standalone-head" },
|
|
1174
|
+
h("b", { className: "dru-standalone-title" }, "📱 远程访问"),
|
|
1175
|
+
h("button", {
|
|
1176
|
+
type: "button",
|
|
1177
|
+
className: "dru-standalone-close",
|
|
1178
|
+
"aria-label": "关闭",
|
|
1179
|
+
onClick: function () { setOpen(false); }
|
|
1180
|
+
}, "×")),
|
|
1181
|
+
h(RemoteControlSection, { standalone: true })
|
|
1182
|
+
));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1105
1185
|
function RemoteNavEntry(props) {
|
|
1106
1186
|
var wide = !(props && props.wide === false);
|
|
1107
1187
|
var seenArr = useState(false);
|
|
@@ -1118,7 +1198,16 @@ window.__ModuleLoader__.load({
|
|
|
1118
1198
|
try { e.preventDefault(); } catch (err) { /* 忽略 */ }
|
|
1119
1199
|
setClicked(true);
|
|
1120
1200
|
dotMarkSeen(); // 用过入口 = 已了解位置 → 收起首次红点
|
|
1121
|
-
|
|
1201
|
+
// 2026-10-25(用户拍板):优先弹**我们自己的独立设置页**(不再依赖官方设置面板 DOM);
|
|
1202
|
+
// 只有宿主没有 shell.overlay 槽(极老版本)时才退回旧的"打开官方设置栏目"链路。
|
|
1203
|
+
try {
|
|
1204
|
+
openRemoteStandalonePanel();
|
|
1205
|
+
setTimeout(function () {
|
|
1206
|
+
if (!standaloneOverlayMounted) {
|
|
1207
|
+
try { openRemoteSettings(); } catch (e2) { /* 忽略 */ }
|
|
1208
|
+
}
|
|
1209
|
+
}, 150);
|
|
1210
|
+
} catch (errOpen) { /* 绝不因打开失败抛到宿主 */ }
|
|
1122
1211
|
}
|
|
1123
1212
|
}, [
|
|
1124
1213
|
h("span", { className: "ico", key: "ico" }, "📱"),
|
|
@@ -1177,6 +1266,7 @@ window.__ModuleLoader__.load({
|
|
|
1177
1266
|
* 之所以是模块级的:拉取发生在 apply() 里(组件还没挂载),而消费方有两个
|
|
1178
1267
|
* (设置面板的卡片 / shell.overlay 上的弹窗),模块级 store 才能让两边看到同一份数据。
|
|
1179
1268
|
*/
|
|
1269
|
+
var annRetryCount = 0; // 公告首次拉取瞬态失败的有界重试计数(成功即清零)
|
|
1180
1270
|
var annState = {
|
|
1181
1271
|
ok: false, // 拉取成功且结构是我们认识的那一种(老服务端 404 / 结构不符 → 一直是 false)
|
|
1182
1272
|
bubble: false, // bubble_enabled === true 才置真(字段缺失一律当 false = fail closed)
|
|
@@ -1339,6 +1429,7 @@ window.__ModuleLoader__.load({
|
|
|
1339
1429
|
var norm = annNormalize(b);
|
|
1340
1430
|
if (!norm) { annState.ok = false; annState.bubble = false; annState.items = []; annState.unread = 0; annEmit(); return false; }
|
|
1341
1431
|
annState.ok = true;
|
|
1432
|
+
annRetryCount = 0;
|
|
1342
1433
|
annState.bubble = norm.bubble;
|
|
1343
1434
|
annState.items = norm.items;
|
|
1344
1435
|
annState.unread = norm.unread;
|
|
@@ -1347,13 +1438,24 @@ window.__ModuleLoader__.load({
|
|
|
1347
1438
|
}
|
|
1348
1439
|
annEmit();
|
|
1349
1440
|
return true;
|
|
1350
|
-
}).catch(function () {
|
|
1441
|
+
}).catch(function (err) {
|
|
1351
1442
|
// 404(老服务端)/ 不可达 / 超时:fail closed —— 不显示入口、不弹窗,**绝不抛**
|
|
1352
1443
|
annState.ok = false;
|
|
1353
1444
|
annState.bubble = false;
|
|
1354
1445
|
annState.items = [];
|
|
1355
1446
|
annState.unread = 0;
|
|
1356
1447
|
annEmit();
|
|
1448
|
+
// 2026-10-25(用户反馈「公告板块没了」):dsh 重启后页面常常比插件宿主路由先就绪,
|
|
1449
|
+
// 首次拉取失败一次,整页(sessionStorage 一次性标记)就再也没有公告入口。
|
|
1450
|
+
// 这里只对**瞬态失败**做有界重试(最多 2 次、间隔 1.5s/3s);404 明确是老服务端,不重试。
|
|
1451
|
+
// 注意:这只发生在**失败后**,成功路径仍然严格"每次页面加载只拉一次"。
|
|
1452
|
+
var st = Number(err && (err.status || err.statusCode));
|
|
1453
|
+
if (st !== 404 && annRetryCount < 2) {
|
|
1454
|
+
annRetryCount += 1;
|
|
1455
|
+
try {
|
|
1456
|
+
setTimeout(function () { annFetch({ openPopup: false }); }, 1500 * annRetryCount);
|
|
1457
|
+
} catch (e2) { /* 忽略 */ }
|
|
1458
|
+
}
|
|
1357
1459
|
return false;
|
|
1358
1460
|
});
|
|
1359
1461
|
}
|
|
@@ -2739,6 +2841,70 @@ window.__ModuleLoader__.load({
|
|
|
2739
2841
|
return { kind: "off", text: text };
|
|
2740
2842
|
}
|
|
2741
2843
|
|
|
2844
|
+
// ══ 🎉 加入用户交流群引导(实际使用满 2 小时后弹一次;2026-10-23 业主需求)═════
|
|
2845
|
+
//
|
|
2846
|
+
// 与手机 Web 端共用同一套企业端接口;桌面端浏览器半没有企业端用户 token 直连,
|
|
2847
|
+
// 由 node 半 `/dsh-remote/community-invite/*` 用 device-login token 代调(见 lib/index.js 同名注释)。
|
|
2848
|
+
// GET /dsh-remote/community-invite/status → { ok, show, reason, used_seconds, required_seconds, qr_url }
|
|
2849
|
+
// POST /dsh-remote/community-invite/shown → { ok:true }(幂等)
|
|
2850
|
+
//
|
|
2851
|
+
// 纪律(与手机端保持同一套,别改松):
|
|
2852
|
+
// ① **弹不弹完全由服务端 show 决定**:时长口径(relay_sessions 累计 ≥ required_seconds)只在服务端算,
|
|
2853
|
+
// 前端一个字节都不自己算;show 非严格 true 一律不弹(fail closed)。
|
|
2854
|
+
// ② **每桌面会话一次**:sessionStorage + 内存双标记;服务端 already_shown(show:false)是第二道保险。
|
|
2855
|
+
// ③ **「看过即算」**:弹层一显示就 POST shown(幂等)——用户直接关页面/不点按钮也不会被反复骚扰;
|
|
2856
|
+
// 两个按钮关闭时**再调一次**同样幂等。
|
|
2857
|
+
// ④ **一切失败静默**:不弹、不报错、不阻塞面板主流程(连日志都不往用户面前抛)。
|
|
2858
|
+
// ⑤ 二维码降级:qr_url 为空 / 图片加载失败 → 隐藏图片但**保留文案与按钮**(绝不弹空白二维码区)。
|
|
2859
|
+
var CI_SEEN_KEY = "dsh-remote-community-invite-seen-v1";
|
|
2860
|
+
var ciSeenInMemory = false; // sessionStorage 不可用(隐私模式)时的兜底:每桌面会话仍只弹一次
|
|
2861
|
+
var ciReported = false; // 已上报 shown 的本地标记(幂等;失败允许关闭时再补一次)
|
|
2862
|
+
/** 本桌面会话内是否已弹过(内存 + sessionStorage 双标记)。 */
|
|
2863
|
+
function ciSeen() {
|
|
2864
|
+
if (ciSeenInMemory) return true;
|
|
2865
|
+
try { return sessionStorage.getItem(CI_SEEN_KEY) === "1"; } catch (e) { return false; }
|
|
2866
|
+
}
|
|
2867
|
+
function ciMarkSeen() {
|
|
2868
|
+
ciSeenInMemory = true;
|
|
2869
|
+
try { sessionStorage.setItem(CI_SEEN_KEY, "1"); } catch (e) { /* 隐私模式忽略 */ }
|
|
2870
|
+
}
|
|
2871
|
+
/** 二维码 URL 安全化:只接受 http(s) 绝对地址或站内绝对路径;data:/javascript: 一律不加载。 */
|
|
2872
|
+
function ciSafeQrUrl(raw) {
|
|
2873
|
+
var s = String(raw || "").trim();
|
|
2874
|
+
if (!s) return "";
|
|
2875
|
+
if (/^https?:\/\//i.test(s)) return s;
|
|
2876
|
+
if (s.charAt(0) === "/" && s.charAt(1) !== "/") {
|
|
2877
|
+
var origin = (typeof location !== "undefined" && location && location.origin) || "";
|
|
2878
|
+
return origin + s;
|
|
2879
|
+
}
|
|
2880
|
+
return "";
|
|
2881
|
+
}
|
|
2882
|
+
/** POST /dsh-remote/community-invite/shown(幂等)。失败静默,并允许关闭时再补一次。 */
|
|
2883
|
+
function ciReportShown() {
|
|
2884
|
+
if (ciReported) return;
|
|
2885
|
+
ciReported = true;
|
|
2886
|
+
api("/dsh-remote/community-invite/shown", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" })
|
|
2887
|
+
.then(function (b) { if (!b || b.ok === false) ciReported = false; })
|
|
2888
|
+
.catch(function () { ciReported = false; /* 网络失败:关闭时再试一次即可 */ });
|
|
2889
|
+
}
|
|
2890
|
+
/**
|
|
2891
|
+
* 问服务端要不要弹。`alive` 由 effect 的 cleanup 控制:面板卸载后回来的响应直接丢弃。
|
|
2892
|
+
* @returns {Promise<{qrUrl:string}|null>} 需要弹 → 返回弹层数据;不弹/失败 → null
|
|
2893
|
+
*/
|
|
2894
|
+
function ciMaybeShow() {
|
|
2895
|
+
if (ciSeen()) return Promise.resolve(null);
|
|
2896
|
+
return api("/dsh-remote/community-invite/status")
|
|
2897
|
+
.then(function (b) {
|
|
2898
|
+
if (!b || b.ok === false || b.show !== true) return null; // 服务端说不弹就不弹
|
|
2899
|
+
return { qrUrl: ciSafeQrUrl(b.qr_url) };
|
|
2900
|
+
})
|
|
2901
|
+
.catch(function () { return null; }); // 静默:接口/网络/字段异常一律不弹
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// 【2026-10-24 移除】业主决定:面板不再展示购买协议(协议只在**手机端支付扫码前**展示)。
|
|
2905
|
+
// 相关常量 / 渲染函数 / 样式已一并删除;node 半 `GET /dsh-remote/purchase-agreement` 代理保留
|
|
2906
|
+
// (手机端 / 其它端后续可复用,见 lib/index.js 同名段注释)。
|
|
2907
|
+
|
|
2742
2908
|
// ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
|
|
2743
2909
|
// 匿名装机统计的「面板打开」只上报一次/每页(node 半还会每进程去重)。
|
|
2744
2910
|
var panelOpenedSent = false;
|
|
@@ -2886,6 +3052,8 @@ window.__ModuleLoader__.load({
|
|
|
2886
3052
|
: "已停止";
|
|
2887
3053
|
// 是否需要重启 DeepSeek harness(首次安装/在线更新后置顶提醒 + 底部常驻按钮)
|
|
2888
3054
|
var restartInfo = (st && st.restart) || {};
|
|
3055
|
+
// 桌面端「暫存插件更新」:新版本已下载,退出应用后由 bridge 自动安装(见 dsh-bridge 的 applier)
|
|
3056
|
+
var desktopUpdatePending = (st && st.desktopUpdatePending) || null;
|
|
2889
3057
|
var restartPending = !!restartInfo.pending;
|
|
2890
3058
|
// 宿主形态(node 半下发):桌面端(Electron)Host 由桌面应用托管,重启这个动作
|
|
2891
3059
|
// 在那边**做不到也不该做**(强行退出会弹原生致命错误)。面板据此:
|
|
@@ -5065,6 +5233,23 @@ window.__ModuleLoader__.load({
|
|
|
5065
5233
|
// annVer 只用于让 useSyncExternalStore 的重渲染生效,本身不参与任何计算 —— 显式丢弃,免得被读成漏用。
|
|
5066
5234
|
void annVer;
|
|
5067
5235
|
|
|
5236
|
+
// ── 🎉 交流群引导:hook 追加在**最后一个既有 hook 之后**(同上"hook 序号铁律")──────
|
|
5237
|
+
// 触发时机 = 「面板加载完成 + 已登录 SaaS 账号」:st 由 refresh() 拉回、loggedIn 由它派生;
|
|
5238
|
+
// 这里不写定时器、不抢首屏(数据回调后再 setState 渲染弹层)。本地模式(自建)不弹:
|
|
5239
|
+
// node 半对这种模式直接回 ok:false/reason:local_mode(没有企业端账号体系)。
|
|
5240
|
+
var ciArr = useState(null); var ciInvite = ciArr[0]; var setCiInvite = ciArr[1]; // {qrUrl}|null
|
|
5241
|
+
useEffect(function () {
|
|
5242
|
+
if (!loggedIn || mode !== "saas") return undefined;
|
|
5243
|
+
var alive = true;
|
|
5244
|
+
ciMaybeShow().then(function (data) {
|
|
5245
|
+
if (!alive || !data) return;
|
|
5246
|
+
ciMarkSeen(); // 先标记再展示:同一次桌面会话绝不重复弹
|
|
5247
|
+
setCiInvite(data);
|
|
5248
|
+
ciReportShown(); // 看过即算(幂等):用户不点按钮、直接关页面也不会被反复骚扰
|
|
5249
|
+
});
|
|
5250
|
+
return function () { alive = false; };
|
|
5251
|
+
}, [loggedIn, mode, phoneKey]);
|
|
5252
|
+
|
|
5068
5253
|
/** 取消本次自动重启(按事件持久化:同一 pending 事件不再自动重启)。 */
|
|
5069
5254
|
var cancelAutoRestart = function () {
|
|
5070
5255
|
markAutoRestartCancelled(restartInfo.at || 0);
|
|
@@ -5095,6 +5280,42 @@ window.__ModuleLoader__.load({
|
|
|
5095
5280
|
);
|
|
5096
5281
|
}
|
|
5097
5282
|
|
|
5283
|
+
/**
|
|
5284
|
+
* 🎉 交流群引导弹层(使用满 2 小时后由服务端 show 触发;与上面的「加入交流群」二维码弹窗
|
|
5285
|
+
* 共用 dru-popup 视觉,不引入新设计语言)。
|
|
5286
|
+
* 关闭纪律:两个按钮 + 点遮罩都走同一个 close —— 关闭时**调用**幂等上报 shown 再收起
|
|
5287
|
+
* (展示时已成功报过则本地标记跳过网络;那次失败时这里正好补报)。
|
|
5288
|
+
* 展示时已经报过一次,所以即使这次上报失败(网络抖动)也已经是"看过即算"。
|
|
5289
|
+
*/
|
|
5290
|
+
function renderCommunityInviteModal() {
|
|
5291
|
+
if (!ciInvite) return null;
|
|
5292
|
+
var close = function () { ciReportShown(); setCiInvite(null); };
|
|
5293
|
+
var qrUrl = ciInvite.qrUrl;
|
|
5294
|
+
return h("div", { className: "dru-popup", "data-dru-theme": theme, onMouseDown: function (e) { if (e.target === e.currentTarget) close(); } },
|
|
5295
|
+
h("div", { className: "dru-popup-card", role: "dialog", "aria-label": "加入用户交流群" },
|
|
5296
|
+
h("div", { className: "dru-popup-body" },
|
|
5297
|
+
h("div", { className: "dru-popup-title" }, "🎉 加入用户交流群"),
|
|
5298
|
+
h("div", { className: "dru-popup-sub" }, "在群里可以交流使用心得、提想法和建议,遇到问题也更容易找到我们"),
|
|
5299
|
+
qrUrl
|
|
5300
|
+
? h("img", {
|
|
5301
|
+
className: "dru-community-qr",
|
|
5302
|
+
src: qrUrl,
|
|
5303
|
+
alt: "用户交流群二维码",
|
|
5304
|
+
// 二维码加载失败:只藏图片,文案与按钮照旧(绝不因为一张图挂了就把引导整块丢掉)
|
|
5305
|
+
onError: function (e) { try { if (e && e.target) e.target.style.display = "none"; } catch (err) { /* 忽略 */ } }
|
|
5306
|
+
})
|
|
5307
|
+
: null,
|
|
5308
|
+
h("div", { className: "dru-hint", style: { marginTop: 10 } },
|
|
5309
|
+
qrUrl ? "用手机微信/企业微信扫码即可入群。" : "二维码暂不可用,可稍后再看(不影响正常使用)。")
|
|
5310
|
+
),
|
|
5311
|
+
h("div", { className: "dru-popup-actions", style: { padding: "0 22px 20px" } },
|
|
5312
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: close }, "加入交流群"),
|
|
5313
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: close }, "稍后再看")
|
|
5314
|
+
)
|
|
5315
|
+
)
|
|
5316
|
+
);
|
|
5317
|
+
}
|
|
5318
|
+
|
|
5098
5319
|
// ── 📣 公告(0.6.18):面板入口卡 / 详情弹窗 / 远程访问卡里的提醒入口 ──────────
|
|
5099
5320
|
//
|
|
5100
5321
|
// 数据来源只有一个:模块级的 annState(由 apply() 里的一次性拉取填充,见 annPull)。
|
|
@@ -5124,8 +5345,12 @@ window.__ModuleLoader__.load({
|
|
|
5124
5345
|
function renderAnnouncementsCard() {
|
|
5125
5346
|
if (!annState.ok || !annState.bubble) return null;
|
|
5126
5347
|
var items = annState.items;
|
|
5127
|
-
if (!items.length) return null;
|
|
5128
5348
|
var unread = annState.unread;
|
|
5349
|
+
// 2026-10-25:列表为空时**保留板块**(显示「暂无公告」)而不是整卡消失 ——
|
|
5350
|
+
// 板块本身是用户确认"公告功能还在"的锚点,静默消失会被当成功能坏了。
|
|
5351
|
+
if (!items.length) {
|
|
5352
|
+
return card("📣 公告", [h("div", { className: "dru-hint" }, "暂无公告")]);
|
|
5353
|
+
}
|
|
5129
5354
|
return card("📣 公告" + (unread > 0 ? "(" + unread + " 条未读)" : ""), [
|
|
5130
5355
|
h("div", { className: "dru-ann-list" }, items.map(function (it) {
|
|
5131
5356
|
return h("button", {
|
|
@@ -5192,12 +5417,18 @@ window.__ModuleLoader__.load({
|
|
|
5192
5417
|
// 不该混在桌面端适配里顺手做掉。桌面端才走新文案。
|
|
5193
5418
|
// 2026-10-17(业主):桌面端运行环境由应用托管、更新自动热加载 ——
|
|
5194
5419
|
// **不再引导用户重启 DeepSeek Harness**,统一只说「刷新页面即可生效」。
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5420
|
+
// 桌面端:插件更新需要退出并重新打开应用(应用独占 profile,不能热改);
|
|
5421
|
+
// 若已暂存新版本,bridge 会在应用退出后自动安装,重开即生效 —— 绝不说「刷新页面即可」。
|
|
5422
|
+
var restartRowText = desktopHost
|
|
5423
|
+
? (desktopUpdatePending
|
|
5424
|
+
? "🆕 新版本已下载:退出并重新打开 DeepSeek Harness 后自动生效(无需终端)"
|
|
5425
|
+
: "🔄 插件已更新:退出并重新打开 DeepSeek Harness 后生效")
|
|
5426
|
+
: (!isRestartHard
|
|
5427
|
+
? "🔄 插件已更新:刷新页面即可生效"
|
|
5428
|
+
: "🔄 插件已更新:重启 dsh web 后生效");
|
|
5198
5429
|
rows.push(h("div", { className: "dru-access-tip-row", key: "restart" },
|
|
5199
5430
|
h("span", null, restartRowText),
|
|
5200
|
-
(
|
|
5431
|
+
(!desktopHost || isRefreshOnly) && !desktopHost ? h("button", {
|
|
5201
5432
|
type: "button",
|
|
5202
5433
|
className: "dru-linkbtn",
|
|
5203
5434
|
disabled: busy !== "",
|
|
@@ -5249,13 +5480,17 @@ window.__ModuleLoader__.load({
|
|
|
5249
5480
|
// **任何情况都不再引导"重启/退出重开应用"**,只说「刷新页面即可生效」。
|
|
5250
5481
|
// 非桌面端(自建 dsh web)保留原有 isRestartHard 判据,文案不变。
|
|
5251
5482
|
desktopHost
|
|
5252
|
-
?
|
|
5483
|
+
? (desktopUpdatePending
|
|
5484
|
+
? "新版本已下载:退出并重新打开 DeepSeek Harness 后生效"
|
|
5485
|
+
: "插件已更新:退出并重新打开 DeepSeek Harness 后生效")
|
|
5253
5486
|
: (isRestartHard
|
|
5254
5487
|
? "插件已更新,重启 dsh web 后生效"
|
|
5255
5488
|
: (isRefreshOnly ? "插件已更新,刷新页面即可生效" : "需要重启 dsh web 才能载入新插件"))),
|
|
5256
5489
|
h("div", { className: "dru-restart-alert-sub" },
|
|
5257
5490
|
desktopHost
|
|
5258
|
-
?
|
|
5491
|
+
? (desktopUpdatePending
|
|
5492
|
+
? "新版本已经下载到本机;DeepSeek Harness 退出后,后台服务会自动完成安装,重新打开应用即生效(无需终端,手机端连接与已登录状态都不受影响)。"
|
|
5493
|
+
: "新插件已在磁盘上,但桌面应用仍在运行旧插件;退出 DeepSeek Harness 后重新打开即可载入新版(应用退出后后台服务会自动完成安装,无需终端)。")
|
|
5259
5494
|
: (isRestartHard
|
|
5260
5495
|
? "新版本已经装到这台电脑上,但当前运行的 dsh web 还是旧代码(新的后台路由与面板要等它重新载入)。手机端连接与已登录状态都不受影响;重启期间页面会短暂断开,然后自动恢复。"
|
|
5261
5496
|
: (isRefreshOnly
|
|
@@ -5325,6 +5560,8 @@ window.__ModuleLoader__.load({
|
|
|
5325
5560
|
}, busy === "restart" ? "重启中…" : "🔄 重启 dsh web")
|
|
5326
5561
|
),
|
|
5327
5562
|
renderCommunityModal(),
|
|
5563
|
+
// 🎉 使用满 2 小时的交流群引导(服务端 show 决定的自动弹层;同一层,点遮罩即收起)
|
|
5564
|
+
renderCommunityInviteModal(),
|
|
5328
5565
|
// 📣 公告详情弹窗(与交流群弹窗同一层:都挂在面板根上,点遮罩即收起)
|
|
5329
5566
|
renderAnnouncementDetail()
|
|
5330
5567
|
);
|
|
@@ -5984,6 +6221,13 @@ window.__ModuleLoader__.load({
|
|
|
5984
6221
|
// 公告是锦上添花,**没有任何资格**把门关上 —— 任何异常都只是"这次没有弹窗"。
|
|
5985
6222
|
}
|
|
5986
6223
|
|
|
6224
|
+
// 📱 独立「远程访问」弹层(入口点击用;见 RemoteStandaloneOverlay 注释)
|
|
6225
|
+
try {
|
|
6226
|
+
ctx.slots.inject("shell.overlay", function () {
|
|
6227
|
+
return ctx.slots.register({ name: "shell.overlay", id: "dsh-remote-standalone", order: 95 }, RemoteStandaloneOverlay);
|
|
6228
|
+
});
|
|
6229
|
+
} catch (eStand) { /* 老宿主没有 shell.overlay:入口会在 150ms 后回退到官方设置栏目链路 */ }
|
|
6230
|
+
|
|
5987
6231
|
// 📣 公告:每次页面加载**只拉一次**(sessionStorage 一次性标记,见 annPullOnce)。
|
|
5988
6232
|
// 放在 apply() 里是刻意的:apply 跑在 app mount **之前**,所以这里**绝不能碰产品 DOM**
|
|
5989
6233
|
// (那时还没有),只做数据拉取;UI 全部交给上面那个槽位与面板卡片。
|