mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: full api server
This commit is contained in:
+46
-3
@@ -1,9 +1,52 @@
|
||||
# Server
|
||||
# ==================================
|
||||
# Database Configuration
|
||||
# ==================================
|
||||
|
||||
# PostgreSQL database connection URL
|
||||
# Format: postgresql://username:password@host:port/database
|
||||
# Example: postgresql://postgres:password@localhost:5432/memohome
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
||||
|
||||
|
||||
# ==================================
|
||||
# API Server Configuration
|
||||
# ==================================
|
||||
API_SERVER_PORT=7002
|
||||
WEB_PORT=7003
|
||||
|
||||
API_BASE_URL=http://localhost:7002
|
||||
WEB_URL=http://localhost:7003
|
||||
|
||||
# Database (PostgreSQL)
|
||||
DATABASE_URL=
|
||||
|
||||
# ==================================
|
||||
# Authentication Configuration
|
||||
# ==================================
|
||||
|
||||
# Root user credentials (super admin)
|
||||
# This user is not stored in the database and has full admin privileges
|
||||
# Use a strong username and password in production
|
||||
ROOT_USER=admin
|
||||
ROOT_USER_PASSWORD=change_this_secure_password
|
||||
|
||||
# JWT secret key for signing tokens
|
||||
# IMPORTANT: Use a long, random string in production (at least 32 characters)
|
||||
# You can generate one with: openssl rand -base64 32
|
||||
JWT_SECRET=your-jwt-secret-key-change-in-production-use-long-random-string
|
||||
|
||||
# JWT token expiration time
|
||||
# Format: number + unit (s=seconds, m=minutes, h=hours, d=days)
|
||||
# Examples: 30m, 2h, 7d, 30d
|
||||
# Default: 7d (7 days)
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
|
||||
# ==================================
|
||||
# Optional: Development Configuration
|
||||
# ==================================
|
||||
|
||||
# Node environment
|
||||
# Options: development, production, test
|
||||
NODE_ENV=development
|
||||
|
||||
# Enable debug logging (optional)
|
||||
# DEBUG=true
|
||||
|
||||
@@ -90,3 +90,4 @@ Thumbs.db
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
.pnpm-store
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
"web:dev": "pnpm --filter @memohome/web dev",
|
||||
"web:build": "pnpm --filter @memohome/web build",
|
||||
"web:start": "pnpm --filter @memohome/web start",
|
||||
"device-api:dev": "pnpm --filter @memohome/device-api dev",
|
||||
"device-api:build": "pnpm --filter @memohome/device-api build",
|
||||
"device-api:start": "pnpm --filter @memohome/device-api start",
|
||||
"db:push": "pnpm --filter @memohome/db push",
|
||||
"db:migrate": "pnpm --filter @memohome/db migrate",
|
||||
"db:generate": "pnpm --filter @memohome/db generate",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"name": "@memohome/agent",
|
||||
"version": "1.0.0",
|
||||
"description": "Agent package for the phonetutor monorepo",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"start": "bun run client/index.ts"
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
# Agent API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
Agent API 提供了一个智能对话代理接口,支持流式响应、记忆管理和工具调用。
|
||||
|
||||
## 端点
|
||||
|
||||
### POST `/agent/stream` 🔒 需要认证
|
||||
|
||||
与 AI Agent 进行流式对话。
|
||||
|
||||
#### 权限要求
|
||||
|
||||
- 需要有效的 Bearer token
|
||||
- 自动使用当前登录用户的身份
|
||||
|
||||
#### 请求
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"message": "你好,请介绍一下你自己",
|
||||
"maxContextLoadTime": 60,
|
||||
"language": "Chinese"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
|
||||
| 字段 | 类型 | 必需 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| message | string | 是 | - | 用户消息内容 |
|
||||
| maxContextLoadTime | number | 否 | 从设置读取或60 | 加载上下文的时间范围(分钟,1-1440) |
|
||||
| language | string | 否 | 从设置读取或"Same as user input" | Agent 回复的首选语言 |
|
||||
|
||||
**注意**: `maxContextLoadTime` 和 `language` 如果在请求中未指定,将从用户设置(Settings)中读取。如果设置中也没有,则使用默认值。
|
||||
|
||||
#### 响应
|
||||
|
||||
返回 Server-Sent Events (SSE) 流式响应。
|
||||
|
||||
**Content-Type:** `text/event-stream`
|
||||
|
||||
**事件格式:**
|
||||
|
||||
每个事件以 `data: ` 开头,后跟 JSON 格式的数据:
|
||||
|
||||
```
|
||||
data: {"type":"text-delta","text":"你"}
|
||||
|
||||
data: {"type":"text-delta","text":"好"}
|
||||
|
||||
data: {"type":"tool-call","toolName":"search_memory","args":{...}}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
**事件类型:**
|
||||
|
||||
1. **text-delta** - 文本增量
|
||||
```json
|
||||
{
|
||||
"type": "text-delta",
|
||||
"text": "文本片段"
|
||||
}
|
||||
```
|
||||
|
||||
2. **tool-call** - 工具调用
|
||||
```json
|
||||
{
|
||||
"type": "tool-call",
|
||||
"toolName": "search_memory",
|
||||
"args": {
|
||||
"query": "搜索内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **[DONE]** - 流结束标记
|
||||
|
||||
4. **error** - 错误事件
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"error": "错误消息"
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**400 Bad Request** - 模型配置未找到
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Model configuration not found. Please configure your models in settings."
|
||||
}
|
||||
```
|
||||
|
||||
**401 Unauthorized** - 未认证
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "No bearer token provided"
|
||||
}
|
||||
```
|
||||
|
||||
**500 Internal Server Error** - 服务器错误
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Failed to process request"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### JavaScript/TypeScript (EventSource)
|
||||
|
||||
```typescript
|
||||
async function streamAgentChat(message: string, token: string) {
|
||||
const response = await fetch('http://localhost:7002/agent/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
maxContextLoadTime: 60,
|
||||
language: 'Chinese',
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error)
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
if (!reader) throw new Error('No reader available')
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value)
|
||||
const lines = chunk.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6)
|
||||
|
||||
if (data === '[DONE]') {
|
||||
console.log('\n✅ Stream completed')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event = JSON.parse(data)
|
||||
|
||||
if (event.type === 'text-delta' && event.text) {
|
||||
process.stdout.write(event.text)
|
||||
} else if (event.type === 'tool-call') {
|
||||
console.log(`\n[Tool: ${event.toolName}]`)
|
||||
} else if (event.type === 'error') {
|
||||
console.error('\n❌ Error:', event.error)
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const token = 'your_jwt_token_here'
|
||||
await streamAgentChat('你好,请介绍一下你自己', token)
|
||||
```
|
||||
|
||||
### curl 示例
|
||||
|
||||
```bash
|
||||
# 1. 登录获取 token
|
||||
TOKEN=$(curl -X POST http://localhost:7002/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"your_password"}' \
|
||||
| jq -r '.data.token')
|
||||
|
||||
# 2. 流式对话
|
||||
curl -N -X POST http://localhost:7002/agent/stream \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message": "你好,请介绍一下你自己",
|
||||
"maxContextLoadTime": 60,
|
||||
"language": "Chinese"
|
||||
}'
|
||||
```
|
||||
|
||||
### Python 示例
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
def stream_agent_chat(message: str, token: str):
|
||||
url = 'http://localhost:7002/agent/stream'
|
||||
headers = {
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
data = {
|
||||
'message': message,
|
||||
'maxContextLoadTime': 60,
|
||||
'language': 'Chinese',
|
||||
}
|
||||
|
||||
with requests.post(url, headers=headers, json=data, stream=True) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
line_str = line.decode('utf-8')
|
||||
if line_str.startswith('data: '):
|
||||
data = line_str[6:]
|
||||
|
||||
if data == '[DONE]':
|
||||
print('\n✅ Stream completed')
|
||||
break
|
||||
|
||||
try:
|
||||
event = json.loads(data)
|
||||
|
||||
if event.get('type') == 'text-delta' and event.get('text'):
|
||||
print(event['text'], end='', flush=True)
|
||||
elif event.get('type') == 'tool-call':
|
||||
print(f"\n[Tool: {event['toolName']}]")
|
||||
elif event.get('type') == 'error':
|
||||
print(f"\n❌ Error: {event['error']}")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 使用示例
|
||||
token = 'your_jwt_token_here'
|
||||
stream_agent_chat('你好,请介绍一下你自己', token)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 1. 认证验证
|
||||
|
||||
- 验证 JWT token
|
||||
- 提取用户信息
|
||||
|
||||
### 2. 模型配置获取
|
||||
|
||||
从用户设置中获取:
|
||||
- Chat Model(对话模型)
|
||||
- Embedding Model(嵌入模型)
|
||||
- Summary Model(摘要模型)
|
||||
|
||||
### 3. Agent 创建
|
||||
|
||||
- 创建 Memory 实例(用于记忆管理)
|
||||
- 创建 Agent 实例,配置回调函数:
|
||||
- `onReadMemory`: 从数据库加载历史对话
|
||||
- `onSearchMemory`: 搜索相关记忆
|
||||
- `onFinish`: 保存对话到记忆
|
||||
|
||||
### 4. 流式响应
|
||||
|
||||
- Agent 处理用户消息
|
||||
- 实时流式返回生成的文本
|
||||
- 报告工具调用事件
|
||||
- 完成后自动保存到记忆
|
||||
|
||||
### 5. 记忆管理
|
||||
|
||||
- 自动加载近期对话历史作为上下文
|
||||
- 使用向量搜索查找相关记忆
|
||||
- 对话结束后自动保存
|
||||
|
||||
---
|
||||
|
||||
## 特性
|
||||
|
||||
### ✅ 流式响应
|
||||
|
||||
- 实时返回生成的文本
|
||||
- 无需等待完整响应
|
||||
- 更好的用户体验
|
||||
|
||||
### ✅ 记忆管理
|
||||
|
||||
- 自动保存对话历史
|
||||
- 智能加载相关上下文
|
||||
- 向量搜索相关记忆
|
||||
|
||||
### ✅ 工具调用
|
||||
|
||||
- 支持搜索记忆工具
|
||||
- 可扩展其他工具
|
||||
- 实时报告工具使用
|
||||
|
||||
### ✅ 个性化配置
|
||||
|
||||
- 每个用户使用自己的模型配置
|
||||
- 可自定义语言偏好
|
||||
- 可配置上下文加载时间
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
### 1. 配置模型和 Agent 设置
|
||||
|
||||
使用 Settings API 配置默认模型和 Agent 参数:
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:7002/settings \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"defaultChatModel": "chat-model-uuid",
|
||||
"defaultEmbeddingModel": "embedding-model-uuid",
|
||||
"defaultSummaryModel": "summary-model-uuid",
|
||||
"maxContextLoadTime": 60,
|
||||
"language": "Chinese"
|
||||
}'
|
||||
```
|
||||
|
||||
详见 [Settings API 文档](./SETTINGS_API.md)
|
||||
|
||||
### 2. 创建模型配置
|
||||
|
||||
使用 Model API 创建模型:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7002/model \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"modelId": "gpt-4",
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
"apiKey": "your-api-key",
|
||||
"clientType": "openai",
|
||||
"name": "GPT-4"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 限制和注意事项
|
||||
|
||||
1. **模型配置必需**: 用户必须先配置好模型才能使用 Agent
|
||||
2. **流式连接**: 需要支持 Server-Sent Events 的客户端
|
||||
3. **超时处理**: 长时间无响应可能导致连接超时
|
||||
4. **并发限制**: 建议每个用户同时只维护一个对话流
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [认证系统](./AUTH_README.md)
|
||||
- [用户管理](./USER_MANAGEMENT.md)
|
||||
- [Settings API](./API_CHANGES.md)
|
||||
- [Model API](./README.md)
|
||||
- [Agent 包文档](../agent/README.md)
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
# API 变更说明
|
||||
|
||||
## 🔒 Settings 和 Memory 模块现已使用认证
|
||||
|
||||
### 概述
|
||||
|
||||
Settings 和 Memory 模块现在使用 JWT 认证中间件,自动从 token 中获取当前用户信息,**不再需要手动传入 userId**。
|
||||
|
||||
---
|
||||
|
||||
## Settings 模块变更
|
||||
|
||||
### ❌ 旧 API(已废弃)
|
||||
|
||||
```bash
|
||||
# 获取用户设置
|
||||
GET /settings/:userId
|
||||
|
||||
# 创建用户设置
|
||||
POST /settings
|
||||
{
|
||||
"userId": "user123",
|
||||
"defaultChatModel": "uuid-here"
|
||||
}
|
||||
|
||||
# 更新用户设置
|
||||
PUT /settings/:userId
|
||||
{
|
||||
"defaultChatModel": "uuid-here"
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ 新 API(需要认证)
|
||||
|
||||
```bash
|
||||
# 获取当前用户的设置
|
||||
GET /settings
|
||||
Authorization: Bearer <token>
|
||||
|
||||
# 更新或创建当前用户的设置
|
||||
PUT /settings
|
||||
Authorization: Bearer <token>
|
||||
{
|
||||
"defaultChatModel": "uuid-here",
|
||||
"defaultEmbeddingModel": "uuid-here",
|
||||
"defaultSummaryModel": "uuid-here",
|
||||
"maxContextLoadTime": 60,
|
||||
"language": "Chinese"
|
||||
}
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 1. 登录获取 token
|
||||
TOKEN=$(curl -X POST http://localhost:7002/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user","password":"pass"}' \
|
||||
| jq -r '.data.token')
|
||||
|
||||
# 2. 获取我的设置
|
||||
curl http://localhost:7002/settings \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 3. 更新我的设置
|
||||
curl -X PUT http://localhost:7002/settings \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"defaultChatModel": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"maxContextLoadTime": 60,
|
||||
"language": "Chinese"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory 模块变更
|
||||
|
||||
### ❌ 旧 API(已废弃)
|
||||
|
||||
```bash
|
||||
# 添加记忆
|
||||
POST /memory
|
||||
{
|
||||
"messages": [...],
|
||||
"timestamp": "2024-01-10T10:00:00Z",
|
||||
"user": "user123"
|
||||
}
|
||||
|
||||
# 搜索记忆
|
||||
GET /memory/search?query=hello&userId=user123
|
||||
|
||||
# 获取消息历史
|
||||
GET /memory/message?page=1&limit=10&userId=user123
|
||||
|
||||
# 按日期过滤消息
|
||||
GET /memory/message/filter?from=2024-01-01&to=2024-01-31&userId=user123
|
||||
```
|
||||
|
||||
### ✅ 新 API(需要认证)
|
||||
|
||||
```bash
|
||||
# 添加记忆(自动使用当前用户)
|
||||
POST /memory
|
||||
Authorization: Bearer <token>
|
||||
{
|
||||
"messages": [...],
|
||||
"timestamp": "2024-01-10T10:00:00Z"
|
||||
}
|
||||
|
||||
# 搜索当前用户的记忆
|
||||
GET /memory/search?query=hello
|
||||
Authorization: Bearer <token>
|
||||
|
||||
# 获取当前用户的消息历史
|
||||
GET /memory/message?page=1&limit=10
|
||||
Authorization: Bearer <token>
|
||||
|
||||
# 按日期过滤当前用户的消息
|
||||
GET /memory/message/filter?from=2024-01-01&to=2024-01-31
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 1. 登录获取 token
|
||||
TOKEN=$(curl -X POST http://localhost:7002/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user","password":"pass"}' \
|
||||
| jq -r '.data.token')
|
||||
|
||||
# 2. 添加记忆
|
||||
curl -X POST http://localhost:7002/memory \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi there!"}
|
||||
],
|
||||
"timestamp": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"
|
||||
}'
|
||||
|
||||
# 3. 搜索记忆
|
||||
curl "http://localhost:7002/memory/search?query=hello" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 4. 获取消息历史(分页)
|
||||
curl "http://localhost:7002/memory/message?page=1&limit=10" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 5. 按日期范围过滤
|
||||
curl "http://localhost:7002/memory/message/filter?from=2024-01-01&to=2024-01-31" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全优势
|
||||
|
||||
### ✅ 更安全
|
||||
|
||||
- 用户只能访问自己的数据
|
||||
- 无法通过修改 userId 参数访问其他用户的数据
|
||||
- 所有操作都需要有效的认证 token
|
||||
|
||||
### ✅ 更简洁
|
||||
|
||||
- API 调用更简单,无需手动传递 userId
|
||||
- 减少了参数验证和错误处理
|
||||
- 代码更清晰易维护
|
||||
|
||||
### ✅ 一致性
|
||||
|
||||
- 与其他需要认证的模块保持一致
|
||||
- 统一的认证流程和错误处理
|
||||
- 符合 RESTful 最佳实践
|
||||
|
||||
---
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 1. 更新客户端代码
|
||||
|
||||
#### 之前:
|
||||
```javascript
|
||||
// 需要手动传入 userId
|
||||
const settings = await fetch(`/settings/${userId}`)
|
||||
const memories = await fetch(`/memory/search?query=hello&userId=${userId}`)
|
||||
```
|
||||
|
||||
#### 现在:
|
||||
```javascript
|
||||
// 自动使用 token 中的用户信息
|
||||
const settings = await fetch('/settings', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
|
||||
const memories = await fetch('/memory/search?query=hello', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 错误处理
|
||||
|
||||
新增错误响应:
|
||||
|
||||
```json
|
||||
// 401 Unauthorized - 未提供 token 或 token 无效
|
||||
{
|
||||
"success": false,
|
||||
"error": "No bearer token provided"
|
||||
}
|
||||
|
||||
// 401 Unauthorized - Token 过期
|
||||
{
|
||||
"success": false,
|
||||
"error": "Invalid or expired token"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 端点总结
|
||||
|
||||
### Settings 模块 `/settings` 🔒
|
||||
|
||||
| 方法 | 路径 | 说明 | 认证 |
|
||||
|------|------|------|------|
|
||||
| GET | `/` | 获取当前用户设置 | ✅ 必需 |
|
||||
| PUT | `/` | 更新当前用户设置 | ✅ 必需 |
|
||||
|
||||
### Memory 模块 `/memory` 🔒
|
||||
|
||||
| 方法 | 路径 | 说明 | 认证 |
|
||||
|------|------|------|------|
|
||||
| POST | `/` | 添加记忆 | ✅ 必需 |
|
||||
| GET | `/search` | 搜索记忆 | ✅ 必需 |
|
||||
| GET | `/message` | 获取消息列表(分页) | ✅ 必需 |
|
||||
| GET | `/message/filter` | 按日期范围过滤消息 | ✅ 必需 |
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [认证系统文档](./AUTH_README.md)
|
||||
- [用户管理文档](./USER_MANAGEMENT.md)
|
||||
- [项目设置指南](../../SETUP.md)
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
# 认证系统使用文档
|
||||
|
||||
## 概述
|
||||
|
||||
本项目使用 JWT (JSON Web Token) 进行用户认证,集成了 Elysia 官方插件:
|
||||
- [@elysiajs/jwt](https://elysiajs.com/plugins/jwt.html) - JWT token 生成和验证
|
||||
- [@elysiajs/bearer](https://elysiajs.com/plugins/bearer.html) - Bearer token 提取
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
在项目根目录的 `.env` 文件中配置以下变量:
|
||||
|
||||
```bash
|
||||
# Root 超级用户配置
|
||||
ROOT_USER=admin
|
||||
ROOT_USER_PASSWORD=your_secure_password
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=your-jwt-secret-key-change-in-production
|
||||
JWT_EXPIRES_IN=7d # Token 有效期,可选
|
||||
|
||||
# API 服务器端口
|
||||
API_SERVER_PORT=7002
|
||||
```
|
||||
|
||||
## API 端点
|
||||
|
||||
### 1. 登录 POST `/auth/login`
|
||||
|
||||
用户登录并获取 JWT token。
|
||||
|
||||
**请求体:**
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "your_password"
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"user": {
|
||||
"id": "root",
|
||||
"username": "admin",
|
||||
"role": "admin",
|
||||
"displayName": "Root User",
|
||||
"email": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Invalid username or password"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 验证 Token GET `/auth/verify`
|
||||
|
||||
验证 JWT token 是否有效。
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
**成功响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"userId": "root",
|
||||
"username": "admin",
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Invalid or expired token"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 获取当前用户信息 GET `/auth/me`
|
||||
|
||||
获取当前登录用户的信息。
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
**成功响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"userId": "root",
|
||||
"username": "admin",
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### JavaScript/TypeScript 客户端
|
||||
|
||||
```typescript
|
||||
// 1. 登录
|
||||
const loginResponse = await fetch('http://localhost:7002/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'your_password',
|
||||
}),
|
||||
})
|
||||
|
||||
const loginData = await loginResponse.json()
|
||||
const token = loginData.data.token
|
||||
|
||||
// 2. 使用 token 访问受保护的资源
|
||||
const meResponse = await fetch('http://localhost:7002/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
const userData = await meResponse.json()
|
||||
console.log(userData.data) // 用户信息
|
||||
```
|
||||
|
||||
### curl 示例
|
||||
|
||||
```bash
|
||||
# 1. 登录
|
||||
curl -X POST http://localhost:7002/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"your_password"}'
|
||||
|
||||
# 2. 使用返回的 token 访问受保护资源
|
||||
TOKEN="<your_jwt_token>"
|
||||
curl http://localhost:7002/auth/me \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 在其他模块中使用认证
|
||||
|
||||
### 使用认证中间件
|
||||
|
||||
项目提供了三个认证中间件:
|
||||
|
||||
#### 1. `authMiddleware` - 强制认证
|
||||
|
||||
要求请求必须包含有效的 Bearer token,否则返回 401。
|
||||
|
||||
```typescript
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from './middlewares'
|
||||
|
||||
export const protectedModule = new Elysia({ prefix: '/protected' })
|
||||
.use(authMiddleware)
|
||||
.get('/data', ({ user }) => {
|
||||
// user 包含: { userId, username, role }
|
||||
return {
|
||||
success: true,
|
||||
message: `Hello ${user.username}!`,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 2. `optionalAuthMiddleware` - 可选认证
|
||||
|
||||
如果有 token 则验证,没有 token 也允许访问(user 为 null)。
|
||||
|
||||
```typescript
|
||||
import Elysia from 'elysia'
|
||||
import { optionalAuthMiddleware } from './middlewares'
|
||||
|
||||
export const publicModule = new Elysia({ prefix: '/public' })
|
||||
.use(optionalAuthMiddleware)
|
||||
.get('/data', ({ user }) => {
|
||||
if (user) {
|
||||
return { message: `Welcome back, ${user.username}!` }
|
||||
}
|
||||
return { message: 'Welcome, guest!' }
|
||||
})
|
||||
```
|
||||
|
||||
#### 3. `adminMiddleware` - 管理员权限
|
||||
|
||||
要求用户必须是 admin 角色,否则返回 403。
|
||||
|
||||
```typescript
|
||||
import Elysia from 'elysia'
|
||||
import { adminMiddleware } from './middlewares'
|
||||
|
||||
export const adminModule = new Elysia({ prefix: '/admin' })
|
||||
.use(adminMiddleware)
|
||||
.get('/users', ({ user }) => {
|
||||
// 只有 admin 才能访问
|
||||
return { success: true, data: [] }
|
||||
})
|
||||
```
|
||||
|
||||
## Root 用户说明
|
||||
|
||||
Root 用户是通过环境变量配置的超级管理员:
|
||||
|
||||
- **优先级最高**:登录时优先检查是否为 Root 用户
|
||||
- **不存储在数据库**:直接通过环境变量验证
|
||||
- **始终是 admin 角色**:拥有最高权限
|
||||
- **用于系统初始化**:可用于创建其他管理员用户
|
||||
|
||||
## 数据库用户
|
||||
|
||||
除了 Root 用户外,系统支持在数据库中创建普通用户:
|
||||
|
||||
```typescript
|
||||
import { createUser } from '@memohome/db/src/user-helpers'
|
||||
|
||||
// 创建新用户
|
||||
const newUser = await createUser({
|
||||
username: 'john_doe',
|
||||
email: 'john@example.com',
|
||||
passwordHash: await Bun.password.hash('password123'),
|
||||
role: 'member',
|
||||
displayName: 'John Doe',
|
||||
})
|
||||
```
|
||||
|
||||
用户密码使用 `Bun.password.hash` 加密存储,登录时使用 `Bun.password.verify` 验证。
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **生产环境配置**:
|
||||
- 使用强密码作为 `ROOT_USER_PASSWORD`
|
||||
- 设置随机的 `JWT_SECRET`(至少 32 字符)
|
||||
- 不要将 `.env` 文件提交到代码仓库
|
||||
|
||||
2. **Token 管理**:
|
||||
- Token 默认有效期为 7 天
|
||||
- 前端应安全存储 token(如 httpOnly cookie 或安全的 localStorage)
|
||||
- Token 过期后需要重新登录
|
||||
|
||||
3. **密码策略**:
|
||||
- 要求用户使用强密码
|
||||
- 考虑实现密码复杂度验证
|
||||
- 考虑添加密码重置功能
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Elysia**:Web 框架
|
||||
- **@elysiajs/jwt**:JWT token 生成和验证插件
|
||||
- **@elysiajs/bearer**:Bearer token 提取插件
|
||||
- **Bun.password**:密码加密和验证(基于 bcrypt)
|
||||
- **Drizzle ORM**:数据库操作
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Elysia JWT Plugin](https://elysiajs.com/plugins/jwt.html)
|
||||
- [Elysia Bearer Plugin](https://elysiajs.com/plugins/bearer.html)
|
||||
- [用户表设计文档](../db/USERS_SCHEMA.md)
|
||||
|
||||
@@ -1 +1,192 @@
|
||||
# @memohome/api
|
||||
|
||||
API 服务器,基于 Elysia 构建。
|
||||
|
||||
## API 模块
|
||||
|
||||
### 认证模块 (`/auth`)
|
||||
|
||||
用户认证和授权管理。
|
||||
|
||||
- `POST /auth/login` - 用户登录
|
||||
- `GET /auth/verify` - 验证 token
|
||||
- `GET /auth/me` - 获取当前用户信息
|
||||
|
||||
详细文档:[AUTH_README.md](./AUTH_README.md)
|
||||
|
||||
### 用户管理模块 (`/user`) 🔒 仅管理员
|
||||
|
||||
完整的用户 CRUD 操作接口。
|
||||
|
||||
- `GET /user` - 获取所有用户
|
||||
- `GET /user/:id` - 获取单个用户
|
||||
- `POST /user` - 创建用户
|
||||
- `PUT /user/:id` - 更新用户信息
|
||||
- `DELETE /user/:id` - 删除用户
|
||||
- `PATCH /user/:id/password` - 更新用户密码
|
||||
|
||||
详细文档:[USER_MANAGEMENT.md](./USER_MANAGEMENT.md)
|
||||
|
||||
### 模型管理模块 (`/model`)
|
||||
|
||||
AI 模型配置管理。
|
||||
|
||||
- `GET /model` - 获取所有模型
|
||||
- `GET /model/:id` - 获取单个模型
|
||||
- `POST /model` - 创建模型配置
|
||||
- `PUT /model/:id` - 更新模型配置
|
||||
- `DELETE /model/:id` - 删除模型配置
|
||||
- `GET /model/chat/default` - 获取默认聊天模型
|
||||
- `GET /model/summary/default` - 获取默认摘要模型
|
||||
- `GET /model/embedding/default` - 获取默认嵌入模型
|
||||
|
||||
### 设置模块 (`/settings`) 🔒 需要认证
|
||||
|
||||
用户偏好设置管理(自动使用当前登录用户)。
|
||||
|
||||
- `GET /settings` - 获取当前用户设置
|
||||
- `PUT /settings` - 更新当前用户设置(支持模型配置、Agent 参数等)
|
||||
|
||||
详细文档:[SETTINGS_API.md](./SETTINGS_API.md)
|
||||
|
||||
### 记忆模块 (`/memory`) 🔒 需要认证
|
||||
|
||||
用户记忆和对话历史管理(自动使用当前登录用户)。
|
||||
|
||||
- `POST /memory` - 添加记忆
|
||||
- `GET /memory/search` - 搜索记忆
|
||||
- `GET /memory/message` - 获取消息历史(分页)
|
||||
- `GET /memory/message/filter` - 按日期范围过滤消息
|
||||
|
||||
详细文档:[API_CHANGES.md](./API_CHANGES.md)
|
||||
|
||||
### Agent 模块 (`/agent`) 🔒 需要认证
|
||||
|
||||
AI Agent 智能对话接口,支持流式响应和记忆管理。
|
||||
|
||||
- `POST /agent/stream` - 流式对话(Server-Sent Events)
|
||||
|
||||
详细文档:[AGENT_API.md](./AGENT_API.md)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 配置环境变量
|
||||
|
||||
复制并编辑环境变量文件:
|
||||
|
||||
```bash
|
||||
cp ../../.env.example ../../.env
|
||||
```
|
||||
|
||||
必需配置:
|
||||
- `DATABASE_URL` - PostgreSQL 连接字符串
|
||||
- `ROOT_USER` - Root 超级管理员用户名
|
||||
- `ROOT_USER_PASSWORD` - Root 超级管理员密码
|
||||
- `JWT_SECRET` - JWT 签名密钥
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
服务器将在 `http://localhost:7002` 启动。
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
pnpm run start
|
||||
```
|
||||
|
||||
## 认证和权限
|
||||
|
||||
### Bearer Token 认证
|
||||
|
||||
所有受保护的 API 端点需要在请求头中携带 JWT token:
|
||||
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
### 权限级别
|
||||
|
||||
- **公开接口**:无需认证
|
||||
- **用户接口**:需要有效的 JWT token
|
||||
- **管理员接口** 🔒:需要管理员角色的 JWT token
|
||||
|
||||
## 中间件
|
||||
|
||||
### `authMiddleware`
|
||||
|
||||
强制认证中间件,要求请求必须包含有效的 Bearer token。
|
||||
|
||||
```typescript
|
||||
import { authMiddleware } from './middlewares'
|
||||
|
||||
const protectedModule = new Elysia()
|
||||
.use(authMiddleware)
|
||||
.get('/protected', ({ user }) => {
|
||||
return { message: `Hello ${user.username}!` }
|
||||
})
|
||||
```
|
||||
|
||||
### `adminMiddleware` 🔒
|
||||
|
||||
管理员权限中间件,要求用户角色为 `admin`。
|
||||
|
||||
```typescript
|
||||
import { adminMiddleware } from './middlewares'
|
||||
|
||||
const adminModule = new Elysia()
|
||||
.use(adminMiddleware)
|
||||
.get('/admin-only', ({ user }) => {
|
||||
return { message: 'Admin access granted' }
|
||||
})
|
||||
```
|
||||
|
||||
### `optionalAuthMiddleware`
|
||||
|
||||
可选认证中间件,如果有 token 则验证,没有则 `user` 为 `null`。
|
||||
|
||||
```typescript
|
||||
import { optionalAuthMiddleware } from './middlewares'
|
||||
|
||||
const publicModule = new Elysia()
|
||||
.use(optionalAuthMiddleware)
|
||||
.get('/public', ({ user }) => {
|
||||
if (user) {
|
||||
return { message: `Welcome back, ${user.username}!` }
|
||||
}
|
||||
return { message: 'Welcome, guest!' }
|
||||
})
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Elysia** - 高性能 Web 框架
|
||||
- **@elysiajs/jwt** - JWT 认证插件
|
||||
- **@elysiajs/bearer** - Bearer token 提取插件
|
||||
- **@elysiajs/cors** - CORS 支持
|
||||
- **Drizzle ORM** - 数据库 ORM
|
||||
- **Zod** - 数据验证
|
||||
- **Bun** - JavaScript 运行时
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [认证系统](./AUTH_README.md)
|
||||
- [用户管理](./USER_MANAGEMENT.md)
|
||||
- [项目设置指南](../../SETUP.md)
|
||||
- [数据库 Schema](../db/USERS_SCHEMA.md)
|
||||
@@ -0,0 +1,281 @@
|
||||
# Settings API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
Settings API 用于管理用户的个性化设置,包括默认模型配置和 Agent 行为设置。
|
||||
|
||||
## 端点
|
||||
|
||||
### GET `/settings` 🔒 需要认证
|
||||
|
||||
获取当前用户的设置。
|
||||
|
||||
#### 请求
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
#### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"userId": "user-id",
|
||||
"defaultChatModel": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"defaultEmbeddingModel": "223e4567-e89b-12d3-a456-426614174001",
|
||||
"defaultSummaryModel": "323e4567-e89b-12d3-a456-426614174002",
|
||||
"maxContextLoadTime": 60,
|
||||
"language": "Chinese"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应 (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Settings not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PUT `/settings` 🔒 需要认证
|
||||
|
||||
更新或创建当前用户的设置(Upsert 操作)。
|
||||
|
||||
#### 请求
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"defaultChatModel": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"defaultEmbeddingModel": "223e4567-e89b-12d3-a456-426614174001",
|
||||
"defaultSummaryModel": "323e4567-e89b-12d3-a456-426614174002",
|
||||
"maxContextLoadTime": 60,
|
||||
"language": "Chinese"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
|
||||
| 字段 | 类型 | 必需 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| defaultChatModel | string (uuid) | 否 | null | 默认聊天模型 ID |
|
||||
| defaultEmbeddingModel | string (uuid) | 否 | null | 默认嵌入模型 ID |
|
||||
| defaultSummaryModel | string (uuid) | 否 | null | 默认摘要模型 ID |
|
||||
| maxContextLoadTime | number | 否 | 60 | Agent 加载上下文的时间范围(分钟,1-1440) |
|
||||
| language | string | 否 | "Same as user input" | Agent 回复的首选语言 |
|
||||
|
||||
#### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"userId": "user-id",
|
||||
"defaultChatModel": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"defaultEmbeddingModel": "223e4567-e89b-12d3-a456-426614174001",
|
||||
"defaultSummaryModel": "323e4567-e89b-12d3-a456-426614174002",
|
||||
"maxContextLoadTime": 60,
|
||||
"language": "Chinese"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 完整工作流
|
||||
|
||||
```bash
|
||||
# 1. 登录获取 token
|
||||
TOKEN=$(curl -X POST http://localhost:7002/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"your_password"}' \
|
||||
| jq -r '.data.token')
|
||||
|
||||
# 2. 获取当前设置
|
||||
curl http://localhost:7002/settings \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 3. 更新设置
|
||||
curl -X PUT http://localhost:7002/settings \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"defaultChatModel": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"defaultEmbeddingModel": "223e4567-e89b-12d3-a456-426614174001",
|
||||
"defaultSummaryModel": "323e4567-e89b-12d3-a456-426614174002",
|
||||
"maxContextLoadTime": 120,
|
||||
"language": "English"
|
||||
}'
|
||||
|
||||
# 4. 只更新部分字段
|
||||
curl -X PUT http://localhost:7002/settings \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"maxContextLoadTime": 30,
|
||||
"language": "Chinese"
|
||||
}'
|
||||
```
|
||||
|
||||
### JavaScript/TypeScript 示例
|
||||
|
||||
```typescript
|
||||
// 获取设置
|
||||
async function getSettings(token: string) {
|
||||
const response = await fetch('http://localhost:7002/settings', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
}
|
||||
|
||||
// 更新设置
|
||||
async function updateSettings(token: string, settings: {
|
||||
defaultChatModel?: string
|
||||
defaultEmbeddingModel?: string
|
||||
defaultSummaryModel?: string
|
||||
maxContextLoadTime?: number
|
||||
language?: string
|
||||
}) {
|
||||
const response = await fetch('http://localhost:7002/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(settings),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const token = 'your_jwt_token'
|
||||
|
||||
// 获取当前设置
|
||||
const currentSettings = await getSettings(token)
|
||||
console.log(currentSettings)
|
||||
|
||||
// 更新 Agent 设置
|
||||
await updateSettings(token, {
|
||||
maxContextLoadTime: 90,
|
||||
language: 'Chinese',
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent 设置说明
|
||||
|
||||
### maxContextLoadTime
|
||||
|
||||
控制 Agent 加载历史对话上下文的时间范围。
|
||||
|
||||
- **类型**: 整数(分钟)
|
||||
- **范围**: 1-1440(1分钟到24小时)
|
||||
- **默认值**: 60(1小时)
|
||||
- **说明**:
|
||||
- 值越大,Agent 能访问更久远的对话历史
|
||||
- 但也会增加 token 使用量和响应时间
|
||||
- 建议根据实际需求调整
|
||||
|
||||
**示例:**
|
||||
- `30` - 加载最近30分钟的对话
|
||||
- `60` - 加载最近1小时的对话(默认)
|
||||
- `1440` - 加载最近24小时的对话
|
||||
|
||||
### language
|
||||
|
||||
设置 Agent 回复的首选语言。
|
||||
|
||||
- **类型**: 字符串
|
||||
- **默认值**: "Same as user input"(与用户输入相同)
|
||||
- **说明**:
|
||||
- 可以设置为任何语言名称
|
||||
- Agent 会尽量使用指定语言回复
|
||||
- 特殊值 "Same as user input" 表示跟随用户输入语言
|
||||
|
||||
**常用值:**
|
||||
- `"Same as user input"` - 自动匹配用户语言(默认)
|
||||
- `"Chinese"` - 中文
|
||||
- `"English"` - 英文
|
||||
- `"Japanese"` - 日文
|
||||
- `"Spanish"` - 西班牙语
|
||||
|
||||
---
|
||||
|
||||
## Agent 使用优先级
|
||||
|
||||
当使用 Agent API 时,配置的优先级为:
|
||||
|
||||
1. **请求参数** - `/agent/stream` 请求中直接指定的参数
|
||||
2. **用户设置** - Settings 中保存的默认值
|
||||
3. **系统默认** - 内置的默认值
|
||||
|
||||
**示例:**
|
||||
|
||||
```bash
|
||||
# 情况1: 使用请求参数(最高优先级)
|
||||
curl -X POST http://localhost:7002/agent/stream \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"message": "Hello",
|
||||
"maxContextLoadTime": 30,
|
||||
"language": "English"
|
||||
}'
|
||||
# 使用: maxContextLoadTime=30, language="English"
|
||||
|
||||
# 情况2: 使用用户设置(如果请求中未指定)
|
||||
# 假设用户设置: maxContextLoadTime=60, language="Chinese"
|
||||
curl -X POST http://localhost:7002/agent/stream \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"message": "Hello"}'
|
||||
# 使用: maxContextLoadTime=60, language="Chinese"
|
||||
|
||||
# 情况3: 使用系统默认(如果都未设置)
|
||||
# 使用: maxContextLoadTime=60, language="Same as user input"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库 Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE settings (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
default_chat_model UUID REFERENCES model(id),
|
||||
default_embedding_model UUID REFERENCES model(id),
|
||||
default_summary_model UUID REFERENCES model(id),
|
||||
max_context_load_time INTEGER DEFAULT 60,
|
||||
language TEXT DEFAULT 'Same as user input'
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Agent API](./AGENT_API.md)
|
||||
- [Model API](./README.md)
|
||||
- [认证系统](./AUTH_README.md)
|
||||
- [API 变更说明](./API_CHANGES.md)
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
# 用户管理 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
用户管理模块提供了完整的用户 CRUD 操作接口,**仅限管理员访问**。所有端点都需要携带有效的管理员 JWT token。
|
||||
|
||||
## 权限要求
|
||||
|
||||
所有用户管理接口都受 `adminMiddleware` 保护:
|
||||
- 必须提供有效的 Bearer token
|
||||
- 用户角色必须是 `admin`
|
||||
- 否则返回 `403 Forbidden` 错误
|
||||
|
||||
## API 端点
|
||||
|
||||
### 基础 URL
|
||||
|
||||
```
|
||||
http://localhost:7002/user
|
||||
```
|
||||
|
||||
### 请求头
|
||||
|
||||
所有请求都需要包含认证 token:
|
||||
|
||||
```
|
||||
Authorization: Bearer <your_admin_jwt_token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 获取所有用户 GET `/user`
|
||||
|
||||
获取系统中所有用户的列表。
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl http://localhost:7002/user \
|
||||
-H "Authorization: Bearer <admin_token>"
|
||||
```
|
||||
|
||||
### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"role": "member",
|
||||
"displayName": "John Doe",
|
||||
"avatarUrl": "https://example.com/avatar.jpg",
|
||||
"isActive": "true",
|
||||
"createdAt": "2024-01-10T10:00:00Z",
|
||||
"updatedAt": "2024-01-10T10:00:00Z",
|
||||
"lastLoginAt": "2024-01-10T12:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 获取单个用户 GET `/user/:id`
|
||||
|
||||
根据用户 ID 获取用户详细信息。
|
||||
|
||||
### 路径参数
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | UUID | 是 | 用户的唯一标识符 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl http://localhost:7002/user/123e4567-e89b-12d3-a456-426614174000 \
|
||||
-H "Authorization: Bearer <admin_token>"
|
||||
```
|
||||
|
||||
### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"role": "member",
|
||||
"displayName": "John Doe",
|
||||
"avatarUrl": "https://example.com/avatar.jpg",
|
||||
"isActive": "true",
|
||||
"createdAt": "2024-01-10T10:00:00Z",
|
||||
"updatedAt": "2024-01-10T10:00:00Z",
|
||||
"lastLoginAt": "2024-01-10T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应 (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "User not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 创建用户 POST `/user`
|
||||
|
||||
创建新用户账户。
|
||||
|
||||
### 请求体
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| username | string | 是 | 用户名(3-50字符,唯一) |
|
||||
| email | string | 否 | 邮箱地址(必须是有效格式,唯一) |
|
||||
| password | string | 是 | 密码(至少6个字符) |
|
||||
| role | string | 否 | 用户角色:`admin` 或 `member`(默认 `member`) |
|
||||
| displayName | string | 否 | 显示名称 |
|
||||
| avatarUrl | string | 否 | 头像 URL(必须是有效的 URL) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7002/user \
|
||||
-H "Authorization: Bearer <admin_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "jane_smith",
|
||||
"email": "jane@example.com",
|
||||
"password": "secure_password123",
|
||||
"role": "member",
|
||||
"displayName": "Jane Smith"
|
||||
}'
|
||||
```
|
||||
|
||||
### 成功响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "223e4567-e89b-12d3-a456-426614174001",
|
||||
"username": "jane_smith",
|
||||
"email": "jane@example.com",
|
||||
"role": "member",
|
||||
"displayName": "Jane Smith",
|
||||
"avatarUrl": null,
|
||||
"isActive": "true",
|
||||
"createdAt": "2024-01-10T15:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意**: 创建用户时会自动创建一个默认的 settings 条目,包含:
|
||||
- `maxContextLoadTime`: 60(分钟)
|
||||
- `language`: "Same as user input"
|
||||
- 所有模型配置为 `null`(需要用户后续配置)
|
||||
|
||||
### 错误响应 (409 Conflict)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Username already exists"
|
||||
}
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Email already exists"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 更新用户 PUT `/user/:id`
|
||||
|
||||
更新用户信息(不包括密码)。
|
||||
|
||||
### 路径参数
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | UUID | 是 | 用户的唯一标识符 |
|
||||
|
||||
### 请求体(所有字段都是可选的)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| email | string | 邮箱地址 |
|
||||
| role | string | 用户角色:`admin` 或 `member` |
|
||||
| displayName | string | 显示名称 |
|
||||
| avatarUrl | string | 头像 URL |
|
||||
| isActive | string | 账户状态:`true` 或 `false` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:7002/user/223e4567-e89b-12d3-a456-426614174001 \
|
||||
-H "Authorization: Bearer <admin_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"displayName": "Jane Smith (Updated)",
|
||||
"role": "admin",
|
||||
"isActive": "true"
|
||||
}'
|
||||
```
|
||||
|
||||
### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "223e4567-e89b-12d3-a456-426614174001",
|
||||
"username": "jane_smith",
|
||||
"email": "jane@example.com",
|
||||
"role": "admin",
|
||||
"displayName": "Jane Smith (Updated)",
|
||||
"avatarUrl": null,
|
||||
"isActive": "true",
|
||||
"createdAt": "2024-01-10T15:00:00Z",
|
||||
"updatedAt": "2024-01-10T16:00:00Z",
|
||||
"lastLoginAt": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应 (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "User not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 删除用户 DELETE `/user/:id`
|
||||
|
||||
删除用户账户。
|
||||
|
||||
### 路径参数
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | UUID | 是 | 用户的唯一标识符 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:7002/user/223e4567-e89b-12d3-a456-426614174001 \
|
||||
-H "Authorization: Bearer <admin_token>"
|
||||
```
|
||||
|
||||
### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "223e4567-e89b-12d3-a456-426614174001",
|
||||
"username": "jane_smith"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应 (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "User not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 更新用户密码 PATCH `/user/:id/password`
|
||||
|
||||
重置或更新用户密码。
|
||||
|
||||
### 路径参数
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | UUID | 是 | 用户的唯一标识符 |
|
||||
|
||||
### 请求体
|
||||
|
||||
| 字段 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| password | string | 是 | 新密码(至少6个字符) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://localhost:7002/user/223e4567-e89b-12d3-a456-426614174001/password \
|
||||
-H "Authorization: Bearer <admin_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"password": "new_secure_password456"
|
||||
}'
|
||||
```
|
||||
|
||||
### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "223e4567-e89b-12d3-a456-426614174001",
|
||||
"username": "jane_smith"
|
||||
},
|
||||
"message": "Password updated successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应 (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "User not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误响应
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
未提供 token 或 token 无效:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "No bearer token provided"
|
||||
}
|
||||
```
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
非管理员用户尝试访问:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Forbidden: Admin access required"
|
||||
}
|
||||
```
|
||||
|
||||
### 400 Bad Request
|
||||
|
||||
请求数据验证失败:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Username must be at least 3 characters"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 完整工作流
|
||||
|
||||
```bash
|
||||
# 1. 管理员登录获取 token
|
||||
TOKEN=$(curl -X POST http://localhost:7002/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"your_admin_password"}' \
|
||||
| jq -r '.data.token')
|
||||
|
||||
# 2. 创建新用户
|
||||
curl -X POST http://localhost:7002/user \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "new_user",
|
||||
"email": "newuser@example.com",
|
||||
"password": "password123",
|
||||
"role": "member",
|
||||
"displayName": "New User"
|
||||
}'
|
||||
|
||||
# 3. 获取所有用户
|
||||
curl http://localhost:7002/user \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 4. 获取特定用户
|
||||
USER_ID="123e4567-e89b-12d3-a456-426614174000"
|
||||
curl http://localhost:7002/user/$USER_ID \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 5. 更新用户信息
|
||||
curl -X PUT http://localhost:7002/user/$USER_ID \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"displayName": "Updated Name",
|
||||
"role": "admin"
|
||||
}'
|
||||
|
||||
# 6. 重置用户密码
|
||||
curl -X PATCH http://localhost:7002/user/$USER_ID/password \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"password": "new_password123"
|
||||
}'
|
||||
|
||||
# 7. 删除用户
|
||||
curl -X DELETE http://localhost:7002/user/$USER_ID \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自动化行为
|
||||
|
||||
### 创建用户时的自动操作
|
||||
|
||||
当创建新用户时,系统会自动执行以下操作:
|
||||
|
||||
1. **密码加密**: 使用 bcrypt 算法加密密码
|
||||
2. **创建 Settings**: 自动为用户创建默认设置条目
|
||||
- `maxContextLoadTime`: 60 分钟
|
||||
- `language`: "Same as user input"
|
||||
- 模型配置: 全部为 null(需要用户后续配置)
|
||||
|
||||
这确保每个用户都有完整的配置,可以立即使用系统功能。
|
||||
|
||||
---
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **管理员权限**:
|
||||
- 所有接口仅限管理员访问
|
||||
- 确保 Root 用户的密码强度足够
|
||||
- 定期审计管理员账户
|
||||
|
||||
2. **密码安全**:
|
||||
- 密码使用 bcrypt 加密存储
|
||||
- 最少 6 个字符(建议更高要求)
|
||||
- 重置密码后建议通知用户
|
||||
|
||||
3. **数据验证**:
|
||||
- 所有输入都经过严格验证
|
||||
- 邮箱和用户名必须唯一
|
||||
- URL 格式必须有效
|
||||
|
||||
4. **用户状态**:
|
||||
- 使用 `isActive` 字段禁用用户而非直接删除
|
||||
- 保留用户数据用于审计
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [认证系统文档](./AUTH_README.md)
|
||||
- [用户表设计](../db/USERS_SCHEMA.md)
|
||||
- [项目设置指南](../../SETUP.md)
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Agent Stream Client Example
|
||||
*
|
||||
* This example demonstrates how to use the /agent/stream API endpoint
|
||||
* to have a streaming conversation with the AI agent.
|
||||
*
|
||||
* Usage:
|
||||
* bun run agent-stream-client.ts
|
||||
*/
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:7002'
|
||||
|
||||
interface LoginResponse {
|
||||
success: boolean
|
||||
data?: {
|
||||
token: string
|
||||
user: {
|
||||
id: string
|
||||
username: string
|
||||
role: string
|
||||
}
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
async function login(username: string, password: string): Promise<string> {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
|
||||
const data: LoginResponse = await response.json()
|
||||
|
||||
if (!data.success || !data.data?.token) {
|
||||
throw new Error(data.error || 'Login failed')
|
||||
}
|
||||
|
||||
return data.data.token
|
||||
}
|
||||
|
||||
async function streamAgentChat(message: string, token: string) {
|
||||
console.log(`\n📤 User: ${message}`)
|
||||
console.log('🤖 Agent: ', { end: '' })
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/agent/stream`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
maxContextLoadTime: 60,
|
||||
language: 'Same as user input',
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Stream request failed')
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
if (!reader) {
|
||||
throw new Error('No reader available')
|
||||
}
|
||||
|
||||
let hasOutput = false
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6).trim()
|
||||
|
||||
if (data === '[DONE]') {
|
||||
console.log('\n\n✅ Stream completed')
|
||||
return
|
||||
}
|
||||
|
||||
if (!data) continue
|
||||
|
||||
try {
|
||||
const event = JSON.parse(data)
|
||||
|
||||
if (event.type === 'text-delta' && event.text) {
|
||||
process.stdout.write(event.text)
|
||||
hasOutput = true
|
||||
} else if (event.type === 'tool-call') {
|
||||
console.log(`\n[🔧 Tool: ${event.toolName}]`)
|
||||
hasOutput = true
|
||||
} else if (event.type === 'error') {
|
||||
console.error('\n❌ Error:', event.error)
|
||||
throw new Error(event.error)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
// Skip invalid JSON
|
||||
continue
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
|
||||
if (!hasOutput) {
|
||||
console.log('(No response)')
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Get credentials from environment or use defaults
|
||||
const username = process.env.USERNAME || 'admin'
|
||||
const password = process.env.PASSWORD || 'admin'
|
||||
|
||||
console.log('🔐 Logging in...')
|
||||
const token = await login(username, password)
|
||||
console.log('✅ Login successful')
|
||||
|
||||
// Example conversations
|
||||
const messages = [
|
||||
'你好,请介绍一下你自己',
|
||||
'你能帮我做什么?',
|
||||
'记住:我的名字是张三',
|
||||
'我的名字是什么?',
|
||||
]
|
||||
|
||||
for (const message of messages) {
|
||||
await streamAgentChat(message, token)
|
||||
// Wait a bit between messages
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
console.log('\n🎉 All conversations completed!')
|
||||
} catch (error) {
|
||||
console.error('\n❌ Error:', error instanceof Error ? error.message : String(error))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Ctrl+C gracefully
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n\n👋 Goodbye!')
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
main()
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
"./client": "./src/client.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/bearer": "^1.1.4",
|
||||
"@elysiajs/cors": "^1.4.1",
|
||||
"@elysiajs/cron": "^1.4.1",
|
||||
"@elysiajs/eden": "^1.4.6",
|
||||
"@elysiajs/jwt": "^1.2.0",
|
||||
"@memohome/agent": "workspace:*",
|
||||
"@memohome/db": "workspace:*",
|
||||
"@memohome/memory": "workspace:*",
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Elysia } from 'elysia'
|
||||
import { corsMiddleware } from './middlewares'
|
||||
import { agentModule, modelModule, settingsModule } from './modules'
|
||||
import { agentModule, authModule, modelModule, settingsModule, userModule } from './modules'
|
||||
import { memoryModule } from './modules/memory'
|
||||
|
||||
const port = process.env.API_SERVER_PORT || 7002
|
||||
|
||||
export const app = new Elysia()
|
||||
.use(corsMiddleware)
|
||||
.use(authModule)
|
||||
.use(agentModule)
|
||||
.use(memoryModule)
|
||||
.use(modelModule)
|
||||
.use(settingsModule)
|
||||
.use(userModule)
|
||||
.listen(port)
|
||||
|
||||
console.log(
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Elysia } from 'elysia'
|
||||
import { bearer } from '@elysiajs/bearer'
|
||||
import { jwt } from '@elysiajs/jwt'
|
||||
|
||||
/**
|
||||
* 认证中间件
|
||||
* 验证 Bearer token 并将用户信息注入到 context 中
|
||||
*/
|
||||
export const authMiddleware = new Elysia({ name: 'auth' })
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
exp: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
)
|
||||
.use(bearer())
|
||||
.derive(async ({ bearer, jwt, set }) => {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
throw new Error('No bearer token provided')
|
||||
}
|
||||
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
throw new Error('Invalid or expired token')
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
userId: payload.userId as string,
|
||||
username: payload.username as string,
|
||||
role: payload.role as string,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 可选认证中间件
|
||||
* 如果有 token 则验证,没有 token 则继续(user 为 null)
|
||||
*/
|
||||
export const optionalAuthMiddleware = new Elysia({ name: 'optional-auth' })
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
exp: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
)
|
||||
.use(bearer())
|
||||
.derive(async ({ bearer, jwt }) => {
|
||||
if (!bearer) {
|
||||
return { user: null }
|
||||
}
|
||||
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
return { user: null }
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
userId: payload.userId as string,
|
||||
username: payload.username as string,
|
||||
role: payload.role as string,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 管理员权限中间件
|
||||
* 验证 token 并检查用户是否为管理员
|
||||
*/
|
||||
export const adminMiddleware = new Elysia({ name: 'admin' })
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
exp: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
)
|
||||
.use(bearer())
|
||||
.derive(async ({ bearer, jwt, set }) => {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
throw new Error('No bearer token provided')
|
||||
}
|
||||
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
throw new Error('Invalid or expired token')
|
||||
}
|
||||
|
||||
const user = {
|
||||
userId: payload.userId as string,
|
||||
username: payload.username as string,
|
||||
role: payload.role as string,
|
||||
}
|
||||
|
||||
// 检查是否为管理员
|
||||
if (user.role !== 'admin') {
|
||||
set.status = 403
|
||||
throw new Error('Forbidden: Admin access required')
|
||||
}
|
||||
|
||||
return { user }
|
||||
})
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './auth'
|
||||
export * from './cors'
|
||||
@@ -1,5 +1,125 @@
|
||||
import Elysia from 'elysia'
|
||||
import { bearer } from '@elysiajs/bearer'
|
||||
import { jwt } from '@elysiajs/jwt'
|
||||
import { AgentStreamModel } from './model'
|
||||
import { createAgentStream } from './service'
|
||||
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
|
||||
import { getSettings } from '../settings/service'
|
||||
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||
|
||||
export const agentModule = new Elysia({
|
||||
prefix: '/agent',
|
||||
})
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
exp: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
)
|
||||
.use(bearer())
|
||||
.derive(async ({ bearer, jwt, set }) => {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
throw new Error('No bearer token provided')
|
||||
}
|
||||
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
throw new Error('Invalid or expired token')
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
userId: payload.userId as string,
|
||||
username: payload.username as string,
|
||||
role: payload.role as string,
|
||||
},
|
||||
}
|
||||
})
|
||||
// Stream agent conversation
|
||||
.post('/stream', async ({ user, body, set }) => {
|
||||
try {
|
||||
// Get user's model configurations and settings
|
||||
const [chatModel, embeddingModel, summaryModel, userSettings] = await Promise.all([
|
||||
getChatModel(user.userId),
|
||||
getEmbeddingModel(user.userId),
|
||||
getSummaryModel(user.userId),
|
||||
getSettings(user.userId),
|
||||
])
|
||||
|
||||
if (!chatModel || !embeddingModel || !summaryModel) {
|
||||
set.status = 400
|
||||
return {
|
||||
success: false,
|
||||
error: 'Model configuration not found. Please configure your models in settings.',
|
||||
}
|
||||
}
|
||||
|
||||
// Use body params if provided, otherwise use settings, otherwise use defaults
|
||||
const maxContextLoadTime = body.maxContextLoadTime
|
||||
?? userSettings?.maxContextLoadTime
|
||||
?? 60
|
||||
const language = body.language
|
||||
?? userSettings?.language
|
||||
?? 'Same as user input'
|
||||
|
||||
// Create agent
|
||||
const agent = await createAgentStream({
|
||||
userId: user.userId,
|
||||
chatModel: chatModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
summaryModel: summaryModel.model as ChatModel,
|
||||
maxContextLoadTime,
|
||||
language,
|
||||
})
|
||||
|
||||
// Set headers for Server-Sent Events
|
||||
set.headers['Content-Type'] = 'text/event-stream'
|
||||
set.headers['Cache-Control'] = 'no-cache'
|
||||
set.headers['Connection'] = 'keep-alive'
|
||||
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
// Send events as they come
|
||||
for await (const event of agent.ask(body.message)) {
|
||||
const data = JSON.stringify(event)
|
||||
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
||||
}
|
||||
|
||||
// Send done event
|
||||
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
||||
controller.close()
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
const errorData = JSON.stringify({
|
||||
type: 'error',
|
||||
error: errorMessage
|
||||
})
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${errorData}\n\n`))
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to process request',
|
||||
}
|
||||
}
|
||||
}, AgentStreamModel)
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const AgentStreamModel = {
|
||||
body: z.object({
|
||||
message: z.string().min(1, 'Message is required'),
|
||||
// Optional overrides - if not provided, will use settings
|
||||
maxContextLoadTime: z.number().int().min(1).max(1440).optional(),
|
||||
language: z.string().optional(),
|
||||
}),
|
||||
}
|
||||
|
||||
export type AgentStreamInput = z.infer<typeof AgentStreamModel['body']>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createAgent } from '@memohome/agent'
|
||||
import { createMemory, filterByTimestamp, MemoryUnit } from '@memohome/memory'
|
||||
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||
|
||||
// Type for messages passed to onFinish callback
|
||||
type MessageType = Record<string, unknown>
|
||||
|
||||
export interface CreateAgentStreamParams {
|
||||
userId: string
|
||||
chatModel: ChatModel
|
||||
embeddingModel: EmbeddingModel
|
||||
summaryModel: ChatModel
|
||||
maxContextLoadTime: number
|
||||
language?: string
|
||||
onFinish?: (messages: MessageType[]) => Promise<void>
|
||||
}
|
||||
|
||||
export async function createAgentStream(params: CreateAgentStreamParams) {
|
||||
const {
|
||||
userId,
|
||||
chatModel,
|
||||
embeddingModel,
|
||||
summaryModel,
|
||||
maxContextLoadTime,
|
||||
language,
|
||||
onFinish,
|
||||
} = params
|
||||
|
||||
// Create memory instance
|
||||
const memoryInstance = createMemory({
|
||||
summaryModel,
|
||||
embeddingModel,
|
||||
})
|
||||
|
||||
// Create agent
|
||||
const agent = createAgent({
|
||||
model: chatModel,
|
||||
maxContextLoadTime,
|
||||
language: language || 'Same as user input',
|
||||
onReadMemory: async (from: Date, to: Date) => {
|
||||
return await filterByTimestamp(from, to, userId)
|
||||
},
|
||||
onSearchMemory: async (query: string) => {
|
||||
const results = await memoryInstance.searchMemory(query, userId)
|
||||
return results
|
||||
},
|
||||
onFinish: async (messages: MessageType[]) => {
|
||||
// Save conversation to memory
|
||||
const memoryUnit: MemoryUnit = {
|
||||
messages: messages as unknown as MemoryUnit['messages'],
|
||||
timestamp: new Date(),
|
||||
user: userId,
|
||||
}
|
||||
await memoryInstance.addMemory(memoryUnit)
|
||||
|
||||
// Call custom onFinish handler if provided
|
||||
await onFinish?.(messages)
|
||||
},
|
||||
})
|
||||
|
||||
return agent
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import Elysia from 'elysia'
|
||||
import { bearer } from '@elysiajs/bearer'
|
||||
import { jwt } from '@elysiajs/jwt'
|
||||
import { LoginModel } from './model'
|
||||
import { validateUser } from './service'
|
||||
|
||||
export const authModule = new Elysia({
|
||||
prefix: '/auth',
|
||||
})
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
exp: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
)
|
||||
.use(bearer())
|
||||
// Login endpoint
|
||||
.post('/login', async ({ body, jwt, set }) => {
|
||||
try {
|
||||
const user = await validateUser(body.username, body.password)
|
||||
|
||||
if (!user) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid username or password',
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 JWT 插件生成 token
|
||||
const token = await jwt.sign({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
displayName: user.displayName,
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to login',
|
||||
}
|
||||
}
|
||||
}, LoginModel)
|
||||
// Verify token endpoint
|
||||
.get('/verify', async ({ bearer, jwt, set }) => {
|
||||
try {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'No bearer token provided',
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 JWT 插件验证 token
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
userId: payload.userId,
|
||||
username: payload.username,
|
||||
role: payload.role,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Get current user info
|
||||
.get('/me', async ({ bearer, jwt, set }) => {
|
||||
try {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'No bearer token provided',
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 JWT 插件验证 token
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
userId: payload.userId,
|
||||
username: payload.username,
|
||||
role: payload.role,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
set.status = 401
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const LoginSchema = z.object({
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
})
|
||||
|
||||
export type LoginInput = z.infer<typeof LoginSchema>
|
||||
|
||||
export const LoginModel = {
|
||||
body: LoginSchema,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { db } from '@memohome/db'
|
||||
import { users, settings } from '@memohome/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
/**
|
||||
* 验证用户凭据
|
||||
* 优先检查是否为 ROOT 用户,否则查询数据库
|
||||
*/
|
||||
export const validateUser = async (username: string, password: string) => {
|
||||
// 检查是否为 ROOT 用户
|
||||
const rootUser = process.env.ROOT_USER
|
||||
const rootPassword = process.env.ROOT_USER_PASSWORD
|
||||
|
||||
if (rootUser && rootPassword && username === rootUser) {
|
||||
if (password === rootPassword) {
|
||||
// 检查 root 用户的 settings 是否存在,不存在则创建
|
||||
const [existingSettings] = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(eq(settings.userId, 'root'))
|
||||
|
||||
if (!existingSettings) {
|
||||
// 为 root 用户创建默认 settings
|
||||
await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: 'root',
|
||||
defaultChatModel: null,
|
||||
defaultEmbeddingModel: null,
|
||||
defaultSummaryModel: null,
|
||||
maxContextLoadTime: 60,
|
||||
language: 'Same as user input',
|
||||
})
|
||||
.onConflictDoNothing() // 避免并发创建导致的冲突
|
||||
}
|
||||
|
||||
// 返回 ROOT 用户信息
|
||||
return {
|
||||
id: 'root',
|
||||
username: rootUser,
|
||||
role: 'admin' as const,
|
||||
displayName: 'Root User',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 查询数据库中的用户
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, username))
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 验证密码 (这里使用简单的 Bun.password.verify)
|
||||
const isValid = await Bun.password.verify(password, user.passwordHash)
|
||||
|
||||
if (!isValid) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查账户是否激活
|
||||
if (user.isActive !== 'true') {
|
||||
return null
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
lastLoginAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, user.id))
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
displayName: user.displayName || user.username,
|
||||
email: user.email,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './agent'
|
||||
export * from './auth'
|
||||
export * from './model'
|
||||
export * from './settings'
|
||||
export * from './user'
|
||||
@@ -1,4 +1,6 @@
|
||||
import Elysia from 'elysia'
|
||||
import { bearer } from '@elysiajs/bearer'
|
||||
import { jwt } from '@elysiajs/jwt'
|
||||
import { messageModule } from './message'
|
||||
import { AddMemoryModel, SearchMemoryModel } from './model'
|
||||
import { addMemory, searchMemory } from './service'
|
||||
@@ -6,32 +8,67 @@ import { MemoryUnit } from '@memohome/memory'
|
||||
|
||||
export const memoryModule = new Elysia({
|
||||
prefix: '/memory',
|
||||
})
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
exp: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
)
|
||||
.use(bearer())
|
||||
.derive(async ({ bearer, jwt, set }) => {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
throw new Error('No bearer token provided')
|
||||
}
|
||||
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
throw new Error('Invalid or expired token')
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
userId: payload.userId as string,
|
||||
username: payload.username as string,
|
||||
role: payload.role as string,
|
||||
},
|
||||
}
|
||||
})
|
||||
.use(messageModule)
|
||||
// Add memory
|
||||
.post('/', async ({ body }) => {
|
||||
// Add memory for current user
|
||||
.post('/', async ({ user, body, set }) => {
|
||||
try {
|
||||
const result = await addMemory(body as unknown as MemoryUnit)
|
||||
const memoryUnit: MemoryUnit = {
|
||||
...body,
|
||||
user: user.userId,
|
||||
}
|
||||
const result = await addMemory(memoryUnit)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to add memory',
|
||||
}
|
||||
}
|
||||
}, AddMemoryModel)
|
||||
// Search memory
|
||||
.get('/search', async ({ query }) => {
|
||||
// Search memory for current user
|
||||
.get('/search', async ({ user, query, set }) => {
|
||||
try {
|
||||
const results = await searchMemory(query.query, query.userId)
|
||||
const results = await searchMemory(query.query, user.userId)
|
||||
return {
|
||||
success: true,
|
||||
data: results,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to search memory',
|
||||
|
||||
@@ -1,22 +1,70 @@
|
||||
import Elysia from 'elysia'
|
||||
import { bearer } from '@elysiajs/bearer'
|
||||
import { jwt } from '@elysiajs/jwt'
|
||||
import { GetMemoryMessageFilterModel, GetMemoryMessageModel } from './model'
|
||||
import { getMemoryMessages, getMemoryMessagesFilter } from './service'
|
||||
|
||||
export const messageModule = new Elysia({
|
||||
prefix: '/message',
|
||||
})
|
||||
.get('/', async ({ query }) => {
|
||||
const units = await getMemoryMessages(query)
|
||||
return {
|
||||
success: true,
|
||||
units,
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
exp: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
)
|
||||
.use(bearer())
|
||||
.derive(async ({ bearer, jwt, set }) => {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
throw new Error('No bearer token provided')
|
||||
}
|
||||
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
throw new Error('Invalid or expired token')
|
||||
}
|
||||
}, GetMemoryMessageModel)
|
||||
.get('/filter', async ({ query }) => {
|
||||
const units = await getMemoryMessagesFilter(query)
|
||||
|
||||
return {
|
||||
units,
|
||||
user: {
|
||||
userId: payload.userId as string,
|
||||
username: payload.username as string,
|
||||
role: payload.role as string,
|
||||
},
|
||||
}
|
||||
})
|
||||
// Get messages for current user (paginated)
|
||||
.get('/', async ({ user, query, set }) => {
|
||||
try {
|
||||
const units = await getMemoryMessages(user.userId, query)
|
||||
return {
|
||||
success: true,
|
||||
data: units,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch messages',
|
||||
}
|
||||
}
|
||||
}, GetMemoryMessageModel)
|
||||
// Get messages by date range for current user
|
||||
.get('/filter', async ({ user, query, set }) => {
|
||||
try {
|
||||
const units = await getMemoryMessagesFilter(user.userId, query)
|
||||
return {
|
||||
success: true,
|
||||
data: units,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to filter messages',
|
||||
}
|
||||
}
|
||||
}, GetMemoryMessageFilterModel)
|
||||
@@ -2,16 +2,14 @@ import { z } from 'zod'
|
||||
|
||||
export const GetMemoryMessageModel = {
|
||||
query: z.object({
|
||||
limit: z.string().transform(Number).default(10),
|
||||
page: z.string().transform(Number).default(1),
|
||||
userId: z.string(),
|
||||
limit: z.coerce.number().default(10),
|
||||
page: z.coerce.number().default(1),
|
||||
}),
|
||||
}
|
||||
|
||||
export const GetMemoryMessageFilterModel = {
|
||||
query: z.object({
|
||||
from: z.date(),
|
||||
to: z.date(),
|
||||
userId: z.string(),
|
||||
from: z.coerce.date(),
|
||||
to: z.coerce.date(),
|
||||
}),
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import { history } from '@memohome/db/schema'
|
||||
import { eq, desc, and, gte, lte, asc } from 'drizzle-orm'
|
||||
|
||||
export const getMemoryMessages = async (
|
||||
userId: string,
|
||||
query: {
|
||||
limit: number
|
||||
page: number
|
||||
userId: string
|
||||
}
|
||||
) => {
|
||||
const { limit, page, userId } = query
|
||||
const { limit, page } = query
|
||||
const results = await db
|
||||
.select()
|
||||
.from(history)
|
||||
@@ -22,13 +22,13 @@ export const getMemoryMessages = async (
|
||||
}
|
||||
|
||||
export const getMemoryMessagesFilter = async (
|
||||
userId: string,
|
||||
query: {
|
||||
from: Date
|
||||
to: Date
|
||||
userId: string
|
||||
}
|
||||
) => {
|
||||
const { from, to, userId } = query
|
||||
const { from, to } = query
|
||||
const results = await db
|
||||
.select()
|
||||
.from(history)
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// MemoryUnit schema
|
||||
const MemoryUnitSchema = z.object({
|
||||
messages: z.array(z.object()),
|
||||
// MemoryUnit schema (without user field, will be added from auth)
|
||||
const MemoryUnitBodySchema = z.object({
|
||||
messages: z.array(z.any()),
|
||||
timestamp: z.coerce.date(),
|
||||
user: z.string(),
|
||||
})
|
||||
|
||||
export const AddMemoryModel = {
|
||||
body: MemoryUnitSchema,
|
||||
body: MemoryUnitBodySchema,
|
||||
}
|
||||
|
||||
export const SearchMemoryModel = {
|
||||
query: z.object({
|
||||
query: z.string().min(1, 'Search query is required'),
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
import Elysia from 'elysia'
|
||||
import {
|
||||
GetSettingsModel,
|
||||
CreateSettingsModel,
|
||||
UpdateSettingsModel,
|
||||
} from './model'
|
||||
import {
|
||||
getSettings,
|
||||
createSettings,
|
||||
updateSettings,
|
||||
upsertSettings,
|
||||
} from './service'
|
||||
import { bearer } from '@elysiajs/bearer'
|
||||
import { jwt } from '@elysiajs/jwt'
|
||||
import { UpdateSettingsModel } from './model'
|
||||
import { getSettings, upsertSettings } from './service'
|
||||
|
||||
export const settingsModule = new Elysia({
|
||||
prefix: '/settings',
|
||||
})
|
||||
// Get user settings
|
||||
.get('/:userId', async ({ params }) => {
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
exp: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
)
|
||||
.use(bearer())
|
||||
.derive(async ({ bearer, jwt, set }) => {
|
||||
if (!bearer) {
|
||||
set.status = 401
|
||||
throw new Error('No bearer token provided')
|
||||
}
|
||||
|
||||
const payload = await jwt.verify(bearer)
|
||||
|
||||
if (!payload) {
|
||||
set.status = 401
|
||||
throw new Error('Invalid or expired token')
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
userId: payload.userId as string,
|
||||
username: payload.username as string,
|
||||
role: payload.role as string,
|
||||
},
|
||||
}
|
||||
})
|
||||
// Get current user's settings
|
||||
.get('/', async ({ user, set }) => {
|
||||
try {
|
||||
const { userId } = params
|
||||
const userSettings = await getSettings(userId)
|
||||
const userSettings = await getSettings(user.userId)
|
||||
if (!userSettings) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'Settings not found',
|
||||
@@ -30,62 +52,27 @@ export const settingsModule = new Elysia({
|
||||
data: userSettings,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch settings',
|
||||
}
|
||||
}
|
||||
}, GetSettingsModel)
|
||||
// Create new settings
|
||||
.post('/', async ({ body }) => {
|
||||
})
|
||||
// Update or create current user's settings
|
||||
.put('/', async ({ user, body, set }) => {
|
||||
try {
|
||||
const newSettings = await createSettings(body)
|
||||
const result = await upsertSettings(user.userId, body)
|
||||
return {
|
||||
success: true,
|
||||
data: newSettings,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create settings',
|
||||
}
|
||||
}
|
||||
}, CreateSettingsModel)
|
||||
// Update settings
|
||||
.put('/:userId', async ({ params, body }) => {
|
||||
try {
|
||||
const { userId } = params
|
||||
const updatedSettings = await updateSettings(userId, body)
|
||||
if (!updatedSettings) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Settings not found',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: updatedSettings,
|
||||
data: result,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update settings',
|
||||
}
|
||||
}
|
||||
}, UpdateSettingsModel)
|
||||
// Upsert settings (create or update)
|
||||
.patch('/', async ({ body }) => {
|
||||
try {
|
||||
const result = await upsertSettings(body)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to upsert settings',
|
||||
}
|
||||
}
|
||||
}, CreateSettingsModel)
|
||||
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const SettingsSchema = z.object({
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
const SettingsBodySchema = z.object({
|
||||
defaultChatModel: z.string().uuid().nullable().optional(),
|
||||
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
|
||||
defaultSummaryModel: z.string().uuid().nullable().optional(),
|
||||
maxContextLoadTime: z.number().int().min(1).max(1440).optional(), // 1 minute to 24 hours
|
||||
language: z.string().optional(),
|
||||
})
|
||||
|
||||
export type SettingsInput = z.infer<typeof SettingsSchema>
|
||||
|
||||
export const GetSettingsModel = {
|
||||
params: z.object({
|
||||
userId: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
export const CreateSettingsModel = {
|
||||
body: SettingsSchema,
|
||||
}
|
||||
export type SettingsInput = z.infer<typeof SettingsBodySchema>
|
||||
|
||||
export const UpdateSettingsModel = {
|
||||
params: z.object({
|
||||
userId: z.string(),
|
||||
}),
|
||||
body: z.object({
|
||||
defaultChatModel: z.string().uuid().nullable().optional(),
|
||||
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
|
||||
defaultSummaryModel: z.string().uuid().nullable().optional(),
|
||||
}),
|
||||
body: SettingsBodySchema,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,51 +11,38 @@ export const getSettings = async (userId: string) => {
|
||||
return result
|
||||
}
|
||||
|
||||
export const createSettings = async (data: SettingsInput) => {
|
||||
const [newSettings] = await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: data.userId,
|
||||
defaultChatModel: data.defaultChatModel || null,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
|
||||
defaultSummaryModel: data.defaultSummaryModel || null,
|
||||
})
|
||||
.returning()
|
||||
return newSettings
|
||||
export const upsertSettings = async (userId: string, data: SettingsInput) => {
|
||||
const updateData: Record<string, unknown> = {}
|
||||
|
||||
if (data.defaultChatModel !== undefined) {
|
||||
updateData.defaultChatModel = data.defaultChatModel
|
||||
}
|
||||
if (data.defaultEmbeddingModel !== undefined) {
|
||||
updateData.defaultEmbeddingModel = data.defaultEmbeddingModel
|
||||
}
|
||||
if (data.defaultSummaryModel !== undefined) {
|
||||
updateData.defaultSummaryModel = data.defaultSummaryModel
|
||||
}
|
||||
if (data.maxContextLoadTime !== undefined) {
|
||||
updateData.maxContextLoadTime = data.maxContextLoadTime
|
||||
}
|
||||
if (data.language !== undefined) {
|
||||
updateData.language = data.language
|
||||
}
|
||||
|
||||
export const updateSettings = async (
|
||||
userId: string,
|
||||
data: Partial<Omit<SettingsInput, 'userId'>>
|
||||
) => {
|
||||
const [updatedSettings] = await db
|
||||
.update(settings)
|
||||
.set({
|
||||
defaultChatModel: data.defaultChatModel,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel,
|
||||
defaultSummaryModel: data.defaultSummaryModel,
|
||||
})
|
||||
.where(eq(settings.userId, userId))
|
||||
.returning()
|
||||
return updatedSettings
|
||||
}
|
||||
|
||||
export const upsertSettings = async (data: SettingsInput) => {
|
||||
const [result] = await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: data.userId,
|
||||
userId: userId,
|
||||
defaultChatModel: data.defaultChatModel || null,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
|
||||
defaultSummaryModel: data.defaultSummaryModel || null,
|
||||
maxContextLoadTime: data.maxContextLoadTime || 60,
|
||||
language: data.language || 'Same as user input',
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: settings.userId,
|
||||
set: {
|
||||
defaultChatModel: data.defaultChatModel,
|
||||
defaultEmbeddingModel: data.defaultEmbeddingModel,
|
||||
defaultSummaryModel: data.defaultSummaryModel,
|
||||
},
|
||||
set: updateData,
|
||||
})
|
||||
.returning()
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import Elysia from 'elysia'
|
||||
import { adminMiddleware } from '../../middlewares'
|
||||
import {
|
||||
GetUserByIdModel,
|
||||
CreateUserModel,
|
||||
UpdateUserModel,
|
||||
DeleteUserModel,
|
||||
UpdatePasswordModel,
|
||||
} from './model'
|
||||
import {
|
||||
getUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
updateUserPassword,
|
||||
} from './service'
|
||||
|
||||
export const userModule = new Elysia({
|
||||
prefix: '/user',
|
||||
})
|
||||
// 使用管理员中间件保护所有路由
|
||||
.use(adminMiddleware)
|
||||
// Get all users
|
||||
.get('/', async () => {
|
||||
try {
|
||||
const userList = await getUsers()
|
||||
return {
|
||||
success: true,
|
||||
data: userList,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch users',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Get user by ID
|
||||
.get('/:id', async ({ params, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const user = await getUserById(id)
|
||||
|
||||
if (!user) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: user,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch user',
|
||||
}
|
||||
}
|
||||
}, GetUserByIdModel)
|
||||
// Create new user
|
||||
.post('/', async ({ body, set }) => {
|
||||
try {
|
||||
const newUser = await createUser(body)
|
||||
set.status = 201
|
||||
return {
|
||||
success: true,
|
||||
data: newUser,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && (
|
||||
error.message.includes('already exists')
|
||||
)) {
|
||||
set.status = 409
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create user',
|
||||
}
|
||||
}
|
||||
}, CreateUserModel)
|
||||
// Update user
|
||||
.put('/:id', async ({ params, body, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const updatedUser = await updateUser(id, body)
|
||||
|
||||
if (!updatedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedUser,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('already exists')) {
|
||||
set.status = 409
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update user',
|
||||
}
|
||||
}
|
||||
}, UpdateUserModel)
|
||||
// Delete user
|
||||
.delete('/:id', async ({ params, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const deletedUser = await deleteUser(id)
|
||||
|
||||
if (!deletedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: deletedUser,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete user',
|
||||
}
|
||||
}
|
||||
}, DeleteUserModel)
|
||||
// Update user password
|
||||
.patch('/:id/password', async ({ params, body, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const updatedUser = await updateUserPassword(id, body.password)
|
||||
|
||||
if (!updatedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedUser,
|
||||
message: 'Password updated successfully',
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update password',
|
||||
}
|
||||
}
|
||||
}, UpdatePasswordModel)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// 用户角色枚举
|
||||
const UserRoleSchema = z.enum(['admin', 'member'])
|
||||
|
||||
// 创建用户的 Schema
|
||||
const CreateUserSchema = z.object({
|
||||
username: z.string().min(3, 'Username must be at least 3 characters').max(50),
|
||||
email: z.string().email('Invalid email format').optional(),
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
role: UserRoleSchema.default('member'),
|
||||
displayName: z.string().optional(),
|
||||
avatarUrl: z.string().url('Invalid URL format').optional(),
|
||||
})
|
||||
|
||||
// 更新用户的 Schema
|
||||
const UpdateUserSchema = z.object({
|
||||
email: z.string().email('Invalid email format').optional(),
|
||||
role: UserRoleSchema.optional(),
|
||||
displayName: z.string().optional(),
|
||||
avatarUrl: z.string().url('Invalid URL format').optional(),
|
||||
isActive: z.enum(['true', 'false']).optional(),
|
||||
})
|
||||
|
||||
// 更新密码的 Schema
|
||||
const UpdatePasswordSchema = z.object({
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
})
|
||||
|
||||
export type CreateUserInput = z.infer<typeof CreateUserSchema>
|
||||
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>
|
||||
export type UpdatePasswordInput = z.infer<typeof UpdatePasswordSchema>
|
||||
|
||||
export const GetUserByIdModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid user ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const CreateUserModel = {
|
||||
body: CreateUserSchema,
|
||||
}
|
||||
|
||||
export const UpdateUserModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid user ID format'),
|
||||
}),
|
||||
body: UpdateUserSchema,
|
||||
}
|
||||
|
||||
export const DeleteUserModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid user ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const UpdatePasswordModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid user ID format'),
|
||||
}),
|
||||
body: UpdatePasswordSchema,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { db } from '@memohome/db'
|
||||
import { users, settings } from '@memohome/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { CreateUserInput, UpdateUserInput } from './model'
|
||||
|
||||
/**
|
||||
* 获取所有用户列表
|
||||
*/
|
||||
export const getUsers = async () => {
|
||||
const userList = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
displayName: users.displayName,
|
||||
avatarUrl: users.avatarUrl,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
lastLoginAt: users.lastLoginAt,
|
||||
})
|
||||
.from(users)
|
||||
.orderBy(users.createdAt)
|
||||
|
||||
return userList
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取用户
|
||||
*/
|
||||
export const getUserById = async (id: string) => {
|
||||
const [user] = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
displayName: users.displayName,
|
||||
avatarUrl: users.avatarUrl,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
lastLoginAt: users.lastLoginAt,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, id))
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*/
|
||||
export const createUser = async (data: CreateUserInput) => {
|
||||
// 检查用户名是否已存在
|
||||
const [existingUser] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, data.username))
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('Username already exists')
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在(如果提供了邮箱)
|
||||
if (data.email) {
|
||||
const [existingEmail] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, data.email))
|
||||
|
||||
if (existingEmail) {
|
||||
throw new Error('Email already exists')
|
||||
}
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const passwordHash = await Bun.password.hash(data.password)
|
||||
|
||||
// 创建用户
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
username: data.username,
|
||||
email: data.email || null,
|
||||
passwordHash,
|
||||
role: data.role || 'member',
|
||||
displayName: data.displayName || null,
|
||||
avatarUrl: data.avatarUrl || null,
|
||||
})
|
||||
.returning({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
displayName: users.displayName,
|
||||
avatarUrl: users.avatarUrl,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
})
|
||||
|
||||
// 自动创建用户的 settings 条目(使用默认值)
|
||||
await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: newUser.id,
|
||||
defaultChatModel: null,
|
||||
defaultEmbeddingModel: null,
|
||||
defaultSummaryModel: null,
|
||||
maxContextLoadTime: 60,
|
||||
language: 'Same as user input',
|
||||
})
|
||||
|
||||
return newUser
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
export const updateUser = async (id: string, data: UpdateUserInput) => {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await getUserById(id)
|
||||
if (!existingUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 如果更新邮箱,检查邮箱是否已被其他用户使用
|
||||
if (data.email) {
|
||||
const [emailUser] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, data.email))
|
||||
|
||||
if (emailUser && emailUser.id !== id) {
|
||||
throw new Error('Email already exists')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
const [updatedUser] = await db
|
||||
.update(users)
|
||||
.set({
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
.returning({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
displayName: users.displayName,
|
||||
avatarUrl: users.avatarUrl,
|
||||
isActive: users.isActive,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
lastLoginAt: users.lastLoginAt,
|
||||
})
|
||||
|
||||
return updatedUser
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
export const deleteUser = async (id: string) => {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await getUserById(id)
|
||||
if (!existingUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [deletedUser] = await db
|
||||
.delete(users)
|
||||
.where(eq(users.id, id))
|
||||
.returning({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
})
|
||||
|
||||
return deletedUser
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户密码
|
||||
*/
|
||||
export const updateUserPassword = async (id: string, password: string) => {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await getUserById(id)
|
||||
if (!existingUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const passwordHash = await Bun.password.hash(password)
|
||||
|
||||
// 更新密码
|
||||
const [updatedUser] = await db
|
||||
.update(users)
|
||||
.set({
|
||||
passwordHash,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
.returning({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
})
|
||||
|
||||
return updatedUser
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @memohome/db
|
||||
|
||||
Database package for memohome project using Drizzle ORM and PostgreSQL.
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Tables
|
||||
|
||||
- **model**: AI model configurations
|
||||
- **history**: Chat history records
|
||||
- **settings**: User settings and preferences
|
||||
- **users**: User accounts with authentication
|
||||
|
||||
See [USERS_SCHEMA.md](./USERS_SCHEMA.md) for detailed user table documentation.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Initialize Database
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# 用户表设计
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### users 表(用户表)
|
||||
|
||||
存储用户的基本信息和登录凭证。
|
||||
|
||||
| 字段名 | 类型 | 说明 | 约束 |
|
||||
|--------|------|------|------|
|
||||
| id | uuid | 用户唯一标识 | 主键,自动生成 |
|
||||
| username | text | 用户名 | 非空,唯一 |
|
||||
| email | text | 邮箱地址 | 唯一(可选)|
|
||||
| password_hash | text | 密码哈希值 | 非空 |
|
||||
| role | user_role | 用户角色 | 枚举:admin/member,默认 member |
|
||||
| display_name | text | 显示名称 | 可选 |
|
||||
| avatar_url | text | 头像地址 | 可选 |
|
||||
| is_active | text | 账户状态 | 默认 true |
|
||||
| created_at | timestamp | 创建时间 | 自动生成 |
|
||||
| updated_at | timestamp | 更新时间 | 自动生成 |
|
||||
| last_login_at | timestamp | 最后登录时间 | 可选 |
|
||||
|
||||
## 角色说明
|
||||
|
||||
系统定义了两种角色:
|
||||
- **admin**: 管理员,拥有完整权限
|
||||
- **member**: 普通成员,拥有基础权限
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 生成迁移文件
|
||||
|
||||
```bash
|
||||
cd packages/db
|
||||
pnpm run generate
|
||||
```
|
||||
|
||||
### 2. 执行数据库迁移
|
||||
|
||||
```bash
|
||||
pnpm run push
|
||||
```
|
||||
|
||||
### 3. 在代码中使用
|
||||
|
||||
```typescript
|
||||
import { users, userRoleEnum } from '@memohome/db/schema'
|
||||
import { db } from '@memohome/db'
|
||||
|
||||
// 创建用户
|
||||
const password = 'password123'
|
||||
const passwordHash = await Bun.password.hash(password) // 使用 Bun 内置的密码加密
|
||||
|
||||
const newUser = await db.insert(users).values({
|
||||
username: 'john_doe',
|
||||
email: 'john@example.com',
|
||||
passwordHash: passwordHash,
|
||||
role: 'member',
|
||||
displayName: 'John Doe',
|
||||
}).returning()
|
||||
|
||||
// 或者使用辅助函数
|
||||
import { createUser } from '@memohome/db/src/user-helpers'
|
||||
|
||||
const user = await createUser({
|
||||
username: 'john_doe',
|
||||
email: 'john@example.com',
|
||||
passwordHash: await Bun.password.hash('password123'),
|
||||
role: 'member',
|
||||
displayName: 'John Doe',
|
||||
})
|
||||
```
|
||||
|
||||
## Root 用户
|
||||
|
||||
系统支持通过环境变量配置超级用户(Root User):
|
||||
|
||||
```bash
|
||||
ROOT_USER=admin
|
||||
ROOT_USER_PASSWORD=your_secure_password
|
||||
```
|
||||
|
||||
Root 用户特点:
|
||||
- 不存储在数据库中
|
||||
- 通过环境变量直接验证
|
||||
- 始终拥有 admin 角色
|
||||
- 用于系统初始化和紧急管理
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **密码存储**:
|
||||
- 永远不要存储明文密码
|
||||
- 使用 Bun.password.hash 进行密码加密
|
||||
- Bun 内置的密码加密基于 bcrypt 算法,安全可靠
|
||||
|
||||
2. **身份验证**:
|
||||
- 使用 JWT 进行用户身份验证
|
||||
- 设置合理的过期时间(默认 7 天)
|
||||
- 在请求头中携带 `Authorization: Bearer <token>`
|
||||
|
||||
3. **权限控制**:
|
||||
- 在业务层实现基于角色的访问控制(RBAC)
|
||||
- 验证用户权限后再执行敏感操作
|
||||
- 记录重要操作的审计日志
|
||||
|
||||
4. **环境变量**:
|
||||
- 设置强密码作为 ROOT_USER_PASSWORD
|
||||
- 在生产环境中设置自定义的 JWT_SECRET
|
||||
- 不要将敏感信息提交到代码仓库
|
||||
|
||||
## 扩展建议
|
||||
|
||||
如果需要更复杂的功能,可以考虑:
|
||||
|
||||
1. 添加用户注册功能
|
||||
2. 添加密码重置功能
|
||||
3. 添加邮箱验证功能
|
||||
4. 实现刷新令牌机制
|
||||
5. 添加用户会话管理
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './history'
|
||||
export * from './model'
|
||||
export * from './settings'
|
||||
export * from './users'
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
|
||||
import { pgTable, text, uuid, integer } from 'drizzle-orm/pg-core'
|
||||
import { model } from './model'
|
||||
|
||||
export const settings = pgTable('settings', {
|
||||
@@ -6,4 +6,7 @@ export const settings = pgTable('settings', {
|
||||
defaultChatModel: uuid('default_chat_model').references(() => model.id),
|
||||
defaultEmbeddingModel: uuid('default_embedding_model').references(() => model.id),
|
||||
defaultSummaryModel: uuid('default_summary_model').references(() => model.id),
|
||||
// Agent settings
|
||||
maxContextLoadTime: integer('max_context_load_time').default(60), // minutes
|
||||
language: text('language').default('Same as user input'),
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { db } from './index'
|
||||
import { users } from './users'
|
||||
|
||||
/**
|
||||
* 用户操作辅助函数
|
||||
* 这些函数提供了常用的用户、用户组操作
|
||||
*/
|
||||
|
||||
// ============= 用户操作 =============
|
||||
|
||||
/**
|
||||
* 根据用户名查找用户
|
||||
*/
|
||||
export async function findUserByUsername(username: string) {
|
||||
const result = await db.select().from(users).where(eq(users.username, username))
|
||||
return result[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱查找用户
|
||||
*/
|
||||
export async function findUserByEmail(email: string) {
|
||||
const result = await db.select().from(users).where(eq(users.email, email))
|
||||
return result[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找用户
|
||||
*/
|
||||
export async function findUserById(id: string) {
|
||||
const result = await db.select().from(users).where(eq(users.id, id))
|
||||
return result[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*/
|
||||
export async function createUser(data: {
|
||||
username: string
|
||||
email?: string
|
||||
passwordHash: string
|
||||
role?: 'admin' | 'member'
|
||||
displayName?: string
|
||||
avatarUrl?: string
|
||||
}) {
|
||||
const result = await db.insert(users).values({
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
passwordHash: data.passwordHash,
|
||||
role: data.role || 'member',
|
||||
displayName: data.displayName,
|
||||
avatarUrl: data.avatarUrl,
|
||||
}).returning()
|
||||
|
||||
return result[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
export async function updateUser(id: string, data: {
|
||||
displayName?: string
|
||||
avatarUrl?: string
|
||||
email?: string
|
||||
role?: 'admin' | 'member'
|
||||
isActive?: string
|
||||
}) {
|
||||
const result = await db
|
||||
.update(users)
|
||||
.set({
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
.returning()
|
||||
|
||||
return result[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户密码
|
||||
*/
|
||||
export async function updateUserPassword(id: string, passwordHash: string) {
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
passwordHash,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新最后登录时间
|
||||
*/
|
||||
export async function updateLastLogin(id: string) {
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
lastLoginAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
export async function deleteUser(id: string) {
|
||||
await db.delete(users).where(eq(users.id, id))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { pgTable, pgEnum, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||
|
||||
// 定义用户角色枚举
|
||||
export const userRoleEnum = pgEnum('user_role', ['admin', 'member'])
|
||||
|
||||
// 用户表
|
||||
export const users = pgTable('users', {
|
||||
// 主键ID
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
// 用户名(唯一)
|
||||
username: text('username').notNull().unique(),
|
||||
|
||||
// 邮箱(可选,唯一)
|
||||
email: text('email').unique(),
|
||||
|
||||
// 密码哈希值(使用 bcrypt 或其他加密方式)
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
|
||||
// 用户角色
|
||||
role: userRoleEnum('role').notNull().default('member'),
|
||||
|
||||
// 显示名称
|
||||
displayName: text('display_name'),
|
||||
|
||||
// 头像 URL
|
||||
avatarUrl: text('avatar_url'),
|
||||
|
||||
// 账户状态(是否激活)
|
||||
isActive: text('is_active').notNull().default('true'),
|
||||
|
||||
// 创建时间
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
|
||||
// 更新时间
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
|
||||
// 最后登录时间
|
||||
lastLoginAt: timestamp('last_login_at'),
|
||||
})
|
||||
Generated
+33
@@ -90,6 +90,9 @@ importers:
|
||||
|
||||
packages/api:
|
||||
dependencies:
|
||||
'@elysiajs/bearer':
|
||||
specifier: ^1.1.4
|
||||
version: 1.4.2(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||
'@elysiajs/cors':
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||
@@ -99,6 +102,9 @@ importers:
|
||||
'@elysiajs/eden':
|
||||
specifier: ^1.4.6
|
||||
version: 1.4.6(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||
'@elysiajs/jwt':
|
||||
specifier: ^1.2.0
|
||||
version: 1.4.0(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||
'@memohome/agent':
|
||||
specifier: workspace:*
|
||||
version: link:../agent
|
||||
@@ -260,6 +266,9 @@ importers:
|
||||
|
||||
packages/web:
|
||||
dependencies:
|
||||
'@memohome/api':
|
||||
specifier: workspace:*
|
||||
version: link:../api
|
||||
'@memohome/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
@@ -557,6 +566,11 @@ packages:
|
||||
'@drizzle-team/brocli@0.10.2':
|
||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||
|
||||
'@elysiajs/bearer@1.4.2':
|
||||
resolution: {integrity: sha512-MK2aCFqnFMqMNSa1e/A6+Ow5uNl5LpKd8K4lCB2LIsyDrI6juxOUHAgqq+esgdSoh3urD1UIMqFC//TsqCQViA==}
|
||||
peerDependencies:
|
||||
elysia: '>= 1.4.3'
|
||||
|
||||
'@elysiajs/cors@1.4.1':
|
||||
resolution: {integrity: sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ==}
|
||||
peerDependencies:
|
||||
@@ -572,6 +586,11 @@ packages:
|
||||
peerDependencies:
|
||||
elysia: '>=1.4.19'
|
||||
|
||||
'@elysiajs/jwt@1.4.0':
|
||||
resolution: {integrity: sha512-Z0PvZhQxdDeKZ8HslXzDoXXD83NKExNPmoiAPki3nI2Xvh5wtUrBH+zWOD17yP14IbRo8fxGj3L25MRCAPsgPA==}
|
||||
peerDependencies:
|
||||
elysia: '>= 1.4.0'
|
||||
|
||||
'@esbuild-kit/core-utils@3.3.2':
|
||||
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
||||
deprecated: 'Merged into tsx: https://tsx.is'
|
||||
@@ -2924,6 +2943,9 @@ packages:
|
||||
jju@1.4.0:
|
||||
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
|
||||
|
||||
jose@6.1.3:
|
||||
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
||||
|
||||
js-tiktoken@1.0.21:
|
||||
resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
|
||||
|
||||
@@ -4648,6 +4670,10 @@ snapshots:
|
||||
|
||||
'@drizzle-team/brocli@0.10.2': {}
|
||||
|
||||
'@elysiajs/bearer@1.4.2(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
||||
|
||||
'@elysiajs/cors@1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
||||
@@ -4661,6 +4687,11 @@ snapshots:
|
||||
dependencies:
|
||||
elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
||||
|
||||
'@elysiajs/jwt@1.4.0(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
||||
jose: 6.1.3
|
||||
|
||||
'@esbuild-kit/core-utils@3.3.2':
|
||||
dependencies:
|
||||
esbuild: 0.18.20
|
||||
@@ -6960,6 +6991,8 @@ snapshots:
|
||||
|
||||
jju@1.4.0: {}
|
||||
|
||||
jose@6.1.3: {}
|
||||
|
||||
js-tiktoken@1.0.21:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
|
||||
Reference in New Issue
Block a user