29788a8f9d
Adds character-driven AI-generated notes pipeline parallel to articles:
- migration: zero_notes table (channel-scoped, status flow draft→approved→published)
- services/zeroPrompt.js — 12 theme buckets, few-shots, anti-repeat by bucket,
sha256 theme_hash for dedup
- services/zeroNotes.js — generateDraft/approve/skip/edit/autoApproveOldDrafts
- services/zeroNotesRunner.js — TG publication with multipart pose images,
FOR UPDATE SKIP LOCKED claim, retry up to 3 attempts
- workers/zeroNotesScheduler.js — 13:00 MSK generate, 07:00 MSK auto-approve,
publish runner every 60s
- routes/zero.js (public, no secret) — character bio + published notes for site
- routes/zeroAdmin.js — full CRUD + manual generate button + regenerate
Settings (app_settings):
ZERO_NOTES_CHANNEL_IDS — csv int, channels to post for (required to enable)
ZERO_NOTES_MODEL — defaults to AI_MODEL_POST
ZERO_SITE_URL_BASE — optional, adds 'Open on site' inline button
64 lines
2.1 KiB
JavaScript
64 lines
2.1 KiB
JavaScript
/**
|
|
* routes/zero.js — публичные роуты для сайта zeropost.ru/zero
|
|
* Монтируется на /api/zero
|
|
*
|
|
* Без аутентификации — отдаём только published.
|
|
*/
|
|
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const zeroNotes = require('../services/zeroNotes');
|
|
const zPrompt = require('../services/zeroPrompt');
|
|
|
|
// GET /api/zero/notes?limit=20&offset=0&channel_id=1
|
|
router.get('/notes', async (req, res) => {
|
|
try {
|
|
const limit = Math.min(parseInt(req.query.limit) || 20, 50);
|
|
const offset = parseInt(req.query.offset) || 0;
|
|
const channelId = req.query.channel_id ? parseInt(req.query.channel_id) : null;
|
|
const items = await zeroNotes.listPublished({ channelId, limit, offset });
|
|
res.json({ ok: true, items, limit, offset });
|
|
} catch (err) {
|
|
console.error('[GET /api/zero/notes]', err.message);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// GET /api/zero/notes/:id — одна published заметка
|
|
router.get('/notes/:id', async (req, res) => {
|
|
try {
|
|
const note = await zeroNotes.getById(parseInt(req.params.id));
|
|
if (!note || note.status !== 'published') {
|
|
return res.status(404).json({ error: 'not found' });
|
|
}
|
|
// Отдаём только публичные поля
|
|
res.json({
|
|
ok: true,
|
|
note: {
|
|
id: note.id,
|
|
channel_id: note.channel_id,
|
|
content: note.content,
|
|
theme: note.theme,
|
|
theme_bucket: note.theme_bucket,
|
|
pose: note.pose,
|
|
image_url: note.image_url,
|
|
published_at: note.published_at,
|
|
channel_message_id: note.channel_message_id,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// GET /api/zero/character — bio и описание для блока "Кто такой Зеро"
|
|
router.get('/character', async (_req, res) => {
|
|
res.json({
|
|
ok: true,
|
|
character: zPrompt.CHARACTER,
|
|
buckets: zPrompt.THEME_BUCKETS.map(b => ({ key: b.key, label: b.label })),
|
|
});
|
|
});
|
|
|
|
module.exports = router;
|