mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
chore: 同步 vendored SDK(qq v1.2.0 + plugindev bridge 修复)
third_party/homeagent-sdk 同步至 SDK 仓 61f307b: - fix(plugindev): bridge 模板补 dispatchIO.SetToolBlocks SDK v0.9.2 给 IOInjector 加了该方法但 C ABI bridge 模板未同步, 导致任何外部插件编译失败(missing method SetToolBlocks) - feat(qq) v1.2.0: msg_id→get_history 7 天兜底(不缓存正文) + qq_list_chats / qq_mark_read 会话列表(按最新消息排序 + 未读数) + 中断模板补 fallback 路径与私聊 user_id .gitignore 补 HarmonyOS 构建工具链缓存(.hvigor-home/ .npm-cache/ .ohpm/), 这些由 HVIGOR_USER_HOME/ohpm 生成,非源码。 go build ./cmd/homed/ 通过。
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@ -46,3 +46,8 @@ dist/
|
|||||||
.pi-glla/
|
.pi-glla/
|
||||||
.omo/
|
.omo/
|
||||||
.pi/
|
.pi/
|
||||||
|
|
||||||
|
# HarmonyOS 构建工具链缓存(HVIGOR_USER_HOME / ohpm 生成,非源码)
|
||||||
|
.hvigor-home/
|
||||||
|
.npm-cache/
|
||||||
|
.ohpm/
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "qq",
|
"name": "qq",
|
||||||
"name_zh": "QQ消息",
|
"name_zh": "QQ消息",
|
||||||
"name_en": "qq",
|
"name_en": "qq",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"description": "QQ 消息收发插件,通过 NapCat 协议桥接",
|
"description": "QQ 消息收发插件,通过 NapCat 协议桥接",
|
||||||
"author": "HomeAgent",
|
"author": "HomeAgent",
|
||||||
"entry": "plugin.so",
|
"entry": "plugin.so",
|
||||||
|
|||||||
356
third_party/homeagent-sdk/example/qq/plugin.go
vendored
356
third_party/homeagent-sdk/example/qq/plugin.go
vendored
@ -107,13 +107,149 @@ type Plugin struct {
|
|||||||
downloadTasks []*DownloadTask
|
downloadTasks []*DownloadTask
|
||||||
typingMu sync.Mutex
|
typingMu sync.Mutex
|
||||||
typingMap map[int64]*typingState
|
typingMap map[int64]*typingState
|
||||||
|
|
||||||
|
// msg_id → peer 映射 + 会话最新状态(<7 天兜底 get_history + list_chats)
|
||||||
|
msgMu sync.Mutex
|
||||||
|
msgMap map[int64]msgRef // message_id → {peer, time}
|
||||||
|
chats map[int64]*chatMeta // peerID → 会话状态(群号或 QQ 号)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type typingState struct {
|
type typingState struct {
|
||||||
userID int64
|
userID int64
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// msgRef 一条已见过的消息的引用:只记录 msg_id → (peer, time) 映射,不缓存正文。
|
||||||
|
// 用途:NapCat get_msg 的临时短号 <7 天失效时,据此把 get_msg 兜底为按 peer 拉 get_history。
|
||||||
|
type msgRef struct {
|
||||||
|
peerID int64
|
||||||
|
isGroup bool
|
||||||
|
time int64 // 秒级时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// chatMeta 一个会话(群/私聊)的最新状态,供 list_chats 展示。
|
||||||
|
// 只维护最新一条的短摘要(≤qqLastSumLen 字符)与未读数,不缓存完整历史。
|
||||||
|
type chatMeta struct {
|
||||||
|
peerID int64
|
||||||
|
isGroup bool
|
||||||
|
name string
|
||||||
|
unread int
|
||||||
|
lastTime int64
|
||||||
|
lastText string
|
||||||
|
lastNick string
|
||||||
|
}
|
||||||
|
|
||||||
|
const qqMsgTTL = 7 * 86400 // 7 天:msg_id → peer 映射的有效期
|
||||||
|
const qqLastSumLen = 60 // list_chats 里最新一条摘要的最大长度
|
||||||
|
|
||||||
|
// snapshotMsg 记录一条策略允许的消息:更新 msg_id→peer 映射与会话未读/最新状态。
|
||||||
|
// 不缓存消息正文(仅最新一条留 ≤qqLastSumLen 的摘要供列表展示)。
|
||||||
|
func (p *Plugin) snapshotMsg(msgID, peerID int64, isGroup bool, t int64, nickname, text string) {
|
||||||
|
if msgID <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.msgMu.Lock()
|
||||||
|
defer p.msgMu.Unlock()
|
||||||
|
|
||||||
|
// msg_id 映射(7 天 TTL,惰性清理)
|
||||||
|
p.msgMap[msgID] = msgRef{peerID: peerID, isGroup: isGroup, time: t}
|
||||||
|
now := time.Now().Unix()
|
||||||
|
if len(p.msgMap) > 2000 { // 定期清理过期项
|
||||||
|
for k, v := range p.msgMap {
|
||||||
|
if now-v.time > qqMsgTTL {
|
||||||
|
delete(p.msgMap, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := p.chats[peerID]
|
||||||
|
if ch == nil {
|
||||||
|
ch = &chatMeta{peerID: peerID, isGroup: isGroup}
|
||||||
|
p.chats[peerID] = ch
|
||||||
|
}
|
||||||
|
if ch.name == "" {
|
||||||
|
if isGroup {
|
||||||
|
ch.name = fmt.Sprintf("群%d", peerID)
|
||||||
|
} else {
|
||||||
|
ch.name = nickname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按到达次序维护未读与最新摘要:仅当本条更新时才更新 lastTime/lastText(保持按时间排)
|
||||||
|
if t > ch.lastTime {
|
||||||
|
ch.lastTime = t
|
||||||
|
ch.lastText = text
|
||||||
|
ch.lastNick = nickname
|
||||||
|
}
|
||||||
|
ch.unread++
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupMsgRef 查 msg_id 映射,返回 (peer, isGroup, time, ok)。超过 7 天视为无效(交给 get_history)。
|
||||||
|
func (p *Plugin) lookupMsgRef(msgID int64) (int64, bool, int64, bool) {
|
||||||
|
p.msgMu.Lock()
|
||||||
|
defer p.msgMu.Unlock()
|
||||||
|
ref, ok := p.msgMap[msgID]
|
||||||
|
if !ok {
|
||||||
|
return 0, false, 0, false
|
||||||
|
}
|
||||||
|
now := time.Now().Unix()
|
||||||
|
if now-ref.time > qqMsgTTL {
|
||||||
|
delete(p.msgMap, msgID)
|
||||||
|
return 0, false, 0, false
|
||||||
|
}
|
||||||
|
return ref.peerID, ref.isGroup, ref.time, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// markChatRead 清零某会话未读数(模型处理完该会话后调用)。
|
||||||
|
func (p *Plugin) markChatRead(peerID int64) {
|
||||||
|
p.msgMu.Lock()
|
||||||
|
defer p.msgMu.Unlock()
|
||||||
|
if ch := p.chats[peerID]; ch != nil {
|
||||||
|
ch.unread = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// listChats 返回会话列表:按最新消息时间降序,含未读数与最新一条摘要。
|
||||||
|
func (p *Plugin) listChats(capN int) []map[string]interface{} {
|
||||||
|
p.msgMu.Lock()
|
||||||
|
list := make([]*chatMeta, 0, len(p.chats))
|
||||||
|
for _, c := range p.chats {
|
||||||
|
list = append(list, c)
|
||||||
|
}
|
||||||
|
p.msgMu.Unlock()
|
||||||
|
|
||||||
|
// 降序(最新消息在前)
|
||||||
|
for i := 1; i < len(list); i++ {
|
||||||
|
for j := i; j > 0 && list[j].lastTime > list[j-1].lastTime; j-- {
|
||||||
|
list[j], list[j-1] = list[j-1], list[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(list) > capN {
|
||||||
|
list = list[:capN]
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]map[string]interface{}, 0, len(list))
|
||||||
|
for _, c := range list {
|
||||||
|
typ := "private"
|
||||||
|
if c.isGroup {
|
||||||
|
typ = "group"
|
||||||
|
}
|
||||||
|
item := map[string]interface{}{
|
||||||
|
"peer_id": c.peerID,
|
||||||
|
"type": typ,
|
||||||
|
"name": c.name,
|
||||||
|
"unread": c.unread,
|
||||||
|
"last_text": c.lastText,
|
||||||
|
"last_nick": c.lastNick,
|
||||||
|
}
|
||||||
|
if c.lastTime > 0 {
|
||||||
|
item["last_time"] = time.Unix(c.lastTime, 0).Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Plugin) Name() string { return p.name }
|
func (p *Plugin) Name() string { return p.name }
|
||||||
|
|
||||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||||
@ -150,6 +286,10 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
|
|
||||||
p.httpClient = &http.Client{Timeout: 30 * time.Second}
|
p.httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
|
// msg_id → peer 映射 + 会话状态(不缓存正文)
|
||||||
|
p.msgMap = make(map[int64]msgRef)
|
||||||
|
p.chats = make(map[int64]*chatMeta)
|
||||||
|
|
||||||
// 从 NapCat 获取 Bot 身份(阻塞等待,最多 5s)
|
// 从 NapCat 获取 Bot 身份(阻塞等待,最多 5s)
|
||||||
p.fetchBotInfo()
|
p.fetchBotInfo()
|
||||||
if p.botID == 0 {
|
if p.botID == 0 {
|
||||||
@ -248,6 +388,27 @@ type 枚举: text(文字)/ voice(语音转文字后发送)/ image(图
|
|||||||
},
|
},
|
||||||
}, p.handleGetHistory)
|
}, p.handleGetHistory)
|
||||||
|
|
||||||
|
p.regTool(s, sdk.ToolDef{
|
||||||
|
Name: tp + "list_chats", Description: "获取QQ会话列表,与真人客户端一致:按最新消息先后排序,每条标注会话(群/私聊)、会话名、未读消息数、最新一条消息摘要与时间。用于发现有未读消息的会话,再配合 qq_get_history 拉取对应会话内容、output_send__qq 回复。",
|
||||||
|
NoMemory: false,
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object", "properties": map[string]interface{}{
|
||||||
|
"count": map[string]interface{}{"type": "integer", "description": "最多返回会话数,默认10"},
|
||||||
|
}, "required": []string{},
|
||||||
|
},
|
||||||
|
}, p.handleListChats)
|
||||||
|
|
||||||
|
p.regTool(s, sdk.ToolDef{
|
||||||
|
Name: tp + "mark_read", Description: "将某个会话的未读计数清零(对象:群聊传 group_id,私聊传 user_id)。处理完某会话消息后可调用,让 list_chats 的未读数回到0,与真人客户端标记已读一致。",
|
||||||
|
NoMemory: false,
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object", "properties": map[string]interface{}{
|
||||||
|
"group_id": map[string]interface{}{"type": "integer", "description": "群号(与user_id二选一)"},
|
||||||
|
"user_id": map[string]interface{}{"type": "integer", "description": "QQ号(与group_id二选一)"},
|
||||||
|
}, "required": []string{},
|
||||||
|
},
|
||||||
|
}, p.handleMarkRead)
|
||||||
|
|
||||||
// ---- 查询 ----
|
// ---- 查询 ----
|
||||||
p.regTool(s, sdk.ToolDef{
|
p.regTool(s, sdk.ToolDef{
|
||||||
Name: tp + "get_groups", Description: "获取QQ群列表,可按关键词搜索群名",
|
Name: tp + "get_groups", Description: "获取QQ群列表,可按关键词搜索群名",
|
||||||
@ -772,6 +933,28 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 记录 msg_id→peer 映射与会话状态(不缓存正文,仅最新一条短摘要)----
|
||||||
|
// 策略允许的消息(群/私聊、是否 @bot 均记),供 get_msg 兜底与 list_chats 使用;
|
||||||
|
// @bot 与否只决定是否发中断,不影响记录——与真人客户端一致看到全部会话。
|
||||||
|
{
|
||||||
|
peerID, isGroup := evt.UserID, false
|
||||||
|
if evt.MessageType == "group" {
|
||||||
|
peerID, isGroup = evt.GroupID, true
|
||||||
|
}
|
||||||
|
sum := text
|
||||||
|
runes := []rune(sum)
|
||||||
|
if len(runes) > qqLastSumLen {
|
||||||
|
sum = string(runes[:qqLastSumLen]) + "…"
|
||||||
|
}
|
||||||
|
if evt.Time == 0 {
|
||||||
|
evt.Time = time.Now().Unix()
|
||||||
|
}
|
||||||
|
p.snapshotMsg(evt.MessageID, peerID, isGroup, evt.Time, nickname, sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.MessageType == "group" {
|
||||||
// 群消息必须 @ 机器人才响应
|
// 群消息必须 @ 机器人才响应
|
||||||
if p.botID == 0 {
|
if p.botID == 0 {
|
||||||
log.Printf("[qq] bot ID unknown, rejecting group message from %d", evt.GroupID)
|
log.Printf("[qq] bot ID unknown, rejecting group message from %d", evt.GroupID)
|
||||||
@ -791,9 +974,9 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|||||||
outputTool := "output_send__" + p.name
|
outputTool := "output_send__" + p.name
|
||||||
var interrupt string
|
var interrupt string
|
||||||
if evt.MessageType == "group" {
|
if evt.MessageType == "group" {
|
||||||
interrupt = fmt.Sprintf("来自「%s」在群「%s」的消息(message_id=%d)。使用%sget_message(message_id=%d)获取消息正文。如果消息包含引用回复,使用%sget_history(group_id=%d)查看上下文。使用%s回复群聊", nickname, "群聊", evt.MessageID, tp, evt.MessageID, tp, evt.GroupID, outputTool)
|
interrupt = fmt.Sprintf("来自「%s」在群「%s」的消息(message_id=%d)。先用%sget_message(message_id=%d)取正文;若取不到(消息已过期),改用%sget_history(group_id=%d)按会话拉取上下文,或用%slist_chats 查看未读会话。用%s回复群聊", nickname, "群聊", evt.MessageID, tp, evt.MessageID, tp, evt.GroupID, tp, outputTool)
|
||||||
} else {
|
} else {
|
||||||
interrupt = fmt.Sprintf("来自「%s」的私聊消息(message_id=%d)。使用%sget_message(message_id=%d)获取消息正文。使用%s回复对方", nickname, evt.MessageID, tp, evt.MessageID, outputTool)
|
interrupt = fmt.Sprintf("来自「%s」的私聊消息(message_id=%d, user_id=%d)。先用%sget_message(message_id=%d)取正文;若取不到(消息已过期),改用%sget_history(user_id=%d)按会话拉取上下文,或用%slist_chats 查看未读会话。用%s回复对方", nickname, evt.MessageID, evt.UserID, tp, evt.MessageID, tp, evt.UserID, tp, outputTool)
|
||||||
}
|
}
|
||||||
if p.isAdmin(evt.UserID) {
|
if p.isAdmin(evt.UserID) {
|
||||||
interrupt = "【重要!老大消息】" + interrupt
|
interrupt = "【重要!老大消息】" + interrupt
|
||||||
@ -841,6 +1024,126 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// ======== Tool Handlers ========
|
// ======== Tool Handlers ========
|
||||||
|
|
||||||
|
// getMsgFromHistoryByTime 按 (peer, isGroup, targetTime) 从 NapCat 拉最近历史,返回距 targetTime 最近的完整消息。
|
||||||
|
func (p *Plugin) getMsgFromHistoryByTime(peerID int64, isGroup bool, targetTime int64) (map[string]interface{}, bool) {
|
||||||
|
ep := "get_friend_msg_history"
|
||||||
|
params := map[string]interface{}{"user_id": peerID, "count": 50}
|
||||||
|
if isGroup {
|
||||||
|
ep = "get_group_msg_history"
|
||||||
|
params = map[string]interface{}{"group_id": peerID, "count": 50}
|
||||||
|
}
|
||||||
|
raw, err := p.napcat(ep, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
rawStr, _ := rawString(raw)
|
||||||
|
if rawStr == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Data *struct {
|
||||||
|
Messages []interface{} `json:"messages"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal([]byte(rawStr), &resp) != nil || resp.Data == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
var best map[string]interface{}
|
||||||
|
bestAbs := int64(-1)
|
||||||
|
for _, m := range resp.Data.Messages {
|
||||||
|
mm, ok := m.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mt, _ := mm["time"].(float64)
|
||||||
|
t := int64(mt)
|
||||||
|
if t == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
abs := t - targetTime
|
||||||
|
if abs < 0 {
|
||||||
|
abs = -abs
|
||||||
|
}
|
||||||
|
if bestAbs < 0 || abs < bestAbs {
|
||||||
|
bestAbs = abs
|
||||||
|
best = mm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if best == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return best, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// msgToGetMsgResult 把一条 NapCat 历史消息对象转成与 get_msg 同构的结果(历史包装语义)。
|
||||||
|
func msgToGetMsgResult(msg map[string]interface{}) map[string]interface{} {
|
||||||
|
nickname := ""
|
||||||
|
if s, ok := msg["sender"].(map[string]interface{}); ok {
|
||||||
|
if n, _ := s["nickname"].(string); n != "" {
|
||||||
|
nickname = n
|
||||||
|
}
|
||||||
|
if c, _ := s["card"].(string); c != "" {
|
||||||
|
nickname = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rawText, _ := msg["raw_message"].(string)
|
||||||
|
content := rawText
|
||||||
|
if content == "" {
|
||||||
|
if segs, ok := msg["message"].([]interface{}); ok {
|
||||||
|
var parts []string
|
||||||
|
for _, seg := range segs {
|
||||||
|
segMap, _ := seg.(map[string]interface{})
|
||||||
|
if segMap == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ, _ := segMap["type"].(string)
|
||||||
|
segData, _ := segMap["data"].(map[string]interface{})
|
||||||
|
if segData == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch typ {
|
||||||
|
case "text":
|
||||||
|
if t, _ := segData["text"].(string); t != "" {
|
||||||
|
parts = append(parts, t)
|
||||||
|
}
|
||||||
|
case "image":
|
||||||
|
parts = append(parts, "[图片]")
|
||||||
|
case "file":
|
||||||
|
if n, _ := segData["name"].(string); n != "" {
|
||||||
|
parts = append(parts, "[文件:"+n+"]")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if typ != "" {
|
||||||
|
parts = append(parts, "["+typ+"]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(parts) > 0 {
|
||||||
|
content = strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mid, _ := msg["message_id"].(float64)
|
||||||
|
uid, _ := msg["user_id"].(float64)
|
||||||
|
gid, _ := msg["group_id"].(float64)
|
||||||
|
mt, _ := msg["time"].(float64)
|
||||||
|
mtType, _ := msg["message_type"].(string)
|
||||||
|
loc := "私聊"
|
||||||
|
if mtType == "group" || gid > 0 {
|
||||||
|
loc = "群聊"
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"content": content,
|
||||||
|
"message_id": int64(mid),
|
||||||
|
"user_id": int64(uid),
|
||||||
|
"group_id": int64(gid),
|
||||||
|
"nickname": nickname,
|
||||||
|
"message_type": mtType,
|
||||||
|
"type": loc,
|
||||||
|
"time": time.Unix(int64(mt), 0).Format("2006-01-02 15:04:05"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Plugin) handleGetMessage(args map[string]interface{}) (interface{}, error) {
|
func (p *Plugin) handleGetMessage(args map[string]interface{}) (interface{}, error) {
|
||||||
msgID, err := convInt64(args["message_id"])
|
msgID, err := convInt64(args["message_id"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -850,10 +1153,24 @@ func (p *Plugin) handleGetMessage(args map[string]interface{}) (interface{}, err
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 本地 msg_id→peer 映射命中且 <7 天 → 用 get_history 语义兜底(NapCat 临时短号失效也不怕)
|
||||||
|
if peerID, isGroup, t, ok := p.lookupMsgRef(msgID); ok {
|
||||||
|
if m, found := p.getMsgFromHistoryByTime(peerID, isGroup, t); found {
|
||||||
|
// 找到同会话、时间最接近的消息,包装为 get_msg 同构返回
|
||||||
|
res := msgToGetMsgResult(m)
|
||||||
|
res["resolved_via"] = "history" // 标明由历史查询兜底
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
// 历史窗口内没找到(消息可能被裁剪/更早),回退 NapCat 原查询
|
||||||
|
}
|
||||||
|
return p.getMsgFromNapcat(msgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) getMsgFromNapcat(msgID int64) (interface{}, error) {
|
||||||
raw, err := p.napcat("get_msg", map[string]interface{}{"message_id": msgID})
|
raw, err := p.napcat("get_msg", map[string]interface{}{"message_id": msgID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"content": fmt.Sprintf("查询 NapCat 失败: %s", err),
|
"content": fmt.Sprintf("查询 NapCat 失败: %s。该 message_id 可能已过期,请改用 qq_get_history 按会话拉取最近消息(或用 qq_list_chats 看未读会话)", err),
|
||||||
"message_id": msgID,
|
"message_id": msgID,
|
||||||
"not_found": true,
|
"not_found": true,
|
||||||
}, nil
|
}, nil
|
||||||
@ -884,7 +1201,7 @@ func (p *Plugin) handleGetMessage(args map[string]interface{}) (interface{}, err
|
|||||||
}
|
}
|
||||||
if err := json.Unmarshal([]byte(rawStr), &resp); err != nil || resp.Data == nil {
|
if err := json.Unmarshal([]byte(rawStr), &resp); err != nil || resp.Data == nil {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"content": "解析 NapCat 响应失败",
|
"content": "解析 NapCat 响应失败(消息可能已过期)。请改用 qq_get_history 按会话拉取最近消息,或用 qq_list_chats 查看未读会话",
|
||||||
"message_id": msgID,
|
"message_id": msgID,
|
||||||
"not_found": true,
|
"not_found": true,
|
||||||
}, nil
|
}, nil
|
||||||
@ -1214,6 +1531,31 @@ func (p *Plugin) handleSendFile(args map[string]interface{}) (interface{}, error
|
|||||||
return p.napcat("send_private_msg", params)
|
return p.napcat("send_private_msg", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) handleListChats(args map[string]interface{}) (interface{}, error) {
|
||||||
|
count := 10
|
||||||
|
if c, err := convInt64(args["count"]); err == nil && c > 0 && c < 100 {
|
||||||
|
count = int(c)
|
||||||
|
}
|
||||||
|
chats := p.listChats(count)
|
||||||
|
return map[string]interface{}{
|
||||||
|
"chats": chats,
|
||||||
|
"total": len(chats),
|
||||||
|
"hint": "按最新消息先后排序;unread 为该会话未读消息数,处理完用 qq_mark_read 清零;用 qq_get_history(group_id/user_id) 拉取会话内容",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) handleMarkRead(args map[string]interface{}) (interface{}, error) {
|
||||||
|
if gid, err := convInt64(args["group_id"]); err == nil {
|
||||||
|
p.markChatRead(gid)
|
||||||
|
return map[string]interface{}{"status": "ok", "group_id": gid, "unread": 0}, nil
|
||||||
|
}
|
||||||
|
if uid, err := convInt64(args["user_id"]); err == nil {
|
||||||
|
p.markChatRead(uid)
|
||||||
|
return map[string]interface{}{"status": "ok", "user_id": uid, "unread": 0}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("need group_id or user_id")
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Plugin) handleGetHistory(args map[string]interface{}) (interface{}, error) {
|
func (p *Plugin) handleGetHistory(args map[string]interface{}) (interface{}, error) {
|
||||||
gid, gerr := convInt64(args["group_id"])
|
gid, gerr := convInt64(args["group_id"])
|
||||||
uid, uerr := convInt64(args["user_id"])
|
uid, uerr := convInt64(args["user_id"])
|
||||||
@ -1352,6 +1694,12 @@ func (p *Plugin) handleGetHistory(args map[string]interface{}) (interface{}, err
|
|||||||
if len(files) > 0 {
|
if len(files) > 0 {
|
||||||
result["files"] = files
|
result["files"] = files
|
||||||
}
|
}
|
||||||
|
// 拉取过某会话历史即视为已读(与真人客户端一致:看过=已读)
|
||||||
|
if gerr == nil {
|
||||||
|
p.markChatRead(gid)
|
||||||
|
} else if uerr == nil {
|
||||||
|
p.markChatRead(uid)
|
||||||
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -540,6 +540,9 @@ func (dispatchIO) InjectInterruptText(s, c, t string) { callVoid(6, s, c, t, 0,
|
|||||||
func (dispatchIO) InjectText(s, c, t string) { callVoid(5, s, c, t, 0, 0) }
|
func (dispatchIO) InjectText(s, c, t string) { callVoid(5, s, c, t, 0, 0) }
|
||||||
func (dispatchIO) InjectTextNoMemory(s, c, t string) { callVoid(7, s, c, t, 0, 0) }
|
func (dispatchIO) InjectTextNoMemory(s, c, t string) { callVoid(7, s, c, t, 0, 0) }
|
||||||
func (dispatchIO) InjectInputSync(s, c, t string) string { r, _ := callString(47, s, c, t, 0, 0); return r }
|
func (dispatchIO) InjectInputSync(s, c, t string) string { r, _ := callString(47, s, c, t, 0, 0); return r }
|
||||||
|
// SetToolBlocks 是 Go 原生(非 ABI)的多模态注入;跨 ABI 的外部插件无对应内核桥接,
|
||||||
|
// 故为空实现(满足接口即可)。需要多模态块时用插件内自持 SDK,不走 ABI。
|
||||||
|
func (dispatchIO) SetToolBlocks([]sdk.ContentBlock) {}
|
||||||
|
|
||||||
type dispatchMemory struct{}
|
type dispatchMemory struct{}
|
||||||
func (dispatchMemory) Recall(q []string, d int) ([]sdk.Entity, []sdk.Relation, error) {
|
func (dispatchMemory) Recall(q []string, d int) ([]sdk.Entity, []sdk.Relation, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user