Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions messages/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@
"copySuccess": "Copied to clipboard",
"copyFailed": "Failed to copy",
"copyResult": "Copy Result",
"close": "Close",
"success": "Success",
"failed": "Failed"
},
Expand Down
1 change: 1 addition & 0 deletions messages/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@
"copySuccess": "已复制到剪贴板",
"copyFailed": "复制失败",
"copyResult": "复制结果",
"close": "关闭",
"success": "成功",
"failed": "失败"
},
Expand Down
1 change: 1 addition & 0 deletions messages/zh-TW/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@
"copySuccess": "已複製到剪貼簿",
"copyFailed": "複製失敗",
"copyResult": "複製結果",
"close": "關閉",
"success": "成功",
"failed": "失敗"
},
Expand Down
9 changes: 7 additions & 2 deletions src/actions/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1568,11 +1568,16 @@ async function executeProviderApiTest(
errorDetail = undefined;
}

// 使用 errorDetail 或 errorText 的前 200 字符作为错误详情
// 添加防御性检查,避免空字符串产生误导性错误消息
const finalErrorDetail =
errorDetail ?? (errorText ? clipText(errorText, 200) : "No error details available");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low: Fallback message "No error details available" could be misleading

Why this is a problem: When both errorDetail and errorText are empty/undefined, the fallback "No error details available" is technically correct but doesn't help users understand what went wrong. This can happen with network timeouts or connection refusals where the response body is empty.

Suggested fix:

const finalErrorDetail =
  errorDetail ?? 
  (errorText ? clipText(errorText, 200) : `HTTP ${response.status} with empty response body`);


logger.error("Provider API test failed", {
providerUrl: normalizedProviderUrl.replace(/:\/\/[^@]*@/, "://***@"),
path: typeof options.path === "string" ? options.path : "dynamic",
status: response.status,
errorDetail: errorDetail ?? clipText(errorText, 200),
errorDetail: finalErrorDetail,
});

return {
Expand All @@ -1582,7 +1587,7 @@ async function executeProviderApiTest(
message: `API 返回错误: HTTP ${response.status}`,
details: {
responseTime,
error: "API 请求失败,查看日志以获得更多信息",
error: finalErrorDetail,
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,17 +527,28 @@ export function ApiTestButton({
</div>
)}

{/* 复制按钮 */}
<Button
type="button"
variant="outline"
size="sm"
onClick={handleCopyResult}
className="w-full"
>
<Copy className="h-4 w-4 mr-2" />
{t("copyResult")}
</Button>
{/* 操作按钮 */}
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={handleCopyResult}
className="flex-1"
>
<Copy className="h-4 w-4 mr-2" />
{t("copyResult")}
</Button>
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => setIsDetailDialogOpen(false)}
className="flex-1"
>
{t("close")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
Expand Down
42 changes: 32 additions & 10 deletions src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@

import { logger } from "@/lib/logger";

/**
* 初始化错误规则检测器
* 提取为独立函数以避免代码重复
*
* 注意: 此函数会传播关键错误,调用者应决定是否需要优雅降级
*/
async function initializeErrorRuleDetector(): Promise<void> {
// 初始化默认错误规则 - 让关键错误传播
const { initializeDefaultErrorRules } = await import("@/repository/error-rules");
await initializeDefaultErrorRules();
logger.info("Default error rules initialized successfully");

// 加载错误规则缓存 - 让关键错误传播
const { errorRuleDetector } = await import("@/lib/error-rule-detector");
await errorRuleDetector.reload();
logger.info("Error rule detector cache loaded successfully");
}

export async function register() {
// 仅在服务器端执行
if (process.env.NEXT_RUNTIME === "nodejs") {
Expand Down Expand Up @@ -36,13 +54,15 @@ export async function register() {
const { ensurePriceTable } = await import("@/lib/price-sync/seed-initializer");
await ensurePriceTable();

// 初始化默认错误规则
const { initializeDefaultErrorRules } = await import("@/repository/error-rules");
// 初始化错误规则检测器(非关键功能,允许优雅降级)
try {
await initializeDefaultErrorRules();
logger.info("Default error rules initialized successfully");
await initializeErrorRuleDetector();
} catch (error) {
logger.error("Failed to initialize default error rules:", error);
logger.error(
"[Instrumentation] Non-critical: Error rule detector initialization failed",
error
);
// 继续启动 - 错误检测不是核心功能的关键依赖
}

// 初始化日志清理任务队列(如果启用)
Expand Down Expand Up @@ -72,13 +92,15 @@ export async function register() {
const { ensurePriceTable } = await import("@/lib/price-sync/seed-initializer");
await ensurePriceTable();

// 初始化默认错误规则
const { initializeDefaultErrorRules } = await import("@/repository/error-rules");
// 初始化错误规则检测器(非关键功能,允许优雅降级)
try {
await initializeDefaultErrorRules();
logger.info("Default error rules initialized successfully");
await initializeErrorRuleDetector();
} catch (error) {
logger.error("Failed to initialize default error rules:", error);
logger.error(
"[Instrumentation] Non-critical: Error rule detector initialization failed",
error
);
// 继续启动 - 错误检测不是核心功能的关键依赖
}

// ⚠️ 开发环境禁用通知队列(Bull + Turbopack 不兼容)
Expand Down
42 changes: 36 additions & 6 deletions src/lib/error-rule-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,9 @@ class ErrorRuleDetector {
private exactPatterns: Map<string, ExactPattern> = new Map();
private lastReloadTime: number = 0;
private isLoading: boolean = false;
private isInitialized: boolean = false; // 跟踪初始化状态

constructor() {
// 初始化时立即加载缓存(异步,不阻塞构造函数)
this.reload().catch((error) => {
logger.error("[ErrorRuleDetector] Failed to initialize cache:", error);
});

// 监听数据库变更事件,自动刷新缓存
eventEmitter.on("errorRulesUpdated", () => {
this.reload().catch((error) => {
Expand All @@ -76,6 +72,17 @@ class ErrorRuleDetector {
});
}

/**
* 确保规则已加载(懒加载,首次使用时或显式 reload 时调用)
* 避免在数据库未准备好时过早加载
*/
private async ensureInitialized(): Promise<void> {
if (this.isInitialized || this.isLoading) {
return;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium: Race condition in ensureInitialized() can cause multiple concurrent reloads

Why this is a problem: If multiple requests call detectAsync() concurrently when isInitialized is false, the check if (this.isInitialized || this.isLoading) can pass for multiple calls before any of them sets isLoading to true. This defeats the purpose of lazy initialization and can cause redundant database queries.

Suggested fix:

private initializationPromise: Promise<void> | null = null;

private async ensureInitialized(): Promise<void> {
  if (this.isInitialized) {
    return;
  }
  if (!this.initializationPromise) {
    this.initializationPromise = this.reload().finally(() => {
      this.initializationPromise = null;
    });
  }
  await this.initializationPromise;
}

}
await this.reload();
}

/**
* 从数据库重新加载错误规则
*/
Expand Down Expand Up @@ -169,6 +176,7 @@ class ErrorRuleDetector {
}

this.lastReloadTime = Date.now();
this.isInitialized = true; // 标记为已初始化

logger.info(
`[ErrorRuleDetector] Loaded ${rules.length} error rules: ` +
Expand All @@ -184,7 +192,22 @@ class ErrorRuleDetector {
}

/**
* 检测错误消息是否匹配任何规则
* 异步检测错误消息(推荐使用)
* 确保规则已加载后再进行检测
*
* @param errorMessage - 错误消息
* @returns 检测结果
*/
async detectAsync(errorMessage: string): Promise<ErrorDetectionResult> {
await this.ensureInitialized();
return this.detect(errorMessage);
}

/**
* 检测错误消息是否匹配任何规则(同步版本)
*
* 注意:如果规则未初始化,会记录警告并返回 false
* 推荐使用 detectAsync() 以确保规则已加载
*
* 检测顺序(性能优先):
* 1. 包含匹配(最快,O(n*m))
Expand All @@ -199,6 +222,13 @@ class ErrorRuleDetector {
return { matched: false };
}

// 如果未初始化,记录警告
if (!this.isInitialized && !this.isLoading) {
logger.warn(
"[ErrorRuleDetector] detect() called before initialization, results may be incomplete. Consider using detectAsync() instead."
);
}

const lowerMessage = errorMessage.toLowerCase();
const trimmedMessage = lowerMessage.trim();

Expand Down
Loading