@angelky7/dsh-browser-control 1.7.7 → 1.7.9
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.en.md +3 -3
- package/README.md +8 -6
- package/lib/index.js +284 -77
- package/lib/types/audit.d.ts +3 -0
- package/lib/types/audit.js +55 -5
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/errors.js +13 -4
- package/lib/types/index.js +156 -29
- package/lib/types/policy.d.ts +5 -0
- package/lib/types/policy.js +27 -3
- package/lib/types/server.d.ts +12 -1
- package/lib/types/server.js +49 -16
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -136,7 +136,7 @@ const HINTS = {
|
|
|
136
136
|
extension_offline: "浏览器扩展没连上:让用户打开装了 DSH Browser Control 的那个浏览器(扩展图标应为绿色),确认端口与 Token 两边一致后再试。",
|
|
137
137
|
command_timeout: "扩展超时未回应:先别重试。用 browser_find / browser_snapshot 看页面当前状态,判断动作是否其实已经生效。",
|
|
138
138
|
link_lost: "命令在途时链路断了:动作可能已经落到页面上。先用只读工具确认页面状态,再决定是否重做。",
|
|
139
|
-
cancelled: "
|
|
139
|
+
cancelled: "调用被取消:effectState=not_started 时命令确定没发出去,可以按需重新发起;unknown 时动作可能已经执行,先确认页面状态再决定,不要直接重做。",
|
|
140
140
|
not_configured: "插件还没读到配置:确认 dsh 已加载本插件(dsh plugin list)后再试。",
|
|
141
141
|
held_by_user: "这是用户在页面上的接管浮层里按下的「中止操作」,不是故障:命令一条都没有发出。先与用户确认;他同意继续之后调用 browser_hold action=off 解除,然后再继续原来的步骤。不要重试,也不要换成别的工具绕过它。",
|
|
142
142
|
blocked_by_policy: "这是本机策略拦下的,不是故障:命令没有发给浏览器(零副作用)。要放开改 profile 的 cordis.patch.yml(browser-bridge 的 readOnly / allowScript)并重启 dsh,或换成本机允许的只读工具;不要原地重试。",
|
|
@@ -190,14 +190,17 @@ var BridgeError = class extends Error {
|
|
|
190
190
|
/**
|
|
191
191
|
* 把任意底层抛出的错误归类成 BridgeError。已经分类过的原样返回,
|
|
192
192
|
* 保证多次包裹不会把 code 洗掉。
|
|
193
|
+
* @param params - 这次调用的参数。只有 `wait` 需要它:`fn` 是在页面里执行任意 JS,
|
|
194
|
+
* 超时/掉线时与写命令同档报 `unknown`(谓词跑过就不能断言页面没被动过);
|
|
195
|
+
* 不带 `fn` 的 wait 仍是只读。
|
|
193
196
|
*/
|
|
194
|
-
function classify(command, error) {
|
|
197
|
+
function classify(command, error, params) {
|
|
195
198
|
if (error instanceof BridgeError) return error;
|
|
196
199
|
const raw = error instanceof Error ? error.message : String(error);
|
|
197
200
|
const locked = raw.startsWith("browser-locked-by-user:") ? "locked_by_user" : raw.startsWith("browser-lock-unreadable:") ? "lock_unreadable" : raw.startsWith("browser-hold-aborted:") ? "held_by_user" : void 0;
|
|
198
201
|
const markerLength = locked === "locked_by_user" ? 23 : locked === "lock_unreadable" ? 24 : locked === "held_by_user" ? 21 : 0;
|
|
199
202
|
const message = locked ? raw.slice(markerLength).trim() : raw;
|
|
200
|
-
const unknownIsPossible = isWriteCommand(command);
|
|
203
|
+
const unknownIsPossible = isWriteCommand(command) || command === "wait" && typeof params?.fn === "string" && params.fn.trim().length > 0;
|
|
201
204
|
let code;
|
|
202
205
|
if (locked !== void 0) code = locked;
|
|
203
206
|
else if (/no browser extension connected/.test(message)) code = "extension_offline";
|
|
@@ -207,7 +210,8 @@ function classify(command, error) {
|
|
|
207
210
|
else if (/尚未加载配置|not configured yet/.test(message)) code = "not_configured";
|
|
208
211
|
else if (/未启用|not enabled|桥接启动失败|listen EADDR|settings integration/.test(message)) code = "bridge_disabled";
|
|
209
212
|
else code = "command_failed";
|
|
210
|
-
|
|
213
|
+
const effectState = code === "command_timeout" || code === "link_lost" ? unknownIsPossible ? "unknown" : "not_started" : code === "command_failed" ? unknownIsPossible ? "unknown" : "not_started" : code === "cancelled" ? /cancelled before send/.test(message) ? "not_started" : unknownIsPossible ? "unknown" : "not_started" : "not_started";
|
|
214
|
+
return new BridgeError(command, code, effectState, message);
|
|
211
215
|
}
|
|
212
216
|
//#endregion
|
|
213
217
|
//#region lib/types/server.js
|
|
@@ -286,7 +290,12 @@ var BridgeServer = class {
|
|
|
286
290
|
connectedAt;
|
|
287
291
|
pending = /* @__PURE__ */ new Map();
|
|
288
292
|
lastError;
|
|
289
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Payloads of the message being reassembled. **Buffers, not strings**: decoding
|
|
295
|
+
* each fragment with `toString('utf8')` corrupts any multi-byte character that
|
|
296
|
+
* straddles a fragment boundary. Decode once, at FIN.
|
|
297
|
+
*/
|
|
298
|
+
continuationChunks = [];
|
|
290
299
|
continuationOpen = false;
|
|
291
300
|
constructor(options) {
|
|
292
301
|
this.options = options;
|
|
@@ -391,7 +400,7 @@ var BridgeServer = class {
|
|
|
391
400
|
if (signal !== void 0) {
|
|
392
401
|
entry.onAbort = () => {
|
|
393
402
|
if (!this.pending.delete(id)) return;
|
|
394
|
-
entry.reject(/* @__PURE__ */ new Error(`browser command cancelled: ${command}`));
|
|
403
|
+
entry.reject(/* @__PURE__ */ new Error(`browser command cancelled after send: ${command}`));
|
|
395
404
|
};
|
|
396
405
|
signal.addEventListener("abort", entry.onAbort, { once: true });
|
|
397
406
|
}
|
|
@@ -406,7 +415,7 @@ var BridgeServer = class {
|
|
|
406
415
|
try {
|
|
407
416
|
return await result;
|
|
408
417
|
} catch (error) {
|
|
409
|
-
throw classify(command, error);
|
|
418
|
+
throw classify(command, error, params);
|
|
410
419
|
} finally {
|
|
411
420
|
clearTimeout(entry.timer);
|
|
412
421
|
if (entry.onAbort !== void 0 && signal !== void 0) signal.removeEventListener("abort", entry.onAbort);
|
|
@@ -544,20 +553,29 @@ var BridgeServer = class {
|
|
|
544
553
|
});
|
|
545
554
|
return;
|
|
546
555
|
}
|
|
547
|
-
if (typeof parsed
|
|
556
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
557
|
+
this.respondJson(res, 400, {
|
|
558
|
+
ok: false,
|
|
559
|
+
error: "body must be {\"command\": \"...\", \"params\": {...}}"
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const body = parsed;
|
|
564
|
+
if (typeof body.command !== "string" || body.command.length === 0) {
|
|
548
565
|
this.respondJson(res, 400, {
|
|
549
566
|
ok: false,
|
|
550
567
|
error: "body must be {\"command\": \"...\", \"params\": {...}}"
|
|
551
568
|
});
|
|
552
569
|
return;
|
|
553
570
|
}
|
|
554
|
-
const timeoutMs = typeof
|
|
555
|
-
|
|
571
|
+
const timeoutMs = typeof body.timeoutMs === "number" ? body.timeoutMs : void 0;
|
|
572
|
+
const options = timeoutMs === void 0 ? {} : { timeoutMs };
|
|
573
|
+
(this.options.httpCommand ?? ((command, params, opts) => this.execute(command, params, opts)))(body.command, body.params ?? {}, options).then((result) => this.respondJson(res, 200, {
|
|
556
574
|
ok: true,
|
|
557
575
|
result
|
|
558
576
|
}), (error) => {
|
|
559
577
|
if (error instanceof BridgeError) {
|
|
560
|
-
const status = error.code === "extension_offline" || error.code === "bridge_disabled" || error.code === "not_configured" ? 503 : error.code === "command_timeout" || error.code === "link_lost" ? 504 : 502;
|
|
578
|
+
const status = error.code === "extension_offline" || error.code === "bridge_disabled" || error.code === "not_configured" ? 503 : error.code === "command_timeout" || error.code === "link_lost" ? 504 : error.code === "blocked_by_policy" ? 403 : 502;
|
|
561
579
|
this.respondJson(res, status, error.toJSON());
|
|
562
580
|
return;
|
|
563
581
|
}
|
|
@@ -610,11 +628,15 @@ Sec-WebSocket-Accept: ${accept}\r\n\r
|
|
|
610
628
|
this.clientSocket = socket;
|
|
611
629
|
this.hello = void 0;
|
|
612
630
|
this.connectedAt = /* @__PURE__ */ new Date();
|
|
613
|
-
this.
|
|
631
|
+
this.continuationChunks = [];
|
|
614
632
|
this.continuationOpen = false;
|
|
615
633
|
this.log(`extension connected from ${socket.remoteAddress}:${socket.remotePort}`);
|
|
616
634
|
const reader = new FrameReader();
|
|
617
635
|
socket.on("data", (chunk) => {
|
|
636
|
+
if (this.clientSocket !== socket) {
|
|
637
|
+
socket.destroy();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
618
640
|
reader.push(chunk);
|
|
619
641
|
try {
|
|
620
642
|
reader.drain((frame) => this.handleFrame(frame));
|
|
@@ -637,7 +659,7 @@ Sec-WebSocket-Accept: ${accept}\r\n\r
|
|
|
637
659
|
switch (frame.opcode) {
|
|
638
660
|
case 1:
|
|
639
661
|
case 0:
|
|
640
|
-
this.handleText(frame.payload
|
|
662
|
+
this.handleText(frame.payload, frame.fin, frame.opcode);
|
|
641
663
|
return;
|
|
642
664
|
case 8:
|
|
643
665
|
this.closeClientSocket(1e3, "");
|
|
@@ -648,14 +670,14 @@ Sec-WebSocket-Accept: ${accept}\r\n\r
|
|
|
648
670
|
default: return;
|
|
649
671
|
}
|
|
650
672
|
}
|
|
651
|
-
handleText(
|
|
673
|
+
handleText(payload, fin, opcode) {
|
|
652
674
|
if (opcode === 0 && !this.continuationOpen) return;
|
|
653
|
-
if (opcode === 1) this.
|
|
675
|
+
if (opcode === 1) this.continuationChunks = [];
|
|
654
676
|
this.continuationOpen = opcode === 0 || !fin;
|
|
655
|
-
this.
|
|
677
|
+
this.continuationChunks.push(payload);
|
|
656
678
|
if (!fin) return;
|
|
657
|
-
const full = this.
|
|
658
|
-
this.
|
|
679
|
+
const full = Buffer.concat(this.continuationChunks).toString("utf8");
|
|
680
|
+
this.continuationChunks = [];
|
|
659
681
|
this.continuationOpen = false;
|
|
660
682
|
let msg;
|
|
661
683
|
try {
|
|
@@ -889,9 +911,22 @@ const READ_ONLY_ALLOWED = /* @__PURE__ */ new Set([
|
|
|
889
911
|
/**
|
|
890
912
|
* 会执行**任意页面脚本**的扩展命令。这条与只读档分开管,因为用户可能想「可以读、
|
|
891
913
|
* 也可以点,但别在我页面里跑任意 JS」——`browser_evaluate` 能绕开其他所有工具做的事。
|
|
914
|
+
*
|
|
915
|
+
* **按命令名判不够**:`wait` 的 `fn` 参数同样是在页面里执行任意 JS(扩展侧用
|
|
916
|
+
* `(${fn})()` 求出谓词),只按名字看它会从只读白名单和脚本开关两道门缝里同时漏过去。
|
|
917
|
+
* 所以真正判「是不是脚本」的是下面 `isScriptCall()`:命令名 + 参数一起看,
|
|
918
|
+
* `wait(selector)` / `wait(text)` 仍按只读处理,`wait({fn})` 与 `eval` 同档。
|
|
892
919
|
*/
|
|
893
920
|
const SCRIPT_COMMANDS = /* @__PURE__ */ new Set(["eval"]);
|
|
894
921
|
/**
|
|
922
|
+
* 这次调用会不会在页面里执行任意 JS。判据是「这次实际要做的动作」,不是命令名:
|
|
923
|
+
* `wait` 只有带 `fn` 时才升级为脚本调用(`selector` / `text` 是纯查询)。
|
|
924
|
+
*/
|
|
925
|
+
function isScriptCall(command, params) {
|
|
926
|
+
if (SCRIPT_COMMANDS.has(command)) return true;
|
|
927
|
+
return command === "wait" && typeof params?.fn === "string" && params.fn.trim().length > 0;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
895
930
|
* 不经过扩展、直接在插件进程里改动本机文件的命令名。
|
|
896
931
|
*
|
|
897
932
|
* `browser_cleanup` 走 `BridgeController.cleanup()`,**不经过 `execute()`**,所以它天然
|
|
@@ -922,7 +957,9 @@ const CONFIG_HINT = "开关在 profile 的 cordis.patch.yml 里(browser-bridge
|
|
|
922
957
|
* @returns 拒绝时写给 agent 的整句理由;放行时 `undefined`。
|
|
923
958
|
*/
|
|
924
959
|
function policyRefusal(command, flags, params) {
|
|
925
|
-
|
|
960
|
+
const scriptCall = isScriptCall(command, params);
|
|
961
|
+
if (!flags.allowScript && scriptCall) return `在页面里执行任意脚本已被关闭(allowScript: false):${command === "wait" ? "命令 wait 的 fn 参数(会在页面里执行任意 JS)被拒绝" : `命令 ${command} 被拒绝`},没有向浏览器发出任何操作。要等页面就绪请改用 wait 的 selector / text(只读),读页面内容用 browser_read / browser_snapshot / browser_find;确实需要执行脚本时,${CONFIG_HINT}之后再说。`;
|
|
962
|
+
if (flags.readOnly && command === "wait" && scriptCall) return `只读模式已开启(readOnly: true):命令 wait 的 fn 参数会在页面里执行任意 JS(可能改变页面状态),已被拒绝,没有向浏览器发出任何操作。要等页面就绪请改用 wait 的 selector / text(只读);要放开请由用户关掉 readOnly,${CONFIG_HINT}不要原地重试本命令。`;
|
|
926
963
|
if (flags.readOnly && LOCAL_DESTRUCTIVE_COMMANDS.has(command)) return `只读模式已开启(readOnly: true):命令 ${command} 会删除本机保存的截图与临时文件,删除不可逆,所以已被拒绝,没有任何文件被删除。只读档的含义是「什么都不要改」,清理属于改动;要释放磁盘请由用户自己关掉 readOnly,${CONFIG_HINT}。不要原地重试本命令。`;
|
|
927
964
|
const isReadOnlyAction = command === "hold" && params?.action === "status";
|
|
928
965
|
if (flags.readOnly && PAGE_MUTATING_COMMANDS.has(command) && !isReadOnlyAction) return `只读模式已开启(readOnly: true):命令 ${command} 会往页面里插入一层接管浮层(改页面 DOM),已被拒绝,没有向浏览器发出任何操作。浮层是给用户看的提示层,不是安全边界;只读档里连这种「只是提示」的改动也一并拒绝。要开浮层请由用户关掉 readOnly,${CONFIG_HINT}不要原地重试本命令。`;
|
|
@@ -937,6 +974,46 @@ const AUDIT_EXPRESSION_MAX_CHARS = 120;
|
|
|
937
974
|
/** 参数里这些键的值一律打码(大小写不敏感)。 */
|
|
938
975
|
const SECRET_KEY = /^(token|authorization|password|passwd|secret|apikey|api_key|cookie)$/i;
|
|
939
976
|
/**
|
|
977
|
+
* URL 里的秘密不能靠「截断」处理:`?token=…&code=…` 截到 120 字符照样是明文。
|
|
978
|
+
* 审计的用途是「做了什么」,所以保留 scheme/host/path 与查询键名,**查询值一律打码**;
|
|
979
|
+
* 片段(#)只在看起来像参数(含 `=` 或 token/secret 字样)时整体打码——SPA 路由
|
|
980
|
+
* (`#/inbox`)要留住,OAuth 隐式流的 `#access_token=…` 不能留。
|
|
981
|
+
*/
|
|
982
|
+
const URL_PATTERN = /https?:\/\/[^\s"'<>]+/g;
|
|
983
|
+
function redactUrlMatch(match) {
|
|
984
|
+
const trailing = match.match(/[).,;:!?]+$/);
|
|
985
|
+
const core = trailing === null ? match : match.slice(0, -trailing[0].length);
|
|
986
|
+
const tail = trailing === null ? "" : trailing[0];
|
|
987
|
+
const queryAt = core.indexOf("?");
|
|
988
|
+
const hashAt = core.indexOf("#");
|
|
989
|
+
const base = queryAt >= 0 ? core.slice(0, queryAt) : hashAt >= 0 ? core.slice(0, hashAt) : core;
|
|
990
|
+
const query = queryAt >= 0 ? core.slice(queryAt + 1, hashAt >= 0 ? hashAt : void 0) : "";
|
|
991
|
+
const hash = hashAt >= 0 ? core.slice(hashAt) : "";
|
|
992
|
+
let changed = false;
|
|
993
|
+
const redactedQuery = query.length === 0 ? "" : query.split("&").map((pair) => {
|
|
994
|
+
const eq = pair.indexOf("=");
|
|
995
|
+
if (eq < 0) return pair;
|
|
996
|
+
const key = pair.slice(0, eq);
|
|
997
|
+
let value = pair.slice(eq + 1);
|
|
998
|
+
try {
|
|
999
|
+
value = decodeURIComponent(value);
|
|
1000
|
+
} catch {}
|
|
1001
|
+
changed = true;
|
|
1002
|
+
return `${key}=[redacted:${[...value].length} chars]`;
|
|
1003
|
+
}).join("&");
|
|
1004
|
+
let redactedHash = hash;
|
|
1005
|
+
if (hash.length > 1 && (/=/.test(hash) || /token|secret|access_token/i.test(hash))) {
|
|
1006
|
+
redactedHash = "#[redacted]";
|
|
1007
|
+
changed = true;
|
|
1008
|
+
}
|
|
1009
|
+
if (!changed) return core + tail;
|
|
1010
|
+
return `${base}${redactedQuery.length > 0 ? `?${redactedQuery}` : ""}${redactedHash}${tail}`;
|
|
1011
|
+
}
|
|
1012
|
+
/** 把一段文本里所有 URL 的查询值/敏感片段打码(用于参数摘要与错误文本)。 */
|
|
1013
|
+
function redactAuditText(text) {
|
|
1014
|
+
return text.replace(URL_PATTERN, redactUrlMatch);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
940
1017
|
* 哪个命令的 `value` 字段是敏感内容:`input` 是往页面里打的字(可能是密码),
|
|
941
1018
|
* `cookies.set` 直接写凭据。其余命令的 value 字段(如果有)不动。
|
|
942
1019
|
*/
|
|
@@ -954,7 +1031,11 @@ function summarizeAuditParams(command, params) {
|
|
|
954
1031
|
text = JSON.stringify(params, (key, value) => {
|
|
955
1032
|
if (SECRET_KEY.test(key)) return "[redacted]";
|
|
956
1033
|
if (key === "value" && VALUE_REDACTED_COMMANDS.has(command)) return redactValue(value);
|
|
957
|
-
if (
|
|
1034
|
+
if (typeof value === "string") {
|
|
1035
|
+
const redacted = redactAuditText(value);
|
|
1036
|
+
if (key === "expression" && [...redacted].length > AUDIT_EXPRESSION_MAX_CHARS) return `${[...redacted].slice(0, AUDIT_EXPRESSION_MAX_CHARS).join("")}…[+${[...redacted].length - AUDIT_EXPRESSION_MAX_CHARS} chars]`;
|
|
1037
|
+
return redacted;
|
|
1038
|
+
}
|
|
958
1039
|
return value;
|
|
959
1040
|
}) ?? "{}";
|
|
960
1041
|
} catch {
|
|
@@ -986,7 +1067,10 @@ var AuditLog = class {
|
|
|
986
1067
|
if (input.tabId !== void 0) entry.tabId = input.tabId;
|
|
987
1068
|
if (input.code !== void 0) entry.code = input.code;
|
|
988
1069
|
if (input.effectState !== void 0) entry.effectState = input.effectState;
|
|
989
|
-
if (input.error !== void 0)
|
|
1070
|
+
if (input.error !== void 0) {
|
|
1071
|
+
const redacted = redactAuditText(input.error);
|
|
1072
|
+
entry.error = [...redacted].length > AUDIT_ERROR_MAX_CHARS ? `${[...redacted].slice(0, AUDIT_ERROR_MAX_CHARS).join("")}…` : redacted;
|
|
1073
|
+
}
|
|
990
1074
|
this.entries.push(entry);
|
|
991
1075
|
if (this.entries.length > 200) this.entries.splice(0, this.entries.length - 200);
|
|
992
1076
|
return entry;
|
|
@@ -1373,6 +1457,7 @@ var BridgeController = class {
|
|
|
1373
1457
|
current;
|
|
1374
1458
|
journal;
|
|
1375
1459
|
journalKey = "";
|
|
1460
|
+
journalError;
|
|
1376
1461
|
/** 操作审计账本(内存环形缓冲,200 条;命令层记录,见 audit.ts 顶部注释)。 */
|
|
1377
1462
|
audit = new AuditLog();
|
|
1378
1463
|
/** 多 session 隔离:每个 dsh 会话的工作标签页(见 sessions.ts)。 */
|
|
@@ -1389,11 +1474,24 @@ var BridgeController = class {
|
|
|
1389
1474
|
return this.current === void 0 ? void 0 : path.resolve(this.current.stateDir);
|
|
1390
1475
|
}
|
|
1391
1476
|
/**
|
|
1477
|
+
* Whether THIS run is persisting the journal, and why not. `browser_audit` /
|
|
1478
|
+
* `browser_journal` surface this: a memory-only run used to be visible only in
|
|
1479
|
+
* one log line, so a missing journal looked like "nothing happened".
|
|
1480
|
+
*/
|
|
1481
|
+
get journalStatus() {
|
|
1482
|
+
if (this.journal !== void 0) return { enabled: true };
|
|
1483
|
+
return this.journalError === void 0 ? { enabled: false } : {
|
|
1484
|
+
enabled: false,
|
|
1485
|
+
error: this.journalError
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1392
1489
|
* Record one audit entry in memory and, when the journal is open, on disk.
|
|
1393
1490
|
* Journal appends are queued and never block the command path.
|
|
1394
1491
|
*/
|
|
1395
1492
|
recordAudit(input) {
|
|
1396
|
-
this.
|
|
1493
|
+
const entry = this.audit.record(input);
|
|
1494
|
+
this.journal?.append(entry);
|
|
1397
1495
|
}
|
|
1398
1496
|
/**
|
|
1399
1497
|
* Current policy flags. Tools that compose several commands (browser_drag
|
|
@@ -1470,8 +1568,10 @@ var BridgeController = class {
|
|
|
1470
1568
|
await previousJournal?.close();
|
|
1471
1569
|
try {
|
|
1472
1570
|
this.journal = await Journal.open(stateDir, this.log);
|
|
1571
|
+
this.journalError = void 0;
|
|
1473
1572
|
} catch (error) {
|
|
1474
|
-
this.
|
|
1573
|
+
this.journalError = errorMessage(error);
|
|
1574
|
+
this.log(`journal: disabled — ${this.journalError}`);
|
|
1475
1575
|
}
|
|
1476
1576
|
}
|
|
1477
1577
|
const key = config.enabled ? `${config.port}|${config.token}|${shotsDir}` : "";
|
|
@@ -1488,7 +1588,8 @@ var BridgeController = class {
|
|
|
1488
1588
|
port: config.port,
|
|
1489
1589
|
token: config.token,
|
|
1490
1590
|
shotsDir,
|
|
1491
|
-
log: this.log
|
|
1591
|
+
log: this.log,
|
|
1592
|
+
httpCommand: (command, params, options) => this.execute(command, params, new AbortController().signal, options.timeoutMs)
|
|
1492
1593
|
});
|
|
1493
1594
|
try {
|
|
1494
1595
|
await server.start();
|
|
@@ -1560,18 +1661,35 @@ var BridgeController = class {
|
|
|
1560
1661
|
* to work; when it is gone we forget it and let the browser's active tab
|
|
1561
1662
|
* answer, exactly like before this feature existed.
|
|
1562
1663
|
*/
|
|
1563
|
-
|
|
1564
|
-
|
|
1664
|
+
/**
|
|
1665
|
+
* The calling session's working tab, after the same liveness probe isolateParams
|
|
1666
|
+
* uses. Tools that compose several commands (browser_drag) need the id *before*
|
|
1667
|
+
* issuing anything, so they must not fall back to the browser's active tab on
|
|
1668
|
+
* their own — that would silently bypass session isolation.
|
|
1669
|
+
*/
|
|
1670
|
+
async sessionTab(signal) {
|
|
1671
|
+
const sessionId = TOOL_SESSION.getStore();
|
|
1672
|
+
if (sessionId === void 0) return void 0;
|
|
1673
|
+
return await this.liveRecordedTab(sessionId, signal);
|
|
1674
|
+
}
|
|
1675
|
+
/** Recorded working tab if it is still open; a gone tab is forgotten here. */
|
|
1676
|
+
async liveRecordedTab(sessionId, signal) {
|
|
1565
1677
|
const recorded = this.sessions.workingTab(sessionId);
|
|
1566
|
-
if (recorded === void 0) return
|
|
1678
|
+
if (recorded === void 0) return void 0;
|
|
1567
1679
|
const server = this.server;
|
|
1568
1680
|
if (server !== void 0) try {
|
|
1569
1681
|
const listing = await server.execute("tabs.list", {}, { signal });
|
|
1570
1682
|
if (!(Array.isArray(listing?.tabs) && listing.tabs.some((tab) => tab?.id === recorded))) {
|
|
1571
1683
|
this.sessions.release(sessionId);
|
|
1572
|
-
return
|
|
1684
|
+
return;
|
|
1573
1685
|
}
|
|
1574
1686
|
} catch {}
|
|
1687
|
+
return recorded;
|
|
1688
|
+
}
|
|
1689
|
+
async isolateParams(command, params, sessionId, signal) {
|
|
1690
|
+
if (typeof params.tabId === "number" || NO_TAB_DEFAULT_COMMANDS.has(command)) return params;
|
|
1691
|
+
const recorded = await this.liveRecordedTab(sessionId, signal);
|
|
1692
|
+
if (recorded === void 0) return params;
|
|
1575
1693
|
return {
|
|
1576
1694
|
...params,
|
|
1577
1695
|
tabId: recorded
|
|
@@ -3667,16 +3785,26 @@ async function resolveDragPoint(ctrl, tabId, target, signal) {
|
|
|
3667
3785
|
}, signal))?.nodeId ?? 0;
|
|
3668
3786
|
if (nodeId === 0) throw new Error(`no element matches selector ${JSON.stringify(selector)} (top document only)`);
|
|
3669
3787
|
await cdpCall(ctrl, tabId, "DOM.scrollIntoViewIfNeeded", { nodeId }, signal);
|
|
3788
|
+
return await measureDragNode(ctrl, tabId, nodeId, selector, signal);
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Read a node's **current** viewport geometry without scrolling. Needed after the
|
|
3792
|
+
* *other* endpoint's scrollIntoViewIfNeeded: that scroll can move the first
|
|
3793
|
+
* endpoint (and this one), so coordinates measured before it are stale. DOM
|
|
3794
|
+
* geometry is viewport-relative; dispatching the old numbers would drag from
|
|
3795
|
+
* wherever the element used to be.
|
|
3796
|
+
*/
|
|
3797
|
+
async function measureDragNode(ctrl, tabId, nodeId, label, signal) {
|
|
3670
3798
|
let box;
|
|
3671
3799
|
try {
|
|
3672
3800
|
box = await cdpCall(ctrl, tabId, "DOM.getBoxModel", { nodeId }, signal);
|
|
3673
3801
|
} catch (error) {
|
|
3674
|
-
throw new Error(`cannot measure ${JSON.stringify(
|
|
3802
|
+
throw new Error(`cannot measure ${JSON.stringify(label)} — hidden, zero-size or detached? (${error instanceof Error ? error.message : String(error)})`);
|
|
3675
3803
|
}
|
|
3676
3804
|
const q = box?.model?.content;
|
|
3677
|
-
if (!Array.isArray(q) || q.length < 8) throw new Error(`DOM.getBoxModel returned no quad for ${JSON.stringify(
|
|
3805
|
+
if (!Array.isArray(q) || q.length < 8) throw new Error(`DOM.getBoxModel returned no quad for ${JSON.stringify(label)}`);
|
|
3678
3806
|
return {
|
|
3679
|
-
selector,
|
|
3807
|
+
selector: label,
|
|
3680
3808
|
x: (q[0] + q[2] + q[4] + q[6]) / 4,
|
|
3681
3809
|
y: (q[1] + q[3] + q[5] + q[7]) / 4,
|
|
3682
3810
|
width: typeof box?.model?.width === "number" ? box.model.width : Math.hypot(q[2] - q[0], q[3] - q[1]),
|
|
@@ -3684,6 +3812,19 @@ async function resolveDragPoint(ctrl, tabId, target, signal) {
|
|
|
3684
3812
|
nodeId
|
|
3685
3813
|
};
|
|
3686
3814
|
}
|
|
3815
|
+
/** CSS viewport size via Page.getLayoutMetrics (no page JS, works with allowScript off). */
|
|
3816
|
+
async function viewportSize(ctrl, tabId, signal) {
|
|
3817
|
+
try {
|
|
3818
|
+
const metrics = await cdpCall(ctrl, tabId, "Page.getLayoutMetrics", {}, signal);
|
|
3819
|
+
const viewport = metrics?.cssVisualViewport ?? metrics?.cssLayoutViewport;
|
|
3820
|
+
const width = Number(viewport?.clientWidth);
|
|
3821
|
+
const height = Number(viewport?.clientHeight);
|
|
3822
|
+
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) return {
|
|
3823
|
+
width,
|
|
3824
|
+
height
|
|
3825
|
+
};
|
|
3826
|
+
} catch {}
|
|
3827
|
+
}
|
|
3687
3828
|
/** Whether the source declares the native HTML5 `draggable` attribute. */
|
|
3688
3829
|
async function sourceIsDraggable(ctrl, tabId, nodeId, signal) {
|
|
3689
3830
|
const attrs = await cdpCall(ctrl, tabId, "DOM.getAttributes", { nodeId }, signal);
|
|
@@ -3859,6 +4000,7 @@ function registerDragTool(ctx, controller) {
|
|
|
3859
4000
|
controller.requireExtensionCommand("cdp", "1.7.2");
|
|
3860
4001
|
if (controller.policy.readOnly) throw new Error("browser_drag changes page state and read-only mode is on — turn readOnly off in the browser-bridge config to drag");
|
|
3861
4002
|
let tabId = args.tabId;
|
|
4003
|
+
if (tabId === void 0) tabId = await controller.sessionTab(exec.signal);
|
|
3862
4004
|
if (tabId === void 0) {
|
|
3863
4005
|
const listing = await controller.execute("tabs.list", {}, exec.signal);
|
|
3864
4006
|
if (typeof listing?.activeTabId !== "number") throw new Error("could not resolve the active tab — pass tabId explicitly");
|
|
@@ -3871,56 +4013,92 @@ function registerDragTool(ctx, controller) {
|
|
|
3871
4013
|
if (mode === "auto" && source?.nodeId !== void 0 && await sourceIsDraggable(controller, tabId, source.nodeId, exec.signal)) resolvedMode = "html5";
|
|
3872
4014
|
if (resolvedMode === "mouse") {
|
|
3873
4015
|
const target = await resolveDragPoint(controller, tabId, to, exec.signal);
|
|
4016
|
+
let sourcePoint = source;
|
|
4017
|
+
let targetPoint = target;
|
|
4018
|
+
if (source?.nodeId !== void 0 && target.nodeId !== void 0) {
|
|
4019
|
+
sourcePoint = await measureDragNode(controller, tabId, source.nodeId, String(from.selector), exec.signal);
|
|
4020
|
+
targetPoint = await measureDragNode(controller, tabId, target.nodeId, String(to.selector), exec.signal);
|
|
4021
|
+
}
|
|
4022
|
+
const viewport = await viewportSize(controller, tabId, exec.signal);
|
|
4023
|
+
if (viewport !== void 0) {
|
|
4024
|
+
const describe = (p) => `${p.selector !== void 0 ? JSON.stringify(p.selector) : "point"} at (${Math.round(p.x)},${Math.round(p.y)})`;
|
|
4025
|
+
const outside = (p) => p.x < 0 || p.y < 0 || p.x > viewport.width || p.y > viewport.height;
|
|
4026
|
+
if (outside(sourcePoint) || outside(targetPoint)) throw new Error(`drag endpoints are not both visible in the viewport (${describe(sourcePoint)} → ${describe(targetPoint)}, viewport ${Math.round(viewport.width)}×${Math.round(viewport.height)}) — scroll so both fit, then retry; mode:"html5" takes selectors and needs no viewport coordinates`);
|
|
4027
|
+
}
|
|
3874
4028
|
const steps = Math.min(50, Math.max(1, Math.trunc(typeof args.steps === "number" ? args.steps : 12)));
|
|
3875
|
-
const sx =
|
|
4029
|
+
const sx = sourcePoint.x, sy = sourcePoint.y, tx = targetPoint.x, ty = targetPoint.y;
|
|
3876
4030
|
const dispatch = async (params) => {
|
|
3877
4031
|
await cdpCall(controller, tabId, "Input.dispatchMouseEvent", params, exec.signal);
|
|
3878
4032
|
};
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
x: sx,
|
|
3882
|
-
y: sy,
|
|
3883
|
-
button: "none",
|
|
3884
|
-
buttons: 0
|
|
3885
|
-
});
|
|
3886
|
-
await dispatch({
|
|
3887
|
-
type: "mousePressed",
|
|
3888
|
-
x: sx,
|
|
3889
|
-
y: sy,
|
|
3890
|
-
button: "left",
|
|
3891
|
-
buttons: 1,
|
|
3892
|
-
clickCount: 1
|
|
3893
|
-
});
|
|
3894
|
-
for (let i = 1; i <= steps; i++) {
|
|
3895
|
-
const f = i / steps;
|
|
4033
|
+
let pressedAt;
|
|
4034
|
+
try {
|
|
3896
4035
|
await dispatch({
|
|
3897
4036
|
type: "mouseMoved",
|
|
3898
|
-
x: sx
|
|
3899
|
-
y: sy
|
|
4037
|
+
x: sx,
|
|
4038
|
+
y: sy,
|
|
4039
|
+
button: "none",
|
|
4040
|
+
buttons: 0
|
|
4041
|
+
});
|
|
4042
|
+
await dispatch({
|
|
4043
|
+
type: "mousePressed",
|
|
4044
|
+
x: sx,
|
|
4045
|
+
y: sy,
|
|
3900
4046
|
button: "left",
|
|
3901
|
-
buttons: 1
|
|
4047
|
+
buttons: 1,
|
|
4048
|
+
clickCount: 1
|
|
4049
|
+
});
|
|
4050
|
+
pressedAt = {
|
|
4051
|
+
x: sx,
|
|
4052
|
+
y: sy
|
|
4053
|
+
};
|
|
4054
|
+
for (let i = 1; i <= steps; i++) {
|
|
4055
|
+
const f = i / steps;
|
|
4056
|
+
await dispatch({
|
|
4057
|
+
type: "mouseMoved",
|
|
4058
|
+
x: sx + (tx - sx) * f,
|
|
4059
|
+
y: sy + (ty - sy) * f,
|
|
4060
|
+
button: "left",
|
|
4061
|
+
buttons: 1
|
|
4062
|
+
});
|
|
4063
|
+
pressedAt = {
|
|
4064
|
+
x: sx + (tx - sx) * f,
|
|
4065
|
+
y: sy + (ty - sy) * f
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
await dispatch({
|
|
4069
|
+
type: "mouseReleased",
|
|
4070
|
+
x: tx,
|
|
4071
|
+
y: ty,
|
|
4072
|
+
button: "left",
|
|
4073
|
+
buttons: 0,
|
|
4074
|
+
clickCount: 1
|
|
4075
|
+
});
|
|
4076
|
+
pressedAt = void 0;
|
|
4077
|
+
await dispatch({
|
|
4078
|
+
type: "mouseMoved",
|
|
4079
|
+
x: tx,
|
|
4080
|
+
y: ty,
|
|
4081
|
+
button: "none",
|
|
4082
|
+
buttons: 0
|
|
3902
4083
|
});
|
|
4084
|
+
} catch (error) {
|
|
4085
|
+
if (pressedAt !== void 0) try {
|
|
4086
|
+
await cdpCall(controller, tabId, "Input.dispatchMouseEvent", {
|
|
4087
|
+
type: "mouseReleased",
|
|
4088
|
+
x: pressedAt.x,
|
|
4089
|
+
y: pressedAt.y,
|
|
4090
|
+
button: "left",
|
|
4091
|
+
buttons: 0,
|
|
4092
|
+
clickCount: 1
|
|
4093
|
+
}, new AbortController().signal);
|
|
4094
|
+
} catch {}
|
|
4095
|
+
throw error;
|
|
3903
4096
|
}
|
|
3904
|
-
await dispatch({
|
|
3905
|
-
type: "mouseReleased",
|
|
3906
|
-
x: tx,
|
|
3907
|
-
y: ty,
|
|
3908
|
-
button: "left",
|
|
3909
|
-
buttons: 0,
|
|
3910
|
-
clickCount: 1
|
|
3911
|
-
});
|
|
3912
|
-
await dispatch({
|
|
3913
|
-
type: "mouseMoved",
|
|
3914
|
-
x: tx,
|
|
3915
|
-
y: ty,
|
|
3916
|
-
button: "none",
|
|
3917
|
-
buttons: 0
|
|
3918
|
-
});
|
|
3919
4097
|
return {
|
|
3920
4098
|
tabId,
|
|
3921
4099
|
mode: "mouse",
|
|
3922
|
-
from: roundDragPoint(
|
|
3923
|
-
to: roundDragPoint(
|
|
4100
|
+
from: roundDragPoint(sourcePoint),
|
|
4101
|
+
to: roundDragPoint(targetPoint),
|
|
3924
4102
|
steps,
|
|
3925
4103
|
dispatched: steps + 4,
|
|
3926
4104
|
elapsedMs: Date.now() - started
|
|
@@ -3954,10 +4132,13 @@ function registerDragTool(ctx, controller) {
|
|
|
3954
4132
|
}
|
|
3955
4133
|
}));
|
|
3956
4134
|
}
|
|
4135
|
+
function journalWarning(status) {
|
|
4136
|
+
return status.enabled ? "" : `\nJournal write: disabled${status.error === void 0 ? "" : ` — ${status.error}`} — this run is memory-only; entries are not being written to disk.`;
|
|
4137
|
+
}
|
|
3957
4138
|
function registerAuditTool(ctx, controller) {
|
|
3958
4139
|
ctx.tools.register(defineTool$1({
|
|
3959
4140
|
name: "browser_audit",
|
|
3960
|
-
description: "Read this plugin session's in-memory audit trail: every command sent to the browser extension (and every policy/gate refusal), with redacted params, outcome code and duration. Newest last; capped at 200 entries and wiped when dsh restarts. Values typed into pages and cookie values are redacted; eval expressions are truncated. Plugin-local read — works in read-only mode.",
|
|
4141
|
+
description: "Read this plugin session's in-memory audit trail: every command sent to the browser extension (and every policy/gate refusal), with redacted params, outcome code and duration. Newest last; capped at 200 entries and wiped when dsh restarts. Values typed into pages and cookie values are redacted; URL query values and URLs inside error text are redacted; eval expressions are truncated. The `journal` field says whether this run is also being written to disk (enabled:false + error = memory-only). Plugin-local read — works in read-only mode.",
|
|
3961
4142
|
parameters: {
|
|
3962
4143
|
command: {
|
|
3963
4144
|
type: "string",
|
|
@@ -3989,6 +4170,17 @@ function registerAuditTool(ctx, controller) {
|
|
|
3989
4170
|
type: "number",
|
|
3990
4171
|
required: true
|
|
3991
4172
|
},
|
|
4173
|
+
journal: {
|
|
4174
|
+
type: "object",
|
|
4175
|
+
additionalProperties: false,
|
|
4176
|
+
properties: {
|
|
4177
|
+
enabled: {
|
|
4178
|
+
type: "boolean",
|
|
4179
|
+
required: true
|
|
4180
|
+
},
|
|
4181
|
+
error: { type: "string" }
|
|
4182
|
+
}
|
|
4183
|
+
},
|
|
3992
4184
|
entries: {
|
|
3993
4185
|
type: "array",
|
|
3994
4186
|
required: true,
|
|
@@ -4031,9 +4223,10 @@ function registerAuditTool(ctx, controller) {
|
|
|
4031
4223
|
},
|
|
4032
4224
|
render: (_args, value) => {
|
|
4033
4225
|
const v = value;
|
|
4226
|
+
const warn = v.journal === void 0 ? "" : journalWarning(v.journal);
|
|
4034
4227
|
if (v.entries.length === 0) return [{
|
|
4035
4228
|
type: "text",
|
|
4036
|
-
text: `Audit: ${v.total} entries, none match — the trail starts when a command is executed
|
|
4229
|
+
text: `Audit: ${v.total} entries, none match — the trail starts when a command is executed.${warn}`
|
|
4037
4230
|
}];
|
|
4038
4231
|
const lines = v.entries.map((e) => {
|
|
4039
4232
|
const tab = e.tabId === void 0 ? "" : ` tab ${e.tabId}`;
|
|
@@ -4043,7 +4236,7 @@ function registerAuditTool(ctx, controller) {
|
|
|
4043
4236
|
});
|
|
4044
4237
|
return [{
|
|
4045
4238
|
type: "text",
|
|
4046
|
-
text: clampRendered(`Audit: ${v.total} entries (showing ${v.shown})\n${lines.join("\n")}`)
|
|
4239
|
+
text: clampRendered(`Audit: ${v.total} entries (showing ${v.shown})\n${lines.join("\n")}${warn}`)
|
|
4047
4240
|
}];
|
|
4048
4241
|
}
|
|
4049
4242
|
},
|
|
@@ -4062,7 +4255,8 @@ function registerAuditTool(ctx, controller) {
|
|
|
4062
4255
|
return {
|
|
4063
4256
|
total: controller.audit.size(),
|
|
4064
4257
|
shown: entries.length,
|
|
4065
|
-
entries
|
|
4258
|
+
entries,
|
|
4259
|
+
journal: controller.journalStatus
|
|
4066
4260
|
};
|
|
4067
4261
|
}
|
|
4068
4262
|
}));
|
|
@@ -4070,7 +4264,7 @@ function registerAuditTool(ctx, controller) {
|
|
|
4070
4264
|
function registerJournalTool(ctx, controller) {
|
|
4071
4265
|
ctx.tools.register(defineTool$1({
|
|
4072
4266
|
name: "browser_journal",
|
|
4073
|
-
description: "Read the persistent command journal across dsh restarts: every command from previous runs of this plugin (written to <stateDir>/journal.jsonl, last 1000 entries), grouped by run. Same redaction as browser_audit. Plugin-local disk read — works with the extension offline and in read-only mode, so it is the way to recover \"what was I doing\" after a restart.",
|
|
4267
|
+
description: "Read the persistent command journal across dsh restarts: every command from previous runs of this plugin (written to <stateDir>/journal.jsonl, last 1000 entries), grouped by run. Same redaction as browser_audit. The `journal` field says whether the CURRENT run is writing (enabled:false + error = this run is memory-only; what you see is from earlier runs). Plugin-local disk read — works with the extension offline and in read-only mode, so it is the way to recover \"what was I doing\" after a restart.",
|
|
4074
4268
|
parameters: {
|
|
4075
4269
|
command: {
|
|
4076
4270
|
type: "string",
|
|
@@ -4102,6 +4296,17 @@ function registerJournalTool(ctx, controller) {
|
|
|
4102
4296
|
type: "number",
|
|
4103
4297
|
required: true
|
|
4104
4298
|
},
|
|
4299
|
+
journal: {
|
|
4300
|
+
type: "object",
|
|
4301
|
+
additionalProperties: false,
|
|
4302
|
+
properties: {
|
|
4303
|
+
enabled: {
|
|
4304
|
+
type: "boolean",
|
|
4305
|
+
required: true
|
|
4306
|
+
},
|
|
4307
|
+
error: { type: "string" }
|
|
4308
|
+
}
|
|
4309
|
+
},
|
|
4105
4310
|
entries: {
|
|
4106
4311
|
type: "array",
|
|
4107
4312
|
required: true,
|
|
@@ -4148,9 +4353,10 @@ function registerJournalTool(ctx, controller) {
|
|
|
4148
4353
|
},
|
|
4149
4354
|
render: (_args, value) => {
|
|
4150
4355
|
const v = value;
|
|
4356
|
+
const warn = v.journal === void 0 ? "" : journalWarning(v.journal);
|
|
4151
4357
|
if (v.entries.length === 0) return [{
|
|
4152
4358
|
type: "text",
|
|
4153
|
-
text: `Journal: ${v.total} entries on disk, none match — nothing recorded across runs yet
|
|
4359
|
+
text: `Journal: ${v.total} entries on disk, none match — nothing recorded across runs yet.${warn}`
|
|
4154
4360
|
}];
|
|
4155
4361
|
const lines = [];
|
|
4156
4362
|
let run = "";
|
|
@@ -4165,7 +4371,7 @@ function registerJournalTool(ctx, controller) {
|
|
|
4165
4371
|
}
|
|
4166
4372
|
return [{
|
|
4167
4373
|
type: "text",
|
|
4168
|
-
text: clampRendered(`Journal: ${v.total} entries in ${v.runs} run(s) (showing ${v.shown})\n${lines.join("\n")}`)
|
|
4374
|
+
text: clampRendered(`Journal: ${v.total} entries in ${v.runs} run(s) (showing ${v.shown})\n${lines.join("\n")}${warn}`)
|
|
4169
4375
|
}];
|
|
4170
4376
|
}
|
|
4171
4377
|
},
|
|
@@ -4186,7 +4392,8 @@ function registerJournalTool(ctx, controller) {
|
|
|
4186
4392
|
total,
|
|
4187
4393
|
shown: entries.length,
|
|
4188
4394
|
runs: new Set(entries.map((e) => e.run)).size,
|
|
4189
|
-
entries
|
|
4395
|
+
entries,
|
|
4396
|
+
journal: controller.journalStatus
|
|
4190
4397
|
};
|
|
4191
4398
|
}
|
|
4192
4399
|
}));
|
package/lib/types/audit.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - **只存内存**(环形缓冲,200 条):跨 dsh 重启的持久化是「会话恢复」那一版的事;
|
|
10
10
|
* 这一版先把账记对,别把磁盘格式写死。
|
|
11
11
|
* - **参数摘要是脱敏后的**:输入框的值、cookie 的值、令牌类字段一律打码;
|
|
12
|
+
* URL 的查询值与敏感片段打码,错误文本里的 URL 同样打码(截断不是脱敏);
|
|
12
13
|
* 表达式截断。审计的用途是「做了什么」,不是「内容是什么」。
|
|
13
14
|
*/
|
|
14
15
|
export interface AuditEntry {
|
|
@@ -37,6 +38,8 @@ export interface AuditFilter {
|
|
|
37
38
|
}
|
|
38
39
|
/** 环形缓冲上限。200 条足够覆盖一次排障现场,又不至于让内存跟着会话长度涨。 */
|
|
39
40
|
export declare const AUDIT_MAX_ENTRIES = 200;
|
|
41
|
+
/** 把一段文本里所有 URL 的查询值/敏感片段打码(用于参数摘要与错误文本)。 */
|
|
42
|
+
export declare function redactAuditText(text: string): string;
|
|
40
43
|
/**
|
|
41
44
|
* 把一次调用的参数压成可审计的摘要:敏感键打码、特定命令的 value 打码、
|
|
42
45
|
* 长表达式截断、整体限长。返回 JSON 文本——审计工具原样把它交给模型。
|