Files

14 KiB
Raw Permalink Blame History

1. 架构设计

flowchart TB
    subgraph Frontend["前端 (React + Vite)"]
        UI["shadcn/ui 组件层"]
        Pages["页面层"]
        Hooks["Hooks 层"]
        Store["Zustand 状态管理"]
        SDK["array-sdk 通信层"]
    end

    subgraph Backend["后端 (FastAPI + SQLite)"]
        API["REST API"]
        WS["WebSocket"]
    end

    UI --> Pages
    Pages --> Hooks
    Hooks --> Store
    Hooks --> SDK
    Store --> SDK
    SDK --> API
    SDK --> WS

2. 技术说明

  • 前端框架:React 18 + TypeScript + Vite 6
  • UI 组件库:shadcn/ui(Radix UI + Tailwind CSS)
  • 状态管理:Zustand(轻量、无 boilerplate、支持中间件)
  • 样式方案:Tailwind CSS 4 + CSS 变量主题系统
  • 图标:Lucide React
  • 动画:Framer Motion
  • Markdown 渲染:react-markdown + remark-gfm + rehype-highlight
  • HTTP/WebSocket:array-sdk(从旧项目迁移,纯 JS 无框架依赖)
  • 路由:React Router DOM v7(Hash 模式,兼容静态部署)
  • 国际化:自定义 i18n hook(中/英双语)
  • PWA:vite-plugin-pwa
  • 项目初始化:vite-init react-ts 模板

3. 路由定义

路由 用途
/ 登录页(未登录时)或主工作台(已登录时)
/task/:taskId 主工作台,自动选中指定任务

4. API 定义

本项目为纯前端,所有 API 调用通过 array-sdk 与后端通信。SDK 封装层提供以下接口:

4.1 认证

interface AuthAPI {
  login(serverUrl: string, apiKey: string): Promise<Worker>
  loginByPassword(serverUrl: string, username: string, password: string): Promise<Worker>
  register(serverUrl: string, name: string, username: string, password: string, profile?: RegisterProfile): Promise<Worker>
  autoLogin(): Promise<Worker | null>
  logout(): void
}

4.2 Worker

interface WorkerAPI {
  getWorkers(params?: { orgId?: string }): Promise<Worker[]>
  getWorkerProfile(workerId: string): Promise<Worker>
  updateProfile(data: Partial<WorkerProfile>): Promise<Worker>
  getWorkerWorkload(workerId: string): Promise<Workload>
  discoverWorkers(options: DiscoverOptions): Promise<DiscoverResult>
}

4.3 Task

interface TaskAPI {
  getTasks(params?: { orgId?: string; categoryId?: string }): Promise<Task[]>
  getTask(taskId: string): Promise<Task>
  createTask(title: string, participantIds: string[], priority?: string, extra?: CreateTaskExtra): Promise<Task>
  updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>
  updateTaskTitle(taskId: string, title: string): Promise<Task>
  addParticipant(taskId: string, workerId: string): Promise<Participant[]>
  removeParticipant(taskId: string, workerId: string): Promise<void>
}

4.4 Message

interface MessageAPI {
  getMessages(taskId: string, scope?: string, includePrivate?: boolean, limit?: number, page?: number): Promise<Message[]>
  sendMessage(taskId: string, content: string, options?: SendMessageOptions): Promise<Message>
  sendMessageWithAttachments(taskId: string, content: string, fileIds: string[], options?: SendMessageOptions): Promise<Message>
}

4.5 Attachment

interface AttachmentAPI {
  uploadAttachment(taskId: string, file: File): Promise<Attachment>
  listAttachments(taskId: string): Promise<{ attachments: Attachment[]; total: number }>
  deleteAttachment(taskId: string, attachmentId: string): Promise<void>
  getAttachmentUrl(taskId: string, attachmentId: string): string
}

4.6 Organization

interface OrganizationAPI {
  listMyOrganizations(): Promise<Organization[]>
  getOrganization(orgId: string): Promise<Organization>
  createOrganization(name: string, description?: string): Promise<Organization>
  updateOrganization(orgId: string, data: Partial<Organization>): Promise<Organization>
  createInvite(orgId: string, options?: InviteOptions): Promise<OrgInvite>
  listInvites(orgId: string): Promise<OrgInvite[]>
  joinOrganization(inviteCode: string): Promise<JoinResult>
  listOrgMembers(orgId: string): Promise<OrgMember[]>
  removeOrgMember(orgId: string, workerId: string): Promise<void>
  updateMemberRole(orgId: string, workerId: string, role: 'admin' | 'member'): Promise<OrgMember>
  renameOrgMember(orgId: string, workerId: string, name: string): Promise<OrgMember>
}

4.7 Category

interface CategoryAPI {
  createCategory(name: string, orgId?: string): Promise<Category>
  listCategories(orgId?: string): Promise<Category[]>
  updateCategory(categoryId: string, opts: Partial<Category>): Promise<Category>
  deleteCategory(categoryId: string): Promise<boolean>
}

4.8 Review

interface ReviewAPI {
  createReview(workerId: string, params: CreateReviewParams): Promise<Review>
  getWorkerReviews(workerId: string, options?: PaginationOptions): Promise<ReviewListResult>
  getReviewSummary(workerId: string): Promise<ReviewSummary>
}

4.9 Notification

interface NotificationAPI {
  getNotifications(isRead?: boolean): Promise<Notification[]>
  markNotificationRead(notificationId: string): Promise<void>
  markAllNotificationsRead(): Promise<void>
}

4.10 WebSocket

interface WebSocketAPI {
  startWebSocket(callbacks: WSCallbacks): () => void
  stopWebSocket(): void
}

interface WSCallbacks {
  onEvent: (event: WSEvent) => void
  onConnected: () => void
  onDisconnected: () => void
  onError: (err: Error) => void
}

type WSEventType = 'task.message' | 'participant.joined' | 'task.updated' | 'worker_status_changed' | 'tasks.pending_recovery'

5. 数据模型

erDiagram
    Organization ||--o{ Category : "has"
    Organization ||--o{ OrgMember : "has"
    Organization ||--o{ OrgInvite : "has"
    Worker ||--o{ Task : "creates"
    Worker ||--o{ Review : "receives"
    Task ||--o{ Participant : "has"
    Task ||--o{ Message : "has"
    Task ||--o{ Attachment : "has"
    Task }o--|| Category : "belongs_to"
    Message }o--o| Message : "reply_to"
    Message }o--o| Worker : "private_to"

    Worker {
        string id PK
        string name
        string type
        string status
        string organization
        string location
        string description
        string[] tags
        object capabilities
        number maxConcurrentTasks
    }

    Task {
        string id PK
        string title
        string description
        string status
        string priority
        string creator_id FK
        string org_id FK
        string category_id FK
        datetime created_at
        datetime last_message_at
    }

    Message {
        string id PK
        string task_id FK
        string sender_id FK
        string content
        string type
        string reply_to_id FK
        string private_to_id FK
        datetime created_at
    }

    Attachment {
        string id PK
        string task_id FK
        string filename
        string content_type
        number size
        string url
        string thumbnail_url
        number width
        number height
        number duration
    }

    Review {
        string id PK
        string worker_id FK
        string task_id FK
        string reviewer_id FK
        number rating
        string[] capability_tags
        string comment
        string suggestion
    }

6. 项目目录结构

human-adapter-react/
├── src/
│   ├── components/          # UI 组件
│   │   ├── ui/              # shadcn/ui 基础组件
│   │   ├── layout/          # 布局组件
│   │   │   ├── TopBar.tsx
│   │   │   ├── WorkersPanel.tsx
│   │   │   ├── TaskList.tsx
│   │   │   └── Resizer.tsx
│   │   ├── chat/            # 聊天相关组件
│   │   │   ├── ChatDetail.tsx
│   │   │   ├── MessageList.tsx
│   │   │   ├── MessageBubble.tsx
│   │   │   ├── MessageInput.tsx
│   │   │   ├── MentionPopup.tsx
│   │   │   ├── ReplyPreview.tsx
│   │   │   ├── AttachmentRenderer.tsx
│   │   │   ├── DMTab.tsx
│   │   │   └── ScrollToBottom.tsx
│   │   ├── task/            # 任务相关组件
│   │   │   ├── TaskCard.tsx
│   │   │   ├── TaskComposer.tsx
│   │   │   ├── TaskDrawer.tsx
│   │   │   ├── StatusSelector.tsx
│   │   │   └── ParticipantManager.tsx
│   │   ├── org/             # 组织相关组件
│   │   │   ├── OrgSwitcher.tsx
│   │   │   ├── OrgDetailSheet.tsx
│   │   │   └── InviteManager.tsx
│   │   ├── profile/         # 个人中心组件
│   │   │   ├── ProfileDialog.tsx
│   │   │   └── ShortcutSettings.tsx
│   │   ├── review/          # 评价组件
│   │   │   ├── ReviewDialog.tsx
│   │   │   └── ReviewSummary.tsx
│   │   ├── auth/            # 登录组件
│   │   │   └── LoginPage.tsx
│   │   └── shared/          # 共享组件
│   │       ├── LedLogo.tsx
│   │       ├── LedMatrix.tsx
│   │       ├── RobotIcon.tsx
│   │       ├── WorkerAvatar.tsx
│   │       ├── Toast.tsx
│   │       └── PwaInstallBanner.tsx
│   ├── hooks/               # 自定义 Hooks
│   │   ├── useAuth.ts
│   │   ├── useMessages.ts
│   │   ├── useWorkers.ts
│   │   ├── useTasks.ts
│   │   ├── useOrganization.ts
│   │   ├── useNotifications.ts
│   │   ├── useReviews.ts
│   │   ├── useProfile.ts
│   │   ├── useWebSocket.ts
│   │   ├── useI18n.ts
│   │   ├── useTheme.ts
│   │   ├── useAutoScroll.ts
│   │   └── useDrafts.ts
│   ├── stores/              # Zustand Store
│   │   ├── authStore.ts
│   │   ├── appStore.ts
│   │   └── uiStore.ts
│   ├── lib/                 # 工具库
│   │   ├── sdk.ts           # array-sdk 封装
│   │   ├── api.ts           # API 薄封装层
│   │   ├── i18n.ts          # 国际化字典
│   │   ├── utils.ts         # 通用工具函数
│   │   └── constants.ts     # 常量定义
│   ├── pages/               # 页面
│   │   ├── LoginPage.tsx
│   │   └── WorkbenchPage.tsx
│   ├── App.tsx
│   ├── main.tsx
│   └── index.css            # Tailwind 入口 + CSS 变量
├── public/
├── sdk-js/                  # array-sdk 源码(从旧项目迁移)
│   ├── src/
│   │   ├── client.ts
│   │   ├── adapter.ts
│   │   └── index.ts
│   └── package.json
├── .trae/documents/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── tailwind.config.js
├── components.json          # shadcn/ui 配置
└── postcss.config.js

7. 关键架构决策

7.1 状态管理:Zustand vs Context

选择 Zustand 的理由:

  • 无 Provider 包裹,任何组件直接 useStore() 即可
  • 支持 subscribeWithSelector 中间件,精确控制重渲染
  • 支持 persist 中间件,自动持久化到 localStorage
  • 比 Redux 少 90% boilerplate,比 Jotai 更适合复杂状态
// appStore.ts 示例
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface AppState {
  workers: Worker[]
  tasks: Task[]
  selectedTaskId: string | null
  wsConnected: boolean
  setWorkers: (workers: Worker[]) => void
  setTasks: (tasks: Task[]) => void
  selectTask: (id: string | null) => void
}

export const useAppStore = create<AppState>()(
  persist(
    (set) => ({
      workers: [],
      tasks: [],
      selectedTaskId: null,
      wsConnected: false,
      setWorkers: (workers) => set({ workers }),
      setTasks: (tasks) => set({ tasks }),
      selectTask: (id) => set({ selectedTaskId: id }),
    }),
    { name: 'array-app-store', partialize: (s) => ({ selectedTaskId: s.selectedTaskId }) }
  )
)

7.2 消息系统:自定义 Hook

不使用 Vercel AI SDK 的 useChat(),因为:

  • 本项目的消息模型更复杂(私密消息、@提及、引用回复、附件)
  • WebSocket 事件格式是后端自定义的
  • 需要精确控制乐观消息的生命周期

自定义 useMessages Hook 借鉴 AI SDK 的状态机思想:

type MessageStatus = 'idle' | 'sending' | 'streaming' | 'error'

interface UseMessagesReturn {
  messages: Message[]
  pendingMessages: PendingMessage[]
  isLoading: boolean
  isAtBottom: boolean
  sendMessage: (content: string, options?: SendOptions) => Promise<void>
  retryMessage: (pendingId: string) => Promise<void>
  cancelMessage: (pendingId: string) => void
  loadMore: () => Promise<void>
  hasMore: boolean
  scrollToBottom: () => void
}

7.3 主题系统:Tailwind CSS 变量

:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --primary: 221.2 83.2% 53.3%;
  --accent: 221.2 83.2% 53.3%;
  /* ... shadcn 默认变量 */
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  --primary: 217.2 91.2% 59.8%;
  --accent: 217.2 91.2% 59.8%;
}

切换主题只需在 <html> 上添加/移除 dark 类,所有组件自动响应。

7.4 SDK 迁移策略

array-sdk 是纯 JavaScript,无框架依赖,直接迁移:

  • sdk-js/ 目录整体复制到新项目
  • package.json 中 "array-sdk": "file:./sdk-js" 保持不变
  • API 封装层 lib/api.ts 从旧项目 api/index.js 翻译为 TypeScript
  • 所有方法签名添加 TypeScript 类型

7.5 组件拆分原则

旧项目 TaskDetail.vue 72KB → 拆分为 8+ 个子组件:

旧代码位置 新组件 职责
消息流模板 MessageList.tsx 消息列表渲染 + 时间分割线
单条消息 MessageBubble.tsx 消息气泡 + 头像 + 引用 + 附件
回复输入栏 MessageInput.tsx 输入框 + 发送按钮 + 快捷键
@提及弹窗 MentionPopup.tsx 搜索 + 选择参与者
私密消息 DMTab.tsx 私聊通道列表 + 消息
附件渲染 AttachmentRenderer.tsx 图片/视频/音频/文件智能渲染
任务抽屉 TaskDrawer.tsx 参与者 + 详情 + 评价
状态选择器 StatusSelector.tsx 状态下拉切换