




















@@ -1,119 +1,21 @@
1-import { hasBinary } from "../agents/skills.js";
21import { formatCliCommand } from "../cli/command-format.js";
3-import { runCommandWithTimeout } from "../process/exec.js";
42import type { RuntimeEnv } from "../runtime.js";
5-import { normalizeStringEntries } from "../shared/string-normalization.js";
63import { formatDocsLink } from "../terminal/links.js";
74import { isRich, theme } from "../terminal/theme.js";
859-const SEARCH_TOOL = "https://docs.openclaw.ai/mcp.search_open_claw";
6+const SEARCH_API = "https://docs.openclaw.ai/api/search";
107const SEARCH_TIMEOUT_MS = 30_000;
11-const DEFAULT_SNIPPET_MAX = 220;
12-const MCP_ERROR_PATTERN = /MCP error\s+-?\d+/i;
138149type DocResult = {
1510title: string;
1611link: string;
1712snippet?: string;
1813};
191420-type NodeRunner = {
21-cmd: string;
22-args: string[];
15+type DocsSearchResponse = {
16+results?: unknown;
2317};
241825-type ToolRunOptions = {
26-input?: string;
27-timeoutMs?: number;
28-};
29-30-function resolveNodeRunner(): NodeRunner {
31-if (hasBinary("pnpm")) {
32-return { cmd: "pnpm", args: ["dlx"] };
33-}
34-if (hasBinary("npx")) {
35-return { cmd: "npx", args: ["-y"] };
36-}
37-throw new Error(
38-`Docs search needs pnpm or npx to run the docs search helper. Install pnpm, or run ${formatCliCommand("npm install -g pnpm")}.`,
39-);
40-}
41-42-async function runNodeTool(tool: string, toolArgs: string[], options: ToolRunOptions = {}) {
43-const runner = resolveNodeRunner();
44-const argv = [runner.cmd, ...runner.args, tool, ...toolArgs];
45-return await runCommandWithTimeout(argv, {
46-timeoutMs: options.timeoutMs ?? SEARCH_TIMEOUT_MS,
47-input: options.input,
48-});
49-}
50-51-async function runTool(tool: string, toolArgs: string[], options: ToolRunOptions = {}) {
52-if (hasBinary(tool)) {
53-return await runCommandWithTimeout([tool, ...toolArgs], {
54-timeoutMs: options.timeoutMs ?? SEARCH_TIMEOUT_MS,
55-input: options.input,
56-});
57-}
58-return await runNodeTool(tool, toolArgs, options);
59-}
60-61-function extractLine(lines: string[], prefix: string): string | undefined {
62-const line = lines.find((value) => value.startsWith(prefix));
63-if (!line) {
64-return undefined;
65-}
66-return line.slice(prefix.length).trim();
67-}
68-69-function normalizeSnippet(raw: string | undefined, fallback: string): string {
70-const base = raw && raw.trim().length > 0 ? raw : fallback;
71-const cleaned = base.replace(/\s+/g, " ").trim();
72-if (!cleaned) {
73-return "";
74-}
75-if (cleaned.length <= DEFAULT_SNIPPET_MAX) {
76-return cleaned;
77-}
78-return `${cleaned.slice(0, DEFAULT_SNIPPET_MAX - 3)}...`;
79-}
80-81-function firstParagraph(text: string): string {
82-return (
83-text
84-.split(/\n\s*\n/)
85-.map((chunk) => chunk.trim())
86-.find(Boolean) ?? ""
87-);
88-}
89-90-function parseSearchOutput(raw: string): DocResult[] {
91-const normalized = raw.replace(/\r/g, "");
92-const blocks = normalizeStringEntries(normalized.split(/\n(?=Title: )/g));
93-94-const results: DocResult[] = [];
95-for (const block of blocks) {
96-const lines = block.split("\n");
97-const title = extractLine(lines, "Title:");
98-const link = extractLine(lines, "Link:");
99-if (!title || !link) {
100-continue;
101-}
102-const content = extractLine(lines, "Content:");
103-const contentIndex = lines.findIndex((line) => line.startsWith("Content:"));
104-const body =
105-contentIndex >= 0
106- ? lines
107-.slice(contentIndex + 1)
108-.join("\n")
109-.trim()
110- : "";
111-const snippet = normalizeSnippet(content, firstParagraph(body));
112-results.push({ title, link, snippet: snippet || undefined });
113-}
114-return results;
115-}
116-11719function escapeMarkdown(text: string): string {
11820return text.replace(/[()[\]]/g, "\\$&");
11921}
@@ -159,6 +61,49 @@ async function renderMarkdown(markdown: string, runtime: RuntimeEnv) {
15961runtime.log(markdown.trimEnd());
16062}
1616364+async function fetchDocsSearch(query: string): Promise<DocResult[]> {
65+const url = new URL(SEARCH_API);
66+url.searchParams.set("q", query);
67+const controller = new AbortController();
68+const timeout = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
69+try {
70+const response = await fetch(url, {
71+headers: { Accept: "application/json" },
72+signal: controller.signal,
73+});
74+if (!response.ok) {
75+throw new Error(`HTTP ${response.status}`);
76+}
77+const payload = (await response.json()) as DocsSearchResponse;
78+return parseDocsSearchResults(payload.results);
79+} finally {
80+clearTimeout(timeout);
81+}
82+}
83+84+function parseDocsSearchResults(raw: unknown): DocResult[] {
85+if (!Array.isArray(raw)) {
86+return [];
87+}
88+const results: DocResult[] = [];
89+for (const item of raw) {
90+if (!item || typeof item !== "object") {
91+continue;
92+}
93+const entry = item as Record<string, unknown>;
94+if (typeof entry.title !== "string" || typeof entry.link !== "string") {
95+continue;
96+}
97+results.push({
98+title: entry.title,
99+link: entry.link,
100+snippet:
101+typeof entry.snippet === "string" && entry.snippet.trim() ? entry.snippet : undefined,
102+});
103+}
104+return results;
105+}
106+162107export async function docsSearchCommand(queryParts: string[], runtime: RuntimeEnv) {
163108const query = queryParts.join(" ").trim();
164109if (!query) {
@@ -173,31 +118,16 @@ export async function docsSearchCommand(queryParts: string[], runtime: RuntimeEn
173118return;
174119}
175120176-const payload = JSON.stringify({ query });
177-const res = await runTool(
178-"mcporter",
179-["call", SEARCH_TOOL, "--args", payload, "--output", "text"],
180-{ timeoutMs: SEARCH_TIMEOUT_MS },
181-);
182-183-if (res.code !== 0) {
184-const err = res.stderr.trim() || res.stdout.trim() || `exit ${res.code}`;
185-runtime.error(`Docs search failed: ${err}`);
186-runtime.exit(1);
187-return;
188-}
189-190-const combined = `${res.stdout}\n${res.stderr}`;
191-if (MCP_ERROR_PATTERN.test(combined)) {
192-const err = (res.stderr.trim() || res.stdout.trim())
193-.split("\n")
194-.find((line) => MCP_ERROR_PATTERN.test(line));
195-runtime.error(`Docs search failed: ${err ?? "MCP error reported by docs search tool"}`);
121+let results: DocResult[];
122+try {
123+results = await fetchDocsSearch(query);
124+} catch (error) {
125+const message = error instanceof Error ? error.message : String(error);
126+runtime.error(`Docs search failed: ${message}`);
196127runtime.exit(1);
197128return;
198129}
199130200-const results = parseSearchOutput(res.stdout);
201131if (isRich()) {
202132renderRichResults(query, results, runtime);
203133return;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。