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).
97 lines
3.0 KiB
Go
97 lines
3.0 KiB
Go
// 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
|
||
}
|