chainlesschain 0.166.30 → 0.166.34
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.md +27 -2
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-D0L1OM_i.js → AIOps-D8PJmSrh.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CeZUkX_V.js → ActionButton-DG6DqHA3.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-P_7CIOew.js → Analytics-DiLleeWs.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-NuqDYrlO.js → AppLayout-DxYkhlHK.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-D1BTQqJM.js → Artifacts-BAW3Kdsi.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-D9XL6DVf.js → Audit-DBr4uY6E.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-Ci2DB2gV.js → BackgroundAgents-Dw0r-bHX.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CreK3Yqn.js → Backup-AyhwYsio.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BkIRKFXX.js → BaseInput-Cj8qJotr.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BoJjlCDT.js → Chat-DcvimQ0u.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer--joJ1e2f.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-BbA932P3.js → Checkbox-C2mlU77l.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BS_f3kQy.js → Codegen-CAfc3YaV.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BJ1tzpRh.js → Col-CIxoxtJI.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DYsZZlgI.js → Community-DvufZoRg.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Cq41A43W.js → Compact-55JTa99J.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CBqSz1op.js → Compliance-BllWog6P.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-ZAn8buD_.js → Cowork-ThsFGc5p.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-DM4WTZQT.js → Cron-CZjl5Ig2.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-S65sJZwe.js → Crosschain-DIAKAoQT.js} +1 -1
- package/src/assets/web-panel/assets/{DID-0Av7FTMv.js → DID-ayygwEil.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-D-1vFtiV.js → Dashboard-C3Xjm2UO.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BZufOCir.js → Dropdown-C1S3zf-j.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DJE8szQZ.js → EmailListRenderer-DRvriU-H.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqqPHaZn.js → FamilyGuardDashboard-iSX2bmsD.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CslSyj60.js → Federation-Dk0Stkv-.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-uTfAl63o.js → FormItemContext-DasXSGff.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BIP2obER.js → GenericCardRenderer-Dv9XGlxI.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CoVpUKSg.js → Git-B5PgXL3Z.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-zW6mlFcK.js → Governance-B78x11e_.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-zfu2tMaa.js → Inference-b7CX1RqW.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Dd38pMCe.js → KnowledgeGraph--MgBzMVp.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BhX28L73.js → Logs-CYCiGmVc.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-yrl10SWw.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-DGBKtVL6.js → Marketplace-CsbwQ0eM.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-C0jSXrYN.js → McpTools-0GBWHVra.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-_xyOJV55.js → Memory-fAywmAYr.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DGm6cS5Y.js → MobileBridge-BeNEyy3s.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Zt1cpErd.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-CSfoeF9f.js → Mtc-CqRhJB70.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-DuXFbCYL.js → MtcAudit-BEyGZflc.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-CdnOBFhs.js → Multisig-luz3EBT2.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-C0URZ4u4.js → NLProgramming-Dkbvp8IX.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CJznx6ft.js → Notes-Cf9I2K8j.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DJcqYoZP.js → NotificationSettings-BTfuzynL.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-DBmCtl2H.js +1 -0
- package/src/assets/web-panel/assets/{Organization-ucvP5Gzp.js → Organization-BbG3-vDy.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Be3DoWvY.js → Overflow-CMtFKEIQ.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BXV17CWM.js → P2P-6SAEb_lU.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-CWG2OLvh.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-Bn0mjFJA.js → Permissions-ClZxZKZl.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CQ2uLeuD.js → PersonalDataHub-BYkyoZDT.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DWv7KJKL.js → Pipeline-B28ZgADI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B4I0G5_J.js → Privacy-BxtZywbB.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CDGy6B7j.js → ProjectInit-DPqoRnxK.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CXUq9nGH.js → ProjectSettings-QjKvMkau.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-B-IA5qsB.js → Projects-Cp0Z8ZG0.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Card8pXM.js → Providers-CrS-rbyb.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DrU0hFRp.js → QrScannerModal-xTrhbVAU.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-cB-AFOxA.js → QuickAsk-DFOqJoq-.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CvAvFr0u.js → Recommend-DXlKqu2-.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BIrtatCm.js → RemoteSession-BLLaZH8K.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-CrRpi112.js → Reputation-CUgAdJ11.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DmaenKNU.js → Row-DzOO3wP-.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CGZBcNLM.js → RssFeed-B6GrRvh8.js} +3 -3
- package/src/assets/web-panel/assets/{Search-Bvxjc4AU.js → Search-Cj3zKLb3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-1Z-5c2n6.js → Security-LnktzidI.js} +3 -3
- package/src/assets/web-panel/assets/{Services-3zYh5_28.js → Services-BQ6sIc2K.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DEyGi_mq.js → Skeleton-CCSV3Jbw.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BjWR0303.js → Skills-CpoQ4SNk.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-C6rE_C6V.js → Sla-DbAbgRPq.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DJ1DtDuf.js → SpeechSettings-C4cJ_hoD.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B2j0LEry.js → SyncSettings-BKFzjN9Z.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-Cf4bl7NS.js +1 -0
- package/src/assets/web-panel/assets/{Templates-Eb0UjdGe.js → Templates-DTEQfFAQ.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BlkC3jZ8.js → Tenant-wToJWKXB.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-MMaa8P6V.js → Terminal-1s3c7kmF.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-B4EsSPro.js → TimelineRenderer-BcfsYW3u.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Chk0rule.js → Tokens-Byez6FY0.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Cww9ZKjK.js → Trigger-Cq8mZO88.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-xSwx_Zhk.js → Trust-DUrMmEwR.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CNaC0gRj.js → UkeySign-CVHwXrVU.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-gpoRF70F.js → VideoEditing-CDKbTRUS.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-eSjvqmUV.js → Wallet-BqnEBIOy.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-C4T03Dyz.js → WebAuthn-CeTMpj7_.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-eR0JIHbW.js → WorkflowEditor-F6OgjV3C.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CPRK3APB.js → chat-CMHUCwq3.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BP4ZS2SA.js → colors-DzVM_fpJ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-C2bvxEX0.js → compact-item-Hhmg51yT.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DfgFYASc.js → createContext-SwHt7x3Q.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-5q_Y0E88.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BlwCGOVd.js → hasIn-HGNKkn9A.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bg_1EOhP.js → index--iOqM_AI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDavuQ3n.js → index-AQcDW8bB.js} +3 -3
- package/src/assets/web-panel/assets/{index-CBJrpDfv.js → index-B6YQN5_R.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPezy3IJ.js → index-BDE-WiE5.js} +1 -1
- package/src/assets/web-panel/assets/{index-JhYgZWbg.js → index-BDhCx17_.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2n5VKtr.js → index-BDt4wSl6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BENgpvOm.js → index-BJqNTF1S.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKZNLHhD.js → index-BRikmONJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3qzM-P9.js → index-B_HgLPC5.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLTzHq3a.js → index-Ba2Kszdy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DEohnDSJ.js → index-BlHB5eax.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQ-CB1zc.js → index-BmPYmoY3.js} +1 -1
- package/src/assets/web-panel/assets/{index-5b_mMYB_.js → index-Bq9MMKno.js} +1 -1
- package/src/assets/web-panel/assets/{index-IYshwMo1.js → index-BstLYy9q.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzHzRE9E.js → index-C-YkiVyL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZHYspFu.js → index-C1QJxiOL.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_LuOKcc.js → index-CDtzUt75.js} +1 -1
- package/src/assets/web-panel/assets/{index-9VWjFlVU.js → index-CMG_2fK2.js} +1 -1
- package/src/assets/web-panel/assets/{index-QE8jhGyd.js → index-COOcGOyK.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6Zbp8DY.js → index-CWkWh48N.js} +1 -1
- package/src/assets/web-panel/assets/{index-_5mGFZRu.js → index-Cc-d6f0F.js} +1 -1
- package/src/assets/web-panel/assets/{index-BO1fPYye.js → index-ChLHLBkF.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-ubYfKF.js → index-Cnm4zroJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgUnGGL8.js → index-Co4RcBua.js} +1 -1
- package/src/assets/web-panel/assets/{index-AbVd9bHP.js → index-D5dcWBqg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZbQQtMn.js → index-DQT5thlV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DR2Tg8uN.js → index-Dcg7nQDe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdaniU1x.js → index-DgoefbLs.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyMwW_p5.js → index-DyZUrnje.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dx3HnzcX.js → index-E-bI8HpV.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2D-mqRz.js → index-F7Grz3-G.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDGliY6u.js → index-MDdTkUpA.js} +1 -1
- package/src/assets/web-panel/assets/{index-XBrnfoX6.js → index-aKV4Py_2.js} +1 -1
- package/src/assets/web-panel/assets/index-dl9TJMh5.js +1 -0
- package/src/assets/web-panel/assets/{index-B0HiXNYh.js → index-jrRtmiUh.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnD3N8YJ.js → index-oz0Xxs8D.js} +1 -1
- package/src/assets/web-panel/assets/{index-Sk-fx_32.js → index-pwLuJGFh.js} +1 -1
- package/src/assets/web-panel/assets/{index-WSC6WoDZ.js → index-sYkFp9A4.js} +1 -1
- package/src/assets/web-panel/assets/index-x0DDgUaT.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-CmR0B8Wx.js → initDefaultProps-C_2QM45F.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BC0UJWJb.js → motion-DkxY04tR.js} +1 -1
- package/src/assets/web-panel/assets/{move-cvEJQRZ_.js → move-D0CJlA7D.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-0ZPAo5px.js → mtc-parser-DjP38YUD.js} +1 -1
- package/src/assets/web-panel/assets/{omit-UBXYakkw.js → omit-C9CJDaGT.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CRZgdSvi.js → pickAttrs-D5Wq2fa8.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B_Y8sKr_.js → placementArrow-DJ6Dixs7.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CFgbiMsJ.js → responsiveObserve-DMmM0Dee.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CF6LYcYS.js → slide-DipmpYA-.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Cj2PusGV.js → statusUtils-DSqw6_nU.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Czg7dDLr.js → styleChecker-t828MYjI.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Bhi9P67E.js → useFlexGapSupport-COsSNzDQ.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-X9lqJXBc.js → useFs-Bq5IEzv_.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iL34aL71.js → usePersonalDataHub-CUKM0HdW.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DJ7TA9fc.js → vnode-l-JwAt2M.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DfQDsJXd.js → zoom-D7xkW-gD.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/data/changelog.json +52 -0
- package/src/lib/evolution/evolution-file-witness.js +59 -13
- package/src/lib/evolution/evolution-ledger-file-backend.js +16 -1
- package/src/lib/remote-read-loop-guard.js +290 -0
- package/src/lib/task-progress-tracker.js +5 -1
- package/src/lib/text-file-search-worker.js +147 -0
- package/src/lib/text-file-search.js +116 -0
- package/src/lib/web-fetch-snapshots.js +211 -0
- package/src/lib/web-fetch.js +317 -26
- package/src/lib/web-search.js +118 -10
- package/src/runtime/agent-core.js +85 -1
- package/src/runtime/coding-agent-contract-shared.cjs +98 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-D68YIuni.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-CbOmzhyX.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-DqNyu4pO.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CEQJHoU9.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-CZDiOia9.js +0 -7
- package/src/assets/web-panel/assets/Tasks-ClYwVLRE.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CahG3JuS.js +0 -1
- package/src/assets/web-panel/assets/index-BZ3kGfit.js +0 -1
- package/src/assets/web-panel/assets/index-Dm5MZg8A.js +0 -1
package/src/lib/web-fetch.js
CHANGED
|
@@ -10,8 +10,11 @@ import https from "node:https";
|
|
|
10
10
|
import { lookup as dnsLookup } from "node:dns";
|
|
11
11
|
import { URL } from "node:url";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
+
import { StringDecoder } from "node:string_decoder";
|
|
14
|
+
import { webFetchSnapshots } from "./web-fetch-snapshots.js";
|
|
13
15
|
|
|
14
|
-
const DEFAULT_MAX_BYTES =
|
|
16
|
+
const DEFAULT_MAX_BYTES = 10_000_000;
|
|
17
|
+
const DEFAULT_MAX_CHARS = 20_000;
|
|
15
18
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
16
19
|
|
|
17
20
|
// RFC 1918 + loopback + link-local (IPv4 and IPv6). Blocked by default unless
|
|
@@ -99,13 +102,23 @@ export function checkAllowed(urlStr, config = {}) {
|
|
|
99
102
|
return { allowed: true, url: parsed };
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
if (!html) return "";
|
|
105
|
+
function stripHtmlNoise(html) {
|
|
104
106
|
let text = String(html);
|
|
105
107
|
// Strip scripts/styles entirely
|
|
106
108
|
text = text.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
107
109
|
text = text.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
108
110
|
text = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
111
|
+
// A bounded download may end inside a script, stylesheet, comment or tag.
|
|
112
|
+
// Closed blocks were removed above; discard any unfinished non-content tail.
|
|
113
|
+
return text
|
|
114
|
+
.replace(/<(?:script|style)\b[^>]*>[\s\S]*$/gi, "")
|
|
115
|
+
.replace(/<!--[\s\S]*$/g, "")
|
|
116
|
+
.replace(/<[^>]*$/g, "");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function htmlToMarkdown(html) {
|
|
120
|
+
if (!html) return "";
|
|
121
|
+
let text = stripHtmlNoise(html);
|
|
109
122
|
// Convert headings
|
|
110
123
|
text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, n, inner) => {
|
|
111
124
|
return `\n\n${"#".repeat(Number(n))} ${inner.trim()}\n\n`;
|
|
@@ -141,7 +154,10 @@ function stripTags(html) {
|
|
|
141
154
|
return String(html).replace(/<[^>]+>/g, "");
|
|
142
155
|
}
|
|
143
156
|
|
|
144
|
-
function webFetchCacheKey(
|
|
157
|
+
function webFetchCacheKey(
|
|
158
|
+
parsed,
|
|
159
|
+
{ format, maxBytes, maxChars, offset, onOverflow, maxRedirects, headers },
|
|
160
|
+
) {
|
|
145
161
|
// A caller-provided header can carry credentials or change representation.
|
|
146
162
|
// Keep those requests out of the shared host cache entirely.
|
|
147
163
|
if (
|
|
@@ -155,6 +171,9 @@ function webFetchCacheKey(parsed, { format, maxBytes, maxRedirects, headers }) {
|
|
|
155
171
|
url: parsed.toString(),
|
|
156
172
|
format,
|
|
157
173
|
maxBytes,
|
|
174
|
+
maxChars,
|
|
175
|
+
offset,
|
|
176
|
+
onOverflow,
|
|
158
177
|
maxRedirects,
|
|
159
178
|
});
|
|
160
179
|
// Do not retain a raw URL/query as a long-lived Map key in the host cache.
|
|
@@ -200,6 +219,9 @@ export function makeSafeLookup(allowPrivateHosts, deps = _deps) {
|
|
|
200
219
|
}
|
|
201
220
|
}
|
|
202
221
|
}
|
|
222
|
+
// Node's autoSelectFamily path requests all addresses. Returning a scalar
|
|
223
|
+
// here makes Node read undefined.address and reject every hostname.
|
|
224
|
+
if (opts.all) return cb(null, list);
|
|
203
225
|
const chosen = list[0];
|
|
204
226
|
cb(null, chosen.address, chosen.family);
|
|
205
227
|
});
|
|
@@ -208,15 +230,24 @@ export function makeSafeLookup(allowPrivateHosts, deps = _deps) {
|
|
|
208
230
|
|
|
209
231
|
async function _doRequest(
|
|
210
232
|
parsed,
|
|
211
|
-
{ maxBytes, timeout, headers, allowPrivateHosts },
|
|
233
|
+
{ maxBytes, timeout, headers, allowPrivateHosts, truncateOnOverflow },
|
|
212
234
|
) {
|
|
213
235
|
const lib = parsed.protocol === "https:" ? https : http;
|
|
214
236
|
return new Promise((resolve, reject) => {
|
|
237
|
+
let timer;
|
|
238
|
+
let settled = false;
|
|
239
|
+
const settle = (error, result) => {
|
|
240
|
+
if (settled) return;
|
|
241
|
+
settled = true;
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
if (error) reject(error);
|
|
244
|
+
else resolve(result);
|
|
245
|
+
};
|
|
215
246
|
const req = lib.request(
|
|
216
247
|
{
|
|
217
248
|
method: "GET",
|
|
218
249
|
protocol: parsed.protocol,
|
|
219
|
-
hostname: parsed.hostname,
|
|
250
|
+
hostname: parsed.hostname.replace(/^\[|\]$/g, ""),
|
|
220
251
|
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
221
252
|
path: parsed.pathname + parsed.search,
|
|
222
253
|
// Resolve-and-check: reject the connection if the host resolves to a
|
|
@@ -237,30 +268,76 @@ async function _doRequest(
|
|
|
237
268
|
res.headers.location
|
|
238
269
|
) {
|
|
239
270
|
res.resume();
|
|
240
|
-
return
|
|
271
|
+
return settle(null, { redirect: res.headers.location });
|
|
241
272
|
}
|
|
242
273
|
const chunks = [];
|
|
243
274
|
let size = 0;
|
|
275
|
+
const complete = (downloadTruncated = false) => {
|
|
276
|
+
const buffer = Buffer.concat(chunks);
|
|
277
|
+
settle(null, {
|
|
278
|
+
statusCode: res.statusCode,
|
|
279
|
+
headers: res.headers,
|
|
280
|
+
// With a partial response, discard an incomplete final UTF-8
|
|
281
|
+
// sequence instead of inventing a replacement character.
|
|
282
|
+
body: downloadTruncated
|
|
283
|
+
? new StringDecoder("utf8").write(buffer)
|
|
284
|
+
: buffer.toString("utf8"),
|
|
285
|
+
bytes: buffer.length,
|
|
286
|
+
downloadTruncated,
|
|
287
|
+
});
|
|
288
|
+
};
|
|
244
289
|
res.on("data", (chunk) => {
|
|
290
|
+
if (settled) return;
|
|
291
|
+
const available = maxBytes - size;
|
|
245
292
|
size += chunk.length;
|
|
246
293
|
if (size > maxBytes) {
|
|
247
|
-
|
|
294
|
+
if (truncateOnOverflow) {
|
|
295
|
+
if (available > 0) chunks.push(chunk.subarray(0, available));
|
|
296
|
+
complete(true);
|
|
297
|
+
res.destroy();
|
|
298
|
+
req.destroy();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const error = Object.assign(
|
|
302
|
+
new Error(`response exceeds maxBytes (${maxBytes})`),
|
|
303
|
+
{
|
|
304
|
+
code: "ERR_RESPONSE_TOO_LARGE",
|
|
305
|
+
maxBytes,
|
|
306
|
+
receivedBytes: size,
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
settle(error);
|
|
310
|
+
res.destroy();
|
|
311
|
+
req.destroy();
|
|
248
312
|
return;
|
|
249
313
|
}
|
|
250
314
|
chunks.push(chunk);
|
|
251
315
|
});
|
|
252
316
|
res.on("end", () => {
|
|
253
|
-
|
|
254
|
-
statusCode: res.statusCode,
|
|
255
|
-
headers: res.headers,
|
|
256
|
-
body: Buffer.concat(chunks).toString("utf8"),
|
|
257
|
-
});
|
|
317
|
+
if (!settled) complete();
|
|
258
318
|
});
|
|
259
|
-
res.on("error",
|
|
319
|
+
res.on("error", (error) => settle(error));
|
|
320
|
+
res.on("aborted", () =>
|
|
321
|
+
settle(
|
|
322
|
+
Object.assign(new Error("response aborted"), {
|
|
323
|
+
code: "ECONNRESET",
|
|
324
|
+
}),
|
|
325
|
+
),
|
|
326
|
+
);
|
|
260
327
|
},
|
|
261
328
|
);
|
|
262
|
-
|
|
263
|
-
|
|
329
|
+
const abortTimeout = () => {
|
|
330
|
+
settle(
|
|
331
|
+
Object.assign(new Error("request timeout"), { code: "ETIMEDOUT" }),
|
|
332
|
+
);
|
|
333
|
+
req.destroy();
|
|
334
|
+
};
|
|
335
|
+
req.on("error", (error) => settle(error));
|
|
336
|
+
req.on("timeout", abortTimeout);
|
|
337
|
+
// Covers DNS resolution and continuously streaming bodies as well as idle
|
|
338
|
+
// sockets. Redirects receive only the remaining overall request budget.
|
|
339
|
+
timer = setTimeout(abortTimeout, timeout);
|
|
340
|
+
timer.unref?.();
|
|
264
341
|
req.end();
|
|
265
342
|
});
|
|
266
343
|
}
|
|
@@ -269,6 +346,14 @@ export async function webFetch(url, options = {}) {
|
|
|
269
346
|
const {
|
|
270
347
|
format = "markdown",
|
|
271
348
|
maxBytes = DEFAULT_MAX_BYTES,
|
|
349
|
+
maxChars = DEFAULT_MAX_CHARS,
|
|
350
|
+
offset = 0,
|
|
351
|
+
snapshotId = null,
|
|
352
|
+
query = null,
|
|
353
|
+
maxMatches = 20,
|
|
354
|
+
contextChars = 150,
|
|
355
|
+
caseSensitive = false,
|
|
356
|
+
onOverflow = "truncate",
|
|
272
357
|
timeout = DEFAULT_TIMEOUT_MS,
|
|
273
358
|
config = {},
|
|
274
359
|
headers = {},
|
|
@@ -278,19 +363,124 @@ export async function webFetch(url, options = {}) {
|
|
|
278
363
|
|
|
279
364
|
const check = checkAllowed(url, config);
|
|
280
365
|
if (!check.allowed) {
|
|
281
|
-
return {
|
|
366
|
+
return {
|
|
367
|
+
error: `web_fetch blocked: ${check.reason}`,
|
|
368
|
+
code: "ERR_FETCH_BLOCKED",
|
|
369
|
+
retryable: false,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
374
|
+
maxBytes <= 0 ||
|
|
375
|
+
!Number.isSafeInteger(maxChars) ||
|
|
376
|
+
maxChars <= 0 ||
|
|
377
|
+
!Number.isSafeInteger(offset) ||
|
|
378
|
+
offset < 0 ||
|
|
379
|
+
(snapshotId !== null && (typeof snapshotId !== "string" || !snapshotId)) ||
|
|
380
|
+
!Number.isSafeInteger(timeout) ||
|
|
381
|
+
timeout <= 0 ||
|
|
382
|
+
timeout > 2_147_483_647 ||
|
|
383
|
+
!Number.isSafeInteger(maxRedirects) ||
|
|
384
|
+
maxRedirects < 0 ||
|
|
385
|
+
!["truncate", "error"].includes(onOverflow)
|
|
386
|
+
) {
|
|
387
|
+
return {
|
|
388
|
+
error: "invalid web_fetch size, output length, timeout or redirect limit",
|
|
389
|
+
code: "ERR_FETCH_OPTIONS",
|
|
390
|
+
retryable: false,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (query !== null && (!snapshotId || typeof query !== "string" || !query)) {
|
|
395
|
+
return {
|
|
396
|
+
error:
|
|
397
|
+
"Download the URL first, then pass snapshotId and a non-empty query to search its saved text",
|
|
398
|
+
code: "ERR_FETCH_OPTIONS",
|
|
399
|
+
retryable: false,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (format === "json" && (snapshotId || offset !== 0)) {
|
|
403
|
+
return {
|
|
404
|
+
error:
|
|
405
|
+
"JSON values cannot be paged. Use format=text to save and read a JSON document in chunks.",
|
|
406
|
+
code: "ERR_FETCH_OPTIONS",
|
|
407
|
+
retryable: false,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const snapshotIdentity = createHash("sha256")
|
|
411
|
+
.update(
|
|
412
|
+
JSON.stringify({
|
|
413
|
+
url: check.url.toString(),
|
|
414
|
+
format,
|
|
415
|
+
headers,
|
|
416
|
+
allowPrivateHosts: config.allowPrivateHosts === true,
|
|
417
|
+
allowedDomains: config.allowedDomains || ["*"],
|
|
418
|
+
}),
|
|
419
|
+
)
|
|
420
|
+
.digest("hex");
|
|
421
|
+
const readSnapshot = (id) =>
|
|
422
|
+
webFetchSnapshots.read(
|
|
423
|
+
id,
|
|
424
|
+
snapshotIdentity,
|
|
425
|
+
hostResourceBudget,
|
|
426
|
+
offset,
|
|
427
|
+
maxChars,
|
|
428
|
+
);
|
|
429
|
+
if (snapshotId) {
|
|
430
|
+
try {
|
|
431
|
+
const result = readSnapshot(snapshotId);
|
|
432
|
+
if (result.downloadTruncated && onOverflow === "error") {
|
|
433
|
+
return {
|
|
434
|
+
error:
|
|
435
|
+
"The saved download is incomplete. Fetch again without snapshotId and with a larger maxBytes.",
|
|
436
|
+
code: "ERR_RESPONSE_TOO_LARGE",
|
|
437
|
+
retryable: false,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (query !== null && !result.error) {
|
|
441
|
+
const search = await webFetchSnapshots.search(
|
|
442
|
+
snapshotId,
|
|
443
|
+
snapshotIdentity,
|
|
444
|
+
hostResourceBudget,
|
|
445
|
+
{
|
|
446
|
+
pattern: query,
|
|
447
|
+
offset,
|
|
448
|
+
maxMatches,
|
|
449
|
+
contextChars,
|
|
450
|
+
caseSensitive,
|
|
451
|
+
url: check.url.toString(),
|
|
452
|
+
},
|
|
453
|
+
);
|
|
454
|
+
return search.error ? search : { ...search, cached: true };
|
|
455
|
+
}
|
|
456
|
+
return result.error ? result : { ...result, cached: true };
|
|
457
|
+
} catch {
|
|
458
|
+
return {
|
|
459
|
+
error:
|
|
460
|
+
"Unable to read the local webpage snapshot. Fetch the URL again without snapshotId.",
|
|
461
|
+
code: "ERR_FETCH_SNAPSHOT_READ",
|
|
462
|
+
retryable: false,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
282
465
|
}
|
|
283
466
|
|
|
284
467
|
const cacheKey = webFetchCacheKey(check.url, {
|
|
285
468
|
format,
|
|
286
469
|
maxBytes,
|
|
470
|
+
maxChars,
|
|
471
|
+
offset,
|
|
472
|
+
onOverflow,
|
|
287
473
|
maxRedirects,
|
|
288
474
|
headers,
|
|
289
475
|
});
|
|
290
476
|
if (cacheKey && hostResourceBudget?.getWebFetch) {
|
|
291
477
|
try {
|
|
292
478
|
const cached = hostResourceBudget.getWebFetch(cacheKey);
|
|
293
|
-
if (cached)
|
|
479
|
+
if (cached) {
|
|
480
|
+
if (!cached.snapshotId) return { ...cached, cached: true };
|
|
481
|
+
const page = readSnapshot(cached.snapshotId);
|
|
482
|
+
if (!page.error) return { ...page, cached: true };
|
|
483
|
+
}
|
|
294
484
|
} catch (error) {
|
|
295
485
|
return hostBudgetUnavailable("web_fetch", error);
|
|
296
486
|
}
|
|
@@ -314,21 +504,38 @@ export async function webFetch(url, options = {}) {
|
|
|
314
504
|
let parsed = check.url;
|
|
315
505
|
let redirects = 0;
|
|
316
506
|
let response;
|
|
507
|
+
const deadline = Date.now() + timeout;
|
|
317
508
|
while (true) {
|
|
509
|
+
const remaining = deadline - Date.now();
|
|
510
|
+
if (remaining <= 0) {
|
|
511
|
+
throw Object.assign(new Error("request timeout"), {
|
|
512
|
+
code: "ETIMEDOUT",
|
|
513
|
+
});
|
|
514
|
+
}
|
|
318
515
|
response = await _doRequest(parsed, {
|
|
319
516
|
maxBytes,
|
|
320
|
-
timeout,
|
|
517
|
+
timeout: remaining,
|
|
321
518
|
headers,
|
|
322
519
|
allowPrivateHosts: config.allowPrivateHosts,
|
|
520
|
+
// An incomplete JSON document cannot be returned as a valid value.
|
|
521
|
+
truncateOnOverflow: onOverflow === "truncate" && format !== "json",
|
|
323
522
|
});
|
|
324
523
|
if (!response.redirect) break;
|
|
325
524
|
if (++redirects > maxRedirects) {
|
|
326
|
-
return {
|
|
525
|
+
return {
|
|
526
|
+
error: "too many redirects",
|
|
527
|
+
code: "ERR_TOO_MANY_REDIRECTS",
|
|
528
|
+
retryable: false,
|
|
529
|
+
};
|
|
327
530
|
}
|
|
328
531
|
const next = new URL(response.redirect, parsed);
|
|
329
532
|
const nextCheck = checkAllowed(next.toString(), config);
|
|
330
533
|
if (!nextCheck.allowed) {
|
|
331
|
-
return {
|
|
534
|
+
return {
|
|
535
|
+
error: `redirect blocked: ${nextCheck.reason}`,
|
|
536
|
+
code: "ERR_FETCH_BLOCKED",
|
|
537
|
+
retryable: false,
|
|
538
|
+
};
|
|
332
539
|
}
|
|
333
540
|
parsed = nextCheck.url;
|
|
334
541
|
}
|
|
@@ -336,29 +543,86 @@ export async function webFetch(url, options = {}) {
|
|
|
336
543
|
const { statusCode, headers: respHeaders, body } = response;
|
|
337
544
|
const contentType = String(respHeaders["content-type"] || "");
|
|
338
545
|
|
|
546
|
+
// Error pages are tool failures, not successful evidence or cache entries.
|
|
547
|
+
// Classify before JSON parsing so a 404 HTML page is not a JSON error.
|
|
548
|
+
if (statusCode >= 400) {
|
|
549
|
+
const rateLimited =
|
|
550
|
+
statusCode === 429 ||
|
|
551
|
+
(statusCode === 403 &&
|
|
552
|
+
(respHeaders["x-ratelimit-remaining"] === "0" ||
|
|
553
|
+
!!respHeaders["retry-after"]));
|
|
554
|
+
const retryable = rateLimited || statusCode === 408 || statusCode >= 500;
|
|
555
|
+
return {
|
|
556
|
+
url: parsed.toString(),
|
|
557
|
+
error: `web_fetch failed: HTTP ${statusCode}`,
|
|
558
|
+
code: "ERR_HTTP_STATUS",
|
|
559
|
+
statusCode,
|
|
560
|
+
retryable,
|
|
561
|
+
...(respHeaders["retry-after"]
|
|
562
|
+
? { retryAfter: String(respHeaders["retry-after"]) }
|
|
563
|
+
: {}),
|
|
564
|
+
body: body.slice(0, 2000),
|
|
565
|
+
hint: rateLimited
|
|
566
|
+
? "Rate limited. Honor Retry-After or the service's rate-limit reset before a bounded retry. Do not retry immediately."
|
|
567
|
+
: [401, 403, 404].includes(statusCode)
|
|
568
|
+
? "Check the URL and authentication/access. An identical unauthenticated request will not fix this. For GitHub Actions, use an authenticated gh run view with --log-failed for the specific run/job."
|
|
569
|
+
: "Retry at most once for a transient failure; otherwise use the existing evidence and report the missing information.",
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
339
573
|
let output = body;
|
|
340
574
|
let outputFormat = format;
|
|
341
575
|
if (format === "markdown") {
|
|
342
576
|
output = /html/i.test(contentType) ? htmlToMarkdown(body) : body;
|
|
343
577
|
} else if (format === "text") {
|
|
344
|
-
output = /html/i.test(contentType)
|
|
578
|
+
output = /html/i.test(contentType)
|
|
579
|
+
? stripTags(stripHtmlNoise(body))
|
|
580
|
+
: body;
|
|
345
581
|
} else if (format === "json") {
|
|
346
582
|
try {
|
|
347
583
|
output = JSON.parse(body);
|
|
348
584
|
} catch {
|
|
349
|
-
return {
|
|
585
|
+
return {
|
|
586
|
+
error: "response is not valid JSON",
|
|
587
|
+
code: "ERR_INVALID_JSON",
|
|
588
|
+
retryable: false,
|
|
589
|
+
statusCode,
|
|
590
|
+
body: body.slice(0, 2000),
|
|
591
|
+
};
|
|
350
592
|
}
|
|
351
593
|
}
|
|
352
594
|
|
|
353
|
-
|
|
595
|
+
// Download limits apply to the raw page, which can contain much more
|
|
596
|
+
// markup than the extracted text. Bound model context AFTER extraction;
|
|
597
|
+
// a short requested answer must not abort an otherwise readable page.
|
|
598
|
+
const totalChars =
|
|
599
|
+
format !== "json" && typeof output === "string" ? output.length : null;
|
|
600
|
+
const downloadTruncated = response.downloadTruncated === true;
|
|
601
|
+
let result = {
|
|
354
602
|
url: parsed.toString(),
|
|
355
603
|
statusCode,
|
|
356
604
|
contentType,
|
|
357
605
|
format: outputFormat,
|
|
358
|
-
bytes:
|
|
606
|
+
bytes: response.bytes,
|
|
359
607
|
content: output,
|
|
608
|
+
...(downloadTruncated ? { downloadTruncated: true, maxBytes } : {}),
|
|
609
|
+
...(downloadTruncated
|
|
610
|
+
? {
|
|
611
|
+
hint: `The raw response exceeded maxBytes=${maxBytes}. Only the downloaded prefix is included; totalChars measures that prefix after extraction, not the complete page. Increase maxBytes to fetch more, or request a focused page/API endpoint. Use maxChars to control returned text. Do not treat this as the complete page.`,
|
|
612
|
+
}
|
|
613
|
+
: {}),
|
|
360
614
|
};
|
|
361
|
-
if (
|
|
615
|
+
if (totalChars !== null) {
|
|
616
|
+
// Save the extracted document once, then read only the requested range
|
|
617
|
+
// from disk. Subsequent snapshot reads never contact the origin server.
|
|
618
|
+
const id = webFetchSnapshots.save(
|
|
619
|
+
result,
|
|
620
|
+
snapshotIdentity,
|
|
621
|
+
hostResourceBudget,
|
|
622
|
+
);
|
|
623
|
+
result = readSnapshot(id);
|
|
624
|
+
}
|
|
625
|
+
if (!downloadTruncated && cacheKey && hostResourceBudget?.putWebFetch) {
|
|
362
626
|
try {
|
|
363
627
|
hostResourceBudget.putWebFetch(cacheKey, result);
|
|
364
628
|
} catch {
|
|
@@ -366,6 +630,33 @@ export async function webFetch(url, options = {}) {
|
|
|
366
630
|
}
|
|
367
631
|
}
|
|
368
632
|
return result;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
const code = error.code || "ERR_WEB_FETCH";
|
|
635
|
+
return {
|
|
636
|
+
error: `web_fetch failed: ${error.message}`,
|
|
637
|
+
code,
|
|
638
|
+
...(code === "ERR_RESPONSE_TOO_LARGE"
|
|
639
|
+
? {
|
|
640
|
+
maxBytes,
|
|
641
|
+
receivedBytes: error.receivedBytes,
|
|
642
|
+
...(maxBytes < DEFAULT_MAX_BYTES
|
|
643
|
+
? { suggestedMaxBytes: DEFAULT_MAX_BYTES }
|
|
644
|
+
: {}),
|
|
645
|
+
}
|
|
646
|
+
: {}),
|
|
647
|
+
retryable: [
|
|
648
|
+
"ETIMEDOUT",
|
|
649
|
+
"ECONNRESET",
|
|
650
|
+
"ECONNREFUSED",
|
|
651
|
+
"EAI_AGAIN",
|
|
652
|
+
].includes(code),
|
|
653
|
+
hint:
|
|
654
|
+
code === "ERR_RESPONSE_TOO_LARGE"
|
|
655
|
+
? maxBytes < DEFAULT_MAX_BYTES
|
|
656
|
+
? `The raw response exceeded maxBytes=${maxBytes}, before text extraction. To limit returned text, use maxChars and omit maxBytes (default ${DEFAULT_MAX_BYTES}). Retry once with corrected limits, not the same small download limit.`
|
|
657
|
+
: "The raw response exceeded the download limit. Request a smaller resource or focused log excerpt, or explicitly raise maxBytes within the permitted resource budget. maxChars only limits the returned text."
|
|
658
|
+
: "Use the error code to check connectivity, DNS or access. Retry a transient failure at most once, then change approach or report the blocker.",
|
|
659
|
+
};
|
|
369
660
|
} finally {
|
|
370
661
|
toolLease?.release();
|
|
371
662
|
}
|
package/src/lib/web-search.js
CHANGED
|
@@ -15,7 +15,7 @@ import http from "http";
|
|
|
15
15
|
import https from "https";
|
|
16
16
|
import { URL } from "url";
|
|
17
17
|
|
|
18
|
-
const DEFAULT_MAX_BYTES =
|
|
18
|
+
const DEFAULT_MAX_BYTES = 10_000_000;
|
|
19
19
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
20
20
|
const DEFAULT_MAX_RESULTS = 8;
|
|
21
21
|
export const MAX_WEB_SEARCH_RESULTS = 20;
|
|
@@ -98,6 +98,15 @@ function _request(
|
|
|
98
98
|
const parsed = new URL(urlStr);
|
|
99
99
|
const lib = parsed.protocol === "https:" ? _deps.https : _deps.http;
|
|
100
100
|
return new Promise((resolve, reject) => {
|
|
101
|
+
let timer;
|
|
102
|
+
let settled = false;
|
|
103
|
+
const settle = (error, result) => {
|
|
104
|
+
if (settled) return;
|
|
105
|
+
settled = true;
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
if (error) reject(error);
|
|
108
|
+
else resolve(result);
|
|
109
|
+
};
|
|
101
110
|
const req = lib.request(
|
|
102
111
|
{
|
|
103
112
|
method,
|
|
@@ -113,28 +122,66 @@ function _request(
|
|
|
113
122
|
timeout,
|
|
114
123
|
},
|
|
115
124
|
(res) => {
|
|
125
|
+
if (res.statusCode >= 400) {
|
|
126
|
+
settle(
|
|
127
|
+
Object.assign(new Error(`HTTP ${res.statusCode}`), {
|
|
128
|
+
code: "ERR_HTTP_STATUS",
|
|
129
|
+
statusCode: res.statusCode,
|
|
130
|
+
retryAfter: res.headers?.["retry-after"],
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
res.destroy?.();
|
|
134
|
+
req.destroy();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
116
137
|
const chunks = [];
|
|
117
138
|
let size = 0;
|
|
118
139
|
res.on("data", (chunk) => {
|
|
140
|
+
if (settled) return;
|
|
119
141
|
size += chunk.length;
|
|
120
142
|
if (size > maxBytes) {
|
|
121
|
-
|
|
143
|
+
settle(
|
|
144
|
+
Object.assign(
|
|
145
|
+
new Error(`response exceeds maxBytes (${maxBytes})`),
|
|
146
|
+
{
|
|
147
|
+
code: "ERR_RESPONSE_TOO_LARGE",
|
|
148
|
+
},
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
res.destroy?.();
|
|
152
|
+
req.destroy();
|
|
122
153
|
return;
|
|
123
154
|
}
|
|
124
155
|
chunks.push(chunk);
|
|
125
156
|
});
|
|
126
157
|
res.on("end", () =>
|
|
127
|
-
|
|
158
|
+
settle(null, {
|
|
128
159
|
statusCode: res.statusCode,
|
|
129
160
|
headers: res.headers,
|
|
130
161
|
body: Buffer.concat(chunks).toString("utf8"),
|
|
131
162
|
}),
|
|
132
163
|
);
|
|
133
|
-
res.on("error",
|
|
164
|
+
res.on("error", (error) => settle(error));
|
|
165
|
+
res.on("aborted", () =>
|
|
166
|
+
settle(
|
|
167
|
+
Object.assign(new Error("response aborted"), {
|
|
168
|
+
code: "ECONNRESET",
|
|
169
|
+
}),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
134
172
|
},
|
|
135
173
|
);
|
|
136
|
-
|
|
137
|
-
|
|
174
|
+
const abortTimeout = () => {
|
|
175
|
+
settle(
|
|
176
|
+
Object.assign(new Error("request timeout"), { code: "ETIMEDOUT" }),
|
|
177
|
+
);
|
|
178
|
+
req.destroy();
|
|
179
|
+
};
|
|
180
|
+
req.on("error", (error) => settle(error));
|
|
181
|
+
req.on("timeout", abortTimeout);
|
|
182
|
+
// An overall deadline also bounds DNS and continuously streaming results.
|
|
183
|
+
timer = setTimeout(abortTimeout, timeout);
|
|
184
|
+
timer.unref?.();
|
|
138
185
|
if (body != null) req.write(body);
|
|
139
186
|
req.end();
|
|
140
187
|
});
|
|
@@ -285,6 +332,19 @@ async function _searchDuckDuckGo(query, { maxResults, timeout, maxBytes }) {
|
|
|
285
332
|
return { error: `duckduckgo HTTP ${res.statusCode}` };
|
|
286
333
|
}
|
|
287
334
|
const html = res.body;
|
|
335
|
+
if (
|
|
336
|
+
res.statusCode === 202 ||
|
|
337
|
+
(res.statusCode >= 300 && res.statusCode < 400) ||
|
|
338
|
+
/anomaly-modal|challenge-form|anomaly\.js|captcha/i.test(html)
|
|
339
|
+
) {
|
|
340
|
+
return {
|
|
341
|
+
error:
|
|
342
|
+
"duckduckgo: verification challenge; search results are unavailable",
|
|
343
|
+
code: "ERR_SEARCH_CHALLENGE",
|
|
344
|
+
retryable: false,
|
|
345
|
+
hint: "Do not report this as no results or repeat the same request. Configure a keyed search provider (Tavily, Brave, Bocha or Qianfan), or use your configured SearXNG instance.",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
288
348
|
const results = [];
|
|
289
349
|
const anchorRe =
|
|
290
350
|
/<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
@@ -453,8 +513,26 @@ export async function webSearch(query, options = {}) {
|
|
|
453
513
|
const config = options.config || {};
|
|
454
514
|
const provider = resolveProvider(options, config);
|
|
455
515
|
let maxResults = normalizeMaxResults(options.maxResults ?? config.maxResults);
|
|
456
|
-
const timeout =
|
|
457
|
-
const maxBytes =
|
|
516
|
+
const timeout = options.timeout ?? config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
517
|
+
const maxBytes = options.maxBytes ?? config.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
518
|
+
const maxSnippetChars =
|
|
519
|
+
options.maxSnippetChars ?? config.maxSnippetChars ?? 2000;
|
|
520
|
+
if (
|
|
521
|
+
!Number.isSafeInteger(timeout) ||
|
|
522
|
+
timeout <= 0 ||
|
|
523
|
+
timeout > 2_147_483_647 ||
|
|
524
|
+
!Number.isSafeInteger(maxBytes) ||
|
|
525
|
+
maxBytes <= 0 ||
|
|
526
|
+
!Number.isSafeInteger(maxSnippetChars) ||
|
|
527
|
+
maxSnippetChars <= 0 ||
|
|
528
|
+
maxSnippetChars > 10000
|
|
529
|
+
) {
|
|
530
|
+
return {
|
|
531
|
+
error: "web_search: invalid download, snippet or timeout limit",
|
|
532
|
+
code: "ERR_SEARCH_OPTIONS",
|
|
533
|
+
retryable: false,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
458
536
|
|
|
459
537
|
if (!SUPPORTED_PROVIDERS.includes(provider)) {
|
|
460
538
|
return {
|
|
@@ -533,17 +611,47 @@ export async function webSearch(query, options = {}) {
|
|
|
533
611
|
return {
|
|
534
612
|
error: `web_search (${provider}) failed: ${err.message}`,
|
|
535
613
|
provider,
|
|
614
|
+
code: err.code || "ERR_WEB_SEARCH",
|
|
615
|
+
...(err.statusCode ? { statusCode: err.statusCode } : {}),
|
|
616
|
+
...(err.retryAfter ? { retryAfter: String(err.retryAfter) } : {}),
|
|
617
|
+
retryable:
|
|
618
|
+
err.statusCode === 429 ||
|
|
619
|
+
err.statusCode >= 500 ||
|
|
620
|
+
["ETIMEDOUT", "ECONNRESET", "EAI_AGAIN"].includes(err.code),
|
|
621
|
+
hint:
|
|
622
|
+
err.code === "ERR_RESPONSE_TOO_LARGE"
|
|
623
|
+
? "Search response exceeds maxBytes. Raise maxBytes for a larger download; use maxResults and maxSnippetChars to limit returned results, not maxBytes."
|
|
624
|
+
: err.statusCode === 429
|
|
625
|
+
? "Honor Retry-After before a bounded retry. Do not repeat the same search immediately."
|
|
626
|
+
: "Check the search provider's configuration, API key and connectivity. Do not treat a provider failure as no results or invent source URLs.",
|
|
536
627
|
};
|
|
537
628
|
}
|
|
538
629
|
|
|
539
630
|
if (out && out.error) return { ...out, provider };
|
|
540
|
-
|
|
631
|
+
let truncated = false;
|
|
632
|
+
const bound = (value, limit) => {
|
|
633
|
+
const text = String(value || "");
|
|
634
|
+
if (text.length <= limit) return text;
|
|
635
|
+
truncated = true;
|
|
636
|
+
return text.slice(
|
|
637
|
+
0,
|
|
638
|
+
/[\uD800-\uDBFF]/.test(text[limit - 1]) ? limit - 1 : limit,
|
|
639
|
+
);
|
|
640
|
+
};
|
|
641
|
+
const results = ((out && out.results) || []).map((result) => ({
|
|
642
|
+
...result,
|
|
643
|
+
title: bound(result.title, 512),
|
|
644
|
+
snippet: bound(result.snippet, maxSnippetChars),
|
|
645
|
+
}));
|
|
646
|
+
const answer = bound(out?.answer, 8000);
|
|
541
647
|
return {
|
|
542
648
|
query: q,
|
|
543
649
|
provider,
|
|
544
650
|
count: results.length,
|
|
545
651
|
results,
|
|
546
|
-
answer
|
|
652
|
+
answer,
|
|
653
|
+
...(truncated ? { truncated: true, maxSnippetChars } : {}),
|
|
654
|
+
hint: "These are search results, not full webpages. Cite the returned source URLs. Use web_fetch only when a page needs closer reading; download it once, then continue with snapshotId and nextOffset as offset.",
|
|
547
655
|
};
|
|
548
656
|
} finally {
|
|
549
657
|
toolLease?.release();
|