fix(config): align dm_scope values with backend routing
This commit is contained in:
parent
6bd8fec87a
commit
e6497e5324
8 changed files with 157 additions and 11 deletions
|
|
@ -44,8 +44,8 @@ func (r *RouteResolver) ResolveRoute(input RouteInput) ResolvedRoute {
|
|||
accountID := NormalizeAccountID(input.AccountID)
|
||||
peer := input.Peer
|
||||
|
||||
dmScope := DMScope(r.cfg.Session.DMScope)
|
||||
if dmScope == "" {
|
||||
dmScope, ok := NormalizeDMScope(r.cfg.Session.DMScope)
|
||||
if !ok {
|
||||
dmScope = DMScopeMain
|
||||
}
|
||||
identityLinks := r.cfg.Session.IdentityLinks
|
||||
|
|
|
|||
|
|
@ -15,6 +15,22 @@ const (
|
|||
DMScopePerAccountChannelPeer DMScope = "per-account-channel-peer"
|
||||
)
|
||||
|
||||
// NormalizeDMScope converts known legacy aliases to their canonical backend values.
|
||||
func NormalizeDMScope(scope string) (DMScope, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(scope)) {
|
||||
case string(DMScopeMain), "global":
|
||||
return DMScopeMain, true
|
||||
case string(DMScopePerPeer):
|
||||
return DMScopePerPeer, true
|
||||
case string(DMScopePerChannelPeer), "per-channel":
|
||||
return DMScopePerChannelPeer, true
|
||||
case string(DMScopePerAccountChannelPeer):
|
||||
return DMScopePerAccountChannelPeer, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// RoutePeer represents a chat peer with kind and ID.
|
||||
type RoutePeer struct {
|
||||
Kind string // "direct", "group", "channel"
|
||||
|
|
@ -56,8 +72,8 @@ func BuildAgentPeerSessionKey(params SessionKeyParams) string {
|
|||
}
|
||||
|
||||
if peerKind == "direct" {
|
||||
dmScope := params.DMScope
|
||||
if dmScope == "" {
|
||||
dmScope, ok := NormalizeDMScope(string(params.DMScope))
|
||||
if !ok {
|
||||
dmScope = DMScopeMain
|
||||
}
|
||||
peerID := strings.TrimSpace(peer.ID)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,32 @@ func TestBuildAgentMainSessionKey_Normalizes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDMScope(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want DMScope
|
||||
ok bool
|
||||
}{
|
||||
{name: "main", input: "main", want: DMScopeMain, ok: true},
|
||||
{name: "global alias", input: "global", want: DMScopeMain, ok: true},
|
||||
{name: "per peer", input: "per-peer", want: DMScopePerPeer, ok: true},
|
||||
{name: "per channel peer", input: "per-channel-peer", want: DMScopePerChannelPeer, ok: true},
|
||||
{name: "per channel alias", input: "per-channel", want: DMScopePerChannelPeer, ok: true},
|
||||
{name: "per account channel peer", input: "per-account-channel-peer", want: DMScopePerAccountChannelPeer, ok: true},
|
||||
{name: "unknown", input: "shared", want: "", ok: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, ok := NormalizeDMScope(tt.input)
|
||||
if got != tt.want || ok != tt.ok {
|
||||
t.Fatalf("NormalizeDMScope(%q) = (%q, %v), want (%q, %v)", tt.input, got, ok, tt.want, tt.ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentPeerSessionKey_DMScopeMain(t *testing.T) {
|
||||
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
||||
AgentID: "main",
|
||||
|
|
@ -31,6 +57,19 @@ func TestBuildAgentPeerSessionKey_DMScopeMain(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentPeerSessionKey_DMScopeGlobalAlias(t *testing.T) {
|
||||
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
||||
AgentID: "main",
|
||||
Channel: "telegram",
|
||||
Peer: &RoutePeer{Kind: "direct", ID: "user123"},
|
||||
DMScope: DMScope("global"),
|
||||
})
|
||||
want := "agent:main:main"
|
||||
if got != want {
|
||||
t.Errorf("DMScope(global) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentPeerSessionKey_DMScopePerPeer(t *testing.T) {
|
||||
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
||||
AgentID: "main",
|
||||
|
|
@ -57,6 +96,19 @@ func TestBuildAgentPeerSessionKey_DMScopePerChannelPeer(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentPeerSessionKey_DMScopePerChannelAlias(t *testing.T) {
|
||||
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
||||
AgentID: "main",
|
||||
Channel: "telegram",
|
||||
Peer: &RoutePeer{Kind: "direct", ID: "user123"},
|
||||
DMScope: DMScope("per-channel"),
|
||||
})
|
||||
want := "agent:main:telegram:direct:user123"
|
||||
if got != want {
|
||||
t.Errorf("DMScope(per-channel) = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAgentPeerSessionKey_DMScopePerAccountChannelPeer(t *testing.T) {
|
||||
got := BuildAgentPeerSessionKey(SessionKeyParams{
|
||||
AgentID: "main",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
"github.com/sipeed/picoclaw/pkg/routing"
|
||||
)
|
||||
|
||||
// registerConfigRoutes binds configuration management endpoints to the ServeMux.
|
||||
|
|
@ -250,6 +251,21 @@ func (h *Handler) handleTestCommandPatterns(w http.ResponseWriter, r *http.Reque
|
|||
func validateConfig(cfg *config.Config) []string {
|
||||
var errs []string
|
||||
|
||||
if rawScope := strings.TrimSpace(cfg.Session.DMScope); rawScope != "" {
|
||||
normalizedScope, ok := routing.NormalizeDMScope(rawScope)
|
||||
if !ok {
|
||||
errs = append(
|
||||
errs,
|
||||
fmt.Sprintf(
|
||||
"session.dm_scope %q is invalid; supported values: main, per-peer, per-channel-peer, per-account-channel-peer",
|
||||
cfg.Session.DMScope,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
cfg.Session.DMScope = string(normalizedScope)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate model_list entries
|
||||
if err := cfg.ValidateModelList(); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
|
|
|
|||
|
|
@ -143,6 +143,45 @@ func TestHandlePatchConfig_AllowsInvalidExecRegexPatternsWhenExecDisabled(t *tes
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateConfig_NormalizesLegacyDMScopeAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{name: "global", input: "global", want: "main"},
|
||||
{name: "per-channel", input: "per-channel", want: "per-channel-peer"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Session.DMScope = tt.input
|
||||
|
||||
errs := validateConfig(cfg)
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("validateConfig() errors = %v, want none", errs)
|
||||
}
|
||||
if cfg.Session.DMScope != tt.want {
|
||||
t.Fatalf("Session.DMScope = %q, want %q", cfg.Session.DMScope, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateConfig_RejectsUnknownDMScope(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Session.DMScope = "shared"
|
||||
|
||||
errs := validateConfig(cfg)
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("validateConfig() errors = nil, want dm_scope validation error")
|
||||
}
|
||||
if !bytes.Contains([]byte(errs[0]), []byte("session.dm_scope")) {
|
||||
t.Fatalf("validateConfig() first error = %q, want dm_scope validation", errs[0])
|
||||
}
|
||||
}
|
||||
|
||||
// setupPicoEnabledEnv creates a test environment with Pico channel enabled and
|
||||
// its token stored only in .security.yml (not in the JSON payload).
|
||||
func setupPicoEnabledEnv(t *testing.T) (string, func()) {
|
||||
|
|
|
|||
|
|
@ -40,11 +40,12 @@ export const DM_SCOPE_OPTIONS = [
|
|||
descDefault: "Separate context for each user in each channel.",
|
||||
},
|
||||
{
|
||||
value: "per-channel",
|
||||
labelKey: "pages.config.session_scope_per_channel",
|
||||
labelDefault: "Per Channel",
|
||||
descKey: "pages.config.session_scope_per_channel_desc",
|
||||
descDefault: "One shared context per channel.",
|
||||
value: "per-account-channel-peer",
|
||||
labelKey: "pages.config.session_scope_per_account_channel_peer",
|
||||
labelDefault: "Per Account + Channel + Peer",
|
||||
descKey: "pages.config.session_scope_per_account_channel_peer_desc",
|
||||
descDefault:
|
||||
"Separate context for each user on each channel account instance.",
|
||||
},
|
||||
{
|
||||
value: "per-peer",
|
||||
|
|
@ -54,7 +55,7 @@ export const DM_SCOPE_OPTIONS = [
|
|||
descDefault: "One context per user across channels.",
|
||||
},
|
||||
{
|
||||
value: "global",
|
||||
value: "main",
|
||||
labelKey: "pages.config.session_scope_global",
|
||||
labelDefault: "Global",
|
||||
descKey: "pages.config.session_scope_global_desc",
|
||||
|
|
@ -62,6 +63,11 @@ export const DM_SCOPE_OPTIONS = [
|
|||
},
|
||||
] as const
|
||||
|
||||
const DM_SCOPE_ALIASES: Record<string, string> = {
|
||||
global: "main",
|
||||
"per-channel": "per-channel-peer",
|
||||
}
|
||||
|
||||
export const EMPTY_FORM: CoreConfigForm = {
|
||||
workspace: "",
|
||||
restrictToWorkspace: true,
|
||||
|
|
@ -104,6 +110,19 @@ function asString(value: unknown): string {
|
|||
return typeof value === "string" ? value : ""
|
||||
}
|
||||
|
||||
function normalizeDmScope(value: unknown): string {
|
||||
const raw = asString(value).trim()
|
||||
if (!raw) {
|
||||
return EMPTY_FORM.dmScope
|
||||
}
|
||||
|
||||
const normalized = DM_SCOPE_ALIASES[raw] ?? raw
|
||||
|
||||
return DM_SCOPE_OPTIONS.some((scope) => scope.value === normalized)
|
||||
? normalized
|
||||
: raw
|
||||
}
|
||||
|
||||
function asBool(value: unknown): boolean {
|
||||
return value === true
|
||||
}
|
||||
|
|
@ -195,7 +214,7 @@ export function buildFormFromConfig(config: unknown): CoreConfigForm {
|
|||
defaults.summarize_token_percent,
|
||||
EMPTY_FORM.summarizeTokenPercent,
|
||||
),
|
||||
dmScope: asString(session.dm_scope) || EMPTY_FORM.dmScope,
|
||||
dmScope: normalizeDmScope(session.dm_scope),
|
||||
heartbeatEnabled:
|
||||
heartbeat.enabled === undefined
|
||||
? EMPTY_FORM.heartbeatEnabled
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"session_scope_hint": "How chat context is isolated across peers/channels.",
|
||||
"session_scope_per_channel_peer": "Per Channel + Peer",
|
||||
"session_scope_per_channel_peer_desc": "Separate context for each user in each channel.",
|
||||
"session_scope_per_account_channel_peer": "Per Account + Channel + Peer",
|
||||
"session_scope_per_account_channel_peer_desc": "Separate context for each user on each channel account instance.",
|
||||
"session_scope_per_channel": "Per Channel",
|
||||
"session_scope_per_channel_desc": "One shared context per channel.",
|
||||
"session_scope_per_peer": "Per Peer",
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"session_scope_hint": "定义不同用户/频道之间如何隔离会话上下文。",
|
||||
"session_scope_per_channel_peer": "按频道+用户隔离",
|
||||
"session_scope_per_channel_peer_desc": "同一频道内不同用户使用独立上下文。",
|
||||
"session_scope_per_account_channel_peer": "按账号+频道+用户隔离",
|
||||
"session_scope_per_account_channel_peer_desc": "不同频道账号实例中的不同用户使用独立上下文。",
|
||||
"session_scope_per_channel": "按频道隔离",
|
||||
"session_scope_per_channel_desc": "同一频道内共享一个上下文。",
|
||||
"session_scope_per_peer": "按用户隔离",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue