|
|
@@ -1,7 +1,8 @@
|
|
|
const state = {
|
|
|
history: [],
|
|
|
lastTables: [],
|
|
|
- liveTrace: []
|
|
|
+ liveTrace: [],
|
|
|
+ selectedToolName: ""
|
|
|
};
|
|
|
|
|
|
const els = {
|
|
|
@@ -36,7 +37,7 @@ function bindEvents() {
|
|
|
sendMessage();
|
|
|
});
|
|
|
els.input.addEventListener("input", () => {
|
|
|
- els.inputCount.textContent = `${els.input.value.trim().length} 字`;
|
|
|
+ updateInputCount();
|
|
|
});
|
|
|
els.input.addEventListener("keydown", (event) => {
|
|
|
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
|
@@ -44,7 +45,9 @@ function bindEvents() {
|
|
|
sendMessage();
|
|
|
}
|
|
|
});
|
|
|
- els.refreshBtn.addEventListener("click", refreshStatus);
|
|
|
+ if (els.refreshBtn) {
|
|
|
+ els.refreshBtn.addEventListener("click", refreshStatus);
|
|
|
+ }
|
|
|
document.querySelector("[data-scroll-tools]").addEventListener("click", () => {
|
|
|
document.querySelector("#toolsPanel").scrollIntoView({ behavior: "smooth" });
|
|
|
});
|
|
|
@@ -59,8 +62,12 @@ async function refreshStatus() {
|
|
|
const health = await getJson("/api/health");
|
|
|
const tools = await getJson("/api/tools");
|
|
|
const mcp = health.mcp || {};
|
|
|
- els.mcpEndpoint.textContent = mcp.path ? `${mcp.name || "mcp"} ${mcp.path}` : "-";
|
|
|
- els.toolCount.textContent = `${tools.tools?.length || 0} 个`;
|
|
|
+ if (els.mcpEndpoint) {
|
|
|
+ els.mcpEndpoint.textContent = mcp.path ? `${mcp.name || "mcp"} ${mcp.path}` : "-";
|
|
|
+ }
|
|
|
+ if (els.toolCount) {
|
|
|
+ els.toolCount.textContent = `${tools.tools?.length || 0} 个`;
|
|
|
+ }
|
|
|
renderTools(tools.tools || []);
|
|
|
if (health.ok) {
|
|
|
setStatus("ok", "已连接", `${mcp.environmentName || "MCP"} 可用`);
|
|
|
@@ -87,7 +94,7 @@ async function sendMessage() {
|
|
|
|
|
|
addUserMessage(message);
|
|
|
els.input.value = "";
|
|
|
- els.inputCount.textContent = "0 字";
|
|
|
+ updateInputCount();
|
|
|
state.liveTrace = [];
|
|
|
setBusy(true);
|
|
|
const progressId = addProgressMessage("正在分析这个需求", "我会先判断是否需要查业务系统、数据库或外部工具。");
|
|
|
@@ -346,17 +353,105 @@ function safeFileName(value) {
|
|
|
function renderTools(tools) {
|
|
|
els.toolList.innerHTML = "";
|
|
|
for (const tool of tools) {
|
|
|
- const item = document.createElement("div");
|
|
|
+ const item = document.createElement("button");
|
|
|
+ item.type = "button";
|
|
|
item.className = "tool-item";
|
|
|
+ item.dataset.toolName = tool.name;
|
|
|
+ item.title = "点击后将工具调用意图填入输入框";
|
|
|
+ if (state.selectedToolName === tool.name) {
|
|
|
+ item.classList.add("active");
|
|
|
+ }
|
|
|
const title = document.createElement("strong");
|
|
|
title.textContent = tool.title || tool.name;
|
|
|
const desc = document.createElement("span");
|
|
|
desc.textContent = `${tool.name}${tool.description ? ` - ${tool.description}` : ""}`;
|
|
|
- item.append(title, desc);
|
|
|
+ const params = document.createElement("small");
|
|
|
+ params.textContent = summarizeToolParams(tool);
|
|
|
+ item.append(title, desc, params);
|
|
|
+ item.addEventListener("click", () => selectTool(tool));
|
|
|
els.toolList.appendChild(item);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function selectTool(tool) {
|
|
|
+ state.selectedToolName = tool.name;
|
|
|
+ els.toolList.querySelectorAll(".tool-item").forEach((node) => {
|
|
|
+ node.classList.toggle("active", node.dataset.toolName === tool.name);
|
|
|
+ });
|
|
|
+
|
|
|
+ els.input.value = buildToolPrompt(tool);
|
|
|
+ updateInputCount();
|
|
|
+ els.input.focus();
|
|
|
+ addAssistantMessage(buildToolSelectionMessage(tool));
|
|
|
+}
|
|
|
+
|
|
|
+function summarizeToolParams(tool) {
|
|
|
+ const schema = tool.inputSchema || {};
|
|
|
+ const properties = schema.properties || {};
|
|
|
+ const keys = Object.keys(properties);
|
|
|
+ if (!keys.length) {
|
|
|
+ return "无参数,点击后可直接发送";
|
|
|
+ }
|
|
|
+ const required = new Set(schema.required || []);
|
|
|
+ const requiredKeys = keys.filter((key) => required.has(key));
|
|
|
+ const optionalKeys = keys.filter((key) => !required.has(key));
|
|
|
+ const parts = [];
|
|
|
+ if (requiredKeys.length) {
|
|
|
+ parts.push(`必填:${requiredKeys.join("、")}`);
|
|
|
+ }
|
|
|
+ if (optionalKeys.length) {
|
|
|
+ parts.push(`可选:${optionalKeys.join("、")}`);
|
|
|
+ }
|
|
|
+ return parts.join(";");
|
|
|
+}
|
|
|
+
|
|
|
+function buildToolPrompt(tool) {
|
|
|
+ const schema = tool.inputSchema || {};
|
|
|
+ const properties = schema.properties || {};
|
|
|
+ const keys = Object.keys(properties);
|
|
|
+ const title = tool.title || tool.name;
|
|
|
+ if (!keys.length) {
|
|
|
+ return `使用「${title}」`;
|
|
|
+ }
|
|
|
+ const required = new Set(schema.required || []);
|
|
|
+ const params = keys.map((key) => {
|
|
|
+ const label = required.has(key) ? "必填" : "可选";
|
|
|
+ const description = properties[key]?.description || "";
|
|
|
+ return `${key}(${label}:${description})=`;
|
|
|
+ });
|
|
|
+ return `使用「${title}」,${params.join(";")}`;
|
|
|
+}
|
|
|
+
|
|
|
+function buildToolSelectionMessage(tool) {
|
|
|
+ const schema = tool.inputSchema || {};
|
|
|
+ const properties = schema.properties || {};
|
|
|
+ const keys = Object.keys(properties);
|
|
|
+ const title = tool.title || tool.name;
|
|
|
+ const lines = [
|
|
|
+ `已选择工具:${title}`,
|
|
|
+ tool.description || "",
|
|
|
+ `工具名:${tool.name}`
|
|
|
+ ].filter(Boolean);
|
|
|
+
|
|
|
+ if (keys.length) {
|
|
|
+ const required = new Set(schema.required || []);
|
|
|
+ const params = keys.map((key) => {
|
|
|
+ const label = required.has(key) ? "必填" : "可选";
|
|
|
+ const description = properties[key]?.description || properties[key]?.type || "";
|
|
|
+ return `- ${key}(${label}):${description}`;
|
|
|
+ });
|
|
|
+ lines.push(`参数:\n${params.join("\n")}`);
|
|
|
+ lines.push("我已把参数模板放到输入框,补齐后发送即可。");
|
|
|
+ } else {
|
|
|
+ lines.push("这个工具不需要参数,我已把可直接发送的指令放到输入框。");
|
|
|
+ }
|
|
|
+ return lines.join("\n\n");
|
|
|
+}
|
|
|
+
|
|
|
+function updateInputCount() {
|
|
|
+ els.inputCount.textContent = `${els.input.value.trim().length} 字`;
|
|
|
+}
|
|
|
+
|
|
|
function renderTrace(trace, toolCalls) {
|
|
|
els.traceList.innerHTML = "";
|
|
|
const rows = trace.length ? trace : toolCalls.map((item) => ({
|