0717928496
## 调试日志
### 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>
214 lines
6.1 KiB
Go
214 lines
6.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/yourname/cyrene-ai/plugin-manager/internal/manager"
|
|
)
|
|
|
|
// PluginHandler exposes the Plugin Manager REST API via net/http.
|
|
type PluginHandler struct {
|
|
mgr *manager.PluginManager
|
|
}
|
|
|
|
func NewPluginHandler(mgr *manager.PluginManager) *PluginHandler {
|
|
return &PluginHandler{mgr: mgr}
|
|
}
|
|
|
|
func (h *PluginHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("/api/v1/plugins", h.listPlugins)
|
|
mux.HandleFunc("/api/v1/plugins/", h.pluginRoute)
|
|
mux.HandleFunc("/api/v1/tools", h.listTools)
|
|
mux.HandleFunc("/api/v1/tools/", h.toolRoute)
|
|
mux.HandleFunc("/api/v1/health", h.health)
|
|
}
|
|
|
|
func (h *PluginHandler) health(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "ok", "service": "plugin-manager"})
|
|
}
|
|
|
|
func (h *PluginHandler) listPlugins(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
return
|
|
}
|
|
plugins := h.mgr.List()
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"plugins": plugins, "total": len(plugins)})
|
|
}
|
|
|
|
func (h *PluginHandler) pluginRoute(w http.ResponseWriter, r *http.Request) {
|
|
// Path: /api/v1/plugins/{id}[/enable|/disable|/reload|/tools]
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/v1/plugins/")
|
|
parts := strings.SplitN(path, "/", 2)
|
|
pluginID := parts[0]
|
|
|
|
if pluginID == "" {
|
|
// GET /api/v1/plugins (handled by listPlugins normally)
|
|
h.listPlugins(w, r)
|
|
return
|
|
}
|
|
|
|
if len(parts) == 1 {
|
|
switch r.Method {
|
|
case "GET":
|
|
h.getPlugin(w, pluginID)
|
|
case "DELETE":
|
|
h.uninstallPlugin(w, r, pluginID)
|
|
default:
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
}
|
|
return
|
|
}
|
|
|
|
action := parts[1]
|
|
switch action {
|
|
case "enable":
|
|
if r.Method != "POST" {
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
return
|
|
}
|
|
h.enablePlugin(w, r, pluginID)
|
|
case "disable":
|
|
if r.Method != "POST" {
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
return
|
|
}
|
|
h.disablePlugin(w, r, pluginID)
|
|
case "reload":
|
|
if r.Method != "POST" {
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
return
|
|
}
|
|
h.reloadPlugin(w, r, pluginID)
|
|
case "tools":
|
|
if r.Method != "GET" {
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
return
|
|
}
|
|
h.pluginTools(w, pluginID)
|
|
default:
|
|
writeJSON(w, http.StatusNotFound, errResp("not found"))
|
|
}
|
|
}
|
|
|
|
func (h *PluginHandler) getPlugin(w http.ResponseWriter, id string) {
|
|
info, ok := h.mgr.Get(id)
|
|
if !ok {
|
|
writeJSON(w, http.StatusNotFound, errResp("plugin not found"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, info)
|
|
}
|
|
|
|
func (h *PluginHandler) enablePlugin(w http.ResponseWriter, r *http.Request, id string) {
|
|
if err := h.mgr.Enable(r.Context(), id); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "enabled"})
|
|
}
|
|
|
|
func (h *PluginHandler) disablePlugin(w http.ResponseWriter, r *http.Request, id string) {
|
|
if err := h.mgr.Disable(r.Context(), id); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "disabled"})
|
|
}
|
|
|
|
func (h *PluginHandler) reloadPlugin(w http.ResponseWriter, r *http.Request, id string) {
|
|
if err := h.mgr.Reload(r.Context(), id); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "reloaded"})
|
|
}
|
|
|
|
func (h *PluginHandler) uninstallPlugin(w http.ResponseWriter, r *http.Request, id string) {
|
|
if err := h.mgr.Uninstall(r.Context(), id); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "uninstalled"})
|
|
}
|
|
|
|
func (h *PluginHandler) pluginTools(w http.ResponseWriter, id string) {
|
|
info, ok := h.mgr.Get(id)
|
|
if !ok {
|
|
writeJSON(w, http.StatusNotFound, errResp("plugin not found"))
|
|
return
|
|
}
|
|
registry := h.mgr.Registry()
|
|
tools := make([]interface{}, 0)
|
|
for _, toolID := range info.Tools {
|
|
if t, ok := registry.Get(toolID); ok {
|
|
tools = append(tools, t.Definition())
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"tools": tools, "total": len(tools)})
|
|
}
|
|
|
|
func (h *PluginHandler) listTools(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
return
|
|
}
|
|
defs := h.mgr.Registry().Definitions()
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"tools": defs, "total": len(defs)})
|
|
}
|
|
|
|
func (h *PluginHandler) toolRoute(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/v1/tools/")
|
|
toolID := path
|
|
|
|
// Check if this is an execute call
|
|
if strings.HasSuffix(path, "/execute") {
|
|
toolID = strings.TrimSuffix(path, "/execute")
|
|
if r.Method != "POST" {
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
return
|
|
}
|
|
h.executeTool(w, r, toolID)
|
|
return
|
|
}
|
|
|
|
if r.Method != "GET" {
|
|
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
|
return
|
|
}
|
|
tool, ok := h.mgr.Registry().Get(toolID)
|
|
if !ok {
|
|
writeJSON(w, http.StatusNotFound, errResp("tool not found"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, tool.Definition())
|
|
}
|
|
|
|
func (h *PluginHandler) executeTool(w http.ResponseWriter, r *http.Request, toolID string) {
|
|
var body struct {
|
|
Arguments map[string]interface{} `json:"arguments"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, errResp("invalid request body"))
|
|
return
|
|
}
|
|
result, err := h.mgr.Registry().Execute(r.Context(), toolID, body.Arguments)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func errResp(msg string) map[string]string {
|
|
return map[string]string{"error": msg}
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|