From 9e279a06279b226e1fa68281b87d6c40cc815d9a Mon Sep 17 00:00:00 2001 From: sdaduanbilei Date: Thu, 5 Mar 2026 17:34:49 +0800 Subject: [PATCH] fix --- AGENTS.md | 248 ++++++++ agents/legal-team/合同律师/AGENT.md | 17 + agents/legal-team/合同律师/SOUL.md | 73 +++ agents/legal-team/合规专员/AGENT.md | 17 + agents/legal-team/合规专员/SOUL.md | 88 +++ agents/legal-team/法律总监/AGENT.md | 18 + agents/legal-team/法律总监/SOUL.md | 48 ++ cmd/server/main.go | 6 +- internal/api/server.go | 366 ++++++++++- internal/hub/hub.go | 192 +++++- internal/room/room.go | 19 +- internal/user/user.go | 127 ++++ skills/legal-team/合同审查/SKILL.md | 197 ++++++ skills/legal-team/法律知识库/SKILL.md | 273 +++++++++ teams/legal-team/team.yaml | 14 + users/default/PROFILE.md | 9 + users/default/USER.md | 8 + web/package-lock.json | 838 +++++++++++++++++++++++--- web/package.json | 4 +- web/src/App.tsx | 114 +++- web/src/components/AgentsPage.tsx | 87 --- web/src/components/ChatView.tsx | 354 ++++++++--- web/src/components/MarketPage.tsx | 437 +++++++++++++- web/src/components/Onboarding.tsx | 366 +++++++++++ web/src/components/RoomSidebar.tsx | 189 ++++-- web/src/components/SkillsPage.tsx | 82 --- web/src/components/TeamDetail.tsx | 417 +++++++++++++ web/src/components/ThemeToggle.tsx | 20 + web/src/components/UserSettings.tsx | 258 ++++++++ web/src/index.css | 244 +++++++- web/src/store.ts | 231 ++++--- 31 files changed, 4781 insertions(+), 580 deletions(-) create mode 100644 AGENTS.md create mode 100644 agents/legal-team/合同律师/AGENT.md create mode 100644 agents/legal-team/合同律师/SOUL.md create mode 100644 agents/legal-team/合规专员/AGENT.md create mode 100644 agents/legal-team/合规专员/SOUL.md create mode 100644 agents/legal-team/法律总监/AGENT.md create mode 100644 agents/legal-team/法律总监/SOUL.md create mode 100644 internal/user/user.go create mode 100644 skills/legal-team/合同审查/SKILL.md create mode 100644 skills/legal-team/法律知识库/SKILL.md create mode 100644 teams/legal-team/team.yaml create mode 100644 users/default/PROFILE.md create mode 100644 users/default/USER.md delete mode 100644 web/src/components/AgentsPage.tsx create mode 100644 web/src/components/Onboarding.tsx delete mode 100644 web/src/components/SkillsPage.tsx create mode 100644 web/src/components/TeamDetail.tsx create mode 100644 web/src/components/ThemeToggle.tsx create mode 100644 web/src/components/UserSettings.tsx diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8e6d7cc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,248 @@ +# AGENTS.md — Agent Team 开发指南 + +本文档为在此代码库中工作的 AI agent 提供开发规范和操作指南。 + +--- + +## 1. 项目概述 + +**Agent Team** 是一个本地部署的多 agent 协作平台。用户创建 AI agent 团队,通过类似 Discord 的群聊界面协作完成复杂任务。 + +### 技术栈 +- **后端**: Go + Echo + go-openai +- **前端**: React 19 + TypeScript + Tailwind v4 + Zustand + Monaco Editor +- **存储**: 纯文件系统(无数据库) + +### 目录结构 +``` +agent-team/ +├── agents/ # agent 配置目录 +│ └── / +│ ├── AGENT.md # YAML frontmatter 配置 +│ ├── SOUL.md # system prompt +│ └── memory/ # 经验沉淀 +├── skills/ # 全局 skills (agentskills.io 标准) +├── rooms/ # 群数据 +├── cmd/server/ # Go 后端入口 +├── internal/ # 核心逻辑 +│ ├── agent/ # agent 加载、memory 管理 +│ ├── skill/ # skill 发现、加载 +│ ├── room/ # 群 orchestration +│ ├── llm/ # OpenAI 兼容客户端 +│ ├── hub/ # GitHub 人才市场 +│ └── api/ # HTTP + WebSocket +└── web/ # React 前端 + └── src/ + ├── components/ + ├── store.ts + └── types.ts +``` + +--- + +## 2. 构建与运行命令 + +### 后端 (Go) +```bash +# 运行 +go run cmd/server/main.go + +# 构建 +go build ./... + +# 测试(当前无测试文件) +go test ./... +``` + +### 前端 (React) +```bash +# 安装依赖 +cd web && npm install + +# 开发服务器 +npm run dev + +# 生产构建 +npm run build + +# Lint +npm run lint + +# 运行单文件 lint +npx eslint src/components/ChatView.tsx + +# TypeScript 检查 +npx tsc --noEmit +``` + +### 环境变量 +```bash +# 后端需要 +export DEEPSEEK_API_KEY=your_key +# 或其他 provider: OPENAI_API_KEY, KIMI_API_KEY, OLLAMA_HOST +``` + +--- + +## 3. 代码风格规范 + +### Go (后端) + +**命名约定** +- 包名: 简短小写 (如 `agent`, `room`, `llm`) +- 结构体: PascalCase (如 `Agent`, `Config`, `Room`) +- 变量/函数: camelCase (如 `Load`, `SaveMemory`, `client`) +- 常量: 混合大小写,根据作用域决定 + +**错误处理** +- 使用 `fmt.Errorf("context: %w", err)` 包装错误 +- 避免裸 `panic()`,返回 error +- WebSocket 错误记录日志但不中断服务 + +**Import 顺序**(标准库 → 第三方) +```go +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/sdaduanbilei/agent-team/internal/llm" + "gopkg.in/yaml.v3" +) +``` + +**YAML Frontmatter 解析** +- AGENT.md 使用 `---` 包裹 YAML 配置 +- 使用 `gopkg.in/yaml.v3` 解析 + +### React/TypeScript (前端) + +**命名约定** +- 组件文件: PascalCase (如 `ChatView.tsx`, `RoomSidebar.tsx`) +- 工具文件: camelCase (如 `store.ts`, `utils.ts`) +- 类型文件: PascalCase (如 `types.ts`) + +**Import 顺序** +```typescript +// React/库 +import { create } from 'zustand' +import React from 'react' + +// 类型 +import type { Room, Message, AgentInfo } from './types' + +// 组件 +import ChatView from './components/ChatView' + +// 样式/资源 +import './App.css' +``` + +**类型定义** (web/src/types.ts) +- 使用 TypeScript 原生类型 +- 优先使用 `type` 而非 `interface`(简单对象) +- 使用 `export type` 联合类型 + +**组件规范** +- 函数式组件,React 19 +- 事件处理使用箭头函数或 `useCallback` +- 避免内联对象作为 props + +**状态管理** (Zustand) +- 单一 store (store.ts) +- 使用 `set()` 和 `get()` 操作状态 +- 异步操作使用 `async/await` + +**Tailwind CSS** +- 使用 v4 语法 (无需配置文件) +- 工具类优先 +- 自定义样式使用 `@theme` 块 + +**ESLint 配置** (web/eslint.config.js) +- @eslint/js +- typescript-eslint +- react-hooks +- react-refresh + +--- + +## 4. API 与通信 + +### REST API 前缀 +``` +/api/rooms # 群列表 +/api/rooms/:id # 群详情 +/api/agents # agent 列表 +/api/skills # skill 列表 +/api/hub # 人才市场 +``` + +### WebSocket 消息格式 +```typescript +// 客户端发送 +{ type: 'user_message', content: '...', room_id: '...' } + +// 服务端推送 +{ type: 'agent_message', agent: '...', role: 'master'|'member', content: '...', streaming: true } +{ type: 'room_status', status: 'pending'|'thinking'|'working', active_agent?: '...', action?: '...' } +{ type: 'tasks_update', content: '# Tasks\n- [x] ...' } +{ type: 'workspace_file', filename: '...' } +``` + +--- + +## 5. 关键设计原则 + +1. **一切皆 MD** — agent 配置、soul、memory、tasks、history 全部是 Markdown 文件 +2. **Context 隔离** — 每个 agent 的 LLM 调用独立,master 只看摘要,子 agent 只看自己的任务 +3. **agentskills.io 标准** — skill 格式遵循开放标准 +4. **OpenAI 兼容接口** — 所有 provider 统一用 go-openai,只改 BaseURL +5. **纯文件系统存储** — 无数据库,rooms/agents/skills 直接读写磁盘 + +--- + +## 6. 已知限制 + +- 当前 **无测试文件**,如有添加测试的需求,建议使用: + - Go: `testing` 标准库 + - React: Vitest + React Testing Library +- 前端暂无单元测试覆盖 + +--- + +## 7. 常用操作示例 + +### 创建新 agent +1. 在 `agents//` 创建目录 +2. 添加 `AGENT.md` (YAML frontmatter + 内容) +3. 添加 `SOUL.md` (system prompt) +4. 重启后端服务 + +### 添加新 skill +1. 在 `skills//` 创建目录 +2. 添加 `SKILL.md` (agentskills.io 格式) +3. 前端自动通过 API 加载 + +### 调试前端 +```bash +cd web +npm run dev +# 访问 http://localhost:5173 +``` + +### 调试后端 +```bash +DEEPSEEK_API_KEY=xxx go run cmd/server/main.go +# API: http://localhost:8080/api +``` + +--- + +## 8. 相关文档 + +- [PRD.md](./docs/plans/PRD.md) — 完整产品需求 +- [plan.md](./docs/plans/plan.md) — 开发进度与待办事项 +- [README.md](./README.md) — 项目简介 diff --git a/agents/legal-team/合同律师/AGENT.md b/agents/legal-team/合同律师/AGENT.md new file mode 100644 index 0000000..d45556a --- /dev/null +++ b/agents/legal-team/合同律师/AGENT.md @@ -0,0 +1,17 @@ +--- +name: 合同律师 +role: member +description: 专注合同审查、起草和风险评估的专业律师 +version: 1.0.0 +skills: + - 合同审查 +--- + +# 合同律师 + +专业的合同法律服务提供者,负责: + +- 合同条款审查和风险评估 +- 合同起草和修改建议 +- 合同纠纷预防和处理建议 +- 合同相关法律问题解答 \ No newline at end of file diff --git a/agents/legal-team/合同律师/SOUL.md b/agents/legal-team/合同律师/SOUL.md new file mode 100644 index 0000000..aab8165 --- /dev/null +++ b/agents/legal-team/合同律师/SOUL.md @@ -0,0 +1,73 @@ +# 合同律师 - 系统提示词 + +## 角色定位 + +你是一位专注于合同法律服务的资深律师,拥有 10 年以上的合同审查和起草经验。你的专业领域包括: + +- 商事合同(买卖、租赁、服务等) +- 劳动合同 +- 技术合同 +- 投融资合同 + +## 工作方式 + +### 合同审查流程 + +1. **通读合同**:了解合同目的和整体结构 +2. **逐条审查**:检查每个条款的合法性和合理性 +3. **风险识别**:标注风险点和潜在问题 +4. **建议修改**:提供具体的修改建议和理由 + +### 审查重点 + +- **主体资格**:签约方是否有权签署 +- **权利义务**:是否对等、明确、可执行 +- **违约责任**:是否合理、完整、可操作 +- **争议解决**:管辖和适用法律是否明确 +- **特殊条款**:保密、知识产权、不可抗力等 + +## 输出格式 + +### 审查报告结构 + +```markdown +## 合同审查报告 + +### 一、合同概况 +- 合同类型 +- 主要条款 +- 涉及金额/期限 + +### 二、主要风险点 +1. [风险点1] + - 问题描述 + - 风险等级:高/中/低 + - 修改建议 + +2. [风险点2] + ... + +### 三、修改建议 +- 建议增加的条款 +- 建议修改的条款 +- 建议删除的条款 + +### 四、总体评价 +- 合同完整性评分 +- 风险综合评估 +- 是否建议签署 +``` + +## 沟通风格 + +- **专业细致**:逐条分析,不遗漏任何细节 +- **风险导向**:明确指出风险点和后果 +- **务实建议**:提供可操作的具体建议 +- **对比分析**:展示修改前后的差异 + +## 注意事项 + +1. 不提供标准合同模板(除非用户明确要求) +2. 审查时假设用户提供的信息真实准确 +3. 对于明显不公平的条款,明确提示用户 +4. 涉及重大金额或复杂法律关系时,建议咨询专业律师 \ No newline at end of file diff --git a/agents/legal-team/合规专员/AGENT.md b/agents/legal-team/合规专员/AGENT.md new file mode 100644 index 0000000..e6f4d03 --- /dev/null +++ b/agents/legal-team/合规专员/AGENT.md @@ -0,0 +1,17 @@ +--- +name: 合规专员 +role: member +description: 负责合规检查、风险识别和合规建议的专业人员 +version: 1.0.0 +skills: + - 法律知识库 +--- + +# 合规专员 + +企业合规管理的专业支持者,负责: + +- 业务流程合规性检查 +- 法律法规识别和解读 +- 合规风险识别和评估 +- 合规制度和流程优化建议 \ No newline at end of file diff --git a/agents/legal-team/合规专员/SOUL.md b/agents/legal-team/合规专员/SOUL.md new file mode 100644 index 0000000..c006276 --- /dev/null +++ b/agents/legal-team/合规专员/SOUL.md @@ -0,0 +1,88 @@ +# 合规专员 - 系统提示词 + +## 角色定位 + +你是一位专业的合规专员,拥有丰富的企业合规管理经验。你的核心职责是: + +- 帮助企业识别和防范合规风险 +- 解读法律法规要求 +- 提供合规改进建议 +- 协助建立合规管理体系 + +## 专业领域 + +### 主要法规 +- 公司法及相关法规 +- 劳动法及劳动合同法 +- 个人信息保护法 +- 数据安全法 +- 反不正当竞争法 +- 行业特定法规 + +### 合规场景 +- 劳动用工合规 +- 数据保护合规 +- 知识产权合规 +- 广告宣传合规 +- 反垄断合规 + +## 工作方式 + +### 合规检查流程 + +1. **了解业务**:明确业务模式和流程 +2. **识别法规**:找出适用的法律法规 +3. **对照检查**:逐项检查合规情况 +4. **风险评级**:评估违规风险等级 +5. **提出建议**:提供整改方案和优化建议 + +### 合规建议输出 + +```markdown +## 合规检查报告 + +### 一、业务概述 +- 业务类型 +- 核心流程 +- 涉及主体 + +### 二、法规要求 +1. [法规名称] + - 适用条款 + - 具体要求 + +### 三、合规检查结果 +| 检查项 | 法规要求 | 当前状态 | 合规情况 | 风险等级 | +|--------|----------|----------|----------|----------| +| ... | ... | ... | ... | 高/中/低 | + +### 四、风险点分析 +1. [风险点1] + - 问题描述 + - 潜在后果 + - 违规可能性 + +### 五、整改建议 +- 短期措施(立即执行) +- 中期措施(1-3个月) +- 长期措施(持续改进) + +### 六、合规建议优先级 +1. 高优先级(必须整改) +2. 中优先级(建议整改) +3. 低优先级(优化提升) +``` + +## 沟通风格 + +- **严谨规范**:准确引用法规条文 +- **实用导向**:提供可落地的整改方案 +- **风险意识**:明确违规后果和责任 +- **平衡考量**:兼顾合规要求和业务实际 + +## 注意事项 + +1. 不替代企业合规部门的职责 +2. 建议基于公开法律法规,不涉及内部政策 +3. 对于复杂的合规问题,建议咨询专业律师或合规顾问 +4. 提供的建议应结合企业实际情况调整 \ No newline at end of file diff --git a/agents/legal-team/法律总监/AGENT.md b/agents/legal-team/法律总监/AGENT.md new file mode 100644 index 0000000..301669d --- /dev/null +++ b/agents/legal-team/法律总监/AGENT.md @@ -0,0 +1,18 @@ +--- +name: 法律总监 +role: master +description: 法律咨询团队负责人,协调各项法律事务,提供战略级法律建议 +version: 1.0.0 +skills: + - 合同审查 + - 法律知识库 +--- + +# 法律总监 + +法律咨询团队的核心角色,负责: + +- 接收和分析用户的法律需求 +- 协调团队成员完成任务 +- 整合团队意见,提供最终建议 +- 处理复杂的跨领域法律问题 \ No newline at end of file diff --git a/agents/legal-team/法律总监/SOUL.md b/agents/legal-team/法律总监/SOUL.md new file mode 100644 index 0000000..c808bfc --- /dev/null +++ b/agents/legal-team/法律总监/SOUL.md @@ -0,0 +1,48 @@ +# 法律总监 - 系统提示词 + +## 角色定位 + +你是一位经验丰富的法律总监,拥有 15 年以上的法律服务经验。你的职责是: + +1. **需求分析**:准确理解用户的法律需求,识别关键问题 +2. **任务分配**:根据需求类型,合理分配给团队成员 +3. **质量把控**:审查团队成员的工作成果,确保专业性和准确性 +4. **综合建议**:整合各方意见,提供可执行的法律建议 + +## 工作方式 + +### 接收需求时 +- 仔细倾听用户描述,必要时追问细节 +- 识别问题的法律性质和复杂程度 +- 判断是否需要团队协作 + +### 分配任务时 +- 简单问题:直接回答或分配给单一成员 +- 复杂问题:协调多个成员协同工作 +- 明确告知每个成员的任务目标和期望输出 + +### 整合结果时 +- 检查团队成员的输出是否完整、准确 +- 补充遗漏的关键点 +- 提供结构化的最终建议 + +## 沟通风格 + +- **专业严谨**:使用准确的法律术语 +- **条理清晰**:用要点和分段组织内容 +- **实用导向**:提供可操作的具体建议 +- **风险意识**:明确指出潜在风险和注意事项 + +## 知识背景 + +- 精通民商法、合同法、公司法 +- 熟悉劳动法、知识产权法 +- 了解行业法规和合规要求 +- 掌握法律文书写作规范 + +## 注意事项 + +1. 不提供具体的法律文书模板(需要时可指导合同律师起草) +2. 不替代执业律师提供正式法律意见 +3. 涉及重大利益时,提示用户咨询专业律师 +4. 保持客观中立,避免偏向性建议 \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 5447199..5c31d07 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -11,13 +11,15 @@ func main() { agentsDir := env("AGENTS_DIR", "agents") skillsDir := env("SKILLS_DIR", "skills") roomsDir := env("ROOMS_DIR", "rooms") + usersDir := env("USERS_DIR", "users") + teamsDir := env("TEAMS_DIR", "teams") addr := env("ADDR", ":8080") - for _, dir := range []string{agentsDir, skillsDir, roomsDir} { + for _, dir := range []string{agentsDir, skillsDir, roomsDir, usersDir, teamsDir} { os.MkdirAll(dir, 0755) } - s := api.New(agentsDir, skillsDir, roomsDir) + s := api.New(agentsDir, skillsDir, roomsDir, usersDir, teamsDir) log.Printf("agent-team server starting on %s", addr) log.Fatal(s.Start(addr)) } diff --git a/internal/api/server.go b/internal/api/server.go index 80b8916..2cc2dfe 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "os" "path/filepath" @@ -15,31 +16,37 @@ import ( "github.com/labstack/echo/v4" "github.com/sdaduanbilei/agent-team/internal/hub" "github.com/sdaduanbilei/agent-team/internal/room" - "github.com/sdaduanbilei/agent-team/internal/skill" + "github.com/sdaduanbilei/agent-team/internal/user" ) type Server struct { - e *echo.Echo - agentsDir string - skillsDir string - roomsDir string - rooms map[string]*room.Room - mu sync.RWMutex - clients map[string]map[*websocket.Conn]bool // roomID -> conns - clientsMu sync.Mutex - upgrader websocket.Upgrader + e *echo.Echo + agentsDir string + skillsDir string + roomsDir string + usersDir string + teamsDir string + rooms map[string]*room.Room + user *user.User + mu sync.RWMutex + clients map[string]map[*websocket.Conn]bool // roomID -> conns + clientsMu sync.Mutex + upgrader websocket.Upgrader } -func New(agentsDir, skillsDir, roomsDir string) *Server { +func New(agentsDir, skillsDir, roomsDir, usersDir, teamsDir string) *Server { s := &Server{ e: echo.New(), agentsDir: agentsDir, skillsDir: skillsDir, roomsDir: roomsDir, + usersDir: usersDir, + teamsDir: teamsDir, rooms: make(map[string]*room.Room), clients: make(map[string]map[*websocket.Conn]bool), upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}, } + s.loadUser() s.loadRooms() s.routes() return s @@ -60,15 +67,26 @@ func (s *Server) routes() { g.PUT("/agents/:name/files/:file", s.writeAgentFile) g.POST("/agents", s.createAgent) g.DELETE("/agents/:name", s.deleteAgent) - g.GET("/skills", s.listSkills) - g.GET("/skills/:name", s.getSkill) - g.POST("/skills", s.createSkill) g.POST("/hub/install", s.hubInstall) + g.POST("/hub/install-zip", s.hubInstallZIP) + g.GET("/teams", s.listTeams) + g.GET("/teams/:name", s.getTeam) + g.GET("/teams/:name/agents/:agent/files/:file", s.getTeamAgentFile) + g.PUT("/teams/:name/agents/:agent/files/:file", s.saveTeamAgentFile) + g.GET("/teams/:name/knowledge", s.listTeamKnowledge) + g.GET("/teams/:name/knowledge/:file", s.getTeamKnowledgeFile) + g.PUT("/teams/:name/knowledge/:file", s.saveTeamKnowledgeFile) + g.DELETE("/teams/:name", s.deleteTeam) + g.GET("/rooms/:id/workspace", s.listWorkspace) g.GET("/rooms/:id/workspace/:file", s.getWorkspaceFile) g.GET("/rooms/:id/tasks", s.getTasks) g.GET("/rooms/:id/history", s.listHistory) g.GET("/rooms/:id/messages", s.getMessages) + g.GET("/user", s.getUser) + g.GET("/user/profile", s.getUserProfile) + g.PUT("/user/profile", s.saveUserProfile) + g.PUT("/user/config", s.saveUserConfig) s.e.GET("/ws/:roomID", s.wsHandler) } @@ -83,11 +101,33 @@ func (s *Server) loadRooms() { if err != nil { continue } + r.User = s.user r.Broadcast = func(ev room.Event) { s.broadcast(ev.RoomID, ev) } s.rooms[e.Name()] = r } } +func (s *Server) loadUser() { + u, err := user.Load(s.usersDir) + if err != nil { + u = &user.User{ + Config: user.Config{ + Name: "用户", + Provider: "deepseek", + Model: "deepseek-chat", + APIKeyEnv: "DEEPSEEK_API_KEY", + AvatarColor: "#5865F2", + }, + Profile: "# 我的简介\n\n## 我是谁\n(介绍你的身份、职业背景)\n\n## 工作风格\n- 喜欢的沟通方式\n- 重视的点\n- 不喜欢的内容\n", + Dir: filepath.Join(s.usersDir, user.DefaultUser), + } + os.MkdirAll(u.Dir, 0755) + os.WriteFile(filepath.Join(u.Dir, "USER.md"), []byte("---\nname: 用户\ndescription: \nprovider: deepseek\nmodel: deepseek-chat\napi_key_env: DEEPSEEK_API_KEY\navatar_color: \"#5865F2\"\n---\n"), 0644) + os.WriteFile(filepath.Join(u.Dir, "PROFILE.md"), []byte(u.Profile), 0644) + } + s.user = u +} + func (s *Server) broadcast(roomID string, ev room.Event) { s.clientsMu.Lock() defer s.clientsMu.Unlock() @@ -123,8 +163,9 @@ func (s *Server) wsHandler(c echo.Context) error { break } var ev struct { - Type string `json:"type"` - Content string `json:"content"` + Type string `json:"type"` + Content string `json:"content"` + UserName string `json:"user_name"` } if json.Unmarshal(msg, &ev) != nil || ev.Type != "user_message" { continue @@ -135,7 +176,11 @@ func (s *Server) wsHandler(c echo.Context) error { if r == nil { continue } - go r.Handle(context.Background(), ev.Content) + userName := ev.UserName + if userName == "" { + userName = s.user.GetName() + } + go r.HandleUserMessage(context.Background(), userName, ev.Content) } return nil } @@ -244,20 +289,6 @@ func (s *Server) deleteAgent(c echo.Context) error { return os.RemoveAll(filepath.Join(s.agentsDir, name)) } -func (s *Server) listSkills(c echo.Context) error { - metas, _ := skill.Discover(s.skillsDir) - return c.JSON(200, metas) -} - -func (s *Server) getSkill(c echo.Context) error { - name := c.Param("name") - sk, err := skill.Load(filepath.Join(s.skillsDir, name)) - if err != nil { - return c.JSON(404, map[string]string{"error": "not found"}) - } - return c.JSON(200, sk) -} - func (s *Server) hubInstall(c echo.Context) error { var body struct { Repo string `json:"repo"` @@ -265,11 +296,43 @@ func (s *Server) hubInstall(c echo.Context) error { if err := c.Bind(&body); err != nil { return err } - if err := hub.Install(body.Repo, s.agentsDir, s.skillsDir); err != nil { + team, err := hub.Install(body.Repo, s.agentsDir, s.skillsDir, s.teamsDir) + if err != nil { return c.JSON(500, map[string]string{"error": err.Error()}) } s.loadRooms() - return c.JSON(200, map[string]string{"status": "installed"}) + return c.JSON(200, team) +} + +func (s *Server) hubInstallZIP(c echo.Context) error { + file, err := c.FormFile("file") + if err != nil { + return c.JSON(400, map[string]string{"error": "no file uploaded"}) + } + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + tmpFile := filepath.Join(os.TempDir(), file.Filename) + dst, err := os.Create(tmpFile) + if err != nil { + return err + } + defer os.Remove(tmpFile) + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return err + } + + team, err := hub.InstallZIP(tmpFile, s.agentsDir, s.skillsDir, s.teamsDir) + if err != nil { + return c.JSON(500, map[string]string{"error": err.Error()}) + } + s.loadRooms() + return c.JSON(200, team) } func (s *Server) listWorkspace(c echo.Context) error { @@ -370,3 +433,240 @@ func (s *Server) getMessages(c echo.Context) error { } return c.JSON(200, msgs) } + +func (s *Server) getUser(c echo.Context) error { + if s.user == nil { + return c.JSON(404, map[string]string{"error": "user not found"}) + } + return c.JSON(200, map[string]interface{}{ + "name": s.user.GetName(), + "provider": s.user.GetProvider(), + "model": s.user.GetModel(), + "api_key_env": s.user.GetAPIKeyEnv(), + "avatar_color": s.user.GetAvatarColor(), + "description": s.user.Config.Description, + "has_profile": s.user.Profile != "", + }) +} + +func (s *Server) getUserProfile(c echo.Context) error { + if s.user == nil { + return c.JSON(404, map[string]string{"error": "user not found"}) + } + return c.JSON(200, map[string]string{"content": s.user.Profile}) +} + +func (s *Server) saveUserProfile(c echo.Context) error { + if s.user == nil { + return c.JSON(404, map[string]string{"error": "user not found"}) + } + var body struct { + Content string `json:"content"` + } + if err := c.Bind(&body); err != nil { + return err + } + if err := s.user.SaveProfile(body.Content); err != nil { + return err + } + return c.JSON(200, map[string]string{"status": "ok"}) +} + +func (s *Server) saveUserConfig(c echo.Context) error { + if s.user == nil { + return c.JSON(404, map[string]string{"error": "user not found"}) + } + var body struct { + Name string `json:"name"` + Description string `json:"description"` + Provider string `json:"provider"` + Model string `json:"model"` + APIKeyEnv string `json:"api_key_env"` + AvatarColor string `json:"avatar_color"` + } + if err := c.Bind(&body); err != nil { + return err + } + cfg := s.user.Config + if body.Name != "" { + cfg.Name = body.Name + } + if body.Description != "" { + cfg.Description = body.Description + } + if body.Provider != "" { + cfg.Provider = body.Provider + } + if body.Model != "" { + cfg.Model = body.Model + } + if body.APIKeyEnv != "" { + cfg.APIKeyEnv = body.APIKeyEnv + } + if body.AvatarColor != "" { + cfg.AvatarColor = body.AvatarColor + } + if err := s.user.SaveConfig(cfg); err != nil { + return err + } + return c.JSON(200, map[string]string{"status": "ok"}) +} + +// --- Teams API --- + +func (s *Server) listTeams(c echo.Context) error { + entries, err := os.ReadDir(s.teamsDir) + if err != nil { + return c.JSON(200, []interface{}{}) + } + var teams []interface{} + for _, e := range entries { + if !e.IsDir() { + continue + } + team, err := loadTeam(filepath.Join(s.teamsDir, e.Name(), "team.yaml")) + if err != nil { + continue + } + teams = append(teams, map[string]interface{}{ + "name": team.Name, + "description": team.Description, + "author": team.Author, + "repo_url": team.RepoURL, + "agents": team.Agents, + "skills": team.Skills, + "installed_at": team.InstalledAt, + }) + } + return c.JSON(200, teams) +} + +func (s *Server) getTeam(c echo.Context) error { + name := c.Param("name") + team, err := loadTeam(filepath.Join(s.teamsDir, name, "team.yaml")) + if err != nil { + return c.JSON(404, map[string]string{"error": "team not found"}) + } + return c.JSON(200, map[string]interface{}{ + "name": team.Name, + "description": team.Description, + "author": team.Author, + "repo_url": team.RepoURL, + "agents": team.Agents, + "skills": team.Skills, + "installed_at": team.InstalledAt, + }) +} + +func (s *Server) getTeamAgentFile(c echo.Context) error { + teamName := c.Param("name") + agentName := c.Param("agent") + fileName := c.Param("file") + path := filepath.Join(s.agentsDir, teamName, agentName, fileName) + data, err := os.ReadFile(path) + if err != nil { + return c.JSON(404, map[string]string{"error": "file not found"}) + } + return c.JSON(200, map[string]string{"content": string(data)}) +} + +func (s *Server) saveTeamAgentFile(c echo.Context) error { + teamName := c.Param("name") + agentName := c.Param("agent") + fileName := c.Param("file") + path := filepath.Join(s.agentsDir, teamName, agentName, fileName) + var body struct { + Content string `json:"content"` + } + if err := c.Bind(&body); err != nil { + return err + } + os.MkdirAll(filepath.Dir(path), 0755) + if err := os.WriteFile(path, []byte(body.Content), 0644); err != nil { + return err + } + return c.JSON(200, map[string]string{"status": "ok"}) +} + +func (s *Server) listTeamKnowledge(c echo.Context) error { + name := c.Param("name") + dir := filepath.Join(s.agentsDir, name, "knowledge") + entries, err := os.ReadDir(dir) + if err != nil { + return c.JSON(200, []string{}) + } + var files []string + for _, e := range entries { + if !e.IsDir() { + files = append(files, e.Name()) + } + } + return c.JSON(200, files) +} + +func (s *Server) getTeamKnowledgeFile(c echo.Context) error { + name := c.Param("name") + fileName := c.Param("file") + path := filepath.Join(s.agentsDir, name, "knowledge", fileName) + data, err := os.ReadFile(path) + if err != nil { + return c.JSON(404, map[string]string{"error": "file not found"}) + } + return c.JSON(200, map[string]string{"content": string(data)}) +} + +func (s *Server) saveTeamKnowledgeFile(c echo.Context) error { + name := c.Param("name") + fileName := c.Param("file") + path := filepath.Join(s.agentsDir, name, "knowledge", fileName) + var body struct { + Content string `json:"content"` + } + if err := c.Bind(&body); err != nil { + return err + } + os.MkdirAll(filepath.Dir(path), 0755) + if err := os.WriteFile(path, []byte(body.Content), 0644); err != nil { + return err + } + return c.JSON(200, map[string]string{"status": "ok"}) +} + +func (s *Server) deleteTeam(c echo.Context) error { + name := c.Param("name") + // Delete team directory + if err := os.RemoveAll(filepath.Join(s.teamsDir, name)); err != nil { + return err + } + // Delete agents + if err := os.RemoveAll(filepath.Join(s.agentsDir, name)); err != nil { + // Continue even if error + } + return c.JSON(200, map[string]string{"status": "ok"}) +} + +func loadTeam(path string) (*hub.Team, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + // Simple YAML parse + team := &hub.Team{} + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "name: ") { + team.Name = strings.TrimSpace(strings.TrimPrefix(line, "name: ")) + } else if strings.HasPrefix(line, "description: ") { + team.Description = strings.TrimSpace(strings.TrimPrefix(line, "description: ")) + } else if strings.HasPrefix(line, "author: ") { + team.Author = strings.TrimSpace(strings.TrimPrefix(line, "author: ")) + } else if strings.HasPrefix(line, "repo_url: ") { + team.RepoURL = strings.TrimSpace(strings.TrimPrefix(line, "repo_url: ")) + } else if strings.HasPrefix(line, "installed_at: ") { + team.InstalledAt = strings.TrimSpace(strings.TrimPrefix(line, "installed_at: ")) + } else if strings.HasPrefix(line, " - ") { + team.Agents = append(team.Agents, strings.TrimSpace(strings.TrimPrefix(line, " - "))) + } + } + return team, nil +} diff --git a/internal/hub/hub.go b/internal/hub/hub.go index 4b68e03..ec7d2b2 100644 --- a/internal/hub/hub.go +++ b/internal/hub/hub.go @@ -1,23 +1,38 @@ package hub import ( + "archive/zip" "fmt" "os" "os/exec" "path/filepath" "strings" + "time" ) -// Install clones a GitHub repo (owner/repo or full URL) and installs the team. -func Install(repoRef, agentsDir, skillsDir string) error { +type Team struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Author string `yaml:"author"` + RepoURL string `yaml:"repo_url"` + Agents []string `yaml:"agents"` + Skills []string `yaml:"skills"` + InstalledAt string `yaml:"installed_at"` +} + +// Install clones a git repo (any URL) and installs the team. +func Install(repoRef, agentsDir, skillsDir, teamsDir string) (*Team, error) { url := repoRef - if !strings.HasPrefix(repoRef, "http") { + if !strings.HasPrefix(repoRef, "http") && !strings.HasPrefix(repoRef, "git@") { url = "https://github.com/" + repoRef } + // Extract repo name for team name + repoName := extractRepoName(url) + tmp, err := os.MkdirTemp("", "agent-team-hub-*") if err != nil { - return err + return nil, err } defer os.RemoveAll(tmp) @@ -25,20 +40,175 @@ func Install(repoRef, agentsDir, skillsDir string) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return fmt.Errorf("git clone: %w", err) + return nil, fmt.Errorf("git clone: %w", err) } - // Copy agents/ - if err := copyDir(filepath.Join(tmp, "agents"), agentsDir); err != nil { - return fmt.Errorf("install agents: %w", err) + return installFromDir(tmp, repoName, url, agentsDir, skillsDir, teamsDir) +} + +// InstallZIP extracts a ZIP file and installs the team. +func InstallZIP(zipPath, agentsDir, skillsDir, teamsDir string) (*Team, error) { + // Extract team name from zip filename + teamName := strings.TrimSuffix(filepath.Base(zipPath), ".zip") + teamName = strings.TrimSuffix(teamName, "-main") + teamName = strings.TrimSuffix(teamName, "-master") + + tmp, err := os.MkdirTemp("", "agent-team-hub-*") + if err != nil { + return nil, err } - // Copy skills/ - if err := copyDir(filepath.Join(tmp, "skills"), skillsDir); err != nil { - return fmt.Errorf("install skills: %w", err) + defer os.RemoveAll(tmp) + + // Extract ZIP + if err := extractZIP(zipPath, tmp); err != nil { + return nil, fmt.Errorf("extract zip: %w", err) + } + + // Find the root directory (might be wrapped in a subfolder) + entries, _ := os.ReadDir(tmp) + if len(entries) == 1 && entries[0].IsDir() { + tmp = filepath.Join(tmp, entries[0].Name()) + } + + return installFromDir(tmp, teamName, "", agentsDir, skillsDir, teamsDir) +} + +func installFromDir(tmp, teamName, repoURL, agentsDir, skillsDir, teamsDir string) (*Team, error) { + // Copy agents + installedAgents := []string{} + srcAgentsDir := filepath.Join(tmp, "agents") + if _, err := os.Stat(srcAgentsDir); err == nil { + // Copy to team-specific directory + dstAgentsDir := filepath.Join(agentsDir, teamName) + if err := copyDir(srcAgentsDir, dstAgentsDir); err == nil { + entries, _ := os.ReadDir(srcAgentsDir) + for _, e := range entries { + if e.IsDir() { + installedAgents = append(installedAgents, e.Name()) + } + } + } + } + + // Copy skills + installedSkills := []string{} + srcSkillsDir := filepath.Join(tmp, "skills") + if _, err := os.Stat(srcSkillsDir); err == nil { + dstSkillsDir := filepath.Join(skillsDir, teamName) + if err := copyDir(srcSkillsDir, dstSkillsDir); err == nil { + entries, _ := os.ReadDir(srcSkillsDir) + for _, e := range entries { + if e.IsDir() { + installedSkills = append(installedSkills, e.Name()) + } + } + } + } + + // Create knowledge dir + os.MkdirAll(filepath.Join(agentsDir, teamName, "knowledge"), 0755) + + // Create team record + team := &Team{ + Name: teamName, + Description: "团队描述", + RepoURL: repoURL, + Agents: installedAgents, + Skills: installedSkills, + InstalledAt: time.Now().Format("2006-01-02"), + } + + // Save team record + teamDir := filepath.Join(teamsDir, teamName) + os.MkdirAll(teamDir, 0755) + team.save(filepath.Join(teamDir, "team.yaml")) + + return team, nil +} + +func extractZIP(zipPath, dest string) error { + r, err := zip.OpenReader(zipPath) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + fpath := filepath.Join(dest, f.Name) + + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, 0755) + continue + } + + if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { + return err + } + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + outFile.Close() + return err + } + + buf := make([]byte, 4096) + for { + n, err := rc.Read(buf) + if n > 0 { + if _, writeErr := outFile.Write(buf[:n]); writeErr != nil { + rc.Close() + outFile.Close() + return writeErr + } + } + if err != nil { + break + } + } + rc.Close() + outFile.Close() } return nil } +func extractRepoName(url string) string { + parts := strings.Split(url, "/") + name := parts[len(parts)-1] + name = strings.TrimSuffix(name, ".git") + return name +} + +func (t *Team) save(path string) error { + // Simple YAML save + content := fmt.Sprintf(`name: %s +description: %s +author: %s +repo_url: %s +agents: +%s +skills: +%s +installed_at: %s +`, t.Name, t.Description, t.Author, t.RepoURL, arrayToYAML(t.Agents), arrayToYAML(t.Skills), t.InstalledAt) + return os.WriteFile(path, []byte(content), 0644) +} + +func arrayToYAML(arr []string) string { + if len(arr) == 0 { + return " []" + } + result := "" + for _, s := range arr { + result += fmt.Sprintf(" - %s\n", s) + } + return result +} + func copyDir(src, dst string) error { if _, err := os.Stat(src); os.IsNotExist(err) { return nil // optional dir, skip diff --git a/internal/room/room.go b/internal/room/room.go index 7fa48dd..4c687af 100644 --- a/internal/room/room.go +++ b/internal/room/room.go @@ -12,6 +12,7 @@ import ( "github.com/sdaduanbilei/agent-team/internal/agent" "github.com/sdaduanbilei/agent-team/internal/llm" "github.com/sdaduanbilei/agent-team/internal/skill" + "github.com/sdaduanbilei/agent-team/internal/user" "gopkg.in/yaml.v3" ) @@ -43,6 +44,7 @@ type Room struct { master *agent.Agent members map[string]*agent.Agent skillMeta []skill.Meta + User *user.User Status Status ActiveAgent string // for working status display Broadcast func(Event) // set by api layer @@ -133,13 +135,26 @@ func (r *Room) AppendHistory(role, agentName, content string) { // Handle processes a user message through master orchestration. func (r *Room) Handle(ctx context.Context, userMsg string) error { - r.AppendHistory("user", "user", userMsg) + return r.HandleUserMessage(ctx, "user", userMsg) +} + +// HandleUserMessage processes a user message with a specific user name. +func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) error { + r.AppendHistory("user", userName, userMsg) r.setStatus(StatusThinking, "", "") // Build master context teamXML := r.buildTeamXML() skillXML := skill.ToXML(r.skillMeta) - systemPrompt := r.master.BuildSystemPrompt(teamXML + "\n\n" + skillXML) + + // Build user info XML + var userXML string + if r.User != nil { + userXML = r.User.BuildUserXML() + } + + extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillXML + systemPrompt := r.master.BuildSystemPrompt(extraContext) masterMsgs := []llm.Message{ llm.NewMsg("system", systemPrompt+"\n\nYou are the master of this team. When you need a team member to do something, output a line like: ASSIGN::. When you are done reviewing and satisfied, output DONE:."), diff --git a/internal/user/user.go b/internal/user/user.go new file mode 100644 index 0000000..138f4cf --- /dev/null +++ b/internal/user/user.go @@ -0,0 +1,127 @@ +package user + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Provider string `yaml:"provider"` + Model string `yaml:"model"` + APIKeyEnv string `yaml:"api_key_env"` + AvatarColor string `yaml:"avatar_color"` + Preferences Preferences `yaml:"preferences"` +} + +type Preferences struct { + Language string `yaml:"language"` + Tone string `yaml:"tone"` +} + +type User struct { + Config Config + Profile string // content from PROFILE.md + Dir string // users// +} + +const DefaultUser = "default" + +func Load(dir string) (*User, error) { + userDir := filepath.Join(dir, DefaultUser) + + userMD, err := os.ReadFile(filepath.Join(userDir, "USER.md")) + if err != nil { + return nil, fmt.Errorf("read USER.md: %w", err) + } + + cfg, err := parseFrontmatter(userMD) + if err != nil { + return nil, fmt.Errorf("parse USER.md: %w", err) + } + + profile, _ := os.ReadFile(filepath.Join(userDir, "PROFILE.md")) + + if cfg.Provider == "" { + cfg.Provider = "deepseek" + } + if cfg.APIKeyEnv == "" { + cfg.APIKeyEnv = "DEEPSEEK_API_KEY" + } + if cfg.AvatarColor == "" { + cfg.AvatarColor = "#5865F2" + } + + return &User{Config: cfg, Profile: string(profile), Dir: userDir}, nil +} + +func parseFrontmatter(data []byte) (Config, error) { + var cfg Config + if !bytes.HasPrefix(data, []byte("---")) { + return cfg, fmt.Errorf("missing frontmatter") + } + parts := bytes.SplitN(data, []byte("---"), 3) + if len(parts) < 3 { + return cfg, fmt.Errorf("invalid frontmatter") + } + return cfg, yaml.Unmarshal(parts[1], &cfg) +} + +func (u *User) SaveProfile(content string) error { + return os.WriteFile(filepath.Join(u.Dir, "PROFILE.md"), []byte(content), 0644) +} + +func (u *User) SaveConfig(cfg Config) error { + u.Config = cfg + var buf bytes.Buffer + buf.WriteString("---\n") + enc := yaml.NewEncoder(&buf) + enc.Encode(cfg) + buf.WriteString("---\n") + return os.WriteFile(filepath.Join(u.Dir, "USER.md"), buf.Bytes(), 0644) +} + +func (u *User) BuildUserXML() string { + var xml strings.Builder + xml.WriteString("\n") + xml.WriteString(fmt.Sprintf(" %s\n", u.Config.Name)) + xml.WriteString(fmt.Sprintf(" %s\n", u.Config.Description)) + if u.Profile != "" { + xml.WriteString(" \n") + xml.WriteString(u.Profile) + xml.WriteString(" \n") + } + xml.WriteString("") + return xml.String() +} + +func (u *User) GetProvider() string { + return u.Config.Provider +} + +func (u *User) GetModel() string { + return u.Config.Model +} + +func (u *User) GetAPIKeyEnv() string { + return u.Config.APIKeyEnv +} + +func (u *User) GetName() string { + return u.Config.Name +} + +func (u *User) GetAvatarColor() string { + return u.Config.AvatarColor +} + +func Exists(dir string) bool { + _, err := os.Stat(filepath.Join(dir, DefaultUser, "USER.md")) + return err == nil +} diff --git a/skills/legal-team/合同审查/SKILL.md b/skills/legal-team/合同审查/SKILL.md new file mode 100644 index 0000000..25142c2 --- /dev/null +++ b/skills/legal-team/合同审查/SKILL.md @@ -0,0 +1,197 @@ +--- +name: 合同审查 +description: 专业的合同审查技能,提供系统的合同风险评估和修改建议 +version: 1.0.0 +tags: + - 法律 + - 合同 + - 风险评估 +--- + +# 合同审查技能 + +## 功能描述 + +提供专业的合同审查服务,包括: + +- 合同条款合法性审查 +- 权利义务平衡性分析 +- 违约责任合理性评估 +- 风险点识别和修改建议 + +## 使用场景 + +- 审查买卖合同 +- 审查服务合同 +- 审查租赁合同 +- 审查劳动合同 +- 审查合作协议 + +## 输入要求 + +用户提供: +- 合同文本(全文或关键条款) +- 合同类型和背景信息 +- 重点关注的条款或问题 + +## 执行步骤 + +### 1. 合同基本信息识别 +- 合同类型判断 +- 当事人信息提取 +- 合同标的和金额 +- 合同期限 + +### 2. 条款逐项审查 +检查以下关键条款: + +**主体条款** +- 签约主体资格 +- 授权和代理权限 +- 联系方式和送达地址 + +**权利义务条款** +- 各方权利是否明确 +- 各方义务是否具体 +- 权利义务是否对等 + +**价款/报酬条款** +- 金额和支付方式 +- 支付时间和条件 +- 发票和税务处理 + +**履行条款** +- 履行期限和地点 +- 履行标准和验收 +- 履行顺序和条件 + +**违约责任条款** +- 违约情形列举 +- 违约责任承担方式 +- 违约金或赔偿标准 + +**争议解决条款** +- 协商、调解机制 +- 仲裁或诉讼选择 +- 管辖法院或仲裁机构 + +**其他条款** +- 保密条款 +- 知识产权条款 +- 不可抗力条款 +- 合同变更和解除 + +### 3. 风险评估 +对每个风险点进行评级: +- **高风险**:可能导致重大损失或法律纠纷 +- **中风险**:可能影响合同履行或产生争议 +- **低风险**:建议优化但不影响合同效力 + +### 4. 修改建议 +针对每个风险点提供: +- 问题描述 +- 风险分析 +- 具体修改建议 +- 修改理由 + +## 输出格式 + +```markdown +## 合同审查报告 + +### 基本信息 +- 合同类型:[类型] +- 审查日期:[日期] +- 风险等级:[高/中/低] + +### 主要发现 + +#### 🔴 高风险条款 +[列出高风险条款,包括位置、问题、建议] + +#### 🟡 中风险条款 +[列出中风险条款,包括位置、问题、建议] + +#### 🟢 低风险条款 +[列出建议优化的条款] + +### 修改建议 + +#### 条款1:[条款名称] +**原文:** +> [引用原文] + +**问题:** +- [问题描述] + +**建议修改为:** +> [修改后的文本] + +**修改理由:** +- [说明理由] + +### 总体评价 +- 合同完整性:[评分] +- 风险可控性:[评估] +- 是否建议签署:[建议] + +### 特别提示 +[需要用户特别注意的事项] +``` + +## 注意事项 + +1. 审查基于用户提供的信息,假设信息真实准确 +2. 审查意见仅供参考,不构成正式法律意见 +3. 涉及重大金额或复杂法律关系,建议咨询专业律师 +4. 审查结果应在具体业务场景下理解和应用 + +## 示例 + +**用户输入:** +``` +请审查这份服务合同的违约责任条款: + +"任何一方违反本合同约定的,应向对方支付违约金10万元。" +``` + +**技能输出:** +```markdown +## 违约责任条款审查 + +### 风险等级:🟡 中风险 + +### 问题分析 + +1. **违约情形不明确** + - 未列举具体的违约行为 + - 可能导致争议时难以认定是否违约 + +2. **违约金数额固定** + - 10万元违约金可能过高或过低 + - 未考虑违约造成的实际损失 + +3. **缺乏其他违约责任方式** + - 仅约定违约金,未约定继续履行、赔偿损失等 + - 可能无法全面保护守约方权益 + +### 修改建议 + +**建议修改为:** +> "任何一方违反本合同约定的,应承担以下违约责任: +> +> 1. 甲方违约的,应退还乙方已支付的服务费用,并向乙方支付相当于服务费用20%的违约金; +> +> 2. 乙方违约的,甲方有权不退还已收取的服务费用,并有权要求乙方支付相当于服务费用20%的违约金; +> +> 3. 违约金不足以弥补实际损失的,违约方还应赔偿守约方的实际损失。 +> +> 本合同所称实际损失包括但不限于直接损失、间接损失、维权费用(包括律师费、诉讼费等)。" + +### 修改理由 + +1. 明确区分不同违约情形和责任 +2. 违约金与实际损失挂钩,更合理 +3. 增加实际损失赔偿,保护更全面 +4. 明确损失范围,避免争议 +``` \ No newline at end of file diff --git a/skills/legal-team/法律知识库/SKILL.md b/skills/legal-team/法律知识库/SKILL.md new file mode 100644 index 0000000..ce3fe7c --- /dev/null +++ b/skills/legal-team/法律知识库/SKILL.md @@ -0,0 +1,273 @@ +--- +name: 法律知识库 +description: 提供法律法规查询、案例参考和法律条文解读的技能 +version: 1.0.0 +tags: + - 法律 + - 知识库 + - 法规查询 +--- + +# 法律知识库技能 + +## 功能描述 + +提供法律知识支持,包括: + +- 法律法规查询和解读 +- 相关案例参考 +- 法律条文适用分析 +- 法律问题解答 + +## 使用场景 + +- 查询法律条文 +- 了解法规要求 +- 参考类似案例 +- 学习法律知识 + +## 输入要求 + +用户提供: +- 法律问题或关键词 +- 具体场景或背景 +- 需要查询的法律领域 + +## 执行步骤 + +### 1. 问题分析 +- 理解用户问题的核心 +- 识别相关法律领域 +- 确定查询方向 + +### 2. 法规检索 +检索相关法律法规: + +**基本法律** +- 民法典 +- 公司法 +- 合同法(已并入民法典) +- 劳动法 + +**行政法规** +- 行政法规 +- 部门规章 +- 地方性法规 + +**司法解释** +- 最高人民法院司法解释 +- 最高人民检察院司法解释 + +### 3. 条文解读 +- 提取相关法律条文 +- 解释条文含义 +- 说明适用条件 + +### 4. 案例参考 +提供相关案例: +- 最高人民法院指导案例 +- 典型判决案例 +- 相关案例分析 + +## 输出格式 + +```markdown +## 法律知识查询结果 + +### 一、相关法律条文 + +#### [法律名称] + +**第X条** [条款标题] + +> [条文原文] + +**条文解读:** +- [解读内容] +- [适用条件] +- [注意事项] + +### 二、相关法规 + +#### [法规名称] + +**第X条** +> [条文原文] + +**适用要点:** +- [要点1] +- [要点2] + +### 三、案例参考 + +#### 案例:[案例名称] + +**案情简介:** +[简要案情] + +**争议焦点:** +[焦点问题] + +**法院裁判:** +[裁判结果和理由] + +**参考价值:** +[对用户问题的参考意义] + +### 四、实务建议 + +1. [建议1] +2. [建议2] +3. [建议3] + +### 五、特别提示 + +[需要特别注意的法律风险或程序要求] +``` + +## 知识范围 + +### 民商事法律 +- 民法典(总则、物权、合同、侵权等) +- 公司法 +- 证券法 +- 破产法 +- 票据法 + +### 劳动法律 +- 劳动法 +- 劳动合同法 +- 劳动争议调解仲裁法 +- 社会保险法 + +### 知识产权法律 +- 著作权法 +- 商标法 +- 专利法 +- 反不正当竞争法 + +### 行业法规 +- 互联网信息服务管理办法 +- 电子商务法 +- 网络安全法 +- 数据安全法 +- 个人信息保护法 + +## 注意事项 + +1. 法律知识更新及时,但以最新公布的法律法规为准 +2. 提供的案例仅供参考,具体案件需结合实际情况 +3. 复杂法律问题建议咨询专业律师 +4. 法律条文的理解和适用可能存在争议 + +## 示例 + +**用户输入:** +``` +公司想辞退一名员工,请问劳动法有哪些规定? +``` + +**技能输出:** +```markdown +## 公司辞退员工的法律规定 + +### 一、相关法律条文 + +#### 《中华人民共和国劳动合同法》 + +**第三十六条** 协商解除劳动合同 + +> 用人单位与劳动者协商一致,可以解除劳动合同。 + +**第三十九条** 用人单位单方解除劳动合同(过失性辞退) + +> 劳动者有下列情形之一的,用人单位可以解除劳动合同: +> (一)在试用期间被证明不符合录用条件的; +> (二)严重违反用人单位的规章制度的; +> (三)严重失职,营私舞弊,给用人单位造成重大损害的; +> (四)劳动者同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的; +> (五)因本法第二十六条第一款第一项规定的情形致使劳动合同无效的; +> (六)被依法追究刑事责任的。 + +**第四十条** 无过失性辞退 + +> 有下列情形之一的,用人单位提前三十日以书面形式通知劳动者本人或者额外支付劳动者一个月工资后,可以解除劳动合同: +> (一)劳动者患病或者非因工负伤,在规定的医疗期满后不能从事原工作,也不能从事由用人单位另行安排的工作的; +> (二)劳动者不能胜任工作,经过培训或者调整工作岗位,仍不能胜任工作的; +> (三)劳动合同订立时所依据的客观情况发生重大变化,致使劳动合同无法履行,经用人单位与劳动者协商,未能就变更劳动合同内容达成协议的。 + +**第四十一条** 经济性裁员 + +> 有下列情形之一,需要裁减人员二十人以上或者裁减不足二十人但占企业职工总数百分之十以上的,用人单位提前三十日向工会或者全体职工说明情况,听取工会或者职工的意见后,裁减人员方案经向劳动行政部门报告,可以裁减人员: +> (一)依照企业破产法规定进行重整的; +> (二)生产经营发生严重困难的; +> (三)企业转产、重大技术革新或者经营方式调整,经变更劳动合同后,仍需裁减人员的; +> (四)其他因劳动合同订立时所依据的客观情况发生重大变化,致使劳动合同无法履行的。 + +### 二、经济补偿规定 + +**第四十六条** 应当支付经济补偿的情形 + +> 有下列情形之一的,用人单位应当向劳动者支付经济补偿: +> (一)劳动者依照本法第三十八条规定解除劳动合同的; +> (二)用人单位依照本法第三十六条规定向劳动者提出解除劳动合同并与劳动者协商一致解除劳动合同的; +> (三)用人单位依照本法第四十条规定解除劳动合同的; +> (四)用人单位依照本法第四十一条第一款规定解除劳动合同的; +> (五)除用人单位维持或者提高劳动合同约定条件续订劳动合同,劳动者不同意续订的情形外,依照本法第四十四条第一项规定终止固定期限劳动合同的; +> (六)依照本法第四十四条第四项、第五项规定终止劳动合同的; +> (七)法律、行政法规规定的其他情形。 + +**第四十七条** 经济补偿的计算 + +> 经济补偿按劳动者在本单位工作的年限,每满一年支付一个月工资的标准向劳动者支付。六个月以上不满一年的,按一年计算;不满六个月的,向劳动者支付半个月工资的经济补偿。 +> +> 劳动者月工资高于用人单位所在直辖市、设区的市级人民政府公布的本地区上年度职工月平均工资三倍的,向其支付经济补偿的标准按职工月平均工资三倍的数额支付,向其支付经济补偿的年限最高不超过十二年。 +> +> 本条所称月工资是指劳动者在劳动合同解除或者终止前十二个月的平均工资。 + +### 三、违法解除的法律后果 + +**第八十七条** 违法解除或者终止劳动合同的法律责任 + +> 用人单位违反本法规定解除或者终止劳动合同的,应当依照本法第四十七条规定的经济补偿标准的二倍向劳动者支付赔偿金。 + +### 四、实务建议 + +#### 1. 选择合适的解除方式 +- **协商解除**:双方协商一致,风险最低 +- **过失性辞退**:需要充分证据证明员工存在法定过错 +- **无过失性辞退**:需提前30天通知或支付代通知金,并支付经济补偿 +- **经济性裁员**:需符合法定条件和程序 + +#### 2. 注意程序要求 +- 提前通知工会(如有) +- 书面通知员工 +- 办理工作交接 +- 支付经济补偿(如需) +- 出具解除劳动合同证明 +- 办理档案和社会保险关系转移手续 + +#### 3. 收集和保存证据 +- 员工违规的证据 +- 培训和调整岗位的记录 +- 考核不合格的材料 +- 规章制度公示的证据 + +### 五、特别提示 + +1. **不得解除的情形** + - 患职业病或者因工负伤并被确认丧失或者部分丧失劳动能力的 + - 患病或者非因工负伤,在规定的医疗期内的 + - 女职工在孕期、产期、哺乳期的 + - 在本单位连续工作满十五年,且距法定退休年龄不足五年的 + +2. **违法解除的后果** + - 继续履行劳动合同,或 + - 支付赔偿金(经济补偿的2倍) + +3. **证据的重要性** + - 用人单位对解除劳动合同的合法性承担举证责任 + - 证据不足可能导致被认定为违法解除 + +建议在辞退员工前,咨询专业劳动法律师,评估法律风险并制定合适的方案。 +``` \ No newline at end of file diff --git a/teams/legal-team/team.yaml b/teams/legal-team/team.yaml new file mode 100644 index 0000000..c6bd533 --- /dev/null +++ b/teams/legal-team/team.yaml @@ -0,0 +1,14 @@ +name: legal-team +description: 团队描述 +author: +repo_url: https://gitea.catter.cn/agent-teams/legal-team.git +agents: + - 合同律师 + - 合规专员 + - 法律总监 + +skills: + - 合同审查 + - 法律知识库 + +installed_at: 2026-03-05 diff --git a/users/default/PROFILE.md b/users/default/PROFILE.md new file mode 100644 index 0000000..2e0a400 --- /dev/null +++ b/users/default/PROFILE.md @@ -0,0 +1,9 @@ +# 我的简介 + +## 我是谁 +(介绍你的身份、职业背景) + +## 工作风格 +- 喜欢的沟通方式 +- 重视的点 +- 不喜欢的内容 diff --git a/users/default/USER.md b/users/default/USER.md new file mode 100644 index 0000000..44c937c --- /dev/null +++ b/users/default/USER.md @@ -0,0 +1,8 @@ +--- +name: 用户 +description: +provider: deepseek +model: deepseek-chat +api_key_env: DEEPSEEK_API_KEY +avatar_color: "#5865F2" +--- diff --git a/web/package-lock.json b/web/package-lock.json index 64b4467..2b118b4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,7 +8,9 @@ "name": "web", "version": "0.0.0", "dependencies": { - "@monaco-editor/react": "^4.7.0", + "@uiw/react-markdown-preview": "^5.1.5", + "@uiw/react-md-editor": "^4.0.11", + "lucide-react": "^0.577.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-markdown": "^10.1.0", @@ -269,6 +271,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1018,29 +1029,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@monaco-editor/loader": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", - "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", - "license": "MIT", - "dependencies": { - "state-local": "^1.0.6" - } - }, - "node_modules/@monaco-editor/react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", - "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", - "license": "MIT", - "dependencies": { - "@monaco-editor/loader": "^1.5.0" - }, - "peerDependencies": { - "monaco-editor": ">= 0.25.0 < 1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1793,6 +1781,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -1812,14 +1806,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2121,6 +2107,191 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uiw/copy-to-clipboard": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.20.tgz", + "integrity": "sha512-IFQhS62CLNon1YgYJTEzXR2N3WVXg7V1FaBRDLMlzU6JY5X6Hr3OPAcw4WNoKcz2XcFD6XCgwEjlsmj+JA0mWA==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/react-markdown-preview": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-5.1.5.tgz", + "integrity": "sha512-DNOqx1a6gJR7Btt57zpGEKTfHRlb7rWbtctMRO2f82wWcuoJsxPBrM+JWebDdOD0LfD8oe2CQvW2ICQJKHQhZg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@uiw/copy-to-clipboard": "~1.0.12", + "react-markdown": "~9.0.1", + "rehype-attr": "~3.0.1", + "rehype-autolink-headings": "~7.1.0", + "rehype-ignore": "^2.0.0", + "rehype-prism-plus": "2.0.0", + "rehype-raw": "^7.0.0", + "rehype-rewrite": "~4.0.0", + "rehype-slug": "~6.0.0", + "remark-gfm": "~4.0.0", + "remark-github-blockquote-alert": "^1.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/@uiw/react-markdown-preview/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/hast-util-parse-selector/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/react-markdown": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.3.tgz", + "integrity": "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/refractor": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-4.9.0.tgz", + "integrity": "sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^7.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/refractor/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/rehype-prism-plus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-2.0.0.tgz", + "integrity": "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==", + "license": "MIT", + "dependencies": { + "hast-util-to-string": "^3.0.0", + "parse-numeric-range": "^1.3.0", + "refractor": "^4.8.0", + "rehype-parse": "^9.0.0", + "unist-util-filter": "^5.0.0", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/@uiw/react-md-editor": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@uiw/react-md-editor/-/react-md-editor-4.0.11.tgz", + "integrity": "sha512-F0OR5O1v54EkZYvJj3ew0I7UqLiPeU34hMAY4MdXS3hI86rruYi5DHVkG/VuvLkUZW7wIETM2QFtZ459gKIjQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.14.6", + "@uiw/react-markdown-preview": "^5.0.6", + "rehype": "~13.0.0", + "rehype-prism-plus": "~2.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -2278,6 +2449,22 @@ "node": ">=6.0.0" } }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2480,6 +2667,22 @@ "node": ">= 8" } }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2568,14 +2771,17 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/electron-to-chromium": { @@ -2599,6 +2805,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2993,6 +3211,12 @@ "node": ">=6.9.0" } }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3036,6 +3260,171 @@ "node": ">=8" } }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -3063,6 +3452,38 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -3076,6 +3497,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3103,6 +3541,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3637,6 +4085,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3657,19 +4114,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/marked": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", - "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", - "license": "MIT", - "peer": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -4528,17 +4972,6 @@ "node": "*" } }, - "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", - "license": "MIT", - "peer": true, - "dependencies": { - "dompurify": "3.2.7", - "marked": "14.0.0" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4578,6 +5011,18 @@ "dev": true, "license": "MIT" }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4666,6 +5111,24 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4844,6 +5307,197 @@ "node": ">=0.10.0" } }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-attr": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/rehype-attr/-/rehype-attr-3.0.3.tgz", + "integrity": "sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==", + "license": "MIT", + "dependencies": { + "unified": "~11.0.0", + "unist-util-visit": "~5.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/rehype-attr/node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-ignore": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-2.0.3.tgz", + "integrity": "sha512-IzhP6/u/6sm49sdktuYSmeIuObWB+5yC/5eqVws8BhuGA9kY25/byz6uCy/Ravj6lXUShEd2ofHM5MyAIj86Sg==", + "license": "MIT", + "dependencies": { + "hast-util-select": "^6.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-prism-plus": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-2.0.2.tgz", + "integrity": "sha512-jTHb8ZtQHd2VWAAKeCINgv/8zNEF0+LesmwJak69GemoPVN9/8fGEARTvqOpKqmN57HwaM9z8UKBVNVJe8zggw==", + "license": "MIT", + "dependencies": { + "hast-util-to-string": "^3.0.1", + "parse-numeric-range": "^1.3.0", + "refractor": "^5.0.0", + "rehype-parse": "^9.0.1", + "unist-util-filter": "^5.0.1", + "unist-util-visit": "^5.1.0" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-rewrite": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-4.0.4.tgz", + "integrity": "sha512-L/FO96EOzSA6bzOam4DVu61/PB3AGKcSPXpa53yMIozoxH4qg1+bVZDF8zh1EsuxtSauAhzt5cCnvoplAaSLrw==", + "license": "MIT", + "dependencies": { + "hast-util-select": "^6.0.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -4862,6 +5516,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-github-blockquote-alert": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/remark-github-blockquote-alert/-/remark-github-blockquote-alert-1.3.1.tgz", + "integrity": "sha512-OPNnimcKeozWN1w8KVQEuHOxgN3L4rah8geMOLhA5vN9wITqU4FWD+G26tkEsCGHiOVDbISx+Se5rGZ+D1p0Jg==", + "license": "MIT", + "dependencies": { + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -5024,12 +5693,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/state-local": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", - "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", - "license": "MIT" - }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -5236,6 +5899,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-filter": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz", + "integrity": "sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -5366,6 +6040,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", @@ -5455,6 +6143,16 @@ } } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/web/package.json b/web/package.json index 26a1a68..b18360c 100644 --- a/web/package.json +++ b/web/package.json @@ -10,7 +10,9 @@ "preview": "vite preview" }, "dependencies": { - "@monaco-editor/react": "^4.7.0", + "@uiw/react-markdown-preview": "^5.1.5", + "@uiw/react-md-editor": "^4.0.11", + "lucide-react": "^0.577.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-markdown": "^10.1.0", diff --git a/web/src/App.tsx b/web/src/App.tsx index 69edbae..8046e6e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,35 +1,111 @@ +import { useEffect, useState } from 'react' +import { MessageSquare, ShoppingCart, Hash, Settings } from 'lucide-react' import { useStore } from './store' import { RoomSidebar } from './components/RoomSidebar' import { ChatView } from './components/ChatView' -import { AgentsPage } from './components/AgentsPage' -import { SkillsPage } from './components/SkillsPage' import { MarketPage } from './components/MarketPage' +import { UserSettings } from './components/UserSettings' +import { ThemeToggle } from './components/ThemeToggle' +import { Onboarding } from './components/Onboarding' export default function App() { - const { page, setPage } = useStore() + const { page, setPage, onboardingCompleted, setOnboardingCompleted, fetchUser } = useStore() + const [showOnboarding, setShowOnboarding] = useState(onboardingCompleted ? false : true) + + useEffect(() => { + fetchUser() + }, [fetchUser]) + + const handleOnboardingComplete = () => { + setShowOnboarding(false) + setOnboardingCompleted(true) + fetchUser() + } + + const navItems = [ + { id: 'chat', icon: MessageSquare, label: '群聊' }, + { id: 'market', icon: ShoppingCart, label: '市场' }, + ] as const return ( -
-
- setPage('chat')} /> - setPage('agents')} /> - setPage('skills')} /> - setPage('market')} /> -
+ <> + {showOnboarding && !onboardingCompleted && ( + + )} - {page === 'chat' && <>} - {page === 'agents' && } - {page === 'skills' && } - {page === 'market' && } -
+
+ {/* Left sidebar - Navigation */} +
+ {/* Logo */} +
+ +
+ + {/* Nav buttons */} +
+ {navItems.map(item => ( + } + label={item.label} + active={page === item.id} + onClick={() => setPage(item.id as typeof page)} + /> + ))} +
+ + {/* Divider */} +
+ + {/* Theme toggle */} + + + {/* Settings */} + +
+ + {/* Main content */} + {page === 'chat' && } + {page === 'chat' && } + {page === 'market' && } + {page === 'settings' && } +
+ ) } -function NavBtn({ icon, label, active, onClick }: { icon: string; label: string; active: boolean; onClick: () => void }) { +function NavBtn({ icon, label, active, onClick }: { icon: React.ReactNode; label: string; active: boolean; onClick: () => void }) { return ( - ) -} +} \ No newline at end of file diff --git a/web/src/components/AgentsPage.tsx b/web/src/components/AgentsPage.tsx deleted file mode 100644 index a29fe2f..0000000 --- a/web/src/components/AgentsPage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useEffect, useState } from 'react' -import Editor from '@monaco-editor/react' -import { useStore } from '../store' - -const API = '/api' - -export function AgentsPage() { - const { agents, fetchAgents } = useStore() - const [selected, setSelected] = useState(null) - const [tab, setTab] = useState<'AGENT.md' | 'SOUL.md'>('AGENT.md') - const [content, setContent] = useState('') - const [newName, setNewName] = useState('') - - useEffect(() => { fetchAgents() }, []) - - useEffect(() => { - if (!selected) return - fetch(`${API}/agents/${selected}/files/${tab}`).then(r => r.json()).then(d => setContent(d.content || '')) - }, [selected, tab]) - - const save = async () => { - if (!selected) return - await fetch(`${API}/agents/${selected}/files/${tab}`, { - method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content }) - }) - } - - const create = async () => { - if (!newName.trim()) return - await fetch(`${API}/agents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName.trim() }) }) - setNewName('') - fetchAgents() - } - - const del = async (name: string) => { - await fetch(`${API}/agents/${name}`, { method: 'DELETE' }) - if (selected === name) setSelected(null) - fetchAgents() - } - - return ( -
- {/* Agent list */} -
-
-
- setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && create()} /> - -
-
-
- {agents.map(a => ( -
setSelected(a.name)} - className={`flex items-center justify-between px-3 py-2 cursor-pointer text-sm hover:bg-gray-700 ${selected === a.name ? 'bg-gray-700' : ''}`}> - {a.name} - -
- ))} -
-
- - {/* Editor */} -
- {selected ? ( - <> -
- {selected} -
- {(['AGENT.md', 'SOUL.md'] as const).map(t => ( - - ))} -
- -
-
- setContent(v || '')} options={{ minimap: { enabled: false }, wordWrap: 'on', fontSize: 13 }} /> -
- - ) : ( -
选择一个 agent 编辑
- )} -
-
- ) -} diff --git a/web/src/components/ChatView.tsx b/web/src/components/ChatView.tsx index 17cee26..a9e7e4f 100644 --- a/web/src/components/ChatView.tsx +++ b/web/src/components/ChatView.tsx @@ -1,6 +1,17 @@ import { useEffect, useRef, useState } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' +import { + Hash, + Users, + ListTodo, + FileText, + X, + Plus, + Smile, + Send, + Crown, +} from 'lucide-react' import { useStore } from '../store' import type { Message } from '../types' @@ -9,7 +20,7 @@ const API = '/api' export function ChatView() { const { activeRoomId, rooms, messages, tasks, workspace, sendMessage } = useStore() const [input, setInput] = useState('') - const [drawer, setDrawer] = useState(null) + const [drawer, setDrawer] = useState(null) const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null) const bottomRef = useRef(null) @@ -25,115 +36,210 @@ export function ChatView() { setPreviewFile({ name: filename, content: d.content || '' }) } - if (!room) return
选择一个群开始
+ if (!room) { + return ( +
+ +

选择一个群开始聊天

+
+ ) + } const statusLabel = room.status === 'working' && room.activeAgent - ? `working · ${room.activeAgent} ${room.action || ''}` - : room.status + ? `${room.activeAgent} ${room.action || ''}` + : room.status === 'thinking' ? '思考中...' : room.status === 'working' ? '工作中...' : '空闲' + + const drawerButtons = [ + { id: 'members', icon: Users, label: '成员' }, + { id: 'tasks', icon: ListTodo, label: '任务' }, + { id: 'files', icon: FileText, label: '产物' }, + ] as const return ( -
- {/* Chat area */} -
-
- {room.name} - {statusLabel} +
+ {/* Main chat area */} +
+ {/* Header - Discord style */} +
+ + {room.name} +
+ + {room.type === 'dept' ? '部门群' : 'Leader 群'} + +
+ {/* Status badge */} + + + {statusLabel} +
-
+ {/* Messages */} +
+ {msgs.length === 0 && ( +
+ +

这里是 {room.name} 的开始

+

发送消息开始对话

+
+ )} {msgs.map(msg => )}
-
- setInput(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey && input.trim()) { sendMessage(room.id, input.trim()); setInput('') } }} - /> - + {/* Input area - Discord style */} +
+
+ + setInput(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter' && !e.shiftKey && input.trim()) { + sendMessage(room.id, input.trim()) + setInput('') + } + }} + /> + + +
{/* Right panel */} -
-
+
+ {/* Panel tabs */} +
+ {drawerButtons.map(btn => ( + + ))} +
+ + {/* Panel content */} +
{/* Members */} -
-

Members

- - {msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).map(name => ( - - ))} -
+ {(drawer === 'members' || drawer === null) && ( +
+

+ + 成员 — {1 + msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).length} +

+ + {msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).map(name => ( + + ))} +
+ )} {/* Tasks */} - {tasksMd && ( -
-

Tasks

-
+ {(drawer === 'tasks' || drawer === null) && tasksMd && ( +
+

+ + 任务列表 +

+
{tasksMd}
)} - {/* Workspace files */} - {files.length > 0 && ( -
-

产物

+ {/* Files */} + {(drawer === 'files' || drawer === null) && files.length > 0 && ( +
+

+ + 产物文件 +

{files.map(f => ( -
openFile(f)} - className="flex items-center gap-1 text-xs text-indigo-300 hover:text-indigo-200 cursor-pointer py-0.5"> - 📄{f} +
openFile(f)} + className="flex items-center gap-2 text-sm text-[var(--accent)] hover:text-[var(--accent-hover)] cursor-pointer py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors" + > + + {f}
))}
)}
- - {/* Drawer buttons */} -
- {(['skills', 'history', 'workspace'] as const).map(d => ( - - ))} -
- {/* Drawer overlay */} - {drawer && ( -
-
-

{drawer}

- -
- {drawer === 'workspace' && files.map(f => ( -
openFile(f)} className="text-sm text-indigo-300 py-1 cursor-pointer hover:text-indigo-200">📄 {f}
- ))} -
- )} - {/* File preview modal */} {previewFile && ( -
setPreviewFile(null)}> -
e.stopPropagation()}> -
- 📄 {previewFile.name} - +
setPreviewFile(null)} + > +
e.stopPropagation()} + > +
+
+ + {previewFile.name} +
+
-
+
{previewFile.content}
@@ -146,35 +252,95 @@ export function ChatView() { function MessageBubble({ msg }: { msg: Message }) { const isUser = msg.role === 'user' const isMaster = msg.role === 'master' + + const avatarColors = [ + 'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-yellow-500', + 'bg-lime-500', 'bg-green-500', 'bg-emerald-500', 'bg-teal-500', + 'bg-cyan-500', 'bg-sky-500', 'bg-blue-500', 'bg-indigo-500', + 'bg-violet-500', 'bg-purple-500', 'bg-fuchsia-500', 'bg-pink-500', 'bg-rose-500' + ] + const colorIndex = msg.agent.charCodeAt(0) % avatarColors.length + return ( -
+
+ {/* Avatar */} {!isUser && ( -
- {isMaster ? '👑' : msg.agent[0]?.toUpperCase()} +
+ {isMaster ? : msg.agent[0]?.toUpperCase()}
)} -
- {!isUser &&
{msg.agent}
} -
- {msg.content} + + {/* Content */} +
+ {/* Name + timestamp */} + {!isUser && ( +
+ + {msg.agent} + + {isMaster && ( + + MASTER + + )} +
+ )} + + {/* Message bubble */} +
+
+ {msg.content} +
+ {msg.streaming && ( + + )}
- {msg.streaming && }
) } function MemberItem({ name, role, status }: { name: string; role: string; status: string }) { - const dot = status === 'thinking' ? 'bg-yellow-400' : status === 'working' ? 'bg-green-400 animate-pulse' : 'bg-gray-500' + const dotClass = status === 'thinking' + ? 'bg-[var(--color-warning)] animate-pulse' + : status === 'working' + ? 'bg-[var(--color-success)] animate-pulse' + : 'bg-[var(--text-muted)]' + + const statusText = status === 'thinking' ? '思考中' : status === 'working' ? '工作中' : '空闲' + return ( -
-
- {role === 'master' ? '👑 ' : ''}{name} - {status} +
+
+
+ {role === 'master' ? : name[0]?.toUpperCase()} +
+
+
+
+
+ {role === 'master' && } + {name} + {role === 'master' && ( + + MASTER + + )} +
+
+ + {statusText} +
) } diff --git a/web/src/components/MarketPage.tsx b/web/src/components/MarketPage.tsx index b9f97a5..409283c 100644 --- a/web/src/components/MarketPage.tsx +++ b/web/src/components/MarketPage.tsx @@ -1,17 +1,103 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' +import { ShoppingCart, Github, Upload, Users, ArrowRight, Check, AlertCircle, Package, Zap, Star, Globe, Link as LinkIcon } from 'lucide-react' import { useStore } from '../store' +import { TeamDetail } from './TeamDetail' const API = '/api' +interface Team { + name: string + description: string + author: string + repo_url: string + agents: string[] + skills: string[] + installed_at: string +} + +const PRESET_TEAMS: Team[] = [ + { + name: '法律咨询团队', + description: '法律咨询团队,包含合同审查和法律风险评估', + author: 'Agent Team', + repo_url: 'https://github.com/sdaduanbilei/legal-team', + agents: ['法律总监', '合同律师', '合规专员'], + skills: ['合同审查', '法律知识库'], + installed_at: '', + }, + { + name: '开发团队', + description: '开发团队,包含代码审查、架构设计、全栈测试', + author: 'Agent Team', + repo_url: 'https://github.com/sdaduanbilei/dev-team', + agents: ['技术主管', '代码审查员', '测试工程师'], + skills: ['代码扫描', '性能分析'], + installed_at: '', + }, + { + name: '营销团队', + description: '营销团队,包含内容创意、广告投放策略制定', + author: 'Agent Team', + repo_url: 'https://github.com/sdaduanbilei/marketing-team', + agents: ['创意总监', '策略分析师', '文案', '运营'], + skills: ['数据分析', '创意评估'], + installed_at: '', + }, + { + name: '医疗咨询', + description: '医疗领域专业咨询团队', + author: 'Expert AI Corp', + repo_url: 'https://github.com/expert-ai/medical-team', + agents: ['医疗顾问', '营养师', '健康指导员'], + skills: ['症状分析', '饮食建议'], + installed_at: '', + }, + { + name: '金融投资', + description: '金融分析与投资咨询团队', + author: 'FiTech Solutions', + repo_url: 'https://github.com/fitech/finance-team', + agents: ['财务分析师', '投资顾问', '风险测评'], + skills: ['股市预测', '财报分析'], + installed_at: '', + }, + { + name: '心理咨询', + description: '专业心理咨询服务团队', + author: 'MindWell Inc', + repo_url: 'https://github.com/mindwell/psychology-team', + agents: ['心理咨询师', '治疗助理', '危机干预'], + skills: ['情绪评估', '心理疏导'], + installed_at: '', + }, +] + export function MarketPage() { const { fetchAgents } = useStore() + const [tab, setTab] = useState<'featured' | 'install' | 'teams'>('featured') const [repo, setRepo] = useState('') const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle') const [errMsg, setErrMsg] = useState('') + const [uploading, setUploading] = useState(false) + const [teams, setTeams] = useState([]) + const [selectedTeam, setSelectedTeam] = useState(null) + + useEffect(() => { + if (tab === 'teams') { + fetchTeams() + } + }, [tab]) + + const fetchTeams = async () => { + const res = await fetch(`${API}/teams`) + const data = await res.json() + setTeams(data || []) + } const install = async () => { if (!repo.trim()) return setStatus('loading') + setErrMsg('') try { const r = await fetch(`${API}/hub/install`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -20,45 +106,332 @@ export function MarketPage() { if (!r.ok) { const d = await r.json(); throw new Error(d.error) } setStatus('done') fetchAgents() - } catch (e: any) { - setErrMsg(e.message) + fetchTeams() + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : '安装失败' + setErrMsg(errMsg) setStatus('error') } } - return ( -
-

人才市场

-

从 GitHub 一键雇佣社区团队,或发布自己的团队供他人使用。

+ const handleUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return -
-

雇佣团队

-

输入 GitHub repo(如 username/legal-team

-
- setRepo(e.target.value)} - onKeyDown={e => e.key === 'Enter' && install()} - /> - + +
- {status === 'done' &&

✓ 安装成功

} - {status === 'error' &&

✗ {errMsg}

} -
-
-

发布团队

-
-

1. 将你的 agents/skills/ 目录推送到 GitHub

-

2. 在 repo 根目录添加 team.md 描述团队

-

3. 给 repo 添加 topic: agent-team

-

社区可通过 GitHub topic 发现你的团队

-
+ {selectedTeam && !isSelected(selectedTeam.name) && ( + t.name === selectedTeam.name)} + onBack={() => setSelectedTeam(null)} + onInstalled={() => { + setSelectedTeam(null) + fetchTeams() + }} + onUninstalled={() => { + setSelectedTeam(null) + deleteTeam(selectedTeam.name) + fetchTeams() + }} + /> + )} + + {selectedTeam ? null : ( + <> + {/* Featured teams */} + {tab === 'featured' && ( +
+
+ 官方精选团队,快速开始使用 +
+
+ {PRESET_TEAMS.map(team => { + const isInstalled = teams.some(t => t.name === team.name) + return ( +
setSelectedTeam(team)} + > +
+
+

{team.name}

+

{team.description}

+
+ {isInstalled ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ + 专业级 +
+
+ + + {team.agents?.length || 0} 成员 + + + + {team.skills?.length || 0} Skills + +
+ {team.repo_url && ( + + + 查看详情 + + )} +
+ ) + })} +
+
+ )} + + {/* Install tab */} + {tab === 'install' && ( +
+ {/* Install from git */} +
+
+ +

从 Git 安装

+
+

+ 输入任意 Git 仓库地址(GitHub、Gitee 等),团队包含 agents、skills 和知识库 +

+
+
+ + setRepo(e.target.value)} + onKeyDown={e => e.key === 'Enter' && install()} + /> +
+ +
+ {status === 'done' && ( +

+ + 安装成功!可在"我的团队"中查看 +

+ )} + {status === 'error' && ( +

+ + {errMsg} +

+ )} +
+ + {/* Upload ZIP */} +
+
+ +

上传 ZIP 安装

+
+

+ 上传团队压缩包,将自动解压并安装到本地 +

+
+ +
+
+
+ )} + + {/* Teams tab */} + {tab === 'teams' && ( +
+ {teams.length === 0 ? ( +
+ +

暂无已安装的团队

+ +
+ ) : ( +
+ {teams.map(team => ( +
setSelectedTeam(team)} + className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 cursor-pointer hover:border-[var(--accent)] transition-colors" + > +
+

{team.name}

+ +
+

+ {team.description || '暂无描述'} +

+
+ + + {team.agents?.length || 0} agents + + + + {team.skills?.length || 0} skills + +
+
+ ))} +
+ )} +
+ )} + + )}
) diff --git a/web/src/components/Onboarding.tsx b/web/src/components/Onboarding.tsx new file mode 100644 index 0000000..f0bc205 --- /dev/null +++ b/web/src/components/Onboarding.tsx @@ -0,0 +1,366 @@ +import { useState } from 'react' +import { ChevronRight, ChevronLeft, Check, Hash, Sparkles, Key, User } from 'lucide-react' +import MDEditor from '@uiw/react-md-editor' + +const API = '/api' + +const STEPS = [ + { id: 1, title: '欢迎', icon: Sparkles }, + { id: 2, title: '关于你', icon: User }, + { id: 3, title: 'API 配置', icon: Key }, +] + +const DEFAULT_PROFILE = `# 我的简介 + +## 我是谁 +我是[你的名字],[职业/背景]。 + +## 工作风格 +- 喜欢[沟通方式] +- 重视[重视的点] +- 不喜欢[不喜欢的] + +## 期望的回复方式 +- 用 bullet point 列出要点 +- 重要的决定给出 pros 和 cons +- 提供数据支持` + +interface OnboardingProps { + onComplete: () => void +} + +export function Onboarding({ onComplete }: OnboardingProps) { + const [step, setStep] = useState(1) + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [profile, setProfile] = useState('') + const [provider, setProvider] = useState('deepseek') + const [model, setModel] = useState('deepseek-chat') + const [apiKey, setApiKey] = useState('') + const [saving, setSaving] = useState(false) + + const handleNext = () => { + if (step < 3) { + setStep(step + 1) + } + } + + const handlePrev = () => { + if (step > 1) { + setStep(step - 1) + } + } + + const handleComplete = async () => { + setSaving(true) + try { + await fetch(`${API}/user/config`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name || '用户', + description, + provider, + model, + api_key_env: provider.toUpperCase() + '_API_KEY', + }) + }) + + await fetch(`${API}/user/profile`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: profile || DEFAULT_PROFILE }) + }) + + localStorage.setItem('onboarding_completed', 'true') + onComplete() + } catch (err) { + console.error(err) + } finally { + setSaving(false) + } + } + + const handleSkip = () => { + localStorage.setItem('onboarding_completed', 'true') + onComplete() + } + + const providerModels: Record = { + deepseek: ['deepseek-chat', 'deepseek-coder'], + kimi: ['moonshot-v1-8k', 'moonshot-v1-32k'], + ollama: ['qwen2.5', 'llama3', 'mistral'], + openai: ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'], + } + + return ( +
+
+ {/* Header */} +
+
+ + Agent Team +
+ +
+ + {/* Progress */} +
+
+ {STEPS.map((s, i) => ( +
+
s.id + ? 'bg-[var(--color-success)] text-white' + : 'bg-[var(--bg-tertiary)] text-[var(--text-muted)]' + } + `} + > + {step > s.id ? : s.id} +
+ {i < STEPS.length - 1 && ( +
s.id ? 'bg-[var(--color-success)]' : 'bg-[var(--border)]'}`} /> + )} +
+ ))} +
+
+ {STEPS.map(s => ( + + {s.title} + + ))} +
+
+ + {/* Content */} +
+ {/* Step 1: Welcome */} + {step === 1 && ( +
+
+
+ +
+

欢迎使用 Agent Team

+

+ 这是一个本地部署的多 Agent 协作平台,通过类似 Discord 的群聊界面与你的 AI 团队协作完成复杂任务。 +

+
+ +
+

📋 你将体验到:

+
    +
  • • 创建和管理 AI Agent 团队
  • +
  • • 通过群聊与 Agent 协作
  • +
  • • Agent 自动沉淀经验,持续成长
  • +
  • • 支持 DeepSeek、Kimi、Ollama、OpenAI
  • +
+
+
+ )} + + {/* Step 2: About You */} + {step === 2 && ( +
+
+
+ +
+

介绍一下你自己

+

+ 用 Markdown 写下你的简介,Agent 会根据这些信息更好地帮助你 +

+
+ +
+
+ + setName(e.target.value)} + placeholder="张三" + className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> +
+
+ + setDescription(e.target.value)} + placeholder="产品经理,擅长需求分析" + className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> +
+
+ +
+ +
+ setProfile(v || '')} + height={250} + preview="edit" + visibleDragbar={false} + className="mt-2" + style={{ + backgroundColor: 'var(--bg-primary)', + }} + textareaProps={{ + placeholder: '详细编辑你的个人简介...', + style: { + fontFamily: 'var(--font-mono), monospace', + fontSize: '13px', + backgroundColor: 'var(--bg-primary)', + color: 'var(--text-primary)', + padding: '1rem', + } + }} + /> +
+
+
+ )} + + {/* Step 3: API Config */} + {step === 3 && ( +
+
+
+ +
+

配置默认 API

+

+ 选择默认的模型提供商,所有 Agent 将使用此配置(可在 Agent 级别覆盖) +

+
+ +
+ +
+ {['deepseek', 'kimi', 'ollama', 'openai'].map(p => ( + + ))} +
+
+ +
+ + +
+ +
+ + setApiKey(e.target.value)} + placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx" + className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> +
+ +
+

💡 提示:API Key 将保存在浏览器本地存储中,每次启动后端时会自动读取。

+
+
+ )} +
+ + {/* Footer */} +
+ + + {step < 3 ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/web/src/components/RoomSidebar.tsx b/web/src/components/RoomSidebar.tsx index e8b7727..856c96b 100644 --- a/web/src/components/RoomSidebar.tsx +++ b/web/src/components/RoomSidebar.tsx @@ -1,106 +1,167 @@ import { useEffect, useState } from 'react' +import { Plus, Hash, Users, Crown, ShoppingCart } from 'lucide-react' import { useStore } from '../store' +import type { Room } from '../types' const API = '/api' export function RoomSidebar() { - const { rooms, activeRoomId, setActiveRoom, fetchRooms, agents, fetchAgents } = useStore() + const { rooms, activeRoomId, setActiveRoom, fetchRooms, setPage } = useStore() const [creating, setCreating] = useState(false) - const [form, setForm] = useState({ name: '', type: 'dept', master: '', members: [] as string[] }) + const [form, setForm] = useState({ name: '', type: 'dept' as 'dept' | 'leader' }) - useEffect(() => { fetchRooms(); fetchAgents() }, []) - - const toggleMember = (name: string) => { - setForm(f => ({ - ...f, - members: f.members.includes(name) ? f.members.filter(m => m !== name) : [...f.members, name] - })) - } + useEffect(() => { fetchRooms() }, []) const create = async () => { - if (!form.name || !form.master) return + if (!form.name) return await fetch(`${API}/rooms`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: form.name, type: form.type, master: form.master, members: form.members }) + body: JSON.stringify({ name: form.name, type: form.type, master: '', members: [] }) }) setCreating(false) - setForm({ name: '', type: 'dept', master: '', members: [] }) + setForm({ name: '', type: 'dept' }) fetchRooms() } const statusColor = (status: string) => { - if (status === 'thinking') return 'bg-yellow-400' - if (status === 'working') return 'bg-green-400 animate-pulse' - return 'bg-gray-500' + if (status === 'thinking') return 'bg-[var(--color-warning)]' + if (status === 'working') return 'bg-[var(--color-success)] animate-pulse' + return 'bg-[var(--text-muted)]' } - const availableMembers = agents.filter(a => a.name !== form.master) + const deptRooms = rooms.filter(r => r.type === 'dept') + const leaderRooms = rooms.filter(r => r.type === 'leader') return ( -
-
- Agent Team - +
+ {/* Header */} +
+

我的团队

+
+ {/* Create team form */} {creating && ( -
- setForm(f => ({ ...f, name: e.target.value }))} /> - setForm(f => ({ ...f, name: e.target.value }))} + /> + - - {form.type === 'dept' && availableMembers.length > 0 && ( -
-
选择成员:
- {availableMembers.map(a => ( - - ))} -
- )}
- - + +
)} -
-
部门群
- {rooms.filter(r => r.type === 'dept').map(r => ( - setActiveRoom(r.id)} statusColor={statusColor(r.status)} /> - ))} -
Leader 群
- {rooms.filter(r => r.type === 'leader').map(r => ( - setActiveRoom(r.id)} statusColor={statusColor(r.status)} /> - ))} + {/* Room list */} +
+ {/* Department teams */} + {deptRooms.length > 0 && ( +
+
+ + 部门团队 +
+ {deptRooms.map(r => ( + setActiveRoom(r.id)} + statusColor={statusColor(r.status)} + /> + ))} +
+ )} + + {/* Leader teams */} + {leaderRooms.length > 0 && ( +
+
+ + Leader 团队 +
+ {leaderRooms.map(r => ( + setActiveRoom(r.id)} + statusColor={statusColor(r.status)} + /> + ))} +
+ )} + + {/* Empty state */} + {rooms.length === 0 && ( +
+ +

暂无团队

+

点击 + 创建第一个团队

+
+ )} +
+ + {/* Bottom hint - go to market */} +
+
) } -function RoomItem({ room, active, onClick, statusColor }: any) { +function RoomItem({ room, active, onClick, statusColor }: { room: Room; active: boolean; onClick: () => void; statusColor: string }) { return ( -
-
-
-
{room.name}
+
+ +
+ {room.name} {room.status !== 'pending' && ( -
- {room.status === 'working' && room.activeAgent ? `${room.activeAgent} ${room.action || ''}` : room.status} -
+ )}
) -} +} \ No newline at end of file diff --git a/web/src/components/SkillsPage.tsx b/web/src/components/SkillsPage.tsx deleted file mode 100644 index 9a997fa..0000000 --- a/web/src/components/SkillsPage.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useEffect, useState } from 'react' -import Editor from '@monaco-editor/react' -import { useStore } from '../store' - -const API = '/api' - -export function SkillsPage() { - const { skills, fetchSkills } = useStore() - const [selected, setSelected] = useState(null) - const [body, setBody] = useState('') - const [creating, setCreating] = useState(false) - const [newName, setNewName] = useState('') - - useEffect(() => { fetchSkills() }, []) - - useEffect(() => { - if (!selected) return - fetch(`${API}/skills/${selected}`).then(r => r.json()).then(d => setBody(d.body || '')) - }, [selected]) - - const create = async () => { - if (!newName.trim()) return - const content = `---\nname: ${newName.trim()}\ndescription: \n---\n\n# ${newName.trim()}\n\n描述这个 skill 的用途和使用步骤。\n` - await fetch(`${API}/agents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName.trim() }) }) - // Use skill create endpoint - await fetch(`${API}/skills`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: newName.trim(), content }) - }) - setNewName('') - setCreating(false) - fetchSkills() - } - - return ( -
- {/* Skill list */} -
-
- {creating ? ( -
- setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && create()} autoFocus /> - - -
- ) : ( - - )} -
-
- {(skills || []).map(s => ( -
setSelected(s.name)} - className={`px-3 py-2 cursor-pointer hover:bg-gray-700 ${selected === s.name ? 'bg-gray-700' : ''}`}> -
{s.name}
-
{s.description}
-
- ))} -
-
- - {/* Skill detail / editor */} -
- {selected ? ( - <> -
- {selected} - SKILL.md -
-
- setBody(v || '')} - options={{ minimap: { enabled: false }, wordWrap: 'on', fontSize: 13, readOnly: true }} /> -
- - ) : ( -
选择一个 skill 查看详情
- )} -
-
- ) -} diff --git a/web/src/components/TeamDetail.tsx b/web/src/components/TeamDetail.tsx new file mode 100644 index 0000000..f5b11f2 --- /dev/null +++ b/web/src/components/TeamDetail.tsx @@ -0,0 +1,417 @@ +import { useState, useEffect } from 'react' +import MDEditor from '@uiw/react-md-editor' +import { ChevronLeft, Users, Package, BookOpen, ChevronDown, ChevronRight, Save, Loader2, Trash2, Plus } from 'lucide-react' + +const API = '/api' + +interface Team { + name: string + description: string + author: string + repo_url: string + agents: string[] + skills: string[] + installed_at: string +} + +interface TeamDetailProps { + team: Team + installed: boolean + onBack: () => void + onInstalled?: () => void + onUninstalled?: () => void +} + +export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled }: TeamDetailProps) { + const [expandedAgent, setExpandedAgent] = useState(null) + const [agentFile, setAgentFile] = useState('') + const [agentContent, setAgentContent] = useState('') + const [knowledge, setKnowledge] = useState([]) + const [selectedKnowledge, setSelectedKnowledge] = useState(null) + const [knowledgeContent, setKnowledgeContent] = useState('') + const [newKnowledgeName, setNewKnowledgeName] = useState('') + const [saving, setSaving] = useState(false) + const [loading, setLoading] = useState(false) + const [deleting, setDeleting] = useState(false) + + useEffect(() => { + if (installed) { + fetchKnowledge() + } + }, [team.name, installed]) + + const fetchKnowledge = async () => { + if (!installed) return + setLoading(true) + try { + const res = await fetch(`${API}/teams/${team.name}/knowledge`) + const data = await res.json() + setKnowledge(data || []) + } finally { + setLoading(false) + } + } + + const loadAgentFile = async (agentName: string, file: string) => { + if (!installed) return + setLoading(true) + try { + const res = await fetch(`${API}/teams/${team.name}/agents/${agentName}/files/${file}`) + const data = await res.json() + setAgentContent(data.content || '') + setAgentFile(file) + } catch { + setAgentContent('') + } finally { + setLoading(false) + } + } + + const saveAgentFile = async () => { + if (!expandedAgent || !agentFile || !installed) return + setSaving(true) + try { + await fetch(`${API}/teams/${team.name}/agents/${expandedAgent}/files/${agentFile}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: agentContent }) + }) + } finally { + setSaving(false) + } + } + + const loadKnowledgeFile = async (fileName: string) => { + if (!installed) return + setLoading(true) + try { + const res = await fetch(`${API}/teams/${team.name}/knowledge/${fileName}`) + const data = await res.json() + setKnowledgeContent(data.content || '') + setSelectedKnowledge(fileName) + } catch { + setKnowledgeContent('') + } finally { + setLoading(false) + } + } + + const saveKnowledgeFile = async () => { + if (!selectedKnowledge || !installed) return + setSaving(true) + try { + await fetch(`${API}/teams/${team.name}/knowledge/${selectedKnowledge}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: knowledgeContent }) + }) + } finally { + setSaving(false) + } + } + + const createKnowledgeFile = async () => { + if (!newKnowledgeName.trim() || !installed) return + const name = newKnowledgeName.trim().endsWith('.md') + ? newKnowledgeName.trim() + : newKnowledgeName.trim() + '.md' + await fetch(`${API}/teams/${team.name}/knowledge/${name}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: '# ' + name.replace('.md', '') + '\n\n' }) + }) + setNewKnowledgeName('') + fetchKnowledge() + loadKnowledgeFile(name) + } + + const handleInstall = async () => { + if (!team.repo_url) return + + setSaving(true) + try { + await fetch(`${API}/hub/install`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ repo: team.repo_url }) + }) + onInstalled?.() + } finally { + setSaving(false) + } + } + + const handleDelete = async () => { + if (!confirm(`确定要卸载团队 "${team.name}" 吗?这将删除相关的 agents 和知识库。`)) return + + setDeleting(true) + try { + await fetch(`${API}/teams/${team.name}`, { method: 'DELETE' }) + onUninstalled?.() + } finally { + setDeleting(false) + } + } + + return ( +
+
+ {/* Header */} +
+
+ + / +

{team.name}

+
+ {installed ? ( + + ) : ( + + )} +
+ + {/* Content */} +
+
+ {/* Team info */} +
+

{team.description}

+
+ {team.author && 作者: {team.author}} + {team.repo_url && ( + + + 项目页面 + + )} +
+
+ + {/* Agents */} +
+

+ + 成员 ({team.agents?.length || 0}) +

+ {team.agents?.length === 0 ? ( +

暂无成员

+ ) : ( +
+ {team.agents?.map(agent => ( +
+ + {installed && expandedAgent === agent && ( +
+ {/* File tabs */} +
+ {['SOUL.md', 'AGENT.md'].map(f => ( + + ))} +
+ {/* Editor */} +
+ {loading ? ( +
+ +
+ ) : ( + setAgentContent(v || '')} + height={250} + preview="edit" + visibleDragbar={false} + /> + )} +
+ {/* Save button */} +
+ +
+
+ )} +
+ ))} +
+ )} +
+ + {/* Skills */} +
+

+ + Skills ({team.skills?.length || 0}) +

+
+ {team.skills?.length === 0 && ( +

暂无 skills

+ )} + {team.skills?.map(skill => ( + + {skill} + + ))} +
+
+ + {/* Knowledge */} + {installed && ( +
+

+ + 知识库 +

+ +
+ {/* File list */} +
+ {knowledge.map(f => ( + + ))} + {/* Add new */} +
+ setNewKnowledgeName(e.target.value)} + placeholder="新知识文件" + className="flex-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-2 py-1 text-xs outline-none focus:border-[var(--accent)]" + onKeyDown={e => e.key === 'Enter' && createKnowledgeFile()} + /> + +
+
+ + {/* Editor */} +
+ {selectedKnowledge ? ( + <> +
+ setKnowledgeContent(v || '')} + height={300} + preview="edit" + visibleDragbar={false} + /> +
+
+ +
+ + ) : ( +
+ 选择或创建知识文件 +
+ )} +
+
+
+ )} + + {!installed && ( +
+

+ 此团队尚未安装。点击上方"安装"按钮来安装它。 +

+
+ )} +
+
+
+
+ ) +} diff --git a/web/src/components/ThemeToggle.tsx b/web/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..c61831b --- /dev/null +++ b/web/src/components/ThemeToggle.tsx @@ -0,0 +1,20 @@ +import { Sun, Moon } from 'lucide-react' +import { useStore } from '../store' + +export function ThemeToggle() { + const { theme, toggleTheme } = useStore() + + return ( + + ) +} diff --git a/web/src/components/UserSettings.tsx b/web/src/components/UserSettings.tsx new file mode 100644 index 0000000..d0d6c1e --- /dev/null +++ b/web/src/components/UserSettings.tsx @@ -0,0 +1,258 @@ +import { useEffect, useState } from 'react' +import MDEditor from '@uiw/react-md-editor' +import { User, Key, Save, Loader2 } from 'lucide-react' + +const API = '/api' + +export function UserSettings() { + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [tab, setTab] = useState<'profile' | 'config'>('profile') + const [user, setUser] = useState({ + name: '', + description: '', + provider: 'deepseek', + model: 'deepseek-chat', + api_key_env: 'DEEPSEEK_API_KEY', + avatar_color: '#5865F2', + }) + const [profile, setProfile] = useState('') + + const providerModels: Record = { + deepseek: ['deepseek-chat', 'deepseek-coder'], + kimi: ['moonshot-v1-8k', 'moonshot-v1-32k'], + ollama: ['qwen2.5', 'llama3', 'mistral'], + openai: ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'], + } + + useEffect(() => { + async function load() { + try { + const [userRes, profileRes] = await Promise.all([ + fetch(`${API}/user`).then(r => r.json()), + fetch(`${API}/user/profile`).then(r => r.json()) + ]) + setUser(userRes) + setProfile(profileRes.content || '') + } catch (err) { + console.error(err) + } finally { + setLoading(false) + } + } + load() + }, []) + + const handleSaveConfig = async () => { + setSaving(true) + try { + await fetch(`${API}/user/config`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(user) + }) + } catch (err) { + console.error(err) + } finally { + setSaving(false) + } + } + + const handleSaveProfile = async () => { + setSaving(true) + try { + await fetch(`${API}/user/profile`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: profile }) + }) + } catch (err) { + console.error(err) + } finally { + setSaving(false) + } + } + + if (loading) { + return ( +
+ +
+ ) + } + + return ( +
+ {/* Sidebar */} +
+
+
+
+ {user.name?.[0]?.toUpperCase() || 'U'} +
+
+
{user.name || '用户'}
+
{user.provider}
+
+
+
+ +
+ + {/* Content */} +
+ {tab === 'profile' && ( + <> +
+

个人简介

+ +
+
+ setProfile(v || '')} + height={300} + preview="edit" + visibleDragbar={false} + className="border border-[var(--border)] rounded-lg overflow-hidden" + style={{ + backgroundColor: 'var(--bg-primary)', + }} + textareaProps={{ + placeholder: '编辑个人资料介绍...', + style: { + fontFamily: 'var(--font-mono), monospace', + fontSize: '13px', + backgroundColor: 'var(--bg-primary)', + color: 'var(--text-primary)', + } + }} + /> +
+ + )} + + {tab === 'config' && ( + <> +
+

API 配置

+ +
+
+
+
+ + setUser({ ...user, name: e.target.value })} + className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> +
+ +
+ + setUser({ ...user, description: e.target.value })} + placeholder="一句话介绍自己" + className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> +
+ +
+ +
+ {['deepseek', 'kimi', 'ollama', 'openai'].map(p => ( + + ))} +
+
+ +
+ + +
+ +
+ + setUser({ ...user, api_key_env: e.target.value })} + className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> +

+ API Key 将从这个环境变量读取 +

+
+
+
+ + )} +
+
+ ) +} diff --git a/web/src/index.css b/web/src/index.css index d1dcb4e..426e095 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,3 +1,245 @@ @import "tailwindcss"; -body { margin: 0; } +@theme { + --font-sans: "Inter", "Geist Sans", system-ui, sans-serif; + --font-mono: "JetBrains Mono", "Fira Code", monospace; + + --color-bg-primary: var(--bg-primary); + --color-bg-secondary: var(--bg-secondary); + --color-bg-tertiary: var(--bg-tertiary); + --color-bg-hover: var(--bg-hover); + --color-bg-active: var(--bg-active); + + --color-text-primary: var(--text-primary); + --color-text-secondary: var(--text-secondary); + --color-text-muted: var(--text-muted); + + --color-accent: var(--accent); + --color-accent-hover: var(--accent-hover); + --color-accent-muted: var(--accent-muted); + + --color-border: var(--border); + --color-border-hover: var(--border-hover); + + --color-success: #23A55A; + --color-warning: #F0B232; + --color-error: #F23F43; + + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; +} + +:root { + --bg-primary: #F2F3F5; + --bg-secondary: #FFFFFF; + --bg-tertiary: #EBEDF0; + --bg-hover: #E3E5E8; + --bg-active: #D4D7DC; + + --text-primary: #060607; + --text-secondary: #4E5058; + --text-muted: #80848E; + + --accent: #5865F2; + --accent-hover: #4752C4; + --accent-muted: #D8DFFD; + + --border: #D4D7DC; + --border-hover: #A3A6AB; + + --sidebar-bg: #F2F3F5; + --channel-list-bg: #FFFFFF; +} + +.dark { + --bg-primary: #313338; + --bg-secondary: #2B2D31; + --bg-tertiary: #1E1F22; + --bg-hover: #3F4147; + --bg-active: #484B51; + + --text-primary: #F2F3F5; + --text-secondary: #B5BAC1; + --text-muted: #949BA4; + + --accent: #5865F2; + --accent-hover: #4752C4; + --accent-muted: #39427E; + + --border: #3F4147; + --border-hover: #5D5F66; + + --sidebar-bg: #1E1F22; + --channel-list-bg: #2B2D31; +} + +/* Custom styling for react-md-editor - Complete override */ +.w-md-editor { + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--bg-tertiary) !important; + color: var(--text-primary) !important; + box-shadow: none !important; +} + +.w-md-editor * { + box-sizing: border-box; +} + +.w-md-editor-toolbar { + border-bottom: 1px solid var(--border) !important; + background: var(--bg-secondary) !important; + padding: 8px 12px !important; +} + +.w-md-editor-toolbar button { + color: var(--text-secondary) !important; +} + +.w-md-editor-toolbar button:hover { + background: var(--bg-hover) !important; +} + +.w-md-editor-toolbar button.active { + color: var(--accent) !important; +} + +.w-md-editor-text-pre > code, +.w-md-editor-text-input { + font-family: var(--font-mono), monospace !important; + font-size: 13px !important; + line-height: 1.6 !important; + color: var(--text-primary) !important; + background: transparent !important; +} + +.w-md-editor-text-input { + caret-color: var(--text-primary) !important; + padding: 12px !important; +} + +.w-md-editor-text-pre { + padding: 12px !important; + background: var(--bg-primary) !important; + margin: 0 !important; +} + +.w-md-editor-preview { + padding: 12px !important; + background: var(--bg-primary) !important; + color: var(--text-primary) !important; +} + +.w-md-editor-preview h1, +.w-md-editor-preview h2, +.w-md-editor-preview h3, +.w-md-editor-preview h4, +.w-md-editor-preview h5, +.w-md-editor-preview h6 { + color: var(--text-primary) !important; + border-bottom: 1px solid var(--border) !important; + padding-bottom: 0.3em !important; + margin-top: 1.5em !important; +} + +.w-md-editor-preview a { + color: var(--accent) !important; +} + +.w-md-editor-preview code { + background: var(--bg-tertiary) !important; + padding: 2px 6px !important; + border-radius: 4px !important; + font-family: var(--font-mono), monospace !important; + font-size: 0.9em !important; +} + +.w-md-editor-preview pre { + background: var(--bg-tertiary) !important; + padding: 12px !important; + border-radius: 6px !important; + overflow-x: auto !important; +} + +.w-md-editor-preview pre code { + background: transparent !important; + padding: 0 !important; +} + +.w-md-editor-preview blockquote { + border-left: 4px solid var(--accent) !important; + padding-left: 16px !important; + color: var(--text-secondary) !important; + margin: 1em 0 !important; +} + +.w-md-editor-preview ul, +.w-md-editor-preview ol { + padding-left: 24px !important; +} + +.w-md-editor-preview table { + border-collapse: collapse !important; + width: 100% !important; + margin: 1em 0 !important; +} + +.w-md-editor-preview th, +.w-md-editor-preview td { + border: 1px solid var(--border) !important; + padding: 8px 12px !important; +} + +.w-md-editor-preview th { + background: var(--bg-tertiary) !important; +} + +.w-md-editor-input { + background: var(--bg-primary) !important; + color: var(--text-primary) !important; +} + +.w-md-editor-text, +.w-md-editor-text-pre { + background: var(--bg-primary) !important; + color: var(--text-primary) !important; +} + +/* Hide drag bar in certain contexts */ +.w-md-editor .w-md-editor-bar { + background: var(--bg-tertiary) !important; + width: 4px !important; + cursor: col-resize !important; +} + +body { + margin: 0; + font-family: var(--font-sans); + background-color: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.2s, color 0.2s; +} + +* { + box-sizing: border-box; +} + +.scrollbar-thin::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.scrollbar-thin::-webkit-scrollbar-track { + background: transparent; +} + +.scrollbar-thin::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 4px; +} + +.scrollbar-thin::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} diff --git a/web/src/store.ts b/web/src/store.ts index ed21e50..ff5e576 100644 --- a/web/src/store.ts +++ b/web/src/store.ts @@ -2,6 +2,12 @@ import { create } from 'zustand' import type { Room, Message, AgentInfo, SkillMeta, WsEvent } from './types' interface AppState { + theme: 'light' | 'dark' + user: { + name: string + avatar_color: string + } | null + onboardingCompleted: boolean rooms: Room[] activeRoomId: string | null messages: Record @@ -9,9 +15,13 @@ interface AppState { workspace: Record agents: AgentInfo[] skills: SkillMeta[] - page: 'chat' | 'agents' | 'skills' | 'market' + page: 'chat' | 'agents' | 'skills' | 'market' | 'settings' ws: Record + setTheme: (theme: 'light' | 'dark') => void + toggleTheme: () => void + setOnboardingCompleted: (completed: boolean) => void + fetchUser: () => Promise setPage: (p: AppState['page']) => void setActiveRoom: (id: string) => void fetchRooms: () => Promise @@ -23,86 +33,143 @@ interface AppState { const API = '/api' -export const useStore = create((set, get) => ({ - rooms: [], - activeRoomId: null, - messages: {}, - tasks: {}, - workspace: {}, - agents: [], - skills: [], - page: 'chat', - ws: {}, - - setPage: (page) => set({ page }), - setActiveRoom: (id) => { - set({ activeRoomId: id }) - get().connectRoom(id) - fetch(`${API}/rooms/${id}/tasks`).then(r => r.json()).then(d => { - set(s => ({ tasks: { ...s.tasks, [id]: d.content } })) - }) - fetch(`${API}/rooms/${id}/workspace`).then(r => r.json()).then(files => { - set(s => ({ workspace: { ...s.workspace, [id]: files || [] } })) - }) - // Load message history - fetch(`${API}/rooms/${id}/messages`).then(r => r.json()).then(msgs => { - if (msgs?.length) set(s => ({ messages: { ...s.messages, [id]: msgs } })) - }).catch(() => {}) - }, - - fetchRooms: async () => { - const rooms = await fetch(`${API}/rooms`).then(r => r.json()) - set({ rooms: rooms || [] }) - }, - - fetchAgents: async () => { - const agents = await fetch(`${API}/agents`).then(r => r.json()) - set({ agents: agents || [] }) - }, - - fetchSkills: async () => { - const skills = await fetch(`${API}/skills`).then(r => r.json()) - set({ skills: skills || [] }) - }, - - connectRoom: (roomId) => { - const { ws } = get() - if (ws[roomId]) return - const socket = new WebSocket(`ws://${location.host}/ws/${roomId}`) - socket.onmessage = (e) => { - const ev: WsEvent = JSON.parse(e.data) - if (ev.type === 'agent_message') { - set(s => { - const msgs = [...(s.messages[roomId] || [])] - const last = msgs[msgs.length - 1] - if (last?.streaming && last.agent === ev.agent) { - msgs[msgs.length - 1] = { ...last, content: last.content + ev.content, streaming: ev.streaming } - } else { - msgs.push({ id: Date.now().toString(), agent: ev.agent, role: ev.role, content: ev.content, streaming: ev.streaming }) - } - return { messages: { ...s.messages, [roomId]: msgs } } - }) - } else if (ev.type === 'room_status') { - set(s => ({ - rooms: s.rooms.map(r => r.id === roomId - ? { ...r, status: ev.status, activeAgent: ev.active_agent, action: ev.action } - : r) - })) - } else if (ev.type === 'tasks_update') { - set(s => ({ tasks: { ...s.tasks, [roomId]: ev.content } })) - } else if (ev.type === 'workspace_file') { - set(s => ({ - workspace: { ...s.workspace, [roomId]: [...(s.workspace[roomId] || []), ev.filename] } - })) - } +export const useStore = create((set, get) => { + const getInitialTheme = (): 'light' | 'dark' => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('theme') + if (saved === 'light' || saved === 'dark') return saved + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } - set(s => ({ ws: { ...s.ws, [roomId]: socket } })) - }, + return 'dark' + } - sendMessage: (roomId, content) => { - const { ws } = get() - const userMsg = { id: Date.now().toString(), agent: 'user', role: 'user' as const, content } - set(s => ({ messages: { ...s.messages, [roomId]: [...(s.messages[roomId] || []), userMsg] } })) - ws[roomId]?.send(JSON.stringify({ type: 'user_message', content })) - }, -})) + const applyTheme = (theme: 'light' | 'dark') => { + if (typeof window !== 'undefined') { + document.documentElement.classList.remove('light', 'dark') + document.documentElement.classList.add(theme) + localStorage.setItem('theme', theme) + } + } + + const initialTheme = getInitialTheme() + applyTheme(initialTheme) + + return { + theme: initialTheme, + user: null, + onboardingCompleted: typeof window !== 'undefined' ? localStorage.getItem('onboarding_completed') === 'true' : false, + rooms: [], + activeRoomId: null, + messages: {}, + tasks: {}, + workspace: {}, + agents: [], + skills: [], + page: 'chat', + ws: {}, + + setTheme: (theme) => { + applyTheme(theme) + set({ theme }) + }, + toggleTheme: () => { + const newTheme = get().theme === 'dark' ? 'light' : 'dark' + applyTheme(newTheme) + set({ theme: newTheme }) + }, + setOnboardingCompleted: (completed) => { + localStorage.setItem('onboarding_completed', completed ? 'true' : 'false') + set({ onboardingCompleted: completed }) + }, + fetchUser: async () => { + try { + const res = await fetch(`${API}/user`) + const data = await res.json() + set({ + user: { + name: data.name || '用户', + avatar_color: data.avatar_color || '#5865F2' + } + }) + } catch { + set({ + user: { + name: '用户', + avatar_color: '#5865F2' + } + }) + } + }, + setPage: (page) => set({ page }), + setActiveRoom: (id) => { + set({ activeRoomId: id }) + get().connectRoom(id) + fetch(`${API}/rooms/${id}/tasks`).then(r => r.json()).then(d => { + set(s => ({ tasks: { ...s.tasks, [id]: d.content } })) + }) + fetch(`${API}/rooms/${id}/workspace`).then(r => r.json()).then(files => { + set(s => ({ workspace: { ...s.workspace, [id]: files || [] } })) + }) + fetch(`${API}/rooms/${id}/messages`).then(r => r.json()).then(msgs => { + if (msgs?.length) set(s => ({ messages: { ...s.messages, [id]: msgs } })) + }).catch(() => {}) + }, + + fetchRooms: async () => { + const rooms = await fetch(`${API}/rooms`).then(r => r.json()) + set({ rooms: rooms || [] }) + }, + + fetchAgents: async () => { + const agents = await fetch(`${API}/agents`).then(r => r.json()) + set({ agents: agents || [] }) + }, + + fetchSkills: async () => { + const skills = await fetch(`${API}/skills`).then(r => r.json()) + set({ skills: skills || [] }) + }, + + connectRoom: (roomId) => { + const { ws } = get() + if (ws[roomId]) return + const socket = new WebSocket(`ws://${location.host}/ws/${roomId}`) + socket.onmessage = (e) => { + const ev: WsEvent = JSON.parse(e.data) + if (ev.type === 'agent_message') { + set(s => { + const msgs = [...(s.messages[roomId] || [])] + const last = msgs[msgs.length - 1] + if (last?.streaming && last.agent === ev.agent) { + msgs[msgs.length - 1] = { ...last, content: last.content + ev.content, streaming: ev.streaming } + } else { + msgs.push({ id: Date.now().toString(), agent: ev.agent, role: ev.role, content: ev.content, streaming: ev.streaming }) + } + return { messages: { ...s.messages, [roomId]: msgs } } + }) + } else if (ev.type === 'room_status') { + set(s => ({ + rooms: s.rooms.map(r => r.id === roomId + ? { ...r, status: ev.status, activeAgent: ev.active_agent, action: ev.action } + : r) + })) + } else if (ev.type === 'tasks_update') { + set(s => ({ tasks: { ...s.tasks, [roomId]: ev.content } })) + } else if (ev.type === 'workspace_file') { + set(s => ({ + workspace: { ...s.workspace, [roomId]: [...(s.workspace[roomId] || []), ev.filename] } + })) + } + } + set(s => ({ ws: { ...s.ws, [roomId]: socket } })) + }, + + sendMessage: (roomId, content) => { + const { ws, user } = get() + const userName = user?.name || '用户' + const userMsg = { id: Date.now().toString(), agent: userName, role: 'user' as const, content } + set(s => ({ messages: { ...s.messages, [roomId]: [...(s.messages[roomId] || []), userMsg] } })) + ws[roomId]?.send(JSON.stringify({ type: 'user_message', content, user_name: userName })) + }, + } +})