Compare commits
1 Commits
main
...
codex/part
| Author | SHA1 | Date | |
|---|---|---|---|
| 31bba5aac9 |
@ -18,6 +18,7 @@ import { ResponsePdfDownloadButton } from "@/components/elements/response-pdf-do
|
||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||
import { splitMarkdownImages } from "@/lib/canvas/markdown-image-segments";
|
||||
import { stableStreamingMarkdown } from "@/lib/canvas/markdown-image-streaming";
|
||||
import { getGatewayErrorPageMessage } from "@/lib/request-error";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@ -1028,7 +1029,10 @@ export const Response = ({
|
||||
|
||||
const rawText =
|
||||
typeof props.children === "string"
|
||||
? normalizeMarkdownContent(stableStreamingMarkdown(props.children, isMarkdownStreaming))
|
||||
? normalizeMarkdownContent(stableStreamingMarkdown(
|
||||
getGatewayErrorPageMessage(props.children) || props.children,
|
||||
isMarkdownStreaming
|
||||
))
|
||||
: null;
|
||||
const shouldShowPdfDownload =
|
||||
Boolean(enablePdfDownload && rawText && hasDownloadableReportContent(rawText));
|
||||
|
||||
@ -7,6 +7,7 @@ import type { Vote } from "@/lib/db/schema";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { cn, sanitizeText } from "@/lib/utils";
|
||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||
import { getGatewayErrorPageMessage } from "@/lib/request-error";
|
||||
import { useDataStream } from "./data-stream-provider";
|
||||
import { DocumentToolResult } from "./document";
|
||||
import { DocumentPreview } from "./document-preview";
|
||||
@ -521,7 +522,7 @@ const PurePreviewMessage = ({
|
||||
const displayModelName = extractedModelName || inferredFallbackModelName;
|
||||
const filteredText =
|
||||
message.role === "assistant"
|
||||
? appendImageDoneSummary(contentWithoutBadge)
|
||||
? getGatewayErrorPageMessage(contentWithoutBadge) || appendImageDoneSummary(contentWithoutBadge)
|
||||
: contentWithoutBadge;
|
||||
const useAssistantTypewriter =
|
||||
message.role === "assistant" &&
|
||||
|
||||
@ -48,6 +48,7 @@ import {
|
||||
VIP98K_REFERENCE_STAGING_KEY_PREFIX,
|
||||
} from "@/lib/ai/vip98k-reference-staging";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
import { getUserFacingImageGenerationErrorMessage } from "@/lib/request-error";
|
||||
import { createDeferredUploadedImage } from "@/lib/storage/image-archive";
|
||||
import {
|
||||
IMAGE_UPLOAD_MAX_BYTES,
|
||||
@ -1188,9 +1189,9 @@ async function submitTask(params: {
|
||||
continue;
|
||||
}
|
||||
if ((response.status === 409 && /idempotency\s+key.*(?:still\s+in\s+progress|processing)/i.test(errorMessage))
|
||||
|| [502, 504].includes(response.status)
|
||||
|| [502, 504, 524].includes(response.status)
|
||||
|| /pending\s+safe\s+reconciliation|request\s+outcome\s+(?:is\s+)?unknown|final\s+outcome\s+could\s+not\s+be\s+confirmed/i.test(errorMessage))
|
||||
throw new ProviderTaskUncertainError(`供应商提交结果未确认,可能仍在处理,请勿重复生成:${errorMessage} (HTTP ${response.status})`);
|
||||
throw new ProviderTaskUncertainError(`供应商提交结果未确认,可能仍在处理,请勿重复生成:${getUserFacingImageGenerationErrorMessage({ message: errorMessage, status: response.status })} (HTTP ${response.status})`);
|
||||
throw new Error(`${errorMessage} (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
isMediaInputFailure,
|
||||
isNonProviderRequestStatus,
|
||||
} from "@/lib/ai/runtime/request-failure-classification";
|
||||
import { isProviderBalanceError, isRateLimitError } from "@/lib/request-error";
|
||||
import { isGatewayResponseTimeout, isProviderBalanceError, isRateLimitError } from "@/lib/request-error";
|
||||
|
||||
export type AiModelHealthEventType =
|
||||
| "success"
|
||||
@ -343,6 +343,7 @@ export function classifyAiModelHealthFailure(error: unknown): {
|
||||
}
|
||||
|
||||
export function shouldFailoverAiModelError(error: unknown) {
|
||||
if (isGatewayResponseTimeout(error)) return false;
|
||||
const classification = classifyAiModelHealthFailure(error);
|
||||
const normalized = getErrorMessage(error).toLowerCase();
|
||||
const imageProviderTaskFailed =
|
||||
|
||||
@ -175,6 +175,28 @@ function getRequestErrorText(error: unknown) {
|
||||
return candidates.join(" ").slice(0, 4000);
|
||||
}
|
||||
|
||||
export function getGatewayErrorPageMessage(content: string): string | null {
|
||||
if (!/^\s*(?:<!doctype\s+html\b|<html\b)/i.test(content)) return null;
|
||||
if (!/\bcf-error-details\b/i.test(content)) return null;
|
||||
if (/\b524\b/.test(content)) {
|
||||
return "模型服务响应超时(HTTP 524),生成结果尚未确认。请先查看历史记录,避免重复提交。";
|
||||
}
|
||||
return "模型服务暂时不可用,请稍后再试或联系管理员。";
|
||||
}
|
||||
|
||||
export function isGatewayResponseTimeout(error: unknown): boolean {
|
||||
return getRequestErrorStatus(error) === 524 ||
|
||||
/\bHTTP\s*524\b|\b524\s*:\s*A timeout occurred\b|Error code\s*524\b/i.test(getRequestErrorText(error));
|
||||
}
|
||||
|
||||
function getPublicGatewayErrorMessage(error: unknown): string | null {
|
||||
if (isGatewayResponseTimeout(error)) {
|
||||
return "模型服务响应超时(HTTP 524),生成结果尚未确认。请先查看历史记录,避免重复提交。";
|
||||
}
|
||||
const message = getRequestErrorMessage(error);
|
||||
return message ? getGatewayErrorPageMessage(message) : null;
|
||||
}
|
||||
|
||||
export function isProviderBalanceError(error: unknown): boolean {
|
||||
const message = getRequestErrorText(error);
|
||||
return PROVIDER_BALANCE_PATTERNS.some((pattern) => pattern.test(message));
|
||||
@ -203,6 +225,8 @@ export function getUserFacingImageGenerationErrorMessage(
|
||||
error: unknown,
|
||||
fallbackMessage = "图片生成失败,请稍后重试",
|
||||
) {
|
||||
const gatewayMessage = getPublicGatewayErrorMessage(error);
|
||||
if (gatewayMessage) return gatewayMessage;
|
||||
if (isProviderBalanceError(error)) {
|
||||
return "图片服务商线路余额或额度不足,请切换模型或联系管理员;与您的积分余额无关。";
|
||||
}
|
||||
@ -227,6 +251,8 @@ export function getUserFacingRequestErrorMessage(
|
||||
rateLimitMessage?: string;
|
||||
}
|
||||
) {
|
||||
const gatewayMessage = getPublicGatewayErrorMessage(error);
|
||||
if (gatewayMessage) return gatewayMessage;
|
||||
if (isProviderBalanceError(error)) {
|
||||
return "模型服务商线路余额或额度不足,请切换模型或联系管理员。";
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neural Particle - Scatter Effect</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { overflow: hidden; background-color: #000; }
|
||||
@ -14,7 +13,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="particle-container"></div>
|
||||
<script>
|
||||
<script type="module">
|
||||
import * as THREE from "/three/0.160.0/three.module.min.js";
|
||||
const CONFIG = {
|
||||
effect: "scatter",
|
||||
effectMode: 1,
|
||||
|
||||
21
public/three/0.160.0/LICENSE
Normal file
21
public/three/0.160.0/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright © 2010-2023 three.js authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
6
public/three/0.160.0/three.module.min.js
vendored
Normal file
6
public/three/0.160.0/three.module.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -2,6 +2,8 @@ import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
getUserFacingImageGenerationErrorMessage,
|
||||
getUserFacingRequestErrorMessage,
|
||||
getGatewayErrorPageMessage,
|
||||
isGatewayResponseTimeout,
|
||||
isProviderBalanceError,
|
||||
isRateLimitError,
|
||||
} from "@/lib/request-error";
|
||||
@ -16,6 +18,49 @@ test("a bare HTTP 429 is rate limiting, not insufficient balance", () => {
|
||||
expect(getUserFacingRequestErrorMessage(error)).toContain("请求过于频繁");
|
||||
});
|
||||
|
||||
const gatewayHtml = '<!DOCTYPE html><html><head><title>vip98k.com | 524: A timeout occurred</title></head><body><div id="cf-error-details">Error code 524</div><script>window.gatewayDebug = true</script></body></html> (HTTP 524)';
|
||||
|
||||
test("gateway timeout pages become a concise unconfirmed-result message", () => {
|
||||
for (const error of [new Error(gatewayHtml), { status: 524, message: "Gateway failed" }, { error: { message: "Request failed (HTTP 524)" } }]) {
|
||||
expect(isGatewayResponseTimeout(error)).toBe(true);
|
||||
for (const text of [getUserFacingImageGenerationErrorMessage(error), getUserFacingRequestErrorMessage(error)]) {
|
||||
expect(text).toContain("HTTP 524");
|
||||
expect(text).toContain("结果尚未确认");
|
||||
expect(text).not.toMatch(/<html|<script|cf-error-details/);
|
||||
}
|
||||
}
|
||||
expect(getGatewayErrorPageMessage(gatewayHtml)).toContain("避免重复提交");
|
||||
});
|
||||
|
||||
test("ordinary HTML documents and fenced examples retain their content", () => {
|
||||
for (const text of ['<!DOCTYPE html><html><title>My page</title><p>Hosted on Cloudflare</p></html>', `\`\`\`html\n${gatewayHtml}\n\`\`\``, "Cloudflare 文档示例", "普通图片错误"]) {
|
||||
expect(getGatewayErrorPageMessage(text)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test("HTTP 524 does not start another model while the original result is unconfirmed", () => {
|
||||
const { health } = healthHarness();
|
||||
expect(health.shouldFailoverAiModelError(new Error(gatewayHtml))).toBe(false);
|
||||
expect(health.shouldFailoverAiModelError({ status: 524, message: "Gateway timeout" })).toBe(false);
|
||||
expect(health.shouldFailoverAiModelError(new Error("Service unavailable (HTTP 503)"))).toBe(true);
|
||||
});
|
||||
|
||||
test("the provider submits once and keeps an HTTP 524 response unconfirmed", async () => {
|
||||
let submissions = 0;
|
||||
const provider = serverLoader({
|
||||
"@/lib/db/queries": { getSystemSetting: async () => null },
|
||||
"@/lib/storage/image-archive": {},
|
||||
"@/lib/storage/service": {},
|
||||
fetch: async () => {
|
||||
submissions++;
|
||||
return new Response(gatewayHtml, { status: 524, headers: { "content-type": "text/html" } });
|
||||
},
|
||||
})("lib/ai/providers/vip98k.ts");
|
||||
const model = provider.createVip98k({ apiKey: "test", baseURL: "https://provider.test" }).chat("gpt-image-2.5");
|
||||
await expect(model.doGenerate({ messages: [{ role: "user", content: "A desk" }] })).rejects.toThrow(/结果未确认.*HTTP 524/);
|
||||
expect(submissions).toBe(1);
|
||||
});
|
||||
|
||||
test("an explicit provider balance error takes precedence over HTTP 429", () => {
|
||||
const error = Object.assign(new Error("余额不足,请点击充值页面续费(HTTP 429)"), { status: 429 });
|
||||
expect(isProviderBalanceError(error)).toBe(true);
|
||||
@ -62,8 +107,10 @@ function healthHarness() {
|
||||
let model: any = { id: "test", modelId: "test", isEnabled: true, excludeFromFailover: true,
|
||||
excludeFromHealthBan: true, healthStatus: "healthy", healthConsecutiveFailures: 0 };
|
||||
const events: any[] = [];
|
||||
const query = (rows: () => any[]) => ({ from() { return this; }, where() { return this; },
|
||||
limit: async () => rows(), then: (resolve: any) => Promise.resolve(rows()).then(resolve), returning: async () => rows() });
|
||||
const query = (rows: () => any[]) => Object.assign(Promise.resolve().then(rows), {
|
||||
from() { return this; }, where() { return this; },
|
||||
limit: async () => rows(), returning: async () => rows(),
|
||||
});
|
||||
const db = {
|
||||
select: () => query(() => [model]),
|
||||
insert: () => ({ values: async (value: any) => { events.push(value); } }),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user