feat: Round 5 - Memory Service, Tool Engine, Call Records, Thinking Logs
- Fix: Session history flash (race condition + WS guard) - Fix: Chat background overlay + sidebar transparency - Fix: IoT device control (Chinese action names, status field) - Feat: Independent memory-service (port 8091, 13 endpoints) - Feat: Independent tool-engine service (port 8092, 13 tools) - Feat: Tool call logs with paginated DevTools panel - Feat: Thinking log records with DevTools panel - Feat: Future development roadmap document - Chore: Updated .gitignore, go.work, DevTools config - Chore: 5-service health check, project review docs
This commit is contained in:
+130
-1
@@ -17,7 +17,9 @@ import { execSync, spawn } from 'child_process';
|
||||
|
||||
import { processManager } from './process-manager.js';
|
||||
import { performanceMonitor } from './performance.js';
|
||||
import { SERVICES, DEVTOOLS_PORT, LOGS_DIR, logFile, GATEWAY_URL, ADMIN_USERNAME, ADMIN_PASSWORD } from './config.js';
|
||||
import { SERVICES, DEVTOOLS_PORT, LOGS_DIR, logFile, GATEWAY_URL, TOOL_ENGINE_URL, ADMIN_USERNAME, ADMIN_PASSWORD } from './config.js';
|
||||
|
||||
const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || 'http://localhost:8091';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const TUNNEL_SCRIPT = path.join(ROOT, 'scripts/tunnel.sh');
|
||||
@@ -562,6 +564,133 @@ app.get('/api/iot/devices/:id/history', async (req, res) => {
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// ---- 工具调用记录代理 (转发到 tool-engine) ----
|
||||
|
||||
/**
|
||||
* 代理请求到 Tool-Engine
|
||||
* @param {string} path - Tool-Engine API 路径
|
||||
* @param {object} opts - fetch 选项
|
||||
*/
|
||||
async function proxyToToolEngine(path, opts = {}) {
|
||||
const url = `${TOOL_ENGINE_URL}${path}`;
|
||||
const logPrefix = `[ToolEngine代理]`;
|
||||
try {
|
||||
console.log(`${logPrefix} ${opts.method || 'GET'} ${path}`);
|
||||
const resp = await fetch(url, {
|
||||
...opts,
|
||||
headers: { 'Content-Type': 'application/json', ...opts.headers },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
const body = await resp.json().catch(() => null);
|
||||
if (!resp.ok) {
|
||||
console.log(`${logPrefix} 请求失败 (HTTP ${resp.status}): ${path}`);
|
||||
}
|
||||
return { status: resp.status, body };
|
||||
} catch (err) {
|
||||
const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
|
||||
console.error(`${logPrefix} 请求异常: ${path} - ${err.message}`);
|
||||
return {
|
||||
status: 502,
|
||||
body: {
|
||||
error: `Tool-Engine 不可达: ${err.message}`,
|
||||
errorType: isConnRefused ? 'tool_engine_not_running' : 'tool_engine_unreachable',
|
||||
hint: isConnRefused
|
||||
? 'Tool-Engine 服务未启动,请先在「服务管理」面板中启动 Tool-Engine'
|
||||
: 'Tool-Engine 服务无响应,请检查网络连接和服务状态',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/tool-calls — 查询工具调用记录
|
||||
app.get('/api/tool-calls', async (req, res) => {
|
||||
const { tool_name, page, limit } = req.query;
|
||||
const params = new URLSearchParams();
|
||||
if (tool_name) params.set('tool_name', tool_name);
|
||||
params.set('page', page || '1');
|
||||
params.set('limit', limit || '20');
|
||||
const result = await proxyToToolEngine(`/api/v1/tools/calls?${params.toString()}`);
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// GET /api/tool-calls/stats — 工具调用统计
|
||||
app.get('/api/tool-calls/stats', async (_req, res) => {
|
||||
const result = await proxyToToolEngine('/api/v1/tools/calls/stats');
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// ---- 自主思考日志代理 (转发到 memory-service) ----
|
||||
|
||||
/**
|
||||
* 代理请求到 Memory-Service
|
||||
* @param {string} path - Memory-Service API 路径
|
||||
* @param {object} opts - fetch 选项
|
||||
*/
|
||||
async function proxyToMemoryService(path, opts = {}) {
|
||||
const url = `${MEMORY_SERVICE_URL}${path}`;
|
||||
const logPrefix = `[MemoryService代理]`;
|
||||
try {
|
||||
console.log(`${logPrefix} ${opts.method || 'GET'} ${path}`);
|
||||
const resp = await fetch(url, {
|
||||
...opts,
|
||||
headers: { 'Content-Type': 'application/json', ...opts.headers },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
const body = await resp.json().catch(() => null);
|
||||
if (!resp.ok) {
|
||||
console.log(`${logPrefix} 请求失败 (HTTP ${resp.status}): ${path}`);
|
||||
}
|
||||
return { status: resp.status, body };
|
||||
} catch (err) {
|
||||
const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
|
||||
console.error(`${logPrefix} 请求异常: ${path} - ${err.message}`);
|
||||
return {
|
||||
status: 502,
|
||||
body: {
|
||||
error: `Memory-Service 不可达: ${err.message}`,
|
||||
errorType: isConnRefused ? 'memory_service_not_running' : 'memory_service_unreachable',
|
||||
hint: isConnRefused
|
||||
? 'Memory-Service 服务未启动,请先在「服务管理」面板中启动 Memory-Service'
|
||||
: 'Memory-Service 服务无响应,请检查网络连接和服务状态',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/v1/thinking — 查询自主思考日志列表
|
||||
app.get('/api/v1/thinking', async (req, res) => {
|
||||
const { user_id, limit, offset } = req.query;
|
||||
const params = new URLSearchParams();
|
||||
if (user_id) params.set('user_id', user_id);
|
||||
if (limit) params.set('limit', limit);
|
||||
if (offset) params.set('offset', offset);
|
||||
const result = await proxyToMemoryService(`/api/v1/thinking?${params.toString()}`);
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// POST /api/v1/thinking — 创建自主思考日志
|
||||
app.post('/api/v1/thinking', async (req, res) => {
|
||||
const result = await proxyToMemoryService('/api/v1/thinking', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// GET /api/v1/thinking/stats — 自主思考统计
|
||||
app.get('/api/v1/thinking/stats', async (req, res) => {
|
||||
const { user_id } = req.query;
|
||||
const params = user_id ? `?user_id=${encodeURIComponent(user_id)}` : '';
|
||||
const result = await proxyToMemoryService(`/api/v1/thinking/stats${params}`);
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// GET /api/v1/thinking/:id — 获取单条自主思考日志
|
||||
app.get('/api/v1/thinking/:id', async (req, res) => {
|
||||
const result = await proxyToMemoryService(`/api/v1/thinking/${req.params.id}`);
|
||||
res.status(result.status).json(result.body);
|
||||
});
|
||||
|
||||
// ---- 健康检查代理 ----
|
||||
app.get('/api/proxy/:id/health', async (req, res) => {
|
||||
const svc = SERVICES[req.params.id];
|
||||
|
||||
Reference in New Issue
Block a user