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 { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||||
import { splitMarkdownImages } from "@/lib/canvas/markdown-image-segments";
|
import { splitMarkdownImages } from "@/lib/canvas/markdown-image-segments";
|
||||||
import { stableStreamingMarkdown } from "@/lib/canvas/markdown-image-streaming";
|
import { stableStreamingMarkdown } from "@/lib/canvas/markdown-image-streaming";
|
||||||
|
import { getGatewayErrorPageMessage } from "@/lib/request-error";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
@ -1028,7 +1029,10 @@ export const Response = ({
|
|||||||
|
|
||||||
const rawText =
|
const rawText =
|
||||||
typeof props.children === "string"
|
typeof props.children === "string"
|
||||||
? normalizeMarkdownContent(stableStreamingMarkdown(props.children, isMarkdownStreaming))
|
? normalizeMarkdownContent(stableStreamingMarkdown(
|
||||||
|
getGatewayErrorPageMessage(props.children) || props.children,
|
||||||
|
isMarkdownStreaming
|
||||||
|
))
|
||||||
: null;
|
: null;
|
||||||
const shouldShowPdfDownload =
|
const shouldShowPdfDownload =
|
||||||
Boolean(enablePdfDownload && rawText && hasDownloadableReportContent(rawText));
|
Boolean(enablePdfDownload && rawText && hasDownloadableReportContent(rawText));
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type { Vote } from "@/lib/db/schema";
|
|||||||
import type { ChatMessage } from "@/lib/types";
|
import type { ChatMessage } from "@/lib/types";
|
||||||
import { cn, sanitizeText } from "@/lib/utils";
|
import { cn, sanitizeText } from "@/lib/utils";
|
||||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||||
|
import { getGatewayErrorPageMessage } from "@/lib/request-error";
|
||||||
import { useDataStream } from "./data-stream-provider";
|
import { useDataStream } from "./data-stream-provider";
|
||||||
import { DocumentToolResult } from "./document";
|
import { DocumentToolResult } from "./document";
|
||||||
import { DocumentPreview } from "./document-preview";
|
import { DocumentPreview } from "./document-preview";
|
||||||
@ -521,7 +522,7 @@ const PurePreviewMessage = ({
|
|||||||
const displayModelName = extractedModelName || inferredFallbackModelName;
|
const displayModelName = extractedModelName || inferredFallbackModelName;
|
||||||
const filteredText =
|
const filteredText =
|
||||||
message.role === "assistant"
|
message.role === "assistant"
|
||||||
? appendImageDoneSummary(contentWithoutBadge)
|
? getGatewayErrorPageMessage(contentWithoutBadge) || appendImageDoneSummary(contentWithoutBadge)
|
||||||
: contentWithoutBadge;
|
: contentWithoutBadge;
|
||||||
const useAssistantTypewriter =
|
const useAssistantTypewriter =
|
||||||
message.role === "assistant" &&
|
message.role === "assistant" &&
|
||||||
|
|||||||
@ -48,6 +48,7 @@ import {
|
|||||||
VIP98K_REFERENCE_STAGING_KEY_PREFIX,
|
VIP98K_REFERENCE_STAGING_KEY_PREFIX,
|
||||||
} from "@/lib/ai/vip98k-reference-staging";
|
} from "@/lib/ai/vip98k-reference-staging";
|
||||||
import { createLogger } from "@/lib/observability/logger";
|
import { createLogger } from "@/lib/observability/logger";
|
||||||
|
import { getUserFacingImageGenerationErrorMessage } from "@/lib/request-error";
|
||||||
import { createDeferredUploadedImage } from "@/lib/storage/image-archive";
|
import { createDeferredUploadedImage } from "@/lib/storage/image-archive";
|
||||||
import {
|
import {
|
||||||
IMAGE_UPLOAD_MAX_BYTES,
|
IMAGE_UPLOAD_MAX_BYTES,
|
||||||
@ -1188,9 +1189,9 @@ async function submitTask(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((response.status === 409 && /idempotency\s+key.*(?:still\s+in\s+progress|processing)/i.test(errorMessage))
|
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))
|
|| /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})`);
|
throw new Error(`${errorMessage} (HTTP ${response.status})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
isMediaInputFailure,
|
isMediaInputFailure,
|
||||||
isNonProviderRequestStatus,
|
isNonProviderRequestStatus,
|
||||||
} from "@/lib/ai/runtime/request-failure-classification";
|
} 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 =
|
export type AiModelHealthEventType =
|
||||||
| "success"
|
| "success"
|
||||||
@ -343,6 +343,7 @@ export function classifyAiModelHealthFailure(error: unknown): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function shouldFailoverAiModelError(error: unknown) {
|
export function shouldFailoverAiModelError(error: unknown) {
|
||||||
|
if (isGatewayResponseTimeout(error)) return false;
|
||||||
const classification = classifyAiModelHealthFailure(error);
|
const classification = classifyAiModelHealthFailure(error);
|
||||||
const normalized = getErrorMessage(error).toLowerCase();
|
const normalized = getErrorMessage(error).toLowerCase();
|
||||||
const imageProviderTaskFailed =
|
const imageProviderTaskFailed =
|
||||||
|
|||||||
@ -175,6 +175,28 @@ function getRequestErrorText(error: unknown) {
|
|||||||
return candidates.join(" ").slice(0, 4000);
|
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 {
|
export function isProviderBalanceError(error: unknown): boolean {
|
||||||
const message = getRequestErrorText(error);
|
const message = getRequestErrorText(error);
|
||||||
return PROVIDER_BALANCE_PATTERNS.some((pattern) => pattern.test(message));
|
return PROVIDER_BALANCE_PATTERNS.some((pattern) => pattern.test(message));
|
||||||
@ -203,6 +225,8 @@ export function getUserFacingImageGenerationErrorMessage(
|
|||||||
error: unknown,
|
error: unknown,
|
||||||
fallbackMessage = "图片生成失败,请稍后重试",
|
fallbackMessage = "图片生成失败,请稍后重试",
|
||||||
) {
|
) {
|
||||||
|
const gatewayMessage = getPublicGatewayErrorMessage(error);
|
||||||
|
if (gatewayMessage) return gatewayMessage;
|
||||||
if (isProviderBalanceError(error)) {
|
if (isProviderBalanceError(error)) {
|
||||||
return "图片服务商线路余额或额度不足,请切换模型或联系管理员;与您的积分余额无关。";
|
return "图片服务商线路余额或额度不足,请切换模型或联系管理员;与您的积分余额无关。";
|
||||||
}
|
}
|
||||||
@ -227,6 +251,8 @@ export function getUserFacingRequestErrorMessage(
|
|||||||
rateLimitMessage?: string;
|
rateLimitMessage?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
const gatewayMessage = getPublicGatewayErrorMessage(error);
|
||||||
|
if (gatewayMessage) return gatewayMessage;
|
||||||
if (isProviderBalanceError(error)) {
|
if (isProviderBalanceError(error)) {
|
||||||
return "模型服务商线路余额或额度不足,请切换模型或联系管理员。";
|
return "模型服务商线路余额或额度不足,请切换模型或联系管理员。";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Neural Particle - Scatter Effect</title>
|
<title>Neural Particle - Scatter Effect</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
|
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { overflow: hidden; background-color: #000; }
|
body { overflow: hidden; background-color: #000; }
|
||||||
@ -14,7 +13,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="particle-container"></div>
|
<div id="particle-container"></div>
|
||||||
<script>
|
<script type="module">
|
||||||
|
import * as THREE from "/three/0.160.0/three.module.min.js";
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
effect: "scatter",
|
effect: "scatter",
|
||||||
effectMode: 1,
|
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 {
|
import {
|
||||||
getUserFacingImageGenerationErrorMessage,
|
getUserFacingImageGenerationErrorMessage,
|
||||||
getUserFacingRequestErrorMessage,
|
getUserFacingRequestErrorMessage,
|
||||||
|
getGatewayErrorPageMessage,
|
||||||
|
isGatewayResponseTimeout,
|
||||||
isProviderBalanceError,
|
isProviderBalanceError,
|
||||||
isRateLimitError,
|
isRateLimitError,
|
||||||
} from "@/lib/request-error";
|
} from "@/lib/request-error";
|
||||||
@ -16,6 +18,49 @@ test("a bare HTTP 429 is rate limiting, not insufficient balance", () => {
|
|||||||
expect(getUserFacingRequestErrorMessage(error)).toContain("请求过于频繁");
|
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", () => {
|
test("an explicit provider balance error takes precedence over HTTP 429", () => {
|
||||||
const error = Object.assign(new Error("余额不足,请点击充值页面续费(HTTP 429)"), { status: 429 });
|
const error = Object.assign(new Error("余额不足,请点击充值页面续费(HTTP 429)"), { status: 429 });
|
||||||
expect(isProviderBalanceError(error)).toBe(true);
|
expect(isProviderBalanceError(error)).toBe(true);
|
||||||
@ -62,8 +107,10 @@ function healthHarness() {
|
|||||||
let model: any = { id: "test", modelId: "test", isEnabled: true, excludeFromFailover: true,
|
let model: any = { id: "test", modelId: "test", isEnabled: true, excludeFromFailover: true,
|
||||||
excludeFromHealthBan: true, healthStatus: "healthy", healthConsecutiveFailures: 0 };
|
excludeFromHealthBan: true, healthStatus: "healthy", healthConsecutiveFailures: 0 };
|
||||||
const events: any[] = [];
|
const events: any[] = [];
|
||||||
const query = (rows: () => any[]) => ({ from() { return this; }, where() { return this; },
|
const query = (rows: () => any[]) => Object.assign(Promise.resolve().then(rows), {
|
||||||
limit: async () => rows(), then: (resolve: any) => Promise.resolve(rows()).then(resolve), returning: async () => rows() });
|
from() { return this; }, where() { return this; },
|
||||||
|
limit: async () => rows(), returning: async () => rows(),
|
||||||
|
});
|
||||||
const db = {
|
const db = {
|
||||||
select: () => query(() => [model]),
|
select: () => query(() => [model]),
|
||||||
insert: () => ({ values: async (value: any) => { events.push(value); } }),
|
insert: () => ({ values: async (value: any) => { events.push(value); } }),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user