import path from 'node:path'; import { createLearnAssistantFamilyRegistry } from './learn-assistant-family.mjs'; import { createLearnAssistantService } from './learn-assistant-service.mjs'; function handleError(res, error) { const status = error?.statusCode ?? 500; const message = error instanceof Error ? error.message : '请求失败'; if (status >= 500) console.error('[LearnAssistant]', message); return res.status(status).json({ error: message }); } async function runHandler(res, work) { try { const body = await work; return res.json(body); } catch (error) { return handleError(res, error); } } function readLegacyPassword(req) { return req.get('x-parent-pw') || req.get('x-admin-pw') || ''; } function currentUser(req) { return req.currentUser ?? null; } export function createLearnAssistantRuntime(options = {}) { const dataRoot = options.dataRoot ?? path.join(options.rootDir ?? process.cwd(), 'data', 'learn-assistant'); const service = options.service ?? createLearnAssistantService({ dataRoot }); const familyRegistry = options.familyRegistry ?? createLearnAssistantFamilyRegistry({ dataRoot, seedFamilyDefaults: service.seedFamilyDefaults }); return { dataRoot, familyRegistry, service }; } export function attachLearnAssistantPublicRoutes(api, options = {}) { const { familyRegistry, service } = createLearnAssistantRuntime(options); const prefix = options.routePrefix ?? '/learn'; async function withSlug(req, res, handler) { try { const family = await familyRegistry.getFamilyBySlug(req.params.slug); return runHandler(res, handler(family)); } catch (error) { return handleError(res, error); } } api.get(`${prefix}/public/:slug/children`, (req, res) => withSlug(req, res, (family) => service.listChildren(family.id))); api.get(`${prefix}/public/:slug/today`, (req, res) => withSlug(req, res, (family) => service.getToday(family.id, req.query.child, req.query.date))); api.get(`${prefix}/public/:slug/day`, (req, res) => withSlug(req, res, (family) => service.getDay(family.id, req.query.child, req.query.date))); api.post(`${prefix}/public/:slug/checkin`, (req, res) => withSlug(req, res, (family) => service.checkin(family.id, req.body?.id, req.body))); api.get(`${prefix}/public/:slug/calendar`, (req, res) => withSlug(req, res, (family) => service.getCalendar(family.id, req.query.child, req.query.month))); api.get(`${prefix}/public/:slug/growth`, (req, res) => withSlug(req, res, (family) => service.getGrowth(family.id, req.query.child))); api.get(`${prefix}/public/:slug/history`, (req, res) => withSlug(req, res, (family) => service.getHistory(family.id, req.query.child))); // legacy slug routes (tang 等旧链接) api.get(`${prefix}/:slug/children`, (req, res) => withSlug(req, res, (family) => service.listChildren(family.id))); api.get(`${prefix}/:slug/today`, (req, res) => withSlug(req, res, (family) => service.getToday(family.id, req.query.child, req.query.date))); api.get(`${prefix}/:slug/day`, (req, res) => withSlug(req, res, (family) => service.getDay(family.id, req.query.child, req.query.date))); api.post(`${prefix}/:slug/checkin`, (req, res) => withSlug(req, res, (family) => service.checkin(family.id, req.body?.id, req.body))); api.get(`${prefix}/:slug/calendar`, (req, res) => withSlug(req, res, (family) => service.getCalendar(family.id, req.query.child, req.query.month))); api.get(`${prefix}/:slug/growth`, (req, res) => withSlug(req, res, (family) => service.getGrowth(family.id, req.query.child))); api.get(`${prefix}/:slug/history`, (req, res) => withSlug(req, res, (family) => service.getHistory(family.id, req.query.child))); // legacy password parent API (tang 旧家长端) async function withLegacyParent(req, res, handler) { try { const family = await familyRegistry.getFamilyBySlug(req.params.slug); await service.assertLegacyPassword(family.id, readLegacyPassword(req)); return runHandler(res, handler(family)); } catch (error) { return handleError(res, error); } } api.get(`${prefix}/:slug/parent/stats`, (req, res) => withLegacyParent(req, res, (family) => service.parentStats(family.id))); api.get(`${prefix}/:slug/parent/templates`, (req, res) => withLegacyParent(req, res, (family) => service.parentListTemplates(family.id))); api.post(`${prefix}/:slug/parent/templates`, (req, res) => withLegacyParent(req, res, (family) => service.parentSaveTemplate(family.id, req.body))); api.put(`${prefix}/:slug/parent/templates/:id`, (req, res) => withLegacyParent(req, res, (family) => service.parentSaveTemplate(family.id, req.body, +req.params.id))); api.delete(`${prefix}/:slug/parent/templates/:id`, (req, res) => withLegacyParent(req, res, (family) => service.parentDeleteTemplate(family.id, +req.params.id))); api.get(`${prefix}/:slug/parent/categories`, (req, res) => withLegacyParent(req, res, (family) => service.parentListCategories(family.id))); api.post(`${prefix}/:slug/parent/categories`, (req, res) => withLegacyParent(req, res, (family) => service.parentAddCategory(family.id, req.body))); api.delete(`${prefix}/:slug/parent/categories/:id`, (req, res) => withLegacyParent(req, res, (family) => service.parentDeleteCategory(family.id, +req.params.id))); api.get(`${prefix}/:slug/parent/children`, (req, res) => withLegacyParent(req, res, (family) => service.parentListChildren(family.id))); api.post(`${prefix}/:slug/parent/children`, (req, res) => withLegacyParent(req, res, (family) => service.parentAddChild(family.id, req.body))); api.delete(`${prefix}/:slug/parent/children/:id`, (req, res) => withLegacyParent(req, res, (family) => service.parentDeleteChild(family.id, +req.params.id))); api.post(`${prefix}/:slug/parent/password`, (req, res) => withLegacyParent(req, res, (family) => service.parentChangePassword(family.id, req.body))); return { familyRegistry, service }; } export function attachLearnAssistantParentRoutes(api, options = {}) { const { familyRegistry, service } = createLearnAssistantRuntime(options); const prefix = options.routePrefix ?? '/learn'; function requireUser(req, res) { const user = currentUser(req); if (!user?.id) { res.status(401).json({ error: '请先登录 Memind' }); return null; } return user; } async function withFamily(req, res, handler) { const user = requireUser(req, res); if (!user) return null; try { const family = await familyRegistry.requireFamilyForUser(user.id, { displayName: user.displayName ?? user.display_name ?? user.username, }); return runHandler(res, handler(family, user)); } catch (error) { return handleError(res, error); } } api.get(`${prefix}/me/family`, async (req, res) => { const user = requireUser(req, res); if (!user) return; try { const { family, created } = await familyRegistry.getFamilyForUser(user.id, { displayName: user.displayName ?? user.display_name ?? user.username, }); const urls = familyRegistry.familyUrls(family); return res.json({ family: { id: family.id, slug: family.slug, name: family.name, members: family.members, }, role: family.members.find((m) => m.userId === user.id)?.role ?? 'parent', urls, created, }); } catch (error) { return handleError(res, error); } }); api.post(`${prefix}/me/invite`, (req, res) => withFamily(req, res, async (family, user) => { const invite = await familyRegistry.createInvite(user.id, { displayName: user.displayName ?? user.display_name ?? user.username, }); const urls = familyRegistry.familyUrls(family); return { ...invite, joinUrl: `${urls.joinPath}${invite.token}`, }; })); api.post(`${prefix}/me/join`, async (req, res) => { const user = requireUser(req, res); if (!user) return; try { const result = await familyRegistry.acceptInvite(user.id, req.body?.token, { displayName: user.displayName ?? user.display_name ?? user.username, }); return res.json({ ok: true, family: { id: result.family.id, slug: result.family.slug, name: result.family.name, members: result.family.members, }, }); } catch (error) { return handleError(res, error); } }); api.get(`${prefix}/me/parent/stats`, (req, res) => withFamily(req, res, (family) => service.parentStats(family.id))); api.get(`${prefix}/me/parent/templates`, (req, res) => withFamily(req, res, (family) => service.parentListTemplates(family.id))); api.post(`${prefix}/me/parent/templates`, (req, res) => withFamily(req, res, (family) => service.parentSaveTemplate(family.id, req.body))); api.put(`${prefix}/me/parent/templates/:id`, (req, res) => withFamily(req, res, (family) => service.parentSaveTemplate(family.id, req.body, +req.params.id))); api.delete(`${prefix}/me/parent/templates/:id`, (req, res) => withFamily(req, res, (family) => service.parentDeleteTemplate(family.id, +req.params.id))); api.get(`${prefix}/me/parent/categories`, (req, res) => withFamily(req, res, (family) => service.parentListCategories(family.id))); api.post(`${prefix}/me/parent/categories`, (req, res) => withFamily(req, res, (family) => service.parentAddCategory(family.id, req.body))); api.delete(`${prefix}/me/parent/categories/:id`, (req, res) => withFamily(req, res, (family) => service.parentDeleteCategory(family.id, +req.params.id))); api.get(`${prefix}/me/parent/children`, (req, res) => withFamily(req, res, (family) => service.parentListChildren(family.id))); api.post(`${prefix}/me/parent/children`, (req, res) => withFamily(req, res, (family) => service.parentAddChild(family.id, req.body))); api.delete(`${prefix}/me/parent/children/:id`, (req, res) => withFamily(req, res, (family) => service.parentDeleteChild(family.id, +req.params.id))); return { familyRegistry, service }; } /** @deprecated use attachLearnAssistantPublicRoutes + attachLearnAssistantParentRoutes */ export function attachLearnAssistantRoutes(api, options = {}) { const runtime = attachLearnAssistantPublicRoutes(api, options); attachLearnAssistantParentRoutes(api, options); return runtime; }