├── .env.example
├── README.md
├── api
└── index.js
├── app.js
├── chat
├── chat.js
├── session.js
├── template.js
└── text.js
├── comm
└── debug.js
├── ding
└── accesstoken.js
├── handler
└── conversation.js
├── package.json
├── public
└── .gitkeep
├── render.yaml
├── route
└── conversation.js
├── service
└── openai.js
└── vercel.json
/.env.example:
--------------------------------------------------------------------------------
1 | PORT = 7070
2 | APPKEY = your appkey
3 | APPSECRET = your appsecret
4 | OPENAI_MODEL = gpt-3.5-turbo
5 | OPENAI_API_KEY = sk-
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # 零代码 一键部署ChatGPT到钉钉 无须翻墙
3 |
4 |
5 |
6 |

7 |
8 |
9 |
10 |

11 |
12 |
13 |
14 | ChatGPT 接入钉钉, 赋能商业成功
15 |
16 |
17 | ## 大模型AI客服邀请您体验
18 | 我们基于chatgpt 大模型, 开发了Ai智能客服,Ai智能客服7*24小时服务能力,大大节省客服成本,提高公司服务效率。
19 | 接入场景包括 *微信*,*公众号*,*视频号小店*,*小程序*等
20 | 需要体验的企业欢迎聊系我,名额有限。
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | https://www.youtube.com/watch?v=Wd6zc7WmeUI
29 |
30 |
31 |
32 | ## 关于本项目
33 | 本项目可以实现一键部署ChatGPT到钉钉中,使ChatGPT与钉钉完美融合,手机或电脑上,打开钉钉,就可以使用强大的ChatGPT智能问答。截止目前,本项目可以提供两个能力:
34 |
35 | 1. 功能集成,将ChatGPT问答功能集成到钉钉,借助钉钉机器人功能,我们可以与ChatGPT一对一问答,或者在群里让ChatGPT参与问答,安装请参照下面的**部署方法**
36 | 2. 更强大的功能扩展,本项目为开源项目,有开发能力的小伙伴可以Fork到自己的仓库,根据自己企业业务需要,比如结合钉钉开放的API,二次开发一些其他功能。
37 |
38 |
39 |
40 |
41 | 1. 创建钉钉应用
42 |
43 | ## 部署方式一 Vercel方式(推荐)
44 | 1. 创建钉钉应用
45 |
46 |
47 | 第一步,创建应用。
48 | 1、登录[钉钉开发者后台](https://open-dev.dingtalk.com/#/),选择应用开发 > 企业内部开发 > 创建应用,单击创建应用;创建应用后,进入机器人与消息推送页面,进入机器人配置页面。
49 |
50 | 
51 |
52 |
53 |

54 |
55 |
56 | 2、单击应用功能 > 机器人与消息推送。
57 | 
58 | 点亮此按扭
59 |
60 | 3、打开机器人配置开关后,填写机器人相关配置信息,除了**消息接收地址**,信息完善后,请点<发布>,成功会看到“编辑成功”提示。
61 |
62 |
63 |
64 |
65 |
66 | 4、配置机器人权限,单击权限管理 > 机器人,将相关权限开通,操作如下图,
67 |
68 |
69 |
70 |
71 |
72 | 第二步,部署前的准备工作
73 |
74 | 1、**open-api-key**
75 |
76 | 这个需要在ChatGPT账号里生成
77 |
78 |
79 | [申请网址API KEY](https://platform.openai.com/account/api-keys)
80 |
81 | 2、**AppKey AppSecret**
82 |
83 |
84 | 3、**要有一个自己的域名**
85 | 自己已经注册好的一个域名。
86 |
87 | 第三步,一键部署安装服务
88 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fsytpb%2Fchatgpt-dingtalk-robot%2Ftree%2Fmain&env=PORT,APPKEY,APPSECRET,OPENAI_MODEL,OPENAI_API_KEY&project-name=chatgpt-dingtalk-robot&repository-name=chatgpt-dingtalk-robot)
89 | 请点右键 > 新标签页打开
90 |
91 |
92 |
93 |
94 |
95 | 请将*Create private Git Repository* 勾点掉,然后点击 Create
96 |
97 |
98 |
99 |
100 |
101 |
102 | 这一步要填入相关参数,注意,前后不要加入多余的空格, OPENAI_MODEL, 可以填入gpt-3.5-turbo或者gpt-4, ** 注意账号不支持gpt4,要填入 gpt-3.5-turbo,否则无法使用 **。 然后点击 Deploy。
103 | # 参数选项请参考下面参数表格说明
104 |
105 |
106 |
107 |
108 |
109 |
110 | 部署成功,如图所示。
111 |
112 |
113 |
114 |
115 |
116 |
117 | 绑定自己的域名,填入域名,点 Add。
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | 保持默认,点 Add
126 |
127 |
128 |
129 |
130 |
131 |
132 | 复制 IP地址
133 |
134 |
135 |
136 |
137 |
138 |
139 | 到自己购买域名的控制台,我这里是腾讯云控制台,给域名增加解析记录,如图所示,一条A记录,一条CNAME记录。
140 |
141 |
142 |
143 |
144 |
145 |
146 | 配置成功,Vercel 页面会自动出现所示标志。
147 |
148 |
149 | **把域名加上/message**, 比如域名是abc.com URL: https://www.abc.com/message, 粘贴到上面**消息接收地址**页面里,点击<调试>,然后再次点击<发布>即可。到此部署完成!
150 |
151 | ## 部署方式二 Render方式
152 |
153 | 点击查看详细
154 |
155 | [指导视频](https://youtu.be/JgBNsWQcSqw)
156 |
157 | 1. 创建钉钉应用
158 |
159 |
160 | 第一步,创建应用。
161 | 1、登录[钉钉开发者后台](https://open-dev.dingtalk.com/#/),选择应用开发 > 企业内部开发 > 创建应用,单击创建应用;创建应用后,进入机器人与消息推送页面,进入机器人配置页面。
162 |
163 | 
164 |
165 |
166 |

167 |
168 |
169 | 2、单击应用功能 > 机器人与消息推送。
170 | 
171 | 点亮此按扭
172 |
173 | 3、打开机器人配置开关后,填写机器人相关配置信息,除了**消息接收地址**,信息完善后,请点<发布>,成功会看到“编辑成功”提示。
174 |
175 |
176 |
177 |
178 |
179 | 4、配置机器人权限,单击权限管理 > 机器人,将相关权限开通,操作如下图,
180 |
181 |
182 |
183 |
184 |
185 |
186 | 第二步,部署前的准备工作
187 |
188 | 1、**open-api-key**
189 |
190 | 这个需要在ChatGPT账号里生成
191 |
192 |
193 | [申请网址API KEY](https://platform.openai.com/account/api-keys)
194 |
195 | 2、**AppKey AppSecret**
196 |
197 |
198 |
199 |
200 |
201 | 第三步,一键部署安装服务
202 |
203 |
204 |
205 | 请点右键 > 新标签页打开
206 |
207 |
208 |
209 |
210 | 如图所示,将上面的字段信息填入,端口填入4位数,比如7070,然后点击Apply。 注意现更新增加了OPENAI_MODEL 值可以是gpt-3.5-turbo 或者gpt-4(如果你的key支持可填)
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 | 需要等1-5分钟部署,完成后复制生成的服务的URL,如下图,**然后拷贝URL后面拼接上/message**, 比如URL是 https://abc.com 拼接成 https://abc.com/message, 粘贴到上面**消息接收地址**页面里,点击<调试>,然后再次点击<发布>即可。
221 |
222 |
223 |
224 |
225 | ## 部署方式三 Docker方式
226 |
227 | 服务器docker部署
228 |
229 | 1. 前提条件:
230 | - 一台服务器
231 | - 一个域名
232 |
233 | 2. 复制变量文件 `.env.example`,填写自己的配置
234 |
235 | 3. 运行docker
236 | 假设新变量文件名为 `.env.local`
237 |
238 | ```bash
239 | # docker4bill/ww-openai-node:alpine 为构建好的镜像,你也可以利用本仓库的 Dockerfile 构建自己的镜像
240 | docker run --env-file .env.local -p 6060:6060 -d docker4bill/ww-openai-node:alpine
241 | ```
242 |
243 | 4. 用 `caddy` 或者 `nginx` 给以上服务做个反代即可
244 |
245 |
246 |
247 | ## 参数请参照下表完成,注意值前后不要有空格
248 |
249 | | Key | value | 说明 |
250 | | --------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
251 | | APPKEY | | |
252 | | APPSECRET | | |
253 | | OPENAI_API_KEY | | |
254 | | OPENAI_MODEL | gpt-3.5-turbo | gpt-3.5-turbo 或者gpt-4 注意:不支持gpt4填入gpt-4无效 |
255 | | PORT | 7070 | 可以改成其他 |
256 | |CHAT_HISTORY | no | yes 或者 no yes支持上下文会话,no 不支持上下文,区别上下文对话token 成本高 |
257 |
258 | ## 功能支持
259 | 部署完成,:100: 下面就可以直接使用了,支持两种聊天模式,一是一对一单聊,另一个是群里添加此机器人,@他的名字,发消息让ChatGPT 回答,如文档开头的两个图片,第一张是一对一单聊,第二张是群里与ChatGPT聊天,更多使用场景请加群讨论。(有问题请提issue)
260 |
261 |
262 | ## 新功能调查
263 |
264 | 您的工作场景,最想要Chatgpt为您做什么?除了现有的问答模式。假如需要以下功能,
265 |
266 | 1、语音对话,什么场景用?
267 |
268 | 2、图片生成,什么场景用?
269 |
270 | 3、其他,请列举
271 |
272 | 欢迎来群里讨论!
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | import app from '../app.js';
2 |
3 | export default app;
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { config } from "dotenv";
3 | import debug from "./comm/debug.js";
4 | import { initAccessToken } from "./ding/accesstoken.js";
5 |
6 | import conversation from "./route/conversation.js";
7 |
8 |
9 | config();
10 |
11 | const app = express();
12 | const PORT = process.env.PORT;
13 |
14 | /*message.log();*/
15 | app.use(express.json());
16 | app.use(express.urlencoded({ extended: false }));
17 |
18 | /*health check for render*/
19 | app.get('/healthz', function (req, res, next) {
20 | res.status(200).end();
21 | });
22 |
23 | app.use('/message', conversation);
24 |
25 |
26 | /*init access_token*/
27 | initAccessToken();
28 |
29 | app.listen(PORT, () => {
30 | debug.out(`Server Running on Port:${PORT}`);
31 | });
32 |
33 | export default app;
--------------------------------------------------------------------------------
/chat/chat.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | const types = {text: "TEXT", image: "IMAGE", voice: "AUDIO"};
4 |
5 | export default class Chat {
6 | #type = null;
7 | constructor(name) {
8 | this.#type = types[name];
9 | }
10 |
11 | type() {
12 | return this.#type;
13 | }
14 |
15 | process(xml, res) {
16 |
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/chat/session.js:
--------------------------------------------------------------------------------
1 |
2 | export default class Session {
3 |
4 | static session = null;
5 | static _cache = null;
6 |
7 | static {
8 | this.session = new Map();
9 | this._cache = new this();
10 | }
11 | /*singleton*/
12 | constructor() {
13 | return this.constructor._cache;
14 | }
15 |
16 | invoke() {};
17 |
18 | static push(id, item) {
19 |
20 | const list = this.session.get(id);
21 | if(!list) {
22 | this.session.set(id, [item]);
23 | } else {
24 | list.push(item);
25 | }
26 | }
27 |
28 | static shift(id) {
29 |
30 | const list = this.session.get(id);
31 | if(list.length === 4) {
32 | list.shift();
33 | }
34 | }
35 |
36 | static update(id, item) {
37 |
38 | this.push(id, item);
39 | this.shift(id);
40 | return this.session.get(id);
41 | }
42 |
43 | static get(id) {
44 |
45 | return this.session.get(id);
46 | }
47 | }
--------------------------------------------------------------------------------
/chat/template.js:
--------------------------------------------------------------------------------
1 | export const MDUserMsg = (title, content) => {
2 |
3 | const data = {
4 | "msgtype": "markdown",
5 | "markdown": {
6 | "title": title,
7 | "text": content
8 | }
9 | };
10 | return data;
11 | }
12 |
13 |
14 | export const MDGroupMsg = (title, senderId, content) => {
15 |
16 | const text = `@${senderId} \n\n ${content}`;
17 |
18 | const data = {
19 | "msgtype": "markdown",
20 | "markdown": {
21 | "title": title,
22 | "text": text
23 | },
24 | "at": {
25 | "atDingtalkIds": [
26 | senderId
27 | ],
28 | }
29 | };
30 | return data;
31 | }
--------------------------------------------------------------------------------
/chat/text.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 | import axios from "axios";
3 | import Chat from "./chat.js";
4 | import Session from "./session.js";
5 | import debug from "../comm/debug.js";
6 | import { OpenAI } from "../service/openai.js";
7 | import { MDUserMsg, MDGroupMsg } from "./template.js";
8 | import { getAccessToken } from "../ding/accesstoken.js";
9 |
10 | export default class TextChat extends Chat {
11 |
12 | constructor(name) {
13 | super(name);
14 | this.host = 'https://api.dingtalk.com';
15 | }
16 |
17 | async toUser(staffID, robotCode, answer, res) {
18 | /*response to dingtalk*/
19 | const token = await getAccessToken();
20 | debug.out(answer);
21 |
22 | const data = {
23 | "robotCode": robotCode,
24 | "userIds": [staffID],
25 | "msgKey": "sampleText",
26 | "msgParam": JSON.stringify({ "content": answer })
27 | };
28 | const url = this.host + '/v1.0/robot/oToMessages/batchSend';
29 |
30 | const config = {
31 | headers: {
32 | 'Accept': "application/json",
33 | 'Content-Type': "application/json",
34 | 'x-acs-dingtalk-access-token': token
35 | }
36 | };
37 |
38 | await axios.post(url, data, config);
39 | res.send("OK");
40 | }
41 |
42 | async toGroup(conversationID, robotCode, answer) {
43 | /*response to dingtalk*/
44 | const token = await getAccessToken();
45 | debug.out(answer);
46 |
47 | const data = {
48 | "robotCode": robotCode,
49 | "openConversationId": conversationID,
50 | "msgKey": "sampleText",
51 | "msgParam": JSON.stringify({ "content": answer })
52 | };
53 |
54 | const url = this.host + '/v1.0/robot/groupMessages/send';
55 |
56 | const config = {
57 | headers: {
58 | 'Accept': "application/json",
59 | 'Content-Type': "application/json",
60 | 'x-acs-dingtalk-access-token': token
61 | }
62 | };
63 |
64 | return axios.post(url, data, config);
65 | }
66 |
67 | async reply(info, answer, res) {
68 | const senderId = info.senderId;
69 | const webHook = info.sessionWebhook;
70 |
71 | let markdown = null;
72 | if (info.conversationType === '1')
73 | markdown = MDUserMsg(answer.slice(0,30), answer);
74 | else if (info.conversationType === '2')
75 | markdown = MDGroupMsg(answer.slice(0,30), senderId, answer);
76 |
77 | res.set({
78 | 'Content-Type': 'application/json',
79 | 'url': webHook
80 | });
81 | const result = res.send(JSON.stringify(markdown));
82 | debug.log(result);
83 | }
84 |
85 |
86 | process(info, res) {
87 |
88 | const question = info?.text?.content;
89 | let context = [{"role":"user" ,"content":question}];
90 | //const staffID = info?.senderStaffId;
91 | const robotCode = info?.robotCode;
92 |
93 | const openai = new OpenAI();
94 | if(process.env.CHAT_HISTORY === "yes")
95 | context = Session.update(info.conversationId, {"role":"user" ,"content":question});
96 | debug.out(context);
97 |
98 | openai.ctChat(context).then(result => {
99 | const message = result?.data?.choices[0]?.message;
100 | debug.log(message?.content);
101 | if (!message?.content)
102 | return;
103 |
104 | const answer = message.content;
105 | this.reply(info, answer, res);
106 | });
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/comm/debug.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export default class debug {
4 |
5 | constructor() {
6 | }
7 |
8 | static stack() {
9 |
10 | const e = new Error();
11 | const regex = /\/([^\/]+\.*):(\d+):(\d+)/;
12 | const match = regex.exec(e.stack.split("\n")[3]);
13 |
14 | return {
15 | name: match[1],
16 | line: match[2],
17 | column: match[3]
18 | };
19 | }
20 |
21 | static log(...info) {
22 |
23 | const s = this.stack();
24 | const name = s.name;
25 | const line = s.line;
26 | console.log(`<${name}:${line}>`, ...info);
27 | }
28 |
29 | static out(...info) {
30 | console.log(...info);
31 | }
32 | }
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/ding/accesstoken.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | let accessToken = { token: 'NO_TICKET', expire: 0 };
4 |
5 |
6 | function newAccessToken() {
7 |
8 | const appKey = process.env.APPKEY;
9 | const appSecret = process.env.APPSECRET;
10 | const url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken';
11 |
12 | const body = {
13 | "appKey": appKey,
14 | "appSecret": appSecret
15 | };
16 |
17 | const config = {
18 | headers:{
19 | 'Accept': "application/json",
20 | 'Content-Type': "application/json"
21 | }
22 | };
23 |
24 | return axios.post(url, body, config);
25 | }
26 |
27 | function setAccessToken(token) {
28 |
29 | const expire = new Date().getTime() + 2 * 60 * 60 * 1000;
30 | accessToken = { ...accessToken, token, expire }
31 | }
32 |
33 | async function getAccessToken() {
34 |
35 | const current = new Date().getTime();
36 | if (accessToken.expire > current) /*timeout*/
37 | return accessToken.token;
38 | else {
39 | console.log("access token expired , refresh...")
40 | const token = await newAccessToken();
41 | setAccessToken(token);
42 | return token;
43 | }
44 | }
45 |
46 | async function initAccessToken() {
47 | const result = await newAccessToken();
48 | const token = result?.data?.accessToken;
49 | console.log("accesstoken === ", token);
50 | setAccessToken(token);
51 | }
52 |
53 | export {
54 | initAccessToken,
55 | getAccessToken
56 | };
57 |
--------------------------------------------------------------------------------
/handler/conversation.js:
--------------------------------------------------------------------------------
1 | import debug from "../comm/debug.js";
2 | import TextChat from "../chat/text.js";
3 |
4 | export default class Conversation {
5 |
6 | constructor() {
7 | }
8 |
9 | urlconfig(req, res) {
10 | res.send("OK");
11 | }
12 |
13 | process(body, res) {
14 |
15 | let chat = null;
16 | const info = body;
17 |
18 | const msgtype = info.msgtype;
19 |
20 | if(msgtype === "text") {
21 | chat = new TextChat(msgtype);
22 | }
23 |
24 | if(!!chat) {
25 | chat.process(info, res);
26 | /*res.send("OK");*/
27 | return;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "messagerobot",
3 | "version": "1.0.0",
4 | "description": "",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "node app.js",
8 | "vercel-build": "echo hello"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "axios": "^1.4.0",
15 | "dotenv": "^16.0.3",
16 | "express": "^4.18.2",
17 | "openai": "^3.2.1"
18 | },
19 | "engines": {
20 | "node": ">=17.3.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sytpb/chatgpt-dingtalk-robot/28a8aa0d8761a9f318535d602dcb86e18fdb162e/public/.gitkeep
--------------------------------------------------------------------------------
/render.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | # node web service
3 | - type: web
4 | name: chatgpt-dingtalk-robot
5 | env: node
6 | rootDir: .
7 | autoDeploy: false
8 | repo: https://github.com/sytpb/chatgpt-dingtalk-robot.git # optional
9 | region: singapore # optional (defaults to oregon)
10 | plan: free # optional (defaults to starter instance type)
11 | branch: beta # optional (defaults to master)
12 | healthCheckPath: /healthz
13 | buildCommand: npm install
14 | startCommand: node app.js
15 | envVars:
16 | - key: PORT
17 | sync: false
18 | - key: APPKEY
19 | sync: false
20 | - key: APPSECRET
21 | sync: false
22 | - key: OPENAI_MODEL
23 | sync: false
24 | - key: OPENAI_API_KEY
25 | sync: false
26 | - key: CHAT_HISTORY
27 | sync: false
--------------------------------------------------------------------------------
/route/conversation.js:
--------------------------------------------------------------------------------
1 |
2 | import debug from "../comm/debug.js";
3 | import Conversation from "../handler/conversation.js";
4 |
5 | import express from "express";
6 | const router = express.Router();
7 |
8 | router.use('/', function(req, res, next) {
9 | let method = req.method;
10 | if(method == 'GET') {
11 | debug.out(`--------------ROUTER MSG [URL SETTING]--------------`);
12 | const conversation = new Conversation();
13 | conversation.urlconfig(req, res);
14 | }
15 | else if(method == 'POST') {
16 | debug.out(`--------------ROUTER MSG [CONVERSATION]--------------`);
17 | const conversation = new Conversation();
18 | conversation.process(req.body, res);
19 | }
20 | });
21 |
22 | export default router;
23 |
24 |
--------------------------------------------------------------------------------
/service/openai.js:
--------------------------------------------------------------------------------
1 |
2 | import { Configuration, OpenAIApi } from "openai";
3 |
4 | const models = ['text-davinci-003','code-davinci-002','gpt-3.5-turbo','gpt-4'];
5 |
6 |
7 | export class OpenAI {
8 | #configuration = null;
9 | #openai = null;
10 |
11 | constructor() {
12 | this.#configuration = new Configuration( {apiKey: process.env.OPENAI_API_KEY} );
13 | this.#openai = new OpenAIApi(this.#configuration);
14 | }
15 |
16 | static create() {
17 | }
18 |
19 | async ctChat(context) {
20 |
21 | try {
22 | const res = await this.#openai.createChatCompletion({
23 | model: process.env.OPENAI_MODEL,
24 | messages: context
25 | });
26 |
27 | return res;
28 | }
29 | catch(error) {
30 | console.log("OpenAI happen error!");
31 | console.log(error?.response?.data?.error);
32 | }
33 | }
34 |
35 | async ctText(question) {
36 |
37 | try {
38 | const res = await this.#openai.createChatCompletion({
39 | model: process.env.OPENAI_MODEL,
40 | messages:[{role:"user",content: question}]
41 | });
42 |
43 | return res;
44 | }
45 | catch(error) {
46 | console.log("OpenAI happen error!");
47 | console.log(error);
48 | }
49 | }
50 |
51 | static ctImage() {
52 |
53 | }
54 |
55 | static ctVoice() {
56 |
57 | }
58 | }
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 |
2 |
3 | {
4 | "rewrites": [{ "source": "/(.*)", "destination": "/api" }]
5 | }
--------------------------------------------------------------------------------