mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-09-01 14:52:19 +08:00
Scope ToolHub resolve cache by task
This commit is contained in:
+1
-1
@@ -31,7 +31,7 @@ function runSuite(suite) {
|
||||
console.log(`\nRunning ${suite} tests...`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(electron, ["--test", `dist/tests/${suite}/*.js`], {
|
||||
const child = spawn(electron, ["--test", `dist/tests/${suite}/*.test.js`], {
|
||||
cwd: projectRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
+13
-1
@@ -27,7 +27,10 @@ if (unknownSuites.length > 0) {
|
||||
rmSync(testsOutDir, { force: true, recursive: true });
|
||||
|
||||
for (const suite of selectedSuites) {
|
||||
const entryPoints = findTestFiles(suite.testDir);
|
||||
const entryPoints = [
|
||||
...findTestFiles(suite.testDir),
|
||||
...runtimeEntryPointsForSuite(suite.name)
|
||||
];
|
||||
if (entryPoints.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -60,6 +63,15 @@ for (const suite of selectedSuites) {
|
||||
});
|
||||
}
|
||||
|
||||
function runtimeEntryPointsForSuite(suiteName) {
|
||||
if (suiteName !== "main") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts")
|
||||
];
|
||||
}
|
||||
|
||||
function findTestFiles(dir) {
|
||||
if (!existsSync(dir)) {
|
||||
return [];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const CLAUDE_CODE_ENABLE_TOOL_SEARCH_ENV = "ENABLE_TOOL_SEARCH";
|
||||
export const CLAUDE_CODE_MCP_CONFIG_ENV = "CCR_CLAUDE_CODE_MCP_CONFIG";
|
||||
export const CODEXL_CLAUDE_CODE_MCP_CONFIG_ENV = "CODEXL_CLAUDE_CODE_MCP_CONFIG";
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ class ToolHubRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
const inFlightResolve = session.inFlightResolves.get(taskHash) ?? firstMapValue(session.inFlightResolves);
|
||||
const inFlightResolve = session.inFlightResolves.get(taskHash);
|
||||
if (inFlightResolve) {
|
||||
const output = await inFlightResolve;
|
||||
const selectedTools = output.selectedTools
|
||||
@@ -707,8 +707,7 @@ class ToolHubRuntime {
|
||||
const session = this.session(scopeKey);
|
||||
const now = Date.now();
|
||||
session.recentlyResolvedTasks = session.recentlyResolvedTasks.filter((item) => now - item.resolvedAt <= repeatedResolveWindowMs);
|
||||
return session.recentlyResolvedTasks.find((item) => item.taskHash === taskHash) ??
|
||||
session.recentlyResolvedTasks.find((item) => item.observationCount === session.recentObservations.length);
|
||||
return session.recentlyResolvedTasks.find((item) => item.taskHash === taskHash);
|
||||
}
|
||||
|
||||
private rememberObservation(scopeKey: string, toolName: string, result: unknown): void {
|
||||
@@ -1516,21 +1515,15 @@ class OpenAiToolHubSearchAgent {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!didCallAnalyzer) {
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: `You must call ${treeSitterToolName} on a TypeScript workflow sketch before your final answer.`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentText = typeof responseMessage.content === "string" ? responseMessage.content.trim() : "";
|
||||
const parsed = firstJsonObject(contentText);
|
||||
if (!parsed) {
|
||||
messages.push(responseMessage);
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: "Return only a valid JSON object with keys \"summary\", \"steps\", \"workflowSketch\", and \"toolNames\"."
|
||||
content: didCallAnalyzer
|
||||
? "Return only a valid JSON object with keys \"summary\", \"steps\", \"workflowSketch\", and \"toolNames\"."
|
||||
: `You must call ${treeSitterToolName} on a TypeScript workflow sketch before your final answer.`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -1555,11 +1548,15 @@ class OpenAiToolHubSearchAgent {
|
||||
.filter((name): name is string => typeof name === "string")
|
||||
);
|
||||
const selectedToolNames = uniqueStrings([...latestResolvedFromAnalyzer, ...llmSelectedNames]).slice(0, topK);
|
||||
const refinementFeedback = buildSearchRefinementFeedback({
|
||||
selectedToolNames,
|
||||
summary,
|
||||
workflowSketch
|
||||
});
|
||||
const refinementFeedback = didCallAnalyzer
|
||||
? buildSearchRefinementFeedback({
|
||||
selectedToolNames,
|
||||
summary,
|
||||
workflowSketch
|
||||
})
|
||||
: selectedToolNames.length === 0
|
||||
? "Your current answer resolved to zero valid catalog tools. Call the tree-sitter tool on a revised TypeScript workflow sketch before answering."
|
||||
: undefined;
|
||||
if (refinementFeedback && turn + 1 < maxTurns) {
|
||||
messages.push(responseMessage);
|
||||
messages.push({ role: "user", content: refinementFeedback });
|
||||
@@ -1568,15 +1565,22 @@ class OpenAiToolHubSearchAgent {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!didCallAnalyzer || analyzerCallCount === 0) {
|
||||
throw new Error("Resolve retrieval LLM did not complete an AST planning round.");
|
||||
}
|
||||
const selectedToolNames = uniqueStrings([...latestResolvedFromAnalyzer, ...llmSelectedNames]).slice(0, topK);
|
||||
if (selectedToolNames.length === 0) {
|
||||
throw new Error("Resolve retrieval did not converge on any valid catalog tools after AST refinement.");
|
||||
throw new Error(didCallAnalyzer || analyzerCallCount > 0
|
||||
? "Resolve retrieval did not converge on any valid catalog tools after AST refinement."
|
||||
: "Resolve retrieval did not converge on any valid catalog tools.");
|
||||
}
|
||||
if (!didCallAnalyzer || analyzerCallCount === 0) {
|
||||
referencedTokens = uniqueStrings([...referencedTokens, ...selectedToolNames]);
|
||||
}
|
||||
if (didCallAnalyzer && analyzerCallCount === 0) {
|
||||
throw new Error("Resolve retrieval LLM did not complete an AST planning round.");
|
||||
}
|
||||
if (!summary) {
|
||||
summary = "Resolved a planned end-to-end tool bundle with AST-assisted retrieval.";
|
||||
summary = didCallAnalyzer
|
||||
? "Resolved a planned end-to-end tool bundle with AST-assisted retrieval."
|
||||
: "Resolved a planned end-to-end tool bundle from the resolver model response.";
|
||||
} else if (summary.toLowerCase().includes("no strong tool bundle match was found")) {
|
||||
summary = "Resolved a candidate tool bundle after iterative AST refinement.";
|
||||
}
|
||||
@@ -1596,14 +1600,14 @@ class OpenAiToolHubSearchAgent {
|
||||
messages: SearchMessage[],
|
||||
timeoutMs: number
|
||||
): Promise<SearchMessage> {
|
||||
const stream = await client.chat.completions.create({
|
||||
const response = await client.chat.completions.create({
|
||||
model,
|
||||
temperature: 0,
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
...messages
|
||||
] as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||
stream: true,
|
||||
stream: false,
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
@@ -1625,70 +1629,36 @@ class OpenAiToolHubSearchAgent {
|
||||
}
|
||||
],
|
||||
tool_choice: "auto"
|
||||
} as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, {
|
||||
} as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, {
|
||||
timeout: timeoutMs
|
||||
});
|
||||
|
||||
let content = "";
|
||||
let sawDelta = false;
|
||||
const toolCalls = new Map<number, {
|
||||
function: {
|
||||
arguments: string;
|
||||
name: string;
|
||||
};
|
||||
id: string;
|
||||
type: "function";
|
||||
}>();
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
if (!delta) {
|
||||
continue;
|
||||
}
|
||||
sawDelta = true;
|
||||
if (typeof delta.content === "string") {
|
||||
content += delta.content;
|
||||
}
|
||||
for (const toolCallDelta of delta.tool_calls ?? []) {
|
||||
const index = typeof toolCallDelta.index === "number" ? toolCallDelta.index : toolCalls.size;
|
||||
const toolCall = toolCalls.get(index) ?? {
|
||||
id: "",
|
||||
type: "function",
|
||||
const message = response.choices[0]?.message;
|
||||
if (!message) {
|
||||
throw new Error("OpenAI resolve retrieval returned no assistant message.");
|
||||
}
|
||||
const content = typeof message.content === "string" ? message.content : "";
|
||||
const toolCalls = (message.tool_calls ?? [])
|
||||
.map((toolCall, index) => {
|
||||
const rawToolCall = toolCall as unknown as { function?: unknown };
|
||||
const functionCall = isRecord(rawToolCall.function) ? rawToolCall.function : {};
|
||||
return {
|
||||
id: toolCall.id || `tool_call_${index}`,
|
||||
type: "function" as const,
|
||||
function: {
|
||||
arguments: "",
|
||||
name: ""
|
||||
arguments: typeof functionCall.arguments === "string" ? functionCall.arguments : "",
|
||||
name: typeof functionCall.name === "string" ? functionCall.name : ""
|
||||
}
|
||||
};
|
||||
if (toolCallDelta.id) {
|
||||
toolCall.id = toolCallDelta.id;
|
||||
}
|
||||
if (toolCallDelta.function?.name) {
|
||||
toolCall.function.name += toolCallDelta.function.name;
|
||||
}
|
||||
if (typeof toolCallDelta.function?.arguments === "string") {
|
||||
toolCall.function.arguments += toolCallDelta.function.arguments;
|
||||
}
|
||||
toolCalls.set(index, toolCall);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sawDelta) {
|
||||
throw new Error("OpenAI resolve retrieval stream returned no assistant delta.");
|
||||
})
|
||||
.filter((toolCall) => toolCall.function.name.length > 0);
|
||||
if (!content && toolCalls.length === 0) {
|
||||
throw new Error("OpenAI resolve retrieval returned no assistant content or tool calls.");
|
||||
}
|
||||
return {
|
||||
role: "assistant",
|
||||
content,
|
||||
tool_calls: Array.from(toolCalls.entries())
|
||||
.sort(([left], [right]) => left - right)
|
||||
.map(([index, toolCall]) => ({
|
||||
id: toolCall.id || `tool_call_${index}`,
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: toolCall.function.name,
|
||||
arguments: toolCall.function.arguments
|
||||
}
|
||||
}))
|
||||
.filter((toolCall) => toolCall.function.name.length > 0)
|
||||
tool_calls: toolCalls
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2673,13 +2643,6 @@ function normalizeResolveTaskKey(value: string): string {
|
||||
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function firstMapValue<K, V>(map: Map<K, V>): V | undefined {
|
||||
for (const value of map.values()) {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readNonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, NO_AVAILABLE_GATEWAY_MO
|
||||
import { replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
|
||||
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
|
||||
import {
|
||||
CLAUDE_CODE_ENABLE_TOOL_SEARCH_ENV,
|
||||
CLAUDE_CODE_MCP_CONFIG_ENV,
|
||||
CODEXL_CLAUDE_CODE_MCP_CONFIG_ENV,
|
||||
claudeCodeMcpConfigEnv,
|
||||
@@ -192,7 +191,7 @@ function cleanupClaudeCodeToolHubSettingsFile(file: string, options: { backup: b
|
||||
|
||||
function deleteClaudeCodeToolHubEnv(env: Record<string, unknown>): boolean {
|
||||
let changed = false;
|
||||
for (const key of [CLAUDE_CODE_ENABLE_TOOL_SEARCH_ENV, CLAUDE_CODE_MCP_CONFIG_ENV, CODEXL_CLAUDE_CODE_MCP_CONFIG_ENV]) {
|
||||
for (const key of [CLAUDE_CODE_MCP_CONFIG_ENV, CODEXL_CLAUDE_CODE_MCP_CONFIG_ENV]) {
|
||||
if (key in env) {
|
||||
delete env[key];
|
||||
changed = true;
|
||||
@@ -239,7 +238,6 @@ function applyClaudeCodeProfile(config: AppConfig, profile: ProfileConfig, token
|
||||
const endpoint = gatewayEndpoint(config);
|
||||
const settings = readJsonObject(settingsFile);
|
||||
const settingsEnv = withoutBotGatewayEnv(Object.fromEntries(stringRecord(settings.env)));
|
||||
delete settingsEnv[CLAUDE_CODE_ENABLE_TOOL_SEARCH_ENV];
|
||||
delete settingsEnv[CLAUDE_CODE_MCP_CONFIG_ENV];
|
||||
delete settingsEnv[CODEXL_CLAUDE_CODE_MCP_CONFIG_ENV];
|
||||
const env = {
|
||||
|
||||
@@ -275,10 +275,12 @@ test("profile service clears stale Claude Code ToolHub artifacts when no gateway
|
||||
assert.equal(existsSync(staleToolHubMcpConfigFile), false);
|
||||
const settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
||||
assert.deepEqual(settings.env, {
|
||||
ENABLE_TOOL_SEARCH: "true",
|
||||
USER_VALUE: "kept"
|
||||
});
|
||||
const staleSettings = JSON.parse(readFileSync(staleSettingsFile, "utf8"));
|
||||
assert.deepEqual(staleSettings.env, {
|
||||
ENABLE_TOOL_SEARCH: "true",
|
||||
USER_VALUE: "old-kept"
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ test("ToolHub MCP runtime source keeps a shebang for direct MCP Inspector execut
|
||||
});
|
||||
|
||||
test("built ToolHub MCP runtime accepts newline JSON stdio used by MCP Inspector", async (t) => {
|
||||
const runtime = path.join(process.cwd(), "packages", "electron", "dist", "main", "toolhub-mcp.js");
|
||||
const runtime = toolHubRuntimePath();
|
||||
if (!existsSync(runtime)) {
|
||||
t.skip("ToolHub MCP runtime has not been built.");
|
||||
return;
|
||||
@@ -83,7 +83,7 @@ test("built ToolHub MCP runtime accepts newline JSON stdio used by MCP Inspector
|
||||
});
|
||||
|
||||
test("built ToolHub MCP runtime waits for local CCR resolver readiness", async (t) => {
|
||||
const runtime = path.join(process.cwd(), "packages", "electron", "dist", "main", "toolhub-mcp.js");
|
||||
const runtime = toolHubRuntimePath();
|
||||
if (!existsSync(runtime)) {
|
||||
t.skip("ToolHub MCP runtime has not been built.");
|
||||
return;
|
||||
@@ -180,6 +180,133 @@ test("built ToolHub MCP runtime waits for local CCR resolver readiness", async (
|
||||
assert.match(response.result.content[0].text, /mcp\.mcd_mcp\.campaign-calendar/);
|
||||
});
|
||||
|
||||
test("built ToolHub MCP runtime keeps resolve cache scoped by task", async (t) => {
|
||||
const runtime = toolHubRuntimePath();
|
||||
if (!existsSync(runtime)) {
|
||||
t.skip("ToolHub MCP runtime has not been built.");
|
||||
return;
|
||||
}
|
||||
|
||||
const backend = createMcpHttpServer({
|
||||
serverName: "multi-mcp",
|
||||
tools: [
|
||||
{
|
||||
description: "查询指定城市的天气预报。",
|
||||
inputSchema: { type: "object" },
|
||||
name: "weather-forecast"
|
||||
},
|
||||
{
|
||||
description: "查询麦当劳中国当月的营销活动日历。",
|
||||
inputSchema: { type: "object" },
|
||||
name: "campaign-calendar"
|
||||
}
|
||||
]
|
||||
});
|
||||
try {
|
||||
await listen(backend);
|
||||
} catch (error) {
|
||||
backend.close();
|
||||
t.skip(`Local HTTP listen is unavailable: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
const backendPort = backend.address().port;
|
||||
t.after(() => backend.close());
|
||||
|
||||
const resolver = createTaskAwareResolverServer();
|
||||
try {
|
||||
await listen(resolver);
|
||||
} catch (error) {
|
||||
resolver.close();
|
||||
t.skip(`Local HTTP listen is unavailable: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
const resolverPort = resolver.address().port;
|
||||
t.after(() => resolver.close());
|
||||
|
||||
const child = spawn(process.execPath, [runtime], {
|
||||
env: {
|
||||
...process.env,
|
||||
TOOLHUB_MCP_SERVERS_JSON: JSON.stringify([
|
||||
{
|
||||
name: "multi-mcp",
|
||||
transport: "streamable-http",
|
||||
url: `http://127.0.0.1:${backendPort}/mcp`
|
||||
}
|
||||
]),
|
||||
TOOLHUB_OPENAI_API_KEY: "test-key",
|
||||
TOOLHUB_OPENAI_BASE_URL: `http://127.0.0.1:${resolverPort}/v1`,
|
||||
TOOLHUB_OPENAI_MODEL: "resolver-model",
|
||||
TOOLHUB_REQUEST_TIMEOUT_MS: "10000"
|
||||
},
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
});
|
||||
t.after(() => child.kill());
|
||||
|
||||
const stderr = [];
|
||||
child.stderr.on("data", (chunk) => stderr.push(chunk.toString("utf8")));
|
||||
const reader = jsonLineReader(child);
|
||||
writeJsonLine(child, {
|
||||
id: 1,
|
||||
jsonrpc: "2.0",
|
||||
method: "initialize",
|
||||
params: {
|
||||
capabilities: {},
|
||||
clientInfo: { name: "resolve-cache-test", version: "1.0.0" },
|
||||
protocolVersion: "2024-11-05"
|
||||
}
|
||||
});
|
||||
await reader.nextMessage(1);
|
||||
writeJsonLine(child, {
|
||||
jsonrpc: "2.0",
|
||||
method: "notifications/initialized",
|
||||
params: {}
|
||||
});
|
||||
|
||||
writeJsonLine(child, {
|
||||
id: 2,
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "tool_hub.resolve",
|
||||
arguments: {
|
||||
task: "查询北京今天的天气"
|
||||
}
|
||||
}
|
||||
});
|
||||
const first = await reader.nextMessage(2, 12_000).catch((error) => {
|
||||
error.message += ` stderr: ${stderr.join("")}`;
|
||||
throw error;
|
||||
});
|
||||
assert.equal(first.error, undefined);
|
||||
assert.deepEqual(first.result.selectedToolNames, ["mcp.multi_mcp.weather-forecast"]);
|
||||
|
||||
writeJsonLine(child, {
|
||||
id: 3,
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "tool_hub.resolve",
|
||||
arguments: {
|
||||
task: "查询麦当劳这个月有什么优惠活动"
|
||||
}
|
||||
}
|
||||
});
|
||||
const second = await reader.nextMessage(3, 12_000).catch((error) => {
|
||||
error.message += ` stderr: ${stderr.join("")}`;
|
||||
throw error;
|
||||
});
|
||||
assert.equal(second.error, undefined);
|
||||
assert.equal(second.result.alreadyResolved, undefined);
|
||||
assert.deepEqual(second.result.selectedToolNames, ["mcp.multi_mcp.campaign-calendar"]);
|
||||
});
|
||||
|
||||
function toolHubRuntimePath() {
|
||||
return [
|
||||
path.join(process.cwd(), "dist", "tests", "main", "toolhub-mcp.js"),
|
||||
path.join(process.cwd(), "packages", "electron", "dist", "main", "toolhub-mcp.js")
|
||||
].find((candidate) => existsSync(candidate)) ?? path.join(process.cwd(), "dist", "tests", "main", "toolhub-mcp.js");
|
||||
}
|
||||
|
||||
function writeJsonLine(child, message) {
|
||||
child.stdin.write(`${JSON.stringify(message)}\n`);
|
||||
}
|
||||
@@ -236,7 +363,15 @@ function jsonLineReader(child) {
|
||||
};
|
||||
}
|
||||
|
||||
function createMcpHttpServer() {
|
||||
function createMcpHttpServer(options = {}) {
|
||||
const serverName = options.serverName ?? "mcd-mcp";
|
||||
const tools = options.tools ?? [
|
||||
{
|
||||
description: "查询麦当劳中国当月的营销活动日历,返回进行中、往期和未来日期的活动。",
|
||||
inputSchema: { type: "object" },
|
||||
name: "campaign-calendar"
|
||||
}
|
||||
];
|
||||
return createServer(async (request, response) => {
|
||||
const payload = await readJsonBody(request);
|
||||
response.setHeader("content-type", "application/json");
|
||||
@@ -248,7 +383,7 @@ function createMcpHttpServer() {
|
||||
result: {
|
||||
capabilities: { tools: {} },
|
||||
protocolVersion: "2024-11-05",
|
||||
serverInfo: { name: "mcd-mcp", version: "1.0.0" }
|
||||
serverInfo: { name: serverName, version: "1.0.0" }
|
||||
}
|
||||
}));
|
||||
return;
|
||||
@@ -263,13 +398,7 @@ function createMcpHttpServer() {
|
||||
id: payload.id,
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
tools: [
|
||||
{
|
||||
description: "查询麦当劳中国当月的营销活动日历,返回进行中、往期和未来日期的活动。",
|
||||
inputSchema: { type: "object" },
|
||||
name: "campaign-calendar"
|
||||
}
|
||||
]
|
||||
tools
|
||||
}
|
||||
}));
|
||||
return;
|
||||
@@ -278,6 +407,60 @@ function createMcpHttpServer() {
|
||||
});
|
||||
}
|
||||
|
||||
function createTaskAwareResolverServer() {
|
||||
return createServer(async (request, response) => {
|
||||
if (request.method === "GET" && request.url === "/v1/models") {
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.end(JSON.stringify({ data: [], object: "list" }));
|
||||
return;
|
||||
}
|
||||
if (request.method === "POST" && request.url === "/v1/chat/completions") {
|
||||
const payload = await readJsonBody(request);
|
||||
const query = readResolverQuery(payload);
|
||||
const toolName = /天气|weather/i.test(query)
|
||||
? "mcp.multi_mcp.weather-forecast"
|
||||
: "mcp.multi_mcp.campaign-calendar";
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.end(JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: JSON.stringify({
|
||||
summary: "ok",
|
||||
toolNames: [toolName]
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
id: "chatcmpl-test",
|
||||
object: "chat.completion"
|
||||
}));
|
||||
return;
|
||||
}
|
||||
response.statusCode = 404;
|
||||
response.end("not found");
|
||||
});
|
||||
}
|
||||
|
||||
function readResolverQuery(payload) {
|
||||
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (!message || message.role !== "user" || typeof message.content !== "string") {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(message.content);
|
||||
if (typeof parsed.query === "string") {
|
||||
return parsed.query;
|
||||
}
|
||||
} catch {
|
||||
return message.content;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function createDelayedResolverServer() {
|
||||
return createServer(async (request, response) => {
|
||||
if (request.method === "GET" && request.url === "/v1/models") {
|
||||
|
||||
Reference in New Issue
Block a user