halbot 1995.1.51 → 1995.1.53
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 +4 -1
- package/web/ogimage.mjs +213 -0
- package/web/turn.html +2 -1
- package/web/turn.mjs +38 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halbot",
|
|
3
3
|
"description": "Just another AI powered Telegram bot, which is simple design, easy to use, extendable and fun.",
|
|
4
|
-
"version": "1995.1.
|
|
4
|
+
"version": "1995.1.53",
|
|
5
5
|
"private": false,
|
|
6
6
|
"homepage": "https://github.com/Leask/halbot",
|
|
7
7
|
"type": "module",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@google-cloud/discoveryengine": "^2.5.2",
|
|
35
35
|
"@google/genai": "^1.42.0",
|
|
36
36
|
"@mozilla/readability": "^0.6.0",
|
|
37
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
37
38
|
"fluent-ffmpeg": "^2.1.3",
|
|
38
39
|
"google-gax": "^5.0.6",
|
|
39
40
|
"ioredis": "^5.9.3",
|
|
@@ -43,8 +44,10 @@
|
|
|
43
44
|
"mysql2": "^3.17.3",
|
|
44
45
|
"office-text-extractor": "^4.0.0",
|
|
45
46
|
"openai": "^6.22.0",
|
|
47
|
+
"parse-numeric-range": "^1.3.0",
|
|
46
48
|
"pg": "^8.18.0",
|
|
47
49
|
"pgvector": "^0.2.1",
|
|
50
|
+
"satori": "^0.19.2",
|
|
48
51
|
"telegraf": "^4.16.3",
|
|
49
52
|
"tellegram": "^1.1.18",
|
|
50
53
|
"tesseract.js": "^7.0.0",
|
package/web/ogimage.mjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { callosum, dbio, hal } from '../index.mjs';
|
|
2
|
+
import satori from 'satori';
|
|
3
|
+
import { Resvg } from '@resvg/resvg-js';
|
|
4
|
+
import { readFile } from 'fs/promises';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
// Pre-load font
|
|
12
|
+
let interFontBuffer;
|
|
13
|
+
let notoFontBuffer;
|
|
14
|
+
|
|
15
|
+
const getFonts = async () => {
|
|
16
|
+
if (!interFontBuffer) {
|
|
17
|
+
try {
|
|
18
|
+
const url = 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZJhjp-Ek-_EeA.woff';
|
|
19
|
+
const res = await fetch(url);
|
|
20
|
+
interFontBuffer = await res.arrayBuffer();
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error('Failed to load Inter font:', e);
|
|
23
|
+
throw new Error('Inter Font loading failed');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!notoFontBuffer) {
|
|
27
|
+
try {
|
|
28
|
+
// Fetching a complete Noto Sans TC OTF from JSDelivr to ensure all CJK glyphs load in Satori
|
|
29
|
+
const url = 'https://cdn.jsdelivr.net/gh/googlefonts/noto-cjk@main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf';
|
|
30
|
+
const res = await fetch(url);
|
|
31
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
32
|
+
notoFontBuffer = await res.arrayBuffer();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error('Failed to load Noto font:', e);
|
|
35
|
+
throw new Error('Noto Font loading failed');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { inter: interFontBuffer, noto: notoFontBuffer };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const process = async (ctx, next) => {
|
|
42
|
+
const token = ctx.params.token;
|
|
43
|
+
|
|
44
|
+
// 1. Fetch Chat Data
|
|
45
|
+
const result = await dbio.queryOne(
|
|
46
|
+
`SELECT * FROM ${hal.table} WHERE token = $1`,
|
|
47
|
+
[token]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!result) {
|
|
51
|
+
ctx.status = 404;
|
|
52
|
+
ctx.body = 'Not Found';
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
result.received = JSON.parse(result.received);
|
|
57
|
+
const msg = result.received.message;
|
|
58
|
+
const userText = result.received_text || '';
|
|
59
|
+
const username = msg.from.username || msg.from.first_name || 'User';
|
|
60
|
+
const chatId = result.chat_id || 'Unknown';
|
|
61
|
+
|
|
62
|
+
// Allow CSS to handle text clamping, but give a safe upper limit to prevent memory overloads
|
|
63
|
+
let previewText = userText.replace(/<[^>]+>/g, '').trim();
|
|
64
|
+
if (previewText.length > 500) {
|
|
65
|
+
previewText = previewText.substring(0, 500) + '...';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Build Satori VDOM
|
|
69
|
+
const fonts = await getFonts();
|
|
70
|
+
|
|
71
|
+
const svg = await satori(
|
|
72
|
+
{
|
|
73
|
+
type: 'div',
|
|
74
|
+
props: {
|
|
75
|
+
style: {
|
|
76
|
+
height: '100%',
|
|
77
|
+
width: '100%',
|
|
78
|
+
display: 'flex',
|
|
79
|
+
flexDirection: 'column',
|
|
80
|
+
alignItems: 'flex-start',
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
backgroundColor: '#0a0a0c', // HAL9000 Dark bg
|
|
83
|
+
backgroundImage: 'radial-gradient(circle at 25px 25px, rgba(255, 255, 255, 0.05) 2%, transparent 0%), radial-gradient(circle at 75px 75px, rgba(255, 255, 255, 0.05) 2%, transparent 0%)',
|
|
84
|
+
backgroundSize: '100px 100px',
|
|
85
|
+
padding: '60px',
|
|
86
|
+
fontFamily: '"Inter"',
|
|
87
|
+
color: '#ffffff',
|
|
88
|
+
},
|
|
89
|
+
children: [
|
|
90
|
+
// Brand / Logo area
|
|
91
|
+
{
|
|
92
|
+
type: 'div',
|
|
93
|
+
props: {
|
|
94
|
+
style: {
|
|
95
|
+
display: 'flex',
|
|
96
|
+
alignItems: 'center',
|
|
97
|
+
marginBottom: '40px',
|
|
98
|
+
},
|
|
99
|
+
children: [
|
|
100
|
+
{
|
|
101
|
+
type: 'img',
|
|
102
|
+
props: {
|
|
103
|
+
src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3CradialGradient id='lens-glow' cx='50%25' cy='50%25' r='50%25' fx='50%25' fy='50%25'%3E%3Cstop offset='0%25' style='stop-color:%23ffeb3b;stop-opacity:1' /%3E%3Cstop offset='20%25' style='stop-color:%23ff9800;stop-opacity:1' /%3E%3Cstop offset='60%25' style='stop-color:%23f44336;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23b71c1c;stop-opacity:1' /%3E%3C/radialGradient%3E%3ClinearGradient id='metal-rim' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23e0e0e0;stop-opacity:1' /%3E%3Cstop offset='50%25' style='stop-color:%239e9e9e;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23616161;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='48' style='fill:url(%23metal-rim);stroke:%23333;stroke-width:2' /%3E%3Ccircle cx='50' cy='50' r='42' style='fill:%23111' /%3E%3Ccircle cx='50' cy='50' r='28' style='fill:url(%23lens-glow);stroke:%23800000;stroke-width:1' /%3E%3Ccircle cx='35' cy='35' r='5' style='fill:%23fff;opacity:0.6;' /%3E%3C/svg%3E",
|
|
104
|
+
style: {
|
|
105
|
+
width: '64px',
|
|
106
|
+
height: '64px',
|
|
107
|
+
marginRight: '20px'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'div',
|
|
113
|
+
props: {
|
|
114
|
+
style: { fontSize: '48px', fontWeight: 'bold', color: '#00f0ff', letterSpacing: '2px' },
|
|
115
|
+
children: 'HAL9000'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
// Chat ID Info
|
|
122
|
+
{
|
|
123
|
+
type: 'div',
|
|
124
|
+
props: {
|
|
125
|
+
style: { fontSize: '36px', color: '#888', marginBottom: '20px' },
|
|
126
|
+
children: `Chat ID: ${chatId}`
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
// Preview text
|
|
130
|
+
{
|
|
131
|
+
type: 'div',
|
|
132
|
+
props: {
|
|
133
|
+
style: {
|
|
134
|
+
display: '-webkit-box',
|
|
135
|
+
WebkitLineClamp: 4,
|
|
136
|
+
WebkitBoxOrient: 'vertical',
|
|
137
|
+
fontSize: '38px',
|
|
138
|
+
color: '#eee',
|
|
139
|
+
lineHeight: '1.5',
|
|
140
|
+
background: 'rgba(255,255,255,0.05)',
|
|
141
|
+
padding: '30px',
|
|
142
|
+
borderLeft: '8px solid #00f0ff',
|
|
143
|
+
maxWidth: '1080px',
|
|
144
|
+
wordBreak: 'break-all',
|
|
145
|
+
overflow: 'hidden',
|
|
146
|
+
textOverflow: 'ellipsis'
|
|
147
|
+
},
|
|
148
|
+
children: previewText
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
// Footer Username
|
|
152
|
+
{
|
|
153
|
+
type: 'div',
|
|
154
|
+
props: {
|
|
155
|
+
style: { marginTop: 'auto', fontSize: '28px', color: '#aaa', display: 'flex', alignItems: 'center' },
|
|
156
|
+
children: [
|
|
157
|
+
{
|
|
158
|
+
type: 'span',
|
|
159
|
+
props: { style: { color: '#00f0ff', marginRight: '10px' }, children: '●' }
|
|
160
|
+
},
|
|
161
|
+
`Prompt by @${username}`
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
width: 1200,
|
|
170
|
+
height: 630,
|
|
171
|
+
fonts: [
|
|
172
|
+
{
|
|
173
|
+
name: 'Inter',
|
|
174
|
+
data: fonts.inter,
|
|
175
|
+
weight: 400,
|
|
176
|
+
style: 'normal',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'Noto Sans TC',
|
|
180
|
+
data: fonts.noto,
|
|
181
|
+
weight: 400,
|
|
182
|
+
style: 'normal',
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// 3. Render SVG to PNG using Resvg
|
|
189
|
+
const resvg = new Resvg(svg, {
|
|
190
|
+
background: '#0a0a0c',
|
|
191
|
+
fitTo: { mode: 'width', value: 1200 },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const pngData = resvg.render();
|
|
195
|
+
const pngBuffer = pngData.asPng();
|
|
196
|
+
|
|
197
|
+
// 4. Send response
|
|
198
|
+
ctx.set('Content-Type', 'image/png');
|
|
199
|
+
ctx.set('Cache-Control', 'public, max-age=86400'); // Cache for 1 day
|
|
200
|
+
ctx.body = pngBuffer;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const { actions } = {
|
|
204
|
+
actions: [
|
|
205
|
+
{
|
|
206
|
+
path: 'og-image/:token',
|
|
207
|
+
method: 'GET',
|
|
208
|
+
process,
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export { process };
|
package/web/turn.html
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<title>HAL9000 Chat</title>
|
|
8
8
|
<link rel="icon" type="image/svg+xml"
|
|
9
9
|
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3CradialGradient id='a' cx='50%25' cy='50%25' r='50%25' fx='50%25' fy='50%25'%3E%3Cstop offset='0%25' stop-color='%23ffeb3b'/%3E%3Cstop offset='20%25' stop-color='%23ff9800'/%3E%3Cstop offset='60%25' stop-color='%23f44336'/%3E%3Cstop offset='100%25' stop-color='%23b71c1c'/%3E%3C/radialGradient%3E%3ClinearGradient id='b' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23e0e0e0'/%3E%3Cstop offset='50%25' stop-color='%239e9e9e'/%3E%3Cstop offset='100%25' stop-color='%23616161'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='48' fill='url(%23b)' stroke='%23333' stroke-width='2'/%3E%3Ccircle cx='50' cy='50' r='42' fill='%23111'/%3E%3Ccircle cx='50' cy='50' r='28' fill='url(%23a)' stroke='%23800000' stroke-width='1'/%3E%3Cellipse cx='60' cy='40' rx='10' ry='6' fill='rgba(255,255,255,0.4)' transform='rotate(-45 60 40)'/%3E%3C/svg%3E">
|
|
10
|
+
{{meta_tags}}
|
|
10
11
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
11
12
|
<link rel="stylesheet"
|
|
12
13
|
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.6.1/github-markdown-light.min.css">
|
|
@@ -497,7 +498,7 @@
|
|
|
497
498
|
// Set Chat ID from data
|
|
498
499
|
if (chatData.chat_id) {
|
|
499
500
|
chatId.textContent = `Chat ID: ${chatData.chat_id}`;
|
|
500
|
-
document.title = `
|
|
501
|
+
document.title = `HAL9000 Chat: ${chatData.chat_id}`;
|
|
501
502
|
} else {
|
|
502
503
|
chatId.textContent = `Chat ID: UNKNOWN`;
|
|
503
504
|
}
|
package/web/turn.mjs
CHANGED
|
@@ -3,7 +3,14 @@ import { readFile } from 'fs/promises';
|
|
|
3
3
|
|
|
4
4
|
const getPath = (subPath) => utilitas.__(import.meta.url, subPath);
|
|
5
5
|
const getHtml = async () => await readFile(getPath('turn.html'), 'utf-8');
|
|
6
|
-
const
|
|
6
|
+
const safeStringify = (obj) => JSON.stringify(obj)
|
|
7
|
+
.replace(/</g, '\\u003c')
|
|
8
|
+
.replace(/>/g, '\\u003e')
|
|
9
|
+
.replace(/&/g, '\\u0026')
|
|
10
|
+
.replace(/\u2028/g, '\\u2028')
|
|
11
|
+
.replace(/\u2029/g, '\\u2029');
|
|
12
|
+
const renderHtml = async (data, metaTags = '') => await getHtml()
|
|
13
|
+
.then((html) => html.replace("'{{data}}'", () => data).replace('{{meta_tags}}', () => metaTags));
|
|
7
14
|
|
|
8
15
|
const file = async (ctx) => {
|
|
9
16
|
try {
|
|
@@ -100,10 +107,38 @@ const process = async (ctx, next) => {
|
|
|
100
107
|
});
|
|
101
108
|
}
|
|
102
109
|
});
|
|
103
|
-
|
|
110
|
+
let previewText = messages.length ? messages[0].text : 'A conversation with HAL9000';
|
|
111
|
+
previewText = previewText.replace(/<[^>]+>/g, '').substring(0, 160).trim() + '...';
|
|
112
|
+
|
|
113
|
+
const ogImageUrl = `https://${ctx.host}/og-image/${ctx.params.token}`;
|
|
114
|
+
|
|
115
|
+
// Construct dynamic meta tags
|
|
116
|
+
const pageTitle = `HAL9000 Chat: ${result.chat_id}`;
|
|
117
|
+
const escapedDesc = previewText.replace(/"/g, '"');
|
|
118
|
+
const escapedTitle = pageTitle.replace(/"/g, '"');
|
|
119
|
+
|
|
120
|
+
const metaTags = `
|
|
121
|
+
<!-- Standard Meta -->
|
|
122
|
+
<meta name="description" content="${escapedDesc}">
|
|
123
|
+
|
|
124
|
+
<!-- Open Graph / Facebook / Meta -->
|
|
125
|
+
<meta property="og:type" content="website">
|
|
126
|
+
<meta property="og:title" content="${escapedTitle}">
|
|
127
|
+
<meta property="og:description" content="${escapedDesc}">
|
|
128
|
+
<meta property="og:site_name" content="HAL9000">
|
|
129
|
+
<meta property="og:image" content="${ogImageUrl}">
|
|
130
|
+
|
|
131
|
+
<!-- Twitter / Telegram -->
|
|
132
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
133
|
+
<meta name="twitter:title" content="${escapedTitle}">
|
|
134
|
+
<meta name="twitter:description" content="${escapedDesc}">
|
|
135
|
+
<meta name="twitter:image" content="${ogImageUrl}">
|
|
136
|
+
`.trim();
|
|
137
|
+
|
|
138
|
+
ctx.body = await renderHtml(safeStringify({
|
|
104
139
|
bot_id: result.bot_id, chat_id: result.chat_id,
|
|
105
140
|
chat_type: result.chat_type, messages, prompt_count,
|
|
106
|
-
}));
|
|
141
|
+
}), metaTags);
|
|
107
142
|
};
|
|
108
143
|
|
|
109
144
|
export const { actions } = {
|