@myassis/gateway 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -0
- package/dist/.env +6 -0
- package/dist/api/index.js +182 -0
- package/dist/config/index.js +41 -0
- package/dist/index.js +183 -0
- package/dist/middleware/auth.js +53 -0
- package/dist/middleware/errorHandler.js +20 -0
- package/dist/routes/agent.js +513 -0
- package/dist/routes/auth.js +172 -0
- package/dist/routes/chat.js +45 -0
- package/dist/routes/config.js +21 -0
- package/dist/routes/models.js +123 -0
- package/dist/routes/service.js +240 -0
- package/dist/routes/settings.js +101 -0
- package/dist/routes/skillHub.js +126 -0
- package/dist/routes/skills.js +159 -0
- package/dist/routes/tasks.js +149 -0
- package/dist/routes/upload.js +129 -0
- package/dist/routes/version.js +66 -0
- package/dist/services/HMSPushService.js +24 -0
- package/dist/services/LocalTaskService.js +223 -0
- package/dist/services/NotificationService.js +242 -0
- package/dist/services/ServiceManager.js +348 -0
- package/dist/services/TaskSchedulerService.js +195 -0
- package/dist/services/TaskService.js +240 -0
- package/dist/services/WebSocketService.js +236 -0
- package/dist/services/agent/Agent.js +120 -0
- package/dist/services/agent/AgentManager.js +265 -0
- package/dist/services/agent/AgentStore.js +73 -0
- package/dist/services/dataService.js +293 -0
- package/dist/services/index.js +15 -0
- package/dist/services/llm/LLMClient.js +724 -0
- package/dist/services/memory/MemoryManager.js +117 -0
- package/dist/services/model/ModelCapabilities.js +141 -0
- package/dist/services/model/index.js +4 -0
- package/dist/services/models.js +16 -0
- package/dist/services/session/MigrationManager.js +176 -0
- package/dist/services/session/Session.js +733 -0
- package/dist/services/session/SessionManager.js +255 -0
- package/dist/services/session/SessionStore.js +186 -0
- package/dist/services/session/index.js +3 -0
- package/dist/services/skills.js +34 -0
- package/dist/services/systemPrompt.js +150 -0
- package/dist/services/task/PushTokenStore.js +124 -0
- package/dist/services/task/TaskStore.js +143 -0
- package/dist/services/tools/calculator.js +27 -0
- package/dist/services/tools/edit.js +318 -0
- package/dist/services/tools/exec.js +119 -0
- package/dist/services/tools/fetch.js +155 -0
- package/dist/services/tools/file.js +315 -0
- package/dist/services/tools/index.js +48 -0
- package/dist/services/tools/keyboard.js +145 -0
- package/dist/services/tools/model.js +86 -0
- package/dist/services/tools/mouse.js +55 -0
- package/dist/services/tools/screenshot.js +19 -0
- package/dist/services/tools/search.js +53 -0
- package/dist/services/tools/skill.js +108 -0
- package/dist/services/tools/task.js +110 -0
- package/dist/services/tools/types.js +1 -0
- package/dist/services/tools/webFetch.js +34 -0
- package/dist/stores/authStore.js +178 -0
- package/dist/stores/index.js +6 -0
- package/dist/stores/memoryStore.js +191 -0
- package/dist/stores/persistStore.js +317 -0
- package/package.json +94 -0
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# 我的助手 Gateway 服务
|
|
2
|
+
|
|
3
|
+
> 我的助手 项目的核心API网关服务,基于Express + TypeScript构建,提供统一的API接口、WebSocket实时通信、权限控制等能力。
|
|
4
|
+
|
|
5
|
+
## ✨ 核心特性
|
|
6
|
+
|
|
7
|
+
- 🚀 **高性能**:基于Express框架,轻量高效,支持高并发
|
|
8
|
+
- 🔒 **安全可靠**:内置Helmet安全防护、CORS内网访问控制、JWT权限校验
|
|
9
|
+
- 📡 **实时通信**:集成WebSocket服务,支持消息推送、实时通知
|
|
10
|
+
- 🎯 **模块化设计**:路由、中间件、服务层分离,易于扩展和维护
|
|
11
|
+
- 📦 **开箱即用**:内置用户认证、模型管理、技能管理、聊天、任务等核心API
|
|
12
|
+
- ⚡ **开发友好**:TypeScript类型支持,热重载,完善的错误处理机制
|
|
13
|
+
|
|
14
|
+
## 🛠️ 技术栈
|
|
15
|
+
|
|
16
|
+
| 技术 | 版本 | 用途 |
|
|
17
|
+
|------|------|------|
|
|
18
|
+
| Node.js | >=18.x | 运行环境 |
|
|
19
|
+
| Express | 4.x | Web框架 |
|
|
20
|
+
| TypeScript | 5.x | 开发语言 |
|
|
21
|
+
| WebSocket | - | 实时通信 |
|
|
22
|
+
| SQLite | - | 本地数据存储 |
|
|
23
|
+
| pnpm | 8.x | 包管理工具 |
|
|
24
|
+
|
|
25
|
+
## 🚀 快速开始
|
|
26
|
+
|
|
27
|
+
### 环境要求
|
|
28
|
+
|
|
29
|
+
- Node.js >= 18.0.0
|
|
30
|
+
- pnpm >= 8.0.0
|
|
31
|
+
|
|
32
|
+
### 安装依赖
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 进入gateway目录
|
|
36
|
+
cd gateway
|
|
37
|
+
|
|
38
|
+
# 安装依赖
|
|
39
|
+
pnpm install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 开发模式运行
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
服务启动后默认运行在 `http://localhost:3100`
|
|
49
|
+
|
|
50
|
+
### 生产构建
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm build
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 生产运行
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pnpm start
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 📁 项目结构
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
gateway/
|
|
66
|
+
├── src/
|
|
67
|
+
│ ├── api/ # 第三方API调用封装
|
|
68
|
+
│ ├── config/ # 配置文件
|
|
69
|
+
│ ├── middleware/ # 中间件(错误处理、权限校验等)
|
|
70
|
+
│ ├── routes/ # 路由定义
|
|
71
|
+
│ │ ├── auth.js # 用户认证相关接口
|
|
72
|
+
│ │ ├── agent.js # AI代理相关接口
|
|
73
|
+
│ │ ├── models.js # 模型管理接口
|
|
74
|
+
│ │ ├── skills.js # 技能管理接口
|
|
75
|
+
│ │ ├── skillHub.js # 技能市场接口
|
|
76
|
+
│ │ ├── chat.js # 聊天相关接口
|
|
77
|
+
│ │ ├── config.js # 配置相关接口
|
|
78
|
+
│ │ ├── settings.js # 设置相关接口
|
|
79
|
+
│ │ ├── tasks.js # 任务管理接口
|
|
80
|
+
│ │ └── upload.js # 文件上传接口
|
|
81
|
+
│ ├── services/ # 业务逻辑层
|
|
82
|
+
│ │ └── WebSocketService.js # WebSocket服务
|
|
83
|
+
│ ├── stores/ # 数据存储层
|
|
84
|
+
│ ├── utils/ # 工具函数
|
|
85
|
+
│ └── index.ts # 服务入口文件
|
|
86
|
+
├── migrations/ # 数据库迁移文件
|
|
87
|
+
├── data/ # 运行时数据存储目录
|
|
88
|
+
├── dist/ # 构建输出目录
|
|
89
|
+
├── package.json
|
|
90
|
+
├── tsconfig.json
|
|
91
|
+
└── .npmrc
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 📡 API 接口
|
|
95
|
+
|
|
96
|
+
所有接口都以 `/api/v1` 为前缀:
|
|
97
|
+
|
|
98
|
+
| 接口前缀 | 说明 |
|
|
99
|
+
|----------|------|
|
|
100
|
+
| `/api/v1/auth` | 用户登录、登出、信息查询等认证相关接口 |
|
|
101
|
+
| `/api/v1/agent` | AI代理执行、工具调用相关接口 |
|
|
102
|
+
| `/api/v1/models` | 大语言模型的增删改查、配置管理 |
|
|
103
|
+
| `/api/v1/skills` | 本地技能的安装、卸载、运行管理 |
|
|
104
|
+
| `/api/v1/skill-hubs` | 技能市场搜索、安装、更新 |
|
|
105
|
+
| `/api/v1/chats` | 聊天会话、消息管理 |
|
|
106
|
+
| `/api/v1/config` | 系统配置获取、更新 |
|
|
107
|
+
| `/api/v1/settings` | 用户设置管理 |
|
|
108
|
+
| `/api/v1/tasks` | 定时任务、后台任务管理 |
|
|
109
|
+
| `/api/v1/upload` | 文件上传、资源管理 |
|
|
110
|
+
|
|
111
|
+
### 健康检查接口
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
GET /health
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
返回示例:
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"status": "ok",
|
|
121
|
+
"service": "gateway",
|
|
122
|
+
"version": "2.0.0",
|
|
123
|
+
"wsOnline": 1
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## ⚙️ 配置说明
|
|
128
|
+
|
|
129
|
+
### CORS配置
|
|
130
|
+
默认仅允许内网地址访问,支持的内网地址范围:
|
|
131
|
+
- localhost
|
|
132
|
+
- 127.x.x.x
|
|
133
|
+
- 10.x.x.x
|
|
134
|
+
- 172.16.x.x ~ 172.31.x.x
|
|
135
|
+
- 192.168.x.x
|
|
136
|
+
- *.local 域名
|
|
137
|
+
|
|
138
|
+
### 端口配置
|
|
139
|
+
默认端口为3100,可以通过修改 `src/config/index.js` 中的 `appConfig.port` 调整。
|
|
140
|
+
|
|
141
|
+
### 上传限制
|
|
142
|
+
请求体大小限制为10MB,支持大文件上传。
|
|
143
|
+
|
|
144
|
+
## 🧩 WebSocket 服务
|
|
145
|
+
|
|
146
|
+
服务启动时自动挂载WebSocket服务,支持:
|
|
147
|
+
- 实时消息推送
|
|
148
|
+
- 任务进度通知
|
|
149
|
+
- AI代理执行状态同步
|
|
150
|
+
- 系统事件广播
|
|
151
|
+
|
|
152
|
+
连接地址:`ws://localhost:3100`
|
|
153
|
+
|
|
154
|
+
## 🔧 开发指南
|
|
155
|
+
|
|
156
|
+
### 新增API接口
|
|
157
|
+
|
|
158
|
+
1. 在 `src/routes/` 下创建对应的路由文件
|
|
159
|
+
2. 在 `src/index.ts` 中注册路由:
|
|
160
|
+
```typescript
|
|
161
|
+
import yourRoutes from './routes/yourRoutes.js';
|
|
162
|
+
app.use('/api/v1/your-path', yourRoutes);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 新增业务逻辑
|
|
166
|
+
|
|
167
|
+
1. 在 `src/services/` 下创建对应的服务类
|
|
168
|
+
2. 在路由中引入并调用服务方法
|
|
169
|
+
|
|
170
|
+
### 数据库迁移
|
|
171
|
+
|
|
172
|
+
新增数据模型时,在 `migrations/` 目录下添加迁移文件,启动时自动执行迁移。
|
|
173
|
+
|
|
174
|
+
## 📝 常见问题
|
|
175
|
+
|
|
176
|
+
### 端口被占用
|
|
177
|
+
修改 `src/config/index.js` 中的端口配置,或者杀掉占用3100端口的进程:
|
|
178
|
+
```bash
|
|
179
|
+
npx kill-port 3100
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 跨域问题
|
|
183
|
+
默认仅允许内网访问,如果需要开放外网访问,修改 `src/index.ts` 中的CORS配置。
|
|
184
|
+
|
|
185
|
+
### 数据丢失
|
|
186
|
+
所有运行时数据都保存在 `data/` 目录下,备份该目录即可完整备份所有数据。
|
|
187
|
+
|
|
188
|
+
## 📄 许可证
|
|
189
|
+
|
|
190
|
+
MIT License
|
|
191
|
+
|
|
192
|
+
## 🤝 贡献
|
|
193
|
+
|
|
194
|
+
欢迎提交Issue和Pull Request来改进这个项目!
|
package/dist/.env
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway 统一 API 调用模块
|
|
3
|
+
* 负责与后端 Server 通信
|
|
4
|
+
* 自动从 authStore 获取 Token 并注入到请求头
|
|
5
|
+
*
|
|
6
|
+
* 服务端路由结构:
|
|
7
|
+
* - /api/v1/auth/* - 认证路由
|
|
8
|
+
* - /api/v1/data/* - 用户数据路由 (models, skills, tasks, settings)
|
|
9
|
+
* - /api/v1/skill-hubs/* - 技能库路由
|
|
10
|
+
*/
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
import { authStore } from '../stores';
|
|
13
|
+
// Server 服务地址(从环境变量读取)
|
|
14
|
+
const SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:3000';
|
|
15
|
+
// 签名密钥(用于请求签名)
|
|
16
|
+
const SIGN_KEY = process.env.API_SIGN_KEY || 'gateway-secret-key';
|
|
17
|
+
// API 响应错误类
|
|
18
|
+
export class ApiError extends Error {
|
|
19
|
+
status;
|
|
20
|
+
code;
|
|
21
|
+
constructor(message, status = 500, code) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'ApiError';
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// 生成签名
|
|
29
|
+
function generateSignature(params, timestamp) {
|
|
30
|
+
const sortedParams = Object.keys(params)
|
|
31
|
+
.sort()
|
|
32
|
+
.map(key => `${key}=${params[key]}`)
|
|
33
|
+
.join('&');
|
|
34
|
+
const signString = `${sortedParams}×tamp=${timestamp}&key=${SIGN_KEY}`;
|
|
35
|
+
return crypto.createHash('sha256').update(signString).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
// 基础请求方法工厂
|
|
38
|
+
// prefix: 路由前缀,如 '/api/v1/auth', '/api/v1/data', '/api/v1/skill-hubs'
|
|
39
|
+
function createRequest(prefix) {
|
|
40
|
+
return async function (path, options = {}) {
|
|
41
|
+
const { method = 'GET', body, headers = {}, params, skipAuth = false } = options;
|
|
42
|
+
// 构建 URL
|
|
43
|
+
let url = `${SERVER_BASE_URL}${prefix}${path}`;
|
|
44
|
+
// 添加查询参数
|
|
45
|
+
if (params) {
|
|
46
|
+
const queryString = Object.entries(params)
|
|
47
|
+
.filter(([_, v]) => v !== undefined && v !== null)
|
|
48
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
49
|
+
.join('&');
|
|
50
|
+
if (queryString) {
|
|
51
|
+
url += `?${queryString}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 生成时间戳和签名
|
|
55
|
+
const timestamp = Date.now().toString();
|
|
56
|
+
const signParams = {};
|
|
57
|
+
const signature = generateSignature(signParams, timestamp);
|
|
58
|
+
// 构建请求头
|
|
59
|
+
const requestHeaders = {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'X-Gateway-Timestamp': timestamp,
|
|
62
|
+
'X-Gateway-Signature': signature,
|
|
63
|
+
...headers
|
|
64
|
+
};
|
|
65
|
+
// 自动注入 Token(除非明确跳过)
|
|
66
|
+
if (!skipAuth) {
|
|
67
|
+
const token = authStore.getToken();
|
|
68
|
+
if (token) {
|
|
69
|
+
requestHeaders['Authorization'] = `Bearer ${token}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(url, {
|
|
74
|
+
method,
|
|
75
|
+
headers: requestHeaders,
|
|
76
|
+
body: body ? JSON.stringify(body) : undefined
|
|
77
|
+
});
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new ApiError(data.message || data.error || '请求失败', response.status, data.code);
|
|
81
|
+
}
|
|
82
|
+
return data;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof ApiError) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
throw new ApiError('网络请求失败', 500, 'NETWORK_ERROR');
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// 创建不同前缀的请求方法
|
|
93
|
+
const authRequest = createRequest('/api/v1/auth'); // /api/v1/auth/*
|
|
94
|
+
const dataRequest = createRequest('/api/v1/data'); // /api/v1/data/*
|
|
95
|
+
const skillHubRequest = createRequest('/api/v1/skill-hubs'); // /api/v1/skill-hubs/*
|
|
96
|
+
// ============ 认证 API ============
|
|
97
|
+
export const authApi = {
|
|
98
|
+
login: (data) => authRequest('/login', { method: 'POST', body: data, skipAuth: true }),
|
|
99
|
+
register: (data) => authRequest('/register', { method: 'POST', body: data, skipAuth: true }),
|
|
100
|
+
logout: () => authRequest('/logout', { method: 'POST', body: {} }),
|
|
101
|
+
refresh: (refreshToken) => authRequest('/refresh', { method: 'POST', body: { refreshToken }, skipAuth: true }),
|
|
102
|
+
me: () => authRequest('/me'),
|
|
103
|
+
updateProfile: (data) => authRequest('/profile', { method: 'PUT', body: data }),
|
|
104
|
+
changePassword: (data) => authRequest('/password', { method: 'PUT', body: data }),
|
|
105
|
+
deleteAccount: (data, token) => {
|
|
106
|
+
const headers = {};
|
|
107
|
+
if (token)
|
|
108
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
109
|
+
return authRequest('/account/delete', { method: 'POST', body: data, headers });
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
// ============ 技能 API (data) ============
|
|
113
|
+
export const skillsApi = {
|
|
114
|
+
list: () => dataRequest('/skills?brief=true'),
|
|
115
|
+
get: (skillId) => dataRequest(`/skills/${skillId}`),
|
|
116
|
+
install: (data) => dataRequest('/skills', { method: 'POST', body: data }),
|
|
117
|
+
create: (data) => dataRequest('/skills', { method: 'POST', body: data }),
|
|
118
|
+
update: (skillId, data) => dataRequest(`/skills/${skillId}`, { method: 'PUT', body: data }),
|
|
119
|
+
uninstall: (skillId) => dataRequest(`/skills/${skillId}`, { method: 'DELETE' }),
|
|
120
|
+
setApiKey: (skillId, data) => dataRequest(`/skills/${skillId}/api-key`, { method: 'POST', body: data }),
|
|
121
|
+
rate: (skillId, data) => dataRequest(`/skills/${skillId}/rating`, { method: 'POST', body: data }),
|
|
122
|
+
parse: (data) => dataRequest('/skills/parse', { method: 'POST', body: data }),
|
|
123
|
+
};
|
|
124
|
+
// ============ 技能库 API (skill-hubs) ============
|
|
125
|
+
export const skillHubApi = {
|
|
126
|
+
list: (params) => {
|
|
127
|
+
const queryParams = {};
|
|
128
|
+
if (params?.page)
|
|
129
|
+
queryParams.page = String(params.page);
|
|
130
|
+
if (params?.pageSize)
|
|
131
|
+
queryParams.pageSize = String(params.pageSize);
|
|
132
|
+
if (params?.keyword)
|
|
133
|
+
queryParams.keyword = params.keyword;
|
|
134
|
+
if (params?.category)
|
|
135
|
+
queryParams.category = params.category;
|
|
136
|
+
return skillHubRequest('/', { params: queryParams });
|
|
137
|
+
},
|
|
138
|
+
get: (hubId) => skillHubRequest(`/${hubId}`),
|
|
139
|
+
categories: () => skillHubRequest('/categories'),
|
|
140
|
+
preinstalled: () => skillHubRequest('/preinstalled'),
|
|
141
|
+
user: () => skillHubRequest(`/user`),
|
|
142
|
+
checkUsed: (hubId) => skillHubRequest(`/${hubId}/used`),
|
|
143
|
+
rate: (hubId, rating) => skillHubRequest(`/${hubId}/rate`, { method: 'POST', body: { rating } }),
|
|
144
|
+
userRating: (hubId) => skillHubRequest(`/${hubId}/user-rating`),
|
|
145
|
+
install: (hubId) => skillHubRequest(`/${hubId}/install`, { method: 'POST', body: {} }),
|
|
146
|
+
use: (hubId) => skillHubRequest(`/${hubId}/use`, { method: 'POST', body: {} }),
|
|
147
|
+
};
|
|
148
|
+
// ============ 模型 API (data) ============
|
|
149
|
+
export const modelsApi = {
|
|
150
|
+
list: () => dataRequest('/models'),
|
|
151
|
+
get: (modelId) => dataRequest(`/models/${modelId}`),
|
|
152
|
+
create: (data) => dataRequest('/models', { method: 'POST', body: data }),
|
|
153
|
+
update: (modelId, data) => dataRequest(`/models/${modelId}`, { method: 'PUT', body: data }),
|
|
154
|
+
delete: (modelId) => dataRequest(`/models/${modelId}`, { method: 'DELETE' }),
|
|
155
|
+
setPrimary: (modelId) => dataRequest(`/models/${modelId}/primary`, { method: 'POST', body: {} }),
|
|
156
|
+
};
|
|
157
|
+
// ============ 任务 API (data) ============
|
|
158
|
+
export const tasksApi = {
|
|
159
|
+
list: (params) => dataRequest('/tasks', { params }),
|
|
160
|
+
get: (taskId) => dataRequest(`/tasks/${taskId}`),
|
|
161
|
+
create: (data) => dataRequest('/tasks', { method: 'POST', body: data }),
|
|
162
|
+
update: (taskId, data) => dataRequest(`/tasks/${taskId}`, { method: 'PUT', body: data }),
|
|
163
|
+
delete: (taskId) => dataRequest(`/tasks/${taskId}`, { method: 'DELETE' }),
|
|
164
|
+
complete: (taskId) => dataRequest(`/tasks/${taskId}/complete`, { method: 'POST', body: {} }),
|
|
165
|
+
cancel: (taskId) => dataRequest(`/tasks/${taskId}/cancel`, { method: 'POST', body: {} }),
|
|
166
|
+
updateStatus: (taskId, data) => dataRequest(`/tasks/${taskId}/status`, { method: 'POST', body: data }),
|
|
167
|
+
notifying: () => dataRequest('/tasks/notifying'),
|
|
168
|
+
resetSession: () => dataRequest('/reset-session', { method: 'POST', body: {} }),
|
|
169
|
+
};
|
|
170
|
+
// ============ 设置 API (data) ============
|
|
171
|
+
export const settingsApi = {
|
|
172
|
+
get: () => dataRequest('/settings'),
|
|
173
|
+
update: (data) => dataRequest('/settings', { method: 'PUT', body: data }),
|
|
174
|
+
};
|
|
175
|
+
export default {
|
|
176
|
+
authApi,
|
|
177
|
+
skillsApi,
|
|
178
|
+
skillHubApi,
|
|
179
|
+
modelsApi,
|
|
180
|
+
tasksApi,
|
|
181
|
+
settingsApi
|
|
182
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getLogger } from '@pocketclaw/shared';
|
|
2
|
+
const logger = getLogger('GatewayConfig');
|
|
3
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
// 打包后从可执行文件所在目录读取 .env,开发时从 CWD 读取
|
|
7
|
+
const getEnvPath = () => {
|
|
8
|
+
// pkg 打包后用 process.execPath 获取 exe 所在目录
|
|
9
|
+
const exeDir = path.dirname(process.execPath);
|
|
10
|
+
const exeEnvPath = path.join(exeDir, '.env');
|
|
11
|
+
if (fs.existsSync(exeEnvPath)) {
|
|
12
|
+
return exeEnvPath;
|
|
13
|
+
}
|
|
14
|
+
// 开发环境:从 CWD 读取
|
|
15
|
+
const cwdEnvPath = path.resolve(process.cwd(), '.env');
|
|
16
|
+
if (fs.existsSync(cwdEnvPath)) {
|
|
17
|
+
return cwdEnvPath;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
};
|
|
21
|
+
const envPath = getEnvPath();
|
|
22
|
+
if (envPath) {
|
|
23
|
+
dotenvConfig({ path: envPath });
|
|
24
|
+
logger.info(`已加载环境变量文件: ${envPath}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
logger.info('未找到 .env 文件,使用系统环境变量');
|
|
28
|
+
}
|
|
29
|
+
// pkg 打包后自动设为 production
|
|
30
|
+
const isPackaged = !!process.pkg;
|
|
31
|
+
const defaultEnv = isPackaged ? 'production' : 'development';
|
|
32
|
+
// 环境变量配置
|
|
33
|
+
export const appConfig = {
|
|
34
|
+
serverBaseUrl: process.env.SERVER_BASE_URL || 'http://localhost:9091',
|
|
35
|
+
port: parseInt(process.env.PORT || '3001', 10),
|
|
36
|
+
clientUrl: process.env.CLIENT_URL || 'http://localhost:3000',
|
|
37
|
+
nodeEnv: process.env.NODE_ENV || defaultEnv,
|
|
38
|
+
messageKeep: 4,
|
|
39
|
+
appName: 'MyClaw'
|
|
40
|
+
};
|
|
41
|
+
export default appConfig;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import helmet from 'helmet';
|
|
4
|
+
import compression from 'compression';
|
|
5
|
+
import http from 'http';
|
|
6
|
+
import { appConfig } from './config/index.js';
|
|
7
|
+
import { getLogger } from '@pocketclaw/shared';
|
|
8
|
+
import authRoutes from './routes/auth.js';
|
|
9
|
+
import agentRoutes from './routes/agent.js';
|
|
10
|
+
import modelsRoutes from './routes/models.js';
|
|
11
|
+
import skillsRoutes from './routes/skills.js';
|
|
12
|
+
import skillHubRoutes from './routes/skillHub.js';
|
|
13
|
+
import chatRoutes from './routes/chat.js';
|
|
14
|
+
import configRoutes from './routes/config.js';
|
|
15
|
+
import settingsRoutes from './routes/settings.js';
|
|
16
|
+
import serviceRoutes from './routes/service.js';
|
|
17
|
+
import tasksRoutes from './routes/tasks.js';
|
|
18
|
+
import uploadRoutes from './routes/upload.js';
|
|
19
|
+
import versionRoutes from './routes/version.js';
|
|
20
|
+
import { errorHandler } from './middleware/errorHandler.js';
|
|
21
|
+
import { authStore } from './stores/index.js';
|
|
22
|
+
import { webSocketService } from './services/WebSocketService.js';
|
|
23
|
+
import { taskSchedulerService } from './services/TaskSchedulerService.js';
|
|
24
|
+
import { getServiceInfo, installService, uninstallService, startService, stopService, updateService, } from './services/ServiceManager.js';
|
|
25
|
+
const logger = getLogger('index');
|
|
26
|
+
// ─── CLI 模式 ─────────────────────────────────────────
|
|
27
|
+
// gateway install | start | stop | uninstall | status | update
|
|
28
|
+
const cliCommand = process.argv[2];
|
|
29
|
+
if (cliCommand) {
|
|
30
|
+
(async () => {
|
|
31
|
+
try {
|
|
32
|
+
let exitCode = 0;
|
|
33
|
+
if (cliCommand === 'status') {
|
|
34
|
+
const info = await getServiceInfo();
|
|
35
|
+
console.log(`服务名称: ${info.displayName}`);
|
|
36
|
+
console.log(`平台: ${info.platform}`);
|
|
37
|
+
console.log(`已安装: ${info.installed ? '是' : '否'}`);
|
|
38
|
+
console.log(`运行中: ${info.running ? '是' : '否'}`);
|
|
39
|
+
if (!info.canManage)
|
|
40
|
+
console.log(`(当前平台不支持服务管理)`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const fnMap = {
|
|
44
|
+
install: installService,
|
|
45
|
+
uninstall: uninstallService,
|
|
46
|
+
start: startService,
|
|
47
|
+
stop: stopService,
|
|
48
|
+
update: updateService,
|
|
49
|
+
};
|
|
50
|
+
const handler = fnMap[cliCommand];
|
|
51
|
+
if (!handler) {
|
|
52
|
+
console.log(`未知命令: ${cliCommand}\n可用命令: install | start | stop | uninstall | status | update`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const result = await handler();
|
|
56
|
+
console.log(result.success ? `✅ ${result.message}` : `❌ ${result.message}`);
|
|
57
|
+
if (!result.success)
|
|
58
|
+
exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
process.exit(exitCode);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(`❌ 操作失败: ${err.message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
// CLI 命令执行 process.exit() 后脚本终止,IIFE 异步执行不阻塞
|
|
68
|
+
// 不写 return,让脚本自然结束(IIFE 完成后进程也退了)
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// ─── HTTP Server 模式 ────────────────────────────────────
|
|
72
|
+
const app = express();
|
|
73
|
+
// Middleware
|
|
74
|
+
app.use(helmet());
|
|
75
|
+
app.use(compression());
|
|
76
|
+
// CORS 配置 - 允许所有内网地址
|
|
77
|
+
const corsOptions = {
|
|
78
|
+
origin: (origin, callback) => {
|
|
79
|
+
// 允许没有 origin 的请求(如移动端或同源请求)
|
|
80
|
+
if (!origin) {
|
|
81
|
+
return callback(null, true);
|
|
82
|
+
}
|
|
83
|
+
// 内网地址正则
|
|
84
|
+
const lanPatterns = [
|
|
85
|
+
/^http:\/\/localhost(:\d+)?$/,
|
|
86
|
+
/^http:\/\/127\.\d+\.\d+\.\d+(:\d+)?$/,
|
|
87
|
+
/^http:\/\/10\.\d+\.\d+\.\d+(:\d+)?$/,
|
|
88
|
+
/^http:\/\/172\.(1[6-9]|2\d|3[01])\.\d+\.\d+(:\d+)?$/,
|
|
89
|
+
/^http:\/\/192\.168\.\d+\.\d+(:\d+)?$/,
|
|
90
|
+
/^http:\/\/\d+\.\d+\.\d+\.\d+(:\d+)?$/,
|
|
91
|
+
/\.local(:\d+)?$/,
|
|
92
|
+
];
|
|
93
|
+
if (lanPatterns.some(pattern => pattern.test(origin))) {
|
|
94
|
+
return callback(null, true);
|
|
95
|
+
}
|
|
96
|
+
callback(new Error('Not allowed by CORS'));
|
|
97
|
+
},
|
|
98
|
+
credentials: true,
|
|
99
|
+
};
|
|
100
|
+
app.use(cors(corsOptions));
|
|
101
|
+
app.use(express.json({ limit: '10mb' }));
|
|
102
|
+
// Health check
|
|
103
|
+
app.get('/health', (req, res) => {
|
|
104
|
+
res.json({ status: 'ok', service: 'gateway', version: '2.0.0', wsOnline: webSocketService.getOnlineCount() });
|
|
105
|
+
});
|
|
106
|
+
// Routes
|
|
107
|
+
app.use('/api/v1/auth', authRoutes);
|
|
108
|
+
app.use('/api/v1/agent', agentRoutes);
|
|
109
|
+
app.use('/api/v1/models', modelsRoutes);
|
|
110
|
+
app.use('/api/v1/skills', skillsRoutes);
|
|
111
|
+
app.use('/api/v1/skill-hubs', skillHubRoutes);
|
|
112
|
+
app.use('/api/v1/chats', chatRoutes);
|
|
113
|
+
app.use('/api/v1/config', configRoutes);
|
|
114
|
+
app.use('/api/v1/settings', settingsRoutes);
|
|
115
|
+
app.use('/api/v1/service', serviceRoutes);
|
|
116
|
+
app.use('/api/v1/tasks', tasksRoutes);
|
|
117
|
+
app.use('/api/v1/upload', uploadRoutes);
|
|
118
|
+
app.use('/api/v1/version', versionRoutes);
|
|
119
|
+
// Load auth from persistent storage on startup
|
|
120
|
+
authStore.load();
|
|
121
|
+
// Error handler
|
|
122
|
+
app.use(errorHandler);
|
|
123
|
+
// 创建 HTTP Server(而非 app.listen,以便挂载 WebSocket)
|
|
124
|
+
const server = http.createServer(app);
|
|
125
|
+
// 初始化 WebSocket 服务
|
|
126
|
+
webSocketService.initialize(server);
|
|
127
|
+
// 启动任务调度器
|
|
128
|
+
taskSchedulerService.start();
|
|
129
|
+
// 端口自动顺延:被占用时自动尝试下一个端口
|
|
130
|
+
const startServer = (port) => {
|
|
131
|
+
server.listen(port, () => {
|
|
132
|
+
// 动态更新 appConfig.port(供 WebSocket 广播实际端口)
|
|
133
|
+
appConfig.port = port;
|
|
134
|
+
if (port !== appConfig.port) {
|
|
135
|
+
logger.info(`Port ${appConfig.port} is in use, fallback to port ${port}`);
|
|
136
|
+
}
|
|
137
|
+
logger.info(`MyClaw Gateway Service running on port ${port}`);
|
|
138
|
+
if (authStore.isAuthenticated()) {
|
|
139
|
+
const user = authStore.getUser();
|
|
140
|
+
logger.info(`User logged in: ${user?.nickname || user?.email} (ID: ${user?.id})`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
logger.info(`No user logged in`);
|
|
144
|
+
}
|
|
145
|
+
}).on('error', (err) => {
|
|
146
|
+
if (err.code === 'EADDRINUSE') {
|
|
147
|
+
const nextPort = port + 1;
|
|
148
|
+
if (nextPort <= (appConfig.port + 10)) {
|
|
149
|
+
logger.warn(`Port ${port} is in use, trying ${nextPort}...`);
|
|
150
|
+
startServer(nextPort);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
logger.error(`All ports from ${appConfig.port} to ${appConfig.port + 10} are in use`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
logger.error(`Server error: ${err.message}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
startServer(appConfig.port);
|
|
164
|
+
// 优雅关闭
|
|
165
|
+
process.on('SIGTERM', () => {
|
|
166
|
+
logger.info('收到 SIGTERM,正在关闭...');
|
|
167
|
+
webSocketService.shutdown();
|
|
168
|
+
taskSchedulerService.stop();
|
|
169
|
+
server.close(() => {
|
|
170
|
+
logger.info('服务已关闭');
|
|
171
|
+
process.exit(0);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
process.on('SIGINT', () => {
|
|
175
|
+
logger.info('收到 SIGINT,正在关闭...');
|
|
176
|
+
webSocketService.shutdown();
|
|
177
|
+
taskSchedulerService.stop();
|
|
178
|
+
server.close(() => {
|
|
179
|
+
logger.info('服务已关闭');
|
|
180
|
+
process.exit(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
|
3
|
+
/**
|
|
4
|
+
* 从请求头提取 Bearer token
|
|
5
|
+
*/
|
|
6
|
+
function extractToken(req) {
|
|
7
|
+
const auth = req.headers.authorization;
|
|
8
|
+
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
|
|
9
|
+
return auth.slice(7);
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 解码 JWT,提取 userId
|
|
15
|
+
*/
|
|
16
|
+
function decodeUserId(token) {
|
|
17
|
+
try {
|
|
18
|
+
const payload = jwt.verify(token, JWT_SECRET);
|
|
19
|
+
return payload.userId;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 需要认证:token 无效返回 401
|
|
27
|
+
*/
|
|
28
|
+
export function requireAuth(req, res, next) {
|
|
29
|
+
const token = extractToken(req);
|
|
30
|
+
if (!token) {
|
|
31
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const userId = decodeUserId(token);
|
|
35
|
+
if (!userId) {
|
|
36
|
+
res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
req.userId = userId;
|
|
40
|
+
req.token = token;
|
|
41
|
+
next();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 可选认证:token 存在则解码,不存在也放行
|
|
45
|
+
*/
|
|
46
|
+
export function optionalAuth(req, _res, next) {
|
|
47
|
+
const token = extractToken(req);
|
|
48
|
+
if (token) {
|
|
49
|
+
req.userId = decodeUserId(token);
|
|
50
|
+
req.token = token;
|
|
51
|
+
}
|
|
52
|
+
next();
|
|
53
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getLogger } from '@pocketclaw/shared';
|
|
2
|
+
const logger = getLogger('ErrorHandler');
|
|
3
|
+
export const errorHandler = (err, req, res, next) => {
|
|
4
|
+
logger.error('[Gateway Error]', err.message, err.stack);
|
|
5
|
+
const statusCode = err.statusCode || 500;
|
|
6
|
+
const message = err.message || 'Internal Server Error';
|
|
7
|
+
res.status(statusCode).json({
|
|
8
|
+
error: {
|
|
9
|
+
message,
|
|
10
|
+
code: err.code,
|
|
11
|
+
status: statusCode
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
export const createError = (message, statusCode, code) => {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.statusCode = statusCode;
|
|
18
|
+
error.code = code;
|
|
19
|
+
return error;
|
|
20
|
+
};
|