fix(kbdb): cypher proxy 補 /kbdb/entries CRUD + report_feedback 改打 /entries

kbdb-base Phase 9.6/9.7(HANDOFF §2 缺口① + §3b 連帶):

- 9.6 cypher kbdb-proxy 補 /kbdb/entries CRUD(POST/GET list/GET :id/PATCH :id)
  純轉發到 KBDB 基本盤 /entries,解鎖 mira _kbdb_client.py 主線遷移。
  租戶隔離同 9.5:寫入注入 owner_id、list 強制本租戶過濾、PATCH 剝 owner_id。
  刻意不開 DELETE(基本盤 delete 無 owner 檢查 → 跨租戶刪除風險)。
- 9.7 arcrun_report_feedback 從死 route /blocks 改打基本盤 /entries
  (entry_type=agent-feedback)。9.4 漏網的同類修;基本盤無 /blocks → 原本 404 假紅。

順帶(HANDOFF §6 harness 表達優化):
- 重寫 cli/harness/CLAUDE.block.md 補三盲點(recipe 是公共投稿 / 缺能力補 API 不拼裝 /
  自製零件退場路徑),目標 Haiku 級 CC 讀懂。
- README 零件 vs recipe 段對齊同三點。

cypher + mcp tsc exit 0。端到端 smoke test 隨後。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-15 13:06:58 +08:00
parent 8f4c5dbe59
commit b1e302b3b5
4 changed files with 113 additions and 16 deletions
+61
View File
@@ -134,3 +134,64 @@ kbdbProxyRouter.get('/kbdb/search', async (c) => {
);
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// ── entries(原子資料 / 樹節點,以租戶 namespace 為 owner_id 隔離)─────────────────
//
// kbdb-base 9.6:基本盤 /entries CRUD 的 proxyHANDOFF §2 缺口①,mira _kbdb_client.py 遷移目標)。
// 租戶隔離同 records(選項①):寫入強制注入 owner_id、list 強制以本租戶 owner_id 過濾;
// by-id 沿用既有 records by-id 慣例(require-key,不額外做 owner 比對——與本檔其他 by-id 端點一致)。
// POST /kbdb/entries — 建一個 entryentry_type 必填,如 block/value/project/workflow)。owner_id 自動注入。
kbdbProxyRouter.post('/kbdb/entries', async (c) => {
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const body = await c.req.json().catch(() => null);
if (!body || !body.entry_type) return c.json({ error: 'entry_type 必填' }, 400);
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/entries`, {
method: 'POST',
headers,
// 強制以租戶身份隔離:忽略 caller 自帶 owner_id,一律用 header 身份(防跨租戶寫入)
body: JSON.stringify({ ...body, owner_id: owner }),
});
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// GET /kbdb/entries — listfilters: entry_type / parent_id / page_name / limit / offset)。
// owner_id 強制覆寫成本租戶(防跨租戶讀;caller 不能查別人的 owner_id)。
kbdbProxyRouter.get('/kbdb/entries', async (c) => {
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const params = new URLSearchParams();
params.set('owner_id', owner); // 強制本租戶,不接受 caller 覆寫
for (const k of ['entry_type', 'parent_id', 'page_name', 'limit', 'offset']) {
const v = c.req.query(k);
if (v) params.set(k, v);
}
const res = await fetch(`${base}/entries?${params.toString()}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// GET /kbdb/entries/:id — 取單筆 entry。
kbdbProxyRouter.get('/kbdb/entries/:id', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/entries/${encodeURIComponent(c.req.param('id'))}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// 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);
const body = await c.req.json().catch(() => ({}));
// 不讓 patch 改 owner_id(防把別人的資料認領過來或踢給別人)
const { owner_id: _drop, ...patch } = body ?? {};
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/entries/${encodeURIComponent(c.req.param('id'))}`, {
method: 'PATCH',
headers,
body: JSON.stringify(patch),
});
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});