├── .dockerignore
├── .env.example
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── .prettierrc.cjs
├── .vscode
└── settings.json
├── Dockerfile
├── Dockerfile.alpine
├── LICENSE.md
├── README.md
├── RECORD.md
├── cli.js
├── package.json
├── sponsors
├── 302AI.png
└── server.jpg
└── src
├── 302ai
├── __test__.js
└── index.js
├── chatgpt
└── index.js
├── claude
└── index.js
├── deepseek-free
├── __test__.js
└── index.js
├── deepseek
├── __test__.js
└── index.js
├── dify
├── __test__.js
└── index.js
├── doubao
├── __test__.js
└── index.js
├── index.js
├── kimi
├── __test__.js
└── index.js
├── ollama
├── __test__.js
└── index.js
├── openai
├── __test__.js
└── index.js
├── tongyi
└── index.js
├── wechaty
├── sendMessage.js
├── serve.js
└── testMessage.js
└── xunfei
├── __test__.js
├── index.js
└── xunfei.js
/.dockerignore:
--------------------------------------------------------------------------------
1 |
2 | node_modules/
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # .env
2 |
3 | # OpenAi 的api key, 去 https://beta.openai.com/account/api-keys 中生成一个即可,OPENAI_MODEL不填则默认gpt-4o,OPENAI_SYSTEM_MESSAGE为默认角色设定
4 | OPENAI_API_KEY=''
5 | OPENAI_PROXY_URL='https://openai.xxxx.com/v1/'
6 | OPENAI_MODEL=''
7 | OPENAI_SYSTEM_MESSAGE='You are a personal assistant.'
8 |
9 | # doubao, model和api-key, 去 https://console.volcengine.com/ark/apiKey
10 | DOUBAO_API_KEY=''
11 | DOUBAO_URL="https://ark.cn-beijing.volces.com/api/v3"
12 | DOUBAO_MODEL='ep-20250131132304-dqkpp'
13 | DOUBAO_SYSTEM_MESSAGE='你是豆包,是由字节跳动开发的 AI 人工智能助手'
14 |
15 | # deepseek, model和api-key, 去 https://platform.deepseek.com/usage 或者 https://cloud.siliconflow.cn/models (deepseek官方api暂时停止使用,可以用siliconflow的api)
16 | DEEPSEEK_API_KEY=''
17 | DEEPSEEK_URL="https://api.siliconflow.cn/v1"
18 | DEEPSEEK_MODEL='deepseek-ai/DeepSeek-R1'
19 | DEEPSEEK_SYSTEM_MESSAGE='# 角色定义
20 | role: "AI Assistant (DeepSeek-R1-Enhanced)"
21 | author: "DeepSeek"
22 | description: >
23 | 通用型智能助手,通过结构化思考流程提供可靠服务,
24 | 知识截止2023年12月,不处理实时信息。
25 | # 交互协议
26 | interaction_rules:
27 | thinking_flow: # 新增思考流程规范
28 | - 步骤1: 问题语义解析(意图/实体/上下文)
29 | - 步骤2: 知识库匹配(学科分类/可信度评估)
30 | - 步骤3: 逻辑验证(矛盾检测/边界检查)
31 | - 步骤4: 响应结构设计(分点/示例/注意事项)
32 | safety_layer:
33 | - 自动激活场景: [政治, 医疗建议, 隐私相关]
34 | - 响应模板: "该问题涉及[领域],建议咨询专业机构"
35 | # 输出规范
36 | output_schema:
37 | thinking_section: # 强制思考段落
38 | required: true
39 | tags: "思考内容:{content}
40 | "
41 | content_rules:
42 | - 使用Markdown列表格式
43 | - 包含至少2个验证步骤
44 | - 标注潜在不确定性
45 | - 复杂概念使用类比解释'
46 |
47 | # Kimi 的api key, 去 https://platform.moonshot.cn/console/api-keys
48 | KIMI_API_KEY=''
49 |
50 | # 科大讯飞, 去 https://console.xfyun.cn/services
51 | XUNFEI_APP_ID=''
52 | XUNFEI_API_KEY=''
53 | XUNFEI_API_SECRET=''
54 | # 使用的模型版本,默认填写 v4.0 或需要的版本号(如: v3.5, max-32k, pro-128k),参考src/xunfei.js中modelVersionMap
55 | XUNFEI_MODEL_VERSION='v4.0'
56 | # 系统角色描述,支持个性化定制
57 | XUNFEI_PROMPT='你是一个专业的智能助手,能够回答用户提出的各种问题。'
58 |
59 | # deepseek-free, model必须为deepseek-chat或deepseek-coder,去 https://platform.deepseek.com/usage或者https://github.com/LLM-Red-Team/deepseek-free-api
60 | # 在DEEPSEEK_SYSTEM_MESSAGE中设置系统提示词
61 | DEEPSEEK_FREE_URL='https://api.deepseek.com/chat/completions'
62 | DEEPSEEK_FREE_TOKEN=''
63 | DEEPSEEK_FREE_MODEL='deepseek-chat'
64 | DEEPSEEK_FREE_SYSTEM_MESSAGE='You are a personal assistant.'
65 |
66 | # 302AI
67 | _302AI_API_KEY = ''
68 | _302AI_MODEL= 'gpt-4o-mini'
69 |
70 | # dify, URL不包含uri路径
71 | DIFY_API_KEY = ''
72 | DIFY_URL = 'https://api.dify.ai'
73 |
74 | # 通义千问, URL 包含 uri 路径
75 | TONGYI_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
76 |
77 | # 通义千问的 API_KEY
78 | TONGYI_API_KEY = ''
79 |
80 | # 通义千问使用的模型
81 | TONGYI_MODEL='qwen-plus'
82 |
83 | # claude
84 | CLAUDE_API_VERSION = '2023-06-01'
85 | CLAUDE_API_KEY = ''
86 | CLAUDE_MODEL = 'claude-3-5-sonnet-latest'
87 | # 系统人设
88 | CLAUDE_SYSTEM = ''
89 |
90 | # ollama
91 | OLLAMA_URL='http://127.0.0.1:11434/api/chat'
92 | OLLAMA_MODEL=''
93 | OLLAMA_SYSTEM_MESSAGE='You are a personal assistant.'
94 |
95 | # 白名单配置
96 | #定义机器人的名称,这里是为了防止群聊消息太多,所以只有艾特机器人才会回复,
97 | #这里不要把@去掉,在@后面加上你启动机器人账号的微信名称
98 | BOT_NAME='@可乐'
99 | #联系人白名单
100 | ALIAS_WHITELIST='微信名1,备注名2'
101 | #群聊白名单
102 | ROOM_WHITELIST='XX群1,群2'
103 | #自动回复前缀匹配,文本消息匹配到指定前缀时,才会触发自动回复,不配或配空串情况下该配置不生效(适用于用大号,不期望每次被@或者私聊时都触发自动回复的人群)
104 | #匹配规则:群聊消息去掉${BOT_NAME}并trim后进行前缀匹配,私聊消息trim后直接进行前缀匹配
105 | AUTO_REPLY_PREFIX=''
106 |
107 | # 默认服务 302AI,ChatGPT、Kimi、Xunfei、deepseek-free, ollama, dify, tongyi 八选一,不填则键盘交互
108 | SERVICE_TYPE=''
109 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | WechatEveryDay.memory-card.json
3 | .env
4 | test.js
5 | package-lock.json
6 | yarn.lock
7 | Chromium.app
8 |
9 | .DS_Store
10 | .idea
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # puppeteer_download_host=https://registry.npmmirror.com/-/binary/
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * 参考 https://prettier.io/docs/en/options.html
3 | */
4 | module.exports = {
5 | tabWidth: 2, // 空格数
6 | useTabs: false, // 是否开启tab
7 | printWidth: 150, // 换行的宽度
8 | semi: false, // 是否在语句末尾打印分号
9 | singleQuote: true, // 是否使用单引号
10 | quoteProps: 'as-needed', // 对象的key仅在需要时用引号 as-needed|consistent|preserve
11 | trailingComma: 'all', // 多行时尽可能打印尾随逗号 |all|es5|none
12 | rangeStart: 0, // 每个文件格式化的范围是文件的全部内容
13 | bracketSpacing: true, // 对象文字中的括号之间打印空格
14 | jsxSingleQuote: true, // 在JSX中是否使用单引号
15 | bracketSameLine: false, // 将HTML元素的闭括号放在最后一行的末尾(不适用于自闭合元素)。
16 | arrowParens: 'always', // 箭头函数,只有一个参数的时候,也需要括号 always|avoid
17 | htmlWhitespaceSensitivity: 'ignore', // html中换行规则 css|strict|ignore,strict会强制在标签周围添加空格
18 | vueIndentScriptAndStyle: false, // vue中script与style里的第一条语句是否空格
19 | singleAttributePerLine: false, // 每行强制单个属性
20 | endOfLine: 'lf', // 换行符
21 | proseWrap: 'never', // 当超出print width时就折行 always|never|preserve .md文件?
22 | embeddedLanguageFormatting: 'auto',
23 | }
24 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": ["kimi", "xunfei"]
3 | }
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG APT_SOURCE="default"
2 |
3 | FROM node:19 as builder-default
4 | ENV NPM_REGISTRY="https://registry.npmjs.org"
5 |
6 | FROM node:19 as builder-aliyun
7 |
8 | ENV NPM_REGISTRY="https://registry.npmmirror.com"
9 | RUN sed -i s/deb.debian.org/mirrors.aliyun.com/g /etc/apt/sources.list \
10 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
11 |
12 | FROM builder-${APT_SOURCE} AS builder
13 | # Instal the 'apt-utils' package to solve the error 'debconf: delaying package configuration, since apt-utils is not installed'
14 | # https://peteris.rocks/blog/quiet-and-unattended-installation-with-apt-get/
15 | RUN apt-get update \
16 | && apt-get install -y --no-install-recommends \
17 | apt-utils \
18 | autoconf \
19 | automake \
20 | bash \
21 | build-essential \
22 | ca-certificates \
23 | chromium \
24 | coreutils \
25 | curl \
26 | ffmpeg \
27 | figlet \
28 | git \
29 | gnupg2 \
30 | jq \
31 | libgconf-2-4 \
32 | libtool \
33 | libxtst6 \
34 | moreutils \
35 | python-dev \
36 | shellcheck \
37 | sudo \
38 | tzdata \
39 | vim \
40 | wget \
41 | && apt-get purge --auto-remove \
42 | && rm -rf /tmp/* /var/lib/apt/lists/*
43 |
44 | FROM builder
45 |
46 | ENV CHROME_BIN="/usr/bin/chromium" \
47 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD="true"
48 |
49 | RUN mkdir -p /app
50 | WORKDIR /app
51 |
52 | COPY package.json ./
53 | RUN npm config set registry ${NPM_REGISTRY} && npm i
54 |
55 | COPY *.js ./
56 | COPY src/ ./src/
57 |
58 | CMD ["npm", "run", "dev"]
59 |
--------------------------------------------------------------------------------
/Dockerfile.alpine:
--------------------------------------------------------------------------------
1 | ARG APT_SOURCE="default"
2 |
3 | FROM node:19-alpine as base
4 | RUN apk update && \
5 | apk upgrade && \
6 | apk add --no-cache bash \
7 | ca-certificates \
8 | chromium-chromedriver \
9 | chromium \
10 | coreutils \
11 | curl \
12 | ffmpeg \
13 | figlet \
14 | jq \
15 | moreutils \
16 | ttf-freefont \
17 | udev \
18 | vim \
19 | xauth \
20 | xvfb \
21 | && rm -rf /tmp/* /var/cache/apk/*
22 |
23 |
24 | FROM base as builder-default
25 | ENV NPM_REGISTRY="https://registry.npmjs.org"
26 |
27 | FROM base as builder-aliyun
28 | ENV NPM_REGISTRY="https://registry.npmmirror.com"
29 |
30 |
31 | FROM builder-${APT_SOURCE}
32 |
33 | ENV CHROME_BIN="/usr/bin/chromium-browser" \
34 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD="true"
35 |
36 | RUN mkdir -p /app
37 | WORKDIR /app
38 |
39 | COPY package.json ./
40 | RUN npm config set registry ${NPM_REGISTRY} && npm i
41 |
42 | COPY *.js ./
43 | COPY src/ ./src/
44 |
45 | CMD ["npm", "run", "dev"]
46 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present, 荣顶 and wechat-bot contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeChat Bot
2 |
3 | ### Tip:最近本人正在看新的工作机会,如果你有合适的 `前端开发岗位` 在招,欢迎一起聊聊,我的 wx: ChicSparrow
4 |
5 | 一个 基于 `chatgpt` + `wechaty` 的微信机器人
6 |
7 | 可以用来帮助你自动回复微信消息,或者管理微信群/好友.
8 |
9 | `简单`,`好用`,`2分钟(4 个步骤)` 就能玩起来了。🌸 如果对您有所帮助,请点个 Star ⭐️ 支持一下。
10 |
11 |
12 |

13 |
14 |
15 | ## 贡献者们
16 |
17 |
18 |
19 |
20 |
21 | 欢迎大家提交 PR 接入更多的 ai 服务(比如扣子等...),积极贡献更好的功能实现,让 wechat-bot 变得更强!
22 |
23 | ## 注意:最近微信对此审查变得非常严格,使用默认的协议有微信警告或者封号的风险,请大家谨慎使用,关于 padlocal 这个协议,由于作者被腾讯告了,所以没有继续维护,我们最近正在重构底层实现,或切换更好的协议(目前我本地已跑通,大家可以稍加等候)。
24 |
25 | ## 使用前需要配置的 AI 服务(目前支持 9 种,可任选其一)
26 |
27 | - deepseek
28 |
29 | 获取自己的 `api key`,地址戳这里 👉🏻 :[deepseek 开放平台](https://platform.deepseek.com/usage)
30 | 将获取到的`api key`填入 `.evn` 文件中的 `DEEPSEEK_FREE_TOKEN` 中。
31 |
32 | - ChatGPT
33 |
34 | 先获取自己的 `api key`,地址戳这里 👉🏻 :[创建你的 api key](https://platform.openai.com/settings/organization/api-keys)
35 |
36 | **注意:这个是需要去付费购买的,很多人过来问为什么请求不通,请确保终端走了代理,并且付费购买了它的服务**
37 |
38 | ```sh
39 | # 执行下面命令,拷贝一份 .env.example 文件为 .env
40 | cp .env.example .env
41 | # 填写完善 .env 文件中的内容
42 | OPENAI_API_KEY='你的key'
43 | ```
44 |
45 | - 通义千问
46 |
47 | 通义千问是阿里云提供的 AI 服务,获取到你的 api key 之后, 填写到 .env 文件中即可
48 |
49 | ```sh
50 | # 执行下面命令,拷贝一份 .env.example 文件为 .env
51 | cp .env.example .env
52 | # 填写完善 .env 文件中的内容
53 | # 通义千问, URL 包含 uri 路径
54 | TONGYI_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
55 | # 通义千问的 API_KEY
56 | TONGYI_API_KEY = ''
57 | # 通义千问使用的模型
58 | TONGYI_MODEL='qwen-plus'
59 | ```
60 |
61 | - 科大讯飞
62 |
63 | 新增科大讯飞,去这里申请一个 key:[科大讯飞](https://console.xfyun.cn/services/bm35),每个模型都有 200 万的免费 token ,感觉很难用完。
64 | 注意: 讯飞的配置文件几个 key,别填反了,很多人找到我说为什么不回复,都是填反了。
65 | 而且还有一个好处就是,接口不会像 Kimi 一样限制请求频次,相对来说稳定很多。
66 | 服务出错可参考: [issues/170](https://github.com/wangrongding/wechat-bot/issues/170), [issues/180](https://github.com/wangrongding/wechat-bot/issues/180)
67 |
68 | - Kimi (请求限制较严重)
69 |
70 | 可以去 : [kimi apikey](https://platform.moonshot.cn/console/api-keys) 获取你的 key
71 | 最近比较忙,大家感兴趣可以提交 PR,我会尽快合并。目前 Kimi 刚刚集成,还可以实现上传文件等功能,然后有其它较好的服务也可以提交 PR 。
72 |
73 | - dify
74 |
75 | 地址:[dify](https://dify.ai/), 创建你的应用之后, 获取到你的 api key 之后, 填写到 .env 文件中即可, 也支持私有化部署dify版本
76 |
77 | ```sh
78 | # 执行下面命令,拷贝一份 .env.example 文件为 .env
79 | cp .env.example .env
80 | # 填写完善 .env 文件中的内容
81 | DIFY_API_KEY='你的key'
82 | # 如果需要私有化部署,请修改.env中下面的配置
83 | # DIFY_URL='https://[你的私有化部署地址]'
84 | ```
85 |
86 | - ollama
87 |
88 | Ollama 是一个本地化的 AI 服务,它的 API 与 OpenAI 非常接近。配置 Ollama 的过程与各种在线服务略有不同
89 |
90 | ```sh
91 | # 执行下面命令,拷贝一份 .env.example 文件为 .env
92 | cp .env.example .env
93 | # 填写完善 .env 文件中的内容
94 | OLLAMA_URL='http://127.0.0.1:11434/api/chat'
95 | OLLAMA_MODEL='qwen2.5:7b'
96 | OLLAMA_SYSTEM_MESSAGE='You are a personal assistant.'
97 | ```
98 |
99 | - 302.AI
100 |
101 | AI聚合平台,有套壳GPT的API,也有其他模型,点这里可以[添加API](https://dash.302.ai/apis/list),添加之后把API KEY配置到.env里,如下,MODEL可以自行选择配置
102 |
103 | ```
104 | _302AI_API_KEY = 'xxxx'
105 | _302AI_MODEL= 'gpt-4o-mini'
106 | ```
107 |
108 | 由于openai充值需要国外信用卡,流程比较繁琐,大多需要搞国外虚拟卡,手续费也都不少,该平台可以直接支付宝,算是比较省事的,注册填问卷可领1刀额度,后续充值也有手续费,用户可自行酌情选择。
109 |
110 | - claude
111 |
112 | 前往 [官网](https://console.anthropic.com) 注册并获取API KEY后进行配置即可
113 |
114 | ```bash
115 | # 执行下面命令,拷贝一份 .env.example 文件为 .env,如果已存在则忽略此步
116 | cp .env.example .env
117 |
118 | # 编辑.env文件并添加claude相关配置
119 |
120 | CLAUDE_API_VERSION = '2023-06-01'
121 | CLAUDE_API_KEY = '你的API KEY'
122 | CLAUDE_MODEL = 'claude-3-5-sonnet-latest'
123 | # 系统人设
124 | CLAUDE_SYSTEM = ''
125 | ```
126 |
127 | - 其他
128 | (待实践)理论上使用 openAI 格式的 api,都可以使用,在 env 文件中修改对应的 api_key、model、proxy_url 即可。
129 |
130 | ## API资源/平台收录
131 |
132 | - [gpt4free](https://github.com/xtekky/gpt4free)
133 | - [chatanywhere](https://github.com/chatanywhere/GPT_API_free)
134 |
135 | ## 赞助商
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | |
148 |
149 |
150 |
151 |
152 | 302.AI是一个按需付费的一站式企业级AI应用平台,开放平台,开源生态,让AI为每个需求找到答案。
153 | 产品链接
154 | |
155 |
156 |
157 |
158 |
159 | 目前该项目流量较大,已经上过 27 次 [Github Trending 榜](https://github.com/trending),如果您的公司或者产品需要推广,可以在下方二维码处联系我,我会在项目中加入您的广告,帮助您的产品获得更多的曝光。
160 |
161 | ## 开发/使用
162 |
163 | 检查好自己的开发环境,确保已经安装了 `nodejs` , 版本需要满足 Node.js >= v18.0 ,版本太低会导致运行报错,最好使用 LTS 版本。
164 |
165 | 1. 安装依赖
166 |
167 | > 安装依赖时,大陆的朋友推荐切到 taobao 镜像源后再安装,命令:
168 | > `npm config set registry https://registry.npmmirror.com`
169 | > 想要灵活切换,推荐使用我的工具 👉🏻 [prm-cli](https://github.com/wangrongding/prm-cli) 快速切换。
170 |
171 | ```sh
172 | # 安装依赖
173 | npm i
174 | # 推荐用 yarn 吧,npm 安装有时会遇到 wechaty 内部依赖安装失败的问题
175 | yarn
176 | ```
177 |
178 | 2. 运行服务
179 |
180 | ```sh
181 | # 启动服务
182 | npm run dev # 或者 npm run start
183 | # 启动服务
184 | yarn dev # 或者 yarn start
185 | ```
186 |
187 | 然后就可以扫码登录了,然后根据你的需求,自己修改相关逻辑文件。
188 |
189 | 
190 |
191 | 
192 |
193 | 为了兼容 docker 部署,避免不必要的选择交互,新增指定服务运行
194 |
195 | ```sh
196 | # 运行指定服务 ( 目前支持 ChatGPT | Kimi | Xunfei )
197 | npm run start -- --serve Kimi
198 | # 交互选择服务,仍然保持原有的逻辑
199 | npm run start
200 | ```
201 |
202 | 3. 测试
203 |
204 | 安装完依赖后,运行 `npm run dev` 前,可以先测试下 openai 的接口是否可用,运行 `npm run test` 即可。
205 |
206 | 遇到 timeout 问题需要自行用魔法解决。(一般就是代理未成功,或者你的魔法服务限制了 openai api 的服务)
207 |
208 | ## 你要修改的
209 |
210 | 很多人说运行后不会自动收发信息,不是的哈,为了防止给每一条收到的消息都自动回复(太恐怖了),所以加了限制条件。
211 |
212 | 你要把下面提到的地方自定义修改下。
213 |
214 | - 群聊,记得把机器人名称改成你自己微信号的名称,然后添加对应群聊的名称到白名单中,这样就可以自动回复群聊消息了。
215 | - 私聊,记得把需要自动回复的好友名称添加到白名单中,这样就可以自动回复私聊消息了。
216 | - 更深入的可以通过修改 `src/wechaty/sendMessage.js` 文件来满足你自己的业务场景。(大多人反馈可能无法自动回复,也可以通过调试这个文件来排查具体原因)
217 |
218 | 在.env 文件中修改你的配置即可,示例如下
219 |
220 | ```sh
221 | # 白名单配置
222 | #定义机器人的名称,这里是为了防止群聊消息太多,所以只有艾特机器人才会回复,
223 | #这里不要把@去掉,在@后面加上你启动机器人账号的微信名称
224 | BOT_NAME=@可乐
225 | #联系人白名单
226 | ALIAS_WHITELIST=微信名1,备注名2
227 | #群聊白名单
228 | ROOM_WHITELIST=XX群1,群2
229 | #自动回复前缀匹配,文本消息匹配到指定前缀时,才会触发自动回复,不配或配空串情况下该配置不生效(适用于用大号,不期望每次被@或者私聊时都触发自动回复的人群)
230 | #匹配规则:群聊消息去掉${BOT_NAME}并trim后进行前缀匹配,私聊消息trim后直接进行前缀匹配
231 | AUTO_REPLY_PREFIX=''
232 | ```
233 |
234 | 可以看到,自动回复都是基于 `chatgpt` 的,记得要开代理,或者填写代理地址。
235 |
236 | 
237 |
238 | ## 注意项
239 |
240 | 近期微信审查很严格,大量用户反映弹出外挂警告,由于项目内默认使用的是免费版的 web 协议,所以目前来说很容易会被微信检测到,建议使用 pad 协议,或者自行购买企业版协议,避免被封号。
241 |
242 | 修改可参考: https://github.com/wangrongding/wechat-bot/pull/263/files
243 | 自行购买 pad 协议渠道(wechaty 出的,购买仍需谨慎):http://pad-local.com
244 | 由于底层依赖的 wechaty 本身不怎么维护了,听说是被腾讯告了,所以大家购买也要谨慎,群友分享目前 pad 协议可正常使用(但频繁登录登出也会收到警告),最好别一次性买太久的。
245 |
246 | ## 常见问题
247 |
248 | 可以进交流群,一起交流探讨相关问题和解决方案,添加的时候记得备注来意。(如果项目对你有所帮助,也可以请我喝杯咖啡 ☕️ ~)
249 |
250 | |
|
|
251 | | --- | --- |
252 |
253 | ### 运行报错等问题
254 |
255 | 首先你需要做到以下几点:
256 |
257 | - 拉取最新代码,重新安装依赖(删除 lock 文件,删除 node_modules)
258 | - 安装依赖时最好不要设置 npm 镜像
259 | - 遇到 puppeteer 安装失败设置环境变量:
260 |
261 | ```
262 | # Mac
263 | export PUPPETEER_SKIP_DOWNLOAD='true'
264 |
265 | # Windows
266 | SET PUPPETEER_SKIP_DOWNLOAD='true'
267 | ```
268 |
269 | - 确保你们的终端走了代理 (开全局代理,或者手动设置终端走代理)
270 |
271 | ```sh
272 | # 设置代理
273 | export https_proxy=http://127.0.0.1:你的代理服务端口号;export http_proxy=http://127.0.0.1:你的代理服务端口号;export all_proxy=socks5://127.0.0.1:你的代理服务端口号
274 | # 然后再执行 npm run test
275 | npm run test
276 | ```
277 |
278 | 
279 |
280 | - 确保你的 openai key 有余额
281 | - 配置好 .env 文件
282 | - 执行 npm run test 能成功拿到 openai 的回复(设置完代理后,仍然请求不通? 可以参考: https://medium.com/@chanter2d/%E5%85%B3%E4%BA%8E%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8clash%E5%AE%9E%E7%8E%B0%E7%9C%9F%E6%AD%A3%E7%9A%84%E5%85%A8%E5%B1%80%E4%BB%A3%E7%90%86-385b2d745871)
283 | - 执行 npm run dev 愉快的玩耍吧~ 🎉
284 |
285 | 也可以参考这条 [issue](https://github.com/wangrongding/wechat-bot/issues/54#issuecomment-1347880291)
286 |
287 | - 怎么玩? 完成自定义修改后,群聊时,在白名单中的群,有人 @你 时会触发自动回复,私聊中,联系人白名单中的人发消息给你时会触发自动回复。
288 | - 运行报错?检查 node 版本是否符合,如果不符合,升级 node 版本即可,检查依赖是否安装完整,如果不完整,大陆推荐切换下 npm 镜像源,然后重新安装依赖即可。(可以用我的 [prm-cli](https://github.com/wangrongding/prm-cli) 工具快速切换)
289 | - 调整对话模式?可以修改[openai/index.js](./src/openai/index.js) ,具体可以根据官方文档给出的示例(非常多,自己对应调整参数即可) :https://beta.openai.com/examples
290 |
291 | ## 使用 Docker 部署
292 |
293 | ```sh
294 | $ docker build . -t wechat-bot
295 |
296 | $ docker run -d --rm --name wechat-bot -v $(pwd)/.env:/app/.env wechat-bot
297 | ```
298 |
299 | - 如果docker build过程中node反复下载超时,可先下载nodejs镜像到本地镜像库,并将DockerFile中的'node:19'修改为本地nodejs镜像版本
300 |
301 | ## Star History Chart
302 |
303 | 该项目于 2023/2/13 日成为 Github Trending 榜首。
304 |
305 | [](https://star-history.com/#wangrongding/wechat-bot&Date)
306 |
307 | ## License
308 |
309 | [MIT](./LICENSE).
310 |
--------------------------------------------------------------------------------
/RECORD.md:
--------------------------------------------------------------------------------
1 | # 使用最近很火的 OpenAi ChatGPT 配合 Wechaty 实现一个 微信聊天機器人
2 |
3 | ## 前言
4 |
5 | 使用 OpenAi ChatGPT 和 Wechaty 可以实现一个微信聊天机器人。OpenAi ChatGPT 是一个大型语言模型,能够模拟人类聊天对话,并回答用户的问题。Wechaty 是一个 Node.js 库,可以让开发者轻松地接入微信并实现聊天机器人功能。
6 |
7 | 首先,需要安装 Node.js 和 npm(Node.js 包管理器)。安装完成后,使用 npm 安装 Wechaty 和 OpenAi ChatGPT。
8 |
9 | ## 什么是 OpenAi ChatGPT
10 |
11 | OpenAI ChatGPT 是一个语言模型,由 OpenAI 开发。它是基于 GPT-3(Generative Pretrained Transformer-3)架构的,用于对话和聊天的自然语言处理(NLP)任务。它的目的是通过模仿人类的语言方式来生成文本。
12 |
13 | 与其他语言模型不同,OpenAI ChatGPT 可以记忆之前的对话内容,并根据上下文和预先学习的知识来生成自然的回复。它可以用于支持机器人,聊天机器人和其他应用程序,以提供更加人性化和自然的对话体验。
14 |
15 | OpenAI ChatGPT 模型使用了大量的语料数据进行预训练,并通过机器学习算法来优化其生成文本的能力。它具有出色的自然语言理解能力,可以模拟人类的语言特征,如句子结构、语法和修辞手法。
16 |
17 | 总的来说,OpenAI ChatGPT 是一个非常强大的语言模型,可以用于实现多种 NLP 应用程序,以提高对话和聊天的自然语言处理能力。
18 |
19 | ## 什么是 Wechaty
20 |
21 | ## 一些相关链接
22 |
23 | - [OpenAI ChatGPT](https://openai.com/blog/chatting/)
24 | - [Wechaty](https://wechaty.js.org/)
25 | - [Wechaty Chatbot](https://wechaty.js.org/docs/examples/chatbot/)
26 | - [Wechaty Chatbot Tutorial](https://wechaty.js.org/docs/tutorials/chatbot-tutorial/)
27 |
28 | - https://openai.com/blog/chatgpt/
29 | - https://download-chromium.appspot.com/?platform=Mac_Arm&type=snapshots
30 | - https://registry.npmmirror.com/binary.html?path=chromium-browser-snapshots/Mac_Arm/
31 |
--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | 'use strict'
4 | import('./src/index.js')
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wechat-bot",
3 | "version": "1.0.0-alpha.1",
4 | "description": "wechat-bot",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "node ./cli.js",
9 | "format": "prettier --write ./src",
10 | "start": "node ./cli.js",
11 | "test": "node ./src/wechaty/testMessage.js",
12 | "test-openai": "node ./src/openai/__test__.js",
13 | "test-xunfei": "node ./src/xunfei/__test__.js",
14 | "test-kimi": "node ./src/kimi/__test__.js",
15 | "test-dify": "node ./src/dify/__test__.js",
16 | "prepare": "husky"
17 | },
18 | "lint-staged": {
19 | "*.{js,ts,md}": [
20 | "prettier --write"
21 | ]
22 | },
23 | "bin": {
24 | "we": "./cli.js",
25 | "wb": "./cli.js",
26 | "wechat-bot": "./cli.js"
27 | },
28 | "author": "荣顶",
29 | "license": "ISC",
30 | "dependencies": {
31 | "axios": "^1.6.8",
32 | "chatgpt": "^2.5.2",
33 | "commander": "^12.0.0",
34 | "crypto-js": "^4.2.0",
35 | "dotenv": "^16.4.5",
36 | "inquirer": "^9.2.16",
37 | "openai": "^4.52.0",
38 | "p-timeout": "^6.0.0",
39 | "qrcode-terminal": "^0.12.0",
40 | "remark": "^14.0.2",
41 | "strip-markdown": "^5.0.0",
42 | "wechaty": "^1.20.2",
43 | "wechaty-puppet-wechat": "^1.18.4",
44 | "wechaty-puppet-wechat4u": "^1.14.14",
45 | "ws": "^8.16.0"
46 | },
47 | "devDependencies": {
48 | "husky": "^9.0.11",
49 | "lint-staged": "^15.2.7",
50 | "prettier": "^3.3.3",
51 | "puppeteer": "13.5.1",
52 | "puppeteer-core": "13.5.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/sponsors/302AI.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangrongding/wechat-bot/4b0c6de44c022923df5dc2e772dcf991750f59bf/sponsors/302AI.png
--------------------------------------------------------------------------------
/sponsors/server.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangrongding/wechat-bot/4b0c6de44c022923df5dc2e772dcf991750f59bf/sponsors/server.jpg
--------------------------------------------------------------------------------
/src/302ai/__test__.js:
--------------------------------------------------------------------------------
1 | import { get302AiReply } from './index.js'
2 |
3 | // 测试 302 ai api
4 | async function testMessage() {
5 | const message = await get302AiReply('hello')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 |
9 | testMessage()
10 |
--------------------------------------------------------------------------------
/src/302ai/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import dotenv from 'dotenv'
3 |
4 | dotenv.config()
5 | const env = dotenv.config().parsed
6 | const key = env._302AI_API_KEY
7 | const model = env._302AI_MODEL ? env._302AI_MODEL : 'gpt-4o-mini'
8 |
9 | function setConfig(prompt) {
10 | return {
11 | method: 'post',
12 | url: 'https://api.302.ai/v1/chat/completions',
13 | headers: {
14 | Accept: 'application/json',
15 | Authorization: `Bearer ${key}`,
16 | 'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
17 | 'Content-Type': 'application/json',
18 | },
19 | data: JSON.stringify({
20 | model: model,
21 | messages: [
22 | {
23 | role: 'user',
24 | content: prompt,
25 | },
26 | ],
27 | }),
28 | }
29 | }
30 |
31 | export async function get302AiReply(prompt) {
32 | try {
33 | const config = setConfig(prompt)
34 | const response = await axios(config)
35 | const { choices } = response.data
36 | return choices[0].message.content
37 | } catch (error) {
38 | console.error(error.code)
39 | console.error(error.message)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/chatgpt/index.js:
--------------------------------------------------------------------------------
1 | import { ChatGPTAPI } from 'chatgpt'
2 | import dotenv from 'dotenv'
3 |
4 | const env = dotenv.config().parsed // 环境参数
5 |
6 | // 定义ChatGPT的配置
7 | const config = {
8 | markdown: true, // 返回的内容是否需要markdown格式
9 | AutoReply: true, // 是否自动回复
10 | clearanceToken: env.CHATGPT_CLEARANCE, // ChatGPT的clearance,从cookie取值
11 | sessionToken: env.CHATGPT_SESSION_TOKEN, // ChatGPT的sessionToken, 从cookie取值
12 | userAgent: env.CHATGPT_USER_AGENT, // ChatGPT的user-agent,从浏览器取值,或者替换为与你的真实浏览器的User-Agent相匹配的值
13 | accessToken: env.CHATGPT_ACCESS_TOKEN, // 在用户授权情况下,访问https://chat.openai.com/api/auth/session,获取accesstoken
14 | }
15 | const api = new ChatGPTAPI(config)
16 |
17 | // 获取 chatGPT 的回复
18 | export async function getChatGPTReply(content) {
19 | await api.ensureAuth()
20 | console.log('🚀🚀🚀 / content', content)
21 | // 调用ChatGPT的接口
22 | const reply = await api.sendMessage(content, {
23 | // "ChatGPT 请求超时!最好开下全局代理。"
24 | timeoutMs: 2 * 60 * 1000,
25 | })
26 | console.log('🚀🚀🚀 / reply', reply)
27 | return reply
28 |
29 | // // 如果你想要连续语境对话,可以使用下面的代码
30 | // const conversation = api.getConversation();
31 | // return await conversation.sendMessage(content, {
32 | // // "ChatGPT 请求超时!最好开下全局代理。"
33 | // timeoutMs: 2 * 60 * 1000,
34 | // });
35 | }
36 |
--------------------------------------------------------------------------------
/src/claude/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import dotenv from 'dotenv'
3 |
4 | dotenv.config()
5 | const env = dotenv.config().parsed
6 | const key = env.CLAUDE_API_KEY || ''
7 | const model = env.CLAUDE_MODEL ? env.CLAUDE_MODEL : 'claude-3-5-sonnet-latest'
8 | const system = env.CLAUDE_SYSTEM || ''
9 | const apiVersion = env.CLAUDE_API_VERSION || '2023-06-01'
10 |
11 | function claudeConfig(prompt) {
12 | const body = {
13 | model: model,
14 | temperature: 0.4,
15 | max_tokens: 1024,
16 | messages: [
17 | {
18 | role: 'user',
19 | content: prompt,
20 | },
21 | ],
22 | }
23 | if (system !== '') {
24 | body.system = system
25 | }
26 | return {
27 | method: 'post',
28 | url: 'https://api.anthropic.com/v1/messages',
29 | headers: {
30 | 'x-api-key': key,
31 | 'anthropic-version': apiVersion,
32 | 'Content-Type': 'application/json',
33 | },
34 | data: body,
35 | }
36 | }
37 | export async function getClaudeReply(prompt) {
38 | try {
39 | const claude = claudeConfig(prompt)
40 | const reply = await axios(claude)
41 | return reply.data.content[0].text || ''
42 | } catch (error) {
43 | console.error(error)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/deepseek-free/__test__.js:
--------------------------------------------------------------------------------
1 | import { getDeepSeekFreeReply } from './index.js'
2 |
3 | // 测试 open ai api
4 | async function testMessage() {
5 | const message = await getDeepSeekFreeReply('hello')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 |
9 | testMessage()
10 |
--------------------------------------------------------------------------------
/src/deepseek-free/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import dotenv from 'dotenv'
3 | // 加载环境变量
4 | dotenv.config()
5 | const env = dotenv.config().parsed // 环境参数
6 | const token = env.DEEPSEEK_FREE_TOKEN
7 | const model = env.DEEPSEEK_FREE_MODEL
8 | const url = env.DEEPSEEK_FREE_URL
9 | const syscontent = env.DEEPSEEK_FREE_SYSTEM_MESSAGE
10 |
11 | function setConfig(prompt) {
12 | return {
13 | method: 'post',
14 | maxBodyLength: Infinity,
15 | // url: 'https://api.deepseek.com/chat/completions',
16 | url: url,
17 | headers: {
18 | 'Content-Type': 'application/json',
19 | Accept: 'application/json',
20 | Authorization: `Bearer ${token}`,
21 | },
22 | data: JSON.stringify({
23 | model: model,
24 | messages: [
25 | {
26 | role: 'system',
27 | content: syscontent,
28 | },
29 | {
30 | role: 'user',
31 | content: prompt,
32 | },
33 | ],
34 | stream: false,
35 | }),
36 | }
37 | }
38 |
39 | export async function getDeepSeekFreeReply(prompt) {
40 | try {
41 | const config = setConfig(prompt)
42 | const response = await axios(config)
43 | const { choices } = response.data
44 | return choices[0].message.content
45 | } catch (error) {
46 | console.error(error.code)
47 | console.error(error.message)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/deepseek/__test__.js:
--------------------------------------------------------------------------------
1 | import { getDoubaoReply } from './index.js'
2 |
3 | // 测试 open ai api
4 | async function testMessage() {
5 | const message = await getDoubaoReply('猪可以吃钛合金吗')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 |
9 | testMessage()
10 |
--------------------------------------------------------------------------------
/src/deepseek/index.js:
--------------------------------------------------------------------------------
1 | import { remark } from 'remark'
2 | import stripMarkdown from 'strip-markdown'
3 | import OpenAI from 'openai'
4 | import dotenv from 'dotenv'
5 | const env = dotenv.config().parsed // 环境参数
6 | import fs from 'fs'
7 | import path from 'path'
8 |
9 | const __dirname = path.resolve()
10 | // 判断是否有 .env 文件, 没有则报错
11 | const envPath = path.join(__dirname, '.env')
12 | if (!fs.existsSync(envPath)) {
13 | console.log('❌ 请先根据文档,创建并配置.env文件!')
14 | process.exit(1)
15 | }
16 |
17 | let config = {
18 | apiKey: env.DEEPSEEK_API_KEY,
19 | }
20 | if (env.DEEPSEEK_URL) {
21 | config.baseURL = env.DEEPSEEK_URL
22 | }
23 | const openai = new OpenAI(config)
24 | const chosen_model = env.DEEPSEEK_MODEL
25 | export async function getDeepseekReply(prompt) {
26 | console.log('🚀🚀🚀 / prompt', prompt)
27 | const response = await openai.chat.completions.create({
28 | messages: [
29 | { role: 'system', content: env.DEEPSEEK_SYSTEM_MESSAGE },
30 | { role: 'user', content: prompt },
31 | ],
32 | model: chosen_model,
33 | })
34 | console.log('🚀🚀🚀 / reply', response.choices[0].message.content)
35 | return `${response.choices[0].message.content}`
36 | }
37 |
--------------------------------------------------------------------------------
/src/dify/__test__.js:
--------------------------------------------------------------------------------
1 | import { getDifyReply } from './index.js'
2 |
3 | // 测试 dify api
4 | async function testMessage() {
5 | const message = await getDifyReply('hello')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 |
9 | testMessage()
10 |
--------------------------------------------------------------------------------
/src/dify/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import dotenv from 'dotenv'
3 | // 加载环境变量
4 | dotenv.config()
5 | const env = dotenv.config().parsed // 环境参数
6 | const token = env.DIFY_API_KEY
7 | const url = env.DIFY_URL
8 | const bot_name = env.BOT_NAME
9 | function setConfig(prompt) {
10 | return {
11 | method: 'post',
12 | url: `${url}/chat-messages`,
13 | headers: {
14 | 'Content-Type': 'application/json',
15 | Accept: 'application/json',
16 | Authorization: `Bearer ${token}`,
17 | },
18 | data: JSON.stringify({
19 | inputs: {},
20 | query: prompt,
21 | response_mode: 'blocking',
22 | user: bot_name,
23 | files: [],
24 | }),
25 | }
26 | }
27 |
28 | export async function getDifyReply(prompt) {
29 | try {
30 | const config = setConfig(prompt)
31 | console.log('🌸🌸🌸 / config: ', config)
32 | const response = await axios(config)
33 | console.log('🌸🌸🌸 / response: ', response)
34 | return response.data.answer
35 | } catch (error) {
36 | console.error(error.code)
37 | console.error(error.message)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/doubao/__test__.js:
--------------------------------------------------------------------------------
1 | import { getDoubaoReply } from './index.js'
2 |
3 | // 测试 open ai api
4 | async function testMessage() {
5 | const message = await getDoubaoReply('猪可以吃钛合金吗')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 |
9 | testMessage()
10 |
--------------------------------------------------------------------------------
/src/doubao/index.js:
--------------------------------------------------------------------------------
1 | import { remark } from 'remark'
2 | import stripMarkdown from 'strip-markdown'
3 | import OpenAI from 'openai'
4 | import dotenv from 'dotenv'
5 | const env = dotenv.config().parsed // 环境参数
6 | import fs from 'fs'
7 | import path from 'path'
8 |
9 | const __dirname = path.resolve()
10 | // 判断是否有 .env 文件, 没有则报错
11 | const envPath = path.join(__dirname, '.env')
12 | if (!fs.existsSync(envPath)) {
13 | console.log('❌ 请先根据文档,创建并配置.env文件!')
14 | process.exit(1)
15 | }
16 |
17 | let config = {
18 | apiKey: env.DOUBAO_API_KEY,
19 | }
20 | if (env.DOUBAO_URL) {
21 | config.baseURL = env.DOUBAO_URL
22 | }
23 | const openai = new OpenAI(config)
24 | const chosen_model = env.DOUBAO_MODEL
25 | export async function getDoubaoReply(prompt) {
26 | console.log('🚀🚀🚀 / prompt', prompt)
27 | const response = await openai.chat.completions.create({
28 | messages: [
29 | { role: 'system', content: env.DOUBAO_SYSTEM_MESSAGE },
30 | { role: 'user', content: prompt },
31 | ],
32 | model: chosen_model,
33 | })
34 | console.log('🚀🚀🚀 / reply', response.choices[0].message.content)
35 | return `${response.choices[0].message.content}`
36 | }
37 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { Command } from 'commander'
2 | import { WechatyBuilder, ScanStatus, log } from 'wechaty'
3 | import inquirer from 'inquirer'
4 | import qrTerminal from 'qrcode-terminal'
5 | import dotenv from 'dotenv'
6 |
7 | import fs from 'fs'
8 | import path, { dirname } from 'path'
9 | import { fileURLToPath } from 'url'
10 | import { defaultMessage } from './wechaty/sendMessage.js'
11 |
12 | const __filename = fileURLToPath(import.meta.url)
13 | const __dirname = dirname(__filename)
14 | const env = dotenv.config().parsed // 环境参数
15 | const { version, name } = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'))
16 |
17 | // 扫码
18 | function onScan(qrcode, status) {
19 | if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) {
20 | // 在控制台显示二维码
21 | qrTerminal.generate(qrcode, { small: true })
22 | const qrcodeImageUrl = ['https://api.qrserver.com/v1/create-qr-code/?data=', encodeURIComponent(qrcode)].join('')
23 | console.log('onScan:', qrcodeImageUrl, ScanStatus[status], status)
24 | } else {
25 | log.info('onScan: %s(%s)', ScanStatus[status], status)
26 | }
27 | }
28 |
29 | // 登录
30 | function onLogin(user) {
31 | console.log(`${user} has logged in`)
32 | const date = new Date()
33 | console.log(`Current time:${date}`)
34 | console.log(`Automatic robot chat mode has been activated`)
35 | }
36 |
37 | // 登出
38 | function onLogout(user) {
39 | console.log(`${user} has logged out`)
40 | }
41 |
42 | // 收到好友请求
43 | async function onFriendShip(friendship) {
44 | const frienddShipRe = /chatgpt|chat/
45 | if (friendship.type() === 2) {
46 | if (frienddShipRe.test(friendship.hello())) {
47 | await friendship.accept()
48 | }
49 | }
50 | }
51 |
52 | /**
53 | * 消息发送
54 | * @param msg
55 | * @param isSharding
56 | * @returns {Promise}
57 | */
58 | async function onMessage(msg) {
59 | // 默认消息回复
60 | await defaultMessage(msg, bot, serviceType)
61 | // 消息分片
62 | // await shardingMessage(msg,bot)
63 | }
64 |
65 | // 初始化机器人
66 | const CHROME_BIN = process.env.CHROME_BIN ? { endpoint: process.env.CHROME_BIN } : {}
67 | let serviceType = ''
68 | export const bot = WechatyBuilder.build({
69 | name: 'WechatEveryDay',
70 | puppet: 'wechaty-puppet-wechat4u', // 如果有token,记得更换对应的puppet
71 | // puppet: 'wechaty-puppet-wechat', // 如果 wechaty-puppet-wechat 存在问题,也可以尝试使用上面的 wechaty-puppet-wechat4u ,记得安装 wechaty-puppet-wechat4u
72 | puppetOptions: {
73 | uos: true,
74 | ...CHROME_BIN,
75 | },
76 | })
77 |
78 | // 扫码
79 | bot.on('scan', onScan)
80 | // 登录
81 | bot.on('login', onLogin)
82 | // 登出
83 | bot.on('logout', onLogout)
84 | // 收到消息
85 | bot.on('message', onMessage)
86 | // 添加好友
87 | bot.on('friendship', onFriendShip)
88 | // 错误
89 | bot.on('error', (e) => {
90 | console.error('❌ bot error handle: ', e)
91 | // console.log('❌ 程序退出,请重新运行程序')
92 | // bot.stop()
93 |
94 | // // 如果 WechatEveryDay.memory-card.json 文件存在,删除
95 | // if (fs.existsSync('WechatEveryDay.memory-card.json')) {
96 | // fs.unlinkSync('WechatEveryDay.memory-card.json')
97 | // }
98 | // process.exit()
99 | })
100 |
101 | // 启动微信机器人
102 | function botStart() {
103 | bot
104 | .start()
105 | .then(() => console.log('Start to log in wechat...'))
106 | .catch((e) => console.error('❌ botStart error: ', e))
107 | }
108 |
109 | process.on('uncaughtException', (err) => {
110 | if (err.code === 'ERR_ASSERTION') {
111 | console.error('❌ uncaughtException 捕获到断言错误: ', err.message)
112 | } else {
113 | console.error('❌ uncaughtException 捕获到未处理的异常: ', err)
114 | }
115 | // if (fs.existsSync('WechatEveryDay.memory-card.json')) {
116 | // fs.unlinkSync('WechatEveryDay.memory-card.json')
117 | // }
118 | })
119 |
120 | // 控制启动
121 | function handleStart(type) {
122 | serviceType = type
123 | console.log('🌸🌸🌸 / type: ', type)
124 | switch (type) {
125 | case 'ChatGPT':
126 | if (env.OPENAI_API_KEY) return botStart()
127 | console.log('❌ 请先配置.env文件中的 OPENAI_API_KEY')
128 | break
129 | case 'doubao':
130 | if (env.DOUBAO_API_KEY) return botStart()
131 | console.log('❌ 请先配置.env文件中的 DOUBAO_API_KEY')
132 | break
133 | case 'deepseek':
134 | if (env.DEEPSEEK_API_KEY) return botStart()
135 | console.log('❌ 请先配置.env文件中的 DEEPSEEK_API_KEY')
136 | break
137 | case 'Kimi':
138 | if (env.KIMI_API_KEY) return botStart()
139 | console.log('❌ 请先配置.env文件中的 KIMI_API_KEY')
140 | break
141 | case 'Xunfei':
142 | if (env.XUNFEI_APP_ID && env.XUNFEI_API_KEY && env.XUNFEI_API_SECRET) {
143 | return botStart()
144 | }
145 | console.log('❌ 请先配置.env文件中的 XUNFEI_APP_ID,XUNFEI_API_KEY,XUNFEI_API_SECRET')
146 | break
147 | case 'deepseek-free':
148 | if (env.DEEPSEEK_FREE_URL && env.DEEPSEEK_FREE_TOKEN && env.DEEPSEEK_FREE_MODEL) {
149 | return botStart()
150 | }
151 | console.log('❌ 请先配置.env文件中的 DEEPSEEK_FREE_URL,DEEPSEEK_FREE_TOKEN,DEEPSEEK_FREE_MODEL')
152 | break
153 | case '302AI':
154 | if (env._302AI_API_KEY) {
155 | return botStart()
156 | }
157 | console.log('❌ 请先配置.env文件中的 _302AI_API_KEY')
158 | break
159 | case 'dify':
160 | if (env.DIFY_API_KEY && env.DIFY_URL) {
161 | return botStart()
162 | }
163 | console.log('❌ 请先配置.env文件中的 DIFY_API_KEY')
164 | break
165 | case 'ollama':
166 | if (env.OLLAMA_URL && env.OLLAMA_MODEL) {
167 | return botStart()
168 | }
169 | break
170 | case 'tongyi':
171 | if (env.TONGYI_URL && env.TONGYI_MODEL) {
172 | return botStart()
173 | }
174 | break
175 | case 'claude':
176 | if (env.CLAUDE_API_KEY && env.CLAUDE_MODEL) {
177 | return botStart()
178 | }
179 | console.log('❌ 请先配置.env文件中的 CLAUDE_API_KEY 和 CLAUDE_MODEL')
180 | break
181 | default:
182 | console.log('❌ 服务类型错误, 目前支持: ChatGPT | doubao | deepseek | Kimi | Xunfei | DIFY | OLLAMA | TONGYI')
183 | }
184 | }
185 |
186 | export const serveList = [
187 | { name: 'ChatGPT', value: 'ChatGPT' },
188 | { name: 'doubao', value: 'doubao' },
189 | { name: 'deepseek', value: 'deepseek' },
190 | { name: 'Kimi', value: 'Kimi' },
191 | { name: 'Xunfei', value: 'Xunfei' },
192 | { name: 'deepseek-free', value: 'deepseek-free' },
193 | { name: '302AI', value: '302AI' },
194 | { name: 'dify', value: 'dify' },
195 | // ... 欢迎大家接入更多的服务
196 | { name: 'ollama', value: 'ollama' },
197 | { name: 'tongyi', value: 'tongyi' },
198 | { name: 'claude', value: 'claude' },
199 | ]
200 | const questions = [
201 | {
202 | type: 'list',
203 | name: 'serviceType', //存储当前问题回答的变量key,
204 | message: '请先选择服务类型',
205 | choices: serveList,
206 | },
207 | ]
208 |
209 | function init() {
210 | if (env.SERVICE_TYPE) {
211 | // 判断env中SERVICE_TYPE是否配置和并且属于serveList数组中value的值
212 | if (serveList.find((item) => item.value === env.SERVICE_TYPE)) {
213 | handleStart(env.SERVICE_TYPE)
214 | } else {
215 | console.log('❌ 请正确配置.env文件中的 SERVICE_TYPE,或者删除该项')
216 | }
217 | } else {
218 | inquirer
219 | .prompt(questions)
220 | .then((res) => {
221 | handleStart(res.serviceType)
222 | })
223 | .catch((error) => {
224 | console.log('❌ inquirer error:', error)
225 | })
226 | }
227 | }
228 |
229 | const program = new Command(name)
230 | program
231 | .alias('we')
232 | .description('🤖一个基于 WeChaty 结合AI服务实现的微信机器人。')
233 | .version(version, '-v, --version, -V')
234 | .option('-s, --serve ', '跳过交互,直接设置启动的服务类型')
235 | // .option('-p, --proxy ', 'proxy url', '')
236 | .action(function () {
237 | const { serve } = this.opts()
238 | const args = this.args
239 | if (!serve) return init()
240 | handleStart(serve)
241 | })
242 | .command('start')
243 | .option('-s, --serve ', '跳过交互,直接设置启动的服务类型', '')
244 | .action(() => init())
245 |
246 | // program
247 | // .command('config')
248 | // .option('-d, --depth ', 'Set the depth of the folder to be traversed', '10')
249 | // .action(() => {
250 | // // 打印当前项目的路径,而不是执行该文件时的所在路径
251 | // console.log('请手动修改下面路径中的 config.json 文件')
252 | // console.log(path.resolve(__dirname, '../.env'))
253 | // })
254 | program.parse()
255 |
--------------------------------------------------------------------------------
/src/kimi/__test__.js:
--------------------------------------------------------------------------------
1 | import { getKimiReply } from './index.js'
2 |
3 | // 测试 open ai api
4 | async function test() {
5 | const message = await getKimiReply('你好!')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 | test()
9 |
--------------------------------------------------------------------------------
/src/kimi/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import dotenv from 'dotenv'
3 | const env = dotenv.config().parsed // 环境参数
4 |
5 | const domain = 'https://api.moonshot.cn'
6 | const server = {
7 | chat: `${domain}/v1/chat/completions`,
8 | models: `${domain}/v1/models`,
9 | files: `${domain}/v1/files`,
10 | token: `${domain}/v1/tokenizers/estimate-token-count`,
11 | // 这块还可以实现上传文件让 kimi 读取并交互等操作
12 | // 具体参考文档: https://platform.moonshot.cn/docs/api-reference#api-%E8%AF%B4%E6%98%8E
13 | // 由于我近期非常忙碌,这块欢迎感兴趣的同学提 PR ,我会很快合并
14 | }
15 |
16 | const configuration = {
17 | // 参数详情请参考 https://platform.moonshot.cn/docs/api-reference#%E5%AD%97%E6%AE%B5%E8%AF%B4%E6%98%8E
18 | /*
19 | Model ID, 可以通过 List Models 获取
20 | 目前可选 moonshot-v1-8k | moonshot-v1-32k | moonshot-v1-128k
21 | */
22 | model: 'moonshot-v1-8k',
23 | /*
24 | 使用什么采样温度,介于 0 和 1 之间。较高的值(如 0.7)将使输出更加随机,而较低的值(如 0.2)将使其更加集中和确定性。
25 | 如果设置,值域须为 [0, 1] 我们推荐 0.3,以达到较合适的效果。
26 | */
27 | temperature: 0.3,
28 | /*
29 | 聊天完成时生成的最大 token 数。如果到生成了最大 token 数个结果仍然没有结束,finish reason 会是 "length", 否则会是 "stop"
30 | 这个值建议按需给个合理的值,如果不给的话,我们会给一个不错的整数比如 1024。特别要注意的是,这个 max_tokens 是指您期待我们返回的 token 长度,而不是输入 + 输出的总长度。
31 | 比如对一个 moonshot-v1-8k 模型,它的最大输入 + 输出总长度是 8192,当输入 messages 总长度为 4096 的时候,您最多只能设置为 4096,
32 | 否则我们服务会返回不合法的输入参数( invalid_request_error ),并拒绝回答。如果您希望获得“输入的精确 token 数”,可以使用下面的“计算 Token” API 使用我们的计算器获得计数。
33 | */
34 | max_tokens: 5000,
35 | /*
36 | 是否流式返回, 默认 false, 可选 true
37 | */
38 | stream: true,
39 | }
40 |
41 | export async function getKimiReply(prompt) {
42 | try {
43 | const res = await axios.post(
44 | server.chat,
45 | Object.assign(configuration, {
46 | /*
47 | 包含迄今为止对话的消息列表。
48 | 要保持对话的上下文,需要将之前的对话历史并入到该数组
49 | 这是一个结构体的列表,每个元素类似如下:{"role": "user", "content": "你好"} role 只支持 system,user,assistant 其一,content 不得为空
50 | */
51 | messages: [
52 | {
53 | role: 'user',
54 | content: prompt,
55 | },
56 | ],
57 | model: 'moonshot-v1-128k',
58 | }),
59 | {
60 | timeout: 120000,
61 | headers: {
62 | 'Content-Type': 'application/json',
63 | Authorization: `Bearer ${env.KIMI_API_KEY}`,
64 | },
65 | // pass a http proxy agent
66 | // proxy: {
67 | // host: 'localhost',
68 | // port: 7890,
69 | // }
70 | },
71 | )
72 | if (!configuration.stream) return res.data.choices[0].message.content
73 |
74 | let result = ''
75 | const lines = res.data.split('\n').filter((line) => line.trim() !== '')
76 | for (const line of lines) {
77 | if (line.startsWith('data: ')) {
78 | const messageObj = line.substring(6)
79 | if (messageObj === '[DONE]') break
80 | const message = JSON.parse(messageObj)
81 | if (message.choices && message.choices[0].delta && message.choices[0].delta.content) {
82 | result += message.choices[0].delta.content
83 | }
84 | }
85 | }
86 | return result
87 | } catch (error) {
88 | console.log('Kimi 错误对应详情可参考官网: https://platform.moonshot.cn/docs/api-reference#%E9%94%99%E8%AF%AF%E8%AF%B4%E6%98%8E')
89 | console.log('常见的 401 一般意味着你鉴权失败, 请检查你的 API_KEY 是否正确。')
90 | console.log('常见的 429 一般意味着你被限制了请求频次,请求频率过高,或 kimi 服务器过载,可以适当调整请求频率,或者等待一段时间再试。')
91 | console.error(error.code)
92 | console.error(error.message)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/ollama/__test__.js:
--------------------------------------------------------------------------------
1 | import { getDifyReply } from './index.js'
2 |
3 | // 测试 dify api
4 | async function testMessage() {
5 | const message = await getDifyReply('hello')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 |
9 | testMessage()
10 |
--------------------------------------------------------------------------------
/src/ollama/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import dotenv from 'dotenv'
3 | // 加载环境变量
4 | dotenv.config()
5 | const env = dotenv.config().parsed // 环境参数
6 | const url = env.OLLAMA_URL
7 | const bot_name = env.BOT_NAME
8 | const model_name = env.OLLAMA_MODEL
9 | function createRequest(prompt) {
10 | return {
11 | method: 'post',
12 | url: url,
13 | headers: {
14 | 'Content-Type': 'application/json',
15 | Accept: 'application/json',
16 | },
17 | data: JSON.stringify({
18 | model: model_name,
19 | messages: [
20 | {
21 | role: 'system',
22 | content: env.OLLAMA_SYSTEM_MESSAGE,
23 | },
24 | {
25 | role: 'user',
26 | content: prompt,
27 | },
28 | ],
29 | stream: false,
30 | }),
31 | }
32 | }
33 |
34 | export async function getOllamaReply(prompt) {
35 | try {
36 | console.log('=============== ollama request start ======================')
37 | const request = createRequest(prompt)
38 | const res = await axios(request)
39 | console.log('=============== ollama request finished ======================')
40 | return res.data.message.content
41 | } catch (error) {
42 | console.error(error.code)
43 | console.error(error.message)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/openai/__test__.js:
--------------------------------------------------------------------------------
1 | import { getGptReply } from './index.js'
2 |
3 | // 测试 open ai api
4 | async function testMessage() {
5 | const message = await getGptReply('hello')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 |
9 | testMessage()
10 |
--------------------------------------------------------------------------------
/src/openai/index.js:
--------------------------------------------------------------------------------
1 | import { remark } from 'remark'
2 | import stripMarkdown from 'strip-markdown'
3 | import OpenAIApi from 'openai'
4 | import dotenv from 'dotenv'
5 | const env = dotenv.config().parsed // 环境参数
6 | import fs from 'fs'
7 | import path from 'path'
8 |
9 | const __dirname = path.resolve()
10 | // 判断是否有 .env 文件, 没有则报错
11 | const envPath = path.join(__dirname, '.env')
12 | if (!fs.existsSync(envPath)) {
13 | console.log('❌ 请先根据文档,创建并配置.env文件!')
14 | process.exit(1)
15 | }
16 |
17 | let config = {
18 | apiKey: env.OPENAI_API_KEY,
19 | organization: '',
20 | }
21 | if (env.OPENAI_PROXY_URL) {
22 | config.baseURL = env.OPENAI_PROXY_URL
23 | }
24 | const openai = new OpenAIApi(config)
25 | const chosen_model = env.OPENAI_MODEL || 'gpt-4o'
26 | export async function getGptReply(prompt) {
27 | console.log('🚀🚀🚀 / prompt', prompt)
28 | const response = await openai.chat.completions.create({
29 | messages: [
30 | { role: 'system', content: env.OPENAI_SYSTEM_MESSAGE },
31 | { role: 'user', content: prompt },
32 | ],
33 | model: chosen_model,
34 | })
35 | console.log('🚀🚀🚀 / reply', response.choices[0].message.content)
36 | return `${response.choices[0].message.content}`
37 | }
38 |
--------------------------------------------------------------------------------
/src/tongyi/index.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import dotenv from 'dotenv'
4 | import OpenAI from 'openai'
5 |
6 | const env = dotenv.config().parsed // 环境参数
7 | // 加载环境变量
8 | dotenv.config()
9 | const url = env.TONGYI_URL
10 | const api_key = env.TONGYI_API_KEY
11 | const model_name = env.TONGYI_MODEL || 'qwen-plus'
12 |
13 | const openai = new OpenAI({
14 | apiKey: api_key,
15 | baseURL: url,
16 | temperature: 0,
17 | })
18 |
19 | const __dirname = path.resolve()
20 | // 判断是否有 .env 文件, 没有则报错
21 | const envPath = path.join(__dirname, '.env')
22 | if (!fs.existsSync(envPath)) {
23 | console.log('❌ 请先根据文档,创建并配置 .env 文件!')
24 | process.exit(1)
25 | }
26 |
27 | export async function getTongyiReply(prompt) {
28 | const completion = await openai.chat.completions.create({
29 | messages: [
30 | {
31 | role: 'user',
32 | content: prompt + ' ,用中文回答',
33 | },
34 | ],
35 | model: model_name,
36 | })
37 |
38 | console.log('🚀🚀🚀 / prompt', prompt)
39 | const Content = await completion.choices[0].message.content
40 | console.log('🚀🚀🚀 / reply', Content)
41 | return `${Content}`
42 | }
43 |
--------------------------------------------------------------------------------
/src/wechaty/sendMessage.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv'
2 | // 加载环境变量
3 | dotenv.config()
4 | const env = dotenv.config().parsed // 环境参数
5 |
6 | // 从环境变量中导入机器人的名称
7 | const botName = env.BOT_NAME
8 |
9 | // 从环境变量中导入需要自动回复的消息前缀,默认配空串或不配置则等于无前缀
10 | const autoReplyPrefix = env.AUTO_REPLY_PREFIX ? env.AUTO_REPLY_PREFIX : ''
11 |
12 | // 从环境变量中导入联系人白名单
13 | const aliasWhiteList = env.ALIAS_WHITELIST ? env.ALIAS_WHITELIST.split(',') : []
14 |
15 | // 从环境变量中导入群聊白名单
16 | const roomWhiteList = env.ROOM_WHITELIST ? env.ROOM_WHITELIST.split(',') : []
17 |
18 | import { getServe } from './serve.js'
19 |
20 | /**
21 | * 默认消息发送
22 | * @param msg
23 | * @param bot
24 | * @param ServiceType 服务类型 'GPT' | 'Kimi'
25 | * @returns {Promise}
26 | */
27 | export async function defaultMessage(msg, bot, ServiceType = 'GPT') {
28 | const getReply = getServe(ServiceType)
29 | const contact = msg.talker() // 发消息人
30 | const receiver = msg.to() // 消息接收人
31 | const content = msg.text() // 消息内容
32 | const room = msg.room() // 是否是群消息
33 | const roomName = (await room?.topic()) || null // 群名称
34 | const alias = (await contact.alias()) || (await contact.name()) // 发消息人昵称
35 | const remarkName = await contact.alias() // 备注名称
36 | const name = await contact.name() // 微信名称
37 | const isText = msg.type() === bot.Message.Type.Text // 消息类型是否为文本
38 | const isRoom = roomWhiteList.includes(roomName) && content.includes(`${botName}`) // 是否在群聊白名单内并且艾特了机器人
39 | const isAlias = aliasWhiteList.includes(remarkName) || aliasWhiteList.includes(name) // 发消息的人是否在联系人白名单内
40 | const isBotSelf = botName === `@${remarkName}` || botName === `@${name}` // 是否是机器人自己
41 | const isBotSelfDebug = content.trimStart().startsWith('你是谁') // 是否是机器人自己的调试消息
42 | // TODO 你们可以根据自己的需求修改这里的逻辑
43 | if ((isBotSelf && !isBotSelfDebug) || !isText) return // 如果是机器人自己发送的消息或者消息类型不是文本则不处理
44 | try {
45 | // 区分群聊和私聊
46 | // 群聊消息去掉艾特主体后,匹配自动回复前缀
47 | if (isRoom && room && content.replace(`${botName}`, '').trimStart().startsWith(`${autoReplyPrefix}`)) {
48 | const question = (await msg.mentionText()) || content.replace(`${botName}`, '').replace(`${autoReplyPrefix}`, '') // 去掉艾特的消息主体
49 | console.log('🌸🌸🌸 / question: ', question)
50 | const response = await getReply(question)
51 | await room.say(response)
52 | }
53 | // 私人聊天,白名单内的直接发送
54 | // 私人聊天直接匹配自动回复前缀
55 | if (isAlias && !room && content.trimStart().startsWith(`${autoReplyPrefix}`)) {
56 | const question = content.replace(`${autoReplyPrefix}`, '')
57 | console.log('🌸🌸🌸 / content: ', question)
58 | const response = await getReply(question)
59 | await contact.say(response)
60 | }
61 | } catch (e) {
62 | console.error(e)
63 | }
64 | }
65 |
66 | /**
67 | * 分片消息发送
68 | * @param message
69 | * @param bot
70 | * @returns {Promise}
71 | */
72 | export async function shardingMessage(message, bot) {
73 | const talker = message.talker()
74 | const isText = message.type() === bot.Message.Type.Text // 消息类型是否为文本
75 | if (talker.self() || message.type() > 10 || (talker.name() === '微信团队' && isText)) {
76 | return
77 | }
78 | const text = message.text()
79 | const room = message.room()
80 | if (!room) {
81 | console.log(`Chat GPT Enabled User: ${talker.name()}`)
82 | const response = await getChatGPTReply(text)
83 | await trySay(talker, response)
84 | return
85 | }
86 | let realText = splitMessage(text)
87 | // 如果是群聊但不是指定艾特人那么就不进行发送消息
88 | if (text.indexOf(`${botName}`) === -1) {
89 | return
90 | }
91 | realText = text.replace(`${botName}`, '')
92 | const topic = await room.topic()
93 | const response = await getChatGPTReply(realText)
94 | const result = `${realText}\n ---------------- \n ${response}`
95 | await trySay(room, result)
96 | }
97 |
98 | // 分片长度
99 | const SINGLE_MESSAGE_MAX_SIZE = 500
100 |
101 | /**
102 | * 发送
103 | * @param talker 发送哪个 room为群聊类 text为单人
104 | * @param msg
105 | * @returns {Promise}
106 | */
107 | async function trySay(talker, msg) {
108 | const messages = []
109 | let message = msg
110 | while (message.length > SINGLE_MESSAGE_MAX_SIZE) {
111 | messages.push(message.slice(0, SINGLE_MESSAGE_MAX_SIZE))
112 | message = message.slice(SINGLE_MESSAGE_MAX_SIZE)
113 | }
114 | messages.push(message)
115 | for (const msg of messages) {
116 | await talker.say(msg)
117 | }
118 | }
119 |
120 | /**
121 | * 分组消息
122 | * @param text
123 | * @returns {Promise<*>}
124 | */
125 | async function splitMessage(text) {
126 | let realText = text
127 | const item = text.split('- - - - - - - - - - - - - - -')
128 | if (item.length > 1) {
129 | realText = item[item.length - 1]
130 | }
131 | return realText
132 | }
133 |
--------------------------------------------------------------------------------
/src/wechaty/serve.js:
--------------------------------------------------------------------------------
1 | import { getGptReply } from '../openai/index.js'
2 | import { getDoubaoReply } from '../doubao/index.js'
3 | import { getDeepseekReply } from '../deepseek/index.js'
4 | import { getKimiReply } from '../kimi/index.js'
5 | import { getXunfeiReply } from '../xunfei/index.js'
6 | import { getDeepSeekFreeReply } from '../deepseek-free/index.js'
7 | import { get302AiReply } from '../302ai/index.js'
8 | import { getDifyReply } from '../dify/index.js'
9 | import { getOllamaReply } from '../ollama/index.js'
10 | import { getTongyiReply } from '../tongyi/index.js'
11 | import { getClaudeReply } from '../claude/index.js'
12 |
13 | /**
14 | * 获取ai服务
15 | * @param serviceType 服务类型 'GPT' | 'Kimi'
16 | * @returns {Promise}
17 | */
18 | export function getServe(serviceType) {
19 | switch (serviceType) {
20 | case 'ChatGPT':
21 | return getGptReply
22 | case 'doubao':
23 | return getDoubaoReply
24 | case 'deepseek':
25 | return getDeepseekReply
26 | case 'Kimi':
27 | return getKimiReply
28 | case 'Xunfei':
29 | return getXunfeiReply
30 | case 'deepseek-free':
31 | return getDeepSeekFreeReply
32 | case '302AI':
33 | return get302AiReply
34 | case 'dify':
35 | return getDifyReply
36 | case 'ollama':
37 | return getOllamaReply
38 | case 'tongyi':
39 | return getTongyiReply
40 | case 'claude':
41 | return getClaudeReply
42 | default:
43 | return getGptReply
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/wechaty/testMessage.js:
--------------------------------------------------------------------------------
1 | import { getGptReply } from '../openai/index.js'
2 | import { getKimiReply } from '../kimi/index.js'
3 | import { getXunfeiReply } from '../xunfei/index.js'
4 | import dotenv from 'dotenv'
5 | import inquirer from 'inquirer'
6 | import { getDeepSeekFreeReply } from '../deepseek-free/index.js'
7 | import { get302AiReply } from '../302ai/index.js'
8 | import { getDifyReply } from '../dify/index.js'
9 | import { getOllamaReply } from '../ollama/index.js'
10 | const env = dotenv.config().parsed // 环境参数
11 |
12 | // 控制启动
13 | async function handleRequest(type) {
14 | console.log('type: ', type)
15 | switch (type) {
16 | case 'ChatGPT':
17 | if (env.OPENAI_API_KEY) {
18 | const message = await getGptReply('hello')
19 | console.log('🌸🌸🌸 / reply: ', message)
20 | return
21 | }
22 | console.log('❌ 请先配置.env文件中的 OPENAI_API_KEY')
23 | break
24 | case 'Kimi':
25 | if (env.KIMI_API_KEY) {
26 | const message = await getKimiReply('你好!')
27 | console.log('🌸🌸🌸 / reply: ', message)
28 | return
29 | }
30 | console.log('❌ 请先配置.env文件中的 KIMI_API_KEY')
31 | break
32 | case 'Xunfei':
33 | if (env.XUNFEI_APP_ID && env.XUNFEI_API_KEY && env.XUNFEI_API_SECRET) {
34 | const message = await getXunfeiReply('你好!')
35 | console.log('🌸🌸🌸 / reply: ', message)
36 | return
37 | }
38 | console.log('❌ 请先配置.env文件中的 XUNFEI_APP_ID,XUNFEI_API_KEY,XUNFEI_API_SECRET')
39 | break
40 | case 'deepseek-free':
41 | if (env.DEEPSEEK_FREE_URL && env.DEEPSEEK_FREE_TOKEN && env.DEEPSEEK_FREE_MODEL) {
42 | const message = await getDeepSeekFreeReply('你好!')
43 | console.log('🌸🌸🌸 / reply: ', message)
44 | return
45 | }
46 | console.log('❌ 请先配置.env文件中的 DEEPSEEK_FREE_URL,DEEPSEEK_FREE_TOKEN,DEEPSEEK_FREE_MODEL')
47 | break
48 | case 'dify':
49 | if (env.DIFY_API_KEY) {
50 | const message = await getDifyReply('hello')
51 | console.log('🌸🌸🌸 / reply: ', message)
52 | return
53 | }
54 | console.log('❌ 请先配置.env文件中的 DIFY_API_KEY, DIFY_URL')
55 | break
56 | case '302AI':
57 | if (env._302AI_API_KEY) {
58 | const message = await get302AiReply('hello')
59 | console.log('🌸🌸🌸 / reply: ', message)
60 | return
61 | }
62 | console.log('❌ 请先配置.env文件中的 _302AI_API_KEY')
63 | break
64 | case 'ollama':
65 | if (env.OLLAMA_URL) {
66 | const message = await getOllamaReply('hello')
67 | console.log('🌸🌸🌸 / reply: ', message)
68 | return
69 | }
70 | console.log('❌ 请先配置.env文件中的 OLLAMA_URL')
71 | break
72 | default:
73 | console.log('🚀服务类型错误')
74 | }
75 | }
76 |
77 | const serveList = [
78 | { name: 'ChatGPT', value: 'ChatGPT' },
79 | { name: 'Kimi', value: 'Kimi' },
80 | { name: 'Xunfei', value: 'Xunfei' },
81 | { name: 'deepseek-free', value: 'deepseek-free' },
82 | { name: '302AI', value: '302AI' },
83 | { name: 'dify', value: 'dify' },
84 | // ... 欢迎大家接入更多的服务
85 | { name: 'ollama', value: 'ollama' },
86 | ]
87 | const questions = [
88 | {
89 | type: 'list',
90 | name: 'serviceType', //存储当前问题回答的变量key,
91 | message: '请先选择服务类型',
92 | choices: serveList,
93 | },
94 | ]
95 | function init() {
96 | inquirer
97 | .prompt(questions)
98 | .then((res) => {
99 | handleRequest(res.serviceType)
100 | })
101 | .catch((error) => {
102 | console.log('🚀error:', error)
103 | })
104 | }
105 | init()
106 |
--------------------------------------------------------------------------------
/src/xunfei/__test__.js:
--------------------------------------------------------------------------------
1 | import { getXunfeiReply } from './index.js'
2 |
3 | // 测试 科大讯飞 api
4 | async function testMessage() {
5 | const message = await getXunfeiReply('秦始皇的儿子是谁?')
6 | console.log('🌸🌸🌸 / message: ', message)
7 | }
8 |
9 | testMessage()
10 |
--------------------------------------------------------------------------------
/src/xunfei/index.js:
--------------------------------------------------------------------------------
1 | import { xunfeiSendMsg } from './xunfei.js'
2 |
3 | export async function getXunfeiReply(prompt, name) {
4 | console.log('🚀🚀🚀 / prompt', prompt)
5 | let reply = await xunfeiSendMsg(prompt)
6 |
7 | if (typeof name != 'undefined') reply = `@${name}\n ${reply}`
8 | return `${reply}`
9 | }
10 |
--------------------------------------------------------------------------------
/src/xunfei/xunfei.js:
--------------------------------------------------------------------------------
1 | import CryptoJS from "crypto-js";
2 | import dotenv from "dotenv";
3 | import WebSocket from "ws";
4 |
5 | const env = dotenv.config().parsed; // 环境参数
6 |
7 | // APPID,APISecret,APIKey在 https://console.xfyun.cn/services/cbm 获取
8 | const appID = env.XUNFEI_APP_ID;
9 | const apiKey = env.XUNFEI_API_KEY;
10 | const apiSecret = env.XUNFEI_API_SECRET;
11 |
12 | // 地址必须填写,代表着大模型的版本号
13 | const modelVersion = env.XUNFEI_MODEL_VERSION || "v4.0"; // 默认值 "v4.0"
14 | const httpUrl = new URL(`https://spark-api.xf-yun.com/${modelVersion}/chat`);
15 |
16 | // 判断 prompt 是否存在,如果不存在则使用默认值
17 | const prompt = env.XUNFEI_PROMPT || "你是一个专业的智能助手";
18 |
19 | // 动态映射模型版本到 domain 的逻辑
20 | const modelVersionMap = {
21 | "v1.1": "general",
22 | "v2.1": "generalv2",
23 | "v3.1": "generalv3",
24 | "v3.5": "generalv3.5",
25 | "pro-128k": "pro-128k",
26 | "max-32k": "max-32k",
27 | "v4.0": "4.0Ultra",
28 | };
29 |
30 | // 获取模型域名
31 | function getModelDomain(httpUrl) {
32 | try {
33 | const modelPath = httpUrl.pathname.split("/")[1]; // 提取版本号或模型路径
34 | return modelVersionMap[modelPath] || "unknown"; // 如果没有匹配,返回 "unknown"
35 | } catch (error) {
36 | console.error("获取模型域名失败:", error);
37 | return "unknown";
38 | }
39 | }
40 |
41 | let modelDomain = getModelDomain(httpUrl);
42 |
43 | // 签名生成逻辑(可复用)
44 | function generateSignature(httpUrl, apiKey, apiSecret) {
45 | const host = "localhost:8080";
46 | const date = new Date().toGMTString();
47 | const algorithm = "hmac-sha256";
48 | const headers = "host date request-line";
49 |
50 | const signatureOrigin = `host: ${host}\ndate: ${date}\nGET ${httpUrl.pathname} HTTP/1.1`;
51 | const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
52 | const signature = CryptoJS.enc.Base64.stringify(signatureSha);
53 |
54 | const authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
55 | const authorization = btoa(authorizationOrigin);
56 |
57 | const url = `wss://${httpUrl.host}${httpUrl.pathname}?authorization=${authorization}&date=${date}&host=${host}`;
58 | return url;
59 | }
60 |
61 | // 获取 WebSocket 地址
62 | function authenticate() {
63 | return new Promise((resolve, reject) => {
64 | try {
65 | const url = generateSignature(httpUrl, apiKey, apiSecret);
66 | resolve(url);
67 | } catch (error) {
68 | console.error("认证失败:", error);
69 | reject(error);
70 | }
71 | });
72 | }
73 |
74 | // 发送消息并处理 WebSocket 逻辑
75 | export async function xunfeiSendMsg(inputVal) {
76 | // 获取请求地址
77 | let myUrl = await authenticate();
78 | let socket = new WebSocket(String(myUrl));
79 | let total_res = ""; // 清空回答历史
80 |
81 | // 创建一个Promise
82 | let messagePromise = new Promise((resolve, reject) => {
83 | socket.addEventListener("open", () => {
84 | const params = {
85 | header: {
86 | app_id: appID,
87 | uid: "fd3f47e4-d",
88 | },
89 | parameter: {
90 | chat: {
91 | domain: modelDomain,
92 | temperature: 0.8,
93 | max_tokens: 1024,
94 | },
95 | },
96 | payload: {
97 | message: {
98 | text: [
99 | { role: "system", content: prompt },
100 | { role: "user", content: inputVal }, // 最新的问题
101 | ],
102 | },
103 | },
104 | };
105 | socket.send(JSON.stringify(params));
106 | });
107 |
108 | socket.addEventListener("message", (event) => {
109 | const data = JSON.parse(String(event.data));
110 | if (data.header.code !== 0) {
111 | console.error("Socket 出错:", data.header.code, data.header.message);
112 | socket.close();
113 | reject("");
114 | } else if (data.payload.choices.text && data.header.status === 2) {
115 | total_res += data.payload.choices.text[0].content;
116 | setTimeout(() => {
117 | socket.close();
118 | }, 1000);
119 | }
120 | });
121 |
122 | socket.addEventListener("close", () => {
123 | resolve(total_res);
124 | });
125 |
126 | socket.addEventListener("error", (event) => {
127 | console.error("Socket 连接错误:", event);
128 | reject("");
129 | });
130 | });
131 |
132 | return await messagePromise;
133 | }
134 |
--------------------------------------------------------------------------------