├── 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 |
28 |
29 |
30 |
31 |
1
32 |
Generate Files
33 |
Run thinkjs command to create module, controler, model, service and so on.
34 |
35 |
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 |
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: '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'
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.getAIReplyWithMultipleWheel (/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.getAIReplyWithMultipleWheel (/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 个
--------------------------------------------------------------------------------