feat(qq): async file download, get_private_file_url, mark-as-read, get_recent_contacts

This commit is contained in:
root
2026-07-23 10:36:49 +08:00
parent e030a9b9f9
commit e182b89983

View File

@ -194,6 +194,12 @@ type 枚举: text文字/ voice语音转文字后发送/ image
},
}, p.handleGetFriends)
p.regTool(s, tp+"get_recent_contacts", "查看最近有消息的联系人和群聊,返回最近消息概览(含消息数、最后一条消息内容)。可用于发现有谁发过消息但未处理。", map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"count": map[string]interface{}{"type": "integer", "description": "获取数量默认10"},
},
}, p.handleGetRecentContacts)
p.regTool(s, tp+"resolve_name", "将QQ号或群号解析为可读的用户昵称或群名称", map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号与group_id二选一"},
@ -257,11 +263,12 @@ type 枚举: text文字/ voice语音转文字后发送/ image
},
}, p.handleGetGroupFiles)
p.regTool(s, tp+"download_file", "从聊天记录下载文件到本地。file_id 从 qq_get_message/qq_get_history 的 files 字段获取。qq_get_message 还会返回 url若可用提供 url 可绕过 NapCat 缓存直接下载。群文件建议提供 group_id。下载成功返回本地路径。", map[string]interface{}{
p.regTool(s, tp+"download_file", "从聊天记录下载文件到本地。file_id 从 qq_get_message/qq_get_history 的 files 字段获取。群文件建议提供 group_id私聊文件建议提供 user_id 以提高成功率。异步下载,完成后推送通知。", map[string]interface{}{
"type": "object", "properties": map[string]interface{}{
"file_id": map[string]interface{}{"type": "string", "description": "文件 ID从 qq_get_message 或 qq_get_history 的 files 字段获取)"},
"url": map[string]interface{}{"type": "string", "description": "文件下载 URL可选qq_get_message 返回的 url 字段,用于绕过 NapCat 缓存直接下载"},
"group_id": map[string]interface{}{"type": "integer", "description": "群号(可选,群文件下载时提供可提高成功率"},
"url": map[string]interface{}{"type": "string", "description": "文件下载 URL可选qq_get_message 返回的 url 字段)"},
"group_id": map[string]interface{}{"type": "integer", "description": "群号(可选,群文件下载)"},
"user_id": map[string]interface{}{"type": "integer", "description": "私聊对象QQ号可选私聊文件下载"},
"filename": map[string]interface{}{"type": "string", "description": "保存文件名(可选,默认用原文件名)"},
}, "required": []string{"file_id"},
}, p.handleDownloadFile)
@ -784,6 +791,15 @@ func (p *Plugin) handleGetMessage(args map[string]interface{}) (interface{}, err
result["files"] = files
}
// 异步标记已读
go func() {
if d.MessageType == "group" && d.GroupID > 0 {
p.napcat("mark_group_msg_as_read", map[string]interface{}{"group_id": d.GroupID})
} else if d.UserID > 0 {
p.napcat("mark_private_msg_as_read", map[string]interface{}{"user_id": d.UserID})
}
}()
return result, nil
}
@ -1133,6 +1149,14 @@ func (p *Plugin) handleGetFriends(args map[string]interface{}) (interface{}, err
return p.napcat("get_friend_list", map[string]interface{}{})
}
func (p *Plugin) handleGetRecentContacts(args map[string]interface{}) (interface{}, error) {
count := 10
if c, err := convInt64(args["count"]); err == nil && c > 0 {
count = int(c)
}
return p.napcat("get_recent_contact", map[string]interface{}{"count": count})
}
func (p *Plugin) handleResolveName(args map[string]interface{}) (interface{}, error) {
if uid, err := convInt64(args["user_id"]); err == nil {
return p.napcat("get_stranger_info", map[string]interface{}{"user_id": uid, "no_cache": true})
@ -1462,28 +1486,43 @@ func (p *Plugin) handleDownloadFile(args map[string]interface{}) (interface{}, e
fileURL, _ := args["url"].(string)
filename, _ := args["filename"].(string)
groupID, _ := convInt64(args["group_id"])
userID, _ := convInt64(args["user_id"])
// 1. 有 URL 时直接下载(绕过 NapCat 缓存,用于私聊文件)
if fileURL != "" {
savePath := p.downloadURL(fileURL, filename)
if savePath != "" {
return map[string]interface{}{
"status": "ok", "path": savePath, "filename": filepath.Base(savePath),
}, nil
dispName := filename
if dispName == "" {
dispName = fileID
if len(dispName) > 16 {
dispName = dispName[:16] + "…"
}
}
// 2. 群文件:用 get_group_file_url 获取新鲜下载链接
if groupID > 0 {
savePath := p.downloadGroupFile(groupID, fileID, filename)
if savePath != "" {
return map[string]interface{}{
"status": "ok", "path": savePath, "filename": filepath.Base(savePath),
}, nil
go func() {
savePath := ""
if fileURL != "" {
savePath = p.downloadURL(fileURL, filename)
}
}
if savePath == "" && groupID > 0 {
savePath = p.downloadGroupFile(groupID, fileID, filename)
}
if savePath == "" && userID > 0 {
savePath = p.downloadPrivateFile(userID, fileID, filename)
}
if savePath == "" {
savePath = p.downloadFromNapCat(fileID, filename)
}
if savePath != "" {
log.Printf("[qq] 文件下载完成: %s", savePath)
if p.sdk != nil {
p.sdk.InjectInterruptText(p.name, p.name,
fmt.Sprintf("文件下载完成: %s保存在 %s", filepath.Base(savePath), savePath))
}
} else {
log.Printf("[qq] 文件下载失败: %s", fileID)
}
}()
return map[string]interface{}{"error": "下载失败,文件可能已过期。请尝试用 qq_get_message 重新获取消息(可能包含新的下载 URL或让发送者重新发送文件"}, nil
return map[string]interface{}{"status": "started", "file_id": fileID, "filename": dispName,
"hint": "下载已后台启动,完成后会推送通知。若长时间未收到完成通知表示文件已过期无法下载,需让发送者重新发送"}, nil
}
func (p *Plugin) handleUploadGroupFile(args map[string]interface{}) (interface{}, error) {
@ -1632,6 +1671,75 @@ func (p *Plugin) downloadGroupFile(groupID int64, fileID, filename string) strin
return savePath
}
func (p *Plugin) downloadPrivateFile(userID int64, fileID, filename string) string {
resp, err := p.napcat("get_private_file_url", map[string]interface{}{"user_id": userID, "file_id": fileID})
if err != nil {
return ""
}
respStr, _ := rawString(resp)
if respStr == "" {
return ""
}
var parsed struct {
Data *struct {
URL string `json:"url"`
} `json:"data"`
}
if json.Unmarshal([]byte(respStr), &parsed) != nil || parsed.Data == nil || parsed.Data.URL == "" {
return ""
}
return p.downloadURL(parsed.Data.URL, filename)
}
func (p *Plugin) downloadFromNapCat(fileID, filename string) string {
if p.filesDir == "" || fileID == "" {
return ""
}
os.MkdirAll(p.filesDir, 0755)
raw, err := p.napcat("get_file", map[string]interface{}{"file_id": fileID})
if err != nil {
return ""
}
rawStr, _ := rawString(raw)
if rawStr == "" {
return ""
}
var resp struct {
Data *struct {
File string `json:"file"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
Base64 string `json:"base64"`
URL string `json:"url"`
} `json:"data"`
}
if json.Unmarshal([]byte(rawStr), &resp) != nil || resp.Data == nil {
return ""
}
info := resp.Data
if info.FileName != "" {
filename = info.FileName
}
if info.Base64 != "" {
data, err := base64.StdEncoding.DecodeString(info.Base64)
if err == nil {
savePath := filepath.Join(p.filesDir, sanitizeFilename(filename))
os.WriteFile(savePath, data, 0644)
return savePath
}
}
if info.URL != "" {
return p.downloadURL(info.URL, filename)
}
if info.File != "" {
savePath := filepath.Join(p.filesDir, sanitizeFilename(filename))
if err := os.WriteFile(savePath, []byte(info.File), 0644); err == nil {
return savePath
}
}
return ""
}
func (p *Plugin) downloadURL(fileURL, filename string) string {
if p.filesDir == "" {
return ""