mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-28 12:39:15 +00:00
Compare commits
1 Commits
update-sec
...
v0.0.7-nig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eda442a1c |
@@ -24,7 +24,7 @@ jobs:
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
with:
|
||||
version: 0.0.7
|
||||
version: 0.0.6
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
settings_json: |
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
ISSUES_TO_TRIAGE: ${{ steps.find_issues.outputs.issues_to_triage }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
with:
|
||||
version: 0.0.7
|
||||
version: 0.0.6
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
OPENAI_MODEL: ${{ secrets.OPENAI_MODEL }}
|
||||
|
||||
10
.github/workflows/qwen-code-pr-review.yml
vendored
10
.github/workflows/qwen-code-pr-review.yml
vendored
@@ -18,11 +18,7 @@ jobs:
|
||||
review-pr:
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
github.event.action == 'opened' &&
|
||||
(github.event.pull_request.author_association == 'OWNER' ||
|
||||
github.event.pull_request.author_association == 'MEMBER' ||
|
||||
github.event.pull_request.author_association == 'COLLABORATOR')) ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'opened') ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@qwen /review') &&
|
||||
@@ -53,9 +49,9 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get PR details (pull_request_target & workflow_dispatch)
|
||||
- name: Get PR details (pull_request & workflow_dispatch)
|
||||
id: get_pr
|
||||
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,26 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.7
|
||||
|
||||
- Fix MCP tools
|
||||
- Fix Web Fetch tool
|
||||
- Fix Web Search tool, by replacing web search from Google/Gemini to Tavily API
|
||||
- Fix: Compatible with occasional tool call parameters returned by LLM that are invalid JSON
|
||||
- Fix: prevent concurrent query submissions on some rare cases
|
||||
- Fix: incorrect qwen logger exit handler setup
|
||||
- Fix: seperate static QR code and dynamic spin components
|
||||
- Sync gemini-cli to v0.1.18
|
||||
|
||||
## 0.0.6
|
||||
|
||||
- Add usage statistics logging for Qwen integration
|
||||
- Make `/init` command respect configured context filename and align docs with QWEN.md
|
||||
- Fix EPERM error when run `qwen --sandbox` in macOS
|
||||
- Fix terminal flicker when waiting for login
|
||||
- Fix `glm-4.5` model request error
|
||||
|
||||
## 0.0.5
|
||||
|
||||
- Support Qwen OAuth login and provide up to 2000 free requests per day
|
||||
- Sync gemini-cli to v0.1.17
|
||||
- Add systemPromptMappings Configuration Feature
|
||||
14
README.md
14
README.md
@@ -15,20 +15,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<!-- Keep these links. Translations will automatically update with the README. -->
|
||||
<a href="https://readme-i18n.com/de/QwenLM/qwen-code">Deutsch</a> |
|
||||
<a href="https://readme-i18n.com/es/QwenLM/qwen-code">Español</a> |
|
||||
<a href="https://readme-i18n.com/fr/QwenLM/qwen-code">français</a> |
|
||||
<a href="https://readme-i18n.com/ja/QwenLM/qwen-code">日本語</a> |
|
||||
<a href="https://readme-i18n.com/ko/QwenLM/qwen-code">한국어</a> |
|
||||
<a href="https://readme-i18n.com/pt/QwenLM/qwen-code">Português</a> |
|
||||
<a href="https://readme-i18n.com/ru/QwenLM/qwen-code">Русский</a> |
|
||||
<a href="https://readme-i18n.com/zh/QwenLM/qwen-code">中文</a>
|
||||
|
||||
</div>
|
||||
|
||||
Qwen Code is a powerful command-line AI workflow tool adapted from [**Gemini CLI**](https://github.com/google-gemini/gemini-cli) ([details](./README.gemini.md)), specifically optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder) models. It enhances your development workflow with advanced code understanding, automated tasks, and intelligent assistance.
|
||||
|
||||
## 💡 Free Options Available
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Reporting Security Issues
|
||||
|
||||
Please report any security issue or Higress crash report to [ASRC](https://security.alibaba.com/) (Alibaba Security Response Center) where the issue will be triaged appropriately.
|
||||
To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz).
|
||||
We use g.co/vulnz for our intake, and do coordination and disclosure here on
|
||||
GitHub (including using GitHub Security Advisory). The Google Security Team will
|
||||
respond within 5 working days of your report on g.co/vulnz.
|
||||
|
||||
Thank you for helping keep our project secure.
|
||||
[GitHub Security Advisory]: https://github.com/google-gemini/gemini-cli/security/advisories
|
||||
|
||||
@@ -4,25 +4,24 @@ This document describes the `web_fetch` tool for the Gemini CLI.
|
||||
|
||||
## Description
|
||||
|
||||
Use `web_fetch` to fetch content from a specified URL and process it using an AI model. The tool takes a URL and a prompt as input, fetches the URL content, converts HTML to markdown, and processes the content with the prompt using a small, fast model.
|
||||
Use `web_fetch` to summarize, compare, or extract information from web pages. The `web_fetch` tool processes content from one or more URLs (up to 20) embedded in a prompt. `web_fetch` takes a natural language prompt and returns a generated response.
|
||||
|
||||
### Arguments
|
||||
|
||||
`web_fetch` takes two arguments:
|
||||
`web_fetch` takes one argument:
|
||||
|
||||
- `url` (string, required): The URL to fetch content from. Must be a fully-formed valid URL starting with `http://` or `https://`.
|
||||
- `prompt` (string, required): The prompt describing what information you want to extract from the page content.
|
||||
- `prompt` (string, required): A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content. For example: `"Summarize https://example.com/article and extract key points from https://another.com/data"`. The prompt must contain at least one URL starting with `http://` or `https://`.
|
||||
|
||||
## How to use `web_fetch` with the Gemini CLI
|
||||
|
||||
To use `web_fetch` with the Gemini CLI, provide a URL and a prompt describing what you want to extract from that URL. The tool will ask for confirmation before fetching the URL. Once confirmed, the tool will fetch the content directly and process it using an AI model.
|
||||
To use `web_fetch` with the Gemini CLI, provide a natural language prompt that contains URLs. The tool will ask for confirmation before fetching any URLs. Once confirmed, the tool will process URLs through Gemini API's `urlContext`.
|
||||
|
||||
The tool automatically converts HTML to text, handles GitHub blob URLs (converting them to raw URLs), and upgrades HTTP URLs to HTTPS for security.
|
||||
If the Gemini API cannot access the URL, the tool will fall back to fetching content directly from the local machine. The tool will format the response, including source attribution and citations where possible. The tool will then provide the response to the user.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
web_fetch(url="https://example.com", prompt="Summarize the main points of this article")
|
||||
web_fetch(prompt="Your prompt, including a URL such as https://google.com.")
|
||||
```
|
||||
|
||||
## `web_fetch` examples
|
||||
@@ -30,25 +29,16 @@ web_fetch(url="https://example.com", prompt="Summarize the main points of this a
|
||||
Summarize a single article:
|
||||
|
||||
```
|
||||
web_fetch(url="https://example.com/news/latest", prompt="Can you summarize the main points of this article?")
|
||||
web_fetch(prompt="Can you summarize the main points of https://example.com/news/latest")
|
||||
```
|
||||
|
||||
Extract specific information:
|
||||
Compare two articles:
|
||||
|
||||
```
|
||||
web_fetch(url="https://arxiv.org/abs/2401.0001", prompt="What are the key findings and methodology described in this paper?")
|
||||
```
|
||||
|
||||
Analyze GitHub documentation:
|
||||
|
||||
```
|
||||
web_fetch(url="https://github.com/google/gemini-react/blob/main/README.md", prompt="What are the installation steps and main features?")
|
||||
web_fetch(prompt="What are the differences in the conclusions of these two papers: https://arxiv.org/abs/2401.0001 and https://arxiv.org/abs/2401.0002?")
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- **Single URL processing:** `web_fetch` processes one URL at a time. To analyze multiple URLs, make separate calls to the tool.
|
||||
- **URL format:** The tool automatically upgrades HTTP URLs to HTTPS and converts GitHub blob URLs to raw format for better content access.
|
||||
- **Content processing:** The tool fetches content directly and processes it using an AI model, converting HTML to readable text format.
|
||||
- **URL processing:** `web_fetch` relies on the Gemini API's ability to access and process the given URLs.
|
||||
- **Output quality:** The quality of the output will depend on the clarity of the instructions in the prompt.
|
||||
- **MCP tools:** If an MCP-provided web fetch tool is available (starting with "mcp\_\_"), prefer using that tool as it may have fewer restrictions.
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -11700,7 +11700,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"dependencies": {
|
||||
"@google/genai": "1.9.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
@@ -11904,7 +11904,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"dependencies": {
|
||||
"@google/genai": "1.9.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
@@ -12052,7 +12052,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.1.18",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
@@ -12063,7 +12063,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "qwen-code-vscode-ide-companion",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -13,7 +13,7 @@
|
||||
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.7"
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.7-nightly.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"description": "Qwen Code",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.7"
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.7-nightly.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "1.9.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"description": "Qwen Code Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -11,7 +11,6 @@ import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
import { Config } from '../config/config.js';
|
||||
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
|
||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||
|
||||
// Mock the child_process module to control grep/git grep behavior
|
||||
vi.mock('child_process', () => ({
|
||||
@@ -33,14 +32,9 @@ describe('GrepTool', () => {
|
||||
let grepTool: GrepTool;
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
const mockFileService = {
|
||||
getGeminiIgnorePatterns: () => [],
|
||||
} as unknown as FileDiscoveryService;
|
||||
|
||||
const mockConfig = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
|
||||
getFileService: () => mockFileService,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -226,43 +220,6 @@ describe('GrepTool', () => {
|
||||
"Model provided invalid parameters. Error: params must have required property 'pattern'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should exclude files matching geminiIgnorePatterns', async () => {
|
||||
// Create a file that should be ignored
|
||||
await fs.writeFile(
|
||||
path.join(tempRootDir, 'ignored-file.txt'),
|
||||
'this file should be ignored\nit contains the word world',
|
||||
);
|
||||
|
||||
// Update the mock file service to return ignore patterns
|
||||
mockFileService.getGeminiIgnorePatterns = () => ['ignored-file.txt'];
|
||||
|
||||
// Re-create the grep tool with the updated mock
|
||||
const grepToolWithIgnore = new GrepTool(mockConfig);
|
||||
|
||||
// Search for 'world' which exists in both the regular file and the ignored file
|
||||
const params: GrepToolParams = { pattern: 'world' };
|
||||
const result = await grepToolWithIgnore.execute(params, abortSignal);
|
||||
|
||||
// Should only find matches in the non-ignored files (3 matches)
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 3 matches for pattern "world" in the workspace directory',
|
||||
);
|
||||
|
||||
// Should find matches in the regular files
|
||||
expect(result.llmContent).toContain('File: fileA.txt');
|
||||
expect(result.llmContent).toContain('L1: hello world');
|
||||
expect(result.llmContent).toContain('L2: second line with world');
|
||||
expect(result.llmContent).toContain(
|
||||
`File: ${path.join('sub', 'fileC.txt')}`,
|
||||
);
|
||||
expect(result.llmContent).toContain('L1: another world in sub dir');
|
||||
|
||||
// Should NOT find matches in the ignored file
|
||||
expect(result.llmContent).not.toContain('ignored-file.txt');
|
||||
|
||||
expect(result.returnDisplay).toBe('Found 3 matches');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-directory workspace', () => {
|
||||
@@ -281,15 +238,10 @@ describe('GrepTool', () => {
|
||||
);
|
||||
|
||||
// Create a mock config with multiple directories
|
||||
const multiDirFileService = {
|
||||
getGeminiIgnorePatterns: () => [],
|
||||
};
|
||||
|
||||
const multiDirConfig = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
||||
getFileService: () => multiDirFileService,
|
||||
} as unknown as Config;
|
||||
|
||||
const multiDirGrepTool = new GrepTool(multiDirConfig);
|
||||
@@ -335,15 +287,10 @@ describe('GrepTool', () => {
|
||||
);
|
||||
|
||||
// Create a mock config with multiple directories
|
||||
const multiDirFileService = {
|
||||
getGeminiIgnorePatterns: () => [],
|
||||
};
|
||||
|
||||
const multiDirConfig = {
|
||||
getTargetDir: () => tempRootDir,
|
||||
getWorkspaceContext: () =>
|
||||
createMockWorkspaceContext(tempRootDir, [secondDir]),
|
||||
getFileService: () => multiDirFileService,
|
||||
} as unknown as Config;
|
||||
|
||||
const multiDirGrepTool = new GrepTool(multiDirConfig);
|
||||
|
||||
@@ -536,18 +536,12 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> {
|
||||
);
|
||||
strategyUsed = 'javascript fallback';
|
||||
const globPattern = include ? include : '**/*';
|
||||
|
||||
// Get the file discovery service to check ignore patterns
|
||||
const fileDiscovery = this.config.getFileService();
|
||||
|
||||
// Basic ignore patterns
|
||||
const ignorePatterns = [
|
||||
'.git/**',
|
||||
'node_modules/**',
|
||||
'bower_components/**',
|
||||
'.svn/**',
|
||||
'.hg/**',
|
||||
...fileDiscovery.getGeminiIgnorePatterns(),
|
||||
]; // Use glob patterns for ignores here
|
||||
|
||||
const filesIterator = globIterate(globPattern, {
|
||||
|
||||
@@ -203,7 +203,7 @@ describe('MemoryTool', () => {
|
||||
});
|
||||
|
||||
it('should call performAddMemoryEntry with correct parameters and return success', async () => {
|
||||
const params = { fact: 'The sky is blue', scope: 'global' as const };
|
||||
const params = { fact: 'The sky is blue' };
|
||||
const result = await memoryTool.execute(params, mockAbortSignal);
|
||||
// Use getCurrentGeminiMdFilename for the default expectation before any setGeminiMdFilename calls in a test
|
||||
const expectedFilePath = path.join(
|
||||
@@ -224,7 +224,7 @@ describe('MemoryTool', () => {
|
||||
expectedFilePath,
|
||||
expectedFsArgument,
|
||||
);
|
||||
const successMessage = `Okay, I've remembered that in global memory: "${params.fact}"`;
|
||||
const successMessage = `Okay, I've remembered that: "${params.fact}"`;
|
||||
expect(result.llmContent).toBe(
|
||||
JSON.stringify({ success: true, message: successMessage }),
|
||||
);
|
||||
@@ -244,7 +244,7 @@ describe('MemoryTool', () => {
|
||||
});
|
||||
|
||||
it('should handle errors from performAddMemoryEntry', async () => {
|
||||
const params = { fact: 'This will fail', scope: 'global' as const };
|
||||
const params = { fact: 'This will fail' };
|
||||
const underlyingError = new Error(
|
||||
'[MemoryTool] Failed to add memory entry: Disk full',
|
||||
);
|
||||
@@ -276,7 +276,7 @@ describe('MemoryTool', () => {
|
||||
});
|
||||
|
||||
it('should return confirmation details when memory file is not allowlisted', async () => {
|
||||
const params = { fact: 'Test fact', scope: 'global' as const };
|
||||
const params = { fact: 'Test fact' };
|
||||
const result = await memoryTool.shouldConfirmExecute(
|
||||
params,
|
||||
mockAbortSignal,
|
||||
@@ -287,9 +287,7 @@ describe('MemoryTool', () => {
|
||||
|
||||
if (result && result.type === 'edit') {
|
||||
const expectedPath = path.join('~', '.qwen', 'QWEN.md');
|
||||
expect(result.title).toBe(
|
||||
`Confirm Memory Save: ${expectedPath} (global)`,
|
||||
);
|
||||
expect(result.title).toBe(`Confirm Memory Save: ${expectedPath}`);
|
||||
expect(result.fileName).toContain(path.join('mock', 'home', '.qwen'));
|
||||
expect(result.fileName).toContain('QWEN.md');
|
||||
expect(result.fileDiff).toContain('Index: QWEN.md');
|
||||
@@ -302,16 +300,16 @@ describe('MemoryTool', () => {
|
||||
});
|
||||
|
||||
it('should return false when memory file is already allowlisted', async () => {
|
||||
const params = { fact: 'Test fact', scope: 'global' as const };
|
||||
const params = { fact: 'Test fact' };
|
||||
const memoryFilePath = path.join(
|
||||
os.homedir(),
|
||||
'.qwen',
|
||||
getCurrentGeminiMdFilename(),
|
||||
);
|
||||
|
||||
// Add the memory file to the allowlist with the new key format
|
||||
// Add the memory file to the allowlist
|
||||
(MemoryTool as unknown as { allowlist: Set<string> }).allowlist.add(
|
||||
`${memoryFilePath}_global`,
|
||||
memoryFilePath,
|
||||
);
|
||||
|
||||
const result = await memoryTool.shouldConfirmExecute(
|
||||
@@ -323,7 +321,7 @@ describe('MemoryTool', () => {
|
||||
});
|
||||
|
||||
it('should add memory file to allowlist when ProceedAlways is confirmed', async () => {
|
||||
const params = { fact: 'Test fact', scope: 'global' as const };
|
||||
const params = { fact: 'Test fact' };
|
||||
const memoryFilePath = path.join(
|
||||
os.homedir(),
|
||||
'.qwen',
|
||||
@@ -342,10 +340,10 @@ describe('MemoryTool', () => {
|
||||
// Simulate the onConfirm callback
|
||||
await result.onConfirm(ToolConfirmationOutcome.ProceedAlways);
|
||||
|
||||
// Check that the memory file was added to the allowlist with the new key format
|
||||
// Check that the memory file was added to the allowlist
|
||||
expect(
|
||||
(MemoryTool as unknown as { allowlist: Set<string> }).allowlist.has(
|
||||
`${memoryFilePath}_global`,
|
||||
memoryFilePath,
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
@@ -386,7 +384,7 @@ describe('MemoryTool', () => {
|
||||
});
|
||||
|
||||
it('should handle existing memory file with content', async () => {
|
||||
const params = { fact: 'New fact', scope: 'global' as const };
|
||||
const params = { fact: 'New fact' };
|
||||
const existingContent =
|
||||
'Some existing content.\n\n## Qwen Added Memories\n- Old fact\n';
|
||||
|
||||
@@ -403,9 +401,7 @@ describe('MemoryTool', () => {
|
||||
|
||||
if (result && result.type === 'edit') {
|
||||
const expectedPath = path.join('~', '.qwen', 'QWEN.md');
|
||||
expect(result.title).toBe(
|
||||
`Confirm Memory Save: ${expectedPath} (global)`,
|
||||
);
|
||||
expect(result.title).toBe(`Confirm Memory Save: ${expectedPath}`);
|
||||
expect(result.fileDiff).toContain('Index: QWEN.md');
|
||||
expect(result.fileDiff).toContain('+- New fact');
|
||||
expect(result.originalContent).toBe(existingContent);
|
||||
@@ -413,37 +409,5 @@ describe('MemoryTool', () => {
|
||||
expect(result.newContent).toContain('- New fact');
|
||||
}
|
||||
});
|
||||
|
||||
it('should prompt for scope selection when scope is not specified', async () => {
|
||||
const params = { fact: 'Test fact' };
|
||||
const result = await memoryTool.shouldConfirmExecute(
|
||||
params,
|
||||
mockAbortSignal,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).not.toBe(false);
|
||||
|
||||
if (result && result.type === 'edit') {
|
||||
expect(result.title).toBe('Choose Memory Storage Location');
|
||||
expect(result.fileName).toBe('Memory Storage Options');
|
||||
expect(result.fileDiff).toContain('Choose where to save this memory');
|
||||
expect(result.fileDiff).toContain('Test fact');
|
||||
expect(result.fileDiff).toContain('Global:');
|
||||
expect(result.fileDiff).toContain('Project:');
|
||||
expect(result.originalContent).toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return error when executing without scope parameter', async () => {
|
||||
const params = { fact: 'Test fact' };
|
||||
const result = await memoryTool.execute(params, mockAbortSignal);
|
||||
|
||||
expect(result.llmContent).toContain(
|
||||
'Please specify where to save this memory',
|
||||
);
|
||||
expect(result.returnDisplay).toContain('Global:');
|
||||
expect(result.returnDisplay).toContain('Project:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,12 +32,6 @@ const memoryToolSchemaData: FunctionDeclaration = {
|
||||
description:
|
||||
'The specific fact or piece of information to remember. Should be a clear, self-contained statement.',
|
||||
},
|
||||
scope: {
|
||||
type: Type.STRING,
|
||||
description:
|
||||
'Where to save the memory: "global" saves to user-level ~/.qwen/QWEN.md (shared across all projects), "project" saves to current project\'s QWEN.md (project-specific). If not specified, will prompt user to choose.',
|
||||
enum: ['global', 'project'],
|
||||
},
|
||||
},
|
||||
required: ['fact'],
|
||||
},
|
||||
@@ -60,10 +54,6 @@ Do NOT use this tool:
|
||||
## Parameters
|
||||
|
||||
- \`fact\` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement. For example, if the user says "My favorite color is blue", the fact would be "My favorite color is blue".
|
||||
- \`scope\` (string, optional): Where to save the memory:
|
||||
- "global": Saves to user-level ~/.qwen/QWEN.md (shared across all projects)
|
||||
- "project": Saves to current project's QWEN.md (project-specific)
|
||||
- If not specified, the tool will ask the user where they want to save the memory.
|
||||
`;
|
||||
|
||||
export const GEMINI_CONFIG_DIR = '.qwen';
|
||||
@@ -102,23 +92,12 @@ interface SaveMemoryParams {
|
||||
fact: string;
|
||||
modified_by_user?: boolean;
|
||||
modified_content?: string;
|
||||
scope?: 'global' | 'project';
|
||||
}
|
||||
|
||||
function getGlobalMemoryFilePath(): string {
|
||||
return path.join(homedir(), GEMINI_CONFIG_DIR, getCurrentGeminiMdFilename());
|
||||
}
|
||||
|
||||
function getProjectMemoryFilePath(): string {
|
||||
return path.join(process.cwd(), getCurrentGeminiMdFilename());
|
||||
}
|
||||
|
||||
function getMemoryFilePath(scope: 'global' | 'project' = 'global'): string {
|
||||
return scope === 'project'
|
||||
? getProjectMemoryFilePath()
|
||||
: getGlobalMemoryFilePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures proper newline separation before appending content.
|
||||
*/
|
||||
@@ -148,20 +127,17 @@ export class MemoryTool
|
||||
);
|
||||
}
|
||||
|
||||
getDescription(params: SaveMemoryParams): string {
|
||||
const scope = params.scope || 'global';
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
return `in ${tildeifyPath(memoryFilePath)} (${scope})`;
|
||||
getDescription(_params: SaveMemoryParams): string {
|
||||
const memoryFilePath = getGlobalMemoryFilePath();
|
||||
return `in ${tildeifyPath(memoryFilePath)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current content of the memory file
|
||||
*/
|
||||
private async readMemoryFileContent(
|
||||
scope: 'global' | 'project' = 'global',
|
||||
): Promise<string> {
|
||||
private async readMemoryFileContent(): Promise<string> {
|
||||
try {
|
||||
return await fs.readFile(getMemoryFilePath(scope), 'utf-8');
|
||||
return await fs.readFile(getGlobalMemoryFilePath(), 'utf-8');
|
||||
} catch (err) {
|
||||
const error = err as Error & { code?: string };
|
||||
if (!(error instanceof Error) || error.code !== 'ENOENT') throw err;
|
||||
@@ -217,35 +193,15 @@ export class MemoryTool
|
||||
params: SaveMemoryParams,
|
||||
_abortSignal: AbortSignal,
|
||||
): Promise<ToolEditConfirmationDetails | false> {
|
||||
// If scope is not specified, prompt the user to choose
|
||||
if (!params.scope) {
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
|
||||
const confirmationDetails: ToolEditConfirmationDetails = {
|
||||
type: 'edit',
|
||||
title: `Choose Memory Storage Location`,
|
||||
fileName: 'Memory Storage Options',
|
||||
fileDiff: `Choose where to save this memory:\n\n"${params.fact}"\n\nOptions:\n- Global: ${globalPath} (shared across all projects)\n- Project: ${projectPath} (current project only)\n\nPlease specify the scope parameter: "global" or "project"`,
|
||||
originalContent: '',
|
||||
newContent: `Memory to save: ${params.fact}\n\nScope options:\n- global: ${globalPath}\n- project: ${projectPath}`,
|
||||
onConfirm: async (_outcome: ToolConfirmationOutcome) => {
|
||||
// This will be handled by the execution flow
|
||||
},
|
||||
};
|
||||
return confirmationDetails;
|
||||
}
|
||||
|
||||
const scope = params.scope;
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
const allowlistKey = `${memoryFilePath}_${scope}`;
|
||||
const memoryFilePath = getGlobalMemoryFilePath();
|
||||
const allowlistKey = memoryFilePath;
|
||||
|
||||
if (MemoryTool.allowlist.has(allowlistKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read current content of the memory file
|
||||
const currentContent = await this.readMemoryFileContent(scope);
|
||||
const currentContent = await this.readMemoryFileContent();
|
||||
|
||||
// Calculate the new content that will be written to the memory file
|
||||
const newContent = this.computeNewContent(currentContent, params.fact);
|
||||
@@ -262,7 +218,7 @@ export class MemoryTool
|
||||
|
||||
const confirmationDetails: ToolEditConfirmationDetails = {
|
||||
type: 'edit',
|
||||
title: `Confirm Memory Save: ${tildeifyPath(memoryFilePath)} (${scope})`,
|
||||
title: `Confirm Memory Save: ${tildeifyPath(memoryFilePath)}`,
|
||||
fileName: memoryFilePath,
|
||||
fileDiff,
|
||||
originalContent: currentContent,
|
||||
@@ -360,27 +316,18 @@ export class MemoryTool
|
||||
};
|
||||
}
|
||||
|
||||
// If scope is not specified, prompt the user to choose
|
||||
if (!params.scope) {
|
||||
const errorMessage =
|
||||
'Please specify where to save this memory. Use scope parameter: "global" for user-level (~/.qwen/QWEN.md) or "project" for current project (./QWEN.md).';
|
||||
return {
|
||||
llmContent: JSON.stringify({ success: false, error: errorMessage }),
|
||||
returnDisplay: `${errorMessage}\n\nGlobal: ${tildeifyPath(getMemoryFilePath('global'))}\nProject: ${tildeifyPath(getMemoryFilePath('project'))}`,
|
||||
};
|
||||
}
|
||||
|
||||
const scope = params.scope;
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
|
||||
try {
|
||||
if (modified_by_user && modified_content !== undefined) {
|
||||
// User modified the content in external editor, write it directly
|
||||
await fs.mkdir(path.dirname(memoryFilePath), {
|
||||
await fs.mkdir(path.dirname(getGlobalMemoryFilePath()), {
|
||||
recursive: true,
|
||||
});
|
||||
await fs.writeFile(memoryFilePath, modified_content, 'utf-8');
|
||||
const successMessage = `Okay, I've updated the ${scope} memory file with your modifications.`;
|
||||
await fs.writeFile(
|
||||
getGlobalMemoryFilePath(),
|
||||
modified_content,
|
||||
'utf-8',
|
||||
);
|
||||
const successMessage = `Okay, I've updated the memory file with your modifications.`;
|
||||
return {
|
||||
llmContent: JSON.stringify({
|
||||
success: true,
|
||||
@@ -390,12 +337,16 @@ export class MemoryTool
|
||||
};
|
||||
} else {
|
||||
// Use the normal memory entry logic
|
||||
await MemoryTool.performAddMemoryEntry(fact, memoryFilePath, {
|
||||
readFile: fs.readFile,
|
||||
writeFile: fs.writeFile,
|
||||
mkdir: fs.mkdir,
|
||||
});
|
||||
const successMessage = `Okay, I've remembered that in ${scope} memory: "${fact}"`;
|
||||
await MemoryTool.performAddMemoryEntry(
|
||||
fact,
|
||||
getGlobalMemoryFilePath(),
|
||||
{
|
||||
readFile: fs.readFile,
|
||||
writeFile: fs.writeFile,
|
||||
mkdir: fs.mkdir,
|
||||
},
|
||||
);
|
||||
const successMessage = `Okay, I've remembered that: "${fact}"`;
|
||||
return {
|
||||
llmContent: JSON.stringify({
|
||||
success: true,
|
||||
@@ -408,7 +359,7 @@ export class MemoryTool
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
`[MemoryTool] Error executing save_memory for fact "${fact}" in ${scope}: ${errorMessage}`,
|
||||
`[MemoryTool] Error executing save_memory for fact "${fact}": ${errorMessage}`,
|
||||
);
|
||||
return {
|
||||
llmContent: JSON.stringify({
|
||||
@@ -422,13 +373,11 @@ export class MemoryTool
|
||||
|
||||
getModifyContext(_abortSignal: AbortSignal): ModifyContext<SaveMemoryParams> {
|
||||
return {
|
||||
getFilePath: (params: SaveMemoryParams) =>
|
||||
getMemoryFilePath(params.scope || 'global'),
|
||||
getCurrentContent: async (params: SaveMemoryParams): Promise<string> =>
|
||||
this.readMemoryFileContent(params.scope || 'global'),
|
||||
getFilePath: (_params: SaveMemoryParams) => getGlobalMemoryFilePath(),
|
||||
getCurrentContent: async (_params: SaveMemoryParams): Promise<string> =>
|
||||
this.readMemoryFileContent(),
|
||||
getProposedContent: async (params: SaveMemoryParams): Promise<string> => {
|
||||
const scope = params.scope || 'global';
|
||||
const currentContent = await this.readMemoryFileContent(scope);
|
||||
const currentContent = await this.readMemoryFileContent();
|
||||
return this.computeNewContent(currentContent, params.fact);
|
||||
},
|
||||
createUpdatedParams: (
|
||||
|
||||
@@ -19,27 +19,23 @@ describe('WebFetchTool', () => {
|
||||
describe('shouldConfirmExecute', () => {
|
||||
it('should return confirmation details with the correct prompt and urls', async () => {
|
||||
const tool = new WebFetchTool(mockConfig);
|
||||
const params = {
|
||||
url: 'https://example.com',
|
||||
prompt: 'summarize this page',
|
||||
};
|
||||
const params = { prompt: 'fetch https://example.com' };
|
||||
const confirmationDetails = await tool.shouldConfirmExecute(params);
|
||||
|
||||
expect(confirmationDetails).toEqual({
|
||||
type: 'info',
|
||||
title: 'Confirm Web Fetch',
|
||||
prompt:
|
||||
'Fetch content from https://example.com and process with: summarize this page',
|
||||
prompt: 'fetch https://example.com',
|
||||
urls: ['https://example.com'],
|
||||
onConfirm: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return github urls as-is in confirmation details', async () => {
|
||||
it('should convert github urls to raw format', async () => {
|
||||
const tool = new WebFetchTool(mockConfig);
|
||||
const params = {
|
||||
url: 'https://github.com/google/gemini-react/blob/main/README.md',
|
||||
prompt: 'summarize the README',
|
||||
prompt:
|
||||
'fetch https://github.com/google/gemini-react/blob/main/README.md',
|
||||
};
|
||||
const confirmationDetails = await tool.shouldConfirmExecute(params);
|
||||
|
||||
@@ -47,8 +43,10 @@ describe('WebFetchTool', () => {
|
||||
type: 'info',
|
||||
title: 'Confirm Web Fetch',
|
||||
prompt:
|
||||
'Fetch content from https://github.com/google/gemini-react/blob/main/README.md and process with: summarize the README',
|
||||
urls: ['https://github.com/google/gemini-react/blob/main/README.md'],
|
||||
'fetch https://github.com/google/gemini-react/blob/main/README.md',
|
||||
urls: [
|
||||
'https://raw.githubusercontent.com/google/gemini-react/main/README.md',
|
||||
],
|
||||
onConfirm: expect.any(Function),
|
||||
});
|
||||
});
|
||||
@@ -58,10 +56,7 @@ describe('WebFetchTool', () => {
|
||||
...mockConfig,
|
||||
getApprovalMode: () => ApprovalMode.AUTO_EDIT,
|
||||
} as unknown as Config);
|
||||
const params = {
|
||||
url: 'https://example.com',
|
||||
prompt: 'summarize this page',
|
||||
};
|
||||
const params = { prompt: 'fetch https://example.com' };
|
||||
const confirmationDetails = await tool.shouldConfirmExecute(params);
|
||||
|
||||
expect(confirmationDetails).toBe(false);
|
||||
@@ -73,10 +68,7 @@ describe('WebFetchTool', () => {
|
||||
...mockConfig,
|
||||
setApprovalMode,
|
||||
} as unknown as Config);
|
||||
const params = {
|
||||
url: 'https://example.com',
|
||||
prompt: 'summarize this page',
|
||||
};
|
||||
const params = { prompt: 'fetch https://example.com' };
|
||||
const confirmationDetails = await tool.shouldConfirmExecute(params);
|
||||
|
||||
if (
|
||||
|
||||
@@ -13,25 +13,49 @@ import {
|
||||
Icon,
|
||||
} from './tools.js';
|
||||
import { Type } from '@google/genai';
|
||||
import { getErrorMessage } from '../utils/errors.js';
|
||||
import { Config, ApprovalMode } from '../config/config.js';
|
||||
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
||||
import { fetchWithTimeout } from '../utils/fetch.js';
|
||||
import { fetchWithTimeout, isPrivateIp } from '../utils/fetch.js';
|
||||
import { convert } from 'html-to-text';
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
|
||||
const URL_FETCH_TIMEOUT_MS = 10000;
|
||||
const MAX_CONTENT_LENGTH = 100000;
|
||||
|
||||
// Helper function to extract URLs from a string
|
||||
function extractUrls(text: string): string[] {
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
return text.match(urlRegex) || [];
|
||||
}
|
||||
|
||||
// Interfaces for grounding metadata (similar to web-search.ts)
|
||||
interface GroundingChunkWeb {
|
||||
uri?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface GroundingChunkItem {
|
||||
web?: GroundingChunkWeb;
|
||||
}
|
||||
|
||||
interface GroundingSupportSegment {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface GroundingSupportItem {
|
||||
segment?: GroundingSupportSegment;
|
||||
groundingChunkIndices?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for the WebFetch tool
|
||||
*/
|
||||
export interface WebFetchToolParams {
|
||||
/**
|
||||
* The URL to fetch content from
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* The prompt to run on the fetched content
|
||||
* The prompt containing URL(s) (up to 20) and instructions for processing their content.
|
||||
*/
|
||||
prompt: string;
|
||||
}
|
||||
@@ -46,20 +70,17 @@ export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
|
||||
super(
|
||||
WebFetchTool.Name,
|
||||
'WebFetch',
|
||||
'Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model\'s response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".\n - The URL must be a fully-formed valid URL\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large',
|
||||
"Processes content from URL(s), including local and private network addresses (e.g., localhost), embedded in a prompt. Include up to 20 URLs and instructions (e.g., summarize, extract specific data) directly in the 'prompt' parameter.",
|
||||
Icon.Globe,
|
||||
{
|
||||
properties: {
|
||||
url: {
|
||||
description: 'The URL to fetch content from',
|
||||
type: Type.STRING,
|
||||
},
|
||||
prompt: {
|
||||
description: 'The prompt to run on the fetched content',
|
||||
description:
|
||||
'A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content (e.g., "Summarize https://example.com/article and extract key points from https://another.com/data"). Must contain as least one URL starting with http:// or https://.',
|
||||
type: Type.STRING,
|
||||
},
|
||||
},
|
||||
required: ['url', 'prompt'],
|
||||
required: ['prompt'],
|
||||
type: Type.OBJECT,
|
||||
},
|
||||
);
|
||||
@@ -69,11 +90,19 @@ export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
|
||||
}
|
||||
}
|
||||
|
||||
private async executeFetch(
|
||||
private async executeFallback(
|
||||
params: WebFetchToolParams,
|
||||
signal: AbortSignal,
|
||||
): Promise<ToolResult> {
|
||||
let url = params.url;
|
||||
const urls = extractUrls(params.prompt);
|
||||
if (urls.length === 0) {
|
||||
return {
|
||||
llmContent: 'Error: No URL found in the prompt for fallback.',
|
||||
returnDisplay: 'Error: No URL found in the prompt for fallback.',
|
||||
};
|
||||
}
|
||||
// For now, we only support one URL for fallback
|
||||
let url = urls[0];
|
||||
|
||||
// Convert GitHub blob URL to raw URL
|
||||
if (url.includes('github.com') && url.includes('/blob/')) {
|
||||
@@ -101,7 +130,7 @@ export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
|
||||
const geminiClient = this.config.getGeminiClient();
|
||||
const fallbackPrompt = `The user requested the following: "${params.prompt}".
|
||||
|
||||
I have fetched the content from ${params.url}. Please use the following content to answer the user's request.
|
||||
I was unable to access the URL directly. Instead, I have fetched the raw content of the page. Please use the following content to answer the user's request. Do not attempt to access the URL again.
|
||||
|
||||
---
|
||||
${textContent}
|
||||
@@ -114,11 +143,11 @@ ${textContent}
|
||||
const resultText = getResponseText(result) || '';
|
||||
return {
|
||||
llmContent: resultText,
|
||||
returnDisplay: `Content from ${params.url} processed successfully.`,
|
||||
returnDisplay: `Content for ${url} processed using fallback fetch.`,
|
||||
};
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
const errorMessage = `Error during fetch for ${url}: ${error.message}`;
|
||||
const errorMessage = `Error during fallback fetch for ${url}: ${error.message}`;
|
||||
return {
|
||||
llmContent: `Error: ${errorMessage}`,
|
||||
returnDisplay: `Error: ${errorMessage}`,
|
||||
@@ -131,17 +160,14 @@ ${textContent}
|
||||
if (errors) {
|
||||
return errors;
|
||||
}
|
||||
if (!params.url || params.url.trim() === '') {
|
||||
return "The 'url' parameter cannot be empty.";
|
||||
if (!params.prompt || params.prompt.trim() === '') {
|
||||
return "The 'prompt' parameter cannot be empty and must contain URL(s) and instructions.";
|
||||
}
|
||||
if (
|
||||
!params.url.startsWith('http://') &&
|
||||
!params.url.startsWith('https://')
|
||||
!params.prompt.includes('http://') &&
|
||||
!params.prompt.includes('https://')
|
||||
) {
|
||||
return "The 'url' must be a valid URL starting with http:// or https://.";
|
||||
}
|
||||
if (!params.prompt || params.prompt.trim() === '') {
|
||||
return "The 'prompt' parameter cannot be empty.";
|
||||
return "The 'prompt' must contain at least one valid URL (starting with http:// or https://).";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -151,7 +177,7 @@ ${textContent}
|
||||
params.prompt.length > 100
|
||||
? params.prompt.substring(0, 97) + '...'
|
||||
: params.prompt;
|
||||
return `Fetching content from ${params.url} and processing with prompt: "${displayPrompt}"`;
|
||||
return `Processing URLs and instructions from prompt: "${displayPrompt}"`;
|
||||
}
|
||||
|
||||
async shouldConfirmExecute(
|
||||
@@ -166,11 +192,22 @@ ${textContent}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform GitHub URL conversion here to differentiate between user-provided
|
||||
// URL and the actual URL to be fetched.
|
||||
const urls = extractUrls(params.prompt).map((url) => {
|
||||
if (url.includes('github.com') && url.includes('/blob/')) {
|
||||
return url
|
||||
.replace('github.com', 'raw.githubusercontent.com')
|
||||
.replace('/blob/', '/');
|
||||
}
|
||||
return url;
|
||||
});
|
||||
|
||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
||||
type: 'info',
|
||||
title: `Confirm Web Fetch`,
|
||||
prompt: `Fetch content from ${params.url} and process with: ${params.prompt}`,
|
||||
urls: [params.url],
|
||||
prompt: params.prompt,
|
||||
urls,
|
||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
|
||||
@@ -192,6 +229,132 @@ ${textContent}
|
||||
};
|
||||
}
|
||||
|
||||
return this.executeFetch(params, signal);
|
||||
const userPrompt = params.prompt;
|
||||
const urls = extractUrls(userPrompt);
|
||||
const url = urls[0];
|
||||
const isPrivate = isPrivateIp(url);
|
||||
|
||||
if (isPrivate) {
|
||||
return this.executeFallback(params, signal);
|
||||
}
|
||||
|
||||
const geminiClient = this.config.getGeminiClient();
|
||||
|
||||
try {
|
||||
const response = await geminiClient.generateContent(
|
||||
[{ role: 'user', parts: [{ text: userPrompt }] }],
|
||||
{ tools: [{ urlContext: {} }] },
|
||||
signal, // Pass signal
|
||||
);
|
||||
|
||||
console.debug(
|
||||
`[WebFetchTool] Full response for prompt "${userPrompt.substring(
|
||||
0,
|
||||
50,
|
||||
)}...":`,
|
||||
JSON.stringify(response, null, 2),
|
||||
);
|
||||
|
||||
let responseText = getResponseText(response) || '';
|
||||
const urlContextMeta = response.candidates?.[0]?.urlContextMetadata;
|
||||
const groundingMetadata = response.candidates?.[0]?.groundingMetadata;
|
||||
const sources = groundingMetadata?.groundingChunks as
|
||||
| GroundingChunkItem[]
|
||||
| undefined;
|
||||
const groundingSupports = groundingMetadata?.groundingSupports as
|
||||
| GroundingSupportItem[]
|
||||
| undefined;
|
||||
|
||||
// Error Handling
|
||||
let processingError = false;
|
||||
|
||||
if (
|
||||
urlContextMeta?.urlMetadata &&
|
||||
urlContextMeta.urlMetadata.length > 0
|
||||
) {
|
||||
const allStatuses = urlContextMeta.urlMetadata.map(
|
||||
(m) => m.urlRetrievalStatus,
|
||||
);
|
||||
if (allStatuses.every((s) => s !== 'URL_RETRIEVAL_STATUS_SUCCESS')) {
|
||||
processingError = true;
|
||||
}
|
||||
} else if (!responseText.trim() && !sources?.length) {
|
||||
// No URL metadata and no content/sources
|
||||
processingError = true;
|
||||
}
|
||||
|
||||
if (
|
||||
!processingError &&
|
||||
!responseText.trim() &&
|
||||
(!sources || sources.length === 0)
|
||||
) {
|
||||
// Successfully retrieved some URL (or no specific error from urlContextMeta), but no usable text or grounding data.
|
||||
processingError = true;
|
||||
}
|
||||
|
||||
if (processingError) {
|
||||
return this.executeFallback(params, signal);
|
||||
}
|
||||
|
||||
const sourceListFormatted: string[] = [];
|
||||
if (sources && sources.length > 0) {
|
||||
sources.forEach((source: GroundingChunkItem, index: number) => {
|
||||
const title = source.web?.title || 'Untitled';
|
||||
const uri = source.web?.uri || 'Unknown URI'; // Fallback if URI is missing
|
||||
sourceListFormatted.push(`[${index + 1}] ${title} (${uri})`);
|
||||
});
|
||||
|
||||
if (groundingSupports && groundingSupports.length > 0) {
|
||||
const insertions: Array<{ index: number; marker: string }> = [];
|
||||
groundingSupports.forEach((support: GroundingSupportItem) => {
|
||||
if (support.segment && support.groundingChunkIndices) {
|
||||
const citationMarker = support.groundingChunkIndices
|
||||
.map((chunkIndex: number) => `[${chunkIndex + 1}]`)
|
||||
.join('');
|
||||
insertions.push({
|
||||
index: support.segment.endIndex,
|
||||
marker: citationMarker,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
insertions.sort((a, b) => b.index - a.index);
|
||||
const responseChars = responseText.split('');
|
||||
insertions.forEach((insertion) => {
|
||||
responseChars.splice(insertion.index, 0, insertion.marker);
|
||||
});
|
||||
responseText = responseChars.join('');
|
||||
}
|
||||
|
||||
if (sourceListFormatted.length > 0) {
|
||||
responseText += `
|
||||
|
||||
Sources:
|
||||
${sourceListFormatted.join('\n')}`;
|
||||
}
|
||||
}
|
||||
|
||||
const llmContent = responseText;
|
||||
|
||||
console.debug(
|
||||
`[WebFetchTool] Formatted tool response for prompt "${userPrompt}:\n\n":`,
|
||||
llmContent,
|
||||
);
|
||||
|
||||
return {
|
||||
llmContent,
|
||||
returnDisplay: `Content processed from prompt.`,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = `Error processing web content for prompt "${userPrompt.substring(
|
||||
0,
|
||||
50,
|
||||
)}...": ${getErrorMessage(error)}`;
|
||||
console.error(errorMessage, error);
|
||||
return {
|
||||
llmContent: `Error: ${errorMessage}`,
|
||||
returnDisplay: `Error: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.1.18",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "qwen-code-vscode-ide-companion",
|
||||
"displayName": "Qwen Code Companion",
|
||||
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.7-nightly.1",
|
||||
"publisher": "qwenlm",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user