This commit is contained in:
pphua committed 2026-08-24 14:14:15 +08:00
1 parent e67ff26e90
commit 58f23a2773
61 files changed
+11611 -443

No files matched your search

+90
View File
@@ -1,12 +1,16 @@
package server
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/artex/guard"
"github.com/Autumn-27/artex/intercept"
"github.com/Autumn-27/norma/llm"
)
// chatGuard returns a guard wired with the manager's interceptor, used for chat
@@ -15,6 +19,58 @@ func (s *Server) chatGuard() *guard.Guard {
return guard.NewWithInterceptor(s.m.interceptor)
}
// wireInterceptReviewer installs the LLM fallback judge into the interceptor. The
// judge runs only on tool calls that matched no rule (see intercept.Judge). It
// resolves the configured judge profile (0 → active/default), builds a provider,
// runs a one-shot single-line classification, and parses ALLOW/ASK/DENY.
func (s *Server) wireInterceptReviewer() {
s.m.interceptor.SetReviewer(func(ctx context.Context, profileID int64, prompt, tool, command string) (intercept.Decision, error) {
if profileID == 0 {
if p, err := s.m.pg.ActiveProfile(); err == nil && p != nil {
profileID = p.ID
}
}
if profileID == 0 {
return intercept.Decision{}, fmt.Errorf("未配置可用的裁判模型")
}
prov, _, ok := s.providerForProfile(profileID)
if !ok {
return intercept.Decision{}, fmt.Errorf("裁判模型 profile %d 不可用", profileID)
}
user := fmt.Sprintf("Tool: %s\nArguments:\n%s", tool, command)
text, err := streamCollectText(ctx, prov, prompt, user)
if err != nil {
return intercept.Decision{}, err
}
v := intercept.ParseVerdict(text)
return intercept.Decision{Action: v.Action, Message: v.Reason}, nil
})
}
// streamCollectText runs a single non-streaming-style completion (thinking off,
// low temperature, tiny output) and returns the concatenated text. Used by the
// LLM fallback judge, whose reply is one short line (ALLOW/ASK:.../DENY:...).
func streamCollectText(ctx context.Context, prov llm.Provider, system, user string) (string, error) {
temp := 0.0
req := llm.CompletionRequest{
System: []string{system},
Messages: []llm.Message{llm.UserText(user)},
MaxTokens: 128,
Temperature: &temp,
Thinking: "disabled",
}
var sb strings.Builder
for ev, err := range prov.Stream(ctx, req) {
if err != nil {
return "", err
}
if ev.Type == llm.SETextDelta {
sb.WriteString(ev.Text)
}
}
return sb.String(), nil
}
// --- intercept rule CRUD ---
func (s *Server) interceptListRules(w http.ResponseWriter, r *http.Request) {
@@ -261,6 +317,40 @@ func (s *Server) interceptSetToolConfig(w http.ResponseWriter, r *http.Request)
writeJSON(w, 200, map[string]any{"ok": true})
}
// --- LLM fallback judge config (全局模型兜底) ---
// interceptGetJudgeConfig returns the resolved judge configuration. Prompt is the
// effective prompt (built-in template when unset), so the UI can prefill it.
func (s *Server) interceptGetJudgeConfig(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, s.m.interceptor.GetJudgeConfig())
}
// interceptSetJudgeConfig persists the judge configuration.
func (s *Server) interceptSetJudgeConfig(w http.ResponseWriter, r *http.Request) {
var req intercept.JudgeConfig
if err := decode(r, &req); err != nil {
writeErr(w, 400, err.Error())
return
}
switch req.FailAction {
case "allow", "ask", "deny":
default:
writeErr(w, 400, "fail_action 必须是 allow、ask 或 deny")
return
}
switch req.AskTimeoutAction {
case "allow", "deny":
default:
writeErr(w, 400, "ask_timeout_action 必须是 allow 或 deny")
return
}
if err := s.m.interceptor.SetJudgeConfig(req); err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"ok": true})
}
// --- helpers ---
type interceptRuleReq struct {