dsh-remote-panel 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +272 -29
- package/lib/index.js +3 -1
- package/package.json +1 -1
package/lib/assets/icon-180.png
CHANGED
|
Binary file
|
package/lib/assets/icon-192.png
CHANGED
|
Binary file
|
package/lib/assets/icon-512.png
CHANGED
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -1075,12 +1075,15 @@ window.__ModuleLoader__.load({
|
|
|
1075
1075
|
/* 独立窗口里别再有下拉刷新 / 橡皮筋 */
|
|
1076
1076
|
body { overscroll-behavior: none; }
|
|
1077
1077
|
|
|
1078
|
+
/* 顶部安全区:默认取 env();WebView 里由壳把真实切角高度写进 --dsh-safe-top */
|
|
1079
|
+
:root { --dsh-safe-top: env(safe-area-inset-top, 0px); }
|
|
1080
|
+
|
|
1078
1081
|
/* 顶栏:52px;刘海屏/独立窗口把状态栏让出来 */
|
|
1079
1082
|
.wSkVaW_header {
|
|
1080
|
-
min-height: calc(52px +
|
|
1081
|
-
padding: calc(
|
|
1083
|
+
min-height: calc(52px + var(--dsh-safe-top, 0px)) !important;
|
|
1084
|
+
padding: calc(12px + var(--dsh-safe-top, 0px)) 60px 0 10px !important;
|
|
1082
1085
|
}
|
|
1083
|
-
.wSkVaW_headerSessionless { padding-top: calc(
|
|
1086
|
+
.wSkVaW_headerSessionless { padding-top: calc(12px + var(--dsh-safe-top, 0px)) !important; }
|
|
1084
1087
|
.wSkVaW_headerCorner { display: none !important; }
|
|
1085
1088
|
/* 手机的「用文件管理器打开」是桌面动作,收掉 */
|
|
1086
1089
|
.OMoRSG_split { display: none !important; }
|
|
@@ -1167,8 +1170,9 @@ window.__ModuleLoader__.load({
|
|
|
1167
1170
|
left: 0;
|
|
1168
1171
|
right: 0;
|
|
1169
1172
|
z-index: 60;
|
|
1170
|
-
top
|
|
1171
|
-
|
|
1173
|
+
/* 与顶栏里那颗汉堡同一个纵向基线:顶栏 padding-top = 12 + safe-top,行高 40 */
|
|
1174
|
+
top: calc(12px + var(--dsh-safe-top, 0px));
|
|
1175
|
+
height: 40px;
|
|
1172
1176
|
margin: 0;
|
|
1173
1177
|
padding: 0 64px;
|
|
1174
1178
|
align-items: center;
|
|
@@ -1193,7 +1197,12 @@ window.__ModuleLoader__.load({
|
|
|
1193
1197
|
.uV2eYG_row { padding: 6px 10px 10px !important; }
|
|
1194
1198
|
|
|
1195
1199
|
/* 抽屉:工作区列表置顶,面板入口退到列表下面,删掉品牌行与「新会话」行 */
|
|
1196
|
-
|
|
1200
|
+
/* 顶部按刘海高度留白,但那块就是页面自己的底色,看起来只是「留白」不是「切角」 */
|
|
1201
|
+
.hHd-Xa_root {
|
|
1202
|
+
display: flex;
|
|
1203
|
+
flex-direction: column;
|
|
1204
|
+
padding-top: calc(12px + var(--dsh-safe-top, 0px)) !important;
|
|
1205
|
+
}
|
|
1197
1206
|
.hHd-Xa_logoRow, .hHd-Xa_newSession { display: none !important; }
|
|
1198
1207
|
.hHd-Xa_regionArea { order: 1; }
|
|
1199
1208
|
/* 手机上的 DSH = 对话 + 改动 + 余额:只留「工作区 Git」,其余面板入口全收 */
|
|
@@ -1222,14 +1231,44 @@ window.__ModuleLoader__.load({
|
|
|
1222
1231
|
html.dsh-search-open .bhn1Oq_searchInput { height: 38px !important; }
|
|
1223
1232
|
|
|
1224
1233
|
/* 点遮罩关抽屉:原生窄屏没有这层,补一个(抽屉之下、正文之上) */
|
|
1225
|
-
|
|
1226
|
-
|
|
1234
|
+
/* 遮罩常驻,用透明度过渡:display 切换会瞬变,看着很硬 */
|
|
1235
|
+
.dsh-mobile-scrim {
|
|
1227
1236
|
position: fixed;
|
|
1228
1237
|
inset: 0;
|
|
1229
1238
|
z-index: 899;
|
|
1230
|
-
|
|
1239
|
+
display: block;
|
|
1240
|
+
opacity: 0;
|
|
1241
|
+
pointer-events: none;
|
|
1242
|
+
/* 纯黑底上「压暗」看不出层级,这里反过来提亮:抽屉保持纯黑,其余区域变灰 */
|
|
1243
|
+
background: rgba(255, 255, 255, .16);
|
|
1244
|
+
transition: opacity 160ms cubic-bezier(.2, .8, .2, 1);
|
|
1245
|
+
}
|
|
1246
|
+
html.dsh-drawer-open .dsh-mobile-scrim {
|
|
1247
|
+
opacity: 1;
|
|
1248
|
+
pointer-events: auto;
|
|
1249
|
+
/* 只在真正要动画时才提成合成层,别让它一直挂着 */
|
|
1250
|
+
will-change: opacity;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/* 微交互:按压/悬停给 120ms 过渡,别让反馈是瞬变 */
|
|
1254
|
+
.dsh-mobile-hamburger, .dsh-mobile-newchat, .dsh-mobile-clear-workspace,
|
|
1255
|
+
.dsh-code-copy, .dsh-mobile-offline {
|
|
1256
|
+
transition: background-color 120ms ease-out, color 120ms ease-out,
|
|
1257
|
+
opacity 120ms ease-out, transform 120ms ease-out;
|
|
1258
|
+
-webkit-tap-highlight-color: transparent;
|
|
1259
|
+
}
|
|
1260
|
+
.dsh-mobile-hamburger:active, .dsh-mobile-newchat:active,
|
|
1261
|
+
.dsh-mobile-clear-workspace:active, .dsh-code-copy:active {
|
|
1262
|
+
transform: scale(.94);
|
|
1263
|
+
}
|
|
1264
|
+
@keyframes dsh-pill-in {
|
|
1265
|
+
from { opacity: 0; transform: translate(-50%, -4px); }
|
|
1266
|
+
to { opacity: 1; transform: translate(-50%, 0); }
|
|
1267
|
+
}
|
|
1268
|
+
.dsh-mobile-offline { animation: dsh-pill-in 160ms cubic-bezier(.2, .8, .2, 1); }
|
|
1269
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1270
|
+
.dsh-mobile-scrim, .dsh-mobile-offline { transition: none; animation: none; }
|
|
1231
1271
|
}
|
|
1232
|
-
.dsh-mobile-scrim { display: none; }
|
|
1233
1272
|
|
|
1234
1273
|
/* 会话内容:代码块 / 表格 / 图片 / 附件在手机上不撑破页面 */
|
|
1235
1274
|
.md-code-block { max-width: 100%; }
|
|
@@ -1270,7 +1309,7 @@ window.__ModuleLoader__.load({
|
|
|
1270
1309
|
.dsh-code-copy[data-done="true"] { opacity: .6; }
|
|
1271
1310
|
.dsh-code-copy--float {
|
|
1272
1311
|
position: absolute;
|
|
1273
|
-
top: 6px;
|
|
1312
|
+
top: calc(6px + var(--dsh-safe-top, 0px));
|
|
1274
1313
|
right: 6px;
|
|
1275
1314
|
z-index: 2;
|
|
1276
1315
|
background: var(--dsw-alias-bg-base, rgba(0, 0, 0, .4));
|
|
@@ -1295,22 +1334,31 @@ window.__ModuleLoader__.load({
|
|
|
1295
1334
|
|
|
1296
1335
|
/* 手机外壳:深色配色(浅色主题保持原生配色,只换骨架) */
|
|
1297
1336
|
@media (max-width: 1023px) {
|
|
1298
|
-
|
|
1337
|
+
/* 纯黑底:连 DSH 自己的深色面(.wSkVaW_root / emptyTabHost / 抽屉)一起刷,
|
|
1338
|
+
否则空白页黑、会话页 #151517,开关抽屉会看到底色跳变。
|
|
1339
|
+
抽屉保持纯黑,靠遮罩提亮来区分层级。 */
|
|
1340
|
+
body[data-ds-dark-theme],
|
|
1341
|
+
body[data-ds-dark-theme] .wSkVaW_root,
|
|
1342
|
+
body[data-ds-dark-theme] [class*="_emptyTabHost"],
|
|
1299
1343
|
body[data-ds-dark-theme] [class*="_frame"],
|
|
1300
|
-
body[data-ds-dark-theme] [class*="_centerCol"]
|
|
1301
|
-
body[data-ds-dark-theme] [class*="
|
|
1344
|
+
body[data-ds-dark-theme] [class*="_centerCol"],
|
|
1345
|
+
body[data-ds-dark-theme] [class*="_scrollBody"],
|
|
1346
|
+
body[data-ds-dark-theme] [class*="_sidebarCol"],
|
|
1347
|
+
body[data-ds-dark-theme] .hHd-Xa_root,
|
|
1348
|
+
body[data-ds-dark-theme] .hHd-Xa_regionArea,
|
|
1349
|
+
body[data-ds-dark-theme] .hHd-Xa_panelList {
|
|
1302
1350
|
background: #000 !important;
|
|
1351
|
+
}
|
|
1352
|
+
body[data-ds-dark-theme] [class*="_sidebarCol"] {
|
|
1303
1353
|
border: 0 !important;
|
|
1304
|
-
box-shadow:
|
|
1354
|
+
box-shadow: none;
|
|
1305
1355
|
}
|
|
1306
|
-
body[data-ds-dark-theme] .hHd-Xa_root { background: #000 !important; }
|
|
1307
1356
|
body[data-ds-dark-theme] .wSkVaW_header { border-bottom: 0 !important; background: transparent !important; }
|
|
1308
1357
|
body[data-ds-dark-theme] .uV2eYG_card {
|
|
1309
|
-
background: #141416 !important;
|
|
1310
1358
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06) !important;
|
|
1311
1359
|
}
|
|
1312
1360
|
body[data-ds-dark-theme] .pXSMma_workspace { background: rgba(255, 255, 255, .07) !important; }
|
|
1313
|
-
/* 列表底部的滚动渐隐原本渐到 #1b1b1c
|
|
1361
|
+
/* 列表底部的滚动渐隐原本渐到 #1b1b1c,跟统一底色对齐 */
|
|
1314
1362
|
body[data-ds-dark-theme] .bhn1Oq_fade {
|
|
1315
1363
|
background-image: linear-gradient(rgba(0, 0, 0, 0), #000) !important;
|
|
1316
1364
|
}
|
|
@@ -1322,7 +1370,7 @@ window.__ModuleLoader__.load({
|
|
|
1322
1370
|
/* 断线胶囊:连续两次探活失败才出现,恢复即消失(任何宽度都显示) */
|
|
1323
1371
|
.dsh-mobile-offline {
|
|
1324
1372
|
position: fixed;
|
|
1325
|
-
top: calc(
|
|
1373
|
+
top: calc(6px + var(--dsh-safe-top, 0px));
|
|
1326
1374
|
left: 50%;
|
|
1327
1375
|
transform: translateX(-50%);
|
|
1328
1376
|
z-index: 80;
|
|
@@ -1391,18 +1439,18 @@ window.__ModuleLoader__.load({
|
|
|
1391
1439
|
padding-bottom: env(safe-area-inset-bottom, 0px) !important;
|
|
1392
1440
|
}
|
|
1393
1441
|
|
|
1394
|
-
/*
|
|
1395
|
-
DSH 不虚拟化消息,几百轮之后节点全在文档里;content-visibility 让浏览器跳过离屏块,
|
|
1396
|
-
滚动时只画视口附近的内容。只在窄屏生效,桌面保持原样。 */
|
|
1442
|
+
/* 长会话滚动:只在窄屏做滚动容器层面的优化,桌面保持原样 */
|
|
1397
1443
|
@media (max-width: 1023px) {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
}
|
|
1444
|
+
/* 曾经给 markdown 块加 content-visibility:auto 想省渲染,
|
|
1445
|
+
但 4x CPU 节流实测滚动会卡出 155ms 的长帧(块进入视口时要重新布局),
|
|
1446
|
+
手机上的「卡顿」就是它。这里去掉,只保留滚动容器本身的优化。 */
|
|
1402
1447
|
[class*="_scrollBody"] {
|
|
1403
1448
|
overscroll-behavior: contain;
|
|
1404
1449
|
-webkit-overflow-scrolling: touch;
|
|
1405
1450
|
}
|
|
1451
|
+
/* 点按不要留系统高亮块(各 ROM 的 WebView 默认会给一层灰/蓝色块) */
|
|
1452
|
+
* { -webkit-tap-highlight-color: transparent; }
|
|
1453
|
+
*:focus:not(:focus-visible) { outline: none; }
|
|
1406
1454
|
}
|
|
1407
1455
|
|
|
1408
1456
|
`
|
|
@@ -1444,11 +1492,22 @@ window.__ModuleLoader__.load({
|
|
|
1444
1492
|
clear.dataset.none = String(isEmpty())
|
|
1445
1493
|
row.appendChild(clear)
|
|
1446
1494
|
}
|
|
1447
|
-
|
|
1495
|
+
// 流式回答时 DOM 每秒要变很多次,ensure 里是一次全量 querySelector,
|
|
1496
|
+
// 必须合并到每帧最多一次,否则主线程全被它占了(手机上就是掉帧)
|
|
1497
|
+
let frame = 0
|
|
1498
|
+
const schedule = () => {
|
|
1499
|
+
if (frame !== 0) return
|
|
1500
|
+
frame = requestAnimationFrame(() => {
|
|
1501
|
+
frame = 0
|
|
1502
|
+
ensure()
|
|
1503
|
+
})
|
|
1504
|
+
}
|
|
1505
|
+
const observer = new MutationObserver(schedule)
|
|
1448
1506
|
observer.observe(document.body, { subtree: true, childList: true })
|
|
1449
1507
|
ensure()
|
|
1450
1508
|
return () => {
|
|
1451
1509
|
observer.disconnect()
|
|
1510
|
+
if (frame !== 0) cancelAnimationFrame(frame)
|
|
1452
1511
|
document.querySelector('.dsh-mobile-clear-workspace')?.remove()
|
|
1453
1512
|
}
|
|
1454
1513
|
}
|
|
@@ -1465,7 +1524,15 @@ window.__ModuleLoader__.load({
|
|
|
1465
1524
|
const onPop = () => {
|
|
1466
1525
|
if (isOpen()) layout?.toggleSidebar?.()
|
|
1467
1526
|
}
|
|
1527
|
+
let frame = 0
|
|
1468
1528
|
const observer = new MutationObserver(() => {
|
|
1529
|
+
if (frame !== 0) return
|
|
1530
|
+
frame = requestAnimationFrame(() => {
|
|
1531
|
+
frame = 0
|
|
1532
|
+
check()
|
|
1533
|
+
})
|
|
1534
|
+
})
|
|
1535
|
+
const check = () => {
|
|
1469
1536
|
if (query && !query.matches) return
|
|
1470
1537
|
const open = isOpen()
|
|
1471
1538
|
if (open && !pushed) {
|
|
@@ -1479,7 +1546,7 @@ window.__ModuleLoader__.load({
|
|
|
1479
1546
|
pushed = false
|
|
1480
1547
|
if (history.state !== null && history.state.dshDrawer === true) history.back()
|
|
1481
1548
|
}
|
|
1482
|
-
}
|
|
1549
|
+
}
|
|
1483
1550
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
1484
1551
|
window.addEventListener('popstate', onPop)
|
|
1485
1552
|
return () => {
|
|
@@ -1488,6 +1555,129 @@ window.__ModuleLoader__.load({
|
|
|
1488
1555
|
}
|
|
1489
1556
|
}
|
|
1490
1557
|
|
|
1558
|
+
/**
|
|
1559
|
+
* 真机性能探针:URL 带 `?dsh-fps=1` 时在左下角显示帧率 / 最慢帧 / 长任务数。
|
|
1560
|
+
*
|
|
1561
|
+
* 只是为了在手机上定位「哪一步卡」,平时不加载任何逻辑(默认直接 return)。
|
|
1562
|
+
*/
|
|
1563
|
+
function installFpsMeter() {
|
|
1564
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return () => {}
|
|
1565
|
+
let enabled = /[?&]dsh-fps=1(&|$)/.test(window.location.search)
|
|
1566
|
+
if (!enabled) {
|
|
1567
|
+
try {
|
|
1568
|
+
enabled = window.sessionStorage.getItem('dsh-fps') === '1'
|
|
1569
|
+
} catch {
|
|
1570
|
+
/* 隐私模式禁 storage,忽略 */
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
if (!enabled) return () => {}
|
|
1574
|
+
|
|
1575
|
+
const box = document.createElement('div')
|
|
1576
|
+
box.style.cssText =
|
|
1577
|
+
'position:fixed;left:8px;bottom:8px;z-index:99999;padding:4px 8px;border-radius:8px;' +
|
|
1578
|
+
'background:rgba(0,0,0,.72);color:#7ef29b;font:11px/1.5 ui-monospace,monospace;' +
|
|
1579
|
+
'pointer-events:none;white-space:pre;'
|
|
1580
|
+
document.body.appendChild(box)
|
|
1581
|
+
|
|
1582
|
+
let frames = 0
|
|
1583
|
+
let since = performance.now()
|
|
1584
|
+
let worst = 0
|
|
1585
|
+
let longTasks = 0
|
|
1586
|
+
let fps = 0
|
|
1587
|
+
let observer = null
|
|
1588
|
+
try {
|
|
1589
|
+
observer = new PerformanceObserver((list) => {
|
|
1590
|
+
for (const entry of list.getEntries()) if (entry.duration > 50) longTasks += 1
|
|
1591
|
+
})
|
|
1592
|
+
observer.observe({ entryTypes: ['longtask'] })
|
|
1593
|
+
} catch {
|
|
1594
|
+
/* 老内核没有 longtask,忽略 */
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
const paint = () => {
|
|
1598
|
+
box.textContent = `${fps}fps worst ${Math.round(worst)}ms long ${longTasks}`
|
|
1599
|
+
worst = 0
|
|
1600
|
+
}
|
|
1601
|
+
let raf = 0
|
|
1602
|
+
const tick = (now) => {
|
|
1603
|
+
frames += 1
|
|
1604
|
+
worst = Math.max(worst, now - since)
|
|
1605
|
+
const elapsed = now - since
|
|
1606
|
+
if (elapsed >= 500) {
|
|
1607
|
+
fps = Math.round((frames * 1000) / elapsed)
|
|
1608
|
+
frames = 0
|
|
1609
|
+
since = now
|
|
1610
|
+
paint()
|
|
1611
|
+
}
|
|
1612
|
+
raf = requestAnimationFrame(tick)
|
|
1613
|
+
}
|
|
1614
|
+
raf = requestAnimationFrame(tick)
|
|
1615
|
+
window.setInterval(paint, 1000)
|
|
1616
|
+
return () => {
|
|
1617
|
+
cancelAnimationFrame(raf)
|
|
1618
|
+
observer?.disconnect()
|
|
1619
|
+
box.remove()
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* 切换工作区后别自动聚焦输入框。
|
|
1625
|
+
*
|
|
1626
|
+
* DSH 打开工作区选择器时会先把焦点交给搜索框;关掉后焦点会回到输入条,
|
|
1627
|
+
* 手机上就是键盘自己弹出来。这里在选择器关闭、或 chip 文案变化后,
|
|
1628
|
+
* 把焦点从输入条上撤走(只撤输入条里的可编辑区,不动选择器自己的搜索框)。
|
|
1629
|
+
*/
|
|
1630
|
+
function installComposerFocusGuard() {
|
|
1631
|
+
if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {}
|
|
1632
|
+
let labelObserver = null
|
|
1633
|
+
let observedLabel = null
|
|
1634
|
+
let lastLabel = ''
|
|
1635
|
+
|
|
1636
|
+
const isComposer = (el) =>
|
|
1637
|
+
el !== null &&
|
|
1638
|
+
el !== document.body &&
|
|
1639
|
+
(el.isContentEditable === true || el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') &&
|
|
1640
|
+
(el.closest('.uV2eYG_root') !== null || el.closest('[class*="_composer"]') !== null)
|
|
1641
|
+
|
|
1642
|
+
const release = () => {
|
|
1643
|
+
const el = document.activeElement
|
|
1644
|
+
if (isComposer(el)) el.blur()
|
|
1645
|
+
}
|
|
1646
|
+
// DSH 可能在我们之后又把焦点塞回去,所以分几拍各撤一次
|
|
1647
|
+
const scheduleRelease = () => {
|
|
1648
|
+
for (const delay of [0, 80, 200, 400]) window.setTimeout(release, delay)
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const watchLabel = () => {
|
|
1652
|
+
const label = document.querySelector('[class*="_workspaceLabel"]')
|
|
1653
|
+
if (label === null || label === observedLabel) return
|
|
1654
|
+
labelObserver?.disconnect()
|
|
1655
|
+
observedLabel = label
|
|
1656
|
+
lastLabel = label.textContent ?? ''
|
|
1657
|
+
labelObserver = new MutationObserver(() => {
|
|
1658
|
+
const now = label.textContent ?? ''
|
|
1659
|
+
if (now === lastLabel) return
|
|
1660
|
+
lastLabel = now
|
|
1661
|
+
scheduleRelease()
|
|
1662
|
+
})
|
|
1663
|
+
labelObserver.observe(label, { childList: true, characterData: true, subtree: true })
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// 选择器关闭 = 里面那个搜索框失焦。用事件判断,别在每次 DOM 变动时全量 querySelector。
|
|
1667
|
+
const onFocusOut = (event) => {
|
|
1668
|
+
const target = event.target
|
|
1669
|
+
if (target instanceof Element && target.closest('[class*="wsc-"]') !== null) scheduleRelease()
|
|
1670
|
+
}
|
|
1671
|
+
document.addEventListener('focusout', onFocusOut, true)
|
|
1672
|
+
const ensureLabel = window.setInterval(watchLabel, 800)
|
|
1673
|
+
watchLabel()
|
|
1674
|
+
return () => {
|
|
1675
|
+
document.removeEventListener('focusout', onFocusOut, true)
|
|
1676
|
+
window.clearInterval(ensureLabel)
|
|
1677
|
+
labelObserver?.disconnect()
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1491
1681
|
/**
|
|
1492
1682
|
* 本机反馈:任务跑完 / 等你确认时,如果页面在后台,闪一下标题 + 震动。
|
|
1493
1683
|
* 不依赖推送服务(v1 不做推送),也不需要服务端配合。
|
|
@@ -1719,8 +1909,23 @@ window.__ModuleLoader__.load({
|
|
|
1719
1909
|
enhanceJobsBadge()
|
|
1720
1910
|
enhanceImages()
|
|
1721
1911
|
}
|
|
1722
|
-
const
|
|
1912
|
+
const RELEVANT = 'pre, img, table, [class*="job"], [class*="Job"], [class*="attach"]'
|
|
1913
|
+
const isRelevant = (records) => {
|
|
1914
|
+
for (const record of records) {
|
|
1915
|
+
const target = record.target
|
|
1916
|
+
if (target instanceof Element && target.closest(RELEVANT) !== null) return true
|
|
1917
|
+
for (const node of record.addedNodes) {
|
|
1918
|
+
if (node.nodeType !== 1) continue
|
|
1919
|
+
if (node.matches?.(RELEVANT) === true || node.querySelector?.(RELEVANT) !== null) return true
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
return false
|
|
1923
|
+
}
|
|
1924
|
+
const observer = new MutationObserver((records) => {
|
|
1723
1925
|
if (scheduled !== 0) return
|
|
1926
|
+
// 流式回答每帧都往消息里塞文本节点,全量扫描会把帧吃光;只有出现
|
|
1927
|
+
// 代码块/图片/表格/任务卡/附件这类节点时才真的需要补按钮
|
|
1928
|
+
if (!isRelevant(records)) return
|
|
1724
1929
|
scheduled = requestAnimationFrame(() => {
|
|
1725
1930
|
scheduled = 0
|
|
1726
1931
|
enhance()
|
|
@@ -1749,6 +1954,40 @@ window.__ModuleLoader__.load({
|
|
|
1749
1954
|
* 断线重连提示:DSH 前端没有「连接断开」的可见反馈,网络一抖页面只是不动了。
|
|
1750
1955
|
* 每 10s 打一次面板自己的 /status,连续两次失败才显示,恢复后自动消失。
|
|
1751
1956
|
*/
|
|
1957
|
+
/**
|
|
1958
|
+
* 点输入条上的「+」(添加文件或调用指令)时,别让输入框自动聚焦。
|
|
1959
|
+
* 手机上一聚焦就弹键盘,把刚打开的面板顶走。
|
|
1960
|
+
*
|
|
1961
|
+
* 做法:在 pointerdown 阶段记一个短窗口(900ms),窗口内如果输入框收到 focus 就立刻 blur 回去;
|
|
1962
|
+
* 点别的地方会立刻清掉窗口,不会影响正常输入。
|
|
1963
|
+
*/
|
|
1964
|
+
function installComposerFocusGuard() {
|
|
1965
|
+
if (typeof document === 'undefined') return () => {}
|
|
1966
|
+
let until = 0
|
|
1967
|
+
const isComposerInput = (el) => el instanceof Element && el.closest?.('.uV2eYG_input') !== null
|
|
1968
|
+
const onPointerDown = (event) => {
|
|
1969
|
+
const target = event.target
|
|
1970
|
+
until = target instanceof Element && target.closest('.uV2eYG_add') !== null ? Date.now() + 900 : 0
|
|
1971
|
+
}
|
|
1972
|
+
const onFocusIn = (event) => {
|
|
1973
|
+
if (Date.now() > until) return
|
|
1974
|
+
const el = event.target
|
|
1975
|
+
if (!isComposerInput(el)) return
|
|
1976
|
+
const drop = () => {
|
|
1977
|
+
if (Date.now() <= until && document.activeElement === el) el.blur?.()
|
|
1978
|
+
}
|
|
1979
|
+
drop()
|
|
1980
|
+
// 有的实现会在下一帧再 focus 一次,补一刀
|
|
1981
|
+
requestAnimationFrame(drop)
|
|
1982
|
+
}
|
|
1983
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
1984
|
+
document.addEventListener('focusin', onFocusIn, true)
|
|
1985
|
+
return () => {
|
|
1986
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
1987
|
+
document.removeEventListener('focusin', onFocusIn, true)
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1752
1991
|
function installConnectivity() {
|
|
1753
1992
|
if (typeof window === 'undefined' || typeof fetch !== 'function') return () => {}
|
|
1754
1993
|
let failures = 0
|
|
@@ -1984,11 +2223,15 @@ window.__ModuleLoader__.load({
|
|
|
1984
2223
|
const cleanupAttention = installAttention()
|
|
1985
2224
|
const cleanupContent = installContentEnhancements()
|
|
1986
2225
|
const cleanupNet = installConnectivity()
|
|
2226
|
+
const cleanupFocus = installComposerFocusGuard()
|
|
2227
|
+
const cleanupFps = installFpsMeter()
|
|
1987
2228
|
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupClear, 'remote-panel: workspace clear')
|
|
1988
2229
|
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupBack, 'remote-panel: back gesture')
|
|
1989
2230
|
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupAttention, 'remote-panel: attention')
|
|
1990
2231
|
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupContent, 'remote-panel: content enhancements')
|
|
1991
2232
|
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupNet, 'remote-panel: connectivity')
|
|
2233
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupFocus, 'remote-panel: composer focus guard')
|
|
2234
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => cleanupFps, 'remote-panel: fps meter')
|
|
1992
2235
|
if (typeof ctx.effect === 'function') ctx.effect(() => cleanup, 'remote-panel: mobile shell css')
|
|
1993
2236
|
|
|
1994
2237
|
ctx.slots.inject('shell.overlay', () =>
|
package/lib/index.js
CHANGED
|
@@ -141,7 +141,9 @@ export function writeGoPage(dir, token, accessKey) {
|
|
|
141
141
|
`<title>DSH</title><meta http-equiv="refresh" content="0;url=${target}">` +
|
|
142
142
|
'<style>html,body{height:100%;margin:0;background:#000;color:#888;font:14px system-ui;display:flex;align-items:center;justify-content:center}</style>' +
|
|
143
143
|
'<p>正在进入 DSH…</p>' +
|
|
144
|
-
|
|
144
|
+
// ?fps=1 时把开关记进 sessionStorage,跳转后由客户端插件读取(用于真机性能排查)
|
|
145
|
+
'<script>try{if(/[?&]fps=1/.test(location.search))sessionStorage.setItem("dsh-fps","1")}catch(e){}' +
|
|
146
|
+
`location.replace(${JSON.stringify(target)})</script>`
|
|
145
147
|
writeFileSync(join(dir, 'go.html'), html)
|
|
146
148
|
return target
|
|
147
149
|
}
|