Files
deerflow-code/frontend-web/src/core/skill-display/runtime.ts
T
吉鑫升andClaude Opus 4.7 4410acaee8 技能展示配置持久化并适配引用来源,新增技能文件预览组件
- 后端将技能展示适配配置存入数据库,引用批次按持久化映射进行适配
- 前端新增技能文件预览弹窗与文件树组件,完善技能来源展示
- 修正 .gitignore 中宽泛的 skills/ 规则误忽略前端组件目录的问题

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:16:56 +08:00

1042 lines
29 KiB
TypeScript

import type { Message } from "@langchain/langgraph-sdk";
import { normalizeSearchResults } from "@/core/messages/search-results";
import type { Skill, SkillDisplayConfig } from "@/core/skills/type";
import {
normalizeDisplaySources,
type DisplaySourceItem,
} from "./display-adapter";
export type RuntimeDisplaySource = DisplaySourceItem & {
index: number;
skillName: string;
mode: SkillDisplayConfig["mode"];
tableColumns?: SkillDisplayConfig["tableColumns"];
};
type RuntimeDisplaySourceDraft = Omit<RuntimeDisplaySource, "index">;
export type SkillDisplayMap = Map<string, SkillDisplayConfig>;
type RuntimeToolCall = {
id?: string;
name: string;
args?: Record<string, unknown>;
};
export function buildSkillDisplayMap(skills: Skill[]): SkillDisplayMap {
const map = new Map<string, SkillDisplayConfig>();
for (const skill of skills) {
const display = skill.display;
if (!display?.enabled || display.mode === "none") {
continue;
}
map.set(skill.name, display);
}
return map;
}
export function findDisplayConfigForTool(
toolName: string,
displays: SkillDisplayMap,
toolArgs?: Record<string, unknown>,
): { skillName: string; display: SkillDisplayConfig } | null {
const exact = displays.get(toolName);
if (exact) {
return { skillName: toolName, display: exact };
}
const normalizedTool = normalizeName(toolName);
for (const [skillName, display] of displays) {
const normalizedSkill = normalizeName(skillName);
if (
normalizedTool === normalizedSkill ||
normalizedTool.endsWith(normalizedSkill) ||
normalizedTool.includes(normalizedSkill)
) {
return { skillName, display };
}
}
// bash/python skill invocations don't carry the skill name in the tool name —
// the path lives inside the command/args (e.g. `python3 skills/custom/demo/scripts/run.py`).
// Scan stringy args for the `skills/{public,custom}/<name>/` shape so display
// configs still resolve. Mirrors the backend `_SKILL_PATH_RE` in
// tool_metrics_middleware.py.
if (toolArgs && canInferSkillFromToolArgs(toolName)) {
const skillFromArgs = extractSkillNameFromArgs(toolArgs);
if (skillFromArgs) {
const direct = displays.get(skillFromArgs);
if (direct) {
return { skillName: skillFromArgs, display: direct };
}
}
}
return null;
}
const SKILL_PATH_RE = /[\\/]skills[\\/](?:public|custom)[\\/]([^\\/]+)(?:[\\/]|$)/;
const TOOL_ARG_SKILL_MATCH_EXCLUDES = new Set([
"read_file",
"readfile",
"ls",
"write_file",
"writefile",
"str_replace",
"strreplace",
"web_fetch",
"webfetch",
]);
function canInferSkillFromToolArgs(toolName: string): boolean {
const normalized = normalizeName(toolName);
if (
TOOL_ARG_SKILL_MATCH_EXCLUDES.has(toolName) ||
TOOL_ARG_SKILL_MATCH_EXCLUDES.has(normalized)
) {
return false;
}
return (
normalized.includes("bash") ||
normalized.includes("shell") ||
normalized.includes("python") ||
normalized.includes("execute") ||
normalized.includes("terminal")
);
}
function extractSkillNameFromArgs(args: Record<string, unknown>): string | undefined {
for (const value of Object.values(args)) {
if (typeof value !== "string") continue;
const match = SKILL_PATH_RE.exec(value);
if (match) {
return match[1];
}
}
return undefined;
}
export function collectRuntimeDisplaySources(
messages: Message[],
displays: SkillDisplayMap,
options: {
afterMessageId?: string;
beforeMessageId?: string;
includeBeforeMessage?: boolean;
includeModes?: SkillDisplayConfig["mode"][];
} = {},
): RuntimeDisplaySource[] {
const start =
options.afterMessageId === undefined
? 0
: Math.max(
messages.findIndex((message) => message.id === options.afterMessageId) + 1,
0,
);
const limit =
options.beforeMessageId === undefined
? messages.length
: Math.max(
messages.findIndex((message) => message.id === options.beforeMessageId) +
(options.includeBeforeMessage ? 1 : 0),
0,
);
const allowedModes = options.includeModes
? new Set(options.includeModes)
: null;
const sources: RuntimeDisplaySource[] = [];
let citationIndex = 0;
const seenCitationKeys = new Map<string, RuntimeDisplaySource>();
const consumedToolMessageIds = new Set<string>();
const consumedToolMessages = new Set<Message>();
for (let index = start; index < limit; index++) {
const message = messages[index];
if (!message || message.type !== "ai") {
continue;
}
for (const toolCall of extractRuntimeToolCalls(message)) {
const toolMatch = findDisplayConfigForTool(
toolCall.name,
displays,
toolCall.args,
);
const toolMessage = findToolResultMessage(
toolCall,
messages,
index,
consumedToolMessageIds,
consumedToolMessages,
);
if (!toolMessage) {
continue;
}
const result = parseToolMessagePayload(toolMessage);
if (result === undefined) {
continue;
}
if (toolMessage.id) {
consumedToolMessageIds.add(toolMessage.id);
}
consumedToolMessages.add(toolMessage);
if (isDisplaySamplePayload(result)) {
continue;
}
const match = toolMatch;
const effectiveMode = match?.display.mode ?? "citation";
if (allowedModes && !allowedModes.has(effectiveMode)) {
continue;
}
const normalized = match
? normalizeDisplaySources(result, match.display)
: shouldUseDefaultCitationMode(
toolCall.name,
result,
toolCall.args,
)
? normalizeDefaultDisplaySources(result)
: [];
appendDisplaySources(
sources,
normalized.map((item) => ({
...item,
skillName: match?.skillName ?? toolCall.name,
mode: effectiveMode,
tableColumns: match?.display.tableColumns,
})),
seenCitationKeys,
() => ++citationIndex,
);
}
}
for (let index = start; index < limit; index++) {
const message = messages[index];
if (
!message ||
message.type !== "tool" ||
consumedToolMessages.has(message) ||
consumedToolMessageIds.has(message.id ?? "")
) {
continue;
}
const result = parseToolMessagePayload(message);
if (result === undefined || isDisplaySamplePayload(result)) {
continue;
}
const toolName = extractToolMessageName(message);
const match = findDisplayConfigForPayload(toolName, result, displays);
const effectiveMode = match?.display.mode ?? "citation";
if (allowedModes && !allowedModes.has(effectiveMode)) {
continue;
}
const normalized = match
? normalizeDisplaySources(result, match.display)
: shouldUseDefaultCitationMode(toolName, result) ||
normalizeDefaultDisplaySources(result).length > 0
? normalizeDefaultDisplaySources(result)
: [];
appendDisplaySources(
sources,
normalized.map((item) => ({
...item,
skillName: match?.skillName ?? toolName,
mode: effectiveMode,
tableColumns: match?.display.tableColumns,
})),
seenCitationKeys,
() => ++citationIndex,
);
}
return sources;
}
function appendDisplaySources(
target: RuntimeDisplaySource[],
drafts: RuntimeDisplaySourceDraft[],
seenCitationKeys: Map<string, RuntimeDisplaySource>,
nextCitationIndex: () => number,
) {
for (const draft of drafts) {
if (draft.mode === "citation") {
const source = {
...draft,
index: nextCitationIndex(),
};
const key = citationSourceKey(draft);
if (key) {
seenCitationKeys.set(`${key}:${source.index}`, source);
}
target.push(source);
continue;
}
target.push({
...draft,
index: nextCitationIndex(),
});
}
}
export function reindexRuntimeDisplaySources(
sources: RuntimeDisplaySource[],
globalSources: RuntimeDisplaySource[],
): RuntimeDisplaySource[] {
if (sources.length === 0 || globalSources.length === 0) {
return sources;
}
const globalByKey = new Map(
globalSources.map((source) => [citationSourceKey(source), source]),
);
return sources.map((source) => {
const global = globalByKey.get(citationSourceKey(source));
return global
? {
...source,
index: global.index,
}
: source;
});
}
function citationSourceKey(source: RuntimeDisplaySourceDraft): string {
const raw = source.raw ?? {};
const id =
source.id ||
source.url ||
stringifyMaybe(raw.recUuid) ||
stringifyMaybe(raw.uuid) ||
stringifyMaybe(raw.docId) ||
stringifyMaybe(raw.id);
if (id) {
return normalizeCitationKey(String(id));
}
const fallback = [
source.skillName,
source.title,
source.source,
source.time,
source.content,
source.snippet,
]
.filter(Boolean)
.join("|");
return normalizeCitationKey(fallback) || normalizeCitationKey(JSON.stringify(raw).slice(0, 300));
}
function extractRuntimeToolCalls(message: Message): RuntimeToolCall[] {
const calls: RuntimeToolCall[] = [];
const direct = (message as { tool_calls?: unknown }).tool_calls;
if (Array.isArray(direct)) {
for (const raw of direct) {
const call = normalizeRuntimeToolCall(raw);
if (call) {
calls.push(call);
}
}
}
const kwargs = (message as { additional_kwargs?: Record<string, unknown> })
.additional_kwargs;
const extra = kwargs?.tool_calls;
if (Array.isArray(extra)) {
for (const raw of extra) {
const call = normalizeRuntimeToolCall(raw);
if (call && !calls.some((item) => item.id && item.id === call.id)) {
calls.push(call);
}
}
}
const chunks = (message as { tool_call_chunks?: unknown }).tool_call_chunks;
if (Array.isArray(chunks)) {
for (const raw of chunks) {
const call = normalizeRuntimeToolCall(raw);
if (call && !calls.some((item) => item.id && item.id === call.id)) {
calls.push(call);
}
}
}
return calls;
}
function normalizeRuntimeToolCall(raw: unknown): RuntimeToolCall | null {
if (!isRecord(raw)) {
return null;
}
const fn = isRecord(raw.function) ? raw.function : null;
const name =
stringifyMaybe(raw.name) ??
stringifyMaybe(fn?.name) ??
stringifyMaybe(raw.tool_name);
if (!name) {
return null;
}
return {
id:
stringifyMaybe(raw.id) ??
stringifyMaybe(raw.tool_call_id) ??
stringifyMaybe(raw.call_id),
name,
args: normalizeToolArgs(raw.args ?? raw.arguments ?? fn?.arguments),
};
}
function normalizeToolArgs(value: unknown): Record<string, unknown> | undefined {
if (isRecord(value)) {
return value;
}
if (typeof value === "string") {
try {
const parsed = JSON.parse(value);
return isRecord(parsed) ? parsed : undefined;
} catch {
return { command: value };
}
}
return undefined;
}
export function findDisplayConfigForPayload(
toolName: string,
_payload: unknown,
displays: SkillDisplayMap,
): { skillName: string; display: SkillDisplayConfig } | null {
return findDisplayConfigForTool(toolName, displays);
}
export function isUnconfiguredSkillInvocation(
toolName: string,
displays: SkillDisplayMap | undefined,
toolArgs?: Record<string, unknown>,
): boolean {
if (!toolArgs || !canInferSkillFromToolArgs(toolName)) {
return false;
}
const skillName = extractSkillNameFromArgs(toolArgs);
return !!skillName && !displays?.has(skillName);
}
export function isDisplaySamplePayload(payload: unknown): boolean {
if (!isRecord(payload)) {
return false;
}
if (payload.__displaySample === true || payload.displaySample === true) {
return true;
}
if (payload.query === "sample query") {
return true;
}
const items =
getArrayByPath(payload, "results") ??
getArrayByPath(payload, "data") ??
getArrayByPath(payload, "items");
const first = items?.find((item): item is Record<string, unknown> => isRecord(item));
if (!first) {
return false;
}
const text = Object.values(first)
.filter((value) => typeof value === "string")
.join("\n")
.toLowerCase();
return (
/^sample-/i.test(String(first.id ?? "")) ||
text.includes("mock result") ||
text.includes("knowledge result one")
);
}
function getArrayByPath(payload: unknown, path: string): unknown[] | null {
const value = path.split(".").reduce<unknown>((acc, key) => {
if (acc && typeof acc === "object") {
return (acc as Record<string, unknown>)[key];
}
return undefined;
}, payload);
return Array.isArray(value) ? value : null;
}
export function shouldUseDefaultCitationMode(
toolName: string,
payload: unknown,
toolArgs?: Record<string, unknown>,
): boolean {
const normalizedName = normalizeName(toolName);
const isSearchLikeTool =
normalizedName.includes("search") ||
normalizedName.includes("retriev") ||
normalizedName.includes("knowledge") ||
normalizedName.includes("query");
const isSkillInvocation = !!(
toolArgs &&
canInferSkillFromToolArgs(toolName) &&
extractSkillNameFromArgs(toolArgs)
);
if (!isSearchLikeTool && !isSkillInvocation) {
return false;
}
return normalizeDefaultDisplaySources(payload).length > 0;
}
export function normalizeDefaultDisplaySources(payload: unknown): DisplaySourceItem[] {
const normalized = normalizeSearchResults(
typeof payload === "string" || isRecord(payload)
? (payload as string | Record<string, unknown>)
: undefined,
);
return normalized.map((item, index) => ({
id:
item.recUuid ??
item.uuid ??
item.docId ??
item.id ??
item.url,
title: item.title ?? item.source ?? `结果 ${index + 1}`,
type: "result",
snippet: item.snippet ?? item.content ?? "",
content: item.content ?? item.snippet,
source: item.source,
time: item.published_date ?? item.publishedDate ?? item.date,
url: item.url,
raw: item as unknown as Record<string, unknown>,
}));
}
export function applyInlineCitations(
content: string,
sources: RuntimeDisplaySource[],
): string {
if (!content || sources.length === 0) {
return content;
}
const referenceSources = sources.filter((source) => source.mode !== "none");
if (referenceSources.length === 0) {
return content;
}
const byIndex = new Map(referenceSources.map((source) => [source.index, source]));
const byUrl = new Map<string, RuntimeDisplaySource>();
const byTitle = new Map<string, RuntimeDisplaySource>();
for (const source of referenceSources) {
if (source.url) {
byUrl.set(source.url, source);
}
if (source.title) {
byTitle.set(source.title.trim(), source);
}
}
const normalized = normalizeExistingCitationLinks(
content,
byIndex,
byUrl,
byTitle,
referenceSources,
);
const markdownLinkPattern = /!?\[(?:[^\[\]]|\[[^\]]*\])*\]\([^)]+\)/g;
let cursor = 0;
let next = "";
let replaced = false;
for (const match of normalized.matchAll(markdownLinkPattern)) {
const start = match.index ?? 0;
const replacedSlice = replaceCitationTokens(
normalized.slice(cursor, start),
byIndex,
referenceSources,
);
if (replacedSlice !== normalized.slice(cursor, start)) {
replaced = true;
}
next += replacedSlice;
next += match[0];
cursor = start + match[0].length;
}
const tail = replaceCitationTokens(normalized.slice(cursor), byIndex, referenceSources);
if (tail !== normalized.slice(cursor)) {
replaced = true;
}
next += tail;
if (replaced || hasCitationMarkdown(next)) {
return next;
}
return appendCitationSummary(next, referenceSources);
}
function replaceCitationTokens(
content: string,
byIndex: Map<number, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
const normalized = normalizeLooseCitationTokens(content, byIndex, orderedSources);
const withDoubleBracketCitations = normalized
.replace(/\[\[(\d+)\]\]/g, (match, rawIndex: string) =>
citationMarkdown(match, Number(rawIndex), byIndex, orderedSources),
);
return replaceBracketCitationTokens(
withDoubleBracketCitations,
byIndex,
orderedSources,
);
}
function replaceBracketCitationTokens(
content: string,
byIndex: Map<number, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
const tokenPattern = /\\?\[(\d{1,3})\\?\](?!\()/g;
let cursor = 0;
let next = "";
for (const match of content.matchAll(tokenPattern)) {
const start = match.index ?? 0;
const previous = start > 0 ? content[start - 1] : "";
if (previous === ":" || previous === "[" || previous === "!") {
continue;
}
next += content.slice(cursor, start);
next += citationMarkdown(match[0], Number(match[1]), byIndex, orderedSources);
cursor = start + match[0].length;
}
next += content.slice(cursor);
return next;
}
function normalizeLooseCitationTokens(
content: string,
byIndex: Map<number, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
const isValidIndex = (index: number) =>
Number.isInteger(index) &&
index > 0 &&
(byIndex.has(index) || !!orderedSources[index - 1]);
const toCitationRun = (raw: string) => {
const parts = raw.match(/\\?\[\d{1,3}\\?\]|\d{1,3}/g) ?? [];
const indexes = parts.map((part) => Number(part.replace(/\D/g, "")));
if (indexes.length === 0 || indexes.some((index) => !isValidIndex(index))) {
return raw;
}
return indexes.map((index) => `[${index}]`).join("");
};
let next = content.replace(
/(^|[^\d\[\]\\])((?:\d{1,3}|\\?\[\d{1,3}\\?\])(?:\\?\[\d{1,3}\\?\]|\d{1,3})+)/g,
(match, prefix: string, rawRun: string) => {
if (!rawRun.includes("[")) {
return match;
}
return `${prefix}${toCitationRun(rawRun)}`;
},
);
next = next.replace(
/(^|[\s\uFF0C\u3002\uFF1B\uFF1A\u3001,!?!?)])(\d{1,3})(?=([\s\uFF0C\u3002\uFF1B\uFF1A\u3001,!?!?)]|$))/g,
(match, prefix: string, rawIndex: string) => {
const index = Number(rawIndex);
if (!isValidIndex(index)) {
return match;
}
return `${prefix}[${index}]`;
},
);
return next;
}
function normalizeExistingCitationLinks(
content: string,
byIndex: Map<number, RuntimeDisplaySource>,
byUrl: Map<string, RuntimeDisplaySource>,
byTitle: Map<string, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
// Resolve every `[citation:LABEL](URL)` link — whether LABEL is a number
// ("[1]", "1") or a free-form title — to a numbered reference link that
// CitationLink renders as a badge and scrolls to the right-side panel.
return content.replace(
/\[citation:([^\]]+)\]\(([^)]+)\)(?:\([^)]+\))*/g,
(match, rawLabel: string, rawUrl: string) => {
const label = rawLabel.trim();
const url = rawUrl.trim();
const numberMatch = label.match(/^\[?(\d+)\]?$/);
if (numberMatch) {
const index = Number(numberMatch[1]);
const source = byIndex.get(index) ?? orderedSources[index - 1];
if (source) {
return citationLinkMarkdown(source);
}
}
const sourceByUrl = byUrl.get(url);
if (sourceByUrl) {
return citationLinkMarkdown(sourceByUrl);
}
const sourceByTitle = byTitle.get(label);
if (sourceByTitle) {
return citationLinkMarkdown(sourceByTitle);
}
return match;
},
);
}
function citationMarkdown(
fallback: string,
index: number,
byIndex: Map<number, RuntimeDisplaySource>,
orderedSources: RuntimeDisplaySource[],
): string {
const source = byIndex.get(index) ?? orderedSources[index - 1];
if (!source) {
return fallback;
}
return citationLinkMarkdown(source);
}
function hasCitationMarkdown(content: string): boolean {
return /\[citation:(?:\[[^\]]+\]|\d+)\]\(#reference-[^)]+\)/.test(content);
}
function appendCitationSummary(
content: string,
citationSources: RuntimeDisplaySource[],
): string {
const citations = citationSources
.slice(0, 5)
.map((source) => citationLinkMarkdown(source))
.join(" ");
if (!citations) {
return content;
}
const trimmed = content.trimEnd();
return `${trimmed}${trimmed ? "\n\n" : ""}参考来源:${citations}`;
}
function citationLinkMarkdown(source: RuntimeDisplaySource): string {
return `[citation:${source.index}](#reference-${source.index})`;
}
function normalizeName(name: string): string {
return name.toLowerCase().replace(/[^a-z0-9]+/g, "");
}
function normalizeCitationKey(value: string): string {
return value
.toLowerCase()
.normalize("NFKC")
.replace(/\s+/g, " ")
.trim();
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function stringifyMaybe(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value : undefined;
}
function findToolResultMessage(
toolCall: RuntimeToolCall,
messages: Message[],
toolCallMessageIndex = -1,
consumedToolMessageIds?: Set<string>,
consumedToolMessages?: Set<Message>,
): Message | undefined {
const byId = findToolMessageById(toolCall.id, messages);
if (
byId &&
!consumedToolMessages?.has(byId) &&
(!byId.id || !consumedToolMessageIds?.has(byId.id))
) {
return byId;
}
return findToolMessageByName(
toolCall,
messages,
toolCallMessageIndex,
consumedToolMessageIds,
consumedToolMessages,
);
}
function parseToolMessagePayload(toolMessage: Message): unknown {
const structured = extractStructuredToolPayload(toolMessage.content);
if (structured !== undefined) {
return structured;
}
const content = extractToolText(toolMessage.content);
if (!content) {
return undefined;
}
const parsed = parseJSONLikeText(content);
if (parsed !== undefined) {
return parsed;
}
return content;
}
function extractToolMessageName(message: Message): string {
return (
stringifyMaybe((message as { name?: unknown }).name) ??
stringifyMaybe(
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
?.name,
) ??
"tool"
);
}
function findToolMessageById(
toolCallId: string | undefined,
messages: Message[],
): Message | undefined {
if (!toolCallId) {
return undefined;
}
return messages.find((message) => {
if (message.type !== "tool") {
return false;
}
const candidates = [
(message as { tool_call_id?: unknown }).tool_call_id,
(message as { id?: unknown }).id,
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
?.tool_call_id,
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
?.id,
];
return candidates.some((candidate) => candidate === toolCallId);
});
}
function findToolMessageByName(
toolCall: RuntimeToolCall,
messages: Message[],
toolCallMessageIndex: number,
consumedToolMessageIds?: Set<string>,
consumedToolMessages?: Set<Message>,
): Message | undefined {
const start = Math.max(toolCallMessageIndex + 1, 0);
for (let index = start; index < messages.length; index++) {
const message = messages[index];
if (!message) {
continue;
}
if (index > start && (message.type === "human" || message.type === "ai")) {
break;
}
if (message.type !== "tool") {
continue;
}
if (message.id && consumedToolMessageIds?.has(message.id)) {
continue;
}
if (consumedToolMessages?.has(message)) {
continue;
}
const name =
stringifyMaybe((message as { name?: unknown }).name) ??
stringifyMaybe(
(message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs
?.name,
);
const messageToolCallId = stringifyMaybe(
(message as { tool_call_id?: unknown }).tool_call_id,
);
if (messageToolCallId && toolCall.id && messageToolCallId !== toolCall.id) {
continue;
}
if (name && normalizeName(name) !== normalizeName(toolCall.name)) {
continue;
}
const payload = extractStructuredToolPayload(message.content);
if (payload !== undefined) {
return message;
}
const text = extractToolText(message.content);
if (text && parseJSONLikeText(text) !== undefined) {
return message;
}
}
return undefined;
}
function extractStructuredToolPayload(content: unknown): unknown {
if (isRecord(content)) {
return unwrapToolPayloadObject(content);
}
if (Array.isArray(content)) {
for (const part of content) {
if (!isRecord(part)) {
continue;
}
if (isRecord(part.json)) {
return unwrapToolPayloadObject(part.json);
}
const unwrapped = unwrapToolPayloadObject(part);
if (unwrapped !== undefined) {
return unwrapped;
}
}
}
return undefined;
}
function unwrapToolPayloadObject(value: Record<string, unknown>): unknown {
if (
Array.isArray(value.results) ||
Array.isArray(value.data) ||
Array.isArray(value.items)
) {
return value;
}
for (const key of ["result", "output", "stdout", "stderr", "content", "text"]) {
const nested = value[key];
if (typeof nested === "string") {
const parsed = parseJSONLikeText(nested);
if (parsed !== undefined) {
return parsed;
}
}
if (isRecord(nested)) {
const unwrapped = unwrapToolPayloadObject(nested);
if (unwrapped !== undefined) {
return unwrapped;
}
}
}
return undefined;
}
function parseJSONLikeText(text: string): unknown {
const trimmed = text.trim();
if (!trimmed) {
return undefined;
}
try {
return JSON.parse(trimmed) as unknown;
} catch {
return extractJSONFromText(trimmed) ?? extractSearchPayloadFromText(trimmed);
}
}
function extractJSONFromText(text: string): unknown {
const candidates: Array<[number, number]> = [];
const firstObj = text.indexOf("{");
if (firstObj !== -1) candidates.push([firstObj, text.lastIndexOf("}")]);
const firstArr = text.indexOf("[");
if (firstArr !== -1) candidates.push([firstArr, text.lastIndexOf("]")]);
candidates.sort(([a], [b]) => a - b);
for (const [start, end] of candidates) {
if (start < 0 || end <= start) continue;
const slice = text.slice(start, end + 1);
try {
return JSON.parse(slice);
} catch {
// try the next candidate
}
}
return undefined;
}
function extractSearchPayloadFromText(text: string): unknown {
if (!text.includes('"results"') || !text.includes('"recUuid"')) {
return undefined;
}
const results: Record<string, unknown>[] = [];
const objectPattern = /\{\s*"index"\s*:\s*\d+[\s\S]*?\n\s*\}/g;
for (const match of text.matchAll(objectPattern)) {
try {
const item = JSON.parse(match[0]) as unknown;
if (isRecord(item) && item.recUuid) {
results.push(item);
}
} catch {
const item = extractSearchItemFromText(match[0]);
if (item) {
results.push(item);
}
}
}
if (results.length === 0) {
return undefined;
}
return {
success: true,
query: extractStringField(text, "query") ?? "",
count: results.length,
source: extractStringField(text, "source") ?? "tool-output",
results,
};
}
function extractSearchItemFromText(text: string): Record<string, unknown> | null {
const recUuid = extractStringField(text, "recUuid");
if (!recUuid) {
return null;
}
const score = extractNumberField(text, "relevance_score");
return {
index: extractNumberField(text, "index") ?? undefined,
recUuid,
relevance_score: score,
title: extractStringField(text, "title") ?? recUuid,
source_category: extractStringField(text, "source_category") ?? "",
source: extractStringField(text, "source") ?? "",
publish_time: extractStringField(text, "publish_time") ?? "",
content_preview: extractStringField(text, "content_preview") ?? "",
content_length: extractNumberField(text, "content_length") ?? undefined,
};
}
function extractStringField(text: string, field: string): string | undefined {
const pattern = new RegExp(`"${escapeRegExp(field)}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`);
const match = pattern.exec(text);
if (!match) {
return undefined;
}
try {
return JSON.parse(`"${match[1]}"`) as string;
} catch {
return match[1];
}
}
function extractNumberField(text: string, field: string): number | undefined {
const pattern = new RegExp(`"${escapeRegExp(field)}"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`);
const match = pattern.exec(text);
if (!match) {
return undefined;
}
const value = Number(match[1]);
return Number.isFinite(value) ? value : undefined;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function extractToolText(content: unknown): string {
if (typeof content === "string") {
return content.trim();
}
if (Array.isArray(content)) {
return content
.map((part) => {
if (typeof part === "string") {
return part;
}
if (part && typeof part === "object" && "text" in part) {
const text = (part as { text?: unknown }).text;
return typeof text === "string" ? text : "";
}
return "";
})
.join("\n")
.trim();
}
return "";
}