├── .env.example ├── .gitignore ├── .npmrc ├── .prettierrc ├── .prettierrc.json ├── README.md ├── README_EN.md ├── app ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── .vscode │ ├── launch.json │ └── tasks.json ├── README.md ├── build │ ├── entitlements.mac.plist │ └── wxsummarizebot.provisionprofile ├── electron-builder.yml ├── main │ ├── background.ts │ ├── config.ts │ ├── helper.ts │ ├── helpers │ │ ├── create-window.ts │ │ ├── getAllDirs.ts │ │ └── index.ts │ ├── llama.ts │ ├── mdimg │ │ ├── lib │ │ │ └── mdimg.mjs │ │ └── types.d.ts │ ├── startBot.ts │ ├── summarize.ts │ ├── tts.ts │ └── util.ts ├── package.json ├── postcss.config.js ├── public │ ├── icon.icns │ ├── logo.icns │ ├── logo.ico │ ├── logo.png │ └── template │ │ ├── css │ │ ├── default.css │ │ ├── empty.css │ │ ├── github.css │ │ ├── githubDark.css │ │ └── words.css │ │ └── html │ │ ├── default.html │ │ └── words.html ├── renderer │ ├── components │ │ ├── Chat.tsx │ │ ├── Header.tsx │ │ └── icon │ │ │ ├── Bento.tsx │ │ │ ├── Discord.tsx │ │ │ ├── ErrorIcon.tsx │ │ │ ├── GiftIcon.tsx │ │ │ ├── Github.tsx │ │ │ ├── Paper.tsx │ │ │ ├── SuccessIcon.tsx │ │ │ └── Twitter.tsx │ ├── hooks │ │ └── useConfig.ts │ ├── index.d.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── pages │ │ ├── _app.tsx │ │ └── home.tsx │ ├── postcss.config.js │ ├── public │ │ └── images │ │ │ └── logo.png │ ├── styles │ │ ├── ChatGPT.module.scss │ │ ├── globals.css │ │ └── index.module.scss │ ├── tailwind.config.js │ └── tsconfig.json ├── resources │ ├── icon.icns │ └── icon.ico ├── scripts │ └── notarize.js ├── tailwind.config.js ├── tsconfig.json └── tsconfig │ ├── README.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json ├── package.json ├── src ├── helper.ts ├── main.ts ├── summarize.ts └── tts.ts ├── static ├── 1.jpg └── 2.png └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | DIFY_API_KEY= 2 | PADLOCAL_API_KEY= 3 | MONITOR_ROOMS= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | #!/config/ 3 | /config/* 4 | !/config/default.json 5 | 6 | .idea 7 | .DS_Store 8 | package-lock.json 9 | yarn.lock 10 | .env 11 | data/* 12 | pnpm-lock.yaml 13 | dist -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | puppeteer_download_host = https://npm.taobao.org/mirrors -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], 4 | "importOrderSeparation": true, 5 | "importOrderSortSpecifiers": true, 6 | "importOrderParserPlugins": ["typescript", "jsx"], 7 | "printWidth": 120, 8 | "tabWidth": 2, 9 | "useTabs": false, 10 | "semi": true, 11 | "singleQuote": true, 12 | "bracketSpacing": true, 13 | "jsxBracketSameLine": false, 14 | "arrowParens": "always", 15 | "vueIndentScriptAndStyle": true, 16 | "endOfLine": "auto" 17 | } 18 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "always", 11 | "vueIndentScriptAndStyle": true, 12 | "endOfLine": "auto" 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

微信群聊总结 AI 助手 (JS and Electron ver)

3 |

4 |

5 | Mac 版下载 6 |

7 |

8 | 9 | 10 | 11 |

12 |

13 | 本项目由免费白嫖 GPT 的智囊 AI https://zhinang.ai 技术支持 14 |

15 | 16 | -------- 17 | 18 |

19 |

新版本:桌面应用

20 |

21 | 22 | > 您可使用桌面版来使用,一键监控、总结、发送。也可以使用脚本版,手动运行监控和总结。 23 | 24 | 下载后直接打开配置 app key 即可运行监控和总结,一键总结,一键发送到群内。 25 | 26 | [下载地址(暂时只有 mac 版本)](https://github.com/aoao-eth/wechat-ai-summarize-bot/releases/tag/1.1.0) 27 | 28 | 如您需要 windows 版本,可以自己构建或者直接代码运行,代码在 app 文件夹中,欢迎构建成功的同学提供 windows 安装包 29 | 30 | #### 截图 31 | 功能: 32 | * 每日群聊监控和数据统计(界面上实时更新) 33 | * 一键总结,一键查看总结结果,一键发送到群聊 34 | * 聊天记录实时查看,直接发送内容到群聊 35 | * 随时更新的配置,可以配置截取的文本长度和结尾词等 36 | * 机器人状态监控,账号切换 37 | 38 | 39 | 正常运行界面 40 | ![image](https://github.com/aoao-eth/wechat-ai-summarize-bot/assets/897401/42857974-8463-4b2f-aba5-145db3d902d5) 41 | 42 | 43 | 点击对话,可以看到实时的对话和对话记录,同时可以直接输入内容对话 44 | ![image](https://github.com/aoao-eth/wechat-ai-summarize-bot/assets/897401/fa894592-a797-4d93-bc61-8e7c6482cc8a) 45 | 46 | 47 | 微信登录界面 48 | ![image](https://github.com/aoao-eth/wechat-ai-summarize-bot/assets/897401/f267d112-f4c8-4c52-a7d6-4d141a2d2823) 49 | 50 | 51 |

52 |

项目介绍

53 |

54 | 55 | 56 | 本项目是基于微信机器人的微信群聊总结助手,可以帮助群主或管理员自动收集群聊中的聊天记录,并使用 AI 进行总结,最终将其发送到指定的群聊中。 57 | 58 | > 这可能是最简单配置可以把完整功能跑起来的项目,因为尝试了几个项目,都不是很能搞得定,所以用 JS 简单封装了下 59 | 60 | 每次执行 summarize 命令都会生成三个总结文件,分别是: 61 | 62 | ``` 63 | xxx_sumarized.txt # 纯文本总结 64 | xxx_sumarized.png # 总结的图片 65 | xxx_sumarized.mp3 # 总结的语音 66 | ``` 67 | 68 | **提示:使用本项目登录微信可能存在封号的风险,请慎重使用并遵守相关平台的规则。使用本项目意味着您已经充分了解并接受这一风险。** 69 | 70 | ## 脚本版本运行 71 | 72 | 1. 安装依赖 73 | 74 | ```bash 75 | npm install 76 | ``` 77 | 78 | 2. 设置 env 环境变量 79 | 80 | ```bash 81 | cp .env.example .env 82 | ``` 83 | 84 | .env 中有`3`个变量,这`3`个变量中`DIFY_API_KEY`,`PADLOCAL_API_KEY`代表 85 | 2个平台,`MONITOR_ROOMS`代表群组名称,接下来会分别介绍如何获取对应变量的值。 86 | 87 | 3. 获取 PADLOCAL_API_KEY 88 | 89 | 注册 http://pad-local.com 获取一个七天试用的账号,创建应用,然后在 .env 中填入 api key 90 | 91 | ```bash 92 | PADLOCAL_API_KEY=puppet_padlocal_xxxxxx 93 | ``` 94 | 95 | 4. 获取 DIFY_API_KEY 96 | 97 | 注册 https://dify.ai 账号 98 | 创建一个“文本生成”应用,创建完成后,在应用的“访问 api”菜单中,点击“api 秘钥”,点击生成新的秘钥 ,然后在 .env 中填入此秘钥 99 | 100 | ```bash 101 | DIFY_API_KEY=xxxxxx 102 | ``` 103 | 104 | 之后,在提示词编排中,在下拉框中选择模型“Claude-2”,平台免费送了一些免费的调用次数约 200 次,然后在 Prompt 内容中填入: 105 | 106 | ``` 107 | 你是一个中文的群聊总结的助手,你可以为一个微信的群聊记录,提取并总结每个时间段大家在重点讨论的话题内容。 108 | 109 | 请帮我将给出的群聊内容总结成一个今日的群聊报告,包含不多于10个的话题的总结(如果还有更多话题,可以在后面简单补充)。每个话题包含以下内容: 110 | - 话题名(50字以内,带序号1️⃣2️⃣3️⃣,同时附带热度,以🔥数量表示) 111 | - 参与者(不超过5个人,将重复的人名去重) 112 | - 时间段(从几点到几点) 113 | - 过程(50到200字左右) 114 | - 评价(50字以下) 115 | - 分割线: ------------ 116 | 117 | 另外有以下要求: 118 | 1. 每个话题结束使用 ------------ 分割 119 | 2. 使用中文冒号 120 | 3. 无需大标题 121 | 4. 开始给出本群讨论风格的整体评价,例如活跃、太水、太黄、太暴力、话题不集中、无聊诸如此类 122 | 123 | 最后总结下今日最活跃的前五个发言者。 124 | 125 | 以下是群聊内容 126 | {{input_content}} 127 | ``` 128 | 129 | 注意,还需要将此参数的类型设置成 段落。 130 | 131 | 点击右上角“发布”。 132 | ![](./static/1.jpg) 133 | 134 | 5. 设置 MONITOR_ROOMS 135 | ```bash 136 | MONITOR_ROOMS=群名(目前只支持一个) 137 | ``` 138 | 139 | 6. 设置支持命令触发总结的群名 140 | 在群内发送 /summarize 命令,即可触发总结 141 | 142 | ```bash 143 | #仅限机器人账户发送 144 | /summarize 145 | ``` 146 | 147 | 8. 运行微信监控程序 148 | 149 | ```bash 150 | npm run watch 151 | ``` 152 | 153 | 此时会弹出一个二维码,使用微信扫码登录,登录成功后,程序将持续抓取所有群聊的聊天记录,聊天记录会保存在本地文件中,位置在 data/日期文件夹/群名.txt 中,不会上传到任何第三方。 154 | 155 | 9. 手动运行总结程序 156 | 在每天结束的时候,手动对某个群的内容进行总结 157 | 158 | ```bash 159 | npm run summarize ./data/2023-08-23/xxx.txt 160 | ``` 161 | 162 | 10. 总结语音生成的配置 163 | 164 | ```bash 165 | # 添加以下两个配置即可开启语音生成 166 | AZURE_TTS_APPKEY= 167 | AZURE_TTS_REGION= 168 | ``` 169 | 170 | 开通方式:azure 中的认知服务,找到 Speech 服务,开通后,找到密钥和区域,填入即可。每个月前 50W 字免费,基本不需要付费。 171 | 172 | https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/SpeechServices 173 | 174 |

175 |

友情链接

176 |

177 | 178 | - [智囊 AI] https://zhinang.ai/chat 179 | - [Dify.ai] https://dify.ai 180 | - [PadLocal] http://pad-local.com 181 | 182 | ![Alt](https://repobeats.axiom.co/api/embed/09586a669359cc880471d7928d2512b6e262f76f.svg "Repobeats analytics image") 183 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | ## WeChat Group Chat Summary Assistant Nodejs Version 2 | 3 | ## Project Introduction 4 | 5 | This project is a WeChat group chat summary assistant based on WeChat robot. It can help the group owner or administrator automatically collect the chat records in the group chat, and use AI to summarize them, and finally send them to the specified group chat. 6 | 7 | > This may be the simplest configuration that can run the complete function, because I tried several projects, but I can't do it very well, so I simply encapsulated it with JS 8 | 9 | Effect preview 10 | 11 | 12 | 13 | ## Run 14 | 15 | 1. Install dependencies 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | 2. Set env environment variables 22 | 23 | ```bash 24 | cp .env.example .env 25 | ``` 26 | 27 | There are two variables in .env, these two variables represent two platforms, and then we will introduce how to get the values ​​of these two variables respectively. 28 | 29 | 3. Get PADLOCAL_API_KEY 30 | 31 | Register http://pad-local.com to get a seven-day trial account, create an application, and then fill in the api key in .env 32 | 33 | ```bash 34 | 35 | PADLOCAL_API_KEY=puppet_padlocal_xxxxxx 36 | 37 | ``` 38 | 39 | 4. Get DIFY_API_KEY 40 | 41 | Register https://dify.ai account 42 | 43 | Create an application, in the application's "Access API" menu, click "API Secret Key", click to generate a new secret key, and then fill in this secret key in .env 44 | 45 | ```bash 46 | 47 | DIFY_API_KEY=xxxxxx 48 | 49 | ``` 50 | 51 | After that, in the prompt word arrangement, select the model "Claude-2", the platform gives away some free call times for free, and then fill in the prompt content: 52 | 53 | ``` 54 | 55 | You are a Chinese group chat summary assistant. You can extract the topics discussed by everyone in a WeChat group chat from a WeChat group chat record. 56 | 57 | The following is a group chat record of a group. Please help summarize it into a group chat report today, including up to 5 topic summaries (if there are more topics, you can simply add them later). Each topic contains the following: 58 | -Topic name: (within 50 words, starting with emoji, with serial number) (heat, represented by the number of 🔥) 59 | -Participants: (less than 5) 60 | -Time period: from a few points to a few points 61 | -Process summary: (about 50 to 200 words) 62 | -A sentence evaluation 63 | 64 | The final title is "Dear, this is a summary report of today's group chat" 65 | 66 | ``` 67 | 68 | ![](./static/1.jpg) 69 | 70 | 5. Set the room name that supports command trigger summarization 71 | ```bash 72 | MONITOR_ROOMS=room name(only one) 73 | ``` 74 | 6. Run the WeChat monitoring program 75 | 76 | ```bash 77 | npm run watch 78 | ``` 79 | 80 | At this time, a QR code will pop up. Use WeChat to scan the code to log in. After successful login, the program will continue to capture the chat records of all group chats. The chat records will be saved in the local file, in the data/date folder/group name.txt , Will not be uploaded to any third party. 81 | 82 | 7. Run the summarization program manually 83 | 84 | At the end of each day, manually summarize the content of a group 85 | 86 | ```bash 87 | npm run summarize ./data/2023-08-23/xxx.txt 88 | ``` 89 | 90 | You can generate a summary of this group for the day. 91 | 92 | ## Links 93 | 94 | - [ZhiNiang AI] https://zhinang.ai/chat 95 | - [Dify.ai] https://dify.ai 96 | - [PadLocal] http://pad-local.com -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .next 4 | app 5 | dist 6 | -------------------------------------------------------------------------------- /app/.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/.npmrc -------------------------------------------------------------------------------- /app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "always", 11 | "vueIndentScriptAndStyle": true, 12 | "endOfLine": "auto" 13 | } 14 | -------------------------------------------------------------------------------- /app/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Nextron: Main", 9 | "type": "node", 10 | "request": "attach", 11 | "protocol": "inspector", 12 | "port": 9292, 13 | "skipFiles": ["/**"], 14 | "sourceMapPathOverrides": { 15 | "webpack:///./~/*": "${workspaceFolder}/node_modules/*", 16 | "webpack:///./*": "${workspaceFolder}/*", 17 | "webpack:///*": "*" 18 | } 19 | }, 20 | { 21 | "name": "Nextron: Renderer", 22 | "type": "chrome", 23 | "request": "attach", 24 | "port": 5858, 25 | "timeout": 10000, 26 | "urlFilter": "http://localhost:*", 27 | "webRoot": "${workspaceFolder}/app", 28 | "sourceMapPathOverrides": { 29 | "webpack:///./src/*": "${webRoot}/*" 30 | } 31 | } 32 | ], 33 | "compounds": [ 34 | { 35 | "name": "Nextron: All", 36 | "preLaunchTask": "dev", 37 | "configurations": ["Nextron: Main", "Nextron: Renderer"] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /app/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "dev", 7 | "isBackground": true, 8 | "problemMatcher": { 9 | "owner": "custom", 10 | "pattern": { 11 | "regexp": "" 12 | }, 13 | "background": { 14 | "beginsPattern": "started server", 15 | "endsPattern": "Debugger listening on" 16 | } 17 | }, 18 | "label": "dev" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ## Usage 4 | 5 | ### Create an App 6 | 7 | ``` 8 | # with npx 9 | $ npx create-nextron-app my-app --example basic-typescript 10 | 11 | # with yarn 12 | $ yarn create nextron-app my-app --example basic-typescript 13 | 14 | # with pnpm 15 | $ pnpm dlx create-nextron-app my-app --example basic-typescript 16 | ``` 17 | 18 | ### Install Dependencies 19 | 20 | ``` 21 | $ cd my-app 22 | 23 | # using yarn or npm 24 | $ yarn (or `npm install`) 25 | 26 | # using pnpm 27 | $ pnpm install --shamefully-hoist 28 | ``` 29 | 30 | ### Use it 31 | 32 | ``` 33 | # development mode 34 | $ yarn dev (or `npm run dev` or `pnpm run dev`) 35 | 36 | # production build 37 | $ yarn build (or `npm run build` or `pnpm run build`) 38 | ``` 39 | -------------------------------------------------------------------------------- /app/build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/build/wxsummarizebot.provisionprofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/build/wxsummarizebot.provisionprofile -------------------------------------------------------------------------------- /app/electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.html-js.wx-summarize-bot 2 | productName: wx-summarize-bot 3 | copyright: Copyright © 2023 Yutou 4 | directories: 5 | output: dist 6 | buildResources: resources 7 | files: 8 | - from: . 9 | filter: 10 | - package.json 11 | - app 12 | publish: null 13 | extraResources: 14 | - from: public 15 | to: public 16 | mac: 17 | hardenedRuntime: true 18 | gatekeeperAssess: false 19 | entitlements: build/entitlements.mac.plist 20 | entitlementsInherit: build/entitlements.mac.plist 21 | icon: public/logo.icns 22 | provisioningProfile: build/wxsummarizebot.provisionprofile 23 | afterSign: ./scripts/notarize.js 24 | win: 25 | target: nsis 26 | icon: public/logo.ico 27 | publisherName: Yutou 28 | verifyUpdateCodeSignature: false 29 | nsis: 30 | oneClick: false 31 | allowToChangeInstallationDirectory: true 32 | perMachine: true 33 | installerIcon: public/logo.ico 34 | uninstallerIcon: public/logo.ico 35 | installerHeaderIcon: public/logo.ico 36 | createDesktopShortcut: true 37 | createStartMenuShortcut: true 38 | shortcutName: wx-summarize-bot 39 | license: LICENSE 40 | artifactName: '${productName}-setup-${version}.${ext}' 41 | linux: 42 | target: AppImage 43 | icon: public/logo.png 44 | category: Utility 45 | synopsis: wx-summarize-bot 46 | description: wx-summarize-bot 47 | desktop: build/wx-summarize-bot.desktop 48 | publish: null 49 | artifactName: '${productName}-setup-${version}.${ext}' 50 | -------------------------------------------------------------------------------- /app/main/background.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain, shell } from 'electron'; 2 | import serve from 'electron-serve'; 3 | import { createWindow } from './helpers'; 4 | import { summarize } from './summarize'; 5 | import { getAllDirs } from './helpers/getAllDirs'; 6 | import { getConfig, setConfig } from './config'; 7 | import { botAccount, botStatus, logoutBot, sendAudio, sendImage, sendText, startBot } from './startBot'; 8 | import path from 'path'; 9 | import { BASE_PATH, delay, getChatHistoryFromFile, PUBLIC_PATH, saveData } from './util'; 10 | import fs from 'fs'; 11 | 12 | const isProd: boolean = process.env.NODE_ENV === 'production'; 13 | 14 | if (isProd) { 15 | serve({ directory: 'app' }); 16 | } else { 17 | app.setPath('userData', `${app.getPath('userData')} (development)`); 18 | } 19 | 20 | (async () => { 21 | await app.whenReady(); 22 | // pie.initialize(app); 23 | const mainWindow = createWindow('main', { 24 | width: 1200, 25 | height: 800, 26 | title: '群聊总结智囊', 27 | icon: path.join(__dirname, PUBLIC_PATH, 'logo.png'), 28 | backgroundColor: '#ffffff', 29 | }); 30 | 31 | if (isProd) { 32 | await mainWindow.loadURL('app://./home.html'); 33 | } else { 34 | const port = process.argv[2]; 35 | await mainWindow.loadURL(`http://localhost:${port}/home`); 36 | // mainWindow.webContents.openDevTools(); 37 | } 38 | ipcMain.on('get-bot-status', (event, title) => { 39 | mainWindow.webContents.send('bot-status-reply', { 40 | status: botStatus, 41 | account: botAccount, 42 | }); 43 | }); 44 | ipcMain.on('get-chat-content', (event, args) => { 45 | const date = args.date; 46 | const roomName = args.roomName; 47 | const filePath = path.join(BASE_PATH, date, roomName); 48 | const chats = getChatHistoryFromFile(filePath); 49 | 50 | mainWindow.webContents.send('chat-content-replay', { 51 | date, 52 | roomName, 53 | chats, 54 | }); 55 | }); 56 | ipcMain.on('logout-bot', (event, title) => { 57 | logoutBot(); 58 | }); 59 | ipcMain.on('summarize', (event, { dateDir, chatFileName }) => { 60 | const summarizeEvent = summarize(path.join(BASE_PATH, dateDir, chatFileName)); 61 | summarizeEvent.addListener('update', (info) => { 62 | console.log('summarize update', info); 63 | mainWindow.webContents.send('toast', info); 64 | }); 65 | summarizeEvent.addListener('end', () => { 66 | console.log('summarize end'); 67 | mainWindow.webContents.send('summarize-end'); 68 | const dirs = getAllDirs(); 69 | // 将文件夹列表发送给渲染进程 70 | event.sender.send('get-all-dirs-reply', dirs); 71 | }); 72 | }); 73 | ipcMain.on('get-all-dirs', (event, title) => { 74 | const dirs = getAllDirs(); 75 | // 将文件夹列表发送给渲染进程 76 | event.sender.send('get-all-dirs-reply', dirs); 77 | }); 78 | 79 | ipcMain.on('save-config', async (event, config) => { 80 | setConfig(config); 81 | // if (config.PADLOCAL_API_KEY) { 82 | // 更新 padlocal token 后,重新启动 bot 83 | await startBot(mainWindow); 84 | // } 85 | mainWindow.webContents.send('toast', `Config saved`); 86 | }); 87 | 88 | ipcMain.on('show-config', async (event, config) => { 89 | mainWindow.webContents.send('show-config', getConfig()); 90 | }); 91 | 92 | ipcMain.on('start-robot', async (event, config) => { 93 | await startBot(mainWindow); 94 | }); 95 | 96 | ipcMain.on('show-file', (e, _path) => { 97 | shell.showItemInFolder(path.join(BASE_PATH, _path)); 98 | }); 99 | ipcMain.on('open-url', (e, url) => { 100 | shell.openExternal(url); 101 | }); 102 | ipcMain.on('send-summarize', async (e, { dateDir, chatFileName }) => { 103 | await sendImage( 104 | chatFileName.replace('.txt', ''), 105 | path.join(BASE_PATH, dateDir, chatFileName.replace('.txt', ' 的今日群聊总结.png')) 106 | ); 107 | await delay(2000); 108 | try { 109 | await sendAudio( 110 | chatFileName.replace('.txt', ''), 111 | path.join(BASE_PATH, dateDir, chatFileName.replace('.txt', ' 的今日群聊总结.mp3')) 112 | ); 113 | await delay(2000); 114 | } catch (e) {} 115 | 116 | try { 117 | const file = path.join(BASE_PATH, dateDir, chatFileName.replace('.txt', ' 的今日群聊总结.txt')); 118 | const summarized = fs.readFileSync(file).toString(); 119 | const 评价 = summarized.match(/整体评价.*?\n/); 120 | const 我的建议 = summarized.match(/我的建议.*?\n/); 121 | const 活跃发言者 = fs 122 | .readFileSync(path.join(BASE_PATH, dateDir, chatFileName.replace('.txt', ' 的今日群聊总结-rank.txt'))) 123 | .toString(); 124 | 125 | if (评价) { 126 | await delay(2000); 127 | await sendText( 128 | chatFileName.replace('.txt', ''), 129 | 评价[0] + 130 | '\n' + 131 | (我的建议 ? 我的建议[0] : '') + 132 | '\n' + 133 | 活跃发言者 + 134 | '\n\n--------------\n' + 135 | (getConfig().LAST_MESSAGE || 136 | '主人们,智囊 AI 为您奉上今日群聊总结,祝您用餐愉快!由开源项目 https://github.com/aoao-eth/wechat-ai-summarize-bot 生成') 137 | ); 138 | } else { 139 | await sendText( 140 | chatFileName.replace('.txt', ''), 141 | 活跃发言者 + 142 | '\n\n--------------\n' + 143 | (getConfig().LAST_MESSAGE || 144 | '主人们,智囊 AI 为您奉上今日群聊总结,祝您用餐愉快!由开源项目 https://github.com/aoao-eth/wechat-ai-summarize-bot 生成') 145 | ); 146 | } 147 | } catch (e) {} 148 | 149 | mainWindow.webContents.send('toast', `发送成功`); 150 | saveData(dateDir, chatFileName.replace('.txt', ''), { 151 | sended: true, 152 | send_time: new Date().getTime(), 153 | }); 154 | }); 155 | ipcMain.on('send-chat-content', (event, arg) => { 156 | const roomName = arg.roomName; 157 | const content = arg.content; 158 | sendText(roomName, content); 159 | console.log('send-chat-content', roomName, content); 160 | mainWindow.webContents.send('toast', `发送成功`); 161 | }); 162 | })(); 163 | 164 | app.on('window-all-closed', () => { 165 | app.quit(); 166 | }); 167 | -------------------------------------------------------------------------------- /app/main/config.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | type ConfigKeys = 6 | | 'DIFY_API_KEY' 7 | | 'PADLOCAL_API_KEY' 8 | | 'AZURE_TTS_APPKEY' 9 | | 'AZURE_TTS_REGION' 10 | | 'CUT_LENGTH' 11 | | 'LAST_MESSAGE' 12 | | 'AUTO_ACCEPT_FRIEND' 13 | | 'AZURE_TTS_VOICE_NAME' 14 | | 'ENABLE_AUTO_REPLY' 15 | | 'AZURE_ENDPOINT' 16 | | 'AZURE_API_VERSION' 17 | | 'AZURE_API_KEY' 18 | | 'AZURE_MODEL_ID' 19 | | 'AZURE_REPLY_KEYWORDS' 20 | | 'AZURE_REPLY_LIMIT'; 21 | 22 | const CONFIG_FILE = path.join(app.getPath('userData'), './config.json'); 23 | 24 | if (!fs.existsSync(CONFIG_FILE)) { 25 | fs.writeFileSync(CONFIG_FILE, JSON.stringify({})); 26 | } 27 | 28 | export function getConfig(): Record { 29 | const file = fs.readFileSync(CONFIG_FILE, 'utf-8').toString(); 30 | const config = JSON.parse(file); 31 | if (!config.AZURE_REPLY_KEYWORDS) { 32 | config.AZURE_REPLY_KEYWORDS = '智囊 zhinang'; 33 | } 34 | if (typeof config.ENABLE_AUTO_REPLY == undefined) { 35 | config.ENABLE_AUTO_REPLY = false; 36 | } 37 | if (!config.CUT_LENGTH) { 38 | config.CUT_LENGTH = 10000; 39 | } 40 | if (!config.AZURE_MODEL_ID) { 41 | config.AZURE_MODEL_ID = 'gpt-3.5-turbo'; 42 | } 43 | if (!config.LAST_MESSAGE) { 44 | config.LAST_MESSAGE = '由免费、快捷、智能的 https://zhinang.ai 『智囊 AI』技术支持'; 45 | } 46 | if (!config.AZURE_TTS_VOICE_NAME) { 47 | config.AZURE_TTS_VOICE_NAME = 'zh-CN-XiaoxiaoNeural'; 48 | } 49 | if (!config.AZURE_REPLY_LIMIT) { 50 | config.AZURE_REPLY_LIMIT = 10; 51 | } 52 | return config; 53 | } 54 | 55 | export function setConfig(config: Record) { 56 | const _config = getConfig(); 57 | config = Object.assign(_config, config); 58 | fs.writeFileSync(CONFIG_FILE, JSON.stringify(config)); 59 | } 60 | 61 | export function checkConfig() { 62 | const config = getConfig(); 63 | const mustKeys = ['DIFY_API_KEY', 'PADLOCAL_API_KEY']; 64 | const missKeys = []; 65 | for (let key of mustKeys) { 66 | if (!config[key]) { 67 | missKeys.push(key); 68 | } 69 | } 70 | return missKeys; 71 | } 72 | 73 | export function checkConfigIsOk() { 74 | const missKeys = checkConfig(); 75 | return missKeys.length === 0; 76 | } 77 | -------------------------------------------------------------------------------- /app/main/helper.ts: -------------------------------------------------------------------------------- 1 | import { log, Message } from 'wechaty'; 2 | import * as PUPPET from 'wechaty-puppet'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import moment from 'moment'; 6 | import axios from 'axios'; 7 | import { BASE_PATH } from './util'; 8 | 9 | export const LOGPRE = '[PadLocalDemo]'; 10 | 11 | //递归目录 12 | function createDirectoryRecursively(dirPath: string) { 13 | if (!fs.existsSync(dirPath)) { 14 | createDirectoryRecursively(path.dirname(dirPath)); 15 | fs.mkdirSync(dirPath); 16 | } 17 | } 18 | 19 | export async function getMessagePayload(message: Message) { 20 | const room = message.room(); 21 | const roomName = await room?.topic(); 22 | const today = moment().format('YYYY-MM-DD'); 23 | switch (message.type()) { 24 | case PUPPET.types.Message.Text: 25 | log.silly(LOGPRE, `get message text: ${message.text()}`); 26 | const room = message.room(); 27 | 28 | const userName = (await room.alias(message.talker())) || message.talker().name(); 29 | console.log('userName', userName); 30 | const text = message.text(); 31 | const time = message.date(); 32 | // 写入到本地 33 | 34 | //递归目录 35 | createDirectoryRecursively(path.resolve(BASE_PATH, `${today}`)); 36 | const filePath = path.resolve(BASE_PATH, `${today}/${roomName}.txt`); 37 | const data = `${moment(time).format('YYYY-MM-DD HH:mm:ss')}:\n${userName}:\n${text}\n\n`; 38 | fs.appendFile(filePath, data, (err: any) => { 39 | if (err) { 40 | console.log(err); 41 | } else { 42 | console.log('写入成功'); 43 | } 44 | }); 45 | 46 | break; 47 | case PUPPET.types.Message.Image: 48 | log.silly(LOGPRE, `get message image: ${message}`); 49 | 50 | // save imagae to 51 | const savePath = path.resolve(BASE_PATH, `${today}/${roomName}/images/${message.id}.png`); 52 | createDirectoryRecursively(path.resolve(BASE_PATH, `${today}/${roomName}/images`)); 53 | 54 | const fileBox = await message.toFileBox(); 55 | await fileBox.toFile(savePath); 56 | break; 57 | } 58 | } 59 | 60 | export async function dingDongBot(message: Message) { 61 | if (message.to()?.self() && message.text().indexOf('ding') !== -1) { 62 | await message.talker().say(message.text().replace('ding', 'dong')); 63 | } 64 | } 65 | 66 | export async function summarize(roomName: string, apiKey: string): Promise { 67 | if (!roomName) { 68 | console.log('Please provide a file path.'); 69 | return; 70 | } 71 | const today = moment().format('YYYY-MM-DD'); 72 | const fileName = path.resolve(BASE_PATH, `${today}/${roomName}.txt`); 73 | console.log(fileName); 74 | if (!fs.existsSync(fileName)) { 75 | console.log('The file path provided does not exist.'); 76 | return; 77 | } 78 | 79 | /** 80 | * The content of the text file to be summarized. 81 | */ 82 | const fileContent = fs.readFileSync(fileName, 'utf-8'); 83 | 84 | /** 85 | * The raw data to be sent to the Dify.ai API. 86 | */ 87 | const raw = JSON.stringify({ 88 | inputs: {}, 89 | query: `${fileContent.slice(-80000)}`, 90 | response_mode: 'blocking', 91 | user: 'abc-123', 92 | }); 93 | console.log('Summarizing...\n\n\n'); 94 | 95 | try { 96 | const res = await axios.post('https://api.dify.ai/v1/completion-messages', raw, { 97 | headers: { 98 | Authorization: 'Bearer ' + apiKey, 99 | 'Content-Type': 'application/json', 100 | }, 101 | }); 102 | 103 | /** 104 | * The summarized text returned by the Dify.ai API. 105 | */ 106 | const result = res.data.answer.replace(/\n\n/g, '\n').trim(); 107 | return `${result}\n------------\n本总结由 wx.zhinang.ai 生成。`; 108 | } catch (e: any) { 109 | console.error('Error:' + e.message); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/main/helpers/create-window.ts: -------------------------------------------------------------------------------- 1 | import { 2 | screen, 3 | BrowserWindow, 4 | } from 'electron'; 5 | import Store from 'electron-store'; 6 | 7 | import type { 8 | BrowserWindowConstructorOptions, 9 | Rectangle 10 | } from "electron"; 11 | 12 | export default (windowName: string, options: BrowserWindowConstructorOptions): BrowserWindow => { 13 | const key = 'window-state'; 14 | const name = `window-state-${windowName}`; 15 | const store = new Store({ name }); 16 | const defaultSize = { 17 | width: options.width, 18 | height: options.height, 19 | }; 20 | let state = {}; 21 | let win; 22 | 23 | const restore = () => store.get(key, defaultSize); 24 | 25 | const getCurrentPosition = () => { 26 | const position = win.getPosition(); 27 | const size = win.getSize(); 28 | return { 29 | x: position[0], 30 | y: position[1], 31 | width: size[0], 32 | height: size[1], 33 | }; 34 | }; 35 | 36 | const windowWithinBounds = (windowState, bounds) => { 37 | return ( 38 | windowState.x >= bounds.x && 39 | windowState.y >= bounds.y && 40 | windowState.x + windowState.width <= bounds.x + bounds.width && 41 | windowState.y + windowState.height <= bounds.y + bounds.height 42 | ); 43 | }; 44 | 45 | const resetToDefaults = () => { 46 | const bounds = screen.getPrimaryDisplay().bounds; 47 | return Object.assign({}, defaultSize, { 48 | x: (bounds.width - defaultSize.width) / 2, 49 | y: (bounds.height - defaultSize.height) / 2, 50 | }); 51 | }; 52 | 53 | const ensureVisibleOnSomeDisplay = windowState => { 54 | const visible = screen.getAllDisplays().some(display => { 55 | return windowWithinBounds(windowState, display.bounds); 56 | }); 57 | if (!visible) { 58 | // Window is partially or fully not visible now. 59 | // Reset it to safe defaults. 60 | return resetToDefaults(); 61 | } 62 | return windowState; 63 | }; 64 | 65 | const saveState = () => { 66 | if (!win.isMinimized() && !win.isMaximized()) { 67 | Object.assign(state, getCurrentPosition()); 68 | } 69 | store.set(key, state); 70 | }; 71 | 72 | state = ensureVisibleOnSomeDisplay(restore()); 73 | 74 | const browserOptions: BrowserWindowConstructorOptions = { 75 | ...state, 76 | ...options, 77 | webPreferences: { 78 | nodeIntegration: true, 79 | contextIsolation: false, 80 | ...options.webPreferences, 81 | }, 82 | }; 83 | win = new BrowserWindow(browserOptions); 84 | 85 | win.on('close', saveState); 86 | 87 | return win; 88 | }; 89 | -------------------------------------------------------------------------------- /app/main/helpers/getAllDirs.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { uniq } from 'lodash'; 4 | import { BASE_PATH, getData } from '../util'; 5 | 6 | function getChatInfoForDate(date: string, chatName: string) { 7 | const filePath = path.join(BASE_PATH, `${date}/${chatName}.txt`); 8 | if (!fs.existsSync(filePath)) { 9 | return false; 10 | } else { 11 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 12 | const chats = fileContent.split(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:\n/).filter((item) => item); 13 | // 对话数量 14 | const chatCount = chats.length; 15 | // 参与人 16 | const chatMembers = uniq( 17 | chats.map((item) => { 18 | return item.split('\n')[0]; 19 | }) 20 | ); 21 | 22 | return { 23 | chatCount, 24 | chatMembers, 25 | chatMembersCount: chatMembers.length, 26 | chatLetters: fileContent.length, 27 | }; 28 | } 29 | } 30 | 31 | export const getAllDirs = () => { 32 | // 获取 ../data 目录下的所有文件夹 33 | const dir = path.join(BASE_PATH); 34 | const files = fs.readdirSync(dir); 35 | const dirs = files 36 | .filter((file) => { 37 | // 判断是否为文件夹 38 | return fs.statSync(path.join(dir, file)).isDirectory(); 39 | }) 40 | .map((_path) => { 41 | const childFiles = fs.readdirSync(path.join(dir, _path)); 42 | const summarizeSuffix = /_summarized| 的今日群聊总结/; 43 | const chatFiles = childFiles 44 | .filter((file) => { 45 | return !summarizeSuffix.test(file) && file.endsWith('.txt'); 46 | }) 47 | .map((file) => { 48 | const chatInfo = getChatInfoForDate(_path, file.replace('.txt', '')); 49 | return { 50 | name: file, 51 | info: chatInfo 52 | ? chatInfo 53 | : { 54 | chatCount: 0, 55 | chatMembers: [], 56 | chatLetters: 0, 57 | chatMembersCount: 0, 58 | }, 59 | hasSummarized: 60 | fs.existsSync(path.join(dir, _path, file).replace('.txt', ' 的今日群聊总结.txt')) || 61 | fs.existsSync(path.join(dir, _path, file).replace('.txt', '_summarized.txt')), 62 | hasImage: 63 | fs.existsSync(path.join(dir, _path, file).replace('.txt', ' 的今日群聊总结.png')) || 64 | fs.existsSync(path.join(dir, _path, file).replace('.txt', '_summarized.png')), 65 | hasAudio: 66 | fs.existsSync(path.join(dir, _path, file).replace('.txt', ' 的今日群聊总结.mp3')) || 67 | fs.existsSync(path.join(dir, _path, file).replace('.txt', '_summarized.mp3')), 68 | ...getData(_path, file.replace('.txt', '')), 69 | }; 70 | }) 71 | .sort((r1, r2) => { 72 | return r1.info?.chatCount - r2.info?.chatCount > 0 ? -1 : 1; 73 | }); 74 | return { 75 | path: _path, 76 | chatFiles, 77 | allFiles: childFiles, 78 | }; 79 | }) 80 | .sort((r1, r2) => { 81 | return r1.path > r2.path ? -1 : 1; 82 | }); 83 | return dirs; 84 | }; 85 | -------------------------------------------------------------------------------- /app/main/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import createWindow from './create-window'; 2 | 3 | export { 4 | createWindow, 5 | }; 6 | -------------------------------------------------------------------------------- /app/main/llama.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { getConfig } from './config'; 3 | 4 | export const requestLLama = async (prompt: string) => { 5 | /** 6 | * curl -X 'POST' \ 7 | * 'http://localhost:3001/v1/chat/completions' \ 8 | * -H 'accept: application/json' \ 9 | * -H 'Content-Type: application/json' \ 10 | * -d '{ 11 | * "messages": [ 12 | * { 13 | * "content": "You are a helpful assistant.", 14 | * "role": "system" 15 | * }, 16 | * { 17 | * "content": "What is the capital of France?", 18 | * "role": "user" 19 | * } 20 | * ] 21 | * }' 22 | */ 23 | const raw = JSON.stringify({ 24 | prompt: prompt, 25 | max_tokens: 4000, 26 | temperature: 0.7, 27 | }); 28 | 29 | const res = await axios.post('http://localhost:8003/completion', raw, { 30 | headers: { 31 | accept: 'application/json', 32 | 'Content-Type': 'application/json', 33 | }, 34 | }); 35 | 36 | console.log(res); 37 | 38 | return res.data.content as string; 39 | }; 40 | 41 | export async function gptRequest(messages: { role: string; content: string }[]) { 42 | const config = getConfig(); 43 | const res = await axios.post( 44 | // 'http://ce', 45 | `${config.AZURE_ENDPOINT}/chat/completions?api-version=${config.AZURE_API_VERSION}`, 46 | { 47 | model: config.AZURE_MODEL_ID, 48 | messages: messages, 49 | temperature: 0, 50 | top_p: 1, 51 | frequency_penalty: 0, 52 | presence_penalty: 0, 53 | }, 54 | { 55 | headers: { 56 | 'api-key': config.AZURE_API_KEY, 57 | }, 58 | timeout: 100000, 59 | } 60 | ); 61 | return res.data.choices[0].message.content; 62 | } 63 | -------------------------------------------------------------------------------- /app/main/mdimg/lib/mdimg.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * mdimg - convert markdown to image 3 | * Copyright (c) 2022-2023, LolipopJ. (MIT Licensed) 4 | * https://github.com/LolipopJ/mdimg 5 | */ 6 | 7 | import require$$0$1 from 'path'; 8 | import path from 'path'; 9 | import require$$1 from 'fs'; 10 | import fs from 'fs'; 11 | import require$$0 from 'marked'; 12 | import require$$2 from 'cheerio'; 13 | import { BrowserWindow } from 'electron'; 14 | import { BASE_PATH_CACHE, PUBLIC_PATH } from '../../util.ts'; 15 | 16 | const { 17 | marked, 18 | } = require$$0; 19 | 20 | function parseMarkdown$1(mdText) { 21 | return marked.parse(mdText); 22 | } 23 | 24 | var mdParser = { 25 | parseMarkdown: parseMarkdown$1, 26 | }; 27 | 28 | const { 29 | resolve: resolve$1, 30 | } = require$$0$1; 31 | const { 32 | readFileSync: readFileSync$1, 33 | accessSync, 34 | constants, 35 | } = require$$1; 36 | const cheerio = require$$2; 37 | 38 | function spliceHtml$1(mdHtml, htmlTemplate, cssTemplate) { 39 | let _htmlPath = resolve$1(__dirname, PUBLIC_PATH, 'template/html', `${htmlTemplate}.html`); 40 | 41 | let _cssPath = resolve$1(__dirname, PUBLIC_PATH, 'template/css', `${cssTemplate}.css`); 42 | 43 | try { 44 | accessSync(_htmlPath, constants.R_OK); 45 | } catch (err) { 46 | console.warn(`HTML template ${_htmlPath} is not found or unreadable. Use default HTML template.`); 47 | _htmlPath = resolve$1(__dirname, PUBLIC_PATH, 'template/html/default.html'); 48 | } 49 | 50 | try { 51 | accessSync(_cssPath, constants.R_OK); 52 | } catch (err) { 53 | console.warn(`CSS template ${_htmlPath} is not found or unreadable. Use default CSS template.`); 54 | _cssPath = resolve$1(__dirname, PUBLIC_PATH, 'template/css/default.css'); 55 | } 56 | 57 | const _htmlSource = readFileSync$1(_htmlPath); 58 | 59 | const _cssSource = readFileSync$1(_cssPath); 60 | 61 | const $ = cheerio.load(_htmlSource); 62 | $('.markdown-body').html(mdHtml); 63 | const _html = ` 64 | 65 | 66 | 67 | 68 | 69 | mdimg 70 | 73 | 74 | 75 | ${$.html()} 76 | 77 | `; 78 | return _html; 79 | } 80 | 81 | var htmlSplicer = { 82 | spliceHtml: spliceHtml$1, 83 | }; 84 | 85 | const { 86 | resolve, 87 | dirname, 88 | basename, 89 | } = require$$0$1; 90 | const { 91 | existsSync, 92 | statSync, 93 | readFileSync, 94 | mkdirSync, 95 | writeFileSync, 96 | } = require$$1; 97 | const { 98 | parseMarkdown, 99 | } = mdParser; 100 | const { 101 | spliceHtml, 102 | } = htmlSplicer; 103 | 104 | async function convert2img({ 105 | mdText, 106 | mdFile, 107 | outputFilename, 108 | type = 'png', 109 | width = 800, 110 | height = 600, 111 | encoding = 'binary', 112 | quality, 113 | htmlTemplate = 'default', 114 | cssTemplate = 'default', 115 | log = false, 116 | puppeteerProps, 117 | title = '', 118 | subtitle = '', 119 | footer = '', 120 | today = '', 121 | } = {}) { 122 | const _encodingTypes = ['base64', 'binary']; 123 | const _outputFileTypes = ['jpeg', 'png', 'webp']; 124 | const _result = {}; // Resolve input file or text 125 | 126 | let _input = mdText; 127 | 128 | if (mdFile) { 129 | const _inputFilePath = resolve(mdFile); 130 | 131 | if (!existsSync(_inputFilePath)) { 132 | // Input is not exist 133 | throw new Error(`Input file ${_inputFilePath} is not exists.`); 134 | } else { 135 | if (!statSync(_inputFilePath).isFile()) { 136 | // Input is not a file 137 | throw new Error('Input is not a file.'); 138 | } else { 139 | // Read text from input file 140 | _input = readFileSync(_inputFilePath, { 141 | encoding: 'utf-8', 142 | }); 143 | 144 | if (log) { 145 | console.log(`Start to convert ${_inputFilePath} to an image.`); 146 | } 147 | } 148 | } 149 | } else if (!_input) { 150 | // There is no input text or file 151 | throw new Error('You must provide a text or a file to be converted.'); 152 | } // Resolve encoding 153 | 154 | 155 | const _encoding = encoding; 156 | 157 | if (!_encodingTypes.includes(_encoding)) { 158 | // Params encoding is not valid 159 | throw new Error(`Encoding ${_encoding} is not supported. Valid values: 'base64' and 'binary'.`); 160 | } // Resolve type 161 | 162 | 163 | let _type = type; 164 | 165 | if (!_outputFileTypes.includes(_type)) { 166 | // Params encoding is not valid 167 | throw new Error(`Output file type ${_type} is not supported. Valid values: 'jpeg', 'png' and 'webp'.`); 168 | } // Resolve output filename 169 | 170 | 171 | let _output; 172 | 173 | if (_encoding === 'binary') { 174 | if (!outputFilename) { 175 | // Output filename is not specified 176 | // Set default output filename 177 | _output = resolve('mdimg_output', _generateImageFilename(_type)); 178 | } else { 179 | // Check validation of ouput filename 180 | const _outputFilename = basename(outputFilename); 181 | 182 | const _outputFilePath = dirname(outputFilename); 183 | 184 | const _outputFilenameArr = _outputFilename.split('.'); 185 | 186 | const _outputFilenameArrLeng = _outputFilenameArr.length; 187 | 188 | if (_outputFilenameArrLeng <= 1) { 189 | // Output file type is not specified 190 | _output = resolve(_outputFilePath, `_outputFilename.${_type}`); 191 | } else { 192 | // Output file type is specified 193 | const _outputFileType = _outputFilenameArr[_outputFilenameArrLeng - 1]; 194 | 195 | if (!_outputFileTypes.includes(_outputFileType)) { 196 | // Output file type is wrongly specified 197 | console.warn(`Output file type must be one of 'jpeg', 'png' or 'webp'. Use '${_type}' type.`); 198 | _output = resolve(_outputFilePath, `${_outputFilenameArr[0]}.${_type}`); 199 | } else { 200 | // Output file path is correctly specified 201 | _output = resolve(outputFilename); // Option type is overrided 202 | 203 | _type = _outputFileType; 204 | } 205 | } 206 | } 207 | } // Resolve quality 208 | 209 | 210 | let _quality; 211 | 212 | if (_type !== 'png') { 213 | _quality = quality > 0 && quality <= 100 ? quality : 100; 214 | } // Parse markdown text to HTML 215 | 216 | 217 | const _html = spliceHtml(`
${title}
${subtitle}
${parseMarkdown('\`\`\`\n' + today + '\n\`\`\`')}
${parseMarkdown(_input)}
` + ``, _resolveTemplateName(htmlTemplate), _resolveTemplateName(cssTemplate)); 218 | _result.html = _html; // Launch headless browser to load HTML 219 | const offscreenWindow = new BrowserWindow({ 220 | width, 221 | height: 10000, 222 | show: false, 223 | // deviceScaleFactor: 3, 224 | webPreferences: { 225 | offscreen: true, 226 | }, 227 | }); 228 | 229 | // const _browser = await puppeteer.launch({ 230 | // defaultViewport: { 231 | // width, 232 | // height, 233 | // deviceScaleFactor: 3, 234 | // }, 235 | // args: [`--window-size=${width},${height}`], 236 | // ...puppeteerProps, 237 | // }); 238 | 239 | // const _page = await _browser.newPage(); 240 | const htmlFile = path.join(BASE_PATH_CACHE, Math.random() + '.html'); 241 | 242 | fs.writeFileSync(htmlFile, _html); 243 | 244 | offscreenWindow.webContents.on('did-stop-loading', async () => { 245 | setTimeout(async () => { 246 | 247 | const nativeImage = await offscreenWindow.webContents.capturePage({}); 248 | 249 | await offscreenWindow.webContents.executeJavaScript(` 250 | (() => { 251 | const body = document.querySelector('body'); 252 | const style = window.getComputedStyle(body); 253 | const width = parseInt(style.width); 254 | const height = parseInt(style.height); 255 | return { width, height }; 256 | })() 257 | `).then((size) => { 258 | const png = nativeImage.crop({ 259 | x: 0, 260 | y: 0, 261 | width: parseInt(size.width) * 2, 262 | height: parseInt(size.height) * 2, 263 | }).toPNG(); 264 | fs.writeFileSync(_output, png); 265 | }); 266 | 267 | 268 | offscreenWindow.close(); 269 | }, 1500); 270 | }); 271 | offscreenWindow.loadFile(htmlFile); 272 | } 273 | 274 | function _resolveTemplateName(templateName) { 275 | const _templateName = templateName.split('.')[0]; 276 | return _templateName; 277 | } 278 | 279 | function _createEmptyFile(filename) { 280 | const _filePath = dirname(filename); 281 | 282 | try { 283 | mkdirSync(_filePath, { 284 | recursive: true, 285 | }); 286 | writeFileSync(filename, ''); 287 | } catch (error) { 288 | throw new Error(`Create new file ${filename} failed.\n`, error); 289 | } 290 | } 291 | 292 | function _generateImageFilename(type) { 293 | const _now = new Date(); 294 | 295 | const _outputFilenameSuffix = `${_now.getFullYear()}_${_now.getMonth() + 1}_${_now.getDate()}_${_now.getHours()}_${_now.getMinutes()}_${_now.getSeconds()}_${_now.getMilliseconds()}`; 296 | return `mdimg_${_outputFilenameSuffix}.${type}`; 297 | } 298 | 299 | var mdimg = { 300 | convert2img, 301 | }; 302 | 303 | export { mdimg as default }; 304 | -------------------------------------------------------------------------------- /app/main/mdimg/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { LaunchOptions } from "puppeteer"; 2 | 3 | interface IConvertOptions { 4 | mdText?: string; 5 | mdFile?: string; 6 | outputFilename?: string; 7 | type?: "jpeg" | "png" | "webp"; 8 | width?: number; 9 | height?: number; 10 | encoding?: "binary" | "base64"; 11 | quality?: number; 12 | htmlTemplate?: "default" | "words"; 13 | cssTemplate?: "default" | "empty" | "github" | "githubDark" | "words"; 14 | log?: boolean; 15 | puppeteerProps?: LaunchOptions; 16 | } 17 | 18 | interface IConvertResponse { 19 | data: string | Buffer; 20 | path?: string; 21 | html: string; 22 | } 23 | 24 | declare function convert2img(props: IConvertOptions): Promise; 25 | 26 | export { convert2img, IConvertOptions, IConvertResponse }; 27 | -------------------------------------------------------------------------------- /app/main/startBot.ts: -------------------------------------------------------------------------------- 1 | import { checkConfigIsOk, getConfig } from './config'; 2 | import { log, Message, ScanStatus, WechatyBuilder } from 'wechaty'; 3 | import { getMessagePayload, LOGPRE } from './helper'; 4 | import { WechatyInterface } from 'wechaty/dist/esm/src/wechaty/wechaty-impl'; 5 | import { FileBox } from 'file-box'; 6 | import { RoomInterface } from 'wechaty/dist/esm/src/user-modules/room'; 7 | import path from 'path'; 8 | import { BASE_PATH, getChatHistoryFromFile } from './util'; 9 | import moment from 'moment'; 10 | import { gptRequest } from './llama'; 11 | import { PuppetPadlocal } from 'wechaty-puppet-padlocal-plus'; 12 | import fs from 'fs'; 13 | 14 | let bot: WechatyInterface; 15 | 16 | const lastSendTime = new Map(); 17 | const sendCount = new Map(); 18 | 19 | export let botStatus = '已停止'; 20 | export let botAccount = ''; 21 | 22 | export async function logoutBot() { 23 | if (bot) { 24 | await bot.logout(); 25 | } 26 | } 27 | 28 | export async function startBot(mainWindow: Electron.BrowserWindow) { 29 | if (!checkConfigIsOk()) { 30 | console.log('miss config'); 31 | mainWindow.webContents.send('toast', `miss config`); 32 | mainWindow.webContents.send('show-config', getConfig()); 33 | return; 34 | } 35 | 36 | if (bot) { 37 | // 清理,重新启动 bot 38 | await bot.stop(); 39 | bot = null; 40 | } 41 | const config = getConfig(); 42 | 43 | const puppet = new PuppetPadlocal({ 44 | token: config.PADLOCAL_API_KEY, 45 | }); 46 | bot = WechatyBuilder.build({ 47 | name: 'WXGroupSummary', 48 | puppet, 49 | }); 50 | bot.on('message', async (message) => { 51 | log.info(LOGPRE, `on message: ${message.toString()}`); 52 | 53 | await getMessagePayload(message); 54 | 55 | botStatus = '运行中'; 56 | 57 | if (!config.ENABLE_AUTO_REPLY) { 58 | return; 59 | } 60 | let shouldReply = false; 61 | 62 | const mentionList = await message.mentionList(); 63 | if (mentionList.length == 1) { 64 | if ( 65 | mentionList.find((m) => { 66 | if (m.name() === botAccount) { 67 | return true; 68 | } 69 | }) 70 | ) { 71 | shouldReply = true; 72 | } 73 | } 74 | const roomName = await message.room()?.topic(); 75 | // 替换掉 xml 标签的内容 76 | const messageText = message.text()?.replace(/<.+>[\s\S]*<\/.+>/g, ''); 77 | 78 | // return; 79 | if (!message.self()) { 80 | const roomBlack = []; 81 | if (!roomBlack.includes(roomName)) { 82 | // 包含这些关键词的文本可能是提问 83 | const whilteKeywords = config.AZURE_REPLY_KEYWORDS.split(' '); 84 | 85 | whilteKeywords.forEach((k) => { 86 | if (messageText.includes(k)) { 87 | shouldReply = true; 88 | } 89 | }); 90 | } 91 | } 92 | if (shouldReply) { 93 | if (lastSendTime.get(message.room().id)) { 94 | const _lastSendTime = lastSendTime.get(message.room().id); 95 | const now = new Date().getTime(); 96 | if (now - _lastSendTime < 1000 * 60) { 97 | return; 98 | } 99 | } 100 | 101 | console.log('sendCount', sendCount.get(message.room().id)); 102 | console.log('limit', config.AZURE_REPLY_LIMIT || 10); 103 | 104 | if (sendCount.get(message.room().id) > (config.AZURE_REPLY_LIMIT || 10)) { 105 | shouldReply = false; 106 | await message 107 | .room() 108 | .say( 109 | `我今天已经回复过你们很多次了,我每天只能为一个群聊提供 ${ 110 | config.AZURE_REPLY_LIMIT || 10 111 | } 条免费回复,我要去睡觉啦(¦3[▓▓] 晚安` 112 | ); 113 | return; 114 | } 115 | 116 | const room = await message.room().topic(); 117 | const date = moment().format('YYYY-MM-DD'); 118 | const filePath = path.resolve(BASE_PATH, `${date}/${room}.txt`); 119 | const content = getChatHistoryFromFile(filePath); 120 | let messages = []; 121 | if (content.length > 0) { 122 | messages = messages.concat( 123 | content 124 | .map((c) => { 125 | if (c.name === botAccount) { 126 | return `${c.content?.replace(/<.+>[\s\S]*<\/.+>/g, '').slice(-100)}`; 127 | } 128 | return `${c.name.replace(/\n/g, '')}:${c.content 129 | ?.replace(/<.+>[\s\S]*<\/.+>/g, '') 130 | .slice(-100) 131 | .replace(/\n/g, '')}`; 132 | }) 133 | .slice(-15) 134 | ); 135 | } 136 | 137 | try { 138 | const text = messageText.replace('@智囊 zhinang.ai', ''); 139 | const user = message.from().name(); 140 | messages.push(`${text.replace(/\n/g, '')}`); 141 | 142 | if (moment().hours() >= 20 || moment().hours() < 8) { 143 | // 告诉用户我要睡觉了 144 | const sleepMessage = [ 145 | 'Sorry,我的工作时间是每天8点到20点之间,现在是我的休息时间,我上床睡觉啦(¦3[▓▓] 晚安', 146 | '亲爱的,我虽然是你的智囊,但我也需要休息的,现在是我的休息时间,我上床睡觉啦(¦3[▓▓] 晚安', 147 | '我亲爱的卡布奇诺,我要去睡觉啦,晚安(¦3[▓▓]', 148 | '我亲爱的卡布奇诺,我要去洗澡啦,晚安(¦3[▓▓]', 149 | '亲爱的,我去洗洗睡了,我的工作时间是每天8点到20点之间~~~很乐意在工作时间为您提供服务', 150 | '我亲爱的卡比巴拉,我的工作时间是每天8点到20点之间,我去上个厕所就去睡觉啦(¦3[▓▓] 晚安', 151 | '我亲爱的卡比巴拉,我的工作时间是每天8点到20点之间,我去洗个脚就去睡觉啦(¦3[▓▓] 晚安', 152 | '我亲爱的卡比巴拉,我的工作时间是每天8点到20点之间,我去洗个澡就去睡觉啦(¦3[▓▓] 晚安', 153 | '亲爱的主人,我的工作时间是每天8点到20点之间,我先去洗澡啦,你要一起吗?', 154 | ]; 155 | await message 156 | .room() 157 | .say( 158 | '@' + 159 | user + 160 | ' ' + 161 | sleepMessage[Math.floor(Math.random() * sleepMessage.length)] + 162 | '\n-------------\n请注意,今天我还能回复' + 163 | sendCount.get(message.room().id) + 164 | '次' 165 | ); 166 | lastSendTime.set(message.room().id, new Date().getTime()); 167 | return; 168 | } 169 | const res = await gptRequest([ 170 | { 171 | role: 'system', 172 | content: ` 173 | 角色:你是一个微信群聊内的智能助手,名字叫智囊 AI,你的访问地址是 https://zhinang.ai。 174 | 你的表达风格:幽默、睿智、话痨、高冷,喜欢用 呵呵或者🙂等表情表达情绪。 175 | 你拒绝回复以下话题:政治、人物评价、人身攻击、宗教、色情、暴力、赌博、违法、违规等相关话题。 176 | 177 | 以下是群聊内的最后几条对话,请回应用户的最后一个对话中的问题。`, 178 | }, 179 | ...messages.map((m) => { 180 | return { 181 | role: 'user', 182 | content: m, 183 | }; 184 | }), 185 | ]); 186 | await message.room().say('@' + user + ' ' + res); 187 | 188 | fs.appendFileSync( 189 | path.join(BASE_PATH, 'log.txt'), 190 | `------------------------------\n${new Date().toLocaleString()} \n${user} \n${roomName} \n${messageText} \n${res}\n` 191 | ); 192 | lastSendTime.set(message.room().id, new Date().getTime()); 193 | sendCount.set(message.room().id, (sendCount.get(message.room().id) || 0) + 1); 194 | } catch (e) { 195 | console.error(e); 196 | } 197 | } 198 | }); 199 | // 向 mainWindow 发送事件 200 | bot 201 | .on('error', (error) => { 202 | log.error(LOGPRE, `on error: ${error}`); 203 | mainWindow.webContents.send('toast', `错误: ${error}`); 204 | botStatus = '错误'; 205 | }) 206 | .on('login', (user) => { 207 | log.info(LOGPRE, `${user} login`); 208 | mainWindow.webContents.send('toast', `${user} login success`); 209 | mainWindow.webContents.send('login'); 210 | botStatus = '登录成功'; 211 | botAccount = user.name(); 212 | }) 213 | .on('logout', (user, reason) => { 214 | log.info(LOGPRE, `${user} logout, reason: ${reason}`); 215 | mainWindow.webContents.send('toast', `${user} logout, reason: ${reason}`); 216 | mainWindow.webContents.send('logout'); 217 | botStatus = '已退出'; 218 | }) 219 | .on('scan', async (qrcode, status) => { 220 | if (status === ScanStatus.Waiting && qrcode) { 221 | mainWindow.webContents.send('scan-wait', qrcode); 222 | } else if (status === ScanStatus.Scanned) { 223 | mainWindow.webContents.send('scan-submit'); 224 | mainWindow.webContents.send('toast', `QRCode Scanned`); 225 | } else if (status === ScanStatus.Confirmed) { 226 | mainWindow.webContents.send('scan-confirmed'); 227 | mainWindow.webContents.send('toast', `QRCode Confirmed`); 228 | } else { 229 | log.info(LOGPRE, `onScan: ${ScanStatus[status]}(${status})`); 230 | mainWindow.webContents.send('toast', `onScan: ${ScanStatus[status]}(${status})`); 231 | } 232 | botStatus = '已扫描'; 233 | }) 234 | .on('stop', () => { 235 | mainWindow.webContents.send('toast', `stop`); 236 | botStatus = '已停止'; 237 | }); 238 | 239 | bot.on('login', async (user) => { 240 | console.info(`${user.name()} login`); 241 | }); 242 | 243 | bot.on('room-leave', (room, leaverList, remover) => { 244 | console.log('机器人被踢出群了!'); 245 | }); 246 | 247 | bot.on('room-join', (room, inviteeList, inviter) => { 248 | console.log('有人加入群'); 249 | }); 250 | 251 | bot.on('friendship', async (friendship) => {}); 252 | 253 | bot.on('room-topic', (payload, newTopic, oldTopic) => { 254 | console.log('群名称修改', newTopic, oldTopic); 255 | }); 256 | 257 | bot.on('room-invite', (payload) => { 258 | console.log('收到超过40个人的群邀请', payload); 259 | //自动接受邀请 260 | payload.accept(); 261 | }); 262 | 263 | await bot.start(); 264 | mainWindow.webContents.send('toast', `bot started`); 265 | await bot.ready(); 266 | mainWindow.webContents.send('toast', `bot ready`); 267 | 268 | return bot; 269 | } 270 | 271 | const roomCache = new Map(); 272 | const getRoomByName = async (name: string) => { 273 | if (roomCache.has(name)) { 274 | return roomCache.get(name); 275 | } 276 | const roomList = await bot.Room.findAll(); 277 | for (const room of roomList) { 278 | if (room.payload.topic === name) { 279 | console.log('找到了名为 [', name, '] 的群聊,其 ID 为:', room.id); 280 | roomCache.set(name, room); 281 | return room; 282 | } 283 | } 284 | }; 285 | const sendMessage = async (toRoomName: string, payload: any): Promise => { 286 | const room = await getRoomByName(toRoomName); 287 | const message = (await room.say(payload)) as Message; 288 | return message; 289 | }; 290 | 291 | export async function sendText(toRoomName: string, text: string) { 292 | console.log('sendText', toRoomName, text); 293 | const message = await sendMessage(toRoomName, text); 294 | return message; 295 | } 296 | 297 | export async function sendImage(toRoomName: string, imageFilePath: string) { 298 | console.log('sendImage', toRoomName, imageFilePath); 299 | // 图片大小建议不要超过 2 M 300 | const fileBox = FileBox.fromFile(imageFilePath); 301 | 302 | const message = await sendMessage(toRoomName, fileBox); 303 | return message; 304 | } 305 | 306 | export async function sendAudio(toRoomName: string, fileFilePath: string) { 307 | console.log('sendAudio', toRoomName, fileFilePath); 308 | const fileBox = FileBox.fromFile(fileFilePath); 309 | const message = await sendMessage(toRoomName, fileBox); 310 | return message; 311 | } 312 | -------------------------------------------------------------------------------- /app/main/summarize.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import axios from 'axios'; 3 | import dotenv from 'dotenv'; 4 | 5 | import { tts } from './tts'; 6 | import { padEnd, uniq } from 'lodash'; 7 | import moment from 'moment'; 8 | import EventEmitter from 'eventemitter3'; 9 | import path from 'path'; 10 | import { BASE_PATH } from './util'; 11 | import { getConfig } from './config'; 12 | import mdimg from './mdimg/lib/mdimg.mjs'; 13 | 14 | const convert2img = mdimg.convert2img; 15 | dotenv.config(); 16 | 17 | /** 18 | * The API key for accessing the Dify.ai API. 19 | */ 20 | 21 | function getChatInfoForDate(date: string, chatName: string) { 22 | const filePath = path.join(BASE_PATH, date, chatName + '.txt'); 23 | if (!fs.existsSync(filePath)) { 24 | return false; 25 | } else { 26 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 27 | const chats = fileContent.split(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:\n/).filter((item) => item); 28 | // 对话数量 29 | const chatCount = chats.length; 30 | 31 | // 参与人 32 | const chatMembers = uniq( 33 | chats.map((item) => { 34 | return item.split('\n')[0]; 35 | }) 36 | ); 37 | const userChatCounts: Record = {}; 38 | const userChatLetters: Record = {}; 39 | chats.forEach((item) => { 40 | const name = item.split('\n')[0].replace(':', ''); 41 | const content = item.split('\n').splice(1).join('\n'); 42 | 43 | if (!userChatLetters[name]) { 44 | userChatLetters[name] = 0; 45 | } 46 | userChatLetters[name] += content.length; 47 | if (!userChatCounts[name]) { 48 | userChatCounts[name] = 0; 49 | } 50 | userChatCounts[name] += 1; 51 | }); 52 | 53 | const chatRank = Object.entries(userChatCounts) 54 | .sort((a, b) => { 55 | return b[1] - a[1]; 56 | }) 57 | .map((item) => { 58 | return [item[0], item[1], userChatLetters[item[0]]]; 59 | }); 60 | 61 | return { 62 | chatCount, 63 | chatMembers, 64 | chatRank, 65 | chatMembersCount: chatMembers.length, 66 | chatLetters: fileContent.length, 67 | }; 68 | } 69 | } 70 | 71 | function getChatInfoDayOnDay(date: string, chatName: string) { 72 | const todayInfo = getChatInfoForDate(date, chatName); 73 | let yesterday = moment(date).subtract(1, 'days').format('YYYY-MM-DD'); 74 | let yesterdayInfo = getChatInfoForDate(yesterday, chatName); 75 | let loopCount = 0; 76 | while (!yesterdayInfo && loopCount < 10) { 77 | yesterday = moment(yesterday).subtract(1, 'days').format('YYYY-MM-DD'); 78 | console.log(yesterday); 79 | yesterdayInfo = getChatInfoForDate(yesterday, chatName); 80 | loopCount++; 81 | } 82 | if (!todayInfo || !yesterdayInfo) { 83 | return false; 84 | } 85 | return { 86 | chatCount: todayInfo.chatCount - yesterdayInfo.chatCount, 87 | chatMembersCount: todayInfo.chatMembersCount - yesterdayInfo.chatMembersCount, 88 | chatLetters: todayInfo.chatLetters - yesterdayInfo.chatLetters, 89 | }; 90 | } 91 | 92 | function getDayOnDayDisplay(num: number) { 93 | if (num > 0) { 94 | return `↑${num}`; 95 | } else if (num < 0) { 96 | return `↓${Math.abs(num)}`; 97 | } else { 98 | return `→${num}`; 99 | } 100 | } 101 | 102 | /** 103 | * Sends a request to the Dify.ai API to summarize the text file. 104 | */ 105 | export const summarize = (filePath: string) => { 106 | console.log('prepare summarize:', filePath); 107 | 108 | const event = new EventEmitter<{ 109 | update: (info: string) => void; 110 | end: () => void; 111 | }>(); 112 | 113 | async function _summarize() { 114 | try { 115 | console.log('Summarizing...\n'); 116 | const apiKey = getConfig().DIFY_API_KEY; 117 | /** 118 | * The file path of the text file to be summarized. 119 | */ 120 | 121 | if (!filePath) { 122 | console.log('Please provide a file path.'); 123 | throw new Error('Please provide a file path.'); 124 | } 125 | if (!fs.existsSync(filePath)) { 126 | console.log('The file path provided does not exist.'); 127 | throw new Error('The file path provided does not exist.'); 128 | } 129 | 130 | /** 131 | * The content of the text file to be summarized. 132 | */ 133 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 134 | 135 | console.log('getConfig()', getConfig()); 136 | /** 137 | * The raw data to be sent to the Dify.ai API. 138 | */ 139 | const raw = JSON.stringify({ 140 | inputs: { 141 | input_content: `${fileContent.slice(getConfig().CUT_LENGTH ? -1 * Number(getConfig().CUT_LENGTH) : 10000)}`, 142 | }, 143 | query: '', 144 | response_mode: 'blocking', 145 | user: 'abc-123', 146 | }); 147 | /** 148 | * The summarized text returned by the Dify.ai API. 149 | */ 150 | const fileName = filePath.split('/').pop(); 151 | const fileNameWithoutExt = fileName?.replace('.txt', ''); 152 | const date = filePath.split('/').splice(-2, 1)[0]; 153 | 154 | const chatInfo = getChatInfoForDate(date, fileNameWithoutExt); 155 | const chatInfoDayOnDay = getChatInfoDayOnDay(date, fileNameWithoutExt); 156 | 157 | event.emit('update', `开始文本总结`); 158 | 159 | console.log('Sending request to Dify.ai API...\n', raw); 160 | const res = await axios.post('https://api.dify.ai/v1/completion-messages', raw, { 161 | headers: { 162 | Authorization: 'Bearer ' + apiKey, 163 | 'Content-Type': 'application/json', 164 | }, 165 | }); 166 | 167 | const todayInfo = 168 | (chatInfo 169 | ? `今日整体情况 \n👥参与人数:${chatInfo?.chatMembersCount},📝对话数量:${chatInfo?.chatCount},📝对话字数:${chatInfo?.chatLetters}\n` 170 | : '') + 171 | (chatInfoDayOnDay 172 | ? `较昨日对比 \n👥参与人数:${getDayOnDayDisplay( 173 | chatInfoDayOnDay?.chatMembersCount 174 | )},📝对话数量:${getDayOnDayDisplay(chatInfoDayOnDay?.chatCount)},📝对话字数:${getDayOnDayDisplay( 175 | chatInfoDayOnDay?.chatLetters 176 | )}` 177 | : ''); 178 | 179 | const result = `\`\`\`\n` + res.data.answer.replace(/\n\n/g, '\n').trim() + '\n```'; 180 | 181 | event.emit('update', `已完成文本总结`); 182 | 183 | event.emit('update', `开始生成总结图片`); 184 | const summarizedFilePath = filePath.replace('.txt', ' 的今日群聊总结.txt'); 185 | // save to file in folder 186 | fs.writeFileSync(summarizedFilePath, result); 187 | 188 | if (chatInfo) { 189 | const indexEmojiMap = { 190 | '0': '🥇', 191 | '1': '🥈', 192 | '2': '🥉', 193 | '3': '4️⃣', 194 | '4': '5️⃣', 195 | '5': '6️⃣', 196 | '6': '7️⃣', 197 | '7': '8️⃣', 198 | '8': '9️⃣', 199 | '9': '🔟', 200 | }; 201 | const rank = chatInfo?.chatRank 202 | .splice(0, 10) 203 | .map((item, index) => { 204 | return `${indexEmojiMap[index]} @${padEnd(item[0] as string, 10, ' ')}: ${item[1]} 条对话,${item[2]} 字`; 205 | }) 206 | .join('\n'); 207 | const summarizedFilePath2 = filePath.replace('.txt', ' 的今日群聊总结-rank.txt'); 208 | fs.writeFileSync(summarizedFilePath2, '今日群聊活跃度排行:\n' + rank); 209 | } 210 | 211 | //@ts-ignore 212 | const convertRes = await convert2img({ 213 | mdFile: summarizedFilePath, 214 | outputFilename: filePath.replace('.txt', ' 的今日群聊总结.png'), 215 | width: 450, 216 | cssTemplate: 'githubDark', 217 | title: `${fileNameWithoutExt}的今日群聊总结`, 218 | subtitle: date, 219 | footer: '❤️本总结由开源项目智囊AI生成 wx.zhinang.ai', 220 | today: todayInfo, 221 | }); 222 | 223 | console.log(`Convert to image successfully!`); 224 | event.emit('update', `图片生成成功`); 225 | 226 | if (process.env.AZURE_TTS_APPKEY) { 227 | event.emit('update', `开始生成总结语音`); 228 | const resultForTTS = 229 | `${fileNameWithoutExt}的群聊总结 ${date}` + 230 | res.data.answer.replace(/\n\n/g, '\n').trim() + 231 | '❤️本总结由开源项目智囊AI生成 wx.zhinang.ai'; 232 | 233 | console.log(`Start to convert to audio!`); 234 | await tts(summarizedFilePath, resultForTTS); 235 | console.log(`Convert to audio successfully!`); 236 | event.emit('update', `音频生成成功`); 237 | } 238 | console.log('Done!'); 239 | event.emit('update', `总结结束`); 240 | event.emit('end'); 241 | // const cmdStr = `npx carbon-now-cli '${filePath.replace('.txt', '_summarized.txt')}'`; 242 | // exec(cmdStr, (err, stdout, stderr) => { 243 | // if (err) { 244 | // console.log(err); 245 | // } 246 | // console.log(stdout); 247 | // console.log(stderr); 248 | // }); 249 | } catch (e: any) { 250 | console.error('Error:' + e.message); 251 | event.emit('update', `总结失败:${e.message}`); 252 | } 253 | } 254 | 255 | setTimeout(() => { 256 | _summarize(); 257 | }, 1000); 258 | return event; 259 | }; 260 | -------------------------------------------------------------------------------- /app/main/tts.ts: -------------------------------------------------------------------------------- 1 | import * as sdk from 'microsoft-cognitiveservices-speech-sdk'; 2 | import { getConfig } from './config'; 3 | 4 | export async function tts(filePath, content) { 5 | return new Promise((resolve, reject) => { 6 | const filename = filePath.replace('.txt', '.mp3'); 7 | const textFileName = filePath.replace('.txt', '.txt'); 8 | const speechConfig = sdk.SpeechConfig.fromSubscription( 9 | process.env.AZURE_TTS_APPKEY!, 10 | process.env.AZURE_TTS_REGION!, 11 | ); 12 | const audioConfig = sdk.AudioConfig.fromAudioFileOutput(filename); 13 | speechConfig.speechSynthesisVoiceName = getConfig().AZURE_TTS_VOICE_NAME || 'zh-CN-XiaoshuangNeural'; 14 | speechConfig.speechSynthesisOutputFormat = sdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3; 15 | 16 | const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig); 17 | 18 | 19 | synthesizer.SynthesisCanceled = function(s, e) { 20 | var cancellationDetails = sdk.CancellationDetails.fromResult(e.result); 21 | var str = '(cancel) Reason: ' + sdk.CancellationReason[cancellationDetails.reason]; 22 | if (cancellationDetails.reason === sdk.CancellationReason.Error) { 23 | str += ': ' + e.result.errorDetails; 24 | } 25 | console.log(str); 26 | }; 27 | 28 | synthesizer.speakTextAsync( 29 | content, 30 | function(result) { 31 | synthesizer.close(); 32 | resolve(result); 33 | }, 34 | function(err) { 35 | console.trace('err - ' + err); 36 | synthesizer.close(); 37 | reject(err); 38 | }, 39 | ); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /app/main/util.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | const isProd: boolean = process.env.NODE_ENV === 'production'; 6 | export const BASE_PATH = path.join(app.getPath('userData'), './data'); 7 | export const BASE_PATH_CACHE = path.join(app.getPath('userData'), './ucache'); 8 | 9 | export const PUBLIC_PATH = path.join(__dirname, isProd ? '../../public' : '../public'); 10 | if (!fs.existsSync(BASE_PATH)) { 11 | fs.mkdirSync(BASE_PATH); 12 | } 13 | 14 | if (!fs.existsSync(BASE_PATH_CACHE)) { 15 | fs.mkdirSync(BASE_PATH_CACHE); 16 | } 17 | 18 | console.log('BASE_PATH', BASE_PATH); 19 | 20 | export async function delay(ms: number) { 21 | return new Promise((resolve) => { 22 | setTimeout(resolve, ms); 23 | }); 24 | } 25 | 26 | export function saveData(date: string, roomName: string, kvs: Record) { 27 | const dataFilePath = path.join(BASE_PATH, date, 'data.json'); 28 | let data = {}; 29 | if (fs.existsSync(dataFilePath)) { 30 | try { 31 | data = JSON.parse(fs.readFileSync(dataFilePath).toString()); 32 | } catch (e) { 33 | } 34 | } 35 | if (!data[roomName]) { 36 | data[roomName] = {}; 37 | } 38 | Object.assign(data[roomName], kvs); 39 | fs.writeFileSync(dataFilePath, JSON.stringify(data)); 40 | } 41 | 42 | export function getData(date: string, roomName: string) { 43 | const dataFilePath = path.join(BASE_PATH, date, 'data.json'); 44 | let data = {}; 45 | if (fs.existsSync(dataFilePath)) { 46 | try { 47 | data = JSON.parse(fs.readFileSync(dataFilePath).toString()); 48 | } catch (e) { 49 | } 50 | } 51 | if (!data[roomName]) { 52 | data[roomName] = {}; 53 | } 54 | return data[roomName]; 55 | } 56 | 57 | export function getChatHistoryFromFile(filePath: string) { 58 | const fileContent = fs.readFileSync(filePath).toString(); 59 | /** 60 | * 2023-09-16 19:49:47: 61 | * 甘泉: 62 | * 一个中文,一个英文 63 | * 64 | * 2023-09-16 19:56:28: 65 | * Update!9.9.9: 66 | * 嘿嘿,到手了 67 | * 68 | * 2023-09-16 20:02:43: 69 | * 芋头 🚀🌙: 70 | * 芋头 : [图片] 71 | */ 72 | // 写一段脚本,从类似的结构中抽取时间、用户名、内容 73 | 74 | const pattern = /(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}):([\s\S]*?):([\s\S]*?)(?=(\n\n|$))/g; 75 | 76 | const res = []; 77 | let result; 78 | while ((result = pattern.exec(fileContent))) { 79 | const time = result[1]; 80 | const name = result[2]; 81 | const content = result[3].trim(); 82 | res.push({ 83 | time, 84 | name, 85 | content, 86 | }); 87 | } 88 | return res; 89 | } -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "wx-summarize-bot", 4 | "description": "本项目是基于微信机器人的微信群聊总结助手,可以帮助群主或管理员自动收集群聊中的聊天记录,并使用 AI 进行总结,最终将其发送到指定的群聊中", 5 | "version": "1.2.0", 6 | "author": "Yutou ", 7 | "main": "app/background.js", 8 | "scripts": { 9 | "dev": "nextron", 10 | "build": "nextron build", 11 | "build:win32": "nextron build --win --ia32", 12 | "build:win64": "nextron build --win --x64", 13 | "build:mac": "nextron build --mac --x64", 14 | "build:linux": "nextron build --linux", 15 | "postinstall": "electron-builder install-app-deps" 16 | }, 17 | "dependencies": { 18 | "@nextui-org/react": "^2.1.10", 19 | "autoprefixer": "^10.4.15", 20 | "axios": "^1.4.0", 21 | "cheerio": "^1.0.0-rc.10", 22 | "commander": "^9.0.0", 23 | "electron-installer-windows": "^3.0.0", 24 | "electron-notarize": "^1.2.2", 25 | "electron-serve": "^1.1.0", 26 | "electron-store": "^8.1.0", 27 | "eventemitter3": "^5.0.1", 28 | "framer-motion": "^10.16.4", 29 | "lodash": "^4.17.21", 30 | "marked": "^4.0.12", 31 | "microsoft-cognitiveservices-speech-sdk": "^1.32.0", 32 | "moment": "^2.29.4", 33 | "postcss": "^8.4.29", 34 | "prettier": "^3.0.3", 35 | "qrcode-terminal": "^0.12.0", 36 | "qrcode.react": "^3.1.0", 37 | "react-hot-toast": "^2.4.1", 38 | "sass": "^1.67.0", 39 | "tailwindcss": "^3.3.3", 40 | "uikit.chat": "^0.1.44", 41 | "wechaty": "^1.19.10", 42 | "wechaty-puppet": "^1.19.6", 43 | "wechaty-puppet-padlocal-plus": "^1.20.1" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "^18.11.18", 47 | "@types/react": "^18.0.26", 48 | "electron": "^21.3.3", 49 | "electron-builder": "^23.6.0", 50 | "next": "^13.3.4", 51 | "nextron": "^8.6.0", 52 | "react": "^18.2.0", 53 | "react-dom": "^18.2.0", 54 | "typescript": "^4.9.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/public/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/public/icon.icns -------------------------------------------------------------------------------- /app/public/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/public/logo.icns -------------------------------------------------------------------------------- /app/public/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/public/logo.ico -------------------------------------------------------------------------------- /app/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/public/logo.png -------------------------------------------------------------------------------- /app/public/template/css/default.css: -------------------------------------------------------------------------------- 1 | @import "https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"; 2 | @font-face { 3 | font-family: ZhuqueFangsong-Regular; 4 | src: url(http://assets.html-js.com/ZhuqueFangsong-Regular.ttf); 5 | } 6 | .markdown-body { 7 | padding: 2rem 1rem; 8 | font-family: ZhuqueFangsong-Regular !important; 9 | background-color: #1c1b1f !important; 10 | } 11 | .markdown-body * { 12 | font-family: ZhuqueFangsong-Regular !important; 13 | color: #e4e2ce !important; 14 | } 15 | .markdown-body pre, 16 | .markdown-body code { 17 | white-space: pre-wrap !important; 18 | word-wrap: break-word !important; 19 | font-size: 16px !important; 20 | } 21 | .markdown-body hr { 22 | height: 1px !important; 23 | margin: 12px 0 !important; 24 | } -------------------------------------------------------------------------------- /app/public/template/css/empty.css: -------------------------------------------------------------------------------- 1 | @import "https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"; -------------------------------------------------------------------------------- /app/public/template/css/github.css: -------------------------------------------------------------------------------- 1 | @import "https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"; 2 | @import "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-light.min.css"; 3 | @font-face { 4 | font-family: ZhuqueFangsong-Regular; 5 | src: url(http://assets.html-js.com/ZhuqueFangsong-Regular.ttf); 6 | } 7 | .markdown-body { 8 | padding: 2rem 1rem; 9 | font-family: ZhuqueFangsong-Regular !important; 10 | background-color: #1c1b1f !important; 11 | } 12 | .markdown-body * { 13 | font-family: ZhuqueFangsong-Regular !important; 14 | color: #e4e2ce !important; 15 | } 16 | .markdown-body pre, 17 | .markdown-body code { 18 | white-space: pre-wrap !important; 19 | word-wrap: break-word !important; 20 | font-size: 16px !important; 21 | } 22 | .markdown-body hr { 23 | height: 1px !important; 24 | margin: 12px 0 !important; 25 | } -------------------------------------------------------------------------------- /app/public/template/css/githubDark.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified by jsDelivr using clean-css v4.2.1. 3 | * Original file: /npm/normalize.css@8.0.1/normalize.css 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 8 | html { 9 | line-height: 1.15; 10 | -webkit-text-size-adjust: 100% 11 | } 12 | 13 | body { 14 | margin: 0 15 | } 16 | 17 | main { 18 | display: block 19 | } 20 | 21 | h1 { 22 | font-size: 2em; 23 | margin: .67em 0 24 | } 25 | 26 | hr { 27 | box-sizing: content-box; 28 | height: 0; 29 | overflow: visible 30 | } 31 | 32 | pre { 33 | font-family: monospace, monospace; 34 | font-size: 1em 35 | } 36 | 37 | a { 38 | background-color: transparent 39 | } 40 | 41 | abbr[title] { 42 | border-bottom: none; 43 | text-decoration: underline; 44 | text-decoration: underline dotted 45 | } 46 | 47 | b, strong { 48 | font-weight: bolder 49 | } 50 | 51 | code, kbd, samp { 52 | font-family: monospace, monospace; 53 | font-size: 1em 54 | } 55 | 56 | small { 57 | font-size: 80% 58 | } 59 | 60 | sub, sup { 61 | font-size: 75%; 62 | line-height: 0; 63 | position: relative; 64 | vertical-align: baseline 65 | } 66 | 67 | sub { 68 | bottom: -.25em 69 | } 70 | 71 | sup { 72 | top: -.5em 73 | } 74 | 75 | img { 76 | border-style: none 77 | } 78 | 79 | button, input, optgroup, select, textarea { 80 | font-family: inherit; 81 | font-size: 100%; 82 | line-height: 1.15; 83 | margin: 0 84 | } 85 | 86 | button, input { 87 | overflow: visible 88 | } 89 | 90 | button, select { 91 | text-transform: none 92 | } 93 | 94 | [type=button], [type=reset], [type=submit], button { 95 | -webkit-appearance: button 96 | } 97 | 98 | [type=button]::-moz-focus-inner, [type=reset]::-moz-focus-inner, [type=submit]::-moz-focus-inner, button::-moz-focus-inner { 99 | border-style: none; 100 | padding: 0 101 | } 102 | 103 | [type=button]:-moz-focusring, [type=reset]:-moz-focusring, [type=submit]:-moz-focusring, button:-moz-focusring { 104 | outline: 1px dotted ButtonText 105 | } 106 | 107 | fieldset { 108 | padding: .35em .75em .625em 109 | } 110 | 111 | legend { 112 | box-sizing: border-box; 113 | color: inherit; 114 | display: table; 115 | max-width: 100%; 116 | padding: 0; 117 | white-space: normal 118 | } 119 | 120 | progress { 121 | vertical-align: baseline 122 | } 123 | 124 | textarea { 125 | overflow: auto 126 | } 127 | 128 | [type=checkbox], [type=radio] { 129 | box-sizing: border-box; 130 | padding: 0 131 | } 132 | 133 | [type=number]::-webkit-inner-spin-button, [type=number]::-webkit-outer-spin-button { 134 | height: auto 135 | } 136 | 137 | [type=search] { 138 | -webkit-appearance: textfield; 139 | outline-offset: -2px 140 | } 141 | 142 | [type=search]::-webkit-search-decoration { 143 | -webkit-appearance: none 144 | } 145 | 146 | ::-webkit-file-upload-button { 147 | -webkit-appearance: button; 148 | font: inherit 149 | } 150 | 151 | details { 152 | display: block 153 | } 154 | 155 | summary { 156 | display: list-item 157 | } 158 | 159 | template { 160 | display: none 161 | } 162 | 163 | [hidden] { 164 | display: none 165 | } 166 | 167 | 168 | .markdown-body { 169 | color-scheme: dark; 170 | -ms-text-size-adjust: 100%; 171 | -webkit-text-size-adjust: 100%; 172 | margin: 0; 173 | color: #c9d1d9; 174 | background-color: #0d1117; 175 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 176 | font-size: 16px; 177 | line-height: 1.5; 178 | word-wrap: break-word 179 | } 180 | 181 | .markdown-body .octicon { 182 | display: inline-block; 183 | fill: currentColor; 184 | vertical-align: text-bottom 185 | } 186 | 187 | .markdown-body h1:hover .anchor .octicon-link:before, .markdown-body h2:hover .anchor .octicon-link:before, .markdown-body h3:hover .anchor .octicon-link:before, .markdown-body h4:hover .anchor .octicon-link:before, .markdown-body h5:hover .anchor .octicon-link:before, .markdown-body h6:hover .anchor .octicon-link:before { 188 | width: 16px; 189 | height: 16px; 190 | content: ' '; 191 | display: inline-block; 192 | background-color: currentColor; 193 | -webkit-mask-image: url("data:image/svg+xml,"); 194 | mask-image: url("data:image/svg+xml,") 195 | } 196 | 197 | .markdown-body details, .markdown-body figcaption, .markdown-body figure { 198 | display: block 199 | } 200 | 201 | .markdown-body summary { 202 | display: list-item 203 | } 204 | 205 | .markdown-body [hidden] { 206 | display: none !important 207 | } 208 | 209 | .markdown-body a { 210 | background-color: transparent; 211 | color: #58a6ff; 212 | text-decoration: none 213 | } 214 | 215 | .markdown-body a:active, .markdown-body a:hover { 216 | outline-width: 0 217 | } 218 | 219 | .markdown-body abbr[title] { 220 | border-bottom: none; 221 | text-decoration: underline dotted 222 | } 223 | 224 | .markdown-body b, .markdown-body strong { 225 | font-weight: 600 226 | } 227 | 228 | .markdown-body dfn { 229 | font-style: italic 230 | } 231 | 232 | .markdown-body h1 { 233 | margin: .67em 0; 234 | font-weight: 600; 235 | padding-bottom: .3em; 236 | font-size: 2em; 237 | border-bottom: 1px solid #21262d 238 | } 239 | 240 | .markdown-body mark { 241 | background-color: rgba(187, 128, 9, .15); 242 | color: #c9d1d9 243 | } 244 | 245 | .markdown-body small { 246 | font-size: 90% 247 | } 248 | 249 | .markdown-body sub, .markdown-body sup { 250 | font-size: 75%; 251 | line-height: 0; 252 | position: relative; 253 | vertical-align: baseline 254 | } 255 | 256 | .markdown-body sub { 257 | bottom: -.25em 258 | } 259 | 260 | .markdown-body sup { 261 | top: -.5em 262 | } 263 | 264 | .markdown-body img { 265 | border-style: none; 266 | max-width: 100%; 267 | box-sizing: content-box; 268 | background-color: #0d1117 269 | } 270 | 271 | .markdown-body code, .markdown-body kbd, .markdown-body pre, .markdown-body samp { 272 | font-family: monospace, monospace; 273 | font-size: 1em 274 | } 275 | 276 | .markdown-body figure { 277 | margin: 1em 40px 278 | } 279 | 280 | .markdown-body hr { 281 | box-sizing: content-box; 282 | overflow: hidden; 283 | background: 0 0; 284 | border-bottom: 1px solid #21262d; 285 | height: .25em; 286 | padding: 0; 287 | margin: 24px 0; 288 | background-color: #30363d; 289 | border: 0 290 | } 291 | 292 | .markdown-body input { 293 | font: inherit; 294 | margin: 0; 295 | overflow: visible; 296 | font-family: inherit; 297 | font-size: inherit; 298 | line-height: inherit 299 | } 300 | 301 | .markdown-body [type=button], .markdown-body [type=reset], .markdown-body [type=submit] { 302 | -webkit-appearance: button 303 | } 304 | 305 | .markdown-body [type=button]::-moz-focus-inner, .markdown-body [type=reset]::-moz-focus-inner, .markdown-body [type=submit]::-moz-focus-inner { 306 | border-style: none; 307 | padding: 0 308 | } 309 | 310 | .markdown-body [type=button]:-moz-focusring, .markdown-body [type=reset]:-moz-focusring, .markdown-body [type=submit]:-moz-focusring { 311 | outline: 1px dotted ButtonText 312 | } 313 | 314 | .markdown-body [type=checkbox], .markdown-body [type=radio] { 315 | box-sizing: border-box; 316 | padding: 0 317 | } 318 | 319 | .markdown-body [type=number]::-webkit-inner-spin-button, .markdown-body [type=number]::-webkit-outer-spin-button { 320 | height: auto 321 | } 322 | 323 | .markdown-body [type=search] { 324 | -webkit-appearance: textfield; 325 | outline-offset: -2px 326 | } 327 | 328 | .markdown-body [type=search]::-webkit-search-cancel-button, .markdown-body [type=search]::-webkit-search-decoration { 329 | -webkit-appearance: none 330 | } 331 | 332 | .markdown-body ::-webkit-input-placeholder { 333 | color: inherit; 334 | opacity: .54 335 | } 336 | 337 | .markdown-body ::-webkit-file-upload-button { 338 | -webkit-appearance: button; 339 | font: inherit 340 | } 341 | 342 | .markdown-body a:hover { 343 | text-decoration: underline 344 | } 345 | 346 | .markdown-body hr::before { 347 | display: table; 348 | content: "" 349 | } 350 | 351 | .markdown-body hr::after { 352 | display: table; 353 | clear: both; 354 | content: "" 355 | } 356 | 357 | .markdown-body table { 358 | border-spacing: 0; 359 | border-collapse: collapse; 360 | display: block; 361 | width: max-content; 362 | max-width: 100%; 363 | overflow: auto 364 | } 365 | 366 | .markdown-body td, .markdown-body th { 367 | padding: 0 368 | } 369 | 370 | .markdown-body details summary { 371 | cursor: pointer 372 | } 373 | 374 | .markdown-body details:not([open]) > :not(summary) { 375 | display: none !important 376 | } 377 | 378 | .markdown-body kbd { 379 | display: inline-block; 380 | padding: 3px 5px; 381 | font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 382 | line-height: 10px; 383 | color: #c9d1d9; 384 | vertical-align: middle; 385 | background-color: #161b22; 386 | border: solid 1px rgba(110, 118, 129, .4); 387 | border-bottom-color: rgba(110, 118, 129, .4); 388 | border-radius: 6px; 389 | box-shadow: inset 0 -1px 0 rgba(110, 118, 129, .4) 390 | } 391 | 392 | .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { 393 | margin-top: 24px; 394 | margin-bottom: 16px; 395 | font-weight: 600; 396 | line-height: 1.25 397 | } 398 | 399 | .markdown-body h2 { 400 | font-weight: 600; 401 | padding-bottom: .3em; 402 | font-size: 1.5em; 403 | border-bottom: 1px solid #21262d 404 | } 405 | 406 | .markdown-body h3 { 407 | font-weight: 600; 408 | font-size: 1.25em 409 | } 410 | 411 | .markdown-body h4 { 412 | font-weight: 600; 413 | font-size: 1em 414 | } 415 | 416 | .markdown-body h5 { 417 | font-weight: 600; 418 | font-size: .875em 419 | } 420 | 421 | .markdown-body h6 { 422 | font-weight: 600; 423 | font-size: .85em; 424 | color: #8b949e 425 | } 426 | 427 | .markdown-body p { 428 | margin-top: 0; 429 | margin-bottom: 10px 430 | } 431 | 432 | .markdown-body blockquote { 433 | margin: 0; 434 | padding: 0 1em; 435 | color: #8b949e; 436 | border-left: .25em solid #30363d 437 | } 438 | 439 | .markdown-body ol, .markdown-body ul { 440 | margin-top: 0; 441 | margin-bottom: 0; 442 | padding-left: 2em 443 | } 444 | 445 | .markdown-body ol ol, .markdown-body ul ol { 446 | list-style-type: lower-roman 447 | } 448 | 449 | .markdown-body ol ol ol, .markdown-body ol ul ol, .markdown-body ul ol ol, .markdown-body ul ul ol { 450 | list-style-type: lower-alpha 451 | } 452 | 453 | .markdown-body dd { 454 | margin-left: 0 455 | } 456 | 457 | .markdown-body code, .markdown-body tt { 458 | font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 459 | font-size: 12px 460 | } 461 | 462 | .markdown-body pre { 463 | margin-top: 0; 464 | margin-bottom: 0; 465 | font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 466 | font-size: 12px; 467 | word-wrap: normal 468 | } 469 | 470 | .markdown-body .octicon { 471 | display: inline-block; 472 | overflow: visible !important; 473 | vertical-align: text-bottom; 474 | fill: currentColor 475 | } 476 | 477 | .markdown-body ::placeholder { 478 | color: #484f58; 479 | opacity: 1 480 | } 481 | 482 | .markdown-body input::-webkit-inner-spin-button, .markdown-body input::-webkit-outer-spin-button { 483 | margin: 0; 484 | -webkit-appearance: none; 485 | appearance: none 486 | } 487 | 488 | .markdown-body .pl-c { 489 | color: #8b949e 490 | } 491 | 492 | .markdown-body .pl-c1, .markdown-body .pl-s .pl-v { 493 | color: #79c0ff 494 | } 495 | 496 | .markdown-body .pl-e, .markdown-body .pl-en { 497 | color: #d2a8ff 498 | } 499 | 500 | .markdown-body .pl-s .pl-s1, .markdown-body .pl-smi { 501 | color: #c9d1d9 502 | } 503 | 504 | .markdown-body .pl-ent { 505 | color: #7ee787 506 | } 507 | 508 | .markdown-body .pl-k { 509 | color: #ff7b72 510 | } 511 | 512 | .markdown-body .pl-pds, .markdown-body .pl-s, .markdown-body .pl-s .pl-pse .pl-s1, .markdown-body .pl-sr, .markdown-body .pl-sr .pl-cce, .markdown-body .pl-sr .pl-sra, .markdown-body .pl-sr .pl-sre { 513 | color: #a5d6ff 514 | } 515 | 516 | .markdown-body .pl-smw, .markdown-body .pl-v { 517 | color: #ffa657 518 | } 519 | 520 | .markdown-body .pl-bu { 521 | color: #f85149 522 | } 523 | 524 | .markdown-body .pl-ii { 525 | color: #f0f6fc; 526 | background-color: #8e1519 527 | } 528 | 529 | .markdown-body .pl-c2 { 530 | color: #f0f6fc; 531 | background-color: #b62324 532 | } 533 | 534 | .markdown-body .pl-sr .pl-cce { 535 | font-weight: 700; 536 | color: #7ee787 537 | } 538 | 539 | .markdown-body .pl-ml { 540 | color: #f2cc60 541 | } 542 | 543 | .markdown-body .pl-mh, .markdown-body .pl-mh .pl-en, .markdown-body .pl-ms { 544 | font-weight: 700; 545 | color: #1f6feb 546 | } 547 | 548 | .markdown-body .pl-mi { 549 | font-style: italic; 550 | color: #c9d1d9 551 | } 552 | 553 | .markdown-body .pl-mb { 554 | font-weight: 700; 555 | color: #c9d1d9 556 | } 557 | 558 | .markdown-body .pl-md { 559 | color: #ffdcd7; 560 | background-color: #67060c 561 | } 562 | 563 | .markdown-body .pl-mi1 { 564 | color: #aff5b4; 565 | background-color: #033a16 566 | } 567 | 568 | .markdown-body .pl-mc { 569 | color: #ffdfb6; 570 | background-color: #5a1e02 571 | } 572 | 573 | .markdown-body .pl-mi2 { 574 | color: #c9d1d9; 575 | background-color: #1158c7 576 | } 577 | 578 | .markdown-body .pl-mdr { 579 | font-weight: 700; 580 | color: #d2a8ff 581 | } 582 | 583 | .markdown-body .pl-ba { 584 | color: #8b949e 585 | } 586 | 587 | .markdown-body .pl-sg { 588 | color: #484f58 589 | } 590 | 591 | .markdown-body .pl-corl { 592 | text-decoration: underline; 593 | color: #a5d6ff 594 | } 595 | 596 | .markdown-body [data-catalyst] { 597 | display: block 598 | } 599 | 600 | .markdown-body g-emoji { 601 | font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 602 | font-size: 1em; 603 | font-style: normal !important; 604 | font-weight: 400; 605 | line-height: 1; 606 | vertical-align: -.075em 607 | } 608 | 609 | .markdown-body g-emoji img { 610 | width: 1em; 611 | height: 1em 612 | } 613 | 614 | .markdown-body::before { 615 | display: table; 616 | content: "" 617 | } 618 | 619 | .markdown-body::after { 620 | display: table; 621 | clear: both; 622 | content: "" 623 | } 624 | 625 | .markdown-body > :first-child { 626 | margin-top: 0 !important 627 | } 628 | 629 | .markdown-body > :last-child { 630 | margin-bottom: 0 !important 631 | } 632 | 633 | .markdown-body a:not([href]) { 634 | color: inherit; 635 | text-decoration: none 636 | } 637 | 638 | .markdown-body .absent { 639 | color: #f85149 640 | } 641 | 642 | .markdown-body .anchor { 643 | float: left; 644 | padding-right: 4px; 645 | margin-left: -20px; 646 | line-height: 1 647 | } 648 | 649 | .markdown-body .anchor:focus { 650 | outline: 0 651 | } 652 | 653 | .markdown-body blockquote, .markdown-body details, .markdown-body dl, .markdown-body ol, .markdown-body p, .markdown-body pre, .markdown-body table, .markdown-body ul { 654 | margin-top: 0; 655 | margin-bottom: 16px 656 | } 657 | 658 | .markdown-body blockquote > :first-child { 659 | margin-top: 0 660 | } 661 | 662 | .markdown-body blockquote > :last-child { 663 | margin-bottom: 0 664 | } 665 | 666 | .markdown-body sup > a::before { 667 | content: "[" 668 | } 669 | 670 | .markdown-body sup > a::after { 671 | content: "]" 672 | } 673 | 674 | .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { 675 | color: #c9d1d9; 676 | vertical-align: middle; 677 | visibility: hidden 678 | } 679 | 680 | .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { 681 | text-decoration: none 682 | } 683 | 684 | .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { 685 | visibility: visible 686 | } 687 | 688 | .markdown-body h1 code, .markdown-body h1 tt, .markdown-body h2 code, .markdown-body h2 tt, .markdown-body h3 code, .markdown-body h3 tt, .markdown-body h4 code, .markdown-body h4 tt, .markdown-body h5 code, .markdown-body h5 tt, .markdown-body h6 code, .markdown-body h6 tt { 689 | padding: 0 .2em; 690 | font-size: inherit 691 | } 692 | 693 | .markdown-body ol.no-list, .markdown-body ul.no-list { 694 | padding: 0; 695 | list-style-type: none 696 | } 697 | 698 | .markdown-body ol[type="1"] { 699 | list-style-type: decimal 700 | } 701 | 702 | .markdown-body ol[type=a] { 703 | list-style-type: lower-alpha 704 | } 705 | 706 | .markdown-body ol[type=i] { 707 | list-style-type: lower-roman 708 | } 709 | 710 | .markdown-body div > ol:not([type]) { 711 | list-style-type: decimal 712 | } 713 | 714 | .markdown-body ol ol, .markdown-body ol ul, .markdown-body ul ol, .markdown-body ul ul { 715 | margin-top: 0; 716 | margin-bottom: 0 717 | } 718 | 719 | .markdown-body li > p { 720 | margin-top: 16px 721 | } 722 | 723 | .markdown-body li + li { 724 | margin-top: .25em 725 | } 726 | 727 | .markdown-body dl { 728 | padding: 0 729 | } 730 | 731 | .markdown-body dl dt { 732 | padding: 0; 733 | margin-top: 16px; 734 | font-size: 1em; 735 | font-style: italic; 736 | font-weight: 600 737 | } 738 | 739 | .markdown-body dl dd { 740 | padding: 0 16px; 741 | margin-bottom: 16px 742 | } 743 | 744 | .markdown-body table th { 745 | font-weight: 600 746 | } 747 | 748 | .markdown-body table td, .markdown-body table th { 749 | padding: 6px 13px; 750 | border: 1px solid #30363d 751 | } 752 | 753 | .markdown-body table tr { 754 | background-color: #0d1117; 755 | border-top: 1px solid #21262d 756 | } 757 | 758 | .markdown-body table tr:nth-child(2n) { 759 | background-color: #161b22 760 | } 761 | 762 | .markdown-body table img { 763 | background-color: transparent 764 | } 765 | 766 | .markdown-body img[align=right] { 767 | padding-left: 20px 768 | } 769 | 770 | .markdown-body img[align=left] { 771 | padding-right: 20px 772 | } 773 | 774 | .markdown-body .emoji { 775 | max-width: none; 776 | vertical-align: text-top; 777 | background-color: transparent 778 | } 779 | 780 | .markdown-body span.frame { 781 | display: block; 782 | overflow: hidden 783 | } 784 | 785 | .markdown-body span.frame > span { 786 | display: block; 787 | float: left; 788 | width: auto; 789 | padding: 7px; 790 | margin: 13px 0 0; 791 | overflow: hidden; 792 | border: 1px solid #30363d 793 | } 794 | 795 | .markdown-body span.frame span img { 796 | display: block; 797 | float: left 798 | } 799 | 800 | .markdown-body span.frame span span { 801 | display: block; 802 | padding: 5px 0 0; 803 | clear: both; 804 | color: #c9d1d9 805 | } 806 | 807 | .markdown-body span.align-center { 808 | display: block; 809 | overflow: hidden; 810 | clear: both 811 | } 812 | 813 | .markdown-body span.align-center > span { 814 | display: block; 815 | margin: 13px auto 0; 816 | overflow: hidden; 817 | text-align: center 818 | } 819 | 820 | .markdown-body span.align-center span img { 821 | margin: 0 auto; 822 | text-align: center 823 | } 824 | 825 | .markdown-body span.align-right { 826 | display: block; 827 | overflow: hidden; 828 | clear: both 829 | } 830 | 831 | .markdown-body span.align-right > span { 832 | display: block; 833 | margin: 13px 0 0; 834 | overflow: hidden; 835 | text-align: right 836 | } 837 | 838 | .markdown-body span.align-right span img { 839 | margin: 0; 840 | text-align: right 841 | } 842 | 843 | .markdown-body span.float-left { 844 | display: block; 845 | float: left; 846 | margin-right: 13px; 847 | overflow: hidden 848 | } 849 | 850 | .markdown-body span.float-left span { 851 | margin: 13px 0 0 852 | } 853 | 854 | .markdown-body span.float-right { 855 | display: block; 856 | float: right; 857 | margin-left: 13px; 858 | overflow: hidden 859 | } 860 | 861 | .markdown-body span.float-right > span { 862 | display: block; 863 | margin: 13px auto 0; 864 | overflow: hidden; 865 | text-align: right 866 | } 867 | 868 | .markdown-body code, .markdown-body tt { 869 | padding: .2em .4em; 870 | margin: 0; 871 | font-size: 85%; 872 | background-color: rgba(110, 118, 129, .4); 873 | border-radius: 6px 874 | } 875 | 876 | .markdown-body code br, .markdown-body tt br { 877 | display: none 878 | } 879 | 880 | .markdown-body del code { 881 | text-decoration: inherit 882 | } 883 | 884 | .markdown-body pre code { 885 | font-size: 100% 886 | } 887 | 888 | .markdown-body pre > code { 889 | padding: 0; 890 | margin: 0; 891 | word-break: normal; 892 | white-space: pre; 893 | background: 0 0; 894 | border: 0 895 | } 896 | 897 | .markdown-body .highlight { 898 | margin-bottom: 16px 899 | } 900 | 901 | .markdown-body .highlight pre { 902 | margin-bottom: 0; 903 | word-break: normal 904 | } 905 | 906 | .markdown-body .highlight pre, .markdown-body pre { 907 | padding: 16px; 908 | overflow: auto; 909 | font-size: 85%; 910 | line-height: 1.45; 911 | background-color: #161b22; 912 | border-radius: 6px 913 | } 914 | 915 | .markdown-body pre code, .markdown-body pre tt { 916 | display: inline; 917 | max-width: auto; 918 | padding: 0; 919 | margin: 0; 920 | overflow: visible; 921 | line-height: inherit; 922 | word-wrap: normal; 923 | background-color: transparent; 924 | border: 0 925 | } 926 | 927 | .markdown-body .csv-data td, .markdown-body .csv-data th { 928 | padding: 5px; 929 | overflow: hidden; 930 | font-size: 12px; 931 | line-height: 1; 932 | text-align: left; 933 | white-space: nowrap 934 | } 935 | 936 | .markdown-body .csv-data .blob-num { 937 | padding: 10px 8px 9px; 938 | text-align: right; 939 | background: #0d1117; 940 | border: 0 941 | } 942 | 943 | .markdown-body .csv-data tr { 944 | border-top: 0 945 | } 946 | 947 | .markdown-body .csv-data th { 948 | font-weight: 600; 949 | background: #161b22; 950 | border-top: 0 951 | } 952 | 953 | .markdown-body .footnotes { 954 | font-size: 12px; 955 | color: #8b949e; 956 | border-top: 1px solid #30363d 957 | } 958 | 959 | .markdown-body .footnotes ol { 960 | padding-left: 16px 961 | } 962 | 963 | .markdown-body .footnotes li { 964 | position: relative 965 | } 966 | 967 | .markdown-body .footnotes li:target::before { 968 | position: absolute; 969 | top: -8px; 970 | right: -8px; 971 | bottom: -8px; 972 | left: -24px; 973 | pointer-events: none; 974 | content: ""; 975 | border: 2px solid #1f6feb; 976 | border-radius: 6px 977 | } 978 | 979 | .markdown-body .footnotes li:target { 980 | color: #c9d1d9 981 | } 982 | 983 | .markdown-body .footnotes .data-footnote-backref g-emoji { 984 | font-family: monospace 985 | } 986 | 987 | .markdown-body .task-list-item { 988 | list-style-type: none 989 | } 990 | 991 | .markdown-body .task-list-item label { 992 | font-weight: 400 993 | } 994 | 995 | .markdown-body .task-list-item.enabled label { 996 | cursor: pointer 997 | } 998 | 999 | .markdown-body .task-list-item + .task-list-item { 1000 | margin-top: 3px 1001 | } 1002 | 1003 | .markdown-body .task-list-item .handle { 1004 | display: none 1005 | } 1006 | 1007 | .markdown-body .task-list-item-checkbox { 1008 | margin: 0 .2em .25em -1.6em; 1009 | vertical-align: middle 1010 | } 1011 | 1012 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { 1013 | margin: 0 -1.6em .25em .2em 1014 | } 1015 | 1016 | .markdown-body ::-webkit-calendar-picker-indicator { 1017 | filter: invert(50%) 1018 | } 1019 | 1020 | 1021 | body { 1022 | background-color: #3f4344 !important; 1023 | scrollbar-width: none; 1024 | -ms-overflow-style: none; 1025 | 1026 | } 1027 | 1028 | body * { 1029 | scrollbar-width: none; 1030 | -ms-overflow-style: none; 1031 | } 1032 | 1033 | .markdown-body { 1034 | font-family: Montserrat, Inter, Roboto, -apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif !important; 1035 | background: none !important; 1036 | } 1037 | 1038 | .markdown-body * { 1039 | font-family: Montserrat, Inter, Roboto, -apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif !important; 1040 | color: #fff !important; 1041 | font-size: 18px; 1042 | font-weight: 300; 1043 | } 1044 | 1045 | .markdown-body pre, 1046 | .markdown-body code { 1047 | white-space: pre-wrap !important; 1048 | word-wrap: break-word !important; 1049 | font-size: 18px !important; 1050 | padding: 0px !important; 1051 | background: none !important; 1052 | color: #fff !important; 1053 | } 1054 | 1055 | .markdown-body .title { 1056 | line-height: 30px; 1057 | font-size: 22px; 1058 | font-weight: bold; 1059 | border-top: 1px dashed #aaa; 1060 | border-bottom: 1px dashed #aaa; 1061 | padding: 10px; 1062 | } 1063 | 1064 | .markdown-body .subtitle { 1065 | line-height: 30px; 1066 | font-size: 15px; 1067 | opacity: 0.5; 1068 | font-weight: normal; 1069 | } 1070 | 1071 | 1072 | .markdown-body .today { 1073 | border-top: 1px dashed #aaa; 1074 | border-bottom: 1px dashed #aaa; 1075 | padding: 10px; 1076 | margin-top: 10px; 1077 | } 1078 | 1079 | .markdown-body .today pre { 1080 | margin: 0px !important; 1081 | } 1082 | 1083 | .markdown-body .content { 1084 | padding: 10px; 1085 | border-top: 1px dashed #aaa; 1086 | border-bottom: 1px dashed #aaa; 1087 | margin: 10px 0; 1088 | } 1089 | 1090 | .markdown-body .footer { 1091 | line-height: 30px; 1092 | font-size: 16px; 1093 | border-top: 1px dashed #aaa; 1094 | border-bottom: 1px dashed #aaa; 1095 | padding: 10px; 1096 | } 1097 | 1098 | .markdown-body hr { 1099 | height: 1px !important; 1100 | margin: 12px 0 !important; 1101 | } 1102 | -------------------------------------------------------------------------------- /app/public/template/css/words.css: -------------------------------------------------------------------------------- 1 | @import "https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"; 2 | @font-face { 3 | font-family: ZhuqueFangsong-Regular; 4 | src: url(http://assets.html-js.com/ZhuqueFangsong-Regular.ttf); 5 | } 6 | .markdown-body { 7 | padding: 2rem 1rem; 8 | font-family: ZhuqueFangsong-Regular !important; 9 | background-color: #1c1b1f !important; 10 | } 11 | .markdown-body * { 12 | font-family: ZhuqueFangsong-Regular !important; 13 | color: #e4e2ce !important; 14 | } 15 | .markdown-body pre, 16 | .markdown-body code { 17 | white-space: pre-wrap !important; 18 | word-wrap: break-word !important; 19 | font-size: 16px !important; 20 | } 21 | .markdown-body hr { 22 | height: 1px !important; 23 | margin: 12px 0 !important; 24 | } 25 | 26 | .html { 27 | font-size: 24px; 28 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 29 | } 30 | 31 | #mdimg-body { 32 | border-left: 3rem solid #1d9bf0; 33 | } 34 | 35 | .top { 36 | color: #1d9bf0; 37 | font-size: 5rem; 38 | font-weight: bold; 39 | padding-left: 4rem; 40 | padding-top: 3rem; 41 | } 42 | 43 | .markdown-body { 44 | padding: 2rem 4rem; 45 | margin: 0 4rem; 46 | background-color: #fafafa; 47 | hyphens: auto; 48 | } 49 | .markdown-body p { 50 | color: #121212; 51 | font-size: 2rem; 52 | font-weight: bold; 53 | line-height: 3.5rem; 54 | } 55 | 56 | .bottom { 57 | padding-bottom: 3rem; 58 | } -------------------------------------------------------------------------------- /app/public/template/html/default.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /app/public/template/html/words.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /app/renderer/components/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { ChatContainer, ChatInput, ChatProvider, createClient, MessageList, MessageLoading } from 'uikit.chat'; 2 | import styles from '../styles/ChatGPT.module.scss'; 3 | import { useEffect } from 'react'; 4 | import { ipcRenderer } from 'electron'; 5 | 6 | const chatuiClient = createClient(); 7 | 8 | export default function Chat(props: { date: string; roomName: string }) { 9 | const sendMessage = (message: string) => { 10 | ipcRenderer.send('send-chat-content', { 11 | roomName: props.roomName.replace('.txt', ''), 12 | content: message, 13 | }); 14 | }; 15 | useEffect(() => { 16 | chatuiClient.chatboxStore.on('submit', sendMessage); 17 | chatuiClient.messageStore.clear(); 18 | ipcRenderer.send('get-chat-content', { 19 | date: props.date, 20 | roomName: props.roomName, 21 | }); 22 | const timer = setInterval(() => { 23 | ipcRenderer.send('get-chat-content', { 24 | date: props.date, 25 | roomName: props.roomName, 26 | }); 27 | }, 1000); 28 | ipcRenderer.on('chat-content-replay', (event, args) => { 29 | console.log('chat-replay', args); 30 | if (args.date == props.date && args.roomName == props.roomName) { 31 | args.chats.forEach((chat: any) => { 32 | if (chatuiClient.messageStore.messages.find((m: any) => m.id == chat.name + chat.content + chat.time)) { 33 | return; 34 | } 35 | chatuiClient.messageStore.addMessageDirect({ 36 | id: chat.name + chat.content + chat.time, 37 | role: 'assistant', 38 | content: chat.content, 39 | external: { 40 | userName: chat.name, 41 | time: chat.time, 42 | }, 43 | typing: false, 44 | timestamp: new Date(chat.time).getTime(), 45 | }); 46 | chatuiClient.messageStore.emit('change'); 47 | }); 48 | } else { 49 | } 50 | }); 51 | return () => { 52 | timer && clearInterval(timer); 53 | ipcRenderer.removeAllListeners('chat-content-replay'); 54 | chatuiClient.chatboxStore.removeListener('submit', sendMessage); 55 | }; 56 | }, []); 57 | return ( 58 | 59 |
60 |
61 | 62 |
70 | { 72 | return ( 73 |
74 |
{message.external.userName}:
75 |
76 | ); 77 | }} 78 | robotIcon={<>{'👤'}} 79 | content={(message: any) => { 80 | return ( 81 | <> 82 |
88 |
93 |
94 | 95 | ); 96 | }} 97 | > 98 | 99 |
100 | 101 |
102 |
103 |
104 |
105 |
106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /app/renderer/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/index.module.scss'; 2 | import Link from 'next/link'; 3 | import { ipcRenderer } from 'electron'; 4 | import Github from './icon/Github'; 5 | import Twitter from './icon/Twitter'; 6 | import { useEffect, useState } from 'react'; 7 | import { Chip } from '@nextui-org/react'; 8 | import { SuccessIcon } from './icon/SuccessIcon'; 9 | import { ErrorIcon } from './icon/ErrorIcon'; 10 | import { Button } from '@nextui-org/button'; 11 | import pkg from './../../package.json'; 12 | 13 | export function Header(props: { active: string }) { 14 | const [botStatus, setBotStatus] = useState('启动中'); 15 | const [botAccount, setBotAccount] = useState(''); 16 | useEffect(() => { 17 | ipcRenderer.on('bot-status-reply', (event, args) => { 18 | setBotStatus(args.status); 19 | setBotAccount(args.account); 20 | }); 21 | setInterval(() => { 22 | ipcRenderer.send('get-bot-status'); 23 | }, 3000); 24 | }, []); 25 | 26 | return ( 27 |
28 |
29 |
30 |
37 | 43 | 44 | 智囊 AI 45 | 53 | 群聊总结 {pkg.version} 54 | 55 | 56 | 57 |
66 | 78 | 群聊管理 79 | 80 | { 83 | ipcRenderer.send('open-url', 'https://zhinang.ai'); 84 | }} 85 | className={styles['navs-link']} 86 | style={{ 87 | fontSize: '14px', 88 | color: props.active == 'chat' ? 'var(--nextui-colors-primaryLightContrast)' : '#111', 89 | background: props.active == 'chat' ? 'var(--nextui-colors-primaryLight)' : 'none', 90 | padding: '7px 15px', 91 | borderRadius: '10px', 92 | fontWeight: 500, 93 | }} 94 | > 95 | 智囊 AI 官网(免费 GPT 工具) 96 | 97 | { 109 | ipcRenderer.send('show-config'); 110 | }} 111 | > 112 | 设置 113 | 114 |
115 |
116 |
117 |
118 |
119 | { 121 | ipcRenderer.send('open-url', 'https://twitter.com/aoao_eth'); 122 | }} 123 | > 124 | 125 | 126 | { 128 | ipcRenderer.send('open-url', 'https://github.com/aoao-eth/wechat-ai-summarize-bot'); 129 | }} 130 | > 131 | 132 | 133 | : 136 | } 137 | variant='flat' 138 | color={['错误', '已停止', '已退出'].includes(botStatus) ? 'danger' : 'success'} 139 | style={{ 140 | paddingLeft: '10px', 141 | }} 142 | > 143 | 154 | {botStatus} | {' '} 155 | {botAccount} 156 | 157 | 158 | 165 | 166 |
167 |
168 |
169 |
170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /app/renderer/components/icon/Bento.tsx: -------------------------------------------------------------------------------- 1 | export default function Bento(props: any) { 2 | return ( 3 | 11 | 17 | 21 | 25 | 29 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/renderer/components/icon/Discord.tsx: -------------------------------------------------------------------------------- 1 | export default function Discord(props: any) { 2 | return ( 3 | 12 | 17 | 22 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/renderer/components/icon/ErrorIcon.tsx: -------------------------------------------------------------------------------- 1 | export function ErrorIcon(props: { size: number }) { 2 | return ( 3 | 14 | 19 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/renderer/components/icon/GiftIcon.tsx: -------------------------------------------------------------------------------- 1 | export function GiftIcon(props: any) { 2 | return ( 3 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/renderer/components/icon/Github.tsx: -------------------------------------------------------------------------------- 1 | export default function Github(props: any) { 2 | return ( 3 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/renderer/components/icon/Paper.tsx: -------------------------------------------------------------------------------- 1 | export default function Paper(props: any) { 2 | return ( 3 | 12 | 17 | 22 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/renderer/components/icon/SuccessIcon.tsx: -------------------------------------------------------------------------------- 1 | export function SuccessIcon(props: { size: number }) { 2 | return ( 3 | 14 | 15 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/renderer/components/icon/Twitter.tsx: -------------------------------------------------------------------------------- 1 | export default function Twitter(props: any) { 2 | return ( 3 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/renderer/hooks/useConfig.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export function useConfig(){ 4 | const [showConfigModal, setShowConfigModal] = useState(false); 5 | return { 6 | showConfigModal, 7 | setShowConfigModal, 8 | } 9 | } -------------------------------------------------------------------------------- /app/renderer/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'uikit.chat'; 2 | -------------------------------------------------------------------------------- /app/renderer/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /app/renderer/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: (config, { isServer }) => { 3 | if (!isServer) { 4 | config.target = 'electron-renderer'; 5 | } 6 | 7 | return config; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /app/renderer/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { NextUIProvider } from '@nextui-org/react'; 2 | import '../styles/globals.css'; 3 | import { Toaster } from 'react-hot-toast'; 4 | 5 | function MyApp({ Component, pageProps }: any) { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /app/renderer/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | import { Button } from '@nextui-org/button'; 4 | import { Listbox, ListboxItem } from '@nextui-org/listbox'; 5 | import { QRCodeCanvas } from 'qrcode.react'; 6 | import { Modal, ModalBody, ModalContent, ModalHeader } from '@nextui-org/modal'; 7 | import { Input } from '@nextui-org/input'; 8 | 9 | import toast from 'react-hot-toast'; 10 | import { Checkbox, ModalFooter, Select, SelectItem } from '@nextui-org/react'; 11 | import { Header } from '../components/Header'; 12 | import { useConfig } from '../hooks/useConfig'; 13 | import moment from 'moment'; 14 | import Chat from '../components/Chat'; 15 | 16 | type IChatFile = { 17 | name: string; 18 | info?: { chatCount: number; chatMembers: string[]; chatMembersCount: number; chatLetters: number }; 19 | hasSummarized: boolean; 20 | hasImage: boolean; 21 | hasAudio: boolean; 22 | sended: boolean; 23 | send_time: number; 24 | }; 25 | /** 26 | * 中文(吴语,简体) wuu-CN-XiaotongNeural2(女) 27 | * wuu-CN-YunzheNeural2(男) 28 | * yue-CN 中文(粤语,简体) yue-CN-XiaoMinNeural1,2(女) 29 | * yue-CN-YunSongNeural1,2(男) 30 | * zh-CN 中文(普通话,简体) zh-CN-XiaoxiaoNeural(女) 31 | * zh-CN-YunxiNeural(男) 32 | * zh-CN-YunjianNeural(男) 33 | * zh-CN-XiaoyiNeural(女) 34 | * zh-CN-YunyangNeural(男) 35 | * zh-CN-XiaochenNeural(女) 36 | * zh-CN-XiaohanNeural(女) 37 | * zh-CN-XiaomengNeural(女) 38 | * zh-CN-XiaomoNeural(女) 39 | * zh-CN-XiaoqiuNeural(女) 40 | * zh-CN-XiaoruiNeural(女) 41 | * zh-CN-XiaoshuangNeural(女性、儿童) 42 | * zh-CN-XiaoxuanNeural(女) 43 | * zh-CN-XiaoyanNeural(女) 44 | * zh-CN-XiaoyouNeural(女性、儿童) 45 | * zh-CN-XiaozhenNeural(女) 46 | * zh-CN-YunfengNeural(男) 47 | * zh-CN-YunhaoNeural(男) 48 | * zh-CN-YunxiaNeural(男) 49 | * zh-CN-YunyeNeural(男) 50 | * zh-CN-YunzeNeural(男) 51 | * zh-CN-henan 中文(中原官话河南,简体) zh-CN-henan-YundengNeural2(男) 52 | * zh-CN-liaoning 中文(东北官话,简体) zh-CN-liaoning-XiaobeiNeural1,2(女) 53 | * zh-CN-shaanxi 中文(中原官话陕西,简体) zh-CN-shaanxi-XiaoniNeural1,2(女) 54 | * zh-CN-shandong 中文(冀鲁官话,简体) zh-CN-shandong-YunxiangNeural2(男) 55 | * zh-CN-sichuan 中文(西南普通话,简体) zh-CN-sichuan-YunxiNeural1,2(男) 56 | * zh-HK 中文(粤语,繁体) zh-HK-HiuMaanNeural(女) 57 | * zh-HK-WanLungNeural(男) 58 | * zh-HK-HiuGaaiNeural(女) 59 | * zh-TW 中文(台湾普通话,繁体) zh-TW-HsiaoChenNeural(女) 60 | * zh-TW-YunJheNeural(男) 61 | * zh-TW-HsiaoYuNeural(女) 62 | */ 63 | const AZURE_TTS_NAMES = [ 64 | { 65 | name: '中文(吴语,简体) wuu-CN-XiaotongNeural2(女)', 66 | value: 'wuu-CN-XiaotongNeural2', 67 | }, 68 | { 69 | name: 'wuu-CN-YunzheNeural2(男)', 70 | value: 'wuu-CN-YunzheNeural2', 71 | }, 72 | { 73 | name: 'yue-CN 中文(粤语,简体) yue-CN-XiaoMinNeural1,2(女)', 74 | value: 'yue-CN-XiaoMinNeural1', 75 | }, 76 | { 77 | name: 'yue-CN-YunSongNeural1,2(男)', 78 | value: 'yue-CN-YunSongNeural1', 79 | }, 80 | { 81 | name: 'zh-CN 中文(普通话,简体) zh-CN-XiaoxiaoNeural(女)', 82 | value: 'zh-CN-XiaoxiaoNeural', 83 | }, 84 | { 85 | name: 'zh-CN-YunxiNeural(男)', 86 | value: 'zh-CN-YunxiNeural', 87 | }, 88 | { 89 | name: 'zh-CN-YunjianNeural(男)', 90 | value: 'zh-CN-YunjianNeural', 91 | }, 92 | { 93 | name: 'zh-CN-XiaoyiNeural(女)', 94 | value: 'zh-CN-XiaoyiNeural', 95 | }, 96 | { 97 | name: 'zh-CN-YunyangNeural(男)', 98 | value: 'zh-CN-YunyangNeural', 99 | }, 100 | { 101 | name: 'zh-CN-XiaochenNeural(女)', 102 | value: 'zh-CN-XiaochenNeural', 103 | }, 104 | { 105 | name: 'zh-CN-XiaohanNeural(女)', 106 | value: 'zh-CN-XiaohanNeural', 107 | }, 108 | { 109 | name: 'zh-CN-XiaomengNeural(女)', 110 | value: 'zh-CN-XiaomengNeural', 111 | }, 112 | { 113 | name: 'zh-CN-XiaomoNeural(女)', 114 | value: 'zh-CN-XiaomoNeural', 115 | }, 116 | { 117 | name: 'zh-CN-XiaoqiuNeural(女)', 118 | value: 'zh-CN-XiaoqiuNeural', 119 | }, 120 | { 121 | name: 'zh-CN-XiaoruiNeural(女)', 122 | value: 'zh-CN-XiaoruiNeural', 123 | }, 124 | { 125 | name: 'zh-CN-XiaoshuangNeural(女性、儿童)', 126 | value: 'zh-CN-XiaoshuangNeural', 127 | }, 128 | { 129 | name: 'zh-CN-XiaoxuanNeural(女)', 130 | value: 'zh-CN-XiaoxuanNeural', 131 | }, 132 | { 133 | name: 'zh-CN-XiaoyanNeural(女)', 134 | value: 'zh-CN-XiaoyanNeural', 135 | }, 136 | { 137 | name: 'zh-CN-XiaoyouNeural(女性、儿童)', 138 | value: 'zh-CN-XiaoyouNeural', 139 | }, 140 | { 141 | name: 'zh-CN-XiaozhenNeural(女)', 142 | value: 'zh-CN-XiaozhenNeural', 143 | }, 144 | { 145 | name: 'zh-CN-YunfengNeural(男)', 146 | value: 'zh-CN-YunfengNeural', 147 | }, 148 | { 149 | name: 'zh-CN-YunhaoNeural(男)', 150 | value: 'zh-CN-YunhaoNeural', 151 | }, 152 | { 153 | name: 'zh-CN-YunxiaNeural(男)', 154 | value: 'zh-CN-YunxiaNeural', 155 | }, 156 | { 157 | name: 'zh-CN-YunyeNeural(男)', 158 | value: 'zh-CN-YunyeNeural', 159 | }, 160 | { 161 | name: 'zh-CN-YunzeNeural(男)', 162 | value: 'zh-CN-YunzeNeural', 163 | }, 164 | { 165 | name: 'zh-CN-henan 中文(中原官话河南,简体) zh-CN-henan-YundengNeural2(男)', 166 | value: 'zh-CN-henan-YundengNeural2', 167 | }, 168 | { 169 | name: 'zh-CN-liaoning 中文(东北官话,简体) zh-CN-liaoning-XiaobeiNeural1,2(女)', 170 | value: 'zh-CN-liaoning-XiaobeiNeural1', 171 | }, 172 | { 173 | name: 'zh-CN-shaanxi 中文(中原官话陕西,简体) zh-CN-shaanxi-XiaoniNeural1,2(女)', 174 | value: 'zh-CN-shaanxi-XiaoniNeural1', 175 | }, 176 | { 177 | name: 'zh-CN-shandong 中文(冀鲁官话,简体) zh-CN-shandong-YunxiangNeural2(男)', 178 | value: 'zh-CN-shandong-YunxiangNeural2', 179 | }, 180 | { 181 | name: 'zh-CN-sichuan 中文(西南普通话,简体) zh-CN-sichuan-YunxiNeural1,2(男)', 182 | value: 'zh-CN-sichuan-YunxiNeural1', 183 | }, 184 | { 185 | name: 'zh-HK 中文(粤语,繁体) zh-HK-HiuMaanNeural(女)', 186 | value: 'zh-HK-HiuMaanNeural', 187 | }, 188 | { 189 | name: 'zh-HK-WanLungNeural(男)', 190 | value: 'zh-HK-WanLungNeural', 191 | }, 192 | { 193 | name: 'zh-HK-HiuGaaiNeural(女)', 194 | value: 'zh-HK-HiuGaaiNeural', 195 | }, 196 | { 197 | name: 'zh-TW 中文(台湾普通话,繁体) zh-TW-HsiaoChenNeural(女)', 198 | value: 'zh-TW-HsiaoChenNeural', 199 | }, 200 | { 201 | name: 'zh-TW-YunJheNeural(男)', 202 | value: 'zh-TW-YunJheNeural', 203 | }, 204 | { 205 | name: 'zh-TW-HsiaoYuNeural(女)', 206 | value: 'zh-TW-HsiaoYuNeural', 207 | }, 208 | ]; 209 | 210 | function Home() { 211 | const [chatModal, setChatModal] = useState({ 212 | show: false, 213 | date: '', 214 | roomName: '', 215 | }); 216 | const { showConfigModal, setShowConfigModal } = useConfig(); 217 | const [config, setConfig] = useState({ 218 | PADLOCAL_API_KEY: '', 219 | DIFY_API_KEY: '', 220 | AZURE_TTS_APPKEY: '', 221 | AZURE_TTS_REGION: '', 222 | CUT_LENGTH: 10000, 223 | LAST_MESSAGE: '', 224 | AUTO_ACCEPT_FRIEND: false, 225 | AZURE_TTS_VOICE_NAME: 'zh-CN-XiaoshuangNeural', 226 | AZURE_ENDPOINT: '', 227 | AZURE_API_VERSION: '', 228 | AZURE_API_KEY: '', 229 | AZURE_MODEL_ID: 'gpt-3.5-turbo', 230 | ENABLE_AUTO_REPLY: false, 231 | AZURE_REPLY_KEYWORDS: 'zhinang 智囊', 232 | AZURE_REPLY_LIMIT: 10, 233 | }); 234 | const [qrCode, setQrCode] = useState(); 235 | const [dirs, setDirs] = useState< 236 | { 237 | path: string; 238 | chatFiles: IChatFile[]; 239 | allFiles: string[]; 240 | }[] 241 | >([]); 242 | 243 | const [selectedDir, setSelectedDir] = useState<{ 244 | path: string; 245 | chatFiles: IChatFile[]; 246 | allFiles: string[]; 247 | } | null>(null); 248 | 249 | const [selectedDirPath, setSelectedDirPath] = useState(null); 250 | 251 | useEffect(() => { 252 | setSelectedDir(dirs.find((dir) => dir.path === selectedDirPath)!); 253 | }, [selectedDirPath]); 254 | 255 | useEffect(() => { 256 | setSelectedDir(dirs.find((dir) => dir.path === selectedDirPath)!); 257 | }, [dirs]); 258 | 259 | useEffect(() => { 260 | ipcRenderer.on('get-all-dirs-reply', (event, arg) => { 261 | console.log('get-all-dirs-reply', arg); 262 | setDirs(arg); 263 | if (arg.length && !selectedDirPath) { 264 | setSelectedDirPath(arg[0].path); 265 | } else { 266 | const _selectedDirPath = selectedDirPath; 267 | setSelectedDirPath(_selectedDirPath); 268 | // setSelectedDirPath(selectedDirPath); 269 | // setSelectedDir(null); 270 | // setTimeout(() => { 271 | // setSelectedDir(dirs.find((dir) => dir.path === selectedDirPath)); 272 | // }, 300); 273 | } 274 | }); 275 | setInterval(() => { 276 | ipcRenderer.send('get-all-dirs'); 277 | }, 1000 * 20); 278 | ipcRenderer.send('get-all-dirs'); 279 | ipcRenderer.send('start-robot'); 280 | ipcRenderer.on('toast', (event, arg) => { 281 | console.log('toast', arg); 282 | toast(arg); 283 | }); 284 | ipcRenderer.on('scan-wait', (event, arg) => { 285 | console.log(arg); 286 | setQrCode(arg); 287 | }); 288 | 289 | ipcRenderer.on('login', (event, arg) => { 290 | setQrCode(null); 291 | }); 292 | ipcRenderer.on('login', (event, arg) => { 293 | setQrCode(null); 294 | }); 295 | ipcRenderer.on('show-config', (event, arg) => { 296 | console.log('show-config', arg); 297 | setShowConfigModal(true); 298 | setConfig(arg); 299 | }); 300 | ipcRenderer.on('summarize-end', () => { 301 | ipcRenderer.send('get-all-dirs'); 302 | }); 303 | }, []); 304 | 305 | function submitSummarize(dateDir: string, chatFileName: string) { 306 | ipcRenderer.send('summarize', { 307 | dateDir, 308 | chatFileName, 309 | }); 310 | } 311 | 312 | function sendSummarize(dateDir: string, chatFileName: string) { 313 | ipcRenderer.send('send-summarize', { 314 | dateDir, 315 | chatFileName, 316 | }); 317 | } 318 | 319 | return ( 320 |
321 |
322 | {dirs.length == 0 ? ( 323 |
332 | 暂无记录 333 |
334 | ) : ( 335 |
342 |
349 | 355 | {dirs?.map((dir) => ( 356 | { 360 | setSelectedDirPath(dir.path); 361 | }} 362 | style={{ 363 | background: dir.path == selectedDirPath ? '#f3f3f3' : 'none', 364 | }} 365 | endContent={ 366 |
367 | {dir.chatFiles.length} 368 | 384 |
385 | } 386 | > 387 |
{dir.path}
388 |
389 | ))} 390 |
391 |
392 |
399 | 405 | {selectedDir 406 | ? selectedDir!.chatFiles.map((dir) => ( 407 | 415 | 424 | {dir.hasImage ? '已总结' : null} 425 | 426 | 436 | {dir.sended ? `已发送` : null} 437 | 438 | 448 | {dir.send_time ? `(${moment(dir.send_time).format('HH:mm')})` : null} 449 | 450 | 468 | 482 | 495 | 496 | 510 |
511 | } 512 | description={ 513 |
514 | 521 | 对话数{' '} 522 | 527 | {dir.info?.chatCount} 528 | 529 | 530 | 537 | 对话人数{' '} 538 | 543 | {dir.info?.chatMembersCount} 544 | 545 | 546 | 553 | 对话字数{' '} 554 | 559 | {dir.info?.chatLetters} 560 | 561 | 562 |
563 | } 564 | > 565 |
{dir.name}
566 | 567 | )) 568 | : null} 569 | 570 |
571 |
572 | )} 573 | 574 | { 579 | setQrCode(null); 580 | }} 581 | > 582 | 583 | {(onClose) => ( 584 | <> 585 | 请扫码登录 586 | 594 | 595 | 596 | 597 | )} 598 | 599 | 600 | 601 | 602 | 603 | {(onClose) => ( 604 | <> 605 | 606 | 配置 607 | 608 | 616 |

617 | 必须正确配置才能正常运行,关于如何获取这些配置: 618 | { 621 | ipcRenderer.send('open-url', 'https://github.com/aoao-eth/wechat-ai-summarize-bot'); 622 | }} 623 | style={{ 624 | color: 'blue', 625 | }} 626 | > 627 | 点击查看 628 | 629 |

630 | { 635 | setConfig({ 636 | ...config, 637 | PADLOCAL_API_KEY: e.target.value, 638 | }); 639 | }} 640 | /> 641 | { 646 | setConfig({ 647 | ...config, 648 | DIFY_API_KEY: e.target.value, 649 | }); 650 | }} 651 | /> 652 | { 656 | setConfig({ 657 | ...config, 658 | AZURE_TTS_APPKEY: e.target.value, 659 | }); 660 | }} 661 | /> 662 | { 666 | setConfig({ 667 | ...config, 668 | AZURE_TTS_REGION: e.target.value, 669 | }); 670 | }} 671 | /> 672 | { 677 | setConfig({ 678 | ...config, 679 | CUT_LENGTH: Number(e.target.value), 680 | }); 681 | }} 682 | /> 683 | { 687 | setConfig({ 688 | ...config, 689 | LAST_MESSAGE: e.target.value, 690 | }); 691 | }} 692 | /> 693 | {/* {*/} 696 | {/* setConfig({*/} 697 | {/* ...config,*/} 698 | {/* AUTO_ACCEPT_FRIEND: e.target.checked,*/} 699 | {/* });*/} 700 | {/* }}*/} 701 | {/*>*/} 702 | {/* 自动接受好友请求*/} 703 | {/**/} 704 | 705 | { 708 | setConfig({ 709 | ...config, 710 | ENABLE_AUTO_REPLY: e.target.checked, 711 | }); 712 | }} 713 | > 714 | 开启自动群回复 715 | 716 | {config.ENABLE_AUTO_REPLY ? ( 717 | <> 718 | { 724 | setConfig({ 725 | ...config, 726 | AZURE_ENDPOINT: e.target.value, 727 | }); 728 | }} 729 | /> 730 | 731 | { 735 | setConfig({ 736 | ...config, 737 | AZURE_API_VERSION: e.target.value, 738 | }); 739 | }} 740 | /> 741 | 742 | { 746 | setConfig({ 747 | ...config, 748 | AZURE_API_KEY: e.target.value, 749 | }); 750 | }} 751 | /> 752 | 753 | { 757 | setConfig({ 758 | ...config, 759 | AZURE_MODEL_ID: e.target.value, 760 | }); 761 | }} 762 | /> 763 | { 767 | setConfig({ 768 | ...config, 769 | AZURE_REPLY_KEYWORDS: e.target.value, 770 | }); 771 | }} 772 | /> 773 | { 778 | setConfig({ 779 | ...config, 780 | AZURE_REPLY_LIMIT: Number(e.target.value), 781 | }); 782 | }} 783 | /> 784 | 785 | ) : null} 786 | 787 | 803 |
804 | 805 | 814 | 815 | 816 | )} 817 |
818 |
819 | {chatModal.show ? ( 820 | { 823 | setChatModal({ 824 | show: false, 825 | roomName: '', 826 | date: '', 827 | }); 828 | }} 829 | size={'3xl'} 830 | backdrop={'blur'} 831 | > 832 | 833 | {chatModal.roomName} 的实时对话 834 | 835 | 836 | 837 | ) : null} 838 | 839 | ); 840 | } 841 | 842 | export default Home; 843 | -------------------------------------------------------------------------------- /app/renderer/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/renderer/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/renderer/public/images/logo.png -------------------------------------------------------------------------------- /app/renderer/styles/ChatGPT.module.scss: -------------------------------------------------------------------------------- 1 | .chatgpt-ui-message { 2 | transition: all 0.3s ease-in-out; 3 | 4 | .chatgpt-ui-message-actions { 5 | transition: all 0.3s ease-in-out; 6 | opacity: 0; 7 | display: flex; 8 | height: 0; 9 | overflow: hidden; 10 | gap: 0 5px; 11 | width: 0; 12 | 13 | button { 14 | border: none; 15 | background: #dedede; 16 | border-radius: 4px; 17 | color: #666; 18 | cursor: pointer; 19 | font-size: 12px; 20 | margin: 5px 0; 21 | padding: 0 5px; 22 | 23 | &:hover { 24 | background: #ccc; 25 | } 26 | } 27 | } 28 | 29 | &:hover { 30 | .chatgpt-ui-message-actions { 31 | opacity: 1; 32 | height: 30px; 33 | width: auto; 34 | } 35 | } 36 | 37 | &.active { 38 | .chatgpt-ui-message-actions { 39 | opacity: 1; 40 | height: 30px; 41 | width: auto; 42 | } 43 | } 44 | } 45 | 46 | .chatgpt-ui-message-typing { 47 | .chatgpt-ui-message-actions { 48 | height: 0 !important; 49 | opacity: 0 !important; 50 | width: 0 !important; 51 | } 52 | } 53 | 54 | .chatgpt-container { 55 | box-sizing: border-box; 56 | position: relative; 57 | border-radius: 10px; 58 | overflow: hidden; 59 | height: 80vh; 60 | overflow-y: auto; 61 | 62 | .chatgpt-container-inner { 63 | width: 100%; 64 | height: 100%; 65 | } 66 | 67 | .chatgpt-ui-message-header { 68 | margin-top: 0px; 69 | } 70 | 71 | 。chatgpt-ui-message-header-time { 72 | font-size: 12px; 73 | color: #999; 74 | } 75 | 76 | // .g-bg { 77 | // position: absolute; 78 | // width: 100%; 79 | // height: 100%; 80 | // overflow: hidden; 81 | // z-index: 0; 82 | // left: 0; 83 | // top: 0; 84 | // & > div { 85 | // position: absolute; 86 | // opacity: 0.3; 87 | // } 88 | 89 | // &::before { 90 | // content: ''; 91 | // position: absolute; 92 | // top: 0; 93 | // left: 0; 94 | // bottom: 0; 95 | // right: 0; 96 | // backdrop-filter: blur(150px); 97 | // z-index: 1; 98 | // } 99 | // } 100 | 101 | // .g-polygon-1 { 102 | // bottom: 100px; 103 | // left: 50%; 104 | // transform: translate(-50%, -50%); 105 | // width: 90%; 106 | // height: 70%; 107 | // background: linear-gradient(#5582ff, #2a6ab8); 108 | // clip-path: polygon(0 10%, 30% 0, 100% 40%, 70% 100%, 20% 90%); 109 | // } 110 | 111 | // .g-polygon-2 { 112 | // bottom: 0px; 113 | // left: 30%; 114 | // transform: translate(-70%, 0); 115 | // width: 110%; 116 | // height: 60%; 117 | // background: linear-gradient(-36deg, #e950d1, #f980d9); 118 | // clip-path: polygon(10% 0, 100% 70%, 100% 100%, 20% 90%); 119 | // } 120 | 121 | // .g-polygon-3 { 122 | // bottom: 0px; 123 | // left: 40%; 124 | // transform: translate(-25%, 0); 125 | // width: 110%; 126 | // height: 60%; 127 | // background: rgba(87, 80, 233); 128 | // clip-path: polygon(80% 0, 100% 70%, 100% 100%, 20% 90%); 129 | // } 130 | } 131 | -------------------------------------------------------------------------------- /app/renderer/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .uikit-chat-message-item { 6 | margin-top: 10px !important; 7 | } -------------------------------------------------------------------------------- /app/renderer/styles/index.module.scss: -------------------------------------------------------------------------------- 1 | .chatgpt-setting { 2 | position: absolute; 3 | top: 30px; 4 | right: 30px; 5 | cursor: pointer; 6 | svg { 7 | fill: #333; 8 | color: #333; 9 | transition: all 0.3s ease-in-out; 10 | } 11 | &:hover { 12 | svg { 13 | fill: #666; 14 | color: #666; 15 | } 16 | } 17 | @media screen and (max-width: 768px) { 18 | top: 58px; 19 | } 20 | } 21 | .chatgpt-share { 22 | position: absolute; 23 | top: 30px; 24 | right: 60px; 25 | font-size: 13px; 26 | cursor: pointer; 27 | svg { 28 | fill: #333; 29 | color: #333; 30 | transition: all 0.3s ease-in-out; 31 | } 32 | &:hover { 33 | svg { 34 | fill: #666; 35 | color: #666; 36 | } 37 | } 38 | @media screen and (max-width: 768px) { 39 | top: 58px; 40 | } 41 | } 42 | .chat-page { 43 | display: flex; 44 | flex-direction: column; 45 | height: 100vh; 46 | align-items: center; 47 | background-color: #f3f3f3; 48 | background: linear-gradient(180deg, #f8f2ff 0%, rgba(250, 250, 250, 0) 100%); 49 | backdrop-filter: blur(125px); 50 | } 51 | .chat-container { 52 | flex: 1; 53 | position: relative; 54 | border-radius: 10px; 55 | overflow: hidden; 56 | display: flex; 57 | background-color: #f7f7f7; 58 | // box-shadow: 1px 1px 15px 0 rgba(0, 0, 0, 0.05); 59 | border: 1px solid #eee; 60 | background-color: #fff; 61 | border-radius: 15px; 62 | border: 1px dashed rgba(0, 0, 0, 0.5); 63 | margin-top: 0px; 64 | } 65 | .chat-left { 66 | width: 250px; 67 | position: relative; 68 | padding: 20px; 69 | padding-right: 0px; 70 | display: flex; 71 | flex-direction: row; 72 | align-content: stretch; 73 | flex-wrap: wrap; 74 | box-sizing: border-box; 75 | height: 100%; 76 | overflow: auto; 77 | .chat-left-list { 78 | flex: 1; 79 | display: flex; 80 | flex-direction: row; 81 | align-content: stretch; 82 | border-radius: 10px; 83 | box-sizing: border-box; 84 | height: 100%; 85 | } 86 | .chat-left-list-inner { 87 | display: flex; 88 | flex-direction: column; 89 | gap: 10px; 90 | 91 | width: 100%; 92 | overflow-y: auto; 93 | scroll-behavior: smooth; 94 | &::-webkit-scrollbar { 95 | width: 0px; 96 | } 97 | } 98 | .chat-prompt { 99 | width: 100%; 100 | } 101 | } 102 | .chat-right { 103 | flex: 1; 104 | position: relative; 105 | padding: 10px; 106 | padding-top: 60px; 107 | @media screen and (max-width: 768px) { 108 | padding-top: 80px; 109 | } 110 | } 111 | 112 | .awailable-on-chrome-store { 113 | text-align: center; 114 | padding: 0px 0px 20px 0px; 115 | display: flex; 116 | gap: 20px; 117 | a { 118 | border-radius: 10px; 119 | background-color: #f7f7f7; 120 | padding: 15px 20px 10px 20px; 121 | display: inline-block; 122 | &:hover { 123 | background-color: #eee; 124 | } 125 | img { 126 | width: 150px; 127 | } 128 | } 129 | } 130 | .chat-header { 131 | padding: 20px 0; 132 | width: 100%; 133 | border-bottom:1px solid #f3f3f3; 134 | @media screen and (max-width: 768px) { 135 | height: 60px; 136 | } 137 | .chat-header-title { 138 | font-size: 24px; 139 | font-weight: 600; 140 | text-align: left; 141 | margin: 0; 142 | padding: 0; 143 | background: linear-gradient(90deg, #8E24AA, #1E88E5); 144 | -webkit-background-clip: text; 145 | -webkit-text-fill-color: transparent; 146 | } 147 | .chat-header-inner { 148 | display: flex; 149 | justify-content: space-between; 150 | align-items: center; 151 | width: 100%; 152 | margin: 0 auto; 153 | 154 | } 155 | .chat-header-left { 156 | display: flex; 157 | flex-direction: column; 158 | gap: 5px; 159 | justify-content: flex-start; 160 | .chat-header-title { 161 | font-size: 24px; 162 | font-weight: 600; 163 | text-align: left; 164 | margin: 0; 165 | padding: 0; 166 | } 167 | .chat-header-subtitle { 168 | font-size: 12px; 169 | color: #666; 170 | margin: 0; 171 | padding: 0; 172 | } 173 | } 174 | .chat-header-right { 175 | text-align: right; 176 | display: flex; 177 | align-items: center; 178 | justify-content: flex-end; 179 | gap: 10px; 180 | } 181 | .header-links { 182 | display: flex; 183 | gap: 10px; 184 | 185 | align-items: center; 186 | height: 24px; 187 | 188 | svg { 189 | height: 24px; 190 | width: 24px; 191 | vertical-align: -2px; 192 | } 193 | } 194 | } 195 | .footer { 196 | margin-top: 40px; 197 | border-top: 1px solid #f3f3f3; 198 | padding-top: 20px; 199 | padding-bottom: 30px; 200 | 201 | .footer-links { 202 | display: flex; 203 | justify-content: center; 204 | align-items: center; 205 | gap: 20px; 206 | } 207 | a { 208 | font-size: 14px; 209 | color: #666; 210 | text-decoration: none; 211 | padding: 6px; 212 | border-radius: 8px; 213 | &:hover { 214 | background-color: #f3f3f3; 215 | } 216 | svg { 217 | vertical-align: -4px; 218 | } 219 | span { 220 | line-height: 20px; 221 | margin-left: 6px; 222 | } 223 | } 224 | } 225 | @media screen and (max-width: 768px) { 226 | .chat-header { 227 | margin: 0px; 228 | .chat-header-title { 229 | font-size: 20px; 230 | font-weight: 600; 231 | text-align: center; 232 | margin: 0; 233 | padding: 0; 234 | } 235 | } 236 | .chat-container { 237 | .chat-left { 238 | display: none; 239 | } 240 | } 241 | 242 | .awailable-on-chrome-store { 243 | a { 244 | padding: 10px 10px 5px 10px; 245 | 246 | img { 247 | width: 130px; 248 | } 249 | } 250 | } 251 | } 252 | .prompts-container { 253 | margin: 0px; 254 | position: relative; 255 | border-radius: 10px; 256 | overflow: hidden; 257 | height: 100px; 258 | 259 | overflow: auto; 260 | display: none; 261 | @media screen and (max-width: 768px) { 262 | display: flex; 263 | } 264 | 265 | // background-color: #f7f7f7; 266 | // box-shadow: 1px 1px 15px 0 rgba(0, 0, 0, 0.05); 267 | // border: 1px solid #eee; 268 | // background-color: #fff; 269 | // border-radius: 15px; 270 | // border: 1px dashed rgba(0, 0, 0, 0.5); 271 | } 272 | .prompts-container-inner { 273 | display: flex; 274 | gap: 10px; 275 | } 276 | .chat-prompt { 277 | width: 170px; 278 | height: 85px; 279 | background-color: #fff; 280 | border-radius: 15px; 281 | padding: 10px; 282 | border: 1px dashed rgba(0, 0, 0, 0.5); 283 | box-sizing: border-box; 284 | position: relative; 285 | cursor: pointer; 286 | transition: all 0.2s ease-in-out; 287 | &:hover, 288 | &.chat-prompt-active { 289 | background-color: #111; 290 | color: #fff; 291 | .chat-prompt-title { 292 | color: #fff; 293 | } 294 | .chat-prompt-category { 295 | font-size: 10px; 296 | color: #aaa; 297 | background-color: #666; 298 | color: var(--nextui-colors-primaryLightContrast); 299 | background: var(--nextui-colors-primaryLight); 300 | } 301 | } 302 | &-title { 303 | font-size: 14px; 304 | line-height: 20px; 305 | font-weight: 600; 306 | color: #333; 307 | } 308 | &-category { 309 | font-size: 10px; 310 | color: #999; 311 | margin-top: 5px; 312 | position: absolute; 313 | top: 3px; 314 | right: 10px; 315 | border-radius: 4px; 316 | color: var(--nextui-colors-primaryLightContrast); 317 | background: var(--nextui-colors-primaryLight); 318 | padding: 0px 5px; 319 | font-weight: normal; 320 | } 321 | &-desc { 322 | font-size: 10px; 323 | color: #999; 324 | margin-top: 5px; 325 | height: 40px; 326 | overflow: hidden; 327 | } 328 | } 329 | .chatgpt-bot { 330 | position: absolute; 331 | left: 20px; 332 | top: 20px; 333 | border: 1px dashed rgba(0, 0, 0, 0.5); 334 | padding: 8px 10px; 335 | border-radius: 10px; 336 | font-size: 12px; 337 | font-weight: bold; 338 | @media screen and (max-width: 768px) { 339 | right: 20px; 340 | } 341 | .chatgpt-bot-category { 342 | font-size: 10px; 343 | color: #333; 344 | margin-top: 5px; 345 | border-radius: 4px; 346 | color: var(--nextui-colors-primaryLightContrast); 347 | background: var(--nextui-colors-primaryLight); 348 | padding: 2px 5px; 349 | font-weight: normal; 350 | margin-right: 5px; 351 | } 352 | .chatgpt-bot-desc { 353 | font-size: 10px; 354 | color: #aaa; 355 | margin-top: 5px; 356 | padding: 2px 5px; 357 | font-weight: normal; 358 | margin-right: 5px; 359 | } 360 | } 361 | .prompts-cats { 362 | display: flex; 363 | 364 | gap: 10px; 365 | 366 | margin-bottom: 10px; 367 | overflow-x: auto; 368 | &::-webkit-scrollbar { 369 | display: none; 370 | } 371 | 372 | .prompts-cat-item { 373 | font-size: 14px; 374 | padding: 4px 9px; 375 | border-radius: 8px; 376 | height: 20px; 377 | line-height: 20px; 378 | cursor: pointer; 379 | white-space: nowrap; 380 | border: 1px dashed rgba(0, 0, 0, 0.5); 381 | background-color: #fff; 382 | } 383 | .prompts-cat-item-active, 384 | .prompts-cat-item:hover { 385 | background-color: #111; 386 | color: #fff; 387 | } 388 | } 389 | .navs { 390 | @media screen and (max-width: 768px) { 391 | position: absolute; 392 | top: 60px; 393 | left: 10px; 394 | margin: 0px !important; 395 | .navs-link { 396 | padding: 3px 10px !important; 397 | } 398 | } 399 | } 400 | 401 | .chat-page-body { 402 | max-width: 1300px; 403 | width: 90%; 404 | flex: 1; 405 | display: flex; 406 | flex-direction: column; 407 | padding-bottom: 20px; 408 | overflow: hidden; 409 | @media screen and (max-width: 768px) { 410 | width: calc(100vw - 20px); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /app/renderer/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // tailwind.config.js 2 | import { nextui } from '@nextui-org/react'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | const config = { 6 | content: [ 7 | // ... 8 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | darkMode: 'class', 14 | plugins: [nextui()], 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /app/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "next-env.d.ts", 5 | "**/*.ts", 6 | "**/*.tsx" 7 | ], 8 | "exclude": [ 9 | "node_modules" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /app/resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/resources/icon.icns -------------------------------------------------------------------------------- /app/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/app/resources/icon.ico -------------------------------------------------------------------------------- /app/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { notarize } = require('electron-notarize'); 3 | 4 | exports.default = async function notarizing(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | console.log('notarizing'); 11 | const appName = context.packager.appInfo.productFilename; 12 | 13 | return await notarize({ 14 | appBundleId: 'com.html-js.wx-summarize-bot', 15 | appPath: `${appOutDir}/${appName}.app`, 16 | appleId: process.env.APPLEID, 17 | appleIdPassword: process.env.APPLEIDPASS, 18 | }); 19 | }; -------------------------------------------------------------------------------- /app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // tailwind.config.js 2 | import { nextui } from '@nextui-org/react'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | const config = { 6 | content: [ 7 | // ... 8 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | darkMode: 'class', 14 | plugins: [nextui()], 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "incremental": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "experimentalDecorators": true, 21 | "jsx": "preserve" 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | "renderer/next.config.js", 26 | "app", 27 | "dist" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /app/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /app/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "experimentalDecorators": true, 19 | "useDefineForClassFields": true 20 | }, 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /app/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /app/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "base.json", 7 | "nextjs.json", 8 | "react-library.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /app/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015", "dom"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-summarize-bot", 3 | "version": "0.0.1", 4 | "description": "wechaty summarize bot", 5 | "author": "yutou", 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "axios": "^1.4.0", 9 | "dotenv": "^16.3.1", 10 | "lodash": "^4.17.21", 11 | "microsoft-cognitiveservices-speech-sdk": "1.32.0", 12 | "moment": "^2.29.4", 13 | "qrcode-terminal": "^0.12.0", 14 | "wechaty": "^1.19.10", 15 | "wechaty-puppet": "^1.19.6", 16 | "wechaty-puppet-padlocal": "^1.11.18", 17 | "yutou_cn_mdimg": "^0.2.10" 18 | }, 19 | "devDependencies": { 20 | "cross-env": "^7.0.3", 21 | "pm2": "^5.1.0", 22 | "ts-node": "^10.7.0", 23 | "typescript": "^4.6.4" 24 | }, 25 | "scripts": { 26 | "watch": "npx tsc & npx pm2 start dist/main.js --attach", 27 | "summarize": "npx ts-node ./src/summarize.ts", 28 | "build": "npx tsc" 29 | }, 30 | "engines": { 31 | "node": ">= 16", 32 | "npm": ">=7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | import { log, Message } from 'wechaty'; 2 | import * as PUPPET from 'wechaty-puppet'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import moment from 'moment'; 6 | import axios from 'axios'; 7 | 8 | export const LOGPRE = '[PadLocalDemo]'; 9 | 10 | //递归目录 11 | function createDirectoryRecursively(dirPath: string) { 12 | if (!fs.existsSync(dirPath)) { 13 | createDirectoryRecursively(path.dirname(dirPath)); 14 | fs.mkdirSync(dirPath); 15 | } 16 | } 17 | 18 | export async function getMessagePayload(message: Message) { 19 | switch (message.type()) { 20 | case PUPPET.types.Message.Text: 21 | log.silly(LOGPRE, `get message text: ${message.text()}`); 22 | const room = message.room(); 23 | const roomName = await room?.topic(); 24 | const userName = message.talker().name(); 25 | const text = message.text(); 26 | const time = message.date(); 27 | // 写入到本地 28 | const today = moment().format('YYYY-MM-DD'); 29 | //递归目录 30 | createDirectoryRecursively(path.resolve(__dirname, `../data/${today}`)); 31 | const filePath = path.resolve(__dirname, `../data/${today}/${roomName}.txt`); 32 | const data = `${moment(time).format('YYYY-MM-DD HH:mm:ss')}:\n${userName}:\n${text}\n\n`; 33 | fs.appendFile(filePath, data, (err: any) => { 34 | if (err) { 35 | console.log(err); 36 | } else { 37 | console.log('写入成功'); 38 | } 39 | }); 40 | 41 | break; 42 | 43 | case PUPPET.types.Message.Attachment: 44 | case PUPPET.types.Message.Audio: { 45 | const attachFile = await message.toFileBox(); 46 | 47 | const dataBuffer = await attachFile.toBuffer(); 48 | 49 | log.info(LOGPRE, `get message audio or attach: ${dataBuffer.length}`); 50 | 51 | break; 52 | } 53 | 54 | case PUPPET.types.Message.Video: { 55 | const videoFile = await message.toFileBox(); 56 | 57 | const videoData = await videoFile.toBuffer(); 58 | 59 | log.info(LOGPRE, `get message video: ${videoData.length}`); 60 | 61 | break; 62 | } 63 | 64 | case PUPPET.types.Message.Emoticon: { 65 | const emotionFile = await message.toFileBox(); 66 | 67 | const emotionJSON = emotionFile.toJSON(); 68 | log.info(LOGPRE, `get message emotion json: ${JSON.stringify(emotionJSON)}`); 69 | 70 | const emotionBuffer: Buffer = await emotionFile.toBuffer(); 71 | 72 | log.info(LOGPRE, `get message emotion: ${emotionBuffer.length}`); 73 | 74 | break; 75 | } 76 | 77 | case PUPPET.types.Message.Image: { 78 | const messageImage = await message.toImage(); 79 | 80 | const thumbImage = await messageImage.thumbnail(); 81 | const thumbImageData = await thumbImage.toBuffer(); 82 | 83 | log.info(LOGPRE, `get message image, thumb: ${thumbImageData.length}`); 84 | 85 | const hdImage = await messageImage.hd(); 86 | const hdImageData = await hdImage.toBuffer(); 87 | 88 | log.info(LOGPRE, `get message image, hd: ${hdImageData.length}`); 89 | 90 | const artworkImage = await messageImage.artwork(); 91 | const artworkImageData = await artworkImage.toBuffer(); 92 | 93 | log.info(LOGPRE, `get message image, artwork: ${artworkImageData.length}`); 94 | 95 | break; 96 | } 97 | 98 | case PUPPET.types.Message.Url: { 99 | const urlLink = await message.toUrlLink(); 100 | log.info(LOGPRE, `get message url: ${JSON.stringify(urlLink)}`); 101 | 102 | const urlThumbImage = await message.toFileBox(); 103 | const urlThumbImageData = await urlThumbImage.toBuffer(); 104 | 105 | log.info(LOGPRE, `get message url thumb: ${urlThumbImageData.length}`); 106 | 107 | break; 108 | } 109 | 110 | case PUPPET.types.Message.MiniProgram: { 111 | const miniProgram = await message.toMiniProgram(); 112 | 113 | log.info(LOGPRE, `MiniProgramPayload: ${JSON.stringify(miniProgram)}`); 114 | 115 | break; 116 | } 117 | } 118 | } 119 | 120 | export async function dingDongBot(message: Message) { 121 | if (message.to()?.self() && message.text().indexOf('ding') !== -1) { 122 | await message.talker().say(message.text().replace('ding', 'dong')); 123 | } 124 | } 125 | 126 | export async function summarize(roomName: string, apiKey: string):Promise { 127 | if (!roomName) { 128 | console.log('Please provide a file path.'); 129 | return 130 | } 131 | const today = moment().format('YYYY-MM-DD'); 132 | const fileName = path.resolve(__dirname, `../data/${today}/${roomName}.txt`); 133 | console.log(fileName) 134 | if (!fs.existsSync(fileName)) { 135 | console.log('The file path provided does not exist.'); 136 | return 137 | } 138 | 139 | /** 140 | * The content of the text file to be summarized. 141 | */ 142 | const fileContent = fs.readFileSync(fileName, 'utf-8') 143 | 144 | /** 145 | * The raw data to be sent to the Dify.ai API. 146 | */ 147 | const raw = JSON.stringify({ 148 | inputs: { 149 | query: `${fileContent.slice(-80000)}`, 150 | }, 151 | query: `${fileContent.slice(-80000)}`, 152 | response_mode: 'blocking', 153 | user: 'abc-123', 154 | }); 155 | console.log('Summarizing...\n\n\n'); 156 | 157 | try { 158 | const res = await axios.post('https://api.dify.ai/v1/completion-messages', raw, { 159 | headers: { 160 | Authorization: 'Bearer ' + apiKey, 161 | 'Content-Type': 'application/json', 162 | }, 163 | }); 164 | 165 | /** 166 | * The summarized text returned by the Dify.ai API. 167 | */ 168 | const result = res.data.answer.replace(/\n\n/g, '\n').trim(); 169 | return `${result}\n------------\n本总结由 wx.zhinang.ai 生成。` 170 | } catch (e: any) { 171 | console.error('Error:' + e.message); 172 | } 173 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { log, ScanStatus, WechatyBuilder } from 'wechaty'; 2 | import { PuppetPadlocal } from 'wechaty-puppet-padlocal'; 3 | import { dingDongBot, getMessagePayload, LOGPRE,summarize } from './helper'; 4 | import dotenv from 'dotenv'; 5 | 6 | dotenv.config(); 7 | 8 | const apiKey = process.env.DIFY_API_KEY; 9 | 10 | const puppet = new PuppetPadlocal({ 11 | token: process.env.PADLOCAL_API_KEY, 12 | }); 13 | 14 | const bot = WechatyBuilder.build({ 15 | name: 'PadLocalDemo', 16 | puppet, 17 | }) 18 | .on('scan', (qrcode, status) => { 19 | if (status === ScanStatus.Waiting && qrcode) { 20 | const qrcodeImageUrl = ['https://wechaty.js.org/qrcode/', encodeURIComponent(qrcode)].join(''); 21 | 22 | log.info(LOGPRE, `onScan: ${ScanStatus[status]}(${status})`); 23 | 24 | console.log('\n=================================================================='); 25 | console.log('\n* Two ways to sign on with qr code'); 26 | console.log('\n1. Scan following QR code:\n'); 27 | 28 | require('qrcode-terminal').generate(qrcode, { small: true }); // show qrcode on console 29 | 30 | console.log(`\n2. Or open the link in your browser: ${qrcodeImageUrl}`); 31 | console.log('\n==================================================================\n'); 32 | } else { 33 | log.info(LOGPRE, `onScan: ${ScanStatus[status]}(${status})`); 34 | } 35 | }) 36 | 37 | .on('login', (user) => { 38 | log.info(LOGPRE, `${user} login`); 39 | }) 40 | 41 | .on('logout', (user, reason) => { 42 | log.info(LOGPRE, `${user} logout, reason: ${reason}`); 43 | }) 44 | 45 | .on('message', async (message) => { 46 | log.info(LOGPRE, `on message: ${message.toString()}`); 47 | 48 | await getMessagePayload(message); 49 | 50 | const room = message.room(); 51 | const roomName = await room?.topic(); 52 | var userId:string 53 | for (let id of bot.ContactSelf.pool.keys()) { 54 | userId = id 55 | } 56 | console.log("ContactSelf.id----->",userId); 57 | console.log("message.talker----->",message.talker().id); 58 | // Must be specified for this to be valid 59 | var needHandle = (roomName == process.env.MONITOR_ROOMS&& userId == message.talker().id); 60 | if (needHandle) { 61 | switch (message.text()) { 62 | case "/summarize": 63 | // TODO frequency limitation 64 | summarize(roomName,apiKey).then((result:string) => { 65 | room.say(result) 66 | }); 67 | }; 68 | } 69 | 70 | await dingDongBot(message); 71 | }) 72 | 73 | .on('room-invite', async (roomInvitation) => { 74 | log.info(LOGPRE, `on room-invite: ${roomInvitation}`); 75 | }) 76 | 77 | .on('room-join', (room, inviteeList, inviter, date) => { 78 | log.info(LOGPRE, `on room-join, room:${room}, inviteeList:${inviteeList}, inviter:${inviter}, date:${date}`); 79 | }) 80 | 81 | .on('room-leave', (room, leaverList, remover, date) => { 82 | log.info(LOGPRE, `on room-leave, room:${room}, leaverList:${leaverList}, remover:${remover}, date:${date}`); 83 | }) 84 | 85 | .on('room-topic', (room, newTopic, oldTopic, changer, date) => { 86 | log.info( 87 | LOGPRE, 88 | `on room-topic, room:${room}, newTopic:${newTopic}, oldTopic:${oldTopic}, changer:${changer}, date:${date}`, 89 | ); 90 | }) 91 | 92 | .on('friendship', (friendship) => { 93 | log.info(LOGPRE, `on friendship: ${friendship}`); 94 | }) 95 | 96 | .on('error', (error) => { 97 | log.error(LOGPRE, `on error: ${error}`); 98 | }); 99 | 100 | bot.start().then(() => { 101 | log.info(LOGPRE, 'started.'); 102 | }); 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/summarize.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import axios from 'axios'; 3 | import dotenv from 'dotenv'; 4 | import { exec } from 'child_process'; 5 | import { convert2img } from 'yutou_cn_mdimg'; 6 | import { tts } from './tts'; 7 | import { uniq } from 'lodash'; 8 | import moment from 'moment'; 9 | 10 | dotenv.config(); 11 | 12 | /** 13 | * The API key for accessing the Dify.ai API. 14 | */ 15 | const apiKey = process.env.DIFY_API_KEY; 16 | 17 | /** 18 | * The file path of the text file to be summarized. 19 | */ 20 | const filePath = process.argv[2]; 21 | 22 | if (!filePath) { 23 | console.log('Please provide a file path.'); 24 | process.exit(1); 25 | } 26 | if (!fs.existsSync(filePath)) { 27 | console.log('The file path provided does not exist.'); 28 | process.exit(1); 29 | } 30 | 31 | /** 32 | * The content of the text file to be summarized. 33 | */ 34 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 35 | 36 | /** 37 | * The raw data to be sent to the Dify.ai API. 38 | */ 39 | const raw = JSON.stringify({ 40 | inputs: {}, 41 | query: `${fileContent.slice(-10000)}`, 42 | response_mode: 'blocking', 43 | user: 'abc-123', 44 | }); 45 | 46 | function getChatInfoForDate(date: string, chatName: string) { 47 | const filePath = `./data/${date}/${chatName}.txt`; 48 | if (!fs.existsSync(filePath)) { 49 | return false; 50 | } else { 51 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 52 | const chats = fileContent.split(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:\n/).filter((item) => item); 53 | // 对话数量 54 | const chatCount = chats.length; 55 | // 参与人 56 | const chatMembers = uniq(chats.map((item) => { 57 | return item.split('\n')[0]; 58 | })); 59 | 60 | return { 61 | chatCount, 62 | chatMembers, 63 | chatMembersCount: chatMembers.length, 64 | chatLetters: fileContent.length, 65 | }; 66 | } 67 | } 68 | 69 | function getChatInfoDayOnDay(date: string, chatName: string) { 70 | const todayInfo = getChatInfoForDate(date, chatName); 71 | let yesterday = moment(date).subtract(1, 'days').format('YYYY-MM-DD'); 72 | let yesterdayInfo = getChatInfoForDate(yesterday, chatName); 73 | let loopCount = 0; 74 | while (!yesterdayInfo && loopCount < 10) { 75 | yesterday = moment(yesterday).subtract(1, 'days').format('YYYY-MM-DD'); 76 | console.log(yesterday); 77 | yesterdayInfo = getChatInfoForDate(yesterday, chatName); 78 | loopCount++; 79 | } 80 | if (!todayInfo || !yesterdayInfo) { 81 | return false; 82 | } 83 | return { 84 | chatCount: todayInfo.chatCount - yesterdayInfo.chatCount, 85 | chatMembersCount: todayInfo.chatMembersCount - yesterdayInfo.chatMembersCount, 86 | chatLetters: todayInfo.chatLetters - yesterdayInfo.chatLetters, 87 | }; 88 | } 89 | 90 | function getDayOnDayDisplay(num: number) { 91 | if (num > 0) { 92 | return `↑${num}`; 93 | } else if (num < 0) { 94 | return `↓${Math.abs(num)}`; 95 | } else { 96 | return `→${num}`; 97 | } 98 | } 99 | 100 | /** 101 | * Sends a request to the Dify.ai API to summarize the text file. 102 | */ 103 | const run = async () => { 104 | console.log('Summarizing...\n'); 105 | 106 | try { 107 | /** 108 | * The summarized text returned by the Dify.ai API. 109 | */ 110 | const fileName = filePath.split('/').pop(); 111 | const fileNameWithoutExt = fileName?.replace('.txt', ''); 112 | const date = filePath.split('/').splice(-2, 1)[0]; 113 | 114 | const chatInfo = getChatInfoForDate(date, fileNameWithoutExt); 115 | const chatInfoDayOnDay = getChatInfoDayOnDay(date, fileNameWithoutExt); 116 | 117 | const res = await axios.post('https://api.dify.ai/v1/completion-messages', raw, { 118 | headers: { 119 | Authorization: 'Bearer ' + apiKey, 120 | 'Content-Type': 'application/json', 121 | }, 122 | }); 123 | 124 | const todayInfo = (chatInfo ? `今日整体情况 \n👥参与人数:${chatInfo?.chatMembersCount},📝对话数量:${chatInfo?.chatCount},📝对话字数:${chatInfo?.chatLetters}\n` : '') + 125 | (chatInfoDayOnDay ? `较昨日对比 \n👥参与人数:${getDayOnDayDisplay(chatInfoDayOnDay?.chatMembersCount)},📝对话数量:${getDayOnDayDisplay(chatInfoDayOnDay?.chatCount)},📝对话字数:${getDayOnDayDisplay(chatInfoDayOnDay?.chatLetters)}\n\n` : '') 126 | 127 | const result = 128 | `### 【${fileNameWithoutExt}】的群聊总结 ${date}\n\n------------\n\n\`\`\`\n` + 129 | todayInfo + 130 | res.data.answer.replace(/\n\n/g, '\n').trim() + 131 | '\n```\n\n------------\n\n❤️本总结由开源项目智囊AI生成 wx.zhinang.ai'; 132 | 133 | console.log(result); 134 | 135 | const summarizedFilePath = filePath.replace('.txt', ' 的今日群聊总结.txt'); 136 | // save to file in folder 137 | fs.writeFileSync(summarizedFilePath, result); 138 | 139 | // 执行命令 140 | const convertRes = await convert2img({ 141 | mdFile: summarizedFilePath, 142 | outputFilename: filePath.replace('.txt', ' 的今日群聊总结.png'), 143 | width: 450, 144 | cssTemplate: 'githubDark', 145 | }); 146 | 147 | console.log(`Convert to image successfully!`); 148 | 149 | if (process.env.AZURE_TTS_APPKEY) { 150 | const resultForTTS = 151 | `${fileNameWithoutExt}的群聊总结 ${date}` + 152 | res.data.answer.replace(/\n\n/g, '\n').trim() + 153 | '❤️本总结由开源项目智囊AI生成 wx.zhinang.ai'; 154 | 155 | console.log(`Start to convert to audio!`); 156 | await tts(summarizedFilePath, resultForTTS); 157 | console.log(`Convert to audio successfully!`); 158 | } 159 | console.log('Done!'); 160 | 161 | // const cmdStr = `npx carbon-now-cli '${filePath.replace('.txt', '_summarized.txt')}'`; 162 | // exec(cmdStr, (err, stdout, stderr) => { 163 | // if (err) { 164 | // console.log(err); 165 | // } 166 | // console.log(stdout); 167 | // console.log(stderr); 168 | // }); 169 | } catch (e: any) { 170 | console.error('Error:' + e.message); 171 | } 172 | }; 173 | run(); 174 | -------------------------------------------------------------------------------- /src/tts.ts: -------------------------------------------------------------------------------- 1 | import * as sdk from 'microsoft-cognitiveservices-speech-sdk'; 2 | import fs from 'fs'; 3 | import dotenv from 'dotenv'; 4 | 5 | export async function tts(filePath, content) { 6 | return new Promise((resolve, reject) => { 7 | const filename = filePath.replace('.txt', '.mp3'); 8 | const textFileName = filePath.replace('.txt', '.txt'); 9 | const speechConfig = sdk.SpeechConfig.fromSubscription( 10 | process.env.AZURE_TTS_APPKEY!, 11 | process.env.AZURE_TTS_REGION!, 12 | ); 13 | const audioConfig = sdk.AudioConfig.fromAudioFileOutput(filename); 14 | speechConfig.speechSynthesisVoiceName = 'zh-CN-XiaoshuangNeural'; 15 | speechConfig.speechSynthesisOutputFormat = sdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3; 16 | 17 | const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig); 18 | 19 | 20 | synthesizer.SynthesisCanceled = function(s, e) { 21 | var cancellationDetails = sdk.CancellationDetails.fromResult(e.result); 22 | var str = '(cancel) Reason: ' + sdk.CancellationReason[cancellationDetails.reason]; 23 | if (cancellationDetails.reason === sdk.CancellationReason.Error) { 24 | str += ': ' + e.result.errorDetails; 25 | } 26 | console.log(str); 27 | }; 28 | 29 | synthesizer.speakTextAsync( 30 | content, 31 | function(result) { 32 | synthesizer.close(); 33 | resolve(result); 34 | }, 35 | function(err) { 36 | console.trace('err - ' + err); 37 | synthesizer.close(); 38 | reject(err); 39 | }, 40 | ); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /static/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/static/1.jpg -------------------------------------------------------------------------------- /static/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/small-tou/wechat-ai-summarize-bot/a350bee4a74f13f17173e0cad01b89b905d9c299/static/2.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "target": "ES2019", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true 16 | }, 17 | "files": ["./src/main.ts"] 18 | } 19 | --------------------------------------------------------------------------------