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
+126 -27
View File
@@ -1,13 +1,138 @@
package config
import (
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"encoding/json"
"fmt"
"strconv"
"strings"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"github.com/spf13/viper"
)
const SettingsPrefix = "ai_agent_"
func FromSettings(values map[string]string) (*Config, error) {
cfg := defaultConfig()
setString(values, SettingsPrefix+"language", &cfg.Language)
setString(values, SettingsPrefix+"storage_default", (*string)(&cfg.Storage.Default))
if err := setInt64(values, SettingsPrefix+"storage_max_upload_size_mb", &cfg.Storage.MaxUploadSizeMB); err != nil {
return nil, err
}
setString(values, SettingsPrefix+"storage_local_root", &cfg.Storage.Local.Root)
setString(values, SettingsPrefix+"storage_local_base_url", &cfg.Storage.Local.BaseURL)
setString(values, SettingsPrefix+"storage_oss_endpoint", &cfg.Storage.OSS.Endpoint)
setString(values, SettingsPrefix+"storage_oss_bucket", &cfg.Storage.OSS.Bucket)
setString(values, SettingsPrefix+"storage_oss_access_key_id", &cfg.Storage.OSS.AccessKeyID)
setString(values, SettingsPrefix+"storage_oss_access_key_secret", &cfg.Storage.OSS.AccessKeySecret)
setString(values, SettingsPrefix+"storage_oss_base_url", &cfg.Storage.OSS.BaseURL)
if err := setBool(values, SettingsPrefix+"storage_oss_private", &cfg.Storage.OSS.Private); err != nil {
return nil, err
}
if err := setInt(values, SettingsPrefix+"storage_oss_signed_url_expire", &cfg.Storage.OSS.SignedURLExpire); err != nil {
return nil, err
}
setString(values, "ai_vector_path", &cfg.VectorDB.Path)
if err := setBool(values, SettingsPrefix+"wxwork_enabled", &cfg.WxWork.Enabled); err != nil {
return nil, err
}
setString(values, SettingsPrefix+"wxwork_corp_id", &cfg.WxWork.CorpID)
setString(values, SettingsPrefix+"wxwork_corp_secret", &cfg.WxWork.CorpSecret)
setString(values, SettingsPrefix+"wxwork_agent_id", &cfg.WxWork.AgentID)
setString(values, SettingsPrefix+"wxwork_rsa_private_key", &cfg.WxWork.RSAPrivateKey)
setString(values, SettingsPrefix+"wxwork_token", &cfg.WxWork.Token)
setString(values, SettingsPrefix+"wxwork_encoding_aes_key", &cfg.WxWork.EncodingAESKey)
if err := setBool(values, SettingsPrefix+"wxwork_notify_enabled", &cfg.WxWork.Notify.Enabled); err != nil {
return nil, err
}
if err := setJSON(values, SettingsPrefix+"wxwork_notify_to_users", &cfg.WxWork.Notify.ToUsers); err != nil {
return nil, err
}
if err := setBool(values, SettingsPrefix+"wxwork_notify_safe", &cfg.WxWork.Notify.Safe); err != nil {
return nil, err
}
if err := setBool(values, SettingsPrefix+"wxwork_notify_dedupe", &cfg.WxWork.Notify.EnableDuplicateCheck); err != nil {
return nil, err
}
if err := setInt(values, SettingsPrefix+"wxwork_notify_dedupe_interval", &cfg.WxWork.Notify.DuplicateCheckInterval); err != nil {
return nil, err
}
return cfg, nil
}
func defaultConfig() *Config {
return &Config{
Language: "zh-CN",
Server: ServerConfig{Port: 8080},
Storage: StorageConfig{
Default: enums.AssetProviderLocal, MaxUploadSizeMB: 20,
Local: LocalStorageConfig{Root: "data/storage", BaseURL: "/storage"},
OSS: OSSStorageConfig{SignedURLExpire: 600},
},
VectorDB: VectorDBConfig{Path: "data/agent/vectors.db"},
WxWork: WxWorkConfig{Notify: WxWorkNotifyConfig{
EnableDuplicateCheck: true, DuplicateCheckInterval: 1800,
}},
}
}
func setString(values map[string]string, key string, target *string) {
if value, ok := values[key]; ok {
*target = strings.TrimSpace(value)
}
}
func setInt(values map[string]string, key string, target *int) error {
value, ok := values[key]
if !ok || strings.TrimSpace(value) == "" {
return nil
}
parsed, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return fmt.Errorf("invalid setting %s: %w", key, err)
}
*target = parsed
return nil
}
func setInt64(values map[string]string, key string, target *int64) error {
value, ok := values[key]
if !ok || strings.TrimSpace(value) == "" {
return nil
}
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return fmt.Errorf("invalid setting %s: %w", key, err)
}
*target = parsed
return nil
}
func setBool(values map[string]string, key string, target *bool) error {
value, ok := values[key]
if !ok || strings.TrimSpace(value) == "" {
return nil
}
parsed, err := strconv.ParseBool(strings.TrimSpace(value))
if err != nil {
return fmt.Errorf("invalid setting %s: %w", key, err)
}
*target = parsed
return nil
}
func setJSON(values map[string]string, key string, target any) error {
value, ok := values[key]
if !ok || strings.TrimSpace(value) == "" {
return nil
}
if err := json.Unmarshal([]byte(value), target); err != nil {
return fmt.Errorf("invalid setting %s: %w", key, err)
}
return nil
}
type Config struct {
Language string `yaml:"language"`
Server ServerConfig `yaml:"server"`
@@ -15,7 +140,6 @@ type Config struct {
Logger LoggerConfig `yaml:"logger"`
Storage StorageConfig `yaml:"storage"`
VectorDB VectorDBConfig `yaml:"vectorDB"`
MCP MCPConfig `yaml:"mcp"`
WxWork WxWorkConfig `yaml:"wxWork"`
}
@@ -114,34 +238,9 @@ type OSSStorageConfig struct {
}
type VectorDBConfig struct {
Type string `yaml:"type"`
Qdrant QdrantVectorDBConfig `yaml:"qdrant"`
LanceDB LanceDBVectorDBConfig `yaml:"lancedb"`
}
type QdrantVectorDBConfig struct {
Host string `yaml:"host"`
GrpcPort int `yaml:"grpcPort"`
APIKey string `yaml:"apiKey"`
UseTLS bool `yaml:"useTls"`
}
type LanceDBVectorDBConfig struct {
Path string `yaml:"path"`
}
type MCPConfig struct {
Enabled bool `yaml:"enabled"`
Servers map[string]MCPServerConfig `yaml:"servers"`
}
type MCPServerConfig struct {
Enabled bool `yaml:"enabled"`
Endpoint string `yaml:"endpoint"`
TimeoutMS int `yaml:"timeoutMs"`
Headers map[string]string `yaml:"headers"`
}
// WxWorkConfig defines the WeCom application used for customer-service
// callbacks and notifications. Dashboard login is owned by be-system.
type WxWorkConfig struct {
+41 -14
View File
@@ -41,23 +41,18 @@ func TestLoadOverridesValuesFromEnvironment(t *testing.T) {
content := []byte(`server:
port: 8083
db:
type: sqlite
dsn: file:./data/app.db?_busy_timeout=5000
type: postgres
dsn: postgres-dsn
storage:
local:
baseUrl: /storage
mcp:
servers:
system:
endpoint: http://127.0.0.1:8083/api/mcp
`)
if err := os.WriteFile(path, content, 0600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
t.Setenv("AGENT_DESK_SERVER_PORT", "8090")
t.Setenv("AGENT_DESK_DB_DSN", "mysql-dsn")
t.Setenv("AGENT_DESK_DB_DSN", "postgres-override-dsn")
t.Setenv("AGENT_DESK_STORAGE_LOCAL_BASEURL", "/files")
t.Setenv("AGENT_DESK_MCP_SERVERS_SYSTEM_ENDPOINT", "http://127.0.0.1:8090/api/mcp")
cfg, err := Load(path)
if err != nil {
@@ -67,16 +62,48 @@ mcp:
if cfg.Server.Port != 8090 {
t.Fatalf("Server.Port=%d want 8090", cfg.Server.Port)
}
if cfg.DB.Type != "sqlite" {
t.Fatalf("DB.Type=%q want sqlite", cfg.DB.Type)
if cfg.DB.Type != "postgres" {
t.Fatalf("DB.Type=%q want postgres", cfg.DB.Type)
}
if cfg.DB.DSN != "mysql-dsn" {
t.Fatalf("DB.DSN=%q want mysql-dsn", cfg.DB.DSN)
if cfg.DB.DSN != "postgres-override-dsn" {
t.Fatalf("DB.DSN=%q want postgres-override-dsn", cfg.DB.DSN)
}
if cfg.Storage.Local.BaseURL != "/files" {
t.Fatalf("Storage.Local.BaseURL=%q want /files", cfg.Storage.Local.BaseURL)
}
if cfg.MCP.Servers["system"].Endpoint != "http://127.0.0.1:8090/api/mcp" {
t.Fatalf("MCP system endpoint=%q", cfg.MCP.Servers["system"].Endpoint)
}
func TestFromSettingsBuildsRuntimeConfig(t *testing.T) {
cfg, err := FromSettings(map[string]string{
"ai_agent_language": "en-US",
"ai_agent_storage_default": "oss",
"ai_agent_storage_max_upload_size_mb": "64",
"ai_agent_storage_oss_private": "true",
"ai_vector_path": "/var/lib/agent-desk/vectors.db",
"ai_agent_wxwork_notify_to_users": `[1,2,3]`,
"ai_agent_wxwork_notify_dedupe_interval": "900",
})
if err != nil {
t.Fatalf("FromSettings() error = %v", err)
}
if cfg.Language != "en-US" || cfg.Storage.Default != "oss" {
t.Fatalf("unexpected basic config: %+v", cfg)
}
if cfg.Storage.MaxUploadSizeMB != 64 || !cfg.Storage.OSS.Private {
t.Fatalf("unexpected storage config: %+v", cfg.Storage)
}
if cfg.VectorDB.Path != "/var/lib/agent-desk/vectors.db" {
t.Fatalf("VectorDB.Path=%q", cfg.VectorDB.Path)
}
if len(cfg.WxWork.Notify.ToUsers) != 3 || cfg.WxWork.Notify.DuplicateCheckInterval != 900 {
t.Fatalf("unexpected WeCom notify config: %+v", cfg.WxWork.Notify)
}
}
func TestFromSettingsRejectsInvalidValues(t *testing.T) {
if _, err := FromSettings(map[string]string{
"ai_agent_wxwork_notify_dedupe_interval": "not-a-number",
}); err == nil {
t.Fatal("FromSettings() error = nil, want invalid setting error")
}
}
+4 -26
View File
@@ -6,17 +6,10 @@ import (
"gopkg.in/yaml.v3"
)
func TestVectorDBConfigUnmarshalNestedProviders(t *testing.T) {
func TestVectorDBConfigUnmarshalLibSQLPath(t *testing.T) {
raw := []byte(`
vectorDB:
type: lancedb
qdrant:
host: 127.0.0.1
grpcPort: 6334
apiKey: secret
useTls: true
lancedb:
path: data/lancedb
path: data/agent/vectors.db
`)
var cfg Config
@@ -24,22 +17,7 @@ vectorDB:
t.Fatalf("yaml.Unmarshal() error = %v", err)
}
if cfg.VectorDB.Type != "lancedb" {
t.Fatalf("VectorDB.Type = %q, want %q", cfg.VectorDB.Type, "lancedb")
}
if cfg.VectorDB.Qdrant.Host != "127.0.0.1" {
t.Fatalf("VectorDB.Qdrant.Host = %q, want %q", cfg.VectorDB.Qdrant.Host, "127.0.0.1")
}
if cfg.VectorDB.Qdrant.GrpcPort != 6334 {
t.Fatalf("VectorDB.Qdrant.GrpcPort = %d, want %d", cfg.VectorDB.Qdrant.GrpcPort, 6334)
}
if cfg.VectorDB.Qdrant.APIKey != "secret" {
t.Fatalf("VectorDB.Qdrant.APIKey = %q, want %q", cfg.VectorDB.Qdrant.APIKey, "secret")
}
if !cfg.VectorDB.Qdrant.UseTLS {
t.Fatal("VectorDB.Qdrant.UseTLS = false, want true")
}
if cfg.VectorDB.LanceDB.Path != "data/lancedb" {
t.Fatalf("VectorDB.LanceDB.Path = %q, want %q", cfg.VectorDB.LanceDB.Path, "data/lancedb")
if cfg.VectorDB.Path != "data/agent/vectors.db" {
t.Fatalf("VectorDB.Path = %q", cfg.VectorDB.Path)
}
}
+9 -47
View File
@@ -14,21 +14,11 @@ type Permission struct {
// 权限常量定义
var (
// 客服会话相关权限
PermissionConversationView = Permission{Name: "查看会话", Code: "conversation.view", Type: "api", GroupName: "conversation", Method: "ANY", APIPath: "/api/dashboard/conversation/list", SortNo: 410}
PermissionConversationAssign = Permission{Name: "分配会话", Code: "conversation.assign", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/assign", SortNo: 430}
PermissionConversationTransfer = Permission{Name: "转接会话", Code: "conversation.transfer", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/transfer", SortNo: 440}
PermissionConversationClose = Permission{Name: "关闭会话", Code: "conversation.close", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/close", SortNo: 450}
PermissionConversationSend = Permission{Name: "发送会话消息", Code: "conversation.send", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/send_message", SortNo: 460}
PermissionConversationTag = Permission{Name: "管理会话标签", Code: "conversation.tag", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/add_tag", SortNo: 470}
PermissionConversationLinkCustomer = Permission{Name: "关联会话客户", Code: "conversation.linkCustomer", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/link_customer", SortNo: 495}
// 工单相关权限
PermissionTicketView = Permission{Name: "查看工单", Code: "ticket.view", Type: "api", GroupName: "ticket", Method: "ANY", APIPath: "/api/dashboard/ticket/list", SortNo: 500}
PermissionTicketCreate = Permission{Name: "创建工单", Code: "ticket.create", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/create", SortNo: 510}
PermissionTicketUpdate = Permission{Name: "更新工单", Code: "ticket.update", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/update", SortNo: 520}
PermissionTicketAssign = Permission{Name: "指派工单", Code: "ticket.assign", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/assign", SortNo: 530}
PermissionTicketChangeStatus = Permission{Name: "变更工单状态", Code: "ticket.changeStatus", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/change_status", SortNo: 540}
PermissionTicketProgress = Permission{Name: "更新工单进展", Code: "ticket.progress", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/progress/create", SortNo: 550}
PermissionConversationView = Permission{Name: "查看会话", Code: "conversation.view", Type: "api", GroupName: "conversation", Method: "ANY", APIPath: "/api/dashboard/conversation/list", SortNo: 410}
PermissionConversationAssign = Permission{Name: "分配会话", Code: "conversation.assign", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/assign", SortNo: 430}
PermissionConversationTransfer = Permission{Name: "转接会话", Code: "conversation.transfer", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/transfer", SortNo: 440}
PermissionConversationClose = Permission{Name: "关闭会话", Code: "conversation.close", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/close", SortNo: 450}
PermissionConversationSend = Permission{Name: "发送会话消息", Code: "conversation.send", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/send_message", SortNo: 460}
// 通知相关权限
PermissionNotificationView = Permission{Name: "查看通知", Code: "notification.view", Type: "api", GroupName: "notification", Method: "ANY", APIPath: "/api/dashboard/notification/list", SortNo: 680}
@@ -40,18 +30,6 @@ var (
PermissionQuickReplyUpdate = Permission{Name: "更新快捷回复", Code: "quickReply.update", Type: "api", GroupName: "quickReply", Method: "POST", APIPath: "/api/dashboard/quick-reply/update", SortNo: 630}
PermissionQuickReplyDelete = Permission{Name: "删除快捷回复", Code: "quickReply.delete", Type: "api", GroupName: "quickReply", Method: "POST", APIPath: "/api/dashboard/quick-reply/delete", SortNo: 640}
// 标签相关权限
PermissionTagView = Permission{Name: "查看标签", Code: "tag.view", Type: "api", GroupName: "tag", Method: "ANY", APIPath: "/api/dashboard/tag/list", SortNo: 550}
PermissionTagCreate = Permission{Name: "创建标签", Code: "tag.create", Type: "api", GroupName: "tag", Method: "POST", APIPath: "/api/dashboard/tag/create", SortNo: 560}
PermissionTagUpdate = Permission{Name: "更新标签", Code: "tag.update", Type: "api", GroupName: "tag", Method: "POST", APIPath: "/api/dashboard/tag/update", SortNo: 570}
PermissionTagDelete = Permission{Name: "删除标签", Code: "tag.delete", Type: "api", GroupName: "tag", Method: "POST", APIPath: "/api/dashboard/tag/delete", SortNo: 580}
// 公司相关权限
PermissionCompanyView = Permission{Name: "查看公司", Code: "company.view", Type: "api", GroupName: "company", Method: "ANY", APIPath: "/api/dashboard/company/list", SortNo: 590}
PermissionCompanyCreate = Permission{Name: "创建公司", Code: "company.create", Type: "api", GroupName: "company", Method: "POST", APIPath: "/api/dashboard/company/create", SortNo: 600}
PermissionCompanyUpdate = Permission{Name: "更新公司", Code: "company.update", Type: "api", GroupName: "company", Method: "POST", APIPath: "/api/dashboard/company/update", SortNo: 610}
PermissionCompanyDelete = Permission{Name: "删除公司", Code: "company.delete", Type: "api", GroupName: "company", Method: "POST", APIPath: "/api/dashboard/company/delete", SortNo: 620}
// 接入渠道相关权限
PermissionChannelView = Permission{Name: "查看接入渠道", Code: "channel.view", Type: "api", GroupName: "channel", Method: "ANY", APIPath: "/api/dashboard/channel/list", SortNo: 625}
PermissionChannelCreate = Permission{Name: "创建接入渠道", Code: "channel.create", Type: "api", GroupName: "channel", Method: "POST", APIPath: "/api/dashboard/channel/create", SortNo: 626}
@@ -62,17 +40,11 @@ var (
PermissionWxWorkOutboxView = Permission{Name: "查看企微 Outbox", Code: "wxworkOutbox.view", Type: "api", GroupName: "wxworkOutbox", Method: "ANY", APIPath: "/api/dashboard/channel/wxwork/outbox/failed/list", SortNo: 629}
PermissionWxWorkOutboxUpdate = Permission{Name: "处置企微 Outbox", Code: "wxworkOutbox.update", Type: "api", GroupName: "wxworkOutbox", Method: "POST", APIPath: "/api/dashboard/channel/wxwork/outbox/retry", SortNo: 630}
// 客户相关权限
PermissionCustomerView = Permission{Name: "查看客户", Code: "customer.view", Type: "api", GroupName: "customer", Method: "POST", APIPath: "/api/dashboard/customer/list", SortNo: 630}
PermissionCustomerCreate = Permission{Name: "创建客户", Code: "customer.create", Type: "api", GroupName: "customer", Method: "POST", APIPath: "/api/dashboard/customer/create", SortNo: 640}
PermissionCustomerUpdate = Permission{Name: "更新客户", Code: "customer.update", Type: "api", GroupName: "customer", Method: "POST", APIPath: "/api/dashboard/customer/update", SortNo: 650}
PermissionCustomerDelete = Permission{Name: "删除客户", Code: "customer.delete", Type: "api", GroupName: "customer", Method: "POST", APIPath: "/api/dashboard/customer/delete", SortNo: 660}
// 客服相关权限
PermissionAgentView = Permission{Name: "查看客服", Code: "agent.view", Type: "api", GroupName: "agent", Method: "ANY", APIPath: "/api/dashboard/agent/list", SortNo: 610}
PermissionAgentCreate = Permission{Name: "创建客服", Code: "agent.create", Type: "api", GroupName: "agent", Method: "POST", APIPath: "/api/dashboard/agent/create", SortNo: 620}
PermissionAgentUpdate = Permission{Name: "更新客服", Code: "agent.update", Type: "api", GroupName: "agent", Method: "POST", APIPath: "/api/dashboard/agent/update", SortNo: 630}
PermissionAgentDelete = Permission{Name: "删除客服", Code: "agent.delete", Type: "api", GroupName: "agent", Method: "POST", APIPath: "/api/dashboard/agent/delete", SortNo: 640}
PermissionAgentView = Permission{Name: "查看客服", Code: "agent.view", Type: "api", GroupName: "agent", Method: "ANY", APIPath: "/api/dashboard/agent/list", SortNo: 610}
PermissionAgentCreate = Permission{Name: "创建客服", Code: "agent.create", Type: "api", GroupName: "agent", Method: "POST", APIPath: "/api/dashboard/agent/create", SortNo: 620}
PermissionAgentUpdate = Permission{Name: "更新客服", Code: "agent.update", Type: "api", GroupName: "agent", Method: "POST", APIPath: "/api/dashboard/agent/update", SortNo: 630}
PermissionAgentDelete = Permission{Name: "删除客服", Code: "agent.delete", Type: "api", GroupName: "agent", Method: "POST", APIPath: "/api/dashboard/agent/delete", SortNo: 640}
// 客服组相关权限
PermissionAgentTeamView = Permission{Name: "查看客服组", Code: "agentTeam.view", Type: "api", GroupName: "agentTeam", Method: "ANY", APIPath: "/api/dashboard/agent-team/list", SortNo: 710}
@@ -119,14 +91,4 @@ var (
PermissionKnowledgeFAQCreate = Permission{Name: "创建知识FAQ", Code: "knowledgeFAQ.create", Type: "api", GroupName: "knowledgeFAQ", Method: "POST", APIPath: "/api/dashboard/knowledge-faq/create", SortNo: 1560}
PermissionKnowledgeFAQUpdate = Permission{Name: "更新知识FAQ", Code: "knowledgeFAQ.update", Type: "api", GroupName: "knowledgeFAQ", Method: "POST", APIPath: "/api/dashboard/knowledge-faq/update", SortNo: 1570}
PermissionKnowledgeFAQDelete = Permission{Name: "删除知识FAQ", Code: "knowledgeFAQ.delete", Type: "api", GroupName: "knowledgeFAQ", Method: "POST", APIPath: "/api/dashboard/knowledge-faq/delete", SortNo: 1580}
// Skill 定义相关权限
PermissionSkillDefinitionView = Permission{Name: "查看技能定义", Code: "skillDefinition.view", Type: "api", GroupName: "skillDefinition", Method: "ANY", APIPath: "/api/dashboard/skill-definition/list", SortNo: 1610}
PermissionSkillDefinitionCreate = Permission{Name: "创建技能定义", Code: "skillDefinition.create", Type: "api", GroupName: "skillDefinition", Method: "POST", APIPath: "/api/dashboard/skill-definition/create", SortNo: 1620}
PermissionSkillDefinitionUpdate = Permission{Name: "更新技能定义", Code: "skillDefinition.update", Type: "api", GroupName: "skillDefinition", Method: "POST", APIPath: "/api/dashboard/skill-definition/update", SortNo: 1630}
PermissionSkillDefinitionDelete = Permission{Name: "删除技能定义", Code: "skillDefinition.delete", Type: "api", GroupName: "skillDefinition", Method: "POST", APIPath: "/api/dashboard/skill-definition/delete", SortNo: 1640}
// MCP 调试相关权限
PermissionMCPView = Permission{Name: "查看MCP调试信息", Code: "mcp.view", Type: "api", GroupName: "mcp", Method: "POST", APIPath: "/api/dashboard/mcp/list_tools", SortNo: 1710}
PermissionMCPCall = Permission{Name: "调用MCP工具", Code: "mcp.call", Type: "api", GroupName: "mcp", Method: "POST", APIPath: "/api/dashboard/mcp/call_tool", SortNo: 1720}
)
+3 -3
View File
@@ -17,13 +17,13 @@ type AuthPrincipal struct {
}
type WxWorkKFChannelConfig struct {
OpenKfID string `json:"openKfId"`
OpenKfID string `json:"open_kf_id"`
}
type WebChannelConfig struct {
Title string `json:"title"`
Subtitle string `json:"subtitle"`
ThemeColor string `json:"themeColor"`
ThemeColor string `json:"theme_color"`
Position string `json:"position"`
Width string `json:"width"`
}
@@ -31,5 +31,5 @@ type WebChannelConfig struct {
type WechatMPChannelConfig struct {
Title string `json:"title"`
Subtitle string `json:"subtitle"`
ThemeColor string `json:"themeColor"`
ThemeColor string `json:"theme_color"`
}
+32
View File
@@ -0,0 +1,32 @@
package dto
import (
"encoding/json"
"strings"
"testing"
)
func TestChannelConfigJSONUsesSnakeCaseFields(t *testing.T) {
payload, err := json.Marshal(struct {
WxWork WxWorkKFChannelConfig `json:"wxwork"`
Web WebChannelConfig `json:"web"`
}{
WxWork: WxWorkKFChannelConfig{OpenKfID: "wkf"},
Web: WebChannelConfig{ThemeColor: "#fff"},
})
if err != nil {
t.Fatalf("marshal channel config: %v", err)
}
text := string(payload)
for _, field := range []string{`"open_kf_id"`, `"theme_color"`} {
if !strings.Contains(text, field) {
t.Fatalf("expected %s in %s", field, text)
}
}
for _, field := range []string{"openKfId", "themeColor"} {
if strings.Contains(text, field) {
t.Fatalf("unexpected camelCase field %q in %s", field, text)
}
}
}
@@ -1,7 +1,7 @@
package request
type RunAgentEvaluationRequest struct {
AIAgentID int64 `json:"aiAgentId"`
AIAgentID int64 `json:"ai_agent_id"`
Cases []AgentEvaluationCase `json:"cases"`
}
+22 -22
View File
@@ -3,16 +3,16 @@ package request
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type CreateAgentProfileRequest struct {
UserID int64 `json:"userId"`
TeamID int64 `json:"teamId"`
AgentCode string `json:"agentCode"`
DisplayName string `json:"displayName"`
UserID int64 `json:"user_id"`
TeamID int64 `json:"team_id"`
AgentCode string `json:"agent_code"`
DisplayName string `json:"display_name"`
Avatar string `json:"avatar"`
ServiceStatus enums.ServiceStatus `json:"serviceStatus"`
MaxConcurrentCount int `json:"maxConcurrentCount"`
PriorityLevel int `json:"priorityLevel"`
AutoAssignEnabled bool `json:"autoAssignEnabled"`
ReceiveOfflineMessage bool `json:"receiveOfflineMessage"`
ServiceStatus enums.ServiceStatus `json:"service_status"`
MaxConcurrentCount int `json:"max_concurrent_count"`
PriorityLevel int `json:"priority_level"`
AutoAssignEnabled bool `json:"auto_assign_enabled"`
ReceiveOfflineMessage bool `json:"receive_offline_message"`
Remark string `json:"remark"`
}
@@ -27,7 +27,7 @@ type DeleteAgentProfileRequest struct {
type CreateAgentTeamRequest struct {
Name string `json:"name"`
LeaderUserID int64 `json:"leaderUserId"`
LeaderUserID int64 `json:"leader_user_id"`
Status int `json:"status"`
Description string `json:"description"`
Remark string `json:"remark"`
@@ -36,7 +36,7 @@ type CreateAgentTeamRequest struct {
type UpdateAgentTeamRequest struct {
ID int64 `json:"id"`
Name string `json:"name"`
LeaderUserID int64 `json:"leaderUserId"`
LeaderUserID int64 `json:"leader_user_id"`
Status int `json:"status"`
Description string `json:"description"`
Remark string `json:"remark"`
@@ -47,9 +47,9 @@ type DeleteAgentTeamRequest struct {
}
type CreateAgentTeamScheduleRequest struct {
TeamID int64 `json:"teamId"`
StartAt string `json:"startAt"`
EndAt string `json:"endAt"`
TeamID int64 `json:"team_id"`
StartAt string `json:"start_at"`
EndAt string `json:"end_at"`
Remark string `json:"remark"`
}
@@ -63,18 +63,18 @@ type DeleteAgentTeamScheduleRequest struct {
}
type AgentTeamScheduleCalendarRequest struct {
StartAt string `json:"startAt"`
EndAt string `json:"endAt"`
TeamID int64 `json:"teamId"`
StartAt string `json:"start_at"`
EndAt string `json:"end_at"`
TeamID int64 `json:"team_id"`
}
type AgentTeamScheduleBatchRequest struct {
TeamIDs []int64 `json:"teamIds"`
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
TeamIDs []int64 `json:"team_ids"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Weekdays []int `json:"weekdays"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Remark string `json:"remark"`
Locale string `json:"-"`
}
@@ -3,8 +3,8 @@ package request
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type SaveAgentRunQualityFeedbackRequest struct {
AgentRunID int64 `json:"agentRunId"`
ResolutionStatus enums.AgentRunResolutionStatus `json:"resolutionStatus"`
EvidenceStatus enums.AgentRunEvidenceStatus `json:"evidenceStatus"`
AgentRunID int64 `json:"agent_run_id"`
ResolutionStatus enums.AgentRunResolutionStatus `json:"resolution_status"`
EvidenceStatus enums.AgentRunEvidenceStatus `json:"evidence_status"`
Comment string `json:"comment"`
}
+27 -48
View File
@@ -2,39 +2,20 @@ package request
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type AIAgentMCPToolRequest struct {
ToolCode string `json:"toolCode"`
ServerCode string `json:"serverCode"`
ToolName string `json:"toolName"`
Title string `json:"title"`
Description string `json:"description"`
RiskLevel string `json:"riskLevel"`
RequireConfirmation bool `json:"requireConfirmation"`
Arguments map[string]string `json:"arguments"`
}
type AIAgentWorkflowBindingRequest struct {
WorkflowVersionID int64 `json:"workflowVersionId"`
ToolName string `json:"toolName"`
TriggerInstruction string `json:"triggerInstruction"`
Priority int `json:"priority"`
Enabled bool `json:"enabled"`
}
type CreateAIConfigRequest struct {
Name string `json:"name"`
Provider enums.AIProvider `json:"provider"`
BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"`
ModelType enums.AIModelType `json:"modelType"`
ModelName string `json:"modelName"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
ModelType enums.AIModelType `json:"model_type"`
ModelName string `json:"model_name"`
Dimension int `json:"dimension"`
MaxContextTokens int `json:"maxContextTokens"`
MaxOutputTokens int `json:"maxOutputTokens"`
TimeoutMS int `json:"timeoutMs"`
MaxRetryCount int `json:"maxRetryCount"`
RPMLimit int `json:"rpmLimit"`
TPMLimit int `json:"tpmLimit"`
MaxContextTokens int `json:"max_context_tokens"`
MaxOutputTokens int `json:"max_output_tokens"`
TimeoutMS int `json:"timeout_ms"`
MaxRetryCount int `json:"max_retry_count"`
RPMLimit int `json:"rpm_limit"`
TPMLimit int `json:"tpm_limit"`
Remark string `json:"remark"`
}
@@ -54,25 +35,23 @@ type UpdateAIConfigStatusRequest struct {
type CreateAIAgentRequest struct {
Name string `json:"name"`
Avatar string `json:"avatar"`
Description string `json:"description"`
AIConfigID int64 `json:"aiConfigId"`
MaxSteps int `json:"maxSteps"`
ContextWindow int `json:"contextWindow"`
ToolPolicy string `json:"toolPolicy"`
KnowledgePolicy string `json:"knowledgePolicy"`
ServiceMode enums.IMConversationServiceMode `json:"serviceMode"`
SystemPrompt string `json:"systemPrompt"`
WelcomeMessage string `json:"welcomeMessage"`
ReplyTimeoutSeconds int `json:"replyTimeoutSeconds"`
RolloutPercent int `json:"rolloutPercent"`
TeamIDs []int64 `json:"teamIds"`
HandoffMode enums.AIAgentHandoffMode `json:"handoffMode"`
FallbackMode enums.AIAgentFallbackMode `json:"fallbackMode"`
FallbackMessage string `json:"fallbackMessage"`
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds"`
SkillIDs []int64 `json:"skillIds"`
MCPTools []AIAgentMCPToolRequest `json:"mcpTools"`
WorkflowBindings []AIAgentWorkflowBindingRequest `json:"workflowBindings"`
AIConfigID int64 `json:"ai_config_id"`
MaxSteps int `json:"max_steps"`
ContextWindow int `json:"context_window"`
ToolPolicy string `json:"tool_policy"`
KnowledgePolicy string `json:"knowledge_policy"`
ServiceMode enums.IMConversationServiceMode `json:"service_mode"`
SystemPrompt string `json:"system_prompt"`
WelcomeMessage string `json:"welcome_message"`
ReplyTimeoutSeconds int `json:"reply_timeout_seconds"`
RolloutPercent int `json:"rollout_percent"`
TeamIDs []int64 `json:"team_ids"`
HandoffMode enums.AIAgentHandoffMode `json:"handoff_mode"`
FallbackMode enums.AIAgentFallbackMode `json:"fallback_mode"`
FallbackMessage string `json:"fallback_message"`
KnowledgeBaseIDs []int64 `json:"knowledge_base_ids"`
}
type UpdateAIAgentRequest struct {
@@ -90,7 +69,7 @@ type PublishAIAgentRequest struct {
type RollbackAIAgentRequest struct {
ID int64 `json:"id"`
RevisionID int64 `json:"revisionId"`
RevisionID int64 `json:"revision_id"`
}
type RollbackAIAgentRolloutRequest struct {
@@ -1,36 +0,0 @@
package request
import "code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/dsl"
type CreateAIWorkflowRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Definition dsl.Definition `json:"definition"`
}
type UpdateAIWorkflowRequest struct {
ID int64 `json:"id"`
CreateAIWorkflowRequest
}
type DeleteAIWorkflowRequest struct {
ID int64 `json:"id"`
}
type ValidateAIWorkflowRequest struct {
Definition dsl.Definition `json:"definition"`
}
type PublishAIWorkflowRequest struct {
WorkflowID int64 `json:"workflowId"`
Definition dsl.Definition `json:"definition"`
}
type AIWorkflowVersionListRequest struct {
WorkflowID int64 `json:"workflowId"`
}
type RestoreAIWorkflowVersionRequest struct {
WorkflowID int64 `json:"workflowId"`
WorkflowVersionID int64 `json:"workflowVersionId"`
}
+4 -4
View File
@@ -1,11 +1,11 @@
package request
type CreateChannelRequest struct {
ChannelType string `json:"channelType"`
AIAgentID int64 `json:"aiAgentId"`
AIAgentRolloutPercent int `json:"aiAgentRolloutPercent"`
ChannelType string `json:"channel_type"`
AIAgentID int64 `json:"ai_agent_id"`
AIAgentRolloutPercent int `json:"ai_agent_rollout_percent"`
Name string `json:"name"`
ConfigJSON string `json:"configJson"`
ConfigJSON string `json:"config_json"`
Status int `json:"status"`
Remark string `json:"remark"`
}
@@ -1,21 +0,0 @@
package request
type CreateCompanyRequest struct {
Name string `json:"name"`
Code string `json:"code"`
Remark string `json:"remark"`
}
type UpdateCompanyRequest struct {
ID int64 `json:"id"`
CreateCompanyRequest
}
type DeleteCompanyRequest struct {
ID int64 `json:"id"`
}
type UpdateCompanyStatusRequest struct {
ID int64 `json:"id"`
Status int `json:"status"`
}
@@ -12,50 +12,39 @@ const (
type ConversationListRequest struct {
Status int `json:"status"`
ServiceMode int `json:"serviceMode"`
CurrentAssigneeID int64 `json:"currentAssigneeId"`
ServiceMode int `json:"service_mode"`
CurrentAssigneeID int64 `json:"current_assignee_id"`
Keyword string `json:"keyword"`
TagID int64 `json:"tagId"`
}
type AssignConversationRequest struct {
ConversationID int64 `json:"conversationId"`
AssigneeID int64 `json:"assigneeId"`
ConversationID int64 `json:"conversation_id"`
AssigneeID int64 `json:"assignee_id"`
Reason string `json:"reason"`
}
type DispatchConversationRequest struct {
ConversationID int64 `json:"conversationId"`
ConversationID int64 `json:"conversation_id"`
}
type TransferConversationRequest struct {
ConversationID int64 `json:"conversationId"`
ToUserID int64 `json:"toUserId"`
ConversationID int64 `json:"conversation_id"`
ToUserID int64 `json:"to_user_id"`
Reason string `json:"reason"`
}
type CloseConversationRequest struct {
ConversationID int64 `json:"conversationId"`
CloseReason string `json:"closeReason"`
ConversationID int64 `json:"conversation_id"`
CloseReason string `json:"close_reason"`
}
type ReadConversationRequest struct {
ConversationID int64 `json:"conversationId"`
MessageID int64 `json:"messageId"`
ConversationID int64 `json:"conversation_id"`
MessageID int64 `json:"message_id"`
}
type AddConversationTagRequest struct {
ConversationID int64 `json:"conversationId"`
TagID int64 `json:"tagId"`
}
type RemoveConversationTagRequest struct {
ConversationID int64 `json:"conversationId"`
TagID int64 `json:"tagId"`
}
// LinkConversationCustomerRequest 将客服会话关联到 CRM 客户(并同步访客身份映射)。
type LinkConversationCustomerRequest struct {
ConversationID int64 `json:"conversationId"`
CustomerID int64 `json:"customerId"`
type ExecuteCustomerQuickActionRequest struct {
ConversationID int64 `json:"conversation_id"`
Code string `json:"code"`
ClientMsgID string `json:"client_msg_id"`
}
@@ -1,27 +0,0 @@
package request
type CreateCustomerContactRequest struct {
CustomerID int64 `json:"customerId"`
ContactType string `json:"contactType"`
ContactValue string `json:"contactValue"`
IsPrimary bool `json:"isPrimary"`
IsVerified bool `json:"isVerified"`
Source string `json:"source"`
Status int `json:"status"`
Remark string `json:"remark"`
}
type UpdateCustomerContactRequest struct {
ID int64 `json:"id"`
ContactType string `json:"contactType"`
ContactValue string `json:"contactValue"`
IsPrimary bool `json:"isPrimary"`
IsVerified bool `json:"isVerified"`
Source string `json:"source"`
Status int `json:"status"`
Remark string `json:"remark"`
}
type DeleteCustomerContactRequest struct {
ID int64 `json:"id"`
}
@@ -1,72 +0,0 @@
package request
// CustomerListRequest 客户分页列表查询(POST /customer/list JSON Body)。
type CustomerListRequest struct {
Page int `json:"page"`
Limit int `json:"limit"`
Status *int `json:"status,omitempty"`
Gender *int `json:"gender,omitempty"`
CompanyID *int64 `json:"companyId,omitempty"`
// Keyword 模糊匹配:客户姓名、主手机号、主邮箱、任意联系方式(t_customer_contact)、公司名称(t_company)。
Keyword string `json:"keyword"`
}
func (r CustomerListRequest) GetPage() int {
if r.Page <= 0 {
return 1
}
return r.Page
}
func (r CustomerListRequest) GetLimit() int {
if r.Limit <= 0 {
return 20
}
return r.Limit
}
func (r CustomerListRequest) Offset() int {
return (r.GetPage() - 1) * r.GetLimit()
}
type CreateCustomerRequest struct {
Name string `json:"name"`
Gender int `json:"gender"`
CompanyID int64 `json:"companyId"`
PrimaryMobile string `json:"primaryMobile"`
PrimaryEmail string `json:"primaryEmail"`
Remark string `json:"remark"`
}
type UpdateCustomerRequest struct {
ID int64 `json:"id"`
CreateCustomerRequest
}
type DeleteCustomerRequest struct {
ID int64 `json:"id"`
}
type UpdateCustomerStatusRequest struct {
ID int64 `json:"id"`
Status int `json:"status"`
}
// CustomerProfileContactItem 保存客户档案时的联系方式行(无 id 表示新建)。
type CustomerProfileContactItem struct {
ID *int64 `json:"id,omitempty"`
ContactType string `json:"contactType"`
ContactValue string `json:"contactValue"`
IsPrimary bool `json:"isPrimary"`
Remark string `json:"remark"`
}
// SaveCustomerProfileRequest 客户主信息与联系方式一并保存(单事务);id 为空或 0 表示新建客户。
type SaveCustomerProfileRequest struct {
ID *int64 `json:"id,omitempty"`
Name string `json:"name"`
Gender int `json:"gender"`
CompanyID int64 `json:"companyId"`
Remark string `json:"remark"`
Contacts []CustomerProfileContactItem `json:"contacts"`
}
+37 -37
View File
@@ -9,15 +9,15 @@ import (
type CreateKnowledgeBaseRequest struct {
Name string `json:"name"`
Description string `json:"description"`
KnowledgeType string `json:"knowledgeType"`
DefaultTopK int `json:"defaultTopK"`
DefaultScoreThreshold float64 `json:"defaultScoreThreshold"`
DefaultRerankLimit int `json:"defaultRerankLimit"`
ChunkProvider string `json:"chunkProvider"`
ChunkTargetTokens int `json:"chunkTargetTokens"`
ChunkMaxTokens int `json:"chunkMaxTokens"`
ChunkOverlapTokens int `json:"chunkOverlapTokens"`
AnswerMode int `json:"answerMode"`
KnowledgeType string `json:"knowledge_type"`
DefaultTopK int `json:"default_top_k"`
DefaultScoreThreshold float64 `json:"default_score_threshold"`
DefaultRerankLimit int `json:"default_rerank_limit"`
ChunkProvider string `json:"chunk_provider"`
ChunkTargetTokens int `json:"chunk_target_tokens"`
ChunkMaxTokens int `json:"chunk_max_tokens"`
ChunkOverlapTokens int `json:"chunk_overlap_tokens"`
AnswerMode int `json:"answer_mode"`
Remark string `json:"remark"`
}
@@ -27,8 +27,8 @@ type UpdateKnowledgeBaseRequest struct {
}
type CreateKnowledgeDirectoryRequest struct {
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
ParentID int64 `json:"parentId"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
ParentID int64 `json:"parent_id"`
Name string `json:"name"`
Remark string `json:"remark"`
}
@@ -43,10 +43,10 @@ type DeleteKnowledgeDirectoryRequest struct {
}
type CreateKnowledgeDocumentRequest struct {
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
DirectoryID int64 `json:"directoryId"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
DirectoryID int64 `json:"directory_id"`
Title string `json:"title"`
ContentType enums.KnowledgeDocumentContentType `json:"contentType"`
ContentType enums.KnowledgeDocumentContentType `json:"content_type"`
Content string `json:"content"`
}
@@ -56,8 +56,8 @@ type UpdateKnowledgeDocumentRequest struct {
}
type BatchMoveKnowledgeDocumentRequest struct {
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
DirectoryID int64 `json:"directoryId"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
DirectoryID int64 `json:"directory_id"`
IDs []int64 `json:"ids"`
}
@@ -66,11 +66,11 @@ type BatchDeleteKnowledgeDocumentRequest struct {
}
type CreateKnowledgeFAQRequest struct {
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
DirectoryID int64 `json:"directoryId"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
DirectoryID int64 `json:"directory_id"`
Question string `json:"question"`
Answer string `json:"answer"`
SimilarQuestions []string `json:"similarQuestions"`
SimilarQuestions []string `json:"similar_questions"`
Remark string `json:"remark"`
}
@@ -80,8 +80,8 @@ type UpdateKnowledgeFAQRequest struct {
}
type BatchMoveKnowledgeFAQRequest struct {
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
DirectoryID int64 `json:"directoryId"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
DirectoryID int64 `json:"directory_id"`
IDs []int64 `json:"ids"`
}
@@ -105,33 +105,33 @@ type ImportKnowledgeFAQRequest struct {
}
type KnowledgeSearchRequest struct {
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds"`
KnowledgeBaseIDs []int64 `json:"knowledge_base_ids"`
Question string `json:"question"`
TopK int `json:"topK"`
ScoreThreshold float64 `json:"scoreThreshold"`
RerankLimit int `json:"rerankLimit"`
TopK int `json:"top_k"`
ScoreThreshold float64 `json:"score_threshold"`
RerankLimit int `json:"rerank_limit"`
Channel string `json:"channel"`
Scene string `json:"scene"`
SessionID string `json:"sessionId"`
ConversationID int64 `json:"conversationId"`
SessionID string `json:"session_id"`
ConversationID int64 `json:"conversation_id"`
}
type KnowledgeAnswerRequest struct {
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds"`
KnowledgeBaseIDs []int64 `json:"knowledge_base_ids"`
Question string `json:"question"`
TopK int `json:"topK"`
ScoreThreshold float64 `json:"scoreThreshold"`
RerankLimit int `json:"rerankLimit"`
TopK int `json:"top_k"`
ScoreThreshold float64 `json:"score_threshold"`
RerankLimit int `json:"rerank_limit"`
Channel string `json:"channel"`
Scene string `json:"scene"`
SessionID string `json:"sessionId"`
ConversationID int64 `json:"conversationId"`
AnswerMode int `json:"answerMode"`
SessionID string `json:"session_id"`
ConversationID int64 `json:"conversation_id"`
AnswerMode int `json:"answer_mode"`
}
type CreateKnowledgeFeedbackRequest struct {
RetrieveLogID int64 `json:"retrieveLogId"`
FeedbackType int `json:"feedbackType"`
FeedbackReason string `json:"feedbackReason"`
RetrieveLogID int64 `json:"retrieve_log_id"`
FeedbackType int `json:"feedback_type"`
FeedbackReason string `json:"feedback_reason"`
Remark string `json:"remark"`
}
-11
View File
@@ -1,11 +0,0 @@
package request
type MCPServerDebugRequest struct {
ServerCode string `json:"serverCode"`
}
type MCPCallToolRequest struct {
ServerCode string `json:"serverCode"`
ToolName string `json:"toolName"`
Arguments map[string]any `json:"arguments"`
}
+7 -7
View File
@@ -3,19 +3,19 @@ package request
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type MessageListRequest struct {
ConversationID int64 `json:"conversationId"`
SenderType string `json:"senderType"`
MessageType string `json:"messageType"`
ConversationID int64 `json:"conversation_id"`
SenderType string `json:"sender_type"`
MessageType string `json:"message_type"`
}
type SendConversationMessageRequest struct {
ConversationID int64 `json:"conversationId"`
MessageType enums.IMMessageType `json:"messageType"`
ConversationID int64 `json:"conversation_id"`
MessageType enums.IMMessageType `json:"message_type"`
Content string `json:"content"`
Payload string `json:"payload"`
ClientMsgID string `json:"clientMsgId"`
ClientMsgID string `json:"client_msg_id"`
}
type RecallConversationMessageRequest struct {
MessageID int64 `json:"messageId"`
MessageID int64 `json:"message_id"`
}
@@ -1,13 +1,13 @@
package request
type CreateNotificationRequest struct {
RecipientUserID int64 `json:"recipientUserId"`
RecipientUserID int64 `json:"recipient_user_id"`
Title string `json:"title"`
Content string `json:"content"`
NotificationType string `json:"notificationType"`
BizType string `json:"bizType"`
BizID int64 `json:"bizId"`
ActionURL string `json:"actionUrl"`
NotificationType string `json:"notification_type"`
BizType string `json:"biz_type"`
BizID int64 `json:"biz_id"`
ActionURL string `json:"action_url"`
}
type MarkNotificationReadRequest struct {
@@ -3,17 +3,17 @@ package request
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type QuickReplyListRequest struct {
GroupName string `json:"groupName"`
GroupName string `json:"group_name"`
Keyword string `json:"keyword"`
Status int `json:"status"`
}
type CreateQuickReplyRequest struct {
GroupName string `json:"groupName"`
GroupName string `json:"group_name"`
Title string `json:"title"`
Content string `json:"content"`
Status enums.Status `json:"status"`
SortNo int `json:"sortNo"`
SortNo int `json:"sort_no"`
}
type UpdateQuickReplyRequest struct {
-47
View File
@@ -1,47 +0,0 @@
package request
type SkillDefinitionListRequest struct {
Name string `json:"name"`
Status int `json:"status"`
}
type CreateSkillDefinitionRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Instruction string `json:"instruction"`
Examples []string `json:"examples"`
ToolWhitelist []string `json:"toolWhitelist"`
Remark string `json:"remark"`
}
type UpdateSkillDefinitionRequest struct {
ID int64 `json:"id"`
CreateSkillDefinitionRequest
}
type DeleteSkillDefinitionRequest struct {
ID int64 `json:"id"`
}
type RestoreSkillDefinitionRequest struct {
ID int64 `json:"id"`
}
type UpdateSkillDefinitionStatusRequest struct {
ID int64 `json:"id"`
Status int `json:"status"`
}
type SkillDebugRunRequest struct {
AIAgentID int64 `json:"aiAgentId"`
ConversationID int64 `json:"conversationId"`
SkillDefinitionID int64 `json:"skillDefinitionId"`
UserMessage string `json:"userMessage"`
}
type SkillDebugResumeRequest struct {
AIAgentID int64 `json:"aiAgentId"`
ConversationID int64 `json:"conversationId"`
CheckPointID string `json:"checkPointId"`
UserMessage string `json:"userMessage"`
}
@@ -0,0 +1,56 @@
package request
import (
"go/ast"
"go/parser"
"go/token"
"reflect"
"strconv"
"strings"
"testing"
"unicode"
)
func TestRequestStructTagsUseSnakeCase(t *testing.T) {
fset := token.NewFileSet()
packages, err := parser.ParseDir(fset, ".", nil, 0)
if err != nil {
t.Fatalf("parse request package: %v", err)
}
for _, pkg := range packages {
for filename, file := range pkg.Files {
ast.Inspect(file, func(node ast.Node) bool {
field, ok := node.(*ast.Field)
if !ok || field.Tag == nil {
return true
}
rawTag, err := strconv.Unquote(field.Tag.Value)
if err != nil {
t.Errorf("%s: invalid struct tag %s: %v", filename, field.Tag.Value, err)
return true
}
for _, key := range []string{"json", "form", "query", "uri"} {
name := strings.Split(reflect.StructTag(rawTag).Get(key), ",")[0]
if name == "" || name == "-" {
continue
}
if !isSnakeCaseRequestName(name) {
t.Errorf("%s: %s tag %q must use snake_case", filename, key, name)
}
}
return true
})
}
}
}
func isSnakeCaseRequestName(name string) bool {
for _, r := range name {
if unicode.IsLower(r) || unicode.IsDigit(r) || r == '_' {
continue
}
return false
}
return true
}
-27
View File
@@ -1,27 +0,0 @@
package request
type TagListRequest struct {
ParentID int64 `json:"parentId"`
Name string `json:"name"`
Status int `json:"status"`
}
type CreateTagRequest struct {
ParentID int64 `json:"parentId"`
Name string `json:"name"`
Remark string `json:"remark"`
}
type UpdateTagRequest struct {
ID int64 `json:"id"`
CreateTagRequest
}
type DeleteTagRequest struct {
ID int64 `json:"id"`
}
type UpdateTagStatusRequest struct {
ID int64 `json:"id"`
Status int `json:"status"`
}
@@ -1,59 +0,0 @@
package request
type CreateTicketRequest struct {
Title string `json:"title"`
Description string `json:"description"`
Source string `json:"source"`
Channel string `json:"channel"`
CustomerID int64 `json:"customerId"`
ConversationID int64 `json:"conversationId"`
TagIDs []int64 `json:"tagIds"`
CurrentAssigneeID int64 `json:"currentAssigneeId"`
}
type CreateTicketFromConversationRequest struct {
ConversationID int64 `json:"conversationId"`
Title string `json:"title"`
Description string `json:"description"`
TagIDs []int64 `json:"tagIds"`
CurrentAssigneeID int64 `json:"currentAssigneeId"`
}
type UpdateTicketRequest struct {
TicketID int64 `json:"ticketId"`
Title string `json:"title"`
Description string `json:"description"`
TagIDs []int64 `json:"tagIds"`
CurrentAssigneeID int64 `json:"currentAssigneeId"`
}
type LinkTicketCustomerRequest struct {
TicketID int64 `json:"ticketId"`
CustomerID int64 `json:"customerId"`
}
type AssignTicketRequest struct {
TicketID int64 `json:"ticketId"`
ToUserID int64 `json:"toUserId"`
Reason string `json:"reason"`
}
type ChangeTicketStatusRequest struct {
TicketID int64 `json:"ticketId"`
Status string `json:"status"`
}
type CreateTicketProgressRequest struct {
TicketID int64 `json:"ticketId"`
Content string `json:"content"`
}
type SaveTicketViewRequest struct {
ID int64 `json:"id"`
Name string `json:"name"`
Filters map[string]any `json:"filters"`
}
type DeleteTicketViewRequest struct {
ID int64 `json:"id"`
}
@@ -1,10 +1,10 @@
package response
type AgentEvaluationResultResponse struct {
CaseID string `json:"caseId"`
CaseID string `json:"case_id"`
Category string `json:"category"`
Passed bool `json:"passed"`
ReplyText string `json:"replyText"`
ReplyText string `json:"reply_text"`
Interrupted bool `json:"interrupted"`
Error string `json:"error,omitempty"`
Finding string `json:"finding,omitempty"`
+24 -24
View File
@@ -11,30 +11,30 @@ type AgentUserOptionResponse struct {
type AgentProfileResponse struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
TeamID int64 `json:"teamId"`
TeamName string `json:"teamName,omitempty"`
UserID int64 `json:"user_id"`
TeamID int64 `json:"team_id"`
TeamName string `json:"team_name,omitempty"`
Username string `json:"username,omitempty"`
Nickname string `json:"nickname,omitempty"`
AgentCode string `json:"agentCode"`
DisplayName string `json:"displayName"`
AgentCode string `json:"agent_code"`
DisplayName string `json:"display_name"`
Avatar string `json:"avatar"`
ServiceStatus enums.ServiceStatus `json:"serviceStatus"`
MaxConcurrentCount int `json:"maxConcurrentCount"`
PriorityLevel int `json:"priorityLevel"`
AutoAssignEnabled bool `json:"autoAssignEnabled"`
ReceiveOfflineMessage bool `json:"receiveOfflineMessage"`
LastOnlineAt string `json:"lastOnlineAt,omitempty"`
LastStatusAt string `json:"lastStatusAt,omitempty"`
ServiceStatus enums.ServiceStatus `json:"service_status"`
MaxConcurrentCount int `json:"max_concurrent_count"`
PriorityLevel int `json:"priority_level"`
AutoAssignEnabled bool `json:"auto_assign_enabled"`
ReceiveOfflineMessage bool `json:"receive_offline_message"`
LastOnlineAt string `json:"last_online_at,omitempty"`
LastStatusAt string `json:"last_status_at,omitempty"`
Remark string `json:"remark"`
}
type AgentTeamResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
LeaderUserID int64 `json:"leaderUserId"`
LeaderUsername string `json:"leaderUsername,omitempty"`
LeaderNickname string `json:"leaderNickname,omitempty"`
LeaderUserID int64 `json:"leader_user_id"`
LeaderUsername string `json:"leader_username,omitempty"`
LeaderNickname string `json:"leader_nickname,omitempty"`
Status enums.Status `json:"status"`
Description string `json:"description"`
Remark string `json:"remark"`
@@ -42,10 +42,10 @@ type AgentTeamResponse struct {
type AgentTeamScheduleResponse struct {
ID int64 `json:"id"`
TeamID int64 `json:"teamId"`
TeamName string `json:"teamName,omitempty"`
StartAt string `json:"startAt"`
EndAt string `json:"endAt"`
TeamID int64 `json:"team_id"`
TeamName string `json:"team_name,omitempty"`
StartAt string `json:"start_at"`
EndAt string `json:"end_at"`
Remark string `json:"remark"`
}
@@ -56,15 +56,15 @@ type AgentTeamScheduleBatchPreviewResponse struct {
}
type AgentTeamScheduleBatchPreviewItem struct {
TeamID int64 `json:"teamId"`
TeamName string `json:"teamName"`
TeamID int64 `json:"team_id"`
TeamName string `json:"team_name"`
Date string `json:"date"`
Weekday int `json:"weekday"`
StartAt string `json:"startAt"`
EndAt string `json:"endAt"`
StartAt string `json:"start_at"`
EndAt string `json:"end_at"`
Remark string `json:"remark"`
Conflict bool `json:"conflict"`
ConflictReason string `json:"conflictReason"`
ConflictReason string `json:"conflict_reason"`
}
type AgentTeamScheduleBatchGenerateResponse struct {
@@ -21,7 +21,7 @@ func TestAgentTeamScheduleResponseOmitsSourceType(t *testing.T) {
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("unmarshal response error = %v", err)
}
if _, ok := decoded["sourceType"]; ok {
t.Fatalf("sourceType should not be exposed: %s", payload)
if _, ok := decoded["source_type"]; ok {
t.Fatalf("source_type should not be exposed: %s", payload)
}
}
+39 -41
View File
@@ -4,62 +4,60 @@ import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type AgentRunResponse struct {
ID int64 `json:"id"`
ConversationID int64 `json:"conversationId"`
AIAgentID int64 `json:"aiAgentId"`
AgentRevisionID int64 `json:"agentRevisionId"`
SourceMessageID int64 `json:"sourceMessageId"`
WorkflowRunID int64 `json:"workflowRunId"`
ConversationID int64 `json:"conversation_id"`
AIAgentID int64 `json:"ai_agent_id"`
AgentRevisionID int64 `json:"agent_revision_id"`
SourceMessageID int64 `json:"source_message_id"`
Status string `json:"status"`
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
StartedAt string `json:"startedAt"`
EndedAt string `json:"endedAt"`
DurationMS int64 `json:"durationMs"`
ErrorMessage string `json:"errorMessage"`
TraceData string `json:"traceData"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
StartedAt string `json:"started_at"`
EndedAt string `json:"ended_at"`
DurationMS int64 `json:"duration_ms"`
ErrorMessage string `json:"error_message"`
TraceData string `json:"trace_data"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Steps []AgentStepResponse `json:"steps,omitempty"`
ToolCalls []AgentToolCallResponse `json:"toolCalls,omitempty"`
QualityFeedback *AgentRunQualityFeedbackResponse `json:"qualityFeedback,omitempty"`
ToolCalls []AgentToolCallResponse `json:"tool_calls,omitempty"`
QualityFeedback *AgentRunQualityFeedbackResponse `json:"quality_feedback,omitempty"`
}
type AgentRunQualityFeedbackResponse struct {
ID int64 `json:"id"`
AgentRunID int64 `json:"agentRunId"`
ResolutionStatus enums.AgentRunResolutionStatus `json:"resolutionStatus"`
EvidenceStatus enums.AgentRunEvidenceStatus `json:"evidenceStatus"`
AgentRunID int64 `json:"agent_run_id"`
ResolutionStatus enums.AgentRunResolutionStatus `json:"resolution_status"`
EvidenceStatus enums.AgentRunEvidenceStatus `json:"evidence_status"`
Comment string `json:"comment"`
UpdateUserName string `json:"updateUserName"`
UpdatedAt string `json:"updatedAt"`
UpdateUserName string `json:"update_user_name"`
UpdatedAt string `json:"updated_at"`
}
type AgentStepResponse struct {
ID int64 `json:"id"`
AgentRunID int64 `json:"agentRunId"`
WorkflowRunID int64 `json:"workflowRunId"`
StepType string `json:"stepType"`
StepCode string `json:"stepCode"`
AgentRunID int64 `json:"agent_run_id"`
StepType string `json:"step_type"`
StepCode string `json:"step_code"`
Status string `json:"status"`
InputPreview string `json:"inputPreview"`
OutputPreview string `json:"outputPreview"`
ErrorMessage string `json:"errorMessage"`
StartedAt string `json:"startedAt"`
EndedAt string `json:"endedAt"`
DurationMS int `json:"durationMs"`
InputPreview string `json:"input_preview"`
OutputPreview string `json:"output_preview"`
ErrorMessage string `json:"error_message"`
StartedAt string `json:"started_at"`
EndedAt string `json:"ended_at"`
DurationMS int `json:"duration_ms"`
}
type AgentToolCallResponse struct {
ID int64 `json:"id"`
AgentRunID int64 `json:"agentRunId"`
AgentStepID int64 `json:"agentStepId"`
ToolCode string `json:"toolCode"`
RiskLevel string `json:"riskLevel"`
RequireConfirm bool `json:"requireConfirm"`
AgentRunID int64 `json:"agent_run_id"`
AgentStepID int64 `json:"agent_step_id"`
ToolCode string `json:"tool_code"`
RiskLevel string `json:"risk_level"`
RequireConfirm bool `json:"require_confirm"`
Status string `json:"status"`
ArgumentsPreview string `json:"argumentsPreview"`
ResultPreview string `json:"resultPreview"`
ErrorMessage string `json:"errorMessage"`
DurationMS int `json:"durationMs"`
CreatedAt string `json:"createdAt"`
ArgumentsPreview string `json:"arguments_preview"`
ResultPreview string `json:"result_preview"`
ErrorMessage string `json:"error_message"`
DurationMS int `json:"duration_ms"`
CreatedAt string `json:"created_at"`
}
+57 -79
View File
@@ -10,65 +10,46 @@ type AIAgentTeamResponse struct {
Name string `json:"name"`
}
type AIAgentSkillResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type AIAgentMCPToolResponse struct {
ToolCode string `json:"toolCode"`
ServerCode string `json:"serverCode"`
ToolName string `json:"toolName"`
Title string `json:"title"`
Description string `json:"description"`
RiskLevel string `json:"riskLevel"`
RequireConfirmation bool `json:"requireConfirmation"`
Arguments map[string]string `json:"arguments"`
}
type AIAgentWorkflowBindingResponse struct {
ID int64 `json:"id"`
WorkflowID int64 `json:"workflowId"`
WorkflowVersionID int64 `json:"workflowVersionId"`
WorkflowName string `json:"workflowName"`
WorkflowVersion int `json:"workflowVersion"`
ToolName string `json:"toolName"`
TriggerInstruction string `json:"triggerInstruction"`
Priority int `json:"priority"`
Enabled bool `json:"enabled"`
}
type AgentRevisionResponse struct {
ID int64 `json:"id"`
AgentID int64 `json:"agentId"`
AgentID int64 `json:"agent_id"`
Revision int `json:"revision"`
Status enums.Status `json:"status"`
DefinitionHash string `json:"definitionHash"`
PublishedAt string `json:"publishedAt"`
PublishedByID int64 `json:"publishedById"`
PublishedByName string `json:"publishedByName"`
DefinitionHash string `json:"definition_hash"`
PublishedAt string `json:"published_at"`
PublishedByID int64 `json:"published_by_id"`
PublishedByName string `json:"published_by_name"`
}
type AIConfigResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Provider enums.AIProvider `json:"provider"`
BaseURL string `json:"baseUrl"`
HasAPIKey bool `json:"hasApiKey"`
ModelType enums.AIModelType `json:"modelType"`
ModelName string `json:"modelName"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key,omitempty"`
HasAPIKey bool `json:"has_api_key"`
ModelType enums.AIModelType `json:"model_type"`
ModelName string `json:"model_name"`
Dimension int `json:"dimension"`
MaxContextTokens int `json:"maxContextTokens"`
MaxOutputTokens int `json:"maxOutputTokens"`
TimeoutMS int `json:"timeoutMs"`
MaxRetryCount int `json:"maxRetryCount"`
RPMLimit int `json:"rpmLimit"`
TPMLimit int `json:"tpmLimit"`
MaxContextTokens int `json:"max_context_tokens"`
MaxOutputTokens int `json:"max_output_tokens"`
TimeoutMS int `json:"timeout_ms"`
MaxRetryCount int `json:"max_retry_count"`
RPMLimit int `json:"rpm_limit"`
TPMLimit int `json:"tpm_limit"`
Status enums.Status `json:"status"`
SortNo int `json:"sortNo"`
SortNo int `json:"sort_no"`
Remark string `json:"remark"`
}
func BuildAIConfigDetailResponse(item *models.AIConfig) AIConfigResponse {
result := BuildAIConfigResponse(item)
if item != nil {
result.APIKey = item.APIKey
}
return result
}
func BuildAIConfigResponse(item *models.AIConfig) AIConfigResponse {
return AIConfigResponse{
ID: item.ID,
@@ -92,39 +73,36 @@ func BuildAIConfigResponse(item *models.AIConfig) AIConfigResponse {
}
type AIAgentResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Status enums.Status `json:"status"`
StatusName string `json:"statusName"`
AIConfigID int64 `json:"aiConfigId"`
AIConfigName string `json:"aiConfigName"`
MaxSteps int `json:"maxSteps"`
ContextWindow int `json:"contextWindow"`
ToolPolicy string `json:"toolPolicy"`
KnowledgePolicy string `json:"knowledgePolicy"`
ServiceMode enums.IMConversationServiceMode `json:"serviceMode"`
ServiceModeName string `json:"serviceModeName"`
SystemPrompt string `json:"systemPrompt"`
WelcomeMessage string `json:"welcomeMessage"`
ReplyTimeoutSeconds int `json:"replyTimeoutSeconds"`
RolloutPercent int `json:"rolloutPercent"`
PreviousRolloutPercent int `json:"previousRolloutPercent"`
Teams []AIAgentTeamResponse `json:"teams"`
HandoffMode enums.AIAgentHandoffMode `json:"handoffMode"`
HandoffModeName string `json:"handoffModeName"`
FallbackMode enums.AIAgentFallbackMode `json:"fallbackMode"`
FallbackModeName string `json:"fallbackModeName"`
FallbackMessage string `json:"fallbackMessage"`
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds"`
SkillIDs []int64 `json:"skillIds"`
Skills []AIAgentSkillResponse `json:"skills"`
MCPTools []AIAgentMCPToolResponse `json:"mcpTools"`
WorkflowBindings []AIAgentWorkflowBindingResponse `json:"workflowBindings"`
PublishedRevisionID int64 `json:"publishedRevisionId"`
SortNo int `json:"sortNo"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
ID int64 `json:"id"`
Name string `json:"name"`
Avatar string `json:"avatar"`
Description string `json:"description"`
Status enums.Status `json:"status"`
StatusName string `json:"status_name"`
AIConfigID int64 `json:"ai_config_id"`
AIConfigName string `json:"ai_config_name"`
MaxSteps int `json:"max_steps"`
ContextWindow int `json:"context_window"`
ToolPolicy string `json:"tool_policy"`
KnowledgePolicy string `json:"knowledge_policy"`
ServiceMode enums.IMConversationServiceMode `json:"service_mode"`
ServiceModeName string `json:"service_mode_name"`
SystemPrompt string `json:"system_prompt"`
WelcomeMessage string `json:"welcome_message"`
ReplyTimeoutSeconds int `json:"reply_timeout_seconds"`
RolloutPercent int `json:"rollout_percent"`
PreviousRolloutPercent int `json:"previous_rollout_percent"`
Teams []AIAgentTeamResponse `json:"teams"`
HandoffMode enums.AIAgentHandoffMode `json:"handoff_mode"`
HandoffModeName string `json:"handoff_mode_name"`
FallbackMode enums.AIAgentFallbackMode `json:"fallback_mode"`
FallbackModeName string `json:"fallback_mode_name"`
FallbackMessage string `json:"fallback_message"`
KnowledgeBaseIDs []int64 `json:"knowledge_base_ids"`
PublishedRevisionID int64 `json:"published_revision_id"`
SortNo int `json:"sort_no"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
CreateUserName string `json:"create_user_name"`
UpdateUserName string `json:"update_user_name"`
}
+23 -4
View File
@@ -21,10 +21,29 @@ func TestBuildAIConfigResponseOmitsAPIKey(t *testing.T) {
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("unmarshal response error = %v", err)
}
if _, ok := decoded["apiKey"]; ok {
t.Fatalf("apiKey should not be exposed: %s", payload)
if _, ok := decoded["api_key"]; ok {
t.Fatalf("api_key should not be exposed: %s", payload)
}
if got, ok := decoded["hasApiKey"].(bool); !ok || !got {
t.Fatalf("hasApiKey = %v, want true: %s", decoded["hasApiKey"], payload)
if got, ok := decoded["has_api_key"].(bool); !ok || !got {
t.Fatalf("has_api_key = %v, want true: %s", decoded["has_api_key"], payload)
}
}
func TestBuildAIConfigDetailResponseIncludesAPIKey(t *testing.T) {
payload, err := json.Marshal(BuildAIConfigDetailResponse(&models.AIConfig{
ID: 1,
Name: "test",
APIKey: "sk-secret",
}))
if err != nil {
t.Fatalf("marshal response error = %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("unmarshal response error = %v", err)
}
if got := decoded["api_key"]; got != "sk-secret" {
t.Fatalf("api_key = %v, want sk-secret: %s", got, payload)
}
}
@@ -1,111 +0,0 @@
package response
import (
"code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/dsl"
workflowregistry "code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/registry"
workflowvalidator "code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/validator"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
)
type AIWorkflowResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Status enums.Status `json:"status"`
DraftDefinition dsl.Definition `json:"draftDefinition"`
PublishedVersionID int64 `json:"publishedVersionId"`
SortNo int `json:"sortNo"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
}
type AIWorkflowVersionResponse struct {
ID int64 `json:"id"`
WorkflowID int64 `json:"workflowId"`
Version int `json:"version"`
Status enums.Status `json:"status"`
Definition dsl.Definition `json:"definition"`
DefinitionHash string `json:"definitionHash"`
PublishedAt string `json:"publishedAt"`
PublishedByID int64 `json:"publishedById"`
PublishedByName string `json:"publishedByName"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type AIWorkflowValidationResponse struct {
Valid bool `json:"valid"`
Errors []workflowvalidator.Error `json:"errors"`
}
type AIWorkflowTemplateResponse struct {
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
Definition dsl.Definition `json:"definition"`
}
type AIWorkflowUsageResponse struct {
AIAgentID int64 `json:"aiAgentId"`
AIAgentName string `json:"aiAgentName"`
WorkflowVersionID int64 `json:"workflowVersionId"`
WorkflowVersion int `json:"workflowVersion"`
Enabled bool `json:"enabled"`
}
type AIWorkflowNodeSpecResponse struct {
Type string `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
Icon string `json:"icon"`
Category string `json:"category"`
Executable bool `json:"executable"`
RiskLevel workflowregistry.NodeRiskLevel `json:"riskLevel"`
Interruptible bool `json:"interruptible"`
RequiresConfirmationPredecessor bool `json:"requiresConfirmationPredecessor"`
ConfigSchema any `json:"configSchema,omitempty"`
InputSchema []workflowregistry.VariableSpec `json:"inputSchema,omitempty"`
OutputSchema []workflowregistry.VariableSpec `json:"outputSchema,omitempty"`
DefaultInputs map[string]dsl.Value `json:"defaultInputs,omitempty"`
}
type AIWorkflowRunResponse struct {
ID int64 `json:"id"`
WorkflowID int64 `json:"workflowId"`
WorkflowVersionID int64 `json:"workflowVersionId"`
WorkflowVersion int `json:"workflowVersion"`
WorkflowName string `json:"workflowName"`
ConversationID int64 `json:"conversationId"`
AIAgentID int64 `json:"aiAgentId"`
AIAgentName string `json:"aiAgentName"`
MessageID int64 `json:"messageId"`
Status int `json:"status"`
StatusName string `json:"statusName"`
StartedAt string `json:"startedAt"`
EndedAt string `json:"endedAt"`
DurationMS int64 `json:"durationMs"`
InterruptType string `json:"interruptType"`
InterruptNodeID string `json:"interruptNodeId"`
ErrorMessage string `json:"errorMessage"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Definition dsl.Definition `json:"definition"`
Nodes []AIWorkflowNodeRunResponse `json:"nodes,omitempty"`
}
type AIWorkflowNodeRunResponse struct {
ID int64 `json:"id"`
WorkflowRunID int64 `json:"workflowRunId"`
NodeID string `json:"nodeId"`
NodeType string `json:"nodeType"`
Status int `json:"status"`
StatusName string `json:"statusName"`
InputPreview string `json:"inputPreview"`
OutputPreview string `json:"outputPreview"`
ErrorMessage string `json:"errorMessage"`
StartedAt string `json:"startedAt"`
EndedAt string `json:"endedAt"`
DurationMS int `json:"durationMs"`
}
+10 -10
View File
@@ -4,18 +4,18 @@ import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type AssetResponse struct {
ID int64 `json:"id"`
AssetID string `json:"assetId"`
AssetID string `json:"asset_id"`
Provider enums.AssetProvider `json:"provider"`
Filename string `json:"filename"`
FileSize int64 `json:"fileSize"`
MimeType string `json:"mimeType"`
FileSize int64 `json:"file_size"`
MimeType string `json:"mime_type"`
Status enums.AssetStatus `json:"status"`
StorageKey string `json:"storageKey"`
StorageKey string `json:"storage_key"`
URL string `json:"url"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
CreateUserID int64 `json:"createUserId"`
CreateUserName string `json:"createUserName"`
UpdateUserID int64 `json:"updateUserId"`
UpdateUserName string `json:"updateUserName"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
CreateUserID int64 `json:"create_user_id"`
CreateUserName string `json:"create_user_name"`
UpdateUserID int64 `json:"update_user_id"`
UpdateUserName string `json:"update_user_name"`
}
+21 -21
View File
@@ -8,40 +8,40 @@ import (
type ChannelResponse struct {
ID int64 `json:"id"`
ChannelType string `json:"channelType"`
ChannelID string `json:"channelId"`
AIAgentID int64 `json:"aiAgentId"`
AIAgentRolloutPercent int `json:"aiAgentRolloutPercent"`
PreviousAIAgentRolloutPercent int `json:"previousAiAgentRolloutPercent"`
AIAgentName string `json:"aiAgentName,omitempty"`
ChannelType string `json:"channel_type"`
ChannelID string `json:"channel_id"`
AIAgentID int64 `json:"ai_agent_id"`
AIAgentRolloutPercent int `json:"ai_agent_rollout_percent"`
PreviousAIAgentRolloutPercent int `json:"previous_ai_agent_rollout_percent"`
AIAgentName string `json:"ai_agent_name,omitempty"`
Name string `json:"name"`
ConfigJSON string `json:"configJson"`
ConfigJSON string `json:"config_json"`
Status enums.Status `json:"status"`
Remark string `json:"remark"`
}
type WxWorkKFAccountResponse struct {
OpenKfID string `json:"openKfId"`
OpenKfID string `json:"open_kf_id"`
Name string `json:"name"`
Avatar string `json:"avatar"`
ManagePrivilege bool `json:"managePrivilege"`
ManagePrivilege bool `json:"manage_privilege"`
}
type ChannelMessageOutboxResponse struct {
ID int64 `json:"id"`
ChannelType string `json:"channelType"`
ConversationID int64 `json:"conversationId"`
MessageID int64 `json:"messageId"`
ChannelType string `json:"channel_type"`
ConversationID int64 `json:"conversation_id"`
MessageID int64 `json:"message_id"`
Payload string `json:"payload"`
SendStatus string `json:"sendStatus"`
RetryCount int `json:"retryCount"`
NextRetryAt string `json:"nextRetryAt"`
LastError string `json:"lastError"`
SentAt string `json:"sentAt"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
SendStatus string `json:"send_status"`
RetryCount int `json:"retry_count"`
NextRetryAt string `json:"next_retry_at"`
LastError string `json:"last_error"`
SentAt string `json:"sent_at"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
CreateUserName string `json:"create_user_name"`
UpdateUserName string `json:"update_user_name"`
}
func BuildChannelResponse(item *models.Channel) ChannelResponse {
@@ -1,14 +0,0 @@
package response
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type CompanyResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
CustomerCount int64 `json:"customerCount"`
Status enums.Status `json:"status"`
Remark string `json:"remark"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
@@ -2,49 +2,55 @@ package response
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type ConversationTagResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type ConversationParticipantResponse struct {
ID int64 `json:"id"`
ParticipantType string `json:"participantType"`
ParticipantID int64 `json:"participantId"`
ExternalParticipantID string `json:"externalParticipantId,omitempty"`
JoinedAt string `json:"joinedAt,omitempty"`
LeftAt string `json:"leftAt,omitempty"`
ParticipantType string `json:"participant_type"`
ParticipantID int64 `json:"participant_id"`
ExternalParticipantID string `json:"external_participant_id,omitempty"`
JoinedAt string `json:"joined_at,omitempty"`
LeftAt string `json:"left_at,omitempty"`
Status enums.Status `json:"status"`
}
type ConversationResponse struct {
ID int64 `json:"id"`
AIAgentID int64 `json:"aiAgentId"`
ChannelID int64 `json:"channelId"`
CustomerID int64 `json:"customerId"`
CustomerName string `json:"customerName"`
AIAgentID int64 `json:"ai_agent_id"`
ChannelID int64 `json:"channel_id"`
CustomerType string `json:"customer_type"`
CustomerID int64 `json:"customer_id"`
CustomerExternalID string `json:"customer_external_id"`
CustomerName string `json:"customer_name"`
Status enums.IMConversationStatus `json:"status"`
ServiceMode enums.IMConversationServiceMode `json:"serviceMode"`
ServiceMode enums.IMConversationServiceMode `json:"service_mode"`
Priority int `json:"priority"`
CurrentAssigneeID int64 `json:"currentAssigneeId"`
CurrentAssigneeName string `json:"currentAssigneeName,omitempty"`
CurrentTeamID int64 `json:"currentTeamId"`
CurrentTeamName string `json:"currentTeamName,omitempty"`
LastMessageID int64 `json:"lastMessageId"`
LastMessageAt string `json:"lastMessageAt,omitempty"`
LastActiveAt string `json:"lastActiveAt,omitempty"`
LastMessageSummary string `json:"lastMessageSummary,omitempty"`
CustomerUnreadCount int `json:"customerUnreadCount"`
AgentUnreadCount int `json:"agentUnreadCount"`
CustomerLastReadMessageID int64 `json:"customerLastReadMessageId"`
CustomerLastReadAt string `json:"customerLastReadAt,omitempty"`
AgentLastReadMessageID int64 `json:"agentLastReadMessageId"`
AgentLastReadAt string `json:"agentLastReadAt,omitempty"`
CustomerOnline bool `json:"customerOnline"`
ClosedAt string `json:"closedAt,omitempty"`
ClosedBy int64 `json:"closedBy"`
ClosedByName string `json:"closedByName,omitempty"`
CloseReason string `json:"closeReason,omitempty"`
CurrentAssigneeID int64 `json:"current_assignee_id"`
CurrentAssigneeName string `json:"current_assignee_name,omitempty"`
CurrentTeamID int64 `json:"current_team_id"`
CurrentTeamName string `json:"current_team_name,omitempty"`
LastMessageID int64 `json:"last_message_id"`
LastMessageAt string `json:"last_message_at,omitempty"`
LastActiveAt string `json:"last_active_at,omitempty"`
LastMessageSummary string `json:"last_message_summary,omitempty"`
CustomerUnreadCount int `json:"customer_unread_count"`
AgentUnreadCount int `json:"agent_unread_count"`
CustomerLastReadMessageID int64 `json:"customer_last_read_message_id"`
CustomerLastReadAt string `json:"customer_last_read_at,omitempty"`
AgentLastReadMessageID int64 `json:"agent_last_read_message_id"`
AgentLastReadAt string `json:"agent_last_read_at,omitempty"`
CustomerOnline bool `json:"customer_online"`
QueueEnteredAt string `json:"queue_entered_at,omitempty"`
QueuePosition int `json:"queue_position"`
QueueAheadCount int `json:"queue_ahead_count"`
QueueWaitingCount int `json:"queue_waiting_count"`
QueueWaitSeconds int64 `json:"queue_wait_seconds"`
QueueEstimatedWaitSeconds int64 `json:"queue_estimated_wait_seconds"`
QueueEscalationLevel int `json:"queue_escalation_level"`
EffectivePriority int `json:"effective_priority"`
QueueServiceOnline bool `json:"queue_service_online"`
ClosedAt string `json:"closed_at,omitempty"`
ClosedBy int64 `json:"closed_by"`
ClosedByName string `json:"closed_by_name,omitempty"`
CloseReason string `json:"close_reason,omitempty"`
}
type ConversationDetailResponse struct {
@@ -1,18 +0,0 @@
package response
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type CustomerContactResponse struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customerId"`
ContactType enums.ContactType `json:"contactType"`
ContactValue string `json:"contactValue"`
IsPrimary bool `json:"isPrimary"`
IsVerified bool `json:"isVerified"`
VerifiedAt string `json:"verifiedAt,omitempty"`
Source string `json:"source"`
Status enums.Status `json:"status"`
Remark string `json:"remark"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
@@ -0,0 +1,12 @@
package response
type CustomerQuickActionResponse struct {
Code string `json:"code"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
}
type CustomerQuickActionExecutionResponse struct {
CustomerMessage MessageResponse `json:"customer_message"`
ReplyMessage MessageResponse `json:"reply_message"`
}
@@ -1,18 +0,0 @@
package response
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type CustomerResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Gender enums.Gender `json:"gender"`
CompanyID int64 `json:"companyId"`
Company *CompanyResponse `json:"company"`
LastActiveAt string `json:"lastActiveAt"`
PrimaryMobile string `json:"primaryMobile"`
PrimaryEmail string `json:"primaryEmail"`
Status enums.Status `json:"status"`
Remark string `json:"remark"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
+35 -35
View File
@@ -2,25 +2,25 @@ package response
type DashboardOverviewResponse struct {
Range string `json:"range"`
GeneratedAt string `json:"generatedAt"`
GeneratedAt string `json:"generated_at"`
Summary DashboardSummaryResponse `json:"summary"`
ConversationStats DashboardSectionStatsResponse `json:"conversationStats"`
AgentStats DashboardAgentStatsResponse `json:"agentStats"`
AIStats DashboardAIStatsResponse `json:"aiStats"`
ConversationStats DashboardSectionStatsResponse `json:"conversation_stats"`
AgentStats DashboardAgentStatsResponse `json:"agent_stats"`
AIStats DashboardAIStatsResponse `json:"ai_stats"`
Alerts []DashboardAlertResponse `json:"alerts"`
QuickLinks []DashboardQuickLinkResponse `json:"quickLinks"`
QuickLinks []DashboardQuickLinkResponse `json:"quick_links"`
}
type DashboardSummaryResponse struct {
TodayNewConversations int64 `json:"todayNewConversations"`
ProcessingConversations int64 `json:"processingConversations"`
PendingDispatchConversations int64 `json:"pendingDispatchConversations"`
OnlineAgents int64 `json:"onlineAgents"`
AIServiceRate float64 `json:"aiServiceRate"`
TodayNewConversations int64 `json:"today_new_conversations"`
ProcessingConversations int64 `json:"processing_conversations"`
PendingDispatchConversations int64 `json:"pending_dispatch_conversations"`
OnlineAgents int64 `json:"online_agents"`
AIServiceRate float64 `json:"ai_service_rate"`
}
type DashboardSectionStatsResponse struct {
StatusDistribution []DashboardStatusDistributionItem `json:"statusDistribution"`
StatusDistribution []DashboardStatusDistributionItem `json:"status_distribution"`
Trend []DashboardTrendItem `json:"trend"`
}
@@ -32,39 +32,39 @@ type DashboardStatusDistributionItem struct {
type DashboardTrendItem struct {
Date string `json:"date"`
NewCount int64 `json:"newCount"`
ClosedCount int64 `json:"closedCount"`
NewCount int64 `json:"new_count"`
ClosedCount int64 `json:"closed_count"`
}
type DashboardAgentStatsResponse struct {
OnlineAgents int64 `json:"onlineAgents"`
BusyAgents int64 `json:"busyAgents"`
OfflineAgents int64 `json:"offlineAgents"`
TeamLoads []DashboardTeamLoadResponse `json:"teamLoads"`
OnlineAgents int64 `json:"online_agents"`
BusyAgents int64 `json:"busy_agents"`
OfflineAgents int64 `json:"offline_agents"`
TeamLoads []DashboardTeamLoadResponse `json:"team_loads"`
}
type DashboardTeamLoadResponse struct {
TeamID int64 `json:"teamId"`
TeamName string `json:"teamName"`
TotalAgents int64 `json:"totalAgents"`
OnlineAgents int64 `json:"onlineAgents"`
BusyAgents int64 `json:"busyAgents"`
OfflineAgents int64 `json:"offlineAgents"`
WaitingConversations int64 `json:"waitingConversations"`
ProcessingConversations int64 `json:"processingConversations"`
MaxConcurrentCapacity int64 `json:"maxConcurrentCapacity"`
LoadRate float64 `json:"loadRate"`
HasScheduleNow bool `json:"hasScheduleNow"`
TeamID int64 `json:"team_id"`
TeamName string `json:"team_name"`
TotalAgents int64 `json:"total_agents"`
OnlineAgents int64 `json:"online_agents"`
BusyAgents int64 `json:"busy_agents"`
OfflineAgents int64 `json:"offline_agents"`
WaitingConversations int64 `json:"waiting_conversations"`
ProcessingConversations int64 `json:"processing_conversations"`
MaxConcurrentCapacity int64 `json:"max_concurrent_capacity"`
LoadRate float64 `json:"load_rate"`
HasScheduleNow bool `json:"has_schedule_now"`
}
type DashboardAIStatsResponse struct {
EnabledAIAgents int64 `json:"enabledAiAgents"`
EnabledChannels int64 `json:"enabledChannels"`
TodayKnowledgeRetrieves int64 `json:"todayKnowledgeRetrieves"`
TodayKnowledgeRetrieveFailCount int64 `json:"todayKnowledgeRetrieveFailCount"`
TodayKnowledgeRetrieveFailRate float64 `json:"todayKnowledgeRetrieveFailRate"`
TodayAgentRunFailCount int64 `json:"todayAgentRunFailCount"`
TodayAIHandoffCount int64 `json:"todayAiHandoffCount"`
EnabledAIAgents int64 `json:"enabled_ai_agents"`
EnabledChannels int64 `json:"enabled_channels"`
TodayKnowledgeRetrieves int64 `json:"today_knowledge_retrieves"`
TodayKnowledgeRetrieveFailCount int64 `json:"today_knowledge_retrieve_fail_count"`
TodayKnowledgeRetrieveFailRate float64 `json:"today_knowledge_retrieve_fail_rate"`
TodayAgentRunFailCount int64 `json:"today_agent_run_fail_count"`
TodayAIHandoffCount int64 `json:"today_ai_handoff_count"`
}
type DashboardAlertResponse struct {
@@ -0,0 +1,40 @@
package response
import (
"go/ast"
"go/parser"
"go/token"
"reflect"
"strings"
"testing"
"unicode"
)
func TestPublicResponseJSONTagsUseSnakeCase(t *testing.T) {
packages, err := parser.ParseDir(token.NewFileSet(), ".", nil, 0)
if err != nil {
t.Fatalf("parse response package: %v", err)
}
pkg := packages["response"]
if pkg == nil {
t.Fatal("response package not found")
}
for filename, file := range pkg.Files {
ast.Inspect(file, func(node ast.Node) bool {
field, ok := node.(*ast.Field)
if !ok || field.Tag == nil {
return true
}
tag := reflect.StructTag(strings.Trim(field.Tag.Value, "`"))
name := strings.Split(tag.Get("json"), ",")[0]
for _, char := range name {
if unicode.IsUpper(char) {
t.Errorf("%s has non-snake-case JSON tag %q", filename, name)
break
}
}
return true
})
}
}
+153 -153
View File
@@ -9,109 +9,109 @@ type KnowledgeBaseResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
KnowledgeType string `json:"knowledgeType"`
KnowledgeTypeName string `json:"knowledgeTypeName"`
KnowledgeType string `json:"knowledge_type"`
KnowledgeTypeName string `json:"knowledge_type_name"`
Status enums.Status `json:"status"`
StatusName string `json:"statusName"`
DefaultTopK int `json:"defaultTopK"`
DefaultScoreThreshold float64 `json:"defaultScoreThreshold"`
DefaultRerankLimit int `json:"defaultRerankLimit"`
ChunkProvider string `json:"chunkProvider"`
ChunkTargetTokens int `json:"chunkTargetTokens"`
ChunkMaxTokens int `json:"chunkMaxTokens"`
ChunkOverlapTokens int `json:"chunkOverlapTokens"`
AnswerMode int `json:"answerMode"`
AnswerModeName string `json:"answerModeName"`
DocumentCount int64 `json:"documentCount"`
FAQCount int64 `json:"faqCount"`
StatusName string `json:"status_name"`
DefaultTopK int `json:"default_top_k"`
DefaultScoreThreshold float64 `json:"default_score_threshold"`
DefaultRerankLimit int `json:"default_rerank_limit"`
ChunkProvider string `json:"chunk_provider"`
ChunkTargetTokens int `json:"chunk_target_tokens"`
ChunkMaxTokens int `json:"chunk_max_tokens"`
ChunkOverlapTokens int `json:"chunk_overlap_tokens"`
AnswerMode int `json:"answer_mode"`
AnswerModeName string `json:"answer_mode_name"`
DocumentCount int64 `json:"document_count"`
FAQCount int64 `json:"faq_count"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreateUserName string `json:"create_user_name"`
UpdateUserName string `json:"update_user_name"`
}
type KnowledgeDocumentResponse struct {
ID int64 `json:"id"`
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
KnowledgeBaseName string `json:"knowledgeBaseName,omitempty"`
DirectoryID int64 `json:"directoryId"`
DirectoryName string `json:"directoryName,omitempty"`
DirectoryPath string `json:"directoryPath,omitempty"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
KnowledgeBaseName string `json:"knowledge_base_name,omitempty"`
DirectoryID int64 `json:"directory_id"`
DirectoryName string `json:"directory_name,omitempty"`
DirectoryPath string `json:"directory_path,omitempty"`
Title string `json:"title"`
ContentType enums.KnowledgeDocumentContentType `json:"contentType"`
ContentType enums.KnowledgeDocumentContentType `json:"content_type"`
Content string `json:"content"`
Status enums.Status `json:"status"`
StatusName string `json:"statusName"`
IndexStatus enums.KnowledgeDocumentIndexStatus `json:"indexStatus"`
IndexStatusName string `json:"indexStatusName"`
IndexedAt *time.Time `json:"indexedAt"`
IndexError string `json:"indexError"`
ContentHash string `json:"contentHash"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
StatusName string `json:"status_name"`
IndexStatus enums.KnowledgeDocumentIndexStatus `json:"index_status"`
IndexStatusName string `json:"index_status_name"`
IndexedAt *time.Time `json:"indexed_at"`
IndexError string `json:"index_error"`
ContentHash string `json:"content_hash"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreateUserName string `json:"create_user_name"`
UpdateUserName string `json:"update_user_name"`
}
type KnowledgeDocumentListResponse struct {
ID int64 `json:"id"`
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
KnowledgeBaseName string `json:"knowledgeBaseName,omitempty"`
DirectoryID int64 `json:"directoryId"`
DirectoryName string `json:"directoryName,omitempty"`
DirectoryPath string `json:"directoryPath,omitempty"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
KnowledgeBaseName string `json:"knowledge_base_name,omitempty"`
DirectoryID int64 `json:"directory_id"`
DirectoryName string `json:"directory_name,omitempty"`
DirectoryPath string `json:"directory_path,omitempty"`
Title string `json:"title"`
ContentType enums.KnowledgeDocumentContentType `json:"contentType"`
ContentType enums.KnowledgeDocumentContentType `json:"content_type"`
Status enums.Status `json:"status"`
StatusName string `json:"statusName"`
IndexStatus enums.KnowledgeDocumentIndexStatus `json:"indexStatus"`
IndexStatusName string `json:"indexStatusName"`
IndexedAt *time.Time `json:"indexedAt"`
IndexError string `json:"indexError"`
ContentHash string `json:"contentHash"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
StatusName string `json:"status_name"`
IndexStatus enums.KnowledgeDocumentIndexStatus `json:"index_status"`
IndexStatusName string `json:"index_status_name"`
IndexedAt *time.Time `json:"indexed_at"`
IndexError string `json:"index_error"`
ContentHash string `json:"content_hash"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreateUserName string `json:"create_user_name"`
UpdateUserName string `json:"update_user_name"`
}
type KnowledgeFAQResponse struct {
ID int64 `json:"id"`
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
KnowledgeBaseName string `json:"knowledgeBaseName,omitempty"`
DirectoryID int64 `json:"directoryId"`
DirectoryName string `json:"directoryName,omitempty"`
DirectoryPath string `json:"directoryPath,omitempty"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
KnowledgeBaseName string `json:"knowledge_base_name,omitempty"`
DirectoryID int64 `json:"directory_id"`
DirectoryName string `json:"directory_name,omitempty"`
DirectoryPath string `json:"directory_path,omitempty"`
Question string `json:"question"`
Answer string `json:"answer"`
SimilarQuestions []string `json:"similarQuestions"`
SimilarQuestions []string `json:"similar_questions"`
Status enums.Status `json:"status"`
StatusName string `json:"statusName"`
IndexStatus enums.KnowledgeDocumentIndexStatus `json:"indexStatus"`
IndexStatusName string `json:"indexStatusName"`
IndexedAt *time.Time `json:"indexedAt"`
IndexError string `json:"indexError"`
StatusName string `json:"status_name"`
IndexStatus enums.KnowledgeDocumentIndexStatus `json:"index_status"`
IndexStatusName string `json:"index_status_name"`
IndexedAt *time.Time `json:"indexed_at"`
IndexError string `json:"index_error"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreateUserName string `json:"create_user_name"`
UpdateUserName string `json:"update_user_name"`
}
type KnowledgeDirectoryResponse struct {
ID int64 `json:"id"`
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
ParentID int64 `json:"parentId"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
ParentID int64 `json:"parent_id"`
Name string `json:"name"`
SortNo int `json:"sortNo"`
SortNo int `json:"sort_no"`
Status enums.Status `json:"status"`
StatusName string `json:"statusName"`
StatusName string `json:"status_name"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreateUserName string `json:"create_user_name"`
UpdateUserName string `json:"update_user_name"`
Children []KnowledgeDirectoryResponse `json:"children"`
}
@@ -136,116 +136,116 @@ type KnowledgeFAQImportResult struct {
}
type KnowledgeSearchResult struct {
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
ChunkID int64 `json:"chunkId"`
DocumentID int64 `json:"documentId"`
DocumentTitle string `json:"documentTitle"`
FaqID int64 `json:"faqId"`
FaqQuestion string `json:"faqQuestion"`
ChunkNo int `json:"chunkNo"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
ChunkID int64 `json:"chunk_id"`
DocumentID int64 `json:"document_id"`
DocumentTitle string `json:"document_title"`
FaqID int64 `json:"faq_id"`
FaqQuestion string `json:"faq_question"`
ChunkNo int `json:"chunk_no"`
Title string `json:"title"`
SectionPath string `json:"sectionPath"`
SectionPath string `json:"section_path"`
Content string `json:"content"`
Score float64 `json:"score"`
RerankScore float64 `json:"rerankScore"`
RerankScore float64 `json:"rerank_score"`
}
type KnowledgeSearchResponse struct {
Question string `json:"question"`
Results []KnowledgeSearchResult `json:"results"`
HitCount int `json:"hitCount"`
LatencyMs int64 `json:"latencyMs"`
HitCount int `json:"hit_count"`
LatencyMs int64 `json:"latency_ms"`
}
type KnowledgeAnswerResponse struct {
Question string `json:"question"`
RewriteQuestion string `json:"rewriteQuestion,omitempty"`
RewriteQuestion string `json:"rewrite_question,omitempty"`
Answer string `json:"answer"`
AnswerStatus int `json:"answerStatus"`
AnswerStatusName string `json:"answerStatusName"`
AnswerStatus int `json:"answer_status"`
AnswerStatusName string `json:"answer_status_name"`
Citations []KnowledgeCitation `json:"citations"`
Hits []KnowledgeSearchResult `json:"hits"`
HitCount int `json:"hitCount"`
TopScore float64 `json:"topScore"`
LatencyMs int64 `json:"latencyMs"`
RetrieveMs int64 `json:"retrieveMs"`
GenerateMs int64 `json:"generateMs"`
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
ModelName string `json:"modelName"`
RetrieveLogID int64 `json:"retrieveLogId"`
HitCount int `json:"hit_count"`
TopScore float64 `json:"top_score"`
LatencyMs int64 `json:"latency_ms"`
RetrieveMs int64 `json:"retrieve_ms"`
GenerateMs int64 `json:"generate_ms"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
ModelName string `json:"model_name"`
RetrieveLogID int64 `json:"retrieve_log_id"`
}
type KnowledgeCitation struct {
DocumentID int64 `json:"documentId"`
DocumentTitle string `json:"documentTitle"`
FaqID int64 `json:"faqId"`
FaqQuestion string `json:"faqQuestion"`
ChunkNo int `json:"chunkNo"`
DocumentID int64 `json:"document_id"`
DocumentTitle string `json:"document_title"`
FaqID int64 `json:"faq_id"`
FaqQuestion string `json:"faq_question"`
ChunkNo int `json:"chunk_no"`
Title string `json:"title"`
SectionPath string `json:"sectionPath"`
SectionPath string `json:"section_path"`
Snippet string `json:"snippet"`
Score float64 `json:"score"`
}
type KnowledgeRetrieveLogResponse struct {
ID int64 `json:"id"`
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
KnowledgeBaseName string `json:"knowledgeBaseName,omitempty"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
KnowledgeBaseName string `json:"knowledge_base_name,omitempty"`
Channel string `json:"channel"`
ChannelName string `json:"channelName"`
ChannelName string `json:"channel_name"`
Scene string `json:"scene"`
SceneName string `json:"sceneName"`
SessionID string `json:"sessionId"`
ConversationID int64 `json:"conversationId"`
RequestID string `json:"requestId"`
SceneName string `json:"scene_name"`
SessionID string `json:"session_id"`
ConversationID int64 `json:"conversation_id"`
RequestID string `json:"request_id"`
Question string `json:"question"`
RewriteQuestion string `json:"rewriteQuestion"`
RewriteQuestion string `json:"rewrite_question"`
Answer string `json:"answer"`
AnswerStatus int `json:"answerStatus"`
AnswerStatusName string `json:"answerStatusName"`
HitCount int `json:"hitCount"`
TopScore float64 `json:"topScore"`
ChunkProvider string `json:"chunkProvider"`
ChunkTargetTokens int `json:"chunkTargetTokens"`
ChunkMaxTokens int `json:"chunkMaxTokens"`
ChunkOverlapTokens int `json:"chunkOverlapTokens"`
RerankEnabled bool `json:"rerankEnabled"`
RerankLimit int `json:"rerankLimit"`
CitationCount int `json:"citationCount"`
UsedChunkCount int `json:"usedChunkCount"`
LatencyMs int64 `json:"latencyMs"`
RetrieveMs int64 `json:"retrieveMs"`
GenerateMs int64 `json:"generateMs"`
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
ModelName string `json:"modelName"`
TraceData string `json:"traceData"`
CreatedAt time.Time `json:"createdAt"`
AnswerStatus int `json:"answer_status"`
AnswerStatusName string `json:"answer_status_name"`
HitCount int `json:"hit_count"`
TopScore float64 `json:"top_score"`
ChunkProvider string `json:"chunk_provider"`
ChunkTargetTokens int `json:"chunk_target_tokens"`
ChunkMaxTokens int `json:"chunk_max_tokens"`
ChunkOverlapTokens int `json:"chunk_overlap_tokens"`
RerankEnabled bool `json:"rerank_enabled"`
RerankLimit int `json:"rerank_limit"`
CitationCount int `json:"citation_count"`
UsedChunkCount int `json:"used_chunk_count"`
LatencyMs int64 `json:"latency_ms"`
RetrieveMs int64 `json:"retrieve_ms"`
GenerateMs int64 `json:"generate_ms"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
ModelName string `json:"model_name"`
TraceData string `json:"trace_data"`
CreatedAt time.Time `json:"created_at"`
}
type KnowledgeRetrieveHitResponse struct {
ID int64 `json:"id"`
RetrieveLogID int64 `json:"retrieveLogId"`
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
ChunkID int64 `json:"chunkId"`
DocumentID int64 `json:"documentId"`
DocumentTitle string `json:"documentTitle"`
FaqID int64 `json:"faqId"`
FaqQuestion string `json:"faqQuestion"`
ChunkNo int `json:"chunkNo"`
RetrieveLogID int64 `json:"retrieve_log_id"`
KnowledgeBaseID int64 `json:"knowledge_base_id"`
ChunkID int64 `json:"chunk_id"`
DocumentID int64 `json:"document_id"`
DocumentTitle string `json:"document_title"`
FaqID int64 `json:"faq_id"`
FaqQuestion string `json:"faq_question"`
ChunkNo int `json:"chunk_no"`
Title string `json:"title"`
SectionPath string `json:"sectionPath"`
ChunkType string `json:"chunkType"`
ChunkTypeName string `json:"chunkTypeName"`
SectionPath string `json:"section_path"`
ChunkType string `json:"chunk_type"`
ChunkTypeName string `json:"chunk_type_name"`
Provider string `json:"provider"`
RankNo int `json:"rankNo"`
RankNo int `json:"rank_no"`
Score float64 `json:"score"`
RerankScore float64 `json:"rerankScore"`
UsedInAnswer bool `json:"usedInAnswer"`
IsCitation bool `json:"isCitation"`
RerankScore float64 `json:"rerank_score"`
UsedInAnswer bool `json:"used_in_answer"`
IsCitation bool `json:"is_citation"`
Snippet string `json:"snippet"`
CreatedAt time.Time `json:"createdAt"`
CreatedAt time.Time `json:"created_at"`
}
type KnowledgeRetrieveLogDetailResponse struct {
@@ -255,12 +255,12 @@ type KnowledgeRetrieveLogDetailResponse struct {
type KnowledgeFeedbackResponse struct {
ID int64 `json:"id"`
RetrieveLogID int64 `json:"retrieveLogId"`
FeedbackType int `json:"feedbackType"`
FeedbackTypeName string `json:"feedbackTypeName"`
FeedbackReason string `json:"feedbackReason"`
UserID int64 `json:"userId"`
AgentID int64 `json:"agentId"`
RetrieveLogID int64 `json:"retrieve_log_id"`
FeedbackType int `json:"feedback_type"`
FeedbackTypeName string `json:"feedback_type_name"`
FeedbackReason string `json:"feedback_reason"`
UserID int64 `json:"user_id"`
AgentID int64 `json:"agent_id"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
CreatedAt time.Time `json:"created_at"`
}
-121
View File
@@ -1,121 +0,0 @@
package response
import (
"code.tczkiot.com/wlw/ai-agent/internal/ai/mcps"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
)
type MCPConnectionResponse struct {
ServerCode string `json:"serverCode"`
Endpoint string `json:"endpoint"`
Protocol string `json:"protocol"`
ServerName string `json:"serverName"`
Version string `json:"version"`
}
func BuildMCPConnectionResponse(item *mcps.ConnectionResult) *MCPConnectionResponse {
if item == nil {
return nil
}
return &MCPConnectionResponse{
ServerCode: item.ServerCode,
Endpoint: item.Endpoint,
Protocol: item.Protocol,
ServerName: item.ServerName,
Version: item.Version,
}
}
type MCPServerInfoResponse struct {
Code string `json:"code"`
Enabled bool `json:"enabled"`
Endpoint string `json:"endpoint"`
TimeoutMS int `json:"timeoutMs"`
}
func BuildMCPServerInfoResponses(items []mcps.ServerInfo) []MCPServerInfoResponse {
ret := make([]MCPServerInfoResponse, 0, len(items))
for _, item := range items {
ret = append(ret, MCPServerInfoResponse{
Code: item.Code,
Enabled: item.Enabled,
Endpoint: item.Endpoint,
TimeoutMS: item.TimeoutMS,
})
}
return ret
}
type MCPToolInfoResponse struct {
Name string `json:"name"`
Title string `json:"title"`
Description string `json:"description"`
InputSchema any `json:"inputSchema"`
OutputSchema any `json:"outputSchema,omitempty"`
ReadOnlyHint bool `json:"readOnlyHint"`
}
func BuildMCPToolInfoResponses(items []mcps.ToolInfo) []MCPToolInfoResponse {
ret := make([]MCPToolInfoResponse, 0, len(items))
for _, item := range items {
ret = append(ret, MCPToolInfoResponse{
Name: item.Name,
Title: item.Title,
Description: item.Description,
InputSchema: item.InputSchema,
OutputSchema: item.OutputSchema,
ReadOnlyHint: item.ReadOnlyHint,
})
}
return ret
}
type MCPToolCatalogResponse struct {
ToolCode string `json:"toolCode"`
ServerCode string `json:"serverCode"`
ToolName string `json:"toolName"`
SourceType enums.ToolSourceType `json:"sourceType"`
AutoInjected bool `json:"autoInjected"`
Title string `json:"title"`
Description string `json:"description"`
InputSchema any `json:"inputSchema"`
OutputSchema any `json:"outputSchema,omitempty"`
RiskLevel string `json:"riskLevel"`
RequireConfirmation bool `json:"requireConfirmation"`
RiskEditable bool `json:"riskEditable"`
}
type MCPToolResultContentResponse struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Data any `json:"data,omitempty"`
}
type MCPCallToolResponse struct {
ServerCode string `json:"serverCode"`
ToolName string `json:"toolName"`
IsError bool `json:"isError"`
Content []MCPToolResultContentResponse `json:"content"`
StructuredContent any `json:"structuredContent,omitempty"`
}
func BuildMCPCallToolResponse(item *mcps.ToolCallResult) *MCPCallToolResponse {
if item == nil {
return nil
}
content := make([]MCPToolResultContentResponse, 0, len(item.Content))
for _, c := range item.Content {
content = append(content, MCPToolResultContentResponse{
Type: c.Type,
Text: c.Text,
Data: c.Data,
})
}
return &MCPCallToolResponse{
ServerCode: item.ServerCode,
ToolName: item.ToolName,
IsError: item.IsError,
Content: content,
StructuredContent: item.StructuredContent,
}
}
+18 -19
View File
@@ -4,25 +4,24 @@ import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type MessageResponse struct {
ID int64 `json:"id"`
ConversationID int64 `json:"conversationId"`
RequestID string `json:"requestId,omitempty"`
WorkflowRunID int64 `json:"workflowRunId,omitempty"`
ClientMsgID string `json:"clientMsgId,omitempty"`
SenderType enums.IMSenderType `json:"senderType"`
SenderID int64 `json:"senderId"`
SenderName string `json:"senderName,omitempty"`
SenderAvatar string `json:"senderAvatar,omitempty"`
MessageType enums.IMMessageType `json:"messageType"`
ConversationID int64 `json:"conversation_id"`
RequestID string `json:"request_id,omitempty"`
ClientMsgID string `json:"client_msg_id,omitempty"`
SenderType enums.IMSenderType `json:"sender_type"`
SenderID int64 `json:"sender_id"`
SenderName string `json:"sender_name,omitempty"`
SenderAvatar string `json:"sender_avatar,omitempty"`
MessageType enums.IMMessageType `json:"message_type"`
Content string `json:"content"`
Payload string `json:"payload,omitempty"`
SendStatus enums.IMMessageStatus `json:"sendStatus"`
SentAt string `json:"sentAt,omitempty"`
DeliveredAt string `json:"deliveredAt,omitempty"`
ReadAt string `json:"readAt,omitempty"`
CustomerRead bool `json:"customerRead"`
CustomerReadAt string `json:"customerReadAt,omitempty"`
AgentRead bool `json:"agentRead"`
AgentReadAt string `json:"agentReadAt,omitempty"`
RecalledAt string `json:"recalledAt,omitempty"`
QuotedMessageID int64 `json:"quotedMessageId,omitempty"`
SendStatus enums.IMMessageStatus `json:"send_status"`
SentAt string `json:"sent_at,omitempty"`
DeliveredAt string `json:"delivered_at,omitempty"`
ReadAt string `json:"read_at,omitempty"`
CustomerRead bool `json:"customer_read"`
CustomerReadAt string `json:"customer_read_at,omitempty"`
AgentRead bool `json:"agent_read"`
AgentReadAt string `json:"agent_read_at,omitempty"`
RecalledAt string `json:"recalled_at,omitempty"`
QuotedMessageID int64 `json:"quoted_message_id,omitempty"`
}
@@ -2,17 +2,17 @@ package response
type NotificationResponse struct {
ID int64 `json:"id"`
RecipientUserID int64 `json:"recipientUserId"`
RecipientUserID int64 `json:"recipient_user_id"`
Title string `json:"title"`
Content string `json:"content"`
NotificationType string `json:"notificationType"`
BizType string `json:"bizType"`
BizID int64 `json:"bizId"`
ActionURL string `json:"actionUrl"`
ReadAt string `json:"readAt,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
NotificationType string `json:"notification_type"`
BizType string `json:"biz_type"`
BizID int64 `json:"biz_id"`
ActionURL string `json:"action_url"`
ReadAt string `json:"read_at,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}
type NotificationUnreadCountResponse struct {
UnreadCount int64 `json:"unreadCount"`
UnreadCount int64 `json:"unread_count"`
}
@@ -4,10 +4,10 @@ import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type QuickReplyResponse struct {
ID int64 `json:"id"`
GroupName string `json:"groupName"`
GroupName string `json:"group_name"`
Title string `json:"title"`
Content string `json:"content"`
Status enums.Status `json:"status"`
SortNo int `json:"sortNo"`
CreatedBy int64 `json:"createdBy"`
SortNo int `json:"sort_no"`
CreatedBy int64 `json:"created_by"`
}
@@ -1,34 +0,0 @@
package response
import "time"
type SkillDefinitionResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Instruction string `json:"instruction"`
Examples []string `json:"examples"`
ToolWhitelist []string `json:"toolWhitelist"`
Status int `json:"status"`
StatusName string `json:"statusName"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreateUserName string `json:"createUserName"`
UpdateUserName string `json:"updateUserName"`
}
type SkillDebugRunResponse struct {
SkillDefinitionID int64 `json:"skillDefinitionId"`
SkillName string `json:"skillName"`
ReplyText string `json:"replyText"`
ToolWhitelist []string `json:"toolWhitelist"`
InvokedToolCodes []string `json:"invokedToolCodes"`
InterruptType string `json:"interruptType"`
CheckPointID string `json:"checkPointId"`
Interrupted bool `json:"interrupted"`
TraceData string `json:"traceData"`
ErrorMessage string `json:"errorMessage"`
ConversationID int64 `json:"conversationId"`
AIAgentID int64 `json:"aiAgentId"`
}
-26
View File
@@ -1,26 +0,0 @@
package response
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type TagResponse struct {
ID int64 `json:"id"`
ParentID int64 `json:"parentId"`
Name string `json:"name"`
Remark string `json:"remark"`
SortNo int `json:"sortNo"`
Status enums.Status `json:"status"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type TagTreeResponse struct {
ID int64 `json:"id"`
ParentID int64 `json:"parentId"`
Name string `json:"name"`
Remark string `json:"remark"`
SortNo int `json:"sortNo"`
Status enums.Status `json:"status"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Children []*TagTreeResponse `json:"children"`
}
@@ -1,55 +0,0 @@
package response
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
type TicketProgressResponse struct {
ID int64 `json:"id"`
TicketID int64 `json:"ticketId"`
Content string `json:"content"`
AuthorID int64 `json:"authorId"`
AuthorName string `json:"authorName,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
}
type TicketResponse struct {
ID int64 `json:"id"`
TicketNo string `json:"ticketNo"`
Title string `json:"title"`
Description string `json:"description"`
Source enums.TicketSource `json:"source"`
Channel string `json:"channel"`
CustomerID int64 `json:"customerId"`
ConversationID int64 `json:"conversationId"`
Tags []TagResponse `json:"tags,omitempty"`
Status enums.TicketStatus `json:"status"`
CurrentAssigneeID int64 `json:"currentAssigneeId"`
CurrentAssigneeName string `json:"currentAssigneeName,omitempty"`
CreatedBy int64 `json:"createdBy"`
CreatedByName string `json:"createdByName,omitempty"`
HandledAt string `json:"handledAt,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
Customer *CustomerResponse `json:"customer,omitempty"`
}
type TicketDetailResponse struct {
Ticket TicketResponse `json:"ticket"`
Progresses []TicketProgressResponse `json:"progresses,omitempty"`
}
type TicketSummaryResponse struct {
All int64 `json:"all"`
Pending int64 `json:"pending"`
InProgress int64 `json:"inProgress"`
Done int64 `json:"done"`
Unassigned int64 `json:"unassigned"`
Mine int64 `json:"mine"`
Stale int64 `json:"stale"`
}
type TicketViewResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Filters map[string]any `json:"filters,omitempty"`
SortNo int `json:"sortNo"`
}
+3 -3
View File
@@ -1,11 +1,11 @@
package response
type WidgetConfigResponse struct {
ChannelID string `json:"channelId"`
ChannelType string `json:"channelType"`
ChannelID string `json:"channel_id"`
ChannelType string `json:"channel_type"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
ThemeColor string `json:"themeColor"`
ThemeColor string `json:"theme_color"`
Position string `json:"position"`
Width string `json:"width"`
}
+15 -14
View File
@@ -240,20 +240,21 @@ func GetAIAgentFallbackModeLabel(mode AIAgentFallbackMode) string {
}
const (
IMRealtimeEventConnected = "connected"
IMRealtimeEventPong = "pong"
IMRealtimeEventSubscribed = "subscribed"
IMRealtimeEventUnsubscribed = "unsubscribed"
IMRealtimeEventResyncRequired = "resyncRequired"
IMRealtimeEventMessageCreated = "message.created"
IMRealtimeEventMessageRecalled = "message.recalled"
IMRealtimeEventConversationCreated = "conversation.created"
IMRealtimeEventConversationUpdated = "conversation.updated"
IMRealtimeEventConversationAssigned = "conversation.assigned"
IMRealtimeEventConversationTransferred = "conversation.transferred"
IMRealtimeEventConversationClosed = "conversation.closed"
IMRealtimeEventConversationRead = "conversation.read"
IMRealtimeEventNotificationCreated = "notification.created"
IMRealtimeEventConnected = "connected"
IMRealtimeEventPong = "pong"
IMRealtimeEventSubscribed = "subscribed"
IMRealtimeEventUnsubscribed = "unsubscribed"
IMRealtimeEventResyncRequired = "resyncRequired"
IMRealtimeEventMessageCreated = "message.created"
IMRealtimeEventMessageRecalled = "message.recalled"
IMRealtimeEventConversationCreated = "conversation.created"
IMRealtimeEventConversationUpdated = "conversation.updated"
IMRealtimeEventConversationAssigned = "conversation.assigned"
IMRealtimeEventConversationTransferred = "conversation.transferred"
IMRealtimeEventConversationClosed = "conversation.closed"
IMRealtimeEventConversationRead = "conversation.read"
IMRealtimeEventConversationQueueUpdated = "conversation.queue_updated"
IMRealtimeEventNotificationCreated = "notification.created"
)
const (
-16
View File
@@ -1,21 +1,5 @@
package enums
type VectorDBType string
const (
VectorDBTypeQdrant VectorDBType = "qdrant"
VectorDBTypeLanceDB VectorDBType = "lancedb"
)
var vectorDBTypeLabelMap = map[VectorDBType]string{
VectorDBTypeQdrant: "Qdrant",
VectorDBTypeLanceDB: "LanceDB",
}
func GetVectorDBTypeLabel(dbType VectorDBType) string {
return vectorDBTypeLabelMap[dbType]
}
type KnowledgeDocumentContentType string
const (
-12
View File
@@ -1,12 +0,0 @@
package enums
import "testing"
func TestVectorDBTypeLabelIncludesLanceDB(t *testing.T) {
if VectorDBTypeLanceDB != "lancedb" {
t.Fatalf("VectorDBTypeLanceDB = %q, want %q", VectorDBTypeLanceDB, "lancedb")
}
if got := GetVectorDBTypeLabel(VectorDBTypeLanceDB); got != "LanceDB" {
t.Fatalf("GetVectorDBTypeLabel(VectorDBTypeLanceDB) = %q, want %q", got, "LanceDB")
}
}
-55
View File
@@ -1,55 +0,0 @@
package enums
type TicketStatus string
const (
TicketStatusPending TicketStatus = "pending"
TicketStatusInProgress TicketStatus = "in_progress"
TicketStatusDone TicketStatus = "done"
)
var TicketStatusValues = []TicketStatus{
TicketStatusPending,
TicketStatusInProgress,
TicketStatusDone,
}
var ticketStatusLabelMap = map[TicketStatus]string{
TicketStatusPending: "待处理",
TicketStatusInProgress: "处理中",
TicketStatusDone: "已处理",
}
func GetTicketStatusLabel(status TicketStatus) string {
return ticketStatusLabelMap[status]
}
func IsValidTicketStatus(status string) bool {
for _, item := range TicketStatusValues {
if string(item) == status {
return true
}
}
return false
}
type TicketSource string
const (
TicketSourceManual TicketSource = "manual"
TicketSourceConversation TicketSource = "conversation"
)
var TicketSourceValues = []TicketSource{
TicketSourceManual,
TicketSourceConversation,
}
func IsValidTicketSource(source string) bool {
for _, item := range TicketSourceValues {
if string(item) == source {
return true
}
}
return false
}
+1 -1
View File
@@ -27,7 +27,7 @@ func GetChannelID(ctx *gin.Context) string {
if channelID := ctx.GetHeader("X-Channel-ID"); strs.IsNotBlank(channelID) {
return channelID
}
if channelID, _ := params.Get(ctx, "channelId"); strs.IsNotBlank(channelID) {
if channelID, _ := params.Get(ctx, "channel_id"); strs.IsNotBlank(channelID) {
return channelID
}
return ""
+68 -4
View File
@@ -1,9 +1,11 @@
package httpx
import (
"code.tczkiot.com/wlw/ai-agent/contract"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/mlogclub/simple/sqls"
@@ -21,10 +23,32 @@ type pageData struct {
paging *sqls.Paging
}
type cursorResult struct {
Results any `json:"results"`
Cursor string `json:"cursor"`
HasMore bool `json:"has_more"`
}
type pageResult struct {
Page *sqls.Paging `json:"page"`
Results any `json:"results"`
}
type localizedError interface {
Message(locale string) string
}
var (
responseWriterMu sync.RWMutex
responseWriter contract.ResponseWriter
)
func SetResponseWriter(writer contract.ResponseWriter) {
responseWriterMu.Lock()
defer responseWriterMu.Unlock()
responseWriter = writer
}
func CursorData(results any, cursor string, hasMore bool) any {
return cursorData{results: results, cursor: cursor, hasMore: hasMore}
}
@@ -34,11 +58,36 @@ func PageData(results any, paging *sqls.Paging) any {
}
func WriteJSON(ctx *gin.Context, result any) {
ctx.JSON(http.StatusOK, buildJSONResult(ctx, result))
writeJSON(ctx, http.StatusOK, result)
}
func WriteHttpStatusJSON(ctx *gin.Context, statusCode int, result any) {
ctx.JSON(statusCode, buildJSONResult(ctx, result))
writeJSON(ctx, statusCode, result)
}
func AbortJSON(ctx *gin.Context, statusCode int, result any) {
writeJSON(ctx, statusCode, result)
ctx.Abort()
}
func writeJSON(ctx *gin.Context, statusCode int, result any) {
payload := buildJSONResult(ctx, result)
responseWriterMu.RLock()
writer := responseWriter
responseWriterMu.RUnlock()
if writer == nil {
ctx.JSON(statusCode, payload)
return
}
if err := writer(ctx.Writer, ctx.Request, contract.Response{
StatusCode: statusCode,
ErrorCode: payload.ErrorCode,
Message: payload.Message,
Data: payload.Data,
Success: payload.Success,
}); err != nil {
_ = ctx.Error(err)
}
}
func buildJSONResult(ctx *gin.Context, result any) *web.JsonResult {
@@ -62,9 +111,24 @@ func buildJSONResult(ctx *gin.Context, result any) *web.JsonResult {
case error:
return web.JsonError(value)
case cursorData:
return web.JsonCursorData(value.results, value.cursor, value.hasMore)
results := value.results
if results == nil {
results = []any{}
}
return web.JsonData(&cursorResult{
Results: results,
Cursor: value.cursor,
HasMore: value.hasMore,
})
case pageData:
return web.JsonPageData(value.results, value.paging)
results := value.results
if results == nil {
results = []any{}
}
return web.JsonData(&pageResult{
Page: value.paging,
Results: results,
})
case web.RspBuilder:
return value.JsonResult()
case *web.RspBuilder:
+22
View File
@@ -85,6 +85,28 @@ func TestWriteJSONLocalizesI18nErrors(t *testing.T) {
}
}
func TestWriteJSONUsesSnakeCaseCursorFields(t *testing.T) {
gin.SetMode(gin.TestMode)
ctx, recorder := testContext()
WriteJSON(ctx, CursorData([]string{"a"}, "next", true))
var body map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatalf("decode response: %v", err)
}
data, ok := body["data"].(map[string]any)
if !ok {
t.Fatalf("data = %#v, want object", body["data"])
}
if got := data["has_more"]; got != true {
t.Fatalf("has_more = %#v, want true", got)
}
if _, exists := data["hasMore"]; exists {
t.Fatalf("camelCase hasMore should not be exposed: %s", recorder.Body.String())
}
}
func TestWriteHttpStatusJSONUsesProvidedStatus(t *testing.T) {
gin.SetMode(gin.TestMode)
ctx, recorder := testContext()
+13 -59
View File
@@ -373,39 +373,18 @@ tool.builtin.toolSearch.appendix: |-
tool.builtin.skill.title: "Load Skill Instructions"
tool.builtin.skill.description: "Loads specialized skill instructions for the current agent when extra task-specific guidance is needed."
tool.graph.triageServiceRequest.title: "Route Service Request"
tool.graph.triageServiceRequest.description: "Analyzes the current conversation to decide whether to keep answering, prepare a ticket draft, or hand off to a human, including a ticket draft when ticket creation is appropriate."
tool.graph.triageServiceRequest.description: "Analyzes the current conversation to decide whether to keep answering or hand off to a human."
tool.graph.triageServiceRequest.appendix: |-
When you need to decide between continuing the answer, creating a ticket, or handing off to a human, call triage_service_request first and follow these rules:
1. The tool returns recommendedAction and includes ticketDraft when ticket creation is needed.
2. If recommendedAction=continue_answering, continue clarifying or answering instead of escalating directly.
3. If recommendedAction=prepare_ticket, use ticketDraft or collect missing fields before calling create_ticket_with_confirmation.
4. If recommendedAction=handoff_to_human, confirm the reason is sufficient before calling handoff_to_human.
5. When the escalation path is unclear, use this tool instead of making a complex routing decision from the main prompt alone.
When you need to decide between continuing the answer or handing off to a human, call triage_service_request first.
1. If recommendedAction=continue_answering, continue clarifying or answering.
2. If recommendedAction=handoff_to_human, confirm the reason is sufficient before calling handoff_to_human.
tool.graph.analyzeConversation.title: "Analyze Conversation Risk and Summary"
tool.graph.analyzeConversation.description: "Summarizes the current conversation, identifies risk signals, and recommends whether to keep answering, create a ticket, or hand off to a human."
tool.graph.analyzeConversation.description: "Summarizes the current conversation, identifies risk signals, and recommends whether to keep answering or hand off to a human."
tool.graph.analyzeConversation.appendix: |-
When the conversation may involve escalation, refunds, compensation, clear negative sentiment, ticket creation, or human handoff, call analyze_conversation first and follow these rules:
1. This tool returns a structured summary, risk signals, and next-step recommendation. It does not create tickets or hand off to a human.
When the conversation may involve escalation, refunds, compensation, clear negative sentiment, or human handoff, call analyze_conversation first.
1. This tool returns a structured summary, risk signals, and next-step recommendation.
2. If the tool recommends handoff_to_human, confirm the handoff conditions before calling handoff_to_human.
3. If the tool recommends prepare_ticket, call prepare_ticket_draft or collect more information before creating a ticket.
4. If the tool recommends continue_answering, continue clarifying and answering instead of escalating too early.
tool.graph.prepareTicketDraft.title: "Prepare Ticket Draft"
tool.graph.prepareTicketDraft.description: "Turns the current conversation and collected details into a ticket draft with a suggested title, description, missing fields, and follow-up questions."
tool.graph.prepareTicketDraft.appendix: |-
When the user has asked to create a ticket, file a complaint, report an issue, or request after-sales handling, but the title or description is still unclear, call prepare_ticket_draft first and follow these rules:
1. This tool prepares a ticket draft and returns a suggested title, suggested description, missing fields, and follow-up questions.
2. If ready=false, ask follow-up questions based on missingFields and followUpQuestions instead of creating a ticket directly.
3. If ready=true, use the result to consider calling create_ticket_with_confirmation.
4. This tool only prepares a draft. It does not create a ticket.
tool.graph.createTicketConfirm.title: "Create Ticket With Confirmation"
tool.graph.createTicketConfirm.description: "Guides ticket creation with parameter preparation, customer confirmation, actual ticket creation, and final result delivery."
tool.graph.createTicketConfirm.appendix: |-
You can call create_ticket_with_confirmation after enough information has been collected, but follow these rules:
1. Only consider this tool when the user explicitly wants to submit a ticket, complaint, issue report, or after-sales request.
2. Before calling it, prepare a clear ticket title and issue description. If the information is still scattered, call prepare_ticket_draft or ask follow-up questions first.
3. Once you are ready to create a ticket, you must call create_ticket_with_confirmation. Do not simply claim in text that the ticket has been created.
4. This Graph Tool asks the user for confirmation first. The ticket is created only after the user confirms; if the user cancels, the flow ends.
5. If the user is only asking questions, complaining generally, or expressing dissatisfaction without explicitly requesting a ticket, continue clarifying instead of proactively creating one.
3. If the tool recommends continue_answering, continue clarifying and answering instead of escalating too early.
tool.graph.handoffConversation.title: "Handoff to Human With Confirmation"
tool.graph.handoffConversation.description: "Guides human handoff with reason preparation, customer confirmation, actual transfer, and final result delivery."
tool.graph.handoffConversation.appendix: |-
@@ -416,20 +395,14 @@ tool.graph.handoffConversation.appendix: |-
4. This Graph Tool asks the user for confirmation first. The handoff happens only after the user confirms; if the user cancels, the flow ends.
5. If the issue can still be solved in the current conversation, continue helping instead of escalating too early.
6. If the tool returns terminal=true and shouldRetry=false, the handoff flow has ended. Do not call it repeatedly.
conversation.handoff.waiting: "We are connecting you to a human support agent. Please wait."
conversation.handoff.waiting: "You are now in the human support queue. You can keep leaving messages, and the agent will see them when connected."
conversation.handoff.offHours: "Human support is currently outside service hours. You can keep describing the issue and I will do my best to help. You can also request a human agent again when service hours resume."
notification.ticketAssigned.title: "Ticket assigned"
notification.ticketAssigned.line: "Ticket %s has been assigned to you."
notification.ticketAssigned.reason: "Assignment reason: %s"
notification.conversationTransferred.title: "Conversation transferred"
notification.conversationAutoAssigned.title: "Conversation auto-assigned"
notification.conversationAssigned.title: "Conversation assigned"
notification.conversationAssigned.line: "Conversation #%s has been assigned to you."
notification.conversationAssigned.reason: "Assignment reason: %s"
notification.conversationTransferred.reason: "Transfer reason: %s"
notification.ticketAssigned.wxwork.no: "Ticket no: %s"
notification.ticketAssigned.wxwork.title: "Title: %s"
notification.ticketAssigned.wxwork.status: "Status: %s"
notification.assignee: "Assignee: %s"
notification.conversationAssigned.wxwork.id: "Conversation ID: #%d"
notification.conversationAssigned.wxwork.summary: "Summary: %s"
@@ -439,41 +412,22 @@ notification.time: "Time: %s"
graph.confirmOrCancel: "Please reply with \"Confirm\" or \"Cancel\"."
graph.needExplicitConfirmation: "I need your explicit confirmation. Please reply with \"Confirm\" or \"Cancel\"."
graph.confirmationExpired: "This confirmation has expired. Please start again."
graph.cancelCreateTicket: "Ticket creation has been cancelled."
graph.cancelHandoff: "Human handoff has been cancelled."
graph.ticketCreated: "Ticket created. Ticket no: %s. Title: %s."
graph.createTicketConfirmPrompt: |-
I am ready to create a ticket for you.
Title: %s
Description: %s
Please reply with "Confirm" or "Cancel".
graph.defaultHandoffReason: "The user needs human support."
graph.handoffConfirmPrompt: |-
I am ready to connect you to a human support agent.
Reason: %s
Please reply with "Confirm" or "Cancel".
conversation.interrupt.defaultPrompt: "Please provide more information and try again."
ticket.defaultConversationTitle: "Conversation ticket"
tool.graph.createTicketConfirm.info: "Graph Tool. Handles ticket parameter preparation, user confirmation, actual ticket creation, and result return. Use only when the user explicitly asks to create a ticket and the title and description are clear."
tool.graph.createTicketConfirm.param.title: "Ticket title. Concisely summarizes the issue."
tool.graph.createTicketConfirm.param.description: "Ticket description. Clearly captures the user's issue, symptoms, and request."
tool.graph.handoffConversation.info: "Graph Tool. Handles handoff reason preparation, user confirmation, actual human handoff, and result return. Use only when the user explicitly asks for a human agent or you have confirmed that human handling is required. Do not repeat the call when the result has terminal=true and shouldRetry=false."
tool.graph.handoffConversation.param.reason: "Handoff reason. Briefly explain why a human is needed, such as explicit user request, manual verification, or after-sales handling."
tool.graph.triageServiceRequest.info: "Graph Tool. Analyzes the current conversation to decide whether to continue answering, prepare a ticket draft, or hand off to a human. When ticket creation is recommended, it returns a structured ticket draft suggestion."
tool.graph.triageServiceRequest.param.goal: "Analysis goal, such as whether to escalate, create a ticket, or hand off to a human."
tool.graph.triageServiceRequest.info: "Graph Tool. Analyzes the current conversation to decide whether to continue answering or hand off to a human."
tool.graph.triageServiceRequest.param.goal: "Analysis goal, such as whether to escalate or hand off to a human."
tool.graph.triageServiceRequest.param.observedIssue: "Main issue or dispute observed in the conversation."
tool.graph.triageServiceRequest.param.needTicket: "Whether to focus on evaluating ticket creation."
tool.graph.triageServiceRequest.param.needHumanHandoff: "Whether to focus on evaluating human handoff."
tool.graph.triageServiceRequest.param.additionalContext: "Additional context, such as risk signals or constraints already identified."
tool.graph.analyzeConversation.info: "Graph Tool. Summarizes the current conversation, identifies complaint/payment/sentiment risk signals, and recommends whether to continue answering, create a ticket, or hand off to a human."
tool.graph.analyzeConversation.param.goal: "Analysis goal, such as whether to hand off to a human, create a ticket, or perform risk review."
tool.graph.analyzeConversation.info: "Graph Tool. Summarizes the current conversation, identifies complaint/payment/sentiment risk signals, and recommends whether to continue answering or hand off to a human."
tool.graph.analyzeConversation.param.goal: "Analysis goal, such as whether to hand off to a human or perform risk review."
tool.graph.analyzeConversation.param.observedIssue: "Main issue or request observed in the conversation."
tool.graph.analyzeConversation.param.needQualityCheck: "Whether to focus on risk or quality review."
tool.graph.analyzeConversation.param.additionalContext: "Additional context, such as disputes, complaint points, or business constraints already identified."
tool.graph.prepareTicketDraft.info: "Graph Tool. Prepares a ticket draft from the current conversation and collected information. Use it before create_ticket_with_confirmation when the ticket content needs to be organized."
tool.graph.prepareTicketDraft.param.title: "Prepared ticket title. Optional."
tool.graph.prepareTicketDraft.param.description: "Prepared ticket description. Optional."
tool.graph.prepareTicketDraft.param.issue: "The issue or error message the user is experiencing."
tool.graph.prepareTicketDraft.param.impact: "Impact scope, such as unable to sign in, unable to place an order, or business interruption."
tool.graph.prepareTicketDraft.param.expectedOutcome: "The user's expected outcome or request."
tool.graph.prepareTicketDraft.param.currentAttempt: "Current attempted troubleshooting steps. Optional."
+13 -59
View File
@@ -373,39 +373,18 @@ tool.builtin.toolSearch.appendix: |-
tool.builtin.skill.title: "加载专项技能说明"
tool.builtin.skill.description: "用于按需加载当前 Agent 可用的专项技能说明文档,适合在需要专项处理规则时再注入上下文。"
tool.graph.triageServiceRequest.title: "服务请求分流"
tool.graph.triageServiceRequest.description: "Graph Tool。分析当前会话,判断应继续回答、准备工单草稿还是转人工;当适合创建工单时,也会准备工单草稿。"
tool.graph.triageServiceRequest.description: "Graph Tool。分析当前会话,判断应继续回答还是转人工。"
tool.graph.triageServiceRequest.appendix: |-
当你需要在继续回答、创建工单、转人工之间做判断时,优先调用 triage_service_request,并遵守以下规则:
1. 工具会返回 recommendedAction,并在需要创建工单时包含 ticketDraft
2. 如果 recommendedAction=continue_answering,继续澄清或回答,不要直接升级。
3. 如果 recommendedAction=prepare_ticket,使用 ticketDraft 或补充缺失字段后,再考虑调用 create_ticket_with_confirmation。
4. 如果 recommendedAction=handoff_to_human,先确认转人工原因足够明确,再调用 handoff_to_human。
5. 当升级路径不明确时,使用此工具,而不是只依赖主提示词做复杂路由判断。
当你需要在继续回答转人工之间做判断时,优先调用 triage_service_request
1. 如果 recommendedAction=continue_answering,继续澄清或回答
2. 如果 recommendedAction=handoff_to_human,先确认转人工原因足够明确,再调用 handoff_to_human。
tool.graph.analyzeConversation.title: "分析会话风险和摘要"
tool.graph.analyzeConversation.description: "Graph Tool。总结当前会话,识别风险信号,并建议继续回答、创建工单或转人工。"
tool.graph.analyzeConversation.description: "Graph Tool。总结当前会话,识别风险信号,并建议继续回答或转人工。"
tool.graph.analyzeConversation.appendix: |-
当会话可能涉及升级、退款、赔偿、明显负面情绪、创建工单或转人工时,优先调用 analyze_conversation,并遵守以下规则:
1. 此工具返回结构化摘要、风险信号和下一步建议;它不会创建工单或执行转人工
当会话可能涉及升级、退款、赔偿、明显负面情绪或转人工时,优先调用 analyze_conversation
1. 此工具返回结构化摘要、风险信号和下一步建议。
2. 如果工具建议 handoff_to_human,先确认转人工条件,再调用 handoff_to_human。
3. 如果工具建议 prepare_ticket,调用 prepare_ticket_draft 或继续收集信息后再创建工单。
4. 如果工具建议 continue_answering,继续澄清和回答,不要过早升级。
tool.graph.prepareTicketDraft.title: "准备工单草稿"
tool.graph.prepareTicketDraft.description: "Graph Tool。基于当前会话和已收集信息准备工单草稿,包括建议标题、描述、缺失字段和追问问题。"
tool.graph.prepareTicketDraft.appendix: |-
当用户要求创建工单、投诉、报障或售后处理,但标题或描述仍不清晰时,优先调用 prepare_ticket_draft,并遵守以下规则:
1. 此工具会准备工单草稿,并返回建议标题、建议描述、缺失字段和追问问题。
2. 如果 ready=false,根据 missingFields 和 followUpQuestions 继续追问,不要直接创建工单。
3. 如果 ready=true,使用结果考虑调用 create_ticket_with_confirmation。
4. 此工具只准备草稿,不会创建工单。
tool.graph.createTicketConfirm.title: "创建工单确认流程"
tool.graph.createTicketConfirm.description: "Graph Tool。处理工单参数准备、用户确认、实际创建工单和结果返回。"
tool.graph.createTicketConfirm.appendix: |-
收集到足够信息后,可以调用 create_ticket_with_confirmation,但必须遵守以下规则:
1. 只有当用户明确想提交工单、投诉、问题报告或售后请求时,才考虑使用此工具。
2. 调用前准备清晰的工单标题和问题描述;如果信息仍然分散,先调用 prepare_ticket_draft 或继续追问。
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation,不要只用文本声称工单已创建。
4. 此 Graph Tool 会先请求用户确认;只有用户确认后才会创建工单,用户取消则流程结束。
5. 如果用户只是提问、泛泛抱怨或表达不满但没有明确要求创建工单,应继续澄清,而不是主动创建。
3. 如果工具建议 continue_answering,继续澄清和回答,不要过早升级。
tool.graph.handoffConversation.title: "转人工确认流程"
tool.graph.handoffConversation.description: "Graph Tool。处理转人工原因准备、用户确认、实际转人工和结果返回。"
tool.graph.handoffConversation.appendix: |-
@@ -416,20 +395,14 @@ tool.graph.handoffConversation.appendix: |-
4. 此 Graph Tool 会先请求用户确认;只有用户确认后才会转人工,用户取消则流程结束。
5. 如果问题仍可在当前会话解决,继续帮助用户,不要过早升级。
6. 如果工具返回 terminal=true 且 shouldRetry=false,说明转人工流程已结束,不要重复调用。
conversation.handoff.waiting: "正在为你入人工客服,请稍候。"
conversation.handoff.waiting: "为你入人工客服队列,你可以继续留言,客服接入后会看到。"
conversation.handoff.offHours: "当前不在人工客服服务时间内。你可以继续描述问题,我会尽力协助;也可以在服务时间恢复后再次申请人工客服。"
notification.ticketAssigned.title: "工单指派提醒"
notification.ticketAssigned.line: "工单 %s 已指派给你"
notification.ticketAssigned.reason: "指派原因: %s"
notification.conversationTransferred.title: "会话转接提醒"
notification.conversationAutoAssigned.title: "会话自动分配提醒"
notification.conversationAssigned.title: "会话分配提醒"
notification.conversationAssigned.line: "会话 #%s 已分配给你"
notification.conversationAssigned.reason: "分配原因: %s"
notification.conversationTransferred.reason: "转接原因: %s"
notification.ticketAssigned.wxwork.no: "工单号: %s"
notification.ticketAssigned.wxwork.title: "工单标题: %s"
notification.ticketAssigned.wxwork.status: "当前状态: %s"
notification.assignee: "处理人: %s"
notification.conversationAssigned.wxwork.id: "会话ID: #%d"
notification.conversationAssigned.wxwork.summary: "摘要: %s"
@@ -439,41 +412,22 @@ notification.time: "时间: %s"
graph.confirmOrCancel: "请回复“确认”或“取消”。"
graph.needExplicitConfirmation: "需要你明确确认。请回复“确认”或“取消”。"
graph.confirmationExpired: "本次确认已过期,请重新发起。"
graph.cancelCreateTicket: "已取消本次工单创建。"
graph.cancelHandoff: "已取消本次转人工。"
graph.ticketCreated: "工单已创建。工单号:%s。标题:%s。"
graph.createTicketConfirmPrompt: |-
我已准备好为你创建工单。
标题:%s
描述:%s
请回复“确认”或“取消”。
graph.defaultHandoffReason: "用户需要人工客服支持。"
graph.handoffConfirmPrompt: |-
我已准备好为你接入人工客服。
原因:%s
请回复“确认”或“取消”。
conversation.interrupt.defaultPrompt: "请补充更多信息后再试。"
ticket.defaultConversationTitle: "会话工单"
tool.graph.createTicketConfirm.info: "Graph Tool。处理工单参数准备、用户确认、实际创建工单和结果返回。仅当用户明确要求创建工单且标题、描述清晰时使用。"
tool.graph.createTicketConfirm.param.title: "工单标题。简洁概括问题。"
tool.graph.createTicketConfirm.param.description: "工单描述。清晰记录用户的问题、现象和诉求。"
tool.graph.handoffConversation.info: "Graph Tool。处理转人工原因准备、用户确认、实际转人工和结果返回。仅当用户明确要求人工客服,或你确认需要人工处理时使用。当结果 terminal=true 且 shouldRetry=false 时不要重复调用。"
tool.graph.handoffConversation.param.reason: "转人工原因。简要说明为什么需要人工,例如用户明确要求、需要人工核验或售后处理。"
tool.graph.triageServiceRequest.info: "Graph Tool。分析当前会话,判断应继续回答、准备工单草稿还是转人工。建议创建工单时,会返回结构化工单草稿建议。"
tool.graph.triageServiceRequest.param.goal: "分析目标,例如是否升级、创建工单或转人工。"
tool.graph.triageServiceRequest.info: "Graph Tool。分析当前会话,判断应继续回答还是转人工。"
tool.graph.triageServiceRequest.param.goal: "分析目标,例如是否升级或转人工。"
tool.graph.triageServiceRequest.param.observedIssue: "当前会话中观察到的主要问题或争议。"
tool.graph.triageServiceRequest.param.needTicket: "是否重点评估创建工单。"
tool.graph.triageServiceRequest.param.needHumanHandoff: "是否重点评估转人工。"
tool.graph.triageServiceRequest.param.additionalContext: "补充上下文,例如已经识别的风险信号或约束。"
tool.graph.analyzeConversation.info: "Graph Tool。总结当前会话,识别投诉、支付、情绪等风险信号,并建议继续回答、创建工单或转人工。"
tool.graph.analyzeConversation.param.goal: "分析目标,例如是否转人工、创建工单或执行风险复核。"
tool.graph.analyzeConversation.info: "Graph Tool。总结当前会话,识别投诉、支付、情绪等风险信号,并建议继续回答或转人工。"
tool.graph.analyzeConversation.param.goal: "分析目标,例如是否转人工或执行风险复核。"
tool.graph.analyzeConversation.param.observedIssue: "当前会话中观察到的主要问题或请求。"
tool.graph.analyzeConversation.param.needQualityCheck: "是否重点进行风险或质量复核。"
tool.graph.analyzeConversation.param.additionalContext: "补充上下文,例如争议点、投诉点或已识别的业务约束。"
tool.graph.prepareTicketDraft.info: "Graph Tool。根据当前会话和已收集信息准备工单草稿。当工单内容需要整理时,应在 create_ticket_with_confirmation 之前使用。"
tool.graph.prepareTicketDraft.param.title: "准备好的工单标题,可选。"
tool.graph.prepareTicketDraft.param.description: "准备好的工单描述,可选。"
tool.graph.prepareTicketDraft.param.issue: "用户正在遇到的问题或报错信息。"
tool.graph.prepareTicketDraft.param.impact: "影响范围,例如无法登录、无法下单或业务中断。"
tool.graph.prepareTicketDraft.param.expectedOutcome: "用户期望的处理结果或诉求。"
tool.graph.prepareTicketDraft.param.currentAttempt: "当前已尝试过的处理步骤,可选。"
+9 -4
View File
@@ -1,12 +1,17 @@
package openidentity
import "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
import (
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
)
// ExternalUser is a be-system user identity adapted for customer-service
// conversations. Authentication is completed by the host before this value is
// created; this package contains no token parsing or verification.
type ExternalUser struct {
ExternalSource enums.ExternalSource `json:"externalSource"`
ExternalID string `json:"externalId"`
ExternalName string `json:"externalName"`
ExternalSource enums.ExternalSource `json:"external_source"`
ExternalID string `json:"external_id"`
ExternalName string `json:"external_name"`
SubjectType identity.SubjectType `json:"subject_type,omitempty"`
SubjectID int64 `json:"subject_id,omitempty"`
}
@@ -0,0 +1,30 @@
package openidentity
import (
"encoding/json"
"strings"
"testing"
)
func TestExternalUserJSONUsesSnakeCaseFields(t *testing.T) {
payload, err := json.Marshal(ExternalUser{
ExternalID: "user:1",
ExternalName: "test",
SubjectID: 1,
})
if err != nil {
t.Fatalf("marshal external user: %v", err)
}
text := string(payload)
for _, field := range []string{`"external_id"`, `"external_name"`, `"subject_id"`} {
if !strings.Contains(text, field) {
t.Fatalf("expected %s in %s", field, text)
}
}
for _, field := range []string{"externalId", "externalName", "subjectId"} {
if strings.Contains(text, field) {
t.Fatalf("unexpected camelCase field %q in %s", field, text)
}
}
}
-36
View File
@@ -103,34 +103,6 @@ var (
Appendix: i18nx.Get("tool.graph.analyzeConversation.appendix"),
AppendixKey: "tool.graph.analyzeConversation.appendix",
}
GraphPrepareTicketDraft = ToolSpec{
Code: "graph/prepare_ticket_draft",
ServerCode: "graph",
Name: "prepare_ticket_draft",
Title: i18nx.Get("tool.graph.prepareTicketDraft.title"),
TitleKey: "tool.graph.prepareTicketDraft.title",
Description: i18nx.Get("tool.graph.prepareTicketDraft.description"),
DescriptionKey: "tool.graph.prepareTicketDraft.description",
SourceType: enums.ToolSourceTypeGraph,
DirectAccess: true,
RuntimeStatic: true,
Appendix: i18nx.Get("tool.graph.prepareTicketDraft.appendix"),
AppendixKey: "tool.graph.prepareTicketDraft.appendix",
}
GraphCreateTicketConfirm = ToolSpec{
Code: "graph/create_ticket_with_confirmation",
ServerCode: "graph",
Name: "create_ticket_with_confirmation",
Title: i18nx.Get("tool.graph.createTicketConfirm.title"),
TitleKey: "tool.graph.createTicketConfirm.title",
Description: i18nx.Get("tool.graph.createTicketConfirm.description"),
DescriptionKey: "tool.graph.createTicketConfirm.description",
SourceType: enums.ToolSourceTypeGraph,
RuntimeStatic: true,
Aliases: []string{"builtin/create_ticket_with_confirmation"},
Appendix: i18nx.Get("tool.graph.createTicketConfirm.appendix"),
AppendixKey: "tool.graph.createTicketConfirm.appendix",
}
GraphHandoffConversation = ToolSpec{
Code: "graph/handoff_to_human",
ServerCode: "graph",
@@ -151,8 +123,6 @@ var (
BuiltinKnowledgeRetrieve,
GraphTriageServiceRequest,
GraphAnalyzeConversation,
GraphPrepareTicketDraft,
GraphCreateTicketConfirm,
GraphHandoffConversation,
}
)
@@ -401,15 +371,9 @@ func IsImpliedAllowedToolCode(toolCode string, allowedToolCodes map[string]struc
}
switch toolCode {
case GraphTriageServiceRequest.Code, GraphAnalyzeConversation.Code:
if _, ok := allowedToolCodes[GraphCreateTicketConfirm.Code]; ok {
return true
}
if _, ok := allowedToolCodes[GraphHandoffConversation.Code]; ok {
return true
}
case GraphPrepareTicketDraft.Code:
_, ok := allowedToolCodes[GraphCreateTicketConfirm.Code]
return ok
}
return false
}
+6 -35
View File
@@ -1,23 +1,19 @@
package toolx
import (
"testing"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
)
import "testing"
func TestResolveToolMetadata(t *testing.T) {
item := ResolveToolMetadata("builtin/create_ticket_with_confirmation", "")
if item.ToolCode != GraphCreateTicketConfirm.Code {
item := ResolveToolMetadata(GraphHandoffConversation.Code, "")
if item.ToolCode != GraphHandoffConversation.Code {
t.Fatalf("unexpected tool code: %s", item.ToolCode)
}
if item.ServerCode != GraphCreateTicketConfirm.ServerCode {
if item.ServerCode != GraphHandoffConversation.ServerCode {
t.Fatalf("unexpected server code: %s", item.ServerCode)
}
if item.ToolName != GraphCreateTicketConfirm.Name {
if item.ToolName != GraphHandoffConversation.Name {
t.Fatalf("unexpected tool name: %s", item.ToolName)
}
if item.SourceType != GraphCreateTicketConfirm.SourceType {
if item.SourceType != GraphHandoffConversation.SourceType {
t.Fatalf("unexpected source type: %s", item.SourceType)
}
}
@@ -37,28 +33,3 @@ func TestResolveToolMetadataFallsBackToName(t *testing.T) {
t.Fatalf("unexpected source type: %s", item.SourceType)
}
}
func TestRegisteredToolTextUsesEnglishLocale(t *testing.T) {
title := GetRegisteredToolTitleLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleEnUS)
if title != "Create Ticket With Confirmation" {
t.Fatalf("unexpected english title: %q", title)
}
description := GetRegisteredToolDescriptionLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleEnUS)
want := "Guides ticket creation with parameter preparation, customer confirmation, actual ticket creation, and final result delivery."
if description != want {
t.Fatalf("unexpected english description: %q", description)
}
}
func TestRegisteredToolTextKeepsChineseLocale(t *testing.T) {
title := GetRegisteredToolTitleLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleZhCN)
if title != "创建工单确认流程" {
t.Fatalf("unexpected chinese title: %q", title)
}
description := GetRegisteredToolDescriptionLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleZhCN)
if description != "Graph Tool。处理工单参数准备、用户确认、实际创建工单和结果返回。" {
t.Fatalf("unexpected chinese description: %q", description)
}
}
-51
View File
@@ -1,51 +0,0 @@
package toolx
import (
"strings"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
)
const (
MCPRiskLevelRead = "read"
MCPRiskLevelWrite = "write"
)
type TrustedMCPToolPolicy struct {
ToolCode string
Title string
RiskLevel string
RequireConfirmation bool
}
var trustedMCPToolPolicies = map[string]TrustedMCPToolPolicy{
"system/server_time": {
ToolCode: "system/server_time",
Title: "获取当前时间",
RiskLevel: MCPRiskLevelRead,
RequireConfirmation: false,
},
"system/service_info": {
ToolCode: "system/service_info",
Title: "查看服务信息",
RiskLevel: MCPRiskLevelRead,
RequireConfirmation: false,
},
}
func GetTrustedMCPToolPolicy(toolCode string) (TrustedMCPToolPolicy, bool) {
policy, ok := trustedMCPToolPolicies[NormalizeToolCodeAlias(strings.TrimSpace(toolCode))]
return policy, ok
}
func ApplyTrustedMCPToolPolicy(item request.AIAgentMCPToolRequest) request.AIAgentMCPToolRequest {
policy, ok := GetTrustedMCPToolPolicy(item.ToolCode)
if !ok {
return item
}
item.ToolCode = policy.ToolCode
item.Title = policy.Title
item.RiskLevel = policy.RiskLevel
item.RequireConfirmation = policy.RequireConfirmation
return item
}
-98
View File
@@ -1,98 +0,0 @@
package toolx
import (
"encoding/json"
"strings"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
)
func BuildMCPToolCode(serverCode, toolName string) string {
serverCode = strings.TrimSpace(serverCode)
toolName = strings.TrimSpace(toolName)
if serverCode == "" || toolName == "" {
return ""
}
return serverCode + "/" + toolName
}
func SplitMCPToolCode(toolCode string) (string, string) {
toolCode = strings.TrimSpace(toolCode)
if toolCode == "" {
return "", ""
}
idx := strings.Index(toolCode, "/")
if idx <= 0 || idx >= len(toolCode)-1 {
return "", ""
}
return strings.TrimSpace(toolCode[:idx]), strings.TrimSpace(toolCode[idx+1:])
}
func NormalizeMCPToolRequest(item request.AIAgentMCPToolRequest) (request.AIAgentMCPToolRequest, error) {
toolCode := strings.TrimSpace(item.ToolCode)
toolCode = NormalizeToolCodeAlias(toolCode)
serverCode := strings.TrimSpace(item.ServerCode)
toolName := strings.TrimSpace(item.ToolName)
if toolCode != "" {
parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode)
if parsedServerCode != "" && parsedToolName != "" {
if serverCode != "" && !strings.EqualFold(serverCode, parsedServerCode) {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0016")
}
if toolName != "" && !strings.EqualFold(toolName, parsedToolName) {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0017")
}
serverCode = parsedServerCode
toolName = parsedToolName
} else {
serverCode = ""
toolName = ""
}
} else {
toolCode = BuildMCPToolCode(serverCode, toolName)
}
if toolCode == "" {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0019")
}
if parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
serverCode = parsedServerCode
toolName = parsedToolName
}
if serverCode == "" && toolName == "" && strings.Contains(toolCode, "/") && !strings.HasPrefix(toolCode, "builtin/") {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0018")
}
ret := request.AIAgentMCPToolRequest{
ToolCode: toolCode,
ServerCode: serverCode,
ToolName: toolName,
Title: strings.TrimSpace(item.Title),
Description: strings.TrimSpace(item.Description),
RiskLevel: strings.ToLower(strings.TrimSpace(item.RiskLevel)),
RequireConfirmation: item.RequireConfirmation,
}
if len(item.Arguments) > 0 {
ret.Arguments = make(map[string]string, len(item.Arguments))
for key, value := range item.Arguments {
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" || value == "" {
continue
}
ret.Arguments[key] = value
}
}
return ret, nil
}
func ParseAgentMCPToolsJSON(raw string) ([]request.AIAgentMCPToolRequest, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
var ret []request.AIAgentMCPToolRequest
if err := json.Unmarshal([]byte(raw), &ret); err != nil {
return nil, err
}
return ret, nil
}
+36 -17
View File
@@ -1,11 +1,11 @@
package utils
import (
"bytes"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/services/storage"
"bytes"
"encoding/json"
"fmt"
"strings"
@@ -16,13 +16,28 @@ import (
)
type imMessageAssetPayload struct {
AssetID string `json:"assetId"`
Provider enums.AssetProvider `json:"provider,omitempty"`
StorageKey string `json:"storageKey,omitempty"`
Filename string `json:"filename,omitempty"`
FileSize int64 `json:"fileSize,omitempty"`
MimeType string `json:"mimeType,omitempty"`
URL string `json:"url,omitempty"`
AssetID string `json:"asset_id,omitempty"`
Provider enums.AssetProvider `json:"provider,omitempty"`
StorageKey string `json:"storage_key,omitempty"`
Filename string `json:"filename,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
MimeType string `json:"mime_type,omitempty"`
URL string `json:"url,omitempty"`
Assets []imMessageAssetPayload `json:"assets,omitempty"`
}
func (p *imMessageAssetPayload) items() []*imMessageAssetPayload {
if p == nil {
return nil
}
if len(p.Assets) == 0 {
return []*imMessageAssetPayload{p}
}
items := make([]*imMessageAssetPayload, 0, len(p.Assets))
for index := range p.Assets {
items = append(items, &p.Assets[index])
}
return items
}
func SanitizeMessageHTML(content string) string {
@@ -111,8 +126,8 @@ func BuildRuntimeMessageText(messageType enums.IMMessageType, content string) st
case enums.IMMessageTypeHTML:
return BuildHTMLSummary(content)
case enums.IMMessageTypeImage:
if content != "" {
return "[图片] " + content
if text := BuildHTMLSummary(content); text != "" {
return "[图片] " + text
}
return "[图片]"
case enums.IMMessageTypeAttachment:
@@ -286,10 +301,12 @@ func buildIMMessageAssetPayloadForResponse(payload string) string {
if err != nil {
return strings.TrimSpace(payload)
}
assetPayload = hydrateIMMessageAssetPayload(assetPayload)
if assetPayload.Provider != "" && assetPayload.StorageKey != "" {
if provider, err := storage.NewProvider(assetPayload.Provider); err == nil {
assetPayload.URL = provider.GetSignedURL(assetPayload.StorageKey)
for _, item := range assetPayload.items() {
hydrateIMMessageAssetPayload(item)
if item.Provider != "" && item.StorageKey != "" {
if provider, err := storage.NewProvider(item.Provider); err == nil {
item.URL = provider.GetSignedURL(item.StorageKey)
}
}
}
data, err := json.Marshal(assetPayload)
@@ -308,9 +325,11 @@ func parseIMMessageAssetPayload(payload string) (*imMessageAssetPayload, error)
if err := json.Unmarshal([]byte(payload), ret); err != nil {
return nil, err
}
ret.AssetID = strings.TrimSpace(ret.AssetID)
ret.Provider = enums.AssetProvider(strings.TrimSpace(string(ret.Provider)))
ret.StorageKey = strings.TrimSpace(ret.StorageKey)
for _, item := range ret.items() {
item.AssetID = strings.TrimSpace(item.AssetID)
item.Provider = enums.AssetProvider(strings.TrimSpace(string(item.Provider)))
item.StorageKey = strings.TrimSpace(item.StorageKey)
}
return ret, nil
}
+4 -4
View File
@@ -23,14 +23,14 @@ func TestBuildIMMessageAssetPayloadForResponseAddsSignedURL(t *testing.T) {
},
})
payload := `{"assetId":"asset_1","provider":"local","storageKey":"attachments/demo.png","filename":"demo.png"}`
payload := `{"asset_id":"asset_1","provider":"local","storage_key":"attachments/demo.png","filename":"demo.png"}`
got := buildIMMessageAssetPayloadForResponse(payload)
if !strings.Contains(got, `"provider":"local"`) {
t.Fatalf("expected provider in payload, got: %s", got)
}
if !strings.Contains(got, `"storageKey":"attachments/demo.png"`) {
t.Fatalf("expected storageKey in payload, got: %s", got)
if !strings.Contains(got, `"storage_key":"attachments/demo.png"`) {
t.Fatalf("expected storage_key in payload, got: %s", got)
}
if !strings.Contains(got, `"url":"https://files.example.com/attachments/demo.png"`) {
t.Fatalf("expected signed url in payload, got: %s", got)
@@ -99,7 +99,7 @@ func TestBuildRenderableMessageTransformsPayloadAndHTML(t *testing.T) {
image := &models.Message{
MessageType: enums.IMMessageTypeImage,
Payload: `{"assetId":"asset_1","provider":"local","storageKey":"attachments/demo.png","filename":"demo.png"}`,
Payload: `{"asset_id":"asset_1","provider":"local","storage_key":"attachments/demo.png","filename":"demo.png"}`,
}
_, imagePayload := BuildRenderableMessage(image)
if !strings.Contains(imagePayload, `"url":"https://files.example.com/attachments/demo.png"`) {