rdsh-gateway 0.9.0 → 0.10.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/dist/join.d.ts +2 -0
- package/dist/join.js +37 -1
- package/dist/rdsh-webview-api.d.ts +17 -0
- package/dist/rdsh-webview-api.js +151 -0
- package/dist/server.js +5 -2
- package/dist/spawn-dsh.d.ts +4 -1
- package/dist/spawn-dsh.js +4 -1
- package/package.json +1 -1
package/dist/join.d.ts
CHANGED
|
@@ -105,6 +105,8 @@ export declare function classifyUpstreamFailure(code: string | undefined, respon
|
|
|
105
105
|
};
|
|
106
106
|
/** JS 响应判定(content-type 含 javascript)。 */
|
|
107
107
|
export declare function isJsContentType(headers: IncomingHttpHeaders): boolean;
|
|
108
|
+
/** HTML 响应判定(content-type 含 text/html)。 */
|
|
109
|
+
export declare function isHtmlContentType(headers: IncomingHttpHeaders): boolean;
|
|
108
110
|
/**
|
|
109
111
|
* 最小 patch:把 DSH 客户端 bundle 里的前端 isLoopback 判定替换为 true
|
|
110
112
|
* (持久设置/API key 输入只对 loopback 开放;E2EE 流上信任基础等同 loopback)。
|
package/dist/join.js
CHANGED
|
@@ -22,6 +22,7 @@ import { acquireJoinLock, releaseJoinLock } from "./lock.js";
|
|
|
22
22
|
import { responderHandshake, Aead } from "./e2ee.js";
|
|
23
23
|
import { loadOrCreateE2eeKeyPair } from "./e2ee-key-store.js";
|
|
24
24
|
import { GATE_COOKIE, signGateCookie, verifyGateCookie, verifyGateCode } from "./access-gate.js";
|
|
25
|
+
import { RDSH_WEBVIEW_API, injectHtmlScript } from "./rdsh-webview-api.js";
|
|
25
26
|
/** 判断错误是否为 TLS 证书类错误(自签/过期/域名不匹配)。 */
|
|
26
27
|
function isCertError(err) {
|
|
27
28
|
const code = err?.code ?? "";
|
|
@@ -157,6 +158,12 @@ export function isJsContentType(headers) {
|
|
|
157
158
|
const s = Array.isArray(ct) ? ct.join(";") : (ct ?? "");
|
|
158
159
|
return /javascript/i.test(s);
|
|
159
160
|
}
|
|
161
|
+
/** HTML 响应判定(content-type 含 text/html)。 */
|
|
162
|
+
export function isHtmlContentType(headers) {
|
|
163
|
+
const ct = headers["content-type"];
|
|
164
|
+
const s = Array.isArray(ct) ? ct.join(";") : (ct ?? "");
|
|
165
|
+
return /text\/html/i.test(s);
|
|
166
|
+
}
|
|
160
167
|
/**
|
|
161
168
|
* 最小 patch:把 DSH 客户端 bundle 里的前端 isLoopback 判定替换为 true
|
|
162
169
|
* (持久设置/API key 输入只对 loopback 开放;E2EE 流上信任基础等同 loopback)。
|
|
@@ -309,6 +316,8 @@ export function startJoin(opts) {
|
|
|
309
316
|
* 键含 query 且不截断:combo 路由用 query 选内容,资源 URL 由内容决定(rev/hash),内容一变 URL 就变。
|
|
310
317
|
*/
|
|
311
318
|
const patchSkipped = new Set();
|
|
319
|
+
/** rdsh WebView API 是否已注入过(每个进程只打一次日志,避免页面加载刷屏)。 */
|
|
320
|
+
let adapterInjectionLogged = false;
|
|
312
321
|
/** 从转发头里取 rdsh_gate cookie(hub D12 白名单透传)。 */
|
|
313
322
|
function gateCookie(headers) {
|
|
314
323
|
const ck = headers["cookie"];
|
|
@@ -484,6 +493,33 @@ export function startJoin(opts) {
|
|
|
484
493
|
const baseHeaders = normalizeRespHeaders(upRes.headers);
|
|
485
494
|
// 已确认该 URL 不含目标串(DSH 前端壳资源就属此类)→ 直接流式,省掉白解压与全量扫描
|
|
486
495
|
const wantsPatch = dio?.jsPatch?.() === true && isJsContentType(upRes.headers) && !patchSkipped.has(path);
|
|
496
|
+
const wantsInject = dio?.htmlInject !== undefined && isHtmlContentType(upRes.headers) && upRes.headers["content-encoding"] === undefined;
|
|
497
|
+
// HTML + 注入:缓冲后注入(htmlInject 会改 body 长度,须先处理再发 OPEN 并重算 content-length)
|
|
498
|
+
if (wantsInject) {
|
|
499
|
+
const htmlChunks = [];
|
|
500
|
+
upRes.on("data", (chunk) => htmlChunks.push(chunk));
|
|
501
|
+
upRes.on("end", () => {
|
|
502
|
+
const html = injectHtmlScript(Buffer.concat(htmlChunks).toString("utf8"), dio.htmlInject);
|
|
503
|
+
const outHeaders = {
|
|
504
|
+
...baseHeaders,
|
|
505
|
+
"content-length": String(Buffer.byteLength(html)),
|
|
506
|
+
};
|
|
507
|
+
delete outHeaders["transfer-encoding"];
|
|
508
|
+
if (!adapterInjectionLogged) {
|
|
509
|
+
adapterInjectionLogged = true;
|
|
510
|
+
console.log("[rdsh] rdsh webview api injected (window.__rdshWebViewApi v1)");
|
|
511
|
+
}
|
|
512
|
+
send(encodeFrame(FRAME_TYPE.OPEN, streamId, jsonPayload({ kind: "http", status, reason: upRes.statusMessage, headers: outHeaders })));
|
|
513
|
+
sendChunkedBody(send, streamId, Buffer.from(html));
|
|
514
|
+
send(encodeFrame(FRAME_TYPE.CLOSE, streamId, jsonPayload({ code: 0 })));
|
|
515
|
+
httpStreams.delete(streamId);
|
|
516
|
+
});
|
|
517
|
+
upRes.on("error", () => {
|
|
518
|
+
send(encodeFrame(FRAME_TYPE.CLOSE, streamId, jsonPayload({ code: 502, message: "upstream error" })));
|
|
519
|
+
httpStreams.delete(streamId);
|
|
520
|
+
});
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
487
523
|
// 非 JS / 未开补丁:保持原流式路径(OPEN 立即发,body 边到边发)
|
|
488
524
|
if (!wantsPatch) {
|
|
489
525
|
send(encodeFrame(FRAME_TYPE.OPEN, streamId, jsonPayload({ kind: "http", status, reason: upRes.statusMessage, headers: baseHeaders })));
|
|
@@ -636,7 +672,7 @@ export function startJoin(opts) {
|
|
|
636
672
|
}
|
|
637
673
|
return { handleFrame, cleanup };
|
|
638
674
|
}
|
|
639
|
-
const plainDispatcher = makeInnerDispatcher(sendTunnelFrame, { jsPatch: () => uiCompat.trustE2EEAsLoopback, gate: true });
|
|
675
|
+
const plainDispatcher = makeInnerDispatcher(sendTunnelFrame, { jsPatch: () => uiCompat.trustE2EEAsLoopback, gate: true, htmlInject: RDSH_WEBVIEW_API });
|
|
640
676
|
// host 端 E2EE 静态密钥对(持久化 ~/.rdsh/e2ee-key.json;join 注册时上送指纹)
|
|
641
677
|
let hostE2eeKeypair;
|
|
642
678
|
try {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rdsh-webview-api.ts — 智能体 API 契约适配脚本(注入 DSH 首页,feature 22)。
|
|
3
|
+
*
|
|
4
|
+
* 挂出稳定契约 `window.__rdshWebViewApi`(version 1),供 WebView 客户端(如 garsync App)优先消费,
|
|
5
|
+
* 以把「DSH 版本适配」从 App(发版慢)挪到 gateway(随 npm 包快速发布)。
|
|
6
|
+
*
|
|
7
|
+
* 契约正文与语义以 garsync `doc/feature/108_agent_api_contract/req.md` R7 为准;
|
|
8
|
+
* 本脚本是 garsync `lib/rdsh/rdsh_web_bridge.dart` 的 `_bridgeJs` 的**精简等价物**:
|
|
9
|
+
* - 保留:isDshPage / readReply / fillAndSend 三方法及其 DOM 细节(Lexical 写入管线、
|
|
10
|
+
* 键盘守卫、最后一轮答案提取、UTF-8 安全 base64);
|
|
11
|
+
* - 去掉:`report()` 通道与 105 诊断探针(那是 App 侧独有的 debug 机制)。
|
|
12
|
+
*
|
|
13
|
+
* 安全约定:幂等(已注入即返回)、防御式(任何 DOM 缺失/异常返回安全默认值、绝不抛错)。
|
|
14
|
+
*/
|
|
15
|
+
export declare const RDSH_WEBVIEW_API = "(function () {\n if (window.__rdshWebViewApi) return;\n function findComposer() {\n var el = document.querySelector('[data-lexical-editor=\"true\"]');\n if (el) return { el: el, kind: 'lexical' };\n el = document.querySelector('[contenteditable=\"true\"]');\n if (el) return { el: el, kind: 'contenteditable' };\n el = document.querySelector('textarea');\n if (el) return { el: el, kind: 'textarea' };\n return null;\n }\n function selectAll(el) {\n var tag = el.tagName;\n if (tag === 'TEXTAREA' || tag === 'INPUT') { el.select(); return; }\n var range = document.createRange();\n range.selectNodeContents(el);\n var sel = window.getSelection();\n sel.removeAllRanges();\n sel.addRange(range);\n }\n function insertText(el, text) {\n el.focus();\n try { selectAll(el); } catch (e) {}\n // 1) execCommand \u8D70\u6D4F\u89C8\u5668\u7F16\u8F91\u7BA1\u7EBF\uFF0C\u8BA9 Lexical \u6536\u5230 input \u4E8B\u4EF6\u3002\n try { if (document.execCommand('insertText', false, text)) return true; } catch (e) {}\n // 2) \u663E\u5F0F beforeinput\u3002\n try {\n el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: text, bubbles: true, cancelable: true }));\n return true;\n } catch (e) {}\n // 3) \u515C\u5E95\uFF1A\u76F4\u63A5\u5199 DOM + \u89E6\u53D1 input\u3002\n try {\n var tag = el.tagName;\n if (tag === 'TEXTAREA' || tag === 'INPUT') {\n var proto = tag === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;\n Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, text);\n } else {\n el.textContent = text;\n }\n el.dispatchEvent(new Event('input', { bubbles: true }));\n return true;\n } catch (e) {}\n return false;\n }\n function pressEnter(el) {\n var opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true, isComposing: false };\n try {\n el.dispatchEvent(new KeyboardEvent('keydown', opts));\n el.dispatchEvent(new KeyboardEvent('keypress', opts));\n el.dispatchEvent(new KeyboardEvent('keyup', opts));\n } catch (e) {}\n }\n // \u952E\u76D8\u5B88\u536B\uFF1A\u8BED\u97F3\u5199\u5165\u540E\u5173\u8F6F\u952E\u76D8\uFF1B\u7528\u6237\u771F\u5B9E\u89E6\u6478\u9875\u9762\u5373\u89E3\u9664\u3002\n var keyboardGuard = false;\n function dismissKeyboard() {\n try {\n var active = document.activeElement;\n if (active && active !== document.body && typeof active.blur === 'function') active.blur();\n var selection = window.getSelection();\n if (selection && selection.removeAllRanges) selection.removeAllRanges();\n } catch (e) {}\n }\n function onUserTouch() { if (keyboardGuard) keyboardGuard = false; }\n document.addEventListener('focusin', function () { if (!keyboardGuard) return; dismissKeyboard(); }, true);\n document.addEventListener('pointerdown', onUserTouch, true);\n document.addEventListener('touchstart', onUserTouch, true);\n\n // \u5355\u4E2A flow \u8282\u70B9\u7684\u53EF\u89C1\u6587\u672C\uFF1BproseOnly \u989D\u5916\u4E22\u5F03\u6574\u5757\u4EE3\u7801\u4E0E\u8868\u683C\uFF08105 \u53EA\u8BFB\u7B54\u6848\u6B63\u6587\uFF09\u3002\n function answerTextOf(el, proseOnly) {\n var clone = el.cloneNode(true);\n var selector = '[data-variant=\"think\"],[data-variant=\"others\"]';\n if (proseOnly) selector += ',pre,table';\n var drop = clone.querySelectorAll(selector);\n for (var i = 0; i < drop.length; i++) { if (drop[i].parentNode) drop[i].parentNode.removeChild(drop[i]); }\n return (clone.innerText || clone.textContent || '').trim();\n }\n // \u6700\u540E\u4E00\u8F6E\u7684\u7B54\u6848\u90E8\u5206\uFF08\u6392\u9664 reasoning\uFF0C\u53D6\u6700\u5927 turn\uFF0C\u9010 part \u62FC\u63A5\uFF09\u3002\n function latestAnswer() {\n var steps = Array.prototype.slice.call(document.querySelectorAll('[data-chat-flow-kind=\"assistant-step\"]'));\n var answers = steps.filter(function (el) { return el.getAttribute('data-chat-group-part') !== 'reasoning'; });\n var maxTurn = -1;\n for (var i = 0; i < answers.length; i++) {\n var turn = parseInt(answers[i].getAttribute('data-chat-turn'), 10);\n if (!isNaN(turn) && turn > maxTurn) maxTurn = turn;\n }\n var parts = answers.filter(function (el) { return parseInt(el.getAttribute('data-chat-turn'), 10) === maxTurn; });\n var texts = [];\n for (var j = 0; j < parts.length; j++) {\n var text = answerTextOf(parts[j], true);\n if (text.length) texts.push(text);\n }\n return texts.join('\\n\\n');\n }\n\n window.__rdshWebViewApi = {\n version: 1,\n // 104\uFF1A\u5199\u5165 composer \u5E76\u63D0\u4EA4\u3002\u8FD4\u56DE\u88F8\u72B6\u6001 token\u3002\n fillAndSend: function (text) {\n var found = findComposer();\n if (!found) return 'no-composer';\n keyboardGuard = false;\n if (!insertText(found.el, text)) return 'fill-failed';\n keyboardGuard = true;\n dismissKeyboard();\n setTimeout(function () { pressEnter(found.el); dismissKeyboard(); }, 80);\n return 'ok';\n },\n // 105\uFF1A\u5F85\u6717\u8BFB\u5185\u5BB9 + \u72B6\u6001\u3002\u8FD4\u56DE UTF-8 \u5B89\u5168\u7684 base64 JSON {dsh, streaming, text}\u3002\n readReply: function () {\n var out = { dsh: false, streaming: 0, text: '' };\n try {\n out.streaming = document.querySelectorAll('[data-streaming]').length;\n if (document.querySelectorAll('[data-chat-flow-kind]').length > 0) {\n out.dsh = true;\n out.text = latestAnswer();\n } else {\n out.text = document.body ? document.body.innerText : '';\n }\n } catch (e) {}\n return window.btoa(unescape(encodeURIComponent(JSON.stringify(out))));\n },\n // 107\uFF1A\u662F\u5426 DSH \u4F1A\u8BDD\u9875\uFF08\u8F7B\u91CF\u5224\u5B9A\uFF0C\u53EF\u8F6E\u8BE2\uFF09\u3002\u8FD4\u56DE\u88F8 '1' / '0'\u3002\n isDshPage: function () {\n try {\n return document.querySelectorAll('[data-chat-flow-kind]').length > 0 ? '1' : '0';\n } catch (e) { return '0'; }\n }\n };\n})();";
|
|
16
|
+
/** 把 `<script>${script}</script>` 插到 `</head>` 前(无则前置);纯函数,可单测。 */
|
|
17
|
+
export declare function injectHtmlScript(html: string, script: string): string;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rdsh-webview-api.ts — 智能体 API 契约适配脚本(注入 DSH 首页,feature 22)。
|
|
3
|
+
*
|
|
4
|
+
* 挂出稳定契约 `window.__rdshWebViewApi`(version 1),供 WebView 客户端(如 garsync App)优先消费,
|
|
5
|
+
* 以把「DSH 版本适配」从 App(发版慢)挪到 gateway(随 npm 包快速发布)。
|
|
6
|
+
*
|
|
7
|
+
* 契约正文与语义以 garsync `doc/feature/108_agent_api_contract/req.md` R7 为准;
|
|
8
|
+
* 本脚本是 garsync `lib/rdsh/rdsh_web_bridge.dart` 的 `_bridgeJs` 的**精简等价物**:
|
|
9
|
+
* - 保留:isDshPage / readReply / fillAndSend 三方法及其 DOM 细节(Lexical 写入管线、
|
|
10
|
+
* 键盘守卫、最后一轮答案提取、UTF-8 安全 base64);
|
|
11
|
+
* - 去掉:`report()` 通道与 105 诊断探针(那是 App 侧独有的 debug 机制)。
|
|
12
|
+
*
|
|
13
|
+
* 安全约定:幂等(已注入即返回)、防御式(任何 DOM 缺失/异常返回安全默认值、绝不抛错)。
|
|
14
|
+
*/
|
|
15
|
+
export const RDSH_WEBVIEW_API = `(function () {
|
|
16
|
+
if (window.__rdshWebViewApi) return;
|
|
17
|
+
function findComposer() {
|
|
18
|
+
var el = document.querySelector('[data-lexical-editor="true"]');
|
|
19
|
+
if (el) return { el: el, kind: 'lexical' };
|
|
20
|
+
el = document.querySelector('[contenteditable="true"]');
|
|
21
|
+
if (el) return { el: el, kind: 'contenteditable' };
|
|
22
|
+
el = document.querySelector('textarea');
|
|
23
|
+
if (el) return { el: el, kind: 'textarea' };
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function selectAll(el) {
|
|
27
|
+
var tag = el.tagName;
|
|
28
|
+
if (tag === 'TEXTAREA' || tag === 'INPUT') { el.select(); return; }
|
|
29
|
+
var range = document.createRange();
|
|
30
|
+
range.selectNodeContents(el);
|
|
31
|
+
var sel = window.getSelection();
|
|
32
|
+
sel.removeAllRanges();
|
|
33
|
+
sel.addRange(range);
|
|
34
|
+
}
|
|
35
|
+
function insertText(el, text) {
|
|
36
|
+
el.focus();
|
|
37
|
+
try { selectAll(el); } catch (e) {}
|
|
38
|
+
// 1) execCommand 走浏览器编辑管线,让 Lexical 收到 input 事件。
|
|
39
|
+
try { if (document.execCommand('insertText', false, text)) return true; } catch (e) {}
|
|
40
|
+
// 2) 显式 beforeinput。
|
|
41
|
+
try {
|
|
42
|
+
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: text, bubbles: true, cancelable: true }));
|
|
43
|
+
return true;
|
|
44
|
+
} catch (e) {}
|
|
45
|
+
// 3) 兜底:直接写 DOM + 触发 input。
|
|
46
|
+
try {
|
|
47
|
+
var tag = el.tagName;
|
|
48
|
+
if (tag === 'TEXTAREA' || tag === 'INPUT') {
|
|
49
|
+
var proto = tag === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
50
|
+
Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, text);
|
|
51
|
+
} else {
|
|
52
|
+
el.textContent = text;
|
|
53
|
+
}
|
|
54
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
55
|
+
return true;
|
|
56
|
+
} catch (e) {}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function pressEnter(el) {
|
|
60
|
+
var opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true, isComposing: false };
|
|
61
|
+
try {
|
|
62
|
+
el.dispatchEvent(new KeyboardEvent('keydown', opts));
|
|
63
|
+
el.dispatchEvent(new KeyboardEvent('keypress', opts));
|
|
64
|
+
el.dispatchEvent(new KeyboardEvent('keyup', opts));
|
|
65
|
+
} catch (e) {}
|
|
66
|
+
}
|
|
67
|
+
// 键盘守卫:语音写入后关软键盘;用户真实触摸页面即解除。
|
|
68
|
+
var keyboardGuard = false;
|
|
69
|
+
function dismissKeyboard() {
|
|
70
|
+
try {
|
|
71
|
+
var active = document.activeElement;
|
|
72
|
+
if (active && active !== document.body && typeof active.blur === 'function') active.blur();
|
|
73
|
+
var selection = window.getSelection();
|
|
74
|
+
if (selection && selection.removeAllRanges) selection.removeAllRanges();
|
|
75
|
+
} catch (e) {}
|
|
76
|
+
}
|
|
77
|
+
function onUserTouch() { if (keyboardGuard) keyboardGuard = false; }
|
|
78
|
+
document.addEventListener('focusin', function () { if (!keyboardGuard) return; dismissKeyboard(); }, true);
|
|
79
|
+
document.addEventListener('pointerdown', onUserTouch, true);
|
|
80
|
+
document.addEventListener('touchstart', onUserTouch, true);
|
|
81
|
+
|
|
82
|
+
// 单个 flow 节点的可见文本;proseOnly 额外丢弃整块代码与表格(105 只读答案正文)。
|
|
83
|
+
function answerTextOf(el, proseOnly) {
|
|
84
|
+
var clone = el.cloneNode(true);
|
|
85
|
+
var selector = '[data-variant="think"],[data-variant="others"]';
|
|
86
|
+
if (proseOnly) selector += ',pre,table';
|
|
87
|
+
var drop = clone.querySelectorAll(selector);
|
|
88
|
+
for (var i = 0; i < drop.length; i++) { if (drop[i].parentNode) drop[i].parentNode.removeChild(drop[i]); }
|
|
89
|
+
return (clone.innerText || clone.textContent || '').trim();
|
|
90
|
+
}
|
|
91
|
+
// 最后一轮的答案部分(排除 reasoning,取最大 turn,逐 part 拼接)。
|
|
92
|
+
function latestAnswer() {
|
|
93
|
+
var steps = Array.prototype.slice.call(document.querySelectorAll('[data-chat-flow-kind="assistant-step"]'));
|
|
94
|
+
var answers = steps.filter(function (el) { return el.getAttribute('data-chat-group-part') !== 'reasoning'; });
|
|
95
|
+
var maxTurn = -1;
|
|
96
|
+
for (var i = 0; i < answers.length; i++) {
|
|
97
|
+
var turn = parseInt(answers[i].getAttribute('data-chat-turn'), 10);
|
|
98
|
+
if (!isNaN(turn) && turn > maxTurn) maxTurn = turn;
|
|
99
|
+
}
|
|
100
|
+
var parts = answers.filter(function (el) { return parseInt(el.getAttribute('data-chat-turn'), 10) === maxTurn; });
|
|
101
|
+
var texts = [];
|
|
102
|
+
for (var j = 0; j < parts.length; j++) {
|
|
103
|
+
var text = answerTextOf(parts[j], true);
|
|
104
|
+
if (text.length) texts.push(text);
|
|
105
|
+
}
|
|
106
|
+
return texts.join('\\n\\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
window.__rdshWebViewApi = {
|
|
110
|
+
version: 1,
|
|
111
|
+
// 104:写入 composer 并提交。返回裸状态 token。
|
|
112
|
+
fillAndSend: function (text) {
|
|
113
|
+
var found = findComposer();
|
|
114
|
+
if (!found) return 'no-composer';
|
|
115
|
+
keyboardGuard = false;
|
|
116
|
+
if (!insertText(found.el, text)) return 'fill-failed';
|
|
117
|
+
keyboardGuard = true;
|
|
118
|
+
dismissKeyboard();
|
|
119
|
+
setTimeout(function () { pressEnter(found.el); dismissKeyboard(); }, 80);
|
|
120
|
+
return 'ok';
|
|
121
|
+
},
|
|
122
|
+
// 105:待朗读内容 + 状态。返回 UTF-8 安全的 base64 JSON {dsh, streaming, text}。
|
|
123
|
+
readReply: function () {
|
|
124
|
+
var out = { dsh: false, streaming: 0, text: '' };
|
|
125
|
+
try {
|
|
126
|
+
out.streaming = document.querySelectorAll('[data-streaming]').length;
|
|
127
|
+
if (document.querySelectorAll('[data-chat-flow-kind]').length > 0) {
|
|
128
|
+
out.dsh = true;
|
|
129
|
+
out.text = latestAnswer();
|
|
130
|
+
} else {
|
|
131
|
+
out.text = document.body ? document.body.innerText : '';
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {}
|
|
134
|
+
return window.btoa(unescape(encodeURIComponent(JSON.stringify(out))));
|
|
135
|
+
},
|
|
136
|
+
// 107:是否 DSH 会话页(轻量判定,可轮询)。返回裸 '1' / '0'。
|
|
137
|
+
isDshPage: function () {
|
|
138
|
+
try {
|
|
139
|
+
return document.querySelectorAll('[data-chat-flow-kind]').length > 0 ? '1' : '0';
|
|
140
|
+
} catch (e) { return '0'; }
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
})();`;
|
|
144
|
+
/** 把 `<script>${script}</script>` 插到 `</head>` 前(无则前置);纯函数,可单测。 */
|
|
145
|
+
export function injectHtmlScript(html, script) {
|
|
146
|
+
const tag = `<script>${script}</script>`;
|
|
147
|
+
if (html.includes("</head>"))
|
|
148
|
+
return html.replace("</head>", `${tag}</head>`);
|
|
149
|
+
return `${tag}${html}`;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=rdsh-webview-api.js.map
|
package/dist/server.js
CHANGED
|
@@ -22,8 +22,11 @@ import { patchLoopbackJs } from "./join.js";
|
|
|
22
22
|
import { pairPageHtml } from "./pair-page.js";
|
|
23
23
|
import { loginPageHtml } from "./login-page.js";
|
|
24
24
|
import { SECURE_CONTEXT_POLYFILL } from "./secure-context-polyfill.js";
|
|
25
|
+
import { RDSH_WEBVIEW_API } from "./rdsh-webview-api.js";
|
|
25
26
|
import { loadConfig } from "./config.js";
|
|
26
27
|
import { ipInCidrs } from "./cidr.js";
|
|
28
|
+
/** 注入 DSH 首页的脚本:非 secure context polyfill + rdsh WebView API 契约。两处 forwardHttp 共用,避免漂移。 */
|
|
29
|
+
const HTML_INJECT = SECURE_CONTEXT_POLYFILL + RDSH_WEBVIEW_API;
|
|
27
30
|
const MAX_LOGIN_FAILS = 5;
|
|
28
31
|
const LOGIN_LOCK_MS = 10 * 60 * 1000;
|
|
29
32
|
export async function startGateway(opts) {
|
|
@@ -159,7 +162,7 @@ async function handleHttp(req, res, ctx, loginLimiter) {
|
|
|
159
162
|
}
|
|
160
163
|
if (ctx.authMode === "none") {
|
|
161
164
|
forwardHttp(req, res, ctx.target, {
|
|
162
|
-
htmlInject:
|
|
165
|
+
htmlInject: HTML_INJECT,
|
|
163
166
|
authCookie: ctx.dshAuthCookieHeader,
|
|
164
167
|
jsPatch: ctx.trustPairedAsLoopback ? patchLoopbackJs : undefined,
|
|
165
168
|
});
|
|
@@ -171,7 +174,7 @@ async function handleHttp(req, res, ctx, loginLimiter) {
|
|
|
171
174
|
return;
|
|
172
175
|
}
|
|
173
176
|
forwardHttp(req, res, ctx.target, {
|
|
174
|
-
htmlInject:
|
|
177
|
+
htmlInject: HTML_INJECT,
|
|
175
178
|
authCookie: ctx.dshAuthCookieHeader,
|
|
176
179
|
// 会话分支同样要打 loopback 补丁:LAN 没有 E2EE shim,设置/API key 唯一依赖这个补丁;
|
|
177
180
|
// 旧实现只在 authMode==="none" 分支传了 jsPatch ⇒ 配对/登录模式下设置页打不开(2026-09-14 复审发现)
|
package/dist/spawn-dsh.d.ts
CHANGED
|
@@ -80,9 +80,12 @@ export declare function compareDshVersions(a: string, b: string): number;
|
|
|
80
80
|
* 2026-09-11:MAX 扩到 `0.1.5-rc.2` —— 真机实测通过(spawn + ready 行 token + 会话 cookie 换发 +
|
|
81
81
|
* `/api` 转发 + HTML 注入 + WS `/api/remote.mux` 桥接 + join 的 `patchLoopbackJs` 命中),
|
|
82
82
|
* 见 doc/review/20260911-dsh-0.1.5-rc.2-plugin-compat.md §5。
|
|
83
|
+
* 2026-09-24:MAX 扩到 `0.1.7-rc.1` —— 同一份 G1–G7 清单重跑通过。0.1.7 只改前端 dist 形态
|
|
84
|
+
* (`<base href="./">` + 相对 `plugins/…`),认证/RPC 未变;网关是同源透明代理,相对引用由浏览器
|
|
85
|
+
* 解析成绝对路径后原样转发,故不受影响,见 doc/review/20260924-dsh-0.1.7-rc.1-compat.md。
|
|
83
86
|
*/
|
|
84
87
|
export declare const DSH_COMPAT_MIN = "0.1.1-rc.2";
|
|
85
|
-
export declare const DSH_COMPAT_MAX = "0.1.
|
|
88
|
+
export declare const DSH_COMPAT_MAX = "0.1.7-rc.1";
|
|
86
89
|
/**
|
|
87
90
|
* 版本落在实测窗口外时的提示文案(零文档导向:直接给动作指令,用户不查表)。
|
|
88
91
|
* - 比 max 新 → 升级 remote-dsh 或暂用旧 dsh;
|
package/dist/spawn-dsh.js
CHANGED
|
@@ -287,9 +287,12 @@ export function compareDshVersions(a, b) {
|
|
|
287
287
|
* 2026-09-11:MAX 扩到 `0.1.5-rc.2` —— 真机实测通过(spawn + ready 行 token + 会话 cookie 换发 +
|
|
288
288
|
* `/api` 转发 + HTML 注入 + WS `/api/remote.mux` 桥接 + join 的 `patchLoopbackJs` 命中),
|
|
289
289
|
* 见 doc/review/20260911-dsh-0.1.5-rc.2-plugin-compat.md §5。
|
|
290
|
+
* 2026-09-24:MAX 扩到 `0.1.7-rc.1` —— 同一份 G1–G7 清单重跑通过。0.1.7 只改前端 dist 形态
|
|
291
|
+
* (`<base href="./">` + 相对 `plugins/…`),认证/RPC 未变;网关是同源透明代理,相对引用由浏览器
|
|
292
|
+
* 解析成绝对路径后原样转发,故不受影响,见 doc/review/20260924-dsh-0.1.7-rc.1-compat.md。
|
|
290
293
|
*/
|
|
291
294
|
export const DSH_COMPAT_MIN = "0.1.1-rc.2";
|
|
292
|
-
export const DSH_COMPAT_MAX = "0.1.
|
|
295
|
+
export const DSH_COMPAT_MAX = "0.1.7-rc.1";
|
|
293
296
|
/**
|
|
294
297
|
* 版本落在实测窗口外时的提示文案(零文档导向:直接给动作指令,用户不查表)。
|
|
295
298
|
* - 比 max 新 → 升级 remote-dsh 或暂用旧 dsh;
|