惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog
The Cloudflare Blog
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
L
LangChain Blog
Recent Announcements
Recent Announcements
Hugging Face - Blog
Hugging Face - Blog
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX
I
InfoQ
博客园 - 司徒正美
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
aimingoo的专栏
aimingoo的专栏
小众软件
小众软件
H
Help Net Security
博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

博客园 - 唯一客服系统开发笔记

十几年后端开发,承接网站/小程序开发接单 worbuddy上线静态页面实现在线客服咨询 Deepseek Harness 安装和使用 拆解当下 AI 智能体真实水平:能力很强,但落地短板卡死商业化 基于扣子旧版智能体搭建专属 AI 客服,打通抖音私信、电商直播全渠道对接 唯一客服系统聊天链接传参教程:精准识别访客、展示会员信息与商品卡片 唯一客服系统已适配达梦数据库 抖音开放平台企业号私信新规解读|7 月 6 日起私信手机号强制脱敏,服务商该如何适配? MySQL 迁移达梦数据库 达梦数据库 Windows 安装 + 基础操作 企业微信怎么设置投诉页面 汽修行业AI知识库应用到抖店智能咨询 快手直播间弹幕评论抓取 知识库 AI Excel表格数据上传处理 扣子知识库 AI 智能体完整创建、对接实操教程 客服私有化部署 + 抖音私信接入说明 抖店飞鸽客服自动回复机器人插件使用 网站客服系统与电商客服自动化工具 唯一客服浏览器插件自动回复抖音评论私信 拼多多客服如何实现24小时全自动值守、大幅降本增效?这款自动化插件帮你轻松破局! 扣子 AI 智能体对接企业微信客服 抖音私信和评论网页端浏览器自动化插件 功能详解 批量扒完抖音评论还能发私信 | 本地开源工具 唯一客服企业微信侧边栏客户画像:让客服在聊天时直接查看客户详情、进群记录和跟进轨迹 自定义微信卡片链接生成工具:自由设置标题、描述与封面图 唯一客服系统接入自定义大模型:一行代码都不用改,写个 HTTP 接口就够了 唯一客服系统多智能体架构深度解析:一个后台,四种 AI 引擎随心切换 GoLiveChat:Golang独立部署海外英文在线客服系统全解析 抖音无痕引流神器!评论自动回复藏微表情,规避系统检测,自动引私域 抖音评论区全自动表情回复插件-引流推广小技巧,有人评论自动化回表情
从零搭建企业微信 AI 客服:Hook 注入 + 扣子知识库实战
唯一客服系统开发笔记 · 2026-06-04 · via 博客园 - 唯一客服系统开发笔记

让你的企业微信员工号秒变 AI 智能客服,支持私聊和群聊自动回复。

原文地址


一、项目背景

企业微信已成为国内企业内外沟通的核心工具。但员工号需要人工回复客户消息,效率低、响应慢。有没有办法让一个普普通通的企业微信员工号,自动拥有 AI 大脑,7×24 小时智能回复客户?

答案是:企业微信 Hook 注入协议 + 扣子(Coze)知识库机器人

本项目完整实现了这一方案——使用 Go 语言构建了一套轻量级的 SCRM 服务,通过 Hook 协议与企业微信客户端通信,接入扣子知识库 API,实现私聊&群聊全自动回复。


二、整体架构

flowchart LR subgraph 企业微信客户端 A[企业微信<br/>注入版客户端] end subgraph Hook 协议层 B[本地 Hook 服务<br/>127.0.0.1:8061] end subgraph SCRM 服务 C[Go 后端<br/>Gin 框架 :8089] D[(SQLite/MySQL)] end subgraph AI 能力 E[扣子 Coze API<br/>api.coze.cn] end subgraph 用户端 F[客户私聊] G[客户群聊] end F --> A G --> A A <-->|WebSocket/HTTP| B B -->|回调通知| C C -->|读取配置| D C -->|调用 AI| E E -->|返回回复| C C -->|发送消息 API| B B -->|路由至| A A -->|回复| F A -->|@回复| G

核心链路

  1. 客户在企业微信中给员工号发消息
  2. 注入版客户端拦截消息,通过 Hook 协议回调到我们的 Go 服务
  3. Go 服务提取消息内容,调用扣子知识库 API
  4. 扣子返回 AI 生成的回复,Go 服务再通过 Hook 协议驱动客户端发送回复
  5. 一次完整的 AI 自动对话就这样完成了

三、Hook 协议封装

3.1 协议简述

企业微信注入版 Hook 在本地暴露了一个 HTTP 服务(默认为 http://127.0.0.1:8061),通过不同的命令码(type)来操作企业微信客户端:

命令码 路径 功能
11029 /cmd/11029 发送普通消息
11035 /cmd/11035 获取账号信息
11069 /cmd/11069 发送群@消息
... ... 登录、扫码、获取联系人等

3.2 Go 客户端封装

我们将 Hook 协议封装为一个 Go 客户端库 lib/wxworkhook/client.go,核心设计如下:

单例客户端

var (
	instance *Client
	once     sync.Once
)

// Client 企业微信Hook协议客户端
type Client struct {
	ClientId   string  // client_id
	Profile    Profile // 当前账号信息(调用 GetProfile 后填充)
	profileFet bool    // 是否已拉取过个人信息
}

// GetClient 获取单例客户端
func GetClient(clientId string) *Client {
	once.Do(func() {
		instance = &Client{
			ClientId: clientId,
		}
	})
	return instance
}

发送普通消息(私聊用):

// SendMsg 发送消息(通过 /cmd/11029)
// conversationID: 会话ID,单聊填用户ID,群聊填群ID
// content: 消息文本内容
func (c *Client) SendTxtMsg(conversationID, content string) (*CmdResponse, error) {
	req := SendMsgReq{
		Type:     11029,
		ClientId: c.ClientId,
		Data: SendMsgData{
			ConversationID: conversationID,
			Content:        content,
		},
	}

	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("序列化请求失败: %v", err)
	}

	url := DefaultBaseURL + "/cmd/11029"
	log.Printf("[企微Hook发送] 请求: %s, 参数: %s\n", url, string(body))

	resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
	if err != nil {
		return nil, fmt.Errorf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	var result CmdResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("解析响应失败: %v", err)
	}

	respBody, _ := json.Marshal(result)
	log.Printf("[企微Hook发送] 响应内容: %s\n", string(respBody))

	if result.ErrCode != 0 {
		return &result, fmt.Errorf("发送消息失败: %s", result.ErrMsg)
	}

	log.Printf("[企微Hook发送] 消息发送成功 -> conversation_id=%s\n", conversationID)
	return &result, nil
}

发送群@消息(群聊用):

// SendAtMsg 发送群@消息(通过 /cmd/11069)
// conversationID: 会话ID(群聊填群ID)
// content: 消息文本内容
// atList: @的用户ID列表
func (c *Client) SendAtMsg(conversationID, content string, atList []string) (*CmdResponse, error) {
	req := SendAtMsgReq{
		Type:     11069,
		ClientId: c.ClientId,
		Data: SendAtMsgData{
			ConversationID: conversationID,
			Content:        content,
			AtList:         atList,
		},
	}

	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("序列化请求失败: %v", err)
	}

	url := DefaultBaseURL + "/cmd/11069"
	log.Printf("[企微Hook群@] 请求: %s, 参数: %s\n", url, string(body))

	resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
	if err != nil {
		return nil, fmt.Errorf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	var result CmdResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("解析响应失败: %v", err)
	}

	respBody, _ := json.Marshal(result)
	log.Printf("[企微Hook群@] 响应内容: %s\n", string(respBody))

	if result.ErrCode != 0 {
		return &result, fmt.Errorf("发送群@消息失败: %s", result.ErrMsg)
	}

	log.Printf("[企微Hook群@] 消息发送成功 -> conversation_id=%s, at_list=%v\n", conversationID, atList)
	return &result, nil
}

3.3 关键细节:私聊 vs 群聊识别

企业微信 Hook 回调中的 conversation_id 字段有明确前缀:

  • S: 开头 → 私聊(Single chat)
  • R: 开头 → 群聊(Room chat)

注意:这并非官方 API 规范,而是注入版 Hook 的约定。不同 Hook 版本可能不同,需根据实际情况调整。


四、扣子 Coze API 接入

4.1 为什么选扣子?

  • 知识库原生支持:上传文档即可构建领域知识库,无需训练模型
  • 会话记忆:每个用户维护独立会话,上下文连续
  • 简单 API:创建会话 → 发送消息 → 轮询结果 → 获取回复,四步搞定
  • 免费额度:个人开发者白嫖友好

4.2 Go 封装实现

扣子 API v3 的核心流程:

sequenceDiagram participant S as SCRM服务 participant C as Coze API Note over S,C: 每个用户首次对话 S->>C: POST /v1/conversation/create C-->>S: conversation_id S->>S: 缓存 conversation_id Note over S,C: 每次发消息 S->>C: POST /v3/chat?conversation_id=xxx C-->>S: chat_id + conversation_id loop 轮询等待(最多30秒) S->>C: GET /v3/chat/retrieve C-->>S: status: in_progress/completed end S->>C: GET /v3/chat/message/list C-->>S: assistant 回复内容 S->>S: 提取 answer 消息返回

关键代码——轮询等待完成后获取 AI 回复:

func (c *Coze) waitForCompletion(chatID, conversationID string) error {
	url := "https://api.coze.cn/v3/chat/retrieve"
	maxRetries := 30
	interval := 1 * time.Second

	for i := 0; i < maxRetries; i++ {
		queryURL := fmt.Sprintf("%s?chat_id=%s&conversation_id=%s", url, chatID, conversationID)
		
		req, err := http.NewRequest("GET", queryURL, nil)
		if err != nil {
			return fmt.Errorf("create request failed: %w", err)
		}

		req.Header.Set("Authorization", "Bearer "+c.API_KEY)

		client := &http.Client{Timeout: 30 * time.Second}
		resp, err := client.Do(req)
		if err != nil {
			return fmt.Errorf("request failed: %w", err)
		}
		defer resp.Body.Close()

		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return fmt.Errorf("read response failed: %w", err)
		}

		var response ChatRetrieveResponse
		if err := json.Unmarshal(body, &response); err != nil {
			return fmt.Errorf("unmarshal response failed: %w", err)
		}

		if response.Code != 0 {
			return fmt.Errorf("API error: %s", response.Msg)
		}

		if response.Data.Status == "completed" {
			return nil
		}

		time.Sleep(interval)
	}

	return fmt.Errorf("wait for completion timeout")
}

func (c *Coze) getFinalResponse(chatID, conversationID string) (string, error) {
	url := "https://api.coze.cn/v3/chat/message/list"
	queryURL := fmt.Sprintf("%s?chat_id=%s&conversation_id=%s", url, chatID, conversationID)

	req, err := http.NewRequest("GET", queryURL, nil)
	if err != nil {
		return "", fmt.Errorf("create request failed: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.API_KEY)

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("read response failed: %w", err)
	}

	var response MessageListResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return "", fmt.Errorf("unmarshal response failed: %w", err)
	}

	if response.Code != 0 {
		return "", fmt.Errorf("API error: %s", response.Msg)
	}

	var assistantReplies []Message
	for _, msg := range response.Data {
		if msg.Role == "assistant" && msg.Type == "answer" {
			assistantReplies = append(assistantReplies, msg)
		}
	}

	if len(assistantReplies) > 0 {
		return assistantReplies[len(assistantReplies)-1].Content, nil
	}

	return "", nil
}

4.3 会话缓存

每个用户的会话 ID 使用 sync.RWMutex 保护的内存 map 缓存,避免重复创建会话:

type Coze struct {
	BOT_ID       string
	API_KEY      string
	conversation map[string]string
	mu           sync.RWMutex
}

这意味着同一位客户的所有对话共享同一个 Coze 会话上下文,AI 能记住之前的对话内容。


五、核心:回调处理与自动回复

这是整个系统的大脑——routers/wxwork_hook.go 中的 PostWxWorkHookCallback

// PostWxWorkHookCallback 企业微信Hook回调(通用回调入口)
func PostWxWorkHookCallback(c *gin.Context) {
	rawData, err := io.ReadAll(c.Request.Body)
	if err != nil {
		log.Printf("[企微Hook回调] 读取body失败: %v\n", err)
		c.JSON(http.StatusOK, gin.H{"code": -1, "msg": "读取失败"})
		return
	}

	log.Printf("[企微Hook回调] Body: %s\n", string(rawData))

	payloadType := gjson.GetBytes(rawData, "payload.type").Int()

	// 普通消息:提取字段存入 LiveComment 表
	clientId := gjson.GetBytes(rawData, "client_id").String()
	content := gjson.GetBytes(rawData, "payload.data.content").String()
	sender := gjson.GetBytes(rawData, "payload.data.sender").String()
	senderName := gjson.GetBytes(rawData, "payload.data.sender_name").String()
	conversationID := gjson.GetBytes(rawData, "payload.data.conversation_id").String()

	// type 11035 = 账号信息回调
	if payloadType == 11035 {
		var p wxworkhook.Profile
		if err := json.Unmarshal([]byte(gjson.GetBytes(rawData, "payload.data").Raw), &p); err != nil {
			log.Printf("[企微Hook回调] 解析账号信息失败: %v\n", err)
		} else {
			client := wxworkhook.GetClient(clientId)
			client.SetProfile(p)
		}
		c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "ok"})
		return
	}

	comment := &models.LiveComment{
		Platform: sender,
		Nickname: senderName,
		Content:  content,
		To:       conversationID,
	}
	if err := models.AddOrUpdateLiveComment(comment); err != nil {
		log.Printf("[企微Hook回调] 存表失败: %v\n", err)
	} else {
		log.Printf("[企微Hook回调] 消息入库 id=%d sender_name=%s content=%s\n", comment.ID, senderName, content)
	}

	// 自动回复(跳过自己的消息,防止死循环)
	myUserId := models.FindSettingDefault("wxworkUserId", "")
	if conversationID != "" && content != "" && myUserId != "" && sender != myUserId {
		botID := models.FindSettingDefault("cozeBotId", "")
		apiKey := models.FindSettingDefault("cozeApiKey", "")
		if botID != "" && apiKey != "" {
			reply, err := coze.ChatCozeAPI(botID, apiKey, sender, content)
			if err != nil {
				log.Printf("[企微Hook回调] Coze调用失败: %v\n", err)
			} else if reply != "" {
				client := wxworkhook.GetClient(clientId)
				var sendErr error
				if strings.HasPrefix(conversationID, "R:") {
					// 群聊:发送群@消息
					_, sendErr = client.SendAtMsg(conversationID, reply, []string{sender})
				} else {
					// 私聊:发送普通文本消息
					_, sendErr = client.SendTxtMsg(conversationID, reply)
				}
				if sendErr != nil {
					log.Printf("[企微Hook回调] 自动回复失败: %v\n", sendErr)
				}
			}
		} else {
			log.Printf("[企微Hook回调] Coze配置未完成,跳过自动回复\n")
		}
	}

	c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "ok"})
}

回调流程拆解:

步骤 操作 说明
① 接收 读取 HTTP Body 企业微信 Hook 回调 POST JSON
② 分发 根据 payload.type 判断 11035→更新账号信息,其他→消息处理
③ 入库 写入 LiveComment 所有消息持久化存储,可追溯
④ 防回环 比对 sendermyUserId 跳过自己发出的消息,避免无限回复
⑤ AI 调用 coze.ChatCozeAPI() 以发送者身份调用扣子 API
⑥ 会话区分 检查 conversationID 前缀 R: → 群@消息;S: → 普通消息
⑦ 回复 SendTxtMsg / SendAtMsg 通过 Hook 协议驱动企微客户端发送

六、配置管理

系统使用 SQLite 存储运行时配置(Setting 表),通过 key-value 方式管理:

func FindSettingDefault(key string, def string) string {
	val := FindSetting(key).Value
	if val == "" {
		return def
	}
	return val
}

三个关键配置项

Key 说明 示例值
wxworkUserId 当前员工号 UserID 用于防止自我回复
cozeBotId 扣子机器人 Bot ID 从 Coze 控制台获取
cozeApiKey 扣子 API Key Bearer Token 认证

这些配置通过后台管理页面 /admin/setting 可视化配置。


七、技术栈总结

层级 技术选型 用途
Web 框架 Gin (Go) HTTP 路由、中间件、模板渲染
数据库 SQLite (GORM) 配置存储、消息记录
缓存 Redis 可选,热数据缓存
WebSocket gorilla/websocket 实时消息推送(直播评论屏等)
AI 引擎 扣子 Coze API v3 知识库问答、上下文会话
通信协议 企业微信 Hook 接收消息回调 + 驱动发消息
前端 原生 HTML + JS 管理后台 Dashboard
  • 轻量:单 Go 二进制,SQLite 无外部依赖,Windows/Linux/Mac 通吃
  • 低侵入:Hook 注入方式,不依赖企业微信官方 API 权限审批
  • 可扩展:模块化设计,Hook 客户端、Coze 引擎均可替换为其他实现
  • 完整闭环:消息入库 → AI 生成 → 自动回复→ 前端实时展示

如果你也有类似需求——让企业微信员工号变身智能客服,不妨参考这套方案。


发表于 2026 年 6 月 4 日 | 标签:Go, 企业微信, Coze, AI, Hook, SCRM