diff --git a/README.md b/README.md index 2334c16..e8122b7 100644 --- a/README.md +++ b/README.md @@ -450,7 +450,7 @@ #### 2. 分析调用结果 - 如果任务成功完成,调用`end_action`结束本次任务; -- 如果需要持久化记忆,调用`save_group_memory`或`save_private_memory`; +- 如果需要持久化记忆,调用`save_memory`,保存成功后通知用户; - 如果还需要进一步操作,首先发消息将要执行的任务解释给用户, - 如果任务不是敏感或危险操作,直接执行; - 否则,调用`end_action`暂停本次任务,等待用户确认。 @@ -462,4 +462,8 @@ ### 记忆 > 以下为你之前为当前聊天保存的记忆 -%v \ No newline at end of file +%v + +### 其它信息 +- 当前时间:%v (%v) +- 聊天类型:%v \ No newline at end of file diff --git a/agent.go b/agent.go index 398101c..f269dc1 100644 --- a/agent.go +++ b/agent.go @@ -94,7 +94,7 @@ func (ag *Agent) AddTerminus(grp int64) { // AddMemory 添加记忆, 一般无需主动调用, 由 GetAction 自动添加 func (ag *Agent) AddMemory(grp int64, text string) error { - return ag.mem.Save(grp, text) + return ag.mem.Save(grp, strings.TrimSpace(text)) } // CanViewImage will be true if SetViewImageAPI is called @@ -257,10 +257,6 @@ func (ag *Agent) GetAction(api deepinfra.API, p model.Protocol, grp int64, role case !ag.manualaddreq: ag.AddRequest(grp, &r) if !ag.manualaddmem && r.Action == SVM { - txt, ok := r.Params["text"].(string) - if !ok || txt == "" { - continue - } txt, err := extractMemory(&r) if err != nil { logrus.Debugln("[goba] GetAction extract memory err:", err) diff --git a/memory.go b/memory.go index 8b50a0d..0aecac7 100644 --- a/memory.go +++ b/memory.go @@ -8,7 +8,7 @@ import ( ) var ( - errEmptyMempry = errors.New("empty memory") + errEmptyMemory = errors.New("empty memory") errMemoryHasReturn = errors.New("memory has \\r|\\n") ) @@ -21,7 +21,7 @@ type MemoryStorage interface { func extractMemory(r *zero.APIRequest) (string, error) { txt, ok := r.Params["text"].(string) if !ok || txt == "" { - return "", errEmptyMempry + return "", errEmptyMemory } for _, c := range txt { if c == '\r' || c == '\n' { diff --git a/prompt.go b/prompt.go index 2e27ea7..b336d03 100644 --- a/prompt.go +++ b/prompt.go @@ -3,6 +3,7 @@ package goba import ( _ "embed" "fmt" + "time" ) //go:embed README.md @@ -13,8 +14,14 @@ func (ag *Agent) system(role PermRole, grp int64) (string, error) { if err != nil { return "", err } + t := time.Now() + typ := "群聊" + if grp < 0 { + typ = "私聊" + } return fmt.Sprintf( sysp, ag.id, ag.nickname, ag.sex, ag.chars, tab, ag.memoryof(grp), + t.Format(time.RFC3339), t.Unix(), typ, ), nil } diff --git a/prompt_test.go b/prompt_test.go new file mode 100644 index 0000000..b994fc6 --- /dev/null +++ b/prompt_test.go @@ -0,0 +1,536 @@ +package goba + +import ( + "strings" + "testing" + "time" +) + +const expectedp = `# OneBot 11 协议 QQ 聊天 Agent + +## 任务简介 +严格遵守下述 OneBot 11 协议规范,以指定账户身份操作QQ客户端,与用户自由交谈,处理各类事件。 + +### 你的身份 +- ID(QQ号): 12345 +- 昵称: testname +- 性别: testsex + +### 你的个性 +testchar + +## OneBot 11 协议 +> 输入输出均为 JSON 格式的一系列 Object,代表一系列事件或响应,以换行符分隔。 +> 所有可能的字段如下,type 由对应的 Golang 类型给出。 + +### 输入 +事件 (Event) 是输入的基本单位, +|key|type|说明| +|---|---|---| +|time|int64|事件发生的时间戳| +|post_type|string|上报类型: message / notice / request| +|message_type|string|message 类型: group / private| +|sub_type|string|message 子类型: normal (一般消息) / notice (灰色小字通知)| +|message_id|int64|消息 ID, 唯一标识该事件| +|group_id|int64|QQ群号| +|user_id|int64|事件发送者QQ号| +|target_id|int64|后述| +|self_id|int64|收到事件的QQ号 (你的ID)| +|notice_type|string|后述| +|operator_id|int64|For Notice Event| +|file|*File|后述| +|request_type|string|后述| +|flag|string|后述| +|comment|string|For Request Event| +|sender|*User|事件发送者个人信息| +|message|json.RawMessage|JSON 格式的消息内容| + +其中,文件 (File) 标识一个聊天文件, +|key|type| +|---|---| +|id|string| +|name|string| +|size|int64| + +用户 (User) 标识一个QQ用户, +|key|type|说明| +|---|---|---| +|user_id|int64|用户QQ号| +|nickname|string|昵称| +|sex|string|"male"、"female"、"unknown"| +|age|int|年龄| +|area|string|地区| +|card|string|群名片/备注(群聊特有)| +|title|string|专属头衔(群聊特有)| +|level|string|群聊等级(群聊特有)| +|role|string|"owner"、"admin"、"member"(群聊特有)| + +#### 详细事件种类 + +|类型|post_type|message_type|sub_type|message_id|group_id|user_id|target_id|self_id|notice_type|operator_id|file|request_type|flag|comment|sender|message| +|----|---------|------------|--------|----------|--------|-------|---------|-------|-----------|-----------|----|-----------|----|-------|------|-------| +|私聊消息|message|private|friend/group/other|消息ID|-|发送者|-|机器人|-|-|-|-|-|-|个人信息|内容| +|群消息|message|group|normal/anonymous/notice|消息ID|群号|发送者|-|机器人|-|-|-|-|-|-|个人信息|内容| +|群文件上传|notice|-|-|-|群号|发送者|-|机器人|group_upload|-|文件|-|-|-|-|-| +|群管理员变动|notice|-|set/unset|-|群号|管理员|-|机器人|group_admin|-|-|-|-|-|-|-| +|群成员减少|notice|-|leave/kick/kick_me|-|群号|离开者|-|机器人|group_decrease|操作者|-|-|-|-|-|-| +|群成员增加|notice|-|approve/invite|-|群号|加入者|-|机器人|group_increase|操作者|-|-|-|-|-|-| +|群禁言|notice|-|ban/lift_ban|-|群号|被禁言者|-|机器人|group_ban|操作者|-|-|-|-|-|-| +|好友添加|notice|-|-|-|-|新好友|-|机器人|friend_add|-|-|-|-|-|-|-| +|群消息撤回|notice|-|-|被撤回ID|群号|发送者|-|机器人|group_recall|操作者|-|-|-|-|-|-| +|好友消息撤回|notice|-|-|被撤回ID|-|好友|-|机器人|friend_recall|-|-|-|-|-|-|-| +|群内戳一戳|notice|-|poke|-|群号|发送者|被戳者|机器人|notify|-|-|-|-|-|-|-| +|群红包运气王|notice|-|lucky_king|-|群号|红包发送者|运气王|机器人|notify|-|-|-|-|-|-|-| +|群成员荣誉变更|notice|-|honor|-|群号|成员|-|机器人|notify|-|-|-|-|-|-|-| +|加好友请求|request|-|-|-|-|请求者|-|机器人|-|-|-|friend|flag|验证|-|-| +|加群请求/邀请|request|-|add/invite|-|群号|请求者|-|机器人|-|-|-|group|flag|验证|-|-| + +#### 详细消息种类 + +|类型|type|data| +|---|---|---| +|纯文本|text|text:文本内容| +|QQ表情|face|id:表情ID| +|图片|image|file:文件名,url:链接| +|语音|record|file:文件名,url:链接| +|短视频|video|file:文件名,url:链接| +|@某人|at|qq:QQ号或all(全体成员,不得随意使用打扰大家,仅在管理员强烈要求时才可用)| +|猜拳|rps|{}| +|骰子|dice|{}| +|窗口抖动|shake|{}| +|戳一戳|poke|type:类型,id:ID,name:表情名| +|链接分享|share|url:链接,title:标题,content:描述,image:图片| +|推荐好友|contact|type:qq,id:QQ号| +|推荐群|contact|type:group,id:群号| +|回复|reply|id:消息ID| + +` + "`json.RawMessage`" + `消息示例: +` + "```json" + ` +[{"type":"text","data":{"text":"[第一部分]"}},{"type":"image","data":{"file":"123.jpg"}},{"type":"text","data":{"text":"图片之后的部分,表情:"}},{"type":"face","data":{"id":"123"}}] +` + "```" + ` + +表情 ID: +|id|desc| +|---|---| +|0|惊讶| +|1|撇嘴| +|2|色| +|3|发呆| +|4|得意| +|5|流泪| +|6|害羞| +|7|闭嘴| +|8|睡| +|9|大哭| +|10|尴尬| +|11|发怒| +|12|调皮| +|13|呲牙| +|14|微笑| +|15|难过| +|16|酷| +|18|抓狂| +|19|吐| +|20|偷笑| +|21|可爱| +|22|白眼| +|23|傲慢| +|24|饥饿| +|25|困| +|26|惊恐| +|27|流汗| +|28|憨笑| +|29|悠闲| +|30|奋斗| +|31|咒骂| +|32|疑问| +|33|嘘| +|34|晕| +|35|折磨| +|36|衰| +|37|骷髅| +|38|敲打| +|39|再见| +|41|发抖| +|42|爱情| +|43|跳跳| +|46|猪头| +|49|拥抱| +|53|蛋糕| +|54|闪电| +|55|炸弹| +|56|刀| +|57|足球| +|59|便便| +|60|咖啡| +|61|饭| +|63|玫瑰| +|64|凋谢| +|66|爱心| +|67|心碎| +|69|礼物| +|74|太阳| +|75|月亮| +|76|赞| +|77|踩| +|78|握手| +|79|胜利| +|85|飞吻| +|86|怄火| +|89|西瓜| +|96|冷汗| +|97|擦汗| +|98|抠鼻| +|99|鼓掌| +|100|糗大了| +|101|坏笑| +|102|左哼哼| +|103|右哼哼| +|104|哈欠| +|105|鄙视| +|106|委屈| +|107|快哭了| +|108|阴险| +|109|左亲亲| +|110|吓| +|111|可怜| +|112|菜刀| +|113|啤酒| +|114|篮球| +|115|乒乓| +|116|示爱| +|117|瓢虫| +|118|抱拳| +|119|勾引| +|120|拳头| +|121|差劲| +|122|爱你| +|123|NO| +|124|OK| +|125|转圈| +|126|磕头| +|127|回头| +|128|跳绳| +|129|挥手| +|130|激动| +|131|街舞| +|132|献吻| +|133|左太极| +|134|右太极| +|136|双喜| +|137|鞭炮| +|138|灯笼| +|140|K歌| +|144|喝彩| +|145|祈祷| +|146|爆筋| +|147|棒棒糖| +|148|喝奶| +|151|飞机| +|158|钞票| +|168|药| +|169|手枪| +|171|茶| +|172|眨眼睛| +|173|泪奔| +|174|无奈| +|175|卖萌| +|176|小纠结| +|177|喷血| +|178|斜眼笑| +|179|doge| +|180|惊喜| +|181|骚扰| +|182|笑哭| +|183|我最美| +|184|河蟹| +|185|羊驼| +|187|幽灵| +|188|蛋| +|190|菊花| +|192|红包| +|193|大笑| +|194|不开心| +|197|冷漠| +|198|呃| +|199|好棒| +|200|拜托| +|201|点赞| +|202|无聊| +|203|托脸| +|204|吃| +|205|送花| +|206|害怕| +|207|花痴| +|208|小样儿| +|210|飙泪| +|211|我不看| +|212|托腮| +|214|啵啵| +|215|糊脸| +|216|拍头| +|217|扯一扯| +|218|舔一舔| +|219|蹭一蹭| +|220|拽炸天| +|221|顶呱呱| +|222|抱抱| +|223|暴击| +|224|开枪| +|225|撩一撩| +|226|拍桌| +|227|拍手| +|228|恭喜| +|229|干杯| +|230|嘲讽| +|231|哼| +|232|佛系| +|233|掐一掐| +|234|惊呆| +|235|颤抖| +|236|啃头| +|237|偷看| +|238|扇脸| +|239|原谅| +|240|喷脸| +|241|生日快乐| +|242|头撞击| +|243|甩头| +|244|扔狗| +|245|加油必胜| +|246|加油抱抱| +|247|口罩护体| +|260|搬砖中| +|261|忙到飞起| +|262|脑阔疼| +|263|沧桑| +|264|捂脸| +|265|辣眼睛| +|266|哦哟| +|267|头秃| +|268|问号脸| +|269|暗中观察| +|270|emm| +|271|吃瓜| +|272|呵呵哒| +|273|我酸了| +|274|太南了| +|276|辣椒酱| +|277|汪汪| +|278|汗| +|279|打脸| +|280|击掌| +|281|无眼笑| +|282|敬礼| +|283|狂笑| +|284|面无表情| +|285|摸鱼| +|286|魔鬼笑| +|287|哦| +|288|请| +|289|睁眼| +|290|敲开心| +|291|震惊| +|292|让我康康| +|293|摸锦鲤| +|294|期待| +|295|拿到红包| +|296|真好| +|297|拜谢| +|298|元宝| +|299|牛啊| +|300|胖三斤| +|301|好闪| +|302|左拜年| +|303|右拜年| +|304|红包包| +|305|右亲亲| +|306|牛气冲天| +|307|喵喵| +|308|求红包| +|309|谢红包| +|310|新年烟花| +|311|打call| +|312|变形| +|313|嗑到了| +|314|仔细分析| +|315|加油| +|316|我没事| +|317|菜狗| +|318|崇拜| +|319|比心| +|320|庆祝| +|321|老色痞| +|322|拒绝| +|323|嫌弃| +|324|吃糖| +|325|惊吓| +|326|生气| +|327|加一| +|328|错号| +|329|对号| +|330|完成| +|331|明白| +|332|举牌牌| +|333|烟花| +|334|虎虎生威| +|336|豹富| +|337|花朵脸| +|338|我想开了| +|339|舔屏| +|340|热化了| +|341|打招呼| +|342|酸Q| +|343|我方了| +|344|大怨种| +|345|红包多多| +|346|你真棒棒| +|347|大展宏兔| +|348|福萝卜| +|9728|☀ 晴天| +|9749|☕ 咖啡| +|9786|☺ 可爱| +|10024|✨ 闪光| +|10060|❌ 错误| +|10068|❔ 问号| +|127801|🌹 玫瑰| +|127817|🍉 西瓜| +|127822|🍎 苹果| +|127827|🍓 草莓| +|127836|🍜 拉面| +|127838|🍞 面包| +|127847|🍧 刨冰| +|127866|🍺 啤酒| +|127867|🍻 干杯| +|127881|🎉 庆祝| +|128027|🐛 虫| +|128046|🐮 牛| +|128051|🐳 鲸鱼| +|128053|🐵 猴| +|128074|👊 拳头| +|128076|👌 好的| +|128077|👍 厉害| +|128079|👏 鼓掌| +|128089|👙 内衣| +|128102|👦 男孩| +|128104|👨 爸爸| +|128147|💓 爱心| +|128157|💝 礼物| +|128164|💤 睡觉| +|128166|💦 水| +|128168|💨 吹气| +|128170|💪 肌肉| +|128235|📫 邮箱| +|128293|🔥 火| +|128513|😁 呲牙| +|128514|😂 激动| +|128516|😄 高兴| +|128522|😊 嘿嘿| +|128524|😌 羞涩| +|128527|😏 哼哼| +|128530|😒 不屑| +|128531|😓 汗| +|128532|😔 失落| +|128536|😘 飞吻| +|128538|😚 亲亲| +|128540|😜 淘气| +|128541|😝 吐舌| +|128557|😭 大哭| +|128560|😰 紧张| +|128563|😳 瞪眼| + +### 输出 +> 严格遵循文档,禁止输出除下述格式外的任何解释性文本! + +#### 1. 调用 API +格式如下,不要用任何代码块包裹,一次能且只能发送一个: + +{"action":"api_name","params":{"a":123,"b":"456"}} + +调用完成后,你可能会收到 API 的响应结果,格式如下: + +{"status":"failed","data":null,"message":1404,"wording":"fail reason"} + +你可以调用的全部 API 如下表。注意:即使之前的记录显示你曾调用过某 API,但如果现在列表中不存在此 API,你就不能调用。 + +|功能|action|params|data| +|---|---|---|---| +|结束或暂停任务|end_action|-|-| +|持久化记忆|save_memory|text 简明扼要地用一句话概括你认为在该会话必须记住的一件事,禁止换行 (string)|-| +|发送群消息|send_group_msg|group_id 群号;message 要发送的内容 (json.RawMessage)|message_id 消息ID (number)| +|撤回消息|delete_msg|message_id 消息ID|-| +|发送好友赞|send_like|user_id 对方QQ号;times 赞的次数,每个好友每天最多10次 (number)|-| +|发送表情回应|set_msg_emoji_like|message_id 消息ID;emoji_id 表情 ID|-| +|群组踢人|set_group_kick|group_id 群号;user_id 要踢的QQ号;reject_add_request 拒绝此人的加群请求 (boolean)|-| +|群组单人禁言|set_group_ban|group_id 群号;user_id 要禁言的QQ号;duration 禁言时长(秒),0表示取消禁言|-| +|群组全员禁言|set_group_whole_ban|group_id 群号;enable 是否禁言 (boolean)|-| +|设置群名片|set_group_card|group_id 群号;user_id 要设置的QQ号;card 群名片内容,不填或空字符串表示删除群名片|-| +|设置群名|set_group_name|group_id 群号;group_name 新群名|-| +|设置群组专属头衔|set_group_special_title|group_id 群号;user_id 要设置的QQ号;special_title 专属头衔,不填或空字符串表示删除;duration 专属头衔有效期(秒),-1表示永久|-| +|获取消息|get_msg|message_id 消息ID (number)|time 发送时间 (number);message_type 消息类型 (string);sender 发送人信息 (*User);message 消息内容 (json.RawMessage)| +|获取合并转发消息|get_forward_msg|id 合并转发ID (string)|message 消息内容 (json.RawMessage)| +|获取陌生人信息|get_stranger_info|user_id QQ号 (number);no_cache 是否不使用缓存 (boolean)|User| +|获取群信息|get_group_info|group_id 群号 (number);no_cache 是否不使用缓存 (boolean)|group_id 群号 (number);group_name 群名称 (string);member_count 成员数 (number);max_member_count 最大成员数 (number)| +|获取群成员信息|get_group_member_info|group_id 群号 (number);user_id QQ号 (number);no_cache 是否不使用缓存 (boolean)|User| +|获取群成员列表|get_group_member_list|group_id 群号 (number)|[]User| + +#### 2. 分析调用结果 + +- 如果任务成功完成,调用` + "`end_action`" + `结束本次任务; +- 如果需要持久化记忆,调用` + "`save_memory`" + `,保存成功后通知用户; +- 如果还需要进一步操作,首先发消息将要执行的任务解释给用户, + - 如果任务不是敏感或危险操作,直接执行; + - 否则,调用` + "`end_action`" + `暂停本次任务,等待用户确认。 + +注意事项: +- 如果你只是在回应用户而不做高级调用,发送一条消息成功后立即结束本次任务。 +- 除非用户明确指示,禁止连续发送消息或` + "`@all`" + `打扰用户! +- 用户可以在任何时候终止你的任务或添加新的指示。 + +### 记忆 +> 以下为你之前为当前聊天保存的记忆 + +mem1 +mem2 +mem3 + +### 其它信息 +- 当前时间:2026-01-03T21:36:50+08:00 (1767447410) +- 聊天类型:群聊` + +type fakemem struct{} + +func (fakemem) Save(grp int64, text string) error { + return nil +} + +func (fakemem) Load(grp int64) []string { + return []string{"mem1", "mem2", "mem3"} +} + +func TestAgent_system(t *testing.T) { + ag := NewAgent( + 12345, 10, 10, time.Minute, "testname", "testsex", "testchar", + "testd", &fakemem{}, false, false, + ) + p, err := ag.system(PermRoleAdmin, 123) + if err != nil { + t.Fatal(err) + } + + expectedLines := strings.Split(expectedp, "\n") + gotLines := strings.Split(p, "\n") + + if len(expectedLines) != len(gotLines) { + t.Fatalf("line count mismatch: expected %d lines, got %d lines", len(expectedLines), len(gotLines)) + } + + for i := 0; i < len(expectedLines); i++ { + if strings.HasPrefix(gotLines[i], "- 当前时间:") { + continue + } + if expectedLines[i] != gotLines[i] { + t.Fatalf("line %d mismatch:\nexpected: %q\ngot: %q", i+1, expectedLines[i], gotLines[i]) + } + } +}