fix: jina partial_success, bash base64, ticket parallel search, ExecutionLogPanel toolCallId, sanitizeTitle regex
This commit is contained in:
@@ -63,33 +63,32 @@ export async function ticketDetail(
|
||||
const tdTimeout = getToolConfig("ticket_detail").timeoutMs;
|
||||
|
||||
// If ticketId looks like a ticketNumber (e.g. "TK-2026-296691"), resolve UUID first.
|
||||
// Strategy: try query-param filter first; if no match, fall back to full list + local find.
|
||||
// Strategy: try query-param filters concurrently; if no match, fall back to full list.
|
||||
let resolvedId = ticketId;
|
||||
if (ticketId.startsWith("TK-")) {
|
||||
// Attempt 1: filter via query param (try both "ticketNumber" and "search" keys)
|
||||
for (const paramName of ["ticketNumber", "search"]) {
|
||||
// Attempt 1: concurrently try both "ticketNumber" and "search" param names
|
||||
const searchPromises = ["ticketNumber", "search"].map(async (paramName) => {
|
||||
const searchUrl = `${base}/api/tickets?${paramName}=${encodeURIComponent(ticketId)}&pageSize=50`;
|
||||
const searchResp = await fetch(searchUrl, {
|
||||
const resp = await fetch(searchUrl, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(tdTimeout),
|
||||
});
|
||||
if (searchResp.ok) {
|
||||
const searchData = await searchResp.json();
|
||||
if (!resp.ok) throw new Error(`${paramName} search failed`);
|
||||
const data = await resp.json();
|
||||
// API may return tickets under "tickets", "data", or "items" key
|
||||
const tickets: Record<string, unknown>[] =
|
||||
searchData.tickets ?? searchData.data ?? searchData.items ?? [];
|
||||
data.tickets ?? data.data ?? data.items ?? [];
|
||||
const match = tickets.find((t) => t.ticketNumber === ticketId);
|
||||
// API may use "id" or "_id" as the primary key
|
||||
const matchId = match?.id ?? match?._id;
|
||||
if (matchId) {
|
||||
resolvedId = String(matchId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matchId) throw new Error("not found");
|
||||
return String(matchId);
|
||||
});
|
||||
|
||||
// Attempt 2: if still unresolved, do a plain list and find locally
|
||||
if (resolvedId === ticketId) {
|
||||
try {
|
||||
resolvedId = await Promise.any(searchPromises);
|
||||
} catch {
|
||||
// Both failed — Attempt 2: fall back to full list + local find
|
||||
const listUrl = `${base}/api/tickets?pageSize=100`;
|
||||
const listResp = await fetch(listUrl, {
|
||||
headers,
|
||||
@@ -137,6 +136,8 @@ export async function webSearch(query: string): Promise<{
|
||||
description: string;
|
||||
content?: string;
|
||||
}>;
|
||||
enrichedCount: number;
|
||||
totalCount: number;
|
||||
}> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${config.jina.apiKey}`,
|
||||
@@ -180,7 +181,11 @@ export async function webSearch(query: string): Promise<{
|
||||
: "",
|
||||
}));
|
||||
|
||||
return { results: enriched };
|
||||
const enrichedCount = readResults.filter(
|
||||
(r) => r.status === "fulfilled" && r.value,
|
||||
).length;
|
||||
|
||||
return { results: enriched, enrichedCount, totalCount: results.length };
|
||||
}
|
||||
|
||||
// --- Serper Google Search (fast, structured) ---
|
||||
@@ -322,11 +327,12 @@ export async function sandboxRun(
|
||||
if (!(SUPPORTED_LANGUAGES as readonly string[]).includes(language)) {
|
||||
throw new Error(`Unsupported language: ${language}. Supported: ${SUPPORTED_LANGUAGES.join(", ")}`);
|
||||
}
|
||||
const escaped = code.replace(/'/g, "'\\''");
|
||||
// Use base64 encoding to avoid shell quoting/escaping issues
|
||||
const b64 = Buffer.from(code).toString("base64");
|
||||
const cmdMap: Record<SupportedLang, string> = {
|
||||
python: `python3 -c '${escaped}'`,
|
||||
javascript: `node -e '${escaped}'`,
|
||||
bash: `bash -c '${escaped}'`,
|
||||
python: `python3 -c "import base64,sys; exec(base64.b64decode('${b64}').decode())"`,
|
||||
javascript: `node -e "eval(Buffer.from('${b64}','base64').toString())"`,
|
||||
bash: `bash -c "$(echo '${b64}' | base64 -d)"`,
|
||||
};
|
||||
const cmd = cmdMap[language as SupportedLang];
|
||||
|
||||
|
||||
@@ -141,8 +141,10 @@ export async function toolExecutorNode(
|
||||
const parsed = webSearchDeepSchema.parse(args);
|
||||
let enriched: Array<{ title: string; url: string; snippet: string }>;
|
||||
let fallbackUsed = false;
|
||||
let readerAllFailed = false;
|
||||
try {
|
||||
const data = await executeWithRetry(() => webSearch(parsed.query));
|
||||
readerAllFailed = data.enrichedCount === 0 && data.totalCount > 0;
|
||||
enriched = (data.results ?? []).slice(0, 5).map((r) => ({
|
||||
title: r.title ?? "",
|
||||
url: r.url ?? "",
|
||||
@@ -205,19 +207,29 @@ export async function toolExecutorNode(
|
||||
results: enriched,
|
||||
citations: wCitations,
|
||||
sourceType: "external_web",
|
||||
confidence: fallbackUsed ? "medium" : "high",
|
||||
confidence: fallbackUsed || readerAllFailed ? "medium" : "high",
|
||||
},
|
||||
},
|
||||
{ message: lastAiMessage },
|
||||
);
|
||||
const wsStatus = fallbackUsed
|
||||
? "fallback"
|
||||
: readerAllFailed
|
||||
? "partial_success"
|
||||
: "ok";
|
||||
const wsStatusMsg = fallbackUsed
|
||||
? "深度搜索不可用,已使用快速搜索替代"
|
||||
: readerAllFailed
|
||||
? "搜索成功但全文读取全部失败,仅返回摘要"
|
||||
: undefined;
|
||||
statusList.push({
|
||||
tool: "web_search_deep",
|
||||
status: fallbackUsed ? "fallback" : "ok",
|
||||
...(fallbackUsed ? { message: "深度搜索不可用,已使用快速搜索替代" } : {}),
|
||||
status: wsStatus,
|
||||
...(wsStatusMsg ? { message: wsStatusMsg } : {}),
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
event: "tool_exec", tool: "web_search_deep",
|
||||
status: fallbackUsed ? "fallback_success" : "success",
|
||||
status: fallbackUsed ? "fallback_success" : readerAllFailed ? "partial_success" : "success",
|
||||
durationMs: Date.now() - startTime,
|
||||
inputSummary: `query: ${parsed.query}`.slice(0, 200),
|
||||
resultCount: enriched.length, traceId,
|
||||
@@ -228,7 +240,11 @@ export async function toolExecutorNode(
|
||||
content: JSON.stringify({
|
||||
total: enriched.length,
|
||||
results: enriched,
|
||||
...(fallbackUsed ? { note: "深度搜索不可用,已使用快速搜索替代" } : {}),
|
||||
...(fallbackUsed
|
||||
? { note: "深度搜索不可用,已使用快速搜索替代" }
|
||||
: readerAllFailed
|
||||
? { note: "全文读取全部失败,仅返回搜索摘要" }
|
||||
: {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ const SOURCE_MAP: Record<string, string> = {
|
||||
|
||||
interface LogEntry {
|
||||
tool: string;
|
||||
toolCallId?: string;
|
||||
status: string;
|
||||
durationMs?: number;
|
||||
retryCount?: number;
|
||||
|
||||
@@ -38,7 +38,8 @@ function getLocalTitle(threadId: string): string | null {
|
||||
}
|
||||
|
||||
// Reject titles that look like UUIDs, short hashes, or empty strings
|
||||
const BAD_TITLE = /^[0-9a-f-]{8,}$/i;
|
||||
// Match: pure hex+dash 8+ chars, or standard UUID format
|
||||
const BAD_TITLE = /^[0-9a-f-]{8,}$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
function sanitizeTitle(s: unknown): string | null {
|
||||
if (typeof s !== "string" || !s.trim() || BAD_TITLE.test(s.trim())) return null;
|
||||
return s.trim();
|
||||
|
||||
Reference in New Issue
Block a user