fix(photo-ai): отдавать health фото-сервиса в статусе заданий
В GET /api/photo-jobs/status вызывался aiHealthCheck() — это health
текстового ИИ, — а результат в ответ не попадал: поле service отсутствовало,
и оператор не видел состояние photo-ai.
Добавлена photoAiHealth(): GET ${PHOTO_AI_URL}/health с таймаутом 5 с, без
исключений; пустой PHOTO_AI_URL -> {configured:false, reachable:false}, обрыв
или таймаут -> {configured:true, reachable:false, error}. Вызов уходит в тот
же Promise.all, что и запросы к БД, поэтому статус не получает лишние 5 с.
Контракт: {configured, reachable, latency_ms, error} + passthrough полей
photo-ai; первые четыре совпадают с контрактом текстового ИИ, который уже
читает public/js/worker.js.
Контракт зафиксирован в api.smoketest.js двумя проверками.
This commit is contained in:
@@ -188,6 +188,17 @@ async function main() {
|
||||
});
|
||||
const workerCfg = photoStatus.data.worker && photoStatus.data.worker.config;
|
||||
ok('worker.config содержит лимит мягких повторов', Boolean(workerCfg && workerCfg.soft_max_retries > 0), workerCfg);
|
||||
// service обязан быть health фото-сервиса, а не текстового ИИ: configured совпадает
|
||||
// с ai_configured, reachable — булево, а при выключенном photo-ai сервис недоступен.
|
||||
const photoSvc = photoStatus.data.service;
|
||||
ok('photo-jobs/status -> service от фото-сервиса', Boolean(photoSvc) && photoSvc.configured === photoStatus.data.ai_configured && typeof photoSvc.reachable === 'boolean', {
|
||||
service: photoSvc,
|
||||
ai_configured: photoStatus.data.ai_configured,
|
||||
});
|
||||
ok('service: без photo-ai reachable=false', photoStatus.data.ai_configured === false ? photoSvc.reachable === false : typeof photoSvc.latency_ms === 'number', {
|
||||
ai_configured: photoStatus.data.ai_configured,
|
||||
reachable: photoSvc.reachable,
|
||||
});
|
||||
ok('worker.config.ai_url соответствует наличию фото-ИИ', Boolean(workerCfg) && workerCfg.ai_url === photoStatus.data.ai_url, {
|
||||
worker_ai_url: workerCfg && workerCfg.ai_url,
|
||||
ai_url: photoStatus.data.ai_url,
|
||||
|
||||
@@ -5655,6 +5655,28 @@ async function aiHealthCheck() {
|
||||
}
|
||||
}
|
||||
|
||||
async function photoAiHealth() {
|
||||
if (!PHOTO_AI_URL) return { configured: false, reachable: false, latency_ms: 0, error: 'PHOTO_AI_URL не настроен' };
|
||||
const startedAt = Date.now();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 5000);
|
||||
try {
|
||||
const r = await fetch(`${PHOTO_AI_URL}/health`, { signal: controller.signal });
|
||||
const latency_ms = Date.now() - startedAt;
|
||||
if (!r.ok) return { configured: true, reachable: false, latency_ms, error: `HTTP ${r.status}` };
|
||||
let data = null;
|
||||
try { data = await r.json(); } catch (e) { data = null; }
|
||||
const payload = data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
||||
return { ...payload, configured: true, reachable: true, latency_ms, error: null };
|
||||
} catch (e) {
|
||||
const latency_ms = Date.now() - startedAt;
|
||||
const error = e && e.name === 'AbortError' ? 'timeout' : (e && e.message ? e.message : 'unreachable');
|
||||
return { configured: true, reachable: false, latency_ms, error };
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/api/ai/status', requireAdmin, async (_, res) => {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT ai_status, count(*)::int AS n FROM entries WHERE deleted_at IS NULL GROUP BY ai_status`
|
||||
@@ -5787,7 +5809,7 @@ app.get('/api/photo-jobs/status', requireAdmin, async (req, res) => {
|
||||
);
|
||||
const counts = { pending: 0, processing: 0, done: 0, error: 0 };
|
||||
rows.forEach(r => { counts[r.status] = r.n; });
|
||||
const [pending, recentTotal, recent, errors] = await Promise.all([
|
||||
const [pending, recentTotal, recent, errors, service] = await Promise.all([
|
||||
pool.query(
|
||||
`SELECT j.id, j.entry_id, j.action, j.created_at, e.student_name, g.name AS group_name
|
||||
FROM photo_jobs j
|
||||
@@ -5820,13 +5842,14 @@ app.get('/api/photo-jobs/status', requireAdmin, async (req, res) => {
|
||||
WHERE j.status = 'error'
|
||||
ORDER BY j.finished_at DESC NULLS LAST, j.id DESC LIMIT 20`
|
||||
),
|
||||
photoAiHealth(),
|
||||
]);
|
||||
const enabled = String(await getSetting('photo_worker_enabled', 'true')) !== 'false';
|
||||
const service = await aiHealthCheck();
|
||||
res.json({
|
||||
enabled,
|
||||
ai_configured: !!PHOTO_AI_URL,
|
||||
ai_url: PHOTO_AI_URL,
|
||||
service,
|
||||
worker: photoWorker ? photoWorker.getInfo() : null,
|
||||
counts,
|
||||
pending: pending.rows,
|
||||
|
||||
Reference in New Issue
Block a user