diff --git a/.env.example b/.env.example index fcee9411..eaf9133e 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,19 @@ # Wechaty -WECHATY_PUPPET="wechaty-puppet-wechat4u" -WECHATY_TOKEN="" +WECHATY_PUPPET="wechaty-puppet-wechat4u" # 可选值:wechaty-puppet-wechat4u、wechaty-puppet-wechat、wechaty-puppet-xp、wechaty-puppet-engine、wechaty-puppet-padlocal、wechaty-puppet-service +WECHATY_TOKEN="" # 使用wechaty-puppet-padlocal、wechaty-puppet-service时需配置此token # 基础配置 ADMINROOM_ADMINROOMTOPIC="替换为你的管理员群名称" # 管理群名称,需尽量保持名称复杂,避免重名群干扰 # 维格表配置 -VIKA_SPACE_NAME="替换为你的维格表空间名称" # 维格表空间名称,与VIKA_SPACE_ID二选一只需要配置一项即可 -VIKA_SPACE_ID="替换为你的维格表空间ID" # 维格表空间ID,与VIKA_SPACE_NAME二选一只需要配置一项即可 -VIKA_TOKEN="替换为你的维格表token" #维格表token +VIKA_SPACE_ID="替换为你的维格表空间ID" # 维格表空间ID或飞书多维表格的appToken +VIKA_TOKEN="替换为你的维格表token" # 维格表token或飞书多维表格信息拼接(使用'/'拼接三个参数:appId/appSecret/appToken) +ENDPOINT="http://127.0.0.1:9503" # 后端管理服务API地址,默认http://127.0.0.1:9503 +# ENDPOINT="http://120.48.99.192:9503" # 官方体验环境地址,可以直接使用,不需要启动chatflow-admin,但服务器不定时重启,不保证稳定 # --------------------------------分割线-------------------------------- -# 以下配置无需关注,功能实现中,暂未使用,仅需要配置上面的配置即可 +# 以下配置无需关注,功能实现中,暂未使用,仅需要配置分割线以上的配置即可 + # 飞书多维表格配置 LARK_APP_ID="" # 飞书多维表格应用ID diff --git a/README.md b/README.md index 49db369b..c7f3b2b8 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,30 @@ ChatFlow是一个聊天机器人管理系统,可以帮助你实现一些原生 ## 快速开始 -> 2.0.25+之后的版本,数据表不兼容,在运行时建议配置全新的维格表空间或删除原空间全部表 +> 升级代码后建议配置全新的维格表空间或删除原空间全部表,请使用nodejs16或18,最新的nodejs20可能无法运行 -1.下载源码并安装依赖 +最新部署方法参考:[ChatFlow3.0Beta部署运行](https://www.yuque.com/atorber/chatflow/gbpvgf01cw0nlxu4) + +1.下载代码及安装启动 + +1.1 下载并运行chatflow-admin + +```Shell +git clone https://github.com/atorber/chatflow-admin.git +cd chatflow-admin + +# 安装依赖 +npm i + +# 启动api服务 +npm run start:dev +``` + +1.2 下载并运行chatflow ```Shell -git clone -cd ./chatflow -npm install +git clone https://github.com/atorber/chatflow.git +cd chatflow ``` 2.分别登陆[微信对话开放平台](https://openai.weixin.qq.com/)和[vika维格表](https://spcp52tvpjhxm.com.vika.cn/?inviteCode=55152973)官网注册账号并获取token @@ -53,12 +69,17 @@ npm install > 快速开始仅需要修改VIKA_TOKEN、VIKA_SPACE_NAME、ADMINROOM_ADMINROOMTOPIC配置项,其他配置项暂时无需修改,使用微信对话开放平台时配置WXOPENAI_TOKEN、WXOPENAI_ENCODINGAESKEY ```.env -# 维格表配置 -VIKA_SPACE_ID="替换为自己的维格表空间ID" -VIKA_TOKEN="替换为自己的维格表token" +# Wechaty +WECHATY_PUPPET="wechaty-puppet-wechat4u" # 可选值:wechaty-puppet-wechat4u、wechaty-puppet-wechat、wechaty-puppet-xp、wechaty-puppet-engine、wechaty-puppet-padlocal、wechaty-puppet-service +WECHATY_TOKEN="" # 使用wechaty-puppet-padlocal、wechaty-puppet-service时需配置此token # 基础配置 -ADMINROOM_ADMINROOMTOPIC="瓦力是群主" # 管理群名称,需尽量保持名称复杂,避免重名群干扰 +ADMINROOM_ADMINROOMTOPIC="替换为你的管理员群名称" # 管理群名称,需尽量保持名称复杂,避免重名群干扰 + +# 维格表配置 +VIKA_SPACE_ID="替换为你的维格表空间ID" # 维格表空间ID或飞书多维表格的appToken +VIKA_TOKEN="替换为你的维格表token" # 维格表token或飞书多维表格信息拼接(使用'/'拼接三个参数:appId/appSecret/appToken) +ENDPOINT="http://127.0.0.1:9503" # 后端管理服务API地址,默认http://127.0.0.1:9503 ``` 4.启动程序 @@ -170,6 +191,28 @@ atorber/chatflow:latest 将过去复杂的IT数据库技术,做得像表格一样简单(如果要注册,通过这个链接,或者使用邀请码 55152973 ) +## 更新日志 + +### 3.0.0-Beta-11 + +- 移除环境变量依赖 + +### 3.0.0-Beta-10 + +- 新增媒体资源接口 +- 新增进群欢迎语接口 +- 新增顺风车接口 + +### 3.0.0-6 + +- 全部接口切换到chatfow-admin,更加稳定可靠 +- 修复微信对话开放平台bug +- 增加ChatGPT支持 + +### 3.0.0-5 + +- 适配飞书多维表格,初步测试通过 + ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=atorber/chatflow&type=Date)](https://star-history.com/#atorber/chatflow&Date) diff --git a/example/ding-dong-bot.ts b/example/ding-dong-bot.ts index f2228d2b..d72d2841 100644 --- a/example/ding-dong-bot.ts +++ b/example/ding-dong-bot.ts @@ -8,53 +8,44 @@ import { import { ChatFlow, getBotOps, - log, logForm, - // LarkDB, - GroupMaster, - GroupMasterConfig, init, -} from '../src/chatflow.js' +} from '../src/index.js' const main = async () => { - // 构建机器人 - const puppet = process.env['WECHATY_PUPPET'] - const token = process.env['WECHATY_TOKEN'] - const ops = getBotOps(puppet, token) + // 从环境变量中获取配置信息, 在环境变量中已经配置了以下信息或者直接赋值 + const WECHATY_PUPPET = process.env['WECHATY_PUPPET'] + const WECHATY_TOKEN = process.env['WECHATY_TOKEN'] + const VIKA_SPACE_ID = process.env['VIKA_SPACE_ID'] + const VIKA_TOKEN = process.env['VIKA_TOKEN'] + const ADMINROOM_ADMINROOMTOPIC = process.env['ADMINROOM_ADMINROOMTOPIC'] // 管理群的topic,可选 + + // 构建wechaty机器人 + const ops = getBotOps(WECHATY_PUPPET, WECHATY_TOKEN) // 获取wechaty配置信息 const bot = WechatyBuilder.build(ops) - await init({ - spaceName: process.env['VIKA_SPACE_NAME'], - spaceId: process.env['VIKA_SPACE_ID'], - token: process.env['VIKA_TOKEN'], - }, bot) - - // 使用Lark - // await LarkDB.init({ - // appId: process.env['LARK_APP_ID'], - // appSecret: process.env['LARK_APP_SECRET'], - // appToken: process.env['LARK_BITABLE_APP_TOKEN'], - // userMobile: process.env['LARK_APP_USER_MOBILE'], - // }) - - // 如果配置了群管理秘书,则启动群管理秘书,这是一个探索性功能,暂未开放,可以忽略 - if (process.env['GROUP_MASTER_ENDPOINT']) { - const configGroupMaster: GroupMasterConfig = { - WX_KEY:process.env['GROUP_MASTER_WX_KEY'] || '', - MQTT_ENDPOINT:process.env['GROUP_MASTER_MQTT_ENDPOINT'] || '', - MQTT_USERNAME:process.env['GROUP_MASTER_MQTT_USERNAME'] || '', - MQTT_PASSWORD:process.env['GROUP_MASTER_MQTT_PASSWORD'] || '', - MQTT_PORT:Number(process.env['GROUP_MASTER_MQTT_PORT'] || '1883'), - HOST:process.env['GROUP_MASTER_ENDPOINT'] || '', - } - bot.use(GroupMaster(configGroupMaster)) + // 初始化检查数据库表,如果不存在则创建 + try { + await init({ + spaceId: VIKA_SPACE_ID, + token: VIKA_TOKEN, + }) + } catch (e) { + logForm('初始化检查失败:' + JSON.stringify(e)) } - bot.use(ChatFlow()) + // 启用ChatFlow插件 + bot.use(ChatFlow({ + spaceId: VIKA_SPACE_ID, + token: VIKA_TOKEN, + adminRoomTopic: ADMINROOM_ADMINROOMTOPIC, + })) + + // 启动机器人 bot.start() .then(() => logForm('1. 机器人启动,如出现二维码,请使用微信扫码登录\n\n2. 如果已经登录成功,则不会显示二维码\n\n3. 如未能成功登录访问 https://www.yuque.com/atorber/chatflow/ibnui5v8mob11d70 查看常见问题解决方法')) - .catch((e: any) => log.error('机器人运行异常:', JSON.stringify(e))) + .catch((e: any) => logForm('机器人运行异常:' + JSON.stringify(e))) } void main() diff --git a/example/group-master-worker.ts b/example/group-master-worker.ts index c2c7ca69..a118ae11 100644 --- a/example/group-master-worker.ts +++ b/example/group-master-worker.ts @@ -5,20 +5,26 @@ import { WechatyBuilder, } from 'wechaty' -import { getBotOps, log, logForm, GroupMaster, GroupMasterConfig } from '../src/chatflow.js' +import { getBotOps, log, logForm, GroupMaster, GroupMasterConfig } from '../src/index.js' const main = async () => { const puppet = process.env['WECHATY_PUPPET'] const token = process.env['WECHATY_TOKEN'] + const WX_KEY = process.env['GROUP_MASTER_WX_KEY'] || '' + const MQTT_ENDPOINT = process.env['GROUP_MASTER_MQTT_ENDPOINT'] || '' + const MQTT_USERNAME = process.env['GROUP_MASTER_MQTT_USERNAME'] || '' + const MQTT_PASSWORD = process.env['GROUP_MASTER_MQTT_PASSWORD'] || '' + const MQTT_PORT = Number(process.env['GROUP_MASTER_MQTT_PORT'] || '1883') + const HOST = process.env['GROUP_MASTER_ENDPOINT'] || '' const config: GroupMasterConfig = { - WX_KEY:process.env['GROUP_MASTER_WX_KEY'] || '', - MQTT_ENDPOINT:process.env['GROUP_MASTER_MQTT_ENDPOINT'] || '', - MQTT_USERNAME:process.env['GROUP_MASTER_MQTT_USERNAME'] || '', - MQTT_PASSWORD:process.env['GROUP_MASTER_MQTT_PASSWORD'] || '', - MQTT_PORT:Number(process.env['GROUP_MASTER_MQTT_PORT'] || '1883'), - HOST:process.env['GROUP_MASTER_ENDPOINT'] || '', + WX_KEY, + MQTT_ENDPOINT, + MQTT_USERNAME, + MQTT_PASSWORD, + MQTT_PORT, + HOST, } // 构建机器人 const ops = getBotOps(puppet, token) diff --git a/package.json b/package.json index 4bfd70a5..6796c504 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@atorber/chatflow", - "version": "2.0.73", + "version": "3.0.0-Beta-11", "description": "ChatFlow-聊天机器人管理平台", "type": "module", "exports": { @@ -21,14 +21,18 @@ "clean": "shx rm -fr dist/*", "dist": "npm-run-all clean build dist:commonjs", "dist:commonjs": "jq -n \"{ type: \\\"commonjs\\\" }\" > dist/cjs/package.json", + "init-db": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./example/init-db.ts", "test-vika-orm": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./tests/vika-orm-test.ts", "start": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./example/ding-dong-bot.ts", + "start:verbose": "cross-env WECHATY_LOG=verbose NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./example/ding-dong-bot.ts", "start:gm": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./example/group-master-worker.ts", "start:index": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/index.ts", "start:notls": "cross-env WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT=true WECHATY_PUPPET_SERVICE_AUTHORITY=\"token-service-discovery-test.juzibot.com\" NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./example/ding-dong-bot.ts", "start:store": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./src/plugins/store-messages-locally.ts", - "start:lark": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./tests/lark.ts", - "start:api": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./tests/api.ts", + "test:lark-api": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./tests/lark.ts", + "test:auth": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./tests/auth.ts", + "test:lark-orm": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./tests/lark-orm-test.ts", + "test:vika-orm": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node ./tests/vika-orm-test.ts", "rm-temp": "rm -r temp; mkdir temp", "rm-cache": "rm -r cache; mkdir cache", "test": "npm run lint", @@ -55,7 +59,7 @@ "@vikadata/vika": "^1.0.5", "@yuque/sdk": "^1.1.1", "api2d": "^0.1.23", - "axios": "^1.4.0", + "axios": "^1.6.7", "axios-retry": "^3.8.0", "boxen": "^7.1.1", "chatgpt": "^1.4.0", @@ -104,7 +108,8 @@ "typescript": "^4.9.3", "wechaty": "^1.20.2", "wechaty-puppet-padlocal": "^1.20.1", - "wechaty-puppet-wechat4u": "^1.14.12" + "wechaty-puppet-wechat4u": "^1.14.12", + "wechaty-puppet-xp": "^1.13.8" }, "files": [ "bin/", @@ -113,10 +118,5 @@ ], "publishConfig": { "tag": "next" - }, - "git": { - "scripts": { - "pre-push": "npx git-scripts-pre-push" - } } -} \ No newline at end of file +} diff --git a/src/api/admin.ts b/src/api/admin.ts index c382b0bc..9afdaf70 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -15,13 +15,9 @@ import { import { EnvChat, NoticeChat, - ContactChat, - RoomChat, ActivityChat, - WhiteListChat, GroupNoticeChat, QaChat, - KeywordChat, } from '../services/mod.js' import { @@ -30,7 +26,8 @@ import { } from '../utils/mod.js' import { WxOpenaiBot, type WxOpenaiBotConfig, type SkillInfoArray } from '../services/wxopenaiService.js' - +import { ServeGetChatbotUsersDetail } from '../api/chatbot.js' +import { ServeGetWhitelistWhiteObject } from '../api/white-list.js' interface CommandActions { [key: string]: (bot: Wechaty, message: Message) => Promise } @@ -40,10 +37,27 @@ interface AdminCommands { } // 使用一个对象来存储命令和对应的处理函数 const adminCommands: AdminCommands = { + 更新智聊用户: async () => { + try { + const chatBotUsers = await ServeGetChatbotUsersDetail() + ChatFlowConfig.chatBotUsers = chatBotUsers.data.items + logger.info('获取到智聊用户信息:' + JSON.stringify(ChatFlowConfig.chatBotUsers)) + return [ true, '智聊用户列表更新成功~' ] + } catch (e) { + return [ false, '智聊用户列表配置更新失败~' ] + } + }, 更新配置: async () => { try { - const botConfig = await EnvChat.downConfigFromVika() - log.info('获取到新配信息:', JSON.stringify(botConfig)) + const res = await EnvChat.getConfigFromVika() + logger.info('ServeGetUserConfig res:' + JSON.stringify(res)) + + const vikaConfig:any = res.data + // logger.info('获取的维格表中的环境变量配置信息vikaConfig:' + JSON.stringify(vikaConfig)) + + // 合并配置信息,如果维格表中有对应配置则覆盖环境变量中的配置 + ChatFlowConfig.configEnv = { ...vikaConfig, ...(ChatFlowConfig.configEnv) } + logger.info('合并后的环境变量信息:' + JSON.stringify(ChatFlowConfig.configEnv)) return [ true, '配置更新成功~' ] } catch (e) { return [ false, '配置更新失败~' ] @@ -59,8 +73,8 @@ const adminCommands: AdminCommands = { }, 更新通讯录: async () => { try { - await ContactChat.updateContacts(ChatFlowConfig.configEnv.WECHATY_PUPPET) - await RoomChat.updateRooms(ChatFlowConfig.configEnv.WECHATY_PUPPET) + await ChatFlowConfig.updateContacts(ChatFlowConfig.configEnv.WECHATY_PUPPET) + await ChatFlowConfig.updateRooms(ChatFlowConfig.configEnv.WECHATY_PUPPET) return [ true, '通讯录更新成功~' ] } catch (e) { return [ false, '通讯录更新失败~' ] @@ -68,7 +82,9 @@ const adminCommands: AdminCommands = { }, 更新白名单: async () => { try { - ChatFlowConfig.whiteList = await WhiteListChat.getWhiteList() + const res = await ServeGetWhitelistWhiteObject() + ChatFlowConfig.whiteList = res.data + logger.info('获取到白名单信息:' + JSON.stringify(ChatFlowConfig.whiteList)) return [ true, '热更新白名单~' ] } catch (e) { return [ false, '白名单更新失败~' ] @@ -139,21 +155,11 @@ const adminCommands: AdminCommands = { 上传配置: async () => { try { await EnvChat.updateConfigToVika(ChatFlowConfig.configEnv) - return [ true, '上传配置信息成功~' ] } catch (e) { return [ false, '上传配置信息失败~' ] } }, - 下载配置: async () => { - try { - const botConfig = await EnvChat.downConfigFromVika() - log.info('botConfig:', JSON.stringify(botConfig)) - return [ true, '下载配置信息成功~' ] - } catch (e) { - return [ false, '下载配置信息失败~' ] - } - }, } // 封装成一个函数来处理错误和成功的消息发送 @@ -189,7 +195,7 @@ export const adminAction = async (message:Message) => { } if (text === '帮助') { - const replyText = await KeywordChat.getSystemKeywordsText() + const replyText = await ChatFlowConfig.getSystemKeywordsText() await sendMsg(message, replyText) } else if (Object.prototype.hasOwnProperty.call(adminCommands, text)) { const command = adminCommands[text as keyof typeof adminCommands] @@ -217,6 +223,114 @@ export const adminAction = async (message:Message) => { } } } + + // 检测链接进行管理操作 + try { + // 处理多维表格设置,如果text中包含https://oou2hscgt2.feishu.cn/或https://vika.cn/则从text中提取多维表格的配置信息 + // 维格表配置信息格式:https://vika.cn/workbench/dstZmz6jDv4H2Ym3qd/viwlGB2EZh1W7?spaceId=spcj6bgt12WoZ,提取结果为:{spaceId: 'spcj6bgt12WoZ', table: 'dstZmz6jDv4H2Ym3qd', view: 'viwlGB2EZh1W7'} + // 飞书多维表格配置信息格式:https://oou2hscgt2.feishu.cn/base/bascnPgZURujrdwZ9T4JkLUSUQc?table=tbl90nnja6sqMuCT&view=vewmgp68n9,提取结果为:{spaceId: 'bascnPgZURujrdwZ9T4JkLUSUQc', table: 'tbl90nnja6sqMuCT', view: 'vewmgp68n9'} + const VIKA_BASE_STRING = 'https://vika.cn/workbench/' + const LARK_BASE_STRING = '.feishu.cn/base/' + if (text.includes(VIKA_BASE_STRING) || text.includes(LARK_BASE_STRING)) { + const config: { + url: string; + spaceId: string | undefined; + table: string | undefined; + view: string | undefined; + } = { url:'', spaceId: '', table: '', view: '' } + const vikaConfigWithSpaceId = text.match(/https:\/\/vika.cn\/workbench\/(.*?)\/(.*)\?spaceId=(.*)/) + log.info('vikaConfig:', vikaConfigWithSpaceId || '不是维格表链接') + + const vikaConfigNoSpaceId = text.match(/https:\/\/vika.cn\/workbench\/(.*?)\/(.*)/) + log.info('vikaConfig:', vikaConfigNoSpaceId || '不是维格表链接') + + const larkConfig = text.match(/.feishu.cn\/base\/(.*?)\?table=(.*)&view=(.*)/) + log.info('larkConfig:', larkConfig || '不是飞书多维表格链接') + if (vikaConfigWithSpaceId && vikaConfigWithSpaceId.length === 4) { + config.url = text + config.spaceId = vikaConfigWithSpaceId[3] + config.table = vikaConfigWithSpaceId[1] + config.view = vikaConfigWithSpaceId[2] + } else if (vikaConfigNoSpaceId && vikaConfigNoSpaceId.length === 3) { + config.url = text + config.table = vikaConfigNoSpaceId[1] + config.view = vikaConfigNoSpaceId[2] + + } else if (larkConfig && larkConfig.length === 4) { + config.url = text + config.spaceId = larkConfig[1] + config.table = larkConfig[2] + config.view = larkConfig[3] + } + + logger.info('多维表格配置信息:' + JSON.stringify(config)) + log.info('多维表格配置信息:', JSON.stringify(config)) + if (config.table && config.view) { + log.info('多维表格配置信息匹配,开始处理...') + // 处理多维表格配置信息 + // 检查config.talbe是否是ChatFlowConfig.db.dataBaseIds中某个key的value,如果存在则查询ChatFlowConfig.db.dataBaseNames找出对应的表名称 + const tableId = config.table + const tableCode = ChatFlowConfig.db.dataBaseIdsMap[tableId] + log.info('数据表标识:', tableCode) + if (tableCode) { + const tableName = ChatFlowConfig.db.dataBaseNames[tableCode as keyof typeof ChatFlowConfig.db.dataBaseNames] + log.info('数据表名称:', tableName) + await message.say(`检测到多维表格链接,表格名称:【${tableName}】,\n是否需要处理?\n配置信息:\n${JSON.stringify(config, null, 2)}`) + + switch (tableCode) { + case 'mediaSheet': + await ChatFlowConfig.updateMediaList() + break + case 'envSheet': + await ChatFlowConfig.updateEnv() + break + case 'groupSheet': + await ChatFlowConfig.updateGroup() + break + case 'chatBotUserSheet': + await ChatFlowConfig.updateChatBotUser() + break + case 'chatBotSheet': + await ChatFlowConfig.updateChatBot() + break + case 'whiteListSheet': + await ChatFlowConfig.updateWhiteList() + break + case 'groupNoticeSheet': + await ChatFlowConfig.updateGroupNotifications() + break + case 'statisticSheet': + await ChatFlowConfig.updateStatistics() + break + case 'noticeSheet': + await ChatFlowConfig.updateReminder() + break + case 'qaSheet': + await ChatFlowConfig.updateQaList() + break + case 'keywordSheet': + await ChatFlowConfig.updateKeywords() + break + case 'welcomeSheet': + await ChatFlowConfig.updateWelcomes() + break + default: + log.info('多维表格配置信息不需要处理...') + } + + } else { + log.info('多维表格配置信息不匹配,不处理...') + await message.say(`检测到多维表格链接,但不是当前系统的多维表格链接,不处理~\n配置信息:\n${JSON.stringify(config, null, 2)}`) + } + + } else { + log.info('多维表格配置信息不全,不处理...') + await message.say('检测到多维表格链接,但配置信息不全,不处理~') + } + } + } catch (e) { + log.error('处理多维表格配置信息失败:', e) + } } // 群消息处理 diff --git a/src/api/auth.ts b/src/api/auth.ts index 6c4d72cc..d61ef46a 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,3 +1,13 @@ +import { + post, + // get, +} from '../utils/request.js' + +// 初始化创建表 +export const ServeUpdatePassword = (data: {} | undefined) => { + return post('/api/v1/auth/init', data) +} + // 用户登录 export const login = async ( param:{ diff --git a/src/api/base-config.ts b/src/api/base-config.ts index e5f31c64..e20d6f71 100644 --- a/src/api/base-config.ts +++ b/src/api/base-config.ts @@ -1,20 +1,76 @@ /* eslint-disable sort-keys */ import type { ProcessEnv } from '../types/mod.js' -import type { IRecord } from '../db/vika.js' import { - log, + Contact, + Room, Wechaty, + log, } from 'wechaty' - import type { WhiteList } from '../services/mod.js' import type { BusinessRoom, BusinessUser } from './contact-room-finder.js' -import { VikaDB } from '../db/vika-db.js' -import { LarkDB } from '../db/lark-db.js' import type { IClientOptions, } from '../proxy/mqtt-proxy.js' import CryptoJS from 'crypto-js' +import { + ServeGetUserConfigObj, + // ServeGetUserConfig, +} from '../api/user.js' + +import { delay, logger } from '../utils/utils.js' +import { + ServeGetContactsRaw, + ServeDeleteBatchContact, + ServeCreateContactBatch, + ServeContactGroupList, +} from '../api/contact.js' + +import { + ServeGetGroupsRaw, + ServeDeleteBatchGroups, + ServeCreateGroupBatch, +} from '../api/room.js' + +import { ServeGetKeywords } from '../api/keyword.js' +import { ServeGetMedias } from '../api/media.js' +import { ServeGetWhitelistWhiteObject } from '../api/white-list.js' +import { ServeGetGroupnotices } from '../api/group-notice.js' +import { ServeGetStatistics } from '../api/statistic.js' +import { ServeGetNotices } from '../api/notice.js' +import { + ServeGetChatbots, + ServeGetChatbotUsers, +} from '../api/chatbot.js' +import { ServeGetWelcomes } from '../api/welcome.js' +import { ServeGetQas } from '../api/qa.js' + +export interface ChatBotUser { + id: string; + botname: string; + wxid: string; + name: string; + alias: string; + quota: number; + state: string; + info: string; + recordId: string; + contact?: BusinessUser; + room?: BusinessRoom; + chatbot: ChatBot; +} + +interface ChatBot { + id: string; + name: string; + desc: string; + type: string; + model: string; + prompt: string; + quota: string; + endpoint: string; + key: string; +} export type WechatyConfig = { puppet: string, @@ -29,7 +85,7 @@ export interface Notifications { id?: string alias?: string state: string - pubTime?: string + pubTime?: number info?: string room?: BusinessRoom contact?: BusinessUser @@ -43,6 +99,7 @@ export interface TaskConfig { targetType: 'contact' | 'room'; target: BusinessRoom | BusinessUser; active: boolean; + rule: string; } export interface DateBase { @@ -57,7 +114,12 @@ export interface DateBase { orderSheet: string stockSheet: string groupNoticeSheet: string - qaSheet:string + qaSheet: string + chatBotSheet: string + chatBotUserSheet: string + groupSheet: string + welcomeSheet: string + mediaSheet: string } export class BiDirectionalMap { @@ -66,7 +128,7 @@ export class BiDirectionalMap { private reverseMap: Map constructor (fields: any[]) { - const initPairs:[string, string][] = fields.map((fields:any) => { + const initPairs: [string, string][] = fields.map((fields: any) => { return this.transformKey(fields.name) }) this.reverseMap = new Map(initPairs) @@ -89,14 +151,29 @@ export class BiDirectionalMap { export class ChatFlowConfig { - static envsOnVika: any[] + static isLogin: boolean = false + static isReady: boolean = false static switchsOnVika: any[] static reminderList: any[] static statisticRecords: any - static configEnv : ProcessEnv - static dataBaseType: 'vika' | 'lark' + static configEnv: ProcessEnv + static spaceId: string + static token: string = '' + static adminRoomTopic?: string + static endpoint?: string + static bot: Wechaty + static adminRoom: Room | undefined + static db: { + dataBaseIds: DateBase, + dataBaseNames: DateBase, + dataBaseIdsMap: { + [key: string]: string; + }; + spaceId: string, + token: string, + } - static whiteList:WhiteList = { + static whiteList: WhiteList = { contactWhiteList: { qa: [], msg: [], @@ -111,126 +188,382 @@ export class ChatFlowConfig { }, } - static bot:Wechaty + static chatBotUsers: ChatBotUser[] = [] + static welcomeList: { + id: string, + topic: string, + text: string, + state: '开启' | '关闭', + }[] = [] - static async init (dataBaseType?:'vika' | 'lark') { - this.dataBaseType = dataBaseType || 'vika' + static keywordList: { + name: string, + desc: string, + type: string, + details?: string, + }[] = [] + + static mediaList: { + name: string, + type: string, + link: string, + link1?: string, + state: string, + }[] = [] + + static setOptions (options: { + spaceId: string, + token: string, + endpoint?: string, + }) { + ChatFlowConfig.spaceId = options.spaceId || ChatFlowConfig.spaceId + ChatFlowConfig.token = options.token || ChatFlowConfig.token + ChatFlowConfig.endpoint = options.endpoint || ChatFlowConfig.token || 'http://127.0.0.1:9503' + } + + static async init (options: { + spaceId?: string, + token?: string, + endpoint?: string, + }) { + ChatFlowConfig.spaceId = options.spaceId || ChatFlowConfig.spaceId + ChatFlowConfig.token = options.token || ChatFlowConfig.token + ChatFlowConfig.endpoint = options.endpoint || ChatFlowConfig.token || 'http://127.0.0.1:9503' // log.info('初始化维格配置信息...,init()') - if (this.dataBaseType === 'vika' && VikaDB.spaceId) { - const vikaIdMap: any = {} - const vikaData: any = {} - const configRecords: any[] = await VikaDB.getAllRecords(VikaDB.dataBaseIds.envSheet) - for (let i = 0; i < configRecords.length; i++) { - const record: IRecord = configRecords[i] as IRecord - const fields = record.fields - const recordId = record.recordId - if (fields['标识|key']) { - if (fields['值|value'] && [ 'false', 'true' ].includes(fields['值|value'])) { - vikaData[record.fields['标识|key'] as string] = fields['值|value'] === 'true' - } else { - vikaData[record.fields['标识|key'] as string] = fields['值|value'] || '' - } - vikaIdMap[record.fields['标识|key'] as string] = recordId + const userConfig = await ServeGetUserConfigObj() + // log.info('userConfig', JSON.stringify(userConfig)) + this.configEnv = userConfig.data + const wechatyConfig: WechatyConfig = { + puppet: this.configEnv.WECHATY_PUPPET, + token: this.configEnv.WECHATY_TOKEN, + } + + // 计算clientid原始字符串 + const clientString = ChatFlowConfig.token + ChatFlowConfig.spaceId + // clientid加密 + const client = CryptoJS.SHA256(clientString).toString() + + const mqttConfig: IClientOptions = { + username: this.configEnv.MQTT_USERNAME, + password: this.configEnv.MQTT_PASSWORD, + host: this.configEnv.MQTT_ENDPOINT, + protocol: 'mqtts', + port: Number(this.configEnv.MQTT_PORT), + clientId: client, + clean: false, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + keepalive: 60, + resubscribe: true, + protocolId: 'MQTT', + protocolVersion: 4, + rejectUnauthorized: false, + } + + // log.info('mqttConfig', JSON.stringify(mqttConfig, undefined, 2)) + const mqttIsOn = Boolean(this.configEnv.MQTT_MQTTMESSAGEPUSH || this.configEnv.MQTT_MQTTCONTROL) + + // log.info('vikaBot配置信息:', JSON.stringify(configVika, undefined, 2)) + + return { + wechatyConfig, + mqttConfig, + mqttIsOn, + } + + } + + // 上传联系人列表 + static async updateContacts (puppet: string) { + let updateCount = 0 + try { + const contacts: Contact[] = await this.bot.Contact.findAll() + log.info('最新联系人数量(包含公众号):', contacts.length) + logger.info('最新联系人数量(包含公众号):' + contacts.length) + + const recordsAll: any = [] + const recordRes = await ServeGetContactsRaw() + const recordExisting = recordRes.data.items + + log.info('云端好友数量(不包含公众号):', recordExisting.length || '0') + logger.info('云端好友数量(不包含公众号):' + recordExisting.length || '0') + + let wxids: string[] = [] + const recordIds: string[] = [] + if (recordExisting.length) { + recordExisting.forEach((fields: any) => { + wxids.push(fields['id'] as string) + recordIds.push(fields.recordId) + }) + } + logger.info('当前bot使用的puppet:' + puppet) + log.info('当前bot使用的puppet:', puppet) + + // 根据多维表格类型设置批量操作的数量和延迟时间 + const batchCount = ChatFlowConfig.token.indexOf('/') === -1 ? 10 : 100 + const delayTime = ChatFlowConfig.token.indexOf('/') === -1 ? 1000 : 500 + + // 如果是wechaty-puppet-wechat或wechaty-puppet-wechat4u,每次登录好友ID会变化,需要分批删除好友 + if (puppet === 'wechaty-puppet-wechat' || puppet === 'wechaty-puppet-wechat4u') { + const count = Math.ceil(recordIds.length / batchCount) + for (let i = 0; i < count; i++) { + const records = recordIds.splice(0, batchCount) + log.info('删除:', records.length) + await ServeDeleteBatchContact({ recordIds: records }) + await delay(delayTime) } + wxids = [] } - this.configEnv = vikaData - const wechatyConfig: WechatyConfig = { - puppet: this.configEnv.WECHATY_PUPPET, - token: this.configEnv.WECHATY_TOKEN, + for (let i = 0; i < contacts.length; i++) { + const item = contacts[i] + const isFriend = item?.friend() || false + // const isIndividual = item?.type() === types.Contact.Individual + // logger.info('好友详情:' + item?.name()) + // log.info('是否好友:' + isFriend) + // logger.info('是否公众号:' + isIndividual) + // if(item) log.info('头像信息:', (JSON.stringify((await item?.avatar()).toJSON()))) + if (item && isFriend && !wxids.includes(item.id)) { + // logger.info('云端不存在:' + item.name()) + let avatar: any = '' + let alias = '' + try { + avatar = (await item.avatar()).toJSON() + avatar = avatar.url + } catch (err) { + // logger.error('获取好友头像失败:'+ err) + } + try { + alias = await item.alias() || '' + } catch (err) { + logger.error('获取好友备注失败:' + err) + } + const fields = { + alias, + avatar, + friend: item.friend(), + gender: String(item.gender() || ''), + updated: new Date().toLocaleString(), + id: item.id, + name: item.name(), + phone: String(await item.phone()), + type: String(item.type()), + } + const record = fields + recordsAll.push(record) + } } + logger.info('待更新的好友数量:' + recordsAll.length || '0') - // 计算clientid原始字符串 - const clientString = VikaDB.token + VikaDB.spaceId - // clientid加密 - const client = CryptoJS.SHA256(clientString).toString() - - const mqttConfig:IClientOptions = { - username: this.configEnv.MQTT_USERNAME, - password: this.configEnv.MQTT_PASSWORD, - host: this.configEnv.MQTT_ENDPOINT, - protocol:'mqtts', - port: Number(this.configEnv.MQTT_PORT), - clientId: client, - clean: false, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - keepalive: 60, - resubscribe: true, - protocolId: 'MQTT', - protocolVersion: 4, - rejectUnauthorized: false, + for (let i = 0; i < recordsAll.length; i = i + batchCount) { + const records = recordsAll.slice(i, i + batchCount) + await ServeCreateContactBatch(records) + log.info('好友列表同步中...' + i + records.length) + updateCount = updateCount + records.length + void await delay(batchCount) } - // log.info('mqttConfig', JSON.stringify(mqttConfig, undefined, 2)) - const mqttIsOn = Boolean(this.configEnv.MQTT_MQTTMESSAGEPUSH || this.configEnv.MQTT_MQTTCONTROL) + log.info('同步好友列表完成,更新到云端好友数量:' + updateCount || '0') + } catch (err) { + log.error('更新好友列表失败:' + err) + } + } - // log.info('vikaBot配置信息:', JSON.stringify(configVika, undefined, 2)) + // 上传群列表 + static async updateRooms (puppet: string) { + let updateCount = 0 + // 根据多维表格类型设置批量操作的数量和延迟时间 + const batchCount = ChatFlowConfig.token.indexOf('/') === -1 ? 10 : 100 + const delayTime = ChatFlowConfig.token.indexOf('/') === -1 ? 1000 : 500 + try { + // 获取最新的群列表 + const rooms: Room[] = await this.bot.Room.findAll() + log.info('最新微信群数量:', rooms.length) + const recordsAll: any = [] - return { - wechatyConfig, - mqttConfig, - mqttIsOn, - } - } else if (this.dataBaseType === 'lark' && LarkDB.config.appToken) { - const vikaIdMap: any = {} - const vikaData: any = {} - const configRecords: any[] = await LarkDB.getAllRecords(LarkDB.dataBaseIds.envSheet) - for (let i = 0; i < configRecords.length; i++) { - const record: IRecord = configRecords[i] as IRecord - const fields = record.fields - const recordId = record.recordId - if (fields['标识|key']) { - if (fields['值|value'] && [ 'false', 'true' ].includes(fields['值|value'])) { - vikaData[record.fields['标识|key'] as string] = fields['值|value'] === 'true' - } else { - vikaData[record.fields['标识|key'] as string] = fields['值|value'] || '' + // 从云端获取已有的群列表 + const roomsRes = await ServeGetGroupsRaw() + const recordExisting = roomsRes.data.items + logger.info('云端群数量:' + recordExisting.length || '0') + + let wxids: string[] = [] + const recordIds: string[] = [] + if (recordExisting.length) { + recordExisting.forEach((fields: any) => { + if (fields['id']) { + wxids.push(fields['id'] as string) + recordIds.push(fields.recordId) } - vikaIdMap[record.fields['标识|key'] as string] = recordId + }) + } + + if (puppet === 'wechaty-puppet-wechat' || puppet === 'wechaty-puppet-wechat4u') { + const count = Math.ceil(recordIds.length / batchCount) + for (let i = 0; i < count; i++) { + const records = recordIds.splice(0, batchCount) + logger.info('删除:', records.length) + await ServeDeleteBatchGroups({ recordIds: records }) + await delay(delayTime) } + wxids = [] } - this.configEnv = vikaData - const wechatyConfig: WechatyConfig = { - puppet: this.configEnv.WECHATY_PUPPET, - token: this.configEnv.WECHATY_TOKEN, + for (let i = 0; i < rooms.length; i++) { + + const item: Room | undefined = rooms[i] + // if(item) log.info('头像信息:', (JSON.stringify((await item.avatar()).toJSON()))) + + if (item && !wxids.includes(item.id)) { + let avatar: any = 'null' + try { + avatar = (await item.avatar()).toJSON() + avatar = avatar.url + } catch (err) { + // logger.error('获取群头像失败:' + err) + } + const ownerId = await item.owner()?.id + // logger.info('第一个群成员:' + ownerId) + const fields = { + avatar, + id: item.id, + ownerId: ownerId || '', + topic: await item.topic() || '', + updated: new Date().toLocaleString(), + } + const record = fields + recordsAll.push(record) + } } - // 计算clientid原始字符串 - const clientString = LarkDB.config.appId + LarkDB.config.appSecret + LarkDB.config.appToken - // clientid加密 - const client = CryptoJS.SHA256(clientString).toString() - - const mqttConfig:IClientOptions = { - username: this.configEnv.MQTT_USERNAME, - password: this.configEnv.MQTT_PASSWORD, - host: this.configEnv.MQTT_ENDPOINT, - port: Number(this.configEnv.MQTT_PORT), - protocol:'mqtts', - clientId: client, - clean: false, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - keepalive: 60, - resubscribe: true, - protocolId: 'MQTT', - protocolVersion: 4, - rejectUnauthorized: false, + for (let i = 0; i < recordsAll.length; i = i + batchCount) { + const records = recordsAll.slice(i, i + batchCount) + try { + await ServeCreateGroupBatch(records) + logger.info('群列表同步完成...' + i + records.length) + updateCount = updateCount + records.length + void await delay(delayTime) + } catch (err) { + logger.error('群列表同步失败,待系统就绪后再管理群发送【更新通讯录】可手动更新...' + i + batchCount) + void await delay(delayTime) + } } - const mqttIsOn = Boolean(this.configEnv.MQTT_MQTTMESSAGEPUSH || this.configEnv.MQTT_MQTTCONTROL) + logger.info('同步群列表完成,更新到云端群数量:' + updateCount || '0') + } catch (err) { + logger.error('更新群列表失败:' + err) - // log.info('vikaBot配置信息:', JSON.stringify(configVika, undefined, 2)) + } - return { - wechatyConfig, - mqttConfig, - mqttIsOn, - } - } else { - log.error('\n\n指定空间不存在,请先创建空间,并在.env文件或环境变量中配置vika信息\n\n================================================\n') - return undefined + } + + // 获取关键字文案 + static async getKeywordsText () { + // const records = await this.getKeywords() + const res = await ServeGetKeywords() + const records = res.data.items + + let text: string = '【操作说明】\n' + for (const fields of records) { + text += `${fields['name']}:${fields['desc']}\n` } + return text + } + + // 获取系统关键字文案 + static async getSystemKeywordsText () { + // const records = await this.getKeywords() + const res = await ServeGetKeywords() + const records = res.data.items + ChatFlowConfig.keywordList = records + let text: string = '【操作说明】\n' + for (const fields of records) { + if (fields['type'] === '系统指令') text += `${fields['name']} : ${fields['desc']}\n` + } + return text + } + + // 更新媒体资源库 + static async updateMediaList () { + const res = await ServeGetMedias({}) + const mediaList = res.data.items + log.info('获取的媒体资源库:' + JSON.stringify(mediaList)) + } + + // 更新环境变量 + static async updateEnv () { + const res = await ServeGetUserConfigObj() + const env = res.data + log.info('获取的环境变量:' + JSON.stringify(env)) + } + + // 更新分组 + static async updateGroup () { + const res = await ServeContactGroupList() + const groups = res.data.items + log.info('获取的分组:' + JSON.stringify(groups)) + } + + // 更新智聊用户名单 + static async updateChatBotUser () { + const res = await ServeGetChatbotUsers() + const chatBotUsers = res.data + log.info('获取的智聊用户名单:' + JSON.stringify(chatBotUsers)) + } + + // 更新智聊ChatBot + static async updateChatBot () { + const res = await ServeGetChatbots() + const chatBots = res.data + log.info('获取的智聊ChatBot:' + JSON.stringify(chatBots)) + } + + // 更新白名单 + static async updateWhiteList () { + const res = await ServeGetWhitelistWhiteObject() + const whiteList = res.data + log.info('获取的白名单:' + JSON.stringify(whiteList)) + } + + // 更新群发通知 + static async updateGroupNotifications () { + const res = await ServeGetGroupnotices() + const groupNotifications = res.data + log.info('获取的群发通知:' + JSON.stringify(groupNotifications)) + } + + // 更新统计打卡 + static async updateStatistics () { + const res = await ServeGetStatistics() + const statistics = res.data.items + log.info('获取的统计打卡:' + JSON.stringify(statistics)) + } + + // 更新定时提醒 + static async updateReminder () { + const res = await ServeGetNotices() + const reminders = res.data.items + log.info('获取的定时提醒:' + JSON.stringify(reminders)) + } + + // 更新问答列表 + static async updateQaList () { + const res = await ServeGetQas() + const qaList = res.data.items + log.info('获取的问答列表:' + JSON.stringify(qaList)) + } + + // 更新关键字 + static async updateKeywords () { + const res = await ServeGetKeywords() + const keywords = res.data.items + log.info('获取的关键字:' + JSON.stringify(keywords)) + } + + // 更新进群欢迎语 + static async updateWelcomes () { + const res = await ServeGetWelcomes() + const welcomes = res.data.items + log.info('获取的进群欢迎语:' + JSON.stringify(welcomes)) } } diff --git a/src/api/carpooling.ts b/src/api/carpooling.ts new file mode 100644 index 00000000..e44bbc67 --- /dev/null +++ b/src/api/carpooling.ts @@ -0,0 +1,16 @@ +import { get, post } from '../utils/request.js' + +// 获取问答列表服务接口 +export const ServeGetCarpoolings = (data:any) => { + return get('/api/v1/carpooling/list', data) +} + +// 创建问答服务接口 +export const ServeCreateCarpoolings = (data: {} | undefined) => { + return post('/api/v1/carpooling/create', data) +} + +// 删除问答服务接口 +export const ServeDeleteCarpoolings = (data: {} | undefined) => { + return post('/api/v1/carpooling/delete', data) +} diff --git a/src/api/chat.ts b/src/api/chat.ts new file mode 100644 index 00000000..29eacc31 --- /dev/null +++ b/src/api/chat.ts @@ -0,0 +1,123 @@ +import { post, get, upload } from '../utils/request.js' + +// 获取聊天列表服务接口 +export const ServeGetTalkList = (data = {}) => { + return get('/api/v1/talk/list', data) +} + +// 聊天列表创建服务接口 +export const ServeCreateTalkList = (data = {}) => { + return post('/api/v1/talk/create', data) +} + +// 删除聊天列表服务接口 +export const ServeDeleteTalkList = (data = {}) => { + return post('/api/v1/talk/delete', data) +} + +// 对话列表置顶服务接口 +export const ServeTopTalkList = (data = {}) => { + return post('/api/v1/talk/topping', data) +} + +// 清除聊天消息未读数服务接口 +export const ServeClearTalkUnreadNum = (data = {}) => { + return post('/api/v1/talk/unread/clear', data) +} + +// 获取聊天记录服务接口 +export const ServeTalkRecords = (data = {}) => { + return get('/api/v1/talk/records', data) +} + +// 获取聊天记录服务接口 +export const ServeCreateTalkRecords = (data:any = []) => { + return post('/api/v1/talk/records', data) +} + +// 获取聊天记录服务接口 +export const ServeCreateTalkRecordsUpload = (data: {} | undefined, options = {}) => { + return post('/api/v1/talk/imageVika', data, options) +} + +// 获取转发会话记录详情列表服务接口 +export const ServeGetForwardRecords = (data = {}) => { + return get('/api/v1/talk/records/forward', data) +} + +// 对话列表置顶服务接口 +export const ServeSetNotDisturb = (data = {}) => { + return post('/api/v1/talk/disturb', data) +} + +// 查找用户聊天记录服务接口 +export const ServeFindTalkRecords = (data = {}) => { + return get('/api/v1/talk/records/history', data) +} + +// 搜索用户聊天记录服务接口 +export const ServeSearchTalkRecords = (data = {}) => { + return get('/api/v1/talk/search-chat-records', data) +} + +export const ServeGetRecordsContext = (data = {}) => { + return get('/api/v1/talk/get-records-context', data) +} + +// 发送消息服务接口 +export const ServePublishMessage = (data = {}) => { + return post('/api/v1/talk/message/publish', data) +} + +// 发送文本消息服务接口 +export const ServeSendTalkText = (data = {}) => { + return post('/api/v1/talk/message/text', data) +} + +// 发送代码块消息服务接口 +export const ServeSendTalkCodeBlock = (data = {}) => { + return post('/api/v1/talk/message/code', data) +} + +// 发送聊天文件服务接口 +export const ServeSendTalkFile = (data = {}) => { + return post('/api/v1/talk/message/file', data) +} + +// 发送聊天图片服务接口 +export const ServeSendTalkImage = (data = {}) => { + return upload('/api/v1/talk/message/image', data) +} + +// 发送表情包服务接口 +export const ServeSendEmoticon = (data = {}) => { + return post('/api/v1/talk/message/emoticon', data) +} + +// 转发消息服务接口 +export const ServeForwardRecords = (data = {}) => { + return post('/api/v1/talk/message/forward', data) +} + +// 撤回消息服务接口 +export const ServeRevokeRecords = (data = {}) => { + return post('/api/v1/talk/message/revoke', data) +} + +// 删除消息服务接口 +export const ServeRemoveRecords = (data = {}) => { + return post('/api/v1/talk/message/delete', data) +} + +// 收藏表情包服务接口 +export const ServeCollectEmoticon = (data = {}) => { + return post('/api/v1/talk/message/collect', data) +} + +export const ServeSendVote = (data = {}) => { + return post('/api/v1/talk/message/vote', data) +} + +export const ServeConfirmVoteHandle = (data = {}) => { + return post('/api/v1/talk/message/vote/handle', data) +} diff --git a/src/api/chatbot.ts b/src/api/chatbot.ts new file mode 100644 index 00000000..d5f7449d --- /dev/null +++ b/src/api/chatbot.ts @@ -0,0 +1,41 @@ +import { get, post } from '../utils/request.js' + +// 获取聊天机器人列表服务接口 +export const ServeGetChatbots = (data = {}) => { + return get('/api/v1/chatbot/list', data) +} + +// 创建聊天机器人服务接口 +export const ServeCreateChatbots = (data: any) => { + return post('/api/v1/chatbot/create', data) +} + +// 删除聊天机器人服务接口 +export const ServeDeleteChatbots = (data: any) => { + return post('/api/v1/chatbot/delete', data) +} + +// 获取机器人用户列表服务接口 +export const ServeGetChatbotUsers = (data:any = {}) => { + return get('/api/v1/chatbot/user/list', data) +} + +// 获取机器人用户列表服务接口 +export const ServeGetChatbotUsersGroup = (data:any = {}) => { + return get('/api/v1/chatbot/user/list/group', data) +} + +// 获取机器人用户列表服务接口 +export const ServeGetChatbotUsersDetail = (data:any = {}) => { + return get('/api/v1/chatbot/user/list/detail', data) +} + +// 创建机器人用户服务接口 +export const ServeCreateChatbotUsers = (data: any) => { + return post('/api/v1/chatbot/user/create', data) +} + +// 删除机器人用户服务接口 +export const ServeDeleteChatbotUsers = (data: any) => { + return post('/api/v1/chatbot/user/delete', data) +} diff --git a/src/api/contact.ts b/src/api/contact.ts index 2646724d..23db0e56 100644 --- a/src/api/contact.ts +++ b/src/api/contact.ts @@ -1,26 +1,109 @@ /* eslint-disable no-console */ /* eslint-disable camelcase */ -import { ContactChat } from '../services/mod.js' +import { post, get } from '../utils/request.js' + +// 获取好友列表服务接口 +export const ServeGetContacts = () => { + return get('/api/v1/contact/list') +} + +// 获取好友列表服务接口 +export const ServeGetContactsRaw = () => { + return get('/api/v1/contact/list/raw') +} + +// 好友更新 +export const ServeCreateContactBatch = (data: {} | undefined) => { + return post('/api/v1/contact/create/batch', data) +} + +// 解除好友关系服务接口 +export const ServeDeleteContact = (data: any | undefined) => { + return post('/api/v1/contact/delete', data) +} + +// 解除好友关系服务接口批量 +export const ServeDeleteBatchContact = (data: any | undefined) => { + return post('/api/v1/contact/deleteBatch', data) +} + +// 更新好友服务接口 +export const ServeUpdateContact = (data: {} | undefined) => { + return post('/api/v1/contact/update', data) +} + +// 修改好友备注服务接口 +export const ServeEditContactRemark = (data: {} | undefined) => { + return post('/api/v1/contact/edit-remark', data) +} + +// 搜索联系人 +export const ServeSearchContact = (data: {} | undefined) => { + return get('/api/v1/contact/search', data) +} + +// 好友申请服务接口 +export const ServeCreateContact = (data: {} | undefined) => { + return post('/api/v1/contact/apply/create', data) +} + +// 查询好友申请服务接口 +export const ServeGetContactApplyRecords = (data: {} | undefined) => { + return get('/api/v1/contact/apply/records', data) +} + +// 处理好友申请服务接口 +export const ServeApplyAccept = (data: {} | undefined) => { + return post('/api/v1/contact/apply/accept', data) +} + +export const ServeApplyDecline = (data: {} | undefined) => { + return post('/api/v1/contact/apply/decline', data) +} + +// 查询好友申请未读数量服务接口 +export const ServeFindFriendApplyNum = () => { + return get('/api/v1/contact/apply/unread-num') +} + +// 搜索用户信息服务接口 +export const ServeSearchUser = (data: {} | undefined) => { + return get('/api/v1/contact/detail', data) +} + +// 搜索用户信息服务接口 +export const ServeContactGroupList = (data?: any) => { + return get('/api/v1/contact/group/list', data) +} + +export const ServeContactMoveGroup = (data: {} | undefined) => { + return post('/api/v1/contact/move-group', data) +} + +export const ServeContactGroupSave = (data: {} | undefined) => { + return post('/api/v1/contact/group/save', data) +} // import { db } from '../db/tables.js' // const contactData = db.contact // 获取好友列表 export async function getContactList () { - const contactListRaw:any = await ContactChat.findAll() + const res = await ServeGetContacts() + const contactListRaw:any = res.data.items // console.log('contactListRaw', JSON.stringify(contactListRaw)) - const contactList: any = contactListRaw.map((value: { fields: { name: any; avatar: any; gender: any; id: any; alias: any }; recordId: any }) => { - if (value.fields.name) { + const contactList: any = contactListRaw.map((fields: { name: any; avatar: any; gender: any; id: any; alias: any ; recordId: any }) => { + if (fields.name) { return { - avatar: value.fields.avatar, - gender: value.fields.gender, + avatar: fields.avatar, + gender: fields.gender, group_id: 0, - id: value.fields.id, + id: fields.id, is_online: 0, motto: '', - nickname: value.fields.name, - remark: value.fields.alias, - recordId: value.recordId, + nickname: fields.name, + remark: fields.alias, + recordId: fields.recordId, } } return false diff --git a/src/api/group-notice.ts b/src/api/group-notice.ts index cfbb92fb..7c90991a 100644 --- a/src/api/group-notice.ts +++ b/src/api/group-notice.ts @@ -1,16 +1,21 @@ -import { GroupNoticeChat } from '../services/mod.js' -// 获取群发通知列表 -export const getGroupNoticeList = async (_data: any) => { - const res = await GroupNoticeChat.db.findAll() - return res +import { get, post } from '../utils/request.js' + +// 获取群发列表服务接口 +export const ServeGetGroupnotices = () => { + return get('/api/v1/groupnotice/list') +} + +// 创建群发任务列表服务接口 +export const ServeCreateGroupnotices = (data: {} | undefined) => { + return post('/api/v1/groupnotice/create', data) } -// 创建群发通知 -export const createGroupNotice = (data: any) => { - return data +// 创建群发任务列表服务接口 +export const ServeUpdateGroupnotices = (data: any[] | undefined) => { + return post('/api/v1/groupnotice/update', data) } -// 删除群发通知 -export const deleteGroupNotice = (data: any) => { - return data +// 删除群发任务列表服务接口 +export const ServeDeleteGroupnotices = (data: {} | undefined) => { + return post('/api/v1/groupnotice/delete', data) } diff --git a/src/api/http.ts b/src/api/http.ts deleted file mode 100644 index eb9f8d12..00000000 --- a/src/api/http.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable no-console */ -/* eslint-disable promise/always-return */ -import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' - -let httpClientInstance: any = null - -export class HttpClient { - - private constructor () {} - - public static getInstance (): HttpClient { - if (!httpClientInstance) { - httpClientInstance = new HttpClient() - } - return httpClientInstance as HttpClient - } - - public async makeRequest (config: AxiosRequestConfig): Promise { - let response: AxiosResponse - try { - response = await axios(config) - } catch (error) { - // Handle the error according to your application logic - console.error(error) - throw error - } - return response.data - } - -} - -const httpClient = HttpClient.getInstance() - -const config: AxiosRequestConfig = { - method: 'get', - url: 'https://api.github.com/users/octocat', -} - -httpClient.makeRequest(config).then(response => { - console.log(response) -}).catch(error => { - console.error(error) -}) diff --git a/src/api/keyword.ts b/src/api/keyword.ts index 8029b567..545d5c63 100644 --- a/src/api/keyword.ts +++ b/src/api/keyword.ts @@ -1,7 +1,7 @@ -import { KeywordChat } from '../services/mod.js' +import { get, post } from '../utils/request.js' // 获取关键词列表 export const getKeywordList = async (_params: any) => { - const res = await KeywordChat.db.findAll() + const res = await ServeGetKeywords() return res } @@ -9,3 +9,18 @@ export const getKeywordList = async (_params: any) => { export const getKeywordTips = (params: any) => { return params } + +// 获取关键词列表服务接口 +export const ServeGetKeywords = () => { + return get('/api/v1/keyword/list') +} + +// 创建关键词服务接口 +export const ServeCreateKeywords = (data: {} | undefined) => { + return post('/api/v1/keyword/create', data) +} + +// 删除关键词服务接口 +export const ServeDeleteKeywords = (data: {} | undefined) => { + return post('/api/v1/keyword/delete', data) +} diff --git a/src/api/media.ts b/src/api/media.ts new file mode 100644 index 00000000..fbb9045f --- /dev/null +++ b/src/api/media.ts @@ -0,0 +1,16 @@ +import { get, post } from '../utils/request.js' + +// 获取问答列表服务接口 +export const ServeGetMedias = (data:any) => { + return get('/api/v1/media/list', data) +} + +// 创建问答服务接口 +export const ServeCreateMedias = (data: {} | undefined) => { + return post('/api/v1/media/create', data) +} + +// 删除问答服务接口 +export const ServeDeleteMedias = (data: {} | undefined) => { + return post('/api/v1/media/delete', data) +} diff --git a/src/api/message.ts b/src/api/message.ts index c8fbfbed..87c83500 100644 --- a/src/api/message.ts +++ b/src/api/message.ts @@ -11,10 +11,7 @@ import { formatTimestamp, getCurrentTime, delay, logger, getCurTime } from '../u import type { ChatMessage } from '../types/interface.js' import { MessageChat, - LarkChat, } from '../services/mod.js' -import { LarkDB } from '../db/lark-db.js' -import { ChatFlowConfig } from './base-config.js' import fs from 'fs' import path from 'path' @@ -88,7 +85,7 @@ export interface MessageToCloud { } export const formatMessageToCloud = async (message: Message) => { - log.info('formatMessageToCloud 消息转换为存储到多维表格的格式...') + // log.info('formatMessageToCloud 消息转换为存储到多维表格的格式...') const room = message.room() const talker = message.talker() try { @@ -230,21 +227,15 @@ export const formatMessageToCloud = async (message: Message) => { text = JSON.stringify(file.toJSON()) log.info('文件text:', text) } catch (e) { - log.error('文件转换JSON失败:', e) + log.error('message 文件转换JSON失败:', e) } try { - if (ChatFlowConfig.dataBaseType === 'lark' && LarkDB.config.appToken) { - uploadedAttachments = await LarkChat.handleFileMessage(file, message) - files.push({ - file_token: uploadedAttachments.file_token, - }) - log.info('上传文件Lark成功:', JSON.stringify(uploadedAttachments)) - } else { - uploadedAttachments = await MessageChat.handleFileMessage(file, message) - files.push(uploadedAttachments.data) - log.info('上传文件Vika成功:', JSON.stringify(uploadedAttachments)) + uploadedAttachments = await MessageChat.handleFileMessage(file, message) + const fileToken = uploadedAttachments.data + text = JSON.stringify(fileToken) + files.push(fileToken) + log.info('上传文件Vika成功:', JSON.stringify(uploadedAttachments)) - } } catch (e) { log.error('文件上传失败:', e) } @@ -288,36 +279,34 @@ export const formatMessageToCloud = async (message: Message) => { try { if (msgType !== 'Unknown') { - const record = { - fields: { - '时间|timeHms':timeHms, - '发送者|name': talker.name(), - '好友备注|alias': await talker.alias(), - '群名称|topic': topic || '--', - '消息内容|messagePayload': text, - '好友ID|wxid': talker.id !== 'null' ? talker.id : '--', - '群ID|roomid': room && room.id ? room.id : '--', - '消息类型|messageType': msgType, - '文件图片|file': files, - '消息ID|messageId': message.id, - '接收人|listener': topic ? '--' : (await listener?.alias() || listener?.name()), - '接收人ID|listenerid':topic ? '--' : listener?.id, - '发送者头像|wxAvatar': wxAvatar, - '群头像|roomAvatar':roomAvatar, - '接收人头像|listenerAvatar':listenerAvatar, - }, + const record:any = { + timeHms, + name: talker.name(), + alias: await talker.alias(), + topic: topic || '--', + messagePayload: text, + wxid: talker.id !== 'null' ? talker.id : '--', + roomid: room && room.id ? room.id : '--', + messageType: msgType, + messageId: message.id, + listener: topic ? '--' : (await listener?.alias() || listener?.name()), + listenerid:topic ? '--' : listener?.id, + wxAvatar, + roomAvatar, + listenerAvatar, } + if (files.length) record['file'] = files // logger.info('addChatRecord:', JSON.stringify(record)) return record } else { return undefined } } catch (e) { - log.error('添加记录失败:', e) + log.error('formatMessageToCloud添加记录失败:', e) return undefined } } catch (e) { - log.error('存储消息消息转换失败:', e) + log.error('formatMessageToCloud存储消息消息转换失败:', e) return undefined } @@ -327,13 +316,9 @@ export const formatMessageToCloud = async (message: Message) => { export const saveMessageToCloud = async (record:any) => { // log.info('saveMessageToCloud messageNew:', JSON.stringify(messageNew)) try { - if (ChatFlowConfig.dataBaseType === 'lark') { - log.info('消息写入到lark:', JSON.stringify(record)) - await LarkChat.addChatRecord(record) - } else { - log.info('消息写入到vika:', JSON.stringify(record)) - await MessageChat.addChatRecord(record) - } + // log.info('消息添加到队列:', JSON.stringify(record)) + log.info('消息添加到队列...', record.id) + await MessageChat.addChatRecord(record) // log.info('消息写入数据库成功:', res._id) return true } catch (e) { @@ -345,11 +330,7 @@ export const saveMessageToCloud = async (record:any) => { // 处理二维码上传 export const uploadQRCodeToCloud = async (qrcode: string, status: ScanStatus) => { - if (ChatFlowConfig.dataBaseType === 'lark') { - await LarkChat.uploadQRCodeToVika(qrcode, status) - } else { - await MessageChat.uploadQRCodeToVika(qrcode, status) - } + await MessageChat.uploadQRCodeToVika(qrcode, status) } // 上传图片/文件到云 diff --git a/src/api/mod.ts b/src/api/mod.ts new file mode 100644 index 00000000..948dcb7d --- /dev/null +++ b/src/api/mod.ts @@ -0,0 +1,19 @@ +import * as contact from './contact.js' +import * as order from './order.js' +import * as groupNotice from './group-notice.js' +import * as qa from './qa.js' +import * as user from './user.js' +import * as keyword from './keyword.js' +import * as room from './room.js' +import * as statistic from './statistic.js' + +export { + contact, + order, + groupNotice, + qa, + user, + keyword, + room, + statistic, +} diff --git a/src/api/notice.ts b/src/api/notice.ts index d668a509..75c09b03 100644 --- a/src/api/notice.ts +++ b/src/api/notice.ts @@ -1,21 +1,21 @@ -import { NoticeChat } from '../services/mod.js' -// 获取定时提醒列表 -export const getNoticeList = async (_params: any) => { - const res = await NoticeChat.db.findAll() - return res +import { get, post } from '../utils/request.js' + +// 获取定时任务列表服务接口 +export const ServeGetNotices = () => { + return get('/api/v1/notice/list') } -// 创建定时提醒 -export const createNotice = (data: any) => { - return data +// 获取定时任务列表服务接口 +export const ServeGetNoticesTask = () => { + return get('/api/v1/notice/task') } -// 删除定时提醒 -export const deleteNotice = (data: any) => { - return data +// 创建定时任务服务接口 +export const ServeCreateNotices = (data: {} | undefined) => { + return post('/api/v1/notice/create', data) } -// 更新定时提醒 -export const updateNotice = (data: any) => { - return data +// 删除定时任务服务接口 +export const ServeDeleteNotices = (data: {} | undefined) => { + return post('/api/v1/notice/delete', data) } diff --git a/src/api/order.ts b/src/api/order.ts index b42c9ad2..496c1c10 100644 --- a/src/api/order.ts +++ b/src/api/order.ts @@ -1,31 +1,22 @@ -import { OrderChat } from '../services/mod.js' -// 获取订单列表 -export const getOrderList = async (_params: any) => { - const res = await OrderChat.db.findAll() - return res -} +import { get, post } from '../utils/request.js' -// 获取订单详情 -export const getOrderDetail = (params: any) => { - return params +// 获取订单列表服务接口 +export const ServeGetOrders = () => { + return get('/api/v1/order/list') } -// 创建订单 -export const createOrder = (data: any) => { - return data +// 创建订单服务接口 +export const ServeCreateOrders = (data: {} | undefined) => { + return post('/api/v1/order/create', data) } -// 取消订单 -export const cancelOrder = (data: any) => { - return data +// 删除订单服务接口 +export const ServeDeleteOrders = (data: {} | undefined) => { + return post('/api/v1/order/delete', data) } -// 删除订单 -export const deleteOrder = (data: any) => { - return data -} - -// 更新订单 -export const updateOrder = (data: any) => { - return data +// 获取订单列表 +export const getOrderList = async (_params: any) => { + const res = await ServeGetOrders() + return res } diff --git a/src/api/qa.ts b/src/api/qa.ts index fb1a0d12..db25440f 100644 --- a/src/api/qa.ts +++ b/src/api/qa.ts @@ -1,26 +1,16 @@ -import { QaChat } from '../services/mod.js' -// 获取问答列表 -export const getQaList = async (_params: any) => { - const res = await QaChat.db.findAll() - return res -} - -// 添加问题 -export const addQa = (data: any) => { - return data -} +import { get, post } from '../utils/request.js' -// 删除问题 -export const deleteQa = (data: any) => { - return data +// 获取问答列表服务接口 +export const ServeGetQas = () => { + return get('/api/v1/qa/list') } -// 更新问题 -export const updateQa = (data: any) => { - return data +// 创建问答服务接口 +export const ServeCreateQas = (data: {} | undefined) => { + return post('/api/v1/qa/create', data) } -// 发布问题 -export const publishQa = (data: any) => { - return data +// 删除问答服务接口 +export const ServeDeleteQas = (data: {} | undefined) => { + return post('/api/v1/qa/delete', data) } diff --git a/src/api/room.ts b/src/api/room.ts index 2c1e31c6..b9cf8d3e 100644 --- a/src/api/room.ts +++ b/src/api/room.ts @@ -1,7 +1,147 @@ /* eslint-disable no-console */ /* eslint-disable camelcase */ -import { RoomChat } from '../services/mod.js' import { db } from '../db/tables.js' +import { post, get } from '../utils/request.js' + +// 查询用户群聊服务接口 +export const ServeGetGroups = () => { + return get('/api/v1/group/list') +} + +// 获取群聊列表服务接口 +export const ServeGetGroupsRaw = () => { + return get('/api/v1/group/list/raw') +} + +// 更新用户群聊服务接口 +export const ServeUpdateGroups = (data: any | undefined) => { + return post('/api/v1/group/update', data) +} + +// 删除群聊服务接口 +export const ServeDeleteGroups = (data: any | undefined) => { + return post('/api/v1/group/delete', data) +} + +// 批量删除群聊服务接口批量 +export const ServeDeleteBatchGroups = (data: any | undefined) => { + return post('/api/v1/group/deleteBatch', data) +} + +export const ServeGroupOvertList = () => { + return get('/api/v1/group/overt/list') +} + +// 获取群信息服务接口 +export const ServeGroupDetail = (data: {} | undefined) => { + return get('/api/v1/group/detail', data) +} + +// 创建群聊服务接口 +export const ServeCreateGroup = (data: {} | undefined) => { + return post('/api/v1/group/create', data) +} + +// 创建群聊服务接口 +export const ServeCreateGroupBatch = (data: {} | undefined) => { + return post('/api/v1/group/create/batch', data) +} + +// 修改群信息 +export const ServeEditGroup = (data: {} | undefined) => { + return post('/api/v1/group/setting', data) +} + +// 邀请好友加入群聊服务接口 +export const ServeInviteGroup = (data: {} | undefined) => { + return post('/api/v1/group/invite', data) +} + +// 移除群聊成员服务接口 +export const ServeRemoveMembersGroup = (data: {} | undefined) => { + return post('/api/v1/group/member/remove', data) +} + +// 管理员解散群聊服务接口 +export const ServeDismissGroup = (data: {} | undefined) => { + return post('/api/v1/group/dismiss', data) +} + +export const ServeMuteGroup = (data: {} | undefined) => { + return post('/api/v1/group/mute', data) +} + +export const ServeOvertGroup = (data: {} | undefined) => { + return post('/api/v1/group/overt', data) +} + +// 用户退出群聊服务接口 +export const ServeSecedeGroup = (data: {} | undefined) => { + return post('/api/v1/group/secede', data) +} + +// 修改群聊名片服务接口 +export const ServeUpdateGroupCard = (data: {} | undefined) => { + return post('/api/v1/group/member/remark', data) +} + +// 获取用户可邀请加入群聊的好友列表 +export const ServeGetInviteFriends = (data: {} | undefined) => { + return get('/api/v1/group/member/invites', data) +} + +// 获取群聊成员列表 +export const ServeGetGroupMembers = (data: {} | undefined) => { + return get('/api/v1/group/member/list', data) +} + +// 获取群聊公告列表 +export const ServeGetGroupNotices = (data: {} | undefined) => { + return get('/api/v1/group/notice/list', data) +} + +// 编辑群公告 +export const ServeEditGroupNotice = (data: {} | undefined) => { + return post('/api/v1/group/notice/edit', data) +} + +export const ServeGetGroupApplyList = (data: {} | undefined) => { + return get('/api/v1/group/apply/list', data) +} + +export const ServeGetGroupApplyAll = (data: {} | undefined) => { + return get('/api/v1/group/apply/all', data) +} + +export const ServeDeleteGroupApply = (data: {} | undefined) => { + return post('/api/v1/group/apply/decline', data) +} + +export const ServeAgreeGroupApply = (data: {} | undefined) => { + return post('/api/v1/group/apply/agree', data) +} + +export const ServeCreateGroupApply = (data: {} | undefined) => { + return post('/api/v1/group/apply/create', data) +} + +export const ServeGroupApplyUnread = (data: {} | undefined) => { + return get('/api/v1/group/apply/unread', data) +} + +// 转让群主 +export const ServeGroupHandover = (data: {} | undefined) => { + return post('/api/v1/group/handover', data) +} + +// 分配管理员 +export const ServeGroupAssignAdmin = (data: {} | undefined) => { + return post('/api/v1/group/assign-admin', data) +} + +export const ServeGroupNoSpeak = (data: {} | undefined) => { + return post('/api/v1/group/no-speak', data) +} const rdb = db.room /** @@ -72,7 +212,8 @@ export async function getRoomRecordContent (rooName:any, day:any): Promise // 查询用户群聊列表接口 export async function getRoomList (): Promise { - const roomListRaw:any = await RoomChat.findAll() + const res = await ServeGetGroups() + const roomListRaw:any = res.data.items // console.log('roomListRaw', JSON.stringify(roomListRaw)) const roomList = roomListRaw.map((value: { fields: { topic: any; avatar: any; ownerId: any; id: any }; recordId: any }) => { if (value.fields.topic) { diff --git a/src/api/setting.ts b/src/api/setting.ts deleted file mode 100644 index 9ca3241d..00000000 --- a/src/api/setting.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { EnvChat } from '../services/mod.js' - -// 获取系统配置信息 -export const getSetting = async () => { - await EnvChat.init() - const settings = await EnvChat.getConfigFromVika() - return settings -} - -// 批量更新配置 -export const updateSetting = (setting: any) => { - return setting -} - -// 更新指定配置 -export const updateSettingByKey = (key: string, value: any) => { - return [ key, value ] -} diff --git a/src/api/statistic.ts b/src/api/statistic.ts index 55187b6b..9f5b3f34 100644 --- a/src/api/statistic.ts +++ b/src/api/statistic.ts @@ -1,21 +1,16 @@ -import { StatisticChat } from '../services/mod.js' -// 获取统计打卡列表 -export const getStatisticList = async (_params: any) => { - const res = await StatisticChat.db.findAll() - return res -} +import { get, post } from '../utils/request.js' -// 创建统计打卡 -export const createStatistic = (data: any) => { - return data +// 获取好友列表服务接口 +export const ServeGetStatistics = () => { + return get('/api/v1/statistic/list') } -// 删除统计打卡 -export const deleteStatistic = (data: any) => { - return data +// 创建好友服务接口 +export const ServeCreateStatistics = (data: {} | undefined) => { + return post('/api/v1/statistic/create', data) } -// 更新统计打卡 -export const updateStatistic = (data: any) => { - return data +// 删除好友服务接口 +export const ServeDeleteStatistics = (data: {} | undefined) => { + return post('/api/v1/statistic/delete', data) } diff --git a/src/api/talk.ts b/src/api/talk.ts index ea12110d..6c6747de 100644 --- a/src/api/talk.ts +++ b/src/api/talk.ts @@ -1,348 +1,5 @@ /* eslint-disable camelcase */ /* eslint-disable no-console */ -import { MessageChat } from '../services/mod.js' -import { ChatFlowConfig } from '../api/base-config.js' -// 获取对话列表 -export const getTalkList = async (_params: any) => { - const res = await MessageChat.findAll() - console.debug('ServeGetTalkList res', res) - const data: any = { - items: [ - { - avatar: 'https://im.gzydong.com/public/media/image/talk/20220221/447d236da1b5787d25f6b0461f889f76_96x96.png', - id: 146, - is_disturb: 0, - is_online: 0, - is_robot: 1, - is_top: 0, - msg_text: '[登录消息]', - name: '登录助手', - receiver_id: 4257, - remark: '', - talk_type: 1, - unread_num: 0, - updated_at: '2023-10-26 17:20:33', - }, - { - avatar: '', - id: 34036, - is_disturb: 0, - is_online: 0, - is_robot: 0, - is_top: 0, - msg_text: 'ok', - name: '11111', - receiver_id: 1028, - remark: '', - talk_type: 2, - unread_num: 0, - updated_at: '2023-10-26 18:00:57', - }, - { - avatar: '', - id: 34062, - is_disturb: 0, - is_online: 0, - is_robot: 0, - is_top: 0, - msg_text: '[群成员解除禁言消息]', - name: '阿萨德', - receiver_id: 1029, - remark: '', - talk_type: 2, - unread_num: 0, - updated_at: '2023-10-26 15:23:28', - }, - ], - } - const items = res.map((value:any) => { - const { roomid, wxid, listenerid, messagePayload, name, alias, listener, timeHms, topic, wxAvatar, roomAvatar, listenerAvatar } = value.fields - let id = '' - let dispayname = '' - let avatar = '' - let receiver_id = '' - - if (roomid !== '--') { - id = roomid - dispayname = topic - avatar = roomAvatar - receiver_id = roomid - } else if (listenerid === ChatFlowConfig.bot.currentUser.id) { - id = wxid - dispayname = alias || name - avatar = wxAvatar - receiver_id = wxid - } else { - id = listenerid - dispayname = listener - avatar = listenerAvatar - receiver_id = listenerid - } - if (dispayname) { - return { - avatar: avatar || 'https://im.gzydong.com/public/media/image/talk/20220221/447d236da1b5787d25f6b0461f889f76_96x96.png', - id, - is_disturb: 0, - is_online: 0, - is_robot: 0, - is_top: 0, - msg_text: messagePayload, - name: roomid !== '--' ? topic : dispayname, - receiver_id, - remark: '', - talk_type: roomid !== '--' ? 2 : 1, - unread_num: 0, - updated_at: timeHms, - recordId: value.recordId, - } - } - return false - }).filter(item => item !== false) - - // 过滤items对象数组中receiver_id相同的对象,只保留最新的一条 - const filteredItems = items.reduce((acc: any, curr: any) => { - const existingItem = acc.find((item: any) => item.id === curr.id) - if (existingItem) { - if (existingItem.updated_at < curr.updated_at) { - acc.splice(acc.indexOf(existingItem), 1, curr) - } - } else { - acc.push(curr) - } - return acc - }, []) - - data.items = filteredItems - - console.debug('ServeGetTalkList talks', data) - return data -} - -// 创建对话 -export const createTalk = (_params: { - talk_type:1|2, - receiver_id:string -}) => { - // {talk_type: 2, receiver_id: "R:10792119241893300"} - const data = { - id: 0, - talk_type: 1, - receiver_id: 0, - name: '未设置昵称', - remark_name: '', - avatar: '', - is_disturb: 0, - is_top: 0, - is_online: 0, - is_robot: 0, - unread_num: 0, - content: '......', - draft_text: '', - msg_text: '', - index_name: '', - created_at: new Date().getTime(), - } - return data -} - -// 获取对话聊天记录接口 -export const getTalkRecord = async (param: { - record_id:string, - receiver_id:string, - talk_type:1|2, - limit:number -}) => { - // ?record_id=0&receiver_id=2053&talk_type=1&limit=30 - param.record_id = ChatFlowConfig.bot.currentUser.id - const data = { - items: [ - { - id: 12013, - sequence: 29, - msg_id: '1d54eb03ebd146168bf92880b83f039c', - talk_type: 1, - msg_type: 1, - user_id: 2055, - receiver_id: 2053, - nickname: '老牛逼了', - avatar: 'https://im.gzydong.com/public/media/image/avatar/20230530/f76a14ce98ca684752df742974f5473a_200x200.png', - is_revoke: 0, - is_mark: 0, - is_read: 0, - content: '000', - created_at: '2023-09-16 00:49:07', - extra: {}, - }, - { - id: 12010, - sequence: 28, - msg_id: '40568d70349b2d01fe1898217bcc7cfa', - talk_type: 1, - msg_type: 4, - user_id: 2055, - receiver_id: 2053, - nickname: '老牛逼了', - avatar: 'https://im.gzydong.com/public/media/image/avatar/20230530/f76a14ce98ca684752df742974f5473a_200x200.png', - is_revoke: 0, - is_mark: 0, - is_read: 0, - content: '', - created_at: '2023-09-15 22:11:49', - extra: { - duration: 0, - name: '', - size: 245804, - suffix: 'wav', - url: 'https://im.gzydong.com/public/media/20230915/bad860440e5fb72974cb2426bead46d6.wav', - }, - }, - ], - } - - // data = { - // record_id: loadConfig.minRecord, - // receiver_id: props.receiver_id, - // talk_type: props.talk_type, - // limit: 30, - // } - let res: any = [] - - if (param.talk_type === 2) { - res = await MessageChat.findByField('roomid', param.receiver_id) - - } else { - res = await MessageChat.findByQuery(`({接收人ID|listenerid}="${param.receiver_id}"&&{好友ID|wxid}="${param.record_id}")||({接收人ID|listenerid}="${param.record_id}"&&{好友ID|wxid}="${param.receiver_id}")`) - } - console.debug('vika res', res) - const items = res.map((value: { fields?: any; recordId?: any }) => { - const { recordId } = value - const { roomid, messagePayload, name, alias, timeHms, messageType, messageId, wxAvatar, file } = value.fields - const { wxid, listenerid } = value.fields - const user_id = wxid - const dispayname = alias || name - const talk_type = roomid !== '--' ? 2 : 1 - let msg_type = 1 - let extra = {} - const receiver_id = roomid !== '--' ? roomid : listenerid - if (file) { - const file0 = file[0] - - // const fileBase64 = await downloadImage(file0) - // console.debug(fileBase64) - - extra = { - height: file0.height, - name: file0.name, - size: file0.size, - suffix: file0.mimeType, - url: file0.url, - width: file0.width, - } - } - - switch (messageType) { - case 'Text': - msg_type = 1 - break - case 'Image': - msg_type = 3 - break - case 'Emoticon': - msg_type = 1 - break - case 'ChatHistory': - msg_type = 9 - break - case 'Audio': - msg_type = 4 - break - case 'Attachment': - msg_type = 6 - break - case 'Video': - msg_type = 5 - break - case 'MiniProgram': - msg_type = 1 - break - case 'Url': - msg_type = 1 - break - case 'Recalled': - msg_type = 1 - break - case 'RedEnvelope': - msg_type = 1 - break - case 'Contact': - msg_type = 1 - break - case 'Location': - msg_type = 1 - break - case 'GroupNote': - msg_type = 1 - break - case 'Transfer': - msg_type = 1 - break - case 'Post': - msg_type = 1 - break - case 'qrcode': - msg_type = 3 - break - case 'Unknown': - msg_type = 1 - break - default: - break - } - - if (dispayname && messageType !== 'Unknown') { - const id = new Date(timeHms).getTime() - return { - id, - sequence: id, - msg_id: messageId, - talk_type, - msg_type, - user_id, - receiver_id, - nickname: dispayname, - avatar: wxAvatar || 'https://im.gzydong.com/public/media/image/talk/20220221/447d236da1b5787d25f6b0461f889f76_96x96.png', - is_revoke: 0, - is_mark: 0, - is_read: 0, - content: messagePayload, - created_at: timeHms, - extra, - recordId, - } - } - return false - }).filter((item: boolean) => item !== false) - data.items = items - console.debug('records', data) - return data -} - -// 查找用户指定对话聊天记录 -export const searchTalkRecord = (params: any) => { - return params -} - -// 搜索用户聊天记录 -export const searchUserRecord = (params: { - talk_type:1|2, - receiver_id:string, - record_id:string, - msg_type:number, - limit:number -}) => { - // talk_type=1&receiver_id=2054&record_id=0&msg_type=0&limit=30 - return params -} // 获取聊天记录上下文接口 export const getTalkRecordContext = (params: any) => { diff --git a/src/api/upload.ts b/src/api/upload.ts new file mode 100644 index 00000000..555d4bb8 --- /dev/null +++ b/src/api/upload.ts @@ -0,0 +1,29 @@ +/* eslint-disable no-console */ +import { post, upload } from '../utils/request.js' + +// 上传头像裁剪图片服务接口 +export const ServeUploadImageVika = (data: {} | undefined, options = {}) => { + console.log(data) + return upload('/api/v1/upload/imageVika', data, options) +} + +// 上传头像裁剪图片服务接口 +export const ServeUploadAvatar = (data: {} | undefined) => { + return post('/api/v1/upload/avatar', data) +} + +// 上传头像裁剪图片服务接口 +export const ServeUploadImage = (data: {} | undefined) => { + console.log(data) + return post('/api/v1/upload/image', data) +} + +// 查询大文件拆分信息服务接口 +export const ServeFindFileSplitInfo = (data = {}) => { + return post('/api/v1/upload/multipart/initiate', data) +} + +// 文件拆分上传服务接口 +export const ServeFileSubareaUpload = (data = {}, options = {}) => { + return upload('/api/v1/upload/multipart', data, options) +} diff --git a/src/api/user.ts b/src/api/user.ts index 180e923f..3f4bdcdf 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { EnvChat } from '../services/mod.js' +import { post, get } from '../utils/request.js' // 查询用户信息接口 export const getUserInfo = (_params: any) => { @@ -16,32 +16,56 @@ export const getUserInfo = (_params: any) => { return data } -// 获取用户相关设置信息接口 -export const getUserSetting = async (_params: any) => { - const res = await EnvChat.findByField('key', 'BASE_BOT_ID') - - console.debug('ServeLoginVika:', res) - - const userInfo: any = { - setting: { - keyboard_event_notify: '', - notify_cue_tone: '', - theme_bag_img: '', - theme_color: '', - theme_mode: '', - }, - user_info: { - avatar: '', - email: '', - gender: 0, - is_qiye: false, - mobile: '13800138000', - motto: '', - nickname: '超哥', - uid: res[0]?.fields['value'] || '', - }, - } +// 修改密码服务接口 +export const ServeUpdatePassword = (data: {} | undefined) => { + return post('/api/v1/users/change/password', data) +} + +// 修改手机号服务接口 +export const ServeUpdateMobile = (data: {} | undefined) => { + return post('/api/v1/users/change/mobile', data) +} + +// 修改手机号服务接口 +export const ServeUpdateEmail = (data: {} | undefined) => { + return post('/api/v1/users/change/email', data) +} + +// 修改个人信息服务接口 +export const ServeUpdateUserDetail = (data: {} | undefined) => { + return post('/api/v1/users/change/detail', data) +} + +// 查询用户信息服务接口 +export const ServeGetUserDetail = () => { + return get('/api/v1/users/detail') +} + +// 获取用户相关设置信息 +export const ServeGetUserSetting = () => { + return get('/api/v1/users/setting') +} + +// 获取用户相关系统配置信息 +export const ServeGetUserConfig = () => { + return get('/api/v1/users/config') +} + +// 获取用户相关系统配置信息分组 +export const ServeGetUserConfigGroup = () => { + return get('/api/v1/users/config/group') +} + +// 获取用户相关系统配置信息 +export const ServeGetUserConfigObj = () => { + return get('/api/v1/users/config/keys') +} + +// 修改配置服务接口 +export const ServeUpdateConfig = (data: {} | undefined) => { + return post('/api/v1/users/config', data) +} - console.debug('userInfo:', userInfo) - return userInfo +export const ServeGetUserConfigBykey = (data: {key:string;value:string}) => { + return post('/api/v1/users/config/bykey', data) } diff --git a/src/api/vika/contact.ts b/src/api/vika/contact.ts deleted file mode 100644 index 7b82828a..00000000 --- a/src/api/vika/contact.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { BaseEntity, MappingOptions } from '../../db/vika-orm.js' // 导入 BaseEntity, VikaOptions, 和 MappingOptions 类型/类 -import { logger } from '../../utils/mod.js' // 导入 logger 对象 - -const mappingOptions: MappingOptions = { // 定义字段映射选项 - fieldMapping: { // 字段映射 - alias: '备注名称|alias', - id: '好友ID|id', - name: '好友昵称|name', - gender: '性别|gender', - updated: '更新时间|updated', - friend: '是否好友|friend', - type: '类型|type', - avatar: '头像|avatar', - phone: '手机号|phone', - file: '头像图片|file', - - }, - tableName: '好友列表|Contact', // 表名 -} - -/** - * 好友实体 - */ -export class Contact extends BaseEntity { // 用户类继承 BaseEntity - - name?: string // 定义名字属性,可选 - - id?: string - - alias?: string - - gender?: string - - updated?: string - - friend?: string - - type?: string - - avatar?: string - - phone?: string - - file?: string - - // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 - - protected static override mappingOptions: MappingOptions = mappingOptions // 设置映射选项为上面定义的 mappingOptions - - protected static override getMappingOptions (): MappingOptions { // 获取映射选项的方法 - return this.mappingOptions // 返回当前类的映射选项 - } - - static override setMappingOptions (options: MappingOptions) { // 设置映射选项的方法 - this.mappingOptions = options // 更新当前类的映射选项 - } - -} - -// 获取好友列表服务接口 -export const ServeGetContactsVika = async () => { - const res = await Contact.findAll() - // console.debug(res) - const contacts: any = { - code: 200, - message: 'success', - data: { - items: [ - { - avatar: '', - gender: 0, - group_id: 0, - id: 7, - is_online: 0, - motto: '', - nickname: 'test5', - remark: 'test5', - }, - ], - }, - } - - const items = res.map((value:any) => { - if (value.fields.name) { - return { - avatar: value.fields.avatar, - gender: value.fields.gender, - group_id: 0, - id: value.fields.id, - is_online: 0, - motto: '', - nickname: value.fields.name, - remark: value.fields.alias, - recordId: value.recordId, - } - } - return false - }).filter(item => item !== false) - - contacts.data.items = items - return contacts -} - -// 查询用户信息服务接口 -export const ServeSearchContactVika = async (parse:{name?:string, id:string}) => { - const id = parse.id - logger.debug('id:' + id) - let contact:any = { - code: 200, - message: 'success', - data: { - avatar: '', - gender: 0, - group_id: 0, - id: 7, - is_online: 0, - motto: '', - nickname: 'test5', - remark: 'test5', - }, - } - const res = await Contact.findByField('id', id) - let item = {} - if (res.length > 0) { - const value:any = res[0] - item = { - avatar: value.fields.avatar, - gender: value.fields.gender, - group_id: 0, - id: value.fields.id, - is_online: 0, - motto: '', - nickname: value.fields.name, - remark: value.fields.alias, - recordId: value.recordId, - email: 0, - friend_apply: 0, - friend_status: 2, - mobile: '--', - } - contact.data = item - } else { - contact = { - code: 305, - message: `strconv.ParseInt: parsing "${id}": invalid syntax`, - } - } - - logger.info('contact' + JSON.stringify(contact)) - return contact -} - -// 更新好友列表服务接口 -export const ServeUpdateContactsVika = async (parse: {recordId:string, fields:{[key:string]:any}}) => { - try { - await Contact.update(parse.recordId, parse.fields) - return { - code: 200, - message: 'success', - } - } catch (e) { - return { - code: 400, - message: e, - } - } - -} diff --git a/src/api/vika/room.ts b/src/api/vika/room.ts deleted file mode 100644 index 3e3f76ec..00000000 --- a/src/api/vika/room.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { BaseEntity, MappingOptions } from '../../db/vika-orm.js' // 导入 BaseEntity, VikaOptions, 和 MappingOptions 类型/类 -import { logger } from '../../utils/mod.js' // 导入 logger 对象 - -// // 定义一个延时方法 -// const wait = (ms: number) => new Promise(resolve => { -// setTimeout(resolve, ms); -// }); - -const mappingOptions: MappingOptions = { // 定义字段映射选项 - fieldMapping: { // 字段映射 - id: '群ID|id', - topic: '群名称|topic', - ownerId: '群主ID|ownerId', - updated: '更新时间|updated', - avatar: '头像|avatar', - file: '头像图片|file', - - }, - tableName: '群列表|Room', // 表名 -} - -/** - * 用户实体 - */ -export class Group extends BaseEntity { // 用户类继承 BaseEntity - - topic?: string // 定义名字属性,可选 - - id?: string - - alias?: string - - updated?: string - - avatar?: string - - file?: string - - // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 - - protected static override mappingOptions: MappingOptions = mappingOptions // 设置映射选项为上面定义的 mappingOptions - - protected static override getMappingOptions (): MappingOptions { // 获取映射选项的方法 - return this.mappingOptions // 返回当前类的映射选项 - } - - static override setMappingOptions (options: MappingOptions) { // 设置映射选项的方法 - this.mappingOptions = options // 更新当前类的映射选项 - } - -} - -// 查询用户群聊服务接口 -export const ServeGetGroupsVika = async () => { - const res = await Group.findAll() - logger.info('res:', JSON.stringify(res)) - const groups: any = { - code: 200, - message: 'success', - data: { - items: [ - { - avatar: '', - creator_id: 2055, - group_name: '抖聊开发群', - id: 1026, - is_disturb: 0, - leader: 2, - profile: '', - }, - ], - }, - } - - const items = res.map((value:any) => { - if (value.fields.topic) { - return { - avatar: value.fields.avatar, - creator_id: value.fields.ownerId, - group_name: value.fields.topic, - id: value.fields.id, - is_disturb: 0, - leader: 2, - profile: '', - recordId: value.recordId, - } - } - return false - }).filter(item => item !== false) - - groups.data.items = items - return groups -} - -// 获取群信息服务接口 -export const ServeGroupDetailVika = async (parse:any) => { - const id = parse.group_id - logger.info('id' + id) - let group:any = { - code: 200, - message: 'success', - data: { - avatar: '', - created_at: '2023-10-20 11:09:48', - group_id: 1012, - group_name: '测试一下', - is_disturb: 0, - is_manager: true, - is_mute: 0, - is_overt: 0, - profile: '', - visit_card: '', - }, - } - const res:any[] = await Group.findByField('id', id) - let item = {} - if (res.length > 0) { - const groupInfo = res[0].fields - item = { - avatar: groupInfo.avatar, - creator_id: groupInfo.ownerId, - group_name: groupInfo.topic, - id: groupInfo.id, - is_disturb: 0, - leader: 2, - profile: '', - recordId: groupInfo.recordId, - } - group.data = item - } else { - group = { - code: 305, - message: `strconv.ParseInt: parsing "${id}": value out of range`, - } - } - - logger.info(group) - return group -} diff --git a/src/api/welcome.ts b/src/api/welcome.ts new file mode 100644 index 00000000..2a2c6a46 --- /dev/null +++ b/src/api/welcome.ts @@ -0,0 +1,16 @@ +import { get, post } from '../utils/request.js' + +// 获取问答列表服务接口 +export const ServeGetWelcomes = () => { + return get('/api/v1/welcome/list') +} + +// 创建问答服务接口 +export const ServeCreateWelcomes = (data: {} | undefined) => { + return post('/api/v1/welcome/create', data) +} + +// 删除问答服务接口 +export const ServeDeleteWelcomes = (data: {} | undefined) => { + return post('/api/v1/welcome/delete', data) +} diff --git a/src/api/white-list.ts b/src/api/white-list.ts index 8d1a5c99..9d594a6b 100644 --- a/src/api/white-list.ts +++ b/src/api/white-list.ts @@ -1,21 +1,26 @@ -import { WhiteListChat } from '../services/mod.js' -// 获取黑白名单 -export const getWhiteList = async () => { - const res = await WhiteListChat.db.findAll() - return res +import { get, post } from '../utils/request.js' + +// 获取白名单列表服务接口 +export const ServeGetWhitelistWhite = () => { + return get('/api/v1/whitelist/list/white') +} + +// 获取白名单列表服务接口 +export const ServeGetWhitelistWhiteObject = () => { + return get('/api/v1/whitelist/list/white/object') } -// 更新黑白名单 -export const updateWhiteList = (data: any) => { - return data +// 创建黑名单列表服务接口 +export const ServeCreateWhitelistWhite = (data: {} | undefined) => { + return post('/api/v1/whitelist/list/white/create', data) } -// 添加黑白名单 -export const addWhiteList = (data: any) => { - return data +// 删除黑名单列表服务接口 +export const ServeDeleteWhitelistWhite = (data: {} | undefined) => { + return post('/api/v1/whitelist/list/white/delete', data) } -// 移除黑白名单 -export const deleteWhiteList = (data: any) => { - return data +// 获取黑名单列表服务接口 +export const ServeGetWhitelistBlack = (data: {} | undefined) => { + return get('/api/v1/whitelist/list/black', data) } diff --git a/src/app/carpooling.ts b/src/app/carpooling.ts new file mode 100644 index 00000000..8770798d --- /dev/null +++ b/src/app/carpooling.ts @@ -0,0 +1,171 @@ +import { Message, log } from 'wechaty' +import axios from 'axios' +import { ChatFlowConfig } from '../api/base-config.js' + +export interface CarpoolInfoChinese { + /** + * 类型(人找车、车找人) + */ + '类型(人找车、车找人)': '人找车' | '车找人'; + /** + * 出发地 + */ + 出发地: string; + /** + * 目的地 + */ + 目的地: string; + /** + * 出发日期 + */ + 出发日期: string; + /** + * 出发时间 + */ + 出发时间: string; + /** + * 联系电话 + */ + 联系电话: string; + /** + * 发布人 + */ + 发布人: string; + /** + * 车费 + */ + 车费: string; + /** + * 途经路线 + */ + 途经路线: string; + /** + * 原始消息 + */ + 原始消息: string; + /** + * 状态 + */ + 状态: '开启'|'关闭'; +} + +export interface CarpoolInfo { + /** + * Type (车找人, 人找车) + */ + type: '车找人' | '人找车'; + /** + * Departure location + */ + departureLocation: string; + /** + * Destination + */ + destination: string; + /** + * Departure date + */ + departureDate: string; + /** + * Departure time + */ + departureTime: string; + /** + * Contact phone + */ + contactPhone: string; + /** + * Publisher + */ + publisher: string; + /** + * Car fee + */ + carFee: string; + /** + * Route + */ + route: string; + /** + * Text + */ + text: string; + /** + * state + */ + topic: string; + roomId: string; + wxid: string; + createdAt: string; + state: '开启'|'关闭'; +} + +// 获取格式化后的顺风车信息 +async function getFormattedRideInfo (message:Message) { + const text: string = message.text() + const room = message.room() + const talker = message.talker() + const name:string = message.talker().name() + const apiUrl = `${ChatFlowConfig.configEnv.CHATGPT_ENDPOINT}/v1/chat/completions` + const headers = { + Authorization: `Bearer ${ChatFlowConfig.configEnv.CHATGPT_KEY}`, // <-- 把 fkxxxxx 替换成你自己的 Forward Key,注意前面的 Bearer 要保留,并且和 Key 中间有一个空格。 + 'Content-Type': 'application/json', + } + const content = `从"发布人:${name}\n信息:${text}"中提取出顺风车信息,提取不到的内容填写“暂无”: +{ + "类型(人找车、车找人)":"", + "出发地":"", + "目的地":"", + "出发日期":"", + "出发时间":"": + "联系电话":"", + "发布人":"", + "车费":"", + "途经路线":"" +}` + const payload = { + messages: [ { content, role: 'user' } ], + model: ChatFlowConfig.configEnv.CHATGPT_MODEL, // 'gpt-3.5-turbo + } + + try { + const response = await axios.post(apiUrl, payload, { headers }) + log.info('顺风车信息检测结果:', JSON.stringify(response.data)) + const rideInfo = response.data + const content = rideInfo.choices[0].message.content + // 先去除content中的换行符,再取出{}之间的字符串(包含{}) + const contentStr = content.replace(/[\r\n]/g, '').match(/{.*}/) + log.info('contentStr信息:', contentStr) + try { + // 对contentStr进行JSON.parse,如果失败则说明contentStr不是json格式 + const rideInfoObj = JSON.parse(contentStr) as CarpoolInfoChinese + log.info('rideInfoObj信息:', JSON.stringify(rideInfoObj, null, 2)) + const rideInfoObjFormatted: CarpoolInfo = { + type: rideInfoObj['类型(人找车、车找人)'], + departureLocation: rideInfoObj.出发地, + destination: rideInfoObj.目的地, + departureDate: rideInfoObj.出发日期, + departureTime: rideInfoObj.出发时间, + contactPhone: rideInfoObj.联系电话, + publisher: rideInfoObj.发布人, + carFee: rideInfoObj.车费, + route: rideInfoObj.途经路线, + text, + topic: await room?.topic() || '', + roomId: room?.id || '', + wxid: talker.id, + createdAt: new Date().toLocaleString(), + state: '开启', + } + return rideInfoObjFormatted + } catch (error) { + log.error('rideInfoObj信息解析失败,不是JSON格式:', error) + throw new Error('rideInfoObj信息解析失败,不JSON格式') + } + } catch (error) { + log.error('顺风车信息检测失败:', error) + throw new Error('顺风车信息检测失败...') + } +} + +export { getFormattedRideInfo } diff --git a/src/app/chatbot.ts b/src/app/chatbot.ts new file mode 100644 index 00000000..a5ffca6d --- /dev/null +++ b/src/app/chatbot.ts @@ -0,0 +1,184 @@ +/* eslint-disable sort-keys */ +import { FileBox } from 'file-box' +import { + Message, + types, + Wechaty, + log, +} from 'wechaty' +import { formatSentMessage, logger } from '../utils/utils.js' +import axios from 'axios' +import { ChatFlowConfig } from '../index.js' +import type { ChatBotUser } from '../api/base-config.js' + +axios.defaults.timeout = 60000 + +async function chatbot (message: Message) { + const bot: Wechaty = ChatFlowConfig.bot + const keyword = '@' + bot.currentUser.name() + const text = extractKeyword(message, keyword) + const talker = message.talker() + const room = message.room() + let answer: any = {} + let chatBotUser:ChatBotUser | undefined + + try { + if (room && message.text().indexOf(keyword) !== -1 && !message.self()) { + log.info('当前群:' + JSON.stringify(room)) + const topic = await room.topic() + chatBotUser = ChatFlowConfig.chatBotUsers.find((user:ChatBotUser) => { + return user.room !== undefined && (user.room.id === room.id || user.room.topic === topic) + }) + log.info('room智聊服务chatBotUser:' + JSON.stringify(chatBotUser)) + } + + if (!room) { + log.info('当前用户:' + JSON.stringify(talker)) + const alias = await talker.alias() + log.info('ChatFlowConfig.chatBotUsers:' + JSON.stringify(ChatFlowConfig.chatBotUsers, null, 2)) + chatBotUser = ChatFlowConfig.chatBotUsers.find((user:ChatBotUser) => { + return user.contact !== undefined && (user.contact.id === talker.id || (user.contact.alias && user.contact.alias === alias) || user.contact.name === talker.name()) + }) + log.info('contact智聊服务chatBotUser:' + JSON.stringify(chatBotUser)) + } + } catch (e) { + logger.error('chatbot error:', e) + } + + logger.info('智聊服务chatBotUser:' + JSON.stringify(chatBotUser)) + // log.info('当前用户或群:' + JSON.stringify(room) + JSON.stringify(talker)) + // log.info('智聊服务chatBotUser:' + JSON.stringify(chatBotUser)) + + try { + if (message.type() === types.Message.Text && chatBotUser !== undefined) { + log.info('调用callGptbot...') + answer = await callGptbot(text, chatBotUser) + log.info('智聊回复消息:' + JSON.stringify(answer)) + } else { + log.info('chatBotUser 为空,不调用callGptbot智聊服务...') + return + } + + if (answer.messageType && answer.text) { + switch (answer.messageType) { + case types.Message.Text: + await sendMessage(answer.text, bot, message) + break + case types.Message.Image: + await sendImage(answer, bot, message) + break + case types.Message.MiniProgram: + await sendMiniProgram(answer, bot, message) + break + } + } + } catch (error) { + logger.error('请求智聊服务 Error:', error) + logger.error(`智聊查询内容,query: ${text}`) + } +} + +function extractKeyword (message: Message, keyword: string): string { + const text = message.text() + // 如果text以keyword开头或结尾,去除text开头或结尾的keyword,返回剩余的text + if (text.startsWith(keyword) || text.endsWith(keyword)) { + return text.replace(keyword, '').trim() + } + return text +} + +async function sendMessage (text: string, bot: Wechaty, message: Message) { + const formattedText = `${text}\n` + const talker = message.talker() + const room = message.room() + if (room) { + await room.say(formattedText, talker) + await formatSentMessage(bot.currentUser, formattedText, undefined, room) + } else { + await message.say(formattedText) + await formatSentMessage(bot.currentUser, formattedText, message.talker(), undefined) + } +} + +async function sendImage (answer: any, bot: Wechaty, message: Message) { + const fileBox = FileBox.fromUrl(answer.text.url) + const room = message.room() + if (room) { + await room.say(fileBox) + await formatSentMessage(bot.currentUser, fileBox.toString(), undefined, room) + } else { + await message.say(fileBox) + await formatSentMessage(bot.currentUser, fileBox.toString(), message.talker(), undefined) + } +} + +async function sendMiniProgram (answer: any, bot: Wechaty, message: Message) { + const room = message.room() + // ... (构建并发送MiniProgram的逻辑) + const miniProgram = new bot.MiniProgram({ + appid: answer.text.appid, + pagePath: answer.text.pagepath, + // thumbUrl: answer.text.thumb_url, + thumbKey: '42f8609e62817ae45cf7d8fefb532e83', + thumbUrl: 'https://openai-75050.gzc.vod.tencent-cloud.com/openaiassets_afffe2516dac42406e06eddc19303a8d.jpg', + title: answer.text.title, + }) + + if (room) { + await room.say(miniProgram) + await formatSentMessage(bot.currentUser, miniProgram.toString(), undefined, message.room()) + + } else { + await message.say(miniProgram) + await formatSentMessage(bot.currentUser, miniProgram.toString(), message.talker(), undefined) + } +} + +async function callGptbot (query: any, chatBotUser:ChatBotUser) { + const answer = { + text:'', + } + + try { + const response = await axios.post( + `${chatBotUser.chatbot.endpoint}/v1/chat/completions`, + { + model: chatBotUser.chatbot.model, // 使用的模型 + messages: [ + { role: 'system', content: chatBotUser.chatbot.prompt || 'You are a helpful assistant.' }, + { role: 'user', content: query }, + // {role: 'assistant', content: 'The Los Angeles Dodgers won the World Series in 2020.'}, + // {role: 'user', content: 'Where was it played?'} + ], // 对话消息数组 + }, + { + headers: { + Authorization: `Bearer ${chatBotUser.chatbot.key}`, + 'Content-Type': 'application/json', + }, + }, + ) + + log.info('智聊gptbot Response:' + JSON.stringify(response.data)) + + if (response.data.choices) { + return { + messageType: types.Message.Text, + text: response.data.choices[0].message.content, + } + } else { + return answer + } + } catch (error) { + log.error('请求智聊服务gptbot Error:', error) + logger.error(`查询内容,query: ${query}`) + return answer + + } +} + +export { + chatbot, +} + +export default chatbot diff --git a/src/app/meet-mate.ts b/src/app/meet-mate.ts index 0bfac481..9fdfa257 100644 --- a/src/app/meet-mate.ts +++ b/src/app/meet-mate.ts @@ -16,13 +16,12 @@ import qrcodeTerminal from 'qrcode-terminal' // 导入语雀相关模块 import Yuque from '@yuque/sdk' -import { ChatFlowConfig } from '../api/base-config.js' // 创建一个语雀客户端对象,使用环境变量中的token和repoId(需要提前设置) const yuqueClient = new Yuque({ - token:ChatFlowConfig.configEnv.YUQUE_TOKEN || 'xxx', + token: 'xxx', }) -const repoId = ChatFlowConfig.configEnv.YUQUE_NAMESPACE || 'xxx' +const repoId = 'xxx' // 创建一个Wechaty实例 // const bot = WechatyBuilder.build({ diff --git a/src/app/qa.ts b/src/app/qa.ts index def72e04..532df074 100644 --- a/src/app/qa.ts +++ b/src/app/qa.ts @@ -13,11 +13,12 @@ async function handleAutoQAForContact (message: Message, keyWord: string) { const talker = message.talker() const text = message.text() const includesKeyWord = text.indexOf(keyWord) !== -1 - log.info('消息中包含关键字:', includesKeyWord) + log.info('消息中包含关键字:', includesKeyWord ? '是' : '否') const AUTOQA_AUTOREPLY = ChatFlowConfig.configEnv.AUTOQA_AUTOREPLY - log.info('自动问答开关开启:', AUTOQA_AUTOREPLY || false) - // 问答开关开启,且消息中包含关键字 - if (AUTOQA_AUTOREPLY && includesKeyWord) { + log.info('自动问答开关开启状态:', AUTOQA_AUTOREPLY ? '开启' : '关闭') + + // 问答开关开启,私聊中不需要@机器人 + if (AUTOQA_AUTOREPLY) { // 判断是否在微信对话平台白名单内 const isInContactWhiteList = await containsContact(ChatFlowConfig.whiteList.contactWhiteList.qa, talker) if (isInContactWhiteList) { @@ -52,9 +53,9 @@ async function handleAutoQA (message: Message, keyWord: string) { const topic = await room.topic() const text = message.text() const includesKeyWord = text.indexOf(keyWord) !== -1 - log.info('消息中包含关键字:', includesKeyWord) + log.info('消息中包含关键字:', includesKeyWord ? '是' : '否') const AUTOQA_AUTOREPLY = ChatFlowConfig.configEnv.AUTOQA_AUTOREPLY - log.info('自动问答开关开启:', AUTOQA_AUTOREPLY || false) + log.info('自动问答开关开启:', AUTOQA_AUTOREPLY ? '开启' : '关闭') // 问答开关开启,且消息中包含关键字 if (AUTOQA_AUTOREPLY && includesKeyWord) { diff --git a/src/app/riding.ts b/src/app/riding.ts deleted file mode 100644 index daf754e7..00000000 --- a/src/app/riding.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Message, log } from 'wechaty' -import axios from 'axios' - -// 获取格式化后的顺风车信息 -async function getFormattedRideInfo (message:Message) { - let text: string = message.text() - const name:string = message.talker().name() - const apiUrl = 'https://openai.api2d.net/v1/chat/completions' - const headers = { - Authorization: 'Bearer xxxx', // <-- 把 fkxxxxx 替换成你自己的 Forward Key,注意前面的 Bearer 要保留,并且和 Key 中间有一个空格。 - 'Content-Type': 'application/json', - } - text = `从"发布人:${name}\n信息:${text}"中提取出:类型(人找车、车找人)、出发地、目的地、出发日期、出发时间、联系电话、发布人、车费、途经路线,不要输出任何其他的描述` - const payload = { - messages: [ { content: text, role: 'user' } ], - model: 'gpt-3.5-turbo', - } - - try { - const response = await axios.post(apiUrl, payload, { headers }) - log.info('顺风车信息检测结果:', JSON.stringify(response.data)) - return response.data - } catch (error) { - console.error(error) - return undefined - } -} - -export { getFormattedRideInfo } diff --git a/src/chatflow.ts b/src/chatflow.ts deleted file mode 100644 index 202c8d06..00000000 --- a/src/chatflow.ts +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env -S node --no-warnings --loader ts-node/esm -/* eslint-disable sort-keys */ -import 'dotenv/config.js' -import { WechatyPlugin, Wechaty, log } from 'wechaty' -import onScan from './handlers/on-scan.js' -import onError from './handlers/on-error.js' -import onRoomjoin from './handlers/on-roomjoin.js' -import onLogout from './handlers/on-logout.js' -import onLogin from './handlers/on-login.js' -import onReady from './handlers/on-ready.js' -import onMessage from './handlers/on-message.js' -import { getBotOps } from './services/configService.js' -import { logForm } from './utils/utils.js' -import { ChatFlowConfig, WechatyConfig } from './api/base-config.js' -import { MqttProxy, IClientOptions } from './proxy/mqtt-proxy.js' -import { VikaDB } from './db/vika-db.js' -import { LarkDB } from './db/lark-db.js' - -import { GroupMaster, GroupMasterConfig } from './plugins/mod.js' - -function ChatFlow (): WechatyPlugin { - logForm('ChatFlow插件开始启动...\n\n启动过程需要30秒到1分钟\n\n请等待系统初始化...') - - return function ChatFlowPlugin (bot: Wechaty): void { - - ChatFlowConfig.bot = bot - bot.on('scan', onScan) - bot.on('login', onLogin) - bot.on('ready', onReady) - bot.on('logout', onLogout) - bot.on('message', onMessage) - bot.on('room-join', onRoomjoin) - bot.on('error', onError) - - } - -} - -const init = async (options:{ - spaceName?: string - spaceId?: string - token: string -}, bot:Wechaty) => { - ChatFlowConfig.bot = bot - // 使用Vika - await VikaDB.init(options) - // 从配置文件中读取配置信息,包括wechaty配置、mqtt配置以及是否启用mqtt推送或控制 - const config: { - mqttConfig: IClientOptions, - wechatyConfig: WechatyConfig, - mqttIsOn: boolean, - } | undefined = await ChatFlowConfig.init() // 默认使用vika,使用lark时,需要传入'lark'参数await ChatFlowConfig.init('lark') - // log.info('config', JSON.stringify(config, undefined, 2)) - if (config) { - // 构建机器人 - // 如果MQTT推送或MQTT控制打开,则启动MQTT代理 - if (config.mqttIsOn) { - log.info('启动MQTT代理...', JSON.stringify(config.mqttConfig)) - try { - const mqttProxy = MqttProxy.getInstance(config.mqttConfig) - if (mqttProxy) { - mqttProxy.setWechaty(ChatFlowConfig.bot) - } - } catch (e) { - log.error('MQTT代理启动失败,检查mqtt配置信息是否正确...', e) - } - } - - } else { - logForm('维格表配置不全,缺少必要的配置信息') - } -} - -export { - init, - getBotOps, - ChatFlowConfig, - log, - logForm, - VikaDB, - LarkDB, - type IClientOptions, - MqttProxy, - ChatFlow, - GroupMaster, -} - -export type { - GroupMasterConfig, -} - -export type { WechatyConfig } diff --git a/src/db/lark-db.ts b/src/db/lark-db.ts index 7ce16cbb..811f8997 100644 --- a/src/db/lark-db.ts +++ b/src/db/lark-db.ts @@ -3,7 +3,8 @@ /* eslint-disable sort-keys */ import type { Sheets, Record } from './vikaModel/Model.js' import { sheets } from './vikaModel/index.js' -import { delay, logForm } from '../utils/utils.js' +import { delay } from '../utils/utils.js' +import CryptoJS from 'crypto-js' import * as lark from '@larksuiteoapi/node-sdk' @@ -16,39 +17,43 @@ import * as lark from '@larksuiteoapi/node-sdk' // console.log(JSON.stringify(res.data)) type Field = { - id?:string, - field_name: string, - type: number, - property?: any, + id?: string; + field_name: string; + type: number; + property?: any; description?: { - text?:string - }, - editable?: boolean, - isPrimary?: boolean, + text?: string; + }; + editable?: boolean; + isPrimary?: boolean; }; export type LarkConfig = { - appId:string, - appSecret:string, - appToken:string, - userMobile:string, -} + appId: string; + appSecret: string; + appToken: string; +}; + +export type BiTableConfig = { + spaceId: string; + token: string; +}; export interface DateBase { - messageSheet: string - keywordSheet: string - contactSheet: string - roomSheet: string - envSheet: string - whiteListSheet: string - noticeSheet: string - statisticSheet: string - orderSheet: string - stockSheet: string - groupNoticeSheet: string - qaSheet:string - chatBotSheet: string - chatBotUserSheet:string + messageSheet: string; + keywordSheet: string; + contactSheet: string; + roomSheet: string; + envSheet: string; + whiteListSheet: string; + noticeSheet: string; + statisticSheet: string; + orderSheet: string; + stockSheet: string; + groupNoticeSheet: string; + qaSheet: string; + chatBotSheet: string; + chatBotUserSheet: string; } export class KeyDisplaynameMap { @@ -57,7 +62,7 @@ export class KeyDisplaynameMap { private reverseMap: Map constructor (fields: any[]) { - const initPairs:[string, string][] = fields.map((fields:any) => { + const initPairs: [string, string][] = fields.map((fields: any) => { return this.transformKey(fields.name) }) this.reverseMap = new Map(initPairs) @@ -78,37 +83,67 @@ export class KeyDisplaynameMap { } -export class LarkDB { - - static spaceName: string - static token: string - static spaceId: string | undefined - static dataBaseIds: DateBase - static dataBaseNames: DateBase - static lark: lark.Client - static config: LarkConfig - static user_id:string | undefined +export class BiTable { + + spaceName!: string + token!: string + spaceId: string | undefined + userId!: string + username!: string + password!: string + isReady: boolean = true + dataBaseIds!: DateBase + dataBaseNames!: DateBase + lark!: lark.Client + larkConfig!: LarkConfig + user_id: string | undefined + hash!: string + static dataBaseIds: any + config!: { + [key: string]: any + accessKeyId?: string + secretAccessKey?: string + region?: string + endpoint?: string + bucketName?: string + CHATGPT_KEY?: string + CHATGPT_ENDPOINT?: string + CHATGPT_MODEL?: string + } - static async init (config:LarkConfig) { - logForm('初始化检查系统表...') + async init (config: BiTableConfig) { + console.debug('初始化检查系统表...') this.config = config + this.spaceId = config.spaceId + this.userId = this.spaceId + this.username = this.spaceId + + this.token = config.token + this.password = config.token + + this.larkConfig = { + appId: config.token.split('/')[0] as string, + appSecret: config.token.split('/')[1] as string, + appToken: config.spaceId, + } this.lark = new lark.Client({ - appId:config.appId, - appSecret:config.appSecret, + appId: this.larkConfig.appId, + appSecret: this.larkConfig.appSecret, }) - const user = await this.lark.contact.user.batchGetId({ - data:{ mobiles:[ config.userMobile ] }, - params:{ user_id_type:'user_id' }, - }) + // const user = await this.lark.contact.user.batchGetId({ + // data: { mobiles: [ config.userMobile ] }, + // params: { user_id_type: 'user_id' }, + // }) // console.log('\nuser:', JSON.stringify(user)) - if (user.data && user.data.user_list && user.data.user_list.length) { - const user_id = user.data.user_list[0]?.user_id - this.user_id = user_id - // console.log('\nuser_id:', user_id) - } + // if (user.data && user.data.user_list && user.data.user_list.length) { + // const user_id = user.data.user_list[0]?.user_id + // // this.user_id = user_id + // this.config.user_id = user_id + // console.log('\nuser_id:', user_id) + // } this.dataBaseIds = { messageSheet: '', @@ -122,26 +157,124 @@ export class LarkDB { orderSheet: '', stockSheet: '', groupNoticeSheet: '', - qaSheet:'', + qaSheet: '', chatBotSheet: '', - chatBotUserSheet:'', + chatBotUserSheet: '', } - this.dataBaseNames = { ...this.dataBaseIds } - if (config.appToken) { - + try { const tables = await this.getNodesList() - console.info('飞书多维表格文件列表:\n', JSON.stringify(tables, undefined, 2)) - + console.info( + '飞书多维表格文件列表:\n', + JSON.stringify(tables, undefined, 2), + ) await delay(200) + if (tables) { + const client = config.token + config.spaceId + console.debug(client) + this.hash = CryptoJS.SHA256(client).toString() + await delay(1000) + + for (const k in sheets) { + // console.info(this) + const sheet = sheets[k as keyof Sheets] + // console.info('数据模型:', k, sheet) + if (sheet && !tables[sheet.name]) { + console.info(`缺少数据表...\n${k}/${sheet.name}`) + this.isReady = false + return { success: false, code: 400, data: '' } + } else if (sheet) { + // console.debug('sheet...', sheet) + console.log(`表已存在:\n${k}/${sheet.name}/${tables[sheet.name]}`) + this.dataBaseIds[k as keyof DateBase] = tables[sheet.name] + this.dataBaseNames[k as keyof DateBase] = sheet.name + } + } + console.debug('初始化表完成...') + const data = JSON.parse(JSON.stringify(this)) + delete data.lark + return { success: true, code: 200, data: JSON.stringify(data) } + } else { + return { success: false, code: 400, data: '' } + } + } catch (error) { + console.error('初始化表失败:', error) + return { success: false, code: 400, data: '' } + } + } + + async createSheet (config: BiTableConfig) { + console.debug('初始化创建或检查系统表...') + this.config = config + this.spaceId = config.spaceId + this.userId = this.spaceId + this.username = this.spaceId + + this.token = config.token + this.password = config.token + + this.larkConfig = { + appId: config.token.split('/')[0] as string, + appSecret: config.token.split('/')[1] as string, + appToken: config.spaceId, + } + + this.lark = new lark.Client({ + appId: this.larkConfig.appId, + appSecret: this.larkConfig.appSecret, + }) + + // const user = await this.lark.contact.user.batchGetId({ + // data: { mobiles: [ config.userMobile ] }, + // params: { user_id_type: 'user_id' }, + // }) + // console.log('\nuser:', JSON.stringify(user)) + + // if (user.data && user.data.user_list && user.data.user_list.length) { + // const user_id = user.data.user_list[0]?.user_id + // this.user_id = user_id + // console.log('\nuser_id:', user_id) + // } + + this.dataBaseIds = { + messageSheet: '', + keywordSheet: '', + contactSheet: '', + roomSheet: '', + envSheet: '', + whiteListSheet: '', + noticeSheet: '', + statisticSheet: '', + orderSheet: '', + stockSheet: '', + groupNoticeSheet: '', + qaSheet: '', + chatBotSheet: '', + chatBotUserSheet: '', + } + this.dataBaseNames = { ...this.dataBaseIds } + + const tables = await this.getNodesList() + console.info( + '飞书多维表格文件列表:', + JSON.stringify(tables, undefined, 2), + ) + await delay(200) + + if (tables) { + const client = config.token + config.spaceId + console.debug('client字符串:', client) + this.hash = CryptoJS.SHA256(client).toString() + await delay(1000) + for (const k in sheets) { // console.info(this) const sheet = sheets[k as keyof Sheets] // console.info('数据模型:', k, sheet) if (sheet && !tables[sheet.name]) { - logForm(`表不存在,创建表并初始化数据...\n${k}/${sheet.name}`) + console.debug(`表不存在,创建表并初始化数据...\n${k}/${sheet.name}`) const fields = sheet.fields // console.info('fields:', JSON.stringify(fields)) const newFields: Field[] = [] @@ -151,7 +284,7 @@ export class LarkDB { type: 1, field_name: field?.name || '', description: { - text:field?.desc || '', + text: field?.desc || '', }, } // console.info('字段定义:', JSON.stringify(field)) @@ -211,13 +344,13 @@ export class LarkDB { newField.type = 7 newFields.push(newField) break - // case 'MagicLink': - // newField.property = {} - // newField.property.foreignDatasheetId = this[field.desc as keyof LarkDB] - // if (field.desc) { - // newFields.push(newField) - // } - // break + // case 'MagicLink': + // newField.property = {} + // newField.property.foreignDatasheetId = this[field.desc as keyof LarkDB] + // if (field.desc) { + // newFields.push(newField) + // } + // break case 'Attachment': newField.type = 17 newFields.push(newField) @@ -228,7 +361,7 @@ export class LarkDB { } } - // console.info('创建表,表信息:', JSON.stringify(newFields)) + console.info('创建表,表信息:', JSON.stringify(newFields)) await this.createDataSheet(k, sheet.name, newFields) console.info('当前表ID:', this.dataBaseIds[k as keyof DateBase]) @@ -237,35 +370,43 @@ export class LarkDB { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (defaultRecords) { // console.info(defaultRecords.length) - const count = Math.ceil(defaultRecords.length / 10) + const count = Math.ceil(defaultRecords.length / 50) for (let i = 0; i < count; i++) { - const records = defaultRecords.splice(0, 10) + const records = defaultRecords.splice(0, 50) console.info('写入:', records.length) console.info('写入表ID:', this.dataBaseIds[k as keyof DateBase]) - await this.createRecord(this.dataBaseIds[k as keyof DateBase], records) + await this.createRecord( + this.dataBaseIds[k as keyof DateBase], + records, + ) await delay(200) } - logForm(sheet.name + '初始化数据写入完成...') + console.debug(sheet.name + '初始化数据写入完成...') } } else if (sheet) { - // logForm(`表已存在:\n${k}/${sheet.name}/${tables[sheet.name]}`) + // console.debug('sheet...', sheet) + console.log(`表已存在:\n${k}/${sheet.name}/${tables[sheet.name]}`) this.dataBaseIds[k as keyof DateBase] = tables[sheet.name] - this.dataBaseNames[sheet.name as keyof DateBase] = tables[sheet.name] - } else { /* empty */ } + this.dataBaseNames[k as keyof DateBase] = sheet.name + } else { + /* empty */ + } } - logForm('初始化表完成...') - - return true + console.debug('初始化表完成...') + const data = JSON.parse(JSON.stringify(this)) + delete data.lark + return { success: true, code: 200, data: JSON.stringify(data) } } else { - logForm('\n\n指定空间不存在,请先创建空间,并在.env文件或环境变量中配置vika信息\n\n') - return false + return { success: false, code: 400, data: '' } } } - protected static async getNodesList () { - if (this.config.appToken) { + protected async getNodesList () { + if (this.larkConfig.appToken) { // 获取指定空间站的一级文件目录 - const nodeListResp = await this.lark.bitable.appTable.list({ path:{ app_token:this.config.appToken } }) + const nodeListResp = await this.lark.bitable.appTable.list({ + path: { app_token: this.larkConfig.appToken }, + }) const tables: any = {} if (nodeListResp.data && nodeListResp.data.items) { // console.info(nodeListResp.data.nodes); @@ -283,15 +424,20 @@ export class LarkDB { } } - protected static async getDataBases () { + protected async getDataBases () { return this.dataBaseIds } - protected static async getSheetFields (datasheetId: string) { - const fieldsResp = await this.lark.bitable.appTableField.list({ path:{ app_token:this.config.appToken, table_id:datasheetId } }) + protected async getSheetFields (datasheetId: string) { + const fieldsResp = await this.lark.bitable.appTableField.list({ + path: { app_token: this.larkConfig.appToken, table_id: datasheetId }, + }) let fields: any[] = [] if (fieldsResp.data && fieldsResp.data.items) { - console.info('getSheetFields获取字段:', JSON.stringify(fieldsResp.data.items)) + console.info( + 'getSheetFields获取字段:', + JSON.stringify(fieldsResp.data.items), + ) fields = fieldsResp.data.items } else { console.error('获取字段失败:', fieldsResp) @@ -299,7 +445,11 @@ export class LarkDB { return fields } - protected static async createDataSheet (key: string, name: string, fields: { field_name: string; type: number }[]) { + protected async createDataSheet ( + key: string, + name: string, + fields: { field_name: string; type: number }[], + ) { console.info('开始创建表...') const datasheetRo = { fields, @@ -308,19 +458,21 @@ export class LarkDB { // console.info('创建表,表信息:', JSON.stringify(datasheetRo)) - if (this.config.appToken) { + if (this.larkConfig.appToken) { try { const res = await this.lark.bitable.appTable.create({ - path:{ - app_token:this.config.appToken, + path: { + app_token: this.larkConfig.appToken, }, - data:{ - table:datasheetRo, + data: { + table: datasheetRo, }, }) console.info('创建表返回:', JSON.stringify(res)) - console.info(`系统表【${name}】创建成功,表ID【${res.data?.table_id}】`) + console.info( + `系统表【${name}】创建成功,表ID【${res.data?.table_id}】`, + ) this.dataBaseIds[key as keyof DateBase] = res.data?.table_id || '' this.dataBaseNames[name as keyof DateBase] = res.data?.table_id || '' @@ -338,17 +490,17 @@ export class LarkDB { } } - protected static async createRecord (datasheetId: string, records: Record[]) { + protected async createRecord (datasheetId: string, records: Record[]) { console.info('写入飞书多维表格ID:', datasheetId) console.info('写入飞书多维表格记录:', records.length) try { const res = await this.lark.bitable.appTableRecord.batchCreate({ - data:{ + data: { records, }, - path:{ - app_token:this.config.appToken, - table_id:datasheetId, + path: { + app_token: this.larkConfig.appToken, + table_id: datasheetId, }, }) @@ -357,23 +509,25 @@ export class LarkDB { } else { console.error('记录写入飞书多维表格失败:', res) } - } catch (err:any) { + } catch (err: any) { console.error('请求飞书多维表格写入失败:', err.code) } - } - protected static async updateRecord (datasheetId: string, records: { - recordId: string - fields: {[key:string]:any} - }[]) { + protected async updateRecord ( + datasheetId: string, + records: { + recordId: string; + fields: { [key: string]: any }; + }[], + ) { console.info('更新飞书多维表格记录:', records.length) try { const res = await this.lark.bitable.appTableRecord.batchUpdate({ - data:{ records }, - path:{ - app_token: this.config.appToken, + data: { records }, + path: { + app_token: this.larkConfig.appToken, table_id: datasheetId, }, }) @@ -383,17 +537,19 @@ export class LarkDB { } catch (err) { console.error('请求飞书多维表格更新失败:', err) } - } - protected static async deleteRecords (datasheetId: string, recordsIds: string[]) { + protected async deleteRecords ( + datasheetId: string, + recordsIds: string[], + ) { // console.info('操作数据表ID:', datasheetId) // console.info('待删除记录IDs:', recordsIds) const response = await this.lark.bitable.appTableRecord.batchDelete({ - data:{ records: recordsIds }, - path:{ - app_token: this.config.appToken, + data: { records: recordsIds }, + path: { + app_token: this.larkConfig.appToken, table_id: datasheetId, }, }) @@ -405,16 +561,16 @@ export class LarkDB { } } - protected static async getRecords (datasheetId: string, query:any = {}) { + protected async getRecords (datasheetId: string, query: any = {}) { let records: any = [] query['pageSize'] = 1000 // 分页获取记录,默认返回第一页 const response = await this.lark.bitable.appTableRecord.list({ - params:{ - filter:query, + params: { + filter: query, }, - path:{ - app_token: this.config.appToken, + path: { + app_token: this.larkConfig.appToken, table_id: datasheetId, }, }) @@ -428,11 +584,11 @@ export class LarkDB { return records } - static async getAllRecords (datasheetId: string) { + async getAllRecords (datasheetId: string) { let records: any = [] const response = await this.lark.bitable.appTableRecord.list({ - path:{ - app_token: this.config.appToken, + path: { + app_token: this.larkConfig.appToken, table_id: datasheetId, }, }) @@ -447,7 +603,7 @@ export class LarkDB { return records } - protected static async clearBlankLines (datasheetId: any) { + protected async clearBlankLines (datasheetId: any) { const records = await this.getRecords(datasheetId, {}) // console.info(records) const recordsIds: any = [] diff --git a/src/db/lark-orm.ts b/src/db/lark-orm.ts new file mode 100644 index 00000000..fec2643f --- /dev/null +++ b/src/db/lark-orm.ts @@ -0,0 +1,565 @@ +/* eslint-disable no-console */ +/* eslint-disable guard-for-in */ +import * as lark from '@larksuiteoapi/node-sdk' +import * as fs from 'node:fs' + +type RecordType = { + fields: { + [key: string]: string; + }; +}; + +interface OrmResponse { + message: 'success' | 'fail'; + data: any; +} + +interface IField { + [key: string]: any; +} + +export interface IRecord { + record_id?: string; + recordId?: string; + fields: IField; +} + +export type LarkConfig = { + appId: string; + appSecret: string; + appToken: string; + datasheetId: string; +}; + +/** + * Vika API 接口选项 + */ +export interface VikaOptions { + apiKey: string; + baseId: string; +} + +/** + * 实体类映射选项 + */ +export interface MappingOptions { + // 表名 + tableName: string; + + // 字段映射 + fieldMapping: Record; +} + +/** + * 基类实体 + */ +export abstract class BaseEntity { + + /** + * Vika API 选项 + */ + protected vikaOptions: VikaOptions | undefined + + /** + * 映射选项 + */ + protected mappingOptions!: MappingOptions + + recordId: string = '' + + datasheet!: lark.Client + + config!: LarkConfig + + /** + * 设置 Vika API 选项 + */ + + // 一个方法,用于访问实例属性 + getId () { + return this.recordId + } + + protected getRecordId (): string { + throw new Error('Must be implemented by subclass') + } + + /** + * 设置 Vika API 选项 + */ + + setVikaOptions (options: VikaOptions) { + // console.info('setVikaOptions:', options) + this.vikaOptions = options + this.config = { + appId: options.apiKey.split('/')[0] as string, + appSecret: options.apiKey.split('/')[1] as string, + appToken: options.apiKey.split('/')[2] as string, + datasheetId: options.baseId, + } + this.datasheet = new lark.Client({ + appId: this.config.appId, + appSecret: this.config.appSecret, + }) + } + + /** + * 设置映射选项 + */ + setMappingOptions (options: MappingOptions) { + this.mappingOptions = options + } + + protected getMappingOptions (): MappingOptions { + throw new Error('Must be implemented by subclass') + } + + /** + * 格式化数据 + */ + formatData (data: Record) { + // console.info('formatData:', data) + const formatted: Record = {} + const mappingOptions = this.getMappingOptions() + // console.info('this.mappingOptions', mappingOptions) + for (const key in data) { + const mappedKey = mappingOptions.fieldMapping[key] + if (mappedKey) { + formatted[mappedKey] = data[key] + } + } + // console.info('formatted:', JSON.stringify(formatted)) + return formatted + } + + /** + * 从记录创建实体 + */ + protected createFromRecord (record: { + fields: { [key: string]: any }; + record_id?: string; + recordId?: string; + }) { + const data: any = record.fields + const entity: any = {} + const mappingOptions = this.getMappingOptions() + + for (const key in mappingOptions.fieldMapping) { + const field = mappingOptions.fieldMapping[key] + entity[key] = data[field as string] + } + record.fields = entity + if (record.record_id) record.recordId = record.record_id + return record + } + + /** + * 创建记录 + */ + async create (entity: { [key: string]: any }): Promise { + const recordFormat = this.formatData(entity) + // console.info('recordFormat', JSON.stringify(recordFormat)) + const records = [ { fields: recordFormat } ] + try { + const res = await this.datasheet.bitable.appTableRecord.batchCreate({ + data: { + records, + }, + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + + if (res.data?.records) { + // console.info(res.data.records) + const record = res.data.records[0] as IRecord + return { + message: 'success', + data: this.createFromRecord(record), + } + } else { + console.error('记录写入飞书多维表格失败:', res) + return { + message: 'fail', + data: res, + } + } + } catch (err: any) { + console.error('请求飞书多维表格写入失败:', err) + return { + message: 'fail', + data: err, + } + } + } + + /** + * 批量创建记录 + */ + async createBatch (entity: IRecord[]): Promise { + // console.info('写入飞书多维表:', records.length) + const records: RecordType[] = entity.map((r: any) => ({ + fields: this.formatData(r), + })) + // console.debug('createBatch:', records) + try { + const res = await this.datasheet.bitable.appTableRecord.batchCreate({ + data: { + records, + }, + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + + if (res.data?.records) { + // console.info('res.data.records:', res.data.records) + const records = res.data.records.map((r: any) => + this.createFromRecord(r), + ) + return { + message: 'success', + data: records, + } + } else { + console.error('记录写入飞书多维表格失败:', res) + return { + message: 'fail', + data: res, + } + } + } catch (err) { + console.error('请求飞书多维表格写入失败:', err) + return { + message: 'fail', + data: err, + } + } + } + + /** + * 更新记录 + */ + async update ( + id: string, + entity: Partial, + ): Promise { + const data = this.formatData(entity) + + try { + const res = await this.datasheet.bitable.appTableRecord.batchUpdate({ + data: { + records: [ + { + fields: data, + record_id: id, + }, + ], + }, + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + // console.info('update res:', JSON.stringify(res)) + if (!res.data || !res.data.records || !res.data.records.length) { + console.error('记录更新飞书多维表失败:', res) + return { + message: 'fail', + data: res, + } + } else { + const records: IRecord[] = res.data.records + const record: IRecord = records[0] as IRecord + return { + message: 'success', + data: this.createFromRecord(record) as IRecord, + } + } + } catch (err) { + console.error('请求飞书多维表更新失败:', err) + return { + message: 'fail', + data: err, + } + } + } + + /** + * 批量更新记录 + */ + async updatEmultiple (records: IRecord[]): Promise { + const datas = records.map((item) => { + return { + fields: this.formatData(item.fields), + record_id: item.record_id, + } + }) + // console.debug('updatEmultiple:', datas) + try { + const res = await this.datasheet.bitable.appTableRecord.batchUpdate({ + data: { + records: datas, + }, + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + // console.info('updatEmultiple res:', res) + if (!res.data || !res.data.records || !res.data.records.length) { + console.error('记录更新飞书多维表失败:', res) + return { + message: 'fail', + data: res, + } + } else { + const records: IRecord[] = res.data.records + records.map((r: any) => this.createFromRecord(r)) as IRecord[] + return { + message: 'success', + data: records.map((r: any) => this.createFromRecord(r)) as IRecord[], + } + } + // const record: IRecord = res.data.records[0]; + // return this.createFromRecord(record) as IRecord; + } catch (err) { + console.error('请求飞书多维表更新失败:', err) + return { + message: 'fail', + data: err, + } + } + } + + /** + * 删除记录 + */ + async delete (id: string): Promise { + const response = await this.datasheet.bitable.appTableRecord.batchDelete({ + data: { records: [ id ] }, + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + if (response.data && response.data.records) { + console.info('删除记录成功:', JSON.stringify(response.data)) + return { message: 'success', data: response.data } + } else { + console.error('删除记录失败:', response) + return { message: 'fail', data: response } + } + } + + /** + * 批量删除记录 + */ + async deleteBatch (ids: string[]): Promise { + const response = await this.datasheet.bitable.appTableRecord.batchDelete({ + data: { + records: ids, + }, + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + if (response.data && response.data.records) { + console.info('删除记录成功:', JSON.stringify(response.data)) + return { message: 'success', data: response.data } + } else { + console.error('删除记录失败:', response) + return { message: 'fail', data: response } + } + } + + /** + * 根据 ID 查找单个记录 + */ + async findById (id: string): Promise { + // 分页获取记录,默认返回第一页 + try { + const response = await this.datasheet.bitable.appTableRecord.get({ + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + record_id: id, + }, + }) + // const response = await this.datasheet.records.query(query) + if (response.data && response.data.record) { + const record = response.data.record + return { + message: 'success', + data: this.createFromRecord(record), + } + } else { + // console.info(records) + return { + message: 'fail', + data: response, + } + } + } catch (err) { + console.error('findById获取数据记录失败:', JSON.stringify(err)) + return { + message: 'fail', + data: err, + } + } + } + + /** + * 根据字段查询多条记录 + */ + async findByField ( + fieldName: string, + value: any, + pageSize: number | undefined = 100, + ): Promise { + const field = this.mappingOptions.fieldMapping[fieldName] + let records: IRecord[] = [] + if (!field) { + throw new Error('Invalid field name') + } + + const filterByFormula = `CurrentValue.[${field}]="${value}"` + console.info('filterByFormula:', filterByFormula) + // 分页获取记录,默认返回第一页 + // const response = await this.datasheet.records.query(query) + const response = await this.datasheet.bitable.appTableRecord.list({ + params: { + filter: filterByFormula, + page_size: pageSize, + }, + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + if (response.data && response.data.items) { + records = response.data.items + // console.info(records) + return { + message: 'success', + data: records.map((r: any) => this.createFromRecord(r)) as IRecord[], + } + } else { + console.error('findByField获取数据记录失败:', JSON.stringify(response)) + return { + message: 'fail', + data: response, + } + } + } + + /** + * 根据字段查询多条记录 + * https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table-record/filter + */ + async findByQuery ( + filterByFormula: string, + pageSize: number | undefined = 100, + ): Promise { + let records: IRecord[] = [] + const response = await this.datasheet.bitable.appTableRecord.list({ + params: { + filter: filterByFormula, + page_size: pageSize, + }, + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + if (response.data && response.data.items) { + records = response.data.items + // console.info(records) + return { + message: 'success', + data: records.map((r: any) => this.createFromRecord(r)) as IRecord[], + } + } else { + console.error('findByField获取数据记录失败:', JSON.stringify(response)) + return { + message: 'fail', + data: response, + } + } + } + + /** + * 查询所有记录 + * https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table-record/filter + * AND(CurrentValue.[订单号].contains("004"),CurrentValue.[订单日期]= TODAY()) + * OR(CurrentValue.[订单号].contains("004"),CurrentValue.[订单号].contains("009")) + * CurrentValue.[订单日期] = TODAY()-1 + */ + async findAll () { + let records: any = [] + const response = await this.datasheet.bitable.appTableRecord.list({ + path: { + app_token: this.config.appToken, + table_id: this.config.datasheetId, + }, + }) + // console.info('原始返回:',response) + if (response.data) { + records = response.data.items + // console.info(records) + return { + message: 'success', + data: records.map((r: any) => this.createFromRecord(r)) as IRecord[], + } + } else { + console.error('获取数据记录失败:', JSON.stringify(response)) + return { + message: 'fail', + data: response, + } + } + } + + async upload (path: string): Promise<{ data: any; message?: any }> { + console.info('文件本地路径:', path) + try { + if (fs.existsSync(path)) { + // 文件存在,可以继续你的操作 + const file = fs.createReadStream(path) + const size = fs.statSync(path).size + const name = path.split('/').pop() as string + const payload: any = { + data: { + file, + file_name: name, + parent_type: 'bitable_file', + parent_node: this.config.appToken, + size, + }, + } + console.info('payload', JSON.stringify(payload)) + + const resp = await this.datasheet.drive.media.uploadAll(payload) + console.info('上传文件成功:', JSON.stringify(resp)) + return { + message: 'success', + data: resp, + } + } else { + console.error('文件不存在') + // 文件不存在,你需要处理这个问题 + return { + message: 'fail', + data: undefined, + } + } + } catch (error) { + console.error('上传文件失败:', error) + return { message: error, data: undefined } + } + } + +} diff --git a/src/db/lark.ts b/src/db/lark.ts deleted file mode 100644 index 595197c0..00000000 --- a/src/db/lark.ts +++ /dev/null @@ -1,322 +0,0 @@ -/* eslint-disable sort-keys */ -import 'dotenv/config.js' -import type * as lark from '@larksuiteoapi/node-sdk' -import { log } from 'wechaty' -import fs from 'fs' -import { FileBox } from 'file-box' -import { LarkDB } from './lark-db.js' - -interface IField { - [key:string]: string | ''; -} - -export interface IRecord { - recordId: string; - fields: IField; -} - -interface IFieldMapping { - id: string; - name: string; - type: string; - property: any; - editable: boolean; - isPrimary?: boolean; -} - -export interface IFieldMappingResponse { - code: number; - success: boolean; - data: { - fields: IFieldMapping[]; - }; - message: string; -} - -type Record = { - fields: { - [key: string]: string - } -} - -export class LarkSheet { - - static client: lark.Client - static datasheetId: string - static offsetValue: number - static limitValue: number - static orderby: any - static fields: any[] = [] - static records: any - - static init (client:lark.Client, datasheetId: string) { - LarkSheet.client = client - LarkSheet.datasheetId = datasheetId - LarkSheet.offsetValue = 0 - LarkSheet.limitValue = 15 - } - - static limit (offset: number, limit: number) { - this.offsetValue = offset || 0 - this.limitValue = limit || 15 - return this - } - - static sort (orderby: any) { - this.orderby = orderby - return this - } - - static async insert (records:Record[]) { - // log.info('写入维格表:', records.length) - - try { - const res = await this.client.bitable.appTableRecord.batchCreate({ - data:{ - records, - }, - path:{ - app_token:LarkDB.config.appToken, - table_id:this.datasheetId, - }, - }) - if (res.data && res.data.records) { - // log.info(res.data.records) - } else { - log.error('记录写入维格表失败:', res) - } - return res - } catch (err) { - log.error('请求维格表写入失败:', err) - return err - } - - } - - static async upload (path:string) { - log.info('文件本地路径:', path) - const file = fs.createReadStream(path) - const fileBox = FileBox.fromFile(path) - // console.info(fileBox) - try { - const resp = await this.client.drive.media.uploadAll({ - data:{ - file_name:fileBox.name, - parent_type: fileBox.mediaType !== 'image/jpeg' ? 'bitable_file' : 'bitable_image', - parent_node: LarkDB.config.appToken, - size: fileBox.size, - file, - }, - }) - if (!resp || !resp.file_token) { - log.info('文件上传请求成功,上传失败', JSON.stringify(resp)) - } - return resp - } catch (error) { - log.error('文件上传请求失败:', error) - return error - } - - } - - static async update (records: { - recordId: string - fields: {[key:string]:any} - }[]) { - log.info('更新维格表记录:', records.length) - - try { - const res = await this.client.bitable.appTableRecord.batchUpdate({ - data:{ - records, - }, - path:{ - app_token:LarkDB.config.appToken, - table_id:this.datasheetId, - }, - }) - if (!res.data || !res.data.records) { - log.error('记录更新维格表失败:', res) - } - return res - } catch (err) { - log.error('请求维格表更新失败:', err) - return err - } - - } - - static async updateOne (recordId: string, fields: {[key:string]:any}) { - - try { - const res = await this.client.bitable.appTableRecord.update({ - data:{ - fields, - }, - path:{ - app_token:LarkDB.config.appToken, - table_id:this.datasheetId, - record_id:recordId, - }, - }) - if (!res.data || !res.data.record) { - log.error('记录更新维格表失败:', res) - } - return res - } catch (err) { - log.error('请求维格表更新失败:', err) - return err - } - - } - - static async remove (recordsIds: string[]) { - // log.info('操作数据表ID:', datasheetId) - // log.info('待删除记录IDs:', recordsIds) - const response = await this.client.bitable.appTableRecord.batchDelete({ - data:{ - records:recordsIds, - }, - path:{ - app_token:LarkDB.config.appToken, - table_id:this.datasheetId, - }, - }) - if (!response.data || !response.data.records) { - log.error('删除记录失败:', response) - } - return response - } - - static async removeOne (recordsId: string) { - // log.info('操作数据表ID:', datasheetId) - // log.info('待删除记录IDs:', recordsIds) - const response = await this.client.bitable.appTableRecord.delete({ - path:{ - app_token:LarkDB.config.appToken, - table_id:this.datasheetId, - record_id:recordsId, - }, - }) - if (!response.data || !response.data.record_id) { - log.error('删除记录失败:', response) - } - return response - } - - static async find (query:any = {}) { - let records: any[] = [] - query['pageSize'] = 1000 - // 分页获取记录,默认返回第一页 - const response = await this.client.bitable.appTableRecord.list({ - params:{ - filter:query, - }, - path:{ - app_token:LarkDB.config.appToken, - table_id:this.datasheetId, - }, - }) - if (response.data && response.data.items) { - records = response.data.items - // log.info(records) - return records - } else { - log.error('获取数据记录失败:', JSON.stringify(response)) - return response - } - } - - static async findOne (query:any = {}) { - let records: any[] = [] - query['pageSize'] = 1 - // 分页获取记录,默认返回第一页 - const response = await this.client.bitable.appTableRecord.list({ - params:{ - filter:query, - }, - path:{ - app_token:LarkDB.config.appToken, - table_id:this.datasheetId, - }, - }) - if (response.data && response.data.items) { - records = response.data.items - if (records.length) return records[0] - // log.info(records) - return {} - } else { - log.error('获取数据记录失败:', JSON.stringify(response)) - return response - } - } - - static async getFields () { - if (this.fields.length) return this.fields - const fieldsResp = await this.client.bitable.appTableField.list() - - if (fieldsResp.data && fieldsResp.data.items) { - this.fields = fieldsResp.data.items - // log.info('fieldsResp:', this.fields) - } else { - console.error(fieldsResp) - } - return this.fields - } - - static async findAll (): Promise { - try { - // Automatically handle pagination and iterate through all records. - const res = await this.client.bitable.appTableRecord.list() - if (res.data && res.data.items) { - // log.info('findAll() records:', records.length) - return res.data.items - } - // log.info('findAll() records:', records.length) - return [] - } catch (error) { - log.error('Error in findAll():', error) - throw error - } - } - - keyConversion (records:any[]) { - return records.map(record => { - const newFields: { [key: string]: any } = {} - - for (const [ oldKey, value ] of Object.entries(record.fields)) { - const newKey = oldKey.split('|')[1] || oldKey // 获取 "|" 后面的部分作为新键 - newFields[newKey] = value - } - record.fields = newFields - return record - }) - } - - static async nameConversion (records: any[]) { - const fields = await this.getFields() - // 创建一个映射表 - const fieldMap: { [key: string]: string } = {} - for (const field of fields) { - const parts = field.name.split('|') - if (parts.length === 2) { - fieldMap[parts[1]] = field.name - } - } - - // 使用映射表转换记录 - const convertedRecords: any[] = records.map((record) => { - const newFields: { [key: string]: string } = {} - for (const [ key, value ] of Object.entries(record.fields)) { - newFields[fieldMap[key] || key] = value as string - } - record.fields = newFields - return record - }) - - return convertedRecords - } - -} - -export default LarkSheet diff --git a/src/db/vika-db.ts b/src/db/vika-db.ts index b6a3e893..03b73d6e 100644 --- a/src/db/vika-db.ts +++ b/src/db/vika-db.ts @@ -1,30 +1,56 @@ +/* eslint-disable no-console */ /* eslint-disable sort-keys */ import { ICreateRecordsReqParams, Vika } from '@vikadata/vika' -import type { Sheets, Field } from './vikaModel/Model.js' +import type { Sheets } from './vikaModel/Model.js' import { sheets } from './vikaModel/index.js' -import { delay, logForm } from '../utils/utils.js' - -export type VikaConfig = { - spaceId?: string, - spaceName?: string, - token: string, -} +import { delay } from '../utils/utils.js' +import CryptoJS from 'crypto-js' +// import { Messages } from './vikaModel/Message/db.js'; +// import { Env } from './vikaModel/Env/db.js'; +// import { Chatbots } from './vikaModel/ChatBot/db.js'; +// import { ChatbotUsers } from './vikaModel/ChatBotUser/db.js'; +// import { Contacts } from './vikaModel/Contact/db.js'; +// import { Groupnotices } from './vikaModel/GroupNotice/db.js'; +// import { Groups } from './vikaModel/Group/db.js'; +// import { Keywords } from './vikaModel/Keyword/db.js'; +// import { Notices } from './vikaModel/Notice/db.js'; +// import { Orders } from './vikaModel/Order/db.js'; +// import { Qas } from './vikaModel/Qa/db.js'; +// import { Rooms } from './vikaModel/Room/db.js'; +// import { Statistics } from './vikaModel/Statistic/db.js'; +// import { Whitelists } from './vikaModel/WhiteList/db.js'; + +type Field = { + id?: string; + name: string; + type: string; + property?: any; + desc?: string; + editable?: boolean; + isPrimary?: boolean; +}; + +export type BiTableConfig = { + spaceId: string; + token: string; +}; export interface DateBase { - messageSheet: string - keywordSheet: string - contactSheet: string - roomSheet: string - envSheet: string - whiteListSheet: string - noticeSheet: string - statisticSheet: string - orderSheet: string - stockSheet: string - groupNoticeSheet: string - qaSheet:string - chatBotSheet: string - chatBotUserSheet:string + messageSheet: string; + keywordSheet: string; + contactSheet: string; + roomSheet: string; + envSheet: string; + whiteListSheet: string; + noticeSheet: string; + statisticSheet: string; + orderSheet: string; + stockSheet: string; + groupNoticeSheet: string; + qaSheet: string; + chatBotSheet: string; + chatBotUserSheet: string; + groupSheet: string; } export class KeyDisplaynameMap { @@ -33,7 +59,7 @@ export class KeyDisplaynameMap { private reverseMap: Map constructor (fields: any[]) { - const initPairs:[string, string][] = fields.map((fields:any) => { + const initPairs: [string, string][] = fields.map((fields: any) => { return this.transformKey(fields.name) }) this.reverseMap = new Map(initPairs) @@ -54,22 +80,62 @@ export class KeyDisplaynameMap { } -export class VikaDB { +export class BiTable { + + spaceName!: string + username!: string + nickname!: string + id!: string + token!: string + password!: string + vika!: Vika + spaceId: string | undefined + userId: string | undefined + dataBaseIds!: DateBase + dataBaseNames!: DateBase + isReady: boolean = true + hash!: string + config!: { + [key: string]: any + accessKeyId?: string + secretAccessKey?: string + region?: string + endpoint?: string + bucketName?: string + CHATGPT_KEY?: string + CHATGPT_ENDPOINT?: string + CHATGPT_MODEL?: string + } - static spaceName: string - static token: string - static vika: Vika - static spaceId: string | undefined - static dataBaseIds: DateBase - static dataBaseNames: DateBase + // db: { + // env: Env; + // message: Messages; + // chatBot: Chatbots; + // chatBotUser: ChatbotUsers; + // contact: Contacts; + // groupNotice: Groupnotices; + // group: Groups; + // keyword: Keywords; + // notice: Notices; + // order: Orders; + // qa: Qas; + // room: Rooms; + // statistic: Statistics; + // whiteList: Whitelists; + // } + + constructor () { + } - static async init (config:VikaConfig) { - logForm('初始化检查系统表...') - this.spaceId = '' - if (config.spaceName) this.spaceName = config.spaceName - if (config.spaceId) this.spaceId = config.spaceId + async init (config: BiTableConfig) { + console.info('初始化检查系统表...') + this.config = config + this.spaceId = config.spaceId + this.userId = this.spaceId + this.username = this.spaceId this.vika = new Vika({ token: config.token }) this.token = config.token + this.password = this.token this.dataBaseIds = { messageSheet: '', keywordSheet: '', @@ -82,152 +148,226 @@ export class VikaDB { orderSheet: '', stockSheet: '', groupNoticeSheet: '', - qaSheet:'', + qaSheet: '', chatBotSheet: '', - chatBotUserSheet:'', + chatBotUserSheet: '', + groupSheet: '', } this.dataBaseNames = { ...this.dataBaseIds } - if (!this.spaceId && this.spaceName) this.spaceId = await this.getSpaceId() + try { + const tables = await this.getNodesList() + // console.info( + // '维格表文件列表:\n', + // JSON.stringify(tables, undefined, 2), + // ); + + if (tables) { + const client = config.token + config.spaceId + console.debug(client) + this.hash = CryptoJS.SHA256(client).toString() + await delay(1000) + + for (const k in sheets) { + // console.info(this) + const sheet = sheets[k as keyof Sheets] + // console.info('数据模型:', k, sheet) + if (sheet && !tables[sheet.name]) { + console.info(`缺少数据表...\n${k}/${sheet.name}`) + this.isReady = false + return { success: false, code: 400, data: '' } + } else if (sheet) { + // console.info(`表已存在:\n${k}/${sheet.name}/${tables[sheet.name]}`) + this.dataBaseIds[k as keyof DateBase] = tables[sheet.name] + this.dataBaseNames[k as keyof DateBase] = sheet.name + } + } + console.info('初始化表完成...') + const data = JSON.parse(JSON.stringify(this)) + delete data.vika + return { success: true, code: 200, data: JSON.stringify(data) } + } else { + return { success: false, code: 400, data: '' } + } + + } catch (error) { + console.error('初始化表失败:', error) + return { success: false, code: 400, data: '' } + } // console.info('空间ID:', this.spaceId) + } - if (this.spaceId) { + async createSheet (config: BiTableConfig) { + this.spaceId = config.spaceId + this.vika = new Vika({ token: config.token }) + this.token = config.token + this.dataBaseIds = { + messageSheet: '', + keywordSheet: '', + contactSheet: '', + roomSheet: '', + envSheet: '', + whiteListSheet: '', + noticeSheet: '', + statisticSheet: '', + orderSheet: '', + stockSheet: '', + groupNoticeSheet: '', + qaSheet: '', + chatBotSheet: '', + chatBotUserSheet: '', + groupSheet: '', + } + this.dataBaseNames = { ...this.dataBaseIds } + try { const tables = await this.getNodesList() console.info('维格表文件列表:\n', JSON.stringify(tables, undefined, 2)) - - await delay(1000) - - for (const k in sheets) { - // console.info(this) - const sheet = sheets[k as keyof Sheets] - // console.info('数据模型:', k, sheet) - if (sheet && !tables[sheet.name]) { - logForm(`表不存在,创建表并初始化数据...\n${k}/${sheet.name}`) - const fields = sheet.fields - // console.info('fields:', JSON.stringify(fields)) - const newFields: Field[] = [] - for (let j = 0; j < fields.length; j++) { - const field = fields[j] - const newField: Field = { - type: field?.type || '', - name: field?.name || '', - desc: field?.desc || '', - // property:{}, - } - // console.info('字段定义:', JSON.stringify(field)) - let options - switch (field?.type) { - case 'SingleText': - newField.property = field.property || {} - newFields.push(newField) - break - case 'SingleSelect': - options = field.property.options - newField.property = {} - newField.property.defaultValue = field.property.defaultValue || options[0].name - newField.property.options = [] - for (let z = 0; z < options.length; z++) { - const option = { - name: options[z].name, - // color: options[z].color.name, + if (tables) { + await delay(500) + + for (const k in sheets) { + // console.info(this) + const sheet = sheets[k as keyof Sheets] + // console.info('数据模型:', k, sheet) + if (sheet && !tables[sheet.name]) { + console.debug(`表不存在,创建表并初始化数据...${k}/${sheet.name}`) + const fields = sheet.fields + // console.info('fields:', JSON.stringify(fields)) + const newFields: Field[] = [] + for (let j = 0; j < fields.length; j++) { + const field = fields[j] + const newField: Field = { + type: field?.type || '', + name: field?.name || '', + desc: field?.desc || '', + // property:{}, + } + // console.info('字段定义:', JSON.stringify(field)) + let options + switch (field?.type) { + case 'SingleText': + newField.property = field.property || {} + newFields.push(newField) + break + case 'SingleSelect': + options = field.property.options + newField.property = {} + newField.property.defaultValue + = field.property.defaultValue || options[0].name + newField.property.options = [] + for (let z = 0; z < options.length; z++) { + const option = { + name: options[z].name, + // color: options[z].color.name, + } + newField.property.options.push(option) } - newField.property.options.push(option) - } - newFields.push(newField) - break - case 'MultiSelect': - options = field.property.options - newField.property = {} - newField.property.defaultValue = field.property.defaultValue || options[0].name - newField.property.options = [] - for (let z = 0; z < options.length; z++) { - const option = { - name: options[z].name, - color: options[z].color.name, + newFields.push(newField) + break + case 'MultiSelect': + options = field.property.options + newField.property = {} + newField.property.defaultValue + = field.property.defaultValue || options[0].name + newField.property.options = [] + for (let z = 0; z < options.length; z++) { + const option = { + name: options[z].name, + color: options[z].color.name, + } + newField.property.options.push(option) } - newField.property.options.push(option) - } - newFields.push(newField) - break - case 'Text': - newFields.push(newField) - break - case 'Number': - newField.property = {} - newField.property.defaultValue = field.property.defaultValue - newField.property.precision = field.property.precision - newFields.push(newField) - break - case 'DateTime': - newField.property = {} - newField.property.dateFormat = 'YYYY-MM-DD' - newField.property.includeTime = true - newField.property.timeFormat = 'HH:mm' - newField.property.autoFill = true - newFields.push(newField) - break - case 'Checkbox': - newField.property = { - icon: 'white_check_mark', - } - newFields.push(newField) - break - // case 'MagicLink': - // newField.property = {} - // newField.property.foreignDatasheetId = this[field.desc as keyof VikaDB] - // if (field.desc) { - // newFields.push(newField) - // } - // break - case 'Attachment': - newFields.push(newField) - break - default: - newFields.push(newField) - break + newFields.push(newField) + break + case 'Text': + newFields.push(newField) + break + case 'Number': + newField.property = {} + newField.property.defaultValue = field.property.defaultValue + newField.property.precision = field.property.precision + newFields.push(newField) + break + case 'DateTime': + newField.property = {} + newField.property.dateFormat = 'YYYY-MM-DD' + newField.property.includeTime = true + newField.property.timeFormat = 'HH:mm' + newField.property.autoFill = true + newFields.push(newField) + break + case 'Checkbox': + newField.property = { + icon: 'white_check_mark', + } + newFields.push(newField) + break + case 'Attachment': + newFields.push(newField) + break + default: + newFields.push(newField) + break + } } - } - console.info('创建表,表信息:', JSON.stringify(newFields, undefined, 2)) - - const resCreate = await this.createDataSheet(k, sheet.name, newFields) - if (resCreate.success) { - await delay(1000) - const defaultRecords = sheet.defaultRecords - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (defaultRecords) { - // console.info(defaultRecords.length) - const count = Math.ceil(defaultRecords.length / 10) - for (let i = 0; i < count; i++) { - const records = defaultRecords.splice(0, 10) - console.info('写入:', records.length) - await this.createRecord(this.dataBaseIds[k as keyof DateBase], records) - await delay(1000) + console.info('创建表,表信息:', JSON.stringify(newFields)) + + const resCreate = await this.createDataSheet( + k, + sheet.name, + newFields, + ) + console.info('创建表结果:', JSON.stringify(resCreate)) + if (resCreate.createdAt) { + await delay(1000) + const defaultRecords = sheet.defaultRecords + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (defaultRecords) { + // console.info(defaultRecords.length) + const count = Math.ceil(defaultRecords.length / 10) + for (let i = 0; i < count; i++) { + const records = defaultRecords.splice(0, 10) + console.info('写入:', records.length) + try { + await this.createRecord( + this.dataBaseIds[k as keyof DateBase], + records, + ) + await delay(1000) + } catch (error) { + console.error('写入表失败:', error) + } + } + console.debug(sheet.name + '初始化数据写入完成...') } - logForm(sheet.name + '初始化数据写入完成...') + } else { + console.info('创建表失败:', resCreate) + throw new Error( + `创建表【${sheet.name}】失败,启动中止,需要重新运行...`, + ) } + } else if (sheet) { + // logForm(`表已存在:\n${k}/${sheet.name}/${tables[sheet.name]}`) + this.dataBaseIds[k as keyof DateBase] = tables[sheet.name] + this.dataBaseNames[sheet.name as keyof DateBase] + = tables[sheet.name] } else { - console.info('创建表失败:', resCreate) - throw new Error(`创建表【${sheet.name}】失败,启动中止,需要重新运行...`) + /* empty */ } - - } else if (sheet) { - // logForm(`表已存在:\n${k}/${sheet.name}/${tables[sheet.name]}`) - this.dataBaseIds[k as keyof DateBase] = tables[sheet.name] - this.dataBaseNames[sheet.name as keyof DateBase] = tables[sheet.name] - } else { /* empty */ } + } + console.debug('初始化表完成...') + return { message: 'success' } + } else { + return { message: '初始化失败,检查配置信息是否有误!!!' } } - logForm('初始化表完成...') - - return true - } else { - logForm('\n\n指定空间不存在,请先创建空间,并在.env文件或环境变量中配置vika信息\n\n') - return false + } catch (error) { + return error } } - protected static async getAllSpaces () { + async getAllSpaces () { // 获取当前用户的空间站列表 const spaceListResp = await this.vika.spaces.list() if (spaceListResp.success) { @@ -239,28 +379,28 @@ export class VikaDB { } } - protected static async getSpaceId () { - if (this.spaceId) { - return this.spaceId - } + async getSpaceId () { const spaceList: any = await this.getAllSpaces() for (const i in spaceList) { if (spaceList[i].name === this.spaceName) { this.spaceId = spaceList[i].id + this.userId = this.spaceId break } } if (this.spaceId) { - return this.spaceId + return { success: true, code: 200, data: this.spaceId } } else { - return undefined + return spaceList } } - protected static async getNodesList () { + async getNodesList () { if (this.spaceId) { // 获取指定空间站的一级文件目录 - const nodeListResp = await this.vika.nodes.list({ spaceId: this.spaceId }) + const nodeListResp = await this.vika.nodes.list({ + spaceId: this.spaceId, + }) const tables: any = {} if (nodeListResp.success) { // console.info(nodeListResp.data.nodes); @@ -271,30 +411,34 @@ export class VikaDB { tables[node.name] = node.id } }) + return tables } else { console.error('获取数据表失败:', nodeListResp) + return undefined } - return tables } else { - return {} + return undefined } } - protected static async getDataBases () { + async getDataBases () { return this.dataBaseIds } - protected static async getVikaSheet (datasheetId: string) { + async getVikaSheet (datasheetId: string) { const datasheet = await this.vika.datasheet(datasheetId) return datasheet } - protected static async getSheetFields (datasheetId: string) { + async getSheetFields (datasheetId: string) { const datasheet = await this.vika.datasheet(datasheetId) const fieldsResp = await datasheet.fields.list() let fields: any = [] if (fieldsResp.success) { - console.info('getSheetFields获取字段:', JSON.stringify(fieldsResp.data.fields)) + console.info( + 'getSheetFields获取字段:', + JSON.stringify(fieldsResp.data.fields), + ) fields = fieldsResp.data.fields } else { console.error('获取字段失败:', fieldsResp) @@ -302,7 +446,11 @@ export class VikaDB { return fields } - static async createDataSheet (key: string, name: string, fields: { name: string; type: string }[]) { + async createDataSheet ( + key: string, + name: string, + fields: { name: string; type: string }[], + ) { // console.info('创建表...') const datasheetRo = { fields, @@ -311,23 +459,20 @@ export class VikaDB { if (this.spaceId) { try { - const res: any = await this.vika.space(this.spaceId).datasheets.create(datasheetRo) - - console.info(`系统表【${name}】创建结果:`, res.message || 'fail') - - if (res.data && res.data.id) { - this.dataBaseIds[key as keyof DateBase] = res.data.id - this.dataBaseNames[name as keyof DateBase] = res.data.id - // console.info('创建表成功:', JSON.stringify(this.dataBaseIds)) - // 删除空白行 - await this.clearBlankLines(res.data.id) - return res - } else { - return res - } - + const res: any = await this.vika + .space(this.spaceId) + .datasheets.create(datasheetRo) + + console.info(`系统表【${name}】创建结果:`, JSON.stringify(res)) + + this.dataBaseIds[key as keyof DateBase] = res.data.id + this.dataBaseNames[name as keyof DateBase] = res.data.id + // console.info('创建表成功:', JSON.stringify(this.dataBaseIds)) + // 删除空白行 + await this.clearBlankLines(res.data.id) + return res.data } catch (error) { - console.error(`系统表${name}创建失败:`, error) + console.error(name, error) return error // TODO: handle error } @@ -336,7 +481,7 @@ export class VikaDB { } } - protected static async createRecord (datasheetId: string, records: ICreateRecordsReqParams) { + async createRecord (datasheetId: string, records: ICreateRecordsReqParams) { // console.info('写入维格表:', records.length) const datasheet = await this.vika.datasheet(datasheetId) @@ -350,13 +495,15 @@ export class VikaDB { } catch (err) { console.error('请求维格表写入失败:', err) } - } - protected static async updateRecord (datasheetId: string, records: { - recordId: string - fields: {[key:string]:any} - }[]) { + async updateRecord ( + datasheetId: string, + records: { + recordId: string; + fields: { [key: string]: any }; + }[], + ) { console.info('更新维格表记录:', records.length) const datasheet = await this.vika.datasheet(datasheetId) @@ -368,22 +515,21 @@ export class VikaDB { } catch (err) { console.error('请求维格表更新失败:', err) } - } - protected static async deleteRecords (datasheetId: string, recordsIds: string | any[]) { + async deleteRecords (datasheetId: string, recordsIds: string | any[]) { // console.info('操作数据表ID:', datasheetId) // console.info('待删除记录IDs:', recordsIds) const datasheet = this.vika.datasheet(datasheetId) const response = await datasheet.records.delete(recordsIds) if (response.success) { - console.info('删除记录成功:', recordsIds.length || '0') + console.info(`删除${recordsIds.length}条记录`) } else { console.error('删除记录失败:', response) } } - protected static async getRecords (datasheetId: string, query:any = {}) { + async getRecords (datasheetId: string, query: any = {}) { let records: any = [] query['pageSize'] = 1000 const datasheet = await this.vika.datasheet(datasheetId) @@ -393,13 +539,13 @@ export class VikaDB { records = response.data.records // console.info(records) } else { - console.error('获取数据记录失败:', JSON.stringify(response)) + console.error('getRecords获取数据记录失败:', JSON.stringify(response)) records = response } return records } - static async getAllRecords (datasheetId: string) { + async getAllRecords (datasheetId: string) { let records: any = [] const datasheet = await this.vika.datasheet(datasheetId) const response: any = await datasheet.records.queryAll() @@ -417,7 +563,7 @@ export class VikaDB { return records } - protected static async clearBlankLines (datasheetId: any) { + async clearBlankLines (datasheetId: any) { const records = await this.getRecords(datasheetId, {}) // console.info(records) const recordsIds: any = [] diff --git a/src/db/vika-orm.ts b/src/db/vika-orm.ts index 75411ced..5f0abe3c 100644 --- a/src/db/vika-orm.ts +++ b/src/db/vika-orm.ts @@ -1,10 +1,14 @@ /* eslint-disable guard-for-in */ import { Vika, ICreateRecordsReqParams } from '@vikadata/vika' -import { log } from 'wechaty' -// import 'dotenv/config.js' +import * as fs from 'node:fs' interface IField { - [key:string]: string | ''; + [key: string]: string | ''; +} + +interface OrmResponse { + message: 'success' | 'fail'; + data: any; } export interface IRecord { @@ -39,15 +43,17 @@ export abstract class BaseEntity { /** * Vika API 选项 */ - protected static vikaOptions: VikaOptions | undefined + protected vikaOptions: VikaOptions | undefined /** * 映射选项 */ - protected static mappingOptions: MappingOptions + protected mappingOptions!: MappingOptions recordId: string = '' + datasheet: any + /** * 设置 Vika API 选项 */ @@ -57,18 +63,16 @@ export abstract class BaseEntity { return this.recordId } - protected static getRecordId (): string { + protected getRecordId (): string { throw new Error('Must be implemented by subclass') } - static datasheet: any - /** * 设置 Vika API 选项 */ - static setVikaOptions (options: VikaOptions) { - // log.info('setVikaOptions:', options) + setVikaOptions (options: VikaOptions) { + // console.info('setVikaOptions:', options) if (!options.apiKey || !options.baseId) { throw Error('loss apiKey or baseId') } else { @@ -81,35 +85,35 @@ export abstract class BaseEntity { /** * 设置映射选项 */ - static setMappingOptions (options: MappingOptions) { + setMappingOptions (options: MappingOptions) { this.mappingOptions = options } - protected static getMappingOptions (): MappingOptions { + protected getMappingOptions (): MappingOptions { throw new Error('Must be implemented by subclass') } /** * 格式化数据 */ - static formatData (data: Record) { + formatData (data: Record) { const formatted: Record = {} const mappingOptions = this.getMappingOptions() - // log.info('this.mappingOptions', mappingOptions) + // console.info('this.mappingOptions', mappingOptions) for (const key in data) { const mappedKey = mappingOptions.fieldMapping[key] if (mappedKey) { formatted[mappedKey] = data[key] } } - // log.info('formatted:', JSON.stringify(formatted)) + // console.info('formatted:', JSON.stringify(formatted)) return formatted } /** * 从记录创建实体 */ - protected static createFromRecord (record: any) { + protected createFromRecord (record: any) { const data: any = record.fields const entity: any = {} const mappingOptions = this.getMappingOptions() @@ -125,135 +129,179 @@ export abstract class BaseEntity { /** * 创建记录 */ - static async create (entity: Partial) { + async create (entity: Partial): Promise { const recordFormat = this.formatData(entity) - // log.info('recordFormat', JSON.stringify(recordFormat)) - const records:ICreateRecordsReqParams = [ { fields:recordFormat } ] + // console.info('recordFormat', JSON.stringify(recordFormat)) + const records: ICreateRecordsReqParams = [ { fields: recordFormat } ] try { const res = await this.datasheet.records.create(records) - // log.info('record res:', JSON.stringify(res)) + // console.info('record res:', JSON.stringify(res)) const record = res.data.records[0] - return this.createFromRecord(record) as T + return { + message: 'success', + data: this.createFromRecord(record) as T, + } } catch (err) { - throw Error('create fail') + // throw Error('create fail'); + console.error('create fail:', err) + return { + message: 'fail', + data: err, + } } } /** * 批量创建记录 */ - static async createBatch (entity: Partial[]) { - // log.info('写入维格表:', records.length) - const recordsNew:ICreateRecordsReqParams = entity.map((r: any) => ({ fields:this.formatData(r) })) + async createBatch (entity: IRecord[]): Promise { + // console.info('写入维格表:', records.length) + const recordsNew: ICreateRecordsReqParams = entity.map((r: any) => ({ + fields: this.formatData(r), + })) try { const res = await this.datasheet.records.create(recordsNew) if (res.success) { - // log.info(res.data.records) + // console.info(res.data.records) } else { - log.error('记录写入维格表失败:', res) + console.error('记录写入维格表失败:', res) + } + const records = res.data.records.map((r: any) => + this.createFromRecord(r), + ) + return { + message: 'success', + data: records, } - const records = res.data.records.map((r: any) => this.createFromRecord(r)) - return records } catch (err) { - log.error('请求维格表写入失败:', err) - return err + console.error('请求维格表写入失败:', err) + return { + message: 'fail', + data: err, + } } } /** * 更新记录 */ - static async update ( - id: string, - entity: Partial, - ) { + async update (id: string, entity: IRecord): Promise { const data = this.formatData(entity) try { - const res = await this.datasheet.records.update([ { fields:data, recordId:id } ]) + const res = await this.datasheet.records.update([ + { fields: data, recordId: id }, + ]) if (!res.success) { - log.error('记录更新维格表失败:', res) + console.error('记录更新维格表失败:', res) + } + const record: IRecord = res.data.records[0] + return { + message: 'success', + data: this.createFromRecord(record) as IRecord, } - const record:IRecord = res.data.records[0] - return this.createFromRecord(record) as IRecord } catch (err) { - log.error('请求维格表更新失败:', err) - return err + console.error('请求维格表更新失败:', err) + return { + message: 'fail', + data: err, + } } } /** * 批量更新记录 */ - static async updatEmultiple ( - records: {recordId: string, fields: Partial}[], - ) { + async updatEmultiple ( + records: { recordId: string; fields: Partial }[], + ): Promise { const datas = records.map((item) => { return { - fields:this.formatData(item), - recordId:item.recordId, + fields: this.formatData(item.fields), + recordId: item.recordId, } }) - + // console.debug('updatEmultiple:', datas) try { const res = await this.datasheet.records.update(datas) if (!res.success) { - log.error('记录更新维格表失败:', res) + console.error('记录更新维格表失败:', res) + } else { + // console.debug('updatEmultiple success res:', res) + } + return { + message: 'success', + data: res, } - const record:IRecord = res.data.records[0] - return this.createFromRecord(record) as IRecord + // const record: IRecord = res.data.records[0]; + // return this.createFromRecord(record) as IRecord; } catch (err) { - log.error('请求维格表更新失败:', err) - return err + console.error('请求维格表更新失败:', err) + return { + message: 'fail', + data: err, + } } } /** * 删除记录 */ - static async delete (id: string) { + async delete (id: string): Promise { const response = await this.datasheet.records.delete([ id ]) if (!response.success) { - log.error('删除记录失败:', response) + console.error('删除记录失败:', response) + } + return { + message: response.success ? 'success' : 'fail', + data: response, } - return response } /** * 批量删除记录 */ - static async deleteBatch (ids: string[]) { + async deleteBatch (ids: string[]): Promise { const response = await this.datasheet.records.delete(ids) if (!response.success) { - log.error('删除记录失败:', response) + console.error('删除记录失败:', response) + } + return { + message: response.success ? 'success' : 'fail', + data: response, } - return response } /** * 根据 ID 查找单个记录 */ - static async findById (id: string): Promise { - + async findById (id: string): Promise { let records: IRecord[] = [] - const query = { recordIds:id } + const query = { recordIds: id } // 分页获取记录,默认返回第一页 const response = await this.datasheet.records.query(query) if (response.success) { records = response.data.records - if (records.length) return this.createFromRecord(records[0]) as IRecord - // log.info(records) - return null + return { + message: 'success', + data: this.createFromRecord(records[0]) as IRecord, + } + } + console.error('findById获取数据记录失败:', JSON.stringify(response)) + return { + message: 'fail', + data: response, } - log.error('获取数据记录失败:', JSON.stringify(response)) - throw response - } /** * 根据字段查询多条记录 */ - static async findByField (fieldName: string, value: any, pageSize: number | undefined = 1000): Promise { + async findByField ( + fieldName: string, + value: any, + pageSize: number | undefined = 100, + ): Promise { const field = this.mappingOptions.fieldMapping[fieldName] let records: IRecord[] = [] if (!field) { @@ -261,80 +309,111 @@ export abstract class BaseEntity { } const query = { - filterByFormula:`{${field}}="${value}"`, + filterByFormula: `{${field}}="${value}"`, pageSize, } - log.info('query:', JSON.stringify(query)) + console.info('findByField:', JSON.stringify(query)) // 分页获取记录,默认返回第一页 const response = await this.datasheet.records.query(query) if (response.success) { records = response.data.records - // log.info(records) - return records.map((r: any) => this.createFromRecord(r)) as IRecord[] + // console.info(records) + return { + message: 'success', + data: records.map((r: any) => this.createFromRecord(r)) as IRecord[], + } + } + console.error('findByField获取数据记录失败:', JSON.stringify(response)) + return { + message: 'fail', + data: response, } - log.error('获取数据记录失败:', JSON.stringify(response)) - return response - } /** * 根据字段查询多条记录 */ - static async findByQuery (filterByFormula: string, pageSize: number | undefined = 1000): Promise { + async findByQuery ( + filterByFormula: string, + pageSize: number | undefined = 100, + ): Promise { const query = { filterByFormula, pageSize, } - log.info('query:', JSON.stringify(query)) + console.info('findByQuery:', JSON.stringify(query)) // 分页获取记录,默认返回第一页 const response = await this.datasheet.records.query(query) if (response.success) { const { records } = response.data - // log.info(records) - return records.map((r: any) => this.createFromRecord(r)) as IRecord[] + // console.info(records) + return { + message: 'success', + data: records.map((r: any) => this.createFromRecord(r)) as IRecord[], + } + } + console.error('findByQuery获取数据记录失败:', JSON.stringify(response)) + return { + message: 'fail', + data: response, } - log.error('获取数据记录失败:', JSON.stringify(response)) - return response - } /** * 查询所有记录 */ - static async findAll (): Promise { - const records:IRecord[] = [] + async findAll (): Promise { + const records: IRecord[] = [] try { // Automatically handle pagination and iterate through all records. const recordsIter = this.datasheet.records.queryAll() // The for-await loop requires an async function and has specific version requirements for Node.js/browser. for await (const eachPageRecords of recordsIter) { - // log.info('findAll ():', JSON.stringify(eachPageRecords)) + // console.info('findAll ():', JSON.stringify(eachPageRecords)) records.push(...eachPageRecords) } - // log.info('findAll() records:', records.length) - return records.map((r: any) => this.createFromRecord(r)) as IRecord[] - + // console.info('findAll() records:', records.length) + return { + message: 'success', + data: records.map((r: any) => this.createFromRecord(r)) as IRecord[], + } } catch (error) { - log.error('Error in findAll():', error) - throw error + console.error('Error in findAll():', error) + return { + message: 'fail', + data: error, + } } } - /** - * 保存实体 - */ - async save (): Promise { - const Constructor = this.constructor as typeof BaseEntity - if (this.recordId) { - await Constructor.update(this.recordId, this) - } else { - const entity:any = await Constructor.create(this) - this.recordId = entity.id + async upload (path: string): Promise { + console.info('文件本地路径:', path) + try { + if (fs.existsSync(path)) { + // 文件存在,可以继续你的操作 + const file = fs.createReadStream(path) + const resp = await this.datasheet.upload(file) + return { + message: 'success', + data: resp, + } + } else { + console.error('文件不存在') + // 文件不存在,你需要处理这个问题 + return { + message: 'fail', + data: '文件不存在', + } + } + } catch (error) { + console.error('上传文件失败:', error) + return { + message: 'fail', + data: error, + } } - - return this } } diff --git a/src/db/vika.ts b/src/db/vika.ts deleted file mode 100644 index 72bc0feb..00000000 --- a/src/db/vika.ts +++ /dev/null @@ -1,279 +0,0 @@ -/* eslint-disable sort-keys */ -import type { ICreateRecordsReqParams, Vika } from '@vikadata/vika' -import { log } from 'wechaty' -import fs from 'fs' -import 'dotenv/config.js' - -interface IField { - [key:string]: string | ''; -} - -export interface IRecord { - recordId: string; - fields: IField; -} - -interface IFieldMapping { - id: string; - name: string; - type: string; - property: any; - editable: boolean; - isPrimary?: boolean; -} - -interface IFieldMappingResponse { - code: number; - success: boolean; - data: { - fields: IFieldMapping[]; - }; - message: string; -} - -export class VikaSheet { - - private datasheet: any - offsetValue!: number - limitValue!: number - orderby!: any - private fields: any[] = [] - records: any - - constructor (client:Vika, datasheetId: string) { - this.datasheet = client.datasheet(datasheetId) - this.offsetValue = 0 - this.limitValue = 15 - } - - public limit (offset: number, limit: number): this { - this.offsetValue = offset || 0 - this.limitValue = limit || 15 - return this - } - - public sort (orderby: any): this { - this.orderby = orderby - return this - } - - async insert (records: ICreateRecordsReqParams) { - // log.info('写入维格表:', records.length) - - try { - const res = await this.datasheet.records.create(records) - if (res.success) { - // log.info(res.data.records) - } else { - log.error('记录写入维格表失败:', res) - } - return res - } catch (err) { - log.error('请求维格表写入失败:', err) - return err - } - - } - - async upload (path:string, file:any) { - if (path) { - log.info('文件本地路径:', path) - file = fs.createReadStream(path) - } - - try { - const resp = await this.datasheet.upload(file) - if (!resp.success) { - log.info('文件上传请求成功,上传失败', JSON.stringify(resp)) - } - return resp - } catch (error) { - log.error('文件上传请求失败:', error) - return error - } - - } - - async update (records: { - recordId: string - fields: {[key:string]:any} - }[]) { - log.info('更新维格表记录:', records.length) - - try { - const res = await this.datasheet.records.update(records) - if (!res.success) { - log.error('记录更新维格表失败:', res) - } - return res - } catch (err) { - log.error('请求维格表更新失败:', err) - return err - } - - } - - async updateOne (recordId: string, fields: {[key:string]:any}) { - - try { - const res = await this.datasheet.records.update([ { recordId, fields } ]) - if (!res.success) { - log.error('记录更新维格表失败:', res) - } - return res - } catch (err) { - log.error('请求维格表更新失败:', err) - return err - } - - } - - async remove (recordsIds: string[]) { - // log.info('操作数据表ID:', datasheetId) - // log.info('待删除记录IDs:', recordsIds) - const response = await this.datasheet.records.delete(recordsIds) - if (!response.success) { - log.error('删除记录失败:', response) - } - return response - } - - async removeOne (recordsId: string) { - // log.info('操作数据表ID:', datasheetId) - // log.info('待删除记录IDs:', recordsIds) - const response = await this.datasheet.records.delete([ recordsId ]) - if (!response.success) { - log.error('删除记录失败:', response) - } - return response - } - - async find (query:any = {}) { - let records: IRecord[] = [] - query['pageSize'] = 1000 - // 分页获取记录,默认返回第一页 - const response = await this.datasheet.records.query(query) - if (response.success) { - records = response.data.records - // log.info(records) - return records - } else { - log.error('获取数据记录失败:', JSON.stringify(response)) - return response - } - } - - async findOne (query:any = {}) { - let records: IRecord[] = [] - query['pageSize'] = 1 - // 分页获取记录,默认返回第一页 - const response = await this.datasheet.records.query(query) - if (response.success) { - records = response.data.records - if (records.length) return records[0] - // log.info(records) - return {} - } else { - log.error('获取数据记录失败:', JSON.stringify(response)) - return response - } - } - - async findAll (): Promise { - const records: IRecord[] = [] - - try { - // Automatically handle pagination and iterate through all records. - const recordsIter = this.datasheet.records.queryAll() - - // The for-await loop requires an async function and has specific version requirements for Node.js/browser. - for await (const eachPageRecords of recordsIter) { - // log.info('findAll ():', JSON.stringify(eachPageRecords)) - records.push(...eachPageRecords) - } - - // log.info('findAll() records:', records.length) - return records - } catch (error) { - log.error('Error in findAll():', error) - throw error - } - } - - async getFields () { - if (this.fields.length) return this.fields - const fieldsResp:IFieldMappingResponse = await this.datasheet.fields.list() - - if (fieldsResp.success) { - this.fields = fieldsResp.data.fields - // log.info('fieldsResp:', this.fields) - } else { - console.error(fieldsResp) - } - return this.fields - } - - async createFields (fieldRo: any) { - try { - const res = await this.datasheet.fields.create(fieldRo) - if (res.success) { - // TODO: save field.id - } - return res - } catch (error) { - // TODO: handle error - return error - } - - } - - async removeFields (fieldId: string) { - try { - const res = await this.datasheet.fields.delete(fieldId) - return res - } catch (error) { - return error - } - - } - - keyConversion (records:ICreateRecordsReqParams) { - return records.map(record => { - const newFields: { [key: string]: any } = {} - - for (const [ oldKey, value ] of Object.entries(record.fields)) { - const newKey = oldKey.split('|')[1] || oldKey // 获取 "|" 后面的部分作为新键 - newFields[newKey] = value - } - record.fields = newFields - return record - }) - } - - async nameConversion (records: ICreateRecordsReqParams) { - const fields = await this.getFields() - // 创建一个映射表 - const fieldMap: { [key: string]: string } = {} - for (const field of fields) { - const parts = field.name.split('|') - if (parts.length === 2) { - fieldMap[parts[1]] = field.name - } - } - - // 使用映射表转换记录 - const convertedRecords: ICreateRecordsReqParams = records.map((record) => { - const newFields: { [key: string]: string } = {} - for (const [ key, value ] of Object.entries(record.fields)) { - newFields[fieldMap[key] || key] = value as string - } - record.fields = newFields - return record - }) - - return convertedRecords - } - -} - -export default VikaSheet diff --git a/src/db/vikaModel/Env/mod.ts b/src/db/vikaModel/Env/mod.ts index 775b48ec..13b51cdb 100644 --- a/src/db/vikaModel/Env/mod.ts +++ b/src/db/vikaModel/Env/mod.ts @@ -31,6 +31,8 @@ const defaultRecords: any fields: { '标识|key': 'WECHATY_PUPPET', '配置项|name': 'Wechaty-Puppet', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '可选值:\nwechaty-puppet-wechat4u\nwechaty-puppet-wechat\nwechaty-puppet-xp\nwechaty-puppet-engine\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\u0000\nwechaty-puppet-padlocal\nwechaty-puppet-service', '值|value': 'wechaty-puppet-wechat4u', }, @@ -42,6 +44,8 @@ const defaultRecords: any fields: { '标识|key': 'WECHATY_TOKEN', '配置项|name': 'Wechaty-Token', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '使用wechaty-puppet-padlocal、wechaty-puppet-service时需配置此token', }, }, @@ -52,6 +56,8 @@ const defaultRecords: any fields: { '标识|key': 'ADMINROOM_ADMINROOMID', '配置项|name': '基础配置-管理群ID', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '管理群的ID,只有在此群内发布管理指令才会生效', }, }, @@ -62,6 +68,8 @@ const defaultRecords: any fields: { '标识|key': 'ADMINROOM_ADMINROOMTOPIC', '配置项|name': '基础配置-管理群名称', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '管理群名称,只有在此群内发布管理指令才会生效', }, }, @@ -72,6 +80,8 @@ const defaultRecords: any fields: { '标识|key': 'BASE_BOT_ID', '配置项|name': '基础配置-机器人微信号', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '机器人微信号,登录成功后自动更新', }, }, @@ -82,6 +92,8 @@ const defaultRecords: any fields: { '标识|key': 'BASE_BOT_NAME', '配置项|name': '基础配置-机器人微信昵称', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '机器人微信昵称,登录成功后自动更新', }, }, @@ -92,6 +104,8 @@ const defaultRecords: any fields: { '标识|key': 'VIKA_USEVIKA', '配置项|name': '维格表-启用维格表', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '启用维格表托管配置', '值|value': 'false', }, @@ -103,6 +117,8 @@ const defaultRecords: any fields: { '标识|key': 'VIKA_UPLOADMESSAGETOVIKA', '配置项|name': '维格表-消息上传到维格表', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '开启后消息记录会自动上传到维格表的【消息记录】表', '值|value': 'true', }, @@ -114,6 +130,8 @@ const defaultRecords: any fields: { '标识|key': 'AUTOQA_AUTOREPLY', '配置项|name': '智能问答-启用自动问答', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '开启后可以使用微信对话平台只能问答', '值|value': 'false', }, @@ -125,6 +143,8 @@ const defaultRecords: any fields: { '标识|key': 'WXOPENAI_TOKEN', '配置项|name': '微信对话开放平台-Token', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '微信对话开放平台中获取', }, }, @@ -135,6 +155,8 @@ const defaultRecords: any fields: { '标识|key': 'WXOPENAI_ENCODINGAESKEY', '配置项|name': '微信对话开放平台-EncodingAESKey', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '微信对话开放平台中获取', }, }, @@ -145,6 +167,8 @@ const defaultRecords: any fields: { '标识|key': 'WXOPENAI_APPID', '配置项|name': '微信对话开放平台-APPID', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '微信对话开放平台中获取,应用ID', }, }, @@ -155,6 +179,8 @@ const defaultRecords: any fields: { '标识|key': 'WXOPENAI_MANAGERID', '配置项|name': '微信对话开放平台-管理员ID', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '微信对话开放平台中获取', }, }, @@ -165,6 +191,8 @@ const defaultRecords: any fields: { '标识|key': 'CHATGPT_KEY', '配置项|name': 'ChatGPT-Key', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': 'openai平台获取', }, }, @@ -175,6 +203,8 @@ const defaultRecords: any fields: { '标识|key': 'CHATGPT_ENDPOINT', '配置项|name': 'ChatGPT-Endpoint', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': 'openai平台获取', }, }, @@ -185,6 +215,8 @@ const defaultRecords: any fields: { '标识|key': 'CHATGPT_MODEL', '配置项|name': 'ChatGPT-Model', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '默认模型gpt-3.5-turbo,可修改为chatgpt支持的其他模型', '值|value': 'gpt-3.5-turbo', }, @@ -196,6 +228,8 @@ const defaultRecords: any fields: { '标识|key': 'MQTT_MQTTMESSAGEPUSH', '配置项|name': 'MQTT连接-MQTT推送', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '开启后消息会发送到MQTT队列,需要先配置MQTT配置项', '值|value': 'false', }, @@ -207,6 +241,8 @@ const defaultRecords: any fields: { '标识|key': 'MQTT_MQTTCONTROL', '配置项|name': 'MQTT连接-MQTT控制', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '开启可以通过MQTT控制微信,需要先配置MQTT配置项', '值|value': 'false', }, @@ -218,6 +254,8 @@ const defaultRecords: any fields: { '标识|key': 'MQTT_USERNAME', '配置项|name': 'MQTT连接-用户名', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': 'MQTT连接配置信息,推荐使用百度云的物联网核心套件', }, }, @@ -228,6 +266,8 @@ const defaultRecords: any fields: { '标识|key': 'MQTT_PASSWORD', '配置项|name': 'MQTT连接-密码', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': 'MQTT连接配置信息,推荐使用百度云的物联网核心套件', }, }, @@ -238,6 +278,8 @@ const defaultRecords: any fields: { '标识|key': 'MQTT_ENDPOINT', '配置项|name': 'MQTT连接-接入地址', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': 'MQTT连接配置信息,推荐使用百度云的物联网核心套件', '值|value': 'broker.emqx.io', }, @@ -249,6 +291,8 @@ const defaultRecords: any fields: { '标识|key': 'MQTT_PORT', '配置项|name': 'MQTT连接-端口号', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': 'MQTT连接配置信息,推荐使用百度云的物联网核心套件', '值|value': '8883', }, @@ -260,6 +304,8 @@ const defaultRecords: any fields: { '标识|key': 'WEBHOOK_WEBHOOKMESSAGEPUSH', '配置项|name': 'HTTP消息推送-WebHook推送', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': 'TODO-开启后系统将机器人事件消息推送到指定的地址', '值|value': 'false', }, @@ -271,6 +317,8 @@ const defaultRecords: any fields: { '标识|key': 'WEBHOOK_URL', '配置项|name': 'HTTP消息推送-地址', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '格式 http://baidu.com/abc,多个地址使用英文逗号隔开,使用post请求推送', }, }, @@ -281,6 +329,8 @@ const defaultRecords: any fields: { '标识|key': 'WEBHOOK_TOKEN', '配置项|name': 'HTTP消息推送-Token', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '当填写token时优先使用token,其次用户名+密码,再次无鉴权请求', }, }, @@ -291,6 +341,8 @@ const defaultRecords: any fields: { '标识|key': 'WEBHOOK_USERNAME', '配置项|name': 'HTTP消息推送-用户名', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '当填写token时优先使用token,其次用户名+密码,再次无鉴权请求', }, }, @@ -301,29 +353,35 @@ const defaultRecords: any fields: { '标识|key': 'WEBHOOK_PASSWORD', '配置项|name': 'HTTP消息推送-密码', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '当填写token时优先使用token,其次用户名+密码,再次无鉴权请求', }, }, - { - recordId: 'recwRJEJCuesw', - createdAt: 1694149441000, - updatedAt: 1694149441000, - fields: { - '标识|key': 'YUQUE_TOKEN', - '配置项|name': '语雀-token', - '说明|desc': '语雀知识库token', - }, - }, - { - recordId: 'reckTO9r9MHFK', - createdAt: 1694149441000, - updatedAt: 1694149441000, - fields: { - '标识|key': 'YUQUE_NAMESPACE', - '配置项|name': '语雀-空间名称', - '说明|desc': '语雀知识库空间名称', - }, - }, + // { + // recordId: 'recwRJEJCuesw', + // createdAt: 1694149441000, + // updatedAt: 1694149441000, + // fields: { + // '标识|key': 'YUQUE_TOKEN', + // '配置项|name': '语雀-token', + // '同步状态|syncStatus': '未同步', + // '操作|action': '选择操作', + // '说明|desc': '语雀知识库token', + // }, + // }, + // { + // recordId: 'reckTO9r9MHFK', + // createdAt: 1694149441000, + // updatedAt: 1694149441000, + // fields: { + // '标识|key': 'YUQUE_NAMESPACE', + // '配置项|name': '语雀-空间名称', + // '同步状态|syncStatus': '未同步', + // '操作|action': '选择操作', + // '说明|desc': '语雀知识库空间名称', + // }, + // }, { recordId: 'recJg5CbSyIlu', createdAt: 1694516737000, @@ -331,6 +389,8 @@ const defaultRecords: any fields: { '标识|key': 'MESSAGE_ENCRYPT', '配置项|name': '消息加密-下发消息加密', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '值|value': 'false', }, }, @@ -341,6 +401,8 @@ const defaultRecords: any fields: { '标识|key': 'MESSAGE_ENCODINGAESKEY', '配置项|name': '消息加密-消息加密密钥', + '同步状态|syncStatus': '未同步', + '操作|action': '选择操作', '说明|desc': '消息加密密钥,vika推送地址https://3sewxanjdvsbp.cfc-execute.bj.baidubce.com/mqtt', '值|value': 'X00fcQHkvRkNUdJefu4FD6pym2oIvs63Y5NP3pnZ5po', }, diff --git a/src/db/vikaModel/Env/records.json b/src/db/vikaModel/Env/records.json index 61ae7b8b..8cc727f9 100644 --- a/src/db/vikaModel/Env/records.json +++ b/src/db/vikaModel/Env/records.json @@ -11,6 +11,8 @@ "fields": { "标识|key": "WECHATY_PUPPET", "配置项|name": "Wechaty-Puppet", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "可选值:\nwechaty-puppet-wechat4u\nwechaty-puppet-wechat\nwechaty-puppet-xp\nwechaty-puppet-engine\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\u0000\nwechaty-puppet-padlocal\nwechaty-puppet-service", "值|value": "wechaty-puppet-wechat4u" } @@ -22,6 +24,8 @@ "fields": { "标识|key": "WECHATY_TOKEN", "配置项|name": "Wechaty-Token", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "使用wechaty-puppet-padlocal、wechaty-puppet-service时需配置此token" } }, @@ -32,6 +36,8 @@ "fields": { "标识|key": "ADMINROOM_ADMINROOMID", "配置项|name": "基础配置-管理群ID", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "管理群的ID,只有在此群内发布管理指令才会生效" } }, @@ -42,6 +48,8 @@ "fields": { "标识|key": "ADMINROOM_ADMINROOMTOPIC", "配置项|name": "基础配置-管理群名称", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "管理群名称,只有在此群内发布管理指令才会生效" } }, @@ -52,6 +60,8 @@ "fields": { "标识|key": "BASE_BOT_ID", "配置项|name": "基础配置-机器人微信号", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "机器人微信号,登录成功后自动更新" } }, @@ -62,6 +72,8 @@ "fields": { "标识|key": "BASE_BOT_NAME", "配置项|name": "基础配置-机器人微信昵称", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "机器人微信昵称,登录成功后自动更新" } }, @@ -72,6 +84,8 @@ "fields": { "标识|key": "VIKA_USEVIKA", "配置项|name": "维格表-启用维格表", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "启用维格表托管配置", "值|value": "false" } @@ -83,6 +97,8 @@ "fields": { "标识|key": "VIKA_UPLOADMESSAGETOVIKA", "配置项|name": "维格表-消息上传到维格表", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "开启后消息记录会自动上传到维格表的【消息记录】表", "值|value": "true" } @@ -94,6 +110,8 @@ "fields": { "标识|key": "AUTOQA_AUTOREPLY", "配置项|name": "智能问答-启用自动问答", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "开启后可以使用微信对话平台只能问答", "值|value": "false" } @@ -105,6 +123,8 @@ "fields": { "标识|key": "WXOPENAI_TOKEN", "配置项|name": "微信对话开放平台-Token", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "微信对话开放平台中获取" } }, @@ -115,6 +135,8 @@ "fields": { "标识|key": "WXOPENAI_ENCODINGAESKEY", "配置项|name": "微信对话开放平台-EncodingAESKey", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "微信对话开放平台中获取" } }, @@ -125,6 +147,8 @@ "fields": { "标识|key": "WXOPENAI_APPID", "配置项|name": "微信对话开放平台-APPID", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "微信对话开放平台中获取,应用ID" } }, @@ -135,6 +159,8 @@ "fields": { "标识|key": "WXOPENAI_MANAGERID", "配置项|name": "微信对话开放平台-管理员ID", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "微信对话开放平台中获取" } }, @@ -145,6 +171,8 @@ "fields": { "标识|key": "CHATGPT_KEY", "配置项|name": "ChatGPT-Key", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "openai平台获取" } }, @@ -155,6 +183,8 @@ "fields": { "标识|key": "CHATGPT_ENDPOINT", "配置项|name": "ChatGPT-Endpoint", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "openai平台获取" } }, @@ -165,6 +195,8 @@ "fields": { "标识|key": "MQTT_MQTTMESSAGEPUSH", "配置项|name": "MQTT连接-MQTT推送", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "开启后消息会发送到MQTT队列,需要先配置MQTT配置项", "值|value": "false" } @@ -176,6 +208,8 @@ "fields": { "标识|key": "MQTT_MQTTCONTROL", "配置项|name": "MQTT连接-MQTT控制", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "开启可以通过MQTT控制微信,需要先配置MQTT配置项", "值|value": "false" } @@ -187,6 +221,8 @@ "fields": { "标识|key": "MQTT_USERNAME", "配置项|name": "MQTT连接-用户名", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "MQTT连接配置信息,推荐使用百度云的物联网核心套件" } }, @@ -197,6 +233,8 @@ "fields": { "标识|key": "MQTT_PASSWORD", "配置项|name": "MQTT连接-密码", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "MQTT连接配置信息,推荐使用百度云的物联网核心套件" } }, @@ -207,6 +245,8 @@ "fields": { "标识|key": "MQTT_ENDPOINT", "配置项|name": "MQTT连接-接入地址", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "MQTT连接配置信息,推荐使用百度云的物联网核心套件", "值|value": "broker.emqx.io" } @@ -218,6 +258,8 @@ "fields": { "标识|key": "MQTT_PORT", "配置项|name": "MQTT连接-端口号", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "MQTT连接配置信息,推荐使用百度云的物联网核心套件", "值|value": "8883" } @@ -229,6 +271,8 @@ "fields": { "标识|key": "WEBHOOK_WEBHOOKMESSAGEPUSH", "配置项|name": "HTTP消息推送-WebHook推送", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "TODO-开启后系统将机器人事件消息推送到指定的地址", "值|value": "false" } @@ -240,6 +284,8 @@ "fields": { "标识|key": "WEBHOOK_URL", "配置项|name": "HTTP消息推送-地址", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "格式 http://baidu.com/abc,多个地址使用英文逗号隔开,使用post请求推送" } }, @@ -250,6 +296,8 @@ "fields": { "标识|key": "WEBHOOK_TOKEN", "配置项|name": "HTTP消息推送-Token", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "当填写token时优先使用token,其次用户名+密码,再次无鉴权请求" } }, @@ -260,6 +308,8 @@ "fields": { "标识|key": "WEBHOOK_USERNAME", "配置项|name": "HTTP消息推送-用户名", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "当填写token时优先使用token,其次用户名+密码,再次无鉴权请求" } }, @@ -270,6 +320,8 @@ "fields": { "标识|key": "WEBHOOK_PASSWORD", "配置项|name": "HTTP消息推送-密码", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "当填写token时优先使用token,其次用户名+密码,再次无鉴权请求" } }, @@ -280,6 +332,8 @@ "fields": { "标识|key": "YUQUE_TOKEN", "配置项|name": "语雀-token", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "语雀知识库token" } }, @@ -290,6 +344,8 @@ "fields": { "标识|key": "YUQUE_NAMESPACE", "配置项|name": "语雀-空间名称", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "语雀知识库空间名称" } }, @@ -300,6 +356,8 @@ "fields": { "标识|key": "MESSAGE_ENCRYPT", "配置项|name": "消息加密-下发消息加密", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "值|value": "false" } }, @@ -310,6 +368,8 @@ "fields": { "标识|key": "MESSAGE_ENCODINGAESKEY", "配置项|name": "消息加密-消息加密密钥", + "同步状态|syncStatus": "未同步", + "操作|action": "选择操作", "说明|desc": "消息加密密钥,vika推送地址https://3sewxanjdvsbp.cfc-execute.bj.baidubce.com/mqtt", "值|value": "X00fcQHkvRkNUdJefu4FD6pym2oIvs63Y5NP3pnZ5po" } diff --git a/src/db/vikaModel/Group/fields.ts b/src/db/vikaModel/Group/fields.ts new file mode 100644 index 00000000..96b9243a --- /dev/null +++ b/src/db/vikaModel/Group/fields.ts @@ -0,0 +1,38 @@ +export const vikaFields = { + code: 200, + success: true, + data: { + fields: [ + { + id: 'fld3QEdSdPya2', + name: '分组名称|groupName', + type: 'SingleText', + property: {}, + editable: true, + isPrimary: true, + }, + { + id: 'fldKykglJwuHz', + name: '好友昵称|name', + type: 'SingleText', + property: { defaultValue: '' }, + editable: true, + }, + { + id: 'fldKykglJwuHz', + name: '备注名称|alias', + type: 'SingleText', + property: { defaultValue: '' }, + editable: true, + }, + { + id: 'fld7cPH8MN7ej', + name: '好友ID|id', + type: 'SingleText', + property: { defaultValue: '' }, + editable: true, + }, + ], + }, + message: 'SUCCESS', +} diff --git a/src/db/vikaModel/Group/mod.ts b/src/db/vikaModel/Group/mod.ts new file mode 100644 index 00000000..f738a8fa --- /dev/null +++ b/src/db/vikaModel/Group/mod.ts @@ -0,0 +1,14 @@ +/* eslint-disable sort-keys */ + +import type { + Sheet, + // Field, +} from '../Model' +import { vikaFields } from './fields.js' +import { defaultRecords } from './records.js' + +export const sheet: Sheet = { + fields: vikaFields.data.fields, + name: '分组|Group', + defaultRecords: defaultRecords.data.records, +} diff --git a/src/db/vikaModel/Group/records.ts b/src/db/vikaModel/Group/records.ts new file mode 100644 index 00000000..8eba286f --- /dev/null +++ b/src/db/vikaModel/Group/records.ts @@ -0,0 +1,6 @@ +export const defaultRecords: any = { + code: 200, + success: true, + data: { total: 0, records: [], pageNum: 1, pageSize: 0 }, + message: 'SUCCESS', +} diff --git a/src/db/vikaModel/index.ts b/src/db/vikaModel/index.ts index d95de84e..c73ffcbb 100644 --- a/src/db/vikaModel/index.ts +++ b/src/db/vikaModel/index.ts @@ -18,6 +18,7 @@ import { stockSheet } from './Stock/mod.js' import { sheet as groupNoticeSheet } from './GroupNotice/mod.js' import { sheet as chatBotSheet } from './ChatBot/mod.js' import { sheet as chatBotUserSheet } from './ChatBotUser/mod.js' +import { sheet as groupSheet } from './Group/mod.js' const sheets: Sheets = { qaSheet, @@ -34,7 +35,7 @@ const sheets: Sheets = { chatBotSheet, chatBotUserSheet, // stockSheet, - // groupSheet, + groupSheet, // switchSheet, // roomWhiteListSheet, // contactWhiteListSheet, diff --git a/src/handlers/on-message.ts b/src/handlers/on-message.ts index ffbf6b33..4d5d063d 100644 --- a/src/handlers/on-message.ts +++ b/src/handlers/on-message.ts @@ -10,7 +10,7 @@ import { import { ChatFlowConfig } from '../api/base-config.js' import { // logger, - logForm, + logForm, logger, } from '../utils/mod.js' import { MqttProxy, eventMessage } from '../proxy/mqtt-proxy.js' @@ -18,11 +18,19 @@ import { uploadMessage } from '../proxy/s3-proxy.js' import { adminAction } from '../api/admin.js' import { qa } from '../app/qa.js' +import { chatbot } from '../app/chatbot.js' import { handleActivityManagement } from '../app/activity.js' import { extractAtContent } from '../app/extract-at.js' +import { ServeGetMedias } from '../api/media.js' +import { ServeCreateCarpoolings } from '../api/carpooling.js' -export async function onMessage (message: Message) { +import { getFormattedRideInfo } from '../plugins/mod.js' +export async function onMessage (message: Message) { + const text = message.text() + const isSelf = message.self() + const talker = message.talker() + const room = message.room() // 存储消息到db,如果写入失败则终止,用于检测是否是重复消息 try { const messageToDB = await formatMessageToDB(message) @@ -40,69 +48,169 @@ export async function onMessage (message: Message) { log.error('消息格式化失败:\n', e) } + if (ChatFlowConfig.isReady && !isSelf) { // 请求管理员群操作 - try { - await adminAction(message) - } catch (e) { - log.error('管理员操作失败 error:', e) - } + try { + await adminAction(message) + } catch (e) { + log.error('管理员操作失败 error:', e) + } - // 请求自动问答 - try { - await qa(message) - } catch (e) { - log.error('自动问答失败 error:', e) - } + // 请求自动问答 + try { + await qa(message) + } catch (e) { + log.error('自动问答失败 error:', e) + } - // 群消息处理,判断非机器人自己发的消息 - const room = message.room() - const isSelf = message.self() - if (room && room.id && !isSelf) { + // 请求智聊服务 try { - // 活动管理 - await handleActivityManagement(message, room) + log.info('智聊服务开始...') + await chatbot(message) } catch (e) { - log.error('活动管理失败 error:', e) + log.error('智聊服务失败 error:', e) } + // 关键字回复 try { - // @机器人消息处理,当引用消息仅包含@机器人时,提取引用消息内容并回复 - await extractAtContent(message) + if (text && ChatFlowConfig.keywordList.length > 0) { + log.info('检测关键字回复...') + const keywordItem = ChatFlowConfig.keywordList.find((item) => item.name === text && item.type === '等于关键字') + if (keywordItem) { + log.info('触发关键字回复,关键字是:', keywordItem.name) + await message.say(keywordItem.desc) + } + } } catch (e) { - log.error('提取@消息失败 error:', e) + log.error('关键字回复失败 error:', e) } - } - // 消息存储到云表格,维格表或者飞书多维表格 - if (ChatFlowConfig.configEnv.VIKA_UPLOADMESSAGETOVIKA) { + // 搜资源 try { - const messageToCloud = await formatMessageToCloud(message) - if (messageToCloud) await saveMessageToCloud(messageToCloud) + const ifMedia = text.startsWith('搜资源') + if (ifMedia) { + log.info('触发搜资源...') + // 资源名称为text去掉'搜资源'后的内容 + const media = text.replace('搜资源', '').trim() + // 如果 + if (media) { + const mediaItem = ChatFlowConfig.mediaList.find((item) => item.name === media) + if (mediaItem) { + await message.say(`「${mediaItem.name}」 ${mediaItem.link}`) + } else { + const res = await ServeGetMedias({ name: media }) + const mediaList = res.data.items + if (mediaList.length === 0) { + await message.say(`没有找到资源「${media}」,在群内@管理员可提供人工搜索`) + } else { + const mediaItem = mediaList.find((item:any) => item.state === '开启') + if (mediaItem) { + await message.say(`「${mediaItem.name}」${mediaItem.link}`) + ChatFlowConfig.mediaList.push(mediaItem) + } else { + await message.say(`没有找到资源「${media}」,在群内@管理员可提供人工搜索`) + } + } + } + } + } } catch (e) { - log.error('消息写入云表格失败:\n', e) + log.error('搜资源失败 error:', e) } - } - // 消息通过MQTT上报 - try { - const mqttProxy = MqttProxy.getInstance() - if (mqttProxy && mqttProxy.isOk && ChatFlowConfig.configEnv.MQTT_MQTTMESSAGEPUSH) { - const messageToMQTT = await formatMessageToMQTT(message) - const eventMessagePayload = eventMessage('onMessage', messageToMQTT) - mqttProxy.pubEvent(eventMessagePayload) + // 群消息处理,判断非机器人自己发的消息 + if (room && room.id) { + try { + // 活动管理 + await handleActivityManagement(message, room) + } catch (e) { + log.error('活动管理失败 error:', e) + } + + // @机器人消息处理,当引用消息仅包含@机器人时,提取引用消息内容并回复 + try { + await extractAtContent(message) + } catch (e) { + log.error('提取@消息失败 error:', e) + } + + // 检测顺风车信息,如果text中包含车找人、人找车、车找n人、n人找车(其中n是一个数字)关键字,则提取信息并回复 + if (text.includes('车找') || text.includes('找车')) { + try { + const rideInfo = await getFormattedRideInfo(message) + // log.info('rideInfo信息:', JSON.stringify(rideInfo, null, 2)) + logger.info('rideInfo信息:' + JSON.stringify(rideInfo, null, 2)) + const res = await ServeCreateCarpoolings(rideInfo) + log.info('保存顺风车信息:', JSON.stringify(res.data, null, 2)) + } catch (e) { + log.error('检测顺风车信息失败 error:', e) + } + } + + // 检测text中是否存在微信ID以及回复内容,提取出nickName、wxid和text,例如text="瓦力:[luyuchao@ledongmao]\n 你在干嘛」\n- - - - - - - - - - - - - - -\n好的,我知道了",则提取出luyuchao、ledongmao和你在干嘛 + try { + const atContent = text.match(/\[(.+)@(.+)\]\n(.+)」\n(.+)\n(.+)/) + log.info('atContent:', atContent) + if (atContent) { + const nickName = atContent[1] + const wxid = atContent[2] + log.info('nickName:', nickName, 'wxid:', wxid) + + // 如果wxid不是weixin或者gh_开头的公众号ID,则提取出回复内容并回复 + if (wxid) { + const contact = await ChatFlowConfig.bot.Contact.find({ id: wxid }) + const content = text.split('- - -\n')[1] + if (contact && content) { + await contact.say(content) + } + } + } else { + log.info('没有找到微信ID') + } + } catch (e) { + log.error('提取微信ID失败 error:', e) + } + + } else { + // 转发到adminRoom + const wxid = talker.id + if (ChatFlowConfig.adminRoom && ![ 'weixin' ].includes(wxid) && wxid.includes('gh_') === false) { + const adminRoom = ChatFlowConfig.adminRoom + await adminRoom.say(`[${talker.name()}@${talker.id}]\n ${message.text()}`) + } } - } catch (e) { - log.error('消息MQTT上报失败:\n', e) - } - // 保存文件到S3 - if (ChatFlowConfig.configEnv.secretAccessKey) { + // 消息存储到云表格,维格表或者飞书多维表格 + if (ChatFlowConfig.configEnv.VIKA_UPLOADMESSAGETOVIKA) { + try { + const messageToCloud = await formatMessageToCloud(message) + if (messageToCloud) await saveMessageToCloud(messageToCloud) + } catch (e) { + log.error('消息写入云表格失败:\n', e) + } + } + + // 消息通过MQTT上报 try { - await uploadMessage(message) + const mqttProxy = MqttProxy.getInstance() + if (mqttProxy && mqttProxy.isOk && ChatFlowConfig.configEnv.MQTT_MQTTMESSAGEPUSH) { + const messageToMQTT = await formatMessageToMQTT(message) + const eventMessagePayload = eventMessage('onMessage', messageToMQTT) + mqttProxy.pubEvent(eventMessagePayload) + } } catch (e) { - log.error('消息S3上传失败:\n', e) + log.error('消息MQTT上报失败:\n', e) } + // 保存文件到S3 + if (ChatFlowConfig.configEnv.secretAccessKey) { + try { + await uploadMessage(message) + } catch (e) { + log.error('消息S3上传失败:\n', e) + } + + } } } diff --git a/src/handlers/on-roomjoin.ts b/src/handlers/on-roomjoin.ts index 2700d9c7..f7a04fd9 100644 --- a/src/handlers/on-roomjoin.ts +++ b/src/handlers/on-roomjoin.ts @@ -1,23 +1,26 @@ import { Contact, Room, log } from 'wechaty' - -const welcomeList:any[] = [] +import { ChatFlowConfig } from '../api/base-config.js' +import { logger } from '../utils/utils.js' /** * 群中有新人进入 */ async function onRoomjoin (room: Room, inviteeList: Contact[], inviter: Contact) { - const roomTopic = await room.topic() - const nameList = inviteeList.map(c => c.name()).join(',') - const inviterName = inviter.name() - - log.info(`Room Join: Room "${roomTopic}" got new members "${nameList}", invited by "${inviterName}"`) + log.info('有新人进入群聊...') + const welcomeList = ChatFlowConfig.welcomeList + const welcomeRoom = welcomeList.find((item) => item.id === room.id) // Check if Vika is OK and if the room is in the welcome list - if (welcomeList.includes(room.id)) { + if (welcomeRoom && welcomeRoom.state === '开启' && welcomeRoom.text) { + const roomTopic = await room.topic() + const nameList = inviteeList.map(c => c.name()).join(',') + const inviterName = inviter.name() + log.info(`新人进群: Room "${roomTopic}" got new members "${nameList}", invited by "${inviterName}"`) + logger.info(`新人进群: Room "${roomTopic}" got new members "${nameList}", invited by "${inviterName}"`) // Send a welcome message only if there are invitees if (inviteeList.length > 0) { - const welcomeMessage = `欢迎加入${roomTopic}, 请阅读群公告~` + const welcomeMessage = `欢迎加入${roomTopic}, ${welcomeRoom.text}~` // await sendMsg(room, welcomeMessage, (chatflowConfig.services as Services).messageService, inviteeList) - await room.say(welcomeMessage) + await room.say(welcomeMessage, ...inviteeList) } } } diff --git a/src/handlers/onReadyOrLogin.ts b/src/handlers/onReadyOrLogin.ts index 31deace9..32216ac0 100644 --- a/src/handlers/onReadyOrLogin.ts +++ b/src/handlers/onReadyOrLogin.ts @@ -3,70 +3,89 @@ import { Contact, Wechaty, log, Message, Room, Sayable } from 'wechaty' import { delay, logger } from '../utils/utils.js' import { ChatFlowConfig } from '../api/base-config.js' -import { initializeServicesAndEnv } from '../proxy/initializeServicesAndEnv.js' import { + MessageChat, EnvChat, - KeywordChat, WhiteListChat, + GroupNoticeChat, + ActivityChat, NoticeChat, + QaChat, } from '../services/mod.js' import { logForm } from '../utils/mod.js' import { getRoom, } from '../plugins/mod.js' - import { onMessage } from './on-message.js' +import { + ServeGetUserConfigGroup, + ServeUpdateConfig, +} from '../api/user.js' +import { + ServeGetWhitelistWhiteObject, +} from '../api/white-list.js' +import { ServeGetChatbotUsersDetail } from '../api/chatbot.js' +import { ServeGetWelcomes } from '../api/welcome.js' + export const handleSay = async (talker: Room | Contact | Message, sayable: Sayable) => { const message: Message | void = await talker.say(sayable) if (message) await onMessage(message) } +// 处理管理员群消息 const notifyAdminRoom = async (bot: Wechaty) => { // log.info('notifyAdminRoom,初始化vika配置信息', bot) if (ChatFlowConfig.configEnv.ADMINROOM_ADMINROOMID || ChatFlowConfig.configEnv.ADMINROOM_ADMINROOMTOPIC) { const adminRoom = await getRoom(bot, { topic: ChatFlowConfig.configEnv.ADMINROOM_ADMINROOMTOPIC, id: ChatFlowConfig.configEnv.ADMINROOM_ADMINROOMID }) - const helpText = await KeywordChat.getSystemKeywordsText() + const helpText = await ChatFlowConfig.getSystemKeywordsText() const text = `${new Date().toLocaleString()}\nchatflow启动成功!\n当前登录用户${bot.currentUser.name()}\n可在管理员群回复对应指令进行操作\n${helpText}\n` - if (adminRoom) await handleSay(adminRoom, text) + if (adminRoom) { + ChatFlowConfig.adminRoom = adminRoom + await handleSay(adminRoom, text) + } // await adminRoom?.say(text) } } +// 从云端加载配置信息 const postVikaInitialization = async (bot: Wechaty) => { // log.info('初始化vika配置信息:', bot) try { log.info('开始请求维格表中的环境变量配置信息...') - const vikaConfig = await EnvChat.getConfigFromVika() + const res = await EnvChat.getConfigFromVika() + logger.info('ServeGetUserConfig res:' + JSON.stringify(res)) + + const vikaConfig:any = res.data + // logger.info('获取的维格表中的环境变量配置信息vikaConfig:' + JSON.stringify(vikaConfig)) // 合并配置信息,如果维格表中有对应配置则覆盖环境变量中的配置 ChatFlowConfig.configEnv = { ...vikaConfig, ...(ChatFlowConfig.configEnv) } logger.info('合并后的环境变量信息:' + JSON.stringify(ChatFlowConfig.configEnv)) + // 下载进群欢迎语 + try { + const welcomes = await ServeGetWelcomes() + ChatFlowConfig.welcomeList = welcomes.data.items + logger.info('获取的进群欢迎语:' + JSON.stringify(ChatFlowConfig.welcomeList)) + } catch (err) { + log.error('获取进群欢迎语失败', err) + } + try { log.info('开始请求获取白名单...') - ChatFlowConfig.whiteList = await WhiteListChat.getWhiteList() - // await (chatflowConfig.services as Services).roomService.updateRooms(bot, configEnv.WECHATY_PUPPET) - // await (chatflowConfig.services as Services).contactService.updateContacts(bot, configEnv.WECHATY_PUPPET) - - // 每30s上报一次心跳 - // setInterval(() => { - // const curDate = new Date().toLocaleString() - // logger.info('当前时间:' + curDate) - // try { - // if (mqttProxy.isOk && configEnv.MQTT_MQTTMESSAGEPUSH) { - // mqttProxy.pubProperty(propertyMessage('lastActive', curDate)) - // } - // } catch (err) { - // logger.error('发送心跳失败:', err) - // } - // }, 300000) + // 获取白名单列表 + const listRes = await ServeGetWhitelistWhiteObject() + ChatFlowConfig.whiteList = listRes.data + logger.info('获取白名单成功...' + JSON.stringify(ChatFlowConfig.whiteList)) try { + // 更新定时任务 await NoticeChat.updateJobs() - const helpText = await KeywordChat.getSystemKeywordsText() + // 获取关键字列表 + const helpText = await ChatFlowConfig.getSystemKeywordsText() logForm(`启动成功,系统准备就绪\n\n当前登录用户:${bot.currentUser.name()}\nID:${bot.currentUser.id}\n\n在当前群(管理员群)回复对应指令进行操作\n${helpText}`) } catch (err) { log.error('获取帮助文案失败', err) @@ -80,58 +99,172 @@ const postVikaInitialization = async (bot: Wechaty) => { } try { + // 向管理员群推送消息 await notifyAdminRoom(bot) log.info('向管理群推送消息成功...') - + const ChatFlowConfigInfo = { + configEnv: ChatFlowConfig.configEnv, + whiteList: ChatFlowConfig.whiteList, + chatBotUsers: ChatFlowConfig.chatBotUsers, + } + logger.info('ChatFlowConfigInfo配置信息:' + JSON.stringify(ChatFlowConfigInfo)) } catch (err) { log.error('向管理群推送消息失败...', err) } } +// bot就绪或登录事件操作 export const onReadyOrLogin = async (bot: Wechaty) => { - log.info('onReadyOrLogin,初始化services服务...') const curTime = new Date().getTime() const user: Contact = bot.currentUser log.info('当前登录的账号信息:', user.name()) - await initializeServicesAndEnv() - await delay(500) + // 初始化服务 + try { + log.info('onReadyOrLogin,初始化services服务...') + logger.info('初始化服务开始...') + + // 消息上传服务初始化 + await MessageChat.init() + await delay(500) - const resBotId = await EnvChat.findByField('key', 'BASE_BOT_ID') - // log.info('当前云端配置的BASE_BOT_ID:', JSON.stringify(res)) - const BASE_BOT_ID:any = resBotId[0] - await delay(500) + // 活动管理服务初始化 + await ActivityChat.init() + await delay(500) - BASE_BOT_ID.fields.value = user.id - BASE_BOT_ID.fields.lastOperationTime = curTime - BASE_BOT_ID.fields.syncStatus = '已同步' - await EnvChat.update(BASE_BOT_ID.recordId, BASE_BOT_ID.fields) - await delay(500) + // 群通知管理服务初始化 + await GroupNoticeChat.init() + await delay(500) - const resBotName = await EnvChat.findByField('key', 'BASE_BOT_NAME') - // log.info('当前云端配置的BASE_BOT_NAME:', JSON.stringify(resBotName)) - if (resBotName.length > 0) { - const BASE_BOT_NAME:any = resBotName[0] + // 定时任务服务初始化 + await NoticeChat.init() await delay(500) - BASE_BOT_NAME.fields.value = user.name() - BASE_BOT_NAME.fields.lastOperationTime = curTime - BASE_BOT_NAME.fields.syncStatus = '已同步' - await EnvChat.update(BASE_BOT_NAME.recordId, BASE_BOT_NAME.fields) - } else { - const fields = { - value: user.name(), - lastOperationTime: curTime, - syncStatus: '已同步', - name:'基础配置-机器人微信昵称', - key:'BASE_BOT_NAME', - desc:'机器人微信昵称,登录成功后自动更新', + // 白名单服务初始化 + await WhiteListChat.init() + await delay(500) + + // 智能问答服务初始化 + await QaChat.init() + // logger.info('services:' + JSON.stringify(services)) + ChatFlowConfig.isReady = true + log.info('初始化服务完成...') + await delay(500) + } catch (err) { + log.error('初始化服务失败', err) + } + + // 更新当前登录的bot信息到云端 + let baseInfo = [] + const resConfig:any = await ServeGetUserConfigGroup() + const configGgroup = resConfig.data + const baseConfig:{ + id?:string; + name:string; + value:any; + key:string; + lastOperationTime:number; + syncStatus:string; + }[] = configGgroup['基础配置'] + + logger.info('获取的基础配置信息:' + JSON.stringify(baseConfig)) + + // 从baseConfig中找出key为BASE_BOT_ID的配置项,更新value为当前登录的bot的id + const BASE_BOT_ID = baseConfig.find((item) => item.key === 'BASE_BOT_ID') + + if (BASE_BOT_ID) { + BASE_BOT_ID.name = `基础配置-${BASE_BOT_ID.name}` + BASE_BOT_ID.value = user.id + BASE_BOT_ID.lastOperationTime = curTime + BASE_BOT_ID.syncStatus = '已同步' + baseInfo.push(BASE_BOT_ID) + } + + const BASE_BOT_NAME = baseConfig.find((item) => item.key === 'BASE_BOT_NAME') + if (BASE_BOT_NAME) { + BASE_BOT_NAME.name = `基础配置-${BASE_BOT_NAME.name}` + BASE_BOT_NAME.value = user.name() + BASE_BOT_NAME.lastOperationTime = curTime + BASE_BOT_NAME.syncStatus = '已同步' + baseInfo.push(BASE_BOT_NAME) + } + + const ADMINROOM_ADMINROOMTOPIC = baseConfig.find((item) => item.key === 'ADMINROOM_ADMINROOMTOPIC') + const topic = ChatFlowConfig.adminRoomTopic || '' + if (ADMINROOM_ADMINROOMTOPIC && topic) { + ADMINROOM_ADMINROOMTOPIC.name = `基础配置-${ADMINROOM_ADMINROOMTOPIC.name}` + ADMINROOM_ADMINROOMTOPIC.value = topic + ADMINROOM_ADMINROOMTOPIC.lastOperationTime = curTime + ADMINROOM_ADMINROOMTOPIC.syncStatus = '已同步' + baseInfo.push(ADMINROOM_ADMINROOMTOPIC) + + const adminRoom = await bot.Room.find({ topic }) + if (adminRoom) { + const ADMINROOM_ADMINROOMID = baseConfig.find((item) => item.key === 'ADMINROOM_ADMINROOMID') + if (ADMINROOM_ADMINROOMID) { + ADMINROOM_ADMINROOMID.name = `基础配置-${ADMINROOM_ADMINROOMID.name}` + ADMINROOM_ADMINROOMID.value = adminRoom.id + ADMINROOM_ADMINROOMID.lastOperationTime = curTime + ADMINROOM_ADMINROOMID.syncStatus = '已同步' + baseInfo.push(ADMINROOM_ADMINROOMID) + } } - await EnvChat.create(fields as any) + } + + const wechatyConfig:{ + id?:string; + name:string; + value:any; + key:string; + lastOperationTime:number; + syncStatus:string; + }[] = configGgroup['Wechaty'] + + const WECHATY_PUPPET = wechatyConfig.find((item) => item.key === 'WECHATY_PUPPET') + if (WECHATY_PUPPET) { + WECHATY_PUPPET.name = `Wechaty-${WECHATY_PUPPET.name}` + WECHATY_PUPPET.value = bot.puppet.name() + WECHATY_PUPPET.lastOperationTime = curTime + WECHATY_PUPPET.syncStatus = '已同步' + baseInfo.push(WECHATY_PUPPET) + } + + const WECHATY_TOKEN = wechatyConfig.find((item) => item.key === 'WECHATY_TOKEN') + if (WECHATY_TOKEN) { + WECHATY_TOKEN.name = `Wechaty-${WECHATY_TOKEN.name}` + WECHATY_TOKEN.value = '' + WECHATY_TOKEN.lastOperationTime = curTime + WECHATY_TOKEN.syncStatus = '已同步' + baseInfo.push(WECHATY_TOKEN) + } + + if (baseInfo.length > 0) { + baseInfo = baseInfo.map((item) => { + const raw:any = { + recordId:'', + fields: {}, + } + raw.recordId = item.id + delete item.id + raw.fields = item + return raw + }) + logger.info('更新基础配置信息:' + JSON.stringify(baseInfo)) + await ServeUpdateConfig(baseInfo) + await delay(500) + } + + try { + const chatBotUsers = await ServeGetChatbotUsersDetail() + ChatFlowConfig.chatBotUsers = chatBotUsers.data.items + logger.info('获取chatBotUsers:' + JSON.stringify(ChatFlowConfig.chatBotUsers)) + } catch (err) { + log.error('获取chatBotUsers失败', err) } try { log.info('调用postVikaInitialization...') + // 初始化vika配置信息 await postVikaInitialization(bot) } catch (err) { log.error('postVikaInitialization(bot) error', err) diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..a0368d12 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,143 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/* eslint-disable sort-keys */ +import 'dotenv/config.js' +import { WechatyPlugin, Wechaty, log } from 'wechaty' +import onScan from './handlers/on-scan.js' +import onError from './handlers/on-error.js' +import onRoomjoin from './handlers/on-roomjoin.js' +import onLogout from './handlers/on-logout.js' +import onLogin from './handlers/on-login.js' +import onReady from './handlers/on-ready.js' +import onMessage from './handlers/on-message.js' +import { getBotOps } from './services/configService.js' +import delay, { logForm, logger } from './utils/utils.js' +import { ChatFlowConfig, WechatyConfig } from './api/base-config.js' +import { MqttProxy, IClientOptions } from './proxy/mqtt-proxy.js' +import { BiTable } from './db/lark-db.js' + +import getAuthClient from './utils/auth.js' +import { GroupMaster, GroupMasterConfig } from './plugins/mod.js' + +function ChatFlow (options?:{ + spaceId: string + token: string + adminRoomTopic?: string + endpoint?: string +}): WechatyPlugin { + logForm('ChatFlow插件开始启动...\n\n启动过程需要30秒到1分钟\n\n请等待系统初始化...') + + return function ChatFlowPlugin (bot: Wechaty): void { + + ChatFlowConfig.bot = bot + ChatFlowConfig.spaceId = options?.spaceId || '' + ChatFlowConfig.token = options?.token || '' + ChatFlowConfig.adminRoomTopic = options?.adminRoomTopic || '' + ChatFlowConfig.endpoint = options?.endpoint || '' + + bot.on('scan', onScan) + bot.on('login', onLogin) + bot.on('ready', onReady) + bot.on('logout', onLogout) + bot.on('message', onMessage) + bot.on('room-join', onRoomjoin) + bot.on('error', onError) + + } + +} + +const init = async (options:{ + spaceId: string + token: string, + endpoint?: string, +}) => { + ChatFlowConfig.setOptions(options) + // 远程加载配置信息,初始化api客户端 + try { + const authClient = getAuthClient({ + password: options.token, + username: options.spaceId, + endpoint: options.endpoint || '', + }) + try { + // 初始化检查数据库表,如果不存在则创建 + const initRes = await authClient.init(options.spaceId, options.token) + logForm('初始化检查系统表结果:' + JSON.stringify(initRes.data)) + logger.info('初始化检查系统表结果:' + JSON.stringify(initRes.data)) + + if (initRes.data && initRes.data.message === 'success') { + logForm('初始化检查系统表成功...') + ChatFlowConfig.db = initRes.data.data + } else { + logForm('初始化检查系统表失败...' + JSON.stringify(initRes.data)) + // 中止程序 + // throw new Error('初始化检查系统表失败...', initRes) + process.exit(1) + } + } catch (e) { + log.error('请求初始化检查系统表失败...', e) + // throw e + process.exit(1) + } + await delay(1000) + try { + const loginRes = await authClient.login(options.spaceId, options.token) + logForm('登录客户端结果:' + JSON.stringify(loginRes)) + ChatFlowConfig.isLogin = true + } catch (e) { + log.error('登录客户端失败...', e) + throw e + } + } catch (e) { + log.error('登录客户端失败...', e) + } + + // 从配置文件中读取配置信息,包括wechaty配置、mqtt配置以及是否启用mqtt推送或控制 + try { + const configAll = await ChatFlowConfig.init(options) + // log.info('configAll', JSON.stringify(configAll)) + + const config: { + mqttConfig: IClientOptions, + wechatyConfig: WechatyConfig, + mqttIsOn: boolean, + } | undefined = configAll // 默认使用vika,使用lark时,需要传入'lark'参数await ChatFlowConfig.init('lark') + // log.info('config', JSON.stringify(config, undefined, 2)) + + // 构建机器人 + // 如果MQTT推送或MQTT控制打开,则启动MQTT代理 + if (config.mqttIsOn) { + log.info('启动MQTT代理...', JSON.stringify(config.mqttConfig)) + try { + const mqttProxy = MqttProxy.getInstance(config.mqttConfig) + if (mqttProxy) { + mqttProxy.setWechaty(ChatFlowConfig.bot) + } + } catch (e) { + log.error('MQTT代理启动失败,检查mqtt配置信息是否正确...', e) + } + } + } catch (e) { + log.error('初始化ChatFlowConfig失败...', e) + } + +} + +export { + init, + getBotOps, + ChatFlowConfig, + log, + logForm, + BiTable, + type IClientOptions, + MqttProxy, + ChatFlow, + GroupMaster, +} + +export type { + GroupMasterConfig, +} + +export type { WechatyConfig } diff --git a/src/mod.ts b/src/mod.ts index 993e789f..29428d75 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -3,22 +3,20 @@ import { ChatFlowConfig, log, logForm, - VikaDB, - LarkDB, + BiTable, type IClientOptions, MqttProxy, ChatFlow, GroupMaster, GroupMasterConfig, -} from './chatflow.js' +} from './index.js' export { getBotOps, ChatFlowConfig, log, logForm, - VikaDB, - LarkDB, + BiTable, type IClientOptions, MqttProxy, ChatFlow, diff --git a/src/plugins/connectors/wx-openai.ts b/src/plugins/connectors/wx-openai.ts index c9296c5c..c09fc950 100644 --- a/src/plugins/connectors/wx-openai.ts +++ b/src/plugins/connectors/wx-openai.ts @@ -216,7 +216,6 @@ async function aibot (sysConfig: any, talker: any, room: any, query: any) { // console.debug(resMsg) log.info('对话返回原始:', resMsg) // log.info('对话返回:', JSON.stringify(resMsg).replace(/[\r\n]/g, "").replace(/\ +/g, "")) - log.info('回答内容:', resMsg.msgtype, resMsg.query, resMsg.answer) // console.debug(resMsg.query) // console.debug(resMsg.answer) diff --git a/src/plugins/mod.ts b/src/plugins/mod.ts index e1c037e1..35465482 100644 --- a/src/plugins/mod.ts +++ b/src/plugins/mod.ts @@ -4,7 +4,7 @@ import { wxai } from '../proxy/weixin-chatbot-proxy.js' import { sendNotice } from '../app/group-notice.js' import { MqttProxy } from '../proxy/mqtt-proxy.js' -import { getFormattedRideInfo } from '../app/riding.js' +import { getFormattedRideInfo } from '../app/carpooling.js' import { exportContactsAndRoomsToCSV, diff --git a/src/proxy/chatgpt-proxy.ts b/src/proxy/chatgpt-proxy.ts index ceaba543..48ea1737 100644 --- a/src/proxy/chatgpt-proxy.ts +++ b/src/proxy/chatgpt-proxy.ts @@ -7,7 +7,7 @@ import { } from 'wechaty' import { formatSentMessage, logger } from '../utils/utils.js' import axios from 'axios' -import { ChatFlowConfig } from '../chatflow.js' +import { ChatFlowConfig } from '../index.js' axios.defaults.timeout = 60000 async function gpt (bot: Wechaty, message: Message) { diff --git a/src/proxy/ernie-proxy.ts b/src/proxy/ernie-proxy.ts index 90bb6d90..0b5ce6c7 100644 --- a/src/proxy/ernie-proxy.ts +++ b/src/proxy/ernie-proxy.ts @@ -9,7 +9,7 @@ import { import { formatSentMessage, logger } from '../utils/utils.js' import type { ProcessEnv } from '../types/mod.js' import axios from 'axios' -import { ChatFlowConfig } from '../chatflow.js' +import { ChatFlowConfig } from '../index.js' /** * 使用 AK,SK 生成鉴权签名(Access Token) diff --git a/src/proxy/initializeServicesAndEnv.ts b/src/proxy/initializeServicesAndEnv.ts deleted file mode 100644 index 31717062..00000000 --- a/src/proxy/initializeServicesAndEnv.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { logger, delay } from '../utils/mod.js' -import { - MessageChat, - EnvChat, - WhiteListChat, - GroupNoticeChat, - RoomChat, - ContactChat, - ActivityChat, - NoticeChat, - QaChat, - KeywordChat, - LarkChat, -} from '../services/mod.js' -import { ChatFlowConfig } from '../chatflow.js' - -export const initializeServicesAndEnv = async () => { - logger.info('初始化服务开始...') - await EnvChat.init() - await delay(500) - if (ChatFlowConfig.dataBaseType === 'lark') { - await LarkChat.init() - await delay(500) - } else { - await MessageChat.init() - await delay(500) - } - await ActivityChat.init() - await delay(500) - await ContactChat.init() - await delay(500) - await GroupNoticeChat.init() - await delay(500) - await KeywordChat.init() - await delay(500) - await NoticeChat.init() - await delay(500) - await RoomChat.init() - await delay(500) - await WhiteListChat.init() - await delay(500) - await QaChat.init() - // logger.info('services:' + JSON.stringify(services)) -} diff --git a/src/proxy/s3-proxy.ts b/src/proxy/s3-proxy.ts index 828593a5..9a0ba3bd 100644 --- a/src/proxy/s3-proxy.ts +++ b/src/proxy/s3-proxy.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import { log, Message, types } from 'wechaty' import { Client } from 'minio' -import { ChatFlowConfig } from '../chatflow.js' +import { ChatFlowConfig } from '../index.js' function upload (file_payload: { cloudPath?: any; fileContent?: any }) { // Instantiate the minio client with the endpoint diff --git a/src/proxy/weixin-chatbot-proxy.ts b/src/proxy/weixin-chatbot-proxy.ts index fe4ef6d4..be23ad50 100644 --- a/src/proxy/weixin-chatbot-proxy.ts +++ b/src/proxy/weixin-chatbot-proxy.ts @@ -135,24 +135,21 @@ async function aibot (sysConfig: ProcessEnv, talker: any, room: any, query: any) // console.log('error', e) // }) - async function wxOpenAiRoutine (openai: any) { - logger.info('开始请求微信对话平台...') - try { - const queryData = prepareWxOpenAiParams(room, topic, nickName, wxid, roomid, query) - - const resMsg:any = await openai.chat(queryData) - - logger.info(`对话平台返回内容: ${JSON.stringify(resMsg)}`) - logger.info(`回答内容: ${resMsg.msgtype}, ${resMsg.query}, ${resMsg.answer}`) - return handleWxOpenAiResponse(resMsg) - } catch (err) { - logger.error(`请求微信对话平台错误: ${err}`) - return {} - } + logger.info('开始请求微信对话平台...') + try { + const queryData = prepareWxOpenAiParams(room, topic, nickName, wxid, roomid, query) + + const resMsg:any = await openai.chat(queryData) + + logger.info(`对话平台返回内容: ${JSON.stringify(resMsg)}`) + log.info(`对话平台返回内容: ${JSON.stringify(resMsg)}`) + log.info(`回答内容: ${resMsg.msgtype}, ${resMsg.title || 'NO_MATCH'}, ${resMsg.msg[0].content || 'NO_MATCH'}`) + answer = handleWxOpenAiResponse(resMsg) + } catch (err) { + logger.error(`请求微信对话平台错误: ${err}`) + answer = {} } - answer = await wxOpenAiRoutine(openai) - return answer } @@ -169,41 +166,49 @@ function prepareWxOpenAiParams (room:Room|undefined, topic:string, nickName:stri function handleWxOpenAiResponse (resMsg: any) { let answer = {} // 置信度大于0.8时回复,低于0.8时不回复 - if (resMsg.msgtype && resMsg.confidence > 0.8) { - answer = prepareAnswerBasedOnMsgType(resMsg) - } - return answer -} - -function prepareAnswerBasedOnMsgType (resMsg: any) { - let answer = {} - switch (resMsg.msgtype) { - case 'text': - answer = { - messageType: types.Message.Text, - text: resMsg.answer || resMsg.msg[0].content, + if (resMsg.msgtype) { + switch (resMsg.msgtype) { + case 'text':{ + let text = resMsg.msg[0].content + if (resMsg.msg[0].ans_node_name === 'NO_MATCH') { + text = 'hi,我还没有掌握这个问题...' + } + + if (resMsg.msg[0].ans_node_name === '问题推荐') { + const options = resMsg.msg[0].options + options.forEach((option: any) => { + text += `\n${option.title}` + }) + } + + answer = { + messageType: types.Message.Text, + text, + } + break } - break - case 'miniprogrampage':{ - const answerJsonMini = JSON.parse(resMsg.answer) - answer = { - messageType: types.Message.MiniProgram, - text: answerJsonMini.miniprogrampage, + case 'miniprogrampage':{ + const answerJsonMini = JSON.parse(resMsg.answer) + answer = { + messageType: types.Message.MiniProgram, + text: answerJsonMini.miniprogrampage, + } + break } - break - } - case 'image':{ - const answerJsonImage = JSON.parse(resMsg.answer) - answer = { - messageType: types.Message.Image, - text: answerJsonImage.image, + case 'image':{ + const answerJsonImage = JSON.parse(resMsg.answer) + answer = { + messageType: types.Message.Image, + text: answerJsonImage.image, + } + break } - break + // Add other cases here as needed + default: + logger.info(JSON.stringify({ msg: '没有命中关键字' })) } - // Add other cases here as needed - default: - logger.info(JSON.stringify({ msg: '没有命中关键字' })) } + log.info('回答内容:', JSON.stringify(answer)) return answer } diff --git a/src/services/activityService.ts b/src/services/activityService.ts index bbf23309..cdb815c2 100644 --- a/src/services/activityService.ts +++ b/src/services/activityService.ts @@ -3,9 +3,12 @@ import { Room, Contact, Message, Wechaty, log } from 'wechaty' import { v4 } from 'uuid' import { formatTimestamp, getCurTime, logger } from '../utils/utils.js' import moment from 'moment' -import { VikaSheet } from '../db/vika.js' -import { VikaDB } from '../db/vika-db.js' import { ChatFlowConfig } from '../api/base-config.js' +import { + ServeGetStatistics, + // ServeCreateStatistics, +} from '../api/statistic.js' +import { ServeCreateOrders } from '../api/order.js' import { db } from '../db/tables.js' const activityData = db.activity @@ -45,16 +48,6 @@ export interface Order { act_decs: string; } -export function transformKeys (obj: Record): Record { - const transformedObj: Record = {} - for (const key in obj) { - const newKey = key.split('|')[1] as string // Extract the part after the slash - transformedObj[newKey] = obj[key] - - } - return transformedObj -} - // 比较两个数组差异 function findArrayDifferences (vika: Activity[], db: Activity[]): { addArray: Activity[], removeArray: Activity[] } { const removeArray: Activity[] = [] @@ -81,7 +74,6 @@ function findArrayDifferences (vika: Activity[], db: Activity[]): { addArray: Ac export class ActivityChat { static activities: Activity[] = [] - static db:VikaSheet static bot:Wechaty private constructor () { @@ -89,31 +81,22 @@ export class ActivityChat { // 初始化 static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.statisticSheet) await this.getStatistics() this.bot = ChatFlowConfig.bot - log.info('初始化 ActivityChat 成功...') } - // 获取维格表中的活动 - static async getAct () { - const records = await this.db.findAll() - // logger.info(JSON.stringify(this.chatflowConfig.dataBaseIds, undefined, 2)) - // logger.info('维格表中的活动记录:' + JSON.stringify(records)) - return records - } - - // 获取统计打卡 + // 获取统计打卡,从云端获取活动列表更新本地活动列表 static async getStatistics () { - const statisticsRecords = await this.getAct() + const statisticsRes = await ServeGetStatistics() + + // log.info('维格表中的统计:' + JSON.stringify(statisticsRes)) + const statisticsRecords = statisticsRes.data.items const activitiesVika: Activity[] = [] - for (const statistics of statisticsRecords) { - const fields = statistics.fields - const fieldsNew: any = transformKeys(fields) - if (fieldsNew._id && fieldsNew.type && fieldsNew.desc && (fieldsNew.topic || fieldsNew.roomid)) { - const activity: Activity = fieldsNew as Activity + for (const fields of statisticsRecords) { + if (fields._id && fields.type && fields.desc && (fields.topic || fields.roomid)) { + const activity: Activity = fields as Activity activity.active = activity.active === '开启' activity._id = String(activity._id) activity.shortCode = String(activity._id) @@ -167,27 +150,32 @@ export class ActivityChat { const curTime = getCurTime() const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') + const records = [ { fields: { - 编号: String(curTime), - 所属活动: 'system', - 昵称: talker.name(), - 好友备注: await talker.alias() || '', - 群: await room?.topic() || '', - 创建时间: timeHms, + serialNumber: curTime, + code: '4', + desc: '测试打卡', + name: talker.name(), + alias: await talker.alias() || '', + wxid: talker.id, + topic: await room?.topic() || '', + createdAt: timeHms, + info: '--', }, }, ] logger.info('订单消息:' + records) - const datasheet = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.orderSheet) - datasheet.records.create(records).then((response: { success: any }) => { - if (!response.success) { - logger.error('创建订单,写入vika失败:' + JSON.stringify(response)) - } - return response - }).catch((err: any) => { logger.error('创建订单,vika写入接口失败:', err) }) + + try { + const res = await ServeCreateOrders(records) + return res + } catch (err) { + logger.error('创建订单,vika写入接口失败:', err) + return err + } } static getHelpText () { diff --git a/src/services/contactService.ts b/src/services/contactService.ts deleted file mode 100644 index 6bc2d362..00000000 --- a/src/services/contactService.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* eslint-disable sort-keys */ -import { VikaSheet, IRecord } from '../db/vika.js' -import { Contact, Wechaty, log } from 'wechaty' -import { delay, logger } from '../utils/utils.js' -import { VikaDB } from '../db/vika-db.js' -import { BaseEntity, MappingOptions } from '../db/vika-orm.js' - -import { ChatFlowConfig } from '../api/base-config.js' - -// import { db } from '../db/tables.js' -// const contactData = db.contact -// logger.info(JSON.stringify(contactData)) - -const mappingOptions: MappingOptions = { // 定义字段映射选项 - fieldMapping: { // 字段映射 - alias: '备注名称|alias', - id: '好友ID|id', - name: '好友昵称|name', - gender: '性别|gender', - updated: '更新时间|updated', - friend: '是否好友|friend', - type: '类型|type', - avatar: '头像|avatar', - phone: '手机号|phone', - file: '头像图片|file', - - }, - tableName: '好友列表|Contact', // 表名 -} - -// 服务类 -export class ContactChat extends BaseEntity { - - static db:VikaSheet - static bot:Wechaty - - name?: string // 定义名字属性,可选 - - id?: string - - alias?: string - - gender?: string - - updated?: string - - friend?: string - - type?: string - - avatar?: string - - phone?: string - - file?: string - - // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 - - protected static override mappingOptions: MappingOptions = mappingOptions // 设置映射选项为上面定义的 mappingOptions - - protected static override getMappingOptions (): MappingOptions { // 获取映射选项的方法 - return this.mappingOptions // 返回当前类的映射选项 - } - - static override setMappingOptions (options: MappingOptions) { // 设置映射选项的方法 - this.mappingOptions = options // 更新当前类的映射选项 - } - - // 初始化 - static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.contactSheet) - ContactChat.setVikaOptions({ - apiKey: VikaDB.token, - baseId: VikaDB.dataBaseIds.contactSheet, // 设置 base ID - }) - await this.getContact() - this.bot = ChatFlowConfig.bot - log.info('初始化 ContactChat 成功...') - } - - static async getContact () { - const records:IRecord[] = await this.db.findAll() - // logger.info('维格表中的记录:' + JSON.stringify(records)) - return records - } - - // 上传联系人列表 - static async updateContacts (puppet:string) { - let updateCount = 0 - try { - const contacts: Contact[] = await this.bot.Contact.findAll() - log.info('最新联系人数量(包含公众号):', contacts.length) - logger.info('最新联系人数量(包含公众号):' + contacts.length) - const recordsAll: any = [] - const recordExisting = await this.db.findAll() - log.info('云端好友数量(不包含公众号):', recordExisting.length || '0') - logger.info('云端好友数量(不包含公众号):' + recordExisting.length || '0') - - let wxids: string[] = [] - const recordIds: string[] = [] - if (recordExisting.length) { - recordExisting.forEach((record: IRecord) => { - wxids.push(record.fields['好友ID|id'] as string) - recordIds.push(record.recordId) - }) - } - logger.info('当前bot使用的puppet:' + puppet) - log.info('当前bot使用的puppet:', puppet) - - if (puppet === 'wechaty-puppet-wechat' || puppet === 'wechaty-puppet-wechat4u') { - const count = Math.ceil(recordIds.length / 10) - for (let i = 0; i < count; i++) { - const records = recordIds.splice(0, 10) - log.info('删除:', records.length) - await this.db.remove(records) - await delay(1000) - } - wxids = [] - } - for (let i = 0; i < contacts.length; i++) { - const item = contacts[i] - const isFriend = item?.friend() || false - // const isIndividual = item?.type() === types.Contact.Individual - // logger.info('好友详情:' + item?.name()) - // log.info('是否好友:' + isFriend) - // logger.info('是否公众号:' + isIndividual) - // if(item) log.info('头像信息:', (JSON.stringify((await item?.avatar()).toJSON()))) - if (item && isFriend && !wxids.includes(item.id)) { - // logger.info('云端不存在:' + item.name()) - let avatar:any = '' - let alias = '' - try { - avatar = (await item.avatar()).toJSON() - avatar = avatar.url - } catch (err) { - // logger.error('获取好友头像失败:'+ err) - } - try { - alias = await item.alias() || '' - } catch (err) { - logger.error('获取好友备注失败:' + err) - } - const fields = { - '备注名称|alias':alias, - '头像|avatar':avatar, - '是否好友|friend': item.friend(), - '性别|gender': String(item.gender() || ''), - '更新时间|updated': new Date().toLocaleString(), - '好友ID|id': item.id, - '好友昵称|name': item.name(), - '手机号|phone': String(await item.phone()), - '类型|type': String(item.type()), - } - const record = { - fields, - } - recordsAll.push(record) - } - } - logger.info('好友数量:' + recordsAll.length || '0') - for (let i = 0; i < recordsAll.length; i = i + 10) { - const records = recordsAll.slice(i, i + 10) - await this.db.insert(records) - logger.info('好友列表同步中...' + i + records.length) - updateCount = updateCount + records.length - void await delay(1000) - } - - log.info('同步好友列表完成,更新到云端好友数量:' + updateCount || '0') - } catch (err) { - log.error('更新好友列表失败:' + err) - - } - - } - -} diff --git a/src/services/envService.ts b/src/services/envService.ts index 8ed8e173..8dfa8d24 100644 --- a/src/services/envService.ts +++ b/src/services/envService.ts @@ -1,109 +1,35 @@ /* eslint-disable sort-keys */ -import { VikaSheet, IRecord } from '../db/vika.js' +// import type { IRecord } from '../db/vika.js' import { log } from 'wechaty' -import { delay } from '../utils/utils.js' -import type { ProcessEnv } from '../types/env.js' -import { BaseEntity, MappingOptions } from '../db/vika-orm.js' -import { VikaDB } from '../db/vika-db.js' +import { logger } from '../utils/utils.js' +import { + ServeGetUserConfigObj, +} from '../api/user.js' // import { db } from '../db/tables.js' // const envData = db.env // log.info(JSON.stringify(envData)) -const mappingOptions: MappingOptions = { // 定义字段映射选项 - fieldMapping: { // 字段映射 - name: '配置项|name', - key: '标识|key', - value: '值|value', - desc: '说明|desc', - syncStatus: '同步状态|syncStatus', - lastOperationTime: '最后操作时间|lastOperationTime', - action: '操作|action', - }, - tableName: '环境变量|Env', // 表名 -} - // 服务类 -export class EnvChat extends BaseEntity { - - static db:VikaSheet - static envIdMap: any - static records: IRecord[] - static envData: ProcessEnv | undefined - static vikaIdMap: any - static vikaData: any - - protected static override mappingOptions: MappingOptions = mappingOptions // 设置映射选项为上面定义的 mappingOptions - - protected static override getMappingOptions (): MappingOptions { // 获取映射选项的方法 - return this.mappingOptions // 返回当前类的映射选项 - } - - static override setMappingOptions (options: MappingOptions) { // 设置映射选项的方法 - this.mappingOptions = options // 更新当前类的映射选项 - } - - // 初始化 - static async init () { - const token = VikaDB.token || '' - const baseId = VikaDB.dataBaseIds.envSheet - this.db = new VikaSheet(VikaDB.vika, baseId) - EnvChat.setVikaOptions({ - apiKey: token, - baseId: VikaDB.dataBaseIds.envSheet, // 设置 base ID - }) - EnvChat.getConfigFromEnv() - this.records = await this.getAll() - } - - static async getAll () { - const records = await this.db.findAll() - // log.info('维格表中的记录:', JSON.stringify(records)) - return records - } +export class EnvChat { // 更新环境变量配置到云端 static async updateConfigToVika (config: any) { log.info('当前环境变量:', config) - } - - // 下载环境变量配置 - static async downConfigFromVika () { - return await this.getConfigFromVika() + // return await ServeUpdateConfig(config) } // 从维格表中获取环境变量配置 static async getConfigFromVika () { log.info('从维格表中获取环境变量配置,getConfigFromVika ()') - const vikaIdMap: any = {} - const vikaData: any = {} - - const configRecords = await EnvChat.getAll() - - await delay(1000) - // log.info(configRecords) - - for (let i = 0; i < configRecords.length; i++) { - const record: IRecord = configRecords[i] as IRecord - const fields = record.fields - const recordId = record.recordId - - if (fields['标识|key']) { - if (fields['值|value'] && [ 'false', 'true' ].includes(fields['值|value'])) { - vikaData[record.fields['标识|key'] as string] = fields['值|value'] === 'true' - } else { - vikaData[record.fields['标识|key'] as string] = fields['值|value'] || '' - } - vikaIdMap[record.fields['标识|key'] as string] = recordId - } - } - - this.vikaIdMap = vikaIdMap - this.vikaData = vikaData + const res: any = await ServeGetUserConfigObj() + logger.info('从维格表中获取环境变量配置,ServeGetUserConfigObj res:' + JSON.stringify(res)) + const vikaData = res.data + logger.info('vikaData:' + JSON.stringify(vikaData)) // log.info('sysConfig:', JSON.stringify(sysConfig, null, '\t')) - return this.vikaData + return vikaData } // 从环境变量中获取环境变量配置 @@ -125,76 +51,9 @@ export class EnvChat extends BaseEntity { } } - this.envData = envData - // log.info('sysConfig:', JSON.stringify(sysConfig, null, '\t')) return envData } - // 将环境变量更新到云 - async updateCloud (config: { [x: string]: string }) { - const newData: { - recordId: string; - fields: { - [key: string]: any; - }; - }[] = [] - for (const key in config) { - if (Object.prototype.hasOwnProperty.call(config, key)) { - if (process.env[key]) { - config[key] = process.env[key]! - const fields:{ - [key: string]: any; - } = { '值|value':process.env[key] } - const item = { recordId:EnvChat.envIdMap[key], fields } - newData.push(item) - } - } - } - - if (newData.length) { - await EnvChat.db.update(newData) - } - - return newData - - } - - // 将云端配置更新到环境变量 - updateEnv (config: { [s: string]: unknown } | ArrayLike) { - for (const [ key, value ] of Object.entries(config)) { - process.env[key] = String(value) - } - - } - - getBotOps () { - const puppet = EnvChat.envData?.WECHATY_PUPPET || 'wechaty-puppet-wechat' - const token = EnvChat.envData?.WECHATY_TOKEN - const ops: any = { - name: 'chatflow', - puppet, - puppetOptions: { - token, - }, - } - - if (puppet === 'wechaty-puppet-service') { - process.env['WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT'] = 'true' - } - - if ([ 'wechaty-puppet-wechat4u', 'wechaty-puppet-xp', 'wechaty-puppet-engine' ].includes(puppet)) { - delete ops.puppetOptions.token - } - - if (puppet === 'wechaty-puppet-wechat') { - delete ops.puppetOptions.token - ops.puppetOptions.uos = true - } - - log.info('Wchaty配置信息:\n', JSON.stringify(ops)) - return ops - } - } diff --git a/src/services/groupNoticeService.ts b/src/services/groupNoticeService.ts index b3e2592f..03b115f7 100644 --- a/src/services/groupNoticeService.ts +++ b/src/services/groupNoticeService.ts @@ -1,12 +1,7 @@ /* eslint-disable sort-keys */ -/* eslint-disable sort-keys */ -import type { TaskConfig, Notifications } from '../api/base-config.js' -import { VikaDB } from '../db/vika-db.js' +import type { Notifications } from '../api/base-config.js' import { ChatFlowConfig } from '../api/base-config.js' - -import { VikaSheet } from '../db/vika.js' import { Wechaty, log } from 'wechaty' -import { transformKeys } from './activityService.js' import type { BusinessRoom, BusinessUser } from '../plugins/mod.js' import { generateRandomNumber, delay, logger } from '../utils/mod.js' import { @@ -14,6 +9,10 @@ import { getRoom, } from '../plugins/mod.js' import { sendMsg } from './configService.js' +import { + ServeGetGroupnotices, + ServeUpdateGroupnotices, +} from '../api/group-notice.js' // import { db } from '../db/tables.js' // const groupNoticeData = db.groupNotice @@ -22,32 +21,19 @@ import { sendMsg } from './configService.js' // 服务类 export class GroupNoticeChat { - static db:VikaSheet - static envsOnVika: any static roomWhiteList: any static contactWhiteList: any - static reminderList: TaskConfig[] = [] static bot:Wechaty - private constructor () { - - } + private constructor () {} // 初始化 static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.groupNoticeSheet) - await this.getRecords() this.bot = ChatFlowConfig.bot log.info('初始化 GroupNoticeChat 成功...') } - static async getRecords () { - const records = await this.db.findAll() - // logger.info('维格表中的记录:', JSON.stringify(records)) - return records - } - // 获取群发通知 static async getGroupNotifications () { // const query = { @@ -56,31 +42,30 @@ export class GroupNoticeChat { // logger.info('query:', JSON.stringify(query)) const groupNotifications: Notifications[] = [] try { - const records = await this.db.findAll() + const statisticsRes = await ServeGetGroupnotices() + const records = statisticsRes.data.items // logger.info('群发通知列表(原始):\n', JSON.stringify(records)) - for (const record of records) { - const fields = record.fields - const recordId = record.recordId - const fieldsNew: Notifications = transformKeys(fields) as Notifications - // logger.info('群发通知列表(格式化key):\n', JSON.stringify(fieldsNew)) - - if (fieldsNew.text && fieldsNew.type && (fieldsNew.id || fieldsNew.alias || fieldsNew.name) && fieldsNew.state === '待发送|waiting') { - fieldsNew.recordId = recordId - fieldsNew.type = fieldsNew.type.split('|')[1] || 'contact' - if (fieldsNew.type === 'room') { - fieldsNew.room = { - id: fieldsNew.id, - topic: fieldsNew.name, + for (const fields of records) { + const recordId = fields.recordId + // logger.info('群发通知列表(格式化key):\n', JSON.stringify(fields)) + + if (fields.text && fields.type && (fields.id || fields.alias || fields.name) && fields.state === '待发送|waiting') { + fields.recordId = recordId + fields.type = fields.type.split('|')[1] || 'contact' + if (fields.type === 'room') { + fields.room = { + id: fields.id, + topic: fields.name, } } else { - fieldsNew.contact = { - id: fieldsNew.id, - name: fieldsNew.name, - alias: fieldsNew.alias, + fields.contact = { + id: fields.id, + name: fields.name, + alias: fields.alias, } } - groupNotifications.push(fieldsNew) + groupNotifications.push(fields) } } logger.info('待发送的群发通知列表(添加接收人):\n', JSON.stringify(groupNotifications)) @@ -110,38 +95,24 @@ export class GroupNoticeChat { try { await sendMsg(room, notice.text) await delay(generateRandomNumber(200)) - resPub.push({ - recordId: notice.recordId, - fields: { - // '昵称/群名称|name':await room.topic(), - // '好友ID/群ID|id':room.id, - '状态|state': '发送成功|success', - '发送时间|pubTime': timestamp, - }, - }) + notice.state = '发送成功|success' + notice.pubTime = timestamp + resPub.push(notice) successRoom.push(notice) logger.info('发送成功:群-', await room.topic()) } catch (e) { - resPub.push({ - recordId: notice.recordId, - fields: { - '状态|state': '发送失败|fail', - '发送时间|pubTime': timestamp, - '信息|info':JSON.stringify(e), - }, - }) + notice.state = '发送失败|fail' + notice.pubTime = timestamp + notice.info = JSON.stringify(e) + resPub.push(notice) failRoom.push(notice) logger.error('发送失败:', e) } } else { - resPub.push({ - recordId: notice.recordId, - fields: { - '状态|state': '发送失败|fail', - '发送时间|pubTime': timestamp, - '信息|info':'群不存在', - }, - }) + notice.state = '发送失败|fail' + notice.pubTime = timestamp + notice.info = '群不存在' + resPub.push(notice) failRoom.push(notice) logger.error('发送失败:群不存在', JSON.stringify(notice)) } @@ -151,39 +122,24 @@ export class GroupNoticeChat { try { await sendMsg(contact, notice.text) await delay(generateRandomNumber(200)) - resPub.push({ - recordId: notice.recordId, - fields: { - // '昵称/群名称|name':contact.name, - // '好友ID/群ID|id':contact.id, - // '好友备注|alias':await contact.alias(), - '状态|state': '发送成功|success', - '发送时间|pubTime': timestamp, - }, - }) + notice.state = '发送成功|success' + notice.pubTime = timestamp + resPub.push(notice) successContact.push(notice) logger.info('发送成功:\n好友-', contact.name()) } catch (e) { - resPub.push({ - recordId: notice.recordId, - fields: { - '状态|state': '发送失败|fail', - '发送时间|pubTime': timestamp, - '信息|info':JSON.stringify(e), - }, - }) + notice.state = '发送失败|fail' + notice.pubTime = timestamp + notice.info = JSON.stringify(e) + resPub.push(notice) failContact.push(notice) logger.error('发送失败:', e) } } else { - resPub.push({ - recordId: notice.recordId, - fields: { - '状态|state': '发送失败|fail', - '发送时间|pubTime': timestamp, - '信息|info':'好友不存在', - }, - }) + notice.state = '发送失败|fail' + notice.pubTime = timestamp + notice.info = '好友不存在' + resPub.push(notice) failContact.push(notice) logger.error('发送失败:好友不存在', JSON.stringify(notice)) } @@ -191,7 +147,7 @@ export class GroupNoticeChat { } for (let i = 0; i < resPub.length; i = i + 10) { const records = resPub.slice(i, i + 10) - await this.db.update(records) + await ServeUpdateGroupnotices(records) logger.info('群发消息同步中...', i + 10) void await delay(1000) } diff --git a/src/services/keywordService.ts b/src/services/keywordService.ts deleted file mode 100644 index d7fce348..00000000 --- a/src/services/keywordService.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable sort-keys */ -import { VikaDB } from '../db/vika-db.js' -import { ChatFlowConfig } from '../api/base-config.js' -import { Wechaty, log } from 'wechaty' - -import { VikaSheet, IRecord } from '../db/vika.js' -import { logger } from '../utils/mod.js' - -// 服务类 -export class KeywordChat { - - static db:VikaSheet - static records: IRecord[] | undefined - static bot:Wechaty - - private constructor () { - - } - - // 初始化 - static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.keywordSheet) - await this.getKeywords() - this.bot = ChatFlowConfig.bot - - log.info('初始化 KeywordChat 成功...') - } - - // 获取关键字 - static async getKeywords () { - if (this.records) return this.records - const keywordsRecords = await this.db.findAll() - this.records = keywordsRecords - logger.info('关键词:\n' + JSON.stringify(keywordsRecords)) - return this.records - } - - static async getKeywordsText () { - const records = await this.getKeywords() - let text :string = '【操作说明】\n' - for (const record of records) { - const fields = record.fields - text += `${fields['指令名称|name']}:${fields['说明|desc']}\n` - } - return text - } - - static async getSystemKeywordsText () { - const records = await this.getKeywords() - let text :string = '【操作说明】\n' - for (const record of records) { - const fields = record.fields - if (fields['类型|type'] === '系统指令') text += `${fields['指令名称|name']} : ${fields['说明|desc']}\n` - } - return text - } - -} diff --git a/src/services/larkService.ts b/src/services/larkService.ts deleted file mode 100644 index 491b401d..00000000 --- a/src/services/larkService.ts +++ /dev/null @@ -1,473 +0,0 @@ -/* eslint-disable sort-keys */ -import { LarkSheet } from '../db/lark.js' -import { Message, ScanStatus, types, Wechaty, log } from 'wechaty' -import { getCurTime, delay, logger } from '../utils/utils.js' -import moment from 'moment' -import { FileBox } from 'file-box' -import { LarkDB } from '../db/lark-db.js' -import { ChatFlowConfig } from '../api/base-config.js' -import type { ChatMessage } from '../types/mod.js' - -import fs from 'fs' -import path from 'path' - -const MEDIA_PATH = 'data/media/image' -const MEDIA_PATH_QRCODE = path.join(MEDIA_PATH, 'qrcode') -const MEDIA_PATH_CONTACT = path.join(MEDIA_PATH, 'contact') -const MEDIA_PATH_ROOM = path.join(MEDIA_PATH, 'room') - -const paths = [ MEDIA_PATH, MEDIA_PATH_QRCODE, MEDIA_PATH_CONTACT, MEDIA_PATH_ROOM ] - -paths.forEach((p) => { - if (!fs.existsSync(p)) { - fs.mkdirSync(p, { recursive: true }) - } -}) - -// 服务类 -export class LarkChat { - - static db:LarkSheet | undefined - static msgStore: any[] = [] - static messageData: any - static bot:Wechaty - - private constructor () { - - } - - // 初始化 - static async init () { - const that = this - LarkSheet.init(LarkDB.lark, LarkDB.dataBaseIds.messageSheet) - this.msgStore = [] - - // 启动定时任务,每秒钟写入一次,每次写入10条 - setInterval(() => { - // logger.info('待处理消息池长度:', that.msgStore.length || '0') - - if (that.msgStore.length) { - const end = that.msgStore.length < 10 ? that.msgStore.length : 10 - const records = that.msgStore.splice(0, end) - // logger.info('写入Lark的消息:', JSON.stringify(records)) - try { - LarkSheet.insert(records).then((response:any) => { - if (!response.data) { - log.error('调用Lark写入接口成功,写入Lark失败:', JSON.stringify(response)) - } - return response - }).catch((err: any) => { log.error('调用Lark写入接口失败:', err) }) - } catch (err) { - log.error('调用datasheet.records.create失败:', err) - } - } - }, 1000) - log.info('初始化 LarkChat 成功...') - this.bot = ChatFlowConfig.bot - } - - static addRecord (record: any) { - logger.info('消息入列:', JSON.stringify(record)) - if (record.fields) { - LarkChat.msgStore.push(record) - // logger.info('最新消息池长度:', this.msgStore.length) - } - } - - static async addChatRecord (record: any) { - try { - log.info('addChatRecord:', JSON.stringify(record)) - LarkChat.msgStore.push(record) - // logger.info('最新消息池长度:', this.msgStore.length) - } catch (e) { - log.error('添加记录失败:', e) - - } - - } - - // 处理二维码上传 - static async uploadQRCodeToVika (qrcode: string, status: ScanStatus) { - - if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { - - const qrcodeUrl = encodeURIComponent(qrcode) - const qrcodeImageUrl = [ 'https://wechaty.js.org/qrcode/', qrcodeUrl ].join('') - // logger.info('StarterBot', 'Lark onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) - - let uploadedAttachments:any = '' - let file: FileBox - let filePath = '' - try { - file = FileBox.fromQRCode(qrcode) - filePath = `${MEDIA_PATH_QRCODE}/` + file.name - try { - // const writeStream = fs.createWriteStream(filePath) - // await file.pipe(writeStream) - await file.toFile(filePath, true) - await delay(1000) - uploadedAttachments = await LarkSheet.upload(filePath) - const text = qrcodeImageUrl - if (uploadedAttachments.file_token) { - try { - await this.addScanRecord(uploadedAttachments, text) - // fs.unlink(filePath, (err) => { - // logger.info('二维码上传Lark完成删除文件:', filePath, err) - // }) - } catch (err) { - logger.error('二维码Lark 写入失败:', err) - } - } - } catch { - logger.info('二维码上传失败:', filePath) - // fs.unlink(filePath, (err) => { - // logger.info('二维码上传Lark失败删除文件', filePath, err) - // }) - } - - } catch (e) { - logger.error('onScan,二维码Lark 写入失败:', e) - } - - } else { - logger.info('机器人启动,二维码上传维格表', 'onScan: %s(%s)', ScanStatus[status], status) - } - } - - static async addScanRecord (uploadedAttachments: any, text: string) { - - const curTime = getCurTime() - const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') - const files: any = [] - if (uploadedAttachments.file_token) { - files.push({ - file_token: uploadedAttachments.file_token, - }) - } - - const records = [ - { - fields: { - '时间|timeHms':timeHms, - '发送者|name': 'system', - '群名称|topic': '--', - '消息内容|messagePayload': text, - '好友ID|wxid': 'system', - '群ID|roomid': '--', - '消息类型|messageType': 'qrcode', - '文件图片|file': files, - }, - }, - ] - - // logger.info('待写入登录二维码消息:', JSON.stringify(records)) - const response:any = await LarkSheet.insert(records) - if (!response || !response.data) { - logger.error('调用Lark写入接口成功,写入Lark失败:', JSON.stringify(response)) - } - return response - } - - static async handleFileMessage (file:FileBox, message: Message) { - const fileName = file.name - const room = message.room() - const talker = message.talker() - let filePath = `${MEDIA_PATH_CONTACT}/${talker.id}_${fileName}` - if (room) { - filePath = `${MEDIA_PATH_ROOM}/${room.id}_${fileName}` - } - - // logger.info('文件路径filePath:', filePath) - - try { - await file.toFile(filePath, true) - log.info('保存文件到本地成功') - } catch (err) { - log.error('保存文件到本地失败', err) - return '' - } - - await delay(1000) - const res = await LarkSheet.upload(filePath) - return res - } - - static async addHeartbeatRecord (text: string) { - - const curTime = getCurTime() - const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') - const files: any = [] - - const record = { - fields: { - '时间|timeHms':timeHms, - '发送者|name': 'system', - '群名称|topic': '--', - '消息内容|messagePayload': text, - '好友ID|wxid': 'system', - '群ID|roomid': '--', - '消息类型|messageType': 'heartbeat', - '文件图片|file': files, - }, - } - logger.info('心跳消息:', JSON.stringify(record)) - LarkChat.msgStore.push(record) - } - - static async onMessage (message: Message) { - log.info('调用onMessage消息存储到lark...') - const room = message.room() - const talker = message.talker() - const files: any = [] - try { - - let uploadedAttachments:any = '' - const msgType = types.Message[message.type()] - let file: any - let text = '' - - let urlLink - let miniProgram - - switch (message.type()) { - // 文本消息 - case types.Message.Text: - text = message.text() - break - - // 图片消息 - - case types.Message.Image:{ - - const img = await message.toImage() - await delay(1500) - try { - file = await img.hd() - // file = await message.toFileBox() - - } catch (e) { - logger.error('Image解析img.hd()失败:', e) - try { - file = await img.thumbnail() - } catch (e) { - logger.error('Image解析img.thumbnail()失败:', e) - } - } - - break - } - - // 链接卡片消息 - case types.Message.Url: - urlLink = await message.toUrlLink() - text = JSON.stringify(JSON.parse(JSON.stringify(urlLink)).payload) - // file = await message.toFileBox(); - break - - // 小程序卡片消息 - case types.Message.MiniProgram: - - miniProgram = await message.toMiniProgram() - - text = JSON.stringify(JSON.parse(JSON.stringify(miniProgram)).payload) - - // logger.info(miniProgram) - /* - miniProgram: 小程序卡片数据 - { - appid: "wx363a...", - description: "贝壳找房 - 真房源", - title: "美国白宫,10室8厅9卫,99999刀/月", - iconUrl: "http://mmbiz.qpic.cn/mmbiz_png/.../640?wx_fmt=png&wxfrom=200", - pagePath: "pages/home/home.html...", - shareId: "0_wx363afd5a1384b770_..._1615104758_0", - thumbKey: "84db921169862291...", - thumbUrl: "3051020100044a304802010002046296f57502033d14...", - username: "gh_8a51...@app" - } - */ - break - - // 语音消息 - case types.Message.Audio: - await delay(1500) - try { - file = await message.toFileBox() - } catch (e) { - logger.error('Audio解析失败:', e) - file = '' - } - - break - - // 视频消息 - case types.Message.Video: - await delay(1500) - try { - file = await message.toFileBox() - - } catch (e) { - logger.error('Video解析失败:', e) - file = '' - } - break - - // 动图表情消息 - case types.Message.Emoticon: - - try { - file = await message.toFileBox() - - } catch (e) { - logger.error('Emoticon解析失败:', e) - file = '' - } - - break - - // 文件消息 - case types.Message.Attachment: - await delay(1500) - try { - file = await message.toFileBox() - - } catch (e) { - logger.error('Attachment解析失败:', e) - file = '' - } - - break - // 文件消息 - case types.Message.Location: - - // const location = await message.toLocation() - text = message.text() - break - case types.Message.Unknown: - // const location = await message.toLocation() - // text = JSON.stringify(JSON.parse(JSON.stringify(location)).payload) - break - // 其他消息 - default: - break - } - - if (file) { - // log.info('文件file:', file) - try { - text = JSON.stringify(file.toJSON()) - } catch (e) { - log.error('文件转换JSON失败:', e) - } - try { - uploadedAttachments = await LarkChat.handleFileMessage(file, message) - files.push({ - file_token: uploadedAttachments.file_token, - }) - // text = JSON.stringify(uploadedAttachments.data) - log.info('上传文件Lark成功:', JSON.stringify(uploadedAttachments)) // 上传成功后返回的数据 - } catch (e) { - log.error('文件上传Lark失败:', e) - } - } - - const listener = message.listener() - let topic = '' - if (room) { - topic = await room.topic() - } - const curTime = getCurTime() - const timeHms = moment(curTime).format('YYYY-MM-DD HH:mm:ss') - let wxAvatar:any = '' - let roomAvatar:any = '' - let listenerAvatar:any = '' - try { - wxAvatar = (await talker.avatar()).toJSON() - wxAvatar = wxAvatar.url - } catch (e) { - logger.error('获取发言人头像失败:', e) - } - - if (listener) { - try { - listenerAvatar = (await listener.avatar()).toJSON() - listenerAvatar = listenerAvatar.url - } catch (e) { - logger.error('获取发言人头像失败:', e) - } - } - - if (room) { - try { - roomAvatar = (await room.avatar()).toJSON() - roomAvatar = roomAvatar.url - } catch (e) { - logger.error('获取发言人头像失败:', e) - } - } - - try { - const record = { - fields: { - '时间|timeHms':timeHms, - '发送者|name': talker.name(), - '好友备注|alias': await talker.alias(), - '群名称|topic': topic || '--', - '消息内容|messagePayload': text, - '好友ID|wxid': talker.id !== 'null' ? talker.id : '--', - '群ID|roomid': room && room.id ? room.id : '--', - '消息类型|messageType': msgType, - '文件图片|file': files, - '消息ID|messageId': message.id, - '接收人|listener': topic ? '--' : (await listener?.alias() || listener?.name()), - '接收人ID|listenerid':topic ? '--' : listener?.id, - '发送者头像|wxAvatar': wxAvatar, - '群头像|roomAvatar':roomAvatar, - '接收人头像|listenerAvatar':listenerAvatar, - }, - } - log.info('addChatRecord:', JSON.stringify(record)) - - if (msgType !== 'Unknown') { - await this.addChatRecord(record) - } - } catch (e) { - log.error('添加记录失败:', e) - } - - } catch (e) { - log.error('onMessage消息转换失败:', e) - } - } - - static async formatMessage (message: Message) { - const text = message.text() - const talker = message.talker() - const listener = message.listener() - const room = message.room() - const topic = await room?.topic() - const type = message.type() - - const chatMessage: ChatMessage = { - id: message.id, - text, - type, - talker: { - name: talker.name(), - id: talker.id, - alias: await talker.alias(), - }, - room: { - topic, - id: room?.id, - }, - listener: { - id: listener?.id, - name: listener?.name(), - alias: await listener?.alias(), - }, - } - return chatMessage - } - -} diff --git a/src/services/messageService.ts b/src/services/messageService.ts index 34105d8f..dcd8791d 100644 --- a/src/services/messageService.ts +++ b/src/services/messageService.ts @@ -1,18 +1,19 @@ /* eslint-disable no-console */ /* eslint-disable sort-keys */ -import { VikaSheet } from '../db/vika.js' import { Message, ScanStatus, types, Wechaty, log } from 'wechaty' import { getCurTime, delay, logger } from '../utils/utils.js' import moment from 'moment' import { FileBox } from 'file-box' -import { VikaDB } from '../db/vika-db.js' -import { BaseEntity, MappingOptions } from '../db/vika-orm.js' import { ChatFlowConfig } from '../api/base-config.js' import type { ChatMessage } from '../types/mod.js' - +import { + ServeCreateTalkRecords, + ServeCreateTalkRecordsUpload, +} from '../api/chat.js' import fs from 'fs' import path from 'path' +import FormData from 'form-data' import { db } from '../db/tables.js' const messageData = db.message @@ -30,120 +31,67 @@ paths.forEach((p) => { } }) -const mappingOptions: MappingOptions = { // 定义字段映射选项 - fieldMapping: { // 字段映射 - timeHms: '时间|timeHms', - name: '发送者|name', - alias: '好友备注|alias', - topic: '群名称|topic', - listener: '接收人|listener', - messagePayload: '消息内容|messagePayload', - file: '文件图片|file', - messageType: '消息类型|messageType', - wxid: '好友ID|wxid', - listenerid: '接收人ID|listenerid', - roomid: '群ID|roomid', - messageId: '消息ID|messageId', - wxAvatar: '发送者头像|wxAvatar', - roomAvatar: '群头像|roomAvatar', - listenerAvatar: '接收人头像|listenerAvatar', - }, - tableName: '消息记录|Message', // 表名 -} - // 服务类 -export class MessageChat extends BaseEntity { +export class MessageChat { - static db:VikaSheet | undefined - static msgStore: any[] + static msgStore: any[] = [] static messageData: any static bot:Wechaty - - timeHms?: string - - name?: string - - alias?: string - - topic?: string - - listener?: string - - messagePayload?: string - - file?: any - - messageType?: string - - wxid?: string - - roomid?: string - - messageId?: string - - wxAvatar?: string - - roomAvatar?: string - - listenerAvatar?: string + static batchCount: number = 10 + static delayTime: number = 1000 // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 - - protected static override mappingOptions: MappingOptions = mappingOptions // 设置映射选项为上面定义的 mappingOptions - - protected static override getMappingOptions (): MappingOptions { // 获取映射选项的方法 - return this.mappingOptions // 返回当前类的映射选项 - } - - static override setMappingOptions (options: MappingOptions) { // 设置映射选项的方法 - this.mappingOptions = options // 更新当前类的映射选项 + constructor () { + const isVika = ChatFlowConfig.token.indexOf('/') === -1 + MessageChat.batchCount = isVika ? 10 : 100 + MessageChat.delayTime = isVika ? 1000 : 500 } // 初始化 static async init () { - if (!MessageChat.db) { - const that = this - try { - // console.debug(VikaDB) - MessageChat.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.messageSheet) - MessageChat.setVikaOptions({ - apiKey: VikaDB.token, - baseId: VikaDB.dataBaseIds.messageSheet, // 设置 base ID - }) - this.msgStore = [] - this.messageData = messageData - - // 启动定时任务,每秒钟写入一次,每次写入10条 - setInterval(() => { + const that = this + try { + this.msgStore = [] + this.messageData = messageData + + // 启动定时任务,每秒钟写入一次,每次写入10条 + setInterval(() => { // logger.info('待处理消息池长度:', that.msgStore.length || '0') - if (that.msgStore.length) { - const end = that.msgStore.length < 10 ? that.msgStore.length : 10 - const records = that.msgStore.splice(0, end) - // logger.info('写入vika的消息:', JSON.stringify(records)) - try { - MessageChat.db?.insert(records).then((response: { success: any }) => { - if (!response.success) { - logger.error('调用vika写入接口成功,写入vika失败:', JSON.stringify(response)) - } - return response - }).catch((err: any) => { logger.error('调用vika写入接口失败:', err) }) - } catch (err) { - logger.error('调用datasheet.records.create失败:', err) - } + if (that.msgStore.length && ChatFlowConfig.isReady) { + const end = that.msgStore.length < this.batchCount ? that.msgStore.length : this.batchCount + const records = that.msgStore.splice(0, end) + // logger.info('写入vika的消息:', JSON.stringify(records)) + try { + ServeCreateTalkRecords({ records }).then((response: any) => { + if (response.message !== 'success') { + log.error('调用消息写入接口成功,写入失败:', JSON.stringify(response)) + } else { + log.info('调用消息写入接口成功,写入成功:', response.data.length) + } + return response + }).catch((err: any) => { log.error('调用消息写入接口失败:', err) }) + + } catch (err) { + logger.error('调用datasheet.records.create失败:', err) } - }, 1000) + } else if (!ChatFlowConfig.isReady) { + log.info('机器人未初始化完成,待处理消息池等待中...') + } else { + // log.info('待处理消息池为空...', new Date().toLocaleString()) + } + return null + }, 1500) - log.info('初始化 MessageChat 成功...') - } catch (e) { - log.error('初始化 MessageChat 失败:', e) - } + log.info('初始化 MessageChat 成功...') + } catch (e) { + log.error('初始化 MessageChat 失败:', e) } this.bot = ChatFlowConfig.bot } static addRecord (record: any) { - logger.info('消息入列:', JSON.stringify(record)) + logger.info('addRecord消息入列:', JSON.stringify(record)) if (record.fields) { MessageChat.msgStore.push(record) // logger.info('最新消息池长度:', this.msgStore.length) @@ -156,8 +104,7 @@ export class MessageChat extends BaseEntity { MessageChat.msgStore.push(record) // logger.info('最新消息池长度:', this.msgStore.length) } catch (e) { - logger.error('添加记录失败:', e) - + logger.error('addChatRecord添加记录失败:', e) } } @@ -181,8 +128,22 @@ export class MessageChat extends BaseEntity { // const writeStream = fs.createWriteStream(filePath) // await file.pipe(writeStream) await file.toFile(filePath, true) - await delay(1000) - uploadedAttachments = await MessageChat.db?.upload(filePath, '') + await delay(this.delayTime) + // uploadedAttachments = await MessageChat.db?.upload(filePath, '') + + // 创建一个新的FormData实例 + const form = new FormData() + // 添加文件到form实例。第一个参数是API期待的字段名 + form.append('file', fs.createReadStream(filePath)) + // 如果API需要特定的头部(如认证),可以在这里添加 + const config = { + headers: { + ...form.getHeaders(), + // 'Authorization': 'Bearer yourToken' // 示例:如何添加认证头部 + }, + } + uploadedAttachments = await ServeCreateTalkRecordsUpload(form, config) + log.info('上传二维码到vika结果:', JSON.stringify(uploadedAttachments)) const text = qrcodeImageUrl if (uploadedAttachments.data) { try { @@ -221,24 +182,25 @@ export class MessageChat extends BaseEntity { const records = [ { - fields: { - '时间|timeHms':timeHms, - '发送者|name': 'system', - '群名称|topic': '--', - '消息内容|messagePayload': text, - '好友ID|wxid': 'system', - '群ID|roomid': '--', - '消息类型|messageType': 'qrcode', - '文件图片|file': files, - }, + timeHms, + name: 'system', + topic: '--', + messagePayload: text, + wxid: 'system', + roomid: '--', + messageType: 'qrcode', + file: files, }, ] // logger.info('待写入登录二维码消息:', JSON.stringify(records)) - const response = await this.db?.insert(records) - if (!response.success) { - logger.error('调用vika写入接口成功,写入vika失败:', JSON.stringify(response)) - } + const response:any = await ServeCreateTalkRecords({ records }) + log.info('调用vika写入接口成功,写入vika结果:', JSON.stringify(response)) + + // if (!response.message.success) { + // logger.error('调用vika写入接口成功,写入vika失败:', JSON.stringify(response)) + // } + return response } @@ -255,14 +217,29 @@ export class MessageChat extends BaseEntity { try { await file.toFile(filePath, true) - logger.info('保存文件到本地成功') + logger.info('保存文件到本地成功', filePath) } catch (err) { logger.error('保存文件到本地失败', err) return '' } - await delay(1000) - return await this.db?.upload(filePath, '') + await delay(this.delayTime) + // return await this.db?.upload(filePath, '') + + // 创建一个新的FormData实例 + const form = new FormData() + // 添加文件到form实例。第一个参数是API期待的字段名 + form.append('file', fs.createReadStream(filePath)) + // 如果API需要特定的头部(如认证),可以在这里添加 + const config = { + headers: { + ...form.getHeaders(), + // 'Authorization': 'Bearer yourToken' // 示例:如何添加认证头部 + }, + } + const uploadedAttachments = await ServeCreateTalkRecordsUpload(form, config) + log.info('上传文件到vika结果:', JSON.stringify(uploadedAttachments)) + return uploadedAttachments } static async addHeartbeatRecord (text: string) { @@ -272,16 +249,14 @@ export class MessageChat extends BaseEntity { const files: any = [] const record = { - fields: { - '时间|timeHms':timeHms, - '发送者|name': 'system', - '群名称|topic': '--', - '消息内容|messagePayload': text, - '好友ID|wxid': 'system', - '群ID|roomid': '--', - '消息类型|messageType': 'heartbeat', - '文件图片|file': files, - }, + timeHms, + name: 'system', + topic: '--', + messagePayload: text, + wxid: 'system', + roomid: '--', + messageType: 'heartbeat', + file: files, } logger.info('心跳消息:', JSON.stringify(record)) MessageChat.msgStore.push(record) @@ -431,7 +406,7 @@ export class MessageChat extends BaseEntity { try { text = JSON.stringify(file.toJSON()) } catch (e) { - log.error('文件转换JSON失败:', e) + log.error('messageService 文件转换JSON失败:', e) } uploadedAttachments = await MessageChat.handleFileMessage(file, message) if (uploadedAttachments) { @@ -476,31 +451,29 @@ export class MessageChat extends BaseEntity { } try { - const record = { - fields: { - '时间|timeHms':timeHms, - '发送者|name': talker.name(), - '好友备注|alias': await talker.alias(), - '群名称|topic': topic || '--', - '消息内容|messagePayload': text, - '好友ID|wxid': talker.id !== 'null' ? talker.id : '--', - '群ID|roomid': room && room.id ? room.id : '--', - '消息类型|messageType': msgType, - '文件图片|file': files, - '消息ID|messageId': message.id, - '接收人|listener': topic ? '--' : (await listener?.alias() || listener?.name()), - '接收人ID|listenerid':topic ? '--' : listener?.id, - '发送者头像|wxAvatar': wxAvatar, - '群头像|roomAvatar':roomAvatar, - '接收人头像|listenerAvatar':listenerAvatar, - }, + const record:any = { + timeHms, + name: talker.name(), + alias: await talker.alias(), + topic: topic || '--', + messagePayload: text, + wxid: talker.id !== 'null' ? talker.id : '--', + roomid: room && room.id ? room.id : '--', + messageType: msgType, + messageId: message.id, + listener: topic ? '--' : (await listener?.alias() || listener?.name()), + listenerid:topic ? '--' : listener?.id, + wxAvatar, + roomAvatar, + listenerAvatar, } + if (files.length) record['file'] = files // logger.info('addChatRecord:', JSON.stringify(record)) if (msgType !== 'Unknown') { await this.addChatRecord(record) } } catch (e) { - logger.error('添加记录失败:', e) + logger.error('onMessage添加记录失败:', e) } diff --git a/src/services/mod.ts b/src/services/mod.ts index 305d3241..34592f25 100644 --- a/src/services/mod.ts +++ b/src/services/mod.ts @@ -1,12 +1,8 @@ import { MessageChat } from './messageService.js' -import { LarkChat } from './larkService.js' import { EnvChat } from './envService.js' import { WhiteListChat, WhiteList } from './whiteListService.js' import { GroupNoticeChat } from './groupNoticeService.js' -import { RoomChat } from './roomService.js' -import { ContactChat } from './contactService.js' import { ActivityChat } from './activityService.js' -import { KeywordChat } from './keywordService.js' import { NoticeChat } from './noticeService.js' import { QaChat } from './qaService.js' import { OrderChat } from './orderService.js' @@ -16,13 +12,9 @@ export type MessageChatType = MessageChat export type EnvChatType = EnvChat export type WhiteListChatType = WhiteListChat export type GroupNoticeChatType = GroupNoticeChat -export type RoomChatType = RoomChat -export type ContactChatType = ContactChat export type ActivityChatType = ActivityChat -export type KeywordChatType = KeywordChat export type NoticeChatType = NoticeChat export type QaChatType = QaChat -export type LarkChatType = LarkChat export type OrderChatType = OrderChat export type StatisticChatType = StatisticChat @@ -31,13 +23,9 @@ export { EnvChat, WhiteListChat, GroupNoticeChat, - RoomChat, - ContactChat, ActivityChat, - KeywordChat, NoticeChat, QaChat, - LarkChat, OrderChat, StatisticChat, } @@ -48,10 +36,7 @@ export interface Services { messageService: MessageChat; whiteListService: WhiteListChat; groupNoticeService: GroupNoticeChat; - roomService: RoomChat; - contactService: ContactChat; activityService: ActivityChat; noticeService: NoticeChat; qaService: QaChat; - keywordService: KeywordChat; } diff --git a/src/services/noticeService.ts b/src/services/noticeService.ts index 954efd2b..d6e57c47 100644 --- a/src/services/noticeService.ts +++ b/src/services/noticeService.ts @@ -1,22 +1,23 @@ /* eslint-disable sort-keys */ import { Wechaty, log } from 'wechaty' import type { TaskConfig } from '../api/base-config.js' -import { VikaSheet, IRecord } from '../db/vika.js' import schedule from 'node-schedule' -import { getRule, delay, logger } from '../utils/mod.js' +import { delay, logger } from '../utils/mod.js' import type { BusinessRoom, BusinessUser } from '../plugins/mod.js' import { getContact, getRoom, } from '../plugins/mod.js' import { sendMsg } from './configService.js' -import { VikaDB } from '../db/vika-db.js' import { ChatFlowConfig } from '../api/base-config.js' +import { + ServeGetNoticesTask, +} from '../api/notice.js' // import { db } from '../db/tables.js' // const noticeData = db.notice -function getRemainingTime (taskTime:number):string { +function getRemainingTime (taskTime: number): string { const time = taskTime - new Date().getTime() const seconds = Math.floor((time / 1000) % 60) const minutes = Math.floor((time / (1000 * 60)) % 60) @@ -25,112 +26,47 @@ function getRemainingTime (taskTime:number):string { return `${days}天${hours}小时${minutes}分钟${seconds}秒` } -type TaskFields = { - '内容|desc'?: string; - '时间|time'?: string; - '周期|cycle'?: string; - '通知目标类型|type'?: string; - '昵称/群名称|name'?: string; - '好友ID/群ID(选填)|id'?: string; - '好友备注(选填)|alias'?: string; - '启用状态|state'?: string; -}; - // 服务类 export class NoticeChat { - static db:VikaSheet - static envsOnVika: any static roomWhiteList: any static contactWhiteList: any - static reminderList: TaskConfig[] = [] - static jobs: {[key:string]:any} - static bot:Wechaty + static jobs: { [key: string]: any } + static bot: Wechaty - private constructor () { - } + private constructor () {} // 初始化 static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.noticeSheet) - await this.getRecords() this.bot = ChatFlowConfig.bot - log.info('初始化 NoticeChat 成功...') } - static async getRecords () { - const records = await this.db.findAll() - // logger.info('维格表中的记录:', JSON.stringify(records)) - return records - } - - // 获取定时提醒 - static async getTimedTask (): Promise { - const taskRecords: IRecord[] = await this.db.findAll() - - const timedTasks: TaskConfig[] = taskRecords - .map((task: IRecord) => { - const { - '内容|desc': desc, - '时间|time': time, - '周期|cycle': cycle, - '通知目标类型|type': type, - '昵称/群名称|name': name, - '好友ID/群ID(选填)|id': id, - '好友备注(选填)|alias': alias, - '启用状态|state': state, - } = task.fields as TaskFields - - const isActive = state === '开启' - const isContact = type === '好友' - const target = isContact - ? { name: name || '', id: id || '', alias: alias || '' } - : { topic: name || '', id: id || '' } - - const taskConfig: TaskConfig = { - id: task.recordId, - msg: desc || '', - time: Number(time) || 0, - cycle: cycle || '无重复', - targetType: isContact ? 'contact' : 'room', - target, - active: isActive, - } - - return isActive && desc && time && cycle && (name || id || alias) ? taskConfig : null - }) - .filter(Boolean) as TaskConfig[] - - this.reminderList = timedTasks - return this.reminderList - } - // 更新任务 static async updateJobs () { const that = this try { - // 结束所有任务 + // 结束所有任务 await schedule.gracefulShutdown() // logger.info('结束所有任务成功...') } catch (e) { log.error('结束所有任务失败:' + e) } try { - const tasks = await this.getTimedTask() - logger.info('获取到的定时提醒任务:' + tasks.length || '0') - // logger.info('获取到的定时提醒任务:\n' + JSON.stringify(tasks)) + const res = await ServeGetNoticesTask() + const tasks: TaskConfig[] = res.data.items + log.info('获取到的定时提醒任务:' + tasks.length || '0') + logger.info('获取到的定时提醒任务:\n' + JSON.stringify(tasks)) + this.jobs = {} for (let i = 0; i < tasks.length; i++) { const task: TaskConfig = tasks[i] as TaskConfig if (task.active) { - // 格式化任务 - const curRule = getRule(task) // logger.info(`任务${i}原始信息:` + JSON.stringify(task)) // logger.info('转换信息:' + curRule) try { - await schedule.scheduleJob(task.id, curRule, async () => { + await schedule.scheduleJob(task.id, task.rule, async () => { let text = task.msg // 如果task.msg中包含“d%”将其替换为task.time减去当前时间得到的剩余时间(精确到秒)赋值给task.msg,示例消息:距离高考还有:d% if (task.msg.includes('d%')) { diff --git a/src/services/orderService.ts b/src/services/orderService.ts index a56d9f2d..3f8e4b53 100644 --- a/src/services/orderService.ts +++ b/src/services/orderService.ts @@ -1,11 +1,10 @@ /* eslint-disable sort-keys */ -import { VikaSheet } from '../db/vika.js' import { delay, logger } from '../utils/utils.js' import type { RoomWhiteList, ContactWhiteList } from '../types/mod.js' import type { BusinessRoom, BusinessUser } from '../api/contact-room-finder.js' -import { VikaDB } from '../db/vika-db.js' import { ChatFlowConfig } from '../api/base-config.js' import { Wechaty, log } from 'wechaty' +import { ServeGetOrders } from '../api/order.js' // import { db } from '../db/tables.js' // const whiteListData = db.whiteList @@ -15,8 +14,6 @@ export type WhiteList = { contactWhiteList: ContactWhiteList; roomWhiteList: Roo // 服务类 export class OrderChat { - static db:VikaSheet - static envsOnVika: any static roomWhiteList: any static contactWhiteList: any static bot:Wechaty @@ -27,7 +24,6 @@ export class OrderChat { // 初始化 static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.whiteListSheet) this.contactWhiteList = { qa: [], msg: [], @@ -47,7 +43,8 @@ export class OrderChat { } static async getRecords () { - const records = await this.db.findAll() + const res = await ServeGetOrders() + const records = res.data.items logger.info('维格表中的记录:' + JSON.stringify(records)) return records } diff --git a/src/services/qaService.ts b/src/services/qaService.ts index 3d2b9b14..f5a40372 100644 --- a/src/services/qaService.ts +++ b/src/services/qaService.ts @@ -1,11 +1,9 @@ /* eslint-disable sort-keys */ -import type { TaskConfig } from '../api/base-config.js' -import { VikaSheet } from '../db/vika.js' import type { SkillInfoArray } from './wxopenaiService.js' import { logger } from '../utils/mod.js' -import { VikaDB } from '../db/vika-db.js' import { ChatFlowConfig } from '../api/base-config.js' import { Wechaty, log } from 'wechaty' +import { ServeGetQas } from '../api/qa.js' // import { db } from '../db/tables.js' // const noticeData = db.notice @@ -14,11 +12,8 @@ import { Wechaty, log } from 'wechaty' // 服务类 export class QaChat { - static db: VikaSheet - static envsOnVika: any static roomWhiteList: any static contactWhiteList: any - static reminderList: TaskConfig[] = [] static records: any static bot:Wechaty @@ -28,21 +23,18 @@ export class QaChat { // 初始化 static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.qaSheet) - const records = await this.getRecords() - this.records = records - this.bot = ChatFlowConfig.bot - - log.info('初始化 QaChat 成功...') - } - - static async getRecords () { - const records = await this.db.findAll() - logger.info('维格表中的记录:', JSON.stringify(records)) - return records + try { + const res = await ServeGetQas() + const records = res.data.items + this.records = records + this.bot = ChatFlowConfig.bot + log.info('初始化 QaChat 成功...') + } catch (e) { + log.info('初始化 QaChat 失败...', e) + } } - // 获取定时提醒 + // 获取问答 static async getQa (): Promise { await this.init() if (!this.records) { @@ -51,21 +43,21 @@ export class QaChat { } const skills: SkillInfoArray = this.records - .filter((record: {recordId:string; fields:{[x: string]: any }}) => record.fields['启用状态|state'] === '启用' && record.fields['标准问题|title'] && record.fields['机器人回答|answer']) - .map((record: {recordId:string; fields:{[x: string]: any }}) => { + .filter((fields: { [x: string]: any ;recordId:string;}) => fields['state'] === '启用' && fields['title'] && fields['answer']) + .map((fields: {[x: string]: any;recordId:string; }) => { const question: string[] = [] for (let i = 1; i <= 3; i++) { - const similarQuestion = record.fields[`相似问题${i}(选填)|question${i}`] + const similarQuestion = fields[`question${i}`] if (similarQuestion) { question.push(similarQuestion) } } return { - skillname: record.fields['分类|skillname'] || '通用问题', - title: record.fields['标准问题|title'], + skillname: fields['skillname'] || '通用问题', + title: fields['title'], question, - answer: [ record.fields['机器人回答|answer'] ], + answer: [ fields['answer'] ], } }) logger.info('skills:', JSON.stringify(skills)) diff --git a/src/services/reminderService-backup.ts b/src/services/reminderService-backup.ts deleted file mode 100644 index 9f53e50e..00000000 --- a/src/services/reminderService-backup.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* eslint-disable sort-keys */ - -/* -定义一个定时提醒任务管理接口,至少包含以下几个动作,给出ts示例代码: -获取任务列表 -获取任务详情 -发布提醒任务 -删除提醒任务 -启用提醒任务 -禁用提醒任务 -更新提醒任务 -*/ - -interface Task { - id: number; - title: string; - description: string; - type: '好友' | '群'; - friendName?: string; - friendId?: string; - time?: Date; - recurring?: '每天' | '每周' | '每月' | '每年'; - enabled: boolean; - } - -interface TaskManager { - getTaskList(): Task[]; - getTaskDetails(id: number): Task | undefined; - createTask(task: Task): void; - deleteTask(id: number): void; - enableTask(id: number): void; - disableTask(id: number): void; - updateTask(id: number, task: Task): void; - } - -class TaskManagerImpl implements TaskManager { - - private tasks: Task[] = [] - - getTaskList (): Task[] { - return this.tasks - } - - getTaskDetails (id: number): Task | undefined { - return this.tasks.find(task => task.id === id) - } - - createTask (task: Task): void { - this.tasks.push(task) - } - - deleteTask (id: number): void { - this.tasks = this.tasks.filter(task => task.id !== id) - } - - enableTask (id: number): void { - const task = this.getTaskDetails(id) - if (task) { - task.enabled = true - } - } - - disableTask (id: number): void { - const task = this.getTaskDetails(id) - if (task) { - task.enabled = false - } - } - - updateTask (id: number, updatedTask: Task): void { - const taskIndex = this.tasks.findIndex(task => task.id === id) - if (taskIndex !== -1) { - this.tasks[taskIndex] = updatedTask - } - } - -} - -// 示例用法 -const taskManager = new TaskManagerImpl() - -// 获取任务列表 -export const tasks = taskManager.getTaskList() - -// 获取任务详情 -const taskId = 1 -export const taskDetails = taskManager.getTaskDetails(taskId) - -// 发布提醒任务 -const newTask: Task = { - id: 2, - title: 'New Task', - description: 'This is a new task', - enabled: true, - type: '群', - time: undefined, -} -taskManager.createTask(newTask) - -// 删除提醒任务 -const taskIdToDelete = 2 -taskManager.deleteTask(taskIdToDelete) - -// 启用提醒任务 -const taskIdToEnable = 1 -taskManager.enableTask(taskIdToEnable) - -// 禁用提醒任务 -const taskIdToDisable = 1 -taskManager.disableTask(taskIdToDisable) - -// 更新提醒任务 -const taskIdToUpdate = 1 -const updatedTask: Task = { - id: 1, - title: 'Updated Task', - description: 'This task has been updated', - enabled: true, - type: '群', - time: undefined, -} - -taskManager.updateTask(taskIdToUpdate, updatedTask) - -export {} diff --git a/src/services/roomService.ts b/src/services/roomService.ts deleted file mode 100644 index 67fc46d8..00000000 --- a/src/services/roomService.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable sort-keys */ -import { VikaSheet, IRecord } from '../db/vika.js' -import { Room, Wechaty, log } from 'wechaty' -import { delay, logger } from '../utils/utils.js' -import { VikaDB } from '../db/vika-db.js' -import { BaseEntity, MappingOptions } from '../db/vika-orm.js' - -import { ChatFlowConfig } from '../api/base-config.js' - -// import { db } from '../db/tables.js' -// const roomData = db.room - -const mappingOptions: MappingOptions = { // 定义字段映射选项 - fieldMapping: { // 字段映射 - id: '群ID|id', - topic: '群名称|topic', - ownerId: '群主ID|ownerId', - updated: '更新时间|updated', - avatar: '头像|avatar', - file: '头像图片|file', - - }, - tableName: '群列表|Room', // 表名 -} - -// 服务类 -export class RoomChat extends BaseEntity { - - static db:VikaSheet - static rooms: any[] - static bot:Wechaty - - topic?: string // 定义名字属性,可选 - - id?: string - - alias?: string - - updated?: string - - avatar?: string - - file?: string - - // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 - - protected static override mappingOptions: MappingOptions = mappingOptions // 设置映射选项为上面定义的 mappingOptions - - protected static override getMappingOptions (): MappingOptions { // 获取映射选项的方法 - return this.mappingOptions // 返回当前类的映射选项 - } - - static override setMappingOptions (options: MappingOptions) { // 设置映射选项的方法 - this.mappingOptions = options // 更新当前类的映射选项 - } - - // 初始化 - static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.roomSheet) - RoomChat.setVikaOptions({ - apiKey: VikaDB.token, - baseId: VikaDB.dataBaseIds.roomSheet, // 设置 base ID - }) - const rooms = await this.getRoom() - this.rooms = rooms - this.bot = ChatFlowConfig.bot - - log.info('初始化 RoomChat 成功...') - } - - static async getRoom () { - const records = await this.db.findAll() - // logger.info('维格表中的记录:', JSON.stringify(records)) - return records - } - - // 上传群列表 - static async updateRooms (puppet:string) { - let updateCount = 0 - try { - const rooms: Room[] = await this.bot.Room.findAll() - log.info('最新微信群数量:', rooms.length) - const recordsAll: any = [] - const recordExisting = await this.db.findAll() - logger.info('云端群数量:' + recordExisting.length || '0') - let wxids: string[] = [] - const recordIds: string[] = [] - if (recordExisting.length) { - recordExisting.forEach((record: IRecord) => { - if (record.fields['群ID|id']) { - wxids.push(record.fields['群ID|id'] as string) - recordIds.push(record.recordId) - } - }) - } - - if (puppet === 'wechaty-puppet-wechat' || puppet === 'wechaty-puppet-wechat4u') { - const count = Math.ceil(recordIds.length / 10) - for (let i = 0; i < count; i++) { - const records = recordIds.splice(0, 10) - logger.info('删除:', records.length) - await this.db.remove(records) - await delay(1000) - } - wxids = [] - } - - for (let i = 0; i < rooms.length; i++) { - - const item: Room | undefined = rooms[i] - // if(item) log.info('头像信息:', (JSON.stringify((await item.avatar()).toJSON()))) - - if (item && !wxids.includes(item.id)) { - let avatar: any = 'null' - try { - avatar = (await item.avatar()).toJSON() - avatar = avatar.url - } catch (err) { - // logger.error('获取群头像失败:' + err) - } - const ownerId = await item.owner()?.id - // logger.info('第一个群成员:' + ownerId) - const fields = { - '头像|avatar':avatar, - '群ID|id': item.id, - '群主ID|ownerId': ownerId || '', - '群名称|topic': await item.topic() || '', - '更新时间|updated': new Date().toLocaleString(), - } - const record = { - fields, - } - recordsAll.push(record) - } - } - - for (let i = 0; i < recordsAll.length; i = i + 10) { - const records = recordsAll.slice(i, i + 10) - try { - await this.db.insert(records) - logger.info('群列表同步完成...' + i + records.length) - updateCount = updateCount + records.length - void await delay(1000) - } catch (err) { - logger.error('群列表同步失败,待系统就绪后再管理群发送【更新通讯录】可手动更新...' + i + 10) - void await delay(1000) - } - } - - logger.info('同步群列表完成,更新到云端群数量:' + updateCount || '0') - } catch (err) { - logger.error('更新群列表失败:' + err) - - } - - } - -} diff --git a/src/services/statisticService.ts b/src/services/statisticService.ts index defeb896..2416eb24 100644 --- a/src/services/statisticService.ts +++ b/src/services/statisticService.ts @@ -1,11 +1,9 @@ /* eslint-disable sort-keys */ -import type { TaskConfig } from '../api/base-config.js' -import { VikaSheet } from '../db/vika.js' import type { SkillInfoArray } from './wxopenaiService.js' import { logger } from '../utils/mod.js' -import { VikaDB } from '../db/vika-db.js' import { ChatFlowConfig } from '../api/base-config.js' import { Wechaty, log } from 'wechaty' +import { ServeGetStatistics } from '../api/statistic.js' // import { db } from '../db/tables.js' // const noticeData = db.notice @@ -14,11 +12,8 @@ import { Wechaty, log } from 'wechaty' // 服务类 export class StatisticChat { - static db: VikaSheet - static envsOnVika: any static roomWhiteList: any static contactWhiteList: any - static reminderList: TaskConfig[] = [] static records: any static bot:Wechaty @@ -28,20 +23,13 @@ export class StatisticChat { // 初始化 static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.qaSheet) - const records = await this.getRecords() - this.records = records + const res = await ServeGetStatistics() + this.records = res.data.items this.bot = ChatFlowConfig.bot log.info('初始化 QaChat 成功...') } - static async getRecords () { - const records = await this.db.findAll() - logger.info('维格表中的记录:', JSON.stringify(records)) - return records - } - // 获取定时提醒 static async getQa (): Promise { await this.init() diff --git a/src/services/whiteListService.ts b/src/services/whiteListService.ts index a4c7296e..b416e338 100644 --- a/src/services/whiteListService.ts +++ b/src/services/whiteListService.ts @@ -1,11 +1,10 @@ /* eslint-disable sort-keys */ -import { VikaSheet } from '../db/vika.js' -import { delay, logger } from '../utils/utils.js' +import { delay } from '../utils/utils.js' import type { RoomWhiteList, ContactWhiteList } from '../types/mod.js' import type { BusinessRoom, BusinessUser } from '../api/contact-room-finder.js' -import { VikaDB } from '../db/vika-db.js' import { ChatFlowConfig } from '../api/base-config.js' import { Wechaty, log } from 'wechaty' +import { ServeGetWhitelistWhite } from '../api/white-list.js' // import { db } from '../db/tables.js' // const whiteListData = db.whiteList @@ -15,8 +14,6 @@ export type WhiteList = { contactWhiteList: ContactWhiteList; roomWhiteList: Roo // 服务类 export class WhiteListChat { - static db:VikaSheet - static envsOnVika: any static roomWhiteList: any static contactWhiteList: any static bot:Wechaty @@ -27,7 +24,6 @@ export class WhiteListChat { // 初始化 static async init () { - this.db = new VikaSheet(VikaDB.vika, VikaDB.dataBaseIds.whiteListSheet) this.contactWhiteList = { qa: [], msg: [], @@ -40,41 +36,34 @@ export class WhiteListChat { act: [], gpt: [], } - await this.getRecords() this.bot = ChatFlowConfig.bot log.info('初始化 WhiteListChat 成功...') } - static async getRecords () { - const records = await this.db.findAll() - logger.info('维格表中的记录:' + JSON.stringify(records)) - return records - } - // 获取白名单 static async getWhiteList () { const whiteList: WhiteList = { contactWhiteList: this.contactWhiteList, roomWhiteList: this.roomWhiteList } - const whiteListRecords: any[] = await this.getRecords() + const res = await ServeGetWhitelistWhite() + const whiteListRecords: any[] = res.data.items await delay(1000) for (let i = 0; i < whiteListRecords.length; i++) { - const record = whiteListRecords[i] - const fields = record.fields - const app: 'qa' | 'msg' | 'act' | 'gpt' = fields['所属应用|app']?.split('|')[1] + const fields = whiteListRecords[i] + const app: 'qa' | 'msg' | 'act' | 'gpt' = fields['app']?.split('|')[1] // logger.info('当前app:' + app) - if (fields['昵称/群名称|name'] || fields['好友ID/群ID(选填)|id'] || fields['好友备注(选填)|alias']) { - if (record.fields['类型|type'] === '群') { + if (fields['name'] || fields['id'] || fields['alias']) { + if (fields['type'] === '群') { const room: BusinessRoom = { - topic: record.fields['昵称/群名称|name'], - id: record.fields['好友ID/群ID(选填)|id'], + topic: fields['name'], + id: fields['id'], } this.roomWhiteList[app].push(room) } else { const contact: BusinessUser = { - name: fields['昵称/群名称|name'], - alias: fields['好友备注(选填)|alias'], - id: fields['好友ID/群ID(选填)|id'], + name: fields['name'], + alias: fields['alias'], + id: fields['id'], } this.contactWhiteList[app].push(contact) } diff --git a/src/types/config.ts b/src/types/config.ts index fc9924d1..be06d8e8 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -91,7 +91,7 @@ interface WechatyConfig { token: string | ''; } -interface VikaConfig { +interface BiTableConfig { spaceName?: string; spaceId?: string; token: string; @@ -153,7 +153,7 @@ interface BaseConfig { interface BotConfig { base: BaseConfig; wechaty: WechatyConfig; - vika: VikaConfig; + vika: BiTableConfig; adminRoom: AdminRoomConfig; autoQa: AutoQaConfig; wxOpenAi: WxOpenAiConfig; @@ -201,7 +201,7 @@ interface Config { } export type { - VikaConfig, + BiTableConfig, BotInfo, BotConfig, ContactConfig, diff --git a/src/types/env.ts b/src/types/env.ts index a93252ad..12066928 100644 --- a/src/types/env.ts +++ b/src/types/env.ts @@ -26,8 +26,8 @@ export enum EnvironmentVariables { WEBHOOK_TOKEN = 'WEBHOOK_TOKEN', WEBHOOK_USERNAME = 'WEBHOOK_USERNAME', WEBHOOK_PASSWORD = 'WEBHOOK_PASSWORD', - YUQUE_TOKEN = 'YUQUE_TOKEN', - YUQUE_NAMESPACE = 'YUQUE_NAMESPACE', + // YUQUE_TOKEN = 'YUQUE_TOKEN', + // YUQUE_NAMESPACE = 'YUQUE_NAMESPACE', AUTOQA_AUTOREPLY = 'AUTOQA_AUTOREPLY', VIKA_USEVIKA = 'VIKA_USEVIKA', VIKA_UPLOADMESSAGETOVIKA = 'VIKA_UPLOADMESSAGETOVIKA', @@ -47,8 +47,9 @@ export enum EnvironmentVariables { export interface ProcessEnv { // 维格表配置 - VIKA_SPACE_NAME: string; + VIKA_SPACE_ID: string; VIKA_TOKEN: string; + ENDPOINT: string; // Wechaty配置 WECHATY_PUPPET: string; @@ -95,8 +96,8 @@ export interface ProcessEnv { WEBHOOK_PASSWORD: string; // 语雀配置 - YUQUE_TOKEN: string; - YUQUE_NAMESPACE: string; + // YUQUE_TOKEN: string; + // YUQUE_NAMESPACE: string; // 系统消息推送 MESSAGE_ENCRYPT: string | boolean; diff --git a/src/types/mod.ts b/src/types/mod.ts index 3c9cdafb..21f88c02 100644 --- a/src/types/mod.ts +++ b/src/types/mod.ts @@ -5,7 +5,7 @@ import type { RoomConfig, Config, SysConfig, - VikaConfig, + BiTableConfig, ContactWhiteList, RoomWhiteList, } from './config.js' @@ -17,7 +17,7 @@ export type { ProcessEnv } from './env.js' export * as configTypes from './config.js' export { - type VikaConfig, + type BiTableConfig, type BotInfo, type BotConfig, type ContactConfig, diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 00000000..e47153cf --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,127 @@ +/* eslint-disable no-console */ +import axios, { AxiosInstance } from 'axios' +import { logForm } from './utils.js' +import 'dotenv/config.js' + +interface TokenResponse { + code: number; + message: string; + data: { + access_token: string; + expires_in: number; // 假设 expiresIn 是以秒为单位的过期时间 + type: string; + }; +} + +class AuthClient { + + private static instance: AuthClient | undefined // 步骤1:私有静态实例 + private axiosInstance: AxiosInstance + private token: string | null = null + private tokenExpirationDate: Date | null = null + private username: string = '' + private password: string = '' + endpoint: string = 'http://127.0.0.1:9503' + + private constructor (ops?:{ + username:string; + password:string, + endpoint:string + }) { // 步骤2:私有构造函数 + this.username = ops?.username || this.username + this.password = ops?.password || this.password + this.endpoint = ops?.endpoint ? ops.endpoint : 'http://127.0.0.1:9503' + this.axiosInstance = axios.create({ + baseURL: this.endpoint, // 你的 API 基础地址 + }) + } + + public static getInstance (ops?:{ + username:string; + password:string, + endpoint:string + }): AuthClient { // 步骤3:公共静态方法 + if (!AuthClient.instance) { + AuthClient.instance = new AuthClient(ops) + } + return AuthClient.instance + } + + async login (username?: string, password?: string): Promise { + this.username = username || this.username + this.password = password || this.password + try { + const response = await this.axiosInstance.post('/api/v1/auth/login', { + mobile: this.username, + password: this.password, + }) + if (response.data.data.access_token) { + logForm('登录成功' + JSON.stringify(response.data)) + this.token = response.data.data.access_token + this.tokenExpirationDate = new Date(new Date().getTime() + response.data.data.expires_in * 1000) + return response.data + } else { + logForm('登录失败' + JSON.stringify(response.data)) + throw new Error(JSON.stringify(response.data)) + } + } catch (error) { + logForm('登录失败' + JSON.stringify(error)) + throw error + } + } + + async init (username?: string, password?: string):Promise { + this.username = username || this.username + this.password = password || this.password + try { + const response:{data:{message:string}} = await this.axiosInstance.post('/api/v1/auth/init', { + mobile: this.username, + password: this.password, + }) + // logForm('系统初始化完成' + JSON.stringify(response.data)) + return response + } catch (error) { + logForm('系统初始化失败' + JSON.stringify(error)) + return error + } + } + + private async refreshTokenIfNeeded (): Promise { + if (!this.token || !this.tokenExpirationDate || new Date() >= this.tokenExpirationDate) { + // logForm('token已过期,刷新token:\n' + this.token) + await this.login() + } else { + // logForm('token未过期,无需刷新:\n' + this.token) + } + } + + public async makeAuthenticatedRequest (path: string, method: 'GET' | 'POST' = 'GET', data?: any): Promise { + await this.refreshTokenIfNeeded() + return this.axiosInstance.request({ + url: this.endpoint + path, + method, + data, + headers: { + Authorization: `Bearer ${this.token}`, + }, + }) + } + + public async getToken (): Promise { + try { + await this.refreshTokenIfNeeded() + return this.token as string + } catch (error) { + console.error('获取token失败', error) + return undefined + } + } + + public delAccessToken (): void { + this.token = null + this.tokenExpirationDate = null + } + +} + +export default AuthClient.getInstance // 修改导出方式,导出getInstance方法 diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 00000000..ba1d3658 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,112 @@ +/* eslint-disable no-console */ +import axios from 'axios' +import getAuthClient from './auth.js' +import 'dotenv/config.js' +import { + // logForm, + logger, +} from './utils.js' + +const authClient = getAuthClient() + +// 创建 axios 实例 +const request = axios.create({ + // API 请求的默认前缀 + baseURL: authClient.endpoint || 'http://127.0.0.1:9503', + + // 请求超时时间 + timeout: 120000, +}) + +/** + * 异常拦截处理器 + * + * @param {*} error + */ +const errorHandler = (error: { response?: { status: number, config:any } }) => { + logger.error('请求异常' + JSON.stringify(error) + '\n') + // 判断是否是响应错误信息 + if (error.response) { + if (error.response.status === 401) { + authClient.delAccessToken() + return request(error.response.config) + } + } + + return Promise.reject(error) +} + +// 请求拦截器 +request.interceptors.request.use(async (config) => { + const token = await authClient.getToken() + + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } + + return config +}, errorHandler) + +// 响应拦截器 +request.interceptors.response.use((response) => { + logger.info(`${response.config.method} ${response.config.url}\n${JSON.stringify(response.data)}\n`) + if (response.data && response.data.message === 'Unauthorized' && response.data.statusCode === 401) { + authClient.delAccessToken() + return request(response.config) + } else { + return response.data + } +}, errorHandler) + +// request.interceptors.response.use((response) => response.data, errorHandler) + +/** + * GET 请求 + * + * @param {String} url + * @param {Object} data + * @param {Object} options + * @returns {Promise} + */ +export const get = (url: string, data = {}, options = {}) => { + return request({ + url, + params: data, + method: 'get', + ...options, + }) +} + +/** + * POST 请求 + * + * @param {String} url + * @param {Object} data + * @param {Object} options + * @returns {Promise} + */ +export const post = (url: string, data = {}, options = {}) => { + return request({ + url, + method: 'post', + data, + ...options, + }) +} + +/** + * 上传文件 POST 请求 + * + * @param {String} url + * @param {Object} data + * @param {Object} options + * @returns {Promise} + */ +export const upload = (url: string, data = {}, options = {}) => { + return request({ + url, + method: 'post', + data, + ...options, + }) +} diff --git a/tests/api.ts b/tests/api.ts deleted file mode 100644 index d772b16d..00000000 --- a/tests/api.ts +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env -S node --no-warnings --loader ts-node/esm -/* eslint-disable sort-keys */ -/* eslint-disable no-console */ -import 'dotenv/config.js' -import { - ChatFlowConfig, - WechatyConfig, - VikaDB, - // LarkDB, - IClientOptions, -} from '../src/chatflow.js' -// import { getSetting } from '../src/api/setting.js' -import { getRoomList } from '../src/api/room.js' -import { getContactList } from '../src/api/contact.js' - -import { MessageChat, ContactChat, RoomChat } from '../src/services/mod.js' - -// import { spawn } from 'child_process' - -// 监听进程退出事件,重新启动程序 -// process.on('exit', (code) => { -// if (code === 1) { -// spawn('npm', [ 'run', 'start' ], { -// stdio: 'inherit', -// }) -// } -// }) - -const main = async () => { - - // 初始化数据表,可以选择Vika或Lark - - // 使用Vika - await VikaDB.init({ - spaceName: process.env['VIKA_SPACE_NAME'], - token: process.env['VIKA_TOKEN'], - }) - // console.debug('VikaDB:', VikaDB) - - // 使用Lark - // await LarkDB.init({ - // appId: process.env['LARK_APP_ID'], - // appSecret: process.env['LARK_APP_SECRET'], - // appToken: process.env['LARK_BITABLE_APP_TOKEN'], - // userMobile: process.env['LARK_APP_USER_MOBILE'], - // }) - - // 从配置文件中读取配置信息,包括wechaty配置、mqtt配置以及是否启用mqtt推送或控制 - const config: { - mqttConfig: IClientOptions, - wechatyConfig: WechatyConfig, - mqttIsOn: boolean, - } | undefined = await ChatFlowConfig.init() // 默认使用vika,使用lark时,需要传入'lark'参数await ChatFlowConfig.init('lark') - console.debug('config:', config) - await MessageChat.init() - // const setting = await getSetting() - // console.debug('setting:', setting) - await ContactChat.init() - const contactList = await getContactList() - console.debug('contactList:', contactList) - - await RoomChat.init() - const roomList = await getRoomList() - console.debug('roomList:', roomList) - -} - -void main() diff --git a/tests/auth.ts b/tests/auth.ts new file mode 100644 index 00000000..cebaf19e --- /dev/null +++ b/tests/auth.ts @@ -0,0 +1,119 @@ +/* eslint-disable no-console */ +// 实现一个类,请求登录接口获得token,自动更新token、当token过期时自动重新请求token +// import axios, { AxiosInstance } from 'axios' +import getAuthClient from '../src/utils/auth.js' +import 'dotenv/config.js' // 导入环境变量配置 + +// import { +// ServeGetUserConfig, +// ServeGetUserDetail, +// ServeGetUserSetting, +// ServeGetUserConfigBykey, +// ServeGetUserConfigGroup, +// } from '../src/api/user.js' + +// import { ServeGetWhitelistWhite } from '../src/api/white-list.js' +// import { ServeGetNotices } from '../src/api/notice.js' +// import { ServeGetKeywords } from '../src/api/keyword.js' +// import { ServeGetStatistics } from '../src/api/statistic.js' +// import { ServeGetGroupnotices } from '../src/api/group-notice.js' + +// import { +// ServeGetChatbots, +// ServeGetChatbotUsers, +// ServeGetChatbotUsersGroup, +// ServeGetChatbotUsersDetail, +// } from '../src/api/chatbot.js' + +// import { ServeGetNoticesTask } from '../src/api/notice.js' +// import { ServeGetWelcomes } from '../src/api/welcome.js' +// import { ServeGetQas } from '../src/api/qa.js' + +// import { ServeGetMedias } from '../src/api/media.js' + +const authClient = getAuthClient() +const res = await authClient.init(process.env.VIKA_SPACE_ID, process.env.VIKA_TOKEN) +console.log('res:', JSON.stringify(res.data, null, 2)) +await authClient.login() + +// const task = await ServeGetNoticesTask() +// console.log('task:', JSON.stringify(task, null, 2)) + +// const chatbots = await ServeGetChatbots() +// console.log('chatbots:', JSON.stringify(chatbots, null, 2)) + +// const chatbotUsers = await ServeGetChatbotUsers({ id:1 }) +// console.log('chatbotUsers:', JSON.stringify(chatbotUsers, null, 2)) + +// const chatbotUsersGroup = await ServeGetChatbotUsersGroup() +// console.log('chatbotUsersGroup:', JSON.stringify(chatbotUsersGroup, null, 2)) + +// const chatbotUsersDetail = await ServeGetChatbotUsersDetail() +// console.log('chatbotUsersDetail:', JSON.stringify(chatbotUsersDetail, null, 2)) + +// const welcomes = await ServeGetWelcomes() +// console.log('welcomes:', JSON.stringify(welcomes, null, 2)) + +// const qas = await ServeGetQas() +// console.log('qas:', JSON.stringify(qas, null, 2)) + +// const userInfo = await ServeGetUserDetail() +// console.log('userInfo:', userInfo) + +// const userSetting = await ServeGetUserSetting() +// console.log('userSetting:', userSetting) + +// const userConfig = await ServeGetUserConfigBykey({ key:'BASE_BOT_NAME', value:'config333' }) +// console.log('userConfig:', userConfig) + +// const userConfigGroup = await ServeGetUserConfigGroup() +// console.log('userConfigGroup:', JSON.stringify(userConfigGroup)) + +// const whiteList = await ServeGetWhitelistWhite() +// console.log('whiteList:', JSON.stringify(whiteList)) + +// const jobs = await ServeGetNotices() +// console.log('jobs:', JSON.stringify(jobs)) + +// const keywords = await ServeGetKeywords() +// console.log('keywords:', JSON.stringify(keywords)) + +// const statistics = await ServeGetStatistics() +// console.log('statistics:', JSON.stringify(statistics)) + +// const groupnotices = await ServeGetGroupnotices() +// console.log('groupnotices:', JSON.stringify(groupnotices)) + +// const userConfigGroup = await ServeGetUserConfigGroup() +// console.log('userConfigGroup:', JSON.stringify(userConfigGroup)) + +// const medias = await ServeGetMedias({ name:'程序开发' }) +// console.log('medias:', JSON.stringify(medias, null, 2)) + +/* 链接检测 */ +// const text = 'https://oou2hscgt2.feishu.cn/base/bascnPgZURujrdwZ9T4JkLUSUQc?table=tbl90nnja6sqMuCT&view=vewmgp68n9' +// const text = 'https://vika.cn/workbench/dstagAfWtNuTqHQizP/viw7KwjQyCjbP?spaceId=spcj6bgt12WoZ' + +// const VIKA_BASE_STRING = 'https://vika.cn/workbench/' +// const LARK_BASE_STRING = '.feishu.cn/base/' +// if (text.includes(VIKA_BASE_STRING) || text.includes(LARK_BASE_STRING)) { +// const config: { +// spaceId: string | undefined, +// table: string | undefined, +// view: string | undefined, +// } = { spaceId: '', table: '', view: '' } +// const vikaConfig = text.match(/https:\/\/vika.cn\/workbench\/(.*?)\/(.*)\?spaceId=(.*)/) +// console.info('vikaConfig:', vikaConfig) +// const larkConfig = text.match(/.feishu.cn\/base\/(.*?)\?table=(.*)&view=(.*)/) +// console.info('larkConfig:', larkConfig) +// if (vikaConfig && vikaConfig.length === 4) { +// config.spaceId = vikaConfig[3] +// config.table = vikaConfig[1] +// config.view = vikaConfig[2] +// } else if (larkConfig && larkConfig.length === 4) { +// config.spaceId = larkConfig[1] +// config.table = larkConfig[2] +// config.view = larkConfig[3] +// } +// console.info('多维表格配置信息:', JSON.stringify(config)) +// } diff --git a/tests/lark-orm-test.ts b/tests/lark-orm-test.ts new file mode 100644 index 00000000..eb055fae --- /dev/null +++ b/tests/lark-orm-test.ts @@ -0,0 +1,242 @@ +/* eslint-disable no-console */ +import 'dotenv/config.js' +import { BiTable } from '../src/db/lark-db.js' +import { BaseEntity, MappingOptions, IRecord } from '../src/db/lark-orm.js' + +const db = new BiTable() +const dbInit = await db.createSheet({ + token: process.env.LARK_APP_ID + '/' + process.env.LARK_APP_SECRET + '/' + process.env.LARK_BITABLE_APP_TOKEN, + spaceId: process.env.LARK_BITABLE_APP_TOKEN, +}) + +console.log('dbInit:', JSON.stringify(dbInit)) + +await db.createSheet + +export class Env extends BaseEntity { + + name?: string + + key?: string + + value?: string + + desc?: string + + syncStatus?: string + + lastOperationTime?: string + + action?: string + + // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 + + protected override mappingOptions: MappingOptions = { + // 定义字段映射选项 + fieldMapping: { + // 字段映射 + name: '配置项|name', + key: '标识|key', + value: '值|value', + desc: '说明|desc', + syncStatus: '同步状态|syncStatus', + lastOperationTime: '最后操作时间|lastOperationTime', + action: '操作|action', + }, + tableName: '环境变量|Env', // 表名 + } // 设置映射选项为上面定义的 mappingOptions + + protected override getMappingOptions (): MappingOptions { + // 获取映射选项的方法 + return this.mappingOptions // 返回当前类的映射选项 + } + + override setMappingOptions (options: MappingOptions) { + // 设置映射选项的方法 + this.mappingOptions = options // 更新当前类的映射选项 + } + +} + +// 测试 +const env = new Env() +await env.setVikaOptions({ + apiKey: process.env.LARK_APP_ID + '/' + process.env.LARK_APP_SECRET + '/' + process.env.LARK_BITABLE_APP_TOKEN, + baseId: db.dataBaseIds.envSheet, // 设置 base ID +}) + +console.log('env:', JSON.stringify(env.config, null, 2)) + +const recordsAll = await env.findAll() +console.log('查询全部记录envData:', JSON.stringify(recordsAll)) + +const records:any[] = [ + { + name: '测试sssss22222', + key: 'MESSAGE_ENCODINGAESKEY', + value: 'X00fcQHkvRkNUdJefu4FD6pym2oIvs63Y5NP3pnZ5po', + syncStatus:'已同步', + desc: '消息加密密钥,vika推送地址https://3sewxanjdvsbp.cfc-execute.bj.baidubce.com/mqtt', + lastOperationTime: 1700326172112, + }, + { + + name: '测试sssss11111', + key: 'MESSAGE_ENCODINGAESKEY', + value: 'X00fcQHkvRkNUdJefu4FD6pym2oIvs63Y5NP3pnZ5po', + syncStatus:'已同步', + desc: '消息加密密钥,vika推送地址https://3sewxanjdvsbp.cfc-execute.bj.baidubce.com/mqtt', + lastOperationTime: 1700326172112, + + }, +] +const record = records[0] as any +const res1 = await env.create(record) +console.log('创建单条记录res1:', JSON.stringify(res1, null, 2)) + +const res2 = await env.createBatch(records) +console.log('批量创建记录res2:', JSON.stringify(res2, null, 2)) + +// const recordsAll2 = recordsAll.filter((item: { fields: { name: string } }) => (item.fields.name === '测试sssss11111' || item.fields.name === '测试sssss22222')) +// console.log('筛选记录recordsAll2:', JSON.stringify(recordsAll2, null, 2)) + +// if (recordsAll2.length) { +// recordsAll2.forEach(async (item: IRecord) => { +// const res3 = await env.delete(item.recordId as string) +// console.log('删除单条记录res3:', JSON.stringify(res3, null, 2)) +// }) +// } + +const recordsAll3 = await env.findAll() +console.log('获取全部记录recordsAll3:', JSON.stringify(recordsAll3)) + +const recordsAll41:IRecord[] = recordsAll3.data as IRecord[] + +const recordsAll4 = recordsAll41.filter((item) => (item.fields['name'] === '测试sssss11111' || item.fields['name'] === '测试sssss22222')) +console.log('筛选符合条件的记录recordsAll4:', JSON.stringify(recordsAll4)) + +// const ids = recordsAll4.map((item: IRecord) => item.recordId as string) +// console.log('获取recordsAll4记录ID数组ids:', JSON.stringify(ids)) + +// if (ids.length) { +// const res4 = await env.deleteBatch(ids) +// console.log('批量删除记录res4:', JSON.stringify(res4)) +// } else { +// console.log('没有记录需要删除...') +// } + +const newRecord0 = recordsAll4[0] as IRecord +const newRecord1 = recordsAll4[1] as IRecord + +newRecord0.fields['name'] = '测试sssss11111-修改' + new Date().toLocaleString() +newRecord1.fields['name'] = '测试sssss22222-修改' + new Date().toLocaleString() + +const recordsAll5 = await env.updatEmultiple(recordsAll4) +console.log('批量更新记录recordsAll5:', JSON.stringify(recordsAll5)) + +const newRecord2 = recordsAll4[2] as IRecord +newRecord2.fields['name'] = '测试sssss333333-修改' + new Date().toLocaleString() + +const recordsAll6 = await env.update(newRecord2.record_id as string, newRecord2.fields) +console.log('更新单条记录recordsAll6:', JSON.stringify(recordsAll6)) + +// 删除测试数据 +const ids = recordsAll4.map((item: IRecord) => item.recordId as string) +console.log('获取recordsAll4记录ID数组ids:', JSON.stringify(ids)) +if (ids.length) { + const res4 = await env.deleteBatch(ids) + console.log('批量删除记录res4:', JSON.stringify(res4)) +} + +const recordsAll7 = await env.findByField('name', 'Wechaty-Puppet') +console.log('根据字段查询recordsAll7:', JSON.stringify(recordsAll7)) + +export class Messages extends BaseEntity { + + timeHms?: string + + name?: string + + alias?: string + + topic?: string + + listener?: string + + messagePayload?: string + + file?: any + + messageType?: string + + wxid?: string + + roomid?: string + + messageId?: string + + wxAvatar?: string + + roomAvatar?: string + + listenerAvatar?: string + + // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 + + protected override mappingOptions: MappingOptions = { + // 定义字段映射选项 + fieldMapping: { + // 字段映射 + timeHms: '时间|timeHms', + name: '发送者|name', + alias: '好友备注|alias', + topic: '群名称|topic', + listener: '接收人|listener', + messagePayload: '消息内容|messagePayload', + file: '文件图片|file', + messageType: '消息类型|messageType', + wxid: '好友ID|wxid', + listenerid: '接收人ID|listenerid', + roomid: '群ID|roomid', + messageId: '消息ID|messageId', + wxAvatar: '发送者头像|wxAvatar', + roomAvatar: '群头像|roomAvatar', + listenerAvatar: '接收人头像|listenerAvatar', + }, + tableName: '消息记录|Message', // 表名 + } // 设置映射选项为上面定义的 mappingOptions + + protected override getMappingOptions (): MappingOptions { + // 获取映射选项的方法 + return this.mappingOptions // 返回当前类的映射选项 + } + + override setMappingOptions (options: MappingOptions) { + // 设置映射选项的方法 + this.mappingOptions = options // 更新当前类的映射选项 + } + +} + +const messages = new Messages() +await messages.setVikaOptions({ + apiKey: process.env.LARK_APP_ID + '/' + process.env.LARK_APP_SECRET + '/' + process.env.LARK_BITABLE_APP_TOKEN, + baseId: db.dataBaseIds.messageSheet, // 设置 base ID +}) + +// const file = await messages.upload('/Users/luyuchao/Documents/GitHub/chatflow/tools/order_20_47_20.xlsx') +// console.log('上传文件recordsAll8:', JSON.stringify(file)) + +const file = await messages.upload('/Users/luyuchao/Documents/GitHub/chatflow/data/media/image/qrcode/qrcode.png') +console.log('上传文件recordsAll8:', JSON.stringify(file)) + +const record9 = { + timeHms: new Date().toLocaleString(), + name: 'Wechaty-Puppet', + alias: 'Wechaty-Puppet', + topic: 'Wechaty-Puppet', + listener: 'Wechaty-Puppet', + file :[ file.data ], +} +const recordsAll9 = await messages.create(record9) +console.log('创建单条记录recordsAll9:', JSON.stringify(recordsAll9)) diff --git a/tests/lark.ts b/tests/lark.ts index 92e999d0..556eacbf 100644 --- a/tests/lark.ts +++ b/tests/lark.ts @@ -3,24 +3,16 @@ import 'dotenv/config.js' import * as lark from '@larksuiteoapi/node-sdk' -const appId = process.env['LARK_APP_ID'] -const appSecret = process.env['LARK_APP_SECRET'] -const app_token = process.env['LARK_BITABLE_APP_TOKEN'] || '' -const user_mobile = process.env['LARK_APP_USER_MOBILE'] || '13800000000' +const appId = process.env.LARK_APP_ID || '' +const appSecret = process.env.LARK_APP_SECRET || '' +const app_token = process.env.LARK_BITABLE_APP_TOKEN || '' +const user_mobile = process.env.LARK_APP_USER_MOBILE || '13800000000' const client = new lark.Client({ appId, appSecret, }) -// const res = await client.request({ -// method: 'GET', -// url: 'https://open.feishu.cn/open-apis/bitable/v1/apps/bascnPgZURujrdwZ9T4JkLUSUQc/tables/tblZgivmheQdZjDe/records', -// data: {}, -// params: {}, -// }) -// console.log(JSON.stringify(res.data)) - const user = await client.contact.user.batchGetId({ data:{ mobiles:[ user_mobile ] }, params:{ user_id_type:'user_id' }, diff --git a/tests/vika-orm-test.ts b/tests/vika-orm-test.ts index c6fe8034..1d7429a4 100644 --- a/tests/vika-orm-test.ts +++ b/tests/vika-orm-test.ts @@ -1,89 +1,86 @@ -import { log } from 'wechaty' // 导入 wechaty 的 log 模块 -import { BaseEntity, VikaOptions, MappingOptions } from '../src/db/vika-orm.js' // 导入 BaseEntity, VikaOptions, 和 MappingOptions 类型/类 -import 'dotenv/config.js' // 导入环境变量配置 -import { delay } from '../src/utils/utils.js' // 导入 delay 功能 -import { v4 as uuidv4 } from 'uuid' - -const vikaOptions: VikaOptions = { // 定义 Vika API 的选项 - apiKey: process.env.VIKA_TOKEN || '', // 从环境变量获取 API 密钥 - baseId: 'dstv5YCjy3PUtN882p', // 设置 base ID -} +/* eslint-disable no-console */ +import 'dotenv/config.js' +import { BiTable } from '../src/db/vika-db.js' +import { BaseEntity, MappingOptions } from '../src/db/vika-orm.js' + +const db = new BiTable() +const dbInit = await db.init({ + token: process.env.VIKA_TOKEN || '', + spaceId: process.env.VIKA_SPACE_ID || '', +}) -const mappingOptions: MappingOptions = { // 定义字段映射选项 - fieldMapping: { // 字段映射 - email: 'Email', - id: 'ID', - name: 'Name', - }, - tableName: 'users', // 表名 -} +console.log('dbInit:', dbInit) -/** - * 用户实体 - */ -class User extends BaseEntity { // 用户类继承 BaseEntity +export class Env extends BaseEntity { - name?: string // 定义名字属性,可选 - email?: string // 定义电子邮件属性,可选 - id?:string + name?: string - // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 + key?: string - protected static override mappingOptions: MappingOptions = mappingOptions // 设置映射选项为上面定义的 mappingOptions + value?: string - protected static override getMappingOptions (): MappingOptions { // 获取映射选项的方法 - return this.mappingOptions // 返回当前类的映射选项 - } - - static override setMappingOptions (options: MappingOptions) { // 设置映射选项的方法 - this.mappingOptions = options // 更新当前类的映射选项 - } + desc?: string -} + syncStatus?: string -User.setVikaOptions(vikaOptions) // 设置 Vika API 选项 + lastOperationTime?: string -// 使用示例 + action?: string -// 增 -const newUser1 = new User() // 创建一个新的用户实例 -newUser1.name = 'Bob' // 设置用户名为 Bob -newUser1.email = 'bob@example.com' // 设置用户电子邮件为 bob@example.com -newUser1.id = uuidv4() -const res = await newUser1.save() // 保存用户实例 -log.info('保存newUser1:', JSON.stringify(res)) // 输出新用户的信息 -await delay(500) // 等待 500 毫秒 - -// 增 -const newUser2 = await User.create({ // 创建一个新的用户 - email: 'test@example.com', // 用户电子邮件为 test@example.com - id:uuidv4(), - name: 'Test', // 用户名为 Test -}) + // protected static override recordId: string = '' // 定义记录ID,初始为空字符串 -log.info('保存newUser2:', JSON.stringify(newUser2)) // 输出新用户的信息 -await delay(500) // 等待 500 毫秒 + protected override mappingOptions: MappingOptions = { + // 定义字段映射选项 + fieldMapping: { + // 字段映射 + name: '配置项|name', + key: '标识|key', + value: '值|value', + desc: '说明|desc', + syncStatus: '同步状态|syncStatus', + lastOperationTime: '最后操作时间|lastOperationTime', + action: '操作|action', + }, + tableName: '环境变量|Env', // 表名 + } // 设置映射选项为上面定义的 mappingOptions + + protected override getMappingOptions (): MappingOptions { + // 获取映射选项的方法 + return this.mappingOptions // 返回当前类的映射选项 + } -// 改 -const updateRes = await User.update(newUser2.recordId, { email: 'newalice@example.com' } as Partial) // 更新新用户的电子邮件 -log.info('更新newUser2:', JSON.stringify(updateRes)) // 输出更新后的新用户信息 -await delay(500) // 等待 500 毫秒 + override setMappingOptions (options: MappingOptions) { + // 设置映射选项的方法 + this.mappingOptions = options // 更新当前类的映射选项 + } -// 查 -const queryRes = await User.findById(newUser2.recordId) -log.info('查询findById:', JSON.stringify(queryRes)) // 输出更新后的新用户信息 -await delay(500) // 等待 500 毫秒 +} -const query2Res = await User.findByField('email', 'bob@example.com') -log.info('查询findByField:', JSON.stringify(query2Res)) // 输出更新后的新用户信息 -await delay(500) // 等待 500 毫秒 +// 测试 +const env = new Env() +await env.setVikaOptions({ + apiKey: db.token, + baseId: db.dataBaseIds.envSheet, // 设置 base ID +}) -// 删 -const deleteRes = await User.delete(newUser2.recordId) // 删除新用户 -log.info('删除newUser2:', JSON.stringify(deleteRes)) // 输出更新后的新用户信息 -await delay(500) // 等待 500 毫秒 +const envData = await env.findAll() +console.log('envData:', JSON.stringify(envData, null, 2)) + +const demo = [ + { + recordId: 'rec0ofQXMfryc', + createdAt: 1694860633000, + updatedAt: 1697695907000, + fields: { + name: 'Wechaty-Puppet', + key: 'WECHATY_PUPPET', + value: 'wechaty-puppet-padlocal', + desc: '可选值:\nwechaty-puppet-wechat4u\nwechaty-puppet-wechat\nwechaty-puppet-xp\nwechaty-puppet-engine\u0000\nwechaty-puppet-padlocal\nwechaty-puppet-service', + syncStatus: '未同步', + lastOperationTime: 1694860632945, + action: '选择操作', + }, + }, +] -// 查 -const users = await User.findAll() // 查找所有用户(当前被注释掉) -log.info('查询users:', JSON.stringify(users)) // 输出新用户的信息 -await delay(500) // 等待 500 毫秒 +console.log('demo:', JSON.stringify(demo)) diff --git a/tsconfig.json b/tsconfig.json index 70355b90..b541734f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,13 +4,12 @@ "outDir": "dist/esm", "module": "esnext", "target": "es2020", + "moduleResolution": "node" }, "exclude": [ "src/**/*.mjs", "node_modules/", - "dist/", - "tests/vika-orm-test.ts", - "tests/lark.ts" + "dist/" ], "include": [ "./*.ts", @@ -20,6 +19,6 @@ "src/plugins/*.cjs", "example/*.ts", ".eslintrc.cjs", - "tests/*.ts" -, "src/proxy/weixin-chatbot-proxy.ts" ] + "tests/*.ts", + "src/proxy/weixin-chatbot-proxy.ts" ] } \ No newline at end of file