fix: Phase 6联调 — 插件管理器端口修正 + 多模型配置系统整合 + 历史消息刷新修复

## 调试日志

### 1. 插件管理器启动失败
- **症状**: DevTools 显示插件管理器一直"已停止",手动启动正常
- **排查**: 对比 process-manager.js 传入的环境变量 vs plugin-manager config.go 读取的变量
- **根因**: config.js 传入 PLUGIN_MANAGER_PORT=8094,但 config.go 读取 os.Getenv("PORT"),env 名不匹配。且 process.env 中 PORT 泄露时被误读为 9090,与 DevTools 端口冲突
- **修复**: config.js 将 PLUGIN_MANAGER_PORT → PORT,使 env 名与代码一致 (c3055f4)

### 2. 历史消息刷新后消失
- **症状**: 浏览器刷新后聊天历史清空
- **排查**: WebSocket history_response handler 中 if (msg.messages) 对空数组 [] 为 truthy
- **根因**: 后端返回空的 history_response (缓存为空) 时,空数组覆盖了 HTTP 已加载的消息
- **修复**: useWebSocket.ts 改为 if (msg.messages && msg.messages.length > 0),空数组走 else-if 分支仅打日志,不覆盖已有消息

### 3. Phase 6 多模型配置系统
- Gateway: ModelsConfigStore (JSON文件持久化) + Admin CRUD API (providers/models/routing)
- ai-core: ModelSelector 支持按 purpose 选择 + fallback_chain,无配置时回退 .env
- DevTools: 模型配置管理面板 (Providers/Models/Routing 三Tab)、在线模型查询代理、路由表单 checkbox 多选、关键词搜索过滤
- .gitignore: models.json + platform_configs.json

### 4. 多端客户端追踪
- Hub 新增 knownClients 映射 (clientID → KnownClient),在线/离线状态追踪
- 客户端备注持久化到 PostgreSQL
- DevTools 客户端管理面板

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 21:23:10 +08:00
parent 965cce7192
commit 0717928496
29 changed files with 3177 additions and 137 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ export const SERVICES = {
cwd: path.join(ROOT, 'backend/plugin-manager'),
command: './main',
env: {
PLUGIN_MANAGER_PORT: '8094',
PORT: '8094',
IOT_SERVICE_URL: process.env.IOT_SERVICE_URL || process.env.IOT_DEBUG_SERVICE_URL || 'http://localhost:8083',
},
healthUrl: 'http://localhost:8094/api/v1/health',
+177
View File
@@ -21,6 +21,7 @@ import { SERVICES, DEVTOOLS_PORT, LOGS_DIR, logFile, GATEWAY_URL, TOOL_ENGINE_UR
const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || 'http://localhost:8091';
const VOICE_SERVICE_URL = process.env.VOICE_SERVICE_URL || 'http://localhost:8093';
const PLATFORM_BRIDGE_URL = process.env.PLATFORM_BRIDGE_URL || 'http://localhost:8095';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
@@ -596,6 +597,182 @@ async function proxyToToolEngine(path, opts = {}) {
}
}
// ---- 第三方聊天平台配置代理 (转发到 platform-bridge) ----
/**
* 代理请求到 Platform-Bridge
* @param {string} path - Platform-Bridge API 路径
* @param {object} opts - fetch 选项
*/
async function proxyToPlatformBridge(path, opts = {}) {
const url = `${PLATFORM_BRIDGE_URL}${path}`;
const logPrefix = `[PlatformBridge代理]`;
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: `Platform-Bridge 不可达: ${err.message}`,
errorType: isConnRefused ? 'bridge_not_running' : 'bridge_unreachable',
hint: isConnRefused
? 'Platform-Bridge 服务未启动,请先在「服务管理」面板中启动该服务'
: 'Platform-Bridge 服务无响应,请检查网络连接和服务状态',
},
};
}
}
// GET /api/chat-platforms/configs — 列出所有平台配置
app.get('/api/chat-platforms/configs', async (_req, res) => {
const result = await proxyToPlatformBridge('/api/v1/configs');
res.status(result.status).json(result.body);
});
// GET /api/chat-platforms/configs/:name — 获取单个配置
app.get('/api/chat-platforms/configs/:name', async (req, res) => {
const result = await proxyToPlatformBridge(`/api/v1/configs/${req.params.name}`);
res.status(result.status).json(result.body);
});
// POST /api/chat-platforms/configs/:name — 创建或更新配置
app.post('/api/chat-platforms/configs/:name', async (req, res) => {
const result = await proxyToPlatformBridge(`/api/v1/configs/${req.params.name}`, {
method: 'POST',
body: JSON.stringify(req.body),
});
res.status(result.status).json(result.body);
});
// DELETE /api/chat-platforms/configs/:name — 删除配置
app.delete('/api/chat-platforms/configs/:name', async (req, res) => {
const result = await proxyToPlatformBridge(`/api/v1/configs/${req.params.name}`, {
method: 'DELETE',
});
res.status(result.status).json(result.body);
});
// GET /api/chat-platforms/logs/:name — 获取消息日志
app.get('/api/chat-platforms/logs/:name', async (req, res) => {
const limit = req.query.limit || '100';
const result = await proxyToPlatformBridge(`/api/v1/logs/${req.params.name}?limit=${limit}`);
res.status(result.status).json(result.body);
});
// ---- 多端客户端管理代理 (转发到 Gateway) ----
// GET /api/clients — 获取已知客户端列表
app.get('/api/clients', async (req, res) => {
const userID = req.query.user_id || 'admin';
const result = await proxyToGateway(`/api/v1/admin/clients?user_id=${encodeURIComponent(userID)}`);
res.status(result.status).json(result.body);
});
// PUT /api/clients/:id/note — 更新客户端备注
app.put('/api/clients/:id/note', async (req, res) => {
const { note } = req.body;
const result = await proxyToGateway(`/api/v1/admin/clients/${req.params.id}/note`, {
method: 'PUT',
body: JSON.stringify({ note }),
});
res.status(result.status).json(result.body);
});
// ---- 模型配置管理代理 (转发到 Gateway admin) ----
// Providers
app.get('/api/model-config/providers', async (_req, res) => {
const result = await proxyToGateway('/api/v1/admin/models/providers');
res.status(result.status).json(result.body);
});
app.get('/api/model-config/providers/:name', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/providers/${req.params.name}`);
res.status(result.status).json(result.body);
});
app.post('/api/model-config/providers/:name', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/providers/${req.params.name}`, {
method: 'POST', body: JSON.stringify(req.body),
});
res.status(result.status).json(result.body);
});
app.delete('/api/model-config/providers/:name', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/providers/${req.params.name}`, {
method: 'DELETE',
});
res.status(result.status).json(result.body);
});
// Models
app.get('/api/model-config/models', async (_req, res) => {
const result = await proxyToGateway('/api/v1/admin/models/models');
res.status(result.status).json(result.body);
});
app.get('/api/model-config/models/:id', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/models/${req.params.id}`);
res.status(result.status).json(result.body);
});
app.post('/api/model-config/models/:id', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/models/${req.params.id}`, {
method: 'POST', body: JSON.stringify(req.body),
});
res.status(result.status).json(result.body);
});
app.delete('/api/model-config/models/:id', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/models/${req.params.id}`, {
method: 'DELETE',
});
res.status(result.status).json(result.body);
});
// Routing
app.get('/api/model-config/routing', async (_req, res) => {
const result = await proxyToGateway('/api/v1/admin/models/routing');
res.status(result.status).json(result.body);
});
app.get('/api/model-config/routing/:purpose', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/routing/${req.params.purpose}`);
res.status(result.status).json(result.body);
});
app.post('/api/model-config/routing/:purpose', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/routing/${req.params.purpose}`, {
method: 'POST', body: JSON.stringify(req.body),
});
res.status(result.status).json(result.body);
});
app.delete('/api/model-config/routing/:purpose', async (req, res) => {
const result = await proxyToGateway(`/api/v1/admin/models/routing/${req.params.purpose}`, {
method: 'DELETE',
});
res.status(result.status).json(result.body);
});
// Health check
app.post('/api/model-config/health-check', async (req, res) => {
const result = await proxyToGateway('/api/v1/admin/models/health-check', {
method: 'POST', body: JSON.stringify(req.body),
});
res.status(result.status).json(result.body);
});
// GET /api/model-config/fetch-models/:name?url=... — 代理查询 Provider 模型列表
app.get('/api/model-config/fetch-models/:name', async (req, res) => {
const urlParam = req.query.url ? '?url=' + encodeURIComponent(req.query.url) : '';
const result = await proxyToGateway('/api/v1/admin/models/fetch-models/' + encodeURIComponent(req.params.name) + urlParam);
res.status(result.status).json(result.body);
});
// GET /api/tool-calls — 查询工具调用记录
app.get('/api/tool-calls', async (req, res) => {
const { tool_name, page, limit } = req.query;