qwen.mjs
javascript•Created 3 Mar 2026, 04:15•241 views
Wrapper from apk qwen ai.
#ai#realtime#chatbot
javascript
0 lines
/***
@ Base: https://play.google.com/store/apps/details?id=ai.qwenlm.chat.android
@ Author: Shannz
@ Note: Wrapper from apk qwen ai.
***/
import axios from 'axios';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
const CONFIG = {
BASE_URL: "https://chat.qwen.ai/api/v2",
HEADERS: {
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 15; 25028RN03A Build/AP3A.240905.015.A2) AliApp(QWENCHAT/1.16.1) AppType/Release AplusBridgeLite',
'Connection': 'Keep-Alive',
'Accept': 'application/json',
'X-Platform': 'android',
'source': 'app',
'Accept-Language': 'en-US',
'Accept-Charset': 'UTF-8'
}
};
const utils = {
sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
generateUUID: () => crypto.randomUUID(),
hashPassword: (password) => {
return crypto.createHash('sha256').update(password).digest('hex');
},
getFileType: (filename) => {
const ext = path.extname(filename).toLowerCase();
if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext)) return 'image';
if (['.mp4', '.avi', '.mov', '.mkv'].includes(ext)) return 'video';
return 'file';
},
getMimeType: (filetype, filename) => {
const ext = path.extname(filename).toLowerCase();
if (filetype === 'image') return 'image/jpeg';
if (filetype === 'video') return 'video/mp4';
return 'application/octet-stream';
},
parseSSE: (chunk) => {
const lines = chunk.toString().split('\n');
const events = [];
let currentEvent = { event: 'message', data: '' };
for (const line of lines) {
if (line.startsWith('event:')) {
if (currentEvent.data) events.push({ ...currentEvent });
currentEvent = { event: line.substring(6).trim(), data: '' };
} else if (line.startsWith('data:')) {
currentEvent.data += line.substring(5).trim();
} else if (line === '' && currentEvent.data) {
events.push({ ...currentEvent });
currentEvent = { event: 'message', data: '' };
}
}
if (currentEvent.data) events.push(currentEvent);
return events;
},
getAuthHeaders: (token) => ({
...CONFIG.HEADERS,
'Authorization': `Bearer ${token}`,
'x-request-id': utils.generateUUID()
})
};
export const qwen = {
login: async (email, password) => {
try {
const hashedPassword = utils.hashPassword(password);
const response = await axios.post(`${CONFIG.BASE_URL}/auths/signin`, {
email: email,
password: hashedPassword
}, {
headers: {
...CONFIG.HEADERS,
'x-request-id': utils.generateUUID()
}
});
if (!response.data.success) throw new Error("Login failed");
return {
token: response.data.data.token,
user: response.data.data
};
} catch (error) {
console.error(`Login error: ${error.message}`);
return null;
}
},
createSession: async (token) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/chats/new`, {
chat_mode: "normal",
project_id: ""
}, {
headers: utils.getAuthHeaders(token)
});
if (!response.data.success) throw new Error('Failed to create session');
return response.data.data.id;
} catch (error) {
console.error(`Create session error: ${error.message}`);
return null;
}
},
getListSession: async (token, page = 1) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/chats/list?exclude_project=true&page=${page}`, {
headers: utils.getAuthHeaders(token)
});
if (!response.data.success) throw new Error('Failed to get session list');
return response.data.data;
} catch (error) {
console.error(`Get list session error: ${error.message}`);
return null;
}
},
getHistory: async (token, chatId) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/chats/${chatId}`, {
headers: utils.getAuthHeaders(token)
});
if (!response.data.success) throw new Error('Failed to get history');
return response.data.data;
} catch (error) {
console.error(`Get history error: ${error.message}`);
return null;
}
},
deleteSession: async (token, chatId) => {
try {
const response = await axios.delete(`${CONFIG.BASE_URL}/chats/${chatId}`, {
headers: utils.getAuthHeaders(token)
});
if (!response.data.success) throw new Error('Failed to delete session');
return response.data.data.status;
} catch (error) {
console.error(`Delete session error: ${error.message}`);
return false;
}
},
upload: async (token, filePath) => {
try {
if (!fs.existsSync(filePath)) throw new Error('File not found');
const stats = fs.statSync(filePath);
const filename = path.basename(filePath);
const filetype = utils.getFileType(filename);
const mimeType = utils.getMimeType(filetype, filename);
const fileBuffer = fs.readFileSync(filePath);
process.stdout.write(`\x1b[36m[Uploading ${filetype}...] \x1b[0m`);
const stsRes = await axios.post(`${CONFIG.BASE_URL}/files/getstsToken`, {
filename: filename, filetype: filetype, filesize: stats.size.toString()
}, { headers: utils.getAuthHeaders(token) });
if (!stsRes.data.success) throw new Error('Failed to get STS Token');
const stsData = stsRes.data.data;
const fileId = stsData.file_id;
const dateStr = new Date().toUTCString();
const stringToSign = `PUT\n\n${mimeType}\n${dateStr}\nx-oss-security-token:${stsData.security_token}\n/${stsData.bucketname}/${stsData.file_path}`;
const signature = crypto.createHmac('sha1', stsData.access_key_secret).update(stringToSign).digest('base64');
const putUrl = `https://${stsData.bucketname}.${stsData.region}.aliyuncs.com/${stsData.file_path}`;
await axios.put(putUrl, fileBuffer, {
headers: {
'User-Agent': 'aliyun-sdk-android/2.9.21',
'Authorization': `OSS ${stsData.access_key_id}:${signature}`,
'x-oss-security-token': stsData.security_token,
'Date': dateStr,
'Content-Type': mimeType
},
maxBodyLength: Infinity
});
if (filetype === 'image' || filetype === 'video') {
process.stdout.write('\x1b[32m DONE!\x1b[0m\n');
return { id: fileId, type: filetype, name: filename, size: stats.size, url: stsData.file_url };
}
process.stdout.write('\x1b[33m[Parsing...]\x1b[0m ');
const parseRes = await axios.post(`${CONFIG.BASE_URL}/files/parse`, { file_id: fileId }, { headers: utils.getAuthHeaders(token) });
if (!parseRes.data.success) throw new Error('Parse init failed');
let attempts = 0;
while (attempts < 30) {
await utils.sleep(3000);
const statusRes = await axios.post(`${CONFIG.BASE_URL}/files/parse/status`, { file_id_list: [fileId] }, { headers: utils.getAuthHeaders(token) });
if (statusRes.data.success && statusRes.data.data.length > 0) {
const statusData = statusRes.data.data[0];
if (statusData.status === 'success') {
process.stdout.write('\x1b[32m DONE!\x1b[0m\n');
return { id: fileId, type: filetype, name: filename, size: stats.size, url: stsData.file_url };
} else if (statusData.status === 'failed' || statusData.error_code) {
process.stdout.write('\x1b[31m FAILED!\x1b[0m\n');
return null;
}
}
process.stdout.write('.');
attempts++;
}
process.stdout.write('\x1b[31m TIMEOUT!\x1b[0m\n');
return null;
} catch (error) {
console.error(`\nUpload error: ${error.message}`);
if(error.response && error.response.data) {
console.error("Detail Error:", error.response.data.toString());
}
return null;
}
},
MODELS: [
"qwen3.5-flash",
"qwen3.5-397b-a17b",
"qwen3.5-122b-a10b",
"qwen3.5-27b",
"qwen3.5-35b-a3b",
"qwen3-max-2026-01-23",
"qwen-plus-2025-07-28",
"qwen3-coder-plus",
"qwen3-vl-plus",
"qwen3-omni-flash-2025-12-01",
"qwen-max-latest",
"qwen3.5-plus" // Default model
],
chat: async (token, chatId, prompt, options = {}) => {
try {
const useStream = options.stream !== false;
const messageId = utils.generateUUID();
const selectedModel = options.model || "qwen3.5-plus";
let fileAttachment = [];
if (options.file) {
fileAttachment = [{
type: options.file.type,
file: {
data: {},
filename: options.file.name,
meta: {
name: options.file.name,
size: options.file.size,
content_type: options.file.type === 'image' ? '' : options.file.type
}
},
url: options.file.url,
name: options.file.name,
file_type: options.file.type === 'image' ? undefined : options.file.name.split('.').pop()
}];
if (options.file.type === 'image') {
fileAttachment[0].image_width = 1000;
fileAttachment[0].image_height = 1000;
}
}
const payload = {
stream: true,
incremental_output: true,
chat_id: chatId,
chat_mode: "normal",
model: selectedModel,
messages: [
{
chat_type: "t2t",
content: prompt,
role: "user",
feature_config: {
output_schema: "phase",
thinking_enabled: options.thinkingEnabled !== false,
thinking_format: "summary",
auto_thinking: true,
auto_search: options.searchEnabled !== false
},
timestamp: Math.floor(Date.now() / 1000),
sub_chat_type: "t2t",
models: [selectedModel],
fid: messageId,
user_action: "chat",
extra: { meta: { subChatType: "t2t" } },
...(fileAttachment.length > 0 && { files: fileAttachment })
}
],
timestamp: Math.floor(Date.now() / 1000),
share_id: "",
origin_branch_message_id: ""
};
if (options.parentId) {
payload.parent_id = options.parentId;
payload.messages[0].parentId = options.parentId;
payload.messages[0].parent_id = options.parentId;
}
const response = await axios.post(`${CONFIG.BASE_URL}/chat/completions?chat_id=${chatId}`, payload, {
headers: {
...utils.getAuthHeaders(token),
'Accept': '*/*,text/event-stream',
'Content-Type': 'application/json; charset=UTF-8'
},
responseType: 'stream'
});
let fullText = '';
let thoughtText = '';
let searchResults = [];
let buffer = '';
let thinkingStarted = false;
let searchingStarted = false;
let printedThoughts = 0;
let searchPrinted = false;
const printToConsole = (type, content) => {
if (!useStream) return;
if (type === 'SEARCH_START') {
if (!searchingStarted) {
process.stdout.write('\x1b[36m\n[🔍 Searching Web / Analyzing...]\x1b[0m\n');
searchingStarted = true;
}
} else if (type === 'SEARCH_RESULT') {
if (Array.isArray(content)) {
content.forEach((res, idx) => {
process.stdout.write(`\x1b[36m > [${idx+1}] ${res.title}\x1b[0m\n`);
});
}
} else if (type === 'THINK') {
if (!thinkingStarted) {
process.stdout.write('\x1b[33m\n[🧠 Thinking]\n\x1b[0m');
thinkingStarted = true;
}
process.stdout.write(`\x1b[33m- ${content}\x1b[0m\n`);
} else if (type === 'RESPONSE') {
if (thinkingStarted || searchingStarted) {
process.stdout.write('\n\n\x1b[0m');
thinkingStarted = false;
searchingStarted = false;
}
process.stdout.write(content);
}
};
return new Promise((resolve, reject) => {
response.data.on('data', (chunk) => {
buffer += chunk.toString();
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
const events = utils.parseSSE(line + '\n\n');
for (const event of events) {
if (!event.data || event.data === ':') continue;
try {
const parsed = JSON.parse(event.data);
if (!parsed.choices || parsed.choices.length === 0) continue;
const delta = parsed.choices[0].delta;
if (!delta) continue;
if (delta.phase === 'thinking_summary') {
if (delta.extra && delta.extra.summary_thought && delta.extra.summary_thought.content) {
const thoughts = delta.extra.summary_thought.content;
for (let i = printedThoughts; i < thoughts.length; i++) {
thoughtText += thoughts[i] + '\n';
printToConsole('THINK', thoughts[i]);
}
printedThoughts = thoughts.length;
}
}
if (delta.phase === 'image_search' || delta.phase === 'search') {
printToConsole('SEARCH_START');
if (delta.extra && delta.extra.search_result && !searchPrinted) {
searchResults = delta.extra.search_result;
printToConsole('SEARCH_RESULT', searchResults);
searchPrinted = true;
}
}
if (delta.phase === 'answer') {
if (delta.content) {
fullText += delta.content;
printToConsole('RESPONSE', delta.content);
}
}
} catch (e) { }
}
}
});
response.data.on('end', () => {
if (useStream) process.stdout.write('\n');
if (useStream) {
resolve(fullText || 'No response');
} else {
resolve({
status: 'success',
thinking: thoughtText.trim(),
search_results: searchResults,
response: fullText.trim()
});
}
});
response.data.on('error', (err) => {
if (useStream) reject(err);
else resolve({ status: 'error', message: err.message });
});
});
} catch (error) {
console.error(`Chat error: ${error.message}`);
return options.stream !== false ? null : { status: 'error', message: error.message };
}
}
};
/*
(async () => {
// 1. Login
console.log("Logging in...");
const auth = await qwen.login("email-lu", "pw-lu");
if (!auth) return console.log("Login Gagal");
const token = auth.token;
console.log('Login Success!');
// 2. Buat Sesi Baru
console.log("Creating session...");
const chatId = await qwen.createSession(token);
console.log('Chat ID:', chatId);
// 3. Upload Media (foto, video, document)
console.log("Uploading file...");
const uploadedFile = await qwen.upload(token, './u.pdf');
// 4. Chat
console.log("Chatting...\n");
const chatResponse = await qwen.chat(token, chatId, "file apa ini?", {
stream: false, // true untuk streaming
model: "qwen-max-latest", // model
thinkingEnabled: true, // thinking mode
searchEnabled: true, // search mode
file: uploadedFile, // contoh pakai file
});
if (!chatResponse.status) {
console.log("\n[Done Streaming]");
} else {
console.log(chatResponse);
}
// 5. History Session
console.log("Get History...");
const history = await qwen.getHistory(token, chatId);
console.log("History:", history);
// 6. List Session
console.log("List All Session");
const list = await qwen.getListSession(token);
console.log("List Session:", list);
// 7. Delete Session
console.log("Delete Session");
const del = await qwen.deleteSession(token, chatId);
console.log("Deleting Session Status:", del);
})();
*/