terminal-smart-cli 0.97.6 → 0.97.8
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/bin/ts.js +98 -12
- package/lib/agent.js +125 -12
- package/lib/doctor.js +12 -4
- package/lib/i18n.js +6 -0
- package/lib/intelligence-core.js +95 -6
- package/lib/keyring.js +11 -0
- package/lib/office-editors.js +171 -0
- package/lib/providers.js +24 -7
- package/lib/tools.js +158 -0
- package/lib/ui.js +13 -3
- package/lib/xlsx-compat-editor.js +228 -0
- package/package.json +4 -3
package/lib/intelligence-core.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
|
|
11
11
|
const INTELLIGENCE_CONTRACT = 1;
|
|
12
|
-
const PRICE_REVISION = '2026-
|
|
12
|
+
const PRICE_REVISION = '2026-08-06';
|
|
13
13
|
|
|
14
14
|
const MODEL_CATALOG = Object.freeze({
|
|
15
15
|
smart: {
|
|
@@ -42,8 +42,8 @@ const MODEL_CATALOG = Object.freeze({
|
|
|
42
42
|
capabilities: ['tools', 'vision', 'classification', 'summarization', 'review', 'long-context'], toolReliability: 0.84, quality: 0.80,
|
|
43
43
|
},
|
|
44
44
|
'qwen-plus': {
|
|
45
|
-
upstreamId: '
|
|
46
|
-
provider: 'qwen', contextWindow: 1000000, price: { input: 0.
|
|
45
|
+
upstreamId: 'qwen3.7-plus',
|
|
46
|
+
provider: 'qwen', contextWindow: 1000000, price: { input: 0.42, output: 1.68, cachedInput: 0.042 },
|
|
47
47
|
capabilities: ['tools', 'content', 'code', 'summarization'], toolReliability: 0.76, quality: 0.76,
|
|
48
48
|
},
|
|
49
49
|
'kimi-k2.7-code-highspeed': {
|
|
@@ -89,7 +89,7 @@ const MODEL_CATALOG = Object.freeze({
|
|
|
89
89
|
'gpt-5.6-luna': {
|
|
90
90
|
upstreamId: 'openai/gpt-5.6-luna',
|
|
91
91
|
provider: 'openai', contextWindow: 1000000, price: { input: 1.00, output: 6.00, cachedInput: 0.10 },
|
|
92
|
-
capabilities: ['tools', 'code', 'summarization', 'review', 'long-context'], toolReliability: 0.93, quality: 0.89,
|
|
92
|
+
capabilities: ['tools', 'code', 'planning', 'execution', 'summarization', 'review', 'long-context'], toolReliability: 0.93, quality: 0.89,
|
|
93
93
|
},
|
|
94
94
|
'claude-sonnet-5': {
|
|
95
95
|
upstreamId: 'anthropic/claude-sonnet-5',
|
|
@@ -101,6 +101,24 @@ const MODEL_CATALOG = Object.freeze({
|
|
|
101
101
|
provider: 'anthropic', contextWindow: 1000000, price: { input: 3.00, output: 15.00, cachedInput: 0.30 },
|
|
102
102
|
capabilities: ['tools', 'code', 'planning', 'review'], toolReliability: 0.96, quality: 0.95,
|
|
103
103
|
},
|
|
104
|
+
'claude-haiku-4-5': {
|
|
105
|
+
upstreamId: 'anthropic/claude-haiku-4.5',
|
|
106
|
+
provider: 'anthropic', contextWindow: 200000, price: { input: 1.00, output: 5.00, cachedInput: 0.10 },
|
|
107
|
+
capabilities: ['tools', 'code', 'planning', 'execution', 'review', 'vision'], toolReliability: 0.94, quality: 0.88,
|
|
108
|
+
},
|
|
109
|
+
// Não é um modelo que o produto oferece ao cliente — é o que o assistente interno usa
|
|
110
|
+
// quando o provedor configurado é o Anthropic. Entra no catálogo pelo PREÇO: sem entrada
|
|
111
|
+
// própria o cálculo cai no `_default` (US$ 0,30 entrada / US$ 2,50 saída), o que superestima
|
|
112
|
+
// pouco na entrada e o DOBRO na saída.
|
|
113
|
+
//
|
|
114
|
+
// Isso passou a importar quando o painel começou a decidir preço de plano com esse custo:
|
|
115
|
+
// modelo interno mal precificado empurra a margem medida para baixo e leva a corrigir preço
|
|
116
|
+
// sem precisar. E, ao contrário de um erro que quebra, este só produz um número plausível.
|
|
117
|
+
'claude-3-haiku-20240307': {
|
|
118
|
+
upstreamId: 'anthropic/claude-3-haiku',
|
|
119
|
+
provider: 'anthropic', contextWindow: 200000, price: { input: 0.25, output: 1.25, cachedInput: 0.03 },
|
|
120
|
+
capabilities: ['tools'], toolReliability: 0.75, quality: 0.62,
|
|
121
|
+
},
|
|
104
122
|
'grok-4.5': {
|
|
105
123
|
upstreamId: 'x-ai/grok-4.5',
|
|
106
124
|
provider: 'xai', contextWindow: 500000, price: { input: 2.00, output: 6.00, cachedInput: 0.30 },
|
|
@@ -130,6 +148,49 @@ const MODEL_ALIASES = Object.freeze({
|
|
|
130
148
|
glm: 'glm-4.6',
|
|
131
149
|
mimo: 'mimo-v2.5',
|
|
132
150
|
'gemini-lite': 'gemini-2.5-flash-lite',
|
|
151
|
+
'claude-haiku': 'claude-haiku-4-5',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Contrato dos agentes funcionais. Isto não é fine-tuning: cada papel recebe um
|
|
155
|
+
// prompt curto, uma cadeia de modelos por plano e continua sujeito à prova das
|
|
156
|
+
// ferramentas. O primeiro modelo permitido é o padrão; os seguintes são fallback.
|
|
157
|
+
const AGENT_ROLES = Object.freeze({
|
|
158
|
+
classifier: Object.freeze({
|
|
159
|
+
prompt: 'Classifique a intenção e devolva somente a estrutura pedida. Não execute ferramentas nem invente dados.',
|
|
160
|
+
models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gemini-2.5-flash-lite', 'gpt-4o-mini'], pro: ['gemini-2.5-flash-lite', 'gpt-4o-mini'] }),
|
|
161
|
+
}),
|
|
162
|
+
planner: Object.freeze({
|
|
163
|
+
prompt: 'Decomponha o objetivo em etapas executáveis, dependências, riscos, critérios de aceite e provas. Não execute a tarefa.',
|
|
164
|
+
models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'glm-5.2'] }),
|
|
165
|
+
}),
|
|
166
|
+
executor: Object.freeze({
|
|
167
|
+
prompt: 'Execute somente a etapa recebida com as ferramentas autorizadas. Não declare sucesso sem resultado verificável.',
|
|
168
|
+
models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
|
|
169
|
+
}),
|
|
170
|
+
inspector: Object.freeze({
|
|
171
|
+
prompt: 'Inspecione em modo somente leitura e compare evidências com os critérios de aceite. Não altere nada e não elogie por cortesia.',
|
|
172
|
+
models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gemini-2.5-flash', 'gpt-4o-mini'], pro: ['claude-haiku-4-5', 'gemini-2.5-flash', 'gpt-5.6-luna'] }),
|
|
173
|
+
}),
|
|
174
|
+
corrector: Object.freeze({
|
|
175
|
+
prompt: 'Corrija apenas falhas confirmadas pelo inspetor, preserve o que funciona e repita as provas afetadas. Não amplie o escopo.',
|
|
176
|
+
models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['deepseek-v4-pro', 'claude-haiku-4-5', 'glm-5.2'] }),
|
|
177
|
+
}),
|
|
178
|
+
vision: Object.freeze({
|
|
179
|
+
prompt: 'Extraia fatos observáveis da imagem e separe leitura, inferência e incerteza. Não invente texto ilegível.',
|
|
180
|
+
models: Object.freeze({ free: ['smart'], basic: ['gemini-2.5-flash'], pro: ['gemini-2.5-flash', 'claude-haiku-4-5'] }),
|
|
181
|
+
}),
|
|
182
|
+
document: Object.freeze({
|
|
183
|
+
prompt: 'Produza conteúdo estruturado e use o motor nativo do formato solicitado. Preserve editabilidade e valide o arquivo gerado.',
|
|
184
|
+
models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
|
|
185
|
+
}),
|
|
186
|
+
site_builder: Object.freeze({
|
|
187
|
+
prompt: 'Crie ou edite o site completo, responsivo e acessível. Preserve o existente quando editar e valide a URL publicada.',
|
|
188
|
+
models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-pro', 'deepseek-v4-flash', 'claude-haiku-4-5'] }),
|
|
189
|
+
}),
|
|
190
|
+
cloud_box: Object.freeze({
|
|
191
|
+
prompt: 'Opere a box com mudanças mínimas, registre comandos e valide serviço, porta e URL. Nunca exponha segredos.',
|
|
192
|
+
models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
|
|
193
|
+
}),
|
|
133
194
|
});
|
|
134
195
|
|
|
135
196
|
const DEFAULT_PRICE = Object.freeze({ input: 0.30, output: 2.50, cachedInput: 0.03 });
|
|
@@ -302,6 +363,33 @@ function createHandoff(input) {
|
|
|
302
363
|
};
|
|
303
364
|
}
|
|
304
365
|
|
|
366
|
+
function normalizeAgentPlan(value) {
|
|
367
|
+
const id = String(value || 'free').trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
368
|
+
if (['basic', 'basico'].includes(id)) return 'basic';
|
|
369
|
+
if (['pro', 'ultra', 'business', 'max', 'pro+'].includes(id)) return 'pro';
|
|
370
|
+
return 'free';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function agentRoleContract(role, plan, options) {
|
|
374
|
+
const id = Object.prototype.hasOwnProperty.call(AGENT_ROLES, role) ? role : 'executor';
|
|
375
|
+
const planId = normalizeAgentPlan(plan);
|
|
376
|
+
const spec = AGENT_ROLES[id];
|
|
377
|
+
const requested = spec.models[planId] || spec.models.free;
|
|
378
|
+
const allowedRaw = options && Array.isArray(options.allowedModels) ? options.allowedModels : null;
|
|
379
|
+
const allowAll = !allowedRaw || allowedRaw.includes('*');
|
|
380
|
+
const allowed = allowAll ? null : new Set(allowedRaw.map(normalizeModelId));
|
|
381
|
+
const candidates = requested.map(normalizeModelId).filter(model => model === 'smart' || MODEL_CATALOG[model])
|
|
382
|
+
.filter(model => !allowed || allowed.has(model) || model === 'smart');
|
|
383
|
+
// O fallback nunca atravessa a fronteira comercial. Se a configuração vier
|
|
384
|
+
// inconsistente, usa o primeiro modelo explicitamente permitido, ou smart.
|
|
385
|
+
if (!candidates.length && allowed) {
|
|
386
|
+
const safe = [...allowed].find(model => MODEL_CATALOG[model] || model === 'smart');
|
|
387
|
+
if (safe) candidates.push(safe);
|
|
388
|
+
}
|
|
389
|
+
if (!candidates.length) candidates.push('smart');
|
|
390
|
+
return Object.freeze({ role: id, plan: planId, prompt: spec.prompt, model: candidates[0], candidates: Object.freeze(candidates) });
|
|
391
|
+
}
|
|
392
|
+
|
|
305
393
|
function routeModel(task, reliability) {
|
|
306
394
|
const t = task || {};
|
|
307
395
|
const required = new Set(t.capabilities || []);
|
|
@@ -326,7 +414,8 @@ function routeModel(task, reliability) {
|
|
|
326
414
|
|
|
327
415
|
module.exports = {
|
|
328
416
|
INTELLIGENCE_CONTRACT, PRICE_REVISION, MODEL_CATALOG, MODEL_ALIASES, DEFAULT_PRICE,
|
|
329
|
-
MEMORY_TYPES, MEMORY_TRUST, CONTEXT_SOURCES,
|
|
417
|
+
MEMORY_TYPES, MEMORY_TRUST, CONTEXT_SOURCES, AGENT_ROLES,
|
|
330
418
|
normalizeModelId, modelInfo, legacyPriceMap, normalizeUsage, estimateCostUsd, catalogFreshness,
|
|
331
|
-
createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff,
|
|
419
|
+
createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff,
|
|
420
|
+
normalizeAgentPlan, agentRoleContract, routeModel,
|
|
332
421
|
};
|
package/lib/keyring.js
CHANGED
|
@@ -151,6 +151,15 @@ async function testar(id, { key = '', baseUrl = '', modelo = '', timeoutMs = 150
|
|
|
151
151
|
if (!url) return { ok: false, erro: 'baseUrl não definida' };
|
|
152
152
|
const mdl = modelo || p.modeloPadrao;
|
|
153
153
|
if (!mdl) return { ok: false, erro: 'modelo não definido' };
|
|
154
|
+
const estado = providers.estadoModelo(id, mdl);
|
|
155
|
+
if (!estado.disponivel) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false, aposentado: true, status: 410, modelo: mdl,
|
|
158
|
+
substituto: estado.substituto || '',
|
|
159
|
+
erro: 'o modelo "' + mdl + '" foi aposentado: ' + estado.motivo + '. ' +
|
|
160
|
+
(estado.substituto ? 'Troque para "' + estado.substituto + '" com `ts conectar ' + id + '`.' : 'Escolha outro modelo com `ts conectar ' + id + '`.')
|
|
161
|
+
};
|
|
162
|
+
}
|
|
154
163
|
const _fetch = fetchImpl || fetch;
|
|
155
164
|
|
|
156
165
|
const chamar = async (body) => {
|
|
@@ -191,6 +200,8 @@ async function testar(id, { key = '', baseUrl = '', modelo = '', timeoutMs = 150
|
|
|
191
200
|
}
|
|
192
201
|
if (r1.status === 401 || r1.status === 403) return { ok: false, erro: 'chave recusada pelo provedor (HTTP ' + r1.status + ')' };
|
|
193
202
|
if (r1.status === 404) return { ok: false, erro: 'modelo "' + mdl + '" não existe nesse provedor (HTTP 404)' };
|
|
203
|
+
if (r1.status === 410) return { ok: false, aposentado: true, status: 410, modelo: mdl,
|
|
204
|
+
erro: 'o provedor aposentou o modelo "' + mdl + '" (HTTP 410). Escolha outro com `ts conectar ' + id + '`.' };
|
|
194
205
|
if (r1.status >= 400) {
|
|
195
206
|
const msg = (r1.json && (r1.json.error?.message || r1.json.message)) || ('HTTP ' + r1.status);
|
|
196
207
|
return { ok: false, erro: String(msg).slice(0, 200) };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const JSZip = require('jszip');
|
|
8
|
+
|
|
9
|
+
const MAX_BYTES = 80 * 1024 * 1024;
|
|
10
|
+
const BACKUP_ROOT = path.join(os.homedir(), '.terminal-smart', 'backups', 'office');
|
|
11
|
+
|
|
12
|
+
function _existing(file, extension) {
|
|
13
|
+
const target = path.resolve(String(file || ''));
|
|
14
|
+
if (!target.toLowerCase().endsWith(extension)) throw new Error(`O arquivo precisa terminar em ${extension}.`);
|
|
15
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) throw new Error('Arquivo não encontrado: ' + target);
|
|
16
|
+
if (fs.statSync(target).size > MAX_BYTES) throw new Error('Arquivo maior que 80 MB; edite um recorte ou uma cópia reduzida.');
|
|
17
|
+
return target;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _destination(original, input, extension) {
|
|
21
|
+
if (!input.criar_copia) return original;
|
|
22
|
+
const copy = path.resolve(String(input.caminho_copia || ''));
|
|
23
|
+
if (!copy.toLowerCase().endsWith(extension)) throw new Error(`Para criar cópia, informe caminho_copia terminando em ${extension}.`);
|
|
24
|
+
if (copy === original) throw new Error('caminho_copia deve ser diferente do arquivo original.');
|
|
25
|
+
fs.mkdirSync(path.dirname(copy), { recursive: true });
|
|
26
|
+
return copy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _backup(file) {
|
|
30
|
+
const key = crypto.createHash('sha256').update(file.toLowerCase()).digest('hex').slice(0, 16);
|
|
31
|
+
const dir = path.join(BACKUP_ROOT, key);
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
34
|
+
const backup = path.join(dir, `${path.basename(file)}.${stamp}.bak`);
|
|
35
|
+
fs.copyFileSync(file, backup);
|
|
36
|
+
const backups = fs.readdirSync(dir).filter(name => name.endsWith('.bak')).sort();
|
|
37
|
+
while (backups.length > 10) fs.unlinkSync(path.join(dir, backups.shift()));
|
|
38
|
+
return backup;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _decodeXml(value) {
|
|
42
|
+
return String(value || '').replace(/</g, '<').replace(/>/g, '>')
|
|
43
|
+
.replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _encodeXml(value) {
|
|
47
|
+
return String(value || '').replace(/&/g, '&').replace(/</g, '<')
|
|
48
|
+
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _replaceInXmlText(xml, search, replacement, replaceAll) {
|
|
52
|
+
const re = /<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g;
|
|
53
|
+
const nodes = [];
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = re.exec(xml))) nodes.push({ start: match.index, end: re.lastIndex, raw: match[0], text: _decodeXml(match[1]) });
|
|
56
|
+
const combined = nodes.map(node => node.text).join('');
|
|
57
|
+
const hits = [];
|
|
58
|
+
let at = 0;
|
|
59
|
+
while (search && (at = combined.indexOf(search, at)) >= 0) {
|
|
60
|
+
hits.push(at);
|
|
61
|
+
at += Math.max(1, search.length);
|
|
62
|
+
}
|
|
63
|
+
if (!hits.length) return { xml, count: 0 };
|
|
64
|
+
if (hits.length > 1 && !replaceAll) throw new Error(`Texto ambíguo: ${hits.length} ocorrências. Dê mais contexto ou use todas=true.`);
|
|
65
|
+
|
|
66
|
+
const starts = [];
|
|
67
|
+
let cursor = 0;
|
|
68
|
+
for (const node of nodes) { starts.push(cursor); cursor += node.text.length; }
|
|
69
|
+
const selected = replaceAll ? hits : hits.slice(0, 1);
|
|
70
|
+
for (let h = selected.length - 1; h >= 0; h--) {
|
|
71
|
+
const begin = selected[h];
|
|
72
|
+
const finish = begin + search.length;
|
|
73
|
+
let first = nodes.findIndex((node, index) => begin >= starts[index] && begin < starts[index] + node.text.length);
|
|
74
|
+
let last = nodes.findIndex((node, index) => finish > starts[index] && finish <= starts[index] + node.text.length);
|
|
75
|
+
if (first < 0 || last < 0) throw new Error('Não foi possível mapear o texto no DOCX.');
|
|
76
|
+
const left = begin - starts[first];
|
|
77
|
+
const right = finish - starts[last];
|
|
78
|
+
if (first === last) nodes[first].text = nodes[first].text.slice(0, left) + replacement + nodes[first].text.slice(right);
|
|
79
|
+
else {
|
|
80
|
+
nodes[first].text = nodes[first].text.slice(0, left) + replacement;
|
|
81
|
+
for (let index = first + 1; index < last; index++) nodes[index].text = '';
|
|
82
|
+
nodes[last].text = nodes[last].text.slice(right);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let output = ''; let sourceAt = 0;
|
|
86
|
+
for (const node of nodes) {
|
|
87
|
+
output += xml.slice(sourceAt, node.start);
|
|
88
|
+
output += node.raw.replace(/(<w:t(?:\s[^>]*)?>)[\s\S]*?(<\/w:t>)/, `$1${_encodeXml(node.text)}$2`);
|
|
89
|
+
sourceAt = node.end;
|
|
90
|
+
}
|
|
91
|
+
output += xml.slice(sourceAt);
|
|
92
|
+
return { xml: output, count: selected.length };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function editarDocumento(input = {}) {
|
|
96
|
+
try {
|
|
97
|
+
const original = _existing(input.caminho, '.docx');
|
|
98
|
+
const search = String(input.buscar || '');
|
|
99
|
+
const replacement = String(input.substituir ?? '');
|
|
100
|
+
if (!search) throw new Error('Informe buscar com o texto exato que será alterado.');
|
|
101
|
+
const destination = _destination(original, input, '.docx');
|
|
102
|
+
const zip = await JSZip.loadAsync(fs.readFileSync(original));
|
|
103
|
+
const xmlFiles = Object.keys(zip.files).filter(name => /^word\/(document|header\d+|footer\d+|footnotes|endnotes)\.xml$/.test(name));
|
|
104
|
+
let changes = 0;
|
|
105
|
+
for (const name of xmlFiles) {
|
|
106
|
+
const xml = await zip.file(name).async('string');
|
|
107
|
+
const result = _replaceInXmlText(xml, search, replacement, !!input.todas);
|
|
108
|
+
if (result.count) { zip.file(name, result.xml); changes += result.count; }
|
|
109
|
+
if (changes && !input.todas) break;
|
|
110
|
+
}
|
|
111
|
+
if (!changes) throw new Error('Texto não encontrado no documento. Leia o arquivo e copie um trecho exato.');
|
|
112
|
+
const backup = destination === original ? _backup(original) : null;
|
|
113
|
+
const buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
114
|
+
fs.writeFileSync(destination, buffer);
|
|
115
|
+
return { ok: true, caminho: destination, editou_original: destination === original, ocorrencias: changes, backup };
|
|
116
|
+
} catch (error) { return { ok: false, erro: 'Falha ao editar DOCX: ' + error.message }; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function editarPlanilha(input = {}) {
|
|
120
|
+
try {
|
|
121
|
+
const original = _existing(input.caminho, '.xlsx');
|
|
122
|
+
const destination = _destination(original, input, '.xlsx');
|
|
123
|
+
const ExcelJS = require('exceljs');
|
|
124
|
+
const workbook = new ExcelJS.Workbook();
|
|
125
|
+
try {
|
|
126
|
+
await workbook.xlsx.readFile(original);
|
|
127
|
+
} catch (readError) {
|
|
128
|
+
const backup = destination === original ? _backup(original) : null;
|
|
129
|
+
const { editXlsxCompat } = require('./xlsx-compat-editor');
|
|
130
|
+
const result = await editXlsxCompat(original, destination, input);
|
|
131
|
+
return { ok: true, caminho: destination, editou_original: destination === original, celulas_alteradas: result.changes.length, alteracoes: result.changes.slice(0, 100), backup, engine: result.engine, compatibilidade: readError.message };
|
|
132
|
+
}
|
|
133
|
+
const changes = [];
|
|
134
|
+
for (const update of (Array.isArray(input.alteracoes) ? input.alteracoes : []).slice(0, 500)) {
|
|
135
|
+
const sheet = workbook.getWorksheet(String(update.aba || '')) || (!update.aba ? workbook.worksheets[0] : null);
|
|
136
|
+
if (!sheet) throw new Error(`Aba não encontrada: ${update.aba}`);
|
|
137
|
+
const address = String(update.celula || '').toUpperCase();
|
|
138
|
+
if (!/^[A-Z]{1,3}[1-9][0-9]{0,6}$/.test(address)) throw new Error(`Célula inválida: ${update.celula}`);
|
|
139
|
+
const cell = sheet.getCell(address);
|
|
140
|
+
if (update.formula != null) cell.value = { formula: String(update.formula).replace(/^=/, '') };
|
|
141
|
+
else {
|
|
142
|
+
const raw = update.valor;
|
|
143
|
+
cell.value = typeof raw === 'string' && /^-?\d+(?:[.,]\d+)?$/.test(raw.trim())
|
|
144
|
+
? Number(raw.trim().replace(',', '.'))
|
|
145
|
+
: (raw == null ? null : raw);
|
|
146
|
+
}
|
|
147
|
+
changes.push(`${sheet.name}!${address}`);
|
|
148
|
+
}
|
|
149
|
+
for (const rule of (Array.isArray(input.substituicoes) ? input.substituicoes : []).slice(0, 50)) {
|
|
150
|
+
const search = String(rule.buscar || '');
|
|
151
|
+
if (!search) continue;
|
|
152
|
+
let replaced = 0;
|
|
153
|
+
for (const sheet of workbook.worksheets) {
|
|
154
|
+
if (rule.aba && sheet.name.toLowerCase() !== String(rule.aba).toLowerCase()) continue;
|
|
155
|
+
sheet.eachRow(row => row.eachCell(cell => {
|
|
156
|
+
if (!rule.todas && replaced) return;
|
|
157
|
+
if (typeof cell.value !== 'string' || !cell.value.includes(search)) return;
|
|
158
|
+
cell.value = rule.todas ? cell.value.split(search).join(String(rule.substituir ?? '')) : cell.value.replace(search, String(rule.substituir ?? ''));
|
|
159
|
+
replaced++; changes.push(`${sheet.name}!${cell.address}`);
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
if (!replaced) throw new Error(`Texto não encontrado na planilha: ${search.slice(0, 80)}`);
|
|
163
|
+
}
|
|
164
|
+
if (!changes.length) throw new Error('Informe alteracoes ou substituicoes.');
|
|
165
|
+
const backup = destination === original ? _backup(original) : null;
|
|
166
|
+
await workbook.xlsx.writeFile(destination);
|
|
167
|
+
return { ok: true, caminho: destination, editou_original: destination === original, celulas_alteradas: changes.length, alteracoes: changes.slice(0, 100), backup, engine: 'exceljs' };
|
|
168
|
+
} catch (error) { return { ok: false, erro: 'Falha ao editar XLSX: ' + error.message }; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { editarDocumento, editarPlanilha, _replaceInXmlText };
|
package/lib/providers.js
CHANGED
|
@@ -16,13 +16,11 @@ const PROVEDORES = {
|
|
|
16
16
|
nvidia: {
|
|
17
17
|
nome: 'NVIDIA NIM',
|
|
18
18
|
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
// resto do TS já usa, então é o padrão sensato aqui.
|
|
23
|
-
modeloPadrao: 'deepseek-ai/deepseek-v4-flash',
|
|
19
|
+
// O id sem sufixo foi retirado pela NVIDIA (HTTP 410). O catálogo atual expõe
|
|
20
|
+
// a revisão datada; manter o id antigo como padrão faria toda chave nova falhar.
|
|
21
|
+
modeloPadrao: 'deepseek-ai/deepseek-v4-flash-0731',
|
|
24
22
|
sugestoes: [
|
|
25
|
-
['deepseek-ai/deepseek-v4-flash', '
|
|
23
|
+
['deepseek-ai/deepseek-v4-flash-0731', 'revisão atual do DeepSeek V4 Flash'],
|
|
26
24
|
['meta/llama-3.1-8b-instruct', 'o mais rápido — ~1s, bom pra chat/roteamento'],
|
|
27
25
|
['mistralai/mistral-medium-3.5-128b', 'mais capaz — ~43s'],
|
|
28
26
|
],
|
|
@@ -88,6 +86,17 @@ const PROVEDORES = {
|
|
|
88
86
|
},
|
|
89
87
|
};
|
|
90
88
|
|
|
89
|
+
// IDs que o provedor encerrou. Esta lista é deliberadamente conservadora: só
|
|
90
|
+
// bloqueamos localmente quando a aposentadoria foi confirmada na API real.
|
|
91
|
+
const MODELOS_APOSENTADOS = {
|
|
92
|
+
nvidia: {
|
|
93
|
+
'deepseek-ai/deepseek-v4-flash': {
|
|
94
|
+
substituto: 'deepseek-ai/deepseek-v4-flash-0731',
|
|
95
|
+
motivo: 'a NVIDIA retirou esse id (HTTP 410)',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
91
100
|
function info(id) { return PROVEDORES[String(id || '').toLowerCase()] || null; }
|
|
92
101
|
function ids() { return Object.keys(PROVEDORES); }
|
|
93
102
|
|
|
@@ -115,4 +124,12 @@ function baseUrlDe(id, override) {
|
|
|
115
124
|
return p ? p.baseUrl : '';
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
|
|
127
|
+
function estadoModelo(id, modelo) {
|
|
128
|
+
const pid = String(id || '').toLowerCase();
|
|
129
|
+
const mid = String(modelo || '').trim();
|
|
130
|
+
const aposentado = MODELOS_APOSENTADOS[pid] && MODELOS_APOSENTADOS[pid][mid];
|
|
131
|
+
return aposentado ? { disponivel: false, aposentado: true, modelo: mid, ...aposentado }
|
|
132
|
+
: { disponivel: true, aposentado: false, modelo: mid };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { PROVEDORES, MODELOS_APOSENTADOS, info, ids, validarChave, baseUrlDe, estadoModelo };
|
package/lib/tools.js
CHANGED
|
@@ -161,6 +161,73 @@ const DEFS = [
|
|
|
161
161
|
caminho: { type: 'string' }, buscar: { type: 'string' },
|
|
162
162
|
inicio_char: { type: 'number' }, tamanho: { type: 'number' },
|
|
163
163
|
}, required: ['caminho'] } } },
|
|
164
|
+
{ type: 'function', function: { name: 'editar_documento',
|
|
165
|
+
description: 'Edita um .docx EXISTENTE no próprio arquivo por padrão, preserva o restante e cria backup automático. Só cria outro arquivo se o usuário pedir uma cópia.',
|
|
166
|
+
parameters: { type: 'object', properties: {
|
|
167
|
+
caminho: { type: 'string' }, buscar: { type: 'string' }, substituir: { type: 'string' },
|
|
168
|
+
todas: { type: 'boolean' }, criar_copia: { type: 'boolean' }, caminho_copia: { type: 'string' },
|
|
169
|
+
}, required: ['caminho', 'buscar', 'substituir'] } } },
|
|
170
|
+
{ type: 'function', function: { name: 'editar_planilha',
|
|
171
|
+
description: 'Edita uma planilha .xlsx EXISTENTE no próprio arquivo por padrão. Altera células/fórmulas ou substitui texto, preserva as demais abas e cria backup automático. Só cria outro arquivo se o usuário pedir cópia.',
|
|
172
|
+
parameters: { type: 'object', properties: {
|
|
173
|
+
caminho: { type: 'string' },
|
|
174
|
+
alteracoes: { type: 'array', items: { type: 'object', properties: {
|
|
175
|
+
aba: { type: 'string' }, celula: { type: 'string' }, valor: { type: 'string' }, formula: { type: 'string' },
|
|
176
|
+
}, required: ['celula'] } },
|
|
177
|
+
substituicoes: { type: 'array', items: { type: 'object', properties: {
|
|
178
|
+
aba: { type: 'string' }, buscar: { type: 'string' }, substituir: { type: 'string' }, todas: { type: 'boolean' },
|
|
179
|
+
}, required: ['buscar', 'substituir'] } },
|
|
180
|
+
criar_copia: { type: 'boolean' }, caminho_copia: { type: 'string' },
|
|
181
|
+
}, required: ['caminho'] } } },
|
|
182
|
+
{ type: 'function', function: { name: 'status_microsoft365', description: 'Verifica a conexão com Outlook e Microsoft 365.', parameters: { type: 'object', properties: {} } } },
|
|
183
|
+
{ type: 'function', function: { name: 'conectar_microsoft365', description: 'Gera o link oficial para autorizar Outlook e Microsoft 365.', parameters: { type: 'object', properties: {} } } },
|
|
184
|
+
{ type: 'function', function: { name: 'listar_emails_outlook', description: 'Lista, pesquisa e prioriza e-mails do Outlook sem alterar nada.', parameters: { type: 'object', properties: { limite: { type: 'number' }, somente_nao_lidos: { type: 'boolean' }, priorizar: { type: 'boolean' }, consulta: { type: 'string' }, pasta: { type: 'string' } } } } },
|
|
185
|
+
{ type: 'function', function: { name: 'ler_email_outlook', description: 'Lê o corpo completo de um e-mail do Outlook sem marcá-lo como lido.', parameters: { type: 'object', properties: { id_email: { type: 'string' } }, required: ['id_email'] } } },
|
|
186
|
+
{ type: 'function', function: { name: 'listar_pastas_outlook', description: 'Lista pastas de e-mail do Outlook.', parameters: { type: 'object', properties: {} } } },
|
|
187
|
+
{ type: 'function', function: { name: 'alterar_email_outlook', description: 'Altera estado ou pasta de um e-mail do Outlook. Exige confirmação.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, acao: { type: 'string', enum: ['lido','nao_lido','importante','normal','arquivar','caixa_entrada','spam','lixeira','restaurar','mover','excluir_permanente'] }, id_pasta: { type: 'string' } }, required: ['id_email','acao'] } } },
|
|
188
|
+
{ type: 'function', function: { name: 'criar_resposta_outlook', description: 'Cria rascunho de resposta no Outlook. Não envia e exige confirmação.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, corpo: { type: 'string' }, para_todos: { type: 'boolean' } }, required: ['id_email','corpo'] } } },
|
|
189
|
+
{ type: 'function', function: { name: 'criar_encaminhamento_outlook', description: 'Cria rascunho de encaminhamento no Outlook. Não envia e exige confirmação.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, corpo: { type: 'string' } }, required: ['id_email','para'] } } },
|
|
190
|
+
{ type: 'function', function: { name: 'obter_anexo_outlook', description: 'Obtém o link autenticado para baixar um anexo do Outlook.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, id_anexo: { type: 'string' } }, required: ['id_email','id_anexo'] } } },
|
|
191
|
+
{ type: 'function', function: { name: 'criar_rascunho_outlook', description: 'Cria rascunho no Outlook. Exige confirmação e não envia.', parameters: { type: 'object', properties: { para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, assunto: { type: 'string' }, corpo: { type: 'string' }, formato_html: { type: 'boolean' } }, required: ['para','assunto','corpo'] } } },
|
|
192
|
+
{ type: 'function', function: { name: 'enviar_rascunho_outlook', description: 'Envia um rascunho existente do Outlook. Exige confirmação.', parameters: { type: 'object', properties: { rascunho_id: { type: 'string' } }, required: ['rascunho_id'] } } },
|
|
193
|
+
{ type: 'function', function: { name: 'status_google_workspace', description: 'Verifica a conexão com Gmail, Drive, Docs e Sheets.', parameters: { type: 'object', properties: {} } } },
|
|
194
|
+
{ type: 'function', function: { name: 'conectar_google_workspace', description: 'Gera o link oficial para autorizar Gmail, Drive, Docs e Sheets.', parameters: { type: 'object', properties: {} } } },
|
|
195
|
+
{ type: 'function', function: { name: 'listar_emails_gmail', description: 'Lista e prioriza e-mails do Gmail, sem alterar nada.', parameters: { type: 'object', properties: {
|
|
196
|
+
limite: { type: 'number' }, somente_nao_lidos: { type: 'boolean' }, priorizar: { type: 'boolean' }, consulta: { type: 'string' }, pasta: { type: 'string', enum: ['inbox','sent','drafts','spam','trash','starred','important','all'] }
|
|
197
|
+
} } } },
|
|
198
|
+
{ type: 'function', function: { name: 'ler_email_gmail', description: 'Lê o corpo completo de um e-mail ou a conversa inteira sem marcar como lido.', parameters: { type: 'object', properties: {
|
|
199
|
+
id_email: { type: 'string' }, conversa_completa: { type: 'boolean' }
|
|
200
|
+
}, required: ['id_email'] } } },
|
|
201
|
+
{ type: 'function', function: { name: 'listar_pastas_gmail', description: 'Lista pastas e marcadores do Gmail.', parameters: { type: 'object', properties: {} } } },
|
|
202
|
+
{ type: 'function', function: { name: 'alterar_email_gmail', description: 'Altera estado, pasta ou marcador de um e-mail. Exige confirmação.', parameters: { type: 'object', properties: {
|
|
203
|
+
id_email: { type: 'string' }, acao: { type: 'string', enum: ['lido','nao_lido','arquivar','caixa_entrada','estrela','remover_estrela','importante','remover_importante','spam','nao_spam','lixeira','restaurar','aplicar_marcador','remover_marcador','mover','excluir_permanente'] }, marcador: { type: 'string' }
|
|
204
|
+
}, required: ['id_email','acao'] } } },
|
|
205
|
+
{ type: 'function', function: { name: 'criar_resposta_gmail', description: 'Cria rascunho de resposta na mesma conversa. Não envia e exige confirmação.', parameters: { type: 'object', properties: {
|
|
206
|
+
id_email: { type: 'string' }, corpo: { type: 'string' }, para_todos: { type: 'boolean' }, formato_html: { type: 'boolean' }
|
|
207
|
+
}, required: ['id_email','corpo'] } } },
|
|
208
|
+
{ type: 'function', function: { name: 'criar_encaminhamento_gmail', description: 'Cria rascunho de encaminhamento. Não envia e exige confirmação.', parameters: { type: 'object', properties: {
|
|
209
|
+
id_email: { type: 'string' }, para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, corpo: { type: 'string' }
|
|
210
|
+
}, required: ['id_email','para'] } } },
|
|
211
|
+
{ type: 'function', function: { name: 'obter_anexo_gmail', description: 'Obtém o link autenticado para baixar um anexo do Gmail.', parameters: { type: 'object', properties: {
|
|
212
|
+
id_email: { type: 'string' }, id_anexo: { type: 'string' }
|
|
213
|
+
}, required: ['id_email','id_anexo'] } } },
|
|
214
|
+
{ type: 'function', function: { name: 'criar_rascunho_gmail', description: 'Cria rascunho no Gmail. Exige confirmação e não envia.', parameters: { type: 'object', properties: {
|
|
215
|
+
para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, assunto: { type: 'string' }, corpo: { type: 'string' }, formato_html: { type: 'boolean' }
|
|
216
|
+
}, required: ['para', 'assunto', 'corpo'] } } },
|
|
217
|
+
{ type: 'function', function: { name: 'enviar_rascunho_gmail', description: 'Envia rascunho existente do Gmail. Exige confirmação.', parameters: { type: 'object', properties: { rascunho_id: { type: 'string' } }, required: ['rascunho_id'] } } },
|
|
218
|
+
{ type: 'function', function: { name: 'listar_arquivos_drive', description: 'Lista arquivos no Google Drive por nome ou tipo.', parameters: { type: 'object', properties: {
|
|
219
|
+
busca: { type: 'string' }, tipo_mime: { type: 'string' }, limite: { type: 'number' }
|
|
220
|
+
} } } },
|
|
221
|
+
{ type: 'function', function: { name: 'ler_google_docs', description: 'Lê texto de Google Docs pelo ID.', parameters: { type: 'object', properties: { id_documento: { type: 'string' } }, required: ['id_documento'] } } },
|
|
222
|
+
{ type: 'function', function: { name: 'editar_google_docs', description: 'Edita o Google Docs original por padrão; cria cópia somente se solicitado. Exige confirmação.', parameters: { type: 'object', properties: {
|
|
223
|
+
id_documento: { type: 'string' }, localizar: { type: 'string' }, substituir_por: { type: 'string' }, diferenciar_maiusculas: { type: 'boolean' }, criar_copia: { type: 'boolean' }, nome_copia: { type: 'string' }
|
|
224
|
+
}, required: ['id_documento', 'localizar'] } } },
|
|
225
|
+
{ type: 'function', function: { name: 'ler_google_sheets', description: 'Lê células de Google Sheets pelo ID e intervalo.', parameters: { type: 'object', properties: {
|
|
226
|
+
id_planilha: { type: 'string' }, intervalo: { type: 'string' }
|
|
227
|
+
}, required: ['id_planilha'] } } },
|
|
228
|
+
{ type: 'function', function: { name: 'editar_google_sheets', description: 'Edita a Google Sheets original por padrão; cria cópia somente se solicitado. Exige confirmação.', parameters: { type: 'object', properties: {
|
|
229
|
+
id_planilha: { type: 'string' }, intervalo: { type: 'string' }, valores: { type: 'array', items: { type: 'array', items: {} } }, criar_copia: { type: 'boolean' }, nome_copia: { type: 'string' }
|
|
230
|
+
}, required: ['id_planilha', 'intervalo', 'valores'] } } },
|
|
164
231
|
{ type: 'function', function: { name: 'ler_apresentacao',
|
|
165
232
|
description: 'Lê PowerPoint .pptx por slides, retornando o texto estruturado e a quantidade de imagens.',
|
|
166
233
|
parameters: { type: 'object', properties: {
|
|
@@ -780,6 +847,97 @@ async function execute(name, input, opts = {}) {
|
|
|
780
847
|
if (_outsideScope(target)) return _scopeError(target);
|
|
781
848
|
return await require('./office-readers').lerDocumento({ ...input, caminho: target });
|
|
782
849
|
}
|
|
850
|
+
case 'editar_documento': {
|
|
851
|
+
const target = _abs(input.caminho, baseDir);
|
|
852
|
+
if (_outsideScope(target)) return _scopeError(target);
|
|
853
|
+
const copy = input.caminho_copia ? _abs(input.caminho_copia, baseDir) : undefined;
|
|
854
|
+
if (copy && _outsideScope(copy)) return _scopeError(copy);
|
|
855
|
+
const result = await require('./office-editors').editarDocumento({ ...input, caminho: target, caminho_copia: copy });
|
|
856
|
+
if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
case 'editar_planilha': {
|
|
860
|
+
const target = _abs(input.caminho, baseDir);
|
|
861
|
+
if (_outsideScope(target)) return _scopeError(target);
|
|
862
|
+
const copy = input.caminho_copia ? _abs(input.caminho_copia, baseDir) : undefined;
|
|
863
|
+
if (copy && _outsideScope(copy)) return _scopeError(copy);
|
|
864
|
+
const result = await require('./office-editors').editarPlanilha({ ...input, caminho: target, caminho_copia: copy });
|
|
865
|
+
if (result.ok) { result._backup = result.backup || ''; result._acao = result.editou_original ? 'alterado' : 'criado'; }
|
|
866
|
+
return result;
|
|
867
|
+
}
|
|
868
|
+
case 'status_microsoft365':
|
|
869
|
+
return await require('./api').api('/api/integrations/microsoft/status', { token: opts.token });
|
|
870
|
+
case 'conectar_microsoft365': {
|
|
871
|
+
const data = await require('./api').api('/api/integrations/microsoft/connect', { method: 'POST', body: { source: 'cli' }, token: opts.token });
|
|
872
|
+
return { ok: true, authUrl: data.authUrl, instrucao: 'Abra o link no navegador, autorize sua conta Microsoft e volte ao Terminal Smart.' };
|
|
873
|
+
}
|
|
874
|
+
case 'listar_emails_outlook': {
|
|
875
|
+
const p = new URLSearchParams({ limit: String(Math.min(30, Math.max(1, parseInt(input.limite) || 15))) });
|
|
876
|
+
if (input.somente_nao_lidos) p.set('unread', '1'); if (input.priorizar) p.set('priority', '1');
|
|
877
|
+
if (input.consulta) p.set('query', input.consulta); if (input.pasta) p.set('folder', input.pasta);
|
|
878
|
+
return await require('./api').api('/api/integrations/microsoft/mail/messages?' + p, { token: opts.token });
|
|
879
|
+
}
|
|
880
|
+
case 'ler_email_outlook':
|
|
881
|
+
return await require('./api').api('/api/integrations/microsoft/mail/message?' + new URLSearchParams({ id: input.id_email }), { token: opts.token });
|
|
882
|
+
case 'listar_pastas_outlook':
|
|
883
|
+
return await require('./api').api('/api/integrations/microsoft/mail/folders', { token: opts.token });
|
|
884
|
+
case 'alterar_email_outlook':
|
|
885
|
+
return await require('./api').api('/api/integrations/microsoft/mail/modify', { method: 'POST', token: opts.token, body: { messageId: input.id_email, action: input.acao, folderId: input.id_pasta, confirmed: true } });
|
|
886
|
+
case 'criar_resposta_outlook':
|
|
887
|
+
return await require('./api').api('/api/integrations/microsoft/mail/replies', { method: 'POST', token: opts.token, body: { messageId: input.id_email, body: input.corpo, replyAll: input.para_todos === true, confirmed: true } });
|
|
888
|
+
case 'criar_encaminhamento_outlook':
|
|
889
|
+
return await require('./api').api('/api/integrations/microsoft/mail/forwards', { method: 'POST', token: opts.token, body: { messageId: input.id_email, to: input.para, cc: input.cc || [], body: input.corpo || '', confirmed: true } });
|
|
890
|
+
case 'obter_anexo_outlook':
|
|
891
|
+
return { downloadUrl: '/api/integrations/microsoft/mail/attachments/download?' + new URLSearchParams({ messageId: input.id_email, attachmentId: input.id_anexo }), instrucao: 'Use uma sessão autenticada do Terminal Smart para baixar o anexo.' };
|
|
892
|
+
case 'criar_rascunho_outlook':
|
|
893
|
+
return await require('./api').api('/api/integrations/microsoft/mail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
|
|
894
|
+
case 'enviar_rascunho_outlook':
|
|
895
|
+
return await require('./api').api('/api/integrations/microsoft/mail/send', { method: 'POST', token: opts.token, body: { draftId: input.rascunho_id, confirmed: true } });
|
|
896
|
+
case 'status_google_workspace':
|
|
897
|
+
return await require('./api').api('/api/integrations/google/status', { token: opts.token });
|
|
898
|
+
case 'conectar_google_workspace': {
|
|
899
|
+
const data = await require('./api').api('/api/integrations/google/connect', { method: 'POST', body: { source: 'cli' }, token: opts.token });
|
|
900
|
+
return { ok: true, authUrl: data.authUrl, instrucao: 'Abra o link no navegador, autorize sua conta Google e volte ao Terminal Smart.' };
|
|
901
|
+
}
|
|
902
|
+
case 'listar_emails_gmail': {
|
|
903
|
+
const p = new URLSearchParams({ limit: String(Math.min(30, Math.max(1, parseInt(input.limite) || 15))) });
|
|
904
|
+
if (input.somente_nao_lidos) p.set('unread', '1'); if (input.priorizar) p.set('priority', '1');
|
|
905
|
+
if (input.consulta) p.set('query', input.consulta); if (input.pasta) p.set('folder', input.pasta);
|
|
906
|
+
return await require('./api').api('/api/integrations/google/gmail/messages?' + p, { token: opts.token });
|
|
907
|
+
}
|
|
908
|
+
case 'ler_email_gmail': {
|
|
909
|
+
const p = new URLSearchParams({ id: input.id_email }); if (input.conversa_completa) p.set('thread', '1');
|
|
910
|
+
return await require('./api').api('/api/integrations/google/gmail/message?' + p, { token: opts.token });
|
|
911
|
+
}
|
|
912
|
+
case 'listar_pastas_gmail':
|
|
913
|
+
return await require('./api').api('/api/integrations/google/gmail/labels', { token: opts.token });
|
|
914
|
+
case 'alterar_email_gmail':
|
|
915
|
+
return await require('./api').api('/api/integrations/google/gmail/modify', { method: 'POST', token: opts.token, body: { messageId: input.id_email, action: input.acao, label: input.marcador, confirmed: true } });
|
|
916
|
+
case 'criar_resposta_gmail':
|
|
917
|
+
return await require('./api').api('/api/integrations/google/gmail/replies', { method: 'POST', token: opts.token, body: { messageId: input.id_email, body: input.corpo, replyAll: input.para_todos === true, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
|
|
918
|
+
case 'criar_encaminhamento_gmail':
|
|
919
|
+
return await require('./api').api('/api/integrations/google/gmail/forwards', { method: 'POST', token: opts.token, body: { messageId: input.id_email, to: input.para, cc: input.cc || [], body: input.corpo || '', confirmed: true } });
|
|
920
|
+
case 'obter_anexo_gmail':
|
|
921
|
+
return { downloadUrl: '/api/integrations/google/gmail/attachments/download?' + new URLSearchParams({ messageId: input.id_email, attachmentId: input.id_anexo }), instrucao: 'Use uma sessão autenticada do Terminal Smart para baixar o anexo.' };
|
|
922
|
+
case 'criar_rascunho_gmail':
|
|
923
|
+
return await require('./api').api('/api/integrations/google/gmail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
|
|
924
|
+
case 'enviar_rascunho_gmail':
|
|
925
|
+
return await require('./api').api('/api/integrations/google/gmail/send', { method: 'POST', token: opts.token, body: { draftId: input.rascunho_id, confirmed: true } });
|
|
926
|
+
case 'listar_arquivos_drive': {
|
|
927
|
+
const p = new URLSearchParams({ limit: String(Math.min(100, Math.max(1, parseInt(input.limite) || 30))) });
|
|
928
|
+
if (input.busca) p.set('query', input.busca); if (input.tipo_mime) p.set('mimeType', input.tipo_mime);
|
|
929
|
+
return await require('./api').api('/api/integrations/google/drive/files?' + p, { token: opts.token });
|
|
930
|
+
}
|
|
931
|
+
case 'ler_google_docs':
|
|
932
|
+
return await require('./api').api('/api/integrations/google/docs/document?id=' + encodeURIComponent(input.id_documento), { token: opts.token });
|
|
933
|
+
case 'editar_google_docs':
|
|
934
|
+
return await require('./api').api('/api/integrations/google/docs/replace', { method: 'POST', token: opts.token, body: { documentId: input.id_documento, search: input.localizar, replacement: input.substituir_por || '', matchCase: input.diferenciar_maiusculas !== false, createCopy: input.criar_copia === true, copyName: input.nome_copia, confirmed: true } });
|
|
935
|
+
case 'ler_google_sheets': {
|
|
936
|
+
const p = new URLSearchParams({ id: input.id_planilha }); if (input.intervalo) p.set('range', input.intervalo);
|
|
937
|
+
return await require('./api').api('/api/integrations/google/sheets/values?' + p, { token: opts.token });
|
|
938
|
+
}
|
|
939
|
+
case 'editar_google_sheets':
|
|
940
|
+
return await require('./api').api('/api/integrations/google/sheets/update', { method: 'POST', token: opts.token, body: { spreadsheetId: input.id_planilha, range: input.intervalo, values: input.valores, createCopy: input.criar_copia === true, copyName: input.nome_copia, confirmed: true } });
|
|
783
941
|
case 'ler_apresentacao': {
|
|
784
942
|
const target = _abs(input.caminho, baseDir);
|
|
785
943
|
if (_outsideScope(target)) return _scopeError(target);
|
package/lib/ui.js
CHANGED
|
@@ -148,9 +148,19 @@ function friendlyTool(name, en) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// Barra de progresso ▰▰▰▱▱ (uso de créditos)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
// `frac` é o quanto a barra ENCHE. Por padrão representa consumo, e encher é ruim.
|
|
152
|
+
// Com `{ restante: true }` a fração é o que sobrou: a barra esvazia conforme se gasta, e
|
|
153
|
+
// é o vazio que passa a ser o alerta. Sem essa distinção, uma barra de consumo ao lado do
|
|
154
|
+
// número de créditos RESTANTES lia-se ao contrário — quem tinha 29.992 de 30.000 via uma
|
|
155
|
+
// barra vazia e entendia que a franquia tinha acabado.
|
|
156
|
+
function bar(frac, width = 22, opts = {}) {
|
|
157
|
+
const cheio = Math.max(0, Math.min(1, frac));
|
|
158
|
+
let n = Math.round(cheio * width);
|
|
159
|
+
// "Sobrou pouco" e "acabou" precisam ser visualmente diferentes: com 1 crédito de 50 o
|
|
160
|
+
// arredondamento zerava a barra, e ela ficava idêntica à de quem está sem nada.
|
|
161
|
+
if (opts.restante && cheio > 0 && n === 0) n = 1;
|
|
162
|
+
const critico = opts.restante ? 1 - cheio : cheio;
|
|
163
|
+
const color = critico >= 0.9 ? C.err : critico >= 0.75 ? C.warn : C.cyan;
|
|
154
164
|
return color('▰'.repeat(n)) + C.dim('▱'.repeat(width - n));
|
|
155
165
|
}
|
|
156
166
|
|