Files
Agent-System-Backend/internal/cmdbus/intents.go
T
weichen 9786dc1456 Wire plugin slash aliases and the intent declaration surface (G2-2)
Why: plan G2-2 requires plugin commands reachable via slash aliases
through the same bus and audit envelope (EXT-10②), and a natural-
language routing entry so '帮我写项目建议书' can reach the compile
flow. The agent-command lib now carries a validated Slash field
(lib PR #6, pin bumped to 4f583ca); the bus binds plugin-spec aliases
into the same slash table; intents are declared via the extension
registry and routed deterministically (normalized containment, longest
phrase first) to commands through the bus — the system.intent builtin
(/intent) is the chat-entrance dispatch point with a candidate list on
miss.

Evidence: TestPluginSlashAliasBindsAndDispatches — alias binds, case/
whitespace-normalized resolution, slash and button entrances hit the
same command handler with committed receipts; TestIntentDeclareMatch
Route — declare/validate, miss on unrelated text, normalized hit,
bus-dispatch through the chat entrance, longest-phrase priority;
server suite green with the dual-entry pair registered; full go test
./... 31/31 packages green.

Impact: cmdbus plugin-spec slash binding + IntentStore; extension
Registry.AddIntent + IntentSpec (SurfaceVersion 2.7.0-experimental);
pluginRegistry.AddIntent wires plugin-declared intents; server
builtin system.intent; command-pairs fixture extended.

Refs: plan G2-2 (line 366); EXT-10②; lib PR #6 (4f583ca).
2026-10-10 00:15:21 +00:00

97 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package cmdbuf 意图路由(G2-2:Intent 声明面与自然语言入口)。
// 插件经 extension.Registry.AddIntent 声明「短语 → 命令」映射;宿主把声明汇入
// 本表。匹配口径:文本归一(去空白/小写)后做包含匹配,最长短语优先——
// 无模型参与的确定性第一档;路由终点仍是命令总线(同审计信封与 HITL 收口)。
package cmdbus
import (
"fmt"
"sort"
"strings"
"sync"
)
// Intent 一条意图声明:短语命中后分发到 CommandID(参数模板随声明携带)。
type Intent struct {
ID string `json:"id"` // 形如 <domain>.<intent>(如 rdm.write-proposal)
Phrases []string `json:"phrases"` // 触发短语(归一后包含匹配)
Command string `json:"command"` // 目标命令 id(须已注册)
Params map[string]any `json:"params,omitempty"` // 参数模板(可被调用方覆盖)
Description string `json:"description,omitempty"`
Plugin string `json:"plugin"` // 声明方(审计归因)
}
// IntentStore 意图声明表(进程内;插件注册期写入,运行期只读)。
type IntentStore struct {
mu sync.RWMutex
byID map[string]Intent
phrases []string // 归一短语(排序后长词优先匹配)
}
// NewIntentStore 建表。
func NewIntentStore() *IntentStore {
return &IntentStore{byID: map[string]Intent{}}
}
// normalize 意图匹配归一:小写、压缩空白。
func normalize(s string) string {
return strings.Join(strings.Fields(strings.ToLower(s)), " ")
}
// Declare 登记意图(同 id 覆盖;短语与命令必填——fail-closed)。
func (s *IntentStore) Declare(in Intent) error {
if in.ID == "" || in.Command == "" || len(in.Phrases) == 0 {
return fmt.Errorf("cmdbus: intent %q 声明不完整(id/phrases/command 必填)", in.ID)
}
s.mu.Lock()
defer s.mu.Unlock()
s.byID[in.ID] = in
s.rebuild()
return nil
}
func (s *IntentStore) rebuild() {
s.phrases = s.phrases[:0]
for _, in := range s.byID {
for _, p := range in.Phrases {
s.phrases = append(s.phrases, normalize(p))
}
}
// 长短语优先:更具体的意图先命中。
sort.Slice(s.phrases, func(i, j int) bool { return len(s.phrases[i]) > len(s.phrases[j]) })
}
// Match 自然语言匹配:返回命中的意图(ok=false=未命中)。
func (s *IntentStore) Match(text string) (Intent, bool) {
n := normalize(text)
if n == "" {
return Intent{}, false
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, ph := range s.phrases {
if ph != "" && strings.Contains(n, ph) {
for _, in := range s.byID {
for _, p := range in.Phrases {
if normalize(p) == ph {
return in, true
}
}
}
}
}
return Intent{}, false
}
// List 全部意图声明(清单面)。
func (s *IntentStore) List() []Intent {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]Intent, 0, len(s.byID))
for _, in := range s.byID {
out = append(out, in)
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}