fix: jina partial_success, bash base64, ticket parallel search, ExecutionLogPanel toolCallId, sanitizeTitle regex
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 27s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 38s

This commit is contained in:
gongzhiyong
2026-04-12 16:06:21 +08:00
parent 73a7175051
commit 33cd268003
4 changed files with 55 additions and 31 deletions
@@ -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;
+2 -1
View File
@@ -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();