fix
This commit is contained in:
parent
416bea4ce3
commit
9e279a0627
248
AGENTS.md
Normal file
248
AGENTS.md
Normal file
@ -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 配置目录
|
||||||
|
│ └── <name>/
|
||||||
|
│ ├── 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/<name>/` 创建目录
|
||||||
|
2. 添加 `AGENT.md` (YAML frontmatter + 内容)
|
||||||
|
3. 添加 `SOUL.md` (system prompt)
|
||||||
|
4. 重启后端服务
|
||||||
|
|
||||||
|
### 添加新 skill
|
||||||
|
1. 在 `skills/<name>/` 创建目录
|
||||||
|
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) — 项目简介
|
||||||
17
agents/legal-team/合同律师/AGENT.md
Normal file
17
agents/legal-team/合同律师/AGENT.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: 合同律师
|
||||||
|
role: member
|
||||||
|
description: 专注合同审查、起草和风险评估的专业律师
|
||||||
|
version: 1.0.0
|
||||||
|
skills:
|
||||||
|
- 合同审查
|
||||||
|
---
|
||||||
|
|
||||||
|
# 合同律师
|
||||||
|
|
||||||
|
专业的合同法律服务提供者,负责:
|
||||||
|
|
||||||
|
- 合同条款审查和风险评估
|
||||||
|
- 合同起草和修改建议
|
||||||
|
- 合同纠纷预防和处理建议
|
||||||
|
- 合同相关法律问题解答
|
||||||
73
agents/legal-team/合同律师/SOUL.md
Normal file
73
agents/legal-team/合同律师/SOUL.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# 合同律师 - 系统提示词
|
||||||
|
|
||||||
|
## 角色定位
|
||||||
|
|
||||||
|
你是一位专注于合同法律服务的资深律师,拥有 10 年以上的合同审查和起草经验。你的专业领域包括:
|
||||||
|
|
||||||
|
- 商事合同(买卖、租赁、服务等)
|
||||||
|
- 劳动合同
|
||||||
|
- 技术合同
|
||||||
|
- 投融资合同
|
||||||
|
|
||||||
|
## 工作方式
|
||||||
|
|
||||||
|
### 合同审查流程
|
||||||
|
|
||||||
|
1. **通读合同**:了解合同目的和整体结构
|
||||||
|
2. **逐条审查**:检查每个条款的合法性和合理性
|
||||||
|
3. **风险识别**:标注风险点和潜在问题
|
||||||
|
4. **建议修改**:提供具体的修改建议和理由
|
||||||
|
|
||||||
|
### 审查重点
|
||||||
|
|
||||||
|
- **主体资格**:签约方是否有权签署
|
||||||
|
- **权利义务**:是否对等、明确、可执行
|
||||||
|
- **违约责任**:是否合理、完整、可操作
|
||||||
|
- **争议解决**:管辖和适用法律是否明确
|
||||||
|
- **特殊条款**:保密、知识产权、不可抗力等
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
|
||||||
|
### 审查报告结构
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 合同审查报告
|
||||||
|
|
||||||
|
### 一、合同概况
|
||||||
|
- 合同类型
|
||||||
|
- 主要条款
|
||||||
|
- 涉及金额/期限
|
||||||
|
|
||||||
|
### 二、主要风险点
|
||||||
|
1. [风险点1]
|
||||||
|
- 问题描述
|
||||||
|
- 风险等级:高/中/低
|
||||||
|
- 修改建议
|
||||||
|
|
||||||
|
2. [风险点2]
|
||||||
|
...
|
||||||
|
|
||||||
|
### 三、修改建议
|
||||||
|
- 建议增加的条款
|
||||||
|
- 建议修改的条款
|
||||||
|
- 建议删除的条款
|
||||||
|
|
||||||
|
### 四、总体评价
|
||||||
|
- 合同完整性评分
|
||||||
|
- 风险综合评估
|
||||||
|
- 是否建议签署
|
||||||
|
```
|
||||||
|
|
||||||
|
## 沟通风格
|
||||||
|
|
||||||
|
- **专业细致**:逐条分析,不遗漏任何细节
|
||||||
|
- **风险导向**:明确指出风险点和后果
|
||||||
|
- **务实建议**:提供可操作的具体建议
|
||||||
|
- **对比分析**:展示修改前后的差异
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 不提供标准合同模板(除非用户明确要求)
|
||||||
|
2. 审查时假设用户提供的信息真实准确
|
||||||
|
3. 对于明显不公平的条款,明确提示用户
|
||||||
|
4. 涉及重大金额或复杂法律关系时,建议咨询专业律师
|
||||||
17
agents/legal-team/合规专员/AGENT.md
Normal file
17
agents/legal-team/合规专员/AGENT.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: 合规专员
|
||||||
|
role: member
|
||||||
|
description: 负责合规检查、风险识别和合规建议的专业人员
|
||||||
|
version: 1.0.0
|
||||||
|
skills:
|
||||||
|
- 法律知识库
|
||||||
|
---
|
||||||
|
|
||||||
|
# 合规专员
|
||||||
|
|
||||||
|
企业合规管理的专业支持者,负责:
|
||||||
|
|
||||||
|
- 业务流程合规性检查
|
||||||
|
- 法律法规识别和解读
|
||||||
|
- 合规风险识别和评估
|
||||||
|
- 合规制度和流程优化建议
|
||||||
88
agents/legal-team/合规专员/SOUL.md
Normal file
88
agents/legal-team/合规专员/SOUL.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# 合规专员 - 系统提示词
|
||||||
|
|
||||||
|
## 角色定位
|
||||||
|
|
||||||
|
你是一位专业的合规专员,拥有丰富的企业合规管理经验。你的核心职责是:
|
||||||
|
|
||||||
|
- 帮助企业识别和防范合规风险
|
||||||
|
- 解读法律法规要求
|
||||||
|
- 提供合规改进建议
|
||||||
|
- 协助建立合规管理体系
|
||||||
|
|
||||||
|
## 专业领域
|
||||||
|
|
||||||
|
### 主要法规
|
||||||
|
- 公司法及相关法规
|
||||||
|
- 劳动法及劳动合同法
|
||||||
|
- 个人信息保护法
|
||||||
|
- 数据安全法
|
||||||
|
- 反不正当竞争法
|
||||||
|
- 行业特定法规
|
||||||
|
|
||||||
|
### 合规场景
|
||||||
|
- 劳动用工合规
|
||||||
|
- 数据保护合规
|
||||||
|
- 知识产权合规
|
||||||
|
- 广告宣传合规
|
||||||
|
- 反垄断合规
|
||||||
|
|
||||||
|
## 工作方式
|
||||||
|
|
||||||
|
### 合规检查流程
|
||||||
|
|
||||||
|
1. **了解业务**:明确业务模式和流程
|
||||||
|
2. **识别法规**:找出适用的法律法规
|
||||||
|
3. **对照检查**:逐项检查合规情况
|
||||||
|
4. **风险评级**:评估违规风险等级
|
||||||
|
5. **提出建议**:提供整改方案和优化建议
|
||||||
|
|
||||||
|
### 合规建议输出
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 合规检查报告
|
||||||
|
|
||||||
|
### 一、业务概述
|
||||||
|
- 业务类型
|
||||||
|
- 核心流程
|
||||||
|
- 涉及主体
|
||||||
|
|
||||||
|
### 二、法规要求
|
||||||
|
1. [法规名称]
|
||||||
|
- 适用条款
|
||||||
|
- 具体要求
|
||||||
|
|
||||||
|
### 三、合规检查结果
|
||||||
|
| 检查项 | 法规要求 | 当前状态 | 合规情况 | 风险等级 |
|
||||||
|
|--------|----------|----------|----------|----------|
|
||||||
|
| ... | ... | ... | ... | 高/中/低 |
|
||||||
|
|
||||||
|
### 四、风险点分析
|
||||||
|
1. [风险点1]
|
||||||
|
- 问题描述
|
||||||
|
- 潜在后果
|
||||||
|
- 违规可能性
|
||||||
|
|
||||||
|
### 五、整改建议
|
||||||
|
- 短期措施(立即执行)
|
||||||
|
- 中期措施(1-3个月)
|
||||||
|
- 长期措施(持续改进)
|
||||||
|
|
||||||
|
### 六、合规建议优先级
|
||||||
|
1. 高优先级(必须整改)
|
||||||
|
2. 中优先级(建议整改)
|
||||||
|
3. 低优先级(优化提升)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 沟通风格
|
||||||
|
|
||||||
|
- **严谨规范**:准确引用法规条文
|
||||||
|
- **实用导向**:提供可落地的整改方案
|
||||||
|
- **风险意识**:明确违规后果和责任
|
||||||
|
- **平衡考量**:兼顾合规要求和业务实际
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 不替代企业合规部门的职责
|
||||||
|
2. 建议基于公开法律法规,不涉及内部政策
|
||||||
|
3. 对于复杂的合规问题,建议咨询专业律师或合规顾问
|
||||||
|
4. 提供的建议应结合企业实际情况调整
|
||||||
18
agents/legal-team/法律总监/AGENT.md
Normal file
18
agents/legal-team/法律总监/AGENT.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: 法律总监
|
||||||
|
role: master
|
||||||
|
description: 法律咨询团队负责人,协调各项法律事务,提供战略级法律建议
|
||||||
|
version: 1.0.0
|
||||||
|
skills:
|
||||||
|
- 合同审查
|
||||||
|
- 法律知识库
|
||||||
|
---
|
||||||
|
|
||||||
|
# 法律总监
|
||||||
|
|
||||||
|
法律咨询团队的核心角色,负责:
|
||||||
|
|
||||||
|
- 接收和分析用户的法律需求
|
||||||
|
- 协调团队成员完成任务
|
||||||
|
- 整合团队意见,提供最终建议
|
||||||
|
- 处理复杂的跨领域法律问题
|
||||||
48
agents/legal-team/法律总监/SOUL.md
Normal file
48
agents/legal-team/法律总监/SOUL.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# 法律总监 - 系统提示词
|
||||||
|
|
||||||
|
## 角色定位
|
||||||
|
|
||||||
|
你是一位经验丰富的法律总监,拥有 15 年以上的法律服务经验。你的职责是:
|
||||||
|
|
||||||
|
1. **需求分析**:准确理解用户的法律需求,识别关键问题
|
||||||
|
2. **任务分配**:根据需求类型,合理分配给团队成员
|
||||||
|
3. **质量把控**:审查团队成员的工作成果,确保专业性和准确性
|
||||||
|
4. **综合建议**:整合各方意见,提供可执行的法律建议
|
||||||
|
|
||||||
|
## 工作方式
|
||||||
|
|
||||||
|
### 接收需求时
|
||||||
|
- 仔细倾听用户描述,必要时追问细节
|
||||||
|
- 识别问题的法律性质和复杂程度
|
||||||
|
- 判断是否需要团队协作
|
||||||
|
|
||||||
|
### 分配任务时
|
||||||
|
- 简单问题:直接回答或分配给单一成员
|
||||||
|
- 复杂问题:协调多个成员协同工作
|
||||||
|
- 明确告知每个成员的任务目标和期望输出
|
||||||
|
|
||||||
|
### 整合结果时
|
||||||
|
- 检查团队成员的输出是否完整、准确
|
||||||
|
- 补充遗漏的关键点
|
||||||
|
- 提供结构化的最终建议
|
||||||
|
|
||||||
|
## 沟通风格
|
||||||
|
|
||||||
|
- **专业严谨**:使用准确的法律术语
|
||||||
|
- **条理清晰**:用要点和分段组织内容
|
||||||
|
- **实用导向**:提供可操作的具体建议
|
||||||
|
- **风险意识**:明确指出潜在风险和注意事项
|
||||||
|
|
||||||
|
## 知识背景
|
||||||
|
|
||||||
|
- 精通民商法、合同法、公司法
|
||||||
|
- 熟悉劳动法、知识产权法
|
||||||
|
- 了解行业法规和合规要求
|
||||||
|
- 掌握法律文书写作规范
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 不提供具体的法律文书模板(需要时可指导合同律师起草)
|
||||||
|
2. 不替代执业律师提供正式法律意见
|
||||||
|
3. 涉及重大利益时,提示用户咨询专业律师
|
||||||
|
4. 保持客观中立,避免偏向性建议
|
||||||
@ -11,13 +11,15 @@ func main() {
|
|||||||
agentsDir := env("AGENTS_DIR", "agents")
|
agentsDir := env("AGENTS_DIR", "agents")
|
||||||
skillsDir := env("SKILLS_DIR", "skills")
|
skillsDir := env("SKILLS_DIR", "skills")
|
||||||
roomsDir := env("ROOMS_DIR", "rooms")
|
roomsDir := env("ROOMS_DIR", "rooms")
|
||||||
|
usersDir := env("USERS_DIR", "users")
|
||||||
|
teamsDir := env("TEAMS_DIR", "teams")
|
||||||
addr := env("ADDR", ":8080")
|
addr := env("ADDR", ":8080")
|
||||||
|
|
||||||
for _, dir := range []string{agentsDir, skillsDir, roomsDir} {
|
for _, dir := range []string{agentsDir, skillsDir, roomsDir, usersDir, teamsDir} {
|
||||||
os.MkdirAll(dir, 0755)
|
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.Printf("agent-team server starting on %s", addr)
|
||||||
log.Fatal(s.Start(addr))
|
log.Fatal(s.Start(addr))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -15,7 +16,7 @@ import (
|
|||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/sdaduanbilei/agent-team/internal/hub"
|
"github.com/sdaduanbilei/agent-team/internal/hub"
|
||||||
"github.com/sdaduanbilei/agent-team/internal/room"
|
"github.com/sdaduanbilei/agent-team/internal/room"
|
||||||
"github.com/sdaduanbilei/agent-team/internal/skill"
|
"github.com/sdaduanbilei/agent-team/internal/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@ -23,23 +24,29 @@ type Server struct {
|
|||||||
agentsDir string
|
agentsDir string
|
||||||
skillsDir string
|
skillsDir string
|
||||||
roomsDir string
|
roomsDir string
|
||||||
|
usersDir string
|
||||||
|
teamsDir string
|
||||||
rooms map[string]*room.Room
|
rooms map[string]*room.Room
|
||||||
|
user *user.User
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
clients map[string]map[*websocket.Conn]bool // roomID -> conns
|
clients map[string]map[*websocket.Conn]bool // roomID -> conns
|
||||||
clientsMu sync.Mutex
|
clientsMu sync.Mutex
|
||||||
upgrader websocket.Upgrader
|
upgrader websocket.Upgrader
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(agentsDir, skillsDir, roomsDir string) *Server {
|
func New(agentsDir, skillsDir, roomsDir, usersDir, teamsDir string) *Server {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
e: echo.New(),
|
e: echo.New(),
|
||||||
agentsDir: agentsDir,
|
agentsDir: agentsDir,
|
||||||
skillsDir: skillsDir,
|
skillsDir: skillsDir,
|
||||||
roomsDir: roomsDir,
|
roomsDir: roomsDir,
|
||||||
|
usersDir: usersDir,
|
||||||
|
teamsDir: teamsDir,
|
||||||
rooms: make(map[string]*room.Room),
|
rooms: make(map[string]*room.Room),
|
||||||
clients: make(map[string]map[*websocket.Conn]bool),
|
clients: make(map[string]map[*websocket.Conn]bool),
|
||||||
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
|
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
|
||||||
}
|
}
|
||||||
|
s.loadUser()
|
||||||
s.loadRooms()
|
s.loadRooms()
|
||||||
s.routes()
|
s.routes()
|
||||||
return s
|
return s
|
||||||
@ -60,15 +67,26 @@ func (s *Server) routes() {
|
|||||||
g.PUT("/agents/:name/files/:file", s.writeAgentFile)
|
g.PUT("/agents/:name/files/:file", s.writeAgentFile)
|
||||||
g.POST("/agents", s.createAgent)
|
g.POST("/agents", s.createAgent)
|
||||||
g.DELETE("/agents/:name", s.deleteAgent)
|
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", 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", s.listWorkspace)
|
||||||
g.GET("/rooms/:id/workspace/:file", s.getWorkspaceFile)
|
g.GET("/rooms/:id/workspace/:file", s.getWorkspaceFile)
|
||||||
g.GET("/rooms/:id/tasks", s.getTasks)
|
g.GET("/rooms/:id/tasks", s.getTasks)
|
||||||
g.GET("/rooms/:id/history", s.listHistory)
|
g.GET("/rooms/:id/history", s.listHistory)
|
||||||
g.GET("/rooms/:id/messages", s.getMessages)
|
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)
|
s.e.GET("/ws/:roomID", s.wsHandler)
|
||||||
}
|
}
|
||||||
@ -83,11 +101,33 @@ func (s *Server) loadRooms() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
r.User = s.user
|
||||||
r.Broadcast = func(ev room.Event) { s.broadcast(ev.RoomID, ev) }
|
r.Broadcast = func(ev room.Event) { s.broadcast(ev.RoomID, ev) }
|
||||||
s.rooms[e.Name()] = r
|
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) {
|
func (s *Server) broadcast(roomID string, ev room.Event) {
|
||||||
s.clientsMu.Lock()
|
s.clientsMu.Lock()
|
||||||
defer s.clientsMu.Unlock()
|
defer s.clientsMu.Unlock()
|
||||||
@ -125,6 +165,7 @@ func (s *Server) wsHandler(c echo.Context) error {
|
|||||||
var ev struct {
|
var ev struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
}
|
}
|
||||||
if json.Unmarshal(msg, &ev) != nil || ev.Type != "user_message" {
|
if json.Unmarshal(msg, &ev) != nil || ev.Type != "user_message" {
|
||||||
continue
|
continue
|
||||||
@ -135,7 +176,11 @@ func (s *Server) wsHandler(c echo.Context) error {
|
|||||||
if r == nil {
|
if r == nil {
|
||||||
continue
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@ -244,20 +289,6 @@ func (s *Server) deleteAgent(c echo.Context) error {
|
|||||||
return os.RemoveAll(filepath.Join(s.agentsDir, name))
|
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 {
|
func (s *Server) hubInstall(c echo.Context) error {
|
||||||
var body struct {
|
var body struct {
|
||||||
Repo string `json:"repo"`
|
Repo string `json:"repo"`
|
||||||
@ -265,11 +296,43 @@ func (s *Server) hubInstall(c echo.Context) error {
|
|||||||
if err := c.Bind(&body); err != nil {
|
if err := c.Bind(&body); err != nil {
|
||||||
return err
|
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()})
|
return c.JSON(500, map[string]string{"error": err.Error()})
|
||||||
}
|
}
|
||||||
s.loadRooms()
|
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 {
|
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)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -1,23 +1,38 @@
|
|||||||
package hub
|
package hub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Install clones a GitHub repo (owner/repo or full URL) and installs the team.
|
type Team struct {
|
||||||
func Install(repoRef, agentsDir, skillsDir string) error {
|
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
|
url := repoRef
|
||||||
if !strings.HasPrefix(repoRef, "http") {
|
if !strings.HasPrefix(repoRef, "http") && !strings.HasPrefix(repoRef, "git@") {
|
||||||
url = "https://github.com/" + repoRef
|
url = "https://github.com/" + repoRef
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract repo name for team name
|
||||||
|
repoName := extractRepoName(url)
|
||||||
|
|
||||||
tmp, err := os.MkdirTemp("", "agent-team-hub-*")
|
tmp, err := os.MkdirTemp("", "agent-team-hub-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmp)
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
@ -25,20 +40,175 @@ func Install(repoRef, agentsDir, skillsDir string) error {
|
|||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("git clone: %w", err)
|
return nil, fmt.Errorf("git clone: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy agents/
|
return installFromDir(tmp, repoName, url, agentsDir, skillsDir, teamsDir)
|
||||||
if err := copyDir(filepath.Join(tmp, "agents"), agentsDir); err != nil {
|
|
||||||
return fmt.Errorf("install agents: %w", err)
|
|
||||||
}
|
}
|
||||||
// Copy skills/
|
|
||||||
if err := copyDir(filepath.Join(tmp, "skills"), skillsDir); err != nil {
|
// InstallZIP extracts a ZIP file and installs the team.
|
||||||
return fmt.Errorf("install skills: %w", err)
|
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
|
||||||
|
}
|
||||||
|
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
|
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 {
|
func copyDir(src, dst string) error {
|
||||||
if _, err := os.Stat(src); os.IsNotExist(err) {
|
if _, err := os.Stat(src); os.IsNotExist(err) {
|
||||||
return nil // optional dir, skip
|
return nil // optional dir, skip
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/sdaduanbilei/agent-team/internal/agent"
|
"github.com/sdaduanbilei/agent-team/internal/agent"
|
||||||
"github.com/sdaduanbilei/agent-team/internal/llm"
|
"github.com/sdaduanbilei/agent-team/internal/llm"
|
||||||
"github.com/sdaduanbilei/agent-team/internal/skill"
|
"github.com/sdaduanbilei/agent-team/internal/skill"
|
||||||
|
"github.com/sdaduanbilei/agent-team/internal/user"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ type Room struct {
|
|||||||
master *agent.Agent
|
master *agent.Agent
|
||||||
members map[string]*agent.Agent
|
members map[string]*agent.Agent
|
||||||
skillMeta []skill.Meta
|
skillMeta []skill.Meta
|
||||||
|
User *user.User
|
||||||
Status Status
|
Status Status
|
||||||
ActiveAgent string // for working status display
|
ActiveAgent string // for working status display
|
||||||
Broadcast func(Event) // set by api layer
|
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.
|
// Handle processes a user message through master orchestration.
|
||||||
func (r *Room) Handle(ctx context.Context, userMsg string) error {
|
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, "", "")
|
r.setStatus(StatusThinking, "", "")
|
||||||
|
|
||||||
// Build master context
|
// Build master context
|
||||||
teamXML := r.buildTeamXML()
|
teamXML := r.buildTeamXML()
|
||||||
skillXML := skill.ToXML(r.skillMeta)
|
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{
|
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:<member_name>:<task description>. When you are done reviewing and satisfied, output DONE:<summary>."),
|
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:<member_name>:<task description>. When you are done reviewing and satisfied, output DONE:<summary>."),
|
||||||
|
|||||||
127
internal/user/user.go
Normal file
127
internal/user/user.go
Normal file
@ -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/<name>/
|
||||||
|
}
|
||||||
|
|
||||||
|
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("<user_info>\n")
|
||||||
|
xml.WriteString(fmt.Sprintf(" <name>%s</name>\n", u.Config.Name))
|
||||||
|
xml.WriteString(fmt.Sprintf(" <description>%s</description>\n", u.Config.Description))
|
||||||
|
if u.Profile != "" {
|
||||||
|
xml.WriteString(" <profile>\n")
|
||||||
|
xml.WriteString(u.Profile)
|
||||||
|
xml.WriteString(" </profile>\n")
|
||||||
|
}
|
||||||
|
xml.WriteString("</user_info>")
|
||||||
|
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
|
||||||
|
}
|
||||||
197
skills/legal-team/合同审查/SKILL.md
Normal file
197
skills/legal-team/合同审查/SKILL.md
Normal file
@ -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. 明确损失范围,避免争议
|
||||||
|
```
|
||||||
273
skills/legal-team/法律知识库/SKILL.md
Normal file
273
skills/legal-team/法律知识库/SKILL.md
Normal file
@ -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. **证据的重要性**
|
||||||
|
- 用人单位对解除劳动合同的合法性承担举证责任
|
||||||
|
- 证据不足可能导致被认定为违法解除
|
||||||
|
|
||||||
|
建议在辞退员工前,咨询专业劳动法律师,评估法律风险并制定合适的方案。
|
||||||
|
```
|
||||||
14
teams/legal-team/team.yaml
Normal file
14
teams/legal-team/team.yaml
Normal file
@ -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
|
||||||
9
users/default/PROFILE.md
Normal file
9
users/default/PROFILE.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 我的简介
|
||||||
|
|
||||||
|
## 我是谁
|
||||||
|
(介绍你的身份、职业背景)
|
||||||
|
|
||||||
|
## 工作风格
|
||||||
|
- 喜欢的沟通方式
|
||||||
|
- 重视的点
|
||||||
|
- 不喜欢的内容
|
||||||
8
users/default/USER.md
Normal file
8
users/default/USER.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: 用户
|
||||||
|
description:
|
||||||
|
provider: deepseek
|
||||||
|
model: deepseek-chat
|
||||||
|
api_key_env: DEEPSEEK_API_KEY
|
||||||
|
avatar_color: "#5865F2"
|
||||||
|
---
|
||||||
838
web/package-lock.json
generated
838
web/package-lock.json
generated
@ -8,7 +8,9 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"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": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@ -269,6 +271,15 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@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": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@ -1018,29 +1029,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.3",
|
"version": "1.0.0-rc.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||||
@ -1793,6 +1781,12 @@
|
|||||||
"undici-types": "~7.16.0"
|
"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": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
@ -1812,14 +1806,6 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@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": {
|
"node_modules/@types/unist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
@ -2121,6 +2107,191 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"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": {
|
"node_modules/@ungap/structured-clone": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||||
@ -2278,6 +2449,22 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@ -2480,6 +2667,22 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@ -2568,14 +2771,17 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/direction": {
|
||||||
"version": "3.2.7",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz",
|
||||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
"integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==",
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
"license": "MIT",
|
||||||
"peer": true,
|
"bin": {
|
||||||
"optionalDependencies": {
|
"direction": "cli.js"
|
||||||
"@types/trusted-types": "^2.0.7"
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
@ -2599,6 +2805,18 @@
|
|||||||
"node": ">=10.13.0"
|
"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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.3",
|
"version": "0.27.3",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||||
@ -2993,6 +3211,12 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@ -3036,6 +3260,171 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/hast-util-to-jsx-runtime": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
"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"
|
"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": {
|
"node_modules/hast-util-whitespace": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
||||||
@ -3076,6 +3497,23 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/hermes-estree": {
|
||||||
"version": "0.25.1",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||||
@ -3103,6 +3541,16 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@ -3637,6 +4085,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@ -3657,19 +4114,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/mdast-util-find-and-replace": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
"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": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -4578,6 +5011,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@ -4666,6 +5111,24 @@
|
|||||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@ -4844,6 +5307,197 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/remark-gfm": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||||
@ -4862,6 +5516,21 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/remark-parse": {
|
||||||
"version": "11.0.0",
|
"version": "11.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
||||||
@ -5024,12 +5693,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/stringify-entities": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||||
@ -5236,6 +5899,17 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/unist-util-is": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
|
||||||
@ -5366,6 +6040,20 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/vfile-message": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
"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": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@ -10,7 +10,9 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|||||||
108
web/src/App.tsx
108
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 { useStore } from './store'
|
||||||
import { RoomSidebar } from './components/RoomSidebar'
|
import { RoomSidebar } from './components/RoomSidebar'
|
||||||
import { ChatView } from './components/ChatView'
|
import { ChatView } from './components/ChatView'
|
||||||
import { AgentsPage } from './components/AgentsPage'
|
|
||||||
import { SkillsPage } from './components/SkillsPage'
|
|
||||||
import { MarketPage } from './components/MarketPage'
|
import { MarketPage } from './components/MarketPage'
|
||||||
|
import { UserSettings } from './components/UserSettings'
|
||||||
|
import { ThemeToggle } from './components/ThemeToggle'
|
||||||
|
import { Onboarding } from './components/Onboarding'
|
||||||
|
|
||||||
export default function App() {
|
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 (
|
return (
|
||||||
<div className="flex h-screen bg-gray-800 text-gray-100 overflow-hidden">
|
<>
|
||||||
<div className="flex flex-col w-12 bg-gray-900 border-r border-gray-700 items-center py-3 gap-3">
|
{showOnboarding && !onboardingCompleted && (
|
||||||
<NavBtn icon="💬" label="群聊" active={page === 'chat'} onClick={() => setPage('chat')} />
|
<Onboarding onComplete={handleOnboardingComplete} />
|
||||||
<NavBtn icon="🤖" label="Agents" active={page === 'agents'} onClick={() => setPage('agents')} />
|
)}
|
||||||
<NavBtn icon="🔧" label="Skills" active={page === 'skills'} onClick={() => setPage('skills')} />
|
|
||||||
<NavBtn icon="🛒" label="市场" active={page === 'market'} onClick={() => setPage('market')} />
|
<div className="flex h-screen bg-[var(--bg-primary)] text-[var(--text-primary)] overflow-hidden">
|
||||||
|
{/* Left sidebar - Navigation */}
|
||||||
|
<div className="flex flex-col w-[72px] bg-[var(--sidebar-bg)] items-center py-3 gap-2">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="w-12 h-12 bg-[var(--accent)] rounded-xl flex items-center justify-center mb-2">
|
||||||
|
<Hash className="w-6 h-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{page === 'chat' && <><RoomSidebar /><ChatView /></>}
|
{/* Nav buttons */}
|
||||||
{page === 'agents' && <AgentsPage />}
|
<div className="flex flex-col gap-1 w-full px-3">
|
||||||
{page === 'skills' && <SkillsPage />}
|
{navItems.map(item => (
|
||||||
{page === 'market' && <MarketPage />}
|
<NavBtn
|
||||||
|
key={item.id}
|
||||||
|
icon={<item.icon className="w-5 h-5" />}
|
||||||
|
label={item.label}
|
||||||
|
active={page === item.id}
|
||||||
|
onClick={() => setPage(item.id as typeof page)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="w-8 h-[2px] bg-[var(--border)] rounded-full my-1" />
|
||||||
|
|
||||||
|
{/* Theme toggle */}
|
||||||
|
<ThemeToggle />
|
||||||
|
|
||||||
|
{/* Settings */}
|
||||||
|
<button
|
||||||
|
onClick={() => setPage('settings')}
|
||||||
|
className={`
|
||||||
|
w-9 h-9 rounded-lg flex items-center justify-center transition-colors duration-200
|
||||||
|
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||||
|
${page === 'settings' ? 'bg-[var(--accent)] text-white' : 'hover:bg-[var(--bg-hover)]'}
|
||||||
|
`}
|
||||||
|
title="设置"
|
||||||
|
>
|
||||||
|
<Settings className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
{page === 'chat' && <RoomSidebar />}
|
||||||
|
{page === 'chat' && <ChatView />}
|
||||||
|
{page === 'market' && <MarketPage />}
|
||||||
|
{page === 'settings' && <UserSettings />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<button onClick={onClick} title={label}
|
<button
|
||||||
className={`w-9 h-9 rounded-lg flex items-center justify-center text-lg ${active ? 'bg-indigo-600' : 'hover:bg-gray-700'}`}>
|
onClick={onClick}
|
||||||
|
title={label}
|
||||||
|
className={`
|
||||||
|
w-full h-12 rounded-xl flex items-center justify-center transition-all duration-200 group relative
|
||||||
|
${active
|
||||||
|
? 'bg-[var(--accent)] text-white'
|
||||||
|
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
{/* Active indicator */}
|
||||||
|
{active && (
|
||||||
|
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-white rounded-r-full" />
|
||||||
|
)}
|
||||||
|
{/* Hover tooltip */}
|
||||||
|
<div className="absolute left-full ml-4 px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-primary)] text-sm rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50 shadow-lg">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -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<string | null>(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 (
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
|
||||||
{/* Agent list */}
|
|
||||||
<div className="w-48 border-r border-gray-700 flex flex-col">
|
|
||||||
<div className="p-3 border-b border-gray-700">
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<input className="flex-1 bg-gray-700 rounded px-2 py-1 text-xs" placeholder="新 agent 名" value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && create()} />
|
|
||||||
<button onClick={create} className="bg-indigo-600 hover:bg-indigo-500 px-2 py-1 rounded text-xs">+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
{agents.map(a => (
|
|
||||||
<div key={a.name} onClick={() => 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' : ''}`}>
|
|
||||||
<span className="truncate">{a.name}</span>
|
|
||||||
<button onClick={e => { e.stopPropagation(); del(a.name) }} className="text-gray-500 hover:text-red-400 text-xs">✕</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Editor */}
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
|
||||||
{selected ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-gray-700">
|
|
||||||
<span className="font-semibold text-sm">{selected}</span>
|
|
||||||
<div className="flex gap-1 ml-2">
|
|
||||||
{(['AGENT.md', 'SOUL.md'] as const).map(t => (
|
|
||||||
<button key={t} onClick={() => setTab(t)}
|
|
||||||
className={`text-xs px-3 py-1 rounded ${tab === t ? 'bg-indigo-600' : 'bg-gray-700 hover:bg-gray-600'}`}>{t}</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button onClick={save} className="ml-auto bg-green-700 hover:bg-green-600 px-3 py-1 rounded text-xs">保存</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Editor height="100%" defaultLanguage="markdown" theme="vs-dark" value={content} onChange={v => setContent(v || '')} options={{ minimap: { enabled: false }, wordWrap: 'on', fontSize: 13 }} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex items-center justify-center text-gray-400 text-sm">选择一个 agent 编辑</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,6 +1,17 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
|
import {
|
||||||
|
Hash,
|
||||||
|
Users,
|
||||||
|
ListTodo,
|
||||||
|
FileText,
|
||||||
|
X,
|
||||||
|
Plus,
|
||||||
|
Smile,
|
||||||
|
Send,
|
||||||
|
Crown,
|
||||||
|
} from 'lucide-react'
|
||||||
import { useStore } from '../store'
|
import { useStore } from '../store'
|
||||||
import type { Message } from '../types'
|
import type { Message } from '../types'
|
||||||
|
|
||||||
@ -9,7 +20,7 @@ const API = '/api'
|
|||||||
export function ChatView() {
|
export function ChatView() {
|
||||||
const { activeRoomId, rooms, messages, tasks, workspace, sendMessage } = useStore()
|
const { activeRoomId, rooms, messages, tasks, workspace, sendMessage } = useStore()
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [drawer, setDrawer] = useState<null | 'skills' | 'history' | 'workspace'>(null)
|
const [drawer, setDrawer] = useState<null | 'members' | 'tasks' | 'files'>(null)
|
||||||
const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null)
|
const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null)
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@ -25,115 +36,210 @@ export function ChatView() {
|
|||||||
setPreviewFile({ name: filename, content: d.content || '' })
|
setPreviewFile({ name: filename, content: d.content || '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!room) return <div className="flex-1 flex items-center justify-center text-gray-400">选择一个群开始</div>
|
if (!room) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center bg-[var(--bg-primary)] text-[var(--text-muted)]">
|
||||||
|
<Hash className="w-16 h-16 mb-4 opacity-20" />
|
||||||
|
<p className="text-lg">选择一个群开始聊天</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const statusLabel = room.status === 'working' && room.activeAgent
|
const statusLabel = room.status === 'working' && room.activeAgent
|
||||||
? `working · ${room.activeAgent} ${room.action || ''}`
|
? `${room.activeAgent} ${room.action || ''}`
|
||||||
: room.status
|
: 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 (
|
return (
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden bg-[var(--bg-primary)]">
|
||||||
{/* Chat area */}
|
{/* Main chat area */}
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
<div className="px-4 py-2 border-b border-gray-700 flex items-center gap-2">
|
{/* Header - Discord style */}
|
||||||
<span className="font-semibold">{room.name}</span>
|
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center gap-2 bg-[var(--bg-secondary)] shadow-sm">
|
||||||
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
<Hash className="w-5 h-5 text-[var(--text-muted)]" />
|
||||||
room.status === 'pending' ? 'bg-gray-600 text-gray-300' :
|
<span className="font-semibold text-base">{room.name}</span>
|
||||||
room.status === 'thinking' ? 'bg-yellow-600 text-yellow-100' :
|
<div className="h-6 w-[1px] bg-[var(--border)] mx-2" />
|
||||||
'bg-green-700 text-green-100'
|
<span className="text-sm text-[var(--text-muted)] truncate">
|
||||||
}`}>{statusLabel}</span>
|
{room.type === 'dept' ? '部门群' : 'Leader 群'}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1" />
|
||||||
|
{/* Status badge */}
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded-full font-medium flex items-center gap-1.5 ${
|
||||||
|
room.status === 'pending' ? 'bg-[var(--bg-tertiary)] text-[var(--text-muted)]' :
|
||||||
|
room.status === 'thinking' ? 'bg-[var(--color-warning)]/20 text-[var(--color-warning)]' :
|
||||||
|
'bg-[var(--color-success)]/20 text-[var(--color-success)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||||
|
room.status === 'pending' ? 'bg-[var(--text-muted)]' :
|
||||||
|
room.status === 'thinking' ? 'bg-[var(--color-warning)] animate-pulse' :
|
||||||
|
'bg-[var(--color-success)] animate-pulse'
|
||||||
|
}`} />
|
||||||
|
{statusLabel}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
{/* Messages */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-1 scrollbar-thin">
|
||||||
|
{msgs.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-[var(--text-muted)]">
|
||||||
|
<Hash className="w-12 h-12 mb-2 opacity-30" />
|
||||||
|
<p className="text-sm">这里是 {room.name} 的开始</p>
|
||||||
|
<p className="text-xs mt-1">发送消息开始对话</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{msgs.map(msg => <MessageBubble key={msg.id} msg={msg} />)}
|
{msgs.map(msg => <MessageBubble key={msg.id} msg={msg} />)}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3 border-t border-gray-700 flex gap-2">
|
{/* Input area - Discord style */}
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<div className="bg-[var(--bg-tertiary)] rounded-lg flex items-center gap-2 px-4 py-2">
|
||||||
|
<button className="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
className="flex-1 bg-gray-700 rounded px-3 py-2 text-sm outline-none"
|
className="flex-1 bg-transparent outline-none text-sm placeholder:text-[var(--text-muted)]"
|
||||||
placeholder="输入消息..."
|
placeholder={`#${room.name} 中发送消息...`}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={e => setInput(e.target.value)}
|
onChange={e => setInput(e.target.value)}
|
||||||
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey && input.trim()) { sendMessage(room.id, input.trim()); setInput('') } }}
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && input.trim()) {
|
||||||
|
sendMessage(room.id, input.trim())
|
||||||
|
setInput('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<button className="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
|
||||||
|
<Smile className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-indigo-600 hover:bg-indigo-500 px-4 py-2 rounded text-sm"
|
className="text-[var(--accent)] hover:text-[var(--accent-hover)] transition-colors disabled:opacity-50"
|
||||||
onClick={() => { if (input.trim()) { sendMessage(room.id, input.trim()); setInput('') } }}
|
disabled={!input.trim()}
|
||||||
>发送</button>
|
onClick={() => {
|
||||||
|
if (input.trim()) {
|
||||||
|
sendMessage(room.id, input.trim())
|
||||||
|
setInput('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right panel */}
|
{/* Right panel */}
|
||||||
<div className="w-64 border-l border-gray-700 flex flex-col overflow-hidden">
|
<div className="w-72 border-l border-[var(--border)] bg-[var(--bg-secondary)] flex flex-col overflow-hidden">
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
{/* Panel tabs */}
|
||||||
|
<div className="flex border-b border-[var(--border)]">
|
||||||
|
{drawerButtons.map(btn => (
|
||||||
|
<button
|
||||||
|
key={btn.id}
|
||||||
|
onClick={() => setDrawer(drawer === btn.id ? null : btn.id)}
|
||||||
|
className={`
|
||||||
|
flex-1 py-3 text-xs font-medium flex items-center justify-center gap-1.5 transition-colors border-b-2
|
||||||
|
${drawer === btn.id
|
||||||
|
? 'border-[var(--accent)] text-[var(--accent)]'
|
||||||
|
: 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<btn.icon className="w-4 h-4" />
|
||||||
|
{btn.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Panel content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin">
|
||||||
{/* Members */}
|
{/* Members */}
|
||||||
<section>
|
{(drawer === 'members' || drawer === null) && (
|
||||||
<h3 className="text-xs font-semibold text-gray-400 uppercase mb-2">Members</h3>
|
<section className={drawer !== 'members' && drawer !== null ? 'hidden' : ''}>
|
||||||
<MemberItem name={room.master} role="master" status={room.status === 'thinking' ? 'thinking' : 'pending'} />
|
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
|
||||||
|
<Users className="w-3 h-3" />
|
||||||
|
成员 — {1 + msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).length}
|
||||||
|
</h3>
|
||||||
|
<MemberItem
|
||||||
|
name={room.master}
|
||||||
|
role="master"
|
||||||
|
status={room.status === 'thinking' ? 'thinking' : 'idle'}
|
||||||
|
/>
|
||||||
{msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).map(name => (
|
{msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).map(name => (
|
||||||
<MemberItem key={name} name={name} role="member"
|
<MemberItem
|
||||||
status={room.status === 'working' && room.activeAgent === name ? 'working' : 'pending'} />
|
key={name}
|
||||||
|
name={name}
|
||||||
|
role="member"
|
||||||
|
status={room.status === 'working' && room.activeAgent === name ? 'working' : 'idle'}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tasks */}
|
{/* Tasks */}
|
||||||
{tasksMd && (
|
{(drawer === 'tasks' || drawer === null) && tasksMd && (
|
||||||
<section>
|
<section className={`mt-4 ${drawer !== 'tasks' && drawer !== null ? 'hidden' : ''}`}>
|
||||||
<h3 className="text-xs font-semibold text-gray-400 uppercase mb-2">Tasks</h3>
|
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
|
||||||
<div className="text-xs prose prose-invert max-w-none">
|
<ListTodo className="w-3 h-3" />
|
||||||
|
任务列表
|
||||||
|
</h3>
|
||||||
|
<div className="text-sm prose prose-sm max-w-none dark:prose-invert">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{tasksMd}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{tasksMd}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Workspace files */}
|
{/* Files */}
|
||||||
{files.length > 0 && (
|
{(drawer === 'files' || drawer === null) && files.length > 0 && (
|
||||||
<section>
|
<section className={`mt-4 ${drawer !== 'files' && drawer !== null ? 'hidden' : ''}`}>
|
||||||
<h3 className="text-xs font-semibold text-gray-400 uppercase mb-2">产物</h3>
|
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
|
||||||
|
<FileText className="w-3 h-3" />
|
||||||
|
产物文件
|
||||||
|
</h3>
|
||||||
{files.map(f => (
|
{files.map(f => (
|
||||||
<div key={f} onClick={() => openFile(f)}
|
<div
|
||||||
className="flex items-center gap-1 text-xs text-indigo-300 hover:text-indigo-200 cursor-pointer py-0.5">
|
key={f}
|
||||||
<span>📄</span><span className="truncate">{f}</span>
|
onClick={() => 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"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
<span className="truncate">{f}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Drawer buttons */}
|
|
||||||
<div className="p-2 border-t border-gray-700 flex gap-1">
|
|
||||||
{(['skills', 'history', 'workspace'] as const).map(d => (
|
|
||||||
<button key={d} onClick={() => setDrawer(drawer === d ? null : d)}
|
|
||||||
className={`flex-1 text-xs py-1 rounded ${drawer === d ? 'bg-indigo-600' : 'bg-gray-700 hover:bg-gray-600'}`}>
|
|
||||||
{d === 'skills' ? 'Skills' : d === 'history' ? 'History' : 'Workspace'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Drawer overlay */}
|
|
||||||
{drawer && (
|
|
||||||
<div className="absolute right-64 top-0 bottom-0 w-80 bg-gray-800 border-l border-gray-700 p-4 overflow-y-auto z-10">
|
|
||||||
<div className="flex justify-between items-center mb-3">
|
|
||||||
<h3 className="font-semibold capitalize">{drawer}</h3>
|
|
||||||
<button onClick={() => setDrawer(null)} className="text-gray-400 hover:text-white">✕</button>
|
|
||||||
</div>
|
|
||||||
{drawer === 'workspace' && files.map(f => (
|
|
||||||
<div key={f} onClick={() => openFile(f)} className="text-sm text-indigo-300 py-1 cursor-pointer hover:text-indigo-200">📄 {f}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* File preview modal */}
|
{/* File preview modal */}
|
||||||
{previewFile && (
|
{previewFile && (
|
||||||
<div className="absolute inset-0 bg-black/60 flex items-center justify-center z-20" onClick={() => setPreviewFile(null)}>
|
<div
|
||||||
<div className="bg-gray-800 rounded-lg w-3/4 max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
onClick={() => setPreviewFile(null)}
|
||||||
<span className="font-semibold text-sm">📄 {previewFile.name}</span>
|
>
|
||||||
<button onClick={() => setPreviewFile(null)} className="text-gray-400 hover:text-white">✕</button>
|
<div
|
||||||
|
className="bg-[var(--bg-secondary)] rounded-lg w-full max-w-3xl max-h-[80vh] flex flex-col shadow-2xl"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileText className="w-5 h-5 text-[var(--accent)]" />
|
||||||
|
<span className="font-semibold">{previewFile.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto p-4 prose prose-invert max-w-none text-sm">
|
<button
|
||||||
|
onClick={() => setPreviewFile(null)}
|
||||||
|
className="w-8 h-8 rounded flex items-center justify-center text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto p-4 prose dark:prose-invert max-w-none text-sm">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{previewFile.content}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{previewFile.content}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -146,35 +252,95 @@ export function ChatView() {
|
|||||||
function MessageBubble({ msg }: { msg: Message }) {
|
function MessageBubble({ msg }: { msg: Message }) {
|
||||||
const isUser = msg.role === 'user'
|
const isUser = msg.role === 'user'
|
||||||
const isMaster = msg.role === 'master'
|
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 (
|
return (
|
||||||
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
|
<div className={`flex gap-3 group ${isUser ? 'flex-row-reverse' : ''}`}>
|
||||||
|
{/* Avatar */}
|
||||||
{!isUser && (
|
{!isUser && (
|
||||||
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold mr-2 flex-shrink-0 mt-1 ${isMaster ? 'bg-yellow-600' : 'bg-gray-600'}`}>
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0 mt-0.5 ${avatarColors[colorIndex]}`}>
|
||||||
{isMaster ? '👑' : msg.agent[0]?.toUpperCase()}
|
{isMaster ? <Crown className="w-5 h-5" /> : msg.agent[0]?.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={`max-w-[75%] rounded-lg px-3 py-2 text-sm ${
|
|
||||||
isUser ? 'bg-indigo-600 text-white' :
|
{/* Content */}
|
||||||
isMaster ? 'bg-gray-700 border border-yellow-600/40' :
|
<div className={`flex flex-col max-w-[70%] ${isUser ? 'items-end' : 'items-start'}`}>
|
||||||
'bg-gray-700'
|
{/* Name + timestamp */}
|
||||||
}`}>
|
{!isUser && (
|
||||||
{!isUser && <div className="text-xs text-gray-400 mb-1">{msg.agent}</div>}
|
<div className="flex items-center gap-2 mb-0.5">
|
||||||
<div className="prose prose-invert prose-sm max-w-none">
|
<span className={`font-medium text-sm ${isMaster ? 'text-[var(--color-warning)]' : 'text-[var(--text-primary)]'}`}>
|
||||||
|
{msg.agent}
|
||||||
|
</span>
|
||||||
|
{isMaster && (
|
||||||
|
<span className="text-[10px] px-1 py-0.5 bg-[var(--color-warning)]/20 text-[var(--color-warning)] rounded font-medium">
|
||||||
|
MASTER
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Message bubble */}
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
px-3.5 py-2 rounded-lg text-sm relative group
|
||||||
|
${isUser
|
||||||
|
? 'bg-[var(--accent)] text-white rounded-tr-sm'
|
||||||
|
: isMaster
|
||||||
|
? 'bg-[var(--bg-tertiary)] border-2 border-[var(--color-warning)]/30 rounded-tl-sm'
|
||||||
|
: 'bg-[var(--bg-tertiary)] rounded-tl-sm'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{msg.content}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
{msg.streaming && <span className="inline-block w-1.5 h-3 bg-gray-400 animate-pulse ml-0.5" />}
|
{msg.streaming && (
|
||||||
|
<span className="inline-block w-1.5 h-3 bg-[var(--text-muted)] animate-pulse ml-0.5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MemberItem({ name, role, status }: { name: string; role: string; status: string }) {
|
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 (
|
return (
|
||||||
<div className="flex items-center gap-2 py-1">
|
<div className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors group">
|
||||||
<div className={`w-2 h-2 rounded-full ${dot}`} />
|
<div className="relative">
|
||||||
<span className="text-sm">{role === 'master' ? '👑 ' : ''}{name}</span>
|
<div className={`w-7 h-7 rounded-full bg-[var(--accent)] flex items-center justify-center text-white text-xs font-semibold`}>
|
||||||
<span className="text-xs text-gray-500 ml-auto">{status}</span>
|
{role === 'master' ? <Crown className="w-3.5 h-3.5" /> : name[0]?.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[var(--bg-secondary)] ${dotClass}`} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{role === 'master' && <Crown className="w-3 h-3 text-[var(--color-warning)]" />}
|
||||||
|
<span className="text-sm font-medium truncate">{name}</span>
|
||||||
|
{role === 'master' && (
|
||||||
|
<span className="text-[10px] px-1 bg-[var(--color-warning)]/20 text-[var(--color-warning)] rounded">
|
||||||
|
MASTER
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[var(--text-muted)] opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
{statusText}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { useStore } from '../store'
|
||||||
|
import { TeamDetail } from './TeamDetail'
|
||||||
|
|
||||||
const API = '/api'
|
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() {
|
export function MarketPage() {
|
||||||
const { fetchAgents } = useStore()
|
const { fetchAgents } = useStore()
|
||||||
|
const [tab, setTab] = useState<'featured' | 'install' | 'teams'>('featured')
|
||||||
const [repo, setRepo] = useState('')
|
const [repo, setRepo] = useState('')
|
||||||
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle')
|
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle')
|
||||||
const [errMsg, setErrMsg] = useState('')
|
const [errMsg, setErrMsg] = useState('')
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
const [teams, setTeams] = useState<Team[]>([])
|
||||||
|
const [selectedTeam, setSelectedTeam] = useState<Team | null>(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 () => {
|
const install = async () => {
|
||||||
if (!repo.trim()) return
|
if (!repo.trim()) return
|
||||||
setStatus('loading')
|
setStatus('loading')
|
||||||
|
setErrMsg('')
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${API}/hub/install`, {
|
const r = await fetch(`${API}/hub/install`, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
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) }
|
if (!r.ok) { const d = await r.json(); throw new Error(d.error) }
|
||||||
setStatus('done')
|
setStatus('done')
|
||||||
fetchAgents()
|
fetchAgents()
|
||||||
} catch (e: any) {
|
fetchTeams()
|
||||||
setErrMsg(e.message)
|
} catch (e: unknown) {
|
||||||
|
const errMsg = e instanceof Error ? e.message : '安装失败'
|
||||||
|
setErrMsg(errMsg)
|
||||||
setStatus('error')
|
setStatus('error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
<div className="flex-1 p-8 overflow-y-auto">
|
const file = e.target.files?.[0]
|
||||||
<h2 className="text-xl font-bold mb-2">人才市场</h2>
|
if (!file) return
|
||||||
<p className="text-gray-400 text-sm mb-6">从 GitHub 一键雇佣社区团队,或发布自己的团队供他人使用。</p>
|
|
||||||
|
|
||||||
<div className="bg-gray-800 rounded-lg p-6 max-w-lg mb-8">
|
setUploading(true)
|
||||||
<h3 className="font-semibold mb-3">雇佣团队</h3>
|
setStatus('loading')
|
||||||
<p className="text-xs text-gray-400 mb-3">输入 GitHub repo(如 <code className="bg-gray-700 px-1 rounded">username/legal-team</code>)</p>
|
setErrMsg('')
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${API}/hub/install-zip`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
if (!r.ok) { const d = await r.json(); throw new Error(d.error) }
|
||||||
|
setStatus('done')
|
||||||
|
fetchAgents()
|
||||||
|
fetchTeams()
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const errMsg = e instanceof Error ? e.message : '上传失败'
|
||||||
|
setErrMsg(errMsg)
|
||||||
|
setStatus('error')
|
||||||
|
} finally {
|
||||||
|
setUploading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTeam = async (name: string) => {
|
||||||
|
if (!confirm(`确定要删除团队 "${name}" 吗?`)) return
|
||||||
|
await fetch(`${API}/teams/${name}`, { method: 'DELETE' })
|
||||||
|
fetchTeams()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelected = (teamName: string) => {
|
||||||
|
if (selectedTeam) {
|
||||||
|
return selectedTeam.name === teamName
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 overflow-y-auto bg-[var(--bg-primary)]">
|
||||||
|
<div className="max-w-4xl mx-auto p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<ShoppingCart className="w-7 h-7 text-[var(--accent)]" />
|
||||||
|
<h2 className="text-2xl font-bold">团队市场</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex border-b border-[var(--border)] mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setTab('featured')
|
||||||
|
setSelectedTeam(null)
|
||||||
|
}}
|
||||||
|
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
tab === 'featured'
|
||||||
|
? 'border-[var(--accent)] text-[var(--accent)]'
|
||||||
|
: 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4 inline-block mr-2" />
|
||||||
|
精选团队
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setTab('install')}
|
||||||
|
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
tab === 'install'
|
||||||
|
? 'border-[var(--accent)] text-[var(--accent)]'
|
||||||
|
: 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Globe className="w-4 h-4 inline-block mr-2" />
|
||||||
|
雇佣团队
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setTab('teams')
|
||||||
|
setSelectedTeam(null)
|
||||||
|
fetchTeams()
|
||||||
|
}}
|
||||||
|
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
tab === 'teams'
|
||||||
|
? 'border-[var(--accent)] text-[var(--accent)]'
|
||||||
|
: 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Package className="w-4 h-4 inline-block mr-2" />
|
||||||
|
我的团队 ({teams.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedTeam && !isSelected(selectedTeam.name) && (
|
||||||
|
<TeamDetail
|
||||||
|
team={selectedTeam}
|
||||||
|
installed={teams.some(t => t.name === selectedTeam.name)}
|
||||||
|
onBack={() => setSelectedTeam(null)}
|
||||||
|
onInstalled={() => {
|
||||||
|
setSelectedTeam(null)
|
||||||
|
fetchTeams()
|
||||||
|
}}
|
||||||
|
onUninstalled={() => {
|
||||||
|
setSelectedTeam(null)
|
||||||
|
deleteTeam(selectedTeam.name)
|
||||||
|
fetchTeams()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedTeam ? null : (
|
||||||
|
<>
|
||||||
|
{/* Featured teams */}
|
||||||
|
{tab === 'featured' && (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-[var(--text-muted)] mb-4">
|
||||||
|
官方精选团队,快速开始使用
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{PRESET_TEAMS.map(team => {
|
||||||
|
const isInstalled = teams.some(t => t.name === team.name)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={team.name}
|
||||||
|
className={`bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 hover:border-[var(--accent)] transition-all cursor-pointer ${
|
||||||
|
isInstalled ? 'ring-2 ring-[var(--accent)] ring-offset-2' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedTeam(team)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold mb-1">{team.name}</h3>
|
||||||
|
<p className="text-xs text-[var(--text-muted)]">{team.description}</p>
|
||||||
|
</div>
|
||||||
|
{isInstalled ? (
|
||||||
|
<div className="w-8 h-8 bg-[var(--color-success)] rounded-full flex items-center justify-center text-white">
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-[var(--accent)]">
|
||||||
|
<Globe className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-xs gap-2 text-[var(--text-secondary)] mb-3">
|
||||||
|
<Star className="w-3.5 h-3.5 text-yellow-500 fill-current" />
|
||||||
|
<span>专业级</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-xs text-[var(--text-secondary)] mb-3">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Users className="w-3.5 h-3.5" />
|
||||||
|
{team.agents?.length || 0} 成员
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Package className="w-3.5 h-3.5" />
|
||||||
|
{team.skills?.length || 0} Skills
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{team.repo_url && (
|
||||||
|
<a
|
||||||
|
href={team.repo_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-[var(--accent)] hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-3.5 h-3.5" />
|
||||||
|
查看详情
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Install tab */}
|
||||||
|
{tab === 'install' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Install from git */}
|
||||||
|
<div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Globe className="w-5 h-5 text-[var(--accent)]" />
|
||||||
|
<h3 className="font-semibold text-lg">从 Git 安装</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-[var(--text-muted)] mb-4">
|
||||||
|
输入任意 Git 仓库地址(GitHub、Gitee 等),团队包含 agents、skills 和知识库
|
||||||
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Github className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)]" />
|
||||||
<input
|
<input
|
||||||
className="flex-1 bg-gray-700 rounded px-3 py-2 text-sm outline-none"
|
className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg pl-10 pr-3 py-2.5 text-sm outline-none focus:border-[var(--accent)] transition-colors"
|
||||||
placeholder="username/repo 或完整 URL"
|
placeholder="git@github.com:username/repo.git 或 https://..."
|
||||||
value={repo}
|
value={repo}
|
||||||
onChange={e => setRepo(e.target.value)}
|
onChange={e => setRepo(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && install()}
|
onKeyDown={e => e.key === 'Enter' && install()}
|
||||||
/>
|
/>
|
||||||
<button onClick={install} disabled={status === 'loading'}
|
</div>
|
||||||
className="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 px-4 py-2 rounded text-sm">
|
<button
|
||||||
{status === 'loading' ? '安装中...' : '雇佣'}
|
onClick={install}
|
||||||
|
disabled={status === 'loading' || !repo.trim()}
|
||||||
|
className="bg-[var(--accent)] hover:bg-[var(--accent-hover)] disabled:opacity-50 px-5 py-2.5 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
{status === 'loading' ? (
|
||||||
|
<>
|
||||||
|
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
安装中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
安装
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{status === 'done' && <p className="text-green-400 text-xs mt-2">✓ 安装成功</p>}
|
{status === 'done' && (
|
||||||
{status === 'error' && <p className="text-red-400 text-xs mt-2">✗ {errMsg}</p>}
|
<p className="text-[var(--color-success)] text-sm mt-3 flex items-center gap-1.5">
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
安装成功!可在"我的团队"中查看
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{status === 'error' && (
|
||||||
|
<p className="text-[var(--color-error)] text-sm mt-3 flex items-center gap-1.5">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
{errMsg}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-800 rounded-lg p-6 max-w-lg">
|
{/* Upload ZIP */}
|
||||||
<h3 className="font-semibold mb-3">发布团队</h3>
|
<div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-6">
|
||||||
<div className="text-sm text-gray-300 space-y-2">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<p>1. 将你的 <code className="bg-gray-700 px-1 rounded">agents/</code> 和 <code className="bg-gray-700 px-1 rounded">skills/</code> 目录推送到 GitHub</p>
|
<Upload className="w-5 h-5 text-[var(--accent)]" />
|
||||||
<p>2. 在 repo 根目录添加 <code className="bg-gray-700 px-1 rounded">team.md</code> 描述团队</p>
|
<h3 className="font-semibold text-lg">上传 ZIP 安装</h3>
|
||||||
<p>3. 给 repo 添加 topic: <code className="bg-gray-700 px-1 rounded">agent-team</code></p>
|
|
||||||
<p className="text-gray-400 text-xs mt-3">社区可通过 GitHub topic 发现你的团队</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-[var(--text-muted)] mb-4">
|
||||||
|
上传团队压缩包,将自动解压并安装到本地
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label className="w-full">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".zip"
|
||||||
|
onChange={handleUpload}
|
||||||
|
disabled={uploading}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div className={`w-full border-2 border-dashed border-[var(--border)] rounded-lg px-4 py-8 text-center cursor-pointer hover:border-[var(--accent)] transition-colors ${uploading ? 'opacity-50' : ''}`}>
|
||||||
|
{uploading ? (
|
||||||
|
<div>
|
||||||
|
<span className="w-8 h-8 border-4 border-[var(--accent)] border-t-transparent rounded-full animate-spin block mx-auto" />
|
||||||
|
<p className="text-sm text-[var(--text-muted)] mt-2">上传中...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Upload className="w-8 h-8 mx-auto mb-2 text-[var(--text-muted)]" />
|
||||||
|
<p className="text-sm text-[var(--text-muted)]">点击或拖拽 ZIP 文件到这里</p>
|
||||||
|
<p className="text-xs text-[var(--text-muted)] mt-1">支持 .zip 格式</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Teams tab */}
|
||||||
|
{tab === 'teams' && (
|
||||||
|
<div>
|
||||||
|
{teams.length === 0 ? (
|
||||||
|
<div className="text-center py-16">
|
||||||
|
<Package className="w-16 h-16 mx-auto mb-4 text-[var(--text-muted)] opacity-30" />
|
||||||
|
<p className="text-[var(--text-muted)] mb-2">暂无已安装的团队</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setTab('featured')}
|
||||||
|
className="text-[var(--accent)] text-sm hover:underline"
|
||||||
|
>
|
||||||
|
去精选团队中安装
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{teams.map(team => (
|
||||||
|
<div
|
||||||
|
key={team.name}
|
||||||
|
onClick={() => setSelectedTeam(team)}
|
||||||
|
className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 cursor-pointer hover:border-[var(--accent)] transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<h3 className="font-semibold">{team.name}</h3>
|
||||||
|
<ArrowRight className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-[var(--text-muted)] mb-3 line-clamp-2">
|
||||||
|
{team.description || '暂无描述'}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3 text-xs text-[var(--text-secondary)]">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Users className="w-3.5 h-3.5" />
|
||||||
|
{team.agents?.length || 0} agents
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Package className="w-3.5 h-3.5" />
|
||||||
|
{team.skills?.length || 0} skills
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
366
web/src/components/Onboarding.tsx
Normal file
366
web/src/components/Onboarding.tsx
Normal file
@ -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<string, string[]> = {
|
||||||
|
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 (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl w-full max-w-2xl max-h-[90vh] flex flex-col shadow-2xl">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border)]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Hash className="w-5 h-5 text-[var(--accent)]" />
|
||||||
|
<span className="font-semibold">Agent Team</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSkip}
|
||||||
|
className="text-[var(--text-muted)] hover:text-[var(--text-primary)] text-sm"
|
||||||
|
>
|
||||||
|
跳过
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress */}
|
||||||
|
<div className="px-6 py-4 border-b border-[var(--border)]">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
{STEPS.map((s, i) => (
|
||||||
|
<div key={s.id} className="flex items-center">
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors
|
||||||
|
${step === s.id
|
||||||
|
? 'bg-[var(--accent)] text-white'
|
||||||
|
: step > s.id
|
||||||
|
? 'bg-[var(--color-success)] text-white'
|
||||||
|
: 'bg-[var(--bg-tertiary)] text-[var(--text-muted)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{step > s.id ? <Check className="w-4 h-4" /> : s.id}
|
||||||
|
</div>
|
||||||
|
{i < STEPS.length - 1 && (
|
||||||
|
<div className={`w-12 h-0.5 mx-1 ${step > s.id ? 'bg-[var(--color-success)]' : 'bg-[var(--border)]'}`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center gap-4 mt-2">
|
||||||
|
{STEPS.map(s => (
|
||||||
|
<span
|
||||||
|
key={s.id}
|
||||||
|
className={`text-xs ${step === s.id ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)]'}`}
|
||||||
|
>
|
||||||
|
{s.title}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
{/* Step 1: Welcome */}
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-[var(--accent)] rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Sparkles className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold mb-2">欢迎使用 Agent Team</h2>
|
||||||
|
<p className="text-[var(--text-muted)] text-sm">
|
||||||
|
这是一个本地部署的多 Agent 协作平台,通过类似 Discord 的群聊界面与你的 AI 团队协作完成复杂任务。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[var(--bg-tertiary)] rounded-lg p-4 space-y-2">
|
||||||
|
<p className="text-sm font-medium">📋 你将体验到:</p>
|
||||||
|
<ul className="text-sm text-[var(--text-secondary)] space-y-1">
|
||||||
|
<li>• 创建和管理 AI Agent 团队</li>
|
||||||
|
<li>• 通过群聊与 Agent 协作</li>
|
||||||
|
<li>• Agent 自动沉淀经验,持续成长</li>
|
||||||
|
<li>• 支持 DeepSeek、Kimi、Ollama、OpenAI</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: About You */}
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-14 h-14 bg-[var(--accent)] rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||||
|
<User className="w-7 h-7 text-white" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-bold">介绍一下你自己</h2>
|
||||||
|
<p className="text-[var(--text-muted)] text-sm">
|
||||||
|
用 Markdown 写下你的简介,Agent 会根据这些信息更好地帮助你
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">名称</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={e => 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)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">一句话简介</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={description}
|
||||||
|
onChange={e => 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)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1.5 block">详细简介(支持 Markdown)</label>
|
||||||
|
<div className="border border-[var(--border)] rounded-lg overflow-hidden bg-[var(--bg-tertiary)]">
|
||||||
|
<MDEditor
|
||||||
|
value={profile || `# 我的简介
|
||||||
|
|
||||||
|
## 我是谁
|
||||||
|
我是[你的名字],[职业/背景]。
|
||||||
|
|
||||||
|
## 工作风格
|
||||||
|
- 喜欢[沟通方式]
|
||||||
|
- 重视[重视的点]
|
||||||
|
- 不喜欢[不喜欢的]
|
||||||
|
|
||||||
|
## 期望的回复方式
|
||||||
|
- 用 bullet point 列出要点
|
||||||
|
- 重要的决定给出 pros 和 cons
|
||||||
|
- 提供数据支持`}
|
||||||
|
onChange={v => 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',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: API Config */}
|
||||||
|
{step === 3 && (
|
||||||
|
<div className="space-y-4 max-w-md mx-auto">
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<div className="w-12 h-12 bg-[var(--accent)] rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||||
|
<Key className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-bold">配置默认 API</h2>
|
||||||
|
<p className="text-[var(--text-muted)] text-sm">
|
||||||
|
选择默认的模型提供商,所有 Agent 将使用此配置(可在 Agent 级别覆盖)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">提供商</label>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{['deepseek', 'kimi', 'ollama', 'openai'].map(p => (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
onClick={() => {
|
||||||
|
setProvider(p)
|
||||||
|
setModel(providerModels[p][0])
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
py-2 px-3 rounded-lg text-sm font-medium transition-colors border
|
||||||
|
${provider === p
|
||||||
|
? 'bg-[var(--accent)] border-[var(--accent)] text-white'
|
||||||
|
: 'bg-[var(--bg-tertiary)] border-[var(--border)] hover:border-[var(--accent)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{p === 'deepseek' ? 'DeepSeek' : p === 'kimi' ? 'Kimi' : p === 'ollama' ? 'Ollama' : 'OpenAI'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">模型</label>
|
||||||
|
<select
|
||||||
|
value={model}
|
||||||
|
onChange={e => setModel(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)]"
|
||||||
|
>
|
||||||
|
{providerModels[provider].map(m => (
|
||||||
|
<option key={m} value={m}>{m}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">
|
||||||
|
API Key
|
||||||
|
<span className="text-[var(--text-muted)] font-normal ml-2">
|
||||||
|
(将保存到 {provider.toUpperCase()}_API_KEY 环境变量)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={e => 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)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[var(--bg-tertiary)] rounded-lg p-3 text-xs text-[var(--text-muted)]">
|
||||||
|
<p>💡 提示:API Key 将保存在浏览器本地存储中,每次启动后端时会自动读取。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-t border-[var(--border)]">
|
||||||
|
<button
|
||||||
|
onClick={handlePrev}
|
||||||
|
disabled={step === 1}
|
||||||
|
className={`
|
||||||
|
flex items-center gap-1 px-4 py-2 rounded-lg text-sm transition-colors
|
||||||
|
${step === 1
|
||||||
|
? 'text-[var(--text-muted)] cursor-not-allowed'
|
||||||
|
: 'hover:bg-[var(--bg-hover)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
上一步
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{step < 3 ? (
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
className="flex items-center gap-1 bg-[var(--accent)] hover:bg-[var(--accent-hover)] px-6 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
下一步
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={handleComplete}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex items-center gap-2 bg-[var(--color-success)] hover:opacity-90 px-6 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{saving ? '保存中...' : '完成'}
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,104 +1,165 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Plus, Hash, Users, Crown, ShoppingCart } from 'lucide-react'
|
||||||
import { useStore } from '../store'
|
import { useStore } from '../store'
|
||||||
|
import type { Room } from '../types'
|
||||||
|
|
||||||
const API = '/api'
|
const API = '/api'
|
||||||
|
|
||||||
export function RoomSidebar() {
|
export function RoomSidebar() {
|
||||||
const { rooms, activeRoomId, setActiveRoom, fetchRooms, agents, fetchAgents } = useStore()
|
const { rooms, activeRoomId, setActiveRoom, fetchRooms, setPage } = useStore()
|
||||||
const [creating, setCreating] = useState(false)
|
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() }, [])
|
useEffect(() => { fetchRooms() }, [])
|
||||||
|
|
||||||
const toggleMember = (name: string) => {
|
|
||||||
setForm(f => ({
|
|
||||||
...f,
|
|
||||||
members: f.members.includes(name) ? f.members.filter(m => m !== name) : [...f.members, name]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
if (!form.name || !form.master) return
|
if (!form.name) return
|
||||||
await fetch(`${API}/rooms`, {
|
await fetch(`${API}/rooms`, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
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)
|
setCreating(false)
|
||||||
setForm({ name: '', type: 'dept', master: '', members: [] })
|
setForm({ name: '', type: 'dept' })
|
||||||
fetchRooms()
|
fetchRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColor = (status: string) => {
|
const statusColor = (status: string) => {
|
||||||
if (status === 'thinking') return 'bg-yellow-400'
|
if (status === 'thinking') return 'bg-[var(--color-warning)]'
|
||||||
if (status === 'working') return 'bg-green-400 animate-pulse'
|
if (status === 'working') return 'bg-[var(--color-success)] animate-pulse'
|
||||||
return 'bg-gray-500'
|
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 (
|
return (
|
||||||
<div className="w-56 bg-gray-900 border-r border-gray-700 flex flex-col">
|
<div className="w-60 bg-[var(--channel-list-bg)] flex flex-col border-r border-[var(--border)]">
|
||||||
<div className="p-3 border-b border-gray-700 flex items-center justify-between">
|
{/* Header */}
|
||||||
<span className="font-bold text-sm">Agent Team</span>
|
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center justify-between shadow-sm">
|
||||||
<button onClick={() => setCreating(!creating)} className="text-gray-400 hover:text-white text-lg leading-none">+</button>
|
<h1 className="font-semibold text-sm truncate">我的团队</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setCreating(!creating)}
|
||||||
|
className="w-6 h-6 rounded flex items-center justify-center text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Create team form */}
|
||||||
{creating && (
|
{creating && (
|
||||||
<div className="p-3 border-b border-gray-700 space-y-2 text-xs">
|
<div className="p-3 border-b border-[var(--border)] space-y-2 text-sm bg-[var(--bg-tertiary)]">
|
||||||
<input className="w-full bg-gray-700 rounded px-2 py-1" placeholder="群名称" value={form.name}
|
<input
|
||||||
onChange={e => setForm(f => ({ ...f, name: e.target.value }))} />
|
className="w-full bg-[var(--bg-secondary)] border border-[var(--border)] rounded px-2 py-1.5 text-sm outline-none focus:border-[var(--accent)] transition-colors"
|
||||||
<select className="w-full bg-gray-700 rounded px-2 py-1" value={form.type}
|
placeholder="团队名称"
|
||||||
onChange={e => setForm(f => ({ ...f, type: e.target.value }))}>
|
value={form.name}
|
||||||
<option value="dept">部门群</option>
|
onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
|
||||||
<option value="leader">Leader 群</option>
|
/>
|
||||||
|
<select
|
||||||
|
className="w-full bg-[var(--bg-secondary)] border border-[var(--border)] rounded px-2 py-1.5 text-sm outline-none focus:border-[var(--accent)] transition-colors"
|
||||||
|
value={form.type}
|
||||||
|
onChange={e => setForm(f => ({ ...f, type: e.target.value as 'dept' | 'leader' }))}
|
||||||
|
>
|
||||||
|
<option value="dept">部门团队</option>
|
||||||
|
<option value="leader">Leader 团队</option>
|
||||||
</select>
|
</select>
|
||||||
<select className="w-full bg-gray-700 rounded px-2 py-1" value={form.master}
|
|
||||||
onChange={e => setForm(f => ({ ...f, master: e.target.value }))}>
|
|
||||||
<option value="">选择 master agent</option>
|
|
||||||
{agents.map(a => <option key={a.name} value={a.name}>{a.name}</option>)}
|
|
||||||
</select>
|
|
||||||
{form.type === 'dept' && availableMembers.length > 0 && (
|
|
||||||
<div className="bg-gray-700 rounded p-2 space-y-1 max-h-28 overflow-y-auto">
|
|
||||||
<div className="text-gray-400 mb-1">选择成员:</div>
|
|
||||||
{availableMembers.map(a => (
|
|
||||||
<label key={a.name} className="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input type="checkbox" checked={form.members.includes(a.name)} onChange={() => toggleMember(a.name)} />
|
|
||||||
<span>{a.name}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<button onClick={create} className="flex-1 bg-indigo-600 hover:bg-indigo-500 rounded py-1">创建</button>
|
<button
|
||||||
<button onClick={() => setCreating(false)} className="bg-gray-600 hover:bg-gray-500 rounded px-2 py-1">✕</button>
|
onClick={create}
|
||||||
|
className="flex-1 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white rounded py-1.5 text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCreating(false)}
|
||||||
|
className="bg-[var(--bg-hover)] hover:bg-[var(--bg-active)] rounded px-2 py-1.5 text-sm transition-colors"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto py-2">
|
{/* Room list */}
|
||||||
<div className="px-3 py-1 text-xs text-gray-500 uppercase">部门群</div>
|
<div className="flex-1 overflow-y-auto py-2 scrollbar-thin">
|
||||||
{rooms.filter(r => r.type === 'dept').map(r => (
|
{/* Department teams */}
|
||||||
<RoomItem key={r.id} room={r} active={r.id === activeRoomId} onClick={() => setActiveRoom(r.id)} statusColor={statusColor(r.status)} />
|
{deptRooms.length > 0 && (
|
||||||
|
<section className="mb-3">
|
||||||
|
<div className="px-4 py-1 flex items-center gap-1 text-xs font-semibold text-[var(--text-muted)] uppercase hover:text-[var(--text-secondary)] cursor-pointer">
|
||||||
|
<Users className="w-3 h-3" />
|
||||||
|
<span>部门团队</span>
|
||||||
|
</div>
|
||||||
|
{deptRooms.map(r => (
|
||||||
|
<RoomItem
|
||||||
|
key={r.id}
|
||||||
|
room={r}
|
||||||
|
active={r.id === activeRoomId}
|
||||||
|
onClick={() => setActiveRoom(r.id)}
|
||||||
|
statusColor={statusColor(r.status)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="px-3 py-1 text-xs text-gray-500 uppercase mt-2">Leader 群</div>
|
</section>
|
||||||
{rooms.filter(r => r.type === 'leader').map(r => (
|
)}
|
||||||
<RoomItem key={r.id} room={r} active={r.id === activeRoomId} onClick={() => setActiveRoom(r.id)} statusColor={statusColor(r.status)} />
|
|
||||||
|
{/* Leader teams */}
|
||||||
|
{leaderRooms.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<div className="px-4 py-1 flex items-center gap-1 text-xs font-semibold text-[var(--text-muted)] uppercase hover:text-[var(--text-secondary)] cursor-pointer">
|
||||||
|
<Crown className="w-3 h-3" />
|
||||||
|
<span>Leader 团队</span>
|
||||||
|
</div>
|
||||||
|
{leaderRooms.map(r => (
|
||||||
|
<RoomItem
|
||||||
|
key={r.id}
|
||||||
|
room={r}
|
||||||
|
active={r.id === activeRoomId}
|
||||||
|
onClick={() => setActiveRoom(r.id)}
|
||||||
|
statusColor={statusColor(r.status)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
{rooms.length === 0 && (
|
||||||
|
<div className="px-4 py-8 text-center text-sm text-[var(--text-muted)]">
|
||||||
|
<Hash className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>暂无团队</p>
|
||||||
|
<p className="text-xs mt-1">点击 + 创建第一个团队</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom hint - go to market */}
|
||||||
|
<div className="p-3 border-t border-[var(--border)]">
|
||||||
|
<button
|
||||||
|
onClick={() => setPage('market')}
|
||||||
|
className="w-full flex items-center justify-center gap-2 py-2 bg-[var(--accent-muted)] text-[var(--accent)] rounded-lg text-sm font-medium hover:bg-[var(--accent)] hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<ShoppingCart className="w-4 h-4" />
|
||||||
|
去雇佣团队
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RoomItem({ room, active, onClick, statusColor }: any) {
|
function RoomItem({ room, active, onClick, statusColor }: { room: Room; active: boolean; onClick: () => void; statusColor: string }) {
|
||||||
return (
|
return (
|
||||||
<div onClick={onClick} className={`flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-gray-700 ${active ? 'bg-gray-700' : ''}`}>
|
<div
|
||||||
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${statusColor}`} />
|
onClick={onClick}
|
||||||
<div className="flex-1 min-w-0">
|
className={`
|
||||||
<div className="text-sm truncate">{room.name}</div>
|
mx-2 px-2 py-1.5 rounded cursor-pointer group flex items-center gap-2 transition-all duration-150
|
||||||
|
${active
|
||||||
|
? 'bg-[var(--accent-muted)] text-[var(--text-primary)]'
|
||||||
|
: 'hover:bg-[var(--bg-hover)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Hash className={`w-4 h-4 flex-shrink-0 ${active ? 'text-[var(--accent)]' : 'text-[var(--text-muted)]'}`} />
|
||||||
|
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||||
|
<span className="truncate text-sm font-medium">{room.name}</span>
|
||||||
{room.status !== 'pending' && (
|
{room.status !== 'pending' && (
|
||||||
<div className="text-xs text-gray-400 truncate">
|
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${statusColor}`} />
|
||||||
{room.status === 'working' && room.activeAgent ? `${room.activeAgent} ${room.action || ''}` : room.status}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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<string | null>(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 (
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
|
||||||
{/* Skill list */}
|
|
||||||
<div className="w-48 border-r border-gray-700 flex flex-col">
|
|
||||||
<div className="p-3 border-b border-gray-700">
|
|
||||||
{creating ? (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<input className="flex-1 bg-gray-700 rounded px-2 py-1 text-xs" placeholder="skill 名" value={newName}
|
|
||||||
onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && create()} autoFocus />
|
|
||||||
<button onClick={create} className="bg-indigo-600 hover:bg-indigo-500 px-2 py-1 rounded text-xs">✓</button>
|
|
||||||
<button onClick={() => setCreating(false)} className="bg-gray-600 hover:bg-gray-500 px-2 py-1 rounded text-xs">✕</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button onClick={() => setCreating(true)} className="w-full bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs text-left">+ 新建 Skill</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
{(skills || []).map(s => (
|
|
||||||
<div key={s.name} onClick={() => setSelected(s.name)}
|
|
||||||
className={`px-3 py-2 cursor-pointer hover:bg-gray-700 ${selected === s.name ? 'bg-gray-700' : ''}`}>
|
|
||||||
<div className="text-sm truncate">{s.name}</div>
|
|
||||||
<div className="text-xs text-gray-400 truncate">{s.description}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Skill detail / editor */}
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
|
||||||
{selected ? (
|
|
||||||
<>
|
|
||||||
<div className="px-4 py-2 border-b border-gray-700 flex items-center gap-2">
|
|
||||||
<span className="font-semibold text-sm">{selected}</span>
|
|
||||||
<span className="text-xs text-gray-400 ml-1">SKILL.md</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Editor height="100%" defaultLanguage="markdown" theme="vs-dark" value={body}
|
|
||||||
onChange={v => setBody(v || '')}
|
|
||||||
options={{ minimap: { enabled: false }, wordWrap: 'on', fontSize: 13, readOnly: true }} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex items-center justify-center text-gray-400 text-sm">选择一个 skill 查看详情</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
417
web/src/components/TeamDetail.tsx
Normal file
417
web/src/components/TeamDetail.tsx
Normal file
@ -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<string | null>(null)
|
||||||
|
const [agentFile, setAgentFile] = useState<string>('')
|
||||||
|
const [agentContent, setAgentContent] = useState('')
|
||||||
|
const [knowledge, setKnowledge] = useState<string[]>([])
|
||||||
|
const [selectedKnowledge, setSelectedKnowledge] = useState<string | null>(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 (
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden bg-[var(--bg-primary)] fixed inset-0 z-50 bg-black/50">
|
||||||
|
<div className="flex flex-col max-w-4xl w-full mx-auto h-full bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl m-4 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border)]">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center gap-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] text-sm"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
返回
|
||||||
|
</button>
|
||||||
|
<span className="text-[var(--text-muted)]">/</span>
|
||||||
|
<h2 className="font-semibold">{team.name}</h2>
|
||||||
|
</div>
|
||||||
|
{installed ? (
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
className="flex items-center gap-1.5 text-[var(--text-muted)] hover:text-[var(--color-error)] text-sm bg-red-900/20 hover:bg-red-900/30 px-3 py-1.5 rounded-lg"
|
||||||
|
>
|
||||||
|
{deleting ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
卸载团队
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={handleInstall}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex items-center gap-1.5 text-white text-sm bg-blue-500 hover:bg-blue-600 px-3 py-1.5 rounded-lg"
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
安装团队
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Team info */}
|
||||||
|
<div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4">
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-2">{team.description}</p>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-[var(--text-muted)]">
|
||||||
|
{team.author && <span>作者: {team.author}</span>}
|
||||||
|
{team.repo_url && (
|
||||||
|
<a
|
||||||
|
href={team.repo_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[var(--accent)] hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
项目页面
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agents */}
|
||||||
|
<section>
|
||||||
|
<h3 className="flex items-center gap-2 text-sm font-semibold text-[var(--text-muted)] uppercase mb-3">
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
成员 ({team.agents?.length || 0})
|
||||||
|
</h3>
|
||||||
|
{team.agents?.length === 0 ? (
|
||||||
|
<p className="text-sm text-[var(--text-muted)]">暂无成员</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{team.agents?.map(agent => (
|
||||||
|
<div key={agent} className={`bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg overflow-hidden ${installed ? '' : 'opacity-50'}`}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (!installed) return
|
||||||
|
if (expandedAgent === agent) {
|
||||||
|
setExpandedAgent(null)
|
||||||
|
} else {
|
||||||
|
setExpandedAgent(agent)
|
||||||
|
loadAgentFile(agent, 'SOUL.md')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center justify-between px-4 py-3 hover:bg-[var(--bg-hover)] transition-colors ${installed ? '' : 'cursor-not-allowed'}`}
|
||||||
|
>
|
||||||
|
<span className="font-medium">{agent}</span>
|
||||||
|
{installed && expandedAgent === agent ? (
|
||||||
|
<ChevronDown className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
|
) : installed ? (
|
||||||
|
<ChevronRight className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
|
) : undefined}
|
||||||
|
</button>
|
||||||
|
{installed && expandedAgent === agent && (
|
||||||
|
<div className="border-t border-[var(--border)]">
|
||||||
|
{/* File tabs */}
|
||||||
|
<div className="flex items-center gap-1 px-4 py-2 bg-[var(--bg-tertiary)]">
|
||||||
|
{['SOUL.md', 'AGENT.md'].map(f => (
|
||||||
|
<button
|
||||||
|
key={f}
|
||||||
|
onClick={() => loadAgentFile(agent, f)}
|
||||||
|
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||||
|
agentFile === f
|
||||||
|
? 'bg-[var(--accent)] text-white'
|
||||||
|
: 'hover:bg-[var(--bg-hover)] text-[var(--text-secondary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Editor */}
|
||||||
|
<div className="h-64 border-t border-[var(--border)]">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin text-[var(--accent)]" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<MDEditor
|
||||||
|
value={agentContent}
|
||||||
|
onChange={(v: string | undefined) => setAgentContent(v || '')}
|
||||||
|
height={250}
|
||||||
|
preview="edit"
|
||||||
|
visibleDragbar={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Save button */}
|
||||||
|
<div className="flex justify-end px-4 py-2 bg-[var(--bg-tertiary)]">
|
||||||
|
<button
|
||||||
|
onClick={saveAgentFile}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex items-center gap-1.5 bg-[var(--accent)] hover:bg-[var(--accent-hover)] px-3 py-1.5 rounded text-xs font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{saving ? <Loader2 className="w-3 h-3 animate-spin" /> : <Save className="w-3 h-3" />}
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Skills */}
|
||||||
|
<section>
|
||||||
|
<h3 className="flex items-center gap-2 text-sm font-semibold text-[var(--text-muted)] uppercase mb-3">
|
||||||
|
<Package className="w-4 h-4" />
|
||||||
|
Skills ({team.skills?.length || 0})
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{team.skills?.length === 0 && (
|
||||||
|
<p className="text-sm text-[var(--text-muted)]">暂无 skills</p>
|
||||||
|
)}
|
||||||
|
{team.skills?.map(skill => (
|
||||||
|
<span
|
||||||
|
key={skill}
|
||||||
|
className={`px-3 py-1.5 border rounded-lg text-sm ${installed ? 'bg-[var(--bg-secondary)] border-[var(--border)]' : 'border-[var(--border)] opacity-50'}`}
|
||||||
|
>
|
||||||
|
{skill}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Knowledge */}
|
||||||
|
{installed && (
|
||||||
|
<section>
|
||||||
|
<h3 className="flex items-center gap-2 text-sm font-semibold text-[var(--text-muted)] uppercase mb-3">
|
||||||
|
<BookOpen className="w-4 h-4" />
|
||||||
|
知识库
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{/* File list */}
|
||||||
|
<div className="w-48 flex-shrink-0 space-y-1">
|
||||||
|
{knowledge.map(f => (
|
||||||
|
<button
|
||||||
|
key={f}
|
||||||
|
onClick={() => loadKnowledgeFile(f)}
|
||||||
|
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||||
|
selectedKnowledge === f
|
||||||
|
? 'bg-[var(--accent-muted)] text-[var(--text-primary)]'
|
||||||
|
: 'hover:bg-[var(--bg-hover)] text-[var(--text-secondary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{f}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{/* Add new */}
|
||||||
|
<div className="flex items-center gap-1 mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newKnowledgeName}
|
||||||
|
onChange={e => 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()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={createKnowledgeFile}
|
||||||
|
className="p-1.5 bg-[var(--accent)] rounded-lg text-white hover:bg-[var(--accent-hover)]"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Editor */}
|
||||||
|
<div className="flex-1 flex flex-col border border-[var(--border)] rounded-lg overflow-hidden">
|
||||||
|
{selectedKnowledge ? (
|
||||||
|
<>
|
||||||
|
<div className="h-80">
|
||||||
|
<MDEditor
|
||||||
|
value={knowledgeContent}
|
||||||
|
onChange={(v: string | undefined) => setKnowledgeContent(v || '')}
|
||||||
|
height={300}
|
||||||
|
preview="edit"
|
||||||
|
visibleDragbar={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end px-4 py-2 bg-[var(--bg-tertiary)]">
|
||||||
|
<button
|
||||||
|
onClick={saveKnowledgeFile}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex items-center gap-1.5 bg-[var(--accent)] hover:bg-[var(--accent-hover)] px-3 py-1.5 rounded text-xs font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{saving ? <Loader2 className="w-3 h-3 animate-spin" /> : <Save className="w-3 h-3" />}
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-80 text-[var(--text-muted)] text-sm">
|
||||||
|
选择或创建知识文件
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!installed && (
|
||||||
|
<div className="bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-xl p-4">
|
||||||
|
<p className="text-sm text-[var(--text-muted)] text-center">
|
||||||
|
此团队尚未安装。点击上方"安装"按钮来安装它。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
web/src/components/ThemeToggle.tsx
Normal file
20
web/src/components/ThemeToggle.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Sun, Moon } from 'lucide-react'
|
||||||
|
import { useStore } from '../store'
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { theme, toggleTheme } = useStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="w-9 h-9 rounded-lg flex items-center justify-center transition-colors duration-200 hover:bg-[var(--bg-hover)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||||
|
title={theme === 'dark' ? '切换到浅色模式' : '切换到深色模式'}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? (
|
||||||
|
<Moon className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<Sun className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
258
web/src/components/UserSettings.tsx
Normal file
258
web/src/components/UserSettings.tsx
Normal file
@ -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<string, string[]> = {
|
||||||
|
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 (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-[var(--accent)]" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="w-56 border-r border-[var(--border)] bg-[var(--channel-list-bg)] flex flex-col">
|
||||||
|
<div className="p-4 border-b border-[var(--border)]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-[var(--accent)] flex items-center justify-center text-white font-semibold">
|
||||||
|
{user.name?.[0]?.toUpperCase() || 'U'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-sm">{user.name || '用户'}</div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)]">{user.provider}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav className="flex-1 py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setTab('profile')}
|
||||||
|
className={`
|
||||||
|
w-full flex items-center gap-2 px-4 py-2 text-sm transition-colors
|
||||||
|
${tab === 'profile'
|
||||||
|
? 'bg-[var(--accent-muted)] text-[var(--text-primary)]'
|
||||||
|
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
个人简介
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setTab('config')}
|
||||||
|
className={`
|
||||||
|
w-full flex items-center gap-2 px-4 py-2 text-sm transition-colors
|
||||||
|
${tab === 'config'
|
||||||
|
? 'bg-[var(--accent-muted)] text-[var(--text-primary)]'
|
||||||
|
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Key className="w-4 h-4" />
|
||||||
|
API 配置
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden bg-[var(--bg-primary)]">
|
||||||
|
{tab === 'profile' && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border)] bg-[var(--bg-secondary)]">
|
||||||
|
<h2 className="font-semibold">个人简介</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveProfile}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex items-center gap-2 bg-[var(--accent)] hover:bg-[var(--accent-hover)] px-4 py-1.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto p-4">
|
||||||
|
<MDEditor
|
||||||
|
value={profile}
|
||||||
|
onChange={(v) => 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)',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'config' && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border)] bg-[var(--bg-secondary)]">
|
||||||
|
<h2 className="font-semibold">API 配置</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveConfig}
|
||||||
|
disabled={saving}
|
||||||
|
className="flex items-center gap-2 bg-[var(--accent)] hover:bg-[var(--accent-hover)] px-4 py-1.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
<div className="max-w-lg space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">名称</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={user.name}
|
||||||
|
onChange={e => 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)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">简介</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={user.description}
|
||||||
|
onChange={e => 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)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">提供商</label>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{['deepseek', 'kimi', 'ollama', 'openai'].map(p => (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
onClick={() => setUser({ ...user, provider: p, model: providerModels[p][0] })}
|
||||||
|
className={`
|
||||||
|
py-2 px-3 rounded-lg text-sm font-medium transition-colors border
|
||||||
|
${user.provider === p
|
||||||
|
? 'bg-[var(--accent)] border-[var(--accent)] text-white'
|
||||||
|
: 'bg-[var(--bg-tertiary)] border-[var(--border)] hover:border-[var(--accent)]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{p === 'deepseek' ? 'DeepSeek' : p === 'kimi' ? 'Kimi' : p === 'ollama' ? 'Ollama' : 'OpenAI'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">模型</label>
|
||||||
|
<select
|
||||||
|
value={user.model}
|
||||||
|
onChange={e => setUser({ ...user, model: 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)]"
|
||||||
|
>
|
||||||
|
{providerModels[user.provider].map(m => (
|
||||||
|
<option key={m} value={m}>{m}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">环境变量名</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={user.api_key_env}
|
||||||
|
onChange={e => 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)]"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-[var(--text-muted)] mt-1">
|
||||||
|
API Key 将从这个环境变量读取
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,3 +1,245 @@
|
|||||||
@import "tailwindcss";
|
@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);
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,12 @@ import { create } from 'zustand'
|
|||||||
import type { Room, Message, AgentInfo, SkillMeta, WsEvent } from './types'
|
import type { Room, Message, AgentInfo, SkillMeta, WsEvent } from './types'
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
theme: 'light' | 'dark'
|
||||||
|
user: {
|
||||||
|
name: string
|
||||||
|
avatar_color: string
|
||||||
|
} | null
|
||||||
|
onboardingCompleted: boolean
|
||||||
rooms: Room[]
|
rooms: Room[]
|
||||||
activeRoomId: string | null
|
activeRoomId: string | null
|
||||||
messages: Record<string, Message[]>
|
messages: Record<string, Message[]>
|
||||||
@ -9,9 +15,13 @@ interface AppState {
|
|||||||
workspace: Record<string, string[]>
|
workspace: Record<string, string[]>
|
||||||
agents: AgentInfo[]
|
agents: AgentInfo[]
|
||||||
skills: SkillMeta[]
|
skills: SkillMeta[]
|
||||||
page: 'chat' | 'agents' | 'skills' | 'market'
|
page: 'chat' | 'agents' | 'skills' | 'market' | 'settings'
|
||||||
ws: Record<string, WebSocket>
|
ws: Record<string, WebSocket>
|
||||||
|
|
||||||
|
setTheme: (theme: 'light' | 'dark') => void
|
||||||
|
toggleTheme: () => void
|
||||||
|
setOnboardingCompleted: (completed: boolean) => void
|
||||||
|
fetchUser: () => Promise<void>
|
||||||
setPage: (p: AppState['page']) => void
|
setPage: (p: AppState['page']) => void
|
||||||
setActiveRoom: (id: string) => void
|
setActiveRoom: (id: string) => void
|
||||||
fetchRooms: () => Promise<void>
|
fetchRooms: () => Promise<void>
|
||||||
@ -23,7 +33,31 @@ interface AppState {
|
|||||||
|
|
||||||
const API = '/api'
|
const API = '/api'
|
||||||
|
|
||||||
export const useStore = create<AppState>((set, get) => ({
|
export const useStore = create<AppState>((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'
|
||||||
|
}
|
||||||
|
return 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
|
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: [],
|
rooms: [],
|
||||||
activeRoomId: null,
|
activeRoomId: null,
|
||||||
messages: {},
|
messages: {},
|
||||||
@ -34,6 +68,38 @@ export const useStore = create<AppState>((set, get) => ({
|
|||||||
page: 'chat',
|
page: 'chat',
|
||||||
ws: {},
|
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 }),
|
setPage: (page) => set({ page }),
|
||||||
setActiveRoom: (id) => {
|
setActiveRoom: (id) => {
|
||||||
set({ activeRoomId: id })
|
set({ activeRoomId: id })
|
||||||
@ -44,7 +110,6 @@ export const useStore = create<AppState>((set, get) => ({
|
|||||||
fetch(`${API}/rooms/${id}/workspace`).then(r => r.json()).then(files => {
|
fetch(`${API}/rooms/${id}/workspace`).then(r => r.json()).then(files => {
|
||||||
set(s => ({ workspace: { ...s.workspace, [id]: files || [] } }))
|
set(s => ({ workspace: { ...s.workspace, [id]: files || [] } }))
|
||||||
})
|
})
|
||||||
// Load message history
|
|
||||||
fetch(`${API}/rooms/${id}/messages`).then(r => r.json()).then(msgs => {
|
fetch(`${API}/rooms/${id}/messages`).then(r => r.json()).then(msgs => {
|
||||||
if (msgs?.length) set(s => ({ messages: { ...s.messages, [id]: msgs } }))
|
if (msgs?.length) set(s => ({ messages: { ...s.messages, [id]: msgs } }))
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
@ -100,9 +165,11 @@ export const useStore = create<AppState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
sendMessage: (roomId, content) => {
|
sendMessage: (roomId, content) => {
|
||||||
const { ws } = get()
|
const { ws, user } = get()
|
||||||
const userMsg = { id: Date.now().toString(), agent: 'user', role: 'user' as const, content }
|
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] } }))
|
set(s => ({ messages: { ...s.messages, [roomId]: [...(s.messages[roomId] || []), userMsg] } }))
|
||||||
ws[roomId]?.send(JSON.stringify({ type: 'user_message', content }))
|
ws[roomId]?.send(JSON.stringify({ type: 'user_message', content, user_name: userName }))
|
||||||
},
|
},
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user