@coffer-org/plugin-telegram 1.3.2 → 2.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/dist/index.js +10 -0
- package/dist/runtime/bot.d.ts +8 -1
- package/dist/runtime/bot.js +179 -40
- package/dist/runtime/chain-store.d.ts +28 -0
- package/dist/runtime/chain-store.js +48 -0
- package/dist/runtime/config.d.ts +3 -0
- package/dist/runtime/config.js +7 -0
- package/dist/runtime/index.js +11 -0
- package/dist/schema.js +243 -50
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -12,6 +12,16 @@ export default definePlugin({
|
|
|
12
12
|
field.password({ key: 'api_hash', label: 'telegram.settings.api_hash' }),
|
|
13
13
|
field.password({ key: 'bot_token', label: 'telegram.settings.bot_token' }),
|
|
14
14
|
field.tel({ key: 'phone', label: 'telegram.settings.phone' }),
|
|
15
|
+
field.select({
|
|
16
|
+
key: 'reasoning_display',
|
|
17
|
+
label: 'telegram.settings.reasoning_display',
|
|
18
|
+
default: 'collapsible',
|
|
19
|
+
options: [
|
|
20
|
+
{ value: 'off', title: 'telegram.settings.reasoning_display_off' },
|
|
21
|
+
{ value: 'collapsible', title: 'telegram.settings.reasoning_display_collapsible' },
|
|
22
|
+
{ value: 'separate', title: 'telegram.settings.reasoning_display_separate' },
|
|
23
|
+
],
|
|
24
|
+
}),
|
|
15
25
|
],
|
|
16
26
|
}),
|
|
17
27
|
});
|
package/dist/runtime/bot.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LiveChannelOps, RenderFn, ReplyChannel, ReplyContext } from '@coffer-org/plugin-orchestrator/runtime';
|
|
2
|
+
import type { LoadOpts, ReasoningDisplay } from './config.ts';
|
|
3
|
+
export declare function stripHtml(s: string): string;
|
|
4
|
+
export declare function escHtml(s: string): string;
|
|
5
|
+
export declare function renderWithReasoning(mode: ReasoningDisplay, max: number): RenderFn;
|
|
6
|
+
export declare const TG_MAX = 4096;
|
|
7
|
+
export declare const TG_THROTTLE_MS = 2000;
|
|
8
|
+
export declare function makeReplyFactory(t: LiveChannelOps, display: ReasoningDisplay): (_chatId: string, _ctx: ReplyContext) => ReplyChannel;
|
|
2
9
|
export type StartOpts = LoadOpts;
|
|
3
10
|
export declare function startBot(opts?: StartOpts): Promise<boolean>;
|
|
4
11
|
export declare function stopBot(): Promise<void>;
|
package/dist/runtime/bot.js
CHANGED
|
@@ -1,13 +1,82 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { TelegramClient, Api } from '
|
|
4
|
-
import { LogLevel } from '
|
|
5
|
-
import { StringSession } from '
|
|
6
|
-
import { NewMessage } from '
|
|
7
|
-
import { handleIncoming } from '@coffer-org/plugin-orchestrator/runtime';
|
|
8
|
-
import {
|
|
3
|
+
import { TelegramClient, Api } from 'teleproto';
|
|
4
|
+
import { LogLevel } from 'teleproto/extensions/Logger.js';
|
|
5
|
+
import { StringSession } from 'teleproto/sessions/index.js';
|
|
6
|
+
import { NewMessage } from 'teleproto/events/index.js';
|
|
7
|
+
import { handleIncoming, loadGatePolicy, makeLiveChannel, chunk } from '@coffer-org/plugin-orchestrator/runtime';
|
|
8
|
+
import { recordUser, recordAssistant, buildChain } from "./chain-store.js";
|
|
9
|
+
import { loadBotConfig, hasCredentials, loadReasoningDisplay } from "./config.js";
|
|
9
10
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
10
11
|
const log = getLogger('telegram');
|
|
12
|
+
const FALLBACK_REPLY_WINDOW_MS = 1_800_000;
|
|
13
|
+
const lastByChat = new Map();
|
|
14
|
+
async function replyWindowMs() {
|
|
15
|
+
try {
|
|
16
|
+
return (await loadGatePolicy()).replyWindow * 1000;
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
log.warn(`gate policy load: ${err instanceof Error ? err.message : String(err)} — using default reply window`);
|
|
20
|
+
return FALLBACK_REPLY_WINDOW_MS;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function stripHtml(s) {
|
|
24
|
+
return s
|
|
25
|
+
.replace(/<[^>]*>/g, '')
|
|
26
|
+
.replace(/</g, '<')
|
|
27
|
+
.replace(/>/g, '>')
|
|
28
|
+
.replace(/"/g, '"')
|
|
29
|
+
.replace(/'/g, "'")
|
|
30
|
+
.replace(/&/g, '&');
|
|
31
|
+
}
|
|
32
|
+
export function escHtml(s) {
|
|
33
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
34
|
+
}
|
|
35
|
+
const OPEN = '<blockquote expandable>';
|
|
36
|
+
const CLOSE = '</blockquote>\n\n';
|
|
37
|
+
const MIN_REASONING = 100;
|
|
38
|
+
export function renderWithReasoning(mode, max) {
|
|
39
|
+
return ({ text, reasoning }) => {
|
|
40
|
+
const answer = (text ?? '').trim() || '⚠️ the agent returned no response';
|
|
41
|
+
if (mode !== 'collapsible')
|
|
42
|
+
return chunk(answer, max);
|
|
43
|
+
const raw = (reasoning ?? '').trim();
|
|
44
|
+
if (!raw)
|
|
45
|
+
return chunk(answer, max);
|
|
46
|
+
const parts = chunk(answer, max);
|
|
47
|
+
const head = parts[0];
|
|
48
|
+
const room = max - head.length - OPEN.length - CLOSE.length;
|
|
49
|
+
if (room < MIN_REASONING)
|
|
50
|
+
return parts;
|
|
51
|
+
let body = escHtml(raw);
|
|
52
|
+
if (body.length > room)
|
|
53
|
+
body = `…${body.slice(-(room - 1))}`;
|
|
54
|
+
return [`${OPEN}${body}${CLOSE}${head}`, ...parts.slice(1)];
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const TELEGRAM_FORMAT = [
|
|
58
|
+
'CRITICAL OUTPUT FORMAT — Telegram HTML ONLY. Format the reply strictly with Telegram\'s HTML subset (<b>, <i>, <code>, <a>, …) and NOTHING else. NEVER use Markdown — no *bold*, _italic_, **bold**, `code`, ```blocks```, # headings, - bullets, or | tables. Markdown does not render here; it shows up as literal characters and breaks the message. When in doubt, use an HTML tag or plain text.',
|
|
59
|
+
'Inline styling (tags may nest): <b>bold</b>, <i>italic</i>, <u>underline</u>, <s>strike</s>, <code>inline code</code>, <spoiler>spoiler</spoiler>. Links: <a href="https://…">text</a> (also <a href="tg://user?id=123">mention</a>).',
|
|
60
|
+
'Blocks: <pre>code block</pre> or, for syntax highlight, <pre><code class="language-python">…</code></pre>. Quotes: <blockquote>quote</blockquote>, or <blockquote expandable>long collapsible quote</blockquote>. Bullet lists: start plain lines with "• ".',
|
|
61
|
+
'Only those tags are parsed. NOT supported here (do not use): <tg-spoiler>, <span>, <ins>, <strike>, <tg-time> — use <spoiler>, <u>, <s> instead. Avoid <tg-emoji> (needs a custom-emoji id + Premium); use ordinary emoji.',
|
|
62
|
+
'DO NOT use markdown (*, _, #, ```), tables, or images. Escape literal <, >, and & in prose as <, >, and & so they are not read as tags.',
|
|
63
|
+
'Render any TABLE as a <pre> block with space-aligned columns — real tables are unsupported.',
|
|
64
|
+
'Beyond the tags, use Unicode symbols and emoji to structure and enrich replies — they render everywhere: bullets (•, ▪, –), separators (─────), arrows (→), status marks (✅ ⚠️ ❌ ⏳), and topic emoji (📅 💰 🏥 🛒 🌱 …). They make messages scannable and friendly.',
|
|
65
|
+
'Keep replies short and skimmable (messages cap at 4096 chars): prefer short paragraphs and "• " bullet lists over long prose.',
|
|
66
|
+
].join('\n');
|
|
67
|
+
export const TG_MAX = 4096;
|
|
68
|
+
export const TG_THROTTLE_MS = 2000;
|
|
69
|
+
export function makeReplyFactory(t, display) {
|
|
70
|
+
return (_chatId, _ctx) => {
|
|
71
|
+
const ch = makeLiveChannel({
|
|
72
|
+
ops: t,
|
|
73
|
+
throttleMs: TG_THROTTLE_MS,
|
|
74
|
+
maxLength: TG_MAX,
|
|
75
|
+
render: renderWithReasoning(display, TG_MAX),
|
|
76
|
+
});
|
|
77
|
+
return display === 'separate' ? ch : { ...ch, segment() { } };
|
|
78
|
+
};
|
|
79
|
+
}
|
|
11
80
|
function loadSessionString(file) {
|
|
12
81
|
try {
|
|
13
82
|
return fs.readFileSync(file, 'utf-8');
|
|
@@ -46,36 +115,62 @@ export async function startBot(opts = {}) {
|
|
|
46
115
|
const me = (await tg.getMe());
|
|
47
116
|
const meId = me?.id?.toString();
|
|
48
117
|
log.info(`connected as @${me?.username || me?.firstName || meId}`);
|
|
49
|
-
async function
|
|
118
|
+
async function sendHtml(peer, text, replyTo) {
|
|
50
119
|
try {
|
|
51
|
-
return await tg.sendMessage(peer, { message: text, parseMode: '
|
|
120
|
+
return await tg.sendMessage(peer, { message: text, parseMode: 'html', replyTo });
|
|
52
121
|
}
|
|
53
122
|
catch {
|
|
54
123
|
try {
|
|
55
|
-
return await tg.sendMessage(peer, { message: text });
|
|
124
|
+
return await tg.sendMessage(peer, { message: stripHtml(text), replyTo });
|
|
56
125
|
}
|
|
57
126
|
catch {
|
|
58
127
|
return undefined;
|
|
59
128
|
}
|
|
60
129
|
}
|
|
61
130
|
}
|
|
62
|
-
async function
|
|
131
|
+
async function editHtml(peer, msgId, text) {
|
|
63
132
|
try {
|
|
64
|
-
await tg.editMessage(peer, { message: msgId, text, parseMode: '
|
|
133
|
+
await tg.editMessage(peer, { message: msgId, text, parseMode: 'html' });
|
|
65
134
|
}
|
|
66
135
|
catch {
|
|
67
136
|
try {
|
|
68
|
-
await tg.editMessage(peer, { message: msgId, text });
|
|
137
|
+
await tg.editMessage(peer, { message: msgId, text: stripHtml(text) });
|
|
69
138
|
}
|
|
70
139
|
catch {
|
|
71
140
|
}
|
|
72
141
|
}
|
|
73
142
|
}
|
|
74
143
|
function startTypingPeer(peer) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
144
|
+
let logged = false;
|
|
145
|
+
let stopped = false;
|
|
146
|
+
let timer;
|
|
147
|
+
const warn = (err) => {
|
|
148
|
+
if (logged)
|
|
149
|
+
return;
|
|
150
|
+
logged = true;
|
|
151
|
+
log.warn(`typing indicator: ${err instanceof Error ? err.message : String(err)}`);
|
|
152
|
+
};
|
|
153
|
+
void (async () => {
|
|
154
|
+
let input;
|
|
155
|
+
try {
|
|
156
|
+
input = await tg.getInputEntity(peer);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
warn(err);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (stopped)
|
|
163
|
+
return;
|
|
164
|
+
const ping = () => tg
|
|
165
|
+
.invoke(new Api.messages.SetTyping({ peer: input, action: new Api.SendMessageTypingAction() }))
|
|
166
|
+
.catch(warn);
|
|
167
|
+
void ping();
|
|
168
|
+
timer = setInterval(() => void ping(), 5000);
|
|
169
|
+
})();
|
|
170
|
+
return () => {
|
|
171
|
+
stopped = true;
|
|
172
|
+
clearInterval(timer);
|
|
173
|
+
};
|
|
79
174
|
}
|
|
80
175
|
tg.addEventHandler(async (event) => {
|
|
81
176
|
try {
|
|
@@ -100,31 +195,75 @@ export async function startBot(opts = {}) {
|
|
|
100
195
|
catch {
|
|
101
196
|
}
|
|
102
197
|
log.info(`msg [${chatLabel}]: ${text.slice(0, 80)}`);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
id
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
198
|
+
const stopTyping = startTypingPeer(peer);
|
|
199
|
+
try {
|
|
200
|
+
const chatId = String(event.chatId ?? '?');
|
|
201
|
+
const userId = message.senderId?.toString() ?? '?';
|
|
202
|
+
const incomingMsgId = String(message.id);
|
|
203
|
+
const replyTo = message.replyTo?.replyToMsgId ?? null;
|
|
204
|
+
let replyToId = replyTo != null ? String(replyTo) : null;
|
|
205
|
+
const nowMs = Date.now();
|
|
206
|
+
if (!replyToId) {
|
|
207
|
+
const prev = lastByChat.get(chatId);
|
|
208
|
+
if (prev && nowMs - prev.ts < (await replyWindowMs()))
|
|
209
|
+
replyToId = prev.msgId;
|
|
210
|
+
}
|
|
211
|
+
lastByChat.set(chatId, { msgId: incomingMsgId, ts: nowMs });
|
|
212
|
+
let displayName;
|
|
213
|
+
try {
|
|
214
|
+
const s = (await message.getSender());
|
|
215
|
+
displayName = s?.firstName || s?.username || undefined;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
}
|
|
219
|
+
await recordUser({ chatId, msgId: incomingMsgId, sender: displayName, text, ts: message.date ?? 0, replyToId });
|
|
220
|
+
const fetch = async (id) => {
|
|
221
|
+
try {
|
|
222
|
+
const arr = (await tg.getMessages(peer, { ids: [Number(id)] }));
|
|
223
|
+
const m = arr?.[0];
|
|
224
|
+
if (!m)
|
|
225
|
+
return null;
|
|
226
|
+
return {
|
|
227
|
+
text: m.message ?? '',
|
|
228
|
+
senderId: m.senderId?.toString() ?? '',
|
|
229
|
+
replyToId: m.replyTo?.replyToMsgId?.toString() ?? null,
|
|
230
|
+
date: m.date ?? 0,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
112
234
|
return null;
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const messages = await buildChain(incomingMsgId, { chatId, fetch, botId: meId ?? '' });
|
|
238
|
+
const reasoningDisplay = await loadReasoningDisplay();
|
|
239
|
+
const connector = {
|
|
240
|
+
id: 'telegram',
|
|
241
|
+
reply: makeReplyFactory({
|
|
242
|
+
send: async (text) => {
|
|
243
|
+
const sent = await sendHtml(peer, text, Number(incomingMsgId));
|
|
244
|
+
return sent ? String(sent.id) : null;
|
|
245
|
+
},
|
|
246
|
+
edit: async (msgId, text) => {
|
|
247
|
+
await editHtml(peer, Number(msgId), text);
|
|
248
|
+
},
|
|
249
|
+
}, reasoningDisplay),
|
|
250
|
+
async recordAssistant(m) {
|
|
251
|
+
await recordAssistant({ ...m, chatId, now: Math.floor(Date.now() / 1000) });
|
|
252
|
+
if (m.botMsgId)
|
|
253
|
+
lastByChat.set(chatId, { msgId: m.botMsgId, ts: Date.now() });
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
await handleIncoming(connector, {
|
|
257
|
+
connectorId: 'telegram',
|
|
258
|
+
chatId,
|
|
259
|
+
channelSystem: TELEGRAM_FORMAT,
|
|
260
|
+
sender: { id: userId, displayName },
|
|
261
|
+
messages,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
stopTyping();
|
|
266
|
+
}
|
|
128
267
|
}
|
|
129
268
|
catch (err) {
|
|
130
269
|
log.error(`message handling: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ConvMessage } from '@coffer-org/plugin-orchestrator/runtime';
|
|
2
|
+
export type FetchMsg = (msgId: string) => Promise<{
|
|
3
|
+
text: string;
|
|
4
|
+
senderId: string;
|
|
5
|
+
replyToId: string | null;
|
|
6
|
+
date: number;
|
|
7
|
+
} | null>;
|
|
8
|
+
export declare function recordUser(m: {
|
|
9
|
+
chatId: string;
|
|
10
|
+
msgId: string;
|
|
11
|
+
sender?: string | null;
|
|
12
|
+
text: string;
|
|
13
|
+
ts: number;
|
|
14
|
+
replyToId: string | null;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
export declare function recordAssistant(m: {
|
|
17
|
+
chatId: string;
|
|
18
|
+
parentMsgId: string | null;
|
|
19
|
+
botMsgId: string | null;
|
|
20
|
+
text: string;
|
|
21
|
+
now: number;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
export declare function buildChain(headMsgId: string, opts: {
|
|
24
|
+
chatId: string;
|
|
25
|
+
fetch: FetchMsg;
|
|
26
|
+
botId: string;
|
|
27
|
+
maxDepth?: number;
|
|
28
|
+
}): Promise<ConvMessage[]>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getThreadMessage, putThreadMessage } from '@coffer-org/server/thread-store';
|
|
2
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
3
|
+
const log = getLogger('telegram');
|
|
4
|
+
const CONNECTOR = 'telegram';
|
|
5
|
+
const DEFAULT_MAX_DEPTH = 30;
|
|
6
|
+
export async function recordUser(m) {
|
|
7
|
+
await putThreadMessage({ connector: CONNECTOR, chatId: m.chatId, msgId: m.msgId, role: 'user', sender: m.sender ?? null, text: m.text, ts: m.ts, replyToId: m.replyToId });
|
|
8
|
+
}
|
|
9
|
+
export async function recordAssistant(m) {
|
|
10
|
+
if (m.botMsgId == null)
|
|
11
|
+
return;
|
|
12
|
+
await putThreadMessage({
|
|
13
|
+
connector: CONNECTOR,
|
|
14
|
+
chatId: m.chatId,
|
|
15
|
+
msgId: m.botMsgId,
|
|
16
|
+
role: 'assistant',
|
|
17
|
+
text: m.text,
|
|
18
|
+
ts: m.now,
|
|
19
|
+
replyToId: m.parentMsgId,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function buildChain(headMsgId, opts) {
|
|
23
|
+
const max = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
24
|
+
const acc = [];
|
|
25
|
+
let cur = headMsgId;
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
while (cur && acc.length < max && !seen.has(cur)) {
|
|
28
|
+
seen.add(cur);
|
|
29
|
+
let stored = await getThreadMessage(CONNECTOR, opts.chatId, cur);
|
|
30
|
+
if (!stored) {
|
|
31
|
+
const fetched = await opts.fetch(cur);
|
|
32
|
+
if (!fetched)
|
|
33
|
+
break;
|
|
34
|
+
const role = fetched.senderId === opts.botId ? 'assistant' : 'user';
|
|
35
|
+
await putThreadMessage({ connector: CONNECTOR, chatId: opts.chatId, msgId: cur, role, sender: null, text: fetched.text, ts: fetched.date, replyToId: fetched.replyToId });
|
|
36
|
+
stored = { msgId: cur, role, sender: null, text: fetched.text, ts: fetched.date, replyToId: fetched.replyToId };
|
|
37
|
+
}
|
|
38
|
+
if (stored.role !== 'reasoning') {
|
|
39
|
+
acc.push({ role: stored.role, content: stored.text, sender: stored.sender, msgId: stored.msgId, ts: stored.ts });
|
|
40
|
+
}
|
|
41
|
+
cur = stored.replyToId;
|
|
42
|
+
}
|
|
43
|
+
if (acc.length >= max)
|
|
44
|
+
log.info(`chain capped at ${max} for ${headMsgId}`);
|
|
45
|
+
while (acc.length && acc.at(-1)?.role !== 'user')
|
|
46
|
+
acc.pop();
|
|
47
|
+
return acc.reverse();
|
|
48
|
+
}
|
package/dist/runtime/config.d.ts
CHANGED
|
@@ -12,3 +12,6 @@ export interface LoadOpts {
|
|
|
12
12
|
}
|
|
13
13
|
export declare function loadBotConfig(opts?: LoadOpts): BotConfig;
|
|
14
14
|
export declare function hasCredentials(cfg: Pick<BotConfig, 'apiId' | 'apiHash' | 'botToken' | 'phone'>): boolean;
|
|
15
|
+
export type ReasoningDisplay = 'off' | 'collapsible' | 'separate';
|
|
16
|
+
export declare function toReasoningDisplay(v: unknown): ReasoningDisplay;
|
|
17
|
+
export declare function loadReasoningDisplay(): Promise<ReasoningDisplay>;
|
package/dist/runtime/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { getPluginSettings } from '@coffer-org/server/plugin-runtime';
|
|
3
4
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
4
5
|
export const PLUGIN_ROOT = path.resolve(__dirname, '../..');
|
|
5
6
|
export function loadBotConfig(opts = {}) {
|
|
@@ -16,3 +17,9 @@ export function loadBotConfig(opts = {}) {
|
|
|
16
17
|
export function hasCredentials(cfg) {
|
|
17
18
|
return Boolean(cfg.apiId && cfg.apiHash && (cfg.botToken || cfg.phone));
|
|
18
19
|
}
|
|
20
|
+
export function toReasoningDisplay(v) {
|
|
21
|
+
return v === 'off' || v === 'separate' ? v : 'collapsible';
|
|
22
|
+
}
|
|
23
|
+
export async function loadReasoningDisplay() {
|
|
24
|
+
return toReasoningDisplay((await getPluginSettings('telegram'))['reasoning_display']);
|
|
25
|
+
}
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getPluginSettings } from '@coffer-org/server/plugin-runtime';
|
|
2
|
+
import { pruneThreadMessages } from '@coffer-org/server/thread-store';
|
|
2
3
|
import { startBot, stopBot } from "./bot.js";
|
|
3
4
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
4
5
|
const log = getLogger('telegram');
|
|
6
|
+
const THREAD_TTL_MS = Number(process.env['TELEGRAM_THREAD_TTL_MS'] ?? 7 * 86_400_000);
|
|
5
7
|
export { startBot, stopBot };
|
|
6
8
|
export { loadBotConfig, hasCredentials } from "./config.js";
|
|
7
9
|
export const serverHooks = {
|
|
@@ -12,4 +14,13 @@ export const serverHooks = {
|
|
|
12
14
|
teardown: async () => {
|
|
13
15
|
await stopBot();
|
|
14
16
|
},
|
|
17
|
+
backgroundTasks: [
|
|
18
|
+
{
|
|
19
|
+
name: 'telegram-thread-prune',
|
|
20
|
+
intervalMs: 86_400_000,
|
|
21
|
+
run: async () => {
|
|
22
|
+
await pruneThreadMessages('telegram', Math.floor((Date.now() - THREAD_TTL_MS) / 1000));
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
15
26
|
};
|
package/dist/schema.js
CHANGED
|
@@ -21,14 +21,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
//#region ../sdk/
|
|
24
|
+
//#region ../sdk/src/plugin.ts
|
|
25
25
|
function definePlugin(p) {
|
|
26
26
|
if (!p.id) throw new Error("[plugin] missing id");
|
|
27
27
|
if (!p.version) console.warn(`[plugin] ${p.id}: missing version`);
|
|
28
28
|
return p;
|
|
29
29
|
}
|
|
30
30
|
//#endregion
|
|
31
|
-
//#region ../sdk/
|
|
31
|
+
//#region ../sdk/src/settings.ts
|
|
32
32
|
function defineSettings(s) {
|
|
33
33
|
return s;
|
|
34
34
|
}
|
|
@@ -154,7 +154,10 @@ function assignProp(target, prop, value) {
|
|
|
154
154
|
}
|
|
155
155
|
function mergeDefs(...defs) {
|
|
156
156
|
const mergedDescriptors = {};
|
|
157
|
-
for (const def of defs)
|
|
157
|
+
for (const def of defs) {
|
|
158
|
+
const descriptors = Object.getOwnPropertyDescriptors(def);
|
|
159
|
+
Object.assign(mergedDescriptors, descriptors);
|
|
160
|
+
}
|
|
158
161
|
return Object.defineProperties({}, mergedDescriptors);
|
|
159
162
|
}
|
|
160
163
|
function esc(str) {
|
|
@@ -476,7 +479,7 @@ var _parse = (_Err) => (schema, value, _ctx, _params) => {
|
|
|
476
479
|
}, ctx);
|
|
477
480
|
if (result instanceof Promise) throw new $ZodAsyncError();
|
|
478
481
|
if (result.issues.length) {
|
|
479
|
-
const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
|
|
482
|
+
const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
|
|
480
483
|
captureStackTrace(e, _params?.callee);
|
|
481
484
|
throw e;
|
|
482
485
|
}
|
|
@@ -493,7 +496,7 @@ var _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
|
|
|
493
496
|
}, ctx);
|
|
494
497
|
if (result instanceof Promise) result = await result;
|
|
495
498
|
if (result.issues.length) {
|
|
496
|
-
const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
|
|
499
|
+
const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
|
|
497
500
|
captureStackTrace(e, params?.callee);
|
|
498
501
|
throw e;
|
|
499
502
|
}
|
|
@@ -1589,13 +1592,13 @@ var $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
|
|
|
1589
1592
|
}
|
|
1590
1593
|
return propValues;
|
|
1591
1594
|
});
|
|
1592
|
-
const isObject$
|
|
1595
|
+
const isObject$2 = isObject;
|
|
1593
1596
|
const catchall = def.catchall;
|
|
1594
1597
|
let value;
|
|
1595
1598
|
inst._zod.parse = (payload, ctx) => {
|
|
1596
1599
|
value ?? (value = _normalized.value);
|
|
1597
1600
|
const input = payload.value;
|
|
1598
|
-
if (!isObject$
|
|
1601
|
+
if (!isObject$2(input)) {
|
|
1599
1602
|
payload.issues.push({
|
|
1600
1603
|
expected: "object",
|
|
1601
1604
|
code: "invalid_type",
|
|
@@ -1718,7 +1721,7 @@ var $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
|
|
|
1718
1721
|
return (payload, ctx) => fn(shape, payload, ctx);
|
|
1719
1722
|
};
|
|
1720
1723
|
let fastpass;
|
|
1721
|
-
const isObject$
|
|
1724
|
+
const isObject$1 = isObject;
|
|
1722
1725
|
const jit = !globalConfig.jitless;
|
|
1723
1726
|
const fastEnabled = jit && allowsEval.value;
|
|
1724
1727
|
const catchall = def.catchall;
|
|
@@ -1726,7 +1729,7 @@ var $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
|
|
|
1726
1729
|
inst._zod.parse = (payload, ctx) => {
|
|
1727
1730
|
value ?? (value = _normalized.value);
|
|
1728
1731
|
const input = payload.value;
|
|
1729
|
-
if (!isObject$
|
|
1732
|
+
if (!isObject$1(input)) {
|
|
1730
1733
|
payload.issues.push({
|
|
1731
1734
|
expected: "object",
|
|
1732
1735
|
code: "invalid_type",
|
|
@@ -1945,7 +1948,7 @@ var $ZodOptional = /*@__PURE__*/ $constructor("$ZodOptional", (inst, def) => {
|
|
|
1945
1948
|
inst._zod.optin = "optional";
|
|
1946
1949
|
inst._zod.optout = "optional";
|
|
1947
1950
|
defineLazy(inst._zod, "values", () => {
|
|
1948
|
-
return def.innerType._zod.values ? new Set([...def.innerType._zod.values, void 0]) : void 0;
|
|
1951
|
+
return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, void 0]) : void 0;
|
|
1949
1952
|
});
|
|
1950
1953
|
defineLazy(inst._zod, "pattern", () => {
|
|
1951
1954
|
const pattern = def.innerType._zod.pattern;
|
|
@@ -1979,7 +1982,7 @@ var $ZodNullable = /*@__PURE__*/ $constructor("$ZodNullable", (inst, def) => {
|
|
|
1979
1982
|
return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : void 0;
|
|
1980
1983
|
});
|
|
1981
1984
|
defineLazy(inst._zod, "values", () => {
|
|
1982
|
-
return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : void 0;
|
|
1985
|
+
return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, null]) : void 0;
|
|
1983
1986
|
});
|
|
1984
1987
|
inst._zod.parse = (payload, ctx) => {
|
|
1985
1988
|
if (payload.value === null) return payload;
|
|
@@ -4078,7 +4081,7 @@ function number(params) {
|
|
|
4078
4081
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
4079
4082
|
}
|
|
4080
4083
|
//#endregion
|
|
4081
|
-
//#region ../sdk/
|
|
4084
|
+
//#region ../sdk/src/units.ts
|
|
4082
4085
|
var UNITS_CURRENCY = [
|
|
4083
4086
|
{
|
|
4084
4087
|
value: "UAH",
|
|
@@ -4386,7 +4389,13 @@ function resolveUnits(u) {
|
|
|
4386
4389
|
return typeof u === "string" ? UNITS_MAP[u] : u;
|
|
4387
4390
|
}
|
|
4388
4391
|
//#endregion
|
|
4389
|
-
//#region ../sdk/
|
|
4392
|
+
//#region ../sdk/src/currencies.ts
|
|
4393
|
+
/**
|
|
4394
|
+
* Валюти з вбудованого `Intl` (ISO 4217) — БЕЗ hand-list і без npm-залежності.
|
|
4395
|
+
* Коди — `Intl.supportedValuesOf('currency')`; символи/назви локалізовані через
|
|
4396
|
+
* `Intl.NumberFormat`/`Intl.DisplayNames`.
|
|
4397
|
+
*/
|
|
4398
|
+
/** Усі ISO 4217 коди. Fallback — кілька основних (на випадок старого рантайму). */
|
|
4390
4399
|
var CURRENCY_CODES = (() => {
|
|
4391
4400
|
try {
|
|
4392
4401
|
return Intl.supportedValuesOf("currency");
|
|
@@ -4401,24 +4410,34 @@ var CURRENCY_CODES = (() => {
|
|
|
4401
4410
|
}
|
|
4402
4411
|
})();
|
|
4403
4412
|
var CODE_SET = new Set(CURRENCY_CODES);
|
|
4413
|
+
/** Чи валідний ISO 4217 код. */
|
|
4404
4414
|
var isCurrencyCode = (c) => CODE_SET.has(c);
|
|
4405
4415
|
//#endregion
|
|
4406
|
-
//#region ../sdk/
|
|
4416
|
+
//#region ../sdk/src/fields/validation.ts
|
|
4417
|
+
/** Структуроване повідомлення для zod: JSON {code, params}. Декодує mutate.ts. */
|
|
4407
4418
|
function vmsg(code, params) {
|
|
4408
4419
|
return JSON.stringify(params ? {
|
|
4409
4420
|
code,
|
|
4410
4421
|
params
|
|
4411
4422
|
} : { code });
|
|
4412
4423
|
}
|
|
4424
|
+
/** v4 error-map: повідомлення для відсутнього значення (колишній required_error). */
|
|
4413
4425
|
function reqErr(code = "required") {
|
|
4414
4426
|
return { error: (iss) => iss.input === void 0 ? vmsg(code) : void 0 };
|
|
4415
4427
|
}
|
|
4428
|
+
/** v4 error-map: повідомлення для невалідного типу (колишній invalid_type_error). */
|
|
4416
4429
|
function typeErr(code = "invalid_type") {
|
|
4417
4430
|
return { error: (iss) => iss.code === "invalid_type" ? vmsg(code) : void 0 };
|
|
4418
4431
|
}
|
|
4432
|
+
/** v4 error-map: required + invalid_type разом (колишні required_error + invalid_type_error). */
|
|
4419
4433
|
function reqTypeErr() {
|
|
4420
4434
|
return { error: (iss) => iss.code === "invalid_type" ? iss.input === void 0 ? vmsg("required") : vmsg("invalid_type") : void 0 };
|
|
4421
4435
|
}
|
|
4436
|
+
/** Parse a JSON string, else return the native value (object/array) as-is.
|
|
4437
|
+
* Tolerant input for native form-state AND legacy JSON-string payloads.
|
|
4438
|
+
* A string that fails JSON.parse is returned unchanged — callers detect
|
|
4439
|
+
* parse failure via `typeof result === 'string'` (inners here are object/array,
|
|
4440
|
+
* never a bare string). */
|
|
4422
4441
|
function jsonValue(raw) {
|
|
4423
4442
|
if (typeof raw === "string") try {
|
|
4424
4443
|
return JSON.parse(raw);
|
|
@@ -4427,6 +4446,12 @@ function jsonValue(raw) {
|
|
|
4427
4446
|
}
|
|
4428
4447
|
return raw;
|
|
4429
4448
|
}
|
|
4449
|
+
/**
|
|
4450
|
+
* Фабрика для полів, що зберігають JSON і валідуються вкладеною zod-схемою.
|
|
4451
|
+
* Приймає нативне object/array (native form-state) АБО JSON-рядок (legacy).
|
|
4452
|
+
* Single-parse через jsonValue → inner.safeParse → issue `code`; рядок, що не
|
|
4453
|
+
* розпарсився, лишається рядком → код 'json'.
|
|
4454
|
+
*/
|
|
4430
4455
|
function jsonRefined(inner, code) {
|
|
4431
4456
|
return unknown().superRefine((raw, ctx) => {
|
|
4432
4457
|
const parsed = jsonValue(raw);
|
|
@@ -4455,7 +4480,7 @@ function optionalize(schema, required) {
|
|
|
4455
4480
|
}).pipe(schema));
|
|
4456
4481
|
}
|
|
4457
4482
|
//#endregion
|
|
4458
|
-
//#region ../sdk/
|
|
4483
|
+
//#region ../sdk/src/fields/normalize.ts
|
|
4459
4484
|
function normalizeOpts(rawIn) {
|
|
4460
4485
|
const raw = rawIn;
|
|
4461
4486
|
const r = raw.rules ?? {};
|
|
@@ -4495,7 +4520,19 @@ function normalizeOpts(rawIn) {
|
|
|
4495
4520
|
return Object.fromEntries(Object.entries(result).filter(([, v]) => v !== void 0));
|
|
4496
4521
|
}
|
|
4497
4522
|
//#endregion
|
|
4498
|
-
//#region ../sdk/
|
|
4523
|
+
//#region ../sdk/src/field-presets.ts
|
|
4524
|
+
/**
|
|
4525
|
+
* Пресети полів — тонкі обгортки над базовими примітивами з fields.ts.
|
|
4526
|
+
*
|
|
4527
|
+
* Кожен пресет = один kind (один віджет). Пресети не приймають `format` —
|
|
4528
|
+
* вони самі є семантичними типами. min/max/step — через `config`.
|
|
4529
|
+
*
|
|
4530
|
+
* Пресети додаються до `f` через `composeF`, який гарантує, що жоден пресет
|
|
4531
|
+
* не перебиває примітив.
|
|
4532
|
+
*
|
|
4533
|
+
* Циклічний імпорт з fields.ts безпечний: фабрики/хелпери — hoisted-декларації,
|
|
4534
|
+
* а пресети викликають їх лише в тілі своїх функцій.
|
|
4535
|
+
*/
|
|
4499
4536
|
function email(raw) {
|
|
4500
4537
|
const o = normalizeOpts(raw);
|
|
4501
4538
|
const required = o.required ?? false;
|
|
@@ -4549,6 +4586,14 @@ function password(raw) {
|
|
|
4549
4586
|
zod: optionalize(s, required)
|
|
4550
4587
|
});
|
|
4551
4588
|
}
|
|
4589
|
+
/**
|
|
4590
|
+
* Internal API-token list — keyValue-shaped collection (name + write-only
|
|
4591
|
+
* token + timestamps), custom renderer (`kind:'internalApiToken'`, own
|
|
4592
|
+
* create/revoke UI — same trick as url()/perWeekday()). "Internal" = not a
|
|
4593
|
+
* general-purpose field type for plugin shelves, only for the core account
|
|
4594
|
+
* settings page. `token` is `kind:'password'` so maskSecrets/preserveTree
|
|
4595
|
+
* already mask/preserve it for free.
|
|
4596
|
+
*/
|
|
4552
4597
|
function internalApiToken(o) {
|
|
4553
4598
|
return group({
|
|
4554
4599
|
key: o.key,
|
|
@@ -4564,6 +4609,25 @@ function internalApiToken(o) {
|
|
|
4564
4609
|
]
|
|
4565
4610
|
});
|
|
4566
4611
|
}
|
|
4612
|
+
/**
|
|
4613
|
+
* Internal "connected apps" list — the OAuth clients (claude.ai & co) the user
|
|
4614
|
+
* has authorized for MCP access. Same "internal" caveat and custom-renderer
|
|
4615
|
+
* trick as internalApiToken(): read-only rows + a revoke action, never part of
|
|
4616
|
+
* the parent form's save.
|
|
4617
|
+
*/
|
|
4618
|
+
function internalOauthGrants(o) {
|
|
4619
|
+
return group({
|
|
4620
|
+
key: o.key,
|
|
4621
|
+
label: o.label ?? o.key,
|
|
4622
|
+
multiple: true,
|
|
4623
|
+
view: { kind: "internalOauthGrants" },
|
|
4624
|
+
fields: [
|
|
4625
|
+
string({ key: "clientName" }),
|
|
4626
|
+
string({ key: "createdAt" }),
|
|
4627
|
+
string({ key: "lastUsedAt" })
|
|
4628
|
+
]
|
|
4629
|
+
});
|
|
4630
|
+
}
|
|
4567
4631
|
var SLUG_RE = /^[a-z0-9-]+$/;
|
|
4568
4632
|
function slug(raw) {
|
|
4569
4633
|
const o = normalizeOpts(raw);
|
|
@@ -4641,8 +4705,10 @@ function link(raw) {
|
|
|
4641
4705
|
}, o.multiple ?? false));
|
|
4642
4706
|
}
|
|
4643
4707
|
var TEL_RE = /^\+?[\d\s()-]{4,}$/;
|
|
4708
|
+
/** Loose URL: будь-який scheme:// АБО host-з-крапкою (+опц. порт/шлях). Без пробілів. */
|
|
4644
4709
|
var LINK_RE = /^([a-z][a-z0-9+.-]*:\/\/\S+|[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?)$/i;
|
|
4645
|
-
|
|
4710
|
+
/** CSS named colors (CSS Color Module L4) — для f.colorname. */
|
|
4711
|
+
var CSS_COLOR_NAMES = /* @__PURE__ */ new Set([
|
|
4646
4712
|
"aliceblue",
|
|
4647
4713
|
"antiquewhite",
|
|
4648
4714
|
"aqua",
|
|
@@ -4900,6 +4966,7 @@ function reminder(raw) {
|
|
|
4900
4966
|
}
|
|
4901
4967
|
var _real = real;
|
|
4902
4968
|
var _int = int;
|
|
4969
|
+
/** Відсоток 0..100 — real з rules:{min:0,max:100}. */
|
|
4903
4970
|
function percent(o) {
|
|
4904
4971
|
return _real({
|
|
4905
4972
|
...o,
|
|
@@ -4910,6 +4977,7 @@ function percent(o) {
|
|
|
4910
4977
|
}
|
|
4911
4978
|
});
|
|
4912
4979
|
}
|
|
4980
|
+
/** Рік — int з rules:{min:1900,max:2100}; межі можна перекрити через rules.min/max. */
|
|
4913
4981
|
function year(o) {
|
|
4914
4982
|
return _int({
|
|
4915
4983
|
...o,
|
|
@@ -4989,7 +5057,8 @@ var presets = {
|
|
|
4989
5057
|
weight,
|
|
4990
5058
|
dimensions,
|
|
4991
5059
|
country,
|
|
4992
|
-
internalApiToken
|
|
5060
|
+
internalApiToken,
|
|
5061
|
+
internalOauthGrants
|
|
4993
5062
|
};
|
|
4994
5063
|
//#endregion
|
|
4995
5064
|
//#region ../../node_modules/iso-639-1/src/data.js
|
|
@@ -5101,7 +5170,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
5101
5170
|
},
|
|
5102
5171
|
cs: {
|
|
5103
5172
|
name: "Czech",
|
|
5104
|
-
nativeName: "
|
|
5173
|
+
nativeName: "čeština"
|
|
5105
5174
|
},
|
|
5106
5175
|
cu: {
|
|
5107
5176
|
name: "Old Church Slavonic",
|
|
@@ -5565,7 +5634,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
5565
5634
|
},
|
|
5566
5635
|
sk: {
|
|
5567
5636
|
name: "Slovak",
|
|
5568
|
-
nativeName: "
|
|
5637
|
+
nativeName: "slovenčtina"
|
|
5569
5638
|
},
|
|
5570
5639
|
sl: {
|
|
5571
5640
|
name: "Slovenian",
|
|
@@ -5730,7 +5799,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
5730
5799
|
};
|
|
5731
5800
|
}));
|
|
5732
5801
|
//#endregion
|
|
5733
|
-
//#region ../sdk/
|
|
5802
|
+
//#region ../sdk/src/fields/constants.ts
|
|
5734
5803
|
var import_src = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
5735
5804
|
var LANGUAGES_LIST = require_data();
|
|
5736
5805
|
var LANGUAGES = {};
|
|
@@ -5786,7 +5855,21 @@ import_src.default.getAllCodes().map((code) => ({
|
|
|
5786
5855
|
name: import_src.default.getNativeName(code)
|
|
5787
5856
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5788
5857
|
//#endregion
|
|
5789
|
-
//#region ../sdk/
|
|
5858
|
+
//#region ../sdk/src/fields.ts
|
|
5859
|
+
/**
|
|
5860
|
+
* The field type system — the heart of the platform. A pure, isomorphic module.
|
|
5861
|
+
* One field description → three consumers:
|
|
5862
|
+
* 1. `zod` — validation (identical on server and client)
|
|
5863
|
+
* 2. `column` — the DB column type
|
|
5864
|
+
* 3. `prim`/`kind` — keys for resolving the renderer (see web/render/registry)
|
|
5865
|
+
*/
|
|
5866
|
+
/**
|
|
5867
|
+
* A visual or storage group of fields (one level of nesting — no groups inside groups).
|
|
5868
|
+
* Without `key` → a layout group (not stored). With `key` → storage:
|
|
5869
|
+
* - embedded (key, no multiple): stored as a column or a nested object.
|
|
5870
|
+
* - collection (key + multiple): a child table (array of rows).
|
|
5871
|
+
* A storage group requires a `label` and only `FieldItem` children (with a storage key).
|
|
5872
|
+
*/
|
|
5790
5873
|
function group(o) {
|
|
5791
5874
|
const r = o.rules ?? {};
|
|
5792
5875
|
const v = o.view ?? {};
|
|
@@ -5802,37 +5885,40 @@ function group(o) {
|
|
|
5802
5885
|
required: o.required,
|
|
5803
5886
|
unique: r.unique,
|
|
5804
5887
|
label: o.label,
|
|
5805
|
-
icon:
|
|
5888
|
+
icon: o.icon,
|
|
5806
5889
|
display: v.display ?? "wrap",
|
|
5807
5890
|
kind: v.kind,
|
|
5808
5891
|
fixed: r.fixed,
|
|
5809
5892
|
fields: o.fields
|
|
5810
5893
|
};
|
|
5811
5894
|
}
|
|
5895
|
+
/** Layout group, flow nowrap + horizontal scroll (single line). Sugar over group(). */
|
|
5812
5896
|
function row(o) {
|
|
5813
5897
|
return group({
|
|
5814
5898
|
label: o.label,
|
|
5899
|
+
icon: o.icon,
|
|
5815
5900
|
fields: o.fields,
|
|
5816
|
-
view: {
|
|
5817
|
-
...o.view,
|
|
5818
|
-
display: "scroll"
|
|
5819
|
-
}
|
|
5901
|
+
view: { display: "scroll" }
|
|
5820
5902
|
});
|
|
5821
5903
|
}
|
|
5904
|
+
/** Tabular collection (array of rows → <table>, aligned by columns). Sugar. */
|
|
5822
5905
|
function table(o) {
|
|
5823
5906
|
return group({
|
|
5824
5907
|
key: o.key,
|
|
5825
5908
|
label: o.label,
|
|
5909
|
+
icon: o.icon,
|
|
5826
5910
|
fields: o.fields,
|
|
5827
5911
|
multiple: true,
|
|
5828
5912
|
required: o.required,
|
|
5829
5913
|
rules: o.rules,
|
|
5830
|
-
view: {
|
|
5831
|
-
...o.view,
|
|
5832
|
-
display: "table"
|
|
5833
|
-
}
|
|
5914
|
+
view: { display: "table" }
|
|
5834
5915
|
});
|
|
5835
5916
|
}
|
|
5917
|
+
/**
|
|
5918
|
+
* Layout group with a 2D layout (rows×columns → aligned CSS grid).
|
|
5919
|
+
* Keyless (flat parent fields). Rows are glued together with brk() separators
|
|
5920
|
+
* into a flat fields; the renderer reconstructs the 2D by splitting on brk. Sugar over group().
|
|
5921
|
+
*/
|
|
5836
5922
|
function sheet(o) {
|
|
5837
5923
|
const flat = [];
|
|
5838
5924
|
o.fields.forEach((rowFields, i) => {
|
|
@@ -5841,13 +5927,17 @@ function sheet(o) {
|
|
|
5841
5927
|
});
|
|
5842
5928
|
return group({
|
|
5843
5929
|
label: o.label,
|
|
5930
|
+
icon: o.icon,
|
|
5844
5931
|
fields: flat,
|
|
5845
|
-
view: {
|
|
5846
|
-
...o.view,
|
|
5847
|
-
display: "sheet"
|
|
5848
|
-
}
|
|
5932
|
+
view: { display: "sheet" }
|
|
5849
5933
|
});
|
|
5850
5934
|
}
|
|
5935
|
+
/**
|
|
5936
|
+
* Composite URL — embedded-group sugar. Storage: separate nested columns
|
|
5937
|
+
* (`key__scheme`, `key__host`, `key__port`, …) in the same table. The renderer
|
|
5938
|
+
* is custom (a single link/input, `kind:'url'`) — string↔parts via
|
|
5939
|
+
* parseUrl/buildUrl. Partial/invalid URLs are stored by parts.
|
|
5940
|
+
*/
|
|
5851
5941
|
function url(o) {
|
|
5852
5942
|
return group({
|
|
5853
5943
|
key: o.key,
|
|
@@ -5871,8 +5961,8 @@ function keyed(o) {
|
|
|
5871
5961
|
return {
|
|
5872
5962
|
...(o.container ?? group)({
|
|
5873
5963
|
label: o.label,
|
|
5874
|
-
|
|
5875
|
-
|
|
5964
|
+
icon: o.icon,
|
|
5965
|
+
fields: [o.by, ...o.fields]
|
|
5876
5966
|
}),
|
|
5877
5967
|
key: o.key,
|
|
5878
5968
|
multiple: true,
|
|
@@ -5881,18 +5971,22 @@ function keyed(o) {
|
|
|
5881
5971
|
fixed: o.fixed
|
|
5882
5972
|
};
|
|
5883
5973
|
}
|
|
5974
|
+
/** Horizontal divider. */
|
|
5884
5975
|
function divider() {
|
|
5885
5976
|
return { el: "divider" };
|
|
5886
5977
|
}
|
|
5978
|
+
/** Forced flow-row wrap (subsequent fields start on a new row). No <hr>. */
|
|
5887
5979
|
function brk() {
|
|
5888
5980
|
return { el: "break" };
|
|
5889
5981
|
}
|
|
5982
|
+
/** Reference markdown block by i18n key. */
|
|
5890
5983
|
function info(textKey) {
|
|
5891
5984
|
return {
|
|
5892
5985
|
el: "info",
|
|
5893
5986
|
text: textKey
|
|
5894
5987
|
};
|
|
5895
5988
|
}
|
|
5989
|
+
/** Action button: invokes the handler registered in actionRegistry under the key `value`. */
|
|
5896
5990
|
function button(o) {
|
|
5897
5991
|
return {
|
|
5898
5992
|
el: "button",
|
|
@@ -5902,6 +5996,7 @@ function button(o) {
|
|
|
5902
5996
|
variant: o.variant
|
|
5903
5997
|
};
|
|
5904
5998
|
}
|
|
5999
|
+
/** Wraps FieldMeta into FieldItem/StaticEl depending on opts.key/opts.value. */
|
|
5905
6000
|
function wrapKey(opts, meta) {
|
|
5906
6001
|
let m = meta;
|
|
5907
6002
|
if (opts.noEditControl) m = {
|
|
@@ -5911,6 +6006,34 @@ function wrapKey(opts, meta) {
|
|
|
5911
6006
|
noEditControl: true
|
|
5912
6007
|
}
|
|
5913
6008
|
};
|
|
6009
|
+
if (opts.emphasis) m = {
|
|
6010
|
+
...m,
|
|
6011
|
+
hints: {
|
|
6012
|
+
...m.hints,
|
|
6013
|
+
emphasis: opts.emphasis
|
|
6014
|
+
}
|
|
6015
|
+
};
|
|
6016
|
+
if (opts.noLabel) m = {
|
|
6017
|
+
...m,
|
|
6018
|
+
hints: {
|
|
6019
|
+
...m.hints,
|
|
6020
|
+
noLabel: true
|
|
6021
|
+
}
|
|
6022
|
+
};
|
|
6023
|
+
if (opts.role) m = {
|
|
6024
|
+
...m,
|
|
6025
|
+
hints: {
|
|
6026
|
+
...m.hints,
|
|
6027
|
+
role: opts.role
|
|
6028
|
+
}
|
|
6029
|
+
};
|
|
6030
|
+
if (opts.pinned) m = {
|
|
6031
|
+
...m,
|
|
6032
|
+
hints: {
|
|
6033
|
+
...m.hints,
|
|
6034
|
+
pinned: true
|
|
6035
|
+
}
|
|
6036
|
+
};
|
|
5914
6037
|
if (opts.default !== void 0) m = {
|
|
5915
6038
|
...m,
|
|
5916
6039
|
default: opts.default
|
|
@@ -5940,6 +6063,14 @@ function safeJsonParse(s) {
|
|
|
5940
6063
|
return;
|
|
5941
6064
|
}
|
|
5942
6065
|
}
|
|
6066
|
+
/**
|
|
6067
|
+
* Wraps a field into a JSON array of values (storage: TEXT). The inner zod is `base.zod`.
|
|
6068
|
+
* Modifier order: base → applyMultiple.
|
|
6069
|
+
*
|
|
6070
|
+
* Exception: kind 'image'/'media' handle multiple themselves in the renderer (gallery) —
|
|
6071
|
+
* the MultipleField* wrappers on the web skip them.
|
|
6072
|
+
*/
|
|
6073
|
+
/** Zod "JSON array of inner values" — shared by applyMultiple and bindSelectSourceZod. */
|
|
5943
6074
|
function multipleZod(inner) {
|
|
5944
6075
|
return unknown().superRefine((raw, ctx) => {
|
|
5945
6076
|
let arr;
|
|
@@ -6266,8 +6397,8 @@ function relation(raw) {
|
|
|
6266
6397
|
displayKey: o.displayKey ?? "name"
|
|
6267
6398
|
},
|
|
6268
6399
|
relation: {
|
|
6269
|
-
|
|
6270
|
-
|
|
6400
|
+
library: raw.options.library,
|
|
6401
|
+
shelf: raw.options.shelf
|
|
6271
6402
|
},
|
|
6272
6403
|
...multi && { json: true },
|
|
6273
6404
|
zod: optionalize(s, required)
|
|
@@ -6532,6 +6663,11 @@ function geo(raw) {
|
|
|
6532
6663
|
zod: optionalize(s, required)
|
|
6533
6664
|
});
|
|
6534
6665
|
}
|
|
6666
|
+
/**
|
|
6667
|
+
* 7 values, Mon–Sun. A thin wrapper over keyed({fixed}): a fixed collection,
|
|
6668
|
+
* by=select('weekdays'), one real value per day. Its own kind 'per-weekday'
|
|
6669
|
+
* dispatches a compact renderer. Storage: a grandchild table of {day, value} rows.
|
|
6670
|
+
*/
|
|
6535
6671
|
function perWeekday(o) {
|
|
6536
6672
|
return {
|
|
6537
6673
|
...keyed({
|
|
@@ -6642,15 +6778,44 @@ function period(raw) {
|
|
|
6642
6778
|
zod: optionalize(s, required)
|
|
6643
6779
|
});
|
|
6644
6780
|
}
|
|
6781
|
+
/** Keys a file entry may carry. Everything else is rejected — see fileEntryIssue. */
|
|
6782
|
+
var FILE_KEYS = /* @__PURE__ */ new Set([
|
|
6783
|
+
"name",
|
|
6784
|
+
"mime",
|
|
6785
|
+
"size"
|
|
6786
|
+
]);
|
|
6787
|
+
/** Keys that mean "the author pasted a remote address" — the one mistake worth naming. */
|
|
6788
|
+
var FILE_URL_KEYS = [
|
|
6789
|
+
"url",
|
|
6790
|
+
"src",
|
|
6791
|
+
"href",
|
|
6792
|
+
"link"
|
|
6793
|
+
];
|
|
6794
|
+
/**
|
|
6795
|
+
* A file entry points at a file already uploaded to the server: `{ name }`, where
|
|
6796
|
+
* name is the bare filename returned by POST /api/upload. Anything else — a remote
|
|
6797
|
+
* URL, an extra key, a path — is refused here, so a record can never hold a
|
|
6798
|
+
* reference the server cannot serve. `mime`/`size` are accepted (legacy payloads
|
|
6799
|
+
* and the web uploader send them) but the server overwrites them from disk.
|
|
6800
|
+
* Returns a vmsg code, or null when the entry is well-formed.
|
|
6801
|
+
*/
|
|
6802
|
+
function fileEntryIssue(it) {
|
|
6803
|
+
if (typeof it !== "object" || it === null || Array.isArray(it)) return "file_structure";
|
|
6804
|
+
const rec = it;
|
|
6805
|
+
if (FILE_URL_KEYS.some((k) => k in rec)) return "file_remote_url";
|
|
6806
|
+
const name = rec["name"];
|
|
6807
|
+
if (typeof name !== "string" || name === "") return "file_structure";
|
|
6808
|
+
if (name.includes("://") || name.startsWith("//")) return "file_remote_url";
|
|
6809
|
+
if (/[\\/]/.test(name) || name.includes("..")) return "file_name";
|
|
6810
|
+
for (const k of Object.keys(rec)) if (!FILE_KEYS.has(k)) return "file_unknown_key";
|
|
6811
|
+
if (rec["mime"] !== void 0 && typeof rec["mime"] !== "string") return "file_structure";
|
|
6812
|
+
if (rec["size"] !== void 0 && typeof rec["size"] !== "number") return "file_structure";
|
|
6813
|
+
return null;
|
|
6814
|
+
}
|
|
6645
6815
|
function makeFile(kind, raw) {
|
|
6646
6816
|
const o = normalizeOpts(raw);
|
|
6647
6817
|
const required = o.required ?? false;
|
|
6648
6818
|
const multiple = o.multiple ?? false;
|
|
6649
|
-
const rowSchema = object({
|
|
6650
|
-
name: string$1().min(1),
|
|
6651
|
-
mime: string$1().optional(),
|
|
6652
|
-
size: number$1().optional()
|
|
6653
|
-
});
|
|
6654
6819
|
const s = unknown().superRefine((raw, ctx) => {
|
|
6655
6820
|
if (typeof raw === "string") try {
|
|
6656
6821
|
JSON.parse(raw);
|
|
@@ -6663,12 +6828,15 @@ function makeFile(kind, raw) {
|
|
|
6663
6828
|
}
|
|
6664
6829
|
const parsed = jsonValue(raw);
|
|
6665
6830
|
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
6666
|
-
for (const it of items)
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6831
|
+
for (const it of items) {
|
|
6832
|
+
const code = fileEntryIssue(it);
|
|
6833
|
+
if (code) {
|
|
6834
|
+
ctx.addIssue({
|
|
6835
|
+
code: ZodIssueCode.custom,
|
|
6836
|
+
message: vmsg(code)
|
|
6837
|
+
});
|
|
6838
|
+
return;
|
|
6839
|
+
}
|
|
6672
6840
|
}
|
|
6673
6841
|
});
|
|
6674
6842
|
return wrapKey(o, {
|
|
@@ -6824,6 +6992,11 @@ function embed(raw) {
|
|
|
6824
6992
|
zod: optionalize(s, required)
|
|
6825
6993
|
});
|
|
6826
6994
|
}
|
|
6995
|
+
/**
|
|
6996
|
+
* Base primitives (storage-aligned) + structural types. Presets (thin wrappers
|
|
6997
|
+
* over these primitives) are added below from field-presets.ts and CANNOT override
|
|
6998
|
+
* any key from here (guard in composeF).
|
|
6999
|
+
*/
|
|
6827
7000
|
var PRIMITIVES = {
|
|
6828
7001
|
string,
|
|
6829
7002
|
text,
|
|
@@ -6873,6 +7046,7 @@ var PRIMITIVES = {
|
|
|
6873
7046
|
info,
|
|
6874
7047
|
button
|
|
6875
7048
|
};
|
|
7049
|
+
/** Assembles `f`, guaranteeing no preset shadows a primitive. */
|
|
6876
7050
|
function composeF(presets) {
|
|
6877
7051
|
for (const k of Object.keys(presets)) if (k in PRIMITIVES) throw new Error(`[f] preset '${k}' overrides a primitive`);
|
|
6878
7052
|
return {
|
|
@@ -6930,6 +7104,25 @@ var src_default = definePlugin({
|
|
|
6930
7104
|
field.tel({
|
|
6931
7105
|
key: "phone",
|
|
6932
7106
|
label: "telegram.settings.phone"
|
|
7107
|
+
}),
|
|
7108
|
+
field.select({
|
|
7109
|
+
key: "reasoning_display",
|
|
7110
|
+
label: "telegram.settings.reasoning_display",
|
|
7111
|
+
default: "collapsible",
|
|
7112
|
+
options: [
|
|
7113
|
+
{
|
|
7114
|
+
value: "off",
|
|
7115
|
+
title: "telegram.settings.reasoning_display_off"
|
|
7116
|
+
},
|
|
7117
|
+
{
|
|
7118
|
+
value: "collapsible",
|
|
7119
|
+
title: "telegram.settings.reasoning_display_collapsible"
|
|
7120
|
+
},
|
|
7121
|
+
{
|
|
7122
|
+
value: "separate",
|
|
7123
|
+
title: "telegram.settings.reasoning_display_separate"
|
|
7124
|
+
}
|
|
7125
|
+
]
|
|
6933
7126
|
})
|
|
6934
7127
|
]
|
|
6935
7128
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/plugin-telegram",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"test": "node --import tsx --test \"src/runtime/*.test.ts\""
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@coffer-org/plugin-orchestrator": "^
|
|
29
|
-
"@coffer-org/sdk": "^
|
|
30
|
-
"@coffer-org/server": "^
|
|
28
|
+
"@coffer-org/plugin-orchestrator": "^2.0.0",
|
|
29
|
+
"@coffer-org/sdk": "^2.0.0",
|
|
30
|
+
"@coffer-org/server": "^2.0.1",
|
|
31
31
|
"input": "^1.0.1",
|
|
32
|
-
"
|
|
32
|
+
"teleproto": "^1.228.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {},
|
|
35
35
|
"coffer": {
|