开发者指南 · 05

前端架构

5.1 目录结构

text
frontend/src/
├── main.tsx                      # React 19 入口(Admin SPA)
├── App.tsx                       # ConfigProvider + AuthProvider + ThemeProvider + StreamProvider + Router
├── router/index.tsx              # BrowserRouter + 三层 ErrorBoundary
├── layouts/AppLayout.tsx         # 主布局(侧栏 + 内容区 + 文件面板触发)

├── stores/
│   ├── authContext.tsx           # 认证状态
│   ├── streamContext.tsx         # SSE 流状态(admin 用)
│   ├── fileWorkspaceContext.tsx  # 文件面板状态
│   └── themeContext.tsx          # 多主题切换 + Antd ThemeConfig

├── services/api.ts               # 统一 API 客户端

├── types/index.ts                # 共享类型(Message / MessageBlock / Subagent / etc.)

├── styles/
│   ├── global.css                # 全局样式 / 滚动条 / markdown
│   ├── themes.css                # 多主题 CSS 变量定义
│   └── theme.ts                  # 后备 JS 常量(实际以 CSS 变量为准)

├── utils/
│   ├── csvParse.ts               # CSV/TSV 状态机解析
│   ├── fileKind.ts               # 扩展名 → kind 分类
│   └── timezone.ts               # 时区工具

├── pages/
│   ├── Login.tsx                 # 品牌分栏登录/注册
│   ├── AdminServices/index.tsx   # Service 管理(4 Tab)
│   ├── Scheduler/index.tsx       # 定时任务(Admin / Service 双 Tab)
│   ├── WeChat/index.tsx          # Admin WeChat 接入
│   ├── Settings/
│   │   ├── index.tsx             # SettingsLayout(侧栏菜单)
│   │   ├── PromptPage.tsx        # System Prompt + Memory & Soul + 能力提示词
│   │   ├── SubagentPage.tsx
│   │   ├── PackagesPage.tsx      # per-user venv
│   │   ├── InboxPage.tsx
│   │   └── GeneralPage.tsx       # API Keys + 时区 + 主题 + Advanced 开关 + BatchRunner 内嵌
│   └── Chat/
│       ├── index.tsx             # 主聊天页
│       ├── chat.module.css       # CSS Modules(--jf-* 变量)
│       ├── markdown.ts           # 唯一渲染管线(含 <<FILE:>> 处理)
│       ├── useSmartScroll.ts     # 智能滚动 hook
│       ├── types.ts              # StreamBlock 联合类型
│       └── components/
│           ├── ThinkingBlock.tsx
│           ├── ToolIndicator.tsx
│           ├── SubagentCard.tsx
│           ├── StreamingMessage.tsx
│           ├── MessageBubble.tsx
│           ├── ApprovalCard.tsx
│           ├── ImageAttachment.tsx
│           ├── VoiceInput.tsx
│           └── PlanTracker.tsx

├── components/
│   ├── FilePanel.tsx
│   ├── FilePreview.tsx           # 多类型文件查看器
│   ├── FileTreePicker.tsx        # 可访问文件/脚本图形选择器
│   ├── HeaderControls.tsx
│   ├── LogoLoading.tsx
│   ├── SplitToggle.tsx
│   ├── ApiKeyWarning.tsx         # 无 LLM Key 时引导 Modal
│   ├── ErrorBoundary.tsx         # 全仓唯一 class 组件
│   └── modals/
│       ├── BatchRunner.tsx       # 内嵌于 GeneralPage
│       ├── SoulSettings.tsx
│       ├── SubagentManager.tsx
│       ├── SystemPromptEditor.tsx
│       └── UserProfileEditor.tsx

└── service-chat/                 # Vite 第二入口(Consumer 端)
    ├── main.tsx
    ├── ServiceChatApp.tsx
    ├── ServiceToolBadge.tsx      # 友好工具状态条(替代 admin 的 ToolIndicator)
    ├── streamHandler.ts          # 轻量 SSE handler(无 HITL/subagent)
    ├── serviceApi.ts             # Consumer 端 API(与 services/api.ts 解耦)
    └── serviceChat.module.css

5.2 路由系统

tsx
<BrowserRouter>
  <Routes>
    <Route path="/login" element={<PublicRoute><Login/></PublicRoute>} />
    <Route element={
      <ErrorBoundary scope="app-layout">
        <ProtectedRoute><AppLayout/></ProtectedRoute>
      </ErrorBoundary>
    }>
      <Route path="/" element={
        <ErrorBoundary scope="chat"><ChatPage/></ErrorBoundary>
      } />
      <Route path="/settings" element={
        <ErrorBoundary scope="settings"><SettingsLayout/></ErrorBoundary>
      }>
        <Route index element={<Navigate to="/settings/prompt" replace/>} />
        <Route path="prompt" element={<PromptPage/>} />
        <Route path="subagents" element={<SubagentPage/>} />
        <Route path="packages" element={<PackagesPage/>} />
        <Route path="batch" element={<Navigate to="/settings/general" replace/>} />
        <Route path="services" element={<AdminServicesPage/>} />
        <Route path="scheduler" element={<SchedulerPage/>} />
        <Route path="wechat" element={<WeChatPage/>} />
        <Route path="inbox" element={<InboxPage/>} />
        <Route path="general" element={<GeneralPage/>} />
      </Route>
    </Route>
    <Route path="*" element={<Navigate to="/" replace/>} />
  </Routes>
</BrowserRouter>
  • ProtectedRoute:未登录重定向 /login
  • PublicRoute:已登录重定向 /
  • AppLayout 提供侧栏导航 + <Outlet/> + Portal 节点 #sider-slot
  • ChatPage / SettingsLayoutcreatePortal 把侧栏内容注入 #sider-slot
  • /settings/batch 重定向到 /settings/general(兼容旧链接)

5.3 状态管理

不使用全局状态库,采用 React Context + 组件局部状态。

Context文件用途
authContextstores/authContext.tsxuser / loading / login / register / logout,Token 存 localStorage
streamContextstores/streamContext.tsxSSE 流状态、blocks 缓冲、HITL 中断
fileWorkspaceContextstores/fileWorkspaceContext.tsx文件面板状态、当前编辑文件
themeContextstores/themeContext.tsx多主题切换 + Antd ThemeConfig

5.4 API 客户端

src/services/api.ts 提供 typed API 客户端。

typescript
async function request<T>(method: string, path: string, body?: unknown): Promise<T>

自动处理:

  • Bearer Token 注入(从 localStorage
  • Content-Type(JSON / FormData)
  • 401 自动清除 Token 并刷新
  • 错误提取 detail 字段

模块划分

模块函数集
Authlogin / register / getMe
Conversationslist/create/get/delete + attachmentUrl
ChatstreamChat / resumeChat / stopChat / abortStream / checkServerStreaming
Fileslist/read/write/edit/delete/move + uploadFiles / downloadFile / mediaUrl
System PromptCRUD + 版本管理
User ProfileCRUD + 版本管理
Capability PromptsgetCapabilityPrompts / updateCapabilityPrompt / resetCapabilityPrompt
Soul ConfiggetSoulConfig / updateSoulConfig
Subagentslist/get/add/update/delete
ScriptsrunScript
AudiotranscribeAudio
ModelsgetModels
BatchuploadBatchExcel / startBatchRun / listBatchTasks / getBatchTask / cancelBatchTask / batchDownloadUrl
SchedulerAdmin + Service 任务 CRUD + runNow
ServicesCRUD + Keys + WeChat channel
Inboxlist/get/updateStatus/delete + getUnreadCount
Packagesvenv 状态 + install/uninstall
API KeysgetApiKeys / updateApiKeys / testApiKeys / getApiKeysStatus
WeChat (Admin)adminWechatQrcode / adminWechatStatus / adminWechatSession / adminWechatMessages
WeChat (Service)serviceWcQrcode / serviceWcSessions / serviceWcSessionMessages

5.5 SSE 流式处理

5.5.1 事件类型

typescript
type SSEEvent =
  | { type: 'token'; content: string }
  | { type: 'thinking'; content: string }
  | { type: 'tool_call'; name: string; args: string }
  | { type: 'tool_call_chunk'; args_delta: string }
  | { type: 'tool_result'; name: string; content: string }
  | { type: 'subagent_call'; name: string; task: string }
  | { type: 'subagent_call_chunk'; args_delta: string }
  | { type: 'subagent_start'; name: string }
  | { type: 'subagent_token'; content: string; agent: string }
  | { type: 'subagent_thinking'; content: string; agent: string }
  | { type: 'subagent_tool_call'; name: string; args: string; agent: string }
  | { type: 'subagent_tool_chunk'; args_delta: string }
  | { type: 'subagent_tool_result'; name: string; content: string; agent: string }
  | { type: 'subagent_end'; name: string; result: string }
  | { type: 'interrupt'; actions: unknown[]; configs: unknown[] }
  | { type: 'done' }
  | { type: 'error'; content: string }

5.5.2 性能优化

text
SSE 回调 → useRef 直接修改 blocks 数组(避免每 token setState)
        → requestAnimationFrame 节流刷新(~60fps)
        → 批量更新到 React state
        → StreamingMessage (React.memo) 避免不必要重渲染

5.5.3 断流恢复

  • 后端 _stream_agent / _stream_consumerfinally 块检测未保存的部分回复,追加 ⚠️ [连接中断 — 已保存已生成内容] 后持久化(_saved 标志位防止重复保存)
  • _active_streams 字典追踪 {thread_id → {user_id, conv_id}}
  • GET /api/chat/streaming-status 返回当前用户活跃 streaming 对话列表
  • 前端 Chat 页加载时 checkServerStreaming(),如发现仍在后台 streaming,显示黄色横幅 + 「终止并保存」/「刷新状态」按钮

5.5.4 Stop 按钮

  • 流式输出时发送按钮变为红色 Stop(Phosphor Stop
  • POST /api/chat/stop 设置 asyncio.Event 取消标志,_stream_agent 每次迭代检查

5.6 Chat 组件层次与共享渲染

5.6.1 组件树

text
ChatPage (pages/Chat/index.tsx)
├── 对话列表(createPortal → #sider-slot)
├── 消息区域
│   ├── MessageBubble(历史消息)
│   │   └── BlocksRenderer(msg.blocks 优先 → fallback 旧逻辑)
│   ├── StreamingMessage(流式消息容器,React.memo)
│   │   ├── ThinkingBlock(折叠/展开)
│   │   ├── ToolIndicator(工具调用,可展开 args/result)
│   │   └── SubagentCard(子代理 timeline 渲染)
│   └── ApprovalCard(HITL 审批:文件 diff / Plan 编辑)
├── 输入区
│   ├── ImageAttachment(粘贴/拖拽/文件选择)
│   ├── VoiceInput(toggle 模式:单击开始 → 单击停止 → 自动转写)
│   ├── 能力开关 / Plan Mode / 模型选择器
│   └── 发送/Stop 按钮
└── useSmartScroll(用户上滚暂停吸底,回底部恢复)

5.6.2 Admin / Service 共享渲染

跨 admin / service 共享的组件(改一次两边都生效):

  • pages/Chat/markdown.ts — 唯一 markdown 渲染管线,含 <<FILE:>> 处理 / DOMPurify / hljs
  • pages/Chat/components/StreamingMessage.tsx — 共享渲染组件,接受 toolRenderer / hideSubagents / avatarSrc props
  • pages/Chat/types.tsStreamBlock 数据结构

渲染差异

维度AdminService
工具块默认 ToolIndicator(真实工具名 + args/result)toolRenderer={ServiceToolBadge}(友好文案,未在白名单的统一"思考中…")
Subagent显示完整 SubagentCardhideSubagents 隐藏
媒体 URLadminMediaUrl(path)setMediaUrlBuilder(buildConsumerMediaUrl) 覆盖为 query 参数携带 service API key

5.6.3 消息 Blocks 持久化

流式输出和历史消息使用统一的交错渲染:

  • 后端save_message(blocks=...)blocks 是有序数组
    • text: {"type": "text", "content": "..."}
    • thinking: {"type": "thinking", "content": "..."}
    • tool: {"type": "tool", "name": "...", "args": "...", "result": "...", "done": true}
    • subagent: {"type": "subagent", "name": "...", "task": "...", "status": "done", "content": "...", "tools": [...], "timeline": [...], "done": true}
  • 前端MessageBubble 检测 msg.blocks 时使用 BlocksRenderer,否则 fallback 旧逻辑

5.7 文件预览面板

  • kind 分类utils/fileKind.ts 按扩展名 → image|audio|video|pdf|markdown|html|csv|json|text|binary
  • openFile 优化:媒体/binary 跳过 api.readFile,直接 setEditingFile(path)
  • 渲染策略
kind渲染方式
image / audio / video / pdf原生 <img>/<audio>/<video>/<iframe>mediaUrl(path),工具栏隐藏「保存」
markdown复用 pages/Chat/markdown.ts::renderMarkdown,样式 .jf-file-md-preview
html<iframe sandbox="allow-scripts">无 same-origin — 允许 Plotly/ECharts 但禁访问父页)
csv / tsvutils/csvParse.ts 状态机解析 → antd Table(最多 2000 行)
json / jsonl / ndjsonJSON.parse + hljs 高亮
text / codetextarea 编辑
binaryEmpty 占位 + 下载按钮
  • 工具栏切换:toggle 类(md/html/csv/json)头部加 antd Segmented「预览/源码」
  • 下载按钮:所有 kind 工具栏统一新增

5.8 错误边界

  • 组件:components/ErrorBoundary.tsx全仓唯一 class 组件,React 19 仍要求 class 形式,刻意豁免 "avoid classes" 规则)
  • 三层部署router/index.tsx):
    • scope="app-layout" 包裹 <AppLayout/> — 兜底整个受保护区域
    • scope="chat" 包裹 <ChatPage/> — 聊天页崩溃不影响侧栏
    • scope="settings" 包裹 <SettingsLayout/> — 设置页崩溃不影响聊天
  • 交互:友好提示 + 可展开错误详情 + 复制错误信息按钮(含 scope/时间/URL/UA/stack/componentStack)
  • 样式:Antd Result + Collapse,背景用 --jf-bg-deep

5.9 设计系统与多主题

5.9.1 多主题(themes.css + themeContext.tsx

  • [data-theme] 属性在 <html> 上,由 ThemeProvider 控制
  • 持久化localStorage.jf-theme
  • 三套已有主题
主题风格主色特殊规则
dark(默认)暖粉紫深色#E89FD9 Primary
cyber-ocean青蓝浅色
terminal磷绿 CRT 终端#33ff00全局 monospace 字体、border-radius: 0、磷光 text-shadow、CRT 扫描线、按钮 hover 反转、text-transform: uppercase
  • 添加新主题
    1. themes.css 复制一个 [data-theme] 块并调整值
    2. themeContext.tsx 添加 THEMES 项 + Antd ThemeConfig
  • CSS 变量命名
    • 品牌色:--jf-primary/secondary/accent/highlight/legacy
    • RGB 三元组:--jf-primary-rgb(用于 rgba(var(--jf-primary-rgb), 0.12)
    • 渐变:--jf-gradient-from/to--jf-user-bubble-bg/shadow
    • 背景:--jf-bg-deep/panel/raised/code/inset
    • 文字:--jf-text/text-muted/text-dim/text-quaternary
    • 边框:--jf-border/border-rgb/border-strong
    • 语义色:--jf-success/warning/error/info
    • 阴影:--jf-shadow-float/hover/brand
    • Diff:--jf-diff-add-bg/del-bg/eq-text
    • Antd:--jf-menu-selected-bg/select-option-bg

5.9.2 圆角规范

档位CSS 变量用途
smvar(--jf-radius-sm)4px,内嵌圆角
mdvar(--jf-radius-md)8px,按钮、面板
lgvar(--jf-radius-lg)12px,卡片、模态
bubblevar(--jf-radius-bubble)16px,消息气泡
  • 圆形元素仍用 '50%'
  • 内联 borderRadius 必须使用变量字符串:'var(--jf-radius-md)',不要硬编码

5.9.3 其他规范

  • 图标统一使用 @phosphor-icons/react(替代 @ant-design/icons
  • 样式优先级:antd 组件 > inline style(含 CSS 变量)> CSS Modules
  • 类型定义集中在 src/types/index.ts
  • API 调用统一通过 src/services/api.ts
  • Logo/media_resources/jellyfishlogo.png
  • 字体:正文 Segoe UI,代码 JetBrains Mono(Google Fonts CDN)

5.9.4 Advanced Tab 可见性

  • GeneralPage.tsx「高级功能」卡片:两个 Switch 控制 Prompt 页的 Advanced Tab 可见性
  • localStorage key:show_advanced_system(操作规则)/ show_advanced_soul(Memory & Soul)
  • 默认关闭,通过自定义事件 advanced-settings-changed 实时响应