|
import { URL } from "url"; |
|
import { PassThrough } from "stream"; |
|
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"; |
|
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 MODEL_NAME = "qwen"; |
|
|
|
const MAX_RETRY_COUNT = 3; |
|
|
|
const RETRY_DELAY = 5000; |
|
|
|
const FAKE_HEADERS = { |
|
Accept: "application/json, text/plain, */*", |
|
"Accept-Encoding": "gzip, deflate, br, zstd", |
|
"Accept-Language": "zh-CN,zh;q=0.9", |
|
"Cache-Control": "no-cache", |
|
Origin: "https://tongyi.aliyun.com", |
|
Pragma: "no-cache", |
|
"Sec-Ch-Ua": |
|
'"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', |
|
"Sec-Ch-Ua-Mobile": "?0", |
|
"Sec-Ch-Ua-Platform": '"Windows"', |
|
"Sec-Fetch-Dest": "empty", |
|
"Sec-Fetch-Mode": "cors", |
|
"Sec-Fetch-Site": "same-site", |
|
Referer: "https://tongyi.aliyun.com/", |
|
"User-Agent": |
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", |
|
"X-Platform": "pc_tongyi", |
|
"X-Xsrf-Token": "48b9ee49-a184-45e2-9f67-fa87213edcdc", |
|
}; |
|
|
|
const FILE_MAX_SIZE = 100 * 1024 * 1024; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function removeConversation(convId: string, ticket: string) { |
|
const result = await axios.post( |
|
`https://qianwen.biz.aliyun.com/dialog/session/delete`, |
|
{ |
|
sessionId: convId, |
|
}, |
|
{ |
|
headers: { |
|
Cookie: generateCookie(ticket), |
|
...FAKE_HEADERS, |
|
}, |
|
timeout: 15000, |
|
validateStatus: () => true, |
|
} |
|
); |
|
checkResult(result); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function createCompletion( |
|
model = MODEL_NAME, |
|
messages: any[], |
|
ticket: string, |
|
refConvId = '', |
|
retryCount = 0 |
|
) { |
|
let session: http2.ClientHttp2Session; |
|
return (async () => { |
|
logger.info(messages); |
|
|
|
|
|
const refFileUrls = extractRefFileUrls(messages); |
|
const refs = refFileUrls.length |
|
? await Promise.all( |
|
refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket)) |
|
) |
|
: []; |
|
|
|
|
|
if (!/[0-9a-z]{32}/.test(refConvId)) |
|
refConvId = ''; |
|
|
|
|
|
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 [sessionId, parentMsgId = ''] = refConvId.split('-'); |
|
const req = session.request({ |
|
":method": "POST", |
|
":path": "/dialog/conversation", |
|
"Content-Type": "application/json", |
|
Cookie: generateCookie(ticket), |
|
...FAKE_HEADERS, |
|
Accept: "text/event-stream", |
|
}); |
|
req.setTimeout(120000); |
|
req.write( |
|
JSON.stringify({ |
|
mode: "chat", |
|
model: "", |
|
action: "next", |
|
userAction: "chat", |
|
requestId: util.uuid(false), |
|
sessionId, |
|
sessionType: "text_chat", |
|
parentMsgId, |
|
params: { |
|
"fileUploadBatchId": util.uuid() |
|
}, |
|
contents: messagesPrepare(messages, refs, !!refConvId), |
|
}) |
|
); |
|
req.setEncoding("utf8"); |
|
const streamStartTime = util.timestamp(); |
|
|
|
const answer = await receiveStream(req); |
|
session.close(); |
|
logger.success( |
|
`Stream has completed transfer ${util.timestamp() - streamStartTime}ms` |
|
); |
|
|
|
|
|
removeConversation(answer.id, ticket).catch((err) => console.error(err)); |
|
|
|
return answer; |
|
})().catch((err) => { |
|
session && session.close(); |
|
if (retryCount < MAX_RETRY_COUNT) { |
|
logger.error(`Stream response error: ${err.message}`); |
|
logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); |
|
return (async () => { |
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); |
|
return createCompletion(model, messages, ticket, refConvId, retryCount + 1); |
|
})(); |
|
} |
|
throw err; |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function createCompletionStream( |
|
model = MODEL_NAME, |
|
messages: any[], |
|
ticket: string, |
|
refConvId = '', |
|
retryCount = 0 |
|
) { |
|
let session: http2.ClientHttp2Session; |
|
return (async () => { |
|
logger.info(messages); |
|
|
|
|
|
const refFileUrls = extractRefFileUrls(messages); |
|
const refs = refFileUrls.length |
|
? await Promise.all( |
|
refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket)) |
|
) |
|
: []; |
|
|
|
|
|
if (!/[0-9a-z]{32}/.test(refConvId)) |
|
refConvId = '' |
|
|
|
|
|
session = await new Promise((resolve, reject) => { |
|
const session = http2.connect("https://qianwen.biz.aliyun.com"); |
|
session.on("connect", () => resolve(session)); |
|
session.on("error", reject); |
|
}); |
|
const [sessionId, parentMsgId = ''] = refConvId.split('-'); |
|
const req = session.request({ |
|
":method": "POST", |
|
":path": "/dialog/conversation", |
|
"Content-Type": "application/json", |
|
Cookie: generateCookie(ticket), |
|
...FAKE_HEADERS, |
|
Accept: "text/event-stream", |
|
}); |
|
req.setTimeout(120000); |
|
req.write( |
|
JSON.stringify({ |
|
mode: "chat", |
|
model: "", |
|
action: "next", |
|
userAction: "chat", |
|
requestId: util.uuid(false), |
|
sessionId, |
|
sessionType: "text_chat", |
|
parentMsgId, |
|
params: { |
|
"fileUploadBatchId": util.uuid() |
|
}, |
|
contents: messagesPrepare(messages, refs, !!refConvId), |
|
}) |
|
); |
|
req.setEncoding("utf8"); |
|
const streamStartTime = util.timestamp(); |
|
|
|
return createTransStream(req, (convId: string) => { |
|
|
|
session.close(); |
|
logger.success( |
|
`Stream has completed transfer ${util.timestamp() - streamStartTime}ms` |
|
); |
|
|
|
removeConversation(convId, ticket).catch((err) => console.error(err)); |
|
}); |
|
})().catch((err) => { |
|
session && session.close(); |
|
if (retryCount < MAX_RETRY_COUNT) { |
|
logger.error(`Stream response error: ${err.message}`); |
|
logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); |
|
return (async () => { |
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); |
|
return createCompletionStream(model, messages, ticket, refConvId, retryCount + 1); |
|
})(); |
|
} |
|
throw err; |
|
}); |
|
} |
|
|
|
async function generateImages( |
|
model = MODEL_NAME, |
|
prompt: string, |
|
ticket: string, |
|
retryCount = 0 |
|
) { |
|
let session: http2.ClientHttp2Session; |
|
return (async () => { |
|
const messages = [ |
|
{ role: "user", content: prompt.indexOf('画') == -1 ? `请画:${prompt}` : prompt }, |
|
]; |
|
|
|
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", |
|
"Content-Type": "application/json", |
|
Cookie: generateCookie(ticket), |
|
...FAKE_HEADERS, |
|
Accept: "text/event-stream", |
|
}); |
|
req.setTimeout(120000); |
|
req.write( |
|
JSON.stringify({ |
|
mode: "chat", |
|
model: "", |
|
action: "next", |
|
userAction: "chat", |
|
requestId: util.uuid(false), |
|
sessionId: "", |
|
sessionType: "text_chat", |
|
parentMsgId: "", |
|
params: { |
|
"fileUploadBatchId": util.uuid() |
|
}, |
|
contents: messagesPrepare(messages), |
|
}) |
|
); |
|
req.setEncoding("utf8"); |
|
const streamStartTime = util.timestamp(); |
|
|
|
const { convId, imageUrls } = await receiveImages(req); |
|
session.close(); |
|
logger.success( |
|
`Stream has completed transfer ${util.timestamp() - streamStartTime}ms` |
|
); |
|
|
|
|
|
removeConversation(convId, ticket).catch((err) => console.error(err)); |
|
|
|
return imageUrls; |
|
})().catch((err) => { |
|
session && session.close(); |
|
if (retryCount < MAX_RETRY_COUNT) { |
|
logger.error(`Stream response error: ${err.message}`); |
|
logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); |
|
return (async () => { |
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); |
|
return generateImages(model, prompt, ticket, retryCount + 1); |
|
})(); |
|
} |
|
throw err; |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function extractRefFileUrls(messages: any[]) { |
|
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; |
|
|
|
if ( |
|
v["type"] == "file" && |
|
_.isObject(v["file_url"]) && |
|
_.isString(v["file_url"]["url"]) |
|
) |
|
urls.push(v["file_url"]["url"]); |
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function messagesPrepare(messages: any[], refs: any[] = [], isRefConv = false) { |
|
let content; |
|
if (isRefConv || messages.length < 2) { |
|
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"] || "") + "\n"; |
|
}, content) |
|
); |
|
} |
|
return content + `${message.content}\n`; |
|
}, ""); |
|
logger.info("\n透传内容:\n" + content); |
|
} |
|
else { |
|
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 + `<|im_start|>${message.role || "user"}\n${v["text"] || ""}<|im_end|>\n`; |
|
}, content); |
|
} |
|
return (content += `<|im_start|>${message.role || "user"}\n${ |
|
message.content |
|
}<|im_end|>\n`); |
|
}, "").replace(/\!\[.*\]\(.+\)/g, ""); |
|
logger.info("\n对话合并:\n" + content); |
|
} |
|
return [ |
|
{ |
|
content, |
|
contentType: "text", |
|
role: "user", |
|
}, |
|
...refs |
|
]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function checkResult(result: AxiosResponse) { |
|
if (!result.data) return null; |
|
const { success, errorCode, errorMsg } = result.data; |
|
if (!_.isBoolean(success) || success) return result.data; |
|
throw new APIException( |
|
EX.API_REQUEST_FAILED, |
|
`[请求qwen失败]: ${errorCode}-${errorMsg}` |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async function receiveStream(stream: any): Promise<any> { |
|
return new Promise((resolve, reject) => { |
|
|
|
const data = { |
|
id: "", |
|
model: MODEL_NAME, |
|
object: "chat.completion", |
|
choices: [ |
|
{ |
|
index: 0, |
|
message: { role: "assistant", content: "" }, |
|
finish_reason: "stop", |
|
}, |
|
], |
|
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, |
|
created: util.unixTimestamp(), |
|
}; |
|
const parser = createParser((event) => { |
|
try { |
|
if (event.type !== "event") return; |
|
if (event.data == "[DONE]") return; |
|
|
|
const result = _.attempt(() => JSON.parse(event.data)); |
|
if (_.isError(result)) |
|
throw new Error(`Stream response invalid: ${event.data}`); |
|
if (!data.id && result.sessionId && result.msgId) |
|
data.id = `${result.sessionId}-${result.msgId}`; |
|
const text = (result.contents || []).reduce((str, part) => { |
|
const { contentType, role, content } = part; |
|
if (contentType != "text" && contentType != "text2image") return str; |
|
if (role != "assistant" && !_.isString(content)) return str; |
|
return str + content; |
|
}, ""); |
|
const exceptCharIndex = text.indexOf("�"); |
|
let chunk = text.substring( |
|
exceptCharIndex != -1 |
|
? Math.min(data.choices[0].message.content.length, exceptCharIndex) |
|
: data.choices[0].message.content.length, |
|
exceptCharIndex == -1 ? text.length : exceptCharIndex |
|
); |
|
if (chunk && result.contentType == "text2image") { |
|
chunk = chunk.replace( |
|
/https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi, |
|
(url) => { |
|
const urlObj = new URL(url); |
|
urlObj.search = ""; |
|
return urlObj.toString(); |
|
} |
|
); |
|
} |
|
if (result.msgStatus != "finished") { |
|
if (result.contentType == "text") |
|
data.choices[0].message.content += chunk; |
|
} else { |
|
data.choices[0].message.content += chunk; |
|
if (!result.canShare) |
|
data.choices[0].message.content += |
|
"\n[内容由于不合规被停止生成,我们换个话题吧]"; |
|
if (result.errorCode) |
|
data.choices[0].message.content += `服务暂时不可用,第三方响应错误:${result.errorCode}`; |
|
resolve(data); |
|
} |
|
} catch (err) { |
|
logger.error(err); |
|
reject(err); |
|
} |
|
}); |
|
|
|
stream.on("data", (buffer) => parser.feed(buffer.toString())); |
|
stream.once("error", (err) => reject(err)); |
|
stream.once("close", () => resolve(data)); |
|
stream.end(); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createTransStream(stream: any, endCallback?: Function) { |
|
|
|
const created = util.unixTimestamp(); |
|
|
|
const transStream = new PassThrough(); |
|
let content = ""; |
|
!transStream.closed && |
|
transStream.write( |
|
`data: ${JSON.stringify({ |
|
id: "", |
|
model: MODEL_NAME, |
|
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; |
|
if (event.data == "[DONE]") return; |
|
|
|
const result = _.attempt(() => JSON.parse(event.data)); |
|
if (_.isError(result)) |
|
throw new Error(`Stream response invalid: ${event.data}`); |
|
const text = (result.contents || []).reduce((str, part) => { |
|
const { contentType, role, content } = part; |
|
if (contentType != "text" && contentType != "text2image") return str; |
|
if (role != "assistant" && !_.isString(content)) return str; |
|
return str + content; |
|
}, ""); |
|
const exceptCharIndex = text.indexOf("�"); |
|
let chunk = text.substring( |
|
exceptCharIndex != -1 |
|
? Math.min(content.length, exceptCharIndex) |
|
: content.length, |
|
exceptCharIndex == -1 ? text.length : exceptCharIndex |
|
); |
|
if (chunk && result.contentType == "text2image") { |
|
chunk = chunk.replace( |
|
/https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi, |
|
(url) => { |
|
const urlObj = new URL(url); |
|
urlObj.search = ""; |
|
return urlObj.toString(); |
|
} |
|
); |
|
} |
|
if (result.msgStatus != "finished") { |
|
if (chunk && result.contentType == "text") { |
|
content += chunk; |
|
const data = `data: ${JSON.stringify({ |
|
id: `${result.sessionId}-${result.msgId}`, |
|
model: MODEL_NAME, |
|
object: "chat.completion.chunk", |
|
choices: [ |
|
{ index: 0, delta: { content: chunk }, finish_reason: null }, |
|
], |
|
created, |
|
})}\n\n`; |
|
!transStream.closed && transStream.write(data); |
|
} |
|
} else { |
|
const delta = { content: chunk || "" }; |
|
if (!result.canShare) |
|
delta.content += "\n[内容由于不合规被停止生成,我们换个话题吧]"; |
|
if (result.errorCode) |
|
delta.content += `服务暂时不可用,第三方响应错误:${result.errorCode}`; |
|
const data = `data: ${JSON.stringify({ |
|
id: `${result.sessionId}-${result.msgId}`, |
|
model: MODEL_NAME, |
|
object: "chat.completion.chunk", |
|
choices: [ |
|
{ |
|
index: 0, |
|
delta, |
|
finish_reason: "stop", |
|
}, |
|
], |
|
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, |
|
created, |
|
})}\n\n`; |
|
!transStream.closed && transStream.write(data); |
|
!transStream.closed && transStream.end("data: [DONE]\n\n"); |
|
content = ""; |
|
endCallback && endCallback(result.sessionId); |
|
} |
|
|
|
|
|
} 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("data: [DONE]\n\n") |
|
); |
|
stream.once( |
|
"close", |
|
() => !transStream.closed && transStream.end("data: [DONE]\n\n") |
|
); |
|
stream.end(); |
|
return transStream; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async function receiveImages( |
|
stream: any |
|
): Promise<{ convId: string; imageUrls: string[] }> { |
|
return new Promise((resolve, reject) => { |
|
let convId = ""; |
|
const imageUrls = []; |
|
const parser = createParser((event) => { |
|
try { |
|
if (event.type !== "event") return; |
|
if (event.data == "[DONE]") return; |
|
|
|
const result = _.attempt(() => JSON.parse(event.data)); |
|
if (_.isError(result)) |
|
throw new Error(`Stream response invalid: ${event.data}`); |
|
if (!convId && result.sessionId) convId = result.sessionId; |
|
const text = (result.contents || []).reduce((str, part) => { |
|
const { role, content } = part; |
|
if (role != "assistant" && !_.isString(content)) return str; |
|
return str + content; |
|
}, ""); |
|
if (result.contentFrom == "text2image") { |
|
const urls = |
|
text.match( |
|
/https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi |
|
) || []; |
|
urls.forEach((url) => { |
|
const urlObj = new URL(url); |
|
urlObj.search = ""; |
|
const imageUrl = urlObj.toString(); |
|
if (imageUrls.indexOf(imageUrl) != -1) return; |
|
imageUrls.push(imageUrl); |
|
}); |
|
} |
|
if (result.msgStatus == "finished") { |
|
if (!result.canShare || imageUrls.length == 0) |
|
throw new APIException(EX.API_CONTENT_FILTERED); |
|
if (result.errorCode) |
|
throw new APIException( |
|
EX.API_REQUEST_FAILED, |
|
`服务暂时不可用,第三方响应错误:${result.errorCode}` |
|
); |
|
} |
|
} catch (err) { |
|
logger.error(err); |
|
reject(err); |
|
} |
|
}); |
|
|
|
stream.on("data", (buffer) => parser.feed(buffer.toString())); |
|
stream.once("error", (err) => reject(err)); |
|
stream.once("close", () => resolve({ convId, imageUrls })); |
|
stream.end(); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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` |
|
); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadFile(fileUrl: string, ticket: string) { |
|
|
|
await checkFileUrl(fileUrl); |
|
|
|
let filename, fileData, mimeType; |
|
|
|
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", |
|
|
|
maxContentLength: FILE_MAX_SIZE, |
|
|
|
timeout: 60000, |
|
})); |
|
} |
|
|
|
|
|
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, |
|
}); |
|
|
|
|
|
await axios.request({ |
|
method: "POST", |
|
url: "https://broadscope-dialogue.oss-cn-beijing.aliyuncs.com/", |
|
data: formData, |
|
|
|
maxBodyLength: FILE_MAX_SIZE, |
|
|
|
timeout: 120000, |
|
headers: { |
|
...FAKE_HEADERS, |
|
"X-Requested-With": "XMLHttpRequest" |
|
} |
|
}); |
|
|
|
const isImage = [ |
|
'image/jpeg', |
|
'image/jpg', |
|
'image/tiff', |
|
'image/png', |
|
'image/bmp', |
|
'image/gif', |
|
'image/svg+xml', |
|
'image/webp', |
|
'image/ico', |
|
'image/heic', |
|
'image/heif', |
|
'image/bmp', |
|
'image/x-icon', |
|
'image/vnd.microsoft.icon', |
|
'image/x-png' |
|
].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 } |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function tokenSplit(authorization: string) { |
|
return authorization.replace("Bearer ", "").split(","); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateCookie(ticket: string) { |
|
return [ |
|
`${ticket.length > 100 ? 'login_aliyunid_ticket' : 'tongyi_sso_ticket'}=${ticket}`, |
|
'aliyun_choice=intl', |
|
"_samesite_flag_=true", |
|
`t=${util.uuid(false)}`, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
].join("; "); |
|
} |
|
|
|
|
|
|
|
|
|
async function getTokenLiveStatus(ticket: string) { |
|
const result = await axios.post( |
|
"https://qianwen.biz.aliyun.com/dialog/session/list", |
|
{}, |
|
{ |
|
headers: { |
|
Cookie: generateCookie(ticket), |
|
...FAKE_HEADERS, |
|
}, |
|
timeout: 15000, |
|
validateStatus: () => true, |
|
} |
|
); |
|
try { |
|
const { data } = checkResult(result); |
|
return _.isArray(data); |
|
} |
|
catch(err) { |
|
return false; |
|
} |
|
} |
|
|
|
export default { |
|
createCompletion, |
|
createCompletionStream, |
|
generateImages, |
|
getTokenLiveStatus, |
|
tokenSplit, |
|
}; |
|
|