@wenbin_wb/dsh-bridge 2.10.7 → 2.10.9
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 +26 -0
- package/README.en.md +27 -0
- package/README.md +27 -0
- package/client/client.js +388 -5
- package/client/index.js +327 -3
- package/client/mobile-styles.js +68 -0
- package/docs/telegram-usage.md +1 -1
- package/lib/auth/dsh-native-cookie.js +148 -0
- package/lib/auth/manager.js +54 -4
- package/lib/bridge-rpc-constants.js +1 -0
- package/lib/bridge-rpc.js +17 -2
- package/lib/compat.js +129 -129
- package/lib/connection-compat.js +115 -0
- package/lib/feishu/index.js +225 -225
- package/lib/feishu/node.js +439 -439
- package/lib/index.js +163 -28
- package/lib/platform/base.js +147 -147
- package/lib/platform/commands.js +221 -221
- package/lib/platform/conversation-bridge.js +821 -821
- package/lib/platform/dsh-storage.js +117 -117
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +229 -229
- package/lib/platform/session-catalog.js +372 -372
- package/lib/platform/stream-slices.js +21 -21
- package/lib/qq/index.js +312 -312
- package/lib/telegram/index.js +216 -216
- package/lib/telegram/node.js +322 -322
- package/lib/wechat/gateway.js +986 -986
- package/lib/wechat/index.js +244 -244
- package/lib/wechat/media.js +285 -285
- package/lib/wechat/node.js +352 -352
- package/package.json +2 -2
package/client/index.js
CHANGED
|
@@ -2397,6 +2397,8 @@ function VersionBanner({ rpcCall }) {
|
|
|
2397
2397
|
|
|
2398
2398
|
const hasUpdate = info?.latest && info?.current && !info.error && semverGt(info.latest, info.current);
|
|
2399
2399
|
const isLatest = info?.latest && info?.current && !info.error && !semverGt(info.latest, info.current);
|
|
2400
|
+
// DSH 宿主更新状态:有线上新版 + 当前版本可比较
|
|
2401
|
+
const dshHasUpdate = !!(info?.dshLatest && info?.dshVersion && !info.error && semverGt(info.dshLatest, info.dshVersion));
|
|
2400
2402
|
|
|
2401
2403
|
const handleUpgrade = React.useCallback(async () => {
|
|
2402
2404
|
if (!info?.latest || upgrading) return;
|
|
@@ -2420,6 +2422,30 @@ function VersionBanner({ rpcCall }) {
|
|
|
2420
2422
|
}
|
|
2421
2423
|
}, [info?.latest, upgrading, rpcCall]);
|
|
2422
2424
|
|
|
2425
|
+
// DSH 宿主一键升级:仅当服务端判定可自动升级(npm 全局安装)时按钮才会出现
|
|
2426
|
+
const [dshUpgrading, setDshUpgrading] = React.useState(false);
|
|
2427
|
+
const [dshUpgradeResult, setDshUpgradeResult] = React.useState(null);
|
|
2428
|
+
const handleUpgradeDsh = React.useCallback(async () => {
|
|
2429
|
+
if (!info?.dshLatest || dshUpgrading) return;
|
|
2430
|
+
setDshUpgrading(true);
|
|
2431
|
+
setDshUpgradeResult(null);
|
|
2432
|
+
setDismissRestart(false);
|
|
2433
|
+
resetRestartStatus();
|
|
2434
|
+
try {
|
|
2435
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.upgradeDsh, { version: info.dshLatest });
|
|
2436
|
+
if (r?.ok && r.value?.ok) {
|
|
2437
|
+
setDshUpgradeResult({ ok: true, message: `DSH 已成功升级到 v${info.dshLatest}!` });
|
|
2438
|
+
} else {
|
|
2439
|
+
const msg = r?.value?.error || r?.error?.message || '升级失败';
|
|
2440
|
+
setDshUpgradeResult({ ok: false, message: msg, manual: true });
|
|
2441
|
+
}
|
|
2442
|
+
} catch (e) {
|
|
2443
|
+
setDshUpgradeResult({ ok: false, message: e.message || '升级请求失败', manual: true });
|
|
2444
|
+
} finally {
|
|
2445
|
+
setDshUpgrading(false);
|
|
2446
|
+
}
|
|
2447
|
+
}, [info?.dshLatest, dshUpgrading, rpcCall]);
|
|
2448
|
+
|
|
2423
2449
|
const links = React.createElement('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
|
|
2424
2450
|
React.createElement('a', {
|
|
2425
2451
|
href: GITHUB_URL, target: '_blank', rel: 'noreferrer', style: s.btnLink,
|
|
@@ -2478,12 +2504,16 @@ function VersionBanner({ rpcCall }) {
|
|
|
2478
2504
|
hasUpdate && React.createElement('span', { style: { fontWeight: 600, fontSize: 11 } }, `➔ v${info.latest}`),
|
|
2479
2505
|
info?.error && React.createElement('span', { style: { color: 'var(--dsw-alias-state-warn-primary,#d97706)', fontSize: 11 } }, '(网络超时)'),
|
|
2480
2506
|
),
|
|
2481
|
-
// DSH
|
|
2507
|
+
// DSH 宿主版本标签(有新版时黄色高亮)
|
|
2482
2508
|
info?.dshVersion && React.createElement('span', {
|
|
2483
2509
|
style: {
|
|
2484
2510
|
...s.tag,
|
|
2485
|
-
background:
|
|
2486
|
-
|
|
2511
|
+
background: dshHasUpdate
|
|
2512
|
+
? 'var(--dsw-alias-state-warn-bg,#fffbeb)'
|
|
2513
|
+
: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
|
|
2514
|
+
color: dshHasUpdate
|
|
2515
|
+
? 'var(--dsw-alias-state-warn-primary,#d97706)'
|
|
2516
|
+
: 'var(--dsw-alias-label-tertiary,#6b7280)',
|
|
2487
2517
|
padding: '3px 10px',
|
|
2488
2518
|
fontSize: 12,
|
|
2489
2519
|
fontWeight: 500,
|
|
@@ -2491,9 +2521,11 @@ function VersionBanner({ rpcCall }) {
|
|
|
2491
2521
|
alignItems: 'center',
|
|
2492
2522
|
gap: 5,
|
|
2493
2523
|
},
|
|
2524
|
+
title: dshHasUpdate ? `发现 DSH 新版本 v${info.dshLatest}` : undefined,
|
|
2494
2525
|
},
|
|
2495
2526
|
React.createElement('span', { style: { opacity: 0.75, fontSize: 11, fontWeight: 400 } }, 'DSH'),
|
|
2496
2527
|
`v${info.dshVersion}`,
|
|
2528
|
+
dshHasUpdate && React.createElement('span', { style: { fontWeight: 600, fontSize: 11 } }, `➔ v${info.dshLatest}`),
|
|
2497
2529
|
),
|
|
2498
2530
|
// 刷新检查按钮
|
|
2499
2531
|
React.createElement('button', {
|
|
@@ -2672,6 +2704,102 @@ function VersionBanner({ rpcCall }) {
|
|
|
2672
2704
|
),
|
|
2673
2705
|
),
|
|
2674
2706
|
),
|
|
2707
|
+
|
|
2708
|
+
// ── DSH 宿主有新版本时的提示 / 一键升级卡片 ──
|
|
2709
|
+
dshHasUpdate && React.createElement('div', {
|
|
2710
|
+
style: {
|
|
2711
|
+
...s.card,
|
|
2712
|
+
background: 'var(--dsw-alias-state-warn-bg,#fffbeb)',
|
|
2713
|
+
border: '1px solid var(--dsw-alias-state-warn-border,#fde68a)',
|
|
2714
|
+
padding: '14px 16px',
|
|
2715
|
+
marginTop: 10,
|
|
2716
|
+
marginBottom: 0,
|
|
2717
|
+
},
|
|
2718
|
+
},
|
|
2719
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 12 } },
|
|
2720
|
+
React.createElement('span', { style: { fontSize: 22 } }, '🛠️'),
|
|
2721
|
+
React.createElement('div', { style: { flex: 1, minWidth: 0 } },
|
|
2722
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8, marginBottom: 8 } },
|
|
2723
|
+
React.createElement('div', {
|
|
2724
|
+
style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary,#92400e)' },
|
|
2725
|
+
}, `DSH 有新版本 v${info.dshLatest}(当前 v${info.dshVersion})`),
|
|
2726
|
+
info?.dshUpgradable
|
|
2727
|
+
? React.createElement('button', {
|
|
2728
|
+
style: {
|
|
2729
|
+
...s.btnPri,
|
|
2730
|
+
height: 28,
|
|
2731
|
+
fontSize: 12,
|
|
2732
|
+
padding: '0 14px',
|
|
2733
|
+
background: dshUpgradeResult?.ok
|
|
2734
|
+
? 'var(--dsw-alias-state-success-primary,#059669)'
|
|
2735
|
+
: 'var(--dsw-alias-brand-primary,#4f6ef7)',
|
|
2736
|
+
opacity: (dshUpgrading || restarting) ? 0.6 : 1,
|
|
2737
|
+
},
|
|
2738
|
+
onClick: handleUpgradeDsh,
|
|
2739
|
+
disabled: dshUpgrading || restarting || dshUpgradeResult?.ok,
|
|
2740
|
+
},
|
|
2741
|
+
dshUpgrading
|
|
2742
|
+
? React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } },
|
|
2743
|
+
React.createElement('span', { style: { animation: 'spin 1s linear infinite', display: 'inline-flex' } }, React.createElement(Icons.refresh)),
|
|
2744
|
+
'正在升级 DSH…',
|
|
2745
|
+
)
|
|
2746
|
+
: dshUpgradeResult?.ok
|
|
2747
|
+
? '✓ DSH 升级完成'
|
|
2748
|
+
: `一键升级 DSH 到 v${info.dshLatest}`,
|
|
2749
|
+
)
|
|
2750
|
+
: React.createElement('a', {
|
|
2751
|
+
href: GITHUB_URL, target: '_blank', rel: 'noreferrer',
|
|
2752
|
+
style: { ...s.btnLink, fontSize: 12, fontWeight: 600 },
|
|
2753
|
+
}, '查看官方升级方式 ↗'),
|
|
2754
|
+
),
|
|
2755
|
+
React.createElement('div', {
|
|
2756
|
+
style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.6 },
|
|
2757
|
+
},
|
|
2758
|
+
info?.dshUpgradable
|
|
2759
|
+
? '升级 DSH 命令行工具后需重启 DSH 服务生效。'
|
|
2760
|
+
: (info?.dshUpgradeReason || '当前 DSH 非 npm 全局安装,无法一键自动升级,请按官方渠道手动更新。'),
|
|
2761
|
+
),
|
|
2762
|
+
dshUpgradeResult && React.createElement('div', {
|
|
2763
|
+
style: {
|
|
2764
|
+
marginTop: 10,
|
|
2765
|
+
fontSize: 12,
|
|
2766
|
+
lineHeight: 1.6,
|
|
2767
|
+
color: dshUpgradeResult.ok
|
|
2768
|
+
? 'var(--dsw-alias-state-success-primary,#059669)'
|
|
2769
|
+
: 'var(--dsw-alias-state-error-primary,#dc2626)',
|
|
2770
|
+
},
|
|
2771
|
+
}, dshUpgradeResult.message),
|
|
2772
|
+
dshUpgradeResult?.ok && !dismissRestart && React.createElement('div', {
|
|
2773
|
+
style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginTop: 10 },
|
|
2774
|
+
},
|
|
2775
|
+
React.createElement('button', {
|
|
2776
|
+
style: { ...s.btnPri, height: 30, fontSize: 12, padding: '0 14px', background: 'var(--dsw-alias-state-success-primary,#059669)' },
|
|
2777
|
+
onClick: handleRestart,
|
|
2778
|
+
}, '🔄 立即重启 DSH 服务'),
|
|
2779
|
+
React.createElement('button', {
|
|
2780
|
+
style: { ...s.btnGhost, height: 30, fontSize: 12, padding: '0 12px' },
|
|
2781
|
+
onClick: () => setDismissRestart(true),
|
|
2782
|
+
}, '稍后手动重启'),
|
|
2783
|
+
),
|
|
2784
|
+
(restarting || restartStatus) && React.createElement('div', {
|
|
2785
|
+
style: {
|
|
2786
|
+
display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, marginTop: 8,
|
|
2787
|
+
color: restartStatus?.phase === 'success'
|
|
2788
|
+
? 'var(--dsw-alias-state-success-primary,#059669)'
|
|
2789
|
+
: restartStatus?.phase === 'timeout'
|
|
2790
|
+
? 'var(--dsw-alias-state-error-primary,#dc2626)'
|
|
2791
|
+
: 'var(--dsw-alias-state-info-primary,#2563eb)',
|
|
2792
|
+
fontWeight: 500,
|
|
2793
|
+
},
|
|
2794
|
+
},
|
|
2795
|
+
restartStatus?.phase !== 'success' && restartStatus?.phase !== 'timeout' && React.createElement('span', {
|
|
2796
|
+
style: { animation: 'spin 1s linear infinite', display: 'inline-flex' },
|
|
2797
|
+
}, React.createElement(Icons.refresh)),
|
|
2798
|
+
restartStatus?.text || '正在处理…',
|
|
2799
|
+
),
|
|
2800
|
+
),
|
|
2801
|
+
),
|
|
2802
|
+
),
|
|
2675
2803
|
);
|
|
2676
2804
|
}
|
|
2677
2805
|
|
|
@@ -4567,6 +4695,199 @@ function RemoteDirectoryFlow(props) {
|
|
|
4567
4695
|
|
|
4568
4696
|
// ---- 插件入口 ----
|
|
4569
4697
|
|
|
4698
|
+
// iOS Safari 键盘弹起时输入框上下跳动适配。
|
|
4699
|
+
//
|
|
4700
|
+
// 根因(实测 2026-09):聊天输入区容器(宿主 wSkVaW_composerSeat)为
|
|
4701
|
+
// position:sticky; bottom:0,其滚动容器高度由 100% 链决定。iOS 键盘弹起时
|
|
4702
|
+
// visual viewport 收缩,sticky 基准随视口变化 + iOS 反复 scroll 调整 → 每敲一字跳动。
|
|
4703
|
+
//
|
|
4704
|
+
// 通用解法(不依赖宿主类名):键盘弹起瞬间找到焦点输入框所属的滚动容器,
|
|
4705
|
+
// 把它当前高度用 px 固定(避免随视口继续收缩/重排),键盘收起后还原;
|
|
4706
|
+
// 并顺带做一次精确 scrollIntoView,阻止 iOS 二次乱滚。
|
|
4707
|
+
function setupIosKeyboardAdapter() {
|
|
4708
|
+
if (typeof window === 'undefined' || !window.visualViewport) return;
|
|
4709
|
+
if (!/iPhone|iPad|iPod/.test(navigator.userAgent || '')) return;
|
|
4710
|
+
|
|
4711
|
+
const vv = window.visualViewport;
|
|
4712
|
+
const isEditable = (el) => el && (el.isContentEditable || el.tagName === 'TEXTAREA' || el.tagName === 'INPUT');
|
|
4713
|
+
let keyboardOpen = false;
|
|
4714
|
+
let pinnedEl = null; // 被固定高度的滚动容器
|
|
4715
|
+
let pinnedH = null; // 原始 height(还原用)
|
|
4716
|
+
let pinnedInline = null;
|
|
4717
|
+
|
|
4718
|
+
const findScrollContainer = (el) => {
|
|
4719
|
+
let n = el;
|
|
4720
|
+
while (n && n !== document.body) {
|
|
4721
|
+
const cs = getComputedStyle(n);
|
|
4722
|
+
if ((cs.overflowY === 'auto' || cs.overflowY === 'scroll') && n.scrollHeight > n.clientHeight) {
|
|
4723
|
+
return n;
|
|
4724
|
+
}
|
|
4725
|
+
n = n.parentElement;
|
|
4726
|
+
}
|
|
4727
|
+
return null;
|
|
4728
|
+
};
|
|
4729
|
+
|
|
4730
|
+
vv.addEventListener('resize', () => {
|
|
4731
|
+
const ratio = vv.height / window.innerHeight;
|
|
4732
|
+
const nowOpen = ratio < 0.75;
|
|
4733
|
+
if (nowOpen === keyboardOpen) return;
|
|
4734
|
+
keyboardOpen = nowOpen;
|
|
4735
|
+
const el = document.activeElement;
|
|
4736
|
+
|
|
4737
|
+
if (nowOpen) {
|
|
4738
|
+
// 键盘弹起:固定滚动容器高度,防随视口继续收缩导致 sticky 输入框跳动
|
|
4739
|
+
const scroller = isEditable(el) ? findScrollContainer(el) : null;
|
|
4740
|
+
if (scroller) {
|
|
4741
|
+
pinnedEl = scroller;
|
|
4742
|
+
pinnedInline = scroller.style.height || '';
|
|
4743
|
+
pinnedH = scroller.getBoundingClientRect().height;
|
|
4744
|
+
scroller.style.height = `${Math.round(pinnedH)}px`;
|
|
4745
|
+
}
|
|
4746
|
+
requestAnimationFrame(() => {
|
|
4747
|
+
if (isEditable(el)) {
|
|
4748
|
+
try { el.scrollIntoView({ block: 'nearest' }); } catch { /* 忽略 */ }
|
|
4749
|
+
}
|
|
4750
|
+
});
|
|
4751
|
+
} else {
|
|
4752
|
+
// 键盘收起:还原滚动容器高度,让布局回到正常
|
|
4753
|
+
if (pinnedEl) {
|
|
4754
|
+
pinnedEl.style.height = pinnedInline;
|
|
4755
|
+
pinnedEl = null; pinnedH = null; pinnedInline = null;
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
// 移动端输入框折叠/展开(body.dsh-composer-collapsed)。
|
|
4762
|
+
// 底部输入区(composer)占用 ~116px,顶部菜单 + header 又占 ~120px,屏幕有限时
|
|
4763
|
+
// 中间消息可视区很小。折叠后 composer 隐藏、消息 viewArea 自动伸展全高,阅读区显著增大。
|
|
4764
|
+
// 折叠态记忆到 localStorage(dsh-composer-fold),跨会话保持用户偏好。
|
|
4765
|
+
function setupComposerCollapse() {
|
|
4766
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
4767
|
+
if (window.innerWidth > 768) return; // 仅移动端
|
|
4768
|
+
const LS_KEY = 'dsh-composer-fold';
|
|
4769
|
+
|
|
4770
|
+
let bar = null; // 折叠态底部"点击输入"细条
|
|
4771
|
+
let busy = false; // 防重入锁(observer 由我们自己改动触发时跳过)
|
|
4772
|
+
let lastHadSeat = null; // 上次检测是否有输入区(避免重复处理)
|
|
4773
|
+
|
|
4774
|
+
// 折叠按钮:注入到聊天头部工具栏(headerUtilities)的 Session 下载按钮旁,
|
|
4775
|
+
// 与 DSH 原生按钮同规格(28px 圆形),视觉与原生一致。返回 null 表示工具栏未就绪。
|
|
4776
|
+
const getFoldBtn = () => {
|
|
4777
|
+
const existing = document.querySelector('.dsh-header-fold-btn');
|
|
4778
|
+
if (existing) return existing;
|
|
4779
|
+
const utils = document.querySelector('div[class*="wSkVaW_headerUtilities"], div[class*="headerUtilities"]');
|
|
4780
|
+
const logBtn = document.querySelector('button[class*="sessionLogButton"], button[class*="nL4_yW_sessionLogButton"]');
|
|
4781
|
+
if (!utils) return null;
|
|
4782
|
+
const btn = document.createElement('button');
|
|
4783
|
+
btn.className = 'dsh-header-fold-btn';
|
|
4784
|
+
btn.setAttribute('aria-label', '收起/展开输入框');
|
|
4785
|
+
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>';
|
|
4786
|
+
// 插到 session 下载按钮左侧(紧邻原生工具钮)
|
|
4787
|
+
if (logBtn && logBtn.parentElement === utils) utils.insertBefore(btn, logBtn);
|
|
4788
|
+
else utils.appendChild(btn);
|
|
4789
|
+
return btn;
|
|
4790
|
+
};
|
|
4791
|
+
const isCollapsed = () => document.body.classList.contains('dsh-composer-collapsed');
|
|
4792
|
+
const readPref = () => { try { return localStorage.getItem(LS_KEY) === '1'; } catch { return false; } };
|
|
4793
|
+
|
|
4794
|
+
const ensureBar = () => {
|
|
4795
|
+
if (bar && document.body.contains(bar)) return bar;
|
|
4796
|
+
const seat = document.querySelector('div[class*="composerSeat"]');
|
|
4797
|
+
const scrollBody = seat ? seat.parentElement : null;
|
|
4798
|
+
if (!scrollBody) return null;
|
|
4799
|
+
bar = document.createElement('div');
|
|
4800
|
+
bar.className = 'dsh-composer-collapsed-bar';
|
|
4801
|
+
bar.textContent = '\u270f\ufe0f 点击输入消息…';
|
|
4802
|
+
scrollBody.insertBefore(bar, seat);
|
|
4803
|
+
bar.addEventListener('click', () => setCollapsed(false));
|
|
4804
|
+
return bar;
|
|
4805
|
+
};
|
|
4806
|
+
const removeBar = () => { if (bar) { try { bar.remove(); } catch {} bar = null; } };
|
|
4807
|
+
|
|
4808
|
+
// 折叠态切换:改 class + 记忆 + 图标 + 细条显隐
|
|
4809
|
+
const setCollapsed = (collapsed) => {
|
|
4810
|
+
if (busy) return;
|
|
4811
|
+
busy = true;
|
|
4812
|
+
try {
|
|
4813
|
+
if (isCollapsed() !== collapsed) {
|
|
4814
|
+
document.body.classList.toggle('dsh-composer-collapsed', collapsed);
|
|
4815
|
+
try { localStorage.setItem(LS_KEY, collapsed ? '1' : '0'); } catch {}
|
|
4816
|
+
updateButton(collapsed);
|
|
4817
|
+
}
|
|
4818
|
+
if (collapsed) ensureBar(); else removeBar();
|
|
4819
|
+
} finally { busy = false; }
|
|
4820
|
+
};
|
|
4821
|
+
|
|
4822
|
+
// 更新顶部折叠钮图标(仅内容变化时写入,避免无谓 DOM 抖动)
|
|
4823
|
+
const updateButton = (collapsed) => {
|
|
4824
|
+
const btn = getFoldBtn();
|
|
4825
|
+
if (!btn) return;
|
|
4826
|
+
btn.title = collapsed ? '展开输入框' : '收起输入框,最大化对话阅读区';
|
|
4827
|
+
const icon = collapsed
|
|
4828
|
+
? '<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>'
|
|
4829
|
+
: '<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>';
|
|
4830
|
+
const existing = btn.querySelector('svg');
|
|
4831
|
+
if (!existing || existing.outerHTML !== icon) btn.innerHTML = icon;
|
|
4832
|
+
};
|
|
4833
|
+
|
|
4834
|
+
// 有输入区(对话页):确保按钮可用 + 按记忆恢复折叠态
|
|
4835
|
+
const handleSeatPresent = () => {
|
|
4836
|
+
const btn = getFoldBtn();
|
|
4837
|
+
if (!btn) return false;
|
|
4838
|
+
btn.style.display = 'inline-flex';
|
|
4839
|
+
if (!btn._dshFoldWired) {
|
|
4840
|
+
btn._dshFoldWired = true;
|
|
4841
|
+
btn.onclick = (e) => { e.stopPropagation(); setCollapsed(!isCollapsed()); };
|
|
4842
|
+
}
|
|
4843
|
+
document.body.classList.add('dsh-composer-active');
|
|
4844
|
+
const want = readPref();
|
|
4845
|
+
if (want !== isCollapsed()) {
|
|
4846
|
+
setCollapsed(want);
|
|
4847
|
+
} else {
|
|
4848
|
+
updateButton(isCollapsed());
|
|
4849
|
+
}
|
|
4850
|
+
return true;
|
|
4851
|
+
};
|
|
4852
|
+
|
|
4853
|
+
// 无输入区:隐藏折叠钮与细条(保留记忆)
|
|
4854
|
+
const handleSeatAbsent = () => {
|
|
4855
|
+
const btn = getFoldBtn();
|
|
4856
|
+
if (btn) { btn.style.display = 'none'; }
|
|
4857
|
+
document.body.classList.remove('dsh-composer-active');
|
|
4858
|
+
removeBar();
|
|
4859
|
+
};
|
|
4860
|
+
|
|
4861
|
+
// 单次检测(observer 回调 / 初始)。
|
|
4862
|
+
// 仅当"是否有输入区"或"折叠钮是否已注入"发生变化时才全量处理,
|
|
4863
|
+
// 避免消息流式输出等高频 DOM 变化下 observer 反复执行注入/状态恢复。
|
|
4864
|
+
// busy 锁 + 注入幂等共同防止自触发循环。
|
|
4865
|
+
const onComposerChange = () => {
|
|
4866
|
+
const hasSeat = !!document.querySelector('div[class*="composerSeat"]');
|
|
4867
|
+
const hasBtn = !!document.querySelector('.dsh-header-fold-btn');
|
|
4868
|
+
if (busy) return;
|
|
4869
|
+
if (hasSeat === lastHadSeat && hasSeat === hasBtn) return; // 状态与注入都没变:忽略
|
|
4870
|
+
busy = true;
|
|
4871
|
+
try {
|
|
4872
|
+
if (hasSeat) {
|
|
4873
|
+
lastHadSeat = handleSeatPresent(); // 内部幂等;工具栏未就绪返回 false,等下次再试
|
|
4874
|
+
} else {
|
|
4875
|
+
handleSeatAbsent();
|
|
4876
|
+
lastHadSeat = false;
|
|
4877
|
+
}
|
|
4878
|
+
} finally { busy = false; }
|
|
4879
|
+
};
|
|
4880
|
+
|
|
4881
|
+
// observer:断开→处理→重连,杜绝"处理里改 DOM 再触发自己"的死循环
|
|
4882
|
+
const observer = new MutationObserver(() => {
|
|
4883
|
+
observer.disconnect();
|
|
4884
|
+
try { onComposerChange(); } finally { observer.observe(document.body, { childList: true, subtree: true }); }
|
|
4885
|
+
});
|
|
4886
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
4887
|
+
onComposerChange(); // 初始检查
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
|
|
4570
4891
|
function apply(ctx) {
|
|
4571
4892
|
window.__dshClientCtx = ctx;
|
|
4572
4893
|
const rpcCall = (endpoint, payload, signal) =>
|
|
@@ -4575,6 +4896,9 @@ function apply(ctx) {
|
|
|
4575
4896
|
window.__dshOpenRemoteWorkspaceModal = (onAdded, onPickDirect, onCancel) =>
|
|
4576
4897
|
showRemoteWorkspaceDialog(rpcCall, onAdded, ctx, onPickDirect, onCancel);
|
|
4577
4898
|
|
|
4899
|
+
setupIosKeyboardAdapter();
|
|
4900
|
+
setupComposerCollapse();
|
|
4901
|
+
|
|
4578
4902
|
setupMobileExperience(rpcCall, ctx);
|
|
4579
4903
|
|
|
4580
4904
|
const injected = () => ({ pick: () => ctx.workspaces?.pickDirectory?.() });
|
package/client/mobile-styles.js
CHANGED
|
@@ -394,6 +394,74 @@ export const MOBILE_STYLES_CSS = `
|
|
|
394
394
|
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/* ── 移动端输入框折叠模式(body.dsh-composer-collapsed)──────────────
|
|
398
|
+
折叠后隐藏吸底输入区,让消息 viewArea 自动伸展到全高,最大化阅读区。
|
|
399
|
+
入口:聊天头部工具栏(Session 下载钮旁)的折叠按钮,或折叠态底部细输入条点它唤回。
|
|
400
|
+
状态持久化到 localStorage。 */
|
|
401
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_composerSeat"] {
|
|
402
|
+
display: none !important;
|
|
403
|
+
}
|
|
404
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_viewArea"] {
|
|
405
|
+
flex: 1 1 auto !important;
|
|
406
|
+
height: auto !important;
|
|
407
|
+
min-height: 0 !important;
|
|
408
|
+
}
|
|
409
|
+
/* 折叠时滚动区底部对齐 safe-area,避免内容被 iPhone 底部横条遮挡 */
|
|
410
|
+
body.dsh-composer-collapsed div[class*="wSkVaW_scrollBody"] {
|
|
411
|
+
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* 折叠输入框按钮:注入在聊天头部工具栏(Session 下载钮旁),
|
|
415
|
+
与 DSH sessionLogButton 同规格(28px 圆形图标钮),视觉与原生一致 */
|
|
416
|
+
.dsh-header-fold-btn {
|
|
417
|
+
min-width: 28px !important;
|
|
418
|
+
width: 28px !important;
|
|
419
|
+
height: 28px !important;
|
|
420
|
+
padding: 0 !important;
|
|
421
|
+
border-radius: 50% !important;
|
|
422
|
+
display: inline-flex !important;
|
|
423
|
+
align-items: center !important;
|
|
424
|
+
justify-content: center !important;
|
|
425
|
+
flex-shrink: 0 !important;
|
|
426
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1)) !important;
|
|
427
|
+
background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.03)) !important;
|
|
428
|
+
color: var(--dsw-alias-label-secondary, #6b7280) !important;
|
|
429
|
+
cursor: pointer !important;
|
|
430
|
+
transition: opacity 0.15s !important;
|
|
431
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03) !important;
|
|
432
|
+
-webkit-tap-highlight-color: transparent !important;
|
|
433
|
+
}
|
|
434
|
+
.dsh-header-fold-btn:active {
|
|
435
|
+
opacity: 0.6 !important;
|
|
436
|
+
}
|
|
437
|
+
.dsh-header-fold-btn svg {
|
|
438
|
+
width: 15px !important;
|
|
439
|
+
height: 15px !important;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* 折叠态底部细输入条:点击唤起输入框(位于原输入区位置,sticky 底部) */
|
|
443
|
+
.dsh-composer-collapsed-bar {
|
|
444
|
+
display: none !important;
|
|
445
|
+
position: sticky !important;
|
|
446
|
+
bottom: 0 !important;
|
|
447
|
+
margin: 8px 12px max(8px, env(safe-area-inset-bottom, 0px)) 12px !important;
|
|
448
|
+
padding: 11px 16px !important;
|
|
449
|
+
border-radius: 22px !important;
|
|
450
|
+
background: var(--dsw-alias-bg-layer-2, #f4f4f7) !important;
|
|
451
|
+
border: 1px solid rgba(0, 0, 0, 0.07) !important;
|
|
452
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1) !important;
|
|
453
|
+
font-size: 13.5px !important;
|
|
454
|
+
line-height: 1.4 !important;
|
|
455
|
+
cursor: pointer !important;
|
|
456
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04) !important;
|
|
457
|
+
-webkit-tap-highlight-color: transparent !important;
|
|
458
|
+
z-index: 40 !important;
|
|
459
|
+
box-sizing: border-box !important;
|
|
460
|
+
}
|
|
461
|
+
body.dsh-composer-collapsed .dsh-composer-collapsed-bar {
|
|
462
|
+
display: block !important;
|
|
463
|
+
}
|
|
464
|
+
|
|
397
465
|
/* 输入卡片:DeepSeek App 圆角大胶囊造型 */
|
|
398
466
|
div[class*="uV2eYG_card"] {
|
|
399
467
|
border-radius: 26px !important;
|
package/docs/telegram-usage.md
CHANGED
|
@@ -90,4 +90,4 @@
|
|
|
90
90
|
2. **安全白名单拦截**:
|
|
91
91
|
- 仅白名单内的用户消息会被放行给 Agent;非白名单用户发送的消息会被直接忽略,绝不消耗 Token 或喂给模型。
|
|
92
92
|
3. **敏感操作审批**:
|
|
93
|
-
- 当 Agent 尝试执行系统命令或敏感文件读写时,Telegram 会自动下发 `[✓ 批准执行]` / `[✕ 拒绝执行]` 交互按键,10 分钟未处理自动超时拒绝。
|
|
93
|
+
- 当 Agent 尝试执行系统命令或敏感文件读写时,Telegram 会自动下发 `[✓ 批准执行]` / `[✕ 拒绝执行]` 交互按键,10 分钟未处理自动超时拒绝。
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// DSH 原生浏览器鉴权的兼容层(lib/auth/dsh-native-cookie.js)
|
|
2
|
+
//
|
|
3
|
+
// 背景
|
|
4
|
+
// ---------------------------------------------------------------
|
|
5
|
+
// DSH 从 0.1.2 起为 `dsh web` 引入了"原生浏览器鉴权":
|
|
6
|
+
// 1. 启动时打印带一次性 launch token 的地址:http://127.0.0.1:<port>/?token=…
|
|
7
|
+
// 2. `GET /?token=<token>` 校验通过后 303 重定向到 `/`,并下发签名会话 cookie
|
|
8
|
+
// 3. 此后所有受保护路由(`/`、`/api`、以及任何经
|
|
9
|
+
// `ctx.connection.rpc.handle()` 注册的通道)都要求该 cookie,否则 401
|
|
10
|
+
//
|
|
11
|
+
// cookie 的构造(见 @deepseek-ai/dsh-client-connection 的 BrowserAuth):
|
|
12
|
+
// 名称:`dsh-auth-` + base64url(sha256(authority)),authority = `127.0.0.1:<port>`
|
|
13
|
+
// 值 :`v1.<base64url(JSON payload)>.<base64url(HMAC-SHA256(secret, body))>`
|
|
14
|
+
// 载荷:`{ version: 1, authority, issuedAt, expiresAt }`(均为安全整数)
|
|
15
|
+
// 密钥:`<DSH_HOME>/.credentials.yaml` 里 record
|
|
16
|
+
// `client-connection/browser-session` 的 `payload.secret`(base64url 32 字节)
|
|
17
|
+
//
|
|
18
|
+
// 为什么本插件需要它
|
|
19
|
+
// ---------------------------------------------------------------
|
|
20
|
+
// 本插件的自建反向代理监听自己的端口(默认 3082),对外提供局域网/隧道访问,
|
|
21
|
+
// 再把请求转发到 DSH 回环地址。浏览器持有的是"代理 origin"的 cookie,
|
|
22
|
+
// 而 DSH 的 cookie 绑定的是 `127.0.0.1:<dshPort>` 这个 authority,两者不是同一个,
|
|
23
|
+
// 因此代理必须自行注入一枚合法的 loopback 会话 cookie,否则远程访问会一律 401。
|
|
24
|
+
//
|
|
25
|
+
// 本模块只做"按 DSH 的格式生成一枚回环会话 cookie",不引入任何三方依赖。
|
|
26
|
+
|
|
27
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
28
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
|
|
32
|
+
/** DSH 会话 cookie 的名称前缀(与 dsh-client-connection 的 COOKIE_PREFIX 一致)。 */
|
|
33
|
+
export const DSH_AUTH_COOKIE_PREFIX = 'dsh-auth-';
|
|
34
|
+
|
|
35
|
+
/** cookie 载荷版本(与 dsh-client-connection 的 COOKIE_PAYLOAD_VERSION 一致)。 */
|
|
36
|
+
export const DSH_AUTH_COOKIE_VERSION = 1;
|
|
37
|
+
|
|
38
|
+
/** 会话 cookie 有效期(毫秒)。与 DSH 默认的 cookieMaxAgeDays=30 对齐。 */
|
|
39
|
+
export const DSH_AUTH_COOKIE_MAX_AGE_MS = 30 * 24 * 3600 * 1000;
|
|
40
|
+
|
|
41
|
+
/** 承载签名密钥的 DSH 凭据记录键。 */
|
|
42
|
+
export const DSH_BROWSER_SESSION_RECORD = 'client-connection/browser-session';
|
|
43
|
+
|
|
44
|
+
function base64Url(value) {
|
|
45
|
+
return Buffer.from(value).toString('base64url');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 计算 DSH 会话 cookie 的名称(authority 绑定)。
|
|
50
|
+
* @param {number|string} targetPort DSH 回环端口
|
|
51
|
+
* @param {string} [host] 默认 127.0.0.1
|
|
52
|
+
* @returns {string} 形如 `dsh-auth-<base64url(sha256("127.0.0.1:3080"))>`
|
|
53
|
+
*/
|
|
54
|
+
export function dshAuthCookieName(targetPort, host = '127.0.0.1') {
|
|
55
|
+
const authority = `${host}:${targetPort}`;
|
|
56
|
+
return DSH_AUTH_COOKIE_PREFIX + base64Url(createHash('sha256').update(authority).digest());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从 DSH 凭据文件内容中取出 browser-session 记录的签名密钥。
|
|
61
|
+
*
|
|
62
|
+
* 必须精确定位 `client-connection/browser-session` 这条 record:
|
|
63
|
+
* 凭据文件里还有 `refs:` 段(各家模型 API key)以及可能存在其它插件写入的
|
|
64
|
+
* record,一旦取到别处的 `secret:`,签名就会无效 —— 现象是"远程访问一律 401",
|
|
65
|
+
* 且极难定位。因此先锚定 record 键,再在其后的小范围内取 secret;
|
|
66
|
+
* 仅当锚点缺失(异常/未来格式)时才退回"全文件第一个 secret:"的旧行为。
|
|
67
|
+
*
|
|
68
|
+
* @param {string} content 凭据文件文本
|
|
69
|
+
* @returns {string} base64url 编码的密钥;取不到时返回空串
|
|
70
|
+
*/
|
|
71
|
+
export function readBrowserSessionSecret(content) {
|
|
72
|
+
if (typeof content !== 'string' || content === '') return '';
|
|
73
|
+
|
|
74
|
+
// 锚定 record 键所在行,随后只在该行往后的一小段内查找 secret
|
|
75
|
+
const anchorMatch = /^[ \t]*client-connection\/browser-session:[ \t]*$/m.exec(content);
|
|
76
|
+
if (anchorMatch) {
|
|
77
|
+
const tail = content.slice(anchorMatch.index);
|
|
78
|
+
const scoped = /secret:[ \t]*([A-Za-z0-9_-]+)/.exec(tail);
|
|
79
|
+
if (scoped) return scoped[1];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 兜底:老格式/异常格式下沿用全文件首个 secret:
|
|
83
|
+
const fallback = /secret:[ \t]*([A-Za-z0-9_-]+)/.exec(content);
|
|
84
|
+
return fallback ? fallback[1] : '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 生成一枚绑定到 loopback authority 的 DSH 原生会话 cookie。
|
|
89
|
+
*
|
|
90
|
+
* @param {object} opts
|
|
91
|
+
* @param {number|string} opts.targetPort DSH 回环端口
|
|
92
|
+
* @param {string} opts.secret base64url 编码的签名密钥
|
|
93
|
+
* @param {string} [opts.host] 默认 127.0.0.1
|
|
94
|
+
* @param {number} [opts.now] 当前时间戳(测试注入用)
|
|
95
|
+
* @param {number} [opts.maxAgeMs] 有效期
|
|
96
|
+
* @returns {string} `name=value`;密钥缺失时返回空串
|
|
97
|
+
*/
|
|
98
|
+
export function buildDshLoopbackCookie({
|
|
99
|
+
targetPort,
|
|
100
|
+
secret,
|
|
101
|
+
host = '127.0.0.1',
|
|
102
|
+
now = Date.now(),
|
|
103
|
+
maxAgeMs = DSH_AUTH_COOKIE_MAX_AGE_MS,
|
|
104
|
+
}) {
|
|
105
|
+
if (!secret || !targetPort) return '';
|
|
106
|
+
|
|
107
|
+
const authority = `${host}:${targetPort}`;
|
|
108
|
+
const name = dshAuthCookieName(targetPort, host);
|
|
109
|
+
// 与 DSH 一致:issuedAt 略微回拨,避免与宿主的同秒比较产生边界问题
|
|
110
|
+
const issuedAt = now - 1000;
|
|
111
|
+
const expiresAt = issuedAt + maxAgeMs;
|
|
112
|
+
|
|
113
|
+
const body = base64Url(Buffer.from(JSON.stringify({
|
|
114
|
+
version: DSH_AUTH_COOKIE_VERSION,
|
|
115
|
+
authority,
|
|
116
|
+
issuedAt,
|
|
117
|
+
expiresAt,
|
|
118
|
+
}), 'utf8'));
|
|
119
|
+
const key = Buffer.from(secret, 'base64url');
|
|
120
|
+
const sig = base64Url(createHmac('sha256', key).update(body).digest());
|
|
121
|
+
return `${name}=v${DSH_AUTH_COOKIE_VERSION}.${body}.${sig}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 读取本机 DSH 凭据并生成回环会话 cookie(失败时返回空串,绝不抛出)。
|
|
126
|
+
*
|
|
127
|
+
* @param {number|string} targetPort DSH 回环端口
|
|
128
|
+
* @param {object} [env] 便于测试注入的环境
|
|
129
|
+
* @param {string} [env.dshHome] DSH 主目录(默认 $DSH_HOME 或 ~/.dsh)
|
|
130
|
+
* @param {Function} [env.readFileSync] 文件读取实现(默认 node:fs)
|
|
131
|
+
* @param {Function} [env.existsSync] 存在性判断实现
|
|
132
|
+
* @param {number} [env.now] 当前时间戳
|
|
133
|
+
* @returns {string} `name=value`,或空串
|
|
134
|
+
*/
|
|
135
|
+
export function getDshLoopbackCookie(targetPort, env = {}) {
|
|
136
|
+
const readFile = env.readFileSync ?? readFileSync;
|
|
137
|
+
const exists = env.existsSync ?? existsSync;
|
|
138
|
+
try {
|
|
139
|
+
const dshHome = env.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
140
|
+
const credPath = join(dshHome, '.credentials.yaml');
|
|
141
|
+
if (!exists(credPath)) return '';
|
|
142
|
+
const secret = readBrowserSessionSecret(readFile(credPath, 'utf8'));
|
|
143
|
+
if (!secret) return '';
|
|
144
|
+
return buildDshLoopbackCookie({ targetPort, secret, now: env.now });
|
|
145
|
+
} catch {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
}
|