# 日志系统重构设计文档 | 字段 | 值 | |------|---| | 文档版本 | v1 | | 创建日期 | 2026-07-07 | | 作者 | Trae 协作生成 | | 状态 | 待用户审查 | | 范围 | pino logger 抽象 + Request ID 贯穿 + 静默失败修复 + error.tsx 上报 + track-event 去重 | | 策略 | 应用层 SDK 改造,不部署外部可观测性服务(OTel / Sentry / Prometheus 等) | --- ## 1. 背景与现状 ### 1.1 现状调研摘要 基于 2026-07-07 全项目调研: | 维度 | 状态 | 说明 | |------|------|------| | 业务审计日志 | ✅ 已成熟 | audit / login / change logger + 3 张 DB 表 + audit 模块管理后台 | | 应用层结构化日志 | ❌ 未实现 | 88 处散乱 `console.*`,无级别、无 JSON、无 request ID | | Request Correlation | ❌ 未实现 | proxy.ts 中间件零日志、不注入请求 ID | | 错误边界上报 | ❌ 全部丢弃 | 130 个 `error.tsx` 接收 `error` prop 后直接丢弃,前端运行时错误对开发者不可见 | | 静默失败 | ⚠️ 严重 | 三个 audit-logger 的 catch 块完全吞没错误,运维无法感知审计写入失败 | | 统一错误处理 | ⚠️ 部分 | `handleActionError` / `handleApiError` 仅 `console.error`,前缀混乱(`[ExamAction]` / `[ActionError]` / `[ApiError]` / 无前缀混用) | | track-event | ⚠️ 重复 | 4 个 no-op stub(shared / rbac / course-plans / questions 各一份) | | 架构图覆盖 | ❌ 未覆盖 | 004 / 005 完全未记录可观测性基础设施 | ### 1.2 console.* 分布 | 类型 | 次数 | 文件数 | 主要位置 | |------|------|--------|----------| | `console.log` | 2 | 1 | `questions/utils/parse-content.ts` | | `console.error` | 67 | 30 | data-access 的 catch 块、`action-utils.ts`、`api-response.ts`、各 Server Action | | `console.warn` | 5 | 5 | `api/web-vitals/route.ts`、redis/store 等 | | `console.info` | 10 | 6 | `track-event.ts`、`api/web-vitals/route.ts`、notifications channels | | `console.debug` | 4 | 3 | 各模块 `track-event.ts`(仅 dev) | ### 1.3 关键约束 - **proxy.ts 在 Edge Runtime 运行**([proxy.ts:64](file:///e:/Desktop/CICD/src/proxy.ts#L64) 注释明确说明),`AsyncLocalStorage` 不可用(依赖 `node:async_hooks`,Edge 不支持) - 其余部分(RSC、Server Action、Route Handler、data-access)运行在 Node.js runtime,`AsyncLocalStorage` 完全支持 - 项目自部署(`next.config.ts` 中 `output: "standalone"`),运维通过 `docker logs` 查看日志 --- ## 2. 目标与非目标 ### 2.1 目标 1. **统一 logger 抽象**:pino 替换 88 处散乱 `console.*`,提供 `info/error/warn/debug` + `createModuleLogger(module)` 子 logger 2. **级别控制**:`LOG_LEVEL` 环境变量控制输出阈值(debug/info/warn/error),Zod 校验 3. **结构化输出**:生产环境 JSON(含 timestamp/level/module/requestId/msg),开发环境 `pino-pretty` 彩色文本 4. **Request ID 贯穿**:proxy.ts 生成 ID 并注入请求头,Node.js runtime 通过 `AsyncLocalStorage` 贯穿到 data-access / audit-logger 5. **静默失败告警**:三个 audit-logger 的 catch 块从 silent 改为 `logger.warn` 6. **统一错误处理接入**:`handleActionError` / `handleApiError` / `safeActionCall` 接入 logger,前缀通过 `module` 字段规范化 7. **track-event 去重合并**:4 个 no-op stub 合并为 1 个,统一通过 `createModuleLogger("track")` 8. **error.tsx 错误上报**:130 个 error.tsx 通过 `useErrorReport` Hook 上报到 `/api/client-error`,含节流防风暴 9. **架构图同步**:004 / 005 / known-issues.md 全量更新 ### 2.2 非目标(YAGNI) - OpenTelemetry / 链路追踪(trace) - Prometheus / `/metrics` 端点 - Sentry / Bugsnag / Datadog 等 SaaS 错误监控 - Web Vitals 后端持久化(保留现有 `console.warn`) - 数据库查询日志 / 慢查询日志 - 日志文件轮转(`docker logs` 已足够) - next-auth events 回调改造(与本次重构解耦) --- ## 3. 架构设计 ### 3.1 整体数据流 ``` [Client] ──HTTP──> [proxy.ts (Edge Runtime)] │ requestId = crypto.randomUUID() // Web Crypto API │ NextResponse.next({ request: { headers } }) │ 注入 x-request-id 到下游请求头 ▼ [RSC / Server Action / Route Handler (Node.js Runtime)] │ withRequestContext(fn): │ 1. headers().get("x-request-id") 读取 │ 2. requestContextStorage.run({ requestId }, fn) ▼ [data-access / audit-logger / 业务逻辑 (Node.js Runtime)] │ logger.info({...}, "msg") 调用 │ pino mixin 自动从 getRequestContext() 取 requestId ▼ [stdout: {"level":"info","time":...,"requestId":"abc-123","module":"audit","msg":"..."}] ``` ### 3.2 新增文件清单 ``` src/shared/lib/ ├─ logger.ts # pino 实例 + createModuleLogger 工厂 ├─ request-context.ts # AsyncLocalStorage(仅 Node.js runtime) └─ with-request-context.ts # Server Action / Route Handler 入口包装 src/shared/hooks/ └─ use-error-report.ts # error.tsx 公共上报 Hook(含节流) src/app/api/client-error/ └─ route.ts # 客户端错误接收端点 ``` ### 3.3 修改文件清单 | 文件 | 改动类型 | 说明 | |------|---------|------| | `package.json` | 新增依赖 | `pino`、`pino-pretty`(dev) | | `src/env.mjs` | 新增字段 | `LOG_LEVEL`(默认 `info`,Zod enum) | | `src/proxy.ts` | 修改 | 生成 requestId 并通过 `NextResponse.next` 注入请求头 | | `src/shared/lib/action-utils.ts` | 修改 | `handleActionError` / `safeActionCall` 用 logger | | `src/shared/lib/api-response.ts` | 修改 | `handleApiError` 用 logger | | `src/shared/lib/audit-logger.ts` | 修改 | catch 块从 silent 改为 `logger.warn` | | `src/shared/lib/change-logger.ts` | 修改 | 同上 | | `src/shared/lib/login-logger.ts` | 修改 | 同上 | | `src/shared/lib/track-event.ts` | 修改 | 改为 `createModuleLogger("track")`,删除 no-op 输出 | | `src/modules/rbac/lib/track.ts` | 删除 | 引用方改为从 `@/shared/lib/track-event` 导入 | | `src/modules/course-plans/lib/track-event.ts` | 删除 | 同上 | | `src/modules/questions/utils/track-event.ts` | 删除 | 同上 | | 88 处 `console.*` 调用点 | 修改 | 替换为 `logger.*` 或 `createModuleLogger(module)` | | 130 个 `error.tsx` | 修改 | 在 useEffect 中调用 `useErrorReport(error)` | | `next.config.ts` | 修改 | 在现有 `serverExternalPackages` 数组中添加 `"pino"`(当前已有 `mysql2`/`tencentcloud-sdk-nodejs`/`exceljs`) | | `.eslintrc` / `eslint.config.mjs` | 修改 | 新增 `no-console` 规则,仅允许 `logger.ts` 中使用 console | --- ## 4. 核心模块设计 ### 4.1 `src/shared/lib/logger.ts` ```ts import pino, { type Logger } from "pino" import { env } from "@/env.mjs" import { getRequestContext } from "./request-context" /** * 全局 logger 实例。 * * - 生产环境:JSON 输出到 stdout(docker logs 友好) * - 开发环境:pino-pretty 彩色文本 * - 自动从 AsyncLocalStorage 混入 requestId / userId(若存在) */ export const logger = pino({ level: env.LOG_LEVEL, base: { service: "cicd-app" }, formatters: { level: (label) => ({ level: label }), }, mixin: () => getRequestContext(), ...(env.NODE_ENV === "development" && { transport: { target: "pino-pretty", options: { colorize: true, translateTime: "SYS:standard" }, }, }), }) /** * 创建模块级子 logger,自动绑定 module 字段。 * * @example * ```ts * const log = createModuleLogger("audit") * log.info({ userId }, "User action logged") * // 输出: {"level":"info","module":"audit","msg":"User action logged", ...} * ``` */ export function createModuleLogger(module: string): Logger { return logger.child({ module }) } export type { Logger } ``` ### 4.2 `src/shared/lib/request-context.ts` ```ts import { AsyncLocalStorage } from "node:async_hooks" /** * 请求上下文,贯穿整个请求生命周期。 * * 仅在 Node.js Runtime 中可用(proxy.ts 是 Edge Runtime,不导入此模块)。 * 通过 withRequestContext 高阶函数注入。 */ export interface RequestContext { requestId: string userId?: string module?: string } export const requestContextStorage = new AsyncLocalStorage() /** * 获取当前请求上下文(若存在)。 * * - 在 withRequestContext 包装的调用栈内:返回完整上下文 * - 在调用栈外(如顶层模块初始化、定时任务):返回空对象 * * pino logger 的 mixin 配置会自动调用此函数混入 requestId。 */ export function getRequestContext(): Partial { return requestContextStorage.getStore() ?? {} } ``` ### 4.3 `src/shared/lib/with-request-context.ts` ```ts import { headers } from "next/headers" import { randomUUID } from "node:crypto" import { requestContextStorage, type RequestContext } from "./request-context" /** * 包装 Server Action / Route Handler,注入 requestId 到 AsyncLocalStorage。 * * 工作流程: * 1. 通过 `headers()` 读取 proxy.ts 注入的 `x-request-id` * 2. 若请求头无此字段(如直接调用的内部函数),生成新 UUID * 3. 通过 `requestContextStorage.run()` 注入到 AsyncLocalStorage * 4. 在调用栈内的所有 logger 调用自动获得 requestId * * @example * ```ts * export const createUserAction = withRequestContext( * async (state: ActionState, input: CreateUserInput) => { * // 此处 logger.info 会自动带 requestId * return handleAction(...) * } * ) * ``` */ export function withRequestContext( fn: (...args: TArgs) => Promise ): (...args: TArgs) => Promise { return async (...args: TArgs) => { const headersList = await headers() const requestId = headersList.get("x-request-id") ?? randomUUID() const ctx: RequestContext = { requestId } return requestContextStorage.run(ctx, () => fn(...args)) } } ``` ### 4.4 `src/proxy.ts` 改造 ```ts import { NextResponse } from "next/server" import type { NextRequest } from "next/server" import { getToken } from "next-auth/jwt" // ... 原有 imports export async function proxy(request: NextRequest) { const { pathname } = request.nextUrl // 生成或复用 requestId(Web Crypto API,Edge 兼容) const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID() // 跳过静态资源和登录页 if ( pathname.startsWith("/_next") || pathname.startsWith("/api/auth") || pathname === "/login" || pathname === "/register" || pathname === "/favicon.ico" ) { return NextResponse.next({ request: { headers: injectRequestId(request, requestId) }, }) } // ... 原有 token / onboarding / 权限检查逻辑 // 所有 NextResponse.next() / NextResponse.redirect() 调用保留, // 但 NextResponse.next() 调用统一传入 request.headers const response = NextResponse.next({ request: { headers: injectRequestId(request, requestId) }, }) response.headers.set("x-request-id", requestId) return response } /** * 创建包含 x-request-id 的新 Headers 对象。 * 通过 NextResponse.next({ request: { headers } }) 注入到下游 RSC 请求。 */ function injectRequestId(request: NextRequest, requestId: string): Headers { const headers = new Headers(request.headers) headers.set("x-request-id", requestId) return headers } ``` > **说明**:proxy.ts 不导入 `request-context.ts`,避免在 Edge Runtime 中加载 `node:async_hooks` 导致构建错误。 ### 4.5 `src/shared/hooks/use-error-report.ts` ```ts "use client" import { useEffect } from "react" interface ClientErrorPayload { message: string stack?: string digest?: string url: string userAgent: string timestamp: string } /** * 客户端错误上报 Hook。 * * 用于 error.tsx 接收 error prop 后上报到 /api/client-error。 * * 节流策略: * - 同一 digest(或 message)在 sessionStorage 中标记,避免 React 重渲染或快速刷新时多次上报 * - 上报失败时静默忽略,避免无限循环 */ export function useErrorReport(error: Error & { digest?: string }): void { useEffect(() => { if (!error) return const digest = error.digest ?? error.message const storageKey = `error-reported:${digest}` if (sessionStorage.getItem(storageKey)) return sessionStorage.setItem(storageKey, "1") const payload: ClientErrorPayload = { message: error.message, stack: error.stack, digest: error.digest, url: window.location.href, userAgent: navigator.userAgent, timestamp: new Date().toISOString(), } fetch("/api/client-error", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), keepalive: true, // 即使页面卸载也尝试发送 }).catch(() => { // 上报失败时不再上报,避免无限循环 }) }, [error]) } ``` ### 4.6 `src/app/api/client-error/route.ts` ```ts import { NextResponse } from "next/server" import { createModuleLogger } from "@/shared/lib/logger" import { withRequestContext } from "@/shared/lib/with-request-context" const log = createModuleLogger("client-error") interface ClientErrorPayload { message: string stack?: string digest?: string url: string userAgent: string timestamp: string } /** * 接收客户端 error.tsx 上报的错误。 * * 注意:客户端错误的 requestId 与原始请求不同(来自 /api/client-error 的 HTTP 请求), * 但 digest 字段可用于关联原始错误。 */ export const POST = withRequestContext(async (request: Request) => { try { const body = (await request.json()) as ClientErrorPayload log.error( { clientMessage: body.message, stack: body.stack, digest: body.digest, url: body.url, userAgent: body.userAgent, clientTimestamp: body.timestamp, }, "Client error reported" ) return NextResponse.json({ ok: true }) } catch (error) { log.error({ err: error }, "Failed to parse client error payload") return NextResponse.json({ ok: false }, { status: 400 }) } }) ``` ### 4.7 `src/shared/lib/audit-logger.ts` 改造(静默失败 → 告警) ```ts import { createModuleLogger } from "@/shared/lib/logger" const log = createModuleLogger("audit-logger") export async function logAudit(params: LogAuditParams): Promise { try { // ... 原有写入逻辑 } catch (error) { // 旧:catch { /* Silently fail */ } // 新:记录到 logger,运维可感知 log.warn( { err: error, action: params.action, module: params.module }, "Audit log write failed" ) } } ``` ### 4.8 `src/shared/lib/action-utils.ts` 改造 ```ts import { createModuleLogger } from "@/shared/lib/logger" import type { ActionState } from "@/shared/types/action-state" import { PermissionDeniedError } from "@/shared/lib/errors" const log = createModuleLogger("action") export function handleActionError(e: unknown): ActionState { if (e instanceof PermissionDeniedError) { return { success: false, message: e.message } } if (e instanceof BusinessError) { return { success: false, message: e.message, errorCode: e.code } } if (e instanceof Error) { // 旧:console.error("[ActionError]", e.name, e.message, e.stack) log.error({ err: e }, "Action failed") return { success: false, message: "操作失败,请稍后重试", errorCode: "unexpected" } } log.error({ err: e }, "Unknown action error") return { success: false, message: "操作失败,请稍后重试", errorCode: "unexpected" } } export async function safeActionCall( action: () => Promise>, options?: { onError?: (error: unknown) => void onFinally?: () => void } ): Promise | null> { try { return await action() } catch (e) { options?.onError?.(e) // 旧:console.error("[SafeActionCall]", e) log.error({ err: e }, "Safe action call threw") return null } finally { options?.onFinally?.() } } ``` ### 4.9 `src/shared/lib/track-event.ts` 改造 ```ts import { createModuleLogger } from "@/shared/lib/logger" const log = createModuleLogger("track") /** * 业务埋点接口。 * * 不再是 no-op stub,通过 logger.info 输出结构化事件, * 后续可扩展为接入外部 analytics 服务。 */ export function trackEvent( name: string, props?: Record ): void { log.info({ event: name, ...props }, "track event") } export function trackExamEvent( name: string, props?: Record ): void { trackEvent(`exam.${name}`, props) } export function trackAuthEvent( name: string, props?: Record ): void { trackEvent(`auth.${name}`, props) } ``` ### 4.10 `src/env.mjs` 改造 ```ts // 在 server schema 中添加: LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), ``` ### 4.11 ESLint 规则 ```js // eslint.config.mjs { rules: { "no-console": ["error", { allow: [], allowWithImplicit: false }] }, // logger.ts 豁免(pino 内部需用 console) overrides: [ { files: ["src/shared/lib/logger.ts"], rules: { "no-console": "off" } } ] } ``` --- ## 5. 改造范围与策略 ### 5.1 console.* 替换映射 | 类型 | 数量 | 替换为 | 调用方式 | |------|------|--------|----------| | `console.log` | 2 | `log.debug` | `const log = createModuleLogger("questions")` | | `console.error` | 67 | `log.error` | 按文件所属模块创建子 logger | | `console.warn` | 5 | `log.warn` | 同上 | | `console.info` | 10 | `log.info` | 同上 | | `console.debug` | 4 | `log.debug` | 同上 | ### 5.2 模块前缀规范化 当前散乱前缀 → 统一通过 `module` 字段: | 当前前缀 | 新 module 字段 | |---------|----------------| | `[ExamAction]` | `exams` | | `[ActionError]` | `action` | | `[ApiError]` | `api` | | `[AuditLogger]` | `audit-logger` | | `[SafeActionCall]` | `action` | | `[Files]` / `[files]` | `files` | | 无前缀 | 按文件所属模块 | ### 5.3 Server Action 包装策略 由于 Server Action 通过 `"use server"` 自动成为 RPC,不能简单用 `withRequestContext` 包装导出函数(会丢失 Next.js 类型推断)。且 `handleActionError` 是同步函数,无法 `await headers()` 读取请求头。 **最终策略**:在每个 Server Action 模块的入口点调用 `withRequestContext` 包装: ```ts // src/modules/audit/actions.ts "use server" import { withRequestContext } from "@/shared/lib/with-request-context" import { createModuleLogger } from "@/shared/lib/logger" const log = createModuleLogger("audit") export const createAuditLogAction = withRequestContext( async (state: ActionState, input: CreateAuditInput) => { // 此处 logger 自动带 requestId log.info({ input }, "Creating audit log") return handleAction(...) } ) ``` **data-access 层无需显式包装**:因为 data-access 总是从 Server Action 调用,AsyncLocalStorage 上下文会自动贯穿到调用栈下游。data-access 中的 logger 调用会自动获得 requestId。 **实施时验证项**: - Next.js 16 是否允许高阶函数包装 Server Action(保留 NextServerAction 标记) - 若不允许,回退方案:在每个 Server Action 函数体首行调用 `await initRequestContext()`,该函数内部读取 headers 并写入 AsyncLocalStorage ### 5.4 error.tsx 改造模式 130 个 error.tsx 统一改为: ```tsx "use client" import { useEffect } from "react" import { useErrorReport } from "@/shared/hooks/use-error-report" // ... 原有 imports export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useErrorReport(error) // ... 原有 UI 渲染 } ``` 每个 error.tsx 仅增加 2 行(import + 调用 Hook)。 --- ## 6. 测试策略 ### 6.1 单元测试 | 测试文件 | 覆盖内容 | |---------|---------| | `__tests__/logger.test.ts` | level 控制、JSON 输出格式、mixin 注入 requestId、createModuleLogger | | `__tests__/request-context.test.ts` | AsyncLocalStorage 读写、嵌套调用、空上下文 | | `__tests__/use-error-report.test.tsx` | 节流(sessionStorage 标记)、fetch 调用、错误处理 | ### 6.2 集成测试 - 启动开发服务器,发起请求,验证日志包含 requestId - 触发 Server Action 错误,验证日志包含 module + requestId + error stack - 触发 client error.tsx,验证 `/api/client-error` 收到请求并记录日志 ### 6.3 回归验证 - `npx tsc --noEmit` 零错误 - `npm run lint` 零错误(含新增 `no-console` 规则) - 现有 vitest 测试套件全部通过 --- ## 7. 风险与权衡 | 风险 | 影响 | 缓解 | |------|------|------| | pino 在 Next.js bundling 中可能有问题 | 高 | 已配置 `serverExternalPackages`,pino 仅服务端导入;开发期 `pino-pretty` 通过 transport 配置启用 | | `AsyncLocalStorage` 在 Server Action 中可能不工作 | 中 | Server Action 运行在 Node.js runtime,完全支持;实施时先做最小验证 | | Server Action 高阶函数包装可能丢失 Next.js 类型 | 中 | 实施时验证,必要时回退为显式 `headers()` 调用 | | 88 处 console.* 替换可能遗漏 | 低 | 通过 ESLint `no-console` 规则强制,仅允许 `logger.ts` 中使用 | | 130 个 error.tsx 改造量大 | 中 | 提取公共 `useErrorReport` Hook,每个 error.tsx 仅加 2 行 | | track-event 合并可能破坏调用方 | 低 | 删除前 grep 所有引用,统一改为从 `@/shared/lib/track-event` 导入 | | 客户端错误风暴(无限循环上报) | 中 | sessionStorage 节流 + fetch 失败静默 + `keepalive` 选项 | | proxy.ts 改造可能影响 Edge Runtime 构建 | 中 | 不导入任何 Node.js 模块,仅用 Web Crypto API | --- ## 8. 架构图同步(强制) 按项目规则"改码必同步图",重构后必须更新: ### 8.1 `docs/architecture/004_architecture_impact_map.md` 新增章节: - `shared/lib/logger.ts` — pino 实例 + createModuleLogger - `shared/lib/request-context.ts` — AsyncLocalStorage 请求上下文 - `shared/lib/with-request-context.ts` — Server Action / Route Handler 入口包装 - `shared/hooks/use-error-report.ts` — 客户端错误上报 Hook - `app/api/client-error/route.ts` — 客户端错误接收端点 修改章节: - `proxy.ts` — 增加 requestId 注入逻辑 - `shared/lib/audit-logger.ts` / `change-logger.ts` / `login-logger.ts` — 静默失败改为 logger.warn - `shared/lib/track-event.ts` — 实现层从 no-op 改为 logger.info - `shared/lib/action-utils.ts` / `api-response.ts` — 接入 logger 删除记录: - `modules/rbac/lib/track.ts` - `modules/course-plans/lib/track-event.ts` - `modules/questions/utils/track-event.ts` ### 8.2 `docs/architecture/005_architecture_data.json` - `modules.shared.lib.exports` 新增 `logger` / `createModuleLogger` / `requestContextStorage` / `getRequestContext` / `withRequestContext` - `modules.shared.hooks.exports` 新增 `useErrorReport` - `modules.app.api.client-error.exports` 新增 `POST` - `dependencyMatrix` 更新:proxy.ts → request-context(仅注入请求头,不导入) - 删除 `modules.rbac.lib.track`、`modules.course-plans.lib.track-event`、`modules.questions.utils.track-event` ### 8.3 `docs/troubleshooting/known-issues.md` 新增规则条目: - pino 集成:`serverExternalPackages` 配置 + `pino-pretty` 仅 dev - Edge Runtime 限制:proxy.ts 不能导入 `node:async_hooks` - Server Action 包装:`withRequestContext` 高阶函数使用模式 - error.tsx 错误上报:`useErrorReport` Hook + 节流策略 - ESLint `no-console` 规则与豁免 --- ## 9. 实施顺序建议 1. **Phase 1:基础底座** - 装 pino / pino-pretty - 创建 `logger.ts` / `request-context.ts` / `with-request-context.ts` - 在 `env.mjs` 添加 LOG_LEVEL - 单元测试 2. **Phase 2:核心接入** - proxy.ts 注入 requestId - `action-utils.ts` / `api-response.ts` 接入 logger - 三个 audit-logger 静默失败 → logger.warn - track-event 去重合并 3. **Phase 3:批量替换** - 88 处 console.* 替换为 logger.* - 前缀规范化 - ESLint `no-console` 规则启用 4. **Phase 4:error.tsx 上报** - 创建 `use-error-report` Hook - 创建 `/api/client-error` Route Handler - 130 个 error.tsx 接入 5. **Phase 5:架构同步** - 更新 004 / 005 / known-issues.md - 验证 tsc / lint / 测试通过