From fffa1f7c7d478e64d08c560521028b36c2617770 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 26 Jun 2026 19:44:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20skill=20=E8=B7=AF=E7=94=B1=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E6=8C=82=E5=9C=A8=20/admin-api/skills=EF=BC=8C?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=20vite=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前台 vite 将 /api → portal(8081),/admin-api → memindadm(8085)。 将 user 路由从 /api/skills 移至 /admin-api/skills,公共 GET 不需要 auth,invoke 需 user auth,admin 操作需 admin auth。 Co-Authored-By: Claude Sonnet 4.6 --- server/app.mjs | 41 ++++++++------ server/skills/routes.mjs | 114 ++++++++++++++++++++------------------- 2 files changed, 85 insertions(+), 70 deletions(-) diff --git a/server/app.mjs b/server/app.mjs index e597ad9..07940a1 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -792,31 +792,40 @@ export function createAdminApp(services) { res.json(result); }); - app.use('/admin-api', adminApi); - - // ── Skill routes ────────────────────────────────────────────────────────── + // ── Skill routes (/admin-api/skills/*) ─────────────────────────────────── + // Mounted BEFORE adminApi so public GET endpoints bypass the global auth. + // Admin-only and user-auth endpoints use their own per-route middleware. if (services.skillService) { - const { adminRouter: skillAdminRouter, userRouter: skillUserRouter } = createSkillRoutes( - services.skillService, - { - requireAdmin, - requireAuth: asyncHandler(async (req, res, next) => { + const { router: skillRouter } = createSkillRoutes(services.skillService, { + requireAdmin: [ + asyncHandler(async (req, res, next) => { await cookieReady; await ready; const me = await userAuth.getMe(userToken(req)); if (!me) return res.status(401).json({ error: 'unauthorized' }); - req.user = me; + req.currentUser = me; next(); }), - }, - ); - adminApi.use('/skills', skillAdminRouter); - const skillPublicRouter = express.Router(); - wrapRouterAsync(skillPublicRouter); - skillPublicRouter.use('/skills', skillUserRouter); - app.use('/api', skillPublicRouter); + requireAdmin, + ], + requireAuth: asyncHandler(async (req, res, next) => { + await cookieReady; + await ready; + const me = await userAuth.getMe(userToken(req)); + if (!me) return res.status(401).json({ error: 'unauthorized' }); + req.user = me; + next(); + }), + }); + const skillApiRouter = express.Router(); + wrapRouterAsync(skillApiRouter); + skillApiRouter.use(jsonBody); + skillApiRouter.use('/skills', skillRouter); + app.use('/admin-api', skillApiRouter); } + app.use('/admin-api', adminApi); + if (services.createOpsApi) { const opsApi = express.Router(); wrapRouterAsync(opsApi); diff --git a/server/skills/routes.mjs b/server/skills/routes.mjs index ea727f3..5b9be36 100644 --- a/server/skills/routes.mjs +++ b/server/skills/routes.mjs @@ -1,86 +1,61 @@ /** - * Skill API routes mounted at /admin-api/skills (admin) - * and /api/skills (user-facing) + * Skill API routes — all mounted under /admin-api/skills on memindadm (port 8085). + * + * Public (no auth): + * GET /admin-api/skills/catalog — list active skills + * GET /admin-api/skills/designs — list design brands + * GET /admin-api/skills/designs/:brand — DESIGN.md content + * + * User (requires auth): + * POST /admin-api/skills/:slug/invoke — invoke a skill + * + * Admin only: + * PATCH /admin-api/skills/:slug/enabled — toggle on/off + * POST /admin-api/skills/:slug/flags — set flag override + * + * Why /admin-api? The vite dev proxy routes /admin-api → memindadm (8085), + * while /api → portal server (8081). Mounting here avoids touching the portal. */ import { Router } from 'express'; import { listDesigns, loadDesignMd, designExists } from './design-catalog.mjs'; export function createSkillRoutes(skillService, { requireAdmin, requireAuth } = {}) { - const adminRouter = Router(); - const userRouter = Router(); + const router = Router(); - // ── Admin routes (/admin-api/skills) ────────────────────────────────────── + // ── Public endpoints ─────────────────────────────────────────────────────── - // List all skills including disabled - adminRouter.get('/', requireAdmin, async (req, res) => { - const skills = await skillService.listSkills({ includeDisabled: true }); - res.json({ skills }); - }); - - // Toggle skill on/off - adminRouter.patch('/:slug/enabled', requireAdmin, async (req, res) => { - const { enabled, note } = req.body ?? {}; - if (typeof enabled !== 'boolean') { - return res.status(400).json({ error: 'enabled (boolean) required' }); - } - await skillService.setSkillEnabled(req.params.slug, enabled, note ?? ''); - res.json({ ok: true, slug: req.params.slug, enabled }); - }); - - // Set a flag override (user / plan / caller_type scope) - adminRouter.post('/:slug/flags', requireAdmin, async (req, res) => { - const { scope_type, scope_id, enabled, note } = req.body ?? {}; - if (!scope_type || typeof enabled !== 'boolean') { - return res.status(400).json({ error: 'scope_type and enabled required' }); - } - await skillService.setSkillFlag(req.params.slug, { - scopeType: scope_type, - scopeId: scope_id, - enabled, - note, - }); - res.json({ ok: true }); - }); - - // ── User-facing routes (/api/skills) ────────────────────────────────────── - - // List active skills - userRouter.get('/', async (req, res) => { + router.get('/catalog', async (_req, res) => { const skills = await skillService.listSkills(); res.json({ skills }); }); - // List available design styles (awesome-design-md catalog) - userRouter.get('/designs', async (req, res) => { - const designs = listDesigns(); - res.json({ designs }); + router.get('/designs', (_req, res) => { + res.json({ designs: listDesigns() }); }); - // Get a single DESIGN.md content - userRouter.get('/designs/:brand', async (req, res) => { + router.get('/designs/:brand', (req, res) => { const { brand } = req.params; if (!designExists(brand)) return res.status(404).json({ error: 'design not found' }); res.json({ brand, content: loadDesignMd(brand) }); }); - // Invoke a skill (one-shot, non-streaming) - userRouter.post('/:slug/invoke', requireAuth, async (req, res) => { - const { slug } = req.params; - const input = req.body ?? {}; + // ── User endpoints (requires auth) ──────────────────────────────────────── + + router.post('/:slug/invoke', requireAuth, async (req, res) => { + const { slug } = req.params; + const input = { ...(req.body ?? {}) }; - // For design-gen skill: inject DESIGN.md if caller passed design= param if (input.design && !input.design_md_content) { const md = loadDesignMd(input.design); if (!md) return res.status(400).json({ error: `Unknown design style: ${input.design}` }); input.design_md_content = md; } - const callerType = req.headers['x-caller-type'] ?? 'frontend_user'; const context = { userId: req.user?.id, planName: req.user?.plan, - callerType, + callerType: req.headers['x-caller-type'] ?? 'frontend_user', }; try { @@ -103,5 +78,36 @@ export function createSkillRoutes(skillService, { requireAdmin, requireAuth } = } }); - return { adminRouter, userRouter }; + // ── Admin-only endpoints ─────────────────────────────────────────────────── + + // List all skills including disabled + router.get('/', requireAdmin, async (_req, res) => { + const skills = await skillService.listSkills({ includeDisabled: true }); + res.json({ skills }); + }); + + router.patch('/:slug/enabled', requireAdmin, async (req, res) => { + const { enabled, note } = req.body ?? {}; + if (typeof enabled !== 'boolean') { + return res.status(400).json({ error: 'enabled (boolean) required' }); + } + await skillService.setSkillEnabled(req.params.slug, enabled, note ?? ''); + res.json({ ok: true, slug: req.params.slug, enabled }); + }); + + router.post('/:slug/flags', requireAdmin, async (req, res) => { + const { scope_type, scope_id, enabled, note } = req.body ?? {}; + if (!scope_type || typeof enabled !== 'boolean') { + return res.status(400).json({ error: 'scope_type and enabled required' }); + } + await skillService.setSkillFlag(req.params.slug, { + scopeType: scope_type, + scopeId: scope_id, + enabled, + note, + }); + res.json({ ok: true }); + }); + + return { router }; }