feat: 薄殼原則落地 + seed 下沉 API + MCP 進主庫 + 部署一致性

壓測四橫向問題修正(docs 壓測報告):

① 薄殼原則成鐵律:能力長在 API,CLI/MCP/lib 只暴露
   - seed 下沉成 API 行為:cypher-executor POST /init/seed(一次灌 API+auth recipe),
     種子資料移到 server src/lib/api-recipe-seeds.ts,CLI 改薄殼一次呼叫
   - 解除 deployFullyOk 連坐 + init 補 seed auth recipe + update 補 seed/全 KV
   - registry SUBMISSIONS_KV 補進 REQUIRED_KV_NAMESPACES(修 20/21)

② MCP 統一帳號來源(單一 remote MCP + .env 切 MCP URL)
   - MCP 從 sibling repo 搬進 arcrun/mcp/(remote Worker,route 改 mcp.arcrun.dev)
   - config 加 mcp_url 三層解析 + getMcpUrl + DEFAULT_MCP_URL
   - 新增 acr mcp-setup:依 config 寫專案 .mcp.json(接案切資料夾自動切 MCP)
   - acr --version 改動態讀 package.json(根治漂移)

③ Deploy 一致性
   - tests/release.feature + scripts/check-release.sh
   - local-deploy.sh:CLI npm publish + auto patch bump + CHANGELOG
   - local-deploy.sh bash 3.2 相容修正(mapfile / 空陣列 set -u)
   - builtins/pnpm-lock.yaml

④ README self-hosted 同步現況(移除 R2 殘留、加 flag/env、多帳號)

CLI bump → 1.3.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-06 15:45:35 +08:00
parent 5f381a44a6
commit 3e65e22775
58 changed files with 8608 additions and 74 deletions
+242
View File
@@ -0,0 +1,242 @@
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<string, unknown> }> }>();
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 — 取得單一 WorkflowGUI 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<string, unknown> }>();
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<string, unknown>;
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<string, unknown> }> }>();
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<string, unknown> }>();
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<string, unknown> }>();
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<string, unknown> = {
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("/mcp", 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;