@wenbin_wb/dsh-bridge 2.10.12 → 2.10.14
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 +46 -1
- package/client/client.js +477 -61
- package/client/index.js +231 -49
- package/client/mobile-styles.js +303 -13
- package/client/picker-yield.js +77 -0
- package/lib/cloudflared-manager.mjs +458 -17
- package/lib/feishu/lark-bundled.mjs +230 -76
- package/lib/index.js +4 -1
- package/package.json +4 -4
package/client/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
queuePendingOperation, unlockAdmin, onUnlocked,
|
|
5
5
|
fetchLoopbackTokenOnce,
|
|
6
6
|
} from './unlock-manager.js'
|
|
7
|
+
import { hasOfficialDirectoryPicker, shouldYieldToOfficialPicker } from './picker-yield.js'
|
|
7
8
|
// dsh-bridge 客户端插件:设置页「远程访问」面板
|
|
8
9
|
|
|
9
10
|
// 兼容非 HTTPS 环境(如手机局域网 HTTP 访问):为非安全上下文补齐 crypto.randomUUID
|
|
@@ -40,6 +41,21 @@ if (typeof window !== 'undefined') window.__dshResourceUrlCompat = RESOURCE_URL_
|
|
|
40
41
|
// 避免 768px 单点上"桥渲染顶栏、宿主却按桌面(push)布局"的错位。
|
|
41
42
|
const MOBILE_MAX_WIDTH = 767;
|
|
42
43
|
|
|
44
|
+
// 设置中心「两级钻取」的窄屏断点:必须与 mobile-styles.js 的
|
|
45
|
+
// `@media (max-width: 480px)` 6.2 块保持一致(改一处必须改另一处)。
|
|
46
|
+
// 481–767px 仍有 ~350px 以上内容宽度,保留左侧图标轨道即可;
|
|
47
|
+
// <=480px 时轨道要吃掉 21% 宽度、第三方设置节必然溢出,才切换为列表/详情两页。
|
|
48
|
+
const SETTINGS_DRILLDOWN_MAX_WIDTH = 480;
|
|
49
|
+
// 交互层的就绪开关,打在 <html> 上(监听器要在弹窗出现前装好,那时 panel 还不存在)。
|
|
50
|
+
// CSS 6.2 整块以它为前置选择器:开关没打开就整块惰性,<=480px 退回 88px 图标轨道,
|
|
51
|
+
// 不会出现「把内容藏了、却没人负责把用户带进去」的死列表。
|
|
52
|
+
const DRILLDOWN_GATE_ATTR = 'data-dshbr-drilldown';
|
|
53
|
+
const DRILLDOWN_GATE_READY = 'ready';
|
|
54
|
+
const DRILLDOWN_GATE_FLAG = 'dshBrDrilldownGate';
|
|
55
|
+
// 视图状态标记:打在宿主设置弹窗的 panel 上,CSS 6.2 块按此切换列表页/详情页。
|
|
56
|
+
const SETTINGS_VIEW_ATTR = 'data-dshbr-settings-view';
|
|
57
|
+
const SETTINGS_VIEW_DETAIL = 'detail';
|
|
58
|
+
|
|
43
59
|
function isLocalEnvironment() {
|
|
44
60
|
if (typeof window === 'undefined') return true;
|
|
45
61
|
const host = window.location.hostname || '';
|
|
@@ -2607,7 +2623,11 @@ function VersionBanner({ rpcCall }) {
|
|
|
2607
2623
|
gap: 8,
|
|
2608
2624
|
},
|
|
2609
2625
|
},
|
|
2610
|
-
|
|
2626
|
+
// 左组:版本徽标 + DSH 版本 + 检查更新。
|
|
2627
|
+
// flexWrap + minWidth:0 是必需的:此前只写 display:flex,左组既不能收缩也不能换行,
|
|
2628
|
+
// 会把整行顶到 305px;手机设置页内容列只有 277px,溢出部分被
|
|
2629
|
+
// overflow-x:hidden 静默裁掉(「检查更新」按钮被切一半)。
|
|
2630
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', minWidth: 0 } },
|
|
2611
2631
|
// 版本状态徽标
|
|
2612
2632
|
React.createElement('span', {
|
|
2613
2633
|
style: {
|
|
@@ -2674,6 +2694,10 @@ function VersionBanner({ rpcCall }) {
|
|
|
2674
2694
|
fontSize: 11,
|
|
2675
2695
|
opacity: loading ? 0.5 : 1,
|
|
2676
2696
|
gap: 4,
|
|
2697
|
+
// 按钮是 inline-flex,文字会成为匿名 flex 项;被挤窄时会把「检查更新」
|
|
2698
|
+
// 折成两行。nowrap + flexShrink:0 保证它整体换行(由父级 flexWrap 负责)。
|
|
2699
|
+
whiteSpace: 'nowrap',
|
|
2700
|
+
flexShrink: 0,
|
|
2677
2701
|
},
|
|
2678
2702
|
onClick: check,
|
|
2679
2703
|
disabled: loading || upgrading || restarting,
|
|
@@ -2951,8 +2975,57 @@ const TABS = [
|
|
|
2951
2975
|
{ id: 'ops', label: '运维监控', icon: Icons.ops },
|
|
2952
2976
|
];
|
|
2953
2977
|
|
|
2978
|
+
// Tab 条在窄屏是横向可滑动的(5 个分类在手机内容列里放不下),但滚动条被
|
|
2979
|
+
// mobile-styles.js 整段隐藏(.dsh-tabbar-container),末尾分类又恰好被切断,
|
|
2980
|
+
// 用户既看不到"还有更多"、也不知道能横滑 —— 主观上就是"页面看不到"。
|
|
2981
|
+
// 这里补两件事:按可滚动方向做边缘渐隐提示;切换时把当前项滚进可视区。
|
|
2954
2982
|
function TabBar({ active, onChange, dots }) {
|
|
2983
|
+
const scrollRef = React.useRef(null);
|
|
2984
|
+
const [edges, setEdges] = React.useState({ left: false, right: false });
|
|
2985
|
+
|
|
2986
|
+
React.useEffect(() => {
|
|
2987
|
+
const el = scrollRef.current;
|
|
2988
|
+
if (!el) return undefined;
|
|
2989
|
+
const update = () => {
|
|
2990
|
+
const max = el.scrollWidth - el.clientWidth;
|
|
2991
|
+
const left = el.scrollLeft > 1;
|
|
2992
|
+
const right = max > 1 && el.scrollLeft < max - 1;
|
|
2993
|
+
setEdges((prev) => (prev.left === left && prev.right === right ? prev : { left, right }));
|
|
2994
|
+
};
|
|
2995
|
+
update();
|
|
2996
|
+
el.addEventListener('scroll', update, { passive: true });
|
|
2997
|
+
let observer = null;
|
|
2998
|
+
if (typeof ResizeObserver === 'function') {
|
|
2999
|
+
observer = new ResizeObserver(update);
|
|
3000
|
+
observer.observe(el);
|
|
3001
|
+
}
|
|
3002
|
+
return () => {
|
|
3003
|
+
el.removeEventListener('scroll', update);
|
|
3004
|
+
if (observer) observer.disconnect();
|
|
3005
|
+
};
|
|
3006
|
+
}, []);
|
|
3007
|
+
|
|
3008
|
+
// 当前分类可能在可视区之外(例如从状态点跳进某个 Tab),切换后主动滚入
|
|
3009
|
+
React.useEffect(() => {
|
|
3010
|
+
const el = scrollRef.current;
|
|
3011
|
+
if (!el) return;
|
|
3012
|
+
const target = el.querySelector('[data-dsh-tab-active="true"]');
|
|
3013
|
+
if (!target) return;
|
|
3014
|
+
const elRect = el.getBoundingClientRect();
|
|
3015
|
+
const tabRect = target.getBoundingClientRect();
|
|
3016
|
+
if (tabRect.left < elRect.left) el.scrollLeft -= (elRect.left - tabRect.left) + 8;
|
|
3017
|
+
else if (tabRect.right > elRect.right) el.scrollLeft += (tabRect.right - elRect.right) + 8;
|
|
3018
|
+
}, [active]);
|
|
3019
|
+
|
|
3020
|
+
const maskStops = [];
|
|
3021
|
+
if (edges.left) maskStops.push('transparent 0px', '#000 20px');
|
|
3022
|
+
else maskStops.push('#000 0px');
|
|
3023
|
+
if (edges.right) maskStops.push('#000 calc(100% - 20px)', 'transparent 100%');
|
|
3024
|
+
else maskStops.push('#000 100%');
|
|
3025
|
+
const maskImage = `linear-gradient(to right, ${maskStops.join(', ')})`;
|
|
3026
|
+
|
|
2955
3027
|
return React.createElement('div', {
|
|
3028
|
+
ref: scrollRef,
|
|
2956
3029
|
className: 'dsh-tabbar-container',
|
|
2957
3030
|
style: {
|
|
2958
3031
|
display: 'flex', gap: 4, marginBottom: 20,
|
|
@@ -2961,6 +3034,8 @@ function TabBar({ active, onChange, dots }) {
|
|
|
2961
3034
|
maxWidth: '100%', flexWrap: 'nowrap',
|
|
2962
3035
|
scrollbarWidth: 'none',
|
|
2963
3036
|
msOverflowStyle: 'none',
|
|
3037
|
+
WebkitMaskImage: maskImage,
|
|
3038
|
+
maskImage,
|
|
2964
3039
|
},
|
|
2965
3040
|
},
|
|
2966
3041
|
TABS.map(({ id, label, icon: TabIcon }) => {
|
|
@@ -2968,6 +3043,7 @@ function TabBar({ active, onChange, dots }) {
|
|
|
2968
3043
|
const hasDot = dots?.[id];
|
|
2969
3044
|
return React.createElement('button', {
|
|
2970
3045
|
key: id,
|
|
3046
|
+
'data-dsh-tab-active': isActive ? 'true' : undefined,
|
|
2971
3047
|
onClick: () => onChange(id),
|
|
2972
3048
|
style: {
|
|
2973
3049
|
font: 'inherit', cursor: 'pointer', border: 'none', background: 'none',
|
|
@@ -3756,6 +3832,86 @@ function injectMobileStyles() {
|
|
|
3756
3832
|
document.head.appendChild(style);
|
|
3757
3833
|
}
|
|
3758
3834
|
|
|
3835
|
+
// 设置中心两级钻取的交互层(配合 mobile-styles.js 6.2 的 <=480px 规则)。
|
|
3836
|
+
//
|
|
3837
|
+
// 为什么需要 JS:宿主的设置弹窗(@deepseek-ai/dsh-client-ui-settings-general)只有
|
|
3838
|
+
// 桌面布局,nav 与 content 是 panel 下的兄弟节点、由 React 按 activeId 渲染,
|
|
3839
|
+
// 没有任何"列表页/详情页"状态,也没有暴露给插件的 slot。
|
|
3840
|
+
//
|
|
3841
|
+
// 为什么用「委托点击 + 打 data 属性」而不是注入返回按钮:
|
|
3842
|
+
// panel 内部整棵树由 React 管理,往里插 DOM 会在 React 卸载/重渲染时触发
|
|
3843
|
+
// removeChild 失配。故不插入任何节点,只给 panel 打一个属性;详情页的返回入口
|
|
3844
|
+
// 复用宿主已有的标题行(CSS 把 navList 收起、给 navTitle 加 ‹ 箭头并显示 pointer),
|
|
3845
|
+
// 因此本函数对宿主 DOM 只做「读 + 打属性」,不改变结构。
|
|
3846
|
+
function setupSettingsDrilldown() {
|
|
3847
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
3848
|
+
const root = document.documentElement;
|
|
3849
|
+
// 幂等位刻意放在最前:它先于 matchMedia 守卫,故首次 apply() 时若 matchMedia 不可用,
|
|
3850
|
+
// 本会话内就不再尝试启用钻取。方向是 fail-safe —— 退回 88px 图标轨道、内容完整可达
|
|
3851
|
+
// (CSS 6.2 整段惰性),不会变成死列表。独立验收已确认该方向无风险。
|
|
3852
|
+
if (root.dataset[DRILLDOWN_GATE_FLAG] === '1') return;
|
|
3853
|
+
root.dataset[DRILLDOWN_GATE_FLAG] = '1';
|
|
3854
|
+
|
|
3855
|
+
// 断点谓词与 CSS 开关必须同源:两者都用这一个 MediaQueryList 实例。
|
|
3856
|
+
// 若让 CSS 走 `@media (max-width:480px)`、而 JS 各自去问 matchMedia,一旦
|
|
3857
|
+
// matchMedia 缺失或对该查询返回 false(polyfill/环境差异),就会出现
|
|
3858
|
+
// 「监听器装上了但恒不匹配 → 回调永远空转,而开关照常打开」的死列表。
|
|
3859
|
+
// 因此 matchMedia 不可用时不启用钻取,直接退回 88px 轨道。
|
|
3860
|
+
if (typeof window.matchMedia !== 'function') return;
|
|
3861
|
+
let narrowQuery;
|
|
3862
|
+
try {
|
|
3863
|
+
narrowQuery = window.matchMedia(`(max-width: ${SETTINGS_DRILLDOWN_MAX_WIDTH}px)`);
|
|
3864
|
+
} catch {
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
if (!narrowQuery) return;
|
|
3868
|
+
const isNarrow = () => narrowQuery.matches === true;
|
|
3869
|
+
|
|
3870
|
+
const onDocumentClick = (event) => {
|
|
3871
|
+
if (!isNarrow()) return;
|
|
3872
|
+
const target = event.target;
|
|
3873
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
3874
|
+
|
|
3875
|
+
const panel = target.closest('div[class*="VOzbGW_panel"]');
|
|
3876
|
+
if (!panel) return;
|
|
3877
|
+
const nav = panel.querySelector('nav[class*="VOzbGW_nav"]');
|
|
3878
|
+
if (!nav || !nav.contains(target)) return;
|
|
3879
|
+
|
|
3880
|
+
// 点分类行 → 进入详情页(宿主自身的 onClick 负责切换 activeId,这里不拦截)
|
|
3881
|
+
if (target.closest('button[class*="VOzbGW_navCell"]')) {
|
|
3882
|
+
panel.setAttribute(SETTINGS_VIEW_ATTR, SETTINGS_VIEW_DETAIL);
|
|
3883
|
+
if (nav.scrollTop) nav.scrollTop = 0;
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
// 详情页里点折叠后的标题行 → 返回分类列表
|
|
3888
|
+
if (panel.getAttribute(SETTINGS_VIEW_ATTR) === SETTINGS_VIEW_DETAIL) {
|
|
3889
|
+
panel.removeAttribute(SETTINGS_VIEW_ATTR);
|
|
3890
|
+
}
|
|
3891
|
+
};
|
|
3892
|
+
|
|
3893
|
+
const syncGate = () => {
|
|
3894
|
+
if (narrowQuery.matches === true) root.setAttribute(DRILLDOWN_GATE_ATTR, DRILLDOWN_GATE_READY);
|
|
3895
|
+
else root.removeAttribute(DRILLDOWN_GATE_ATTR);
|
|
3896
|
+
};
|
|
3897
|
+
|
|
3898
|
+
try {
|
|
3899
|
+
document.addEventListener('click', onDocumentClick, false);
|
|
3900
|
+
} catch {
|
|
3901
|
+
// 装不上监听器就不要打开开关:CSS 6.2 保持惰性,<=480px 退回 767 块的 88px 图标轨道
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// 断点变化(转屏/窗口缩放)时让开关跟随,避免开关与实际匹配状态漂移
|
|
3906
|
+
try {
|
|
3907
|
+
if (typeof narrowQuery.addEventListener === 'function') narrowQuery.addEventListener('change', syncGate);
|
|
3908
|
+
else if (typeof narrowQuery.addListener === 'function') narrowQuery.addListener(syncGate);
|
|
3909
|
+
} catch { /* 订阅失败不影响正确性:下面的 syncGate() 仍会按当前 matches 设一次 */ }
|
|
3910
|
+
|
|
3911
|
+
// 监听器装好之后才同步 CSS 开关(见 mobile-styles.js 6.2 块的安全说明)
|
|
3912
|
+
syncGate();
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3759
3915
|
function setupMobileExperience(rpcCall, ctx) {
|
|
3760
3916
|
if (typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
3761
3917
|
injectMobileStyles();
|
|
@@ -4545,60 +4701,69 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4545
4701
|
async function switchToWorkspace(wsId, wsPath) {
|
|
4546
4702
|
if (isSubmitting) return;
|
|
4547
4703
|
isSubmitting = true;
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
if (typeof onPicked === 'function' && wsPath) {
|
|
4553
|
-
try {
|
|
4554
|
-
onPicked(wsPath);
|
|
4555
|
-
} catch (e) {}
|
|
4556
|
-
}
|
|
4704
|
+
try {
|
|
4705
|
+
statusMessage = `正在切换工作区…`;
|
|
4706
|
+
isErrorMessage = false;
|
|
4707
|
+
render();
|
|
4557
4708
|
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
switched = true;
|
|
4563
|
-
} catch (e) {
|
|
4564
|
-
console.warn('[dsh-bridge] startSession failed:', e);
|
|
4709
|
+
if (typeof onPicked === 'function' && wsPath) {
|
|
4710
|
+
try {
|
|
4711
|
+
onPicked(wsPath);
|
|
4712
|
+
} catch (e) {}
|
|
4565
4713
|
}
|
|
4566
|
-
}
|
|
4567
4714
|
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
}
|
|
4715
|
+
let switched = false;
|
|
4716
|
+
if (clientCtx?.workspaces?.startSession && wsId) {
|
|
4717
|
+
try {
|
|
4718
|
+
clientCtx.workspaces.startSession(wsId);
|
|
4719
|
+
switched = true;
|
|
4720
|
+
} catch (e) {
|
|
4721
|
+
console.warn('[dsh-bridge] startSession failed:', e);
|
|
4576
4722
|
}
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
if (!switched && wsPath) {
|
|
4726
|
+
try {
|
|
4727
|
+
if (clientCtx?.workspaces?.create) {
|
|
4728
|
+
const ws = await clientCtx.workspaces.create({ path: wsPath });
|
|
4729
|
+
if (ws?.workspaceId && clientCtx?.workspaces?.startSession) {
|
|
4730
|
+
clientCtx.workspaces.startSession(ws.workspaceId);
|
|
4583
4731
|
switched = true;
|
|
4584
|
-
}
|
|
4732
|
+
}
|
|
4585
4733
|
}
|
|
4586
|
-
if (!switched
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4734
|
+
if (!switched) {
|
|
4735
|
+
const raw = await authRpc(BRIDGE_ENDPOINTS.addRemoteWorkspace, { path: wsPath });
|
|
4736
|
+
const res = raw?.value || raw;
|
|
4737
|
+
if (res?.workspaceId && clientCtx?.workspaces?.startSession) {
|
|
4738
|
+
try {
|
|
4739
|
+
clientCtx.workspaces.startSession(res.workspaceId);
|
|
4740
|
+
switched = true;
|
|
4741
|
+
} catch (e) {}
|
|
4742
|
+
}
|
|
4743
|
+
if (!switched && res?.sessionId && clientCtx?.sessions?.open) {
|
|
4744
|
+
try {
|
|
4745
|
+
// 最后手段:直接打开会话。成功后无需再置位 switched ——
|
|
4746
|
+
// 此后不再有基于 switched 的分支(CodeQL js/useless-assignment-to-local)。
|
|
4747
|
+
clientCtx.sessions.open(res.sessionId);
|
|
4748
|
+
} catch (e) {}
|
|
4749
|
+
}
|
|
4591
4750
|
}
|
|
4592
|
-
}
|
|
4593
|
-
}
|
|
4594
|
-
}
|
|
4751
|
+
} catch (e) {}
|
|
4752
|
+
}
|
|
4595
4753
|
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4754
|
+
statusMessage = `✓ 已切换至工作区!`;
|
|
4755
|
+
render();
|
|
4756
|
+
setTimeout(() => {
|
|
4757
|
+
closeModal();
|
|
4758
|
+
document.body.classList.remove('dsh-drawer-open');
|
|
4759
|
+
}, 400);
|
|
4760
|
+
} finally {
|
|
4761
|
+
// 与 doSubmit 对称:本函数此前只置位、从不复位,导致点过一次「已注册工作区」后,
|
|
4762
|
+
// 同一弹窗实例的 isSubmitting 永久为 true,loadDirectory/doSubmit 的守卫
|
|
4763
|
+
// 让整个弹窗彻底无响应(点目录、点添加都没反应)。用 finally 兜住所有路径,
|
|
4764
|
+
// 包括 render() 抛错以及将来新增的提前 return。
|
|
4765
|
+
isSubmitting = false;
|
|
4766
|
+
}
|
|
4602
4767
|
}
|
|
4603
4768
|
|
|
4604
4769
|
async function loadDirectory(targetPath) {
|
|
@@ -4708,8 +4873,9 @@ function showRemoteWorkspaceDialog(rpcCall, onWorkspaceAdded, clientCtx, onPicke
|
|
|
4708
4873
|
|
|
4709
4874
|
if (!switched && clientCtx?.sessions?.open && res.sessionId) {
|
|
4710
4875
|
try {
|
|
4876
|
+
// 最后手段:直接打开会话。此后不再读取 switched
|
|
4877
|
+
// (CodeQL js/useless-assignment-to-local),故不再置位。
|
|
4711
4878
|
clientCtx.sessions.open(res.sessionId);
|
|
4712
|
-
switched = true;
|
|
4713
4879
|
} catch (e) {
|
|
4714
4880
|
console.warn('[dsh-bridge] sessions.open failed:', e);
|
|
4715
4881
|
}
|
|
@@ -5028,6 +5194,9 @@ function setupComposerCollapse() {
|
|
|
5028
5194
|
|
|
5029
5195
|
function apply(ctx) {
|
|
5030
5196
|
window.__dshClientCtx = ctx;
|
|
5197
|
+
// 放在最前:交互层先就绪,后续任何初始化抛异常都不会留下「CSS 生效但监听器缺失」
|
|
5198
|
+
// 的状态(CSS 6.2 块另有 <html> 就绪开关双重兜底)。
|
|
5199
|
+
setupSettingsDrilldown();
|
|
5031
5200
|
const rpcCall = (endpoint, payload, signal) =>
|
|
5032
5201
|
ctx.connection.rpc.call(BRIDGE_RPC_CHANNEL, endpoint, payload, signal);
|
|
5033
5202
|
|
|
@@ -5041,9 +5210,22 @@ function apply(ctx) {
|
|
|
5041
5210
|
|
|
5042
5211
|
const injected = () => ({ pick: () => ctx.workspaces?.pickDirectory?.() });
|
|
5043
5212
|
|
|
5044
|
-
// 注册至 DSH 原生目录选择 Slot
|
|
5213
|
+
// 注册至 DSH 原生目录选择 Slot(priority: -10 覆盖原生 Electron 选择器,在远程/移动网页端生效)
|
|
5214
|
+
//
|
|
5215
|
+
// 但 DSH 0.1.5 起自带官方目录选择器(@deepseek-ai/dsh-host-directory-picker-auto
|
|
5216
|
+
// 按宿主能力分发 native / browse),且注册到完全相同的两个 Slot。DSH 的 slots 是
|
|
5217
|
+
// shadow 语义(动态注册的插件天然覆盖内置实现,详见 client/picker-yield.js),
|
|
5218
|
+
// 因此本机 + 官方 picker 可用时必须「不注册」才能让位 —— 用低优先级注册是无效的,
|
|
5219
|
+
// 否则本机 127.0.0.1 点「添加工作区」弹的是插件的远程抽屉(issue #28 第 2 条)。
|
|
5220
|
+
// 旧版 DSH(无官方 picker)与远程/移动访问继续由插件接管,后者保留管理密码解锁
|
|
5221
|
+
// 与 local_only 目录策略(换成官方 picker 会绕过插件的访问限制)。
|
|
5045
5222
|
ctx.slots.inject('conversation.hero.workspace.directoryFlow', () =>
|
|
5046
5223
|
ctx.slots.inject('sidebar.workspaces.directoryFlow', function* () {
|
|
5224
|
+
if (shouldYieldToOfficialPicker({
|
|
5225
|
+
local: isLocalEnvironment(),
|
|
5226
|
+
officialPicker: hasOfficialDirectoryPicker(ctx),
|
|
5227
|
+
})) return;
|
|
5228
|
+
|
|
5047
5229
|
yield ctx.slots.register(
|
|
5048
5230
|
{
|
|
5049
5231
|
name: 'conversation.hero.workspace.directoryFlow',
|