botql 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Connectors.js +105 -0
- package/Database.js +146 -0
- package/FileSystem.js +108 -0
- package/LICENSE +21 -0
- package/Parser.js +762 -0
- package/RAG.js +572 -0
- package/README.md +23 -0
- package/botql.js +789 -0
- package/package.json +34 -0
package/Connectors.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ConnectorRegistry — resolve o nome de uma plataforma (ex: "TELEGRAM")
|
|
7
|
+
* para o módulo que sabe falar a API real dessa plataforma.
|
|
8
|
+
*
|
|
9
|
+
* O registry NÃO tenta cobrir todas as plataformas existentes — só regista
|
|
10
|
+
* as que o próprio projeto usa, tal como o xlang-modules.json faz para
|
|
11
|
+
* bibliotecas. Uma plataforma sem entrada dá erro claro, não falha
|
|
12
|
+
* silenciosa.
|
|
13
|
+
*
|
|
14
|
+
* O mapeamento plataforma -> ficheiro vive em Connectors.json (JSON puro,
|
|
15
|
+
* sem lógica), esta classe é que sabe carregar e instanciar esses ficheiros.
|
|
16
|
+
*/
|
|
17
|
+
class ConnectorRegistry {
|
|
18
|
+
constructor(registryPathOrObject = path.join(__dirname, 'Connectors.json')) {
|
|
19
|
+
if (typeof registryPathOrObject === 'string') {
|
|
20
|
+
this.baseDir = path.dirname(path.resolve(registryPathOrObject));
|
|
21
|
+
this.registry = { ...require(path.resolve(registryPathOrObject)) };
|
|
22
|
+
} else {
|
|
23
|
+
this.baseDir = __dirname;
|
|
24
|
+
this.registry = { ...registryPathOrObject };
|
|
25
|
+
}
|
|
26
|
+
this.cache = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Regista/substitui uma plataforma em runtime. Aceita:
|
|
30
|
+
// - uma função factory: (credential) => connector
|
|
31
|
+
// - o caminho (string) para um módulo que exporta essa factory
|
|
32
|
+
register(platform, moduleOrFactory) {
|
|
33
|
+
this.registry[platform] = typeof moduleOrFactory === 'function'
|
|
34
|
+
? { factory: moduleOrFactory }
|
|
35
|
+
: moduleOrFactory;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resolve(platform, credential) {
|
|
39
|
+
const entry = this.registry[platform];
|
|
40
|
+
if (!entry) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`BotQL: nenhum connector registado para a plataforma "${platform}". ` +
|
|
43
|
+
`Regista-a em Connectors.json ou via registry.register("${platform}", adapter).`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cacheKey = `${platform}:${credential}`;
|
|
48
|
+
if (this.cache.has(cacheKey)) return this.cache.get(cacheKey);
|
|
49
|
+
|
|
50
|
+
// entry pode ser: string (caminho de módulo), ou { factory } vindo de register().
|
|
51
|
+
const factory = typeof entry === 'string'
|
|
52
|
+
? require(path.resolve(this.baseDir, entry))
|
|
53
|
+
: entry.factory || require(path.resolve(this.baseDir, entry.module));
|
|
54
|
+
|
|
55
|
+
const connector = factory(credential);
|
|
56
|
+
this.cache.set(cacheKey, connector);
|
|
57
|
+
return connector;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Liga um BotQLInterpreter já carregado a esta registry: para cada
|
|
63
|
+
* CONNECT registado no bot, resolve o connector real e passa a usá-lo
|
|
64
|
+
* como destino de REPLY / FORWARD TO / SEND TO para essa plataforma.
|
|
65
|
+
*
|
|
66
|
+
* Chamadas manuais a onReply/onForward/onSend passadas nas opções do
|
|
67
|
+
* interpreter continuam a ter prioridade — isto só preenche o que não
|
|
68
|
+
* foi definido à mão (onReply/onForward/onSend ficam `null` no
|
|
69
|
+
* interpreter enquanto não forem definidos, ver botql.js).
|
|
70
|
+
*/
|
|
71
|
+
function attachConnectors(interpreter, registry) {
|
|
72
|
+
const byService = new Map();
|
|
73
|
+
for (const conn of interpreter.connections) {
|
|
74
|
+
byService.set(conn.service, registry.resolve(conn.service, conn.credential));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const platformConnector = interpreter.platform ? byService.get(interpreter.platform) : null;
|
|
78
|
+
|
|
79
|
+
if (!interpreter.onReply) {
|
|
80
|
+
interpreter.onReply = async ({ target, text, client }) => {
|
|
81
|
+
const connector = platformConnector || byService.get('ADMIN');
|
|
82
|
+
if (!connector) return;
|
|
83
|
+
await connector.send(target || client, text);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!interpreter.onForward) {
|
|
88
|
+
interpreter.onForward = async ({ target, client }) => {
|
|
89
|
+
if (!platformConnector) return;
|
|
90
|
+
await platformConnector.send(target, `Encaminhado de ${client}`);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!interpreter.onSend) {
|
|
95
|
+
interpreter.onSend = async ({ target, signal }) => {
|
|
96
|
+
const connector = byService.get(target);
|
|
97
|
+
if (!connector) return;
|
|
98
|
+
await connector.send(target, JSON.stringify(signal));
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return interpreter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { ConnectorRegistry, attachConnectors };
|
package/Database.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Database.js — Camada de banco de dados do BotQL
|
|
5
|
+
*
|
|
6
|
+
* Qualquer adapter aqui implementa a mesma interface, usada pelo botql.js:
|
|
7
|
+
* createTable(name, columns, preventDefault)
|
|
8
|
+
* insert(table, columnNames, values) -> id
|
|
9
|
+
* update(table, column, value, whereColumn, whereValue)
|
|
10
|
+
* getRows(table) -> array de linhas
|
|
11
|
+
*
|
|
12
|
+
* MemoryDatabase: guarda tudo em memória, sem persistência — bom para
|
|
13
|
+
* testes e para o exemplo do botql.js.
|
|
14
|
+
*
|
|
15
|
+
* SQLiteDatabase: persiste num ficheiro .sqlite real, usando o módulo
|
|
16
|
+
* nativo `node:sqlite` (Node 22+), sem dependências externas.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const CONTEXT_SCHEMA = [
|
|
20
|
+
{ name: 'id', columnType: 'INT', constraints: ['PRIMARY', 'KEY', 'AUTO_INCREMENT'] },
|
|
21
|
+
{ name: 'client', columnType: 'TEXT', constraints: [] },
|
|
22
|
+
{ name: 'message', columnType: 'TEXT', constraints: [] },
|
|
23
|
+
{ name: 'reply', columnType: 'TEXT', constraints: [] },
|
|
24
|
+
{ name: 'created_at', columnType: 'DATETIME', constraints: [] }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ===== MemoryDatabase =====
|
|
28
|
+
|
|
29
|
+
class MemoryDatabase {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.tables = new Map();
|
|
32
|
+
this.autoIncrement = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
createTable(name, columns, preventDefault) {
|
|
36
|
+
const resolvedColumns = (name === 'Context' && columns.length === 0)
|
|
37
|
+
? CONTEXT_SCHEMA
|
|
38
|
+
: columns;
|
|
39
|
+
|
|
40
|
+
if (this.tables.has(name)) {
|
|
41
|
+
if (preventDefault) return;
|
|
42
|
+
throw new Error(`runtime error: table "${name}" already exists`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.tables.set(name, { columns: resolvedColumns, rows: [] });
|
|
46
|
+
this.autoIncrement.set(name, 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
insert(table, columnNames, values) {
|
|
50
|
+
const t = this.tables.get(table);
|
|
51
|
+
if (!t) throw new Error(`runtime error: table "${table}" does not exist`);
|
|
52
|
+
|
|
53
|
+
const nextId = this.autoIncrement.get(table) + 1;
|
|
54
|
+
this.autoIncrement.set(table, nextId);
|
|
55
|
+
|
|
56
|
+
const row = { id: nextId };
|
|
57
|
+
columnNames.forEach((col, i) => { row[col] = values[i]; });
|
|
58
|
+
t.rows.push(row);
|
|
59
|
+
|
|
60
|
+
return nextId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
update(table, column, value, whereColumn, whereValue) {
|
|
64
|
+
const t = this.tables.get(table);
|
|
65
|
+
if (!t) throw new Error(`runtime error: table "${table}" does not exist`);
|
|
66
|
+
|
|
67
|
+
const row = whereColumn
|
|
68
|
+
? t.rows.find((r) => r[whereColumn] === whereValue)
|
|
69
|
+
: t.rows[t.rows.length - 1];
|
|
70
|
+
|
|
71
|
+
if (row) row[column] = value;
|
|
72
|
+
return row || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getRows(table) {
|
|
76
|
+
const t = this.tables.get(table);
|
|
77
|
+
return t ? t.rows.slice() : [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ===== SQLiteDatabase =====
|
|
82
|
+
|
|
83
|
+
function mapColumnType(columnType) {
|
|
84
|
+
const type = (columnType || '').toUpperCase();
|
|
85
|
+
if (type === 'INT' || type === 'INTEGER') return 'INTEGER';
|
|
86
|
+
if (type === 'DATETIME' || type === 'DATE') return 'TEXT';
|
|
87
|
+
return 'TEXT';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildColumnDef(col) {
|
|
91
|
+
const type = mapColumnType(col.columnType);
|
|
92
|
+
const isPrimary = col.constraints.includes('PRIMARY') && col.constraints.includes('KEY');
|
|
93
|
+
const isAutoIncrement = col.constraints.includes('AUTO_INCREMENT');
|
|
94
|
+
|
|
95
|
+
let def = `${col.name} ${type}`;
|
|
96
|
+
if (isPrimary) def += ' PRIMARY KEY';
|
|
97
|
+
if (isPrimary && isAutoIncrement) def += ' AUTOINCREMENT';
|
|
98
|
+
return def;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class SQLiteDatabase {
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} filePath Caminho do ficheiro .sqlite, ou ":memory:" para um banco temporário.
|
|
104
|
+
*/
|
|
105
|
+
constructor(filePath = ':memory:') {
|
|
106
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
107
|
+
this.driver = new DatabaseSync(filePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
createTable(name, columns, preventDefault) {
|
|
111
|
+
const resolvedColumns = (name === 'Context' && columns.length === 0)
|
|
112
|
+
? CONTEXT_SCHEMA
|
|
113
|
+
: columns;
|
|
114
|
+
|
|
115
|
+
const columnDefs = resolvedColumns.map(buildColumnDef).join(', ');
|
|
116
|
+
const ifNotExists = preventDefault ? 'IF NOT EXISTS ' : '';
|
|
117
|
+
this.driver.exec(`CREATE TABLE ${ifNotExists}${name} (${columnDefs})`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
insert(table, columnNames, values) {
|
|
121
|
+
const placeholders = columnNames.map(() => '?').join(', ');
|
|
122
|
+
const sql = `INSERT INTO ${table} (${columnNames.join(', ')}) VALUES (${placeholders})`;
|
|
123
|
+
const info = this.driver.prepare(sql).run(...values);
|
|
124
|
+
return Number(info.lastInsertRowid);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
update(table, column, value, whereColumn, whereValue) {
|
|
128
|
+
if (whereColumn) {
|
|
129
|
+
const sql = `UPDATE ${table} SET ${column} = ? WHERE ${whereColumn} = ?`;
|
|
130
|
+
this.driver.prepare(sql).run(value, whereValue);
|
|
131
|
+
} else {
|
|
132
|
+
const sql = `UPDATE ${table} SET ${column} = ? WHERE id = (SELECT MAX(id) FROM ${table})`;
|
|
133
|
+
this.driver.prepare(sql).run(value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getRows(table) {
|
|
138
|
+
return this.driver.prepare(`SELECT * FROM ${table}`).all();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
close() {
|
|
142
|
+
this.driver.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { MemoryDatabase, SQLiteDatabase };
|
package/FileSystem.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FileSystem.js — Acesso a ficheiros isomorfico (Node ou browser)
|
|
5
|
+
*
|
|
6
|
+
* botql.js precisa de ler ficheiros para resolver IMPORT. Em Node isso e
|
|
7
|
+
* fs.readFileSync + path real. No browser (ou em qualquer ambiente sem
|
|
8
|
+
* fs), nao ha disco: os "ficheiros" sao apenas texto que o utilizador
|
|
9
|
+
* forneceu antecipadamente (ex: nos campos de import do editor).
|
|
10
|
+
*
|
|
11
|
+
* Qualquer adapter aqui implementa o mesmo contrato, usado por botql.js:
|
|
12
|
+
* exists(path) -> boolean
|
|
13
|
+
* readFile(path) -> string
|
|
14
|
+
* resolve(basePath, relativePath) -> string (caminho absoluto/canonico)
|
|
15
|
+
* dirname(path) -> string
|
|
16
|
+
*
|
|
17
|
+
* NodeFileSystem: usa fs/path reais do Node. E o omissao quando o codigo
|
|
18
|
+
* corre em Node (Bootstrap.js, CLI, testes).
|
|
19
|
+
*
|
|
20
|
+
* MemoryFileSystem: guarda ficheiros num Map (caminho -> conteudo) em
|
|
21
|
+
* memoria. Usado no browser: o Index.html regista ai o conteudo de cada
|
|
22
|
+
* ficheiro importado antes de correr o bot. Os caminhos sao tratados como
|
|
23
|
+
* chaves de texto simples, normalizadas (sem "./", sem duplicar "/").
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
class MemoryFileSystem {
|
|
27
|
+
constructor(files = {}) {
|
|
28
|
+
// files: { "caminho/ficheiro.sql": "conteudo..." }
|
|
29
|
+
this.files = new Map(
|
|
30
|
+
Object.entries(files).map(([k, v]) => [this._normalize(k), v])
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_normalize(p) {
|
|
35
|
+
// Remove "./" do inicio e barras duplicadas, mantem caminho relativo simples.
|
|
36
|
+
return String(p).replace(/^\.\//, '').replace(/\/+/g, '/').trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setFile(path, content) {
|
|
40
|
+
this.files.set(this._normalize(path), content);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
exists(path) {
|
|
44
|
+
return this.files.has(this._normalize(path));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
readFile(path) {
|
|
48
|
+
const key = this._normalize(path);
|
|
49
|
+
if (!this.files.has(key)) {
|
|
50
|
+
throw new Error(`BotQL: ficheiro não encontrado: "${path}"`);
|
|
51
|
+
}
|
|
52
|
+
return this.files.get(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// basePath e' ignorado de proposito: no browser nao ha nocao real de
|
|
56
|
+
// diretorio corrente, os ficheiros importados sao identificados pelo
|
|
57
|
+
// nome/caminho tal como o utilizador os registou.
|
|
58
|
+
resolve(basePath, relativePath) {
|
|
59
|
+
return this._normalize(relativePath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
dirname(filePath) {
|
|
63
|
+
const norm = this._normalize(filePath);
|
|
64
|
+
const idx = norm.lastIndexOf('/');
|
|
65
|
+
return idx === -1 ? '' : norm.slice(0, idx);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class NodeFileSystem {
|
|
70
|
+
constructor() {
|
|
71
|
+
this._fs = require('fs');
|
|
72
|
+
this._path = require('path');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
exists(path) {
|
|
76
|
+
return this._fs.existsSync(path);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
readFile(path) {
|
|
80
|
+
return this._fs.readFileSync(path, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
resolve(basePath, relativePath) {
|
|
84
|
+
return this._path.resolve(basePath, relativePath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dirname(filePath) {
|
|
88
|
+
return this._path.dirname(filePath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Deteta o ambiente uma unica vez. `require` e `module` so existem em Node/CommonJS.
|
|
93
|
+
const isNode = typeof process !== 'undefined'
|
|
94
|
+
&& process.versions
|
|
95
|
+
&& !!process.versions.node;
|
|
96
|
+
|
|
97
|
+
function createDefaultFileSystem() {
|
|
98
|
+
return isNode ? new NodeFileSystem() : new MemoryFileSystem();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const api = { MemoryFileSystem, NodeFileSystem, createDefaultFileSystem };
|
|
102
|
+
|
|
103
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
104
|
+
module.exports = api;
|
|
105
|
+
} else {
|
|
106
|
+
// Browser sem bundler/CommonJS: expõe global.
|
|
107
|
+
this.BotQLFileSystem = api;
|
|
108
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adilson C. Rafael
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|