Selaa lähdekoodia

feat:agent-pt样式修改后提交

韩洋 1 kuukausi sitten
sitoutus
855ffda0d2
100 muutettua tiedostoa jossa 18402 lisäystä ja 0 poistoa
  1. 66 0
      .gitignore
  2. BIN
      chrome_test.png
  3. 8 0
      dist/assets/index--h7aYXM5.js
  4. 8 0
      dist/assets/index-1KOQHalf.js
  5. 0 0
      dist/assets/index-3zRr7rNx.css
  6. 0 0
      dist/assets/index-5cH65p7j.css
  7. 0 0
      dist/assets/index-8Di7Vd4e.css
  8. 0 0
      dist/assets/index-B2AT6MuX.css
  9. 0 0
      dist/assets/index-BBKpgKqS.css
  10. 0 0
      dist/assets/index-BGJzR7bO.css
  11. 8 0
      dist/assets/index-BRHPI7p1.js
  12. 8 0
      dist/assets/index-BRbaVW_b.js
  13. 0 0
      dist/assets/index-BVhdHt2u.css
  14. 8 0
      dist/assets/index-BdwN_Cbc.js
  15. 0 0
      dist/assets/index-Bi1Dpv1n.css
  16. 0 0
      dist/assets/index-Bx-HZr9X.css
  17. 8 0
      dist/assets/index-C0jXhULb.js
  18. 8 0
      dist/assets/index-C3grXTKS.js
  19. 8 0
      dist/assets/index-C7BLZE9n.js
  20. 8 0
      dist/assets/index-CCN1RHtp.js
  21. 0 0
      dist/assets/index-CCUDalut.css
  22. 0 0
      dist/assets/index-CCk14gi3.css
  23. 8 0
      dist/assets/index-CE4Hxv09.js
  24. 0 0
      dist/assets/index-CJNDpFq6.css
  25. 8 0
      dist/assets/index-CRZ6KAUU.js
  26. 8 0
      dist/assets/index-CZPKjmNq.js
  27. 8 0
      dist/assets/index-C_tRo6pe.js
  28. 0 0
      dist/assets/index-Cb8WaEKa.css
  29. 8 0
      dist/assets/index-Cg6emnox.js
  30. 8 0
      dist/assets/index-CgifawpR.js
  31. 8 0
      dist/assets/index-Ckk64mV8.js
  32. 8 0
      dist/assets/index-DCmS4cHY.js
  33. 8 0
      dist/assets/index-DJ6irjXJ.js
  34. 8 0
      dist/assets/index-DLlXY2H8.js
  35. 8 0
      dist/assets/index-DVBZLYJi.js
  36. 8 0
      dist/assets/index-DYgxE38B.js
  37. 0 0
      dist/assets/index-DfNTy5j_.css
  38. 0 0
      dist/assets/index-Dk-3_q1f.css
  39. 0 0
      dist/assets/index-PqjZUx8o.css
  40. 8 0
      dist/assets/index-V83SpqLc.js
  41. 0 0
      dist/assets/index-WI8pZzOl.css
  42. 8 0
      dist/assets/index-YXzlYvWE.js
  43. 0 0
      dist/assets/index-iqYcRzNm.css
  44. 0 0
      dist/assets/index-mmoUBvDp.css
  45. 0 0
      dist/assets/index-wLd-DUm_.css
  46. 0 0
      dist/assets/pdf-CGMCCvRt.svg
  47. 0 0
      dist/assets/word-C1pNX1hL.svg
  48. 14 0
      dist/index.html
  49. BIN
      dist/logo.png
  50. 327 0
      docs/chat-messages.txt
  51. 193 0
      docs/token运营平台技术文档.md
  52. 333 0
      docs/xx.md
  53. 215 0
      docs/上传文件.md
  54. 1074 0
      docs/产品发展.md
  55. 712 0
      docs/发送对话消息.md
  56. 319 0
      docs/应用配置获取应用参数.md
  57. 443 0
      docs/执行工作流.md
  58. 210 0
      docs/数据集平台.md
  59. 599 0
      docs/数据集平台最终.md
  60. 300 0
      docs/数据集采集详情.md
  61. 203 0
      docs/知识库建设.md
  62. 144 0
      docs/获取应用-webapp-设置.md
  63. 101 0
      docs/获取应用元数据.md
  64. 101 0
      docs/获取应用基本信息.md
  65. 13 0
      index.html
  66. 29 0
      package.json
  67. BIN
      public/logo.png
  68. 5701 0
      src/App.css
  69. 99 0
      src/App.jsx
  70. 0 0
      src/assets/icons/pdf.svg
  71. 0 0
      src/assets/icons/word.svg
  72. 207 0
      src/components/ApiKeyModal.jsx
  73. 352 0
      src/components/Layout.jsx
  74. 195 0
      src/components/ProfileModal.jsx
  75. 12 0
      src/components/ProtectedRoute.jsx
  76. 51 0
      src/components/UserDropdown.jsx
  77. 66 0
      src/config/agentEmptyStates.json
  78. 14 0
      src/config/index.js
  79. 38 0
      src/contexts/AuthContext.jsx
  80. 58 0
      src/index.css
  81. 11 0
      src/main.jsx
  82. 351 0
      src/pages/AgentApply.jsx
  83. 1781 0
      src/pages/AgentExecute.jsx
  84. 381 0
      src/pages/AgentList.jsx
  85. 81 0
      src/pages/AvailableAgents.jsx
  86. 62 0
      src/pages/ExternalMock.jsx
  87. 88 0
      src/pages/History.jsx
  88. 238 0
      src/pages/Login.css
  89. 161 0
      src/pages/Login.jsx
  90. 1455 0
      src/pages/Plaza.css
  91. 492 0
      src/pages/Plaza.jsx
  92. 73 0
      src/service/account.js
  93. 51 0
      src/service/auth.js
  94. 188 0
      src/service/base.js
  95. 146 0
      src/service/business.js
  96. 391 0
      src/service/dify.js
  97. 42 0
      src/utils/auth.js
  98. 10 0
      src/utils/emoji-mart.js
  99. 10 0
      src/utils/encryption.js
  100. 17 0
      vite.config.js

+ 66 - 0
.gitignore

@@ -0,0 +1,66 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+/.history
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# npm
+package-lock.json
+
+# yarn
+.pnp.cjs
+.pnp.loader.mjs
+.yarn/
+
+.favorites.json
+
+# storybook
+/storybook-static
+*storybook.log
+
+# mise
+mise.toml
+
+
+# PWA generated files
+public/sw.js
+public/sw.js.map
+public/workbox-*.js
+public/workbox-*.js.map
+public/fallback-*.js
+
+.vscode/settings.json
+.vscode/mcp.json

BIN
chrome_test.png


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index--h7aYXM5.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-1KOQHalf.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-3zRr7rNx.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-5cH65p7j.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-8Di7Vd4e.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-B2AT6MuX.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-BBKpgKqS.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-BGJzR7bO.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-BRHPI7p1.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-BRbaVW_b.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-BVhdHt2u.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-BdwN_Cbc.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-Bi1Dpv1n.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-Bx-HZr9X.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-C0jXhULb.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-C3grXTKS.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-C7BLZE9n.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-CCN1RHtp.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-CCUDalut.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-CCk14gi3.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-CE4Hxv09.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-CJNDpFq6.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-CRZ6KAUU.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-CZPKjmNq.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-C_tRo6pe.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-Cb8WaEKa.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-Cg6emnox.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-CgifawpR.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-Ckk64mV8.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-DCmS4cHY.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-DJ6irjXJ.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-DLlXY2H8.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-DVBZLYJi.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-DYgxE38B.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-DfNTy5j_.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-Dk-3_q1f.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-PqjZUx8o.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-V83SpqLc.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-WI8pZzOl.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
dist/assets/index-YXzlYvWE.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-iqYcRzNm.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-mmoUBvDp.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/index-wLd-DUm_.css


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/pdf-CGMCCvRt.svg


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/assets/word-C1pNX1hL.svg


+ 14 - 0
dist/index.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>智能体使用平台</title>
+    <script type="module" crossorigin src="/assets/index-BRbaVW_b.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-CCUDalut.css">
+  </head>
+  <body>
+    <div id="root"></div>
+  </body>
+</html>

BIN
dist/logo.png


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 327 - 0
docs/chat-messages.txt


+ 193 - 0
docs/token运营平台技术文档.md

@@ -0,0 +1,193 @@
+
+---
+
+# Token运营平台技术方案(基于New API改造)
+
+## 1. 方案定位与设计原则
+
+本次建设基于开源New API项目进行深度改造,目标是在最短周期内构建一个具备生产级能力的Token运营平台。整体设计遵循以下原则:
+
+- **复用优先,适度改造**:充分利用New API已有的通道管理、API Key生成、调用路由等核心能力,避免重复造轮子,仅对不足部分进行增强替换。
+- **多租户隔离内置**:从设计之初将企业租户、部门、角色的多级隔离体系融入权限与数据链路,而非后期外挂。
+- **运营与数据并重**:平台不仅满足调用、管控、计费等运营闭环,同时将数据埋点、合规标识、回流接口作为一等公民设计,为后续数据集治理奠定基础。
+- **可观测与可审计**:全链路调用日志、操作审计日志、费用明细三者联动,确保每一笔Token消耗和每一次配置变更均可追溯。
+- **平滑演进**:预留私有化模型接入、数据集系统对接、更多计费策略等扩展点,支撑后续从V1.0到V2.0的迭代。
+
+---
+
+## 2. 整体技术架构
+
+### 2.1 架构分层
+
+平台采用分层架构,各层职责清晰、依赖单向,便于独立迭代与横向扩展:
+
+| 层级 | 说明 |
+|---|---|
+| **接入层** | 对外提供统一HTTP API(兼容OpenAI格式),对内负责鉴权、限流、路由转发至对应模型通道 |
+| **业务服务层** | 承载核心业务逻辑,包括模型管理、定价计算、配额校验、费用核算、日志记录、埋点采集等 |
+| **权限控制层** | 基于RBAC+租户隔离的权限模型,贯穿所有业务操作,控制数据可见性与操作边界 |
+| **数据访问层** | 对数据库、缓存、消息队列的统一抽象,提供分库分表、读写分离的能力基础 |
+| **基础设施层** | 底层存储(关系型数据库、时序数据库、对象存储)、缓存(Redis)、消息中间件、容器运行环境 |
+
+### 2.2 逻辑架构视图
+
+- **管理端**:面向平台超级管理员、运营管理员,提供渠道、模型、租户、全局看板等管理功能。
+- **企业端/用户端**:面向企业租户管理员及普通员工,提供本企业/个人维度的用户管理、API Key管理、用量查看、看板等功能。
+- **核心调度引擎**:基于New API改造的模型路由与负载均衡模块,负责多通道Key轮询、优先级调度、故障自动剔除与恢复。
+- **数据埋点与回流模块**:与主业务流程解耦,采用异步方式记录全场景字段,并通过预留接口供数据集系统拉取。
+
+### 2.3 部署架构
+
+- 采用容器化部署(Kubernetes),支持水平弹性扩缩容。
+- 微服务拆分:将权限、计费、日志等相对独立的能力沉淀为独立服务,便于复用。
+- 多环境隔离:开发、测试、预发布、生产环境独立部署,配置分离。
+- 数据存储:业务库使用MySQL(分库分表),日志与调用明细使用ClickHouse或ElasticSearch以支撑高效检索与聚合。
+
+---
+
+## 3. 核心模块划分与职责
+
+| 模块 | 职责 | 与New API的关系 |
+|---|---|---|
+| **渠道接入与调度模块** | 管理模型厂商通道配置、API Key池、轮询策略、优先级、熔断降级;兼容DeepSeek、通义、智谱、文心等主流厂商,预留私有化接入点 | **重度改造**:New API已有基础,但需增加健康检查、自动禁用恢复、权重调度等能力 |
+| **模型定价与计费模块** | 维护模型元数据(版本、上下文窗口)、全局基准单价;根据实际调用Token量实时计算费用,支持多维度费用汇总 | **新增**:New API无此能力 |
+| **多租户与权限管控模块** | 实现平台级、企业级、个人级三层权限隔离;控制菜单可见性、数据行级过滤、操作能力边界 | **新增**:需从零构建完整的RBAC+租户体系 |
+| **企业管理模块** | 管理企业资质、部门结构、员工账号;支持批量导入、状态冻结/启用、密码重置等操作 | **新增** |
+| **API Key全生命周期管理** | 支持用户创建、配额限制、模型白名单、过期时间、密钥脱敏展示、禁用/启用等管理能力 | **增强**:New API支持基础Key生成,需新增配额、模型限制、脱敏等能力 |
+| **调用日志与审计模块** | 记录每次调用的完整上下文(时间、用户、模型、Token、费用、状态、响应时长等),同时记录后台所有配置变更操作日志 | **新增**:New API仅有简单日志 |
+| **统计分析看板** | 提供全局、企业、个人多层级看板,聚合展示调用量、Token消耗、费用趋势、热门模型、异常分布等关键指标 | **新增** |
+| **数据集埋点与回流接口模块** | 在调用链路中采集完整上下文信息(业务来源、知识引用、文件、问数、用户反馈等),落库并预留数据集拉取接口 | **新增** |
+| **数据合规标记模块** | 对每条调用记录标注敏感级别、是否脱敏、是否可入库、是否可训练、是否可导出等合规标签 | **新增** |
+
+---
+
+## 4. 权限体系设计
+
+权限体系采用 **“平台 — 企业 — 个人”** 三层模型,结合角色赋予操作权限,通过数据行级权限控制实现严格的租户隔离。
+
+### 4.1 角色定义
+
+| 角色 | 权限范围 | 数据范围 |
+|---|---|---|
+| 平台超级管理员 | 全功能操作(渠道、模型、租户、用户、全部看板、全部日志) | 全平台数据 |
+| 平台运营管理员 | 仅全局查看(看板、日志、数据集回流配置),无增删改操作 | 全平台数据(只读) |
+| 企业租户管理员 | 本企业内用户管理、API Key管理、本企业用量查看、企业内部日志 | 仅限所属企业数据 |
+| 普通员工 | 个人API Key管理、个人用量查看、个人调用日志 | 仅限本人数据 |
+
+### 4.2 权限控制方式
+
+- **功能权限**:基于角色+菜单/按钮资源配置,前端控制展示,后端强制校验。
+- **数据权限**:所有查询接口自动追加租户过滤条件(企业ID/部门ID/用户ID),确保跨租户数据不可见。
+- **接口鉴权**:后台管理接口采用JWT+RBAC,API调用采用API Key+配额/模型策略校验。
+
+---
+
+## 5. 数据模型与数据链路设计
+
+### 5.1 核心实体关系
+
+- **租户维度**:企业(租户) → 部门 → 员工(用户)
+- **权限维度**:用户 → 角色 → 权限(功能+数据范围)
+- **调用维度**:API Key → 调用记录 → Token消耗明细 → 费用明细
+- **模型维度**:渠道 → 模型 → 定价策略
+- **埋点维度**:调用记录(1) → 扩展埋点信息(业务场景、知识引用、文件、问数、用户反馈等)
+
+### 5.2 数据流转链路
+
+```mermaid
+flowchart LR
+    A[用户发起调用] --> B[API Key鉴权&配额校验]
+    B --> C[模型路由与负载均衡]
+    C --> D[调用上游模型通道]
+    D --> E[返回结果&Token计费]
+    E --> F[同步写入调用日志]
+    F --> G[异步写入埋点明细]
+    G --> H[实时统计聚合]
+    F --> I[审计日志记录]
+    H --> J[看板展示]
+    G --> K[预留数据集回流接口]
+    K --> L[数据集系统采集]
+```
+
+### 5.3 数据分层存储策略
+
+| 数据类型 | 存储方案 | 说明 |
+|---|---|---|
+| 核心业务数据(企业、用户、模型、定价、API Key) | MySQL(分库分表) | 强一致性要求,事务性操作 |
+| 调用日志与埋点明细 | ClickHouse / ElasticSearch | 海量写入,高效时序查询与全文检索 |
+| 运营看板聚合数据 | Redis缓存 + 定时物化视图 | 提升大屏展示与高频查询性能 |
+| 操作审计日志 | MySQL + 归档 | 合规保留,支持长期审计 |
+
+---
+
+## 6. 关键流程设计
+
+### 6.1 模型调用流程(含埋点)
+
+1. 用户通过API Key发起请求,网关层拦截并解析请求。
+2. 校验API Key有效性、配额剩余、模型白名单、过期时间。
+3. 根据请求模型匹配对应渠道,按照轮询/优先级策略选取可用通道Key。
+4. 转发请求至上游模型厂商,记录开始时间。
+5. 接收响应后计算输入/输出Token数量,依据定价策略核算费用。
+6. 同步记录调用日志(基础字段),异步写入埋点明细(业务上下文、引用、文件、反馈等)。
+7. 更新用户配额与费用汇总数据。
+8. 若调用失败,触发重试或故障剔除策略,并记录错误码。
+
+### 6.2 配额与费用核算流程
+
+- **实时扣减**:每次调用成功后实时扣减用户配额(按企业/个人配置),并记录流水。
+- **定时汇总**:每日定时任务按企业、部门、用户、模型维度汇总Token消耗与费用,生成对账报表。
+- **预警机制**:当配额剩余低于阈值时,支持配置告警通知。
+
+### 6.3 数据集埋点采集流程
+
+- **异步解耦**:调用结束后,将埋点数据发送至消息队列(Kafka/RocketMQ),由独立消费者处理写入,不阻塞主调用链路。
+- **合规标记**:在写入埋点数据时,根据配置规则自动添加合规标识(敏感级别、是否可入库、是否可用于训练等)。
+- **接口预留**:提供标准HTTP接口供数据集系统按时间范围拉取增量埋点数据,支持分页与断点续传。
+
+---
+
+## 7. 技术选型建议
+
+| 组件 | 选型建议 | 理由 |
+|---|---|---|
+| 后端框架 | Go(推荐)或Java Spring Boot | New API原为Go实现,若团队对Go熟练可保持技术栈一致;若Java生态更成熟可迁移至Spring Boot |
+| 数据库 | MySQL(业务主库)+ TiDB(可选) | 支持分库分表,满足租户数据隔离与扩展 |
+| 日志/时序存储 | ClickHouse / ElasticSearch | 支持海量日志写入与快速聚合查询 |
+| 缓存 | Redis | 缓存定价配置、配额、渠道Key池,提升调用性能 |
+| 消息队列 | Kafka / RocketMQ | 解耦埋点写入,保证削峰填谷 |
+| 部署编排 | Kubernetes + Istio(可选) | 支持弹性伸缩、灰度发布与服务治理 |
+| 监控告警 | Prometheus + Grafana | 监控平台运行状态与调用SLA |
+
+---
+
+## 8. 扩展性设计
+
+- **模型接入扩展**:渠道管理采用可插拔适配器模式,新增厂商仅需实现标准接口,无需改动核心调度逻辑。
+- **计费策略扩展**:预留多套定价模型接口(如按模型、按时段、按企业协议价),支持后续灵活切换。
+- **数据集回流扩展**:接口设计遵循分页+增量时间戳模式,支持后续切换为消息推送或Binlog同步方式。
+- **国际化扩展**:预留多语言资源配置,支持后续国际化部署。
+
+---
+
+## 9. 非功能性设计考量
+
+| 维度 | 设计措施 |
+|---|---|
+| **性能** | 调用链路关键环节使用缓存;日志/埋点异步写入;数据库读写分离;支持水平扩容 |
+| **可用性** | 核心服务多实例部署;下游模型通道故障自动剔除与恢复;关键数据定期备份 |
+| **安全性** | API Key脱敏展示;传输层TLS加密;敏感日志脱敏存储;操作审计全记录 |
+| **可维护性** | 统一日志规范;全链路Trace ID贯穿;模块化设计便于独立升级 |
+| **数据合规** | 内置合规标识体系;支持数据导出审批流程;满足数据分级分类管理要求 |
+
+---
+
+## 10. 分阶段演进策略
+
+| 阶段 | 重点目标 |
+|---|---|
+| V1.0(当前) | 基于New API改造完成平台核心运营能力、权限体系、埋点规范与回流接口预留,满足内部试用与对外演示 |
+| V1.1(后续) | 增强数据集采集能力,对接数据集系统的清洗、脱敏、标注、入库流水线,实现自动化数据沉淀 |
+| V2.0(远期) | 引入更多模型计费策略、智能路由(基于成本/延迟/质量)、企业级SLA保障、开放API网关能力 |
+
+---

+ 333 - 0
docs/xx.md

@@ -0,0 +1,333 @@
+# 企业内部统一Token运营分发平台技术选型方案
+
+## 一、选型背景与目标
+
+### 1.1 业务痛点
+
+集团各事业部独立持有文心、通义、OpenAI、Claude等多厂商大模型API密钥,存在以下治理问题:
+
+| 序号 | 痛点 | 影响程度 |
+|------|------|----------|
+| 1 | 密钥分散存储,无统一托管与回收机制 | 严重 |
+| 2 | 无标准化内部子Token分发,权限/额度无法按部门隔离 | 严重 |
+| 3 | 单机工具无法承载万级QPS并发 | 高危 |
+| 4 | 无调用日志、用量统计、审计溯源 | 高危 |
+| 5 | 开源工具存在停更、适配滞后风险 | 中等 |
+
+### 1.2 硬性约束条件
+
+| 编号 | 约束项 | 说明 |
+|------|--------|------|
+| H1 | 源码完全开放 | 支持企业私有化深度二次开发 |
+| H2 | 核心定位 | Token集中管理、内部子Token分发运营 |
+| H3 | 分布式集群扩展 | 具备支撑集团大规模业务的扩展潜力 |
+| H4 | 社区可持续维护 | 规避长期无人迭代风险 |
+
+### 1.3 建设目标
+
+1. **统一密钥托管**:全厂商API Key集中收纳、加密存储、分级权限管控
+2. **标准化Token分发**:子Token支持配额、时效、IP、模型权限精细化管控
+3. **分布式高可用底座**:多实例水平扩容,支撑集团全业务并发调用
+4. **全链路流量治理**:负载均衡、故障切换、限流熔断、重试降级
+5. **合规可观测体系**:调用日志、操作审计、用量报表、异常告警
+6. **可定制扩展**:对接LDAP/SSO、成本核算、审批流等
+7. **长期可持续迭代**:依托活跃开源社区,减少重复造轮子
+
+## 二、候选方案横向对比总览
+
+### 2.1 五方案综合能力评分矩阵
+
+| 评估维度(权重) | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|------------------|--------|---------|---------|----------------|------|
+| **技术架构合理性(15%)** | ★★★ | ★★★★★ | ★★ | ★★★★ | ★★★★★ |
+| **源码与二次开发(15%)** | ★★★★ | ★★★★★ | ★★★ | ★★★ | ★★★★★ |
+| **Token运营能力(15%)** | ★★★★★ | ★★★ | ★★★★ | ★★★★ | ★★★★★ |
+| **分布式集群能力(15%)** | ★★ | ★★★★★ | ★ | ★★★ | ★★★★★ |
+| **社区活跃度(10%)** | ★★★★★ | ★★★★★ | ★★ | ★★ | — |
+| **大模型适配(10%)** | ★★★★★ | ★★★★ | ★★★ | ★★★ | ★★★★★ |
+| **流量治理(10%)** | ★★★ | ★★★★★ | ★★ | ★★★ | ★★★★★ |
+| **可观测与审计(10%)** | ★★★ | ★★★★★ | ★★ | ★★★ | ★★★★★ |
+| **综合得分** | **3.8** | **4.7** | **2.4** | **3.1** | **4.9** |
+
+> ⭐ 评分说明:1=严重缺陷,2=不足,3=达标,4=良好,5=优秀
+
+---
+
+## 三、分维度逐项对比详情
+
+### 3.1 技术架构
+
+| 对比项 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|--------|--------|---------|---------|----------------|------|
+| **架构模式** | 单体一体化 | 分层解耦云原生 | 单体一体化 | 分层分离架构 | 完全自定义 |
+| **网关与后台耦合度** | 高耦合 | 完全解耦 | 高耦合 | 完全解耦 | 按需设计 |
+| **状态设计** | 有状态(需改造) | 完全无状态 | 有状态 | 网关无状态 | 按需设计 |
+| **配置同步机制** | 依赖Redis改造 | Redis原生支持 | 无 | Redis基础支持 | 按需设计 |
+| **K8s原生适配** | 需改造 | 天然适配 | 不支持 | 缺少官方模板 | 按需设计 |
+| **架构评分** | ★★★ | ★★★★★ | ★★ | ★★★★ | ★★★★★ |
+
+**分析**:
+- **LiteLLM**采用云原生分层解耦架构,网关完全无状态,配置/配额统一托管Redis,天然适配K8s弹性伸缩,架构最成熟。
+- **NewAPI**为单体一体化架构,虽可改造为集群部署,但网关与后台共用资源,高并发下管理操作挤占转发资源。
+- **LLMProxy-Admin**架构设计合理(网关与后台分离),但工程实现缺少成熟集群方案。
+
+---
+
+### 3.2 开发语言与技术栈
+
+| 对比项 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|--------|--------|---------|---------|----------------|------|
+| **后端语言** | Go | Python(异步IO) | Go | Go | Go/Java自由选 |
+| **前端框架** | Vue | 无(API Only) | Vue | Vue | Vue/React自由选 |
+| **部署方式** | 单文件二进制 | 容器化优先 | 单文件二进制 | 单文件二进制 | 按需选型 |
+| **性能特点** | 内存占用低 | 异步高性能 | 内存占用低 | 轻量高效 | 按需设计 |
+| **技术评分** | ★★★★ | ★★★ | ★★★★ | ★★★★ | ★★★★★ |
+
+**分析**:
+- NewAPI/Sub2API/LLMProxy-Admin均采用Go+Vue组合,编译型语言性能稳定,部署简单。
+- LiteLLM采用Python异步IO架构,生态丰富、插件化扩展能力强,但运行环境依赖容器化。
+- 自研方案技术栈自由,但需企业自行承担技术选型与基建成本。
+
+---
+
+### 3.3 源码开放性与二次开发
+
+| 对比项 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|--------|--------|---------|---------|----------------|------|
+| **开源协议** | MIT | MIT | MIT | MIT | 100%自有 |
+| **源码完整度** | 完整开放 | 完整开放 | 完整开放 | 完整开放 | — |
+| **代码分层设计** | 中等(耦合较重) | 优秀(插件化) | 较差(高耦合) | 中等(文档少) | 完全可控 |
+| **扩展改造成本** | 中等 | 低(插件机制) | 高 | 较高 | 自主可控 |
+| **企业定制扩展点** | 渠道/配额/前端 | 全插件接口 | 计费/账单模块 | 需深度读源码 | 无限 |
+| **二次开发评分** | ★★★★ | ★★★★★ | ★★★ | ★★★ | ★★★★★ |
+
+**分析**:
+- **LiteLLM**插件化架构为最大优势,鉴权、路由、计量、缓存全部标准化接口,自定义逻辑不修改底层核心,上游版本升级冲突风险最低。
+- **NewAPI**代码结构清晰、国内开发者熟悉度高,但单体耦合严重,拆分网关需大规模重构。
+- **Sub2API**大量代码围绕付费计费逻辑,内部免费场景下冗余模块删减困难。
+
+---
+
+### 3.4 核心Token运营能力(硬性约束H2)
+
+| 功能项 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|--------|--------|---------|---------|----------------|------|
+| **原始密钥批量导入/托管** | ✅ 完善 | ✅ API方式 | ✅ 基础 | ✅ 基础 | ✅ 自定义 |
+| **子Token创建与配额管理** | ✅ 日/月额度 | ✅ 多维度限制 | ✅ 基础 | ✅ 基础 | ✅ 自定义 |
+| **模型黑白名单控制** | ✅ | ✅ | 部分 | 部分 | ✅ |
+| **IP白名单/时效管控** | ✅ | ✅ | ✅ | ✅ | ✅ |
+| **可视化运营后台** | ✅ 开箱即用 | ❌ 无(需自建) | ✅ 内置 | ✅ 内置 | ✅ 自定义 |
+| **租户/部门资源隔离** | ✅ 分组管理 | ✅ 租户隔离 | 粒度粗 | 功能缺失 | ✅ 自定义 |
+| **国产多模型分组管理** | ✅ 最完善 | 部分滞后 | 弱 | 基础 | ✅ 自定义 |
+| **内置成本分摊/账单** | 基础用量 | ❌ | ✅ 完善 | ❌ | ✅ 自定义 |
+| **运营能力评分** | ★★★★★ | ★★★ | ★★★★ | ★★★★ | ★★★★★ |
+
+**关键差异**:
+- **NewAPI**开箱即用完整运营后台,是唯一无需前端开发即可完成日常密钥运维的方案。
+- **LiteLLM**底层能力完备,但所有操作需通过API调用,可视化运营需全栈二次开发,人力投入最大。
+- **Sub2API**内置账单模块适合付费场景,但对内部免费分发场景冗余功能过多。
+
+---
+
+### 3.5 分布式集群扩展能力(硬性约束H3)
+
+| 对比项 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|--------|--------|---------|---------|----------------|------|
+| **原生分布式支持** | ❌ 需改造 | ✅ 原生设计 | ❌ 无 | 架构支持但工程弱 | ✅ 自主设计 |
+| **多实例数据一致性** | 需Redis改造 | Redis统一托管 | 无机制 | Redis基础实现 | 自主实现 |
+| **网关独立弹性伸缩** | 不支持 | ✅ 无状态独立扩缩 | 不支持 | 理论可行 | ✅ |
+| **K8s HPA自动扩缩** | 需改造 | ✅ 原生支持 | 不支持 | 无官方模板 | 自主实现 |
+| **跨机房多集群部署** | 困难 | ✅ 原生支持 | 不支持 | 未验证 | 自主实现 |
+| **日均调用支撑上限** | 数万级 | 百万级以上 | 单机万级 | 数万级 | 不限 |
+| **分布式评分** | ★★ | ★★★★★ | ★ | ★★★ | ★★★★★ |
+
+**分析**:
+- **LiteLLM**分布式能力最优,转发服务无状态、Redis全局统一配额,支持K8s HPA自动弹性,百万级日调用无瓶颈。
+- **NewAPI**单体架构先天限制,多实例需依赖Redis改造共享配置,且网关与后台耦合,无法独立弹性伸缩。
+- **Sub2API**完全不适合分布式集群场景,多节点并发会出现额度重复扣减、路由不一致。
+- **LLMProxy-Admin**架构合理但实现不成熟,高并发下存在同步延迟问题,生产级大规模案例极少。
+
+---
+
+### 3.6 社区活跃度与可持续维护(硬性约束H4)
+
+| 对比项 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|--------|--------|---------|---------|----------------|------|
+| **项目维护模式** | 数十位贡献者 | 商业公司+全球社区 | 单人维护 | 单人独立维护 | 内部团队 |
+| **代码提交频率** | 每周稳定提交 | 每日提交 | 间歇性 | 数月无更新 | 自主掌控 |
+| **版本发布节奏** | 月度固定Release | 规划清晰Roadmap | 无固定节奏 | 无固定节奏 | 自主规划 |
+| **Issue响应速度** | 快 | 极快 | 长期积压 | 慢 | 自主处理 |
+| **国产模型适配速度** | 国内最快 | 海外优先,国内滞后 | 缓慢 | 缓慢 | 自主可控 |
+| **企业落地案例** | 大量国内企业 | 全球大厂 | 少量创业团队 | 极少 | — |
+| **社区评分** | ★★★★★ | ★★★★★ | ★★ | ★★ | — |
+
+**风险结论**:
+- **NewAPI**与**LiteLLM**均为高活跃项目,不存在单人维护风险,长期可持续迭代有保障。
+- **Sub2API**与**LLMProxy-Admin**为单人维护,Issue长期积压,Bug修复周期长,集团级生产环境落地风险极高。
+- **自研**无外部依赖,但迭代节奏完全依赖内部研发资源投入。
+
+---
+
+### 3.7 大模型厂商适配
+
+| 厂商/模型 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|-----------|--------|---------|---------|----------------|------|
+| OpenAI | ✅ 完整 | ✅ 最完善 | ✅ 优先适配 | ✅ 基础 | ✅ |
+| Claude/Anthropic | ✅ | ✅ 最完善 | 部分 | 部分 | ✅ |
+| Gemini/Google | ✅ | ✅ 完整 | 部分 | 部分 | ✅ |
+| Azure OpenAI | ✅ | ✅ 完整 | 部分 | 部分 | ✅ |
+| 文心一言 | ✅ 第一梯队 | 适配滞后 | 覆盖不全 | 基础 | ✅ |
+| 通义千问 | ✅ 第一梯队 | 适配滞后 | 覆盖不全 | 基础 | ✅ |
+| 讯飞星火 | ✅ 第一梯队 | 适配滞后 | 覆盖不全 | 基础 | ✅ |
+| 智谱/ChatGLM | ✅ 第一梯队 | 适配滞后 | 覆盖不全 | 基础 | ✅ |
+| DeepSeek | ✅ | 适配滞后 | — | — | ✅ |
+| MiniMax | ✅ | 适配滞后 | — | — | ✅ |
+| 本地私有化模型 | 需二开 | 需插件开发 | 不支持 | 不支持 | ✅ |
+| **适配评分** | ★★★★★ | ★★★★ | ★★★ | ★★★ | ★★★★★ |
+
+**分析**:
+- **NewAPI**国产模型适配能力最强,国内厂商新接口第一时间跟进,社区插件丰富。
+- **LiteLLM**海外模型生态最完善,但国内小众模型适配滞后,中文本地化能力弱,需自行编写适配插件。
+- **自研**可快速对接企业私有化本地大模型集群,响应速度完全可控。
+
+---
+
+### 3.8 流量治理能力
+
+| 治理能力 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|----------|--------|---------|---------|----------------|------|
+| 多密钥轮询/权重负载 | ✅ | ✅ | ✅ | ✅ | ✅ |
+| 渠道故障自动剔除 | ✅ | ✅ | 基础 | 基础 | ✅ |
+| 单次失败重试 | ✅ | ✅ | ✅ | ✅ | ✅ |
+| **分布式全局限流** | ❌ | ✅ 原生支持 | ❌ | 基础实现 | ✅ |
+| **多级熔断降级** | ❌ | ✅ 完善 | ❌ | ❌ | ✅ |
+| **灰度路由/流量染色** | ❌ | ✅ | ❌ | ❌ | ✅ |
+| **KV缓存加速** | ❌ | ✅ | ❌ | ❌ | ✅ |
+| **超时自适应降级** | ❌ | ✅ | ❌ | ❌ | ✅ |
+| **治理评分** | ★★★ | ★★★★★ | ★★ | ★★★ | ★★★★★ |
+
+**分析**:
+- **LiteLLM**提供企业级完整流量治理体系,分布式限流基于Redis计数器,多节点额度不超额,灰度路由、流量染色等能力完备。
+- **NewAPI**仅内置单机级基础治理能力,无分布式全局限流、多级熔断等高阶能力。
+- 自研方案全部能力需自主开发,但可按需叠加任意治理策略。
+
+---
+
+### 3.9 可观测与审计合规
+
+| 对比项 | NewAPI | LiteLLM | Sub2API | LLMProxy-Admin | 自研 |
+|--------|--------|---------|---------|----------------|------|
+| 调用日志 | 基础日志 | 全链路结构化 | 简陋 | 简单日志 | 自定义 |
+| 用量统计 | 基础统计 | 多维度计量 | 账单为主 | 基础报表 | 自定义 |
+| **OTel链路追踪** | ❌ | ✅ 原生支持 | ❌ | ❌ | ✅ 可集成 |
+| **Prometheus监控** | ❌ 需二开 | ✅ 原生暴露 | ❌ | ❌ | ✅ 可集成 |
+| **操作审计留痕** | 简单记录 | 完整审计字段 | 缺失 | 不足 | 自定义 |
+| **ELK对接能力** | 需二开 | ✅ 结构化输出 | 困难 | 困难 | 自定义 |
+| **告警推送** | 无原生 | ✅ 自定义钩子 | 无 | 无 | 自定义 |
+| **等保合规** | 需补充开发 | 少量补充 | 不满足 | 不满足 | 完全贴合 |
+| **观测评分** | ★★★ | ★★★★★ | ★★ | ★★★ | ★★★★★ |
+
+**分析**:
+- **LiteLLM**原生支持OTel、Prometheus标准接口,全链路日志结构化输出,可直接对接ELK,审计字段完整。
+- **NewAPI**基础功能具备,但需二次开发对接企业监控体系,无标准化指标暴露接口。
+- Sub2API/LLMProxy-Admin合规能力严重不足,无法满足集团等保审计要求。
+
+---
+
+## 四、各方案业务场景适配结论
+
+| 方案 | 最佳适配场景 | 企业规模 | 推荐度 |
+|------|-------------|----------|--------|
+| **NewAPI** | 国内中小企业、内网小规模调用、快速上线可视化运营 | 中小规模(日调用数万级) | ⭐⭐⭐⭐ |
+| **LiteLLM** | 云原生K8s架构、超大并发、海外模型为主、有前端自研能力 | 中大型集团(百万级日调用) | ⭐⭐⭐⭐ |
+| **Sub2API** | 小型创业团队、对外付费AI服务运营 | 极小型(单机) | ⭐ |
+| **LLMProxy-Admin** | 小业务线临时中转、测试环境 | 测试/临时场景 | ⭐⭐ |
+| **自研** | 涉密内网、特殊合规、充足研发人力 | 超大型集团 | ⭐⭐⭐⭐ |
+| **LiteLLM+NewAPI组合** | 国内大型集团、兼顾国产适配+超大并发+开箱即用 | 集团级(全规模) | ⭐⭐⭐⭐⭐ |
+
+---
+
+## 五、最终推荐方案
+
+### 方案A:最优组合(推荐集团正式落地)⭐
+
+**LiteLLM分布式转发网关集群 + NewAPI Token可视化运营后台**
+
+| 硬性约束 | 满足情况 |
+|----------|----------|
+| H1 源码开放可二开 | ✅ 两套均为MIT协议,完整Fork至企业私有仓库 |
+| H2 Token运营分发 | ✅ NewAPI完整运营后台 + LiteLLM底层完备能力 |
+| H3 分布式集群扩展 | ✅ LiteLLM原生无状态,K8s HPA弹性伸缩,百万级QPS |
+| H4 社区可持续维护 | ✅ 两套均为高活跃主流项目,单一停更可单独替换 |
+
+**组合优势**:
+- 架构互补:LiteLLM解决集群扩容+流量治理,NewAPI补齐可视化运营+国产模型适配
+- 风险对冲:分层解耦,任一项目迭代放缓可单独替换对应层级
+- 落地路径清晰:短期NewAPI快速上线,长期LiteLLM支撑业务爆发式扩容
+
+### 方案B:仅国内中小规模业务
+
+单独选用**NewAPI**,部署简单、运维成本低、国产模型适配友好;业务增长至瓶颈后接入LiteLLM做转发层拆分改造。
+
+### 方案C:涉密/强合规/充足人力
+
+选用**自研方案**,彻底消除外部开源依赖,架构、安全、流程完全贴合内部管控标准。
+
+### 方案D:直接淘汰
+
+**Sub2API**、**LLMProxy-Admin**:社区活跃度低、长期维护风险不可控,分布式能力先天缺陷,不适合集团级平台。
+
+---
+
+## 六、组合方案落地架构概览
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                      接入负载均衡层                              │
+│               K8s Ingress / Nginx(HTTPS + IP白名单)           │
+└───────────────────────────┬─────────────────────────────────────┘
+                            │
+┌───────────────────────────▼─────────────────────────────────────┐
+│                分布式转发集群层(LiteLLM Proxy)                  │
+│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
+│  │ Pod 1    │  │ Pod 2    │  │ Pod 3    │  │ Pod N    │       │
+│  │无状态网关 │  │无状态网关 │  │无状态网关 │  │无状态网关 │       │
+│  └──────────┘  └──────────┘  └──────────┘  └──────────┘       │
+│  子Token鉴权 | 模型路由 | 分布式限流熔断 | 负载均衡 | 计量      │
+└───────────────────────────┬─────────────────────────────────────┘
+                            │ Redis Pub/Sub 配置实时同步
+┌───────────────────────────▼─────────────────────────────────────┐
+│                 Token运营后台层(NewAPI)                        │
+│  ┌──────────┐  ┌──────────┐                                    │
+│  │ Admin 1  │  │ Admin 2  │(多副本,不承载转发流量)           │
+│  └──────────┘  └──────────┘                                    │
+│  密钥加密入库 | 子Token创建 | 配额配置 | 用量报表 | 操作审计     │
+└───────────────────────────┬─────────────────────────────────────┘
+                            │
+┌───────────────────────────▼─────────────────────────────────────┐
+│           分布式缓存层(Redis哨兵集群)                           │
+│   实时额度计数器 | 路由配置缓存 | 分布式限流规则 | 会话数据       │
+└───────────────────────────┬─────────────────────────────────────┘
+                            │
+┌───────────────────────────▼─────────────────────────────────────┐
+│           持久化与可观测存储层                                   │
+│  ┌──────────┐  ┌──────────┐  ┌───────────────┐                │
+│  │ MySQL    │  │  ELK     │  │ Prometheus+    │                │
+│  │ 主从集群  │  │ 日志集群  │  │ Grafana监控   │                │
+│  └──────────┘  └──────────┘  └───────────────┘                │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 七、选型结论
+
+| 维度 | 结论 |
+|------|------|
+| **最优方案** | LiteLLM + NewAPI 组合架构 |
+| **核心优势** | 同时满足源码开放、Token运营、分布式扩容、社区可持续四项硬性约束 |
+| **风险可控性** | 双开源分层解耦,单一项目停更可单独替换,风险对冲 |
+| **落地性价比** | 短期依托NewAPI快速上线,长期依托LiteLLM支撑扩容,兼顾速度与架构 |
+| **对比淘汰项** | Sub2API/LLMProxy-Admin社区与架构缺陷明显,不满足集团级要求;自研成本过高 |
+
+**建议立即启动阶段一试点验证,3天内完成单机POC,为集团全面推广奠定基础。**

+ 215 - 0
docs/上传文件.md

@@ -0,0 +1,215 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://docs.dify.ai/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# 上传文件
+
+> 上传文件用于发送消息时使用,支持图片、文档、音频和视频的多模态理解。上传的文件仅供当前终端用户使用。
+
+
+
+## OpenAPI
+
+````yaml /zh/api-reference/openapi_completion.json post /files/upload
+openapi: 3.0.1
+info:
+  title: 文本生成型应用 API
+  description: 文本生成型应用提供无会话支持,适用于翻译、文章撰写、摘要总结 AI 等场景。
+  version: 1.0.0
+servers:
+  - url: https://{api_base_url}
+    description: 文本生成型应用 API 的基础 URL。自托管部署时,替换为你的 API 基础 URL。
+    variables:
+      api_base_url:
+        default: api.dify.ai/v1
+        description: API 基础 URL 的主机与路径,不含 `https://` 前缀。
+security:
+  - ApiKeyAuth: []
+tags:
+  - name: 文本生成
+    description: 文本生成相关操作。
+  - name: 文件操作
+    description: 文件管理相关操作。
+  - name: 终端用户
+    description: 终端用户信息相关操作。
+  - name: 消息反馈
+    description: 用户反馈相关操作。
+  - name: 语音与文字转换
+    description: 语音转文字和文字转语音相关操作。
+  - name: 应用配置
+    description: 获取应用设置和信息的操作。
+paths:
+  /files/upload:
+    post:
+      tags:
+        - 文件操作
+      summary: 上传文件
+      description: 上传文件用于发送消息时使用,支持图片、文档、音频和视频的多模态理解。上传的文件仅供当前终端用户使用。
+      operationId: uploadFile
+      requestBody:
+        description: 文件上传请求。需要 multipart/form-data 格式。
+        required: true
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              required:
+                - file
+                - user
+              properties:
+                file:
+                  type: string
+                  format: binary
+                  description: 要上传的文件。支持的类型包括图片、文档、音频和视频。
+                user:
+                  type: string
+                  description: >-
+                    用户标识符,由开发者定义的规则生成,必须在应用内唯一。Service API 与 WebApp 的用户 ID
+                    相互独立,即使取值相同也不指向同一用户。
+      responses:
+        '201':
+          description: 文件上传成功。
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/FileUploadResponse'
+              examples:
+                uploadSuccess:
+                  summary: 响应示例
+                  value:
+                    id: a1b2c3d4-5678-90ab-cdef-1234567890ab
+                    name: product-photo.png
+                    size: 204800
+                    extension: png
+                    mime_type: image/png
+                    created_by: f1e2d3c4-b5a6-7890-abcd-ef1234567890
+                    created_at: 1705407629
+                    preview_url: null
+                    source_url: null
+                    original_url: null
+                    user_id: f1e2d3c4-b5a6-7890-abcd-ef1234567890
+                    tenant_id: 11223344-5566-7788-99aa-bbccddeeff00
+                    conversation_id: null
+                    file_key: uploads/product-photo.png
+        '400':
+          description: |-
+            - `no_file_uploaded` : 请求中未提供文件。
+            - `too_many_files` : 每次请求仅允许上传一个文件。
+            - `filename_not_exists_error` : 上传的文件没有文件名。
+          content:
+            application/json:
+              examples:
+                no_file_uploaded:
+                  summary: no_file_uploaded
+                  value:
+                    status: 400
+                    code: no_file_uploaded
+                    message: Please upload your file.
+                too_many_files:
+                  summary: too_many_files
+                  value:
+                    status: 400
+                    code: too_many_files
+                    message: Only one file is allowed.
+                filename_not_exists_error:
+                  summary: filename_not_exists_error
+                  value:
+                    status: 400
+                    code: filename_not_exists_error
+                    message: The specified filename does not exist.
+        '413':
+          description: '`file_too_large` : 文件大小超出限制。'
+          content:
+            application/json:
+              examples:
+                file_too_large:
+                  summary: file_too_large
+                  value:
+                    status: 413
+                    code: file_too_large
+                    message: File size exceeded.
+        '415':
+          description: '`unsupported_file_type` : 不允许的文件类型。'
+          content:
+            application/json:
+              examples:
+                unsupported_file_type:
+                  summary: unsupported_file_type
+                  value:
+                    status: 415
+                    code: unsupported_file_type
+                    message: File type not allowed.
+components:
+  schemas:
+    FileUploadResponse:
+      type: object
+      properties:
+        id:
+          type: string
+          format: uuid
+          description: 唯一文件 ID。
+        name:
+          type: string
+          description: 文件名。
+        size:
+          type: integer
+          description: 文件大小(字节)。
+        extension:
+          type: string
+          nullable: true
+          description: 文件扩展名。
+        mime_type:
+          type: string
+          nullable: true
+          description: 文件的 MIME 类型。
+        created_by:
+          type: string
+          format: uuid
+          nullable: true
+          description: 上传文件的用户 ID。
+        created_at:
+          type: integer
+          format: int64
+          description: 上传时间戳(Unix 纪元秒)。
+        preview_url:
+          type: string
+          nullable: true
+          description: 文件的预览 URL。
+        source_url:
+          type: string
+          nullable: true
+          description: 文件的源 URL。
+        original_url:
+          type: string
+          nullable: true
+          description: 文件的原始 URL。
+        user_id:
+          type: string
+          format: uuid
+          nullable: true
+          description: 关联的用户 ID。
+        tenant_id:
+          type: string
+          format: uuid
+          nullable: true
+          description: 关联的租户 ID。
+        conversation_id:
+          type: string
+          format: uuid
+          nullable: true
+          description: 关联的会话 ID。
+        file_key:
+          type: string
+          nullable: true
+          description: 文件的存储键。
+  securitySchemes:
+    ApiKeyAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: API_KEY
+      description: >-
+        API Key 认证。对于所有 API 请求,请在 `Authorization` HTTP 头中包含您的 API Key,并加上
+        `Bearer ` 前缀。示例:`Authorization: Bearer {API_KEY}`。**强烈建议将 API Key
+        存储在服务端,不要在客户端共享或存储,以避免 API Key 泄漏导致严重后果。**
+
+````

+ 1074 - 0
docs/产品发展.md

@@ -0,0 +1,1074 @@
+我看了你这个用户侧雏形 `E:\project_file\ai-agentic-pt`,它现在已经不是“展示页”,而是一个很明确的 **用户智能体门户**:
+
+- 有登录与受保护路由。
+- 有广场 `Plaza`。
+- 有智能体推荐、热门、最新、系统智能体。
+- 有权限申请 `AgentApply`。
+- 有执行页 `AgentExecute`,已经接 Dify `/v1/info`、`/v1/parameters`、`/v1/workflows/run`、`/v1/chat-messages`。
+- 有“我的推荐/标记”能力。
+- 有历史页、可用智能体页,但这两块目前还是静态雏形。
+
+所以我会把你的整体产品定义成两端:
+
+```text
+管理平台:给管理员、运营、架构师、智能体开发者用
+用户平台:给普通业务用户、员工、客户、合作方使用智能体
+```
+
+更准确一点:
+
+```text
+管理平台 = 智能体控制台 / Agent Control Plane
+用户平台 = 智能体工作台 / Agent Workspace
+```
+
+这两个平台不要混在一起。你现在这个方向是对的。
+
+**总体定位**
+
+我建议你把 Dify 改造后的管理平台定位为:
+
+```text
+智能体的创建、发布、治理、调度、监控平台
+```
+
+把 `ai-agentic-pt` 定位为:
+
+```text
+用户发现、申请、使用、订阅、协作智能体的平台
+```
+
+二者关系应该像这样:
+
+```text
+管理平台负责生产和治理智能体
+用户平台负责消费和使用智能体
+```
+
+也就是:
+
+```text
+管理平台:
+谁能创建智能体?
+智能体如何配置?
+有哪些工具和知识?
+是否上架?
+谁能用?
+怎么审批?
+怎么监控?
+怎么计费?
+怎么审计?
+
+用户平台:
+我能用哪些智能体?
+哪些智能体适合我?
+怎么申请权限?
+怎么发起任务?
+怎么查看结果?
+怎么收藏常用智能体?
+怎么继续历史会话?
+怎么让智能体定期帮我做事?
+```
+
+这个边界非常重要。边界清楚,后面架构就不会打架。
+
+**一、用户平台应该怎么做**
+
+我建议用户平台不要做成“Dify 应用列表壳子”,而要做成 **企业级 AI 工作入口**。
+
+用户打开平台时,不应该只看到一堆智能体卡片,而应该感受到:
+
+```text
+这里是我使用所有 AI 能力的统一入口。
+```
+
+核心一级菜单建议是:
+
+```text
+首页
+广场
+我的智能体
+任务
+历史
+知识/文件
+审批
+个人设置
+```
+
+如果你的用户是企业内部员工,推荐结构:
+
+```text
+首页
+- 推荐给我的智能体
+- 最近使用
+- 待处理任务
+- 我的申请进度
+- 常用入口
+
+广场
+- 推荐
+- 热门
+- 最新
+- 按部门
+- 按场景
+- 按行业
+- 系统智能体
+- 外部智能体
+
+我的智能体
+- 已授权
+- 我收藏的
+- 最近使用
+- 即将过期
+- 申请中
+- 被拒绝
+
+任务
+- 我发起的任务
+- 定时任务
+- 待确认任务
+- 执行中
+- 失败任务
+- 任务结果
+
+历史
+- 对话历史
+- 工作流执行历史
+- 文件生成记录
+- 下载记录
+- 可复用结果
+
+审批
+- 我的申请
+- 待我审批,若用户也是主管
+- 审批记录
+
+个人设置
+- 个人资料
+- API Key
+- 通知偏好
+- 数据授权
+```
+
+你现在已经有:
+
+```text
+广场
+智能体
+可用智能体
+历史
+申请
+执行
+```
+
+我建议下一步把它整理成:
+
+```text
+首页 / Dashboard
+广场 / Plaza
+我的 / My Agents
+任务 / Tasks
+历史 / History
+```
+
+其中“智能体”侧边栏的二级推荐可以保留,但不要让它承担全部导航职责。
+
+**二、用户平台和管理平台怎么打通**
+
+最关键的是建立一套明确的发布和消费链路。
+
+我建议定义一条完整生命周期:
+
+```text
+管理员创建智能体
+-> 配置工具、知识、模型、权限
+-> 测试
+-> 设置上架信息
+-> 设置访问策略
+-> 发布到用户平台广场
+-> 用户发现
+-> 用户申请或直接使用
+-> 执行对话/工作流/任务
+-> 生成历史与结果
+-> 管理端查看运行、质量、成本、反馈
+-> 迭代新版本
+```
+
+这条链路里面,两个平台分别负责:
+
+```text
+管理平台:
+创建、配置、发布、权限、审批、监控、版本、审计
+
+用户平台:
+发现、申请、使用、收藏、任务、历史、反馈
+```
+
+中间需要一个业务层,不建议让用户平台直接强依赖 Dify 原始 Console API。
+
+你现在 `ai-agentic-pt` 已经有 `/business/*` 接口,这是正确方向。未来建议所有用户平台接口都走业务层:
+
+```text
+/business/apps/recommended
+/business/apps/hottest
+/business/apps/latest
+/business/apps/list
+/business/apps/:id/basic-info
+/business/apps/:id/access-status
+/business/apps/:id/access-requests
+/business/apps/:id/api-key
+/business/agent-marks
+/business/agent-pros
+```
+
+继续扩展时也保持这个风格:
+
+```text
+/business/me/agents
+/business/me/tasks
+/business/me/history
+/business/me/approvals
+/business/me/notifications
+/business/apps/:id/reviews
+/business/apps/:id/feedback
+/business/apps/:id/usage
+```
+
+不要让用户侧关心 Dify 里的 `app`、`workflow`、`advanced-chat`、`agent-chat` 这些内部实现太多。用户侧应该看到的是:
+
+```text
+智能体
+任务
+结果
+权限
+```
+
+而不是:
+
+```text
+Dify App
+Dify Workflow
+Dify API Key
+Dify Service API
+```
+
+这些可以在技术层存在,但产品层要隐藏。
+
+**三、用户平台最应该补齐的产品闭环**
+
+你现在已经有雏形,但还缺几个闭环。
+
+**1. 发现闭环**
+
+现在广场已经有推荐、热门、最新、系统智能体。下一步应该补:
+
+```text
+分类
+搜索
+标签
+适用部门
+适用场景
+权限状态
+是否已收藏
+是否可直接使用
+是否需要申请
+```
+
+智能体卡片上不要只展示名称和描述,建议展示:
+
+```text
+智能体名称
+一句话价值
+适用场景
+权限状态
+使用次数/热度
+评分或满意度
+最近更新
+创建方
+是否官方
+```
+
+卡片按钮根据状态变化:
+
+```text
+公开智能体:立即使用
+需申请:申请使用
+申请中:查看进度
+已授权:打开
+已拒绝:重新申请
+外部智能体:打开外部服务 / 申请集成权限
+```
+
+你现在已经在做 `getAppApiKey` 判断,这很好。但产品层最好不要每次点击才判断,可以列表接口直接返回:
+
+```json
+{
+  "access_status": "available | need_apply | pending | rejected | private",
+  "is_marked": true,
+  "can_execute": true
+}
+```
+
+这样用户不会点击后才突然跳申请页,体验更稳。
+
+**2. 权限申请闭环**
+
+`AgentApply` 已经做得有雏形了,有状态、有轮询、有重新申请。下一步建议补:
+
+```text
+申请理由模板
+申请用途
+使用频率
+所属项目
+数据范围
+期望有效期
+审批人
+审批 SLA
+审批记录
+被拒原因
+重新申请
+```
+
+申请不是单纯“我要用”,而是企业权限治理的一部分。
+
+权限模型建议至少有:
+
+```text
+public:公开,登录用户可用
+protected:需要申请
+department:部门内可用
+role_based:指定角色可用
+private:不可申请,仅管理员分配
+external:外部系统授权
+```
+
+当前你代码里有 `public / partial / private`,可以先用,但未来建议扩成上面的业务语义。
+
+**3. 使用闭环**
+
+`AgentExecute` 现在已经能执行 workflow 和 chat,这是非常关键的主功能。下一步要让它从“技术执行页”变成“用户工作台”。
+
+用户进入一个智能体后,应该看到:
+
+```text
+智能体介绍
+适合做什么
+输入区
+参数表单
+对话/执行区域
+历史结果
+常用模板
+收藏
+反馈
+重新运行
+导出结果
+分享结果
+创建定时任务
+```
+
+特别建议在执行页加两个能力:
+
+```text
+保存为任务
+设为定时任务
+```
+
+比如用户跑了一次“生成周报”,填好了参数。系统应该让用户:
+
+```text
+以后每周五 17:00 自动运行这个智能体,并把结果发给我。
+```
+
+这会直接把用户平台和你前面想做的“定时任务模块”连接起来。
+
+**4. 历史闭环**
+
+你现在 `History.jsx` 还是静态数据。这个页面很重要,不应该只是“历史记录列表”,而应该成为用户的 AI 工作成果库。
+
+建议历史分成:
+
+```text
+对话历史
+工作流执行历史
+任务结果
+生成文件
+失败记录
+收藏结果
+```
+
+每条历史至少有:
+
+```text
+智能体
+标题
+输入摘要
+输出摘要
+执行状态
+开始时间
+耗时
+消耗 token / 成本
+可查看详情
+可继续对话
+可重新运行
+可导出
+可反馈
+```
+
+后台对应需要记录:
+
+```text
+conversation_id
+workflow_run_id
+app_id
+user_id
+inputs
+outputs
+status
+error
+started_at
+ended_at
+elapsed
+metadata
+```
+
+不要只依赖 Dify 原始日志。你需要用户平台自己的业务历史表,用来做用户视角的聚合。
+
+**5. 任务闭环**
+
+这是我认为你下一步最有战略价值的一块。
+
+用户平台里的“任务”不是管理平台里的“调度配置”。用户视角应该是:
+
+```text
+我让哪个智能体帮我做了什么事?
+现在做到哪一步?
+结果在哪里?
+失败了怎么办?
+下次什么时候执行?
+```
+
+建议任务模块设计为:
+
+```text
+任务列表
+- 一次性任务
+- 定时任务
+- 事件触发任务
+- 等待我确认
+- 已完成
+- 失败
+
+任务详情
+- 任务目标
+- 绑定智能体
+- 输入参数
+- 执行计划
+- 当前状态
+- 执行日志
+- 输出结果
+- 失败原因
+- 重试
+- 暂停/恢复
+```
+
+这和管理平台的关系是:
+
+```text
+用户平台:用户创建和查看自己的任务
+管理平台:管理员治理全局任务、队列、失败、权限、审计
+```
+
+同一个任务系统,不同视角。
+
+**四、管理平台应该怎么支撑用户平台**
+
+为了让用户平台好用,管理平台不能只做“Dify 原始配置”。它要多出一层“上架运营能力”。
+
+建议在管理端给每个智能体增加这些字段:
+
+```text
+上架状态
+- 未上架
+- 内测
+- 已上架
+- 下架
+
+展示信息
+- 用户侧名称
+- 一句话介绍
+- 详细介绍
+- 图标
+- 分类
+- 标签
+- 适用人群
+- 示例问题
+- 输入示例
+- 输出示例
+- 使用说明
+
+权限策略
+- 公开
+- 申请制
+- 部门可见
+- 角色可见
+- 指定用户
+- 私有
+- 外部授权
+
+申请策略
+- 是否允许申请
+- 默认审批人
+- 是否自动审批
+- 有效期
+- 最大调用次数
+- 成本额度
+
+运营策略
+- 推荐分
+- 是否热门
+- 是否官方
+- 排序权重
+- 上架时间
+- 最新版本说明
+
+运行策略
+- 并发限制
+- 调用频率限制
+- 超时
+- 重试
+- 敏感操作确认
+
+质量策略
+- 评分
+- 反馈
+- 成功率
+- 平均响应时间
+- 最近失败率
+```
+
+这些字段不一定属于 Dify 原始 `apps` 表。更建议你单独建业务表,比如:
+
+```text
+business_agent_listing
+business_agent_access_policy
+business_agent_access_request
+business_agent_mark
+business_agent_usage_history
+business_agent_task
+business_agent_feedback
+business_agent_category
+```
+
+不要把用户平台运营字段硬塞进 Dify 核心表。那样未来升级 Dify 会很痛。
+
+**五、推荐的整体架构**
+
+我建议最终架构是:
+
+```text
+用户平台 ai-agentic-pt
+        |
+        | /business/*
+        v
+业务 API 层 / Portal API
+        |
+        | 读写业务表
+        v
+业务数据库
+        |
+        | 调用 / 代理
+        v
+Dify Console / Service API
+        |
+        v
+Dify Runtime / Workflow / Chat
+```
+
+更完整一点:
+
+```text
+[用户平台]
+- 广场
+- 我的智能体
+- 执行页
+- 申请
+- 任务
+- 历史
+- 反馈
+
+[业务 API 层]
+- 认证适配
+- 智能体上架
+- 权限申请
+- API Key 代理
+- 任务调度
+- 历史聚合
+- 用户推荐
+- 审批流
+- 通知
+
+[Dify 管理平台]
+- 创建应用/智能体
+- 配置模型
+- 配置工具
+- 配置知识库
+- 配置工作流
+- 监控日志
+
+[调度与执行]
+- Celery / Scheduler
+- Redis Queue
+- Dify Service API
+- Webhook / 外部系统
+
+[治理]
+- RBAC
+- 审计
+- 成本
+- 限流
+- 版本
+- 评测
+```
+
+重点是:**用户平台不要直接变成 Dify 的另一个皮肤,而要有自己的业务 API 和业务数据模型。**
+
+**六、你现在这个用户平台的具体建议**
+
+基于我看到的文件和功能,我建议这样推进。
+
+**第一步:先把信息架构收敛**
+
+现在路由是:
+
+```text
+/plaza
+/agents
+/history
+/available
+/agents/execute/:appId
+/agents/apply/:appId
+/agents/external/:agentProId
+```
+
+建议调整产品概念为:
+
+```text
+/plaza                  广场
+/my/agents              我的智能体
+/my/tasks               我的任务
+/my/history             历史记录
+/agents/:id             智能体详情
+/agents/:id/apply       申请使用
+/agents/:id/run         使用智能体
+/agents/external/:id    外部智能体
+```
+
+即使代码暂时不改,也建议产品文档和后端接口按这个心智设计。
+
+**第二步:把 `AvailableAgents` 从静态改成真实“我的智能体”**
+
+这个页面应该调用:
+
+```text
+GET /business/me/agents
+```
+
+返回:
+
+```json
+{
+  "id": "...",
+  "name": "...",
+  "description": "...",
+  "icon": "...",
+  "access_status": "available",
+  "expires_at": "...",
+  "last_used_at": "...",
+  "is_marked": true,
+  "usage_count": 12
+}
+```
+
+它应该合并:
+
+```text
+已授权智能体
+收藏智能体
+最近使用智能体
+申请中智能体
+```
+
+可以用 tabs:
+
+```text
+全部
+已授权
+收藏
+申请中
+即将过期
+```
+
+**第三步:补智能体详情页**
+
+现在广场点击后直接去执行或申请。这个路径很高效,但缺少“了解这个智能体”的中间页。
+
+建议新增:
+
+```text
+/agents/:id
+```
+
+详情页负责展示:
+
+```text
+介绍
+示例
+权限状态
+版本
+创建方
+使用说明
+评价
+最近更新
+使用按钮/申请按钮
+```
+
+用户从广场点卡片先到详情页。卡片上的主按钮可以直接“使用/申请”,但详情页必须有。
+
+这是平台感的关键,不然广场像一堆快捷方式。
+
+**第四步:把执行历史做成真实业务历史**
+
+目前 `AgentExecute` 里执行 Dify SSE,但用户历史页没有接真实数据。建议执行开始和结束时,业务层记录:
+
+```text
+POST /business/runs
+PATCH /business/runs/:id
+GET /business/me/runs
+GET /business/runs/:id
+```
+
+如果暂时不好全量代理 Dify SSE,至少在前端执行开始前创建一条业务 run,结束后把摘要、状态、耗时写回业务 API。
+
+长期更建议由后端代理 Dify 执行,这样历史、权限、审计、限流都能集中控制。
+
+**第五步:加入任务中心**
+
+这是你要做“面向用户智能体应用平台”的杀手锏。
+
+用户不是每次都主动打开平台聊天,很多场景是:
+
+```text
+每天给我生成日报
+每周总结销售线索
+当有新工单时自动分类
+当客户发邮件时自动生成回复草稿
+每小时巡检系统日志
+每月生成经营分析
+```
+
+用户平台应有:
+
+```text
+创建任务
+选择智能体
+填写输入
+选择触发方式
+选择输出方式
+确认权限
+查看执行结果
+```
+
+管理平台则有:
+
+```text
+任务模板
+调度策略
+队列监控
+失败重试
+全局审计
+```
+
+这两个是同一个任务系统的两张脸。
+
+**第六步:自然语言入口**
+
+用户平台也应该有自然语言入口,但和管理平台不同。
+
+管理平台自然语言入口是:
+
+```text
+帮我创建一个智能体
+帮我配置权限
+帮我发布到销售部门
+帮我加一个定时任务模板
+```
+
+用户平台自然语言入口是:
+
+```text
+我想做一个销售周报
+帮我找能处理合同审核的智能体
+以后每天 9 点提醒我看线索
+帮我把这份文件交给合适的智能体处理
+```
+
+用户平台的自然语言入口应该像一个 **智能体路由器 / AI 助理前台**:
+
+```text
+用户说需求
+-> 系统识别意图
+-> 推荐智能体
+-> 自动填充参数
+-> 发起执行/申请/任务
+```
+
+举例:
+
+> 我想分析一下这个月销售数据。
+
+系统应该回复:
+
+```text
+推荐使用:销售数据分析智能体
+需要上传:销售数据表
+可选输出:图表报告 / 管理摘要 / Excel 明细
+是否现在开始?
+```
+
+而不是让用户自己逛广场找卡片。
+
+**七、两个平台之间的对象模型建议**
+
+我建议你把“智能体”抽象成业务层统一对象,不要让用户平台直接区分太多 Dify 内部类型。
+
+```text
+Agent
+- id
+- source_type: dify_app | agent_pro | external | composite
+- source_id
+- name
+- description
+- icon
+- category
+- tags
+- owner
+- status
+- visibility
+- access_policy
+- listing_info
+- runtime_config
+```
+
+用户侧看到的是 `Agent`。
+
+管理侧可以看到更细:
+
+```text
+Dify App
+Workflow
+Chat App
+External Agent
+Agent Pro
+Composite Agent
+```
+
+权限对象:
+
+```text
+AgentAccess
+- user_id
+- agent_id
+- status
+- granted_by
+- granted_at
+- expires_at
+- quota
+- scope
+```
+
+申请对象:
+
+```text
+AgentAccessRequest
+- user_id
+- agent_id
+- reason
+- status
+- approver_id
+- rejected_reason
+- created_at
+- updated_at
+```
+
+运行对象:
+
+```text
+AgentRun
+- id
+- user_id
+- agent_id
+- task_id
+- mode
+- inputs
+- outputs
+- status
+- error
+- started_at
+- ended_at
+- cost
+- trace_id
+```
+
+任务对象:
+
+```text
+AgentTask
+- id
+- user_id
+- agent_id
+- title
+- trigger_type
+- schedule
+- inputs
+- output_target
+- status
+- next_run_at
+- last_run_at
+```
+
+反馈对象:
+
+```text
+AgentFeedback
+- run_id
+- user_id
+- agent_id
+- rating
+- comment
+- issue_type
+```
+
+这些对象是用户平台和管理平台共同的“语言”。
+
+**八、产品策略建议**
+
+我建议你不要一上来做“万能超级智能体平台”。先做一个很清楚的闭环:
+
+```text
+管理员发布智能体
+用户发现智能体
+用户申请权限
+用户使用智能体
+用户查看历史
+用户收藏智能体
+用户创建定时任务
+管理员看使用数据和反馈
+管理员迭代智能体
+```
+
+这条闭环跑通以后,再做自然语言创建和多智能体协作。
+
+推荐 MVP 优先级:
+
+```text
+P0
+- 广场真实列表
+- 智能体详情
+- 权限申请
+- 执行页稳定
+- 我的智能体
+- 真实历史记录
+
+P1
+- 收藏/推荐
+- 分类/搜索/标签
+- 使用反馈
+- 管理端上架配置
+- 管理端审批
+- 用户侧任务中心
+
+P2
+- 定时任务
+- 通知
+- 审批流
+- 成本/额度
+- 自然语言找智能体
+- 自然语言创建任务
+
+P3
+- 自然语言创建智能体
+- 多智能体团队
+- 任务模板市场
+- 行业方案
+- 自动评测和优化建议
+```
+
+**九、我最建议你现在做的架构决策**
+
+第一,保留两个平台,不要合并。  
+管理平台复杂、面向专业角色;用户平台轻、面向业务用户。合并会让普通用户看到太多配置,也会让管理端被用户体验约束。
+
+第二,抽出业务 API 层。  
+用户平台所有东西走 `/business/*`,不要直接调用 Dify Console API。Dify Service API 的执行可以短期前端直连,但长期最好后端代理。
+
+第三,智能体上架信息独立建模。  
+不要把广场、推荐、权限、申请、热度、评分这些都塞进 Dify 原生 app。你要做的是“Dify 之上的智能体运营层”。
+
+第四,任务中心从一开始按平台级能力设计。  
+不要只做“cron 页面”。用户侧叫“任务”,管理侧叫“调度/任务治理”,底层同一套任务模型。
+
+第五,自然语言入口分两类。  
+管理平台的自然语言是“创建和管理智能体”;用户平台的自然语言是“找到、使用、委托智能体”。
+
+第六,历史和审计要自己掌握。  
+这是平台价值沉淀的地方。只靠 Dify 内部日志,用户侧体验和管理侧治理都会受限。
+
+**十、推荐你形成的最终产品形态**
+
+我会把最终形态想成这样:
+
+```text
+用户打开 AI agentic 平台
+-> 首页看到推荐智能体、最近任务、待处理事项
+-> 可以在广场找智能体
+-> 可以用自然语言描述需求,让平台推荐智能体
+-> 可以申请权限
+-> 可以直接使用智能体
+-> 可以把一次执行保存为定时任务
+-> 可以在历史里查看所有 AI 工作成果
+-> 可以给结果反馈
+```
+
+管理员打开管理平台:
+
+```text
+创建智能体
+-> 绑定知识、工具、模型
+-> 设置用户侧展示信息
+-> 设置权限和审批
+-> 发布到广场
+-> 查看申请和使用数据
+-> 查看失败、成本、质量
+-> 调整版本
+-> 下架或灰度发布
+```
+
+这就是一个完整的“双平台智能体系统”:
+
+```text
+管理平台负责供给
+用户平台负责消费
+业务 API 负责连接
+任务系统负责持续执行
+治理系统负责安全可控
+```
+
+我的结论很明确:你现在的 `ai-agentic-pt` 应该继续做,而且应该把它从“智能体广场雏形”升级成“用户 AI 工作台”。管理平台不要承担用户使用体验,管理平台只负责创建、发布、治理和监控。两端通过统一的业务智能体模型、权限模型、任务模型、历史模型打通。
+
+如果下一步只选一个最重要的方向,我建议先做:
+
+```text
+智能体详情 + 我的智能体 + 真实历史 + 任务中心雏形
+```
+
+这四个补齐之后,用户平台就从“能点开智能体”变成“真的能日常工作”。

+ 712 - 0
docs/发送对话消息.md

@@ -0,0 +1,712 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://docs.dify.ai/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# 发送对话消息
+
+> 向对话型应用发送请求。
+
+
+
+## OpenAPI
+
+````yaml /zh/api-reference/openapi_chatflow.json post /chat-messages
+openapi: 3.0.1
+info:
+  title: 工作流编排对话型应用 API (对话流 API)
+  description: >-
+    对话流应用支持会话持久化,允许将之前的聊天记录作为响应的上下文。对话流应用使用 `advanced-chat`
+    模式,提供工作流级别的流式事件,用于详细的执行追踪,包括节点开始、完成、迭代和工作流生命周期。
+  version: 1.0.0
+servers:
+  - url: https://{api_base_url}
+    description: 工作流编排对话型应用 API 的基础 URL。自托管部署时,替换为你的 API 基础 URL。
+    variables:
+      api_base_url:
+        default: api.dify.ai/v1
+        description: API 基础 URL 的主机与路径,不含 `https://` 前缀。
+security:
+  - ApiKeyAuth: []
+tags:
+  - name: 对话流
+    description: 对话流应用的操作,包括对话消息、工作流执行详情和事件流。
+  - name: 文件操作
+    description: 文件上传和预览操作。
+  - name: 终端用户
+    description: 终端用户信息相关操作。
+  - name: 消息反馈
+    description: 用户反馈操作。
+  - name: 会话管理
+    description: 与管理会话相关的操作。
+  - name: 语音与文字转换
+    description: 文字转语音和语音转文字操作。
+  - name: 应用配置
+    description: 获取应用设置和信息的操作。
+  - name: 标注管理
+    description: 与管理标注直接回复相关的操作。
+  - name: 人工介入
+    description: 暂停等待人工输入的工作流恢复操作。
+paths:
+  /chat-messages:
+    post:
+      tags:
+        - 对话流
+      summary: 发送对话消息
+      description: 向对话型应用发送请求。
+      operationId: sendChatMessageCn
+      requestBody:
+        description: 发送对话消息的请求体。
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/ChatRequest'
+            examples:
+              streaming_example:
+                summary: 请求示例 - 流式模式
+                value:
+                  inputs:
+                    city: San Francisco
+                  query: What are the specs of the iPhone 13 Pro Max?
+                  response_mode: streaming
+                  conversation_id: ''
+                  user: abc-123
+                  files:
+                    - type: image
+                      transfer_method: remote_url
+                      url: https://cloud.dify.ai/logo/logo-site.png
+              blocking_example:
+                summary: 请求示例 - 阻塞模式
+                value:
+                  inputs: {}
+                  query: What are the specs of the iPhone 13 Pro Max?
+                  response_mode: blocking
+                  conversation_id: 45701982-8118-4bc5-8e9b-64562b4555f2
+                  user: abc-123
+      responses:
+        '200':
+          description: >-
+            请求成功。内容类型和结构取决于请求中的 `response_mode` 参数。
+
+
+            - 如果 `response_mode` 为 `blocking`,返回 `application/json` 和
+            `ChatCompletionResponse` 对象。
+
+            - 如果 `response_mode` 为 `streaming`,返回 `text/event-stream` 和服务器发送事件流。
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ChatCompletionResponse'
+              examples:
+                blockingResponse:
+                  summary: 响应示例 - 阻塞模式
+                  value:
+                    event: message
+                    task_id: c3800678-a077-43df-a102-53f23ed20b88
+                    id: b01a39de-3480-4f3e-9f1e-4841a80f8e5e
+                    message_id: 9da23599-e713-473b-982c-4328d4f5c78a
+                    conversation_id: 45701982-8118-4bc5-8e9b-64562b4555f2
+                    mode: advanced-chat
+                    answer: iPhone 13 Pro Max specs are listed here:...
+                    metadata:
+                      usage:
+                        prompt_tokens: 1033
+                        prompt_unit_price: '0.001'
+                        prompt_price_unit: '0.001'
+                        prompt_price: '0.0010330'
+                        completion_tokens: 128
+                        completion_unit_price: '0.002'
+                        completion_price_unit: '0.001'
+                        completion_price: '0.0002560'
+                        total_tokens: 1161
+                        total_price: '0.0012890'
+                        currency: USD
+                        latency: 0.7682376249867957
+                      retriever_resources:
+                        - position: 1
+                          dataset_id: 101b4c97-fc2e-463c-90b1-5261a4cdcafb
+                          dataset_name: iPhone
+                          document_id: 8dd1ad74-0b5f-4175-b735-7d98bbbb4e00
+                          document_name: iPhone List
+                          segment_id: ed599c7f-2766-4294-9d1d-e5235a61270a
+                          score: 0.98457545
+                          content: >-
+                            "Model","Release Date","Display
+                            Size","Resolution","Processor","RAM","Storage","Camera","Battery","Operating
+                            System" "iPhone 13 Pro Max","September 24,
+                            2021","6.7 inch","1284 x 2778","Hexa-core (2x3.23
+                            GHz Avalanche + 4x1.82 GHz Blizzard)","6 GB","128,
+                            256, 512 GB, 1TB","12 MP","4352 mAh","iOS 15"
+                    created_at: 1705407629
+            text/event-stream:
+              schema:
+                type: string
+                description: >-
+                  服务器发送事件 (SSE) 流。
+
+
+                  **解析**:每个事件是一行以 `data: ` 为前缀的 JSON 对象,以 `\n\n` 终止。解析 JSON 前先去除
+                  `data: ` 前缀,再读取 `event` 字段确定事件类型。忽略 `ping` 事件,它以 `event: ping`
+                  行的形式(无 `data:` 载荷)每 10 秒发送一次以保持连接活跃。
+
+
+                  **流生命周期**:工作流进度以 `workflow_started`、节点事件(`node_started` 和
+                  `node_finished`,以及迭代和循环的相应变体)流式返回,回复则以 `message`
+                  事件返回。结束序列取决于运行结果:
+
+                  - **成功**:`message_end`,随后 `workflow_finished`
+
+                  - **失败**:`workflow_finished`(状态为 `failed`),随后 `error`;不发送
+                  `message_end`
+
+                  - **暂停**:`human_input_required`,随后
+                  `workflow_paused`(流到此结束,运行将另行恢复)
+
+
+                  开启文本转语音(TTS)自动播放时,`tts_message_end` 跟随在结束事件之后。
+
+
+                  **事件**:除 `ping` 外,每个事件都包含 `conversation_id`、`message_id` 和
+                  `created_at`(Unix 纪元秒);除 `error` 外,其余事件还包含
+                  `task_id`。工作流、节点和人工介入事件还将载荷嵌套在 `data` 中,且除 `agent_log` 外都带有顶层
+                  `workflow_run_id`。
+
+
+                  **回复事件**
+
+
+                  | 事件 | 触发时机 | 关键字段 |
+
+                  |:---|:---|:---|
+
+                  | `message` | 每个回答片段(按顺序拼接) | `answer` |
+
+                  | `message_replace` | 内容审核替换已生成的回答 | `answer`、`reason` |
+
+                  | `message_end` | 回答完成 |
+                  `metadata`(`usage`、`retriever_resources`) |
+
+                  | `tts_message`、`tts_message_end` | 音频片段/结束,开启 TTS 自动播放时 |
+                  `audio` |
+
+
+                  **工作流与节点事件**
+
+
+                  每个事件都将载荷嵌套在 `data` 对象中。
+
+
+                  | 事件 | 触发时机 | 关键 `data` 字段 |
+
+                  |:---|:---|:---|
+
+                  | `workflow_started` | 运行开始 | `inputs` |
+
+                  | `node_started` | 节点开始 | `node_id`、`node_type`、`title` |
+
+                  | `node_finished` | 节点结束 |
+                  `status`、`outputs`、`execution_metadata` |
+
+                  | `node_retry` | 节点失败后重试 | `retry_index` |
+
+                  | `iteration_started`、`iteration_next`、`iteration_completed` |
+                  迭代节点进度(信息性,可不处理) | `data` |
+
+                  | `loop_started`、`loop_next`、`loop_completed` |
+                  循环节点进度(信息性,可不处理) | `data` |
+
+                  | `agent_log` | Agent 节点步骤日志(信息性,可不处理;无 `workflow_run_id`) |
+                  `data` |
+
+                  | `workflow_finished` | 运行结束 |
+                  `status`(`succeeded`、`failed`、`partial-succeeded`、`stopped`)、`outputs`、`total_tokens`
+                  |
+
+                  | `workflow_paused` | 运行暂停 | `paused_nodes`、`reasons` |
+
+                  | `human_input_required` | 运行到达人工介入节点 |
+                  `form_token`、`form_content`、`expiration_time` |
+
+
+                  暂停后,本流在 `workflow_paused` 处结束。通过
+                  [提交人工介入表单](/api-reference/人工介入/提交人工介入表单)
+                  提交表单,或等待其超时;恢复后的运行(包括从
+                  `human_input_form_filled`/`human_input_form_timeout` 到
+                  `workflow_finished` 的事件)由
+                  [流式获取工作流事件](/api-reference/对话流/流式获取工作流事件) 流式返回。
+
+
+                  **传输事件**
+
+
+                  | 事件 | 触发时机 | 关键字段 |
+
+                  |:---|:---|:---|
+
+                  | `error` | 失败导致流结束;HTTP 仍为 `200` | `status`(如 `400`)、`code`(如
+                  `invalid_param`)、`message` |
+
+                  | `ping` | 每 10 秒保持连接 | 无 |
+              examples:
+                streamingResponseBasic:
+                  summary: Response Example - Streaming (Basic)
+                  value: >-
+                    data: {"event": "message", "task_id":"mock_task_id",
+                    "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290",
+                    "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2",
+                    "answer": " I", "created_at": 1679586595} data: {"event":
+                    "message_end", "task_id":"mock_task_id", "message_id":
+                    "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id":
+                    "45701982-8118-4bc5-8e9b-64562b4555f2", "metadata":
+                    {"usage": {"total_tokens": 10, "latency": 1.0}}}
+                streamingResponseWorkflow:
+                  summary: Response Example - Streaming (工作流)
+                  value: >-
+                    data: {"event": "workflow_started", "task_id": "task123",
+                    "workflow_run_id": "wfr_abc123", "message_id": "msg123",
+                    "conversation_id": "conv123", "data": {"id": "wfr_abc123",
+                    "workflow_id": "wf_def456", "inputs": {"city": "San
+                    Francisco"}, "created_at": 1705395332}} data: {"event":
+                    "node_started", "task_id": "task123", "workflow_run_id":
+                    "wfr_abc123", "message_id": "msg123", "conversation_id":
+                    "conv123", "data": {"id": "ne_001", "node_id": "node_llm_1",
+                    "node_type": "llm", "title": "LLM", "index": 1,
+                    "created_at": 1705395332}} data: {"event": "message",
+                    "task_id": "task123", "message_id": "msg123",
+                    "conversation_id": "conv123", "answer": " I", "created_at":
+                    1705395333} data: {"event": "node_finished", "task_id":
+                    "task123", "workflow_run_id": "wfr_abc123", "message_id":
+                    "msg123", "conversation_id": "conv123", "data": {"id":
+                    "ne_001", "node_id": "node_llm_1", "node_type": "llm",
+                    "title": "LLM", "index": 1, "status": "succeeded",
+                    "elapsed_time": 1.5, "created_at": 1705395332,
+                    "finished_at": 1705395334}} data: {"event": "message_end",
+                    "task_id": "task123", "message_id": "msg123",
+                    "conversation_id": "conv123", "metadata": {"usage":
+                    {"total_tokens": 50, "latency": 2.5}}} data: {"event":
+                    "workflow_finished", "task_id": "task123",
+                    "workflow_run_id": "wfr_abc123", "message_id": "msg123",
+                    "conversation_id": "conv123", "data": {"id": "wfr_abc123",
+                    "workflow_id": "wf_def456", "status": "succeeded",
+                    "elapsed_time": 2.5, "total_tokens": 50, "total_steps": 2,
+                    "created_at": 1705395332, "finished_at": 1705395335}}
+                humanInputPause:
+                  summary: 响应示例 - 人工介入暂停
+                  value: >-
+                    data: {"event": "workflow_started", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "message_id":
+                    "2e4f6a8b-1c3d-5e7f-9a0b-2c4d6e8f0a1b", "conversation_id":
+                    "9d3a2f1b-6c7d-4e8f-a0b1-c2d3e4f5a6b7", "created_at":
+                    1705407629, "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data": {"id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "workflow_id":
+                    "7c3e33d4-2a8b-4e5f-9b1a-d3c6e8f12345", "inputs": {"draft":
+                    "Hello"}, "created_at": 1705407629, "reason": "initial"}}
+                    data: {"event": "human_input_required", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "message_id":
+                    "2e4f6a8b-1c3d-5e7f-9a0b-2c4d6e8f0a1b", "conversation_id":
+                    "9d3a2f1b-6c7d-4e8f-a0b1-c2d3e4f5a6b7", "created_at":
+                    1705407629, "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data": {"form_id":
+                    "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "form_token":
+                    "tok_abc123", "node_id": "approval_node", "node_title":
+                    "Approval", "form_content": "Please review the draft.",
+                    "inputs": [{"type": "text_input", "output_variable_name":
+                    "comment", "default": null}], "actions": [{"id": "approve",
+                    "title": "Approve", "button_style": "primary"}],
+                    "display_in_ui": false, "resolved_default_values":
+                    {"comment": ""}, "expiration_time": 1705494029}} data:
+                    {"event": "workflow_paused", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "message_id":
+                    "2e4f6a8b-1c3d-5e7f-9a0b-2c4d6e8f0a1b", "conversation_id":
+                    "9d3a2f1b-6c7d-4e8f-a0b1-c2d3e4f5a6b7", "created_at":
+                    1705407629, "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data":
+                    {"workflow_run_id": "fb47b2e6-5e43-4f90-be01-d5c5a088d156",
+                    "paused_nodes": ["approval_node"], "outputs": {}, "reasons":
+                    [{"TYPE": "human_input_required", "form_id":
+                    "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "form_content":
+                    "Please review the draft.", "inputs": [{"type":
+                    "text_input", "output_variable_name": "comment", "default":
+                    null}], "actions": [{"id": "approve", "title": "Approve",
+                    "button_style": "primary"}], "node_id": "approval_node",
+                    "node_title": "Approval", "resolved_default_values":
+                    {"comment": ""}, "form_token": "tok_abc123",
+                    "expiration_time": 1705494029}], "status": "paused",
+                    "created_at": 1705407629, "elapsed_time": 0.5,
+                    "total_tokens": 0, "total_steps": 1}}
+        '400':
+          description: |-
+            - `app_unavailable` : 应用不可用或配置错误。
+            - `not_chat_app` : App mode does not match the API route.
+            - `conversation_completed` : The conversation has ended.
+            - `provider_not_initialize` : 未找到有效的模型提供商凭据。
+            - `provider_quota_exceeded` : 模型提供商配额已用尽。
+            - `model_currently_not_support` : 当前模型不可用。
+            - `completion_request_error` : 文本生成失败。
+            - `bad_request` : Cannot use draft 工作流 version.
+            - `bad_request` : Invalid `workflow_id` format.
+          content:
+            application/json:
+              examples:
+                app_unavailable:
+                  summary: app_unavailable
+                  value:
+                    status: 400
+                    code: app_unavailable
+                    message: App unavailable, please check your app configurations.
+                not_chat_app:
+                  summary: not_chat_app
+                  value:
+                    status: 400
+                    code: not_chat_app
+                    message: Please check if your app mode matches the right API route.
+                conversation_completed:
+                  summary: conversation_completed
+                  value:
+                    status: 400
+                    code: conversation_completed
+                    message: >-
+                      The conversation has ended. Please start a new
+                      conversation.
+                provider_not_initialize:
+                  summary: provider_not_initialize
+                  value:
+                    status: 400
+                    code: provider_not_initialize
+                    message: >-
+                      No valid model provider credentials found. Please go to
+                      Settings -> Model Provider to complete your provider
+                      credentials.
+                provider_quota_exceeded:
+                  summary: provider_quota_exceeded
+                  value:
+                    status: 400
+                    code: provider_quota_exceeded
+                    message: >-
+                      Your quota for Dify Hosted OpenAI has been exhausted.
+                      Please go to Settings -> Model Provider to complete your
+                      own provider credentials.
+                model_currently_not_support:
+                  summary: model_currently_not_support
+                  value:
+                    status: 400
+                    code: model_currently_not_support
+                    message: >-
+                      Dify Hosted OpenAI trial currently not support the GPT-4
+                      model.
+                completion_request_error:
+                  summary: completion_request_error
+                  value:
+                    status: 400
+                    code: completion_request_error
+                    message: Completion request failed.
+                is_draft_workflow:
+                  summary: bad_request
+                  value:
+                    status: 400
+                    code: bad_request
+                    message: >-
+                      Cannot use draft workflow version. Workflow ID:
+                      a1b2c3d4-5678-90ab-cdef-1234567890ab. 
+                workflow_id_format_error:
+                  summary: bad_request
+                  value:
+                    status: 400
+                    code: bad_request
+                    message: 'Invalid workflow_id format: ''not-a-valid-id''. '
+        '404':
+          description: |-
+            - `not_found` : Conversation does not exist.
+            - `not_found` : 工作流 not found with the specified `workflow_id`.
+          content:
+            application/json:
+              examples:
+                conversation_not_exists:
+                  summary: not_found
+                  value:
+                    status: 404
+                    code: not_found
+                    message: Conversation Not Exists.
+                workflow_not_found:
+                  summary: not_found
+                  value:
+                    status: 404
+                    code: not_found
+                    message: >-
+                      Workflow not found with id:
+                      a1b2c3d4-5678-90ab-cdef-1234567890ab
+        '429':
+          description: |-
+            - `too_many_requests` : 该应用的并发请求过多。
+            - `rate_limit_error` : 上游模型提供商的速率限制已超出。
+          content:
+            application/json:
+              examples:
+                too_many_requests:
+                  summary: too_many_requests
+                  value:
+                    status: 429
+                    code: too_many_requests
+                    message: Too many requests. Please try again later.
+                rate_limit_error:
+                  summary: rate_limit_error
+                  value:
+                    status: 429
+                    code: rate_limit_error
+                    message: Rate Limit Error
+        '500':
+          description: '`internal_server_error` : 内部服务器错误。'
+          content:
+            application/json:
+              examples:
+                internal_server_error:
+                  summary: internal_server_error
+                  value:
+                    status: 500
+                    code: internal_server_error
+                    message: Internal server error.
+components:
+  schemas:
+    ChatRequest:
+      type: object
+      required:
+        - inputs
+        - query
+        - user
+      properties:
+        query:
+          type: string
+          description: 用户输入/问题内容。
+        inputs:
+          type: object
+          description: >-
+            允许输入应用定义的各种变量值。包含键值对。请参阅 [获取应用参数](/api-reference/应用配置/获取应用参数) 响应中的
+            `user_input_form` 字段,以了解应用所需的变量名称和类型。
+          additionalProperties: true
+        response_mode:
+          type: string
+          enum:
+            - streaming
+            - blocking
+          description: >-
+            响应返回模式。`streaming`(推荐)使用 SSE。`blocking`
+            等待完成后返回(长时间处理可能会被中断)。Cloudflare 超时为 `100 s`。省略时默认为阻塞模式。
+        user:
+          type: string
+          description: 用户标识符,在应用内唯一。此标识符用于限定数据访问范围——会话、消息和文件仅在使用相同的 `user` 值查询时可见。
+        conversation_id:
+          type: string
+          description: >-
+            用于继续会话的会话 ID。传入上一条消息的 `conversation_id`。要开始新会话,请省略此字段或传入空字符串。响应会返回一个
+            `conversation_id`——在后续消息中传入该值即可继续该会话。
+        files:
+          type: array
+          description: >-
+            用于多模态理解的文件列表,包括图片、文档、音频和视频。要附加本地文件,请先通过
+            [上传文件](/api-reference/文件操作/上传文件) 上传,然后将返回的 `id` 作为
+            `upload_file_id`,并设置 `transfer_method: local_file`。
+          items:
+            type: object
+            required:
+              - type
+              - transfer_method
+            properties:
+              type:
+                type: string
+                enum:
+                  - image
+                  - document
+                  - audio
+                  - video
+                  - custom
+                description: 文件类型。
+              transfer_method:
+                type: string
+                enum:
+                  - remote_url
+                  - local_file
+                description: 传输方式:文件 URL 使用 `remote_url`,上传文件使用 `local_file`。
+              url:
+                type: string
+                format: url
+                description: 文件 URL(当 `transfer_method` 为 `remote_url` 时必填)。
+              upload_file_id:
+                type: string
+                description: >-
+                  通过 [上传文件](/api-reference/文件操作/上传文件) API 获取的已上传文件 ID(当
+                  `transfer_method` 为 `local_file` 时必填)。
+        auto_generate_name:
+          type: boolean
+          description: >-
+            自动生成会话标题。如果为 `false`,请使用 [重命名会话](/api-reference/会话管理/重命名会话) API 并设置
+            `auto_generate: true` 来异步生成标题。
+          default: true
+        workflow_id:
+          type: string
+          description: 指定要执行的已发布工作流版本 ID。如果未提供,将使用最新发布的版本。
+    ChatCompletionResponse:
+      type: object
+      properties:
+        event:
+          type: string
+          description: 事件类型,固定为 `message`。
+        task_id:
+          type: string
+          format: uuid
+          description: 用于请求追踪和停止响应 API 的任务 ID。
+        id:
+          type: string
+          format: uuid
+          description: 该响应事件的唯一 ID。
+        message_id:
+          type: string
+          format: uuid
+          description: 唯一的消息 ID。调用反馈或推荐问题接口时,将此值作为 `message_id` 参数使用。
+        conversation_id:
+          type: string
+          format: uuid
+          description: 会话 ID。
+        mode:
+          type: string
+          description: 应用模式,固定为 `advanced-chat`。
+        answer:
+          type: string
+          description: 完整的响应内容。
+        metadata:
+          type: object
+          description: 包含用量和检索资源的元数据。
+          properties:
+            usage:
+              $ref: '#/components/schemas/Usage'
+            retriever_resources:
+              type: array
+              description: 使用的检索资源列表。
+              items:
+                $ref: '#/components/schemas/RetrieverResource'
+        created_at:
+          type: integer
+          format: int64
+          description: 消息创建时间戳(Unix 纪元秒)。
+    Usage:
+      type: object
+      description: 模型使用信息。
+      properties:
+        prompt_tokens:
+          type: integer
+          description: 提示词中的令牌数。
+        prompt_unit_price:
+          type: string
+          format: decimal
+          description: 每个提示词令牌的单价。
+        prompt_price_unit:
+          type: string
+          format: decimal
+          description: 提示词令牌的价格单位。
+        prompt_price:
+          type: string
+          format: decimal
+          description: 提示词令牌的总价。
+        completion_tokens:
+          type: integer
+          description: 补全内容中的令牌数。
+        completion_unit_price:
+          type: string
+          format: decimal
+          description: 每个补全令牌的单价。
+        completion_price_unit:
+          type: string
+          format: decimal
+          description: 补全令牌的价格单位。
+        completion_price:
+          type: string
+          format: decimal
+          description: 补全令牌的总价。
+        total_tokens:
+          type: integer
+          description: 使用的令牌总数。
+        total_price:
+          type: string
+          format: decimal
+          description: 所有令牌的总价。
+        currency:
+          type: string
+          description: 计价货币。
+        latency:
+          type: number
+          format: double
+          description: 延迟时间(秒)。
+    RetrieverResource:
+      type: object
+      description: 检索资源的引用和归属信息。
+      properties:
+        id:
+          type: string
+          format: uuid
+          description: 检索资源的唯一 ID。
+        message_id:
+          type: string
+          format: uuid
+          description: 该资源所属消息的 ID。
+        position:
+          type: integer
+          description: 资源在列表中的位置。
+        dataset_id:
+          type: string
+          format: uuid
+          description: 知识库 ID。
+        dataset_name:
+          type: string
+          description: 知识库名称。
+        document_id:
+          type: string
+          format: uuid
+          description: 文档 ID。
+        document_name:
+          type: string
+          description: 文档名称。
+        data_source_type:
+          type: string
+          description: 数据源类型。
+        segment_id:
+          type: string
+          format: uuid
+          description: 文档中特定块的 ID。
+        score:
+          type: number
+          format: float
+          description: 资源的相关性评分。
+        hit_count:
+          type: integer
+          description: 该块被命中的次数。
+        word_count:
+          type: integer
+          description: 块的字数。
+        segment_position:
+          type: integer
+          description: 块在文档中的位置。
+        index_node_hash:
+          type: string
+          description: 索引节点的哈希值。
+        content:
+          type: string
+          description: 资源的内容片段。
+        summary:
+          type: string
+          nullable: true
+          description: 块内容的摘要。
+        created_at:
+          type: integer
+          format: int64
+          description: 创建时间戳(Unix 纪元秒)。
+  securitySchemes:
+    ApiKeyAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: API_KEY
+      description: >-
+        API Key 认证。对于所有 API 请求,请在 `Authorization` HTTP 头中包含您的 API Key,并加上
+        `Bearer ` 前缀。示例:`Authorization: Bearer {API_KEY}`。**强烈建议将 API Key
+        存储在服务端,不要在客户端共享或存储,以避免 API Key 泄漏导致严重后果。**
+
+````

+ 319 - 0
docs/应用配置获取应用参数.md

@@ -0,0 +1,319 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://docs.dify.ai/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# 获取应用参数
+
+> 获取应用的输入表单配置,包括功能开关、输入参数名称、类型和默认值。
+
+
+
+## OpenAPI
+
+````yaml /zh/api-reference/openapi_completion.json get /parameters
+openapi: 3.0.1
+info:
+  title: 文本生成型应用 API
+  description: 文本生成型应用提供无会话支持,适用于翻译、文章撰写、摘要总结 AI 等场景。
+  version: 1.0.0
+servers:
+  - url: https://{api_base_url}
+    description: 文本生成型应用 API 的基础 URL。自托管部署时,替换为你的 API 基础 URL。
+    variables:
+      api_base_url:
+        default: api.dify.ai/v1
+        description: API 基础 URL 的主机与路径,不含 `https://` 前缀。
+security:
+  - ApiKeyAuth: []
+tags:
+  - name: 文本生成
+    description: 文本生成相关操作。
+  - name: 文件操作
+    description: 文件管理相关操作。
+  - name: 终端用户
+    description: 终端用户信息相关操作。
+  - name: 消息反馈
+    description: 用户反馈相关操作。
+  - name: 语音与文字转换
+    description: 语音转文字和文字转语音相关操作。
+  - name: 应用配置
+    description: 获取应用设置和信息的操作。
+paths:
+  /parameters:
+    get:
+      tags:
+        - 应用配置
+      summary: 获取应用参数
+      description: 获取应用的输入表单配置,包括功能开关、输入参数名称、类型和默认值。
+      operationId: getAppParameters
+      responses:
+        '200':
+          description: 应用参数信息。
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CompletionAppParametersResponse'
+              examples:
+                appParameters:
+                  summary: 响应示例
+                  value:
+                    opening_statement: Hello! How can I help you today?
+                    suggested_questions:
+                      - What can you do?
+                      - Tell me about your features.
+                    suggested_questions_after_answer:
+                      enabled: true
+                    speech_to_text:
+                      enabled: false
+                    text_to_speech:
+                      enabled: false
+                      voice: alloy
+                      language: en-US
+                      autoPlay: disabled
+                    retriever_resource:
+                      enabled: true
+                    annotation_reply:
+                      enabled: false
+                    more_like_this:
+                      enabled: false
+                    sensitive_word_avoidance:
+                      enabled: false
+                    user_input_form:
+                      - text-input:
+                          label: City
+                          variable: city
+                          required: true
+                          default: ''
+                    file_upload:
+                      image:
+                        enabled: true
+                        number_limits: 3
+                        detail: high
+                        transfer_methods:
+                          - remote_url
+                          - local_file
+                    system_parameters:
+                      file_size_limit: 15
+                      image_file_size_limit: 10
+                      audio_file_size_limit: 50
+                      video_file_size_limit: 100
+                      workflow_file_upload_limit: 10
+        '400':
+          description: '`app_unavailable` : 应用不可用或配置错误。'
+          content:
+            application/json:
+              examples:
+                app_unavailable:
+                  summary: app_unavailable
+                  value:
+                    status: 400
+                    code: app_unavailable
+                    message: App unavailable, please check your app configurations.
+components:
+  schemas:
+    CompletionAppParametersResponse:
+      type: object
+      properties:
+        opening_statement:
+          type: string
+          description: 开始时显示的开场白。
+        suggested_questions:
+          type: array
+          items:
+            type: string
+          description: 建议的初始问题列表。
+        suggested_questions_after_answer:
+          type: object
+          description: 回答后建议问题的配置。
+          properties:
+            enabled:
+              type: boolean
+              description: 是否启用此功能。
+        speech_to_text:
+          type: object
+          description: 语音转文字配置。
+          properties:
+            enabled:
+              type: boolean
+              description: 是否启用此功能。
+        text_to_speech:
+          type: object
+          description: 文字转语音配置。
+          properties:
+            enabled:
+              type: boolean
+              description: 是否启用此功能。
+            voice:
+              type: string
+              description: TTS 的声音标识符。
+            language:
+              type: string
+              description: TTS 的语言。
+            autoPlay:
+              type: string
+              enum:
+                - enabled
+                - disabled
+              description: 自动播放设置。
+        retriever_resource:
+          type: object
+          description: 检索资源配置。
+          properties:
+            enabled:
+              type: boolean
+              description: 是否启用此功能。
+        annotation_reply:
+          type: object
+          description: 标注回复配置。
+          properties:
+            enabled:
+              type: boolean
+              description: 是否启用此功能。
+        more_like_this:
+          type: object
+          description: 更多类似内容配置。
+          properties:
+            enabled:
+              type: boolean
+              description: 是否启用此功能。
+        sensitive_word_avoidance:
+          type: object
+          description: 敏感词过滤配置。
+          properties:
+            enabled:
+              type: boolean
+              description: 是否启用此功能。
+        user_input_form:
+          type: array
+          description: 用户输入表单元素列表。
+          items:
+            $ref: '#/components/schemas/UserInputFormItem'
+        file_upload:
+          type: object
+          description: 文件上传配置。
+          properties:
+            image:
+              type: object
+              description: 图片上传设置。
+              properties:
+                enabled:
+                  type: boolean
+                  description: 是否启用图片上传。
+                number_limits:
+                  type: integer
+                  description: 最大图片数量。
+                detail:
+                  type: string
+                  description: 图片精细程度。
+                transfer_methods:
+                  type: array
+                  items:
+                    type: string
+                    enum:
+                      - remote_url
+                      - local_file
+                  description: 允许的传输方式。
+        system_parameters:
+          type: object
+          description: 系统级参数和限制。
+          properties:
+            file_size_limit:
+              type: integer
+              description: 最大文件大小(MB)。
+            image_file_size_limit:
+              type: integer
+              description: 最大图片文件大小(MB)。
+            audio_file_size_limit:
+              type: integer
+              description: 最大音频文件大小(MB)。
+            video_file_size_limit:
+              type: integer
+              description: 最大视频文件大小(MB)。
+            workflow_file_upload_limit:
+              type: integer
+              description: 工作流文件上传的最大文件数。
+    UserInputFormItem:
+      type: object
+      oneOf:
+        - $ref: '#/components/schemas/TextInputControlWrapper'
+        - $ref: '#/components/schemas/ParagraphControlWrapper'
+        - $ref: '#/components/schemas/SelectControlWrapper'
+    TextInputControlWrapper:
+      title: Text Input
+      type: object
+      properties:
+        text-input:
+          $ref: '#/components/schemas/TextInputControl'
+    ParagraphControlWrapper:
+      title: Paragraph
+      type: object
+      properties:
+        paragraph:
+          $ref: '#/components/schemas/ParagraphControl'
+    SelectControlWrapper:
+      title: Select
+      type: object
+      properties:
+        select:
+          $ref: '#/components/schemas/SelectControl'
+    TextInputControl:
+      type: object
+      properties:
+        label:
+          type: string
+          description: 表单字段的显示标签。
+        variable:
+          type: string
+          description: 用作 `inputs` 对象中键的变量名。
+        required:
+          type: boolean
+          description: 提交前是否必须填写此字段。
+        default:
+          type: string
+          description: 表单字段中预填的默认值。
+    ParagraphControl:
+      type: object
+      properties:
+        label:
+          type: string
+          description: 表单字段的显示标签。
+        variable:
+          type: string
+          description: 用作 `inputs` 对象中键的变量名。
+        required:
+          type: boolean
+          description: 提交前是否必须填写此字段。
+        default:
+          type: string
+          description: 表单字段中预填的默认值。
+    SelectControl:
+      type: object
+      properties:
+        label:
+          type: string
+          description: 表单字段的显示标签。
+        variable:
+          type: string
+          description: 用作 `inputs` 对象中键的变量名。
+        required:
+          type: boolean
+          description: 提交前是否必须做出选择。
+        default:
+          type: string
+          description: 默认选中的值。
+        options:
+          type: array
+          items:
+            type: string
+          description: 此表单控件的可选值列表。
+  securitySchemes:
+    ApiKeyAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: API_KEY
+      description: >-
+        API Key 认证。对于所有 API 请求,请在 `Authorization` HTTP 头中包含您的 API Key,并加上
+        `Bearer ` 前缀。示例:`Authorization: Bearer {API_KEY}`。**强烈建议将 API Key
+        存储在服务端,不要在客户端共享或存储,以避免 API Key 泄漏导致严重后果。**
+
+````

+ 443 - 0
docs/执行工作流.md

@@ -0,0 +1,443 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://docs.dify.ai/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# 执行工作流
+
+> 执行工作流。没有已发布的工作流时无法执行。
+
+
+
+## OpenAPI
+
+````yaml /zh/api-reference/openapi_workflow.json post /workflows/run
+openapi: 3.0.1
+info:
+  title: 工作流应用 API
+  description: 工作流应用提供无会话支持,适用于翻译、文章写作、摘要 AI 等场景。
+  version: 1.0.0
+servers:
+  - url: https://{api_base_url}
+    description: 工作流应用 API 的基础 URL。自托管部署时,替换为你的 API 基础 URL。
+    variables:
+      api_base_url:
+        default: api.dify.ai/v1
+        description: API 基础 URL 的主机与路径,不含 `https://` 前缀。
+security:
+  - ApiKeyAuth: []
+tags:
+  - name: 工作流
+    description: 用于执行和管理工作流的操作。
+  - name: 文件操作
+    description: 文件上传和下载操作。
+  - name: 终端用户
+    description: 终端用户信息相关操作。
+  - name: 语音与文字转换
+    description: 语音转文字和文字转语音转换。
+  - name: 应用配置
+    description: 应用设置、参数和元数据。
+  - name: 人工介入
+    description: 暂停等待人工输入的工作流恢复操作。
+paths:
+  /workflows/run:
+    post:
+      tags:
+        - 工作流
+      summary: 执行工作流
+      description: 执行工作流。没有已发布的工作流时无法执行。
+      operationId: executeWorkflowCn
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/WorkflowExecutionRequest'
+            examples:
+              streaming_example:
+                summary: 请求示例 - 流式模式
+                value:
+                  inputs:
+                    query: >-
+                      Summarize this text: The quick brown fox jumps over the
+                      lazy dog.
+                  response_mode: streaming
+                  user: user_workflow_123
+              blocking_example:
+                summary: 请求示例 - 阻塞模式
+                value:
+                  inputs:
+                    query: 'Translate this to French: Hello world'
+                  response_mode: blocking
+                  user: user_workflow_456
+              with_file_array_variable:
+                summary: Request Example - File array input
+                value:
+                  inputs:
+                    my_documents:
+                      - type: document
+                        transfer_method: local_file
+                        upload_file_id: a1b2c3d4-5678-90ab-cdef-1234567890ab
+                      - type: image
+                        transfer_method: remote_url
+                        url: https://example.com/image.jpg
+                  response_mode: blocking
+                  user: user_workflow_789
+      responses:
+        '200':
+          description: >-
+            请求成功。内容类型和结构取决于请求中的 `response_mode` 参数。
+
+
+            - 如果 `response_mode` 为 `blocking`,返回 `application/json` 和
+            `WorkflowBlockingResponse` 对象。
+
+            - 如果 `response_mode` 为 `streaming`,返回 `text/event-stream` 和
+            `ChunkWorkflowEvent` 对象流。
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/WorkflowBlockingResponse'
+              examples:
+                blockingResponse:
+                  summary: 响应示例 - 阻塞模式
+                  value:
+                    task_id: c3800678-a077-43df-a102-53f23ed20b88
+                    workflow_run_id: fb47b2e6-5e43-4f90-be01-d5c5a088d156
+                    data:
+                      id: fb47b2e6-5e43-4f90-be01-d5c5a088d156
+                      workflow_id: 7c3e33d4-2a8b-4e5f-9b1a-d3c6e8f12345
+                      status: succeeded
+                      outputs:
+                        result: Bonjour le monde
+                      error: null
+                      elapsed_time: 1.23
+                      total_tokens: 150
+                      total_steps: 3
+                      created_at: 1705407629
+                      finished_at: 1705407630
+            text/event-stream:
+              schema:
+                type: string
+                description: >-
+                  服务器发送事件 (SSE) 流。
+
+
+                  **解析**:每行以 `data: ` 开头,后接一个 JSON 对象,以 `\n\n` 终止。解析 JSON 前先去除
+                  `data: ` 前缀,再读取 `event` 字段确定事件类型。忽略 `ping` 事件,该事件每 10
+                  秒发送一次以保持连接活跃。
+
+
+                  **流生命周期**:收到 `workflow_finished`、`workflow_paused` 或 `error`
+                  事件时流关闭。错误以流内 `error` 事件的形式返回,HTTP 状态码始终为
+                  `200`;请检查事件载荷获取详情,不要依赖状态码判断。
+
+
+                  **人工介入事件**:
+
+                  - `human_input_required`:工作流到达人工介入节点时与 `workflow_paused`
+                  一同触发。使用载荷中的 `form_token` 通过 [人工介入
+                  API](/api-reference/人工介入/获取人工介入表单) 处理表单。
+
+                  - `human_input_form_filled`:接收者提交了表单,工作流恢复执行。
+
+                  - `human_input_form_timeout`:表单超时未响应。如定义了超时回退分支,工作流将沿该分支执行。
+              examples:
+                streamingResponse:
+                  summary: 响应示例 - 流式模式
+                  value: >-
+                    data: {"event": "workflow_started", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data": {"id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "workflow_id":
+                    "7c3e33d4-2a8b-4e5f-9b1a-d3c6e8f12345", "inputs": {"query":
+                    "Translate this"}, "created_at": 1705407629, "reason":
+                    "initial"}} data: {"event": "node_started", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data": {"id":
+                    "node_exec_1", "node_id": "node_1", "node_type": "llm",
+                    "title": "LLM Node", "index": 1, "created_at": 1705407629}}
+                    data: {"event": "text_chunk", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data": {"text":
+                    "Bonjour", "from_variable_selector": ["node_1", "text"]}}
+                    data: {"event": "workflow_finished", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data": {"id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "workflow_id":
+                    "7c3e33d4-2a8b-4e5f-9b1a-d3c6e8f12345", "status":
+                    "succeeded", "outputs": {"result": "Bonjour le monde"},
+                    "elapsed_time": 1.23, "total_tokens": 150, "total_steps": 3,
+                    "created_at": 1705407629, "finished_at": 1705407630}}
+                humanInputPause:
+                  summary: 响应示例 - 人工介入暂停
+                  value: >-
+                    data: {"event": "workflow_started", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data": {"id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "workflow_id":
+                    "7c3e33d4-2a8b-4e5f-9b1a-d3c6e8f12345", "inputs": {"draft":
+                    "Hello"}, "created_at": 1705407629, "reason": "initial"}}
+                    data: {"event": "human_input_required", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data": {"form_id":
+                    "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "form_token":
+                    "tok_abc123", "node_id": "approval_node", "node_title":
+                    "Approval", "form_content": "Please review the draft.",
+                    "inputs": [{"type": "text_input", "output_variable_name":
+                    "comment", "default": null}], "actions": [{"id": "approve",
+                    "title": "Approve", "button_style": "primary"}],
+                    "display_in_ui": false, "resolved_default_values":
+                    {"comment": ""}, "expiration_time": 1705494029}} data:
+                    {"event": "workflow_paused", "task_id":
+                    "c3800678-a077-43df-a102-53f23ed20b88", "workflow_run_id":
+                    "fb47b2e6-5e43-4f90-be01-d5c5a088d156", "data":
+                    {"workflow_run_id": "fb47b2e6-5e43-4f90-be01-d5c5a088d156",
+                    "status": "paused", "created_at": 1705407629,
+                    "elapsed_time": 0.5}}
+        '400':
+          description: |-
+            - `not_workflow_app` : App mode does not match the API route.
+            - `provider_not_initialize` : 未找到有效的模型提供商凭据。
+            - `provider_quota_exceeded` : 模型提供商配额已用尽。
+            - `model_currently_not_support` : 当前模型不可用。
+            - `completion_request_error` : 工作流 execution request failed.
+            - `invalid_param` : Invalid parameter value.
+          content:
+            application/json:
+              examples:
+                not_workflow_app:
+                  summary: not_workflow_app
+                  value:
+                    status: 400
+                    code: not_workflow_app
+                    message: Please check if your app mode matches the right API route.
+                provider_not_initialize:
+                  summary: provider_not_initialize
+                  value:
+                    status: 400
+                    code: provider_not_initialize
+                    message: >-
+                      No valid model provider credentials found. Please go to
+                      Settings -> Model Provider to complete your provider
+                      credentials.
+                provider_quota_exceeded:
+                  summary: provider_quota_exceeded
+                  value:
+                    status: 400
+                    code: provider_quota_exceeded
+                    message: >-
+                      Your quota for Dify Hosted OpenAI has been exhausted.
+                      Please go to Settings -> Model Provider to complete your
+                      own provider credentials.
+                model_currently_not_support:
+                  summary: model_currently_not_support
+                  value:
+                    status: 400
+                    code: model_currently_not_support
+                    message: >-
+                      Dify Hosted OpenAI trial currently not support the GPT-4
+                      model.
+                completion_request_error:
+                  summary: completion_request_error
+                  value:
+                    status: 400
+                    code: completion_request_error
+                    message: Completion request failed.
+                invalid_param:
+                  summary: invalid_param
+                  value:
+                    status: 400
+                    code: invalid_param
+                    message: Arg user must be provided.
+        '429':
+          description: |-
+            - `too_many_requests` : 该应用的并发请求过多。
+            - `rate_limit_error` : 上游模型提供商的速率限制已超出。
+          content:
+            application/json:
+              examples:
+                too_many_requests:
+                  summary: too_many_requests
+                  value:
+                    status: 429
+                    code: too_many_requests
+                    message: Too many requests. Please try again later.
+                rate_limit_error:
+                  summary: rate_limit_error
+                  value:
+                    status: 429
+                    code: rate_limit_error
+                    message: Rate Limit Error
+        '500':
+          description: '`internal_server_error` : 内部服务器错误。'
+          content:
+            application/json:
+              examples:
+                internal_server_error:
+                  summary: internal_server_error
+                  value:
+                    status: 500
+                    code: internal_server_error
+                    message: Internal Server Error.
+components:
+  schemas:
+    WorkflowExecutionRequest:
+      type: object
+      required:
+        - inputs
+        - user
+      properties:
+        inputs:
+          type: object
+          description: >-
+            工作流输入变量的键值对。文件类型变量的值应为文件对象数组,包含 `type`、`transfer_method` 以及 `url` 或
+            `upload_file_id`。请参阅 [获取应用参数](/api-reference/应用配置/获取应用参数) 响应中的
+            `user_input_form` 字段,以了解应用所需的变量名称和类型。
+          additionalProperties:
+            oneOf:
+              - type: string
+              - type: number
+              - type: boolean
+              - type: object
+              - type: array
+                items:
+                  $ref: '#/components/schemas/InputFileObject'
+        response_mode:
+          type: string
+          enum:
+            - streaming
+            - blocking
+          description: >-
+            响应模式。使用 `blocking` 获取同步响应(Cloudflare 超时时间为 `100 s`),使用 `streaming`
+            获取服务器发送事件。未指定时默认为阻塞模式。
+        user:
+          type: string
+          description: >-
+            用户标识符,由开发者自定义规则,在应用内必须唯一。此标识符用于限定数据访问范围——工作流运行和文件仅在使用相同的 `user`
+            值查询时可见。
+        files:
+          type: array
+          items:
+            $ref: '#/components/schemas/InputFileObject'
+          nullable: true
+          description: >-
+            文件列表。适用于需要将文件与文本组合输入的场景,仅在模型支持 Vision 能力时可用。要附加本地文件,请先通过
+            [上传文件](/api-reference/文件操作/上传文件) 上传,然后将返回的 `id` 作为
+            `upload_file_id`,并设置 `transfer_method: local_file`。
+    WorkflowBlockingResponse:
+      type: object
+      properties:
+        task_id:
+          type: string
+          format: uuid
+          description: >-
+            进行中的执行任务 ID。配合 [停止工作流任务](/api-reference/工作流/停止-工作流-任务)
+            使用以取消运行中的工作流。仅在执行期间有效。
+        workflow_run_id:
+          type: string
+          format: uuid
+          description: >-
+            此工作流运行记录的持久化标识符。配合 [获取工作流运行详情](/api-reference/工作流/获取-工作流-执行情况)
+            使用以在执行后获取结果。
+        data:
+          $ref: '#/components/schemas/WorkflowFinishedData'
+    InputFileObject:
+      type: object
+      required:
+        - type
+        - transfer_method
+      properties:
+        type:
+          type: string
+          enum:
+            - document
+            - image
+            - audio
+            - video
+            - custom
+          description: 文件类型。
+        transfer_method:
+          type: string
+          enum:
+            - remote_url
+            - local_file
+          description: 传输方式:文件 URL 使用 `remote_url`,上传文件使用 `local_file`。
+        url:
+          type: string
+          format: url
+          description: 文件 URL(当 `transfer_method` 为 `remote_url` 时)。
+        upload_file_id:
+          type: string
+          description: >-
+            通过 [上传文件](/api-reference/文件操作/上传文件) API 获取的已上传文件 ID(当
+            `transfer_method` 为 `local_file` 时使用)。
+    WorkflowFinishedData:
+      type: object
+      properties:
+        id:
+          type: string
+          format: uuid
+          description: 工作流运行 ID。
+        workflow_id:
+          type: string
+          format: uuid
+          description: 工作流 ID.
+        status:
+          type: string
+          description: >-
+            工作流执行状态。`running` 表示执行中,`succeeded` 表示成功完成,`failed` 表示执行出错,`stopped`
+            表示手动停止,`partial-succeeded` 表示部分节点成功但其他失败,`paused` 表示等待人工介入。
+        outputs:
+          type: object
+          additionalProperties: true
+          nullable: true
+          description: 工作流的输出数据。
+        error:
+          type: string
+          nullable: true
+          description: 工作流失败时的错误消息。
+        elapsed_time:
+          type: number
+          format: float
+          description: 总耗时(秒)。
+        total_tokens:
+          type: integer
+          description: 所有节点消耗的总令牌数。
+        total_steps:
+          type: integer
+          description: 已执行的工作流总步数。
+        created_at:
+          type: integer
+          format: int64
+          description: 工作流运行创建时的 Unix 时间戳。
+        finished_at:
+          type: integer
+          format: int64
+          nullable: true
+          description: 工作流运行结束时的 Unix 时间戳。
+        created_by:
+          type: object
+          additionalProperties: true
+          description: 创建者信息。仅存在于流式 `workflow_finished` 事件中。
+        exceptions_count:
+          type: integer
+          nullable: true
+          description: 执行过程中遇到的异常数量。仅在流式 `workflow_finished` 事件中出现。
+        files:
+          type: array
+          items:
+            type: object
+            additionalProperties: true
+          nullable: true
+          description: 工作流执行期间生成的文件。仅存在于流式 `workflow_finished` 事件中。
+  securitySchemes:
+    ApiKeyAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: API_KEY
+      description: >-
+        API Key 认证。对于所有 API 请求,请在 `Authorization` HTTP 头中包含您的 API Key,并加上
+        `Bearer ` 前缀。示例:`Authorization: Bearer {API_KEY}`。**强烈建议将 API Key
+        存储在服务端,不要在客户端共享或存储,以避免 API Key 泄漏导致严重后果。**
+
+````

+ 210 - 0
docs/数据集平台.md

@@ -0,0 +1,210 @@
+基于您的要求,我为您补充**详细技术栈清单**、**系统架构图(文字版)**,并明确以 **Python** 和 **Go** 为主要开发语言的技术选型与分工。
+
+---
+
+## 一、技术栈详细清单(Python + Go 双语言架构)
+
+### 1.1 总体技术选型原则
+| 维度 | 选型策略 |
+|------|----------|
+| **Go** | 负责高并发、高性能、I/O密集型的**数据采集、传输、API网关、调度引擎** |
+| **Python** | 负责**数据处理、AI标注、质量评估、数据分析、算法模型**等计算密集型/灵活迭代模块 |
+| **共同** | 元数据管理、日志、监控、权限等基础组件共用 |
+
+---
+
+### 1.2 详细技术栈清单
+
+| 层级 | 模块 | 技术选型 | 语言 |
+|------|------|----------|------|
+| **数据源层** | Token运营平台 / AI工作台 | 对接方式:Kafka / REST API / gRPC / SDK | - |
+| **采集接入层** | 统一采集网关 | **Go** + Gin/Fiber 框架,支持流式消费和批量拉取 | Go |
+| | 消息队列 | Apache Kafka(或 RocketMQ) | - |
+| | 数据同步工具 | **Go** 实现的 DataX 增强版 / 自研同步组件 | Go |
+| | 调度器 | **Go** + Cron / XXL-JOB(轻量级任务调度) | Go |
+| **数据处理层** | 流式处理引擎 | Apache Flink(支持 Python API)或 **Python** + Faust(流式处理库) | Python |
+| | 批量处理 | **Python** + Pandas / Dask(大数据量并行计算) | Python |
+| | 数据清洗 | **Python** + Pydantic(数据校验) + regex / 自研规则引擎 | Python |
+| | 数据脱敏 | **Python** + Cryptography(AES/SM4) + 动态脱敏策略中心 | Python |
+| | 数据分类 | **Python** + Scikit-learn / 规则分类器 | Python |
+| | 数据标注 | **Python** + Flask/FastAPI(标注后台API) + 前端标注台(Vue) | Python |
+| | 质量评估 | **Python** + 自研评分引擎(规则+统计模型) | Python |
+| **审核管理层** | 工作流引擎 | **Go** + Temporal(分布式工作流) 或 **Python** + SpiffWorkflow | Go(推荐) |
+| | 审核台后台 | **Go** + Gin + JWT 权限控制 | Go |
+| **数据集管理层** | 数据集管理服务 | **Go** + GORM + MySQL(元数据存储) | Go |
+| | 数据文件存储 | MinIO / 阿里云OSS / HDFS | - |
+| | 版本管理 | **Go** + 自研Delta版本差异算法(存储快照+增量) | Go |
+| **数据服务层** | 数据集看板(后端API) | **Go** + Gin + WebSocket(实时推送) | Go |
+| | 看板前端 | Vue 3 + TypeScript + ECharts / AntV | - |
+| | 对外API接口 | **Go** + gRPC / RESTful(预留扩展) | Go |
+| **治理与审计层** | 元数据管理 | Apache Atlas(集成) 或 自研元数据中心(**Go** + Elasticsearch) | Go |
+| | 审计日志 | **Go** 采集 + ELK(Elasticsearch + Logstash + Kibana) | Go |
+| | 权限控制 | **Go** + Casbin(RBAC + ABAC模型) | Go |
+| | 全链路追踪 | Jaeger + OpenTelemetry(Go SDK) | Go |
+| **基础设施** | 容器编排 | Kubernetes + Docker | - |
+| | 服务注册/配置 | Consul / Nacos(**Go** 客户端) | Go |
+| | 监控告警 | Prometheus + Grafana(**Go** 采集指标暴露) | Go |
+| | CI/CD | GitLab CI / Jenkins + ArgoCD | - |
+
+---
+
+## 二、系统架构图(文字版架构描述)
+
+> 由于无法直接绘制图片,以下为**分层架构图**的详细文字描述,可直接用于绘图工具(如 Draw.io / Visio)生成架构图。
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│                           数据源层(Data Source Layer)                     │
+│  ┌────────────────────┐          ┌──────────────────────────────────┐      │
+│  │  Token 运营平台     │          │  AI 工作台                      │      │
+│  │  - 调用日志         │          │  - 问答记录                     │      │
+│  │  - 业务指标         │          │  - 文件解析结果                 │      │
+│  └─────────┬──────────┘          │  - 知识引用记录                 │      │
+│            │                     │  - 智能问数/材料生成            │      │
+│            └─────────────────────┴────────────────┬─────────────────┘      │
+└─────────────────────────────────────────────────┬───────────────────────────┘
+                                                  │
+                                                  ▼
+┌─────────────────────────────────────────────────────────────────────────────┐
+│                    采集接入层(Ingestion Layer)[Go]                       │
+│  ┌──────────────────────────────────────────────────────────────────┐      │
+│  │  统一采集网关(Go + Gin)                                       │      │
+│  │  - 实时消费 Kafka 流                                            │      │
+│  │  - 定时拉取 API 批量数据                                        │      │
+│  │  - 数据路由与初步校验                                           │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  消息队列(Kafka)—— 缓冲削峰                                 │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  数据同步调度器(Go + Cron) - 增量/全量同步管理               │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+└─────────────────────────────────────────────────────────────────────────────┘
+                                                  │
+                                                  ▼
+┌─────────────────────────────────────────────────────────────────────────────┐
+│                   数据处理层(Processing Layer)[Python]                   │
+│  ┌──────────────────────────────────────────────────────────────────┐      │
+│  │  清洗引擎(Python + Pandas/Dask)                              │      │
+│  │  - 去重、格式化、空值处理、字段映射                            │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  脱敏引擎(Python + Cryptography)                              │      │
+│  │  - 敏感字段识别与脱敏(掩码/加密/替换)                        │      │
+│  │  - 动态脱敏策略中心                                             │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  分类引擎(Python + Scikit-learn / 规则分类器)                │      │
+│  │  - 按行业/场景/来源/类型自动打标                               │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  标注引擎(Python + FastAPI + 预训练模型)                     │      │
+│  │  - 规则标注 / 半自动标注(NLP模型辅助) / 人工标注台           │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  质量评估引擎(Python + 规则评分模型)                         │      │
+│  │  - 完整性/准确性/可用性/合规性打分                             │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+└─────────────────────────────────────────────────────────────────────────────┘
+                                                  │
+                                                  ▼
+┌─────────────────────────────────────────────────────────────────────────────┐
+│                   审核管理层(Review Layer)[Go]                           │
+│  ┌──────────────────────────────────────────────────────────────────┐      │
+│  │  工作流引擎(Go + Temporal)                                   │      │
+│  │  - 多级审批流程(提交→初审→复审→归档)                       │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  审核管理后台(Go + Gin)                                      │      │
+│  │  - 授权状态 / 敏感级别 / 审核状态管理                          │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+└─────────────────────────────────────────────────────────────────────────────┘
+                                                  │
+                                                  ▼
+┌─────────────────────────────────────────────────────────────────────────────┐
+│                数据集管理层(Dataset Management Layer)[Go]               │
+│  ┌──────────────────────────────────────────────────────────────────┐      │
+│  │  数据集管理服务(Go + GORM + MySQL)                           │      │
+│  │  - 数据集创建/版本管理/状态管理/台账                          │      │
+│  │  - 元数据存储(MySQL) + 文件存储(MinIO/OSS)                │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  版本控制模块(Go + Delta算法)                                │      │
+│  │  - 快照+增量差异存储,支持回滚与追溯                          │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+└─────────────────────────────────────────────────────────────────────────────┘
+                                                  │
+                                                  ▼
+┌─────────────────────────────────────────────────────────────────────────────┐
+│                   数据服务层(Service Layer)[Go + Vue]                    │
+│  ┌──────────────────────────────────────────────────────────────────┐      │
+│  │  数据集看板后端(Go + Gin + WebSocket)                       │      │
+│  └──────────────────────────────┬───────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  看板前端(Vue3 + TypeScript + ECharts)                       │      │
+│  │  - 数据量/质量/行业分布/审核状态/入库趋势                      │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+│                                 │                                          │
+│  ┌──────────────────────────────▼───────────────────────────────────┐      │
+│  │  第三方接口预留(Go + gRPC / REST)                            │      │
+│  │  - 标准化数据输出接口,支持后续扩展                            │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+└─────────────────────────────────────────────────────────────────────────────┘
+                                                  │
+                                                  ▼
+┌─────────────────────────────────────────────────────────────────────────────┐
+│              治理与审计层(Governance & Audit Layer)[Go]                  │
+│  ┌──────────────────────────────────────────────────────────────────┐      │
+│  │  元数据中心(Go + Elasticsearch) + 审计日志(ELK)           │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+│  ┌──────────────────────────────────────────────────────────────────┐      │
+│  │  权限控制(Go + Casbin - RBAC/ABAC)                          │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+│  ┌──────────────────────────────────────────────────────────────────┐      │
+│  │  全链路追踪(Jaeger + OpenTelemetry Go SDK)                  │      │
+│  └──────────────────────────────────────────────────────────────────┘      │
+└─────────────────────────────────────────────────────────────────────────────┘
+                                                  │
+                                                  ▼
+┌─────────────────────────────────────────────────────────────────────────────┐
+│                    基础设施层(Infrastructure)                              │
+│  Kubernetes + Docker + Prometheus + Grafana + Nacos/Consul                │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 三、开发语言分工总结
+
+| 模块类型 | 使用语言 | 原因 |
+|----------|----------|------|
+| 数据采集网关、调度器、消息消费 | **Go** | 高并发、低延迟、I/O密集,适合处理大量数据流 |
+| 数据清洗、脱敏、分类、标注、质量评估 | **Python** | 丰富的AI/数据分析生态(Pandas/Scikit-learn),灵活迭代算法 |
+| 工作流引擎、审核后台、数据集管理、API服务 | **Go** | 高稳定性和性能,适合业务逻辑密集型服务 |
+| 看板后端、权限控制、审计日志、链路追踪 | **Go** | 统一服务治理,降低运维复杂度 |
+| 看板前端 | **TypeScript + Vue** | 主流前端框架,生态成熟 |
+
+---
+
+## 四、关键组件交互流程(数据流转)
+
+```
+1. 数据产生 → Token/AI工作台写入Kafka
+2. Go采集网关消费Kafka → 初步校验 → 写入数据缓冲区
+3. Python处理引擎定期/实时拉取 → 清洗 → 脱敏 → 分类 → 标注 → 质量评分
+4. 处理后数据 → 触发Go工作流引擎 → 进入合规审核流程
+5. 审核通过 → Go数据集管理服务 → 创建数据集版本 → 存储元数据+文件
+6. 看板API(Go)聚合展示 → 前端渲染
+7. 所有环节 → 审计日志写入ELK → 全链路追踪
+```
+
+---

+ 599 - 0
docs/数据集平台最终.md

@@ -0,0 +1,599 @@
+# 数据集平台系统架构设计文档
+
+## 文档说明
+
+- **文档版本**:v2.0
+- **编制日期**:2026年7月
+- **文档类型**:系统架构设计说明书(Architecture Design Document)
+- **阅读对象**:技术决策者、架构师、技术负责人、核心开发人员
+- **文档目的**:阐述系统的架构设计理念、技术选型依据、模块划分、数据流转及非功能性保障策略
+
+---
+
+## 一、项目背景与目标
+
+### 1.1 业务背景
+
+随着AI能力在Token运营平台和AI工作台中的深度应用,各业务系统产生了海量的调用日志、问答记录、文件解析结果和知识引用数据。这些数据天然具有以下特征:
+
+- **来源分散**:横跨多个业务系统,数据格式各异
+- **价值密度不均**:部分数据可直接用于模型训练,部分需加工治理
+- **合规要求高**:涉及用户隐私和业务敏感信息,需严格脱敏和审计
+- **需要版本化管理**:数据集需可追溯、可回滚、可对比
+
+因此,需要一个**企业级数据集治理平台**,将分散的原始数据转化为高质量、合规、可追溯的标准化数据集,服务于AI模型训练、业务分析等下游场景。
+
+### 1.2 核心建设目标
+
+| 目标维度 | 具体描述 |
+|----------|----------|
+| **统一采集** | 支持多数据源(Token平台、AI工作台)的统一接入,标准化数据格式 |
+| **智能加工** | 自动化完成清洗、脱敏、分类、标注、质量评估,减少人工干预 |
+| **合规可控** | 全过程脱敏、多级审批、完整审计,满足企业数据治理要求 |
+| **版本管理** | 支持数据集的版本化存储、差异对比和任意版本回滚 |
+| **高效服务** | 提供实时看板、标准化API,支撑数据消费和运营决策 |
+| **可扩展** | 支持水平扩展,可平滑接入新数据源和新的处理环节 |
+
+### 1.3 关键非功能性指标
+
+| 指标 | 目标值 |
+|------|--------|
+| 采集吞吐量 | ≥ 50,000 条/秒 |
+| 端到端处理延迟 | P95 < 5秒(从产生到入库) |
+| 系统可用性 | 99.95%(年停机时间 < 4.5小时) |
+| 数据准确性 | ≥ 99.99%(端到端校验) |
+| 单日处理能力 | ≥ 1亿条记录 |
+| 数据集查询响应 | 百万级数据 < 500ms |
+| 合规审计覆盖率 | 100%(所有操作可追溯) |
+
+---
+
+## 二、系统架构设计
+
+### 2.1 架构设计原则
+
+| 原则 | 说明 | 体现 |
+|------|------|------|
+| **职责单一** | 每个模块只做一件事,边界清晰 | 采集只负责接入,清洗只负责清洗,互不耦合 |
+| **异步解耦** | 模块间通过消息队列通信,降低依赖 | 全链路基于Kafka流转数据 |
+| **语言择优** | 不同场景用最合适的语言 | Go负责高并发I/O,Python负责数据处理和AI |
+| **可观测性优先** | 所有操作可追踪、可度量 | 日志+指标+链路追踪三位一体 |
+| **安全内置** | 安全不是附加层,而是贯穿全流程 | 脱敏、权限、审计从设计阶段即纳入 |
+| **弹性扩展** | 瓶颈模块可独立扩容 | Kubernetes HPA + Kafka消费者组 |
+
+### 2.2 系统分层架构
+
+系统采用**七层架构**,从下至上分别为基础设施层、治理审计层、数据服务层、数据集管理层、审核管理层、数据处理层、采集接入层、数据源层。
+
+```
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│                              数据源层(外部系统)                              │
+│                    Token运营平台    │    AI工作台    │    其他数据源            │
+└────────────────────────────────────────┬────────────────────────────────────────┘
+                                         │
+                                         ▼
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│                     采集接入层(Go)—— 数据入口                               │
+│    统一采集网关     │     消息队列(Kafka)     │     同步调度器              │
+│  实时消费/批量拉取   │     缓冲削峰/解耦       │   定时/增量同步管理          │
+└────────────────────────────────────────┬────────────────────────────────────────┘
+                                         │
+                                         ▼
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│                   数据处理层(Python)—— 数据加工                             │
+│  清洗引擎 → 脱敏引擎 → 分类引擎 → 标注引擎 → 质量评估引擎                    │
+│  去重/格式化   加密/掩码   自动打标   半自动标注   五维评分                    │
+└────────────────────────────────────────┬────────────────────────────────────────┘
+                                         │
+                                         ▼
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│                  审核管理层(Go)—— 合规管控                                  │
+│           工作流引擎(Temporal)    │    审核管理后台                         │
+│         多级审批/状态机管理          │   授权/敏感级别/审核状态管理            │
+└────────────────────────────────────────┬────────────────────────────────────────┘
+                                         │
+                                         ▼
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│               数据集管理层(Go)—— 核心资产                                   │
+│       数据集管理服务      │      版本控制模块      │      存储管理             │
+│   创建/状态/台账管理       │   Delta差异/快照/回滚  │  元数据+文件存储          │
+└────────────────────────────────────────┬────────────────────────────────────────┘
+                                         │
+                                         ▼
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│                 数据服务层(Go + Vue)—— 能力输出                             │
+│   看板后端(WebSocket)   │   看板前端(ECharts)  │   第三方API(gRPC/REST)   │
+│   实时聚合推送            │   可视化运营           │   标准化数据输出           │
+└────────────────────────────────────────┬────────────────────────────────────────┘
+                                         │
+                                         ▼
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│             治理与审计层(Go)—— 贯穿全局                                     │
+│  元数据中心  │  审计日志(ELK)  │  权限控制(Casbin)  │  链路追踪(Jaeger)    │
+└─────────────────────────────────────────────────────────────────────────────────┘
+                                         │
+                                         ▼
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│                    基础设施层(全栈支撑)                                       │
+│      Kubernetes  │  Docker  │  Prometheus/Grafana  │  Nacos/Consul            │
+│      容器编排     │  容器化   │   监控告警           │   服务发现/配置           │
+└─────────────────────────────────────────────────────────────────────────────────┘
+```
+
+### 2.3 部署架构与高可用设计
+
+系统采用**多可用区部署**方案,确保高可用性:
+
+| 组件 | 部署策略 | 容灾机制 |
+|------|----------|----------|
+| 采集网关 | 多副本部署(≥3),Kubernetes管理 | 滚动更新,自动恢复 |
+| 数据处理引擎 | 多Worker部署,任务队列分发 | 失败自动重试,死信队列 |
+| 数据集服务 | 多副本部署(≥3),无状态设计 | 自动扩缩容,故障漂移 |
+| MySQL | 主从架构 + 定期全量备份 | 主备切换,数据备份恢复 |
+| Kafka | 多Broker集群(≥3) | 分区副本,ISR机制 |
+| MinIO | 分布式部署(≥4节点) | 纠删码,多节点容错 |
+| Elasticsearch | 多节点集群(≥3) | 主分片+副本分片 |
+
+---
+
+## 三、技术选型与决策依据
+
+### 3.1 双语言架构设计
+
+系统采用**Go + Python**双语言架构,核心设计思想是"用对的语言做对的事"。
+
+| 语言 | 负责领域 | 选型理由 | 典型模块 |
+|------|----------|----------|----------|
+| **Go** | 接入、传输、管理、服务 | 高并发性能优异,内存占用低,部署简单,适合构建长期运行的基础设施服务 | 采集网关、数据集管理、审核后台、看板API、权限控制 |
+| **Python** | 处理、分析、AI | 数据科学生态最成熟(Pandas/Scikit-learn),算法迭代灵活,AI模型集成方便 | 清洗、脱敏、分类、标注、质量评估 |
+
+**模块间通信机制**:
+- 异步数据流:通过Kafka主题连接
+- 同步管理调用:通过gRPC/RESTful接口
+- 配置共享:通过Nacos统一配置中心
+
+### 3.2 关键技术组件选型
+
+| 技术领域 | 选型方案 | 备选方案 | 选型理由 |
+|----------|----------|----------|----------|
+| **消息队列** | Apache Kafka | RocketMQ/Pulsar | 高吞吐、持久化、消费者组机制成熟,与大数据生态集成好 |
+| **数据存储(文件)** | MinIO + 云OSS | HDFS/Ceph | S3兼容,与公有云无缝对接,部署简单,支持生命周期管理 |
+| **元数据存储** | MySQL | PostgreSQL | 团队熟悉度高,运维生态成熟,事务支持完善 |
+| **日志存储** | Elasticsearch | ClickHouse | 全文检索能力强,ELK生态成熟,支持复杂聚合分析 |
+| **工作流引擎** | Temporal | Activiti/Flowable | 支持Go原生,状态持久化,宕机恢复能力强 |
+| **权限管理** | Casbin | Spring Security | 轻量级,支持RBAC+ABAC混合模型,多语言支持 |
+| **链路追踪** | Jaeger + OpenTelemetry | Zipkin | 开源生态活跃,Go SDK支持完善,与Kubernetes集成好 |
+| **容器编排** | Kubernetes | Docker Swarm | 事实标准,生态最丰富,支持自动扩缩容 |
+| **配置管理** | Nacos | Consul/Etcd | 支持配置热更新,服务发现,与Go/Python集成方便 |
+| **监控告警** | Prometheus + Grafana | Zabbix | 云原生标准,与Kubernetes深度集成 |
+
+### 3.3 技术风险与应对
+
+| 风险点 | 影响 | 应对措施 |
+|--------|------|----------|
+| Go与Python跨语言调试困难 | 问题定位耗时 | 引入OpenTelemetry全链路追踪,统一日志格式 |
+| Kafka消费积压 | 端到端延迟增大 | 配置消费者组自动扩容,设置积压告警阈值 |
+| Python处理性能瓶颈 | 处理速度不足 | 多Worker并行,Dask处理大数据集,优化算法 |
+| 存储成本快速增长 | 成本失控 | 建立数据生命周期策略,冷热分层存储 |
+| 版本Delta链过长 | 版本恢复变慢 | 每N个版本自动合并为快照,定期清理过期Delta |
+
+---
+
+## 四、模块功能定义与交互关系
+
+### 4.1 各层模块职责详解
+
+#### (1)采集接入层(Go)
+
+| 模块 | 核心职责 | 输入 | 输出 |
+|------|----------|------|------|
+| 统一采集网关 | 消费Kafka数据流,初步校验,路由分发 | Kafka消息/REST请求 | 清洗后写入Kafka或死信队列 |
+| 消息队列(Kafka) | 数据总线,削峰填谷,模块解耦 | 各上游Producer写入 | 各下游Consumer读取 |
+| 同步调度器 | 管理定时/增量同步任务,控制同步节奏 | Cron触发/API触发 | 调度任务执行状态 |
+
+**关键能力**:
+- 支持批量拉取和流式消费双模式
+- 数据校验失败时自动写入死信队列,不阻塞主流程
+- 消费位移管理,支持数据重放
+
+#### (2)数据处理层(Python)
+
+| 模块 | 核心职责 | 依赖 | 输出标准 |
+|------|----------|------|----------|
+| 清洗引擎 | 去重、格式标准化、空值处理、字段映射 | 规则库(可配置) | 数据完整性≥99% |
+| 脱敏引擎 | 敏感字段识别、动态脱敏(掩码/加密/替换) | 策略中心 | 合规性100%(无明文敏感数据) |
+| 分类引擎 | 规则匹配+机器学习自动打标 | 分类模型、规则库 | 分类准确率≥90% |
+| 标注引擎 | 预标注(NLP模型)+ 人工标注台辅助 | 标注模型、前端标注平台 | 标注准确率≥95% |
+| 质量评估引擎 | 五维评分(完整性/准确性/一致性/时效性/合规性) | 评分模型 | 输出0-1量化评分 |
+
+**模块协作模式**:
+- 各引擎通过Kafka主题串联,形成处理流水线
+- 每个引擎可独立扩容,互不影响
+- 质量评估低于阈值的数据被回退,形成反馈闭环
+
+#### (3)审核管理层(Go)
+
+| 模块 | 核心职责 | 关键机制 |
+|------|----------|----------|
+| 工作流引擎 | 管理多级审批流程,状态机驱动 | 提交→初审→复审→归档,超时自动提醒 |
+| 审核管理后台 | 审核台操作界面后端 | 权限控制,审核任务分配,审核记录留痕 |
+
+**审核流程设计**:
+- 支持三级审批(初审、复审、终审),可根据数据敏感级别动态调整
+- 审核超时自动升级,确保流程不被阻塞
+- 驳回数据需附带驳回原因,数据退回处理层重新加工
+
+#### (4)数据集管理层(Go)
+
+| 模块 | 核心职责 | 关键设计 |
+|------|----------|----------|
+| 数据集管理服务 | 数据集CRUD、状态管理、台账维护 | 元数据存MySQL,文件存MinIO |
+| 版本控制模块 | 版本创建、差异存储、回滚、追溯 | Delta算法+定期快照 |
+| 存储管理 | 文件读写、生命周期管理、压缩优化 | Parquet格式+Snappy压缩 |
+
+**版本管理策略**:
+- 增量存储:每次变更仅保存Delta差异,节省70%存储空间
+- 快照策略:每10个版本自动创建全量快照
+- 回滚能力:从任意历史版本快速重建
+
+#### (5)数据服务层(Go + Vue)
+
+| 模块 | 核心职责 | 技术实现 |
+|------|----------|----------|
+| 看板后端 | 聚合多源数据,实时推送统计信息 | WebSocket长连接 + 定期拉取 |
+| 看板前端 | 可视化展示数据集运营状态 | Vue3 + TypeScript + ECharts |
+| 第三方API | 标准化数据输出,支持下游消费 | gRPC(高性能)+ RESTful(兼容性) |
+
+**看板数据聚合来源**:
+- MySQL:数据集元数据统计
+- ES:审计日志聚合分析
+- Prometheus:系统运行指标
+- Redis:实时计数器
+
+#### (6)治理与审计层(Go)
+
+| 模块 | 核心职责 | 关键设计 |
+|------|----------|----------|
+| 元数据中心 | 统一管理数据字典、血缘关系 | 基于ES实现快速检索 |
+| 审计日志 | 全操作记录,合规追溯 | 异步写入ES,按天滚动索引 |
+| 权限控制 | 细粒度访问控制 | Casbin实现RBAC+ABAC混合模型 |
+| 全链路追踪 | 分布式调用链分析 | OpenTelemetry标准 + Jaeger可视化 |
+
+**审计日志覆盖范围**:
+- 所有API调用(谁、何时、做了什么操作、结果如何)
+- 数据处理行为(每条数据经过哪些环节、结果状态)
+- 审核行为(谁审核的、审核结果、审核意见)
+- 数据消费行为(谁导出了什么数据、导出量级)
+
+### 4.2 模块间关键交互流程
+
+#### 流程一:数据采集与预处理
+
+```
+数据源产生数据 → 采集网关消费Kafka → 网关校验(通过/失败)→ 
+通过数据写入"待处理"主题 → 失败数据写入死信队列 → 告警通知
+```
+
+#### 流程二:数据处理流水线
+
+```
+待处理数据 → 清洗引擎(去重/格式化)→ 脱敏引擎(加密/掩码)→ 
+分类引擎(自动打标)→ 标注引擎(半自动标注)→ 质量评估引擎(五维打分)→ 
+高质量数据→触发审核流程 / 低质量数据→退回清洗引擎重新加工
+```
+
+#### 流程三:审核与入库
+
+```
+质量达标数据 → 工作流引擎发起审核 → 初审 → 复审 → 审核通过 → 
+回调数据集管理服务 → 创建数据集版本 → 存储快照+Delta → 更新元数据
+```
+
+#### 流程四:数据消费与输出
+
+```
+外部请求 → 权限校验(Casbin)→ 查询数据集元数据 → 
+从MinIO读取数据文件 → 按权限级别脱敏输出 → 记录消费审计日志
+```
+
+### 4.3 关键交互时序(逻辑描述)
+
+**场景:一条新数据从产生到看板可见的完整旅程**
+
+1. 用户在Token平台执行一次API调用,系统自动生成调用日志
+2. 日志通过Kafka Producer实时写入`raw-data`主题
+3. 采集网关作为Consumer拉取数据,完成字段完整性校验
+4. 校验通过后写入`to-clean`主题,失败则写入`dlq`死信队列
+5. 清洗引擎消费`to-clean`,执行去重和格式标准化,输出到`to-desensitize`
+6. 脱敏引擎消费并执行动态脱敏,输出到`to-classify`
+7. 分类引擎打标后输出到`to-annotate`
+8. 标注引擎预标注,低置信度样本送人工标注台,完成后输出到`to-evaluate`
+9. 质量评估引擎打分,≥0.75分的数据进入`to-audit`,<0.75分退回`to-clean`
+10. 工作流引擎检测到`to-audit`有新数据,启动审核流程
+11. 审核人员在审核台操作,通过后回调数据集服务
+12. 数据集服务创建版本,存储数据到MinIO,记录元数据到MySQL
+13. 看板服务通过WebSocket推送最新统计数据
+14. 整个过程的每一步都记录审计日志到ES
+
+---
+
+## 五、数据架构与存储设计
+
+### 5.1 数据分类与存储映射
+
+系统根据数据性质选择不同的存储方案,形成**混合存储架构**。
+
+| 数据类型 | 存储位置 | 数据格式 | 保留周期 | 访问频率 |
+|----------|----------|----------|----------|----------|
+| **元数据**(数据集信息、版本信息、配置) | MySQL | 结构化关系表 | 永久(仅归档) | 高频读写 |
+| **数据文件**(快照、Delta、原始归档) | MinIO/OSS | Parquet/JSON | 3年(原始归档)+永久(数据集) | 中频读写 |
+| **审计日志**(操作记录、行为轨迹) | Elasticsearch | JSON文档 | 90天(热)+归档 | 高频查询 |
+| **链路追踪**(调用链、耗时分析) | Elasticsearch | JSON文档 | 30天 | 中频查询 |
+| **流转数据**(处理中的数据) | Kafka | 二进制/JSON | 7天 | 极高频读写 |
+| **缓存数据**(热点统计、实时计数器) | Redis | 键值对 | TTL自动过期 | 极高频读写 |
+| **系统指标**(CPU、内存、QPS) | Prometheus | 时序数据 | 15天 | 高频查询 |
+
+### 5.2 核心数据模型
+
+#### 数据集元数据模型(MySQL)
+
+数据集的元数据是系统的核心资产索引,包含以下核心实体:
+
+**数据集主表**记录数据集的全局信息,包括唯一标识、名称描述、行业分类、来源类型、当前版本指针、综合质量评分、记录总数、存储大小、生命周期状态以及创建和更新信息。
+
+**版本表**记录数据集的每一次版本变更,包括版本标识、所属数据集、版本标签、父版本(形成版本链)、该版本的记录数和存储大小、快照文件地址、Delta操作日志、质量评分、审核状态和审核意见。
+
+**数据源配置表**管理接入的数据源信息,包括数据源类型、连接配置(加密存储)、同步调度表达式、最后同步时间和同步状态。
+
+**工作流实例表**追踪审核流程状态,包括工作流实例标识、关联的数据集和版本、工作流类型、当前步骤、状态、当前处理人和上下文变量。
+
+#### 审计日志模型(Elasticsearch)
+
+审计日志文档包含操作时间戳、操作用户标识和用户名、操作类型(增删改查)、资源类型和标识、操作路径、请求参数、响应码、客户端IP和User-Agent、操作耗时以及关联的数据集和版本标识。所有字段均支持全文检索和聚合分析。
+
+### 5.3 存储容量规划与增长模型
+
+基于业务量预估,对未来3年的存储需求进行规划。
+
+#### MySQL容量预估
+
+| 数据表 | 日增量 | 年增量 | 3年总量 | 备注 |
+|--------|--------|--------|---------|------|
+| datasets(数据集主表) | 50条 | 18,250条 | 约5.5万条 | 每条2KB,总量极小 |
+| dataset_versions(版本表) | 5,000条 | 182万条 | 约550万条 | 每条5KB,总量约27GB |
+| workflow_instances(工作流表) | 5,000条 | 182万条 | 约550万条 | 每条3KB,总量约16GB |
+| process_logs(处理日志) | 10万条 | 3650万条 | 约1.1亿条 | 按天分区,只保留3个月 |
+| 其他配置表 | 少量 | 少量 | <1GB | - |
+
+**结论**:MySQL总数据量控制在100GB以内,单机可支撑,无需分库。
+
+#### MinIO容量预估
+
+| 数据类型 | 日增量 | 年增量 | 3年总量 | 压缩比 |
+|----------|--------|--------|---------|--------|
+| 数据集快照(Parquet) | 50GB | 18TB | 54TB | 3:1(相比JSON) |
+| Delta增量文件 | 5GB | 1.8TB | 5.4TB | - |
+| 原始归档 | 20GB | 7.3TB | 21.9TB | 压缩存储 |
+| 临时处理文件 | 50GB | 18TB | - | 24小时自动删除 |
+
+**结论**:3年总存储需求约80TB,采用冷热分层可有效控制成本。
+
+#### Elasticsearch容量预估
+
+| 索引类型 | 日增量 | 年增量 | 3年总量 | 保留策略 |
+|----------|--------|--------|---------|----------|
+| 审计日志 | 100GB | 36TB | 108TB | 保留90天,实际热数据约9TB |
+| 链路追踪 | 50GB | 18TB | 54TB | 保留30天,实际热数据约4.5TB |
+| 应用日志 | 30GB | 11TB | 33TB | 保留30天,实际约3TB |
+
+**结论**:ES实际热数据总量约17TB,7节点集群可支撑。
+
+### 5.4 数据生命周期管理策略
+
+系统为不同类型的数据建立完整的生命周期管理机制:
+
+**数据集快照**的生命周期:从创建开始,前30天存储在MinIO SSD高速存储区,31-180天自动迁移至HDD标准存储区,180天以上转为云OSS归档存储,永久保留直至手动删除。
+
+**Delta增量文件**的生命周期:创建后保留30天用于版本回滚,31-90天保留但标记为可清理,在触发快照合并后自动删除,释放存储空间。
+
+**原始归档数据**的生命周期:前180天存储在MinIO,181-1095天迁移到OSS冷存储,1095天(3年)后根据合规要求判断是否允许删除。
+
+**审计日志**的生命周期:近7天存储在ES热节点(SSD),响应快速;8-30天自动迁移至ES温节点(HDD),查询速度略降;31-90天进入ES冷节点(冻结索引),查询需解冻;超过90天从ES删除,但先创建快照到OSS进行合规存档。
+
+---
+
+## 六、关键架构决策与权衡
+
+### 6.1 为什么选择双语言而非单一语言
+
+| 对比维度 | 全Go方案 | 全Python方案 | 双语言方案(选定) |
+|----------|----------|--------------|-------------------|
+| 采集吞吐量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
+| AI/数据处理生态 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
+| 开发效率(业务逻辑) | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
+| 部署运维复杂度 | ⭐⭐⭐⭐⭐(简单) | ⭐⭐⭐⭐ | ⭐⭐⭐(略有增加) |
+| 人才招聘难度 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
+
+**决策结论**:虽然双语言增加了跨语言通信的复杂度,但在性能和数据智能处理两个维度上都获得了最优解,收益远大于成本。
+
+### 6.2 为什么选择Kafka作为数据总线
+
+| 评估维度 | Kafka | RabbitMQ | Pulsar |
+|----------|-------|----------|--------|
+| 吞吐量 | 极高(百万级/秒) | 中(万级/秒) | 高(十万级/秒) |
+| 消息持久化 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
+| 消费重放能力 | ⭐⭐⭐⭐⭐(支持) | ⭐⭐(有限) | ⭐⭐⭐⭐ |
+| 生态成熟度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
+| 运维复杂度 | 中 | 低 | 中高 |
+
+**决策结论**:Kafka的持久化和消费重放能力对数据平台至关重要(支持数据回溯重新处理),高吞吐特性满足日亿级数据量需求。
+
+### 6.3 为什么选择Delta增量存储而非全量快照
+
+| 对比维度 | 全量快照 | Delta增量(选定) |
+|----------|----------|-------------------|
+| 存储效率 | 低(每次变更存全量) | 高(只存变化部分) |
+| 版本恢复速度 | 快(直接读取) | 中(需合并Delta) |
+| 版本对比能力 | 弱(需全量对比) | 强(天然支持差异) |
+| 存储节省 | - | 约70% |
+| 实现复杂度 | 低 | 中 |
+
+**决策结论**:数据集平台核心价值之一是版本管理,Delta存储天然支持差异对比和增量追溯,配合定期快照机制解决了恢复速度问题,综合优势明显。
+
+### 6.4 为什么用Temporal而非传统工作流引擎
+
+| 评估维度 | Temporal | Activiti/Flowable |
+|----------|----------|-------------------|
+| 语言亲和性 | Go原生 | Java原生(需额外集成) |
+| 状态持久化 | ✅ 内置,自动恢复 | 需手动持久化 |
+| 超时控制 | ✅ 原生支持 | 需额外实现 |
+| 可观测性 | ✅ 内置Web UI | 部分支持 |
+| 轻量级 | 是 | 否(较重量) |
+
+**决策结论**:Temporal与Go生态无缝集成,状态机自动持久化可大幅降低审核流程的开发复杂度,且保证了宕机后的流程恢复能力。
+
+### 6.5 技术债务与演进规划
+
+| 当前决策 | 潜在问题 | 未来演进方向 |
+|----------|----------|--------------|
+| MySQL存储元数据 | 版本表快速增长 | 1年后迁移历史版本到TiDB或ClickHouse |
+| Python单机处理 | 超大数据量时性能不足 | 引入Dask分布式计算或迁移到Flink |
+| Kafka单集群 | 单点风险 | 未来部署双集群跨机房容灾 |
+| MinIO自建 | 运维成本 | 逐步迁移到公有云OSS服务 |
+| 人工标注依赖 | 标注效率瓶颈 | 持续优化NLP预标注模型,降低人工比例 |
+
+---
+
+## 七、非功能性保障方案
+
+### 7.1 高可用架构
+
+| 组件 | 可用性保障 | RTO | RPO |
+|------|------------|-----|-----|
+| 采集网关 | 多副本+Kubernetes自动恢复 | < 1分钟 | 0(无状态) |
+| 数据处理引擎 | 多Worker+任务队列 | < 1分钟 | 0(幂等设计) |
+| 数据集服务 | 多副本+Kubernetes | < 1分钟 | 0(无状态) |
+| MySQL | 主从+自动切换 | < 5分钟 | < 1分钟(异步复制) |
+| Kafka | 多Broker+ISR | < 2分钟 | 0(持久化) |
+| MinIO | 分布式纠删码 | < 5分钟 | 0(多副本) |
+| Elasticsearch | 多节点+副本分片 | < 5分钟 | 0(副本) |
+
+**整体可用性目标**:99.95%(年停机时间 < 4.5小时)
+
+### 7.2 性能保障策略
+
+| 策略 | 具体措施 | 适用范围 |
+|------|----------|----------|
+| **水平扩展** | Kubernetes HPA自动扩容,Kafka消费者组动态增减 | 采集、处理、API服务 |
+| **缓存加速** | Redis缓存热点统计和常用数据集元数据 | 看板、数据集查询 |
+| **异步处理** | 耗时操作(脱敏、标注)通过Kafka异步化 | 数据处理全链路 |
+| **批量操作** | 采集批次1000条,处理批次5000条 | 采集、清洗、存储 |
+| **读写分离** | MySQL主写从读,ES冷热分离 | 数据库、ES |
+| **限流熔断** | 网关层限流,下游熔断保护 | 采集网关、API服务 |
+| **预聚合** | 每日定时聚合统计数据,减少实时计算 | 看板、报表 |
+
+### 7.3 安全防护体系
+
+系统采用**纵深防御**的安全策略:
+
+**网络安全层**:
+- 服务间通信强制mTLS双向认证
+- 对外接口仅开放HTTPS(TLS 1.3)
+- 部署WAF防护常见攻击
+- 内部网络隔离,数据库不暴露公网
+
+**数据安全层**:
+- 传输加密:全链路TLS加密
+- 存储加密:MinIO服务端加密(SSE-S3),MySQL透明数据加密(TDE)
+- 敏感字段:AES-256-GCM/SM4加密存储,动态脱敏查询
+- 密钥管理:Hashicorp Vault统一管理,定期轮换
+
+**身份与权限层**:
+- 身份认证:JWT Token + 刷新Token机制
+- 权限控制:Casbin实现RBAC+ABAC混合模型
+- 细粒度控制:支持数据级权限(用户只能访问有权限的数据集)
+
+**审计追溯层**:
+- 100%操作审计覆盖,不可篡改
+- 审计日志存储到ES,保留90天
+- 敏感操作(删除、导出、权限变更)实时告警
+
+### 7.4 可观测性体系
+
+系统建立**Logging + Metrics + Tracing**三位一体的可观测体系:
+
+**Logging(日志)**:
+- 结构化日志(JSON格式),包含TraceID关联
+- 日志分级(DEBUG/INFO/WARN/ERROR)
+- 集中存储到ELK,支持全文检索和告警
+
+**Metrics(指标)**:
+- 业务指标:日处理量、质量评分分布、审核通过率
+- 系统指标:QPS、延迟、错误率、资源利用率
+- 采集方式:Prometheus Pull模型,Grafana可视化
+
+**Tracing(追踪)**:
+- 基于OpenTelemetry标准
+- 全链路追踪覆盖所有服务
+- Jaeger提供可视化调用链分析
+- 关键操作(数据处理、审核流程)自动埋点
+
+---
+
+## 八、部署与运维架构
+
+### 8.1 容器化部署方案
+
+所有服务采用容器化部署,基于Kubernetes进行编排管理。
+
+| 服务类型 | 副本数 | CPU请求/限制 | 内存请求/限制 | 特殊配置 |
+|----------|--------|--------------|---------------|----------|
+| 采集网关(Go) | 3-10(HPA) | 500m / 2核 | 512Mi / 2Gi | 与Kafka消费Lag联动HPA |
+| 数据集服务(Go) | 3-5 | 500m / 2核 | 512Mi / 2Gi | 无状态,水平扩展 |
+| 审核后台(Go) | 2-3 | 500m / 1核 | 512Mi / 1Gi | 无状态 |
+| 看板API(Go) | 2-3 | 500m / 1核 | 512Mi / 1Gi | WebSocket连接管理 |
+| Python处理引擎 | 4-10(HPA) | 2核 / 4核 | 4Gi / 8Gi | Celery Worker,任务队列感知 |
+| 看板前端(Vue) | 2 | 200m / 500m | 256Mi / 512Mi | Nginx静态服务 |
+
+### 8.2 持续集成与交付
+
+采用**GitOps**实践,实现自动化CI/CD流水线:
+
+1. **代码提交**:开发人员提交代码到Git仓库
+2. **CI阶段**:触发单元测试、代码扫描、镜像构建、镜像推送
+3. **CD阶段**:ArgoCD监控Git仓库中的部署配置,自动同步到Kubernetes集群
+4. **灰度发布**:金丝雀部署策略,新版本先替换10%流量,观察后再全量
+5. **回滚能力**:一键回滚到任意历史版本
+
+### 8.3 监控告警配置
+
+建立多级告警机制,确保问题及时发现和响应:
+
+| 告警级别 | 触发条件 | 响应方式 | 响应时效 |
+|----------|----------|----------|----------|
+| **P0(紧急)** | 服务不可用、数据丢失 | 电话+短信+钉钉 | 立即响应,15分钟内恢复 |
+| **P1(严重)** | 处理延迟>10秒、错误率>5% | 钉钉+短信 | 30分钟内响应 |
+| **P2(警告)** | 存储使用率>80%、Kafka积压>100万 | 钉钉通知 | 2小时内处理 |
+| **P3(提示)** | 处理速度下降、质量评分波动 | 邮件通知 | 工作时间处理 |
+
+---
+
+## 九、开发路线图与里程碑
+
+### 9.1 分阶段实施计划
+
+| 阶段 | 周期 | 核心交付 | 里程碑 | 风险点 |
+|------|------|----------|--------|--------|
+| **Phase 1:基础接入** | 4周 | Go采集网关、Kafka对接、基础数据模型 | 完成数据从源端到Kafka的采集 | Kafka消费组管理、分区再平衡 |
+| **Phase 2:核心处理** | 5周 | Python清洗+脱敏引擎、质量评估基础版、MySQL元数据 | 完成清洗→脱敏→评估的完整流水线 | 大文件内存溢出(Dask分批处理) |
+| **Phase 3:版本管理** | 3周 | Go数据集管理服务、Delta版本控制、MinIO存储 | 完成数据集创建、版本存储和查询 | Delta算法性能需Benchmark优化 |
+| **Phase 4:审核流程** | 3周 | Temporal工作流引擎、审核后台、审核台前端 | 完成从数据到审核通过入库的完整闭环 | 工作流状态机复杂度管理 |
+| **Phase 5:服务输出** | 3周 | 看板API+WebSocket、前端看板、开放API | 完成数据服务化输出能力 | WebSocket连接数管理 |
+| **Phase 6:治理安全** | 3周 | Casbin权限、ELK审计日志、Jaeger追踪 | 完成合规安全体系全覆盖 | 策略动态加载性能 |
+| **Phase 7:上线运营** | 3周 | K8s部署、监控告警、压测调优、文档完善 | 系统正式上线,SLA达标 | 资源配额与自动扩缩容策略调优 |
+
+### 9.2 关键里程碑验收标准
+
+| 里程碑 | 验收标准 |
+|--------|----------|
+| M1:采集上线 | 采集网关

+ 300 - 0
docs/数据集采集详情.md

@@ -0,0 +1,300 @@
+# 数据集平台系统架构与数据流全景说明
+
+## 一、系统整体定位
+
+这是一个**企业级AI数据集治理平台**,核心使命是将分散在各业务系统(Token运营平台、AI工作台)中的原始数据,经过**采集→清洗→脱敏→分类→标注→质量评估→审核→版本管理**的全链条处理,最终形成高质量、合规、可追溯的标准化数据集,供下游AI模型训练、数据分析等场景使用。
+
+---
+
+## 二、系统分层架构总览
+
+系统采用**七层架构**设计,自上而下分别为:
+
+| 层级 | 职责 | 核心组件 |
+|------|------|----------|
+| **数据源层** | 产生原始数据的外部系统 | Token运营平台、AI工作台 |
+| **采集接入层** | 将数据从源端接入平台 | 采集网关、消息队列、调度器 |
+| **数据处理层** | 对原始数据进行加工治理 | 清洗、脱敏、分类、标注、质量评估引擎 |
+| **审核管理层** | 对处理后的数据进行合规审批 | 工作流引擎、审核后台 |
+| **数据集管理层** | 对通过审核的数据进行版本化管理 | 数据集服务、版本控制、存储管理 |
+| **数据服务层** | 对外提供数据展示和输出能力 | 看板后端、WebSocket推送、开放API |
+| **治理与审计层** | 保障系统安全合规可追溯 | 元数据中心、权限控制、审计日志、链路追踪 |
+
+各层之间通过**Kafka消息队列**和**RESTful/gRPC接口**进行异步解耦和同步调用,基础设施层(Kubernetes + Prometheus + Grafana)为全系统提供容器编排、监控告警和弹性伸缩能力。
+
+---
+
+## 三、端到端数据流转全流程
+
+### 3.1 完整数据链路(10个关键步骤)
+
+```
+步骤1:数据产生
+  └─ Token运营平台记录用户调用日志(每次API调用的输入输出)
+  └─ AI工作台记录问答对话、文件解析结果、知识引用记录
+  └─ 这些数据通过Kafka Producer实时写入消息队列
+
+步骤2:数据采集
+  └─ Go采集网关作为Kafka Consumer,持续消费数据流
+  └─ 网关进行初步校验(字段完整性、格式合法性)
+  └─ 校验通过的数据写入Kafka的"待处理"分区,失败的进入死信队列
+
+步骤3:数据清洗
+  └─ Python清洗引擎拉取"待处理"数据
+  └─ 执行去重(基于TraceID)、空值填充、格式标准化、字段映射
+  └─ 清洗后的数据回写Kafka的"已清洗"主题
+
+步骤4:数据脱敏
+  └─ Python脱敏引擎消费"已清洗"数据
+  └─ 自动识别敏感字段(手机号、身份证、邮箱等)
+  └─ 根据策略中心配置执行掩码/AES加密/SM4加密
+  └─ 脱敏后数据进入"已脱敏"主题
+
+步骤5:智能分类
+  └─ Python分类引擎读取"已脱敏"数据
+  └─ 结合规则匹配和机器学习模型自动打标
+  └─ 按行业(金融/医疗/政务)、场景(对话/问答/材料生成)、来源自动归类
+  └─ 分类结果写入"已分类"主题
+
+步骤6:数据标注
+  └─ Python标注引擎处理"已分类"数据
+  └─ 先通过预训练NLP模型进行半自动预标注
+  └─ 低置信度样本推送至人工标注台(Vue前端)
+  └─ 标注完成后数据进入"已标注"主题
+
+步骤7:质量评估
+  └─ Python质量评估引擎对每条/每批数据打分
+  └─ 从完整性、准确性、一致性、时效性、合规性五个维度综合评分
+  └─ 低于阈值(如0.75分)的数据打回重新处理或标记为低质量
+  └─ 评分结果作为元数据附加到数据集上
+
+步骤8:合规审核
+  └─ 质量达标的数据触发Go工作流引擎(Temporal)
+  └─ 启动多级审批流程:提交→初审→复审→归档
+  └─ 审核人员通过审核台(Go + Gin后台)查看数据详情和敏感级别
+  └─ 审核通过的数据进入"待入库"状态
+
+步骤9:数据集版本化管理
+  └─ Go数据集管理服务接收审核通过的数据
+  └─ 创建新数据集或已有数据集的新版本
+  └─ 采用Delta差异算法存储(每10个版本做一次全量快照)
+  └─ 元数据存入MySQL,实际数据文件存入MinIO/OSS对象存储
+
+步骤10:数据服务与消费
+  └─ 看板API(Go)聚合各类统计信息,通过WebSocket实时推送给前端
+  └─ 看板前端(Vue3 + ECharts)展示数据量、质量趋势、行业分布、审核状态
+  └─ 第三方系统通过标准化gRPC/REST接口订阅或拉取数据集
+  └─ 全链路审计日志写入ELK,权限控制贯穿所有操作
+```
+
+### 3.2 数据流转过程中的关键决策点
+
+| 决策点 | 触发条件 | 处理逻辑 | 去向 |
+|--------|----------|----------|------|
+| **采集校验失败** | 字段缺失/格式错误 | 记录失败原因,写入死信队列 | 人工排查或重试 |
+| **质量评分过低** | 综合评分 < 0.75 | 标记为"待优化",触发人工复核 | 退回处理层重新加工 |
+| **敏感级别过高** | 合规性评分 < 0.6 | 拒绝入库,强制人工审核 | 进入特殊审批流程 |
+| **版本回滚需求** | 用户发起回滚操作 | 从Delta日志重建指定版本 | 生成新版本号并标记为回滚版 |
+| **存储策略切换** | 数据集达到阈值(如100万条) | 增量存储切换为快照模式 | 创建全量快照释放Delta累积 |
+
+---
+
+## 四、各模块功能配合关系
+
+### 4.1 采集网关与数据处理引擎的配合
+
+采集网关(Go)和数据处理引擎(Python)通过**Kafka主题分区**实现解耦:
+
+- 采集网关负责"生产速度",确保高吞吐数据不丢失
+- 数据处理引擎负责"消费能力",按自身处理速度拉取数据
+- 当处理引擎消费能力不足时,Kafka自动积压起到**削峰填谷**作用
+- 两者通过**消费者组**机制实现水平扩展,互不阻塞
+
+### 4.2 数据处理引擎内部各子模块配合
+
+六个子模块形成**流水线(Pipeline)**架构:
+
+```
+原始数据 → [清洗] → [脱敏] → [分类] → [标注] → [质量评估] → 待审核
+   ↑         ↑         ↑         ↑         ↑          ↑
+   |    规则库    策略中心   模型库   标注平台   评分模型
+   └─────── 各模块通过Kafka主题串联,异步并行 ───────┘
+```
+
+每个模块独立部署,**通过Kafka主题传递数据**,实现了:
+- **弹性伸缩**:瓶颈模块可单独扩容
+- **故障隔离**:单个模块宕机不影响上下游
+- **灰度发布**:可单独升级某个处理环节
+
+### 4.3 工作流引擎与数据集管理的联动
+
+审核通过后,工作流引擎(Temporal)通过**异步回调**通知数据集管理服务:
+
+```
+Temporal工作流状态机:
+  [提交] → (初审中) → [初审通过/驳回] 
+         → (复审中) → [复审通过/驳回] 
+         → (归档中) → [归档完成] → 回调通知数据集服务
+```
+
+数据集管理服务收到回调后,执行**原子化操作**:
+1. 开启数据库事务
+2. 创建数据集版本记录(含元数据)
+3. 将数据文件写入对象存储
+4. 更新数据集当前版本指针
+5. 提交事务
+6. 触发后续事件(如通知看板刷新、发送消息通知)
+
+### 4.4 看板服务与全链路信息的聚合
+
+看板服务(Go)作为**信息聚合层**,从多个数据源实时拉取信息:
+
+| 数据来源 | 获取方式 | 看板展示内容 |
+|----------|----------|--------------|
+| MySQL(数据集表) | 定时查询 | 数据集总数、版本数、各状态数量 |
+| Elasticsearch(审计日志) | 聚合查询 | 操作行为统计、用户活跃度 |
+| Prometheus(监控指标) | 实时查询 | 系统吞吐量、处理延迟、错误率 |
+| Kafka(实时流) | WebSocket推送 | 今日入库量、实时质量评分趋势 |
+| 处理引擎(Python) | Redis缓存 | 各处理环节的积压情况、处理速度 |
+
+---
+
+## 五、关键技术选型与设计考量
+
+### 5.1 为什么Go和Python分工协作?
+
+| 语言 | 负责模块 | 核心优势 | 典型场景 |
+|------|----------|----------|----------|
+| **Go** | 采集网关、调度器、数据集管理、审核后台、看板API、权限控制 | 高并发、低内存占用、快速启动、自带并发原语 | 处理大量连接、高频API调用、I/O密集型任务 |
+| **Python** | 清洗、脱敏、分类、标注、质量评估 | 丰富的科学计算库、AI生态完善、算法迭代灵活 | 数据处理、模型推理、复杂业务规则实现 |
+
+**边界清晰**:Go负责"搬运"和"管理",Python负责"加工"和"判断",两者通过Kafka和API接口通信,互不侵入对方代码。
+
+### 5.2 消息队列的核心作用
+
+Kafka在整个系统中承担**数据总线**的角色:
+- **削峰填谷**:采集高峰时数据先在Kafka积压,处理层按能力消费
+- **解耦模块**:各处理环节通过Kafka主题连接,可独立部署和升级
+- **重放能力**:处理失败时可重置消费偏移量,重新处理历史数据
+- **多路分发**:同一份数据可被多个消费者组同时消费(如数据分析和审计)
+
+### 5.3 存储架构的分层设计
+
+```
+热数据(近期高频访问)→  MySQL(元数据) + MinIO(文件)
+温数据(定期访问)    →  HDFS冷存储 + 元数据保留在MySQL
+冷数据(归档备份)    →  云存储归档层(OSS IA/Glacier)
+审计日志              →  Elasticsearch(近实时检索)
+处理中间态            →  Kafka(最多保留7天)
+```
+
+### 5.4 为什么选择Temporal作为工作流引擎?
+
+相比传统的工作流引擎(如Activiti、Flowable),Temporal提供:
+- **强一致性**:工作流状态持久化,宕机后可从中断点恢复
+- **超时控制**:每个步骤可设置超时,超时后自动触发补偿
+- **可观测性**:内置Web UI查看所有工作流执行状态
+- **Go原生**:与平台主语言一致,代码嵌入性强
+
+---
+
+## 六、系统关键能力
+
+### 6.1 数据质量保障机制
+
+贯穿全链路的**四重质量保障**:
+
+1. **采集校验**:字段完整性、数据格式检查
+2. **处理质量**:清洗引擎剔除脏数据、脱敏引擎确保合规
+3. **主动评估**:质量评分引擎量化数据可用性
+4. **人工审核**:关键数据经过多级审批
+
+### 6.2 数据版本管理能力
+
+- **增量存储**:每次变更只存储差异(Delta),节省存储空间约70%
+- **快照机制**:每10个版本自动创建全量快照,保证重建效率
+- **一键回滚**:可从任意历史版本快速重建数据集
+- **差异对比**:可视化展示两个版本之间的字段变化和记录差异
+
+### 6.3 全链路可观测性
+
+- **分布式追踪**:Jaeger追踪单个数据从采集到入库的全链路耗时
+- **指标监控**:Prometheus采集每个服务的QPS、延迟、错误率
+- **结构化日志**:ELK集中存储,支持复杂查询和告警
+- **业务看板**:实时展示数据处理进度、积压量、质量趋势
+
+---
+
+## 七、系统核心流程时序图
+
+以下用文字描述关键流程的时序配合:
+
+### 流程一:一条数据从产生到入库的完整旅程
+
+```
+用户操作 → Token平台产生日志 → Kafka → 采集网关拉取 
+→ 网关校验后写入"待处理"主题 → 清洗引擎消费并清洗 → 写入"已清洗"主题
+→ 脱敏引擎消费并脱敏 → 写入"已脱敏"主题 → 分类引擎打标 → 写入"已分类"主题
+→ 标注引擎预标注 → 低置信度送人工标注台 → 写入"已标注"主题
+→ 质量评估引擎打分 → 低质量打回重处理 → 高质量触发工作流
+→ 工作流发起审核 → 审核员审批通过 → 回调数据集服务
+→ 数据集服务创建版本 → 存储元数据+文件 → 看板实时更新
+→ 审计日志记录全过程
+```
+
+### 流程二:数据异常时的处理路径
+
+```
+情况A:采集校验失败
+  采集网关拒绝接收 → 写入死信队列 → 告警通知运维 → 人工排查后重推
+
+情况B:质量评分过低
+  质量引擎标记低质量 → 不进入审核流程 → 退回数据处理层 → 人工介入优化规则
+
+情况C:审核驳回
+  工作流状态变更为"已驳回" → 不创建数据集版本 → 记录驳回原因 → 数据进入待优化池
+```
+
+### 流程三:数据消费与输出流程
+
+```
+外部系统申请数据集 → 权限校验(Casbin) → 校验通过
+→ 查询数据集管理服务 → 获取最新版本元数据
+→ 从MinIO读取实际文件 → 按权限级别脱敏输出
+→ 记录消费审计日志 → 通过gRPC/REST返回数据
+```
+
+---
+
+## 八、系统扩展性设计
+
+### 8.1 水平扩展能力
+
+- **采集网关**:通过Kafka消费者组实现,增加节点自动分担分区
+- **处理引擎**:Celery Worker可动态增减,任务队列自动负载均衡
+- **数据集服务**:无状态设计,Kubernetes HPA根据CPU/内存自动扩缩容
+
+### 8.2 新数据源接入
+
+- 新增数据源只需实现Kafka Producer接口,将数据写入标准Topic
+- 采集网关支持多源配置,无需修改代码
+- 数据分类引擎通过新增规则/模型即可支持新类型数据
+
+### 8.3 新处理环节添加
+
+- 在数据处理流水线中新增模块,只需订阅上游Topic,写入下游Topic
+- 不影响现有模块运行,实现"热插拔"
+
+---
+
+## 九、总结
+
+整个系统的核心设计理念是**"职责分离、异步解耦、分层治理"**:
+
+1. **职责分离**:Go负责高性能I/O和业务逻辑,Python负责数据智能处理,各取所长
+2. **异步解耦**:Kafka作为数据总线连接所有模块,实现松耦合、高可用
+3. **分层治理**:从采集到服务的七层架构,每层有明确的输入输出和质量标准
+4. **可观测性**:日志、指标、追踪三位一体,确保系统运行状态透明可见
+5. **合规安全**:脱敏、权限、审计贯穿全流程,满足企业级数据治理要求
+
+这套架构已在类似的大规模数据平台中得到验证,能够支撑**日均亿级数据量**的处理,并具有良好的扩展性和维护性。

+ 203 - 0
docs/知识库建设.md

@@ -0,0 +1,203 @@
+
+# 企业级知识库系统建设方案(架构师决策版)
+
+> 版本:V2.0 | 编制日期:2026年7月 | 密级:内部公开
+
+---
+
+## 一、战略定位与建设愿景
+
+### 1.1 战略定位
+本系统不是简单的文档管理系统(DMS),而是**企业知识资产化的核心基础设施**。其战略价值体现在三个层面:
+
+- **资产沉淀层**:将散落在文件服务器、个人电脑、邮件附件中的非结构化文档,转化为可检索、可关联、可复用的结构化知识单元。
+- **智能服务层**:以RAG(检索增强生成)能力赋能上层AI应用,为内部员工、外部客户、生态伙伴提供精准知识服务。
+- **安全合规层**:在私有化环境中实现组织级数据强隔离、全链路溯源审计,满足等保、GDPR及行业监管要求。
+
+### 1.2 建设目标(SMART原则)
+| 维度 | 量化目标 |
+|------|----------|
+| 覆盖范围 | 支撑≥10个一级组织,≥100个知识库,≥500万份文档入库 |
+| 解析精度 | 原生PDF结构化字段准确率≥95%,扫描件OCR文字识别率≥90%(中英文) |
+| 检索性能 | 语义检索P99≤800ms,全文检索P99≤300ms |
+| 系统可用性 | 年度可用性≥99.9%,RPO≤15分钟,RTO≤30分钟 |
+| API吞吐 | 单组织QPS≥200,支持突发流量10倍弹性扩展 |
+
+---
+
+## 二、架构深化与关键决策
+
+### 2.1 总体架构视图(四横三纵)
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                      接入层(统一网关)                          │
+│   管理端(Web/运营后台)  │  客户端(Web/App)  │  OpenAPI      │
+└─────────────────────────────────────────────────────────────────┘
+                                    │
+┌─────────────────────────────────────────────────────────────────┐
+│                   业务服务层                                     │
+│  组织权限  │  文档资源  │  知识目录  │  溯源审计  │  RAG问答   │
+└─────────────────────────────────────────────────────────────────┘
+                                    │
+┌─────────────────────────────────────────────────────────────────┐
+│                智能解析与索引层(核心能力池)                    │
+│  解析调度引擎 → 专项Worker集群 → 结构化输出 → 向量化/索引构建  │
+└─────────────────────────────────────────────────────────────────┘
+                                    │
+┌─────────────────────────────────────────────────────────────────┐
+│                    混合存储层(多模数据)                       │
+│  MySQL  │  Elasticsearch  │  Milvus  │  MinIO  │  Redis      │
+└─────────────────────────────────────────────────────────────────┘
+                                    │
+┌─────────────┐  ┌─────────────┐  ┌─────────────┐
+│  运维体系   │  │  安全体系   │  │  监控体系   │
+│  日志·链路·│  │  加密·鉴权·│  │  指标·告警·│
+│  备份·恢复 │  │  审计·脱敏 │  │  链路追踪   │
+└─────────────┘  └─────────────┘  └─────────────┘
+```
+
+### 2.2 核心技术架构决策(含备选与权衡)
+
+| 决策点 | 选定方案 | 备选方案 | 决策依据与权衡 |
+|--------|----------|----------|----------------|
+| **PDF/扫描解析** | MinerU(主)+ PyMuPDF(降级) | 自研OCR+版面分析 | MinerU开源且Apache2.0,精度业内领先,但GPU消耗较大,需独立算力池 |
+| **向量数据库** | Milvus(分布式) | Qdrant / pgvector | Milvus支持多组织物理分区、百万级向量检索性能稳定,社区活跃 |
+| **检索引擎** | Elasticsearch + 向量混合检索 | OpenSearch | ES生态完善,支持全文+向量混合查询,且团队熟悉度高 |
+| **消息队列** | RabbitMQ(持久化+死信) | RocketMQ / Pulsar | RabbitMQ轻量稳定,死信机制成熟,满足异步重试和降级需求 |
+| **对象存储** | MinIO(集群模式) | Ceph / 华为OBS | MinIO兼容S3,部署简单,支持多租户桶隔离,适合私有化场景 |
+| **网关** |  Kong / APISIX /nginx| 集成JWT+API Key双鉴权 |
+
+### 2.3 数据模型与隔离设计(核心安全架构)
+
+- **强制组织隔离策略**:所有数据模型(MySQL表、ES索引、Milvus分区、MinIO桶路径)均以`org_id`作为**一级分区键**,全局拦截器在DAO层自动注入过滤条件,上层业务代码完全无感。
+- **三权分立**:系统管理员(平台运维)、安全审计员(日志审计)、业务管理员(组织知识管理)权限分离,满足内控要求。
+- **细粒度权限矩阵**:支持“目录级-文档级-字段级”三层权限控制,API访问权限独立于用户权限体系,采用独立API Key+Scope机制。
+
+---
+
+## 三、核心能力深化设计
+
+### 3.1 解析引擎架构(性能与精度双优)
+
+```
+                   ┌───────────┐
+                   │ 文件上传  │
+                   └─────┬─────┘
+                         ▼
+                   ┌───────────┐
+                   │ 类型识别  │
+                   └─────┬─────┘
+                         │
+         ┌───────────────┼───────────────┐
+         ▼               ▼               ▼
+   ┌───────────┐  ┌───────────┐  ┌───────────┐
+   │PDF队列    │  │Office队列 │  │图片队列   │
+   └─────┬─────┘  └─────┬─────┘  └─────┬─────┘
+         ▼               ▼               ▼
+   ┌───────────┐  ┌───────────┐  ┌───────────┐
+   │MinerU集群 │  │Tika+POI   │  │PaddleOCR  │
+   │(GPU×4)   │  │集群(CPU)  │  │集群(CPU)  │
+   └─────┬─────┘  └─────┬─────┘  └─────┬─────┘
+         └───────────────┼───────────────┘
+                         ▼
+              ┌─────────────────────┐
+              │ 统一结构化输出格式   │
+              │  (JSON + Markdown)  │
+              │  + 溯源元数据固化   │
+              └─────────────────────┘
+```
+
+**关键设计细节**:
+- **热数据优先解析**:上传即解析,解析结果缓存到Redis(有效期7天),重复检索无需二次解析。
+- **冷数据批量回填**:存量文档通过定时任务按优先级(最近访问→最近修改→最早入库)分批解析入库,避免瞬时资源抢占。
+- **降级链**:MinerU失败 → PyMuPDF提取纯文本 → 兜底返回“暂不支持解析”+人工标记。
+
+### 3.2 混合检索与RAG能力
+
+- **双路召回架构**:
+  - 关键词路:ES BM25 + 自定义同义词词典(行业术语库)
+  - 语义路:Milvus ANN检索(Embedding模型采用BGE-M3,支持多语言)
+- **融合排序**:采用RRF(倒数排名融合)算法,结合业务权重(文档新鲜度、访问热度)二次调权。
+- **溯源增强**:检索结果携带完整的`(file_id, page_num, bbox, snapshot_url)`,前端直接高亮定位。
+
+### 3.3 开放API安全设计
+
+- **API分级**:
+  - L1(公开):知识库目录查询、文档元数据查询(需API Key)
+  - L2(受限):全文/语义检索、智能问答(需API Key+IP白名单)
+  - L3(内部):文档上传、更新、删除(需API Key+用户Token双认证)
+- **数据脱敏规则**:对外API默认不返回全文内容,仅返回片段+溯源链接(需单独鉴权下载),防止批量知识外泄。
+
+---
+
+## 四、落地实施路线图(分四期)
+
+| 阶段 | 周期 | 核心交付 | 验收标准 |
+|------|------|----------|----------|
+| **一期:基础底座+核心解析** | 基础设施部署、MinerU集群搭建、PDF/Office解析上线、组织权限模型 | 支持10份混合PDF解析,结构化输出准确率≥85% |
+| **二期:检索+溯源+API** | 混合检索引擎、全链路溯源、OpenAPI网关、管理后台 | 检索响应<1s,溯源信息完整率100%,API通过安全扫描 |
+| **三期:AI增强+高可用** | RAG问答服务、向量库集群、主从切换、监控告警体系 | QPS≥100,可用性≥99.9%,故障自动恢复<5min |
+| **四期:规模化+优化** | 存量文档批量导入、性能压测调优、运维SOP文档、培训推广 | 支撑百万级文档,P99达标,通过等保二级测评 |
+
+---
+
+## 五、资源评估与成本控制
+
+### 5.1 硬件资源估算(以10万份文档/年增量、日活500用户为基准)
+
+| 组件 | 配置建议 | 数量 | 年估算成本(万元) |
+|------|----------|------|-------------------|
+| MinerUp解析节点 | GPU:A10/RTX4090,CPU:8C,内存:32G | 2~4(弹性扩缩) | 8~15 |
+| CPU解析节点(Office/OCR) | CPU:16C,内存:64G | 3~5 | 4~6 |
+| 业务微服务节点 | CPU:8C,内存:16G | 6(3主3备) | 3~5 |
+| MySQL/RDS | 16C64G SSD,主从+只读 | 2主4从 | 4~6 |
+| Elasticsearch集群 | 16C64G SSD,3热2温节点 | 5 | 5~8 |
+| Milvus向量库 | 16C64G SSD,3分片2副本 | 6 | 6~10 |
+| MinIO集群 | 8C32G,4T HDD×3节点 | 3 | 2~3 |
+| Redis缓存 | 16G内存,主从+哨兵 | 2 | 1~2 |
+| **合计(首年)** | | | **33~55万** |
+
+> 注:可根据实际文档量按需缩减,初期可合并节点,后续线性扩展。
+
+### 5.2 成本优化策略
+- 使用Spot实例/抢占式实例用于非关键解析任务(离线批量回填)。
+- 向量检索分层存储:热数据(近3个月)用Milvus内存索引,温数据用磁盘索引,冷数据归档到对象存储+外部索引文件。
+- 采用Kubernetes HPA(水平Pod自动伸缩)按工作日/夜间负载自动调整节点数。
+
+---
+
+## 六、风险治理与应急预案
+
+| 风险类别 | 具体描述 | 缓解措施 | 应急预案 |
+|----------|----------|----------|----------|
+| **解析性能瓶颈** | 高峰期大量PDF上传导致MinerU队列积压 | 独立GPU队列+最大并发控制(每节点2任务)+超时熔断 | 自动降级为PyMuPDF快速提取,延迟入库,事后补解析 |
+| **数据越权泄露** | 跨组织检索或API返回其他组织数据 | 三层过滤(SQL注入org_id、ES索引分区、MinIO桶隔离)+ 定期渗透测试 | 实时告警+自动冻结涉嫌API Key,人工审计 |
+| **向量/索引不一致** | 文档更新后旧向量未清理 | 采用“软删除+异步清理”机制,事务性更新MySQL状态后,MQ触发清理任务 | 每日凌晨全量对账,发现差异自动修复 |
+| **服务雪崩** | 依赖组件(ES/Milvus)故障导致整体不可用 | 缓存热点数据+熔断降级(检索失败返回缓存结果) | 自动摘除故障节点,切换至备用集群(冷备) |
+| **AI生成幻觉** | RAG问答生成不实内容 | 强制检索结果作为上下文,生成内容附引用源,置信度阈值过滤 | 人工抽检+反馈闭环,模型定期微调 |
+
+---
+
+## 七、架构演进方向
+
+- **多模态扩展**:支持视频帧提取+语音转写,构建富媒体知识库。
+- **知识图谱构建**:从文档中自动抽取实体关系,实现“检索”到“推理”的升级。
+- **联邦学习**:在多组织场景下,在不共享原始文档的前提下,训练全局Embedding模型,提升冷门知识检索效果。
+- **Serverless化**:解析任务按需启动函数计算,进一步降低空闲资源成本。
+
+---
+
+## 八、总结与决策建议
+
+本方案在企业级知识库领域具备**三个核心差异化优势**:
+
+1. **解析精度行业领先**:MinerU为核心的开源解析栈,成本可控且效果超越闭源商业产品。
+2. **安全合规内建**:从存储到业务到网关的三层隔离机制,满足金融、政务等严苛行业要求。
+3. **开放与可演进**:API标准化程度高,RAG能力为未来AI应用爆发预留接口。
+
+**建议决策路径**:
+- 若企业**文档规模<1万份、以Office为主**,可考虑降本方案(Tika+ES全文检索为主,暂不引入MinerU)。
+- 若**PDF/扫描件占比较高、且需对外提供智能服务**,推荐按本方案一期+二期优先落地,抢占知识管理先机。
+
+---

+ 144 - 0
docs/获取应用-webapp-设置.md

@@ -0,0 +1,144 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://docs.dify.ai/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# 获取应用 WebApp 设置
+
+> 获取应用的 WebApp 设置,包括站点配置、主题和自定义选项。
+
+
+
+## OpenAPI
+
+````yaml /zh/api-reference/openapi_completion.json get /site
+openapi: 3.0.1
+info:
+  title: 文本生成型应用 API
+  description: 文本生成型应用提供无会话支持,适用于翻译、文章撰写、摘要总结 AI 等场景。
+  version: 1.0.0
+servers:
+  - url: https://{api_base_url}
+    description: 文本生成型应用 API 的基础 URL。自托管部署时,替换为你的 API 基础 URL。
+    variables:
+      api_base_url:
+        default: api.dify.ai/v1
+        description: API 基础 URL 的主机与路径,不含 `https://` 前缀。
+security:
+  - ApiKeyAuth: []
+tags:
+  - name: 文本生成
+    description: 文本生成相关操作。
+  - name: 文件操作
+    description: 文件管理相关操作。
+  - name: 终端用户
+    description: 终端用户信息相关操作。
+  - name: 消息反馈
+    description: 用户反馈相关操作。
+  - name: 语音与文字转换
+    description: 语音转文字和文字转语音相关操作。
+  - name: 应用配置
+    description: 获取应用设置和信息的操作。
+paths:
+  /site:
+    get:
+      tags:
+        - 应用配置
+      summary: 获取应用 WebApp 设置
+      description: 获取应用的 WebApp 设置,包括站点配置、主题和自定义选项。
+      operationId: getWebAppSettings
+      responses:
+        '200':
+          description: 应用的 WebApp 设置。
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/WebAppSettingsResponse'
+              examples:
+                webAppSettings:
+                  summary: 响应示例
+                  value:
+                    title: My Chat App
+                    chat_color_theme: '#4A90D9'
+                    chat_color_theme_inverted: false
+                    icon_type: emoji
+                    icon: 🤖
+                    icon_background: '#FFFFFF'
+                    icon_url: null
+                    description: 一个有用的客服聊天机器人。
+                    copyright: 2025 Dify
+                    privacy_policy: https://example.com/privacy
+                    custom_disclaimer: ''
+                    default_language: en-US
+                    show_workflow_steps: false
+                    use_icon_as_answer_icon: true
+        '403':
+          description: '`forbidden` : 未找到此应用的站点或工作空间已归档。'
+          content:
+            application/json:
+              examples:
+                forbidden:
+                  summary: forbidden
+                  value:
+                    status: 403
+                    code: forbidden
+                    message: Forbidden.
+components:
+  schemas:
+    WebAppSettingsResponse:
+      type: object
+      properties:
+        title:
+          type: string
+          description: WebApp 标题。
+        chat_color_theme:
+          type: string
+          description: 聊天主题颜色。
+        chat_color_theme_inverted:
+          type: boolean
+          description: 聊天主题颜色是否反转。
+        icon_type:
+          type: string
+          description: 使用的图标类型。`emoji` 为表情图标,`image` 为上传的图片图标。
+        icon:
+          type: string
+          description: 图标内容(表情或图片 ID)。
+        icon_background:
+          type: string
+          description: 图标背景颜色。
+        icon_url:
+          type: string
+          format: url
+          nullable: true
+          description: 图标图片的 URL。
+        description:
+          type: string
+          description: WebApp 描述。
+        copyright:
+          type: string
+          description: 版权文本。
+        privacy_policy:
+          type: string
+          description: 隐私政策 URL。
+        custom_disclaimer:
+          type: string
+          description: 自定义免责声明文本。
+        default_language:
+          type: string
+          description: 默认语言代码。
+        show_workflow_steps:
+          type: boolean
+          description: 是否显示工作流步骤。
+        use_icon_as_answer_icon:
+          type: boolean
+          description: 是否使用应用图标作为回答图标。
+  securitySchemes:
+    ApiKeyAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: API_KEY
+      description: >-
+        API Key 认证。对于所有 API 请求,请在 `Authorization` HTTP 头中包含您的 API Key,并加上
+        `Bearer ` 前缀。示例:`Authorization: Bearer {API_KEY}`。**强烈建议将 API Key
+        存储在服务端,不要在客户端共享或存储,以避免 API Key 泄漏导致严重后果。**
+
+````

+ 101 - 0
docs/获取应用元数据.md

@@ -0,0 +1,101 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://docs.dify.ai/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# 获取应用元数据
+
+> 获取应用的元数据,包括工具图标和其他配置详情。
+
+
+
+## OpenAPI
+
+````yaml /zh/api-reference/openapi_completion.json get /meta
+openapi: 3.0.1
+info:
+  title: 文本生成型应用 API
+  description: 文本生成型应用提供无会话支持,适用于翻译、文章撰写、摘要总结 AI 等场景。
+  version: 1.0.0
+servers:
+  - url: https://{api_base_url}
+    description: 文本生成型应用 API 的基础 URL。自托管部署时,替换为你的 API 基础 URL。
+    variables:
+      api_base_url:
+        default: api.dify.ai/v1
+        description: API 基础 URL 的主机与路径,不含 `https://` 前缀。
+security:
+  - ApiKeyAuth: []
+tags:
+  - name: 文本生成
+    description: 文本生成相关操作。
+  - name: 文件操作
+    description: 文件管理相关操作。
+  - name: 终端用户
+    description: 终端用户信息相关操作。
+  - name: 消息反馈
+    description: 用户反馈相关操作。
+  - name: 语音与文字转换
+    description: 语音转文字和文字转语音相关操作。
+  - name: 应用配置
+    description: 获取应用设置和信息的操作。
+paths:
+  /meta:
+    get:
+      tags:
+        - 应用配置
+      summary: 获取应用元数据
+      description: 获取应用的元数据,包括工具图标和其他配置详情。
+      operationId: getCompletionAppMetaZh
+      responses:
+        '200':
+          description: 成功获取应用元数据。
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/AppMetaResponse'
+              examples:
+                appMeta:
+                  summary: 响应示例
+                  value:
+                    tool_icons:
+                      dalle3: https://example.com/icons/dalle3.png
+                      calculator:
+                        background: '#4A90D9'
+                        content: 🧮
+components:
+  schemas:
+    AppMetaResponse:
+      type: object
+      properties:
+        tool_icons:
+          type: object
+          additionalProperties:
+            oneOf:
+              - title: Icon URL
+                type: string
+                format: url
+                description: 图标的 URL。
+              - $ref: '#/components/schemas/ToolIconDetail'
+          description: 工具图标。键为工具名称。
+    ToolIconDetail:
+      title: Emoji Icon
+      type: object
+      description: 使用 emoji 的工具图标详情。
+      properties:
+        background:
+          type: string
+          description: 十六进制格式的背景颜色。
+        content:
+          type: string
+          description: Emoji 内容。
+  securitySchemes:
+    ApiKeyAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: API_KEY
+      description: >-
+        API Key 认证。对于所有 API 请求,请在 `Authorization` HTTP 头中包含您的 API Key,并加上
+        `Bearer ` 前缀。示例:`Authorization: Bearer {API_KEY}`。**强烈建议将 API Key
+        存储在服务端,不要在客户端共享或存储,以避免 API Key 泄漏导致严重后果。**
+
+````

+ 101 - 0
docs/获取应用基本信息.md

@@ -0,0 +1,101 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://docs.dify.ai/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# 获取应用基本信息
+
+> 获取应用的基本信息,包括名称、描述、标签和模式。
+
+
+
+## OpenAPI
+
+````yaml /zh/api-reference/openapi_completion.json get /info
+openapi: 3.0.1
+info:
+  title: 文本生成型应用 API
+  description: 文本生成型应用提供无会话支持,适用于翻译、文章撰写、摘要总结 AI 等场景。
+  version: 1.0.0
+servers:
+  - url: https://{api_base_url}
+    description: 文本生成型应用 API 的基础 URL。自托管部署时,替换为你的 API 基础 URL。
+    variables:
+      api_base_url:
+        default: api.dify.ai/v1
+        description: API 基础 URL 的主机与路径,不含 `https://` 前缀。
+security:
+  - ApiKeyAuth: []
+tags:
+  - name: 文本生成
+    description: 文本生成相关操作。
+  - name: 文件操作
+    description: 文件管理相关操作。
+  - name: 终端用户
+    description: 终端用户信息相关操作。
+  - name: 消息反馈
+    description: 用户反馈相关操作。
+  - name: 语音与文字转换
+    description: 语音转文字和文字转语音相关操作。
+  - name: 应用配置
+    description: 获取应用设置和信息的操作。
+paths:
+  /info:
+    get:
+      tags:
+        - 应用配置
+      summary: 获取应用基本信息
+      description: 获取应用的基本信息,包括名称、描述、标签和模式。
+      operationId: getAppInfo
+      responses:
+        '200':
+          description: 应用的基本信息。
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/AppInfoResponse'
+              examples:
+                appInfo:
+                  summary: 响应示例
+                  value:
+                    name: My Completion App
+                    description: 一个有用的客服聊天机器人。
+                    tags:
+                      - customer-service
+                      - chatbot
+                    mode: completion
+                    author_name: Dify Team
+components:
+  schemas:
+    AppInfoResponse:
+      type: object
+      properties:
+        name:
+          type: string
+          description: 应用名称。
+        description:
+          type: string
+          description: 应用描述。
+        tags:
+          type: array
+          items:
+            type: string
+          description: 应用标签。
+        mode:
+          type: string
+          description: >-
+            应用模式。`completion` 为文本生成应用,`chat` 为基础对话应用,`agent-chat` 为 Agent
+            应用,`advanced-chat` 为对话流应用,`workflow` 为工作流应用。
+        author_name:
+          type: string
+          description: 应用作者名称。
+  securitySchemes:
+    ApiKeyAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: API_KEY
+      description: >-
+        API Key 认证。对于所有 API 请求,请在 `Authorization` HTTP 头中包含您的 API Key,并加上
+        `Bearer ` 前缀。示例:`Authorization: Bearer {API_KEY}`。**强烈建议将 API Key
+        存储在服务端,不要在客户端共享或存储,以避免 API Key 泄漏导致严重后果。**
+
+````

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>智能体使用平台</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.jsx"></script>
+  </body>
+</html>

+ 29 - 0
package.json

@@ -0,0 +1,29 @@
+{
+  "name": "ai-agentic-pt",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "lint": "eslint .",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@arco-design/web-react": "^2.66.16",
+    "@emoji-mart/data": "^1.2.1",
+    "marked": "^18.0.6",
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1",
+    "react-router-dom": "^6.23.1"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.29.7",
+    "@babel/plugin-transform-react-jsx": "^7.29.7",
+    "@babel/traverse": "^7.29.7",
+    "@types/react": "^18.3.3",
+    "@types/react-dom": "^18.3.0",
+    "@vitejs/plugin-react": "^4.3.1",
+    "vite": "^5.3.1"
+  }
+}

BIN
public/logo.png


+ 5701 - 0
src/App.css

@@ -0,0 +1,5701 @@
+.app {
+  display: flex;
+  min-height: 100vh;
+  position: relative;
+  z-index: 1;
+}
+
+/* Arco shell layout used by src/components/Layout.jsx */
+.arco-shell {
+  min-height: 100vh;
+  background: #f5f7fb;
+}
+
+.arco-shell-sider {
+  position: fixed !important;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 30;
+  height: 100vh;
+  background: #ffffff !important;
+  border-right: 1px solid #e5e8ef;
+  box-shadow: 8px 0 24px rgba(15, 23, 42, 0.05);
+}
+
+.arco-shell-sider .arco-layout-sider-children {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.arco-shell-brand {
+  height: 72px;
+  padding: 14px 16px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  cursor: pointer;
+  border-bottom: 1px solid #f0f2f5;
+}
+
+.arco-shell-logo {
+  width: 38px;
+  height: 38px;
+  object-fit: contain;
+  flex-shrink: 0;
+}
+
+.arco-shell-brand-text {
+  display: flex;
+  flex-direction: column;
+  min-width: 0;
+}
+
+.arco-shell-brand-name {
+  color: #1d2129;
+  font-size: 15px;
+  font-weight: 700;
+  line-height: 20px;
+}
+
+.arco-shell-brand-subtitle {
+  color: #86909c;
+  font-size: 12px;
+  line-height: 18px;
+}
+
+.arco-shell-menu {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  padding: 8px;
+  border-right: 0 !important;
+}
+
+.arco-shell-menu .arco-menu-item,
+.arco-shell-menu .arco-menu-inline-header {
+  border-radius: 8px;
+  margin: 2px 0;
+}
+
+.arco-shell-menu .arco-menu-item-inner,
+.arco-shell-menu .arco-menu-inline-header-title {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.arco-shell-menu .arco-icon {
+  width: 16px;
+  height: 16px;
+  flex: 0 0 16px;
+  margin-right: 0;
+}
+
+.arco-shell-menu .arco-menu-item-inner > span:not(.layout-agent-icon),
+.arco-shell-menu .arco-menu-inline-header-title > span:not(.layout-agent-icon) {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.layout-agent-icon {
+  width: 22px;
+  height: 22px;
+  border-radius: 5px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  background: #f2f3f5;
+  flex-shrink: 0;
+  margin-right: 10px;
+  vertical-align: middle;
+}
+
+.layout-agent-icon img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.layout-agent-emoji {
+  font-size: 14px;
+}
+
+.layout-agent-name {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.arco-shell-sider-footer {
+  padding: 10px 8px;
+  border-top: 1px solid #f0f2f5;
+  flex-shrink: 0;
+}
+
+.arco-shell-collapse {
+  width: 100%;
+  justify-content: center;
+  color: #4e5969;
+}
+
+.arco-shell-body {
+  height: 100vh;
+  margin-left: 248px;
+  transition: margin-left 0.2s ease;
+  overflow: hidden;
+}
+
+.arco-layout-has-sider:has(.arco-layout-sider-collapsed) .arco-shell-body {
+  margin-left: 64px;
+}
+
+.arco-shell-header {
+  flex-shrink: 0;
+  z-index: 15;
+  height: 64px;
+  padding: 0 28px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: rgba(255, 255, 255, 0.92);
+  border-bottom: 1px solid #e5e8ef;
+  backdrop-filter: blur(8px);
+}
+
+.arco-shell-header-title {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  min-width: 0;
+}
+
+.arco-shell-kicker {
+  color: #1d2129;
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 24px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.arco-shell-title.arco-typography {
+  margin: 0;
+  color: #86909c;
+  font-size: 12px;
+  font-weight: 400;
+  line-height: 16px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.arco-shell-user {
+  height: 40px;
+  padding: 4px 8px;
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  color: #4e5969;
+}
+
+.arco-shell-avatar {
+  background: #165dff;
+}
+
+.arco-shell-user-name {
+  font-size: 14px;
+  max-width: 160px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.arco-shell .main-content.arco-shell-content {
+  margin-left: 0;
+  height: calc(100vh - 64px);
+  background: #f5f7fb;
+  overflow: auto;
+}
+
+.arco-shell .main-page-content.arco-shell-page {
+  padding: 24px 28px 36px;
+  width: 100%;
+  max-width: 1440px;
+  margin: 0 auto;
+}
+
+.layout-user-menu {
+  min-width: 176px;
+}
+
+.layout-user-menu-danger {
+  color: #f53f3f;
+}
+
+@media (max-width: 900px) {
+  .arco-shell-body {
+    margin-left: 64px;
+  }
+
+  .arco-shell-header {
+    padding: 0 16px;
+  }
+
+  .arco-shell-user-name,
+  .arco-shell-kicker {
+    display: none;
+  }
+
+  .arco-shell .main-page-content.arco-shell-page {
+    padding: 18px 16px 28px;
+  }
+}
+
+.sidebar {
+  width: 280px;
+  background: linear-gradient(180deg, #ffffff 0%, #f8faff 100%);
+  border-right: 1px solid rgba(102, 126, 234, 0.2);
+  box-shadow: 4px 0 20px rgba(102, 126, 234, 0.1);
+  display: flex;
+  flex-direction: column;
+  position: fixed;
+  height: 100vh;
+  left: 0;
+  top: 0;
+  z-index: 100;
+  transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* 收起状态 */
+.app-collapsed .sidebar {
+  width: 72px;
+}
+
+.app-collapsed .main-content {
+  margin-left: 72px;
+}
+
+.app-collapsed .logo-text,
+.app-collapsed .menu-item-label,
+.app-collapsed .arrow,
+.app-collapsed .submenu-inline {
+  display: none;
+}
+
+.app-collapsed .menu {
+  padding: 20px 0;
+}
+
+.app-collapsed .menu-item {
+  padding: 14px 0;
+  margin: 4px 8px;
+  justify-content: center;
+  border-radius: 12px;
+  border-left: none;
+}
+
+.app-collapsed .menu-item:hover {
+  transform: none;
+}
+
+.app-collapsed .menu-item-text {
+  justify-content: center;
+  gap: 0;
+}
+
+.app-collapsed .menu-item-parent {
+  position: relative;
+}
+
+.app-collapsed .menu-icon {
+  filter: drop-shadow(0 2px 4px rgba(102, 126, 234, 0.3));
+}
+
+.app-collapsed .sidebar-header {
+  padding: 18px 12px;
+  display: flex;
+  justify-content: center;
+}
+
+.app-collapsed .logo {
+  justify-content: center;
+  gap: 0;
+}
+
+.app-collapsed .logo-image {
+  width: 32px;
+  height: 32px;
+}
+
+/* 折叠态下的子菜单浮层 */
+.submenu-popover {
+  position: absolute;
+  left: calc(100% + 8px);
+  top: 0;
+  min-width: 180px;
+  max-height: 0;
+  overflow: hidden;
+  background: white;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 12px;
+  box-shadow: 0 10px 30px rgba(102, 126, 234, 0.18);
+  z-index: 200;
+  padding: 6px 0;
+  opacity: 0;
+  visibility: hidden;
+  transform: translateX(-6px);
+  transition: max-height 0.3s ease, opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
+}
+
+.app-collapsed .menu-item-parent:hover .submenu-popover {
+  max-height: 400px;
+  opacity: 1;
+  visibility: visible;
+  transform: translateX(0);
+}
+
+.app-collapsed .submenu-popover .submenu-item {
+  padding: 10px 18px;
+  margin: 2px 8px;
+  border-radius: 8px;
+  color: #4a5568;
+  font-size: 13px;
+  font-weight: 500;
+  transition: all 0.2s ease;
+}
+
+.app-collapsed .submenu-popover .submenu-item::before {
+  left: 12px;
+}
+
+.app-collapsed .submenu-popover .submenu-item:hover {
+  color: #667eea;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(0, 212, 255, 0.06) 100%);
+}
+
+.app-collapsed .submenu-popover .submenu-item.active {
+  color: #764ba2;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(0, 212, 255, 0.08) 100%);
+}
+
+.sidebar-header {
+  padding: 28px 24px;
+  border-bottom: 1px solid rgba(102, 126, 234, 0.2);
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.05) 50%, transparent 100%);
+  transition: padding 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.sidebar-toggle {
+  width: 100%;
+  padding: 8px 12px;
+  border: none;
+  border-radius: 10px;
+  background: transparent;
+  color: #64748b;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  transition: all 0.2s ease;
+  line-height: 1;
+}
+
+.sidebar-toggle-icon {
+  font-size: 16px;
+  font-weight: 700;
+  color: #667eea;
+  transition: transform 0.2s ease;
+}
+
+.sidebar-toggle:hover {
+  background: rgba(102, 126, 234, 0.08);
+  color: #667eea;
+}
+
+.sidebar-toggle:hover .sidebar-toggle-icon {
+  transform: scale(1.15);
+}
+
+.logo {
+  font-weight: bold;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.logo-text {
+  display: flex;
+  align-items: baseline;
+  gap: 6px;
+  position: relative;
+}
+
+.logo-text-main {
+  font-family: 'Brush Script MT', 'Lucida Handwriting', 'Snell Roundhand', 'Apple Chancery', cursive;
+  font-size: 30px;
+  font-weight: 800;
+  letter-spacing: 0.5px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 45%, #f093fb 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  text-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
+  font-style: italic;
+  transform: skewX(-6deg);
+  display: inline-block;
+  position: relative;
+  line-height: 1;
+}
+
+.logo-text-main::after {
+  content: '';
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: -3px;
+  height: 2px;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
+  border-radius: 2px;
+  opacity: 0.7;
+}
+
+.logo-text-sub {
+  font-family: 'Optima', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+  font-size: 18px;
+  font-weight: 700;
+  letter-spacing: 4px;
+  background: linear-gradient(135deg, #764ba2 0%, #f093fb 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  position: relative;
+  padding-left: 6px;
+  line-height: 1;
+}
+
+.logo-text-sub::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 2px;
+  height: 14px;
+  background: linear-gradient(180deg, #667eea 0%, #f093fb 100%);
+  border-radius: 2px;
+}
+
+.logo-icon {
+  width: 36px;
+  height: 36px;
+  border: 2px solid transparent;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  background-clip: padding-box;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  animation: glow-pulse 2.5s ease-in-out infinite;
+}
+
+.logo-image {
+  width: 36px;
+  height: 36px;
+  object-fit: contain;
+  filter: drop-shadow(0 2px 8px rgba(102, 126, 234, 0.4));
+}
+
+.logo-icon::before {
+  content: '';
+  position: absolute;
+  inset: -2px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #00d4ff 100%);
+  z-index: -1;
+  opacity: 0.6;
+  filter: blur(4px);
+}
+
+@keyframes glow-pulse {
+  0%, 100% {
+    box-shadow: 0 0 15px rgba(102, 126, 234, 0.4), 0 0 30px rgba(118, 75, 162, 0.2);
+  }
+  50% {
+    box-shadow: 0 0 25px rgba(102, 126, 234, 0.6), 0 0 50px rgba(118, 75, 162, 0.3);
+  }
+}
+
+.menu {
+  flex: 1;
+  padding: 20px 0;
+  overflow-y: auto;
+}
+
+.sidebar-footer {
+  padding: 20px 24px;
+  border-top: 1px solid rgba(102, 126, 234, 0.2);
+  background: linear-gradient(0deg, rgba(102, 126, 234, 0.05) 0%, transparent 100%);
+}
+
+.logout-btn {
+  width: 100%;
+  padding: 14px 20px;
+  background: rgba(239, 68, 68, 0.1);
+  border: 1px solid rgba(239, 68, 68, 0.2);
+  border-radius: 12px;
+  color: #dc2626;
+  font-size: 15px;
+  font-weight: 600;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  transition: all 0.3s ease;
+}
+
+.logout-btn:hover {
+  background: rgba(239, 68, 68, 0.15);
+  border-color: rgba(239, 68, 68, 0.3);
+  transform: translateX(4px);
+  box-shadow: 0 4px 15px rgba(239, 68, 68, 0.15);
+}
+
+.menu-item {
+  padding: 16px 24px;
+  cursor: pointer;
+  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border-left: 4px solid transparent;
+  position: relative;
+  margin: 4px 12px;
+  border-radius: 0 12px 12px 0;
+}
+
+.menu-item:hover {
+  background: linear-gradient(90deg, rgba(102, 126, 234, 0.12) 0%, rgba(0, 212, 255, 0.08) 100%);
+  border-left-color: #667eea;
+  transform: translateX(4px);
+}
+
+.menu-item.active {
+  background: linear-gradient(90deg, rgba(102, 126, 234, 0.18) 0%, rgba(0, 212, 255, 0.12) 100%);
+  border-left-color: #764ba2;
+}
+
+.menu-item::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100%;
+  width: 0;
+  background: linear-gradient(90deg, rgba(102, 126, 234, 0.15), transparent);
+  transition: width 0.4s ease;
+  border-radius: 0 12px 12px 0;
+}
+
+.menu-item:hover::before,
+.menu-item.active::before {
+  width: 100%;
+}
+
+.menu-item-text {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  position: relative;
+  z-index: 1;
+  font-weight: 500;
+}
+
+.menu-icon {
+  width: 24px;
+  height: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+  filter: drop-shadow(0 2px 4px rgba(102, 126, 234, 0.3));
+}
+
+.arrow {
+  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  font-size: 14px;
+  color: #667eea;
+}
+
+.arrow.open {
+  transform: rotate(90deg);
+}
+
+.submenu {
+  max-height: 0;
+  overflow: hidden;
+  transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  background: rgba(102, 126, 234, 0.03);
+}
+
+.submenu.open {
+  max-height: 600px;
+}
+
+.submenu-item {
+  padding: 14px 24px 14px 60px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  color: #4a5568;
+  position: relative;
+  margin: 2px 16px;
+  border-radius: 8px;
+}
+
+.submenu-item::before {
+  content: '';
+  position: absolute;
+  left: 40px;
+  top: 50%;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  transform: translateY(-50%) scale(0.6);
+  transition: all 0.3s ease;
+  opacity: 0.5;
+}
+
+.submenu-item:hover {
+  color: #667eea;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(0, 212, 255, 0.06) 100%);
+  transform: translateX(4px);
+}
+
+.submenu-item:hover::before {
+  transform: translateY(-50%) scale(1);
+  opacity: 1;
+  box-shadow: 0 0 12px rgba(102, 126, 234, 0.6);
+}
+
+.submenu-item.active {
+  color: #764ba2;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(0, 212, 255, 0.08) 100%);
+}
+
+.submenu-item.active::before {
+  transform: translateY(-50%) scale(1);
+  opacity: 1;
+  box-shadow: 0 0 15px rgba(102, 126, 234, 0.7);
+}
+
+.main-content {
+  flex: 1;
+  margin-left: 280px;
+  min-height: 100vh;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.main-header {
+  padding: 0;
+  min-height: 0;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  position: relative;
+  z-index: 1001;
+}
+
+.header-avatar {
+  width: 44px;
+  height: 44px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.header-avatar:hover {
+  transform: scale(1.1);
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+}
+
+.header-avatar-wrapper {
+  position: fixed;
+  top: 16px;
+  right: 32px;
+  z-index: 1100;
+}
+
+.user-dropdown {
+  position: absolute;
+  top: 60px;
+  right: 0;
+  background: white;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 16px;
+  box-shadow: 0 10px 40px rgba(102, 126, 234, 0.2), 0 4px 20px rgba(118, 75, 162, 0.1);
+  min-width: 200px;
+  padding: 8px 0;
+  z-index: 1000;
+  animation: dropdownSlideIn 0.2s ease-out;
+}
+
+@keyframes dropdownSlideIn {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.dropdown-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 20px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  font-size: 14px;
+  font-weight: 500;
+  color: #1e293b;
+}
+
+.dropdown-item:hover {
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(0, 212, 255, 0.04) 100%);
+  color: #667eea;
+}
+
+.dropdown-item.logout:hover {
+  background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(239, 68, 68, 0.04) 100%);
+  color: #dc2626;
+}
+
+.dropdown-icon {
+  font-size: 18px;
+}
+
+.dropdown-divider {
+  height: 1px;
+  background: rgba(102, 126, 234, 0.1);
+  margin: 8px 0;
+}
+
+.avatar-icon {
+  font-size: 24px;
+}
+
+.main-page-content {
+  flex: 1;
+  padding: 0 40px 40px 40px;
+  position: relative;
+}
+
+.page-header {
+  margin-bottom: 40px;
+}
+
+.page-title {
+  font-size: 38px;
+  font-weight: 800;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #00d4ff 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  margin-bottom: 10px;
+  letter-spacing: -0.5px;
+}
+
+.page-subtitle {
+  color: #64748b;
+  font-size: 15px;
+  font-weight: 500;
+}
+
+/* 卡片样式 */
+.card-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+  gap: 28px;
+}
+
+.card {
+  background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 20px;
+  padding: 28px;
+  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  position: relative;
+  overflow: hidden;
+  box-shadow: 0 4px 20px rgba(102, 126, 234, 0.08);
+}
+
+.card::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 3px;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #00d4ff 100%);
+  transform: translateX(-100%);
+  transition: transform 0.6s ease;
+}
+
+.card::after {
+  content: '';
+  position: absolute;
+  inset: -1px;
+  border-radius: 20px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.1) 50%, rgba(0, 212, 255, 0.08) 100%);
+  opacity: 0;
+  transition: opacity 0.4s ease;
+  z-index: -1;
+  filter: blur(8px);
+}
+
+.card:hover {
+  transform: translateY(-8px) scale(1.02);
+  border-color: rgba(102, 126, 234, 0.4);
+  box-shadow: 0 20px 60px rgba(102, 126, 234, 0.2), 0 8px 25px rgba(118, 75, 162, 0.15);
+}
+
+.card:hover::before {
+  transform: translateX(100%);
+}
+
+.card:hover::after {
+  opacity: 1;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  gap: 18px;
+  margin-bottom: 18px;
+}
+
+.card-icon {
+  width: 56px;
+  height: 56px;
+  border-radius: 16px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(0, 212, 255, 0.1) 100%);
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28px;
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.15);
+  transition: all 0.4s ease;
+}
+
+.card:hover .card-icon {
+  transform: scale(1.1) rotate(5deg);
+  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+}
+
+.card-title {
+  font-size: 20px;
+  font-weight: 700;
+  color: #1e293b;
+  margin-bottom: 6px;
+  background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.card-description {
+  color: #64748b;
+  font-size: 14px;
+  line-height: 1.7;
+  font-weight: 500;
+}
+
+.card-tags {
+  display: flex;
+  gap: 10px;
+  margin-top: 20px;
+  flex-wrap: wrap;
+}
+
+.tag {
+  padding: 6px 16px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(0, 212, 255, 0.08) 100%);
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 24px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #667eea;
+  transition: all 0.3s ease;
+}
+
+.tag:hover {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  transform: scale(1.05);
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
+}
+
+/* 列表样式 */
+.list {
+  display: flex;
+  flex-direction: column;
+  gap: 18px;
+}
+
+.list-item {
+  background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 16px;
+  padding: 24px 28px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  position: relative;
+  overflow: hidden;
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.06);
+}
+
+.list-item::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  width: 4px;
+  background: linear-gradient(180deg, #667eea 0%, #764ba2 50%, #00d4ff 100%);
+  transform: scaleY(0);
+  transition: transform 0.4s ease;
+  border-radius: 0 2px 2px 0;
+}
+
+.list-item::after {
+  content: '';
+  position: absolute;
+  inset: 0;
+  background: linear-gradient(90deg, rgba(102, 126, 234, 0.08) 0%, rgba(0, 212, 255, 0.04) 100%);
+  opacity: 0;
+  transition: opacity 0.4s ease;
+}
+
+.list-item:hover {
+  transform: translateX(8px) translateY(-2px);
+  border-color: rgba(102, 126, 234, 0.4);
+  box-shadow: 0 10px 40px rgba(102, 126, 234, 0.15), 0 4px 20px rgba(118, 75, 162, 0.1);
+}
+
+.list-item:hover::before {
+  transform: scaleY(1);
+}
+
+.list-item:hover::after {
+  opacity: 1;
+}
+
+.list-item-content {
+  flex: 1;
+  position: relative;
+  z-index: 1;
+}
+
+.list-item-title {
+  font-size: 18px;
+  font-weight: 700;
+  color: #1e293b;
+  margin-bottom: 8px;
+}
+
+.list-item-meta {
+  display: flex;
+  gap: 20px;
+  color: #64748b;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.meta-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.list-item-action {
+  padding: 12px 28px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border: none;
+  border-radius: 12px;
+  color: white;
+  font-weight: 700;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  position: relative;
+  z-index: 1;
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.list-item-action:hover {
+  transform: scale(1.08) translateY(-2px);
+  box-shadow: 0 8px 30px rgba(102, 126, 234, 0.5), 0 4px 15px rgba(118, 75, 162, 0.3);
+  background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
+}
+
+.history-page {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.history-page-header {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.history-page-title.arco-typography {
+  margin: 0 0 6px;
+  color: #1d2129;
+  font-size: 24px;
+  font-weight: 600;
+  line-height: 32px;
+}
+
+.history-page-subtitle.arco-typography {
+  color: #86909c;
+  font-size: 14px;
+  line-height: 22px;
+}
+
+.history-count-tag.arco-tag {
+  margin-top: 4px;
+  border-radius: 4px;
+}
+
+.history-card.arco-card {
+  border-color: #e5e6eb;
+  border-radius: 4px;
+  box-shadow: none;
+}
+
+.history-card .arco-card-body {
+  padding: 0;
+}
+
+.history-list {
+  display: flex;
+  flex-direction: column;
+}
+
+.history-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  min-height: 80px;
+  padding: 16px 20px;
+  border-bottom: 1px solid #f2f3f5;
+  background: #ffffff;
+  transition: background-color 0.2s ease;
+}
+
+.history-item:last-child {
+  border-bottom: 0;
+}
+
+.history-item:hover {
+  background: #f7f8fa;
+}
+
+.history-item-main {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  min-width: 0;
+}
+
+.history-item-icon {
+  width: 36px;
+  height: 36px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  border-radius: 4px;
+  background: #e8f3ff;
+  color: #165dff;
+  font-size: 18px;
+}
+
+.history-item-content {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  min-width: 0;
+}
+
+.history-item-title.arco-typography {
+  color: #1d2129;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+}
+
+.history-item-meta.arco-space {
+  color: #86909c;
+  font-size: 13px;
+  line-height: 20px;
+}
+
+.history-item-meta span {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.history-detail-btn.arco-btn-text {
+  flex-shrink: 0;
+  color: #165dff;
+}
+
+.history-detail-btn.arco-btn-text:hover {
+  background: #e8f3ff;
+  color: #4080ff;
+}
+
+.available-agents-page {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.available-agents-header {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.available-agents-title.arco-typography {
+  margin: 0 0 6px;
+  color: #1d2129;
+  font-size: 24px;
+  font-weight: 600;
+  line-height: 32px;
+}
+
+.available-agents-subtitle.arco-typography {
+  color: #86909c;
+  font-size: 14px;
+  line-height: 22px;
+}
+
+.available-agents-count-tag.arco-tag {
+  margin-top: 4px;
+  border-radius: 4px;
+}
+
+.available-agents-card.arco-card {
+  border-color: #e5e6eb;
+  border-radius: 4px;
+  box-shadow: none;
+}
+
+.available-agents-card .arco-card-body {
+  padding: 0;
+}
+
+.available-agents-list {
+  display: flex;
+  flex-direction: column;
+}
+
+.available-agent-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  min-height: 92px;
+  padding: 18px 20px;
+  border-bottom: 1px solid #f2f3f5;
+  background: #ffffff;
+  transition: background-color 0.2s ease;
+}
+
+.available-agent-item:last-child {
+  border-bottom: 0;
+}
+
+.available-agent-item:hover {
+  background: #f7f8fa;
+}
+
+.available-agent-main {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  min-width: 0;
+}
+
+.available-agent-icon {
+  width: 40px;
+  height: 40px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  border-radius: 4px;
+  background: #e8f3ff;
+  color: #165dff;
+  font-size: 20px;
+}
+
+.available-agent-content {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+  min-width: 0;
+}
+
+.available-agent-name.arco-typography {
+  color: #1d2129;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+}
+
+.available-agent-description.arco-typography {
+  color: #4e5969;
+  font-size: 13px;
+  line-height: 20px;
+}
+
+.available-agent-meta.arco-space {
+  color: #86909c;
+  font-size: 12px;
+  line-height: 20px;
+}
+
+.available-agent-meta span {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.available-agent-actions {
+  display: inline-flex;
+  align-items: center;
+  gap: 12px;
+  flex-shrink: 0;
+}
+
+.available-agent-status.arco-tag {
+  border-radius: 4px;
+}
+
+.available-agent-use-btn.arco-btn-primary {
+  border-radius: 4px;
+  box-shadow: none;
+}
+
+/* 科技感装饰线 */
+.tech-line {
+  height: 2px;
+  background: linear-gradient(90deg, transparent 0%, #667eea 25%, #764ba2 50%, #00d4ff 75%, transparent 100%);
+  margin: 40px 0;
+  border-radius: 2px;
+}
+
+/* Profile Modal Styles */
+.profile-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  backdrop-filter: blur(4px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.profile-modal {
+  background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 24px;
+  width: 90%;
+  max-width: 900px;
+  height: 70vh;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 20px 60px rgba(102, 126, 234, 0.2), 0 8px 25px rgba(118, 75, 162, 0.15);
+  animation: modalSlideIn 0.3s ease-out;
+}
+
+@keyframes modalSlideIn {
+  from {
+    opacity: 0;
+    transform: scale(0.9) translateY(-20px);
+  }
+  to {
+    opacity: 1;
+    transform: scale(1) translateY(0);
+  }
+}
+
+.profile-modal-header {
+  padding: 24px 28px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid rgba(102, 126, 234, 0.15);
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, transparent 100%);
+}
+
+.profile-modal-header h2 {
+  margin: 0;
+  font-size: 22px;
+  font-weight: 800;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #00d4ff 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.profile-close-btn {
+  width: 36px;
+  height: 36px;
+  border-radius: 10px;
+  border: none;
+  background: rgba(239, 68, 68, 0.1);
+  color: #dc2626;
+  font-size: 18px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s ease;
+}
+
+.profile-close-btn:hover {
+  background: rgba(239, 68, 68, 0.2);
+  transform: rotate(90deg);
+}
+
+.profile-tabs {
+  display: flex;
+  gap: 8px;
+  padding: 16px 28px;
+  border-bottom: 1px solid rgba(102, 126, 234, 0.1);
+}
+
+.profile-tab {
+  padding: 10px 20px;
+  border: none;
+  border-radius: 12px;
+  background: transparent;
+  color: #64748b;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.profile-tab:hover {
+  background: rgba(102, 126, 234, 0.08);
+  color: #667eea;
+}
+
+.profile-tab.active {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+}
+
+.profile-tab-content {
+  flex: 1;
+  padding: 28px;
+  overflow-y: auto;
+}
+
+/* Account Info */
+.account-info {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.account-avatar-section {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+  padding: 20px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(0, 212, 255, 0.03) 100%);
+  border-radius: 16px;
+  border: 1px solid rgba(102, 126, 234, 0.15);
+}
+
+.account-avatar {
+  width: 80px;
+  height: 80px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 40px;
+  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+}
+
+.account-name-edit {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  width: 100%;
+  justify-content: center;
+}
+
+.account-name {
+  font-size: 20px;
+  font-weight: 700;
+  color: #1e293b;
+}
+
+.account-name-input {
+  flex: 1;
+  padding: 10px 16px;
+  border: 1px solid rgba(102, 126, 234, 0.3);
+  border-radius: 10px;
+  background: white;
+  color: #1e293b;
+  font-size: 16px;
+  font-weight: 600;
+  outline: none;
+  text-align: center;
+}
+
+.account-name-input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
+}
+
+.account-name-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.account-name-btn {
+  padding: 8px 16px;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.account-name-btn.save {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border: none;
+  color: white;
+}
+
+.account-name-btn.save:hover {
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.35);
+}
+
+.account-name-btn.cancel {
+  background: rgba(102, 126, 234, 0.1);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  color: #667eea;
+}
+
+.account-name-btn.cancel:hover {
+  background: rgba(102, 126, 234, 0.2);
+}
+
+.account-name-edit-btn {
+  padding: 6px 12px;
+  background: rgba(102, 126, 234, 0.1);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 8px;
+  color: #667eea;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.account-name-edit-btn:hover {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.account-field {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.account-field label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #64748b;
+}
+
+.account-field input {
+  padding: 14px 18px;
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 12px;
+  background: rgba(255, 255, 255, 0.8);
+  color: #1e293b;
+  font-size: 15px;
+  outline: none;
+  transition: all 0.3s ease;
+}
+
+.account-field input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
+  background: white;
+}
+
+.account-actions {
+  display: flex;
+  gap: 12px;
+}
+
+.account-action-btn {
+  flex: 1;
+  padding: 14px;
+  border: 1px solid rgba(102, 126, 234, 0.3);
+  border-radius: 12px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(0, 212, 255, 0.04) 100%);
+  color: #667eea;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.account-action-btn:hover {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35);
+}
+
+.account-password-section {
+  margin-top: 20px;
+  padding-top: 20px;
+  border-top: 1px solid rgba(102, 126, 234, 0.15);
+}
+
+.account-password-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1e293b;
+  margin-bottom: 12px;
+}
+
+.account-password-form {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding: 16px;
+  background: rgba(102, 126, 234, 0.03);
+  border: 1px solid rgba(102, 126, 234, 0.1);
+  border-radius: 12px;
+}
+
+.account-password-field {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.account-password-field label {
+  font-size: 13px;
+  font-weight: 600;
+  color: #64748b;
+}
+
+.account-password-field input {
+  padding: 12px 16px;
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 10px;
+  background: white;
+  color: #1e293b;
+  font-size: 14px;
+  outline: none;
+  transition: all 0.3s ease;
+}
+
+.account-password-field input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
+}
+
+.account-password-actions {
+  display: flex;
+  gap: 12px;
+  margin-top: 8px;
+}
+
+.account-password-btn {
+  flex: 1;
+  padding: 12px;
+  border-radius: 10px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.account-password-btn.save {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border: none;
+  color: white;
+}
+
+.account-password-btn.save:hover:not(:disabled) {
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35);
+  transform: translateY(-2px);
+}
+
+.account-password-btn.save:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+.account-password-btn.cancel {
+  background: rgba(102, 126, 234, 0.1);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  color: #667eea;
+}
+
+.account-password-btn.cancel:hover {
+  background: rgba(102, 126, 234, 0.2);
+}
+
+/* API Key Info */
+.apikey-info {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.apikey-create-section {
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(0, 212, 255, 0.03) 100%);
+  border: 1px solid rgba(102, 126, 234, 0.15);
+  border-radius: 12px;
+  padding: 20px;
+}
+
+.apikey-create-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1e293b;
+  margin-bottom: 12px;
+}
+
+.apikey-create-form {
+  display: flex;
+  gap: 12px;
+}
+
+.apikey-expires-input {
+  flex: 1;
+  padding: 12px 16px;
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 10px;
+  background: white;
+  font-size: 14px;
+  outline: none;
+  transition: all 0.3s ease;
+}
+
+.apikey-expires-input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
+}
+
+.apikey-create-btn {
+  padding: 12px 24px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border: none;
+  border-radius: 10px;
+  color: white;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  white-space: nowrap;
+}
+
+.apikey-create-btn:hover:not(:disabled) {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35);
+}
+
+.apikey-create-btn:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+.apikey-list {
+  border: 1px solid rgba(102, 126, 234, 0.15);
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.apikey-list-header {
+  display: grid;
+  grid-template-columns: 2fr 100px 180px 140px;
+  gap: 16px;
+  padding: 12px 16px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(0, 212, 255, 0.04) 100%);
+  border-bottom: 1px solid rgba(102, 126, 234, 0.15);
+  font-size: 12px;
+  font-weight: 600;
+  color: #64748b;
+  text-transform: uppercase;
+}
+
+.apikey-list-item {
+  display: grid;
+  grid-template-columns: 2fr 100px 180px 140px;
+  gap: 16px;
+  padding: 12px 16px;
+  border-bottom: 1px solid rgba(102, 126, 234, 0.1);
+  align-items: center;
+  transition: background 0.2s ease;
+}
+
+.apikey-list-item:last-child {
+  border-bottom: none;
+}
+
+.apikey-list-item:hover {
+  background: rgba(102, 126, 234, 0.03);
+}
+
+.apikey-loading,
+.apikey-empty {
+  padding: 40px 16px;
+  text-align: center;
+  color: #64748b;
+  font-size: 14px;
+}
+
+.apikey-token {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.apikey-token-input {
+  flex: 1;
+  padding: 8px 12px;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 8px;
+  background: rgba(255, 255, 255, 0.6);
+  font-size: 13px;
+  font-family: monospace;
+  color: #1e293b;
+}
+
+.apikey-copy-btn {
+  padding: 6px 10px;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 6px;
+  background: rgba(102, 126, 234, 0.05);
+  cursor: pointer;
+  transition: all 0.2s ease;
+  font-size: 12px;
+}
+
+.apikey-copy-btn:hover {
+  background: rgba(102, 126, 234, 0.15);
+}
+
+.apikey-status {
+  display: inline-block;
+  padding: 4px 12px;
+  border-radius: 20px;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.apikey-status.active {
+  background: rgba(34, 197, 94, 0.1);
+  color: #22c55e;
+}
+
+.apikey-status.inactive {
+  background: rgba(107, 114, 128, 0.1);
+  color: #6b7280;
+}
+
+.apikey-expires {
+  font-size: 13px;
+  color: #64748b;
+}
+
+.apikey-remain {
+  font-size: 12px;
+  color: #94a3b8;
+  margin-left: 4px;
+}
+
+.apikey-toggle-btn,
+.apikey-delete-btn {
+  padding: 6px 12px;
+  border-radius: 8px;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  margin-right: 8px;
+}
+
+.apikey-toggle-btn {
+  background: rgba(102, 126, 234, 0.1);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  color: #667eea;
+}
+
+.apikey-toggle-btn:hover:not(:disabled) {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.apikey-delete-btn {
+  background: rgba(239, 68, 68, 0.1);
+  border: 1px solid rgba(239, 68, 68, 0.2);
+  color: #dc2626;
+}
+
+.apikey-delete-btn:hover:not(:disabled) {
+  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+  color: white;
+}
+
+.apikey-toggle-btn:disabled,
+.apikey-delete-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.apikey-description {
+  padding: 16px 20px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(0, 212, 255, 0.03) 100%);
+  border-radius: 12px;
+  border: 1px solid rgba(102, 126, 234, 0.15);
+  color: #64748b;
+  font-size: 14px;
+  line-height: 1.6;
+}
+
+.apikey-display {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.apikey-display input {
+  flex: 1;
+  padding: 14px 18px;
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 12px;
+  background: rgba(255, 255, 255, 0.8);
+  color: #1e293b;
+  font-size: 15px;
+  font-family: monospace;
+  outline: none;
+  transition: all 0.3s ease;
+}
+
+.apikey-toggle-btn-old {
+  padding: 14px 20px;
+  border: 1px solid rgba(102, 126, 234, 0.3);
+  border-radius: 12px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(0, 212, 255, 0.04) 100%);
+  color: #667eea;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  white-space: nowrap;
+  transition: all 0.3s ease;
+}
+
+.apikey-toggle-btn-old:hover {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.35);
+}
+
+.apikey-actions {
+  display: flex;
+  gap: 12px;
+}
+
+.apikey-action-btn {
+  flex: 1;
+  padding: 14px;
+  border: 1px solid rgba(102, 126, 234, 0.3);
+  border-radius: 12px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(0, 212, 255, 0.04) 100%);
+  color: #667eea;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.apikey-action-btn.secondary {
+  background: rgba(102, 126, 234, 0.05);
+  border-color: rgba(102, 126, 234, 0.2);
+}
+
+.apikey-action-btn:hover {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35);
+}
+
+.apikey-action-btn.secondary:hover {
+  background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
+}
+
+/* Logout Section */
+.logout-section {
+  display: flex;
+  flex-direction: column;
+  gap: 28px;
+  padding: 20px 0;
+}
+
+.logout-warning {
+  padding: 24px;
+  background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(239, 68, 68, 0.03) 100%);
+  border: 1px solid rgba(239, 68, 68, 0.2);
+  border-radius: 16px;
+  text-align: center;
+  font-size: 16px;
+  font-weight: 600;
+  color: #dc2626;
+}
+
+.logout-actions {
+  display: flex;
+  gap: 12px;
+}
+
+.logout-confirm-btn,
+.logout-cancel-btn {
+  flex: 1;
+  padding: 14px;
+  border-radius: 12px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.logout-confirm-btn {
+  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+  border: none;
+  color: white;
+  box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
+}
+
+.logout-confirm-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
+}
+
+.logout-cancel-btn {
+  background: rgba(102, 126, 234, 0.08);
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  color: #667eea;
+}
+
+.logout-cancel-btn:hover {
+  background: rgba(102, 126, 234, 0.15);
+  transform: translateY(-2px);
+}
+
+/* API Key Modal */
+.api-key-modal {
+  width: 95%;
+  max-width: 1100px;
+  height: 80vh;
+  max-height: 800px;
+}
+
+/* ============ 智能体执行页 / 申请页 公共样式 ============ */
+.back-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 6px 14px;
+  background: rgba(102, 126, 234, 0.08);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 8px;
+  color: #667eea;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  margin-bottom: 12px;
+  transition: all 0.2s ease;
+}
+
+.back-btn:hover {
+  background: rgba(102, 126, 234, 0.15);
+  transform: translateX(-2px);
+}
+
+/* 智能体执行页 */
+.agent-execute {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  height: calc(100vh - 80px);
+}
+
+/* 执行页 - 详情卡 */
+.execute-detail-card {
+  background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 16px;
+  padding: 20px 24px;
+  box-shadow: 0 4px 20px rgba(102, 126, 234, 0.08);
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+  flex-shrink: 0;
+}
+
+.execute-detail-head {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.execute-detail-icon {
+  width: 64px;
+  height: 64px;
+  border-radius: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32px;
+  flex-shrink: 0;
+  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'Twemoji Mozilla', sans-serif;
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
+  overflow: hidden;
+}
+
+.execute-detail-icon-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  border-radius: 16px;
+}
+
+.execute-detail-card > .execute-detail-desc,
+.execute-detail-card > .execute-detail-info,
+.execute-detail-card > .execute-detail-disclaimer {
+  display: none;
+}
+
+.execute-msg-actions {
+  display: flex;
+  justify-content: flex-start;
+  gap: 8px;
+  margin-top: 8px;
+  margin-left: 72px;
+}
+
+.execute-msg-action-btn {
+  border: 1px solid rgba(102, 126, 234, 0.24);
+  background: #fff;
+  color: #4f46e5;
+  border-radius: 8px;
+  padding: 6px 12px;
+  font-size: 12px;
+  font-weight: 700;
+  cursor: pointer;
+}
+
+.execute-msg-action-btn:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+.execute-drawer-mask {
+  position: fixed;
+  inset: 0;
+  background: rgba(15, 23, 42, 0.28);
+  z-index: 1200;
+}
+
+.execute-drawer {
+  position: fixed;
+  top: 0;
+  right: 0;
+  width: min(420px, 90vw);
+  height: 100vh;
+  background: #fff;
+  z-index: 1201;
+  box-shadow: -16px 0 40px rgba(15, 23, 42, 0.18);
+  display: flex;
+  flex-direction: column;
+}
+
+.execute-drawer-head {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 18px 20px;
+  border-bottom: 1px solid #e5e7eb;
+}
+
+.execute-drawer-close {
+  width: 28px;
+  height: 28px;
+  border: 0;
+  border-radius: 8px;
+  background: #f1f5f9;
+  cursor: pointer;
+}
+
+.execute-drawer-title {
+  font-size: 16px;
+  font-weight: 800;
+  color: #1e293b;
+}
+
+.execute-drawer-body {
+  padding: 12px;
+  overflow-y: auto;
+}
+
+.execute-drawer-status {
+  padding: 24px;
+  color: #64748b;
+  text-align: center;
+}
+
+.execute-drawer-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.execute-drawer-item {
+  padding: 12px;
+  border: 1px solid #e5e7eb;
+  border-radius: 10px;
+  cursor: pointer;
+}
+
+.execute-drawer-item:hover {
+  border-color: rgba(102, 126, 234, 0.4);
+  background: #f8faff;
+}
+
+.execute-drawer-item-name {
+  font-weight: 700;
+  color: #1e293b;
+  margin-bottom: 4px;
+}
+
+.execute-drawer-item-meta {
+  font-size: 12px;
+  color: #64748b;
+}
+
+.execute-detail-disclaimer {
+  margin-top: 12px;
+  padding: 8px 12px;
+  background: rgba(245, 158, 11, 0.06);
+  border: 1px solid rgba(245, 158, 11, 0.2);
+  border-left: 3px solid #f59e0b;
+  border-radius: 6px;
+  font-size: 12px;
+  color: #92400e;
+  line-height: 1.6;
+}
+
+.execute-detail-meta {
+  flex: 1;
+  min-width: 0;
+}
+
+.execute-detail-name {
+  font-size: 22px;
+  font-weight: 800;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  margin: 0 0 6px 0;
+  line-height: 1.2;
+}
+
+.execute-detail-sub {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
+  font-size: 12px;
+  color: #64748b;
+}
+
+.execute-detail-id {
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+  color: #94a3b8;
+}
+
+.execute-detail-tag {
+  padding: 2px 8px;
+  background: rgba(102, 126, 234, 0.1);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 6px;
+  color: #667eea;
+  font-weight: 600;
+}
+
+.execute-detail-status {
+  padding: 2px 8px;
+  border-radius: 6px;
+  font-weight: 600;
+  text-transform: capitalize;
+}
+
+.execute-status-active {
+  background: rgba(34, 197, 94, 0.12);
+  color: #16a34a;
+}
+
+.execute-status-inactive,
+.execute-status-disabled {
+  background: rgba(148, 163, 184, 0.15);
+  color: #64748b;
+}
+
+.execute-detail-error {
+  margin-top: 6px;
+  padding: 6px 10px;
+  background: rgba(239, 68, 68, 0.08);
+  border: 1px solid rgba(239, 68, 68, 0.2);
+  border-radius: 6px;
+  color: #dc2626;
+  font-size: 12px;
+}
+
+.execute-detail-apikey {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 4px;
+  padding: 8px 14px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.04) 100%);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 10px;
+  max-width: 220px;
+}
+
+.apikey-label {
+  font-size: 11px;
+  font-weight: 600;
+  color: #667eea;
+  letter-spacing: 0.5px;
+}
+
+.execute-detail-apikey code {
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+  font-size: 13px;
+  color: #1e293b;
+  font-weight: 600;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 100%;
+}
+
+/* ===== API Key 触发按钮(右上角) ===== */
+.execute-detail-apikey-trigger,
+.execute-detail-history-trigger,
+.execute-detail-new-trigger {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 16px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border: none;
+  border-radius: 10px;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
+  letter-spacing: 0.3px;
+}
+
+.execute-detail-new-trigger {
+  margin-left: auto;
+  background: #fff;
+  color: #5568d3;
+  border: 1px solid rgba(102, 126, 234, 0.28);
+  box-shadow: none;
+}
+
+.execute-detail-apikey-trigger:hover,
+.execute-detail-history-trigger:hover,
+.execute-detail-new-trigger:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 14px rgba(102, 126, 234, 0.4);
+  background: linear-gradient(135deg, #5a6fd8 0%, #6a4391 100%);
+}
+
+.execute-detail-new-trigger:hover {
+  color: #5568d3;
+  border-color: rgba(102, 126, 234, 0.45);
+  background: rgba(102, 126, 234, 0.08);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.12);
+}
+
+.execute-detail-apikey-trigger:active,
+.execute-detail-history-trigger:active,
+.execute-detail-new-trigger:active {
+  transform: translateY(0);
+  box-shadow: 0 1px 4px rgba(102, 126, 234, 0.3);
+}
+
+.execute-detail-apikey-trigger .apikey-icon,
+.execute-detail-history-trigger .history-icon,
+.execute-detail-history-trigger .new-chat-icon,
+.execute-detail-new-trigger svg {
+  flex-shrink: 0;
+}
+
+/* ===== API Key 弹框 ===== */
+.apikey-modal-mask {
+  position: fixed;
+  inset: 0;
+  background: rgba(15, 23, 42, 0.5);
+  backdrop-filter: blur(4px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  animation: apikey-modal-fade 0.2s ease;
+}
+
+@keyframes apikey-modal-fade {
+  from { opacity: 0; }
+  to   { opacity: 1; }
+}
+
+.apikey-modal {
+  width: min(520px, 92vw);
+  background: #fff;
+  border-radius: 14px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
+  overflow: hidden;
+  animation: apikey-modal-pop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+@keyframes apikey-modal-pop {
+  from { opacity: 0; transform: scale(0.92) translateY(10px); }
+  to   { opacity: 1; transform: scale(1) translateY(0); }
+}
+
+.apikey-modal-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  border-bottom: 1px solid #e2e8f0;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
+}
+
+.apikey-modal-head h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 700;
+  color: #1e293b;
+}
+
+.apikey-modal-close {
+  width: 28px;
+  height: 28px;
+  border: none;
+  background: transparent;
+  color: #64748b;
+  font-size: 22px;
+  line-height: 1;
+  cursor: pointer;
+  border-radius: 6px;
+  transition: all 0.15s;
+}
+
+.apikey-modal-close:hover {
+  background: rgba(15, 23, 42, 0.08);
+  color: #1e293b;
+}
+
+.apikey-modal-body {
+  padding: 20px;
+}
+
+.apikey-modal-value {
+  display: block;
+  width: 100%;
+  padding: 14px 16px;
+  background: #f1f5f9;
+  border: 1px solid #e2e8f0;
+  border-radius: 10px;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+  font-size: 13px;
+  color: #1e293b;
+  word-break: break-all;
+  line-height: 1.6;
+  box-sizing: border-box;
+  user-select: all;
+}
+
+.apikey-modal-tip {
+  margin: 12px 0 0;
+  font-size: 12px;
+  color: #94a3b8;
+  line-height: 1.5;
+}
+
+.apikey-modal-foot {
+  padding: 14px 20px;
+  border-top: 1px solid #e2e8f0;
+  display: flex;
+  justify-content: flex-end;
+  background: #fafbfc;
+}
+
+.apikey-modal-copy {
+  padding: 8px 22px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+  min-width: 90px;
+}
+
+.apikey-modal-copy:hover {
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35);
+  transform: translateY(-1px);
+}
+
+.apikey-modal-copy.is-copied {
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+}
+
+/* ===== 聊天模式:参数配置按钮(输入框右上角) ===== */
+.execute-params-btn {
+  position: relative;
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 0 14px;
+  height: 38px;
+  background: rgba(102, 126, 234, 0.08);
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 10px;
+  color: #667eea;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+
+.execute-params-btn:hover:not(:disabled) {
+  background: rgba(102, 126, 234, 0.16);
+  border-color: rgba(102, 126, 234, 0.45);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 10px rgba(102, 126, 234, 0.18);
+}
+
+.execute-params-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.execute-params-btn svg {
+  flex-shrink: 0;
+}
+
+.execute-params-dot {
+  position: absolute;
+  top: -6px;
+  right: -6px;
+  min-width: 18px;
+  height: 18px;
+  padding: 0 5px;
+  background: linear-gradient(135deg, #ef4444 0%, #f97316 100%);
+  color: #fff;
+  font-size: 11px;
+  font-weight: 700;
+  line-height: 18px;
+  text-align: center;
+  border-radius: 9px;
+  box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4);
+  box-sizing: border-box;
+}
+
+/* ===== 聊天模式:参数弹窗(参考 workflow 表单样式) ===== */
+.chat-params-modal {
+  width: min(560px, 92vw);
+  max-height: 86vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.chat-params-modal-body {
+  padding: 20px 24px;
+  overflow-y: auto;
+  flex: 1;
+}
+
+.chat-params-modal-body .wf-form {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.chat-params-modal-body .wf-form-field {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.chat-params-modal-body .wf-form-label {
+  font-size: 13px;
+  font-weight: 600;
+  color: #334155;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.chat-params-modal-body .wf-form-input,
+.chat-params-modal-body .wf-form-textarea,
+.chat-params-modal-body .wf-form-select {
+  width: 100%;
+  padding: 10px 12px;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 8px;
+  background: rgba(255, 255, 255, 0.8);
+  font-size: 14px;
+  color: #1e293b;
+  font-family: inherit;
+  transition: all 0.2s;
+  box-sizing: border-box;
+}
+
+.chat-params-modal-body .wf-form-input:focus,
+.chat-params-modal-body .wf-form-textarea:focus,
+.chat-params-modal-body .wf-form-select:focus {
+  outline: none;
+  border-color: #667eea;
+  background: #ffffff;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+}
+
+.chat-params-modal-body .wf-form-textarea {
+  resize: vertical;
+  min-height: 80px;
+  font-family: inherit;
+}
+
+.chat-params-modal-body .wf-form-checkbox-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 8px 12px;
+  border: 1px solid rgba(102, 126, 234, 0.15);
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.chat-params-modal-body .wf-form-file-trigger {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 14px;
+  border: 1.5px dashed rgba(102, 126, 234, 0.35);
+  border-radius: 8px;
+  background: rgba(102, 126, 234, 0.04);
+  color: #667eea;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.chat-params-modal-body .wf-form-file-trigger:hover {
+  background: rgba(102, 126, 234, 0.1);
+  border-color: rgba(102, 126, 234, 0.55);
+}
+
+.chat-params-modal-body .wf-form-file-list {
+  list-style: none;
+  margin: 8px 0 0;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.chat-params-modal-body .wf-form-file-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 10px;
+  background: rgba(102, 126, 234, 0.06);
+  border: 1px solid rgba(102, 126, 234, 0.15);
+  border-radius: 6px;
+  font-size: 12px;
+}
+
+.chat-params-modal-body .wf-form-file-remove {
+  margin-left: auto;
+  width: 22px;
+  height: 22px;
+  border: none;
+  background: transparent;
+  color: #94a3b8;
+  font-size: 18px;
+  line-height: 1;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.chat-params-modal-body .wf-form-file-remove:hover {
+  background: rgba(239, 68, 68, 0.1);
+  color: #ef4444;
+}
+
+.chat-params-modal-body .wf-form-file-hidden {
+  display: none;
+}
+
+/* 弹窗内滚动条 */
+.chat-params-modal-body::-webkit-scrollbar {
+  width: 8px;
+}
+.chat-params-modal-body::-webkit-scrollbar-thumb {
+  background: #c9cdd4;
+  border: 2px solid #f7f8fa;
+  border-radius: 999px;
+}
+.chat-params-modal-body::-webkit-scrollbar-thumb:hover {
+  background: #a9aeb8;
+}
+
+/* ===== 聊天模式:停止按钮 ===== */
+.execute-stop-btn {
+  background: linear-gradient(135deg, #ef4444 0%, #f97316 100%) !important;
+}
+
+.execute-stop-btn:hover {
+  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4) !important;
+}
+
+/* ===== 聊天模式:错误提示 ===== */
+.execute-chat-error {
+  margin: 0 16px 0;
+  padding: 8px 12px;
+  border-radius: 8px;
+  font-size: 12px;
+}
+
+/* ===== 流式输出光标 ===== */
+.wf-step-cursor {
+  display: inline-block;
+  margin-left: 2px;
+  color: #165dff;
+  font-weight: 700;
+  animation: wf-cursor-blink 1s steps(2, end) infinite;
+}
+
+@keyframes wf-cursor-blink {
+  0%, 50%   { opacity: 1; }
+  50.01%, 100% { opacity: 0; }
+}
+
+/* assistant 消息流式气泡 */
+.execute-msg-assistant .execute-msg-bubble {
+  min-height: 20px;
+}
+
+/* ===== 模式徽章 ===== */
+.execute-detail-mode-tag {
+  display: inline-block;
+  padding: 3px 12px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  font-size: 12px;
+  font-weight: 600;
+  border-radius: 12px;
+  letter-spacing: 0.5px;
+  box-shadow: 0 2px 6px rgba(102, 126, 234, 0.2);
+}
+
+/* ===== Workflow 左右分栏布局 ===== */
+.workflow-layout {
+  display: flex;
+  align-items: flex-start;
+  gap: 16px;
+  position: relative;
+}
+
+.workflow-main {
+  flex: 1 1 auto;
+  min-width: 0; /* 允许内容收缩 */
+  transition: flex 0.3s ease;
+}
+
+.workflow-side {
+  flex: 0 0 400px;
+  position: sticky;
+  top: 16px;
+  align-self: flex-start;
+  transition: flex-basis 0.3s ease, width 0.3s ease;
+}
+
+.workflow-side.is-collapsed {
+  flex: 0 0 56px;
+  width: 56px;
+  overflow: visible;
+}
+
+/* ===== 折叠态:竖向按钮条 ===== */
+.workflow-side-collapsed {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 14px;
+  padding: 14px 8px;
+  background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
+  border: 1px solid #e2e8f0;
+  border-radius: 14px;
+  box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
+  position: sticky;
+  top: 16px;
+  animation: wf-collapsed-in 0.25s ease;
+}
+
+@keyframes wf-collapsed-in {
+  from { opacity: 0; transform: translateX(8px); }
+  to   { opacity: 1; transform: translateX(0); }
+}
+
+.workflow-side-expand {
+  width: 32px;
+  height: 32px;
+  border-radius: 8px;
+  background: rgba(102, 126, 234, 0.1);
+  color: #667eea;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.2s ease;
+  padding: 0;
+}
+
+.workflow-side-expand:hover {
+  background: #667eea;
+  color: #fff;
+  transform: translateX(-1px);
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+.workflow-side-collapsed-title {
+  writing-mode: vertical-rl;
+  text-orientation: upright;
+  font-size: 12px;
+  font-weight: 700;
+  color: #64748b;
+  letter-spacing: 4px;
+  padding: 8px 0;
+}
+
+.workflow-side-collapsed-run {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border: none;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  padding: 0;
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35);
+  transition: all 0.2s ease;
+}
+
+.workflow-side-collapsed-run:hover:not(:disabled) {
+  transform: scale(1.08);
+  box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
+}
+
+.workflow-side-collapsed-run:disabled {
+  background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%);
+  cursor: not-allowed;
+  box-shadow: none;
+}
+
+/* ===== 展开态卡片头部(含 X 按钮) ===== */
+.workflow-side-card-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 18px;
+  border-bottom: 1px solid #eef2f7;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.04) 0%, rgba(118, 75, 162, 0.04) 100%);
+  border-radius: 14px 14px 0 0;
+}
+
+.workflow-side-card-head .execute-card-title {
+  margin: 0;
+  font-size: 15px;
+  font-weight: 700;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  -webkit-background-clip: text;
+  background-clip: text;
+  color: transparent;
+}
+
+.workflow-side-close {
+  width: 28px;
+  height: 28px;
+  border-radius: 8px;
+  background: transparent;
+  color: #64748b;
+  border: 1px solid transparent;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+  transition: all 0.15s ease;
+}
+
+.workflow-side-close:hover {
+  background: rgba(239, 68, 68, 0.1);
+  color: #ef4444;
+  border-color: rgba(239, 68, 68, 0.2);
+}
+
+.workflow-side-card {
+  margin: 0;
+  border-radius: 14px;
+  overflow: hidden;
+  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
+  border: 1px solid #e2e8f0;
+  background: #fff;
+}
+
+.workflow-side .wf-form {
+  max-height: calc(100vh - 320px);
+  overflow-y: auto;
+  padding: 18px;
+}
+
+.workflow-side .wf-form::-webkit-scrollbar {
+  width: 8px;
+}
+.workflow-side .wf-form::-webkit-scrollbar-thumb {
+  background: #c9cdd4;
+  border: 2px solid #f7f8fa;
+  border-radius: 999px;
+}
+.workflow-side .wf-form::-webkit-scrollbar-thumb:hover {
+  background: #a9aeb8;
+}
+
+/* 响应式:窄屏时上下堆叠 */
+@media (max-width: 1024px) {
+  .workflow-layout {
+    flex-direction: column;
+  }
+  .workflow-side {
+    flex: 1 1 auto;
+    width: 100%;
+    position: static;
+  }
+  .workflow-side.is-collapsed {
+    flex: 0 0 56px;
+    width: 56px;
+  }
+}
+
+/* ===== 表单字段:精致样式 ===== */
+.wf-form-field {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+  margin-bottom: 10px;
+}
+
+.wf-form-label {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #475569;
+  letter-spacing: 0.3px;
+}
+
+.wf-form-required {
+  color: #ef4444;
+  font-style: normal;
+  font-weight: 700;
+  margin-left: 2px;
+}
+
+/* ===== 字段右侧的徽章组:类型 + 必填 ===== */
+.wf-form-meta {
+  margin-left: auto;
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  flex-shrink: 0;
+}
+
+.wf-form-type-badge {
+  display: inline-block;
+  padding: 1px 7px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.1) 100%);
+  color: #667eea;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 4px;
+  font-size: 10px;
+  font-weight: 700;
+  letter-spacing: 0.5px;
+  line-height: 1.4;
+  white-space: nowrap;
+}
+
+.wf-form-required-badge {
+  display: inline-block;
+  padding: 1px 7px;
+  background: linear-gradient(135deg, rgba(239, 68, 68, 0.12) 0%, rgba(220, 38, 38, 0.1) 100%);
+  color: #ef4444;
+  border: 1px solid rgba(239, 68, 68, 0.25);
+  border-radius: 4px;
+  font-size: 10px;
+  font-weight: 700;
+  letter-spacing: 0.5px;
+  line-height: 1.4;
+  white-space: nowrap;
+}
+
+.wf-form-tip {
+  color: #94a3b8;
+  font-weight: 400;
+  font-size: 11px;
+  margin-left: 4px;
+}
+
+.wf-form-input,
+.wf-form-textarea {
+  width: 100%;
+  padding: 9px 12px;
+  background: #f8fafc;
+  border: 1.5px solid #e2e8f0;
+  border-radius: 8px;
+  font-size: 13px;
+  color: #1e293b;
+  font-family: inherit;
+  box-sizing: border-box;
+  transition: all 0.18s ease;
+  outline: none;
+}
+
+.wf-form-input:hover,
+.wf-form-textarea:hover {
+  border-color: #cbd5e1;
+  background: #fff;
+}
+
+.wf-form-input:focus,
+.wf-form-textarea:focus {
+  border-color: #667eea;
+  background: #fff;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+}
+
+.wf-form-input::placeholder,
+.wf-form-textarea::placeholder {
+  color: #cbd5e1;
+}
+
+.wf-form-textarea {
+  resize: vertical;
+  min-height: 100px;
+  line-height: 1.6;
+}
+
+/* ===== 数字输入框:隐藏原生 spinner,保留直接输入 ===== */
+.wf-form-input[type="number"] {
+  -moz-appearance: textfield;
+  appearance: textfield;
+}
+
+.wf-form-input[type="number"]::-webkit-outer-spin-button,
+.wf-form-input[type="number"]::-webkit-inner-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
+
+.wf-form-textarea-counter {
+  align-self: flex-end;
+  font-size: 11px;
+  color: #94a3b8;
+  margin-top: -2px;
+}
+
+/* ===== Select 下拉(自定义箭头) ===== */
+.wf-form-select-wrapper {
+  position: relative;
+  display: flex;
+  align-items: center;
+}
+
+.wf-form-select {
+  width: 100%;
+  padding: 9px 36px 9px 12px;
+  background: #f8fafc;
+  border: 1.5px solid #e2e8f0;
+  border-radius: 8px;
+  font-size: 13px;
+  color: #1e293b;
+  cursor: pointer;
+  appearance: none;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  outline: none;
+  transition: all 0.18s ease;
+  font-family: inherit;
+}
+
+.wf-form-select:hover {
+  border-color: #cbd5e1;
+  background: #fff;
+}
+
+.wf-form-select:focus {
+  border-color: #667eea;
+  background: #fff;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+}
+
+.wf-form-select-arrow {
+  position: absolute;
+  right: 12px;
+  top: 50%;
+  transform: translateY(-50%);
+  color: #94a3b8;
+  pointer-events: none;
+  transition: transform 0.2s;
+}
+
+.wf-form-select-wrapper:hover .wf-form-select-arrow {
+  color: #667eea;
+}
+
+/* ===== 文件上传 ===== */
+.wf-form-file-hidden {
+  display: none;
+}
+
+.wf-form-file-trigger {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  width: 100%;
+  padding: 14px;
+  background: #f8fafc;
+  border: 1.5px dashed #cbd5e1;
+  border-radius: 10px;
+  color: #64748b;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  font-family: inherit;
+}
+
+.wf-form-file-trigger:hover {
+  background: rgba(102, 126, 234, 0.04);
+  border-color: #667eea;
+  border-style: solid;
+  color: #667eea;
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.08);
+}
+
+.wf-form-file-trigger svg {
+  flex-shrink: 0;
+}
+
+.wf-form-file-list {
+  list-style: none;
+  margin: 8px 0 0;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.wf-form-file-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 8px 12px;
+  background: #f1f5f9;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  font-size: 12px;
+}
+
+.wf-form-file-icon {
+  font-size: 16px;
+  flex-shrink: 0;
+}
+
+.wf-form-file-name {
+  flex: 1;
+  color: #1e293b;
+  font-weight: 500;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.wf-form-file-size {
+  color: #94a3b8;
+  font-size: 11px;
+  flex-shrink: 0;
+}
+
+.wf-form-file-remove {
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  background: transparent;
+  border: none;
+  color: #94a3b8;
+  font-size: 16px;
+  line-height: 1;
+  cursor: pointer;
+  flex-shrink: 0;
+  transition: all 0.15s ease;
+  padding: 0;
+}
+
+.wf-form-file-remove:hover {
+  background: rgba(239, 68, 68, 0.1);
+  color: #ef4444;
+}
+
+/* ===== Checkbox 行(与输入框对齐) ===== */
+.wf-form-checkbox-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 9px 12px;
+  background: #f8fafc;
+  border: 1.5px solid #e2e8f0;
+  border-radius: 8px;
+  font-size: 13px;
+  color: #1e293b;
+  cursor: pointer;
+  margin-bottom: 10px;
+  transition: all 0.18s ease;
+  min-height: 38px;
+  box-sizing: border-box;
+}
+
+.wf-form-checkbox-row:hover {
+  border-color: #cbd5e1;
+  background: #fff;
+}
+
+.wf-form-checkbox-row:has(.wf-form-checkbox:checked) {
+  border-color: #667eea;
+  background: rgba(102, 126, 234, 0.04);
+}
+
+.wf-form-checkbox {
+  width: 16px;
+  height: 16px;
+  accent-color: #667eea;
+  cursor: pointer;
+  margin: 0;
+  flex-shrink: 0;
+}
+
+.wf-form-checkbox-label {
+  font-size: 13px;
+  color: #1e293b;
+  font-weight: 500;
+  line-height: 1;
+  user-select: none;
+}
+
+/* ===== 空状态/加载/错误 ===== */
+.wf-form-empty {
+  padding: 24px 16px;
+  text-align: center;
+  color: #94a3b8;
+  font-size: 13px;
+  background: #f8fafc;
+  border-radius: 8px;
+  border: 1px dashed #e2e8f0;
+}
+
+.wf-form-loading {
+  padding: 20px;
+  text-align: center;
+  color: #64748b;
+  font-size: 13px;
+}
+
+.execute-detail-desc {
+  font-size: 13px;
+  color: #475569;
+  line-height: 1.7;
+  padding: 12px 16px;
+  background: rgba(102, 126, 234, 0.04);
+  border-left: 3px solid #667eea;
+  border-radius: 6px;
+}
+
+.execute-detail-info {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+
+.execute-detail-tags {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+}
+
+.execute-detail-chip {
+  padding: 3px 10px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(0, 212, 255, 0.08) 100%);
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #667eea;
+}
+
+.execute-detail-stats {
+  display: flex;
+  gap: 14px;
+  font-size: 12px;
+  color: #64748b;
+  font-weight: 500;
+}
+
+.execute-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 4px 20px rgba(102, 126, 234, 0.08);
+}
+
+.execute-messages {
+  flex: 1;
+  padding: 24px;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  scrollbar-width: thin;
+  scrollbar-color: rgba(22, 93, 255, 0.38) rgba(22, 93, 255, 0.08);
+}
+
+.execute-messages::-webkit-scrollbar {
+  width: 8px;
+}
+
+.execute-messages::-webkit-scrollbar-track {
+  background: rgba(22, 93, 255, 0.06);
+  border-radius: 999px;
+}
+
+.execute-messages::-webkit-scrollbar-thumb {
+  background: linear-gradient(180deg, rgba(22, 93, 255, 0.45) 0%, rgba(114, 46, 209, 0.38) 100%);
+  border: 2px solid rgba(248, 250, 255, 0.95);
+  border-radius: 999px;
+}
+
+.execute-messages::-webkit-scrollbar-thumb:hover {
+  background: linear-gradient(180deg, rgba(22, 93, 255, 0.68) 0%, rgba(114, 46, 209, 0.58) 100%);
+}
+
+.execute-empty {
+  margin: auto;
+  color: #999;
+  font-size: 14px;
+}
+
+.execute-msg {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  max-width: 80%;
+}
+
+.execute-msg-user {
+  align-self: flex-end;
+  align-items: flex-end;
+}
+
+.execute-msg-assistant {
+  align-self: flex-start;
+  align-items: flex-start;
+}
+
+.execute-msg-role {
+  font-size: 12px;
+  color: #888;
+  font-weight: 500;
+}
+
+.execute-msg-bubble {
+  padding: 10px 14px;
+  border-radius: 12px;
+  font-size: 14px;
+  line-height: 1.6;
+  word-break: break-word;
+}
+
+.execute-msg-generating {
+  display: inline-block;
+  color: #64748b;
+}
+
+.markdown-body {
+  color: inherit;
+  line-height: 1.7;
+  white-space: normal;
+}
+
+.markdown-body > :first-child {
+  margin-top: 0;
+}
+
+.markdown-body > :last-child {
+  margin-bottom: 0;
+}
+
+.markdown-body p {
+  margin: 0 0 10px;
+}
+
+.markdown-body h1,
+.markdown-body h2,
+.markdown-body h3,
+.markdown-body h4,
+.markdown-body h5,
+.markdown-body h6 {
+  margin: 16px 0 8px;
+  color: inherit;
+  font-weight: 700;
+  line-height: 1.35;
+}
+
+.markdown-body h1 { font-size: 22px; }
+.markdown-body h2 { font-size: 19px; }
+.markdown-body h3 { font-size: 17px; }
+.markdown-body h4,
+.markdown-body h5,
+.markdown-body h6 { font-size: 15px; }
+
+.markdown-body ul,
+.markdown-body ol {
+  margin: 8px 0 12px;
+  padding-left: 22px;
+}
+
+.markdown-body li {
+  margin: 4px 0;
+}
+
+.markdown-body li > p {
+  margin: 4px 0;
+}
+
+.markdown-body blockquote {
+  margin: 10px 0;
+  padding: 8px 12px;
+  border-left: 3px solid #667eea;
+  background: rgba(102, 126, 234, 0.08);
+  color: #475569;
+}
+
+.markdown-body pre {
+  margin: 10px 0;
+  padding: 12px;
+  overflow-x: auto;
+  border: 1px solid rgba(102, 126, 234, 0.14);
+  border-radius: 8px;
+  background: #f8fafc;
+  white-space: pre;
+}
+
+.markdown-body code {
+  padding: 2px 5px;
+  border-radius: 5px;
+  background: rgba(15, 23, 42, 0.07);
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+  font-size: 0.92em;
+}
+
+.markdown-body pre code {
+  padding: 0;
+  background: transparent;
+  border-radius: 0;
+}
+
+.markdown-body table {
+  display: block;
+  width: 100%;
+  margin: 10px 0;
+  overflow-x: auto;
+  border-collapse: collapse;
+}
+
+.markdown-body th,
+.markdown-body td {
+  padding: 7px 10px;
+  border: 1px solid #dbe3ef;
+  text-align: left;
+  vertical-align: top;
+}
+
+.markdown-body th {
+  background: #f1f5f9;
+  font-weight: 700;
+}
+
+.markdown-body hr {
+  margin: 14px 0;
+  border: 0;
+  border-top: 1px solid #dbe3ef;
+}
+
+.markdown-body a {
+  color: #5568d3;
+  text-decoration: none;
+}
+
+.markdown-body a:hover {
+  text-decoration: underline;
+}
+
+.execute-msg-user .markdown-body a {
+  color: #fff;
+  text-decoration: underline;
+}
+
+.execute-msg-user .execute-msg-bubble {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.execute-msg-assistant .execute-msg-bubble {
+  background: rgba(102, 126, 234, 0.08);
+  color: #1e293b;
+  border: 1px solid rgba(102, 126, 234, 0.15);
+}
+
+.execute-input-bar {
+  display: flex;
+  gap: 10px;
+  padding: 16px;
+  border-top: 1px solid rgba(102, 126, 234, 0.12);
+  background: rgba(255, 255, 255, 0.6);
+}
+
+.execute-input {
+  flex: 1;
+  padding: 10px 14px;
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 10px;
+  font-size: 14px;
+  outline: none;
+  transition: all 0.2s ease;
+}
+
+.execute-input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+}
+
+.execute-send-btn {
+  padding: 10px 22px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border: none;
+  border-radius: 10px;
+  color: white;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.execute-send-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.execute-send-btn:not(:disabled):hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+/* 智能体申请页 */
+.agent-apply {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  max-width: 760px;
+  margin: 0 auto;
+}
+
+.apply-error-tip {
+  padding: 10px 16px;
+  background: rgba(239, 68, 68, 0.08);
+  border: 1px solid rgba(239, 68, 68, 0.25);
+  border-radius: 10px;
+  color: #dc2626;
+  font-size: 13px;
+}
+
+.apply-card {
+  background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 16px;
+  padding: 28px;
+  box-shadow: 0 4px 20px rgba(102, 126, 234, 0.08);
+}
+
+.apply-agent-info {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 16px;
+  background: rgba(102, 126, 234, 0.05);
+  border: 1px solid rgba(102, 126, 234, 0.15);
+  border-radius: 12px;
+  margin-bottom: 24px;
+}
+
+.apply-agent-icon {
+  width: 52px;
+  height: 52px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 26px;
+  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'Twemoji Mozilla', sans-serif;
+  flex-shrink: 0;
+}
+
+.apply-agent-name {
+  font-size: 16px;
+  font-weight: 700;
+  color: #1e293b;
+  margin-bottom: 4px;
+}
+
+.apply-agent-desc {
+  font-size: 13px;
+  color: #64748b;
+  line-height: 1.5;
+}
+
+.apply-form {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.apply-label {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  font-size: 13px;
+  font-weight: 600;
+  color: #334155;
+}
+
+.apply-label em {
+  color: #dc2626;
+  font-style: normal;
+  margin-left: 2px;
+}
+
+.apply-select,
+.apply-textarea {
+  padding: 10px 14px;
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 10px;
+  font-size: 14px;
+  outline: none;
+  background: white;
+  color: #1e293b;
+  font-family: inherit;
+  transition: all 0.2s ease;
+  resize: vertical;
+}
+
+.apply-select:focus,
+.apply-textarea:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+}
+
+.apply-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  margin-top: 8px;
+}
+
+.apply-secondary-btn,
+.apply-primary-btn {
+  padding: 10px 24px;
+  border-radius: 10px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  border: none;
+}
+
+.apply-secondary-btn {
+  background: rgba(102, 126, 234, 0.08);
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  color: #667eea;
+}
+
+.apply-secondary-btn:hover {
+  background: rgba(102, 126, 234, 0.15);
+}
+
+.apply-primary-btn {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.apply-primary-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+  box-shadow: none;
+}
+
+.apply-primary-btn:not(:disabled):hover {
+  transform: translateY(-1px);
+  box-shadow: 0 6px 18px rgba(102, 126, 234, 0.45);
+}
+
+.apply-success {
+  text-align: center;
+  padding: 48px 28px;
+}
+
+.apply-success-icon {
+  font-size: 56px;
+  margin-bottom: 16px;
+}
+
+.apply-success h2 {
+  font-size: 22px;
+  font-weight: 700;
+  color: #1e293b;
+  margin: 0 0 12px 0;
+}
+
+.apply-success p {
+  color: #64748b;
+  font-size: 14px;
+  line-height: 1.6;
+  margin: 0 0 24px 0;
+}
+
+/* ============ 申请页:详情区 / 状态区 ============ */
+.apply-detail-card {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.apply-agent-meta {
+  flex: 1;
+  min-width: 0;
+}
+
+.apply-agent-name {
+  font-size: 18px;
+  font-weight: 800;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  margin-bottom: 6px;
+  line-height: 1.2;
+}
+
+.apply-agent-sub {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex-wrap: wrap;
+  margin-bottom: 6px;
+}
+
+.apply-tag {
+  display: inline-block;
+  padding: 2px 8px;
+  font-size: 11px;
+  font-weight: 600;
+  border-radius: 6px;
+  background: rgba(102, 126, 234, 0.1);
+  border: 1px solid rgba(102, 126, 234, 0.18);
+  color: #667eea;
+}
+
+.apply-tag-version {
+  background: rgba(245, 158, 11, 0.1);
+  border-color: rgba(245, 158, 11, 0.25);
+  color: #b45309;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+}
+
+.apply-tag-listed {
+  background: rgba(34, 197, 94, 0.1);
+  border-color: rgba(34, 197, 94, 0.25);
+  color: #16a34a;
+}
+
+.apply-perm-public {
+  background: rgba(34, 197, 94, 0.1);
+  border-color: rgba(34, 197, 94, 0.25);
+  color: #16a34a;
+}
+
+.apply-perm-partial {
+  background: rgba(245, 158, 11, 0.1);
+  border-color: rgba(245, 158, 11, 0.25);
+  color: #b45309;
+}
+
+.apply-perm-private {
+  background: rgba(239, 68, 68, 0.1);
+  border-color: rgba(239, 68, 68, 0.25);
+  color: #dc2626;
+}
+
+.apply-agent-desc {
+  font-size: 13px;
+  color: #475569;
+  line-height: 1.6;
+}
+
+.apply-version-block,
+.apply-intro-block {
+  padding: 14px 16px;
+  background: rgba(102, 126, 234, 0.04);
+  border-left: 3px solid #667eea;
+  border-radius: 8px;
+}
+
+.apply-version-block {
+  border-left-color: #f59e0b;
+  background: rgba(245, 158, 11, 0.05);
+}
+
+.apply-intro-block {
+  border-left-color: #8b5cf6;
+  background: rgba(139, 92, 246, 0.04);
+}
+
+.apply-version-head {
+  display: flex;
+  justify-content: space-between;
+  align-items: baseline;
+  margin-bottom: 8px;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.apply-version-label,
+.apply-intro-label {
+  font-size: 12px;
+  font-weight: 700;
+  color: #1e293b;
+  letter-spacing: 0.5px;
+}
+
+.apply-version-time {
+  font-size: 11px;
+  color: #94a3b8;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+}
+
+.apply-version-desc,
+.apply-intro-desc {
+  font-size: 13px;
+  color: #475569;
+  line-height: 1.7;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+/* 状态卡(5 种 tone) */
+.apply-status-card {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.apply-tone-success {
+  border-color: rgba(34, 197, 94, 0.3);
+  background: linear-gradient(135deg, rgba(34, 197, 94, 0.05) 0%, #ffffff 100%);
+}
+
+.apply-tone-pending {
+  border-color: rgba(245, 158, 11, 0.3);
+  background: linear-gradient(135deg, rgba(245, 158, 11, 0.05) 0%, #ffffff 100%);
+}
+
+.apply-tone-rejected {
+  border-color: rgba(239, 68, 68, 0.3);
+  background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, #ffffff 100%);
+}
+
+.apply-tone-neutral {
+  border-color: rgba(102, 126, 234, 0.3);
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.04) 0%, #ffffff 100%);
+}
+
+.apply-status-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.apply-status-icon {
+  width: 40px;
+  height: 40px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+  flex-shrink: 0;
+}
+
+.apply-tone-success .apply-status-icon {
+  background: rgba(34, 197, 94, 0.12);
+}
+
+.apply-tone-pending .apply-status-icon {
+  background: rgba(245, 158, 11, 0.12);
+}
+
+.apply-tone-rejected .apply-status-icon {
+  background: rgba(239, 68, 68, 0.12);
+}
+
+.apply-tone-neutral .apply-status-icon {
+  background: rgba(102, 126, 234, 0.12);
+}
+
+.apply-status-meta {
+  flex: 1;
+  min-width: 0;
+}
+
+.apply-status-label {
+  font-size: 15px;
+  font-weight: 700;
+  color: #1e293b;
+  margin-bottom: 2px;
+}
+
+.apply-status-help {
+  font-size: 12px;
+  color: #64748b;
+  line-height: 1.5;
+}
+
+.apply-status-polling {
+  font-size: 11px;
+  color: #b45309;
+  font-weight: 600;
+  padding: 3px 8px;
+  background: rgba(245, 158, 11, 0.12);
+  border-radius: 6px;
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.apply-status-polling::before {
+  content: '';
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #f59e0b;
+  animation: applyPulse 1.5s ease-in-out infinite;
+}
+
+@keyframes applyPulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.3; }
+}
+
+/* 申请详情块 */
+.apply-request-block {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding: 14px 16px;
+  background: rgba(255, 255, 255, 0.6);
+  border: 1px solid rgba(102, 126, 234, 0.12);
+  border-radius: 10px;
+}
+
+.apply-request-row {
+  display: grid;
+  grid-template-columns: 90px 1fr;
+  gap: 12px;
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.apply-request-key {
+  color: #94a3b8;
+  font-weight: 500;
+}
+
+.apply-request-val {
+  color: #1e293b;
+  word-break: break-word;
+}
+
+.apply-mono {
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+  font-size: 12px;
+}
+
+.apply-field-error {
+  margin-top: 6px;
+  padding: 6px 10px;
+  background: rgba(239, 68, 68, 0.08);
+  border: 1px solid rgba(239, 68, 68, 0.2);
+  border-radius: 6px;
+  color: #dc2626;
+  font-size: 12px;
+}
+
+/* ============ 工作流模式:动态表单 + Timeline ============ */
+.execute-card-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+  gap: 12px;
+}
+
+.execute-card-title {
+  margin: 0;
+  font-size: 15px;
+  font-weight: 700;
+  color: #1e293b;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.execute-card-tip {
+  font-size: 11px;
+  color: #94a3b8;
+  padding: 2px 8px;
+  background: rgba(102, 126, 234, 0.06);
+  border-radius: 4px;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+}
+
+.wf-form {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.wf-form-field {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.wf-form-label {
+  font-size: 13px;
+  font-weight: 600;
+  color: #334155;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.wf-form-required {
+  color: #ef4444;
+  font-style: normal;
+  font-weight: 700;
+}
+
+.wf-form-tip {
+  color: #94a3b8;
+  font-size: 11px;
+  font-weight: 400;
+}
+
+.wf-form-input,
+.wf-form-textarea,
+.wf-form-select {
+  width: 100%;
+  padding: 10px 12px;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 8px;
+  background: rgba(255, 255, 255, 0.8);
+  font-size: 14px;
+  color: #1e293b;
+  font-family: inherit;
+  transition: all 0.2s;
+  box-sizing: border-box;
+}
+
+.wf-form-input:focus,
+.wf-form-textarea:focus,
+.wf-form-select:focus {
+  outline: none;
+  border-color: #667eea;
+  background: #ffffff;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+}
+
+.wf-form-textarea {
+  resize: vertical;
+  min-height: 80px;
+  font-family: inherit;
+}
+
+.wf-form-empty,
+.wf-form-loading {
+  padding: 18px;
+  text-align: center;
+  font-size: 13px;
+  color: #94a3b8;
+  background: rgba(102, 126, 234, 0.04);
+  border-radius: 8px;
+  border: 1px dashed rgba(102, 126, 234, 0.2);
+}
+
+.wf-form-actions {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-top: 16px;
+  flex-wrap: wrap;
+}
+
+/* summary 徽章 */
+.wf-summary {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  font-weight: 600;
+  padding: 4px 10px;
+  border-radius: 6px;
+}
+
+.wf-summary-succeeded {
+  background: rgba(34, 197, 94, 0.1);
+  color: #16a34a;
+  border: 1px solid rgba(34, 197, 94, 0.3);
+}
+
+.wf-summary-failed {
+  background: rgba(239, 68, 68, 0.1);
+  color: #dc2626;
+  border: 1px solid rgba(239, 68, 68, 0.3);
+}
+
+.wf-summary-stopped {
+  background: rgba(245, 158, 11, 0.1);
+  color: #b45309;
+  border: 1px solid rgba(245, 158, 11, 0.3);
+}
+
+/* timeline */
+.wf-timeline {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  max-height: 600px;
+  overflow-y: auto;
+  padding-right: 4px;
+  scrollbar-width: thin;
+  scrollbar-color: rgba(22, 93, 255, 0.38) rgba(22, 93, 255, 0.08);
+}
+
+.wf-timeline::-webkit-scrollbar {
+  width: 8px;
+}
+
+.wf-timeline::-webkit-scrollbar-track {
+  background: rgba(22, 93, 255, 0.06);
+  border-radius: 999px;
+}
+
+.wf-timeline::-webkit-scrollbar-thumb {
+  background: linear-gradient(180deg, rgba(22, 93, 255, 0.45) 0%, rgba(114, 46, 209, 0.38) 100%);
+  border: 2px solid rgba(248, 250, 255, 0.95);
+  border-radius: 999px;
+}
+
+.wf-timeline::-webkit-scrollbar-thumb:hover {
+  background: linear-gradient(180deg, rgba(22, 93, 255, 0.68) 0%, rgba(114, 46, 209, 0.58) 100%);
+}
+
+.wf-step {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 10px 12px;
+  background: rgba(255, 255, 255, 0.7);
+  border: 1px solid rgba(102, 126, 234, 0.1);
+  border-radius: 8px;
+  transition: all 0.2s;
+}
+
+.wf-step:hover {
+  background: rgba(255, 255, 255, 0.95);
+  border-color: rgba(102, 126, 234, 0.2);
+}
+
+.wf-step-running {
+  border-left: 3px solid #667eea;
+  animation: wfStepPulse 2s ease-in-out infinite;
+}
+
+@keyframes wfStepPulse {
+  0%, 100% { box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.2); }
+  50% { box-shadow: 0 0 0 6px rgba(102, 126, 234, 0); }
+}
+
+.wf-step-marker {
+  width: 28px;
+  height: 28px;
+  flex-shrink: 0;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: 700;
+  background: rgba(102, 126, 234, 0.1);
+  color: #667eea;
+  border: 2px solid rgba(102, 126, 234, 0.2);
+}
+
+.wf-step-marker-start {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-color: transparent;
+}
+
+.wf-step-marker-running {
+  background: rgba(245, 158, 11, 0.15);
+  color: #f59e0b;
+  border-color: rgba(245, 158, 11, 0.3);
+  animation: wfStepSpin 1.5s linear infinite;
+}
+
+@keyframes wfStepSpin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+.wf-step-marker-succeeded {
+  background: rgba(34, 197, 94, 0.15);
+  color: #16a34a;
+  border-color: rgba(34, 197, 94, 0.3);
+}
+
+.wf-step-marker-failed {
+  background: rgba(239, 68, 68, 0.15);
+  color: #dc2626;
+  border-color: rgba(239, 68, 68, 0.3);
+}
+
+.wf-step-marker-stopped {
+  background: rgba(245, 158, 11, 0.15);
+  color: #b45309;
+  border-color: rgba(245, 158, 11, 0.3);
+}
+
+.wf-step-marker-paused {
+  background: rgba(245, 158, 11, 0.15);
+  color: #b45309;
+  border-color: rgba(245, 158, 11, 0.3);
+}
+
+.wf-step-marker-output {
+  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+  color: #fff;
+  border-color: transparent;
+}
+
+.wf-step-marker-human {
+  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+  color: #fff;
+  border-color: transparent;
+}
+
+.wf-step-marker-error {
+  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+  color: #fff;
+  border-color: transparent;
+}
+
+.wf-step-body {
+  flex: 1;
+  min-width: 0;
+}
+
+.wf-step-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #1e293b;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex-wrap: wrap;
+  line-height: 1.5;
+}
+
+.wf-step-meta {
+  font-size: 11px;
+  color: #94a3b8;
+  margin-top: 2px;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+}
+
+.wf-step-tag {
+  display: inline-block;
+  padding: 1px 6px;
+  font-size: 10px;
+  font-weight: 600;
+  background: rgba(102, 126, 234, 0.08);
+  color: #667eea;
+  border-radius: 4px;
+  margin-left: 4px;
+}
+
+.wf-step-type {
+  display: inline-block;
+  padding: 1px 6px;
+  font-size: 10px;
+  font-weight: 500;
+  background: rgba(139, 92, 246, 0.08);
+  color: #8b5cf6;
+  border-radius: 4px;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+}
+
+.wf-step-status {
+  padding: 1px 6px;
+  font-size: 10px;
+  font-weight: 700;
+  border-radius: 4px;
+  text-transform: uppercase;
+}
+
+.wf-step-status-succeeded {
+  background: rgba(34, 197, 94, 0.12);
+  color: #16a34a;
+}
+
+.wf-step-status-failed {
+  background: rgba(239, 68, 68, 0.12);
+  color: #dc2626;
+}
+
+.wf-step-status-stopped {
+  background: rgba(245, 158, 11, 0.12);
+  color: #b45309;
+}
+
+.wf-step-pending {
+  font-size: 10px;
+  font-weight: 600;
+  color: #f59e0b;
+  padding: 1px 6px;
+  background: rgba(245, 158, 11, 0.1);
+  border-radius: 4px;
+  animation: wfStepBlink 1.5s ease-in-out infinite;
+}
+
+@keyframes wfStepBlink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.4; }
+}
+
+.wf-step-text {
+  margin-top: 6px;
+  padding: 10px 12px;
+  background: linear-gradient(135deg, rgba(240, 147, 251, 0.05) 0%, rgba(245, 87, 108, 0.03) 100%);
+  border-left: 3px solid #f5576c;
+  border-radius: 6px;
+  font-size: 13px;
+  line-height: 1.7;
+  white-space: pre-wrap;
+  word-break: break-word;
+  color: #1e293b;
+}
+
+.wf-step-cursor {
+  display: inline-block;
+  color: #165dff;
+  font-weight: 700;
+  animation: wfCursorBlink 1s steps(2) infinite;
+  margin-left: 2px;
+}
+
+@keyframes wfCursorBlink {
+  0%, 50% { opacity: 1; }
+  51%, 100% { opacity: 0; }
+}
+
+.wf-step-error {
+  margin-top: 6px;
+  padding: 8px 10px;
+  background: rgba(239, 68, 68, 0.06);
+  border: 1px solid rgba(239, 68, 68, 0.2);
+  border-radius: 6px;
+  color: #dc2626;
+  font-size: 12px;
+  line-height: 1.5;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+}
+
+.wf-step-details {
+  margin-top: 6px;
+  font-size: 12px;
+}
+
+.wf-step-details summary {
+  cursor: pointer;
+  color: #667eea;
+  font-weight: 500;
+  padding: 4px 0;
+  user-select: none;
+}
+
+.wf-step-details summary:hover {
+  color: #5568d3;
+}
+
+.wf-step-details pre {
+  margin-top: 6px;
+  padding: 10px;
+  background: rgba(102, 126, 234, 0.04);
+  border: 1px solid rgba(102, 126, 234, 0.12);
+  border-radius: 6px;
+  font-size: 11px;
+  line-height: 1.5;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+  color: #1e293b;
+  overflow-x: auto;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+.agent-execute {
+  gap: 12px;
+  color: #1d2129;
+}
+
+.execute-detail-card,
+.execute-card,
+.workflow-side-card,
+.workflow-side-collapsed {
+  background: #ffffff;
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+  box-shadow: none;
+}
+
+.execute-detail-card {
+  padding: 16px 20px;
+}
+
+.execute-detail-head {
+  gap: 12px;
+}
+
+.execute-detail-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 4px;
+  box-shadow: none;
+  font-size: 24px;
+}
+
+.execute-detail-icon-img {
+  border-radius: 4px;
+}
+
+.execute-detail-name {
+  color: #1d2129;
+  background: none;
+  -webkit-text-fill-color: currentColor;
+  font-size: 18px;
+  font-weight: 600;
+  line-height: 26px;
+  margin: 0 0 4px;
+}
+
+.execute-detail-sub,
+.execute-detail-stats,
+.execute-detail-id,
+.execute-msg-role,
+.wf-form-tip,
+.execute-empty {
+  color: #86909c;
+}
+
+.execute-detail-mode-tag,
+.execute-detail-tag,
+.execute-card-tip,
+.wf-step-tag,
+.wf-form-type-badge,
+.wf-form-required-badge,
+.wf-summary {
+  background: #f2f3f5;
+  border: 1px solid #e5e6eb;
+  border-radius: 2px;
+  color: #4e5969;
+  font-weight: 500;
+}
+
+.execute-detail-apikey-trigger,
+.execute-detail-history-trigger,
+.execute-detail-new-trigger,
+.execute-send-btn,
+.apikey-modal-copy,
+.workflow-side-collapsed-run {
+  background: #165dff;
+  border: 1px solid #165dff;
+  border-radius: 4px;
+  box-shadow: none;
+  color: #ffffff;
+  font-weight: 500;
+  transform: none;
+}
+
+.execute-detail-new-trigger,
+.execute-params-btn,
+.execute-msg-action-btn,
+.workflow-side-expand,
+.workflow-side-close {
+  background: #ffffff;
+  border: 1px solid #c9cdd4;
+  border-radius: 4px;
+  box-shadow: none;
+  color: #1d2129;
+}
+
+.execute-detail-apikey-trigger:hover,
+.execute-detail-history-trigger:hover,
+.execute-detail-new-trigger:hover,
+.execute-send-btn:not(:disabled):hover,
+.apikey-modal-copy:hover,
+.workflow-side-collapsed-run:hover:not(:disabled) {
+  background: #4080ff;
+  border-color: #4080ff;
+  box-shadow: none;
+  color: #ffffff;
+  transform: none;
+}
+
+.execute-detail-history-trigger {
+  height: 32px;
+  padding: 0 12px;
+  gap: 6px;
+  border-radius: 4px;
+  font-size: 13px;
+  font-weight: 500;
+  letter-spacing: 0;
+}
+
+.execute-detail-history-trigger .history-icon,
+.execute-detail-history-trigger .new-chat-icon {
+  width: 15px;
+  height: 15px;
+}
+
+.execute-params-btn:hover:not(:disabled),
+.execute-msg-action-btn:hover:not(:disabled),
+.workflow-side-expand:hover,
+.workflow-side-close:hover {
+  background: #f2f3f5;
+  border-color: #c9cdd4;
+  box-shadow: none;
+  color: #165dff;
+  transform: none;
+}
+
+.execute-stop-btn,
+.execute-stop-btn:hover {
+  background: #f53f3f !important;
+  border-color: #f53f3f !important;
+  box-shadow: none !important;
+}
+
+.execute-send-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  min-width: 78px;
+}
+
+.execute-btn-icon {
+  width: 15px;
+  height: 15px;
+  flex-shrink: 0;
+}
+
+.execute-card-head,
+.workflow-side-card-head,
+.execute-input-bar,
+.apikey-modal-head,
+.apikey-modal-foot {
+  background: #ffffff;
+  border-color: #e5e6eb;
+}
+
+.execute-card-head {
+  margin-bottom: 12px;
+}
+
+.workflow-side-card-head {
+  border-radius: 0;
+}
+
+.execute-card-title,
+.workflow-side-card-head .execute-card-title,
+.wf-step-title,
+.wf-form-label,
+.apikey-modal-head h3 {
+  color: #1d2129;
+  background: none;
+  font-weight: 600;
+}
+
+.workflow-side-card-head .execute-card-title {
+  -webkit-background-clip: initial;
+  background-clip: initial;
+}
+
+.execute-msg {
+  max-width: 76%;
+}
+
+.execute-msg-bubble {
+  border-radius: 4px;
+  line-height: 1.7;
+}
+
+.execute-msg-user .execute-msg-bubble {
+  background: #165dff;
+  color: #ffffff;
+}
+
+.execute-msg-assistant .execute-msg-bubble {
+  background: #f7f8fa;
+  border: 1px solid #e5e6eb;
+  color: #1d2129;
+}
+
+.markdown-body blockquote {
+  background: #f7f8fa;
+  border-left-color: #165dff;
+  color: #4e5969;
+}
+
+.markdown-body pre,
+.wf-step-details pre,
+.apikey-modal-value {
+  background: #f7f8fa;
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+}
+
+.markdown-body code {
+  background: #f2f3f5;
+  border-radius: 2px;
+}
+
+.execute-input,
+.wf-form-input,
+.wf-form-textarea,
+.wf-form-select,
+.chat-params-modal-body .wf-form-input,
+.chat-params-modal-body .wf-form-textarea,
+.chat-params-modal-body .wf-form-select {
+  background: #ffffff;
+  border: 1px solid #c9cdd4;
+  border-radius: 4px;
+  color: #1d2129;
+  box-shadow: none;
+}
+
+.execute-input:focus,
+.wf-form-input:focus,
+.wf-form-textarea:focus,
+.wf-form-select:focus,
+.chat-params-modal-body .wf-form-input:focus,
+.chat-params-modal-body .wf-form-textarea:focus,
+.chat-params-modal-body .wf-form-select:focus {
+  background: #ffffff;
+  border-color: #165dff;
+  box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.12);
+}
+
+.wf-form-empty,
+.wf-form-loading {
+  background: #f7f8fa;
+  border: 1px dashed #c9cdd4;
+  border-radius: 4px;
+  color: #86909c;
+}
+
+.wf-form-file-trigger,
+.chat-params-modal-body .wf-form-file-trigger {
+  background: #ffffff;
+  border: 1px dashed #c9cdd4;
+  border-radius: 4px;
+  color: #4e5969;
+}
+
+.wf-form-file-trigger:hover,
+.chat-params-modal-body .wf-form-file-trigger:hover {
+  background: #f7f8fa;
+  border-color: #165dff;
+  color: #165dff;
+}
+
+.wf-form-file-item,
+.chat-params-modal-body .wf-form-file-item {
+  background: #f7f8fa;
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+}
+
+.wf-step {
+  background: #ffffff;
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+}
+
+.wf-step:hover {
+  background: #f7f8fa;
+  border-color: #c9cdd4;
+}
+
+.wf-step-running {
+  border-left: 3px solid #165dff;
+  animation: none;
+}
+
+.wf-step-marker,
+.wf-step-marker-start,
+.wf-step-marker-running,
+.wf-step-marker-succeeded,
+.wf-step-marker-failed,
+.wf-step-marker-output,
+.wf-step-marker-paused,
+.wf-step-marker-human,
+.wf-step-marker-error {
+  background: #f2f3f5;
+  border: 1px solid #c9cdd4;
+  color: #4e5969;
+  box-shadow: none;
+  animation: none;
+}
+
+.wf-step-marker-start,
+.wf-step-marker-output,
+.wf-step-marker-running {
+  background: #e8f3ff;
+  border-color: #94bfff;
+  color: #165dff;
+}
+
+.wf-step-marker-succeeded {
+  background: #e8ffea;
+  border-color: #7be188;
+  color: #00b42a;
+}
+
+.wf-step-marker-failed,
+.wf-step-marker-error {
+  background: #ffece8;
+  border-color: #fdcdc5;
+  color: #f53f3f;
+}
+
+.wf-step-text {
+  background: #f7f8fa;
+  border: 1px solid #e5e6eb;
+  border-left: 3px solid #165dff;
+  border-radius: 4px;
+  color: #1d2129;
+}
+
+.wf-step-error,
+.execute-detail-error,
+.execute-chat-error {
+  background: #ffece8;
+  border: 1px solid #fdcdc5;
+  border-radius: 4px;
+  color: #f53f3f;
+}
+
+.wf-summary-succeeded {
+  background: #e8ffea;
+  border-color: #aff0b5;
+  color: #00b42a;
+}
+
+.wf-summary-failed {
+  background: #ffece8;
+  border-color: #fdcdc5;
+  color: #f53f3f;
+}
+
+.wf-summary-stopped {
+  background: #fff7e8;
+  border-color: #ffe4ba;
+  color: #ff7d00;
+}
+
+.execute-messages,
+.wf-timeline,
+.workflow-side .wf-form,
+.chat-params-modal-body {
+  scrollbar-color: #c9cdd4 #f7f8fa;
+}
+
+.execute-messages::-webkit-scrollbar-track,
+.wf-timeline::-webkit-scrollbar-track,
+.workflow-side .wf-form::-webkit-scrollbar-track,
+.chat-params-modal-body::-webkit-scrollbar-track {
+  background: #f7f8fa;
+}
+
+.execute-messages::-webkit-scrollbar-thumb,
+.wf-timeline::-webkit-scrollbar-thumb,
+.workflow-side .wf-form::-webkit-scrollbar-thumb,
+.chat-params-modal-body::-webkit-scrollbar-thumb {
+  background: #c9cdd4;
+  border: 2px solid #f7f8fa;
+  border-radius: 999px;
+}
+
+.execute-messages::-webkit-scrollbar-thumb:hover,
+.wf-timeline::-webkit-scrollbar-thumb:hover,
+.workflow-side .wf-form::-webkit-scrollbar-thumb:hover,
+.chat-params-modal-body::-webkit-scrollbar-thumb:hover {
+  background: #a9aeb8;
+}
+
+.apikey-modal-mask {
+  background: rgba(29, 33, 41, 0.45);
+  backdrop-filter: none;
+}
+
+.apikey-modal,
+.chat-params-modal {
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+  box-shadow: 0 8px 24px rgba(29, 33, 41, 0.12);
+}
+
+.apikey-modal-copy.is-copied {
+  background: #00b42a;
+  border-color: #00b42a;
+}
+
+.execute-msg-actions {
+  align-items: center;
+  gap: 8px;
+  margin-top: 10px;
+  margin-left: 0;
+}
+
+.execute-msg-download-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  height: 30px;
+  padding: 0 10px 0 8px;
+  background: #ffffff;
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+  color: #4e5969;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 1;
+  box-shadow: none;
+}
+
+.execute-msg-download-btn:hover:not(:disabled) {
+  background: #f7f8fa;
+  border-color: #c9cdd4;
+  color: #1d2129;
+}
+
+.execute-msg-download-word:hover:not(:disabled) {
+  border-color: #94bfff;
+  color: #165dff;
+}
+
+.execute-msg-download-pdf:hover:not(:disabled) {
+  border-color: #fdcdc5;
+  color: #f53f3f;
+}
+
+.execute-msg-download-icon {
+  width: 18px;
+  height: 18px;
+  object-fit: contain;
+  flex-shrink: 0;
+}
+
+.execute-msg-download-text {
+  min-width: 32px;
+  text-align: left;
+}
+
+.execute-msg-download-btn:disabled .execute-msg-download-icon {
+  opacity: 0.55;
+}
+
+.arco-shell .main-content.arco-shell-content:has(.agent-execute) {
+  background:
+    radial-gradient(circle at 18% 12%, rgba(22, 93, 255, 0.16), transparent 28%),
+    radial-gradient(circle at 88% 10%, rgba(20, 201, 201, 0.14), transparent 30%),
+    linear-gradient(135deg, #eef5ff 0%, #f8fbff 42%, #edf7f7 100%);
+}
+
+.arco-shell .main-page-content.arco-shell-page:has(.agent-execute) {
+  max-width: none;
+  min-height: calc(100vh - 64px);
+  margin: 0;
+  padding: 16px 18px 20px;
+}
+
+.agent-execute {
+  position: relative;
+  width: 100%;
+  height: calc(100vh - 100px);
+  min-height: 0;
+  gap: 14px;
+  padding: 0;
+  color: #1d2129;
+}
+
+.agent-execute::before {
+  content: '';
+  position: absolute;
+  inset: -16px -18px -20px -18px;
+  pointer-events: none;
+  background:
+    linear-gradient(rgba(22, 93, 255, 0.035) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(22, 93, 255, 0.035) 1px, transparent 1px),
+    radial-gradient(circle at 8% 24%, rgba(22, 93, 255, 0.12), transparent 30%),
+    radial-gradient(circle at 94% 72%, rgba(20, 201, 201, 0.12), transparent 28%);
+  background-size: 32px 32px, 32px 32px, auto, auto;
+  border-radius: 26px;
+  z-index: 0;
+}
+
+.agent-execute > :not(.apikey-modal-mask):not(.execute-drawer-mask):not(.execute-drawer) {
+  position: relative;
+  z-index: 1;
+}
+
+.agent-execute > .apikey-modal-mask {
+  position: fixed;
+  inset: 0;
+  z-index: 1000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.agent-execute > .execute-drawer-mask {
+  position: fixed;
+  inset: 0;
+  z-index: 1200;
+}
+
+.agent-execute > .execute-drawer {
+  position: fixed;
+  top: 0;
+  right: 0;
+  z-index: 1201;
+}
+
+.execute-detail-card {
+  padding: 16px 20px;
+  border: 0;
+  border-radius: 0;
+  background: transparent;
+  box-shadow: none;
+  backdrop-filter: none;
+}
+
+.execute-detail-head {
+  align-items: flex-start;
+  gap: 14px;
+}
+
+.execute-detail-icon {
+  width: 54px;
+  height: 54px;
+  border-radius: 16px;
+  box-shadow: 0 10px 26px rgba(22, 93, 255, 0.16);
+  font-size: 28px;
+}
+
+.execute-detail-icon-img {
+  border-radius: 16px;
+}
+
+.execute-detail-name {
+  margin: 0 0 6px;
+  color: #0f1f3d;
+  font-size: 22px;
+  font-weight: 700;
+  letter-spacing: -0.02em;
+  background: none;
+  -webkit-text-fill-color: currentColor;
+}
+
+.execute-detail-mode-tag {
+  padding: 4px 10px;
+  border: 1px solid rgba(22, 93, 255, 0.18);
+  border-radius: 999px;
+  background: #e8f3ff;
+  color: #165dff;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.execute-detail-history-trigger {
+  height: 36px;
+  padding: 0 14px;
+  border: 1px solid rgba(22, 93, 255, 0.16);
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.92);
+  color: #1d2129;
+  box-shadow: 0 8px 18px rgba(29, 33, 41, 0.06);
+}
+
+.execute-detail-history-trigger:hover {
+  border-color: rgba(22, 93, 255, 0.38);
+  background: #e8f3ff;
+  color: #165dff;
+  transform: translateY(-1px);
+}
+
+.execute-chat-shell {
+  position: relative;
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  overflow: hidden;
+  border-radius: 26px;
+}
+
+.execute-chat-orb {
+  position: absolute;
+  border-radius: 999px;
+  filter: blur(8px);
+  opacity: 0.82;
+  pointer-events: none;
+}
+
+.execute-chat-orb-a {
+  width: 180px;
+  height: 180px;
+  right: 44px;
+  top: 32px;
+  background: rgba(22, 93, 255, 0.13);
+}
+
+.execute-chat-orb-b {
+  width: 220px;
+  height: 220px;
+  left: 8%;
+  bottom: -86px;
+  background: rgba(20, 201, 201, 0.12);
+}
+
+.execute-chat-card {
+  position: relative;
+  flex: 1;
+  min-height: 0;
+  border: 1px solid rgba(22, 93, 255, 0.13);
+  border-radius: 26px;
+  background:
+    radial-gradient(circle at 98% 0%, rgba(22, 93, 255, 0.12), transparent 34%),
+    radial-gradient(circle at 0% 100%, rgba(20, 201, 201, 0.1), transparent 36%),
+    linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(247, 250, 255, 0.82));
+  box-shadow: 0 22px 58px rgba(29, 33, 41, 0.12);
+  overflow: hidden;
+}
+
+.execute-chat-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  padding: 18px 22px 14px;
+  border-bottom: 1px solid rgba(229, 230, 235, 0.76);
+  background: rgba(255, 255, 255, 0.72);
+  backdrop-filter: blur(14px);
+}
+
+.execute-chat-agent-head {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  min-width: 0;
+}
+
+.execute-chat-agent-meta {
+  min-width: 0;
+}
+
+.execute-chat-kicker {
+  margin-bottom: 4px;
+  color: #165dff;
+  font-size: 11px;
+  font-weight: 700;
+  letter-spacing: 0.12em;
+}
+
+.execute-chat-heading {
+  margin: 0;
+  color: #0f1f3d;
+  font-size: 18px;
+  font-weight: 700;
+  letter-spacing: -0.01em;
+}
+
+.execute-chat-toolbar-actions {
+  display: inline-flex;
+  align-items: center;
+  gap: 10px;
+  flex-shrink: 0;
+}
+
+.execute-back-active-btn {
+  height: 32px;
+  padding: 0 12px;
+  border: 1px solid rgba(22, 93, 255, 0.24);
+  border-radius: 999px;
+  background: #e8f3ff;
+  color: #165dff;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.execute-back-active-btn:hover {
+  border-color: rgba(22, 93, 255, 0.42);
+  background: #dbeaff;
+  transform: translateY(-1px);
+}
+
+.execute-chat-status {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  height: 32px;
+  padding: 0 12px;
+  border: 1px solid #e5e6eb;
+  border-radius: 999px;
+  background: #ffffff;
+  color: #4e5969;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.execute-chat-status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 999px;
+  background: #00b42a;
+  box-shadow: 0 0 0 4px rgba(0, 180, 42, 0.12);
+}
+
+.execute-chat-status-dot.is-running {
+  background: #165dff;
+  box-shadow: 0 0 0 4px rgba(22, 93, 255, 0.14);
+  animation: executePulse 1.2s ease-in-out infinite;
+}
+
+@keyframes executePulse {
+  0%, 100% { transform: scale(1); opacity: 1; }
+  50% { transform: scale(0.72); opacity: 0.62; }
+}
+
+.execute-messages {
+  padding: 26px clamp(24px, 4vw, 56px) 18px;
+  gap: 18px;
+  background-image:
+    linear-gradient(rgba(22, 93, 255, 0.035) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(22, 93, 255, 0.035) 1px, transparent 1px),
+    linear-gradient(180deg, rgba(232, 243, 255, 0.22), rgba(255, 255, 255, 0));
+  background-size: 28px 28px, 28px 28px, auto;
+}
+
+.execute-empty {
+  width: min(540px, 92%);
+  margin: auto;
+  padding: 34px 34px 30px;
+  border: 1px solid rgba(22, 93, 255, 0.13);
+  border-radius: 28px;
+  background: rgba(255, 255, 255, 0.76);
+  box-shadow: 0 18px 46px rgba(29, 33, 41, 0.08);
+  color: #4e5969;
+  text-align: center;
+  backdrop-filter: blur(18px);
+}
+
+.execute-empty-icon {
+  width: 56px;
+  height: 56px;
+  margin: 0 auto 16px;
+  display: grid;
+  place-items: center;
+  border-radius: 18px;
+  background: linear-gradient(135deg, #165dff, #14c9c9);
+  color: #ffffff;
+  font-size: 28px;
+  box-shadow: 0 14px 28px rgba(22, 93, 255, 0.22);
+}
+
+.execute-empty-title {
+  color: #0f1f3d;
+  font-size: 22px;
+  font-weight: 700;
+  letter-spacing: -0.02em;
+}
+
+.execute-empty-desc {
+  margin: 10px auto 0;
+  max-width: 410px;
+  color: #86909c;
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+.execute-empty-prompts {
+  display: flex;
+  justify-content: center;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-top: 22px;
+}
+
+.execute-empty-prompts span {
+  padding: 7px 12px;
+  border: 1px solid rgba(22, 93, 255, 0.13);
+  border-radius: 999px;
+  background: #f7f8fa;
+  color: #4e5969;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.execute-msg {
+  max-width: min(920px, 84%);
+  gap: 7px;
+}
+
+.execute-msg-assistant {
+  max-width: min(1080px, 90%);
+}
+
+.execute-msg-user {
+  max-width: min(780px, 72%);
+}
+
+.execute-msg-role {
+  padding: 0 4px;
+  color: #86909c;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.execute-msg-bubble {
+  padding: 13px 16px;
+  border-radius: 18px;
+  font-size: 14px;
+  line-height: 1.78;
+  box-shadow: 0 10px 28px rgba(29, 33, 41, 0.07);
+}
+
+.execute-msg-user .execute-msg-bubble {
+  border: 1px solid rgba(22, 93, 255, 0.22);
+  background: linear-gradient(135deg, #165dff 0%, #0fc6c2 100%);
+  color: #ffffff;
+  border-bottom-right-radius: 6px;
+}
+
+.execute-msg-assistant .execute-msg-bubble {
+  border: 1px solid rgba(229, 230, 235, 0.95);
+  background: rgba(255, 255, 255, 0.92);
+  color: #1d2129;
+  border-bottom-left-radius: 6px;
+  padding: 16px 20px;
+}
+
+.execute-input-panel {
+  padding: 12px clamp(18px, 3vw, 42px) 18px;
+  border-top: 1px solid rgba(229, 230, 235, 0.76);
+  background: rgba(255, 255, 255, 0.84);
+  backdrop-filter: blur(18px);
+}
+
+.execute-input-hint {
+  margin: 0 0 8px 14px;
+  color: #a9aeb8;
+  font-size: 12px;
+}
+
+.execute-input-bar {
+  align-items: flex-end;
+  gap: 10px;
+  padding: 10px;
+  border: 1px solid rgba(22, 93, 255, 0.14);
+  border-radius: 20px;
+  background: #ffffff;
+  box-shadow: 0 12px 30px rgba(29, 33, 41, 0.08);
+}
+
+.execute-input {
+  min-height: 44px;
+  max-height: 132px;
+  padding: 11px 14px;
+  border: 0;
+  border-radius: 14px;
+  background: #f7f8fa;
+  color: #1d2129;
+  font-size: 14px;
+  line-height: 22px;
+  box-shadow: none;
+  resize: vertical;
+  overflow-y: auto;
+}
+
+.execute-input:focus {
+  background: #ffffff;
+  border: 0;
+  box-shadow: inset 0 0 0 2px rgba(22, 93, 255, 0.18);
+}
+
+.execute-params-btn {
+  height: 42px;
+  padding: 0 13px;
+  border-radius: 14px;
+  border-color: #e5e6eb;
+  background: #ffffff;
+}
+
+.execute-send-btn {
+  height: 42px;
+  min-width: 92px;
+  padding: 0 18px;
+  border: 0;
+  border-radius: 14px;
+  background: linear-gradient(135deg, #165dff 0%, #14c9c9 100%);
+  box-shadow: 0 10px 22px rgba(22, 93, 255, 0.22);
+}
+
+.execute-send-btn:not(:disabled):hover {
+  background: linear-gradient(135deg, #4080ff 0%, #23d3d3 100%);
+  box-shadow: 0 14px 28px rgba(22, 93, 255, 0.28);
+  transform: translateY(-1px);
+}
+
+.execute-send-btn:disabled {
+  opacity: 0.48;
+  cursor: not-allowed;
+}
+
+.execute-chat-error {
+  margin: 0 18px 8px;
+}
+
+.chat-params-modal {
+  width: min(760px, calc(100vw - 64px));
+  max-height: 88vh;
+  border-radius: 18px;
+}
+
+.chat-params-modal .apikey-modal-head {
+  padding: 18px 24px;
+}
+
+.chat-params-modal-body {
+  padding: 24px 28px;
+}
+
+.chat-params-modal-body .wf-form {
+  gap: 18px;
+}
+
+.chat-params-modal-body .wf-form-file-trigger {
+  min-height: 92px;
+  border-radius: 14px;
+}
+
+.chat-params-modal .apikey-modal-foot {
+  padding: 16px 24px;
+}
+
+@media (max-width: 768px) {
+  .agent-execute {
+    height: auto;
+    min-height: calc(100vh - 72px);
+    padding: 0 0 10px;
+  }
+
+  .execute-detail-head,
+  .execute-chat-toolbar,
+  .execute-input-bar {
+    flex-wrap: wrap;
+  }
+
+  .execute-chat-heading {
+    font-size: 16px;
+  }
+
+  .execute-msg {
+    max-width: 92%;
+  }
+
+  .execute-messages {
+    padding: 20px 16px 14px;
+    min-height: 420px;
+  }
+
+  .execute-input,
+  .execute-params-btn,
+  .execute-send-btn {
+    flex: 1 1 auto;
+  }
+}
+
+.apikey-create-section {
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.04) 0%, rgba(118, 75, 162, 0.02) 100%);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 20px;
+}
+
+.apikey-create-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #1a1a1a;
+  margin-bottom: 12px;
+}
+
+.apikey-create-form {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.apikey-expires-input {
+  flex: 1;
+  height: 40px;
+  padding: 0 14px;
+  background: rgba(255, 255, 255, 0.9);
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 8px;
+  font-size: 14px;
+  color: #1a1a1a;
+  outline: none;
+  transition: all 0.2s ease;
+}
+
+.apikey-expires-input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.apikey-list {
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 12px;
+  overflow: hidden;
+  background: white;
+}
+
+.apikey-list-header,
+.apikey-list-item {
+  display: grid;
+  grid-template-columns: 1fr 120px 200px 180px;
+  gap: 16px;
+  padding: 14px 20px;
+  align-items: center;
+}
+
+.apikey-list-header {
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.05) 100%);
+  font-size: 12px;
+  font-weight: 600;
+  color: #4a4a4a;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+
+.apikey-list-item {
+  border-top: 1px solid rgba(102, 126, 234, 0.1);
+  transition: background 0.2s ease;
+}
+
+.apikey-list-item:hover {
+  background: rgba(102, 126, 234, 0.03);
+}
+
+.apikey-col-key {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  min-width: 0;
+}
+
+.apikey-token-text {
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+  font-size: 13px;
+  color: #1a1a1a;
+  font-weight: 500;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.apikey-copy-btn {
+  flex-shrink: 0;
+  width: 32px;
+  height: 32px;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  background: rgba(102, 126, 234, 0.05);
+  border-radius: 6px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  font-size: 14px;
+  transition: all 0.2s ease;
+}
+
+.apikey-copy-btn:hover {
+  background: rgba(102, 126, 234, 0.15);
+  border-color: #667eea;
+  transform: translateY(-1px);
+}
+
+.apikey-status {
+  display: inline-block;
+  padding: 4px 12px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.apikey-status.active {
+  background: rgba(34, 197, 94, 0.12);
+  color: #16a34a;
+}
+
+.apikey-status.inactive {
+  background: rgba(120, 120, 120, 0.1);
+  color: #6b7280;
+}
+
+.apikey-col-expires {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.apikey-expires-text {
+  font-size: 13px;
+  color: #4a4a4a;
+  font-weight: 500;
+}
+
+.apikey-remain-text {
+  font-size: 11px;
+  color: #999;
+}
+
+.apikey-col-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+
+.apikey-action-btn {
+  height: 32px;
+  padding: 0 14px;
+  border-radius: 6px;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  border: 1px solid;
+}
+
+.apikey-action-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.apikey-action-btn.enable {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-color: transparent;
+  color: white;
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+.apikey-action-btn.enable:hover:not(:disabled) {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.apikey-action-btn.disable,
+.apikey-action-btn.delete {
+  background: rgba(102, 126, 234, 0.08);
+  border-color: rgba(102, 126, 234, 0.25);
+  color: #667eea;
+}
+
+.apikey-action-btn.disable:hover:not(:disabled),
+.apikey-action-btn.delete:hover:not(:disabled) {
+  background: rgba(102, 126, 234, 0.15);
+  border-color: #667eea;
+}
+
+.apikey-action-btn.delete:hover:not(:disabled) {
+  background: rgba(239, 68, 68, 0.1);
+  border-color: rgba(239, 68, 68, 0.3);
+  color: #dc2626;
+}
+
+.apikey-loading,
+.apikey-empty {
+  padding: 60px 20px;
+  text-align: center;
+  color: #999;
+  font-size: 14px;
+}

+ 99 - 0
src/App.jsx

@@ -0,0 +1,99 @@
+import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
+import { AuthProvider, useAuth } from './contexts/AuthContext'
+import ProtectedRoute from './components/ProtectedRoute'
+import Layout from './components/Layout'
+import Login from './pages/Login'
+import Plaza from './pages/Plaza'
+import AgentList from './pages/AgentList'
+import History from './pages/History'
+import AvailableAgents from './pages/AvailableAgents'
+import AgentExecute from './pages/AgentExecute'
+import AgentApply from './pages/AgentApply'
+import ExternalMock from './pages/ExternalMock'
+import './App.css'
+
+function AppRoutes() {
+  const { authenticated } = useAuth()
+
+  return (
+    <Routes>
+      <Route 
+        path="/login" 
+        element={authenticated ? <Navigate to="/" replace /> : <Login />} 
+      />
+      <Route path="/" element={
+        <ProtectedRoute>
+          <Layout>
+            <Plaza />
+          </Layout>
+        </ProtectedRoute>
+      } />
+      <Route path="/plaza" element={
+        <ProtectedRoute>
+          <Layout>
+            <Plaza />
+          </Layout>
+        </ProtectedRoute>
+      } />
+      <Route path="/agents" element={
+        <ProtectedRoute>
+          <Layout>
+            <AgentList />
+          </Layout>
+        </ProtectedRoute>
+      } />
+      <Route path="/history" element={
+        <ProtectedRoute>
+          <Layout>
+            <History />
+          </Layout>
+        </ProtectedRoute>
+      } />
+      <Route path="/available" element={
+        <ProtectedRoute>
+          <Layout>
+            <AvailableAgents />
+          </Layout>
+        </ProtectedRoute>
+      } />
+      <Route path="/agents/execute/:appId" element={
+        <ProtectedRoute>
+          <Layout>
+            <AgentExecute />
+          </Layout>
+        </ProtectedRoute>
+      } />
+      <Route path="/agents/apply/:appId" element={
+        <ProtectedRoute>
+          <Layout>
+            <AgentApply />
+          </Layout>
+        </ProtectedRoute>
+      } />
+      <Route path="/agents/external/:agentProId" element={
+        <ProtectedRoute>
+          <Layout>
+            <ExternalMock />
+          </Layout>
+        </ProtectedRoute>
+      } />
+    </Routes>
+  )
+}
+
+function App() {
+  return (
+    <AuthProvider>
+      <Router
+        future={{
+          v7_startTransition: true,
+          v7_relativeSplatPath: true,
+        }}
+      >
+        <AppRoutes />
+      </Router>
+    </AuthProvider>
+  )
+}
+
+export default App

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/assets/icons/pdf.svg


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/assets/icons/word.svg


+ 207 - 0
src/components/ApiKeyModal.jsx

@@ -0,0 +1,207 @@
+import { useState, useEffect, useMemo } from 'react'
+import { useAccountApiKeys, useCreateAccountApiKey, useUpdateAccountApiKey, useDeleteAccountApiKey } from '@/service/account'
+import '../App.css'
+
+const maskToken = (token) => `${token.slice(0, 10)}${'*'.repeat(16)}`
+
+const toDateText = (timestamp) => {
+  if (!timestamp) return '永久有效'
+  return new Date(timestamp * 1000).toLocaleDateString('zh-CN')
+}
+
+const getRemainDays = (timestamp) => {
+  if (!timestamp) return null
+  const remainMs = timestamp * 1000 - Date.now()
+  return Math.max(0, Math.ceil(remainMs / 86400000))
+}
+
+const isExpired = (apiKey) => apiKey.expires_at && apiKey.expires_at * 1000 <= Date.now()
+
+function ApiKeyModal({ onClose }) {
+  const [apiKeys, setApiKeys] = useState([])
+  const [loading, setLoading] = useState(false)
+  const [expiresInDays, setExpiresInDays] = useState('')
+  const [copied, setCopied] = useState(false)
+  const [error, setError] = useState('')
+  const [success, setSuccess] = useState('')
+  const [submitting, setSubmitting] = useState(false)
+  const [operateId, setOperateId] = useState(null)
+
+  useEffect(() => {
+    fetchKeys()
+  }, [])
+
+  const fetchKeys = async () => {
+    setLoading(true)
+    try {
+      const response = await useAccountApiKeys()
+      setApiKeys(response?.data || response || [])
+    } catch (e) {
+      setError(e.message || '获取API Key失败')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  const handleCreate = async () => {
+    const days = expiresInDays.trim() ? Number(expiresInDays) : null
+    if (days !== null && (!Number.isInteger(days) || days <= 0)) {
+      setError('请输入有效的过期天数')
+      return
+    }
+    setSubmitting(true)
+    setError('')
+    setSuccess('')
+    try {
+      await useCreateAccountApiKey({ body: { expires_in_days: days } })
+      setSuccess('API Key 创建成功')
+      setExpiresInDays('')
+      fetchKeys()
+    } catch (e) {
+      setError(e.message || '创建API Key失败')
+    } finally {
+      setSubmitting(false)
+    }
+  }
+
+  const handleCopy = (token) => {
+    navigator.clipboard.writeText(token)
+    setCopied(true)
+    setTimeout(() => setCopied(false), 2000)
+  }
+
+  const handleToggleStatus = async (apiKey) => {
+    const nextStatus = apiKey.status === 'active' ? 'inactive' : 'active'
+    setOperateId(apiKey.id)
+    try {
+      await useUpdateAccountApiKey(apiKey.id, { body: { status: nextStatus } })
+      fetchKeys()
+    } catch (e) {
+      setError(e.message || '更新状态失败')
+    } finally {
+      setOperateId(null)
+    }
+  }
+
+  const handleDelete = async (apiKey) => {
+    if (!window.confirm(`确定要删除 API Key 吗?`)) return
+    setOperateId(apiKey.id)
+    try {
+      await useDeleteAccountApiKey(apiKey.id)
+      setSuccess('API Key 已删除')
+      fetchKeys()
+    } catch (e) {
+      setError(e.message || '删除失败')
+    } finally {
+      setOperateId(null)
+    }
+  }
+
+  const isMutating = submitting || operateId !== null
+
+  const copiedTip = useMemo(() => {
+    return copied ? '已复制' : '复制'
+  }, [copied])
+
+  return (
+    <div className="profile-modal-overlay" onClick={onClose}>
+      <div className="profile-modal" onClick={e => e.stopPropagation()}>
+        <div className="profile-modal-header">
+          <h2>API Key 管理</h2>
+          <button className="profile-close-btn" onClick={onClose}>✕</button>
+        </div>
+
+        <div className="profile-tab-content">
+          {error && <div className="alert alert-error">{error}</div>}
+          {success && <div className="alert alert-success">{success}</div>}
+
+          <div className="apikey-create-section">
+            <div className="apikey-create-title">创建新的 API Key</div>
+            <div className="apikey-create-form">
+              <input
+                type="number"
+                min={1}
+                value={expiresInDays}
+                onChange={e => setExpiresInDays(e.target.value)}
+                placeholder="过期天数(留空表示永久有效)"
+                className="apikey-expires-input"
+              />
+              <button 
+                className="btn btn-primary" 
+                onClick={handleCreate}
+                disabled={isMutating}
+              >
+                {submitting ? '创建中...' : '添加'}
+              </button>
+            </div>
+          </div>
+
+          <div className="apikey-list">
+            <div className="apikey-list-header">
+              <div className="apikey-col apikey-col-key">Key</div>
+              <div className="apikey-col apikey-col-status">状态</div>
+              <div className="apikey-col apikey-col-expires">过期时间</div>
+              <div className="apikey-col apikey-col-actions">操作</div>
+            </div>
+            {loading ? (
+              <div className="apikey-loading">加载中...</div>
+            ) : apiKeys.length === 0 ? (
+              <div className="apikey-empty">暂无 API Key</div>
+            ) : (
+              apiKeys.map(apiKey => {
+                const expired = apiKey.status === 'active' && isExpired(apiKey)
+                const displayStatus = expired
+                  ? '已过期'
+                  : apiKey.status === 'active' ? '启用' : '禁用'
+                const remainDays = getRemainDays(apiKey.expires_at)
+                return (
+                  <div key={apiKey.id} className="apikey-list-item">
+                    <div className="apikey-col apikey-col-key">
+                      <span className="apikey-token-text">{maskToken(apiKey.token)}</span>
+                      <button 
+                        className="apikey-copy-btn"
+                        onClick={() => handleCopy(apiKey.token)}
+                        title={copiedTip}
+                      >
+                        {copied ? '✓' : '📋'}
+                      </button>
+                    </div>
+                    <div className="apikey-col apikey-col-status">
+                      <span className={`apikey-status ${apiKey.status === 'active' && !expired ? 'active' : 'inactive'}`}>
+                        {displayStatus}
+                      </span>
+                    </div>
+                    <div className="apikey-col apikey-col-expires">
+                      <div className="apikey-expires-text">{toDateText(apiKey.expires_at)}</div>
+                      {remainDays !== null && apiKey.expires_at && (
+                        <div className="apikey-remain-text">({remainDays} 天)</div>
+                      )}
+                    </div>
+                    <div className="apikey-col apikey-col-actions">
+                      <button 
+                        className={`apikey-action-btn ${apiKey.status === 'active' ? 'disable' : 'enable'}`}
+                        onClick={() => handleToggleStatus(apiKey)}
+                        disabled={isMutating}
+                      >
+                        {apiKey.status === 'active' ? '禁用' : '启用'}
+                      </button>
+                      <button 
+                        className="apikey-action-btn delete"
+                        onClick={() => handleDelete(apiKey)}
+                        disabled={isMutating}
+                      >
+                        删除
+                      </button>
+                    </div>
+                  </div>
+                )
+              })
+            )}
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default ApiKeyModal

+ 352 - 0
src/components/Layout.jsx

@@ -0,0 +1,352 @@
+import { useMemo, useState, useEffect } from 'react'
+import { useLocation, useNavigate } from 'react-router-dom'
+import {
+  Layout as ArcoLayout,
+  Menu,
+  Button,
+  Avatar,
+  Dropdown,
+  Typography,
+  Spin,
+  Message,
+} from '@arco-design/web-react'
+import {
+  IconApps,
+  IconMenuFold,
+  IconMenuUnfold,
+  IconHistory,
+  IconRobot,
+  IconUser,
+  IconSettings,
+  IconQuestionCircle,
+  IconExport,
+  IconLock,
+} from '@arco-design/web-react/icon'
+import { useAuth } from '@/contexts/AuthContext'
+import { logout as logoutService } from '@/service/auth'
+import { useUserProfile } from '@/service/account'
+import { clearTokens } from '@/utils/auth'
+import { getMyRecommendedApps, getAppApiKey } from '@/service/business'
+import { API_BASE_URL } from '@/config'
+import emojiMartData from '@emoji-mart/data'
+import ProfileModal from './ProfileModal'
+import ApiKeyModal from './ApiKeyModal'
+import '../App.css'
+
+const { Sider, Header, Content } = ArcoLayout
+const MenuItem = Menu.Item
+const SubMenu = Menu.SubMenu
+const SIDEBAR_COLLAPSED_KEY = 'sidebar_collapsed'
+
+const isNativeEmoji = (str) => {
+  if (!str) return false
+  return /[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(str)
+}
+
+const emojiNameToNative = (name) => {
+  if (!name) return '🤖'
+  const emoji = emojiMartData.emojis?.[name]
+  return emoji?.skins?.[0]?.native || '🤖'
+}
+
+const resolveIcon = (icon) => {
+  if (!icon) return '🤖'
+  if (isNativeEmoji(icon)) return icon
+  return emojiNameToNative(icon)
+}
+
+const buildIconUrl = (icon) => {
+  if (!icon) return ''
+  if (/^https?:\/\//i.test(icon)) return icon
+  if (icon.startsWith('/')) return `${API_BASE_URL}${icon}`
+  return icon
+}
+
+function Layout({ children }) {
+  const location = useLocation()
+  const navigate = useNavigate()
+  const { logout } = useAuth()
+  const [showProfile, setShowProfile] = useState(false)
+  const [showApiKey, setShowApiKey] = useState(false)
+  const [collapsed, setCollapsed] = useState(() => {
+    try { return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1' } catch { return false }
+  })
+  const [recommendedAgents, setRecommendedAgents] = useState([])
+  const [recLoading, setRecLoading] = useState(false)
+  const [recError, setRecError] = useState('')
+  const [navClickingId, setNavClickingId] = useState(null)
+  const [userProfile, setUserProfile] = useState(null)
+
+  useEffect(() => {
+    let cancelled = false
+    const fetchRecommended = async () => {
+      setRecLoading(true)
+      setRecError('')
+      try {
+        const res = await getMyRecommendedApps()
+        if (cancelled) return
+        const list = (res?.data || res || []).map(item => {
+          const iconType = item.icon_type || 'emoji'
+          let iconValue = '🤖'
+          if (iconType === 'image') {
+            const raw = item.icon || item.logo || ''
+            iconValue = { type: 'image', url: buildIconUrl(raw) }
+          } else if (typeof item.icon === 'string') {
+            iconValue = item.icon
+          }
+          return {
+            id: item.target_id,
+            name: item.name || '未命名',
+            desc: item.description || '',
+            icon: iconValue,
+            iconBackground: item.icon_background || '#FFEAD5',
+            mode: item.mode || '',
+            targetType: item.target_type,
+            sourceType: item.source_type,
+            sourceAppId: item.source_app_id,
+            sourceName: item.source_name,
+          }
+        })
+        setRecommendedAgents(list)
+      } catch (e) {
+        if (!cancelled) setRecError(e?.message || '加载失败')
+      } finally {
+        if (!cancelled) setRecLoading(false)
+      }
+    }
+    fetchRecommended()
+    return () => { cancelled = true }
+  }, [])
+
+  useEffect(() => {
+    let cancelled = false
+    const fetchUserProfile = async () => {
+      try {
+        const profile = await useUserProfile()
+        if (!cancelled) setUserProfile(profile?.data ?? profile ?? null)
+      } catch (e) {
+        if (!cancelled) setUserProfile(null)
+      }
+    }
+    fetchUserProfile()
+    return () => { cancelled = true }
+  }, [])
+
+  const selectedKeys = useMemo(() => {
+    const pathname = location.pathname
+    if (pathname === '/' || pathname === '/plaza') return ['plaza']
+    if (pathname === '/history') return ['history']
+    if (pathname === '/available') return ['available']
+    if (pathname.startsWith('/agents/execute/')) return [`agent:${pathname.split('/').pop()}`]
+    if (pathname.startsWith('/agents/external/')) return [`external:${pathname.split('/').pop()}`]
+    return []
+  }, [location.pathname])
+
+  const headerInfo = useMemo(() => {
+    const pathname = location.pathname
+    if (pathname === '/' || pathname === '/plaza') return { kicker: '智能体广场', title: '发现并使用推荐智能体' }
+    if (pathname === '/history') return { kicker: '历史记录', title: '查看最近的智能体执行记录' }
+    if (pathname === '/available') return { kicker: '可用智能体', title: '管理已开通权限的智能体' }
+    if (pathname.startsWith('/agents/execute/')) return { kicker: '智能体', title: '当前会话与任务执行' }
+    if (pathname.startsWith('/agents/external/')) return { kicker: '外部智能体', title: '跳转并使用外部服务' }
+    if (pathname.startsWith('/agents/apply/')) return { kicker: '权限申请', title: '申请智能体使用权限' }
+    return { kicker: '工作台', title: 'AI Agent 智能体平台' }
+  }, [location.pathname])
+
+  const userDisplayName = userProfile?.name || userProfile?.email || '用户'
+
+  const toggleSidebar = () => {
+    setCollapsed(prev => {
+      const next = !prev
+      try { localStorage.setItem(SIDEBAR_COLLAPSED_KEY, next ? '1' : '0') } catch {}
+      return next
+    })
+  }
+
+  const handleAgentNavClick = async (agent) => {
+    if (!agent?.id || navClickingId === agent.id) return
+    setNavClickingId(agent.id)
+    if (agent.sourceType === 'external') {
+      navigate(`/agents/external/${agent.id}`, { state: { agent } })
+      setNavClickingId(null)
+      return
+    }
+
+    const appId = agent.sourceAppId || agent.id
+    try {
+      const res = await getAppApiKey(appId)
+      const payload = res?.data ?? res ?? {}
+      if (payload.has_permission) {
+        navigate(`/agents/execute/${appId}`, {
+          state: { agent: { ...agent, id: appId }, apiKey: payload.api_key || '' },
+        })
+      } else {
+        navigate(`/agents/apply/${appId}`, { state: { agent: { ...agent, id: appId } } })
+      }
+    } catch (err) {
+      navigate(`/agents/apply/${appId}`, {
+        state: { agent: { ...agent, id: appId }, error: err?.message || '权限校验失败' },
+      })
+    } finally {
+      setNavClickingId(null)
+    }
+  }
+
+  const handleLogout = async () => {
+    try {
+      await logoutService()
+    } catch (e) {
+      // Logout should still clear local state even when the server call fails.
+    }
+    clearTokens()
+    logout()
+    navigate('/login')
+  }
+
+  const handleAdmin = () => {
+    Message.info('后台管理功能开发中')
+  }
+
+  const handleHelp = () => {
+    Message.info('使用说明功能开发中')
+  }
+
+  const renderAgentIcon = (agent) => {
+    if (agent.icon && typeof agent.icon === 'object' && agent.icon.type === 'image') {
+      return (
+        <span className="layout-agent-icon" style={{ background: agent.iconBackground || '#FFEAD5' }}>
+          <img src={buildIconUrl(agent.icon.url)} alt="" />
+        </span>
+      )
+    }
+    return <span className="layout-agent-icon layout-agent-emoji">{resolveIcon(agent.icon)}</span>
+  }
+
+  const userMenu = (
+    <Menu className="layout-user-menu">
+      <MenuItem key="profile" onClick={() => setShowProfile(true)}>
+        <IconUser /> 个人资料
+      </MenuItem>
+      <MenuItem key="apikey" onClick={() => setShowApiKey(true)}>
+        <IconLock /> API Key
+      </MenuItem>
+      <MenuItem key="admin" onClick={handleAdmin}>
+        <IconSettings /> 后台管理
+      </MenuItem>
+      <MenuItem key="help" onClick={handleHelp}>
+        <IconQuestionCircle /> 使用说明
+      </MenuItem>
+      <MenuItem key="logout" className="layout-user-menu-danger" onClick={handleLogout}>
+        <IconExport /> 退出登录
+      </MenuItem>
+    </Menu>
+  )
+
+  return (
+    <ArcoLayout className="app arco-shell">
+      <Sider
+        className="arco-shell-sider"
+        width={248}
+        collapsed={collapsed}
+        collapsedWidth={64}
+        trigger={null}
+      >
+        <div className="arco-shell-brand" onClick={() => navigate('/plaza')}>
+          <img src="/logo.png" alt="Logo" className="arco-shell-logo" />
+          {!collapsed && (
+            <div className="arco-shell-brand-text">
+              <Typography.Text className="arco-shell-brand-name">AI agentic</Typography.Text>
+              <Typography.Text className="arco-shell-brand-subtitle">智能体平台</Typography.Text>
+            </div>
+          )}
+        </div>
+
+        <Menu
+          className="arco-shell-menu"
+          selectedKeys={selectedKeys}
+          defaultOpenKeys={['agents', 'my']}
+          collapse={collapsed}
+          autoOpen
+          hasCollapseButton={false}
+        >
+          <MenuItem key="plaza" onClick={() => navigate('/plaza')}>
+            <IconApps />
+            <span>广场</span>
+          </MenuItem>
+
+          <SubMenu key="agents" title={<><IconRobot /><span>智能体</span></>}>
+            {recLoading && (
+              <MenuItem key="agents-loading" disabled>
+                <Spin size={14} /> 加载中
+              </MenuItem>
+            )}
+            {!recLoading && recError && (
+              <MenuItem key="agents-error" disabled>{recError}</MenuItem>
+            )}
+            {!recLoading && !recError && recommendedAgents.length === 0 && (
+              <MenuItem key="agents-empty" disabled>暂无推荐智能体</MenuItem>
+            )}
+            {!recLoading && !recError && recommendedAgents.map(agent => {
+              const targetAppId = agent.sourceAppId || agent.id
+              const menuKey = agent.sourceType === 'external' ? `external:${agent.id}` : `agent:${targetAppId}`
+              return (
+                <MenuItem key={menuKey} onClick={() => handleAgentNavClick(agent)}>
+                  {renderAgentIcon(agent)}
+                  <span className="layout-agent-name">{agent.name}</span>
+                  {navClickingId === agent.id && <Spin size={12} />}
+                </MenuItem>
+              )
+            })}
+          </SubMenu>
+
+          <SubMenu key="my" title={<><IconUser /><span>我的</span></>}>
+            <MenuItem key="history" onClick={() => navigate('/history')}>
+              <IconHistory />
+              <span>历史记录</span>
+            </MenuItem>
+            <MenuItem key="available" onClick={() => navigate('/available')}>
+              <IconRobot />
+              <span>可用智能体</span>
+            </MenuItem>
+          </SubMenu>
+        </Menu>
+
+        <div className="arco-shell-sider-footer">
+          <Button
+            type="text"
+            className="arco-shell-collapse"
+            icon={collapsed ? <IconMenuUnfold /> : <IconMenuFold />}
+            onClick={toggleSidebar}
+          >
+            {!collapsed && '收起菜单'}
+          </Button>
+        </div>
+      </Sider>
+
+      <ArcoLayout className="arco-shell-body">
+        <Header className="arco-shell-header">
+          <div className="arco-shell-header-title">
+            <Typography.Text className="arco-shell-kicker">{headerInfo.kicker}</Typography.Text>
+            <Typography.Title heading={5} className="arco-shell-title">{headerInfo.title}</Typography.Title>
+          </div>
+          <Dropdown droplist={userMenu} position="br" trigger="click">
+            <Button type="text" className="arco-shell-user">
+              <Avatar size={32} className="arco-shell-avatar"><IconUser /></Avatar>
+              <span className="arco-shell-user-name">{userDisplayName}</span>
+            </Button>
+          </Dropdown>
+        </Header>
+        <Content className="main-content arco-shell-content">
+          <div className="main-page-content arco-shell-page">
+            {children}
+          </div>
+        </Content>
+      </ArcoLayout>
+
+      {showProfile && <ProfileModal onClose={() => setShowProfile(false)} />}
+      {showApiKey && <ApiKeyModal onClose={() => setShowApiKey(false)} />}
+    </ArcoLayout>
+  )
+}
+
+export default Layout

+ 195 - 0
src/components/ProfileModal.jsx

@@ -0,0 +1,195 @@
+import { useState, useEffect } from 'react'
+import { useUserProfile, updateAccountName, updatePassword } from '@/service/account'
+
+function ProfileModal({ onClose }) {
+  // 加载用户信息
+  const [userProfile, setUserProfile] = useState(null)
+  const [loadingProfile, setLoadingProfile] = useState(false)
+
+  useEffect(() => {
+    loadUserProfile()
+  }, [])
+
+  const loadUserProfile = async () => {
+    setLoadingProfile(true)
+    try {
+      const response = await useUserProfile()
+      setUserProfile(response)
+    } catch (error) {
+      console.error('Failed to load user profile:', error)
+    } finally {
+      setLoadingProfile(false)
+    }
+  }
+
+
+
+  // 更新用户名
+  const [editingName, setEditingName] = useState(false)
+  const [newName, setNewName] = useState('')
+
+  const handleEditName = () => {
+    setNewName(userProfile?.name || '')
+    setEditingName(true)
+  }
+
+  const handleSaveName = async () => {
+    if (!newName.trim()) {
+      alert('用户名不能为空')
+      return
+    }
+    try {
+      const response = await updateAccountName(newName)
+      setUserProfile(response)
+      setEditingName(false)
+      alert('用户名更新成功')
+    } catch (error) {
+      console.error('Failed to update name:', error)
+      alert('更新用户名失败')
+    }
+  }
+
+  // 更新密码
+  const [showPasswordForm, setShowPasswordForm] = useState(false)
+  const [currentPassword, setCurrentPassword] = useState('')
+  const [newPassword, setNewPassword] = useState('')
+  const [repeatPassword, setRepeatPassword] = useState('')
+  const [updatingPassword, setUpdatingPassword] = useState(false)
+
+  const handleUpdatePassword = async () => {
+    if (newPassword !== repeatPassword) {
+      alert('两次输入的新密码不一致')
+      return
+    }
+    if (newPassword.length < 8) {
+      alert('新密码长度至少8位')
+      return
+    }
+    setUpdatingPassword(true)
+    try {
+      await updatePassword(currentPassword, newPassword, repeatPassword)
+      alert('密码更新成功')
+      setCurrentPassword('')
+      setNewPassword('')
+      setRepeatPassword('')
+      setShowPasswordForm(false)
+    } catch (error) {
+      console.error('Failed to update password:', error)
+      alert('更新密码失败')
+    } finally {
+      setUpdatingPassword(false)
+    }
+  }
+
+  return (
+    <div className="profile-modal-overlay" onClick={onClose}>
+      <div className="profile-modal" onClick={e => e.stopPropagation()}>
+        <div className="profile-modal-header">
+          <h2>个人信息</h2>
+          <button className="profile-close-btn" onClick={onClose}>✕</button>
+        </div>
+
+        <div className="profile-tab-content">
+          <div className="account-info">
+            <div className="account-avatar-section">
+              <div className="account-avatar">👤</div>
+              {editingName ? (
+                <div className="account-name-edit">
+                  <input
+                    type="text"
+                    value={newName}
+                    onChange={(e) => setNewName(e.target.value)}
+                    className="account-name-input"
+                    minLength={3}
+                    maxLength={30}
+                  />
+                  <div className="account-name-actions">
+                    <button onClick={handleSaveName} className="account-name-btn save">保存</button>
+                    <button onClick={() => setEditingName(false)} className="account-name-btn cancel">取消</button>
+                  </div>
+                </div>
+              ) : (
+                <div className="account-name-edit">
+                  <div className="account-name">{userProfile?.name || '用户'}</div>
+                  <button onClick={handleEditName} className="account-name-edit-btn">编辑</button>
+                </div>
+              )}
+            </div>
+            <div className="account-field">
+              <label>邮箱</label>
+              <input type="email" value={userProfile?.email || ''} readOnly />
+            </div>
+            <div className="account-field">
+              <label>个人数据</label>
+              <input type="text" value={`创建于: ${userProfile?.created_at ? new Date(userProfile.created_at * 1000).toLocaleDateString() : '未知'}`} readOnly />
+            </div>
+            
+            <div className="account-password-section">
+              <div className="account-password-title">密码管理</div>
+              {showPasswordForm ? (
+                <div className="account-password-form">
+                  <div className="account-password-field">
+                    <label>当前密码</label>
+                    <input
+                      type="password"
+                      value={currentPassword}
+                      onChange={(e) => setCurrentPassword(e.target.value)}
+                      placeholder="请输入当前密码"
+                    />
+                  </div>
+                  <div className="account-password-field">
+                    <label>新密码</label>
+                    <input
+                      type="password"
+                      value={newPassword}
+                      onChange={(e) => setNewPassword(e.target.value)}
+                      placeholder="请输入新密码(至少8位)"
+                    />
+                  </div>
+                  <div className="account-password-field">
+                    <label>确认新密码</label>
+                    <input
+                      type="password"
+                      value={repeatPassword}
+                      onChange={(e) => setRepeatPassword(e.target.value)}
+                      placeholder="请再次输入新密码"
+                    />
+                  </div>
+                  <div className="account-password-actions">
+                    <button 
+                      className="account-password-btn save" 
+                      onClick={handleUpdatePassword}
+                      disabled={updatingPassword}
+                    >
+                      {updatingPassword ? '保存中...' : '保存'}
+                    </button>
+                    <button 
+                      className="account-password-btn cancel" 
+                      onClick={() => {
+                        setShowPasswordForm(false)
+                        setCurrentPassword('')
+                        setNewPassword('')
+                        setRepeatPassword('')
+                      }}
+                    >
+                      取消
+                    </button>
+                  </div>
+                </div>
+              ) : (
+                <button 
+                  className="account-action-btn"
+                  onClick={() => setShowPasswordForm(true)}
+                >
+                  更改密码
+                </button>
+              )}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default ProfileModal

+ 12 - 0
src/components/ProtectedRoute.jsx

@@ -0,0 +1,12 @@
+import { Navigate } from 'react-router-dom'
+import { useAuth } from '@/contexts/AuthContext'
+
+export default function ProtectedRoute({ children }) {
+  const { authenticated } = useAuth()
+
+  if (!authenticated) {
+    return <Navigate to="/login" replace />
+  }
+
+  return children
+}

+ 51 - 0
src/components/UserDropdown.jsx

@@ -0,0 +1,51 @@
+import { useEffect, useRef } from 'react'
+
+function UserDropdown({ show, onClose, onProfile, onApiKey, onAdmin, onHelp, onLogout }) {
+  const dropdownRef = useRef(null)
+
+  useEffect(() => {
+    const handleClickOutside = (event) => {
+      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+        onClose()
+      }
+    }
+
+    if (show) {
+      document.addEventListener('mousedown', handleClickOutside)
+    }
+
+    return () => {
+      document.removeEventListener('mousedown', handleClickOutside)
+    }
+  }, [show, onClose])
+
+  if (!show) return null
+
+  return (
+    <div className="user-dropdown" ref={dropdownRef}>
+      <div className="dropdown-item" onClick={onProfile}>
+        <span className="dropdown-icon">👤</span>
+        <span>个人信息</span>
+      </div>
+      <div className="dropdown-item" onClick={onApiKey}>
+        <span className="dropdown-icon">🔑</span>
+        <span>API Key</span>
+      </div>
+      <div className="dropdown-item" onClick={onAdmin}>
+        <span className="dropdown-icon">⚙️</span>
+        <span>后台管理</span>
+      </div>
+      <div className="dropdown-item" onClick={onHelp}>
+        <span className="dropdown-icon">📖</span>
+        <span>使用说明</span>
+      </div>
+      <div className="dropdown-divider"></div>
+      <div className="dropdown-item logout" onClick={onLogout}>
+        <span className="dropdown-icon">🚪</span>
+        <span>退出登录</span>
+      </div>
+    </div>
+  )
+}
+
+export default UserDropdown

+ 66 - 0
src/config/agentEmptyStates.json

@@ -0,0 +1,66 @@
+{
+  "default": {
+    "icon": "✦",
+    "title": "选择目标,开始对话",
+    "description": "告诉我你想完成的任务、期望格式和关键要求,我会结合当前智能体能力为你生成清晰、可执行的结果。",
+    "prompts": ["说明任务目标", "补充关键信息", "按要求输出"]
+  },
+  "agents": [
+    {
+      "name": "智能问数",
+      "aliases": ["问数", "数据分析", "数据助手"],
+      "icon": "⌁",
+      "title": "上传数据,提出分析问题",
+      "description": "请提供数据表、指标口径或业务问题。我会帮你拆解数据关系、生成分析结论,并提炼可落地的洞察建议。",
+      "prompts": ["分析核心指标", "查找异常波动", "生成数据结论"]
+    },
+    {
+      "name": "报告生成",
+      "aliases": ["报告", "总结报告", "文档报告"],
+      "icon": "✦",
+      "title": "上传文档,说明报告重点",
+      "description": "请先在参数中上传 PDF 文档,再输入报告要求。我会提炼核心内容、梳理结构,并生成一份可下载的总结报告。",
+      "prompts": ["提炼核心观点", "生成总结报告", "按指定格式输出"]
+    },
+    {
+      "name": "智能PPT",
+      "aliases": ["PPT", "演示文稿", "幻灯片"],
+      "icon": "◆",
+      "title": "输入主题,生成演示结构",
+      "description": "请告诉我汇报主题、受众对象和页数要求。我会帮你规划目录、提炼页面要点,并生成适合演示的 PPT 内容框架。",
+      "prompts": ["规划PPT大纲", "生成页面文案", "优化汇报逻辑"]
+    },
+    {
+      "name": "邮件助手",
+      "aliases": ["邮件", "写邮件", "邮件撰写"],
+      "icon": "✉",
+      "title": "说明场景,快速生成邮件",
+      "description": "请提供收件对象、沟通目的和语气要求。我会帮你生成结构清晰、表达得体的邮件正文。",
+      "prompts": ["撰写商务邮件", "润色邮件语气", "生成回复模板"]
+    },
+    {
+      "name": "新闻助手",
+      "aliases": ["新闻", "资讯", "新闻写作"],
+      "icon": "◎",
+      "title": "输入素材,生成新闻内容",
+      "description": "请提供新闻主题、背景信息或采访素材。我会帮你梳理重点、组织叙事,并生成适合发布的新闻稿。",
+      "prompts": ["提炼新闻导语", "生成新闻稿", "优化标题表达"]
+    },
+    {
+      "name": "翻译专家",
+      "aliases": ["翻译", "多语言", "语言翻译"],
+      "icon": "文",
+      "title": "输入文本,指定翻译目标",
+      "description": "请粘贴需要翻译的内容,并说明目标语言、应用场景和语气风格。我会尽量保持语义准确与表达自然。",
+      "prompts": ["中英互译", "润色本地化表达", "保留专业术语"]
+    },
+    {
+      "name": "代码专家",
+      "aliases": ["代码", "编程", "开发助手"],
+      "icon": "⌘",
+      "title": "描述问题,获取代码方案",
+      "description": "请提供技术栈、目标功能、报错信息或代码片段。我会帮你分析原因、给出实现思路,并生成可参考的代码方案。",
+      "prompts": ["排查代码问题", "生成实现方案", "优化代码结构"]
+    }
+  ]
+}

+ 14 - 0
src/config/index.js

@@ -0,0 +1,14 @@
+// 浏览器直接连后端 5001。Vite proxy 路径已停用以减少中间层开销。
+// CORS 走后端配置(CONSOLE_CORS_ALLOW_ORIGINS / WEB_API_CORS_ALLOW_ORIGINS)放行。
+export const API_BASE_URL = 'http://192.168.100.26:5001'
+// export const API_BASE_URL = 'http://localhost:5001'
+export const API_PREFIX = `${API_BASE_URL}/console/api`
+export const PUBLIC_API_PREFIX = `${API_BASE_URL}/api`
+// Dify Service API 前缀(应用级 API,需用应用 API Key 通过 Bearer 鉴权)
+export const DIFY_V1_PREFIX = `${API_BASE_URL}/v1`
+
+export const ACCESS_TOKEN_LOCAL_STORAGE_NAME = 'access_token'
+
+export const emailRegex = /^[\w.!#$%&'*+\-/=?^{|}~]+@([\w-]+\.)+[\w-]{2,}$/
+
+export const validPassword = /^(?=.*[a-z])(?=.*\d)\S{8,}$/i

+ 38 - 0
src/contexts/AuthContext.jsx

@@ -0,0 +1,38 @@
+import { createContext, useContext, useState, useEffect } from 'react'
+import { isAuthenticated as checkAuth, clearLoggedIn } from '@/utils/auth'
+
+const AuthContext = createContext()
+
+export function AuthProvider({ children }) {
+  const [authenticated, setAuthenticated] = useState(() => {
+    return checkAuth()
+  })
+
+  useEffect(() => {
+    const handleStorageChange = () => {
+      setAuthenticated(checkAuth())
+    }
+    
+    window.addEventListener('storage', handleStorageChange)
+    return () => window.removeEventListener('storage', handleStorageChange)
+  }, [])
+
+  const login = () => {
+    setAuthenticated(true)
+  }
+
+  const logout = () => {
+    clearLoggedIn()
+    setAuthenticated(false)
+  }
+
+  return (
+    <AuthContext.Provider value={{ authenticated, login, logout }}>
+      {children}
+    </AuthContext.Provider>
+  )
+}
+
+export function useAuth() {
+  return useContext(AuthContext)
+}

+ 58 - 0
src/index.css

@@ -0,0 +1,58 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+  background: linear-gradient(135deg, #f0f4ff 0%, #e6f0ff 50%, #f8faff 100%);
+  color: #1a1f3a;
+  min-height: 100vh;
+  overflow-x: hidden;
+  position: relative;
+}
+
+body::before {
+  content: '';
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: 
+    radial-gradient(circle at 20% 80%, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
+    radial-gradient(circle at 80% 20%, rgba(118, 75, 162, 0.15) 0%, transparent 50%),
+    radial-gradient(circle at 40% 40%, rgba(0, 212, 255, 0.1) 0%, transparent 40%);
+  pointer-events: none;
+  z-index: 0;
+}
+
+#root {
+  min-height: 100vh;
+}
+
+* {
+  scrollbar-width: thin;
+  scrollbar-color: #c9cdd4 #f7f8fa;
+}
+
+::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+::-webkit-scrollbar-track {
+  background: #f7f8fa;
+  border-radius: 999px;
+}
+
+::-webkit-scrollbar-thumb {
+  background: #c9cdd4;
+  border: 2px solid #f7f8fa;
+  border-radius: 999px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: #a9aeb8;
+}

+ 11 - 0
src/main.jsx

@@ -0,0 +1,11 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import '@arco-design/web-react/dist/css/arco.css'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+)

+ 351 - 0
src/pages/AgentApply.jsx

@@ -0,0 +1,351 @@
+import { useState, useEffect, useRef, useCallback } from 'react'
+import { useLocation, useNavigate, useParams } from 'react-router-dom'
+import {
+  getAppBasicInfo,
+  getAccessStatus,
+  createAccessRequest
+} from '@/service/business'
+import emojiMartData from '@emoji-mart/data'
+
+// 解析 emoji icon
+const isNativeEmoji = (str) => /[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(str || '')
+const emojiNameToNative = (name) => {
+  if (!name) return '🤖'
+  const emoji = emojiMartData.emojis?.[name]
+  return emoji?.skins?.[0]?.native || '🤖'
+}
+const resolveIcon = (icon) => {
+  if (!icon) return '🤖'
+  if (isNativeEmoji(icon)) return icon
+  return emojiNameToNative(icon)
+}
+
+// 时间戳(秒)转 yyyy-MM-dd HH:mm
+const formatDateTime = (ts) => {
+  if (!ts && ts !== 0) return '-'
+  const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts)
+  if (Number.isNaN(d.getTime())) return '-'
+  const pad = (n) => String(n).padStart(2, '0')
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
+}
+
+// 状态 -> 文案 + 视觉类型
+const STATUS_META = {
+  not_requested: { label: '未申请', tone: 'neutral', help: '该智能体需要申请权限后才能使用' },
+  pending:        { label: '审核中', tone: 'pending', help: '申请已提交,正在等待管理员审核' },
+  approved:       { label: '已通过', tone: 'success', help: '申请已通过,可进入执行页' },
+  rejected:       { label: '已拒绝', tone: 'rejected', help: '申请被拒绝,可调整理由后重新申请' },
+  has_permission: { label: '已有权限', tone: 'success', help: '你已拥有该智能体的使用权限' },
+  no_permission:  { label: '无权限', tone: 'rejected', help: '该智能体为私有,暂不开放申请' }
+}
+
+// 申请权限枚举 -> 友好文案
+const PERMISSION_LABEL = {
+  public: '公开',
+  partial: '指定用户',
+  private: '私有'
+}
+
+function AgentApply() {
+  const { appId } = useParams()
+  const location = useLocation()
+  const navigate = useNavigate()
+  const passedAgent = location.state?.agent || { id: appId, name: '智能体' }
+
+  // ===== 数据状态 =====
+  const [appInfo, setAppInfo] = useState(null)            // basic-info 返回
+  const [status, setStatus] = useState(null)              // access-status.status
+  const [accessRequest, setAccessRequest] = useState(null) // access-status.request
+  const [hasPermission, setHasPermission] = useState(false)
+  const [progress, setProgress] = useState('')            // 进度描述
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState('')
+
+  // ===== 申请表单 =====
+  const [reason, setReason] = useState('')
+  const [submitting, setSubmitting] = useState(false)
+  const [submitError, setSubmitError] = useState('')
+
+  // ===== 轮询(仅 pending 时启用) =====
+  const pollTimerRef = useRef(null)
+  const stopPolling = useCallback(() => {
+    if (pollTimerRef.current) {
+      clearInterval(pollTimerRef.current)
+      pollTimerRef.current = null
+    }
+  }, [])
+
+  // 加载 basic-info + access-status
+  const loadAll = useCallback(async (showLoading = true) => {
+    if (!appId) return
+    if (showLoading) setLoading(true)
+    setError('')
+    try {
+      const [basicRes, statusRes] = await Promise.all([
+        getAppBasicInfo(appId),
+        getAccessStatus(appId)
+      ])
+
+      // basic-info: 后端可能直接返回对象,也可能 {data: {...}}
+      const basic = basicRes?.data || basicRes || {}
+      setAppInfo({
+        id: basic.id || appId,
+        name: basic.name || passedAgent.name || '智能体',
+        description: basic.description || '',
+        introduction: basic.introduction || '',
+        icon: resolveIcon(basic.icon),
+        iconBackground: basic.icon_background || '#FFEAD5',
+        mode: basic.mode || '',
+        version: basic.version || '',
+        versionDescription: basic.version_description || '',
+        listedAt: basic.listed_at,
+        createdAt: basic.created_at,
+        updatedAt: basic.updated_at,
+        appPermission: basic.app_permission || '',
+        isListed: Boolean(basic.is_listed)
+      })
+
+      // access-status
+      const st = statusRes?.data || statusRes || {}
+      setStatus(st.status || '')
+      setProgress(st.progress || st.status || '')
+      setHasPermission(Boolean(st.has_permission))
+      setAccessRequest(st.request || null)
+    } catch (e) {
+      setError(e?.message || '加载申请信息失败')
+    } finally {
+      if (showLoading) setLoading(false)
+    }
+  }, [appId, passedAgent.name])
+
+  // 首次加载
+  useEffect(() => {
+    loadAll(true)
+    return () => stopPolling()
+  }, [loadAll, stopPolling])
+
+  // pending 时启动轮询
+  useEffect(() => {
+    stopPolling()
+    if (status === 'pending') {
+      pollTimerRef.current = setInterval(() => {
+        // 静默刷新,不触发 loading
+        loadAll(false)
+      }, 8000)
+    }
+    return stopPolling
+  }, [status, loadAll, stopPolling])
+
+  // ===== 提交申请 =====
+  const handleSubmit = async () => {
+    if (!reason.trim()) {
+      setSubmitError('请填写使用理由')
+      return
+    }
+    setSubmitting(true)
+    setSubmitError('')
+    try {
+      const res = await createAccessRequest(appId, reason.trim())
+      const payload = res?.data || res || {}
+      // 重新拉一次最新状态
+      await loadAll(false)
+      if (payload.status === 'already_has_permission') {
+        setStatus('has_permission')
+        setHasPermission(true)
+      } else {
+        setStatus('pending')
+        setProgress('pending')
+      }
+      if (payload.request) {
+        setAccessRequest(payload.request)
+      }
+    } catch (e) {
+      setSubmitError(e?.message || '提交申请失败')
+    } finally {
+      setSubmitting(false)
+    }
+  }
+
+  // ===== 重新申请(被拒绝后) =====
+  const handleReapply = () => {
+    setStatus('not_requested')
+    setAccessRequest(null)
+    setReason('')
+    setSubmitError('')
+  }
+
+  // 跳到执行页
+  const goExecute = () => {
+    navigate(`/agents/execute/${appId}`, {
+      state: { agent: appInfo ? { ...passedAgent, ...appInfo } : passedAgent }
+    })
+  }
+
+  // ===== 渲染辅助 =====
+  const meta = STATUS_META[status] || { label: status || '未知', tone: 'neutral', help: '' }
+  const showApplyForm = status === 'not_requested'
+  const showPending   = status === 'pending'
+  const showRejected  = status === 'rejected'
+  const showApproved  = status === 'approved' || status === 'has_permission'
+  const showNoPerm    = status === 'no_permission'
+
+  return (
+    <div className="agent-apply">
+      {/* 页头 */}
+      <div className="page-header">
+        <button className="back-btn" onClick={() => navigate(-1)}>‹ 返回广场</button>
+      </div>
+
+      {/* 智能体详情卡(含版本) */}
+      <div className="apply-card apply-detail-card">
+        <div className="apply-agent-info">
+          <div className="apply-agent-icon" style={{ background: appInfo?.iconBackground || passedAgent.iconBackground || '#FFEAD5' }}>
+            {appInfo?.icon || passedAgent.icon || '🤖'}
+          </div>
+          <div className="apply-agent-meta">
+            <div className="apply-agent-name">
+              {loading ? '加载中…' : (appInfo?.name || passedAgent.name || '智能体')}
+            </div>
+            <div className="apply-agent-sub">
+              {appInfo?.mode && <span className="apply-tag">{appInfo.mode}</span>}
+              {appInfo?.appPermission && (
+                <span className={`apply-tag apply-perm-${appInfo.appPermission}`}>
+                  {PERMISSION_LABEL[appInfo.appPermission] || appInfo.appPermission}
+                </span>
+              )}
+              {appInfo?.version && <span className="apply-tag apply-tag-version">v{appInfo.version}</span>}
+              {appInfo?.isListed && <span className="apply-tag apply-tag-listed">已上架</span>}
+            </div>
+            {(appInfo?.description || passedAgent.desc) && (
+              <div className="apply-agent-desc">{appInfo?.description || passedAgent.desc}</div>
+            )}
+          </div>
+        </div>
+
+        {/* 版本说明 */}
+        {appInfo?.versionDescription && (
+          <div className="apply-version-block">
+            <div className="apply-version-head">
+              <span className="apply-version-label">版本说明</span>
+              <span className="apply-version-time">
+                上架时间:{formatDateTime(appInfo.listedAt || appInfo.updatedAt)}
+              </span>
+            </div>
+            <div className="apply-version-desc">{appInfo.versionDescription}</div>
+          </div>
+        )}
+
+        {/* 介绍(长描述) */}
+        {appInfo?.introduction && (
+          <div className="apply-intro-block">
+            <div className="apply-intro-label">智能体介绍</div>
+            <div className="apply-intro-desc">{appInfo.introduction}</div>
+          </div>
+        )}
+
+        {error && <div className="apply-error-tip">⚠ {error}(已使用基础信息)</div>}
+      </div>
+
+      {/* 申请进度区 */}
+      <div className={`apply-card apply-status-card apply-tone-${meta.tone}`}>
+        <div className="apply-status-row">
+          <div className="apply-status-icon">
+            {meta.tone === 'success' && '✅'}
+            {meta.tone === 'pending' && '⏳'}
+            {meta.tone === 'rejected' && '❌'}
+            {meta.tone === 'neutral' && 'ℹ️'}
+          </div>
+          <div className="apply-status-meta">
+            <div className="apply-status-label">当前状态:{meta.label}</div>
+            <div className="apply-status-help">{meta.help}</div>
+          </div>
+          {showPending && <div className="apply-status-polling">轮询中…</div>}
+        </div>
+
+        {/* 申请详情(pending / approved / rejected 都展示) */}
+        {accessRequest && (showPending || showApproved || showRejected) && (
+          <div className="apply-request-block">
+            <div className="apply-request-row">
+              <span className="apply-request-key">申请编号</span>
+              <span className="apply-request-val apply-mono">{accessRequest.id}</span>
+            </div>
+            <div className="apply-request-row">
+              <span className="apply-request-key">提交时间</span>
+              <span className="apply-request-val">{formatDateTime(accessRequest.created_at)}</span>
+            </div>
+            {accessRequest.reason && (
+              <div className="apply-request-row">
+                <span className="apply-request-key">申请理由</span>
+                <span className="apply-request-val">{accessRequest.reason}</span>
+              </div>
+            )}
+            {accessRequest.review_message && (
+              <div className="apply-request-row">
+                <span className="apply-request-key">审核备注</span>
+                <span className="apply-request-val">{accessRequest.review_message}</span>
+              </div>
+            )}
+            {accessRequest.reviewer_name && (
+              <div className="apply-request-row">
+                <span className="apply-request-key">审核人</span>
+                <span className="apply-request-val">{accessRequest.reviewer_name} · {formatDateTime(accessRequest.reviewed_at)}</span>
+              </div>
+            )}
+          </div>
+        )}
+
+        {/* 表单 / 操作按钮 */}
+        <div className="apply-form">
+          {showApplyForm && (
+            <label className="apply-label">
+              <span>使用场景 / 申请理由 <em>*</em></span>
+              <textarea
+                className="apply-textarea"
+                rows={5}
+                value={reason}
+                onChange={e => { setReason(e.target.value); setSubmitError('') }}
+                placeholder="请描述你计划如何使用该智能体,以便审批人快速评估..."
+              />
+              {submitError && <div className="apply-field-error">{submitError}</div>}
+            </label>
+          )}
+
+          <div className="apply-actions">
+            {showApplyForm && (
+              <>
+                <button className="apply-secondary-btn" onClick={() => navigate(-1)}>取消</button>
+                <button
+                  className="apply-primary-btn"
+                  onClick={handleSubmit}
+                  disabled={!reason.trim() || submitting}
+                >
+                  {submitting ? '提交中...' : '提交申请'}
+                </button>
+              </>
+            )}
+            {showPending && (
+              <button className="apply-secondary-btn" onClick={() => navigate(-1)}>返回广场</button>
+            )}
+            {showApproved && (
+              <>
+                <button className="apply-secondary-btn" onClick={() => navigate('/plaza')}>返回广场</button>
+                <button className="apply-primary-btn" onClick={goExecute}>前往执行页</button>
+              </>
+            )}
+            {showRejected && (
+              <>
+                <button className="apply-secondary-btn" onClick={() => navigate('/plaza')}>返回广场</button>
+                <button className="apply-primary-btn" onClick={handleReapply}>重新申请</button>
+              </>
+            )}
+            {showNoPerm && (
+              <button className="apply-secondary-btn" onClick={() => navigate('/plaza')}>返回广场</button>
+            )}
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default AgentApply

+ 1781 - 0
src/pages/AgentExecute.jsx

@@ -0,0 +1,1781 @@
+import { useState, useEffect, useRef, useCallback } from 'react'
+import { useLocation, useParams } from 'react-router-dom'
+import { Modal } from '@arco-design/web-react'
+import { getDifyAppParameters, getAppFullInfo, runWorkflow, runChatMessage, consumeSSE, uploadFile } from '@/service/dify'
+import { getBusinessConversations, getBusinessConversationDetail, downloadMessageContent } from '@/service/business'
+import { API_BASE_URL } from '@/config'
+import agentEmptyStates from '@/config/agentEmptyStates.json'
+import emojiMartData from '@emoji-mart/data'
+import { marked } from 'marked'
+import wordIcon from '@/assets/icons/word.svg'
+import pdfIcon from '@/assets/icons/pdf.svg'
+
+// 通过 emoji-mart data 把 emoji 名称转换为 native Unicode 字符
+const emojiNameToNative = (name) => {
+  if (!name) return '🤖'
+  const emoji = emojiMartData.emojis?.[name]
+  return emoji?.skins?.[0]?.native || '🤖'
+}
+const isNativeEmoji = (str) => /[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(str || '')
+const resolveIcon = (icon) => {
+  if (!icon) return '🤖'
+  if (isNativeEmoji(icon)) return icon
+  return emojiNameToNative(icon)
+}
+
+const buildIconUrl = (icon) => {
+  if (!icon) return ''
+  if (/^https?:\/\//i.test(icon)) return icon
+  if (icon.startsWith('/')) return `${API_BASE_URL}${icon}`
+  return icon
+}
+
+marked.setOptions({
+  gfm: true,
+  breaks: true,
+})
+
+const sanitizeHtml = (html) => {
+  if (typeof window === 'undefined') return html
+  const template = document.createElement('template')
+  template.innerHTML = html
+  template.content.querySelectorAll('script, iframe, object, embed').forEach(node => node.remove())
+  template.content.querySelectorAll('*').forEach(node => {
+    Array.from(node.attributes).forEach(attr => {
+      const name = attr.name.toLowerCase()
+      const value = attr.value || ''
+      if (name.startsWith('on')) node.removeAttribute(attr.name)
+      if ((name === 'href' || name === 'src') && /^\s*javascript:/i.test(value)) node.removeAttribute(attr.name)
+    })
+    if (node.tagName.toLowerCase() === 'a' && node.getAttribute('href')) {
+      node.setAttribute('target', '_blank')
+      node.setAttribute('rel', 'noopener noreferrer')
+    }
+  })
+  return template.innerHTML
+}
+
+const renderMarkdown = (text) => {
+  if (!text) return ''
+  try {
+    return sanitizeHtml(marked.parse(text))
+  } catch (_) {
+    return String(text).replace(/[<>&]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]))
+  }
+}
+
+const STREAM_WAITING_TEXT = '内容生成中'
+const AUTO_SCROLL_THRESHOLD = 24
+
+const isScrollAtBottom = (node) => {
+  if (!node) return true
+  return node.scrollHeight - node.scrollTop - node.clientHeight <= AUTO_SCROLL_THRESHOLD
+}
+
+const scrollToBottom = (node) => {
+  if (!node) return
+  node.scrollTop = node.scrollHeight
+}
+
+const formatDownloadTimestamp = () => {
+  const d = new Date()
+  const pad = (n) => String(n).padStart(2, '0')
+  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
+}
+
+const sanitizeDownloadName = (name) => {
+  return String(name || '智能体').replace(/[\\/:*?"<>|]/g, '_').trim() || '智能体'
+}
+
+// 数字格式化(保留以备后续接口扩展时使用)
+// const formatNumber = (num) => {
+//   if (num === null || num === undefined) return '0'
+//   return Number(num).toLocaleString('en-US')
+// }
+
+// 解析时间戳(秒)→ HH:mm:ss
+const formatTime = (ts) => {
+  if (!ts) return ''
+  const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts)
+  if (Number.isNaN(d.getTime())) return ''
+  const pad = (n) => String(n).padStart(2, '0')
+  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+}
+
+// 友好模式名
+const MODE_LABEL = {
+  workflow: '工作流',
+  chat: '基础对话',
+  'advanced-chat': '对话流',
+  'agent-chat': 'Agent 对话',
+  agentic: '智能体编排',
+  completion: '文本生成'
+}
+
+const normalizeAgentName = (name) => String(name || '').toLowerCase().replace(/[\s_-]/g, '')
+
+const getAgentEmptyState = (agent) => {
+  const currentName = normalizeAgentName(agent?.name || agent?.title)
+  const matched = agentEmptyStates.agents.find(item => {
+    const names = [item.name, ...(item.aliases || [])]
+    return names.some(name => {
+      const normalized = normalizeAgentName(name)
+      return normalized && (currentName.includes(normalized) || normalized.includes(currentName))
+    })
+  })
+  return matched || agentEmptyStates.default
+}
+
+function AgentExecute() {
+  const { appId } = useParams()
+  const location = useLocation()
+  const passedAgent = location.state?.agent || { id: appId, name: '智能体' }
+  const apiKey = location.state?.apiKey || ''
+
+  // ===== 通用状态 =====
+  const [agent, setAgent] = useState(passedAgent)
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState('')
+  const [mode, setMode] = useState(passedAgent.mode || '')
+
+  // ===== workflow 模式专属 =====
+  const [userInputForm, setUserInputForm] = useState([])  // user_input_form 数组
+  const [formValues, setFormValues] = useState({})         // {variable: value}
+  const [paramsLoading, setParamsLoading] = useState(false)
+  const [paramsError, setParamsError] = useState('')
+  const [running, setRunning] = useState(false)
+  const [runError, setRunError] = useState('')
+  const [steps, setSteps] = useState([])                   // 时间线条目
+  const [runSummary, setRunSummary] = useState(null)       // workflow_finished 数据
+  const timelineRef = useRef(null)                          // 自动滚动到底部
+  const timelineAutoScrollRef = useRef(true)
+  const runAbortRef = useRef(null)
+
+  // ===== 聊天模式专属(必须放在 useCallback 之前,避免 TDZ 报错) =====
+  const [chatRunning, setChatRunning] = useState(false)
+  const [chatConversationId, setChatConversationId] = useState('')
+  const [chatStreamId, setChatStreamId] = useState(null)   // 当前正在流式输出的 assistant 消息 id
+  const [chatRunError, setChatRunError] = useState('')
+  const [showChatParamsModal, setShowChatParamsModal] = useState(false)
+  const chatAbortRef = useRef(null)
+  // 用 ref 镜像 conversation_id,避免 useCallback 因依赖该 state 而频繁重建
+  const chatConversationIdRef = useRef('')
+  const chatStreamIdRef = useRef(null)
+  const activeChatSnapshotRef = useRef(null)
+  const viewingHistoryRef = useRef(false)
+  const setChatStreamIdSafe = (val) => {
+    chatStreamIdRef.current = val
+    setChatStreamId(val)
+  }
+  const [hasActiveChatSnapshot, setHasActiveChatSnapshot] = useState(false)
+  const [historyOpen, setHistoryOpen] = useState(false)
+  const [historyList, setHistoryList] = useState([])
+  const [historyLoading, setHistoryLoading] = useState(false)
+  const [historyError, setHistoryError] = useState('')
+  const [resumingConv, setResumingConv] = useState(null)
+  const [openDownloadId, setOpenDownloadId] = useState(null)
+  const messagesRef = useRef(null)
+  const messagesAutoScrollRef = useRef(true)
+  const emptyState = getAgentEmptyState(agent)
+
+  useEffect(() => {
+    runAbortRef.current?.abort()
+    chatAbortRef.current?.abort()
+    setAgent(passedAgent)
+    setMode(passedAgent.mode || '')
+    setUserInputForm([])
+    setFormValues({})
+    setParamsError('')
+    setRunError('')
+    setSteps([])
+    setRunSummary(null)
+    setMessages([])
+    setInput('')
+    setChatRunError('')
+    setChatConversationId('')
+    chatConversationIdRef.current = ''
+    setChatStreamIdSafe(null)
+    activeChatSnapshotRef.current = null
+    viewingHistoryRef.current = false
+    setHasActiveChatSnapshot(false)
+    setHistoryOpen(false)
+    setHistoryList([])
+    setHistoryError('')
+    setOpenDownloadId(null)
+  }, [appId])
+
+  // ===== 加载详情 + workflow 参数(走 Dify Service API)=====
+  useEffect(() => {
+    if (!appId) return
+    let cancelled = false
+    const controller = new AbortController()
+    const fetchAll = async () => {
+      setLoading(true)
+      setError('')
+
+      // 没有 apiKey 就无法走 Service API,直接报错
+      if (!apiKey) {
+        setError('缺少 API Key,请从广场重新进入该应用(确保有访问权限)')
+        setLoading(false)
+        return
+      }
+
+      try {
+        // 用 Bearer apiKey 调 /v1/info + /v1/site,**不再调 /console/api/apps/<id>**
+        const raw = await getAppFullInfo(apiKey, { signal: controller.signal })
+        if (cancelled) return
+        const m = raw.mode || passedAgent.mode || ''
+
+        // 处理 icon:
+        //   icon_type === 'image' 用 icon_url;否则用 emoji(resolveIcon 兼容 native + mart name)
+        let iconValue
+        if (raw.icon_type === 'image' && raw.icon_url) {
+          iconValue = { type: 'image', url: buildIconUrl(raw.icon_url) }
+        } else {
+          iconValue = resolveIcon(raw.icon)
+        }
+
+        setAgent({
+          id: appId,
+          name: raw.name || passedAgent.name || '智能体',
+          desc: raw.description || '',
+          icon: iconValue,
+          iconType: raw.icon_type || 'emoji',
+          iconBackground: raw.icon_background || '#FFEAD5',
+          mode: m,
+          tags: raw.tags || [],
+          author: raw.author_name || '',
+          title: raw.title || raw.name || '',
+          // 站点级配置(供后续使用)
+          showWorkflowSteps: raw.show_workflow_steps,
+          chatColorTheme: raw.chat_color_theme,
+          copyright: raw.copyright,
+          customDisclaimer: raw.custom_disclaimer,
+          defaultLanguage: raw.default_language,
+        })
+        setMode(m)
+
+        // workflow / chat 模式:拉表单参数
+        //   workflow 走 /workflows/run,参数在 inputs
+        //   advanced-chat / chat / agent-chat / agentic 走 /chat-messages,参数在 inputs
+        const isWorkflowMode = m === 'workflow'
+        const isChatMode = m === 'advanced-chat' || m === 'chat' || m === 'agent-chat' || m === 'agentic'
+        if ((isWorkflowMode || isChatMode) && apiKey) {
+          setParamsLoading(true)
+          setParamsError('')
+          try {
+            const p = await getDifyAppParameters(apiKey, { signal: controller.signal })
+            if (cancelled) return
+            const form = p?.user_input_form || p?.data?.user_input_form || []
+            setUserInputForm(form)
+            // 初始化默认值
+            const init = {}
+            for (const item of form) {
+              const type = Object.keys(item)[0]
+              const cfg = item[type]
+              if (cfg?.variable) init[cfg.variable] = cfg.default ?? ''
+            }
+            setFormValues(init)
+          } catch (e) {
+            if (!cancelled && e?.name !== 'AbortError') setParamsError(e?.message || '加载表单参数失败')
+          } finally {
+            if (!cancelled) setParamsLoading(false)
+          }
+        }
+      } catch (e) {
+        if (!cancelled && e?.name !== 'AbortError') setError(e?.message || '获取智能体详情失败')
+      } finally {
+        if (!cancelled) setLoading(false)
+      }
+    }
+    fetchAll()
+    return () => {
+      cancelled = true
+      controller.abort()
+    }
+  }, [appId, apiKey])
+
+  useEffect(() => {
+    return () => {
+      runAbortRef.current?.abort()
+      chatAbortRef.current?.abort()
+    }
+  }, [])
+
+  useEffect(() => {
+    if (timelineAutoScrollRef.current) scrollToBottom(timelineRef.current)
+  }, [steps, running])
+
+  const handleTimelineScroll = useCallback((e) => {
+    timelineAutoScrollRef.current = isScrollAtBottom(e.currentTarget)
+  }, [])
+
+  const handleMessagesScroll = useCallback((e) => {
+    messagesAutoScrollRef.current = isScrollAtBottom(e.currentTarget)
+  }, [])
+
+  // ===== 渲染动态表单字段 =====
+  // 文件大小格式化
+  const formatFileSize = (bytes) => {
+    if (!bytes && bytes !== 0) return ''
+    if (bytes < 1024) return bytes + ' B'
+    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
+    if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB'
+    return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'
+  }
+
+  // 文件 accept 属性构造
+  //   - 优先用 allowed_file_extensions(真正的文件后缀)
+  //   - 没有 extensions 但有 allowed_file_types(文件类型分类 image/document/...),按类型映射到 MIME 通配
+  //   - 都没有则不限制
+  const FILE_TYPE_MIME_MAP = {
+    image: 'image/*',
+    audio: 'audio/*',
+    video: 'video/*',
+    document: 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv',
+    custom: '*/*',
+  }
+  const buildFileAccept = (cfg) => {
+    if (cfg.allowed_file_extensions?.length > 0) {
+      return cfg.allowed_file_extensions
+        .map(ext => ext.startsWith('.') ? ext : '.' + ext)
+        .join(',')
+    }
+    if (cfg.allowed_file_types?.length > 0) {
+      return cfg.allowed_file_types
+        .map(t => FILE_TYPE_MIME_MAP[t] || '*/*')
+        .join(',')
+    }
+    return undefined
+  }
+
+  const isBrowserFile = (value) => {
+    return !!value
+      && typeof value === 'object'
+      && typeof value.name === 'string'
+      && typeof value.size === 'number'
+      && typeof value.arrayBuffer === 'function'
+  }
+
+  const extractFilesFromValue = (value) => {
+    if (isBrowserFile(value)) return [value]
+    if (Array.isArray(value)) return value.flatMap(extractFilesFromValue)
+    if (value && typeof value === 'object') {
+      if (typeof FileList !== 'undefined' && value instanceof FileList) return Array.from(value)
+      if (isBrowserFile(value.file)) return [value.file]
+      if (isBrowserFile(value.originFileObj)) return [value.originFileObj]
+      if (isBrowserFile(value.raw)) return [value.raw]
+    }
+    return []
+  }
+
+  const isRequiredValueEmpty = (value, type) => {
+    if (type === 'checkbox') return value !== true
+    if (type === 'file' || type === 'file-list') return extractFilesFromValue(value).length === 0
+    if (Array.isArray(value)) return value.length === 0
+    if (value === 0) return false
+    return value === '' || value === undefined || value === null || String(value).trim() === ''
+  }
+
+  const getMissingRequiredFields = () => {
+    return userInputForm
+      .map((item, index) => {
+        const type = Object.keys(item)[0]
+        const cfg = item[type] || {}
+        const varName = cfg.variable || `field_${index}`
+        if (!cfg.required) return null
+        const value = formValues[varName] ?? cfg.default ?? ''
+        if (!isRequiredValueEmpty(value, type)) return null
+        return cfg.label || varName
+      })
+      .filter(Boolean)
+  }
+
+  const formatRequiredFieldsError = (fields) => {
+    return `请先完善必填参数:${fields.join('、')}`
+  }
+
+  const isPdfFile = (file) => {
+    const name = file?.name || ''
+    return file?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf')
+  }
+
+  const getInputFileType = (file) => {
+    if (file?.type?.startsWith('image/')) return 'image'
+    if (file?.type?.startsWith('audio/')) return 'audio'
+    if (file?.type?.startsWith('video/')) return 'video'
+    return 'document'
+  }
+
+  const formatUploadError = (file, error) => {
+    const status = error?.status || error?.code
+    const message = error?.message || 'Upload failed'
+    return `${file?.name || 'file'}: ${status ? `[${status}] ` : ''}${message}`
+  }
+
+  const buildInputsWithUploadedPdfs = async (inputs, signal) => {
+    const nextInputs = { ...inputs }
+    const errors = []
+
+    for (const [key, value] of Object.entries(inputs || {})) {
+      const files = extractFilesFromValue(value)
+      if (files.length === 0) continue
+
+      const uploadedFiles = []
+      for (const file of files) {
+        if (!isPdfFile(file)) {
+          uploadedFiles.push(file)
+          continue
+        }
+        try {
+          const uploaded = await uploadFile(apiKey, file, undefined, { signal })
+          uploadedFiles.push({
+            type: getInputFileType(file),
+            transfer_method: 'local_file',
+            upload_file_id: uploaded.id,
+          })
+        } catch (error) {
+          if (error?.code === 'REQUEST_ABORTED' || error?.name === 'AbortError') throw error
+          errors.push(formatUploadError(file, error))
+        }
+      }
+
+      nextInputs[key] = Array.isArray(value) ? uploadedFiles : (uploadedFiles[0] || null)
+    }
+
+    if (errors.length > 0) {
+      const err = new Error(errors.join('\n'))
+      err.uploadErrors = errors
+      throw err
+    }
+
+    return nextInputs
+  }
+
+  // 统一解析 select 的 options(兼容 ["a","b"] 和 [{value,label}] 两种格式)
+  const normalizeSelectOptions = (options) => {
+    if (!Array.isArray(options)) return []
+    return options.map((o, i) => {
+      if (o == null) return { value: '', label: '' }
+      if (typeof o === 'string') return { value: o, label: o }
+      if (typeof o === 'object') {
+        return { value: o.value ?? o.label ?? '', label: o.label ?? o.value ?? '' }
+      }
+      return { value: String(o), label: String(o) }
+    })
+  }
+
+  // 表单字段类型 → 中文徽章文案
+  const TYPE_LABEL = {
+    'text-input': '文本',
+    paragraph: '段落',
+    select: '选择',
+    number: '数字',
+    file: '文件',
+    'file-list': '文件',
+    checkbox: '布尔',
+  }
+
+  // 字段右侧的徽章组:类型 + 是否必填
+  const FieldMeta = ({ type, required }) => (
+    <span className="wf-form-meta">
+      <span className="wf-form-type-badge">{TYPE_LABEL[type] || type}</span>
+      {required && <span className="wf-form-required-badge">必填</span>}
+    </span>
+  )
+
+  // 文件上传子组件
+  const FileField = ({ cfg, multiple, value, onChange, varName }) => {
+    const fileList = Array.isArray(value) ? value : (value ? [value] : [])
+    const fileInputRef = useRef(null)
+
+    const handleFileChange = (e) => {
+      const files = Array.from(e.target.files || [])
+      e.target.value = '' // 允许选择同名文件
+      if (multiple) {
+        onChange([...fileList, ...files])
+      } else {
+        onChange(files[0] || null)
+      }
+    }
+
+    const removeFile = (idx) => {
+      if (multiple) {
+        onChange(fileList.filter((_, i) => i !== idx))
+      } else {
+        onChange(null)
+      }
+    }
+
+    const accept = buildFileAccept(cfg)
+
+    return (
+      <div className="wf-form-field">
+        <span className="wf-form-label">
+          {cfg.label || varName}
+          {cfg.required && <em className="wf-form-required">*</em>}
+          <span className="wf-form-tip">
+            {multiple ? '(可上传多个文件)' : '(单文件)'}
+            {cfg.allowed_file_extensions?.length > 0 && ` 支持: ${cfg.allowed_file_extensions.join(', ')}`}
+            {!cfg.allowed_file_extensions?.length && cfg.allowed_file_types?.length > 0 && ` 支持: ${cfg.allowed_file_types.join(', ')}`}
+          </span>
+          <FieldMeta type={multiple ? 'file-list' : 'file'} required={cfg.required} />
+        </span>
+        <input
+          ref={fileInputRef}
+          type="file"
+          className="wf-form-file-hidden"
+          multiple={multiple}
+          accept={accept}
+          onChange={handleFileChange}
+        />
+        <button
+          type="button"
+          className="wf-form-file-trigger"
+          onClick={() => fileInputRef.current?.click()}
+        >
+          <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
+            <polyline points="17 8 12 3 7 8" />
+            <line x1="12" y1="3" x2="12" y2="15" />
+          </svg>
+          <span>点击选择文件</span>
+        </button>
+        {fileList.length > 0 && (
+          <ul className="wf-form-file-list">
+            {fileList.map((f, i) => (
+              <li key={i} className="wf-form-file-item">
+                <span className="wf-form-file-icon">
+                  {f.type?.startsWith('image/') ? '🖼️' : f.type?.includes('pdf') ? '📄' : '📎'}
+                </span>
+                <span className="wf-form-file-name">{f.name}</span>
+                <span className="wf-form-file-size">{formatFileSize(f.size)}</span>
+                <button
+                  type="button"
+                  className="wf-form-file-remove"
+                  onClick={() => removeFile(i)}
+                  title="移除"
+                >×</button>
+              </li>
+            ))}
+          </ul>
+        )}
+      </div>
+    )
+  }
+
+  const renderFormField = (item, key) => {
+    const type = Object.keys(item)[0]
+    const cfg = item[type] || {}
+    const varName = cfg.variable || `field_${key}`
+    const value = formValues[varName] ?? cfg.default ?? ''
+    const setVal = (v) => setFormValues(prev => ({ ...prev, [varName]: v }))
+
+    if (type === 'text-input') {
+      return (
+        <label key={key} className="wf-form-field">
+          <span className="wf-form-label">
+            {cfg.label || varName}
+            {cfg.required && <em className="wf-form-required">*</em>}
+            {cfg.max_length && <span className="wf-form-tip">(最多 {cfg.max_length} 字)</span>}
+            <FieldMeta type={type} required={cfg.required} />
+          </span>
+          <input
+            type="text"
+            className="wf-form-input"
+            value={value}
+            maxLength={cfg.max_length}
+            placeholder={cfg.placeholder || `请输入${cfg.label || varName}`}
+            onChange={e => setVal(e.target.value)}
+          />
+        </label>
+      )
+    }
+    if (type === 'paragraph') {
+      return (
+        <label key={key} className="wf-form-field">
+          <span className="wf-form-label">
+            {cfg.label || varName}
+            {cfg.required && <em className="wf-form-required">*</em>}
+            {cfg.max_length && <span className="wf-form-tip">(最多 {cfg.max_length} 字)</span>}
+            <FieldMeta type={type} required={cfg.required} />
+          </span>
+          <textarea
+            className="wf-form-textarea"
+            rows={5}
+            value={value}
+            maxLength={cfg.max_length}
+            placeholder={cfg.placeholder || `请输入${cfg.label || varName}`}
+            onChange={e => setVal(e.target.value)}
+          />
+          {cfg.max_length && (
+            <div className="wf-form-textarea-counter">
+              {String(value || '').length} / {cfg.max_length}
+            </div>
+          )}
+        </label>
+      )
+    }
+    if (type === 'select') {
+      return (
+        <label key={key} className="wf-form-field">
+          <span className="wf-form-label">
+            {cfg.label || varName}
+            {cfg.required && <em className="wf-form-required">*</em>}
+            <FieldMeta type={type} required={cfg.required} />
+          </span>
+          <div className="wf-form-select-wrapper">
+            <select
+              className="wf-form-select"
+              value={value}
+              onChange={e => setVal(e.target.value)}
+            >
+              <option value="" disabled>请选择</option>
+              {normalizeSelectOptions(cfg.options).map((o, i) => (
+                <option key={i} value={o.value}>{o.label}</option>
+              ))}
+            </select>
+            <svg className="wf-form-select-arrow" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
+              <polyline points="6 9 12 15 18 9" />
+            </svg>
+          </div>
+        </label>
+      )
+    }
+    if (type === 'number') {
+      return (
+        <label key={key} className="wf-form-field">
+          <span className="wf-form-label">
+            {cfg.label || varName}
+            {cfg.required && <em className="wf-form-required">*</em>}
+            <FieldMeta type={type} required={cfg.required} />
+          </span>
+          <input
+            type="number"
+            className="wf-form-input"
+            value={value}
+            min={cfg.min}
+            max={cfg.max}
+            step={cfg.step}
+            placeholder={cfg.placeholder || ''}
+            onChange={e => setVal(e.target.value)}
+          />
+        </label>
+      )
+    }
+    if (type === 'file' || type === 'file-list') {
+      return (
+        <FileField
+          key={key}
+          cfg={cfg}
+          varName={varName}
+          multiple={type === 'file-list'}
+          value={value}
+          onChange={setVal}
+        />
+      )
+    }
+    if (type === 'checkbox') {
+      return (
+        <label key={key} className="wf-form-checkbox-row">
+          <input
+            type="checkbox"
+            className="wf-form-checkbox"
+            checked={!!value}
+            onChange={e => setVal(e.target.checked)}
+          />
+          <span className="wf-form-checkbox-label">
+            {cfg.label || varName}
+            {cfg.required && <em className="wf-form-required">*</em>}
+          </span>
+          <FieldMeta type={type} required={cfg.required} />
+        </label>
+      )
+    }
+    // 未知类型,文本输入兜底
+    return (
+      <label key={key} className="wf-form-field">
+        <span className="wf-form-label">{cfg.label || varName} <small style={{ color: '#94a3b8' }}>({type})</small><FieldMeta type={type} required={cfg.required} /></span>
+        <input
+          type="text"
+          className="wf-form-input"
+          value={value}
+          onChange={e => setVal(e.target.value)}
+        />
+      </label>
+    )
+  }
+
+  // ===== 处理 SSE 事件 =====
+  const handleSSEEvent = useCallback((evt) => {
+    const { event, data } = evt
+    const ts = Date.now()
+
+    if (event === 'workflow_started') {
+      setSteps(prev => [
+        ...prev,
+        {
+          id: 'start-' + ts,
+          type: 'workflow_started',
+          title: '工作流启动',
+          time: ts,
+          inputs: data?.inputs,
+          workflowId: data?.workflow_id,
+        }
+      ])
+    } else if (event === 'node_started') {
+      setSteps(prev => [
+        ...prev,
+        {
+          id: data?.id || `node-${ts}`,
+          type: 'node_started',
+          nodeId: data?.node_id,
+          nodeType: data?.node_type,
+          title: data?.title || data?.node_type,
+          index: data?.index,
+          time: ts,
+          status: 'running',
+        }
+      ])
+    } else if (event === 'text_chunk') {
+      // 累积到独立的 final-text 条目
+      setSteps(prev => {
+        const idx = prev.findIndex(s => s.type === 'text_chunk')
+        if (idx >= 0) {
+          const next = [...prev]
+          next[idx] = { ...next[idx], text: (next[idx].text || '') + (data?.text || '') }
+          return next
+        }
+        return [
+          ...prev,
+          {
+            id: 'final-text',
+            type: 'text_chunk',
+            title: '输出文本',
+            time: ts,
+            text: data?.text || '',
+            status: 'running',
+          }
+        ]
+      })
+    } else if (event === 'node_finished') {
+      setSteps(prev => prev.map(s =>
+        s.id === data?.id
+          ? { ...s, status: data?.status || 'succeeded', outputs: data?.outputs, elapsedTime: data?.elapsed_time, error: data?.error, finished: true }
+          : s
+      ))
+    } else if (event === 'workflow_finished') {
+      setRunSummary({
+        status: data?.status,
+        outputs: data?.outputs,
+        totalSteps: data?.total_steps,
+        elapsedTime: data?.elapsed_time,
+        totalTokens: data?.total_tokens,
+        createdAt: data?.created_at,
+        finishedAt: data?.finished_at,
+      })
+      setSteps(prev => [
+        ...prev,
+        {
+          id: 'finish-' + ts,
+          type: 'workflow_finished',
+          title: '工作流完成',
+          time: ts,
+          status: data?.status,
+          totalSteps: data?.total_steps,
+          elapsedTime: data?.elapsed_time,
+          totalTokens: data?.total_tokens,
+        }
+      ])
+    } else if (event === 'workflow_paused') {
+      setSteps(prev => [
+        ...prev,
+        { id: 'paused-' + ts, type: 'workflow_paused', title: '工作流暂停', time: ts, data }
+      ])
+    } else if (event === 'human_input_required') {
+      setSteps(prev => [
+        ...prev,
+        {
+          id: 'human-input-' + ts,
+          type: 'human_input',
+          title: '需要人工介入',
+          time: ts,
+          formId: data?.form_id,
+          nodeTitle: data?.node_title,
+          formContent: data?.form_content,
+        }
+      ])
+    } else if (event === 'error') {
+      setSteps(prev => [
+        ...prev,
+        {
+          id: 'err-' + ts,
+          type: 'error',
+          title: '执行错误',
+          time: ts,
+          message: data?.message || evt?.message || '未知错误',
+          data
+        }
+      ])
+    }
+    // 其它事件忽略
+  }, [])
+
+  // ===== 处理 chat-messages SSE 事件 =====
+  //   关键事件(来自文档):
+  //   - message            每个回答片段(按顺序拼接)→ 累积到当前 assistant 消息的 answer
+  //   - message_end        回答完成 → 标记流式结束
+  //   - error              错误 → 写入错误文本
+  //   - workflow_started / node_*  advanced-chat 模式还会有节点事件,这里仅记录不影响 UI
+  const handleChatSSEEvent = useCallback((evt) => {
+    const event = evt?.event
+    const data = evt?.data || {}
+    const answer = evt?.answer ?? data?.answer ?? ''
+    const evtConversationId = evt?.conversation_id ?? data?.conversation_id
+    const evtMetadata = evt?.metadata ?? data?.metadata
+    const evtMessageId = evt?.id ?? evt?.message_id ?? data?.message_id
+    const evtErrorMessage = evt?.message ?? data?.message
+    if (event === 'message') {
+      const streamId = chatStreamIdRef.current
+      if (streamId) {
+        const updateMessages = prev => prev.map(m =>
+          m.id === streamId
+            ? { ...m, content: m.pending ? answer : (m.content || '') + answer, pending: false }
+            : m
+        )
+        if (viewingHistoryRef.current && activeChatSnapshotRef.current) {
+          activeChatSnapshotRef.current = {
+            ...activeChatSnapshotRef.current,
+            messages: updateMessages(activeChatSnapshotRef.current.messages || []),
+          }
+        } else {
+          setMessages(updateMessages)
+        }
+      }
+      if (evtConversationId && !chatConversationIdRef.current) {
+        chatConversationIdRef.current = evtConversationId
+        setChatConversationId(evtConversationId)
+        if (activeChatSnapshotRef.current) {
+          activeChatSnapshotRef.current = {
+            ...activeChatSnapshotRef.current,
+            conversationId: evtConversationId,
+          }
+        }
+      }
+    } else if (event === 'message_end') {
+      const streamId = chatStreamIdRef.current
+      if (streamId) {
+        const updateMessages = prev => prev.map(m =>
+          m.id === streamId
+            ? { ...m, streaming: false, pending: false, metadata: evtMetadata, messageId: evtMessageId }
+            : m
+        )
+        if (viewingHistoryRef.current && activeChatSnapshotRef.current) {
+          activeChatSnapshotRef.current = {
+            ...activeChatSnapshotRef.current,
+            messages: updateMessages(activeChatSnapshotRef.current.messages || []),
+            streamId: null,
+          }
+        } else {
+          setMessages(updateMessages)
+        }
+      }
+      setChatStreamIdSafe(null)
+      if (evtConversationId) {
+        chatConversationIdRef.current = evtConversationId
+        setChatConversationId(evtConversationId)
+        if (activeChatSnapshotRef.current) {
+          activeChatSnapshotRef.current = {
+            ...activeChatSnapshotRef.current,
+            conversationId: evtConversationId,
+          }
+        }
+      }
+    } else if (event === 'error') {
+      setChatStreamId(prevId => {
+        if (prevId) {
+          const updateMessages = prev => prev.map(m =>
+            m.id === prevId
+              ? { ...m, streaming: false, content: (m.content || '') + '\n\n⚠ ' + (data?.message || '未知错误') }
+              : m
+          )
+          if (viewingHistoryRef.current && activeChatSnapshotRef.current) {
+            activeChatSnapshotRef.current = {
+              ...activeChatSnapshotRef.current,
+              messages: updateMessages(activeChatSnapshotRef.current.messages || []),
+              streamId: null,
+              error: data?.message || '对话请求失败',
+            }
+          } else {
+            setMessages(updateMessages)
+          }
+        }
+        return null
+      })
+      setChatRunError(data?.message || '对话请求失败')
+    } else if (event === 'message_replace') {
+      const streamId = chatStreamIdRef.current
+      if (streamId) {
+        const updateMessages = prev => prev.map(m =>
+          m.id === streamId
+            ? { ...m, content: data?.answer || '' }
+            : m
+        )
+        if (viewingHistoryRef.current && activeChatSnapshotRef.current) {
+          activeChatSnapshotRef.current = {
+            ...activeChatSnapshotRef.current,
+            messages: updateMessages(activeChatSnapshotRef.current.messages || []),
+          }
+        } else {
+          setMessages(updateMessages)
+        }
+      }
+    }
+  }, [])
+
+  // ===== 执行工作流 =====
+  const handleRunWorkflow = async () => {
+    if (running) return
+    const missingFields = getMissingRequiredFields()
+    if (missingFields.length > 0) {
+      setRunError(formatRequiredFieldsError(missingFields))
+      return
+    }
+    runAbortRef.current?.abort()
+    const controller = new AbortController()
+    runAbortRef.current = controller
+    setRunError('')
+    setSteps([])
+    setRunSummary(null)
+    timelineAutoScrollRef.current = true
+    setRunning(true)
+    try {
+      const workflowInputs = await buildInputsWithUploadedPdfs(formValues, controller.signal)
+      const res = await runWorkflow(apiKey, workflowInputs, undefined, { signal: controller.signal })
+      await consumeSSE(
+        res,
+        handleSSEEvent,
+        () => { setRunning(false) },
+        (err) => { setRunError(err?.message || 'SSE 流读取失败'); setRunning(false) },
+        { signal: controller.signal }
+      )
+    } catch (e) {
+      if (e?.name !== 'AbortError') {
+        setRunError(e?.message || '执行失败')
+      }
+    } finally {
+      if (runAbortRef.current === controller) {
+        runAbortRef.current = null
+      }
+      setRunning(false)
+    }
+  }
+
+  // ===== 聊天模式(占位,保留旧 UI) =====
+  const [input, setInput] = useState('')
+  const [messages, setMessages] = useState([])
+  const isChat = mode === 'chat' || mode === 'advanced-chat' || mode === 'agent-chat' || mode === 'agentic' || mode === 'completion' || !mode
+
+  useEffect(() => {
+    if (messagesAutoScrollRef.current) scrollToBottom(messagesRef.current)
+  }, [messages, chatRunning])
+
+  // ===== API Key 弹框 =====
+  const [showApiKeyModal, setShowApiKeyModal] = useState(false)
+  const [copied, setCopied] = useState(false)
+
+  // ===== workflow 右侧运行面板折叠 =====
+  const [runPanelCollapsed, setRunPanelCollapsed] = useState(false)
+
+  const handleCopyApiKey = async () => {
+    if (!apiKey) return
+    try {
+      await navigator.clipboard.writeText(apiKey)
+      setCopied(true)
+      setTimeout(() => setCopied(false), 1500)
+    } catch (e) {
+      // 降级:execCommand
+      const ta = document.createElement('textarea')
+      ta.value = apiKey
+      document.body.appendChild(ta)
+      ta.select()
+      try { document.execCommand('copy'); setCopied(true); setTimeout(() => setCopied(false), 1500) } catch (_) {}
+      document.body.removeChild(ta)
+    }
+  }
+
+  const fetchConversations = useCallback(async () => {
+    setHistoryLoading(true)
+    setHistoryError('')
+    try {
+      const res = await getBusinessConversations()
+      const list = Array.isArray(res?.data) ? res.data : (Array.isArray(res?.data?.data) ? res.data.data : [])
+      setHistoryList(list.filter(conv => !conv.app_id || conv.app_id === appId))
+    } catch (e) {
+      setHistoryError(e?.message || '加载历史记录失败')
+      setHistoryList([])
+    } finally {
+      setHistoryLoading(false)
+    }
+  }, [appId])
+
+  const handleToggleHistory = useCallback(() => {
+    setHistoryOpen(prev => {
+      const next = !prev
+      if (next) fetchConversations()
+      return next
+    })
+  }, [fetchConversations])
+
+  const handleSelectHistory = useCallback(async (conv) => {
+    if (!conv?.id || resumingConv) return
+    if (chatRunning && !viewingHistoryRef.current) {
+      activeChatSnapshotRef.current = {
+        messages,
+        input,
+        conversationId: chatConversationIdRef.current || chatConversationId || '',
+        streamId: chatStreamIdRef.current,
+        error: chatRunError,
+      }
+      setHasActiveChatSnapshot(true)
+    }
+    setResumingConv(conv.id)
+    try {
+      const res = await getBusinessConversationDetail(conv.id)
+      const detail = res?.data ?? res ?? {}
+      const rows = Array.isArray(detail.messages) ? detail.messages : []
+      const restored = rows
+        .filter(m => m && (m.query || m.answer))
+        .sort((a, b) => (a.created_at || 0) - (b.created_at || 0))
+        .flatMap(m => [
+          ...(m.query ? [{ id: `history-u-${m.id}`, role: 'user', content: m.query, streaming: false }] : []),
+          ...(m.answer ? [{ id: `history-a-${m.id}`, role: 'assistant', content: m.answer, streaming: false, messageId: m.id }] : []),
+        ])
+      chatConversationIdRef.current = conv.id
+      setChatConversationId(conv.id)
+      messagesAutoScrollRef.current = true
+      viewingHistoryRef.current = true
+      setMessages(restored)
+      setChatStreamIdSafe(null)
+      setHistoryOpen(false)
+    } catch (e) {
+      setChatRunError(e?.message || '加载历史详情失败')
+    } finally {
+      setResumingConv(null)
+    }
+  }, [chatConversationId, chatRunError, chatRunning, input, messages, resumingConv])
+
+  const handleBackToActiveChat = useCallback(() => {
+    const snapshot = activeChatSnapshotRef.current
+    if (!snapshot) return
+    viewingHistoryRef.current = false
+    chatConversationIdRef.current = snapshot.conversationId || ''
+    setChatConversationId(snapshot.conversationId || '')
+    setChatStreamIdSafe(snapshot.streamId || chatStreamIdRef.current || null)
+    messagesAutoScrollRef.current = true
+    setMessages(snapshot.messages || [])
+    setInput(snapshot.input || '')
+    setChatRunError(snapshot.error || '')
+    setHistoryOpen(false)
+    setHasActiveChatSnapshot(false)
+  }, [])
+
+  const handleDownloadMessage = useCallback(async (m, format) => {
+    if (!m?.messageId || m.downloading) return
+    setMessages(prev => prev.map(x => x.id === m.id ? { ...x, downloading: format } : x))
+    try {
+      const { blob } = await downloadMessageContent(m.messageId, format)
+      const url = URL.createObjectURL(blob)
+      const a = document.createElement('a')
+      a.href = url
+      const ext = format === 'pdf' ? 'pdf' : 'docx'
+      a.download = `${sanitizeDownloadName(agent.name)}-${formatDownloadTimestamp()}.${ext}`
+      document.body.appendChild(a)
+      a.click()
+      document.body.removeChild(a)
+      setTimeout(() => URL.revokeObjectURL(url), 1000)
+    } catch (e) {
+      setChatRunError(e?.message || '下载失败')
+    } finally {
+      setMessages(prev => prev.map(x => x.id === m.id ? { ...x, downloading: null } : x))
+    }
+  }, [agent.name])
+
+  const handleNewChat = useCallback(() => {
+    chatAbortRef.current?.abort()
+    chatConversationIdRef.current = ''
+    setChatConversationId('')
+    setChatStreamIdSafe(null)
+    messagesAutoScrollRef.current = true
+    setMessages([])
+    setInput('')
+    setChatRunError('')
+    setOpenDownloadId(null)
+    setHistoryOpen(false)
+  }, [])
+
+  const handleNewChatClick = useCallback(() => {
+    if (!chatRunning) {
+      handleNewChat()
+      return
+    }
+
+    Modal.confirm({
+      title: '开启新会话?',
+      content: '当前正在生成回复,开启新会话会中断本次问答并清空当前内容。',
+      okText: '中断并开启',
+      cancelText: '继续等待',
+      okButtonProps: { status: 'danger' },
+      onOk: handleNewChat,
+    })
+  }, [chatRunning, handleNewChat])
+
+  const handleSend = async () => {
+    const text = input.trim()
+    if (!text || chatRunning) return
+    const missingFields = getMissingRequiredFields()
+    if (missingFields.length > 0) {
+      setChatRunError(formatRequiredFieldsError(missingFields))
+      setShowChatParamsModal(true)
+      return
+    }
+    if (!apiKey) {
+      setChatRunError('缺少 API Key')
+      return
+    }
+    chatAbortRef.current?.abort()
+    const controller = new AbortController()
+    chatAbortRef.current = controller
+    setChatRunError('')
+
+    // 1) 推入 user 消息
+    const userMsgId = 'u-' + Date.now()
+    const assistantMsgId = 'a-' + Date.now()
+    messagesAutoScrollRef.current = true
+    setMessages(prev => [
+      ...prev,
+      { id: userMsgId, role: 'user', content: text },
+      { id: assistantMsgId, role: 'assistant', content: STREAM_WAITING_TEXT, streaming: true, pending: true },
+    ])
+    setInput('')
+    setChatStreamIdSafe(assistantMsgId)
+    setChatRunning(true)
+
+    try {
+      // 2) 调 /chat-messages 接口(带 inputs + conversation_id 续传)
+      const res = await runChatMessage(
+        apiKey,
+        {
+          query: text,
+          inputs: await buildInputsWithUploadedPdfs(formValues, controller.signal),
+          conversation_id: chatConversationIdRef.current || chatConversationId || ''
+        },
+        undefined,
+        { signal: controller.signal }
+      )
+      // 3) 解析 SSE 流
+      await consumeSSE(
+        res,
+        handleChatSSEEvent,
+        () => { /* 流结束 */ },
+        (err) => {
+          setChatRunError(err?.message || 'SSE 流读取失败')
+          // 失败时给当前 assistant 消息加错误提示
+          setMessages(prev => prev.map(m =>
+            m.id === assistantMsgId && m.streaming
+              ? { ...m, streaming: false, content: (m.content || '') + '\n\n⚠ ' + (err?.message || 'SSE 流读取失败') }
+              : m
+          ))
+        },
+        { signal: controller.signal }
+      )
+    } catch (e) {
+      if (e?.name !== 'AbortError') {
+        setChatRunError(e?.message || '请求失败')
+        setMessages(prev => prev.map(m =>
+          m.id === assistantMsgId && m.streaming
+            ? { ...m, streaming: false, content: '⚠ ' + (e?.message || '请求失败') }
+            : m
+        ))
+      }
+    } finally {
+      if (chatAbortRef.current === controller) {
+        chatAbortRef.current = null
+      }
+      setChatRunning(false)
+      setChatStreamId(prev => {
+        // 如果还停留在 assistantMsgId(异常未触发 message_end),把它收尾
+        if (prev === assistantMsgId) {
+          setMessages(p => p.map(m => m.id === prev ? { ...m, streaming: false, pending: false } : m))
+          chatStreamIdRef.current = null
+          return null
+        }
+        return prev
+      })
+    }
+  }
+
+  // 停止当前对话流
+  const handleStopChat = () => {
+    chatAbortRef.current?.abort()
+  }
+
+  return (
+    <div className="agent-execute">
+      {/* 智能体详情卡片 */}
+      {!isChat && <div className="execute-detail-card">
+        <div className="execute-detail-head">
+          <div className="execute-detail-icon" style={{ background: agent.iconBackground || '#FFEAD5' }}>
+            {agent.icon && typeof agent.icon === 'object' && agent.icon.type === 'image' ? (
+              <img src={buildIconUrl(agent.icon.url)} alt={agent.name} className="execute-detail-icon-img" />
+            ) : (
+              agent.icon || '🤖'
+            )}
+          </div>
+          <div className="execute-detail-meta">
+            <h1 className="execute-detail-name">
+              {loading ? '加载中…' : (agent.name || '智能体')}
+            </h1>
+            <div className="execute-detail-sub">
+              {mode && <span className="execute-detail-mode-tag">{MODE_LABEL[mode] || mode}</span>}
+            </div>
+            {error && <div className="execute-detail-error">⚠ {error}</div>}
+          </div>
+        </div>
+
+        {agent.desc && <div className="execute-detail-desc">{agent.desc}</div>}
+
+        {(agent.tags?.length > 0 || agent.author) && (
+          <div className="execute-detail-info">
+            {agent.tags?.length > 0 && (
+              <div className="execute-detail-tags">
+                {agent.tags.map((t, i) => (<span key={i} className="execute-detail-chip">{t}</span>))}
+              </div>
+            )}
+            <div className="execute-detail-stats">
+              {agent.author && <span>👤 {agent.author}</span>}
+              {agent.copyright && <span>© {agent.copyright}</span>}
+            </div>
+          </div>
+        )}
+
+        {agent.customDisclaimer && (
+          <div className="execute-detail-disclaimer">📌 {agent.customDisclaimer}</div>
+        )}
+      </div>}
+
+      {/* ============== workflow 模式 ============== */}
+      {mode === 'workflow' && (
+        <div className="workflow-layout">
+          {/* 左侧:执行输出(timeline) */}
+          <div className="workflow-main">
+            {(steps.length > 0 || running) && (
+              <div className="execute-card">
+                <div className="execute-card-head">
+                  <h3 className="execute-card-title">📜 执行过程</h3>
+                  {runSummary && (
+                    <span className={`wf-summary wf-summary-${runSummary.status}`}>
+                      {runSummary.status === 'succeeded' ? '✅' : '❌'} {runSummary.status}
+                      {runSummary.elapsedTime != null && ` · ${runSummary.elapsedTime.toFixed(2)}s`}
+                      {runSummary.totalSteps != null && ` · 步骤 ${runSummary.totalSteps}`}
+                      {runSummary.totalTokens != null && ` · Tokens ${runSummary.totalTokens}`}
+                    </span>
+                  )}
+                </div>
+
+              <div className="wf-timeline" ref={timelineRef} onScroll={handleTimelineScroll}>
+                {steps.map((s, i) => (
+                  <div key={s.id || i} className={`wf-step wf-step-${s.status || 'pending'} wf-step-type-${s.type}`}>
+                    {s.type === 'workflow_started' && (
+                      <>
+                        <div className="wf-step-marker wf-step-marker-start">▶</div>
+                        <div className="wf-step-body">
+                          <div className="wf-step-title">工作流启动</div>
+                          <div className="wf-step-meta">{formatTime(s.time)}</div>
+                          {s.inputs && Object.keys(s.inputs).length > 0 && (
+                            <details className="wf-step-details">
+                              <summary>输入参数</summary>
+                              <pre>{JSON.stringify(s.inputs, null, 2)}</pre>
+                            </details>
+                          )}
+                        </div>
+                      </>
+                    )}
+
+                    {s.type === 'node_started' && (
+                      <>
+                        <div className={`wf-step-marker wf-step-marker-${s.status}`}>
+                          {s.status === 'running' ? '◐' : '◉'}
+                        </div>
+                        <div className="wf-step-body">
+                          <div className="wf-step-title">
+                            {s.title || s.nodeType} {s.index != null && <span className="wf-step-tag">#{s.index}</span>}
+                            {s.status === 'running' && <span className="wf-step-pending">运行中</span>}
+                          </div>
+                          <div className="wf-step-meta">
+                            {s.nodeType && <span className="wf-step-type">类型: {s.nodeType}</span>}
+                            {formatTime(s.time) && <span> · {formatTime(s.time)}</span>}
+                          </div>
+                        </div>
+                      </>
+                    )}
+
+                    {s.type === 'node_finished' && (
+                      <>
+                        <div className={`wf-step-marker wf-step-marker-${s.status}`}>●</div>
+                        <div className="wf-step-body">
+                          <div className="wf-step-title">
+                            {s.title} · <span className={`wf-step-status wf-step-status-${s.status}`}>{s.status}</span>
+                            {s.elapsedTime != null && <span className="wf-step-tag">⏱ {s.elapsedTime.toFixed(2)}s</span>}
+                          </div>
+                          {s.error && <div className="wf-step-error">{typeof s.error === 'string' ? s.error : JSON.stringify(s.error)}</div>}
+                          {s.outputs && Object.keys(s.outputs).length > 0 && (
+                            <details className="wf-step-details">
+                              <summary>输出</summary>
+                              <pre>{JSON.stringify(s.outputs, null, 2)}</pre>
+                            </details>
+                          )}
+                        </div>
+                      </>
+                    )}
+
+                    {s.type === 'text_chunk' && (
+                      <>
+                        <div className="wf-step-marker wf-step-marker-output">✎</div>
+                        <div className="wf-step-body">
+                          <div className="wf-step-title">输出文本 {s.status === 'running' && <span className="wf-step-pending">生成中</span>}</div>
+                          <div className="wf-step-text">{s.text}<span className="wf-step-cursor">▍</span></div>
+                        </div>
+                      </>
+                    )}
+
+                    {s.type === 'workflow_finished' && (
+                      <>
+                        <div className={`wf-step-marker wf-step-marker-${s.status}`}>
+                          {s.status === 'succeeded' ? '✓' : '✕'}
+                        </div>
+                        <div className="wf-step-body">
+                          <div className="wf-step-title">
+                            工作流完成
+                            {s.totalSteps != null && <span className="wf-step-tag">步骤 {s.totalSteps}</span>}
+                            {s.elapsedTime != null && <span className="wf-step-tag">⏱ {s.elapsedTime.toFixed(2)}s</span>}
+                            {s.totalTokens != null && <span className="wf-step-tag">Tokens {s.totalTokens}</span>}
+                          </div>
+                        </div>
+                      </>
+                    )}
+
+                    {s.type === 'workflow_paused' && (
+                      <>
+                        <div className="wf-step-marker wf-step-marker-paused">⏸</div>
+                        <div className="wf-step-body">
+                          <div className="wf-step-title">工作流暂停(等待人工介入)</div>
+                        </div>
+                      </>
+                    )}
+
+                    {s.type === 'human_input' && (
+                      <>
+                        <div className="wf-step-marker wf-step-marker-human">👤</div>
+                        <div className="wf-step-body">
+                          <div className="wf-step-title">需要人工介入:{s.nodeTitle}</div>
+                          {s.formContent && <div className="wf-step-text">{s.formContent}</div>}
+                        </div>
+                      </>
+                    )}
+
+                    {s.type === 'error' && (
+                      <>
+                        <div className="wf-step-marker wf-step-marker-error">!</div>
+                        <div className="wf-step-body">
+                          <div className="wf-step-title">执行错误</div>
+                          <div className="wf-step-error">{s.message}</div>
+                        </div>
+                      </>
+                    )}
+                  </div>
+                ))}
+
+                {running && (
+                  <div className="wf-step wf-step-running">
+                    <div className="wf-step-marker wf-step-marker-running">⋯</div>
+                    <div className="wf-step-body">
+                      <div className="wf-step-title">等待事件…</div>
+                    </div>
+                  </div>
+                )}
+              </div>
+            </div>
+          )}
+          </div>
+
+          {/* 右侧:运行参数面板(可折叠) */}
+          <aside className={`workflow-side ${runPanelCollapsed ? 'is-collapsed' : ''}`}>
+            {/* 折叠态:仅显示展开按钮(不放运行按钮,必须点开展开才能运行) */}
+            {runPanelCollapsed && (
+              <div className="workflow-side-collapsed">
+                <button
+                  type="button"
+                  className="workflow-side-expand"
+                  onClick={() => setRunPanelCollapsed(false)}
+                  title="展开运行面板"
+                >
+                  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
+                    <polyline points="15 18 9 12 15 6" />
+                  </svg>
+                </button>
+                <div className="workflow-side-collapsed-title">运行</div>
+              </div>
+            )}
+
+            {/* 展开态:完整表单卡片 */}
+            {!runPanelCollapsed && (
+              <div className="execute-card workflow-side-card">
+                <div className="execute-card-head workflow-side-card-head">
+                  <h3 className="execute-card-title">
+                    {running ? '⚡ 运行中…' : '⚙️ 运行参数'}
+                  </h3>
+                  <button
+                    type="button"
+                    className="workflow-side-close"
+                    onClick={() => setRunPanelCollapsed(true)}
+                    title="收起运行面板"
+                    aria-label="收起运行面板"
+                  >
+                    <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
+                      <line x1="18" y1="6" x2="6" y2="18" />
+                      <line x1="6" y1="6" x2="18" y2="18" />
+                    </svg>
+                  </button>
+                </div>
+
+                {paramsLoading && <div className="wf-form-loading">表单参数加载中…</div>}
+                {paramsError && <div className="execute-detail-error">⚠ {paramsError}</div>}
+
+                {!paramsLoading && userInputForm.length > 0 && (
+                  <div className="wf-form">
+                    {userInputForm.map((item, i) => renderFormField(item, i))}
+                  </div>
+                )}
+                {!paramsLoading && !paramsError && userInputForm.length === 0 && (
+                  <div className="wf-form-empty">该工作流无输入参数,点击下方按钮直接执行</div>
+                )}
+
+                <div className="wf-form-actions">
+                  <button
+                    className="execute-send-btn"
+                    onClick={handleRunWorkflow}
+                    disabled={running}
+                  >
+                    {running ? '执行中…' : '🚀 开始执行'}
+                  </button>
+                  {runError && <span className="execute-detail-error">⚠ {runError}</span>}
+                </div>
+              </div>
+            )}
+          </aside>
+        </div>
+      )}
+
+      {/* ============== 聊天模式(chat/advanced-chat/agent-chat/agentic) ============== */}
+      {isChat && (
+        <div className="execute-chat-shell">
+          <div className="execute-chat-orb execute-chat-orb-a" />
+          <div className="execute-chat-orb execute-chat-orb-b" />
+          <div className="execute-card execute-chat-card">
+            <div className="execute-chat-toolbar">
+              <div className="execute-chat-agent-head">
+                <div className="execute-detail-icon" style={{ background: agent.iconBackground || '#FFEAD5' }}>
+                  {agent.icon && typeof agent.icon === 'object' && agent.icon.type === 'image' ? (
+                    <img src={buildIconUrl(agent.icon.url)} alt={agent.name} className="execute-detail-icon-img" />
+                  ) : (
+                    agent.icon || '🤖'
+                  )}
+                </div>
+                <div className="execute-chat-agent-meta">
+                  <h1 className="execute-detail-name">
+                    {loading ? '加载中…' : (agent.name || '智能体')}
+                  </h1>
+                  <div className="execute-detail-sub">
+                    {mode && <span className="execute-detail-mode-tag">{MODE_LABEL[mode] || mode}</span>}
+                  </div>
+                  {error && <div className="execute-detail-error">⚠ {error}</div>}
+                </div>
+              </div>
+              <div className="execute-chat-toolbar-actions">
+                <button
+                  type="button"
+                  className="execute-detail-history-trigger"
+                  onClick={handleNewChatClick}
+                  title="新会话"
+                >
+                  <svg className="new-chat-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                    <path d="M12 5v14" />
+                    <path d="M5 12h14" />
+                  </svg>
+                  <span>{'新会话'}</span>
+                </button>
+                <button
+                  type="button"
+                  className="execute-detail-history-trigger"
+                  onClick={handleToggleHistory}
+                  title="历史记录"
+                >
+                  <svg className="history-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                    <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
+                    <path d="M3 3v5h5" />
+                    <path d="M12 7v5l3 2" />
+                  </svg>
+                  <span>历史记录</span>
+                </button>
+                {hasActiveChatSnapshot && (
+                  <button
+                    type="button"
+                    className="execute-back-active-btn"
+                    onClick={handleBackToActiveChat}
+                  >
+                    返回正在回答
+                  </button>
+                )}
+                <div className="execute-chat-status">
+                  <span className={`execute-chat-status-dot ${chatRunning ? 'is-running' : ''}`} />
+                  {chatRunning ? '正在生成' : '随时可问'}
+                </div>
+              </div>
+            </div>
+            <div className="execute-messages" ref={messagesRef} onScroll={handleMessagesScroll}>
+            {messages.length === 0 ? (
+              <div className="execute-empty">
+                <div className="execute-empty-icon">{emptyState.icon}</div>
+                <div className="execute-empty-title">{emptyState.title}</div>
+                <div className="execute-empty-desc">{emptyState.description}</div>
+                <div className="execute-empty-prompts">
+                  {emptyState.prompts.map(prompt => <span key={prompt}>{prompt}</span>)}
+                </div>
+              </div>
+            ) : (
+              messages.map((m, i) => (
+                <div key={m.id || i} className={`execute-msg execute-msg-${m.role}`}>
+                  <span className="execute-msg-role">{m.role === 'user' ? '我' : agent.name}</span>
+                  <div className="execute-msg-bubble">
+                    {m.pending ? (
+                      <span className="execute-msg-generating">{m.content}</span>
+                    ) : (
+                      <div
+                        className="markdown-body"
+                        dangerouslySetInnerHTML={{ __html: renderMarkdown(m.content) }}
+                      />
+                    )}
+                    {m.streaming && <span className="wf-step-cursor">▍</span>}
+                  </div>
+                  {!m.streaming && m.role === 'assistant' && m.messageId && m.content && (
+                    <div className="execute-msg-actions">
+                      <button
+                        type="button"
+                        className="execute-msg-action-btn execute-msg-download-btn execute-msg-download-word"
+                        disabled={!!m.downloading}
+                        onClick={() => handleDownloadMessage(m, 'word')}
+                        title="下载 Word"
+                      >
+                        <img src={wordIcon} alt="" className="execute-msg-download-icon" />
+                        <span className="execute-msg-download-text">{m.downloading === 'word' ? '生成中...' : 'Word'}</span>
+                      </button>
+                      <button
+                        type="button"
+                        className="execute-msg-action-btn execute-msg-download-btn execute-msg-download-pdf"
+                        disabled={!!m.downloading}
+                        onClick={() => handleDownloadMessage(m, 'pdf')}
+                        title="下载 PDF"
+                      >
+                        <img src={pdfIcon} alt="" className="execute-msg-download-icon" />
+                        <span className="execute-msg-download-text">{m.downloading === 'pdf' ? '生成中...' : 'PDF'}</span>
+                      </button>
+                    </div>
+                  )}
+                </div>
+              ))
+            )}
+            </div>
+            {chatRunError && <div className="execute-detail-error execute-chat-error">⚠ {chatRunError}</div>}
+            <div className="execute-input-panel">
+              <div className="execute-input-hint">Shift + Enter 换行 · Enter 发送</div>
+              <div className="execute-input-bar">
+                <textarea
+                  className="execute-input"
+                  value={input}
+                  onChange={e => setInput(e.target.value)}
+                  onKeyDown={e => {
+                    if (e.key === 'Enter' && !e.shiftKey) {
+                      e.preventDefault()
+                      handleSend()
+                    }
+                  }}
+                  placeholder="请描述报告要求,例如:总结研究背景、核心结论和风险建议,输出为正式汇报口吻..."
+                  disabled={chatRunning}
+                  rows={2}
+                />
+            <button
+              type="button"
+              className="execute-params-btn"
+              onClick={() => setShowChatParamsModal(true)}
+              title="配置参数"
+              disabled={paramsLoading}
+            >
+              <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                <line x1="4" y1="21" x2="4" y2="14" />
+                <line x1="4" y1="10" x2="4" y2="3" />
+                <line x1="12" y1="21" x2="12" y2="12" />
+                <line x1="12" y1="8" x2="12" y2="3" />
+                <line x1="20" y1="21" x2="20" y2="16" />
+                <line x1="20" y1="12" x2="20" y2="3" />
+                <line x1="1" y1="14" x2="7" y2="14" />
+                <line x1="9" y1="8" x2="15" y2="8" />
+                <line x1="17" y1="16" x2="23" y2="16" />
+              </svg>
+              <span>参数</span>
+              {(() => {
+                const filled = userInputForm.filter(item => {
+                  const t = Object.keys(item)[0]
+                  const c = item[t]
+                  if (!c?.variable) return false
+                  const v = formValues[c.variable]
+                  return v !== '' && v !== undefined && v !== null
+                }).length
+                return filled > 0 ? <span className="execute-params-dot">{filled}</span> : null
+              })()}
+            </button>
+            {chatRunning ? (
+              <button
+                className="execute-send-btn execute-stop-btn"
+                onClick={handleStopChat}
+                type="button"
+              >
+                <svg className="execute-btn-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
+                  <rect x="7" y="7" width="10" height="10" rx="1.5" />
+                </svg>
+                <span>停止</span>
+              </button>
+            ) : (
+              <button
+                className="execute-send-btn"
+                onClick={handleSend}
+                disabled={!input.trim() || loading || paramsLoading}
+                type="button"
+              >
+                <svg className="execute-btn-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
+                  <path d="M22 2L11 13" />
+                  <path d="M22 2L15 22L11 13L2 9L22 2Z" />
+                </svg>
+                <span>发送</span>
+              </button>
+            )}
+              </div>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* ============== API Key 弹框 ============== */}
+      {showApiKeyModal && apiKey && (
+        <div className="apikey-modal-mask" onClick={() => setShowApiKeyModal(false)}>
+          <div className="apikey-modal" onClick={e => e.stopPropagation()}>
+            <div className="apikey-modal-head">
+              <h3>API Key</h3>
+              <button
+                type="button"
+                className="apikey-modal-close"
+                onClick={() => setShowApiKeyModal(false)}
+                aria-label="关闭"
+              >×</button>
+            </div>
+            <div className="apikey-modal-body">
+              <code className="apikey-modal-value">{apiKey}</code>
+              <p className="apikey-modal-tip">请妥善保管,不要在客户端代码中暴露此 Key。</p>
+            </div>
+            <div className="apikey-modal-foot">
+              <button
+                type="button"
+                className={`apikey-modal-copy ${copied ? 'is-copied' : ''}`}
+                onClick={handleCopyApiKey}
+              >
+                {copied ? '✓ 已复制' : '复制'}
+              </button>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* ============== 聊天模式:参数配置弹框 ============== */}
+      {showChatParamsModal && isChat && (
+        <div className="apikey-modal-mask" onClick={() => setShowChatParamsModal(false)}>
+          <div className="apikey-modal chat-params-modal" onClick={e => e.stopPropagation()}>
+            <div className="apikey-modal-head">
+              <h3>⚙️ 输入参数</h3>
+              <button
+                type="button"
+                className="apikey-modal-close"
+                onClick={() => setShowChatParamsModal(false)}
+                aria-label="关闭"
+              >×</button>
+            </div>
+            <div className="apikey-modal-body chat-params-modal-body">
+              {paramsLoading && <div className="wf-form-loading">表单参数加载中…</div>}
+              {paramsError && <div className="execute-detail-error">⚠ {paramsError}</div>}
+              {!paramsLoading && userInputForm.length > 0 && (
+                <div className="wf-form">
+                  {userInputForm.map((item, i) => renderFormField(item, i))}
+                </div>
+              )}
+              {!paramsLoading && !paramsError && userInputForm.length === 0 && (
+                <div className="wf-form-empty">该对话流无输入参数,可直接发送消息</div>
+              )}
+            </div>
+            <div className="apikey-modal-foot">
+              <button
+                type="button"
+                className="apikey-modal-copy"
+                onClick={() => setShowChatParamsModal(false)}
+              >
+                完成
+              </button>
+            </div>
+          </div>
+        </div>
+      )}
+      {historyOpen && (
+        <>
+          <div className="execute-drawer-mask is-open" onClick={() => setHistoryOpen(false)} />
+          <aside className="execute-drawer is-open" role="dialog" aria-label="历史记录">
+            <div className="execute-drawer-head">
+              <button type="button" className="execute-drawer-close" onClick={() => setHistoryOpen(false)} aria-label="关闭">x</button>
+              <div className="execute-drawer-title">历史记录</div>
+            </div>
+            <div className="execute-drawer-body">
+              {historyLoading ? (
+                <div className="execute-drawer-status">加载中...</div>
+              ) : historyError ? (
+                <div className="execute-drawer-status is-error">{historyError}</div>
+              ) : historyList.length === 0 ? (
+                <div className="execute-drawer-status">暂无历史记录</div>
+              ) : (
+                <ul className="execute-drawer-list">
+                  {historyList.map(conv => (
+                    <li key={conv.id} className={`execute-drawer-item ${resumingConv === conv.id ? 'is-loading' : ''}`} onClick={() => handleSelectHistory(conv)}>
+                      <div className="execute-drawer-item-main">
+                        <div className="execute-drawer-item-name">{conv.name || conv.summary || '新会话'}</div>
+                        <div className="execute-drawer-item-meta">{conv.message_count || 0} 条消息</div>
+                      </div>
+                    </li>
+                  ))}
+                </ul>
+              )}
+            </div>
+          </aside>
+        </>
+      )}
+
+    </div>
+  )
+}
+
+export default AgentExecute

+ 381 - 0
src/pages/AgentList.jsx

@@ -0,0 +1,381 @@
+import { useState, useEffect, useMemo } from 'react'
+import { useNavigate, useSearchParams } from 'react-router-dom'
+import { getHottestApps, getLatestApps, getBusinessAppsList, getAgentPros, getAppApiKey } from '@/service/business'
+import { API_BASE_URL } from '@/config'
+import emojiMartData from '@emoji-mart/data'
+
+// 检查是否是 Unicode emoji 字符
+const isNativeEmoji = (str) => {
+  if (!str) return false
+  return /[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(str)
+}
+
+// 通过 emoji-mart data 把 emoji 名称转换为 native Unicode 字符
+const emojiNameToNative = (name) => {
+  if (!name) return '🤖'
+  const emoji = emojiMartData.emojis?.[name]
+  return emoji?.skins?.[0]?.native || '🤖'
+}
+
+const resolveIcon = (icon) => {
+  if (!icon) return '🤖'
+  if (isNativeEmoji(icon)) return icon
+  return emojiNameToNative(icon)
+}
+
+// 把后端返回的 icon 路径拼成完整可访问 URL
+//   - 已经是 http(s):// 开头 → 原样返回
+//   - 以 / 开头(相对路径)→ 前面拼上 API_BASE_URL
+//   - 其它(名字/emoji)→ 原样返回
+const buildIconUrl = (icon) => {
+  if (!icon) return ''
+  if (/^https?:\/\//i.test(icon)) return icon
+  if (icon.startsWith('/')) return `${API_BASE_URL}${icon}`
+  return icon
+}
+
+// 数字格式化
+const formatNumber = (num) => {
+  if (!num && num !== 0) return '0'
+  return Number(num).toLocaleString('en-US')
+}
+
+// 时间分级(复用 Plaza 的逻辑)
+const formatRelativeTime = (timestamp) => {
+  if (!timestamp) return ''
+  const ts = timestamp * 1000
+  const startOfToday = new Date()
+  startOfToday.setHours(0, 0, 0, 0)
+  const startOfYesterday = new Date(startOfToday)
+  startOfYesterday.setDate(startOfYesterday.getDate() - 1)
+  const startOfThreeDaysAgo = new Date(startOfToday)
+  startOfThreeDaysAgo.setDate(startOfThreeDaysAgo.getDate() - 3)
+
+  if (ts >= startOfToday.getTime()) return '今日'
+  if (ts >= startOfYesterday.getTime()) return '昨日'
+  if (ts >= startOfThreeDaysAgo.getTime()) return '近三天'
+  const d = new Date(ts)
+  const pad = (n) => String(n).padStart(2, '0')
+  return `${pad(d.getMonth() + 1)}/${pad(d.getDate())}`
+}
+
+// 模式选项(从后端 mode 枚举)
+const MODE_OPTIONS = [
+  { value: '', label: '全部模式' },
+  { value: 'workflow', label: '工作流' },
+  { value: 'advanced-chat', label: '对话流' },
+  { value: 'chat', label: '基础对话' },
+  { value: 'agent-chat', label: 'Agent 对话' },
+  { value: 'completion', label: '文本生成' },
+]
+
+// 排序选项
+const SORT_OPTIONS = [
+  { value: 'default', label: '综合' },
+  { value: 'heat', label: '热度最高' },
+  { value: 'latest', label: '最新发布' },
+  { value: 'recommend', label: '推荐优先' },
+  { value: 'name', label: '名称 A-Z' },
+]
+
+// 标题映射
+const TYPE_META = {
+  hot: {
+    title: '热门智能体',
+    subtitle: '按近 7 天调用热度排序,看看平台最受欢迎的智能体',
+  },
+  latest: {
+    title: '最新智能体',
+    subtitle: '按上架时间排序,发现平台最新发布的智能体',
+  },
+  system: {
+    title: '系统智能体',
+    subtitle: '平台预置的系统级智能体,涵盖多种业务场景',
+  },
+  all: {
+    title: '智能体列表',
+    subtitle: '浏览所有可用的智能体,选择适合您需求的工具',
+  },
+}
+
+// 数据标准化(兼容接口字段差异)
+const normalize = (item) => {
+  const iconType = item.icon_type || 'emoji'
+  let iconValue = '🤖'
+  if (iconType === 'image') {
+    const raw = item.icon || item.icon_url || item.logo || ''
+    iconValue = { type: 'image', url: buildIconUrl(raw) }
+  } else {
+    iconValue = resolveIcon(item.icon)
+  }
+  return {
+    id: item.id,
+    name: item.name || '',
+    desc: item.description || item.introduction || '',
+    icon: iconValue,
+    iconType,
+    iconBackground: item.icon_background || '#FFEAD5',
+    heat: item.heat ?? item.call_count ?? 0,
+    mode: item.mode || '',
+    created_at: item.created_at,
+    updated_at: item.updated_at,
+    recommend_score: item.recommend_score ?? 0,
+    tags: item.tags || [],
+    // 系统智能体专属字段:用于点击时按 source_type 分流
+    sourceType: item.source_type,
+    sourceAppId: item.source_app_id,
+  }
+}
+
+function AgentList() {
+  const navigate = useNavigate()
+  const [searchParams] = useSearchParams()
+  const type = searchParams.get('type') || 'all'   // hot | latest | all
+  const meta = TYPE_META[type] || TYPE_META.all
+
+  const [agents, setAgents] = useState([])
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState('')
+  const [clickingId, setClickingId] = useState(null)
+
+  // 筛选条件
+  const [keyword, setKeyword] = useState('')
+  const [modeFilter, setModeFilter] = useState('')
+  const [sortBy, setSortBy] = useState(() => {
+    if (type === 'latest') return 'latest'
+    if (type === 'hot') return 'heat'
+    if (type === 'system') return 'recommend'
+    return 'default'
+  })
+
+  useEffect(() => {
+    let cancelled = false
+    const fetchList = async () => {
+      setLoading(true)
+      setError('')
+      try {
+        // 根据 type 选择不同的接口:
+        //   hot    → /business/apps/hottest
+        //   latest → /business/apps/latest
+        //   all    → /business/apps/list
+        let res
+        if (type === 'hot') {
+          res = await getHottestApps()
+        } else if (type === 'latest') {
+          res = await getLatestApps()
+        } else if (type === 'system') {
+          // 系统智能体:GET /business/agent-pros(返回 {data:[...], has_more})
+          res = await getAgentPros()
+        } else {
+          res = await getBusinessAppsList()
+        }
+        if (cancelled) return
+        const list = (res?.data || res || []).map(normalize)
+        setAgents(list)
+      } catch (e) {
+        if (!cancelled) setError(e?.message || '加载失败')
+      } finally {
+        if (!cancelled) setLoading(false)
+      }
+    }
+    fetchList()
+    return () => { cancelled = true }
+  }, [type])
+
+  // type 切换时同步默认排序,避免用户从 /agents?type=all 点"查看更多"后 sortBy 仍是 default
+  useEffect(() => {
+    if (type === 'latest') setSortBy('latest')
+    else if (type === 'hot') setSortBy('heat')
+    else if (type === 'system') setSortBy('recommend')
+  }, [type])
+
+  // 客户端筛选 + 排序
+  const filtered = useMemo(() => {
+    let list = agents.slice()
+    // 关键词
+    if (keyword.trim()) {
+      const kw = keyword.trim().toLowerCase()
+      list = list.filter(a => (a.name || '').toLowerCase().includes(kw) || (a.desc || '').toLowerCase().includes(kw))
+    }
+    // 模式
+    if (modeFilter) {
+      list = list.filter(a => a.mode === modeFilter)
+    }
+    // 排序
+    switch (sortBy) {
+      case 'heat':
+        list.sort((a, b) => (b.heat || 0) - (a.heat || 0))
+        break
+      case 'latest':
+        list.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
+        break
+      case 'recommend':
+        list.sort((a, b) => (b.recommend_score || 0) - (a.recommend_score || 0))
+        break
+      case 'name':
+        list.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
+        break
+      default:
+        // 综合:保持原顺序
+        break
+    }
+    return list
+  }, [agents, keyword, modeFilter, sortBy])
+
+  // 点击卡片:按 source_type 分流
+  //   - external → ExternalMock 占位页
+  //   - 其它 → 用 sourceAppId(兜底 target_id)走权限校验后跳 execute/apply
+  const handleCardClick = async (agent) => {
+    if (!agent?.id || clickingId === agent.id) return
+    setClickingId(agent.id)
+    try {
+      if (agent.sourceType === 'external') {
+        navigate(`/agents/external/${agent.id}`, { state: { agent } })
+        return
+      }
+      const appId = agent.sourceAppId || agent.id
+      const res = await getAppApiKey(appId)
+      const payload = res?.data ?? res ?? {}
+      if (payload.has_permission) {
+        navigate(`/agents/execute/${appId}`, {
+          state: { agent: { ...agent, id: appId }, apiKey: payload.api_key || '' },
+        })
+      } else {
+        navigate(`/agents/apply/${appId}`, { state: { agent: { ...agent, id: appId } } })
+      }
+    } catch (e) {
+      navigate(`/agents/apply/${agent.sourceAppId || agent.id}`, {
+        state: { agent, error: e?.message || '权限校验失败' },
+      })
+    } finally {
+      setClickingId(null)
+    }
+  }
+
+  const handleBack = () => {
+    navigate('/')
+  }
+
+  // 模式标签友好显示
+  const MODE_LABEL = {
+    workflow: '工作流',
+    'advanced-chat': '对话流',
+    chat: '基础对话',
+    'agent-chat': 'Agent',
+    completion: '文本生成',
+  }
+
+  return (
+    <div className="agent-list-page">
+      {/* 页头 */}
+      <div className="page-header agent-list-header">
+        <button type="button" className="agent-list-back" onClick={handleBack} title="返回广场">
+          <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
+            <polyline points="15 18 9 12 15 6" />
+          </svg>
+          <span>返回广场</span>
+        </button>
+        <h1 className="page-title">{meta.title}</h1>
+        <p className="page-subtitle">{meta.subtitle}</p>
+      </div>
+
+      {/* 筛选条 */}
+      <div className="agent-list-filters">
+        <div className="agent-list-search">
+          <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+            <circle cx="11" cy="11" r="7" />
+            <line x1="21" y1="21" x2="16.65" y2="16.65" />
+          </svg>
+          <input
+            type="text"
+            placeholder="搜索智能体名称或描述"
+            value={keyword}
+            onChange={e => setKeyword(e.target.value)}
+          />
+          {keyword && (
+            <button type="button" className="agent-list-search-clear" onClick={() => setKeyword('')} title="清空">
+              ×
+            </button>
+          )}
+        </div>
+
+        <div className="agent-list-select">
+          <select value={modeFilter} onChange={e => setModeFilter(e.target.value)}>
+            {MODE_OPTIONS.map(opt => (
+              <option key={opt.value} value={opt.value}>{opt.label}</option>
+            ))}
+          </select>
+        </div>
+
+        <div className="agent-list-select">
+          <select value={sortBy} onChange={e => setSortBy(e.target.value)}>
+            {SORT_OPTIONS.map(opt => (
+              <option key={opt.value} value={opt.value}>{opt.label}</option>
+            ))}
+          </select>
+        </div>
+
+        <div className="agent-list-result-count">
+          共 <strong>{filtered.length}</strong> 个智能体
+        </div>
+      </div>
+
+      {/* 列表 */}
+      {loading ? (
+        <div className="agent-list-status">加载中...</div>
+      ) : error ? (
+        <div className="agent-list-status agent-list-error">{error}</div>
+      ) : filtered.length === 0 ? (
+        <div className="agent-list-status agent-list-empty">暂无符合条件的智能体</div>
+      ) : (
+        <div className="card-grid agent-list-grid">
+          {filtered.map(agent => {
+            const isImageIcon = agent.icon && typeof agent.icon === 'object' && agent.icon.type === 'image'
+            return (
+              <div
+                key={agent.id}
+                className={`card agent-list-card ${clickingId === agent.id ? 'is-loading' : ''}`}
+                onClick={() => handleCardClick(agent)}
+              >
+                <div className="card-header">
+                  <div
+                    className="card-icon"
+                    style={isImageIcon ? { background: agent.iconBackground, overflow: 'hidden' } : undefined}
+                  >
+                    {isImageIcon ? (
+                      <img src={agent.icon.url} alt={agent.name} className="card-icon-img" />
+                    ) : (
+                      agent.icon
+                    )}
+                  </div>
+                  <div className="agent-list-card-meta">
+                    <div className="card-title">{agent.name}</div>
+                    {agent.mode && <span className="agent-list-mode-tag">{MODE_LABEL[agent.mode] || agent.mode}</span>}
+                  </div>
+                  {clickingId === agent.id && <span className="agent-list-card-checking">校验中…</span>}
+                </div>
+                <div className="card-description">{agent.desc || '暂无描述'}</div>
+                <div className="agent-list-card-foot">
+                  {type === 'hot' || (sortBy === 'heat' && agent.heat > 0) ? (
+                    <span className="agent-list-stat">
+                      <span className="agent-list-stat-icon">🔥</span>
+                      <span>{formatNumber(agent.heat)} 次使用</span>
+                    </span>
+                  ) : null}
+                  {agent.created_at ? (
+                    <span className="agent-list-stat agent-list-stat-time">
+                      <span className="agent-list-stat-icon">🕒</span>
+                      <span>{formatRelativeTime(agent.created_at)}</span>
+                    </span>
+                  ) : null}
+                </div>
+              </div>
+            )
+          })}
+        </div>
+      )}
+    </div>
+  )
+}
+
+export default AgentList

+ 81 - 0
src/pages/AvailableAgents.jsx

@@ -0,0 +1,81 @@
+import { Button, Card, Empty, Space, Tag, Typography } from '@arco-design/web-react'
+import { IconApps, IconClockCircle, IconRobot } from '@arco-design/web-react/icon'
+
+function AvailableAgents() {
+  const availableAgents = [
+    {
+      id: 1,
+      name: '代码助手',
+      description: '专业的代码开发和调试助手',
+      icon: '💻',
+      status: '可用',
+      expires: '2024-12-31'
+    },
+    {
+      id: 2,
+      name: '文档生成器',
+      description: '自动化文档生成工具',
+      icon: '📚',
+      status: '可用',
+      expires: '2024-12-31'
+    },
+    {
+      id: 3,
+      name: '数据分析',
+      description: '智能数据分析和可视化',
+      icon: '📊',
+      status: '可用',
+      expires: '2024-06-30'
+    },
+    {
+      id: 4,
+      name: '内容创作',
+      description: 'AI 内容创作助手',
+      icon: '✍️',
+      status: '可用',
+      expires: '2024-08-15'
+    }
+  ]
+
+  return (
+    <div className="available-agents-page">
+      <div className="available-agents-header">
+        <div>
+          <Typography.Title heading={4} className="available-agents-title">可用智能体</Typography.Title>
+          <Typography.Text className="available-agents-subtitle">管理您已获取的智能体权限</Typography.Text>
+        </div>
+        <Tag color="arcoblue" className="available-agents-count-tag">{availableAgents.length} 个可用</Tag>
+      </div>
+
+      <Card className="available-agents-card" bordered>
+        {availableAgents.length === 0 ? (
+          <Empty description="暂无可用智能体" />
+        ) : (
+          <div className="available-agents-list">
+            {availableAgents.map(agent => (
+              <div key={agent.id} className="available-agent-item">
+                <div className="available-agent-main">
+                  <div className="available-agent-icon">{agent.icon || <IconRobot />}</div>
+                  <div className="available-agent-content">
+                    <Typography.Text className="available-agent-name">{agent.name}</Typography.Text>
+                    <Typography.Text className="available-agent-description">{agent.description}</Typography.Text>
+                    <Space size={16} wrap className="available-agent-meta">
+                      <span><IconApps />已授权</span>
+                      <span><IconClockCircle />有效期至 {agent.expires}</span>
+                    </Space>
+                  </div>
+                </div>
+                <div className="available-agent-actions">
+                  <Tag color="green" className="available-agent-status">{agent.status}</Tag>
+                  <Button type="primary" className="available-agent-use-btn">立即使用</Button>
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+      </Card>
+    </div>
+  )
+}
+
+export default AvailableAgents

+ 62 - 0
src/pages/ExternalMock.jsx

@@ -0,0 +1,62 @@
+import { useLocation, useNavigate } from 'react-router-dom'
+import Layout from '@/components/Layout'
+
+// 外部智能体(agent_pro source_type === 'external')的占位页面
+//   my-recommended 接口会返回 source_type,external 类型的智能体
+//   在此页面仅展示基础信息,不加载业务数据、不调用具体 API。
+function ExternalMock() {
+  const location = useLocation()
+  const navigate = useNavigate()
+  const state = location.state || {}
+  const agent = state.agent || {}
+  const name = agent.name || '外部智能体'
+  const description = agent.desc || agent.description || '该智能体由外部系统提供,暂未对接具体业务。'
+  const mode = agent.mode || ''
+  const targetId = agent.id || ''
+
+  return (
+    <Layout>
+      <div className="page-content">
+        <div className="page-header">
+          <button className="page-back-btn" onClick={() => navigate(-1)}>← 返回</button>
+          <h1 className="page-title">{name}</h1>
+          <div className="page-subtitle">外部智能体(占位预览)</div>
+        </div>
+
+        <div className="external-mock-card">
+          <div className="external-mock-icon">🔌</div>
+          <div className="external-mock-title">{name}</div>
+          <div className="external-mock-desc">{description}</div>
+
+          <div className="external-mock-meta">
+            <div className="external-mock-meta-item">
+              <span className="external-mock-meta-label">来源类型</span>
+              <span className="external-mock-meta-value">external</span>
+            </div>
+            {mode && (
+              <div className="external-mock-meta-item">
+                <span className="external-mock-meta-label">运行模式</span>
+                <span className="external-mock-meta-value">{mode}</span>
+              </div>
+            )}
+            <div className="external-mock-meta-item">
+              <span className="external-mock-meta-label">目标 ID</span>
+              <span className="external-mock-meta-value external-mock-meta-mono">{targetId}</span>
+            </div>
+          </div>
+
+          <div className="external-mock-tip">
+            <span className="external-mock-tip-icon">💡</span>
+            <span>该智能体由外部系统提供,当前为占位页面,尚未对接具体业务能力。</span>
+          </div>
+
+          <button className="external-mock-back" onClick={() => navigate('/plaza')}>
+            返回广场
+          </button>
+        </div>
+      </div>
+    </Layout>
+  )
+}
+
+export default ExternalMock

+ 88 - 0
src/pages/History.jsx

@@ -0,0 +1,88 @@
+import { Button, Card, Empty, Space, Tag, Typography } from '@arco-design/web-react'
+import { IconClockCircle, IconHistory, IconRobot } from '@arco-design/web-react/icon'
+
+function History() {
+  const historyRecords = [
+    {
+      id: 1,
+      agent: '代码助手',
+      title: 'React 组件开发',
+      time: '2024-01-15 14:30',
+      duration: '15分钟'
+    },
+    {
+      id: 2,
+      agent: '文档生成器',
+      title: 'API 文档编写',
+      time: '2024-01-15 11:20',
+      duration: '8分钟'
+    },
+    {
+      id: 3,
+      agent: '数据分析',
+      title: '销售数据分析',
+      time: '2024-01-14 16:45',
+      duration: '22分钟'
+    },
+    {
+      id: 4,
+      agent: '内容创作',
+      title: '产品推广文案',
+      time: '2024-01-14 10:15',
+      duration: '12分钟'
+    },
+    {
+      id: 5,
+      agent: '代码助手',
+      title: 'Python 脚本调试',
+      time: '2024-01-13 15:30',
+      duration: '25分钟'
+    },
+    {
+      id: 6,
+      agent: '图像生成',
+      title: '产品设计图',
+      time: '2024-01-13 09:00',
+      duration: '10分钟'
+    }
+  ]
+
+  return (
+    <div className="history-page">
+      <div className="history-page-header">
+        <div>
+          <Typography.Title heading={4} className="history-page-title">历史记录</Typography.Title>
+          <Typography.Text className="history-page-subtitle">查看您使用智能体的历史记录</Typography.Text>
+        </div>
+        <Tag color="arcoblue" className="history-count-tag">{historyRecords.length} 条记录</Tag>
+      </div>
+
+      <Card className="history-card" bordered>
+        {historyRecords.length === 0 ? (
+          <Empty description="暂无历史记录" />
+        ) : (
+          <div className="history-list">
+            {historyRecords.map(record => (
+              <div key={record.id} className="history-item">
+                <div className="history-item-main">
+                  <div className="history-item-icon"><IconHistory /></div>
+                  <div className="history-item-content">
+                    <Typography.Text className="history-item-title">{record.title}</Typography.Text>
+                    <Space size={16} wrap className="history-item-meta">
+                      <span><IconRobot />{record.agent}</span>
+                      <span><IconClockCircle />{record.time}</span>
+                      <span>耗时 {record.duration}</span>
+                    </Space>
+                  </div>
+                </div>
+                <Button type="text" status="normal" className="history-detail-btn">查看详情</Button>
+              </div>
+            ))}
+          </div>
+        )}
+      </Card>
+    </div>
+  )
+}
+
+export default History

+ 238 - 0
src/pages/Login.css

@@ -0,0 +1,238 @@
+.login-container {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #f0f4ff 0%, #e6f0ff 50%, #f8faff 100%);
+  position: relative;
+  overflow: hidden;
+}
+
+.bg-decoration {
+  position: absolute;
+  border-radius: 50%;
+  filter: blur(80px);
+  opacity: 0.4;
+  pointer-events: none;
+}
+
+.bg-decoration-1 {
+  width: 400px;
+  height: 400px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  top: -100px;
+  left: -100px;
+  animation: float 8s ease-in-out infinite;
+}
+
+.bg-decoration-2 {
+  width: 300px;
+  height: 300px;
+  background: linear-gradient(135deg, #00d4ff 0%, #667eea 100%);
+  bottom: -50px;
+  right: -50px;
+  animation: float 6s ease-in-out infinite reverse;
+}
+
+.bg-decoration-3 {
+  width: 250px;
+  height: 250px;
+  background: linear-gradient(135deg, #764ba2 0%, #00d4ff 100%);
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  animation: pulse 4s ease-in-out infinite;
+}
+
+@keyframes float {
+  0%, 100% {
+    transform: translate(0, 0);
+  }
+  50% {
+    transform: translate(30px, 30px);
+  }
+}
+
+@keyframes pulse {
+  0%, 100% {
+    opacity: 0.2;
+    transform: translate(-50%, -50%) scale(1);
+  }
+  50% {
+    opacity: 0.4;
+    transform: translate(-50%, -50%) scale(1.1);
+  }
+}
+
+.login-box {
+  background: rgba(255, 255, 255, 0.9);
+  backdrop-filter: blur(20px);
+  border-radius: 24px;
+  padding: 48px;
+  width: 100%;
+  max-width: 440px;
+  box-shadow: 0 20px 60px rgba(102, 126, 234, 0.15), 0 8px 25px rgba(118, 75, 162, 0.1);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  position: relative;
+  z-index: 1;
+}
+
+.login-header {
+  text-align: center;
+  margin-bottom: 40px;
+}
+
+.logo-icon {
+  width: 64px;
+  height: 64px;
+  margin: 0 auto 20px;
+  border-radius: 16px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32px;
+  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+  animation: glow-pulse 2.5s ease-in-out infinite;
+}
+
+.login-logo {
+  width: 80px;
+  height: 80px;
+  margin: 0 auto 20px;
+  display: block;
+  object-fit: contain;
+  filter: drop-shadow(0 4px 12px rgba(102, 126, 234, 0.4));
+}
+
+@keyframes glow-pulse {
+  0%, 100% {
+    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+  }
+  50% {
+    box-shadow: 0 8px 40px rgba(102, 126, 234, 0.5);
+  }
+}
+
+.login-title {
+  font-size: 28px;
+  font-weight: 800;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #00d4ff 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  margin-bottom: 8px;
+}
+
+.login-subtitle {
+  color: #64748b;
+  font-size: 15px;
+  font-weight: 500;
+}
+
+.login-form {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.form-label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1e293b;
+}
+
+.form-input {
+  padding: 14px 18px;
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 12px;
+  font-size: 15px;
+  background: rgba(255, 255, 255, 0.8);
+  transition: all 0.3s ease;
+  outline: none;
+}
+
+.form-input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
+  background: #fff;
+}
+
+.form-input::placeholder {
+  color: #94a3b8;
+}
+
+.password-input-wrapper {
+  position: relative;
+}
+
+.password-input {
+  width: 100%;
+  padding-right: 56px;
+}
+
+.toggle-password-btn {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  transform: translateY(-50%);
+  background: none;
+  border: none;
+  font-size: 20px;
+  cursor: pointer;
+  padding: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 8px;
+  transition: background 0.2s ease;
+}
+
+.toggle-password-btn:hover {
+  background: rgba(102, 126, 234, 0.1);
+}
+
+.error-message {
+  background: rgba(239, 68, 68, 0.1);
+  border: 1px solid rgba(239, 68, 68, 0.3);
+  color: #dc2626;
+  padding: 12px 16px;
+  border-radius: 10px;
+  font-size: 14px;
+  font-weight: 500;
+  text-align: center;
+}
+
+.login-btn {
+  padding: 16px;
+  border: none;
+  border-radius: 12px;
+  font-size: 16px;
+  font-weight: 700;
+  color: white;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
+}
+
+.login-btn:hover:not(:disabled) {
+  transform: translateY(-2px);
+  box-shadow: 0 12px 35px rgba(102, 126, 234, 0.4);
+  background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
+}
+
+.login-btn:active:not(:disabled) {
+  transform: translateY(0);
+}
+
+.login-btn:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}

+ 161 - 0
src/pages/Login.jsx

@@ -0,0 +1,161 @@
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useAuth } from '@/contexts/AuthContext'
+import { encryptPassword } from '@/utils/encryption'
+import { setLoggedIn } from '@/utils/auth'
+import { emailRegex, API_PREFIX } from '@/config'
+import './Login.css'
+
+export default function Login() {
+  const navigate = useNavigate()
+  const { login } = useAuth()
+  const [email, setEmail] = useState('')
+  const [password, setPassword] = useState('')
+  const [showPassword, setShowPassword] = useState(false)
+  const [isLoading, setIsLoading] = useState(false)
+  const [error, setError] = useState('')
+
+  const handleLogin = async (e) => {
+    e.preventDefault()
+    setError('')
+
+    if (!email) {
+      setError('请输入邮箱')
+      return
+    }
+    if (!emailRegex.test(email)) {
+      setError('请输入有效的邮箱地址')
+      return
+    }
+    if (!password?.trim()) {
+      setError('请输入密码')
+      return
+    }
+
+    try {
+      setIsLoading(true)
+      const encryptedPwd = encryptPassword(password)
+      
+      const response = await fetch(`${API_PREFIX}/login`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          email,
+          password: encryptedPwd,
+          language: 'zh-Hans',
+          remember_me: true,
+        }),
+        credentials: 'include',
+      })
+
+      const res = await response.json()
+
+      // 调试:登录响应原始结构(确认 access_token 在不在 body 里)
+      console.log('[Login] raw response →', res)
+
+      if (res.result === 'success') {
+        // 登录成功,提取 access_token 存到 localStorage
+        // Dify 不同版本响应结构:
+        //   { result: 'success', data: { access_token, refresh_token, ... } }
+        //   { result: 'success', access_token, refresh_token, ... }(挂在根上)
+        //   { result: 'success', token: '...' }(少数版本)
+        const dataObj = (res.data && typeof res.data === 'object') ? res.data : {}
+        const token =
+          dataObj.access_token ||
+          res.access_token ||
+          res.token ||
+          ''
+        if (token) {
+          try { localStorage.setItem('access_token', token) } catch (_) {}
+        }
+        // 登录成功,设置登录标记
+        setLoggedIn()
+        login()
+        navigate('/')
+      } else {
+        setError(typeof res.data === 'string' ? res.data : '登录失败')
+      }
+    } catch (err) {
+      console.error('Login error:', err)
+      if (err.code === 'authentication_failed') {
+        setError('邮箱或密码错误')
+      } else {
+        setError(err.message || '登录失败,请稍后重试')
+      }
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  return (
+    <div className="login-container">
+      <div className="login-box">
+        <div className="login-header">
+          <img src="/logo.png" alt="Logo" className="login-logo" />
+          <h1 className="login-title">AI Agent 智能体平台</h1>
+          <p className="login-subtitle">登录以继续使用</p>
+        </div>
+
+        <form className="login-form" onSubmit={handleLogin}>
+          <div className="form-group">
+            <label className="form-label">邮箱</label>
+            <input
+              type="email"
+              className="form-input"
+              value={email}
+              onChange={(e) => setEmail(e.target.value)}
+              placeholder="请输入邮箱"
+              autoComplete="email"
+            />
+          </div>
+
+          <div className="form-group">
+            <label className="form-label">密码</label>
+            <div className="password-input-wrapper">
+              <input
+                type={showPassword ? 'text' : 'password'}
+                className="form-input password-input"
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                onKeyDown={(e) => {
+                  if (e.key === 'Enter') {
+                    handleLogin(e)
+                  }
+                }}
+                placeholder="请输入密码"
+                autoComplete="current-password"
+              />
+              <button
+                type="button"
+                className="toggle-password-btn"
+                onClick={() => setShowPassword(!showPassword)}
+              >
+                {showPassword ? '👀' : '😝'}
+              </button>
+            </div>
+          </div>
+
+          {error && (
+            <div className="error-message">
+              {error}
+            </div>
+          )}
+
+          <button
+            type="submit"
+            className="login-btn"
+            disabled={isLoading || !email || !password}
+          >
+            {isLoading ? '登录中...' : '登录'}
+          </button>
+        </form>
+      </div>
+
+      <div className="bg-decoration bg-decoration-1"></div>
+      <div className="bg-decoration bg-decoration-2"></div>
+      <div className="bg-decoration bg-decoration-3"></div>
+    </div>
+  )
+}

+ 1455 - 0
src/pages/Plaza.css

@@ -0,0 +1,1455 @@
+.plaza-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+/* 顶部 Hero 区 */
+.plaza-hero {
+  position: relative;
+  background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 50%, #dfeaff 100%);
+  border-radius: 16px;
+  padding: 40px 48px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  overflow: hidden;
+  border: 1px solid rgba(102, 126, 234, 0.12);
+  box-shadow: 0 8px 30px rgba(102, 126, 234, 0.08);
+}
+
+/* 紧凑型 Hero:居中、低高度 */
+.plaza-hero-compact {
+  padding: 18px 32px;
+  justify-content: center;
+  text-align: center;
+  min-height: 0;
+}
+
+.plaza-hero-compact .plaza-hero-content {
+  flex: 0 1 auto;
+}
+
+.plaza-hero-compact .plaza-hero-title {
+  justify-content: center;
+  font-size: 26px;
+  margin-bottom: 6px;
+  gap: 8px;
+}
+
+.plaza-hero-compact .plaza-hero-icon {
+  font-size: 24px;
+}
+
+.plaza-hero-compact .plaza-hero-desc {
+  font-size: 13px;
+  margin: 0;
+}
+
+.plaza-hero::before {
+  content: '';
+  position: absolute;
+  top: -50%;
+  right: -10%;
+  width: 400px;
+  height: 400px;
+  background: radial-gradient(circle, rgba(102, 126, 234, 0.08) 0%, transparent 70%);
+  border-radius: 50%;
+  pointer-events: none;
+}
+
+.plaza-hero-content {
+  flex: 1;
+  position: relative;
+  z-index: 1;
+}
+
+.plaza-hero-title {
+  font-size: 36px;
+  font-weight: 800;
+  margin: 0 0 12px 0;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #5a4fcf 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  letter-spacing: 1px;
+}
+
+.plaza-hero-icon {
+  font-size: 32px;
+  -webkit-text-fill-color: initial;
+}
+
+.plaza-hero-desc {
+  font-size: 15px;
+  color: #5a5a5a;
+  margin: 0;
+  font-weight: 400;
+  line-height: 1.6;
+}
+
+.plaza-hero-create-btn {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 12px 24px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border: none;
+  border-radius: 12px;
+  color: white;
+  font-size: 15px;
+  font-weight: 600;
+  cursor: pointer;
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35);
+  transition: all 0.3s ease;
+  white-space: nowrap;
+}
+
+.plaza-hero-create-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.45);
+}
+
+.plaza-hero-create-btn span {
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 1;
+}
+
+/* 三列列表 */
+.plaza-section {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.plaza-section-lists {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 16px;
+}
+
+.plaza-list-card {
+  background: white;
+  border: 1px solid rgba(102, 126, 234, 0.15);
+  border-radius: 14px;
+  overflow: hidden;
+  box-shadow: 0 4px 16px rgba(102, 126, 234, 0.06);
+  display: flex;
+  flex-direction: column;
+}
+
+.plaza-list-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 14px 18px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.05) 100%);
+  border-bottom: 1px solid rgba(102, 126, 234, 0.1);
+}
+
+.plaza-list-title {
+  font-size: 16px;
+  font-weight: 700;
+  color: #1a1a1a;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.plaza-title-icon {
+  font-size: 18px;
+}
+
+/* 列表标题后的小标注(如"热门智能体(近7d)") */
+.plaza-title-badge {
+  margin-left: 4px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #ff6b35;
+  font-family: inherit;
+  white-space: nowrap;
+}
+
+.plaza-list-more {
+  font-size: 12px;
+  color: #667eea;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  user-select: none;
+}
+
+.plaza-list-more:hover {
+  color: #764ba2;
+  transform: translateX(2px);
+}
+
+.plaza-list-more:active {
+  color: #4c51bf;
+}
+
+.plaza-list {
+  display: flex;
+  flex-direction: column;
+  max-height: 520px;
+  overflow-y: auto;
+}
+
+.plaza-list::-webkit-scrollbar {
+  width: 8px;
+}
+
+.plaza-list::-webkit-scrollbar-track {
+  background: #f7f8fa;
+  border-radius: 999px;
+}
+
+.plaza-list::-webkit-scrollbar-thumb {
+  background: #c9cdd4;
+  border: 2px solid #f7f8fa;
+  border-radius: 999px;
+}
+
+.plaza-list::-webkit-scrollbar-thumb:hover {
+  background: #a9aeb8;
+}
+
+/* 列表行 */
+.plaza-table-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 18px;
+  border-bottom: 1px solid rgba(102, 126, 234, 0.06);
+  transition: background 0.2s ease;
+  cursor: pointer;
+}
+
+.plaza-table-row:hover {
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.04) 0%, rgba(118, 75, 162, 0.02) 100%);
+}
+
+.plaza-table-row:last-child {
+  border-bottom: none;
+}
+
+.plaza-row-rank {
+  width: 18px;
+  font-size: 13px;
+  font-weight: 700;
+  color: #667eea;
+  text-align: center;
+  flex-shrink: 0;
+  font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+}
+
+.plaza-row-icon {
+  width: 30px;
+  height: 30px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 18px;
+  flex-shrink: 0;
+  line-height: 1;
+  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'Twemoji Mozilla', sans-serif;
+}
+
+.plaza-row-info {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.plaza-row-name {
+  font-size: 13px;
+  font-weight: 600;
+  color: #1a1a1a;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.plaza-row-extra {
+  flex-shrink: 0;
+  font-size: 12px;
+  color: #999;
+  text-align: right;
+  white-space: nowrap;
+  max-width: 140px;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+}
+
+.plaza-extra-heat {
+  color: #667eea;
+  font-weight: 500;
+}
+
+.plaza-extra-author {
+  color: #888;
+}
+
+.plaza-extra-username {
+  color: #667eea;
+  font-weight: 500;
+  margin-right: 2px;
+}
+
+/* 推荐智能体列:创建人 + 时间 组合块 */
+.plaza-extra-author-block {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  max-width: 100%;
+}
+
+.plaza-extra-author-block .plaza-extra-username {
+  max-width: 80px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+/* 热度徽章(热门智能体列) */
+.plaza-extra-heat-badge {
+  display: inline-flex;
+  align-items: center;
+  gap: 3px;
+  padding: 2px 8px;
+  border-radius: 10px;
+  font-size: 12px;
+  font-weight: 600;
+  line-height: 1.4;
+  white-space: nowrap;
+}
+
+.plaza-heat-normal {
+  color: #999;
+  background: rgba(102, 126, 234, 0.06);
+}
+
+.plaza-heat-top {
+  color: white;
+}
+
+.plaza-heat-top-1 {
+  background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
+  box-shadow: 0 2px 6px rgba(255, 107, 53, 0.35);
+}
+
+.plaza-heat-top-2 {
+  background: linear-gradient(135deg, #ffa726 0%, #ffb74d 100%);
+  box-shadow: 0 2px 6px rgba(255, 167, 38, 0.35);
+}
+
+.plaza-heat-top-3 {
+  background: linear-gradient(135deg, #ffca28 0%, #ffd54f 100%);
+  box-shadow: 0 2px 6px rgba(255, 202, 40, 0.35);
+}
+
+.plaza-heat-fire {
+  font-size: 12px;
+  line-height: 1;
+  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'Twemoji Mozilla', sans-serif;
+}
+
+/* 时间分级(最新智能体列) */
+.plaza-extra-time {
+  display: inline-block;
+  padding: 2px 8px;
+  border-radius: 10px;
+  font-size: 12px;
+  font-weight: 600;
+  line-height: 1.4;
+  white-space: nowrap;
+  color: #5a5a5a;
+  background: rgba(102, 126, 234, 0.06);
+  font-variant-numeric: tabular-nums;
+}
+
+.plaza-time-最新 {
+  color: #fff;
+  background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
+  box-shadow: 0 2px 6px rgba(255, 77, 79, 0.3);
+}
+
+.plaza-time-今日 {
+  color: #fff;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
+}
+
+.plaza-time-昨日 {
+  color: #667eea;
+  background: rgba(102, 126, 234, 0.12);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+}
+
+.plaza-time-近三天 {
+  color: #5a5a5a;
+  background: rgba(102, 126, 234, 0.06);
+}
+
+.plaza-time-一周内 {
+  color: #5a5a5a;
+  background: rgba(102, 126, 234, 0.06);
+}
+
+/* 系统智能体 */
+.plaza-section-header {
+  padding: 0 4px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.plaza-section-title {
+  font-size: 20px;
+  font-weight: 700;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  margin: 0;
+  letter-spacing: 0.5px;
+}
+
+.plaza-system-grid {
+  display: grid;
+  grid-template-columns: repeat(6, 1fr);
+  gap: 14px;
+  background: rgba(255, 255, 255, 0.7);
+  border: 1px solid rgba(102, 126, 234, 0.15);
+  border-radius: 14px;
+  padding: 20px 16px;
+  box-shadow: 0 4px 16px rgba(102, 126, 234, 0.06);
+}
+
+.plaza-system-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 6px;
+  padding: 10px 6px;
+  border-radius: 10px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+/* 系统智能体小卡片(6 个时使用,显示 logo+名称+介绍) */
+.plaza-system-card {
+  align-items: flex-start;
+  gap: 10px;
+  padding: 14px;
+  background: #ffffff;
+  border: 1px solid rgba(102, 126, 234, 0.12);
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.06);
+  min-height: 132px;
+}
+
+.plaza-system-card:hover {
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.06) 0%, rgba(118, 75, 162, 0.03) 100%);
+  transform: translateY(-3px);
+  box-shadow: 0 8px 22px rgba(102, 126, 234, 0.18);
+  border-color: rgba(102, 126, 234, 0.32);
+}
+
+.plaza-system-item:hover {
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.06) 100%);
+  transform: translateY(-3px);
+  box-shadow: 0 6px 16px rgba(102, 126, 234, 0.12);
+}
+
+.plaza-system-icon {
+  width: 42px;
+  height: 42px;
+  border-radius: 12px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22px;
+  box-shadow: 0 4px 10px rgba(102, 126, 234, 0.3);
+  transition: all 0.3s ease;
+  flex-shrink: 0;
+}
+
+/* 卡片态:logo 在顶部左对齐 */
+.plaza-system-card .plaza-system-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 12px;
+}
+
+.plaza-system-item:hover .plaza-system-icon {
+  transform: rotate(8deg) scale(1.05);
+}
+
+.plaza-system-name {
+  font-size: 12px;
+  font-weight: 600;
+  color: #1a1a1a;
+  text-align: center;
+  white-space: nowrap;
+}
+
+/* 卡片态:名称左对齐 + 加大字号 */
+.plaza-system-card .plaza-system-name {
+  font-size: 14px;
+  text-align: left;
+  width: 100%;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+/* 系统智能体介绍(卡片专属) */
+.plaza-system-desc {
+  display: none;  /* 默认隐藏(非卡片态) */
+}
+
+/* 卡片态:显示介绍,2 行截断 */
+.plaza-system-card .plaza-system-desc {
+  display: -webkit-box;
+  font-size: 12px;
+  color: #64748b;
+  line-height: 1.5;
+  text-align: left;
+  width: 100%;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  word-break: break-word;
+}
+
+.plaza-system-more .plaza-system-icon {
+  background: rgba(102, 126, 234, 0.1);
+  border: 2px dashed rgba(102, 126, 234, 0.4);
+  box-shadow: none;
+  font-size: 18px;
+  font-weight: 700;
+  color: #667eea;
+}
+
+/* 系统智能体:图片类型 icon */
+.plaza-system-icon-img {
+  width: 100%;
+  height: 100%;
+  border-radius: inherit;
+  object-fit: cover;
+  display: block;
+}
+
+/* 系统智能体:加载/错误/空状态(横跨整行) */
+.plaza-system-status {
+  grid-column: 1 / -1;
+  text-align: center;
+  font-size: 13px;
+  padding: 32px 16px;
+  color: #94a3b8;
+  font-weight: 500;
+}
+
+.plaza-system-loading {
+  color: #667eea;
+}
+
+.plaza-system-error {
+  color: #dc2626;
+}
+
+.plaza-system-empty {
+  color: #94a3b8;
+}
+
+.plaza-system-more .plaza-system-name {
+  color: #667eea;
+  font-weight: 700;
+}
+
+/* ============ 智能体列表页(/agents) ============ */
+.agent-list-page {
+  max-width: 1280px;
+  margin: 0 auto;
+}
+
+.agent-list-header {
+  position: relative;
+  margin-bottom: 24px;
+}
+
+.agent-list-back {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 14px;
+  background: rgba(102, 126, 234, 0.08);
+  border: 1px solid rgba(102, 126, 234, 0.25);
+  border-radius: 999px;
+  color: #667eea;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  margin-bottom: 12px;
+}
+
+.agent-list-back:hover {
+  background: rgba(102, 126, 234, 0.16);
+  border-color: rgba(102, 126, 234, 0.45);
+  transform: translateX(-2px);
+}
+
+.agent-list-filters {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 14px 18px;
+  background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
+  border: 1px solid rgba(102, 126, 234, 0.18);
+  border-radius: 14px;
+  box-shadow: 0 4px 16px rgba(102, 126, 234, 0.06);
+  margin-bottom: 24px;
+  flex-wrap: wrap;
+}
+
+.agent-list-search {
+  position: relative;
+  flex: 1;
+  min-width: 220px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 0 12px;
+  height: 36px;
+  background: rgba(102, 126, 234, 0.04);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 10px;
+  color: #94a3b8;
+  transition: all 0.2s ease;
+}
+
+.agent-list-search:focus-within {
+  background: #ffffff;
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+  color: #667eea;
+}
+
+.agent-list-search input {
+  flex: 1;
+  border: none;
+  outline: none;
+  background: transparent;
+  font-size: 13px;
+  color: #1e293b;
+  min-width: 0;
+}
+
+.agent-list-search input::placeholder {
+  color: #94a3b8;
+}
+
+.agent-list-search-clear {
+  border: none;
+  background: transparent;
+  color: #94a3b8;
+  font-size: 18px;
+  line-height: 1;
+  cursor: pointer;
+  padding: 0 4px;
+  border-radius: 4px;
+  transition: all 0.15s;
+}
+
+.agent-list-search-clear:hover {
+  color: #ef4444;
+  background: rgba(239, 68, 68, 0.1);
+}
+
+.agent-list-select {
+  position: relative;
+  flex-shrink: 0;
+}
+
+.agent-list-select select {
+  appearance: none;
+  -webkit-appearance: none;
+  height: 36px;
+  padding: 0 32px 0 14px;
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.8)
+    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>")
+    no-repeat right 10px center;
+  font-size: 13px;
+  color: #334155;
+  font-family: inherit;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.agent-list-select select:hover {
+  border-color: rgba(102, 126, 234, 0.4);
+  background-color: #ffffff;
+}
+
+.agent-list-select select:focus {
+  outline: none;
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
+}
+
+.agent-list-result-count {
+  margin-left: auto;
+  font-size: 12px;
+  color: #64748b;
+  font-weight: 500;
+}
+
+.agent-list-result-count strong {
+  color: #667eea;
+  font-weight: 700;
+  font-size: 13px;
+}
+
+.agent-list-grid {
+  /* 卡片网格(自动填充) */
+}
+
+.agent-list-card {
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+}
+
+.agent-list-card.is-loading {
+  opacity: 0.6;
+  pointer-events: none;
+}
+
+.agent-list-card-meta {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.agent-list-mode-tag {
+  display: inline-block;
+  padding: 2px 8px;
+  background: rgba(102, 126, 234, 0.08);
+  border: 1px solid rgba(102, 126, 234, 0.2);
+  border-radius: 4px;
+  font-size: 11px;
+  font-weight: 600;
+  color: #667eea;
+  align-self: flex-start;
+  font-family: inherit;
+}
+
+.agent-list-card-checking {
+  flex-shrink: 0;
+  font-size: 11px;
+  font-weight: 600;
+  color: #667eea;
+  background: rgba(102, 126, 234, 0.1);
+  padding: 2px 8px;
+  border-radius: 4px;
+  animation: agentListPulse 1.4s ease-in-out infinite;
+}
+
+@keyframes agentListPulse {
+  0%, 100% { opacity: 0.5; }
+  50%      { opacity: 1; }
+}
+
+.card-icon-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  border-radius: inherit;
+  display: block;
+}
+
+.agent-list-card-foot {
+  display: flex;
+  gap: 14px;
+  margin-top: 16px;
+  padding-top: 14px;
+  border-top: 1px dashed rgba(102, 126, 234, 0.15);
+  flex-wrap: wrap;
+}
+
+.agent-list-stat {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 12px;
+  color: #64748b;
+  font-weight: 500;
+}
+
+.agent-list-stat-icon {
+  font-size: 13px;
+  line-height: 1;
+  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'Twemoji Mozilla', sans-serif;
+}
+
+.agent-list-status {
+  text-align: center;
+  padding: 60px 20px;
+  font-size: 14px;
+  color: #94a3b8;
+  font-weight: 500;
+}
+
+.agent-list-error {
+  color: #dc2626;
+}
+
+.agent-list-empty {
+  color: #94a3b8;
+}
+
+@media (max-width: 720px) {
+  .agent-list-filters {
+    flex-direction: column;
+    align-items: stretch;
+  }
+  .agent-list-result-count {
+    margin-left: 0;
+    text-align: right;
+  }
+}
+
+/* 状态 */
+.plaza-loading,
+.plaza-empty,
+.plaza-error {
+  padding: 30px 20px;
+  text-align: center;
+  font-size: 13px;
+  color: #999;
+}
+
+.plaza-error {
+  color: #dc2626;
+}
+
+/* 行点击与校验中状态 */
+.plaza-table-row {
+  cursor: pointer;
+  user-select: none;
+}
+
+.plaza-row-loading {
+  pointer-events: none;
+  opacity: 0.7;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.04) 100%) !important;
+}
+
+.plaza-row-checking {
+  color: #667eea;
+  font-size: 12px;
+  font-weight: 500;
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.plaza-row-checking::before {
+  content: '';
+  width: 10px;
+  height: 10px;
+  border: 2px solid rgba(102, 126, 234, 0.3);
+  border-top-color: #667eea;
+  border-radius: 50%;
+  animation: plazaSpin 0.8s linear infinite;
+}
+
+@keyframes plazaSpin {
+  to { transform: rotate(360deg); }
+}
+
+/* 响应式 */
+@media (max-width: 1200px) {
+  .plaza-section-lists {
+    grid-template-columns: 1fr;
+  }
+  .plaza-system-grid {
+    grid-template-columns: repeat(6, 1fr);
+  }
+}
+
+@media (max-width: 768px) {
+  .plaza-system-grid {
+    grid-template-columns: repeat(4, 1fr);
+  }
+  .plaza-hero {
+    flex-direction: column;
+    gap: 20px;
+    text-align: center;
+  }
+  .plaza-hero-title {
+    font-size: 28px;
+  }
+}
+
+.plaza-container {
+  gap: 24px;
+}
+
+.plaza-hero.arco-card {
+  border-color: #e5e6eb;
+  border-radius: 4px;
+  background: #ffffff;
+  box-shadow: none;
+  overflow: hidden;
+}
+
+.plaza-hero .arco-card-body {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 24px;
+  padding: 24px 28px;
+}
+
+.plaza-hero::before {
+  display: none;
+}
+
+.plaza-hero-content {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-hero-compact .plaza-hero-title.arco-typography {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  gap: 10px;
+  margin: 0 0 8px;
+  color: #1d2129;
+  font-size: 28px;
+  font-weight: 600;
+  line-height: 36px;
+  background: none;
+  -webkit-text-fill-color: currentColor;
+  letter-spacing: 0;
+  min-width: 0;
+  max-width: 100%;
+}
+
+.plaza-hero-icon {
+  width: 36px;
+  height: 36px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 4px;
+  background: #e8f3ff;
+  color: #165dff;
+  font-size: 20px;
+  flex-shrink: 0;
+}
+
+.plaza-hero-desc.arco-typography {
+  display: block;
+  max-width: 760px;
+  color: #4e5969;
+  font-size: 14px;
+  line-height: 22px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-hero-stats {
+  display: flex;
+  gap: 12px;
+  flex-shrink: 0;
+}
+
+.plaza-hero-stats div {
+  min-width: 98px;
+  padding: 12px 14px;
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+  background: #f7f8fa;
+  overflow: hidden;
+}
+
+.plaza-hero-stats strong {
+  display: block;
+  color: #165dff;
+  font-size: 22px;
+  font-weight: 600;
+  line-height: 28px;
+}
+
+.plaza-hero-stats span {
+  color: #86909c;
+  font-size: 12px;
+  line-height: 20px;
+}
+
+.plaza-section-lists {
+  gap: 16px;
+}
+
+.plaza-list-card.arco-card {
+  border-color: #e5e6eb;
+  border-radius: 4px;
+  background: #ffffff;
+  box-shadow: none;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-list-card .arco-card-body {
+  padding: 0;
+}
+
+.plaza-list-header {
+  min-height: 48px;
+  padding: 12px 16px;
+  background: #ffffff;
+  border-bottom: 1px solid #f2f3f5;
+  gap: 8px;
+  overflow: hidden;
+}
+
+.plaza-list-title {
+  min-width: 0;
+  color: #1d2129;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-title-icon {
+  width: 24px;
+  height: 24px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 4px;
+  background: #f2f3f5;
+  font-size: 14px;
+  flex-shrink: 0;
+}
+
+.plaza-title-badge {
+  color: #86909c;
+  font-size: 12px;
+  font-weight: 400;
+  white-space: nowrap;
+}
+
+.plaza-list-more.arco-btn-text {
+  height: 24px;
+  padding: 0 6px;
+  color: #165dff;
+  border-radius: 4px;
+  font-size: 12px;
+  flex-shrink: 0;
+}
+
+.plaza-list-more.arco-btn-text:hover {
+  background: #e8f3ff;
+  color: #4080ff;
+  transform: none;
+}
+
+.plaza-list {
+  max-height: 420px;
+}
+
+.plaza-table-row {
+  min-height: 58px;
+  gap: 10px;
+  padding: 12px 16px;
+  border-bottom: 1px solid #f2f3f5;
+  background: #ffffff;
+  transition: background-color 0.2s ease;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-table-row:hover {
+  background: #f7f8fa;
+}
+
+.plaza-row-rank {
+  width: 22px;
+  color: #86909c;
+  font-size: 12px;
+  font-weight: 600;
+  font-family: inherit;
+}
+
+.plaza-table-row:nth-child(1) .plaza-row-rank,
+.plaza-table-row:nth-child(2) .plaza-row-rank,
+.plaza-table-row:nth-child(3) .plaza-row-rank {
+  color: #165dff;
+}
+
+.plaza-row-icon {
+  width: 34px;
+  height: 34px;
+  border-radius: 4px;
+  font-size: 18px;
+  overflow: hidden;
+}
+
+.plaza-row-info {
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-row-name.arco-typography {
+  display: block;
+  color: #1d2129;
+  font-size: 14px;
+  font-weight: 600;
+  line-height: 22px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-row-extra {
+  max-width: 150px;
+  color: #86909c;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-row-extra > * {
+  min-width: 0;
+  max-width: 100%;
+}
+
+.plaza-extra-author-block {
+  overflow: hidden;
+}
+
+.plaza-extra-username {
+  color: #4e5969;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-extra-time,
+.plaza-extra-heat-badge {
+  display: inline-flex;
+  align-items: center;
+  max-width: 100%;
+  border-radius: 4px;
+  font-weight: 500;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-heat-normal,
+.plaza-time-近三天,
+.plaza-time-一周内 {
+  color: #86909c;
+  background: #f2f3f5;
+}
+
+.plaza-heat-top,
+.plaza-time-今日 {
+  color: #165dff;
+  background: #e8f3ff;
+  box-shadow: none;
+}
+
+.plaza-time-昨日 {
+  color: #4080ff;
+  background: #f2f7ff;
+  border: 0;
+}
+
+.plaza-heat-top-1,
+.plaza-heat-top-2,
+.plaza-heat-top-3 {
+  background: #fff7e8;
+  color: #ff7d00;
+  box-shadow: none;
+}
+
+.plaza-status,
+.plaza-loading,
+.plaza-empty,
+.plaza-error {
+  min-height: 180px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px;
+  color: #86909c;
+}
+
+.plaza-section-header {
+  padding: 0;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-section-title.arco-typography,
+.plaza-section-title {
+  min-width: 0;
+  margin: 0;
+  color: #1d2129;
+  font-size: 20px;
+  font-weight: 600;
+  line-height: 28px;
+  background: none;
+  -webkit-text-fill-color: currentColor;
+  letter-spacing: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-system-grid {
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 16px;
+  padding: 0;
+  border: 0;
+  border-radius: 0;
+  background: transparent;
+  box-shadow: none;
+}
+
+.plaza-system-card.arco-card {
+  min-height: 150px;
+  border-color: #e5e6eb;
+  border-radius: 4px;
+  background: #ffffff;
+  box-shadow: none;
+  cursor: pointer;
+  transition: border-color 0.2s ease, background-color 0.2s ease;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-system-card.arco-card:hover {
+  border-color: #bedaff;
+  background: #fbfdff;
+  box-shadow: none;
+  transform: none;
+}
+
+.plaza-system-card .arco-card-body {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 10px;
+  padding: 16px;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-system-card .plaza-system-icon {
+  width: 44px;
+  height: 44px;
+  border-radius: 4px;
+  background: #e8f3ff;
+  color: #165dff;
+  box-shadow: none;
+  font-size: 22px;
+  overflow: hidden;
+}
+
+.plaza-system-item:hover .plaza-system-icon {
+  transform: none;
+}
+
+.plaza-system-name.arco-typography {
+  display: block;
+  width: 100%;
+  color: #1d2129;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-system-desc.arco-typography {
+  display: -webkit-box;
+  width: 100%;
+  margin: 0;
+  color: #4e5969;
+  font-size: 13px;
+  line-height: 20px;
+  overflow: hidden;
+  word-break: break-word;
+}
+
+.plaza-system-status {
+  min-height: 160px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #e5e6eb;
+  border-radius: 4px;
+  background: #ffffff;
+}
+
+.plaza-agent-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+  gap: 16px;
+}
+
+.plaza-agent-card.arco-card {
+  border-color: #e5e6eb;
+  border-radius: 4px;
+  background: #ffffff;
+  box-shadow: none;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-agent-card.arco-card:hover {
+  border-color: #bedaff;
+  box-shadow: none;
+}
+
+.plaza-agent-card .arco-card-body {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  min-height: 178px;
+  padding: 16px;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-agent-card-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-agent-card-header > div:last-child {
+  min-width: 0;
+  overflow: hidden;
+}
+
+.plaza-agent-card-icon {
+  width: 40px;
+  height: 40px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  border-radius: 4px;
+  background: #e8f3ff;
+  color: #165dff;
+  font-size: 20px;
+  overflow: hidden;
+}
+
+.plaza-agent-card-title.arco-typography {
+  display: block;
+  color: #1d2129;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-agent-card-description.arco-typography {
+  margin: 0;
+  color: #4e5969;
+  font-size: 13px;
+  line-height: 20px;
+  overflow: hidden;
+  word-break: break-word;
+}
+
+.plaza-agent-card-tags {
+  display: flex;
+  gap: 8px;
+  margin-top: auto;
+  flex-wrap: wrap;
+  max-height: 24px;
+  overflow: hidden;
+}
+
+.plaza-agent-card-tags .arco-tag {
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.plaza-row-loading {
+  background: #f2f7ff !important;
+}
+
+.plaza-row-checking {
+  color: #165dff;
+}
+
+.plaza-row-checking::before {
+  border-color: rgba(22, 93, 255, 0.25);
+  border-top-color: #165dff;
+}
+
+@media (max-width: 1200px) {
+  .plaza-system-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (max-width: 768px) {
+  .plaza-hero .arco-card-body {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .plaza-hero-stats {
+    width: 100%;
+  }
+
+  .plaza-hero-stats div {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .plaza-system-grid {
+    grid-template-columns: 1fr;
+  }
+}

+ 492 - 0
src/pages/Plaza.jsx

@@ -0,0 +1,492 @@
+import { useState, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { Button, Card, Empty, Spin, Tag, Tooltip, Typography } from '@arco-design/web-react'
+import { getRecommendedApps, getHottestApps, getLatestApps, getAgentPros, getAppApiKey } from '@/service/business'
+import { API_BASE_URL } from '@/config'
+
+// 把后端返回的 icon 路径拼成完整可访问 URL
+//   - 已经是 http(s):// 开头 → 原样返回
+//   - 以 / 开头(相对路径)→ 前面拼上 API_BASE_URL
+//   - 其它(名字/emoji)→ 原样返回
+const buildIconUrl = (icon) => {
+  if (!icon) return ''
+  if (/^https?:\/\//i.test(icon)) return icon
+  if (icon.startsWith('/')) return `${API_BASE_URL}${icon}`
+  return icon
+}
+import emojiMartData from '@emoji-mart/data'
+import './Plaza.css'
+
+// 检查是否是 Unicode emoji 字符
+const isNativeEmoji = (str) => {
+  if (!str) return false
+  return /[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(str)
+}
+
+// 通过 emoji-mart data 把 emoji 名称转换为 native Unicode 字符
+const emojiNameToNative = (name) => {
+  if (!name) return '🤖'
+  const emoji = emojiMartData.emojis?.[name]
+  return emoji?.skins?.[0]?.native || '🤖'
+}
+
+// 把后端 icon 字段统一转换为 native Unicode emoji
+const resolveIcon = (icon) => {
+  if (!icon) return '🤖'
+  if (isNativeEmoji(icon)) return icon
+  return emojiNameToNative(icon)
+}
+
+// 数字格式化:19820431 -> "19,820,431"
+const formatNumber = (num) => {
+  if (!num && num !== 0) return '0'
+  return Number(num).toLocaleString('en-US')
+}
+
+// 时间戳转 "03/03 14:30" 格式
+const formatDate = (timestamp) => {
+  if (!timestamp) return ''
+  const date = new Date(timestamp * 1000)
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  const hours = String(date.getHours()).padStart(2, '0')
+  const minutes = String(date.getMinutes()).padStart(2, '0')
+  return `${month}/${day} ${hours}:${minutes}`
+}
+
+// 时间分级: 今日 / 昨日 / 仅三天(3天内) / 具体时间(MM/DD HH:mm)
+//   同一份逻辑同时被"推荐智能体"和"最新智能体"使用
+const formatRelativeTime = (timestamp) => {
+  if (!timestamp) return ''
+  const ts = timestamp * 1000
+
+  const startOfToday = new Date()
+  startOfToday.setHours(0, 0, 0, 0)
+  const startOfYesterday = new Date(startOfToday)
+  startOfYesterday.setDate(startOfYesterday.getDate() - 1)
+  const startOfThreeDaysAgo = new Date(startOfToday)
+  startOfThreeDaysAgo.setDate(startOfThreeDaysAgo.getDate() - 3)
+
+  if (ts >= startOfToday.getTime()) return '今日'
+  if (ts >= startOfYesterday.getTime()) return '昨日'
+  if (ts >= startOfThreeDaysAgo.getTime()) return '近三天'
+  return formatDate(timestamp)
+}
+
+const OverflowTip = ({ content, children }) => (
+  <Tooltip content={content || '-'} position="top" mini>
+    {children}
+  </Tooltip>
+)
+
+// 模块级守卫:useRef 在 React.StrictMode dev 二次挂载时会被重置,
+// 改用模块级变量可以跨过 StrictMode 的"卸载→重挂载"。
+// 卸载时通过 cleanup 把 flag 清零,保证离开页面后再回来仍能重新请求。
+let plazaInitStarted = false
+
+function Plaza() {
+  const navigate = useNavigate()
+  const [hotModels, setHotModels] = useState([])      // 热门大模型
+  const [hotAgents, setHotAgents] = useState([])       // 热门智能体
+  const [newAgents, setNewAgents] = useState([])       // 最新智能体
+  const [systemAgents, setSystemAgents] = useState([]) // 系统智能体(Agent Pro)
+  const [loading, setLoading] = useState({ model: true, hot: true, latest: true, system: true })
+  const [error, setError] = useState({})
+  const [clickingId, setClickingId] = useState(null)   // 正在请求权限的智能体 id
+
+  useEffect(() => {
+    if (plazaInitStarted) return
+    plazaInitStarted = true
+    fetchRecommended()   // 推荐智能体:GET /business/apps/recommended
+    fetchHot()           // 热门智能体:GET /business/apps/hottest
+    fetchLatest()        // 最新智能体:GET /business/apps/latest
+    fetchAgentPros()     // 系统智能体:GET /business/agent-pros
+    return () => {
+      // 离开 Plaza 后再回来,重新允许拉数据
+      plazaInitStarted = false
+    }
+  }, [])
+
+  // 点击智能体:调用权限接口,根据 has_permission 跳转
+  const handleAgentClick = async (agent) => {
+    // 调试:打印 agent 完整信息
+    console.log('[handleAgentClick] agent →', agent)
+    if (!agent?.id || clickingId === agent.id) return
+    setClickingId(agent.id)
+    try {
+      const resp = await getAppApiKey(agent.id)
+      console.log('[handleAgentClick] getAppApiKey resp →', resp)
+      // 兼容多种返回结构:Dify 不同版本可能返回不同格式
+      const apiKey = resp?.api_key
+        ?? resp?.data?.api_key
+        ?? resp?.key
+        ?? resp?.data?.key
+        ?? resp?.data
+      console.log('[handleAgentClick] parsed apiKey →', apiKey)
+      if (!apiKey) {
+        throw new Error('getAppApiKey 未返回 api_key(请把上面 resp 贴给开发者)')
+      }
+      // ... 原有逻辑继续
+      const hasPermission = resp?.has_permission ?? resp?.data?.has_permission
+      if (hasPermission) {
+        navigate(`/agents/execute/${agent.id}`, { state: { apiKey, agent } })
+      } else {
+        navigate(`/agents/apply/${agent.id}`, { state: { agent } })
+      }
+    } catch (err) {
+      console.error('[handleAgentClick] error →', err)
+      setClickingId(null)
+    }
+    try {
+      const res = await getAppApiKey(agent.id)
+      const payload = res?.data ?? res ?? {}
+      if (payload.has_permission) {
+        navigate(`/agents/execute/${agent.id}`, { state: { agent, apiKey: payload.api_key || '' } })
+      } else {
+        navigate(`/agents/apply/${agent.id}`, { state: { agent } })
+      }
+    } catch (e) {
+      // 接口异常时默认跳转到申请页
+      navigate(`/agents/apply/${agent.id}`, { state: { agent, error: e?.message || '权限校验失败' } })
+    } finally {
+      setClickingId(null)
+    }
+  }
+
+  const handleData = (data) => {
+    return (data?.data || data || []).map(item => ({
+      id: item.id,
+      name: item.name || '',
+      desc: item.description || '',
+      icon: resolveIcon(item.icon),
+      iconBackground: item.icon_background || '#FFEAD5',
+      heat: item.heat || item.call_count,
+      created_at: item.created_at,
+      // 最新智能体的"上架时间"独立字段,广场最新区展示用它
+      listed_at: item.listed_at ?? item.created_at,
+      author: item.author_name || item.created_by || item.creator || '',
+      mode: item.mode
+    }))
+  }
+
+  const fetchRecommended = async () => {
+    try {
+      const res = await getRecommendedApps()
+      setHotModels(handleData(res))
+    } catch (e) {
+      setError(prev => ({ ...prev, model: e.message || '加载失败' }))
+    } finally {
+      setLoading(prev => ({ ...prev, model: false }))
+    }
+  }
+
+  const fetchHot = async () => {
+    try {
+      const res = await getHottestApps()
+      setHotAgents(handleData(res))
+    } catch (e) {
+      setError(prev => ({ ...prev, hot: e.message || '加载失败' }))
+    } finally {
+      setLoading(prev => ({ ...prev, hot: false }))
+    }
+  }
+
+  const fetchLatest = async () => {
+    try {
+      const res = await getLatestApps()
+      setNewAgents(handleData(res))
+    } catch (e) {
+      setError(prev => ({ ...prev, latest: e.message || '加载失败' }))
+    } finally {
+      setLoading(prev => ({ ...prev, latest: false }))
+    }
+  }
+
+  // 系统智能体
+  const handleSystemAgentData = (data) => {
+    // 兼容 { data: [...] } / 直接数组 / 其它
+    const list = data?.data || data || []
+    return list.map(item => {
+      // icon 兼容:
+      //   icon_type === 'image' → 用 item.icon 当成相对/绝对路径,前面拼 API_BASE_URL
+      //   icon_type === 'emoji' (或没声明) → 用 item.icon 当 emoji mart 名字或 native emoji
+      const iconType = item.icon_type || 'emoji'
+      let iconValue = '🤖'
+      if (iconType === 'image') {
+        const raw = item.icon || item.logo || ''
+        iconValue = { type: 'image', url: buildIconUrl(raw) }
+      } else if (typeof item.icon === 'string') {
+        iconValue = item.icon
+      }
+      return {
+        id: item.id,
+        name: item.name || '',
+        desc: item.description || '',
+        icon: iconValue,
+        iconType,
+        iconBackground: item.icon_background || '#FFEAD5',
+        // source_type / source_app_id 用于点击时按 Layout 同样的规则分流
+        sourceType: item.source_type,
+        sourceAppId: item.source_app_id,
+      }
+    })
+  }
+
+  // 点击系统智能体卡片:按 source_type 分流
+  //   - external → ExternalMock 占位页
+  //   - internal / 其它 → 用 source_app_id(兜底 target_id)走权限校验后跳 execute/apply
+  const handleSystemAgentClick = async (agent) => {
+    if (!agent?.id || clickingId === agent.id) return
+    setClickingId(agent.id)
+    try {
+      if (agent.sourceType === 'external') {
+        navigate(`/agents/external/${agent.id}`, { state: { agent } })
+        return
+      }
+      const appId = agent.sourceAppId || agent.id
+      const res = await getAppApiKey(appId)
+      const payload = res?.data ?? res ?? {}
+      if (payload.has_permission) {
+        navigate(`/agents/execute/${appId}`, {
+          state: { agent: { ...agent, id: appId }, apiKey: payload.api_key || '' },
+        })
+      } else {
+        navigate(`/agents/apply/${appId}`, { state: { agent: { ...agent, id: appId } } })
+      }
+    } catch (e) {
+      navigate(`/agents/apply/${agent.sourceAppId || agent.id}`, {
+        state: { agent, error: e?.message || '权限校验失败' },
+      })
+    } finally {
+      setClickingId(null)
+    }
+  }
+
+  const fetchAgentPros = async () => {
+    try {
+      const res = await getAgentPros()
+      // 广场只展示前 6 个作为精选,完整列表在 AgentList(/agents)里
+      setSystemAgents(handleSystemAgentData(res).slice(0, 6))
+    } catch (e) {
+      setError(prev => ({ ...prev, system: e.message || '加载失败' }))
+    } finally {
+      setLoading(prev => ({ ...prev, system: false }))
+    }
+  }
+
+  const renderAgentRow = (agent, idx, rightInfo) => (
+    <div
+      key={agent.id}
+      className={`plaza-table-row ${clickingId === agent.id ? 'plaza-row-loading' : ''}`}
+      onClick={() => handleAgentClick(agent)}
+    >
+      <div className="plaza-row-rank">{idx + 1}</div>
+      <div
+        className="plaza-row-icon plaza-list-icon-native"
+        style={{ background: agent.iconBackground }}
+      >
+        {agent.icon}
+      </div>
+      <div className="plaza-row-info">
+        <OverflowTip content={agent.name}>
+          <Typography.Text className="plaza-row-name">{agent.name}</Typography.Text>
+        </OverflowTip>
+      </div>
+      <div className="plaza-row-extra">
+        {clickingId === agent.id ? <span className="plaza-row-checking">校验中…</span> : rightInfo}
+      </div>
+    </div>
+  )
+
+  const renderListState = (type, list, renderContent) => {
+    if (loading[type]) return <div className="plaza-status"><Spin size={20} tip="加载中..." /></div>
+    if (error[type]) return <div className="plaza-status"><Empty description={error[type]} /></div>
+    if (list.length === 0) return <div className="plaza-status"><Empty description="暂无数据" /></div>
+    return renderContent()
+  }
+
+  return (
+    <div className="plaza-container">
+      <Card className="plaza-hero plaza-hero-compact" bordered>
+        <div className="plaza-hero-content">
+          <Typography.Title heading={3} className="plaza-hero-title">
+            <span className="plaza-hero-icon">🤖</span>
+            智能体广场
+          </Typography.Title>
+          <OverflowTip content="汇聚平台所有公开 AI 智能体,支持一键预览、体验、复用与收藏,实现优质智能体能力共享、快速赋能业务落地">
+            <Typography.Text className="plaza-hero-desc">汇聚平台所有公开 AI 智能体,支持一键预览、体验、复用与收藏,实现优质智能体能力共享、快速赋能业务落地</Typography.Text>
+          </OverflowTip>
+        </div>
+        <div className="plaza-hero-stats">
+          <div><strong>{hotModels.length + hotAgents.length + newAgents.length}</strong><span>广场推荐</span></div>
+          <div><strong>{systemAgents.length}</strong><span>系统精选</span></div>
+        </div>
+      </Card>
+
+      <div className="plaza-section plaza-section-lists">
+        <Card className="plaza-list-card" bordered>
+          <div className="plaza-list-header">
+            <span className="plaza-list-title">
+              <span className="plaza-title-icon">👍</span>推荐智能体
+            </span>
+          </div>
+          <div className="plaza-list">
+            {renderListState('model', hotModels, () => hotModels.map((agent, idx) => {
+                const relTime = formatRelativeTime(agent.created_at)
+                return renderAgentRow(agent, idx,
+                  <span className="plaza-extra-author-block">
+                    {agent.author && (
+                      <OverflowTip content={agent.author}>
+                        <span className="plaza-extra-username">{agent.author}</span>
+                      </OverflowTip>
+                    )}
+                    <span className={`plaza-extra-time plaza-time-${relTime}`}>{relTime}</span>
+                  </span>
+                )
+              }))}
+          </div>
+        </Card>
+
+        <Card className="plaza-list-card" bordered>
+          <div className="plaza-list-header">
+            <span className="plaza-list-title">
+              <span className="plaza-title-icon">🔥</span>热门智能体<span className="plaza-title-badge">(近7d)</span>
+            </span>
+            <Button
+              type="text"
+              size="mini"
+              className="plaza-list-more"
+              onClick={() => navigate('/agents?type=hot')}
+            >查看更多</Button>
+          </div>
+          <div className="plaza-list">
+            {renderListState('hot', hotAgents, () => hotAgents.map((agent, idx) => renderAgentRow(agent, idx,
+                <span className={`plaza-extra-heat-badge ${idx < 3 ? `plaza-heat-top plaza-heat-top-${idx + 1}` : 'plaza-heat-normal'}`}>
+                  {idx < 3 && <span className="plaza-heat-fire">🔥</span>}
+                  {formatNumber(agent.heat)}
+                </span>
+              )))}
+          </div>
+        </Card>
+
+        <Card className="plaza-list-card" bordered>
+          <div className="plaza-list-header">
+            <span className="plaza-list-title">
+              <span className="plaza-title-icon">✨</span>最新智能体
+            </span>
+            <Button
+              type="text"
+              size="mini"
+              className="plaza-list-more"
+              onClick={() => navigate('/agents?type=latest')}
+            >查看更多</Button>
+          </div>
+          <div className="plaza-list">
+            {renderListState('latest', newAgents, () => newAgents.map((agent, idx) => {
+                const relTime = formatRelativeTime(agent.listed_at)
+                return renderAgentRow(agent, idx,
+                  <span className={`plaza-extra-time plaza-time-${relTime}`}>
+                    {relTime}
+                  </span>
+                )
+              }))}
+          </div>
+        </Card>
+      </div>
+
+      <div className="plaza-section">
+        <div className="plaza-section-header">
+          <Typography.Title heading={5} className="plaza-section-title">系统智能体</Typography.Title>
+          <Button
+            type="text"
+            size="small"
+            className="plaza-list-more"
+            onClick={() => navigate('/agents?type=system')}
+          >查看更多</Button>
+        </div>
+        <div className="plaza-system-grid">
+          {loading.system ? (
+            <div className="plaza-system-status plaza-system-loading"><Spin size={20} tip="加载中..." /></div>
+          ) : error.system ? (
+            <div className="plaza-system-status plaza-system-error"><Empty description={error.system} /></div>
+          ) : systemAgents.length === 0 ? (
+            <div className="plaza-system-status plaza-system-empty"><Empty description="暂无系统智能体" /></div>
+          ) : (
+            <>
+              {systemAgents.map(agent => {
+                const isImageIcon = agent.icon && typeof agent.icon === 'object' && agent.icon.type === 'image'
+                return (
+                  <Card
+                    key={agent.id}
+                    className="plaza-system-item plaza-system-card"
+                    onClick={() => handleSystemAgentClick(agent)}
+                    title={agent.desc || agent.name}
+                    bordered
+                  >
+                    <div
+                      className="plaza-system-icon"
+                      style={isImageIcon ? { background: agent.iconBackground || '#FFEAD5' } : undefined}
+                    >
+                      {isImageIcon ? (
+                        <img src={agent.icon.url} alt={agent.name} className="plaza-system-icon-img" />
+                      ) : (
+                        resolveIcon(agent.icon)
+                      )}
+                    </div>
+                    <OverflowTip content={agent.name}>
+                      <Typography.Text className="plaza-system-name">{agent.name}</Typography.Text>
+                    </OverflowTip>
+                    <OverflowTip content={agent.desc || '暂无介绍'}>
+                      <Typography.Paragraph className="plaza-system-desc" ellipsis={{ rows: 2 }}>{agent.desc || '暂无介绍'}</Typography.Paragraph>
+                    </OverflowTip>
+                  </Card>
+                )
+              })}
+            </>
+          )}
+        </div>
+      </div>
+
+      <div className="plaza-section">
+        <div className="plaza-section-header">
+          <Typography.Title heading={5} className="plaza-section-title">智能体列表</Typography.Title>
+        </div>
+        <div className="plaza-agent-grid">
+          {[
+            { id: 1, name: '代码助手 Pro', description: '智能代码生成、调试和优化,支持多种编程语言。', icon: '💻', tags: ['开发', '代码'] },
+            { id: 2, name: '文档生成器', description: '自动生成技术文档、API文档和项目文档。', icon: '📚', tags: ['文档', '写作'] },
+            { id: 3, name: '数据分析大师', description: '智能数据分析、可视化和报告生成。', icon: '📊', tags: ['数据', '分析'] },
+            { id: 4, name: '内容创作专家', description: 'AI驱动的内容创作,支持文章、博客、营销文案。', icon: '✍️', tags: ['内容', '营销'] },
+            { id: 5, name: '智能客服', description: '7x24小时智能客服,自动回答用户问题。', icon: '🎧', tags: ['客服'] },
+            { id: 6, name: '翻译官', description: '多语言智能翻译,保持语义准确和语境连贯。', icon: '🌍', tags: ['翻译'] },
+            { id: 7, name: '图像识别', description: '智能图像识别,支持物体检测、人脸识别。', icon: '🖼️', tags: ['图像'] },
+            { id: 8, name: '语音助手', description: '智能语音识别与合成,准确率高达 99%。', icon: '🎙️', tags: ['语音'] }
+          ].map(agent => (
+            <Card key={agent.id} className="plaza-agent-card" bordered hoverable>
+              <div className="plaza-agent-card-header">
+                <div className="plaza-agent-card-icon">{agent.icon}</div>
+                <div>
+                  <OverflowTip content={agent.name}>
+                    <Typography.Text className="plaza-agent-card-title">{agent.name}</Typography.Text>
+                  </OverflowTip>
+                </div>
+              </div>
+              <OverflowTip content={agent.description}>
+                <Typography.Paragraph className="plaza-agent-card-description" ellipsis={{ rows: 2 }}>{agent.description}</Typography.Paragraph>
+              </OverflowTip>
+              <div className="plaza-agent-card-tags">
+                {agent.tags.map((tag, idx) => (
+                  <OverflowTip key={idx} content={tag}>
+                    <Tag color="arcoblue">{tag}</Tag>
+                  </OverflowTip>
+                ))}
+              </div>
+            </Card>
+          ))}
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default Plaza

+ 73 - 0
src/service/account.js

@@ -0,0 +1,73 @@
+import { get, post, put, del } from './base'
+
+// 账户 API Keys 类型
+export const AccountApiKeyStatus = {
+  ACTIVE: 'active',
+  INACTIVE: 'inactive'
+}
+
+// 获取用户信息
+export const useUserProfile = () => {
+  return get('/account/profile')
+}
+
+// 更新用户名
+export const updateAccountName = (name) => {
+  return post('/account/name', { body: { name } })
+}
+
+// 更新头像
+export const updateAccountAvatar = (avatar) => {
+  return post('/account/avatar', { body: { avatar } })
+}
+
+// 更新界面语言
+export const updateInterfaceLanguage = (interface_language) => {
+  return post('/account/interface-language', { body: { interface_language } })
+}
+
+// 更新主题
+export const updateInterfaceTheme = (interface_theme) => {
+  return post('/account/interface-theme', { body: { interface_theme } })
+}
+
+// 更新时区
+export const updateTimezone = (timezone) => {
+  return post('/account/timezone', { body: { timezone } })
+}
+
+// 更新密码
+export const updatePassword = (password, new_password, repeat_new_password) => {
+  return post('/account/password', { 
+    body: { 
+      password, 
+      new_password, 
+      repeat_new_password 
+    } 
+  })
+}
+
+// 获取账户 API Keys
+export const useAccountApiKeys = () => {
+  return get('/account/api-keys')
+}
+
+// 创建账户 API Key
+export const useCreateAccountApiKey = (body) => {
+  return post('/account/api-keys', body)
+}
+
+// 更新账户 API Key
+export const useUpdateAccountApiKey = (id, body) => {
+  return put(`/account/api-keys/${id}`, body)
+}
+
+// 删除账户 API Key
+export const useDeleteAccountApiKey = (id) => {
+  return del(`/account/api-keys/${id}`)
+}
+
+// 退出登录
+export const useLogout = () => {
+  return post('/logout')
+}

+ 51 - 0
src/service/auth.js

@@ -0,0 +1,51 @@
+import { API_PREFIX } from '@/config'
+
+export const login = (data) => {
+  // 登录是公共接口,直接使用原生 fetch
+  return fetch(`${API_PREFIX}/login`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    credentials: 'include',
+    body: JSON.stringify(data),
+  }).then(response => response.json())
+}
+
+export const logout = () => {
+  return fetch(`${API_PREFIX}/logout`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    credentials: 'include',
+  }).then(response => response.json())
+}
+
+/**
+ * 用 refresh_token 刷新 access_token。
+ * Dify 后端在响应里会通过 Set-Cookie 下发新的 access_token / refresh_token,
+ * 浏览器自动覆盖同名 cookie,无需前端处理。
+ * 成功返回 { result: 'success' };失败抛错。
+ */
+export const refreshToken = () => {
+  return fetch(`${API_PREFIX}/refresh-token`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    credentials: 'include',
+  }).then(async response => {
+    if (!response.ok) {
+      const err = await response.json().catch(() => ({}))
+      throw { code: err.code || response.status, message: err.message || '刷新 token 失败' }
+    }
+    const data = await response.json()
+    // 后端在 body 里也返回了 access_token,存到 localStorage 覆盖旧的
+    const newToken = data?.data?.access_token || data?.access_token || data?.token
+    if (newToken) {
+      try { localStorage.setItem('access_token', newToken) } catch (_) {}
+    }
+    return data
+  })
+}

+ 188 - 0
src/service/base.js

@@ -0,0 +1,188 @@
+import { API_PREFIX, PUBLIC_API_PREFIX } from '@/config'
+import { refreshToken } from './auth'
+import { triggerLogout } from '@/utils/auth'
+
+const CSRF_COOKIE_NAME = 'csrf_token'
+const CSRF_HEADER_NAME = 'X-CSRF-Token'
+const DEFAULT_REQUEST_TIMEOUT = 30000
+
+let refreshInFlight = null
+
+function getCsrfToken() {
+  const cookies = document.cookie.split(';')
+  const csrfCookie = cookies.find(c => c.trim().startsWith(CSRF_COOKIE_NAME + '='))
+  if (csrfCookie) {
+    return csrfCookie.split('=')[1]
+  }
+  return ''
+}
+
+function getAccessToken() {
+  try {
+    return localStorage.getItem('access_token') || ''
+  } catch (_) {
+    return ''
+  }
+}
+
+function buildAuthHeaders(headers = {}) {
+  const nextHeaders = {
+    'Content-Type': 'application/json',
+    [CSRF_HEADER_NAME]: getCsrfToken(),
+    ...headers,
+  }
+  const accessToken = getAccessToken()
+  if (accessToken && !nextHeaders.Authorization) {
+    nextHeaders.Authorization = `Bearer ${accessToken}`
+  }
+  return nextHeaders
+}
+
+async function ensureFreshToken() {
+  if (refreshInFlight) return refreshInFlight
+  refreshInFlight = refreshToken()
+  try {
+    return await refreshInFlight
+  } finally {
+    refreshInFlight = null
+  }
+}
+
+async function request(url, options = {}, isPublicAPI = false, _isRetry = false) {
+  const { timeout = DEFAULT_REQUEST_TIMEOUT, signal: externalSignal, ...fetchOptions } = options
+  const controller = new AbortController()
+  let timeoutId
+  let didTimeout = false
+  const abortFromExternalSignal = () => controller.abort(externalSignal.reason)
+
+  if (externalSignal?.aborted) {
+    abortFromExternalSignal()
+  } else if (externalSignal) {
+    externalSignal.addEventListener('abort', abortFromExternalSignal, { once: true })
+  }
+
+  if (timeout > 0) {
+    timeoutId = window.setTimeout(() => {
+      didTimeout = true
+      controller.abort()
+    }, timeout)
+  }
+
+  const headers = buildAuthHeaders(options.headers)
+  const baseUrl = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
+  const fullUrl = url.startsWith('http') ? url : `${baseUrl}${url}`
+
+  try {
+    const response = await fetch(fullUrl, {
+      ...fetchOptions,
+      headers,
+      credentials: 'include',
+      signal: controller.signal,
+    })
+
+    if (response.status === 401 && !_isRetry && !isPublicAPI) {
+      try {
+        await ensureFreshToken()
+        return await request(url, options, isPublicAPI, true)
+      } catch (refreshErr) {
+        triggerLogout()
+        throw { code: 401, message: 'Login expired, please sign in again' }
+      }
+    }
+
+    if (!response.ok) {
+      const errorData = await response.json().catch(() => ({}))
+      throw { code: errorData.code || response.status, message: errorData.message || 'Request failed' }
+    }
+
+    return response.json()
+  } catch (error) {
+    if (didTimeout) {
+      throw { code: 'REQUEST_TIMEOUT', message: 'Request timed out, please try again later' }
+    }
+    if (error?.name === 'AbortError') {
+      throw { code: 'REQUEST_ABORTED', message: 'Request was aborted' }
+    }
+    throw error
+  } finally {
+    if (timeoutId) window.clearTimeout(timeoutId)
+    if (externalSignal) {
+      externalSignal.removeEventListener('abort', abortFromExternalSignal)
+    }
+  }
+}
+
+export function get(url, params = {}, isPublicAPI = false) {
+  let queryString = ''
+  if (typeof params === 'string') {
+    queryString = params
+  } else if (params && typeof params === 'object') {
+    const entries = []
+    for (const [k, v] of Object.entries(params)) {
+      if (v === undefined || v === null) continue
+      entries.push([k, String(v)])
+    }
+    queryString = new URLSearchParams(entries).toString()
+  }
+  const fullUrl = queryString ? `${url}?${queryString}` : url
+  return request(fullUrl, { method: 'GET' }, isPublicAPI)
+}
+
+export async function downloadFile(url, isPublicAPI = false) {
+  const baseUrl = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
+  const fullUrl = url.startsWith('http') ? url : `${baseUrl}${url}`
+  const response = await fetch(fullUrl, {
+    method: 'GET',
+    credentials: 'include',
+    headers: buildAuthHeaders(),
+  })
+
+  if (!response.ok) {
+    let msg = `Download failed (${response.status})`
+    try {
+      const j = await response.json()
+      if (j?.message) msg = j.message
+    } catch (_) {
+      /* ignore */
+    }
+    throw { code: response.status, message: msg }
+  }
+
+  const disp = response.headers.get('content-disposition') || ''
+  let filename = ''
+  const rfc5987 = disp.match(/filename\*\s*=\s*(?:UTF-8'')?([^;]+)/i)
+  if (rfc5987) {
+    filename = decodeURIComponent(rfc5987[1].replace(/^["']|["']$/g, '').trim())
+  } else {
+    const fnMatch = disp.match(/filename\s*=\s*["']?([^"';]+)["']?/i)
+    if (fnMatch) filename = fnMatch[1].trim()
+  }
+
+  const blob = await response.blob()
+  return { blob, filename }
+}
+
+export function post(url, body = {}, isPublicAPI = false) {
+  return request(url, {
+    method: 'POST',
+    body: JSON.stringify(body),
+  }, isPublicAPI)
+}
+
+export function put(url, body = {}, isPublicAPI = false) {
+  return request(url, {
+    method: 'PUT',
+    body: JSON.stringify(body),
+  }, isPublicAPI)
+}
+
+export function del(url, isPublicAPI = false) {
+  return request(url, { method: 'DELETE' }, isPublicAPI)
+}
+
+export function patch(url, body = {}, isPublicAPI = false) {
+  return request(url, {
+    method: 'PATCH',
+    body: JSON.stringify(body),
+  }, isPublicAPI)
+}

+ 146 - 0
src/service/business.js

@@ -0,0 +1,146 @@
+import { get, post, del, downloadFile } from './base'
+
+// 推荐智能体
+export const getRecommendedApps = () => {
+  return get('/business/apps/recommended')
+}
+
+// 热门智能体
+export const getHottestApps = () => {
+  return get('/business/apps/hottest')
+}
+
+// 最新智能体
+export const getLatestApps = () => {
+  return get('/business/apps/latest')
+}
+
+// 系统智能体(Agent Pro)列表
+//   GET /business/agent-pros
+//   响应字段: id, source_type, source_app_id, name, description, mode,
+//            icon_type, icon, icon_background, logo, recommend_score,
+//            request_method, request_url, request_headers, request_body_template,
+//            parameter_schema, external_config, created_at, updated_at
+export const getAgentPros = () => {
+  return get('/business/agent-pros')
+}
+
+// 当前用户的推荐智能体(混合类型:app / agent_pro)
+//   GET /business/apps/my-recommended
+//   响应字段(BusinessMixedAgentResponse):
+//     target_type: "app" | "agent_pro"
+//     target_id:   智能体 id(可能为 app id 或 agent_pro id)
+//     name, description, mode, icon_type, icon, icon_background,
+//     source_type / source_name / source_app_id (agent_pro 才有),
+//     logo, marked_at, created_at, updated_at
+//   返回最多 10 条:用户标记的优先,然后用 recommend_score 排序的 agent_pro 补足
+export const getMyRecommendedApps = () => {
+  return get('/business/apps/my-recommended')
+}
+
+// ===== 智能体标记 / 取消标记 =====
+//   单个用户最多标记 10 个智能体(BUSINESS_AGENT_MARK_LIMIT)
+//   target_type: 'app' | 'agent_pro'
+
+// 列出当前用户已标记的智能体
+//   GET /business/agent-marks
+export const getAgentMarks = () => {
+  return get('/business/agent-marks')
+}
+
+// 标记智能体
+//   POST /business/agent-marks
+//   body: { target_type: 'app' | 'agent_pro', target_id: string(uuid) }
+//   返回: { result, status: 'marked', mark: BusinessMixedAgentResponse }
+//   错误码: mark-limit-exceeded (已超过 10 个)
+export const markAgent = ({ target_type, target_id }) => {
+  return post('/business/agent-marks', { target_type, target_id })
+}
+
+// 取消标记智能体
+//   DELETE /business/agent-marks/<target_type>/<target_id>
+//   返回: { result: 'success', status: 'unmarked' }
+export const unmarkAgent = ({ target_type, target_id }) => {
+  return del(`/business/agent-marks/${target_type}/${target_id}`)
+}
+
+// 平台已上架智能体列表(带热度,可选标签过滤)
+export const getBusinessAppsList = (params = {}) => {
+  return get('/business/apps/list', params)
+}
+
+// 检查智能体 API Key 权限(用于判断是否可进入执行页)
+export const getAppApiKey = (appId) => {
+  return get(`/business/apps/${appId}/api-key`)
+}
+
+// 获取智能体详情(执行页用)
+export const getAppDetail = (appId) => {
+  return get(`/apps/${appId}`)
+}
+
+// 智能体基本信息(含 version / version_description / introduction / app_permission / is_listed)
+export const getAppBasicInfo = (appId) => {
+  return get(`/business/apps/${appId}/basic-info`)
+}
+
+// 获取当前用户对该智能体的权限与最新申请进度
+export const getAccessStatus = (appId) => {
+  return get(`/business/apps/${appId}/access-status`)
+}
+
+// 提交智能体使用申请
+export const createAccessRequest = (appId, reason) => {
+  return post(`/business/apps/${appId}/access-requests`, { reason })
+}
+
+// ===== 历史会话 =====
+
+// 当前用户的 API 会话列表(后端按 updated_at desc 排序)
+//   GET /business/conversations?page=1&limit=10
+//   响应字段(BusinessConversationResponse):
+//     id, app_id, app_name, name, summary, status, mode,
+//     from_source, from_end_user_id, from_end_user_session_id,
+//     message_count, created_at, updated_at
+//   返回: { data:[...], page, limit, total, has_more }
+//
+// 注意:后端 pydantic 在 Query 解析失败时(可能因 Flask-RESTX 的 args 序列化触发 list 形态),
+//   会报 "Input should be a valid integer [type=int_type, input_value=['1']]" 错误。
+//   解决方案:不传 page/limit(让后端用 default 兜底 page=1/limit=10),完全跳过 Query 解析。
+//   后续如果需要分页,在前端用 query 字符串形式手动传,不要用对象。
+export const getBusinessConversations = (params = {}) => {
+  // 不传 page/limit(让后端 pydantic 用 default 兜底 page=1/limit=10),
+  // 避免后端 Query 解析触发 "Input should be a valid integer [type=int_type, input_value=['1']]"
+  // 错误(已知后端 bug,Flask-RESTX args 序列化时会把值序列化成 list 形态)。
+  // 后续如需分页:在 schema 把 page/limit 标为 Optional,或后端改成 from_request 直接读 dict。
+  void params // 暂忽略,保持接口签名兼容
+  return get('/business/conversations')
+}
+
+// 单个会话的详情(带分页消息轮次)
+//   GET /business/conversations/<conversation_id>?page=1&limit=10
+//   响应: { conversation: {...}, messages: [...], page, limit, total, has_more }
+//   每条消息字段(BusinessConversationMessageResponse):
+//     id, conversation_id, query, answer, status, error,
+//     workflow_run_id, parent_message_id, created_at, updated_at
+export const getBusinessConversationDetail = (conversationId, params = {}) => {
+  return get(`/business/conversations/${conversationId}`, params)
+}
+
+// 下载单条消息的文档(Word / PDF)
+//   GET /business/messages/<message_id>/content/download?format=word|pdf
+//   后端 BusinessMessageContentDownloadQuery.format: Literal['word', 'pdf'],默认 word
+//   返回: send_file(... as_attachment=True, download_name='message-XXXXXXXX.{docx|pdf}')
+//   这里用 base.downloadFile 直接走 fetch(不走 JSON 解析),返回 { blob, filename }
+//
+// 注意:后端 pydantic Literal 类型在 query 解析失败时(Flask-RESTX args 序列化成 list 形态),
+//   会报 "Input should be 'word' or 'pdf' [type=literal_error, input_value=['word']]"。
+//   解决:format=word(默认)时**不传 query**,让后端用 default 兜底,完全跳过 query 解析;
+//        format=pdf 时再显式传,绕不开时才走 query(由后端 Literal 校验)。
+export const downloadMessageContent = async (messageId, format = 'word') => {
+  const safeFormat = format === 'pdf' ? 'pdf' : 'word'
+  const url = safeFormat === 'word'
+    ? `/business/messages/${messageId}/content/download`
+    : `/business/messages/${messageId}/content/download?format=${safeFormat}`
+  return downloadFile(url)
+}

+ 391 - 0
src/service/dify.js

@@ -0,0 +1,391 @@
+// Dify 应用级 Service API(/v1)封装
+// 鉴权:Authorization: Bearer <api_key>   —— api_key 来自 /business/apps/<id>/api-key
+// user 字段:'user=' + cookie 中的 access_token
+// 主要用于应用执行页:拉取应用信息 / 表单参数 / 触发流式执行
+import { DIFY_V1_PREFIX } from '@/config'
+
+// 登录后端通过 Set-Cookie 种在浏览器上的会话 token
+// 兼容多种存储位置和命名(不同 Dify 版本/部署方式可能不同)
+const ACCESS_TOKEN_KEYS = [
+  'access_token',
+  'console_token',
+  'webapp_access_token',
+  'token',
+  'auth_token',
+]
+
+// 无效 token 黑名单(之前测试时可能被存进 localStorage)
+const INVALID_TOKEN_VALUES = new Set([
+  'no_auth', 'null', 'undefined', 'guest', 'anonymous', 'false', '',
+])
+
+function isValidToken(v) {
+  if (!v) return false
+  if (INVALID_TOKEN_VALUES.has(v)) return false
+  if (v.length < 10) return false
+  // 允许 JWT 字符集:字母数字 + -_ + .(JWT 有 2 个点)+ =(base64 padding)
+  if (/^[\w\-./+=]+$/.test(v) === false) return false
+  return true
+}
+
+/**
+ * 从 localStorage / document.cookie 中读取 access_token。
+ * 优先级:localStorage(多种 key)→ cookie(多种 name)→ 生成 UUID 兜底
+ * 注意:HttpOnly cookie 不会被 document.cookie 列出,所以优先靠 localStorage。
+ */
+function getAccessTokenFromCookie() {
+  if (typeof window === 'undefined') return ''
+
+  const found = []
+
+  // 1) 优先从 localStorage 读(Dify 前端的常见做法,不受 HttpOnly 限制)
+  try {
+    for (const k of ACCESS_TOKEN_KEYS) {
+      const v = window.localStorage.getItem(k)
+      if (v != null) found.push({ source: 'ls:' + k, value: v, valid: isValidToken(v) })
+      if (isValidToken(v)) return v
+    }
+  } catch (_) { /* localStorage 不可用 */ }
+
+  // 2) 兜底:document.cookie(非 HttpOnly 的情况下能读到)
+  //    注意:必须按 ACCESS_TOKEN_KEYS 优先级查找,而不是按 cookie 字符串里的出现顺序,
+  //    否则 `token=no_auth` 这种垃圾值会先匹配上,把真正的 `access_token=eyJ...` 盖掉。
+  try {
+    if (document.cookie) {
+      const cookieMap = {}
+      for (const c of document.cookie.split(';')) {
+        const [k, ...rest] = c.trim().split('=')
+        if (k) cookieMap[k] = decodeURIComponent(rest.join('='))
+      }
+      for (const k of ACCESS_TOKEN_KEYS) {
+        const v = cookieMap[k]
+        if (v != null) {
+          found.push({ source: 'cookie:' + k, value: v, valid: isValidToken(v) })
+          if (isValidToken(v)) return v
+        }
+      }
+    }
+  } catch (_) { /* noop */ }
+
+  // 调试:把找到的所有 token 候选都打出来,一眼看清谁被过滤了
+  if (typeof console !== 'undefined') {
+    console.log('[getAccessTokenFromCookie] candidates →', found)
+  }
+
+  // 3) 最终兜底:生成 UUID 存到 localStorage
+  try {
+    let uuid = window.localStorage.getItem('dify_webapp_user_uuid')
+    if (!uuid || !isValidToken(uuid)) {
+      uuid = ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
+        (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4)).toString(16)
+      )
+      window.localStorage.setItem('dify_webapp_user_uuid', uuid)
+    }
+    return uuid
+  } catch (_) { /* noop */ }
+
+  return ''
+}
+
+/**
+ * 构造工作流请求体中的 user 字段
+ * 规则:'user=' + access_token (与 Dify 后端约定)
+ */
+export function buildWorkflowUser() {
+  const token = getAccessTokenFromCookie()
+  return token ? `user=${token}` : ''
+}
+
+/**
+ * 通用 Dify 鉴权 fetch
+ *
+ * **不发送 cookie**:Dify Service API(/v1/*)采用 Bearer token(Authorization 头)
+ *  + body 中的 user 字段双重鉴权,不需要 cookie。如果带 credentials: 'include',
+ *  浏览器会要求后端 preflight 返回 Access-Control-Allow-Credentials: true,
+ *  不配置就会触发 CORS 拒绝 → 真实请求不发。
+ *
+ * @param {string} url 完整 URL
+ * @param {string} apiKey 应用 API Key(来自 /business/apps/<id>/api-key)
+ * @param {object} options fetch 选项
+ * @returns {Promise<Response>} 原生 Response 对象
+ */
+async function difyFetch(url, apiKey, options = {}) {
+  const res = await fetch(url, {
+    ...options,
+    headers: {
+      'Content-Type': 'application/json',
+      'Authorization': `Bearer ${apiKey}`,
+      Accept: 'text/event-stream',
+      ...options.headers,
+    },
+    // 关键:omit 而不是 include,避免触发 Access-Control-Allow-Credentials 检查
+    credentials: 'omit',
+  })
+  if (!res.ok) {
+    const err = await res.json().catch(() => ({}))
+    throw {
+      code: err.code || res.status,
+      message: err.message || `Dify request failed (${res.status})`,
+      status: res.status
+    }
+  }
+  return res
+}
+
+// ===== 应用配置 =====
+
+async function difyFormFetch(url, apiKey, formData, options = {}) {
+  const { headers, ...restOptions } = options
+  const res = await fetch(url, {
+    ...restOptions,
+    method: restOptions.method || 'POST',
+    headers: {
+      'Authorization': `Bearer ${apiKey}`,
+      ...headers,
+    },
+    body: formData,
+    credentials: 'omit',
+  })
+  if (!res.ok) {
+    const err = await res.json().catch(() => ({}))
+    throw {
+      code: err.code || res.status,
+      message: err.message || `Dify request failed (${res.status})`,
+      status: res.status
+    }
+  }
+  return res
+}
+
+/**
+ * GET /v1/info - app basic info.
+ * Returns: { name, description, tags, mode, author_name }
+ */
+export const getDifyAppInfo = (apiKey, options = {}) => {
+  return difyFetch(`${DIFY_V1_PREFIX}/info`, apiKey, { method: 'GET', ...options }).then(r => r.json())
+}
+
+/**
+ * GET /v1/parameters  ——  应用输入参数(user_input_form 等)
+ * 返回:{ opening_statement, suggested_questions, user_input_form, file_upload, system_parameters, ... }
+ */
+export const getDifyAppParameters = (apiKey, options = {}) => {
+  return difyFetch(`${DIFY_V1_PREFIX}/parameters`, apiKey, { method: 'GET', ...options }).then(r => r.json())
+}
+
+/**
+ * GET /v1/site  ——  WebApp 设置
+ */
+export const getDifyAppSite = (apiKey, options = {}) => {
+  return difyFetch(`${DIFY_V1_PREFIX}/site`, apiKey, { method: 'GET', ...options }).then(r => r.json())
+}
+
+/**
+ * 获取应用完整信息:合并 /v1/info(基本元信息)+ /v1/site(图标、主题、站点信息)
+ * @param {string} apiKey 应用 API Key(来自 /business/apps/<id>/api-key)
+ * @returns {Promise<{
+ *   name, description, tags, mode, author_name,
+ *   title, icon_type, icon, icon_background, icon_url, chat_color_theme, show_workflow_steps
+ * }>}
+ *
+ * 即使其中一个失败(站点未配置等),也会尽量返回另一个的字段。
+ */
+export const getAppFullInfo = async (apiKey, options = {}) => {
+  const [infoRes, siteRes] = await Promise.allSettled([
+    getDifyAppInfo(apiKey, options),
+    getDifyAppSite(apiKey, options),
+  ])
+  const info = infoRes.status === 'fulfilled' ? (infoRes.value || {}) : {}
+  const site = siteRes.status === 'fulfilled' ? (siteRes.value || {}) : {}
+
+  return {
+    // ---- /v1/info ----
+    name: info.name || site.title || '智能体',
+    description: info.description || site.description || '',
+    tags: Array.isArray(info.tags) ? info.tags : [],
+    mode: info.mode || '',
+    author_name: info.author_name || '',
+    // ---- /v1/site ----
+    title: site.title || info.name || '智能体',
+    icon_type: site.icon_type || 'emoji',
+    icon: site.icon || '',
+    icon_background: site.icon_background || '#FFEAD5',
+    icon_url: site.icon_url || null,
+    chat_color_theme: site.chat_color_theme || '',
+    chat_color_theme_inverted: !!site.chat_color_theme_inverted,
+    show_workflow_steps: !!site.show_workflow_steps,
+    use_icon_as_answer_icon: !!site.use_icon_as_answer_icon,
+    copyright: site.copyright || '',
+    privacy_policy: site.privacy_policy || '',
+    custom_disclaimer: site.custom_disclaimer || '',
+    default_language: site.default_language || 'zh-Hans',
+    // ---- 错误聚合(供 UI 决策)----
+    _errors: {
+      info: infoRes.status === 'rejected' ? infoRes.reason?.message : null,
+      site: siteRes.status === 'rejected' ? siteRes.reason?.message : null,
+    },
+  }
+}
+
+// ===== 工作流执行 =====
+
+/**
+ * POST /v1/workflows/run  ——  流式执行工作流
+ *
+ * - Authorization:  Bearer <apiKey>           ← apiKey 来自 /business/apps/<id>/api-key
+ * - request body:   { inputs, response_mode: 'streaming', user: 'user=<access_token>' }
+ *
+ * @param {string} apiKey 应用 API Key
+ * @param {object} inputs 用户输入的 inputs
+ * @param {string} [user] 可选,user 字段。默认自动从 cookie 中读取 access_token 并拼接 'user=' 前缀
+ * @returns {Promise<Response>} 返回 SSE 流(不调用 .json())
+ */
+/**
+ * POST /files/upload - upload one file with multipart/form-data.
+ * Returns: { id, name, size, extension, mime_type, created_by, created_at, ... }
+ */
+export const uploadFile = (apiKey, file, user, options = {}) => {
+  const finalUser = user || buildWorkflowUser() || 'webapp-user'
+  const formData = new FormData()
+  formData.append('file', file)
+  formData.append('user', finalUser)
+
+  return difyFormFetch(`${DIFY_V1_PREFIX}/files/upload`, apiKey, formData, options).then(r => r.json())
+}
+
+export const runWorkflow = (apiKey, inputs, user, options = {}) => {
+  const finalUser = user || buildWorkflowUser() || 'webapp-user'
+  const body = {
+    inputs,
+    response_mode: 'streaming',
+    user: finalUser,
+  }
+  // 调试日志:开 dev 时直接看实际请求体结构
+  if (typeof console !== 'undefined') {
+    console.log('[runWorkflow] final body →', JSON.stringify(body))
+  }
+  return difyFetch(`${DIFY_V1_PREFIX}/workflows/run`, apiKey, {
+    ...options,
+    method: 'POST',
+    body: JSON.stringify(body),
+  })
+}
+
+// ===== 对话流执行 =====
+
+/**
+ * POST /v1/chat-messages  ——  向对话型应用发送消息(流式)
+ *
+ * - Authorization:  Bearer <apiKey>
+ * - request body:   {
+ *                       query,                     // 用户消息
+ *                       inputs,                    // 应用输入参数
+ *                       response_mode: 'streaming',
+ *                       conversation_id,           // 续传时填,否则留空字符串
+ *                       user: 'user=<access_token>',
+ *                       files?                     // 多模态文件
+ *                     }
+ *
+ * @param {string} apiKey 应用 API Key
+ * @param {object} params { query, inputs, conversation_id, files }
+ * @param {string} [user] 可选,user 字段。默认自动从 cookie 中读取 access_token 并拼接 'user=' 前缀
+ * @returns {Promise<Response>} 返回 SSE 流
+ */
+export const runChatMessage = (apiKey, params = {}, user, options = {}) => {
+  const finalUser = user || buildWorkflowUser() || 'webapp-user'
+  const body = {
+    query: params.query || '',
+    inputs: params.inputs || {},
+    response_mode: 'streaming',
+    conversation_id: params.conversation_id || '',
+    user: finalUser,
+  }
+  if (Array.isArray(params.files) && params.files.length > 0) {
+    body.files = params.files
+  }
+  if (typeof console !== 'undefined') {
+    console.log('[runChatMessage] final body →', JSON.stringify(body))
+  }
+  return difyFetch(`${DIFY_V1_PREFIX}/chat-messages`, apiKey, {
+    ...options,
+    method: 'POST',
+    body: JSON.stringify(body),
+  })
+}
+
+/**
+ * 解析 SSE(Server-Sent Events)流
+ * 自动忽略 ping 事件,逐个回调解析后的 JSON
+ * @param {Response} response fetch 响应对象(response.body.getReader())
+ * @param {(evt: object) => void} onEvent 每解析一个事件就回调
+ * @param {() => void} onDone 流结束时回调
+ * @param {(err: Error) => void} onError 出错时回调
+ * @param {{signal?: AbortSignal}} [options] 可选取消信号
+ * @returns {Promise<void>}
+ */
+export async function consumeSSE(response, onEvent, onDone, onError, options = {}) {
+  if (!response.body) {
+    onError?.(new Error('Response body is empty'))
+    return
+  }
+  const reader = response.body.getReader()
+  const cancelReader = () => reader.cancel().catch(() => {})
+  if (options.signal?.aborted) {
+    cancelReader()
+    return
+  }
+  options.signal?.addEventListener('abort', cancelReader, { once: true })
+  const decoder = new TextDecoder('utf-8')
+  let buffer = ''
+  try {
+    while (true) {
+      const { done, value } = await reader.read()
+      if (done) break
+      buffer += decoder.decode(value, { stream: true })
+
+      // SSE 以 \n\n 分隔事件
+      const parts = buffer.split('\n\n')
+      buffer = parts.pop() // 保留未完成的一段
+
+      for (const part of parts) {
+        const lines = part.split('\n')
+        for (const line of lines) {
+          const trimmed = line.trim()
+          if (!trimmed.startsWith('data:')) continue
+          const payload = trimmed.slice(5).trim()
+          if (!payload) continue
+          // 跳过 ping 心跳
+          if (payload === 'ping' || payload.includes('"event":"ping"')) continue
+          try {
+            const evt = JSON.parse(payload)
+            onEvent?.(evt)
+          } catch (e) {
+            // 忽略非 JSON 行
+            // console.warn('SSE parse failed:', e, payload)
+          }
+        }
+      }
+    }
+    // 处理残留 buffer
+    if (buffer.trim()) {
+      const lines = buffer.split('\n')
+      for (const line of lines) {
+        const trimmed = line.trim()
+        if (!trimmed.startsWith('data:')) continue
+        const payload = trimmed.slice(5).trim()
+        if (!payload || payload === 'ping' || payload.includes('"event":"ping"')) continue
+        try {
+          const evt = JSON.parse(payload)
+          onEvent?.(evt)
+        } catch (e) {}
+      }
+    }
+    onDone?.()
+  } catch (e) {
+    if (!options.signal?.aborted) {
+      onError?.(e)
+    }
+  } finally {
+    options.signal?.removeEventListener('abort', cancelReader)
+  }
+}

+ 42 - 0
src/utils/auth.js

@@ -0,0 +1,42 @@
+const LOGGED_IN_KEY = 'is_logged_in'
+const DEFAULT_LOGIN_PATH = '/login'
+
+export function setLoggedIn() {
+  localStorage.setItem(LOGGED_IN_KEY, 'true')
+}
+
+export function isLoggedIn() {
+  return localStorage.getItem(LOGGED_IN_KEY) === 'true'
+}
+
+export function clearLoggedIn() {
+  localStorage.removeItem(LOGGED_IN_KEY)
+}
+
+// 保持旧接口兼容
+export function clearTokens() {
+  clearLoggedIn()
+}
+
+// 检查是否已认证
+export function isAuthenticated() {
+  return isLoggedIn()
+}
+
+export function isTokenExpired() {
+  return false
+}
+
+/**
+ * 触发强制登出:
+ *   1. 清理本地登录态
+ *   2. 跳转登录页(用 location.href 触发整页刷新,保证所有内存状态被清空)
+ * 避免在 base.js 这种不依赖路由的纯工具层直接 import router。
+ */
+export function triggerLogout(redirectTo = DEFAULT_LOGIN_PATH) {
+  try { clearLoggedIn() } catch (_) { /* noop */ }
+  // 如果当前已经在登录页,就不再重复跳转
+  if (typeof window !== 'undefined' && window.location.pathname !== redirectTo) {
+    window.location.href = redirectTo
+  }
+}

+ 10 - 0
src/utils/emoji-mart.js

@@ -0,0 +1,10 @@
+import { init } from 'emoji-mart'
+import data from '@emoji-mart/data'
+
+let initialized = false
+
+export function setupEmojiMart() {
+  if (initialized) return
+  init({ data })
+  initialized = true
+}

+ 10 - 0
src/utils/encryption.js

@@ -0,0 +1,10 @@
+export function encryptPassword(password) {
+  try {
+    const utf8Bytes = new TextEncoder().encode(password)
+    const base64 = btoa(String.fromCharCode(...utf8Bytes))
+    return base64
+  } catch (error) {
+    console.error('Field encoding failed:', error)
+    throw new Error('Encoding failed. Please check your input.')
+  }
+}

+ 17 - 0
vite.config.js

@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+export default defineConfig({
+  plugins: [react()],
+  server: {
+    port: 6888,
+    open: true,
+    strictPort: true
+  },
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, './src')
+    }
+  }
+})

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä