191 lines
7.0 KiB
TypeScript
191 lines
7.0 KiB
TypeScript
// docs.ts — ツール詳細ドキュメント参照用ツール
|
||
//
|
||
// リポジトリ内 docs/tools/{name}.md を読み込んで返す。
|
||
// ワークスペース外の固定パスから読むため、Read ツールでは到達できない。
|
||
// Tool description にこのツールへのポインタを書いておくと、
|
||
// 詳細な使い方を必要に応じてエージェントが取得できる。
|
||
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import { ToolDef } from '../../llm/openai-compat.js';
|
||
import type { ToolContext, ToolResult } from './core.js';
|
||
import { logger } from '../../logger.js';
|
||
|
||
type McpToolLookup = (serverId: string, toolName: string) => { description: string | null; input_schema: string | null } | null;
|
||
let _mcpLookup: McpToolLookup | null = null;
|
||
export function setMcpToolLookup(fn: McpToolLookup | null): void {
|
||
_mcpLookup = fn;
|
||
}
|
||
|
||
// dist/engine/tools/docs.js または src/engine/tools/docs.ts から
|
||
// リポジトリルートを解決し、docs/tools/ を指す
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||
const DOCS_DIR = path.join(REPO_ROOT, 'docs', 'tools');
|
||
|
||
// 関連ツールが同じ doc を参照できるようエイリアスを定義
|
||
// キー・値ともに小文字
|
||
const TOOL_DOC_ALIASES: Record<string, string> = {
|
||
// updateuseragents.md にまとめる
|
||
readuseragents: 'updateuseragents',
|
||
// checklist.md にまとめる
|
||
createchecklist: 'checklist',
|
||
checkitem: 'checklist',
|
||
getchecklist: 'checklist',
|
||
// searchknowledge.md にまとめる
|
||
listnamespaces: 'searchknowledge',
|
||
listdocuments: 'searchknowledge',
|
||
ingestdocument: 'searchknowledge',
|
||
ingeststatus: 'searchknowledge',
|
||
// x.ts ツールをまとめる
|
||
xuserposts: 'xsearch',
|
||
xpostdetail: 'xsearch',
|
||
xfetchcardmedia: 'xsearch',
|
||
xtimeline: 'xsearch',
|
||
// youtube.ts をまとめる
|
||
searchyoutube: 'getyoutubetranscript',
|
||
// maps.ts をまとめる
|
||
getdirections: 'searchplaces',
|
||
reversegeocode: 'searchplaces',
|
||
// office.ts をまとめる
|
||
readpdf: 'office',
|
||
readexcel: 'office',
|
||
readdocx: 'office',
|
||
readpptx: 'office',
|
||
readmsg: 'office',
|
||
pdftoimages: 'office',
|
||
splitexcelsheets: 'office',
|
||
splitdocxsections: 'office',
|
||
// pieces.ts をまとめる
|
||
getpiece: 'listpieces',
|
||
createpiece: 'listpieces',
|
||
updatepiece: 'listpieces',
|
||
// ms-learn.ts をまとめる
|
||
fetchmicrosoftlearn: 'searchmicrosoftlearn',
|
||
searchmicrosoftlearncache: 'searchmicrosoftlearn',
|
||
refreshmicrosoftlearncache: 'searchmicrosoftlearn',
|
||
// browser.ts: InteractiveBrowse / BrowseWithSession は browseweb.md にまとめる
|
||
interactivebrowse: 'browseweb',
|
||
browsewithsession: 'browseweb',
|
||
// ssh.ts をまとめる
|
||
sshexec: 'ssh-tools',
|
||
sshupload: 'ssh-tools',
|
||
sshdownload: 'ssh-tools',
|
||
sshlistconnections: 'ssh-tools',
|
||
// ssh-console.ts をまとめる
|
||
sshconsoleensure: 'ssh-console-tools',
|
||
sshconsolesend: 'ssh-console-tools',
|
||
sshconsolesnapshot: 'ssh-console-tools',
|
||
sshconsolerun: 'ssh-console-tools',
|
||
// slide.ts をまとめる
|
||
settheme: 'slide',
|
||
addslide: 'slide',
|
||
buildpptx: 'slide',
|
||
resetslides: 'slide',
|
||
// notes.ts をまとめる
|
||
searchnotes: 'notes',
|
||
readnote: 'notes',
|
||
writenote: 'notes',
|
||
};
|
||
|
||
const READ_TOOL_DOC_DEF: ToolDef = {
|
||
type: 'function',
|
||
function: {
|
||
name: 'ReadToolDoc',
|
||
description:
|
||
'ツールの詳細な使い方ドキュメントを読み込む。各ツールの description は概要のみで、詳細な手順や例が必要なときはこれを呼ぶ。'
|
||
+ 'docs/tools/{name}.md(リポジトリ内固定パス)を参照する。',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
name: {
|
||
type: 'string',
|
||
description: '読みたいツール名(例: "BrowseWeb", "SearchKnowledge")',
|
||
},
|
||
},
|
||
required: ['name'],
|
||
},
|
||
},
|
||
};
|
||
|
||
export const TOOL_DEFS: Record<string, ToolDef> = {
|
||
ReadToolDoc: READ_TOOL_DOC_DEF,
|
||
};
|
||
|
||
export async function executeTool(
|
||
name: string,
|
||
input: Record<string, unknown>,
|
||
_ctx: ToolContext,
|
||
): Promise<ToolResult | null> {
|
||
if (name !== 'ReadToolDoc') return null;
|
||
|
||
const toolName = input['name'] as string | undefined;
|
||
if (!toolName || typeof toolName !== 'string') {
|
||
return { output: 'ReadToolDoc error: name パラメータが必要です', isError: true };
|
||
}
|
||
|
||
// MCP ツール名 (mcp__<serverId>__<toolName>) の場合はキャッシュから返す
|
||
if (toolName.startsWith('mcp__')) {
|
||
// Lazy-import to avoid a hard dependency on mcp module at module-load time.
|
||
const { parseToolName } = await import('../../mcp/tool-adapter.js');
|
||
const parsed = parseToolName(toolName);
|
||
if (!parsed) {
|
||
return { output: `ReadToolDoc: 不正な MCP ツール名 "${toolName}"`, isError: true };
|
||
}
|
||
if (!_mcpLookup) {
|
||
return { output: 'ReadToolDoc: MCP サブシステムが初期化されていません', isError: true };
|
||
}
|
||
const row = _mcpLookup(parsed.serverId, parsed.toolName);
|
||
if (!row) {
|
||
return { output: `ReadToolDoc: ${toolName} のキャッシュ情報がありません`, isError: false };
|
||
}
|
||
let schemaBlock = '';
|
||
if (row.input_schema) {
|
||
try {
|
||
const schema = JSON.parse(row.input_schema);
|
||
schemaBlock = `\n\n## Input schema\n\n\`\`\`json\n${JSON.stringify(schema, null, 2)}\n\`\`\``;
|
||
} catch {
|
||
schemaBlock = `\n\n## Input schema (raw)\n\n\`\`\`\n${row.input_schema}\n\`\`\``;
|
||
}
|
||
}
|
||
return {
|
||
output: `# ${toolName}\n\n${row.description ?? '(no description)'}${schemaBlock}`,
|
||
isError: false,
|
||
};
|
||
}
|
||
|
||
// パストラバーサル防止: 英数字とハイフン・アンダースコアのみ許可
|
||
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(toolName)) {
|
||
return { output: `ReadToolDoc error: 不正なツール名 "${toolName}"`, isError: true };
|
||
}
|
||
|
||
const lowerName = toolName.toLowerCase();
|
||
const resolvedName = TOOL_DOC_ALIASES[lowerName] ?? lowerName;
|
||
const docPath = path.join(DOCS_DIR, `${resolvedName}.md`);
|
||
try {
|
||
const content = await fs.promises.readFile(docPath, 'utf-8');
|
||
return { output: content, isError: false };
|
||
} catch (e: any) {
|
||
if (e.code === 'ENOENT') {
|
||
// 利用可能なドキュメント一覧を返す
|
||
try {
|
||
const files = await fs.promises.readdir(DOCS_DIR);
|
||
const available = files
|
||
.filter((f) => f.endsWith('.md'))
|
||
.map((f) => f.replace(/\.md$/, ''))
|
||
.sort();
|
||
return {
|
||
output: `ReadToolDoc: "${toolName}" のドキュメントは存在しません。\n利用可能なドキュメント:\n${available.map((n) => `- ${n}`).join('\n')}`,
|
||
isError: true,
|
||
};
|
||
} catch {
|
||
return { output: `ReadToolDoc: "${toolName}" のドキュメントは存在しません。`, isError: true };
|
||
}
|
||
}
|
||
logger.warn(`[ReadToolDoc] failed to read ${docPath}: ${e.message}`);
|
||
return { output: `ReadToolDoc error: ${e.message}`, isError: true };
|
||
}
|
||
}
|