mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 16:09:40 +02:00
feat(api): port remaining 12 modules to unified API server
Complete consolidation of all 15 app servers into one Hono/Bun process. Modules added: chat, context, picture, storage, todo, planta, nutriphi, guides, moodlit, news, traces, presi Total: 15 modules, one server, one port (3050), ~2400 LOC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eb97378438
commit
9363063cd7
14 changed files with 2014 additions and 0 deletions
89
apps/api/src/modules/planta/routes.ts
Normal file
89
apps/api/src/modules/planta/routes.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* Planta module — Photo upload + AI plant analysis
|
||||
* Ported from apps/planta/apps/server
|
||||
*
|
||||
* CRUD for plants, photos, watering handled by mana-sync.
|
||||
* This module handles S3 uploads and Gemini Vision analysis.
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const LLM_URL = process.env.MANA_LLM_URL || 'http://localhost:3025';
|
||||
|
||||
const routes = new Hono();
|
||||
|
||||
// ─── Photo Upload (server-only: S3 storage) ─────────────────
|
||||
|
||||
routes.post('/photos/upload', async (c) => {
|
||||
const userId = c.get('userId');
|
||||
const formData = await c.req.formData();
|
||||
const file = formData.get('file') as File | null;
|
||||
const plantId = formData.get('plantId') as string | null;
|
||||
|
||||
if (!file) return c.json({ error: 'No file provided' }, 400);
|
||||
if (file.size > 10 * 1024 * 1024) return c.json({ error: 'File too large (max 10MB)' }, 400);
|
||||
|
||||
try {
|
||||
const { createPlantaStorage, generateUserFileKey, getContentType } = await import(
|
||||
'@manacore/shared-storage'
|
||||
);
|
||||
const storage = createPlantaStorage();
|
||||
const key = generateUserFileKey(userId, file.name);
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
const result = await storage.upload(key, buffer, {
|
||||
contentType: getContentType(file.name),
|
||||
public: true,
|
||||
});
|
||||
|
||||
return c.json({ storagePath: key, publicUrl: result.url, plantId }, 201);
|
||||
} catch (err) {
|
||||
console.error('Upload failed:', err);
|
||||
return c.json({ error: 'Upload failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── AI Analysis (server-only: Gemini Vision) ───────────────
|
||||
|
||||
routes.post('/analysis/identify', async (c) => {
|
||||
const { photoUrl } = await c.req.json();
|
||||
if (!photoUrl) return c.json({ error: 'photoUrl required' }, 400);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${LLM_URL}/api/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'Du bist ein Pflanzenexperte. Analysiere das Bild und gib JSON zurück: {scientificName, commonNames[], confidence, healthAssessment, wateringAdvice, lightAdvice, generalTips[]}',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'Analysiere diese Pflanze.' },
|
||||
{ type: 'image_url', image_url: { url: photoUrl } },
|
||||
],
|
||||
},
|
||||
],
|
||||
model: process.env.VISION_MODEL || 'gemini-2.0-flash',
|
||||
response_format: { type: 'json_object' },
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) return c.json({ error: 'AI analysis failed' }, 502);
|
||||
|
||||
const data = await res.json();
|
||||
const content = data.choices?.[0]?.message?.content;
|
||||
const analysis = typeof content === 'string' ? JSON.parse(content) : content;
|
||||
|
||||
return c.json(analysis);
|
||||
} catch (err) {
|
||||
console.error('Analysis failed:', err);
|
||||
return c.json({ error: 'Analysis failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
export { routes as plantaRoutes };
|
||||
Loading…
Add table
Add a link
Reference in a new issue