├── public
└── .gitkeep
├── api
└── index.js
├── vercel.json
├── .env.example
├── chat
├── chat.js
├── template.js
├── session.js
└── text.js
├── package.json
├── handler
└── conversation.js
├── route
└── conversation.js
├── comm
└── debug.js
├── app.js
├── render.yaml
├── service
└── openai.js
├── ding
└── accesstoken.js
└── README.md
/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | import app from '../app.js';
2 |
3 | export default app;
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 |
2 |
3 | {
4 | "rewrites": [{ "source": "/(.*)", "destination": "/api" }]
5 | }
--------------------------------------------------------------------------------
/.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-
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # 零代码 一键部署ChatGPT到钉钉 无须翻墙
3 |
4 |
5 |
7 |
11 |
54 |
63 |
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 | 请点右键 > 新标签页打开
167 |
176 |
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 |
208 |
209 |
210 | 如图所示,将上面的字段信息填入,端口填入4位数,比如7070,然后点击Apply。 注意现更新增加了OPENAI_MODEL 值可以是gpt-3.5-turbo 或者gpt-4(如果你的key支持可填)
214 |
215 |
217 |