├── 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 |
68 |
69 | 70 |
71 |
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![MEME](${imageUrl})`; 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 | ![](./doc/example-9.png) 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 | ![](./doc/example-9.png) 63 | 64 | ## 启用微信客服 65 | 66 | 前往微信客服([https://kf.weixin.qq.com/](https://kf.weixin.qq.com/))扫码登录(首先你得是企业微信管理员)。 67 | 68 | 进入开发配置选项,点击“开始使用”启用企业内部接入。 69 | 70 | ![](./doc/example-1.png) 71 | 72 | 回调地址请填写 [https://example.com/message/notify](https://example.com/message/notify)(域名请替换为你的,如果你没有域名请替换为IP地址和端口同时协议改为http,地址是指向本服务部署地址)。 73 | 74 | Token和EncodingAESKey可以自行填写或随机生成并保留记录。 75 | 76 | ![](./doc/example-2.png) 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 | ![](./doc/example-10.png) 111 | 112 | 113 | 点击“完成”按钮,完成服务器回调的验证。 114 | 115 | 验证通过后会进入以下界面: 116 | 117 | ![](./doc/example-3.png) 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 | ![](./doc/example-11.png) 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 | ![](./doc/example-8.png) 232 | 233 | 打开微信客服([https://kf.weixin.qq.com/](https://kf.weixin.qq.com/))的客服账号,可以看到自动创建好的智能体列表。 234 | 235 | ![](./doc/example-4.png) 236 | 237 | ## 验证功能 238 | 239 | 通过客服链接或卡片进入后,就可以直接与智能体对话了! 240 | 241 | ![](./doc/example-5.png) 242 | 243 | ## 分享卡片 244 | 245 | 你可以将智能体客服分享为卡片。 246 | 247 | ![](./doc/example-7.png) 248 | 249 | 点开客服账号右上角,再点击头像,再点击下图右上角位置即可将智能体客服以卡片形式分享给好友。 250 | 251 | ![](./doc/example-6.png) 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 | } --------------------------------------------------------------------------------