fix(KBService): update Puppeteer configuration for headless mode and executable path
fix(node.yaml): correct deployment paths and improve Node.js setup fix(.gitignore): add 'botpoc' to ignored files fix(SystemKeywords): enhance PDF conversion and image processing logic fix(DialogKeywords): adjust Puppeteer launch options for better performance fix(KeywordsExpressions): fix syntax error in PDF assignment
This commit is contained in:
parent
2b2ab3a42e
commit
43242cb433
7 changed files with 200 additions and 111 deletions
|
@ -17,33 +17,33 @@ jobs:
|
|||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# - name: Setup Node.js
|
||||
# uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: '20'
|
||||
# cache: 'npm'
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Copy files to deployment location
|
||||
run: |
|
||||
echo "[General Bots Deployer] Copying files to deploy location..."
|
||||
sudo rm -rf /opt/gbo/bin/BotServer/dist
|
||||
sudo cp -r ./* /opt/gbo/bin/BotServer
|
||||
sudo rm -rf /opt/gbo/bin/bot/botserver/dist
|
||||
sudo cp -r ./* /opt/gbo/bin/bot/botserver
|
||||
|
||||
|
||||
- name: Install production dependencies in deployment location
|
||||
run: |
|
||||
echo "[General Bots Deployer] Building BotServer..."
|
||||
# rm -rf /opt/gbo/bin/BotServer/node_modules
|
||||
cd /opt/gbo/bin/BotServer
|
||||
#sudo npm ci --production
|
||||
# rm -rf /opt/gbo/bin/bot/botserver/node_modules
|
||||
cd /opt/gbo/bin/bot/botserver
|
||||
sudo npm ci --production
|
||||
npm run build-server
|
||||
# npm run build-gbui
|
||||
npm run build-gbui
|
||||
|
||||
|
||||
- name: Restart Bots Deployer
|
||||
run: |
|
||||
echo "[General Bots Deployer] Restarting..."
|
||||
sudo systemctl stop botserver
|
||||
sudo systemctl stop bot
|
||||
echo "[General Bots Deployer] Stopped."
|
||||
sudo systemctl start botserver
|
||||
sudo systemctl start bot
|
||||
echo "[General Bots Deployer] Started."
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -30,4 +30,5 @@ logo.svg
|
|||
screenshot.png
|
||||
data.db
|
||||
.wwebjs_cache
|
||||
*doula*
|
||||
*doula*
|
||||
*botpoc*
|
|
@ -197,7 +197,6 @@
|
|||
"pdf-parse": "1.1.1",
|
||||
"pdf-to-png-converter": "3.3.0",
|
||||
"pdfjs-dist": "4.6.82",
|
||||
"pdfkit": "0.15.0",
|
||||
"pg": "^8.13.1",
|
||||
"phone": "3.1.50",
|
||||
"pizzip": "3.1.7",
|
||||
|
|
|
@ -42,7 +42,7 @@ import mime from 'mime-types';
|
|||
import tesseract from 'node-tesseract-ocr';
|
||||
import path from 'path';
|
||||
import { CollectionUtil } from 'pragmatismo-io-framework';
|
||||
import puppeteer from 'puppeteer';
|
||||
import puppeteer, { executablePath } from 'puppeteer';
|
||||
import qrcode from 'qrcode';
|
||||
import urlJoin from 'url-join';
|
||||
import pkg from 'whatsapp-web.js';
|
||||
|
@ -94,7 +94,11 @@ export class DialogKeywords {
|
|||
|
||||
// Launch Puppeteer to render the chart
|
||||
|
||||
const browser = await puppeteer.launch();
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROME_PATH ? process.env.CHROME_PATH : executablePath(),
|
||||
}
|
||||
);
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Load Billboard.js styles and scripts
|
||||
|
|
|
@ -1304,7 +1304,7 @@ export class KeywordsExpressions {
|
|||
keywords[i++] = [
|
||||
/^\s*((?:[a-z]+.?)(?:(?:\w+).)(?:\w+)*)\s*=\s*(.*)\s*as\s*pdf/gim,
|
||||
($0, $1, $2) => {
|
||||
return `${$1} = await sys.asPdf({pid: pid, data: ${$2})`;
|
||||
return `${$1} = await sys.asPdf({pid: pid, data: ${$2}})`;
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -68,6 +68,11 @@ import { DialogKeywords } from './DialogKeywords.js';
|
|||
import { GBVMService } from './GBVMService.js';
|
||||
import { KeywordsExpressions } from './KeywordsExpressions.js';
|
||||
import { WebAutomationServices } from './WebAutomationServices.js';
|
||||
import { exec } from 'child_process';
|
||||
import util from 'util';
|
||||
|
||||
// Promisify the exec function for async/await usage
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
import { md5 } from 'js-md5';
|
||||
import { Client } from 'minio';
|
||||
|
@ -361,8 +366,46 @@ export class SystemKeywords {
|
|||
);
|
||||
}
|
||||
|
||||
public async asPDF({ pid, data }) {
|
||||
let file = await this.renderTable(pid, data, true, false);
|
||||
private async convertWithLibreOffice(pid, inputPath) {
|
||||
const { min } = await DialogKeywords.getProcessInfo(pid);
|
||||
const gbaiName = GBUtil.getGBAIPath(min.botId);
|
||||
const localName = path.join('work', gbaiName, 'cache', `img${GBAdminService.getRndReadableIdentifier()}.pdf`);
|
||||
|
||||
try {
|
||||
// LibreOffice command for conversion using localName as output
|
||||
const command = `libreoffice --headless --convert-to pdf --outdir "${path.dirname(localName)}" "${inputPath}"`;
|
||||
|
||||
GBLogEx.info(min, `Executing: ${command}`);
|
||||
const { stdout, stderr } = await execPromise(command);
|
||||
|
||||
if (stderr) {
|
||||
GBLogEx.error(min, `LibreOffice stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
GBLogEx.info(min, `LibreOffice stdout: ${stdout}`);
|
||||
|
||||
const url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', path.basename(localName));
|
||||
|
||||
return { localName, url};
|
||||
|
||||
} catch (error) {
|
||||
GBLogEx.error(min, `Error converting file to PDF: ${error}`);
|
||||
throw new Error('PDF conversion failed');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async asPdf({ pid, data }) {
|
||||
|
||||
let file;
|
||||
|
||||
if (data.url) {
|
||||
file = await this.convertWithLibreOffice(pid, data.localName);
|
||||
}
|
||||
else {
|
||||
file = await this.renderTable(pid, data, true, false);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
|
@ -390,7 +433,7 @@ export class SystemKeywords {
|
|||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', chunk => chunks.push(chunk));
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks.map(chunk => new Uint8Array(chunk)))));
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -2113,35 +2156,99 @@ export class SystemKeywords {
|
|||
*
|
||||
* Fills a .docx or .pptx with template data.
|
||||
*
|
||||
* doc = FILL "templates/template.docx", data
|
||||
* doc = FILL "templates/template.docx" WITH data
|
||||
*
|
||||
*/
|
||||
public async fill({ pid, templateName, data }) {
|
||||
const { min, user } = await DialogKeywords.getProcessInfo(pid);
|
||||
private async getTemplateBuffer(min: any, gbaiName: string, templateName: string): Promise<Buffer> {
|
||||
const botId = min.instance.botId;
|
||||
const gbaiName = GBUtil.getGBAIPath(botId, 'gbdata');
|
||||
let localName;
|
||||
|
||||
// Downloads template from .gbdrive.
|
||||
if (GBConfigService.get('GB_MODE') === 'legacy') {
|
||||
// Legacy mode - using Microsoft Graph API
|
||||
const { baseUrl, client } = await GBDeployer.internalGetDriveClient(min);
|
||||
const packagePath = '/' + urlJoin(gbaiName, `${botId}.gbdrive`);
|
||||
const template = await this.internalGetDocument(client, baseUrl, packagePath, templateName);
|
||||
const url = template['@microsoft.graph.downloadUrl'];
|
||||
const res = await fetch(url);
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
else if (GBConfigService.get('GB_MODE') === 'gbcluster') {
|
||||
// GBCluster mode - using MinIO
|
||||
const minioClient = this.createMinioClient();
|
||||
const bucketName = (process.env.DRIVE_ORG_PREFIX + botId + '.gbai').toLowerCase();
|
||||
const filePath = urlJoin(gbaiName, `${botId}.gbdrive`, templateName);
|
||||
|
||||
let { baseUrl, client } = await GBDeployer.internalGetDriveClient(min);
|
||||
let packagePath = '/' + urlJoin(gbaiName, `${botId}.gbdrive`);
|
||||
let template = await this.internalGetDocument(client, baseUrl, packagePath, templateName);
|
||||
let url = template['@microsoft.graph.downloadUrl'];
|
||||
const res = await fetch(url);
|
||||
let buf: any = Buffer.from(await res.arrayBuffer());
|
||||
localName = path.join('work', gbaiName, 'cache', `tmp${GBAdminService.getRndReadableIdentifier()}.docx`);
|
||||
await fs.writeFile(localName, new Uint8Array(buf), { encoding: null });
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Uint8Array[] = [];
|
||||
minioClient.getObject(bucketName, filePath).then(stream => {
|
||||
stream.on('data', chunk => chunks.push(new Uint8Array(chunk)));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
stream.on('error', reject);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Default mode - direct filesystem access
|
||||
const gbdriveName = GBUtil.getGBAIPath(botId, 'gbdrive');
|
||||
const templatePath = path.join(GBConfigService.get('STORAGE_LIBRARY'), gbdriveName, templateName);
|
||||
return fs.readFile(templatePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace image path on all elements of data.
|
||||
private async getImageBuffer(min: any, gbaiName: string, imagePath: string): Promise<Buffer> {
|
||||
const botId = min.instance.botId;
|
||||
|
||||
if (GBConfigService.get('GB_MODE') === 'legacy') {
|
||||
const { baseUrl, client } = await GBDeployer.internalGetDriveClient(min);
|
||||
let packagePath = urlJoin(gbaiName, `${botId}.gbdrive`);
|
||||
if (imagePath.indexOf('/') !== -1) {
|
||||
packagePath = '/' + urlJoin(packagePath, path.dirname(imagePath));
|
||||
imagePath = path.basename(imagePath);
|
||||
}
|
||||
const ref = await this.internalGetDocument(client, baseUrl, packagePath, imagePath);
|
||||
const url = ref['@microsoft.graph.downloadUrl'];
|
||||
const response = await fetch(url);
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
}
|
||||
else if (GBConfigService.get('GB_MODE') === 'gbcluster') {
|
||||
const minioClient = this.createMinioClient();
|
||||
const bucketName = (process.env.DRIVE_ORG_PREFIX + botId + '.gbai').toLowerCase();
|
||||
const filePath = urlJoin(gbaiName, `${botId}.gbdrive`, imagePath);
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
try {
|
||||
const stream = await minioClient.getObject(bucketName, filePath);
|
||||
stream.on('data', chunk => chunks.push(chunk));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
stream.on('error', reject);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
const gbdriveName = GBUtil.getGBAIPath(botId, 'gbdrive');
|
||||
const fullPath = path.join(GBConfigService.get('STORAGE_LIBRARY'), gbdriveName, imagePath);
|
||||
return fs.readFile(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
private createMinioClient(): Client {
|
||||
return new Client({
|
||||
endPoint: process.env.DRIVE_SERVER || 'localhost',
|
||||
port: parseInt(process.env.DRIVE_PORT || '9000', 10),
|
||||
useSSL: process.env.DRIVE_USE_SSL === 'true',
|
||||
accessKey: process.env.DRIVE_ACCESSKEY,
|
||||
secretKey: process.env.DRIVE_SECRET,
|
||||
});
|
||||
}
|
||||
|
||||
private async processImagesInData(min: any, gbaiName: string, data: any): Promise<any[]> {
|
||||
const images = [];
|
||||
let index = 0;
|
||||
packagePath = path.join(gbaiName, 'cache', `tmp${GBAdminService.getRndReadableIdentifier()}.docx`);
|
||||
url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', path.basename(localName));
|
||||
|
||||
const traverseDataToInjectImageUrl = async o => {
|
||||
for (var i in o) {
|
||||
const traverseDataToInjectImageUrl = async (o: any) => {
|
||||
for (const i in o) {
|
||||
let value = o[i];
|
||||
|
||||
if (value && value.gbarray) {
|
||||
|
@ -2150,111 +2257,86 @@ export class SystemKeywords {
|
|||
}
|
||||
|
||||
for (const kind of ['png', 'jpg', 'jpeg']) {
|
||||
if (value.endsWith && value.endsWith(`.${kind}`)) {
|
||||
const { baseUrl, client } = await GBDeployer.internalGetDriveClient(min);
|
||||
|
||||
packagePath = urlJoin(gbaiName, `${botId}.gbdrive`);
|
||||
if (value.indexOf('/') !== -1) {
|
||||
packagePath = '/' + urlJoin(packagePath, path.dirname(value));
|
||||
value = path.basename(value);
|
||||
}
|
||||
|
||||
const ref = await this.internalGetDocument(client, baseUrl, packagePath, value);
|
||||
let url = ref['@microsoft.graph.downloadUrl'];
|
||||
if (value?.endsWith?.(`.${kind}`)) {
|
||||
const imageBuffer = await this.getImageBuffer(min, gbaiName, value);
|
||||
const imageName = path.join(
|
||||
'work',
|
||||
gbaiName,
|
||||
'cache',
|
||||
`tmp${GBAdminService.getRndReadableIdentifier()}-${value}.png`
|
||||
`tmp${GBAdminService.getRndReadableIdentifier()}-${path.basename(value)}.png`
|
||||
);
|
||||
const response = await fetch(url);
|
||||
const buf = Buffer.from(await response.arrayBuffer());
|
||||
await fs.writeFile(imageName, new Uint8Array(buf), { encoding: null });
|
||||
await fs.writeFile(imageName, new Uint8Array(imageBuffer), { encoding: null });
|
||||
|
||||
const getNormalSize = ({ width, height, orientation }) => {
|
||||
const getNormalSize = ({ width, height, orientation }: any) => {
|
||||
return (orientation || 0) >= 5 ? [height, width] : [width, height];
|
||||
};
|
||||
|
||||
// TODO: sharp. const metadata = await sharp(buf).metadata();
|
||||
const size = getNormalSize({
|
||||
width: 400,
|
||||
height: 400,
|
||||
orientation: '0'
|
||||
});
|
||||
url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', path.basename(imageName));
|
||||
images[index++] = { url: url, size: size, buf: buf };
|
||||
const url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', path.basename(imageName));
|
||||
images[index++] = { url, size, buf: imageBuffer };
|
||||
}
|
||||
}
|
||||
if (o[i] !== null && typeof o[i] == 'object') {
|
||||
|
||||
if (o[i] !== null && typeof o[i] === 'object') {
|
||||
await traverseDataToInjectImageUrl(o[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await traverseDataToInjectImageUrl(data);
|
||||
return images;
|
||||
}
|
||||
|
||||
public async fill({ pid, templateName, data }) {
|
||||
const { min, user } = await DialogKeywords.getProcessInfo(pid);
|
||||
const botId = min.instance.botId;
|
||||
const gbaiName = GBUtil.getGBAIPath(botId);
|
||||
|
||||
// Get template buffer based on GB_MODE
|
||||
const templateBuffer = await this.getTemplateBuffer(min, gbaiName, templateName);
|
||||
|
||||
// Process images in data
|
||||
const images = await this.processImagesInData(min, gbaiName, data);
|
||||
|
||||
// Prepare local file
|
||||
const localName = path.join('work', gbaiName, 'cache', `tmp${GBAdminService.getRndReadableIdentifier()}.docx`);
|
||||
const url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', path.basename(localName));
|
||||
|
||||
// Prepare docxtemplater options
|
||||
let indexImage = 0;
|
||||
var opts = {
|
||||
const opts = {
|
||||
fileType: 'docx',
|
||||
centered: false,
|
||||
getImage: (tagValue, tagName) => {
|
||||
return images[indexImage].buf;
|
||||
},
|
||||
getSize: (img, tagValue, tagName) => {
|
||||
return images[indexImage++].size;
|
||||
}
|
||||
getImage: () => images[indexImage].buf,
|
||||
getSize: () => images[indexImage++].size
|
||||
};
|
||||
|
||||
// Loads the file as binary content.
|
||||
|
||||
let zip = new PizZip(buf);
|
||||
let doc = new Docxtemplater();
|
||||
// Process the template
|
||||
const zip = new PizZip(templateBuffer);
|
||||
const doc = new Docxtemplater();
|
||||
doc.setOptions({ paragraphLoop: true, linebreaks: true });
|
||||
doc.loadZip(zip);
|
||||
|
||||
if (localName.endsWith('.pptx')) {
|
||||
doc.attachModule(pptxTemplaterModule);
|
||||
}
|
||||
|
||||
doc.attachModule(new ImageModule(opts));
|
||||
doc.render(data);
|
||||
|
||||
await traverseDataToInjectImageUrl(data);
|
||||
doc.setData(data).render();
|
||||
const outputBuffer = doc.getZip().generate({ type: 'nodebuffer', compression: 'DEFLATE' });
|
||||
await fs.writeFile(localName, new Uint8Array(outputBuffer), { encoding: null });
|
||||
|
||||
buf = doc.getZip().generate({ type: 'nodebuffer', compression: 'DEFLATE' });
|
||||
await fs.writeFile(localName, new Uint8Array(buf), { encoding: null });
|
||||
|
||||
return { localName: localName, url: url, data: buf };
|
||||
return { localName, url, data: outputBuffer };
|
||||
}
|
||||
|
||||
|
||||
public screenCapture(pid) {
|
||||
// scrcpy Disabled
|
||||
// function captureImage({ x, y, w, h }) {
|
||||
// const pic = robot.screen.capture(x, y, w, h)
|
||||
// const width = pic.byteWidth / pic.bytesPerPixel // pic.width is sometimes wrong!
|
||||
// const height = pic.height
|
||||
// const image = new Jimp(width, height)
|
||||
// let red, green, blue
|
||||
// pic.image.forEach((byte, i) => {
|
||||
// switch (i % 4) {
|
||||
// case 0: return blue = byte
|
||||
// case 1: return green = byte
|
||||
// case 2: return red = byte
|
||||
// case 3:
|
||||
// image.bitmap.data[i - 3] = red
|
||||
// image.bitmap.data[i - 2] = green
|
||||
// image.bitmap.data[i - 1] = blue
|
||||
// image.bitmap.data[i] = 255
|
||||
// }
|
||||
// })
|
||||
// return image
|
||||
// }
|
||||
// let file = 'out.png';
|
||||
// captureImage({ x: 60, y: 263, w: 250, h: 83 }).write(file)
|
||||
// const config = {
|
||||
// lang: "eng",
|
||||
// oem: 1,
|
||||
// psm: 3,
|
||||
// }
|
||||
// tesseract.recognize(file, config).then(value => {
|
||||
// console.log(value);
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
private numberToLetters(num) {
|
||||
|
@ -2617,7 +2699,7 @@ export class SystemKeywords {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HEAR description
|
||||
* text = REWRITE description
|
||||
|
|
|
@ -52,7 +52,7 @@ import isICO from 'icojs';
|
|||
import getColors from 'get-image-colors';
|
||||
import { Document } from 'langchain/document';
|
||||
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
import puppeteer, { executablePath, Page } from 'puppeteer';
|
||||
import { Jimp } from 'jimp';
|
||||
import {
|
||||
GBDialogStep,
|
||||
|
@ -1087,7 +1087,10 @@ export class KBService implements IGBKBService {
|
|||
'--no-default-browser-check'
|
||||
];
|
||||
|
||||
let browser = await puppeteer.launch({ headless: false, args });
|
||||
let browser = await puppeteer.launch({ headless: true,
|
||||
executablePath: process.env.CHROME_PATH ? process.env.CHROME_PATH : executablePath(),
|
||||
|
||||
args });
|
||||
const page = await this.getFreshPage(browser, website);
|
||||
|
||||
let logo = await this.getLogoByPage(min, page);
|
||||
|
|
Loading…
Add table
Reference in a new issue