refactor: 将客服后端重构为宿主可嵌入模块

- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
t
2026-08-28 22:23:13 +08:00
parent 6845c728f8
commit 18c9354095
377 changed files with 13199 additions and 22881 deletions
+32
View File
@@ -1,6 +1,8 @@
package runtime
import (
"errors"
"strings"
"testing"
"time"
@@ -85,6 +87,36 @@ func TestResolveReplyTimeout(t *testing.T) {
}
}
func TestAIReplyFailureTextShowsSafeActionableErrors(t *testing.T) {
tests := []struct {
name string
err error
contains string
}{
{name: "request id", err: errors.New("AI 请求标识未设置"), contains: "AI 请求标识无效"},
{name: "balance", err: errors.New("insufficient_ai_balance: AI 额度不足"), contains: "AI 额度不足"},
{name: "key", err: errors.New("invalid_ai_key"), contains: "AI Key 无效或已撤销"},
{name: "model", err: errors.New("ai_gateway_not_configured"), contains: "AI 模型尚未配置"},
{name: "timeout", err: errors.New("context deadline exceeded"), contains: "AI 请求超时"},
{name: "gateway internal", err: errors.New("internal_error: 网关内部异常 (request_id: req-qwen-123)"), contains: "排查编号:req-qwen-123"},
{name: "upstream model", err: errors.New(`ai_upstream_failed: deepseek returned 400: {"message":"Model Not Exist"}`), contains: "模型不存在或暂不可用"},
{name: "upstream key", err: errors.New("ai_upstream_failed: Authentication Fails, invalid api key"), contains: "API Key 无效或无权限"},
{name: "upstream unknown", err: errors.New("ai_upstream_failed: provider returned 502"), contains: "上游模型服务返回错误"},
{name: "wrapped qwen parameter", err: errors.New(`failed to generate: status code: 502, message: qwen 返回 400: {"code":"InvalidParameter","message":"The parameter temperature is invalid"}`), contains: "千问请求参数不兼容"},
{name: "wrapped qwen tool unsupported", err: errors.New(`status code: 502, message: qwen 返回 400: {"code":"InvalidParameter","message":"The model does not support tools"}`), contains: "不支持客服工具调用"},
{name: "qwen arrearage", err: errors.New(`qwen 返回 400: {"code":"Arrearage","message":"Access denied due to owing balance"}`), contains: "额度不足或已欠费"},
{name: "qwen unknown", err: errors.New(`qwen 返回 500: {"code":"InternalError","message":"Temporary upstream failure"}`), contains: "千问服务返回错误"},
{name: "unknown", err: errors.New("database password leaked"), contains: aiReplyFailedReply},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := aiReplyFailureText(test.err); !strings.Contains(got, test.contains) {
t.Fatalf("aiReplyFailureText() = %q, want it to contain %q", got, test.contains)
}
})
}
}
func TestResolveInterruptPrompt(t *testing.T) {
summary := &applicationruntime.RunResult{
Interrupts: []applicationruntime.InterruptContextSummary{