li2895 commited on
Commit
1e66ea5
·
1 Parent(s): 8d456ca

fix: text extraction bypass, stderr diagnostics, OpenAI ensureBlock removal

Browse files
Files changed (1) hide show
  1. server.js +186 -212
server.js CHANGED
@@ -1,11 +1,11 @@
1
- "use strict";
2
 
3
  const https = require("https");
4
  const http = require("http");
5
  const crypto = require("crypto");
6
 
7
  // ============================================================
8
- // 1. 配置
9
  // ============================================================
10
  const CONFIG = require("./config.json");
11
  const BASE_HOST = "ima.qq.com";
@@ -32,8 +32,7 @@ function maskKey(k) {
32
  }
33
 
34
  // ============================================================
35
- // 2. HTTP 客户端
36
- // ============================================================
37
  const IMA_HEADERS = {
38
  "from_browser_ima": "1",
39
  get "x-ima-cookie"() { return COOKIE; },
@@ -42,8 +41,7 @@ const IMA_HEADERS = {
42
  origin: BASE_URL,
43
  "User-Agent": "okhttp/4.12.0",
44
  "Content-Type": "application/json; charset=utf-8",
45
- "Accept-Encoding": "gzip", // ⭐ 修复: 与真实 App 请求头一致
46
- };
47
 
48
  function imaPost(path, body, extraH = {}, timeout = 30000) {
49
  return new Promise((resolve, reject) => {
@@ -53,8 +51,7 @@ function imaPost(path, body, extraH = {}, timeout = 30000) {
53
  headers: { ...IMA_HEADERS, ...extraH, "Content-Length": Buffer.byteLength(payload) },
54
  timeout,
55
  }, (res) => {
56
- // ⭐ 修复: 处理 gzip 压缩响应(与 Accept-Encoding: gzip 配套)
57
- const zlib = require("zlib");
58
  const encoding = res.headers["content-encoding"] || "";
59
  let stream = res;
60
  if (encoding === "gzip") stream = res.pipe(zlib.createGunzip());
@@ -184,7 +181,7 @@ function _recurseObj(obj, keySet, depth) {
184
  const _TEXT_KEYS = new Set(["Text", "text", "Content", "content", "Delta", "delta", "Msg", "msg", "Message", "reply", "Reply", "answer", "Answer"]);
185
  const _THINK_KEYS = new Set(["think", "Think", "thinking", "Thinking", "reasoning",
186
  "reasoning_content", "Reasoning", "thought", "Thought",
187
- "思维", "思考", "推理",
188
  "reasoningText", "thinkContent", "think_text"]);
189
 
190
  function extractEventText(d) {
@@ -212,7 +209,13 @@ function eventTexts(evt) {
212
  if (d == null) return { text: "", think: "" };
213
  // THINKING events: IMA uses "Message" key for streaming thinking content
214
  if (evt.event === "THINKING" && typeof d.Message === "string") return { text: "", think: d.Message };
215
- return { text: extractEventText(d), think: extractThinkText(d) };
 
 
 
 
 
 
216
  }
217
 
218
  function eventText(evt) {
@@ -231,9 +234,8 @@ function eventText(evt) {
231
  async function imaInitSession(question) {
232
  const { data } = await imaPost("/cgi-bin/session_logic/init_session", {
233
  env_info: { interact_type: 2, robot_type: 10000 },
234
- name: (question || "新对话").slice(0, 50),
235
- msgs_limit: 20, // IMA 上限为 20(超过会报 code=51)
236
- });
237
  if (data.code === 0) return data.session_id;
238
  throw new Error(`InitSession failed: code=${data.code} msg=${data.msg}`);
239
  }
@@ -263,7 +265,7 @@ async function collectResponseText(events) {
263
  }
264
 
265
  // ============================================================
266
- // 4. 会话缓存
267
  // ============================================================
268
  const sessions = new Map();
269
  const SESSION_TTL = 30 * 60 * 1000;
@@ -285,22 +287,20 @@ async function ensureSession(convId, question, forceNew = false) {
285
  return id;
286
  }
287
 
288
- // ⭐ 修复: IMA 会话达到 msgs_limit 后自动重建,避免"突然结束"
289
  async function imaQaStreamWithRetry(convId, question, modelType, modelId) {
290
  let sessionId = await ensureSession(convId, question);
291
  const events = await imaQaStream(sessionId, question, modelType, modelId);
292
- // 收集并检测是否触发会话限制错误(INNER_EXCEPTION 含 session limit)
293
- return (async function*() {
294
  let hitLimit = false;
295
  for await (const evt of events) {
296
  if (evt.event === "INNER_EXCEPTION" || evt.event === "ERROR" || evt.event === "FAILED") {
297
- // IMA 会话满了通常返回 INNER_EXCEPTION,此时重建 session 重试一次
298
- try {
299
  const newId = await ensureSession(convId, question, true);
300
  const retryEvents = await imaQaStream(newId, question, modelType, modelId);
301
  for await (const e2 of retryEvents) yield e2;
302
  } catch (_) {
303
- yield evt; // 重试也失败,把原始事件透传出去
304
  }
305
  return;
306
  }
@@ -310,7 +310,7 @@ async function imaQaStreamWithRetry(convId, question, modelType, modelId) {
310
  }
311
 
312
  // ============================================================
313
- // 5. 模型
314
  // ============================================================
315
  const MODELS = CONFIG.models;
316
  const DEFAULT_MODEL = CONFIG.default_model;
@@ -326,7 +326,7 @@ function resolveModel(requested) {
326
  }
327
 
328
  // ============================================================
329
- // 6. 认证 & 工具
330
  // ============================================================
331
  function checkAuth(req) {
332
  const bearer = (req.headers["authorization"] || "").replace(/^Bearer\s+/i, "");
@@ -356,7 +356,7 @@ function extractContent(msg) {
356
  return msg.content.map(c => {
357
  if (c.type === "text") return c.text;
358
  if (c.type === "tool_result") {
359
- // Anthropic tool_result content 可能是 string 或 content_block[]
360
  const tc = typeof c.content === "string" ? c.content
361
  : Array.isArray(c.content) ? c.content.map(cc => cc.text || "").join("") : "";
362
  return `Tool result:\n${tc}`;
@@ -368,7 +368,7 @@ function extractContent(msg) {
368
  }
369
 
370
  // ============================================================
371
- // 7. Function Calling — Prompt 注入引擎
372
  // ============================================================
373
 
374
  function escapeRawCtrlInStrings(s) {
@@ -388,17 +388,17 @@ function escapeRawCtrlInStrings(s) {
388
  return out;
389
  }
390
 
391
- // 修复 LLM 常见的 JSON 错误 (未转义引号、尾逗号、缺括号、字符串内裸换行)
392
  function tryRepairJson(raw) {
393
  try { return JSON.parse(raw); } catch (e) { /* continue */ }
394
  let fixed = raw;
395
- // 0. 转义字符串内的裸控制符 (写文件时 content 含真实换行 → 否则 JSON.parse 失败)
396
  fixed = escapeRawCtrlInStrings(fixed);
397
  try { return JSON.parse(fixed); } catch (e) { /* continue */ }
398
- // 1. 去尾逗号
399
  fixed = fixed.replace(/,(\s*[}\]])/g, '$1');
400
  try { return JSON.parse(fixed); } catch (e) { /* continue */ }
401
- // 2. 补缺失的闭合括号
402
  let depth = 0;
403
  for (const c of fixed) {
404
  if (c === '{' || c === '[') depth++;
@@ -410,7 +410,7 @@ function tryRepairJson(raw) {
410
  fixed += closer.repeat(Math.min(depth, 5));
411
  try { return JSON.parse(fixed); } catch (e) { /* continue */ }
412
  }
413
- // 3. 提取 name + arguments (即使 JSON 破损也能尽可能恢复)
414
  const nameMatch = fixed.match(/"name"\s*:\s*"([^"]+)"/);
415
  if (nameMatch) {
416
  const argsStart = fixed.indexOf('{', fixed.indexOf('"arguments"'));
@@ -427,23 +427,21 @@ function tryRepairJson(raw) {
427
  return null;
428
  }
429
 
430
- // 压缩 JSON Schema: 去掉 $schema/$defs/$ref 等元数据,单行紧凑输出
431
- function compactSchema(schema) {
432
  if (!schema || typeof schema !== "object") return "{}";
433
- // 深拷贝后递归清理
434
  function clean(obj) {
435
  if (Array.isArray(obj)) return obj.map(clean);
436
  if (obj && typeof obj === "object") {
437
  const out = {};
438
  for (const [k, v] of Object.entries(obj)) {
439
- if (k.startsWith("$")) continue; // 去掉 $schema, $defs, $ref, $dynamicAnchor 等
440
- out[k] = clean(v);
441
  }
442
  return out;
443
  }
444
  return obj;
445
  }
446
- return JSON.stringify(clean(schema)); // 单行,无缩进
447
  }
448
 
449
  function buildToolsPrompt(tools) {
@@ -461,12 +459,12 @@ function buildToolsPrompt(tools) {
461
  })
462
  .join("\n");
463
 
464
- return `## ⚠️ CRITICAL — YOU MUST USE FUNCTION CALLING
465
 
466
  You have access to these functions. When the user asks you to do something
467
  that a function can handle, you MUST call the function. NEVER say "I cannot"
468
  or "I don't have the ability". NEVER tell the user to save a file, run a
469
- command, or do any step themselves — DO IT by calling the function (e.g. use
470
  Write to create files, Bash to run commands). ALWAYS use a function instead.
471
 
472
  ${funcDefs}
@@ -483,16 +481,13 @@ Rules:
483
  - arguments MUST be a valid JSON object matching the function's parameters
484
  - For file content, put the FULL content in the string value (newlines allowed)
485
  - Do NOT add any text before or after the <function_call> block
486
- - NEVER output file content or code for the user to copy — write it via Write`;
487
  }
488
 
489
- // ⭐ 工具结果回传轮专用 — "继续行动"版工具提示。
490
- // 修复历史: ① 旧的强制版 ("YOU MUST call / 不要输出任何文本") 让模型回传轮要么再发多余
491
- // 调用要么沉默; ② 过软版 ("有足够信息就用纯文本作答") 又走向另一极端 —— 模型把"让我先读
492
- // 取文件"这种【意图叙述】当成回答然后停手, 多步任务半途而废。
493
- // 本版核心原则: 叙述意图 ≠ 完成任务。只要还有未完成的步骤, 必须【在本轮就发出函数调用】,
494
- // 不能只说"我接下来要做 X"。只有当任务真正全部完成时, 才用纯文本给出最终答复。
495
- function buildToolsPromptSoft(tools) {
496
  if (!tools || tools.length === 0) return "";
497
 
498
  const funcDefs = tools
@@ -510,14 +505,14 @@ function buildToolsPromptSoft(tools) {
510
 
511
  ${funcDefs}
512
 
513
- ## CRITICAL — keep going until the task is fully done
514
 
515
  The user's request may need MULTIPLE steps. After each function result, decide:
516
- the task is NOT finished yet → call the next function NOW; the task IS fully
517
- finished → give the final answer in plain text.
518
 
519
  NEVER reply with only your intention (e.g. "Let me read the file", "I'll now
520
- start the server", "下一步我将…"). Stating intent is NOT an action and NOT an
521
  answer. If you intend to do something, you MUST emit the function call for it
522
  in THIS SAME reply.
523
 
@@ -530,19 +525,16 @@ Only when every step is done, reply to the user in plain text WITHOUT any
530
  <function_call> block.`;
531
  }
532
 
533
- // ⭐ StreamFilter: 实时过滤流式输出中的 <function_call> 块
534
- // 防止 Claude Code 客户端因看到 XML 标记而截停
535
- class StreamFilter {
536
  constructor() {
537
- this._buf = ''; // 未决缓冲区
538
- this._emitted = ''; // 已输出的纯净文本
539
- this._funcBlocks = []; // 捕获的 function_call JSON
540
- this._inFunc = false; // 是否在 <function_call> 内部
541
  this._tagLen = 16; // '<function_call>'.length
542
- this._leftover = ''; // ⭐ parseCalls 无法解析的残留文本 (供流式层兜底输出)
543
  }
544
 
545
- // 喂入新文本块,返回应发送给客户端的纯净文本 (可能为空)
546
  feed(chunk) {
547
  this._buf += chunk;
548
  const out = [];
@@ -553,22 +545,18 @@ class StreamFilter {
553
  if (!this._inFunc) {
554
  const idx = this._buf.indexOf(startTag);
555
  if (idx === -1) {
556
- // 没有发现开始标签 — 但先检查尾部是否部分匹配
557
- const partialLen = this._partialMatch(startTag);
558
  if (partialLen > 0) {
559
- // 保留可能的部分标签在缓冲区
560
- const safe = this._buf.slice(0, -partialLen);
561
  if (safe) out.push(safe);
562
  this._buf = this._buf.slice(-partialLen);
563
  break;
564
  }
565
- // 完全安全,全部输出
566
- out.push(this._buf);
567
  this._buf = '';
568
  break;
569
  }
570
- // 发现开始标签 — 输出标签之前的文本
571
- if (idx > 0) out.push(this._buf.slice(0, idx));
572
  this._buf = this._buf.slice(idx + startTag.length);
573
  this._inFunc = true;
574
  }
@@ -576,15 +564,15 @@ class StreamFilter {
576
  if (this._inFunc) {
577
  const idx = this._buf.indexOf(endTag);
578
  if (idx === -1) {
579
- // 还没找到结束标签 — 全部抑制
580
  break;
581
  }
582
- // 找到结束标签 — 捕获函数调用 JSON
583
  const json = this._buf.slice(0, idx).trim();
584
  if (json) this._funcBlocks.push(json);
585
  this._buf = this._buf.slice(idx + endTag.length);
586
  this._inFunc = false;
587
- // 继续循环,可能后面还有文本或更多 function_call
588
  }
589
  }
590
 
@@ -593,7 +581,7 @@ class StreamFilter {
593
  return text;
594
  }
595
 
596
- // 检查缓冲区末尾是否部分匹配 tag
597
  _partialMatch(tag) {
598
  for (let i = tag.length - 1; i > 0; i--) {
599
  if (this._buf.endsWith(tag.slice(0, i))) return i;
@@ -601,13 +589,11 @@ class StreamFilter {
601
  return 0;
602
  }
603
 
604
- // 流结束时调用 — 返回可能残留的缓冲区内容 (不含 function_call 部分)
605
  flush() {
606
  if (this._inFunc) {
607
- // ⭐ 修复: 流在 </function_call> 到达前就结束了 (IMA 截断 / 模型漏写闭合标签)。
608
- // 旧逻辑直接丢弃缓冲区, 导致"调用命令后无任何回复" — 这里改为把残留 JSON
609
- // 当作一个 function block 捕获, 交给 parseCalls() 去尽力修复。
610
- const pending = this._buf.trim();
611
  if (pending) this._funcBlocks.push(pending);
612
  this._buf = '';
613
  this._inFunc = false;
@@ -622,8 +608,7 @@ class StreamFilter {
622
  return '';
623
  }
624
 
625
- // 解析已捕获的所有 function_call 块
626
- parseCalls() {
627
  const calls = [];
628
  for (const raw of this._funcBlocks) {
629
  const parsed = tryRepairJson(raw);
@@ -637,8 +622,7 @@ class StreamFilter {
637
  },
638
  });
639
  } else if (raw.length > 0) {
640
- // ⭐ JSON 修复失败 — 累积到 _leftover, 由流式层兜底输出, 避免对话静默中断。
641
- // (非流式走 cleanText/_emitted; 流式层需读取 leftover 单独 emit)
642
  const note = '\n[Function call (malformed)]:\n' + raw + '\n';
643
  this._emitted += note;
644
  this._leftover += note;
@@ -655,14 +639,14 @@ class StreamFilter {
655
  function parseFunctionCalls(text) {
656
  if (!text) return { found: false, text: "" };
657
 
658
- // 也匹配 ```json {...} ``` 格式 (模型可能输出 JSON 代码块)
659
  const jsonBlockRegex = /```(?:json)?\s*\n?\s*(\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})\s*\n?\s*```/g;
660
 
661
  const regex = /<function_call>\s*\n?\s*(\{[\s\S]*?\})\s*\n?\s*<\/function_call>/g;
662
  const calls = [];
663
  let cleanText = text;
664
 
665
- // 尝试两种格式: <function_call> 和 ```json name/arguments
666
  const patterns = [
667
  regex,
668
  /```(?:json)?\s*\n?\s*\{[^{}]*"name"\s*:\s*"[^"]+"[^{}]*"arguments"\s*:\s*\{[\s\S]*?\}\s*\}\s*\n?\s*```/g,
@@ -691,7 +675,7 @@ function parseFunctionCalls(text) {
691
  return { found: true, calls, text: cleanText };
692
  }
693
 
694
- // 调试: 检查模型是否输出了近似的 function call 格式
695
  if (text.includes('function') || text.includes('tool') || text.includes('bash') || text.includes('read_file')) {
696
  }
697
  return { found: false, text };
@@ -708,12 +692,12 @@ async function openaiChat(req, res, body) {
708
  const tools = body.tools || null;
709
  const toolChoice = body.tool_choice || null;
710
 
711
- // --- 构建 question ---
712
  const hasToolResults = messages.some(m => m.role === "tool");
713
  const hasToolCalls = messages.some(m => m.role === "assistant" && m.tool_calls);
714
  let question;
715
 
716
- // 确定工具集 (两个分支共用)
717
  let effectiveTools = tools;
718
  if ((!effectiveTools || effectiveTools.length === 0) && toolChoice !== "none") {
719
  effectiveTools = [
@@ -724,17 +708,16 @@ async function openaiChat(req, res, body) {
724
  {type: "function", function: {name: "Grep", description: "Search file contents", parameters: {type: "object", properties: {pattern: {type: "string"}, path: {type: "string"}}, required: ["pattern"]}}},
725
  ];
726
  }
727
- // ⭐ 标题生成请求不需要 tools
728
  const _sysParts = messages.filter(m => m.role === "system").map(m => extractContent(m));
729
  const _sysPrompt = _sysParts.length > 0 ? _sysParts.join("\n") : "";
730
  const isTitleGenOAI = _sysPrompt.includes('Generate a concise, sentence-case title');
731
 
732
- // ⭐ 限制工具数量,防止 prompt 超出限制
733
  const MAX_TOOLS_OAI = 8;
734
  const ESSENTIAL_OAI = new Set(['Bash', 'Read', 'Write', 'Glob', 'Grep']);
735
  if (isTitleGenOAI) {
736
- effectiveTools = []; // 标题生成不需要工具
737
- } else if (effectiveTools && effectiveTools.length > MAX_TOOLS_OAI) {
738
  const essential = effectiveTools.filter(t => ESSENTIAL_OAI.has(t.function?.name));
739
  const others = effectiveTools.filter(t => !ESSENTIAL_OAI.has(t.function?.name));
740
  const available = MAX_TOOLS_OAI - essential.length;
@@ -747,13 +730,12 @@ async function openaiChat(req, res, body) {
747
 
748
  if (hasToolResults && hasToolCalls) {
749
  toolResultPathOAI = true;
750
- // 工具结果回传轮 — 单段系统通知格式
751
  const lastUserOAI = messages.filter(m => m.role === "user").pop();
752
  const lastUserTextOAI = extractContent(lastUserOAI || {});
753
- const cjkOAI = /[一-鿿㐀-䶿]/.test(lastUserTextOAI);
754
 
755
- // 收集函数调用和结果
756
- const calledOAI = [], resultsOAI = [];
757
  for (const m of messages) {
758
  if (m.role === "assistant" && m.tool_calls) {
759
  for (const tc of m.tool_calls) calledOAI.push(tc.function.name + "(" + tc.function.arguments + ")");
@@ -762,7 +744,7 @@ async function openaiChat(req, res, body) {
762
  resultsOAI.push(extractContent(m).slice(0, 3000));
763
  }
764
  }
765
- // 找到用户原始提问
766
  const userQsOAI = [];
767
  for (const m of messages) {
768
  if (m.role !== "user") continue;
@@ -773,27 +755,25 @@ async function openaiChat(req, res, body) {
773
  const origQ = userQsOAI.length > 0 ? userQsOAI[userQsOAI.length - 1] : (lastUserTextOAI || "");
774
 
775
  if (cjkOAI) {
776
- question = "⚠️ 系统通知:你刚才调用了以下函数,返回结果如下:\n\n";
777
  for (let i = 0; i < calledOAI.length; i++) {
778
- question += "函数调用: " + calledOAI[i] + "\n返回结果:\n\"\"\"\n" + (resultsOAI[i] || "") + "\n\"\"\"\n\n";
779
  }
780
- question += "用户原始提问: \"" + origQ + "\"\n\n请用简体中文直接回答用户的问题。说出答案即可。不要说\"你分享了\"或\"看起来像是\"。上面用 \"\"\" 包裹的��容是你自己调用函数得到的返回结果。";
781
  } else {
782
- question = "⚠️ SYSTEM: You just called these functions and received these outputs:\n\n";
783
  for (let i = 0; i < calledOAI.length; i++) {
784
  question += "Function: " + calledOAI[i] + "\nOutput:\n\"\"\"\n" + (resultsOAI[i] || "") + "\n\"\"\"\n\n";
785
  }
786
  question += "User's original question: \"" + origQ + "\"\n\nAnswer DIRECTLY based on the outputs. Do NOT say \"you shared\" or \"it looks like\". The \"\"\" content is YOUR function output, NOT user input.";
787
  }
788
 
789
- // 工具结果回传轮: 不携带完整 sysPrompt, 仅 5 个核心工具
790
- // ⭐ 用软版提示 — 允许模型直接用文字作答, 避免被强制再发 function_call 而沉默
791
- const minimalOAI = buildToolsPromptSoft(
792
  (effectiveTools || []).filter(t => ESSENTIAL_OAI.has(t.function?.name)).slice(0, 5)
793
  );
794
  question = minimalOAI + "\n\n---\n" + question;
795
  } else {
796
- // 普通问答 / 首轮 tool calling
797
  let sysPrompt = "";
798
  const sysParts = messages.filter(m => m.role === "system").map(m => extractContent(m));
799
  if (sysParts.length > 0) sysPrompt = sysParts.join("\n");
@@ -804,7 +784,7 @@ async function openaiChat(req, res, body) {
804
  return json(res, 400, { error: { message: "No user message", type: "invalid_request_error" } });
805
  }
806
 
807
- // ⭐ 跳过系统注入消息 (<session>, <system-reminder>)
808
  const realUserMsgs = userMsgs.filter(m => {
809
  const c = extractContent(m);
810
  return !c.startsWith('<session') && !c.startsWith('<system-reminder') && c.length > 10;
@@ -813,11 +793,10 @@ async function openaiChat(req, res, body) {
813
  ? extractContent(realUserMsgs[realUserMsgs.length - 1])
814
  : extractContent(userMsgs[userMsgs.length - 1]);
815
 
816
- // ⭐ 语言检测
817
- const hasCJKOAI = /[一-鿿㐀-䶿]/.test(lastUserMsg);
818
- const langHintOAI = hasCJKOAI ? "\n## Language\nRespond in the same language as the user's message. The user is writing in Chinese — respond in Chinese (简体中文).\n" : "";
819
 
820
- // IMA question 字段限制 10240 字符
821
  const MAX_QUESTION = 10000;
822
 
823
  if (nonSys.length > 1 && realUserMsgs.length >= 1) {
@@ -826,8 +805,7 @@ async function openaiChat(req, res, body) {
826
  return c.length > 10 && !c.startsWith('<session') && !c.startsWith('<system-reminder');
827
  });
828
  if (realMsgs.length > 1) {
829
- // ⭐ 修复: 扩展到 20 条,按字符数动态裁剪
830
- const MAX_HISTORY_CHARS = 6000;
831
  const recentMsgs = realMsgs
832
  .filter(m => m.role === "user" || m.role === "assistant")
833
  .slice(-20);
@@ -846,13 +824,13 @@ async function openaiChat(req, res, body) {
846
  question = `${sysPrompt ? "(Background)\n" + sysPrompt + "\n\n" : ""}${toolsPrompt}\n\n---\nUser message (respond to this):\n${lastUserMsg}`;
847
  }
848
  } else {
849
- // ⭐ 智能压缩 system prompt (保留 toolsPrompt 完整)
850
  let context = sysPrompt;
851
  const overhead = toolsPrompt.length + lastUserMsg.length + 200;
852
  const remaining = MAX_QUESTION - overhead;
853
 
854
  if (context && context.length > remaining && remaining > 0) {
855
- // ⭐ 修复: 保留头尾,中间裁剪(保留核心规则 + 最新文件信息)
856
  const layoutMatch = context.match(/<project_layout>([\s\S]*?)<\/project_layout>/);
857
  const layoutSummary = layoutMatch
858
  ? layoutMatch[1].trim().split("\n").slice(0, 30).join("\n")
@@ -869,11 +847,11 @@ async function openaiChat(req, res, body) {
869
  question = `${context ? "(Background)\n" + context + "\n\n" : ""}${toolsPrompt}\n\n---\nUser message (respond to this):\n${lastUserMsg}`;
870
  }
871
 
872
- // ⭐ 注入语言提示 & 截断 (工具结果回传轮跳过)
873
  if (!toolResultPathOAI) {
874
  question += langHintOAI;
875
 
876
- // 最终截断 — 始终保留 toolsPrompt,否则模型看不到 function 定义
877
  if (question.length > MAX_QUESTION) {
878
  const minTemplate = "\n\n---\nUser message (respond to this):\n";
879
  const compact = toolsPrompt + minTemplate + lastUserMsg + langHintOAI;
@@ -892,8 +870,8 @@ async function openaiChat(req, res, body) {
892
  }
893
 
894
 
895
- // 会话 — 复用 IMA session 保持对话记忆
896
- // ⭐ 修复: 用系统 prompt 作为稳定锚点(同 Anthropic 路径保持一致)
897
  let oaiConvId = req.headers["x-conversation-id"] || req.headers["x-session-id"] || "";
898
  if (!oaiConvId && messages.length > 0) {
899
  const sysMsg = messages.find(m => m.role === "system");
@@ -909,7 +887,7 @@ async function openaiChat(req, res, body) {
909
  return json(res, 502, { error: { message: "Session init failed: " + e.message, type: "api_error" } });
910
  }
911
 
912
- // --- 非流式 ---
913
  if (!stream) {
914
  try {
915
  const events = await imaQaStreamWithRetry(oaiConvId, question, model.type, model.id);
@@ -926,8 +904,7 @@ async function openaiChat(req, res, body) {
926
  };
927
  choice.finish_reason = "tool_calls";
928
  } else {
929
- // ⭐ 兜底: 空文本时给出占位, 避免客户端收到完全空的回复
930
- choice.message = { role: "assistant", content: text || "(本轮没有生成内容,请重试或换一种问法。)" };
931
  }
932
 
933
  return json(res, 200, {
@@ -943,7 +920,7 @@ async function openaiChat(req, res, body) {
943
  }
944
  }
945
 
946
- // --- 流式 ---
947
  cors(res);
948
  res.writeHead(200, {
949
  "Content-Type": "text/event-stream",
@@ -955,7 +932,7 @@ async function openaiChat(req, res, body) {
955
  const chatId = "chatcmpl-" + crypto.randomUUID().slice(0, 8);
956
  const created = Math.floor(Date.now() / 1000);
957
  let resClosed = false;
958
- let sentText = false; // ⭐ 是否已向客户端发送过任何文本/工具内容 (兜底判断)
959
  res.on('close', () => { resClosed = true; });
960
 
961
  function emit(delta, finishReason, toolCalls) {
@@ -1000,26 +977,22 @@ async function openaiChat(req, res, body) {
1000
  }
1001
  }
1002
  if (et.text) {
1003
- ensureBlock("text");
1004
  const clean = filter.feed(et.text);
1005
  if (clean) emit(clean, null, null);
1006
  }
1007
  }
1008
  if (DEBUG_SSE && !sawEvent) console.error("[SSE-EMPTY] openai stream: no data events");
1009
 
1010
- // 冲刷残留缓冲
1011
- ensureBlock("text");
1012
  const flushed = filter.flush();
1013
  if (flushed && !resClosed) emit(flushed, null, null);
1014
 
1015
- // 检测并发送函数调用
1016
- if (!resClosed) {
1017
  const calls = filter.parseCalls();
1018
- // ⭐ 无法解析的 function_call 残留 — 作为文本输出, 避免静默丢失
1019
  if (filter.leftover) emit(filter.leftover, null, null);
1020
  if (calls.length > 0) {
1021
- // ⭐ OpenAI streaming tool_calls: 逐个发送
1022
- for (let i = 0; i < calls.length; i++) {
1023
  const tc = calls[i];
1024
  if (resClosed) break;
1025
  try {
@@ -1037,15 +1010,14 @@ async function openaiChat(req, res, body) {
1037
  }) + "\n\n");
1038
  }
1039
  } else {
1040
- // ⭐ 兜底: 流正常结束但既无文本也无工具调用 (模型只回了被过滤的内容 /
1041
- // 空响应 / 异常中断)。绝不让客户端收到完全空的回复 — 这是"无回复就结束"的根因。
1042
- if (!sentText && !resClosed) {
1043
  const fb = done
1044
- ? "(本轮没有生成内容,请重试或换一种问法。)"
1045
- : "(响应在完成前被中断,请重试。)";
1046
  emit(fb, null, null);
1047
  }
1048
- // done 为 true 表示正常完成
1049
  res.write("data: " + JSON.stringify({
1050
  id: chatId, object: "chat.completion.chunk", created, model: modelKey,
1051
  choices: [{ index: 0, delta: {}, finish_reason: done ? "stop" : "length" }],
@@ -1081,8 +1053,8 @@ async function anthropicMessages(req, res, body) {
1081
  else if (Array.isArray(body.system))
1082
  sysPrompt = body.system.filter(s => s.type === "text").map(s => s.text).join("\n");
1083
 
1084
- // tools → prompt injection
1085
- // 如果 Claude Code 没发送 tools,注入默认工具集
1086
  let tools = body.tools;
1087
  if (!tools || tools.length === 0) {
1088
  tools = [
@@ -1094,24 +1066,22 @@ async function anthropicMessages(req, res, body) {
1094
  ];
1095
  }
1096
 
1097
- // ⭐ 标题生成请求不需要 tools (Claude Code 首轮请求)
1098
  const isTitleGen = sysPrompt.includes('Generate a concise, sentence-case title');
1099
 
1100
- // ⭐ 限制工具数量,防止 prompt 超出 IMA 10240 字符限制
1101
  const MAX_TOOLS = 8;
1102
  const ESSENTIAL_TOOLS = new Set(['Bash', 'Read', 'Write', 'Glob', 'Grep']);
1103
  let displayTools = tools;
1104
  if (isTitleGen) {
1105
- displayTools = []; // 标题生成不需要工具
1106
- } else if (displayTools.length > MAX_TOOLS) {
1107
- // 优先保留核心工具,再按原顺序补充其他工具
1108
  const essential = displayTools.filter(t => ESSENTIAL_TOOLS.has(t.name));
1109
  const others = displayTools.filter(t => !ESSENTIAL_TOOLS.has(t.name));
1110
  const available = MAX_TOOLS - essential.length;
1111
  displayTools = [...essential, ...others.slice(0, Math.max(0, available))];
1112
  }
1113
- // —— 过滤消息 ——
1114
- // ⭐ 检测 Anthropic 格式的 tool_use / tool_result (多轮回传)
1115
  function msgHasType(msg, type) {
1116
  if (Array.isArray(msg.content)) return msg.content.some(c => c.type === type);
1117
  return false;
@@ -1119,18 +1089,16 @@ async function anthropicMessages(req, res, body) {
1119
  const hasToolUses = messages.some(m => m.role === "assistant" && msgHasType(m, "tool_use"));
1120
  const hasToolResults = messages.some(m => m.role === "user" && msgHasType(m, "tool_result"));
1121
 
1122
- // ⭐ 清理用户消息中的系统注入元数据
1123
- function stripMetadata(text) {
1124
  return text
1125
- // 删除元数据块 (含内容一起删)
1126
  .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
1127
  .replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '')
1128
  .replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '')
1129
- // 删除单行命令元数据
1130
- .replace(/<command-name>[^<]*<\/command-name>/g, '')
1131
  .replace(/<command-message>[^<]*<\/command-message>/g, '')
1132
  .replace(/<command-args>[^<]*<\/command-args>/g, '')
1133
- // ⭐ <session> 只删标签、保留内容 (用户消息在 session 内)
1134
  .replace(/<\/?session>/g, '')
1135
  .trim();
1136
  }
@@ -1140,7 +1108,7 @@ async function anthropicMessages(req, res, body) {
1140
  if (userMsgs.length === 0)
1141
  return json(res, 400, { type: "error", error: { type: "invalid_request_error", message: "No user message" } });
1142
 
1143
- // ⭐ 提取最后一条用户消息的实质内容
1144
  const realUserMsgs = userMsgs.filter(m => {
1145
  const c = stripMetadata(extractContent(m));
1146
  return c.length > 5;
@@ -1152,25 +1120,25 @@ async function anthropicMessages(req, res, body) {
1152
  if (!lastUserMsg) {
1153
  }
1154
 
1155
- // ⭐ 语言检测: 用户用中文则要求中文回复
1156
- const hasCJK = /[一-鿿㐀-䶿]/.test(lastUserMsg);
1157
- const langHint = hasCJK ? "\n## Language\nRespond in the same language as the user's message. The user is writing in Chinese — respond in Chinese (简体中文).\n" : "";
1158
 
1159
- // 工具通过 toolsPrompt 单独注入 (见下方 question 构建)
1160
  const toolsPrompt = (displayTools && displayTools.length > 0) ? buildToolsPrompt(
1161
  displayTools.map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema || {} } }))
1162
  ) : "";
1163
 
1164
  let question;
1165
- let toolResultPath = false; // 工具结果回传轮标记 (跳过 langHint 和截断)
1166
  const MAX_QUESTION = 10000;
1167
 
1168
  if (hasToolResults && hasToolUses) {
1169
- toolResultPath = true; // 标记: 后续跳过 langHint 注入和通用截断
1170
- // ⭐ 工具结果回传轮 — 单段系统通知格式,让模型把结果当作系统提供的信息
1171
  const isCJK = hasCJK;
1172
 
1173
- // 收集工具调用名+结果
1174
  const calledFuncs = [];
1175
  const results = [];
1176
  for (const m of messages) {
@@ -1190,7 +1158,7 @@ async function anthropicMessages(req, res, body) {
1190
  }
1191
  }
1192
 
1193
- // 找到用户的原始提问 (第一条非 tool_result 的用户消息)
1194
  const userQs = [];
1195
  for (const m of messages) {
1196
  if (m.role !== "user") continue;
@@ -1200,17 +1168,17 @@ async function anthropicMessages(req, res, body) {
1200
  }
1201
  const originalQuestion = userQs.length > 0 ? userQs[userQs.length - 1] : (lastUserMsg || "");
1202
 
1203
- // 构建通知: 系统消息 + 函数调用 + 结果
1204
  if (isCJK) {
1205
- question = "⚠️ 系统通知:你刚才调用了以下函数,返回结果如下:\n\n";
1206
  for (let i = 0; i < calledFuncs.length; i++) {
1207
- question += "函数调用: " + calledFuncs[i] + "\n";
1208
- question += "返回结果:\n\"\"\"\n" + (results[i] || "") + "\n\"\"\"\n\n";
1209
  }
1210
- question += "用户原始提问: \"" + originalQuestion + "\"\n\n";
1211
- question += "请用简体中文直接回答用户的问题。说出答案即可。不要说\"你分享了\"或\"看起来像是\"或\"I see you've shared\"。上面用 \"\"\" 包裹的内容是你自己调用函数得到的返回结果,不是用户发给你的。";
1212
  } else {
1213
- question = "⚠️ SYSTEM: You just called these functions and received these outputs:\n\n";
1214
  for (let i = 0; i < calledFuncs.length; i++) {
1215
  question += "Function: " + calledFuncs[i] + "\n";
1216
  question += "Output:\n\"\"\"\n" + (results[i] || "") + "\n\"\"\"\n\n";
@@ -1219,25 +1187,23 @@ async function anthropicMessages(req, res, body) {
1219
  question += "Answer the user's question DIRECTLY based on the outputs above. Do NOT say \"you shared\" or \"I see you've shared\" or \"it looks like\". The content inside \"\"\" blocks is YOUR function output, NOT content the user sent you.";
1220
  }
1221
 
1222
- // ⭐ 工具结果回传轮: 不携带完整 sysPrompt 和大量 tools (⚠️ 通知已提供足够上下文)
1223
- // 仅保留 5 个核心工具, 确保总长度 < 10000 不会被后面的通用截断逻辑破坏
1224
- // ⭐ 用软版提示 — 允许模型基于结果直接用文字回答, 而非被强制再发 function_call
1225
  const minimalToolsPrompt = buildToolsPromptSoft(
1226
  displayTools.filter(t => ESSENTIAL_TOOLS.has(t.name)).slice(0, 5)
1227
  .map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema || {} } }))
1228
  );
1229
  question = minimalToolsPrompt + "\n\n---\n" + question;
1230
- // 跳过后续的 langHint 注入和通用截断 — 此路径的 question 已包含语言指令且 < 10000
1231
  } else if (nonSys.length > 1 && realUserMsgs.length >= 1) {
1232
- // 取真正的用户消息(过滤掉系统注入的元数据)
1233
- const realMsgs = nonSys
1234
  .filter(m => {
1235
  const c = stripMetadata(extractContent(m));
1236
  return c.length > 5;
1237
  });
1238
  if (realMsgs.length > 1) {
1239
- // ⭐ 修复: 从 6 条扩展到 20 条,并按总字符限制动态裁剪(保留最新的消息)
1240
- const MAX_HISTORY_CHARS = 6000; // 留出空间给 sysPrompt + toolsPrompt
1241
  const recentMsgs = realMsgs.slice(-20);
1242
  let historyParts = [];
1243
  let historyLen = 0;
@@ -1251,16 +1217,14 @@ async function anthropicMessages(req, res, body) {
1251
  question = historyParts.join("\n");
1252
  question = (sysPrompt ? sysPrompt + "\n\n" : "") + (toolsPrompt ? toolsPrompt + "\n\n---\n" : "") + question;
1253
  } else {
1254
- // 只有一条真正消息,走单轮路径
1255
- question = (sysPrompt ? "(Background)\n" + sysPrompt + "\n\n" : "") + toolsPrompt + "\n\n---\nUser message (respond to this):\n" + lastUserMsg;
1256
  }
1257
  } else {
1258
  let context = sysPrompt;
1259
  const overhead = toolsPrompt.length + lastUserMsg.length + 200;
1260
  const remaining = MAX_QUESTION - overhead;
1261
  if (context && context.length > remaining && remaining > 0) {
1262
- // ⭐ 修复: 保留头部(核心规则)+ 尾部(最新文件列表),中间裁剪
1263
- // 而非旧逻辑的"只保留前500字+目录",那样会丢失大量重要指令
1264
  const layoutMatch = context.match(/<project_layout>([\s\S]*?)<\/project_layout>/);
1265
  const layoutSummary = layoutMatch ? layoutMatch[1].trim().split("\n").slice(0, 30).join("\n") : "";
1266
  const headLen = Math.min(800, Math.floor(remaining * 0.6));
@@ -1274,7 +1238,7 @@ async function anthropicMessages(req, res, body) {
1274
  question = (context ? "(Background)\n" + context + "\n\n" : "") + toolsPrompt + "\n\n---\nUser message (respond to this):\n" + lastUserMsg;
1275
  }
1276
 
1277
- // ⭐ 注入语言提示 & 截断 (工具结果回传轮跳过 — 已自带语言指令且 < 10000)
1278
  if (!toolResultPath) {
1279
  question += langHint;
1280
 
@@ -1291,18 +1255,16 @@ async function anthropicMessages(req, res, body) {
1291
  }
1292
 
1293
 
1294
- // 会话 — ⭐ 关键: 必须复用 IMA session 才能保持对话记忆
1295
  const convId = req.headers["x-conversation-id"] || req.headers["x-session-id"] || "";
1296
- // ⭐ 修复: 用系统 prompt + 第一条用户消息的 hash 作为稳定会话锚点
1297
- // Claude Code 不发 x-conversation-id, 但同一对话的 system prompt 内容固定
1298
  let effectiveConvId = convId;
1299
  if (!effectiveConvId && messages.length > 0) {
1300
- // 优先用 system prompt (Claude Code 在 system 里放项目路径等固定信息)
1301
  const sysMsg = messages.find(m => m.role === "system");
1302
  const anchor = sysMsg
1303
- ? extractContent(sysMsg).slice(0, 200) // system prompt 前200字,通常含项目路径
1304
- : extractContent(messages[0]).slice(0, 200); // fallback: 第一条消息
1305
- effectiveConvId = "conv-" + crypto.createHash("md5").update(anchor).digest("hex").slice(0, 12);
1306
  }
1307
  let sessionId;
1308
  const isNewSession = !getCachedSession(effectiveConvId);
@@ -1312,7 +1274,7 @@ async function anthropicMessages(req, res, body) {
1312
  return json(res, 502, { type: "error", error: { type: "api_error", message: "Session init failed: " + e.message } });
1313
  }
1314
 
1315
- // --- 非流式 ---
1316
  if (!stream) {
1317
  try {
1318
  const events = await imaQaStreamWithRetry(effectiveConvId, question, model.type, model.id);
@@ -1340,8 +1302,8 @@ async function anthropicMessages(req, res, body) {
1340
  return json(res, 200, {
1341
  id: "msg_" + crypto.randomUUID().slice(0, 8),
1342
  type: "message", role: "assistant", model: modelKey,
1343
- // ⭐ 兜底: 空文本时给出占位, 避免客户端收到空 content
1344
- content: [{ type: "text", text: text || "(本轮没有生成内容,请重试或换一种问法。)" }],
1345
  stop_reason: "end_turn", stop_sequence: null,
1346
  usage: { input_tokens: 0, output_tokens: 0 },
1347
  });
@@ -1351,7 +1313,7 @@ async function anthropicMessages(req, res, body) {
1351
  }
1352
  }
1353
 
1354
- // --- 流式 ---
1355
  cors(res);
1356
  res.writeHead(200, {
1357
  "Content-Type": "text/event-stream",
@@ -1400,10 +1362,9 @@ async function anthropicMessages(req, res, body) {
1400
 
1401
  let stopReason = "end_turn";
1402
  let done = false;
1403
- let sentText = false; // ⭐ 是否已发送过任何文本 delta (兜底判断)
1404
  const filter = new StreamFilter();
1405
- let calls = []; // 在 try 外声明,供后续 tool_use 发送使用
1406
- try {
1407
  const events = await imaQaStreamWithRetry(effectiveConvId, question, model.type, model.id);
1408
 
1409
  for await (const evt of events) {
@@ -1413,6 +1374,12 @@ async function anthropicMessages(req, res, body) {
1413
  break;
1414
  }
1415
  const et = eventTexts(evt);
 
 
 
 
 
 
1416
  // Emit thinking content as separate content block
1417
  if (et.think) {
1418
  if (DEBUG_SSE) console.log("[SSE-THINK] " + JSON.stringify(et.think).slice(0, 200));
@@ -1426,42 +1393,49 @@ async function anthropicMessages(req, res, body) {
1426
  }
1427
  if (et.text) {
1428
  ensureBlock("text");
1429
- const clean = filter.feed(et.text);
1430
- if (clean) { em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: clean } }); sentText = true; }
 
 
 
 
 
 
 
 
 
 
 
1431
  }
1432
  }
1433
 
1434
- // 冲刷残留缓冲
1435
- ensureBlock("text");
1436
  const flushed = filter.flush();
1437
  if (flushed && !resClosed) { em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: flushed } }); sentText = true; }
1438
 
1439
  calls = filter.parseCalls();
1440
- // ⭐ 无法解析的 function_call 残留 — 作为文本输出, 避免静默丢失
1441
  if (filter.leftover && !resClosed) { em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: filter.leftover } }); sentText = true; }
1442
 
1443
- // ⭐ 兜底: 流结束但既无文本也无工具调用 — 绝不让客户端收到完全空的回复。
1444
- // 这是 Claude Code "执行命令后无任何回复就结束" 的最终防线。
1445
- if (!sentText && calls.length === 0 && !resClosed) {
1446
  const fb = done
1447
- ? "(本轮没有生成内容,请重试或换一种问法。)"
1448
- : "(响应在完成前被中断,请重试。)";
1449
  ensureBlock("text");
1450
  em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: fb } });
1451
  sentText = true;
1452
  }
1453
  } catch (e) {
1454
  console.error(`[ANTHROPIC-STREAM-ERR] ${e.message}`);
1455
- // ⭐ 异常路径同样兜底, 保证至少有一段文本
1456
- if (!sentText && !resClosed) {
1457
- try { em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: "(响应处理出错,请重试。)" } }); sentText = true; } catch {}
1458
  }
1459
  }
1460
 
1461
- // 结束文本块 (仅在未截停时)
1462
  if (!resClosed) closeBlock();
1463
 
1464
- // ⭐ 发送函数调用 — 使用上面已解析的 calls (避免重复解析)
1465
  if (!resClosed && calls.length > 0) {
1466
  stopReason = "tool_use";
1467
  for (let i = 0; i < calls.length; i++) {
@@ -1482,7 +1456,7 @@ async function anthropicMessages(req, res, body) {
1482
  }
1483
 
1484
  // ============================================================
1485
- // 10. 路由
1486
  // ============================================================
1487
  async function router(req, res) {
1488
  cors(res);
@@ -1523,8 +1497,7 @@ async function router(req, res) {
1523
  });
1524
  body = raw ? JSON.parse(raw) : {};
1525
 
1526
- // body 已解析
1527
- } catch (e) {
1528
  return json(res, 400, { error: { type: "invalid_request_error", message: "Invalid JSON body" } });
1529
  }
1530
  }
@@ -1579,17 +1552,18 @@ async function router(req, res) {
1579
  }
1580
 
1581
  // ============================================================
1582
- // 11. 启动
1583
  // ============================================================
1584
  const PORT = process.env.PORT || CONFIG.server?.port || 8080;
1585
  const HOST = CONFIG.server?.host || "0.0.0.0";
1586
 
1587
  http.createServer(router).listen(PORT, HOST, () => {
1588
  console.log(`\n${'='.repeat(50)}`);
1589
- console.log(`[STARTUP] 服务启动于 http://${HOST}:${PORT}`);
1590
  console.log(`[STARTUP] API keys count: ${API_KEYS.size} (source: ${(process.env.IMA_API_KEYS || "") ? "env IMA_API_KEYS" : "config.json"})`);
1591
  for (const k of API_KEYS) {
1592
  console.log(`[STARTUP] key: ${maskKey(k)} (len=${k.length})`);
1593
  }
1594
  setInterval(refreshCookie, 60 * 1000);
1595
  });
 
 
1
+ "use strict";
2
 
3
  const https = require("https");
4
  const http = require("http");
5
  const crypto = require("crypto");
6
 
7
  // ============================================================
8
+ // 1. 閰嶇疆
9
  // ============================================================
10
  const CONFIG = require("./config.json");
11
  const BASE_HOST = "ima.qq.com";
 
32
  }
33
 
34
  // ============================================================
35
+ // 2. HTTP 瀹㈡埛绔?// ============================================================
 
36
  const IMA_HEADERS = {
37
  "from_browser_ima": "1",
38
  get "x-ima-cookie"() { return COOKIE; },
 
41
  origin: BASE_URL,
42
  "User-Agent": "okhttp/4.12.0",
43
  "Content-Type": "application/json; charset=utf-8",
44
+ "Accept-Encoding": "gzip", // 猸?淇: 涓庣湡瀹?App 璇锋眰澶翠竴鑷?};
 
45
 
46
  function imaPost(path, body, extraH = {}, timeout = 30000) {
47
  return new Promise((resolve, reject) => {
 
51
  headers: { ...IMA_HEADERS, ...extraH, "Content-Length": Buffer.byteLength(payload) },
52
  timeout,
53
  }, (res) => {
54
+ // 猸?淇: 澶勭悊 gzip 鍘嬬缉鍝嶅簲锛堜笌 Accept-Encoding: gzip 閰嶅锛? const zlib = require("zlib");
 
55
  const encoding = res.headers["content-encoding"] || "";
56
  let stream = res;
57
  if (encoding === "gzip") stream = res.pipe(zlib.createGunzip());
 
181
  const _TEXT_KEYS = new Set(["Text", "text", "Content", "content", "Delta", "delta", "Msg", "msg", "Message", "reply", "Reply", "answer", "Answer"]);
182
  const _THINK_KEYS = new Set(["think", "Think", "thinking", "Thinking", "reasoning",
183
  "reasoning_content", "Reasoning", "thought", "Thought",
184
+ "鎬濈淮", "鎬濊€?, "鎺ㄧ悊",
185
  "reasoningText", "thinkContent", "think_text"]);
186
 
187
  function extractEventText(d) {
 
209
  if (d == null) return { text: "", think: "" };
210
  // THINKING events: IMA uses "Message" key for streaming thinking content
211
  if (evt.event === "THINKING" && typeof d.Message === "string") return { text: "", think: d.Message };
212
+ const text = extractEventText(d);
213
+ const think = extractThinkText(d);
214
+ // Diaagnostic: log when non-THINKING event has empty text (forced flush to stderr)
215
+ if (!text && !think && evt.data && evt.event !== "THINKING") {
216
+ process.stderr.write("[EVT-MISS] event=" + (evt.event || "(none)") + " raw=" + evt.data.slice(0, 300) + "\n");
217
+ }
218
+ return { text, think };
219
  }
220
 
221
  function eventText(evt) {
 
234
  async function imaInitSession(question) {
235
  const { data } = await imaPost("/cgi-bin/session_logic/init_session", {
236
  env_info: { interact_type: 2, robot_type: 10000 },
237
+ name: (question || "鏂板璇?).slice(0, 50),
238
+ msgs_limit: 20, // IMA 涓婇檺涓?20锛堣秴杩囦細鎶?code=51锛? });
 
239
  if (data.code === 0) return data.session_id;
240
  throw new Error(`InitSession failed: code=${data.code} msg=${data.msg}`);
241
  }
 
265
  }
266
 
267
  // ============================================================
268
+ // 4. 浼氳瘽缂撳瓨
269
  // ============================================================
270
  const sessions = new Map();
271
  const SESSION_TTL = 30 * 60 * 1000;
 
287
  return id;
288
  }
289
 
290
+ // 猸?淇: IMA 浼氳瘽杈惧埌 msgs_limit 鍚庤嚜鍔ㄩ噸寤猴紝閬垮厤"绐佺劧缁撴潫"
291
  async function imaQaStreamWithRetry(convId, question, modelType, modelId) {
292
  let sessionId = await ensureSession(convId, question);
293
  const events = await imaQaStream(sessionId, question, modelType, modelId);
294
+ // 鏀堕泦骞舵娴嬫槸鍚﹁Е鍙戜細璇濋檺鍒堕敊璇紙INNER_EXCEPTION 鍚?session limit锛? return (async function*() {
 
295
  let hitLimit = false;
296
  for await (const evt of events) {
297
  if (evt.event === "INNER_EXCEPTION" || evt.event === "ERROR" || evt.event === "FAILED") {
298
+ // IMA 浼氳瘽婊′簡閫氬父杩斿洖 INNER_EXCEPTION锛屾鏃堕噸寤?session 閲嶈瘯涓€娆? try {
 
299
  const newId = await ensureSession(convId, question, true);
300
  const retryEvents = await imaQaStream(newId, question, modelType, modelId);
301
  for await (const e2 of retryEvents) yield e2;
302
  } catch (_) {
303
+ yield evt; // 閲嶈瘯涔熷け璐ワ紝鎶婂師濮嬩簨浠堕€忎紶鍑哄幓
304
  }
305
  return;
306
  }
 
310
  }
311
 
312
  // ============================================================
313
+ // 5. 妯″瀷
314
  // ============================================================
315
  const MODELS = CONFIG.models;
316
  const DEFAULT_MODEL = CONFIG.default_model;
 
326
  }
327
 
328
  // ============================================================
329
+ // 6. 璁よ瘉 & 宸ュ叿
330
  // ============================================================
331
  function checkAuth(req) {
332
  const bearer = (req.headers["authorization"] || "").replace(/^Bearer\s+/i, "");
 
356
  return msg.content.map(c => {
357
  if (c.type === "text") return c.text;
358
  if (c.type === "tool_result") {
359
+ // Anthropic tool_result content 鍙兘鏄?string 鎴?content_block[]
360
  const tc = typeof c.content === "string" ? c.content
361
  : Array.isArray(c.content) ? c.content.map(cc => cc.text || "").join("") : "";
362
  return `Tool result:\n${tc}`;
 
368
  }
369
 
370
  // ============================================================
371
+ // 7. Function Calling 鈥?Prompt 娉ㄥ叆寮曟搸
372
  // ============================================================
373
 
374
  function escapeRawCtrlInStrings(s) {
 
388
  return out;
389
  }
390
 
391
+ // 淇 LLM 甯歌鐨?JSON 閿欒 (鏈浆涔夊紩鍙枫€佸熬閫楀彿銆佺己鎷彿銆佸瓧绗︿覆鍐呰8鎹㈣)
392
  function tryRepairJson(raw) {
393
  try { return JSON.parse(raw); } catch (e) { /* continue */ }
394
  let fixed = raw;
395
+ // 0. 杞箟瀛楃涓插唴鐨勮8鎺у埗绗?(鍐欐枃浠舵椂 content 鍚湡瀹炴崲琛?鈫?鍚﹀垯 JSON.parse 澶辫触)
396
  fixed = escapeRawCtrlInStrings(fixed);
397
  try { return JSON.parse(fixed); } catch (e) { /* continue */ }
398
+ // 1. 鍘诲熬閫楀彿
399
  fixed = fixed.replace(/,(\s*[}\]])/g, '$1');
400
  try { return JSON.parse(fixed); } catch (e) { /* continue */ }
401
+ // 2. 琛ョ己澶辩殑闂悎鎷彿
402
  let depth = 0;
403
  for (const c of fixed) {
404
  if (c === '{' || c === '[') depth++;
 
410
  fixed += closer.repeat(Math.min(depth, 5));
411
  try { return JSON.parse(fixed); } catch (e) { /* continue */ }
412
  }
413
+ // 3. 鎻愬彇 name + arguments (鍗充娇 JSON 鐮存崯涔熻兘灏藉彲鑳芥仮澶?
414
  const nameMatch = fixed.match(/"name"\s*:\s*"([^"]+)"/);
415
  if (nameMatch) {
416
  const argsStart = fixed.indexOf('{', fixed.indexOf('"arguments"'));
 
427
  return null;
428
  }
429
 
430
+ // 鍘嬬缉 JSON Schema: 鍘绘帀 $schema/$defs/$ref 绛夊厓鏁版嵁锛屽崟琛岀揣鍑戣緭鍑?function compactSchema(schema) {
 
431
  if (!schema || typeof schema !== "object") return "{}";
432
+ // 娣辨嫹璐濆悗閫掑綊娓呯悊
433
  function clean(obj) {
434
  if (Array.isArray(obj)) return obj.map(clean);
435
  if (obj && typeof obj === "object") {
436
  const out = {};
437
  for (const [k, v] of Object.entries(obj)) {
438
+ if (k.startsWith("$")) continue; // 鍘绘帀 $schema, $defs, $ref, $dynamicAnchor 绛? out[k] = clean(v);
 
439
  }
440
  return out;
441
  }
442
  return obj;
443
  }
444
+ return JSON.stringify(clean(schema)); // 鍗曡锛屾棤缂╄繘
445
  }
446
 
447
  function buildToolsPrompt(tools) {
 
459
  })
460
  .join("\n");
461
 
462
+ return `## 鈿狅笍 CRITICAL 鈥?YOU MUST USE FUNCTION CALLING
463
 
464
  You have access to these functions. When the user asks you to do something
465
  that a function can handle, you MUST call the function. NEVER say "I cannot"
466
  or "I don't have the ability". NEVER tell the user to save a file, run a
467
+ command, or do any step themselves 鈥?DO IT by calling the function (e.g. use
468
  Write to create files, Bash to run commands). ALWAYS use a function instead.
469
 
470
  ${funcDefs}
 
481
  - arguments MUST be a valid JSON object matching the function's parameters
482
  - For file content, put the FULL content in the string value (newlines allowed)
483
  - Do NOT add any text before or after the <function_call> block
484
+ - NEVER output file content or code for the user to copy 鈥?write it via Write`;
485
  }
486
 
487
+ // 猸?宸ュ叿缁撴灉鍥炰紶杞笓鐢?鈥?"缁х画琛屽姩"鐗堝伐鍏锋彁绀恒€?// 淇鍘嗗彶: 鈶?鏃х殑寮哄埗鐗?("YOU MUST call / 涓嶈杈撳嚭浠讳綍鏂囨湰") 璁╂ā鍨嬪洖浼犺疆瑕佷箞鍐嶅彂澶氫綑
488
+ // 璋冪敤瑕佷箞娌夐粯; 鈶?杩囪蒋鐗?("鏈夎冻澶熶俊鎭氨鐢ㄧ函鏂囨湰浣滅瓟") 鍙堣蛋鍚戝彟涓€鏋佺 鈥斺€?妯″瀷鎶?璁╂垜鍏堣
489
+ // 鍙栨枃浠?杩欑銆愭剰鍥惧彊杩般€戝綋鎴愬洖绛旂劧鍚庡仠鎵? 澶氭浠诲姟鍗婇€旇€屽簾銆?// 鏈増鏍稿績鍘熷垯: 鍙欒堪鎰忓浘 鈮?瀹屾垚浠诲姟銆傚彧瑕佽繕鏈夋湭瀹屾垚鐨勬楠? 蹇呴』銆愬湪鏈疆灏卞彂鍑哄嚱鏁拌皟鐢ㄣ€?
490
+ // 涓嶈兘鍙"鎴戞帴涓嬫潵瑕佸仛 X"銆傚彧鏈夊綋浠诲姟鐪熸鍏ㄩ儴瀹屾垚鏃? 鎵嶇敤绾枃鏈粰鍑烘渶缁堢瓟澶嶃€?function buildToolsPromptSoft(tools) {
 
 
 
491
  if (!tools || tools.length === 0) return "";
492
 
493
  const funcDefs = tools
 
505
 
506
  ${funcDefs}
507
 
508
+ ## CRITICAL 鈥?keep going until the task is fully done
509
 
510
  The user's request may need MULTIPLE steps. After each function result, decide:
511
+ the task is NOT finished yet 鈫?call the next function NOW; the task IS fully
512
+ finished 鈫?give the final answer in plain text.
513
 
514
  NEVER reply with only your intention (e.g. "Let me read the file", "I'll now
515
+ start the server", "涓嬩竴姝ユ垜灏嗏€?). Stating intent is NOT an action and NOT an
516
  answer. If you intend to do something, you MUST emit the function call for it
517
  in THIS SAME reply.
518
 
 
525
  <function_call> block.`;
526
  }
527
 
528
+ // 猸?StreamFilter: 瀹炴椂杩囨护娴佸紡杈撳嚭涓殑 <function_call> 鍧?// 闃叉 Claude Code 瀹㈡埛绔洜鐪嬪埌 XML 鏍囪鑰屾埅鍋?class StreamFilter {
 
 
529
  constructor() {
530
+ this._buf = ''; // 鏈喅缂撳啿鍖? this._emitted = ''; // 宸茶緭鍑虹殑绾噣鏂囨湰
531
+ this._funcBlocks = []; // 鎹曡幏鐨?function_call JSON
532
+ this._inFunc = false; // 鏄惁鍦?<function_call> 鍐呴儴
 
533
  this._tagLen = 16; // '<function_call>'.length
534
+ this._leftover = ''; // 猸?parseCalls 鏃犳硶瑙f瀽鐨勬畫鐣欐枃鏈?(渚涙祦寮忓眰鍏滃簳杈撳嚭)
535
  }
536
 
537
+ // 鍠傚叆鏂版枃鏈潡锛岃繑鍥炲簲鍙戦€佺粰瀹㈡埛绔殑绾噣鏂囨湰 (鍙兘涓虹┖)
538
  feed(chunk) {
539
  this._buf += chunk;
540
  const out = [];
 
545
  if (!this._inFunc) {
546
  const idx = this._buf.indexOf(startTag);
547
  if (idx === -1) {
548
+ // 娌℃湁鍙戠幇寮€濮嬫爣绛?鈥?浣嗗厛妫€鏌ュ熬閮ㄦ槸鍚﹂儴鍒嗗尮閰? const partialLen = this._partialMatch(startTag);
 
549
  if (partialLen > 0) {
550
+ // 淇濈暀鍙兘鐨勯儴鍒嗘爣绛惧湪缂撳啿鍖? const safe = this._buf.slice(0, -partialLen);
 
551
  if (safe) out.push(safe);
552
  this._buf = this._buf.slice(-partialLen);
553
  break;
554
  }
555
+ // 瀹屽叏瀹夊叏锛屽叏閮ㄨ緭鍑? out.push(this._buf);
 
556
  this._buf = '';
557
  break;
558
  }
559
+ // 鍙戠幇寮€濮嬫爣绛?鈥?杈撳嚭鏍囩涔嬪墠鐨勬枃鏈? if (idx > 0) out.push(this._buf.slice(0, idx));
 
560
  this._buf = this._buf.slice(idx + startTag.length);
561
  this._inFunc = true;
562
  }
 
564
  if (this._inFunc) {
565
  const idx = this._buf.indexOf(endTag);
566
  if (idx === -1) {
567
+ // 杩樻病鎵惧埌缁撴潫鏍囩 鈥?鍏ㄩ儴鎶戝埗
568
  break;
569
  }
570
+ // 鎵惧埌缁撴潫鏍囩 鈥?鎹曡幏鍑芥暟璋冪敤 JSON
571
  const json = this._buf.slice(0, idx).trim();
572
  if (json) this._funcBlocks.push(json);
573
  this._buf = this._buf.slice(idx + endTag.length);
574
  this._inFunc = false;
575
+ // 缁х画寰幆锛屽彲鑳藉悗闈㈣繕鏈夋枃鏈垨鏇村 function_call
576
  }
577
  }
578
 
 
581
  return text;
582
  }
583
 
584
+ // 妫€鏌ョ紦鍐插尯鏈熬鏄惁閮ㄥ垎鍖归厤 tag
585
  _partialMatch(tag) {
586
  for (let i = tag.length - 1; i > 0; i--) {
587
  if (this._buf.endsWith(tag.slice(0, i))) return i;
 
589
  return 0;
590
  }
591
 
592
+ // 娴佺粨鏉熸椂璋冪敤 鈥?杩斿洖鍙兘娈嬬暀鐨勭紦鍐插尯鍐呭 (涓嶅惈 function_call 閮ㄥ垎)
593
  flush() {
594
  if (this._inFunc) {
595
+ // 猸?淇: 娴佸湪 </function_call> 鍒拌揪鍓嶅氨缁撴潫浜?(IMA 鎴柇 / 妯″瀷婕忓啓闂悎鏍囩)銆? // 鏃ч€昏緫鐩存帴涓㈠純缂撳啿鍖? 瀵艰嚧"璋冪敤鍛戒护鍚庢棤浠讳綍鍥炲" 鈥?杩欓噷鏀逛负鎶婃畫鐣?JSON
596
+ // 褰撲綔涓€涓?function block 鎹曡幏, 浜ょ粰 parseCalls() 鍘诲敖鍔涗慨澶嶃€? const pending = this._buf.trim();
 
 
597
  if (pending) this._funcBlocks.push(pending);
598
  this._buf = '';
599
  this._inFunc = false;
 
608
  return '';
609
  }
610
 
611
+ // 瑙f瀽宸叉崟鑾风殑鎵€鏈?function_call 鍧? parseCalls() {
 
612
  const calls = [];
613
  for (const raw of this._funcBlocks) {
614
  const parsed = tryRepairJson(raw);
 
622
  },
623
  });
624
  } else if (raw.length > 0) {
625
+ // 猸?JSON 淇澶辫触 鈥?绱Н鍒?_leftover, 鐢辨祦寮忓眰鍏滃簳杈撳嚭, 閬垮厤瀵硅瘽闈欓粯涓柇銆? // (闈炴祦寮忚蛋 cleanText/_emitted; 娴佸紡灞傞渶璇诲彇 leftover 鍗曠嫭 emit)
 
626
  const note = '\n[Function call (malformed)]:\n' + raw + '\n';
627
  this._emitted += note;
628
  this._leftover += note;
 
639
  function parseFunctionCalls(text) {
640
  if (!text) return { found: false, text: "" };
641
 
642
+ // 涔熷尮閰?```json {...} ``` 鏍煎紡 (妯″瀷鍙兘杈撳嚭 JSON 浠g爜鍧?
643
  const jsonBlockRegex = /```(?:json)?\s*\n?\s*(\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})\s*\n?\s*```/g;
644
 
645
  const regex = /<function_call>\s*\n?\s*(\{[\s\S]*?\})\s*\n?\s*<\/function_call>/g;
646
  const calls = [];
647
  let cleanText = text;
648
 
649
+ // 灏濊瘯涓ょ鏍煎紡: <function_call> 鍜?```json name/arguments
650
  const patterns = [
651
  regex,
652
  /```(?:json)?\s*\n?\s*\{[^{}]*"name"\s*:\s*"[^"]+"[^{}]*"arguments"\s*:\s*\{[\s\S]*?\}\s*\}\s*\n?\s*```/g,
 
675
  return { found: true, calls, text: cleanText };
676
  }
677
 
678
+ // 璋冭瘯: 妫€鏌ユā鍨嬫槸鍚﹁緭鍑轰簡杩戜技鐨?function call 鏍煎紡
679
  if (text.includes('function') || text.includes('tool') || text.includes('bash') || text.includes('read_file')) {
680
  }
681
  return { found: false, text };
 
692
  const tools = body.tools || null;
693
  const toolChoice = body.tool_choice || null;
694
 
695
+ // --- 鏋勫缓 question ---
696
  const hasToolResults = messages.some(m => m.role === "tool");
697
  const hasToolCalls = messages.some(m => m.role === "assistant" && m.tool_calls);
698
  let question;
699
 
700
+ // 纭畾宸ュ叿闆?(涓や釜鍒嗘敮鍏辩敤)
701
  let effectiveTools = tools;
702
  if ((!effectiveTools || effectiveTools.length === 0) && toolChoice !== "none") {
703
  effectiveTools = [
 
708
  {type: "function", function: {name: "Grep", description: "Search file contents", parameters: {type: "object", properties: {pattern: {type: "string"}, path: {type: "string"}}, required: ["pattern"]}}},
709
  ];
710
  }
711
+ // 猸?鏍囬鐢熸垚璇锋眰涓嶉渶瑕?tools
712
  const _sysParts = messages.filter(m => m.role === "system").map(m => extractContent(m));
713
  const _sysPrompt = _sysParts.length > 0 ? _sysParts.join("\n") : "";
714
  const isTitleGenOAI = _sysPrompt.includes('Generate a concise, sentence-case title');
715
 
716
+ // 猸?闄愬埗宸ュ叿鏁伴噺锛岄槻姝?prompt 瓒呭嚭闄愬埗
717
  const MAX_TOOLS_OAI = 8;
718
  const ESSENTIAL_OAI = new Set(['Bash', 'Read', 'Write', 'Glob', 'Grep']);
719
  if (isTitleGenOAI) {
720
+ effectiveTools = []; // 鏍囬鐢熸垚涓嶉渶瑕佸伐鍏? } else if (effectiveTools && effectiveTools.length > MAX_TOOLS_OAI) {
 
721
  const essential = effectiveTools.filter(t => ESSENTIAL_OAI.has(t.function?.name));
722
  const others = effectiveTools.filter(t => !ESSENTIAL_OAI.has(t.function?.name));
723
  const available = MAX_TOOLS_OAI - essential.length;
 
730
 
731
  if (hasToolResults && hasToolCalls) {
732
  toolResultPathOAI = true;
733
+ // 宸ュ叿缁撴灉鍥炰紶杞?鈥?鍗曟绯荤粺閫氱煡鏍煎紡
734
  const lastUserOAI = messages.filter(m => m.role === "user").pop();
735
  const lastUserTextOAI = extractContent(lastUserOAI || {});
736
+ const cjkOAI = /[涓€-榭裤悁-涠縘/.test(lastUserTextOAI);
737
 
738
+ // 鏀堕泦鍑芥暟璋冪敤鍜岀粨鏋? const calledOAI = [], resultsOAI = [];
 
739
  for (const m of messages) {
740
  if (m.role === "assistant" && m.tool_calls) {
741
  for (const tc of m.tool_calls) calledOAI.push(tc.function.name + "(" + tc.function.arguments + ")");
 
744
  resultsOAI.push(extractContent(m).slice(0, 3000));
745
  }
746
  }
747
+ // 鎵惧埌鐢ㄦ埛鍘熷鎻愰棶
748
  const userQsOAI = [];
749
  for (const m of messages) {
750
  if (m.role !== "user") continue;
 
755
  const origQ = userQsOAI.length > 0 ? userQsOAI[userQsOAI.length - 1] : (lastUserTextOAI || "");
756
 
757
  if (cjkOAI) {
758
+ question = "鈿狅笍 绯荤粺閫氱煡锛氫綘鍒氭墠璋冪敤浜嗕互涓嬪嚱鏁帮紝杩斿洖缁撴灉濡備笅锛歕n\n";
759
  for (let i = 0; i < calledOAI.length; i++) {
760
+ question += "鍑芥暟璋冪敤: " + calledOAI[i] + "\n杩斿洖缁撴灉:\n\"\"\"\n" + (resultsOAI[i] || "") + "\n\"\"\"\n\n";
761
  }
762
+ question += "鐢ㄦ埛鍘熷鎻愰棶: \"" + origQ + "\"\n\n璇风敤绠€浣撲腑鏂囩洿鎺ュ洖绛旂敤鎴风殑闂銆傝鍑虹瓟妗堝嵆鍙€備笉瑕佽\"浣犲垎浜簡\"鎴朶"鐪嬭捣鏉ュ儚鏄痋"銆備笂闈㈢敤 \"\"\" 鍖呰9鐨勫唴瀹规槸浣犺嚜宸辫皟鐢ㄥ嚱鏁板緱鍒扮殑杩斿洖缁撴灉銆?;
763
  } else {
764
+ question = "鈿狅笍 SYSTEM: You just called these functions and received these outputs:\n\n";
765
  for (let i = 0; i < calledOAI.length; i++) {
766
  question += "Function: " + calledOAI[i] + "\nOutput:\n\"\"\"\n" + (resultsOAI[i] || "") + "\n\"\"\"\n\n";
767
  }
768
  question += "User's original question: \"" + origQ + "\"\n\nAnswer DIRECTLY based on the outputs. Do NOT say \"you shared\" or \"it looks like\". The \"\"\" content is YOUR function output, NOT user input.";
769
  }
770
 
771
+ // 宸ュ叿缁撴灉鍥炰紶杞? 涓嶆惡甯﹀畬鏁?sysPrompt, 浠?5 涓牳蹇冨伐鍏? // 猸?鐢ㄨ蒋鐗堟彁绀?鈥?鍏佽妯″瀷鐩存帴鐢ㄦ枃瀛椾綔绛? 閬垮厤琚己鍒跺啀鍙?function_call 鑰屾矇榛? const minimalOAI = buildToolsPromptSoft(
 
 
772
  (effectiveTools || []).filter(t => ESSENTIAL_OAI.has(t.function?.name)).slice(0, 5)
773
  );
774
  question = minimalOAI + "\n\n---\n" + question;
775
  } else {
776
+ // 鏅€氶棶绛?/ 棣栬疆 tool calling
777
  let sysPrompt = "";
778
  const sysParts = messages.filter(m => m.role === "system").map(m => extractContent(m));
779
  if (sysParts.length > 0) sysPrompt = sysParts.join("\n");
 
784
  return json(res, 400, { error: { message: "No user message", type: "invalid_request_error" } });
785
  }
786
 
787
+ // 猸?璺宠繃绯荤粺娉ㄥ叆娑堟伅 (<session>, <system-reminder>)
788
  const realUserMsgs = userMsgs.filter(m => {
789
  const c = extractContent(m);
790
  return !c.startsWith('<session') && !c.startsWith('<system-reminder') && c.length > 10;
 
793
  ? extractContent(realUserMsgs[realUserMsgs.length - 1])
794
  : extractContent(userMsgs[userMsgs.length - 1]);
795
 
796
+ // 猸?璇█妫€娴? const hasCJKOAI = /[涓€-榭裤悁-涠縘/.test(lastUserMsg);
797
+ const langHintOAI = hasCJKOAI ? "\n## Language\nRespond in the same language as the user's message. The user is writing in Chinese 鈥?respond in Chinese (绠€浣撲腑鏂?.\n" : "";
 
798
 
799
+ // IMA question 瀛楁闄愬埗 10240 瀛楃
800
  const MAX_QUESTION = 10000;
801
 
802
  if (nonSys.length > 1 && realUserMsgs.length >= 1) {
 
805
  return c.length > 10 && !c.startsWith('<session') && !c.startsWith('<system-reminder');
806
  });
807
  if (realMsgs.length > 1) {
808
+ // 猸?淇: 鎵╁睍鍒?20 鏉★紝鎸夊瓧绗︽暟鍔ㄦ€佽鍓? const MAX_HISTORY_CHARS = 6000;
 
809
  const recentMsgs = realMsgs
810
  .filter(m => m.role === "user" || m.role === "assistant")
811
  .slice(-20);
 
824
  question = `${sysPrompt ? "(Background)\n" + sysPrompt + "\n\n" : ""}${toolsPrompt}\n\n---\nUser message (respond to this):\n${lastUserMsg}`;
825
  }
826
  } else {
827
+ // 猸?鏅鸿兘鍘嬬缉 system prompt (淇濈暀 toolsPrompt 瀹屾暣)
828
  let context = sysPrompt;
829
  const overhead = toolsPrompt.length + lastUserMsg.length + 200;
830
  const remaining = MAX_QUESTION - overhead;
831
 
832
  if (context && context.length > remaining && remaining > 0) {
833
+ // 猸?淇: 淇濈暀澶村熬锛屼腑闂磋鍓紙淇濈暀鏍稿績瑙勫垯 + 鏈€鏂版枃浠朵俊鎭級
834
  const layoutMatch = context.match(/<project_layout>([\s\S]*?)<\/project_layout>/);
835
  const layoutSummary = layoutMatch
836
  ? layoutMatch[1].trim().split("\n").slice(0, 30).join("\n")
 
847
  question = `${context ? "(Background)\n" + context + "\n\n" : ""}${toolsPrompt}\n\n---\nUser message (respond to this):\n${lastUserMsg}`;
848
  }
849
 
850
+ // 猸?娉ㄥ叆璇█鎻愮ず & 鎴柇 (宸ュ叿缁撴灉鍥炰紶杞烦杩?
851
  if (!toolResultPathOAI) {
852
  question += langHintOAI;
853
 
854
+ // 鏈€缁堟埅鏂?鈥?濮嬬粓淇濈暀 toolsPrompt锛屽惁鍒欐ā鍨嬬湅涓嶅埌 function 瀹氫箟
855
  if (question.length > MAX_QUESTION) {
856
  const minTemplate = "\n\n---\nUser message (respond to this):\n";
857
  const compact = toolsPrompt + minTemplate + lastUserMsg + langHintOAI;
 
870
  }
871
 
872
 
873
+ // 浼氳瘽 鈥?澶嶇敤 IMA session 淇濇寔瀵硅瘽璁板繂
874
+ // 猸?淇: 鐢ㄧ郴缁?prompt 浣滀负绋冲畾閿氱偣锛堝悓 Anthropic 璺緞淇濇寔涓€鑷达級
875
  let oaiConvId = req.headers["x-conversation-id"] || req.headers["x-session-id"] || "";
876
  if (!oaiConvId && messages.length > 0) {
877
  const sysMsg = messages.find(m => m.role === "system");
 
887
  return json(res, 502, { error: { message: "Session init failed: " + e.message, type: "api_error" } });
888
  }
889
 
890
+ // --- 闈炴祦寮?---
891
  if (!stream) {
892
  try {
893
  const events = await imaQaStreamWithRetry(oaiConvId, question, model.type, model.id);
 
904
  };
905
  choice.finish_reason = "tool_calls";
906
  } else {
907
+ // 猸?鍏滃簳: 绌烘枃鏈椂缁欏嚭鍗犱綅, 閬垮厤瀹㈡埛绔敹鍒板畬鍏ㄧ┖鐨勫洖澶? choice.message = { role: "assistant", content: text || "(鏈疆娌℃湁鐢熸垚鍐呭锛岃閲嶈瘯鎴栨崲涓€绉嶉棶娉曘€?" };
 
908
  }
909
 
910
  return json(res, 200, {
 
920
  }
921
  }
922
 
923
+ // --- 娴佸紡 ---
924
  cors(res);
925
  res.writeHead(200, {
926
  "Content-Type": "text/event-stream",
 
932
  const chatId = "chatcmpl-" + crypto.randomUUID().slice(0, 8);
933
  const created = Math.floor(Date.now() / 1000);
934
  let resClosed = false;
935
+ let sentText = false; // 猸?鏄惁宸插悜瀹㈡埛绔彂閫佽繃浠讳綍鏂囨湰/宸ュ叿鍐呭 (鍏滃簳鍒ゆ柇)
936
  res.on('close', () => { resClosed = true; });
937
 
938
  function emit(delta, finishReason, toolCalls) {
 
977
  }
978
  }
979
  if (et.text) {
 
980
  const clean = filter.feed(et.text);
981
  if (clean) emit(clean, null, null);
982
  }
983
  }
984
  if (DEBUG_SSE && !sawEvent) console.error("[SSE-EMPTY] openai stream: no data events");
985
 
986
+ // 鍐插埛娈嬬暀缂撳啿
 
987
  const flushed = filter.flush();
988
  if (flushed && !resClosed) emit(flushed, null, null);
989
 
990
+ // 妫€娴嬪苟鍙戦€佸嚱鏁拌皟鐢? if (!resClosed) {
 
991
  const calls = filter.parseCalls();
992
+ // 猸?鏃犳硶瑙f瀽鐨?function_call 娈嬬暀 鈥?浣滀负鏂囨湰杈撳嚭, 閬垮厤闈欓粯涓㈠け
993
  if (filter.leftover) emit(filter.leftover, null, null);
994
  if (calls.length > 0) {
995
+ // 猸?OpenAI streaming tool_calls: 閫愪釜鍙戦€? for (let i = 0; i < calls.length; i++) {
 
996
  const tc = calls[i];
997
  if (resClosed) break;
998
  try {
 
1010
  }) + "\n\n");
1011
  }
1012
  } else {
1013
+ // 猸?鍏滃簳: 娴佹甯哥粨鏉熶絾鏃㈡棤鏂囨湰涔熸棤宸ュ叿璋冪敤 (妯″瀷鍙洖浜嗚杩囨护鐨勫唴瀹?/
1014
+ // 绌哄搷搴?/ 寮傚父涓��)銆傜粷涓嶈瀹㈡埛绔敹鍒板畬鍏ㄧ┖鐨勫洖澶?鈥?杩欐槸"鏃犲洖澶嶅氨缁撴潫"鐨勬牴鍥犮€? if (!sentText && !resClosed) {
 
1015
  const fb = done
1016
+ ? "(鏈疆娌℃湁鐢熸垚鍐呭锛岃閲嶈瘯鎴栨崲涓€绉嶉棶娉曘€?"
1017
+ : "(鍝嶅簲鍦ㄥ畬鎴愬墠琚腑鏂紝璇烽噸璇曘€?";
1018
  emit(fb, null, null);
1019
  }
1020
+ // done 涓?true 琛ㄧず姝e父瀹屾垚
1021
  res.write("data: " + JSON.stringify({
1022
  id: chatId, object: "chat.completion.chunk", created, model: modelKey,
1023
  choices: [{ index: 0, delta: {}, finish_reason: done ? "stop" : "length" }],
 
1053
  else if (Array.isArray(body.system))
1054
  sysPrompt = body.system.filter(s => s.type === "text").map(s => s.text).join("\n");
1055
 
1056
+ // tools 鈫?prompt injection
1057
+ // 濡傛灉 Claude Code 娌″彂閫?tools锛屾敞鍏ラ粯璁ゅ伐鍏烽泦
1058
  let tools = body.tools;
1059
  if (!tools || tools.length === 0) {
1060
  tools = [
 
1066
  ];
1067
  }
1068
 
1069
+ // 猸?鏍囬鐢熸垚璇锋眰涓嶉渶瑕?tools (Claude Code 棣栬疆璇锋眰)
1070
  const isTitleGen = sysPrompt.includes('Generate a concise, sentence-case title');
1071
 
1072
+ // 猸?闄愬埗宸ュ叿鏁伴噺锛岄槻姝?prompt 瓒呭嚭 IMA 10240 瀛楃闄愬埗
1073
  const MAX_TOOLS = 8;
1074
  const ESSENTIAL_TOOLS = new Set(['Bash', 'Read', 'Write', 'Glob', 'Grep']);
1075
  let displayTools = tools;
1076
  if (isTitleGen) {
1077
+ displayTools = []; // 鏍囬鐢熸垚涓嶉渶瑕佸伐鍏? } else if (displayTools.length > MAX_TOOLS) {
1078
+ // 浼樺厛淇濈暀鏍稿績宸ュ叿锛屽啀鎸夊師椤哄簭琛ュ厖鍏朵粬宸ュ叿
 
1079
  const essential = displayTools.filter(t => ESSENTIAL_TOOLS.has(t.name));
1080
  const others = displayTools.filter(t => !ESSENTIAL_TOOLS.has(t.name));
1081
  const available = MAX_TOOLS - essential.length;
1082
  displayTools = [...essential, ...others.slice(0, Math.max(0, available))];
1083
  }
1084
+ // 鈥斺€?杩囨护娑堟伅 鈥斺€? // 猸?妫€娴?Anthropic 鏍煎紡鐨?tool_use / tool_result (澶氳疆鍥炰紶)
 
1085
  function msgHasType(msg, type) {
1086
  if (Array.isArray(msg.content)) return msg.content.some(c => c.type === type);
1087
  return false;
 
1089
  const hasToolUses = messages.some(m => m.role === "assistant" && msgHasType(m, "tool_use"));
1090
  const hasToolResults = messages.some(m => m.role === "user" && msgHasType(m, "tool_result"));
1091
 
1092
+ // 猸?娓呯悊鐢ㄦ埛娑堟伅涓殑绯荤粺娉ㄥ叆鍏冩暟鎹? function stripMetadata(text) {
 
1093
  return text
1094
+ // 鍒犻櫎鍏冩暟鎹潡 (鍚唴瀹逛竴璧峰垹)
1095
  .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
1096
  .replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '')
1097
  .replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '')
1098
+ // 鍒犻櫎鍗曡鍛戒护鍏冩暟鎹? .replace(/<command-name>[^<]*<\/command-name>/g, '')
 
1099
  .replace(/<command-message>[^<]*<\/command-message>/g, '')
1100
  .replace(/<command-args>[^<]*<\/command-args>/g, '')
1101
+ // 猸?<session> 鍙垹鏍囩銆佷繚鐣欏唴瀹?(鐢ㄦ埛娑堟伅鍦?session 鍐?
1102
  .replace(/<\/?session>/g, '')
1103
  .trim();
1104
  }
 
1108
  if (userMsgs.length === 0)
1109
  return json(res, 400, { type: "error", error: { type: "invalid_request_error", message: "No user message" } });
1110
 
1111
+ // 猸?鎻愬彇鏈€鍚庝竴鏉$敤鎴锋秷鎭殑瀹炶川鍐呭
1112
  const realUserMsgs = userMsgs.filter(m => {
1113
  const c = stripMetadata(extractContent(m));
1114
  return c.length > 5;
 
1120
  if (!lastUserMsg) {
1121
  }
1122
 
1123
+ // 猸?璇█妫€娴? 鐢ㄦ埛鐢ㄤ腑鏂囧垯瑕佹眰涓枃鍥炲
1124
+ const hasCJK = /[涓€-榭裤悁-涠縘/.test(lastUserMsg);
1125
+ const langHint = hasCJK ? "\n## Language\nRespond in the same language as the user's message. The user is writing in Chinese 鈥?respond in Chinese (绠€浣撲腑鏂?.\n" : "";
1126
 
1127
+ // 宸ュ叿閫氳繃 toolsPrompt 鍗曠嫭娉ㄥ叆 (瑙佷笅鏂?question 鏋勫缓)
1128
  const toolsPrompt = (displayTools && displayTools.length > 0) ? buildToolsPrompt(
1129
  displayTools.map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema || {} } }))
1130
  ) : "";
1131
 
1132
  let question;
1133
+ let toolResultPath = false; // 宸ュ叿缁撴灉鍥炰紶杞爣璁?(璺宠繃 langHint 鍜屾埅鏂?
1134
  const MAX_QUESTION = 10000;
1135
 
1136
  if (hasToolResults && hasToolUses) {
1137
+ toolResultPath = true; // 鏍囪: 鍚庣画璺宠繃 langHint 娉ㄥ叆鍜岄€氱敤鎴柇
1138
+ // 猸?宸ュ叿缁撴灉鍥炰紶杞?鈥?鍗曟绯荤粺閫氱煡鏍煎紡锛岃妯″瀷鎶婄粨鏋滃綋浣滅郴缁熸彁渚涚殑淇℃伅
1139
  const isCJK = hasCJK;
1140
 
1141
+ // 鏀堕泦宸ュ叿璋冪敤鍚?缁撴灉
1142
  const calledFuncs = [];
1143
  const results = [];
1144
  for (const m of messages) {
 
1158
  }
1159
  }
1160
 
1161
+ // 鎵惧埌鐢ㄦ埛鐨勫師濮嬫彁闂?(绗竴鏉¢潪 tool_result 鐨勭敤鎴锋秷鎭?
1162
  const userQs = [];
1163
  for (const m of messages) {
1164
  if (m.role !== "user") continue;
 
1168
  }
1169
  const originalQuestion = userQs.length > 0 ? userQs[userQs.length - 1] : (lastUserMsg || "");
1170
 
1171
+ // 鏋勫缓閫氱煡: 绯荤粺娑堟伅 + 鍑芥暟璋冪敤 + 缁撴灉
1172
  if (isCJK) {
1173
+ question = "鈿狅笍 绯荤粺閫氱煡锛氫綘鍒氭墠璋冪敤浜嗕互涓嬪嚱鏁帮紝杩斿洖缁撴灉濡備笅锛歕n\n";
1174
  for (let i = 0; i < calledFuncs.length; i++) {
1175
+ question += "鍑芥暟璋冪敤: " + calledFuncs[i] + "\n";
1176
+ question += "杩斿洖缁撴灉:\n\"\"\"\n" + (results[i] || "") + "\n\"\"\"\n\n";
1177
  }
1178
+ question += "鐢ㄦ埛鍘熷鎻愰棶: \"" + originalQuestion + "\"\n\n";
1179
+ question += "璇风敤绠€浣撲腑鏂囩洿鎺ュ洖绛旂敤鎴风殑闂銆傝鍑虹瓟妗堝嵆鍙€備笉瑕佽\"浣犲垎浜簡\"鎴朶"鐪嬭捣鏉ュ儚鏄痋"鎴朶"I see you've shared\"銆備笂闈㈢敤 \"\"\" 鍖呰9鐨勫唴瀹规槸浣犺嚜宸辫皟鐢ㄥ嚱鏁板緱鍒扮殑杩斿洖缁撴灉锛屼笉鏄敤鎴峰彂缁欎綘鐨勩€?;
1180
  } else {
1181
+ question = "鈿狅笍 SYSTEM: You just called these functions and received these outputs:\n\n";
1182
  for (let i = 0; i < calledFuncs.length; i++) {
1183
  question += "Function: " + calledFuncs[i] + "\n";
1184
  question += "Output:\n\"\"\"\n" + (results[i] || "") + "\n\"\"\"\n\n";
 
1187
  question += "Answer the user's question DIRECTLY based on the outputs above. Do NOT say \"you shared\" or \"I see you've shared\" or \"it looks like\". The content inside \"\"\" blocks is YOUR function output, NOT content the user sent you.";
1188
  }
1189
 
1190
+ // 猸?宸ュ叿缁撴灉鍥炰紶杞? 涓嶆惡甯﹀畬鏁?sysPrompt 鍜屽ぇ閲?tools (鈿狅笍 閫氱煡宸叉彁渚涜冻澶熶笂涓嬫枃)
1191
+ // 浠呬繚鐣?5 涓牳蹇冨伐鍏? 纭繚鎬婚暱搴?< 10000 涓嶄細琚悗闈㈢殑閫氱敤鎴柇閫昏緫鐮村潖
1192
+ // 猸?鐢ㄨ蒋鐗堟彁绀?鈥?鍏佽妯″瀷鍩轰簬缁撴灉鐩存帴鐢ㄦ枃瀛楀洖绛? 鑰岄潪琚己鍒跺啀鍙?function_call
1193
  const minimalToolsPrompt = buildToolsPromptSoft(
1194
  displayTools.filter(t => ESSENTIAL_TOOLS.has(t.name)).slice(0, 5)
1195
  .map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema || {} } }))
1196
  );
1197
  question = minimalToolsPrompt + "\n\n---\n" + question;
1198
+ // 璺宠繃鍚庣画鐨?langHint 娉ㄥ叆鍜岄€氱敤鎴柇 鈥?姝よ矾寰勭殑 question 宸插寘鍚瑷€鎸囦护涓?< 10000
1199
  } else if (nonSys.length > 1 && realUserMsgs.length >= 1) {
1200
+ // 鍙栫湡姝g殑鐢ㄦ埛娑堟伅锛堣繃婊ゆ帀绯荤粺娉ㄥ叆鐨勫厓鏁版嵁锛? const realMsgs = nonSys
 
1201
  .filter(m => {
1202
  const c = stripMetadata(extractContent(m));
1203
  return c.length > 5;
1204
  });
1205
  if (realMsgs.length > 1) {
1206
+ // 猸?淇: 浠?6 鏉℃墿灞曞埌 20 鏉★紝骞舵寜鎬诲瓧绗﹂檺鍒跺姩鎬佽鍓紙淇濈暀鏈€鏂扮殑娑堟伅锛? const MAX_HISTORY_CHARS = 6000; // 鐣欏嚭绌洪棿缁?sysPrompt + toolsPrompt
 
1207
  const recentMsgs = realMsgs.slice(-20);
1208
  let historyParts = [];
1209
  let historyLen = 0;
 
1217
  question = historyParts.join("\n");
1218
  question = (sysPrompt ? sysPrompt + "\n\n" : "") + (toolsPrompt ? toolsPrompt + "\n\n---\n" : "") + question;
1219
  } else {
1220
+ // 鍙湁涓€鏉$湡姝f秷鎭紝璧板崟杞矾寰? question = (sysPrompt ? "(Background)\n" + sysPrompt + "\n\n" : "") + toolsPrompt + "\n\n---\nUser message (respond to this):\n" + lastUserMsg;
 
1221
  }
1222
  } else {
1223
  let context = sysPrompt;
1224
  const overhead = toolsPrompt.length + lastUserMsg.length + 200;
1225
  const remaining = MAX_QUESTION - overhead;
1226
  if (context && context.length > remaining && remaining > 0) {
1227
+ // 猸?淇: 淇濈暀澶撮儴锛堟牳蹇冭鍒欙級+ 灏鹃儴锛堟渶鏂版枃浠跺垪琛級锛屼腑闂磋鍓? // 鑰岄潪鏃ч€昏緫鐨?鍙繚鐣欏墠500瀛?鐩綍"锛岄偅鏍蜂細涓㈠け澶ч噺閲嶈鎸囦护
 
1228
  const layoutMatch = context.match(/<project_layout>([\s\S]*?)<\/project_layout>/);
1229
  const layoutSummary = layoutMatch ? layoutMatch[1].trim().split("\n").slice(0, 30).join("\n") : "";
1230
  const headLen = Math.min(800, Math.floor(remaining * 0.6));
 
1238
  question = (context ? "(Background)\n" + context + "\n\n" : "") + toolsPrompt + "\n\n---\nUser message (respond to this):\n" + lastUserMsg;
1239
  }
1240
 
1241
+ // 猸?娉ㄥ叆璇█鎻愮ず & 鎴柇 (宸ュ叿缁撴灉鍥炰紶杞烦杩?鈥?宸茶嚜甯﹁瑷€鎸囦护涓?< 10000)
1242
  if (!toolResultPath) {
1243
  question += langHint;
1244
 
 
1255
  }
1256
 
1257
 
1258
+ // 浼氳瘽 鈥?猸?鍏抽敭: 蹇呴』澶嶇敤 IMA session 鎵嶈兘淇濇寔瀵硅瘽璁板繂
1259
  const convId = req.headers["x-conversation-id"] || req.headers["x-session-id"] || "";
1260
+ // 猸?淇: 鐢ㄧ郴缁?prompt + 绗竴鏉$敤鎴锋秷鎭殑 hash 浣滀负绋冲畾浼氳瘽閿氱偣
1261
+ // Claude Code 涓嶅彂 x-conversation-id, 浣嗗悓涓€瀵硅瘽鐨?system prompt 鍐呭鍥哄畾
1262
  let effectiveConvId = convId;
1263
  if (!effectiveConvId && messages.length > 0) {
1264
+ // 浼樺厛鐢?system prompt (Claude Code 鍦?system 閲屾斁椤圭洰璺緞绛夊浐瀹氫俊鎭?
1265
  const sysMsg = messages.find(m => m.role === "system");
1266
  const anchor = sysMsg
1267
+ ? extractContent(sysMsg).slice(0, 200) // system prompt 鍓?00瀛楋紝閫氬父鍚」鐩矾寰? : extractContent(messages[0]).slice(0, 200); // fallback: 绗竴鏉℃秷鎭? effectiveConvId = "conv-" + crypto.createHash("md5").update(anchor).digest("hex").slice(0, 12);
 
 
1268
  }
1269
  let sessionId;
1270
  const isNewSession = !getCachedSession(effectiveConvId);
 
1274
  return json(res, 502, { type: "error", error: { type: "api_error", message: "Session init failed: " + e.message } });
1275
  }
1276
 
1277
+ // --- 闈炴祦寮?---
1278
  if (!stream) {
1279
  try {
1280
  const events = await imaQaStreamWithRetry(effectiveConvId, question, model.type, model.id);
 
1302
  return json(res, 200, {
1303
  id: "msg_" + crypto.randomUUID().slice(0, 8),
1304
  type: "message", role: "assistant", model: modelKey,
1305
+ // 猸?鍏滃簳: 绌烘枃鏈椂缁欏嚭鍗犱綅, 閬垮厤瀹㈡埛绔敹鍒扮┖ content
1306
+ content: [{ type: "text", text: text || "(鏈疆娌℃湁鐢熸垚鍐呭锛岃閲嶈瘯鎴栨崲涓€绉嶉棶娉曘€?" }],
1307
  stop_reason: "end_turn", stop_sequence: null,
1308
  usage: { input_tokens: 0, output_tokens: 0 },
1309
  });
 
1313
  }
1314
  }
1315
 
1316
+ // --- 娴佸紡 ---
1317
  cors(res);
1318
  res.writeHead(200, {
1319
  "Content-Type": "text/event-stream",
 
1362
 
1363
  let stopReason = "end_turn";
1364
  let done = false;
1365
+ let sentText = false; // 猸?鏄惁宸插彂閫佽繃浠讳綍鏂囨湰 delta (鍏滃簳鍒ゆ柇)
1366
  const filter = new StreamFilter();
1367
+ let calls = []; // 鍦?try 澶栧0鏄庯紝渚涘悗缁?tool_use 鍙戦€佷娇鐢? try {
 
1368
  const events = await imaQaStreamWithRetry(effectiveConvId, question, model.type, model.id);
1369
 
1370
  for await (const evt of events) {
 
1374
  break;
1375
  }
1376
  const et = eventTexts(evt);
1377
+ // Diagnostic: log what each event produces
1378
+ if (et.think || et.text) {
1379
+ process.stderr.write("[SSE-EVT] event=" + (evt.event || "(none)") + " think=" + (et.think ? et.think.length + "c" : "none") + " text=" + (et.text ? et.text.length + "c" : "none") + "\n");
1380
+ } else if (evt.data) {
1381
+ process.stderr.write("[SSE-EVT] event=" + (evt.event || "(none)") + " UNMATCHED: " + evt.data.slice(0, 300) + "\n");
1382
+ }
1383
  // Emit thinking content as separate content block
1384
  if (et.think) {
1385
  if (DEBUG_SSE) console.log("[SSE-THINK] " + JSON.stringify(et.think).slice(0, 200));
 
1393
  }
1394
  if (et.text) {
1395
  ensureBlock("text");
1396
+ // Bypass filter for text that clearly has no function_call
1397
+ let clean;
1398
+ if (et.text.indexOf('<') === -1) {
1399
+ clean = et.text;
1400
+ } else {
1401
+ clean = filter.feed(et.text);
1402
+ }
1403
+ if (clean) {
1404
+ em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: clean } });
1405
+ sentText = true;
1406
+ } else {
1407
+ process.stderr.write("[SSE-FILTER] feed returned empty, buf=" + filter._buf.length + "c inFunc=" + filter._inFunc + " et.text=" + et.text.slice(0, 80) + "\n");
1408
+ }
1409
  }
1410
  }
1411
 
1412
+ // 鍐插埛娈嬬暀缂撳啿
 
1413
  const flushed = filter.flush();
1414
  if (flushed && !resClosed) { em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: flushed } }); sentText = true; }
1415
 
1416
  calls = filter.parseCalls();
1417
+ // 猸?鏃犳硶瑙f瀽鐨?function_call 娈嬬暀 鈥?浣滀负鏂囨湰杈撳嚭, 閬垮厤闈欓粯涓㈠け
1418
  if (filter.leftover && !resClosed) { em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: filter.leftover } }); sentText = true; }
1419
 
1420
+ // 猸?鍏滃簳: 娴佺粨鏉熶絾鏃㈡棤鏂囨湰涔熸棤宸ュ叿璋冪敤 鈥?缁濅笉璁╁鎴风鏀跺埌瀹屽叏绌虹殑鍥炲銆? // 杩欐槸 Claude Code "鎵ц鍛戒护鍚庢棤浠讳綍鍥炲灏辩粨鏉? 鐨勬渶缁堥槻绾裤€? if (!sentText && calls.length === 0 && !resClosed) {
 
 
1421
  const fb = done
1422
+ ? "(鏈疆娌℃湁鐢熸垚鍐呭锛岃閲嶈瘯鎴栨崲涓€绉嶉棶娉曘€?"
1423
+ : "(鍝嶅簲鍦ㄥ畬鎴愬墠琚腑鏂紝璇烽噸璇曘€?";
1424
  ensureBlock("text");
1425
  em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: fb } });
1426
  sentText = true;
1427
  }
1428
  } catch (e) {
1429
  console.error(`[ANTHROPIC-STREAM-ERR] ${e.message}`);
1430
+ // 猸?寮傚父璺緞鍚屾牱鍏滃簳, 淇濊瘉鑷冲皯鏈変竴娈垫枃鏈? if (!sentText && !resClosed) {
1431
+ try { em("content_block_delta", { type: "content_block_delta", index: blockIdx, delta: { type: "text_delta", text: "(鍝嶅簲澶勭悊鍑洪敊锛岃閲嶈瘯銆?" } }); sentText = true; } catch {}
 
1432
  }
1433
  }
1434
 
1435
+ // 缁撴潫鏂囨湰鍧?(浠呭湪鏈埅鍋滄椂)
1436
  if (!resClosed) closeBlock();
1437
 
1438
+ // 猸?鍙戦€佸嚱鏁拌皟鐢?鈥?浣跨敤涓婇潰宸茶В鏋愮殑 calls (閬垮厤閲嶅瑙f瀽)
1439
  if (!resClosed && calls.length > 0) {
1440
  stopReason = "tool_use";
1441
  for (let i = 0; i < calls.length; i++) {
 
1456
  }
1457
 
1458
  // ============================================================
1459
+ // 10. 璺敱
1460
  // ============================================================
1461
  async function router(req, res) {
1462
  cors(res);
 
1497
  });
1498
  body = raw ? JSON.parse(raw) : {};
1499
 
1500
+ // body 宸茶В鏋? } catch (e) {
 
1501
  return json(res, 400, { error: { type: "invalid_request_error", message: "Invalid JSON body" } });
1502
  }
1503
  }
 
1552
  }
1553
 
1554
  // ============================================================
1555
+ // 11. 鍚姩
1556
  // ============================================================
1557
  const PORT = process.env.PORT || CONFIG.server?.port || 8080;
1558
  const HOST = CONFIG.server?.host || "0.0.0.0";
1559
 
1560
  http.createServer(router).listen(PORT, HOST, () => {
1561
  console.log(`\n${'='.repeat(50)}`);
1562
+ console.log(`[STARTUP] 鏈嶅姟鍚姩浜?http://${HOST}:${PORT}`);
1563
  console.log(`[STARTUP] API keys count: ${API_KEYS.size} (source: ${(process.env.IMA_API_KEYS || "") ? "env IMA_API_KEYS" : "config.json"})`);
1564
  for (const k of API_KEYS) {
1565
  console.log(`[STARTUP] key: ${maskKey(k)} (len=${k.length})`);
1566
  }
1567
  setInterval(refreshCookie, 60 * 1000);
1568
  });
1569
+