refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
package tooling
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var toolNameSanitizer = regexp.MustCompile(`[^a-zA-Z0-9_]`)
|
||||
|
||||
type MCPToolDefinition struct {
|
||||
ToolCode string
|
||||
ServerCode string
|
||||
ToolName string
|
||||
ModelName string
|
||||
Title string
|
||||
Description string
|
||||
FixedArgs map[string]string
|
||||
}
|
||||
|
||||
func BuildModelToolName(definition MCPToolDefinition) string {
|
||||
if strings.TrimSpace(definition.ModelName) != "" {
|
||||
return strings.TrimSpace(definition.ModelName)
|
||||
}
|
||||
base := "mcp_" + strings.TrimSpace(definition.ServerCode) + "_" + strings.TrimSpace(definition.ToolName)
|
||||
base = toolNameSanitizer.ReplaceAllString(base, "_")
|
||||
base = strings.Trim(base, "_")
|
||||
if base == "" {
|
||||
base = "mcp_tool"
|
||||
}
|
||||
checksum := crc32.ChecksumIEEE([]byte(definition.ToolCode))
|
||||
return fmt.Sprintf("%s_%08x", base, checksum)
|
||||
}
|
||||
@@ -9,9 +9,9 @@ type ToolResult struct {
|
||||
Handled bool `json:"handled"`
|
||||
Terminal bool `json:"terminal"`
|
||||
Action string `json:"action"`
|
||||
ReplyText string `json:"replyText,omitempty"`
|
||||
ReplySent bool `json:"replySent,omitempty"`
|
||||
ShouldRetry bool `json:"shouldRetry"`
|
||||
ReplyText string `json:"reply_text,omitempty"`
|
||||
ReplySent bool `json:"reply_sent,omitempty"`
|
||||
ShouldRetry bool `json:"should_retry"`
|
||||
}
|
||||
|
||||
func MarshalToolResult(result ToolResult) string {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
package tooling
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/mcps"
|
||||
)
|
||||
|
||||
const (
|
||||
maxToolResultSummaryChars = 4000
|
||||
maxToolResultSegments = 12
|
||||
)
|
||||
|
||||
var reductionInfoPattern = regexp.MustCompile(`\[tool result reduced: original_length=(\d+), kept_length=(\d+)\]`)
|
||||
|
||||
type ReductionInfo struct {
|
||||
Reduced bool
|
||||
OriginalChars int
|
||||
KeptChars int
|
||||
}
|
||||
|
||||
// BuildReducedToolResultSummary returns a bounded text summary for MCP tool results.
|
||||
// It keeps the main payload visible to the model while preventing a single large tool
|
||||
// response from exhausting too much context.
|
||||
func BuildReducedToolResultSummary(result *mcps.ToolCallResult) string {
|
||||
if result == nil {
|
||||
return ""
|
||||
}
|
||||
segments := collectToolResultSegments(result)
|
||||
if len(segments) == 0 {
|
||||
return ""
|
||||
}
|
||||
text := strings.TrimSpace(strings.Join(segments, "\n"))
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(text)
|
||||
if len(runes) <= maxToolResultSummaryChars {
|
||||
return text
|
||||
}
|
||||
truncated := strings.TrimSpace(string(runes[:maxToolResultSummaryChars]))
|
||||
return fmt.Sprintf("%s\n\n[tool result reduced: original_length=%d, kept_length=%d]", truncated, len(runes), maxToolResultSummaryChars)
|
||||
}
|
||||
|
||||
func ParseReductionInfo(summary string) ReductionInfo {
|
||||
matches := reductionInfoPattern.FindStringSubmatch(strings.TrimSpace(summary))
|
||||
if len(matches) != 3 {
|
||||
return ReductionInfo{}
|
||||
}
|
||||
originalChars, err1 := strconv.Atoi(matches[1])
|
||||
keptChars, err2 := strconv.Atoi(matches[2])
|
||||
if err1 != nil || err2 != nil {
|
||||
return ReductionInfo{}
|
||||
}
|
||||
return ReductionInfo{
|
||||
Reduced: true,
|
||||
OriginalChars: originalChars,
|
||||
KeptChars: keptChars,
|
||||
}
|
||||
}
|
||||
|
||||
func collectToolResultSegments(result *mcps.ToolCallResult) []string {
|
||||
segments := make([]string, 0, len(result.Content)+2)
|
||||
if result.IsError {
|
||||
segments = append(segments, "tool returned an error")
|
||||
}
|
||||
if result.StructuredContent != nil {
|
||||
if data, err := json.Marshal(result.StructuredContent); err == nil {
|
||||
segments = appendNonBlankSegment(segments, string(data))
|
||||
}
|
||||
}
|
||||
for _, item := range result.Content {
|
||||
if len(segments) >= maxToolResultSegments {
|
||||
segments = append(segments, "[tool result reduced: remaining segments omitted]")
|
||||
break
|
||||
}
|
||||
switch item.Type {
|
||||
case "text":
|
||||
segments = appendNonBlankSegment(segments, item.Text)
|
||||
default:
|
||||
if item.Data == nil {
|
||||
continue
|
||||
}
|
||||
if data, err := json.Marshal(item.Data); err == nil {
|
||||
segments = appendNonBlankSegment(segments, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
func appendNonBlankSegment(input []string, value string) []string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return input
|
||||
}
|
||||
key := canonicalToolResultSegment(value)
|
||||
for _, existing := range input {
|
||||
if canonicalToolResultSegment(existing) == key {
|
||||
return input
|
||||
}
|
||||
}
|
||||
return append(input, value)
|
||||
}
|
||||
|
||||
func canonicalToolResultSegment(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
var payload any
|
||||
if err := json.Unmarshal([]byte(value), &payload); err != nil {
|
||||
return value
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return value
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package tooling
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/ai/mcps"
|
||||
)
|
||||
|
||||
func TestBuildReducedToolResultSummaryDeduplicatesStructuredAndTextContent(t *testing.T) {
|
||||
result := &mcps.ToolCallResult{
|
||||
StructuredContent: map[string]any{
|
||||
"timestamp": "2026-07-28 11:51:52",
|
||||
"timezone": "Local",
|
||||
},
|
||||
Content: []mcps.ToolResultContent{{
|
||||
Type: "text",
|
||||
Text: `{"timezone":"Local","timestamp":"2026-07-28 11:51:52"}`,
|
||||
}},
|
||||
}
|
||||
|
||||
summary := BuildReducedToolResultSummary(result)
|
||||
if strings.Count(summary, "timestamp") != 1 {
|
||||
t.Fatalf("duplicate MCP result was not removed: %q", summary)
|
||||
}
|
||||
if summary != `{"timestamp":"2026-07-28 11:51:52","timezone":"Local"}` {
|
||||
t.Fatalf("unexpected reduced result: %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildReducedToolResultSummaryKeepsDistinctSegments(t *testing.T) {
|
||||
result := &mcps.ToolCallResult{
|
||||
StructuredContent: map[string]any{"status": "ok"},
|
||||
Content: []mcps.ToolResultContent{{
|
||||
Type: "text",
|
||||
Text: "additional context",
|
||||
}},
|
||||
}
|
||||
|
||||
summary := BuildReducedToolResultSummary(result)
|
||||
if !strings.Contains(summary, `{"status":"ok"}`) || !strings.Contains(summary, "additional context") {
|
||||
t.Fatalf("distinct MCP result segments were lost: %q", summary)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user