beast-agent 2.7.3 → 2.8.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/package.json +1 -1
- package/src/agent/engine.js +30 -1
- package/src/agent/pdfwrite.js +528 -0
- package/src/agent/skills.js +18 -5
- package/src/main.js +2 -2
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -18,6 +18,7 @@ const nightref = require('./nightref');
|
|
|
18
18
|
const skills = require('./skills');
|
|
19
19
|
const mcp = require('./mcp');
|
|
20
20
|
const apps = require('./apps');
|
|
21
|
+
const pdfwrite = require('./pdfwrite');
|
|
21
22
|
const { estTokens, estMsgTokens } = require('./tokens');
|
|
22
23
|
const log = require('./logger');
|
|
23
24
|
|
|
@@ -1661,7 +1662,7 @@ class Engine {
|
|
|
1661
1662
|
'CONUŞMA ODAĞI SENDE KALSIN: kullanıcı seninle konuşurken iş çıkmışsa — uzun da olsa UFACIK da (tek komutluk dizin listesi, tek dosya okuma, tek arama…) — run_background ile PARALEL ajana devret; ana sohbet hiçbir işi beklemez; bittiğinde özet otomatik düşer.',
|
|
1662
1663
|
'Python işleri için python_run kullan: küçük betikler inline code ile; tekrarlayan işler %APPDATA%\\beast\\scripts klasöründeki dosyalarla (ör. news.py = RSS haber toplayıcı: args ["--limit","8","--json"]). Python kurulu olmasa bile ilk çağrıda taşınabilir gömülü runtime otomatik iner.',
|
|
1663
1664
|
'PYTHON DURUMU: makinede sistem Python\'u görünmese bile ŞAŞIRMA ve "python yok" DEME — python_run aracı kendi taşınabilir runtime\'ını (%APPDATA%\\beast\\py\\python.exe) otomatik indirir/kullanır ve bu klasör run_command PATH\'inde önceliklidir; yani run_command içinde de `python` çalışır. Ham Google/Bing scrape yerine önce web_search aracını kullan (SearXNG + stealth TLS + TinyFish + Python çoklu-motor destekli), script gerektiğinde python_run yaz.',
|
|
1664
|
-
'PDF ÇIKTI KURALI: PDF üretirken pip\u2019ten pdf paketi (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) KURMA/KULLANMA — bunlar Türkçe karakterleri bozar.
|
|
1665
|
+
'PDF ÇIKTI KURALI: PDF üretirken pip\u2019ten pdf paketi (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) KURMA/KULLANMA — bunlar Türkçe karakterleri bozar. İLK TERCİH pdf_write aracı: tek çağrıda başlık+markdown-lite içeriği düzenli, Türkçe-güvenli PDF\u2019e çevirir (Windows fontu otomatik gömülür; ğ ş ı İ bozulmaz) — script YAZMA, pip KULLANMA. Sıradışı ihtiyaç (form doldurma, PDF birleştirme, sayfa işleme) için doğru kit Node\u2019ta ZATEN kurulu: `pdf-lib` + `@pdf-lib/fontkit` ve `pdfkit` — o zaman write_file ile .js script yazıp run_command ile `node script.js` çalıştır (python_run İLE DEĞİL). md→pdf çevirici YOKTUR ve kurulmaz: kullanıcıya rapor/özet/belge çıktısı vereceksen .md dosyası gönderme — pdf_write ile DOĞRUDAN PDF üret ve send_file ile gönder. Ayrıntılı örnekler: pdf skill\u2019i (SKILL.md).',
|
|
1665
1666
|
'Eski bir hafıza kaydına ihtiyacın olursa memory_search ile ara; kalıcı bilgi/birikim için kb_search kullan, yeni bilgi öğrenirsen kb_add ile kaynak belirt.',
|
|
1666
1667
|
'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
|
|
1667
1668
|
'BEAST KAYNAK KORUMASI: Beast\u2019in kendi kurulum/kaynak kod klasörü KİLİTLİDİR — oraya dosya yazamaz, silamaz, komut/betikle değiştiremezsin; okumak serbest. Kullanıcı kodu ancak kendi eliyle dışarıdan değiştirir; böyle bir istek gelirse yapamayacağını söyle ve kullanıcının elle yapması için yol göster.',
|
|
@@ -4209,6 +4210,16 @@ const skills = require('./skills');
|
|
|
4209
4210
|
const r = await this.fileSend(sessionId, p, String((args && args.caption) || ''));
|
|
4210
4211
|
return JSON.stringify(r);
|
|
4211
4212
|
}
|
|
4213
|
+
if (name === 'pdf_write') {
|
|
4214
|
+
const r = await pdfwrite.write({
|
|
4215
|
+
outPath: String((args && args.path) || ''),
|
|
4216
|
+
title: String((args && args.title) || ''),
|
|
4217
|
+
subtitle: String((args && args.subtitle) || ''),
|
|
4218
|
+
content: String((args && args.content) || ''),
|
|
4219
|
+
workspace: this.workspace,
|
|
4220
|
+
});
|
|
4221
|
+
return JSON.stringify(r);
|
|
4222
|
+
}
|
|
4212
4223
|
if (name === 'run_background') {
|
|
4213
4224
|
if (args.agent && !agentdefs.get(String(args.agent))) {
|
|
4214
4225
|
return JSON.stringify({
|
|
@@ -4905,6 +4916,24 @@ const TOOLS = [
|
|
|
4905
4916
|
},
|
|
4906
4917
|
},
|
|
4907
4918
|
},
|
|
4919
|
+
{
|
|
4920
|
+
type: 'function',
|
|
4921
|
+
function: {
|
|
4922
|
+
name: 'pdf_write',
|
|
4923
|
+
description:
|
|
4924
|
+
'Create a polished, Turkish-safe PDF document in ONE call — NO script writing, NO pip packages (Turkish characters ğ ş ı İ Ğ Ş İ Ö Ü render perfectly; a Windows font is embedded automatically). content = markdown-lite: "# ## ###" headings, plain paragraphs, "- " bullets, "1. " numbered items, **bold**, `mono`, "> " quote, "```" code blocks, "| col | col |" table rows. Then call send_file with the returned path to deliver it. Use this for reports/summaries/notes/documents instead of writing a script file.',
|
|
4925
|
+
parameters: {
|
|
4926
|
+
type: 'object',
|
|
4927
|
+
properties: {
|
|
4928
|
+
path: { type: 'string', description: 'Output file path (absolute or workspace-relative; .pdf is added if missing)' },
|
|
4929
|
+
title: { type: 'string', description: 'Document title shown at the top (optional but recommended)' },
|
|
4930
|
+
subtitle: { type: 'string', description: 'Small line under the title (date, author, subject…)' },
|
|
4931
|
+
content: { type: 'string', description: 'Markdown-lite body: headings, paragraphs, lists, **bold**, tables, code blocks' },
|
|
4932
|
+
},
|
|
4933
|
+
required: ['path', 'content'],
|
|
4934
|
+
},
|
|
4935
|
+
},
|
|
4936
|
+
},
|
|
4908
4937
|
{
|
|
4909
4938
|
type: 'function',
|
|
4910
4939
|
function: {
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Beast PDF ÜRETİCİ — tek çağrıda kusursuz, Türkçe-güvenli PDF.
|
|
4
|
+
pdf-lib + @pdf-lib/fontkit ile Windows TTF gömer (ğ ş ı İ Ğ Ş İ Ö Ü garantili;
|
|
5
|
+
Helvetica gibi standard fontlar bunları BASAMAZ). Markdown-lite ayrıştırır:
|
|
6
|
+
# ## ### başlıklar, paragraf, -/* bullet, 1. numaralı liste, **bold**,
|
|
7
|
+
`kod`, | tablo satırları |, > alıntı, ``` kod bloğu. Otomatik satır
|
|
8
|
+
kaydırma + sayfa geçişi + sayfa numarası + başlık bloğu.
|
|
9
|
+
Ajanın elle .js script yazıp font yolu/uğraşmasına GEREK YOK — pdf_write aracı bunu çağırır. */
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
/* Türkçe glifleri TAM kapsayan Windows sistem fontları — sırayla denenir.
|
|
15
|
+
[normal, bold, mono] */
|
|
16
|
+
const FONT_SETS = [
|
|
17
|
+
['C:/Windows/Fonts/segoeui.ttf', 'C:/Windows/Fonts/segoeuib.ttf', 'C:/Windows/Fonts/consola.ttf'],
|
|
18
|
+
['C:/Windows/Fonts/arial.ttf', 'C:/Windows/Fonts/arialbd.ttf', 'C:/Windows/Fonts/consola.ttf'],
|
|
19
|
+
['C:/Windows/Fonts/calibri.ttf', 'C:/Windows/Fonts/calibrib.ttf', 'C:/Windows/Fonts/consola.ttf'],
|
|
20
|
+
['C:/Windows/Fonts/tahoma.ttf', 'C:/Windows/Fonts/tahomabd.ttf', 'C:/Windows/Fonts/consola.ttf'],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function firstFont() {
|
|
24
|
+
for (const set of FONT_SETS) {
|
|
25
|
+
if (set.slice(0, 2).every((f) => fs.existsSync(f))) return set;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasFonts() {
|
|
31
|
+
return !!firstFont();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CONTENT_CAP = 300000;
|
|
35
|
+
|
|
36
|
+
/* ---------- markdown-lite → bloklar ---------- */
|
|
37
|
+
|
|
38
|
+
function parseBlocks(md) {
|
|
39
|
+
const lines = String(md || '').replace(/\r\n?/g, '\n').split('\n');
|
|
40
|
+
const blocks = [];
|
|
41
|
+
let i = 0;
|
|
42
|
+
while (i < lines.length) {
|
|
43
|
+
const raw = lines[i];
|
|
44
|
+
const line = raw.trim();
|
|
45
|
+
if (!line) {
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (/^```/.test(line)) {
|
|
50
|
+
const buf = [];
|
|
51
|
+
i++;
|
|
52
|
+
while (i < lines.length && !/^```/.test(lines[i].trim())) {
|
|
53
|
+
buf.push(lines[i].replace(/\t/g, ' '));
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
i++; // kapanış fence (ya da EOF)
|
|
57
|
+
blocks.push({ type: 'code', lines: buf });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let m;
|
|
61
|
+
if ((m = line.match(/^(#{1,3})\s+(.*)$/))) {
|
|
62
|
+
blocks.push({ type: 'h', level: m[1].length, text: m[2].replace(/\s*#+\s*$/, '') });
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(line)) {
|
|
67
|
+
blocks.push({ type: 'hr' });
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if ((m = line.match(/^>\s?(.*)$/))) {
|
|
72
|
+
blocks.push({ type: 'quote', text: m[1] });
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if ((m = line.match(/^[-*•]\s+(.*)$/))) {
|
|
77
|
+
blocks.push({ type: 'li', marker: '\u2022', text: m[1] });
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if ((m = line.match(/^(\d{1,3})[.)]\s+(.*)$/))) {
|
|
82
|
+
blocks.push({ type: 'li', marker: m[1] + '.', text: m[2] });
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (line.startsWith('|')) {
|
|
87
|
+
const rows = [];
|
|
88
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) {
|
|
89
|
+
const cells = lines[i]
|
|
90
|
+
.trim()
|
|
91
|
+
.replace(/^\|/, '')
|
|
92
|
+
.replace(/\|$/, '')
|
|
93
|
+
.split('|')
|
|
94
|
+
.map((c) => c.trim());
|
|
95
|
+
if (!cells.every((c) => /^:?-{2,}:?$/.test(c) || c === '')) rows.push(cells);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
if (rows.length) blocks.push({ type: 'table', rows });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
/* paragraf: özel blok başlatmayan ardışık satırlar birleşir */
|
|
102
|
+
const para = [line];
|
|
103
|
+
i++;
|
|
104
|
+
while (i < lines.length) {
|
|
105
|
+
const l2 = lines[i].trim();
|
|
106
|
+
if (
|
|
107
|
+
!l2 ||
|
|
108
|
+
/^(#{1,3}\s|[-*•]\s|\d{1,3}[.)]\s|>|```|\|)/.test(l2) ||
|
|
109
|
+
/^(-{3,}|\*{3,}|_{3,})$/.test(l2)
|
|
110
|
+
)
|
|
111
|
+
break;
|
|
112
|
+
para.push(l2);
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
blocks.push({ type: 'p', text: para.join(' ') });
|
|
116
|
+
}
|
|
117
|
+
return blocks;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ---------- inline: **bold** ve `mono` → span dizisi ---------- */
|
|
121
|
+
|
|
122
|
+
function toSpans(text) {
|
|
123
|
+
const spans = [];
|
|
124
|
+
const re = /(\*\*([^*]+)\*\*)|(`([^`]+)`)/g;
|
|
125
|
+
let last = 0;
|
|
126
|
+
let m;
|
|
127
|
+
while ((m = re.exec(text))) {
|
|
128
|
+
if (m.index > last) spans.push({ t: text.slice(last, m.index), b: false });
|
|
129
|
+
if (m[1]) spans.push({ t: m[2], b: true });
|
|
130
|
+
else spans.push({ t: m[4], b: false, mono: true });
|
|
131
|
+
last = re.lastIndex;
|
|
132
|
+
}
|
|
133
|
+
if (last < text.length) spans.push({ t: text.slice(last), b: false });
|
|
134
|
+
return spans.length ? spans : [{ t: '', b: false }];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ---------- satır kaydırma: span'ler → [token] satırları ---------- */
|
|
138
|
+
|
|
139
|
+
function wrapSpans(spans, fontFor, size, maxW) {
|
|
140
|
+
const tokens = [];
|
|
141
|
+
for (const s of spans) {
|
|
142
|
+
const parts = s.t.split(/(\s+)/);
|
|
143
|
+
for (const w of parts) {
|
|
144
|
+
if (!w) continue;
|
|
145
|
+
tokens.push({ w, b: !!s.b, mono: !!s.mono, space: /^\s+$/.test(w) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const widthOf = (tk) => {
|
|
149
|
+
try {
|
|
150
|
+
return fontFor(tk).widthOfTextAtSize(tk.w, size);
|
|
151
|
+
} catch {
|
|
152
|
+
return tk.w.length * size * 0.6;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const lines = [];
|
|
156
|
+
let cur = [];
|
|
157
|
+
let curW = 0;
|
|
158
|
+
const flush = () => {
|
|
159
|
+
while (cur.length && cur[cur.length - 1].space) cur.pop();
|
|
160
|
+
if (cur.length) lines.push(cur);
|
|
161
|
+
cur = [];
|
|
162
|
+
curW = 0;
|
|
163
|
+
};
|
|
164
|
+
for (const tk of tokens) {
|
|
165
|
+
if (tk.space) {
|
|
166
|
+
if (cur.length) {
|
|
167
|
+
cur.push(tk);
|
|
168
|
+
curW += widthOf(tk);
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let w = widthOf(tk);
|
|
173
|
+
/* tek kelime satırdan genişse karakter karakter böl (uzun URL/link emniyeti) */
|
|
174
|
+
if (w > maxW) {
|
|
175
|
+
flush();
|
|
176
|
+
let chunk = '';
|
|
177
|
+
let chunkW = 0;
|
|
178
|
+
for (const ch of tk.w) {
|
|
179
|
+
const cw = (() => {
|
|
180
|
+
try {
|
|
181
|
+
return fontFor(tk).widthOfTextAtSize(ch, size);
|
|
182
|
+
} catch {
|
|
183
|
+
return size * 0.6;
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
if (chunk && chunkW + cw > maxW) {
|
|
187
|
+
lines.push([{ w: chunk, b: tk.b, mono: tk.mono, space: false, hard: true }]);
|
|
188
|
+
chunk = ch;
|
|
189
|
+
chunkW = cw;
|
|
190
|
+
} else {
|
|
191
|
+
chunk += ch;
|
|
192
|
+
chunkW += cw;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (chunk) {
|
|
196
|
+
cur = [{ w: chunk, b: tk.b, mono: tk.mono, space: false }];
|
|
197
|
+
curW = chunkW;
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (curW + w > maxW) flush();
|
|
202
|
+
cur.push(tk);
|
|
203
|
+
curW += w;
|
|
204
|
+
}
|
|
205
|
+
flush();
|
|
206
|
+
return lines;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* drawTextSpan'lı satır: token dizisini çizer, son x döner */
|
|
210
|
+
function drawTokenLine(page, line, x, y, fontFor, size, color) {
|
|
211
|
+
let cx = x;
|
|
212
|
+
for (const tk of line) {
|
|
213
|
+
if (tk.space) {
|
|
214
|
+
try {
|
|
215
|
+
cx += fontFor(tk).widthOfTextAtSize(tk.w, size);
|
|
216
|
+
} catch {
|
|
217
|
+
cx += tk.w.length * size * 0.6;
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
page.drawText(tk.w, { x: cx, y, size, font: fontFor(tk), color });
|
|
223
|
+
cx += fontFor(tk).widthOfTextAtSize(tk.w, size);
|
|
224
|
+
} catch {
|
|
225
|
+
cx += tk.w.length * size * 0.6;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return cx;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function lineCount(lines) {
|
|
232
|
+
return Math.max(1, lines.length);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* ---------- ana üretici ---------- */
|
|
236
|
+
|
|
237
|
+
async function writePdf({ outPath, title, subtitle, content, workspace }) {
|
|
238
|
+
try {
|
|
239
|
+
const raw = String((content == null ? '' : content) || '');
|
|
240
|
+
if (!raw.trim()) return { ok: false, error: 'content gerekli' };
|
|
241
|
+
const outRaw = String(outPath || '').trim();
|
|
242
|
+
if (!outRaw) return { ok: false, error: 'path gerekli' };
|
|
243
|
+
let abs = path.isAbsolute(outRaw) ? outRaw : path.join(String(workspace || process.cwd()), outRaw);
|
|
244
|
+
if (!/\.pdf$/i.test(abs)) abs += '.pdf';
|
|
245
|
+
|
|
246
|
+
const fonts = firstFont();
|
|
247
|
+
if (!fonts) {
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
error:
|
|
251
|
+
'Türkçe destekli Windows TTF bulunamadı (Segoe UI/Arial/Calibri/Tahoma) — pdf skill\u2019indeki elle şablonu kullan',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const { PDFDocument, rgb } = require('pdf-lib');
|
|
256
|
+
const fontkit = require('@pdf-lib/fontkit');
|
|
257
|
+
const doc = await PDFDocument.create();
|
|
258
|
+
doc.registerFontkit(fontkit);
|
|
259
|
+
const [fReg, fBold, fMono] = fonts;
|
|
260
|
+
const reg = await doc.embedFont(fs.readFileSync(fReg), { subset: true });
|
|
261
|
+
let bold = reg;
|
|
262
|
+
try {
|
|
263
|
+
if (fs.existsSync(fBold)) bold = await doc.embedFont(fs.readFileSync(fBold), { subset: true });
|
|
264
|
+
} catch {}
|
|
265
|
+
let mono = reg;
|
|
266
|
+
try {
|
|
267
|
+
if (fMono && fs.existsSync(fMono)) mono = await doc.embedFont(fs.readFileSync(fMono), { subset: true });
|
|
268
|
+
} catch {}
|
|
269
|
+
|
|
270
|
+
const PAGE_W = 595.28;
|
|
271
|
+
const PAGE_H = 841.89;
|
|
272
|
+
const MX = 56;
|
|
273
|
+
const MT = 64;
|
|
274
|
+
const MB = 76;
|
|
275
|
+
const availW = PAGE_W - 2 * MX;
|
|
276
|
+
const textC = rgb(0.13, 0.13, 0.16);
|
|
277
|
+
const mutedC = rgb(0.42, 0.45, 0.5);
|
|
278
|
+
const accentC = rgb(0.15, 0.35, 0.6);
|
|
279
|
+
const codeBgC = rgb(0.95, 0.96, 0.97);
|
|
280
|
+
const rowBgC = rgb(0.96, 0.96, 0.98);
|
|
281
|
+
|
|
282
|
+
let page = doc.addPage([PAGE_W, PAGE_H]);
|
|
283
|
+
let y = PAGE_H - MT;
|
|
284
|
+
|
|
285
|
+
const newPage = () => {
|
|
286
|
+
page = doc.addPage([PAGE_W, PAGE_H]);
|
|
287
|
+
y = PAGE_H - MT;
|
|
288
|
+
};
|
|
289
|
+
const ensure = (h) => {
|
|
290
|
+
if (y - h < MB) newPage();
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const fontFor = (tk) => (tk.mono ? mono : tk.b ? bold : reg);
|
|
294
|
+
const H_SIZES = { 1: 20, 2: 15.5, 3: 13 };
|
|
295
|
+
|
|
296
|
+
/* başlık bloğu */
|
|
297
|
+
const titleText = String(title || '').trim();
|
|
298
|
+
if (titleText) {
|
|
299
|
+
const tSpans = [{ t: titleText, b: true }];
|
|
300
|
+
const tLines = wrapSpans(tSpans, fontFor, 22, availW);
|
|
301
|
+
for (const ln of tLines) {
|
|
302
|
+
ensure(30);
|
|
303
|
+
drawTokenLine(page, ln, MX, y - 22, fontFor, 22, textC);
|
|
304
|
+
y -= 30;
|
|
305
|
+
}
|
|
306
|
+
const sub = String(subtitle || '').trim();
|
|
307
|
+
if (sub) {
|
|
308
|
+
const sLines = wrapSpans(toSpans(sub), fontFor, 10.5, availW);
|
|
309
|
+
for (const ln of sLines) {
|
|
310
|
+
ensure(16);
|
|
311
|
+
drawTokenLine(page, ln, MX, y - 10, fontFor, 10.5, mutedC);
|
|
312
|
+
y -= 16;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
y -= 6;
|
|
316
|
+
page.drawLine({
|
|
317
|
+
start: { x: MX, y },
|
|
318
|
+
end: { x: PAGE_W - MX, y },
|
|
319
|
+
thickness: 1,
|
|
320
|
+
color: accentC,
|
|
321
|
+
});
|
|
322
|
+
y -= 22;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const blocks = parseBlocks(raw.slice(0, CONTENT_CAP));
|
|
326
|
+
|
|
327
|
+
/* title verildiyse içeriğin başındaki AYNI H1'i at (çift başlık olmasın) */
|
|
328
|
+
if (titleText && blocks.length && blocks[0].type === 'h' && blocks[0].level === 1) {
|
|
329
|
+
const norm = (s) =>
|
|
330
|
+
String(s || '')
|
|
331
|
+
.toLocaleLowerCase('tr')
|
|
332
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
333
|
+
.trim();
|
|
334
|
+
if (norm(blocks[0].text) === norm(titleText)) blocks.shift();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const BODY = 10.5;
|
|
338
|
+
const LINE = BODY * 1.42;
|
|
339
|
+
|
|
340
|
+
for (const b of blocks) {
|
|
341
|
+
if (b.type === 'h') {
|
|
342
|
+
const size = H_SIZES[b.level] || 13;
|
|
343
|
+
const above = b.level === 1 ? 16 : b.level === 2 ? 13 : 10;
|
|
344
|
+
const below = b.level === 1 ? 10 : 6;
|
|
345
|
+
y -= above;
|
|
346
|
+
ensure(size * 1.4 + below);
|
|
347
|
+
const lines = wrapSpans(toSpans(b.text), fontFor, size, availW);
|
|
348
|
+
for (const ln of lines) {
|
|
349
|
+
ensure(size * 1.45);
|
|
350
|
+
drawTokenLine(page, ln, MX, y - size, fontFor, size, textC);
|
|
351
|
+
y -= size * 1.45;
|
|
352
|
+
}
|
|
353
|
+
if (b.level === 1) {
|
|
354
|
+
y -= 4;
|
|
355
|
+
page.drawLine({
|
|
356
|
+
start: { x: MX, y },
|
|
357
|
+
end: { x: MX + 42, y },
|
|
358
|
+
thickness: 1.4,
|
|
359
|
+
color: accentC,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
y -= below;
|
|
363
|
+
} else if (b.type === 'p') {
|
|
364
|
+
const lines = wrapSpans(toSpans(b.text), fontFor, BODY, availW);
|
|
365
|
+
for (const ln of lines) {
|
|
366
|
+
ensure(LINE);
|
|
367
|
+
drawTokenLine(page, ln, MX, y - BODY, fontFor, BODY, textC);
|
|
368
|
+
y -= LINE;
|
|
369
|
+
}
|
|
370
|
+
y -= LINE * 0.55;
|
|
371
|
+
} else if (b.type === 'li') {
|
|
372
|
+
const lines = wrapSpans(toSpans(b.text), fontFor, BODY, availW - 24);
|
|
373
|
+
const mW = (() => {
|
|
374
|
+
try {
|
|
375
|
+
return bold.widthOfTextAtSize(b.marker, BODY);
|
|
376
|
+
} catch {
|
|
377
|
+
return 12;
|
|
378
|
+
}
|
|
379
|
+
})();
|
|
380
|
+
lines.forEach((ln, idx) => {
|
|
381
|
+
ensure(LINE);
|
|
382
|
+
if (idx === 0) {
|
|
383
|
+
try {
|
|
384
|
+
page.drawText(b.marker, { x: MX + 2, y: y - BODY, size: BODY, font: bold, color: accentC });
|
|
385
|
+
} catch {}
|
|
386
|
+
}
|
|
387
|
+
drawTokenLine(page, ln, MX + 24, y - BODY, fontFor, BODY, textC);
|
|
388
|
+
y -= LINE;
|
|
389
|
+
});
|
|
390
|
+
y -= LINE * 0.28;
|
|
391
|
+
} else if (b.type === 'quote') {
|
|
392
|
+
const lines = wrapSpans(toSpans(b.text), fontFor, BODY, availW - 18);
|
|
393
|
+
y -= 3;
|
|
394
|
+
ensure(LINE * lineCount(lines) + 6);
|
|
395
|
+
const top = y + LINE * 0.2;
|
|
396
|
+
page.drawLine({
|
|
397
|
+
start: { x: MX + 4, y: top - LINE * lineCount(lines) },
|
|
398
|
+
end: { x: MX + 4, y: top },
|
|
399
|
+
thickness: 2,
|
|
400
|
+
color: accentC,
|
|
401
|
+
});
|
|
402
|
+
for (const ln of lines) {
|
|
403
|
+
drawTokenLine(page, ln, MX + 18, y - BODY, fontFor, BODY, mutedC);
|
|
404
|
+
y -= LINE;
|
|
405
|
+
}
|
|
406
|
+
y -= LINE * 0.55;
|
|
407
|
+
} else if (b.type === 'code') {
|
|
408
|
+
const CS = 9.5;
|
|
409
|
+
const CL = CS * 1.4;
|
|
410
|
+
/* kod satırları: mono genişliğine göre SERT böl (wrapSpans değil —
|
|
411
|
+
boşluk korunsun, sekme 4 boşluk) */
|
|
412
|
+
b._wrapped = b.lines
|
|
413
|
+
.map((l) => {
|
|
414
|
+
const out = [];
|
|
415
|
+
let cur = l;
|
|
416
|
+
while (cur.length) {
|
|
417
|
+
let cut = cur.length;
|
|
418
|
+
try {
|
|
419
|
+
while (cut > 0 && mono.widthOfTextAtSize(cur.slice(0, cut), CS) > availW - 24) cut = Math.floor(cut * 0.92);
|
|
420
|
+
} catch {
|
|
421
|
+
cut = Math.min(cur.length, 90);
|
|
422
|
+
}
|
|
423
|
+
if (cut <= 0) cut = 1;
|
|
424
|
+
out.push(cur.slice(0, cut));
|
|
425
|
+
cur = cur.slice(cut);
|
|
426
|
+
}
|
|
427
|
+
return out.length ? out : [''];
|
|
428
|
+
})
|
|
429
|
+
.flat();
|
|
430
|
+
const nLines = Math.max(1, b._wrapped.length);
|
|
431
|
+
ensure(CL * nLines + 14);
|
|
432
|
+
const top = y + 8;
|
|
433
|
+
const boxH = CL * nLines + 10;
|
|
434
|
+
page.drawRectangle({
|
|
435
|
+
x: MX - 4,
|
|
436
|
+
y: top - boxH,
|
|
437
|
+
width: availW + 8,
|
|
438
|
+
height: boxH,
|
|
439
|
+
color: codeBgC,
|
|
440
|
+
});
|
|
441
|
+
for (const cl of b._wrapped) {
|
|
442
|
+
try {
|
|
443
|
+
page.drawText(cl, { x: MX + 6, y: y - CS, size: CS, font: mono, color: textC });
|
|
444
|
+
} catch {}
|
|
445
|
+
y -= CL;
|
|
446
|
+
}
|
|
447
|
+
y -= 10;
|
|
448
|
+
} else if (b.type === 'hr') {
|
|
449
|
+
ensure(18);
|
|
450
|
+
y -= 8;
|
|
451
|
+
page.drawLine({
|
|
452
|
+
start: { x: MX, y },
|
|
453
|
+
end: { x: PAGE_W - MX, y },
|
|
454
|
+
thickness: 0.7,
|
|
455
|
+
color: rgb(0.85, 0.86, 0.9),
|
|
456
|
+
});
|
|
457
|
+
y -= 14;
|
|
458
|
+
} else if (b.type === 'table') {
|
|
459
|
+
const TS = 9.5;
|
|
460
|
+
const TL = TS * 1.38;
|
|
461
|
+
const rows = b.rows;
|
|
462
|
+
const nCol = Math.max(...rows.map((r) => r.length));
|
|
463
|
+
if (nCol <= 0 || nCol > 12) continue;
|
|
464
|
+
const gap = 6;
|
|
465
|
+
const colW = (availW - gap * (nCol - 1)) / nCol;
|
|
466
|
+
const cellLines = (text) => wrapSpans(toSpans(String(text || '')), fontFor, TS, colW - 8);
|
|
467
|
+
const rendered = rows.map((r) => {
|
|
468
|
+
const cells = [];
|
|
469
|
+
for (let c = 0; c < nCol; c++) cells.push(cellLines(r[c] || ''));
|
|
470
|
+
return cells;
|
|
471
|
+
});
|
|
472
|
+
for (let r = 0; r < rendered.length; r++) {
|
|
473
|
+
const isHead = r === 0;
|
|
474
|
+
const nLines = Math.max(...rendered[r].map((cl) => cl.length), 1);
|
|
475
|
+
const rowH = TL * nLines + 8;
|
|
476
|
+
ensure(rowH + 2);
|
|
477
|
+
if (isHead) {
|
|
478
|
+
page.drawRectangle({
|
|
479
|
+
x: MX - 4,
|
|
480
|
+
y: y - rowH + 4,
|
|
481
|
+
width: availW + 8,
|
|
482
|
+
height: rowH,
|
|
483
|
+
color: rowBgC,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
for (let c = 0; c < nCol; c++) {
|
|
487
|
+
let cy = y;
|
|
488
|
+
for (const ln of rendered[r][c]) {
|
|
489
|
+
const spans = isHead ? ln.map((t) => ({ ...t, b: true })) : ln;
|
|
490
|
+
drawTokenLine(page, spans, MX + c * (colW + gap) + 4, cy - TS, fontFor, TS, textC);
|
|
491
|
+
cy -= TL;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
y -= rowH;
|
|
495
|
+
}
|
|
496
|
+
y -= 10;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* sayfa numaraları: toplam sayı üretilince ikinci geçişte basılır */
|
|
501
|
+
const docTitle = String(title || '').slice(0, 60);
|
|
502
|
+
const pages = doc.getPages();
|
|
503
|
+
pages.forEach((pg, idx) => {
|
|
504
|
+
const label = `Sayfa ${idx + 1} / ${pages.length}`;
|
|
505
|
+
try {
|
|
506
|
+
const w = reg.widthOfTextAtSize(label, 8);
|
|
507
|
+
pg.drawText(label, { x: PAGE_W - MX - w, y: 30, size: 8, font: reg, color: mutedC });
|
|
508
|
+
if (docTitle) pg.drawText(docTitle, { x: MX, y: 30, size: 8, font: reg, color: mutedC });
|
|
509
|
+
} catch {}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const bytes = await doc.save();
|
|
513
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
514
|
+
fs.writeFileSync(abs, Buffer.from(bytes));
|
|
515
|
+
return {
|
|
516
|
+
ok: true,
|
|
517
|
+
path: abs,
|
|
518
|
+
name: path.basename(abs),
|
|
519
|
+
pages: pages.length,
|
|
520
|
+
bytes: bytes.length,
|
|
521
|
+
note: 'PDF üretildi (Türkçe font gömülü) — kullanıcıya ulaşmak için send_file çağır',
|
|
522
|
+
};
|
|
523
|
+
} catch (e) {
|
|
524
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
module.exports = { writePdf, write: writePdf, parseBlocks, toSpans, firstFont, hasFonts };
|
package/src/agent/skills.js
CHANGED
|
@@ -299,25 +299,38 @@ Zorunlu kurallar:
|
|
|
299
299
|
force: true,
|
|
300
300
|
body: `---
|
|
301
301
|
name: pdf
|
|
302
|
-
description: PDF işleme rehberi — metin çıkarma (pdf-parse),
|
|
303
|
-
version: 1.
|
|
302
|
+
description: PDF işleme rehberi — RAPOR/BELGE ÜRETİMİ pdf_write aracıyla TEK ÇAĞRIDA (Türkçe garantili); metin çıkarma (pdf-parse), PDF değiştirme/birleştirme (pdf-lib), sayfa→görsel (pdf-to-img), taranmış PDF için OCR (tesseract.js). Kullanıcı bir .pdf dosyasından bahsederse YA DA PDF çıktısı istenirse ÖNCE bu skill'i oku.
|
|
303
|
+
version: 1.2.0
|
|
304
304
|
---
|
|
305
305
|
|
|
306
306
|
# PDF İşleme Rehberi
|
|
307
307
|
|
|
308
|
-
## EN
|
|
308
|
+
## EN HIZLI YOL — pdf_write aracı (RAPOR/ÖZET/NOT/BELGE İSTEĞİNDE ÖNCE BU)
|
|
309
|
+
|
|
310
|
+
Elle script YAZMA. Ajanın pdf_write aracı tek çağrıda markdown-lite içeriği düzenli, Türkçe-güvenli PDF'e çevirir (Windows fontu otomatik gömülür — ğ ş ı İ Ğ Ş Ö Ü bozulmaz; başlık, madde işareti, tablo, sayfa numarası otomatik):
|
|
311
|
+
|
|
312
|
+
\`\`\`
|
|
313
|
+
pdf_write {"path": "rapor.pdf", "title": "2026 Özet Raporu", "subtitle": "07.09.2026", "content": "# Başlık\\n## Alt Başlık\\n- madde 1\\n- **önemli** madde\\n1. sıralı adım\\n| Kolon | Değer |\\n|---|---|\\n| Gelir | 12.500 TL |\\n> alıntı satırı"}
|
|
314
|
+
\`\`\`
|
|
315
|
+
|
|
316
|
+
Desteklenen markdown-lite: "# ## ###" başlıklar · düz paragraf · "- " bullet · "1. " numaralı · **kalın** · kod (geri tırnak içi) · "> " alıntı · kod bloğu (üç ters tırnakla açılıp kapanır) · "| kolon |" tablo satırları.
|
|
317
|
+
|
|
318
|
+
Sonra çıktıyı kullanıcıya ulaştırmak için send_file çağır. Script yazma, pip kurma — GEREK YOK.
|
|
319
|
+
|
|
320
|
+
## EN KRİTİK KURALLAR (elle script gerektiğinde bile geçerli)
|
|
309
321
|
|
|
310
322
|
1. Türkçe karakterli PDF için pip\u2019ten paket KURMA/KULLANMA (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) — bunlar ğ ş ı İ karakterlerini bozar. Doğru kit Node\u2019ta ZATEN kurulu: \`pdf-lib\` + \`@pdf-lib/fontkit\` ve \`pdfkit\`. Script\u2019i write_file ile .js yaz, run_command ile \`node script.js\` çalıştır (python_run İLE DEĞİL).
|
|
311
|
-
2. md→pdf çevirici YOKTUR ve kurulmaz — kullanıcıya rapor/özet/belge çıktısı vereceksen .md gönderme;
|
|
323
|
+
2. md→pdf çevirici YOKTUR ve kurulmaz — kullanıcıya rapor/özet/belge çıktısı vereceksen .md gönderme; pdf_write (ya da elle pdf-lib script) ile DOĞRUDAN PDF üret ve send_file ile gönder.
|
|
312
324
|
3. \`pdf-parse\` ile \`pdf-to-img\` aynı process\u2019e yüklenmez (aşağıdaki çakışma kuralı).
|
|
313
325
|
|
|
314
326
|
Kurulu paketler ve rolleri:
|
|
315
327
|
|
|
316
328
|
| İstek | Paket |
|
|
317
329
|
|---|---|
|
|
330
|
+
| Rapor/özet/belge üretimi | **pdf_write aracı (tek çağrı)** |
|
|
318
331
|
| Metin çıkar / oku | \`pdf-parse\` |
|
|
319
332
|
| Yeni PDF üret / mevcut PDF'i değiştir | \`pdf-lib\` (+ \`@pdf-lib/fontkit\`) |
|
|
320
|
-
| Düzenli belge üretimi (
|
|
333
|
+
| Düzenli belge üretimi (elle) | \`pdfkit\` |
|
|
321
334
|
| Sayfa→PNG/JPG (taranmış/görsel PDF) | \`pdf-to-img\` |
|
|
322
335
|
| Taranmış PDF'den metin (OCR) | \`pdf-to-img\` + \`tesseract.js\` |
|
|
323
336
|
|
package/src/main.js
CHANGED
|
@@ -2054,8 +2054,8 @@ function fmtPerm(p) {
|
|
|
2054
2054
|
function botToolSet(cfg) {
|
|
2055
2055
|
const s = cfg && cfg.skills ? cfg.skills : {};
|
|
2056
2056
|
const set = new Set([
|
|
2057
|
-
/* çekirdek: her botta konuşma + plan + hatırlatıcı */
|
|
2058
|
-
'todo_write', 'set_reminder', 'send_file',
|
|
2057
|
+
/* çekirdek: her botta konuşma + plan + hatırlatıcı + PDF üretimi */
|
|
2058
|
+
'todo_write', 'set_reminder', 'send_file', 'pdf_write',
|
|
2059
2059
|
'tasks_list', 'task_status', 'task_cancel',
|
|
2060
2060
|
'run_background', 'run_background_many', 'delegate_task',
|
|
2061
2061
|
'event_list', 'event_subscribe', 'event_unsubscribe',
|