Add market watch and match hub workflows
这个提交包含在:
@@ -39,10 +39,12 @@ export function getSessionCookieOptions(
|
||||
// ? hostname
|
||||
// : undefined;
|
||||
|
||||
const secure = isSecureRequest(req);
|
||||
|
||||
return {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "none",
|
||||
secure: isSecureRequest(req),
|
||||
sameSite: secure ? "none" : "lax",
|
||||
secure,
|
||||
};
|
||||
}
|
||||
|
||||
29
server/_core/fetch.test.ts
普通文件
29
server/_core/fetch.test.ts
普通文件
@@ -0,0 +1,29 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { fetchWithTimeout } from "./fetch";
|
||||
|
||||
describe("fetchWithTimeout", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("retries timeout-like errors for allowed methods", async () => {
|
||||
const fetchMock = vi.fn()
|
||||
.mockRejectedValueOnce(new Error("Request timed out after 100ms"))
|
||||
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const response = await fetchWithTimeout("https://example.com", {
|
||||
method: "POST",
|
||||
}, {
|
||||
timeoutMs: 100,
|
||||
retries: 1,
|
||||
retryMethods: ["POST"],
|
||||
baseDelayMs: 1,
|
||||
});
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,12 @@ function shouldRetryError(method: string, error: unknown, options: FetchRetryOpt
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.name === "AbortError" || error.name === "TimeoutError" || error.message.includes("fetch");
|
||||
return (
|
||||
error.name === "AbortError" ||
|
||||
error.name === "TimeoutError" ||
|
||||
error.message.startsWith("Request timed out after ") ||
|
||||
error.message.includes("fetch")
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { appRouter } from "../routers";
|
||||
import { createContext } from "./context";
|
||||
import { registerMediaProxy } from "./mediaProxy";
|
||||
import { serveStatic } from "./static";
|
||||
import { createBackgroundTask, getAdminUserId, hasRecentBackgroundTaskOfType, seedAchievementDefinitions, seedAppSettings, seedTutorials, seedVisionReferenceImages } from "../db";
|
||||
import { createBackgroundTask, getAdminUserId, getAppSettingValue, hasRecentBackgroundTaskOfType, seedAchievementDefinitions, seedAppSettings, seedTutorials, seedVisionReferenceImages } from "../db";
|
||||
import { nanoid } from "nanoid";
|
||||
import { syncTutorialImages } from "../tutorialImages";
|
||||
|
||||
@@ -64,6 +64,32 @@ async function scheduleDailyNtrpRefresh() {
|
||||
});
|
||||
}
|
||||
|
||||
async function scheduleMarketWatchRefresh() {
|
||||
const intervalMinutes = Math.max(5, await getAppSettingValue("market_watch_refresh_interval_minutes", 30));
|
||||
const since = new Date(Date.now() - intervalMinutes * 60_000);
|
||||
const exists = await hasRecentBackgroundTaskOfType("market_watch_refresh", since);
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adminUserId = await getAdminUserId();
|
||||
if (!adminUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = nanoid();
|
||||
await createBackgroundTask({
|
||||
id: taskId,
|
||||
userId: adminUserId,
|
||||
type: "market_watch_refresh",
|
||||
title: "全网球拍行情刷新",
|
||||
message: "系统已自动创建球拍行情刷新任务",
|
||||
payload: { scope: "all_users", trigger: "scheduler" },
|
||||
progress: 0,
|
||||
maxAttempts: 3,
|
||||
});
|
||||
}
|
||||
|
||||
function isPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const server = net.createServer();
|
||||
@@ -129,6 +155,9 @@ async function startServer() {
|
||||
void scheduleDailyNtrpRefresh().catch((error) => {
|
||||
console.error("[scheduler] failed to schedule NTRP refresh", error);
|
||||
});
|
||||
void scheduleMarketWatchRefresh().catch((error) => {
|
||||
console.error("[scheduler] failed to schedule market refresh", error);
|
||||
});
|
||||
}, 60_000);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ export type InvokeParams = {
|
||||
output_schema?: OutputSchema;
|
||||
responseFormat?: ResponseFormat;
|
||||
response_format?: ResponseFormat;
|
||||
timeoutMs?: number;
|
||||
retryCount?: number;
|
||||
};
|
||||
|
||||
export type ToolCall = {
|
||||
@@ -286,6 +288,8 @@ export async function invokeLLM(params: InvokeParams): Promise<InvokeResult> {
|
||||
output_schema,
|
||||
responseFormat,
|
||||
response_format,
|
||||
timeoutMs,
|
||||
retryCount,
|
||||
} = params;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
@@ -332,8 +336,8 @@ export async function invokeLLM(params: InvokeParams): Promise<InvokeResult> {
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
}, {
|
||||
timeoutMs: ENV.llmTimeoutMs,
|
||||
retries: ENV.llmRetryCount,
|
||||
timeoutMs: timeoutMs ?? ENV.llmTimeoutMs,
|
||||
retries: retryCount ?? ENV.llmRetryCount,
|
||||
retryMethods: ["POST"],
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户