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