import { Hono } from "hono"; import { cors } from "hono/cors"; import { Env } from "./types.js"; import { partnerAuthMiddleware } from "./middleware/partner-auth.js"; import { handleMcpRequest } from "./mcp-handler.js"; import { inspectorHtml } from "./pages/inspector.js"; import { kbdbFetch } from "./lib/kbdb-client.js"; const _app = new Hono<{ Bindings: Env; Variables: { org_namespace: string; partner_token: string } }>(); const app = _app.basePath('/mcp'); app.use("*", cors({ origin: "*", allowMethods: ["GET", "POST", "OPTIONS"], allowHeaders: ["Content-Type", "Authorization"], exposeHeaders: ["Content-Type"], maxAge: 600, })); app.get("/", (c) => c.text("u6u MCP Server is running.")); app.get("/inspector", (c) => { return c.html(inspectorHtml); }); // ── GUI 認證端點 ─────────────────────────────────────────────────────────────── // GET /auth/verify — GUI 登入驗證,重用 partnerAuthMiddleware app.get("/auth/verify", partnerAuthMiddleware, (c) => { const orgNamespace = c.get("org_namespace"); return c.json({ valid: true, org_namespace: orgNamespace }); }); // ── GUI REST 端點(與 MCP tools 平行) ──────────────────────────────────────── // GET /workflows — 列出 Workflow 清單(GUI 用) app.get("/workflows", partnerAuthMiddleware, async (c) => { const orgNamespace = c.get("org_namespace"); try { const resp = await kbdbFetch( c.env, `/records/search?template=workflow_metadata&user_id=${encodeURIComponent(orgNamespace)}` ); if (!resp.ok) return c.json({ workflows: [] }); const data = await resp.json<{ records: Array<{ id: string; slots?: Record }> }>(); const workflows = (data.records ?? []).map(r => ({ id: r.id, name: (r.slots?.display_name as string | undefined) ?? (r.slots?.name as string | undefined) ?? r.id, last_run: r.slots?.last_run as string | undefined, status: r.slots?.status as string | undefined, slots: r.slots, })); return c.json({ workflows }); } catch { return c.json({ workflows: [] }); } }); // GET /workflows/:id — 取得單一 Workflow(GUI poll 用) app.get("/workflows/:id", partnerAuthMiddleware, async (c) => { const id = c.req.param("id") ?? ''; try { if (!id) return c.json({ error: "Missing id" }, 400); const resp = await kbdbFetch(c.env, `/records/${encodeURIComponent(id)}`); if (!resp.ok) return c.json({ error: "Not found" }, 404); const data = await resp.json<{ id: string; slots?: Record }>(); return c.json({ id: data.id, name: (data.slots?.display_name as string | undefined) ?? data.id, slots: data.slots, }); } catch { return c.json({ error: "Internal error" }, 500); } }); // POST /action-log — GUI 寫入用戶動作記錄 app.post("/action-log", partnerAuthMiddleware, async (c) => { const orgNamespace = c.get("org_namespace"); try { const body = await c.req.json<{ action_type: string; payload?: Record; occurred_at?: string; }>(); const occurred_at = body.occurred_at ?? new Date().toISOString(); await kbdbFetch(c.env, "/records", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ template_id: "tpl-action-log", user_id: orgNamespace, slots: { org_namespace: orgNamespace, action_type: body.action_type, payload: JSON.stringify(body.payload ?? {}), occurred_at, }, }), }); return c.json({ ok: true }); } catch { return c.json({ ok: false }, 500); } }); // ── Prototype Pages REST 端點 ───────────────────────────────────────────────── // GET /prototype-pages — 列出 Prototype Pages app.get("/prototype-pages", partnerAuthMiddleware, async (c) => { const orgNamespace = c.get("org_namespace"); try { const resp = await kbdbFetch( c.env, `/records/search?template=tpl-page-block&user_id=${encodeURIComponent(orgNamespace)}` ); if (!resp.ok) return c.json({ pages: [] }); const data = await resp.json<{ records: Array<{ id: string; slots?: Record }> }>(); const pages = (data.records ?? []).map(r => ({ id: r.id, page_name: (r.slots?.page_name as string | undefined) ?? 'Untitled', components_json: (r.slots?.components_json as string | undefined) ?? '[]', last_edited_by: (r.slots?.last_edited_by as string | undefined) ?? 'gui', last_edited_at: (r.slots?.last_edited_at as string | undefined) ?? '', status: (r.slots?.status as string | undefined) ?? 'draft', })); return c.json({ pages }); } catch { return c.json({ pages: [] }); } }); // POST /prototype-pages — 建立新 Prototype Page app.post("/prototype-pages", partnerAuthMiddleware, async (c) => { const orgNamespace = c.get("org_namespace"); try { const body = await c.req.json<{ page_name?: string }>(); const page_name = body.page_name ?? 'Untitled'; const now = new Date().toISOString(); const resp = await kbdbFetch(c.env, "/records", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ template_id: "tpl-page-block", user_id: orgNamespace, slots: { page_name, org_namespace: orgNamespace, components_json: '[]', last_edited_by: 'gui', last_edited_at: now, status: 'draft', }, }), }); if (!resp.ok) return c.json({ error: "Failed to create" }, 500); const data = await resp.json<{ id: string; slots?: Record }>(); return c.json({ id: data.id, page_name, components_json: '[]', last_edited_by: 'gui', last_edited_at: now, status: 'draft', }, 201); } catch { return c.json({ error: "Internal error" }, 500); } }); // GET /prototype-pages/:id — 取得單一 Prototype Page app.get("/prototype-pages/:id", partnerAuthMiddleware, async (c) => { const id = c.req.param("id") ?? ''; try { if (!id) return c.json({ error: "Missing id" }, 400); const resp = await kbdbFetch(c.env, `/records/${encodeURIComponent(id)}`); if (!resp.ok) return c.json({ error: "Not found" }, 404); const data = await resp.json<{ id: string; slots?: Record }>(); return c.json({ id: data.id, page_name: (data.slots?.page_name as string | undefined) ?? 'Untitled', components_json: (data.slots?.components_json as string | undefined) ?? '[]', last_edited_by: (data.slots?.last_edited_by as string | undefined) ?? 'gui', last_edited_at: (data.slots?.last_edited_at as string | undefined) ?? '', status: (data.slots?.status as string | undefined) ?? 'draft', }); } catch { return c.json({ error: "Internal error" }, 500); } }); // PUT /prototype-pages/:id — 儲存 Prototype Page app.put("/prototype-pages/:id", partnerAuthMiddleware, async (c) => { const id = c.req.param("id") ?? ''; try { if (!id) return c.json({ error: "Missing id" }, 400); const body = await c.req.json<{ components_json?: string; page_name?: string; }>(); const now = new Date().toISOString(); const slots: Record = { last_edited_by: 'gui', last_edited_at: now, }; if (body.components_json !== undefined) slots.components_json = body.components_json; if (body.page_name !== undefined) slots.page_name = body.page_name; const resp = await kbdbFetch(c.env, `/records/${encodeURIComponent(id)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ slots }), }); if (!resp.ok) return c.json({ error: "Failed to save" }, 500); return c.json({ ok: true }); } catch { return c.json({ error: "Internal error" }, 500); } }); // ── MCP 端點 ────────────────────────────────────────────────────────────────── app.options("/mcp", (c) => { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", }, }); }); app.post("/", partnerAuthMiddleware, async (c) => { const orgNamespace = c.get("org_namespace"); const partnerToken = c.get("partner_token"); return handleMcpRequest(c.req.raw, c.env, orgNamespace, partnerToken); }); export default app;