diff --git a/_doc/消息.md b/_doc/消息.md new file mode 100644 index 0000000..4b784ed --- /dev/null +++ b/_doc/消息.md @@ -0,0 +1,544 @@ +messages[ + { + "uncount": 1, + "flag": 0, + "bizType": 21050003, + "mid": 306388872406018, + "received": true, + "securityId": "cVB1gt2iMS_FV-B1EuTtuprq9nPmk046hua_alhFbU9irJdmTCNqWxiCzyABGAJxRhHJ19omwFtTF62aNLx0Kb-jEBjLnK5zAvB5QJQ74XNKfm3GvYbAw6Hat_UPGUYrWuoC78uf6-i_4mQDfnri1cgi33PGu4t76fHCBmlQgeyNQYZtvlemjQ~~", + "cmid": 0, + "type": 3, + "body": { + "type": 8, + "templateId": 1, + "jobDesc": { + "education": "本科", + "boss": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "distance": "", + "city": "上海 浦东新区 北蔡", + "lid": "", + "partTimeDesc": "", + "expectId": 1421763565, + "title": "信息系统产品经理", + "salary": "20-30K", + "experience": "3-5年", + "bottomText": "2月1日 18:19 由你发起的沟通", + "content": "", + "jobLabel": "", + "latlon": "", + "expectPosition": "", + "company": "中建八局上海公司", + "url": "bosszp://bosszhipin.app/openwith?type=jobview&jid=510160286&uid=631237132&securityId=_gWIU90mUoEub-b1Uw8xol50QDeii-lYj5J6gGaUDpLqCFr6_7TAHagl3UwLBh3S8tgBUY_91xh-LMPqDo56Bhg2tM5ptshBG4OSYvjb2szm9uGWm64E8GFe2pIXtMoJJW53RY_enX-0S41IAQyZSwBHGDLKYC-ajb88mwLIgFzHv26-aCA6uQXewrLCx0JKQZdIcGdUYOLlbbjVOXgCwPV04Ih_o6_BUwTq8iZjpMMBSuMa_abda3A_rjsOeY_qaN0tPcB92T2m3a0xMseYDQyJQj6x8CqTN3Wn8NhF3RuYC3KvDRGYC22HGNfGIHRHIbXlOIm5N6HK6UFRkfQMoYIl6KIJOm9-q3mHf4exItvw1_K8ArABDBF2koQ0lyCMu0bvrscAuUZCY4ggK_r4iUNaJz-r7mvO5JLona10LTHVmZ5gRGXej4kIPT9F2lj02KdK0nKWG8ilYQ2AzD0FbUI52lYa5CgAu5hhO9_kHcsJdfhxKCaSbiM_To6iTrJtfEksQnmqwMLU6tpTw-N3SA-UJsXfhwJRV3uhDsKGJeIFDpJjZ6IFr3qVVIDzwROyUZlK6Tu8Y8_nnKHxrtuIKfQPU8mFrWf02LtWWJg~", + "extend": "{\"jobType\":0}", + "jobId": 510160286, + "stage": "未融资", + "geek": { + "uid": 546224865, + "headImg": 0, + "name": "", + "company": "", + "avatar": "", + "source": 0, + "certification": 0 + }, + "iconFlag": 0, + "positionCategory": "产品经理", + "bossTitle": "招聘主管", + "expectSalary": "" + }, + "headTitle": "您正在与Boss韩先生直接沟通如下职位" + }, + "offline": false, + "pushSound": 0, + "from": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "time": 1769941171000, + "taskId": 0, + "status": 2 + }, + { + "uncount": 0, + "flag": 0, + "bizType": 101, + "mid": 306388872410117, + "received": true, + "securityId": "HGFvURTQnW2V8-_1M1FQeOyKfMqBVGmx4iYXvV-ja6Ax2OHah-vLgPuQbHIZIxOV9DTQVpbE4IDFvjkbMEeBVFun5rsz1N-55pIQUpuqzGCHJTPKTSH6pYKpFoUtSueouw04tw~~", + "cmid": 0, + "type": 3, + "body": { + "extend": "", + "text": "您正在招的信息系统产品经理我很有兴趣,如果方便,希望可以和您进一步沟通,谢谢!", + "type": 1, + "templateId": 1, + "headTitle": "" + }, + "offline": false, + "pushSound": 0, + "bizId": "42", + "from": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "time": 1769941171000, + "pushText": "肖伟民:您正在招的信息系统产品经理我很有兴趣,如果方便,希望可以和您进一步沟通,谢谢!", + "taskId": 0, + "status": 2 + }, + { + "uncount": 1, + "flag": 0, + "bizType": 317, + "mid": 306388872438784, + "received": true, + "securityId": "scWxOY-tkn0ih-A1bkgY7cd1djlTDxgd7J24wQXQqZvi8wn_teicgd2zAj7dz74o5LYrEdUJsKDHzdyUzdqrWs55p9cUQXsroGgVJrQlUspiITmPBY23-nVeei7VytBBuFATuw~~", + "cmid": 0, + "type": 4, + "body": { + "style": 3, + "type": 16, + "templateId": 1, + "articles": [ + { + "extend": "{\"descriptionHighParts\":[{\"endIndex\":8,\"startIndex\":6},{\"endIndex\":14,\"startIndex\":12}],\"avatarList\":[\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09095058edb5e6d5d9273d3250d5a0c4c56bb61e3b7bce0931da574d19d1d82c88.png\",\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09ba9684830b5f873d7f98f2d5757b12c86bb61e3b7bce0931da574d19d1d82c88.png\",\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09f663dc109c9552b92d74732363d6a4056bb61e3b7bce0931da574d19d1d82c88.png\"]}", + "picUrl": "https://img.bosszhipin.com/beijin/icon/bed51f39faf420a15620181baffee482f7aba6f40b0808dd3b1d96fc3abbc5af.png", + "highlightParts": [ + { + "startIndex": 1, + "endIndex": 3 + }, + { + "startIndex": 10, + "endIndex": 12 + } + ], + "subTitle": "共**人投递,你超过**竞争者", + "description": "优秀竞争者会**,建议你**", + "statisticParameters": "", + "title": "你与该职位竞争者PK情况", + "templateId": 5, + "url": "https://m.zhipin.com/mpa/html/props/transit?targetId=a875d648fa6db1490nV62tu9ElpW&sendNum=1&bossId=5e86870fd010f0eb0Xd72d66EVFS", + "bottomText": "查看详细分析", + "timeout": 0 + } + ], + "headTitle": "" + }, + "offline": false, + "pushSound": 0, + "from": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "time": 1769941171000, + "pushText": "你与该职位竞争者PK情况", + "taskId": 0, + "status": 2 + }, + { + "uncount": 0, + "flag": 0, + "bizType": 110, + "mid": 306962385814530, + "received": true, + "securityId": "2Qs1rcvXn2axz-Z1F_qA9Hr2oJm3oy4XqZZxP3u_iQc1b13a9hYHxYzEYHGRR5iffvX9fV3HQ02WePxjcHPZ3H2gHkLE2kI8aSRvEYf18TcPjNkA7nR8bBL75uun6ErmddoTvQ~~", + "cmid": 0, + "type": 3, + "body": { + "extend": "", + "text": "我们感谢您的投递,但您的专业技能与我们目前的职位需求并不完全吻合。祝您在BOSS直聘找到更适合您的工作!", + "type": 1, + "templateId": 1, + "headTitle": "" + }, + "offline": false, + "pushSound": 0, + "from": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "time": 1770081189000, + "pushText": "韩先生:我们感谢您的投递,但您的专业技能与我们目前的职位需求并不完全吻合。祝您在BOSS直聘找到更适合您的工作!", + "taskId": 0, + "status": 2 + }, + { + "uncount": 0, + "flag": 0, + "mid": 307012846076929, + "received": true, + "securityId": "G91RCDcRlfd6n-Y1v0PcuW2JmcEY6RpuBJKIAI3ODWIfRD2XePO7K6i_APp7VZlOzCXxU_Scck1oUQ9TfKG1gGr0ym8Ar0yFmsCud7dQJOCe6v9PnHym2ahnoRR9Z0DILSr7XA~~", + "cmid": 4611687788520895950, + "type": 1, + "body": { + "extend": "", + "text": "好的,谢谢您的时间!", + "type": 1, + "templateId": 1, + "headTitle": "" + }, + "offline": false, + "pushSound": 0, + "from": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "time": 1770093508000, + "pushText": "肖伟民:好的,谢谢您的时间!", + "taskId": 0, + "status": 1 + } +] + + + + +[ + { + "uncount": 1, + "flag": 0, + "bizType": 21050003, + "mid": 306388872406018, + "received": true, + "securityId": "l6FAuQDH5EgEB-Z1zJPUxoiSUg2Z9523Jp_in3wnvBpa97RjDD49bO-Acv9BDLEXoAlWVRufC9SHOXAYZJooF7jwLsgm5Lv3ez-EX_utlkxCIW6po1_ncoGdjyT9qh7wajk-5UjNNTGpCuszVaoArX4VKt6S1O-YlMotVz0eILByZhvb1DBbmA~~", + "cmid": 0, + "type": 3, + "body": { + "type": 8, + "templateId": 1, + "jobDesc": { + "education": "本科", + "boss": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "distance": "", + "city": "上海 浦东新区 北蔡", + "lid": "", + "partTimeDesc": "", + "expectId": 1421763565, + "title": "信息系统产品经理", + "salary": "20-30K", + "experience": "3-5年", + "bottomText": "2月1日 18:19 由你发起的沟通", + "content": "", + "jobLabel": "", + "latlon": "", + "expectPosition": "", + "company": "中建八局上海公司", + "url": "bosszp://bosszhipin.app/openwith?type=jobview&jid=510160286&uid=631237132&securityId=_gWIU90mUoEub-b1Uw8xol50QDeii-lYj5J6gGaUDpLqCFr6_7TAHagl3UwLBh3S8tgBUY_91xh-LMPqDo56Bhg2tM5ptshBG4OSYvjb2szm9uGWm64E8GFe2pIXtMoJJW53RY_enX-0S41IAQyZSwBHGDLKYC-ajb88mwLIgFzHv26-aCA6uQXewrLCx0JKQZdIcGdUYOLlbbjVOXgCwPV04Ih_o6_BUwTq8iZjpMMBSuMa_abda3A_rjsOeY_qaN0tPcB92T2m3a0xMseYDQyJQj6x8CqTN3Wn8NhF3RuYC3KvDRGYC22HGNfGIHRHIbXlOIm5N6HK6UFRkfQMoYIl6KIJOm9-q3mHf4exItvw1_K8ArABDBF2koQ0lyCMu0bvrscAuUZCY4ggK_r4iUNaJz-r7mvO5JLona10LTHVmZ5gRGXej4kIPT9F2lj02KdK0nKWG8ilYQ2AzD0FbUI52lYa5CgAu5hhO9_kHcsJdfhxKCaSbiM_To6iTrJtfEksQnmqwMLU6tpTw-N3SA-UJsXfhwJRV3uhDsKGJeIFDpJjZ6IFr3qVVIDzwROyUZlK6Tu8Y8_nnKHxrtuIKfQPU8mFrWf02LtWWJg~", + "extend": "{\"jobType\":0}", + "jobId": 510160286, + "stage": "未融资", + "geek": { + "uid": 546224865, + "headImg": 0, + "name": "", + "company": "", + "avatar": "", + "source": 0, + "certification": 0 + }, + "iconFlag": 0, + "positionCategory": "产品经理", + "bossTitle": "招聘主管", + "expectSalary": "" + }, + "headTitle": "您正在与Boss韩先生直接沟通如下职位" + }, + "offline": false, + "pushSound": 0, + "from": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "time": 1769941171000, + "taskId": 0, + "status": 2 + }, + { + "uncount": 0, + "flag": 0, + "bizType": 101, + "mid": 306388872410117, + "received": true, + "securityId": "ICqTpLTj9ZNwJ-z1EH0JV5QdmFoLScV7hG92WmVbwh8OOUMHjLT1wI49GP2NbxQ0XgOl0BWmE32TShuCnB7aBYL2Tmu5w_OLGAZWHy4iwex-v68JL6m90raPWD-xJK7PFUshRA~~", + "cmid": 0, + "type": 3, + "body": { + "extend": "", + "text": "您正在招的信息系统产品经理我很有兴趣,如果方便,希望可以和您进一步沟通,谢谢!", + "type": 1, + "templateId": 1, + "headTitle": "" + }, + "offline": false, + "pushSound": 0, + "bizId": "42", + "from": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "time": 1769941171000, + "pushText": "肖伟民:您正在招的信息系统产品经理我很有兴趣,如果方便,希望可以和您进一步沟通,谢谢!", + "taskId": 0, + "status": 2 + }, + { + "uncount": 1, + "flag": 0, + "bizType": 317, + "mid": 306388872438784, + "received": true, + "securityId": "hzKRrkb1wHEAg-p1U4iTIhg_RchIKhXZ3wxOSwcIOUeWiGyipwyvAYQhZ_2yYcMrYEcZnDiGkOFlXW23Z4qMV8SxSbBx3UlHZtnh1OQLLCnzC8BOANRVYRHOieH82O44gBThQw~~", + "cmid": 0, + "type": 4, + "body": { + "style": 3, + "type": 16, + "templateId": 1, + "articles": [ + { + "extend": "{\"descriptionHighParts\":[{\"endIndex\":8,\"startIndex\":6},{\"endIndex\":14,\"startIndex\":12}],\"avatarList\":[\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09095058edb5e6d5d9273d3250d5a0c4c56bb61e3b7bce0931da574d19d1d82c88.png\",\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09ba9684830b5f873d7f98f2d5757b12c86bb61e3b7bce0931da574d19d1d82c88.png\",\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09f663dc109c9552b92d74732363d6a4056bb61e3b7bce0931da574d19d1d82c88.png\"]}", + "picUrl": "https://img.bosszhipin.com/beijin/icon/bed51f39faf420a15620181baffee482f7aba6f40b0808dd3b1d96fc3abbc5af.png", + "highlightParts": [ + { + "startIndex": 1, + "endIndex": 3 + }, + { + "startIndex": 10, + "endIndex": 12 + } + ], + "subTitle": "共**人投递,你超过**竞争者", + "description": "优秀竞争者会**,建议你**", + "statisticParameters": "", + "title": "你与该职位竞争者PK情况", + "templateId": 5, + "url": "https://m.zhipin.com/mpa/html/props/transit?targetId=a875d648fa6db1490nV62tu9ElpW&sendNum=1&bossId=5e86870fd010f0eb0Xd72d66EVFS", + "bottomText": "查看详细分析", + "timeout": 0 + } + ], + "headTitle": "" + }, + "offline": false, + "pushSound": 0, + "from": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "time": 1769941171000, + "pushText": "你与该职位竞争者PK情况", + "taskId": 0, + "status": 2 + }, + { + "uncount": 0, + "flag": 0, + "bizType": 110, + "mid": 306962385814530, + "received": true, + "securityId": "6NQl3ioceNxKx-G1nOrvVjeUEQLy_S6KIErL7eOTqON2kEHVzdEw36KXlKU6wBXG4VTP227-XF9M5Ge8BKcS7BDkxqAzgrfNBxKsCZhXOlsiGlCe-SshPg43KJ1SmrpXrJa3ow~~", + "cmid": 0, + "type": 3, + "body": { + "extend": "", + "text": "我们感谢您的投递,但您的专业技能与我们目前的职位需求并不完全吻合。祝您在BOSS直聘找到更适合您的工作!", + "type": 1, + "templateId": 1, + "headTitle": "" + }, + "offline": false, + "pushSound": 0, + "from": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "time": 1770081189000, + "pushText": "韩先生:我们感谢您的投递,但您的专业技能与我们目前的职位需求并不完全吻合。祝您在BOSS直聘找到更适合您的工作!", + "taskId": 0, + "status": 2 + }, + { + "uncount": 0, + "flag": 0, + "mid": 307012846076929, + "received": true, + "securityId": "HOZrJc7V5lAgt-I1xBVW4AIzOdmJyVezMBCstpqbYRgw8N23mSYYlslQXE15U9gbK7-EBHJcUbnhQSRF6VIrp4rZlOUgB2cMB9qz09C4DXUYv7SVELEU8i89_0YZqXlDdMq8pg~~", + "cmid": 4611687788520895950, + "type": 1, + "body": { + "extend": "", + "text": "好的,谢谢您的时间!", + "type": 1, + "templateId": 1, + "headTitle": "" + }, + "offline": false, + "pushSound": 0, + "from": { + "uid": 546224865, + "headImg": 15, + "name": "肖伟民", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png", + "source": 0, + "certification": 0 + }, + "to": { + "uid": 631237132, + "headImg": 7, + "name": "韩先生", + "company": "", + "avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png", + "source": 0, + "certification": 0 + }, + "time": 1770093508000, + "pushText": "肖伟民:好的,谢谢您的时间!", + "taskId": 0, + "status": 1 + } +] \ No newline at end of file diff --git a/api/middleware/job/managers/chatManager.js b/api/middleware/job/managers/chatManager.js index 0be62c9..46367e7 100644 --- a/api/middleware/job/managers/chatManager.js +++ b/api/middleware/job/managers/chatManager.js @@ -1,4 +1,5 @@ const ai_service = require('../../../services/ai_service'); +const db = require('../../dbProxy'); /** * 聊天管理模块 @@ -63,13 +64,13 @@ class ChatManager { // 存储数据库 - + console.log(`[聊天管理] 成功获取聊天列表,共 ${parsed.friendList.length} 个联系人`); return parsed; } - + /** * 解析 get_chat_detail 设备端返回格式 * 格式: { type, code, message, data: { success, apiData: { response: { zpData } }, getBossData: { response: { zpData } } } } @@ -195,21 +196,63 @@ class ChatManager { return response; } + /** 是否为系统/模板消息(竞争者PK、拒绝模板、系统卡片等),不参与回复判断 */ + _isSystemMessage(msg) { + const body = msg.body || {}; + if (msg.bizType === 317 || msg.bizType === 21050003) return true; + if (msg.type === 4) return true; + if (body.type === 16) return true; + return false; + } + + /** 过滤出 HR 发的、非系统、可回复的消息列表(已排除自己发的) */ + _filterHrReplyableMessages(messages, geek_uid) { + if (!geek_uid || !Array.isArray(messages)) return []; + let list = messages.filter(msg => { + if (!msg.from || msg.from.uid === geek_uid) return false; + if (this._isSystemMessage(msg)) return false; + return true; + }); + + return list + + } + + /** AI 回复后写入 chat_reply_intent_log,options 含 sn_code/platform/friendId/encryptFriendId 时落库 */ + _saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, replied, reason) { + if (!options || options.sn_code == null) return; + try { + const model = db.getModel('chat_reply_intent_log'); + model.create({ + sn_code: options.sn_code || '', + platform: options.platform || 'boss', + friendId: options.friendId ?? null, + encrypt_friend_id: options.encryptFriendId || '', + hr_message_text: hr_message_text || null, + action: action || '', + reply_content: reply_content || null, + replied: !!replied, + reason: reason || null, + job_name: (jobInfo && (jobInfo.jobName || jobInfo.title)) || null, + create_time: new Date() + }).catch(e => console.warn('[聊天管理] 写入 chat_reply_intent_log 失败:', e.message)); + } catch (e) { + console.warn('[聊天管理] 写入 chat_reply_intent_log 失败:', e.message); + } + } + /** - * 根据沟通详情(get_chat_detail 的解析结果)判断是否需回复,并用 AI 生成回复文案 - * 供任务层在「获取详情」指令执行后调用,不包含发送消息(由任务层再下发 send_chat_message 指令) - * + * 根据沟通详情判断是否需回复,并用 AI 判断意图(文字回复 / 发简历)及生成内容 * @param {object} detail - 沟通详情,含 variant、messages、job 等 - * @returns {Promise} { replied: true, reply_content, hr_message_text } | { replied: false, reason } + * @param {object} options - 可选,落库用 { sn_code, platform, friendId, encryptFriendId } + * @returns {Promise} { replied, action?, reply_content?, hr_message_text?, reason? } */ - async getReplyContentFromDetail(detail) { + async getReplyContentFromDetail(detail, options) { if (!detail || detail.variant !== 'messages' || !Array.isArray(detail.messages) || detail.messages.length === 0) { return { replied: false, reason: '无可用消息' }; } const messages = detail.messages; - - // 推断 HR 与 求职者 uid let hr_uid = null; let geek_uid = null; @@ -217,58 +260,62 @@ class ChatManager { const body = msg.body || {}; const jobDesc = body.jobDesc || body.job_desc || null; if (jobDesc) { - if (jobDesc.boss && jobDesc.boss.uid && !hr_uid) { - hr_uid = jobDesc.boss.uid; - } - if (jobDesc.geek && jobDesc.geek.uid && !geek_uid) { - geek_uid = jobDesc.geek.uid; - } + if (jobDesc.boss && jobDesc.boss.uid && !hr_uid) hr_uid = jobDesc.boss.uid; + if (jobDesc.geek && jobDesc.geek.uid && !geek_uid) geek_uid = jobDesc.geek.uid; } if (hr_uid && geek_uid) break; } - const last = messages[messages.length - 1]; - - // 兜底:还没有 hr_uid 时,用最后一条的 from/to 做简单推断 - if ((!hr_uid || !geek_uid) && last && last.from && last.to) { - hr_uid = hr_uid || last.from.uid; - geek_uid = geek_uid || last.to.uid; + const lastRaw = messages[messages.length - 1]; + if (lastRaw && lastRaw.from && lastRaw.to) { + hr_uid = hr_uid || lastRaw.from.uid; + geek_uid = geek_uid || lastRaw.to.uid; } - if (!last || !last.from || !hr_uid || last.from.uid !== hr_uid) { - // 最后一条不是 HR 发的,不自动回复 - return { replied: false, reason: '最后一条不是HR消息' }; + const jobInfo = detail.job || {}; + + const hrList = this._filterHrReplyableMessages(messages, geek_uid); + if (hrList.length === 0) { + this._saveReplyIntentLog(options, '', jobInfo, '', '', false, '无HR可回复消息(已过滤系统与己方)'); + return { replied: false, reason: '无HR可回复消息(已过滤系统与己方)' }; + } + + const last = hrList[hrList.length - 1]; + if (!last.from || last.from.uid !== hr_uid) { + this._saveReplyIntentLog(options, '', jobInfo, '', '', false, '最后一条可回复消息不是HR'); + return { replied: false, reason: '最后一条可回复消息不是HR' }; } - // 取 HR 文本内容(普通文本优先) const body = last.body || {}; const hr_message_text = (typeof body.text === 'string' && body.text) || (typeof last.pushText === 'string' && last.pushText) || ''; - if (!hr_message_text || !hr_message_text.trim()) { - return { replied: false, reason: 'HR消息没有可用文本' }; - } - // 3. 调用阿里云 Qwen 生成回复文案(已在 config 中切换为 qwen-plus) - const jobInfo = detail.job || {}; - - const reply_content = await ai_service.generateChatContent({ + const { action, reply_content } = await ai_service.replyIntentAndContent({ jobInfo, - resumeInfo: null, - chatType: 'reply', hrMessage: hr_message_text, - previousMessages: [] // 如需上下文,这里可以把 detail.messages 映射进去 + previousMessages: hrList.slice(-5).map(m => (m.body && m.body.text) || m.pushText || '') }); - if (!reply_content || !reply_content.trim()) { - return { replied: false, reason: 'AI 未生成有效回复' }; + + if (action === 'no_reply') { + this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, false, 'HR表示暂不匹配/无需回复'); + return { replied: false, reason: 'HR表示暂不匹配/无需回复' }; } + const needContent = action === 'text'; + if (needContent && (!reply_content || !reply_content.trim())) { + this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, false, 'AI 未生成有效回复文案'); + return { replied: false, reason: 'AI 未生成有效回复文案' }; + } + + this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, true, null); return { replied: true, - reply_content, + action: action || 'text', + reply_content: reply_content || '', hr_message_text }; } @@ -287,12 +334,25 @@ class ChatManager { if (!friendId) throw new Error('friendId 不能为空'); const parsed = await this.get_chat_detail(sn_code, mqttClient, { platform, ...detailParams }); - const decision = await this.getReplyContentFromDetail(parsed); + const decision = await this.getReplyContentFromDetail(parsed, { + sn_code, + platform, + friendId, + encryptFriendId: detailParams.encryptFriendId || '' + }); if (!decision.replied) return decision; + const action = decision.action || 'text'; + const content = decision.reply_content || ''; + const actionMessages = { + send_resume: [{ type: 'send_resume', content }], + exchange_wechat: [{ type: 'exchange_wechat', content }], + exchange_phone: [{ type: 'exchange_phone', content }] + }; + const messages = actionMessages[action] || [{ type: 'text', content }]; await this.send_chat_message(sn_code, mqttClient, { friendId, - messages: [{ type: 'text', content: decision.reply_content }], + messages, chatType: 'reply', platform }); diff --git a/api/middleware/schedule/handlers/chatHandler.js b/api/middleware/schedule/handlers/chatHandler.js index 3a724d7..5d757b1 100644 --- a/api/middleware/schedule/handlers/chatHandler.js +++ b/api/middleware/schedule/handlers/chatHandler.js @@ -189,16 +189,30 @@ class ChatHandler extends BaseHandler { await this._saveChatMessagesToDb(parsed, friend, sn_code, platform_type); - const decision = await chatManager.getReplyContentFromDetail(parsed || {}); + const decision = await chatManager.getReplyContentFromDetail(parsed || {}, { + sn_code, + platform: platform_type, + friendId: friend_id, + encryptFriendId: friend.encryptFriendId || '' + }); - if (decision.replied && decision.reply_content) { + if (decision.replied) { + const action = decision.action || 'text'; + const content = decision.reply_content || ''; + const actionMessages = { + send_resume: [{ type: 'send_resume', content }], + exchange_wechat: [{ type: 'exchange_wechat', content }], + exchange_phone: [{ type: 'exchange_phone', content }] + }; + const messages = actionMessages[action] || [{ type: 'text', content }]; + const actionNames = { send_resume: '发送简历', exchange_wechat: '换微信', exchange_phone: '换电话' }; const send_command = { command_type: 'send_chat_message', - command_name: '发送聊天消息', + command_name: actionNames[action] || '发送聊天消息', command_params: { platform: platform_type, friendId: friend_id, - messages: [{ type: 'text', content: decision.reply_content }], + messages, chatType: 'reply' }, priority: config.getTaskPriority('auto_chat') || 6 diff --git a/api/model/chat_reply_intent_log.js b/api/model/chat_reply_intent_log.js new file mode 100644 index 0000000..2ccd470 --- /dev/null +++ b/api/model/chat_reply_intent_log.js @@ -0,0 +1,81 @@ +const Sequelize = require('sequelize'); + +/** + * 沟通回复意图 AI 调用记录 + * 记录 getReplyContentFromDetail 中 replyIntentAndContent 的入参与结果,便于排查与统计 + */ +module.exports = (db) => { + const chat_reply_intent_log = db.define('chat_reply_intent_log', { + sn_code: { + comment: '设备SN码', + type: Sequelize.STRING(50), + allowNull: true, + defaultValue: '' + }, + platform: { + comment: '平台: boss / liepin', + type: Sequelize.STRING(20), + allowNull: true, + defaultValue: 'boss' + }, + friendId: { + comment: '好友/会话ID', + type: Sequelize.BIGINT, + allowNull: true + }, + encrypt_friend_id: { + comment: '好友加密ID', + type: Sequelize.STRING(100), + allowNull: true, + defaultValue: '' + }, + hr_message_text: { + comment: 'HR 最新消息原文(AI 入参)', + type: Sequelize.TEXT, + allowNull: true + }, + action: { + comment: 'AI 返回意图: no_reply/text/send_resume/exchange_wechat/exchange_phone', + type: Sequelize.STRING(30), + allowNull: true, + defaultValue: '' + }, + reply_content: { + comment: 'AI 返回的回复文案', + type: Sequelize.TEXT, + allowNull: true + }, + replied: { + comment: '是否执行了回复', + type: Sequelize.BOOLEAN, + allowNull: true, + defaultValue: false + }, + reason: { + comment: '未回复时的原因', + type: Sequelize.STRING(200), + allowNull: true + }, + job_name: { + comment: '职位名称(便于排查)', + type: Sequelize.STRING(200), + allowNull: true + }, + create_time: { + comment: '创建时间', + type: Sequelize.DATE, + allowNull: true, + defaultValue: Sequelize.NOW + } + }, { + timestamps: false, + indexes: [ + { unique: false, fields: ['sn_code', 'platform', 'friendId'] }, + { unique: false, fields: ['create_time'] } + ] + }); + + // chat_reply_intent_log.sync({ force: true }); + + return chat_reply_intent_log; +}; diff --git a/api/services/ai_service.js b/api/services/ai_service.js index 0ae95cc..bdf5829 100644 --- a/api/services/ai_service.js +++ b/api/services/ai_service.js @@ -253,6 +253,65 @@ class aiService { return result; } + /** + * 根据 HR 消息判断回复意图并生成内容 + * @param {object} params - { jobInfo, hrMessage, previousMessages? } + * @returns {Promise<{ action: 'text'|'send_resume'|'exchange_wechat'|'exchange_phone', reply_content: string }>} + */ + async replyIntentAndContent(params) { + const { jobInfo = {}, hrMessage = '', previousMessages = [] } = params; + const jobName = jobInfo.jobName || jobInfo.title || '未知职位'; + const companyName = jobInfo.brandName || jobInfo.companyName || '未知公司'; + + const prompt = ` +你正在处理 BOSS 直聘上的求职沟通。根据 HR 最新消息判断求职者应采取的回复动作。 + +【职位】${jobName} +【公司】${companyName} + +【HR 最新消息】 +${hrMessage || '(HR 未发文字,仅存在职位卡片等)'} + +请严格按以下 JSON 格式返回(不要包含其他说明或换行): +{"action":"动作","reply_content":"内容"} + +action 仅允许以下五种之一: +- no_reply:不需要回复(HR 明确表示暂不匹配、感谢关注、有合适机会再沟通、婉拒、不招了等,无需求职者再回复) +- text:仅文字回复(普通聊天、打招呼、问是否考虑机会等) +- send_resume:发简历(HR 要求发简历、看简历、投递等) +- exchange_wechat:换微信(HR 要求加微信、留微信、发微信等) +- exchange_phone:换电话(HR 要求留电话、发电话、联系方式等) + +规则: +1. 若 HR 明确表示暂不匹配、感谢关注、有合适机会再沟通、与岗位不够匹配、婉拒、不考虑、不招了、已招到 等 → action 为 no_reply,reply_content 留空。 +2. 若 HR 明确要求发简历/投递/看简历 → action 为 send_resume,reply_content 可为简短附言或空。 +3. 若 HR 明确要求加微信/留微信/发微信 → action 为 exchange_wechat,reply_content 可为简短附言或空。 +4. 若 HR 明确要求留电话/发电话/联系方式 → action 为 exchange_phone,reply_content 可为简短附言或空。 +5. 若仅为普通聊天、打招呼 → action 为 text,reply_content 为一句自然回复(50字以内)。 +6. reply_content 必须为字符串,不要换行。 + `.trim(); + + const result = await this.callAPI(prompt, { + systemPrompt: '你是求职沟通助手。根据 HR 消息判断动作:no_reply(不需要回复)、text(仅文字)、send_resume(发简历)、exchange_wechat(换微信)、exchange_phone(换电话)。HR 婉拒/暂不匹配/感谢关注时用 no_reply。输出 JSON:{"action":"上述五选一","reply_content":"..."}。只返回合法 JSON。', + temperature: 0.3, + maxTokens: 500, + business_type: 'chat_reply_intent', + service_type: 'completion' + }); + + const raw = (result && result.content) ? result.content.trim() : ''; + const allowed = ['no_reply', 'text', 'send_resume', 'exchange_wechat', 'exchange_phone']; + try { + const jsonMatch = raw.match(/\{[\s\S]*\}/); + const parsed = jsonMatch ? JSON.parse(jsonMatch[0]) : {}; + const action = allowed.includes(parsed.action) ? parsed.action : 'text'; + const reply_content = typeof parsed.reply_content === 'string' ? parsed.reply_content.trim() : ''; + return { action, reply_content }; + } catch (e) { + return { action: 'text', reply_content: raw || '收到,谢谢您。' }; + } + } + /** * 分析简历要素 * @param {string} resumeText - 简历文本内容