chore(build): 更新.gitignore配置和清理Wails临时文件

- 添加dist/目录到.gitignore,用于排除打包输出的绿色免安装版
- 添加Wails打包过程中的临时文件和自动生成文件到.gitignore
- 删除build/windows/installer/wails_tools.nsh自动生成文件
- 添加Windows安装器临时目录和Webview2安装文件到忽略列表

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

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

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

- 添加NotificationOptions接口定义
- 添加NotificationAction接口定义
- 添加NotificationCategory接口定义
- 扩展通知相关的运行时API类型声明,包括初始化、发送、注册分类等功能
This commit is contained in:
2026-06-25 18:13:11 +08:00
parent 858cb68f4f
commit a926ee6b1b
34 changed files with 1178 additions and 275 deletions

View File

@@ -351,9 +351,56 @@ func extractAutoReplyMessage(clientID int32, raw map[string]interface{}) autoRep
} else {
msg.MessageType = "non_text"
}
logEmptyMediaDiagnostics(msg, raw)
return msg
}
// logEmptyMediaDiagnostics 仅在"图片/视频/表情等非文本媒体消息,且 URL/本地路径/FileID 三个来源全空"
// 时触发,把媒体相关字段(已脱敏)打到日志,用于定位企微 DLL 回调实际下发了哪些字段。
// 文本消息(11041)与字段已成功填充的消息都不会触发,避免刷屏与泄露正常聊天内容。
func logEmptyMediaDiagnostics(msg autoReplyMessage, raw map[string]interface{}) {
if globalLogger == nil {
return
}
// 只关心需要拉取媒体文件来识别的类型;纯文本/位置等不在此列。
switch msg.RawType {
case 11042, 11043, 11047: // image / video / link(图片/表情)
default:
return
}
hasMediaSource := strings.TrimSpace(msg.MediaURL) != "" ||
strings.TrimSpace(msg.MediaLocalPath) != "" ||
strings.TrimSpace(msg.MediaFileID) != ""
if hasMediaSource {
return // 字段已填到,下载链路可以走,无需诊断
}
// 只提取诊断相关的字段,避免泄露用户昵称、群名、会话内容等敏感信息
diagnosticFields := make(map[string]interface{})
diagnosticFields["event"] = raw["event"]
diagnosticFields["type"] = raw["type"]
if data, ok := raw["data"].(map[string]interface{}); ok {
// 只记录媒体相关字段和类型标识
for _, key := range []string{"event", "content_type", "contentType", "messageType", "message_type",
"image_url", "imageUrl", "preview_img_url", "previewImgUrl",
"md_url", "mdUrl", "ld_url", "ldUrl", "url",
"file_id", "fileId", "local_path", "localPath",
"media_kind", "mediaKind", "aes_key", "aesKey", "auth_key", "authKey"} {
if val, exists := data[key]; exists && val != nil {
diagnosticFields[key] = val
}
}
}
diagJSON, err := json.Marshal(diagnosticFields)
if err != nil {
globalLogger.Warn("[媒体诊断] rawType=%d 媒体字段全空,且诊断数据序列化失败: %v", msg.RawType, err)
return
}
globalLogger.Warn("[媒体诊断] rawType=%d mediaKind=%s 媒体字段(URL/本地路径/FileID)全空,无法识别。诊断字段: %s",
msg.RawType, msg.MediaKind, string(diagJSON))
}
func rawTypeFromEvent(raw map[string]interface{}) int {
event := strings.TrimSpace(stringFromAny(raw["event"]))
if event == "" {
@@ -361,6 +408,7 @@ func rawTypeFromEvent(raw map[string]interface{}) int {
event = strings.TrimSpace(stringFromAny(data["event"]))
}
}
// 优先使用 event 字段DLL 真实事件20002=文本 20003=图片 20004=视频 20012=语音 20005=文件 20014=链接
switch event {
case "20002":
return 11041
@@ -375,18 +423,24 @@ func rawTypeFromEvent(raw map[string]interface{}) int {
case "20014":
return 11047
}
// event 为空时才用 content_type模拟事件或老版本 DLL2=文本 101=图片 103=视频 16=语音 102=文件 6=位置 13=链接
// 注意不能把文本(2)误判成图片,否则会触发图片识别并回退"无法识别"话术。
if data, ok := raw["data"].(map[string]interface{}); ok {
switch intFromAny(firstNonNil(data["messageType"], data["content_type"], data["contentType"])) {
switch intFromAny(firstNonNil(data["content_type"], data["contentType"])) {
case 2:
return 11041
case 101:
return 11042
case 4:
case 103:
return 11043
case 16:
return 11044
case 6:
case 102:
return 11045
case 48:
case 6:
return 11046
case 13:
return 11047
}
}
return 0

View File

@@ -5,6 +5,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
@@ -286,36 +287,54 @@ func syncAutoReplyMaterials(root string, indexPath string) (autoReplyMaterialSyn
func discoverAutoReplyMaterials(root string) []AutoReplyMaterial {
dir := resolveAutoReplyPath(root)
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
items := make([]AutoReplyMaterial, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
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 // 单个条目出错跳过,不中断整体扫描
}
name := entry.Name()
if d.IsDir() {
return nil
}
name := d.Name()
if strings.EqualFold(name, "materials.json") {
continue
return nil
}
materialType := inferMaterialType(name)
if materialType == "" {
continue
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(title),
ID: materialIDFromTitle(strings.TrimSuffix(rel, filepath.Ext(rel))),
Title: title,
Keywords: defaultMaterialKeywords(title, materialType),
Keywords: keywords,
QuestionPatterns: defaultMaterialQuestionPatterns(title),
MaterialType: materialType,
Path: name,
Path: rel,
Caption: defaultMaterialCaption(materialType),
Priority: 1,
Enabled: true,
})
}
return nil
})
return normalizeAutoReplyMaterials(items)
}