Explorar el Código

fix: 修复 RPC 客户端类型安全并增强 ResponsePolyfill 接口

主要更改:
- rpc-client.ts: 移除 as any 绕过,使用正确的 Hono 泛型类型约束
  - 添加 fetch 函数的完整类型签名 (RequestInfo | URL, RequestInit)
  - 使用 T 泛型约束确保类型安全
  - 修复 error instanceof Error 检查

- response-polyfill.ts: 增强标准 Response 接口兼容性
  - 添加 redirected, type, url 标准属性
  - 实现 bytes(), blob(), formData() 方法
  - 修复 clone() 返回类型为 this
  - 修复 ESLint 错误:使用 unknown 替代 any

- enterpriseDisabilityClient.ts: 修复路由类型导入
  - 使用 typeof personExtensionRoutes 替代类型导入
  - 解决 Hono/client 对路由类型的要求

- sprint-status.yaml: 更新 Epic 状态
  - Epic 4-7 标记为已跳过
  - 添加详细的跳过原因说明

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname hace 1 día
padre
commit
3d14e938a3

+ 52 - 27
_bmad-output/implementation-artifacts/sprint-status.yaml

@@ -75,35 +75,60 @@ development_status:
   epic-3-retrospective: done              # 回顾完成于 2026-01-11
 
   # Epic 4: 表单工具开发与验证
-  # 模式: 工具开发 → 真实 E2E 测试验证 → 稳定性验证
-  epic-4: in-progress
-  4-1-form-helper-tool: ready-for-dev          # 开发表单辅助工具函数
-  4-2-form-unit-tests: backlog           # 编写表单工具的单元测试
-  4-3-form-e2e-integration: backlog      # 在 web/tests/e2e 中验证表单工具
-  4-4-form-stability-test: backlog       # 表单稳定性验证
-  epic-4-retrospective: optional
+  # ⏭️ 已跳过 - 2026-01-16
+  # 跳过原因:
+  # - 现有工具(Select、文件上传)+ Playwright API + Page Object 模式已完全满足表单测试需求
+  # - Playwright MCP 提供了手动测试表单的能力
+  # - Story 10.12 评估结论:表单工具是"可选优化",非必需
+  # - 多个业务 Epic(8、9、10、11-13)已证明当前方案可行
+  # - scrollToSection() 已在 Page Object 中实现
+  epic-4: done
+  4-1-form-helper-tool: n/a          # 开发表单辅助工具函数 - 已跳过
+  4-2-form-unit-tests: n/a           # 编写表单工具的单元测试 - 已跳过
+  4-3-form-e2e-integration: n/a      # 在 web/tests/e2e 中验证表单工具 - 已跳过
+  4-4-form-stability-test: n/a       # 表单稳定性验证 - 已跳过
+  epic-4-retrospective: n/a           # Epic 已跳过,无需回顾
 
   # Epic 5: 列表和对话框工具开发与验证
-  # 模式: 工具开发 → 真实 E2E 测试验证 → 稳定性验证
-  epic-5: backlog
-  5-1-dynamic-list-dialog-tool: backlog  # 开发动态列表和对话框工具函数
-  5-2-list-dialog-unit-tests: backlog   # 编写列表和对话框的单元测试
-  5-3-list-dialog-e2e-integration: backlog  # 在 web/tests/e2e 中验证
-  5-4-list-dialog-stability-test: backlog   # 稳定性验证
-  epic-5-retrospective: optional
+  # ⏭️ 已跳过 - 2026-01-16
+  # 跳过原因:
+  # - 现有 Page Object 模式已完全实现列表操作(addBankCard, deleteBankCard, addVisit, deleteVisit, addNote, deleteNote 等)
+  # - 对话框操作已在多个 Page Object 中实现(waitForDialogClosed, cancelDialog, confirmDelete 等)
+  # - Playwright MCP 提供了手动测试能力
+  # - Epic 8-13(6 个业务 Epic)已证明当前方案可行
+  # - Story 10.12 评估模式:工具扩展需求少于 3 个,表单工具被评估为"可选优化"
+  # - 列表和对话框工具比表单工具更特定于业务场景,通用性更低
+  epic-5: done
+  5-1-dynamic-list-dialog-tool: n/a          # 开发动态列表和对话框工具函数 - 已跳过
+  5-2-list-dialog-unit-tests: n/a           # 编写列表和对话框的单元测试 - 已跳过
+  5-3-list-dialog-e2e-integration: n/a      # 在 web/tests/e2e 中验证 - 已跳过
+  5-4-list-dialog-stability-test: n/a       # 稳定性验证 - 已跳过
+  epic-5-retrospective: n/a                 # Epic 已跳过,无需回顾
 
   # Epic 6: 完整验证(残疾人管理)
-  # 状态: backlog - 等待所有工具开发和验证完成
-  epic-6: backlog
-  6-1-complete-flow-test: backlog        # 完整流程测试
-  6-2-stability-test: backlog            # 稳定性测试
-  epic-6-retrospective: optional
+  # ⏭️ 已跳过 - 2026-01-16
+  # 跳过原因:
+  # - Epic 9(残疾人管理完整 E2E 测试覆盖)已完成所有验证目标
+  # - Epic 9 包含:照片上传(9.1)、银行卡(9.2)、备注(9.3)、回访(9.4)、CRUD(9.5)、并行隔离(9.6)、稳定性验证(9.7)
+  # - Epic 9 回顾完成于 2026-01-12,通过率从 77.4% 提升到 90.3%
+  # - 业务 Epic 8-13 已充分证明工具包可靠性
+  epic-6: done
+  6-1-complete-flow-test: n/a        # 完整流程测试 - 已合并到 Epic 9
+  6-2-stability-test: n/a            # 稳定性测试 - 已合并到 Epic 9
+  epic-6-retrospective: n/a           # Epic 已跳过,无需回顾
 
   # Epic 7: 完善文档与开发者体验
-  epic-7: backlog
-  7-1-improve-readme-docs: backlog       # 完善 README、API 文档和示例
-  7-2-vscode-snippets: backlog           # VS Code Snippets 和开发体验
-  epic-7-retrospective: optional
+  # ⏭️ 已跳过 - 2026-01-16
+  # 跳过原因:
+  # - README.md 已完善(568 行),包含安装、快速入门、完整 API 文档、使用示例
+  # - DEVELOPER_CHECKLIST.md 已存在(开发者自查清单)
+  # - 业务 Epic 8-13 已证明当前文档足够支持开发
+  # - VS Code Snippets 非必需(工具扩展需求少于 3 个)
+  # - JSDoc 已提供完整的 IDE 类型提示支持
+  epic-7: done
+  7-1-improve-readme-docs: n/a       # 当前文档已满足需求 - 已跳过
+  7-2-vscode-snippets: n/a           # 非必需,可后续添加 - 已跳过
+  epic-7-retrospective: n/a           # Epic 已跳过,无需回顾
 
   # Epic 8: 区域管理 E2E 测试 (Epic B - 业务测试 Epic)
   # 目标: 测试开发者可以为区域管理功能编写完整的 E2E 测试
@@ -249,10 +274,10 @@ development_status:
 #   - Epic 11: 基础配置管理测试 (9/9 Stories done)
 #
 # Epic G: e2e-test-utils 包维护 🌟 支持性任务
-#   - Epic 4: 表单工具开发与验证 🔄 进行中
-#   - Epic 5: 列表和对话框工具开发与验证
-#   - Epic 6: 完整验证(已合并到 Epic 9)
-#   - Epic 7: 文档与开发者体验
+#   - Epic 4: 表单工具开发与验证 ⏭️ 已跳过 (2026-01-16)
+#   - Epic 5: 列表和对话框工具开发与验证 ⏭️ 已跳过 (2026-01-16)
+#   - Epic 6: 完整验证 ⏭️ 已跳过 (2026-01-16) - 已合并到 Epic 9
+#   - Epic 7: 文档与开发者体验 ⏭️ 已跳过 (2026-01-16)
 #
 # Epic 依赖关系:
 # =========================

+ 49 - 8
mini-ui-packages/mini-shared-ui-components/src/utils/rpc/response-polyfill.ts

@@ -3,15 +3,28 @@ if (typeof globalThis.Headers === 'undefined') {
   // 这里会由headers-polyfill.js注册
 }
 
+// Response 类型枚举
+type ResponseType = 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect'
+
 export class ResponsePolyfill {
+  // 标准 Response 接口的额外属性(小程序环境简化实现)
+  readonly redirected: boolean = false
+  readonly type: ResponseType = 'basic'
+  readonly url: string = ''
+
   constructor(
     public body: string | ArrayBuffer | null,
     public init: {
       status?: number
       statusText?: string
       headers?: Record<string, string>
+      url?: string
     } = {}
-  ) {}
+  ) {
+    if (init.url) {
+      this.url = init.url
+    }
+  }
 
   get ok(): boolean {
     return this.status >= 200 && this.status < 300
@@ -33,6 +46,19 @@ export class ResponsePolyfill {
     return false // 小程序环境简单实现
   }
 
+  // bytes() 是一个返回 Promise<Uint8Array> 的方法(较新的 Web API)
+  async bytes(): Promise<Uint8Array> {
+    if (this.body instanceof ArrayBuffer) {
+      return new Uint8Array(this.body)
+    }
+    if (typeof this.body === 'string') {
+      // 将字符串转为 Uint8Array
+      const encoder = new TextEncoder()
+      return encoder.encode(this.body)
+    }
+    throw new Error('No body available')
+  }
+
   async arrayBuffer(): Promise<ArrayBuffer> {
     if (this.body instanceof ArrayBuffer) {
       return this.body
@@ -40,6 +66,16 @@ export class ResponsePolyfill {
     throw new Error('Not implemented')
   }
 
+  async blob(): Promise<Blob> {
+    // 小程序环境不支持 Blob,抛出错误
+    throw new Error('Blob is not supported in mini program environment')
+  }
+
+  async formData(): Promise<FormData> {
+    // 小程序环境不支持 FormData,抛出错误
+    throw new Error('FormData is not supported in mini program environment')
+  }
+
   async text(): Promise<string> {
     if (typeof this.body === 'string') {
       return this.body
@@ -47,29 +83,33 @@ export class ResponsePolyfill {
     throw new Error('Not implemented')
   }
 
-  async json<T = any>(): Promise<T> {
+  async json<T = unknown>(): Promise<T> {
     if (typeof this.body === 'string') {
       try {
         return JSON.parse(this.body)
-      } catch (e) {
+      } catch {
         throw new Error('Invalid JSON')
       }
     }
     throw new Error('Not implemented')
   }
 
-  clone(): ResponsePolyfill {
-    return new ResponsePolyfill(this.body, { ...this.init })
+  clone(): this {
+    return new ResponsePolyfill(this.body, { ...this.init }) as this
   }
 
-  static json(data: any, init?: ResponseInit): ResponsePolyfill {
+  static json(data: unknown, init?: ResponseInit): ResponsePolyfill {
     const headers = new Headers(init && 'headers' in init ? init.headers : undefined)
     if (!headers.has('Content-Type')) {
       headers.set('Content-Type', 'application/json')
     }
+    // @ts-expect-error - Headers.raw() 是非标准 API,但在小程序环境中需要使用
+    const headersRecord = typeof (headers as { raw?: () => Record<string, string> }).raw === 'function'
+      ? (headers as { raw: () => Record<string, string> }).raw()
+      : {}
     return new ResponsePolyfill(JSON.stringify(data), {
       ...init,
-      headers: (headers as any).raw ? (headers as any).raw() : {}
+      headers: headersRecord
     })
   }
 
@@ -88,6 +128,7 @@ export class ResponsePolyfill {
 // 可选的全局注册函数
 export function registerGlobalResponsePolyfill(): void {
   if (typeof globalThis.Response === 'undefined') {
-    globalThis.Response = ResponsePolyfill as any
+    // @ts-expect-error - 全局类型赋值
+    globalThis.Response = ResponsePolyfill
   }
 }

+ 15 - 15
mini-ui-packages/mini-shared-ui-components/src/utils/rpc/rpc-client.ts

@@ -1,5 +1,6 @@
 import Taro from '@tarojs/taro'
 import { hc } from 'hono/client'
+import type { Hono } from 'hono'
 import { ResponsePolyfill } from './response-polyfill'
 import './headers-polyfill'
 
@@ -88,11 +89,11 @@ const API_BASE_URL = process.env.TARO_APP_API_BASE_URL || 'http://localhost:3000
 // const BASE_URL = `${API_BASE_URL}/api/${API_VERSION}`
 
 // 创建自定义fetch函数,适配Taro.request,支持token自动刷新
-const taroFetch: any = async (input, init) => {
-  const url = typeof input === 'string' ? input : input.url
-  const method = init.method || 'GET'
+const taroFetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
+  const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
+  const method = init?.method || 'GET'
 
-  const requestHeaders: Record<string, string> = init.headers;
+  const requestHeaders: Record<string, string> = { ...(init?.headers as Record<string, string> | undefined) ?? {} }
 
   const keyOfContentType = Object.keys(requestHeaders).find(item => item.toLowerCase() === 'content-type')
   if (!keyOfContentType) {
@@ -102,8 +103,8 @@ const taroFetch: any = async (input, init) => {
   // 构建Taro请求选项
   const options: Taro.request.Option = {
     url,
-    method: method as any,
-    data: init.body,
+    method: method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS',
+    data: init?.body,
     header: requestHeaders
   }
 
@@ -127,13 +128,13 @@ const taroFetch: any = async (input, init) => {
   }
 
   // 发送请求
-  const sendRequest = async (): Promise<any> => {
+  const sendRequest = async (): Promise<Response> => {
     try {
       console.log('API请求:', options.url)
       const response = await Taro.request(options)
       console.log('API响应', response)
 
-      const responseHeaders = response.header;
+      const responseHeaders = response.header as Record<string, string>;
       const contentType = getHeaderValue(responseHeaders, 'content-type')
 
       // 处理204 No Content响应,不设置body
@@ -150,7 +151,7 @@ const taroFetch: any = async (input, init) => {
           statusText: response.errMsg || 'OK',
           headers: responseHeaders
         }
-      )
+      ) as unknown as Response
     } catch (error) {
       console.error('API Error:', error)
       throw error
@@ -183,8 +184,9 @@ const taroFetch: any = async (input, init) => {
     return response
   } catch (error) {
     console.error('API请求失败:', error)
+    const errorMessage = error instanceof Error ? error.message : '网络错误'
     Taro.showToast({
-      title: error.message || '网络错误',
+      title: errorMessage,
       icon: 'none'
     })
     throw error
@@ -192,10 +194,8 @@ const taroFetch: any = async (input, init) => {
 }
 
 // 创建Hono RPC客户端
-export const rpcClient = <T = any>(apiBasePath?: string) => {
-  // @ts-expect-error - hc 函数类型约束与我们的使用方式不匹配
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  return (hc as any)<T>(`${API_BASE_URL}${apiBasePath}`, {
-    fetch: taroFetch as any
+export const rpcClient = <T extends Hono<unknown, unknown, unknown>>(apiBasePath?: string): ReturnType<typeof hc<T>> => {
+  return hc<T>(`${API_BASE_URL}${apiBasePath}`, {
+    fetch: taroFetch
   })
 }

+ 2 - 2
mini-ui-packages/yongren-talent-management-ui/src/api/enterpriseDisabilityClient.ts

@@ -1,4 +1,4 @@
-import type { PersonExtensionRoutes } from '@d8d/allin-disability-module/routes';
+import { personExtensionRoutes } from '@d8d/allin-disability-module/routes';
 import { rpcClient } from '@d8d/mini-shared-ui-components/utils/rpc/rpc-client';
 
-export const enterpriseDisabilityClient = rpcClient<PersonExtensionRoutes>('/api/v1/yongren/disability-person'); 
+export const enterpriseDisabilityClient = rpcClient<typeof personExtensionRoutes>('/api/v1/yongren/disability-person'); 

+ 3 - 0
pnpm-lock.yaml

@@ -1946,6 +1946,9 @@ importers:
       '@d8d/rencai-shared-ui':
         specifier: workspace:*
         version: link:../rencai-shared-ui
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../../packages/shared-types
       '@tanstack/react-query':
         specifier: ^5.90.12
         version: 5.90.12(react@18.3.1)