fix: DevTools 记忆查询跨用户支持 + 会话监看路由权限修正
- memory_handler: Query/List/Add 支持管理员通过 user_id 参数跨用户查询 - router: sessions/active 移到 admin 路由组 (需要管理员权限) - devtools: sessions 代理路径更新为 /api/v1/admin/sessions/active
This commit is contained in:
@@ -5,7 +5,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -30,8 +32,16 @@ func NewMemoryHandler(aiCoreURL string) *MemoryHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Query 搜索用户记忆 — 代理 GET /api/v1/memory/search?user_id=...&q=...
|
// Query 搜索用户记忆 — 代理 GET /api/v1/memory/search?user_id=...&q=...
|
||||||
|
// 管理员可通过 user_id 查询参数查询任意用户的记忆
|
||||||
func (h *MemoryHandler) Query(c *gin.Context) {
|
func (h *MemoryHandler) Query(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
authUserID := middleware.GetUserID(c)
|
||||||
|
userID := c.Query("user_id")
|
||||||
|
|
||||||
|
// 非管理员只能查询自己的记忆;管理员可通过查询参数指定目标用户
|
||||||
|
if !strings.HasPrefix(authUserID, "admin_") || userID == "" {
|
||||||
|
userID = authUserID
|
||||||
|
}
|
||||||
|
|
||||||
query := c.Query("q")
|
query := c.Query("q")
|
||||||
if query == "" {
|
if query == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "查询参数q不能为空"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "查询参数q不能为空"})
|
||||||
@@ -43,7 +53,12 @@ func (h *MemoryHandler) Query(c *gin.Context) {
|
|||||||
|
|
||||||
resp, err := h.client.Get(url)
|
resp, err := h.client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("AI-Core 不可达: %v", err)})
|
log.Printf("[memory] AI-Core 不可达 (Query): %v", err)
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{
|
||||||
|
"error": fmt.Sprintf("AI-Core 不可达: %v", err),
|
||||||
|
"errorType": "ai_core_unreachable",
|
||||||
|
"hint": "AI-Core 服务未启动或不可达,请先在「服务管理」面板中启动 AI-Core",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -56,14 +71,26 @@ func (h *MemoryHandler) Query(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List 列出用户所有记忆 — 代理 GET /api/v1/memory?user_id=...
|
// List 列出用户所有记忆 — 代理 GET /api/v1/memory?user_id=...
|
||||||
|
// 管理员可通过 user_id 查询参数查询任意用户的记忆
|
||||||
func (h *MemoryHandler) List(c *gin.Context) {
|
func (h *MemoryHandler) List(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
authUserID := middleware.GetUserID(c)
|
||||||
|
userID := c.Query("user_id")
|
||||||
|
|
||||||
|
// 非管理员只能查询自己的记忆;管理员可通过查询参数指定目标用户
|
||||||
|
if !strings.HasPrefix(authUserID, "admin_") || userID == "" {
|
||||||
|
userID = authUserID
|
||||||
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/api/v1/memory?user_id=%s", h.aiCoreURL, userID)
|
url := fmt.Sprintf("%s/api/v1/memory?user_id=%s", h.aiCoreURL, userID)
|
||||||
|
|
||||||
resp, err := h.client.Get(url)
|
resp, err := h.client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("AI-Core 不可达: %v", err)})
|
log.Printf("[memory] AI-Core 不可达 (List): %v", err)
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{
|
||||||
|
"error": fmt.Sprintf("AI-Core 不可达: %v", err),
|
||||||
|
"errorType": "ai_core_unreachable",
|
||||||
|
"hint": "AI-Core 服务未启动或不可达,请先在「服务管理」面板中启动 AI-Core",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -76,10 +103,12 @@ func (h *MemoryHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add 手动添加记忆 — 代理 POST /api/v1/memory
|
// Add 手动添加记忆 — 代理 POST /api/v1/memory
|
||||||
|
// 管理员可通过请求体中的 user_id 字段为任意用户添加记忆
|
||||||
func (h *MemoryHandler) Add(c *gin.Context) {
|
func (h *MemoryHandler) Add(c *gin.Context) {
|
||||||
userID := middleware.GetUserID(c)
|
authUserID := middleware.GetUserID(c)
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
Content string `json:"content" binding:"required"`
|
Content string `json:"content" binding:"required"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
@@ -97,6 +126,12 @@ func (h *MemoryHandler) Add(c *gin.Context) {
|
|||||||
req.Priority = 1
|
req.Priority = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 管理员可通过请求体指定目标用户,否则使用认证用户
|
||||||
|
userID := authUserID
|
||||||
|
if strings.HasPrefix(authUserID, "admin_") && req.UserID != "" {
|
||||||
|
userID = req.UserID
|
||||||
|
}
|
||||||
|
|
||||||
// 转发到 AI-Core
|
// 转发到 AI-Core
|
||||||
aiReq := map[string]interface{}{
|
aiReq := map[string]interface{}{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
@@ -116,7 +151,12 @@ func (h *MemoryHandler) Add(c *gin.Context) {
|
|||||||
|
|
||||||
resp, err := h.client.Do(httpReq)
|
resp, err := h.client.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("AI-Core 不可达: %v", err)})
|
log.Printf("[memory] AI-Core 不可达 (Add): %v", err)
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{
|
||||||
|
"error": fmt.Sprintf("AI-Core 不可达: %v", err),
|
||||||
|
"errorType": "ai_core_unreachable",
|
||||||
|
"hint": "AI-Core 服务未启动或不可达,请先在「服务管理」面板中启动 AI-Core",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -146,7 +186,12 @@ func (h *MemoryHandler) Delete(c *gin.Context) {
|
|||||||
|
|
||||||
resp, err := h.client.Do(req)
|
resp, err := h.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("AI-Core 不可达: %v", err)})
|
log.Printf("[memory] AI-Core 不可达 (Delete): %v", err)
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{
|
||||||
|
"error": fmt.Sprintf("AI-Core 不可达: %v", err),
|
||||||
|
"errorType": "ai_core_unreachable",
|
||||||
|
"hint": "AI-Core 服务未启动或不可达,请先在「服务管理」面板中启动 AI-Core",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|||||||
@@ -52,14 +52,13 @@ func Setup(r *gin.Engine, hub *ws.Hub, cfg *config.Config) {
|
|||||||
|
|
||||||
// 会话管理
|
// 会话管理
|
||||||
sessions := protected.Group("/sessions")
|
sessions := protected.Group("/sessions")
|
||||||
{
|
{
|
||||||
sessions.POST("", sessionHandler.Create)
|
sessions.POST("", sessionHandler.Create)
|
||||||
sessions.GET("", sessionHandler.List)
|
sessions.GET("", sessionHandler.List)
|
||||||
sessions.GET("/active", sessionHandler.GetActiveSessions)
|
sessions.GET("/:id", sessionHandler.Get)
|
||||||
sessions.GET("/:id", sessionHandler.Get)
|
sessions.DELETE("/:id", sessionHandler.Delete)
|
||||||
sessions.DELETE("/:id", sessionHandler.Delete)
|
sessions.GET("/:id/messages", sessionHandler.GetMessages)
|
||||||
sessions.GET("/:id/messages", sessionHandler.GetMessages)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 记忆管理
|
// 记忆管理
|
||||||
memory := protected.Group("/memory")
|
memory := protected.Group("/memory")
|
||||||
@@ -75,6 +74,7 @@ func Setup(r *gin.Engine, hub *ws.Hub, cfg *config.Config) {
|
|||||||
admin.Use(adminAuth())
|
admin.Use(adminAuth())
|
||||||
{
|
{
|
||||||
admin.GET("/sessions", sessionHandler.ListActiveSessions)
|
admin.GET("/sessions", sessionHandler.ListActiveSessions)
|
||||||
|
admin.GET("/sessions/active", sessionHandler.GetActiveSessions)
|
||||||
admin.GET("/sessions/:id", sessionHandler.GetSession)
|
admin.GET("/sessions/:id", sessionHandler.GetSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-4
@@ -104,7 +104,14 @@ async function getGatewayToken() {
|
|||||||
async function proxyToGateway(path, opts = {}) {
|
async function proxyToGateway(path, opts = {}) {
|
||||||
const token = await getGatewayToken();
|
const token = await getGatewayToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return { status: 502, body: { error: '无法连接到 Gateway 认证服务' } };
|
return {
|
||||||
|
status: 502,
|
||||||
|
body: {
|
||||||
|
error: '无法连接到 Gateway 认证服务',
|
||||||
|
errorType: 'gateway_auth_failed',
|
||||||
|
hint: '请确认 Gateway 服务已启动 (端口 8080)',
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${GATEWAY_URL}${path}`;
|
const url = `${GATEWAY_URL}${path}`;
|
||||||
@@ -119,7 +126,17 @@ async function proxyToGateway(path, opts = {}) {
|
|||||||
const body = await resp.json().catch(() => null);
|
const body = await resp.json().catch(() => null);
|
||||||
return { status: resp.status, body };
|
return { status: resp.status, body };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { status: 502, body: { error: `Gateway 不可达: ${err.message}` } };
|
const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
|
||||||
|
return {
|
||||||
|
status: 502,
|
||||||
|
body: {
|
||||||
|
error: `Gateway 不可达: ${err.message}`,
|
||||||
|
errorType: isConnRefused ? 'gateway_not_running' : 'gateway_unreachable',
|
||||||
|
hint: isConnRefused
|
||||||
|
? 'Gateway 服务未启动,请先在「服务管理」面板中启动 Gateway'
|
||||||
|
: 'Gateway 服务无响应,请检查网络连接和服务状态',
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,9 +215,9 @@ app.get('/api/sessions', async (_req, res) => {
|
|||||||
res.status(result.status).json(result.body);
|
res.status(result.status).json(result.body);
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/sessions/active — 获取按用户分组的活跃会话
|
// GET /api/sessions/active — 获取按用户分组的活跃会话 (管理员权限)
|
||||||
app.get('/api/sessions/active', async (_req, res) => {
|
app.get('/api/sessions/active', async (_req, res) => {
|
||||||
const result = await proxyToGateway('/api/v1/sessions/active');
|
const result = await proxyToGateway('/api/v1/admin/sessions/active');
|
||||||
res.status(result.status).json(result.body);
|
res.status(result.status).json(result.body);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user