sync: update from private repo (dfadcd5f)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-23 06:38:48 +00:00
parent 6a2f2cc736
commit 29ccaf1e92
377 changed files with 31028 additions and 8994 deletions
+81 -9
View File
@@ -5,6 +5,7 @@ import { logger } from '../../logger.js';
import type { SearchFilterConfig } from '../../config.js';
import { checkSSRF, ssrfSafeFetch } from './shared/ssrf.js';
import { htmlToText } from './shared/html.js';
import { extractReadableMarkdown, truncateForAgent } from './shared/readability.js';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
@@ -14,6 +15,7 @@ import {
SNIFF_HEAD_BYTES,
type BinaryVerdict,
} from './binary-detect.js';
import { saveSourceFile, appendSourceIndex } from './source-library.js';
const BINARY_CONTENT_TYPE_PREFIXES = [
'application/pdf',
@@ -29,6 +31,14 @@ const BINARY_CONTENT_TYPE_PREFIXES = [
// WebFetch text body は最大 5MB で打ち切る(巨大 HTML による context 膨張防止)
const MAX_WEBFETCH_BODY_BYTES = 5 * 1024 * 1024;
// agent のコンテキストに返すクリーン Markdown の上限文字数。
// 全文は source/ に保存されるので、ここは context 節約のための切り詰め。
const MAX_AGENT_RETURN_CHARS = 10000;
// source/ に保存するクリーン Markdown の上限バイト数(病的に巨大なページ対策)。
// 5MB body cap が入力を制限済みだが、Markdown 化後にも保険をかける。
const MAX_SOURCE_MARKDOWN_BYTES = 2 * 1024 * 1024;
// --- ツール定義 ---
const WEBSEARCH_DEF: ToolDef = {
@@ -67,13 +77,13 @@ const DOWNLOADFILE_DEF: ToolDef = {
type: 'function',
function: {
name: 'DownloadFile',
description: 'URL からファイル(PDF・画像・バイナリ等)をダウンロードし、workspace の input/ または output/ に保存する。WebFetch では取得できない PDF や画像ファイルに使う。詳細・命名規約は ReadToolDoc({ name: "DownloadFile" })。',
description: 'URL からファイル(PDF・画像・バイナリ等)をダウンロードし、workspace の input/・output/・source/(調査資料をスペースのソースライブラリに蓄積)に保存する。詳細・命名規約は ReadToolDoc({ name: "DownloadFile" })。',
parameters: {
type: 'object',
properties: {
url: { type: 'string', description: 'ダウンロードする URL' },
filename: { type: 'string', description: '保存するファイル名(省略時は URL から推定)' },
section: { type: 'string', description: '保存先: "input" または "output" (デフォルト: "input")' },
section: { type: 'string', description: '保存先: "input" / "output" / "source"(調査資料の蓄積用)(デフォルト: "input")' },
},
required: ['url'],
},
@@ -810,8 +820,8 @@ async function executeWebFetch(
ctx: ToolContext,
): Promise<ToolResult> {
const rawUrl = input['url'] as string;
// selector パラメータは受け取るが、cheerio なしでは CSS セレクト不可のため無視
// (仕様上 selector は省略可能で、対応できない旨をログに残す)
// selector 指定時は、linkedom + querySelector でそのサブツリーだけをクリーン抽出する。
// 要素が見つからなければ全ページの readability 抽出にフォールバックする。
const selector = input['selector'] as string | undefined;
const timeoutSec = ctx.toolsConfig?.webfetchTimeout ?? 30;
@@ -854,7 +864,7 @@ async function executeWebFetch(
}
if (selector) {
logger.debug(`[WebFetch] selector "${selector}" is specified but CSS selection is not supported without cheerio — fetching full page`);
logger.debug(`[WebFetch] selector "${selector}" specified — will clean-extract that subtree (falls back to full page if not found)`);
}
logger.debug(`[WebFetch] fetching ${rawUrl}`);
@@ -932,9 +942,59 @@ async function executeWebFetch(
isError: true,
};
}
const text =
htmlToText(sniffed.text) +
(sniffed.truncated ? '\n\n[truncated: body exceeded 5MB]' : '');
// Readability ベースのクリーン抽出を試みる。記事本文だけを構造保持で
// Markdown 化する。失敗(非記事・SPA・空)したら従来の素朴な htmlToText に
// フォールバックして WebFetch を絶対に hard-fail させない。
const bodyTruncatedNote = sniffed.truncated ? '\n\n[truncated: body exceeded 5MB]' : '';
let extracted = extractReadableMarkdown(sniffed.text, {
baseUrl: rawUrl,
...(selector ? { selector } : {}),
});
// selector が見つからなかった場合は、ページ全体の readability 抽出を試す。
if (!extracted && selector) {
extracted = extractReadableMarkdown(sniffed.text, { baseUrl: rawUrl });
}
let fullClean: string;
let extractionTitle: string | undefined;
if (extracted) {
fullClean = extracted.markdown;
extractionTitle = extracted.title;
} else {
// フォールバック: 旧 htmlToText(内部で 10k 切り詰め済み)
logger.debug(`[WebFetch] readability extraction empty, falling back to naive htmlToText for ${rawUrl}`);
fullClean = htmlToText(sniffed.text);
}
fullClean = (fullClean + bodyTruncatedNote).trim();
// source 保存用は全文だが、病的に巨大なページに備えて上限でガードする。
// 5MB body cap が入力を既に制限しているが、Markdown 化後にも保険をかける。
let sourceBody = fullClean;
if (Buffer.byteLength(sourceBody, 'utf-8') > MAX_SOURCE_MARKDOWN_BYTES) {
const sliced = sourceBody.slice(0, MAX_SOURCE_MARKDOWN_BYTES);
sourceBody = sliced + '\n\n... (truncated: extracted markdown exceeded source size cap)';
}
// スペース文脈 (ctx.spaceId != null) なら全文クリーン Markdown を source/ に蓄積する。
// ephemeral / 非スペースタスク(spaceId undefined)は従来どおり保存しない。
// 既存の logs/raw への保存挙動は別経路(raw-save.ts)なので変更しない。
let savedSourceFile: string | null = null;
if (ctx.spaceId != null && sourceBody.trim().length > 0) {
try {
savedSourceFile = saveSourceFile(ctx.workspacePath, {
url: rawUrl,
...(extractionTitle ? { title: extractionTitle } : {}),
body: sourceBody,
tool: 'WebFetch',
ext: '.md',
});
} catch (e) {
logger.warn(`[WebFetch] failed to persist source: ${(e as Error).message}`);
}
}
// agent のコンテキストに返すのは切り詰めた版。全文は source/ に保存済み。
const text = truncateForAgent(fullClean, MAX_AGENT_RETURN_CHARS, savedSourceFile);
// vlmEnabled 時はファーストビューのスクショを並行取得して画像を添付する。
// 失敗時は警告ログのみで WebFetch 自体は成功扱いとする。
@@ -982,7 +1042,7 @@ async function executeDownloadFile(
): Promise<ToolResult> {
const rawUrl = input['url'] as string;
const rawSection = typeof input['section'] === 'string' ? input['section'] : 'input';
const section = rawSection === 'output' ? 'output' : 'input';
const section = rawSection === 'output' || rawSection === 'source' ? rawSection : 'input';
const timeoutSec = ctx.toolsConfig?.webfetchTimeout ?? 60;
const allowedHosts = ctx.toolsConfig?.webfetchAllowedHosts ?? [];
@@ -1062,6 +1122,18 @@ async function executeDownloadFile(
bytes: buf.length, outcome: 'success',
});
// section='source' のとき出典メタを source/index.jsonl に追記する。
// (ファイルはすでに source/ 配下に保存済みなので、ここでは index 行だけ追記)
if (section === 'source') {
appendSourceIndex(ctx.workspacePath, {
file: filename,
url: rawUrl,
fetched_at: new Date().toISOString(),
tool: 'DownloadFile',
bytes: buf.length,
});
}
return {
output: `Downloaded ${buf.length} bytes from ${rawUrl} -> ${section}/${filename} (content-type: ${contentType || 'unknown'})`,
isError: false,