@wenbin_wb/dsh-bridge 2.11.2 → 2.11.3
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 +21 -0
- package/client/client.js +186 -8
- package/client/index.js +70 -8
- package/client/locales.mjs +174 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v2.11.3] - 2026-10-02
|
|
8
|
+
|
|
9
|
+
> 本版为移动端修复 + 国际化版:修复顶栏会话标题恒为「新会话」,并让插件 UI 与显示名称跟随 DSH 主体语言(中/英)。
|
|
10
|
+
|
|
11
|
+
### 🐞 修复
|
|
12
|
+
|
|
13
|
+
- **移动端顶栏会话标题恒为「新会话」**:DSH 0.1.7 起 `sessions.list` 快照不再有 `current` 字段,旧逻辑取不到当前会话。现改为读宿主「会话层级」面包屑(`Session hierarchy`)的最后一级——与 PC 顶栏同源、宿主实时维护,空会话回退「新会话」;并用 MutationObserver 守护切换同步(写前比对避免自循环)。
|
|
14
|
+
|
|
15
|
+
### ✨ 新功能
|
|
16
|
+
|
|
17
|
+
- **移动端自绘 UI 文案国际化**:顶栏按钮(打开菜单/新建会话)、标题回退、输入区提示、返回对话、收起/展开输入框等文案跟随 DSH 主体语言(`<html lang>` 同步,中/英词典见 `client/locales.mjs`)。
|
|
18
|
+
- **插件显示名称本地化**:新增 `locale/en.json` 与 `locale/zh.json`(`meta.title`/`meta.description`),插件管理页与市场按当前语言显示本插件名称(如 "DSH Bridge — Remote Access & Security" / "DSH Bridge — 远程访问与安全守护")。
|
|
19
|
+
|
|
20
|
+
### 🧪 测试
|
|
21
|
+
|
|
22
|
+
- 新增「移动端顶栏标题读取宿主会话层级面包屑(中英文锚点)」结构断言、「客户端国际化(locales.mjs 中英键一致)」与「插件管理页本地化(locale/*.json 存在且 exports 暴露)」断言;`test/browser/mobile-sidebar-drawer.mjs` 新增「顶栏标题跟随当前会话」浏览器断言。全量 343 项测试 100% 通过。
|
|
23
|
+
|
|
24
|
+
> ⚠️ 生效说明:客户端文案刷新页面即生效;插件管理页显示名称在 dsh 重启后完全生效(元数据由宿主进程读取)。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
7
28
|
## [v2.11.2] - 2026-10-02
|
|
8
29
|
|
|
9
30
|
> 本版为移动端修复版:解决英文界面下侧边栏(抽屉)点击后空白,以及 DSH 0.1.7 升级后右侧栏点击后不显示的问题。
|
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 = {
|
|
@@ -1758,6 +1900,7 @@ var RELEASES_URL = "https://github.com/wenbin-wb/dsh-bridge/releases";
|
|
|
1758
1900
|
var ISSUES_URL = "https://github.com/wenbin-wb/dsh-bridge/issues/new";
|
|
1759
1901
|
var TUNNEL_DOCS_URL = "https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/custom-tunnel.md";
|
|
1760
1902
|
var CLOUDFLARE_TUTORIAL_URL = "https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/cloudflare-fixed-domain.md";
|
|
1903
|
+
var localeT = (key) => key;
|
|
1761
1904
|
function upgradeCommands(latest) {
|
|
1762
1905
|
const spec = `@wenbin_wb/dsh-bridge@${latest}`;
|
|
1763
1906
|
return [
|
|
@@ -5909,7 +6052,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
5909
6052
|
header.className = "dsh-mobile-app-header";
|
|
5910
6053
|
const leftBtn = document.createElement("button");
|
|
5911
6054
|
leftBtn.className = "dsh-header-menu-btn";
|
|
5912
|
-
leftBtn.title = "
|
|
6055
|
+
leftBtn.title = localeT("menu.open");
|
|
5913
6056
|
leftBtn.innerHTML = `
|
|
5914
6057
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
|
|
5915
6058
|
<line x1="3" y1="8" x2="21" y2="8"></line>
|
|
@@ -5935,7 +6078,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
5935
6078
|
};
|
|
5936
6079
|
const rightBtn = document.createElement("button");
|
|
5937
6080
|
rightBtn.className = "dsh-header-new-btn";
|
|
5938
|
-
rightBtn.title = "
|
|
6081
|
+
rightBtn.title = localeT("menu.newSession");
|
|
5939
6082
|
rightBtn.innerHTML = `
|
|
5940
6083
|
<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
6084
|
<circle cx="12" cy="12" r="9.5"></circle>
|
|
@@ -5954,22 +6097,55 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
5954
6097
|
header.appendChild(rightBtn);
|
|
5955
6098
|
document.body.appendChild(header);
|
|
5956
6099
|
}
|
|
6100
|
+
const MOBILE_TITLE_FALLBACK_TEXT = () => localeT("title.fallback");
|
|
6101
|
+
const readHostSessionTitle = () => {
|
|
6102
|
+
if (typeof document === "undefined") return "";
|
|
6103
|
+
const nav = document.querySelector('nav[aria-label="Session hierarchy"], nav[aria-label="\u4F1A\u8BDD\u5C42\u7EA7"]');
|
|
6104
|
+
if (!nav) return "";
|
|
6105
|
+
const segs = [...nav.querySelectorAll('[class*="crumbSeg"]')];
|
|
6106
|
+
const lastSeg = segs[segs.length - 1];
|
|
6107
|
+
const lastCrumb = lastSeg ? lastSeg.querySelector('[class*="crumbCurrent"]') || lastSeg.querySelector("span, button") : null;
|
|
6108
|
+
if (!lastCrumb) return "";
|
|
6109
|
+
const text = (lastCrumb.getAttribute?.("aria-label") || lastCrumb.textContent || "").trim();
|
|
6110
|
+
if (!text || /^\d+\s+(item|items|项|个)/i.test(text)) return "";
|
|
6111
|
+
return text;
|
|
6112
|
+
};
|
|
6113
|
+
const setTitleText = (next) => {
|
|
6114
|
+
if (titleEl.textContent === next) return;
|
|
6115
|
+
titleEl.innerText = next;
|
|
6116
|
+
};
|
|
5957
6117
|
const syncMobileTitle = () => {
|
|
5958
6118
|
if (!titleEl) titleEl = document.querySelector(".dsh-mobile-header-title");
|
|
5959
6119
|
if (!titleEl) return;
|
|
6120
|
+
const hostTitle = readHostSessionTitle();
|
|
6121
|
+
if (hostTitle) {
|
|
6122
|
+
setTitleText(hostTitle);
|
|
6123
|
+
return;
|
|
6124
|
+
}
|
|
5960
6125
|
const snap = ctx?.sessions?.list?.getSnapshot?.();
|
|
5961
6126
|
if (!snap) return;
|
|
5962
6127
|
const cur = snap.current ? snap.byId?.[snap.current] : null;
|
|
5963
6128
|
if (!cur || cur.blank) {
|
|
5964
|
-
|
|
6129
|
+
setTitleText(MOBILE_TITLE_FALLBACK_TEXT());
|
|
5965
6130
|
} else {
|
|
5966
|
-
|
|
6131
|
+
setTitleText(cur.displayTitle || cur.title || MOBILE_TITLE_FALLBACK_TEXT());
|
|
5967
6132
|
}
|
|
5968
6133
|
};
|
|
5969
6134
|
syncMobileTitle();
|
|
5970
6135
|
if (typeof ctx?.sessions?.list?.subscribe === "function") {
|
|
5971
6136
|
ctx.sessions.list.subscribe(syncMobileTitle);
|
|
5972
6137
|
}
|
|
6138
|
+
if (typeof MutationObserver !== "undefined") {
|
|
6139
|
+
let mobileTitleRaf = 0;
|
|
6140
|
+
const mobileTitleObserver = new MutationObserver(() => {
|
|
6141
|
+
if (mobileTitleRaf) return;
|
|
6142
|
+
mobileTitleRaf = requestAnimationFrame(() => {
|
|
6143
|
+
mobileTitleRaf = 0;
|
|
6144
|
+
syncMobileTitle();
|
|
6145
|
+
});
|
|
6146
|
+
});
|
|
6147
|
+
mobileTitleObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
6148
|
+
}
|
|
5973
6149
|
if (typeof ctx?.sessions?.active?.subscribe === "function") {
|
|
5974
6150
|
ctx.sessions.active.subscribe(() => {
|
|
5975
6151
|
if (document.body.classList.contains("dsh-drawer-open")) {
|
|
@@ -5994,7 +6170,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
5994
6170
|
if (bar && !bar.querySelector(".dsh-mobile-panel-close-btn")) {
|
|
5995
6171
|
const btn = document.createElement("button");
|
|
5996
6172
|
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
|
|
6173
|
+
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
6174
|
btn.onclick = (e) => {
|
|
5999
6175
|
e.stopPropagation();
|
|
6000
6176
|
document.body.classList.remove("dsh-workbench-open");
|
|
@@ -6877,7 +7053,7 @@ function setupComposerCollapse() {
|
|
|
6877
7053
|
if (!utils) return null;
|
|
6878
7054
|
const btn = document.createElement("button");
|
|
6879
7055
|
btn.className = "dsh-header-fold-btn";
|
|
6880
|
-
btn.setAttribute("aria-label", "
|
|
7056
|
+
btn.setAttribute("aria-label", `${localeT("composer.expand")} / ${localeT("composer.collapse")}`);
|
|
6881
7057
|
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
7058
|
if (logBtn && logBtn.parentElement === utils) utils.insertBefore(btn, logBtn);
|
|
6883
7059
|
else utils.appendChild(btn);
|
|
@@ -6898,7 +7074,7 @@ function setupComposerCollapse() {
|
|
|
6898
7074
|
if (!scrollBody) return null;
|
|
6899
7075
|
bar = document.createElement("div");
|
|
6900
7076
|
bar.className = "dsh-composer-collapsed-bar";
|
|
6901
|
-
bar.textContent = "
|
|
7077
|
+
bar.textContent = localeT("composer.placeholder");
|
|
6902
7078
|
scrollBody.insertBefore(bar, seat);
|
|
6903
7079
|
bar.addEventListener("click", () => setCollapsed(false));
|
|
6904
7080
|
return bar;
|
|
@@ -6933,7 +7109,7 @@ function setupComposerCollapse() {
|
|
|
6933
7109
|
const updateButton = (collapsed) => {
|
|
6934
7110
|
const btn = getFoldBtn();
|
|
6935
7111
|
if (!btn) return;
|
|
6936
|
-
btn.title = collapsed ? "
|
|
7112
|
+
btn.title = collapsed ? localeT("composer.expand") : localeT("composer.collapse");
|
|
6937
7113
|
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
7114
|
const existing = btn.querySelector("svg");
|
|
6939
7115
|
if (!existing || existing.outerHTML !== icon) btn.innerHTML = icon;
|
|
@@ -6996,6 +7172,8 @@ function setupComposerCollapse() {
|
|
|
6996
7172
|
}
|
|
6997
7173
|
function apply(ctx) {
|
|
6998
7174
|
window.__dshClientCtx = ctx;
|
|
7175
|
+
const intl = createTranslator(ctx);
|
|
7176
|
+
localeT = (key, vars) => intl.t(key, vars);
|
|
6999
7177
|
setupSettingsDrilldown();
|
|
7000
7178
|
if (typeof ctx.effect === "function") {
|
|
7001
7179
|
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
|
|
@@ -3939,7 +3947,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3939
3947
|
// 左侧双横线菜单按钮 (DeepSeek App 原生图标):联动展开 DSH 原生侧边栏会话列表
|
|
3940
3948
|
const leftBtn = document.createElement('button');
|
|
3941
3949
|
leftBtn.className = 'dsh-header-menu-btn';
|
|
3942
|
-
leftBtn.title = '
|
|
3950
|
+
leftBtn.title = localeT('menu.open');
|
|
3943
3951
|
leftBtn.innerHTML = `
|
|
3944
3952
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round">
|
|
3945
3953
|
<line x1="3" y1="8" x2="21" y2="8"></line>
|
|
@@ -3972,7 +3980,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3972
3980
|
// 右侧 (+) 新建会话按钮 (DeepSeek App 圆形加号风格)
|
|
3973
3981
|
const rightBtn = document.createElement('button');
|
|
3974
3982
|
rightBtn.className = 'dsh-header-new-btn';
|
|
3975
|
-
rightBtn.title = '
|
|
3983
|
+
rightBtn.title = localeT('menu.newSession');
|
|
3976
3984
|
rightBtn.innerHTML = `
|
|
3977
3985
|
<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
3986
|
<circle cx="12" cy="12" r="9.5"></circle>
|
|
@@ -3995,16 +4003,52 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
3995
4003
|
}
|
|
3996
4004
|
|
|
3997
4005
|
// 绑定会话标题实时同步 (切换会话或收到首条回复自动更新)
|
|
4006
|
+
//
|
|
4007
|
+
// 标题来源优先取宿主「会话层级」面包屑(PC 顶栏同源):宿主自己维护当前会话标题,
|
|
4008
|
+
// 随会话切换实时更新,0.1.7 的 sessions.list 快照没有 current 字段(旧方案取不到 →
|
|
4009
|
+
// 顶栏永远显示「新会话」)。crumbs 的 nav 带稳定 aria-label(中英双语),最后一级
|
|
4010
|
+
// crumb 就是当前会话标题;空会话时无 crumbs,回退「新会话」。
|
|
4011
|
+
const MOBILE_TITLE_FALLBACK_TEXT = () => localeT('title.fallback');
|
|
4012
|
+
// 取宿主会话层级面包屑的「当前会话标题」。稳定锚点是 nav 的 aria-label(中英双语);
|
|
4013
|
+
// 标题是最后一个 crumbSeg 里的「crumb(当前段)」,不是 nav 里任意最后一个 span——
|
|
4014
|
+
// 子 agent 会话会把末级 crumb 换成 dropdown(内含计数 span),取最后一个 span 会拿到
|
|
4015
|
+
// 计数文本。crumb 本身是 span/button 且带 crumbCurrent 类;取不到则回退空串。
|
|
4016
|
+
const readHostSessionTitle = () => {
|
|
4017
|
+
if (typeof document === 'undefined') return '';
|
|
4018
|
+
const nav = document.querySelector('nav[aria-label="Session hierarchy"], nav[aria-label="会话层级"]');
|
|
4019
|
+
if (!nav) return '';
|
|
4020
|
+
const segs = [...nav.querySelectorAll('[class*="crumbSeg"]')];
|
|
4021
|
+
const lastSeg = segs[segs.length - 1];
|
|
4022
|
+
const lastCrumb = lastSeg
|
|
4023
|
+
? lastSeg.querySelector('[class*="crumbCurrent"]') || lastSeg.querySelector('span, button')
|
|
4024
|
+
: null;
|
|
4025
|
+
if (!lastCrumb) return '';
|
|
4026
|
+
const text = ((lastCrumb.getAttribute?.('aria-label') || lastCrumb.textContent) || '').trim();
|
|
4027
|
+
// 非标题节点(如子 agent 计数 "2 items"/"2 项")不应被当作标题
|
|
4028
|
+
if (!text || /^\d+\s+(item|items|项|个)/i.test(text)) return '';
|
|
4029
|
+
return text;
|
|
4030
|
+
};
|
|
4031
|
+
const setTitleText = (next) => {
|
|
4032
|
+
if (titleEl.textContent === next) return; // 写前比对:避免每帧重写触发 observer 自循环
|
|
4033
|
+
titleEl.innerText = next;
|
|
4034
|
+
};
|
|
3998
4035
|
const syncMobileTitle = () => {
|
|
3999
4036
|
if (!titleEl) titleEl = document.querySelector('.dsh-mobile-header-title');
|
|
4000
4037
|
if (!titleEl) return;
|
|
4038
|
+
// 1) 宿主当前会话标题(最准确,随切换实时更新)
|
|
4039
|
+
const hostTitle = readHostSessionTitle();
|
|
4040
|
+
if (hostTitle) {
|
|
4041
|
+
setTitleText(hostTitle);
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
// 2) 兜底:从 sessions.list 快照尝试(旧宿主可能有 current;新宿主空会话时回退)
|
|
4001
4045
|
const snap = ctx?.sessions?.list?.getSnapshot?.();
|
|
4002
4046
|
if (!snap) return;
|
|
4003
4047
|
const cur = snap.current ? snap.byId?.[snap.current] : null;
|
|
4004
4048
|
if (!cur || cur.blank) {
|
|
4005
|
-
|
|
4049
|
+
setTitleText(MOBILE_TITLE_FALLBACK_TEXT());
|
|
4006
4050
|
} else {
|
|
4007
|
-
|
|
4051
|
+
setTitleText(cur.displayTitle || cur.title || MOBILE_TITLE_FALLBACK_TEXT());
|
|
4008
4052
|
}
|
|
4009
4053
|
};
|
|
4010
4054
|
|
|
@@ -4012,6 +4056,19 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
4012
4056
|
if (typeof ctx?.sessions?.list?.subscribe === 'function') {
|
|
4013
4057
|
ctx.sessions.list.subscribe(syncMobileTitle);
|
|
4014
4058
|
}
|
|
4059
|
+
// 宿主会话层级面包屑是 React 渲染,切换会话/标题变化未必走 ctx.sessions.list 通知;
|
|
4060
|
+
// 补一个 MutationObserver 守护顶栏标题始终与宿主当前会话一致。
|
|
4061
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
4062
|
+
let mobileTitleRaf = 0;
|
|
4063
|
+
const mobileTitleObserver = new MutationObserver(() => {
|
|
4064
|
+
if (mobileTitleRaf) return;
|
|
4065
|
+
mobileTitleRaf = requestAnimationFrame(() => {
|
|
4066
|
+
mobileTitleRaf = 0;
|
|
4067
|
+
syncMobileTitle();
|
|
4068
|
+
});
|
|
4069
|
+
});
|
|
4070
|
+
mobileTitleObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
4071
|
+
}
|
|
4015
4072
|
if (typeof ctx?.sessions?.active?.subscribe === 'function') {
|
|
4016
4073
|
ctx.sessions.active.subscribe(() => {
|
|
4017
4074
|
if (document.body.classList.contains('dsh-drawer-open')) {
|
|
@@ -4039,7 +4096,7 @@ function setupMobileExperience(rpcCall, ctx) {
|
|
|
4039
4096
|
if (bar && !bar.querySelector('.dsh-mobile-panel-close-btn')) {
|
|
4040
4097
|
const btn = document.createElement('button');
|
|
4041
4098
|
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
|
|
4099
|
+
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
4100
|
btn.onclick = (e) => {
|
|
4044
4101
|
e.stopPropagation();
|
|
4045
4102
|
document.body.classList.remove('dsh-workbench-open');
|
|
@@ -5111,7 +5168,7 @@ function setupComposerCollapse() {
|
|
|
5111
5168
|
if (!utils) return null;
|
|
5112
5169
|
const btn = document.createElement('button');
|
|
5113
5170
|
btn.className = 'dsh-header-fold-btn';
|
|
5114
|
-
btn.setAttribute('aria-label', '
|
|
5171
|
+
btn.setAttribute('aria-label', `${localeT('composer.expand')} / ${localeT('composer.collapse')}`);
|
|
5115
5172
|
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
5173
|
// 插到 session 下载按钮左侧(紧邻原生工具钮)
|
|
5117
5174
|
if (logBtn && logBtn.parentElement === utils) utils.insertBefore(btn, logBtn);
|
|
@@ -5128,7 +5185,7 @@ function setupComposerCollapse() {
|
|
|
5128
5185
|
if (!scrollBody) return null;
|
|
5129
5186
|
bar = document.createElement('div');
|
|
5130
5187
|
bar.className = 'dsh-composer-collapsed-bar';
|
|
5131
|
-
bar.textContent = '
|
|
5188
|
+
bar.textContent = localeT('composer.placeholder');
|
|
5132
5189
|
scrollBody.insertBefore(bar, seat);
|
|
5133
5190
|
bar.addEventListener('click', () => setCollapsed(false));
|
|
5134
5191
|
return bar;
|
|
@@ -5153,7 +5210,7 @@ function setupComposerCollapse() {
|
|
|
5153
5210
|
const updateButton = (collapsed) => {
|
|
5154
5211
|
const btn = getFoldBtn();
|
|
5155
5212
|
if (!btn) return;
|
|
5156
|
-
btn.title = collapsed ? '
|
|
5213
|
+
btn.title = collapsed ? localeT('composer.expand') : localeT('composer.collapse');
|
|
5157
5214
|
const icon = collapsed
|
|
5158
5215
|
? '<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
5216
|
: '<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 +5277,11 @@ function setupComposerCollapse() {
|
|
|
5220
5277
|
|
|
5221
5278
|
function apply(ctx) {
|
|
5222
5279
|
window.__dshClientCtx = ctx;
|
|
5280
|
+
// 国际化:创建翻译器(跟随 DSH 主体语言,读 <html lang>;有 locale 服务则顺带注册
|
|
5281
|
+
// 词典,拿不到/被门控也不抛错)。注:locale 服务不能声明进 inject——动态插件声明
|
|
5282
|
+
// 会导致激活等待(宿主 env 无该服务,entry did not activate)。
|
|
5283
|
+
const intl = createTranslator(ctx);
|
|
5284
|
+
localeT = (key, vars) => intl.t(key, vars);
|
|
5223
5285
|
// 放在最前:交互层先就绪,后续任何初始化抛异常都不会留下「CSS 生效但监听器缺失」
|
|
5224
5286
|
// 的状态(CSS 6.2 块另有 <html> 就绪开关双重兜底)。
|
|
5225
5287
|
setupSettingsDrilldown();
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// dsh-bridge 客户端国际化:跟随 DSH 主体语言(LocaleFace)。
|
|
2
|
+
//
|
|
3
|
+
// DSH 0.1.7 的 client 提供 locale 服务(`dsh-client-locale` 的 LocaleFace):
|
|
4
|
+
// - ctx.locale.register(namespace, { zh: {...}, en: {...} }) 注册词典;
|
|
5
|
+
// - ctx.locale.translate(map) 按当前语言取译文(纯字符串原样返回、map 走词典,
|
|
6
|
+
// 未声明语言回退到 fallback 链,最终到 en);
|
|
7
|
+
// - ctx.locale.subscribe(fn) 在语言切换时通知。
|
|
8
|
+
// 插件把 "locale" 加进 inject 后即可拿到 ctx.locale,从而自动跟随 DSH 主体语言
|
|
9
|
+
// (含用户手动切换、浏览器语言回退、<html lang> 同步),无需自建语言开关。
|
|
10
|
+
//
|
|
11
|
+
// 本文件只收「移动端自绘 UI 的用户可见文案」(A 类):顶栏按钮、工作区选择器、
|
|
12
|
+
// 远程访问面板、浏览器输入区提示等。内部字符串(DOM/CSS 类名匹配、注释、日志)
|
|
13
|
+
// 保持中文原样——它们不是 UI 文案,翻译会破坏匹配(见 readHostSessionTitle 教训)。
|
|
14
|
+
|
|
15
|
+
export const LOCALE_NAMESPACE = '@wenbin_wb/dsh-bridge';
|
|
16
|
+
|
|
17
|
+
/** 词典:每个命名空间至少要有 en(最终 fallback);zh 为默认语言。 */
|
|
18
|
+
export const zh = {
|
|
19
|
+
// 移动端顶栏
|
|
20
|
+
'menu.open': '打开菜单',
|
|
21
|
+
'menu.newSession': '新建会话',
|
|
22
|
+
'title.fallback': '新会话',
|
|
23
|
+
// 移动端输入区
|
|
24
|
+
'composer.placeholder': '✍️ 点击输入消息…',
|
|
25
|
+
'composer.expand': '展开输入框',
|
|
26
|
+
'composer.collapse': '收起输入框,最大化对话阅读区',
|
|
27
|
+
'panel.backToChat': '返回对话',
|
|
28
|
+
// 移动端设置面板 / 抽屉(dsh-bridge 自绘部分)
|
|
29
|
+
'workspace.choose': '选择电脑工作区',
|
|
30
|
+
'workspace.reading': '正在读取目录内容…',
|
|
31
|
+
'workspace.currentDir': '当前目录:',
|
|
32
|
+
'workspace.registeredHint': '已注册工作区 (${count} 个,点击直接切换):',
|
|
33
|
+
'workspace.enterHint': '点击进入文件夹,或点击「+ 选为工作区」直接添加并切换',
|
|
34
|
+
'workspace.unlockHint': '远程访问时浏览/添加工作区需输入后台管理密码解锁(与访问密码不同)。',
|
|
35
|
+
'workspace.chooseFolder': '选择工作区',
|
|
36
|
+
'workspace.addFolder': '添加',
|
|
37
|
+
'workspace.refresh': '刷新目录',
|
|
38
|
+
'workspace.back': '返回上一级',
|
|
39
|
+
'workspace.filter': '过滤子文件夹…',
|
|
40
|
+
'workspace.more': '还有更多',
|
|
41
|
+
'workspace.addSubfolder': '直接添加此子文件夹为工作区并进入',
|
|
42
|
+
'workspace.onlyAdmin': '仅限本机管理',
|
|
43
|
+
'workspace.isCurrent': '当前主入口',
|
|
44
|
+
'workspace.isFallback': '主入口',
|
|
45
|
+
// 远程访问面板
|
|
46
|
+
'access.copyLink': '复制链接',
|
|
47
|
+
'access.copied': '✓ 已复制',
|
|
48
|
+
'access.hideQr': '隐藏二维码',
|
|
49
|
+
'access.showQr': '显示二维码',
|
|
50
|
+
'access.privateEnv': '请在私密环境下使用',
|
|
51
|
+
'access.switching': '切换中…',
|
|
52
|
+
'access.publicEntry': '公网访问入口',
|
|
53
|
+
'access.copy': '复制',
|
|
54
|
+
'access.stopReconnect': '停止重连',
|
|
55
|
+
'access.close': '关闭',
|
|
56
|
+
'access.connecting': '连接中…',
|
|
57
|
+
'access.downloading': '下载中…',
|
|
58
|
+
'access.enable': '开启公网隧道',
|
|
59
|
+
'access.scanHint': '请在私密环境下扫码使用',
|
|
60
|
+
'access.autoStart': '随 DSH 启动自动开启',
|
|
61
|
+
'access.tunnelConfig': '隧道配置',
|
|
62
|
+
'access.expand': '展开 ▾',
|
|
63
|
+
'access.collapse': '收起 ▴',
|
|
64
|
+
'access.saveError': '保存配置失败',
|
|
65
|
+
'access.serverConfig': '隧道服务器配置',
|
|
66
|
+
'access.saving': '保存中…',
|
|
67
|
+
'access.saved': '✓ 已保存',
|
|
68
|
+
'access.saveConfig': '保存配置',
|
|
69
|
+
'access.clear': '清除',
|
|
70
|
+
'access.externalTunnel': '外部已部署隧道',
|
|
71
|
+
'access.saveFixed': '保存固定域名配置',
|
|
72
|
+
'access.updateFailed': '更新失败',
|
|
73
|
+
'access.securityHint': '设置外部访客访问密码',
|
|
74
|
+
'access.goSetup': '去开启 ➔',
|
|
75
|
+
'access.gotoSettings': '设置 ➔',
|
|
76
|
+
'access.tutorial': '查看自建隧道服务器搭建教程',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const en = {
|
|
80
|
+
'menu.open': 'Open menu',
|
|
81
|
+
'menu.newSession': 'New session',
|
|
82
|
+
'title.fallback': 'New Session',
|
|
83
|
+
'composer.placeholder': '✍️ Tap to type a message…',
|
|
84
|
+
'composer.expand': 'Expand input',
|
|
85
|
+
'composer.collapse': 'Collapse input to maximize chat view',
|
|
86
|
+
'panel.backToChat': 'Back to chat',
|
|
87
|
+
'workspace.choose': 'Choose a workspace',
|
|
88
|
+
'workspace.reading': 'Reading directory…',
|
|
89
|
+
'workspace.currentDir': 'Current directory:',
|
|
90
|
+
'workspace.registeredHint': 'Registered workspaces (${count}, click to switch):',
|
|
91
|
+
'workspace.enterHint': 'Enter a folder, or tap “+ Select as workspace” to add & switch',
|
|
92
|
+
'workspace.unlockHint': 'Browsing/adding workspaces remotely requires the admin password to unlock (different from the access password).',
|
|
93
|
+
'workspace.chooseFolder': 'Choose workspace',
|
|
94
|
+
'workspace.addFolder': 'Add',
|
|
95
|
+
'workspace.refresh': 'Refresh',
|
|
96
|
+
'workspace.back': 'Back',
|
|
97
|
+
'workspace.filter': 'Filter subfolders…',
|
|
98
|
+
'workspace.more': 'More',
|
|
99
|
+
'workspace.addSubfolder': 'Add this subfolder as a workspace and enter',
|
|
100
|
+
'workspace.onlyAdmin': 'Local admin only',
|
|
101
|
+
'workspace.isCurrent': 'Current entry',
|
|
102
|
+
'workspace.isFallback': 'Main entry',
|
|
103
|
+
'access.copyLink': 'Copy link',
|
|
104
|
+
'access.copied': '✓ Copied',
|
|
105
|
+
'access.hideQr': 'Hide QR',
|
|
106
|
+
'access.showQr': 'Show QR',
|
|
107
|
+
'access.privateEnv': 'Use in a private environment',
|
|
108
|
+
'access.switching': 'Switching…',
|
|
109
|
+
'access.publicEntry': 'Public access entry',
|
|
110
|
+
'access.copy': 'Copy',
|
|
111
|
+
'access.stopReconnect': 'Stop reconnecting',
|
|
112
|
+
'access.close': 'Close',
|
|
113
|
+
'access.connecting': 'Connecting…',
|
|
114
|
+
'access.downloading': 'Downloading…',
|
|
115
|
+
'access.enable': 'Enable public tunnel',
|
|
116
|
+
'access.scanHint': 'Scan in a private environment',
|
|
117
|
+
'access.autoStart': 'Start automatically with DSH',
|
|
118
|
+
'access.tunnelConfig': 'Tunnel configuration',
|
|
119
|
+
'access.expand': 'Expand ▾',
|
|
120
|
+
'access.collapse': 'Collapse ▴',
|
|
121
|
+
'access.saveError': 'Failed to save configuration',
|
|
122
|
+
'access.serverConfig': 'Tunnel server configuration',
|
|
123
|
+
'access.saving': 'Saving…',
|
|
124
|
+
'access.saved': '✓ Saved',
|
|
125
|
+
'access.saveConfig': 'Save configuration',
|
|
126
|
+
'access.clear': 'Clear',
|
|
127
|
+
'access.externalTunnel': 'External tunnel',
|
|
128
|
+
'access.saveFixed': 'Save fixed-domain configuration',
|
|
129
|
+
'access.updateFailed': 'Update failed',
|
|
130
|
+
'access.securityHint': 'Set an external visitor access password',
|
|
131
|
+
'access.goSetup': 'Set up ➔',
|
|
132
|
+
'access.gotoSettings': 'Settings ➔',
|
|
133
|
+
'access.tutorial': 'View the self-hosted tunnel server tutorial',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 构造一个翻译函数:优先用宿主的 ctx.locale.translate(跟随主体语言),
|
|
138
|
+
* 宿主不支持时退化为「取当前语言词典(en/zh)+ 互为兜底」。
|
|
139
|
+
* @param {object} ctx - Cordis 客户端上下文(含可选 locale 服务)
|
|
140
|
+
* @returns {{ t: Function, active: () => string, subscribe: Function }}
|
|
141
|
+
*/
|
|
142
|
+
/**
|
|
143
|
+
* 构造翻译器:读 DSH 主体语言(`<html lang>`,宿主 LocaleFace 实时同步),
|
|
144
|
+
* 中英词典按当前语言选词。`ctx.locale` 只作可选的词典注册(拿到则注册,
|
|
145
|
+
* 拿不到或未声明 inject 也不抛错、不影响翻译——动态插件无法把 locale 加进
|
|
146
|
+
* inject,宿主会对未声明服务抛 denyRead,详见 index.js 注释)。
|
|
147
|
+
* @param {object} ctx - Cordis 客户端上下文(可选)
|
|
148
|
+
* @returns {{ t: Function, active: () => string }}
|
|
149
|
+
*/
|
|
150
|
+
export function createTranslator(ctx) {
|
|
151
|
+
// 当前语言:宿主把 active 语言同步到 <html lang>(zh→zh-CN,其余原样)。
|
|
152
|
+
// 读它即可跟随 DSH 主体语言且无需声明 locale 服务。
|
|
153
|
+
const active = () => {
|
|
154
|
+
const lang = (typeof document !== 'undefined' ? document.documentElement.getAttribute('lang') : '') || '';
|
|
155
|
+
return lang.toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
156
|
+
};
|
|
157
|
+
const entryOf = (key) => ({ zh: zh[key], en: en[key] });
|
|
158
|
+
const t = (key, vars) => {
|
|
159
|
+
const e = entryOf(key);
|
|
160
|
+
let text = e[active()] ?? e.en ?? e.zh ?? '';
|
|
161
|
+
if (vars && text) {
|
|
162
|
+
for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`\${${k}}`, String(v));
|
|
163
|
+
}
|
|
164
|
+
return text;
|
|
165
|
+
};
|
|
166
|
+
// 可选:向宿主注册词典(仅当其有 register 且可访问时;失败无妨)
|
|
167
|
+
try {
|
|
168
|
+
const locale = ctx && (typeof ctx.get === 'function' ? ctx.get('locale') : ctx.locale);
|
|
169
|
+
if (locale && typeof locale.register === 'function') {
|
|
170
|
+
locale.register(LOCALE_NAMESPACE, { zh, en });
|
|
171
|
+
}
|
|
172
|
+
} catch { /* 动态插件的 locale 服务被宿主门控时忽略 */ }
|
|
173
|
+
return { t, active };
|
|
174
|
+
}
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "DSH Bridge — Remote Access & Security",
|
|
4
|
+
"description": "Scan the QR code to keep using DeepSeek Harness on mobile or public networks when you're away from your computer. One-tap LAN QR code, Cloudflare public tunnel, self-hosted tunnel, and WeChat / QQ / Lark / Telegram bots (multi-workspace / session persistence / media / card approval / streaming output), no public server required."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenbin_wb/dsh-bridge",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.3",
|
|
4
4
|
"description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
|
|
5
|
-
"releaseNotes": "【v2.11.
|
|
5
|
+
"releaseNotes": "【v2.11.3】移动端顶栏标题修复 + 国际化(跟随 DSH 主体语言)\n• 修复:移动端顶栏会话标题恒为「新会话」——0.1.7 起会话快照无 current 字段,改为读宿主会话层级面包屑(PC 顶栏同源)\n• 新功能:移动端自绘 UI 文案国际化,随 DSH 主体语言切换(中/英)\n• 新功能:插件管理页/市场显示本插件的本地化名称(locale/en.json + zh.json)\n• 说明:纯前端改动 + 包元数据,刷新页面即生效",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"exports": {
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
"default": "./lib/index.js"
|
|
11
11
|
},
|
|
12
12
|
"./client": "./client/client.js",
|
|
13
|
+
"./locale/*": "./locale/*",
|
|
13
14
|
"./package.json": "./package.json"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"lib",
|
|
17
18
|
"client",
|
|
19
|
+
"locale",
|
|
18
20
|
"cordis.patch.yml",
|
|
19
21
|
"README.md",
|
|
20
22
|
"README.en.md",
|