jinzd-ai-cli 0.4.334 → 0.4.338
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{batch-6UJGEEVB.js → batch-N4DOEUEW.js} +2 -2
- package/dist/{chunk-W4P2KJZN.js → chunk-7BOHS5IX.js} +72 -0
- package/dist/{chunk-SZQ2C3HH.js → chunk-ATOF545W.js} +1 -1
- package/dist/{chunk-7GGWVBHG.js → chunk-DCWBDSMO.js} +2 -2
- package/dist/{chunk-RQOOEXDU.js → chunk-DOO4QAGJ.js} +2 -2
- package/dist/{chunk-XMHVHS2X.js → chunk-DRCN44SZ.js} +4 -4
- package/dist/{chunk-YTAYWGS4.js → chunk-JUYK4OML.js} +1 -1
- package/dist/{chunk-DPUZ22D7.js → chunk-KGHVSYGI.js} +27 -23
- package/dist/{chunk-EOYIIGBJ.js → chunk-NJREBH6A.js} +5 -5
- package/dist/{chunk-FHHBPCCZ.js → chunk-NRFAGZVJ.js} +1 -1
- package/dist/{chunk-YVEBRHSV.js → chunk-OXVJZLJM.js} +6 -5
- package/dist/{chunk-YY4QFBBZ.js → chunk-PKYNCSQC.js} +475 -240
- package/dist/{chunk-KSZEQADY.js → chunk-RIDNF4G2.js} +1 -1
- package/dist/{chunk-27RTBMY7.js → chunk-TTLLXFJC.js} +40 -3
- package/dist/{chunk-DX4Y4CRH.js → chunk-UWEBIOHD.js} +8 -20
- package/dist/{chunk-ZK5VCQD2.js → chunk-WQAUMAEV.js} +1 -1
- package/dist/{ci-S7WXRTZ6.js → ci-HFAPFMZY.js} +6 -6
- package/dist/{ci-format-TPCL2KWQ.js → ci-format-QTRMHY3N.js} +2 -2
- package/dist/{constants-WJAUJAQQ.js → constants-JZH2QKMQ.js} +1 -1
- package/dist/{constants-5VQMHBFU.js → constants-QJNJFMC6.js} +1 -1
- package/dist/{doctor-cli-DYQLNMPT.js → doctor-cli-FZ4IVZ6H.js} +6 -6
- package/dist/electron-server.js +622 -285
- package/dist/{hub-A7P2ZBYB.js → hub-IRILXTL3.js} +1 -1
- package/dist/index.js +25 -23
- package/dist/{persistent-memory-WFGBTGWT.js → persistent-memory-UZIZVHRT.js} +2 -2
- package/dist/{persistent-memory-SKGOCEGT.js → persistent-memory-VRBNWOWN.js} +2 -2
- package/dist/{pr-IPXLVP2K.js → pr-4KNQWYC6.js} +7 -7
- package/dist/{run-tests-BG6SGRWQ.js → run-tests-N2VKD7BU.js} +2 -2
- package/dist/{run-tests-TUTI7HIN.js → run-tests-UXEOFPAW.js} +2 -2
- package/dist/{server-4QJPQBEW.js → server-2VSO2ZX4.js} +6 -6
- package/dist/{server-S6OROC7S.js → server-HWKYWQEP.js} +21 -20
- package/dist/{task-orchestrator-6JG5GAXU.js → task-orchestrator-SC42LAFG.js} +6 -6
- package/dist/{usage-LPQQ6NGG.js → usage-ZVV27BHD.js} +2 -2
- package/dist/web/client/vendor/purify.es.mjs +272 -196
- package/dist/web/client/vendor/purify.es.mjs.br +0 -0
- package/dist/web/client/vendor/purify.es.mjs.gz +0 -0
- package/package.json +6 -3
package/dist/electron-server.js
CHANGED
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
touchMemoryReferences,
|
|
56
56
|
updateMemoryApproval,
|
|
57
57
|
updateMemoryEntry
|
|
58
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-JUYK4OML.js";
|
|
59
59
|
import {
|
|
60
60
|
DEFAULT_PATTERNS,
|
|
61
61
|
redactJson,
|
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
recoverGbkMojibake,
|
|
82
82
|
runTestsTool,
|
|
83
83
|
shellInvocation
|
|
84
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-DCWBDSMO.js";
|
|
85
85
|
import {
|
|
86
86
|
getCurrentSessionKey,
|
|
87
87
|
getExecutionConfigDir,
|
|
@@ -123,7 +123,7 @@ import {
|
|
|
123
123
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
124
124
|
VERSION,
|
|
125
125
|
buildUserIdentityPrompt
|
|
126
|
-
} from "./chunk-
|
|
126
|
+
} from "./chunk-ATOF545W.js";
|
|
127
127
|
|
|
128
128
|
// src/web/access.ts
|
|
129
129
|
import { randomBytes, timingSafeEqual } from "crypto";
|
|
@@ -262,32 +262,36 @@ var UserProfileSchema = z.object({
|
|
|
262
262
|
locale: z.string().max(20).optional(),
|
|
263
263
|
/** 自定义人设补充(自由格式,直接注入 system prompt) */
|
|
264
264
|
extra: z.string().max(5e3).optional()
|
|
265
|
-
}).
|
|
265
|
+
}).prefault({});
|
|
266
266
|
var ConfigSchema = z.object({
|
|
267
267
|
version: z.string().default("1.0.0"),
|
|
268
268
|
// 用户身份档案 — 跨 Provider 的 "灵魂"
|
|
269
269
|
userProfile: UserProfileSchema,
|
|
270
270
|
defaultProvider: z.string().default("claude"),
|
|
271
271
|
// 每个 provider 的默认模型(key 为 provider ID)
|
|
272
|
-
defaultModels: z.record(z.string()).default({}),
|
|
272
|
+
defaultModels: z.record(z.string(), z.string()).default({}),
|
|
273
273
|
// API Keys:放宽为任意 record,支持自定义 provider ID
|
|
274
|
-
apiKeys: z.record(z.string()).default({}),
|
|
275
|
-
customBaseUrls: z.record(z.string()).default({}),
|
|
274
|
+
apiKeys: z.record(z.string(), z.string()).default({}),
|
|
275
|
+
customBaseUrls: z.record(z.string(), z.string()).default({}),
|
|
276
276
|
// Per-provider timeout in ms (e.g. { deepseek: 60000 })
|
|
277
277
|
// ⚠️ Timeout 优先级说明(L5):
|
|
278
278
|
// 1. modelParams[modelId].timeout — 最高优先级,精确到具体模型(如 deepseek-reasoner 需要更长超时)
|
|
279
279
|
// 2. timeouts[providerId] — Provider 级默认,覆盖内置默认值
|
|
280
280
|
// 3. 内置 Provider 的硬编码默认值 — 最低兜底(如 OpenAI 兼容系列默认 60000ms)
|
|
281
281
|
// 实现位置:src/providers/registry.ts initialize(),将 timeouts[id] 注入 provider
|
|
282
|
-
timeouts: z.record(z.number()).default({}),
|
|
282
|
+
timeouts: z.record(z.string(), z.number()).default({}),
|
|
283
283
|
// HTTP/HTTPS 代理地址(Node.js 不自动使用系统代理,需显式配置)
|
|
284
284
|
// 例:http://127.0.0.1:10809
|
|
285
285
|
// 也可通过环境变量 HTTPS_PROXY / HTTP_PROXY 覆盖
|
|
286
286
|
proxy: z.string().max(500).optional(),
|
|
287
|
+
// 不走代理的 API 主机(后缀匹配,如 "deepseek.com" 也覆盖 api.deepseek.com;"*" = 全部直连)。
|
|
288
|
+
// 不填 = 内置国内模型清单(DeepSeek/智谱/Kimi/Qwen/MiniMax 直连,见 core/proxy.ts);
|
|
289
|
+
// 填 [] = 所有 Provider 都走代理(公司网络只能经代理出网时用)。
|
|
290
|
+
noProxy: z.array(z.string().max(200)).max(100).optional(),
|
|
287
291
|
// 自定义 Provider 列表(OpenAI 兼容接口,无需改代码)
|
|
288
292
|
customProviders: z.array(CustomProviderConfigSchema).default([]),
|
|
289
293
|
// 按模型 ID 存储的推理参数(key 为模型 ID,如 "deepseek-chat")
|
|
290
|
-
modelParams: z.record(ModelParamsSchema).default({}),
|
|
294
|
+
modelParams: z.record(z.string(), ModelParamsSchema).default({}),
|
|
291
295
|
ui: z.object({
|
|
292
296
|
streaming: z.boolean().default(true),
|
|
293
297
|
markdownRendering: z.boolean().default(true),
|
|
@@ -321,12 +325,12 @@ var ConfigSchema = z.object({
|
|
|
321
325
|
toolResult: z.string().optional(),
|
|
322
326
|
heading: z.string().optional()
|
|
323
327
|
}).optional()
|
|
324
|
-
}).
|
|
328
|
+
}).prefault({}),
|
|
325
329
|
session: z.object({
|
|
326
330
|
autoSave: z.boolean().default(true),
|
|
327
331
|
maxHistoryDays: z.number().default(30),
|
|
328
332
|
systemPrompt: z.string().max(1e5).optional()
|
|
329
|
-
}).
|
|
333
|
+
}).prefault({}),
|
|
330
334
|
// 项目上下文文件配置
|
|
331
335
|
// 启动时自动读取并注入 system prompt,类似 Claude Code 的 CLAUDE.md / Codex AGENTS.md 机制
|
|
332
336
|
// 默认按顺序查找:AICLI.override.md → AGENTS.override.md → AICLI.md → CLAUDE.md → AGENTS.md
|
|
@@ -336,13 +340,13 @@ var ConfigSchema = z.object({
|
|
|
336
340
|
projectDocFallbackFilenames: z.array(z.string().max(500)).max(20).default([]),
|
|
337
341
|
projectDocMaxBytes: z.number().int().positive().default(32 * 1024),
|
|
338
342
|
showLoadedContextSources: z.boolean().default(false)
|
|
339
|
-
}).
|
|
343
|
+
}).prefault({}),
|
|
340
344
|
// 持久记忆注入配置(MEM-005)
|
|
341
345
|
// maxChars: 注入 system prompt 的字符预算,超出丢最老条目并显式标注截断。
|
|
342
346
|
// 注意 CJK ≈ 1 字符/token——中文记忆的 token 开销约等于字符数。
|
|
343
347
|
memory: z.object({
|
|
344
348
|
maxChars: z.number().int().positive().max(2e5).default(1e4)
|
|
345
|
-
}).
|
|
349
|
+
}).prefault({}),
|
|
346
350
|
// Google Custom Search API 的 Search Engine ID (cx 参数)
|
|
347
351
|
// API Key 通过 apiKeys['google-search'] 或 AICLI_API_KEY_GOOGLESEARCH 环境变量配置
|
|
348
352
|
// CX 也可通过 AICLI_GOOGLE_CX 环境变量覆盖
|
|
@@ -354,7 +358,7 @@ var ConfigSchema = z.object({
|
|
|
354
358
|
// 仍想用 API 版?把 enabled 设为 true,并务必到 GCP Console 把 Custom Search Quota 设硬上限。
|
|
355
359
|
googleSearch: z.object({
|
|
356
360
|
enabled: z.boolean().default(false)
|
|
357
|
-
}).
|
|
361
|
+
}).prefault({ enabled: false }),
|
|
358
362
|
// MCP (Model Context Protocol) 全局开关(v0.4.135+)
|
|
359
363
|
// false 时启动跳过 connectAll(),所有 MCP 服务器都不连,节省冷启动时间 + 工具定义 token。
|
|
360
364
|
// 对应你的 skills(whitelist 不依赖 MCP 工具时)完全不受影响。
|
|
@@ -363,10 +367,10 @@ var ConfigSchema = z.object({
|
|
|
363
367
|
// 声明外部 MCP 服务器,启动时自动连接、发现工具并注册
|
|
364
368
|
// 配置格式兼容 Claude Desktop(command + args + env)
|
|
365
369
|
// 示例:{ "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"] } }
|
|
366
|
-
mcpServers: z.record(z.object({
|
|
370
|
+
mcpServers: z.record(z.string(), z.object({
|
|
367
371
|
command: z.string().max(500),
|
|
368
372
|
args: z.array(z.string()).max(100).default([]),
|
|
369
|
-
env: z.record(z.string()).optional(),
|
|
373
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
370
374
|
timeout: z.number().int().min(100).max(3e5).default(3e4)
|
|
371
375
|
})).default({}),
|
|
372
376
|
// Hooks 生命周期(v0.4.212+):兼容旧 pre/post 字符串,同时支持 events.<EventName> 结构化 JSON hook。
|
|
@@ -374,7 +378,7 @@ var ConfigSchema = z.object({
|
|
|
374
378
|
enabled: z.boolean().default(true),
|
|
375
379
|
preToolExecution: z.string().optional(),
|
|
376
380
|
postToolExecution: z.string().optional(),
|
|
377
|
-
events: z.record(z.union([
|
|
381
|
+
events: z.record(z.string(), z.union([
|
|
378
382
|
z.string(),
|
|
379
383
|
z.object({
|
|
380
384
|
command: z.string(),
|
|
@@ -412,12 +416,12 @@ var ConfigSchema = z.object({
|
|
|
412
416
|
google_search: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
413
417
|
shell: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
414
418
|
mcp: z.enum(["allow", "confirm", "deny"]).optional()
|
|
415
|
-
}).
|
|
416
|
-
}).
|
|
419
|
+
}).prefault({})
|
|
420
|
+
}).prefault({ enabled: false, defaultAction: "confirm", allowDomains: [], denyDomains: [], allowPorts: [], denyPorts: [], allowPrivateNetwork: false, tools: {} }),
|
|
417
421
|
// 权限 Profile(v0.4.211+):legacy 保持旧行为;其余 profile 提供更清晰的安全边界。
|
|
418
422
|
defaultPermissionProfile: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).default("legacy"),
|
|
419
423
|
allowedPermissionProfiles: z.array(z.string().max(50)).max(20).default(["legacy", "read-only", "workspace-write", "danger-full-access"]),
|
|
420
|
-
permissionProfiles: z.record(z.object({
|
|
424
|
+
permissionProfiles: z.record(z.string(), z.object({
|
|
421
425
|
extends: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).optional(),
|
|
422
426
|
defaultAction: z.enum(["auto-approve", "deny", "confirm"]).optional(),
|
|
423
427
|
workspaceRoots: z.array(z.string()).optional(),
|
|
@@ -492,7 +496,7 @@ var ConfigSchema = z.object({
|
|
|
492
496
|
redactOnSend: z.boolean().default(false),
|
|
493
497
|
mode: z.enum(["default", "strict", "off"]).default("default"),
|
|
494
498
|
customPatterns: z.array(z.string().max(200)).max(50).default([])
|
|
495
|
-
}).
|
|
499
|
+
}).prefault({}),
|
|
496
500
|
// 智能模型路由(v0.4.68+)
|
|
497
501
|
// 按用户每轮输入的内容/标签/长度动态选择模型,在同一 provider 内切换,
|
|
498
502
|
// 例:短问题走 haiku(省钱),planning 走 opus(质量)。
|
|
@@ -512,7 +516,7 @@ var ConfigSchema = z.object({
|
|
|
512
516
|
name: z.string().max(100).optional()
|
|
513
517
|
})).default([]),
|
|
514
518
|
fallback: z.string().optional()
|
|
515
|
-
}).
|
|
519
|
+
}).prefault({ enabled: false, rules: [] }),
|
|
516
520
|
// Provider 容错(v0.4.144+):网络抖动 / 5xx / 429 时,先在当前 provider 上做指数退避重试,
|
|
517
521
|
// 仍失败则按顺序尝试 chain 里的 fallback provider。auth / 400 / 422 等"硬失败"直接跳过同
|
|
518
522
|
// provider 重试,但仍会尝试 chain(不同 key/schema 可能能成功)。
|
|
@@ -532,7 +536,7 @@ var ConfigSchema = z.object({
|
|
|
532
536
|
provider: z.string().max(100),
|
|
533
537
|
model: z.string().optional()
|
|
534
538
|
})).default([])
|
|
535
|
-
}).
|
|
539
|
+
}).prefault({ enabled: false, retries: 2, initialBackoffMs: 500, maxBackoffMs: 8e3, chain: [] })
|
|
536
540
|
});
|
|
537
541
|
|
|
538
542
|
// src/config/env-loader.ts
|
|
@@ -806,7 +810,7 @@ ${err}`
|
|
|
806
810
|
const draft = { ...this.config, [key]: value };
|
|
807
811
|
const result = ConfigSchema.safeParse(draft);
|
|
808
812
|
if (!result.success) {
|
|
809
|
-
const firstErr = result.error.
|
|
813
|
+
const firstErr = result.error.issues[0];
|
|
810
814
|
throw new ConfigError(
|
|
811
815
|
`Invalid transient config value for "${String(key)}": ${firstErr?.message ?? "validation failed"}`
|
|
812
816
|
);
|
|
@@ -858,7 +862,7 @@ ${err}`
|
|
|
858
862
|
current[keys[keys.length - 1]] = value;
|
|
859
863
|
const result = ConfigSchema.safeParse(draft);
|
|
860
864
|
if (!result.success) {
|
|
861
|
-
const firstErr = result.error.
|
|
865
|
+
const firstErr = result.error.issues[0];
|
|
862
866
|
throw new ConfigError(`Invalid config value for "${path3}": ${firstErr?.message ?? "validation failed"}`);
|
|
863
867
|
}
|
|
864
868
|
this.config = result.data;
|
|
@@ -1479,6 +1483,77 @@ var BaseProvider = class {
|
|
|
1479
1483
|
function isFileWriteTool(name) {
|
|
1480
1484
|
return name === "write_file" || name === "edit_file" || name === "notebook_edit";
|
|
1481
1485
|
}
|
|
1486
|
+
var READ_ONLY_CMDLETS = new Set([
|
|
1487
|
+
"Get-ChildItem",
|
|
1488
|
+
"gci",
|
|
1489
|
+
"Get-Content",
|
|
1490
|
+
"gc",
|
|
1491
|
+
"Get-Item",
|
|
1492
|
+
"gi",
|
|
1493
|
+
"Get-ItemProperty",
|
|
1494
|
+
"gp",
|
|
1495
|
+
"Get-ItemPropertyValue",
|
|
1496
|
+
"Get-Location",
|
|
1497
|
+
"gl",
|
|
1498
|
+
"Test-Path",
|
|
1499
|
+
"Resolve-Path",
|
|
1500
|
+
"Split-Path",
|
|
1501
|
+
"Join-Path",
|
|
1502
|
+
"Get-Command",
|
|
1503
|
+
"gcm",
|
|
1504
|
+
"Get-Service",
|
|
1505
|
+
"gsv",
|
|
1506
|
+
"Get-Process",
|
|
1507
|
+
"gps",
|
|
1508
|
+
"Get-Date",
|
|
1509
|
+
"Get-Alias",
|
|
1510
|
+
"Get-Module",
|
|
1511
|
+
"Get-PSDrive",
|
|
1512
|
+
"Get-Acl",
|
|
1513
|
+
"Get-FileHash",
|
|
1514
|
+
"Get-CimInstance",
|
|
1515
|
+
"Get-NetTCPConnection",
|
|
1516
|
+
"Get-NetIPAddress",
|
|
1517
|
+
"Get-WinEvent",
|
|
1518
|
+
"Get-ComputerInfo",
|
|
1519
|
+
"Get-Culture",
|
|
1520
|
+
"Get-ExecutionPolicy",
|
|
1521
|
+
"Get-Unique",
|
|
1522
|
+
"Select-Object",
|
|
1523
|
+
"select",
|
|
1524
|
+
"Where-Object",
|
|
1525
|
+
"Sort-Object",
|
|
1526
|
+
"Measure-Object",
|
|
1527
|
+
"Group-Object",
|
|
1528
|
+
"Compare-Object",
|
|
1529
|
+
"Select-String",
|
|
1530
|
+
"sls",
|
|
1531
|
+
"Format-Table",
|
|
1532
|
+
"ft",
|
|
1533
|
+
"Format-List",
|
|
1534
|
+
"fl",
|
|
1535
|
+
"Format-Wide",
|
|
1536
|
+
"Out-String",
|
|
1537
|
+
"Write-Output",
|
|
1538
|
+
"Write-Host",
|
|
1539
|
+
// Waiting is harmless on its own; chained segments still have to pass this list (v0.4.338).
|
|
1540
|
+
"Start-Sleep",
|
|
1541
|
+
// ls is read-only both as the PowerShell alias and as the Unix binary; models pair it with
|
|
1542
|
+
// Start-Sleep to poll an output folder (2026-11-05 lecture-video run).
|
|
1543
|
+
"ls",
|
|
1544
|
+
"ConvertTo-Json",
|
|
1545
|
+
"ConvertFrom-Json",
|
|
1546
|
+
"ConvertTo-Csv",
|
|
1547
|
+
"ConvertFrom-Csv"
|
|
1548
|
+
].map((name) => name.toLowerCase()));
|
|
1549
|
+
function isReadOnlyCmdletChain(cmd) {
|
|
1550
|
+
const normalized = cmd.trim().replace(/\s2>\s*\$null\b/gi, " ").replace(/\s2>&1\b/g, " ").replace(/\$(?:env:\w+|null|true|false)\b/gi, "X");
|
|
1551
|
+
if (!normalized || /[&`$<>\n\r(){}]|::/.test(normalized)) return false;
|
|
1552
|
+
return normalized.split(/[;|]/).every((segment) => {
|
|
1553
|
+
const name = segment.trim().split(/\s+/)[0].toLowerCase();
|
|
1554
|
+
return READ_ONLY_CMDLETS.has(name);
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1482
1557
|
function getDangerLevel(toolName, args) {
|
|
1483
1558
|
if (toolName.startsWith("mcp__")) {
|
|
1484
1559
|
const rawName = toolName.split("__").pop() ?? "";
|
|
@@ -1508,6 +1583,7 @@ function getDangerLevel(toolName, args) {
|
|
|
1508
1583
|
if (/\b(python3?|node|deno|bun|perl|ruby|php|bash|sh|zsh|pwsh|powershell)(\.exe)?\b[^\n]*?\s-(?:Command|EncodedCommand|c|e|p|r)\b/i.test(cmd)) return "write";
|
|
1509
1584
|
if (/\bcmd(\.exe)?\s+\/[ck]\b/i.test(cmd)) return "write";
|
|
1510
1585
|
if (!/[;&|`$<>\n\r(){}]/.test(cmd) && (/^(?:pwd|ls|cat|head|tail|wc|Get-ChildItem|Get-Location|Get-Content)(?:\s+[\w ./\\:*?=,@'"-]+)?\s*$/i.test(cmd.trim()) || /^echo(?:\s+[\w .,'"-]+)?\s*$/.test(cmd.trim()) || /^(?:node|python3?)\s+(?:-v|--version)\s*$/.test(cmd.trim()) || /^git\s+(?:status|log)(?:\s+(?:--short|--porcelain|--oneline|-\d+))*\s*$/.test(cmd.trim()))) return "safe";
|
|
1586
|
+
if (isReadOnlyCmdletChain(cmd)) return "safe";
|
|
1511
1587
|
return "write";
|
|
1512
1588
|
}
|
|
1513
1589
|
if (toolName === "run_tests") return "write";
|
|
@@ -2568,6 +2644,41 @@ function readReasoningContent(message) {
|
|
|
2568
2644
|
return typeof value === "string" ? value : void 0;
|
|
2569
2645
|
}
|
|
2570
2646
|
|
|
2647
|
+
// src/core/proxy.ts
|
|
2648
|
+
var DEFAULT_NO_PROXY_HOSTS = [
|
|
2649
|
+
"deepseek.com",
|
|
2650
|
+
"bigmodel.cn",
|
|
2651
|
+
"moonshot.cn",
|
|
2652
|
+
"moonshot.ai",
|
|
2653
|
+
"aliyuncs.com",
|
|
2654
|
+
"minimaxi.com"
|
|
2655
|
+
];
|
|
2656
|
+
function shouldBypassProxy(baseUrl, noProxy = DEFAULT_NO_PROXY_HOSTS) {
|
|
2657
|
+
if (!baseUrl) return false;
|
|
2658
|
+
let host;
|
|
2659
|
+
try {
|
|
2660
|
+
host = new URL(baseUrl).hostname.toLowerCase();
|
|
2661
|
+
} catch {
|
|
2662
|
+
return false;
|
|
2663
|
+
}
|
|
2664
|
+
if (!host) return false;
|
|
2665
|
+
return noProxy.some((raw) => {
|
|
2666
|
+
const entry = raw.trim().toLowerCase().replace(/^\*?\./, "");
|
|
2667
|
+
if (!entry) return false;
|
|
2668
|
+
if (entry === "*") return true;
|
|
2669
|
+
return host === entry || host.endsWith("." + entry);
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
async function setupProxy(configProxy) {
|
|
2673
|
+
const proxyUrl = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.https_proxy ?? process.env.http_proxy ?? configProxy;
|
|
2674
|
+
if (!proxyUrl) return;
|
|
2675
|
+
try {
|
|
2676
|
+
const { ProxyAgent, setGlobalDispatcher } = await import("undici");
|
|
2677
|
+
setGlobalDispatcher(new ProxyAgent({ uri: proxyUrl }));
|
|
2678
|
+
} catch {
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2571
2682
|
// src/tools/hallucination.ts
|
|
2572
2683
|
var HALLUCINATION_PATTERNS = [
|
|
2573
2684
|
/文件路径[::]\s*`?[^\s`]+\.\w{1,5}/,
|
|
@@ -3901,7 +4012,8 @@ var OpenAICompatibleProvider = class extends BaseProvider {
|
|
|
3901
4012
|
timeout: this.defaultTimeout,
|
|
3902
4013
|
maxRetries
|
|
3903
4014
|
};
|
|
3904
|
-
const
|
|
4015
|
+
const noProxy = Array.isArray(options?.noProxy) ? options.noProxy : DEFAULT_NO_PROXY_HOSTS;
|
|
4016
|
+
const proxyUrl = isLoopbackBaseUrl(clientOptions.baseURL) || shouldBypassProxy(clientOptions.baseURL, noProxy) ? void 0 : options?.proxy;
|
|
3905
4017
|
try {
|
|
3906
4018
|
const { Agent: Agent2, ProxyAgent, fetch: undiciFetch } = await import("undici");
|
|
3907
4019
|
const STREAM_BODY_TIMEOUT = 30 * 60 * 1e3;
|
|
@@ -8361,6 +8473,14 @@ function domainMatches(pattern, host) {
|
|
|
8361
8473
|
const h = host.trim().toLowerCase().replace(/:\d+$/, "");
|
|
8362
8474
|
return h === p || h.endsWith(`.${p}`);
|
|
8363
8475
|
}
|
|
8476
|
+
function isHostAllowedByNetworkPolicy(host, policy) {
|
|
8477
|
+
if (!policy?.enabled) return true;
|
|
8478
|
+
if (policy.denyDomains?.some((pattern) => domainMatches(pattern, host))) return false;
|
|
8479
|
+
if (policy.allowDomains && policy.allowDomains.length > 0) {
|
|
8480
|
+
return policy.allowDomains.some((pattern) => domainMatches(pattern, host));
|
|
8481
|
+
}
|
|
8482
|
+
return true;
|
|
8483
|
+
}
|
|
8364
8484
|
function isPrivateNetworkHost(host) {
|
|
8365
8485
|
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
8366
8486
|
if (h === "" || h === "localhost") return true;
|
|
@@ -10720,6 +10840,231 @@ ${stderr}`);
|
|
|
10720
10840
|
import { promises as dnsPromises } from "dns";
|
|
10721
10841
|
import net from "net";
|
|
10722
10842
|
import { Agent } from "undici";
|
|
10843
|
+
|
|
10844
|
+
// src/tools/builtin/google-search.ts
|
|
10845
|
+
var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
|
|
10846
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
10847
|
+
var MAX_RESULTS = 10;
|
|
10848
|
+
var DEFAULT_RESULTS = 5;
|
|
10849
|
+
var googleSearchContext = {};
|
|
10850
|
+
var googleSearchTool = {
|
|
10851
|
+
definition: {
|
|
10852
|
+
name: "google_search",
|
|
10853
|
+
description: "\u26A0\uFE0F NOT RECOMMENDED \u2014 uses Google Custom Search API which requires a paid API key. API keys have leaked in the wild causing tens of thousands of USD in unauthorized charges. PREFER `web_search` (keyless Bing/Google scraping, zero billing risk). This tool is disabled by default; user must set config.googleSearch.enabled=true to use it.",
|
|
10854
|
+
parameters: {
|
|
10855
|
+
query: {
|
|
10856
|
+
type: "string",
|
|
10857
|
+
description: "The search query string.",
|
|
10858
|
+
required: true
|
|
10859
|
+
},
|
|
10860
|
+
num_results: {
|
|
10861
|
+
type: "number",
|
|
10862
|
+
description: "Number of results to return (1-10, default 5).",
|
|
10863
|
+
required: false
|
|
10864
|
+
}
|
|
10865
|
+
},
|
|
10866
|
+
dangerous: false
|
|
10867
|
+
},
|
|
10868
|
+
async execute(args) {
|
|
10869
|
+
const query = String(args["query"] ?? "").trim();
|
|
10870
|
+
if (!query) throw new ToolError("google_search", "query parameter is required");
|
|
10871
|
+
const enabledFlag = googleSearchContext.configManager?.get("googleSearch")?.enabled ?? false;
|
|
10872
|
+
if (!enabledFlag) {
|
|
10873
|
+
throw new ToolError(
|
|
10874
|
+
"google_search",
|
|
10875
|
+
'\u{1F6AB} google_search is disabled by default (v0.4.135+).\n\nWHY: Google Custom Search has no native daily-spend cap. Keys leak (search GitHub for "AIza" \u2014 thousands of public exposures), bots scrape them within hours, and the GCP bill lands days later. Multiple engineers have lost $10k+ USD this way.\n\nRECOMMENDED: use `web_search` instead \u2014 same use case, scrapes Bing/Google public pages, zero API key, zero billing risk.\n\nIF YOU INSIST: edit ~/.aicli/config.json and set `googleSearch.enabled = true`. ALSO go to GCP Console \u2192 Custom Search API \u2192 Quotas and set a hard daily request cap (e.g. 100/day) \u2014 that is the only real damage cap.'
|
|
10876
|
+
);
|
|
10877
|
+
}
|
|
10878
|
+
const numResults = Math.min(
|
|
10879
|
+
Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
|
|
10880
|
+
MAX_RESULTS
|
|
10881
|
+
);
|
|
10882
|
+
const { apiKey, cx } = resolveConfig();
|
|
10883
|
+
const url = new URL(GOOGLE_SEARCH_API);
|
|
10884
|
+
url.searchParams.set("key", apiKey);
|
|
10885
|
+
url.searchParams.set("cx", cx);
|
|
10886
|
+
url.searchParams.set("q", query);
|
|
10887
|
+
url.searchParams.set("num", String(numResults));
|
|
10888
|
+
const controller = new AbortController();
|
|
10889
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
10890
|
+
try {
|
|
10891
|
+
const response = await fetch(url.toString(), {
|
|
10892
|
+
method: "GET",
|
|
10893
|
+
headers: {
|
|
10894
|
+
"Accept": "application/json"
|
|
10895
|
+
},
|
|
10896
|
+
signal: controller.signal
|
|
10897
|
+
});
|
|
10898
|
+
if (!response.ok) {
|
|
10899
|
+
const errorBody = await response.text().catch(() => "");
|
|
10900
|
+
if (response.status === 403) {
|
|
10901
|
+
throw new NetworkError(
|
|
10902
|
+
`Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
|
|
10903
|
+
Please check your API Key and Search Engine ID configuration.`,
|
|
10904
|
+
403
|
|
10905
|
+
);
|
|
10906
|
+
}
|
|
10907
|
+
if (response.status === 429) {
|
|
10908
|
+
throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
|
|
10909
|
+
}
|
|
10910
|
+
throw new NetworkError(
|
|
10911
|
+
`Google Search API error: HTTP ${response.status} ${response.statusText}
|
|
10912
|
+
${errorBody.slice(0, 500)}`,
|
|
10913
|
+
response.status
|
|
10914
|
+
);
|
|
10915
|
+
}
|
|
10916
|
+
const data = await response.json();
|
|
10917
|
+
return formatResults(query, data, numResults);
|
|
10918
|
+
} catch (err) {
|
|
10919
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
10920
|
+
throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
|
|
10921
|
+
}
|
|
10922
|
+
throw err;
|
|
10923
|
+
} finally {
|
|
10924
|
+
clearTimeout(timeout);
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
};
|
|
10928
|
+
function resolveConfig() {
|
|
10929
|
+
let apiKey;
|
|
10930
|
+
let cx;
|
|
10931
|
+
if (googleSearchContext.configManager) {
|
|
10932
|
+
apiKey = googleSearchContext.configManager.getApiKey("google-search");
|
|
10933
|
+
cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
|
|
10934
|
+
} else {
|
|
10935
|
+
apiKey = EnvLoader.getApiKey("google-search");
|
|
10936
|
+
cx = EnvLoader.getGoogleSearchEngineId();
|
|
10937
|
+
}
|
|
10938
|
+
if (!apiKey) {
|
|
10939
|
+
throw new ToolError(
|
|
10940
|
+
"google_search",
|
|
10941
|
+
'Google Search API Key not configured.\nConfigure via one of:\n 1. Run /config \u2192 Configure Google Search\n 2. Set env var AICLI_API_KEY_GOOGLESEARCH\n 3. Add apiKeys["google-search"] to ~/.aicli/config.json'
|
|
10942
|
+
);
|
|
10943
|
+
}
|
|
10944
|
+
if (!cx) {
|
|
10945
|
+
throw new ToolError(
|
|
10946
|
+
"google_search",
|
|
10947
|
+
"Google Search Engine ID (cx) not configured.\nConfigure via one of:\n 1. Run /config \u2192 Configure Google Search\n 2. Set env var AICLI_GOOGLE_CX\n 3. Add googleSearchEngineId to ~/.aicli/config.json\n\nGet one at: https://programmablesearchengine.google.com/ \u2192 Create search engine \u2192 Copy Search Engine ID"
|
|
10948
|
+
);
|
|
10949
|
+
}
|
|
10950
|
+
return { apiKey, cx };
|
|
10951
|
+
}
|
|
10952
|
+
function formatResults(query, data, _requested) {
|
|
10953
|
+
const items = data.items ?? [];
|
|
10954
|
+
if (items.length === 0) {
|
|
10955
|
+
const info2 = data.searchInformation;
|
|
10956
|
+
return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
|
|
10957
|
+
}
|
|
10958
|
+
const info = data.searchInformation;
|
|
10959
|
+
const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
|
|
10960
|
+
`;
|
|
10961
|
+
const results = items.map((item, i) => {
|
|
10962
|
+
const title = item.title ?? "Untitled";
|
|
10963
|
+
const link = item.link ?? "";
|
|
10964
|
+
const snippet = item.snippet ?? "";
|
|
10965
|
+
return `${i + 1}. **${title}**
|
|
10966
|
+
URL: ${link}
|
|
10967
|
+
${snippet}`;
|
|
10968
|
+
});
|
|
10969
|
+
return header + "\n" + results.join("\n\n");
|
|
10970
|
+
}
|
|
10971
|
+
|
|
10972
|
+
// src/tools/builtin/deepseek-web-api.ts
|
|
10973
|
+
var DEEPSEEK_TOOLS_HOST = "api.deepseek.com";
|
|
10974
|
+
var BASE_URL = `https://${DEEPSEEK_TOOLS_HOST}/v1/tools`;
|
|
10975
|
+
var SEARCH_TIMEOUT_MS = 2e4;
|
|
10976
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
10977
|
+
var DEEPSEEK_MAX_QUERY_CHARS = 128;
|
|
10978
|
+
function resolveDeepSeekWebKey(configManager = googleSearchContext.configManager) {
|
|
10979
|
+
const key = configManager ? configManager.getApiKey("deepseek") : EnvLoader.getApiKey("deepseek");
|
|
10980
|
+
if (!key || !key.trim()) return void 0;
|
|
10981
|
+
const policy = configManager?.get("networkPolicy");
|
|
10982
|
+
if (!isHostAllowedByNetworkPolicy(DEEPSEEK_TOOLS_HOST, policy)) return void 0;
|
|
10983
|
+
return key.trim();
|
|
10984
|
+
}
|
|
10985
|
+
async function deepseekSearch(apiKey, query, count) {
|
|
10986
|
+
const data = await postJson(apiKey, "search", { query, count }, SEARCH_TIMEOUT_MS);
|
|
10987
|
+
return parseDeepSeekSearchResponse(data);
|
|
10988
|
+
}
|
|
10989
|
+
async function deepseekFetch(apiKey, url) {
|
|
10990
|
+
const data = await postJson(apiKey, "fetch", { url }, FETCH_TIMEOUT_MS);
|
|
10991
|
+
const content = typeof data?.content === "string" ? data.content : "";
|
|
10992
|
+
if (!content.trim()) throw new NetworkError("DeepSeek Fetch returned no content.");
|
|
10993
|
+
return { title: typeof data.title === "string" ? data.title : "", content };
|
|
10994
|
+
}
|
|
10995
|
+
function parseDeepSeekSearchResponse(data) {
|
|
10996
|
+
const raw = data?.results;
|
|
10997
|
+
if (!Array.isArray(raw)) {
|
|
10998
|
+
throw new NetworkError("DeepSeek Search returned an unexpected response (no results array).");
|
|
10999
|
+
}
|
|
11000
|
+
const out = [];
|
|
11001
|
+
for (const item of raw) {
|
|
11002
|
+
if (!item || typeof item !== "object") continue;
|
|
11003
|
+
const r = item;
|
|
11004
|
+
const url = typeof r["url"] === "string" ? r["url"] : "";
|
|
11005
|
+
const title = typeof r["title"] === "string" ? r["title"].trim() : "";
|
|
11006
|
+
if (!url || !title) continue;
|
|
11007
|
+
out.push({
|
|
11008
|
+
title,
|
|
11009
|
+
url,
|
|
11010
|
+
snippet: typeof r["snippet"] === "string" ? r["snippet"].trim() : "",
|
|
11011
|
+
summary: typeof r["summary"] === "string" ? r["summary"].trim() : "",
|
|
11012
|
+
siteName: typeof r["site_name"] === "string" && r["site_name"] ? r["site_name"] : null,
|
|
11013
|
+
date: typeof r["date"] === "string" && r["date"] ? r["date"] : null,
|
|
11014
|
+
rerankScore: typeof r["rerank_score"] === "number" ? r["rerank_score"] : null,
|
|
11015
|
+
authority: typeof r["authority"] === "number" ? r["authority"] : null
|
|
11016
|
+
});
|
|
11017
|
+
}
|
|
11018
|
+
return out;
|
|
11019
|
+
}
|
|
11020
|
+
async function postJson(apiKey, endpoint, body, timeoutMs) {
|
|
11021
|
+
const controller = new AbortController();
|
|
11022
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11023
|
+
try {
|
|
11024
|
+
const res = await fetch(`${BASE_URL}/${endpoint}`, {
|
|
11025
|
+
method: "POST",
|
|
11026
|
+
headers: {
|
|
11027
|
+
Authorization: `Bearer ${apiKey}`,
|
|
11028
|
+
"Content-Type": "application/json",
|
|
11029
|
+
Accept: "application/json"
|
|
11030
|
+
},
|
|
11031
|
+
body: JSON.stringify(body),
|
|
11032
|
+
signal: controller.signal
|
|
11033
|
+
});
|
|
11034
|
+
const text = await res.text();
|
|
11035
|
+
if (!res.ok) {
|
|
11036
|
+
throw new NetworkError(
|
|
11037
|
+
`DeepSeek ${endpoint} HTTP ${res.status}: ${describeDeepSeekError(res.status, text)}`,
|
|
11038
|
+
res.status
|
|
11039
|
+
);
|
|
11040
|
+
}
|
|
11041
|
+
try {
|
|
11042
|
+
return JSON.parse(text);
|
|
11043
|
+
} catch {
|
|
11044
|
+
throw new NetworkError(`DeepSeek ${endpoint} returned non-JSON response.`);
|
|
11045
|
+
}
|
|
11046
|
+
} catch (err) {
|
|
11047
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
11048
|
+
throw new NetworkError(`DeepSeek ${endpoint} timed out after ${timeoutMs / 1e3}s.`, void 0, err);
|
|
11049
|
+
}
|
|
11050
|
+
throw err;
|
|
11051
|
+
} finally {
|
|
11052
|
+
clearTimeout(timer);
|
|
11053
|
+
}
|
|
11054
|
+
}
|
|
11055
|
+
function describeDeepSeekError(status, bodyText) {
|
|
11056
|
+
let message = "";
|
|
11057
|
+
try {
|
|
11058
|
+
const parsed = JSON.parse(bodyText);
|
|
11059
|
+
if (typeof parsed?.error?.message === "string") message = parsed.error.message;
|
|
11060
|
+
} catch {
|
|
11061
|
+
}
|
|
11062
|
+
if (!message) message = bodyText.trim().slice(0, 200) || "(empty body)";
|
|
11063
|
+
const hint = status === 401 ? " \u2014 DeepSeek API key missing or invalid" : status === 402 ? " \u2014 DeepSeek account balance is insufficient" : status === 403 ? " \u2014 rejected by DeepSeek security policy" : status === 404 ? " \u2014 no usable content at this URL" : status === 429 ? " \u2014 DeepSeek QPS limit exceeded" : "";
|
|
11064
|
+
return message + hint;
|
|
11065
|
+
}
|
|
11066
|
+
|
|
11067
|
+
// src/tools/builtin/web-fetch.ts
|
|
10723
11068
|
function htmlToText(html) {
|
|
10724
11069
|
let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<svg[\s\S]*?<\/svg>/gi, "");
|
|
10725
11070
|
const HTML_REGEX_LIMIT = 2e5;
|
|
@@ -10892,7 +11237,7 @@ function ssrfSafeDispatcher() {
|
|
|
10892
11237
|
var webFetchTool = {
|
|
10893
11238
|
definition: {
|
|
10894
11239
|
name: "web_fetch",
|
|
10895
|
-
description: "Fetch a URL and return its content as plain text / Markdown. Use this to read documentation, API references, READMEs, articles, or any public web page. Follows redirects automatically. Returns the first ~16000 characters of extracted text.",
|
|
11240
|
+
description: "Fetch a URL and return its content as plain text / Markdown. Use this to read documentation, API references, READMEs, articles, or any public web page. Follows redirects automatically. Returns the first ~16000 characters of extracted text. If the direct request fails and a DeepSeek API key is configured, retries once via the DeepSeek Fetch API.",
|
|
10896
11241
|
parameters: {
|
|
10897
11242
|
url: {
|
|
10898
11243
|
type: "string",
|
|
@@ -10922,94 +11267,26 @@ var webFetchTool = {
|
|
|
10922
11267
|
if (e.message.startsWith("Blocked:")) throw e;
|
|
10923
11268
|
throw new NetworkError(`Invalid URL: "${url}"`);
|
|
10924
11269
|
}
|
|
10925
|
-
|
|
10926
|
-
const timeoutId = setTimeout(() => controller.abort(), 2e4);
|
|
10927
|
-
let rawHtml;
|
|
10928
|
-
let finalUrl;
|
|
10929
|
-
let contentType;
|
|
10930
|
-
const MAX_REDIRECTS = 10;
|
|
10931
|
-
const FETCH_HEADERS = {
|
|
10932
|
-
"User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
|
|
10933
|
-
Accept: "text/html,application/xhtml+xml,text/plain,*/*",
|
|
10934
|
-
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
|
|
10935
|
-
};
|
|
10936
|
-
const dispatcher = ssrfSafeDispatcher();
|
|
11270
|
+
let text;
|
|
10937
11271
|
try {
|
|
10938
|
-
|
|
10939
|
-
let resp = null;
|
|
10940
|
-
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
10941
|
-
const parsedHop = new URL(currentUrl);
|
|
10942
|
-
if (isPrivateHost(parsedHop.hostname)) {
|
|
10943
|
-
throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
|
|
10944
|
-
}
|
|
10945
|
-
await resolveAndCheck(parsedHop.hostname);
|
|
10946
|
-
const r = await fetch(currentUrl, {
|
|
10947
|
-
signal: controller.signal,
|
|
10948
|
-
headers: FETCH_HEADERS,
|
|
10949
|
-
redirect: "manual",
|
|
10950
|
-
// 手动控制重定向
|
|
10951
|
-
dispatcher
|
|
10952
|
-
// dual undici-types:`dispatcher` 字段在 undici 与 @types/node 内置的
|
|
10953
|
-
// undici-types 两份 RequestInit 间判不兼容,localized 转换。
|
|
10954
|
-
});
|
|
10955
|
-
if (r.status >= 300 && r.status < 400) {
|
|
10956
|
-
if (hop >= MAX_REDIRECTS) {
|
|
10957
|
-
throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
|
|
10958
|
-
}
|
|
10959
|
-
const location = r.headers.get("Location");
|
|
10960
|
-
if (!location) {
|
|
10961
|
-
resp = r;
|
|
10962
|
-
break;
|
|
10963
|
-
}
|
|
10964
|
-
currentUrl = new URL(location, currentUrl).href;
|
|
10965
|
-
continue;
|
|
10966
|
-
}
|
|
10967
|
-
resp = r;
|
|
10968
|
-
break;
|
|
10969
|
-
}
|
|
10970
|
-
if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
|
|
10971
|
-
finalUrl = currentUrl;
|
|
10972
|
-
contentType = resp.headers.get("content-type") ?? "";
|
|
10973
|
-
if (!resp.ok) {
|
|
10974
|
-
throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
|
|
10975
|
-
}
|
|
10976
|
-
const bodyBytes = await readBodyCapped(resp, MAX_BODY_BYTES);
|
|
10977
|
-
clearTimeout(timeoutId);
|
|
10978
|
-
rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
|
|
11272
|
+
text = await fetchDirect(url);
|
|
10979
11273
|
} catch (err) {
|
|
10980
|
-
|
|
10981
|
-
if (
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
10992
|
-
);
|
|
11274
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11275
|
+
if (msg.startsWith("Blocked:")) throw err;
|
|
11276
|
+
const key = resolveDeepSeekWebKey();
|
|
11277
|
+
if (!key) throw err;
|
|
11278
|
+
try {
|
|
11279
|
+
const ds = await deepseekFetch(key, url);
|
|
11280
|
+
text = [
|
|
11281
|
+
ds.title ? `# ${ds.title}` : "",
|
|
11282
|
+
`Source: ${url} (via DeepSeek Fetch \u2014 direct request failed: ${msg.split("\n")[0]})`,
|
|
11283
|
+
""
|
|
11284
|
+
].filter(Boolean).join("\n") + "\n\n" + ds.content;
|
|
11285
|
+
} catch (dsErr) {
|
|
11286
|
+
const dsMsg = dsErr instanceof Error ? dsErr.message : String(dsErr);
|
|
11287
|
+
throw new NetworkError(`${msg}
|
|
11288
|
+
DeepSeek Fetch fallback also failed: ${dsMsg}`, void 0, err);
|
|
10993
11289
|
}
|
|
10994
|
-
throw err;
|
|
10995
|
-
} finally {
|
|
10996
|
-
void dispatcher.close().catch(() => {
|
|
10997
|
-
});
|
|
10998
|
-
}
|
|
10999
|
-
let text;
|
|
11000
|
-
if (contentType.includes("text/plain") || contentType.includes("application/json")) {
|
|
11001
|
-
text = rawHtml;
|
|
11002
|
-
} else {
|
|
11003
|
-
const title = extractTitle(rawHtml);
|
|
11004
|
-
const desc = extractDescription(rawHtml);
|
|
11005
|
-
const body = htmlToText(rawHtml);
|
|
11006
|
-
const header = [
|
|
11007
|
-
title ? `# ${title}` : "",
|
|
11008
|
-
desc ? `> ${desc}` : "",
|
|
11009
|
-
`Source: ${finalUrl}`,
|
|
11010
|
-
""
|
|
11011
|
-
].filter(Boolean).join("\n");
|
|
11012
|
-
text = header + "\n\n" + body;
|
|
11013
11290
|
}
|
|
11014
11291
|
if (selector) {
|
|
11015
11292
|
const lower = selector.toLowerCase();
|
|
@@ -11034,16 +11311,132 @@ var webFetchTool = {
|
|
|
11034
11311
|
return text;
|
|
11035
11312
|
}
|
|
11036
11313
|
};
|
|
11314
|
+
async function fetchDirect(url) {
|
|
11315
|
+
const controller = new AbortController();
|
|
11316
|
+
const timeoutId = setTimeout(() => controller.abort(), 2e4);
|
|
11317
|
+
let rawHtml;
|
|
11318
|
+
let finalUrl;
|
|
11319
|
+
let contentType;
|
|
11320
|
+
const MAX_REDIRECTS = 10;
|
|
11321
|
+
const FETCH_HEADERS = {
|
|
11322
|
+
"User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
|
|
11323
|
+
Accept: "text/html,application/xhtml+xml,text/plain,*/*",
|
|
11324
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
|
|
11325
|
+
};
|
|
11326
|
+
const dispatcher = ssrfSafeDispatcher();
|
|
11327
|
+
try {
|
|
11328
|
+
let currentUrl = url;
|
|
11329
|
+
let resp = null;
|
|
11330
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
11331
|
+
const parsedHop = new URL(currentUrl);
|
|
11332
|
+
if (isPrivateHost(parsedHop.hostname)) {
|
|
11333
|
+
throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
|
|
11334
|
+
}
|
|
11335
|
+
await resolveAndCheck(parsedHop.hostname);
|
|
11336
|
+
const r = await fetch(currentUrl, {
|
|
11337
|
+
signal: controller.signal,
|
|
11338
|
+
headers: FETCH_HEADERS,
|
|
11339
|
+
redirect: "manual",
|
|
11340
|
+
// 手动控制重定向
|
|
11341
|
+
dispatcher
|
|
11342
|
+
// dual undici-types:`dispatcher` 字段在 undici 与 @types/node 内置的
|
|
11343
|
+
// undici-types 两份 RequestInit 间判不兼容,localized 转换。
|
|
11344
|
+
});
|
|
11345
|
+
if (r.status >= 300 && r.status < 400) {
|
|
11346
|
+
if (hop >= MAX_REDIRECTS) {
|
|
11347
|
+
throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
|
|
11348
|
+
}
|
|
11349
|
+
const location = r.headers.get("Location");
|
|
11350
|
+
if (!location) {
|
|
11351
|
+
resp = r;
|
|
11352
|
+
break;
|
|
11353
|
+
}
|
|
11354
|
+
currentUrl = new URL(location, currentUrl).href;
|
|
11355
|
+
continue;
|
|
11356
|
+
}
|
|
11357
|
+
resp = r;
|
|
11358
|
+
break;
|
|
11359
|
+
}
|
|
11360
|
+
if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
|
|
11361
|
+
finalUrl = currentUrl;
|
|
11362
|
+
contentType = resp.headers.get("content-type") ?? "";
|
|
11363
|
+
if (!resp.ok) {
|
|
11364
|
+
throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
|
|
11365
|
+
}
|
|
11366
|
+
const bodyBytes = await readBodyCapped(resp, MAX_BODY_BYTES);
|
|
11367
|
+
clearTimeout(timeoutId);
|
|
11368
|
+
rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
|
|
11369
|
+
} catch (err) {
|
|
11370
|
+
clearTimeout(timeoutId);
|
|
11371
|
+
if (err.name === "AbortError") {
|
|
11372
|
+
throw new NetworkError(`Request timed out after 20s: ${url}`, void 0, err);
|
|
11373
|
+
}
|
|
11374
|
+
const cause = err.cause;
|
|
11375
|
+
if (cause?.message?.startsWith("Blocked:")) throw new NetworkError(cause.message);
|
|
11376
|
+
if (err instanceof Error && err.message === "fetch failed") {
|
|
11377
|
+
const detail = cause?.message ? ` (${cause.message})` : "";
|
|
11378
|
+
throw new NetworkError(
|
|
11379
|
+
`Connection failed for ${url}${detail}. The domain may be unreachable from the current network (some sites require a proxy \u2014 see config.proxy) or temporarily down. Do not retry the same URL; try an alternative source.`,
|
|
11380
|
+
void 0,
|
|
11381
|
+
err
|
|
11382
|
+
);
|
|
11383
|
+
}
|
|
11384
|
+
throw err;
|
|
11385
|
+
} finally {
|
|
11386
|
+
void dispatcher.close().catch(() => {
|
|
11387
|
+
});
|
|
11388
|
+
}
|
|
11389
|
+
let text;
|
|
11390
|
+
if (contentType.includes("text/plain") || contentType.includes("application/json")) {
|
|
11391
|
+
text = rawHtml;
|
|
11392
|
+
} else {
|
|
11393
|
+
const title = extractTitle(rawHtml);
|
|
11394
|
+
const desc = extractDescription(rawHtml);
|
|
11395
|
+
const body = htmlToText(rawHtml);
|
|
11396
|
+
const header = [
|
|
11397
|
+
title ? `# ${title}` : "",
|
|
11398
|
+
desc ? `> ${desc}` : "",
|
|
11399
|
+
`Source: ${finalUrl}`,
|
|
11400
|
+
""
|
|
11401
|
+
].filter(Boolean).join("\n");
|
|
11402
|
+
text = header + "\n\n" + body;
|
|
11403
|
+
}
|
|
11404
|
+
return text;
|
|
11405
|
+
}
|
|
11037
11406
|
|
|
11038
11407
|
// src/tools/builtin/web-search.ts
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11408
|
+
function planEngineOrder(engine, query, hasDeepSeekKey) {
|
|
11409
|
+
const skipped = [];
|
|
11410
|
+
if (engine === "bing") return { order: ["bing"], skipped };
|
|
11411
|
+
if (engine === "google") return { order: ["google"], skipped };
|
|
11412
|
+
const tooLong = [...query].length > DEEPSEEK_MAX_QUERY_CHARS;
|
|
11413
|
+
if (engine === "deepseek") {
|
|
11414
|
+
if (!hasDeepSeekKey) {
|
|
11415
|
+
throw new ToolError(
|
|
11416
|
+
"web_search",
|
|
11417
|
+
"engine 'deepseek' needs a DeepSeek API key (config apiKeys.deepseek or AICLI_API_KEY_DEEPSEEK), and networkPolicy must allow api.deepseek.com. Use engine 'auto' or 'bing' instead."
|
|
11418
|
+
);
|
|
11419
|
+
}
|
|
11420
|
+
if (tooLong) {
|
|
11421
|
+
throw new ToolError(
|
|
11422
|
+
"web_search",
|
|
11423
|
+
`engine 'deepseek' accepts queries up to ${DEEPSEEK_MAX_QUERY_CHARS} characters \u2014 shorten the query.`
|
|
11424
|
+
);
|
|
11425
|
+
}
|
|
11426
|
+
return { order: ["deepseek"], skipped };
|
|
11427
|
+
}
|
|
11428
|
+
if (hasDeepSeekKey && !tooLong) return { order: ["deepseek", "bing", "google"], skipped };
|
|
11429
|
+
if (hasDeepSeekKey && tooLong) skipped.push(`deepseek: query longer than ${DEEPSEEK_MAX_QUERY_CHARS} chars, skipped`);
|
|
11430
|
+
return { order: ["bing", "google"], skipped };
|
|
11431
|
+
}
|
|
11432
|
+
var REQUEST_TIMEOUT_MS2 = 15e3;
|
|
11433
|
+
var MAX_RESULTS2 = 10;
|
|
11434
|
+
var DEFAULT_RESULTS2 = 5;
|
|
11042
11435
|
var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
|
|
11043
11436
|
var webSearchTool = {
|
|
11044
11437
|
definition: {
|
|
11045
11438
|
name: "web_search",
|
|
11046
|
-
description: 'Search the web
|
|
11439
|
+
description: 'Search the web. Uses the DeepSeek Search API when a DeepSeek API key is configured (results carry a relevance score and a longer summary), otherwise scrapes Bing / Google public result pages. Returns title, url and summary/snippet for each result. Use this for fresh facts, news, or any "what does X say about Y" lookup. With the deepseek engine the query (max 128 chars) supports operators: site:example.com, -site:, "exact phrase", -term, lang:en, after:2025, before:2026-06-01. Prefer this over google_search (which needs a paid API key).',
|
|
11047
11440
|
parameters: {
|
|
11048
11441
|
query: {
|
|
11049
11442
|
type: "string",
|
|
@@ -11057,7 +11450,7 @@ var webSearchTool = {
|
|
|
11057
11450
|
},
|
|
11058
11451
|
engine: {
|
|
11059
11452
|
type: "string",
|
|
11060
|
-
description: "Search engine: '
|
|
11453
|
+
description: "Search engine: 'auto' (default \u2014 DeepSeek Search API if a DeepSeek key is configured, then Bing, then Google on empty/error), 'deepseek', 'bing', or 'google'.",
|
|
11061
11454
|
required: false
|
|
11062
11455
|
}
|
|
11063
11456
|
},
|
|
@@ -11067,21 +11460,30 @@ var webSearchTool = {
|
|
|
11067
11460
|
const query = String(args["query"] ?? "").trim();
|
|
11068
11461
|
if (!query) throw new ToolError("web_search", "query parameter is required");
|
|
11069
11462
|
const numResults = Math.min(
|
|
11070
|
-
Math.max(Math.floor(Number(args["num_results"] ??
|
|
11071
|
-
|
|
11463
|
+
Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS2)), 1),
|
|
11464
|
+
MAX_RESULTS2
|
|
11072
11465
|
);
|
|
11073
11466
|
const engine = String(args["engine"] ?? "auto").toLowerCase();
|
|
11074
|
-
if (!["bing", "google", "auto"].includes(engine)) {
|
|
11075
|
-
throw new ToolError("web_search", `engine must be one of: bing, google
|
|
11467
|
+
if (!["deepseek", "bing", "google", "auto"].includes(engine)) {
|
|
11468
|
+
throw new ToolError("web_search", `engine must be one of: auto, deepseek, bing, google (got: ${engine})`);
|
|
11076
11469
|
}
|
|
11077
|
-
const
|
|
11078
|
-
const
|
|
11470
|
+
const deepseekKey = engine === "bing" || engine === "google" ? void 0 : resolveDeepSeekWebKey();
|
|
11471
|
+
const { order, skipped } = planEngineOrder(engine, query, deepseekKey !== void 0);
|
|
11472
|
+
const errors = [...skipped];
|
|
11079
11473
|
let weakBingFallback = null;
|
|
11080
11474
|
let weakBingReason = null;
|
|
11081
11475
|
for (let i = 0; i < order.length; i++) {
|
|
11082
11476
|
const eng = order[i];
|
|
11083
11477
|
const isLastEngine = i === order.length - 1;
|
|
11084
11478
|
try {
|
|
11479
|
+
if (eng === "deepseek") {
|
|
11480
|
+
const dsResults = await deepseekSearch(deepseekKey, query, numResults);
|
|
11481
|
+
if (dsResults.length === 0) {
|
|
11482
|
+
errors.push("deepseek: 0 results");
|
|
11483
|
+
continue;
|
|
11484
|
+
}
|
|
11485
|
+
return formatDeepSeekResults(query, dsResults);
|
|
11486
|
+
}
|
|
11085
11487
|
const results = eng === "bing" ? await searchBing(query, numResults) : await searchGoogle(query, numResults);
|
|
11086
11488
|
if (results.length === 0) {
|
|
11087
11489
|
errors.push(`${eng}: 0 results`);
|
|
@@ -11096,13 +11498,13 @@ var webSearchTool = {
|
|
|
11096
11498
|
continue;
|
|
11097
11499
|
}
|
|
11098
11500
|
}
|
|
11099
|
-
return
|
|
11501
|
+
return formatResults2(query, eng, results);
|
|
11100
11502
|
} catch (err) {
|
|
11101
11503
|
errors.push(`${eng}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11102
11504
|
}
|
|
11103
11505
|
}
|
|
11104
11506
|
if (weakBingFallback) {
|
|
11105
|
-
return
|
|
11507
|
+
return formatResults2(query, "bing", weakBingFallback, {
|
|
11106
11508
|
warning: `engine signaled weak relevance (${weakBingReason}). These results may be Bing's "popular sites" fallback rather than real matches. Consider rephrasing the query (shorter, fewer constraints), or use web_fetch against a known authoritative URL (e.g. baike.baidu.com, mfa.gov.cn, news.cctv.com).`
|
|
11107
11509
|
});
|
|
11108
11510
|
}
|
|
@@ -11308,7 +11710,7 @@ function parseGoogleResults(html, maxResults) {
|
|
|
11308
11710
|
}
|
|
11309
11711
|
async function fetchHtml(url) {
|
|
11310
11712
|
const controller = new AbortController();
|
|
11311
|
-
const timer = setTimeout(() => controller.abort(),
|
|
11713
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
11312
11714
|
try {
|
|
11313
11715
|
const res = await fetch(url, {
|
|
11314
11716
|
method: "GET",
|
|
@@ -11325,7 +11727,7 @@ async function fetchHtml(url) {
|
|
|
11325
11727
|
return await res.text();
|
|
11326
11728
|
} catch (err) {
|
|
11327
11729
|
if (err instanceof Error && err.name === "AbortError") {
|
|
11328
|
-
throw new NetworkError(`Request timed out after ${
|
|
11730
|
+
throw new NetworkError(`Request timed out after ${REQUEST_TIMEOUT_MS2 / 1e3}s.`, void 0, err);
|
|
11329
11731
|
}
|
|
11330
11732
|
throw err;
|
|
11331
11733
|
} finally {
|
|
@@ -11338,7 +11740,31 @@ function stripTags2(html) {
|
|
|
11338
11740
|
function decodeHtmlEntities(s) {
|
|
11339
11741
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(///g, "/").replace(/ /g, " ");
|
|
11340
11742
|
}
|
|
11341
|
-
|
|
11743
|
+
var DEEPSEEK_SUMMARY_MAX_CHARS = 600;
|
|
11744
|
+
function clipSummary(s) {
|
|
11745
|
+
const flat = s.replace(/\s+/g, " ").trim();
|
|
11746
|
+
const chars = [...flat];
|
|
11747
|
+
if (chars.length <= DEEPSEEK_SUMMARY_MAX_CHARS) return flat;
|
|
11748
|
+
return chars.slice(0, DEEPSEEK_SUMMARY_MAX_CHARS).join("") + "\u2026";
|
|
11749
|
+
}
|
|
11750
|
+
function formatDeepSeekResults(query, results) {
|
|
11751
|
+
const preamble = `Search results for "${query}" via deepseek (${results.length} results; relevance 0\u20133, \u22651.5 is high quality):
|
|
11752
|
+
`;
|
|
11753
|
+
const body = results.map((r, i) => {
|
|
11754
|
+
const meta = [
|
|
11755
|
+
r.siteName,
|
|
11756
|
+
r.date ? r.date.slice(0, 10) : null,
|
|
11757
|
+
r.rerankScore !== null ? `relevance ${r.rerankScore.toFixed(2)}` : null
|
|
11758
|
+
].filter(Boolean).join(" \xB7 ");
|
|
11759
|
+
const text = clipSummary(r.summary || r.snippet) || "(no summary)";
|
|
11760
|
+
return `${i + 1}. **${r.title}**
|
|
11761
|
+
URL: ${r.url}${meta ? `
|
|
11762
|
+
${meta}` : ""}
|
|
11763
|
+
${text}`;
|
|
11764
|
+
}).join("\n\n");
|
|
11765
|
+
return preamble + "\n" + body;
|
|
11766
|
+
}
|
|
11767
|
+
function formatResults2(query, engine, results, opts) {
|
|
11342
11768
|
let preamble = `Search results for "${query}" via ${engine} (${results.length} results):
|
|
11343
11769
|
`;
|
|
11344
11770
|
if (opts?.warning) {
|
|
@@ -11632,134 +12058,6 @@ function renderTodoList(todos) {
|
|
|
11632
12058
|
console.log();
|
|
11633
12059
|
}
|
|
11634
12060
|
|
|
11635
|
-
// src/tools/builtin/google-search.ts
|
|
11636
|
-
var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
|
|
11637
|
-
var REQUEST_TIMEOUT_MS2 = 15e3;
|
|
11638
|
-
var MAX_RESULTS2 = 10;
|
|
11639
|
-
var DEFAULT_RESULTS2 = 5;
|
|
11640
|
-
var googleSearchContext = {};
|
|
11641
|
-
var googleSearchTool = {
|
|
11642
|
-
definition: {
|
|
11643
|
-
name: "google_search",
|
|
11644
|
-
description: "\u26A0\uFE0F NOT RECOMMENDED \u2014 uses Google Custom Search API which requires a paid API key. API keys have leaked in the wild causing tens of thousands of USD in unauthorized charges. PREFER `web_search` (keyless Bing/Google scraping, zero billing risk). This tool is disabled by default; user must set config.googleSearch.enabled=true to use it.",
|
|
11645
|
-
parameters: {
|
|
11646
|
-
query: {
|
|
11647
|
-
type: "string",
|
|
11648
|
-
description: "The search query string.",
|
|
11649
|
-
required: true
|
|
11650
|
-
},
|
|
11651
|
-
num_results: {
|
|
11652
|
-
type: "number",
|
|
11653
|
-
description: "Number of results to return (1-10, default 5).",
|
|
11654
|
-
required: false
|
|
11655
|
-
}
|
|
11656
|
-
},
|
|
11657
|
-
dangerous: false
|
|
11658
|
-
},
|
|
11659
|
-
async execute(args) {
|
|
11660
|
-
const query = String(args["query"] ?? "").trim();
|
|
11661
|
-
if (!query) throw new ToolError("google_search", "query parameter is required");
|
|
11662
|
-
const enabledFlag = googleSearchContext.configManager?.get("googleSearch")?.enabled ?? false;
|
|
11663
|
-
if (!enabledFlag) {
|
|
11664
|
-
throw new ToolError(
|
|
11665
|
-
"google_search",
|
|
11666
|
-
'\u{1F6AB} google_search is disabled by default (v0.4.135+).\n\nWHY: Google Custom Search has no native daily-spend cap. Keys leak (search GitHub for "AIza" \u2014 thousands of public exposures), bots scrape them within hours, and the GCP bill lands days later. Multiple engineers have lost $10k+ USD this way.\n\nRECOMMENDED: use `web_search` instead \u2014 same use case, scrapes Bing/Google public pages, zero API key, zero billing risk.\n\nIF YOU INSIST: edit ~/.aicli/config.json and set `googleSearch.enabled = true`. ALSO go to GCP Console \u2192 Custom Search API \u2192 Quotas and set a hard daily request cap (e.g. 100/day) \u2014 that is the only real damage cap.'
|
|
11667
|
-
);
|
|
11668
|
-
}
|
|
11669
|
-
const numResults = Math.min(
|
|
11670
|
-
Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS2)), 1),
|
|
11671
|
-
MAX_RESULTS2
|
|
11672
|
-
);
|
|
11673
|
-
const { apiKey, cx } = resolveConfig();
|
|
11674
|
-
const url = new URL(GOOGLE_SEARCH_API);
|
|
11675
|
-
url.searchParams.set("key", apiKey);
|
|
11676
|
-
url.searchParams.set("cx", cx);
|
|
11677
|
-
url.searchParams.set("q", query);
|
|
11678
|
-
url.searchParams.set("num", String(numResults));
|
|
11679
|
-
const controller = new AbortController();
|
|
11680
|
-
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
11681
|
-
try {
|
|
11682
|
-
const response = await fetch(url.toString(), {
|
|
11683
|
-
method: "GET",
|
|
11684
|
-
headers: {
|
|
11685
|
-
"Accept": "application/json"
|
|
11686
|
-
},
|
|
11687
|
-
signal: controller.signal
|
|
11688
|
-
});
|
|
11689
|
-
if (!response.ok) {
|
|
11690
|
-
const errorBody = await response.text().catch(() => "");
|
|
11691
|
-
if (response.status === 403) {
|
|
11692
|
-
throw new NetworkError(
|
|
11693
|
-
`Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
|
|
11694
|
-
Please check your API Key and Search Engine ID configuration.`,
|
|
11695
|
-
403
|
|
11696
|
-
);
|
|
11697
|
-
}
|
|
11698
|
-
if (response.status === 429) {
|
|
11699
|
-
throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
|
|
11700
|
-
}
|
|
11701
|
-
throw new NetworkError(
|
|
11702
|
-
`Google Search API error: HTTP ${response.status} ${response.statusText}
|
|
11703
|
-
${errorBody.slice(0, 500)}`,
|
|
11704
|
-
response.status
|
|
11705
|
-
);
|
|
11706
|
-
}
|
|
11707
|
-
const data = await response.json();
|
|
11708
|
-
return formatResults2(query, data, numResults);
|
|
11709
|
-
} catch (err) {
|
|
11710
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
11711
|
-
throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS2 / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
|
|
11712
|
-
}
|
|
11713
|
-
throw err;
|
|
11714
|
-
} finally {
|
|
11715
|
-
clearTimeout(timeout);
|
|
11716
|
-
}
|
|
11717
|
-
}
|
|
11718
|
-
};
|
|
11719
|
-
function resolveConfig() {
|
|
11720
|
-
let apiKey;
|
|
11721
|
-
let cx;
|
|
11722
|
-
if (googleSearchContext.configManager) {
|
|
11723
|
-
apiKey = googleSearchContext.configManager.getApiKey("google-search");
|
|
11724
|
-
cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
|
|
11725
|
-
} else {
|
|
11726
|
-
apiKey = EnvLoader.getApiKey("google-search");
|
|
11727
|
-
cx = EnvLoader.getGoogleSearchEngineId();
|
|
11728
|
-
}
|
|
11729
|
-
if (!apiKey) {
|
|
11730
|
-
throw new ToolError(
|
|
11731
|
-
"google_search",
|
|
11732
|
-
'Google Search API Key not configured.\nConfigure via one of:\n 1. Run /config \u2192 Configure Google Search\n 2. Set env var AICLI_API_KEY_GOOGLESEARCH\n 3. Add apiKeys["google-search"] to ~/.aicli/config.json'
|
|
11733
|
-
);
|
|
11734
|
-
}
|
|
11735
|
-
if (!cx) {
|
|
11736
|
-
throw new ToolError(
|
|
11737
|
-
"google_search",
|
|
11738
|
-
"Google Search Engine ID (cx) not configured.\nConfigure via one of:\n 1. Run /config \u2192 Configure Google Search\n 2. Set env var AICLI_GOOGLE_CX\n 3. Add googleSearchEngineId to ~/.aicli/config.json\n\nGet one at: https://programmablesearchengine.google.com/ \u2192 Create search engine \u2192 Copy Search Engine ID"
|
|
11739
|
-
);
|
|
11740
|
-
}
|
|
11741
|
-
return { apiKey, cx };
|
|
11742
|
-
}
|
|
11743
|
-
function formatResults2(query, data, _requested) {
|
|
11744
|
-
const items = data.items ?? [];
|
|
11745
|
-
if (items.length === 0) {
|
|
11746
|
-
const info2 = data.searchInformation;
|
|
11747
|
-
return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
|
|
11748
|
-
}
|
|
11749
|
-
const info = data.searchInformation;
|
|
11750
|
-
const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
|
|
11751
|
-
`;
|
|
11752
|
-
const results = items.map((item, i) => {
|
|
11753
|
-
const title = item.title ?? "Untitled";
|
|
11754
|
-
const link = item.link ?? "";
|
|
11755
|
-
const snippet = item.snippet ?? "";
|
|
11756
|
-
return `${i + 1}. **${title}**
|
|
11757
|
-
URL: ${link}
|
|
11758
|
-
${snippet}`;
|
|
11759
|
-
});
|
|
11760
|
-
return header + "\n" + results.join("\n\n");
|
|
11761
|
-
}
|
|
11762
|
-
|
|
11763
12061
|
// src/core/tool-capable-provider.ts
|
|
11764
12062
|
function isToolCapableProvider(provider) {
|
|
11765
12063
|
if (!provider) return false;
|
|
@@ -11816,7 +12114,7 @@ var HookEventNameSchema = z2.enum([
|
|
|
11816
12114
|
var McpServerSchema = z2.object({
|
|
11817
12115
|
command: z2.string(),
|
|
11818
12116
|
args: z2.array(z2.string()).default([]),
|
|
11819
|
-
env: z2.record(z2.string()).optional(),
|
|
12117
|
+
env: z2.record(z2.string(), z2.string()).optional(),
|
|
11820
12118
|
timeout: z2.number().default(3e4)
|
|
11821
12119
|
});
|
|
11822
12120
|
var PluginManifestSchema = z2.object({
|
|
@@ -11826,10 +12124,10 @@ var PluginManifestSchema = z2.object({
|
|
|
11826
12124
|
skills: z2.array(z2.string()).default([]),
|
|
11827
12125
|
hooks: z2.object({
|
|
11828
12126
|
enabled: z2.boolean().optional(),
|
|
11829
|
-
events: z2.
|
|
11830
|
-
}).
|
|
12127
|
+
events: z2.partialRecord(HookEventNameSchema, HookCommandSchema).default({})
|
|
12128
|
+
}).prefault({ events: {} }),
|
|
11831
12129
|
commands: z2.array(z2.string()).default([]),
|
|
11832
|
-
mcpServers: z2.record(McpServerSchema).default({}),
|
|
12130
|
+
mcpServers: z2.record(z2.string(), McpServerSchema).default({}),
|
|
11833
12131
|
agents: z2.array(z2.string()).default([]),
|
|
11834
12132
|
permissionHints: z2.array(z2.string()).default([])
|
|
11835
12133
|
}).strict();
|
|
@@ -12904,26 +13202,75 @@ var taskCreateTool = {
|
|
|
12904
13202
|
ID: ${task.id}
|
|
12905
13203
|
Command: ${command}
|
|
12906
13204
|
Description: ${description}
|
|
12907
|
-
Use task_list to check status, task_stop to terminate.`;
|
|
13205
|
+
Use task_list to check status (task_list with id + wait_seconds waits for it to finish \u2014 no bash sleep needed), task_stop to terminate.`;
|
|
12908
13206
|
}
|
|
12909
13207
|
};
|
|
12910
13208
|
|
|
12911
13209
|
// src/tools/builtin/task-list.ts
|
|
13210
|
+
var MAX_TASK_WAIT_SECONDS = 300;
|
|
13211
|
+
var POLL_MS = 200;
|
|
13212
|
+
async function waitForTasks(id, seconds) {
|
|
13213
|
+
const isRunning = (taskId) => getTaskOutput(taskId)?.status === "running";
|
|
13214
|
+
const watched = id ? [id] : listTasks().filter((t) => t.status === "running").map((t) => t.id);
|
|
13215
|
+
if (!watched.some(isRunning)) return "nothing-running";
|
|
13216
|
+
const signal = currentAbortSignal();
|
|
13217
|
+
const deadline = Date.now() + seconds * 1e3;
|
|
13218
|
+
for (; ; ) {
|
|
13219
|
+
if (signal.aborted) return "interrupted";
|
|
13220
|
+
if (!watched.every(isRunning)) return "finished";
|
|
13221
|
+
const left = deadline - Date.now();
|
|
13222
|
+
if (left <= 0) return "timeout";
|
|
13223
|
+
await new Promise((resolve13) => {
|
|
13224
|
+
const timer = setTimeout(done, Math.min(POLL_MS, left));
|
|
13225
|
+
function done() {
|
|
13226
|
+
clearTimeout(timer);
|
|
13227
|
+
signal.removeEventListener("abort", done);
|
|
13228
|
+
resolve13();
|
|
13229
|
+
}
|
|
13230
|
+
signal.addEventListener("abort", done, { once: true });
|
|
13231
|
+
});
|
|
13232
|
+
}
|
|
13233
|
+
}
|
|
13234
|
+
function parseWaitSeconds(raw) {
|
|
13235
|
+
const n = Number(raw);
|
|
13236
|
+
if (raw === void 0 || raw === null || raw === "" || !Number.isFinite(n) || n <= 0) {
|
|
13237
|
+
return { seconds: 0, clamped: false };
|
|
13238
|
+
}
|
|
13239
|
+
return n > MAX_TASK_WAIT_SECONDS ? { seconds: MAX_TASK_WAIT_SECONDS, clamped: true } : { seconds: n, clamped: false };
|
|
13240
|
+
}
|
|
12912
13241
|
var taskListTool = {
|
|
12913
13242
|
definition: {
|
|
12914
13243
|
name: "task_list",
|
|
12915
|
-
description: `List all background tasks with their status. Provide an ID to get detailed output for a specific task.`,
|
|
13244
|
+
description: `List all background tasks with their status. Provide an ID to get detailed output for a specific task. To wait for a background task, pass wait_seconds (max ${MAX_TASK_WAIT_SECONDS}) instead of polling with bash sleep: the call returns as soon as the task (or, without an id, any running task) finishes.`,
|
|
12916
13245
|
parameters: {
|
|
12917
13246
|
id: {
|
|
12918
13247
|
type: "string",
|
|
12919
13248
|
description: "Optional: specific task ID to get detailed stdout/stderr output",
|
|
12920
13249
|
required: false
|
|
13250
|
+
},
|
|
13251
|
+
wait_seconds: {
|
|
13252
|
+
type: "number",
|
|
13253
|
+
description: `Optional: wait up to this many seconds (max ${MAX_TASK_WAIT_SECONDS}) for the task with this id \u2014 or, without id, for any running task \u2014 to finish, then report. Returns early on completion.`,
|
|
13254
|
+
required: false
|
|
12921
13255
|
}
|
|
12922
13256
|
},
|
|
12923
13257
|
dangerous: false
|
|
12924
13258
|
},
|
|
12925
13259
|
async execute(args) {
|
|
12926
13260
|
const id = args["id"] ? String(args["id"]) : void 0;
|
|
13261
|
+
const { seconds, clamped } = parseWaitSeconds(args["wait_seconds"]);
|
|
13262
|
+
let note = "";
|
|
13263
|
+
if (seconds > 0) {
|
|
13264
|
+
const started = Date.now();
|
|
13265
|
+
const outcome = await waitForTasks(id, seconds);
|
|
13266
|
+
const waited = ((Date.now() - started) / 1e3).toFixed(1);
|
|
13267
|
+
if (outcome === "timeout") note = `(waited ${waited}s; still running \u2014 call again with wait_seconds to keep waiting)`;
|
|
13268
|
+
else if (outcome === "finished") note = `(waited ${waited}s; task finished)`;
|
|
13269
|
+
else if (outcome === "interrupted") note = `(wait interrupted by user after ${waited}s)`;
|
|
13270
|
+
if (clamped) note += `${note ? " " : ""}(wait_seconds capped at ${MAX_TASK_WAIT_SECONDS})`;
|
|
13271
|
+
}
|
|
13272
|
+
const withNote = (body) => note ? `${note}
|
|
13273
|
+
${body}` : body;
|
|
12927
13274
|
if (id) {
|
|
12928
13275
|
const output = getTaskOutput(id);
|
|
12929
13276
|
if (!output) return `Task not found: ${id}`;
|
|
@@ -12935,7 +13282,7 @@ ${output.stdout}`);
|
|
|
12935
13282
|
\u2500\u2500 stderr \u2500\u2500
|
|
12936
13283
|
${output.stderr}`);
|
|
12937
13284
|
if (!output.stdout && !output.stderr) parts.push("\n(no output yet)");
|
|
12938
|
-
return parts.join("");
|
|
13285
|
+
return withNote(parts.join(""));
|
|
12939
13286
|
}
|
|
12940
13287
|
const all = listTasks();
|
|
12941
13288
|
if (all.length === 0) return "No background tasks.";
|
|
@@ -12947,7 +13294,7 @@ ${output.stderr}`);
|
|
|
12947
13294
|
lines.push(` [${t.id}] ${status} (${elapsed.toFixed(1)}s) \u2014 ${t.description}`);
|
|
12948
13295
|
if (t.exitCode !== null) lines.push(` exit code: ${t.exitCode}`);
|
|
12949
13296
|
}
|
|
12950
|
-
return lines.join("\n");
|
|
13297
|
+
return withNote(lines.join("\n"));
|
|
12951
13298
|
}
|
|
12952
13299
|
};
|
|
12953
13300
|
|
|
@@ -17944,7 +18291,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
|
|
|
17944
18291
|
Cost: ${formatCost(cost)}` : "";
|
|
17945
18292
|
let memoryLine = "";
|
|
17946
18293
|
try {
|
|
17947
|
-
const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-
|
|
18294
|
+
const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-UZIZVHRT.js");
|
|
17948
18295
|
const pending = countPendingMemories2(ctx.config.getConfigDir());
|
|
17949
18296
|
if (pending > 0) memoryLine = `
|
|
17950
18297
|
Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
|
|
@@ -18932,7 +19279,7 @@ async function handleTest(args, ctx) {
|
|
|
18932
19279
|
const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
|
|
18933
19280
|
testArgs = isCommand ? { command: argStr } : { filter: argStr };
|
|
18934
19281
|
}
|
|
18935
|
-
const runTests = ctx.runTests ?? (await import("./run-tests-
|
|
19282
|
+
const runTests = ctx.runTests ?? (await import("./run-tests-UXEOFPAW.js")).executeTests;
|
|
18936
19283
|
const report = await runTests(testArgs);
|
|
18937
19284
|
ctx.send({ type: "info", message: report });
|
|
18938
19285
|
} catch (err) {
|
|
@@ -19323,7 +19670,7 @@ async function handleMemory(args, ctx) {
|
|
|
19323
19670
|
}
|
|
19324
19671
|
if (intent.kind === "status") {
|
|
19325
19672
|
const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-RLVLQRRC.js");
|
|
19326
|
-
const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-
|
|
19673
|
+
const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-UZIZVHRT.js");
|
|
19327
19674
|
const memories = listMemoryEntries2(configDir, { includeExpired: true, includeRejected: true });
|
|
19328
19675
|
const injected = formatMemoryForPrompt2(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
|
|
19329
19676
|
const view = buildMemoryStatusView({
|
|
@@ -19346,7 +19693,7 @@ async function handleMemory(args, ctx) {
|
|
|
19346
19693
|
return;
|
|
19347
19694
|
}
|
|
19348
19695
|
if (intent.kind === "recall") {
|
|
19349
|
-
const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-
|
|
19696
|
+
const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-UZIZVHRT.js");
|
|
19350
19697
|
const { searchChatMemory: searchChatMemory2 } = await import("./chat-index-RLVLQRRC.js");
|
|
19351
19698
|
const persistentHits = searchPersistentMemories2(configDir, intent.query, process.cwd(), 3);
|
|
19352
19699
|
if (persistentHits.length > 0) {
|
|
@@ -19467,7 +19814,7 @@ async function handleMemory(args, ctx) {
|
|
|
19467
19814
|
ctx.handleMemoryManage(intent.action, args[1], intent.action === "expire" ? args[2] : void 0);
|
|
19468
19815
|
} else if (intent.kind === "export") {
|
|
19469
19816
|
if (args[1] === "md") {
|
|
19470
|
-
const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-
|
|
19817
|
+
const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-UZIZVHRT.js");
|
|
19471
19818
|
ctx.send({
|
|
19472
19819
|
type: "export_data",
|
|
19473
19820
|
format: "md",
|
|
@@ -20473,7 +20820,7 @@ No tools match "${filter}".
|
|
|
20473
20820
|
const { join: join27 } = await import("path");
|
|
20474
20821
|
const { existsSync: existsSync33 } = await import("fs");
|
|
20475
20822
|
const { getGitRoot: getGitRoot2 } = await import("./git-context-VNCA2GU7.js");
|
|
20476
|
-
const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-
|
|
20823
|
+
const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-JZH2QKMQ.js");
|
|
20477
20824
|
const { approveProject, hashMcpFile } = await import("./project-trust-6CJB5ZMX.js");
|
|
20478
20825
|
const cwd = process.cwd();
|
|
20479
20826
|
const projectRoot = getGitRoot2(cwd) ?? cwd;
|
|
@@ -21392,7 +21739,7 @@ function createReviewCommands() {
|
|
|
21392
21739
|
usage: "/test [command|filter]",
|
|
21393
21740
|
async execute(args, ctx) {
|
|
21394
21741
|
try {
|
|
21395
|
-
const { executeTests } = await import("./run-tests-
|
|
21742
|
+
const { executeTests } = await import("./run-tests-UXEOFPAW.js");
|
|
21396
21743
|
const argStr = args.join(" ").trim();
|
|
21397
21744
|
let testArgs = {};
|
|
21398
21745
|
if (argStr) {
|
|
@@ -23616,7 +23963,8 @@ var SessionHandler = class _SessionHandler {
|
|
|
23616
23963
|
return;
|
|
23617
23964
|
}
|
|
23618
23965
|
case "interjection":
|
|
23619
|
-
this.userInterjection =
|
|
23966
|
+
this.userInterjection = this.userInterjection ? `${this.userInterjection}
|
|
23967
|
+
${msg.content}` : msg.content;
|
|
23620
23968
|
return;
|
|
23621
23969
|
}
|
|
23622
23970
|
}
|
|
@@ -24084,7 +24432,6 @@ ${systemPromptVolatile}` : systemPrompt;
|
|
|
24084
24432
|
if (!this.userInterjection) return null;
|
|
24085
24433
|
const msg = this.userInterjection;
|
|
24086
24434
|
this.userInterjection = null;
|
|
24087
|
-
this.send({ type: "info", message: `\u26A1 Interjection: "${msg}"` });
|
|
24088
24435
|
return msg;
|
|
24089
24436
|
},
|
|
24090
24437
|
onRoundStart: (round, total) => {
|
|
@@ -24943,17 +25290,6 @@ Backup saved: ${result.backupPath}` : "";
|
|
|
24943
25290
|
}
|
|
24944
25291
|
};
|
|
24945
25292
|
|
|
24946
|
-
// src/core/proxy.ts
|
|
24947
|
-
async function setupProxy(configProxy) {
|
|
24948
|
-
const proxyUrl = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.https_proxy ?? process.env.http_proxy ?? configProxy;
|
|
24949
|
-
if (!proxyUrl) return;
|
|
24950
|
-
try {
|
|
24951
|
-
const { ProxyAgent, setGlobalDispatcher } = await import("undici");
|
|
24952
|
-
setGlobalDispatcher(new ProxyAgent({ uri: proxyUrl }));
|
|
24953
|
-
} catch {
|
|
24954
|
-
}
|
|
24955
|
-
}
|
|
24956
|
-
|
|
24957
25293
|
// src/web/auth.ts
|
|
24958
25294
|
import { existsSync as existsSync31, readFileSync as readFileSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync15, openSync, closeSync, unlinkSync as unlinkSync8 } from "fs";
|
|
24959
25295
|
import { isAbsolute as isAbsolute3, join as join25, relative as relative5 } from "path";
|
|
@@ -25394,7 +25730,8 @@ async function startWebServer(options = {}) {
|
|
|
25394
25730
|
(id) => ({
|
|
25395
25731
|
baseUrl: config.get("customBaseUrls")[id],
|
|
25396
25732
|
timeout: config.get("timeouts")[id],
|
|
25397
|
-
proxy: config.get("proxy")
|
|
25733
|
+
proxy: config.get("proxy"),
|
|
25734
|
+
noProxy: config.get("noProxy")
|
|
25398
25735
|
}),
|
|
25399
25736
|
config.get("customProviders"),
|
|
25400
25737
|
config.getConfigDir()
|