fix: skill 路由统一挂在 /admin-api/skills,兼容 vite proxy

前台 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 <noreply@anthropic.com>
This commit is contained in:
john
2026-06-26 19:44:31 +08:00
parent 2e6b0c1818
commit fffa1f7c7d
2 changed files with 85 additions and 70 deletions
+25 -16
View File
@@ -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);
+60 -54
View File
@@ -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 };
}