@wenbin_wb/dsh-bridge 2.11.2 → 2.12.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/CHANGELOG.md +76 -0
- package/README.en.md +25 -25
- package/README.md +25 -25
- package/client/client.js +235 -10
- package/client/index.js +124 -12
- package/client/locales.mjs +174 -0
- package/lib/bridge-rpc-constants.js +2 -0
- package/lib/bridge-rpc.js +33 -2
- package/lib/index.js +192 -11
- package/lib/platform/conversation-bridge.js +21 -6
- package/lib/platform/dsh-storage.js +64 -10
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +6 -5
- package/docs/CODE_REVIEW.md +0 -220
- package/docs/banner.jpg +0 -0
- package/docs/cloudflare-fixed-domain.md +0 -158
- package/docs/custom-tunnel.md +0 -220
- package/docs/feishu-usage.md +0 -116
- package/docs/qq-usage.md +0 -387
- package/docs/release-process.md +0 -45
- package/docs/screenshots/admin-lock-screen.jpg +0 -0
- package/docs/screenshots/feishu-bot-config.jpg +0 -0
- package/docs/screenshots/feishu-chat.jpg +0 -0
- package/docs/screenshots/lan-access.jpg +0 -0
- package/docs/screenshots/mobile-chat.jpg +0 -0
- package/docs/screenshots/mobile-drawer.jpg +0 -0
- package/docs/screenshots/mobile-remote-settings.jpg +0 -0
- package/docs/screenshots/mobile-settings-im.jpg +0 -0
- package/docs/screenshots/mobile-settings-lan.jpg +0 -0
- package/docs/screenshots/mobile-settings-security.jpg +0 -0
- package/docs/screenshots/mobile-settings-tunnel.jpg +0 -0
- package/docs/screenshots/mobile-workspace-picker.jpg +0 -0
- package/docs/screenshots/qq-bot-config.jpg +0 -0
- package/docs/screenshots/qq-chat.jpg +0 -0
- package/docs/screenshots/qq-group.jpg +0 -0
- package/docs/screenshots/qr-scan.jpg +0 -0
- package/docs/screenshots/remote-auth-login.jpg +0 -0
- package/docs/screenshots/remote-web-mobile.jpg +0 -0
- package/docs/screenshots/security-auth-config.jpg +0 -0
- package/docs/screenshots/telegram-bot-config.jpg +0 -0
- package/docs/screenshots/tunnel-access.jpg +0 -0
- package/docs/screenshots/wechat-bot-config.jpg +0 -0
- package/docs/screenshots/wechat-chat.jpg +0 -0
- package/docs/telegram-usage.md +0 -93
- package/docs/wechat-usage.md +0 -104
package/client/client.js
CHANGED
|
@@ -1568,6 +1568,148 @@ function registerSettingsNavIcon(resolveLabel = () => "\u8FDC\u7A0B\u8BBF\u95EE"
|
|
|
1568
1568
|
};
|
|
1569
1569
|
}
|
|
1570
1570
|
|
|
1571
|
+
// client/locales.mjs
|
|
1572
|
+
var LOCALE_NAMESPACE = "@wenbin_wb/dsh-bridge";
|
|
1573
|
+
var zh = {
|
|
1574
|
+
// 移动端顶栏
|
|
1575
|
+
"menu.open": "\u6253\u5F00\u83DC\u5355",
|
|
1576
|
+
"menu.newSession": "\u65B0\u5EFA\u4F1A\u8BDD",
|
|
1577
|
+
"title.fallback": "\u65B0\u4F1A\u8BDD",
|
|
1578
|
+
// 移动端输入区
|
|
1579
|
+
"composer.placeholder": "\u270D\uFE0F \u70B9\u51FB\u8F93\u5165\u6D88\u606F\u2026",
|
|
1580
|
+
"composer.expand": "\u5C55\u5F00\u8F93\u5165\u6846",
|
|
1581
|
+
"composer.collapse": "\u6536\u8D77\u8F93\u5165\u6846\uFF0C\u6700\u5927\u5316\u5BF9\u8BDD\u9605\u8BFB\u533A",
|
|
1582
|
+
"panel.backToChat": "\u8FD4\u56DE\u5BF9\u8BDD",
|
|
1583
|
+
// 移动端设置面板 / 抽屉(dsh-bridge 自绘部分)
|
|
1584
|
+
"workspace.choose": "\u9009\u62E9\u7535\u8111\u5DE5\u4F5C\u533A",
|
|
1585
|
+
"workspace.reading": "\u6B63\u5728\u8BFB\u53D6\u76EE\u5F55\u5185\u5BB9\u2026",
|
|
1586
|
+
"workspace.currentDir": "\u5F53\u524D\u76EE\u5F55:",
|
|
1587
|
+
"workspace.registeredHint": "\u5DF2\u6CE8\u518C\u5DE5\u4F5C\u533A (${count} \u4E2A\uFF0C\u70B9\u51FB\u76F4\u63A5\u5207\u6362)\uFF1A",
|
|
1588
|
+
"workspace.enterHint": "\u70B9\u51FB\u8FDB\u5165\u6587\u4EF6\u5939\uFF0C\u6216\u70B9\u51FB\u300C+ \u9009\u4E3A\u5DE5\u4F5C\u533A\u300D\u76F4\u63A5\u6DFB\u52A0\u5E76\u5207\u6362",
|
|
1589
|
+
"workspace.unlockHint": "\u8FDC\u7A0B\u8BBF\u95EE\u65F6\u6D4F\u89C8/\u6DFB\u52A0\u5DE5\u4F5C\u533A\u9700\u8F93\u5165\u540E\u53F0\u7BA1\u7406\u5BC6\u7801\u89E3\u9501\uFF08\u4E0E\u8BBF\u95EE\u5BC6\u7801\u4E0D\u540C\uFF09\u3002",
|
|
1590
|
+
"workspace.chooseFolder": "\u9009\u62E9\u5DE5\u4F5C\u533A",
|
|
1591
|
+
"workspace.addFolder": "\u6DFB\u52A0",
|
|
1592
|
+
"workspace.refresh": "\u5237\u65B0\u76EE\u5F55",
|
|
1593
|
+
"workspace.back": "\u8FD4\u56DE\u4E0A\u4E00\u7EA7",
|
|
1594
|
+
"workspace.filter": "\u8FC7\u6EE4\u5B50\u6587\u4EF6\u5939\u2026",
|
|
1595
|
+
"workspace.more": "\u8FD8\u6709\u66F4\u591A",
|
|
1596
|
+
"workspace.addSubfolder": "\u76F4\u63A5\u6DFB\u52A0\u6B64\u5B50\u6587\u4EF6\u5939\u4E3A\u5DE5\u4F5C\u533A\u5E76\u8FDB\u5165",
|
|
1597
|
+
"workspace.onlyAdmin": "\u4EC5\u9650\u672C\u673A\u7BA1\u7406",
|
|
1598
|
+
"workspace.isCurrent": "\u5F53\u524D\u4E3B\u5165\u53E3",
|
|
1599
|
+
"workspace.isFallback": "\u4E3B\u5165\u53E3",
|
|
1600
|
+
// 远程访问面板
|
|
1601
|
+
"access.copyLink": "\u590D\u5236\u94FE\u63A5",
|
|
1602
|
+
"access.copied": "\u2713 \u5DF2\u590D\u5236",
|
|
1603
|
+
"access.hideQr": "\u9690\u85CF\u4E8C\u7EF4\u7801",
|
|
1604
|
+
"access.showQr": "\u663E\u793A\u4E8C\u7EF4\u7801",
|
|
1605
|
+
"access.privateEnv": "\u8BF7\u5728\u79C1\u5BC6\u73AF\u5883\u4E0B\u4F7F\u7528",
|
|
1606
|
+
"access.switching": "\u5207\u6362\u4E2D\u2026",
|
|
1607
|
+
"access.publicEntry": "\u516C\u7F51\u8BBF\u95EE\u5165\u53E3",
|
|
1608
|
+
"access.copy": "\u590D\u5236",
|
|
1609
|
+
"access.stopReconnect": "\u505C\u6B62\u91CD\u8FDE",
|
|
1610
|
+
"access.close": "\u5173\u95ED",
|
|
1611
|
+
"access.connecting": "\u8FDE\u63A5\u4E2D\u2026",
|
|
1612
|
+
"access.downloading": "\u4E0B\u8F7D\u4E2D\u2026",
|
|
1613
|
+
"access.enable": "\u5F00\u542F\u516C\u7F51\u96A7\u9053",
|
|
1614
|
+
"access.scanHint": "\u8BF7\u5728\u79C1\u5BC6\u73AF\u5883\u4E0B\u626B\u7801\u4F7F\u7528",
|
|
1615
|
+
"access.autoStart": "\u968F DSH \u542F\u52A8\u81EA\u52A8\u5F00\u542F",
|
|
1616
|
+
"access.tunnelConfig": "\u96A7\u9053\u914D\u7F6E",
|
|
1617
|
+
"access.expand": "\u5C55\u5F00 \u25BE",
|
|
1618
|
+
"access.collapse": "\u6536\u8D77 \u25B4",
|
|
1619
|
+
"access.saveError": "\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25",
|
|
1620
|
+
"access.serverConfig": "\u96A7\u9053\u670D\u52A1\u5668\u914D\u7F6E",
|
|
1621
|
+
"access.saving": "\u4FDD\u5B58\u4E2D\u2026",
|
|
1622
|
+
"access.saved": "\u2713 \u5DF2\u4FDD\u5B58",
|
|
1623
|
+
"access.saveConfig": "\u4FDD\u5B58\u914D\u7F6E",
|
|
1624
|
+
"access.clear": "\u6E05\u9664",
|
|
1625
|
+
"access.externalTunnel": "\u5916\u90E8\u5DF2\u90E8\u7F72\u96A7\u9053",
|
|
1626
|
+
"access.saveFixed": "\u4FDD\u5B58\u56FA\u5B9A\u57DF\u540D\u914D\u7F6E",
|
|
1627
|
+
"access.updateFailed": "\u66F4\u65B0\u5931\u8D25",
|
|
1628
|
+
"access.securityHint": "\u8BBE\u7F6E\u5916\u90E8\u8BBF\u5BA2\u8BBF\u95EE\u5BC6\u7801",
|
|
1629
|
+
"access.goSetup": "\u53BB\u5F00\u542F \u2794",
|
|
1630
|
+
"access.gotoSettings": "\u8BBE\u7F6E \u2794",
|
|
1631
|
+
"access.tutorial": "\u67E5\u770B\u81EA\u5EFA\u96A7\u9053\u670D\u52A1\u5668\u642D\u5EFA\u6559\u7A0B"
|
|
1632
|
+
};
|
|
1633
|
+
var en = {
|
|
1634
|
+
"menu.open": "Open menu",
|
|
1635
|
+
"menu.newSession": "New session",
|
|
1636
|
+
"title.fallback": "New Session",
|
|
1637
|
+
"composer.placeholder": "\u270D\uFE0F Tap to type a message\u2026",
|
|
1638
|
+
"composer.expand": "Expand input",
|
|
1639
|
+
"composer.collapse": "Collapse input to maximize chat view",
|
|
1640
|
+
"panel.backToChat": "Back to chat",
|
|
1641
|
+
"workspace.choose": "Choose a workspace",
|
|
1642
|
+
"workspace.reading": "Reading directory\u2026",
|
|
1643
|
+
"workspace.currentDir": "Current directory:",
|
|
1644
|
+
"workspace.registeredHint": "Registered workspaces (${count}, click to switch):",
|
|
1645
|
+
"workspace.enterHint": "Enter a folder, or tap \u201C+ Select as workspace\u201D to add & switch",
|
|
1646
|
+
"workspace.unlockHint": "Browsing/adding workspaces remotely requires the admin password to unlock (different from the access password).",
|
|
1647
|
+
"workspace.chooseFolder": "Choose workspace",
|
|
1648
|
+
"workspace.addFolder": "Add",
|
|
1649
|
+
"workspace.refresh": "Refresh",
|
|
1650
|
+
"workspace.back": "Back",
|
|
1651
|
+
"workspace.filter": "Filter subfolders\u2026",
|
|
1652
|
+
"workspace.more": "More",
|
|
1653
|
+
"workspace.addSubfolder": "Add this subfolder as a workspace and enter",
|
|
1654
|
+
"workspace.onlyAdmin": "Local admin only",
|
|
1655
|
+
"workspace.isCurrent": "Current entry",
|
|
1656
|
+
"workspace.isFallback": "Main entry",
|
|
1657
|
+
"access.copyLink": "Copy link",
|
|
1658
|
+
"access.copied": "\u2713 Copied",
|
|
1659
|
+
"access.hideQr": "Hide QR",
|
|
1660
|
+
"access.showQr": "Show QR",
|
|
1661
|
+
"access.privateEnv": "Use in a private environment",
|
|
1662
|
+
"access.switching": "Switching\u2026",
|
|
1663
|
+
"access.publicEntry": "Public access entry",
|
|
1664
|
+
"access.copy": "Copy",
|
|
1665
|
+
"access.stopReconnect": "Stop reconnecting",
|
|
1666
|
+
"access.close": "Close",
|
|
1667
|
+
"access.connecting": "Connecting\u2026",
|
|
1668
|
+
"access.downloading": "Downloading\u2026",
|
|
1669
|
+
"access.enable": "Enable public tunnel",
|
|
1670
|
+
"access.scanHint": "Scan in a private environment",
|
|
1671
|
+
"access.autoStart": "Start automatically with DSH",
|
|
1672
|
+
"access.tunnelConfig": "Tunnel configuration",
|
|
1673
|
+
"access.expand": "Expand \u25BE",
|
|
1674
|
+
"access.collapse": "Collapse \u25B4",
|
|
1675
|
+
"access.saveError": "Failed to save configuration",
|
|
1676
|
+
"access.serverConfig": "Tunnel server configuration",
|
|
1677
|
+
"access.saving": "Saving\u2026",
|
|
1678
|
+
"access.saved": "\u2713 Saved",
|
|
1679
|
+
"access.saveConfig": "Save configuration",
|
|
1680
|
+
"access.clear": "Clear",
|
|
1681
|
+
"access.externalTunnel": "External tunnel",
|
|
1682
|
+
"access.saveFixed": "Save fixed-domain configuration",
|
|
1683
|
+
"access.updateFailed": "Update failed",
|
|
1684
|
+
"access.securityHint": "Set an external visitor access password",
|
|
1685
|
+
"access.goSetup": "Set up \u2794",
|
|
1686
|
+
"access.gotoSettings": "Settings \u2794",
|
|
1687
|
+
"access.tutorial": "View the self-hosted tunnel server tutorial"
|
|
1688
|
+
};
|
|
1689
|
+
function createTranslator(ctx) {
|
|
1690
|
+
const active = () => {
|
|
1691
|
+
const lang = (typeof document !== "undefined" ? document.documentElement.getAttribute("lang") : "") || "";
|
|
1692
|
+
return lang.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
1693
|
+
};
|
|
1694
|
+
const entryOf = (key) => ({ zh: zh[key], en: en[key] });
|
|
1695
|
+
const t = (key, vars) => {
|
|
1696
|
+
const e = entryOf(key);
|
|
1697
|
+
let text = e[active()] ?? e.en ?? e.zh ?? "";
|
|
1698
|
+
if (vars && text) {
|
|
1699
|
+
for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`\${${k}}`, String(v));
|
|
1700
|
+
}
|
|
1701
|
+
return text;
|
|
1702
|
+
};
|
|
1703
|
+
try {
|
|
1704
|
+
const locale = ctx && (typeof ctx.get === "function" ? ctx.get("locale") : ctx.locale);
|
|
1705
|
+
if (locale && typeof locale.register === "function") {
|
|
1706
|
+
locale.register(LOCALE_NAMESPACE, { zh, en });
|
|
1707
|
+
}
|
|
1708
|
+
} catch {
|
|
1709
|
+
}
|
|
1710
|
+
return { t, active };
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1571
1713
|
// lib/bridge-rpc-constants.js
|
|
1572
1714
|
var BRIDGE_RPC_CHANNEL = "/dsh-bridge";
|
|
1573
1715
|
var BRIDGE_ENDPOINTS = {
|
|
@@ -1600,6 +1742,8 @@ var BRIDGE_ENDPOINTS = {
|
|
|
1600
1742
|
authRegenerateToken: "authRegenerateToken",
|
|
1601
1743
|
authAdminUnlock: "authAdminUnlock",
|
|
1602
1744
|
authAdminLock: "authAdminLock",
|
|
1745
|
+
// 首次启用引导:用户确认已了解后置位,避免重复打扰
|
|
1746
|
+
dismissFirstRunGuide: "dismissFirstRunGuide",
|
|
1603
1747
|
// 平台管理器(多 IM 平台统一接口)
|
|
1604
1748
|
listPlatforms: "listPlatforms",
|
|
1605
1749
|
platformLogin: "platformLogin",
|
|
@@ -1758,6 +1902,7 @@ var RELEASES_URL = "https://github.com/wenbin-wb/dsh-bridge/releases";
|
|
|
1758
1902
|
var ISSUES_URL = "https://github.com/wenbin-wb/dsh-bridge/issues/new";
|
|
1759
1903
|
var TUNNEL_DOCS_URL = "https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/custom-tunnel.md";
|
|
1760
1904
|
var CLOUDFLARE_TUTORIAL_URL = "https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/cloudflare-fixed-domain.md";
|
|
1905
|
+
var localeT = (key) => key;
|
|
1761
1906
|
function upgradeCommands(latest) {
|
|
1762
1907
|
const spec = `@wenbin_wb/dsh-bridge@${latest}`;
|
|
1763
1908
|
return [
|
|
@@ -4280,6 +4425,38 @@ function BackupRestoreWidget({ rpcCall, onUpdate }) {
|
|
|
4280
4425
|
}, msg.text)
|
|
4281
4426
|
);
|
|
4282
4427
|
}
|
|
4428
|
+
function formatActiveWorkWarning(work) {
|
|
4429
|
+
if (!work || !work.total) return null;
|
|
4430
|
+
const lines = [];
|
|
4431
|
+
if (work.sessions?.length) {
|
|
4432
|
+
lines.push(`\u2022 \u8FDB\u884C\u4E2D\u7684\u4F1A\u8BDD ${work.sessions.length} \u4E2A\uFF1A`);
|
|
4433
|
+
for (const s2 of work.sessions.slice(0, 5)) {
|
|
4434
|
+
const detail = s2.tools > 0 ? `${s2.tools} \u6B21\u5DE5\u5177\u8C03\u7528${s2.lastTool ? `\uFF08\u6700\u8FD1\uFF1A${s2.lastTool}\uFF09` : ""}` : "\u601D\u8003\u4E2D";
|
|
4435
|
+
lines.push(` - ${s2.title}\uFF08\u7B2C ${s2.turn} \u8F6E\uFF0C${detail}\uFF09`);
|
|
4436
|
+
}
|
|
4437
|
+
if (work.sessions.length > 5) lines.push(` \u2026\u7B49\u5171 ${work.sessions.length} \u4E2A`);
|
|
4438
|
+
}
|
|
4439
|
+
if (work.subagentSessions?.length) {
|
|
4440
|
+
lines.push(`\u2022 \u8FDB\u884C\u4E2D\u7684\u5B50\u4EE3\u7406/agent \u4EFB\u52A1 ${work.subagentSessions.length} \u4E2A\uFF1A`);
|
|
4441
|
+
for (const s2 of work.subagentSessions.slice(0, 5)) {
|
|
4442
|
+
const detail = s2.tools > 0 ? `${s2.tools} \u6B21\u5DE5\u5177\u8C03\u7528` : "\u601D\u8003\u4E2D";
|
|
4443
|
+
lines.push(` - ${s2.title}\uFF08\u7B2C ${s2.turn} \u8F6E\uFF0C${detail}\uFF09`);
|
|
4444
|
+
}
|
|
4445
|
+
if (work.subagentSessions.length > 5) lines.push(` \u2026\u7B49\u5171 ${work.subagentSessions.length} \u4E2A`);
|
|
4446
|
+
}
|
|
4447
|
+
if (work.pendingApprovals?.length) {
|
|
4448
|
+
lines.push(`\u2022 \u5F85\u5BA1\u6279\u8BF7\u6C42 ${work.pendingApprovals.length} \u4E2A\uFF1A`);
|
|
4449
|
+
for (const a of work.pendingApprovals.slice(0, 5)) {
|
|
4450
|
+
lines.push(` - [${a.platform}] ${a.summary}`);
|
|
4451
|
+
}
|
|
4452
|
+
if (work.pendingApprovals.length > 5) lines.push(` \u2026\u7B49\u5171 ${work.pendingApprovals.length} \u4E2A`);
|
|
4453
|
+
}
|
|
4454
|
+
return `\u26A0\uFE0F \u68C0\u6D4B\u5230\u6709\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u91CD\u542F\u4F1A\u4E2D\u65AD\u5B83\u4EEC\uFF1A
|
|
4455
|
+
|
|
4456
|
+
${lines.join("\n")}
|
|
4457
|
+
|
|
4458
|
+
\u4E2D\u65AD\u540E\u53EF\u80FD\u9700\u8981\u91CD\u65B0\u53D1\u8D77\u3002\u786E\u5B9A\u8981\u7EE7\u7EED\u91CD\u542F\u5417\uFF1F`;
|
|
4459
|
+
}
|
|
4283
4460
|
function useDshRestart({ rpcCall, maxAttempts = 30, texts = {} } = {}) {
|
|
4284
4461
|
const T = {
|
|
4285
4462
|
restarting: "\u6B63\u5728\u5411 DSH \u670D\u52A1\u53D1\u9001\u91CD\u542F\u6307\u4EE4\u2026",
|
|
@@ -4301,8 +4478,21 @@ function useDshRestart({ rpcCall, maxAttempts = 30, texts = {} } = {}) {
|
|
|
4301
4478
|
setStatus({ phase: "restarting", text: T.restarting });
|
|
4302
4479
|
try {
|
|
4303
4480
|
const r = await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
|
|
4304
|
-
if (r && r.ok === false) {
|
|
4305
|
-
|
|
4481
|
+
if (r && r.ok === false && r.error?.code === "busy") {
|
|
4482
|
+
const warning = formatActiveWorkWarning(r.error?.details?.activeWork);
|
|
4483
|
+
if (!window.confirm(warning || "\u5F53\u524D\u6709\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u91CD\u542F\u4F1A\u4E2D\u65AD\u5B83\u4EEC\u3002\u786E\u5B9A\u7EE7\u7EED\u5417\uFF1F")) {
|
|
4484
|
+
setStatus(null);
|
|
4485
|
+
setRestarting(false);
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
const r2 = await rpcCall(BRIDGE_ENDPOINTS.restartDsh, { confirm: true });
|
|
4489
|
+
if (r2 && r2.ok === false) {
|
|
4490
|
+
setStatus({ phase: "timeout", text: `${T.timeout}\uFF1A${r2.error?.message || "\u672A\u77E5\u539F\u56E0"}` });
|
|
4491
|
+
setRestarting(false);
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
4494
|
+
} else if (r && r.ok === false) {
|
|
4495
|
+
setStatus({ phase: "timeout", text: `${T.timeout}\uFF1A${r.error?.message || r.error || "\u672A\u77E5\u539F\u56E0"}` });
|
|
4306
4496
|
setRestarting(false);
|
|
4307
4497
|
return;
|
|
4308
4498
|
}
|
|
@@ -5909,7 +6099,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
5909
6099
|
header.className = "dsh-mobile-app-header";
|
|
5910
6100
|
const leftBtn = document.createElement("button");
|
|
5911
6101
|
leftBtn.className = "dsh-header-menu-btn";
|
|
5912
|
-
leftBtn.title = "
|
|
6102
|
+
leftBtn.title = localeT("menu.open");
|
|
5913
6103
|
leftBtn.innerHTML = `
|
|
5914
6104
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
|
|
5915
6105
|
<line x1="3" y1="8" x2="21" y2="8"></line>
|
|
@@ -5935,7 +6125,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
5935
6125
|
};
|
|
5936
6126
|
const rightBtn = document.createElement("button");
|
|
5937
6127
|
rightBtn.className = "dsh-header-new-btn";
|
|
5938
|
-
rightBtn.title = "
|
|
6128
|
+
rightBtn.title = localeT("menu.newSession");
|
|
5939
6129
|
rightBtn.innerHTML = `
|
|
5940
6130
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
5941
6131
|
<circle cx="12" cy="12" r="9.5"></circle>
|
|
@@ -5954,22 +6144,55 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
5954
6144
|
header.appendChild(rightBtn);
|
|
5955
6145
|
document.body.appendChild(header);
|
|
5956
6146
|
}
|
|
6147
|
+
const MOBILE_TITLE_FALLBACK_TEXT = () => localeT("title.fallback");
|
|
6148
|
+
const readHostSessionTitle = () => {
|
|
6149
|
+
if (typeof document === "undefined") return "";
|
|
6150
|
+
const nav = document.querySelector('nav[aria-label="Session hierarchy"], nav[aria-label="\u4F1A\u8BDD\u5C42\u7EA7"]');
|
|
6151
|
+
if (!nav) return "";
|
|
6152
|
+
const segs = [...nav.querySelectorAll('[class*="crumbSeg"]')];
|
|
6153
|
+
const lastSeg = segs[segs.length - 1];
|
|
6154
|
+
const lastCrumb = lastSeg ? lastSeg.querySelector('[class*="crumbCurrent"]') || lastSeg.querySelector("span, button") : null;
|
|
6155
|
+
if (!lastCrumb) return "";
|
|
6156
|
+
const text = (lastCrumb.getAttribute?.("aria-label") || lastCrumb.textContent || "").trim();
|
|
6157
|
+
if (!text || /^\d+\s+(item|items|项|个)/i.test(text)) return "";
|
|
6158
|
+
return text;
|
|
6159
|
+
};
|
|
6160
|
+
const setTitleText = (next) => {
|
|
6161
|
+
if (titleEl.textContent === next) return;
|
|
6162
|
+
titleEl.innerText = next;
|
|
6163
|
+
};
|
|
5957
6164
|
const syncMobileTitle = () => {
|
|
5958
6165
|
if (!titleEl) titleEl = document.querySelector(".dsh-mobile-header-title");
|
|
5959
6166
|
if (!titleEl) return;
|
|
6167
|
+
const hostTitle = readHostSessionTitle();
|
|
6168
|
+
if (hostTitle) {
|
|
6169
|
+
setTitleText(hostTitle);
|
|
6170
|
+
return;
|
|
6171
|
+
}
|
|
5960
6172
|
const snap = ctx?.sessions?.list?.getSnapshot?.();
|
|
5961
6173
|
if (!snap) return;
|
|
5962
6174
|
const cur = snap.current ? snap.byId?.[snap.current] : null;
|
|
5963
6175
|
if (!cur || cur.blank) {
|
|
5964
|
-
|
|
6176
|
+
setTitleText(MOBILE_TITLE_FALLBACK_TEXT());
|
|
5965
6177
|
} else {
|
|
5966
|
-
|
|
6178
|
+
setTitleText(cur.displayTitle || cur.title || MOBILE_TITLE_FALLBACK_TEXT());
|
|
5967
6179
|
}
|
|
5968
6180
|
};
|
|
5969
6181
|
syncMobileTitle();
|
|
5970
6182
|
if (typeof ctx?.sessions?.list?.subscribe === "function") {
|
|
5971
6183
|
ctx.sessions.list.subscribe(syncMobileTitle);
|
|
5972
6184
|
}
|
|
6185
|
+
if (typeof MutationObserver !== "undefined") {
|
|
6186
|
+
let mobileTitleRaf = 0;
|
|
6187
|
+
const mobileTitleObserver = new MutationObserver(() => {
|
|
6188
|
+
if (mobileTitleRaf) return;
|
|
6189
|
+
mobileTitleRaf = requestAnimationFrame(() => {
|
|
6190
|
+
mobileTitleRaf = 0;
|
|
6191
|
+
syncMobileTitle();
|
|
6192
|
+
});
|
|
6193
|
+
});
|
|
6194
|
+
mobileTitleObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
6195
|
+
}
|
|
5973
6196
|
if (typeof ctx?.sessions?.active?.subscribe === "function") {
|
|
5974
6197
|
ctx.sessions.active.subscribe(() => {
|
|
5975
6198
|
if (document.body.classList.contains("dsh-drawer-open")) {
|
|
@@ -5994,7 +6217,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
5994
6217
|
if (bar && !bar.querySelector(".dsh-mobile-panel-close-btn")) {
|
|
5995
6218
|
const btn = document.createElement("button");
|
|
5996
6219
|
btn.className = "dsh-mobile-panel-close-btn";
|
|
5997
|
-
btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg><span
|
|
6220
|
+
btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg><span>${localeT("panel.backToChat")}</span>`;
|
|
5998
6221
|
btn.onclick = (e) => {
|
|
5999
6222
|
e.stopPropagation();
|
|
6000
6223
|
document.body.classList.remove("dsh-workbench-open");
|
|
@@ -6877,7 +7100,7 @@ function setupComposerCollapse() {
|
|
|
6877
7100
|
if (!utils) return null;
|
|
6878
7101
|
const btn = document.createElement("button");
|
|
6879
7102
|
btn.className = "dsh-header-fold-btn";
|
|
6880
|
-
btn.setAttribute("aria-label", "
|
|
7103
|
+
btn.setAttribute("aria-label", `${localeT("composer.expand")} / ${localeT("composer.collapse")}`);
|
|
6881
7104
|
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="18" x2="21" y2="18"></line><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
6882
7105
|
if (logBtn && logBtn.parentElement === utils) utils.insertBefore(btn, logBtn);
|
|
6883
7106
|
else utils.appendChild(btn);
|
|
@@ -6898,7 +7121,7 @@ function setupComposerCollapse() {
|
|
|
6898
7121
|
if (!scrollBody) return null;
|
|
6899
7122
|
bar = document.createElement("div");
|
|
6900
7123
|
bar.className = "dsh-composer-collapsed-bar";
|
|
6901
|
-
bar.textContent = "
|
|
7124
|
+
bar.textContent = localeT("composer.placeholder");
|
|
6902
7125
|
scrollBody.insertBefore(bar, seat);
|
|
6903
7126
|
bar.addEventListener("click", () => setCollapsed(false));
|
|
6904
7127
|
return bar;
|
|
@@ -6933,7 +7156,7 @@ function setupComposerCollapse() {
|
|
|
6933
7156
|
const updateButton = (collapsed) => {
|
|
6934
7157
|
const btn = getFoldBtn();
|
|
6935
7158
|
if (!btn) return;
|
|
6936
|
-
btn.title = collapsed ? "
|
|
7159
|
+
btn.title = collapsed ? localeT("composer.expand") : localeT("composer.collapse");
|
|
6937
7160
|
const icon = collapsed ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"></rect><line x1="6" y1="10" x2="6.01" y2="10"></line><line x1="10" y1="10" x2="10.01" y2="10"></line><line x1="14" y1="10" x2="14.01" y2="10"></line><line x1="6" y1="14" x2="10" y2="14"></line><line x1="14" y1="14" x2="18" y2="14"></line></svg>' : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="18" x2="21" y2="18"></line><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
6938
7161
|
const existing = btn.querySelector("svg");
|
|
6939
7162
|
if (!existing || existing.outerHTML !== icon) btn.innerHTML = icon;
|
|
@@ -6996,6 +7219,8 @@ function setupComposerCollapse() {
|
|
|
6996
7219
|
}
|
|
6997
7220
|
function apply(ctx) {
|
|
6998
7221
|
window.__dshClientCtx = ctx;
|
|
7222
|
+
const intl = createTranslator(ctx);
|
|
7223
|
+
localeT = (key, vars) => intl.t(key, vars);
|
|
6999
7224
|
setupSettingsDrilldown();
|
|
7000
7225
|
if (typeof ctx.effect === "function") {
|
|
7001
7226
|
ctx.effect(() => registerSettingsNavIcon(() => "\u8FDC\u7A0B\u8BBF\u95EE"), "dsh-bridge: settings nav icon");
|
package/client/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getRuntimeFeatureOverrides,
|
|
14
14
|
} from './mobile-yield.js'
|
|
15
15
|
import { registerSettingsNavIcon } from './settings-nav-icon.js'
|
|
16
|
+
import { createTranslator } from './locales.mjs'
|
|
16
17
|
// dsh-bridge 客户端插件:设置页「远程访问」面板
|
|
17
18
|
|
|
18
19
|
// 兼容非 HTTPS 环境(如手机局域网 HTTP 访问):为非安全上下文补齐 crypto.randomUUID
|
|
@@ -121,6 +122,10 @@ const ISSUES_URL = 'https://github.com/wenbin-wb/dsh-bridge/issues/new';
|
|
|
121
122
|
const TUNNEL_DOCS_URL = 'https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/custom-tunnel.md';
|
|
122
123
|
const CLOUDFLARE_TUTORIAL_URL = 'https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/cloudflare-fixed-domain.md';
|
|
123
124
|
|
|
125
|
+
// 模块级翻译函数:apply() 里随 ctx.locale 初始化(跟随 DSH 主体语言),
|
|
126
|
+
// 供 setupMobileExperience / setupComposerCollapse 等各函数读取(不通过参数传递)。
|
|
127
|
+
let localeT = (key) => key;
|
|
128
|
+
|
|
124
129
|
// 生成升级命令(拼接具体版本号;用 add 而非 update,update --latest 受已安装依赖版本约束可能无法升级到最新版)
|
|
125
130
|
function upgradeCommands(latest) {
|
|
126
131
|
const spec = `@wenbin_wb/dsh-bridge@${latest}`;
|
|
@@ -131,6 +136,9 @@ function upgradeCommands(latest) {
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
const name = 'dsh-bridge';
|
|
139
|
+
// 注:locale 服务不能通过 inject 声明(动态插件激活时该服务不在运行时上下文,
|
|
140
|
+
// 声明会导致「entry did not activate」);改用 apply 内 ctx.get('locale') 可选获取,
|
|
141
|
+
// 无则退化为本地 en/zh 词典(见 locales.mjs)。
|
|
134
142
|
const inject = ['slots', 'connection', 'workspaces', 'sessions'];
|
|
135
143
|
|
|
136
144
|
// semver 比较:a > b
|
|
@@ -2340,6 +2348,41 @@ function BackupRestoreWidget({ rpcCall, onUpdate }) {
|
|
|
2340
2348
|
// 重启 + 健康轮询公共逻辑:定时器保存在 ref 中,组件卸载即清理。
|
|
2341
2349
|
// 修复点:原先两处复制粘贴的 setInterval 不随组件卸载清理,用户切走 Tab 后
|
|
2342
2350
|
// 轮询仍会继续运行,并在服务恢复时无条件执行 window.location.reload()。
|
|
2351
|
+
|
|
2352
|
+
/**
|
|
2353
|
+
* 把「正在运行的任务」格式化成二次确认文案。
|
|
2354
|
+
* 返回 null 表示没有任务在跑,调用方无需确认。
|
|
2355
|
+
* @param {{sessions?:Array, subagentSessions?:Array, pendingApprovals?:Array, total?:number}} work
|
|
2356
|
+
*/
|
|
2357
|
+
function formatActiveWorkWarning(work) {
|
|
2358
|
+
if (!work || !work.total) return null;
|
|
2359
|
+
const lines = [];
|
|
2360
|
+
if (work.sessions?.length) {
|
|
2361
|
+
lines.push(`• 进行中的会话 ${work.sessions.length} 个:`);
|
|
2362
|
+
for (const s of work.sessions.slice(0, 5)) {
|
|
2363
|
+
const detail = s.tools > 0 ? `${s.tools} 次工具调用${s.lastTool ? `(最近:${s.lastTool})` : ''}` : '思考中';
|
|
2364
|
+
lines.push(` - ${s.title}(第 ${s.turn} 轮,${detail})`);
|
|
2365
|
+
}
|
|
2366
|
+
if (work.sessions.length > 5) lines.push(` …等共 ${work.sessions.length} 个`);
|
|
2367
|
+
}
|
|
2368
|
+
if (work.subagentSessions?.length) {
|
|
2369
|
+
lines.push(`• 进行中的子代理/agent 任务 ${work.subagentSessions.length} 个:`);
|
|
2370
|
+
for (const s of work.subagentSessions.slice(0, 5)) {
|
|
2371
|
+
const detail = s.tools > 0 ? `${s.tools} 次工具调用` : '思考中';
|
|
2372
|
+
lines.push(` - ${s.title}(第 ${s.turn} 轮,${detail})`);
|
|
2373
|
+
}
|
|
2374
|
+
if (work.subagentSessions.length > 5) lines.push(` …等共 ${work.subagentSessions.length} 个`);
|
|
2375
|
+
}
|
|
2376
|
+
if (work.pendingApprovals?.length) {
|
|
2377
|
+
lines.push(`• 待审批请求 ${work.pendingApprovals.length} 个:`);
|
|
2378
|
+
for (const a of work.pendingApprovals.slice(0, 5)) {
|
|
2379
|
+
lines.push(` - [${a.platform}] ${a.summary}`);
|
|
2380
|
+
}
|
|
2381
|
+
if (work.pendingApprovals.length > 5) lines.push(` …等共 ${work.pendingApprovals.length} 个`);
|
|
2382
|
+
}
|
|
2383
|
+
return `⚠️ 检测到有任务正在运行,重启会中断它们:\n\n${lines.join('\n')}\n\n中断后可能需要重新发起。确定要继续重启吗?`;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2343
2386
|
function useDshRestart({ rpcCall, maxAttempts = 30, texts = {} } = {}) {
|
|
2344
2387
|
const T = {
|
|
2345
2388
|
restarting: '正在向 DSH 服务发送重启指令…',
|
|
@@ -2363,10 +2406,25 @@ function useDshRestart({ rpcCall, maxAttempts = 30, texts = {} } = {}) {
|
|
|
2363
2406
|
setStatus({ phase: 'restarting', text: T.restarting });
|
|
2364
2407
|
try {
|
|
2365
2408
|
const r = await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
|
|
2366
|
-
//
|
|
2367
|
-
//
|
|
2368
|
-
if (r && r.ok === false) {
|
|
2369
|
-
|
|
2409
|
+
// 有任务在跑:宿主不会重启,而是把现场回给面板,由这里弹二次确认。
|
|
2410
|
+
// 确认后带 confirm:true 再请求一次(宿主才真正执行重启)。
|
|
2411
|
+
if (r && r.ok === false && r.error?.code === 'busy') {
|
|
2412
|
+
const warning = formatActiveWorkWarning(r.error?.details?.activeWork);
|
|
2413
|
+
if (!window.confirm(warning || '当前有任务正在运行,重启会中断它们。确定继续吗?')) {
|
|
2414
|
+
setStatus(null);
|
|
2415
|
+
setRestarting(false);
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
const r2 = await rpcCall(BRIDGE_ENDPOINTS.restartDsh, { confirm: true });
|
|
2419
|
+
if (r2 && r2.ok === false) {
|
|
2420
|
+
setStatus({ phase: 'timeout', text: `${T.timeout}:${r2.error?.message || '未知原因'}` });
|
|
2421
|
+
setRestarting(false);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
} else if (r && r.ok === false) {
|
|
2425
|
+
// 宿主已明确告知"没能安排重启"(例如 systemd 调用失败、助手都派生不出来):
|
|
2426
|
+
// 这时绝不能再假装"正在重连"——直接报错并停下,否则用户只会看到永远转圈。
|
|
2427
|
+
setStatus({ phase: 'timeout', text: `${T.timeout}:${r.error?.message || r.error || '未知原因'}` });
|
|
2370
2428
|
setRestarting(false);
|
|
2371
2429
|
return;
|
|
2372
2430
|
}
|
|
@@ -3939,7 +3997,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3939
3997
|
// 左侧双横线菜单按钮 (DeepSeek App 原生图标):联动展开 DSH 原生侧边栏会话列表
|
|
3940
3998
|
const leftBtn = document.createElement('button');
|
|
3941
3999
|
leftBtn.className = 'dsh-header-menu-btn';
|
|
3942
|
-
leftBtn.title = '
|
|
4000
|
+
leftBtn.title = localeT('menu.open');
|
|
3943
4001
|
leftBtn.innerHTML = `
|
|
3944
4002
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
|
|
3945
4003
|
<line x1="3" y1="8" x2="21" y2="8"></line>
|
|
@@ -3972,7 +4030,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3972
4030
|
// 右侧 (+) 新建会话按钮 (DeepSeek App 圆形加号风格)
|
|
3973
4031
|
const rightBtn = document.createElement('button');
|
|
3974
4032
|
rightBtn.className = 'dsh-header-new-btn';
|
|
3975
|
-
rightBtn.title = '
|
|
4033
|
+
rightBtn.title = localeT('menu.newSession');
|
|
3976
4034
|
rightBtn.innerHTML = `
|
|
3977
4035
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
3978
4036
|
<circle cx="12" cy="12" r="9.5"></circle>
|
|
@@ -3995,16 +4053,52 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3995
4053
|
}
|
|
3996
4054
|
|
|
3997
4055
|
// 绑定会话标题实时同步 (切换会话或收到首条回复自动更新)
|
|
4056
|
+
//
|
|
4057
|
+
// 标题来源优先取宿主「会话层级」面包屑(PC 顶栏同源):宿主自己维护当前会话标题,
|
|
4058
|
+
// 随会话切换实时更新,0.1.7 的 sessions.list 快照没有 current 字段(旧方案取不到 →
|
|
4059
|
+
// 顶栏永远显示「新会话」)。crumbs 的 nav 带稳定 aria-label(中英双语),最后一级
|
|
4060
|
+
// crumb 就是当前会话标题;空会话时无 crumbs,回退「新会话」。
|
|
4061
|
+
const MOBILE_TITLE_FALLBACK_TEXT = () => localeT('title.fallback');
|
|
4062
|
+
// 取宿主会话层级面包屑的「当前会话标题」。稳定锚点是 nav 的 aria-label(中英双语);
|
|
4063
|
+
// 标题是最后一个 crumbSeg 里的「crumb(当前段)」,不是 nav 里任意最后一个 span——
|
|
4064
|
+
// 子 agent 会话会把末级 crumb 换成 dropdown(内含计数 span),取最后一个 span 会拿到
|
|
4065
|
+
// 计数文本。crumb 本身是 span/button 且带 crumbCurrent 类;取不到则回退空串。
|
|
4066
|
+
const readHostSessionTitle = () => {
|
|
4067
|
+
if (typeof document === 'undefined') return '';
|
|
4068
|
+
const nav = document.querySelector('nav[aria-label="Session hierarchy"], nav[aria-label="会话层级"]');
|
|
4069
|
+
if (!nav) return '';
|
|
4070
|
+
const segs = [...nav.querySelectorAll('[class*="crumbSeg"]')];
|
|
4071
|
+
const lastSeg = segs[segs.length - 1];
|
|
4072
|
+
const lastCrumb = lastSeg
|
|
4073
|
+
? lastSeg.querySelector('[class*="crumbCurrent"]') || lastSeg.querySelector('span, button')
|
|
4074
|
+
: null;
|
|
4075
|
+
if (!lastCrumb) return '';
|
|
4076
|
+
const text = ((lastCrumb.getAttribute?.('aria-label') || lastCrumb.textContent) || '').trim();
|
|
4077
|
+
// 非标题节点(如子 agent 计数 "2 items"/"2 项")不应被当作标题
|
|
4078
|
+
if (!text || /^\d+\s+(item|items|项|个)/i.test(text)) return '';
|
|
4079
|
+
return text;
|
|
4080
|
+
};
|
|
4081
|
+
const setTitleText = (next) => {
|
|
4082
|
+
if (titleEl.textContent === next) return; // 写前比对:避免每帧重写触发 observer 自循环
|
|
4083
|
+
titleEl.innerText = next;
|
|
4084
|
+
};
|
|
3998
4085
|
const syncMobileTitle = () => {
|
|
3999
4086
|
if (!titleEl) titleEl = document.querySelector('.dsh-mobile-header-title');
|
|
4000
4087
|
if (!titleEl) return;
|
|
4088
|
+
// 1) 宿主当前会话标题(最准确,随切换实时更新)
|
|
4089
|
+
const hostTitle = readHostSessionTitle();
|
|
4090
|
+
if (hostTitle) {
|
|
4091
|
+
setTitleText(hostTitle);
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
// 2) 兜底:从 sessions.list 快照尝试(旧宿主可能有 current;新宿主空会话时回退)
|
|
4001
4095
|
const snap = ctx?.sessions?.list?.getSnapshot?.();
|
|
4002
4096
|
if (!snap) return;
|
|
4003
4097
|
const cur = snap.current ? snap.byId?.[snap.current] : null;
|
|
4004
4098
|
if (!cur || cur.blank) {
|
|
4005
|
-
|
|
4099
|
+
setTitleText(MOBILE_TITLE_FALLBACK_TEXT());
|
|
4006
4100
|
} else {
|
|
4007
|
-
|
|
4101
|
+
setTitleText(cur.displayTitle || cur.title || MOBILE_TITLE_FALLBACK_TEXT());
|
|
4008
4102
|
}
|
|
4009
4103
|
};
|
|
4010
4104
|
|
|
@@ -4012,6 +4106,19 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
4012
4106
|
if (typeof ctx?.sessions?.list?.subscribe === 'function') {
|
|
4013
4107
|
ctx.sessions.list.subscribe(syncMobileTitle);
|
|
4014
4108
|
}
|
|
4109
|
+
// 宿主会话层级面包屑是 React 渲染,切换会话/标题变化未必走 ctx.sessions.list 通知;
|
|
4110
|
+
// 补一个 MutationObserver 守护顶栏标题始终与宿主当前会话一致。
|
|
4111
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
4112
|
+
let mobileTitleRaf = 0;
|
|
4113
|
+
const mobileTitleObserver = new MutationObserver(() => {
|
|
4114
|
+
if (mobileTitleRaf) return;
|
|
4115
|
+
mobileTitleRaf = requestAnimationFrame(() => {
|
|
4116
|
+
mobileTitleRaf = 0;
|
|
4117
|
+
syncMobileTitle();
|
|
4118
|
+
});
|
|
4119
|
+
});
|
|
4120
|
+
mobileTitleObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
4121
|
+
}
|
|
4015
4122
|
if (typeof ctx?.sessions?.active?.subscribe === 'function') {
|
|
4016
4123
|
ctx.sessions.active.subscribe(() => {
|
|
4017
4124
|
if (document.body.classList.contains('dsh-drawer-open')) {
|
|
@@ -4039,7 +4146,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
4039
4146
|
if (bar && !bar.querySelector('.dsh-mobile-panel-close-btn')) {
|
|
4040
4147
|
const btn = document.createElement('button');
|
|
4041
4148
|
btn.className = 'dsh-mobile-panel-close-btn';
|
|
4042
|
-
btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg><span
|
|
4149
|
+
btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg><span>${localeT('panel.backToChat')}</span>`;
|
|
4043
4150
|
btn.onclick = (e) => {
|
|
4044
4151
|
e.stopPropagation();
|
|
4045
4152
|
document.body.classList.remove('dsh-workbench-open');
|
|
@@ -5111,7 +5218,7 @@ function setupComposerCollapse() {
|
|
|
5111
5218
|
if (!utils) return null;
|
|
5112
5219
|
const btn = document.createElement('button');
|
|
5113
5220
|
btn.className = 'dsh-header-fold-btn';
|
|
5114
|
-
btn.setAttribute('aria-label', '
|
|
5221
|
+
btn.setAttribute('aria-label', `${localeT('composer.expand')} / ${localeT('composer.collapse')}`);
|
|
5115
5222
|
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="18" x2="21" y2="18"></line><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
5116
5223
|
// 插到 session 下载按钮左侧(紧邻原生工具钮)
|
|
5117
5224
|
if (logBtn && logBtn.parentElement === utils) utils.insertBefore(btn, logBtn);
|
|
@@ -5128,7 +5235,7 @@ function setupComposerCollapse() {
|
|
|
5128
5235
|
if (!scrollBody) return null;
|
|
5129
5236
|
bar = document.createElement('div');
|
|
5130
5237
|
bar.className = 'dsh-composer-collapsed-bar';
|
|
5131
|
-
bar.textContent = '
|
|
5238
|
+
bar.textContent = localeT('composer.placeholder');
|
|
5132
5239
|
scrollBody.insertBefore(bar, seat);
|
|
5133
5240
|
bar.addEventListener('click', () => setCollapsed(false));
|
|
5134
5241
|
return bar;
|
|
@@ -5153,7 +5260,7 @@ function setupComposerCollapse() {
|
|
|
5153
5260
|
const updateButton = (collapsed) => {
|
|
5154
5261
|
const btn = getFoldBtn();
|
|
5155
5262
|
if (!btn) return;
|
|
5156
|
-
btn.title = collapsed ? '
|
|
5263
|
+
btn.title = collapsed ? localeT('composer.expand') : localeT('composer.collapse');
|
|
5157
5264
|
const icon = collapsed
|
|
5158
5265
|
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"></rect><line x1="6" y1="10" x2="6.01" y2="10"></line><line x1="10" y1="10" x2="10.01" y2="10"></line><line x1="14" y1="10" x2="14.01" y2="10"></line><line x1="6" y1="14" x2="10" y2="14"></line><line x1="14" y1="14" x2="18" y2="14"></line></svg>'
|
|
5159
5266
|
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="18" x2="21" y2="18"></line><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
@@ -5220,6 +5327,11 @@ function setupComposerCollapse() {
|
|
|
5220
5327
|
|
|
5221
5328
|
function apply(ctx) {
|
|
5222
5329
|
window.__dshClientCtx = ctx;
|
|
5330
|
+
// 国际化:创建翻译器(跟随 DSH 主体语言,读 <html lang>;有 locale 服务则顺带注册
|
|
5331
|
+
// 词典,拿不到/被门控也不抛错)。注:locale 服务不能声明进 inject——动态插件声明
|
|
5332
|
+
// 会导致激活等待(宿主 env 无该服务,entry did not activate)。
|
|
5333
|
+
const intl = createTranslator(ctx);
|
|
5334
|
+
localeT = (key, vars) => intl.t(key, vars);
|
|
5223
5335
|
// 放在最前:交互层先就绪,后续任何初始化抛异常都不会留下「CSS 生效但监听器缺失」
|
|
5224
5336
|
// 的状态(CSS 6.2 块另有 <html> 就绪开关双重兜底)。
|
|
5225
5337
|
setupSettingsDrilldown();
|