支持长文档解读和图像解析

This commit is contained in:
Vinlic 2024-04-05 02:19:25 +08:00
parent eab1767a4e
commit 7031611320
5 changed files with 376 additions and 51 deletions

126
README.md
View File

@ -5,7 +5,7 @@
![](https://img.shields.io/github/forks/llm-red-team/qwen-free-api.svg)
![](https://img.shields.io/docker/pulls/vinlic/qwen-free-api.svg)
支持高速流式输出、支持多轮对话、支持无水印AI绘图、支持长文档解读(正在开发)、图像解析(正在开发)零配置部署多路token支持自动清理会话痕迹。
支持高速流式输出、支持多轮对话、支持无水印AI绘图、支持长文档解读、图像解析零配置部署多路token支持自动清理会话痕迹。
与ChatGPT接口完全兼容。
@ -66,11 +66,11 @@ https://udify.app/chat/qOXzVl5kkvhQXM8r
### 长文档解读
正在开发...
![AI绘图](./doc/example-5.png)
### 图像解析
正在开发...
![AI绘图](./doc/example-6.png)
### 10线程并发测试
@ -276,11 +276,127 @@ Authorization: Bearer [login_tongyi_ticket]
### 文档解读
正在开发...
提供一个可访问的文件URL或者BASE64_URL进行解析。
**POST /v1/chat/completions**
header 需要设置 Authorization 头部:
```
Authorization: Bearer [refresh_token]
```
请求数据:
```json
{
"model": "qwen",
"messages": [
{
"role": "user",
"content": [
{
"type": "file",
"file_url": {
"url": "https://mj101-1317487292.cos.ap-shanghai.myqcloud.com/ai/test.pdf"
}
},
{
"type": "text",
"text": "文档里说了什么?"
}
]
}
]
}
```
响应数据:
```json
{
"id": "b56ea6c9e86140429fa2de6a6ec028ff",
"model": "qwen",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "文档中包含了四个古代魔法仪式或咒语的描述,它们似乎旨在影响或控制一个特定女性的情感和行为,使其对施术者产生强烈的爱意。以下是每个仪式的具体内容:\n\n1. **仪式一**PMG 4.1390 1495\n - 施术者需留下一些面包,将其掰成七小块。\n - 前往一处英雄、角斗士或其他暴力死亡者丧生的地方。\n - 对着面包碎片念诵咒语后丢弃,并从该地取一些受污染的泥土扔进目标女性的住所。\n - 咒语内容包括向命运三女神Moirai、罗马版的命运女神Fates、自然力量Daemons、饥荒与嫉妒之神以及非正常死亡者献祭食物并请求他们以痛苦折磨目标使她在梦中惊醒心生忧虑与恐惧最终跟随施术者的步伐并顺从其意愿。此过程以赫卡忒Hecate女神为命令的源泉。\n\n2. **仪式二**PMG 4.1342 57\n - 施术者召唤恶魔Daemon通过一系列神秘的神祇名号如Erekisephthe Araracharara Ephthesikere要求其将名为Tereous的女子Apia所生带至施术者DidymosTaipiam所生身边。\n - 请求该女子在灵魂、心智及女性器官上遭受剧烈痛苦直至她主动找寻Didymos并与之紧密相连唇对唇、发对发、腹部对腹部。整个过程要求立即执行。\n\n3. **仪式三**PGM 4.1265 74\n - 揭示了阿佛洛狄忒Aphrodite鲜为人知的名字——NEPHERIĒRI[nfr-iry-t]。\n - 如果想赢得一位美丽女子的芳心,施术者应保持三天纯净,献上乳香,并在心中默念该名字七次。\n - 这样的做法需持续七天,据说这样便能成功吸引女子。\n\n4. **仪式四**PGM 4.1496 1\n - 施术者在燃烧的煤炭上供奉没药myrrh同时念诵咒语。\n - 咒语将没药称为“苦涩的调和者”、“热力的激发者”,并命令它前往指定的女子(及其母亲的名字)处,阻止她进行日常活动(如坐、饮、食、注视他人、亲吻他人),迫使她心中只有施术者,对其产生强烈的欲望与爱意。\n - 咒语还指示没药直接穿透女子的灵魂,驻留在其心中,焚烧其内脏、胸部、肝脏、气息、骨骼、骨髓,直到她来到施术者身边。\n\n这些仪式反映了古代魔法实践中试图借助超自然力量操控他人情感与行为的企图涉及对神灵、恶魔、神秘名字及特定物质如面包、泥土、乳香、没药的运用通常伴随着严格的仪式规程和咒语念诵。此类行为在现代伦理和法律框架下被视为不恰当甚至违法且缺乏科学依据。"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 2
},
"created": 1712253736
}
```
### 图像解析
正在开发...
提供一个可访问的图像URL或者BASE64_URL进行解析。
此格式兼容 [gpt-4-vision-preview](https://platform.openai.com/docs/guides/vision) API格式您也可以用这个格式传送文档进行解析。
**POST /v1/chat/completions**
header 需要设置 Authorization 头部:
```
Authorization: Bearer [refresh_token]
```
请求数据:
```json
{
"model": "qwen",
"messages": [
{
"role": "user",
"content": [
{
"type": "file",
"file_url": {
"url": "https://img.alicdn.com/imgextra/i1/O1CN01CC9kic1ig1r4sAY5d_!!6000000004441-2-tps-880-210.png"
}
},
{
"type": "text",
"text": "图像描述了什么?"
}
]
}
]
}
```
响应数据:
```json
{
"id": "895fbe7fa22442d499ba67bb5213e842",
"model": "qwen",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "图像展示了通义千问的标志,一个紫色的六边形和一个蓝色的三角形,以及“通义千问”四个白色的汉字。"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 2
},
"created": 1712254066
}
```
## 注意事项

BIN
doc/example-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

BIN
doc/example-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -26,6 +26,7 @@
"cron": "^3.1.6",
"date-fns": "^3.3.1",
"eventsource-parser": "^1.1.2",
"form-data": "^4.0.0",
"fs-extra": "^11.2.0",
"koa": "^2.15.0",
"koa-body": "^5.0.0",

View File

@ -4,6 +4,7 @@ import http2 from "http2";
import path from "path";
import _ from "lodash";
import mime from "mime";
import FormData from "form-data";
import axios, { AxiosResponse } from "axios";
import APIException from "@/lib/exceptions/APIException.ts";
@ -87,18 +88,20 @@ async function createCompletion(
// 提取引用文件URL并上传qwen获得引用的文件ID列表
const refFileUrls = extractRefFileUrls(messages);
// const refs = refFileUrls.length
// ? await Promise.all(
// refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket))
// )
// : [];
const refs = refFileUrls.length
? await Promise.all(
refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket))
)
: [];
// 请求流
const session: http2.ClientHttp2Session = await new Promise((resolve, reject) => {
const session = http2.connect("https://qianwen.biz.aliyun.com");
session.on('connect', () => resolve(session));
session.on("error", reject);
});
const session: http2.ClientHttp2Session = await new Promise(
(resolve, reject) => {
const session = http2.connect("https://qianwen.biz.aliyun.com");
session.on("connect", () => resolve(session));
session.on("error", reject);
}
);
const req = session.request({
":method": "POST",
":path": "/dialog/conversation",
@ -118,7 +121,7 @@ async function createCompletion(
sessionId: "",
sessionType: "text_chat",
parentMsgId: "",
contents: messagesPrepare(messages),
contents: messagesPrepare(messages, refs),
})
);
req.setEncoding("utf8");
@ -169,16 +172,16 @@ async function createCompletionStream(
// 提取引用文件URL并上传qwen获得引用的文件ID列表
const refFileUrls = extractRefFileUrls(messages);
// const refs = refFileUrls.length
// ? await Promise.all(
// refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket))
// )
// : [];
const refs = refFileUrls.length
? await Promise.all(
refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket))
)
: [];
// 请求流
session = await new Promise((resolve, reject) => {
const session = http2.connect("https://qianwen.biz.aliyun.com");
session.on('connect', () => resolve(session));
session.on("connect", () => resolve(session));
session.on("error", reject);
});
const req = session.request({
@ -200,7 +203,7 @@ async function createCompletionStream(
sessionId: "",
sessionType: "text_chat",
parentMsgId: "",
contents: messagesPrepare(messages),
contents: messagesPrepare(messages, refs),
})
);
req.setEncoding("utf8");
@ -307,29 +310,34 @@ async function generateImages(
* @param messages gpt系列消息格式
*/
function extractRefFileUrls(messages: any[]) {
return messages.reduce((urls, message) => {
if (_.isArray(message.content)) {
message.content.forEach((v) => {
if (!_.isObject(v) || !["file", "image_url"].includes(v["type"]))
return;
// qwen-free-api支持格式
if (
v["type"] == "file" &&
_.isObject(v["file_url"]) &&
_.isString(v["file_url"]["url"])
)
urls.push(v["file_url"]["url"]);
// 兼容gpt-4-vision-preview API格式
else if (
v["type"] == "image_url" &&
_.isObject(v["image_url"]) &&
_.isString(v["image_url"]["url"])
)
urls.push(v["image_url"]["url"]);
});
}
const urls = [];
// 如果没有消息,则返回[]
if (!messages.length) {
return urls;
}, []);
}
// 只获取最新的消息
const lastMessage = messages[messages.length - 1];
if (_.isArray(lastMessage.content)) {
lastMessage.content.forEach((v) => {
if (!_.isObject(v) || !["file", "image_url"].includes(v["type"])) return;
// glm-free-api支持格式
if (
v["type"] == "file" &&
_.isObject(v["file_url"]) &&
_.isString(v["file_url"]["url"])
)
urls.push(v["file_url"]["url"]);
// 兼容gpt-4-vision-preview API格式
else if (
v["type"] == "image_url" &&
_.isObject(v["image_url"]) &&
_.isString(v["image_url"]["url"])
)
urls.push(v["image_url"]["url"]);
});
}
logger.info("本次请求上传:" + urls.length + "个文件");
return urls;
}
/**
@ -342,12 +350,12 @@ function extractRefFileUrls(messages: any[]) {
*
* @param messages gpt系列消息格式
*/
function messagesPrepare(messages: any[]) {
function messagesPrepare(messages: any[], refs: any[] = []) {
const content = messages.reduce((content, message) => {
if (_.isArray(message.content)) {
return message.content.reduce((_content, v) => {
if (!_.isObject(v) || v["type"] != "text") return _content;
return _content + (v["text"] || "");
return _content + `<|im_start|>${message.role || "user"}\n${v["text"] || ""}<|im_end|>\n`;
}, content);
}
return (content += `<|im_start|>${message.role || "user"}\n${
@ -361,6 +369,7 @@ function messagesPrepare(messages: any[]) {
contentType: "text",
content,
},
...refs
];
}
@ -451,10 +460,7 @@ async function receiveStream(stream: any): Promise<any> {
}
});
// 将流数据喂给SSE转换器
stream.on("data", (buffer) => {
console.log(buffer.toString());
parser.feed(buffer.toString());
});
stream.on("data", (buffer) => parser.feed(buffer.toString()));
stream.once("error", (err) => reject(err));
stream.once("close", () => resolve(data));
stream.end();
@ -641,6 +647,208 @@ async function receiveImages(
});
}
/**
*
*
* @param ticket login_tongyi_ticket值
*/
async function acquireUploadParams(ticket: string) {
const result = await axios.post(
"https://qianwen.biz.aliyun.com/dialog/uploadToken",
{},
{
timeout: 15000,
headers: {
Cookie: generateCookie(ticket),
...FAKE_HEADERS,
},
validateStatus: () => true,
}
);
const { data } = checkResult(result);
return data;
}
/**
* URL有效性
*
* @param fileUrl URL
*/
async function checkFileUrl(fileUrl: string) {
if (util.isBASE64Data(fileUrl)) return;
const result = await axios.head(fileUrl, {
timeout: 15000,
validateStatus: () => true,
});
if (result.status >= 400)
throw new APIException(
EX.API_FILE_URL_INVALID,
`File ${fileUrl} is not valid: [${result.status}] ${result.statusText}`
);
// 检查文件大小
if (result.headers && result.headers["content-length"]) {
const fileSize = parseInt(result.headers["content-length"], 10);
if (fileSize > FILE_MAX_SIZE)
throw new APIException(
EX.API_FILE_EXECEEDS_SIZE,
`File ${fileUrl} is not valid`
);
}
}
/**
*
*
* @param fileUrl URL
* @param ticket login_tongyi_ticket值
*/
async function uploadFile(fileUrl: string, ticket: string) {
// 预检查远程文件URL可用性
await checkFileUrl(fileUrl);
let filename, fileData, mimeType;
// 如果是BASE64数据则直接转换为Buffer
if (util.isBASE64Data(fileUrl)) {
mimeType = util.extractBASE64DataFormat(fileUrl);
const ext = mime.getExtension(mimeType);
filename = `${util.uuid()}.${ext}`;
fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), "base64");
}
// 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存
else {
filename = path.basename(fileUrl);
({ data: fileData } = await axios.get(fileUrl, {
responseType: "arraybuffer",
// 100M限制
maxContentLength: FILE_MAX_SIZE,
// 60秒超时
timeout: 60000,
}));
}
// 获取文件的MIME类型
mimeType = mimeType || mime.getType(filename);
// 获取上传参数
const { accessId, policy, signature, dir } = await acquireUploadParams(
ticket
);
const formData = new FormData();
formData.append("OSSAccessKeyId", accessId);
formData.append("policy", policy);
formData.append("signature", signature);
formData.append("key", `${dir}${filename}`);
formData.append("dir", dir);
formData.append("success_action_status", "200");
formData.append("file", fileData, {
filename,
contentType: mimeType,
});
// 上传文件到OSS
await axios.request({
method: "POST",
url: "https://broadscope-dialogue.oss-cn-beijing.aliyuncs.com/",
data: formData,
// 100M限制
maxBodyLength: FILE_MAX_SIZE,
// 60秒超时
timeout: 120000,
headers: {
...FAKE_HEADERS,
"X-Requested-With": "XMLHttpRequest"
}
});
const isImage = [
'image/jpeg',
'image/jpg',
'image/tiff',
'image/png',
'image/bmp',
'image/gif'
].includes(mimeType);
if(isImage) {
const result = await axios.post(
"https://qianwen.biz.aliyun.com/dialog/downloadLink",
{
fileKey: filename,
fileType: "image",
dir
},
{
timeout: 15000,
headers: {
Cookie: generateCookie(ticket),
...FAKE_HEADERS,
},
validateStatus: () => true,
}
);
const { data } = checkResult(result);
return {
role: "user",
contentType: "image",
content: data.url
};
}
else {
let result = await axios.post(
"https://qianwen.biz.aliyun.com/dialog/downloadLink/batch",
{
fileKeys: [filename],
fileType: "file",
dir
},
{
timeout: 15000,
headers: {
Cookie: generateCookie(ticket),
...FAKE_HEADERS,
},
validateStatus: () => true,
}
);
const { data } = checkResult(result);
if(!data.results[0] || !data.results[0].url)
throw new Error(`文件上传失败:${data.results[0] ? data.results[0].errorMsg : '未知错误'}`);
const url = data.results[0].url;
const startTime = util.timestamp();
while(true) {
result = await axios.post(
"https://qianwen.biz.aliyun.com/dialog/secResult/batch",
{
urls: [url]
},
{
timeout: 15000,
headers: {
Cookie: generateCookie(ticket),
...FAKE_HEADERS,
},
validateStatus: () => true,
}
);
const { data } = checkResult(result);
if(data.pollEndFlag) {
if(data.statusList[0] && data.statusList[0].status === 0)
throw new Error(`文件处理失败:${data.statusList[0].errorMsg || '未知错误'}`);
break;
}
if(util.timestamp() > startTime + 120000)
throw new Error("文件处理超时超出120秒");
}
return {
role: "user",
contentType: "file",
content: url,
ext: { fileSize: fileData.byteLength }
};
}
}
/**
* Token切分
*