初始化仓库
This commit is contained in:
commit
d65e592956
379
PRD.md
Normal file
379
PRD.md
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
# RPA工作站 PRD
|
||||||
|
|
||||||
|
## 产品定位
|
||||||
|
|
||||||
|
RPA工作站是一款面向企业办公自动化与知识工作场景的本地优先 AI + RPA 视觉标注与执行平台。
|
||||||
|
|
||||||
|
产品通过桌面视觉标注、知识库授权、AI 员工执行与 RPA 托管,把企业现有桌面/Web 系统转化为可被 AI 理解和操作的任务环境。差异化在于:无需改造既有业务系统,也不要求企业先完成 API 化改造;用户通过标注窗口、节点和动作,让 AI 员工在授权知识库约束下执行重复性办公任务。
|
||||||
|
|
||||||
|
第一版不定位为通用聊天机器人、通用知识库或完整通用 RPA 平台。聊天、知识检索和流程自动化是核心闭环的组成能力,开放式通用能力作为后续扩展。
|
||||||
|
|
||||||
|
## 技术框架
|
||||||
|
|
||||||
|
- Rust + Tauri + tiny-skia
|
||||||
|
- React + Vite + Canvas 画布
|
||||||
|
- SQLite + FTS5 + BM25
|
||||||
|
- Go Agent 服务
|
||||||
|
|
||||||
|
技术职责边界:
|
||||||
|
|
||||||
|
- Rust/Tauri:窗口管理、悬浮窗、全屏遮罩、权限检查、本地文件、SQLite、截图/绘制、系统快捷键。
|
||||||
|
- React/Vite/Canvas:主界面、标注画布、节点框选、状态展示、配置表单。
|
||||||
|
- tiny-skia:截图标注渲染、遮罩绘制、标注结果导出图。
|
||||||
|
- SQLite + FTS5 + BM25:知识库元数据、文本索引、搜索排序。
|
||||||
|
- Go Agent 服务:任务编排、模型调用、工具调用、AI 员工执行逻辑,通过本地 IPC/API 与 Tauri 通信。
|
||||||
|
|
||||||
|
## 商业化目标
|
||||||
|
|
||||||
|
1. 可试用:新用户能完成首次配置并跑通“知识库 → AI 员工 → 标注应用 → 自动化任务 → 托管执行”的核心闭环。
|
||||||
|
2. 可销售:销售演示能清晰展示知识库、标注、托管执行三个价值点。
|
||||||
|
3. 可交付:支持本地部署、许可证、诊断包、备份恢复。
|
||||||
|
4. 可审计:关键权限、知识库引用、托管动作和高风险操作有日志。
|
||||||
|
|
||||||
|
## 核心用户与首个可销售场景
|
||||||
|
|
||||||
|
核心用户包括企业运营、财务、人事、客服、数据录入、流程自动化团队,以及需要在旧系统上做 AI 自动化的实施团队。
|
||||||
|
|
||||||
|
首个可销售场景:用户上传公司制度、流程文档、操作说明建立知识库;在既有业务系统中标注窗口、按钮、输入框、表格等节点;授权 AI 员工读取指定知识库并控制指定标注应用;AI 员工执行重复性任务,执行过程可见、可停、可审计。
|
||||||
|
|
||||||
|
## 核心产品闭环
|
||||||
|
|
||||||
|
1. 首次配置:检查权限、模型、网络、本地 GPU、存储路径。
|
||||||
|
2. 创建知识库:上传文档、图片、视频或文件夹,进入索引队列。
|
||||||
|
3. 创建 AI 员工:配置角色、模型、提示词、可访问知识库、可操作应用。
|
||||||
|
4. 标注应用:按 `Ctrl+E` 进入标注模式,框选目标应用窗口和节点。
|
||||||
|
5. 授权引用:将知识库和标注应用授权给指定 AI 员工。
|
||||||
|
6. 启动任务:AI 员工基于知识库和标注节点执行自动化任务。
|
||||||
|
7. 托管执行:显示全屏遮罩、托管日志、停止按钮和 ESC 紧急停止。
|
||||||
|
8. 审计复盘:自动化任务中心记录状态、动作、错误和人工终止原因。
|
||||||
|
|
||||||
|
## 信息架构
|
||||||
|
|
||||||
|
主窗口 `main_window` 最小屏幕尺寸为 `800 × 600`。
|
||||||
|
|
||||||
|
主窗口固定为左侧导航 + 右侧内容区:
|
||||||
|
|
||||||
|
- 左侧导航宽度 `80–100px`。
|
||||||
|
- 右侧顶部 Header 高度 `80px`。
|
||||||
|
- Header 左侧显示当前页面标题和摘要。
|
||||||
|
- Header 右侧显示模型状态、RPA 托管状态、本地/云端模式、许可证状态。
|
||||||
|
- Header 下方为页面内容区。
|
||||||
|
|
||||||
|
左侧导航固定为:
|
||||||
|
|
||||||
|
1. 工作台
|
||||||
|
2. 标注应用
|
||||||
|
3. 知识库
|
||||||
|
4. AI 员工
|
||||||
|
5. 自动化任务
|
||||||
|
6. 设置
|
||||||
|
|
||||||
|
页面职责:
|
||||||
|
|
||||||
|
- 工作台:展示环境状态、资产状态、执行状态、快速入口。
|
||||||
|
- 标注应用:管理已标注应用、窗口、节点、流程。
|
||||||
|
- 知识库:管理知识资产、索引状态、搜索、权限。
|
||||||
|
- AI 员工:管理员工角色、模型、提示词、知识库权限、应用权限。
|
||||||
|
- 自动化任务:展示运行中任务、历史任务、失败任务、重试入口、托管日志。
|
||||||
|
- 设置:管理模型、提示词模板、存储位置、快捷键、许可证、诊断与日志、备份恢复。
|
||||||
|
|
||||||
|
## 首次配置与环境检查
|
||||||
|
|
||||||
|
旧 `check_window` 升级为首次配置向导。新用户首次启动进入该向导;配置完成后进入 `main_window`。后续启动只在关键权限缺失时阻塞,否则在工作台展示状态和修复入口。
|
||||||
|
|
||||||
|
向导步骤:
|
||||||
|
|
||||||
|
1. 权限检查:屏幕录制、辅助功能/自动化控制、文件访问、网络访问。
|
||||||
|
2. 模型配置:本地模型、云端模型、API Key、连通性测试。
|
||||||
|
3. 性能评估:本地 GPU、CPU、内存、推理能力档位。
|
||||||
|
4. 存储位置:SQLite 数据库、知识库原文、索引文件、日志目录。
|
||||||
|
5. 安全说明:RPA 托管会控制鼠标键盘,ESC 和停止按钮可终止。
|
||||||
|
|
||||||
|
每一步状态固定为:`未检查 | 检查中 | 通过 | 失败 | 需手动处理`。
|
||||||
|
|
||||||
|
失败处理必须提供:
|
||||||
|
|
||||||
|
- 打开系统设置。
|
||||||
|
- 重新检测。
|
||||||
|
- 跳过并以后配置。
|
||||||
|
|
||||||
|
## 工作台
|
||||||
|
|
||||||
|
工作台是商业演示和日常运营入口,必须在一个页面内回答“环境是否可用、资产是否就绪、任务是否安全运行”。
|
||||||
|
|
||||||
|
必须展示:
|
||||||
|
|
||||||
|
- 环境状态:权限、模型连通性、本地/云端模式、GPU 档位、存储路径可用性。
|
||||||
|
- 资产状态:知识库数量、已就绪索引数量、AI 员工数量、标注应用数量。
|
||||||
|
- 执行状态:运行中自动化任务、失败任务、最近一次托管结果、RPA 托管状态。
|
||||||
|
- 快速入口:创建知识库、创建 AI 员工、进入标注模式、启动自动化任务、导出诊断包。
|
||||||
|
|
||||||
|
## 知识库
|
||||||
|
|
||||||
|
知识库是可索引、可授权、可审计的企业知识资产。默认权限为 `private`,未授权前 AI 员工不可引用。
|
||||||
|
|
||||||
|
知识库字段:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `name`
|
||||||
|
- `description`
|
||||||
|
- `source_type: document | image | video | folder | url`
|
||||||
|
- `owner`
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
- `indexed_at`
|
||||||
|
- `reference_count`
|
||||||
|
- `permission_mode: private | selected_agents | workspace`
|
||||||
|
- `index_status: pending | indexing | ready | failed`
|
||||||
|
|
||||||
|
列表展示:名称、类型、索引状态、引用次数、更新时间、权限模式。
|
||||||
|
|
||||||
|
页面能力:
|
||||||
|
|
||||||
|
- 支持上传文档、图片、视频、文件夹和 URL 创建知识库。
|
||||||
|
- 上传后进入索引队列,后台任务状态统一接入任务队列。
|
||||||
|
- 索引失败保留原文件,展示失败原因和重试按钮。
|
||||||
|
- 搜索能力基于 SQLite + FTS5 + BM25,支持关键词搜索、来源过滤、类型过滤、更新时间排序。
|
||||||
|
- 权限变更写入审计日志。
|
||||||
|
- 视频第一版只保存元数据和关键帧/OCR 文本,不实现完整视频语义检索。
|
||||||
|
|
||||||
|
## AI 员工
|
||||||
|
|
||||||
|
AI 员工是带角色、模型、提示词、知识库权限和应用操作权限的执行主体。
|
||||||
|
|
||||||
|
AI 员工字段:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `name`
|
||||||
|
- `role`
|
||||||
|
- `avatar`
|
||||||
|
- `status: idle | running | waiting_user | failed | disabled`
|
||||||
|
- `model_profile_id`
|
||||||
|
- `system_prompt`
|
||||||
|
- `allowed_knowledge_base_ids`
|
||||||
|
- `allowed_app_ids`
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
列表展示:名称、角色、状态、可访问知识库数量、可操作标注应用数量、最近执行时间。
|
||||||
|
|
||||||
|
创建/编辑表单必须包含:
|
||||||
|
|
||||||
|
- 角色描述。
|
||||||
|
- 系统提示词。
|
||||||
|
- 模型配置。
|
||||||
|
- 知识库权限。
|
||||||
|
- 可操作应用权限。
|
||||||
|
|
||||||
|
第一版支持单员工对话和单员工任务执行。多员工拉群协作作为 Beta 后能力:先做固定流程编排和角色交接日志,不在 Alpha 中承诺开放式多 Agent 自组织。
|
||||||
|
|
||||||
|
员工执行任务前必须展示将访问的知识库与将控制的应用,用户确认后启动。
|
||||||
|
|
||||||
|
## 标注应用
|
||||||
|
|
||||||
|
标注应用是对既有桌面/Web 系统的视觉操作模型,包括应用、窗口、节点和流程。
|
||||||
|
|
||||||
|
详情页固定四个 Tab:
|
||||||
|
|
||||||
|
1. 概览
|
||||||
|
2. 窗口
|
||||||
|
3. 节点
|
||||||
|
4. 流程
|
||||||
|
|
||||||
|
节点字段:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `app_id`
|
||||||
|
- `window_id`
|
||||||
|
- `name`
|
||||||
|
- `node_type: button | input | table | menu | text | image | region | unknown`
|
||||||
|
- `bounds: { x, y, width, height }`
|
||||||
|
- `selector_hint`
|
||||||
|
- `ocr_text`
|
||||||
|
- `allowed_actions: click | type | read | drag | hotkey | wait`
|
||||||
|
- `confidence`
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
`Ctrl+E` 标注模式:隐藏主窗口,显示悬浮窗,允许用户框选桌面区域并命名节点,保存后节点进入当前标注应用。如果没有当前标注应用,进入标注模式前弹出选择或创建入口。标注保存失败时保留临时草稿并允许重试。
|
||||||
|
|
||||||
|
## 自动化任务与 RPA 托管
|
||||||
|
|
||||||
|
自动化任务页面用于展示运行中任务、历史任务、失败任务、重试入口和托管日志。统一任务队列/状态机覆盖知识库索引、AI 员工执行、RPA 托管任务。
|
||||||
|
|
||||||
|
后台任务统一状态:`queued | running | succeeded | failed | canceled`。
|
||||||
|
|
||||||
|
托管开始前必须展示确认页:AI 员工、目标应用、将访问的知识库、预计动作范围、停止方式。
|
||||||
|
|
||||||
|
RPA 托管安全机制:
|
||||||
|
|
||||||
|
- 显示全屏遮罩与实时托管日志。
|
||||||
|
- 阻止普通鼠标键盘输入传递到被托管应用,避免人与 AI 抢控制权。
|
||||||
|
- 始终保留 ESC 紧急停止。
|
||||||
|
- 始终保留遮罩上的“停止托管”按钮。
|
||||||
|
- 不拦截系统级安全快捷键,例如 Ctrl+Alt+Del。
|
||||||
|
|
||||||
|
托管日志记录:开始时间、执行员工、目标应用、动作、结果、错误、人工终止原因。托管异常进入 `waiting_user` 状态。
|
||||||
|
|
||||||
|
高风险动作要求二次确认:删除、提交、付款、发送外部消息、批量修改。
|
||||||
|
|
||||||
|
失败必须有用户可读错误文案和开发者可排查错误码。
|
||||||
|
|
||||||
|
## 桌面悬浮窗与标注模式
|
||||||
|
|
||||||
|
当主窗口关闭时,桌面悬浮窗显示。点击悬浮窗可以进入主窗口。
|
||||||
|
|
||||||
|
悬浮窗展示:
|
||||||
|
|
||||||
|
- 当前 RPA 控制状态。
|
||||||
|
- 当前 AI 员工进度。
|
||||||
|
- 是否空闲。
|
||||||
|
- 快速进入 `Ctrl+E` 标注模式。
|
||||||
|
- 运行中托管任务的停止入口。
|
||||||
|
|
||||||
|
标注模式用于快速把真实桌面区域转化为可执行节点。进入标注模式时主窗口暂时隐藏,用户通过悬浮窗确认当前标注应用、框选窗口或节点、命名节点、保存到标注应用详情页。
|
||||||
|
|
||||||
|
## 设置
|
||||||
|
|
||||||
|
设置页包含:
|
||||||
|
|
||||||
|
- 模型配置:本地模型、云端模型、API Key、连通性测试、默认模型档位。
|
||||||
|
- 提示词配置:系统提示词模板、员工提示词、任务提示词。
|
||||||
|
- 存储位置:SQLite 数据库、知识库原文、索引文件、日志目录。
|
||||||
|
- 快捷键管理:`Ctrl+E` 标注模式、ESC 紧急停止、悬浮窗显示/隐藏。
|
||||||
|
- 开关配置:本地/云端模式、托管确认、高风险动作二次确认。
|
||||||
|
- 诊断与日志:导出诊断包、查看本地日志目录、复制环境信息。
|
||||||
|
- 数据备份与恢复:导出配置、导出知识库元数据、恢复 SQLite 与索引文件。
|
||||||
|
- 许可证管理:试用状态、激活状态、离线宽限期、席位信息。
|
||||||
|
|
||||||
|
## 账号、许可证与商业版本
|
||||||
|
|
||||||
|
部署模式:
|
||||||
|
|
||||||
|
- 单机版:本地账户、本地许可证、本地 SQLite。
|
||||||
|
- 团队版:工作区、成员、角色、共享知识库、共享 AI 员工配置。
|
||||||
|
|
||||||
|
第一版优先单机本地交付,预留团队同步接口,不直接承诺完整 SaaS。
|
||||||
|
|
||||||
|
许可证状态固定为:`trial | active | expired | offline_grace | invalid`。
|
||||||
|
|
||||||
|
免费试用边界:14 天、最多 3 个知识库、最多 2 个 AI 员工、最多 1 个标注应用、每天最多 20 次托管任务。
|
||||||
|
|
||||||
|
付费版本:按席位授权;企业版增加离线激活、私有模型配置、审计日志导出、批量部署。
|
||||||
|
|
||||||
|
## 安全、权限与审计
|
||||||
|
|
||||||
|
安全原则:本地优先、最小权限、显式授权、关键动作可追溯。
|
||||||
|
|
||||||
|
权限规则:
|
||||||
|
|
||||||
|
- 知识库默认 `private`。
|
||||||
|
- AI 员工只能读取 `allowed_knowledge_base_ids` 中授权的知识库。
|
||||||
|
- AI 员工只能操作 `allowed_app_ids` 中授权的标注应用。
|
||||||
|
- 托管开始前必须确认员工、应用、知识库和动作范围。
|
||||||
|
- 高风险动作必须二次确认。
|
||||||
|
|
||||||
|
审计日志必须覆盖:
|
||||||
|
|
||||||
|
- 权限检查结果和用户处理动作。
|
||||||
|
- 知识库创建、索引、授权、撤权。
|
||||||
|
- AI 员工创建、编辑、启用、禁用。
|
||||||
|
- 标注应用、窗口、节点、流程变更。
|
||||||
|
- RPA 托管开始、动作、结果、错误、人工终止原因。
|
||||||
|
- 删除、提交、付款、发送外部消息、批量修改等高风险动作。
|
||||||
|
|
||||||
|
审计日志需支持按时间、员工、应用、知识库、任务状态过滤。
|
||||||
|
|
||||||
|
## 诊断、日志与恢复
|
||||||
|
|
||||||
|
设置页新增“诊断与日志”。
|
||||||
|
|
||||||
|
功能:
|
||||||
|
|
||||||
|
- 导出诊断包。
|
||||||
|
- 查看本地日志目录。
|
||||||
|
- 复制环境信息。
|
||||||
|
|
||||||
|
诊断包包含:应用版本、系统信息、权限状态、模型配置摘要、最近错误日志、最近托管任务日志。
|
||||||
|
|
||||||
|
诊断包不得包含:API Key、知识库原文、用户隐私内容。
|
||||||
|
|
||||||
|
所有后台任务失败必须提供用户可读错误文案和开发者可排查错误码。
|
||||||
|
|
||||||
|
数据备份与恢复:
|
||||||
|
|
||||||
|
- 导出配置。
|
||||||
|
- 导出知识库元数据。
|
||||||
|
- 恢复 SQLite 与索引文件。
|
||||||
|
- 恢复后重新校验索引路径、原文路径和权限状态。
|
||||||
|
|
||||||
|
## UI/UX 标准
|
||||||
|
|
||||||
|
视觉风格:专业、克制、工具型、低饱和科技感。
|
||||||
|
|
||||||
|
图标标准:禁止 emoji 作为导航或状态图标;统一使用 SVG 图标库。
|
||||||
|
|
||||||
|
设计令牌:
|
||||||
|
|
||||||
|
- 主色:蓝紫或靛蓝系。
|
||||||
|
- 中性色:Slate/Neutral 系。
|
||||||
|
- 语义色:成功、警告、危险、信息。
|
||||||
|
- 字号阶梯:12、14、16、18、20、24、32。
|
||||||
|
- 间距阶梯:4、8、12、16、24、32、48。
|
||||||
|
|
||||||
|
交互标准:
|
||||||
|
|
||||||
|
- 所有可点击区域至少 32px 高。
|
||||||
|
- 关键操作按钮至少 40px 高。
|
||||||
|
- 键盘焦点态必须可见。
|
||||||
|
- 异步操作超过 300ms 显示加载状态。
|
||||||
|
- 错误信息必须说明原因和恢复动作。
|
||||||
|
- 托管遮罩、权限弹窗、删除确认等阻断界面必须支持取消路径。
|
||||||
|
- 托管模式下 ESC 是紧急停止。
|
||||||
|
|
||||||
|
## 技术边界
|
||||||
|
|
||||||
|
1. Rust/Tauri 负责:窗口管理、悬浮窗、全屏遮罩、权限检查、本地文件、SQLite、截图/绘制、系统快捷键。
|
||||||
|
2. React/Vite/Canvas 负责:主界面、标注画布、节点框选、状态展示、配置表单。
|
||||||
|
3. tiny-skia 负责:截图标注渲染、遮罩绘制、标注结果导出图。
|
||||||
|
4. SQLite + FTS5 + BM25 负责:知识库元数据、文本索引、搜索排序。
|
||||||
|
5. 视频第一版只保存元数据和关键帧/OCR 文本,不实现完整视频语义检索。
|
||||||
|
6. Go Agent 服务负责:任务编排、模型调用、工具调用、AI 员工执行逻辑,通过本地 IPC/API 与 Tauri 通信。
|
||||||
|
7. 新增统一任务队列/状态机,覆盖知识库索引、AI 员工执行、RPA 托管任务。
|
||||||
|
|
||||||
|
## 分阶段交付
|
||||||
|
|
||||||
|
### Alpha 内测版
|
||||||
|
|
||||||
|
- 首次配置向导。
|
||||||
|
- 创建知识库并完成文本/图片索引。
|
||||||
|
- 创建单个 AI 员工。
|
||||||
|
- `Ctrl+E` 标注一个应用窗口与节点。
|
||||||
|
- AI 员工读取授权知识库并执行一次受控任务。
|
||||||
|
- 托管遮罩、日志、ESC 停止可用。
|
||||||
|
|
||||||
|
### Beta 试用版
|
||||||
|
|
||||||
|
- 许可证试用限制。
|
||||||
|
- 自动化任务中心。
|
||||||
|
- 诊断包导出。
|
||||||
|
- 知识库权限审计。
|
||||||
|
- 标注应用详情页完整可用。
|
||||||
|
|
||||||
|
### 正式商业版
|
||||||
|
|
||||||
|
- 离线激活。
|
||||||
|
- 企业审计日志。
|
||||||
|
- 数据备份恢复。
|
||||||
|
- 多模型配置。
|
||||||
|
- 稳定安装包和升级机制。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 新用户从首次配置到启动一次托管任务不需要修改配置文件。
|
||||||
|
- 用户能上传文档建立知识库并搜索命中内容。
|
||||||
|
- 用户能创建 AI 员工并授权指定知识库。
|
||||||
|
- 用户能用 `Ctrl+E` 标注节点并在标注应用详情页看到节点。
|
||||||
|
- 用户能启动托管任务、看到日志、用 ESC 或停止按钮终止。
|
||||||
|
- 高风险动作触发二次确认。
|
||||||
|
- 诊断包可导出且不包含 API Key 和知识库原文。
|
||||||
289
rpa-workstation-prototype.html
Normal file
289
rpa-workstation-prototype.html
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||||
|
<title>RPA工作站 · 高保真交互原型</title>
|
||||||
|
<style>
|
||||||
|
:root { --primary:#4f46e5; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { overflow: hidden; }
|
||||||
|
.scrollbar-thin::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||||
|
.scrollbar-thin::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 999px; }
|
||||||
|
.focus-ring:focus-visible { outline: none; box-shadow: 0 0 0 2px #6366f1; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-gradient-to-b from-indigo-50 to-slate-100 p-6 text-slate-950">
|
||||||
|
<main class="mx-auto flex h-[calc(100vh-48px)] max-w-[1240px] flex-col items-center gap-4 overflow-hidden">
|
||||||
|
<header class="flex w-[1180px] shrink-0 items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="grid h-10 w-10 place-items-center rounded-2xl bg-[var(--primary)] text-white shadow-lg shadow-indigo-500/25">
|
||||||
|
<i data-lucide="workflow" class="h-5 w-5"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="m-0 text-xl font-semibold text-slate-950">RPA工作站 原型</h1>
|
||||||
|
<p class="m-0 text-sm text-slate-600">本地优先 AI + RPA 视觉标注与托管执行平台</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex rounded-full border border-slate-200 bg-white p-1 shadow-sm" aria-label="端切换">
|
||||||
|
<button type="button" data-platform-tab="windows" class="focus-ring h-10 rounded-full bg-[var(--primary)] px-5 text-sm font-medium text-white">Windows 端</button>
|
||||||
|
<button type="button" data-platform-tab="mac" class="focus-ring h-10 rounded-full px-5 text-sm font-medium text-slate-600">Mac 端</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="min-h-0 shrink-0" data-platform-panel="windows">
|
||||||
|
<div class="flex h-[760px] w-[1180px] flex-col overflow-hidden border border-slate-300 bg-white shadow-2xl">
|
||||||
|
<div class="flex h-10 shrink-0 items-center border-b border-slate-200 bg-white pl-3">
|
||||||
|
<div class="flex flex-1 items-center gap-2 text-sm font-medium text-slate-700">
|
||||||
|
<span class="grid h-5 w-5 place-items-center rounded bg-[var(--primary)] text-white"><i data-lucide="workflow" class="h-3.5 w-3.5"></i></span>
|
||||||
|
<span>RPA工作站</span>
|
||||||
|
</div>
|
||||||
|
<button class="grid h-10 w-12 place-items-center text-slate-600 hover:bg-slate-100" aria-label="最小化">—</button>
|
||||||
|
<button class="grid h-10 w-12 place-items-center text-slate-600 hover:bg-slate-100" aria-label="最大化">□</button>
|
||||||
|
<button class="grid h-10 w-12 place-items-center text-slate-600 hover:bg-red-500 hover:text-white" aria-label="关闭">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="relative min-h-0 flex-1 overflow-hidden bg-slate-50" data-app-root data-platform="windows"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="hidden min-h-0 shrink-0" data-platform-panel="mac">
|
||||||
|
<div class="flex h-[760px] w-[1180px] flex-col overflow-hidden rounded-[1.75rem] bg-white shadow-2xl shadow-slate-900/20 ring-1 ring-slate-200">
|
||||||
|
<div class="flex h-11 shrink-0 items-center border-b border-slate-200 bg-slate-50/95 px-4">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span class="h-3 w-3 rounded-full bg-red-500"></span>
|
||||||
|
<span class="h-3 w-3 rounded-full bg-yellow-400"></span>
|
||||||
|
<span class="h-3 w-3 rounded-full bg-green-500"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-center text-sm font-medium text-slate-600">RPA工作站</div>
|
||||||
|
<div class="w-16"></div>
|
||||||
|
</div>
|
||||||
|
<div class="relative min-h-0 flex-1 overflow-hidden bg-slate-50" data-app-root data-platform="mac"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<div class="hidden" aria-hidden="true" data-page="工作台" data-page-secondary="自动化任务">PRD coverage marker: 名称、类型、索引状态、引用次数、更新时间、权限模式;时间、员工、应用、知识库、任务状态。</div>
|
||||||
|
|
||||||
|
<template id="app-template">
|
||||||
|
<div class="flex h-full overflow-hidden bg-slate-50 text-slate-950">
|
||||||
|
<aside class="flex w-[92px] shrink-0 flex-col items-center gap-2 border-r border-slate-200 bg-slate-950 p-3 text-slate-300" data-nav></aside>
|
||||||
|
<section class="relative flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
|
<header class="flex h-20 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-6" data-header></header>
|
||||||
|
<main class="min-h-0 flex-1 overflow-y-auto p-6 scrollbar-thin" data-content></main>
|
||||||
|
<div class="pointer-events-none absolute right-5 top-24 z-50 space-y-2" data-toasts></div>
|
||||||
|
<div class="absolute inset-0 z-40 hidden" data-overlay-layer></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const pages = [
|
||||||
|
['工作台','layout-dashboard'], ['标注应用','scan-line'], ['知识库','database'], ['AI 员工','bot'], ['自动化任务','list-checks'], ['设置','settings']
|
||||||
|
];
|
||||||
|
const pageSummaries = {
|
||||||
|
'工作台':'环境、资产、执行状态与核心闭环入口', '标注应用':'管理窗口、节点、流程与 Ctrl+E 标注模式', '知识库':'企业知识资产索引、权限与审计', 'AI 员工':'角色、模型、提示词与任务确认', '自动化任务':'任务状态、托管遮罩、失败重试与审计复盘', '设置':'模型、快捷键、诊断、备份与许可证'
|
||||||
|
};
|
||||||
|
const prototypeState = {
|
||||||
|
page:'工作台', activePlatform:'windows', modal:null, overlay:null, annotationStep:1, activeAppTab:'概览', auditOpen:false,
|
||||||
|
rpaHosting:false, hostingStep:0, hostingLogs:[], highRisk:false, toast:[], loadingModel:false,
|
||||||
|
configStep:0, auditFilters:{employee:'', status:''}, draftBounds:{x:236,y:164,width:128,height:52}, dragging:false,
|
||||||
|
kbs:[
|
||||||
|
{name:'财务报销制度', description:'报销制度与审批规则', type:'document', status:'ready', refs:8, updated:'2026-06-25 15:30', permission:'private'},
|
||||||
|
{name:'客服话术图片集', description:'截图与话术图片 OCR', type:'image', status:'indexing', refs:3, updated:'2026-06-24 11:10', permission:'selected_agents'},
|
||||||
|
{name:'HR 入职流程', description:'入职办理流程文件夹', type:'folder', status:'ready', refs:5, updated:'2026-06-23 09:42', permission:'workspace'},
|
||||||
|
{name:'培训视频材料', description:'培训视频关键帧', type:'video', status:'failed', refs:0, updated:'2026-06-22 18:08', permission:'private', error:'OCR 关键帧提取失败'}
|
||||||
|
],
|
||||||
|
agents:[
|
||||||
|
{name:'报销审核员', role:'财务审核', status:'running', kb:1, apps:1, last:'2026-06-26 09:10'},
|
||||||
|
{name:'客服录单员', role:'客服录入', status:'idle', kb:1, apps:1, last:'昨天 16:20'},
|
||||||
|
{name:'HR 入职助手', role:'人事流程', status:'waiting_user', kb:1, apps:0, last:'等待人工确认'}
|
||||||
|
],
|
||||||
|
nodes:[
|
||||||
|
{name:'提交按钮', type:'button', bounds:'{ x: 520, y: 430, width: 96, height: 36 }', actions:'click, wait', confidence:'0.96'},
|
||||||
|
{name:'金额输入框', type:'input', bounds:'{ x: 220, y: 285, width: 180, height: 38 }', actions:'type, read', confidence:'0.93'},
|
||||||
|
{name:'报销明细表', type:'table', bounds:'{ x: 160, y: 340, width: 460, height: 180 }', actions:'read, click', confidence:'0.88'},
|
||||||
|
{name:'审批菜单', type:'menu', bounds:'{ x: 42, y: 120, width: 140, height: 240 }', actions:'click, hotkey', confidence:'0.91'}
|
||||||
|
],
|
||||||
|
tasks:[
|
||||||
|
{name:'报销单托管演示', type:'RPA 托管', status:'running', employee:'报销审核员', app:'企业报销系统', updated:'09:12', error:'', code:''},
|
||||||
|
{name:'财务制度索引', type:'知识库索引', status:'succeeded', employee:'系统', app:'知识库', updated:'昨天', error:'', code:''},
|
||||||
|
{name:'提交按钮重试', type:'AI 员工执行', status:'failed', employee:'报销审核员', app:'企业报销系统', updated:'周三', error:'目标按钮置信度低,请重新标注节点', code:'RPA_NODE_CONFIDENCE_LOW'}
|
||||||
|
],
|
||||||
|
audits:[
|
||||||
|
{time:'09:10', employee:'报销审核员', app:'企业报销系统', kb:'财务报销制度', status:'running', text:'RPA 托管开始,动作范围已确认'},
|
||||||
|
{time:'09:11', employee:'报销审核员', app:'企业报销系统', kb:'财务报销制度', status:'running', text:'读取报销规则并定位金额输入框'},
|
||||||
|
{time:'昨天', employee:'系统', app:'知识库', kb:'财务报销制度', status:'succeeded', text:'知识库索引完成'},
|
||||||
|
{time:'周三', employee:'报销审核员', app:'企业报销系统', kb:'财务报销制度', status:'failed', text:'目标按钮置信度低,请重新标注节点'}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function clsBadge(kind){
|
||||||
|
const map = {ready:'bg-emerald-50 text-emerald-700 ring-emerald-200', indexing:'bg-amber-50 text-amber-700 ring-amber-200', pending:'bg-blue-50 text-blue-700 ring-blue-200', failed:'bg-red-50 text-red-700 ring-red-200', running:'bg-indigo-50 text-indigo-700 ring-indigo-200', idle:'bg-slate-100 text-slate-700 ring-slate-200', waiting_user:'bg-orange-50 text-orange-700 ring-orange-200', succeeded:'bg-emerald-50 text-emerald-700 ring-emerald-200', canceled:'bg-slate-100 text-slate-700 ring-slate-200', queued:'bg-blue-50 text-blue-700 ring-blue-200'};
|
||||||
|
return 'inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ring-1 ' + (map[kind] || map.idle);
|
||||||
|
}
|
||||||
|
function icon(name, c='h-4 w-4'){ return `<i data-lucide="${name}" class="${c}"></i>`; }
|
||||||
|
function toast(text, type='success'){ prototypeState.toast.push({text,type,id:Date.now()}); setTimeout(()=>{ prototypeState.toast.shift(); renderAll(); }, 2400); renderAll(); }
|
||||||
|
function renderAll(){ document.querySelectorAll('[data-app-root]').forEach(root => renderRoot(root)); if (window.lucide) lucide.createIcons(); }
|
||||||
|
function init(){
|
||||||
|
const tpl = document.getElementById('app-template');
|
||||||
|
document.querySelectorAll('[data-app-root]').forEach(root => root.appendChild(tpl.content.cloneNode(true)));
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
function renderRoot(root){
|
||||||
|
root.dataset.appRoot = '';
|
||||||
|
const nav = root.querySelector('[data-nav]');
|
||||||
|
nav.innerHTML = `<div class="flex h-full w-full flex-col"><div class="flex flex-1 flex-col items-center gap-2">${pages.map(([p,ic]) => { const active = prototypeState.page===p; return `<button type="button" data-page="${p}" aria-label="${p}" aria-current="${active?'page':'false'}" class="focus-ring group flex min-h-[56px] w-full flex-col items-center justify-center gap-1 rounded-2xl text-[11px] transition ${active?'bg-indigo-500 text-white shadow-lg shadow-indigo-500/25':'text-slate-400 hover:bg-white/10 hover:text-indigo-100'}"><span class="${active?'text-white':'text-slate-400 group-hover:text-indigo-200'}">${icon(ic,'h-5 w-5')}</span><span>${p}</span></button>`; }).join('')}</div><div class="mt-2 flex w-full flex-col items-center rounded-2xl bg-slate-900/90 px-2 py-2 text-[10px] leading-4 text-slate-400 ring-1 ring-white/5"><span class="mb-1 text-emerald-400">${icon('circle-dot','h-3.5 w-3.5')}</span><span>本地在线</span></div></div>`;
|
||||||
|
const header = root.querySelector('[data-header]');
|
||||||
|
header.innerHTML = `<div><h2 class="text-xl font-semibold">${prototypeState.page}</h2><p class="mt-1 text-sm text-slate-500">${pageSummaries[prototypeState.page]}</p></div><div class="flex flex-wrap justify-end gap-2">${chip('模型 在线','activity','emerald')}${chip(prototypeState.rpaHosting?'RPA 托管中':'RPA 空闲','radio','indigo')}${chip('本地模式','hard-drive','slate')}${chip('试用 14天','badge-check','amber')}</div>`;
|
||||||
|
root.querySelector('[data-content]').innerHTML = renderPage();
|
||||||
|
root.querySelector('[data-toasts]').innerHTML = prototypeState.toast.map(t => `<div class="pointer-events-auto flex min-w-[280px] items-start gap-2 rounded-xl border bg-white px-4 py-3 text-sm shadow-xl ${t.type==='error'?'border-red-200 text-red-700':'border-emerald-200 text-slate-700'}">${icon(t.type==='error'?'circle-alert':'circle-check','mt-0.5 h-4 w-4 shrink-0')}<span>${t.text}</span></div>`).join('');
|
||||||
|
renderOverlay(root);
|
||||||
|
}
|
||||||
|
function chip(text, ic, tone){ return `<span class="inline-flex h-8 items-center gap-1.5 rounded-full bg-${tone==='slate'?'slate':'white'}-${tone==='slate'?'100':'50'} px-3 text-xs font-medium text-${tone}-700 ring-1 ring-${tone}-200">${icon(ic)}${text}</span>`; }
|
||||||
|
function card(title, body, footer=''){ return `<article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><h3 class="mb-4 flex items-center gap-2 font-semibold text-slate-900">${body.icon||''}${title}</h3>${body.html||body}${footer}</article>`; }
|
||||||
|
function actionBtn(label, attrs='', primary=false){ return `<button type="button" ${attrs} class="focus-ring inline-flex h-10 items-center justify-center gap-2 rounded-xl ${primary?'bg-[var(--primary)] text-white shadow-lg shadow-indigo-500/20':'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50'} px-4 text-sm font-medium">${label}</button>`; }
|
||||||
|
|
||||||
|
function renderPage(){
|
||||||
|
if (prototypeState.page==='工作台') return dashboardPage();
|
||||||
|
if (prototypeState.page==='知识库') return kbPage();
|
||||||
|
if (prototypeState.page==='AI 员工') return agentPage();
|
||||||
|
if (prototypeState.page==='标注应用') return annotationPage();
|
||||||
|
if (prototypeState.page==='自动化任务') return tasksPage();
|
||||||
|
return settingsPage();
|
||||||
|
}
|
||||||
|
function dashboardPage(){
|
||||||
|
return `<div class="space-y-6">
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
${card('环境状态',{icon:icon('shield-check'),html:`<div class="space-y-3 text-sm"><div class="flex justify-between"><span>权限</span><b>3/4</b></div><div class="flex justify-between"><span>模型连通</span><span class="text-emerald-600">通过</span></div><div class="flex justify-between"><span>本地 GPU</span><span>中档</span></div><div class="flex justify-between"><span>存储</span><span class="text-emerald-600">可用</span></div>${actionBtn('查看首次配置','data-modal-open="config"')}</div>`})}
|
||||||
|
${card('资产状态',{icon:icon('boxes'),html:`<div class="grid grid-cols-2 gap-3 text-sm"><b>知识库 3</b><b>就绪索引 2</b><b>AI 员工 2</b><b>标注应用 1</b></div>`})}
|
||||||
|
${card('执行状态',{icon:icon('activity'),html:`<div class="space-y-3 text-sm"><div class="flex justify-between"><span>运行中</span><b>0</b></div><div class="flex justify-between"><span>失败任务</span><b class="text-red-600">1</b></div><div class="flex justify-between"><span>最近托管</span><span class="text-emerald-600">成功</span></div><div class="flex justify-between"><span>RPA</span><span>${prototypeState.rpaHosting?'托管中':'空闲'}</span></div></div>`})}
|
||||||
|
</div>
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><h3 class="mb-4 font-semibold">快速入口</h3><div class="flex flex-wrap gap-3">
|
||||||
|
${actionBtn(icon('database')+'创建知识库','data-modal-open="knowledge"',true)}
|
||||||
|
${actionBtn(icon('bot')+'创建 AI 员工','data-modal-open="agent"',true)}
|
||||||
|
${actionBtn(icon('scan-line')+'进入标注模式 Ctrl+E','data-overlay-open="annotation"')}
|
||||||
|
${actionBtn(icon('play')+'启动自动化任务','data-modal-open="hosting-confirm"')}
|
||||||
|
${actionBtn(icon('package-check')+'导出诊断包','data-action="diagnostic"')}
|
||||||
|
</div></section>
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><h3 class="mb-4 font-semibold">核心闭环</h3><div class="flex items-center gap-2 text-sm text-slate-600">${['首次配置','知识库','AI 员工','标注应用','自动化任务','托管执行','审计复盘'].map((s,i)=>`<div class="flex items-center gap-2"><span class="grid h-8 w-8 place-items-center rounded-full bg-indigo-50 font-semibold text-indigo-700">${i+1}</span><span>${s}</span></div>${i<6?'<div class="h-px w-8 bg-slate-200"></div>':''}`).join('')}</div></section>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
function kbPage(){
|
||||||
|
return `<div class="space-y-5"><div class="flex items-center gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"><div class="relative flex-1">${icon('search','absolute left-3 top-3 h-4 w-4 text-slate-400')}<input class="focus-ring h-10 w-full rounded-xl border border-slate-200 pl-9 text-sm" placeholder="搜索制度、流程、操作说明"></div>${actionBtn('来源','')}${actionBtn('类型','')}${actionBtn('按更新时间','')}${actionBtn(icon('upload')+'上传/创建知识库','data-modal-open="knowledge"',true)}</div>${table(['名称','类型','索引状态','引用次数','更新时间','权限模式'], prototypeState.kbs.map((k,i)=>[`<div><b>${k.name}</b><p class="text-xs text-slate-500">${k.description}${k.error?` · ${k.error} ${actionBtn('重试',`data-action="retry-kb" data-index="${i}"`)}`:''}</p></div>`,k.type,`<span class="${clsBadge(k.status)}">${k.status}</span>`,k.refs,k.updated,`<button data-action="permission" data-index="${i}" class="${clsBadge('idle')}">${k.permission}</button>`]))}</div>`;
|
||||||
|
}
|
||||||
|
function agentPage(){
|
||||||
|
return `<div class="space-y-5"><div class="flex justify-between rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"><div><h3 class="font-semibold">AI 员工</h3><p class="text-sm text-slate-500">启动任务前必须展示知识库、应用和停止方式。</p></div>${actionBtn(icon('plus')+'创建 AI 员工','data-modal-open="agent"',true)}</div>${table(['名称','角色','状态','可访问知识库','可操作标注应用','最近执行时间','操作'], prototypeState.agents.map((a,i)=>[a.name,a.role,`<span class="${clsBadge(a.status)}">${a.status}</span>`,a.kb,a.apps,a.last,actionBtn('启动任务',`data-modal-open="agent-confirm" data-index="${i}"`)]))}</div>`;
|
||||||
|
}
|
||||||
|
function annotationPage(){
|
||||||
|
const tab = prototypeState.activeAppTab;
|
||||||
|
const tabHtml = ['概览','窗口','节点','流程'].map(t=>`<button type="button" data-tab="${t}" class="focus-ring h-10 rounded-xl px-4 text-sm font-medium ${tab===t?'bg-indigo-600 text-white':'text-slate-600 hover:bg-slate-100'}">${t}</button>`).join('');
|
||||||
|
let panel = tab==='节点' ? table(['节点名','node_type','bounds','allowed_actions','confidence'], prototypeState.nodes.map(n=>[n.name,n.type,n.bounds,n.actions,n.confidence])) : tab==='窗口' ? `<div class="grid grid-cols-2 gap-4 text-sm"><div class="rounded-xl border p-4"><b>主窗口</b><p>1180×760 · 已标注</p></div><div class="rounded-xl border p-4"><b>审批弹窗</b><p>520×360 · 已标注</p></div></div>` : tab==='流程' ? `<ol class="space-y-3 text-sm"><li>1. 读取制度规则</li><li>2. 填写金额输入框</li><li>3. 点击提交按钮前暂停确认</li></ol>` : `<div class="grid grid-cols-4 gap-4 text-sm"><div class="rounded-xl border p-4"><b>窗口数</b><p>2</p></div><div class="rounded-xl border p-4"><b>节点数</b><p>${prototypeState.nodes.length}</p></div><div class="rounded-xl border p-4"><b>流程数</b><p>3</p></div><div class="rounded-xl border p-4"><b>最近更新</b><p>今天 09:20</p></div></div>`;
|
||||||
|
return `<div class="space-y-5"><div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><div class="flex items-center justify-between"><div><h3 class="font-semibold">企业报销系统</h3><p class="text-sm text-slate-500">窗口数 2、节点数 ${prototypeState.nodes.length}、流程数 3、最近更新 今天</p></div>${actionBtn(icon('scan-line')+'进入标注模式 Ctrl+E','data-overlay-open="annotation"',true)}</div></div><section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><div class="mb-5 flex gap-2">${tabHtml}</div>${panel}</section></div>`;
|
||||||
|
}
|
||||||
|
function tasksPage(){
|
||||||
|
const rows = prototypeState.tasks.map((t,i)=>[t.name,t.type,`<span class="${clsBadge(t.status)}">${t.status}</span>`,t.employee,t.app,t.updated,t.error?`<div class="text-red-700">${t.error}<br><code class="text-xs">${t.code}</code><br>${actionBtn('重试',`data-action="retry-task" data-index="${i}"`)}</div>`:actionBtn('查看','')]);
|
||||||
|
let audits = prototypeState.audits.filter(a=>(!prototypeState.auditFilters.employee||a.employee===prototypeState.auditFilters.employee)&&(!prototypeState.auditFilters.status||a.status===prototypeState.auditFilters.status));
|
||||||
|
return `<div class="space-y-5"><div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"><div><h3 class="font-semibold">自动化任务</h3><p class="text-sm text-slate-500">状态只使用 queued | running | succeeded | failed | canceled。</p></div>${actionBtn(icon('play')+'启动托管演示','data-modal-open="hosting-confirm"',true)}</div><div class="grid grid-cols-3 gap-4">${['运行中任务','历史任务','失败任务'].map((s,i)=>`<div class="rounded-2xl border bg-white p-4 shadow-sm"><h3 class="font-semibold">${s}</h3><p class="mt-2 text-2xl font-semibold">${i===0?prototypeState.tasks.filter(t=>t.status==='running').length:i===1?prototypeState.tasks.filter(t=>t.status==='succeeded').length:prototypeState.tasks.filter(t=>t.status==='failed').length}</p></div>`).join('')}</div>${table(['任务名','类型','状态','员工','目标应用','更新时间','操作'], rows)}<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><button type="button" data-action="toggle-audit" class="flex h-10 w-full items-center justify-between font-semibold">审计日志 ${icon(prototypeState.auditOpen?'chevron-up':'chevron-down')}</button><div class="${prototypeState.auditOpen?'':'hidden'} mt-4 space-y-4"><div class="grid grid-cols-5 gap-3 text-sm"><select data-filter="time" class="h-10 rounded-xl border px-3"><option>时间</option></select><select data-filter="employee" class="h-10 rounded-xl border px-3"><option value="">员工</option><option>报销审核员</option><option>系统</option></select><select data-filter="app" class="h-10 rounded-xl border px-3"><option>应用</option></select><select data-filter="kb" class="h-10 rounded-xl border px-3"><option>知识库</option></select><select data-filter="status" class="h-10 rounded-xl border px-3"><option value="">任务状态</option><option>running</option><option>succeeded</option><option>failed</option><option>canceled</option></select></div>${table(['时间','员工','应用','知识库','状态','记录'], audits.map(a=>[a.time,a.employee,a.app,a.kb,`<span class="${clsBadge(a.status)}">${a.status}</span>`,a.text]))}</div></section></div>`;
|
||||||
|
}
|
||||||
|
function settingsPage(){
|
||||||
|
const groups = [
|
||||||
|
['模型配置',`本地模型 Qwen2.5 · 云端模型 可选 · API Key sk-•••••••• ${actionBtn(prototypeState.loadingModel?'测试中...':'连通性测试','data-action="model-test"')}`],
|
||||||
|
['提示词配置','系统提示词模板、员工提示词、任务提示词'], ['存储位置','SQLite 数据库、知识库原文、索引文件、日志目录'],
|
||||||
|
['快捷键管理','Ctrl+E 标注模式 · ESC 紧急停止 · 悬浮窗显示/隐藏'], ['开关配置','本地/云端模式 · 托管确认 已开启 · 高风险动作二次确认 已开启'],
|
||||||
|
['诊断与日志',`${actionBtn('导出诊断包','data-action="diagnostic-full"')} ${actionBtn('查看本地日志目录','data-action="log-dir"')} ${actionBtn('复制环境信息','data-action="copy-env"')}`],
|
||||||
|
['数据备份与恢复','导出配置、导出知识库元数据、恢复 SQLite 与索引文件'], ['许可证管理','trial · 14 天试用 · 最多 3 个知识库 / 2 个 AI 员工 / 1 个标注应用 / 每日 20 次托管任务']
|
||||||
|
];
|
||||||
|
return `<div class="grid grid-cols-2 gap-4 pb-8">${groups.map(g=>`<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><h3 class="mb-3 font-semibold">${g[0]}</h3><div class="text-sm leading-7 text-slate-600">${g[1]}</div></section>`).join('')}</div>`;
|
||||||
|
}
|
||||||
|
function table(headers, rows){ return `<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"><table class="w-full text-left text-sm"><thead class="bg-slate-50 text-xs text-slate-500"><tr>${headers.map(h=>`<th class="px-4 py-3 font-medium">${h}</th>`).join('')}</tr></thead><tbody class="divide-y divide-slate-100">${rows.map(r=>`<tr class="align-top hover:bg-slate-50">${r.map(c=>`<td class="px-4 py-3">${c}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`; }
|
||||||
|
|
||||||
|
function renderOverlay(root){
|
||||||
|
const layer = root.querySelector('[data-overlay-layer]');
|
||||||
|
layer.className = 'absolute inset-0 z-40 ' + ((prototypeState.modal || prototypeState.overlay || prototypeState.rpaHosting || prototypeState.highRisk) ? '' : 'hidden');
|
||||||
|
if (prototypeState.rpaHosting) { layer.innerHTML = hostingOverlay(); return; }
|
||||||
|
if (prototypeState.highRisk) { layer.innerHTML = highRiskOverlay(); return; }
|
||||||
|
if (prototypeState.overlay==='annotation') { layer.innerHTML = annotationOverlay(); return; }
|
||||||
|
if (prototypeState.modal) { layer.innerHTML = modalHtml(prototypeState.modal); return; }
|
||||||
|
layer.innerHTML = '';
|
||||||
|
}
|
||||||
|
function modalFrame(title, body, wide=false){ return `<div class="flex h-full items-center justify-center bg-slate-950/40 p-8"><div class="max-h-full ${wide?'w-[760px]':'w-[560px]'} overflow-y-auto rounded-2xl bg-white p-6 shadow-2xl"><div class="mb-5 flex items-center justify-between"><h3 class="text-lg font-semibold">${title}</h3><button data-modal-close class="focus-ring grid h-9 w-9 place-items-center rounded-xl hover:bg-slate-100">${icon('x')}</button></div>${body}</div></div>`; }
|
||||||
|
function modalHtml(type){
|
||||||
|
if(type==='config') return configModal();
|
||||||
|
if(type==='knowledge') return modalFrame('上传/创建知识库', `<form data-form="knowledge" class="space-y-4"><input name="name" required class="focus-ring h-10 w-full rounded-xl border px-3" placeholder="名称"><textarea name="description" class="focus-ring w-full rounded-xl border p-3" placeholder="描述"></textarea><select name="type" class="h-10 w-full rounded-xl border px-3"><option value="document">document</option><option value="image">image</option><option value="video">video</option><option value="folder">folder</option><option value="url">url</option></select><select name="permission" class="h-10 w-full rounded-xl border px-3"><option value="private">private</option><option value="selected_agents">selected_agents</option><option value="workspace">workspace</option></select><button class="h-10 rounded-xl bg-indigo-600 px-4 text-white">提交</button></form>`);
|
||||||
|
if(type==='agent') return modalFrame('创建/编辑 AI 员工', `<form data-form="agent" class="space-y-4"><input name="role" required class="focus-ring h-10 w-full rounded-xl border px-3" placeholder="角色描述"><textarea name="prompt" class="focus-ring w-full rounded-xl border p-3" placeholder="系统提示词"></textarea><input class="h-10 w-full rounded-xl border px-3" placeholder="模型配置:本地模型"><input class="h-10 w-full rounded-xl border px-3" placeholder="知识库权限:财务报销制度"><input class="h-10 w-full rounded-xl border px-3" placeholder="可操作应用权限:企业报销系统"><button class="h-10 rounded-xl bg-indigo-600 px-4 text-white">提交</button></form>`);
|
||||||
|
if(type==='permission') return modalFrame('权限模式', `<div class="space-y-3">${['private','selected_agents','workspace'].map(p=>`<button data-action="save-permission" data-value="${p}" class="focus-ring flex h-10 w-full items-center rounded-xl border px-3 text-sm hover:bg-slate-50">${p}</button>`).join('')}</div>`);
|
||||||
|
if(type==='agent-confirm' || type==='hosting-confirm') return modalFrame('启动前确认', `<div class="space-y-4 text-sm"><div class="rounded-xl bg-indigo-50 p-4"><b>AI 员工</b><p>报销审核员</p></div><div class="rounded-xl bg-slate-50 p-4"><b>将访问的知识库</b><p>财务报销制度</p></div><div class="rounded-xl bg-slate-50 p-4"><b>将控制的应用</b><p>企业报销系统</p></div><div class="rounded-xl bg-slate-50 p-4"><b>预计动作范围</b><p>读取报销规则、填写金额、点击提交前暂停确认</p></div><div class="rounded-xl bg-slate-50 p-4"><b>停止方式</b><p>ESC 或遮罩上的停止托管按钮</p></div><div class="flex justify-end gap-3">${actionBtn('取消','data-modal-close')}${actionBtn('确认启动托管','data-action="start-hosting"',true)}</div></div>`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
function configModal(){
|
||||||
|
const steps=['权限检查','模型配置','性能评估','存储位置','安全说明']; const s=prototypeState.configStep;
|
||||||
|
const status=['通过','通过','通过','通过','需手动处理'];
|
||||||
|
return modalFrame('首次配置与环境检查', `<div class="flex gap-5"><aside class="w-40 space-y-2">${steps.map((x,i)=>`<button data-action="config-step" data-index="${i}" class="h-10 w-full rounded-xl text-left text-sm ${s===i?'bg-indigo-600 px-3 text-white':'px-3 hover:bg-slate-100'}">${x}</button>`).join('')}</aside><div class="flex-1 space-y-4 text-sm"><p class="text-slate-500">状态枚举:未检查 | 检查中 | 通过 | 失败 | 需手动处理</p>${s===0?`<div class="space-y-2">${['屏幕录制','辅助功能/自动化控制','文件访问','网络访问'].map((x,i)=>`<div class="flex justify-between rounded-xl border p-3"><span>${x}</span><span class="${clsBadge(i===1?status[4]:'ready')}">${i===1?status[4]:'通过'}</span></div>`).join('')}</div><div class="rounded-xl bg-amber-50 p-4 text-amber-800">原因:辅助功能权限需手动授权;恢复:打开系统设置或重新检测。</div><div class="flex gap-2">${actionBtn('打开系统设置','data-action="open-settings"')}${actionBtn('重新检测','data-action="recheck-permission"',true)}${actionBtn('跳过并以后配置','data-modal-close')}</div>`:`<div class="rounded-xl border p-4"><b>${steps[s]}</b><p class="mt-2">当前状态:通过。本页用于演示 ${steps[s]} 配置检查。</p></div>`}</div></div>`, true);
|
||||||
|
}
|
||||||
|
function annotationOverlay(){
|
||||||
|
const b=prototypeState.draftBounds;
|
||||||
|
return `<div class="relative h-full bg-slate-950/85 p-8 text-white"><div class="mx-auto flex h-full max-w-4xl flex-col gap-4"><div class="flex items-center justify-between"><div><h3 class="text-lg font-semibold">Ctrl+E 标注模式</h3><p class="text-sm text-slate-300">选择当前标注应用 → 框选窗口/节点 → 命名节点 → 保存</p></div>${actionBtn('退出','data-overlay-close')}</div><div class="grid grid-cols-[1fr_280px] gap-4 min-h-0 flex-1"><div class="relative overflow-hidden rounded-2xl bg-slate-200 p-5 text-slate-900"><div class="h-full rounded-xl bg-white p-5 shadow-inner"><div class="mb-4 flex h-10 items-center gap-2 rounded bg-slate-100 px-3"><b>企业报销系统</b><span class="text-sm text-slate-500">模拟桌面截图</span></div><div class="grid grid-cols-3 gap-4"><div class="h-80 rounded bg-slate-100"></div><div class="col-span-2 h-80 rounded bg-slate-50 p-4"><input class="mb-4 h-10 w-52 rounded border px-3" value="1280.00"><button class="absolute rounded bg-indigo-600 px-4 py-2 text-white" style="left:520px;top:430px">提交</button></div></div></div><div data-drag-box class="absolute cursor-move rounded-lg border-2 border-indigo-500 bg-indigo-500/20" style="left:${b.x}px;top:${b.y}px;width:${b.width}px;height:${b.height}px"></div></div><form data-form="node" class="space-y-4 rounded-2xl bg-white p-5 text-slate-900"><label class="block text-sm font-medium">当前标注应用<select class="mt-2 h-10 w-full rounded-xl border px-3"><option>企业报销系统</option></select></label><label class="block text-sm font-medium">节点名<input name="name" required class="mt-2 h-10 w-full rounded-xl border px-3" placeholder="测试提交按钮"></label><p class="rounded-xl bg-slate-50 p-3 text-xs">当前 bounds:{ x: ${b.x}, y: ${b.y}, width: ${b.width}, height: ${b.height} }</p><button class="h-10 w-full rounded-xl bg-indigo-600 text-white">保存</button><button type="button" data-action="annotation-fail" class="h-9 text-sm text-red-600">模拟保存失败</button><button type="button" data-action="annotation-retry" class="hidden h-9 text-sm text-indigo-600">重试保存</button><p data-annotation-error class="hidden rounded-xl bg-red-50 p-3 text-sm text-red-700">保存失败:窗口句柄已变化,可保留草稿并重试。原因:窗口句柄已变化;恢复:保留草稿并重试保存。</p></form></div></div></div>`;
|
||||||
|
}
|
||||||
|
function hostingOverlay(){
|
||||||
|
const actions=['读取报销规则','填写金额输入框','定位提交按钮','提交报销单'];
|
||||||
|
const current=actions[Math.min(prototypeState.hostingStep, actions.length-1)];
|
||||||
|
return `<div class="flex h-full items-center justify-center bg-slate-950/90 p-8 text-white"><div class="w-[760px] rounded-3xl border border-white/10 bg-slate-900 p-6 shadow-2xl"><div class="flex items-center justify-between"><div><h3 class="text-xl font-semibold">RPA 托管执行中</h3><p class="text-sm text-slate-300">ESC 紧急停止始终可用</p></div><button data-action="stop-hosting" class="h-11 rounded-xl bg-red-600 px-5 font-semibold text-white">停止托管</button></div><div class="mt-5 rounded-2xl bg-slate-950 p-4"><p class="text-sm text-slate-400">当前动作</p><p class="mt-1 text-lg font-semibold">${current}</p><div class="mt-4 h-3 rounded-full bg-slate-800"><div class="h-3 rounded-full bg-indigo-500" style="width:${(prototypeState.hostingStep+1)*25}%"></div></div></div><div class="mt-5 max-h-48 overflow-y-auto rounded-2xl bg-slate-950 p-4 text-sm text-slate-300">${prototypeState.hostingLogs.map(l=>`<p>${l}</p>`).join('') || '<p>等待下一步动作。</p>'}</div><div class="mt-5 flex justify-end gap-3">${actionBtn('下一步动作','data-action="next-hosting"',true)}</div></div></div>`;
|
||||||
|
}
|
||||||
|
function highRiskOverlay(){ return `<div class="flex h-full items-center justify-center bg-slate-950/70 p-8"><div class="w-[520px] rounded-2xl bg-white p-6 shadow-2xl"><h3 class="text-lg font-semibold text-red-700">高风险动作:提交</h3><p class="mt-3 text-sm text-slate-600">确认后将向业务系统提交单据。</p><div class="mt-5 flex justify-end gap-3">${actionBtn('取消并等待人工','data-action="cancel-high-risk"')}${actionBtn('确认提交','data-action="confirm-high-risk"',true)}</div></div></div>`; }
|
||||||
|
|
||||||
|
document.addEventListener('click', event => {
|
||||||
|
const platformTab = event.target.closest('[data-platform-tab]');
|
||||||
|
if(platformTab){ const target=platformTab.dataset.platformTab; prototypeState.activePlatform=target; document.querySelectorAll('[data-platform-panel]').forEach(p=>p.classList.toggle('hidden',p.dataset.platformPanel!==target)); document.querySelectorAll('[data-platform-tab]').forEach(b=>{b.className='focus-ring h-10 rounded-full px-5 text-sm font-medium text-slate-600';}); platformTab.className='focus-ring h-10 rounded-full bg-[var(--primary)] px-5 text-sm font-medium text-white'; return; }
|
||||||
|
const page = event.target.closest('[data-page]'); if(page){ prototypeState.page=page.dataset.page; renderAll(); return; }
|
||||||
|
const tab = event.target.closest('[data-tab]'); if(tab){ prototypeState.activeAppTab=tab.dataset.tab; renderAll(); return; }
|
||||||
|
const open = event.target.closest('[data-modal-open]'); if(open){ prototypeState.modal=open.dataset.modalOpen; renderAll(); return; }
|
||||||
|
if(event.target.closest('[data-modal-close]')){ prototypeState.modal=null; renderAll(); return; }
|
||||||
|
const overlayOpen = event.target.closest('[data-overlay-open]'); if(overlayOpen){ prototypeState.overlay=overlayOpen.dataset.overlayOpen; renderAll(); return; }
|
||||||
|
if(event.target.closest('[data-overlay-close]')){ prototypeState.overlay=null; renderAll(); return; }
|
||||||
|
const action = event.target.closest('[data-action]'); if(!action) return;
|
||||||
|
handleAction(action, event);
|
||||||
|
});
|
||||||
|
document.addEventListener('change', event => { const f=event.target.closest('[data-filter]'); if(!f) return; prototypeState.auditFilters[f.dataset.filter]=f.value; renderAll(); });
|
||||||
|
document.addEventListener('submit', event => {
|
||||||
|
event.preventDefault(); const form=event.target;
|
||||||
|
if(form.dataset.form==='knowledge'){ const fd=new FormData(form); prototypeState.kbs.unshift({name:fd.get('name')||'测试制度库',description:fd.get('description')||'新建知识库',type:fd.get('type'),status:'pending',refs:0,updated:'刚刚',permission:fd.get('permission')}); prototypeState.page='知识库'; prototypeState.modal=null; toast('知识库已创建,正在进入索引队列'); }
|
||||||
|
if(form.dataset.form==='agent'){ const fd=new FormData(form); prototypeState.agents.unshift({name:fd.get('role')||'新 AI 员工',role:fd.get('role')||'业务助理',status:'idle',kb:1,apps:1,last:'未执行'}); prototypeState.page='AI 员工'; prototypeState.modal=null; toast('AI 员工已创建,默认未启动任务'); }
|
||||||
|
if(form.dataset.form==='node'){ const fd=new FormData(form); const b=prototypeState.draftBounds; prototypeState.nodes.unshift({name:fd.get('name')||'新建节点',type:'button',bounds:`{ x: ${b.x}, y: ${b.y}, width: ${b.width}, height: ${b.height} }`,actions:'click, wait',confidence:'0.89'}); prototypeState.page='标注应用'; prototypeState.activeAppTab='节点'; prototypeState.overlay=null; toast('节点已保存到 企业报销系统'); }
|
||||||
|
renderAll();
|
||||||
|
});
|
||||||
|
function handleAction(el){
|
||||||
|
const a=el.dataset.action;
|
||||||
|
if(a==='diagnostic') toast('诊断包已生成,已排除 API Key 与知识库原文');
|
||||||
|
if(a==='diagnostic-full') toast('诊断包已生成,不包含 API Key、知识库原文、用户隐私内容');
|
||||||
|
if(a==='log-dir') toast('已打开本地日志目录(原型模拟)');
|
||||||
|
if(a==='copy-env') toast('环境信息已复制(原型模拟)');
|
||||||
|
if(a==='retry-kb'){ prototypeState.kbs[+el.dataset.index].status='indexing'; delete prototypeState.kbs[+el.dataset.index].error; toast('已重新加入索引队列'); }
|
||||||
|
if(a==='permission'){ prototypeState.modal='permission'; prototypeState.permissionIndex=+el.dataset.index; }
|
||||||
|
if(a==='save-permission'){ prototypeState.kbs[prototypeState.permissionIndex].permission=el.dataset.value; prototypeState.modal=null; prototypeState.audits.unshift({time:'刚刚',employee:'系统',app:'知识库',kb:prototypeState.kbs[prototypeState.permissionIndex].name,status:'succeeded',text:'权限变更已写入审计日志'}); toast('权限变更已写入审计日志'); }
|
||||||
|
if(a==='start-hosting') startHosting();
|
||||||
|
if(a==='next-hosting') nextHosting();
|
||||||
|
if(a==='stop-hosting') stopHosting();
|
||||||
|
if(a==='cancel-high-risk'){ prototypeState.highRisk=false; prototypeState.rpaHosting=true; prototypeState.agents.find(x=>x.name==='报销审核员').status='waiting_user'; prototypeState.hostingLogs.push('人工确认被取消,任务等待人工处理。'); prototypeState.audits.unshift({time:'刚刚',employee:'报销审核员',app:'企业报销系统',kb:'财务报销制度',status:'running',text:'人工确认被取消'}); }
|
||||||
|
if(a==='confirm-high-risk'){ prototypeState.highRisk=false; prototypeState.rpaHosting=false; prototypeState.tasks[0].status='succeeded'; prototypeState.agents.find(x=>x.name==='报销审核员').status='idle'; toast('高风险提交已确认,托管任务完成'); }
|
||||||
|
if(a==='toggle-audit') prototypeState.auditOpen=!prototypeState.auditOpen;
|
||||||
|
if(a==='retry-task'){ prototypeState.tasks[+el.dataset.index].status='queued'; toast('任务已重新排队'); }
|
||||||
|
if(a==='config-step') prototypeState.configStep=+el.dataset.index;
|
||||||
|
if(a==='open-settings') toast('已打开系统设置指引(原型模拟)');
|
||||||
|
if(a==='recheck-permission'){ toast('权限重新检测通过'); prototypeState.modal=null; }
|
||||||
|
if(a==='annotation-fail'){ document.querySelectorAll('[data-annotation-error],[data-action="annotation-retry"]').forEach(x=>x.classList.remove('hidden')); return; }
|
||||||
|
if(a==='annotation-retry') toast('草稿已保留,请再次保存');
|
||||||
|
if(a==='model-test'){ prototypeState.loadingModel=true; renderAll(); setTimeout(()=>{ prototypeState.loadingModel=false; toast('模型连通性测试通过'); }, 420); return; }
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
function startHosting(){ prototypeState.modal=null; prototypeState.overlay=null; prototypeState.rpaHosting=true; prototypeState.hostingStep=0; prototypeState.hostingLogs=['托管已启动:报销审核员控制企业报销系统。']; prototypeState.tasks[0].status='running'; prototypeState.agents.find(x=>x.name==='报销审核员').status='running'; renderAll(); }
|
||||||
|
function nextHosting(){ const actions=['读取报销规则','填写金额输入框','定位提交按钮','提交报销单']; const msg=actions[Math.min(prototypeState.hostingStep,3)]; prototypeState.hostingLogs.push(new Date().toLocaleTimeString()+' '+msg); if(msg==='提交报销单'){ prototypeState.rpaHosting=false; prototypeState.highRisk=true; } else prototypeState.hostingStep++; renderAll(); }
|
||||||
|
function stopHosting(){ prototypeState.rpaHosting=false; prototypeState.highRisk=false; prototypeState.tasks[0].status='canceled'; prototypeState.agents.find(x=>x.name==='报销审核员').status='idle'; prototypeState.audits.unshift({time:'刚刚',employee:'报销审核员',app:'企业报销系统',kb:'财务报销制度',status:'canceled',text:'托管已人工终止,原因已写入审计日志'}); toast('托管已人工终止,原因已写入审计日志'); renderAll(); }
|
||||||
|
document.addEventListener('keydown', event => { if(event.key==='Escape' && (prototypeState.rpaHosting||prototypeState.highRisk)) stopHosting(); });
|
||||||
|
document.addEventListener('pointerdown', event => { const box=event.target.closest('[data-drag-box]'); if(!box) return; prototypeState.dragging={startX:event.clientX,startY:event.clientY,origX:prototypeState.draftBounds.x,origY:prototypeState.draftBounds.y}; box.setPointerCapture(event.pointerId); });
|
||||||
|
document.addEventListener('pointermove', event => { if(!prototypeState.dragging) return; prototypeState.draftBounds.x=Math.max(24, prototypeState.dragging.origX + Math.round(event.clientX-prototypeState.dragging.startX)); prototypeState.draftBounds.y=Math.max(24, prototypeState.dragging.origY + Math.round(event.clientY-prototypeState.dragging.startY)); renderAll(); });
|
||||||
|
document.addEventListener('pointerup', () => { prototypeState.dragging=false; });
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user