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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user