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:
2026-05-18 20:05:14 +08:00
parent b6ec36886c
commit 78e3f450c2
54 changed files with 7846 additions and 106 deletions
+3
View File
@@ -13,6 +13,7 @@ const ROOT = path.resolve(__dirname, '../..');
export const DEVTOOLS_PORT = process.env.DEVTOOLS_PORT || 9090;
export const LOGS_DIR = path.resolve(__dirname, '../logs');
export const GATEWAY_URL = process.env.GATEWAY_URL || 'http://localhost:8080';
export const TOOL_ENGINE_URL = process.env.TOOL_ENGINE_URL || 'http://localhost:8092';
export const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin';
export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'cyrene-dev-admin';
@@ -54,6 +55,8 @@ export const SERVICES = {
GATEWAY_PORT: '8080',
JWT_SECRET: process.env.JWT_SECRET || 'dev-secret-key-change-me',
AI_CORE_URL: 'http://localhost:8081',
MEMORY_SERVICE_URL: process.env.MEMORY_SERVICE_URL || 'http://localhost:8091',
TOOL_ENGINE_URL: process.env.TOOL_ENGINE_URL || 'http://localhost:8092',
ADMIN_USERNAME: process.env.ADMIN_USERNAME || 'admin',
ADMIN_PASSWORD: process.env.ADMIN_PASSWORD || 'cyrene-dev-admin',
REGISTRATION_ENABLED: process.env.REGISTRATION_ENABLED || 'true',
+130 -1
View File
@@ -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];