fix(coordinator): use the caller name and reject non-decimal user ids

Ambiguous consult only redirects on low confidence, and the testing template greets the signed-in user instead of a hardcoded name. Session tokens no longer parse as user id 0. Live chat sessions now evict the oldest entry at a hard cap.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-19 09:19:36 +00:00
co-authored by Cursor
parent 265f09c1f2
commit d4041a4f0c
5 changed files with 115 additions and 10 deletions
+29 -9
View File
@@ -405,10 +405,10 @@ func (e *CoordinatorEngine) handleAmbiguousConsult(ctx context.Context, cand *in
projName = "当前项目"
}
// 若用户输入实质上包含具体的技术测试、方案、难点或业务要点询问,不应机械回复商业接待犹豫套话
if strings.Contains(rawInput, "测试") || strings.Contains(rawInput, "验证") || strings.Contains(rawInput, "型检") ||
// 只在低置信度的模糊咨询里,才用关键词改走测试分析。高置信度保持原意图。
if cand.Confidence < 0.6 && (strings.Contains(rawInput, "测试") || strings.Contains(rawInput, "验证") || strings.Contains(rawInput, "型检") ||
strings.Contains(rawInput, "难点") || strings.Contains(rawInput, "注意") || strings.Contains(rawInput, "比较难") ||
strings.Contains(rawInput, "方案") {
strings.Contains(rawInput, "方案")) {
if cand.FilledSlots == nil {
cand.FilledSlots = make(map[string]string)
}
@@ -482,8 +482,14 @@ func (e *CoordinatorEngine) handleQueryProgress(ctx context.Context, cand *inten
projName = "肌骨超声AI辅助诊断系统"
}
greet := strings.TrimSpace(userCtx.User.Name)
if greet == "" {
greet = "您好"
} else {
greet += ",您好"
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("张工您好!针对「%s」的系统验证方案(SVP)与型式检验要求,我为您梳理以下核心难点项目与特别注意事项:\n\n", projName))
sb.WriteString(fmt.Sprintf("%s!针对「%s」的系统验证方案(SVP)与型式检验要求,我为您梳理以下核心难点项目与特别注意事项:\n\n", greet, projName))
sb.WriteString("### 一、 核心测试难点项目\n\n")
sb.WriteString("1. **声学输出安全与探头表面温升极限(GB 9706.237-2020)**\n")
sb.WriteString(" • **难点分析**:肌骨超声常需配合高频线阵探头(10-18MHz)进行浅表滑膜微血流彩色多普勒(CDFI/Power Doppler)检测。连续高占空比发射脉冲会导致探头声透镜声吸收发热。根据强标要求,探头施加于人体的表面温升严禁超过 **41℃**,同时机械指数(MI)需严格低于 1.9,热指数(TIS/TIB)需低于 1.0。\n")
@@ -499,7 +505,7 @@ func (e *CoordinatorEngine) handleQueryProgress(ctx context.Context, cand *inten
sb.WriteString("• **电磁兼容(EMC GB 9706.102-2021)抗扰度储备**:高频微弱回波信号接收通道抗射频电磁场辐射敏感度高,测试方案中需提前验证探头线缆双层屏蔽接地与机箱屏蔽缝隙,避免正式型检时辐射发射(RE)超标返工;\n")
sb.WriteString("• **DICOM 与数据脱敏合规**:软件系统测试中,所有导出样本、测试影像的患者个人隐私标识(PII)脱敏规则需执行 100% 覆盖率验证;\n")
sb.WriteString("• **极端异常输入与容错安全防呆**:探头带电热插拔、探头声透镜声胶脱落阻抗异常检测、长时间无操作探头自动降频防发热休眠逻辑,均需设计破坏性负向测试用例。\n\n")
sb.WriteString("💡 **建议行动**:张工,若您需要,我可以为您起草该模块对应的《系统验证方案 (SVP) 关键测试用例矩阵》,列出详细的测试条件、前置配置与合格判据。")
sb.WriteString("💡 **建议行动**:若您需要,我可以为您起草该模块对应的《系统验证方案 (SVP) 关键测试用例矩阵》,列出详细的测试条件、前置配置与合格判据。")
return &CoordinationResult{
Status: StatusAccepted,
@@ -928,10 +934,10 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.
var projectID, userID int64
if userCtx.Project.ID != "" {
fmt.Sscanf(userCtx.Project.ID, "%d", &projectID)
projectID = parseDecimalID(userCtx.Project.ID)
}
if userCtx.User.ID != "" {
fmt.Sscanf(userCtx.User.ID, "%d", &userID)
userID = parseDecimalID(userCtx.User.ID)
}
taskRes, err := e.tasks().ExecuteTask(ctx, stageflow.TaskRunRequest{
@@ -1065,10 +1071,10 @@ func (e *CoordinatorEngine) handleTriggerAdhocTask(ctx context.Context, cand *in
var projectID, userID int64
if userCtx.Project.ID != "" {
fmt.Sscanf(userCtx.Project.ID, "%d", &projectID)
projectID = parseDecimalID(userCtx.Project.ID)
}
if userCtx.User.ID != "" {
fmt.Sscanf(userCtx.User.ID, "%d", &userID)
userID = parseDecimalID(userCtx.User.ID)
}
if st := stream.FromContext(ctx); st != nil {
@@ -1822,3 +1828,17 @@ func (e *CoordinatorEngine) handleQueryUserRights(ctx context.Context, cand *int
UpdatedContext: userCtx,
}, nil
}
// parseDecimalID accepts only a non-negative decimal integer.
// Values like u_sess_* must not become UserID 0 via a partial Sscanf match.
func parseDecimalID(raw string) int64 {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0
}
n, err := strconv.ParseInt(raw, 10, 64)
if err != nil || n < 0 {
return 0
}
return n
}
+6
View File
@@ -981,4 +981,10 @@ func TestCoordinatorEngine_TestingAnalysis(t *testing.T) {
if strings.Contains(res.NaturalReply, "完全理解您想再掂量一下") {
t.Errorf("reply must not contain commercial triage boilerplate, got: %s", res.NaturalReply)
}
if !strings.Contains(res.NaturalReply, "张工,您好") {
t.Errorf("greeting must use the user name, got: %s", res.NaturalReply)
}
if strings.Contains(res.NaturalReply, "张工您好") {
t.Errorf("hardcoded greeting must be gone, got: %s", res.NaturalReply)
}
}
+18
View File
@@ -0,0 +1,18 @@
package coordinator
import "testing"
func TestParseDecimalIDRejectsSessionTokens(t *testing.T) {
if got := parseDecimalID("u_sess_12"); got != 0 {
t.Fatalf("session token parsed as %d", got)
}
if got := parseDecimalID("108abc"); got != 0 {
t.Fatalf("partial decimal parsed as %d", got)
}
if got := parseDecimalID("108"); got != 108 {
t.Fatalf("got %d", got)
}
if got := parseDecimalID(" 0 "); got != 0 {
t.Fatalf("got %d", got)
}
}
+29 -1
View File
@@ -406,6 +406,32 @@ func (s *Server) cleanStaleSessionsLocked(ttl time.Duration) int {
return cleaned
}
const maxLiveSessions = 4096
func (s *Server) enforceSessionCapLocked() {
for len(s.sessions) >= maxLiveSessions {
var oldestID string
var oldest time.Time
found := false
for id, sess := range s.sessions {
if sess == nil {
delete(s.sessions, id)
continue
}
at := sess.LastAccessed()
if !found || at.Before(oldest) {
found = true
oldest = at
oldestID = id
}
}
if !found || oldestID == "" {
return
}
delete(s.sessions, oldestID)
}
}
// getOrCreateSession 获取或动态构建会话。callerUserID>0 时校验持久层归属,不匹配返回 ErrSessionOwnerMismatch。
func (s *Server) getOrCreateSession(
ctx context.Context,
@@ -456,10 +482,12 @@ func (s *Server) getOrCreateSession(
return existing, nil
}
// 惰性内存防泄漏保护 (会话数超过 200 时清理空闲超 120 分钟会话)
// 惰性清理,并在仍超过硬上限时按最近访问淘汰,避免随机 session id 撑爆内存。
// 会话 id 仍由客户端提供:历史恢复按该 id 查找,不能改成只接受服务端签发。
if len(s.sessions) > 200 {
s.cleanStaleSessionsLocked(120 * time.Minute)
}
s.enforceSessionCapLocked()
// 尝试从持久层恢复会话上下文 (服务重启后跨进程保持 G3)
var restored *ctxmgr.RestoredSessionContext
+33
View File
@@ -0,0 +1,33 @@
package server
import (
"fmt"
"testing"
"time"
"chat-agent-system/internal/chat"
)
func TestEnforceSessionCapEvictsOldest(t *testing.T) {
s := &Server{sessions: map[string]*chat.Session{}}
oldest := chat.NewSession("oldest", nil, nil, nil)
oldest.TouchLastAccessed()
s.sessions[oldest.ID] = oldest
time.Sleep(5 * time.Millisecond)
for i := 0; i < maxLiveSessions-1; i++ {
id := fmt.Sprintf("s%d", i)
sess := chat.NewSession(id, nil, nil, nil)
sess.TouchLastAccessed()
s.sessions[id] = sess
}
if len(s.sessions) != maxLiveSessions {
t.Fatalf("setup len=%d", len(s.sessions))
}
s.enforceSessionCapLocked()
if _, ok := s.sessions["oldest"]; ok {
t.Fatal("oldest session must be evicted at the cap")
}
if len(s.sessions) != maxLiveSessions-1 {
t.Fatalf("len=%d", len(s.sessions))
}
}