Импорт только текстовых сообщений, HISTORY_LIMIT по умолчанию 5000
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,4 +18,4 @@ OPENROUTER_API_KEY=
|
|||||||
|
|
||||||
# Путь к SQLite-базе и сколько сообщений на чат хранить
|
# Путь к SQLite-базе и сколько сообщений на чат хранить
|
||||||
DB_PATH=data/bot.db
|
DB_PATH=data/bot.db
|
||||||
HISTORY_LIMIT=2000
|
HISTORY_LIMIT=5000
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ Telegram не отдаёт ботам сообщения, написанные
|
|||||||
2. Пришли боту `result.json` с подписью `/import` — прямо в этот чат или боту в личку (в личке бот сам поймёт, какой это чат). Можно и ответить `/import` на уже отправленный файл.
|
2. Пришли боту `result.json` с подписью `/import` — прямо в этот чат или боту в личку (в личке бот сам поймёт, какой это чат). Можно и ответить `/import` на уже отправленный файл.
|
||||||
3. Бот ответит, сколько сообщений и участников добавил.
|
3. Бот ответит, сколько сообщений и участников добавил.
|
||||||
|
|
||||||
|
Загружаются только сообщения с текстом: стикеры, голосовые, опросы и медиа без подписи пропускаются. HTML-экспорт не подходит — в нём нет ID авторов.
|
||||||
|
|
||||||
- Импорт идемпотентен: повторная загрузка ничего не дублирует, уже сохранённые ботом сообщения и ники не перезаписываются.
|
- Импорт идемпотентен: повторная загрузка ничего не дублирует, уже сохранённые ботом сообщения и ники не перезаписываются.
|
||||||
- Хранится не больше `HISTORY_LIMIT` последних сообщений на чат — для большой истории подними лимит.
|
- Хранится не больше `HISTORY_LIMIT` последних сообщений на чат — для большой истории подними лимит.
|
||||||
- Ников в экспорте нет, только имена: участники из экспорта находятся по имени, ник подтянется, когда человек напишет при боте.
|
- Ников в экспорте нет, только имена: участники из экспорта находятся по имени, ник подтянется, когда человек напишет при боте.
|
||||||
@@ -61,7 +63,7 @@ pnpm dev
|
|||||||
|
|
||||||
\* нужен ключ того провайдера, чья модель выбрана; бот проверяет это при старте.
|
\* нужен ключ того провайдера, чья модель выбрана; бот проверяет это при старте.
|
||||||
| `DB_PATH` | нет | путь к SQLite, по умолчанию `data/bot.db` (в Docker — `/app/data/bot.db`) |
|
| `DB_PATH` | нет | путь к SQLite, по умолчанию `data/bot.db` (в Docker — `/app/data/bot.db`) |
|
||||||
| `HISTORY_LIMIT` | нет | сколько сообщений на чат хранить, по умолчанию 2000 |
|
| `HISTORY_LIMIT` | нет | сколько сообщений на чат хранить, по умолчанию 5000 |
|
||||||
|
|
||||||
## Деплой (Dokploy)
|
## Деплой (Dokploy)
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const parseModel = (): ModelChoice => {
|
|||||||
export const DB_PATH = envValue("DB_PATH") ?? "data/bot.db";
|
export const DB_PATH = envValue("DB_PATH") ?? "data/bot.db";
|
||||||
|
|
||||||
/** Сколько последних сообщений каждого чата хранить в базе; нужен боту и скрипту импорта. */
|
/** Сколько последних сообщений каждого чата хранить в базе; нужен боту и скрипту импорта. */
|
||||||
export const HISTORY_LIMIT = positiveInt("HISTORY_LIMIT", 2000);
|
export const HISTORY_LIMIT = positiveInt("HISTORY_LIMIT", 5000);
|
||||||
|
|
||||||
export const loadBotConfig = () => ({
|
export const loadBotConfig = () => ({
|
||||||
botToken: required("BOT_TOKEN"),
|
botToken: required("BOT_TOKEN"),
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ const exportMessageSchema = z.object({
|
|||||||
file: z.string().optional(),
|
file: z.string().optional(),
|
||||||
file_name: z.string().optional(),
|
file_name: z.string().optional(),
|
||||||
media_type: z.string().optional(),
|
media_type: z.string().optional(),
|
||||||
sticker_emoji: z.string().optional(),
|
|
||||||
poll: z.object({ question: z.string() }).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Экспорт одного чата из Telegram Desktop: «Экспорт истории чата» → формат JSON. */
|
/** Экспорт одного чата из Telegram Desktop: «Экспорт истории чата» → формат JSON. */
|
||||||
@@ -66,19 +64,19 @@ const plainText = (text: ExportMessage["text"]): string => {
|
|||||||
return text.map((part) => (typeof part === "string" ? part : part.text)).join("");
|
return text.map((part) => (typeof part === "string" ? part : part.text)).join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Та же разметка медиа, что у живых сообщений (`MEDIA_LABELS`), чтобы история читалась одинаково. */
|
/**
|
||||||
|
* Импортируем только сообщения с текстом: стикеры, голосовые, опросы и медиа без подписи пропускаем.
|
||||||
|
* У медиа с подписью оставляем пометку из `MEDIA_LABELS` — как у живых сообщений.
|
||||||
|
*/
|
||||||
const exportMessageText = (msg: ExportMessage): string | undefined => {
|
const exportMessageText = (msg: ExportMessage): string | undefined => {
|
||||||
const text = plainText(msg.text).trim();
|
const text = plainText(msg.text).trim();
|
||||||
|
|
||||||
|
if (!text) return undefined;
|
||||||
if (msg.photo) return withLabel(MEDIA_LABELS.photo, text);
|
if (msg.photo) return withLabel(MEDIA_LABELS.photo, text);
|
||||||
if (msg.media_type === "sticker") return withLabel(MEDIA_LABELS.sticker, msg.sticker_emoji ?? "");
|
|
||||||
if (msg.media_type === "voice_message") return MEDIA_LABELS.voice;
|
|
||||||
if (msg.media_type === "video_message") return MEDIA_LABELS.videoNote;
|
|
||||||
if (msg.media_type === "video_file" || msg.media_type === "animation") return withLabel(MEDIA_LABELS.video, text);
|
if (msg.media_type === "video_file" || msg.media_type === "animation") return withLabel(MEDIA_LABELS.video, text);
|
||||||
if (msg.file) return withLabel(MEDIA_LABELS.file(msg.file_name), text);
|
if (msg.file) return withLabel(MEDIA_LABELS.file(msg.file_name), text);
|
||||||
if (msg.poll) return withLabel(MEDIA_LABELS.poll, msg.poll.question);
|
|
||||||
|
|
||||||
return text || undefined;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Новые экспорты кладут `date_unixtime`; в старых только локальное время без пояса — берём как есть. */
|
/** Новые экспорты кладут `date_unixtime`; в старых только локальное время без пояса — берём как есть. */
|
||||||
|
|||||||
Reference in New Issue
Block a user