diff --git a/package-lock.json b/package-lock.json index f48523d..1a00fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@ai-sdk/anthropic": "^2.0.54", "@ai-sdk/deepseek": "^1.0.31", + "@tavily/core": "^0.6.0", "ai": "^5.0.108", "chalk": "^5.3.0", "commander": "^12.1.0", @@ -900,6 +901,17 @@ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, + "node_modules/@tavily/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@tavily/core/-/core-0.6.0.tgz", + "integrity": "sha512-QJQko6BtDWFYNeE7BKFVDMPuKfLJWjRVyQmo5jAhG3A3Xgu1e/EIIRTdWFc5TyFccc8t14zrzPxxUy1YL2/AYg==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.7", + "https-proxy-agent": "^7.0.6", + "js-tiktoken": "^1.0.14" + } + }, "node_modules/@types/node": { "version": "22.19.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", @@ -919,6 +931,15 @@ "node": ">= 20" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ai": { "version": "5.0.108", "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.108.tgz", @@ -964,6 +985,56 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1036,6 +1107,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -1045,12 +1128,97 @@ "node": ">=18" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", @@ -1102,6 +1270,42 @@ "node": ">=18.0.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1117,6 +1321,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -1129,6 +1342,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -1142,6 +1392,70 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", @@ -1217,6 +1531,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -1251,6 +1574,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1263,6 +1616,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -1330,6 +1689,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", diff --git a/package.json b/package.json index 9379985..eab2c17 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dependencies": { "@ai-sdk/anthropic": "^2.0.54", "@ai-sdk/deepseek": "^1.0.31", + "@tavily/core": "^0.6.0", "ai": "^5.0.108", "chalk": "^5.3.0", "commander": "^12.1.0", diff --git a/src/permission/checkers/index.ts b/src/permission/checkers/index.ts index 8a655ab..7c84cf5 100644 --- a/src/permission/checkers/index.ts +++ b/src/permission/checkers/index.ts @@ -1,3 +1,4 @@ export type { PermissionChecker, BasePermissionConfig } from './base.js'; export { BashPermissionChecker } from './bash.js'; export { FilePermissionChecker } from './file.js'; +export { WebPermissionChecker } from './web.js'; diff --git a/src/permission/checkers/web.ts b/src/permission/checkers/web.ts new file mode 100644 index 0000000..307f3d2 --- /dev/null +++ b/src/permission/checkers/web.ts @@ -0,0 +1,159 @@ +import type { + WebPermissionConfig, + WebPermissionContext, + PermissionCheckResult, + PermissionDecision, + PermissionContext, +} from '../types.js'; +import type { PermissionChecker } from './base.js'; + +// 默认 Web 权限配置 +const DEFAULT_CONFIG: WebPermissionConfig = { + default: 'ask', // 默认需要确认 + allowAdvancedSearch: true, + allowedTopics: [], // 空数组表示允许所有主题 +}; + +/** + * Web 搜索权限检查器 + * 控制网络搜索操作的权限 + */ +export class WebPermissionChecker implements PermissionChecker { + readonly name = 'web'; + + private config: WebPermissionConfig; + private askCallback?: (ctx: PermissionContext) => Promise; + private sessionPermissions = new Map(); + + constructor() { + this.config = { ...DEFAULT_CONFIG }; + } + + /** + * 设置权限询问回调 + */ + setAskCallback(callback: (ctx: PermissionContext) => Promise): void { + this.askCallback = callback; + } + + /** + * 检查 Web 搜索权限 + */ + async checkWebPermission(ctx: WebPermissionContext): Promise { + const { query, searchDepth, topic } = ctx; + + // 1. 检查深度搜索权限 + if (searchDepth === 'advanced' && !this.config.allowAdvancedSearch) { + return { + allowed: false, + action: 'deny', + reason: '不允许深度搜索', + }; + } + + // 2. 检查主题限制 + if (this.config.allowedTopics.length > 0 && topic) { + if (!this.config.allowedTopics.includes(topic)) { + return { + allowed: false, + action: 'deny', + reason: `不允许搜索主题: ${topic}`, + }; + } + } + + // 3. 检查会话级别的临时权限 + const sessionKey = `web_search`; + const sessionPerm = this.sessionPermissions.get(sessionKey); + if (sessionPerm === 'allow') { + return { + allowed: true, + action: 'allow', + reason: '本次会话已允许网络搜索', + }; + } + if (sessionPerm === 'deny') { + return { + allowed: false, + action: 'deny', + reason: '本次会话已拒绝网络搜索', + }; + } + + // 4. 根据默认策略处理 + const action = this.config.default; + + if (action === 'allow') { + return { + allowed: true, + action: 'allow', + reason: '默认允许网络搜索', + }; + } + + if (action === 'deny') { + return { + allowed: false, + action: 'deny', + reason: '默认拒绝网络搜索', + }; + } + + // action === 'ask' + if (!this.askCallback) { + return { + allowed: false, + action: 'ask', + needsConfirmation: true, + reason: `搜索: ${query}`, + }; + } + + // 调用回调询问用户 + const decision = await this.askCallback({ + command: `web_search: ${query}`, + workdir: process.cwd(), + }); + + if (decision.remember) { + this.sessionPermissions.set(sessionKey, decision.allow ? 'allow' : 'deny'); + } + + return { + allowed: decision.allow, + action: decision.allow ? 'allow' : 'deny', + reason: decision.allow ? '用户允许' : '用户拒绝', + }; + } + + /** + * 实现 PermissionChecker 接口的 check 方法 + * 从通用 PermissionContext 中提取 Web 搜索信息 + */ + async check(ctx: PermissionContext): Promise { + // 从 command 中提取搜索查询 + const query = ctx.command.replace(/^web_search:\s*/, ''); + return this.checkWebPermission({ query }); + } + + /** + * 清除会话权限 + */ + clearSessionPermissions(): void { + this.sessionPermissions.clear(); + } + + /** + * 获取当前配置 + */ + getConfig(): WebPermissionConfig { + return { ...this.config }; + } + + /** + * 更新配置 + */ + setConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + } +} diff --git a/src/permission/manager.ts b/src/permission/manager.ts index f2db086..2acab17 100644 --- a/src/permission/manager.ts +++ b/src/permission/manager.ts @@ -3,10 +3,12 @@ import type { PermissionCheckResult, PermissionDecision, FilePermissionContext, + WebPermissionContext, } from './types.js'; import type { PermissionChecker } from './checkers/base.js'; import { BashPermissionChecker } from './checkers/bash.js'; import { FilePermissionChecker } from './checkers/file.js'; +import { WebPermissionChecker } from './checkers/web.js'; /** * 权限管理器 @@ -20,6 +22,7 @@ export class PermissionManager { // 注册默认的检查器 this.registerChecker(new BashPermissionChecker(projectRoot)); this.registerChecker(new FilePermissionChecker(projectRoot)); + this.registerChecker(new WebPermissionChecker()); } /** @@ -100,6 +103,22 @@ export class PermissionManager { return fileChecker.checkFilePermission(ctx); } + /** + * 检查 Web 搜索权限(便捷方法) + */ + async checkWebPermission(ctx: WebPermissionContext): Promise { + const webChecker = this.getChecker('web'); + if (!webChecker) { + return { + allowed: false, + action: 'ask', + needsConfirmation: true, + reason: 'Web 权限检查器未注册', + }; + } + return webChecker.checkWebPermission(ctx); + } + /** * 清除所有检查器的会话权限 */ diff --git a/src/permission/types.ts b/src/permission/types.ts index 52704b0..3773aa7 100644 --- a/src/permission/types.ts +++ b/src/permission/types.ts @@ -88,3 +88,21 @@ export interface PermissionDecision { allow: boolean; remember?: boolean; // 是否记住这个决定 } + +// Web 搜索权限请求上下文 +export interface WebPermissionContext { + query: string; // 搜索查询 + searchDepth?: 'basic' | 'advanced'; // 搜索深度 + topic?: 'general' | 'news' | 'finance'; // 搜索主题 + maxResults?: number; // 最大结果数 +} + +// Web 权限配置 +export interface WebPermissionConfig { + // 默认策略 + default: PermissionAction; + // 是否允许深度搜索 + allowAdvancedSearch: boolean; + // 搜索主题限制(空数组表示允许所有) + allowedTopics: ('general' | 'news' | 'finance')[]; +} diff --git a/src/tools/descriptions/web_search.txt b/src/tools/descriptions/web_search.txt new file mode 100644 index 0000000..f47ba8c --- /dev/null +++ b/src/tools/descriptions/web_search.txt @@ -0,0 +1,21 @@ +搜索网络获取最新信息。使用 Tavily API 进行智能搜索,返回相关网页内容和 AI 摘要。 + +这是进行网络搜索的首选工具,不要使用 curl 或 bash 命令来搜索网络。 + +适用场景: +- 查询最新新闻、事件、游戏更新 +- 搜索技术文档、API 参考 +- 获取实时数据(股价、天气等) +- 查找开源项目、库的信息 +- 了解最新的技术趋势 + +参数说明: +- query: 搜索关键词(必填) +- max_results: 返回结果数量,1-20,默认 5 +- search_depth: "basic" 快速搜索 / "advanced" 深度搜索 +- topic: "general" 通用 / "news" 新闻 / "finance" 财经 +- include_answer: 是否包含 AI 摘要,默认 true + +返回内容: +- AI 生成的摘要答案 +- 相关网页列表(标题、链接、内容摘要) diff --git a/src/tools/index.ts b/src/tools/index.ts index 85aad83..0ee4e29 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -23,6 +23,9 @@ import { deleteFileTool, } from './filesystem/index.js'; +// Web 工具 +import { webSearchTool } from './web/index.js'; + // 所有工具列表(用于注册) const allToolsWithMetadata: ToolWithMetadata[] = [ // 核心工具 (deferLoading: false) @@ -43,6 +46,9 @@ const allToolsWithMetadata: ToolWithMetadata[] = [ moveFileTool, copyFileTool, deleteFileTool, + + // Web 工具 (deferLoading: true) + webSearchTool, ]; // 注册所有工具到 registry diff --git a/src/tools/tool-search.ts b/src/tools/tool-search.ts index 202f75c..53b16b1 100644 --- a/src/tools/tool-search.ts +++ b/src/tools/tool-search.ts @@ -62,7 +62,7 @@ export const toolSearchTool: ToolWithMetadata = { return { success: true, - output: `找到 ${results.length} 个相关工具:\n\n${toolList}\n\n这些工具现在可以使用了。请选择合适的工具来完成任务。`, + output: `找到 ${results.length} 个相关工具:\n\n${toolList}\n\n重要:这些工具现在已经可以直接调用了。请立即使用合适的工具(如 web_search)来完成任务,不要使用 bash 命令代替。`, }; }, }; diff --git a/src/tools/web/index.ts b/src/tools/web/index.ts new file mode 100644 index 0000000..70771e6 --- /dev/null +++ b/src/tools/web/index.ts @@ -0,0 +1 @@ +export { webSearchTool } from './web_search.js'; diff --git a/src/tools/web/web_search.ts b/src/tools/web/web_search.ts new file mode 100644 index 0000000..05e51fc --- /dev/null +++ b/src/tools/web/web_search.ts @@ -0,0 +1,138 @@ +import { tavily } from '@tavily/core'; +import type { ToolResult } from '../../types/index.js'; +import type { ToolWithMetadata } from '../types.js'; +import { loadDescription } from '../load_description.js'; +import { getConfig } from '../../utils/config.js'; +import { getPermissionManager } from '../../permission/index.js'; + +export const webSearchTool: ToolWithMetadata = { + name: 'web_search', + description: loadDescription('web_search'), + metadata: { + name: 'web_search', + category: 'web', + description: '搜索网络获取最新信息', + keywords: ['search', 'web', 'internet', 'google', 'query', '搜索', '网络', '查询', '互联网'], + deferLoading: false, // 核心工具,始终可用 + }, + parameters: { + query: { + type: 'string', + description: '搜索查询关键词', + required: true, + }, + max_results: { + type: 'number', + description: '返回结果数量(默认 5,最大 20)', + required: false, + }, + search_depth: { + type: 'string', + description: '搜索深度: "basic" 快速搜索,"advanced" 深度搜索(默认 basic)', + required: false, + }, + topic: { + type: 'string', + description: '搜索主题: "general" 通用,"news" 新闻,"finance" 财经(默认 general)', + required: false, + }, + include_answer: { + type: 'boolean', + description: '是否包含 AI 生成的摘要答案(默认 true)', + required: false, + }, + }, + execute: async (params: Record): Promise => { + const query = params.query as string; + const maxResults = Math.min((params.max_results as number) || 5, 20); + const searchDepth = (params.search_depth as 'basic' | 'advanced') || 'basic'; + const topic = (params.topic as 'general' | 'news' | 'finance') || 'general'; + const includeAnswer = params.include_answer !== false; + + // 权限检查 + const permissionManager = getPermissionManager(); + const permResult = await permissionManager.checkWebPermission({ + query, + searchDepth, + topic, + maxResults, + }); + + if (!permResult.allowed) { + // 如果需要用户确认但没有设置回调,返回等待确认的状态 + if (permResult.needsConfirmation) { + return { + success: false, + output: '', + error: `需要用户确认网络搜索: "${query}"\n原因: ${permResult.reason || '需要权限确认'}`, + }; + } + + return { + success: false, + output: '', + error: `网络搜索权限被拒绝: ${permResult.reason || '搜索不被允许'}`, + }; + } + + // 获取 Tavily API Key + const config = getConfig(); + const apiKey = process.env.TAVILY_API_KEY || config.tavilyApiKey; + + if (!apiKey) { + return { + success: false, + output: '', + error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY 或在配置文件中添加 tavilyApiKey。', + }; + } + + try { + // 使用 Tavily SDK + const client = tavily({ apiKey }); + const response = await client.search(query, { + searchDepth, + topic, + maxResults, + includeAnswer, + }); + + // 格式化输出 + let output = `## 搜索结果: "${query}"\n\n`; + + // 如果有 AI 摘要答案 + if (response.answer) { + output += `### 摘要\n${response.answer}\n\n`; + } + + // 搜索结果列表 + if (response.results && response.results.length > 0) { + output += `### 相关链接 (${response.results.length} 条)\n\n`; + + for (let i = 0; i < response.results.length; i++) { + const result = response.results[i]; + output += `**${i + 1}. ${result.title}**\n`; + output += `链接: ${result.url}\n`; + // 截断过长的内容 + const content = result.content.length > 300 + ? result.content.substring(0, 300) + '...' + : result.content; + output += `${content}\n\n`; + } + } else { + output += '未找到相关结果。\n'; + } + + return { + success: true, + output, + }; + } catch (error) { + return { + success: false, + output: '', + error: `搜索失败: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, +}; diff --git a/src/utils/config.ts b/src/utils/config.ts index ea129ab..73f83a5 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -12,6 +12,7 @@ interface StoredConfig { deepseekApiKey?: string; model?: string; maxTokens?: number; + tavilyApiKey?: string; } // 默认模型配置 @@ -40,6 +41,19 @@ const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手 当前工作目录: ${process.cwd()} 操作系统: ${process.platform}`; +// 获取原始配置(包含所有字段) +export function getConfig(): StoredConfig { + if (fs.existsSync(CONFIG_FILE)) { + try { + const content = fs.readFileSync(CONFIG_FILE, 'utf-8'); + return JSON.parse(content); + } catch { + return {}; + } + } + return {}; +} + // 加载配置 export function loadConfig(): AgentConfig { // 从环境变量获取