refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -6,14 +6,12 @@ type Assembler struct{}
|
||||
|
||||
type AssemblerInput struct {
|
||||
AgentInstruction string
|
||||
SkillInstruction string
|
||||
ToolAppendices []string
|
||||
}
|
||||
|
||||
type AssemblySummary struct {
|
||||
SectionTitles []string
|
||||
HasAgentRule bool
|
||||
HasSkillRule bool
|
||||
HasToolRule bool
|
||||
}
|
||||
|
||||
@@ -38,11 +36,6 @@ func (a *Assembler) Assemble(input AssemblerInput) AssemblyResult {
|
||||
summary.HasAgentRule = true
|
||||
summary.SectionTitles = append(summary.SectionTitles, "Agent 规则")
|
||||
}
|
||||
if skillInstruction := strings.TrimSpace(input.SkillInstruction); skillInstruction != "" {
|
||||
parts = append(parts, buildInstructionSection("当前技能上下文", skillInstruction))
|
||||
summary.HasSkillRule = true
|
||||
summary.SectionTitles = append(summary.SectionTitles, "当前技能上下文")
|
||||
}
|
||||
if appendix := buildToolAppendix(input.ToolAppendices); appendix != "" {
|
||||
parts = append(parts, buildInstructionSection("工具补充规则", appendix))
|
||||
summary.HasToolRule = true
|
||||
|
||||
@@ -8,19 +8,15 @@ import (
|
||||
func TestAssemblerRespectsProvidedSources(t *testing.T) {
|
||||
result := NewAssembler().Assemble(AssemblerInput{
|
||||
AgentInstruction: "agent-rule",
|
||||
SkillInstruction: "skill-rule",
|
||||
ToolAppendices: []string{"tool-rule-1", "tool-rule-2"},
|
||||
})
|
||||
if !strings.Contains(result.Text, "Agent 规则:\nagent-rule") {
|
||||
t.Fatalf("missing agent instruction: %s", result.Text)
|
||||
}
|
||||
if !strings.Contains(result.Text, "当前技能上下文:\nskill-rule") {
|
||||
t.Fatalf("missing skill instruction: %s", result.Text)
|
||||
}
|
||||
if !strings.Contains(result.Text, "工具补充规则:\ntool-rule-1") {
|
||||
t.Fatalf("missing tool appendix: %s", result.Text)
|
||||
}
|
||||
if !result.Summary.HasAgentRule || !result.Summary.HasSkillRule || !result.Summary.HasToolRule {
|
||||
if !result.Summary.HasAgentRule || !result.Summary.HasToolRule {
|
||||
t.Fatalf("unexpected summary: %#v", result.Summary)
|
||||
}
|
||||
}
|
||||
@@ -30,7 +26,7 @@ func TestAssemblerReturnsEmptyTextWhenInputIsEmpty(t *testing.T) {
|
||||
if result.Text != "" {
|
||||
t.Fatalf("expected empty assembled text, got: %s", result.Text)
|
||||
}
|
||||
if len(result.Summary.SectionTitles) != 0 || result.Summary.HasAgentRule || result.Summary.HasSkillRule || result.Summary.HasToolRule {
|
||||
if len(result.Summary.SectionTitles) != 0 || result.Summary.HasAgentRule || result.Summary.HasToolRule {
|
||||
t.Fatalf("expected empty summary, got %#v", result.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package instruction
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
runtimetooling "code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/tooling"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
|
||||
)
|
||||
|
||||
func BuildSelectedSkillActivationInstruction(skill *models.SkillDefinition) string {
|
||||
if skill == nil {
|
||||
return ""
|
||||
}
|
||||
lines := []string{
|
||||
"当前命中的专项技能:",
|
||||
fmt.Sprintf("- id: %d", skill.ID),
|
||||
fmt.Sprintf("- name: %s", strings.TrimSpace(skill.Name)),
|
||||
}
|
||||
if desc := strings.TrimSpace(skill.Description); desc != "" {
|
||||
lines = append(lines, fmt.Sprintf("- description: %s", desc))
|
||||
}
|
||||
lines = append(lines, "", "执行要求:", "- 本轮优先处理该技能范围内的问题。", fmt.Sprintf("- 需要专项处理细节时,优先调用 %s 工具加载该技能说明后再继续。", toolx.BuiltinSkill.Name), "- 如果关键信息不足,先向用户追问。", "- 不得调用当前技能未授权的工具。")
|
||||
return strings.TrimSpace(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func BuildSelectedSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string {
|
||||
return BuildSkillDocument(skill, toolDefinitions)
|
||||
}
|
||||
|
||||
func BuildSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string {
|
||||
if skill == nil {
|
||||
return ""
|
||||
}
|
||||
lines := []string{
|
||||
"当前命中的专项技能:",
|
||||
fmt.Sprintf("- id: %d", skill.ID),
|
||||
fmt.Sprintf("- name: %s", strings.TrimSpace(skill.Name)),
|
||||
}
|
||||
if desc := strings.TrimSpace(skill.Description); desc != "" {
|
||||
lines = append(lines, fmt.Sprintf("- description: %s", desc))
|
||||
}
|
||||
if content := strings.TrimSpace(skill.Instruction); content != "" {
|
||||
lines = append(lines, "", "技能说明:", content)
|
||||
}
|
||||
if examples := parseJSONStringArray(skill.Examples); len(examples) > 0 {
|
||||
lines = append(lines, "", "典型示例问法:")
|
||||
for _, item := range examples {
|
||||
lines = append(lines, "- "+item)
|
||||
}
|
||||
}
|
||||
if len(toolDefinitions) > 0 {
|
||||
lines = append(lines, "", "当前技能允许使用的工具:")
|
||||
for _, item := range toolDefinitions {
|
||||
if strings.TrimSpace(item.ToolCode) == "" {
|
||||
continue
|
||||
}
|
||||
line := "- " + strings.TrimSpace(item.ToolCode)
|
||||
if title := strings.TrimSpace(item.Title); title != "" {
|
||||
line += " | " + title
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
lines = append(lines, "", "执行要求:", "- 优先遵循该技能说明完成任务。", "- 如果关键信息不足,先向用户追问。", "- 不得调用当前技能未授权的工具。")
|
||||
return strings.TrimSpace(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func parseJSONStringArray(raw string) []string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var ret []string
|
||||
if err := json.Unmarshal([]byte(raw), &ret); err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(ret))
|
||||
for _, item := range ret {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package instruction
|
||||
|
||||
import (
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/tooling"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
|
||||
)
|
||||
|
||||
type ToolAppendixProvider struct{}
|
||||
|
||||
func NewToolAppendixProvider() *ToolAppendixProvider {
|
||||
return &ToolAppendixProvider{}
|
||||
}
|
||||
|
||||
type SkillInstructionProvider struct{}
|
||||
|
||||
func NewSkillInstructionProvider() *SkillInstructionProvider {
|
||||
return &SkillInstructionProvider{}
|
||||
}
|
||||
|
||||
func (p *SkillInstructionProvider) Resolve(selectedSkill *models.SkillDefinition) string {
|
||||
return BuildSelectedSkillActivationInstruction(selectedSkill)
|
||||
}
|
||||
|
||||
func (p *ToolAppendixProvider) Build(toolDefinitions []tooling.MCPToolDefinition, extraToolCodes map[string]string) []string {
|
||||
appendixParts := make([]string, 0, 1)
|
||||
toolCodes := make([]string, 0, len(toolDefinitions)+len(extraToolCodes))
|
||||
for _, item := range toolDefinitions {
|
||||
toolCodes = append(toolCodes, item.ToolCode)
|
||||
}
|
||||
for _, item := range extraToolCodes {
|
||||
toolCodes = append(toolCodes, item)
|
||||
}
|
||||
appendixParts = append(appendixParts, toolx.BuildToolAppendicesForCodes(len(toolDefinitions) > 0, toolCodes)...)
|
||||
return appendixParts
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package instruction
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
runtimetooling "code.tczkiot.com/wlw/ai-agent/internal/ai/runtime/tooling"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
assembler *Assembler
|
||||
skillInstructionProvider *SkillInstructionProvider
|
||||
toolAppendixProvider *ToolAppendixProvider
|
||||
}
|
||||
|
||||
func NewService(
|
||||
assembler *Assembler,
|
||||
skillProvider *SkillInstructionProvider,
|
||||
toolProvider *ToolAppendixProvider,
|
||||
) *Service {
|
||||
if assembler == nil {
|
||||
assembler = NewAssembler()
|
||||
}
|
||||
if skillProvider == nil {
|
||||
skillProvider = NewSkillInstructionProvider()
|
||||
}
|
||||
if toolProvider == nil {
|
||||
toolProvider = NewToolAppendixProvider()
|
||||
}
|
||||
return &Service{
|
||||
assembler: assembler,
|
||||
skillInstructionProvider: skillProvider,
|
||||
toolAppendixProvider: toolProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Build(
|
||||
aiAgent models.AIAgent,
|
||||
selectedSkill *models.SkillDefinition,
|
||||
toolDefinitions []runtimetooling.MCPToolDefinition,
|
||||
extraToolCodes map[string]string,
|
||||
) AssemblyResult {
|
||||
skillInstruction := ""
|
||||
toolAppendices := make([]string, 0)
|
||||
if s != nil && s.skillInstructionProvider != nil {
|
||||
skillInstruction = s.skillInstructionProvider.Resolve(selectedSkill)
|
||||
}
|
||||
if s != nil && s.toolAppendixProvider != nil {
|
||||
toolAppendices = s.toolAppendixProvider.Build(toolDefinitions, extraToolCodes)
|
||||
}
|
||||
assembler := NewAssembler()
|
||||
if s != nil && s.assembler != nil {
|
||||
assembler = s.assembler
|
||||
}
|
||||
return assembler.Assemble(AssemblerInput{
|
||||
AgentInstruction: strings.TrimSpace(aiAgent.SystemPrompt),
|
||||
SkillInstruction: skillInstruction,
|
||||
ToolAppendices: toolAppendices,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user