fix: text extraction bypass, stderr diagnostics, OpenAI ensureBlock removal
Browse files
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", //
|
| 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 |
-
//
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 || "
|
| 235 |
-
msgs_limit: 20, // IMA
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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
|
| 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
|
| 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
|
| 372 |
// ============================================================
|
| 373 |
|
| 374 |
function escapeRawCtrlInStrings(s) {
|
|
@@ -388,17 +388,17 @@ function escapeRawCtrlInStrings(s) {
|
|
| 388 |
return out;
|
| 389 |
}
|
| 390 |
|
| 391 |
-
//
|
| 392 |
function tryRepairJson(raw) {
|
| 393 |
try { return JSON.parse(raw); } catch (e) { /* continue */ }
|
| 394 |
let fixed = raw;
|
| 395 |
-
// 0.
|
| 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.
|
| 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 |
-
//
|
| 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; //
|
| 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 `##
|
| 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
|
| 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
|
| 487 |
}
|
| 488 |
|
| 489 |
-
//
|
| 490 |
-
//
|
| 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
|
| 514 |
|
| 515 |
The user's request may need MULTIPLE steps. After each function result, decide:
|
| 516 |
-
the task is NOT finished yet
|
| 517 |
-
finished
|
| 518 |
|
| 519 |
NEVER reply with only your intention (e.g. "Let me read the file", "I'll now
|
| 520 |
-
start the server", "
|
| 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 |
-
//
|
| 534 |
-
// 防止 Claude Code 客户端因看到 XML 标记而截停
|
| 535 |
-
class StreamFilter {
|
| 536 |
constructor() {
|
| 537 |
-
this._buf = ''; //
|
| 538 |
-
this.
|
| 539 |
-
this.
|
| 540 |
-
this._inFunc = false; // 是否在 <function_call> 内部
|
| 541 |
this._tagLen = 16; // '<function_call>'.length
|
| 542 |
-
this._leftover = ''; //
|
| 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 |
-
//
|
| 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 |
-
//
|
| 588 |
}
|
| 589 |
}
|
| 590 |
|
|
@@ -593,7 +581,7 @@ class StreamFilter {
|
|
| 593 |
return text;
|
| 594 |
}
|
| 595 |
|
| 596 |
-
//
|
| 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 |
-
//
|
| 605 |
flush() {
|
| 606 |
if (this._inFunc) {
|
| 607 |
-
//
|
| 608 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
// ---
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 = /[
|
| 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 = "
|
| 777 |
for (let i = 0; i < calledOAI.length; i++) {
|
| 778 |
-
question += "
|
| 779 |
}
|
| 780 |
-
question += "
|
| 781 |
} else {
|
| 782 |
-
question = "
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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
|
| 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
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 896 |
-
//
|
| 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 |
-
//
|
| 1019 |
if (filter.leftover) emit(filter.leftover, null, null);
|
| 1020 |
if (calls.length > 0) {
|
| 1021 |
-
//
|
| 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
|
| 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
|
| 1085 |
-
//
|
| 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 |
-
//
|
| 1098 |
const isTitleGen = sysPrompt.includes('Generate a concise, sentence-case title');
|
| 1099 |
|
| 1100 |
-
//
|
| 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 |
-
|
| 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 |
-
//
|
| 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 = /[
|
| 1157 |
-
const langHint = hasCJK ? "\n## Language\nRespond in the same language as the user's message. The user is writing in Chinese
|
| 1158 |
|
| 1159 |
-
//
|
| 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; //
|
| 1166 |
const MAX_QUESTION = 10000;
|
| 1167 |
|
| 1168 |
if (hasToolResults && hasToolUses) {
|
| 1169 |
-
toolResultPath = true; //
|
| 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 |
-
//
|
| 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 = "
|
| 1206 |
for (let i = 0; i < calledFuncs.length; i++) {
|
| 1207 |
-
question += "
|
| 1208 |
-
question += "
|
| 1209 |
}
|
| 1210 |
-
question += "
|
| 1211 |
-
question += "
|
| 1212 |
} else {
|
| 1213 |
-
question = "
|
| 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 |
-
//
|
| 1223 |
-
//
|
| 1224 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 1278 |
if (!toolResultPath) {
|
| 1279 |
question += langHint;
|
| 1280 |
|
|
@@ -1291,18 +1255,16 @@ async function anthropicMessages(req, res, body) {
|
|
| 1291 |
}
|
| 1292 |
|
| 1293 |
|
| 1294 |
-
//
|
| 1295 |
const convId = req.headers["x-conversation-id"] || req.headers["x-session-id"] || "";
|
| 1296 |
-
//
|
| 1297 |
-
// Claude Code
|
| 1298 |
let effectiveConvId = convId;
|
| 1299 |
if (!effectiveConvId && messages.length > 0) {
|
| 1300 |
-
//
|
| 1301 |
const sysMsg = messages.find(m => m.role === "system");
|
| 1302 |
const anchor = sysMsg
|
| 1303 |
-
? extractContent(sysMsg).slice(0, 200) // system prompt
|
| 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 |
-
//
|
| 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; //
|
| 1404 |
const filter = new StreamFilter();
|
| 1405 |
-
let calls = []; //
|
| 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 |
-
|
| 1430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 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 |
-
|
| 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 |
-
//
|
| 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]
|
| 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 |
+
|