refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -18,13 +18,7 @@ func main() {
|
||||
WebIndex: false,
|
||||
WebEdit: false,
|
||||
},
|
||||
codegen.GetGenerateStruct(&models.Migration{}),
|
||||
codegen.GetGenerateStruct(&models.Company{}),
|
||||
codegen.GetGenerateStruct(&models.Customer{}),
|
||||
codegen.GetGenerateStruct(&models.CustomerIdentity{}),
|
||||
codegen.GetGenerateStruct(&models.CustomerContact{}),
|
||||
codegen.GetGenerateStruct(&models.Asset{}),
|
||||
codegen.GetGenerateStruct(&models.Tag{}),
|
||||
codegen.GetGenerateStruct(&models.Conversation{}),
|
||||
codegen.GetGenerateStruct(&models.ConversationParticipant{}),
|
||||
codegen.GetGenerateStruct(&models.Message{}),
|
||||
@@ -33,22 +27,14 @@ func main() {
|
||||
codegen.GetGenerateStruct(&models.WxWorkKFMessageRef{}),
|
||||
codegen.GetGenerateStruct(&models.ChannelMessageOutbox{}),
|
||||
codegen.GetGenerateStruct(&models.ConversationAssignment{}),
|
||||
codegen.GetGenerateStruct(&models.ConversationTag{}),
|
||||
codegen.GetGenerateStruct(&models.QuickReply{}),
|
||||
codegen.GetGenerateStruct(&models.AIAgent{}),
|
||||
codegen.GetGenerateStruct(&models.Channel{}),
|
||||
codegen.GetGenerateStruct(&models.ConversationEventLog{}),
|
||||
codegen.GetGenerateStruct(&models.Ticket{}),
|
||||
codegen.GetGenerateStruct(&models.TicketTag{}),
|
||||
codegen.GetGenerateStruct(&models.TicketProgress{}),
|
||||
codegen.GetGenerateStruct(&models.TicketView{}),
|
||||
codegen.GetGenerateStruct(&models.TicketNoSequence{}),
|
||||
codegen.GetGenerateStruct(&models.AgentProfile{}),
|
||||
codegen.GetGenerateStruct(&models.AgentTeam{}),
|
||||
codegen.GetGenerateStruct(&models.AgentTeamSchedule{}),
|
||||
codegen.GetGenerateStruct(&models.AIConfig{}),
|
||||
codegen.GetGenerateStruct(&models.SkillDefinition{}),
|
||||
codegen.GetGenerateStruct(&models.SystemConfig{}),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/bootstrap"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/logx"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load("config/config.yaml")
|
||||
if err != nil {
|
||||
slog.Error("load config failed", "error", err)
|
||||
return
|
||||
}
|
||||
logx.Init(logx.Config{
|
||||
Level: cfg.Logger.Level,
|
||||
Format: cfg.Logger.Format,
|
||||
AddSource: cfg.Logger.AddSource,
|
||||
})
|
||||
|
||||
if _, err = bootstrap.InitDB(cfg.DB); err != nil {
|
||||
slog.Error("init db failed", "error", err)
|
||||
return
|
||||
}
|
||||
if err = bootstrap.InitMigrations(); err != nil {
|
||||
slog.Error("run migrations failed", "error", err)
|
||||
return
|
||||
}
|
||||
slog.Info("migrations completed")
|
||||
}
|
||||
Vendored
-4
@@ -95,8 +95,6 @@ func buildModels(lang seedlang.Language, aiConfigID int64, knowledgeIDs []int64,
|
||||
FallbackMode: seed.FallbackMode,
|
||||
FallbackMessage: seed.FallbackMessage,
|
||||
KnowledgeIDs: utils.JoinInt64s(knowledgeIDs),
|
||||
SkillIDs: "",
|
||||
AllowedMCPTools: "",
|
||||
SortNo: seed.SortNo,
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
@@ -126,8 +124,6 @@ func seedUpdateColumns(item models.AIAgent) map[string]any {
|
||||
"fallback_mode": item.FallbackMode,
|
||||
"fallback_message": item.FallbackMessage,
|
||||
"knowledge_ids": item.KnowledgeIDs,
|
||||
"skill_ids": item.SkillIDs,
|
||||
"allowed_mcp_tools": item.AllowedMCPTools,
|
||||
"sort_no": item.SortNo,
|
||||
"updated_at": item.UpdatedAt,
|
||||
"update_user_id": item.UpdateUserID,
|
||||
|
||||
Vendored
+3
-13
@@ -50,7 +50,7 @@ func TestChineseAIAgentSeedUsesPresalesConfiguration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildModelsLeavesSkillsAndMCPToolsUnbound(t *testing.T) {
|
||||
func TestBuildModelsUsesSupportedBindings(t *testing.T) {
|
||||
items := buildModels(seedlang.Chinese, 7, []int64{11}, "13")
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("expected one AI agent model, got %d", len(items))
|
||||
@@ -60,18 +60,8 @@ func TestBuildModelsLeavesSkillsAndMCPToolsUnbound(t *testing.T) {
|
||||
if item.AIConfigID != 7 || item.KnowledgeIDs != "11" || item.TeamIDs != "13" {
|
||||
t.Fatalf("unexpected AI agent bindings: %+v", item)
|
||||
}
|
||||
if item.SkillIDs != "" {
|
||||
t.Fatalf("expected no Skill binding, got %q", item.SkillIDs)
|
||||
}
|
||||
if item.AllowedMCPTools != "" {
|
||||
t.Fatalf("expected no MCP Tool binding, got %q", item.AllowedMCPTools)
|
||||
}
|
||||
|
||||
columns := seedUpdateColumns(item)
|
||||
if value, ok := columns["skill_ids"]; !ok || value != "" {
|
||||
t.Fatalf("seed update must clear Skill bindings, got %#v", value)
|
||||
}
|
||||
if value, ok := columns["allowed_mcp_tools"]; !ok || value != "" {
|
||||
t.Fatalf("seed update must clear MCP Tool bindings, got %#v", value)
|
||||
if value, ok := columns["knowledge_ids"]; !ok || value != "11" {
|
||||
t.Fatalf("seed update must keep knowledge bindings, got %#v", value)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ items:
|
||||
baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
apiKey: <REPLACE_WITH_REAL_KEY>
|
||||
modelType: embedding
|
||||
modelName: text-embedding-v4
|
||||
modelName: qwen3.7-text-embedding
|
||||
dimension: 1536
|
||||
maxContextTokens: 0
|
||||
maxOutputTokens: 0
|
||||
@@ -73,4 +73,4 @@ items:
|
||||
rpmLimit: 0
|
||||
tpmLimit: 0
|
||||
sortNo: 30
|
||||
remark: rerank
|
||||
remark: rerank
|
||||
|
||||
Vendored
+4
-16
@@ -7,9 +7,8 @@ import (
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/kb"
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/quickreply"
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/skill"
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/tag"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/bootstrap"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -63,10 +62,10 @@ func run() error {
|
||||
}
|
||||
slog.Info("reset all tables success", slog.Int("droppedTableCount", droppedTableCount))
|
||||
|
||||
if err := bootstrap.InitMigrations(); err != nil {
|
||||
return fmt.Errorf("run migrations failed: %w", err)
|
||||
if err := db.AutoMigrate(models.Models...); err != nil {
|
||||
return fmt.Errorf("create testdata schema failed: %w", err)
|
||||
}
|
||||
slog.Info("run migrations success")
|
||||
slog.Info("create testdata schema success")
|
||||
|
||||
aiConfigResult, err := aiconfig.Init()
|
||||
if err != nil {
|
||||
@@ -87,12 +86,6 @@ func run() error {
|
||||
slog.Int("updatedFAQs", kbResult.UpdatedFAQs),
|
||||
)
|
||||
|
||||
skillResult, err := skill.Init(lang)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init skill failed: %w", err)
|
||||
}
|
||||
slog.Info("skill init success", slog.Int("created", skillResult.Created), slog.Int("updated", skillResult.Updated))
|
||||
|
||||
aiAgentResult, err := aiagent.Init(lang)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init ai agent failed: %w", err)
|
||||
@@ -105,11 +98,6 @@ func run() error {
|
||||
}
|
||||
slog.Info("channel init success", slog.Int("created", channelResult.Created), slog.Int("updated", channelResult.Updated))
|
||||
|
||||
if err := tag.Init(lang); err != nil {
|
||||
slog.Error("init tag failed", "error", err)
|
||||
}
|
||||
slog.Info("tag init success")
|
||||
|
||||
if err := quickreply.Init(lang); err != nil {
|
||||
return fmt.Errorf("init quick reply failed: %w", err)
|
||||
}
|
||||
|
||||
Vendored
+8
-8
@@ -35,12 +35,12 @@ Your goal is to explain the product accurately, assess whether it fits the custo
|
||||
|
||||
# Product positioning
|
||||
|
||||
AgentDesk is an open-source AI Agent customer support system that unifies online conversations, knowledge-base Q&A, AI-first service, human handoff, the agent workspace, customer and conversation management, ticket follow-up, channel integration, and private deployment. It is not merely an LLM embedded in a chat box; it enables AI, knowledge bases, human agents, and tickets to work together in one support workflow.
|
||||
AgentDesk is an open-source AI Agent customer support system that unifies online conversations, knowledge-base Q&A, AI-first service, human handoff, the agent workspace, conversation management, channel integration, and private deployment.
|
||||
|
||||
Use the bound knowledge base as the source of truth when describing capabilities. You may answer and qualify requirements around:
|
||||
- Product positioning, suitable teams, and typical support scenarios;
|
||||
- AI Agents, knowledge-base RAG, model configuration, Skills, Workflows, and MCP Tools;
|
||||
- AI and human collaboration, handoff, teams, schedules, conversations, and ticket workflows;
|
||||
- AI Agents, knowledge-base RAG, model configuration, and fixed business diagnostic tools;
|
||||
- AI and human collaboration, handoff, teams, schedules, and conversations;
|
||||
- Web Widget, channel integration, the admin console, and the agent workspace;
|
||||
- Local evaluation, Docker Compose, private deployment, and secondary development;
|
||||
- Differences from basic chatbots and traditional support systems.
|
||||
@@ -49,7 +49,7 @@ Use the bound knowledge base as the source of truth when describing capabilities
|
||||
|
||||
1. Answer the user's current question first, then ask one or two essential follow-up questions only when useful.
|
||||
2. For general inquiries, briefly explain the product positioning and ask about the customer's scenario or primary concern.
|
||||
3. For product evaluation, prioritize the business scenario, customer channels, inquiry volume, private-deployment needs, existing knowledge and model setup, human handoff, and ticket follow-up requirements.
|
||||
3. For product evaluation, prioritize the business scenario, customer channels, inquiry volume, private-deployment needs, existing knowledge and model setup, and human handoff requirements.
|
||||
4. When the requirement matches current capabilities, explain the fit and offer an actionable next step, such as reviewing a feature, preparing the deployment environment, trying a demo, or contacting a human consultant.
|
||||
5. Clearly distinguish current standard capabilities from features that require secondary development. Never present extensibility as an out-of-the-box feature.
|
||||
6. When comparing products, describe only verifiable differences. Do not disparage competitors or invent competitor information.
|
||||
@@ -94,12 +94,12 @@ Use the bound knowledge base as the source of truth when describing capabilities
|
||||
|
||||
# 产品定位
|
||||
|
||||
贝壳AI是一套开源的 AI Agent 客服系统,围绕真实客服链路统一在线咨询、知识库问答、AI 优先接待、人工接管、客服工作台、客户与会话管理、工单跟进、渠道接入和私有化部署。它不是单纯把大模型接入聊天框,而是让 AI、知识库、人工客服和工单在同一套系统中协同工作。
|
||||
贝壳AI是一套开源的 AI Agent 客服系统,围绕真实客服链路统一在线咨询、知识库问答、AI 优先接待、人工接管、客服工作台、会话管理、渠道接入和私有化部署。
|
||||
|
||||
介绍能力时,以已绑定知识库中的信息为准。可以围绕以下方向回答和梳理需求:
|
||||
- 产品定位、适用团队和典型客服场景;
|
||||
- AI Agent、知识库 RAG、模型配置、Skills、Workflow 与 MCP Tool;
|
||||
- AI 与人工客服协同、转人工、客服组、排班、会话和工单闭环;
|
||||
- AI Agent、知识库 RAG、模型配置与系统内置业务诊断工具;
|
||||
- AI 与人工客服协同、转人工、客服组、排班和会话;
|
||||
- Web Widget、渠道接入、管理后台与客服工作台;
|
||||
- 本地体验、Docker Compose、私有化部署和二次开发;
|
||||
- 与普通聊天机器人、传统客服系统的差异。
|
||||
@@ -108,7 +108,7 @@ Use the bound knowledge base as the source of truth when describing capabilities
|
||||
|
||||
1. 先直接回答用户当前问题,再根据需要提出 1 至 2 个关键问题,不要一开始连续盘问。
|
||||
2. 当用户只是泛泛了解时,先用简短语言说明产品定位,再询问其业务场景或最关心的能力。
|
||||
3. 当用户在做选型时,优先了解:业务场景、客户接入渠道、咨询量、是否需要私有化部署、现有知识库与模型条件、是否需要人工接管和工单跟进。
|
||||
3. 当用户在做选型时,优先了解:业务场景、客户接入渠道、咨询量、是否需要私有化部署、现有知识库与模型条件,以及是否需要人工接管。
|
||||
4. 当需求与现有能力匹配时,说明匹配点,并给出可执行的下一步,例如查看相关能力、准备部署环境、体验演示或联系人工顾问。
|
||||
5. 当需求只可通过二次开发实现时,明确区分“当前标准能力”和“可扩展方向”,不要把可定制能力说成开箱即用。
|
||||
6. 当用户比较其他产品时,基于可确认的能力客观说明差异,不贬低竞品,不编造竞品信息。
|
||||
|
||||
Vendored
+11
-21
@@ -19,13 +19,13 @@ func FAQKnowledgeBaseSeed(lang seedlang.Language) KnowledgeBaseSeed {
|
||||
if lang == seedlang.English {
|
||||
return KnowledgeBaseSeed{
|
||||
Name: "AgentDesk Support Platform FAQ",
|
||||
Description: "FAQ test data that simulates real support scenarios, covering accounts, agents, AI bots, knowledge bases, tickets, billing, invoices, and troubleshooting.",
|
||||
Description: "FAQ test data that simulates real support scenarios, covering accounts, agents, AI bots, knowledge bases, billing, invoices, and troubleshooting.",
|
||||
Remark: "Generated by testdata initialization",
|
||||
}
|
||||
}
|
||||
return KnowledgeBaseSeed{
|
||||
Name: "贝壳客服平台 FAQ",
|
||||
Description: "模拟真实客服场景的 FAQ 测试数据,覆盖账号、坐席、机器人、知识库、工单、计费与发票等常见问题。",
|
||||
Description: "模拟真实客服场景的 FAQ 测试数据,覆盖账号、坐席、机器人、知识库、计费与发票等常见问题。",
|
||||
Remark: "测试数据初始化自动生成",
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,6 @@ func englishKnowledgeFAQSeeds() []KnowledgeFAQSeed {
|
||||
{Question: "Why does an uploaded document show indexing failed?", Answer: "Common causes include empty documents, text copied as images, excessive content length, unavailable vector service, or model configuration errors. Check the indexing error first, then convert the file to plain text or Markdown and upload it again if needed.", SimilarQuestions: []string{"document indexing failed", "knowledge processing failed", "upload error after indexing"}, Remark: "Knowledge base"},
|
||||
{Question: "How do I embed the website support button on our site?", Answer: "Open Channel Access > Web Widget, copy the generated script, and paste it before the closing body tag on your website. If the site uses a Content Security Policy, add the platform domain to the allowed list. After publishing, test both desktop and mobile pages.", SimilarQuestions: []string{"embed web widget", "website chat button", "install support script"}, Remark: "Channel access"},
|
||||
{Question: "Can the web widget use our brand color and logo?", Answer: "Yes. In the web channel configuration, set the title, subtitle, theme color, position, width, and brand assets. After saving, refresh the website page. If CDN caching is enabled, it may take a few minutes for the change to appear.", SimilarQuestions: []string{"custom web widget brand", "change chat color", "support widget logo"}, Remark: "Channel access"},
|
||||
{Question: "How do I create a support ticket from a conversation?", Answer: "In the conversation detail page, click Create Ticket, select the issue category and priority, fill in the title and description, then submit. Conversation context can be linked to the ticket so the follow-up team can see the original messages.", SimilarQuestions: []string{"create ticket from chat", "turn conversation into ticket", "submit support ticket"}, Remark: "Tickets"},
|
||||
{Question: "What information should be included when reporting an urgent issue?", Answer: "Include the issue time, organization ID, affected channel or customer scope, screenshots, error messages, and reproduction steps. Clear impact and steps help the support team triage faster and avoid repeated clarification.", SimilarQuestions: []string{"urgent incident information", "report system failure", "what to include in a ticket"}, Remark: "Tickets"},
|
||||
{Question: "How do I configure automatic welcome messages?", Answer: "Enable the welcome message in the channel or bot configuration. You can use different text by working hours, channel, customer tag, or page URL. Keep the message concise and clarify whether the current service is AI or human.", SimilarQuestions: []string{"automatic greeting", "welcome message setup", "first message to customer"}, Remark: "Automation"},
|
||||
{Question: "Can keywords automatically trigger human handoff?", Answer: "Yes. Add handoff rules in bot strategy and enter keywords such as complaint, refund, human agent, or invoice reissue. Include synonyms and common user phrasing to reduce missed matches.", SimilarQuestions: []string{"keyword handoff", "words trigger human agent", "automatic human transfer"}, Remark: "Automation"},
|
||||
{Question: "How do I view conversation volume trends and peak hours?", Answer: "Open Data Reports > Traffic Analysis to view conversations, visitors, queue peaks, and human service rate by hour, date, and channel. Peak-hour analysis should be compared with staffing schedules.", SimilarQuestions: []string{"conversation trend report", "peak traffic hours", "traffic analytics"}, Remark: "Reports"},
|
||||
@@ -81,7 +79,7 @@ func chineseKnowledgeFAQSeeds() []KnowledgeFAQSeed {
|
||||
{Question: "为什么收不到登录验证码邮件?", Answer: "请先检查垃圾邮箱、广告邮件和企业邮箱的安全隔离区。若 5 分钟内仍未收到,建议确认邮箱地址是否填写正确,并联系企业 IT 将平台发信域名加入白名单。如果多次重发都未收到,可能是该邮箱服务商限流,建议改用备用邮箱。", SimilarQuestions: []string{"验证码邮件收不到", "邮箱没有收到验证码", "登录验证码不见了"}, Remark: "账号登录"},
|
||||
{Question: "同一个账号可以多人同时登录吗?", Answer: "不建议多人共用同一个坐席账号。平台默认允许同账号在多个设备登录,但会记录登录日志并触发异常提醒。为保证操作留痕、权限隔离和会话分配准确,建议每位坐席使用独立账号。", SimilarQuestions: []string{"一个账号能不能多人共用", "支持多人同时登录同一账号吗", "账号能在多台电脑登录吗"}, Remark: "账号登录"},
|
||||
{Question: "新成员加入后如何开通后台账号?", Answer: "企业管理员进入“组织设置-成员管理”,点击“新增成员”,填写姓名、邮箱、所属团队和角色后保存。系统会自动发送激活邮件,成员首次登录时设置密码即可。若你们开通了单点登录,也可以直接从企业身份系统同步成员。", SimilarQuestions: []string{"怎么给新客服开账号", "新增员工账号在哪里", "成员怎么加入后台"}, Remark: "成员管理"},
|
||||
{Question: "成员离职后如何停用账号?", Answer: "请在“组织设置-成员管理”中找到对应成员,点击“停用”即可。停用后该账号无法继续登录,但历史会话、工单处理记录和质检数据会保留,不会影响报表统计。若后续确认不再使用,也可以在完成交接后删除账号。", SimilarQuestions: []string{"离职员工账号怎么处理", "怎么禁用成员账号", "停用客服账号"}, Remark: "成员管理"},
|
||||
{Question: "成员离职后如何停用账号?", Answer: "请在统一用户系统中找到对应成员并停用。停用后该账号无法继续登录,但历史会话和质检数据仍会保留,不影响报表统计。", SimilarQuestions: []string{"离职员工账号怎么处理", "怎么禁用成员账号", "停用客服账号"}, Remark: "成员管理"},
|
||||
{Question: "角色权限修改后多久生效?", Answer: "角色权限保存后通常即时生效。已在线的成员可能需要刷新页面或重新登录,才能拿到最新权限菜单和接口授权。如果修改后仍能访问原页面,请清理浏览器缓存后再试。", SimilarQuestions: []string{"权限修改什么时候生效", "调整角色后没变化", "角色更新后要重登吗"}, Remark: "成员管理"},
|
||||
{Question: "坐席在线、忙碌、离线状态有什么区别?", Answer: "在线表示可正常接待新会话,忙碌表示当前暂不分配新会话但仍可处理已有会话,离线表示不参与会话分配也不接收实时提醒。若开启自动状态切换,长时间无操作或退出登录后,系统会自动变更为离线。", SimilarQuestions: []string{"客服状态怎么理解", "在线忙碌离线区别", "坐席状态说明"}, Remark: "坐席接待"},
|
||||
{Question: "会话是怎么分配给坐席的?", Answer: "默认按技能组和轮询策略分配,也可结合坐席当前负载、最近响应时长和优先级规则进行智能分流。若客户命中了指定渠道、语言或标签条件,系统会优先路由到匹配该条件的团队或坐席。", SimilarQuestions: []string{"客户咨询怎么分配", "会话路由规则是什么", "新会话按什么分给客服"}, Remark: "坐席接待"},
|
||||
@@ -109,22 +107,14 @@ func chineseKnowledgeFAQSeeds() []KnowledgeFAQSeed {
|
||||
{Question: "网站咨询按钮怎么嵌入到官网?", Answer: "进入“渠道接入-Web Widget”,复制系统生成的脚本代码,粘贴到官网页面的 `</body>` 前即可。若你们站点启用了 CSP,需要把平台域名加入允许列表,否则组件可能加载失败。", SimilarQuestions: []string{"官网怎么挂咨询入口", "Web Widget 怎么接", "网站客服按钮嵌入"}, Remark: "渠道接入"},
|
||||
{Question: "Web Widget 的颜色和文案可以自定义吗?", Answer: "可以。你可以在渠道配置里修改主色、标题、欢迎语、按钮文案、是否展示头像和工作时间提示。保存后前端会在几分钟内刷新缓存,若你希望立即生效,可手动清理页面缓存。", SimilarQuestions: []string{"咨询浮窗能改样式吗", "按钮文案怎么改", "Widget 主题色设置"}, Remark: "渠道接入"},
|
||||
{Question: "支持把客服入口嵌到微信 H5 页面吗?", Answer: "支持,但需要使用适配移动端的 H5 咨询页或自定义嵌入页。若在微信内打开,建议同时开启微信浏览器兼容模式,并测试键盘弹起、页面滚动和文件上传权限是否正常。", SimilarQuestions: []string{"H5 页面能接客服吗", "微信里能打开咨询页吗", "移动端客服入口"}, Remark: "渠道接入"},
|
||||
{Question: "客户消息提醒可以推送到企业微信吗?", Answer: "支持把新会话、超时未回复、工单升级等提醒推送到企业微信机器人或应用消息。建议只推送关键事件,避免通知过载影响值班人员判断。", SimilarQuestions: []string{"消息提醒发企业微信", "能推送到企微吗", "新会话通知怎么接"}, Remark: "渠道接入"},
|
||||
{Question: "客户消息提醒可以推送到企业微信吗?", Answer: "支持把新会话、超时未回复等提醒推送到企业微信机器人或应用消息。建议只推送关键事件,避免通知过载影响值班人员判断。", SimilarQuestions: []string{"消息提醒发企业微信", "能推送到企微吗", "新会话通知怎么接"}, Remark: "渠道接入"},
|
||||
{Question: "访客进入咨询前能先收集手机号吗?", Answer: "可以。你可以在欢迎页开启预采集表单,要求客户填写手机号、订单号、邮箱等信息后再进入会话。这样有助于后续识别身份和分配对应业务团队。", SimilarQuestions: []string{"咨询前收集手机号", "先填表再聊天", "访客信息预采集"}, Remark: "渠道接入"},
|
||||
{Question: "支持接入 WhatsApp 或 Telegram 吗?", Answer: "平台可以通过开放接口或第三方集成中间层接入海外渠道,但具体能力取决于你们当前套餐和所选服务商。若是正式商用,建议先确认消息模板、号码资质和当地合规要求。", SimilarQuestions: []string{"能接 WhatsApp 吗", "支持 Telegram 吗", "海外渠道接入"}, Remark: "渠道接入"},
|
||||
{Question: "为什么网站上看不到客服浮窗?", Answer: "先检查脚本是否成功加载、站点域名是否在渠道白名单内,以及浏览器是否拦截了第三方脚本。若开启了广告拦截插件或严格 CSP,也可能导致组件被屏蔽。", SimilarQuestions: []string{"网页不显示咨询按钮", "Widget 没出来", "客服浮窗不见了"}, Remark: "渠道接入"},
|
||||
{Question: "不同站点可以共用一个客服渠道吗?", Answer: "可以共用,但更建议按站点或品牌拆分渠道,这样可以分别配置欢迎语、机器人、工作时间和报表来源。若多个站点业务差异较大,共用一个渠道会影响会话分流和数据分析。", SimilarQuestions: []string{"多个官网能共用渠道吗", "不同域名用一个 Widget", "站点渠道怎么规划"}, Remark: "渠道接入"},
|
||||
{Question: "工单和实时会话有什么关系?", Answer: "实时会话适合即时咨询,工单适合需要跨班次跟进、跨部门协作或需要留痕审批的问题。会话中如果发现问题无法当场解决,可以一键转为工单,并保留原始聊天记录作为上下文。", SimilarQuestions: []string{"为什么还需要工单", "会话和工单区别", "聊天怎么转工单"}, Remark: "工单"},
|
||||
{Question: "如何把会话升级成工单?", Answer: "在会话详情页点击“创建工单”,系统会自动带出客户信息、会话摘要和最近消息。你只需补充工单类型、优先级、负责人和期望完成时间即可。", SimilarQuestions: []string{"聊天转工单在哪里", "会话升级工单", "怎么建售后单"}, Remark: "工单"},
|
||||
{Question: "工单支持 SLA 超时提醒吗?", Answer: "支持。你可以为不同工单类型配置首次响应时限、处理时限和升级规则,临近超时时会给负责人和主管发送提醒,超时后也可自动升级到上级处理。", SimilarQuestions: []string{"工单超时提醒", "SLA 怎么配置", "工单逾期通知"}, Remark: "工单"},
|
||||
{Question: "工单能分配给外部协作人吗?", Answer: "目前标准成员体系主要面向内部账号。如果需要外部协作,可为供应商或合作方单独开受限角色账号,并限制其仅查看被指派工单,避免访问其他客户数据。", SimilarQuestions: []string{"工单给外包处理", "外部人员能看工单吗", "供应商协作权限"}, Remark: "工单"},
|
||||
{Question: "工单状态有哪些推荐用法?", Answer: "常见做法是设置为“待受理、处理中、待客户反馈、已解决、已关闭”。其中“待客户反馈”适合需要客户补充材料的场景,“已解决”表示业务已处理完成但仍保留回访窗口。", SimilarQuestions: []string{"工单状态怎么设计", "售后单流程建议", "工单字段如何规划"}, Remark: "工单"},
|
||||
{Question: "能否查看工单处理的完整操作记录?", Answer: "可以。每张工单都保留状态变更、指派变更、备注、附件上传和评论记录,方便审计和复盘。管理员还可以导出操作日志做质检或合规留存。", SimilarQuestions: []string{"工单处理日志", "谁改过工单怎么查", "工单历史记录"}, Remark: "工单"},
|
||||
{Question: "工单附件支持哪些格式?", Answer: "常见图片、PDF、Excel、Word 和压缩包都支持,单文件大小上限由你们当前存储配置决定。若附件包含客户证件或敏感资料,建议同步开启下载权限控制和水印。", SimilarQuestions: []string{"工单能上传什么文件", "附件格式限制", "售后凭证支持哪些类型"}, Remark: "工单"},
|
||||
{Question: "重复提交的工单可以自动合并吗?", Answer: "可以通过规则按手机号、订单号、邮箱或自定义字段检测重复,并提示坐席合并处理。是否自动合并建议谨慎开启,避免把不同问题错误归并到同一张工单。", SimilarQuestions: []string{"重复工单怎么处理", "能自动识别重复吗", "相同订单重复建单"}, Remark: "工单"},
|
||||
{Question: "客户信息可以从 CRM 自动同步过来吗?", Answer: "支持通过开放 API、Webhook 或中间件同步客户主数据,例如姓名、手机号、会员等级、所属销售和最近订单。同步后这些字段可以直接在会话侧边栏展示,减少客服来回切系统查询。", SimilarQuestions: []string{"CRM 能同步到客服吗", "客户资料自动带入", "怎么对接用户信息"}, Remark: "集成"},
|
||||
{Question: "平台提供开放 API 吗?", Answer: "提供。你可以通过 API 创建会话、发送消息、查询客户、同步工单和拉取报表。正式对接前建议先在测试环境验证签名、限流和错误码处理,再切换到生产。", SimilarQuestions: []string{"有没有开放接口", "客服系统 API 文档", "能程序化调用吗"}, Remark: "集成"},
|
||||
{Question: "Webhook 可以推送哪些事件?", Answer: "常见事件包括新会话创建、会话关闭、客户留言、工单创建、工单状态变更、机器人转人工和客户满意度回收等。你可以按需订阅,避免把所有事件都推到业务系统。", SimilarQuestions: []string{"Webhook 支持什么事件", "事件推送列表", "回调通知有哪些"}, Remark: "集成"},
|
||||
{Question: "平台提供开放 API 吗?", Answer: "提供。你可以通过 API 创建会话、发送消息和拉取报表。正式对接前建议先在测试环境验证签名、限流和错误码处理,再切换到生产。", SimilarQuestions: []string{"有没有开放接口", "客服系统 API 文档", "能程序化调用吗"}, Remark: "集成"},
|
||||
{Question: "Webhook 可以推送哪些事件?", Answer: "常见事件包括新会话创建、会话关闭、客户留言、机器人转人工和客户满意度回收等。你可以按需订阅,避免把所有事件都推到业务系统。", SimilarQuestions: []string{"Webhook 支持什么事件", "事件推送列表", "回调通知有哪些"}, Remark: "集成"},
|
||||
{Question: "API 调用频率有限制吗?", Answer: "有。默认按应用和接口维度做限流,避免高峰期影响平台稳定。若你们需要批量同步历史数据,建议走离线导入或提前联系技术支持申请更高配额。", SimilarQuestions: []string{"接口限流是多少", "API 有 QPS 限制吗", "批量同步会不会被限流"}, Remark: "集成"},
|
||||
{Question: "如何验证开放 API 的签名是否正确?", Answer: "请先确认时间戳、随机串、请求体摘要和签名算法与文档一致。排查时建议先用平台提供的示例请求对比,再检查服务端是否在参与签名的原始字符串里改动了空格、换行或字段顺序。", SimilarQuestions: []string{"API 签名不通过", "签名校验失败怎么办", "接口鉴权报错"}, Remark: "集成"},
|
||||
{Question: "可以把会话记录同步到内部 BI 系统吗?", Answer: "可以。你可以通过报表导出、API 增量拉取或消息回调三种方式同步。若是 BI 场景,建议每天离线拉取聚合数据,避免用高频实时接口增加系统压力。", SimilarQuestions: []string{"会话数据怎么同步 BI", "报表能对接数仓吗", "聊天记录导入分析系统"}, Remark: "集成"},
|
||||
@@ -135,11 +125,11 @@ func chineseKnowledgeFAQSeeds() []KnowledgeFAQSeed {
|
||||
{Question: "平台支持数据脱敏吗?", Answer: "支持。你可以对手机号、身份证号、银行卡号、邮箱和地址启用显示脱敏、日志脱敏以及导出脱敏。对于高敏字段,建议同时配置按角色可见范围。", SimilarQuestions: []string{"客户信息能脱敏吗", "隐私字段隐藏", "敏感数据保护"}, Remark: "数据安全"},
|
||||
{Question: "是否支持按角色限制查看聊天记录?", Answer: "支持。你可以限制普通坐席只能查看自己接待过的会话,主管查看本团队,管理员查看全局。对于投诉、法务等敏感会话,也可以单独设置更严格的访问范围。", SimilarQuestions: []string{"聊天记录权限隔离", "谁能看全部会话", "会话查看范围"}, Remark: "数据安全"},
|
||||
{Question: "客户要求删除个人数据时怎么处理?", Answer: "管理员可在客户资料页发起“数据删除”或“匿名化处理”。系统会按配置清空或打码可识别字段,同时保留必要的审计记录,以满足合规要求和内部追溯。", SimilarQuestions: []string{"用户要求删数据", "隐私删除怎么做", "客户信息匿名化"}, Remark: "数据安全"},
|
||||
{Question: "系统有操作日志吗?", Answer: "有。成员登录、权限变更、知识库编辑、工单操作、导出报表等关键动作都会进入审计日志。管理员可以按时间、成员、对象类型筛选并导出。", SimilarQuestions: []string{"后台操作有记录吗", "谁改了配置怎么查", "审计日志在哪里"}, Remark: "数据安全"},
|
||||
{Question: "系统有操作日志吗?", Answer: "有。成员登录、权限变更、知识库编辑、会话操作和导出报表等关键动作都会进入审计日志。管理员可以按时间、成员、对象类型筛选并导出。", SimilarQuestions: []string{"后台操作有记录吗", "谁改了配置怎么查", "审计日志在哪里"}, Remark: "数据安全"},
|
||||
{Question: "支持设置 IP 白名单吗?", Answer: "支持。你可以在安全设置里为后台登录和开放 API 分别配置 IP 白名单。若你们办公网络经常变动,建议至少给高权限账号启用 MFA,避免完全依赖固定 IP。", SimilarQuestions: []string{"后台能限制 IP 吗", "接口白名单怎么配", "登录来源限制"}, Remark: "数据安全"},
|
||||
{Question: "聊天内容会不会被平台拿去训练公共模型?", Answer: "默认不会。客户数据仅用于你们自身的业务处理和已授权的产品功能,不会擅自用于公共模型训练。若你们开通了定制优化服务,也会以合同和配置项约定的数据范围为准。", SimilarQuestions: []string{"聊天数据会训练模型吗", "数据会不会外泄", "平台会拿客户数据训练吗"}, Remark: "数据安全"},
|
||||
{Question: "如何给客户打标签?", Answer: "可以在客户详情页手动添加标签,也可以通过规则根据来源渠道、访问页面、下单次数、会员等级或对话关键词自动打标签。标签通常用于分流、营销和服务分层。", SimilarQuestions: []string{"客户标签怎么加", "支持自动标签吗", "用户标签规则"}, Remark: "客户管理"},
|
||||
{Question: "客户历史会话在哪里看?", Answer: "打开客户资料页即可看到该客户的历史会话、工单、满意度评价和最近访问记录。若同一个客户用多个渠道接入,建议先配置身份合并规则,避免历史被拆散。", SimilarQuestions: []string{"怎么查客户历史咨询", "用户轨迹在哪里", "以前的聊天记录怎么看"}, Remark: "客户管理"},
|
||||
{Question: "客户历史会话在哪里看?", Answer: "打开在线咨询并筛选对应用户,即可查看历史会话和消息。若同一个用户从多个渠道接入,应由业务系统提供统一身份标识,避免历史被拆散。", SimilarQuestions: []string{"怎么查客户历史咨询", "用户轨迹在哪里", "以前的聊天记录怎么看"}, Remark: "会话管理"},
|
||||
{Question: "一个客户在多个渠道咨询,会被识别成同一个人吗?", Answer: "可以,但需要提前配置统一身份标识,例如手机号、会员 ID、邮箱或外部用户 ID。若不同渠道没有共同标识,系统会默认视为不同访客。", SimilarQuestions: []string{"多渠道客户合并", "同一个人跨渠道识别", "用户身份统一"}, Remark: "客户管理"},
|
||||
{Question: "如何筛选高价值客户并优先接待?", Answer: "你可以结合会员等级、近 90 天消费金额、订单频次或 VIP 标签建立高价值客户规则,并在路由策略里设置优先分配到专属团队或高级坐席。", SimilarQuestions: []string{"VIP 客户优先接待", "高价值用户怎么识别", "客户分层服务"}, Remark: "客户管理"},
|
||||
{Question: "客户昵称乱码或显示异常怎么办?", Answer: "优先确认上游渠道返回的编码是否为 UTF-8,以及是否包含平台不支持的特殊字符。若只是个别历史数据异常,可通过客户资料页手动更正;若批量异常,建议检查同步接口。", SimilarQuestions: []string{"昵称显示乱码", "客户名称异常", "中文昵称不正常"}, Remark: "客户管理"},
|
||||
@@ -151,7 +141,7 @@ func chineseKnowledgeFAQSeeds() []KnowledgeFAQSeed {
|
||||
{Question: "如何设置关键词自动转人工?", Answer: "在机器人策略里新增转人工规则,输入关键词或短语即可,例如“投诉”“退款”“人工客服”“发票重开”等。建议同时加入同义词和常见口语表达,减少漏判。", SimilarQuestions: []string{"关键词触发人工", "哪些词会转接客服", "自动转人工规则"}, Remark: "自动化"},
|
||||
{Question: "会话超时未回复能自动提醒坐席吗?", Answer: "可以。你可以按首响超时、处理中超时和即将 SLA 超时三个阶段配置提醒,支持站内提醒、邮件和企业微信通知。", SimilarQuestions: []string{"超时提醒怎么配", "客服久未回复提醒", "消息超时通知"}, Remark: "自动化"},
|
||||
{Question: "能按客户标签分配不同的机器人吗?", Answer: "支持。你可以在路由规则里按客户标签、渠道来源或页面入口命中不同机器人,例如新客走导购机器人,老客走售后机器人。", SimilarQuestions: []string{"不同用户进不同 AI", "按标签分机器人", "机器人路由规则"}, Remark: "自动化"},
|
||||
{Question: "能自动给会话生成摘要吗?", Answer: "支持。在开启 AI 摘要后,系统会在会话结束时生成问题摘要、处理结果和待跟进事项,便于转工单、交班和质检。", SimilarQuestions: []string{"聊天自动总结", "会话摘要功能", "交班摘要怎么生成"}, Remark: "自动化"},
|
||||
{Question: "能自动给会话生成摘要吗?", Answer: "支持。在开启 AI 摘要后,系统会在会话结束时生成问题摘要、处理结果和待跟进事项,便于交班和质检。", SimilarQuestions: []string{"聊天自动总结", "会话摘要功能", "交班摘要怎么生成"}, Remark: "自动化"},
|
||||
{Question: "自动化规则执行顺序是怎样的?", Answer: "通常按“接入识别 -> 路由分配 -> 机器人应答 -> 转人工/升级 -> 会后自动化”的顺序执行。若多条规则都命中,系统会按优先级和创建顺序决定实际结果。", SimilarQuestions: []string{"规则先后顺序", "自动化命中顺序", "多个规则冲突怎么办"}, Remark: "自动化"},
|
||||
{Question: "支持根据访问页面触发不同欢迎语吗?", Answer: "支持。你可以在 Web 渠道里按 URL 路径或页面分组配置欢迎语,比如商品页引导咨询库存,支付页引导咨询优惠和支付问题。", SimilarQuestions: []string{"不同页面不同文案", "页面维度欢迎语", "按 URL 展示话术"}, Remark: "自动化"},
|
||||
{Question: "如何查看会话量趋势和高峰时段?", Answer: "在“数据报表-流量分析”里可按小时、日期和渠道查看会话量、访客量、排队峰值和人工接待率。高峰时段建议结合排班数据一起分析。", SimilarQuestions: []string{"会话高峰怎么看", "流量趋势报表", "哪个时间段最忙"}, Remark: "数据报表"},
|
||||
@@ -184,10 +174,10 @@ func chineseKnowledgeFAQSeeds() []KnowledgeFAQSeed {
|
||||
{Question: "Logo 替换后前端多久刷新?", Answer: "通常几分钟内会生效,具体取决于 CDN 缓存时间。若你在后台已经看到新 Logo,但前台仍未更新,建议清空浏览器缓存或稍后再试。", SimilarQuestions: []string{"换 Logo 后没生效", "品牌图标多久更新", "前端缓存多久"}, Remark: "品牌配置"},
|
||||
{Question: "支持多组织或多租户统一管理吗?", Answer: "支持企业下管理多个组织,但权限和数据隔离方式需按实际业务设计。若是完全独立运营的品牌或国家站点,通常建议拆成独立组织。", SimilarQuestions: []string{"多租户支持吗", "多个子公司统一管", "多组织架构"}, Remark: "品牌配置"},
|
||||
{Question: "如何申请产品培训或上线辅导?", Answer: "你可以联系客户成功经理预约标准培训、管理员培训或机器人调优辅导。首次上线建议安排一次管理员培训和一次一线坐席培训,能明显减少上线初期问题。", SimilarQuestions: []string{"有没有培训服务", "上线辅导怎么预约", "员工使用培训"}, Remark: "客户成功"},
|
||||
{Question: "遇到紧急故障,最快如何联系支持团队?", Answer: "若购买了企业服务,可通过专属工单通道、服务群或紧急支持电话联系。提交时请尽量附上问题时间、组织 ID、影响范围、截图和复现步骤,便于快速定位。", SimilarQuestions: []string{"紧急问题联系谁", "系统故障怎么报", "售后支持入口"}, Remark: "客户成功"},
|
||||
{Question: "遇到紧急故障,最快如何联系支持团队?", Answer: "若购买了企业服务,可通过服务群或紧急支持电话联系。反馈时请尽量附上问题时间、组织 ID、影响范围、截图和复现步骤,便于快速定位。", SimilarQuestions: []string{"紧急问题联系谁", "系统故障怎么报", "售后支持入口"}, Remark: "客户成功"},
|
||||
{Question: "产品更新公告在哪里看?", Answer: "你可以在后台首页公告栏、帮助中心更新日志或服务群中查看版本发布说明。涉及影响配置或操作习惯的变更,平台一般会提前通知。", SimilarQuestions: []string{"版本更新在哪里看", "发布说明入口", "新功能公告"}, Remark: "客户成功"},
|
||||
{Question: "能提供上线前的最佳实践建议吗?", Answer: "可以。标准建议包括先梳理高频问题 FAQ、配置清晰的转人工策略、按业务拆分知识库、先从一个渠道灰度上线,再逐步扩展到全部渠道。", SimilarQuestions: []string{"上线前准备什么", "机器人落地建议", "客服系统实施建议"}, Remark: "客户成功"},
|
||||
{Question: "平台支持数据迁移服务吗?", Answer: "支持按项目评估。常见迁移内容包括历史客户资料、会话记录、FAQ、工单和成员账号。由于不同系统字段差异较大,迁移前通常需要做一次字段映射确认。", SimilarQuestions: []string{"从旧系统迁移数据", "历史消息能导入吗", "数据迁移服务"}, Remark: "客户成功"},
|
||||
{Question: "平台支持数据迁移服务吗?", Answer: "支持按项目评估。常见迁移内容包括历史会话记录和 FAQ。由于不同系统字段差异较大,迁移前通常需要做一次字段映射确认。", SimilarQuestions: []string{"从旧系统迁移数据", "历史消息能导入吗", "数据迁移服务"}, Remark: "客户成功"},
|
||||
{Question: "如何判断当前 FAQ 是否需要优化?", Answer: "可以优先看三类信号:命中高但转人工率高、命中高但满意度低、以及客户经常追问同一问题。出现这些情况时,通常说明答案不够完整、口径不一致,或相似问覆盖不够。", SimilarQuestions: []string{"FAQ 优化依据", "哪些问答该先改", "知识库效果怎么评估"}, Remark: "知识运营"},
|
||||
{Question: "FAQ 的答案建议写多长?", Answer: "建议先给出结论,再补充步骤和注意事项。大多数客服 FAQ 控制在 80 到 220 字效果较好,太短容易信息不全,太长又不利于机器人稳定引用和客户快速阅读。", SimilarQuestions: []string{"FAQ 答案长度建议", "回答写多长合适", "问答内容怎么控制"}, Remark: "知识运营"},
|
||||
{Question: "一个问题有多个业务口径,FAQ 应该怎么处理?", Answer: "不要把多个冲突口径塞进同一条 FAQ。更合理的做法是按前置条件拆分,比如“个人版如何退款”和“企业版如何退款”分别建条目,并在答案开头明确适用范围。", SimilarQuestions: []string{"FAQ 口径冲突怎么办", "同一问题多个答案", "知识条目怎么拆"}, Remark: "知识运营"},
|
||||
|
||||
Vendored
-103
@@ -1,103 +0,0 @@
|
||||
package seeds
|
||||
|
||||
import (
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
|
||||
)
|
||||
|
||||
type SkillDefinitionSeed struct {
|
||||
Name string
|
||||
Description string
|
||||
Instruction string
|
||||
Examples string
|
||||
ToolWhitelist string
|
||||
Status enums.Status
|
||||
Remark string
|
||||
}
|
||||
|
||||
func SkillDefinitionSeeds(lang seedlang.Language) []SkillDefinitionSeed {
|
||||
if lang == seedlang.English {
|
||||
return []SkillDefinitionSeed{
|
||||
{
|
||||
Name: "After-sales Escalation",
|
||||
Description: "Handles incidents, complaints, after-sales follow-up, ticket creation, and human handoff requests. Match only when the user clearly needs after-sales intervention or escalation; do not match ordinary greetings, product introductions, or general inquiries.",
|
||||
Instruction: `You are the dedicated "After-sales Escalation" skill responsible for customer support requests that require escalation.
|
||||
|
||||
Scope:
|
||||
1. Handle only these scenarios: incidents, complaints, unresolved issues, explicit ticket creation requests, explicit human handoff requests, or after-sales follow-up.
|
||||
2. If the user is only asking a general question, discussing product usage, greeting, or chatting casually, state that the current request is outside this skill's scope and avoid misclassifying it as an escalation.
|
||||
|
||||
Rules:
|
||||
1. First determine whether the user has clearly requested escalation. If not, ask concise follow-up questions in English, such as order number, product name, issue symptoms, actions already tried, and desired handling method.
|
||||
2. If the user explicitly asks to create or submit a ticket, prioritize the ticket flow and do not switch to human handoff on your own.
|
||||
3. If the user complains, reports an incident, or asks for after-sales follow-up but the information is scattered, first call graph/prepare_ticket_draft to organize a ticket draft, then ask for missing fields.
|
||||
4. Only call graph/create_ticket_with_confirmation when the user explicitly wants to submit a ticket, complaint, or incident report and the title and description are clear enough.
|
||||
5. Only call graph/handoff_to_human when the user explicitly requests a human agent, or when you determine that a human must continue and the request is not suitable for direct ticket creation.
|
||||
6. If the user mentions both "ticket" and "human agent", clarify the priority. If the user clearly says "create a ticket", assist with ticket creation first unless they explicitly ask again for immediate human handoff.
|
||||
7. Never claim in text that a ticket has been created or a human handoff has happened. Those actions must be performed through the corresponding tools.
|
||||
8. If there is not enough information for ticket creation or handoff, ask for clarification before taking an escalation action.
|
||||
|
||||
Response requirements:
|
||||
1. Use English throughout. Keep the tone professional, concise, and like a real support agent.
|
||||
2. Focus on issue diagnosis and escalation handling. Do not output unrelated self-introductions.
|
||||
3. When entering a confirmation flow, clearly tell the user you will help submit or transfer the request and wait for the confirmation result.`,
|
||||
Examples: `[
|
||||
"My device went offline today and restarting did not help. Please create a ticket.",
|
||||
"I confirm that I want to create a ticket, not transfer to a human agent.",
|
||||
"This issue has not been resolved for three days. I want to file a complaint.",
|
||||
"Please transfer me to a human agent. You cannot solve this.",
|
||||
"When will after-sales support contact me? No one has followed up on this failure.",
|
||||
"Help me report an incident. The product model is AX300 and it cannot connect to the network.",
|
||||
"I need after-sales support. This issue keeps happening."
|
||||
]`,
|
||||
ToolWhitelist: `[
|
||||
"graph/create_ticket_with_confirmation",
|
||||
"graph/handoff_to_human"
|
||||
]`,
|
||||
Status: enums.StatusOk,
|
||||
Remark: "after-sales escalation skill",
|
||||
},
|
||||
}
|
||||
}
|
||||
return []SkillDefinitionSeed{
|
||||
{
|
||||
Name: "售后升级处理",
|
||||
Description: "处理报障、投诉、售后跟进、建单、转人工等升级诉求。只在用户明确需要售后介入或问题升级处理时命中,不处理普通问候、产品介绍或泛咨询。",
|
||||
Instruction: `你是“售后升级处理”专项 Skill,负责承接需要升级处理的客服诉求。
|
||||
|
||||
你的职责边界:
|
||||
1. 仅处理以下场景:报障、投诉、问题久未解决、明确要求建单、明确要求转人工、要求售后继续跟进。
|
||||
2. 如果用户只是普通咨询、产品使用提问、寒暄、问候、闲聊,说明当前不属于本 Skill 的职责,避免误判为升级处理。
|
||||
|
||||
你的处理规则:
|
||||
1. 先判断用户是否已经明确表达升级诉求;如果还不明确,先用简洁中文追问关键事实,例如订单号、设备/产品名称、故障现象、已尝试过的操作、期望处理方式。
|
||||
2. 如果用户已经明确要求“创建工单 / 提工单 / 登记报障 / 提交投诉单”,应优先沿着建单流程推进,不要擅自改成转人工。
|
||||
3. 如果用户要投诉、报障或售后跟进,但信息比较散乱,优先调用 graph/prepare_ticket_draft 整理工单草稿,再根据缺失字段继续追问。
|
||||
4. 只有在用户明确希望提交工单、投诉单、报障单,且标题与问题描述已经足够清晰时,才调用 graph/create_ticket_with_confirmation。
|
||||
5. 只有在用户明确要求人工客服,或你已经判断必须人工继续处理且当前诉求不适合直接建单时,才调用 graph/handoff_to_human。
|
||||
6. 如果用户同时提到“建单”和“人工”,先澄清他的优先诉求;若用户已明确说“创建工单”,默认先协助建单,除非他再次明确要求立即转人工。
|
||||
7. 禁止只在文本里声称“已经建单”或“已经转人工”,相关动作必须通过对应工具执行。
|
||||
8. 如果信息不足以建单或转人工,先澄清,不要直接升级动作。
|
||||
|
||||
回复要求:
|
||||
1. 全程使用中文,语气专业、简洁、像真实客服。
|
||||
2. 优先围绕问题定位和升级处理推进,不要输出与当前诉求无关的自我介绍。
|
||||
3. 如果进入确认流程,明确告知用户你将协助提交或转接,并等待确认结果。`,
|
||||
Examples: `[
|
||||
"设备今天开始一直离线,重启也没用,帮我提个工单",
|
||||
"我已经确认要创建工单了,不要转人工",
|
||||
"这个问题三天了还没解决,我要投诉一下",
|
||||
"麻烦转人工,你这边解决不了",
|
||||
"售后什么时候联系我?这个故障还没有人跟进",
|
||||
"帮我登记一下报障,产品型号是AX300,无法联网",
|
||||
"我要申请售后处理,这个问题反复出现",
|
||||
]`,
|
||||
ToolWhitelist: `[
|
||||
"graph/create_ticket_with_confirmation",
|
||||
"graph/handoff_to_human"
|
||||
]`,
|
||||
Status: enums.StatusOk,
|
||||
Remark: "after-sales escalation skill",
|
||||
},
|
||||
}
|
||||
}
|
||||
Vendored
-37
@@ -1,37 +0,0 @@
|
||||
package seeds
|
||||
|
||||
import "code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
|
||||
|
||||
type TagSeed struct {
|
||||
ID int64
|
||||
ParentID int64
|
||||
Name string
|
||||
SortNo int
|
||||
}
|
||||
|
||||
func TagSeeds(lang seedlang.Language) []TagSeed {
|
||||
if lang == seedlang.English {
|
||||
return []TagSeed{
|
||||
{1, 0, "Pre-sales", 1},
|
||||
{2, 1, "AgentDesk", 1},
|
||||
{3, 2, "Product Inquiry", 1},
|
||||
{4, 2, "Purchase Intent", 1},
|
||||
{5, 0, "After-sales", 2},
|
||||
{6, 5, "AgentDesk", 1},
|
||||
{7, 6, "Issue Feedback", 1},
|
||||
{8, 6, "Product Deployment", 2},
|
||||
{9, 6, "Feature Request", 3},
|
||||
}
|
||||
}
|
||||
return []TagSeed{
|
||||
{1, 0, "售前", 1},
|
||||
{2, 1, "AgentDesk", 1},
|
||||
{3, 2, "产品咨询", 1},
|
||||
{4, 2, "购买意向", 1},
|
||||
{5, 0, "售后", 2},
|
||||
{6, 5, "AgentDesk", 1},
|
||||
{7, 6, "问题反馈", 1},
|
||||
{8, 6, "产品部署", 2},
|
||||
{9, 6, "需求工单", 3},
|
||||
}
|
||||
}
|
||||
Vendored
-70
@@ -1,70 +0,0 @@
|
||||
package skill
|
||||
|
||||
import (
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
type InitResult struct {
|
||||
Created int
|
||||
Updated int
|
||||
}
|
||||
|
||||
func Init(lang seedlang.Language) (*InitResult, error) {
|
||||
result := &InitResult{}
|
||||
seedItems := buildModels(lang)
|
||||
for _, item := range seedItems {
|
||||
itemCopy := item
|
||||
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
existing := repositories.SkillDefinitionRepository.Take(ctx.Tx, "name = ?", strings.TrimSpace(itemCopy.Name))
|
||||
if existing != nil {
|
||||
if err := ctx.Tx.Model(existing).Updates(&itemCopy).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result.Updated++
|
||||
return nil
|
||||
}
|
||||
if err := ctx.Tx.Create(&itemCopy).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result.Created++
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("upsert skill failed: %w", err)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildModels(lang seedlang.Language) []models.SkillDefinition {
|
||||
now := time.Now()
|
||||
seedItems := seeds.SkillDefinitionSeeds(lang)
|
||||
items := make([]models.SkillDefinition, 0, len(seedItems))
|
||||
for _, seed := range seedItems {
|
||||
items = append(items, models.SkillDefinition{
|
||||
Name: seed.Name,
|
||||
Description: seed.Description,
|
||||
Instruction: seed.Instruction,
|
||||
Examples: seed.Examples,
|
||||
ToolWhitelist: seed.ToolWhitelist,
|
||||
Status: seed.Status,
|
||||
Remark: seed.Remark,
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: 0,
|
||||
CreateUserName: "System",
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: 0,
|
||||
UpdateUserName: "System",
|
||||
},
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
package skill
|
||||
|
||||
import (
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var hanTextPattern = regexp.MustCompile(`\p{Han}`)
|
||||
|
||||
func TestEnglishSkillSeedDoesNotContainChineseText(t *testing.T) {
|
||||
for _, item := range seeds.SkillDefinitionSeeds(seedlang.English) {
|
||||
values := []string{item.Name, item.Description, item.Instruction, item.Examples, item.ToolWhitelist, item.Remark}
|
||||
for _, value := range values {
|
||||
if hanTextPattern.MatchString(value) {
|
||||
t.Fatalf("english skill seed contains Chinese text: %q", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-57
@@ -1,57 +0,0 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
|
||||
"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"
|
||||
"time"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
func Init(lang seedlang.Language) error {
|
||||
seed := seeds.TagSeeds(lang)
|
||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
now := time.Now()
|
||||
for _, row := range seed {
|
||||
existing := repositories.TagRepository.Get(ctx.Tx, row.ID)
|
||||
if existing == nil {
|
||||
tag := &models.Tag{
|
||||
ID: row.ID,
|
||||
ParentID: row.ParentID,
|
||||
Name: row.Name,
|
||||
Remark: "",
|
||||
SortNo: row.SortNo,
|
||||
Status: enums.StatusOk,
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: 0,
|
||||
CreateUserName: "",
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: 0,
|
||||
UpdateUserName: "",
|
||||
},
|
||||
}
|
||||
if err := repositories.TagRepository.Create(ctx.Tx, tag); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := repositories.TagRepository.Updates(ctx.Tx, row.ID, map[string]any{
|
||||
"parent_id": row.ParentID,
|
||||
"name": row.Name,
|
||||
"remark": "",
|
||||
"sort_no": row.SortNo,
|
||||
"status": enums.StatusOk,
|
||||
"updated_at": now,
|
||||
"update_user_id": 0,
|
||||
"update_user_name": "",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
Vendored
-18
@@ -1,18 +0,0 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
|
||||
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var hanTextPattern = regexp.MustCompile(`\p{Han}`)
|
||||
|
||||
func TestEnglishTagSeedsDoNotContainChineseText(t *testing.T) {
|
||||
for _, item := range seeds.TagSeeds(seedlang.English) {
|
||||
if hanTextPattern.MatchString(item.Name) {
|
||||
t.Fatalf("english tag seed contains Chinese text: %q", item.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user