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;
|
const workerCfg = photoStatus.data.worker && photoStatus.data.worker.config;
|
||||||
ok('worker.config содержит лимит мягких повторов', Boolean(workerCfg && workerCfg.soft_max_retries > 0), workerCfg);
|
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, {
|
ok('worker.config.ai_url соответствует наличию фото-ИИ', Boolean(workerCfg) && workerCfg.ai_url === photoStatus.data.ai_url, {
|
||||||
worker_ai_url: workerCfg && workerCfg.ai_url,
|
worker_ai_url: workerCfg && workerCfg.ai_url,
|
||||||
ai_url: photoStatus.data.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) => {
|
app.get('/api/ai/status', requireAdmin, async (_, res) => {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT ai_status, count(*)::int AS n FROM entries WHERE deleted_at IS NULL GROUP BY ai_status`
|
`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 };
|
const counts = { pending: 0, processing: 0, done: 0, error: 0 };
|
||||||
rows.forEach(r => { counts[r.status] = r.n; });
|
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(
|
pool.query(
|
||||||
`SELECT j.id, j.entry_id, j.action, j.created_at, e.student_name, g.name AS group_name
|
`SELECT j.id, j.entry_id, j.action, j.created_at, e.student_name, g.name AS group_name
|
||||||
FROM photo_jobs j
|
FROM photo_jobs j
|
||||||
@@ -5820,13 +5842,14 @@ app.get('/api/photo-jobs/status', requireAdmin, async (req, res) => {
|
|||||||
WHERE j.status = 'error'
|
WHERE j.status = 'error'
|
||||||
ORDER BY j.finished_at DESC NULLS LAST, j.id DESC LIMIT 20`
|
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 enabled = String(await getSetting('photo_worker_enabled', 'true')) !== 'false';
|
||||||
const service = await aiHealthCheck();
|
|
||||||
res.json({
|
res.json({
|
||||||
enabled,
|
enabled,
|
||||||
ai_configured: !!PHOTO_AI_URL,
|
ai_configured: !!PHOTO_AI_URL,
|
||||||
ai_url: PHOTO_AI_URL,
|
ai_url: PHOTO_AI_URL,
|
||||||
|
service,
|
||||||
worker: photoWorker ? photoWorker.getInfo() : null,
|
worker: photoWorker ? photoWorker.getInfo() : null,
|
||||||
counts,
|
counts,
|
||||||
pending: pending.rows,
|
pending: pending.rows,
|
||||||
|
|||||||
Reference in New Issue
Block a user