├── www └── static │ ├── css │ └── .gitkeep │ ├── js │ └── .gitkeep │ └── image │ └── .gitkeep ├── src ├── bootstrap │ ├── master.js │ ├── worker.js │ └── load.js ├── config │ ├── router.js │ ├── config.production.js │ ├── extend.js │ ├── crontab.js │ ├── config.js │ ├── middleware.js │ └── adapter.js ├── model │ ├── index.js │ ├── wechat.js │ ├── message.js │ └── task.js ├── .DS_Store ├── logic │ └── index.js ├── controller │ ├── base.js │ ├── chat.js │ ├── logs.js │ ├── test.js │ ├── roles.js │ ├── auth.js │ ├── system.js │ ├── admin.js │ ├── account.js │ ├── task.js │ ├── dashboard.js │ ├── friends.js │ ├── wechat.js │ ├── websocket.js │ └── message.text ├── bootstrap.js └── service │ ├── token.js │ ├── messageSender.js │ ├── wechat.js │ ├── websocket.js │ ├── base.js │ ├── taskScheduler.js │ └── ai.js ├── .eslintrc ├── .DS_Store ├── test └── index.js ├── production.js ├── development.js ├── pm2.json ├── .gitignore ├── nginx.conf ├── package.json ├── view └── index_index.html ├── README.md └── ai_wechat.sql /www/static/css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/static/js/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/static/image/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/bootstrap/master.js: -------------------------------------------------------------------------------- 1 | // invoked in master 2 | -------------------------------------------------------------------------------- /src/config/router.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 3 | ]; 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "*": "off" 4 | } 5 | } -------------------------------------------------------------------------------- /src/bootstrap/worker.js: -------------------------------------------------------------------------------- 1 | const load = require('./load.js') 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lofteryang/ai-wechat-api/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/model/index.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Model { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lofteryang/ai-wechat-api/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/logic/index.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Logic { 2 | indexAction() { 3 | 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/controller/base.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Controller { 2 | __before() { 3 | 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/config/config.production.js: -------------------------------------------------------------------------------- 1 | // production config, it will load in production enviroment 2 | module.exports = { 3 | workers: 2, 4 | } 5 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const path = require('path'); 3 | require(path.join(process.cwd(), 'production.js')); 4 | 5 | test('first test', t => { 6 | const indexModel = think.model('index'); 7 | }) 8 | -------------------------------------------------------------------------------- /src/controller/chat.js: -------------------------------------------------------------------------------- 1 | const AIService = require('../service/ai.js') 2 | const Base = require('./base.js') 3 | 4 | module.exports = class extends Base { 5 | async indexAction() { 6 | let test = await think.service('ai').test() 7 | 8 | return this.success(test) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /production.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Application = require('thinkjs'); 3 | 4 | const instance = new Application({ 5 | ROOT_PATH: __dirname, 6 | APP_PATH: path.join(__dirname, 'src'), 7 | proxy: true, // use proxy 8 | env: 'production' 9 | }); 10 | 11 | instance.run(); 12 | -------------------------------------------------------------------------------- /development.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Application = require('thinkjs'); 3 | const watcher = require('think-watcher'); 4 | 5 | const instance = new Application({ 6 | ROOT_PATH: __dirname, 7 | APP_PATH: path.join(__dirname, 'src'), 8 | watcher: watcher, 9 | env: 'development' 10 | }); 11 | 12 | instance.run(); 13 | -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [{ 3 | "name": "ai-wechat-api", 4 | "script": "production.js", 5 | "cwd": "/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api", 6 | "exec_mode": "fork", 7 | "max_memory_restart": "1G", 8 | "autorestart": true, 9 | "node_args": [], 10 | "args": [], 11 | "env": { 12 | 13 | } 14 | }] 15 | } 16 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | console.log('Bootstrap loaded successfully.') 3 | console.log('应用启动完成后初始化任务调度') 4 | app.on('appReady', async () => { 5 | try { 6 | const scheduler = think.service('taskScheduler') 7 | await scheduler.init() 8 | think.logger.info('定时任务初始化完成') 9 | } catch (e) { 10 | think.logger.error('定时任务初始化失败:', e) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/model/wechat.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Model { 2 | get tableName() { 3 | return 'ai_wechat_account' 4 | } 5 | async loginOutUpdate(key) { 6 | await this.where({ auth_key: key }).update({ 7 | online: false, 8 | onlineTime: null, 9 | expiryTime: null, 10 | loginErrMsg: null, 11 | onlineDays: null, 12 | loginTime: null, 13 | totalOnline: null, 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/config/extend.js: -------------------------------------------------------------------------------- 1 | const view = require('think-view') 2 | const model = require('think-model') 3 | const cache = require('think-cache') 4 | const session = require('think-session') 5 | const moment = require('moment-timezone') 6 | module.exports = { 7 | datetime(date) { 8 | return moment(date).tz('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss') 9 | }, 10 | } 11 | module.exports = [ 12 | view, // make application support view 13 | model(think.app), 14 | cache, 15 | session, 16 | ] 17 | -------------------------------------------------------------------------------- /src/config/crontab.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = [ 4 | { 5 | immediate: false, 6 | enable: true, 7 | interval: '10000', 8 | handle: 'websocket/checkWsActive', 9 | type: 'all', 10 | }, 11 | { 12 | //每天凌晨12:30 检查是否有用户会员到期 13 | cron: '0 32 0 * * * ', 14 | enable: true, 15 | immediate: false, 16 | handle: 'friends/checkUserVipTime', 17 | }, 18 | 19 | { 20 | //每24小时检查一次 21 | cron: '0 0 0/24 * * * ', 22 | enable: true, 23 | immediate: false, 24 | handle: 'friends/checkAboutExpir', 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /src/bootstrap/load.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | // const { app } = require('thinkjs') 3 | console.log('LOAD.JS 已加载') 4 | // 初始化应用 5 | 6 | // 添加 appReady 事件监听器 7 | think.app.on('appReady', async () => { 8 | try { 9 | if (think.config('isDebuger')) { 10 | think.logger.info('当前环境为开发环境,跳过定时任务初始化') 11 | return 12 | } 13 | const scheduler = think.service('taskScheduler') 14 | await scheduler.init() 15 | const taskModel = think.model('task') 16 | taskModel.schedulerEventEmitter = scheduler.eventEmitter 17 | 18 | think.logger.info('定时任务初始化完成') 19 | } catch (e) { 20 | think.logger.error('定时任务初始化失败:', e) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage/ 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Dependency directory 23 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 24 | node_modules/ 25 | 26 | # IDE config 27 | .idea 28 | 29 | # output 30 | output/ 31 | output.tar.gz 32 | 33 | runtime/ 34 | app/ 35 | 36 | config.development.js 37 | adapter.development.js 38 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | // default config 2 | module.exports = { 3 | port: 8375, 4 | isDebuger: true, 5 | workers: 1, 6 | 7 | wechatpadpro: { 8 | adminKey: 'wanboai', 9 | hostUrl: 'http://wechatpadpro.xunruijie.com:8059', 10 | // hostUrl: 'http://127.0.0.1:8059', 11 | wsUrl: 'ws://wechatpadpro.xunruijie.com:8059/ws/GetSyncMsg?key=', 12 | genAuthKey1: '/admin/GenAuthKey1', 13 | GetLoginQrCodeNew: '/login/GetLoginQrCodeNew', 14 | CheckLoginStatus: '/login/CheckLoginStatus', 15 | GetFriendList: '/friend/GetFriendList', 16 | GetContactList: '/friend/GetContactList', //获取全部联系人 17 | //Message 18 | SendTextMessage: '/message/SendTextMessage', 19 | GetLoginStatus: '/login/GetLoginStatus', 20 | LogOut: '/login/LogOut', 21 | GetProfile: '/user/GetProfile', 22 | GetMsgBigImg: '/message/GetMsgBigImg', //获取图片(高清图片下载) 23 | 24 | //admin 25 | DelayAuthKey: '/admin/DelayAuthKey', 26 | DeleteAuthKey: '/admin/DeleteAuthKey', 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /src/controller/logs.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base') 2 | 3 | module.exports = class extends Base { 4 | /** 5 | * 获取任务列表 6 | */ 7 | async indexAction() { 8 | const page = this.get('page') || 1 9 | const size = this.get('size') || 10 10 | const msg = this.get('msg') || '' 11 | const text = this.get('text') || '' 12 | const reson = this.get('reson') || '' 13 | const reply = this.get('reply') || '' 14 | 15 | let queryMap = {} 16 | if (msg) { 17 | queryMap.msg = ['like', `%${msg}%`] 18 | } 19 | if (text) { 20 | queryMap.text = ['like', `%${text}%`] 21 | } 22 | if (reson) { 23 | queryMap.reson = ['like', `%${reson}%`] 24 | } 25 | if (reply) { 26 | queryMap.reply = ['like', `%${reply}%`] 27 | } 28 | 29 | const model = this.model('logs') 30 | const list = await model 31 | .order('id desc') 32 | .where(queryMap) 33 | .page(page, size) 34 | .countSelect() 35 | return this.success(list) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name example.com www.example.com; 4 | root /Users/Victor/Desktop/ai-wechat-api/ai-wechat-api; 5 | set $node_port 8360; 6 | 7 | index index.js index.html index.htm; 8 | if ( -f $request_filename/index.html ){ 9 | rewrite (.*) $1/index.html break; 10 | } 11 | if ( !-f $request_filename ){ 12 | rewrite (.*) /index.js; 13 | } 14 | location = /index.js { 15 | proxy_http_version 1.1; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | proxy_set_header Host $http_host; 19 | proxy_set_header X-NginX-Proxy true; 20 | proxy_set_header Upgrade $http_upgrade; 21 | proxy_set_header Connection "upgrade"; 22 | proxy_pass http://127.0.0.1:$node_port$request_uri; 23 | proxy_redirect off; 24 | } 25 | 26 | location ~ /static/ { 27 | etag on; 28 | expires max; 29 | } 30 | } -------------------------------------------------------------------------------- /src/controller/test.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base') 2 | const schedule = require('node-schedule') 3 | const moment = require('moment') // 添加moment处理时间 4 | 5 | // const cron = require('cron') 6 | module.exports = class extends Base { 7 | async testAction() { 8 | let start = '2025-07-24 09:00:00' 9 | let end = '' 10 | return this.success(this.isNowInRange(start, end)) 11 | } 12 | 13 | isNowInRange(start, end) { 14 | const now = moment() 15 | const startDate = start ? moment(start) : null 16 | const endDate = end ? moment(end) : null 17 | 18 | // 处理纯日期格式(无时间部分) 19 | if (endDate && end.length === 10) { 20 | endDate.endOf('day') // 设置为当天的最后一毫秒 21 | } 22 | 23 | // 已超过结束时间 → 返回2(已失效) 24 | if (endDate && now.isAfter(endDate)) { 25 | return 2 26 | } 27 | 28 | // 在有效范围内 → 返回1 29 | if ( 30 | (!startDate || now.isSameOrAfter(startDate)) && 31 | (!endDate || now.isSameOrBefore(endDate)) 32 | ) { 33 | return 1 34 | } 35 | 36 | // 早于开始时间 → 返回0(尚未生效) 37 | return 0 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/config/middleware.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const isDev = think.env === 'development' 3 | const bootstrap = require('../bootstrap') 4 | const kcors = require('kcors') 5 | 6 | module.exports = [ 7 | { 8 | handle: kcors, 9 | options: { 10 | origin: '*', 11 | allowMethods: '*', 12 | allowHeaders: '*', 13 | credentials: true, 14 | maxAge: 86400, // 24小时 15 | }, // 可根据需求自定义配置 16 | }, 17 | 18 | { 19 | handle: 'meta', 20 | options: { 21 | logRequest: isDev, 22 | sendResponseTime: isDev, 23 | }, 24 | }, 25 | { 26 | handle: 'resource', 27 | enable: isDev, 28 | options: { 29 | root: path.join(think.ROOT_PATH, 'www'), 30 | publicPath: /^\/(static|favicon\.ico)/, 31 | }, 32 | }, 33 | { 34 | handle: 'trace', 35 | enable: !think.isCli, 36 | options: { 37 | debug: isDev, 38 | }, 39 | }, 40 | { 41 | handle: 'payload', 42 | options: { 43 | keepExtensions: true, 44 | limit: '5mb', 45 | }, 46 | }, 47 | { 48 | handle: 'router', 49 | options: {}, 50 | }, 51 | 'logic', 52 | 'controller', 53 | ] 54 | -------------------------------------------------------------------------------- /src/service/token.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const secret = 'SLDLKKDS323ssdd@#@@gf' 3 | 4 | const rp = require('request-promise') 5 | const http = require('http') 6 | 7 | module.exports = class extends think.Service { 8 | /** 9 | * 根据header中的X-Nideshop-Token值获取用户id 10 | */ 11 | async getUserId() { 12 | const token = think.token 13 | if (!token) { 14 | return 0 15 | } 16 | 17 | const result = await this.parse() 18 | if (think.isEmpty(result) || result.user_id <= 0) { 19 | return 0 20 | } 21 | 22 | return result.user_id 23 | } 24 | 25 | /** 26 | * 根据值获取用户信息 27 | */ 28 | async getUserInfo() { 29 | const userId = await this.getUserId() 30 | if (userId <= 0) { 31 | return null 32 | } 33 | 34 | const userInfo = await this.model('admin').where({ id: userId }).find() 35 | 36 | return think.isEmpty(userInfo) ? null : userInfo 37 | } 38 | 39 | async create(userInfo) { 40 | const token = jwt.sign(userInfo, secret) 41 | return token 42 | } 43 | 44 | async parse() { 45 | if (think.token) { 46 | try { 47 | return jwt.verify(think.token, secret) 48 | } catch (err) { 49 | return null 50 | } 51 | } 52 | return null 53 | } 54 | 55 | async verify() { 56 | const result = await this.parse() 57 | if (think.isEmpty(result)) { 58 | return false 59 | } 60 | 61 | return true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/model/message.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Model { 2 | get tableName() { 3 | return 'ai_messages' 4 | } 5 | 6 | async getMasterHistoryChatRecord(masterWxid) { 7 | const startTime = new Date(Date.now() - 24 * 60 * 60 * 1000) 8 | .toISOString() 9 | .slice(0, 19) 10 | .replace('T', ' ') 11 | const messages = await this.model('messages').query(` 12 | SELECT * 13 | FROM ai_messages 14 | WHERE ( 15 | from_user = '${masterWxid}' OR 16 | to_user = '${masterWxid}' 17 | ) 18 | AND msg_type = 1 19 | AND create_at >= '${startTime}' 20 | ORDER BY create_time ASC 21 | `) 22 | return messages 23 | } 24 | 25 | async getHistoryChatRecord(sender, receiver, day) { 26 | if (day) { 27 | day = parseInt(day) 28 | } else { 29 | day = (await this.model('system').where({ key: 'hisMsgDay' }).find()) 30 | .value 31 | } 32 | const startTime = new Date(Date.now() - day * 60 * 60 * 1000) 33 | .toISOString() 34 | .slice(0, 19) 35 | .replace('T', ' ') 36 | 37 | const messages = await this.model('messages').query(` 38 | SELECT * 39 | FROM ai_messages 40 | WHERE ( 41 | (from_user = '${sender}' AND to_user = '${receiver}') 42 | OR (from_user = '${receiver}' AND to_user = '${sender}') 43 | ) 44 | AND msg_type = 1 45 | AND create_at >= '${startTime}' 46 | ORDER BY create_time ASC 47 | `) 48 | return messages 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-wechat-api", 3 | "description": "application created by thinkjs", 4 | "version": "1.0.0", 5 | "author": "Mac Studio <2206749767@qq.com>", 6 | "scripts": { 7 | "start": "node development.js", 8 | "test": "THINK_UNIT_TEST=1 nyc ava test/ && nyc report --reporter=html", 9 | "lint": "eslint src/", 10 | "lint-fix": "eslint --fix src/" 11 | }, 12 | "dependencies": { 13 | "@koa/router": "^13.1.1", 14 | "cron": "^4.3.1", 15 | "events": "^3.3.0", 16 | "jsonwebtoken": "^9.0.2", 17 | "kcors": "^2.2.2", 18 | "md5": "^2.3.0", 19 | "node-schedule": "^2.1.1", 20 | "request": "^2.88.2", 21 | "request-promise": "^4.2.6", 22 | "think-cache": "^1.0.0", 23 | "think-cache-file": "^1.0.8", 24 | "think-logger3": "^1.0.0", 25 | "think-model": "^1.5.4", 26 | "think-model-mysql": "^1.0.0", 27 | "think-session": "^1.0.0", 28 | "think-session-file": "^1.0.5", 29 | "think-view": "^1.0.0", 30 | "think-view-nunjucks": "^1.0.1", 31 | "thinkjs": "^3.2.15", 32 | "ws": "^8.18.3" 33 | }, 34 | "devDependencies": { 35 | "ava": "^0.18.0", 36 | "eslint": "^4.2.0", 37 | "eslint-config-think": "^1.0.0", 38 | "nyc": "^7.0.0", 39 | "think-watcher": "^3.0.0" 40 | }, 41 | "repository": "", 42 | "license": "MIT", 43 | "engines": { 44 | "node": ">=6.0.0" 45 | }, 46 | "readmeFilename": "README.md", 47 | "thinkjs": { 48 | "metadata": { 49 | "name": "ai-wechat-api", 50 | "description": "application created by thinkjs", 51 | "author": "Mac Studio <2206749767@qq.com>", 52 | "babel": false 53 | }, 54 | "projectName": "ai-wechat-api", 55 | "template": "/usr/local/lib/node_modules/think-cli/default_template", 56 | "clone": false, 57 | "isMultiModule": false 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /view/index_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | New ThinkJS Application 6 | 21 | 22 | 23 |
24 |
25 |

A New App Created By ThinkJS

26 |
27 |
28 |
29 |
30 |
31 |
1
32 |

Generate Files

33 |

Run thinkjs command to create module, controler, model, service and so on.

34 |
35 |
36 |
2
37 |

Documentation

38 |

ThinkJS has online html documents. visit https://thinkjs.org/doc.html.

39 |
40 |
41 |
3
42 |

GitHub

43 |

If you have some questions, please new a issue.

44 |
45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /src/controller/roles.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base') 2 | 3 | module.exports = class extends Base { 4 | /** 5 | * 获取任务列表 6 | */ 7 | async indexAction() { 8 | const page = this.get('page') || 1 9 | const size = this.get('size') || 10 10 | const model = this.model('roles') 11 | let queryMap = {} 12 | 13 | const list = await model.page(page, size).countSelect() 14 | return this.success(list) 15 | } 16 | 17 | async listAllAction() { 18 | const model = this.model('roles') 19 | const list = await model.select() 20 | return this.success(list) 21 | } 22 | 23 | async deleteAction() { 24 | const id = this.get('id') 25 | if (!id) { 26 | return this.fail('id不能为空') 27 | } 28 | const role = await this.model('roles').where({ id: id }).find() 29 | if (!role) { 30 | return this.fail('角色不存在') 31 | } 32 | if (role.is_system) { 33 | return this.fail('系统角色不能删除') 34 | } 35 | // const friends = await this.model('friends').where({ role_id: id }).find() 36 | // if (friends) { 37 | // return this.fail('角色下有好友,不能删除') 38 | // } 39 | await this.model('roles').where({ id }).delete() 40 | return this.success() 41 | } 42 | 43 | async editAction() { 44 | const id = this.post('id') 45 | if (!id) { 46 | return this.fail('id不能为空') 47 | } 48 | const role = await this.model('roles').where({ id }).find() 49 | if (!role) { 50 | return this.fail('角色不存在') 51 | } 52 | const name = this.post('name') 53 | if (!name) { 54 | return this.fail('name不能为空') 55 | } 56 | const prompt = this.post('prompt') 57 | if (!prompt) { 58 | return this.fail('prompt不能为空') 59 | } 60 | await this.model('roles').where({ id }).update({ 61 | name, 62 | prompt, 63 | }) 64 | return this.success() 65 | } 66 | async addAction() { 67 | const name = this.post('name') 68 | if (!name) { 69 | return this.fail('name不能为空') 70 | } 71 | const prompt = this.post('prompt') 72 | if (!prompt) { 73 | return this.fail('prompt不能为空') 74 | } 75 | await this.model('roles').add({ 76 | name, 77 | prompt, 78 | }) 79 | return this.success() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/service/messageSender.js: -------------------------------------------------------------------------------- 1 | module.exports = class { 2 | constructor() { 3 | // 实例化所需Model 4 | this.friendsTaskModel = think.model('friends_task') 5 | this.friendsModel = think.model('friends') 6 | this.wechatAccountModel = think.model('wechat_account') 7 | this.messageModel = think.model('messages') 8 | } 9 | /** 10 | * 发送消息 11 | * @param {Object} params 12 | */ 13 | async sendMessage(params) { 14 | const { wxid, content, taskId, account_id } = params 15 | 16 | let accountInfo = {} 17 | let friendInfo = {} 18 | let auth_key 19 | if (think.isEmpty(account_id)) { 20 | const taskInfo = await this.friendsTaskModel.where({ id: taskId }).find() 21 | friendInfo = await this.friendsModel 22 | .where({ id: taskInfo.friend_id }) 23 | .find() 24 | 25 | accountInfo = await this.wechatAccountModel 26 | .where({ id: friendInfo.account_id }) 27 | .find() 28 | auth_key = accountInfo.auth_key 29 | } else { 30 | accountInfo = await this.wechatAccountModel 31 | .where({ id: account_id }) 32 | .find() 33 | auth_key = ( 34 | await this.wechatAccountModel.where({ id: account_id }).find() 35 | ).auth_key 36 | friendInfo = await this.friendsModel 37 | .where({ account_id: account_id, wxid: wxid }) 38 | .find() 39 | } 40 | 41 | think.logger.info(`发送任务消息 [任务ID: ${taskId}] 给 ${wxid}: ${content}`) 42 | 43 | //文案调整 44 | 45 | if (think.config('isDebuger')) { 46 | console.log('调试模式,不发送消息,看到此消息表示已发送') 47 | return { 48 | code: 0, 49 | msg: '调试模式,不发送消息', 50 | } 51 | } 52 | 53 | let txt = await think 54 | .service('ai') 55 | .getCopyModWithAI(content, 1001, friendInfo) 56 | console.log('文案调整后的内容==>', txt) 57 | if (think.isEmpty(txt)) { 58 | return 59 | } else { 60 | // 调用微信服务发送消息 61 | const wechatService = think.service('wechat') 62 | const result = await wechatService.sendTextMessage({ 63 | key: auth_key, 64 | wxid: wxid, 65 | content: txt, 66 | }) 67 | await this.messageModel.add({ 68 | from_user: accountInfo.wx_id, 69 | to_user: wxid, 70 | content: txt, 71 | is_ai: 1, 72 | }) 73 | } 74 | 75 | return result 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/controller/auth.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js') 2 | module.exports = class extends Base { 3 | async loginWithPwAction() { 4 | const username = this.post('username') 5 | const password = this.post('password') 6 | if (!password || !username) { 7 | return this.fail(500, '非法请求') 8 | } 9 | const admin = await this.model('admin') 10 | .where({ 11 | username: username, 12 | password: password, 13 | }) 14 | .find() 15 | if (think.isEmpty(admin)) { 16 | return this.fail(401, '用户名或密码不正确!') 17 | } 18 | // 更新登录信息 19 | await this.model('admin') 20 | .where({ 21 | id: admin.id, 22 | }) 23 | .update({ 24 | last_login_time: parseInt(Date.now() / 1000), 25 | last_login_ip: this.ctx.ip, 26 | }) 27 | const TokenSerivce = this.service('token') 28 | const sessionKey = await TokenSerivce.create({ 29 | id: admin.id, 30 | }) 31 | if (think.isEmpty(sessionKey)) { 32 | return this.fail('登录失败') 33 | } 34 | const userInfo = { 35 | id: admin.id, 36 | username: admin.username, 37 | type: admin.type, 38 | password: admin.password, 39 | } 40 | return this.success({ 41 | token: sessionKey, 42 | userInfo: userInfo, 43 | }) 44 | } 45 | async loginAction() { 46 | const username = this.post('username') 47 | const password = this.post('password') 48 | const admin = await this.model('admin') 49 | .where({ 50 | username: username, 51 | }) 52 | .find() 53 | if (think.isEmpty(admin)) { 54 | return this.fail(401, '用户名或密码不正确!') 55 | } 56 | console.log(think.md5(password + '' + admin.password_salt)) 57 | console.log(admin.password) 58 | if (think.md5(password + '' + admin.password_salt) !== admin.password) { 59 | return this.fail(400, '用户名或密码不正确!!') 60 | } 61 | // 更新登录信息 62 | await this.model('admin') 63 | .where({ 64 | id: admin.id, 65 | }) 66 | .update({ 67 | last_login_time: parseInt(Date.now() / 1000), 68 | last_login_ip: this.ctx.ip, 69 | }) 70 | const TokenSerivce = think.service('token') 71 | const sessionKey = await TokenSerivce.create({ 72 | id: admin.id, 73 | }) 74 | if (think.isEmpty(sessionKey)) { 75 | return this.fail('登录失败') 76 | } 77 | const userInfo = { 78 | id: admin.id, 79 | username: admin.username, 80 | type: admin.type, 81 | password: admin.password, 82 | } 83 | return this.success({ 84 | token: sessionKey, 85 | userInfo: userInfo, 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/config/adapter.js: -------------------------------------------------------------------------------- 1 | const fileCache = require('think-cache-file') 2 | const nunjucks = require('think-view-nunjucks') 3 | const fileSession = require('think-session-file') 4 | const mysql = require('think-model-mysql') 5 | const { Console, File, DateFile } = require('think-logger3') 6 | const path = require('path') 7 | const isDev = think.env === 'development' 8 | 9 | /** 10 | * cache adapter config 11 | * @type {Object} 12 | */ 13 | exports.cache = { 14 | type: 'file', 15 | common: { 16 | timeout: 24 * 60 * 60 * 1000, // millisecond 17 | }, 18 | file: { 19 | handle: fileCache, 20 | cachePath: path.join(think.ROOT_PATH, 'runtime/cache'), // absoulte path is necessarily required 21 | pathDepth: 1, 22 | gcInterval: 24 * 60 * 60 * 1000, // gc interval 23 | }, 24 | } 25 | 26 | /** 27 | * model adapter config 28 | * @type {Object} 29 | */ 30 | exports.model = { 31 | type: 'mysql', 32 | common: { 33 | logConnect: isDev, 34 | logSql: isDev, 35 | logger: (msg) => think.logger.info(msg), 36 | }, 37 | mysql: { 38 | handle: mysql, 39 | database: 'ai_wechat', 40 | prefix: 'ai_', 41 | encoding: 'utf8', 42 | host: 'Your DB Host', 43 | port: '3306', 44 | user: 'Your DB User', 45 | password: 'Your DB Password', 46 | dateStrings: true, 47 | charset: 'utf8mb4', 48 | timezone: '+08:00', 49 | }, 50 | } 51 | 52 | /** 53 | * session adapter config 54 | * @type {Object} 55 | */ 56 | exports.session = { 57 | type: 'file', 58 | common: { 59 | cookie: { 60 | name: 'thinkjs', 61 | // keys: ['werwer', 'werwer'], 62 | // signed: true 63 | }, 64 | }, 65 | file: { 66 | handle: fileSession, 67 | sessionPath: path.join(think.ROOT_PATH, 'runtime/session'), 68 | }, 69 | } 70 | 71 | /** 72 | * view adapter config 73 | * @type {Object} 74 | */ 75 | exports.view = { 76 | type: 'nunjucks', 77 | common: { 78 | viewPath: path.join(think.ROOT_PATH, 'view'), 79 | sep: '_', 80 | extname: '.html', 81 | }, 82 | nunjucks: { 83 | handle: nunjucks, 84 | }, 85 | } 86 | 87 | /** 88 | * logger adapter config 89 | * @type {Object} 90 | */ 91 | exports.logger = { 92 | type: isDev ? 'console' : 'dateFile', 93 | console: { 94 | handle: Console, 95 | }, 96 | file: { 97 | handle: File, 98 | backups: 10, // max chunk number 99 | absolute: true, 100 | maxLogSize: 50 * 1024, // 50M 101 | filename: path.join(think.ROOT_PATH, 'logs/app.log'), 102 | }, 103 | dateFile: { 104 | handle: DateFile, 105 | level: 'ALL', 106 | absolute: true, 107 | pattern: '-yyyy-MM-dd', 108 | alwaysIncludePattern: true, 109 | filename: path.join(think.ROOT_PATH, 'logs/app.log'), 110 | }, 111 | } 112 | -------------------------------------------------------------------------------- /src/model/task.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | 3 | module.exports = class extends think.Model { 4 | get tableName() { 5 | return 'ai_friends_task' 6 | } 7 | 8 | async getActiveTasks() { 9 | let data = await this.where({ is_active: 1 }).select() 10 | 11 | for (let item of data) { 12 | let res = this.isNowInRange(item.start_time, item.end_time) 13 | if (res == 2) { 14 | // 使用实例更新确保触发钩子 15 | const task = await this.where({ id: item.id }).find() 16 | if (!think.isEmpty(task)) { 17 | task.is_active = 0 18 | await this.update(task) 19 | } 20 | } 21 | } 22 | return await this.where({ is_active: 1 }).select() 23 | } 24 | 25 | isNowInRange(start, end) { 26 | const now = moment() 27 | const startDate = start ? moment(start) : null 28 | const endDate = end ? moment(end) : null 29 | 30 | if (endDate && end.length === 10) { 31 | endDate.endOf('day') 32 | } 33 | 34 | if (endDate && now.isAfter(endDate)) { 35 | return 2 36 | } 37 | 38 | if ( 39 | (!startDate || now.isSameOrAfter(startDate)) && 40 | (!endDate || now.isSameOrBefore(endDate)) 41 | ) { 42 | return 1 43 | } 44 | return 0 45 | } 46 | 47 | async getTaskById(taskId) { 48 | return this.where({ id: taskId }).find() 49 | } 50 | 51 | async updateNextExecution(taskId, nextTime) { 52 | const task = await this.where({ id: taskId }).find() 53 | if (!think.isEmpty(task)) { 54 | task.next_execution_time = nextTime 55 | return this.update(task) 56 | } 57 | return false 58 | } 59 | 60 | async updateExecutionInfo(id, lastExec) { 61 | const task = await this.where({ id }).find() 62 | if (!think.isEmpty(task)) { 63 | task.last_execution_time = lastExec 64 | task.execution_count = (task.execution_count || 0) + 1 65 | return this.update(task) 66 | } 67 | return false 68 | } 69 | 70 | async recordExecutionError(taskId, message) { 71 | const task = await this.where({ id: taskId }).find() 72 | if (!think.isEmpty(task)) { 73 | task.last_execution_time = think.datetime(new Date()) 74 | task.error_message = message 75 | return this.update(task) 76 | } 77 | return false 78 | } 79 | 80 | // 模型钩子 81 | async afterAdd(data) { 82 | if (this.schedulerEventEmitter) { 83 | this.schedulerEventEmitter.emit('task:create', data.id) 84 | } 85 | return data 86 | } 87 | 88 | async afterUpdate(data) { 89 | if (this.schedulerEventEmitter) { 90 | this.schedulerEventEmitter.emit('task:update', data.id) 91 | } 92 | return data 93 | } 94 | 95 | async afterDelete(data) { 96 | if (this.schedulerEventEmitter) { 97 | this.schedulerEventEmitter.emit('task:delete', data.id) 98 | } 99 | return data 100 | } 101 | 102 | // 事件发射器设置 103 | set schedulerEventEmitter(emitter) { 104 | this._schedulerEventEmitter = emitter 105 | } 106 | 107 | get schedulerEventEmitter() { 108 | return this._schedulerEventEmitter 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/service/wechat.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise') 2 | const { json } = require('../controller') 3 | 4 | module.exports = class extends think.Service { 5 | async getMsgBigImg(auth_key, msgid) { 6 | let url = 7 | think.config('wechatpadpro.hostUrl') + 8 | think.config('wechatpadpro.GetMsgBigImg') + 9 | '?key=' + 10 | auth_key 11 | let body = { 12 | CompressType: 0, 13 | FromUserName: 'wxid_hebkbciz5spt22', 14 | MsgId: msgid, 15 | Section: { 16 | DataLen: 61440, 17 | StartPos: 0, 18 | }, 19 | ToUserName: 'ai374949369', 20 | TotalLen: 0, 21 | } 22 | console.log(body) 23 | const options = { 24 | method: 'POST', 25 | uri: url, 26 | headers: { 27 | 'Content-Type': 'application/json; charset=utf-8', 28 | }, 29 | body: body, 30 | json: true, 31 | } 32 | let data = await rp(options) 33 | console.log('data==》', data) 34 | // if (data.Code == '200') { 35 | // let buf = data.Data.Data.Buffer 36 | // console.log(buf) 37 | 38 | // if (buf) { 39 | // const filename = msgid + '.png' 40 | // const savePath = path.join(think.ROOT_PATH, 'runtime/test', filename) 41 | // var base64Data = buf.replace(/^data:image\/\w+;base64,/, '') 42 | // const dataBuffer = Buffer.from(base64Data, 'base64') 43 | // fs.writeFile(savePath, dataBuffer, function (err) { 44 | // if (err) { 45 | // console.log(err) 46 | // return false 47 | // } else { 48 | // console.log('写入成功') 49 | // return savePath 50 | // } 51 | // }) 52 | // } 53 | // return false 54 | // } else { 55 | // return false 56 | // } 57 | } 58 | 59 | async sendTextMessage(params) { 60 | let key, wxid, content 61 | 62 | if (params) { 63 | // 内部直接调用 64 | key = params.key 65 | wxid = params.wxid 66 | content = params.content 67 | } 68 | if (!key) { 69 | console.log('没有key') 70 | 71 | return 'key不能为空' 72 | } 73 | if (!wxid) { 74 | return 'wxid不能为空' 75 | } 76 | if (!content) { 77 | return 'content不能为空' 78 | } 79 | 80 | if (think.config('isDebuger')) { 81 | return { 82 | code: 0, 83 | msg: '调试模式,不发送消息', 84 | } 85 | } 86 | 87 | let url = 88 | think.config('wechatpadpro.hostUrl') + 89 | think.config('wechatpadpro.SendTextMessage') + 90 | '?key=' + 91 | key 92 | const options = { 93 | method: 'POST', 94 | url: url, 95 | headers: { 96 | 'Content-Type': 'application/json; charset=utf-8', 97 | }, 98 | body: JSON.stringify({ 99 | MsgItem: [ 100 | { 101 | AtWxIDList: [], 102 | ImageContent: '', 103 | MsgType: 1, 104 | TextContent: content, 105 | ToUserName: wxid, 106 | }, 107 | ], 108 | }), 109 | } 110 | let data = await rp(options) 111 | console.log('消息发送结果===》', data) 112 | data = JSON.parse(data) 113 | if (data.Code == '200') { 114 | return data 115 | } else { 116 | return false 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/controller/system.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base') 2 | 3 | module.exports = class extends Base { 4 | /** 5 | * 获取任务列表 6 | */ 7 | async indexAction() { 8 | const page = this.get('page') || 1 9 | const size = this.get('size') || 10 10 | const model = this.model('system') 11 | let queryMap = {} 12 | 13 | const list = await model.page(page, size).countSelect() 14 | return this.success(list) 15 | } 16 | 17 | async userTAction() { 18 | let userId = this.get('userId') 19 | let serve = think.service('ai') 20 | let friendInfo = await this.model('friends').where({ id: userId }).find() 21 | 22 | let res = await serve.getUserSummary('1', friendInfo) 23 | // let res = await serve.safeJsonParse('{"name":"张"三"","age":18}') 24 | 25 | return this.success(res) 26 | // return this.success(res.choices[0].message.content) 27 | } 28 | 29 | async testAction() { 30 | const host = this.post('host') 31 | if (!host) { 32 | return this.fail('host不能为空') 33 | } 34 | const key = this.post('key') 35 | if (!key) { 36 | return this.fail('key不能为空') 37 | } 38 | const model = this.post('model') 39 | if (!model) { 40 | return this.fail('model不能为空') 41 | } 42 | const prompt = this.post('prompt') 43 | if (!prompt) { 44 | return this.fail('prompt不能为空') 45 | } 46 | let info = { 47 | host, 48 | key, 49 | model, 50 | prompt, 51 | } 52 | let serve = think.service('ai') 53 | let res = await serve.getTestAIReply(info) 54 | 55 | return this.success(res.choices[0].message.content) 56 | } 57 | 58 | async deleteAction() { 59 | const id = this.get('id') 60 | if (!id) { 61 | return this.fail('id不能为空') 62 | } 63 | const role = await this.model('system').where({ id: id }).find() 64 | if (!role) { 65 | return this.fail('参数不存在') 66 | } 67 | 68 | await this.model('system').where({ id }).delete() 69 | return this.success() 70 | } 71 | 72 | async editAction() { 73 | const id = this.post('id') 74 | if (!id) { 75 | return this.fail('id不能为空') 76 | } 77 | const role = await this.model('system').where({ id }).find() 78 | if (!role) { 79 | return this.fail('参数不存在') 80 | } 81 | const key = this.post('key') 82 | if (!key) { 83 | return this.fail('key') 84 | } 85 | const value = this.post('value') 86 | if (!value) { 87 | return this.fail('value不能为空') 88 | } 89 | 90 | const desc = this.post('desc') 91 | if (!desc) { 92 | return this.fail('desc不能为空') 93 | } 94 | await this.model('system').where({ id }).update({ 95 | key, 96 | value, 97 | desc, 98 | }) 99 | return this.success() 100 | } 101 | async addAction() { 102 | const key = this.post('key') 103 | if (!key) { 104 | return this.fail('key不能为空') 105 | } 106 | const value = this.post('value') 107 | if (!value) { 108 | return this.fail('value不能为空') 109 | } 110 | const desc = this.post('desc') 111 | if (!desc) { 112 | return this.fail('desc不能为空') 113 | } 114 | await this.model('system').add({ 115 | key, 116 | value, 117 | desc, 118 | }) 119 | return this.success() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/controller/admin.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js') 2 | const moment = require('moment') 3 | const md5 = require('md5') 4 | module.exports = class extends Base { 5 | /** 6 | * index action 7 | * @return {Promise} [] 8 | */ 9 | async indexAction() { 10 | let id = this.get('id') 11 | const data = await this.model('admin') 12 | .where({ 13 | // is_show: 1, 14 | is_delete: 0, 15 | id: id, 16 | }) 17 | .select() 18 | for (const item of data) { 19 | if (item.last_login_time != 0) { 20 | item.last_login_time = moment 21 | .unix(item.last_login_time) 22 | .format('YYYY-MM-DD HH:mm:ss') 23 | } else { 24 | item.last_login_time = '还没登录过' 25 | } 26 | item.password = '' 27 | } 28 | return this.success(data) 29 | } 30 | async saleAdminListAction() { 31 | let data = await this.model('user_admin').select() 32 | return this.success(data) 33 | } 34 | async adminDetailAction() { 35 | let id = this.post('id') 36 | let info = await this.model('admin') 37 | .where({ 38 | id: id, 39 | }) 40 | .find() 41 | return this.success(info) 42 | } 43 | async adminAddAction() { 44 | let user = this.post('user') 45 | let password = user.password 46 | let upData = { 47 | username: info.username, 48 | password_salt: 'HIOLABS', 49 | } 50 | if (password.replace(/(^\s*)|(\s*$)/g, '').length != 0) { 51 | password = md5(info.password + '' + upData.password_salt) 52 | upData.password = password 53 | } 54 | await this.model('admin').add(upData) 55 | return this.success() 56 | } 57 | async adminSaveAction() { 58 | let user = this.post('user') 59 | let change = this.post('change') 60 | let upData = { 61 | username: user.username, 62 | } 63 | if (change == true) { 64 | let newPassword = user.newpassword 65 | if (newPassword.replace(/(^\s*)|(\s*$)/g, '').length != 0) { 66 | newPassword = md5(user.newpassword + '' + user.password_salt) 67 | upData.password = newPassword 68 | } 69 | } 70 | let ex = await this.model('admin') 71 | .where({ 72 | username: user.username, 73 | id: ['<>', user.id], 74 | }) 75 | .find() 76 | if (!think.isEmpty(ex)) { 77 | return this.fail(400, '重名了') 78 | } 79 | // if (user.id == 14) { 80 | // return this.fail(400, '演示版后台的管理员密码不能修改!本地开发,删除这个判断') 81 | // } 82 | await this.model('admin') 83 | .where({ 84 | id: user.id, 85 | }) 86 | .update(upData) 87 | return this.success() 88 | } 89 | async infoAction() { 90 | const id = this.get('id') 91 | const model = this.model('user') 92 | const data = await model 93 | .where({ 94 | id: id, 95 | }) 96 | .find() 97 | return this.success(data) 98 | } 99 | async storeAction() { 100 | if (!this.isPost) { 101 | return false 102 | } 103 | const values = this.post() 104 | const id = this.post('id') 105 | const model = this.model('user') 106 | values.is_show = values.is_show ? 1 : 0 107 | values.is_new = values.is_new ? 1 : 0 108 | if (id > 0) { 109 | await model 110 | .where({ 111 | id: id, 112 | }) 113 | .update(values) 114 | } else { 115 | delete values.id 116 | await model.add(values) 117 | } 118 | return this.success(values) 119 | } 120 | async deleAdminAction() { 121 | const id = this.post('id') 122 | await this.model('admin') 123 | .where({ 124 | id: id, 125 | }) 126 | .limit(1) 127 | .delete() 128 | return this.success() 129 | } 130 | async showsetAction() { 131 | const model = this.model('show_settings') 132 | let data = await model.find() 133 | return this.success(data) 134 | } 135 | async showsetStoreAction() { 136 | let id = 1 137 | const values = this.post() 138 | const model = this.model('show_settings') 139 | await model 140 | .where({ 141 | id: id, 142 | }) 143 | .update(values) 144 | return this.success(values) 145 | } 146 | async changeAutoStatusAction() { 147 | const status = this.post('status') 148 | await this.model('settings') 149 | .where({ 150 | id: 1, 151 | }) 152 | .update({ 153 | autoDelivery: status, 154 | }) 155 | return this.success() 156 | } 157 | async storeShipperSettingsAction() { 158 | const values = this.post() 159 | await this.model('settings') 160 | .where({ 161 | id: values.id, 162 | }) 163 | .update(values) 164 | return this.success() 165 | } 166 | async senderInfoAction() { 167 | let info = await this.model('settings') 168 | .where({ 169 | id: 1, 170 | }) 171 | .find() 172 | return this.success(info) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/controller/account.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js') 2 | const rp = require('request-promise') 3 | 4 | module.exports = class extends Base { 5 | async getAIReplyAction() { 6 | const id = this.post('id') 7 | const prompt = this.post('prompt') 8 | if (id > 0) { 9 | let info = await this.model('wechat_account') 10 | .where({ 11 | id: id, 12 | }) 13 | .find() 14 | 15 | if (!info.wx_id) { 16 | return this.fail() 17 | } 18 | 19 | let hismsg = await this.model('message').getMasterHistoryChatRecord( 20 | info.wx_id 21 | ) 22 | // console.log('历史消息==>', hismsg) 23 | let aiReply = await think 24 | .service('ai') 25 | .getAIReplyWithHumanSummary(prompt, info.wx_id, hismsg) 26 | return this.success(aiReply) 27 | } else { 28 | return this.fail(500, '错误,联系管理员') 29 | } 30 | } 31 | 32 | async allListAction() { 33 | let data = await this.model('wechat_account').order('id desc').select() 34 | return this.success(data) 35 | } 36 | 37 | async indexAction() { 38 | const page = this.get('page') || 1 39 | const size = this.get('size') || 10 40 | let data = await this.model('wechat_account') 41 | .order('id desc') 42 | .page(page, size) 43 | .countSelect() 44 | for (let item of data.data) { 45 | item.ws_status = item.ws_status.toString() 46 | let count = await this.model('friends') 47 | .where({ 48 | account_id: item.id, 49 | }) 50 | .count() 51 | 52 | item.friend_count = count 53 | 54 | this.model('wechat_account').update( 55 | { friend_count: count }, 56 | { where: { id: item.id } } 57 | ) 58 | try { 59 | let res = await this.refreshOnlineStatus(item.auth_key) 60 | if (res) { 61 | item.online = true 62 | item.onlineTime = res.onlineTime 63 | item.expiryTime = res.expiryTime 64 | item.loginErrMsg = res.loginErrMsg 65 | item.onlineDays = res.onlineDays 66 | item.loginTime = res.loginTime 67 | item.totalOnline = res.totalOnline 68 | } else { 69 | item.online = false 70 | item.onlineTime = null 71 | item.expiryTime = null 72 | item.loginErrMsg = null 73 | item.onlineDays = null 74 | item.loginTime = null 75 | item.totalOnline = null 76 | } 77 | } catch (error) { 78 | console.log('error==》', error) 79 | } 80 | } 81 | return this.success(data) 82 | } 83 | 84 | async refreshOnlineStatus(auth_key) { 85 | let url = 86 | this.config('wechatpadpro').hostUrl + 87 | this.config('wechatpadpro').GetLoginStatus + 88 | '?key=' + 89 | auth_key 90 | const options = { 91 | method: 'GET', 92 | url: url, 93 | headers: { 94 | 'Content-Type': 'application/json; charset=utf-8', 95 | }, 96 | } 97 | let data = await rp(options) 98 | data = JSON.parse(data) 99 | 100 | if (data.Code == '200') { 101 | let map = { 102 | onlineTime: data.Data.onlineTime, 103 | expiryTime: data.Data.expiryTime, 104 | loginErrMsg: data.Data.loginErrMsg, 105 | onlineDays: data.Data.onlineDays, 106 | loginTime: data.Data.loginTime, 107 | totalOnline: data.Data.totalOnline, 108 | } 109 | 110 | await this.model('wechat_account') 111 | .where({ auth_key: auth_key }) 112 | .update(map) 113 | return map 114 | } else { 115 | await this.model('wechat_account').where({ auth_key: auth_key }).update({ 116 | online: false, 117 | onlineTime: null, 118 | expiryTime: null, 119 | loginErrMsg: null, 120 | onlineDays: null, 121 | loginTime: null, 122 | totalOnline: null, 123 | }) 124 | return false 125 | } 126 | } 127 | 128 | async insertAction() { 129 | const { auth_key } = this.post() 130 | if (!auth_key) { 131 | return this.fail('设备码不能为空') 132 | } 133 | let data = await this.model('wechat_account').add({ 134 | auth_key, 135 | }) 136 | return this.success(data) 137 | } 138 | 139 | async updateAction() { 140 | const { auth_key, wx_id, auth_key_remaining_time, avatar, nickname } = 141 | this.post() 142 | if (!auth_key) { 143 | return this.fail('auth_key不能为空') 144 | } 145 | let data = await this.model('wechat_account') 146 | .where({ auth_key: auth_key }) 147 | .update({ 148 | wx_id, 149 | wx_id, 150 | auth_key_remaining_time, 151 | avatar, 152 | nickname, 153 | }) 154 | return this.success(data) 155 | } 156 | async deleteAction() { 157 | const { auth_key } = this.post() 158 | if (!auth_key) { 159 | return this.fail('auth_key不能为空') 160 | } 161 | let data = await this.model('wechat_account') 162 | .where({ auth_key: auth_key }) 163 | .delete() 164 | return this.success(data) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/controller/task.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base') 2 | const schedule = require('node-schedule') 3 | 4 | // const cron = require('cron') 5 | module.exports = class extends Base { 6 | /** 7 | * 获取任务列表 8 | */ 9 | async listAction() { 10 | const model = this.model('friends_task') 11 | const tasks = await model 12 | .where({ 13 | is_active: 1, 14 | }) 15 | .select() 16 | let ser = think.service('base') 17 | 18 | for (let item of tasks) { 19 | try { 20 | item.cron_expression_chinese = ser.parseCronToChinese( 21 | item.cron_expression 22 | ) 23 | } catch (e) { 24 | item.cron_expression_chinese = '中文解析失败' 25 | } 26 | } 27 | return this.success(tasks) 28 | } 29 | 30 | async testAction() { 31 | try { 32 | const cronExpression = '50 19 15 8 7 2' // 您的原始 cron 表达式 33 | 34 | // 创建临时调度任务 35 | const job = schedule.scheduleJob(cronExpression, () => {}) 36 | 37 | // 获取下次执行时间 38 | const nextTime = job.nextInvocation() 39 | job.cancel() // 立即取消这个临时任务 40 | 41 | console.log(`The job would run at: ${nextTime}`) 42 | return this.success(nextTime) 43 | } catch (e) { 44 | console.error('解析cron表达式失败:', e) 45 | return this.fail('解析cron表达式失败') 46 | } 47 | } 48 | async closeAllTaskAction() { 49 | let friend_id = this.post('friend_id') 50 | if (!friend_id) { 51 | return this.fail('friend_id 不能为空') 52 | } 53 | const model = this.model('friends_task') 54 | let res = model 55 | .where({ 56 | friend_id: friend_id, 57 | }) 58 | .update({ 59 | is_active: 0, 60 | }) 61 | return this.success(res) 62 | } 63 | 64 | async listWithFriendIdAction() { 65 | let friend_id = this.post('friend_id') 66 | if (!friend_id) { 67 | return this.fail('friend_id 不能为空') 68 | } 69 | const model = this.model('friends_task') 70 | const tasks = await model 71 | .where({ 72 | friend_id: Number(friend_id), 73 | is_active: 1, 74 | }) 75 | .order('is_active desc, create_time desc') 76 | .select() 77 | for (let task of tasks) { 78 | task.is_active = task.is_active.toString() 79 | } 80 | return this.success(tasks) 81 | } 82 | async updateTaskAction() { 83 | let task = this.post() 84 | let map = { 85 | start_date: task.start_date, 86 | end_date: task.end_date, 87 | cron_expression: task.cron_expression, 88 | task_name: task.task_name, 89 | friend_id: task.friend_id, 90 | } 91 | if (task.id) { 92 | let res = await this.model('task').where({ id: task.id }).update(map) 93 | return res 94 | } else { 95 | let res = await this.model('task').add(map) 96 | return res 97 | } 98 | } 99 | async updateTaskStatusAction() { 100 | const model = this.model('friends_task') 101 | const id = this.get('id') 102 | const is_active = this.get('is_active') 103 | if (id > 0) { 104 | let res = await model 105 | .where({ 106 | id: id, 107 | }) 108 | .update({ 109 | is_active: is_active, 110 | }) 111 | // return this.success(res) 112 | } else { 113 | return this.fail(500, '错误,联系管理员') 114 | } 115 | } 116 | /** 117 | * 创建新任务 118 | */ 119 | async createAction() { 120 | const params = this.post() 121 | const model = this.model('task') 122 | 123 | // 验证cron表达式 124 | if (!this.validateCron(params.cron_expression)) { 125 | return this.fail('无效的cron表达式') 126 | } 127 | 128 | const id = await model.add(params) 129 | 130 | // 如果任务激活,立即调度 131 | if (params.is_active === 1) { 132 | const task = await model.where({ id }).find() 133 | const scheduler = think.service('taskScheduler') 134 | scheduler.scheduleTask(task) 135 | } 136 | 137 | return this.success({ id }) 138 | } 139 | 140 | /** 141 | * 更新任务 142 | */ 143 | async updateAction() { 144 | const id = this.get('id') 145 | const params = this.post() 146 | const model = this.model('task') 147 | 148 | if (params.cron_expression && !this.validateCron(params.cron_expression)) { 149 | return this.fail('无效的cron表达式') 150 | } 151 | 152 | await model.where({ id }).update(params) 153 | 154 | // 重新加载任务 155 | const scheduler = think.service('taskScheduler') 156 | await scheduler.reloadTasks() 157 | 158 | return this.success('任务更新成功') 159 | } 160 | 161 | /** 162 | * 删除任务 163 | */ 164 | async deleteAction() { 165 | const id = this.get('id') 166 | await this.model('task').where({ id }).delete() 167 | 168 | // 取消任务调度 169 | const scheduler = think.service('taskScheduler') 170 | scheduler.scheduledTasks.get(id)?.stop() 171 | scheduler.scheduledTasks.delete(id) 172 | 173 | return this.success('任务删除成功') 174 | } 175 | 176 | /** 177 | * 重新加载所有任务 178 | */ 179 | async reloadAction() { 180 | const scheduler = think.service('taskScheduler') 181 | await scheduler.reloadTasks() 182 | return this.success('任务已重新加载') 183 | } 184 | 185 | /** 186 | * 验证cron表达式 187 | */ 188 | validateCron(expression) { 189 | try { 190 | return true 191 | // const cron = require('node-cron') 192 | // return cron.validate(expression) 193 | } catch (e) { 194 | return false 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/service/websocket.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws') 2 | const EventEmitter = require('events') 3 | 4 | class WebSocketManager { 5 | constructor() { 6 | if (WebSocketManager.instance) { 7 | return WebSocketManager.instance 8 | } 9 | 10 | this.connections = new Map() 11 | this.states = new Map() // 存储连接状态历史 12 | console.log('✅ WebSocketManager 单例已创建') 13 | 14 | // 确保只有一个实例 15 | WebSocketManager.instance = this 16 | return this 17 | } 18 | 19 | // 清空现有连接(用于重连等场景) 20 | clearConnections() { 21 | this.connections.clear() 22 | } 23 | /** 24 | * 创建或获取WebSocket连接 25 | * @param {string} url WebSocket地址 26 | * @param {Object} handlers 事件处理器 27 | * @returns {WebSocket} WebSocket实例 28 | */ 29 | getConnection(url, handlers = {}) { 30 | // 如果已有连接且状态正常,则返回现有连接 31 | if (this.connections.has(url)) { 32 | const { conn, state } = this.connections.get(url) 33 | if (state === 'OPEN' || state === 'CONNECTING') { 34 | console.log(`使用现有连接: ${url}`) 35 | return conn 36 | } 37 | // 清理无效连接 38 | this.connections.delete(url) 39 | } 40 | 41 | console.log(`创建新连接: ${url}`) 42 | const ws = new WebSocket(url) 43 | 44 | // 创建带状态跟踪的连接对象 45 | const connData = { 46 | conn: ws, 47 | state: 'CONNECTING', 48 | url, 49 | createdAt: Date.now(), 50 | lastUpdated: Date.now(), 51 | } 52 | 53 | this.connections.set(url, connData) 54 | this._updateState(url, 'CONNECTING') 55 | 56 | // 设置事件处理器 57 | ws.on('open', () => { 58 | console.log(`✅ ${url} 连接已建立`) 59 | this._updateState(url, 'OPEN') 60 | if (handlers.onOpen) handlers.onOpen(ws) 61 | }) 62 | 63 | ws.on('message', (data) => { 64 | if (handlers.onMessage) handlers.onMessage(data) 65 | }) 66 | 67 | ws.on('error', (error) => { 68 | console.error(`‼️ ${url} 错误:`, error) 69 | this._updateState(url, 'ERROR') 70 | if (handlers.onError) handlers.onError(error) 71 | }) 72 | 73 | ws.on('close', (code, reason) => { 74 | console.log(`⛔ ${url} 连接关闭: ${code} - ${reason}`) 75 | this._updateState(url, 'CLOSED') 76 | if (handlers.onClose) handlers.onClose(code, reason) 77 | 78 | // 自动重连 79 | if (code !== 1000 && handlers.autoReconnect !== false) { 80 | console.log(`尝试在3秒后重新连接: ${url}`) 81 | setTimeout(() => this.getConnection(url, handlers), 3000) 82 | } 83 | }) 84 | 85 | // 心跳检测 86 | let heartbeatInterval 87 | ws.on('open', () => { 88 | heartbeatInterval = setInterval(() => { 89 | if (ws.readyState === WebSocket.OPEN) { 90 | ws.ping() 91 | } 92 | }, 30000) 93 | }) 94 | 95 | ws.on('close', () => { 96 | clearInterval(heartbeatInterval) 97 | }) 98 | 99 | return ws 100 | } 101 | 102 | /** 103 | * 更新连接状态 104 | * @private 105 | */ 106 | _updateState(url, newState) { 107 | const connData = this.connections.get(url) 108 | if (connData) { 109 | connData.state = newState 110 | connData.lastUpdated = Date.now() 111 | } 112 | 113 | // 记录状态历史(可选) 114 | if (!this.states.has(url)) { 115 | this.states.set(url, []) 116 | } 117 | this.states.get(url).push({ 118 | state: newState, 119 | timestamp: Date.now(), 120 | }) 121 | } 122 | 123 | /** 124 | * 获取连接状态 125 | * @param {string} url 126 | * @returns {string} 状态字符串 127 | */ 128 | getConnectionState(url) { 129 | if (!this.connections.has(url)) return 'NOT_FOUND' 130 | return this.connections.get(url).state 131 | } 132 | 133 | /** 134 | * 关闭指定连接 135 | * @param {string} url WebSocket地址 136 | * @param {number} code 关闭代码 137 | * @param {string} reason 关闭原因 138 | */ 139 | closeConnection(url, code = 1000, reason = '主动关闭') { 140 | if (this.connections.has(url)) { 141 | const { conn } = this.connections.get(url) 142 | if (conn.readyState === WebSocket.OPEN) { 143 | conn.close(code, reason) 144 | this._updateState(url, 'CLOSING') 145 | } 146 | return true 147 | } 148 | return false 149 | } 150 | 151 | /** 152 | * 关闭所有连接 153 | */ 154 | closeAll() { 155 | this.connections.forEach(({ conn }, url) => { 156 | if (conn.readyState === WebSocket.OPEN) { 157 | conn.close(1000, '系统关闭') 158 | } 159 | this._updateState(url, 'CLOSED') 160 | }) 161 | } 162 | 163 | /** 164 | * 获取所有连接状态 165 | * @returns {Array} 连接状态列表 166 | */ 167 | getConnectionsStatus() { 168 | return Array.from(this.connections.entries()).map(([url, connData]) => ({ 169 | url, 170 | state: connData.state, 171 | stateCode: this.getStateCode(connData.state), 172 | since: new Date(connData.createdAt), 173 | lastUpdated: new Date(connData.lastUpdated), 174 | })) 175 | } 176 | 177 | /** 178 | * 获取状态数字码 179 | */ 180 | getStateCode(stateName) { 181 | const states = { 182 | CONNECTING: 0, 183 | OPEN: 1, 184 | CLOSING: 2, 185 | CLOSED: 3, 186 | ERROR: 4, 187 | } 188 | return states[stateName] !== undefined ? states[stateName] : -1 189 | } 190 | // 在WebSocketManager类中添加以下方法 191 | getStateName(state) { 192 | // 如果传入的是数字状态码 193 | if (typeof state === 'number') { 194 | return this._mapStateNumberToName(state) 195 | } 196 | 197 | // 如果是我们的自定义状态字符串 198 | return state 199 | } 200 | 201 | // 私有方法:将WebSocket数字状态码转换为名称 202 | _mapStateNumberToName(state) { 203 | const states = { 204 | 0: 'CONNECTING', 205 | 1: 'OPEN', 206 | 2: 'CLOSING', 207 | 3: 'CLOSED', 208 | } 209 | return states[state] || 'UNKNOWN' 210 | } 211 | } 212 | 213 | module.exports = new WebSocketManager() // 直接导出单例实例 214 | -------------------------------------------------------------------------------- /src/service/base.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise') 2 | 3 | module.exports = class extends think.Service { 4 | /** 5 | * 将Cron表达式解析为自然语言描述 6 | * @param {string} cronExpression - 6位Cron表达式(秒 分 时 日 月 周) 7 | * @returns {string} 自然语言描述 8 | */ 9 | parseCronToChinese(cronExpression) { 10 | const parts = cronExpression.trim().split(/\s+/) 11 | if (parts.length !== 6) { 12 | return `无效的Cron表达式: ${cronExpression}` 13 | } 14 | 15 | const [second, minute, hour, day, month, weekday] = parts 16 | 17 | // 星期映射 (0=周日, 1=周一, ... 6=周六, 7=周日) 18 | const weekdays = { 19 | 0: '周日', 20 | 1: '周一', 21 | 2: '周二', 22 | 3: '周三', 23 | 4: '周四', 24 | 5: '周五', 25 | 6: '周六', 26 | 7: '周日', 27 | } 28 | 29 | // 月份映射 30 | const months = { 31 | 1: '1月', 32 | 2: '2月', 33 | 3: '3月', 34 | 4: '4月', 35 | 5: '5月', 36 | 6: '6月', 37 | 7: '7月', 38 | 8: '8月', 39 | 9: '9月', 40 | 10: '10月', 41 | 11: '11月', 42 | 12: '12月', 43 | } 44 | 45 | // 检查是否是特定时间点 46 | const isSpecificTime = 47 | !isWildcard(second) && !isWildcard(minute) && !isWildcard(hour) 48 | 49 | // 检查是否是特定日期 50 | const isSpecificDate = 51 | !isWildcard(day) && !isWildcard(month) && !isWildcard(weekday) 52 | 53 | // 辅助函数:判断是否为通配符 54 | function isWildcard(part) { 55 | return part === '*' || part === '?' 56 | } 57 | 58 | // 辅助函数:解析数字部分 59 | function parseNumber(part, mapping) { 60 | if (isWildcard(part)) return null 61 | 62 | // 处理逗号分隔的列表 63 | if (part.includes(',')) { 64 | const values = part.split(',').map((v) => mapping[v] || v) 65 | return values.join('、') 66 | } 67 | 68 | // 处理范围 69 | if (part.includes('-')) { 70 | const [start, end] = part.split('-') 71 | return `${mapping[start] || start}到${mapping[end] || end}` 72 | } 73 | 74 | return mapping[part] || part 75 | } 76 | 77 | // 1. 处理特定日期+时间的情况 (如 0 0 11 16 7 3) 78 | if (isSpecificDate && isSpecificTime) { 79 | const monthStr = parseNumber(month, months) || '' 80 | const dayStr = parseNumber(day, {}) || '' 81 | const weekdayStr = parseNumber(weekday, weekdays) || '' 82 | const timeStr = formatTime(hour, minute, second) 83 | 84 | // 组合日期部分 85 | let datePart = '' 86 | if (monthStr && dayStr) { 87 | datePart = `${monthStr}${dayStr}日` 88 | if (weekdayStr) datePart += weekdayStr 89 | } else if (weekdayStr) { 90 | datePart = `每周${weekdayStr}` 91 | } 92 | 93 | return `${datePart} ${timeStr}执行` 94 | } 95 | 96 | // 2. 处理特定时间的情况 97 | if (isSpecificTime) { 98 | const timeStr = formatTime(hour, minute, second) 99 | let description = `每天${timeStr}` 100 | 101 | // 添加日期条件 102 | const dateConditions = [] 103 | 104 | if (!isWildcard(day)) { 105 | dateConditions.push(`每月${parseNumber(day, {})}日`) 106 | } 107 | 108 | if (!isWildcard(weekday)) { 109 | dateConditions.push(`每周${parseNumber(weekday, weekdays)}`) 110 | } 111 | 112 | if (!isWildcard(month)) { 113 | dateConditions.push(`每年${parseNumber(month, months)}`) 114 | } 115 | 116 | if (dateConditions.length > 0) { 117 | description += `,${dateConditions.join('且')}` 118 | } 119 | 120 | return description + '执行' 121 | } 122 | 123 | // 3. 通用解析 (其他情况) 124 | return parseGenericCron(parts) 125 | } 126 | 127 | /** 128 | * 格式化时间部分 129 | */ 130 | formatTime(hour, minute, second) { 131 | const h = hour.padStart(2, '0') 132 | const m = minute.padStart(2, '0') 133 | 134 | if (second === '0' || second === '00') { 135 | return `${h}:${m}` 136 | } 137 | return `${h}:${m}:${second.padStart(2, '0')}` 138 | } 139 | 140 | /** 141 | * 通用Cron解析器 (用于非特定时间的情况) 142 | */ 143 | parseGenericCron(parts) { 144 | const [second, minute, hour, day, month, weekday] = parts 145 | const descriptions = [] 146 | 147 | // 解析时间部分 148 | if (second !== '0' && second !== '*') { 149 | descriptions.push(`${second}秒`) 150 | } 151 | 152 | if (minute !== '*') { 153 | descriptions.push(parsePart(minute, '分')) 154 | } else { 155 | descriptions.push('每分钟') 156 | } 157 | 158 | if (hour !== '*') { 159 | descriptions.push(parsePart(hour, '点')) 160 | } else { 161 | descriptions.push('每小时') 162 | } 163 | 164 | // 解析日期部分 165 | const dateDescriptions = [] 166 | 167 | if (day !== '*') { 168 | dateDescriptions.push(parsePart(day, '日')) 169 | } 170 | 171 | if (month !== '*') { 172 | dateDescriptions.push(parsePart(month, '月')) 173 | } 174 | 175 | if (weekday !== '*') { 176 | dateDescriptions.push( 177 | parsePart(weekday, '周', { 178 | 0: '日', 179 | 1: '一', 180 | 2: '二', 181 | 3: '三', 182 | 4: '四', 183 | 5: '五', 184 | 6: '六', 185 | 7: '日', 186 | }) 187 | ) 188 | } 189 | 190 | if (dateDescriptions.length > 0) { 191 | descriptions.push(dateDescriptions.join('、')) 192 | } 193 | 194 | return descriptions.join('') + '执行' 195 | } 196 | 197 | /** 198 | * 解析Cron表达式部分 199 | */ 200 | parsePart(part, unit, mapping = {}) { 201 | if (part === '*') return `每${unit}` 202 | if (/^\d+$/.test(part)) return `${mapping[part] || part}${unit}` 203 | 204 | if (part.includes(',')) { 205 | const values = part.split(',').map((v) => mapping[v] || v) 206 | return `${values.join('、')}${unit}` 207 | } 208 | 209 | if (part.includes('-')) { 210 | const [start, end] = part.split('-') 211 | return `${mapping[start] || start}到${mapping[end] || end}${unit}` 212 | } 213 | 214 | if (part.includes('/')) { 215 | const [, interval] = part.split('/') 216 | return `每${interval}${unit}` 217 | } 218 | 219 | return part 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖 AI-WeChat-Mater-智能微信助手平台 2 | 3 | # 📝 项目说明 4 | 5 | > 开源智能微信助手平台,将 AI 能力无缝融入微信生态,实现自动化消息处理与智能交互 6 | 7 | 1、大模型接口 来自:[原能引擎,大模型中转站不掺水-真稳定](https://api.evopower.net) \- 稳定可靠,实测 gemini 模型表现优异 **[加入 TG 交流群](https://t.me/evopower_ai)** 8 | 9 | 2、核心依赖服务 [WechatPadPro](https://github.com/WeChatPadPro/WeChatPadPro) 请先安装此服务 10 | 11 | # ✨ 主要功能 12 | 13 | ### 🤝 多账号统一管理 14 | 15 | - 支持多微信账号同时在线与管理 16 | - 统一的授权与权限控制系统 17 | - 账号状态实时监控与告警 18 | 19 | ### 🧠 智能 AI 对话引擎 20 | 21 | - 多家大模型 API,支持灵活切换 22 | - **特别推荐:[原能引擎 - 大模型中转站](https://api.evopower.net/)** - 稳定可靠,实测 gemini 模型表现优异 23 | - 自定义提示词与上下文记忆功能 24 | - 多轮对话与场景化交互 25 | 26 | ### ⏰ 自动化任务处理 27 | 28 | - 定时消息发送与事件提醒 29 | - 智能消息过滤与优先级处理 30 | - 自动化好友管理与群组操作 31 | 32 | ### 📊 全方位监控分析 33 | 34 | - 消息记录与交互数据分析 35 | - 用户行为模式识别 36 | - 性能指标与使用统计 37 | 38 | ## 🚀 应用场景 39 | 40 | ### 🎓 智能学习伴侣 41 | 42 | - **线上督促学习老师** - 定期提醒学习计划,检查进度 43 | - **智能答疑助手** - 随时解答学习问题,提供参考资料 44 | 45 | ### 💪 AI 健身教练 46 | 47 | **线上 AI 健身教练(配合多模态 AI 以及相关)** 48 | 49 | - **个性化训练计划** - 根据用户情况制定健身方案 50 | - **动作指导与纠正** - 通过多模态 AI 分析训练动作 51 | - **进度跟踪与激励** - 记录训练数据,提供正向反馈 52 | 53 | ### 📈 智能营销平台 54 | 55 | **线上营销智能体、自动加微信、自动发消息、自动群发、等功能。(需二次开发)** 56 | 57 | - **自动客户开发** - 智能筛选目标客户,自动添加微信 58 | - **个性化消息推送** - 根据用户画像发送定制内容 59 | - **群发管理与优化** - 高效管理群发任务,提升转化率 60 | - **营销效果分析** - 跟踪消息打开率与转化数据 61 | 62 | ### 👥 个人事务助理 63 | 64 | - **日程管理与提醒** - 重要事件自动提醒 65 | - **信息收集与整理** - 自动归类重要消息与文件 66 | - **智能回复与代答** - 根据场景自动生成回复内容 67 | 68 | #### 已实现功能 69 | 70 | - 多微信账号管理与授权 71 | - AI 智能聊天与自动回复(支持自定义 prompt、历史消息上下文) 72 | - 定时自动发送微信消息以及处理微信事物 73 | - WebSocket 实时消息推送与接收 74 | - 消息记录、日志与统计分析 75 | - 好友/群管理、备注、标签等 76 | 77 | #### 待实现功能 78 | 79 | - 数据库优化 80 | 81 | - 图片处理 82 | - 语音、视频处理 83 | - 专属模型训练,自动加好友,多 ai 角色多模型 84 | - event 定期总结 85 | - 群聊接入 86 | - wechatpadpro 更多功能接入 87 | 88 | # 📚 运行与部署 89 | 90 | 本项目分为 API 端和 Admin 端 91 | 92 | **[AI-Wechat-Api](https://github.com/lofteryang/ai-wechat-api)** 93 | 94 | **[AI-Wechat-Admini](https://github.com/lofteryang/ai-wechat-admin)** 95 | 96 | ## 1.API 端 97 | 98 | #### 1.环境要求 99 | 100 | - Node.js >= 16 101 | - MySQL >= 5.7 102 | - [ThinkJS](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) 103 | - 依赖包见 package.json 104 | 105 | 安装:`npm install ` 106 | 107 | 运行:`npm run start` 108 | 109 | #### 2.必要配置 110 | 111 | `/src/config.js` :hostUrl:微信模拟器的地址 112 | 113 | `/adapter/adapter.js` mysql:数据库信息 114 | 115 | 数据库配置 ai_system 116 | 117 | ### 核心配置项 118 | 119 | 数据库:ai_system 120 | 121 | | 配置项 | 说明 | 示例值 | 122 | | :-------------- | :--------------- | :--------------------------------------------- | 123 | | `aiHost` | AI 服务地址 | `https://api.evopower.net/v1/chat/completions` | 124 | | `aiApiKey` | API 密钥 | `sk-xxxxxxxxxxxxxxxx` | 125 | | `aiChatModel` | 对话模型 | `gemini-2.5-pro` | 126 | | `hisMsgDay` | 历史消息保留天数 | `7` | 127 | | `to_noti_wxid` | 管理员通知 ID | `wxid_admin` | 128 | | `from_noti_key` | 消息发送方 ID | `wxid_sender` | 129 | 130 | ### 微信配置 131 | 132 | 确保已安装并配置 [WechatPadPro](https://github.com/WeChatPadPro/WeChatPadPro) 服务,并在配置中正确设置连接地址。 133 | 134 | #### 3.相关接口 135 | 136 | - `GET /account/allList` 获取所有微信账号 137 | - `POST /account/getAIReply` 获取 AI 回复 138 | - `GET /friends/list` 获取好友列表 139 | - `POST /wechat/sendTextMessage` 发送微信消息 140 | - ...参考/controller 下文件 141 | 142 | #### 4.部署与运维 143 | 144 | - 推荐使用 [PM2](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) 进行进程管理 145 | - Nginx 反向代理配置见 [nginx.conf](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html) 146 | - 日志文件位于 `logs/` 目录 147 | 148 | ## 2. Admin 管理端 149 | 150 | AI WeChat Admin 是一个基于 Vue 2 和 Element UI 的微信管理后台,支持多微信账号管理、好友管理、AI 日志、系统参数配置等功能。适用于企业或个人对微信账号的集中管理和数据分析。 151 | 152 | ## 特性 153 | 154 | - 多微信账号统一管理 155 | - 好友信息管理与备注 156 | - AI 聊天日志与推理过程可视化 157 | - 角色与权限配置 158 | - 系统参数自定义 159 | - 数据看板与趋势图表 160 | - 管理员账号管理 161 | - 支持 Element UI 组件库 162 | - 响应式布局,适配主流浏览器 163 | 164 | ## 快速开始 165 | 166 | ### 克隆项目 167 | 168 | ```sh 169 | git clone https://github.com/lofteryang/ai-wechat-admin.git 170 | cd ai-wechat-admin 171 | ``` 172 | 173 | ### 安装依赖 174 | 175 | ```sh 176 | yarn install 177 | # 或者 178 | npm install 179 | ``` 180 | 181 | ### 本地开发 182 | 183 | ```sh 184 | yarn serve 185 | # 或者 186 | npm run serve 187 | ``` 188 | 189 | ### 构建生产环境 190 | 191 | ```sh 192 | yarn build 193 | # 或者 194 | npm run build 195 | ``` 196 | 197 | ### 代码检查 198 | 199 | ```sh 200 | yarn lint 201 | # 或者 202 | npm run lint 203 | ``` 204 | 205 | ## 目录结构 206 | 207 | ``` 208 | ├── public/ # 静态资源 209 | ├── src/ 210 | │ ├── assets/ # 图片等资源 211 | │ ├── components/ # 业务组件 212 | │ ├── config/ # 配置文件 213 | │ ├── router/ # 路由配置 214 | │ ├── store/ # Vuex 状态管理 215 | │ ├── styles/ # 样式文件 216 | │ ├── App.vue # 根组件 217 | │ └── main.js # 入口文件 218 | ├── package.json 219 | └── README.md 220 | ``` 221 | 222 | ## 接口配置 223 | 224 | 请在 `src/config/api.js` 中配置后端 API 地址: 225 | 226 | ```js 227 | const rootUrl = 'http://your-api-server:port' 228 | const api = { rootUrl } 229 | export default api 230 | ``` 231 | 232 | ## 参与贡献 233 | 234 | 欢迎提交 Issue 和 Pull Request! 235 | 236 | 1. Fork 本仓库 237 | 2. 新建分支 (`git checkout -b feature/xxx`) 238 | 3. 提交更改 (`git commit -am 'Add new feature'`) 239 | 4. 推送分支 (`git push origin feature/xxx`) 240 | 5. 新建 Pull Request 241 | 242 | # 重要说明 243 | 244 | 1、本项目仅用于研究学习 245 | 246 | 2、根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。 247 | 248 | # 👥 项目作者 249 | 250 |
lofteryang
lofteryang
xiaofute
xiaofute
251 | -------------------------------------------------------------------------------- /src/controller/dashboard.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js') 2 | const moment = require('moment') 3 | 4 | module.exports = class extends Base { 5 | async basicAction() { 6 | const FriendModel = this.model('friends') 7 | const AccountModel = this.model('wechat_account') 8 | const MessageModel = this.model('messages') 9 | const SentModel = this.model('messages') 10 | 11 | const [friendsTotal, onlineAccounts, receivedMessages, sentType1] = 12 | await Promise.all([ 13 | FriendModel.count(), 14 | AccountModel.where({ onlineTime: ['!=', null] }).count(), 15 | MessageModel.count(), 16 | SentModel.where({ type: 1 }).count(), 17 | ]) 18 | 19 | return this.success({ 20 | friends: friendsTotal, 21 | online: onlineAccounts, 22 | received: receivedMessages, 23 | sent: sentType1, 24 | }) 25 | } 26 | 27 | async chartsDataAction() { 28 | const MessageModel = this.model('messages') 29 | const TaskModel = this.model('friends_task') 30 | 31 | let msgSql = 32 | 'WITH date_range AS(SELECT CURDATE()-INTERVAL n DAY AS stat_date FROM(SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19)AS days),message_stats AS(SELECT DATE(create_at)AS msg_date,COUNT(CASE WHEN is_ai=1 THEN 1 END)AS ai_msg_count,COUNT(CASE WHEN TYPE=1 THEN 1 END)AS total_msg_count FROM ai_messages WHERE create_at>=CURDATE()-INTERVAL 19 DAY GROUP BY DATE(create_at))SELECT dr.stat_date,COALESCE(ms.ai_msg_count,0)AS ai_msg_count,COALESCE(ms.total_msg_count,0)AS total_msg_count FROM date_range dr LEFT JOIN message_stats ms ON dr.stat_date=ms.msg_date ORDER BY dr.stat_date DESC;' 33 | 34 | let taskSql = 35 | 'WITH date_range AS(SELECT CURDATE()-INTERVAL n DAY AS stat_date FROM(SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19)AS days),task_stats AS(SELECT DATE(create_time)AS task_date,COUNT(*)AS new_task_count FROM ai_friends_task WHERE create_time>=CURDATE()-INTERVAL 19 DAY GROUP BY DATE(create_time))SELECT dr.stat_date,COALESCE(ts.new_task_count,0)AS new_task_count FROM date_range dr LEFT JOIN task_stats ts ON dr.stat_date=ts.task_date ORDER BY dr.stat_date DESC;' 36 | 37 | let dailyMessages = await MessageModel.query(msgSql) 38 | console.log(dailyMessages) 39 | let dailyTasks = await TaskModel.query(taskSql) 40 | 41 | let data = { 42 | dailyMessages: dailyMessages, 43 | dailyTasks: dailyTasks, 44 | } 45 | return this.success(data) 46 | } 47 | 48 | async chartsAction() { 49 | const MessageModel = this.model('messages') 50 | const TaskModel = this.model('friends_task') 51 | 52 | const now = moment() 53 | const startTime24h = now 54 | .clone() 55 | .subtract(24, 'hours') 56 | .format('YYYY-MM-DD HH:mm:ss') 57 | 58 | // 1. 消息统计部分 59 | const messageLabels = Array.from({ length: 25 }, (_, i) => { 60 | return now 61 | .clone() 62 | .subtract(24 - i, 'hours') 63 | .format('MM-DD HH:00') 64 | }) 65 | 66 | // 使用正确的参数绑定方式 67 | const getMessageStats = async (isAI = false) => { 68 | const whereCondition = { 69 | create_at: ['>=', startTime24h], 70 | } 71 | 72 | if (isAI) { 73 | whereCondition.is_ai = 1 74 | } 75 | 76 | const results = await MessageModel.field( 77 | `DATE_FORMAT(create_at, '%Y-%m-%d %H:00') AS hour_group, COUNT(*) AS count` 78 | ) 79 | .where(whereCondition) // 直接使用对象语法,避免手动绑定 80 | .group('hour_group') 81 | .select() 82 | 83 | const countMap = new Map( 84 | results.map((item) => [item.hour_group, item.count]) 85 | ) 86 | 87 | const currentData = [] 88 | for (let i = 0; i < 24; i++) { 89 | const hourPoint = now 90 | .clone() 91 | .subtract(24 - i, 'hours') 92 | .startOf('hour') 93 | .format('YYYY-MM-DD HH:mm') 94 | 95 | currentData.push(countMap.get(hourPoint) || 0) 96 | } 97 | return currentData 98 | } 99 | 100 | // 使用 Promise.all 并行查询 101 | const [totalResults, aiResults] = await Promise.all([ 102 | getMessageStats(), 103 | getMessageStats(true), 104 | ]) 105 | 106 | // 2. 任务统计部分 - 完全避免手动 SQL 绑定 107 | const taskStartTime = now 108 | .clone() 109 | .subtract(3, 'hours') 110 | .format('YYYY-MM-DD HH:mm:ss') 111 | 112 | const getTaskStats = async (field) => { 113 | // 使用 ThinkJS ORM 的对象语法代替手动绑定 114 | const whereCondition = { 115 | [field]: ['>=', taskStartTime], 116 | } 117 | 118 | const groupExpression = 119 | field === 'create_time' ? 'COUNT(*)' : 'SUM(execution_count)' 120 | 121 | const results = await TaskModel.field( 122 | ` 123 | DATE_FORMAT(${field}, '%Y-%m-%d %H:00') AS hour_group, 124 | ${groupExpression} AS value 125 | ` 126 | ) 127 | .where(whereCondition) // 直接使用对象语法 128 | .group('hour_group') 129 | .select() 130 | 131 | const countMap = new Map( 132 | results.map((item) => [item.hour_group, Number(item.value)]) 133 | ) 134 | 135 | const hourLabels = [] 136 | const counts = [] 137 | 138 | for (let i = 0; i <= 3; i++) { 139 | const hourPoint = now 140 | .clone() 141 | .subtract(3 - i, 'hours') 142 | .startOf('hour') 143 | .format('YYYY-MM-DD HH:mm') 144 | 145 | hourLabels.push( 146 | now 147 | .clone() 148 | .subtract(3 - i, 'hours') 149 | .format('HH:00') 150 | ) 151 | 152 | counts.push(countMap.get(hourPoint) || 0) 153 | } 154 | 155 | return { labels: hourLabels, data: counts } 156 | } 157 | 158 | // 并行查询任务统计数据 159 | const taskData = await Promise.all([ 160 | getTaskStats('create_time'), 161 | getTaskStats('last_execution_time'), 162 | ]) 163 | 164 | // 3. 额外统计 165 | const messageTypes = await MessageModel.field( 166 | ` 167 | CASE 168 | WHEN msg_type = 1 THEN '文本' 169 | WHEN msg_type = 3 THEN '图片' 170 | WHEN msg_type = 34 THEN '语音' 171 | ELSE '其他' 172 | END AS type, 173 | COUNT(*) AS count 174 | ` 175 | ) 176 | .where('create_at >= :startTime', { startTime: startTime24h }) // 使用命名参数语法 177 | .group('type') 178 | .select() 179 | 180 | // 4. 任务状态统计 181 | const taskStatusDistribution = await TaskModel.field( 182 | ` 183 | CASE 184 | WHEN is_active = 1 AND last_execution_time IS NOT NULL THEN '执行中' 185 | WHEN is_active = 1 AND last_execution_time IS NULL THEN '待启动' 186 | WHEN is_active = 0 AND execution_count > 0 THEN '已结束' 187 | ELSE '未激活' 188 | END AS status, 189 | COUNT(*) AS count 190 | ` 191 | ) 192 | .group('status') 193 | .select() 194 | 195 | // 5. 返回结果 196 | return this.success({ 197 | messages: { 198 | labels: messageLabels, 199 | totalData: totalResults, 200 | aiData: aiResults, 201 | types: messageTypes, 202 | }, 203 | tasks: { 204 | labels: taskData[0].labels, 205 | counts: taskData[0].data, 206 | executions: taskData[1].data, 207 | statusDistribution: taskStatusDistribution, 208 | }, 209 | summary: { 210 | totalMessages: totalResults.reduce((a, b) => a + b, 0), 211 | aiMessages: aiResults.reduce((a, b) => a + b, 0), 212 | aiMessageRatio: 213 | aiResults.reduce((a, b) => a + b, 0) / 214 | totalResults.reduce((a, b) => a + b, 0) || 0, 215 | }, 216 | }) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/service/taskScheduler.js: -------------------------------------------------------------------------------- 1 | const CronJob = require('cron').CronJob 2 | const Base = require('../service/base') 3 | const EventEmitter = require('events') 4 | const moment = require('moment') // 添加moment处理时间 5 | 6 | module.exports = class extends Base { 7 | constructor() { 8 | super() 9 | this.scheduledTasks = new Map() 10 | this.lastRefreshTime = null 11 | this.refreshInterval = 15000 12 | this.refreshTasks = this.refreshTasks.bind(this) 13 | this.modelEvents = new EventEmitter() 14 | } 15 | 16 | async init() { 17 | await this.refreshTasks() 18 | setInterval(this.refreshTasks, this.refreshInterval) 19 | this.setupModelListeners() 20 | 21 | // 关键修复:连接模型事件发射器 22 | this.model('task').schedulerEventEmitter = this.modelEvents 23 | 24 | think.logger.info( 25 | `定时任务系统已启动,刷新间隔 ${this.refreshInterval / 1000}秒` 26 | ) 27 | } 28 | 29 | setupModelListeners() { 30 | const handleTaskUpdate = (taskId) => { 31 | this.model('task') 32 | .getTaskById(taskId) 33 | .then((task) => { 34 | if (task) { 35 | if (task.is_active === 1) { 36 | this.updateTaskSchedule(task) 37 | } else { 38 | this.removeTaskSchedule(taskId) 39 | } 40 | } 41 | }) 42 | .catch((e) => { 43 | think.logger.error(`任务更新处理失败:`, e) 44 | }) 45 | } 46 | 47 | this.modelEvents.on('task:create', handleTaskUpdate) 48 | this.modelEvents.on('task:update', handleTaskUpdate) 49 | 50 | this.modelEvents.on('task:delete', (taskId) => { 51 | this.removeTaskSchedule(taskId) 52 | think.logger.info(`任务 #${taskId} 已删除`) 53 | }) 54 | } 55 | 56 | async refreshTasks() { 57 | try { 58 | think.logger.debug('刷新定时任务列表...') 59 | const tasks = await this.model('task').getActiveTasks() 60 | const activeTaskIds = new Set(tasks.map((t) => t.id)) 61 | 62 | // 处理所有任务(包括更新) 63 | for (const task of tasks) { 64 | if (!this.scheduledTasks.has(task.id)) { 65 | // 新增任务 66 | this.scheduleTask(task) 67 | think.logger.debug(`新增任务 #${task.id}`) 68 | } else { 69 | // 检查现有任务是否需要更新 70 | const existingTask = this.scheduledTasks.get(task.id) 71 | 72 | // console.log('existingTask==>', existingTask) 73 | // console.log('task==>', task) 74 | // 检查关键配置是否变更 75 | const isConfigChanged = existingTask.cron !== task.cron_expression 76 | if (isConfigChanged) { 77 | think.logger.debug(`任务 #${task.id} 配置变更,重新调度`) 78 | this.scheduleTask(task) // 重新调度 79 | } 80 | } 81 | } 82 | 83 | // 检查并移除删除/禁用任务 84 | const existingTaskIds = Array.from(this.scheduledTasks.keys()) 85 | for (const taskId of existingTaskIds) { 86 | if (!activeTaskIds.has(taskId)) { 87 | this.removeTaskSchedule(taskId) 88 | } 89 | } 90 | 91 | this.lastRefreshTime = new Date() 92 | think.logger.debug( 93 | `任务刷新完成: 活跃任务 ${this.scheduledTasks.size} 个` 94 | ) 95 | } catch (e) { 96 | think.logger.error('刷新任务时出错:', e) 97 | } 98 | } 99 | scheduleTask(task) { 100 | // console.log('this.scheduledTasks==>', this.scheduledTasks) 101 | try { 102 | // 如果任务已存在,先取消 103 | if (this.scheduledTasks.has(task.id)) { 104 | const { job } = this.scheduledTasks.get(task.id) 105 | job.stop() 106 | think.logger.warn(`任务 #${task.id} 已存在,重新调度`) 107 | } 108 | 109 | // 创建新任务 - 修复cron 4.3.1语法 110 | const job = new CronJob( 111 | task.cron_expression, 112 | async () => { 113 | await this.executeTask(task) 114 | }, 115 | null, 116 | true, // 立即启动 117 | 'Asia/Shanghai' 118 | ) 119 | 120 | // 存储任务信息 121 | this.scheduledTasks.set(task.id, { 122 | job, 123 | cron: task.cron_expression, 124 | }) 125 | 126 | // 更新下次执行时间 127 | this.updateTaskNextExecTime(task.id) 128 | 129 | think.logger.info(`任务 #${task.id} 调度成功: ${task.cron_expression}`) 130 | } catch (e) { 131 | think.logger.error(`任务调度失败 #${task.id}:`, e) 132 | } 133 | } 134 | 135 | updateTaskSchedule(task) { 136 | if (task.is_active !== 1) { 137 | this.removeTaskSchedule(task.id) 138 | return 139 | } 140 | 141 | // 获取现有任务 142 | const existingTask = this.scheduledTasks.get(task.id) 143 | if (!existingTask) { 144 | this.scheduleTask(task) 145 | return 146 | } 147 | 148 | // 检查cron表达式是否变更 149 | if (existingTask.cron !== task.cron_expression) { 150 | this.scheduleTask(task) 151 | think.logger.info(`任务 #${task.id} cron表达式变更,已重新调度`) 152 | } else { 153 | this.updateTaskNextExecTime(task.id) 154 | think.logger.debug(`任务 #${task.id} 配置更新(无需重新调度)`) 155 | } 156 | } 157 | 158 | removeTaskSchedule(taskId) { 159 | if (!this.scheduledTasks.has(taskId)) return 160 | 161 | const { job } = this.scheduledTasks.get(taskId) 162 | job.stop() 163 | this.scheduledTasks.delete(taskId) 164 | // 清除数据库中的下次执行时间 165 | this.model('task').updateNextExecution(taskId, null) 166 | think.logger.info(`任务 #${taskId} 已取消调度`) 167 | } 168 | 169 | updateTaskNextExecTime(taskId) { 170 | const taskData = this.scheduledTasks.get(taskId) 171 | if (!taskData) return 172 | 173 | try { 174 | // 获取下次执行时间(cron 4.3.1返回的是Date数组) 175 | const nextDates = taskData.job.nextDates(1) 176 | 177 | if (nextDates.length > 0) { 178 | // 关键修复:cron 4.3.1返回的是Date对象,不是Moment 179 | const nextDate = nextDates[0] 180 | 181 | // 存储到数据库 182 | this.model('task').updateNextExecution(taskId, think.datetime(nextDate)) 183 | } else { 184 | think.logger.warn(`任务 #${taskId} 无下次执行时间`) 185 | } 186 | } catch (e) { 187 | think.logger.error(`更新任务 #${taskId} 下次执行时间失败:`, e) 188 | } 189 | } 190 | 191 | isNowInRange(start, end) { 192 | const now = moment() 193 | const startDate = start ? moment(start) : null 194 | const endDate = end ? moment(end) : null 195 | 196 | // 如果是日期格式(没有时间部分),设置最大结束时间 197 | if (endDate && end.length === 10) { 198 | endDate.endOf('day') 199 | } 200 | 201 | return ( 202 | (!startDate || now.isSameOrAfter(startDate)) && 203 | (!endDate || now.isSameOrBefore(endDate)) 204 | ) 205 | } 206 | 207 | async executeTask(task) { 208 | const taskInfo = await this.model('task').where({ id: task.id }).find() 209 | 210 | try { 211 | // 获取任务详情 - 使用正确的模型 212 | 213 | if (!taskInfo) { 214 | think.logger.error(`任务 #${task.id} 不存在`) 215 | return this.removeTaskSchedule(task.id) 216 | } 217 | 218 | // 检查任务状态 219 | if (taskInfo.is_active !== 1) { 220 | think.logger.warn(`任务 #${task.id} 已禁用`) 221 | return this.removeTaskSchedule(task.id) 222 | } 223 | 224 | // 检查时间范围 225 | if (!this.isNowInRange(taskInfo.start_date, taskInfo.end_date)) { 226 | think.logger.warn(`任务 #${task.id} 不在有效期内`) 227 | return 228 | } 229 | 230 | // 获取好友信息 231 | const friend = await this.model('friends') 232 | .where({ id: taskInfo.friend_id }) 233 | .find() 234 | 235 | if (think.isEmpty(friend)) { 236 | throw new Error(`关联好友不存在: ${taskInfo.friend_id}`) 237 | } 238 | if (friend.is_active != 1) { 239 | throw new Error(`关联好友已禁用,无法发送消息: ${taskInfo.friend_id}`) 240 | } 241 | if (friend.ai_active != 1) { 242 | throw new Error( 243 | `关联好友已关闭AI托管,无法发送消息: ${taskInfo.friend_id}` 244 | ) 245 | } 246 | 247 | const randomDelay = Math.floor(Math.random() * 60000) 248 | 249 | think.logger.info( 250 | `任务 #${task.id} 进入发送队列,将在 ${randomDelay / 1000} 秒后执行` 251 | ) 252 | 253 | await new Promise((resolve) => setTimeout(resolve, randomDelay)) 254 | 255 | // 延迟后再次检查任务状态(防止延迟期间任务被禁用) 256 | const refreshedTask = await this.model('task') 257 | .where({ id: task.id }) 258 | .find() 259 | if (!refreshedTask || refreshedTask.is_active !== 1) { 260 | think.logger.warn(`任务 #${task.id} 在延迟期间已被禁用,取消发送`) 261 | return 262 | } 263 | 264 | // 发送消息 265 | const sender = think.service('messageSender') 266 | // 使用更合理的消息格式 267 | 268 | const content = 269 | taskInfo.message || 270 | `${friend.my_remark ? friend.my_remark + ',' : ''}${ 271 | taskInfo.task_name 272 | }` 273 | 274 | await sender.sendMessage({ 275 | wxid: friend.wxid, 276 | content: content, 277 | taskId: taskInfo.id, 278 | account_id: friend.account_id, 279 | }) 280 | 281 | // 记录执行信息 282 | await this.model('task').updateExecutionInfo( 283 | taskInfo.id, 284 | think.datetime(new Date()) 285 | ) 286 | 287 | think.logger.info(`任务 #${task.id} 执行完成`) 288 | 289 | this.model('task_logs').add({ 290 | task_id: taskInfo.id, 291 | task_name: taskInfo.task_name, 292 | status: '执行完成', 293 | }) 294 | } catch (e) { 295 | think.logger.error(`任务执行失败 #${task.id}:`, e) 296 | this.model('task_logs').add({ 297 | task_id: taskInfo.id, 298 | task_name: taskInfo.task_name, 299 | status: '执行失败', 300 | content: e.message, 301 | }) 302 | 303 | await this.model('task').recordExecutionError(task.id, e.message) 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/controller/friends.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base') 2 | 3 | module.exports = class extends Base { 4 | /** 5 | * 获取任务列表 6 | */ 7 | async listAction() { 8 | const page = this.get('page') || 1 9 | const size = this.get('size') || 10 10 | let nickname = this.get('nickname') || '' 11 | let remark = this.get('remark') || '' 12 | let wxid = this.get('wxid') || '' 13 | let id = this.get('id') || '' 14 | 15 | let account_id = this.get('account_id') || '' 16 | const is_active = this.get('is_active') 17 | 18 | const model = this.model('friends') 19 | 20 | let queryMap = {} 21 | if (!think.isEmpty(nickname)) { 22 | queryMap['u.nickname'] = ['like', `%${nickname}%`] 23 | } 24 | if (!think.isEmpty(remark)) { 25 | queryMap['u.remark'] = ['like', `%${remark}%`] 26 | } 27 | if (!think.isEmpty(wxid)) { 28 | queryMap['u.wxid'] = wxid 29 | } 30 | if (!think.isEmpty(id)) { 31 | queryMap['u.id'] = id 32 | } 33 | if (!think.isEmpty(account_id)) { 34 | queryMap['u.account_id'] = account_id 35 | } 36 | if (is_active) { 37 | queryMap['u.is_active'] = is_active 38 | } 39 | 40 | const list = await model 41 | .alias('u') 42 | .join({ 43 | table: 'wechat_account', 44 | join: 'left', 45 | as: 'a', 46 | on: ['u.account_id', 'a.id'], 47 | }) 48 | .where(queryMap) 49 | .field('u.*,a.nickname as ow_nickname,a.avatar as ow_avatar') 50 | .order('u.is_active desc,u.account_id desc,u.nickname desc') 51 | .page(page, size) 52 | .countSelect() 53 | for (let item of list.data) { 54 | item.is_active = item.is_active.toString() 55 | item.ai_active = item.ai_active.toString() 56 | item.ai_reply = item.ai_reply.toString() 57 | let tasks = await this.model('friends_task') 58 | .where({ 59 | friend_id: item.id, 60 | is_active: 1, 61 | }) 62 | .order('id asc') 63 | .select() 64 | let ser = think.service('base') 65 | for (let task of tasks) { 66 | try { 67 | task.cron_expression_chinese = ser.parseCronToChinese( 68 | task.cron_expression 69 | ) 70 | } catch (e) { 71 | task.cron_expression_chinese = '中文解析失败' 72 | } 73 | } 74 | item.tasks = tasks 75 | } 76 | return this.success(list) 77 | } 78 | 79 | async updateMemberStatusAction() { 80 | const model = this.model('friends') 81 | const id = this.get('id') 82 | const is_active = this.get('is_active') 83 | if (id > 0) { 84 | let res = await model 85 | .where({ 86 | id: id, 87 | }) 88 | .update({ 89 | is_active: is_active, 90 | }) 91 | return this.success(res) 92 | } else { 93 | return this.fail(500, '错误,联系管理员') 94 | } 95 | } 96 | 97 | async updateMemberAIReplyStatusAction() { 98 | const model = this.model('friends') 99 | const id = this.get('id') 100 | const ai_reply = this.get('ai_reply') 101 | if (id > 0) { 102 | let res = await model 103 | .where({ 104 | id: id, 105 | }) 106 | .update({ 107 | ai_reply: ai_reply, 108 | }) 109 | return this.success(res) 110 | } else { 111 | return this.fail(500, '错误,联系管理员') 112 | } 113 | } 114 | async updateMemberAIStatusAction() { 115 | const model = this.model('friends') 116 | const id = this.get('id') 117 | const ai_active = this.get('ai_active') 118 | if (id > 0) { 119 | let res = await model 120 | .where({ 121 | id: id, 122 | }) 123 | .update({ 124 | ai_active: ai_active, 125 | }) 126 | return this.success(res) 127 | } else { 128 | return this.fail(500, '错误,联系管理员') 129 | } 130 | } 131 | 132 | async updateMarkAction() { 133 | const id = this.post('id') 134 | const mark = this.post('mark') 135 | 136 | if (id > 0) { 137 | let res = await this.model('friends') 138 | .where({ 139 | id: id, 140 | }) 141 | .update({ 142 | mark: mark, 143 | }) 144 | 145 | return this.success(res) 146 | } else { 147 | return this.fail(500, '错误,联系管理员') 148 | } 149 | } 150 | async updateMyRemarkAction() { 151 | const id = this.post('id') 152 | const my_remark = this.post('my_remark') 153 | 154 | if (id > 0) { 155 | let res = await this.model('friends') 156 | .where({ 157 | id: id, 158 | }) 159 | .update({ 160 | my_remark: my_remark, 161 | }) 162 | 163 | return this.success(res) 164 | } else { 165 | return this.fail(500, '错误,联系管理员') 166 | } 167 | } 168 | 169 | async checkUserVipTimeAction() { 170 | try { 171 | // 获取当前时间(格式: YYYY-MM-DD HH:mm:ss) 172 | const now = think.datetime(new Date(), 'YYYY-MM-DD HH:mm:ss') 173 | 174 | // 查询所有活跃会员中已过期的记录 175 | const expiredUsers = await this.model('friends') 176 | .where({ 177 | is_active: 1, // 只查询当前活跃的会员 178 | end_time: ['<', now], // 结束时间小于当前时间 179 | }) 180 | .select() 181 | 182 | // 如果没有过期会员,直接返回 183 | if (think.isEmpty(expiredUsers)) { 184 | think.logger.info(`[${now}] 没有需要处理的过期会员`) 185 | return 186 | } 187 | 188 | // 收集所有过期会员ID 189 | const userIds = expiredUsers.map((user) => user.id) 190 | // 批量更新会员状态为过期 191 | const updateCount = await this.model('friends') 192 | .where({ id: ['IN', userIds] }) 193 | .update({ is_active: 0, ai_active: 0 }) 194 | 195 | // 记录日志 196 | think.logger.info(`[${now}] 已处理 ${updateCount} 个过期会员:`, userIds) 197 | 198 | const userNames = expiredUsers.map((user) => user.remark || user.nickname) 199 | //发送提醒: 200 | if (updateCount > 0) { 201 | await this.sendRemindMessage(`注意:${userNames.join(',')} 会员已到期`) 202 | } 203 | } catch (e) { 204 | think.logger.error('检查会员到期时出错:', e) 205 | } 206 | } 207 | 208 | async sendRemindMessage(text) { 209 | let aiParamsList = await this.model('system') 210 | .where({ 211 | key: ['IN', ['to_noti_wxid', 'from_noti_key']], 212 | }) 213 | .select() 214 | let to_noti_wxid = aiParamsList.find( 215 | (item) => item.key == 'to_noti_wxid' 216 | ).value 217 | let from_noti_key = aiParamsList.find( 218 | (item) => item.key == 'from_noti_key' 219 | ).value 220 | 221 | let param = { 222 | key: from_noti_key, 223 | wxid: to_noti_wxid, 224 | content: text, 225 | } 226 | console.log(param) 227 | await think.service('wechat').sendTextMessage(param) 228 | } 229 | 230 | async checkAboutExpirAction() { 231 | let friends = await this.getUsersExpiringIn24Hours() 232 | if (think.isEmpty(friends)) { 233 | think.logger.info('没有用户会员将在24小时内到期') 234 | return this.success() 235 | } 236 | const userNames = friends.map((user) => user.remark || user.nickname) 237 | await this.sendRemindMessage( 238 | `注意:${userNames.join(',')} 会员将在24小时内到期` 239 | ) 240 | return this.success(friends) 241 | } 242 | 243 | async getUsersExpiringIn24Hours() { 244 | try { 245 | // 获取当前时间 246 | const now = new Date() 247 | 248 | // 计算24小时后的时间点 249 | const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000) 250 | 251 | // 获取SQL时间格式 252 | const nowStr = think.datetime(now, 'YYYY-MM-DD HH:mm:ss') 253 | const in24HoursStr = think.datetime(in24Hours, 'YYYY-MM-DD HH:mm:ss') 254 | console.log(nowStr, in24HoursStr) 255 | // 查询条件:活跃用户 & 结束时间在 (当前时间, 当前时间+24小时] 范围内 256 | return await this.model('friends') 257 | .where({ 258 | is_active: 1, 259 | }) 260 | .where({ 261 | end_time: ['BETWEEN', nowStr, in24HoursStr], 262 | }) 263 | .select() 264 | } catch (e) { 265 | think.logger.error('查询24小时内即将过期会员时出错:', e) 266 | return [] 267 | } 268 | } 269 | 270 | async updateStoryAction() { 271 | const id = this.post('id') 272 | const user_story = this.post('user_story') 273 | const role_id = this.post('role_id') 274 | const start_time = this.post('start_time') 275 | const end_time = this.post('end_time') 276 | 277 | const timeFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/ 278 | 279 | if (start_time && !timeFormat.test(start_time)) { 280 | return this.fail(500, '无效的开始时间格式') 281 | } 282 | 283 | if (id > 0) { 284 | let res = await this.model('friends') 285 | .where({ 286 | id: id, 287 | }) 288 | .update({ 289 | user_story: user_story, 290 | role_id: role_id, 291 | start_time: start_time || null, 292 | end_time: end_time || null, 293 | }) 294 | return this.success(res) 295 | } else { 296 | return this.fail(500, 'id不能为空') 297 | } 298 | } 299 | 300 | async updateMarkAction() { 301 | const id = this.post('id') 302 | const mark = this.post('mark') 303 | if (id > 0) { 304 | let res = await this.model('friends') 305 | .where({ 306 | id: id, 307 | }) 308 | .update({ 309 | mark: mark, 310 | }) 311 | return this.success(res) 312 | } else { 313 | return this.fail(500, '错误,联系管理员') 314 | } 315 | } 316 | async updatePromptAndGetAIReplyAction() { 317 | const id = this.post('id') 318 | const prompt = this.post('prompt') 319 | if (id > 0) { 320 | let res = await this.model('friends') 321 | .where({ 322 | id: id, 323 | }) 324 | .update({ 325 | prompt: prompt, 326 | }) 327 | //现在获取AI回复getAIReplyWithNoHisMsg 328 | let friendInfo = await this.model('friends') 329 | .where({ 330 | id: id, 331 | }) 332 | .find() 333 | let aiReply = await think 334 | .service('ai') 335 | .getAIReplyWithUserChat(prompt, friendInfo, false) 336 | return this.success(aiReply) 337 | } else { 338 | return this.fail(500, '错误,联系管理员') 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /ai_wechat.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : glass 5 | Source Server Type : MySQL 6 | Source Server Version : 80025 (8.0.25) 7 | Source Host : 121.41.197.182:3306 8 | Source Schema : ai_wechat 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 80025 (8.0.25) 12 | File Encoding : 65001 13 | 14 | Date: 02/09/2025 21:14:36 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for ai_admin 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `ai_admin`; 24 | CREATE TABLE `ai_admin` ( 25 | `id` int NOT NULL AUTO_INCREMENT, 26 | `username` varchar(25) NOT NULL DEFAULT '', 27 | `password` varchar(255) NOT NULL DEFAULT '', 28 | `password_salt` varchar(255) NOT NULL DEFAULT '', 29 | `last_login_ip` varchar(60) NOT NULL DEFAULT '', 30 | `last_login_time` int NOT NULL DEFAULT '0', 31 | `is_delete` tinyint(1) DEFAULT '0', 32 | `type` tinyint(1) DEFAULT '0', 33 | PRIMARY KEY (`id`) 34 | ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 35 | 36 | -- ---------------------------- 37 | -- Table structure for ai_friends 38 | -- ---------------------------- 39 | DROP TABLE IF EXISTS `ai_friends`; 40 | CREATE TABLE `ai_friends` ( 41 | `id` int NOT NULL AUTO_INCREMENT, 42 | `account_id` int DEFAULT NULL COMMENT '关联wechat_account表的ID', 43 | `avatar` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像', 44 | `nickname` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称', 45 | `wxid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '属于谁的好友', 46 | `creaate_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 47 | `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 48 | `prompt` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '对AI的预置角色要求', 49 | `start_time` timestamp NULL DEFAULT NULL COMMENT '服务开始时间', 50 | `end_time` timestamp NULL DEFAULT NULL COMMENT '服务终止时间', 51 | `my_remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '称呼', 52 | `remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称', 53 | `signature` mediumtext COLLATE utf8mb4_unicode_ci COMMENT '昵称', 54 | `province` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称', 55 | `city` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称', 56 | `country` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称', 57 | `is_active` int NOT NULL DEFAULT '0' COMMENT '是否付费用户', 58 | `ai_active` int NOT NULL DEFAULT '0' COMMENT '是否允许AI处理消息', 59 | `user_story` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户需求', 60 | `role_id` int DEFAULT '1009' COMMENT '机器人', 61 | `mark` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称', 62 | `ai_reply` int NOT NULL DEFAULT '0' COMMENT '是否允许AI回复消息', 63 | PRIMARY KEY (`id`) 64 | ) ENGINE=InnoDB AUTO_INCREMENT=8816 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 65 | 66 | -- ---------------------------- 67 | -- Table structure for ai_friends_task 68 | -- ---------------------------- 69 | DROP TABLE IF EXISTS `ai_friends_task`; 70 | CREATE TABLE `ai_friends_task` ( 71 | `id` int NOT NULL AUTO_INCREMENT, 72 | `friend_id` int NOT NULL COMMENT '关联friend得ID', 73 | `task_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, 74 | `cron_expression` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '标准cron表达式', 75 | `execution_count` int DEFAULT '0' COMMENT '执行次数', 76 | `start_date` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '任务生效开始日期', 77 | `end_date` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '任务生效结束日期', 78 | `is_active` tinyint(1) DEFAULT '1' COMMENT '是否激活', 79 | `next_execution_time` datetime DEFAULT NULL COMMENT '下次执行时间', 80 | `last_execution_time` datetime DEFAULT NULL COMMENT '上次执行时间', 81 | `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 82 | `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 83 | `require_content` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '任务要求', 84 | `message_content` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 85 | `error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, 86 | `task_content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '任务内容', 87 | `cron_text` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, 88 | PRIMARY KEY (`id`) 89 | ) ENGINE=InnoDB AUTO_INCREMENT=6532 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 90 | 91 | -- ---------------------------- 92 | -- Table structure for ai_friends_task_record 93 | -- ---------------------------- 94 | DROP TABLE IF EXISTS `ai_friends_task_record`; 95 | CREATE TABLE `ai_friends_task_record` ( 96 | `id` int NOT NULL AUTO_INCREMENT, 97 | `friend_id` int DEFAULT NULL, 98 | `task_name` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '任务名称', 99 | `result` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '完成结果', 100 | `summarize` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '总结', 101 | `suggestion` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '建议', 102 | `date` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '日期', 103 | `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 104 | `score` int DEFAULT NULL COMMENT '得分', 105 | PRIMARY KEY (`id`) 106 | ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 107 | 108 | -- ---------------------------- 109 | -- Table structure for ai_logs 110 | -- ---------------------------- 111 | DROP TABLE IF EXISTS `ai_logs`; 112 | CREATE TABLE `ai_logs` ( 113 | `id` int NOT NULL AUTO_INCREMENT, 114 | `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 115 | `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '用户消息', 116 | `reson` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '推理过程', 117 | `msg` text COLLATE utf8mb4_unicode_ci COMMENT '全部消息', 118 | `reply` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '回复消息', 119 | PRIMARY KEY (`id`), 120 | KEY `idx_create_time` (`create_time`), 121 | KEY `idx_text_prefix` (`text`(100)) 122 | ) ENGINE=InnoDB AUTO_INCREMENT=24038 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; 123 | 124 | -- ---------------------------- 125 | -- Table structure for ai_messages 126 | -- ---------------------------- 127 | DROP TABLE IF EXISTS `ai_messages`; 128 | CREATE TABLE `ai_messages` ( 129 | `id` bigint NOT NULL AUTO_INCREMENT, 130 | `from_user` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 131 | `to_user` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 132 | `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, 133 | `msg_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 134 | `status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'queued', 135 | `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据库时间', 136 | `update_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据库时间', 137 | `is_ai` tinyint DEFAULT '0' COMMENT '=1 是ai =0人工', 138 | `type` tinyint DEFAULT '0' COMMENT '=1:发送,=0接收', 139 | `msg_type` int DEFAULT NULL COMMENT '1 文字 3 图片 34 语音', 140 | `create_time` bigint DEFAULT NULL COMMENT '消息时间 微信端', 141 | `raw_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '原始消息内容', 142 | PRIMARY KEY (`id`), 143 | KEY `idx_from_user` (`from_user`), 144 | KEY `idx_to_user` (`to_user`), 145 | KEY `idx_status` (`status`), 146 | KEY `idx_create_at` (`create_at`), 147 | KEY `idx_user_pair` (`from_user`,`to_user`), 148 | KEY `idx_time_status` (`create_at`,`status`) 149 | ) ENGINE=InnoDB AUTO_INCREMENT=113598 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 150 | 151 | -- ---------------------------- 152 | -- Table structure for ai_roles 153 | -- ---------------------------- 154 | DROP TABLE IF EXISTS `ai_roles`; 155 | CREATE TABLE `ai_roles` ( 156 | `id` int NOT NULL AUTO_INCREMENT, 157 | `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '角色名称', 158 | `prompt` text COLLATE utf8mb4_unicode_ci COMMENT '此角色的prompt', 159 | `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 160 | `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 161 | `is_system` tinyint DEFAULT '0', 162 | PRIMARY KEY (`id`) 163 | ) ENGINE=InnoDB AUTO_INCREMENT=1012 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 164 | 165 | -- ---------------------------- 166 | -- Table structure for ai_system 167 | -- ---------------------------- 168 | DROP TABLE IF EXISTS `ai_system`; 169 | CREATE TABLE `ai_system` ( 170 | `id` int NOT NULL AUTO_INCREMENT, 171 | `key` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 172 | `value` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 173 | `desc` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述', 174 | PRIMARY KEY (`id`) 175 | ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 176 | 177 | -- ---------------------------- 178 | -- Table structure for ai_task_logs 179 | -- ---------------------------- 180 | DROP TABLE IF EXISTS `ai_task_logs`; 181 | CREATE TABLE `ai_task_logs` ( 182 | `id` int NOT NULL AUTO_INCREMENT, 183 | `task_id` int DEFAULT NULL, 184 | `task_name` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 185 | `status` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 186 | `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 187 | PRIMARY KEY (`id`) 188 | ) ENGINE=InnoDB AUTO_INCREMENT=15941 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 189 | 190 | -- ---------------------------- 191 | -- Table structure for ai_wechat_account 192 | -- ---------------------------- 193 | DROP TABLE IF EXISTS `ai_wechat_account`; 194 | CREATE TABLE `ai_wechat_account` ( 195 | `id` int NOT NULL AUTO_INCREMENT, 196 | `auth_key` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备码', 197 | `wx_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '当前登录得微信id', 198 | `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 199 | `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间', 200 | `auth_key_remaining_time` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备码剩余时间', 201 | `avatar` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '微信头像', 202 | `nickname` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '微信昵称', 203 | `friend_count` bigint DEFAULT NULL COMMENT '好友数量', 204 | `onlineTime` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '本次在线', 205 | `totalOnline` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '累计在线', 206 | `onlineDays` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '在线天数', 207 | `expiryTime` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '过期时间', 208 | `loginErrMsg` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '在线状态描述', 209 | `alias` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '微信号', 210 | `ws_status` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT 'WS连接状态', 211 | PRIMARY KEY (`id`) 212 | ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 213 | 214 | SET FOREIGN_KEY_CHECKS = 1; 215 | -------------------------------------------------------------------------------- /src/controller/wechat.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js') 2 | const rp = require('request-promise') 3 | 4 | const adminKey = think.config('wechatpadpro.adminKey') 5 | const hostUrl = think.config('wechatpadpro.hostUrl') 6 | 7 | const genAuthKey1 = think.config('wechatpadpro.genAuthKey1') 8 | const GetLoginQrCodeNew = think.config('wechatpadpro.GetLoginQrCodeNew') 9 | const CheckLoginStatus = think.config('wechatpadpro.CheckLoginStatus') 10 | const GetFriendList = think.config('wechatpadpro.GetFriendList') 11 | //Message 12 | const SendTextMessage = think.config('wechatpadpro.SendTextMessage') 13 | 14 | module.exports = class extends Base { 15 | async deleteAuthKeyAction() { 16 | let key = this.get('key') 17 | if (!key) { 18 | return this.fail(500, 'key不能为空') 19 | } 20 | var options = { 21 | method: 'POST', 22 | uri: 23 | hostUrl + 24 | think.config('wechatpadpro.DeleteAuthKey') + 25 | '?key=' + 26 | adminKey, 27 | body: { 28 | Key: key, 29 | Opt: 0, 30 | }, 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | json: true, 35 | } 36 | let data = await rp(options) 37 | console.log('设备删除结果==》', data) 38 | if (data.Code == '200') { 39 | await this.model('wechat_account').where({ auth_key: key }).delete() 40 | return this.success() 41 | } else { 42 | return this.fail(500, '请联系管理员') 43 | } 44 | } 45 | async delayAuthKeyAction() { 46 | let key = this.get('key') 47 | if (!key) { 48 | return this.fail(500, 'key不能为空') 49 | } 50 | var options = { 51 | method: 'POST', 52 | uri: 53 | hostUrl + 54 | think.config('wechatpadpro.DelayAuthKey') + 55 | '?key=' + 56 | adminKey, 57 | body: { 58 | Days: 30, 59 | ExpiryDate: '', 60 | AuthKey: key, 61 | }, 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | json: true, 66 | } 67 | try { 68 | let data = await rp(options) 69 | console.log('设备延期=》', data) 70 | if (data.Code == '200') { 71 | return this.success() 72 | } else { 73 | return this.fail(500, '请联系管理员') 74 | } 75 | } catch (error) { 76 | console.error('延长授权码失败:', error) 77 | return this.fail(500, '延长授权码失败,请稍后再试') 78 | } 79 | } 80 | async logOutAction() { 81 | let key = this.get('key') 82 | if (!key) { 83 | return this.fail(500, 'key不能为空') 84 | } 85 | let url = hostUrl + think.config('wechatpadpro.LogOut') + '?key=' + key 86 | const options = { 87 | method: 'GET', 88 | url: url, 89 | headers: { 90 | 'Content-Type': 'application/json; charset=utf-8', 91 | }, 92 | } 93 | let data = await rp(options) 94 | data = JSON.parse(data) 95 | if (data.Code == '200') { 96 | await this.model('wechat').loginOutUpdate(key) 97 | return this.success('退出成功') 98 | } else { 99 | return this.fail(500, '退出失败') 100 | } 101 | } 102 | //生成授权码 103 | async genNewAuthKey1Action() { 104 | // http://192.168.110.85:8059/admin/GenAuthKey2 生成授权码(新设备) 105 | var options = { 106 | method: 'POST', 107 | uri: hostUrl + genAuthKey1 + '?key=' + adminKey, 108 | body: { 109 | Count: 1, 110 | Days: 90, 111 | }, 112 | headers: { 113 | 'Content-Type': 'application/json', 114 | }, 115 | json: true, 116 | } 117 | let data = await rp(options) 118 | console.log(data) 119 | if (data.Code == '200') { 120 | let key = data.Data.authKeys[0] 121 | let res = await this.model('wechat_account').add({ 122 | auth_key: key, 123 | }) 124 | return this.success(data.Data[0]) 125 | } else { 126 | return this.fail(500, '请联系管理员') 127 | } 128 | } 129 | 130 | async genAuthKey1Action() { 131 | // http://192.168.110.85:8059/admin/GenAuthKey2 生成授权码(新设备) 132 | var options = { 133 | method: 'POST', 134 | uri: hostUrl + genAuthKey1 + '?key=' + adminKey, 135 | body: { 136 | Count: 1, 137 | Days: 90, 138 | }, 139 | headers: { 140 | 'Content-Type': 'application/json', 141 | }, 142 | json: true, 143 | } 144 | let data = await rp(options) 145 | if (data.Code == '200') { 146 | let key = data.Data[0] 147 | let res = await this.model('wechat_account').add({ 148 | auth_key: key, 149 | }) 150 | return this.success(data.Data[0]) 151 | } else { 152 | return this.fail(500, '请联系管理员') 153 | } 154 | } 155 | 156 | async getProfileInfoAction() { 157 | ///user/GetProfile 获取个人资料信息 158 | let key = this.get('key') 159 | if (!key) { 160 | return this.fail(500, 'key不能为空') 161 | } 162 | let url = hostUrl + think.config('wechatpadpro.GetProfile') + '?key=' + key 163 | const options = { 164 | method: 'GET', 165 | url: url, 166 | headers: { 167 | 'Content-Type': 'application/json; charset=utf-8', 168 | }, 169 | } 170 | let data = await rp(options) 171 | data = JSON.parse(data) 172 | // console.log(data) 173 | if (data.Code == '200') { 174 | let map = { 175 | wx_id: data.Data.userInfo.userName.str, 176 | nickname: data.Data.userInfo.nickName.str, 177 | avatar: data.Data.userInfoExt.smallHeadImgUrl, 178 | alias: data.Data.userInfo.alias, 179 | } 180 | let res = await this.model('wechat_account') 181 | .where({ auth_key: key }) 182 | .update(map) 183 | return this.success(res) 184 | } else { 185 | return this.fail(500, '同步失败') 186 | } 187 | } 188 | 189 | //http://192.168.110.85:8059/login/GetLoginQrCodeNew 获取登录二维码(异地IP用代理) 190 | async getLoginCodeAction() { 191 | let key = this.get('key') 192 | if (!key) { 193 | return this.fail(500, 'key不能为空') 194 | } 195 | var options = { 196 | method: 'POST', 197 | uri: hostUrl + GetLoginQrCodeNew + '?key=' + key, 198 | body: { 199 | Check: false, 200 | Proxy: '', 201 | }, 202 | headers: { 203 | 'Content-Type': 'application/json', 204 | }, 205 | json: true, 206 | } 207 | let data = await rp(options) 208 | console.log(data) 209 | 210 | if (data.Code == '200') { 211 | let QrCodeUrl = data.Data.QrCodeUrl 212 | return this.success(QrCodeUrl) 213 | } else { 214 | return this.fail(500, '请联系管理员') 215 | } 216 | } 217 | //http://192.168.110.85:8059/login/CheckLoginStatus 检测扫码状态 218 | async checkLoginStatusAction() { 219 | let key = this.get('key') 220 | if (!key) { 221 | return this.fail(500, 'key不能为空') 222 | } 223 | let url = hostUrl + CheckLoginStatus + '?key=' + key 224 | const options = { 225 | method: 'GET', 226 | url: url, 227 | headers: { 228 | 'Content-Type': 'application/json; charset=utf-8', 229 | }, 230 | } 231 | let data = await rp(options) 232 | console.log('检测扫码状态', data) 233 | data = JSON.parse(data) 234 | 235 | if (data.Code == '200') { 236 | // { 237 | // "Code": 200, 238 | // "Data": { 239 | // "uuid": "Y8zbtWCwg3I1GCH34y7G", 240 | // "state": 2, 241 | // "wxid": "ai374949369", 242 | // "wxnewpass": "extdevnewpwd_CiNBYmMycTRhN2VlY2x4VWdDR29IMkRpV0RAcXJ0aWNrZXRfMBJAUUZvdkJvYmxMUVVOcVRaTnh6OXRlQnQwcWxMaTVjWkdiNnVrQjY2RW1YQk9VSnVjM3NoTGtQM2Q1M3RKN3FJRxoYZ1NlVlZVbVJoMkZiM1FUb1RobUc4ajV5", 243 | // "head_img_url": "http://wx.qlogo.cn/mmhead/ver_1/LeSMEExy6b445TfBsn1uHXywc1zIzhstq7PialEa8pHLERHeZo8SOBRPt2BDHjKVz5vMxdxFGz6ZOe646ghzKlw/0", 244 | // "push_login_url_expired_time": 604200, 245 | // "nick_name": "Lofter 杨", 246 | // "effective_time": 198, 247 | // "unknow": 671104083, 248 | // "device": "android", 249 | // "ret": 0, 250 | // "othersInServerLogin": false, 251 | // "tarGetServerIp": "", 252 | // "uuId": "", 253 | // "msg": "" 254 | // }, 255 | // "Text": "" 256 | // } 257 | let map = { 258 | auth_key: key, 259 | wx_id: data.Data.wxid, 260 | nickname: data.Data.nick_name, 261 | avatar: data.Data.head_img_url, 262 | auth_key_remaining_time: data.Data.effective_time, 263 | } 264 | let where = await this.model('wechat_account') 265 | .where({ 266 | wx_id: data.Data.wxid, 267 | }) 268 | .find() 269 | let res = 0 270 | if (where.id > 0) { 271 | res = await this.model('wechat_account') 272 | .where({ 273 | wx_id: data.Data.wxid, 274 | }) 275 | .update(map) 276 | } else { 277 | res = await this.model('wechat_account').add(map) 278 | } 279 | return this.success(res) 280 | } else { 281 | return this.fail(500, data) 282 | } 283 | } 284 | 285 | async getContactListAction() { 286 | let key = this.get('key') 287 | if (!key) { 288 | return this.fail(500, 'key不能为空') 289 | } 290 | let url = 291 | hostUrl + think.config('wechatpadpro.GetContactList') + '?key=' + key 292 | const options = { 293 | method: 'GET', 294 | url: url, 295 | headers: { 296 | 'Content-Type': 'application/json; charset=utf-8', 297 | }, 298 | } 299 | let data = await rp(options) 300 | data = JSON.parse(data) 301 | console.log('获取联系人列表', data) 302 | if (data.Code == '200') { 303 | return this.success() 304 | } else { 305 | return this.fail(500, data) 306 | } 307 | } 308 | async getFriendListAction() { 309 | let key = this.get('key') 310 | if (!key) { 311 | return this.fail(500, 'key不能为空') 312 | } 313 | let url = hostUrl + GetFriendList + '?key=' + key 314 | const options = { 315 | method: 'GET', 316 | url: url, 317 | headers: { 318 | 'Content-Type': 'application/json; charset=utf-8', 319 | }, 320 | } 321 | let data = await rp(options) 322 | data = JSON.parse(data) 323 | console.log('获取好友列表', data) 324 | if (data.Code == '200') { 325 | let count = data.Data.count 326 | let IsInitFinished = data.Data.IsInitFinished 327 | let friendList = data.Data.friendList 328 | 329 | await this.model('wechat_account') 330 | .where({ 331 | auth_key: key, 332 | }) 333 | .update({ 334 | friend_count: count, 335 | }) 336 | let wechatInfo = await this.model('wechat_account') 337 | .where({ 338 | auth_key: key, 339 | }) 340 | .find() 341 | if (wechatInfo.id > 0) { 342 | let newList = [] 343 | for (let item of friendList) { 344 | let map = { 345 | account_id: wechatInfo.id, 346 | wxid: item.userName.str || '', 347 | nickname: item.nickName.str || '', 348 | avatar: item.smallHeadImgUrl || '', 349 | remark: item.remark.str || '', 350 | signature: item.signature || '', 351 | province: item.province || '', 352 | city: item.city || '', 353 | country: item.country || '', 354 | } 355 | let isHave = await this.model('friends') 356 | .where({ 357 | account_id: wechatInfo.id, 358 | wxid: item.userName.str, 359 | }) 360 | .find() 361 | if (isHave.id > 0) { 362 | await this.model('friends') 363 | .where({ 364 | id: isHave.id, 365 | }) 366 | .update(map) 367 | } else { 368 | // await this.model('friends').add(map) 369 | newList.push(map) 370 | } 371 | } 372 | if (newList.length > 0) { 373 | let res = await this.model('friends').addMany(newList) 374 | } 375 | } 376 | return this.success(count) 377 | } else { 378 | return this.fail(500, data) 379 | } 380 | } 381 | 382 | async sendTextMessageAction(params) { 383 | let key, wxid, content 384 | 385 | if (params) { 386 | // 内部直接调用 387 | key = params.key 388 | wxid = params.wxid 389 | content = params.content 390 | } else { 391 | // HTTP 请求调用 392 | key = this.get('key') 393 | wxid = this.get('wxid') 394 | content = this.get('content') 395 | } 396 | 397 | if (!key) { 398 | console.log('没有key') 399 | 400 | return this.fail(500, 'key不能为空') 401 | } 402 | if (!wxid) { 403 | return this.fail(500, 'wxid不能为空') 404 | } 405 | if (!content) { 406 | return this.fail(500, 'content不能为空') 407 | } 408 | 409 | const wechatService = think.service('wechat') 410 | const result = await wechatService.sendTextMessage({ 411 | key: key, 412 | wxid: wxid, 413 | content: content, 414 | }) 415 | return this.success(result) 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /src/controller/websocket.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js') 2 | const rp = require('request-promise') 3 | const moment = require('moment') // 添加moment处理时间 4 | 5 | //Message 6 | const SendTextMessage = '/message/SendTextMessage' 7 | 8 | class MessageProcessor { 9 | constructor(model) { 10 | this.model = model 11 | this.messageQueue = new Map() 12 | this.processingTimers = new Map() 13 | this.delayTime = 10000 // 20秒延迟 14 | } 15 | 16 | addMessage(wxid, message) { 17 | // 初始化用户队列 18 | if (!this.messageQueue.has(wxid)) { 19 | this.messageQueue.set(wxid, { 20 | messages: [], 21 | lastReceived: Date.now(), 22 | }) 23 | } 24 | 25 | const userQueue = this.messageQueue.get(wxid) 26 | 27 | // 添加新消息 28 | userQueue.messages.push({ 29 | content: message.content?.str || '', 30 | create_time: message.create_time || Date.now(), 31 | raw: message, // 保存原始消息 32 | }) 33 | 34 | userQueue.lastReceived = Date.now() 35 | 36 | // 重置处理定时器 37 | this.resetProcessingTimer(wxid, message) 38 | } 39 | 40 | resetProcessingTimer(wxid, message) { 41 | // 清除现有定时器 42 | if (this.processingTimers.has(wxid)) { 43 | clearTimeout(this.processingTimers.get(wxid)) 44 | } 45 | 46 | // 设置新的定时器 47 | const timer = setTimeout(async () => { 48 | await this.processUserMessages(wxid, message) 49 | }, this.delayTime) 50 | 51 | this.processingTimers.set(wxid, timer) 52 | } 53 | 54 | async processUserMessages(wxid, msg) { 55 | if (!this.messageQueue.has(wxid)) return 56 | 57 | const userQueue = this.messageQueue.get(wxid) 58 | const messages = [...userQueue.messages] // 复制消息数组 59 | 60 | // 清理队列 61 | this.messageQueue.delete(wxid) 62 | this.processingTimers.delete(wxid) 63 | 64 | // 按时间排序消息 65 | messages.sort((a, b) => a.create_time - b.create_time) 66 | 67 | // 合并消息内容 68 | const combinedContent = messages.map((m) => m.content).join('\n') 69 | 70 | console.log(`处理 ${wxid} 的 ${messages.length} 条消息:`, combinedContent) 71 | 72 | try { 73 | // 检查好友激活状态 74 | const checkActive = await this.model('friends') 75 | .where({ 76 | wxid: wxid, 77 | is_active: 1, 78 | ai_active: 1, 79 | }) 80 | .find() 81 | console.log('combinedContent==》', combinedContent) 82 | if (checkActive.id > 0) { 83 | // 获取AI回复 84 | const aiResponse = await think 85 | .service('ai') 86 | .getAIReplyWithUserChat(combinedContent, checkActive) 87 | console.log('aiResponse==》', aiResponse) 88 | 89 | if (aiResponse && checkActive.ai_reply == 1) { 90 | const delay = (ms) => 91 | new Promise((resolve) => setTimeout(resolve, ms)) 92 | 93 | for (let item of aiResponse) { 94 | if (item) { 95 | // 发送消息 96 | let wechatAccount = await this.model('wechat_account') 97 | .where({ 98 | wx_id: msg.to_user_name.str, 99 | }) 100 | .find() 101 | console.log('发送消息==》', { 102 | key: wechatAccount.auth_key, 103 | wxid: wxid, 104 | content: item, 105 | }) 106 | await this.controllerRef.sendTextMessageAction({ 107 | key: wechatAccount.auth_key, 108 | wxid: wxid, 109 | content: item, 110 | from: wechatAccount.wx_id, 111 | }) 112 | 113 | if (aiResponse.indexOf(item) < aiResponse.length - 1) { 114 | await delay(2000) 115 | } 116 | } 117 | } 118 | // 更新消息状态为已处理 119 | await this.updateMessagesStatus(messages, 'processed') 120 | } else { 121 | console.log('AI回复为空或未启用AI回复') 122 | // 更新消息状态为未处理 123 | await this.updateMessagesStatus(messages, 'processed') 124 | } 125 | } 126 | } catch (error) { 127 | console.error(`处理用户 ${wxid} 消息失败:`, error) 128 | // 更新消息状态为失败 129 | await this.updateMessagesStatus(messages, 'failed') 130 | } 131 | } 132 | 133 | async updateMessagesStatus(messages, status) { 134 | const messageIds = messages.map((m) => m.raw.msg_id) // 假设原始消息有msg_id 135 | 136 | if (messageIds.length > 0) { 137 | try { 138 | await this.model('messages') 139 | .where({ msg_id: ['IN', messageIds] }) 140 | .update({ status: status }) 141 | } catch (e) { 142 | console.error('更新消息状态失败:', e) 143 | } 144 | } 145 | } 146 | } 147 | const WebSocketManager = require('../service/websocket') 148 | 149 | module.exports = class extends Base { 150 | async __before() { 151 | // 初始化WebSocket管理器 152 | if (!this.wsManager) { 153 | this.wsManager = think.service('websocket') || WebSocketManager 154 | // console.log('使用的 WebSocketManager 实例:', this.wsManager) 155 | } 156 | // this.wsManager = this.service('websocket') 157 | // 初始化消息处理器 158 | if (!this.messageProcessor) { 159 | this.messageProcessor = new MessageProcessor(this.model.bind(this)) 160 | this.messageProcessor.controllerRef = this // 设置控制器引用 161 | } 162 | } 163 | 164 | async _updateWsStatus() { 165 | try { 166 | // 获取所有微信账号 167 | const accounts = await this.model('wechat_account') 168 | .field('auth_key, id, wx_id, ws_status') 169 | .select() 170 | 171 | // 调试:打印当前所有连接 172 | // console.log('所有连接:', this.wsManager.connections.keys()) 173 | 174 | const statusUpdates = [] 175 | const results = [] 176 | 177 | // 检查每个账号的状态 178 | for (const account of accounts) { 179 | const url = think.config('wechatpadpro.wsUrl') + account.auth_key 180 | 181 | // 检查连接是否存在 182 | const stateName = this.wsManager.getConnectionState(url) 183 | const stateCode = this.mapStateToCode(stateName) 184 | 185 | // 记录结果 186 | results.push({ 187 | account_id: account.id, 188 | auth_key: account.auth_key, 189 | url, 190 | state: stateName, 191 | stateCode, 192 | currentDBStatus: account.ws_status, 193 | }) 194 | 195 | // 如果状态变化则更新数据库 196 | if (account.ws_status !== stateCode) { 197 | if (think.config('isDebuger')) { 198 | console.log('调试状态,不修改ws连接状态值') 199 | } else { 200 | await this.model('wechat_account') 201 | .where({ id: account.id }) 202 | .update({ ws_status: stateCode }) 203 | console.log( 204 | `更新账号 ${account.wx_id} 状态: ${account.ws_status} -> ${stateCode}` 205 | ) 206 | } 207 | } 208 | } 209 | 210 | return results 211 | } catch (error) { 212 | console.error('检查WebSocket状态时出错:', error) 213 | return [] 214 | } 215 | } 216 | 217 | // 状态映射到数据库存储的数值 218 | mapStateToCode(stateName) { 219 | const statesMap = { 220 | CONNECTING: 0, 221 | OPEN: 1, 222 | CLOSING: 2, 223 | CLOSED: 3, 224 | NOT_FOUND: 3, 225 | ERROR: 4, 226 | } 227 | return statesMap[stateName] !== undefined ? statesMap[stateName] : 3 228 | } 229 | 230 | async checkWsActiveAction() { 231 | const results = await this._updateWsStatus() 232 | return this.success(results) 233 | // 234 | } 235 | 236 | async connectTestAction() { 237 | await this.connectAction('838071e7-5bf4-4767-81df-16d87bc69907') 238 | } 239 | 240 | /** 241 | * 创建WebSocket连接 242 | */ 243 | async connectAction(para) { 244 | let auth_key = this.get('key') 245 | if (para) { 246 | auth_key = para 247 | } 248 | if (!auth_key) { 249 | return this.fail('key不能为空') 250 | } 251 | const url = think.config('wechatpadpro.wsUrl') + auth_key 252 | 253 | // 创建消息处理器 254 | const handlers = { 255 | onMessage: (data) => { 256 | try { 257 | const message = JSON.parse(data) 258 | 259 | // 处理消息逻辑 260 | this.handleWsMessage(message, auth_key) 261 | } catch (e) { 262 | console.error('消息解析失败:', e) 263 | } 264 | }, 265 | 266 | onError: (error) => { 267 | console.error('连接错误:', error) 268 | }, 269 | 270 | onClose: (code, reason) => { 271 | console.log(`连接关闭: ${code} - ${reason}`) 272 | }, 273 | } 274 | 275 | // 获取或创建连接 276 | const ws = this.wsManager.getConnection(url, handlers) 277 | 278 | await this.model('wechat_account') 279 | .where({ auth_key: auth_key }) 280 | .update({ ws_status: 1 }) 281 | 282 | return this.success({ 283 | message: 'WebSocket连接已启动', 284 | url, 285 | state: this.wsManager.getStateName('OPEN'), 286 | }) 287 | } 288 | 289 | /** 290 | * 处理WebSocket消息 291 | * @param {Object} message 消息对象 292 | */ 293 | async handleWsMessage(message, auth_key) { 294 | const from_user_name = message.from_user_name?.str 295 | const to_user_name = message.to_user_name?.str 296 | console.log('处理消息==》', message) 297 | if (!from_user_name) return 298 | try { 299 | let isSave = await this.model('messages') 300 | .where({ 301 | msg_id: message.msg_id, 302 | }) 303 | .find() 304 | if (isSave.id > 0) { 305 | return 306 | } 307 | 308 | let checkMsgNeed = await this.checkMsgNeedHandle( 309 | from_user_name, 310 | to_user_name, 311 | auth_key 312 | ) 313 | 314 | if (checkMsgNeed == 0) { 315 | return 316 | } else { 317 | if (message.msg_type != 51) { 318 | await this.saveMessageToDB(message, checkMsgNeed == 1 ? 1 : 0) 319 | } 320 | if (checkMsgNeed == 3) { 321 | console.log('收到会员消息==》', message.content?.str || '') 322 | 323 | this.messageProcessor.addMessage(from_user_name, message) 324 | } 325 | } 326 | } catch (error) { 327 | console.error('消息处理失败:', error) 328 | } 329 | } 330 | async checkMsgNeedHandle(from_user_name, to_user_name, auth_key) { 331 | let checkSendAccount = await this.model('wechat_account') 332 | .where({ 333 | wx_id: from_user_name, 334 | auth_key: auth_key, 335 | }) 336 | .find() 337 | //自己发的 无需AI处理,保存记录即可 338 | if (checkSendAccount.id) { 339 | return 1 340 | } 341 | let checkGetAccount = await this.model('friends') 342 | .where({ 343 | wxid: from_user_name, 344 | is_active: 1, 345 | }) 346 | .find() 347 | 348 | //收到会员消息 但是没有AI托管 349 | if (checkGetAccount.id > 0 && checkGetAccount.ai_active == 0) { 350 | return 2 351 | } 352 | 353 | //收到会员消息 但是AI托管 354 | if (checkGetAccount.id > 0 && checkGetAccount.ai_active == 1) { 355 | return 3 356 | } 357 | return 0 358 | } 359 | 360 | async saveMessageToDB(message, type) { 361 | const from_user_name = message.from_user_name?.str 362 | const content = message.content?.str || '' 363 | const create_time = message.create_time || Date.now() 364 | const to_user_name = message.to_user_name?.str || '' 365 | const msg_id = message.msg_id 366 | try { 367 | let map = { 368 | type: type, 369 | msg_type: message.msg_type, 370 | msg_id: msg_id, 371 | from_user: from_user_name, 372 | content: content, 373 | create_time: create_time, 374 | status: type == 1 ? 'done' : 'queued', // 标记为排队中 375 | to_user: to_user_name, 376 | raw_data: JSON.stringify(message), // 保存原始数据 377 | is_ai: 0, //1是AI回复的,0是人工回复的 378 | } 379 | let check = await this.model('messages').where({ msg_id: msg_id }).find() 380 | if (check.id > 0) { 381 | return 0 382 | } else { 383 | if (think.isEmpty(map.content)) { 384 | return 0 385 | } 386 | const insertId = await this.model('messages').add(map) 387 | return insertId 388 | } 389 | } catch (dbError) { 390 | console.error('保存消息到数据库失败:', dbError) 391 | throw dbError 392 | } 393 | } 394 | async testTimeAction() { 395 | let res = this.isNowInRange('2025-07-09 10:50:00', '2025-07-09 01:45:00') 396 | return this.success(res) 397 | } 398 | isNowInRange(start, end) { 399 | const now = moment() 400 | const startDate = start ? moment(start) : null 401 | const endDate = end ? moment(end) : null 402 | 403 | // 如果是日期格式(没有时间部分),设置最大结束时间 404 | if (endDate && end.length === 10) { 405 | endDate.endOf('day') 406 | } 407 | 408 | return ( 409 | (!startDate || now.isSameOrAfter(startDate)) && 410 | (!endDate || now.isSameOrBefore(endDate)) 411 | ) 412 | } 413 | /** 414 | * 发送文本消息 415 | */ 416 | async sendTextMessageAction(params) { 417 | // 实际发送消息的实现... 418 | let key, wxid, content, from 419 | 420 | if (params) { 421 | // 内部直接调用 422 | key = params.key 423 | wxid = params.wxid 424 | content = params.content 425 | from = params.from 426 | } else { 427 | // HTTP 请求调用 428 | key = this.get('key') 429 | wxid = this.get('wxid') 430 | content = this.get('content') 431 | from = ( 432 | await this.model('wechat_account').where({ auth_key: key }).find() 433 | ).wx_id 434 | } 435 | 436 | if (!key) { 437 | console.log('没有key') 438 | 439 | return this.fail(500, 'key不能为空') 440 | } 441 | if (!wxid) { 442 | return this.fail(500, 'wxid不能为空') 443 | } 444 | if (!content) { 445 | return this.fail(500, 'content不能为空') 446 | } 447 | 448 | const wechatService = think.service('wechat') 449 | const result = await wechatService.sendTextMessage({ 450 | key: key, 451 | wxid: wxid, 452 | content: content, 453 | }) 454 | 455 | await this.model('messages').add({ 456 | from_user: from, 457 | to_user: wxid, 458 | content: content, 459 | is_ai: 1, 460 | }) 461 | 462 | return 463 | } 464 | 465 | /** 466 | * 关闭指定连接 467 | */ 468 | async closeAction(para) { 469 | let auth_key = this.get('key') 470 | if (para) { 471 | auth_key = para 472 | } 473 | if (!auth_key) { 474 | return this.fail('key不能为空') 475 | } 476 | const url = think.config('wechatpadpro.wsUrl') + auth_key 477 | 478 | const result = this.wsManager.closeConnection(url) 479 | await this.model('wechat_account') 480 | .where({ auth_key: auth_key }) 481 | .update({ ws_status: 0 }) 482 | return result 483 | ? this.success('连接关闭指令已发送') 484 | : this.fail('连接不存在或已关闭') 485 | } 486 | 487 | /** 488 | * 获取所有连接状态 489 | */ 490 | async statusAction() { 491 | const connections = this.wsManager.getConnectionsStatus() 492 | return this.success(connections) 493 | } 494 | 495 | /** 496 | * 关闭所有连接 497 | */ 498 | async closeAllAction() { 499 | this.wsManager.closeAll() 500 | await this.model('wechat_account').where({ 1: 1 }).update({ ws_status: 0 }) 501 | 502 | return this.success('所有连接关闭指令已发送') 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /src/service/ai.js: -------------------------------------------------------------------------------- 1 | const rp = require('request-promise') 2 | const Base = require('../service/base') 3 | const { del } = require('request') 4 | const { ai } = require('../config/config') 5 | const { json } = require('../controller/base') 6 | 7 | module.exports = class extends think.Service { 8 | constructor() { 9 | super() 10 | // 实例化所需Model 11 | this.friendsTaskModel = think.model('friends_task') 12 | this.friendsModel = think.model('friends') 13 | this.wechatAccountModel = think.model('wechat_account') 14 | this.systemModel = think.model('system') 15 | } 16 | 17 | async getTestAIReply(info) { 18 | console.log('获取AI回复参数==>', info) 19 | let host = info.host 20 | let apiKey = info.key 21 | let model = info.model 22 | let msg = [ 23 | { 24 | role: 'user', 25 | content: info.prompt || '你好,AI!请帮我生成一段代码。', 26 | }, 27 | ] 28 | const options = { 29 | method: 'POST', 30 | uri: host, 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | Authorization: `Bearer ${apiKey}`, 34 | }, 35 | body: { 36 | model: model, 37 | messages: msg, 38 | }, 39 | json: true, 40 | } 41 | try { 42 | const result = await rp(options) 43 | console.log( 44 | '推理过程==>', 45 | result.choices[0].message.reasoning_content || '' 46 | ) 47 | console.log('回复内容==>', result.choices[0].message.content) 48 | console.log('message==>', result.choices[0].message) 49 | 50 | const lastElement = msg.at(-1) 51 | await this.saveReason( 52 | msg || [], 53 | lastElement.content || '', 54 | result.choices[0].message.reasoning_content || '', 55 | result.choices[0].message.content || '' 56 | ) 57 | 58 | return result 59 | } catch (error) { 60 | console.log('AI回复请求失败:', error) 61 | } 62 | return 63 | } 64 | 65 | async testAI(text, role_id) { 66 | if (!text) { 67 | return 68 | } 69 | let sysTip = await this.getPrompt(role_id) 70 | let msg = [ 71 | { 72 | role: 'system', 73 | content: sysTip, 74 | }, 75 | { 76 | role: 'user', 77 | content: text || '', 78 | }, 79 | ] 80 | 81 | let result = await this.getAIReply(msg) 82 | let replyContent = result.choices[0].message.content 83 | return replyContent 84 | } 85 | 86 | async getAIReplyWithHumanSummary(text, wxid, MsgList) { 87 | if (!text) { 88 | return 89 | } 90 | 91 | let newsMesList = [] 92 | for (let item of MsgList) { 93 | let newMap = { 94 | role: 'user', 95 | content: item.raw_data, 96 | } 97 | newsMesList.push(newMap) 98 | } 99 | let sysTip = (await this.getPrompt(1000)) + ' 我的账号是' + wxid 100 | newsMesList.push({ 101 | role: 'system', 102 | content: sysTip, 103 | }) 104 | 105 | newsMesList.push({ 106 | role: 'user', 107 | content: text || '', 108 | }) 109 | let result = await this.getAIReply(newsMesList) 110 | let replyContent = result.choices[0].message.content 111 | return result.choices[0].message 112 | } 113 | 114 | async getCopyModWithAI(text, ai_id, friendInfo) { 115 | if (!text) { 116 | return 117 | } 118 | let sysTip = await this.getPrompt(ai_id) 119 | 120 | let friend_id = friendInfo.id 121 | let name = friendInfo.my_remark || '' 122 | 123 | let accountInfo = await this.model('wechat_account') 124 | .where({ id: friendInfo.account_id }) 125 | .find() 126 | 127 | let newTaskList = await this.getUserTaskList(friend_id) 128 | 129 | let newsMesList = await this.getHistMsg(friendInfo.wxid, accountInfo.wx_id) 130 | 131 | let newTip = sysTip 132 | .replaceAll('{{friend_id}}', friend_id) 133 | .replaceAll('{{name}}', name) 134 | .replaceAll('{{user_story}}', friendInfo.user_story || '无') 135 | .replaceAll('{{current_time}}', this.getCurrentTimeString()) 136 | .replaceAll('{{task_list}}', JSON.stringify(newTaskList)) 137 | 138 | newsMesList.push({ 139 | role: 'system', 140 | content: newTip, 141 | }) 142 | 143 | // newsMesList.push({ 144 | // role: 'assistant', 145 | // content: '{', 146 | // }) 147 | newsMesList.push({ 148 | role: 'user', 149 | content: text || '', 150 | }) 151 | 152 | let result = await this.getAIReply(newsMesList) 153 | let replyContent = result.choices[0].message.content 154 | return replyContent 155 | } 156 | 157 | async getUserSummary(text, friendInfo) { 158 | if (!text) { 159 | return 160 | } 161 | let friend_id = friendInfo.id 162 | let name = friendInfo.my_remark || '' 163 | 164 | let accountInfo = await this.model('wechat_account') 165 | .where({ id: friendInfo.account_id }) 166 | .find() 167 | 168 | let newTaskList = await this.getUserTaskList(friend_id) 169 | 170 | let newsMesList = await this.getHistMsg( 171 | friendInfo.wxid, 172 | accountInfo.wx_id, 173 | 1 174 | ) 175 | 176 | let sysTip = await this.getPrompt(1010) 177 | 178 | let newTip = sysTip 179 | .replaceAll('{{friend_id}}', friend_id) 180 | .replaceAll('{{name}}', name) 181 | .replaceAll('{{user_story}}', friendInfo.user_story || '无') 182 | .replaceAll('{{current_time}}', this.getCurrentTimeString()) 183 | .replaceAll('{{task_list}}', JSON.stringify(newTaskList)) 184 | .replaceAll('{{chatHistory}}', JSON.stringify(newsMesList)) 185 | let msg = [] 186 | msg.push({ 187 | role: 'system', 188 | content: newTip, 189 | }) 190 | msg.push({ 191 | role: 'user', 192 | content: text || '', 193 | }) 194 | let result = await this.getAIReply(msg) 195 | let replyContent = result.choices[0].message.content 196 | 197 | replyContent = await this.safeJsonParse(replyContent) 198 | if (replyContent.error) { 199 | console.error('AI JSON 生成错误:', replyContent) 200 | return 201 | } else { 202 | let messageL = replyContent.message 203 | let summarize = replyContent.summarize 204 | 205 | console.log('summarize==>', summarize) 206 | 207 | if (summarize) { 208 | await this.recordSummary(summarize) 209 | } 210 | // await this.operateUserCron(cron, friendInfo.remark) 211 | 212 | return replyContent 213 | } 214 | } 215 | 216 | async recordSummary(summary) { 217 | try { 218 | await this.model('friends_task_record').addMany(summary) 219 | } catch (e) { 220 | console.error('记录总结失败:', e) 221 | } 222 | } 223 | 224 | async getAIReplyWithUserChat(text, friendInfo, isNeedHisMsg = true) { 225 | if (!text) { 226 | return 227 | } 228 | let friend_id = friendInfo.id 229 | let name = friendInfo.my_remark || '' 230 | 231 | let accountInfo = await this.model('wechat_account') 232 | .where({ id: friendInfo.account_id }) 233 | .find() 234 | 235 | let newTaskList = await this.getUserTaskList(friend_id) 236 | 237 | let newsMesList = [] 238 | 239 | if (isNeedHisMsg) { 240 | //需要历史消息 241 | newsMesList = await this.getHistMsg(friendInfo.wxid, accountInfo.wx_id) 242 | } 243 | 244 | let sysTip = await this.getPrompt(friendInfo.role_id || 999) 245 | console.log('消息发送时间==》', this.getCurrentTimeString()) 246 | 247 | let newTip = sysTip 248 | .replaceAll('{{friend_id}}', friend_id) 249 | .replaceAll('{{name}}', name) 250 | .replaceAll('{{user_story}}', friendInfo.user_story || '无') 251 | .replaceAll('{{current_time}}', this.getCurrentTimeString()) 252 | .replaceAll('{{task_list}}', JSON.stringify(newTaskList)) 253 | 254 | newsMesList.push({ 255 | role: 'system', 256 | content: newTip, 257 | }) 258 | 259 | newsMesList.push({ 260 | role: 'user', 261 | content: text || '', 262 | }) 263 | let result = await this.getAIReply(newsMesList) 264 | let replyContent = result.choices[0].message.content 265 | 266 | replyContent = await this.safeJsonParse(replyContent) 267 | if (replyContent.error) { 268 | console.error('AI JSON 生成错误:', replyContent) 269 | return 270 | } else { 271 | let isContinue = replyContent.continue 272 | let cron = replyContent.cron 273 | let messageL = replyContent.message 274 | 275 | await this.operateUserCron(cron, friendInfo.remark) 276 | 277 | return messageL 278 | } 279 | } 280 | 281 | // msg || [], 282 | // lastElement.content || '', 283 | // result.choices[0].message.reasoning_content || '', 284 | // result.choices[0].message.content||'' 285 | async saveReason(msg, text, reson, reply) { 286 | try { 287 | await this.model('logs').add({ 288 | msg: JSON.stringify(msg), 289 | text: text, 290 | reson: reson, 291 | reply: reply, 292 | }) 293 | } catch (e) { 294 | console.error('保存原因失败:', e) 295 | } 296 | } 297 | async safeJsonParse(str) { 298 | // 步骤1:基本清理 299 | let cleaned = str 300 | .trim() 301 | // 修复中文引号 302 | .replace(/[“”]/g, '"') 303 | // 修复中文单引号(如果有) 304 | .replace(/[‘’]/g, "'") 305 | // 修复多余逗号 306 | .replace(/,(\s*[}\]])/g, '$1') 307 | // 修复中文数字 308 | .replace(/十六/g, '16') 309 | .replace(/二十/g, '20') 310 | // 修复多余冒号 311 | .replace(/:{2,}/g, ':') 312 | // 修复日期格式错误(如 "23:00::00") 313 | .replace(/(\d{2}:\d{2})::(\d{2})/g, '$1:$2') 314 | .replace(/^```json\s*/, '') // 移除开头的```json 315 | .replace(/\s*```$/, '') // 移除结尾的``` 316 | .replace(/\n```$/, '') 317 | 318 | // 步骤2:尝试解析 319 | try { 320 | return JSON.parse(cleaned) 321 | } catch (primaryError) { 322 | console.warn('首次解析失败,尝试修复:', primaryError.message) 323 | 324 | try { 325 | // 尝试补充可能的缺失括号 326 | let fixed = cleaned 327 | const openBraces = (cleaned.match(/{/g) || []).length 328 | const closeBraces = (cleaned.match(/}/g) || []).length 329 | 330 | // 补充缺失的闭合括号 331 | if (openBraces > closeBraces) { 332 | fixed += '}'.repeat(openBraces - closeBraces) 333 | } 334 | // 补充缺失的开放括号 335 | else if (closeBraces > openBraces) { 336 | fixed = '{'.repeat(closeBraces - openBraces) + fixed 337 | } 338 | 339 | return JSON.parse(fixed) 340 | } catch (secondaryError) { 341 | // 最终错误处理和日志 342 | console.error('JSON 二次解析失败:', { 343 | originalString: str, 344 | cleanedString: cleaned, 345 | primaryError, 346 | secondaryError, 347 | }) 348 | let aiJson = await this.aiGetJsonParse(str) 349 | return aiJson 350 | /// 351 | 352 | // 返回结构化错误而非 null 353 | return { 354 | error: 'JSON_PARSE_FAILED', 355 | message: '无法解析 JSON 字符串', 356 | details: { 357 | primaryError: primaryError.message, 358 | secondaryError: secondaryError.message, 359 | }, 360 | originalString: str, 361 | cleanedString: cleaned, 362 | } 363 | } 364 | } 365 | } 366 | 367 | async aiGetJsonParse(str) { 368 | let msg = [ 369 | { 370 | role: 'system', 371 | content: 372 | '你是一个json解析器,你的任务是将用户提供的字符串解析为有效的JSON格式。如果无法解析,请返回0。请确保返回的JSON格式正确且可解析。', 373 | }, 374 | // { 375 | // role: 'assistant', 376 | // content: '{', 377 | // }, 378 | { 379 | role: 'user', 380 | content: str, 381 | }, 382 | ] 383 | let res = await this.getAIReply(msg) 384 | let replyContent = res.choices[0].message.content 385 | 386 | // 步骤1:基本清理 387 | let cleaned = replyContent 388 | .trim() 389 | // 修复中文引号 390 | .replace(/[“”]/g, '"') 391 | // 修复中文单引号(如果有) 392 | .replace(/[‘’]/g, "'") 393 | // 修复多余逗号 394 | .replace(/,(\s*[}\]])/g, '$1') 395 | // 修复中文数字 396 | .replace(/十六/g, '16') 397 | .replace(/二十/g, '20') 398 | // 修复多余冒号 399 | .replace(/:{2,}/g, ':') 400 | // 修复日期格式错误(如 "23:00::00") 401 | .replace(/(\d{2}:\d{2})::(\d{2})/g, '$1:$2') 402 | .replace(/^```json\s*/, '') // 移除开头的```json 403 | .replace(/\s*```$/, '') // 移除结尾的``` 404 | 405 | // 步骤2:尝试解析 406 | try { 407 | return JSON.parse(cleaned) 408 | } catch (primaryError) { 409 | console.error('JSON 二次解析失败:', { 410 | originalString: str, 411 | cleanedString: cleaned, 412 | primaryError, 413 | }) 414 | return { 415 | error: 'JSON_PARSE_FAILED', 416 | message: '无法解析 JSON 字符串', 417 | } 418 | } 419 | } 420 | 421 | async getPrompt(id) { 422 | let res = await this.model('roles').where({ id: id }).find() 423 | return res.prompt 424 | } 425 | getCurrentTimeString() { 426 | const weeks = [ 427 | '星期日', 428 | '星期一', 429 | '星期二', 430 | '星期三', 431 | '星期四', 432 | '星期五', 433 | '星期六', 434 | ] 435 | const now = new Date() 436 | let timeString = `${now.toLocaleDateString()} ${now.toLocaleTimeString()} ${ 437 | weeks[now.getDay()] 438 | }` 439 | return timeString 440 | } 441 | async getUserTaskList(friend_id) { 442 | let taskList = await this.model('friends_task') 443 | .where({ friend_id: friend_id, is_active: 1 }) 444 | .select() 445 | 446 | let newTaskList = [] 447 | for (let item of taskList) { 448 | let map = { 449 | id: item.id, 450 | friend_id: item.friend_id, 451 | cron_expression: item.cron_expression, 452 | task_name: item.task_name, 453 | is_active: item.is_active, 454 | end_date: item.end_date, 455 | start_date: item.start_date, 456 | } 457 | newTaskList.push(map) 458 | } 459 | return newTaskList 460 | } 461 | 462 | async getHistMsg(from, to, day) { 463 | let newsMesList = [] 464 | let hisMessagesList = await this.model('message').getHistoryChatRecord( 465 | from, 466 | to, 467 | day ? day : null 468 | ) 469 | for (let item of hisMessagesList) { 470 | let newMap = { 471 | role: item.from_user == to ? 'assistant' : 'user', 472 | content: item.create_at + ':' + item.content, 473 | } 474 | console.log('聊天记录==>', newMap) 475 | newsMesList.push(newMap) 476 | } 477 | return newsMesList 478 | } 479 | async operateUserCron(cronList, remark) { 480 | let aiParamsList = await this.systemModel 481 | .where({ 482 | key: ['IN', ['to_noti_wxid', 'from_noti_key']], 483 | }) 484 | .select() 485 | let to_noti_wxid = aiParamsList.find( 486 | (item) => item.key == 'to_noti_wxid' 487 | ).value 488 | let from_noti_key = aiParamsList.find( 489 | (item) => item.key == 'from_noti_key' 490 | ).value 491 | 492 | if (!think.isEmpty(cronList)) { 493 | let param = { 494 | key: from_noti_key, 495 | wxid: to_noti_wxid, 496 | content: 497 | '当前' + remark + '的cron任务列表已更新:' + JSON.stringify(cronList), 498 | } 499 | try { 500 | await think.service('wechat').sendTextMessage(param) 501 | } catch (error) { 502 | console.error('发送通知失败:', error) 503 | } 504 | } 505 | 506 | for (let item of cronList) { 507 | item.cron_expression = item.cron_expression.replace(/\?/g, '*') 508 | 509 | if (item.cron_expression == '* * * * * *') { 510 | item.cron_expression = '* 1 * * * *' // 511 | } 512 | // if((item.start_date == item.end_date) && item.start_date && item.end_date) { 513 | // item.end_date = item.start + ' 23:59:59' 514 | // } 515 | if (item.id == -1 && item.operate == 'add') { 516 | //新增 517 | delete item.id 518 | delete item.operate 519 | delete item.should_update 520 | await this.friendsTaskModel.add(item) 521 | } 522 | if (item.operate == 'update') { 523 | delete item.operate 524 | delete item.should_update 525 | await this.friendsTaskModel.where({ id: item.id }).update(item) 526 | } 527 | if (item.operate == 'delete') { 528 | delete item.operate 529 | delete item.should_update 530 | await this.friendsTaskModel.where({ id: item.id }).delete() 531 | } 532 | } 533 | } 534 | 535 | async getAIReply(msg) { 536 | console.log('开始请求AI回复:', msg) 537 | 538 | let aiParamsList = await this.model('system') 539 | .where({ 540 | key: ['IN', ['aiHost', 'aiApiKey', 'aiChatModel']], 541 | }) 542 | .select() 543 | let host = aiParamsList.find((item) => item.key == 'aiHost').value 544 | let apiKey = aiParamsList.find((item) => item.key == 'aiApiKey').value 545 | let model = aiParamsList.find((item) => item.key == 'aiChatModel').value 546 | const options = { 547 | method: 'POST', 548 | uri: host, 549 | headers: { 550 | 'Content-Type': 'application/json', 551 | Authorization: `Bearer ${apiKey}`, 552 | }, 553 | body: { 554 | model: model, 555 | messages: msg, 556 | }, 557 | json: true, 558 | } 559 | try { 560 | const result = await rp(options) 561 | console.log( 562 | '推理过程==>', 563 | result.choices[0].message.reasoning_content || '' 564 | ) 565 | console.log('回复内容==>', result.choices[0].message.content) 566 | console.log('message==>', result.choices[0].message) 567 | 568 | const lastElement = msg.at(-1) 569 | await this.saveReason( 570 | msg || [], 571 | lastElement.content || '', 572 | result.choices[0].message.reasoning_content || '', 573 | result.choices[0].message.content || '' 574 | ) 575 | 576 | return result 577 | } catch (error) { 578 | console.log('AI回复请求失败:', error) 579 | } 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /src/controller/message.text: -------------------------------------------------------------------------------- 1 | 收到消息: { 2 | msg_id: 455179403, 3 | from_user_name: { str: 'ai374949369' }, 4 | to_user_name: { str: 'wxid_hebkbciz5spt22' }, 5 | msg_type: 1, 6 | content: { str: '主动发送' }, 7 | status: 3, 8 | img_status: 1, 9 | img_buf: { len: 0 }, 10 | create_time: 1751707503, 11 | msg_source: '\n' + 12 | '\t1\n' + 13 | '\t1\n' + 14 | '\tN0_V1_zyvKfcou|v1_rsXPWszR\n' + 15 | '\t\n' + 16 | '\t\t\n' + 17 | '\t\n' + 18 | '\n', 19 | new_msg_id: 993082329029909800 20 | } 21 | 22 | 23 | 24 | { 25 | msg_id: 641344639, 26 | from_user_name: { str: 'wxid_hebkbciz5spt22' }, 27 | to_user_name: { str: 'ai374949369' }, 28 | msg_type: 1, 29 | content: { str: '文字内容' }, 30 | status: 3, 31 | img_status: 1, 32 | img_buf: { len: 0 }, 33 | create_time: 1751688591, 34 | msg_source: '\n' + 35 | '\t1\n' + 36 | '\t1\n' + 37 | '\tN0_V1_XDurkwKV|v1_9g4m/VF1\n' + 38 | '\t\n' + 39 | '\t\t\n' + 40 | '\t\n' + 41 | '\n', 42 | push_content: '杨 : 文字内容', 43 | new_msg_id: 3678253154489006600 44 | } 45 | 46 | 47 | # 语音 48 | 收到消息: { 49 | msg_id: 1203762387, 50 | from_user_name: { str: 'wxid_hebkbciz5spt22' }, 51 | to_user_name: { str: 'ai374949369' }, 52 | msg_type: 34, 53 | content: { 54 | str: '' 55 | }, 56 | status: 3, 57 | img_status: 1, 58 | img_buf: { 59 | len: 4415, 60 | buffer: 'AiMhU0lMS19WMwwApyt096juSeXgI3BDCwCnK3T3qZynot6/PwwApyt096mUM6ea5a2PCwCnK3T3hDZbXDn0PwsApyt096mcp6Levz8LAKcrdPepnKei3r8/CwCnK3T3qZynot6/PwsApyt096mcp6Levz8RAKc4FoLDb3U+9hJNcDq75I7XDwCpLg0QzYMaqSJDE0KgGtcPAKijua7Wh4HUZuAV01x8XxIAqOcp4q3M8bLWXtYLxsJ7oQAfEwCqKIwYQAubWjdi8ntCwfE75mT/FACpPOBk7ifIHuzAc31CpIjw/z1WfxUAqTgph7GBy7KQULrmbA+op4eQsNvfEQCpNEK0Q7o5RFIMnF/0QTkAvxIAqXDv19kZydTytsQePQZwgqcGEACpw0dqDdPi7gO4+gViZvefEQCpNEL9R2Ai9kUCu1I7wAZXvxEAqTRCQyE7qH2//FHC2XL5m98VAKk0QrhS915kdM2RYwF8L8QYtMxqTxMAqTeWaSOa6FXUzt3de6endIzSHxAAqcr22Zn/yOScJmrI7mEX5xEAqcr3LdVXSi1kOn/QFd4UKScPAKnHv3qgn2zM6pd5+MTgEhIAqcr2nwPISQ/3lxlDCQ/+7T7/EACpyvafHi8+oYtYx/bF1+1/EACptDsJMdNmG3Ktu+qdHWpfFACpNEfv/a1Grk4LTNoyhkZdrixJ/xQAqVUV2g85qXocjzM5le431u/lI78fAKS3J9tcmo0nXNUDHoUs7hF0+iUf7th6gvJnisu4gp8iALA5kNM6E6TtJ1siReduWqhgus5DKFxpRol2e5zVo30edb8dAK3+d05RcZKT1ml6fzJLP5hA+LJ4mGFqc6Pdsr9/FQCsoB6wQhGx0vu0ZhW/eHy6xMRjTGETAKuvDAxY1hFuQV6oJNRZgkT1LkcSAKqy1qpaxEYg8eWYo+qWWgn3fxIAqlQzLNAvMiMK0JbURDrkgXcfEQCqU+1gB7jDZHtFIJhOYYvYTxIAqkCBYGkf97/5HPQXhDAs0sZvEQCpPOZM35ccGC7jHTHaH1w+hw8AqTiTzCYL4JkOPA/d33ifEgCooSIbPm5DgvfaJ8wP/GJCni8UAKjChw7giSnI2e3NKYhwyKJEiEV/KQChC/EKabnV0A1Q0YV3Cm9Dv80anaqU+p2zLeWnNjN1AjI7VzDVdK3JaScAteQu3fbEGLGLJfra03EX14UnQYKu/6zZ9ustBPtvoPTjbEuZvAZ/LwCOdjbO5bCaTCGYbqoYxCvjXFOsdrQ3zl/MJ202OuvnIdX6fhGm0HsHwa8jD1ofUSUAjvI6DQhuILI5BO08El4wadC269fhrHaYYFZhK280I0NLlA4Y5yMAjqGybIBAPXFQkcHiVDuuSbxiHqQBd9yT1sVJ03Tcm4RkVG8vAI23cdhZ2B4+EJ6KyqxqrJDWp3BcQ+4dH0FLYRzwMR0Mg2jPDsPgK2SB6H791ZHnKwCSIS8HvWKOG420ONLe0tCKt2++tJQTdHCWgw3yf0dOM0Ui3n9Wz1kpyuOUNACYx+WIeYBRI7rrfkb3/QV+xzhLk6VNEB9QoIDyhQUsp3WkqeF+ieQX30fVaD4P7DwrVwd7KQC4O25d8+KmQmHt6vvDP1ObywnQ+lq8xLhhnvwQVnKa7qR8egCMtcvmnyIAuPdhQ+MPiN/LvYWDznkWUadsAZac4gQUDcWsI0KxZCyq/ygAuHCi2v9AjI30ittQLVDYFaLnXBHC8f0UI9TOx46xN5eOuBo4bfQv+ycAml7NnAeq8mJQzlaPjfSqWJJWjdedKfk65jS/FDG2HuNqoFPElrUXHQCVQegNSKSyRTY2U+bPe9K/eHWLBtgH40SIBOS/9yMAkCPzyM/U8aAAfsHznZHMj6sjmTP8nJMisB9DRl5xfWHKyKApAI/3J+Wwp/Nv9auRoHmPmoBKFxkyuY85Z/JbVfYx88LmVs5BUpPzS6x/IgCOu3WzB0v818BzMuqpxyhsqPVh0K9ubSVYyKmxHgkgX+dtLQCNt/qf3hY5ZelAzLfuRfD20IH9lhjk4J3dSzKefpF+x61VdWk5/zsSHNVOjP8gAI2erzJd1GziLzQL+PVaV+sk/JsrvgJI/T5bMMEbsTrrJgCNfDoZpIRD4/HaKJhRu4pg+pCpofRS27ASSOCg3Grtb0nP6HTqHyoAil7cO+PVz9LUuEk7MjYhKZObzjfvKlqJnlRlo5ebwKpfdD6NbE/67gbPKwCElmh4H5w+xBndUV5GIMO3JbB/dONjQ09NszCoCHzBvcU9yrhpD03e58yfKgCmq0IdBj5zhiTT7VoqSN1HRgMywsUobDnbNhz8Z5YxvlMRqICqM4unFX8kALhI4a7RTRZtRvlCFeC+mnAH43yDl3ljAp0qF2HOzXCRRnrz5yUAtjNBkhmqUjDJhysx7Q+s+ZBtq9gaaA24Pwy/xQLJl9WHO6TnTyEAtKwLdo51J0RRPCDhZCNklBGvGNOYkDFUS+k4Wg9N1h1PKQCQanq96muuKS2d9h6eaowMVzN+vGxkwGWQT2wA4Zh+txNcZQQpDcA/fykAkQIh9DL8WQGQNpzkRrwfF7ax4WmSVSNhcB0tSTbRfNpEzozkQaPHbMUgAI0SYUwRBXRCTiVq8rCr3XbxsK+OWCT6QvDK35nzzZpvJACMQsexGMiQzJ85iTOXznOLs2o0n64ESwzP1tyyVQD+XTcROv8jAIwZrwrm2N62gogO8SmtZQXxUHsyIl3zzJ6LMW3S/EJu6uyfKwCK9RijhE8jbx6PAm6Xj1ELyoI9RQRFnqZSU398GwSZHXEZoJ/GTDAq8l2TKACKtOYIrQXx+akqKn31HFxIT10EPgD2wrvn9PplVLQTPWrXRhtJn4z/IACJvvcrUb8/Ru0RlVbFoL9GwQnUeG5rZRMB4i5TEQxvzC0AibhkKHPDpu2bUkA0z2/6WxHysI2MDyq/KOAO6qkVN6i5Ov5QkwpHlA3FLgABKACIrKeQqE3+bWtn/1RDSOHoTByg4+CmQjT7g+WoGJh6bPypcetA1yD/JgCJuGTpiSQEO5BUPwWaFtVzUzf0RrdBPDquQZwT8vXg0NRPrWCcXyYAibhk6ZqIjWbyLQyKaTnGh8s6UBgr0rsx4SRI+IaYWyOMCYOQx58qAIlL/B8V22FMHCw6WAoH/CvL7lZ6v3FF//H1rMkDPbqx/kDyK1BcYK2xCisAiGmLDQVS9udWsY17AuKf5eJE+KcoriqVr3Sd7jIOyErhrnmoE9kO8m3hkyYAhlhxj85s8CJW1u3tRmBw7t+sQTW6TsZlSswa2AKku+yPfgrDro8hAIZYYZovBmLLAnQnhJPq8jfFL0qdzRqm3v+9d0eaeAwMfyMAhVUvv+IPw5/mLitSYigMmzcStRhVTMUa7bLCAs7QdhbIJn8lAIVVL7+OCDAzxVzSJinFpGimpnXu1oaWs/RWXEtH474Te47+3M8hAIVJ5J8QEEZj38+BG7oHl9MndKra/YMr6lNnEsEBkQyu6yQAhXrIaYIOXCPMZpgrHn6J9h3B/AIMU9KfLyGMsYQEXqRuV7u7JQCHm9PXURbDbschXYWcBCufJBox9plU9erWgFjcZqRmAU+CfAX/JgCIAPnBlzD6g/3NVp4Ik7FEnq6nteJynGx2i2q0NZQzijEaqqCrJywAhojvB8n+v4cmfnOs2wJpXwr6HGchjnCPLkn48x3cdlcOp1yQVHxOj/MTu/8pAIduSP9PDyyslrEMIkgwjsMCFlPzCH1p1gUFjb4gS2aPJdCudYHpTNjvIQCHbkj5wwoech/nPOU1j2ygE2bGjW3ccogrQDyddWA5/L8kAIduSdYwIRVOELiqSot9aW5hStgk8B1v/oyGljpECIF79aBC/yYAh25J1iRx+6jE9Ikbhqh2gJAPim3lH5vEpBUq0dlrQh1eyvO2178oAIeGBQp2lv1QEsy43j1mynl89RGygX6AhG5nJhAMjFZ8n5EVerC0HW8qAIjniRTdjHVHQ3de5bBx+xgqyQEzGN5KYnfpbMMT0UoyohV4FKT0tlOpfzQAii2MZfBsvaEWvEC89ENXv6XyBFuqpPOIV/9N6yyhree4V5mFAPvaFaTXERLX77TWR01J7yEAiwKdN0e/d5h5e4WS1ZacgB5kWUvXqSAyboYdj72UQP//KQCK7moSnA+ozCzb/0yErjZQLtGR5J/rCmzuUfTxllx0I2aSwxZTC7EQ+SgAjEoQMq+SDPTRNrOooo0rsezJH6NLFq7NJYbM6xi4/xtAYMn2CNFALycAi8PfF9gWaHZGsVmy5/u1X2qS6M64TpOha/wMKfszPV/uJg1guw0/JACKc0nZ90kBOmtqIBctMzoKCZfD7S9YwsuOroOUWcNDJOEPyP8lAIiBu6vFD8FT+YfHlTDEZTqSm2+1jjIZb36UKMHsPQl5HNpZQz8fAIiB8KeK/Qzj9IxMnXWhxpQ/DK/gu+x84iImMVC/ej8mAIfSgdvAQ2MGvoRXovw2DiM/RtircAnK250UQ8vZXwamQrtmaMZfHwCJxaxVhZqVSF0+t603PY+cBC8m7mTmVMhSB6H0NccfGgCLAtr61tpStePiwIZCdgUgjfiXiL+jLquAfx8AiwKb8Kxt6TpOyavs2D2VYAf9EBzvfX7O27jaAbK0fyQAiwKcAxF53zk6Q9C6tB33pw30JNy+0kfoBRw14SGOX2eY3t7/JACK8+OxIEZWod30zLrgOnm5YKb6h4AbdX9MwM1m0m2Z4FF7K/8rAIsCmtc2Uu2GXWnQlm98J+40ChXsFsm1FePJeM2Fr1a648gd044s7HwsaCcpAIxKJuom+SFjmSqup+DzZ9sBrKWxd4M6/g61KWlenTGrF1XscwMwQqJ/KwCMSil8YOd95vVIYS3WpJhrTy7H/pBLDlauJYsFcAVb4jgCwr9iDJoLkQNjJwCMZYQVTv1vNzQkuif1KS04wfgc1VgxBqb8+67o0zUq6oCFQ+vMsxIgAI2bDhh7sp9kz4QDlaAmQuQpOQMieyHY5Xe0PQbQGt3/IgCNCzH5TLAKozk6tCCaQpYhITBzzDKHAP3PvbZez463LAdfIgCLArhQyKSS6KDHlnv7aCfB9bbtfJAgUeu7K8A/rC9qxPUbIwCJrNU0XRUc3R+Krc+CHDc6Glmtc8ou+kcUc1VQiQXIk2FVHyMAih9gIgtYtVqB7r1+qmzORCGSdouAIAchYkJJijnWpdwB49cgAIpW+rWr7h//Ng6t3s2Fggc4MjqDkVsyTETZEFBVZDGfIgCzr2a7eFZ7/xPzNin0FH2YhLL51JkgzJYdkq0ZXLs8w4ZsHgC2BYUgkAAZ+4/LR12zmwDTheuNrf5CHumJHlDyXa8eALXZGID94WwIJmGwfbL1RNl+hAFp9vXJkyE3XPDT/yQAtQWiNAUdFxYKbRzVGa+g20EMW3nbjB3UB2EAi/+Zi/vdQZB/HgCycUMJTkxTH+jKpo6eu8k+kt7VrxXc1ftrTbxwUYkdALDmtI1b5YsBBFX25sqaYm1YOBEwJ/Gp8x8jcZH/KwCFg8bM/sIir0/87UbtbwtEcEoWFx7LQZm2DzOw503xeOg+lWCRraoPF2L/KQCJt1BoPvFyLrowszktisd7DOa7QAt5lVB78uSm18bnABYSP57HTD+pYx0AiBqeX4cwFBBjXc3de468QaTO0mdkdb4xYDb+oAsqAIesINYIXflwRqy+mwmHaSvU9igW9ylpWdl1hkY1/ILF1pReXNEaUdwXfygAotynJofX6jWcrBWSFpsfI2E8z7kD2/iRnIBpGrxio6gYjXLuA7gv/yYAtskJOkGcmLDrbpwFGpE58ZEscF1x7JWHdnQ4ISt01BVRAkh4LL8=' 61 | }, 62 | create_time: 1751688597, 63 | msg_source: '\n' + 64 | '\tN0_V1_3d4SW/OX|v1_rzGUpcMM\n' + 65 | '\t\n' + 66 | '\t\t\n' + 67 | '\t\n' + 68 | '\n', 69 | push_content: '杨 : [语音]', 70 | new_msg_id: 8092566104805375000 71 | } 72 | 73 | # 图片 74 | 75 | 收到消息: { 76 | msg_id: 1483876273, 77 | from_user_name: { str: 'wxid_hebkbciz5spt22' }, 78 | to_user_name: { str: 'ai374949369' }, 79 | msg_type: 3, 80 | content: { 81 | str: '\n' + 82 | '\n' + 83 | '\t\n' + 84 | '\t\teyJwaGFzaCI6IjUwMDAwMDAwMDAwMDIwMDAiLCJwZHFIYXNoIjoiNTVkYTAwZmZmZWM4ZmU5OGE4\n' + 85 | 'OTlhOWJkNTcwMGU5YzQxNGVmMDA1ZjAwN2YwYTdmNTUyNjJhN2ZjNDI2NTU1MiJ9\n' + 86 | '\n' + 87 | '\t\t\n' + 88 | '\t\t\t0\n' + 89 | '\t\t\t0\n' + 90 | '\t\t\t\n' + 91 | '\t\t\t\n' + 92 | '\t\t\t0\n' + 93 | '\t\t\t\n' + 94 | '\t\t\t\n' + 95 | '\t\t\t0\n' + 96 | '\t\t\n' + 97 | '\t\n' + 98 | '\t\n' + 99 | '\t\n' + 100 | '\t\n' + 101 | '\t\t\n' + 102 | '\t\t0\n' + 103 | '\t\n' + 104 | '\n' 105 | }, 106 | status: 3, 107 | img_status: 2, 108 | img_buf: { 109 | len: 2756, 110 | buffer: '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' 111 | }, 112 | create_time: 1751688604, 113 | msg_source: '\n' + 114 | '\t\n' + 115 | '\t\tf913719643d92acbf8e27635ba9dd154_\n' + 116 | '\t\t\n' + 117 | '\t\t\n' + 118 | '\t\n' + 119 | '\tN0_V1_nfatcMT6|v1_HVZDlxfP\n' + 120 | '\t\n' + 121 | '\t\t\n' + 122 | '\t\n' + 123 | '\n', 124 | push_content: '杨 : [图片]', 125 | new_msg_id: 6117587016103724000 126 | } 127 | 128 | 129 | 130 | 131 | SQL: INSERT INTO `ai_friends_task` (`friend_id`,`cron_expression`,`start_date`,`end_date`,`is_active`,`task_name`) VALUES (1744,'0 0 20 * * 1-4','','',1,'晚上8点开始学雅思啦'), Time: 56ms 132 | Error: ER_TRUNCATED_WRONG_VALUE: Incorrect date value: '' for column 'start_date' at row 1 133 | at Query.Sequence._packetToError (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14) 134 | at Query.ErrorPacket (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/sequences/Query.js:79:18) 135 | at Protocol._parsePacket (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Protocol.js:291:23) 136 | at Parser._parsePacket (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Parser.js:433:10) 137 | at Parser.write (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Parser.js:43:10) 138 | at Protocol.write (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Protocol.js:38:16) 139 | at Socket. (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/Connection.js:88:28) 140 | at Socket. (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/Connection.js:526:10) 141 | at Socket.emit (events.js:315:20) 142 | at addChunk (internal/streams/readable.js:309:12) 143 | at readableAddChunk (internal/streams/readable.js:284:9) 144 | at Socket.Readable.push (internal/streams/readable.js:223:10) 145 | at TCP.onStreamRead (internal/stream_base_commons.js:188:23) 146 | -------------------- 147 | at Protocol._enqueue (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Protocol.js:144:48) 148 | at PoolConnection.query (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/Connection.js:198:25) 149 | at /Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/think-helper/index.js:83:10 150 | at new Promise () 151 | at /Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/think-helper/index.js:82:12 152 | at ThinkMysql.[think-mysql-query] (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/think-mysql/index.js:169:12) 153 | at /Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/think-mysql/index.js:247:25 154 | at processTicksAndRejections (internal/process/task_queues.js:93:5) { 155 | code: 'ER_TRUNCATED_WRONG_VALUE', 156 | errno: 1292, 157 | sqlMessage: "Incorrect date value: '' for column 'start_date' at row 1", 158 | sqlState: '22007', 159 | index: 0, 160 | sql: "INSERT INTO `ai_friends_task` (`friend_id`,`cron_expression`,`start_date`,`end_date`,`is_active`,`task_name`) VALUES (1744,'0 0 20 * * 1-4','','',1,'晚上8点开始学雅思啦')" 161 | } 162 | 处理用户 smjiloveyou 消息失败: TypeError: Cannot read property 'logger' of undefined 163 | at module.exports.getAIReplyWithMultiple‌Wheel (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/src/service/ai.js:131:16) 164 | at processTicksAndRejections (internal/process/task_queues.js:93:5) 165 | at MessageProcessor.processUserMessages (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/src/controller/websocket.js:87:28) 166 | at Timeout._onTimeout (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/src/controller/websocket.js:53:7) 167 | 168 | 169 | 170 | 171 | SQL: INSERT INTO `ai_messages` (`from_user`,`to_user`,`content`,`raw_data`,`msg_id`,`status`,`is_ai`,`type`,`msg_type`,`create_time`) VALUES ('smjiloveyou','ai374949369','那从明天开始吧?','{\"msg_id\":1578020077,\"from_user_name\":{\"str\":\"smjiloveyou\"},\"to_user_name\":{\"str\":\"ai374949369\"},\"msg_type\":1,\"content\":{\"str\":\"那从明天开始吧?\"},\"status\":3,\"img_status\":1,\"img_buf\":{\"len\":0},\"create_time\":1751717961,\"msg_source\":\"\\n\\t1\\n\\t1\\n\\tN0_V1_7zGdeRs5|v1_88KsMFAi\\n\\t\\n\\t\\t\\n\\t\\n\\n\",\"push_content\":\"童瑞 : 那从明天开始吧?\",\"new_msg_id\":2286718553428272400}',1578020077,'queued',0,0,1,1751717961), Time: 39ms 172 | Error: ER_TRUNCATED_WRONG_VALUE: Incorrect date value: '' for column 'start_date' at row 1 173 | at Query.Sequence._packetToError (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14) 174 | at Query.ErrorPacket (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/sequences/Query.js:79:18) 175 | at Protocol._parsePacket (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Protocol.js:291:23) 176 | at Parser._parsePacket (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Parser.js:433:10) 177 | at Parser.write (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Parser.js:43:10) 178 | at Protocol.write (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Protocol.js:38:16) 179 | at Socket. (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/Connection.js:88:28) 180 | at Socket. (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/Connection.js:526:10) 181 | at Socket.emit (events.js:315:20) 182 | at addChunk (internal/streams/readable.js:309:12) 183 | at readableAddChunk (internal/streams/readable.js:284:9) 184 | at Socket.Readable.push (internal/streams/readable.js:223:10) 185 | at TCP.onStreamRead (internal/stream_base_commons.js:188:23) 186 | -------------------- 187 | at Protocol._enqueue (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/protocol/Protocol.js:144:48) 188 | at PoolConnection.query (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/mysql/lib/Connection.js:198:25) 189 | at /Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/think-helper/index.js:83:10 190 | at new Promise () 191 | at /Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/think-helper/index.js:82:12 192 | at ThinkMysql.[think-mysql-query] (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/think-mysql/index.js:169:12) 193 | at /Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/think-mysql/index.js:247:25 194 | at processTicksAndRejections (internal/process/task_queues.js:93:5) { 195 | code: 'ER_TRUNCATED_WRONG_VALUE', 196 | errno: 1292, 197 | sqlMessage: "Incorrect date value: '' for column 'start_date' at row 1", 198 | sqlState: '22007', 199 | index: 0, 200 | sql: "INSERT INTO `ai_friends_task` (`friend_id`,`cron_expression`,`start_date`,`end_date`,`is_active`,`task_name`) VALUES (1744,'0 0 10 * * 0,6','','',1,'周末早上10点开始学雅思')" 201 | 202 | 203 | 204 | 205 | 206 | - SQL: INSERT INTO `ai_friends_task` (`friend_id`,`cron_expression`,`start_date`,`end_date`,`is_active`,`task_name`) VALUES (1744,'0 0 10 * * 0,6','','',1,'周末早上10点开始学雅思'), Time: 34ms 207 | 处理用户 smjiloveyou 消息失败: TypeError: Cannot read property 'logger' of undefined 208 | at module.exports.getAIReplyWithMultiple‌Wheel (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/src/service/ai.js:131:16) 209 | at processTicksAndRejections (internal/process/task_queues.js:93:5) 210 | at MessageProcessor.processUserMessages (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/src/controller/websocket.js:87:28) 211 | at Timeout._onTimeout (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/src/controller/websocket.js:53:7) 212 | [2025-07-05T20:19:23.168] [78924] [INFO] - SQL: UPDATE `ai_messages` SET `status`='failed' WHERE ( `msg_id` = 655932313 ), Time: 39ms 213 | 处理 smjiloveyou 的 1 条消息: 那从明天开始吧? 214 | combinedContent==》 那从明天开始吧? 215 | [2025-07-05T20:19:23.224] [78924] [INFO] - SQL: SELECT * 216 | 217 | 218 | SQL: SELECT * FROM `ai_friends_task` WHERE ( `is_active` = 1 ), Time: 26ms 219 | [2025-07-05T20:46:31.258] [88118] [ERROR] - 任务 该继续学习啦,别摸鱼! 调度失败: Error: Too few fields 220 | at CronTime._parse (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/cron/lib/cron.js:418:11) 221 | at new CronTime (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/cron/lib/cron.js:46:9) 222 | at new CronJob (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/node_modules/cron/lib/cron.js:565:19) 223 | at module.exports.scheduleTask (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/src/service/taskScheduler.js:118:19) 224 | at module.exports.refreshTasks (/Users/Victor/Desktop/ai-wechat-api/ai-wechat-api/src/service/taskScheduler.js:85:16) 225 | at runMicrotasks () 226 | at processTicksAndRejections (internal/process/task_queues.js:93:5) 227 | [2025-07-05T20:46:31.259] [88118] [DEBUG] - 任务刷新完成: 当前活跃任务 2 个 --------------------------------------------------------------------------------