-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 78b267c
Showing
32 changed files
with
1,711 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
dist/ | ||
node_modules/ | ||
logs/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
chatCompletion: | ||
provider: zhipuai | ||
model: glm-4 | ||
apiKey: 99b6167d7f421fc2187785503d2ffe9f.WncREpjXE26ediTw | ||
contextLength: 131072 | ||
maxToken: 8192 | ||
concurrencyLimit: 5 | ||
waitReponseTimeout: 30000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# 服务名称 | ||
name: kimi-free-api | ||
# 服务绑定主机地址 | ||
host: '0.0.0.0' | ||
# 服务绑定端口 | ||
port: 8000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# 是否开启请求日志 | ||
requestLog: true | ||
# 临时目录路径 | ||
tmpDir: ./tmp | ||
# 日志目录路径 | ||
logDir: ./logs | ||
# 日志写入间隔(毫秒) | ||
logWriteInterval: 200 | ||
# 日志文件有效期(毫秒) | ||
logFileExpires: 2626560000 | ||
# 公共目录路径 | ||
publicDir: ./public | ||
# 临时文件有效期(毫秒) | ||
tmpFileExpires: 86400000 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
{ | ||
"name": "kimi-free-api", | ||
"version": "0.0.1", | ||
"description": "Kimi Free Server", | ||
"type": "module", | ||
"main": "dist/index.js", | ||
"module": "dist/index.mjs", | ||
"types": "dist/index.d.ts", | ||
"directories": { | ||
"dist": "dist" | ||
}, | ||
"files": [ | ||
"dist/" | ||
], | ||
"scripts": { | ||
"dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node dist/index.js\"", | ||
"build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public" | ||
}, | ||
"author": "Vinlic", | ||
"license": "ISC", | ||
"dependencies": { | ||
"axios": "^1.6.7", | ||
"colors": "^1.4.0", | ||
"crc-32": "^1.2.2", | ||
"cron": "^3.1.6", | ||
"date-fns": "^3.3.1", | ||
"eventsource-parser": "^1.1.2", | ||
"fs-extra": "^11.2.0", | ||
"https-proxy-agent": "^7.0.4", | ||
"koa": "^2.15.0", | ||
"koa-body": "^5.0.0", | ||
"koa-bodyparser": "^4.4.1", | ||
"koa-range": "^0.3.0", | ||
"koa-router": "^12.0.1", | ||
"koa2-cors": "^2.0.6", | ||
"lodash": "^4.17.21", | ||
"mime": "^4.0.1", | ||
"minimist": "^1.2.8", | ||
"randomstring": "^1.3.0", | ||
"socks-proxy-agent": "^8.0.2", | ||
"uuid": "^9.0.1", | ||
"yaml": "^2.3.4" | ||
}, | ||
"devDependencies": { | ||
"@types/lodash": "^4.14.202", | ||
"@types/mime": "^3.0.4", | ||
"tsup": "^8.0.2", | ||
"typescript": "^5.3.3" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export default { | ||
API_TEST: [-9999, 'API异常错误'], | ||
API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'], | ||
API_REQUEST_FAILED: [-2001, '请求失败'], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { PassThrough } from "stream"; | ||
import _ from 'lodash'; | ||
import axios, { AxiosResponse } from 'axios'; | ||
|
||
import APIException from "@/lib/exceptions/APIException.ts"; | ||
import EX from "@/api/consts/exceptions.ts"; | ||
import { createParser } from 'eventsource-parser' | ||
import logger from '@/lib/logger.ts'; | ||
import util from '@/lib/util.ts'; | ||
|
||
const TOKEN_EXPIRES = 120; | ||
let currentAccessToken: string | null = null; | ||
let currentRefreshToken: string | null = null; | ||
let latestRefreshTime = 0; | ||
|
||
function setRefreshToken(refreshToken: string) { | ||
currentRefreshToken = refreshToken || 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyLWNlbnRlciIsImV4cCI6MTcxNzY2NzkzMSwiaWF0IjoxNzA5ODkxOTMxLCJqdGkiOiJjbmxlMm1wcmRpamFpbGxzcHJuMCIsInR5cCI6InJlZnJlc2giLCJzdWIiOiJjbmVyMGgybG5sOTU3N3MzMmluZyIsInNwYWNlX2lkIjoiY25lcXA1ODNyMDdkajd1a3JqcjAiLCJhYnN0cmFjdF91c2VyX2lkIjoiY25lcXA1ODNyMDdkajd1a3JqcWcifQ.XMDecAmBq817_n3xtRqIwIlS9QQLIClS1PaVh4EY8bqhiHr8SxFxbiTEyuRuPPTnCB90eUJNc_LchLMjUo8cKA'; | ||
} | ||
|
||
async function refreshToken() { | ||
const refreshToken = currentRefreshToken; | ||
const result = await axios.get('https://kimi.moonshot.cn/api/auth/token/refresh', { | ||
headers: { | ||
Authorization: `Bearer ${refreshToken}` | ||
} | ||
}); | ||
const { | ||
access_token, | ||
refresh_token | ||
} = checkResult(result); | ||
currentAccessToken = access_token; | ||
currentRefreshToken = refresh_token; | ||
logger.info(`Current access_token: ${currentAccessToken}`); | ||
logger.info(`Current refresh_token: ${currentRefreshToken}`); | ||
logger.success('Token refresh completed'); | ||
} | ||
|
||
async function requestToken() { | ||
if (util.unixTimestamp() - latestRefreshTime > TOKEN_EXPIRES) | ||
await refreshToken(); | ||
return currentAccessToken; | ||
} | ||
|
||
async function createConversation(name: string) { | ||
const token = await requestToken(); | ||
const result = await axios.post('https://kimi.moonshot.cn/api/chat', { | ||
name, | ||
is_example: false | ||
}, { | ||
headers: { | ||
Authorization: `Bearer ${token}` | ||
} | ||
}); | ||
const { | ||
id: convId | ||
} = checkResult(result); | ||
return convId; | ||
} | ||
|
||
async function removeConversation(convId: string) { | ||
const token = await requestToken(); | ||
const result = await axios.delete(`https://kimi.moonshot.cn/api/chat/${convId}`, { | ||
headers: { | ||
Authorization: `Bearer ${token}` | ||
} | ||
}); | ||
checkResult(result); | ||
} | ||
|
||
async function createCompletionStream(messages: any[], useSearch = true) { | ||
console.log(messages); | ||
const convId = await createConversation(`cmpl-${util.uuid(false)}`); | ||
const token = await requestToken(); | ||
const result = await axios.post(`https://kimi.moonshot.cn/api/chat/${convId}/completion/stream`, { | ||
messages, | ||
use_search: useSearch | ||
}, { | ||
headers: { | ||
Authorization: `Bearer ${token}` | ||
}, | ||
responseType: 'stream' | ||
}); | ||
return createTransStream(convId, result.data); | ||
} | ||
|
||
function checkResult(result: AxiosResponse) { | ||
if(!result.data) | ||
return null; | ||
const { error_type, message } = result.data; | ||
if (!_.isString(error_type)) | ||
return result.data; | ||
console.log(result.data); | ||
throw new APIException(EX.API_REQUEST_FAILED, message); | ||
} | ||
|
||
function createTransStream(convId: string, stream: any) { | ||
const created = parseInt(performance.now() as any); | ||
const transStream = new PassThrough(); | ||
|
||
!transStream.closed && transStream.write(`data: ${JSON.stringify({ | ||
id: convId, | ||
model: 'kimi', | ||
object: 'chat.completion.chunk', | ||
choices: [ | ||
{ index: 0, delta: { role: 'assistant', content: '' }, finish_reason: null } | ||
], | ||
created | ||
})}\n\n`); | ||
const parser = createParser(event => { | ||
try { | ||
if (event.type !== "event") return; | ||
const result = _.attempt(() => JSON.parse(event.data)); | ||
if (_.isError(result)) | ||
throw new Error(`stream response invalid: ${event.data}`); | ||
if (result.event == 'cmpl') { | ||
const data = `data: ${JSON.stringify({ | ||
id: convId, | ||
model: 'kimi', | ||
object: 'chat.completion.chunk', | ||
choices: [ | ||
{ index: 0, delta: { content: result.text }, finish_reason: null } | ||
], | ||
created | ||
})}\n\n`; | ||
!transStream.closed && transStream.write(data); | ||
} | ||
else if (result.event == 'all_done') { | ||
const data = `data: ${JSON.stringify({ | ||
id: convId, | ||
model: 'kimi', | ||
object: 'chat.completion.chunk', | ||
choices: [ | ||
{ index: 0, delta: {}, finish_reason: 'stop' } | ||
], | ||
created | ||
})}\n\n`; | ||
!transStream.closed && transStream.write(data); | ||
!transStream.closed && transStream.end('[DONE]'); | ||
removeConversation(convId).catch(err => console.error(err)); | ||
} | ||
} | ||
catch (err) { | ||
logger.error(err); | ||
!transStream.closed && transStream.end('\n\n'); | ||
} | ||
}); | ||
stream.on("data", buffer => parser.feed(buffer.toString())); | ||
stream.once("error", () => !transStream.closed && transStream.end('[DONE]')); | ||
stream.once("close", () => !transStream.closed && transStream.end('[DONE]')); | ||
return transStream; | ||
} | ||
|
||
export default { | ||
setRefreshToken, | ||
refreshToken, | ||
createConversation, | ||
createCompletionStream | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import _ from 'lodash'; | ||
|
||
import Request from '@/lib/request/Request.ts'; | ||
import Response from '@/lib/response/Response.ts'; | ||
import chat from '@/api/controllers/chat.ts'; | ||
|
||
export default { | ||
|
||
prefix: '/v1/chat', | ||
|
||
post: { | ||
|
||
'/completions': async (request: Request) => { | ||
request | ||
.validate('body.messages', _.isArray) | ||
chat.setRefreshToken(request.body.refresh_token); | ||
const stream = await chat.createCompletionStream(request.body.messages, request.body.use_search); | ||
return new Response(stream, { | ||
type: "text/event-stream" | ||
}); | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import chat from "./chat.ts"; | ||
|
||
export default [ | ||
chat | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/** | ||
* 守护进程 | ||
*/ | ||
|
||
import process from 'process'; | ||
import path from 'path'; | ||
import { spawn } from 'child_process'; | ||
|
||
import fs from 'fs-extra'; | ||
import { format as dateFormat } from 'date-fns'; | ||
import 'colors'; | ||
|
||
const CRASH_RESTART_LIMIT = 600; //进程崩溃重启次数限制 | ||
const CRASH_RESTART_DELAY = 5000; //进程崩溃重启延迟 | ||
const LOG_PATH = path.resolve("./logs/daemon.log"); //守护进程日志路径 | ||
let crashCount = 0; //进程崩溃次数 | ||
let currentProcess; //当前运行进程 | ||
|
||
/** | ||
* 写入守护进程日志 | ||
*/ | ||
function daemonLog(value, color?: string) { | ||
try { | ||
const head = `[daemon][${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")}] `; | ||
value = head + value; | ||
console.log(color ? value[color] : value); | ||
fs.ensureDirSync(path.dirname(LOG_PATH)); | ||
fs.appendFileSync(LOG_PATH, value + "\n"); | ||
} | ||
catch(err) { | ||
console.error("daemon log write error:", err); | ||
} | ||
} | ||
|
||
daemonLog(`daemon pid: ${process.pid}`); | ||
|
||
function createProcess() { | ||
const childProcess = spawn("node", ["index.js", ...process.argv.slice(2)]); //启动子进程 | ||
childProcess.stdout.pipe(process.stdout, { end: false }); //将子进程输出管道到当前进程输出 | ||
childProcess.stderr.pipe(process.stderr, { end: false }); //将子进程错误输出管道到当前进程输出 | ||
currentProcess = childProcess; //更新当前进程 | ||
daemonLog(`process(${childProcess.pid}) has started`); | ||
childProcess.on("error", err => daemonLog(`process(${childProcess.pid}) error: ${err.stack}`, "red")); | ||
childProcess.on("close", code => { | ||
if(code === 0) //进程正常退出 | ||
daemonLog(`process(${childProcess.pid}) has exited`); | ||
else if(code === 2) //进程已被杀死 | ||
daemonLog(`process(${childProcess.pid}) has been killed!`, "bgYellow"); | ||
else if(code === 3) { //进程主动重启 | ||
daemonLog(`process(${childProcess.pid}) has restart`, "yellow"); | ||
createProcess(); //重新创建进程 | ||
} | ||
else { //进程发生崩溃 | ||
if(crashCount++ < CRASH_RESTART_LIMIT) { //进程崩溃次数未达重启次数上限前尝试重启 | ||
daemonLog(`process(${childProcess.pid}) has crashed! delay ${CRASH_RESTART_DELAY}ms try restarting...(${crashCount})`, "bgRed"); | ||
setTimeout(() => createProcess(), CRASH_RESTART_DELAY); //延迟指定时长后再重启 | ||
} | ||
else //进程已崩溃,且无法重启 | ||
daemonLog(`process(${childProcess.pid}) has crashed! unable to restart`, "bgRed"); | ||
} | ||
}); //子进程关闭监听 | ||
} | ||
|
||
process.on("exit", code => { | ||
if(code === 0) | ||
daemonLog("daemon process exited"); | ||
else if(code === 2) | ||
daemonLog("daemon process has been killed!"); | ||
}); //守护进程退出事件 | ||
|
||
process.on("SIGTERM", () => { | ||
daemonLog("received kill signal", "yellow"); | ||
currentProcess && currentProcess.kill("SIGINT"); | ||
process.exit(2); | ||
}); //kill退出守护进程 | ||
|
||
process.on("SIGINT", () => { | ||
currentProcess && currentProcess.kill("SIGINT"); | ||
process.exit(0); | ||
}); //主动退出守护进程 | ||
|
||
createProcess(); //创建进程 |
Oops, something went wrong.