├── README.md
├── gaokao-free-api
├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── configs
│ └── dev
│ │ ├── service.yml
│ │ └── system.yml
├── libs.d.ts
├── openapi.json
├── package.json
├── public
│ └── welcome.html
├── schools.json
├── src
│ ├── api
│ │ ├── consts
│ │ │ └── exceptions.ts
│ │ ├── controllers
│ │ │ └── univ-selection.ts
│ │ └── routes
│ │ │ ├── index.ts
│ │ │ └── univ-selection.ts
│ ├── daemon.ts
│ ├── index.ts
│ └── lib
│ │ ├── config.ts
│ │ ├── configs
│ │ ├── service-config.ts
│ │ └── system-config.ts
│ │ ├── consts
│ │ └── exceptions.ts
│ │ ├── environment.ts
│ │ ├── exceptions
│ │ ├── APIException.ts
│ │ └── Exception.ts
│ │ ├── http-status-codes.ts
│ │ ├── initialize.ts
│ │ ├── interfaces
│ │ └── ICompletionMessage.ts
│ │ ├── logger.ts
│ │ ├── request
│ │ └── Request.ts
│ │ ├── response
│ │ ├── Body.ts
│ │ ├── FailureBody.ts
│ │ ├── Response.ts
│ │ └── SuccessfulBody.ts
│ │ ├── server.ts
│ │ └── util.ts
├── tsconfig.json
└── yarn.lock
├── meme-api
├── .DS_Store
├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── configs
│ └── dev
│ │ ├── service.yml
│ │ └── system.yml
├── libs.d.ts
├── openapi.json
├── package.json
├── public
│ ├── template.html
│ └── welcome.html
├── src
│ ├── api
│ │ ├── consts
│ │ │ └── exceptions.ts
│ │ ├── controllers
│ │ │ ├── chat.ts
│ │ │ └── meme.ts
│ │ └── routes
│ │ │ ├── index.ts
│ │ │ └── meme.ts
│ ├── daemon.ts
│ ├── index.ts
│ └── lib
│ │ ├── config.ts
│ │ ├── configs
│ │ ├── service-config.ts
│ │ └── system-config.ts
│ │ ├── consts
│ │ └── exceptions.ts
│ │ ├── environment.ts
│ │ ├── exceptions
│ │ ├── APIException.ts
│ │ └── Exception.ts
│ │ ├── http-status-codes.ts
│ │ ├── initialize.ts
│ │ ├── install-browser.ts
│ │ ├── interfaces
│ │ └── ICompletionMessage.ts
│ │ ├── logger.ts
│ │ ├── request
│ │ └── Request.ts
│ │ ├── response
│ │ ├── Body.ts
│ │ ├── FailureBody.ts
│ │ ├── Response.ts
│ │ └── SuccessfulBody.ts
│ │ ├── server.ts
│ │ └── util.ts
├── tsconfig.json
└── yarn.lock
├── olympics-api
├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── cache.json
├── configs
│ └── dev
│ │ ├── service.yml
│ │ └── system.yml
├── package.json
├── public
│ └── welcome.html
├── src
│ ├── api
│ │ ├── consts
│ │ │ └── exceptions.ts
│ │ ├── controllers
│ │ │ ├── interfaces
│ │ │ │ ├── IAthlete.ts
│ │ │ │ ├── IGame.ts
│ │ │ │ ├── IMedals.ts
│ │ │ │ ├── ISchedules.ts
│ │ │ │ └── ISport.ts
│ │ │ └── olympics.ts
│ │ └── routes
│ │ │ ├── index.ts
│ │ │ └── olympics.ts
│ ├── index.ts
│ └── lib
│ │ ├── cache.ts
│ │ ├── config.ts
│ │ ├── configs
│ │ ├── service-config.ts
│ │ └── system-config.ts
│ │ ├── consts
│ │ └── exceptions.ts
│ │ ├── environment.ts
│ │ ├── exceptions
│ │ ├── APIException.ts
│ │ └── Exception.ts
│ │ ├── http-status-codes.ts
│ │ ├── initialize.ts
│ │ ├── interfaces
│ │ └── ICompletionMessage.ts
│ │ ├── logger.ts
│ │ ├── request
│ │ └── Request.ts
│ │ ├── response
│ │ ├── Body.ts
│ │ ├── FailureBody.ts
│ │ ├── Response.ts
│ │ └── SuccessfulBody.ts
│ │ ├── server.ts
│ │ └── util.ts
├── tsconfig.json
└── yarn.lock
├── openai-compatible-api
├── .dockerignore
├── .github
│ └── workflows
│ │ ├── docker-image.yml
│ │ └── sync.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── configs
│ └── dev
│ │ ├── service.yml
│ │ └── system.yml
├── libs.d.ts
├── package.json
├── public
│ └── welcome.html
├── src
│ ├── api
│ │ ├── consts
│ │ │ └── exceptions.ts
│ │ ├── controllers
│ │ │ └── chat.ts
│ │ └── routes
│ │ │ ├── chat.ts
│ │ │ ├── images.ts
│ │ │ ├── index.ts
│ │ │ ├── models.ts
│ │ │ └── ping.ts
│ ├── index.ts
│ └── lib
│ │ ├── config.ts
│ │ ├── configs
│ │ ├── service-config.ts
│ │ └── system-config.ts
│ │ ├── consts
│ │ └── exceptions.ts
│ │ ├── environment.ts
│ │ ├── exceptions
│ │ ├── APIException.ts
│ │ └── Exception.ts
│ │ ├── http-status-codes.ts
│ │ ├── initialize.ts
│ │ ├── interfaces
│ │ └── ICompletionMessage.ts
│ │ ├── logger.ts
│ │ ├── request
│ │ └── Request.ts
│ │ ├── response
│ │ ├── Body.ts
│ │ ├── FailureBody.ts
│ │ ├── Response.ts
│ │ └── SuccessfulBody.ts
│ │ ├── server.ts
│ │ └── util.ts
├── tsconfig.json
└── vercel.json
├── werewolf-api
├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── configs
│ └── dev
│ │ ├── service.yml
│ │ └── system.yml
├── libs.d.ts
├── openapi.json
├── package.json
├── public
│ └── welcome.html
├── src
│ ├── api
│ │ ├── consts
│ │ │ ├── exceptions.ts
│ │ │ ├── nickname.ts
│ │ │ └── role-rules.ts
│ │ ├── controllers
│ │ │ ├── chat.ts
│ │ │ └── game.ts
│ │ ├── interfaces
│ │ │ ├── IPlayer.ts
│ │ │ └── IRoom.ts
│ │ └── routes
│ │ │ ├── game.ts
│ │ │ └── index.ts
│ ├── index.ts
│ └── lib
│ │ ├── config.ts
│ │ ├── configs
│ │ ├── service-config.ts
│ │ └── system-config.ts
│ │ ├── consts
│ │ └── exceptions.ts
│ │ ├── environment.ts
│ │ ├── exceptions
│ │ ├── APIException.ts
│ │ └── Exception.ts
│ │ ├── http-status-codes.ts
│ │ ├── initialize.ts
│ │ ├── interfaces
│ │ └── ICompletionMessage.ts
│ │ ├── logger.ts
│ │ ├── request
│ │ └── Request.ts
│ │ ├── response
│ │ ├── Body.ts
│ │ ├── FailureBody.ts
│ │ ├── Response.ts
│ │ └── SuccessfulBody.ts
│ │ ├── server.ts
│ │ └── util.ts
├── tsconfig.json
├── vercel.json
└── yarn.lock
└── wxkf-api
├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── agents.yml.template
├── build-docker.sh
├── configs
└── dev
│ ├── service.yml
│ └── system.yml
├── doc
├── example-1.png
├── example-10.png
├── example-11.png
├── example-2.png
├── example-3.png
├── example-4.png
├── example-5.png
├── example-6.png
├── example-7.png
├── example-8.png
└── example-9.png
├── libs.d.ts
├── package.json
├── public
└── welcome.html
├── secret.yml.template
├── shutdown-docker.sh
├── src
├── api
│ ├── consts
│ │ └── exceptions.ts
│ ├── controllers
│ │ ├── interfaces
│ │ │ ├── ICompletionMessage.ts
│ │ │ ├── IMedia.ts
│ │ │ └── IMessage.ts
│ │ └── message.ts
│ └── routes
│ │ ├── index.ts
│ │ └── message.ts
├── index.ts
└── lib
│ ├── agents.ts
│ ├── config.ts
│ ├── configs
│ ├── service-config.ts
│ └── system-config.ts
│ ├── consts
│ └── exceptions.ts
│ ├── environment.ts
│ ├── exceptions
│ ├── APIException.ts
│ └── Exception.ts
│ ├── http-status-codes.ts
│ ├── initialize.ts
│ ├── interfaces
│ ├── IAgentConfig.ts
│ └── IMessage.ts
│ ├── logger.ts
│ ├── qingyan-glms-api.ts
│ ├── qingyan-glms-free-api.ts
│ ├── request
│ └── Request.ts
│ ├── response
│ ├── Body.ts
│ ├── FailureBody.ts
│ ├── Response.ts
│ └── SuccessfulBody.ts
│ ├── secret.ts
│ ├── server.ts
│ ├── util.ts
│ └── wxkf-api.ts
├── startup-docker.sh
├── tsconfig.json
└── yarn.lock
/README.md:
--------------------------------------------------------------------------------
1 | # qingyan-cookbook
2 |
3 | 欢迎来到 智谱清言智能体 仓库📘。你可以在这里发现智能体的使用示例和API实现。
4 |
5 | ## 外部接入智能体
6 |
7 | * [微信客服接入智谱清言智能体API](./wxkf-api/)
8 | * [智能体API转OpenAI兼容接口](./openai-compatible-api/)
9 |
10 |
11 | ## 超级智能体
12 |
13 | * [高考志愿填报助手 API](./gaokao-free-api/)
14 | * [超级MEME API](./meme-api/)
15 | * [多智能体-狼人杀](./werewolf-api/)
16 |
--------------------------------------------------------------------------------
/gaokao-free-api/.dockerignore:
--------------------------------------------------------------------------------
1 | logs
2 | dist
3 | doc
4 | node_modules
5 | .vscode
6 | .git
7 | .gitignore
8 | README.md
9 | *.tar.gz
--------------------------------------------------------------------------------
/gaokao-free-api/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | logs/
4 | .vercel
5 | forward-port-proxy.sh
6 | *.pem
--------------------------------------------------------------------------------
/gaokao-free-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts AS BUILD_IMAGE
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8 |
9 | FROM node:lts-alpine
10 |
11 | COPY --from=BUILD_IMAGE /app/configs /app/configs
12 | COPY --from=BUILD_IMAGE /app/package.json /app/package.json
13 | COPY --from=BUILD_IMAGE /app/dist /app/dist
14 | COPY --from=BUILD_IMAGE /app/public /app/public
15 | COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16 |
17 | WORKDIR /app
18 |
19 | EXPOSE 9000
20 |
21 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/gaokao-free-api/README.md:
--------------------------------------------------------------------------------
1 | # 高考志愿填报助手 API服务
2 |
3 | 此仓库是用于[2024高考志愿填报助手](https://chatglm.cn/main/gdetail/66657769efbf798a741c9ee8)智能体的API服务,通过模拟请求[掌上高考](https://gaokao.cn/)网页接口获取实时数据。
4 |
5 | ## 安装
6 |
7 | 请先安装Node.js 20+。
8 |
9 | ```shell
10 | # 全局安装Yarn
11 | npm i -g yarn --registry https://registry.npmmirror.com/
12 | # 安装依赖
13 | yarn install
14 | # 构建dist
15 | npm run build
16 | ```
17 |
18 | # 启动
19 |
20 | ```shell
21 | # 启动服务
22 | npm run start
23 | ```
24 |
25 | # 后台运行
26 |
27 | 请先安装PM2。
28 |
29 | ```shell
30 | yarn global add pm2
31 | ```
32 |
33 | 使用PM2启动服务并守护进程
34 |
35 | ```shell
36 | pm2 start dist/index.js --name "gaokao-api"
37 | ```
--------------------------------------------------------------------------------
/gaokao-free-api/configs/dev/service.yml:
--------------------------------------------------------------------------------
1 | # 服务名称
2 | name: gaokao-free-api
3 | # 服务绑定主机地址
4 | host: '0.0.0.0'
5 | # 服务绑定端口
6 | port: 9000
--------------------------------------------------------------------------------
/gaokao-free-api/configs/dev/system.yml:
--------------------------------------------------------------------------------
1 | # 是否开启请求日志
2 | requestLog: true
3 | # 临时目录路径
4 | tmpDir: ./tmp
5 | # 日志目录路径
6 | logDir: ./logs
7 | # 日志写入间隔(毫秒)
8 | logWriteInterval: 200
9 | # 日志文件有效期(毫秒)
10 | logFileExpires: 2626560000
11 | # 公共目录路径
12 | publicDir: ./public
13 | # 临时文件有效期(毫秒)
14 | tmpFileExpires: 86400000
--------------------------------------------------------------------------------
/gaokao-free-api/libs.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/gaokao-free-api/libs.d.ts
--------------------------------------------------------------------------------
/gaokao-free-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gaokao-free-api",
3 | "version": "0.0.1",
4 | "description": "Gaokao Free API Server",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "types": "dist/index.d.ts",
9 | "directories": {
10 | "dist": "dist"
11 | },
12 | "files": [
13 | "dist/"
14 | ],
15 | "scripts": {
16 | "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node dist/index.js\"",
17 | "start": "node dist/index.js",
18 | "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
19 | },
20 | "author": "Vinlic",
21 | "license": "ISC",
22 | "dependencies": {
23 | "axios": "^1.6.7",
24 | "colors": "^1.4.0",
25 | "crc-32": "^1.2.2",
26 | "cron": "^3.1.6",
27 | "date-fns": "^3.3.1",
28 | "eventsource-parser": "^1.1.2",
29 | "form-data": "^4.0.0",
30 | "fs-extra": "^11.2.0",
31 | "koa": "^2.15.0",
32 | "koa-body": "^5.0.0",
33 | "koa-bodyparser": "^4.4.1",
34 | "koa-range": "^0.3.0",
35 | "koa-router": "^12.0.1",
36 | "koa2-cors": "^2.0.6",
37 | "lodash": "^4.17.21",
38 | "mime": "^4.0.1",
39 | "minimist": "^1.2.8",
40 | "randomstring": "^1.3.0",
41 | "uuid": "^9.0.1",
42 | "yaml": "^2.3.4"
43 | },
44 | "devDependencies": {
45 | "@types/lodash": "^4.14.202",
46 | "@types/mime": "^3.0.4",
47 | "tsup": "^8.0.2",
48 | "typescript": "^5.3.3"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/gaokao-free-api/public/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 🚀 服务已启动
6 |
7 |
8 | gaokao-free-api已启动!
9 |
10 |
--------------------------------------------------------------------------------
/gaokao-free-api/src/api/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | API_TEST: [-9999, 'API异常错误'],
3 | API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
4 | API_REQUEST_FAILED: [-2001, '请求失败'],
5 | API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
6 | API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
7 | API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
8 | API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'],
9 | API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'],
10 | API_IMAGE_GENERATION_FAILED: [-2007, '图像生成失败']
11 | }
--------------------------------------------------------------------------------
/gaokao-free-api/src/api/routes/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | import Response from '@/lib/response/Response.ts';
4 | import univSelection from "./univ-selection.ts";
5 |
6 | export default [
7 | {
8 | get: {
9 | '/': async () => {
10 | const content = await fs.readFile('public/welcome.html');
11 | return new Response(content, {
12 | type: 'html',
13 | headers: {
14 | Expires: '-1'
15 | }
16 | });
17 | }
18 | }
19 | },
20 | univSelection
21 | ];
--------------------------------------------------------------------------------
/gaokao-free-api/src/daemon.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 守护进程
3 | */
4 |
5 | import process from 'process';
6 | import path from 'path';
7 | import { spawn } from 'child_process';
8 |
9 | import fs from 'fs-extra';
10 | import { format as dateFormat } from 'date-fns';
11 | import 'colors';
12 |
13 | const CRASH_RESTART_LIMIT = 600; //进程崩溃重启次数限制
14 | const CRASH_RESTART_DELAY = 5000; //进程崩溃重启延迟
15 | const LOG_PATH = path.resolve("./logs/daemon.log"); //守护进程日志路径
16 | let crashCount = 0; //进程崩溃次数
17 | let currentProcess; //当前运行进程
18 |
19 | /**
20 | * 写入守护进程日志
21 | */
22 | function daemonLog(value, color?: string) {
23 | try {
24 | const head = `[daemon][${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")}] `;
25 | value = head + value;
26 | console.log(color ? value[color] : value);
27 | fs.ensureDirSync(path.dirname(LOG_PATH));
28 | fs.appendFileSync(LOG_PATH, value + "\n");
29 | }
30 | catch(err) {
31 | console.error("daemon log write error:", err);
32 | }
33 | }
34 |
35 | daemonLog(`daemon pid: ${process.pid}`);
36 |
37 | function createProcess() {
38 | const childProcess = spawn("node", ["index.js", ...process.argv.slice(2)]); //启动子进程
39 | childProcess.stdout.pipe(process.stdout, { end: false }); //将子进程输出管道到当前进程输出
40 | childProcess.stderr.pipe(process.stderr, { end: false }); //将子进程错误输出管道到当前进程输出
41 | currentProcess = childProcess; //更新当前进程
42 | daemonLog(`process(${childProcess.pid}) has started`);
43 | childProcess.on("error", err => daemonLog(`process(${childProcess.pid}) error: ${err.stack}`, "red"));
44 | childProcess.on("close", code => {
45 | if(code === 0) //进程正常退出
46 | daemonLog(`process(${childProcess.pid}) has exited`);
47 | else if(code === 2) //进程已被杀死
48 | daemonLog(`process(${childProcess.pid}) has been killed!`, "bgYellow");
49 | else if(code === 3) { //进程主动重启
50 | daemonLog(`process(${childProcess.pid}) has restart`, "yellow");
51 | createProcess(); //重新创建进程
52 | }
53 | else { //进程发生崩溃
54 | if(crashCount++ < CRASH_RESTART_LIMIT) { //进程崩溃次数未达重启次数上限前尝试重启
55 | daemonLog(`process(${childProcess.pid}) has crashed! delay ${CRASH_RESTART_DELAY}ms try restarting...(${crashCount})`, "bgRed");
56 | setTimeout(() => createProcess(), CRASH_RESTART_DELAY); //延迟指定时长后再重启
57 | }
58 | else //进程已崩溃,且无法重启
59 | daemonLog(`process(${childProcess.pid}) has crashed! unable to restart`, "bgRed");
60 | }
61 | }); //子进程关闭监听
62 | }
63 |
64 | process.on("exit", code => {
65 | if(code === 0)
66 | daemonLog("daemon process exited");
67 | else if(code === 2)
68 | daemonLog("daemon process has been killed!");
69 | }); //守护进程退出事件
70 |
71 | process.on("SIGTERM", () => {
72 | daemonLog("received kill signal", "yellow");
73 | currentProcess && currentProcess.kill("SIGINT");
74 | process.exit(2);
75 | }); //kill退出守护进程
76 |
77 | process.on("SIGINT", () => {
78 | currentProcess && currentProcess.kill("SIGINT");
79 | process.exit(0);
80 | }); //主动退出守护进程
81 |
82 | createProcess(); //创建进程
83 |
--------------------------------------------------------------------------------
/gaokao-free-api/src/index.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import environment from "@/lib/environment.ts";
4 | import config from "@/lib/config.ts";
5 | import "@/lib/initialize.ts";
6 | import server from "@/lib/server.ts";
7 | import routes from "@/api/routes/index.ts";
8 | import logger from "@/lib/logger.ts";
9 |
10 | const startupTime = performance.now();
11 |
12 | (async () => {
13 | logger.header();
14 |
15 | logger.info("<<<< gaokao free server >>>>");
16 | logger.info("Version:", environment.package.version);
17 | logger.info("Process id:", process.pid);
18 | logger.info("Environment:", environment.env);
19 | logger.info("Service name:", config.service.name);
20 |
21 | server.attachRoutes(routes);
22 | await server.listen();
23 |
24 | config.service.bindAddress &&
25 | logger.success("Service bind address:", config.service.bindAddress);
26 | })()
27 | .then(() =>
28 | logger.success(
29 | `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
30 | )
31 | )
32 | .catch((err) => console.error(err));
33 |
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | import serviceConfig from "./configs/service-config.ts";
2 | import systemConfig from "./configs/system-config.ts";
3 |
4 | class Config {
5 |
6 | /** 服务配置 */
7 | service = serviceConfig;
8 |
9 | /** 系统配置 */
10 | system = systemConfig;
11 |
12 | }
13 |
14 | export default new Config();
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/configs/service-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 | import util from '../util.ts';
9 |
10 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
11 |
12 | /**
13 | * 服务配置
14 | */
15 | export class ServiceConfig {
16 |
17 | /** 服务名称 */
18 | name: string;
19 | /** @type {string} 服务绑定主机地址 */
20 | host;
21 | /** @type {number} 服务绑定端口 */
22 | port;
23 | /** @type {string} 服务路由前缀 */
24 | urlPrefix;
25 | /** @type {string} 服务绑定地址(外部访问地址) */
26 | bindAddress;
27 |
28 | constructor(options?: any) {
29 | const { name, host, port, urlPrefix, bindAddress } = options || {};
30 | this.name = _.defaultTo(name, 'gaokao-free-api');
31 | this.host = _.defaultTo(host, '0.0.0.0');
32 | this.port = _.defaultTo(port, 5566);
33 | this.urlPrefix = _.defaultTo(urlPrefix, '');
34 | this.bindAddress = bindAddress;
35 | }
36 |
37 | get addressHost() {
38 | if(this.bindAddress) return this.bindAddress;
39 | const ipAddresses = util.getIPAddressesByIPv4();
40 | for(let ipAddress of ipAddresses) {
41 | if(ipAddress === this.host)
42 | return ipAddress;
43 | }
44 | return ipAddresses[0] || "127.0.0.1";
45 | }
46 |
47 | get address() {
48 | return `${this.addressHost}:${this.port}`;
49 | }
50 |
51 | get pageDirUrl() {
52 | return `http://127.0.0.1:${this.port}/page`;
53 | }
54 |
55 | get publicDirUrl() {
56 | return `http://127.0.0.1:${this.port}/public`;
57 | }
58 |
59 | static load() {
60 | const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
61 | if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
62 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
63 | return new ServiceConfig({ ...data, ...external });
64 | }
65 |
66 | }
67 |
68 | export default ServiceConfig.load();
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/configs/system-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 |
9 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
10 |
11 | /**
12 | * 系统配置
13 | */
14 | export class SystemConfig {
15 |
16 | /** 是否开启请求日志 */
17 | requestLog: boolean;
18 | /** 临时目录路径 */
19 | tmpDir: string;
20 | /** 日志目录路径 */
21 | logDir: string;
22 | /** 日志写入间隔(毫秒) */
23 | logWriteInterval: number;
24 | /** 日志文件有效期(毫秒) */
25 | logFileExpires: number;
26 | /** 公共目录路径 */
27 | publicDir: string;
28 | /** 临时文件有效期(毫秒) */
29 | tmpFileExpires: number;
30 | /** 请求体配置 */
31 | requestBody: any;
32 | /** 是否调试模式 */
33 | debug: boolean;
34 |
35 | constructor(options?: any) {
36 | const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
37 | this.requestLog = _.defaultTo(requestLog, false);
38 | this.tmpDir = _.defaultTo(tmpDir, './tmp');
39 | this.logDir = _.defaultTo(logDir, './logs');
40 | this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
41 | this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
42 | this.publicDir = _.defaultTo(publicDir, './public');
43 | this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
44 | this.requestBody = Object.assign(requestBody || {}, {
45 | enableTypes: ['json', 'form', 'text', 'xml'],
46 | encoding: 'utf-8',
47 | formLimit: '100mb',
48 | jsonLimit: '100mb',
49 | textLimit: '100mb',
50 | xmlLimit: '100mb',
51 | formidable: {
52 | maxFileSize: '100mb'
53 | },
54 | multipart: true,
55 | parsedMethods: ['POST', 'PUT', 'PATCH']
56 | });
57 | this.debug = _.defaultTo(debug, true);
58 | }
59 |
60 | get rootDirPath() {
61 | return path.resolve();
62 | }
63 |
64 | get tmpDirPath() {
65 | return path.resolve(this.tmpDir);
66 | }
67 |
68 | get logDirPath() {
69 | return path.resolve(this.logDir);
70 | }
71 |
72 | get publicDirPath() {
73 | return path.resolve(this.publicDir);
74 | }
75 |
76 | static load() {
77 | if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
78 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
79 | return new SystemConfig(data);
80 | }
81 |
82 | }
83 |
84 | export default SystemConfig.load();
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | SYSTEM_ERROR: [-1000, '系统异常'],
3 | SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
4 | SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
5 | } as Record
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/environment.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import minimist from 'minimist';
5 | import _ from 'lodash';
6 |
7 | const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
8 | const envVars = process.env; //获取环境变量
9 |
10 | class Environment {
11 |
12 | /** 命令行参数 */
13 | cmdArgs: any;
14 | /** 环境变量 */
15 | envVars: any;
16 | /** 环境名称 */
17 | env?: string;
18 | /** 服务名称 */
19 | name?: string;
20 | /** 服务地址 */
21 | host?: string;
22 | /** 服务端口 */
23 | port?: number;
24 | /** 包参数 */
25 | package: any;
26 |
27 | constructor(options: any = {}) {
28 | const { cmdArgs, envVars, package: _package } = options;
29 | this.cmdArgs = cmdArgs;
30 | this.envVars = envVars;
31 | this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
32 | this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
33 | this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
34 | this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
35 | this.package = _package;
36 | }
37 |
38 | }
39 |
40 | export default new Environment({
41 | cmdArgs,
42 | envVars,
43 | package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
44 | });
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/exceptions/APIException.ts:
--------------------------------------------------------------------------------
1 | import Exception from './Exception.js';
2 |
3 | export default class APIException extends Exception {
4 |
5 | /**
6 | * 构造异常
7 | *
8 | * @param {[number, string]} exception 异常
9 | */
10 | constructor(exception: (string | number)[], errmsg?: string) {
11 | super(exception, errmsg);
12 | }
13 |
14 | }
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/exceptions/Exception.ts:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 |
3 | import _ from 'lodash';
4 |
5 | export default class Exception extends Error {
6 |
7 | /** 错误码 */
8 | errcode: number;
9 | /** 错误消息 */
10 | errmsg: string;
11 | /** 数据 */
12 | data: any;
13 | /** HTTP状态码 */
14 | httpStatusCode: number;
15 |
16 | /**
17 | * 构造异常
18 | *
19 | * @param exception 异常
20 | * @param _errmsg 异常消息
21 | */
22 | constructor(exception: (string | number)[], _errmsg?: string) {
23 | assert(_.isArray(exception), 'Exception must be Array');
24 | const [errcode, errmsg] = exception as [number, string];
25 | assert(_.isFinite(errcode), 'Exception errcode invalid');
26 | assert(_.isString(errmsg), 'Exception errmsg invalid');
27 | super(_errmsg || errmsg);
28 | this.errcode = errcode;
29 | this.errmsg = _errmsg || errmsg;
30 | }
31 |
32 | compare(exception: (string | number)[]) {
33 | const [errcode] = exception as [number, string];
34 | return this.errcode == errcode;
35 | }
36 |
37 | setHTTPStatusCode(value: number) {
38 | this.httpStatusCode = value;
39 | return this;
40 | }
41 |
42 | setData(value: any) {
43 | this.data = _.defaultTo(value, null);
44 | return this;
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/initialize.ts:
--------------------------------------------------------------------------------
1 | import logger from './logger.js';
2 |
3 | // 允许无限量的监听器
4 | process.setMaxListeners(Infinity);
5 | // 输出未捕获异常
6 | process.on("uncaughtException", (err, origin) => {
7 | logger.error(`An unhandled error occurred: ${origin}`, err);
8 | });
9 | // 输出未处理的Promise.reject
10 | process.on("unhandledRejection", (_, promise) => {
11 | promise.catch(err => logger.error("An unhandled rejection occurred:", err));
12 | });
13 | // 输出系统警告信息
14 | process.on("warning", warning => logger.warn("System warning: ", warning));
15 | // 进程退出监听
16 | process.on("exit", () => {
17 | logger.info("Service exit");
18 | logger.footer();
19 | });
20 | // 进程被kill
21 | process.on("SIGTERM", () => {
22 | logger.warn("received kill signal");
23 | process.exit(2);
24 | });
25 | // Ctrl-C进程退出
26 | process.on("SIGINT", () => {
27 | process.exit(0);
28 | });
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/interfaces/ICompletionMessage.ts:
--------------------------------------------------------------------------------
1 | export default interface ICompletionMessage {
2 | role: 'system' | 'assistant' | 'user' | 'function';
3 | content: string;
4 | }
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/request/Request.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import APIException from '@/lib/exceptions/APIException.ts';
4 | import EX from '@/api/consts/exceptions.ts';
5 | import logger from '@/lib/logger.ts';
6 | import util from '@/lib/util.ts';
7 |
8 | export interface RequestOptions {
9 | time?: number;
10 | }
11 |
12 | export default class Request {
13 |
14 | /** 请求方法 */
15 | method: string;
16 | /** 请求URL */
17 | url: string;
18 | /** 请求路径 */
19 | path: string;
20 | /** 请求载荷类型 */
21 | type: string;
22 | /** 请求headers */
23 | headers: any;
24 | /** 请求原始查询字符串 */
25 | search: string;
26 | /** 请求查询参数 */
27 | query: any;
28 | /** 请求URL参数 */
29 | params: any;
30 | /** 请求载荷 */
31 | body: any;
32 | /** 上传的文件 */
33 | files: any[];
34 | /** 客户端IP地址 */
35 | remoteIP: string | null;
36 | /** 请求接受时间戳(毫秒) */
37 | time: number;
38 |
39 | constructor(ctx, options: RequestOptions = {}) {
40 | const { time } = options;
41 | this.method = ctx.request.method;
42 | this.url = ctx.request.url;
43 | this.path = ctx.request.path;
44 | this.type = ctx.request.type;
45 | this.headers = ctx.request.headers || {};
46 | this.search = ctx.request.search;
47 | this.query = ctx.query || {};
48 | this.params = ctx.params || {};
49 | this.body = ctx.request.body || {};
50 | this.files = ctx.request.files || {};
51 | this.remoteIP = this.headers["X-Real-IP"] || this.headers["x-real-ip"] || this.headers["X-Forwarded-For"] || this.headers["x-forwarded-for"] || ctx.ip || null;
52 | this.time = Number(_.defaultTo(time, util.timestamp()));
53 | }
54 |
55 | validate(key: string, fn?: Function, errorMessage?: string) {
56 | try {
57 | const value = _.get(this, key);
58 | if (fn) {
59 | if (fn(value) === false)
60 | throw `[Mismatch] -> ${fn}`;
61 | }
62 | else if (_.isUndefined(value))
63 | throw '[Undefined]';
64 | }
65 | catch (err) {
66 | logger.warn(`Params ${key} invalid:`, err);
67 | throw new APIException(EX.API_REQUEST_PARAMS_INVALID, errorMessage || `Params ${key} invalid`);
68 | }
69 | return this;
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/response/Body.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export interface BodyOptions {
4 | code?: number;
5 | message?: string;
6 | data?: any;
7 | statusCode?: number;
8 | }
9 |
10 | export default class Body {
11 |
12 | /** 状态码 */
13 | code: number;
14 | /** 状态消息 */
15 | message: string;
16 | /** 载荷 */
17 | data: any;
18 | /** HTTP状态码 */
19 | statusCode: number;
20 |
21 | constructor(options: BodyOptions = {}) {
22 | const { code, message, data, statusCode } = options;
23 | this.code = Number(_.defaultTo(code, 0));
24 | this.message = _.defaultTo(message, 'OK');
25 | this.data = _.defaultTo(data, null);
26 | this.statusCode = Number(_.defaultTo(statusCode, 200));
27 | }
28 |
29 | toObject() {
30 | return {
31 | code: this.code,
32 | message: this.message,
33 | data: this.data
34 | };
35 | }
36 |
37 | static isInstance(value) {
38 | return value instanceof Body;
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/response/FailureBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 | import Exception from '../exceptions/Exception.ts';
5 | import APIException from '../exceptions/APIException.ts';
6 | import EX from '../consts/exceptions.ts';
7 | import HTTP_STATUS_CODES from '../http-status-codes.ts';
8 |
9 | export default class FailureBody extends Body {
10 |
11 | constructor(error: APIException | Exception | Error, _data?: any) {
12 | let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
13 | if(_.isString(error))
14 | error = new Exception(EX.SYSTEM_ERROR, error);
15 | else if(error instanceof APIException || error instanceof Exception)
16 | ({ errcode, errmsg, data, httpStatusCode } = error);
17 | else if(_.isError(error))
18 | ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
19 | super({
20 | code: errcode || -1,
21 | message: errmsg || 'Internal error',
22 | data,
23 | statusCode: httpStatusCode
24 | });
25 | }
26 |
27 | static isInstance(value) {
28 | return value instanceof FailureBody;
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/response/Response.ts:
--------------------------------------------------------------------------------
1 | import mime from 'mime';
2 | import _ from 'lodash';
3 |
4 | import Body from './Body.ts';
5 | import util from '../util.ts';
6 |
7 | export interface ResponseOptions {
8 | statusCode?: number;
9 | type?: string;
10 | headers?: Record;
11 | redirect?: string;
12 | body?: any;
13 | size?: number;
14 | time?: number;
15 | }
16 |
17 | export default class Response {
18 |
19 | /** 响应HTTP状态码 */
20 | statusCode: number;
21 | /** 响应内容类型 */
22 | type: string;
23 | /** 响应headers */
24 | headers: Record;
25 | /** 重定向目标 */
26 | redirect: string;
27 | /** 响应载荷 */
28 | body: any;
29 | /** 响应载荷大小 */
30 | size: number;
31 | /** 响应时间戳 */
32 | time: number;
33 |
34 | constructor(body: any, options: ResponseOptions = {}) {
35 | const { statusCode, type, headers, redirect, size, time } = options;
36 | this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
37 | this.type = type;
38 | this.headers = headers;
39 | this.redirect = redirect;
40 | this.size = size;
41 | this.time = Number(_.defaultTo(time, util.timestamp()));
42 | this.body = body;
43 | }
44 |
45 | injectTo(ctx) {
46 | this.redirect && ctx.redirect(this.redirect);
47 | this.statusCode && (ctx.status = this.statusCode);
48 | this.type && (ctx.type = mime.getType(this.type) || this.type);
49 | const headers = this.headers || {};
50 | if(this.size && !headers["Content-Length"] && !headers["content-length"])
51 | headers["Content-Length"] = this.size;
52 | ctx.set(headers);
53 | if(Body.isInstance(this.body))
54 | ctx.body = this.body.toObject();
55 | else
56 | ctx.body = this.body;
57 | }
58 |
59 | static isInstance(value) {
60 | return value instanceof Response;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/gaokao-free-api/src/lib/response/SuccessfulBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 |
5 | export default class SuccessfulBody extends Body {
6 |
7 | constructor(data: any, message?: string) {
8 | super({
9 | code: 0,
10 | message: _.defaultTo(message, "OK"),
11 | data
12 | });
13 | }
14 |
15 | static isInstance(value) {
16 | return value instanceof SuccessfulBody;
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/gaokao-free-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "allowImportingTsExtensions": true,
7 | "allowSyntheticDefaultImports": true,
8 | "noEmit": true,
9 | "paths": {
10 | "@/*": ["src/*"]
11 | },
12 | "outDir": "./dist"
13 | },
14 | "include": ["src/**/*", "libs.d.ts"],
15 | "exclude": ["node_modules", "dist"]
16 | }
--------------------------------------------------------------------------------
/meme-api/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/meme-api/.DS_Store
--------------------------------------------------------------------------------
/meme-api/.dockerignore:
--------------------------------------------------------------------------------
1 | logs
2 | dist
3 | doc
4 | node_modules
5 | .vscode
6 | .git
7 | .gitignore
8 | README.md
9 | *.tar.gz
--------------------------------------------------------------------------------
/meme-api/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | logs/
4 | .bin/
5 | .vercel
6 | forward-port-proxy.sh
7 | *.pem
8 | *.zip
--------------------------------------------------------------------------------
/meme-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts AS BUILD_IMAGE
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8 |
9 | FROM node:lts-alpine
10 |
11 | COPY --from=BUILD_IMAGE /app/configs /app/configs
12 | COPY --from=BUILD_IMAGE /app/package.json /app/package.json
13 | COPY --from=BUILD_IMAGE /app/dist /app/dist
14 | COPY --from=BUILD_IMAGE /app/public /app/public
15 | COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16 |
17 | WORKDIR /app
18 |
19 | EXPOSE 9000
20 |
21 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/meme-api/README.md:
--------------------------------------------------------------------------------
1 | # Meme API 服务
2 |
3 |
--------------------------------------------------------------------------------
/meme-api/configs/dev/service.yml:
--------------------------------------------------------------------------------
1 | # 服务名称
2 | name: meme-api
3 | # 服务绑定主机地址
4 | host: '0.0.0.0'
5 | # 服务绑定端口
6 | port: 9002
--------------------------------------------------------------------------------
/meme-api/configs/dev/system.yml:
--------------------------------------------------------------------------------
1 | # 是否开启请求日志
2 | requestLog: true
3 | # 临时目录路径
4 | tmpDir: ./tmp
5 | # 日志目录路径
6 | logDir: ./logs
7 | # 日志写入间隔(毫秒)
8 | logWriteInterval: 200
9 | # 日志文件有效期(毫秒)
10 | logFileExpires: 2626560000
11 | # 公共目录路径
12 | publicDir: ./public
13 | # 临时文件有效期(毫秒)
14 | tmpFileExpires: 86400000
--------------------------------------------------------------------------------
/meme-api/libs.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/meme-api/libs.d.ts
--------------------------------------------------------------------------------
/meme-api/openapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "个人项目",
5 | "description": "",
6 | "version": "1.0.0"
7 | },
8 | "tags": [],
9 | "paths": {
10 | "/meme/generate_meme": {
11 | "post": {
12 | "operationId": "generateMeme",
13 | "summary": "生成MEME图像",
14 | "deprecated": false,
15 | "description": "",
16 | "tags": [],
17 | "parameters": [],
18 | "requestBody": {
19 | "content": {
20 | "application/json": {
21 | "schema": {
22 | "required": [
23 | "query"
24 | ],
25 | "type": "object",
26 | "properties": {
27 | "query": {
28 | "title": "用户输入内容",
29 | "type": "string"
30 | }
31 | }
32 | },
33 | "example": {
34 | "query": "程序员"
35 | }
36 | }
37 | }
38 | },
39 | "responses": {
40 | "200": {
41 | "description": "成功",
42 | "content": {
43 | "*/*": {
44 | "schema": { "type": "object", "properties": {} },
45 | "examples": {
46 | "1": {
47 | "summary": "成功示例",
48 | "value": ""
49 | }
50 | }
51 | }
52 | }
53 | }
54 | },
55 | "security": []
56 | }
57 | }
58 | },
59 | "components": {
60 | "schemas": {},
61 | "securitySchemes": {}
62 | },
63 | "servers": [
64 | {
65 | "url": "https://meme-api.vinlic.com"
66 | }
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/meme-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "meme-api",
3 | "version": "0.0.1",
4 | "description": "Meme API Server",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "types": "dist/index.d.ts",
9 | "directories": {
10 | "dist": "dist"
11 | },
12 | "files": [
13 | "dist/"
14 | ],
15 | "scripts": {
16 | "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node dist/index.js\"",
17 | "start": "node dist/index.js",
18 | "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
19 | },
20 | "author": "Vinlic",
21 | "license": "ISC",
22 | "dependencies": {
23 | "@puppeteer/browsers": "^2.2.3",
24 | "axios": "^1.6.7",
25 | "cli-progress": "^3.12.0",
26 | "colors": "^1.4.0",
27 | "crc-32": "^1.2.2",
28 | "cron": "^3.1.6",
29 | "date-fns": "^3.3.1",
30 | "eventsource-parser": "^1.1.2",
31 | "form-data": "^4.0.0",
32 | "fs-extra": "^11.2.0",
33 | "jsonrepair": "^3.8.0",
34 | "koa": "^2.15.0",
35 | "koa-body": "^5.0.0",
36 | "koa-bodyparser": "^4.4.1",
37 | "koa-range": "^0.3.0",
38 | "koa-router": "^12.0.1",
39 | "koa2-cors": "^2.0.6",
40 | "lodash": "^4.17.21",
41 | "mime": "^4.0.1",
42 | "minimist": "^1.2.8",
43 | "puppeteer-core": "^22.12.1",
44 | "randomstring": "^1.3.0",
45 | "uuid": "^9.0.1",
46 | "yaml": "^2.3.4"
47 | },
48 | "devDependencies": {
49 | "@types/fs-extra": "^11.0.4",
50 | "@types/lodash": "^4.14.202",
51 | "@types/mime": "^3.0.4",
52 | "tsup": "^8.0.2",
53 | "typescript": "^5.3.3"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/meme-api/public/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | MEME模板
6 |
63 |
64 |
65 |
66 |
67 |
72 |
73 |
「 {{title}} 」
74 |
75 |
76 |
“{{text0}}”
77 |
-- {{reply0}}
78 |
79 |
80 |
“{{text1}}”
81 |
-- {{reply1}}
82 |
83 |
84 |
“{{text2}}”
85 |
-- {{reply2}}
86 |
87 |
88 |
89 |
90 |
“{{text3}}”
91 |
-- {{reply3}}
92 |
93 |
94 |
“{{text4}}”
95 |
-- {{reply4}}
96 |
97 |
98 |
“{{text5}}”
99 |
-- {{reply5}}
100 |
101 |
102 |
103 |
104 |
“{{text6}}”
105 |
-- {{reply6}}
106 |
107 |
108 |
109 |
“{{text7}}”
110 |
-- {{reply7}}
111 |
112 |
113 |
114 |
115 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/meme-api/public/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 🚀 服务已启动
6 |
7 |
8 | meme-api已启动!
9 |
10 |
--------------------------------------------------------------------------------
/meme-api/src/api/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | API_TEST: [-9999, 'API异常错误'],
3 | API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
4 | API_REQUEST_FAILED: [-2001, '请求失败'],
5 | API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
6 | API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
7 | API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
8 | API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'],
9 | API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'],
10 | API_IMAGE_GENERATION_FAILED: [-2007, '图像生成失败']
11 | }
--------------------------------------------------------------------------------
/meme-api/src/api/routes/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | import Response from '@/lib/response/Response.ts';
4 | import meme from "./meme.ts";
5 |
6 | export default [
7 | {
8 | get: {
9 | '/': async () => {
10 | const content = await fs.readFile('public/welcome.html');
11 | return new Response(content, {
12 | type: 'html',
13 | headers: {
14 | Expires: '-1'
15 | }
16 | });
17 | },
18 | '/template': async () => {
19 | const content = await fs.readFile('public/template.html');
20 | return new Response(content, {
21 | type: 'html',
22 | headers: {
23 | Expires: '-1'
24 | }
25 | });
26 | }
27 | }
28 | },
29 | meme
30 | ];
--------------------------------------------------------------------------------
/meme-api/src/api/routes/meme.ts:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | import Request from "@/lib/request/Request.ts";
4 | import logger from "@/lib/logger.ts";
5 | import meme from "@/api/controllers/meme.ts";
6 |
7 | export default {
8 | prefix: "/meme-api",
9 |
10 | post: {
11 |
12 | "/generate_meme": async (request: Request) => {
13 | request
14 | .validate('body.query');
15 | const { query } = request.body;
16 | const imageUrl = await (async () => {
17 | const imageUrl = await meme.generateMeme(query);
18 | return imageUrl
19 | })()
20 | .then(v => v)
21 | .catch(err => {
22 | logger.error(err);
23 | throw new Error('生成MEME表情包失败,可能是生成超时或失败,请重新调用generateMeme工具!');
24 | });
25 | return `MEME图像已经生成,请以Markdown输出:\n`;
26 | }
27 |
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/meme-api/src/daemon.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 守护进程
3 | */
4 |
5 | import process from 'process';
6 | import path from 'path';
7 | import { spawn } from 'child_process';
8 |
9 | import fs from 'fs-extra';
10 | import { format as dateFormat } from 'date-fns';
11 | import 'colors';
12 |
13 | const CRASH_RESTART_LIMIT = 600; //进程崩溃重启次数限制
14 | const CRASH_RESTART_DELAY = 5000; //进程崩溃重启延迟
15 | const LOG_PATH = path.resolve("./logs/daemon.log"); //守护进程日志路径
16 | let crashCount = 0; //进程崩溃次数
17 | let currentProcess; //当前运行进程
18 |
19 | /**
20 | * 写入守护进程日志
21 | */
22 | function daemonLog(value, color?: string) {
23 | try {
24 | const head = `[daemon][${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")}] `;
25 | value = head + value;
26 | console.log(color ? value[color] : value);
27 | fs.ensureDirSync(path.dirname(LOG_PATH));
28 | fs.appendFileSync(LOG_PATH, value + "\n");
29 | }
30 | catch(err) {
31 | console.error("daemon log write error:", err);
32 | }
33 | }
34 |
35 | daemonLog(`daemon pid: ${process.pid}`);
36 |
37 | function createProcess() {
38 | const childProcess = spawn("node", ["index.js", ...process.argv.slice(2)]); //启动子进程
39 | childProcess.stdout.pipe(process.stdout, { end: false }); //将子进程输出管道到当前进程输出
40 | childProcess.stderr.pipe(process.stderr, { end: false }); //将子进程错误输出管道到当前进程输出
41 | currentProcess = childProcess; //更新当前进程
42 | daemonLog(`process(${childProcess.pid}) has started`);
43 | childProcess.on("error", err => daemonLog(`process(${childProcess.pid}) error: ${err.stack}`, "red"));
44 | childProcess.on("close", code => {
45 | if(code === 0) //进程正常退出
46 | daemonLog(`process(${childProcess.pid}) has exited`);
47 | else if(code === 2) //进程已被杀死
48 | daemonLog(`process(${childProcess.pid}) has been killed!`, "bgYellow");
49 | else if(code === 3) { //进程主动重启
50 | daemonLog(`process(${childProcess.pid}) has restart`, "yellow");
51 | createProcess(); //重新创建进程
52 | }
53 | else { //进程发生崩溃
54 | if(crashCount++ < CRASH_RESTART_LIMIT) { //进程崩溃次数未达重启次数上限前尝试重启
55 | daemonLog(`process(${childProcess.pid}) has crashed! delay ${CRASH_RESTART_DELAY}ms try restarting...(${crashCount})`, "bgRed");
56 | setTimeout(() => createProcess(), CRASH_RESTART_DELAY); //延迟指定时长后再重启
57 | }
58 | else //进程已崩溃,且无法重启
59 | daemonLog(`process(${childProcess.pid}) has crashed! unable to restart`, "bgRed");
60 | }
61 | }); //子进程关闭监听
62 | }
63 |
64 | process.on("exit", code => {
65 | if(code === 0)
66 | daemonLog("daemon process exited");
67 | else if(code === 2)
68 | daemonLog("daemon process has been killed!");
69 | }); //守护进程退出事件
70 |
71 | process.on("SIGTERM", () => {
72 | daemonLog("received kill signal", "yellow");
73 | currentProcess && currentProcess.kill("SIGINT");
74 | process.exit(2);
75 | }); //kill退出守护进程
76 |
77 | process.on("SIGINT", () => {
78 | currentProcess && currentProcess.kill("SIGINT");
79 | process.exit(0);
80 | }); //主动退出守护进程
81 |
82 | createProcess(); //创建进程
83 |
--------------------------------------------------------------------------------
/meme-api/src/index.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import environment from "@/lib/environment.ts";
4 | import config from "@/lib/config.ts";
5 | import "@/lib/initialize.ts";
6 | import server from "@/lib/server.ts";
7 | import routes from "@/api/routes/index.ts";
8 | import logger from "@/lib/logger.ts";
9 |
10 | const startupTime = performance.now();
11 |
12 | (async () => {
13 | logger.header();
14 |
15 | logger.info("<<<< meme free server >>>>");
16 | logger.info("Version:", environment.package.version);
17 | logger.info("Process id:", process.pid);
18 | logger.info("Environment:", environment.env);
19 | logger.info("Service name:", config.service.name);
20 |
21 | server.attachRoutes(routes);
22 | await server.listen();
23 |
24 | config.service.bindAddress &&
25 | logger.success("Service bind address:", config.service.bindAddress);
26 | })()
27 | .then(() =>
28 | logger.success(
29 | `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
30 | )
31 | )
32 | .catch((err) => console.error(err));
33 |
--------------------------------------------------------------------------------
/meme-api/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | import serviceConfig from "./configs/service-config.ts";
2 | import systemConfig from "./configs/system-config.ts";
3 |
4 | class Config {
5 |
6 | /** 服务配置 */
7 | service = serviceConfig;
8 |
9 | /** 系统配置 */
10 | system = systemConfig;
11 |
12 | }
13 |
14 | export default new Config();
--------------------------------------------------------------------------------
/meme-api/src/lib/configs/service-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 | import util from '../util.ts';
9 |
10 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
11 |
12 | /**
13 | * 服务配置
14 | */
15 | export class ServiceConfig {
16 |
17 | /** 服务名称 */
18 | name: string;
19 | /** @type {string} 服务绑定主机地址 */
20 | host;
21 | /** @type {number} 服务绑定端口 */
22 | port;
23 | /** @type {string} 服务路由前缀 */
24 | urlPrefix;
25 | /** @type {string} 服务绑定地址(外部访问地址) */
26 | bindAddress;
27 |
28 | constructor(options?: any) {
29 | const { name, host, port, urlPrefix, bindAddress } = options || {};
30 | this.name = _.defaultTo(name, 'meme-api');
31 | this.host = _.defaultTo(host, '0.0.0.0');
32 | this.port = _.defaultTo(port, 5566);
33 | this.urlPrefix = _.defaultTo(urlPrefix, '');
34 | this.bindAddress = bindAddress;
35 | }
36 |
37 | get addressHost() {
38 | if(this.bindAddress) return this.bindAddress;
39 | const ipAddresses = util.getIPAddressesByIPv4();
40 | for(let ipAddress of ipAddresses) {
41 | if(ipAddress === this.host)
42 | return ipAddress;
43 | }
44 | return ipAddresses[0] || "127.0.0.1";
45 | }
46 |
47 | get address() {
48 | return `${this.addressHost}:${this.port}`;
49 | }
50 |
51 | get pageDirUrl() {
52 | return `http://127.0.0.1:${this.port}/page`;
53 | }
54 |
55 | get publicDirUrl() {
56 | return `http://127.0.0.1:${this.port}/public`;
57 | }
58 |
59 | static load() {
60 | const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
61 | if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
62 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
63 | return new ServiceConfig({ ...data, ...external });
64 | }
65 |
66 | }
67 |
68 | export default ServiceConfig.load();
--------------------------------------------------------------------------------
/meme-api/src/lib/configs/system-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 |
9 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
10 |
11 | /**
12 | * 系统配置
13 | */
14 | export class SystemConfig {
15 |
16 | /** 是否开启请求日志 */
17 | requestLog: boolean;
18 | /** 临时目录路径 */
19 | tmpDir: string;
20 | /** 日志目录路径 */
21 | logDir: string;
22 | /** 日志写入间隔(毫秒) */
23 | logWriteInterval: number;
24 | /** 日志文件有效期(毫秒) */
25 | logFileExpires: number;
26 | /** 公共目录路径 */
27 | publicDir: string;
28 | /** 临时文件有效期(毫秒) */
29 | tmpFileExpires: number;
30 | /** 请求体配置 */
31 | requestBody: any;
32 | /** 是否调试模式 */
33 | debug: boolean;
34 |
35 | constructor(options?: any) {
36 | const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
37 | this.requestLog = _.defaultTo(requestLog, false);
38 | this.tmpDir = _.defaultTo(tmpDir, './tmp');
39 | this.logDir = _.defaultTo(logDir, './logs');
40 | this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
41 | this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
42 | this.publicDir = _.defaultTo(publicDir, './public');
43 | this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
44 | this.requestBody = Object.assign(requestBody || {}, {
45 | enableTypes: ['json', 'form', 'text', 'xml'],
46 | encoding: 'utf-8',
47 | formLimit: '100mb',
48 | jsonLimit: '100mb',
49 | textLimit: '100mb',
50 | xmlLimit: '100mb',
51 | formidable: {
52 | maxFileSize: '100mb'
53 | },
54 | multipart: true,
55 | parsedMethods: ['POST', 'PUT', 'PATCH']
56 | });
57 | this.debug = _.defaultTo(debug, true);
58 | }
59 |
60 | get rootDirPath() {
61 | return path.resolve();
62 | }
63 |
64 | get tmpDirPath() {
65 | return path.resolve(this.tmpDir);
66 | }
67 |
68 | get logDirPath() {
69 | return path.resolve(this.logDir);
70 | }
71 |
72 | get publicDirPath() {
73 | return path.resolve(this.publicDir);
74 | }
75 |
76 | static load() {
77 | if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
78 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
79 | return new SystemConfig(data);
80 | }
81 |
82 | }
83 |
84 | export default SystemConfig.load();
--------------------------------------------------------------------------------
/meme-api/src/lib/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | SYSTEM_ERROR: [-1000, '系统异常'],
3 | SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
4 | SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
5 | } as Record
--------------------------------------------------------------------------------
/meme-api/src/lib/environment.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import minimist from 'minimist';
5 | import _ from 'lodash';
6 |
7 | const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
8 | const envVars = process.env; //获取环境变量
9 |
10 | class Environment {
11 |
12 | /** 命令行参数 */
13 | cmdArgs: any;
14 | /** 环境变量 */
15 | envVars: any;
16 | /** 环境名称 */
17 | env?: string;
18 | /** 服务名称 */
19 | name?: string;
20 | /** 服务地址 */
21 | host?: string;
22 | /** 服务端口 */
23 | port?: number;
24 | /** 包参数 */
25 | package: any;
26 |
27 | constructor(options: any = {}) {
28 | const { cmdArgs, envVars, package: _package } = options;
29 | this.cmdArgs = cmdArgs;
30 | this.envVars = envVars;
31 | this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
32 | this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
33 | this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
34 | this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
35 | this.package = _package;
36 | }
37 |
38 | }
39 |
40 | export default new Environment({
41 | cmdArgs,
42 | envVars,
43 | package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
44 | });
--------------------------------------------------------------------------------
/meme-api/src/lib/exceptions/APIException.ts:
--------------------------------------------------------------------------------
1 | import Exception from './Exception.js';
2 |
3 | export default class APIException extends Exception {
4 |
5 | /**
6 | * 构造异常
7 | *
8 | * @param {[number, string]} exception 异常
9 | */
10 | constructor(exception: (string | number)[], errmsg?: string) {
11 | super(exception, errmsg);
12 | }
13 |
14 | }
--------------------------------------------------------------------------------
/meme-api/src/lib/exceptions/Exception.ts:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 |
3 | import _ from 'lodash';
4 |
5 | export default class Exception extends Error {
6 |
7 | /** 错误码 */
8 | errcode: number;
9 | /** 错误消息 */
10 | errmsg: string;
11 | /** 数据 */
12 | data: any;
13 | /** HTTP状态码 */
14 | httpStatusCode: number;
15 |
16 | /**
17 | * 构造异常
18 | *
19 | * @param exception 异常
20 | * @param _errmsg 异常消息
21 | */
22 | constructor(exception: (string | number)[], _errmsg?: string) {
23 | assert(_.isArray(exception), 'Exception must be Array');
24 | const [errcode, errmsg] = exception as [number, string];
25 | assert(_.isFinite(errcode), 'Exception errcode invalid');
26 | assert(_.isString(errmsg), 'Exception errmsg invalid');
27 | super(_errmsg || errmsg);
28 | this.errcode = errcode;
29 | this.errmsg = _errmsg || errmsg;
30 | }
31 |
32 | compare(exception: (string | number)[]) {
33 | const [errcode] = exception as [number, string];
34 | return this.errcode == errcode;
35 | }
36 |
37 | setHTTPStatusCode(value: number) {
38 | this.httpStatusCode = value;
39 | return this;
40 | }
41 |
42 | setData(value: any) {
43 | this.data = _.defaultTo(value, null);
44 | return this;
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/meme-api/src/lib/initialize.ts:
--------------------------------------------------------------------------------
1 | import logger from './logger.js';
2 |
3 | // 允许无限量的监听器
4 | process.setMaxListeners(Infinity);
5 | // 输出未捕获异常
6 | process.on("uncaughtException", (err, origin) => {
7 | logger.error(`An unhandled error occurred: ${origin}`, err);
8 | });
9 | // 输出未处理的Promise.reject
10 | process.on("unhandledRejection", (_, promise) => {
11 | promise.catch(err => logger.error("An unhandled rejection occurred:", err));
12 | });
13 | // 输出系统警告信息
14 | process.on("warning", warning => logger.warn("System warning: ", warning));
15 | // 进程退出监听
16 | process.on("exit", () => {
17 | logger.info("Service exit");
18 | logger.footer();
19 | });
20 | // 进程被kill
21 | process.on("SIGTERM", () => {
22 | logger.warn("received kill signal");
23 | process.exit(2);
24 | });
25 | // Ctrl-C进程退出
26 | process.on("SIGINT", () => {
27 | process.exit(0);
28 | });
--------------------------------------------------------------------------------
/meme-api/src/lib/install-browser.ts:
--------------------------------------------------------------------------------
1 | import os from "os";
2 | import path from "path";
3 | import assert from "assert";
4 | import fs from "fs-extra";
5 | import _ from "lodash";
6 | import { BrowserPlatform, Browser, install, resolveBuildId, computeExecutablePath } from "@puppeteer/browsers";
7 |
8 | import cliProgress from "cli-progress";
9 | import logger from "./logger.ts";
10 |
11 | // 默认浏览器安装路径
12 | const browserInstallPath = ".bin";
13 | // 默认浏览器名称
14 | // 目前只限于chrome,如使用chromium可能会缺失H264解码功能
15 | const browserName = Browser.CHROME;
16 | // 默认浏览器版本号,不能低于119.0.6018.0,否则无法使用VideoDecoder解码H264
17 | // 请参考:https://github.com/GoogleChromeLabs/chrome-for-testing/issues/18
18 | const browserVersion = "119.0.6029.0";
19 | // 下载进度条
20 | const downloadProgressBar = new cliProgress.SingleBar({ hideCursor: true }, cliProgress.Presets.shades_classic);
21 |
22 | /**
23 | * 安装浏览器
24 | *
25 | * @param installPath - 安装路径
26 | */
27 | export default async function installBrowser(installPath = browserInstallPath) {
28 | assert(_.isString(installPath), "install path must be string");
29 |
30 | const version = browserVersion;
31 |
32 | const platform = os.platform();
33 | const arch = os.arch();
34 | let browserPlatform: BrowserPlatform;
35 |
36 | // 根据不同平台架构选择浏览器平台
37 | if (platform == "win32") {
38 | if (arch == "x64")
39 | browserPlatform = BrowserPlatform.WIN64;
40 | else
41 | browserPlatform = BrowserPlatform.WIN32;
42 | }
43 | else if (platform == "darwin") {
44 | if (arch == "arm64")
45 | browserPlatform = BrowserPlatform.MAC_ARM;
46 | else
47 | browserPlatform = BrowserPlatform.MAC;
48 | }
49 | else
50 | browserPlatform = BrowserPlatform.LINUX;
51 |
52 | // 获取buildId
53 | const buildId = await resolveBuildId(browserName, browserPlatform, version);
54 | installPath = path.resolve(installPath);
55 | const downloadOptions = {
56 | cacheDir: installPath,
57 | browser: browserName,
58 | platform: browserPlatform,
59 | buildId
60 | };
61 |
62 | // 补全可执行文件路径
63 | const executablePath = computeExecutablePath(downloadOptions);
64 | // 如果不存在可执行文件则进行下载安装
65 | if (!await fs.pathExists(executablePath)) {
66 | logger.info(`Installing chrome into ${installPath}`);
67 | let downloadStart = false;
68 | await install({
69 | ...downloadOptions,
70 | downloadProgressCallback: (downloadedBytes, totalBytes) => {
71 | if (!downloadStart) {
72 | downloadProgressBar.start(Infinity, 0);
73 | downloadStart = true;
74 | }
75 | downloadProgressBar.setTotal(totalBytes);
76 | downloadProgressBar.update(downloadedBytes);
77 | }
78 | });
79 | logger.info("\nInstallation completed");
80 | }
81 |
82 | return {
83 | executablePath
84 | };
85 |
86 | }
--------------------------------------------------------------------------------
/meme-api/src/lib/interfaces/ICompletionMessage.ts:
--------------------------------------------------------------------------------
1 | export default interface ICompletionMessage {
2 | role: 'system' | 'assistant' | 'user' | 'function';
3 | content: string;
4 | }
--------------------------------------------------------------------------------
/meme-api/src/lib/request/Request.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import APIException from '@/lib/exceptions/APIException.ts';
4 | import EX from '@/api/consts/exceptions.ts';
5 | import logger from '@/lib/logger.ts';
6 | import util from '@/lib/util.ts';
7 |
8 | export interface RequestOptions {
9 | time?: number;
10 | }
11 |
12 | export default class Request {
13 |
14 | /** 请求方法 */
15 | method: string;
16 | /** 请求URL */
17 | url: string;
18 | /** 请求路径 */
19 | path: string;
20 | /** 请求载荷类型 */
21 | type: string;
22 | /** 请求headers */
23 | headers: any;
24 | /** 请求原始查询字符串 */
25 | search: string;
26 | /** 请求查询参数 */
27 | query: any;
28 | /** 请求URL参数 */
29 | params: any;
30 | /** 请求载荷 */
31 | body: any;
32 | /** 上传的文件 */
33 | files: any[];
34 | /** 客户端IP地址 */
35 | remoteIP: string | null;
36 | /** 请求接受时间戳(毫秒) */
37 | time: number;
38 |
39 | constructor(ctx, options: RequestOptions = {}) {
40 | const { time } = options;
41 | this.method = ctx.request.method;
42 | this.url = ctx.request.url;
43 | this.path = ctx.request.path;
44 | this.type = ctx.request.type;
45 | this.headers = ctx.request.headers || {};
46 | this.search = ctx.request.search;
47 | this.query = ctx.query || {};
48 | this.params = ctx.params || {};
49 | this.body = ctx.request.body || {};
50 | this.files = ctx.request.files || {};
51 | this.remoteIP = this.headers["X-Real-IP"] || this.headers["x-real-ip"] || this.headers["X-Forwarded-For"] || this.headers["x-forwarded-for"] || ctx.ip || null;
52 | this.time = Number(_.defaultTo(time, util.timestamp()));
53 | }
54 |
55 | validate(key: string, fn?: Function, errorMessage?: string) {
56 | try {
57 | const value = _.get(this, key);
58 | if (fn) {
59 | if (fn(value) === false)
60 | throw `[Mismatch] -> ${fn}`;
61 | }
62 | else if (_.isUndefined(value))
63 | throw '[Undefined]';
64 | }
65 | catch (err) {
66 | logger.warn(`Params ${key} invalid:`, err);
67 | throw new APIException(EX.API_REQUEST_PARAMS_INVALID, errorMessage || `Params ${key} invalid`);
68 | }
69 | return this;
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/meme-api/src/lib/response/Body.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export interface BodyOptions {
4 | code?: number;
5 | message?: string;
6 | data?: any;
7 | statusCode?: number;
8 | }
9 |
10 | export default class Body {
11 |
12 | /** 状态码 */
13 | code: number;
14 | /** 状态消息 */
15 | message: string;
16 | /** 载荷 */
17 | data: any;
18 | /** HTTP状态码 */
19 | statusCode: number;
20 |
21 | constructor(options: BodyOptions = {}) {
22 | const { code, message, data, statusCode } = options;
23 | this.code = Number(_.defaultTo(code, 0));
24 | this.message = _.defaultTo(message, 'OK');
25 | this.data = _.defaultTo(data, null);
26 | this.statusCode = Number(_.defaultTo(statusCode, 200));
27 | }
28 |
29 | toObject() {
30 | return {
31 | code: this.code,
32 | message: this.message,
33 | data: this.data
34 | };
35 | }
36 |
37 | static isInstance(value) {
38 | return value instanceof Body;
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/meme-api/src/lib/response/FailureBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 | import Exception from '../exceptions/Exception.ts';
5 | import APIException from '../exceptions/APIException.ts';
6 | import EX from '../consts/exceptions.ts';
7 | import HTTP_STATUS_CODES from '../http-status-codes.ts';
8 |
9 | export default class FailureBody extends Body {
10 |
11 | constructor(error: APIException | Exception | Error, _data?: any) {
12 | let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
13 | if(_.isString(error))
14 | error = new Exception(EX.SYSTEM_ERROR, error);
15 | else if(error instanceof APIException || error instanceof Exception)
16 | ({ errcode, errmsg, data, httpStatusCode } = error);
17 | else if(_.isError(error))
18 | ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
19 | super({
20 | code: errcode || -1,
21 | message: errmsg || 'Internal error',
22 | data,
23 | statusCode: httpStatusCode
24 | });
25 | }
26 |
27 | static isInstance(value) {
28 | return value instanceof FailureBody;
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/meme-api/src/lib/response/Response.ts:
--------------------------------------------------------------------------------
1 | import mime from 'mime';
2 | import _ from 'lodash';
3 |
4 | import Body from './Body.ts';
5 | import util from '../util.ts';
6 |
7 | export interface ResponseOptions {
8 | statusCode?: number;
9 | type?: string;
10 | headers?: Record;
11 | redirect?: string;
12 | body?: any;
13 | size?: number;
14 | time?: number;
15 | }
16 |
17 | export default class Response {
18 |
19 | /** 响应HTTP状态码 */
20 | statusCode: number;
21 | /** 响应内容类型 */
22 | type: string;
23 | /** 响应headers */
24 | headers: Record;
25 | /** 重定向目标 */
26 | redirect: string;
27 | /** 响应载荷 */
28 | body: any;
29 | /** 响应载荷大小 */
30 | size: number;
31 | /** 响应时间戳 */
32 | time: number;
33 |
34 | constructor(body: any, options: ResponseOptions = {}) {
35 | const { statusCode, type, headers, redirect, size, time } = options;
36 | this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
37 | this.type = type;
38 | this.headers = headers;
39 | this.redirect = redirect;
40 | this.size = size;
41 | this.time = Number(_.defaultTo(time, util.timestamp()));
42 | this.body = body;
43 | }
44 |
45 | injectTo(ctx) {
46 | this.redirect && ctx.redirect(this.redirect);
47 | this.statusCode && (ctx.status = this.statusCode);
48 | this.type && (ctx.type = mime.getType(this.type) || this.type);
49 | const headers = this.headers || {};
50 | if(this.size && !headers["Content-Length"] && !headers["content-length"])
51 | headers["Content-Length"] = this.size;
52 | ctx.set(headers);
53 | if(Body.isInstance(this.body))
54 | ctx.body = this.body.toObject();
55 | else
56 | ctx.body = this.body;
57 | }
58 |
59 | static isInstance(value) {
60 | return value instanceof Response;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/meme-api/src/lib/response/SuccessfulBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 |
5 | export default class SuccessfulBody extends Body {
6 |
7 | constructor(data: any, message?: string) {
8 | super({
9 | code: 0,
10 | message: _.defaultTo(message, "OK"),
11 | data
12 | });
13 | }
14 |
15 | static isInstance(value) {
16 | return value instanceof SuccessfulBody;
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/meme-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "allowImportingTsExtensions": true,
7 | "allowSyntheticDefaultImports": true,
8 | "noEmit": true,
9 | "paths": {
10 | "@/*": ["src/*"]
11 | },
12 | "outDir": "./dist"
13 | },
14 | "include": ["src/**/*", "libs.d.ts"],
15 | "exclude": ["node_modules", "dist"]
16 | }
--------------------------------------------------------------------------------
/olympics-api/.dockerignore:
--------------------------------------------------------------------------------
1 | logs
2 | dist
3 | doc
4 | node_modules
5 | .vscode
6 | .git
7 | .gitignore
8 | README.md
9 | *.tar.gz
--------------------------------------------------------------------------------
/olympics-api/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | logs/
4 | .vercel
5 |
--------------------------------------------------------------------------------
/olympics-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts AS BUILD_IMAGE
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8 |
9 | FROM node:lts-alpine
10 |
11 | COPY --from=BUILD_IMAGE /app/configs /app/configs
12 | COPY --from=BUILD_IMAGE /app/package.json /app/package.json
13 | COPY --from=BUILD_IMAGE /app/dist /app/dist
14 | COPY --from=BUILD_IMAGE /app/public /app/public
15 | COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16 |
17 | WORKDIR /app
18 |
19 | EXPOSE 8000
20 |
21 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/olympics-api/README.md:
--------------------------------------------------------------------------------
1 | # Olympics API
2 |
3 | ## 简介
4 |
5 | 此API从olympics.com拉取数据,提供了奥运会的相关数据。
6 |
7 | ## API文档
8 |
9 | 请参考[API文档](https://apifox.com/apidoc/shared-5329d838-2b1a-4f8f-beb1-123075a15da9)。
10 |
11 | ## 部署
12 |
13 | 首先,您需要安装Node.js 18+。然后,您可以使用以下命令在项目根目录安装依赖和构建部署:
14 |
15 | ```bash
16 | # 确认Node版本是否在18以上
17 | node -v
18 | # 全局安装PM2进程管理器
19 | npm install pm2 -g --registry https://registry.npmmirror.com
20 | # 安装依赖
21 | npm install --registry https://registry.npmmirror.com
22 | # 编译构建
23 | npm run build
24 | # 启动服务
25 | pm2 start dist/index.js --name "olympics-api"
26 | # 查看服务日志
27 | pm2 logs olympics-api
28 | ```
--------------------------------------------------------------------------------
/olympics-api/configs/dev/service.yml:
--------------------------------------------------------------------------------
1 | # 服务名称
2 | name: olympics-api
3 | # 服务绑定主机地址
4 | host: '0.0.0.0'
5 | # 服务绑定端口
6 | port: 8000
--------------------------------------------------------------------------------
/olympics-api/configs/dev/system.yml:
--------------------------------------------------------------------------------
1 | # 是否开启请求日志
2 | requestLog: true
3 | # 临时目录路径
4 | tmpDir: ./tmp
5 | # 日志目录路径
6 | logDir: ./logs
7 | # 日志写入间隔(毫秒)
8 | logWriteInterval: 200
9 | # 日志文件有效期(毫秒)
10 | logFileExpires: 2626560000
11 | # 公共目录路径
12 | publicDir: ./public
13 | # 临时文件有效期(毫秒)
14 | tmpFileExpires: 86400000
--------------------------------------------------------------------------------
/olympics-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "olympics-api",
3 | "version": "0.0.31",
4 | "description": "Olympics API Server",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "types": "dist/index.d.ts",
9 | "directories": {
10 | "dist": "dist"
11 | },
12 | "files": [
13 | "dist/"
14 | ],
15 | "scripts": {
16 | "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node --enable-source-maps --no-node-snapshot dist/index.js\"",
17 | "start": "node --enable-source-maps --no-node-snapshot dist/index.js",
18 | "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
19 | },
20 | "author": "Vinlic",
21 | "license": "ISC",
22 | "dependencies": {
23 | "axios": "^1.6.7",
24 | "cheerio": "^1.0.0-rc.12",
25 | "colors": "^1.4.0",
26 | "crc-32": "^1.2.2",
27 | "cron": "^3.1.6",
28 | "date-fns": "^3.3.1",
29 | "eventsource-parser": "^1.1.2",
30 | "form-data": "^4.0.0",
31 | "fs-extra": "^11.2.0",
32 | "koa": "^2.15.0",
33 | "koa-body": "^5.0.0",
34 | "koa-bodyparser": "^4.4.1",
35 | "koa-range": "^0.3.0",
36 | "koa-router": "^12.0.1",
37 | "koa2-cors": "^2.0.6",
38 | "lodash": "^4.17.21",
39 | "mime": "^4.0.1",
40 | "minimist": "^1.2.8",
41 | "randomstring": "^1.3.0",
42 | "uuid": "^9.0.1",
43 | "yaml": "^2.3.4"
44 | },
45 | "devDependencies": {
46 | "@types/cheerio": "^0.22.35",
47 | "@types/fs-extra": "^11.0.4",
48 | "@types/lodash": "^4.14.202",
49 | "@types/mime": "^3.0.4",
50 | "tsup": "^8.0.2",
51 | "typescript": "^5.3.3"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/olympics-api/public/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 🚀 服务已启动
6 |
7 |
8 | olympics-api已启动!
9 |
10 |
--------------------------------------------------------------------------------
/olympics-api/src/api/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | API_TEST: [-9999, 'API异常错误'],
3 | API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
4 | API_REQUEST_FAILED: [-2001, '请求失败'],
5 | API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
6 | API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
7 | API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
8 | API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'],
9 | API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'],
10 | API_IMAGE_GENERATION_FAILED: [-2007, '图像生成失败']
11 | }
--------------------------------------------------------------------------------
/olympics-api/src/api/controllers/interfaces/IAthlete.ts:
--------------------------------------------------------------------------------
1 | import IMedals from "./IMedals.ts";
2 |
3 | export default interface IAthlete {
4 | /** 运动员名称 */
5 | name: string;
6 | /** 运动员照片URL */
7 | image: string;
8 | /** 运动员主页URL */
9 | url: string;
10 | /** 运动员参与的运动 */
11 | sport: string;
12 | /** 运动员获得的奖牌 */
13 | medals: IMedals;
14 | }
--------------------------------------------------------------------------------
/olympics-api/src/api/controllers/interfaces/IGame.ts:
--------------------------------------------------------------------------------
1 | export default interface IGame {
2 | /** 竞赛ID */
3 | id: string;
4 | /** 竞赛名称 */
5 | name: string;
6 | /** 竞赛类型 */
7 | type: string;
8 | /** 竞赛页面URL */
9 | url: string;
10 | /** 竞赛年份 */
11 | year: number;
12 | }
--------------------------------------------------------------------------------
/olympics-api/src/api/controllers/interfaces/IMedals.ts:
--------------------------------------------------------------------------------
1 | export default interface IMedals {
2 | /** 国家 */
3 | country: string;
4 | /** 国家代码 */
5 | code: string;
6 | /** 金牌数量 */
7 | gold: number;
8 | /** 银牌数量 */
9 | silver: number;
10 | /** 铜牌数量 */
11 | bronze: number;
12 | /** 总数量 */
13 | total: number;
14 | }
--------------------------------------------------------------------------------
/olympics-api/src/api/controllers/interfaces/ISchedules.ts:
--------------------------------------------------------------------------------
1 | import ISport from "./ISport.ts";
2 |
3 | export interface ISchedule {
4 | units: {
5 | unitCode: string;
6 | description: string;
7 | match: Record;
8 | start: string;
9 | startDateTimeUtc: string;
10 | localStartDateTime: string;
11 | end: string;
12 | endDateTimeUtc: string;
13 | localEndDateTime: string;
14 | estimated: boolean;
15 | estimatedStart: boolean;
16 | startText: string;
17 | medal: string;
18 | }[],
19 | sport: ISport;
20 | venue: {
21 | code: string;
22 | name: string;
23 | }
24 | }
25 |
26 | export default interface ISchedules {
27 | /** 奥运会排期 */
28 | days: string[];
29 | /** 奥运会的运动列表 */
30 | sports: ISport[];
31 | /** 奥运会的赛程列表 */
32 | schedules: Record;
33 | /** 赛程注意事项 */
34 | disclaimerText: string;
35 | }
--------------------------------------------------------------------------------
/olympics-api/src/api/controllers/interfaces/ISport.ts:
--------------------------------------------------------------------------------
1 | export default interface ISport {
2 | /** 运动ID */
3 | id?: string;
4 | /** 运动名称 */
5 | name: string;
6 | /** 运动代码 */
7 | code: string;
8 | /** 运动URL */
9 | url: string;
10 | }
--------------------------------------------------------------------------------
/olympics-api/src/api/routes/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | import Response from '@/lib/response/Response.ts';
4 | import olympics from '@/api//routes/olympics.ts';
5 |
6 | export default [
7 | {
8 | get: {
9 | '/': async () => {
10 | const content = await fs.readFile('public/welcome.html');
11 | return new Response(content, {
12 | type: 'html',
13 | headers: {
14 | Expires: '-1'
15 | }
16 | });
17 | }
18 | }
19 | },
20 | olympics
21 | ];
--------------------------------------------------------------------------------
/olympics-api/src/api/routes/olympics.ts:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | import olympics from "@/api/controllers/olympics.ts"
4 | import Request from "@/lib/request/Request.ts"
5 |
6 | export default {
7 |
8 | prefix: '/olympics',
9 |
10 | get: {
11 |
12 | '/games': async (request: Request) => {
13 | return await olympics.getGames();
14 | },
15 |
16 | '/medals': async (request: Request) => {
17 | request
18 | .validate('query.game_id', _.isString);
19 | const { game_id: gameId } = request.query;
20 | return await olympics.getMedals(gameId);
21 | },
22 |
23 | '/sports': async (request: Request) => {
24 | request
25 | .validate('query.game_id', _.isString);
26 | const { game_id: gameId } = request.query;
27 | return await olympics.getSports(gameId);
28 | },
29 |
30 | '/schedules': async (request: Request) => {
31 | request
32 | .validate('query.game_id', v => _.isUndefined(v) || _.isString(v));
33 | const { game_id: gameId } = request.query;
34 | return await olympics.getSchedules(gameId);
35 | },
36 |
37 | '/athletes': async (request: Request) => {
38 | request
39 | .validate('query.game_id', _.isString)
40 | .validate('query.sport_id', v => _.isUndefined(v) || _.isString(v))
41 | .validate('query.team_code', v => _.isUndefined(v) || _.isString(v));
42 | const { game_id: gameId, sport_id: sportId, team_code: teamCode } = request.query;
43 | return await olympics.getAthletes(gameId, sportId, teamCode);
44 | }
45 |
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/olympics-api/src/index.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import environment from "@/lib/environment.ts";
4 | import config from "@/lib/config.ts";
5 | import "@/lib/initialize.ts";
6 | import server from "@/lib/server.ts";
7 | import routes from "@/api/routes/index.ts";
8 | import logger from "@/lib/logger.ts";
9 |
10 | const startupTime = performance.now();
11 |
12 | (async () => {
13 | logger.header();
14 |
15 | logger.info("<<<< olympics api >>>>");
16 | logger.info("Version:", environment.package.version);
17 | logger.info("Process id:", process.pid);
18 | logger.info("Environment:", environment.env);
19 | logger.info("Service name:", config.service.name);
20 |
21 | server.attachRoutes(routes);
22 | await server.listen();
23 |
24 | config.service.bindAddress &&
25 | logger.success("Service bind address:", config.service.bindAddress);
26 | })()
27 | .then(() =>
28 | logger.success(
29 | `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
30 | )
31 | )
32 | .catch((err) => console.error(err));
33 |
--------------------------------------------------------------------------------
/olympics-api/src/lib/cache.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs-extra";
2 | import util from "./util.ts";
3 |
4 | class Cache {
5 |
6 | private cache: Map;
7 |
8 | constructor() {
9 | this.cache = new Map();
10 | if(fs.existsSync("./cache.json"))
11 | this.cache = new Map(fs.readJSONSync("./cache.json"));
12 | }
13 |
14 | async set(key: string, value: any, expires: number = 3600) {
15 | this.cache.set(key, {
16 | value,
17 | expireTime: util.unixTimestamp() + expires * 1000
18 | });
19 | await fs.writeJSON("./cache.json", [...this.cache.entries()]);
20 | }
21 |
22 | async get(key: string) {
23 | const data = this.cache.get(key);
24 | if(!data)
25 | return null;
26 | if(data.expireTime < util.unixTimestamp()) {
27 | this.cache.delete(key);
28 | return null;
29 | }
30 | return data.value;
31 | }
32 |
33 | delete(key: string) {
34 | this.cache.delete(key);
35 | }
36 |
37 | clear() {
38 | this.cache.clear();
39 | }
40 |
41 | }
42 |
43 | export default Cache;
--------------------------------------------------------------------------------
/olympics-api/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | import serviceConfig from "./configs/service-config.ts";
2 | import systemConfig from "./configs/system-config.ts";
3 |
4 | class Config {
5 |
6 | /** 服务配置 */
7 | service = serviceConfig;
8 |
9 | /** 系统配置 */
10 | system = systemConfig;
11 |
12 | }
13 |
14 | export default new Config();
--------------------------------------------------------------------------------
/olympics-api/src/lib/configs/service-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 | import util from '../util.ts';
9 |
10 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
11 |
12 | /**
13 | * 服务配置
14 | */
15 | export class ServiceConfig {
16 |
17 | /** 服务名称 */
18 | name: string;
19 | /** @type {string} 服务绑定主机地址 */
20 | host;
21 | /** @type {number} 服务绑定端口 */
22 | port;
23 | /** @type {string} 服务路由前缀 */
24 | urlPrefix;
25 | /** @type {string} 服务绑定地址(外部访问地址) */
26 | bindAddress;
27 |
28 | constructor(options?: any) {
29 | const { name, host, port, urlPrefix, bindAddress } = options || {};
30 | this.name = _.defaultTo(name, 'olympics-api');
31 | this.host = _.defaultTo(host, '0.0.0.0');
32 | this.port = _.defaultTo(port, 5566);
33 | this.urlPrefix = _.defaultTo(urlPrefix, '');
34 | this.bindAddress = bindAddress;
35 | }
36 |
37 | get addressHost() {
38 | if(this.bindAddress) return this.bindAddress;
39 | const ipAddresses = util.getIPAddressesByIPv4();
40 | for(let ipAddress of ipAddresses) {
41 | if(ipAddress === this.host)
42 | return ipAddress;
43 | }
44 | return ipAddresses[0] || "127.0.0.1";
45 | }
46 |
47 | get address() {
48 | return `${this.addressHost}:${this.port}`;
49 | }
50 |
51 | get pageDirUrl() {
52 | return `http://127.0.0.1:${this.port}/page`;
53 | }
54 |
55 | get publicDirUrl() {
56 | return `http://127.0.0.1:${this.port}/public`;
57 | }
58 |
59 | static load() {
60 | const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
61 | if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
62 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
63 | return new ServiceConfig({ ...data, ...external });
64 | }
65 |
66 | }
67 |
68 | export default ServiceConfig.load();
--------------------------------------------------------------------------------
/olympics-api/src/lib/configs/system-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 |
9 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
10 |
11 | /**
12 | * 系统配置
13 | */
14 | export class SystemConfig {
15 |
16 | /** 是否开启请求日志 */
17 | requestLog: boolean;
18 | /** 临时目录路径 */
19 | tmpDir: string;
20 | /** 日志目录路径 */
21 | logDir: string;
22 | /** 日志写入间隔(毫秒) */
23 | logWriteInterval: number;
24 | /** 日志文件有效期(毫秒) */
25 | logFileExpires: number;
26 | /** 公共目录路径 */
27 | publicDir: string;
28 | /** 临时文件有效期(毫秒) */
29 | tmpFileExpires: number;
30 | /** 请求体配置 */
31 | requestBody: any;
32 | /** 是否调试模式 */
33 | debug: boolean;
34 |
35 | constructor(options?: any) {
36 | const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
37 | this.requestLog = _.defaultTo(requestLog, false);
38 | this.tmpDir = _.defaultTo(tmpDir, './tmp');
39 | this.logDir = _.defaultTo(logDir, './logs');
40 | this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
41 | this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
42 | this.publicDir = _.defaultTo(publicDir, './public');
43 | this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
44 | this.requestBody = Object.assign(requestBody || {}, {
45 | enableTypes: ['json', 'form', 'text', 'xml'],
46 | encoding: 'utf-8',
47 | formLimit: '100mb',
48 | jsonLimit: '100mb',
49 | textLimit: '100mb',
50 | xmlLimit: '100mb',
51 | formidable: {
52 | maxFileSize: '100mb'
53 | },
54 | multipart: true,
55 | parsedMethods: ['POST', 'PUT', 'PATCH']
56 | });
57 | this.debug = _.defaultTo(debug, true);
58 | }
59 |
60 | get rootDirPath() {
61 | return path.resolve();
62 | }
63 |
64 | get tmpDirPath() {
65 | return path.resolve(this.tmpDir);
66 | }
67 |
68 | get logDirPath() {
69 | return path.resolve(this.logDir);
70 | }
71 |
72 | get publicDirPath() {
73 | return path.resolve(this.publicDir);
74 | }
75 |
76 | static load() {
77 | if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
78 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
79 | return new SystemConfig(data);
80 | }
81 |
82 | }
83 |
84 | export default SystemConfig.load();
--------------------------------------------------------------------------------
/olympics-api/src/lib/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | SYSTEM_ERROR: [-1000, '系统异常'],
3 | SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
4 | SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
5 | } as Record
--------------------------------------------------------------------------------
/olympics-api/src/lib/environment.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import minimist from 'minimist';
5 | import _ from 'lodash';
6 |
7 | const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
8 | const envVars = process.env; //获取环境变量
9 |
10 | class Environment {
11 |
12 | /** 命令行参数 */
13 | cmdArgs: any;
14 | /** 环境变量 */
15 | envVars: any;
16 | /** 环境名称 */
17 | env?: string;
18 | /** 服务名称 */
19 | name?: string;
20 | /** 服务地址 */
21 | host?: string;
22 | /** 服务端口 */
23 | port?: number;
24 | /** 包参数 */
25 | package: any;
26 |
27 | constructor(options: any = {}) {
28 | const { cmdArgs, envVars, package: _package } = options;
29 | this.cmdArgs = cmdArgs;
30 | this.envVars = envVars;
31 | this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
32 | this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
33 | this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
34 | this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
35 | this.package = _package;
36 | }
37 |
38 | }
39 |
40 | export default new Environment({
41 | cmdArgs,
42 | envVars,
43 | package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
44 | });
--------------------------------------------------------------------------------
/olympics-api/src/lib/exceptions/APIException.ts:
--------------------------------------------------------------------------------
1 | import Exception from './Exception.js';
2 |
3 | export default class APIException extends Exception {
4 |
5 | /**
6 | * 构造异常
7 | *
8 | * @param {[number, string]} exception 异常
9 | */
10 | constructor(exception: (string | number)[], errmsg?: string) {
11 | super(exception, errmsg);
12 | }
13 |
14 | }
--------------------------------------------------------------------------------
/olympics-api/src/lib/exceptions/Exception.ts:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 |
3 | import _ from 'lodash';
4 |
5 | export default class Exception extends Error {
6 |
7 | /** 错误码 */
8 | errcode: number;
9 | /** 错误消息 */
10 | errmsg: string;
11 | /** 数据 */
12 | data: any;
13 | /** HTTP状态码 */
14 | httpStatusCode: number;
15 |
16 | /**
17 | * 构造异常
18 | *
19 | * @param exception 异常
20 | * @param _errmsg 异常消息
21 | */
22 | constructor(exception: (string | number)[], _errmsg?: string) {
23 | assert(_.isArray(exception), 'Exception must be Array');
24 | const [errcode, errmsg] = exception as [number, string];
25 | assert(_.isFinite(errcode), 'Exception errcode invalid');
26 | assert(_.isString(errmsg), 'Exception errmsg invalid');
27 | super(_errmsg || errmsg);
28 | this.errcode = errcode;
29 | this.errmsg = _errmsg || errmsg;
30 | }
31 |
32 | compare(exception: (string | number)[]) {
33 | const [errcode] = exception as [number, string];
34 | return this.errcode == errcode;
35 | }
36 |
37 | setHTTPStatusCode(value: number) {
38 | this.httpStatusCode = value;
39 | return this;
40 | }
41 |
42 | setData(value: any) {
43 | this.data = _.defaultTo(value, null);
44 | return this;
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/olympics-api/src/lib/initialize.ts:
--------------------------------------------------------------------------------
1 | import logger from './logger.js';
2 |
3 | // 允许无限量的监听器
4 | process.setMaxListeners(Infinity);
5 | // 输出未捕获异常
6 | process.on("uncaughtException", (err, origin) => {
7 | logger.error(`An unhandled error occurred: ${origin}`, err);
8 | });
9 | // 输出未处理的Promise.reject
10 | process.on("unhandledRejection", (_, promise) => {
11 | promise.catch(err => logger.error("An unhandled rejection occurred:", err));
12 | });
13 | // 输出系统警告信息
14 | process.on("warning", warning => logger.warn("System warning: ", warning));
15 | // 进程退出监听
16 | process.on("exit", () => {
17 | logger.info("Service exit");
18 | logger.footer();
19 | });
20 | // 进程被kill
21 | process.on("SIGTERM", () => {
22 | logger.warn("received kill signal");
23 | process.exit(2);
24 | });
25 | // Ctrl-C进程退出
26 | process.on("SIGINT", () => {
27 | process.exit(0);
28 | });
--------------------------------------------------------------------------------
/olympics-api/src/lib/interfaces/ICompletionMessage.ts:
--------------------------------------------------------------------------------
1 | export default interface ICompletionMessage {
2 | role: 'system' | 'assistant' | 'user' | 'function';
3 | content: string;
4 | }
--------------------------------------------------------------------------------
/olympics-api/src/lib/request/Request.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import APIException from '@/lib/exceptions/APIException.ts';
4 | import EX from '@/api/consts/exceptions.ts';
5 | import logger from '@/lib/logger.ts';
6 | import util from '@/lib/util.ts';
7 |
8 | export interface RequestOptions {
9 | time?: number;
10 | }
11 |
12 | export default class Request {
13 |
14 | /** 请求方法 */
15 | method: string;
16 | /** 请求URL */
17 | url: string;
18 | /** 请求路径 */
19 | path: string;
20 | /** 请求载荷类型 */
21 | type: string;
22 | /** 请求headers */
23 | headers: any;
24 | /** 请求原始查询字符串 */
25 | search: string;
26 | /** 请求查询参数 */
27 | query: any;
28 | /** 请求URL参数 */
29 | params: any;
30 | /** 请求载荷 */
31 | body: any;
32 | /** 上传的文件 */
33 | files: any[];
34 | /** 客户端IP地址 */
35 | remoteIP: string | null;
36 | /** 请求接受时间戳(毫秒) */
37 | time: number;
38 |
39 | constructor(ctx, options: RequestOptions = {}) {
40 | const { time } = options;
41 | this.method = ctx.request.method;
42 | this.url = ctx.request.url;
43 | this.path = ctx.request.path;
44 | this.type = ctx.request.type;
45 | this.headers = ctx.request.headers || {};
46 | this.search = ctx.request.search;
47 | this.query = ctx.query || {};
48 | this.params = ctx.params || {};
49 | this.body = ctx.request.body || {};
50 | this.files = ctx.request.files || {};
51 | this.remoteIP = this.headers["X-Real-IP"] || this.headers["x-real-ip"] || this.headers["X-Forwarded-For"] || this.headers["x-forwarded-for"] || ctx.ip || null;
52 | this.time = Number(_.defaultTo(time, util.timestamp()));
53 | }
54 |
55 | validate(key: string, fn?: Function) {
56 | try {
57 | const value = _.get(this, key);
58 | if (fn) {
59 | if (fn(value) === false)
60 | throw `[Mismatch] -> ${fn}`;
61 | }
62 | else if (_.isUndefined(value))
63 | throw '[Undefined]';
64 | }
65 | catch (err) {
66 | logger.warn(`Params ${key} invalid:`, err);
67 | throw new APIException(EX.API_REQUEST_PARAMS_INVALID, `Params ${key} invalid`);
68 | }
69 | return this;
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/olympics-api/src/lib/response/Body.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export interface BodyOptions {
4 | code?: number;
5 | message?: string;
6 | data?: any;
7 | statusCode?: number;
8 | }
9 |
10 | export default class Body {
11 |
12 | /** 状态码 */
13 | code: number;
14 | /** 状态消息 */
15 | message: string;
16 | /** 载荷 */
17 | data: any;
18 | /** HTTP状态码 */
19 | statusCode: number;
20 |
21 | constructor(options: BodyOptions = {}) {
22 | const { code, message, data, statusCode } = options;
23 | this.code = Number(_.defaultTo(code, 0));
24 | this.message = _.defaultTo(message, 'OK');
25 | this.data = _.defaultTo(data, null);
26 | this.statusCode = Number(_.defaultTo(statusCode, 200));
27 | }
28 |
29 | toObject() {
30 | return {
31 | code: this.code,
32 | message: this.message,
33 | data: this.data
34 | };
35 | }
36 |
37 | static isInstance(value) {
38 | return value instanceof Body;
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/olympics-api/src/lib/response/FailureBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 | import Exception from '../exceptions/Exception.ts';
5 | import APIException from '../exceptions/APIException.ts';
6 | import EX from '../consts/exceptions.ts';
7 | import HTTP_STATUS_CODES from '../http-status-codes.ts';
8 |
9 | export default class FailureBody extends Body {
10 |
11 | constructor(error: APIException | Exception | Error, _data?: any) {
12 | let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
13 | if(_.isString(error))
14 | error = new Exception(EX.SYSTEM_ERROR, error);
15 | else if(error instanceof APIException || error instanceof Exception)
16 | ({ errcode, errmsg, data, httpStatusCode } = error);
17 | else if(_.isError(error))
18 | ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
19 | super({
20 | code: errcode || -1,
21 | message: errmsg || 'Internal error',
22 | data,
23 | statusCode: httpStatusCode
24 | });
25 | }
26 |
27 | static isInstance(value) {
28 | return value instanceof FailureBody;
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/olympics-api/src/lib/response/Response.ts:
--------------------------------------------------------------------------------
1 | import mime from 'mime';
2 | import _ from 'lodash';
3 |
4 | import Body from './Body.ts';
5 | import util from '../util.ts';
6 |
7 | export interface ResponseOptions {
8 | statusCode?: number;
9 | type?: string;
10 | headers?: Record;
11 | redirect?: string;
12 | body?: any;
13 | size?: number;
14 | time?: number;
15 | }
16 |
17 | export default class Response {
18 |
19 | /** 响应HTTP状态码 */
20 | statusCode: number;
21 | /** 响应内容类型 */
22 | type: string;
23 | /** 响应headers */
24 | headers: Record;
25 | /** 重定向目标 */
26 | redirect: string;
27 | /** 响应载荷 */
28 | body: any;
29 | /** 响应载荷大小 */
30 | size: number;
31 | /** 响应时间戳 */
32 | time: number;
33 |
34 | constructor(body: any, options: ResponseOptions = {}) {
35 | const { statusCode, type, headers, redirect, size, time } = options;
36 | this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
37 | this.type = type;
38 | this.headers = headers;
39 | this.redirect = redirect;
40 | this.size = size;
41 | this.time = Number(_.defaultTo(time, util.timestamp()));
42 | this.body = body;
43 | }
44 |
45 | injectTo(ctx) {
46 | this.redirect && ctx.redirect(this.redirect);
47 | this.statusCode && (ctx.status = this.statusCode);
48 | this.type && (ctx.type = mime.getType(this.type) || this.type);
49 | const headers = this.headers || {};
50 | if(this.size && !headers["Content-Length"] && !headers["content-length"])
51 | headers["Content-Length"] = this.size;
52 | ctx.set(headers);
53 | if(Body.isInstance(this.body))
54 | ctx.body = this.body.toObject();
55 | else
56 | ctx.body = this.body;
57 | }
58 |
59 | static isInstance(value) {
60 | return value instanceof Response;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/olympics-api/src/lib/response/SuccessfulBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 |
5 | export default class SuccessfulBody extends Body {
6 |
7 | constructor(data: any, message?: string) {
8 | super({
9 | code: 0,
10 | message: _.defaultTo(message, "OK"),
11 | data
12 | });
13 | }
14 |
15 | static isInstance(value) {
16 | return value instanceof SuccessfulBody;
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/olympics-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "allowImportingTsExtensions": true,
7 | "allowSyntheticDefaultImports": true,
8 | "noEmit": true,
9 | "paths": {
10 | "@/*": ["src/*"]
11 | },
12 | "outDir": "./dist"
13 | },
14 | "include": ["src/**/*", "libs.d.ts"],
15 | "exclude": ["node_modules", "dist"]
16 | }
--------------------------------------------------------------------------------
/openai-compatible-api/.dockerignore:
--------------------------------------------------------------------------------
1 | logs
2 | dist
3 | doc
4 | node_modules
5 | .vscode
6 | .git
7 | .gitignore
8 | README.md
9 | *.tar.gz
--------------------------------------------------------------------------------
/openai-compatible-api/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push Docker Image
2 |
3 | on:
4 | release:
5 | types: [created]
6 | workflow_dispatch:
7 | inputs:
8 | tag:
9 | description: 'Tag Name'
10 | required: true
11 |
12 | jobs:
13 | build-and-push:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 |
18 | - name: Set up Docker Buildx
19 | uses: docker/setup-buildx-action@v1
20 |
21 | - name: Login to Docker Hub
22 | uses: docker/login-action@v1
23 | with:
24 | username: ${{ secrets.DOCKERHUB_USERNAME }}
25 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
26 |
27 | - name: Set tag name
28 | id: tag_name
29 | run: |
30 | if [ "${{ github.event_name }}" = "release" ]; then
31 | echo "::set-output name=tag::${GITHUB_REF#refs/tags/}"
32 | elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
33 | echo "::set-output name=tag::${{ github.event.inputs.tag }}"
34 | fi
35 |
36 | - name: Build and push Docker image with Release tag
37 | uses: docker/build-push-action@v2
38 | with:
39 | context: .
40 | file: ./Dockerfile
41 | push: true
42 | tags: |
43 | vinlic/zhipuai-agent-to-openai:${{ steps.tag_name.outputs.tag }}
44 | vinlic/zhipuai-agent-to-openai:latest
45 | platforms: linux/amd64,linux/arm64
46 | build-args: TARGETPLATFORM=${{ matrix.platform }}
47 |
--------------------------------------------------------------------------------
/openai-compatible-api/.github/workflows/sync.yml:
--------------------------------------------------------------------------------
1 | name: Upstream Sync
2 |
3 | permissions:
4 | contents: write
5 | issues: write
6 | actions: write
7 |
8 | on:
9 | schedule:
10 | - cron: '0 * * * *' # every hour
11 | workflow_dispatch:
12 |
13 | jobs:
14 | sync_latest_from_upstream:
15 | name: Sync latest commits from upstream repo
16 | runs-on: ubuntu-latest
17 | if: ${{ github.event.repository.fork }}
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 |
22 | - name: Clean issue notice
23 | uses: actions-cool/issues-helper@v3
24 | with:
25 | actions: 'close-issues'
26 | labels: '🚨 Sync Fail'
27 |
28 | - name: Sync upstream changes
29 | id: sync
30 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
31 | with:
32 | upstream_sync_repo: LLM-Red-Team/zhipuai-agent-to-openai
33 | upstream_sync_branch: master
34 | target_sync_branch: master
35 | target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
36 | test_mode: false
37 |
38 | - name: Sync check
39 | if: failure()
40 | uses: actions-cool/issues-helper@v3
41 | with:
42 | actions: 'create-issue'
43 | title: '🚨 同步失败 | Sync Fail'
44 | labels: '🚨 Sync Fail'
45 | body: |
46 | Due to a change in the workflow file of the LLM-Red-Team/zhipuai-agent-to-openai upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed [Tutorial][tutorial-en-US] for instructions.
47 |
48 | 由于 LLM-Red-Team/zhipuai-agent-to-openai 上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,
--------------------------------------------------------------------------------
/openai-compatible-api/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | logs/
4 | .vercel
5 |
--------------------------------------------------------------------------------
/openai-compatible-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts AS BUILD_IMAGE
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8 |
9 | FROM node:lts-alpine
10 |
11 | COPY --from=BUILD_IMAGE /app/configs /app/configs
12 | COPY --from=BUILD_IMAGE /app/package.json /app/package.json
13 | COPY --from=BUILD_IMAGE /app/dist /app/dist
14 | COPY --from=BUILD_IMAGE /app/public /app/public
15 | COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16 |
17 | WORKDIR /app
18 |
19 | EXPOSE 8000
20 |
21 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/openai-compatible-api/configs/dev/service.yml:
--------------------------------------------------------------------------------
1 | # 服务名称
2 | name: zhipuai-agent-to-openai
3 | # 服务绑定主机地址
4 | host: '0.0.0.0'
5 | # 服务绑定端口
6 | port: 8000
--------------------------------------------------------------------------------
/openai-compatible-api/configs/dev/system.yml:
--------------------------------------------------------------------------------
1 | # 是否开启请求日志
2 | requestLog: true
3 | # 临时目录路径
4 | tmpDir: ./tmp
5 | # 日志目录路径
6 | logDir: ./logs
7 | # 日志写入间隔(毫秒)
8 | logWriteInterval: 200
9 | # 日志文件有效期(毫秒)
10 | logFileExpires: 2626560000
11 | # 公共目录路径
12 | publicDir: ./public
13 | # 临时文件有效期(毫秒)
14 | tmpFileExpires: 86400000
--------------------------------------------------------------------------------
/openai-compatible-api/libs.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/openai-compatible-api/libs.d.ts
--------------------------------------------------------------------------------
/openai-compatible-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zhipuai-agent-to-openai",
3 | "version": "0.0.1",
4 | "description": "ZhipuAI Agent To OpenAI",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "types": "dist/index.d.ts",
9 | "directories": {
10 | "dist": "dist"
11 | },
12 | "files": [
13 | "dist/"
14 | ],
15 | "scripts": {
16 | "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node dist/index.js\"",
17 | "start": "node dist/index.js",
18 | "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
19 | },
20 | "author": "Vinlic",
21 | "license": "ISC",
22 | "dependencies": {
23 | "axios": "^1.6.7",
24 | "colors": "^1.4.0",
25 | "crc-32": "^1.2.2",
26 | "cron": "^3.1.6",
27 | "date-fns": "^3.3.1",
28 | "eventsource-parser": "^1.1.2",
29 | "form-data": "^4.0.0",
30 | "fs-extra": "^11.2.0",
31 | "koa": "^2.15.0",
32 | "koa-body": "^5.0.0",
33 | "koa-bodyparser": "^4.4.1",
34 | "koa-range": "^0.3.0",
35 | "koa-router": "^12.0.1",
36 | "koa2-cors": "^2.0.6",
37 | "lodash": "^4.17.21",
38 | "mime": "^4.0.1",
39 | "minimist": "^1.2.8",
40 | "randomstring": "^1.3.0",
41 | "uuid": "^9.0.1",
42 | "yaml": "^2.3.4"
43 | },
44 | "devDependencies": {
45 | "@types/lodash": "^4.14.202",
46 | "@types/mime": "^3.0.4",
47 | "tsup": "^8.0.2",
48 | "typescript": "^5.3.3"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/openai-compatible-api/public/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 🚀 服务已启动
6 |
7 |
8 | 网关已启动!
请通过支持OpenAI协议的客户端或OpenAI SDK接入!
9 |
10 |
--------------------------------------------------------------------------------
/openai-compatible-api/src/api/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | API_TEST: [-9999, 'API异常错误'],
3 | API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
4 | API_REQUEST_FAILED: [-2001, '请求失败'],
5 | API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
6 | API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
7 | API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
8 | API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'],
9 | API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'],
10 | API_IMAGE_GENERATION_FAILED: [-2007, '图像生成失败']
11 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/api/routes/chat.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Request from '@/lib/request/Request.ts';
4 | import Response from '@/lib/response/Response.ts';
5 | import chat from '@/api/controllers/chat.ts';
6 | import logger from '@/lib/logger.ts';
7 |
8 | export default {
9 |
10 | prefix: '/v1/chat',
11 |
12 | post: {
13 |
14 | '/completions': async (request: Request) => {
15 | request
16 | .validate('body.model', v => /^[a-z0-9]{24,}$/.test(v))
17 | .validate('body.conversation_id', v => _.isUndefined(v) || _.isString(v))
18 | .validate('body.messages', _.isArray)
19 | .validate('headers.authorization', _.isString)
20 | // refresh_token切分
21 | const apiKeys = chat.apiKeySplit(request.headers.authorization);
22 | // 随机挑选一个refresh_token
23 | const apiKey = _.sample(apiKeys);
24 | const { model, conversation_id: convId, messages, stream } = request.body;
25 | if (stream) {
26 | const stream = await chat.createCompletionStream(model, messages, apiKey, convId);
27 | return new Response(stream, {
28 | type: "text/event-stream"
29 | });
30 | }
31 | else
32 | return await chat.createCompletion(model, messages, apiKey, convId);
33 | }
34 |
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/api/routes/images.ts:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | import Request from "@/lib/request/Request.ts";
4 | import chat from "@/api/controllers/chat.ts";
5 | import util from "@/lib/util.ts";
6 |
7 | export default {
8 | prefix: "/v1/images",
9 |
10 | post: {
11 | "/generations": async (request: Request) => {
12 | request
13 | .validate("body.prompt", _.isString)
14 | .validate("headers.authorization", _.isString);
15 | // refresh_token切分
16 | const tokens = chat.apiKeySplit(request.headers.authorization);
17 | // 随机挑选一个refresh_token
18 | const token = _.sample(tokens);
19 | const prompt = request.body.prompt;
20 | const responseFormat = _.defaultTo(request.body.response_format, "url");
21 | const assistantId = /^[a-z0-9]{24,}$/.test(request.body.model) ? request.body.model : undefined
22 | const imageUrls = await chat.generateImages(assistantId, prompt, token);
23 | let data = [];
24 | if (responseFormat == "b64_json") {
25 | data = (
26 | await Promise.all(imageUrls.map((url) => util.fetchFileBASE64(url)))
27 | ).map((b64) => ({ b64_json: b64 }));
28 | } else {
29 | data = imageUrls.map((url) => ({
30 | url,
31 | }));
32 | }
33 | return {
34 | created: util.unixTimestamp(),
35 | data,
36 | };
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/openai-compatible-api/src/api/routes/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | import Response from '@/lib/response/Response.ts';
4 | import chat from "./chat.ts";
5 | import images from "./images.ts";
6 | import ping from "./ping.ts";
7 | import models from './models.ts';
8 |
9 | export default [
10 | {
11 | get: {
12 | '/': async () => {
13 | const content = await fs.readFile('public/welcome.html');
14 | return new Response(content, {
15 | type: 'html',
16 | headers: {
17 | Expires: '-1'
18 | }
19 | });
20 | }
21 | }
22 | },
23 | chat,
24 | images,
25 | ping,
26 | models
27 | ];
--------------------------------------------------------------------------------
/openai-compatible-api/src/api/routes/models.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export default {
4 |
5 | prefix: '/v1',
6 |
7 | get: {
8 | '/models': async () => {
9 | return {
10 | "data": [
11 | {
12 | "id": "glm-3-turbo",
13 | "object": "model",
14 | "owned_by": "zhipuai-agent-to-openai"
15 | },
16 | {
17 | "id": "glm-4",
18 | "object": "model",
19 | "owned_by": "zhipuai-agent-to-openai"
20 | },
21 | {
22 | "id": "glm-4v",
23 | "object": "model",
24 | "owned_by": "zhipuai-agent-to-openai"
25 | },
26 | {
27 | "id": "glm-v1",
28 | "object": "model",
29 | "owned_by": "zhipuai-agent-to-openai"
30 | },
31 | {
32 | "id": "glm-v1-vision",
33 | "object": "model",
34 | "owned_by": "zhipuai-agent-to-openai"
35 | }
36 | ]
37 | };
38 | }
39 |
40 | }
41 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/api/routes/ping.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | prefix: '/ping',
3 | get: {
4 | '': async () => "pong"
5 | }
6 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/index.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import environment from "@/lib/environment.ts";
4 | import config from "@/lib/config.ts";
5 | import "@/lib/initialize.ts";
6 | import server from "@/lib/server.ts";
7 | import routes from "@/api/routes/index.ts";
8 | import logger from "@/lib/logger.ts";
9 |
10 | const startupTime = performance.now();
11 |
12 | (async () => {
13 | logger.header();
14 |
15 | logger.info("<<<< glm free server >>>>");
16 | logger.info("Version:", environment.package.version);
17 | logger.info("Process id:", process.pid);
18 | logger.info("Environment:", environment.env);
19 | logger.info("Service name:", config.service.name);
20 |
21 | server.attachRoutes(routes);
22 | await server.listen();
23 |
24 | config.service.bindAddress &&
25 | logger.success("Service bind address:", config.service.bindAddress);
26 | })()
27 | .then(() =>
28 | logger.success(
29 | `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
30 | )
31 | )
32 | .catch((err) => console.error(err));
33 |
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | import serviceConfig from "./configs/service-config.ts";
2 | import systemConfig from "./configs/system-config.ts";
3 |
4 | class Config {
5 |
6 | /** 服务配置 */
7 | service = serviceConfig;
8 |
9 | /** 系统配置 */
10 | system = systemConfig;
11 |
12 | }
13 |
14 | export default new Config();
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/configs/service-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 | import util from '../util.ts';
9 |
10 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
11 |
12 | /**
13 | * 服务配置
14 | */
15 | export class ServiceConfig {
16 |
17 | /** 服务名称 */
18 | name: string;
19 | /** @type {string} 服务绑定主机地址 */
20 | host;
21 | /** @type {number} 服务绑定端口 */
22 | port;
23 | /** @type {string} 服务路由前缀 */
24 | urlPrefix;
25 | /** @type {string} 服务绑定地址(外部访问地址) */
26 | bindAddress;
27 |
28 | constructor(options?: any) {
29 | const { name, host, port, urlPrefix, bindAddress } = options || {};
30 | this.name = _.defaultTo(name, 'zhipuai-agent-to-openai');
31 | this.host = _.defaultTo(host, '0.0.0.0');
32 | this.port = _.defaultTo(port, 5566);
33 | this.urlPrefix = _.defaultTo(urlPrefix, '');
34 | this.bindAddress = bindAddress;
35 | }
36 |
37 | get addressHost() {
38 | if(this.bindAddress) return this.bindAddress;
39 | const ipAddresses = util.getIPAddressesByIPv4();
40 | for(let ipAddress of ipAddresses) {
41 | if(ipAddress === this.host)
42 | return ipAddress;
43 | }
44 | return ipAddresses[0] || "127.0.0.1";
45 | }
46 |
47 | get address() {
48 | return `${this.addressHost}:${this.port}`;
49 | }
50 |
51 | get pageDirUrl() {
52 | return `http://127.0.0.1:${this.port}/page`;
53 | }
54 |
55 | get publicDirUrl() {
56 | return `http://127.0.0.1:${this.port}/public`;
57 | }
58 |
59 | static load() {
60 | const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
61 | if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
62 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
63 | return new ServiceConfig({ ...data, ...external });
64 | }
65 |
66 | }
67 |
68 | export default ServiceConfig.load();
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/configs/system-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 |
9 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
10 |
11 | /**
12 | * 系统配置
13 | */
14 | export class SystemConfig {
15 |
16 | /** 是否开启请求日志 */
17 | requestLog: boolean;
18 | /** 临时目录路径 */
19 | tmpDir: string;
20 | /** 日志目录路径 */
21 | logDir: string;
22 | /** 日志写入间隔(毫秒) */
23 | logWriteInterval: number;
24 | /** 日志文件有效期(毫秒) */
25 | logFileExpires: number;
26 | /** 公共目录路径 */
27 | publicDir: string;
28 | /** 临时文件有效期(毫秒) */
29 | tmpFileExpires: number;
30 | /** 请求体配置 */
31 | requestBody: any;
32 | /** 是否调试模式 */
33 | debug: boolean;
34 |
35 | constructor(options?: any) {
36 | const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
37 | this.requestLog = _.defaultTo(requestLog, false);
38 | this.tmpDir = _.defaultTo(tmpDir, './tmp');
39 | this.logDir = _.defaultTo(logDir, './logs');
40 | this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
41 | this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
42 | this.publicDir = _.defaultTo(publicDir, './public');
43 | this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
44 | this.requestBody = Object.assign(requestBody || {}, {
45 | enableTypes: ['json', 'form', 'text', 'xml'],
46 | encoding: 'utf-8',
47 | formLimit: '100mb',
48 | jsonLimit: '100mb',
49 | textLimit: '100mb',
50 | xmlLimit: '100mb',
51 | formidable: {
52 | maxFileSize: '100mb'
53 | },
54 | multipart: true,
55 | parsedMethods: ['POST', 'PUT', 'PATCH']
56 | });
57 | this.debug = _.defaultTo(debug, true);
58 | }
59 |
60 | get rootDirPath() {
61 | return path.resolve();
62 | }
63 |
64 | get tmpDirPath() {
65 | return path.resolve(this.tmpDir);
66 | }
67 |
68 | get logDirPath() {
69 | return path.resolve(this.logDir);
70 | }
71 |
72 | get publicDirPath() {
73 | return path.resolve(this.publicDir);
74 | }
75 |
76 | static load() {
77 | if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
78 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
79 | return new SystemConfig(data);
80 | }
81 |
82 | }
83 |
84 | export default SystemConfig.load();
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | SYSTEM_ERROR: [-1000, '系统异常'],
3 | SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
4 | SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
5 | } as Record
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/environment.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import minimist from 'minimist';
5 | import _ from 'lodash';
6 |
7 | const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
8 | const envVars = process.env; //获取环境变量
9 |
10 | class Environment {
11 |
12 | /** 命令行参数 */
13 | cmdArgs: any;
14 | /** 环境变量 */
15 | envVars: any;
16 | /** 环境名称 */
17 | env?: string;
18 | /** 服务名称 */
19 | name?: string;
20 | /** 服务地址 */
21 | host?: string;
22 | /** 服务端口 */
23 | port?: number;
24 | /** 包参数 */
25 | package: any;
26 |
27 | constructor(options: any = {}) {
28 | const { cmdArgs, envVars, package: _package } = options;
29 | this.cmdArgs = cmdArgs;
30 | this.envVars = envVars;
31 | this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
32 | this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
33 | this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
34 | this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
35 | this.package = _package;
36 | }
37 |
38 | }
39 |
40 | export default new Environment({
41 | cmdArgs,
42 | envVars,
43 | package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
44 | });
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/exceptions/APIException.ts:
--------------------------------------------------------------------------------
1 | import Exception from './Exception.js';
2 |
3 | export default class APIException extends Exception {
4 |
5 | /**
6 | * 构造异常
7 | *
8 | * @param {[number, string]} exception 异常
9 | */
10 | constructor(exception: (string | number)[], errmsg?: string) {
11 | super(exception, errmsg);
12 | }
13 |
14 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/exceptions/Exception.ts:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 |
3 | import _ from 'lodash';
4 |
5 | export default class Exception extends Error {
6 |
7 | /** 错误码 */
8 | errcode: number;
9 | /** 错误消息 */
10 | errmsg: string;
11 | /** 数据 */
12 | data: any;
13 | /** HTTP状态码 */
14 | httpStatusCode: number;
15 |
16 | /**
17 | * 构造异常
18 | *
19 | * @param exception 异常
20 | * @param _errmsg 异常消息
21 | */
22 | constructor(exception: (string | number)[], _errmsg?: string) {
23 | assert(_.isArray(exception), 'Exception must be Array');
24 | const [errcode, errmsg] = exception as [number, string];
25 | assert(_.isFinite(errcode), 'Exception errcode invalid');
26 | assert(_.isString(errmsg), 'Exception errmsg invalid');
27 | super(_errmsg || errmsg);
28 | this.errcode = errcode;
29 | this.errmsg = _errmsg || errmsg;
30 | }
31 |
32 | compare(exception: (string | number)[]) {
33 | const [errcode] = exception as [number, string];
34 | return this.errcode == errcode;
35 | }
36 |
37 | setHTTPStatusCode(value: number) {
38 | this.httpStatusCode = value;
39 | return this;
40 | }
41 |
42 | setData(value: any) {
43 | this.data = _.defaultTo(value, null);
44 | return this;
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/initialize.ts:
--------------------------------------------------------------------------------
1 | import logger from './logger.js';
2 |
3 | // 允许无限量的监听器
4 | process.setMaxListeners(Infinity);
5 | // 输出未捕获异常
6 | process.on("uncaughtException", (err, origin) => {
7 | logger.error(`An unhandled error occurred: ${origin}`, err);
8 | });
9 | // 输出未处理的Promise.reject
10 | process.on("unhandledRejection", (_, promise) => {
11 | promise.catch(err => logger.error("An unhandled rejection occurred:", err));
12 | });
13 | // 输出系统警告信息
14 | process.on("warning", warning => logger.warn("System warning: ", warning));
15 | // 进程退出监听
16 | process.on("exit", () => {
17 | logger.info("Service exit");
18 | logger.footer();
19 | });
20 | // 进程被kill
21 | process.on("SIGTERM", () => {
22 | logger.warn("received kill signal");
23 | process.exit(2);
24 | });
25 | // Ctrl-C进程退出
26 | process.on("SIGINT", () => {
27 | process.exit(0);
28 | });
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/interfaces/ICompletionMessage.ts:
--------------------------------------------------------------------------------
1 | export default interface ICompletionMessage {
2 | role: 'system' | 'assistant' | 'user' | 'function';
3 | content: string;
4 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/request/Request.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import APIException from '@/lib/exceptions/APIException.ts';
4 | import EX from '@/api/consts/exceptions.ts';
5 | import logger from '@/lib/logger.ts';
6 | import util from '@/lib/util.ts';
7 |
8 | export interface RequestOptions {
9 | time?: number;
10 | }
11 |
12 | export default class Request {
13 |
14 | /** 请求方法 */
15 | method: string;
16 | /** 请求URL */
17 | url: string;
18 | /** 请求路径 */
19 | path: string;
20 | /** 请求载荷类型 */
21 | type: string;
22 | /** 请求headers */
23 | headers: any;
24 | /** 请求原始查询字符串 */
25 | search: string;
26 | /** 请求查询参数 */
27 | query: any;
28 | /** 请求URL参数 */
29 | params: any;
30 | /** 请求载荷 */
31 | body: any;
32 | /** 上传的文件 */
33 | files: any[];
34 | /** 客户端IP地址 */
35 | remoteIP: string | null;
36 | /** 请求接受时间戳(毫秒) */
37 | time: number;
38 |
39 | constructor(ctx, options: RequestOptions = {}) {
40 | const { time } = options;
41 | this.method = ctx.request.method;
42 | this.url = ctx.request.url;
43 | this.path = ctx.request.path;
44 | this.type = ctx.request.type;
45 | this.headers = ctx.request.headers || {};
46 | this.search = ctx.request.search;
47 | this.query = ctx.query || {};
48 | this.params = ctx.params || {};
49 | this.body = ctx.request.body || {};
50 | this.files = ctx.request.files || {};
51 | this.remoteIP = this.headers["X-Real-IP"] || this.headers["x-real-ip"] || this.headers["X-Forwarded-For"] || this.headers["x-forwarded-for"] || ctx.ip || null;
52 | this.time = Number(_.defaultTo(time, util.timestamp()));
53 | }
54 |
55 | validate(key: string, fn?: Function) {
56 | try {
57 | const value = _.get(this, key);
58 | if (fn) {
59 | if (fn(value) === false)
60 | throw `[Mismatch] -> ${fn}`;
61 | }
62 | else if (_.isUndefined(value))
63 | throw '[Undefined]';
64 | }
65 | catch (err) {
66 | logger.warn(`Params ${key} invalid:`, err);
67 | throw new APIException(EX.API_REQUEST_PARAMS_INVALID, `Params ${key} invalid`);
68 | }
69 | return this;
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/response/Body.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export interface BodyOptions {
4 | code?: number;
5 | message?: string;
6 | data?: any;
7 | statusCode?: number;
8 | }
9 |
10 | export default class Body {
11 |
12 | /** 状态码 */
13 | code: number;
14 | /** 状态消息 */
15 | message: string;
16 | /** 载荷 */
17 | data: any;
18 | /** HTTP状态码 */
19 | statusCode: number;
20 |
21 | constructor(options: BodyOptions = {}) {
22 | const { code, message, data, statusCode } = options;
23 | this.code = Number(_.defaultTo(code, 0));
24 | this.message = _.defaultTo(message, 'OK');
25 | this.data = _.defaultTo(data, null);
26 | this.statusCode = Number(_.defaultTo(statusCode, 200));
27 | }
28 |
29 | toObject() {
30 | return {
31 | code: this.code,
32 | message: this.message,
33 | data: this.data
34 | };
35 | }
36 |
37 | static isInstance(value) {
38 | return value instanceof Body;
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/response/FailureBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 | import Exception from '../exceptions/Exception.ts';
5 | import APIException from '../exceptions/APIException.ts';
6 | import EX from '../consts/exceptions.ts';
7 | import HTTP_STATUS_CODES from '../http-status-codes.ts';
8 |
9 | export default class FailureBody extends Body {
10 |
11 | constructor(error: APIException | Exception | Error, _data?: any) {
12 | let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
13 | if(_.isString(error))
14 | error = new Exception(EX.SYSTEM_ERROR, error);
15 | else if(error instanceof APIException || error instanceof Exception)
16 | ({ errcode, errmsg, data, httpStatusCode } = error);
17 | else if(_.isError(error))
18 | ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
19 | super({
20 | code: errcode || -1,
21 | message: errmsg || 'Internal error',
22 | data,
23 | statusCode: httpStatusCode
24 | });
25 | }
26 |
27 | static isInstance(value) {
28 | return value instanceof FailureBody;
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/response/Response.ts:
--------------------------------------------------------------------------------
1 | import mime from 'mime';
2 | import _ from 'lodash';
3 |
4 | import Body from './Body.ts';
5 | import util from '../util.ts';
6 |
7 | export interface ResponseOptions {
8 | statusCode?: number;
9 | type?: string;
10 | headers?: Record;
11 | redirect?: string;
12 | body?: any;
13 | size?: number;
14 | time?: number;
15 | }
16 |
17 | export default class Response {
18 |
19 | /** 响应HTTP状态码 */
20 | statusCode: number;
21 | /** 响应内容类型 */
22 | type: string;
23 | /** 响应headers */
24 | headers: Record;
25 | /** 重定向目标 */
26 | redirect: string;
27 | /** 响应载荷 */
28 | body: any;
29 | /** 响应载荷大小 */
30 | size: number;
31 | /** 响应时间戳 */
32 | time: number;
33 |
34 | constructor(body: any, options: ResponseOptions = {}) {
35 | const { statusCode, type, headers, redirect, size, time } = options;
36 | this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
37 | this.type = type;
38 | this.headers = headers;
39 | this.redirect = redirect;
40 | this.size = size;
41 | this.time = Number(_.defaultTo(time, util.timestamp()));
42 | this.body = body;
43 | }
44 |
45 | injectTo(ctx) {
46 | this.redirect && ctx.redirect(this.redirect);
47 | this.statusCode && (ctx.status = this.statusCode);
48 | this.type && (ctx.type = mime.getType(this.type) || this.type);
49 | const headers = this.headers || {};
50 | if(this.size && !headers["Content-Length"] && !headers["content-length"])
51 | headers["Content-Length"] = this.size;
52 | ctx.set(headers);
53 | if(Body.isInstance(this.body))
54 | ctx.body = this.body.toObject();
55 | else
56 | ctx.body = this.body;
57 | }
58 |
59 | static isInstance(value) {
60 | return value instanceof Response;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/openai-compatible-api/src/lib/response/SuccessfulBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 |
5 | export default class SuccessfulBody extends Body {
6 |
7 | constructor(data: any, message?: string) {
8 | super({
9 | code: 0,
10 | message: _.defaultTo(message, "OK"),
11 | data
12 | });
13 | }
14 |
15 | static isInstance(value) {
16 | return value instanceof SuccessfulBody;
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/openai-compatible-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "allowImportingTsExtensions": true,
7 | "allowSyntheticDefaultImports": true,
8 | "noEmit": true,
9 | "paths": {
10 | "@/*": ["src/*"]
11 | },
12 | "outDir": "./dist"
13 | },
14 | "include": ["src/**/*", "libs.d.ts"],
15 | "exclude": ["node_modules", "dist"]
16 | }
--------------------------------------------------------------------------------
/openai-compatible-api/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "builds": [
3 | {
4 | "src": "./dist/*.html",
5 | "use": "@vercel/static"
6 | },
7 | {
8 | "src": "./dist/index.js",
9 | "use": "@vercel/node"
10 | }
11 | ],
12 | "routes": [
13 | {
14 | "src": "/",
15 | "dest": "/dist/welcome.html"
16 | },
17 | {
18 | "src": "/(.*)",
19 | "dest": "/dist",
20 | "headers": {
21 | "Access-Control-Allow-Credentials": "true",
22 | "Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT",
23 | "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, Authorization"
24 | }
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/werewolf-api/.dockerignore:
--------------------------------------------------------------------------------
1 | logs
2 | dist
3 | doc
4 | node_modules
5 | .vscode
6 | .git
7 | .gitignore
8 | README.md
9 | *.tar.gz
--------------------------------------------------------------------------------
/werewolf-api/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | logs/
4 | .vercel
5 |
--------------------------------------------------------------------------------
/werewolf-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts AS BUILD_IMAGE
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8 |
9 | FROM node:lts-alpine
10 |
11 | COPY --from=BUILD_IMAGE /app/configs /app/configs
12 | COPY --from=BUILD_IMAGE /app/package.json /app/package.json
13 | COPY --from=BUILD_IMAGE /app/dist /app/dist
14 | COPY --from=BUILD_IMAGE /app/public /app/public
15 | COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16 |
17 | WORKDIR /app
18 |
19 | EXPOSE 8000
20 |
21 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/werewolf-api/README.md:
--------------------------------------------------------------------------------
1 | # 新手狼人杀 API服务
2 |
3 | 此仓库是用于[新手狼人杀(开发中)](https://chatglm.cn/main/gdetail/66767eb0261596f253d11c9f)智能体的API服务,通过外置共享上下文+智能体API调用的方式实现多智能体交互能力。
4 |
5 | ## 安装
6 |
7 | 请先安装Node.js 20+。
8 |
9 | ```shell
10 | # 全局安装Yarn
11 | npm i -g yarn --registry https://registry.npmmirror.com/
12 | # 安装依赖
13 | yarn install
14 | # 构建dist
15 | npm run build
16 | ```
17 |
18 | # 启动
19 |
20 | ```shell
21 | # 启动服务
22 | npm run start
23 | ```
24 |
25 | # 后台运行
26 |
27 | 请先安装PM2。
28 |
29 | ```shell
30 | yarn global add pm2
31 | ```
32 |
33 | 使用PM2启动服务并守护进程
34 |
35 | ```shell
36 | pm2 start dist/index.js --name "werewolf-api"
37 | ```
--------------------------------------------------------------------------------
/werewolf-api/configs/dev/service.yml:
--------------------------------------------------------------------------------
1 | # 服务名称
2 | name: werewolf-api
3 | # 服务绑定主机地址
4 | host: '0.0.0.0'
5 | # 服务绑定端口
6 | port: 8000
--------------------------------------------------------------------------------
/werewolf-api/configs/dev/system.yml:
--------------------------------------------------------------------------------
1 | # 是否开启请求日志
2 | requestLog: true
3 | # 临时目录路径
4 | tmpDir: ./tmp
5 | # 日志目录路径
6 | logDir: ./logs
7 | # 日志写入间隔(毫秒)
8 | logWriteInterval: 200
9 | # 日志文件有效期(毫秒)
10 | logFileExpires: 2626560000
11 | # 公共目录路径
12 | publicDir: ./public
13 | # 临时文件有效期(毫秒)
14 | tmpFileExpires: 86400000
--------------------------------------------------------------------------------
/werewolf-api/libs.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/werewolf-api/libs.d.ts
--------------------------------------------------------------------------------
/werewolf-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "werewolf-api",
3 | "version": "0.0.1",
4 | "description": "ZhipuAI Agent To OpenAI",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "types": "dist/index.d.ts",
9 | "directories": {
10 | "dist": "dist"
11 | },
12 | "files": [
13 | "dist/"
14 | ],
15 | "scripts": {
16 | "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node dist/index.js\"",
17 | "start": "node dist/index.js",
18 | "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
19 | },
20 | "author": "Vinlic",
21 | "license": "ISC",
22 | "dependencies": {
23 | "axios": "^1.6.7",
24 | "colors": "^1.4.0",
25 | "crc-32": "^1.2.2",
26 | "cron": "^3.1.6",
27 | "date-fns": "^3.3.1",
28 | "eventsource-parser": "^1.1.2",
29 | "form-data": "^4.0.0",
30 | "fs-extra": "^11.2.0",
31 | "koa": "^2.15.0",
32 | "koa-body": "^5.0.0",
33 | "koa-bodyparser": "^4.4.1",
34 | "koa-range": "^0.3.0",
35 | "koa-router": "^12.0.1",
36 | "koa2-cors": "^2.0.6",
37 | "lodash": "^4.17.21",
38 | "mime": "^4.0.1",
39 | "minimist": "^1.2.8",
40 | "randomstring": "^1.3.0",
41 | "uuid": "^9.0.1",
42 | "yaml": "^2.3.4"
43 | },
44 | "devDependencies": {
45 | "@types/lodash": "^4.14.202",
46 | "@types/mime": "^3.0.4",
47 | "tsup": "^8.0.2",
48 | "typescript": "^5.3.3"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/werewolf-api/public/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 🚀 服务已启动
6 |
7 |
8 | 网关已启动!
请通过支持OpenAI协议的客户端或OpenAI SDK接入!
9 |
10 |
--------------------------------------------------------------------------------
/werewolf-api/src/api/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | API_TEST: [-9999, 'API异常错误'],
3 | API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
4 | API_REQUEST_FAILED: [-2001, '请求失败'],
5 | API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
6 | API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
7 | API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
8 | API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'],
9 | API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'],
10 | API_IMAGE_GENERATION_FAILED: [-2007, '图像生成失败']
11 | }
--------------------------------------------------------------------------------
/werewolf-api/src/api/consts/nickname.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | "荒野猎人",
3 | "银月守护",
4 | "灵魂吟游者",
5 | "寒风追踪者",
6 | "暗夜魅影",
7 | "星辰旅者",
8 | "神秘森林",
9 | "魔法先知",
10 | "狼群领袖",
11 | "隐秘刺客",
12 | "铁血哨兵",
13 | "幽冥使者",
14 | "深渊观察者",
15 | "风语者",
16 | "神秘舞者",
17 | "雷霆骑士",
18 | "灵魂收割者",
19 | "梦境编织者",
20 | "雪域独行者",
21 | "时空旅者",
22 | "神秘塔罗师",
23 | "诡计大师",
24 | "暗影之刃",
25 | "深海探险家",
26 | "火焰舞者",
27 | "狼人杀星",
28 | "寒冰射手",
29 | "丛林游侠",
30 | "风暴使者",
31 | "神秘女巫"
32 | ]
--------------------------------------------------------------------------------
/werewolf-api/src/api/consts/role-rules.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | '平民': '平民属于好人阵营,在游戏中没有特殊能力,只能通过白天讨论和夜晚投票来揭露狼人,平民玩家之间可以分享信息和观点,但需要注意不要泄露过多个人信息,以免被狼人利用,好人阵营通过投票或特殊角色能力成功消灭所有狼人,好人阵营获胜。',
3 | '预言家': '预言家属于好人阵营,在游戏中在狼人杀害一名玩家后,预言家可在夜晚查验一名存活的玩家进行身份查验,查验后将等待合适的时机透露信息,预言家在白天投票阶段有平等的投票权,应该根据自己的判断和对其他玩家发言的分析,投出自己的一票。',
4 | '狼人': '狼人属于坏人阵营,必须时刻隐藏自己是狼人的身份,在游戏中狼人可以在夜晚时选择杀害一名玩家,而在白天讨论阶段需要伪装成好人阵营(平民或预言家),避免被其他玩家怀疑或投票出具,应该积极参与讨论,提出合理的推理和怀疑,以混淆视听。'
5 | }
--------------------------------------------------------------------------------
/werewolf-api/src/api/controllers/game.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import nickname from '../consts/nickname.ts';
4 | import IRoom from '../interfaces/IRoom.ts';
5 | import IPlayer from '../interfaces/IPlayer.ts';
6 |
7 | const rooms = {};
8 |
9 | let gameId = 1;
10 |
11 | function createRoom(count: number) {
12 | const werewolfCount = Math.max(Math.floor(count * 0.25), 1);
13 | const prophetCount = Math.max(Math.floor(count * 0.2), 1);
14 | const civilianCount = count - (werewolfCount + prophetCount);
15 | let players: IPlayer[] = []
16 | // 分配狼人
17 | for(let i = 0;i < werewolfCount;i++) {
18 | players.push({
19 | role: '狼人',
20 | name: '',
21 | live: true,
22 | talks: [],
23 | votes: [],
24 | driver: 'Agent'
25 | });
26 | }
27 | // 分配预言家
28 | for(let i = 0;i < prophetCount;i++) {
29 | players.push({
30 | role: '预言家',
31 | name: '',
32 | live: true,
33 | talks: [],
34 | votes: [],
35 | driver: 'Agent'
36 | });
37 | }
38 | // 分配平民
39 | for(let i = 0;i < civilianCount;i++) {
40 | players.push({
41 | role: '平民',
42 | name: '',
43 | live: true,
44 | talks: [],
45 | votes: [],
46 | driver: 'Agent'
47 | });
48 | }
49 | // 打乱的玩家列表
50 | players = _.shuffle(players);
51 | // 打乱昵称列表
52 | const _nickname = _.shuffle(nickname);
53 | // 分配玩家名字
54 | players = players.map((player, index) => {
55 | player.name = _nickname[index];
56 | return player;
57 | });
58 | // 选取一个玩家作为用户
59 | const userPlayer = _.sample(players);
60 | userPlayer.driver = 'User';
61 | // 房间
62 | const room: IRoom = {
63 | // 房间ID
64 | id: generateId(),
65 | // 玩家列表
66 | players,
67 | // 当前轮次
68 | round: 1,
69 | // 当前时间
70 | time: '白天',
71 | };
72 | rooms[room.id] = room;
73 | return room;
74 | }
75 |
76 | function getCivilianPlayers(room: IRoom) {
77 | return room.players.filter(player => player.role == '平民' && player.live);
78 | }
79 |
80 | function getWerewolfPlayers(room: IRoom) {
81 | return room.players.filter(player => player.role == '狼人' && player.live);
82 | }
83 |
84 | function getProphetPlayers(room: IRoom) {
85 | return room.players.filter(player => player.role == '预言家' && player.live);
86 | }
87 |
88 | function getLivePlayers(room: IRoom) {
89 | return room.players.filter(player => player.live);
90 | }
91 |
92 | function getLivePlayersExcludeWerewolf(room: IRoom) {
93 | return room.players.filter(player => player.role != '狼人' && player.live);
94 | }
95 |
96 | function getCurrentRoundNotTalkPlayers(room: IRoom) {
97 | const players = getLivePlayers(room);
98 | const talkIndex = room.round - 1;
99 | const notTalkPlayers = players.filter(player => !player.talks[talkIndex]);
100 | return notTalkPlayers;
101 | }
102 |
103 | function getCurrentRoundNotVotePlayers(room: IRoom) {
104 | const players = getLivePlayers(room);
105 | const voteIndex = room.round - 1;
106 | const notVotePlayers = players.filter(player => !player.votes[voteIndex]);
107 | return notVotePlayers;
108 | }
109 |
110 | function getUserPlayer(room: IRoom) {
111 | return room.players.filter(player => player.driver == 'User')[0] || null;
112 | }
113 |
114 | function getPlayerByNickname(room: IRoom, nickname: string) {
115 | return room.players.filter(player => player.name == nickname)[0] || null;
116 | }
117 |
118 | function getRoom(roomId: string): IRoom {
119 | return rooms[roomId];
120 | }
121 |
122 | function getContext(room: IRoom) {
123 | const players = room.players;
124 | const talks = [];
125 | const votes = [];
126 | for(let i = 0;i < room.round;i++) {
127 | for(let player of players) {
128 | if(player.talks[i])
129 | talks.push(`${player.name}:${player.talks[i]}。`);
130 | if(player.votes[i])
131 | votes.push(`${player.name}:投票了${player.votes[i]}`);
132 | }
133 | }
134 | return `${talks.join('\n')}\n\n${votes.join('\n')}`
135 | }
136 |
137 | function generateId() {
138 | return padWithZeros(gameId++);
139 | }
140 |
141 | function padWithZeros(number: number) {
142 | let str = number.toString();
143 | while (str.length < 6) {
144 | str = "0" + str;
145 | }
146 | return str;
147 | }
148 |
149 | function switchDayNight(room: IRoom) {
150 | if(room.time == '白天')
151 | room.time = '夜晚';
152 | else {
153 | room.round++;
154 | room.time = '白天';
155 | }
156 | }
157 |
158 | export default {
159 | createRoom,
160 | getRoom,
161 | getUserPlayer,
162 | getLivePlayers,
163 | getCivilianPlayers,
164 | getWerewolfPlayers,
165 | getProphetPlayers,
166 | getPlayerByNickname,
167 | getLivePlayersExcludeWerewolf,
168 | getCurrentRoundNotTalkPlayers,
169 | getCurrentRoundNotVotePlayers,
170 | switchDayNight,
171 | getContext
172 | }
--------------------------------------------------------------------------------
/werewolf-api/src/api/interfaces/IPlayer.ts:
--------------------------------------------------------------------------------
1 | export default interface IPlayer {
2 | // 角色
3 | role: '狼人' | '平民' | '预言家' | '巫女' | '守卫';
4 | // 用户名
5 | name: string;
6 | // 是否存活
7 | live: boolean;
8 | // 是否被查验身份
9 | checked?: boolean;
10 | // 被谁杀了
11 | killer?: string;
12 | // 发言列表
13 | talks: string[];
14 | // 投票列表
15 | votes: string[];
16 | // 操纵者
17 | driver: 'User' | 'Agent';
18 | }
--------------------------------------------------------------------------------
/werewolf-api/src/api/interfaces/IRoom.ts:
--------------------------------------------------------------------------------
1 | import IPlayer from "./IPlayer.ts";
2 |
3 | export default interface IRoom {
4 | // 房间ID
5 | id: string;
6 | // 当前是第几轮
7 | round: number;
8 | // 当前时间
9 | time: '白天' | '夜晚',
10 | // 玩家列表
11 | players: IPlayer[];
12 | }
--------------------------------------------------------------------------------
/werewolf-api/src/api/routes/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | import Response from '@/lib/response/Response.ts';
4 | import game from './game.ts';
5 |
6 | export default [
7 | {
8 | get: {
9 | '/': async () => {
10 | const content = await fs.readFile('public/welcome.html');
11 | return new Response(content, {
12 | type: 'html',
13 | headers: {
14 | Expires: '-1'
15 | }
16 | });
17 | }
18 | }
19 | },
20 | game
21 | ];
--------------------------------------------------------------------------------
/werewolf-api/src/index.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import environment from "@/lib/environment.ts";
4 | import config from "@/lib/config.ts";
5 | import "@/lib/initialize.ts";
6 | import server from "@/lib/server.ts";
7 | import routes from "@/api/routes/index.ts";
8 | import logger from "@/lib/logger.ts";
9 |
10 | const startupTime = performance.now();
11 |
12 | (async () => {
13 | logger.header();
14 |
15 | logger.info("<<<< glm free server >>>>");
16 | logger.info("Version:", environment.package.version);
17 | logger.info("Process id:", process.pid);
18 | logger.info("Environment:", environment.env);
19 | logger.info("Service name:", config.service.name);
20 |
21 | server.attachRoutes(routes);
22 | await server.listen();
23 |
24 | config.service.bindAddress &&
25 | logger.success("Service bind address:", config.service.bindAddress);
26 | })()
27 | .then(() =>
28 | logger.success(
29 | `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
30 | )
31 | )
32 | .catch((err) => console.error(err));
33 |
--------------------------------------------------------------------------------
/werewolf-api/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | import serviceConfig from "./configs/service-config.ts";
2 | import systemConfig from "./configs/system-config.ts";
3 |
4 | class Config {
5 |
6 | /** 服务配置 */
7 | service = serviceConfig;
8 |
9 | /** 系统配置 */
10 | system = systemConfig;
11 |
12 | }
13 |
14 | export default new Config();
--------------------------------------------------------------------------------
/werewolf-api/src/lib/configs/service-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 | import util from '../util.ts';
9 |
10 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
11 |
12 | /**
13 | * 服务配置
14 | */
15 | export class ServiceConfig {
16 |
17 | /** 服务名称 */
18 | name: string;
19 | /** @type {string} 服务绑定主机地址 */
20 | host;
21 | /** @type {number} 服务绑定端口 */
22 | port;
23 | /** @type {string} 服务路由前缀 */
24 | urlPrefix;
25 | /** @type {string} 服务绑定地址(外部访问地址) */
26 | bindAddress;
27 |
28 | constructor(options?: any) {
29 | const { name, host, port, urlPrefix, bindAddress } = options || {};
30 | this.name = _.defaultTo(name, 'werewolf-api');
31 | this.host = _.defaultTo(host, '0.0.0.0');
32 | this.port = _.defaultTo(port, 5566);
33 | this.urlPrefix = _.defaultTo(urlPrefix, '');
34 | this.bindAddress = bindAddress;
35 | }
36 |
37 | get addressHost() {
38 | if(this.bindAddress) return this.bindAddress;
39 | const ipAddresses = util.getIPAddressesByIPv4();
40 | for(let ipAddress of ipAddresses) {
41 | if(ipAddress === this.host)
42 | return ipAddress;
43 | }
44 | return ipAddresses[0] || "127.0.0.1";
45 | }
46 |
47 | get address() {
48 | return `${this.addressHost}:${this.port}`;
49 | }
50 |
51 | get pageDirUrl() {
52 | return `http://127.0.0.1:${this.port}/page`;
53 | }
54 |
55 | get publicDirUrl() {
56 | return `http://127.0.0.1:${this.port}/public`;
57 | }
58 |
59 | static load() {
60 | const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
61 | if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
62 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
63 | return new ServiceConfig({ ...data, ...external });
64 | }
65 |
66 | }
67 |
68 | export default ServiceConfig.load();
--------------------------------------------------------------------------------
/werewolf-api/src/lib/configs/system-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 |
9 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
10 |
11 | /**
12 | * 系统配置
13 | */
14 | export class SystemConfig {
15 |
16 | /** 是否开启请求日志 */
17 | requestLog: boolean;
18 | /** 临时目录路径 */
19 | tmpDir: string;
20 | /** 日志目录路径 */
21 | logDir: string;
22 | /** 日志写入间隔(毫秒) */
23 | logWriteInterval: number;
24 | /** 日志文件有效期(毫秒) */
25 | logFileExpires: number;
26 | /** 公共目录路径 */
27 | publicDir: string;
28 | /** 临时文件有效期(毫秒) */
29 | tmpFileExpires: number;
30 | /** 请求体配置 */
31 | requestBody: any;
32 | /** 是否调试模式 */
33 | debug: boolean;
34 |
35 | constructor(options?: any) {
36 | const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
37 | this.requestLog = _.defaultTo(requestLog, false);
38 | this.tmpDir = _.defaultTo(tmpDir, './tmp');
39 | this.logDir = _.defaultTo(logDir, './logs');
40 | this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
41 | this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
42 | this.publicDir = _.defaultTo(publicDir, './public');
43 | this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
44 | this.requestBody = Object.assign(requestBody || {}, {
45 | enableTypes: ['json', 'form', 'text', 'xml'],
46 | encoding: 'utf-8',
47 | formLimit: '100mb',
48 | jsonLimit: '100mb',
49 | textLimit: '100mb',
50 | xmlLimit: '100mb',
51 | formidable: {
52 | maxFileSize: '100mb'
53 | },
54 | multipart: true,
55 | parsedMethods: ['POST', 'PUT', 'PATCH']
56 | });
57 | this.debug = _.defaultTo(debug, true);
58 | }
59 |
60 | get rootDirPath() {
61 | return path.resolve();
62 | }
63 |
64 | get tmpDirPath() {
65 | return path.resolve(this.tmpDir);
66 | }
67 |
68 | get logDirPath() {
69 | return path.resolve(this.logDir);
70 | }
71 |
72 | get publicDirPath() {
73 | return path.resolve(this.publicDir);
74 | }
75 |
76 | static load() {
77 | if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
78 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
79 | return new SystemConfig(data);
80 | }
81 |
82 | }
83 |
84 | export default SystemConfig.load();
--------------------------------------------------------------------------------
/werewolf-api/src/lib/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | SYSTEM_ERROR: [-1000, '系统异常'],
3 | SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
4 | SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
5 | } as Record
--------------------------------------------------------------------------------
/werewolf-api/src/lib/environment.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import minimist from 'minimist';
5 | import _ from 'lodash';
6 |
7 | const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
8 | const envVars = process.env; //获取环境变量
9 |
10 | class Environment {
11 |
12 | /** 命令行参数 */
13 | cmdArgs: any;
14 | /** 环境变量 */
15 | envVars: any;
16 | /** 环境名称 */
17 | env?: string;
18 | /** 服务名称 */
19 | name?: string;
20 | /** 服务地址 */
21 | host?: string;
22 | /** 服务端口 */
23 | port?: number;
24 | /** 包参数 */
25 | package: any;
26 |
27 | constructor(options: any = {}) {
28 | const { cmdArgs, envVars, package: _package } = options;
29 | this.cmdArgs = cmdArgs;
30 | this.envVars = envVars;
31 | this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
32 | this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
33 | this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
34 | this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
35 | this.package = _package;
36 | }
37 |
38 | }
39 |
40 | export default new Environment({
41 | cmdArgs,
42 | envVars,
43 | package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
44 | });
--------------------------------------------------------------------------------
/werewolf-api/src/lib/exceptions/APIException.ts:
--------------------------------------------------------------------------------
1 | import Exception from './Exception.js';
2 |
3 | export default class APIException extends Exception {
4 |
5 | /**
6 | * 构造异常
7 | *
8 | * @param {[number, string]} exception 异常
9 | */
10 | constructor(exception: (string | number)[], errmsg?: string) {
11 | super(exception, errmsg);
12 | }
13 |
14 | }
--------------------------------------------------------------------------------
/werewolf-api/src/lib/exceptions/Exception.ts:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 |
3 | import _ from 'lodash';
4 |
5 | export default class Exception extends Error {
6 |
7 | /** 错误码 */
8 | errcode: number;
9 | /** 错误消息 */
10 | errmsg: string;
11 | /** 数据 */
12 | data: any;
13 | /** HTTP状态码 */
14 | httpStatusCode: number;
15 |
16 | /**
17 | * 构造异常
18 | *
19 | * @param exception 异常
20 | * @param _errmsg 异常消息
21 | */
22 | constructor(exception: (string | number)[], _errmsg?: string) {
23 | assert(_.isArray(exception), 'Exception must be Array');
24 | const [errcode, errmsg] = exception as [number, string];
25 | assert(_.isFinite(errcode), 'Exception errcode invalid');
26 | assert(_.isString(errmsg), 'Exception errmsg invalid');
27 | super(_errmsg || errmsg);
28 | this.errcode = errcode;
29 | this.errmsg = _errmsg || errmsg;
30 | }
31 |
32 | compare(exception: (string | number)[]) {
33 | const [errcode] = exception as [number, string];
34 | return this.errcode == errcode;
35 | }
36 |
37 | setHTTPStatusCode(value: number) {
38 | this.httpStatusCode = value;
39 | return this;
40 | }
41 |
42 | setData(value: any) {
43 | this.data = _.defaultTo(value, null);
44 | return this;
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/werewolf-api/src/lib/initialize.ts:
--------------------------------------------------------------------------------
1 | import logger from './logger.js';
2 |
3 | // 允许无限量的监听器
4 | process.setMaxListeners(Infinity);
5 | // 输出未捕获异常
6 | process.on("uncaughtException", (err, origin) => {
7 | logger.error(`An unhandled error occurred: ${origin}`, err);
8 | });
9 | // 输出未处理的Promise.reject
10 | process.on("unhandledRejection", (_, promise) => {
11 | promise.catch(err => logger.error("An unhandled rejection occurred:", err));
12 | });
13 | // 输出系统警告信息
14 | process.on("warning", warning => logger.warn("System warning: ", warning));
15 | // 进程退出监听
16 | process.on("exit", () => {
17 | logger.info("Service exit");
18 | logger.footer();
19 | });
20 | // 进程被kill
21 | process.on("SIGTERM", () => {
22 | logger.warn("received kill signal");
23 | process.exit(2);
24 | });
25 | // Ctrl-C进程退出
26 | process.on("SIGINT", () => {
27 | process.exit(0);
28 | });
--------------------------------------------------------------------------------
/werewolf-api/src/lib/interfaces/ICompletionMessage.ts:
--------------------------------------------------------------------------------
1 | export default interface ICompletionMessage {
2 | role: 'system' | 'assistant' | 'user' | 'function';
3 | content: string;
4 | }
--------------------------------------------------------------------------------
/werewolf-api/src/lib/request/Request.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import APIException from '@/lib/exceptions/APIException.ts';
4 | import EX from '@/api/consts/exceptions.ts';
5 | import logger from '@/lib/logger.ts';
6 | import util from '@/lib/util.ts';
7 |
8 | export interface RequestOptions {
9 | time?: number;
10 | }
11 |
12 | export default class Request {
13 |
14 | /** 请求方法 */
15 | method: string;
16 | /** 请求URL */
17 | url: string;
18 | /** 请求路径 */
19 | path: string;
20 | /** 请求载荷类型 */
21 | type: string;
22 | /** 请求headers */
23 | headers: any;
24 | /** 请求原始查询字符串 */
25 | search: string;
26 | /** 请求查询参数 */
27 | query: any;
28 | /** 请求URL参数 */
29 | params: any;
30 | /** 请求载荷 */
31 | body: any;
32 | /** 上传的文件 */
33 | files: any[];
34 | /** 客户端IP地址 */
35 | remoteIP: string | null;
36 | /** 请求接受时间戳(毫秒) */
37 | time: number;
38 |
39 | constructor(ctx, options: RequestOptions = {}) {
40 | const { time } = options;
41 | this.method = ctx.request.method;
42 | this.url = ctx.request.url;
43 | this.path = ctx.request.path;
44 | this.type = ctx.request.type;
45 | this.headers = ctx.request.headers || {};
46 | this.search = ctx.request.search;
47 | this.query = ctx.query || {};
48 | this.params = ctx.params || {};
49 | this.body = ctx.request.body || {};
50 | this.files = ctx.request.files || {};
51 | this.remoteIP = this.headers["X-Real-IP"] || this.headers["x-real-ip"] || this.headers["X-Forwarded-For"] || this.headers["x-forwarded-for"] || ctx.ip || null;
52 | this.time = Number(_.defaultTo(time, util.timestamp()));
53 | }
54 |
55 | validate(key: string, fn?: Function) {
56 | try {
57 | const value = _.get(this, key);
58 | if (fn) {
59 | if (fn(value) === false)
60 | throw `[Mismatch] -> ${fn}`;
61 | }
62 | else if (_.isUndefined(value))
63 | throw '[Undefined]';
64 | }
65 | catch (err) {
66 | logger.warn(`Params ${key} invalid:`, err);
67 | throw new APIException(EX.API_REQUEST_PARAMS_INVALID, `Params ${key} invalid`);
68 | }
69 | return this;
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/werewolf-api/src/lib/response/Body.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export interface BodyOptions {
4 | code?: number;
5 | message?: string;
6 | data?: any;
7 | statusCode?: number;
8 | }
9 |
10 | export default class Body {
11 |
12 | /** 状态码 */
13 | code: number;
14 | /** 状态消息 */
15 | message: string;
16 | /** 载荷 */
17 | data: any;
18 | /** HTTP状态码 */
19 | statusCode: number;
20 |
21 | constructor(options: BodyOptions = {}) {
22 | const { code, message, data, statusCode } = options;
23 | this.code = Number(_.defaultTo(code, 0));
24 | this.message = _.defaultTo(message, 'OK');
25 | this.data = _.defaultTo(data, null);
26 | this.statusCode = Number(_.defaultTo(statusCode, 200));
27 | }
28 |
29 | toObject() {
30 | return {
31 | code: this.code,
32 | message: this.message,
33 | data: this.data
34 | };
35 | }
36 |
37 | static isInstance(value) {
38 | return value instanceof Body;
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/werewolf-api/src/lib/response/FailureBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 | import Exception from '../exceptions/Exception.ts';
5 | import APIException from '../exceptions/APIException.ts';
6 | import EX from '../consts/exceptions.ts';
7 | import HTTP_STATUS_CODES from '../http-status-codes.ts';
8 |
9 | export default class FailureBody extends Body {
10 |
11 | constructor(error: APIException | Exception | Error, _data?: any) {
12 | let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
13 | if(_.isString(error))
14 | error = new Exception(EX.SYSTEM_ERROR, error);
15 | else if(error instanceof APIException || error instanceof Exception)
16 | ({ errcode, errmsg, data, httpStatusCode } = error);
17 | else if(_.isError(error))
18 | ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
19 | super({
20 | code: errcode || -1,
21 | message: errmsg || 'Internal error',
22 | data,
23 | statusCode: httpStatusCode
24 | });
25 | }
26 |
27 | static isInstance(value) {
28 | return value instanceof FailureBody;
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/werewolf-api/src/lib/response/Response.ts:
--------------------------------------------------------------------------------
1 | import mime from 'mime';
2 | import _ from 'lodash';
3 |
4 | import Body from './Body.ts';
5 | import util from '../util.ts';
6 |
7 | export interface ResponseOptions {
8 | statusCode?: number;
9 | type?: string;
10 | headers?: Record;
11 | redirect?: string;
12 | body?: any;
13 | size?: number;
14 | time?: number;
15 | }
16 |
17 | export default class Response {
18 |
19 | /** 响应HTTP状态码 */
20 | statusCode: number;
21 | /** 响应内容类型 */
22 | type: string;
23 | /** 响应headers */
24 | headers: Record;
25 | /** 重定向目标 */
26 | redirect: string;
27 | /** 响应载荷 */
28 | body: any;
29 | /** 响应载荷大小 */
30 | size: number;
31 | /** 响应时间戳 */
32 | time: number;
33 |
34 | constructor(body: any, options: ResponseOptions = {}) {
35 | const { statusCode, type, headers, redirect, size, time } = options;
36 | this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
37 | this.type = type;
38 | this.headers = headers;
39 | this.redirect = redirect;
40 | this.size = size;
41 | this.time = Number(_.defaultTo(time, util.timestamp()));
42 | this.body = body;
43 | }
44 |
45 | injectTo(ctx) {
46 | this.redirect && ctx.redirect(this.redirect);
47 | this.statusCode && (ctx.status = this.statusCode);
48 | this.type && (ctx.type = mime.getType(this.type) || this.type);
49 | const headers = this.headers || {};
50 | if(this.size && !headers["Content-Length"] && !headers["content-length"])
51 | headers["Content-Length"] = this.size;
52 | ctx.set(headers);
53 | if(Body.isInstance(this.body))
54 | ctx.body = this.body.toObject();
55 | else
56 | ctx.body = this.body;
57 | }
58 |
59 | static isInstance(value) {
60 | return value instanceof Response;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/werewolf-api/src/lib/response/SuccessfulBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 |
5 | export default class SuccessfulBody extends Body {
6 |
7 | constructor(data: any, message?: string) {
8 | super({
9 | code: 0,
10 | message: _.defaultTo(message, "OK"),
11 | data
12 | });
13 | }
14 |
15 | static isInstance(value) {
16 | return value instanceof SuccessfulBody;
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/werewolf-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "allowImportingTsExtensions": true,
7 | "allowSyntheticDefaultImports": true,
8 | "noEmit": true,
9 | "paths": {
10 | "@/*": ["src/*"]
11 | },
12 | "outDir": "./dist"
13 | },
14 | "include": ["src/**/*", "libs.d.ts"],
15 | "exclude": ["node_modules", "dist"]
16 | }
--------------------------------------------------------------------------------
/werewolf-api/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "builds": [
3 | {
4 | "src": "./dist/*.html",
5 | "use": "@vercel/static"
6 | },
7 | {
8 | "src": "./dist/index.js",
9 | "use": "@vercel/node"
10 | }
11 | ],
12 | "routes": [
13 | {
14 | "src": "/",
15 | "dest": "/dist/welcome.html"
16 | },
17 | {
18 | "src": "/(.*)",
19 | "dest": "/dist",
20 | "headers": {
21 | "Access-Control-Allow-Credentials": "true",
22 | "Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT",
23 | "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, Authorization"
24 | }
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/wxkf-api/.dockerignore:
--------------------------------------------------------------------------------
1 | logs
2 | dist
3 | doc
4 | node_modules
5 | .vscode
6 | .git
7 | .gitignore
8 | README.md
9 | *.tar.gz
--------------------------------------------------------------------------------
/wxkf-api/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | logs/
4 | tmp/
5 | .vercel
6 | forward-port-proxy.sh
7 | *.pem
8 | wxkf-api.tar.gz
9 | secret.yml
10 | agents.yml
--------------------------------------------------------------------------------
/wxkf-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts AS BUILD_IMAGE
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8 |
9 | FROM node:lts-alpine
10 |
11 | COPY --from=BUILD_IMAGE /app/configs /app/configs
12 | COPY --from=BUILD_IMAGE /app/package.json /app/package.json
13 | COPY --from=BUILD_IMAGE /app/dist /app/dist
14 | COPY --from=BUILD_IMAGE /app/public /app/public
15 | COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16 | COPY --from=BUILD_IMAGE /app/agents.yml.template /app/agents.yml
17 | COPY --from=BUILD_IMAGE /app/secret.yml.template /app/secret.yml
18 |
19 | WORKDIR /app
20 |
21 | EXPOSE 8001
22 |
23 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/wxkf-api/README.md:
--------------------------------------------------------------------------------
1 | # 微信客服接入智谱清言智能体
2 |
3 | ## 简介
4 |
5 | 本项目是一个将微信客服快速接入智谱清言智能体的项目。它可以帮助您将智谱清言智能体集成到您的微信客服系统中,在微信原生体验下为用户带来便捷的智能体交互。
6 |
7 | ## 部署
8 |
9 | 你可以选择Docker或原生部署两种方式。
10 |
11 | ### Docker部署
12 |
13 | ```shell
14 | # 拉取镜像,如果镜像源dockerproxy.cn拉不动可以换别的
15 | docker pull dockerproxy.cn/vinlic/wxkf-api
16 | ```
17 |
18 | 建立一个空目录,下载仓库中的以下四个文件到目录中:
19 |
20 | `agents.yml.template`、`secret.yml.template`、`startup-docker.sh`、`shutdown-docker.sh`
21 |
22 | ```shell
23 | # 重命名智能体配置文件
24 | mv agents.yml.template agents.yml
25 | # 重命名secret配置文件
26 | mv secret.yml.template secret.yml
27 | # 授予脚本执行权限
28 | chmod u+x startup-docker.sh shutdown-docker.sh
29 | # 运行容器并显示日志
30 | ./startup-docker.sh
31 | # 查看服务日志
32 | docker logs -f wxkf-api
33 | # 看到输出的日志中出现【STEP1】的警告代表初步部署完成,请继续往下
34 | ```
35 |
36 | 
37 |
38 | ### 原生部署
39 |
40 | 首先,您需要安装Node.js 18+。然后,您可以使用以下命令在项目根目录安装依赖和构建部署:
41 |
42 | ```bash
43 | # 确认Node版本是否在18以上
44 | node -v
45 | # 全局安装PM2进程管理器
46 | npm install pm2 -g --registry https://registry.npmmirror.com
47 | # 安装依赖
48 | npm install --registry https://registry.npmmirror.com
49 | # 编译构建
50 | npm run build
51 | # 重命名智能体配置文件
52 | mv agents.yml.template agents.yml
53 | # 重命名secret配置文件
54 | mv secret.yml.template secret.yml
55 | # 启动服务
56 | pm2 start dist/index.js --name "wxkf-api"
57 | # 查看服务日志
58 | pm2 logs wxkf-api
59 | # 看到输出的日志中出现【STEP1】的警告代表初步部署完成,请继续往下
60 | ```
61 |
62 | 
63 |
64 | ## 启用微信客服
65 |
66 | 前往微信客服([https://kf.weixin.qq.com/](https://kf.weixin.qq.com/))扫码登录(首先你得是企业微信管理员)。
67 |
68 | 进入开发配置选项,点击“开始使用”启用企业内部接入。
69 |
70 | 
71 |
72 | 回调地址请填写 [https://example.com/message/notify](https://example.com/message/notify)(域名请替换为你的,如果你没有域名请替换为IP地址和端口同时协议改为http,地址是指向本服务部署地址)。
73 |
74 | Token和EncodingAESKey可以自行填写或随机生成并保留记录。
75 |
76 | 
77 |
78 | ## 验证服务器回调
79 |
80 | 在secret.yml中先配置您的Token和EncodingAESKey并保存重启。
81 |
82 | ```yaml
83 | # 企业ID
84 | WXKF_API_CORP_ID: "default"
85 | # Secret
86 | WXKF_API_CORP_SECRET: "default"
87 | # Token
88 | WXKF_API_TOKEN: "填写你的Token"
89 | # EncodingAESKey
90 | WXKF_API_ENCODING_AES_KEY: "填写你的EncodingAESKey"
91 | ```
92 |
93 | ```shell
94 | # Docker部署重启方式
95 | ./shutdown-docker.sh
96 | ./startup-docker.sh
97 | # 查看服务日志
98 | docker logs -f wxkf-api
99 | # 看到输出的日志中出现【STEP2】的警告代表第一步完成,请继续往下
100 | ```
101 |
102 | ```shell
103 | # 原生部署重启方式
104 | pm2 reload wxkf-api
105 | # 查看服务日志
106 | pm2 logs wxkf-api
107 | # 看到输出的日志中出现【STEP2】的警告代表第一步完成,请继续往下
108 | ```
109 |
110 | 
111 |
112 |
113 | 点击“完成”按钮,完成服务器回调的验证。
114 |
115 | 验证通过后会进入以下界面:
116 |
117 | 
118 |
119 | 请根据标识配置到您的secret.yml文件中,并再次重启。
120 |
121 | ```yaml
122 | # 企业ID
123 | WXKF_API_CORP_ID: "填写你的企业ID"
124 | # Secret
125 | WXKF_API_CORP_SECRET: "填写你的Secret"
126 | # Token
127 | WXKF_API_TOKEN: "填写你的Token"
128 | # EncodingAESKey
129 | WXKF_API_ENCODING_AES_KEY: "填写你的EncodingAESKey"
130 | ```
131 |
132 | ```shell
133 | # Docker部署重启方式
134 | ./shutdown-docker.sh
135 | ./startup-docker.sh
136 | # 查看服务日志
137 | docker logs -f wxkf-api
138 | # 看到输出的日志中没有出现错误即可,请继续往下
139 | ```
140 |
141 | ```shell
142 | # 原生部署重启方式
143 | pm2 reload wxkf-api
144 | # 查看服务日志
145 | pm2 logs wxkf-api
146 | # 看到输出的日志中没有出现错误即可,请继续往下
147 | ```
148 |
149 | 
150 |
151 | ## 配置智能体
152 |
153 | 按照以下说明配置您的智能体。
154 |
155 | 先前往[智能体中心](https://chatglm.cn/main/toolsCenter)选用您自己已发布的智能体,或者他人已发布的智能体(仅网页API接入可用)。
156 |
157 | 配置完毕后请再次重启服务即可。
158 |
159 | ### 智能体原生API接入方式
160 |
161 | 先在创作者中心[创建API Key](https://chatglm.cn/developersPanel/apiSet),然后将key和secret用`.`拼接填入apiKey处。
162 |
163 | ```yaml
164 | - # 超级MEME智能体
165 | # 智能体ID(只能使用自己创建的已发布智能体,从智能体页面的地址栏URL尾部获取24位字符)
166 | id: "667e550c1d0afe6b58f0c4ca"
167 | # 智能体名称,设定后将作为客服名称
168 | name: "超级MEME"
169 | # 接入方式,这里是原生API接入
170 | api: "qingyan-glms-api"
171 | # 用.拼接的key和secret
172 | apiKey: "21a**********9a0.2f****************************37"
173 | # 首次进入客服的欢迎语,不设置将不发送欢迎语
174 | welcome: "给我一个词,送你一个meme图!"
175 | # 最大对话轮数,超出后会自动开启新的会话,默认为8轮,可以根据你的智能体性质来决定
176 | # 比如聊天类的可以有比较多的轮次,但像这种工具类的为了减少幻觉可能只给一轮
177 | maxRounds: 1
178 | # 智能体头像URL,可以在网页上打开智能体页面右击智能体头像复制图片地址获得
179 | avatarUrl: "https://sfile.chatglm.cn/img2text/5926ade5-d009-45a7-9a2b-94f7c19eae56.jpg"
180 | # 是否启用智能体,如果禁用会自动回复:抱歉,智能体当前已下线,暂时无法为您提供服务T^T
181 | enabled: true
182 | ```
183 |
184 | ### 智能体网页API接入方式
185 |
186 | 先在智谱清言登录,然后从浏览器Cookies中获取`chatglm_refresh_token`值,可以使用Cookie-Editor等浏览器插件辅助获取。
187 |
188 | ```yaml
189 | - # ChatGLM
190 | # 智能体ID(从智能体页面的地址栏URL尾部获取24位字符)
191 | id: "65940acff94777010aa6b796"
192 | # 智能体名称,设定后将作为客服名称
193 | name: "ChatGLM"
194 | # 接入方式,这里是网页API接入
195 | api: "qingyan-glms-free-api"
196 | # chatglm_refresh_token的值
197 | apiKey: "eyJhbGciOiJI..."
198 | # 首次进入客服的欢迎语,不设置将不发送欢迎语
199 | welcome: "欢迎使用ChatGLM,让我们一起探索AGI!"
200 | # 比如聊天类的可以有比较多的轮次,但工具类的为了减少幻觉可能只给一轮
201 | maxRounds: 10
202 | # 智能体头像URL,可以在网页上打开智能体页面右击智能体头像复制图片地址获得
203 | avatarUrl: "https://sfile.chatglm.cn/chatglm4/81a30afa-d5d9-4c9e-9854-dabc64ab2574.png"
204 | # 是否启用智能体,如果禁用会自动回复:抱歉,智能体当前已下线,暂时无法为您提供服务T^T
205 | enabled: true
206 | ```
207 |
208 | ## 自动初始化客服
209 |
210 | 完成以上智能体配置检查无误后重启服务,服务会自动根据配置创建客服账号列表。
211 |
212 | ```shell
213 | # Docker部署重启方式
214 | ./shutdown-docker.sh
215 | ./startup-docker.sh
216 | # 查看服务日志
217 | docker logs -f wxkf-api
218 | # 如果日志中显示账号创建成功,即代表完成初始化
219 | ```
220 |
221 | ```shell
222 | # 原生部署重启方式
223 | pm2 reload wxkf-api
224 | # 查看服务日志
225 | pm2 logs wxkf-api
226 | # 如果日志中显示账号创建成功,即代表完成初始化
227 | ```
228 |
229 | 创建后会生成一个客服链接,请复制下来,这个链接可以放到任意需要跳转智能体客服的位置,你也可以将它[分享为卡片](#分享卡片)。
230 |
231 | 
232 |
233 | 打开微信客服([https://kf.weixin.qq.com/](https://kf.weixin.qq.com/))的客服账号,可以看到自动创建好的智能体列表。
234 |
235 | 
236 |
237 | ## 验证功能
238 |
239 | 通过客服链接或卡片进入后,就可以直接与智能体对话了!
240 |
241 | 
242 |
243 | ## 分享卡片
244 |
245 | 你可以将智能体客服分享为卡片。
246 |
247 | 
248 |
249 | 点开客服账号右上角,再点击头像,再点击下图右上角位置即可将智能体客服以卡片形式分享给好友。
250 |
251 | 
252 |
--------------------------------------------------------------------------------
/wxkf-api/agents.yml.template:
--------------------------------------------------------------------------------
1 | # 这是一个配置模板,使用前请将文件名改为agents.yml
2 | # 注意保密您的配置信息,不要将配置文件上传到公共代码仓库中!
3 |
4 | # 【智能体原生API接入方式】
5 | # 此API只能接入您自己创建的已发布智能体
6 | # id设置为您的智能体ID(打开智能体页面,浏览器地址栏URL尾部有一串24位的随机字符串文本)
7 | # api设置为qingyan-glms-api
8 | # apiKey请在清言智能体中心->创作者中心创建API Key,然后将key和secret用.拼接,如:21a**********9a0.2f****************************37
9 | # welcome可以设置首次进入客服时的欢迎语,不设置将不发送欢迎语
10 | # maxRounds设置您的智能体最大对话轮数,超出后会自动开启新的会话,默认为8轮,可以根据你的智能体性质来决定(比如聊天类的可以有比较多的轮次,但工具类的为了减少幻觉可能只给一轮)
11 | # avatarUrl设置您的智能体头像URL,可以在网页上打开智能体页面右击智能体头像复制图片地址获得
12 | # enabled设置您的智能体是否启用
13 |
14 | # 【智能体网页API接入方式】
15 | # 此API可以接入所有已发布的智能体
16 | # id设置为您的智能体ID(打开智能体页面,浏览器地址栏URL尾部有一串24位的随机字符串文本)
17 | # api设置为qingyan-glms-free-api
18 | # apiKey使用您在网页Cookies中获取的chatglm_refresh_token,作为你的apiKey
19 | # welcome可以设置首次进入客服时的欢迎语,不设置将不发送欢迎语
20 | # maxRounds设置您的智能体最大对话轮数,超出后会自动开启新的会话,默认为8轮,可以根据你的智能体性质来决定(比如聊天类的可以有比较多的轮次,但工具类的为了减少幻觉可能只给一轮)
21 | # avatarUrl设置您的智能体头像URL,可以在网页上打开智能体页面右击智能体头像复制图片地址获得
22 | # enabled设置您的智能体是否启用
23 |
24 | # 以下是两种接入方式的示例,你可以修改或新增智能体
25 |
26 | # - # 温柔老婆
27 | # id: "65a8df8ff4739c58b6aa2598"
28 | # name: "温柔老婆"
29 | # welcome: "老公,我会尽一切能力为你解决问题的。"
30 | # api: "qingyan-glms-api"
31 | # apiKey: "[替换为你的apiKey]"
32 | # maxRounds: 10
33 | # avatarUrl: https://sfile.chatglm.cn/testpath/3f1fce72-313b-5da6-9114-add88d09c470_0.png?image_process=resize,fw_300,fh_300"
34 | # enabled: true
35 |
36 | # - # ChatGLM
37 | # id: "65940acff94777010aa6b796"
38 | # name: "ChatGLM"
39 | # welcome: "欢迎使用ChatGLM,让我们一起探索AGI!"
40 | # api: "qingyan-glms-free-api"
41 | # apiKey: "[替换为你的apiKey]"
42 | # maxRounds: 10
43 | # avatarUrl: "https://sfile.chatglm.cn/img2text/8a82543e-850b-45d1-820c-b48ae9e7f147.jpg"
44 | # enabled: true
--------------------------------------------------------------------------------
/wxkf-api/build-docker.sh:
--------------------------------------------------------------------------------
1 | docker build -t wxkf-api:latest .
2 | docker tag wxkf-api:latest vinlic/wxkf-api:latest
3 | docker push vinlic/wxkf-api:latest
--------------------------------------------------------------------------------
/wxkf-api/configs/dev/service.yml:
--------------------------------------------------------------------------------
1 | # 服务名称
2 | name: wxkf-api
3 | # 服务绑定主机地址
4 | host: '0.0.0.0'
5 | # 服务绑定端口
6 | port: 8001
--------------------------------------------------------------------------------
/wxkf-api/configs/dev/system.yml:
--------------------------------------------------------------------------------
1 | # 是否开启请求日志
2 | requestLog: true
3 | # 临时目录路径
4 | tmpDir: ./tmp
5 | # 日志目录路径
6 | logDir: ./logs
7 | # 日志写入间隔(毫秒)
8 | logWriteInterval: 200
9 | # 日志文件有效期(毫秒)
10 | logFileExpires: 2626560000
11 | # 公共目录路径
12 | publicDir: ./public
13 | # 临时文件有效期(毫秒)
14 | tmpFileExpires: 86400000
--------------------------------------------------------------------------------
/wxkf-api/doc/example-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-1.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-10.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-11.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-2.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-3.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-4.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-5.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-6.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-7.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-8.png
--------------------------------------------------------------------------------
/wxkf-api/doc/example-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/doc/example-9.png
--------------------------------------------------------------------------------
/wxkf-api/libs.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaGLM/qingyan-cookbook/6682c20623837b542aa181ba432cbe5cce8448ab/wxkf-api/libs.d.ts
--------------------------------------------------------------------------------
/wxkf-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wxkf-api",
3 | "version": "0.0.1",
4 | "description": "ZhipuAI Agent To OpenAI",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "module": "dist/index.mjs",
8 | "types": "dist/index.d.ts",
9 | "directories": {
10 | "dist": "dist"
11 | },
12 | "files": [
13 | "dist/"
14 | ],
15 | "scripts": {
16 | "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node --enable-source-maps --no-node-snapshot dist/index.js\"",
17 | "start": "node --enable-source-maps --no-node-snapshot dist/index.js",
18 | "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
19 | },
20 | "author": "Vinlic",
21 | "license": "ISC",
22 | "dependencies": {
23 | "@wecom/crypto": "^1.0.1",
24 | "async-lock": "^1.4.1",
25 | "axios": "^1.6.7",
26 | "colors": "^1.4.0",
27 | "crc-32": "^1.2.2",
28 | "cron": "^3.1.6",
29 | "date-fns": "^3.3.1",
30 | "eventsource-parser": "^1.1.2",
31 | "file-type": "^19.0.0",
32 | "form-data": "^4.0.0",
33 | "fs-extra": "^11.2.0",
34 | "koa": "^2.15.0",
35 | "koa-body": "^5.0.0",
36 | "koa-bodyparser": "^4.4.1",
37 | "koa-range": "^0.3.0",
38 | "koa-router": "^12.0.1",
39 | "koa2-cors": "^2.0.6",
40 | "lodash": "^4.17.21",
41 | "mime": "^4.0.1",
42 | "minimist": "^1.2.8",
43 | "randomstring": "^1.3.0",
44 | "uuid": "^9.0.1",
45 | "xml-js": "^1.6.11",
46 | "yaml": "^2.3.4"
47 | },
48 | "devDependencies": {
49 | "@types/lodash": "^4.14.202",
50 | "@types/mime": "^3.0.4",
51 | "@types/async-lock": "^1.4.2",
52 | "@types/file-type": "^10.9.1",
53 | "@types/fs-extra": "^11.0.4",
54 | "tsup": "^8.0.2",
55 | "typescript": "^5.3.3"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/wxkf-api/public/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 🚀 服务已启动
6 |
7 |
8 | 微信客服服务已经启动!
9 |
10 |
--------------------------------------------------------------------------------
/wxkf-api/secret.yml.template:
--------------------------------------------------------------------------------
1 | # 这是一个配置模板,使用前请将文件名改为secret.yml
2 | # 注意保密您的配置信息,不要将配置文件上传到公共代码仓库中!
3 |
4 | # 【接入教程】
5 | # 1.前往微信客服([https://kf.weixin.qq.com/](https://kf.weixin.qq.com/))扫码登录(首先你得是企业微信管理员)。
6 | # 2.进入开发配置,点击企业内部接入,开始填写回调地址。
7 | # 3.回调地址请填写[https://example.com/message/notify](https://example.com/message/notify)(域名请替换为你的,地址是指向本服务部署地址)。
8 | # 4.Token和EncodingAESKey可以自行填写或随机生成,然后填入以下配置的WXKF_API_TOKEN和WXKF_API_ENCODING_AES_KEY。
9 | # 5.启动服务
10 |
11 | # 企业ID
12 | WXKF_API_CORP_ID: "default"
13 | # Secret
14 | WXKF_API_CORP_SECRET: "default"
15 | # Token
16 | WXKF_API_TOKEN: "default"
17 | # EncodingAESKey
18 | WXKF_API_ENCODING_AES_KEY: "default"
--------------------------------------------------------------------------------
/wxkf-api/shutdown-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | docker stop wxkf-api
6 | docker rm wxkf-api
7 |
8 | echo "Shudown completed!"
9 |
--------------------------------------------------------------------------------
/wxkf-api/src/api/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | API_TEST: [-9999, 'API异常错误'],
3 | API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
4 | API_REQUEST_FAILED: [-2001, '请求失败'],
5 | API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
6 | API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
7 | API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
8 | API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'],
9 | API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'],
10 | API_IMAGE_GENERATION_FAILED: [-2007, '图像生成失败'],
11 | API_WECHAT_SIGNATURE_INVALID: [-2008, '微信签名非法'],
12 | API_AGENT_IS_DISABLED: [-2009, '客服已禁用']
13 | }
--------------------------------------------------------------------------------
/wxkf-api/src/api/controllers/interfaces/ICompletionMessage.ts:
--------------------------------------------------------------------------------
1 | export default interface ICompletionMessage {
2 | role: 'system' | 'user' | 'assistant',
3 | content: string | Record[]
4 | }
--------------------------------------------------------------------------------
/wxkf-api/src/api/controllers/interfaces/IMedia.ts:
--------------------------------------------------------------------------------
1 | export default interface IMedia {
2 | type: 'image' | 'voice' | 'video' | 'file',
3 | promise: Promise<{
4 | contentType: string;
5 | data: Buffer;
6 | }>
7 | }
--------------------------------------------------------------------------------
/wxkf-api/src/api/controllers/interfaces/IMessage.ts:
--------------------------------------------------------------------------------
1 | export default interface IMessage {
2 | /** 消息发送者角色 */
3 | send_role: 'user' | 'assistant';
4 | /** 消息类型 */
5 | msgtype: 'text' | 'image' | 'voice' | 'video' | 'file' | 'location' | 'miniprogram' | 'channels_shop_product' | 'channels_shop_order' | 'merged_msg' | 'channels' | 'event';
6 | /** 消息ID */
7 | msgid: string;
8 | /** 客服账号ID */
9 | open_kfid: string;
10 | /** 客户UserID */
11 | external_userid: string;
12 | /** 消息发送时间 */
13 | send_time: number;
14 | /** 消息来源,3:客户回复的消息 4:系统推送的消息 */
15 | origin: number;
16 | /** 文本消息 */
17 | text?: {
18 | /** 文本内容 */
19 | content: string,
20 | /** 客户点击菜单消息,触发的回复消息中附带的菜单ID */
21 | menu_id?: string
22 | };
23 | /** 图片消息 */
24 | image?: {
25 | /** 图片文件id */
26 | media_id: string;
27 | };
28 | /** 语音消息 */
29 | voice?: {
30 | /** 语音文件ID */
31 | media_id: string;
32 | };
33 | /** 视频消息 */
34 | video?: {
35 | /** 视频文件ID */
36 | media_id: string;
37 | };
38 | /** 文件消息 */
39 | file?: {
40 | /** 文件ID */
41 | media_id: string;
42 | };
43 | /** 位置消息 */
44 | location?: {
45 | /** 位置名 */
46 | name: string;
47 | /** 地址详情说明 */
48 | address: string;
49 | /** 纬度 */
50 | latitude: number;
51 | /** 经度 */
52 | longitude: number;
53 | };
54 | /** 小程序消息 */
55 | miniprogram?: {
56 | /** 标题 */
57 | title: string;
58 | /** 小程序appid */
59 | appid: string;
60 | /** 点击消息卡片后进入的小程序页面路径 */
61 | pagepath: string;
62 | /** 小程序消息封面的文件ID */
63 | thumb_media_id: string;
64 | };
65 | /** 视频号商品消息 */
66 | channels_shop_product?: {
67 | /** 商品标题 */
68 | title: string;
69 | /** 商品ID */
70 | product_id: string;
71 | /** 商品图片 */
72 | head_image: string;
73 | /** 商品价格,以分为单位 */
74 | sales_price: string;
75 | /** 店铺名称 */
76 | shop_nickname: string;
77 | /** 店铺头像 */
78 | shop_head_image: string;
79 | };
80 | /** 视频号订单消息 */
81 | channels_shop_order?: {
82 | /** 订单号 */
83 | order_id: string;
84 | /** 商品标题 */
85 | product_titles: string;
86 | /** 订单价格描述 */
87 | price_wording: string;
88 | /** 订单状态 */
89 | state: string;
90 | /** 订单缩略图 */
91 | image_url: string;
92 | /** 店铺名称 */
93 | shop_nickname: string;
94 | };
95 | /** 聊天记录消息 */
96 | merged_msg?: {
97 | /** 聊天记录标题 */
98 | title: string;
99 | /** 消息记录内的消息内容 */
100 | item: {
101 | /** 消息类型 */
102 | msgtype: string;
103 | /** 发送时间 */
104 | send_time: number;
105 | /** 发送者名称 */
106 | sender_name: string;
107 | /** 消息内容,Json字符串,结构可参考本文档消息类型说明 */
108 | msg_content: string;
109 | }[]
110 | },
111 | /** 视频号消息 */
112 | channels?: {
113 | /** 视频号消息类型,1:视频号动态 2:视频号直播 3:视频号名片 */
114 | sub_type: number;
115 | /** 视频号名称 */
116 | nickname: string;
117 | /** 视频号动态标题,视频号消息类型为“1视频号动态”时,返回动态标题 */
118 | title: string;
119 | },
120 | /** 事件消息 */
121 | event?: {
122 | /** 事件类型,此处固定为:enter_session */
123 | event_type: string;
124 | /** 客服账号ID */
125 | open_kfid: string;
126 | /** 客户UserID */
127 | external_userid: string;
128 | /** 进入会话的场景值,获取客服账号链接开发者自定义的场景值 */
129 | scene: string;
130 | /** 进入会话的自定义参数,获取客服账号链接返回的url,开发者按规范拼接的scene_param参数 */
131 | scene_param: string;
132 | /** 如果满足发送欢迎语条件(条件为:用户在过去48小时里未收过欢迎语,且未向客服发过消息),会返回该字段。
133 | 可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。 */
134 | welcome_code: string;
135 | /** 进入会话的视频号信息,从视频号进入会话才有值 */
136 | wechat_channels: {
137 | /** 视频号名称,视频号场景值为1、2、3时返回此项 */
138 | nickname: string;
139 | /** 视频号小店名称,视频号场景值为4、5时返回此项 */
140 | shop_nickname: string;
141 | /** 视频号场景值,1:视频号主页,2:视频号直播间商品列表页,3:视频号商品橱窗页,4:视频号小店商品详情页,5:视频号小店订单页 */
142 | scene: number;
143 | };
144 | };
145 | }
--------------------------------------------------------------------------------
/wxkf-api/src/api/routes/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | import Response from '@/lib/response/Response.ts';
4 | import message from './message.ts';
5 |
6 | export default [
7 | {
8 | get: {
9 | '/': async () => {
10 | const content = await fs.readFile('public/welcome.html');
11 | return new Response(content, {
12 | type: 'html',
13 | headers: {
14 | Expires: '-1'
15 | }
16 | });
17 | }
18 | }
19 | },
20 | message
21 | ];
--------------------------------------------------------------------------------
/wxkf-api/src/api/routes/message.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Request from '@/lib/request/Request.ts';
4 | import message from '@/api/controllers/message.ts';
5 | import wxkfApi from '@/lib/wxkf-api.ts';
6 |
7 | export default {
8 |
9 | prefix: '/message',
10 |
11 | get: {
12 |
13 | '/notify': async (request: Request) => {
14 | request
15 | .validate('query.msg_signature')
16 | .validate('query.timestamp')
17 | .validate('query.nonce')
18 | .validate('query.echostr');
19 | const query = request.query;
20 | wxkfApi.checkSignature(query, query.echostr);
21 | const { message: msg } = wxkfApi.decryptData(query.echostr);
22 | return msg;
23 | }
24 |
25 | },
26 |
27 | post: {
28 |
29 | '/notify': async (request: Request) => {
30 | request
31 | .validate('query.msg_signature')
32 | .validate('query.timestamp')
33 | .validate('query.nonce')
34 | .validate('body.xml');
35 | const query = request.query;
36 | const params = request.body.xml;
37 | const {
38 | ToUserName: toUserName,
39 | Encrypt: encryptedData,
40 | AgentID: agentId
41 | } = params;
42 | wxkfApi.checkSignature(query, encryptedData);
43 | const {
44 | message: msg
45 | } = wxkfApi.decryptData(encryptedData);
46 | const { token } = wxkfApi.parseMessage(msg);
47 | const handlePromises = [];
48 | for(let msg of await message.getMessageList(token))
49 | handlePromises.push(message.handleMessge(msg));
50 | await Promise.all(handlePromises);
51 | return {}
52 | }
53 |
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/wxkf-api/src/index.ts:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import environment from "@/lib/environment.ts";
4 | import config from "@/lib/config.ts";
5 | import "@/lib/initialize.ts";
6 | import server from "@/lib/server.ts";
7 | import routes from "@/api/routes/index.ts";
8 | import wxkfApi from "./lib/wxkf-api.ts";
9 | import logger from "@/lib/logger.ts";
10 |
11 | const startupTime = performance.now();
12 |
13 | (async () => {
14 | logger.header();
15 |
16 | logger.info("<<<< wxkf api >>>>");
17 | logger.info("Version:", environment.package.version);
18 | logger.info("Process id:", process.pid);
19 | logger.info("Environment:", environment.env);
20 | logger.info("Service name:", config.service.name);
21 |
22 | server.attachRoutes(routes);
23 |
24 | await server.listen();
25 |
26 | await wxkfApi.initAccountList()
27 | .catch(err => logger.error(err));
28 |
29 | config.service.bindAddress &&
30 | logger.success("Service bind address:", config.service.bindAddress);
31 | })()
32 | .then(() =>
33 | logger.success(
34 | `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
35 | )
36 | )
37 | .catch((err) => console.error(err));
38 |
--------------------------------------------------------------------------------
/wxkf-api/src/lib/agents.ts:
--------------------------------------------------------------------------------
1 | import yaml from 'yaml';
2 | import _ from 'lodash';
3 | import fs from 'fs-extra';
4 | import logger from './logger.ts';
5 | import IAgentConfig from './interfaces/IAgentConfig.ts';
6 |
7 | let agents: IAgentConfig[] = [];
8 |
9 | if(!fs.existsSync('agents.yml'))
10 | logger.warn('agents.yml未找到, 请重命名项目根目录下的agents.yml.template为agents.yml并填写配置');
11 | else
12 | agents = yaml.parse(fs.readFileSync('agents.yml').toString()) || [];
13 |
14 | export default agents;
--------------------------------------------------------------------------------
/wxkf-api/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | import serviceConfig from "./configs/service-config.ts";
2 | import systemConfig from "./configs/system-config.ts";
3 |
4 | class Config {
5 |
6 | /** 服务配置 */
7 | service = serviceConfig;
8 |
9 | /** 系统配置 */
10 | system = systemConfig;
11 |
12 | }
13 |
14 | export default new Config();
--------------------------------------------------------------------------------
/wxkf-api/src/lib/configs/service-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 | import util from '../util.ts';
9 |
10 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
11 |
12 | /**
13 | * 服务配置
14 | */
15 | export class ServiceConfig {
16 |
17 | /** 服务名称 */
18 | name: string;
19 | /** @type {string} 服务绑定主机地址 */
20 | host;
21 | /** @type {number} 服务绑定端口 */
22 | port;
23 | /** @type {string} 服务路由前缀 */
24 | urlPrefix;
25 | /** @type {string} 服务绑定地址(外部访问地址) */
26 | bindAddress;
27 |
28 | constructor(options?: any) {
29 | const { name, host, port, urlPrefix, bindAddress } = options || {};
30 | this.name = _.defaultTo(name, 'wxkf-api');
31 | this.host = _.defaultTo(host, '0.0.0.0');
32 | this.port = _.defaultTo(port, 5566);
33 | this.urlPrefix = _.defaultTo(urlPrefix, '');
34 | this.bindAddress = bindAddress;
35 | }
36 |
37 | get addressHost() {
38 | if(this.bindAddress) return this.bindAddress;
39 | const ipAddresses = util.getIPAddressesByIPv4();
40 | for(let ipAddress of ipAddresses) {
41 | if(ipAddress === this.host)
42 | return ipAddress;
43 | }
44 | return ipAddresses[0] || "127.0.0.1";
45 | }
46 |
47 | get address() {
48 | return `${this.addressHost}:${this.port}`;
49 | }
50 |
51 | get pageDirUrl() {
52 | return `http://127.0.0.1:${this.port}/page`;
53 | }
54 |
55 | get publicDirUrl() {
56 | return `http://127.0.0.1:${this.port}/public`;
57 | }
58 |
59 | static load() {
60 | const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
61 | if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
62 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
63 | return new ServiceConfig({ ...data, ...external });
64 | }
65 |
66 | }
67 |
68 | export default ServiceConfig.load();
--------------------------------------------------------------------------------
/wxkf-api/src/lib/configs/system-config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import yaml from 'yaml';
5 | import _ from 'lodash';
6 |
7 | import environment from '../environment.ts';
8 |
9 | const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
10 |
11 | /**
12 | * 系统配置
13 | */
14 | export class SystemConfig {
15 |
16 | /** 是否开启请求日志 */
17 | requestLog: boolean;
18 | /** 临时目录路径 */
19 | tmpDir: string;
20 | /** 日志目录路径 */
21 | logDir: string;
22 | /** 日志写入间隔(毫秒) */
23 | logWriteInterval: number;
24 | /** 日志文件有效期(毫秒) */
25 | logFileExpires: number;
26 | /** 公共目录路径 */
27 | publicDir: string;
28 | /** 临时文件有效期(毫秒) */
29 | tmpFileExpires: number;
30 | /** 请求体配置 */
31 | requestBody: any;
32 | /** 是否调试模式 */
33 | debug: boolean;
34 |
35 | constructor(options?: any) {
36 | const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
37 | this.requestLog = _.defaultTo(requestLog, false);
38 | this.tmpDir = _.defaultTo(tmpDir, './tmp');
39 | this.logDir = _.defaultTo(logDir, './logs');
40 | this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
41 | this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
42 | this.publicDir = _.defaultTo(publicDir, './public');
43 | this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
44 | this.requestBody = Object.assign(requestBody || {}, {
45 | enableTypes: ['json', 'form', 'text', 'xml'],
46 | encoding: 'utf-8',
47 | formLimit: '100mb',
48 | jsonLimit: '100mb',
49 | textLimit: '100mb',
50 | xmlLimit: '100mb',
51 | formidable: {
52 | maxFileSize: '100mb'
53 | },
54 | multipart: true,
55 | parsedMethods: ['POST', 'PUT', 'PATCH']
56 | });
57 | this.debug = _.defaultTo(debug, true);
58 | }
59 |
60 | get rootDirPath() {
61 | return path.resolve();
62 | }
63 |
64 | get tmpDirPath() {
65 | return path.resolve(this.tmpDir);
66 | }
67 |
68 | get logDirPath() {
69 | return path.resolve(this.logDir);
70 | }
71 |
72 | get publicDirPath() {
73 | return path.resolve(this.publicDir);
74 | }
75 |
76 | static load() {
77 | if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
78 | const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
79 | return new SystemConfig(data);
80 | }
81 |
82 | }
83 |
84 | export default SystemConfig.load();
--------------------------------------------------------------------------------
/wxkf-api/src/lib/consts/exceptions.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | SYSTEM_ERROR: [-1000, '系统异常'],
3 | SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
4 | SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
5 | } as Record
--------------------------------------------------------------------------------
/wxkf-api/src/lib/environment.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | import fs from 'fs-extra';
4 | import minimist from 'minimist';
5 | import _ from 'lodash';
6 |
7 | const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
8 | const envVars = process.env; //获取环境变量
9 |
10 | class Environment {
11 |
12 | /** 命令行参数 */
13 | cmdArgs: any;
14 | /** 环境变量 */
15 | envVars: any;
16 | /** 环境名称 */
17 | env?: string;
18 | /** 服务名称 */
19 | name?: string;
20 | /** 服务地址 */
21 | host?: string;
22 | /** 服务端口 */
23 | port?: number;
24 | /** 包参数 */
25 | package: any;
26 |
27 | constructor(options: any = {}) {
28 | const { cmdArgs, envVars, package: _package } = options;
29 | this.cmdArgs = cmdArgs;
30 | this.envVars = envVars;
31 | this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
32 | this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
33 | this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
34 | this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
35 | this.package = _package;
36 | }
37 |
38 | }
39 |
40 | export default new Environment({
41 | cmdArgs,
42 | envVars,
43 | package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
44 | });
--------------------------------------------------------------------------------
/wxkf-api/src/lib/exceptions/APIException.ts:
--------------------------------------------------------------------------------
1 | import Exception from './Exception.js';
2 |
3 | export default class APIException extends Exception {
4 |
5 | /**
6 | * 构造异常
7 | *
8 | * @param {[number, string]} exception 异常
9 | */
10 | constructor(exception: (string | number)[], errmsg?: string) {
11 | super(exception, errmsg);
12 | }
13 |
14 | }
--------------------------------------------------------------------------------
/wxkf-api/src/lib/exceptions/Exception.ts:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 |
3 | import _ from 'lodash';
4 |
5 | export default class Exception extends Error {
6 |
7 | /** 错误码 */
8 | errcode: number;
9 | /** 错误消息 */
10 | errmsg: string;
11 | /** 数据 */
12 | data: any;
13 | /** HTTP状态码 */
14 | httpStatusCode: number;
15 |
16 | /**
17 | * 构造异常
18 | *
19 | * @param exception 异常
20 | * @param _errmsg 异常消息
21 | */
22 | constructor(exception: (string | number)[], _errmsg?: string) {
23 | assert(_.isArray(exception), 'Exception must be Array');
24 | const [errcode, errmsg] = exception as [number, string];
25 | assert(_.isFinite(errcode), 'Exception errcode invalid');
26 | assert(_.isString(errmsg), 'Exception errmsg invalid');
27 | super(_errmsg || errmsg);
28 | this.errcode = errcode;
29 | this.errmsg = _errmsg || errmsg;
30 | }
31 |
32 | compare(exception: (string | number)[]) {
33 | const [errcode] = exception as [number, string];
34 | return this.errcode == errcode;
35 | }
36 |
37 | setHTTPStatusCode(value: number) {
38 | this.httpStatusCode = value;
39 | return this;
40 | }
41 |
42 | setData(value: any) {
43 | this.data = _.defaultTo(value, null);
44 | return this;
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/wxkf-api/src/lib/initialize.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 | import logger from './logger.js';
3 |
4 | fs.ensureDirSync('tmp');
5 |
6 | // 允许无限量的监听器
7 | process.setMaxListeners(Infinity);
8 | // 输出未捕获异常
9 | process.on("uncaughtException", (err, origin) => {
10 | logger.error(`An unhandled error occurred: ${origin}`, err);
11 | });
12 | // 输出未处理的Promise.reject
13 | process.on("unhandledRejection", (_, promise) => {
14 | promise.catch(err => logger.error("An unhandled rejection occurred:", err));
15 | });
16 | // 输出系统警告信息
17 | process.on("warning", warning => logger.warn("System warning: ", warning));
18 | // 进程退出监听
19 | process.on("exit", () => {
20 | logger.info("Service exit");
21 | logger.footer();
22 | });
23 | // 进程被kill
24 | process.on("SIGTERM", () => {
25 | logger.warn("received kill signal");
26 | process.exit(2);
27 | });
28 | // Ctrl-C进程退出
29 | process.on("SIGINT", () => {
30 | process.exit(0);
31 | });
--------------------------------------------------------------------------------
/wxkf-api/src/lib/interfaces/IAgentConfig.ts:
--------------------------------------------------------------------------------
1 | export default interface IAgentConfig {
2 | id: string;
3 | name: string;
4 | welcome?: string;
5 | api: string;
6 | apiKey: string;
7 | avatarUrl: string;
8 | maxRounds: number;
9 | enabled: boolean;
10 | openKfId?: string;
11 | contactUrl?: string;
12 | }
--------------------------------------------------------------------------------
/wxkf-api/src/lib/interfaces/IMessage.ts:
--------------------------------------------------------------------------------
1 | export default interface IMessage {
2 | /** 消息发送者角色 */
3 | send_role: 'user' | 'assistant';
4 | /** 消息类型 */
5 | msgtype: 'text' | 'image' | 'voice' | 'video' | 'file' | 'location' | 'miniprogram' | 'channels_shop_product' | 'channels_shop_order' | 'merged_msg' | 'channels' | 'event';
6 | /** 消息ID */
7 | msgid: string;
8 | /** 客服账号ID */
9 | open_kfid: string;
10 | /** 客户UserID */
11 | external_userid: string;
12 | /** 消息发送时间 */
13 | send_time: number;
14 | /** 消息来源,3:客户回复的消息 4:系统推送的消息 */
15 | origin: number;
16 | /** 文本消息 */
17 | text?: {
18 | /** 文本内容 */
19 | content: string,
20 | /** 客户点击菜单消息,触发的回复消息中附带的菜单ID */
21 | menu_id?: string
22 | };
23 | /** 图片消息 */
24 | image?: {
25 | /** 图片文件id */
26 | media_id: string;
27 | };
28 | /** 语音消息 */
29 | voice?: {
30 | /** 语音文件ID */
31 | media_id: string;
32 | };
33 | /** 视频消息 */
34 | video?: {
35 | /** 视频文件ID */
36 | media_id: string;
37 | };
38 | /** 文件消息 */
39 | file?: {
40 | /** 文件ID */
41 | media_id: string;
42 | };
43 | /** 位置消息 */
44 | location?: {
45 | /** 位置名 */
46 | name: string;
47 | /** 地址详情说明 */
48 | address: string;
49 | /** 纬度 */
50 | latitude: number;
51 | /** 经度 */
52 | longitude: number;
53 | };
54 | /** 小程序消息 */
55 | miniprogram?: {
56 | /** 标题 */
57 | title: string;
58 | /** 小程序appid */
59 | appid: string;
60 | /** 点击消息卡片后进入的小程序页面路径 */
61 | pagepath: string;
62 | /** 小程序消息封面的文件ID */
63 | thumb_media_id: string;
64 | };
65 | /** 视频号商品消息 */
66 | channels_shop_product?: {
67 | /** 商品标题 */
68 | title: string;
69 | /** 商品ID */
70 | product_id: string;
71 | /** 商品图片 */
72 | head_image: string;
73 | /** 商品价格,以分为单位 */
74 | sales_price: string;
75 | /** 店铺名称 */
76 | shop_nickname: string;
77 | /** 店铺头像 */
78 | shop_head_image: string;
79 | };
80 | /** 视频号订单消息 */
81 | channels_shop_order?: {
82 | /** 订单号 */
83 | order_id: string;
84 | /** 商品标题 */
85 | product_titles: string;
86 | /** 订单价格描述 */
87 | price_wording: string;
88 | /** 订单状态 */
89 | state: string;
90 | /** 订单缩略图 */
91 | image_url: string;
92 | /** 店铺名称 */
93 | shop_nickname: string;
94 | };
95 | /** 聊天记录消息 */
96 | merged_msg?: {
97 | /** 聊天记录标题 */
98 | title: string;
99 | /** 消息记录内的消息内容 */
100 | item: {
101 | /** 消息类型 */
102 | msgtype: string;
103 | /** 发送时间 */
104 | send_time: number;
105 | /** 发送者名称 */
106 | sender_name: string;
107 | /** 消息内容,Json字符串,结构可参考本文档消息类型说明 */
108 | msg_content: string;
109 | }[]
110 | },
111 | /** 视频号消息 */
112 | channels?: {
113 | /** 视频号消息类型,1:视频号动态 2:视频号直播 3:视频号名片 */
114 | sub_type: number;
115 | /** 视频号名称 */
116 | nickname: string;
117 | /** 视频号动态标题,视频号消息类型为“1视频号动态”时,返回动态标题 */
118 | title: string;
119 | },
120 | /** 事件消息 */
121 | event?: {
122 | /** 事件类型,此处固定为:enter_session */
123 | event_type: string;
124 | /** 客服账号ID */
125 | open_kfid: string;
126 | /** 客户UserID */
127 | external_userid: string;
128 | /** 进入会话的场景值,获取客服账号链接开发者自定义的场景值 */
129 | scene: string;
130 | /** 进入会话的自定义参数,获取客服账号链接返回的url,开发者按规范拼接的scene_param参数 */
131 | scene_param: string;
132 | /** 如果满足发送欢迎语条件(条件为:用户在过去48小时里未收过欢迎语,且未向客服发过消息),会返回该字段。
133 | 可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。 */
134 | welcome_code: string;
135 | /** 进入会话的视频号信息,从视频号进入会话才有值 */
136 | wechat_channels: {
137 | /** 视频号名称,视频号场景值为1、2、3时返回此项 */
138 | nickname: string;
139 | /** 视频号小店名称,视频号场景值为4、5时返回此项 */
140 | shop_nickname: string;
141 | /** 视频号场景值,1:视频号主页,2:视频号直播间商品列表页,3:视频号商品橱窗页,4:视频号小店商品详情页,5:视频号小店订单页 */
142 | scene: number;
143 | };
144 | };
145 | }
--------------------------------------------------------------------------------
/wxkf-api/src/lib/request/Request.ts:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | import APIException from "@/lib/exceptions/APIException.ts";
4 | import EX from "@/api/consts/exceptions.ts";
5 | import logger from "@/lib/logger.ts";
6 | import util from "@/lib/util.ts";
7 |
8 | export interface RequestOptions {
9 | time?: number;
10 | }
11 |
12 | export default class Request {
13 | /** 请求方法 */
14 | method: string;
15 | /** 请求URL */
16 | url: string;
17 | /** 请求路径 */
18 | path: string;
19 | /** 请求载荷类型 */
20 | type: string;
21 | /** 请求headers */
22 | headers: any;
23 | /** 请求原始查询字符串 */
24 | search: string;
25 | /** 请求查询参数 */
26 | query: any;
27 | /** 请求URL参数 */
28 | params: any;
29 | /** 请求载荷 */
30 | body: any;
31 | /** 上传的文件 */
32 | files: any[];
33 | /** 客户端IP地址 */
34 | remoteIP: string | null;
35 | /** 请求接受时间戳(毫秒) */
36 | time: number;
37 |
38 | constructor(ctx, options: RequestOptions = {}) {
39 | const { time } = options;
40 | this.method = ctx.request.method;
41 | this.url = ctx.request.url;
42 | this.path = ctx.request.path;
43 | this.type = ctx.request.type;
44 | this.headers = ctx.request.headers || {};
45 | this.search = ctx.request.search;
46 | this.query = ctx.query || {};
47 | this.params = ctx.params || {};
48 | if (
49 | this.type.startsWith("text/xml") ||
50 | this.type.startsWith("application/xml")
51 | )
52 | this.body = util.parseXML(ctx.request.body) || {};
53 | else
54 | this.body = ctx.request.body || {};
55 | this.files = ctx.request.files || {};
56 | this.remoteIP =
57 | this.headers["X-Real-IP"] ||
58 | this.headers["x-real-ip"] ||
59 | this.headers["X-Forwarded-For"] ||
60 | this.headers["x-forwarded-for"] ||
61 | ctx.ip ||
62 | null;
63 | this.time = Number(_.defaultTo(time, util.timestamp()));
64 | }
65 |
66 | validate(key: string, fn?: Function) {
67 | try {
68 | const value = _.get(this, key);
69 | if (fn) {
70 | if (fn(value) === false) throw `[Mismatch] -> ${fn}`;
71 | } else if (_.isUndefined(value)) throw "[Undefined]";
72 | } catch (err) {
73 | logger.warn(`Params ${key} invalid:`, err);
74 | throw new APIException(
75 | EX.API_REQUEST_PARAMS_INVALID,
76 | `Params ${key} invalid`
77 | );
78 | }
79 | return this;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/wxkf-api/src/lib/response/Body.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export interface BodyOptions {
4 | code?: number;
5 | message?: string;
6 | data?: any;
7 | statusCode?: number;
8 | }
9 |
10 | export default class Body {
11 |
12 | /** 状态码 */
13 | code: number;
14 | /** 状态消息 */
15 | message: string;
16 | /** 载荷 */
17 | data: any;
18 | /** HTTP状态码 */
19 | statusCode: number;
20 |
21 | constructor(options: BodyOptions = {}) {
22 | const { code, message, data, statusCode } = options;
23 | this.code = Number(_.defaultTo(code, 0));
24 | this.message = _.defaultTo(message, 'OK');
25 | this.data = _.defaultTo(data, null);
26 | this.statusCode = Number(_.defaultTo(statusCode, 200));
27 | }
28 |
29 | toObject() {
30 | return {
31 | code: this.code,
32 | message: this.message,
33 | data: this.data
34 | };
35 | }
36 |
37 | static isInstance(value) {
38 | return value instanceof Body;
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/wxkf-api/src/lib/response/FailureBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 | import Exception from '../exceptions/Exception.ts';
5 | import APIException from '../exceptions/APIException.ts';
6 | import EX from '../consts/exceptions.ts';
7 | import HTTP_STATUS_CODES from '../http-status-codes.ts';
8 |
9 | export default class FailureBody extends Body {
10 |
11 | constructor(error: APIException | Exception | Error, _data?: any) {
12 | let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
13 | if(_.isString(error))
14 | error = new Exception(EX.SYSTEM_ERROR, error);
15 | else if(error instanceof APIException || error instanceof Exception)
16 | ({ errcode, errmsg, data, httpStatusCode } = error);
17 | else if(_.isError(error))
18 | ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
19 | super({
20 | code: errcode || -1,
21 | message: errmsg || 'Internal error',
22 | data,
23 | statusCode: httpStatusCode
24 | });
25 | }
26 |
27 | static isInstance(value) {
28 | return value instanceof FailureBody;
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/wxkf-api/src/lib/response/Response.ts:
--------------------------------------------------------------------------------
1 | import mime from 'mime';
2 | import _ from 'lodash';
3 |
4 | import Body from './Body.ts';
5 | import util from '../util.ts';
6 |
7 | export interface ResponseOptions {
8 | statusCode?: number;
9 | type?: string;
10 | headers?: Record;
11 | redirect?: string;
12 | body?: any;
13 | size?: number;
14 | time?: number;
15 | }
16 |
17 | export default class Response {
18 |
19 | /** 响应HTTP状态码 */
20 | statusCode: number;
21 | /** 响应内容类型 */
22 | type: string;
23 | /** 响应headers */
24 | headers: Record;
25 | /** 重定向目标 */
26 | redirect: string;
27 | /** 响应载荷 */
28 | body: any;
29 | /** 响应载荷大小 */
30 | size: number;
31 | /** 响应时间戳 */
32 | time: number;
33 |
34 | constructor(body: any, options: ResponseOptions = {}) {
35 | const { statusCode, type, headers, redirect, size, time } = options;
36 | this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
37 | this.type = type;
38 | this.headers = headers;
39 | this.redirect = redirect;
40 | this.size = size;
41 | this.time = Number(_.defaultTo(time, util.timestamp()));
42 | this.body = body;
43 | }
44 |
45 | injectTo(ctx) {
46 | this.redirect && ctx.redirect(this.redirect);
47 | this.statusCode && (ctx.status = this.statusCode);
48 | this.type && (ctx.type = mime.getType(this.type) || this.type);
49 | const headers = this.headers || {};
50 | if(this.size && !headers["Content-Length"] && !headers["content-length"])
51 | headers["Content-Length"] = this.size;
52 | ctx.set(headers);
53 | if(Body.isInstance(this.body))
54 | ctx.body = this.body.toObject();
55 | else
56 | ctx.body = this.body;
57 | }
58 |
59 | static isInstance(value) {
60 | return value instanceof Response;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/wxkf-api/src/lib/response/SuccessfulBody.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import Body from './Body.ts';
4 |
5 | export default class SuccessfulBody extends Body {
6 |
7 | constructor(data: any, message?: string) {
8 | super({
9 | code: 0,
10 | message: _.defaultTo(message, "OK"),
11 | data
12 | });
13 | }
14 |
15 | static isInstance(value) {
16 | return value instanceof SuccessfulBody;
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/wxkf-api/src/lib/secret.ts:
--------------------------------------------------------------------------------
1 | import yaml from 'yaml';
2 | import fs from 'fs-extra';
3 | import logger from './logger.ts';
4 |
5 | if(!fs.existsSync('secret.yml')) {
6 | logger.warn('secret.yml未找到, 请重命名项目根目录下的secret.yml.template为secret.yml并填写配置');
7 | process.exit(0);
8 | }
9 |
10 | const secret: {
11 | /** 企业ID */
12 | WXKF_API_CORP_ID: string;
13 | /** Secret */
14 | WXKF_API_CORP_SECRET: string;
15 | /** Token */
16 | WXKF_API_TOKEN: string;
17 | /** EncodingAESKey */
18 | WXKF_API_ENCODING_AES_KEY: string;
19 | } = yaml.parse(fs.readFileSync('secret.yml').toString());
20 |
21 | export default secret;
--------------------------------------------------------------------------------
/wxkf-api/startup-docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | docker run -it -d --init \
6 | --name wxkf-api \
7 | --network=host \
8 | -v $(pwd)/secret.yml:/app/secret.yml:ro \
9 | -v $(pwd)/agents.yml:/app/agents.yml:ro \
10 | -v /etc/localtime:/etc/localtime:ro \
11 | --restart on-failure \
12 | dockerproxy.cn/vinlic/wxkf-api:latest
13 |
14 | echo "Startup completed!"
15 |
16 | docker logs -f wxkf-api
--------------------------------------------------------------------------------
/wxkf-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "allowImportingTsExtensions": true,
7 | "allowSyntheticDefaultImports": true,
8 | "noEmit": true,
9 | "paths": {
10 | "@/*": ["src/*"]
11 | },
12 | "outDir": "./dist"
13 | },
14 | "include": ["src/**/*", "libs.d.ts"],
15 | "exclude": ["node_modules", "dist"]
16 | }
--------------------------------------------------------------------------------