const axios = require('axios');
const crypto = require('crypto');
const qrcode = require('qrcode-terminal');
const fs = require('fs');
const sleep = ms => new Promise(r => setTimeout(r, ms));
class DolaAI {
constructor() {
this.cookies = null;
this.inst = axios.create({
baseURL: 'https://www.dola.com',
headers: {
'accept': '*/*',
'agw-js-conv': 'str, str',
'content-type': 'application/json',
'origin': 'https://www.dola.com',
'referer': 'https://www.dola.com/chat/',
'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'
}
});
this.inst.interceptors.request.use(config => {
if (this.cookies) config.headers['cookie'] = this.cookies.join('; ');
return config;
});
this.inst.interceptors.response.use(res => {
const setCookie = res.headers['set-cookie'];
if (setCookie?.length) {
this.cookies = this.cookies || [];
for (const c of setCookie) {
const m = c.match(/^([^=]+)=([^;]+)/);
if (m && !this.cookies.some(ck => ck.startsWith(m[1] + '='))) {
this.cookies.push(`${m[1]}=${m[2]}`);
}
}
}
return res;
});
}
_uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = crypto.randomInt(16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
_aws4Sign(method, host, urlPath, qs, body, accessKey, secretKey, sessionToken) {
const now = new Date();
const amzDate = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
const dateStamp = amzDate.substring(0, 8);
const payloadHash = crypto.createHash('sha256').update(body || '').digest('hex');
const signedHeaders = body ? 'x-amz-content-sha256;x-amz-date;x-amz-security-token' : 'x-amz-date;x-amz-security-token';
const canonicalHeaders = body ? `x-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\nx-amz-security-token:${sessionToken}\n` : `x-amz-date:${amzDate}\nx-amz-security-token:${sessionToken}\n`;
const canonicalRequest = [method, urlPath, qs, canonicalHeaders, signedHeaders, payloadHash].join('\n');
const credentialScope = `${dateStamp}/us-east-1/imagex/aws4_request`;
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, crypto.createHash('sha256').update(canonicalRequest).digest('hex')].join('\n');
const kDate = crypto.createHmac('sha256', 'AWS4' + secretKey).update(dateStamp).digest();
const kRegion = crypto.createHmac('sha256', kDate).update('us-east-1').digest();
const kService = crypto.createHmac('sha256', kRegion).update('imagex').digest();
const signingKey = crypto.createHmac('sha256', kService).update('aws4_request').digest();
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
return {
authorization: `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
'x-amz-date': amzDate,
'x-amz-content-sha256': payloadHash
};
}
_crc32(buf) {
let crc = 0xFFFFFFFF;
const table = new Int32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
}
table[i] = c;
}
for (let i = 0; i < buf.length; i++) {
crc = table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
}
return ((crc ^ 0xFFFFFFFF) >>> 0).toString(16).padStart(8, '0');
}
_buildBlocks(message, attachmentUris = []) {
const blocks = [];
for (const uri of attachmentUris) {
blocks.push({
block_type: 10052,
content: {
attachment_block: {
attachments: [{
type: 1,
identifier: this._uuid(),
image: {
name: 'image.png',
uri: uri,
image_ori: { url: '', width: 0, height: 0, format: '', url_formats: {} }
},
parse_state: 0,
review_state: 1,
upload_status: 1,
progress: 100,
src: ''
}]
},
pc_event_block: ''
},
block_id: this._uuid(),
parent_id: '',
meta_info: [],
append_fields: []
});
}
blocks.push({
block_type: 10000,
content: {
text_block: { text: message, icon_url: '', icon_url_dark: '', summary: '' },
pc_event_block: ''
},
block_id: this._uuid(),
parent_id: '',
meta_info: [],
append_fields: []
});
return blocks;
}
_buildBody(blocks, conversationId, localConvId, deepThink = false, search = false) {
return {
client_meta: {
local_conversation_id: localConvId,
conversation_id: conversationId || '',
bot_id: '7339470689562525703',
last_section_id: '',
last_message_index: null
},
messages: [{
local_message_id: this._uuid(),
content_block: blocks,
message_status: 0
}],
option: {
send_message_scene: search ? 'search' : '',
create_time_ms: Date.now(),
need_deep_think: deepThink ? 1 : 0,
need_create_conversation: !conversationId,
conversation_init_option: { need_ack_conversation: true },
unique_key: this._uuid(),
sse_recv_event_options: { support_chunk_delta: true },
recovery_option: {
is_recovery: false,
req_create_time_sec: Math.floor(Date.now() / 1000),
append_sse_event_scene: 0
},
message_storage_type: 0
},
user_context: [],
ext: {
use_deep_think: deepThink ? '1' : '0',
fp: 'verify_' + Array.from({ length: 12 }, () => crypto.randomInt(0, 10)).join(''),
sub_conv_firstmet_type: '1',
conversation_init_option: '{"need_ack_conversation":true}'
}
};
}
_parseSSE(data) {
const result = {
text: '',
images: [],
agentName: null,
conversationId: null,
localConversationId: null,
messageId: null
};
const extract = block => {
if (block.content?.text_block?.text) result.text += block.content.text_block.text;
if (block.content?.creation_block?.creations) {
for (const c of block.content.creation_block.creations) {
if (c.image?.image_thumb?.url) {
result.images.push({
url: c.image.image_thumb.url,
originalUrl: c.image.image_ori?.url || '',
key: c.image.key || '',
width: c.image.image_thumb.width || 0,
height: c.image.image_thumb.height || 0,
id: c.id
});
}
}
}
};
for (const event of data.split('\n\n')) {
let eventType = '';
let eventData = '';
for (const line of event.split('\n')) {
if (line.startsWith('event:')) eventType = line.slice(6).trim();
if (line.startsWith('data:')) eventData = line.slice(5).trim();
}
if (!eventData) continue;
try {
const json = JSON.parse(eventData);
if (json.ack_client_meta) {
result.conversationId = json.ack_client_meta.conversation_id;
result.localConversationId = json.ack_client_meta.local_conversation_id;
}
if (json.message_id) result.messageId = json.message_id;
if (json.content?.ext?.agent_name) result.agentName = json.content.ext.agent_name;
if (eventType === 'STREAM_MSG_NOTIFY') {
for (const block of (json.content?.content_block || [])) {
extract(block);
}
}
if (eventType === 'STREAM_CHUNK') {
for (const patch of (json.patch_op || [])) {
if (patch.patch_value?.ext?.agent_name) {
result.agentName = patch.patch_value.ext.agent_name;
}
for (const block of (patch.patch_value?.content_block || [])) {
extract(block);
}
}
}
} catch (e) {}
}
return result;
}
_uploadAttachments = async function (buffers) {
try {
const uris = [];
for (const buf of buffers) {
const prepResp = await this.inst.post('/alice/resource/prepare_upload', {
tenant_id: '5',
scene_id: '4',
resource_type: 2
}, {
params: {
aid: '495671',
device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
device_platform: 'web',
language: 'en',
pc_version: '3.25.3',
pkg_type: 'release_version',
real_aid: '495671',
region: 'ID',
samantha_web: '1',
sys_region: 'ID',
tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
use_olympus_account: '1',
version_code: '20800',
web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
web_platform: 'browser',
web_tab_id: this._uuid()
}
});
if (prepResp.data.code !== 0) throw new Error('prepare_upload failed');
const { service_id: serviceId, upload_auth_token: authToken } = prepResp.data.data;
const host = 'imagex-ap-southeast-1.bytevcloudapi.com';
const applyParams = {
Action: 'ApplyImageUpload',
FileExtension: '.png',
FileSize: buf.length.toString(),
ServiceId: serviceId,
Version: '2018-08-01',
s: crypto.randomBytes(8).toString('hex')
};
const applyQs = Object.keys(applyParams).sort().map(k => `${k}=${encodeURIComponent(applyParams[k])}`).join('&');
const applyAuth = this._aws4Sign('GET', host, '/', applyQs, '', authToken.access_key, authToken.secret_key, authToken.session_token);
const applyResp = await axios.get(`https://${host}/?${applyQs}`, {
headers: {
'authorization': applyAuth.authorization,
'x-amz-date': applyAuth['x-amz-date'],
'x-amz-security-token': authToken.session_token,
'referer': 'https://www.dola.com/',
'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'
}
});
if (!applyResp.data.Result?.UploadAddress?.StoreInfos?.length) throw new Error('applyImageUpload failed');
const storeInfo = applyResp.data.Result.UploadAddress.StoreInfos[0];
const uploadHost = applyResp.data.Result.UploadAddress.UploadHosts?.[0] || 'tos-mya3-share.vodupload.com';
await axios.post(`https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`, buf, {
headers: {
'authorization': storeInfo.Auth,
'content-type': 'application/octet-stream',
'content-disposition': 'attachment; filename="image.png"',
'content-crc32': this._crc32(Buffer.from(buf)),
'referer': 'https://www.dola.com/',
'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'
},
timeout: 60000
});
const commitQs = Object.keys({ Action: 'CommitImageUpload', ServiceId: serviceId, Version: '2018-08-01' }).sort().map(k => `${k}=${encodeURIComponent({ Action: 'CommitImageUpload', ServiceId: serviceId, Version: '2018-08-01' }[k])}`).join('&');
const commitBody = JSON.stringify({ SessionKey: applyResp.data.Result.UploadAddress.SessionKey });
const commitAuth = this._aws4Sign('POST', host, '/', commitQs, commitBody, authToken.access_key, authToken.secret_key, authToken.session_token);
const commitResp = await axios.post(`https://${host}/?${commitQs}`, commitBody, {
headers: {
'authorization': commitAuth.authorization,
'x-amz-content-sha256': commitAuth['x-amz-content-sha256'],
'x-amz-date': commitAuth['x-amz-date'],
'x-amz-security-token': authToken.session_token,
'referer': 'https://www.dola.com/',
'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'
}
});
if (!commitResp.data.Result?.Results?.length) throw new Error('commitUpload failed');
const imageUri = commitResp.data.Result.Results[0].Uri;
const localMsgId = this._uuid();
await this.inst.post('/alice/message/pre_handle_v2_without_conv', {
uplink_entity: {
entity_type: 2,
entity_content: { image: { key: imageUri } },
identifier: localMsgId
},
bot_id: '7339470689562525703',
local_message_id: localMsgId
}, {
params: {
aid: '495671',
device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
device_platform: 'web',
language: 'en',
pc_version: '3.25.3',
pkg_type: 'release_version',
real_aid: '495671',
region: 'ID',
samantha_web: '1',
sys_region: 'ID',
tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
use_olympus_account: '1',
version_code: '20800',
web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
web_platform: 'browser',
web_tab_id: this._uuid()
}
});
uris.push(imageUri);
}
return uris;
} catch (error) {
throw new Error(error.message);
}
}
login = async function () {
try {
const initResp = await axios.get('https://www.dola.com/chat/', {
headers: {
'accept': 'text/html',
'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'
},
maxRedirects: 5
});
this.cookies = [];
if (initResp.headers['set-cookie']) {
for (const c of initResp.headers['set-cookie']) {
const m = c.match(/^([^=]+)=([^;]+)/);
if (m) {
this.cookies.push(`${m[1]}=${m[2]}`);
}
}
}
console.log('Requesting QR code...\n');
const qrResp = await this.inst.get('/passport/web/get_qrcode/', {
params: {
aid: '495671',
device_platform: 'web',
language: 'en',
account_sdk_source: 'web',
passport_jssdk_version: '2.0.1-verify-center.1',
next: 'https://www.dola.com',
verifyFp: `verify_${Array.from({ length: 12 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 8 }, () => crypto.randomInt(0, 10)).join('')}`,
sign: crypto.randomBytes(32).toString('hex'),
qs: Buffer.from(`flow_user_country=ID|flow_ssr_sidebar_expand=1|web_id=${Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join('')}`).toString('base64')
}
});
if (qrResp.data.data?.error_code !== 0) throw new Error('Failed to get QR code');
const qrToken = qrResp.data.data.token;
qrcode.generate(qrResp.data.data.qrcode_index_url, { small: true }, qr => console.log(qr));
await sleep(300);
console.log('Waiting for scan... (Ctrl+C to cancel)');
let attempts = 0;
while (attempts < 120) {
attempts++;
try {
const statusResp = await this.inst.get('/passport/web/check_qrconnect/', {
params: {
aid: '495671',
device_platform: 'web',
language: 'en',
account_sdk_source: 'web',
passport_jssdk_version: '2.0.1-verify-center.1',
next: 'https://www.dola.com',
token: qrToken,
verifyFp: `verify_${Array.from({ length: 12 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 8 }, () => crypto.randomInt(0, 10)).join('')}`,
sign: crypto.randomBytes(32).toString('hex'),
qs: Buffer.from(`flow_user_country=ID|flow_ssr_sidebar_expand=1|web_id=${Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join('')}`).toString('base64')
}
});
const data = statusResp.data.data;
if (data.status === 'confirmed') {
console.log('\n Confirmed!');
if (data.redirect_url) {
await axios.get(data.redirect_url, {
headers: {
'cookie': this.cookies.join('; '),
'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'
},
maxRedirects: 5
});
}
const cookieFile = `dola-cookies-${Date.now()}.json`;
fs.writeFileSync(cookieFile, JSON.stringify(this.cookies, null, 4));
console.log(`\nLogin Successful`);
console.log(`Cookies saved to: ${cookieFile}`);
return cookieFile;
}
if (data.status === 'expired') throw new Error('QR code expired');
process.stdout.write(`\r Waiting... (${attempts}s) `);
} catch (e) {
if (e.message === 'QR code expired') {
throw e;
}
}
await sleep(1000);
}
throw new Error('Login timeout');
} catch (error) {
throw new Error(error.message);
}
}
loadCookies = async function (cookiePath) {
try {
this.cookies = JSON.parse(fs.readFileSync(cookiePath, 'utf-8'));
} catch (error) {
throw new Error(error.message);
}
}
chat = async function (message, { conversationId = null, attachments = [], deepThink = false, search = false } = {}) {
try {
const localConvId = 'local_' + Array.from({ length: 13 }, () => crypto.randomInt(0, 10)).join('');
const attachmentUris = attachments.length > 0 ? await this._uploadAttachments(attachments) : [];
const blocks = this._buildBlocks(message, attachmentUris);
const body = this._buildBody(blocks, conversationId, localConvId, deepThink, search);
const resp = await this.inst.post('/chat/completion', body, {
params: {
aid: '495671',
device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
device_platform: 'web',
language: 'en',
pc_version: '3.25.3',
pkg_type: 'release_version',
real_aid: '495671',
region: 'ID',
samantha_web: '1',
sys_region: 'ID',
tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
use_olympus_account: '1',
version_code: '20800',
web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
web_platform: 'browser',
web_tab_id: this._uuid()
},
headers: {
'x-flow-trace': '04-' + crypto.randomBytes(16).toString('hex') + '-01'
},
timeout: 120000,
responseType: 'text'
});
const result = this._parseSSE(resp.data);
return {
id: result.messageId,
conversationId: result.conversationId || conversationId,
agentName: result.agentName,
content: {
text: result.text,
images: result.images
}
};
} catch (error) {
throw new Error(error.message);
}
}
listConversation = async function (options = {}) {
try {
const { count = 50, offset = 0 } = options;
const resp = await this.inst.post('/alice/conversation/list', {
count,
offset
}, {
params: {
aid: '495671',
device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
device_platform: 'web',
language: 'en',
pc_version: '3.25.3',
pkg_type: 'release_version',
real_aid: '495671',
region: 'ID',
samantha_web: '1',
sys_region: 'ID',
tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
use_olympus_account: '1',
version_code: '20800',
web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
web_platform: 'browser',
web_tab_id: this._uuid()
}
});
return resp.data?.data?.conversation_list || [];
} catch (error) {
throw new Error(error.message);
}
}
getConversation = async function (conversationId) {
try {
const resp = await this.inst.post('/alice/conversation/info', {
conversation_id: conversationId
}, {
params: {
aid: '495671',
device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
device_platform: 'web',
language: 'en',
pc_version: '3.25.3',
pkg_type: 'release_version',
real_aid: '495671',
region: 'ID',
samantha_web: '1',
sys_region: 'ID',
tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
use_olympus_account: '1',
version_code: '20800',
web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
web_platform: 'browser',
web_tab_id: this._uuid()
}
});
return resp.data?.data?.conversation || null;
} catch (error) {
throw new Error(error.message);
}
}
deleteConversation = async function (conversationId) {
try {
const resp = await this.inst.post('/samantha/thread/delete', {
thread_id: conversationId
}, {
params: {
aid: '495671',
device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
device_platform: 'web',
language: 'en',
pc_version: '3.25.3',
pkg_type: 'release_version',
real_aid: '495671',
region: 'ID',
samantha_web: '1',
sys_region: 'ID',
tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
use_olympus_account: '1',
version_code: '20800',
web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
web_platform: 'browser',
web_tab_id: this._uuid()
}
});
return resp.data;
} catch (error) {
throw new Error(error.message);
}
}
}
(async () => {
const dola = new DolaAI();
await dola.loadCookies('./dola-cookies.json');
const chat1 = await dola.chat('haii! namaku riruu');
console.log(JSON.stringify(chat1, null, 2));
const chat2 = await dola.chat('siapaa namaku??', { conversationId: chat1.conversationId });
console.log(JSON.stringify(chat2, null, 2));
})();