diff --git a/cypher-executor/src/routes/console-auth.ts b/cypher-executor/src/routes/console-auth.ts index 690e6d7..d5215ee 100644 --- a/cypher-executor/src/routes/console-auth.ts +++ b/cypher-executor/src/routes/console-auth.ts @@ -29,6 +29,17 @@ const CREDS_KEY = 'console:credentials'; const SESSION_PREFIX = 'console_sess:'; const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 天 +/** + * 驗證 console session(給其他 route 共用,如 /console/inbox-data)。 + * authHeader 形如 "Bearer ";有效回 true。 + */ +export async function validateConsoleSession(env: Bindings, authHeader: string | undefined): Promise { + const token = (authHeader ?? '').match(/^Bearer\s+(\S+)/i)?.[1]; + if (!token) return false; + const sess = await env.SESSIONS_KV.get(`${SESSION_PREFIX}${token}`); + return !!sess; +} + interface StoredCredentials { email: string; salt: string; // hex diff --git a/cypher-executor/src/routes/console-dashboard.ts b/cypher-executor/src/routes/console-dashboard.ts index 39ee541..6e2a31b 100644 --- a/cypher-executor/src/routes/console-dashboard.ts +++ b/cypher-executor/src/routes/console-dashboard.ts @@ -29,6 +29,7 @@ import { Hono } from 'hono'; import type { Bindings } from '../types'; import { kbdbBase } from './kbdb-proxy'; +import { validateConsoleSession } from './console-auth'; export const consoleDashboardRouter = new Hono<{ Bindings: Bindings }>(); @@ -164,97 +165,188 @@ consoleDashboardRouter.get('/console/dashboard-data', async (c) => { }); }); +// GET /console/inbox-data — 收件匣清單(Mira Console 頁 7)。**需 console session**(Bearer): +// dashboard-data 只吐計數可免登入;這裡吐訊息原文(leo 丟給 Telegram bot 的指令內容)屬機敏,鎖登入。 +// 資料源同 dashboard-data 的 inbox entries(entry_type=inbox,content JSON {"text","status","result"?})。 +consoleDashboardRouter.get('/console/inbox-data', async (c) => { + const ok = await validateConsoleSession(c.env, c.req.header('authorization')); + if (!ok) return c.json({ error: '需要登入(console session)' }, 401); + + const tenant = c.env.CONSOLE_TENANT || 'leo'; + const entries = await fetchEntries(c.env, tenant, 'inbox', 200); + const items = entries.map((e) => { + const j = parseJsonContent(e); + return { + id: e.id, + text: typeof j?.text === 'string' ? (j.text as string) : (e.content ?? ''), + status: j && j.status === 'done' ? 'done' : 'new', + result: typeof j?.result === 'string' ? (j.result as string) : '', + at: e.created_at, + }; + }); + return c.json({ + items, + new_count: items.filter((i) => i.status !== 'done').length, + total: items.length, + }); +}); + function renderDashboardHtml(): string { return ` -arcrun 駕駛艙 +Mira 駕駛艙
-
-
載入中
-
-
-
今日完成度
-
+
+
Mira 駕駛艙
+
+
+
+
+
+
載入中
+
-
-
今日路線
-
  • 載入中...
- -
    -
    等你的事
    -
      -
      +
      +
      +
      今日完成
      +
      /
      +
      +
      +
      +
      收件匣未處理
      +
      +
      來自 Telegram
      +
      -
      +
      +
      等你的事
      +
      載入中…
      +
      +
      今日路線狀態即時同步
      +
      • 載入中…
      + +
        +
        每 60 秒自動刷新
        + 進入完整控制台 ›
        @@ -383,8 +1212,8 @@ function renderConsoleHtml(registryBase: string): string { `; } -// GET /console — 控制台頁 v0(Arcrun#3)。registry base 現算:同帳號 arcrun-registry worker -// (WORKER_SUBDOMAIN 沿用既有 KBDB_BASE_URL 那套組法),沒設就退回官方公開 registry。 +// GET /console — Mira Console 完整版(Arcrun#3 console 系)。registry base 現算:同帳號 +// arcrun-registry worker(WORKER_SUBDOMAIN 沿用既有 KBDB_BASE_URL 那套組法),沒設就退回官方公開 registry。 consoleRouter.get('/console', (c) => { const subdomain = c.env.WORKER_SUBDOMAIN; const registryBase = subdomain diff --git a/cypher-executor/src/routes/credentials.ts b/cypher-executor/src/routes/credentials.ts index 4219b6c..73ab77a 100644 --- a/cypher-executor/src/routes/credentials.ts +++ b/cypher-executor/src/routes/credentials.ts @@ -205,6 +205,31 @@ credentialsRouter.delete('/credentials/:name', async (c) => { return c.json({ success: true, name }); }); +// GET /credentials/catalog — D1 目錄唯讀 list(Mira Console 完整版,Arcrun#3 console 系)。 +// 只回 metadata(name/service/sensitivity/created_at/last_used_at),**絕不回密文值**—— +// 密文在 Workers Secrets,本 worker 自己也讀不回(D19「擁有目錄,不擁有內容物」)。 +// 與舊 GET /credentials(KV 名稱清單)並存:這裡是新制 D1 目錄;舊 KV 寫入的看不到(T5 已知誠實缺口)。 +// 註冊須在 GET /credentials/:name 類 route 之前?本檔無 :name GET route,無攔截問題。 +credentialsRouter.get('/credentials/catalog', async (c) => { + const apiKey = c.req.header('X-Arcrun-API-Key'); + if (!apiKey) { + return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401); + } + try { + const rows = await c.env.CREDENTIALS_DB + .prepare( + `SELECT name, service, sensitivity, created_at, last_used_at + FROM credentials WHERE api_key = ? ORDER BY created_at DESC`, + ) + .bind(apiKey) + .all<{ name: string; service: string | null; sensitivity: string; created_at: number; last_used_at: number | null }>(); + return c.json({ success: true, credentials: rows.results ?? [], total: (rows.results ?? []).length }); + } catch (e) { + // 誠實回報:D1 未建表 / migration 未跑(不假綠回空陣列裝沒事) + return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502); + } +}); + // GET /credentials — 列出 credential 名稱(不含值)(未動:仍是舊 KV 路徑,T9 範圍) credentialsRouter.get('/credentials', async (c) => { const apiKey = c.req.header('X-Arcrun-API-Key'); diff --git a/cypher-executor/src/routes/kbdb-proxy.ts b/cypher-executor/src/routes/kbdb-proxy.ts index f60db6a..8d12267 100644 --- a/cypher-executor/src/routes/kbdb-proxy.ts +++ b/cypher-executor/src/routes/kbdb-proxy.ts @@ -189,6 +189,33 @@ kbdbProxyRouter.get('/kbdb/entries/:id', async (c) => { return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } }); }); +// ── graph(kbdb-graph-plugin proxy,Mira Console 卡片詳頁「關聯視圖」)─────────────── +// +// 純轉發(rule 07):圖能力真身在 kbdb-graph-plugin worker(GET /graph/neighbors/:name, +// 回 { node, edges[], neighbors[], edgeCount, neighborCount })。瀏覽器不能持 KBDB_INTERNAL_TOKEN, +// 故經 cypher 代轉(token 只在 server 側)。plugin base 現算慣例同 registry/KBDB_BASE_URL。 + +function graphBase(env: Bindings): string { + if (env.KBDB_GRAPH_URL) return env.KBDB_GRAPH_URL.replace(/\/$/, ''); + return `https://kbdb-graph-plugin.${env.WORKER_SUBDOMAIN}.workers.dev`; +} + +// GET /kbdb/graph/neighbors/:name — 查某節點(entity/卡片名)的鄰居 + 邊。 +// 查無 triplet 資料時 plugin 回空陣列——前端據此顯示「尚無關聯資料」(誠實,不編造關聯)。 +kbdbProxyRouter.get('/kbdb/graph/neighbors/:name', async (c) => { + if (!tenant(c)) return c.json(NEED_KEY, 401); + const base = graphBase(c.env); + const headers: Record = {}; + if (c.env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${c.env.KBDB_INTERNAL_TOKEN}`; + try { + const res = await fetch(`${base}/graph/neighbors/${encodeURIComponent(c.req.param('name'))}`, { headers }); + return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } }); + } catch (e) { + // plugin worker 沒部署 / 不可達 → 誠實回報(前端顯示「關聯服務未部署」而非假裝無關聯) + return c.json({ error: `kbdb-graph-plugin 不可達(${base}):${e instanceof Error ? e.message : String(e)}` }, 502); + } +}); + // PATCH /kbdb/entries/:id — 更新單筆 entry。owner_id 不可被改(剝除 caller 自帶的 owner_id)。 kbdbProxyRouter.patch('/kbdb/entries/:id', async (c) => { if (!tenant(c)) return c.json(NEED_KEY, 401); diff --git a/cypher-executor/src/types.ts b/cypher-executor/src/types.ts index 057b627..1c0bd36 100644 --- a/cypher-executor/src/types.ts +++ b/cypher-executor/src/types.ts @@ -78,6 +78,10 @@ export type Bindings = { // console 登入後端一律用這個字串打 /kbdb/*、/workflows/search(不做多租戶,登入系統只擋外人看頁面)。 // 未設 → routes/console-auth.ts 預設 "leo"(發現①已核實:D1 458,357 筆資料實際使用的租戶字串)。 CONSOLE_TENANT?: string; + // kbdb-graph-plugin worker base URL(可選)。未設 → 用 WORKER_SUBDOMAIN 現算 + // https://kbdb-graph-plugin..workers.dev(該 repo wrangler.toml name 固定)。 + // console 卡片詳頁「關聯視圖」經 cypher proxy 打它(kbdb-proxy.ts /kbdb/graph/neighbors/:name)。 + KBDB_GRAPH_URL?: string; }; // 重新 export Cloudflare Workers ExecutionContext 以便其他 module 用