Why: the user ruled out self-registration (2026-09-30): accounts are created only by admins who set an initial password; first login (and first login after any admin reset) must force a password change. Evidence: - migration v10 auth_accounts (users password/must_change/is_admin + sessions table; upgrade test walks v1<->v10 both directions). - argon2id hash/verify with PHC encoding; store tests cover hash roundtrip, duplicate-owner conflict, must-change lifecycle, session keep/except semantics. - e2e TestAdminAccountFlow: static login page reachable without auth, wrong password 401, login sets HttpOnly cookie + mustChangePassword, admin creates user (409 on duplicate), non-admin create 403, forced change clears flag and old password dies, admin reset revokes sessions and re-arms forced change, API-key channel untouched, 5-fail backoff 429, logout kills session. Full go test ./... green. Impact: middleware now accepts Bearer key OR session cookie and exempts static + /api/auth/login (also fixes the latent case where the login page itself could 401). Embedded no-auth mode unchanged. Refs: proposal 2026-09-30-ui-redesign-and-auth.md section 5 (admin-issued variant, user decision 2026-09-30)
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
// Package intentgate 接入 agent-intent(生态仓):插件意图 overlay → 注册表 →
|
||
// HITL 自由输入路由的确定性识别(设计 4.5 三分支之「命中其他意图→转轨」)。
|
||
// 未配置/未装载意图 = 行为回退(T0:直接按澄清回填),识别失败不阻断。
|
||
package intentgate
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"sync"
|
||
|
||
agentintent "agent-intent/intent"
|
||
)
|
||
|
||
type Gate struct {
|
||
mu sync.RWMutex
|
||
reg *agentintent.Registry
|
||
rec *agentintent.FunnelRecognizer
|
||
specs int
|
||
}
|
||
|
||
func New() *Gate {
|
||
return &Gate{rec: agentintent.NewRecognizer(agentintent.Options{}, nil, nil)}
|
||
}
|
||
|
||
// Rebuild 用插件意图 overlay 重建注册表;失败保留上一快照(fail-closed)并返回错误。
|
||
func (g *Gate) Rebuild(ctx context.Context, overlay []json.RawMessage) error {
|
||
if len(overlay) == 0 {
|
||
g.mu.Lock()
|
||
g.reg, g.specs = nil, 0
|
||
g.mu.Unlock()
|
||
return nil
|
||
}
|
||
specs := make([]agentintent.IntentSpec, 0, len(overlay))
|
||
for i, raw := range overlay {
|
||
var s agentintent.IntentSpec
|
||
if err := json.Unmarshal(raw, &s); err != nil {
|
||
return fmt.Errorf("intentgate: spec[%d]: %w", i, err)
|
||
}
|
||
specs = append(specs, s)
|
||
}
|
||
b := agentintent.NewRegistryBuilder(agentintent.Options{TopN: 3})
|
||
if err := b.Register(specs...); err != nil {
|
||
return fmt.Errorf("intentgate: register: %w", err)
|
||
}
|
||
reg, err := b.Build()
|
||
if err != nil {
|
||
return fmt.Errorf("intentgate: build: %w", err)
|
||
}
|
||
g.mu.Lock()
|
||
g.reg, g.specs = reg, len(specs)
|
||
g.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
// Match 识别自由输入;返回 (branch, topID)。
|
||
// branch:matched | ambiguous | fallback | ""(未配置/系统级错误 → 回退通道)。
|
||
func (g *Gate) Match(ctx context.Context, input string) (string, string) {
|
||
g.mu.RLock()
|
||
reg, rec := g.reg, g.rec
|
||
g.mu.RUnlock()
|
||
if reg == nil {
|
||
return "", ""
|
||
}
|
||
res, err := rec.Recognize(ctx, input, reg.Snapshot())
|
||
if err != nil {
|
||
return "", ""
|
||
}
|
||
topID := ""
|
||
if res.Top != nil {
|
||
topID = res.Top.ID
|
||
}
|
||
switch res.Branch {
|
||
case agentintent.BranchMatched:
|
||
return "matched", topID
|
||
case agentintent.BranchAmbiguous:
|
||
return "ambiguous", topID
|
||
default:
|
||
return "fallback", topID
|
||
}
|
||
}
|
||
|
||
// Enabled 报告是否装载了意图(诊断/设置展示用)。
|
||
func (g *Gate) Enabled() bool {
|
||
g.mu.RLock()
|
||
defer g.mu.RUnlock()
|
||
return g.reg != nil && g.specs > 0
|
||
}
|
||
|
||
// SpecCount 返回已装载意图数。
|
||
func (g *Gate) SpecCount() int {
|
||
g.mu.RLock()
|
||
defer g.mu.RUnlock()
|
||
return g.specs
|
||
}
|