InfoLens / cf /facade /src /remote_log.js
dqy08's picture
模型输出异常时加格式强化提示词重试;远程失败写入 STATE KV
03687f3
Raw
History Blame Contribute Delete
5.43 kB
import { feedbackKey, utcSavedAt } from './extension_feedback.js';
const ERROR_DETAIL_MAX = 500;
export const CUSTOM_DOMAIN = 'api.info-lens.app';
export const AUTO_ERROR_TTL_SEC = 7 * 24 * 3600; // 7 天(与 Workers Observability 日志保留周期对齐)
/** 格式 unparseable 时,首次之外再打几次(带 formatReminder)。 */
export const UNPARSEABLE_RETRIES = 3;
export function isCustomDomain(request) {
if (!request) return false;
try {
const hostHeader = request.headers?.get?.('host') || '';
const hostName = hostHeader.split(':')[0].trim().toLowerCase();
if (hostName === CUSTOM_DOMAIN) return true;
const url = new URL(request.url);
return url.hostname.toLowerCase() === CUSTOM_DOMAIN;
} catch {
return false;
}
}
/**
* 远程推理失败粗分(message 给用户;error_detail 仅日志/反馈,不含 query/text):
* - network:Worker↔上游 传输层(超时、断连、fetch 失败)
* - inference:推理 API 暂时/服务端错误(408/429/5xx、响应体内 error)
* - internal:我方未预期(缺密钥、输出不可解析、4xx 鉴权/请求、未知)
*
* 扩展本地错误(正文变化、无正文等)不经此函数,见 content.js showFindError。
* @returns {{ kind: 'network' | 'inference' | 'internal', message: string, error_detail: string }}
*/
export function publicRemoteError(err) {
const raw = err && err.message != null ? String(err.message) : String(err);
const error_detail =
raw.length <= ERROR_DETAIL_MAX ? raw : raw.slice(0, ERROR_DETAIL_MAX - 1) + '…';
if (
/^network:/i.test(raw) ||
(typeof TypeError !== 'undefined' && err instanceof TypeError) ||
/network|fetch failed|timed out|timeout|ECONNRESET|connection/i.test(raw)
) {
return { kind: 'network', message: 'Network error', error_detail };
}
const http =
raw.match(/OpenRouter HTTP (\d+)/) ||
raw.match(/OpenRouter non-JSON response: HTTP (\d+)/);
if (http) {
const code = parseInt(http[1], 10);
if (code === 408 || code === 429 || code >= 500) {
return {
kind: 'inference',
message: `Inference API temporarily unavailable (${code})`,
error_detail,
};
}
// 4xx(含 401/403/402/404/400):配置或请求侧,归我方未预期
return {
kind: 'internal',
message: `Unexpected analysis error (${code})`,
error_detail,
};
}
// 流式中断内嵌错误(HTTP 200 后的 SSE error 事件):携带上游 code;5xx/429/408 归一时性
const streamErr =
raw.match(/OpenRouter stream error:/);
if (streamErr) {
const se = raw.match(/code=(\d+)/);
if (se) {
const code = parseInt(se[1], 10);
if (code === 408 || code === 429 || code >= 500) {
return {
kind: 'inference',
message: `Inference API temporarily unavailable (${code})`,
error_detail,
};
}
}
// 无 code 或 4xx 属模型/provider 侧异常,仍是推理过程问题(非我方契约错误)
return {
kind: 'inference',
message: 'Inference API temporarily unavailable',
error_detail,
};
}
// HTTP 200 但 body 带 error:视为推理侧
if (/^OpenRouter error:/i.test(raw)) {
return {
kind: 'inference',
message: 'Inference API temporarily unavailable',
error_detail,
};
}
// 模型输出无法按约定解析(unparseable / 缺 tool_calls):多为格式契约问题,重试无益;
// 不向用户暴露具体原因,统一提示格式异常;原始输出保留在 error_detail(日志/反馈)用于诊断。
if (
/^unparseable /i.test(raw) ||
/^missing tool_calls/i.test(raw)
) {
return {
kind: 'internal',
message: 'Unparsable model output',
error_detail,
};
}
return {
kind: 'internal',
message: 'Unexpected analysis error',
error_detail,
};
}
/**
* 远程推理失败:结构化写入 Workers Logs,并在请求来自自定义域名时向 STATE KV 写入错误快照。
*/
export async function logRemoteFailure(event, err, pub, env, request, extra) {
const raw = err && err.message != null ? String(err.message) : String(err);
const extraFields = extra && typeof extra === 'object' ? extra : {};
console.error({
event,
kind: pub.kind,
message: pub.message,
// 日志可长于返回给客户端的 error_detail(后者仍截断 500)
error_detail: raw.length <= 4000 ? raw : raw.slice(0, 3999) + '…',
...extraFields,
});
if (env && env.STATE && isCustomDomain(request)) {
try {
const id8 = (
typeof globalThis.crypto?.randomUUID === 'function'
? globalThis.crypto.randomUUID()
: Math.random().toString(36).slice(2, 10)
)
.replace(/-/g, '')
.slice(0, 8);
const key = feedbackKey(id8);
const record = {
saved_at: utcSavedAt(),
source: 'facade_auto',
event,
kind: pub.kind,
message: pub.message,
error_detail: pub.error_detail,
domain: CUSTOM_DOMAIN,
...extraFields,
};
await env.STATE.put(key, JSON.stringify(record), {
expirationTtl: AUTO_ERROR_TTL_SEC,
});
} catch (e) {
const msg = e && e.message ? String(e.message) : String(e);
console.error('[facade_auto] failed to record error in KV:', msg);
}
}
}