@myassis/gateway 1.0.56 → 1.0.57

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/dist/api/index.js CHANGED
@@ -13,7 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  return (mod && mod.__esModule) ? mod : { "default": mod };
14
14
  };
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.settingsApi = exports.tasksApi = exports.modelsApi = exports.skillHubApi = exports.skillsApi = exports.authApi = exports.ApiError = exports.runWithToken = void 0;
16
+ exports.settingsApi = exports.tasksApi = exports.modelsApi = exports.skillHubApi = exports.skillsApi = exports.authApi = exports.ApiError = exports.getServerBaseUrl = exports.getRequestToken = exports.runWithToken = void 0;
17
17
  const crypto_1 = __importDefault(require("crypto"));
18
18
  const async_hooks_1 = require("async_hooks");
19
19
  // 多用户模式请求上下文:存储当前请求的 token
@@ -22,6 +22,16 @@ function runWithToken(token, fn) {
22
22
  return requestContext.run({ token }, fn);
23
23
  }
24
24
  exports.runWithToken = runWithToken;
25
+ // 获取当前请求的 token(用于 LLMClient 系统模式调用 Server)
26
+ function getRequestToken() {
27
+ return requestContext.getStore()?.token;
28
+ }
29
+ exports.getRequestToken = getRequestToken;
30
+ // 获取 Server 基础 URL
31
+ function getServerBaseUrl() {
32
+ return SERVER_BASE_URL;
33
+ }
34
+ exports.getServerBaseUrl = getServerBaseUrl;
25
35
  // Server 服务地址(从环境变量读取)
26
36
  const SERVER_BASE_URL = process.env.SERVER_BASE_URL || 'http://localhost:3000';
27
37
  // 签名密钥(用于请求签名)
@@ -107,6 +117,7 @@ function createRequest(prefix) {
107
117
  const _authRequest = createRequest('/api/v1/auth');
108
118
  const _dataRequest = createRequest('/api/v1/data');
109
119
  const _skillHubRequest = createRequest('/api/v1/skill-hubs');
120
+ const _llmRequest = createRequest('/api/v1/llm');
110
121
  function authRequest(path, options) {
111
122
  return _authRequest(path, options || {});
112
123
  }
@@ -171,6 +182,8 @@ exports.modelsApi = {
171
182
  update: (modelId, data, token) => dataRequest(`/models/${modelId}`, { method: 'PUT', body: data, token }),
172
183
  delete: (modelId, token) => dataRequest(`/models/${modelId}`, { method: 'DELETE', token }),
173
184
  setPrimary: (modelId, token) => dataRequest(`/models/${modelId}/primary`, { method: 'POST', body: {}, token }),
185
+ /** 获取系统模型列表(服务端配置的外部模型) */
186
+ listSystem: () => _llmRequest('/models'),
174
187
  };
175
188
  // ============ 任务 API (data) ============
176
189
  exports.tasksApi = {
package/dist/main.js CHANGED
@@ -23,6 +23,7 @@ const service_js_1 = __importDefault(require("./routes/service.js"));
23
23
  const tasks_js_1 = __importDefault(require("./routes/tasks.js"));
24
24
  const upload_js_1 = __importDefault(require("./routes/upload.js"));
25
25
  const version_js_1 = __importDefault(require("./routes/version.js"));
26
+ const quota_js_1 = __importDefault(require("./routes/quota.js"));
26
27
  const errorHandler_js_1 = require("./middleware/errorHandler.js");
27
28
  const index_js_2 = require("./stores/index.js");
28
29
  const persistStore_js_1 = require("./stores/persistStore.js");
@@ -38,7 +39,9 @@ const cliCommand = process.argv[2];
38
39
  // PowerShell 立即退出后这些句柄失效,任何 console.log 都会触发 EPIPE/EBADF,
39
40
  // Node 若无捕获会直接崩溃 → 表现为「gateway 运行一段时间就停止了」。
40
41
  // 因此在业务代码启动前把 stdout/stderr 重定向到日志文件,并挂全局兜底。
41
- if (!cliCommand && process.platform === 'win32') {
42
+ // 仅生产环境重定向;开发环境保留原始 stdout,方便调试。
43
+ const isProduction = process.env.NODE_ENV === 'production';
44
+ if (!cliCommand && process.platform === 'win32' && isProduction) {
42
45
  try {
43
46
  // 使用 require 而非 import,避免打包器把 fs/path 提前解析导致副作用
44
47
  // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -262,6 +265,7 @@ else {
262
265
  app.use('/api/v1/tasks', tasks_js_1.default);
263
266
  app.use('/api/v1/upload', upload_js_1.default);
264
267
  app.use('/api/v1/version', version_js_1.default);
268
+ app.use('/api/v1/quota', quota_js_1.default);
265
269
  // Load auth from persistent storage on startup
266
270
  index_js_2.authStore.load();
267
271
  // Error handler
@@ -252,10 +252,12 @@ router.post('/:id/sessions', ensureAgentManager, async (req, res) => {
252
252
  if (!agent) {
253
253
  return res.status(404).json({ success: false, error: 'Agent not found' });
254
254
  }
255
- const { title, selectModelId } = req.body;
255
+ const { title, selectModelId, useSystemMode, systemModelId } = req.body;
256
256
  const session = agent.createSession({
257
257
  title: title || '新会话',
258
258
  selectModelId,
259
+ useSystemMode,
260
+ systemModelId,
259
261
  });
260
262
  res.json({
261
263
  success: true,
@@ -264,6 +266,8 @@ router.post('/:id/sessions', ensureAgentManager, async (req, res) => {
264
266
  agentId: session.agentId,
265
267
  title: session.title,
266
268
  selectModelId: session.selectModelId,
269
+ useSystemMode: session.useSystemMode,
270
+ systemModelId: session.systemModelId,
267
271
  messageQueue: session.messageQueue,
268
272
  messageQueueAutoExecute: session.messageQueueAutoExecute,
269
273
  createdAt: session.createdAt,
@@ -288,8 +292,8 @@ router.put('/:id/sessions/:sessionId', ensureAgentManager, async (req, res) => {
288
292
  if (!agent) {
289
293
  return res.status(404).json({ success: false, error: 'Agent not found' });
290
294
  }
291
- const { title, selectModelId, unreadCount, messageQueue, messageQueueAutoExecute } = req.body;
292
- const session = agent.updateSession(req.params.sessionId, { title, selectModelId, unreadCount, messageQueue, messageQueueAutoExecute });
295
+ const { title, selectModelId, unreadCount, messageQueue, messageQueueAutoExecute, useSystemMode, systemModelId } = req.body;
296
+ const session = agent.updateSession(req.params.sessionId, { title, selectModelId, unreadCount, messageQueue, messageQueueAutoExecute, useSystemMode, systemModelId });
293
297
  res.json({
294
298
  success: true,
295
299
  data: {
@@ -297,6 +301,8 @@ router.put('/:id/sessions/:sessionId', ensureAgentManager, async (req, res) => {
297
301
  agentId: session.agentId,
298
302
  title: session.title,
299
303
  selectModelId: session.selectModelId,
304
+ useSystemMode: session.useSystemMode,
305
+ systemModelId: session.systemModelId,
300
306
  messageQueue: session.messageQueue,
301
307
  messageQueueAutoExecute: session.messageQueueAutoExecute,
302
308
  createdAt: session.createdAt,
@@ -437,13 +443,18 @@ router.post('/sessions/:sessionId/stream', ensureAgentManager, async (req, res)
437
443
  try {
438
444
  const userId = req.userId;
439
445
  const { sessionId } = req.params;
440
- const { content, attachments, userMessageId, assistantMessageId } = req.body;
446
+ const { content, attachments, userMessageId, assistantMessageId, useSystemMode, systemModelId } = req.body;
441
447
  if (!content && attachments.length === 0) {
442
448
  return res.status(400).json({ success: false, error: 'Content or attachments are required' });
443
449
  }
444
450
  // Find session directly
445
451
  const session = (0, index_js_2.getSessionManager)(userId).getSession(sessionId);
446
452
  if (session) {
453
+ // 同步前端本次请求携带的模式设置,确保切换后立即生效(避免使用 session 中的旧值)
454
+ if (useSystemMode !== undefined)
455
+ session.useSystemMode = useSystemMode;
456
+ if (systemModelId !== undefined)
457
+ session.selectModelId = systemModelId;
447
458
  // Set SSE headers
448
459
  res.setHeader('Content-Type', 'text/event-stream');
449
460
  res.setHeader('Cache-Control', 'no-cache');
@@ -28,6 +28,20 @@ router.get('/', async (req, res) => {
28
28
  res.status(500).json({ success: false, error: '获取模型列表失败' });
29
29
  }
30
30
  });
31
+ /**
32
+ * 获取系统模式可用模型
33
+ * GET /api/v1/models/system
34
+ */
35
+ router.get('/system', async (req, res) => {
36
+ try {
37
+ const result = await index_js_1.modelsService.listSystem();
38
+ res.json(result);
39
+ }
40
+ catch (error) {
41
+ logger.error('获取系统模型失败:', error?.message);
42
+ res.status(500).json({ success: false, error: '获取系统模型失败' });
43
+ }
44
+ });
31
45
  /**
32
46
  * 获取单个模型配置
33
47
  * GET /api/v1/models/:id
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ const shared_1 = require("@myassis/shared");
8
+ const auth_js_1 = require("../middleware/auth.js");
9
+ const index_js_1 = require("../api/index.js");
10
+ const logger = (0, shared_1.getLogger)('quota');
11
+ const router = express_1.default.Router();
12
+ // 所有路由都需要认证
13
+ router.use(auth_js_1.requireAuth);
14
+ /**
15
+ * 查询配额
16
+ * GET /api/v1/quota
17
+ * 转发到 Server GET /api/v1/llm/quota
18
+ */
19
+ router.get('/', async (req, res) => {
20
+ try {
21
+ const serverUrl = (0, index_js_1.getServerBaseUrl)();
22
+ const token = req.token;
23
+ if (!token) {
24
+ return res.status(401).json({ success: false, error: '未登录' });
25
+ }
26
+ const response = await fetch(`${serverUrl}/api/v1/llm/quota`, {
27
+ headers: { 'Authorization': `Bearer ${token}` },
28
+ });
29
+ if (!response.ok) {
30
+ const errorText = await response.text();
31
+ logger.error('查询配额失败:', response.status, errorText);
32
+ return res.status(response.status).json({ success: false, error: `查询配额失败: ${response.status}` });
33
+ }
34
+ const data = await response.json();
35
+ res.json(data);
36
+ }
37
+ catch (error) {
38
+ logger.error('查询配额异常:', error?.message);
39
+ res.status(500).json({ success: false, error: '查询配额失败' });
40
+ }
41
+ });
42
+ /**
43
+ * 购买 Token(桌面端测试模式)
44
+ * POST /api/v1/quota/purchase
45
+ * 转发到 Server POST /api/v1/llm/purchase
46
+ */
47
+ router.post('/purchase', async (req, res) => {
48
+ try {
49
+ const serverUrl = (0, index_js_1.getServerBaseUrl)();
50
+ const token = req.token;
51
+ if (!token) {
52
+ return res.status(401).json({ success: false, error: '未登录' });
53
+ }
54
+ const { productId } = req.body;
55
+ const response = await fetch(`${serverUrl}/api/v1/payments/iap/desktop-purchase`, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Authorization': `Bearer ${token}`,
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify({ productId }),
62
+ });
63
+ if (!response.ok) {
64
+ const errorText = await response.text();
65
+ logger.error('购买失败:', response.status, errorText);
66
+ return res.status(response.status).json({ success: false, error: `购买失败: ${response.status}` });
67
+ }
68
+ const data = await response.json();
69
+ res.json(data);
70
+ }
71
+ catch (error) {
72
+ logger.error('购买异常:', error?.message);
73
+ res.status(500).json({ success: false, error: '购买失败' });
74
+ }
75
+ });
76
+ /**
77
+ * 获取可购买的产品列表
78
+ * GET /api/v1/quota/products
79
+ * 转发到 Server GET /api/v1/payments/iap/products
80
+ */
81
+ router.get('/products', async (req, res) => {
82
+ try {
83
+ const serverUrl = (0, index_js_1.getServerBaseUrl)();
84
+ const token = req.token;
85
+ if (!token) {
86
+ return res.status(401).json({ success: false, error: '未登录' });
87
+ }
88
+ const response = await fetch(`${serverUrl}/api/v1/payments/iap/products`, {
89
+ headers: { 'Authorization': `Bearer ${token}` },
90
+ });
91
+ if (!response.ok) {
92
+ const errorText = await response.text();
93
+ logger.error('获取产品列表失败:', response.status, errorText);
94
+ return res.status(response.status).json({ success: false, error: `获取产品列表失败: ${response.status}` });
95
+ }
96
+ const data = await response.json();
97
+ res.json(data);
98
+ }
99
+ catch (error) {
100
+ logger.error('获取产品列表异常:', error?.message);
101
+ res.status(500).json({ success: false, error: '获取产品列表失败' });
102
+ }
103
+ });
104
+ exports.default = router;
@@ -48,6 +48,8 @@ class Agent {
48
48
  title: config.title || '新会话',
49
49
  selectModelId: config.selectModelId,
50
50
  agentId: this.id, // Associate with this agent
51
+ useSystemMode: config.useSystemMode,
52
+ systemModelId: config.systemModelId,
51
53
  });
52
54
  return session;
53
55
  }
@@ -166,6 +166,8 @@ exports.modelsService = {
166
166
  update: (modelId, data, token) => index_js_1.modelsApi.update(modelId, data, token),
167
167
  delete: (modelId, token) => index_js_1.modelsApi.delete(modelId, token),
168
168
  setPrimary: (modelId, token) => index_js_1.modelsApi.setPrimary(modelId, token),
169
+ /** 获取服务端配置的外部模型列表 */
170
+ listSystem: () => index_js_1.modelsApi.listSystem(),
169
171
  };
170
172
  exports.tasksService = {
171
173
  list: async (userId, params) => {
@@ -26,6 +26,11 @@ class LLMClient {
26
26
  preferredModelId;
27
27
  signal;
28
28
  timeoutMs = 120000;
29
+ // 系统模式:转发到 Server LLM 代理
30
+ systemMode = false;
31
+ systemModelId;
32
+ serverUrl;
33
+ authToken;
29
34
  constructor(models, messages, signal, tools, timeoutMs) {
30
35
  this.models = models;
31
36
  this.messages = messages;
@@ -45,6 +50,16 @@ class LLMClient {
45
50
  setPreferredModel(modelId) {
46
51
  this.preferredModelId = modelId;
47
52
  }
53
+ /**
54
+ * 设置系统模式
55
+ * 启用后,LLM 请求将转发到 Server 的 LLM 代理端点
56
+ */
57
+ setSystemMode(enabled, modelId, serverUrl, authToken) {
58
+ this.systemMode = enabled;
59
+ this.systemModelId = modelId;
60
+ this.serverUrl = serverUrl;
61
+ this.authToken = authToken;
62
+ }
48
63
  /**
49
64
  * 非流式 Chat 调用
50
65
  * 返回完整的 content、reasoningContent 和 toolCalls
@@ -63,7 +78,7 @@ class LLMClient {
63
78
  const controller = new AbortController();
64
79
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
65
80
  try {
66
- const request = await this.buildRequest(model, false);
81
+ const request = await this.buildRequest(false, model);
67
82
  // 组合外部 signal 和内部 controller.signal,任一 abort 都取消请求
68
83
  const combinedSignal = this.signal
69
84
  ? (this.signal.aborted ? this.signal : AbortSignal.any([this.signal, controller.signal]))
@@ -85,6 +100,7 @@ class LLMClient {
85
100
  return this.parseResponse(data, model.modelName);
86
101
  }
87
102
  catch (error) {
103
+ logger.error(error);
88
104
  if (error.message === 'exceed max message tokens') {
89
105
  throw error;
90
106
  }
@@ -115,7 +131,10 @@ class LLMClient {
115
131
  if (!model)
116
132
  break;
117
133
  try {
118
- await this.streamRequest(model, callbacks);
134
+ const result = await this.streamRequest(model, callbacks);
135
+ if (result?.modelName) {
136
+ callbacks.onModelName?.(result.modelName);
137
+ }
119
138
  return;
120
139
  }
121
140
  catch (error) {
@@ -133,183 +152,206 @@ class LLMClient {
133
152
  return selector;
134
153
  }
135
154
  /**
136
- * 构建请求
155
+ * 统一的消息映射函数
156
+ * systemMode 和非 systemMode 共用,保证消息处理逻辑一致
137
157
  */
138
- async buildRequest(model, stream) {
139
- const apiKey = index_js_1.persistStore.getModelApiKey(model.id);
140
- if (!apiKey) {
141
- throw new Error('No API Key');
142
- }
143
- // OpenAI compatible API
158
+ mapMessages(messages) {
144
159
  let existAttachments = false;
145
160
  let existScreenShot = false;
146
- const body = {
147
- model: model.modelId,
148
- messages: this.messages.map((m, index) => {
149
- if (m.role === 'tool') {
150
- if (m.tool_call_name == 'screenshot') {
151
- if (index === this.messages.length - 1) {
152
- existScreenShot = true;
153
- const contentObj = JSON.parse(m.content);
154
- return [{
155
- role: 'tool',
156
- tool_call_id: m.tool_call_id,
157
- content: '内容在user里面返回',
158
- }, {
159
- role: 'user',
160
- content: [{
161
- type: 'text',
162
- text: '请识别这张屏幕截图,然后继续当前任务'
163
- }, {
164
- type: 'image_url',
165
- image_url: {
166
- url: `data:image/png;base64,${contentObj.output}`
167
- }
168
- }]
169
- }];
170
- }
171
- else {
172
- return [{
173
- role: 'tool',
174
- tool_call_id: m.tool_call_id,
175
- content: m.content.substring(0, 100) + '...(内容过长已截断)',
176
- }];
177
- }
161
+ const mapped = messages.map((m, index) => {
162
+ if (m.role === 'tool') {
163
+ if (m.tool_call_name == 'screenshot') {
164
+ if (index === messages.length - 1) {
165
+ existScreenShot = true;
166
+ const contentObj = JSON.parse(m.content);
167
+ return [{
168
+ role: 'tool',
169
+ tool_call_id: m.tool_call_id,
170
+ content: '内容在user里面返回',
171
+ }, {
172
+ role: 'user',
173
+ content: [{
174
+ type: 'text',
175
+ text: '请识别这张屏幕截图,然后继续当前任务'
176
+ }, {
177
+ type: 'image_url',
178
+ image_url: {
179
+ url: `data:image/png;base64,${contentObj.output}`
180
+ }
181
+ }]
182
+ }];
178
183
  }
179
184
  else {
180
185
  return [{
181
186
  role: 'tool',
182
187
  tool_call_id: m.tool_call_id,
183
- content: m.content,
188
+ content: m.content.substring(0, 100) + '...(内容过长已截断)',
184
189
  }];
185
190
  }
186
191
  }
187
- // 助手调用工具的消息:必须保留 tool_calls!!(你之前丢了,导致报错)
188
- if (m.role === 'assistant' && m.tool_calls) {
192
+ else {
189
193
  return [{
190
- role: 'assistant',
191
- content: m.content || null,
192
- tool_calls: m.tool_calls.map(m => {
193
- return {
194
- id: m.id,
195
- function: {
196
- name: m.toolName,
197
- arguments: m.input
198
- },
199
- type: 'function'
200
- };
201
- }),
194
+ role: 'tool',
195
+ tool_call_id: m.tool_call_id,
196
+ content: m.content,
202
197
  }];
203
198
  }
204
- if (m.role === 'assistant' && m.toolCalls) {
205
- let items = [];
206
- for (let toolCall of m.toolCalls) {
207
- const item = {
208
- role: 'assistant',
209
- content: toolCall.content,
210
- reasoning_content: toolCall.reasoningContent,
211
- tool_calls: []
212
- };
213
- const tools = [];
214
- for (let i = 0; i < toolCall.toolCalls.length; i++) {
215
- const toolCallItem = toolCall.toolCalls[i];
216
- if (toolCallItem.output != null) {
217
- item.tool_calls.push({
218
- id: toolCallItem.id,
219
- function: {
220
- name: toolCallItem.toolName,
221
- arguments: toolCallItem.input
222
- },
223
- type: 'function'
224
- });
225
- tools.push({
226
- role: 'tool',
227
- tool_call_id: toolCallItem.id,
228
- content: toolCallItem.output.substring(0, 100) + '...' + '(内容过长已截断)'
229
- });
230
- }
231
- }
232
- if (item.tool_calls.length > 0) {
233
- items.push(item);
234
- items = items.concat(tools);
199
+ }
200
+ // 助手调用工具的消息:必须保留 tool_calls!!
201
+ if (m.role === 'assistant' && m.tool_calls) {
202
+ return [{
203
+ role: 'assistant',
204
+ content: m.content || null,
205
+ tool_calls: m.tool_calls.map((tc) => ({
206
+ id: tc.id,
207
+ function: {
208
+ name: tc.toolName,
209
+ arguments: tc.input
210
+ },
211
+ type: 'function'
212
+ })),
213
+ }];
214
+ }
215
+ if (m.role === 'assistant' && m.toolCalls) {
216
+ let items = [];
217
+ for (let toolCall of m.toolCalls) {
218
+ const item = {
219
+ role: 'assistant',
220
+ content: toolCall.content,
221
+ reasoning_content: toolCall.reasoningContent,
222
+ tool_calls: []
223
+ };
224
+ const tools = [];
225
+ for (let i = 0; i < toolCall.toolCalls.length; i++) {
226
+ const toolCallItem = toolCall.toolCalls[i];
227
+ if (toolCallItem.output != null) {
228
+ item.tool_calls.push({
229
+ id: toolCallItem.id,
230
+ function: {
231
+ name: toolCallItem.toolName,
232
+ arguments: toolCallItem.input
233
+ },
234
+ type: 'function'
235
+ });
236
+ tools.push({
237
+ role: 'tool',
238
+ tool_call_id: toolCallItem.id,
239
+ content: toolCallItem.output.substring(0, 100) + '...' + '(内容过长已截断)'
240
+ });
235
241
  }
236
242
  }
237
- if (m.content) {
238
- items.push({
239
- role: 'assistant',
240
- content: m.content
241
- });
243
+ if (item.tool_calls.length > 0) {
244
+ items.push(item);
245
+ items = items.concat(tools);
242
246
  }
243
- return items;
244
247
  }
245
- // 处理用户消息中的附件
246
- if (m.role === 'user' && m.attachments && m.attachments.length > 0) {
247
- existAttachments = true;
248
- const contentArray = [];
249
- // 添加附件
250
- for (const attachment of m.attachments) {
251
- if (attachment.type === 'image') {
252
- // 图片附件
253
- contentArray.push({
254
- type: 'image_url',
255
- image_url: {
256
- url: attachment.url,
257
- detail: 'high',
258
- },
259
- });
260
- }
261
- else if (attachment.type === 'audio') {
262
- // 音频附件 - 使用 file 类型
248
+ if (m.content) {
249
+ items.push({
250
+ role: 'assistant',
251
+ content: m.content
252
+ });
253
+ }
254
+ return items;
255
+ }
256
+ // 处理用户消息中的附件
257
+ if (m.role === 'user' && m.attachments && m.attachments.length > 0) {
258
+ existAttachments = true;
259
+ const contentArray = [];
260
+ for (const attachment of m.attachments) {
261
+ if (attachment.type === 'image') {
262
+ contentArray.push({
263
+ type: 'image_url',
264
+ image_url: {
265
+ url: attachment.url,
266
+ detail: 'high',
267
+ },
268
+ });
269
+ }
270
+ else if (attachment.type === 'audio') {
271
+ contentArray.push({
272
+ type: 'input_audio',
273
+ input_audio: {
274
+ data: attachment.url,
275
+ format: attachment.mimeType?.split('/')[1] || 'wav',
276
+ },
277
+ });
278
+ }
279
+ else {
280
+ const fileInfo = `[附件: ${attachment.name}](${attachment.url})`;
281
+ if (contentArray.length === 0) {
263
282
  contentArray.push({
264
- type: 'input_audio',
265
- input_audio: {
266
- data: attachment.url, // 如果是 base64 格式
267
- format: attachment.mimeType?.split('/')[1] || 'wav',
268
- },
283
+ type: 'text',
284
+ text: fileInfo,
269
285
  });
270
286
  }
271
287
  else {
272
- // 其他文件类型:video, file - 添加为文本引用
273
- const fileInfo = `[附件: ${attachment.name}](${attachment.url})`;
274
- if (contentArray.length === 0) {
288
+ const lastItem = contentArray[contentArray.length - 1];
289
+ if (lastItem.type === 'text') {
290
+ lastItem.text += `\n${fileInfo}`;
291
+ }
292
+ else {
275
293
  contentArray.push({
276
294
  type: 'text',
277
295
  text: fileInfo,
278
296
  });
279
297
  }
280
- else {
281
- // 如果已经有文本,将文件信息追加到文本中
282
- const lastItem = contentArray[contentArray.length - 1];
283
- if (lastItem.type === 'text') {
284
- lastItem.text += `\n${fileInfo}`;
285
- }
286
- else {
287
- contentArray.push({
288
- type: 'text',
289
- text: fileInfo,
290
- });
291
- }
292
- }
293
298
  }
294
299
  }
295
- // 如果有文本内容,添加文本
296
- if (m.content && m.content.trim()) {
297
- contentArray.push({
298
- type: 'text',
299
- text: m.content,
300
- });
301
- }
302
- return [{
303
- role: m.role,
304
- content: contentArray,
305
- }];
306
300
  }
307
- // 普通消息(无附件)
301
+ if (m.content && m.content.trim()) {
302
+ contentArray.push({
303
+ type: 'text',
304
+ text: m.content,
305
+ });
306
+ }
308
307
  return [{
309
308
  role: m.role,
310
- content: m.content,
309
+ content: contentArray,
311
310
  }];
312
- }).flatMap(x => x),
311
+ }
312
+ // 普通消息(无附件)
313
+ return [{
314
+ role: m.role,
315
+ content: m.content,
316
+ }];
317
+ }).flatMap(x => x);
318
+ return { mapped, existAttachments, existScreenShot };
319
+ }
320
+ /**
321
+ * 构建请求
322
+ */
323
+ async buildRequest(stream, model) {
324
+ // 系统模式:转发到 Server LLM 代理
325
+ if (this.systemMode) {
326
+ const { mapped } = this.mapMessages(this.messages);
327
+ return {
328
+ url: `${this.serverUrl}/api/v1/llm/${stream ? 'chat' : 'invoke'}`,
329
+ headers: {
330
+ 'Content-Type': 'application/json',
331
+ 'Authorization': `Bearer ${this.authToken}`,
332
+ },
333
+ body: {
334
+ modelId: model.modelId,
335
+ messages: mapped,
336
+ temperature: 0.7,
337
+ tools: this.tools,
338
+ },
339
+ isSystemMode: true,
340
+ };
341
+ }
342
+ // 自定义模式:直接调用 LLM API
343
+ if (!model) {
344
+ throw new Error('No available model');
345
+ }
346
+ const apiKey = index_js_1.persistStore.getModelApiKey(model.id);
347
+ if (!apiKey) {
348
+ throw new Error('No API Key');
349
+ }
350
+ // 使用统一的消息映射
351
+ const { mapped: mappedMessages, existAttachments, existScreenShot } = this.mapMessages(this.messages);
352
+ const body = {
353
+ model: model.modelId,
354
+ messages: mappedMessages,
313
355
  stream,
314
356
  enable_thinking: true,
315
357
  };
@@ -328,13 +370,14 @@ class LLMClient {
328
370
  'Authorization': `Bearer ${apiKey}`,
329
371
  },
330
372
  body,
373
+ isSystemMode: false,
331
374
  };
332
375
  }
333
376
  /**
334
377
  * 执行流式请求
335
378
  */
336
379
  async streamRequest(model, callbacks) {
337
- const request = await this.buildRequest(model, true);
380
+ const request = await this.buildRequest(true, model);
338
381
  const response = await fetch(request.url, {
339
382
  method: 'POST',
340
383
  headers: request.headers,
@@ -361,6 +404,13 @@ class LLMClient {
361
404
  };
362
405
  // 解析增量内容
363
406
  const parseDelta = (data) => {
407
+ // 系统模式 /api/v1/llm/chat 流式格式
408
+ if (data.content !== undefined && typeof data.content === 'string') {
409
+ return { content: data.content };
410
+ }
411
+ if (data.quota?.modelName) {
412
+ return { modelName: data.quota.modelName };
413
+ }
364
414
  // Coze 格式
365
415
  if (data.type === 'conversation_message.delta') {
366
416
  return { content: data.message?.content?.[0]?.text || '' };
@@ -425,6 +475,7 @@ class LLMClient {
425
475
  }
426
476
  return {};
427
477
  };
478
+ let resolvedModelName;
428
479
  try {
429
480
  while (true) {
430
481
  const { done, value } = await reader.read();
@@ -440,6 +491,9 @@ class LLMClient {
440
491
  try {
441
492
  const data = JSON.parse(parsed.data);
442
493
  const result = parseDelta(data);
494
+ if (result.modelName) {
495
+ resolvedModelName = result.modelName;
496
+ }
443
497
  if (result.content) {
444
498
  callbacks.onContent?.(result.content);
445
499
  }
@@ -462,11 +516,27 @@ class LLMClient {
462
516
  finally {
463
517
  reader.releaseLock();
464
518
  }
519
+ return { modelName: resolvedModelName };
465
520
  }
466
521
  /**
467
522
  * 解析非流式响应
468
523
  */
469
524
  parseResponse(data, modelName) {
525
+ // 系统模式 /api/v1/llm/invoke 返回格式
526
+ if (data.success === true && data.data) {
527
+ const d = data.data;
528
+ const toolCalls = d.toolCalls?.map((tc) => ({
529
+ id: tc.id || '',
530
+ toolName: tc.function?.name || '',
531
+ input: tc.function?.arguments || '',
532
+ actionName: this.getActionName(tc.function?.arguments || ''),
533
+ }));
534
+ return {
535
+ content: d.content || '',
536
+ toolCalls: toolCalls?.length ? toolCalls : undefined,
537
+ modelName: data.quota?.modelName || modelName,
538
+ };
539
+ }
470
540
  // Coze 格式
471
541
  if (data.type === 'conversation_message.completed' || data.message) {
472
542
  const messageData = data.message || {};
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toModel = void 0;
3
+ exports.toSystemModel = exports.toModel = void 0;
4
4
  function toModel(m) {
5
5
  return {
6
6
  id: String(m.id),
7
- modelName: m.modelName,
8
- modelId: m.modelId,
7
+ modelName: m.modelName || m.name,
8
+ modelId: m.modelId || m.id,
9
9
  provider: m.provider,
10
10
  type: m.type || 'chat',
11
11
  description: m.description || '',
@@ -18,3 +18,20 @@ function toModel(m) {
18
18
  };
19
19
  }
20
20
  exports.toModel = toModel;
21
+ function toSystemModel(m) {
22
+ return {
23
+ id: String(m.id),
24
+ modelName: m.modelName || m.name,
25
+ modelId: m.modelId || m.id,
26
+ provider: m.provider,
27
+ type: m.type || 'chat',
28
+ description: m.description || '',
29
+ capabilities: m.capabilities || [],
30
+ maxTokens: m.maxTokens || 0,
31
+ isActive: 1,
32
+ baseUrl: m.baseUrl,
33
+ score: m.score || 0,
34
+ isPrimary: m.isPrimary
35
+ };
36
+ }
37
+ exports.toSystemModel = toSystemModel;
@@ -1,4 +1,27 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
26
  exports.Session = exports.handleApprovalResponse = exports.registerApprovalWaiter = void 0;
4
27
  const uuid_1 = require("uuid");
@@ -72,6 +95,8 @@ class Session {
72
95
  agentId;
73
96
  title;
74
97
  selectModelId;
98
+ useSystemMode = false;
99
+ systemModelId;
75
100
  messages;
76
101
  messageQueue;
77
102
  messageQueueAutoExecute;
@@ -93,6 +118,8 @@ class Session {
93
118
  this.agentId = data.agentId;
94
119
  this.title = data.title || 'New Chat';
95
120
  this.selectModelId = data.selectModelId;
121
+ this.useSystemMode = data.useSystemMode ?? false;
122
+ this.systemModelId = data.systemModelId;
96
123
  this.messages = [];
97
124
  this.messageQueue = data.messageQueue || [];
98
125
  this.messageQueueAutoExecute = data.messageQueueAutoExecute ?? true;
@@ -396,6 +423,8 @@ class Session {
396
423
  agentId: this.agentId,
397
424
  title: this.title,
398
425
  selectModelId: this.selectModelId,
426
+ useSystemMode: this.useSystemMode,
427
+ systemModelId: this.systemModelId,
399
428
  voiceState: this.voiceState,
400
429
  createdAt: this.createdAt,
401
430
  updatedAt: this.updatedAt,
@@ -422,6 +451,8 @@ class Session {
422
451
  agentId: this.agentId,
423
452
  title: this.title,
424
453
  selectModelId: this.selectModelId || '',
454
+ useSystemMode: this.useSystemMode,
455
+ systemModelId: this.systemModelId || '',
425
456
  voiceState: this.voiceState,
426
457
  createdAt: this.createdAt,
427
458
  updatedAt: this.updatedAt,
@@ -450,6 +481,7 @@ class Session {
450
481
  */
451
482
  async streamChat(content, attachments = [], res, userMessageId, assistantMessageId, childAgent = false) {
452
483
  // 如果正在生成,等待当前生成完成(最多等待 2 秒)
484
+ console.log('------------------------------');
453
485
  if (this.isGenerating) {
454
486
  const maxWait = 2000;
455
487
  const startTime = Date.now();
@@ -557,7 +589,9 @@ class Session {
557
589
  const toolCalls = [];
558
590
  // 获取本地工具定义
559
591
  const tools = (0, index_js_1.getToolDefinitions)();
560
- const models = (await dataService_js_1.modelsService.list(token)).data.map(x => (0, models_js_1.toModel)(x));
592
+ const models = this.useSystemMode
593
+ ? (await dataService_js_1.modelsService.listSystem()).data.map(x => (0, models_js_1.toSystemModel)(x))
594
+ : (await dataService_js_1.modelsService.list(token)).data.map(x => (0, models_js_1.toModel)(x));
561
595
  // 递归处理函数(支持多轮工具调用)
562
596
  const processModelResponse = async () => {
563
597
  if (!this.abortController || !this.abortController.signal || this.abortController.signal.aborted) {
@@ -610,6 +644,11 @@ class Session {
610
644
  }
611
645
  const llmClient = new LLMClient_js_1.LLMClient(models, messages, this.abortController.signal, tools);
612
646
  llmClient.setPreferredModel(this.selectModelId);
647
+ // 系统模式:转发到 Server LLM 代理
648
+ if (this.useSystemMode) {
649
+ const { getRequestToken, getServerBaseUrl } = await Promise.resolve().then(() => __importStar(require('../../api/index.js')));
650
+ llmClient.setSystemMode(true, this.systemModelId, getServerBaseUrl(), getRequestToken());
651
+ }
613
652
  // 使用 streamChat 调用模型
614
653
  const llmResult = await llmClient.Chat();
615
654
  // 验证 LLM 结果
@@ -88,6 +88,8 @@ class SessionManager {
88
88
  agentId: config.agentId,
89
89
  title: config.title || 'New Chat',
90
90
  selectModelId: config.selectModelId,
91
+ useSystemMode: config.useSystemMode ?? false,
92
+ systemModelId: config.systemModelId,
91
93
  voiceState: { isRecording: false, isPlaying: false },
92
94
  createdAt: Date.now(),
93
95
  updatedAt: Date.now(),
@@ -177,6 +179,10 @@ class SessionManager {
177
179
  session.messageQueue = updates.messageQueue;
178
180
  if (updates.messageQueueAutoExecute !== undefined)
179
181
  session.messageQueueAutoExecute = updates.messageQueueAutoExecute;
182
+ if (updates.useSystemMode !== undefined)
183
+ session.useSystemMode = updates.useSystemMode;
184
+ if (updates.systemModelId !== undefined)
185
+ session.systemModelId = updates.systemModelId;
180
186
  session.updatedAt = Date.now();
181
187
  session.save();
182
188
  return session;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myassis/gateway",
3
- "version": "1.0.56",
3
+ "version": "1.0.57",
4
4
  "description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
5
5
  "main": "dist/index.js",
6
6
  "bin": {