@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.3
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/CHANGELOG.md +9 -140
- package/dist/application/task-lifecycle/advance.js +4 -17
- package/dist/application/task-lifecycle/observe.js +29 -43
- package/dist/application/task-lifecycle/plan-transitions.js +4 -5
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +55 -20
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/status.js +57 -0
- package/dist/commands/task-contract.js +269 -0
- package/dist/executors/dag-pi-executor.js +230 -269
- package/dist/executors/pi-executor.js +0 -2
- package/dist/executors/pi-sdk-executor.js +39 -77
- package/dist/executors/shell-executor.js +2 -3
- package/dist/executors/shell-write-guard.js +32 -27
- package/dist/shared/pi-context-pressure/extension.js +16 -26
- package/dist/shared/pi-retry-settings.js +0 -40
- package/dist/shared/prompts.js +26 -0
- package/dist/task/config.js +2 -0
- package/dist/task/contract/validate-draft.js +106 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/index.js +12 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/operator/capabilities.js +6 -0
- package/dist/task/operator/envelope.js +2 -0
- package/dist/task/operator/index.js +5 -0
- package/dist/task/operator/registry.js +2 -0
- package/dist/task/operator/types.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
- package/dist/task/source-references.js +23 -48
- package/dist/task/source-state.js +1 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/worker/console/chat/assistant-content.js +2 -23
- package/dist/worker/console/chat/explore-tools.js +0 -13
- package/dist/worker/console/chat/pi-runtime.js +1182 -221
- package/dist/worker/console/chat/routes.js +71 -213
- package/dist/worker/console/chat/sdd-data-alignment.js +9 -65
- package/dist/worker/console/chat/session-catalog.js +0 -32
- package/dist/worker/console/chat/session-store.js +6 -22
- package/dist/worker/console/chat/shortcuts.js +0 -6
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/chat/tool-preview.js +0 -11
- package/dist/worker/console/chat/tools.js +0 -6
- package/dist/worker/console/console-update-and-init.js +19 -1
- package/dist/worker/console/console-update-runtime.js +20 -5
- package/dist/worker/console/index.js +20 -0
- package/dist/worker/console/operator-actions.js +1 -13
- package/dist/worker/console/routes.js +0 -8
- package/dist/worker/console/server.js +29 -11
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-hUmM_GRf.js} +1 -1
- package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-Bt-xuieW.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-DTqkL4DJ.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-Cy0MNnzV.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-DDdlT0Pd.js} +1 -1
- package/dist/worker/console/static/assets/channel-BpzRnZmU.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-3JawwNZ6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-DoTp7Cri.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-CcirhZaS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-VrG21gnP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DDOgIS70.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CBjmgvJ_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-CCQSht8M.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-BpGMkUUV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-DNDT41QL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-euXnDFbn.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-BJ6NAeql.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-F5JKIvIq.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-Cwmkl_Gi.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-BmCZ8815.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-zWA3hUnX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-BzA-888F.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-tPNheoch.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-Cdi7Fyqp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-DFK9VBf1.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-xCUr1kpg.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-BANQHY_A.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-VHJYEMkP.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-DiyZQ0C6.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-CZtIeBBa.js} +1 -1
- package/dist/worker/console/static/assets/index-BlN6ytLr.css +1 -0
- package/dist/worker/console/static/assets/index-CA-jfWlE.js +449 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-BuSspC0X.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DRJZyPFy.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-DIfXIVuE.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-CT10_7Lt.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-ufemj74H.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-DEQe8-_m.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-BRLUrbrC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BgibKtiF.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-D3eyfdV9.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DXeTk5Px.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-BgXdFEe7.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-CseC2A7R.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-D80PFzfJ.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BWzHW7LS.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-xWQYTLAn.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-CLxtpZeL.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-DwugtmWV.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-DHEOwHIe.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-pmbZKlBK.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-CivHoM5q.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BfHIE7o6.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +10 -122
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +0 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +15 -35
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +62 -136
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +68 -159
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +56 -75
- package/dist/worker/console/static-src/operator-chat/useComposer.js +78 -206
- package/dist/worker/console/workspace-context.js +1 -21
- package/dist/worker/observe/static/router.js +1 -1
- package/dist/worker/observe/static/styles.css +4 -7
- package/dist/worker/observe/static/views/dag-inspector.js +19 -12
- package/dist/worker/observe/static/views/dag.js +2 -2
- package/dist/worker/scheduler/index.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-design-policy.js +1 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +4 -8
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-repair.js +76 -55
- package/dist/workflows/dag/frontend-review-context.js +1 -2
- package/dist/workflows/dag/frontend-review-scopes.js +12 -34
- package/dist/workflows/dag/frontend-shape-facts.js +2 -12
- package/dist/workflows/dag/frontend-verification-trace.js +42 -8
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +160 -97
- package/dist/workflows/dynamic/index.js +7 -0
- package/docs/architecture/runtime-boundaries.md +3 -3
- package/docs/templates/frontend-implementation-dag.json +8 -9
- package/package.json +3 -7
- package/skills/frontend-bounded-implement/SKILL.md +3 -4
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/references/contract-protocol.md +1 -3
- package/skills/frontend-design-review/SKILL.md +7 -12
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +0 -19
- package/skills/frontend-plan/references/decision-contract.md +19 -26
- package/skills/frontend-review/SKILL.md +18 -12
- package/skills/frontend-review/references/review-findings.md +43 -24
- package/dist/shared/pi-provider-recovery.js +0 -182
- package/dist/worker/console/chat/browser-automation.js +0 -363
- package/dist/worker/console/chat/browser-policy.js +0 -113
- package/dist/worker/console/chat/browser-routes.js +0 -162
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +0 -44
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +0 -89
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +0 -202
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +0 -79
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +0 -86
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +0 -38
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +0 -28
- package/dist/worker/console/chat/pi-runtime/inspection.js +0 -439
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +0 -153
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +0 -5
- package/dist/worker/console/chat/scm-routes.js +0 -217
- package/dist/worker/console/chat/scm-service.js +0 -283
- package/dist/worker/console/chat/scm-tools.js +0 -111
- package/dist/worker/console/chat/session-mode-view.js +0 -49
- package/dist/worker/console/chat/session-mode.js +0 -220
- package/dist/worker/console/chat/terminal-routes.js +0 -216
- package/dist/worker/console/chat/terminal-sessions.js +0 -348
- package/dist/worker/console/chat/terminal-tools.js +0 -211
- package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
- package/dist/worker/console/static/assets/index-wXlQohNs.js +0 -469
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +0 -10
- package/dist/worker/console/static-src/operator-chat/chat-link.js +0 -94
- package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +0 -24
- package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +0 -192
- package/dist/worker/console/static-src/operator-chat/process-label.js +0 -41
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +0 -13
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +0 -10
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +0 -51
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +0 -21
- package/dist/worker/console/workspace-initialization.js +0 -209
- package/dist/workflows/dag/frontend-capacity.js +0 -9
- package/dist/workflows/dag/frontend-plan-completeness.js +0 -186
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +0 -18
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { buildGeneratedOverflowClassifierSource, isContextOverflow, } from "./pi-context-pressure/overflow.js";
|
|
2
|
-
export const NORMALIZED_TRANSIENT_PROVIDER_PREFIX = "provider returned error:";
|
|
3
|
-
const ERROR_TEXT_LIMIT = 1_500;
|
|
4
|
-
const PERMANENT_ERROR_PATTERNS = [
|
|
5
|
-
/\b(?:401|403|429)\b|unauthori[sz]ed|forbidden|authentication|invalid api[-_ ]?key|认证错误|鉴权失败/i,
|
|
6
|
-
/\b(?:quota|billing|insufficient.?credit|out of budget)\b|余额不足|配额(?:不足|耗尽)/i,
|
|
7
|
-
/rate.?limit|too many requests|请求.{0,8}(?:频繁|过多)|频率.{0,8}限制|限流/i,
|
|
8
|
-
/aborted by user|user cancel|cancelled|canceled|用户取消/i,
|
|
9
|
-
/business validation|invalid task|schema validation|业务校验/i,
|
|
10
|
-
];
|
|
11
|
-
export const BUILT_IN_TRANSIENT_PROVIDER_RULES = Object.freeze([
|
|
12
|
-
Object.freeze({
|
|
13
|
-
id: "wrapped-transient-provider-error",
|
|
14
|
-
all: Object.freeze([
|
|
15
|
-
/\b(?:UnknownError|TypeValidationError)\b|Type validation failed/i,
|
|
16
|
-
]),
|
|
17
|
-
any: Object.freeze([
|
|
18
|
-
/\b502\b|\bcode[:\s]*502\b/i,
|
|
19
|
-
/\bLLMRequestError\b/i,
|
|
20
|
-
/network fluctuation|timeout|model processing timeout/i,
|
|
21
|
-
/网络波动|模型处理超时|当前请求超时/i,
|
|
22
|
-
/transient provider failure|non-standard (?:payload|response)|upstream returned/i,
|
|
23
|
-
]),
|
|
24
|
-
}),
|
|
25
|
-
Object.freeze({
|
|
26
|
-
id: "internal-llm-request-error",
|
|
27
|
-
all: Object.freeze([/\bLLMRequestError\b/i]),
|
|
28
|
-
any: Object.freeze([
|
|
29
|
-
/\b502\b|\bcode[:\s]*502\b/i,
|
|
30
|
-
/network fluctuation|timeout|model processing timeout/i,
|
|
31
|
-
/网络波动|模型处理超时|当前请求超时/i,
|
|
32
|
-
/non-standard (?:payload|response)|upstream returned/i,
|
|
33
|
-
]),
|
|
34
|
-
}),
|
|
35
|
-
Object.freeze({
|
|
36
|
-
id: "internal-provider-fluctuation",
|
|
37
|
-
any: Object.freeze([/网络波动|模型处理超时|当前请求超时/i]),
|
|
38
|
-
}),
|
|
39
|
-
]);
|
|
40
|
-
function stringifyErrorPart(value) {
|
|
41
|
-
if (typeof value === "string")
|
|
42
|
-
return value;
|
|
43
|
-
if (value == null)
|
|
44
|
-
return "";
|
|
45
|
-
try {
|
|
46
|
-
return JSON.stringify(value);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return String(value);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function errorText(error) {
|
|
53
|
-
if (typeof error === "string")
|
|
54
|
-
return error;
|
|
55
|
-
if (error == null)
|
|
56
|
-
return "";
|
|
57
|
-
if (typeof error !== "object")
|
|
58
|
-
return String(error);
|
|
59
|
-
const record = error;
|
|
60
|
-
return [
|
|
61
|
-
stringifyErrorPart(record.name),
|
|
62
|
-
stringifyErrorPart(record.message),
|
|
63
|
-
stringifyErrorPart(record.status),
|
|
64
|
-
stringifyErrorPart(record.code),
|
|
65
|
-
stringifyErrorPart(record.data),
|
|
66
|
-
stringifyErrorPart(error),
|
|
67
|
-
]
|
|
68
|
-
.filter(Boolean)
|
|
69
|
-
.join(" ");
|
|
70
|
-
}
|
|
71
|
-
function patternMatches(pattern, text) {
|
|
72
|
-
pattern.lastIndex = 0;
|
|
73
|
-
const matched = pattern.test(text);
|
|
74
|
-
pattern.lastIndex = 0;
|
|
75
|
-
return matched;
|
|
76
|
-
}
|
|
77
|
-
function ruleMatches(rule, text) {
|
|
78
|
-
const all = rule.all ?? [];
|
|
79
|
-
const any = rule.any ?? [];
|
|
80
|
-
if (all.length === 0 && any.length === 0)
|
|
81
|
-
return false;
|
|
82
|
-
if (!all.every((pattern) => patternMatches(pattern, text)))
|
|
83
|
-
return false;
|
|
84
|
-
return any.length === 0 || any.some((pattern) => patternMatches(pattern, text));
|
|
85
|
-
}
|
|
86
|
-
function isPermanentProviderError(error, text) {
|
|
87
|
-
if (isContextOverflow(error))
|
|
88
|
-
return true;
|
|
89
|
-
return PERMANENT_ERROR_PATTERNS.some((pattern) => patternMatches(pattern, text));
|
|
90
|
-
}
|
|
91
|
-
export function classifyTransientProviderError(error, options = {}) {
|
|
92
|
-
const text = errorText(error);
|
|
93
|
-
if (!text || isPermanentProviderError(error, text)) {
|
|
94
|
-
return { retryable: false, reason: "permanent" };
|
|
95
|
-
}
|
|
96
|
-
const rules = [
|
|
97
|
-
...BUILT_IN_TRANSIENT_PROVIDER_RULES,
|
|
98
|
-
...(options.additionalRules ?? []),
|
|
99
|
-
];
|
|
100
|
-
const matched = rules.find((rule) => ruleMatches(rule, text));
|
|
101
|
-
return matched
|
|
102
|
-
? { retryable: true, ruleId: matched.id }
|
|
103
|
-
: { retryable: false, reason: "no-match" };
|
|
104
|
-
}
|
|
105
|
-
export function normalizeTransientProviderErrorMessage(current, options = {}) {
|
|
106
|
-
if (!current || current.startsWith(NORMALIZED_TRANSIENT_PROVIDER_PREFIX)) {
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
if (!classifyTransientProviderError(current, options).retryable)
|
|
110
|
-
return undefined;
|
|
111
|
-
const bounded = current.length <= ERROR_TEXT_LIMIT
|
|
112
|
-
? current
|
|
113
|
-
: `${current.slice(0, ERROR_TEXT_LIMIT - 1)}…`;
|
|
114
|
-
return `${NORMALIZED_TRANSIENT_PROVIDER_PREFIX} ${bounded}`;
|
|
115
|
-
}
|
|
116
|
-
export function shouldRewriteAssistantTransientProviderError(message, options = {}) {
|
|
117
|
-
if (!message || typeof message !== "object")
|
|
118
|
-
return undefined;
|
|
119
|
-
const record = message;
|
|
120
|
-
if (record.role !== "assistant")
|
|
121
|
-
return undefined;
|
|
122
|
-
if (record.stopReason !== undefined && record.stopReason !== "error") {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
const current = typeof record.errorMessage === "string" ? record.errorMessage : "";
|
|
126
|
-
const rewritten = normalizeTransientProviderErrorMessage(current, options);
|
|
127
|
-
return rewritten
|
|
128
|
-
? { message: { ...record, errorMessage: rewritten } }
|
|
129
|
-
: undefined;
|
|
130
|
-
}
|
|
131
|
-
function serializePatterns(patterns) {
|
|
132
|
-
return JSON.stringify(patterns.map((pattern) => ({ source: pattern.source, flags: pattern.flags })));
|
|
133
|
-
}
|
|
134
|
-
/** Generate the pure predicate embedded in the project-level OpenCode plugin. */
|
|
135
|
-
export function buildGeneratedTransientProviderClassifierSource() {
|
|
136
|
-
const rules = BUILT_IN_TRANSIENT_PROVIDER_RULES.map((rule) => ({
|
|
137
|
-
id: rule.id,
|
|
138
|
-
all: (rule.all ?? []).map((pattern) => ({
|
|
139
|
-
source: pattern.source,
|
|
140
|
-
flags: pattern.flags,
|
|
141
|
-
})),
|
|
142
|
-
any: (rule.any ?? []).map((pattern) => ({
|
|
143
|
-
source: pattern.source,
|
|
144
|
-
flags: pattern.flags,
|
|
145
|
-
})),
|
|
146
|
-
}));
|
|
147
|
-
return `${buildGeneratedOverflowClassifierSource()}
|
|
148
|
-
const TRANSIENT_PROVIDER_PERMANENT_PATTERNS = ${serializePatterns(PERMANENT_ERROR_PATTERNS)};
|
|
149
|
-
const TRANSIENT_PROVIDER_RULES = ${JSON.stringify(rules)};
|
|
150
|
-
function transientProviderStringify(value) {
|
|
151
|
-
if (typeof value === "string") return value;
|
|
152
|
-
if (value == null) return "";
|
|
153
|
-
try { return JSON.stringify(value); } catch { return String(value); }
|
|
154
|
-
}
|
|
155
|
-
function transientProviderText(error) {
|
|
156
|
-
if (typeof error === "string") return error;
|
|
157
|
-
if (error == null) return "";
|
|
158
|
-
if (typeof error !== "object") return String(error);
|
|
159
|
-
return [
|
|
160
|
-
transientProviderStringify(error.name),
|
|
161
|
-
transientProviderStringify(error.message),
|
|
162
|
-
transientProviderStringify(error.status),
|
|
163
|
-
transientProviderStringify(error.code),
|
|
164
|
-
transientProviderStringify(error.data),
|
|
165
|
-
transientProviderStringify(error),
|
|
166
|
-
].filter(Boolean).join(" ");
|
|
167
|
-
}
|
|
168
|
-
function transientProviderPatternMatches(pattern, text) {
|
|
169
|
-
return new RegExp(pattern.source, pattern.flags).test(text);
|
|
170
|
-
}
|
|
171
|
-
function transientProviderRuleMatches(rule, text) {
|
|
172
|
-
if (rule.all.length === 0 && rule.any.length === 0) return false;
|
|
173
|
-
if (!rule.all.every((pattern) => transientProviderPatternMatches(pattern, text))) return false;
|
|
174
|
-
return rule.any.length === 0 || rule.any.some((pattern) => transientProviderPatternMatches(pattern, text));
|
|
175
|
-
}
|
|
176
|
-
function isKnownTransientProviderError(error) {
|
|
177
|
-
const text = transientProviderText(error);
|
|
178
|
-
if (!text || isOverflowMessage(text)) return false;
|
|
179
|
-
if (TRANSIENT_PROVIDER_PERMANENT_PATTERNS.some((pattern) => transientProviderPatternMatches(pattern, text))) return false;
|
|
180
|
-
return TRANSIENT_PROVIDER_RULES.some((rule) => transientProviderRuleMatches(rule, text));
|
|
181
|
-
}`;
|
|
182
|
-
}
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-side implementation for the model-callable sidebar browser tools.
|
|
3
|
-
*
|
|
4
|
-
* Navigation is delivered to the visible BrowserPanel through a small pending
|
|
5
|
-
* command queue. Page inspection uses the same http(s) URL loaded by that
|
|
6
|
-
* panel and parses the server-visible HTML. Static GET forms are submitted as
|
|
7
|
-
* real navigations; dynamic scripts and POST forms are intentionally refused.
|
|
8
|
-
*/
|
|
9
|
-
import { normalizeBrowserUrl } from "./browser-policy.js";
|
|
10
|
-
export class BrowserAutomationService {
|
|
11
|
-
fetchImpl;
|
|
12
|
-
pending = [];
|
|
13
|
-
sequence = 0;
|
|
14
|
-
constructor(fetchImpl = fetch) {
|
|
15
|
-
this.fetchImpl = fetchImpl;
|
|
16
|
-
}
|
|
17
|
-
async navigate(rawUrl, options) {
|
|
18
|
-
const normalized = normalizeBrowserUrl(rawUrl);
|
|
19
|
-
if (normalized.kind !== "ok") {
|
|
20
|
-
return {
|
|
21
|
-
ok: false,
|
|
22
|
-
error: normalized.kind === "invalid"
|
|
23
|
-
? "invalid browser url"
|
|
24
|
-
: "browser urls must use http(s)",
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
let snapshot;
|
|
28
|
-
const snapshotSource = rewriteBaiduSearchUrlForSnapshot(normalized.url);
|
|
29
|
-
if (options?.includeSnapshot) {
|
|
30
|
-
try {
|
|
31
|
-
snapshot = await this.inspect(snapshotSource);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
// Navigation itself can still succeed when the panel cannot
|
|
35
|
-
// fetch the page server-side; the iframe remains the fallback.
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const displayUrl = snapshotSource === normalized.url ? normalized.url : snapshotSource;
|
|
39
|
-
const command = {
|
|
40
|
-
kind: "navigate",
|
|
41
|
-
id: this.nextId(),
|
|
42
|
-
url: displayUrl,
|
|
43
|
-
...(snapshot ? { snapshot } : {}),
|
|
44
|
-
createdAt: new Date().toISOString(),
|
|
45
|
-
};
|
|
46
|
-
this.pending = [command];
|
|
47
|
-
return { ok: true, command };
|
|
48
|
-
}
|
|
49
|
-
reload() {
|
|
50
|
-
const command = {
|
|
51
|
-
kind: "reload",
|
|
52
|
-
id: this.nextId(),
|
|
53
|
-
createdAt: new Date().toISOString(),
|
|
54
|
-
};
|
|
55
|
-
this.pending = [command];
|
|
56
|
-
return { ok: true, command };
|
|
57
|
-
}
|
|
58
|
-
take() {
|
|
59
|
-
const commands = this.pending;
|
|
60
|
-
this.pending = [];
|
|
61
|
-
return commands;
|
|
62
|
-
}
|
|
63
|
-
async inspect(rawUrl) {
|
|
64
|
-
const normalized = normalizeBrowserUrl(rawUrl);
|
|
65
|
-
if (normalized.kind !== "ok")
|
|
66
|
-
throw new BrowserAutomationError("INVALID_URL", "invalid browser url");
|
|
67
|
-
const controller = new AbortController();
|
|
68
|
-
const timer = setTimeout(() => controller.abort(), 12_000);
|
|
69
|
-
try {
|
|
70
|
-
const response = await this.fetchImpl(normalized.url, {
|
|
71
|
-
redirect: "follow",
|
|
72
|
-
signal: controller.signal,
|
|
73
|
-
headers: { accept: "text/html,application/xhtml+xml" },
|
|
74
|
-
});
|
|
75
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
76
|
-
if (!contentType.includes("html")) {
|
|
77
|
-
throw new BrowserAutomationError("NOT_HTML", "the browser page did not return an HTML document");
|
|
78
|
-
}
|
|
79
|
-
const html = await response.text();
|
|
80
|
-
if (html.length > 3_000_000) {
|
|
81
|
-
throw new BrowserAutomationError("PAGE_TOO_LARGE", "page exceeds 3MB");
|
|
82
|
-
}
|
|
83
|
-
return parseBrowserPage(html, response.url || normalized.url);
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
if (error instanceof BrowserAutomationError)
|
|
87
|
-
throw error;
|
|
88
|
-
throw new BrowserAutomationError("FETCH_FAILED", "could not load the page", {
|
|
89
|
-
cause: error,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
clearTimeout(timer);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async search(rawQuery, rawUrl) {
|
|
97
|
-
const query = rawQuery.trim();
|
|
98
|
-
if (!query)
|
|
99
|
-
throw new BrowserAutomationError("EMPTY_QUERY", "search query is empty");
|
|
100
|
-
let target = rawUrl?.trim() ? rawUrl.trim() : "https://m.baidu.com/s?wd=";
|
|
101
|
-
const normalized = normalizeBrowserUrl(target);
|
|
102
|
-
if (normalized.kind !== "ok") {
|
|
103
|
-
throw new BrowserAutomationError("INVALID_URL", "search engine url must use http(s)");
|
|
104
|
-
}
|
|
105
|
-
const url = new URL(normalized.url);
|
|
106
|
-
url.searchParams.set(url.searchParams.has("q") ? "q" : "wd", query);
|
|
107
|
-
const result = await this.navigate(url.href, { includeSnapshot: true });
|
|
108
|
-
if (!result.ok)
|
|
109
|
-
throw new BrowserAutomationError("INVALID_URL", result.error);
|
|
110
|
-
return result.command.kind === "navigate" && result.command.snapshot
|
|
111
|
-
? result.command.snapshot
|
|
112
|
-
: await this.inspect(url.href);
|
|
113
|
-
}
|
|
114
|
-
async act(rawUrl, request) {
|
|
115
|
-
const snapshot = await this.inspect(rawUrl);
|
|
116
|
-
const element = selectBrowserElement(snapshot, request);
|
|
117
|
-
if (!element) {
|
|
118
|
-
throw new BrowserAutomationError("ELEMENT_NOT_FOUND", "selector did not match a clickable element or form field");
|
|
119
|
-
}
|
|
120
|
-
if (request.type === "fill") {
|
|
121
|
-
if (element.tag !== "input" && element.tag !== "textarea") {
|
|
122
|
-
throw new BrowserAutomationError("NOT_FILLABLE", "matched element is not fillable");
|
|
123
|
-
}
|
|
124
|
-
const submitted = this.submitForm(snapshot, element, request.value);
|
|
125
|
-
const submittedSnapshot = await this.inspect(submitted.url);
|
|
126
|
-
const navigation = await this.navigate(submitted.url, { includeSnapshot: true });
|
|
127
|
-
if (!navigation.ok)
|
|
128
|
-
throw new BrowserAutomationError("INVALID_URL", navigation.error);
|
|
129
|
-
return {
|
|
130
|
-
...submittedSnapshot,
|
|
131
|
-
action: { type: "fill", selector: request.selector, value: request.value, url: submitted.url },
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
const formSubmit = element.clickable && element.formIndex !== undefined && !element.href
|
|
135
|
-
? this.submitForm(snapshot, element)
|
|
136
|
-
: undefined;
|
|
137
|
-
const targetUrl = formSubmit?.url ?? element.href;
|
|
138
|
-
if (targetUrl === undefined) {
|
|
139
|
-
throw new BrowserAutomationError("NOT_CLICKABLE", "matched element has no safe click target");
|
|
140
|
-
}
|
|
141
|
-
const navigation = await this.navigate(targetUrl, { includeSnapshot: true });
|
|
142
|
-
if (!navigation.ok)
|
|
143
|
-
throw new BrowserAutomationError("INVALID_URL", navigation.error);
|
|
144
|
-
return {
|
|
145
|
-
...snapshot,
|
|
146
|
-
action: {
|
|
147
|
-
type: "click",
|
|
148
|
-
selector: request.selector,
|
|
149
|
-
url: targetUrl,
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
submitForm(snapshot, target, valueOverride) {
|
|
154
|
-
if (target.formIndex === undefined) {
|
|
155
|
-
throw new BrowserAutomationError("FORM_NOT_FOUND", "matched field is not inside a form");
|
|
156
|
-
}
|
|
157
|
-
if ((target.formMethod ?? "get") !== "get") {
|
|
158
|
-
throw new BrowserAutomationError("FORM_METHOD_NOT_SUPPORTED", "only GET forms can be automated without browser scripts");
|
|
159
|
-
}
|
|
160
|
-
const url = new URL(target.formAction ?? snapshot.url);
|
|
161
|
-
for (const element of snapshot.elements) {
|
|
162
|
-
if (element.formIndex !== target.formIndex || !element.name)
|
|
163
|
-
continue;
|
|
164
|
-
if (element.tag === "button")
|
|
165
|
-
continue;
|
|
166
|
-
if (element.tag === "input" && ["submit", "button", "reset", "image"].includes(element.type ?? "")) {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const value = element.selector === target.selector && valueOverride !== undefined
|
|
170
|
-
? valueOverride
|
|
171
|
-
: element.value ?? "";
|
|
172
|
-
url.searchParams.set(element.name, value);
|
|
173
|
-
}
|
|
174
|
-
return { url: url.href };
|
|
175
|
-
}
|
|
176
|
-
nextId() {
|
|
177
|
-
this.sequence += 1;
|
|
178
|
-
return `browser-${Date.now().toString(36)}-${this.sequence}`;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
export class BrowserAutomationError extends Error {
|
|
182
|
-
code;
|
|
183
|
-
constructor(code, message, options) {
|
|
184
|
-
super(message, options);
|
|
185
|
-
this.code = code;
|
|
186
|
-
this.name = "BrowserAutomationError";
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
export function rewriteBaiduSearchUrlForSnapshot(rawUrl) {
|
|
190
|
-
try {
|
|
191
|
-
const url = new URL(rawUrl);
|
|
192
|
-
if (url.protocol !== "https:" || url.hostname !== "www.baidu.com")
|
|
193
|
-
return rawUrl;
|
|
194
|
-
if (url.pathname.replace(/\/+$/, "") !== "/s")
|
|
195
|
-
return rawUrl;
|
|
196
|
-
const query = url.searchParams.get("wd") ?? url.searchParams.get("word");
|
|
197
|
-
if (query === null)
|
|
198
|
-
return rawUrl;
|
|
199
|
-
const mobile = new URL("https://m.baidu.com/s");
|
|
200
|
-
mobile.searchParams.set("wd", query);
|
|
201
|
-
return mobile.href;
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
return rawUrl;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
const SKIP_TAGS = new Set(["script", "style", "template", "noscript"]);
|
|
208
|
-
const INTERACTIVE_TAGS = new Set(["a", "button", "input", "select", "textarea"]);
|
|
209
|
-
export function parseBrowserPage(html, baseUrl) {
|
|
210
|
-
const state = { snapshot: { url: baseUrl, title: "", elements: [] }, nextFormIndex: 0 };
|
|
211
|
-
const tokenizer = /<!--[\s\S]*?-->|<\/?([a-zA-Z][a-zA-Z0-9:-]*)([^>]*)>/g;
|
|
212
|
-
const openTags = [];
|
|
213
|
-
let cursor = 0;
|
|
214
|
-
let match;
|
|
215
|
-
while ((match = tokenizer.exec(html)) !== null) {
|
|
216
|
-
const text = decodeEntities(html.slice(cursor, match.index)).replace(/\s+/g, " ").trim();
|
|
217
|
-
const parent = openTags[openTags.length - 1];
|
|
218
|
-
if (parent && !SKIP_TAGS.has(parent.tag))
|
|
219
|
-
appendText(parent, text);
|
|
220
|
-
cursor = match.index + match[0].length;
|
|
221
|
-
if (match[0].startsWith("<!--"))
|
|
222
|
-
continue;
|
|
223
|
-
const tag = match[1]?.toLowerCase() ?? "";
|
|
224
|
-
const attrs = parseAttributes(match[2] ?? "");
|
|
225
|
-
if (match[0].startsWith("</")) {
|
|
226
|
-
const index = findOpenTag(openTags, tag);
|
|
227
|
-
if (index >= 0) {
|
|
228
|
-
const closed = openTags[index];
|
|
229
|
-
openTags.length = index;
|
|
230
|
-
closeElement(state, closed, baseUrl);
|
|
231
|
-
}
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
const parentForForm = openTags[openTags.length - 1];
|
|
235
|
-
let form = parentForForm?.form;
|
|
236
|
-
if (tag === "form" && !form) {
|
|
237
|
-
form = {
|
|
238
|
-
index: state.nextFormIndex,
|
|
239
|
-
action: attrs.action,
|
|
240
|
-
method: attrs.method?.toLowerCase(),
|
|
241
|
-
};
|
|
242
|
-
state.nextFormIndex += 1;
|
|
243
|
-
}
|
|
244
|
-
const element = { tag, attrs, text: "", form };
|
|
245
|
-
if (/\/>$/.test(match[0]) || isVoidTag(tag)) {
|
|
246
|
-
closeElement(state, element, baseUrl);
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
openTags.push(element);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (state.snapshot.elements.length > 300)
|
|
253
|
-
state.snapshot.elements.length = 300;
|
|
254
|
-
return state.snapshot;
|
|
255
|
-
}
|
|
256
|
-
function parseAttributes(raw) {
|
|
257
|
-
const attrs = {};
|
|
258
|
-
const pattern = /([:@a-zA-Z_#][^\s"'<>`=]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
|
|
259
|
-
let match;
|
|
260
|
-
while ((match = pattern.exec(raw)) !== null) {
|
|
261
|
-
attrs[match[1]?.toLowerCase() ?? ""] = decodeEntities(match[2] ?? match[3] ?? match[4] ?? "");
|
|
262
|
-
}
|
|
263
|
-
return attrs;
|
|
264
|
-
}
|
|
265
|
-
function isVoidTag(tag) {
|
|
266
|
-
return tag === "input" || tag === "img" || tag === "br" || tag === "hr" || tag === "meta" || tag === "link";
|
|
267
|
-
}
|
|
268
|
-
function findOpenTag(stack, tag) {
|
|
269
|
-
for (let index = stack.length - 1; index >= 0; index -= 1) {
|
|
270
|
-
if (stack[index]?.tag === tag)
|
|
271
|
-
return index;
|
|
272
|
-
}
|
|
273
|
-
return -1;
|
|
274
|
-
}
|
|
275
|
-
function appendText(target, text) {
|
|
276
|
-
target.text = target.text ? `${target.text} ${text}` : text;
|
|
277
|
-
}
|
|
278
|
-
function closeElement(state, element, baseUrl) {
|
|
279
|
-
const { tag, attrs, text } = element;
|
|
280
|
-
if (tag === "title") {
|
|
281
|
-
state.snapshot.title = text.slice(0, 300);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (!INTERACTIVE_TAGS.has(tag))
|
|
285
|
-
return;
|
|
286
|
-
let href;
|
|
287
|
-
if (tag === "a" && attrs.href && !/^(javascript|mailto|tel):/i.test(attrs.href.trim())) {
|
|
288
|
-
try {
|
|
289
|
-
href = new URL(attrs.href, baseUrl).href;
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
href = undefined;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
const id = attrs.id;
|
|
296
|
-
const name = attrs.name;
|
|
297
|
-
const selector = buildSelector(tag, id, name, attrs.class, state.snapshot.elements.length);
|
|
298
|
-
const rawText = text || attrs.value || attrs.placeholder || attrs["aria-label"] || attrs.title || "";
|
|
299
|
-
state.snapshot.elements.push({
|
|
300
|
-
tag,
|
|
301
|
-
...(id ? { id } : {}),
|
|
302
|
-
text: rawText.replace(/\s+/g, " ").trim().slice(0, 240),
|
|
303
|
-
...(href ? { href } : {}),
|
|
304
|
-
...(name ? { name } : {}),
|
|
305
|
-
...(attrs.type ? { type: attrs.type } : {}),
|
|
306
|
-
...(attrs.value ? { value: attrs.value } : {}),
|
|
307
|
-
...(attrs.placeholder ? { placeholder: attrs.placeholder } : {}),
|
|
308
|
-
...(element.form ? { formIndex: element.form.index } : {}),
|
|
309
|
-
...(element.form ? {
|
|
310
|
-
formAction: element.form.action === undefined
|
|
311
|
-
? baseUrl
|
|
312
|
-
: new URL(element.form.action, baseUrl).href,
|
|
313
|
-
formMethod: element.form.method ?? "get",
|
|
314
|
-
} : {}),
|
|
315
|
-
selector,
|
|
316
|
-
clickable: tag === "a" || tag === "button" || (tag === "input" && ["submit", "button", "radio", "checkbox"].includes(attrs.type ?? "")),
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
function buildSelector(tag, id, name, className, index) {
|
|
320
|
-
if (id)
|
|
321
|
-
return `#${escapeSelectorValue(id)}`;
|
|
322
|
-
if (name)
|
|
323
|
-
return `${tag}[name="${escapeSelectorValue(name)}"]`;
|
|
324
|
-
if (className?.trim()) {
|
|
325
|
-
const firstClass = className.trim().split(/\s+/)[0];
|
|
326
|
-
if (firstClass)
|
|
327
|
-
return `${tag}.${escapeSelectorValue(firstClass)}`;
|
|
328
|
-
}
|
|
329
|
-
return `${tag}:nth-of-type(${index + 1})`;
|
|
330
|
-
}
|
|
331
|
-
function escapeSelectorValue(value) {
|
|
332
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
333
|
-
}
|
|
334
|
-
export function selectBrowserElement(snapshot, request) {
|
|
335
|
-
const selector = request.selector.trim();
|
|
336
|
-
if (!selector)
|
|
337
|
-
return undefined;
|
|
338
|
-
return snapshot.elements.find((element) => element.selector === selector)
|
|
339
|
-
?? snapshot.elements.find((element) => element.id === selector.slice(1))
|
|
340
|
-
?? snapshot.elements.find((element) => element.name === selector)
|
|
341
|
-
?? snapshot.elements.find((element) => element.text.toLowerCase().includes(selector.toLowerCase()) && selector.length >= 2);
|
|
342
|
-
}
|
|
343
|
-
const ENTITY_MAP = {
|
|
344
|
-
amp: "&",
|
|
345
|
-
lt: "<",
|
|
346
|
-
gt: ">",
|
|
347
|
-
quot: "\"",
|
|
348
|
-
apos: "'",
|
|
349
|
-
nbsp: " ",
|
|
350
|
-
};
|
|
351
|
-
function decodeEntities(value) {
|
|
352
|
-
return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (whole, entity) => {
|
|
353
|
-
if (entity.startsWith("#x") || entity.startsWith("#X")) {
|
|
354
|
-
const code = Number.parseInt(entity.slice(2), 16);
|
|
355
|
-
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
356
|
-
}
|
|
357
|
-
if (entity.startsWith("#")) {
|
|
358
|
-
const code = Number.parseInt(entity.slice(1), 10);
|
|
359
|
-
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
360
|
-
}
|
|
361
|
-
return ENTITY_MAP[entity.toLowerCase()] ?? whole;
|
|
362
|
-
});
|
|
363
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure URL policy for the Operator Chat browser panel: normalize user input
|
|
3
|
-
* into an http(s) URL. Dependency-free so it is unit-testable.
|
|
4
|
-
*
|
|
5
|
-
* Only the scheme is gated: javascript:, data:, file: and friends must never
|
|
6
|
-
* reach the iframe. Local addresses (localhost / 127.0.0.1 / ::1 / LAN
|
|
7
|
-
* hosts) are deliberately allowed — the panel is a development console and
|
|
8
|
-
* its users routinely browse local dev servers.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Extract the `frame-ancestors` source list of a Content-Security-Policy
|
|
12
|
-
* header, or undefined when the directive is absent (or empty).
|
|
13
|
-
*/
|
|
14
|
-
export function extractFrameAncestors(csp) {
|
|
15
|
-
if (csp === null)
|
|
16
|
-
return undefined;
|
|
17
|
-
for (const directive of csp.split(";")) {
|
|
18
|
-
const parts = directive.trim().split(/\s+/);
|
|
19
|
-
if (parts[0] === "frame-ancestors") {
|
|
20
|
-
const sources = parts.slice(1).filter((source) => source !== "");
|
|
21
|
-
return sources.length === 0 ? undefined : sources;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Decide whether a site can render inside the panel iframe. The signals are
|
|
28
|
-
* exactly the ones the browser enforces when it refuses an iframe load:
|
|
29
|
-
* X-Frame-Options DENY/SAMEORIGIN, or a frame-ancestors directive that does
|
|
30
|
-
* not allow `*` ('self' means the SITE's own origin — never ours, so it also
|
|
31
|
-
* blocks the panel). A site we could not reach yields 'unknown' and the
|
|
32
|
-
* plain iframe stays.
|
|
33
|
-
*/
|
|
34
|
-
export function embeddabilityOf(probe) {
|
|
35
|
-
if (probe.reachable !== true)
|
|
36
|
-
return "unknown";
|
|
37
|
-
const xfo = probe.xFrameOptions?.trim().toUpperCase();
|
|
38
|
-
if (xfo === "DENY" || xfo === "SAMEORIGIN")
|
|
39
|
-
return "blocked";
|
|
40
|
-
if (probe.frameAncestors !== undefined &&
|
|
41
|
-
!probe.frameAncestors.some((source) => source === "*")) {
|
|
42
|
-
return "blocked";
|
|
43
|
-
}
|
|
44
|
-
return "embeddable";
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Schemes that must never reach the iframe even without `//` (javascript:,
|
|
48
|
-
* data:, file:, ...). Host:port lookalikes (example.com:8080) are NOT here —
|
|
49
|
-
* they parse as hosts below.
|
|
50
|
-
*/
|
|
51
|
-
const FORBIDDEN_SCHEMES = new Set([
|
|
52
|
-
"javascript",
|
|
53
|
-
"data",
|
|
54
|
-
"file",
|
|
55
|
-
"about",
|
|
56
|
-
"vbscript",
|
|
57
|
-
"blob",
|
|
58
|
-
"mailto",
|
|
59
|
-
"tel",
|
|
60
|
-
"ftp",
|
|
61
|
-
"ftps",
|
|
62
|
-
"ws",
|
|
63
|
-
"wss",
|
|
64
|
-
"sftp",
|
|
65
|
-
"ssh",
|
|
66
|
-
"chrome",
|
|
67
|
-
"chrome-extension",
|
|
68
|
-
"moz-extension",
|
|
69
|
-
"edge",
|
|
70
|
-
"opera",
|
|
71
|
-
"resource",
|
|
72
|
-
"view-source",
|
|
73
|
-
]);
|
|
74
|
-
/**
|
|
75
|
-
* Normalize one address-bar input against the navigation policy.
|
|
76
|
-
* @param input raw user text.
|
|
77
|
-
*/
|
|
78
|
-
export function normalizeBrowserUrl(input) {
|
|
79
|
-
const trimmed = input.trim();
|
|
80
|
-
if (trimmed === "")
|
|
81
|
-
return { kind: "invalid" };
|
|
82
|
-
// Distinguish an explicit scheme from a bare host:port. "example.com:8080"
|
|
83
|
-
// would match a naive scheme regex (dots are legal in schemes), so a
|
|
84
|
-
// scheme prefix is only honored when it is http(s) or a known-forbidden
|
|
85
|
-
// scheme; anything else is treated as a host and gets https://.
|
|
86
|
-
const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed);
|
|
87
|
-
let withScheme;
|
|
88
|
-
if (schemeMatch === null) {
|
|
89
|
-
withScheme = `https://${trimmed}`;
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
const scheme = schemeMatch[1]?.toLowerCase() ?? "";
|
|
93
|
-
if (scheme === "http" || scheme === "https")
|
|
94
|
-
withScheme = trimmed;
|
|
95
|
-
else if (FORBIDDEN_SCHEMES.has(scheme))
|
|
96
|
-
return { kind: "blocked", reason: "scheme" };
|
|
97
|
-
else
|
|
98
|
-
withScheme = `https://${trimmed}`;
|
|
99
|
-
}
|
|
100
|
-
let url;
|
|
101
|
-
try {
|
|
102
|
-
url = new URL(withScheme);
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return { kind: "invalid" };
|
|
106
|
-
}
|
|
107
|
-
// Protocol backstop: any URL that still parses to a non-http(s) scheme
|
|
108
|
-
// (ftp://, ws:// — which carry `//` and skip the list) is refused here.
|
|
109
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
110
|
-
return { kind: "blocked", reason: "scheme" };
|
|
111
|
-
}
|
|
112
|
-
return { kind: "ok", url: url.href };
|
|
113
|
-
}
|