ZhaoShanGeng commited on
Commit
a0b676c
·
1 Parent(s): 0937160

feat: 思维链/工具签名缓存、适配更多工具调用格式

Browse files

修改文件:
- src/api/client.js:流式/非流式响应透传 thoughtSignature,写入全局缓存供后续请求复用
- src/utils/utils.js:历史 assistant 消息补充思考块+签名块,支持 Claude/Gemini 不同签名,工具名 sanitize
- src/server/index.js:流式响应中透传 reasoning_content 和 thoughtSignature

新增文件:
- src/utils/thoughtSignatureCache.js:按 sessionId + model 维度缓存思维链签名和工具签名
- src/utils/toolNameCache.js:工具名映射缓存,sanitize 后可还原原始工具名

src/api/client.js CHANGED
@@ -7,6 +7,8 @@ import { saveBase64Image } from '../utils/imageStorage.js';
7
  import logger from '../utils/logger.js';
8
  import memoryManager, { MemoryPressure, registerMemoryPoolCleanup } from '../utils/memoryManager.js';
9
  import { buildAxiosRequestConfig } from '../utils/httpClient.js';
 
 
10
 
11
  // 请求客户端:优先使用 AntigravityRequester,失败则降级到 axios
12
  let requester = null;
@@ -234,16 +236,23 @@ async function handleApiError(error, token) {
234
  }
235
 
236
  // 转换 functionCall 为 OpenAI 格式(使用对象池)
237
- function convertToToolCall(functionCall) {
 
238
  const toolCall = getToolCallObject();
239
  toolCall.id = functionCall.id || generateToolCallId();
240
- toolCall.function.name = functionCall.name;
 
 
 
 
 
241
  toolCall.function.arguments = JSON.stringify(functionCall.args);
242
  return toolCall;
243
  }
244
 
245
  // 解析并发送流式响应片段(会修改 state 并触发 callback)
246
  // 支持 DeepSeek 格式:思维链内容通过 reasoning_content 字段输出
 
247
  function parseAndEmitStreamChunk(line, state, callback) {
248
  if (!line.startsWith(DATA_PREFIX)) return;
249
 
@@ -256,13 +265,31 @@ function parseAndEmitStreamChunk(line, state, callback) {
256
  for (const part of parts) {
257
  if (part.thought === true) {
258
  // 思维链内容 - 使用 DeepSeek 格式的 reasoning_content
259
- callback({ type: 'reasoning', reasoning_content: part.text || '' });
 
 
 
 
 
 
 
 
 
 
 
260
  } else if (part.text !== undefined) {
261
  // 普通文本内容
262
  callback({ type: 'text', content: part.text });
263
  } else if (part.functionCall) {
264
- // 工具调用
265
- state.toolCalls.push(convertToToolCall(part.functionCall));
 
 
 
 
 
 
 
266
  }
267
  }
268
  }
@@ -296,7 +323,13 @@ function parseAndEmitStreamChunk(line, state, callback) {
296
  export async function generateAssistantResponse(requestBody, token, callback) {
297
 
298
  const headers = buildHeaders(token);
299
- const state = { toolCalls: [] };
 
 
 
 
 
 
300
  const lineBuffer = getLineBuffer(); // 从对象池获取
301
 
302
  const processChunk = (chunk) => {
@@ -479,6 +512,7 @@ export async function generateAssistantResponseNoStream(requestBody, token) {
479
  const parts = data.response?.candidates?.[0]?.content?.parts || [];
480
  let content = '';
481
  let reasoningContent = '';
 
482
  const toolCalls = [];
483
  const imageUrls = [];
484
 
@@ -486,10 +520,17 @@ export async function generateAssistantResponseNoStream(requestBody, token) {
486
  if (part.thought === true) {
487
  // 思维链内容 - 使用 DeepSeek 格式的 reasoning_content
488
  reasoningContent += part.text || '';
 
 
 
489
  } else if (part.text !== undefined) {
490
  content += part.text;
491
  } else if (part.functionCall) {
492
- toolCalls.push(convertToToolCall(part.functionCall));
 
 
 
 
493
  } else if (part.inlineData) {
494
  // 保存图片到本地并获取 URL
495
  const imageUrl = saveBase64Image(part.inlineData.data, part.inlineData.mimeType);
@@ -505,14 +546,28 @@ export async function generateAssistantResponseNoStream(requestBody, token) {
505
  total_tokens: usage.totalTokenCount || 0
506
  } : null;
507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  // 生图模型:转换为 markdown 格式
509
  if (imageUrls.length > 0) {
510
  let markdown = content ? content + '\n\n' : '';
511
  markdown += imageUrls.map(url => `![image](${url})`).join('\n\n');
512
- return { content: markdown, reasoningContent: reasoningContent || null, toolCalls, usage: usageData };
513
  }
514
 
515
- return { content, reasoningContent: reasoningContent || null, toolCalls, usage: usageData };
516
  }
517
 
518
  export async function generateImageForSD(requestBody, token) {
 
7
  import logger from '../utils/logger.js';
8
  import memoryManager, { MemoryPressure, registerMemoryPoolCleanup } from '../utils/memoryManager.js';
9
  import { buildAxiosRequestConfig } from '../utils/httpClient.js';
10
+ import { setReasoningSignature, setToolSignature } from '../utils/thoughtSignatureCache.js';
11
+ import { getOriginalToolName } from '../utils/toolNameCache.js';
12
 
13
  // 请求客户端:优先使用 AntigravityRequester,失败则降级到 axios
14
  let requester = null;
 
236
  }
237
 
238
  // 转换 functionCall 为 OpenAI 格式(使用对象池)
239
+ // 会尝试将安全工具名还原为原始工具名
240
+ function convertToToolCall(functionCall, sessionId, model) {
241
  const toolCall = getToolCallObject();
242
  toolCall.id = functionCall.id || generateToolCallId();
243
+ let name = functionCall.name;
244
+ if (sessionId && model) {
245
+ const original = getOriginalToolName(sessionId, model, functionCall.name);
246
+ if (original) name = original;
247
+ }
248
+ toolCall.function.name = name;
249
  toolCall.function.arguments = JSON.stringify(functionCall.args);
250
  return toolCall;
251
  }
252
 
253
  // 解析并发送流式响应片段(会修改 state 并触发 callback)
254
  // 支持 DeepSeek 格式:思维链内容通过 reasoning_content 字段输出
255
+ // 同时透传 thoughtSignature,方便客户端后续复用
256
  function parseAndEmitStreamChunk(line, state, callback) {
257
  if (!line.startsWith(DATA_PREFIX)) return;
258
 
 
265
  for (const part of parts) {
266
  if (part.thought === true) {
267
  // 思维链内容 - 使用 DeepSeek 格式的 reasoning_content
268
+ // 缓存最新的签名,方便后续片段缺省时复用,并写入全局缓存
269
+ if (part.thoughtSignature) {
270
+ state.reasoningSignature = part.thoughtSignature;
271
+ if (state.sessionId && state.model) {
272
+ setReasoningSignature(state.sessionId, state.model, part.thoughtSignature);
273
+ }
274
+ }
275
+ callback({
276
+ type: 'reasoning',
277
+ reasoning_content: part.text || '',
278
+ thoughtSignature: part.thoughtSignature || state.reasoningSignature || null
279
+ });
280
  } else if (part.text !== undefined) {
281
  // 普通文本内容
282
  callback({ type: 'text', content: part.text });
283
  } else if (part.functionCall) {
284
+ // 工具调用,透传工具签名,并写入全局缓存
285
+ const toolCall = convertToToolCall(part.functionCall, state.sessionId, state.model);
286
+ if (part.thoughtSignature) {
287
+ toolCall.thoughtSignature = part.thoughtSignature;
288
+ if (state.sessionId && state.model) {
289
+ setToolSignature(state.sessionId, state.model, part.thoughtSignature);
290
+ }
291
+ }
292
+ state.toolCalls.push(toolCall);
293
  }
294
  }
295
  }
 
323
  export async function generateAssistantResponse(requestBody, token, callback) {
324
 
325
  const headers = buildHeaders(token);
326
+ // state 中临时缓存思维链签名,供流式多片段复用,并携带 session model 信息以写入全局缓存
327
+ const state = {
328
+ toolCalls: [],
329
+ reasoningSignature: null,
330
+ sessionId: requestBody.request?.sessionId,
331
+ model: requestBody.model
332
+ };
333
  const lineBuffer = getLineBuffer(); // 从对象池获取
334
 
335
  const processChunk = (chunk) => {
 
512
  const parts = data.response?.candidates?.[0]?.content?.parts || [];
513
  let content = '';
514
  let reasoningContent = '';
515
+ let reasoningSignature = null;
516
  const toolCalls = [];
517
  const imageUrls = [];
518
 
 
520
  if (part.thought === true) {
521
  // 思维链内容 - 使用 DeepSeek 格式的 reasoning_content
522
  reasoningContent += part.text || '';
523
+ if (part.thoughtSignature && !reasoningSignature) {
524
+ reasoningSignature = part.thoughtSignature;
525
+ }
526
  } else if (part.text !== undefined) {
527
  content += part.text;
528
  } else if (part.functionCall) {
529
+ const toolCall = convertToToolCall(part.functionCall, requestBody.request?.sessionId, requestBody.model);
530
+ if (part.thoughtSignature) {
531
+ toolCall.thoughtSignature = part.thoughtSignature;
532
+ }
533
+ toolCalls.push(toolCall);
534
  } else if (part.inlineData) {
535
  // 保存图片到本地并获取 URL
536
  const imageUrl = saveBase64Image(part.inlineData.data, part.inlineData.mimeType);
 
546
  total_tokens: usage.totalTokenCount || 0
547
  } : null;
548
 
549
+ // 将新的签名写入全局缓存(按 sessionId + model),供后续请求兜底使用
550
+ const sessionId = requestBody.request?.sessionId;
551
+ const model = requestBody.model;
552
+ if (sessionId && model) {
553
+ if (reasoningSignature) {
554
+ setReasoningSignature(sessionId, model, reasoningSignature);
555
+ }
556
+ // 工具签名:取第一个带 thoughtSignature 的工具作为缓存源
557
+ const toolSig = toolCalls.find(tc => tc.thoughtSignature)?.thoughtSignature;
558
+ if (toolSig) {
559
+ setToolSignature(sessionId, model, toolSig);
560
+ }
561
+ }
562
+
563
  // 生图模型:转换为 markdown 格式
564
  if (imageUrls.length > 0) {
565
  let markdown = content ? content + '\n\n' : '';
566
  markdown += imageUrls.map(url => `![image](${url})`).join('\n\n');
567
+ return { content: markdown, reasoningContent: reasoningContent || null, reasoningSignature, toolCalls, usage: usageData };
568
  }
569
 
570
+ return { content, reasoningContent: reasoningContent || null, reasoningSignature, toolCalls, usage: usageData };
571
  }
572
 
573
  export async function generateImageForSD(requestBody, token) {
src/server/index.js CHANGED
@@ -107,7 +107,10 @@ const createStreamChunk = (id, created, model, delta, finish_reason = null) => {
107
  const writeStreamData = (res, data) => {
108
  const json = JSON.stringify(data);
109
  // 释放对象回池
110
- if (data.choices) releaseChunkObject(data);
 
 
 
111
  res.write(SSE_PREFIX);
112
  res.write(json);
113
  res.write(SSE_SUFFIX);
@@ -275,6 +278,9 @@ app.post('/v1/chat/completions', async (req, res) => {
275
  usageData = data.usage;
276
  } else if (data.type === 'reasoning') {
277
  const delta = { reasoning_content: data.reasoning_content };
 
 
 
278
  writeStreamData(res, createStreamChunk(id, created, model, delta));
279
  } else if (data.type === 'tool_calls') {
280
  hasToolCall = true;
@@ -304,7 +310,7 @@ app.post('/v1/chat/completions', async (req, res) => {
304
  req.setTimeout(0); // 禁用请求超时
305
  res.setTimeout(0); // 禁用响应超时
306
 
307
- const { content, reasoningContent, toolCalls, usage } = await with429Retry(
308
  () => generateAssistantResponseNoStream(requestBody, token),
309
  safeRetries,
310
  'chat.no_stream '
@@ -312,6 +318,7 @@ app.post('/v1/chat/completions', async (req, res) => {
312
  // DeepSeek 格式:reasoning_content 在 content 之前
313
  const message = { role: 'assistant' };
314
  if (reasoningContent) message.reasoning_content = reasoningContent;
 
315
  message.content = content;
316
  if (toolCalls.length > 0) message.tool_calls = toolCalls;
317
 
 
107
  const writeStreamData = (res, data) => {
108
  const json = JSON.stringify(data);
109
  // 释放对象回池
110
+ const delta = { reasoning_content: data.reasoning_content };
111
+ if (data.thoughtSignature) {
112
+ delta.thoughtSignature = data.thoughtSignature;
113
+ }
114
  res.write(SSE_PREFIX);
115
  res.write(json);
116
  res.write(SSE_SUFFIX);
 
278
  usageData = data.usage;
279
  } else if (data.type === 'reasoning') {
280
  const delta = { reasoning_content: data.reasoning_content };
281
+ if (data.thoughtSignature) {
282
+ delta.thoughtSignature = data.thoughtSignature;
283
+ }
284
  writeStreamData(res, createStreamChunk(id, created, model, delta));
285
  } else if (data.type === 'tool_calls') {
286
  hasToolCall = true;
 
310
  req.setTimeout(0); // 禁用请求超时
311
  res.setTimeout(0); // 禁用响应超时
312
 
313
+ const { content, reasoningContent, reasoningSignature, toolCalls, usage } = await with429Retry(
314
  () => generateAssistantResponseNoStream(requestBody, token),
315
  safeRetries,
316
  'chat.no_stream '
 
318
  // DeepSeek 格式:reasoning_content 在 content 之前
319
  const message = { role: 'assistant' };
320
  if (reasoningContent) message.reasoning_content = reasoningContent;
321
+ if (reasoningSignature) message.thoughtSignature = reasoningSignature;
322
  message.content = content;
323
  if (toolCalls.length > 0) message.tool_calls = toolCalls;
324
 
src/utils/thoughtSignatureCache.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 简单内存缓存:按 sessionId + model 维度缓存思维链签名和工具签名
2
+ // 同时集成内存管理器,在压力较高时自动收缩/清空缓存
3
+
4
+ import memoryManager, { MemoryPressure } from './memoryManager.js';
5
+
6
+ const reasoningSignatureCache = new Map();
7
+ const toolSignatureCache = new Map();
8
+
9
+ // 正常情况下允许的最大条目数(低压力时)
10
+ const MAX_REASONING_ENTRIES = 256;
11
+ const MAX_TOOL_ENTRIES = 256;
12
+
13
+ // 过期时间与定时清理间隔(毫秒)
14
+ const ENTRY_TTL_MS = 30 * 60 * 1000; // 30 分钟
15
+ const CLEAN_INTERVAL_MS = 10 * 60 * 1000; // 每 10 分钟扫一遍
16
+
17
+ function makeKey(sessionId, model) {
18
+ return `${sessionId || ''}::${model || ''}`;
19
+ }
20
+
21
+ function pruneMap(map, targetSize) {
22
+ if (map.size <= targetSize) return;
23
+ const removeCount = map.size - targetSize;
24
+ let removed = 0;
25
+ for (const key of map.keys()) {
26
+ map.delete(key);
27
+ removed++;
28
+ if (removed >= removeCount) break;
29
+ }
30
+ }
31
+
32
+ function pruneExpired(map, now) {
33
+ for (const [key, entry] of map.entries()) {
34
+ if (!entry || typeof entry.ts !== 'number') continue;
35
+ if (now - entry.ts > ENTRY_TTL_MS) {
36
+ map.delete(key);
37
+ }
38
+ }
39
+ }
40
+
41
+ // 注册到内存管理器,在不同压力级别下自动清理缓存
42
+ memoryManager.registerCleanup((pressure) => {
43
+ if (pressure === MemoryPressure.MEDIUM) {
44
+ // 中等压力:收缩到一半容量
45
+ pruneMap(reasoningSignatureCache, Math.floor(MAX_REASONING_ENTRIES / 2));
46
+ pruneMap(toolSignatureCache, Math.floor(MAX_TOOL_ENTRIES / 2));
47
+ } else if (pressure === MemoryPressure.HIGH) {
48
+ // 高压力:大幅收缩
49
+ pruneMap(reasoningSignatureCache, Math.floor(MAX_REASONING_ENTRIES / 4));
50
+ pruneMap(toolSignatureCache, Math.floor(MAX_TOOL_ENTRIES / 4));
51
+ } else if (pressure === MemoryPressure.CRITICAL) {
52
+ // 紧急压力:直接清空,优先保活
53
+ reasoningSignatureCache.clear();
54
+ toolSignatureCache.clear();
55
+ }
56
+ });
57
+
58
+ // 定时清理:不依赖压力等级,按 TTL 移除过期签名
59
+ setInterval(() => {
60
+ const now = Date.now();
61
+ pruneExpired(reasoningSignatureCache, now);
62
+ pruneExpired(toolSignatureCache, now);
63
+ }, CLEAN_INTERVAL_MS).unref?.();
64
+
65
+ export function setReasoningSignature(sessionId, model, signature) {
66
+ if (!signature) return;
67
+ const key = makeKey(sessionId, model);
68
+ reasoningSignatureCache.set(key, { signature, ts: Date.now() });
69
+ // 防止在低压力下无限增长
70
+ pruneMap(reasoningSignatureCache, MAX_REASONING_ENTRIES);
71
+ }
72
+
73
+ export function getReasoningSignature(sessionId, model) {
74
+ const key = makeKey(sessionId, model);
75
+ const entry = reasoningSignatureCache.get(key);
76
+ if (!entry) return null;
77
+ const now = Date.now();
78
+ if (typeof entry.ts === 'number' && now - entry.ts > ENTRY_TTL_MS) {
79
+ reasoningSignatureCache.delete(key);
80
+ return null;
81
+ }
82
+ return entry.signature || null;
83
+ }
84
+
85
+ export function setToolSignature(sessionId, model, signature) {
86
+ if (!signature) return;
87
+ const key = makeKey(sessionId, model);
88
+ toolSignatureCache.set(key, { signature, ts: Date.now() });
89
+ pruneMap(toolSignatureCache, MAX_TOOL_ENTRIES);
90
+ }
91
+
92
+ export function getToolSignature(sessionId, model) {
93
+ const key = makeKey(sessionId, model);
94
+ const entry = toolSignatureCache.get(key);
95
+ if (!entry) return null;
96
+ const now = Date.now();
97
+ if (typeof entry.ts === 'number' && now - entry.ts > ENTRY_TTL_MS) {
98
+ toolSignatureCache.delete(key);
99
+ return null;
100
+ }
101
+ return entry.signature || null;
102
+ }
103
+
104
+ // 预留:手动清理接口(目前未在外部使用,但方便将来扩展)
105
+ export function clearThoughtSignatureCaches() {
106
+ reasoningSignatureCache.clear();
107
+ toolSignatureCache.clear();
108
+ }
src/utils/toolNameCache.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 工具名称映射缓存:按 sessionId + model + safeName 维度
2
+ // 解决:发送到上游时工具名必须 sanitize,返回时需要还原为原始工具名
3
+
4
+ import memoryManager, { MemoryPressure } from './memoryManager.js';
5
+
6
+ // safeKey: `${sessionId}::${model}::${safeName}` -> { originalName, ts }
7
+ const toolNameMap = new Map();
8
+
9
+ const MAX_ENTRIES = 512;
10
+ const ENTRY_TTL_MS = 30 * 60 * 1000; // 30 分钟
11
+ const CLEAN_INTERVAL_MS = 10 * 60 * 1000; // 每 10 分钟扫一遍
12
+
13
+ function makeKey(sessionId, model, safeName) {
14
+ return `${sessionId || ''}::${model || ''}::${safeName || ''}`;
15
+ }
16
+
17
+ function pruneSize(targetSize) {
18
+ if (toolNameMap.size <= targetSize) return;
19
+ const removeCount = toolNameMap.size - targetSize;
20
+ let removed = 0;
21
+ for (const key of toolNameMap.keys()) {
22
+ toolNameMap.delete(key);
23
+ removed++;
24
+ if (removed >= removeCount) break;
25
+ }
26
+ }
27
+
28
+ function pruneExpired(now) {
29
+ for (const [key, entry] of toolNameMap.entries()) {
30
+ if (!entry || typeof entry.ts !== 'number') continue;
31
+ if (now - entry.ts > ENTRY_TTL_MS) {
32
+ toolNameMap.delete(key);
33
+ }
34
+ }
35
+ }
36
+
37
+ // 按内存压力收缩缓存
38
+ memoryManager.registerCleanup((pressure) => {
39
+ if (pressure === MemoryPressure.MEDIUM) {
40
+ pruneSize(Math.floor(MAX_ENTRIES / 2));
41
+ } else if (pressure === MemoryPressure.HIGH) {
42
+ pruneSize(Math.floor(MAX_ENTRIES / 4));
43
+ } else if (pressure === MemoryPressure.CRITICAL) {
44
+ toolNameMap.clear();
45
+ }
46
+ });
47
+
48
+ // 定时按 TTL 清理
49
+ setInterval(() => {
50
+ const now = Date.now();
51
+ pruneExpired(now);
52
+ }).unref?.();
53
+
54
+ export function setToolNameMapping(sessionId, model, safeName, originalName) {
55
+ if (!safeName || !originalName || safeName === originalName) return;
56
+ const key = makeKey(sessionId, model, safeName);
57
+ toolNameMap.set(key, { originalName, ts: Date.now() });
58
+ pruneSize(MAX_ENTRIES);
59
+ }
60
+
61
+ export function getOriginalToolName(sessionId, model, safeName) {
62
+ if (!safeName) return null;
63
+ const key = makeKey(sessionId, model, safeName);
64
+ const entry = toolNameMap.get(key);
65
+ if (!entry) return null;
66
+ const now = Date.now();
67
+ if (typeof entry.ts === 'number' && now - entry.ts > ENTRY_TTL_MS) {
68
+ toolNameMap.delete(key);
69
+ return null;
70
+ }
71
+ return entry.originalName || null;
72
+ }
73
+
74
+ export function clearToolNameMappings() {
75
+ toolNameMap.clear();
76
+ }
src/utils/utils.js CHANGED
@@ -1,10 +1,27 @@
1
  import config from '../config/config.js';
2
  import tokenManager from '../auth/token_manager.js';
3
  import { generateRequestId } from './idGenerator.js';
4
- import os from 'os';
 
 
5
 
6
- // 思维链签名占位(用于启用思考模型但没有真实签名时)
7
- const DEFAULT_THOUGHT_SIGNATURE = 'RXFRRENrZ0lDaEFDR0FJcVFKV1Bvcy9GV20wSmtMV2FmWkFEbGF1ZTZzQTdRcFlTc1NvbklmemtSNFo4c1dqeitIRHBOYW9hS2NYTE1TeTF3bjh2T1RHdE1KVjVuYUNQclZ5cm9DMFNETHk4M0hOSWsrTG1aRUhNZ3hvTTl0ZEpXUDl6UUMzOExxc2ZJakI0UkkxWE1mdWJ1VDQrZnY0Znp0VEoyTlhtMjZKL2daYi9HL1gwcmR4b2x0VE54empLemtLcEp0ZXRia2plb3NBcWlRSWlXUHloMGhVVTk1dHNha1dyNDVWNUo3MTJjZDNxdHQ5Z0dkbjdFaFk4dUllUC9CcThVY2VZZC9YbFpYbDc2bHpEbmdzL2lDZXlNY3NuZXdQMjZBTDRaQzJReXdibVQzbXlSZmpld3ZSaUxxOWR1TVNidHIxYXRtYTJ0U1JIRjI0Z0JwUnpadE1RTmoyMjR4bTZVNUdRNXlOSWVzUXNFNmJzRGNSV0RTMGFVOEZERExybmhVQWZQT2JYMG5lTGR1QnU1VGZOWW9NZGlRbTgyUHVqVE1xaTlmN0t2QmJEUUdCeXdyVXR2eUNnTEFHNHNqeWluZDRCOEg3N2ZJamt5blI3Q3ZpQzlIOTVxSENVTCt3K3JzMmsvV0sxNlVsbGlTK0pET3UxWXpPMWRPOUp3V3hEMHd5ZVU0a0Y5MjIxaUE5Z2lUd2djZXhSU2c4TWJVMm1NSjJlaGdlY3g0YjJ3QloxR0FFPQ==';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  function extractImagesFromContent(content) {
10
  const result = { text: '', images: [] };
@@ -42,6 +59,64 @@ function extractImagesFromContent(content) {
42
 
43
  return result;
44
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  function handleUserMessage(extracted, antigravityMessages){
46
  antigravityMessages.push({
47
  role: "user",
@@ -71,20 +146,38 @@ function sanitizeToolName(name) {
71
  }
72
  return cleaned;
73
  }
74
- function handleAssistantMessage(message, antigravityMessages, enableThinking){
75
  const lastMessage = antigravityMessages[antigravityMessages.length - 1];
76
  const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
77
  const hasContent = message.content && message.content.trim() !== '';
78
 
79
- const antigravityTools = hasToolCalls ? message.tool_calls.map(toolCall => ({
80
- functionCall: {
81
- id: toolCall.id,
82
- name: sanitizeToolName(toolCall.function.name),
83
- args: {
84
- query: toolCall.function.arguments
 
 
 
 
 
85
  }
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
- })) : [];
 
 
88
 
89
  if (lastMessage?.role === "model" && hasToolCalls && !hasContent){
90
  lastMessage.parts.push(...antigravityTools)
@@ -102,6 +195,12 @@ function handleAssistantMessage(message, antigravityMessages, enableThinking){
102
  // ]
103
  // }
104
  if (enableThinking) {
 
 
 
 
 
 
105
  // 默认思考内容不能是完全空字符串,否则上游会要求 thinking 字段
106
  // 这里用一个不可见的退格符作为占位,实际展示时等价于“空思考块”
107
  let reasoningText = '';
@@ -112,7 +211,7 @@ function handleAssistantMessage(message, antigravityMessages, enableThinking){
112
  }
113
  parts.push({ text: reasoningText, thought: true });
114
  // 思维链签名占位,避免上游校验缺少签名字段
115
- parts.push({ text: ' ', thoughtSignature: DEFAULT_THOUGHT_SIGNATURE });
116
  }
117
 
118
  if (hasContent) parts.push({ text: message.content.trimEnd() });
@@ -141,12 +240,17 @@ function handleToolCall(message, antigravityMessages){
141
  }
142
 
143
  const lastMessage = antigravityMessages[antigravityMessages.length - 1];
 
 
 
 
 
144
  const functionResponse = {
145
  functionResponse: {
146
  id: message.tool_call_id,
147
  name: functionName,
148
  response: {
149
- output: message.content
150
  }
151
  }
152
  };
@@ -161,7 +265,7 @@ function handleToolCall(message, antigravityMessages){
161
  });
162
  }
163
  }
164
- function openaiMessageToAntigravity(openaiMessages, enableThinking){
165
  const antigravityMessages = [];
166
  for (const message of openaiMessages) {
167
  if (message.role === "user") {
@@ -172,7 +276,7 @@ function openaiMessageToAntigravity(openaiMessages, enableThinking){
172
  const extracted = extractImagesFromContent(message.content);
173
  handleUserMessage(extracted, antigravityMessages);
174
  } else if (message.role === "assistant") {
175
- handleAssistantMessage(message, antigravityMessages, enableThinking);
176
  } else if (message.role === "tool") {
177
  handleToolCall(message, antigravityMessages);
178
  }
@@ -308,7 +412,7 @@ function cleanParameters(obj) {
308
  return cleaned;
309
  }
310
 
311
- function convertOpenAIToolsToAntigravity(openaiTools){
312
  if (!openaiTools || openaiTools.length === 0) return [];
313
  return openaiTools.map((tool)=>{
314
  // 先清洗一遍参数,过滤/规范化不兼容字段
@@ -325,7 +429,13 @@ function convertOpenAIToolsToAntigravity(openaiTools){
325
  cleanedParams.properties = {};
326
  }
327
 
328
- const safeName = sanitizeToolName(tool.function?.name);
 
 
 
 
 
 
329
 
330
  return {
331
  functionDeclarations: [
@@ -382,8 +492,8 @@ function generateRequestBody(openaiMessages,modelName,parameters,openaiTools,tok
382
  project: token.projectId,
383
  requestId: generateRequestId(),
384
  request: {
385
- contents: openaiMessageToAntigravity(filteredMessages, enableThinking),
386
- tools: convertOpenAIToolsToAntigravity(openaiTools),
387
  toolConfig: {
388
  functionCallingConfig: {
389
  mode: "VALIDATED"
@@ -423,21 +533,8 @@ function prepareImageRequest(requestBody) {
423
 
424
  return requestBody;
425
  }
426
-
427
- function getDefaultIp(){
428
- const interfaces = os.networkInterfaces();
429
- for (const iface of Object.values(interfaces)){
430
- for (const inter of iface){
431
- if (inter.family === 'IPv4' && !inter.internal){
432
- return inter.address;
433
- }
434
- }
435
- }
436
- return '127.0.0.1';
437
- }
438
  export{
439
  generateRequestId,
440
  generateRequestBody,
441
- prepareImageRequest,
442
- getDefaultIp
443
  }
 
1
  import config from '../config/config.js';
2
  import tokenManager from '../auth/token_manager.js';
3
  import { generateRequestId } from './idGenerator.js';
4
+ import { saveBase64Image } from './imageStorage.js';
5
+ import { getReasoningSignature, getToolSignature } from './thoughtSignatureCache.js';
6
+ import { setToolNameMapping } from './toolNameCache.js';
7
 
8
+ // 思维链签名常量
9
+ // Claude 模型签名
10
+ const CLAUDE_THOUGHT_SIGNATURE = 'RXFRRENrZ0lDaEFDR0FJcVFKV1Bvcy9GV20wSmtMV2FmWkFEbGF1ZTZzQTdRcFlTc1NvbklmemtSNFo4c1dqeitIRHBOYW9hS2NYTE1TeTF3bjh2T1RHdE1KVjVuYUNQclZ5cm9DMFNETHk4M0hOSWsrTG1aRUhNZ3hvTTl0ZEpXUDl6UUMzOExxc2ZJakI0UkkxWE1mdWJ1VDQrZnY0Znp0VEoyTlhtMjZKL2daYi9HL1gwcmR4b2x0VE54empLemtLcEp0ZXRia2plb3NBcWlRSWlXUHloMGhVVTk1dHNha1dyNDVWNUo3MTJjZDNxdHQ5Z0dkbjdFaFk4dUllUC9CcThVY2VZZC9YbFpYbDc2bHpEbmdzL2lDZXlNY3NuZXdQMjZBTDRaQzJReXdibVQzbXlSZmpld3ZSaUxxOWR1TVNidHIxYXRtYTJ0U1JIRjI0Z0JwUnpadE1RTmoyMjR4bTZVNUdRNXlOSWVzUXNFNmJzRGNSV0RTMGFVOEZERExybmhVQWZQT2JYMG5lTGR1QnU1VGZOWW9NZGlRbTgyUHVqVE1xaTlmN0t2QmJEUUdCeXdyVXR2eUNnTEFHNHNqeWluZDRCOEg3N2ZJamt5blI3Q3ZpQzlIOTVxSENVTCt3K3JzMmsvV0sxNlVsbGlTK0pET3UxWXpPMWRPOUp3V3hEMHd5ZVU0a0Y5MjIxaUE5Z2lUd2djZXhSU2c4TWJVMm1NSjJlaGdlY3g0YjJ3QloxR0FFPQ==';
11
+ // Gemini 思维链签名
12
+ const GEMINI_THOUGHT_SIGNATURE = 'EqAHCp0HAXLI2nygRbdzD4Vgzxxi7tbM87zIRkNgPLqTj+Jxv9mY8Q0G87DzbTtvsIFhWB0RZMoEK6ntm5GmUe6ADtxHk4zgHUs/FKqTu8tzUdPRDrKn3KCAtFW4LJqijZoFxNKMyQRmlgPUX4tGYE7pllD77UK6SjCwKhKZoSVZLMiPXP9YFktbida1Q5upXMrzG1t8abPmpFo983T/rgWlNqJp+Fb+bsoH0zuSpmU4cPKO3LIGsxBhvRhM/xydahZD+VpEX7TEJAN58z1RomFyx9u0IR7ukwZr2UyoNA+uj8OChUDFupQsVwbm3XE1UAt22BGvfYIyyZ42fxgOgsFFY+AZ72AOufcmZb/8vIw3uEUgxHczdl+NGLuS4Hsy/AAntdcH9sojSMF3qTf+ZK1FMav23SPxUBtU5T9HCEkKqQWRnMsVGYV1pupFisWo85hRLDTUipxVy9ug1hN8JBYBNmGLf8KtWLhVp7Z11PIAZj3C6HzoVyiVeuiorwNrn0ZaaXNe+y5LHuDF0DNZhrIfnXByq6grLLSAv4fTLeCJvfGzTWWyZDMbVXNx1HgumKq8calP9wv33t0hfEaOlcmfGIyh1J/N+rOGR0WXcuZZP5/VsFR44S2ncpwTPT+MmR0PsjocDenRY5m/X4EXbGGkZ+cfPnWoA64bn3eLeJTwxl9W1ZbmYS6kjpRGUMxExgRNOzWoGISddHCLcQvN7o50K8SF5k97rxiS5q4rqDmqgRPXzQTQnZyoL3dCxScX9cvLSjNCZDcotonDBAWHfkXZ0/EmFiONQcLJdANtAjwoA44Mbn50gubrTsNd7d0Rm/hbNEh/ZceUalV5MMcl6tJtahCJoybQMsnjWuBXl7cXiKmqAvxTDxIaBgQBYAo4FrbV4zQv35zlol+O3YiyjJn/U0oBeO5pEcH1d0vnLgYP71jZVY2FjWRKnDR9aw4JhiuqAa+i0tupkBy+H4/SVwHADFQq6wcsL8qvXlwktJL9MIAoaXDkIssw6gKE9EuGd7bSO9f+sA8CZ0I8LfJ3jcHUsE/3qd4pFrn5RaET56+1p8ZHZDDUQ0p1okApUCCYsC2WuL6O9P4fcg3yitAA/AfUUNjHKANE+ANneQ0efMG7fx9bvI+iLbXgPupApoov24JRkmhHsrJiu9bp+G/pImd2PNv7ArunJ6upl0VAUWtRyLWyGfdl6etGuY8vVJ7JdWEQ8aWzRK3g6e+8YmDtP5DAfw==';
13
+ // 工具调用思维链签名
14
+ const TOOL_THOUGHT_SIGNATURE = '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';
15
+ // 兜底签名(非 Claude/Gemini 时)
16
+ const DEFAULT_THOUGHT_SIGNATURE = CLAUDE_THOUGHT_SIGNATURE;
17
+
18
+ function getThoughtSignatureForModel(actualModelName) {
19
+ if (!actualModelName) return DEFAULT_THOUGHT_SIGNATURE;
20
+ const lower = actualModelName.toLowerCase();
21
+ if (lower.includes('claude')) return CLAUDE_THOUGHT_SIGNATURE;
22
+ if (lower.includes('gemini')) return GEMINI_THOUGHT_SIGNATURE;
23
+ return DEFAULT_THOUGHT_SIGNATURE;
24
+ }
25
 
26
  function extractImagesFromContent(content) {
27
  const result = { text: '', images: [] };
 
59
 
60
  return result;
61
  }
62
+
63
+ // 尝试从工具调用返回的 JSON 字符串中提取图片,
64
+ // 保存到本地图床并将 data 替换为 URL,同时附带 markdown 字段。
65
+ function transformToolOutputForImages(rawContent) {
66
+ if (!rawContent) return rawContent;
67
+
68
+ let obj = rawContent;
69
+ let isString = false;
70
+
71
+ if (typeof rawContent === 'string') {
72
+ isString = true;
73
+ try {
74
+ obj = JSON.parse(rawContent);
75
+ } catch {
76
+ // 不是 JSON,直接返回原始内容
77
+ return rawContent;
78
+ }
79
+ }
80
+
81
+ if (!obj || typeof obj !== 'object') {
82
+ return rawContent;
83
+ }
84
+
85
+ const response = obj.response;
86
+ const contents = response?.content;
87
+ if (!Array.isArray(contents)) {
88
+ return rawContent;
89
+ }
90
+
91
+ const markdownBlocks = [];
92
+
93
+ for (const item of contents) {
94
+ if (item && item.type === 'image' && item.data && item.mimeType) {
95
+ try {
96
+ const url = saveBase64Image(item.data, item.mimeType);
97
+ // 去掉大体积的 base64,改为 URL
98
+ delete item.data;
99
+ item.url = url;
100
+
101
+ const alt = item.alt || 'image';
102
+ markdownBlocks.push(`![${alt}](${url})`);
103
+ } catch {
104
+ // 单张图片保存失败时忽略,继续处理其它内容
105
+ }
106
+ }
107
+ }
108
+
109
+ if (markdownBlocks.length > 0) {
110
+ const markdown = markdownBlocks.join('\n\n');
111
+ if (typeof obj.markdown === 'string' && obj.markdown.trim()) {
112
+ obj.markdown += `\n\n${markdown}`;
113
+ } else {
114
+ obj.markdown = markdown;
115
+ }
116
+ }
117
+
118
+ return isString ? JSON.stringify(obj) : obj;
119
+ }
120
  function handleUserMessage(extracted, antigravityMessages){
121
  antigravityMessages.push({
122
  role: "user",
 
146
  }
147
  return cleaned;
148
  }
149
+ function handleAssistantMessage(message, antigravityMessages, enableThinking, actualModelName, sessionId){
150
  const lastMessage = antigravityMessages[antigravityMessages.length - 1];
151
  const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
152
  const hasContent = message.content && message.content.trim() !== '';
153
 
154
+ const antigravityTools = hasToolCalls ? message.tool_calls.map(toolCall => {
155
+ const originalName = toolCall.function.name;
156
+ const safeName = sanitizeToolName(originalName);
157
+
158
+ const part = {
159
+ functionCall: {
160
+ id: toolCall.id,
161
+ name: safeName,
162
+ args: {
163
+ query: toolCall.function.arguments
164
+ }
165
  }
166
+ };
167
+
168
+ // 记录原始工具名到安全名的映射(仅当确实发生了变化时)
169
+ if (sessionId && actualModelName && safeName !== originalName) {
170
+ setToolNameMapping(sessionId, actualModelName, safeName, originalName);
171
+ }
172
+
173
+ // 启用思考模型时,工具调用优先使用实时签名(如果上游带了),否则兜底用常量
174
+ if (enableThinking) {
175
+ const cachedToolSig = getToolSignature(sessionId, actualModelName);
176
+ part.thoughtSignature = toolCall.thoughtSignature || cachedToolSig || TOOL_THOUGHT_SIGNATURE;
177
  }
178
+
179
+ return part;
180
+ }) : [];
181
 
182
  if (lastMessage?.role === "model" && hasToolCalls && !hasContent){
183
  lastMessage.parts.push(...antigravityTools)
 
195
  // ]
196
  // }
197
  if (enableThinking) {
198
+ // 普通思维链签名:
199
+ // 1. 优先使用消息自身携带的 thoughtSignature
200
+ // 2. 其次使用缓存中的最新签名(同 session + model)
201
+ // 3. 最后按模型类型选择内置兜底签名
202
+ const cachedSig = getReasoningSignature(sessionId, actualModelName);
203
+ const thoughtSignature = message.thoughtSignature || cachedSig || getThoughtSignatureForModel(actualModelName);
204
  // 默认思考内容不能是完全空字符串,否则上游会要求 thinking 字段
205
  // 这里用一个不可见的退格符作为占位,实际展示时等价于“空思考块”
206
  let reasoningText = '';
 
211
  }
212
  parts.push({ text: reasoningText, thought: true });
213
  // 思维链签名占位,避免上游校验缺少签名字段
214
+ parts.push({ text: ' ', thoughtSignature });
215
  }
216
 
217
  if (hasContent) parts.push({ text: message.content.trimEnd() });
 
240
  }
241
 
242
  const lastMessage = antigravityMessages[antigravityMessages.length - 1];
243
+
244
+ // 尝试从工具输出中提取并持久化图片,返回值仍保持为字符串/原始格式,
245
+ // 但内部的图片 data 会被替换为图床 URL,并附带 markdown 字段。
246
+ const transformedContent = transformToolOutputForImages(message.content);
247
+
248
  const functionResponse = {
249
  functionResponse: {
250
  id: message.tool_call_id,
251
  name: functionName,
252
  response: {
253
+ output: transformedContent
254
  }
255
  }
256
  };
 
265
  });
266
  }
267
  }
268
+ function openaiMessageToAntigravity(openaiMessages, enableThinking, actualModelName, sessionId){
269
  const antigravityMessages = [];
270
  for (const message of openaiMessages) {
271
  if (message.role === "user") {
 
276
  const extracted = extractImagesFromContent(message.content);
277
  handleUserMessage(extracted, antigravityMessages);
278
  } else if (message.role === "assistant") {
279
+ handleAssistantMessage(message, antigravityMessages, enableThinking, actualModelName, sessionId);
280
  } else if (message.role === "tool") {
281
  handleToolCall(message, antigravityMessages);
282
  }
 
412
  return cleaned;
413
  }
414
 
415
+ function convertOpenAIToolsToAntigravity(openaiTools, sessionId, actualModelName){
416
  if (!openaiTools || openaiTools.length === 0) return [];
417
  return openaiTools.map((tool)=>{
418
  // 先清洗一遍参数,过滤/规范化不兼容字段
 
429
  cleanedParams.properties = {};
430
  }
431
 
432
+ const originalName = tool.function?.name;
433
+ const safeName = sanitizeToolName(originalName);
434
+
435
+ // 仅当发生转换时才缓存映射
436
+ if (sessionId && actualModelName && safeName !== originalName) {
437
+ setToolNameMapping(sessionId, actualModelName, safeName, originalName);
438
+ }
439
 
440
  return {
441
  functionDeclarations: [
 
492
  project: token.projectId,
493
  requestId: generateRequestId(),
494
  request: {
495
+ contents: openaiMessageToAntigravity(filteredMessages, enableThinking, actualModelName, token.sessionId),
496
+ tools: convertOpenAIToolsToAntigravity(openaiTools, token.sessionId, actualModelName),
497
  toolConfig: {
498
  functionCallingConfig: {
499
  mode: "VALIDATED"
 
533
 
534
  return requestBody;
535
  }
 
 
 
 
 
 
 
 
 
 
 
 
536
  export{
537
  generateRequestId,
538
  generateRequestBody,
539
+ prepareImageRequest
 
540
  }