@mrrisega/dsh-remote 0.6.13 → 0.6.15
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/clients/dsh-remote/dsh-bridge.mjs +124 -14
- package/clients/dsh-remote/dsh-events.mjs +22 -2
- package/clients/dsh-remote/e2ee-client.mjs +97 -13
- package/clients/dsh-remote/e2ee-shim-script.js +31 -3
- package/clients/dsh-remote/mobile-adapter.mjs +283 -1
- package/clients/dsh-remote/test/boot-tip.test.mjs +158 -0
- package/clients/dsh-remote/test/dsh-events.test.mjs +89 -0
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +73 -3
- package/clients/dsh-remote/test/e2ee-client.test.mjs +42 -1
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +10 -1
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +18 -1
- package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +325 -0
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +3 -1
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +196 -0
- package/clients/dsh-remote/test/wechat-runtime.test.mjs +268 -1
- package/clients/dsh-remote/upstream-discovery.mjs +448 -0
- package/clients/dsh-remote/wechat-channel.mjs +86 -3
- package/clients/dsh-remote/wechat-runtime.mjs +253 -10
- package/dsh-setup.mjs +225 -23
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +31 -1
- package/packages/dsh-remote-web/lib/index.js +464 -15
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +124 -14
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +22 -2
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +97 -13
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +31 -3
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +283 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/upstream-discovery.mjs +448 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +86 -3
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +253 -10
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +225 -23
- package/packages/dsh-remote-web/test/connect-stuck-visibility.test.mjs +16 -1
- package/packages/dsh-remote-web/test/doctor-cli.test.mjs +117 -0
- package/packages/dsh-remote-web/test/linux-bridge.test.mjs +346 -0
- package/packages/dsh-remote-web/test/picker-pin.test.mjs +201 -0
- package/packages/dsh-remote-web/test/self-manage.test.mjs +12 -0
- package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +170 -0
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
* left+top 陈旧锚点,默认回到右下;再兜底一次“卡左上角”检测(合成鼠标事件走它自己
|
|
37
37
|
* 的拖拽收尾,state/localStorage 由它自洽)。
|
|
38
38
|
* 仅对 ≤820 且锚点命中 left+top 干预;透明区点击/滚动仍穿透,与桌面语义一致。
|
|
39
|
+
* 6)【0.6.15】**手机图片上传兼容**:官方只把 image/png|jpeg|webp|gif 当图片(本地编码随 prompt
|
|
40
|
+
* 发给模型),其余一律当普通文件后台上传 —— 模型于是"看不到图"。而手机最容易产出非白名单
|
|
41
|
+
* 类型(iPhone「高效」= image/heic;部分 Android 图库给空 type)。适配层在**捕获阶段**拦下
|
|
42
|
+
* 官方隐藏 input[type=file] 的 change,把这类文件**无损改 MIME**(扩展名可判时)或
|
|
43
|
+
* canvas 转 JPEG 后重新派发;任何失败都原样重发,绝不丢文件。详见脚本内长注释。
|
|
39
44
|
*
|
|
40
45
|
* 宿主特征门(防误注入其它 html):
|
|
41
46
|
* - 注入前:content-type 必须 text/html 且含 </head> 且原文含官方标记
|
|
@@ -95,7 +100,34 @@ const RUNTIME_HOST_RE =
|
|
|
95
100
|
/(data-sidebar-collapsed|data-details-collapsed|data-rightbar-collapsed|data-shell-overlay)|(pI_x6G_frame|hHd-Xa_root|hHd-Xa_toggle)/;
|
|
96
101
|
|
|
97
102
|
const STYLE = `
|
|
98
|
-
/* =====
|
|
103
|
+
/* ===== 首屏加载提示(0.6.14)=====
|
|
104
|
+
为什么要有它:镜像页首屏要拉 dsh web 的全部客户端插件(实测 ~14 MB,一个不可拆分的
|
|
105
|
+
/plugins/?? 聚合请求),免费档限速下几十秒起步。此前页面上**只有官方那个转圈**,
|
|
106
|
+
用户无法区分"在加载"和"卡死了",只能反复刷新 —— 反而更慢。
|
|
107
|
+
这里给一句人话提示 + 分阶段的补充说明,加载完成(或超时兜底)即自动移除。
|
|
108
|
+
⚠️ 刻意**不**放进 @media (max-width:820px):Windows/桌面浏览器打开镜像页同样要等。
|
|
109
|
+
⚠️ pointer-events:none + 半透明:万一移除逻辑失效,也绝不挡住任何点击。 */
|
|
110
|
+
.dsh-ma-boot {
|
|
111
|
+
position: fixed; inset: 0; z-index: 2147483000;
|
|
112
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px;
|
|
113
|
+
background: color-mix(in srgb, var(--dsw-alias-bg-base, #0f1115) 88%, transparent);
|
|
114
|
+
backdrop-filter: blur(2px);
|
|
115
|
+
pointer-events: none;
|
|
116
|
+
font-family: inherit; text-align: center; padding: 24px;
|
|
117
|
+
transition: opacity .3s ease;
|
|
118
|
+
}
|
|
119
|
+
.dsh-ma-boot[data-fading="1"] { opacity: 0; }
|
|
120
|
+
.dsh-ma-boot-spin {
|
|
121
|
+
width: 34px; height: 34px; border-radius: 50%;
|
|
122
|
+
border: 3px solid color-mix(in srgb, currentColor 22%, transparent);
|
|
123
|
+
border-top-color: currentColor;
|
|
124
|
+
animation: dsh-ma-boot-spin 1s linear infinite;
|
|
125
|
+
}
|
|
126
|
+
@keyframes dsh-ma-boot-spin { to { transform: rotate(360deg); } }
|
|
127
|
+
.dsh-ma-boot-title { font-size: 15px; font-weight: 600; }
|
|
128
|
+
.dsh-ma-boot-sub { font-size: 13px; line-height: 1.6; opacity: .75; max-width: 22em; }
|
|
129
|
+
.dsh-ma-boot-elapsed { font-size: 12px; opacity: .55; font-variant-numeric: tabular-nums; }
|
|
130
|
+
|
|
99
131
|
@media (max-width: 820px) {
|
|
100
132
|
html, body { max-width: 100%; overflow-x: hidden; }
|
|
101
133
|
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
|
@@ -426,6 +458,89 @@ html[data-dsh-ma-merged-e2ee] #dsh-e2ee-badge { display: none !important; }
|
|
|
426
458
|
const SCRIPT = `(() => {
|
|
427
459
|
"use strict";
|
|
428
460
|
|
|
461
|
+
/* ===== 首屏加载提示(0.6.14)=====
|
|
462
|
+
用户反馈:「普通用户加载进入镜像页面时等待时间较长,目前等待过程中一直在转圈」
|
|
463
|
+
—— 官方那个转圈不告诉用户任何事。这里在**页面解析的第一时间**就挂上一条人话提示,
|
|
464
|
+
并在官方 UI 就绪(或兜底超时)后自动移除。
|
|
465
|
+
|
|
466
|
+
⚠️ 必须放在所有主机判定(HOSTISH / NARROW)之前:首屏慢的正是这些判定等不到的时候。
|
|
467
|
+
⚠️ 只做提示,不做任何拦截:pointer-events:none,也没有遮罩语义。 */
|
|
468
|
+
var maBootTip = null, maBootTimers = [], maBootStart = Date.now(), maBootElapsedTimer = 0, maBootWatchTimer = 0;
|
|
469
|
+
function maBootSet(cls, text) {
|
|
470
|
+
try { var n = maBootTip && maBootTip.querySelector(cls); if (n) n.textContent = text; } catch (e) { /* 忽略 */ }
|
|
471
|
+
}
|
|
472
|
+
function maBootShow() {
|
|
473
|
+
try {
|
|
474
|
+
if (maBootTip) return;
|
|
475
|
+
if (!document.body) return; // 解析太早(body 还没出来)→ 交给 DOMContentLoaded 再挂
|
|
476
|
+
maBootTip = document.createElement("div");
|
|
477
|
+
maBootTip.className = "dsh-ma-boot";
|
|
478
|
+
maBootTip.setAttribute("role", "status");
|
|
479
|
+
maBootTip.setAttribute("aria-live", "polite");
|
|
480
|
+
maBootTip.innerHTML =
|
|
481
|
+
'<div class="dsh-ma-boot-spin" aria-hidden="true"></div>' +
|
|
482
|
+
'<div class="dsh-ma-boot-title">正在加载远程桌面…</div>' +
|
|
483
|
+
'<div class="dsh-ma-boot-sub">首次打开需要下载较完整的前端资源,请稍候</div>' +
|
|
484
|
+
'<div class="dsh-ma-boot-elapsed">已等待 0 秒</div>';
|
|
485
|
+
document.body.appendChild(maBootTip);
|
|
486
|
+
// 秒表:让"到底等了多久"可见(用户据此判断是慢还是死了)
|
|
487
|
+
maBootElapsedTimer = setInterval(function () {
|
|
488
|
+
try {
|
|
489
|
+
var s = Math.floor((Date.now() - maBootStart) / 1000);
|
|
490
|
+
var n = maBootTip && maBootTip.querySelector(".dsh-ma-boot-elapsed");
|
|
491
|
+
if (n) n.textContent = "已等待 " + s + " 秒";
|
|
492
|
+
} catch (e) { /* 忽略 */ }
|
|
493
|
+
}, 1000);
|
|
494
|
+
maBootTimers.push(setTimeout(function () {
|
|
495
|
+
maBootSet(".dsh-ma-boot-sub", "网络较慢时可能需要一两分钟;加载完成后会自动进入,不用重复刷新");
|
|
496
|
+
}, 8000));
|
|
497
|
+
maBootTimers.push(setTimeout(function () {
|
|
498
|
+
maBootSet(".dsh-ma-boot-sub", "还在加载…如果长时间停在这里,可以下拉刷新重试(重复刷新不会更快)");
|
|
499
|
+
}, 30 * 1000));
|
|
500
|
+
} catch (e) { /* 提示失败绝不影响主功能 */ }
|
|
501
|
+
}
|
|
502
|
+
function maBootHide() {
|
|
503
|
+
try {
|
|
504
|
+
for (var i = 0; i < maBootTimers.length; i++) clearTimeout(maBootTimers[i]);
|
|
505
|
+
maBootTimers = [];
|
|
506
|
+
if (maBootElapsedTimer) { clearInterval(maBootElapsedTimer); maBootElapsedTimer = 0; }
|
|
507
|
+
if (maBootWatchTimer) { try { clearInterval(maBootWatchTimer); } catch (e) { /* 忽略 */ } maBootWatchTimer = 0; }
|
|
508
|
+
var tip = maBootTip;
|
|
509
|
+
maBootTip = null;
|
|
510
|
+
if (tip && tip.parentNode) {
|
|
511
|
+
try { tip.setAttribute("data-fading", "1"); } catch (e) { /* 忽略 */ }
|
|
512
|
+
setTimeout(function () { try { if (tip.parentNode) tip.parentNode.removeChild(tip); } catch (e) { /* 忽略 */ } }, 300);
|
|
513
|
+
}
|
|
514
|
+
} catch (e) { /* 忽略 */ }
|
|
515
|
+
}
|
|
516
|
+
/** 官方 UI 是否已经可用了(= 提示可以撤了)。 */
|
|
517
|
+
function maBootLooksReady() {
|
|
518
|
+
try {
|
|
519
|
+
// ① 适配层认出来的官方 frame(与下方 boot() 同一组选择器)
|
|
520
|
+
if (document.querySelector(".pI_x6G_frame, [data-sidebar-collapsed], [data-details-collapsed], [data-rightbar-collapsed]")) return true;
|
|
521
|
+
// ② 出现输入框/会话区也算"进来了"(官方改版首选信号失灵时的兜底)
|
|
522
|
+
return Boolean(document.querySelector('textarea, [contenteditable="true"], [role="textbox"]'));
|
|
523
|
+
} catch (e) { return false; }
|
|
524
|
+
}
|
|
525
|
+
if (document.readyState === "loading") {
|
|
526
|
+
document.addEventListener("DOMContentLoaded", maBootShow);
|
|
527
|
+
} else {
|
|
528
|
+
maBootShow();
|
|
529
|
+
}
|
|
530
|
+
// 轮询撤下:1s 一次,成本可忽略(只是两次 querySelector);一旦撤下就自我停止。
|
|
531
|
+
// 上限与"4 分钟兜底"对齐,避免页面异常时留下常驻定时器。
|
|
532
|
+
maBootWatchTimer = setInterval(function () {
|
|
533
|
+
if (maBootLooksReady()) { maBootHide(); return; }
|
|
534
|
+
if (!maBootTip) { try { clearInterval(maBootWatchTimer); } catch (e) { /* 忽略 */ } maBootWatchTimer = 0; }
|
|
535
|
+
}, 1000);
|
|
536
|
+
maBootTimers.push(setTimeout(function () { maBootHide(); }, 240 * 1000));
|
|
537
|
+
// 页面卸载/进 bfcache 时收干净(测试与真机都靠这条不留悬挂定时器)
|
|
538
|
+
try {
|
|
539
|
+
var maBootBye = function () { maBootHide(); };
|
|
540
|
+
window.addEventListener("pagehide", maBootBye);
|
|
541
|
+
window.addEventListener("unload", maBootBye);
|
|
542
|
+
} catch (e) { /* 忽略 */ }
|
|
543
|
+
|
|
429
544
|
/* —— 主机门解除(2026-09):镜像页(如 n.risegao.cn)经设备流认证回连同一台 127.0.0.1:3080,
|
|
430
545
|
桥已把 Host 回环化并剥 Origin(不扩大信任面)。官方客户端以 transport.ownsHost 判定
|
|
431
546
|
「自有主机」,非回环域名下不置 true 会把设置页置为 memory(不可用)镜像 →
|
|
@@ -1589,6 +1704,172 @@ const SCRIPT = `(() => {
|
|
|
1589
1704
|
try { if (typeof requestAnimationFrame === "function") { requestAnimationFrame(run); return; } } catch (eRaf) { /* 退化 */ }
|
|
1590
1705
|
setTimeout(run, 0);
|
|
1591
1706
|
};
|
|
1707
|
+
/* ===== 手机图片上传兼容(0.6.15)=====
|
|
1708
|
+
业主反馈:「手机端无法识别图片」——现象是选了照片发出去,模型看不到图 / 当成普通文件。
|
|
1709
|
+
|
|
1710
|
+
根因(官方 dsh web 客户端逻辑,dsh-client-ui-conversation 的 createDrafts):
|
|
1711
|
+
· 只有 file.type ∈ {image/png, image/jpeg, image/webp, image/gif} 才走**图片**通道
|
|
1712
|
+
(本地编码后随 prompt 发出,模型真的"能看");
|
|
1713
|
+
· 其余一律当**普通文件**:走 Web Worker 后台上传,模型只拿到一个文件引用。
|
|
1714
|
+
而手机恰恰最容易产出"非白名单"的图片:
|
|
1715
|
+
· iPhone 默认「高效」格式拍出来是 image/heic(不在白名单);
|
|
1716
|
+
· 部分 Android 图库 / 文件提供器给出的是**空 type**("");
|
|
1717
|
+
· 从"文件"入口选的还可能是 heif / bmp / tiff / avif。
|
|
1718
|
+
|
|
1719
|
+
做法(全在浏览器里完成,不动官方代码、不动 host):
|
|
1720
|
+
1) 在**捕获阶段**监听 change(比 React 根容器的监听更早):官方隐藏 input[type=file]
|
|
1721
|
+
被选中后,若里面有"是图片但不在白名单"的文件,就先拦下这次事件,
|
|
1722
|
+
用 canvas 把它转成 JPEG,再**替换 input.files 并重新派发一次 change** ——
|
|
1723
|
+
这样官方按图片通道收下,模型就能"看见"了;
|
|
1724
|
+
2) 任何一步失败(浏览器解不了 HEIC / canvas 不可用 / DataTransfer 不可用…)
|
|
1725
|
+
一律**把原始文件原样重新派发**,绝不比现状更差(宁可当普通文件,也不能丢文件);
|
|
1726
|
+
3) 官方认得的类型一个字节都不碰(零行为)。 */
|
|
1727
|
+
var MA_IMG_OK = { "image/png": 1, "image/jpeg": 1, "image/webp": 1, "image/gif": 1 };
|
|
1728
|
+
var MA_IMG_EXT_RE = /\\.(png|jpe?g|webp|gif|heic|heif|bmp|tiff?|avif)$/i;
|
|
1729
|
+
var MA_MAX_DIM = 4096; // 与官方 imageLimits.maxImageDimension 对齐,避免超限被拒
|
|
1730
|
+
var MA_CONVERT_TIMEOUT = 8000; // 解码卡住就放弃(原样放行)
|
|
1731
|
+
var MA_REENTRY = "__dshMaImageDone"; // 标记"这次 change 是我们自己派发的"
|
|
1732
|
+
|
|
1733
|
+
function maIsImageFile(f) {
|
|
1734
|
+
var t = String((f && f.type) || "").toLowerCase();
|
|
1735
|
+
if (t) return t.indexOf("image/") === 0;
|
|
1736
|
+
return MA_IMG_EXT_RE.test(String((f && f.name) || ""));
|
|
1737
|
+
}
|
|
1738
|
+
/** 需要转码吗:是图片、但不在官方白名单里。 */
|
|
1739
|
+
function maNeedsConvert(f) {
|
|
1740
|
+
var t = String((f && f.type) || "").toLowerCase();
|
|
1741
|
+
if (MA_IMG_OK[t]) return false;
|
|
1742
|
+
return maIsImageFile(f);
|
|
1743
|
+
}
|
|
1744
|
+
/** 扩展名 → 白名单里的 MIME(拿不到返回空)。 */
|
|
1745
|
+
function maMimeFromName(name) {
|
|
1746
|
+
var m = /\.([A-Za-z0-9]+)$/.exec(String(name || ""));
|
|
1747
|
+
if (!m) return "";
|
|
1748
|
+
var e = m[1].toLowerCase();
|
|
1749
|
+
if (e === "png") return "image/png";
|
|
1750
|
+
if (e === "jpg" || e === "jpeg") return "image/jpeg";
|
|
1751
|
+
if (e === "webp") return "image/webp";
|
|
1752
|
+
if (e === "gif") return "image/gif";
|
|
1753
|
+
return "";
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* 只改 type、不重编码(**无损**)。
|
|
1757
|
+
* 为什么需要:Android 图库/文件提供器很常见地给出 type:"",而文件本身其实是标准 PNG/JPEG ——
|
|
1758
|
+
* 这时把字节丢进 canvas 重编码纯属浪费(还可能丢透明通道),改个 MIME 就够了。
|
|
1759
|
+
*/
|
|
1760
|
+
function maRelabel(file, mime) {
|
|
1761
|
+
try {
|
|
1762
|
+
return new File([file], String(file.name || "image"), { type: mime, lastModified: file.lastModified || Date.now() });
|
|
1763
|
+
} catch (e) {
|
|
1764
|
+
try { var b = file.slice(0, file.size, mime); b.name = file.name; return b; } catch (e2) { return null; }
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
/** 单个文件的归一化:优先无损改 MIME,实在不行才 canvas 转 JPEG。 */
|
|
1768
|
+
async function maNormalizeImage(file) {
|
|
1769
|
+
var mime = maMimeFromName(file && file.name);
|
|
1770
|
+
if (mime && String(file.type || "") !== mime) {
|
|
1771
|
+
var relabeled = maRelabel(file, mime);
|
|
1772
|
+
if (relabeled) return relabeled;
|
|
1773
|
+
}
|
|
1774
|
+
return await maToJpeg(file);
|
|
1775
|
+
}
|
|
1776
|
+
function maToJpeg(file) {
|
|
1777
|
+
return new Promise(function (resolve) {
|
|
1778
|
+
var url = "";
|
|
1779
|
+
try { url = URL.createObjectURL(file); } catch (e0) { resolve(null); return; }
|
|
1780
|
+
var settled = false;
|
|
1781
|
+
var finish = function (out) {
|
|
1782
|
+
if (settled) return;
|
|
1783
|
+
settled = true;
|
|
1784
|
+
try { URL.revokeObjectURL(url); } catch (e1) { /* 忽略 */ }
|
|
1785
|
+
resolve(out);
|
|
1786
|
+
};
|
|
1787
|
+
var timer = setTimeout(function () { finish(null); }, MA_CONVERT_TIMEOUT);
|
|
1788
|
+
var img;
|
|
1789
|
+
try { img = new Image(); } catch (e2) { clearTimeout(timer); finish(null); return; }
|
|
1790
|
+
img.onload = function () {
|
|
1791
|
+
clearTimeout(timer);
|
|
1792
|
+
try {
|
|
1793
|
+
var w = img.naturalWidth || img.width || 0;
|
|
1794
|
+
var h = img.naturalHeight || img.height || 0;
|
|
1795
|
+
if (!w || !h) return finish(null);
|
|
1796
|
+
var scale = Math.min(1, MA_MAX_DIM / Math.max(w, h));
|
|
1797
|
+
var cw = Math.max(1, Math.round(w * scale));
|
|
1798
|
+
var ch = Math.max(1, Math.round(h * scale));
|
|
1799
|
+
var cv = document.createElement("canvas");
|
|
1800
|
+
cv.width = cw; cv.height = ch;
|
|
1801
|
+
var ctx = cv.getContext("2d");
|
|
1802
|
+
if (!ctx) return finish(null);
|
|
1803
|
+
ctx.drawImage(img, 0, 0, cw, ch);
|
|
1804
|
+
cv.toBlob(function (blob) {
|
|
1805
|
+
if (!blob) return finish(null);
|
|
1806
|
+
var base = String(file.name || "photo").replace(/\\.[^./\\\\]+$/, "") || "photo";
|
|
1807
|
+
try {
|
|
1808
|
+
finish(new File([blob], base + ".jpg", { type: "image/jpeg", lastModified: Date.now() }));
|
|
1809
|
+
} catch (e3) {
|
|
1810
|
+
// 极老的浏览器没有 File 构造器 → 用 type 改写过的 Blob 兜底(官方只看 type/name)
|
|
1811
|
+
try { blob.name = base + ".jpg"; finish(blob); } catch (e4) { finish(null); }
|
|
1812
|
+
}
|
|
1813
|
+
}, "image/jpeg", 0.92);
|
|
1814
|
+
} catch (e5) { finish(null); }
|
|
1815
|
+
};
|
|
1816
|
+
img.onerror = function () { clearTimeout(timer); finish(null); };
|
|
1817
|
+
try { img.src = url; } catch (e6) { clearTimeout(timer); finish(null); }
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
/** 把 files 塞回 input(DataTransfer 缺失/只读 → 返回 false,调用方走"原样重发")。 */
|
|
1821
|
+
function maSetInputFiles(input, files) {
|
|
1822
|
+
try {
|
|
1823
|
+
var dt = new DataTransfer();
|
|
1824
|
+
for (var i = 0; i < files.length; i++) dt.items.add(files[i]);
|
|
1825
|
+
if (!dt.files || dt.files.length !== files.length) return false;
|
|
1826
|
+
input.files = dt.files;
|
|
1827
|
+
return true;
|
|
1828
|
+
} catch (e) { return false; }
|
|
1829
|
+
}
|
|
1830
|
+
/** 重新派发一次 change(带重入标记,避免我们自己的监听再拦一次)。 */
|
|
1831
|
+
function maRedispatch(input) {
|
|
1832
|
+
try {
|
|
1833
|
+
input[MA_REENTRY] = true;
|
|
1834
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1835
|
+
} catch (e) { /* 忽略 */ } finally {
|
|
1836
|
+
setTimeout(function () { try { input[MA_REENTRY] = false; } catch (e2) { /* 忽略 */ } }, 0);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
async function maOnFilePicked(input) {
|
|
1840
|
+
var list = input && input.files ? Array.prototype.slice.call(input.files) : [];
|
|
1841
|
+
if (!list.length) return;
|
|
1842
|
+
var need = list.filter(maNeedsConvert);
|
|
1843
|
+
if (!need.length) return; // 官方认得的类型 → 零行为
|
|
1844
|
+
var out = [];
|
|
1845
|
+
for (var i = 0; i < list.length; i++) {
|
|
1846
|
+
if (!maNeedsConvert(list[i])) { out.push(list[i]); continue; }
|
|
1847
|
+
var conv = await maNormalizeImage(list[i]);
|
|
1848
|
+
if (conv) out.push(conv);
|
|
1849
|
+
else out.push(list[i]); // 转不了就按原文件走(与现状一致,绝不丢)
|
|
1850
|
+
}
|
|
1851
|
+
if (maSetInputFiles(input, out)) maRedispatch(input);
|
|
1852
|
+
else maRedispatch(input); // 塞不回去 → 原样重发(文件不丢)
|
|
1853
|
+
}
|
|
1854
|
+
function maInstallImageCompat() {
|
|
1855
|
+
try {
|
|
1856
|
+
document.addEventListener("change", function (e) {
|
|
1857
|
+
var t = e.target;
|
|
1858
|
+
if (!t || String(t.tagName || "").toUpperCase() !== "INPUT") return;
|
|
1859
|
+
if (String(t.type || "").toLowerCase() !== "file") return;
|
|
1860
|
+
if (t[MA_REENTRY]) return; // 我们自己派发的那次 → 放行
|
|
1861
|
+
var files = t.files ? Array.prototype.slice.call(t.files) : [];
|
|
1862
|
+
if (!files.length) return;
|
|
1863
|
+
var any = false;
|
|
1864
|
+
for (var i = 0; i < files.length; i++) { if (maNeedsConvert(files[i])) { any = true; break; } }
|
|
1865
|
+
if (!any) return; // 无需转码 → 完全不拦(官方照旧处理)
|
|
1866
|
+
// 拦下这一次:官方若先看到原文件,会立刻按"普通文件"起后台上传(那就白改了)
|
|
1867
|
+
try { e.stopPropagation(); } catch (eS) { /* 忽略 */ }
|
|
1868
|
+
maOnFilePicked(t).catch(function () { maRedispatch(t); });
|
|
1869
|
+
}, true); // ★ 捕获阶段:必须早于 React 根容器的 change 监听
|
|
1870
|
+
} catch (e) { /* 适配层绝不把页面拖挂 */ }
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1592
1873
|
/* 挂上"叫醒"的两条路。做成可重入(先清旧的再建新的),这样 bfcache 恢复后能重新武装。 */
|
|
1593
1874
|
const maArm = () => {
|
|
1594
1875
|
maTorn = false;
|
|
@@ -1608,6 +1889,7 @@ const SCRIPT = `(() => {
|
|
|
1608
1889
|
try { maInterval = setInterval(pump, 1000); } catch (eInt) { maInterval = 0; }
|
|
1609
1890
|
};
|
|
1610
1891
|
maArm();
|
|
1892
|
+
maInstallImageCompat(); // 手机图片上传兼容(见上方长注释):装了就一直有效,与 pump 无关
|
|
1611
1893
|
/* 清理:页面卸载时断开观察者与兜底表(bfcache/长驻页面都不留悬挂回调)。
|
|
1612
1894
|
pagehide 覆盖 bfcache 场景,unload 兜底更老的浏览器。 */
|
|
1613
1895
|
const maTeardown = () => {
|