dsh-remote-panel 0.1.0 → 0.2.2
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/README.md +43 -1
- package/deploy/templates/Caddyfile.tmpl +34 -6
- package/deploy/templates/install.sh.tmpl +4 -3
- package/lib/allowlist.js +2 -4
- package/lib/api.js +18 -0
- package/lib/assets/icon-180.png +0 -0
- package/lib/assets/icon-192.png +0 -0
- package/lib/assets/icon-512.png +0 -0
- package/lib/assets/icon-maskable-512.png +0 -0
- package/lib/client.js +1016 -81
- package/lib/deploy/render.js +19 -2
- package/lib/index.js +333 -29
- package/lib/pair.js +15 -5
- package/lib/probe.js +258 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -19,6 +19,8 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
const API = '/remote-panel/api'
|
|
20
20
|
const TOKEN_GLOBAL = '__DSH_REMOTE_PANEL__'
|
|
21
21
|
const TOKEN_HEADER = 'x-dsh-remote-panel'
|
|
22
|
+
// HMR 时新模块实例接手旧实例的内容增强(同一页面只允许一个 Observer)
|
|
23
|
+
const CONTENT_CLEANUP_KEY = '__DSH_REMOTE_PANEL_CONTENT_ENHANCEMENTS__'
|
|
22
24
|
const stroke = 'var(--dsw-elevation-stroke-color, rgba(128,128,128,.24))'
|
|
23
25
|
|
|
24
26
|
const styles = {
|
|
@@ -35,6 +37,7 @@ window.__ModuleLoader__.load({
|
|
|
35
37
|
title: { fontSize: 14, fontWeight: 600, margin: 0 },
|
|
36
38
|
muted: { opacity: 0.62, margin: 0 },
|
|
37
39
|
hint: { fontSize: 11, opacity: 0.5 },
|
|
40
|
+
link: { color: '#4d7dfe', textDecoration: 'underline', cursor: 'pointer' },
|
|
38
41
|
row: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
|
|
39
42
|
grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 10 },
|
|
40
43
|
field: { display: 'flex', flexDirection: 'column', gap: 4 },
|
|
@@ -280,7 +283,7 @@ window.__ModuleLoader__.load({
|
|
|
280
283
|
jsx(Dot, { ok: check.state === 'ok' }),
|
|
281
284
|
jsx('span', {
|
|
282
285
|
style: { ...styles.hint, minWidth: 78, opacity: 0.75 },
|
|
283
|
-
children: check.id,
|
|
286
|
+
children: check.label ?? check.id,
|
|
284
287
|
}),
|
|
285
288
|
jsx('span', { style: styles.hint, children: check.detail }),
|
|
286
289
|
],
|
|
@@ -409,6 +412,20 @@ window.__ModuleLoader__.load({
|
|
|
409
412
|
),
|
|
410
413
|
),
|
|
411
414
|
}),
|
|
415
|
+
jsxs('label', {
|
|
416
|
+
style: { ...styles.row, marginTop: 8, fontSize: 12 },
|
|
417
|
+
children: [
|
|
418
|
+
jsx('input', {
|
|
419
|
+
type: 'checkbox',
|
|
420
|
+
checked: form.tls === true,
|
|
421
|
+
onChange: (event) => setForm((current) => ({ ...current, tls: event.target.checked })),
|
|
422
|
+
}),
|
|
423
|
+
jsx('span', {
|
|
424
|
+
style: styles.muted,
|
|
425
|
+
children: '启用 HTTPS(Caddy 内部 CA;手机需先装根证书,见「远程访问 → 手机全屏」里的下载链接)',
|
|
426
|
+
}),
|
|
427
|
+
],
|
|
428
|
+
}),
|
|
412
429
|
jsxs('div', {
|
|
413
430
|
style: styles.row,
|
|
414
431
|
children: [
|
|
@@ -524,7 +541,7 @@ window.__ModuleLoader__.load({
|
|
|
524
541
|
|
|
525
542
|
// ---------------------------------------------------------------- 第二幕
|
|
526
543
|
|
|
527
|
-
function StatusCard({ state, version, onRedeploy }) {
|
|
544
|
+
function StatusCard({ state, version, scheme, onRedeploy }) {
|
|
528
545
|
const [probe, setProbe] = useState(null)
|
|
529
546
|
const [rejects, setRejects] = useState([])
|
|
530
547
|
const [busy, setBusy] = useState(false)
|
|
@@ -545,11 +562,16 @@ window.__ModuleLoader__.load({
|
|
|
545
562
|
refresh()
|
|
546
563
|
}, [refresh])
|
|
547
564
|
|
|
565
|
+
const cert = probe?.cert
|
|
566
|
+
const certHours = typeof cert?.hoursLeft === 'number' ? Math.round(cert.hoursLeft * 10) / 10 : null
|
|
548
567
|
const lights = [
|
|
549
568
|
['EasyTier', probe?.easytier],
|
|
550
569
|
['Caddy', probe?.caddy],
|
|
551
570
|
['healthz', probe?.healthz],
|
|
552
571
|
['上游 DSH', probe?.upstream],
|
|
572
|
+
// 内部 CA 的叶子证书只有 12 小时,续签失败时探活照样握手成功(rejectUnauthorized:false),
|
|
573
|
+
// 所以单独看它:剩余不足 2 小时或 SAN 不匹配都算红灯
|
|
574
|
+
[certHours === null ? '证书' : `证书 ${certHours}h`, cert === undefined ? undefined : { ok: cert.ok === true }],
|
|
553
575
|
]
|
|
554
576
|
|
|
555
577
|
return jsxs(Card, {
|
|
@@ -559,7 +581,7 @@ window.__ModuleLoader__.load({
|
|
|
559
581
|
children: [
|
|
560
582
|
jsx(Dot, { ok: probe?.ok }),
|
|
561
583
|
jsx('strong', { style: styles.title, children: '运行状态' }),
|
|
562
|
-
jsx('span', { style: styles.muted, children:
|
|
584
|
+
jsx('span', { style: styles.muted, children: `${scheme ?? (window.location?.protocol ?? 'http:').replace(':', '')}://${state.ip}:${state.port}/` }),
|
|
563
585
|
jsx('button', {
|
|
564
586
|
type: 'button',
|
|
565
587
|
style: styles.button,
|
|
@@ -569,6 +591,12 @@ window.__ModuleLoader__.load({
|
|
|
569
591
|
}),
|
|
570
592
|
],
|
|
571
593
|
}),
|
|
594
|
+
cert !== undefined && cert.ok !== true
|
|
595
|
+
? jsx('p', {
|
|
596
|
+
style: { ...styles.hint, color: '#f2b8b5' },
|
|
597
|
+
children: `证书告警:${cert.reason ?? '不可用'}${certHours === null ? '' : `(剩余 ${certHours} 小时)`}`,
|
|
598
|
+
})
|
|
599
|
+
: null,
|
|
572
600
|
jsx('div', {
|
|
573
601
|
style: styles.row,
|
|
574
602
|
children: lights.map(([label, item]) =>
|
|
@@ -651,12 +679,19 @@ window.__ModuleLoader__.load({
|
|
|
651
679
|
|
|
652
680
|
function PairCard() {
|
|
653
681
|
const [state, setState] = useState({ kind: 'loading' })
|
|
682
|
+
const [round, setRound] = useState(0)
|
|
683
|
+
|
|
684
|
+
// DSH 每次重启都会换令牌;面板开着时自动刷新,避免扫到过期的码
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
const timer = setInterval(() => setRound((value) => value + 1), 60000)
|
|
687
|
+
return () => clearInterval(timer)
|
|
688
|
+
}, [])
|
|
654
689
|
|
|
655
690
|
useEffect(() => {
|
|
656
691
|
callApi('/pair')
|
|
657
692
|
.then((data) => setState({ kind: 'ok', data }))
|
|
658
693
|
.catch((error) => setState({ kind: 'error', message: error.message }))
|
|
659
|
-
}, [])
|
|
694
|
+
}, [round])
|
|
660
695
|
|
|
661
696
|
const data = state.kind === 'ok' ? state.data : null
|
|
662
697
|
return jsxs(Card, {
|
|
@@ -667,6 +702,7 @@ window.__ModuleLoader__.load({
|
|
|
667
702
|
jsx(Dot, { ok: data?.found === true }),
|
|
668
703
|
jsx('strong', { style: styles.title, children: '手机配对' }),
|
|
669
704
|
jsx('span', { style: styles.muted, children: state.kind === 'loading' ? '读取中…' : data?.found ? '扫码即可' : '未找到令牌' }),
|
|
705
|
+
jsx('button', { type: 'button', style: styles.button, onClick: () => setRound((value) => value + 1), children: '刷新' }),
|
|
670
706
|
],
|
|
671
707
|
}),
|
|
672
708
|
state.kind === 'error' && jsx('p', { style: styles.muted, children: state.message }),
|
|
@@ -685,20 +721,40 @@ window.__ModuleLoader__.load({
|
|
|
685
721
|
jsxs('div', {
|
|
686
722
|
style: { display: 'flex', flexDirection: 'column', gap: 6, flex: 1, minWidth: 210 },
|
|
687
723
|
children: [
|
|
688
|
-
jsx('span', { style: styles.hint, children: '
|
|
724
|
+
jsx('span', { style: styles.hint, children: '扫这一张即可:先种门禁(一年),再自动跳到令牌' }),
|
|
725
|
+
// 二维码编的就是这条组合链接,显示同一个值,别再换成别的
|
|
689
726
|
jsx('code', {
|
|
690
|
-
style: { ...styles.code, maxHeight:
|
|
691
|
-
children: data.urls.
|
|
727
|
+
style: { ...styles.code, maxHeight: 72 },
|
|
728
|
+
children: data.urls.combo ?? data.urls.token,
|
|
692
729
|
}),
|
|
693
|
-
jsx('span', { style: styles.hint, children: '② 令牌(30 天 Cookie)' }),
|
|
694
|
-
jsx('code', { style: { ...styles.code, maxHeight: 56 }, children: data.urls.token }),
|
|
695
730
|
jsxs('div', {
|
|
696
731
|
style: styles.row,
|
|
697
732
|
children: [
|
|
698
|
-
jsx(CopyButton, { text: data.urls.token, label: '
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
733
|
+
jsx(CopyButton, { text: data.urls.combo ?? data.urls.token, label: '复制配对链接' }),
|
|
734
|
+
],
|
|
735
|
+
}),
|
|
736
|
+
jsx('details', {
|
|
737
|
+
children: [
|
|
738
|
+
jsx('summary', { style: styles.hint, children: '手动备用(门禁 / 令牌分开用)' }),
|
|
739
|
+
jsxs('div', {
|
|
740
|
+
style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 },
|
|
741
|
+
children: [
|
|
742
|
+
jsx('code', {
|
|
743
|
+
style: { ...styles.code, maxHeight: 56 },
|
|
744
|
+
children: data.urls.gate ?? '(回填码未含口令)',
|
|
745
|
+
}),
|
|
746
|
+
jsx('code', { style: { ...styles.code, maxHeight: 56 }, children: data.urls.token }),
|
|
747
|
+
jsxs('div', {
|
|
748
|
+
style: styles.row,
|
|
749
|
+
children: [
|
|
750
|
+
jsx(CopyButton, { text: data.urls.token, label: '复制令牌链接' }),
|
|
751
|
+
data.urls.gate
|
|
752
|
+
? jsx(CopyButton, { text: data.urls.gate, label: '复制门禁链接' })
|
|
753
|
+
: null,
|
|
754
|
+
],
|
|
755
|
+
}),
|
|
756
|
+
],
|
|
757
|
+
}),
|
|
702
758
|
],
|
|
703
759
|
}),
|
|
704
760
|
],
|
|
@@ -837,16 +893,108 @@ window.__ModuleLoader__.load({
|
|
|
837
893
|
|
|
838
894
|
// ---------------------------------------------------------------- 总装
|
|
839
895
|
|
|
896
|
+
/** 手机 App 化:全屏按钮 + 添加到主屏幕的指引(iOS 靠 A2HS,Android 靠 PWA/全屏 API)。 */
|
|
897
|
+
function AppCard() {
|
|
898
|
+
const [full, setFull] = useState(false)
|
|
899
|
+
useEffect(() => {
|
|
900
|
+
const sync = () => setFull(document.fullscreenElement != null)
|
|
901
|
+
document.addEventListener('fullscreenchange', sync)
|
|
902
|
+
sync()
|
|
903
|
+
return () => document.removeEventListener('fullscreenchange', sync)
|
|
904
|
+
}, [])
|
|
905
|
+
const standalone =
|
|
906
|
+
typeof window !== 'undefined' &&
|
|
907
|
+
(window.matchMedia?.('(display-mode: standalone)')?.matches === true || window.navigator?.standalone === true)
|
|
908
|
+
const secure = typeof window !== 'undefined' && window.location?.protocol === 'https:'
|
|
909
|
+
const [ping, setPing] = useState(() => {
|
|
910
|
+
try {
|
|
911
|
+
return localStorage.getItem('dsh-remote-panel:attention') !== 'off'
|
|
912
|
+
} catch {
|
|
913
|
+
return true
|
|
914
|
+
}
|
|
915
|
+
})
|
|
916
|
+
const togglePing = () => {
|
|
917
|
+
const next = !ping
|
|
918
|
+
setPing(next)
|
|
919
|
+
try {
|
|
920
|
+
localStorage.setItem('dsh-remote-panel:attention', next ? 'on' : 'off')
|
|
921
|
+
} catch {
|
|
922
|
+
/* 隐私模式忽略 */
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const supported = typeof document !== 'undefined' && document.fullscreenEnabled === true
|
|
926
|
+
const enter = () => {
|
|
927
|
+
document.documentElement.requestFullscreen?.().catch((error) => {
|
|
928
|
+
console.warn('[remote-panel] fullscreen failed', error)
|
|
929
|
+
})
|
|
930
|
+
}
|
|
931
|
+
return jsxs(Card, {
|
|
932
|
+
children: [
|
|
933
|
+
jsx('div', { style: styles.head, children: jsx('p', { style: styles.title, children: '手机 App' }) }),
|
|
934
|
+
jsx('p', {
|
|
935
|
+
style: styles.muted,
|
|
936
|
+
children: standalone
|
|
937
|
+
? '✓ 已经在独立窗口里运行,没有地址栏。'
|
|
938
|
+
: secure
|
|
939
|
+
? '把它装到主屏幕:装完后从图标打开就是独立窗口,不再有浏览器 UI。'
|
|
940
|
+
: '当前是 http,装不成独立应用;先用下面的按钮进全屏(或按 README 开启 HTTPS 再装)。',
|
|
941
|
+
}),
|
|
942
|
+
standalone || !secure
|
|
943
|
+
? null
|
|
944
|
+
: jsx('p', {
|
|
945
|
+
style: styles.hint,
|
|
946
|
+
children: 'Android:浏览器菜单 → 安装应用 / 添加到主屏幕;iPhone:分享 → 添加到主屏幕。',
|
|
947
|
+
}),
|
|
948
|
+
jsx('p', {
|
|
949
|
+
style: styles.hint,
|
|
950
|
+
children: [
|
|
951
|
+
'第一次装之前要先信任 Caddy 根证书:',
|
|
952
|
+
jsx('a', {
|
|
953
|
+
href: '/remote-panel/app/ca.crt',
|
|
954
|
+
download: 'dsh-caddy-root.crt',
|
|
955
|
+
style: styles.link,
|
|
956
|
+
children: ' 下载根证书 ',
|
|
957
|
+
}),
|
|
958
|
+
'(Android:设置 → 安全 → 加密与凭据 → 安装证书;iOS:下载后到「设置 → 通用 → 关于本机 → 证书信任设置」打开信任)。',
|
|
959
|
+
],
|
|
960
|
+
}),
|
|
961
|
+
supported && !standalone && !full
|
|
962
|
+
? jsx('div', {
|
|
963
|
+
style: styles.row,
|
|
964
|
+
children: jsx('button', {
|
|
965
|
+
type: 'button',
|
|
966
|
+
style: styles.primary,
|
|
967
|
+
onClick: enter,
|
|
968
|
+
children: '进入全屏',
|
|
969
|
+
}),
|
|
970
|
+
})
|
|
971
|
+
: null,
|
|
972
|
+
jsxs('label', {
|
|
973
|
+
style: { ...styles.row, marginTop: 10, fontSize: 12 },
|
|
974
|
+
children: [
|
|
975
|
+
jsx('input', { type: 'checkbox', checked: ping, onChange: togglePing }),
|
|
976
|
+
jsx('span', {
|
|
977
|
+
style: styles.muted,
|
|
978
|
+
children: '任务跑完 / 需要确认时震动并闪标题(页面在后台时)',
|
|
979
|
+
}),
|
|
980
|
+
],
|
|
981
|
+
}),
|
|
982
|
+
],
|
|
983
|
+
})
|
|
984
|
+
}
|
|
985
|
+
|
|
840
986
|
function Panel() {
|
|
841
987
|
const [stage, setStage] = useState('loading')
|
|
842
988
|
const [state, setState] = useState(null)
|
|
843
989
|
const [version, setVersion] = useState(null)
|
|
990
|
+
const [scheme, setScheme] = useState(null)
|
|
844
991
|
const [showWizard, setShowWizard] = useState(false)
|
|
845
992
|
|
|
846
993
|
const load = useCallback(async () => {
|
|
847
994
|
try {
|
|
848
995
|
const status = await callApi('/status')
|
|
849
996
|
setVersion(status.version)
|
|
997
|
+
setScheme(status.scheme ?? null)
|
|
850
998
|
const current = (await callApi('/state')).state
|
|
851
999
|
setState(current)
|
|
852
1000
|
setStage(current === null ? 'fresh' : 'running')
|
|
@@ -890,7 +1038,7 @@ window.__ModuleLoader__.load({
|
|
|
890
1038
|
}),
|
|
891
1039
|
state === null
|
|
892
1040
|
? null
|
|
893
|
-
: jsx(StatusCard, { state, version, onRedeploy: () => setShowWizard(false) }),
|
|
1041
|
+
: jsx(StatusCard, { state, version, scheme, onRedeploy: () => setShowWizard(false) }),
|
|
894
1042
|
state === null ? null : jsx(PairCard, {}),
|
|
895
1043
|
],
|
|
896
1044
|
})
|
|
@@ -900,9 +1048,10 @@ window.__ModuleLoader__.load({
|
|
|
900
1048
|
style: styles.wrap,
|
|
901
1049
|
children: [
|
|
902
1050
|
jsx(StaleBanner, {}),
|
|
903
|
-
jsx(StatusCard, { state, version, onRedeploy: () => setShowWizard(true) }),
|
|
1051
|
+
jsx(StatusCard, { state, version, scheme, onRedeploy: () => setShowWizard(true) }),
|
|
904
1052
|
jsx(PairCard, {}),
|
|
905
1053
|
jsx(AccessCard, {}),
|
|
1054
|
+
jsx(AppCard, {}),
|
|
906
1055
|
],
|
|
907
1056
|
})
|
|
908
1057
|
}
|
|
@@ -910,6 +1059,7 @@ window.__ModuleLoader__.load({
|
|
|
910
1059
|
// ---------------------------------------------------------------- 外壳
|
|
911
1060
|
|
|
912
1061
|
const SHELL_CSS = `
|
|
1062
|
+
/* 手机外壳:结构(主题无关) */
|
|
913
1063
|
@media (max-width: 1023px) {
|
|
914
1064
|
[class*="_frame"] { grid-template-columns: minmax(0, 1fr) 0 0 !important; }
|
|
915
1065
|
[class*="_sidebarCol"]:has([class*="_collapsed"]) { display: none !important; }
|
|
@@ -919,20 +1069,732 @@ window.__ModuleLoader__.load({
|
|
|
919
1069
|
height: auto !important;
|
|
920
1070
|
z-index: 900;
|
|
921
1071
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
922
|
-
|
|
1072
|
+
border-radius: 0 16px 16px 0 !important;
|
|
1073
|
+
box-shadow: 0 0 28px rgba(0, 0, 0, .32) !important;
|
|
1074
|
+
}
|
|
1075
|
+
/* 独立窗口里别再有下拉刷新 / 橡皮筋 */
|
|
1076
|
+
body { overscroll-behavior: none; }
|
|
1077
|
+
|
|
1078
|
+
/* 顶栏:52px;刘海屏/独立窗口把状态栏让出来 */
|
|
1079
|
+
.wSkVaW_header {
|
|
1080
|
+
min-height: calc(52px + env(safe-area-inset-top, 0px)) !important;
|
|
1081
|
+
padding: calc(11px + env(safe-area-inset-top, 0px)) 60px 0 10px !important;
|
|
1082
|
+
}
|
|
1083
|
+
.wSkVaW_headerSessionless { padding-top: calc(11px + env(safe-area-inset-top, 0px)) !important; }
|
|
1084
|
+
.wSkVaW_headerCorner { display: none !important; }
|
|
1085
|
+
/* 手机的「用文件管理器打开」是桌面动作,收掉 */
|
|
1086
|
+
.OMoRSG_split { display: none !important; }
|
|
1087
|
+
|
|
1088
|
+
/* 会话内顶栏瘦身(只影响手机):
|
|
1089
|
+
原生在标题右边依次摆「子智能体 / 后台任务」动作条和「更多操作(导出会话日志)」。
|
|
1090
|
+
390 下这两组固定件只给标题留下约 104px。这里:
|
|
1091
|
+
1) 「更多操作」整组收掉(桌面仍可导出);
|
|
1092
|
+
2) 子智能体只留 spinner + 箭头,后台任务只留数字角标;
|
|
1093
|
+
3) 标题允许吃满剩余宽度。
|
|
1094
|
+
☰ 和右下角的 +(新建对话)都不动。 */
|
|
1095
|
+
.wSkVaW_headerUtilities { display: none !important; }
|
|
1096
|
+
.wSkVaW_titleCluster { gap: 4px !important; }
|
|
1097
|
+
.wSkVaW_headerActions { gap: 4px !important; }
|
|
1098
|
+
.wSkVaW_crumbCurrent { max-width: 100% !important; }
|
|
1099
|
+
.ZKlsPq_trigger { padding: 2px !important; gap: 3px !important; }
|
|
1100
|
+
/* 子智能体:只留 spinner + 展开箭头(文字收进 aria-label / 弹层) */
|
|
1101
|
+
.ZKlsPq_trigger > span:not([class]) { display: none !important; }
|
|
1102
|
+
/* 后台任务:数字收成角标(data-rp-jobs 由脚本从原文案里提取) */
|
|
1103
|
+
.QsffPG_trigger { gap: 2px !important; padding: 2px !important; }
|
|
1104
|
+
.QsffPG_count { display: none !important; }
|
|
1105
|
+
.QsffPG_trigger::after {
|
|
1106
|
+
content: attr(data-rp-jobs);
|
|
1107
|
+
display: inline-flex;
|
|
1108
|
+
align-items: center;
|
|
1109
|
+
justify-content: center;
|
|
1110
|
+
box-sizing: border-box;
|
|
1111
|
+
min-width: 16px;
|
|
1112
|
+
height: 16px;
|
|
1113
|
+
padding: 0 3px;
|
|
1114
|
+
border-radius: 999px;
|
|
1115
|
+
background: rgba(128, 128, 128, .18);
|
|
1116
|
+
font-size: 11px;
|
|
1117
|
+
font-weight: 600;
|
|
1118
|
+
line-height: 16px;
|
|
1119
|
+
font-variant-numeric: tabular-nums;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/* 工作区 chip 右侧的 ×:回到原生「请选择工作区」空态(不发明新会话状态) */
|
|
1123
|
+
.dsh-mobile-clear-workspace {
|
|
1124
|
+
align-items: center;
|
|
1125
|
+
justify-content: center;
|
|
1126
|
+
flex: 0 0 auto;
|
|
1127
|
+
width: 22px;
|
|
1128
|
+
height: 22px;
|
|
1129
|
+
margin-left: 2px;
|
|
1130
|
+
padding: 0;
|
|
1131
|
+
border: 0;
|
|
1132
|
+
border-radius: 999px;
|
|
1133
|
+
background: transparent;
|
|
1134
|
+
color: rgba(255, 255, 255, .62);
|
|
1135
|
+
cursor: pointer;
|
|
1136
|
+
}
|
|
1137
|
+
.dsh-mobile-clear-workspace:hover { background: rgba(255, 255, 255, .12); color: #fff; }
|
|
1138
|
+
.dsh-mobile-hamburger { display: inline-flex !important; align-items: center; justify-content: center; height: 40px; padding: 0 !important; }
|
|
1139
|
+
.dsh-mobile-newchat,
|
|
1140
|
+
.dsh-mobile-clear-workspace { display: inline-flex !important; }
|
|
1141
|
+
/* 已经回到「请选择工作区」空态时没有可清空的对象 */
|
|
1142
|
+
.dsh-mobile-clear-workspace[data-none="true"] { display: none !important; }
|
|
1143
|
+
/* 抽屉/遮罩打开时顶栏按钮让位 */
|
|
1144
|
+
html.dsh-drawer-open .dsh-mobile-newchat { display: none !important; }
|
|
1145
|
+
|
|
1146
|
+
/* 空态:鱼标按原型放大并竖排到标题上方;标题 22/650,收掉「预览版」角标 */
|
|
1147
|
+
.pXSMma_headline { flex-direction: column !important; align-items: center !important; gap: 14px !important; }
|
|
1148
|
+
.pXSMma_fish, .pXSMma_fishHitbox { width: 112px !important; height: 82.4px !important; }
|
|
1149
|
+
.pXSMma_fish { color: #4d7dfe !important; }
|
|
1150
|
+
.pXSMma_titleGroup { justify-content: center !important; }
|
|
1151
|
+
.pXSMma_titleGroup,
|
|
1152
|
+
.pXSMma_titleGroup > * { font-size: 22px !important; font-weight: 650 !important; letter-spacing: -.02em !important; }
|
|
1153
|
+
.pXSMma_previewBadge { display: none !important; }
|
|
1154
|
+
/* 原生把「问候语 + 输入条」一起居中;手机按原型拆开:
|
|
1155
|
+
问候语留在屏幕中间(原生居中),输入条钉到底部 */
|
|
1156
|
+
.wSkVaW_composerHero .uV2eYG_root {
|
|
1157
|
+
position: fixed;
|
|
1158
|
+
left: 0;
|
|
1159
|
+
right: 0;
|
|
1160
|
+
bottom: max(18px, env(safe-area-inset-bottom, 0px));
|
|
1161
|
+
z-index: 55;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/* 工作区 chip:从空态 hero 行提到顶栏中间;只在空态存在,会话里不出现 */
|
|
1165
|
+
.wSkVaW_composerHero .wSkVaW_heroWorkspaceRow {
|
|
1166
|
+
position: fixed;
|
|
1167
|
+
left: 0;
|
|
1168
|
+
right: 0;
|
|
1169
|
+
z-index: 60;
|
|
1170
|
+
top: env(safe-area-inset-top, 0px);
|
|
1171
|
+
height: 52px;
|
|
1172
|
+
margin: 0;
|
|
1173
|
+
padding: 0 64px;
|
|
1174
|
+
align-items: center;
|
|
1175
|
+
justify-content: center;
|
|
1176
|
+
pointer-events: none;
|
|
1177
|
+
}
|
|
1178
|
+
.wSkVaW_heroWorkspaceRow > * { pointer-events: auto; }
|
|
1179
|
+
.wSkVaW_heroWorkspaceRow > :not([class*="_workspace"]):not(.dsh-mobile-clear-workspace) { display: none !important; }
|
|
1180
|
+
.pXSMma_workspace {
|
|
1181
|
+
height: 30px !important;
|
|
1182
|
+
padding: 0 10px 0 9px !important;
|
|
1183
|
+
border-radius: 999px !important;
|
|
1184
|
+
gap: 6px !important;
|
|
1185
|
+
}
|
|
1186
|
+
.pXSMma_workspaceLabel { font-size: 13px !important; font-weight: 600 !important; max-width: 160px; }
|
|
1187
|
+
.pXSMma_workspace svg { width: 14px; height: 14px; }
|
|
1188
|
+
|
|
1189
|
+
/* 输入条:一整块圆角卡片。按钮行保留原生全套(+ / 权限 / GenUI / 模型 / 发送) */
|
|
1190
|
+
.uV2eYG_card { border: 0 !important; border-radius: 26px !important; }
|
|
1191
|
+
.uV2eYG_scroll { padding: 10px 14px 0 !important; }
|
|
1192
|
+
.uV2eYG_input, .uV2eYG_placeholder { font-size: 15px !important; }
|
|
1193
|
+
.uV2eYG_row { padding: 6px 10px 10px !important; }
|
|
1194
|
+
|
|
1195
|
+
/* 抽屉:工作区列表置顶,面板入口退到列表下面,删掉品牌行与「新会话」行 */
|
|
1196
|
+
.hHd-Xa_root { display: flex; flex-direction: column; padding-top: env(safe-area-inset-top, 0px) !important; }
|
|
1197
|
+
.hHd-Xa_logoRow, .hHd-Xa_newSession { display: none !important; }
|
|
1198
|
+
.hHd-Xa_regionArea { order: 1; }
|
|
1199
|
+
/* 手机上的 DSH = 对话 + 改动 + 余额:只留「工作区 Git」,其余面板入口全收 */
|
|
1200
|
+
.hHd-Xa_panelList { order: 2; margin: 0 8px 4px !important; }
|
|
1201
|
+
.hHd-Xa_panelRow[aria-label="插件"],
|
|
1202
|
+
.hHd-Xa_panelRow[aria-label="自动化任务"],
|
|
1203
|
+
.hHd-Xa_panelRow[aria-label="命令面板"],
|
|
1204
|
+
.hHd-Xa_panelRow[aria-label="MCP连接器"],
|
|
1205
|
+
.hHd-Xa_panelRow[aria-label="更多"] { display: none !important; }
|
|
1206
|
+
.hHd-Xa_footArea { order: 3; border-top: 0 !important; }
|
|
1207
|
+
.bhn1Oq_sectionHeader { height: 32px !important; }
|
|
1208
|
+
.bhn1Oq_sectionLabel { font-size: 13px !important; }
|
|
1209
|
+
.YDXeBa_projectRow { height: 35px !important; border-radius: 12px !important; margin: 1px 10px !important; }
|
|
1210
|
+
.YDXeBa_sessionRow { height: 42px !important; border-radius: 12px !important; margin: 1px 10px !important; padding: 0 6px 0 10px !important; }
|
|
1211
|
+
.YDXeBa_sessionRow .YDXeBa_title { font-size: 13px !important; }
|
|
1212
|
+
|
|
1213
|
+
/* 搜索展开:抽屉整屏、盖掉面板入口与账号区(原型里搜索是一整页) */
|
|
1214
|
+
html.dsh-search-open [class*="_sidebarCol"]:not(:has([class*="_collapsed"])) {
|
|
1215
|
+
width: 100% !important;
|
|
1216
|
+
border-radius: 0 !important;
|
|
1217
|
+
box-shadow: none !important;
|
|
1218
|
+
}
|
|
1219
|
+
html.dsh-search-open .hHd-Xa_panelList,
|
|
1220
|
+
html.dsh-search-open .hHd-Xa_footArea { display: none !important; }
|
|
1221
|
+
html.dsh-search-open .bhn1Oq_searchExpanded { height: 40px !important; border-radius: 16px !important; }
|
|
1222
|
+
html.dsh-search-open .bhn1Oq_searchInput { height: 38px !important; }
|
|
1223
|
+
|
|
1224
|
+
/* 点遮罩关抽屉:原生窄屏没有这层,补一个(抽屉之下、正文之上) */
|
|
1225
|
+
html.dsh-drawer-open .dsh-mobile-scrim {
|
|
1226
|
+
display: block;
|
|
1227
|
+
position: fixed;
|
|
1228
|
+
inset: 0;
|
|
1229
|
+
z-index: 899;
|
|
1230
|
+
background: rgba(0, 0, 0, .35);
|
|
1231
|
+
}
|
|
1232
|
+
.dsh-mobile-scrim { display: none; }
|
|
1233
|
+
|
|
1234
|
+
/* 会话内容:代码块 / 表格 / 图片 / 附件在手机上不撑破页面 */
|
|
1235
|
+
.md-code-block { max-width: 100%; }
|
|
1236
|
+
.md-code-block pre,
|
|
1237
|
+
._markdown_1ypvv_5 pre {
|
|
1238
|
+
box-sizing: border-box;
|
|
1239
|
+
max-width: 100%;
|
|
1240
|
+
overflow-x: auto !important;
|
|
1241
|
+
-webkit-overflow-scrolling: touch;
|
|
1242
|
+
/* 原生 11px 在手机上太小;长 token 不再从中间断词,改为块内横向滚动 */
|
|
1243
|
+
font-size: 13px !important;
|
|
1244
|
+
line-height: 20px !important;
|
|
1245
|
+
word-break: normal;
|
|
1246
|
+
}
|
|
1247
|
+
.md-code-block pre code,
|
|
1248
|
+
._markdown_1ypvv_5 pre code,
|
|
1249
|
+
.md-code-block pre .line {
|
|
1250
|
+
font-size: 13px !important;
|
|
1251
|
+
line-height: 20px !important;
|
|
1252
|
+
}
|
|
1253
|
+
/* 原生 11px 的「复制」太小,手机上换成脚本注入的 28px 按钮 */
|
|
1254
|
+
.md-code-block [class*="_copyButton_"] { display: none !important; }
|
|
1255
|
+
.dsh-code-copy {
|
|
1256
|
+
box-sizing: border-box;
|
|
1257
|
+
min-width: 48px;
|
|
1258
|
+
min-height: 28px;
|
|
1259
|
+
padding: 2px 10px;
|
|
1260
|
+
border: 1px solid rgba(128, 128, 128, .28);
|
|
1261
|
+
border-radius: 8px;
|
|
1262
|
+
background: transparent;
|
|
1263
|
+
color: inherit;
|
|
1264
|
+
font: inherit;
|
|
1265
|
+
font-size: 12px;
|
|
1266
|
+
line-height: 20px;
|
|
1267
|
+
cursor: pointer;
|
|
1268
|
+
-webkit-tap-highlight-color: transparent;
|
|
1269
|
+
}
|
|
1270
|
+
.dsh-code-copy[data-done="true"] { opacity: .6; }
|
|
1271
|
+
.dsh-code-copy--float {
|
|
1272
|
+
position: absolute;
|
|
1273
|
+
top: 6px;
|
|
1274
|
+
right: 6px;
|
|
1275
|
+
z-index: 2;
|
|
1276
|
+
background: var(--dsw-alias-bg-base, rgba(0, 0, 0, .4));
|
|
1277
|
+
}
|
|
1278
|
+
._tableScroll_1ypvv_192 {
|
|
1279
|
+
max-width: 100%;
|
|
1280
|
+
overflow-x: auto !important;
|
|
1281
|
+
-webkit-overflow-scrolling: touch;
|
|
1282
|
+
}
|
|
1283
|
+
._markdown_1ypvv_5 img { max-width: 100% !important; height: auto !important; }
|
|
1284
|
+
/* 附件行:文件卡片 / 图片缩略图都不横向溢出 */
|
|
1285
|
+
.Sixlwa_attachmentRow {
|
|
1286
|
+
max-width: 100%;
|
|
1287
|
+
min-width: 0;
|
|
1288
|
+
overflow-x: auto;
|
|
1289
|
+
-webkit-overflow-scrolling: touch;
|
|
1290
|
+
}
|
|
1291
|
+
.Sixlwa_fileCard { flex: 0 1 240px; max-width: 100%; min-width: 0; }
|
|
1292
|
+
.R_Yw7q_gallery { max-width: 100%; min-width: 0; }
|
|
1293
|
+
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/* 手机外壳:深色配色(浅色主题保持原生配色,只换骨架) */
|
|
1297
|
+
@media (max-width: 1023px) {
|
|
1298
|
+
body[data-ds-dark-theme] { background: #000 !important; }
|
|
1299
|
+
body[data-ds-dark-theme] [class*="_frame"],
|
|
1300
|
+
body[data-ds-dark-theme] [class*="_centerCol"] { background: #000 !important; }
|
|
1301
|
+
body[data-ds-dark-theme] [class*="_sidebarCol"] {
|
|
1302
|
+
background: #000 !important;
|
|
1303
|
+
border: 0 !important;
|
|
1304
|
+
box-shadow: 0 0 32px rgba(0, 0, 0, .45);
|
|
1305
|
+
}
|
|
1306
|
+
body[data-ds-dark-theme] .hHd-Xa_root { background: #000 !important; }
|
|
1307
|
+
body[data-ds-dark-theme] .wSkVaW_header { border-bottom: 0 !important; background: transparent !important; }
|
|
1308
|
+
body[data-ds-dark-theme] .uV2eYG_card {
|
|
1309
|
+
background: #141416 !important;
|
|
1310
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06) !important;
|
|
1311
|
+
}
|
|
1312
|
+
body[data-ds-dark-theme] .pXSMma_workspace { background: rgba(255, 255, 255, .07) !important; }
|
|
1313
|
+
/* 列表底部的滚动渐隐原本渐到 #1b1b1c,在纯黑抽屉上会显出一条灰条 */
|
|
1314
|
+
body[data-ds-dark-theme] .bhn1Oq_fade {
|
|
1315
|
+
background-image: linear-gradient(rgba(0, 0, 0, 0), #000) !important;
|
|
923
1316
|
}
|
|
924
|
-
[class*="_centerCol"] { padding-bottom: 56px; box-sizing: border-box; }
|
|
925
|
-
html.dsh-drawer-open .dsh-mobile-bar { display: none; }
|
|
926
|
-
.dsh-mobile-hamburger { display: inline-flex !important; align-items: center; }
|
|
927
1317
|
}
|
|
928
1318
|
|
|
929
1319
|
@media (min-width: 1024px) {
|
|
930
|
-
.dsh-mobile-
|
|
931
|
-
.dsh-mobile-hamburger { display: none !important; }
|
|
1320
|
+
.dsh-mobile-hamburger, .dsh-mobile-newchat, .dsh-mobile-clear-workspace { display: none !important; }
|
|
932
1321
|
}
|
|
1322
|
+
/* 断线胶囊:连续两次探活失败才出现,恢复即消失(任何宽度都显示) */
|
|
1323
|
+
.dsh-mobile-offline {
|
|
1324
|
+
position: fixed;
|
|
1325
|
+
top: calc(env(safe-area-inset-top, 0px) + 6px);
|
|
1326
|
+
left: 50%;
|
|
1327
|
+
transform: translateX(-50%);
|
|
1328
|
+
z-index: 80;
|
|
1329
|
+
display: flex;
|
|
1330
|
+
align-items: center;
|
|
1331
|
+
gap: 6px;
|
|
1332
|
+
padding: 6px 12px;
|
|
1333
|
+
border-radius: 999px;
|
|
1334
|
+
font-size: 12px;
|
|
1335
|
+
line-height: 1.2;
|
|
1336
|
+
color: #f2b8b5;
|
|
1337
|
+
background: rgba(40, 40, 44, .94);
|
|
1338
|
+
border: 1px solid rgba(242, 184, 181, .25);
|
|
1339
|
+
pointer-events: none;
|
|
1340
|
+
}
|
|
1341
|
+
.dsh-mobile-offline::before {
|
|
1342
|
+
content: '';
|
|
1343
|
+
width: 6px;
|
|
1344
|
+
height: 6px;
|
|
1345
|
+
border-radius: 999px;
|
|
1346
|
+
background: #f2b8b5;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
/* 设置弹窗:原生是「左导航 188 + 右内容」,到 390 内容列只剩 154。
|
|
1351
|
+
这里把导航改成顶部一条可横滑的标签条,内容占满宽度;只匹配「有导航列的 dialog」。 */
|
|
1352
|
+
[role="dialog"]:has(> [class*="_nav"]) {
|
|
1353
|
+
flex-direction: column !important;
|
|
1354
|
+
width: calc(100vw - 16px) !important;
|
|
1355
|
+
max-width: none !important;
|
|
1356
|
+
max-height: calc(100dvh - 24px);
|
|
1357
|
+
}
|
|
1358
|
+
[role="dialog"] > [class*="_nav"] {
|
|
1359
|
+
flex: 0 0 auto !important;
|
|
1360
|
+
width: 100% !important;
|
|
1361
|
+
height: auto !important;
|
|
1362
|
+
padding: 4px 8px 0 !important;
|
|
1363
|
+
border-right: 0 !important;
|
|
1364
|
+
border-bottom: 1px solid rgba(255, 255, 255, .08);
|
|
1365
|
+
}
|
|
1366
|
+
[role="dialog"] > [class*="_nav"] > [class*="_navTitle"] { display: none !important; }
|
|
1367
|
+
[role="dialog"] > [class*="_nav"] [class*="_navList"] {
|
|
1368
|
+
flex-direction: row !important;
|
|
1369
|
+
height: auto !important;
|
|
1370
|
+
overflow-x: auto;
|
|
1371
|
+
overflow-y: hidden;
|
|
1372
|
+
gap: 2px;
|
|
1373
|
+
padding-bottom: 4px;
|
|
1374
|
+
scrollbar-width: none;
|
|
1375
|
+
}
|
|
1376
|
+
[role="dialog"] > [class*="_nav"] [class*="_navList"]::-webkit-scrollbar { height: 0; }
|
|
1377
|
+
[role="dialog"] > [class*="_nav"] [class*="_navCell"] {
|
|
1378
|
+
flex: 0 0 auto !important;
|
|
1379
|
+
width: auto !important;
|
|
1380
|
+
height: 34px !important;
|
|
1381
|
+
padding: 0 10px !important;
|
|
1382
|
+
white-space: nowrap;
|
|
1383
|
+
}
|
|
1384
|
+
[role="dialog"] > [class*="_content"] {
|
|
1385
|
+
flex: 1 1 auto !important;
|
|
1386
|
+
width: 100% !important;
|
|
1387
|
+
min-width: 0 !important;
|
|
1388
|
+
overflow-y: auto;
|
|
1389
|
+
-webkit-overflow-scrolling: touch;
|
|
1390
|
+
overscroll-behavior: contain;
|
|
1391
|
+
padding-bottom: env(safe-area-inset-bottom, 0px) !important;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/* 长会话滚动:离屏的 markdown 块不参与布局/绘制。
|
|
1395
|
+
DSH 不虚拟化消息,几百轮之后节点全在文档里;content-visibility 让浏览器跳过离屏块,
|
|
1396
|
+
滚动时只画视口附近的内容。只在窄屏生效,桌面保持原样。 */
|
|
1397
|
+
@media (max-width: 1023px) {
|
|
1398
|
+
[class*="_markdown"] {
|
|
1399
|
+
content-visibility: auto;
|
|
1400
|
+
contain-intrinsic-size: auto 320px;
|
|
1401
|
+
}
|
|
1402
|
+
[class*="_scrollBody"] {
|
|
1403
|
+
overscroll-behavior: contain;
|
|
1404
|
+
-webkit-overflow-scrolling: touch;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
933
1408
|
`
|
|
1409
|
+
/**
|
|
1410
|
+
* 工作区 chip 右侧的 ×。
|
|
1411
|
+
* 宿主没有「无工作区草稿」——所以这里只做一件原生已有的事:
|
|
1412
|
+
* 取消当前选中(uiWorkspace.clearMain),回到「请选择工作区」的空态。
|
|
1413
|
+
*/
|
|
1414
|
+
function installWorkspaceClear(ctx) {
|
|
1415
|
+
if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {}
|
|
1416
|
+
const tag = `rp-${Math.random().toString(36).slice(2, 8)}`
|
|
1417
|
+
// 没有选中会话(原生「请选择工作区」空态)时不该出现 ×
|
|
1418
|
+
const isEmpty = () => ctx.get('uiWorkspace')?.mainReference === undefined
|
|
1419
|
+
const ensure = () => {
|
|
1420
|
+
const row = document.querySelector('.wSkVaW_heroWorkspaceRow')
|
|
1421
|
+
if (!row) return
|
|
1422
|
+
const existing = row.querySelector('.dsh-mobile-clear-workspace')
|
|
1423
|
+
if (existing && existing.dataset.rp === tag) {
|
|
1424
|
+
existing.dataset.none = String(isEmpty())
|
|
1425
|
+
return
|
|
1426
|
+
}
|
|
1427
|
+
if (existing) existing.remove()
|
|
1428
|
+
const clear = document.createElement('button')
|
|
1429
|
+
clear.type = 'button'
|
|
1430
|
+
clear.className = 'dsh-mobile-clear-workspace'
|
|
1431
|
+
clear.title = '重新选择工作区'
|
|
1432
|
+
clear.setAttribute('aria-label', '重新选择工作区')
|
|
1433
|
+
clear.dataset.rp = tag
|
|
1434
|
+
clear.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>'
|
|
1435
|
+
clear.addEventListener('click', (event) => {
|
|
1436
|
+
event.preventDefault()
|
|
1437
|
+
event.stopPropagation()
|
|
1438
|
+
try {
|
|
1439
|
+
ctx.get('uiWorkspace')?.clearMain?.()
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
console.warn('[remote-panel] clear selection failed', error)
|
|
1442
|
+
}
|
|
1443
|
+
})
|
|
1444
|
+
clear.dataset.none = String(isEmpty())
|
|
1445
|
+
row.appendChild(clear)
|
|
1446
|
+
}
|
|
1447
|
+
const observer = new MutationObserver(ensure)
|
|
1448
|
+
observer.observe(document.body, { subtree: true, childList: true })
|
|
1449
|
+
ensure()
|
|
1450
|
+
return () => {
|
|
1451
|
+
observer.disconnect()
|
|
1452
|
+
document.querySelector('.dsh-mobile-clear-workspace')?.remove()
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* 手机返回键:抽屉打开时接管,先收起抽屉而不是退出应用。
|
|
1458
|
+
* 做法是打开时 push 一条带标记的历史,关闭时只回退自己那一条。
|
|
1459
|
+
*/
|
|
1460
|
+
function installBackGesture(layout) {
|
|
1461
|
+
if (typeof window === 'undefined' || typeof MutationObserver === 'undefined') return () => {}
|
|
1462
|
+
const query = window.matchMedia?.('(max-width: 1023px)')
|
|
1463
|
+
const isOpen = () => document.documentElement.classList.contains('dsh-drawer-open')
|
|
1464
|
+
let pushed = false
|
|
1465
|
+
const onPop = () => {
|
|
1466
|
+
if (isOpen()) layout?.toggleSidebar?.()
|
|
1467
|
+
}
|
|
1468
|
+
const observer = new MutationObserver(() => {
|
|
1469
|
+
if (query && !query.matches) return
|
|
1470
|
+
const open = isOpen()
|
|
1471
|
+
if (open && !pushed) {
|
|
1472
|
+
pushed = true
|
|
1473
|
+
try {
|
|
1474
|
+
history.pushState({ dshDrawer: true }, '')
|
|
1475
|
+
} catch {
|
|
1476
|
+
/* 某些 WebView 禁 pushState,忽略 */
|
|
1477
|
+
}
|
|
1478
|
+
} else if (!open && pushed) {
|
|
1479
|
+
pushed = false
|
|
1480
|
+
if (history.state !== null && history.state.dshDrawer === true) history.back()
|
|
1481
|
+
}
|
|
1482
|
+
})
|
|
1483
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
1484
|
+
window.addEventListener('popstate', onPop)
|
|
1485
|
+
return () => {
|
|
1486
|
+
observer.disconnect()
|
|
1487
|
+
window.removeEventListener('popstate', onPop)
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* 本机反馈:任务跑完 / 等你确认时,如果页面在后台,闪一下标题 + 震动。
|
|
1493
|
+
* 不依赖推送服务(v1 不做推送),也不需要服务端配合。
|
|
1494
|
+
*/
|
|
1495
|
+
function installAttention() {
|
|
1496
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return () => {}
|
|
1497
|
+
const KEY = 'dsh-remote-panel:attention'
|
|
1498
|
+
let timer = 0
|
|
1499
|
+
let flashTimer = 0
|
|
1500
|
+
let flashing = false
|
|
1501
|
+
let wasRunning = false
|
|
1502
|
+
let wasWaiting = false
|
|
1503
|
+
|
|
1504
|
+
const enabled = () => {
|
|
1505
|
+
try {
|
|
1506
|
+
return localStorage.getItem(KEY) !== 'off'
|
|
1507
|
+
} catch {
|
|
1508
|
+
return true
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
const stopFlash = () => {
|
|
1512
|
+
if (flashTimer !== 0) {
|
|
1513
|
+
clearInterval(flashTimer)
|
|
1514
|
+
flashTimer = 0
|
|
1515
|
+
}
|
|
1516
|
+
if (flashing) {
|
|
1517
|
+
flashing = false
|
|
1518
|
+
document.title = baseTitle
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
let baseTitle = document.title
|
|
1522
|
+
const flash = (label) => {
|
|
1523
|
+
if (typeof document.hidden === 'boolean' && !document.hidden && document.hasFocus?.()) return
|
|
1524
|
+
baseTitle = document.title
|
|
1525
|
+
if (flashing) return
|
|
1526
|
+
flashing = true
|
|
1527
|
+
let on = false
|
|
1528
|
+
flashTimer = setInterval(() => {
|
|
1529
|
+
on = !on
|
|
1530
|
+
document.title = on ? label : baseTitle
|
|
1531
|
+
}, 900)
|
|
1532
|
+
notifyVibrate()
|
|
1533
|
+
window.setTimeout(stopFlash, 20000)
|
|
1534
|
+
}
|
|
1535
|
+
const notifyVibrate = () => {
|
|
1536
|
+
try {
|
|
1537
|
+
navigator.vibrate?.([120, 60, 120])
|
|
1538
|
+
} catch {
|
|
1539
|
+
/* 不支持震动就算了 */
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const isRunning = () => {
|
|
1543
|
+
const button = document.querySelector('.uV2eYG_primary')
|
|
1544
|
+
const aria = button?.getAttribute('aria-label') ?? ''
|
|
1545
|
+
// 空闲时是「发送消息」,生成中会换成「停止…」
|
|
1546
|
+
return aria !== '' && !aria.includes('发送')
|
|
1547
|
+
}
|
|
1548
|
+
const isWaiting = () => {
|
|
1549
|
+
const dialog = document.querySelector('[role="dialog"], [class*="_dialog"], [class*="_modal"]')
|
|
1550
|
+
if (dialog === null) return false
|
|
1551
|
+
return /允许|批准|拒绝|确认执行/.test(dialog.innerText ?? '')
|
|
1552
|
+
}
|
|
1553
|
+
const tick = () => {
|
|
1554
|
+
if (!enabled()) return
|
|
1555
|
+
const running = isRunning()
|
|
1556
|
+
const waiting = isWaiting()
|
|
1557
|
+
if (wasRunning && !running) flash('✔ 已完成 · DSH')
|
|
1558
|
+
if (!wasWaiting && waiting) flash('● 待确认 · DSH')
|
|
1559
|
+
wasRunning = running
|
|
1560
|
+
wasWaiting = waiting
|
|
1561
|
+
}
|
|
1562
|
+
const onVisible = () => {
|
|
1563
|
+
if (!document.hidden) {
|
|
1564
|
+
stopFlash()
|
|
1565
|
+
tick()
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
timer = window.setInterval(tick, 1500)
|
|
1569
|
+
document.addEventListener('visibilitychange', onVisible)
|
|
1570
|
+
window.addEventListener('focus', stopFlash)
|
|
1571
|
+
return () => {
|
|
1572
|
+
if (timer !== 0) clearInterval(timer)
|
|
1573
|
+
document.removeEventListener('visibilitychange', onVisible)
|
|
1574
|
+
window.removeEventListener('focus', stopFlash)
|
|
1575
|
+
stopFlash()
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* 会话内容增强(只在 ≤1023px 生效):
|
|
1581
|
+
* - 代码块:注入 28px 的「复制」按钮;原生 11px 的复制按钮由 CSS 隐藏;
|
|
1582
|
+
* - 后台任务 chip:把「N 个后台任务」收成数字角标(data-rp-jobs + CSS ::after);
|
|
1583
|
+
* - markdown 图片:原生已有点击看原图时让给原生,其余兜底新标签打开原图。
|
|
1584
|
+
* 所有注入幂等(Observer 反复执行不叠加),返回 cleanup;HMR / 切换会话都安全。
|
|
1585
|
+
*/
|
|
1586
|
+
function installContentEnhancements() {
|
|
1587
|
+
if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {}
|
|
1588
|
+
if (document.body === null) return () => {}
|
|
1589
|
+
// HMR:上一个模块实例如果还活着,先让它自清理,避免 Observer / 按钮叠加
|
|
1590
|
+
try {
|
|
1591
|
+
globalThis[CONTENT_CLEANUP_KEY]?.()
|
|
1592
|
+
} catch {
|
|
1593
|
+
/* 旧实例清理失败不阻塞新实例 */
|
|
1594
|
+
}
|
|
1595
|
+
const query = window.matchMedia?.('(max-width: 1023px)')
|
|
1596
|
+
const tag = `rp-${Math.random().toString(36).slice(2, 8)}`
|
|
1597
|
+
const copyTimers = new Set()
|
|
1598
|
+
const zoomImages = new Map()
|
|
1599
|
+
|
|
1600
|
+
const isNarrow = () => (query === undefined ? window.innerWidth < 1024 : query.matches)
|
|
1601
|
+
|
|
1602
|
+
const writeClipboard = (text) => {
|
|
1603
|
+
// 失败静默:不弹窗、不 alert
|
|
1604
|
+
try {
|
|
1605
|
+
if (navigator.clipboard?.writeText) {
|
|
1606
|
+
return navigator.clipboard.writeText(text).then(
|
|
1607
|
+
() => true,
|
|
1608
|
+
() => false,
|
|
1609
|
+
)
|
|
1610
|
+
}
|
|
1611
|
+
} catch {
|
|
1612
|
+
/* 非安全上下文 / 旧 WebView:静默 */
|
|
1613
|
+
}
|
|
1614
|
+
return Promise.resolve(false)
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const makeCopyButton = (pre, block) => {
|
|
1618
|
+
const button = document.createElement('button')
|
|
1619
|
+
button.type = 'button'
|
|
1620
|
+
button.className = 'dsh-code-copy'
|
|
1621
|
+
button.dataset.rp = tag
|
|
1622
|
+
button.textContent = '复制'
|
|
1623
|
+
button.setAttribute('aria-label', '复制代码')
|
|
1624
|
+
button.addEventListener('click', (event) => {
|
|
1625
|
+
event.preventDefault()
|
|
1626
|
+
event.stopPropagation()
|
|
1627
|
+
// 流式输出时 React 可能换掉 pre:点击时重新解析当前节点
|
|
1628
|
+
const target = pre.isConnected ? pre : block?.querySelector('pre')
|
|
1629
|
+
if (!target) return
|
|
1630
|
+
// 用 textContent:折叠在「思考」里的代码块 innerText 会是空串
|
|
1631
|
+
const text = target.textContent ?? ''
|
|
1632
|
+
writeClipboard(text).then((ok) => {
|
|
1633
|
+
if (!ok) return
|
|
1634
|
+
button.dataset.done = 'true'
|
|
1635
|
+
button.textContent = '已复制'
|
|
1636
|
+
const timer = window.setTimeout(() => {
|
|
1637
|
+
copyTimers.delete(timer)
|
|
1638
|
+
button.dataset.done = 'false'
|
|
1639
|
+
button.textContent = '复制'
|
|
1640
|
+
}, 1200)
|
|
1641
|
+
copyTimers.add(timer)
|
|
1642
|
+
})
|
|
1643
|
+
})
|
|
1644
|
+
return button
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
const enhanceCodeBlocks = () => {
|
|
1648
|
+
for (const block of document.querySelectorAll('.md-code-block')) {
|
|
1649
|
+
const action = block.querySelector('[data-code-block-banner] [class*="_action_"]')
|
|
1650
|
+
const pre = block.querySelector('pre')
|
|
1651
|
+
if (!action || !pre) continue
|
|
1652
|
+
if (action.querySelector('.dsh-code-copy') !== null) continue
|
|
1653
|
+
action.appendChild(makeCopyButton(pre, block))
|
|
1654
|
+
}
|
|
1655
|
+
// 没有 banner 的 pre(少见):浮在右上角,父节点临时 position: relative
|
|
1656
|
+
for (const pre of document.querySelectorAll('pre')) {
|
|
1657
|
+
if (pre.closest('.md-code-block') !== null) continue
|
|
1658
|
+
const parent = pre.parentElement
|
|
1659
|
+
if (!parent || parent.querySelector('.dsh-code-copy') !== null) continue
|
|
1660
|
+
if (getComputedStyle(parent).position === 'static') {
|
|
1661
|
+
parent.dataset.rpPosition = tag
|
|
1662
|
+
parent.dataset.rpPrevPosition = parent.style.position
|
|
1663
|
+
parent.style.position = 'relative'
|
|
1664
|
+
}
|
|
1665
|
+
const button = makeCopyButton(pre, null)
|
|
1666
|
+
button.classList.add('dsh-code-copy--float')
|
|
1667
|
+
parent.appendChild(button)
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
const enhanceJobsBadge = () => {
|
|
1672
|
+
for (const trigger of document.querySelectorAll('[class*="QsffPG_trigger"]')) {
|
|
1673
|
+
const label = trigger.getAttribute('aria-label') ?? trigger.textContent ?? ''
|
|
1674
|
+
const value = label.match(/\d+/)?.[0]
|
|
1675
|
+
if (value !== undefined && trigger.dataset.rpJobs !== value) trigger.dataset.rpJobs = value
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
const enhanceImages = () => {
|
|
1680
|
+
for (const img of document.querySelectorAll('._markdown_1ypvv_5 img')) {
|
|
1681
|
+
if (zoomImages.has(img)) continue
|
|
1682
|
+
// 原生 markdown 图片已经包在 button 里(点击打开内置大图查看),不重复接管
|
|
1683
|
+
if (img.closest('button, a') !== null) continue
|
|
1684
|
+
const handler = () => {
|
|
1685
|
+
const src = img.currentSrc || img.src
|
|
1686
|
+
if (!src || src.startsWith('data:')) return
|
|
1687
|
+
window.open(src, '_blank', 'noopener,noreferrer')
|
|
1688
|
+
}
|
|
1689
|
+
img.addEventListener('click', handler)
|
|
1690
|
+
img.dataset.rpZoom = tag
|
|
1691
|
+
zoomImages.set(img, handler)
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const removeOwned = () => {
|
|
1696
|
+
for (const button of document.querySelectorAll(`.dsh-code-copy[data-rp="${tag}"]`)) button.remove()
|
|
1697
|
+
for (const timer of copyTimers) clearTimeout(timer)
|
|
1698
|
+
copyTimers.clear()
|
|
1699
|
+
for (const [img, handler] of zoomImages) {
|
|
1700
|
+
img.removeEventListener('click', handler)
|
|
1701
|
+
delete img.dataset.rpZoom
|
|
1702
|
+
}
|
|
1703
|
+
zoomImages.clear()
|
|
1704
|
+
for (const parent of document.querySelectorAll(`[data-rp-position="${tag}"]`)) {
|
|
1705
|
+
parent.style.position = parent.dataset.rpPrevPosition ?? ''
|
|
1706
|
+
delete parent.dataset.rpPosition
|
|
1707
|
+
delete parent.dataset.rpPrevPosition
|
|
1708
|
+
}
|
|
1709
|
+
for (const trigger of document.querySelectorAll('[data-rp-jobs]')) delete trigger.dataset.rpJobs
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
let scheduled = 0
|
|
1713
|
+
const enhance = () => {
|
|
1714
|
+
if (!isNarrow()) {
|
|
1715
|
+
removeOwned()
|
|
1716
|
+
return
|
|
1717
|
+
}
|
|
1718
|
+
enhanceCodeBlocks()
|
|
1719
|
+
enhanceJobsBadge()
|
|
1720
|
+
enhanceImages()
|
|
1721
|
+
}
|
|
1722
|
+
const observer = new MutationObserver(() => {
|
|
1723
|
+
if (scheduled !== 0) return
|
|
1724
|
+
scheduled = requestAnimationFrame(() => {
|
|
1725
|
+
scheduled = 0
|
|
1726
|
+
enhance()
|
|
1727
|
+
})
|
|
1728
|
+
})
|
|
1729
|
+
observer.observe(document.body, { subtree: true, childList: true })
|
|
1730
|
+
const onQueryChange = () => enhance()
|
|
1731
|
+
query?.addEventListener?.('change', onQueryChange)
|
|
1732
|
+
enhance()
|
|
1733
|
+
|
|
1734
|
+
let disposed = false
|
|
1735
|
+
const cleanup = () => {
|
|
1736
|
+
if (disposed) return
|
|
1737
|
+
disposed = true
|
|
1738
|
+
if (scheduled !== 0) cancelAnimationFrame(scheduled)
|
|
1739
|
+
observer.disconnect()
|
|
1740
|
+
query?.removeEventListener?.('change', onQueryChange)
|
|
1741
|
+
removeOwned()
|
|
1742
|
+
if (globalThis[CONTENT_CLEANUP_KEY] === cleanup) delete globalThis[CONTENT_CLEANUP_KEY]
|
|
1743
|
+
}
|
|
1744
|
+
globalThis[CONTENT_CLEANUP_KEY] = cleanup
|
|
1745
|
+
return cleanup
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* 断线重连提示:DSH 前端没有「连接断开」的可见反馈,网络一抖页面只是不动了。
|
|
1750
|
+
* 每 10s 打一次面板自己的 /status,连续两次失败才显示,恢复后自动消失。
|
|
1751
|
+
*/
|
|
1752
|
+
function installConnectivity() {
|
|
1753
|
+
if (typeof window === 'undefined' || typeof fetch !== 'function') return () => {}
|
|
1754
|
+
let failures = 0
|
|
1755
|
+
let timer = 0
|
|
1756
|
+
let banner = null
|
|
1757
|
+
const show = () => {
|
|
1758
|
+
if (banner !== null) return
|
|
1759
|
+
banner = document.createElement('div')
|
|
1760
|
+
banner.className = 'dsh-mobile-offline'
|
|
1761
|
+
banner.setAttribute('role', 'status')
|
|
1762
|
+
banner.textContent = '连接已断开,正在重连…'
|
|
1763
|
+
document.body.appendChild(banner)
|
|
1764
|
+
}
|
|
1765
|
+
const hide = () => {
|
|
1766
|
+
banner?.remove()
|
|
1767
|
+
banner = null
|
|
1768
|
+
}
|
|
1769
|
+
const probe = async () => {
|
|
1770
|
+
try {
|
|
1771
|
+
await callApi('/status')
|
|
1772
|
+
failures = 0
|
|
1773
|
+
hide()
|
|
1774
|
+
} catch {
|
|
1775
|
+
failures += 1
|
|
1776
|
+
if (failures >= 2) show()
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
const onOnline = () => {
|
|
1780
|
+
failures = 0
|
|
1781
|
+
probe()
|
|
1782
|
+
}
|
|
1783
|
+
timer = window.setInterval(() => {
|
|
1784
|
+
if (!document.hidden) probe()
|
|
1785
|
+
}, 10000)
|
|
1786
|
+
window.addEventListener('online', onOnline)
|
|
1787
|
+
window.addEventListener('offline', show)
|
|
1788
|
+
probe()
|
|
1789
|
+
return () => {
|
|
1790
|
+
if (timer !== 0) clearInterval(timer)
|
|
1791
|
+
window.removeEventListener('online', onOnline)
|
|
1792
|
+
window.removeEventListener('offline', show)
|
|
1793
|
+
hide()
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
934
1796
|
|
|
935
|
-
function installShellCss() {
|
|
1797
|
+
function installShellCss(layout) {
|
|
936
1798
|
if (typeof document === 'undefined') return () => {}
|
|
937
1799
|
const style = document.createElement('style')
|
|
938
1800
|
style.id = 'dsh-remote-panel-shell'
|
|
@@ -946,6 +1808,11 @@ window.__ModuleLoader__.load({
|
|
|
946
1808
|
const column = document.querySelector('[class*="_sidebarCol"]')
|
|
947
1809
|
const open = column !== null && column.querySelector('[class*="_collapsed"]') === null
|
|
948
1810
|
document.documentElement.classList.toggle('dsh-drawer-open', open)
|
|
1811
|
+
// 搜索展开时抽屉整屏(原生只是把输入框在 280px 抽屉里拉长)
|
|
1812
|
+
document.documentElement.classList.toggle(
|
|
1813
|
+
'dsh-search-open',
|
|
1814
|
+
open && document.querySelector('[class*="_searchExpanded"]') !== null,
|
|
1815
|
+
)
|
|
949
1816
|
})
|
|
950
1817
|
}
|
|
951
1818
|
const observer = new MutationObserver(sync)
|
|
@@ -958,71 +1825,116 @@ window.__ModuleLoader__.load({
|
|
|
958
1825
|
}
|
|
959
1826
|
}, 4000)
|
|
960
1827
|
|
|
1828
|
+
// 窄屏:点遮罩关抽屉;选中会话 / 面板后也自动收起(桌面保持原样,不参与)
|
|
1829
|
+
const narrow = window.matchMedia('(max-width: 1023px)')
|
|
1830
|
+
const scrim = document.createElement('div')
|
|
1831
|
+
scrim.className = 'dsh-mobile-scrim'
|
|
1832
|
+
scrim.addEventListener('click', () => {
|
|
1833
|
+
if (!narrow.matches) return
|
|
1834
|
+
try {
|
|
1835
|
+
layout?.toggleSidebar?.()
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
console.warn('[remote-panel] close drawer failed', error)
|
|
1838
|
+
}
|
|
1839
|
+
})
|
|
1840
|
+
document.body.appendChild(scrim)
|
|
1841
|
+
|
|
1842
|
+
const onDocumentClick = (event) => {
|
|
1843
|
+
if (!narrow.matches) return
|
|
1844
|
+
if (!document.documentElement.classList.contains('dsh-drawer-open')) return
|
|
1845
|
+
const target = event.target
|
|
1846
|
+
if (!(target instanceof Element)) return
|
|
1847
|
+
if (!target.closest('[class*="_sidebarCol"]')) return
|
|
1848
|
+
if (!target.closest('[class*="_sessionRow"], [class*="_panelRow"]')) return
|
|
1849
|
+
setTimeout(() => {
|
|
1850
|
+
try {
|
|
1851
|
+
layout?.toggleSidebar?.()
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
console.warn('[remote-panel] close drawer failed', error)
|
|
1854
|
+
}
|
|
1855
|
+
}, 0)
|
|
1856
|
+
}
|
|
1857
|
+
document.addEventListener('click', onDocumentClick, true)
|
|
1858
|
+
|
|
1859
|
+
// 抽屉开着时 ESC 收起(宿主窄屏没有这个键位)
|
|
1860
|
+
const onKeyDown = (event) => {
|
|
1861
|
+
if (event.key !== 'Escape' || !narrow.matches) return
|
|
1862
|
+
if (!document.documentElement.classList.contains('dsh-drawer-open')) return
|
|
1863
|
+
event.preventDefault()
|
|
1864
|
+
try {
|
|
1865
|
+
layout?.toggleSidebar?.()
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
console.warn('[remote-panel] close drawer failed', error)
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
document.addEventListener('keydown', onKeyDown)
|
|
1871
|
+
|
|
961
1872
|
return () => {
|
|
962
1873
|
clearTimeout(canary)
|
|
963
1874
|
cancelAnimationFrame(frame)
|
|
964
1875
|
observer.disconnect()
|
|
1876
|
+
document.removeEventListener('click', onDocumentClick, true)
|
|
1877
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
1878
|
+
scrim.remove()
|
|
965
1879
|
style.remove()
|
|
966
|
-
document.documentElement.classList.remove('dsh-drawer-open')
|
|
1880
|
+
document.documentElement.classList.remove('dsh-drawer-open', 'dsh-search-open')
|
|
967
1881
|
}
|
|
968
1882
|
}
|
|
969
1883
|
|
|
970
|
-
function
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
left: 0,
|
|
975
|
-
right: 0,
|
|
976
|
-
bottom: 0,
|
|
977
|
-
height: 56,
|
|
978
|
-
display: 'flex',
|
|
979
|
-
alignItems: 'stretch',
|
|
980
|
-
background: 'color-mix(in srgb, Canvas 92%, transparent)',
|
|
981
|
-
borderTop: `1px solid ${stroke}`,
|
|
982
|
-
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
|
983
|
-
zIndex: 2147483000,
|
|
984
|
-
pointerEvents: 'auto',
|
|
985
|
-
backdropFilter: 'blur(12px)',
|
|
986
|
-
}
|
|
987
|
-
const item = {
|
|
988
|
-
flex: 1,
|
|
989
|
-
font: 'inherit',
|
|
990
|
-
fontSize: 12,
|
|
991
|
-
border: 0,
|
|
992
|
-
background: 'transparent',
|
|
993
|
-
color: 'CanvasText',
|
|
994
|
-
cursor: 'pointer',
|
|
995
|
-
}
|
|
996
|
-
const act = (fn) => () => {
|
|
1884
|
+
function NewChatButton(props) {
|
|
1885
|
+
// 宿主没有顶栏「新建」;这里复用 uiWorkspace 的落点规则:
|
|
1886
|
+
// 当前会话所在工作区 → 最近工作区;都没有就回到空态(由工作区 chip 接手)。
|
|
1887
|
+
const run = () => {
|
|
997
1888
|
try {
|
|
998
|
-
|
|
1889
|
+
const ui = props?.uiWorkspace
|
|
1890
|
+
if (ui && typeof ui.startSession === 'function') {
|
|
1891
|
+
ui.startSession()
|
|
1892
|
+
return
|
|
1893
|
+
}
|
|
1894
|
+
props?.layout?.selectPanel?.('chat')
|
|
999
1895
|
} catch (error) {
|
|
1000
|
-
console.warn('[remote-panel]
|
|
1896
|
+
console.warn('[remote-panel] new chat failed', error)
|
|
1001
1897
|
}
|
|
1002
1898
|
}
|
|
1003
|
-
return
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1899
|
+
return jsx('button', {
|
|
1900
|
+
type: 'button',
|
|
1901
|
+
'aria-label': '新建对话',
|
|
1902
|
+
title: '新建对话',
|
|
1903
|
+
className: 'dsh-mobile-newchat',
|
|
1904
|
+
onClick: run,
|
|
1905
|
+
style: {
|
|
1906
|
+
display: 'none',
|
|
1907
|
+
position: 'fixed',
|
|
1908
|
+
top: 6,
|
|
1909
|
+
right: 10,
|
|
1910
|
+
zIndex: 70,
|
|
1911
|
+
alignItems: 'center',
|
|
1912
|
+
justifyContent: 'center',
|
|
1913
|
+
width: 40,
|
|
1914
|
+
height: 40,
|
|
1915
|
+
padding: 0,
|
|
1916
|
+
border: 0,
|
|
1917
|
+
borderRadius: 12,
|
|
1918
|
+
background: 'transparent',
|
|
1919
|
+
color: 'inherit',
|
|
1920
|
+
cursor: 'pointer',
|
|
1921
|
+
font: 'inherit',
|
|
1922
|
+
},
|
|
1923
|
+
children: jsx('svg', {
|
|
1924
|
+
width: 18,
|
|
1925
|
+
height: 18,
|
|
1926
|
+
viewBox: '0 0 16 16',
|
|
1927
|
+
fill: 'none',
|
|
1928
|
+
'aria-hidden': 'true',
|
|
1929
|
+
children: [
|
|
1930
|
+
jsx('path', {
|
|
1931
|
+
d: 'M2.37091 11.2501C1.58745 9.89288 1.32067 8.29835 1.61969 6.76006C1.91872 5.22177 2.76342 3.8433 3.99826 2.87846C5.2331 1.91362 6.77494 1.42737 8.33988 1.50925C9.90482 1.59113 11.3875 2.23562 12.5149 3.32406C13.6425 4.41269 14.3387 5.87206 14.4754 7.4334C14.612 8.99474 14.18 10.5529 13.2587 11.8209C12.3375 13.0888 10.9891 13.9813 9.46194 14.3337C8.18691 14.628 6.85895 14.5294 5.64989 14.0605C5.1712 13.8748 4.76962 13.4932 4.26534 13.3967C3.67413 13.2835 2.95257 13.5598 2.03794 14.3337',
|
|
1932
|
+
stroke: 'currentColor',
|
|
1933
|
+
}),
|
|
1934
|
+
jsx('path', { d: 'M8 5V11', stroke: 'currentColor' }),
|
|
1935
|
+
jsx('path', { d: 'M5 8H11', stroke: 'currentColor' }),
|
|
1936
|
+
],
|
|
1937
|
+
}),
|
|
1026
1938
|
})
|
|
1027
1939
|
}
|
|
1028
1940
|
|
|
@@ -1047,25 +1959,48 @@ window.__ModuleLoader__.load({
|
|
|
1047
1959
|
background: 'transparent',
|
|
1048
1960
|
color: 'inherit',
|
|
1049
1961
|
cursor: 'pointer',
|
|
1962
|
+
width: 40,
|
|
1963
|
+
height: 40,
|
|
1050
1964
|
padding: '0 8px',
|
|
1051
1965
|
},
|
|
1052
|
-
children: '
|
|
1966
|
+
children: jsx('svg', {
|
|
1967
|
+
width: 18,
|
|
1968
|
+
height: 12,
|
|
1969
|
+
viewBox: '0 0 18 12',
|
|
1970
|
+
fill: 'none',
|
|
1971
|
+
'aria-hidden': 'true',
|
|
1972
|
+
children: [
|
|
1973
|
+
jsx('path', { d: 'M1 1h16', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
|
|
1974
|
+
jsx('path', { d: 'M1 9h11', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
|
|
1975
|
+
],
|
|
1976
|
+
}),
|
|
1053
1977
|
})
|
|
1054
1978
|
}
|
|
1055
1979
|
|
|
1056
1980
|
function apply(ctx) {
|
|
1057
|
-
const cleanup = installShellCss()
|
|
1981
|
+
const cleanup = installShellCss(ctx.layout)
|
|
1982
|
+
const cleanupClear = installWorkspaceClear(ctx)
|
|
1983
|
+
const cleanupBack = installBackGesture(ctx.layout)
|
|
1984
|
+
const cleanupAttention = installAttention()
|
|
1985
|
+
const cleanupContent = installContentEnhancements()
|
|
1986
|
+
const cleanupNet = installConnectivity()
|
|
1987
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupClear, 'remote-panel: workspace clear')
|
|
1988
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupBack, 'remote-panel: back gesture')
|
|
1989
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupAttention, 'remote-panel: attention')
|
|
1990
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupContent, 'remote-panel: content enhancements')
|
|
1991
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupNet, 'remote-panel: connectivity')
|
|
1058
1992
|
if (typeof ctx.effect === 'function') ctx.effect(() => cleanup, 'remote-panel: mobile shell css')
|
|
1059
1993
|
|
|
1060
1994
|
ctx.slots.inject('shell.overlay', () =>
|
|
1061
1995
|
ctx.slots.register(
|
|
1062
1996
|
{
|
|
1063
1997
|
name: 'shell.overlay',
|
|
1064
|
-
id: 'remote-panel.
|
|
1998
|
+
id: 'remote-panel.new-chat',
|
|
1065
1999
|
order: 100,
|
|
1066
|
-
|
|
2000
|
+
// uiWorkspace 只在 web 端由 dsh-client-ui-workspace 提供;拿不到时退回切面板
|
|
2001
|
+
inject: () => ({ layout: ctx.layout, uiWorkspace: ctx.get('uiWorkspace') }),
|
|
1067
2002
|
},
|
|
1068
|
-
|
|
2003
|
+
NewChatButton,
|
|
1069
2004
|
),
|
|
1070
2005
|
)
|
|
1071
2006
|
|
|
@@ -1085,7 +2020,7 @@ window.__ModuleLoader__.load({
|
|
|
1085
2020
|
return {
|
|
1086
2021
|
inject: ['slots', 'layout'],
|
|
1087
2022
|
apply,
|
|
1088
|
-
components: { Panel, DeployWizard, StatusCard, PairCard, AccessCard, StaleBanner },
|
|
2023
|
+
components: { Panel, DeployWizard, StatusCard, PairCard, AccessCard, StaleBanner, AppCard },
|
|
1089
2024
|
}
|
|
1090
2025
|
},
|
|
1091
2026
|
})
|