Files
qiweimanager-master/helper/auto_reply_materials.go
yuanzhipeng a926ee6b1b chore(build): 更新.gitignore配置和清理Wails临时文件
- 添加dist/目录到.gitignore,用于排除打包输出的绿色免安装版
- 添加Wails打包过程中的临时文件和自动生成文件到.gitignore
- 删除build/windows/installer/wails_tools.nsh自动生成文件
- 添加Windows安装器临时目录和Webview2安装文件到忽略列表

feat(docs): 添加万川平台对接文档和产品素材

- 创建万川平台登录到获取模型信息的流程说明文档
- 添加万川平台对接实施计划文档
- 新增产品图片、公司简介图、宣传海报、教程截图、案例展示等素材文件

refactor(runtime): 扩展通知功能类型定义

- 添加NotificationOptions接口定义
- 添加NotificationAction接口定义
- 添加NotificationCategory接口定义
- 扩展通知相关的运行时API类型声明,包括初始化、发送、注册分类等功能
2026-06-25 18:13:11 +08:00

798 lines
24 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"unicode"
)
type AutoReplyMaterial struct {
ID string `json:"id"`
Title string `json:"title"`
Keywords []string `json:"keywords"`
QuestionPatterns []string `json:"questionPatterns"`
MaterialType string `json:"materialType"`
Path string `json:"path"`
Caption string `json:"caption"`
Priority int `json:"priority"`
Enabled bool `json:"enabled"`
}
type autoReplyMaterialsFile struct {
Materials []AutoReplyMaterial `json:"materials"`
}
type autoReplyMaterialMatch struct {
Material AutoReplyMaterial
Path string
Score int
}
type autoReplyMaterialSyncResult struct {
Added int `json:"added"`
Removed int `json:"removed"`
Total int `json:"total"`
Materials []AutoReplyMaterial `json:"materials"`
IndexPath string `json:"indexPath"`
Directory string `json:"directory"`
AddedPaths []string `json:"addedPaths"`
RemovedPaths []string `json:"removedPaths"`
}
func (e *AutoReplyEngine) matchMaterials(userQuery string, searchContext string, hits []KnowledgeChunk) []autoReplyMaterialMatch {
cfg := e.getConfig()
if !cfg.Materials.AutoSendEnabled {
return nil
}
if isBroadAllMaterialRequest(userQuery) {
return nil
}
materials, err := loadAutoReplyMaterials(cfg.Materials.IndexPath)
if err != nil {
if !os.IsNotExist(err) {
e.setLastErrorWithScope(autoReplyErrorScopeKnowledge, "load materials failed: "+err.Error())
}
}
if len(materials) == 0 {
materials = discoverAutoReplyMaterials(cfg.Materials.Directory)
}
if len(materials) == 0 {
return nil
}
requestedTypes := requestedMaterialTypes(userQuery)
hasSendIntent := hasMaterialSendIntent(userQuery)
if hasSendIntent && isGenericMaterialRequest(userQuery) && !materialQueryHasSpecificSignal(userQuery, materials) && strings.TrimSpace(searchContext) == strings.TrimSpace(userQuery) {
return nil
}
queryText := buildMaterialSearchText(userQuery, "", nil, false)
matches := e.collectMaterialMatches(materials, cfg.Materials.Directory, requestedTypes, queryText, hasSendIntent)
if len(matches) > 0 {
return limitMaterialMatches(matches, cfg.Materials.MaxPerReply)
}
if !hasSendIntent {
return nil
}
searchText := buildMaterialSearchText(userQuery, "", hits, true)
matches = e.collectMaterialMatches(materials, cfg.Materials.Directory, requestedTypes, searchText, hasSendIntent)
return limitMaterialMatches(matches, cfg.Materials.MaxPerReply)
}
func (e *AutoReplyEngine) collectMaterialMatches(materials []AutoReplyMaterial, root string, requestedTypes map[string]bool, searchText string, hasSendIntent bool) []autoReplyMaterialMatch {
matches := make([]autoReplyMaterialMatch, 0, 4)
for _, material := range materials {
if len(requestedTypes) > 0 && !requestedTypes[material.MaterialType] {
continue
}
path := resolveAutoReplyMaterialPath(root, material.Path)
score := materialMatchScore(searchText, material, hasSendIntent)
if score <= 0 {
continue
}
if _, err := os.Stat(path); err != nil {
e.setLastErrorWithScope(autoReplyErrorScopeKnowledge, fmt.Sprintf("material file missing: %s", path))
continue
}
matches = append(matches, autoReplyMaterialMatch{Material: material, Path: path, Score: score})
}
sort.Slice(matches, func(i, j int) bool {
if matches[i].Score != matches[j].Score {
return matches[i].Score > matches[j].Score
}
if matches[i].Material.Priority != matches[j].Material.Priority {
return matches[i].Material.Priority > matches[j].Material.Priority
}
return matches[i].Material.Title < matches[j].Material.Title
})
return matches
}
func limitMaterialMatches(matches []autoReplyMaterialMatch, maxPerReply int) []autoReplyMaterialMatch {
limit := maxPerReply
if limit <= 0 {
limit = 2
}
if len(matches) > limit {
matches = matches[:limit]
}
return matches
}
func buildMaterialSearchText(userQuery string, searchContext string, hits []KnowledgeChunk, includeContext bool) string {
parts := []string{userQuery}
if includeContext {
parts = append(parts, searchContext)
for _, hit := range hits {
parts = append(parts, hit.Source, hit.Title, hit.Content)
}
}
return strings.ToLower(strings.Join(parts, "\n"))
}
func hasMaterialSendIntent(query string) bool {
text := normalizeGreetingText(query)
if text == "" {
return false
}
return containsAnyMaterialIntent(text, []string{
"发我", "发给我", "发一下", "发下", "发来", "发送", "传给我", "给我发",
"给我", "我要", "我想要", "需要", "有吗", "有没有", "资料", "素材",
"手册", "文档", "文件", "附件", "说明书", "宣传册", "ppt", "pdf",
"视频", "图片", "表格", "清单", "案例", "模板",
})
}
func requestedMaterialTypes(query string) map[string]bool {
text := strings.ToLower(strings.TrimSpace(query))
if text == "" {
return nil
}
result := map[string]bool{}
if containsAnyMaterialIntent(text, []string{
"\u56fe\u7247", "\u7167\u7247", "\u76f8\u7247", "\u56fe\u50cf", "\u622a\u56fe", "\u914d\u56fe",
"image", "photo", "jpg", "jpeg", "png", "webp",
}) {
result["image"] = true
}
if containsAnyMaterialIntent(text, []string{
"\u89c6\u9891", "\u5f55\u50cf", "\u5f71\u7247", "\u77ed\u89c6\u9891", "video", "movie", "mp4", "mov",
}) {
result["video"] = true
}
if containsAnyMaterialIntent(text, []string{
"\u52a8\u56fe", "\u8868\u60c5\u5305", "gif",
}) {
result["gif"] = true
}
if containsAnyMaterialIntent(text, []string{
"\u6587\u4ef6", "\u6587\u6863", "\u6587\u7a3f", "\u9644\u4ef6", "\u8868\u683c",
"\u624b\u518c", "\u8d44\u6599", "\u65b9\u6848", "\u8bf4\u660e\u4e66",
"file", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx",
}) {
result["file"] = true
}
if len(result) == 0 {
return nil
}
return result
}
func containsAnyMaterialIntent(text string, keywords []string) bool {
for _, keyword := range keywords {
if strings.Contains(text, keyword) {
return true
}
}
return false
}
func loadAutoReplyMaterials(indexPath string) ([]AutoReplyMaterial, error) {
path := resolveAutoReplyPath(indexPath)
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var wrapped autoReplyMaterialsFile
if err := json.Unmarshal(data, &wrapped); err == nil {
return normalizeAutoReplyMaterials(wrapped.Materials), nil
}
var list []AutoReplyMaterial
if err := json.Unmarshal(data, &list); err != nil {
return nil, err
}
return normalizeAutoReplyMaterials(list), nil
}
func (e *AutoReplyEngine) syncAutoReplyMaterials() (autoReplyMaterialSyncResult, error) {
cfg := e.getConfig()
return syncAutoReplyMaterials(cfg.Materials.Directory, cfg.Materials.IndexPath)
}
func syncAutoReplyMaterials(root string, indexPath string) (autoReplyMaterialSyncResult, error) {
result := autoReplyMaterialSyncResult{
Directory: resolveAutoReplyPath(root),
IndexPath: resolveAutoReplyPath(indexPath),
}
if err := os.MkdirAll(result.Directory, 0755); err != nil {
return result, err
}
existing, err := loadAutoReplyMaterials(indexPath)
if err != nil && !os.IsNotExist(err) {
return result, err
}
discovered := discoverAutoReplyMaterials(root)
discoveredByPath := make(map[string]AutoReplyMaterial, len(discovered))
for _, item := range discovered {
discoveredByPath[materialPathKey(item.Path)] = item
}
synced := make([]AutoReplyMaterial, 0, len(discovered))
seen := make(map[string]bool, len(discovered))
for _, item := range existing {
key := materialPathKey(item.Path)
if key == "" || seen[key] {
continue
}
if _, ok := discoveredByPath[key]; !ok {
result.Removed++
result.RemovedPaths = append(result.RemovedPaths, item.Path)
continue
}
synced = append(synced, item)
seen[key] = true
}
for _, item := range discovered {
key := materialPathKey(item.Path)
if key == "" || seen[key] {
continue
}
synced = append(synced, item)
seen[key] = true
result.Added++
result.AddedPaths = append(result.AddedPaths, item.Path)
}
sort.SliceStable(synced, func(i, j int) bool {
li := strings.ToLower(synced[i].Path)
lj := strings.ToLower(synced[j].Path)
if li != lj {
return li < lj
}
return strings.ToLower(synced[i].Title) < strings.ToLower(synced[j].Title)
})
if err := os.MkdirAll(filepath.Dir(result.IndexPath), 0755); err != nil {
return result, err
}
data, err := json.MarshalIndent(autoReplyMaterialsFile{Materials: synced}, "", " ")
if err != nil {
return result, err
}
if err := os.WriteFile(result.IndexPath, data, 0644); err != nil {
return result, err
}
result.Materials = synced
result.Total = len(synced)
return result, nil
}
func discoverAutoReplyMaterials(root string) []AutoReplyMaterial {
dir := resolveAutoReplyPath(root)
items := make([]AutoReplyMaterial, 0, 8)
// 递归遍历子目录filepath.WalkDir支持 config/materials 下任意层级嵌套。
// Path 存相对 root 的子路径并统一为 / 分隔;顶层文件相对路径即文件名,向后兼容旧索引。
_ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil // 单个条目出错跳过,不中断整体扫描
}
if d.IsDir() {
return nil
}
name := d.Name()
if strings.EqualFold(name, "materials.json") {
return nil
}
materialType := inferMaterialType(name)
if materialType == "" {
return nil
}
rel, relErr := filepath.Rel(dir, path)
if relErr != nil {
rel = name
}
rel = filepath.ToSlash(rel)
title := strings.TrimSuffix(name, filepath.Ext(name))
keywords := defaultMaterialKeywords(title, materialType)
// 把子目录名也并入关键词,便于"发我<目录名>的图/文件"命中。
// 只添加目录名本身,不再分词,避免关键词过度膨胀。
if dirPart := filepath.ToSlash(filepath.Dir(rel)); dirPart != "." && dirPart != "" {
for _, seg := range strings.Split(dirPart, "/") {
if seg = strings.TrimSpace(seg); seg != "" {
keywords = append(keywords, seg)
}
}
keywords = dedupeNonEmptyStrings(keywords)
}
items = append(items, AutoReplyMaterial{
ID: materialIDFromTitle(strings.TrimSuffix(rel, filepath.Ext(rel))),
Title: title,
Keywords: keywords,
QuestionPatterns: defaultMaterialQuestionPatterns(title),
MaterialType: materialType,
Path: rel,
Caption: defaultMaterialCaption(materialType),
Priority: 1,
Enabled: true,
})
return nil
})
return normalizeAutoReplyMaterials(items)
}
func materialIDFromTitle(title string) string {
base := strings.TrimSpace(strings.ToLower(title))
var builder strings.Builder
lastDash := false
for _, r := range base {
switch {
case unicode.IsLetter(r), unicode.IsDigit(r):
builder.WriteRune(r)
lastDash = false
case r == '-' || r == '_':
if builder.Len() > 0 {
builder.WriteRune(r)
lastDash = false
}
default:
if builder.Len() > 0 && !lastDash {
builder.WriteByte('-')
lastDash = true
}
}
}
id := strings.Trim(builder.String(), "-_")
if id == "" {
sum := sha1.Sum([]byte(base))
id = "material-" + hex.EncodeToString(sum[:])[:12]
}
return id
}
func defaultMaterialQuestionPatterns(title string) []string {
title = strings.TrimSpace(title)
if title == "" {
return nil
}
return []string{"我要" + title, "发我" + title, "看" + title, "有没有" + title, "把" + title + "发我", "需要" + title}
}
func defaultMaterialKeywords(title string, materialType string) []string {
keywords := []string{strings.TrimSpace(title)}
keywords = append(keywords, materialSearchTokens(title)...)
switch materialType {
case "image":
keywords = append(keywords, specificMaterialTokensForType(materialType)...)
case "video":
keywords = append(keywords, specificMaterialTokensForType(materialType)...)
case "gif":
keywords = append(keywords, specificMaterialTokensForType(materialType)...)
default:
keywords = append(keywords, specificMaterialTokensForType(materialType)...)
}
return dedupeNonEmptyStrings(keywords)
}
func specificMaterialTokensForType(materialType string) []string {
switch materialType {
case "video":
return []string{"安装视频", "演示视频", "教程视频"}
case "image":
return []string{"示意图", "效果图", "截图"}
case "gif":
return []string{"动图"}
default:
return nil
}
}
func materialPathKey(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
return strings.ToLower(filepath.ToSlash(filepath.Clean(path)))
}
func normalizeAutoReplyMaterials(items []AutoReplyMaterial) []AutoReplyMaterial {
result := make([]AutoReplyMaterial, 0, len(items))
for _, item := range items {
item.ID = strings.TrimSpace(item.ID)
item.Title = strings.TrimSpace(item.Title)
item.MaterialType = strings.ToLower(strings.TrimSpace(item.MaterialType))
item.Path = strings.TrimSpace(item.Path)
item.Caption = strings.TrimSpace(item.Caption)
if item.MaterialType == "" {
item.MaterialType = inferMaterialType(item.Path)
}
if item.Path == "" || item.MaterialType == "" {
continue
}
if !item.Enabled && strings.TrimSpace(item.ID+item.Title) == "" {
continue
}
result = append(result, item)
}
return result
}
func materialMatchScore(searchText string, material AutoReplyMaterial, hasSendIntent bool) int {
score := 0
for _, keyword := range append(material.Keywords, material.QuestionPatterns...) {
keyword = strings.ToLower(strings.TrimSpace(keyword))
if keyword == "" || isGenericMaterialIntentToken(keyword) {
continue
}
if strings.Contains(searchText, keyword) {
score += 10
}
}
for _, field := range []string{material.Title, filepath.Base(material.Path), strings.TrimSuffix(filepath.Base(material.Path), filepath.Ext(material.Path))} {
field = strings.ToLower(strings.TrimSpace(field))
if field != "" && strings.Contains(searchText, field) {
score += 4
}
score += fuzzyMaterialTokenScore(searchText, field)
}
if hasSendIntent && score > 0 {
score += 3
}
return score
}
func isBroadAllMaterialRequest(query string) bool {
text := normalizeGreetingText(query)
if text == "" {
return false
}
phrases := []string{
"全部资料", "所有资料", "全部文件", "所有文件", "全部素材", "所有素材", "全部发", "全都发",
"都发我", "都发给我", "资料都发", "文件都发", "全套资料", "所有手册", "全部手册",
}
for _, phrase := range phrases {
if strings.Contains(text, normalizeGreetingText(phrase)) {
return true
}
}
return false
}
func isGenericMaterialRequest(query string) bool {
text := normalizeGreetingText(query)
if text == "" || !hasMaterialSendIntent(query) {
return false
}
generic := []string{
"资料", "文件", "文档", "附件", "素材", "手册", "说明书", "宣传册", "方案",
"模板", "案例", "清单", "表格", "图片", "照片", "截图", "视频", "ppt", "pdf", "doc", "docx", "xls", "xlsx",
"发我", "发给我", "发一个", "发下", "发来", "发送", "传给我", "给我发", "给我", "我要", "我想要", "需要", "有吗", "有没有",
}
remaining := text
for _, token := range generic {
remaining = strings.ReplaceAll(remaining, normalizeGreetingText(token), "")
}
remaining = strings.Trim(remaining, " \t\r\n,,。.!?;::、()()[]【】")
return len([]rune(remaining)) == 0
}
func materialQueryHasSpecificSignal(query string, materials []AutoReplyMaterial) bool {
text := strings.ToLower(normalizeGreetingText(query))
if text == "" {
return false
}
for _, material := range materials {
fields := []string{
material.Title,
filepath.Base(material.Path),
strings.TrimSuffix(filepath.Base(material.Path), filepath.Ext(material.Path)),
}
for _, keyword := range append(material.Keywords, material.QuestionPatterns...) {
if !isGenericMaterialIntentToken(keyword) {
fields = append(fields, keyword)
}
}
for _, field := range fields {
field = strings.ToLower(normalizeGreetingText(field))
if len([]rune(field)) >= 3 && strings.Contains(text, field) {
return true
}
}
}
return false
}
func isGenericMaterialIntentToken(token string) bool {
token = normalizeGreetingText(token)
if token == "" {
return true
}
switch token {
case "资料", "文件", "文档", "附件", "素材", "手册", "说明书", "宣传册",
"方案", "模板", "案例", "清单", "表格", "图片", "照片", "截图",
"视频", "录像", "ppt", "pptx", "pdf", "doc", "docx", "xls", "xlsx",
"发我", "给我", "需要", "有没有", "我要", "发一下":
return true
default:
return false
}
}
func fuzzyMaterialTokenScore(searchText string, field string) int {
tokens := materialSearchTokens(field)
if len(tokens) == 0 {
return 0
}
score := 0
for _, token := range tokens {
if len([]rune(token)) < 2 {
continue
}
if isGenericMaterialIntentToken(token) {
continue
}
if strings.Contains(searchText, token) {
score += 2
}
}
return score
}
func materialSearchTokens(text string) []string {
text = strings.ToLower(strings.TrimSpace(text))
if text == "" {
return nil
}
separators := func(r rune) bool {
return !(unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.Is(unicode.Han, r))
}
parts := strings.FieldsFunc(text, separators)
result := make([]string, 0, len(parts)*2)
for _, part := range parts {
part = strings.TrimSpace(part)
if len([]rune(part)) < 2 {
continue
}
result = append(result, part)
runes := []rune(part)
if len(runes) > 4 {
for i := 0; i+2 <= len(runes); i++ {
result = append(result, string(runes[i:i+2]))
}
}
}
return dedupeNonEmptyStrings(result)
}
func resolveAutoReplyMaterialPath(root string, materialPath string) string {
materialPath = strings.TrimSpace(materialPath)
if filepath.IsAbs(materialPath) {
return filepath.Clean(materialPath)
}
return filepath.Join(resolveAutoReplyPath(root), filepath.Clean(materialPath))
}
func inferMaterialType(path string) string {
switch strings.ToLower(filepath.Ext(path)) {
case ".jpg", ".jpeg", ".png", ".bmp", ".webp":
return "image"
case ".gif":
return "gif"
case ".mp4", ".mov", ".avi", ".mkv", ".wmv":
return "video"
case ".json":
return ""
default:
return "file"
}
}
func (e *AutoReplyEngine) sendMaterials(msg autoReplyMessage, matches []autoReplyMaterialMatch, reason string, timings autoReplyTimings) error {
if len(matches) == 0 {
return nil
}
captions := make([]string, 0, len(matches))
for _, match := range matches {
if caption := customMaterialCaptionForSend(match.Material); caption != "" {
captions = append(captions, caption)
}
}
if len(captions) == 0 {
captions = append(captions, combinedMaterialCaption(matches))
}
caption := strings.Join(uniqueMaterialStrings(captions), "\n")
if err := sendAutoReplyText(uint32(msg.ClientID), msg.ConversationID, caption); err != nil {
return err
}
e.rememberAutoSentMessage(uint32(msg.ClientID), msg.ConversationID, caption)
sent := make([]string, 0, len(matches))
for _, match := range matches {
if err := sendAutoReplyMaterial(uint32(msg.ClientID), msg.ConversationID, match.Material.MaterialType, match.Path); err != nil {
return fmt.Errorf("send material %s failed: %w", match.Path, err)
}
sent = append(sent, fmt.Sprintf("%s:%s", match.Material.MaterialType, match.Path))
}
e.markCooldown(msg)
e.incStatus("replied")
e.noteReason(reason)
e.addRecord(AutoReplyRecord{
RobotID: msg.RobotID,
ClientID: msg.ClientID,
UserID: msg.RobotID,
ConversationID: msg.ConversationID,
Source: msg.sourceLabel(),
FromWxID: msg.FromWxID,
FromNickName: msg.FromNickName,
Question: msg.Content,
Action: "replied",
Reason: reason,
Answer: strings.Join(sent, "\n"),
SenderIdentity: msg.SenderIdentity,
IdentitySource: msg.IdentitySource,
KeywordScore: timings.KeywordScore,
VectorScore: timings.VectorScore,
RerankScore: timings.RerankScore,
RetrievalMode: timings.RetrievalMode,
UsedKnowledgeSources: strings.Join(timings.UsedKnowledgeSources, ", "),
KnowledgeDurationMS: timings.KnowledgeDurationMS,
KeywordDurationMS: timings.KeywordDurationMS,
VectorDurationMS: timings.VectorDurationMS,
RerankDurationMS: timings.RerankDurationMS,
AIDurationMS: timings.AIDurationMS,
TotalDurationMS: timings.TotalDurationMS,
})
return nil
}
func materialCaptionForSend(material AutoReplyMaterial) string {
if caption := customMaterialCaptionForSend(material); caption != "" {
return caption
}
return defaultMaterialCaption(material.MaterialType)
}
func customMaterialCaptionForSend(material AutoReplyMaterial) string {
caption := strings.TrimSpace(material.Caption)
if caption != "" && !isLegacyGenericMaterialCaption(caption) {
return caption
}
return ""
}
func isLegacyGenericMaterialCaption(caption string) bool {
text := normalizeGreetingText(caption)
switch text {
case normalizeGreetingText("我把相关资料直接发你。"),
normalizeGreetingText("我把相关资料发你。"):
return true
default:
return false
}
}
func defaultMaterialCaption(materialType string) string {
switch strings.ToLower(strings.TrimSpace(materialType)) {
case "image":
return "我把图片发你。"
case "video":
return "我把视频发你。"
case "gif":
return "我把动图发你。"
default:
return "我把文件发你。"
}
}
func combinedMaterialCaption(matches []autoReplyMaterialMatch) string {
if len(matches) == 0 {
return "我把文件发你。"
}
seen := map[string]bool{}
labels := make([]string, 0, 4)
add := func(materialType string, label string) {
if !seen[materialType] {
seen[materialType] = true
labels = append(labels, label)
}
}
for _, match := range matches {
switch strings.ToLower(strings.TrimSpace(match.Material.MaterialType)) {
case "image":
add("image", "图片")
case "video":
add("video", "视频")
case "gif":
add("gif", "动图")
default:
add("file", "文件")
}
}
if len(labels) == 1 {
return defaultMaterialCaption(matches[0].Material.MaterialType)
}
return "我把" + strings.Join(labels, "和") + "发你。"
}
func uniqueMaterialStrings(items []string) []string {
seen := make(map[string]bool, len(items))
result := make([]string, 0, len(items))
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" || seen[item] {
continue
}
seen[item] = true
result = append(result, item)
}
return result
}
var sendAutoReplyMaterialSender = sendAutoReplyMaterialRequest
func sendAutoReplyMaterial(clientID uint32, conversationID string, materialType string, path string) error {
return sendAutoReplyMaterialSender(clientID, conversationID, materialType, path)
}
func sendAutoReplyMaterialRequest(clientID uint32, conversationID string, materialType string, path string) error {
if strings.TrimSpace(conversationID) == "" {
return fmt.Errorf("conversationId is empty")
}
if strings.TrimSpace(path) == "" {
return fmt.Errorf("material path is empty")
}
messageType := 11031
switch strings.ToLower(strings.TrimSpace(materialType)) {
case "image":
messageType = 11030
case "video":
messageType = 11067
case "gif":
messageType = 11070
case "file":
messageType = 11031
default:
messageType = 11031
}
request := map[string]interface{}{
"type": messageType,
"data": map[string]interface{}{
"conversation_id": conversationID,
"file": path,
},
}
data, err := json.Marshal(request)
if err != nil {
return err
}
result, err := handleSendWxWorkData(map[string]interface{}{
"data": string(data),
"clientId": clientID,
})
if err != nil {
return err
}
if resultMap, ok := result.(map[string]interface{}); ok {
if success, ok := resultMap["success"].(bool); ok && !success {
return fmt.Errorf("%v", resultMap["error"])
}
}
return nil
}