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:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user