feat(mira): 河道 → wiki 自動化(fire-and-forget 觸發 wiki_synthesis)

對應 polaris/mira/.agents/specs/mira-app/tasks.md 7B.3h(簡化版)。

原計畫用 arcrun cron 零件 → cypher-executor scheduled() handler,但發現
cron 零件只是 validator,cypher-executor 還沒實作 scheduled()。為了不擋
「河道書寫 → 自動產 wiki」這條 UX,先做 fire-and-forget 版本:

- 新 cypher-executor route POST /mira/wiki-from-raw
  - body: { raw_block_id }
  - server 端從 MIRA_CONFIG secret 補 partner key / mira_token / 三個 block IDs
  - waitUntil 背景跑 executeWebhookGraph,立刻回 202
- landing 河道 post composer 成功寫 raw 後 fire-and-forget triggerWikiSynthesis()
  跟既有 triggerAiReply() 同範式
- types.ts 加 MIRA_CONFIG?: string

部署後需手動:
  echo '{"service_api_key":"ak_...","data_api_key":"ak_...","schema_block_id":"...","skill_block_id":"...","entities_block_id":"...","mira_token":"..."}' \
    | wrangler secret put MIRA_CONFIG

UX:河道貼一則 → AI reply 30s 內 → wiki 60-90s 內出現在 /mira/wiki。

arcrun.md 記 P1 #3:cypher-executor 加 scheduled() handler,那是真正的
cron 路線,封測前不擋。
This commit is contained in:
2026-05-14 13:50:13 +08:00
parent 933ae6cb13
commit 660b32eafd
5 changed files with 159 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* Mira-specific routes — 給 mira applanding/app/mira/feed)從前端 fire-and-forget
* 觸發 wiki_synthesis workflow,而不需要前端持有 mira_token / partner key / block IDs。
*
* SDD: polaris/mira/.agents/specs/mira-app/design.md §3.5.12 + §5.3
* 對應 task: mira 7B.3h(簡化版:以 frontend fire-and-forget 取代 cron 觸發;
* 真正的 cron 排程留 arcrun.md P1 #3 — cypher-executor scheduled() handler
*
* 設定(self-host fork):
* wrangler secret put MIRA_CONFIG (cypher-executor)
* 值為 JSON 字串,欄位見 MiraConfig type
*/
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { executeWebhookGraph } from '../actions/webhook-handlers';
export const miraRouter = new Hono<{ Bindings: Bindings }>();
type MiraConfig = {
service_api_key: string; // 部署 wiki_synthesis 的 partner keyacr push 用)
data_api_key: string; // mira 寫 KBDB 用的 partner key(前端 /me 拿的)
schema_block_id: string; // mira-wiki-schema block
skill_block_id: string; // mira-wiki-skill block
entities_block_id: string; // mira-wiki-index-entities block
mira_token: string; // claude_api → mira daemon 的 bearer
};
function parseMiraConfig(raw?: string): MiraConfig | null {
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as Partial<MiraConfig>;
if (
!parsed.service_api_key ||
!parsed.data_api_key ||
!parsed.schema_block_id ||
!parsed.skill_block_id ||
!parsed.entities_block_id ||
!parsed.mira_token
) {
return null;
}
return parsed as MiraConfig;
} catch {
return null;
}
}
// POST /mira/wiki-from-raw — 對一個 raw block 跑 wiki_synthesis
// Body: { raw_block_id: string }
// 給前端 fire-and-forget 用,不等結果回(workflow 跑 60-90s
miraRouter.post('/mira/wiki-from-raw', async (c) => {
const cfg = parseMiraConfig(c.env.MIRA_CONFIG);
if (!cfg) {
return c.json(
{
error: 'Mira 未配置:請 wrangler secret put MIRA_CONFIG(見 routes/mira.ts header',
},
501,
);
}
let body: { raw_block_id?: string } = {};
try {
body = (await c.req.json()) as { raw_block_id?: string };
} catch {
return c.json({ error: 'body 必須是 JSON' }, 400);
}
if (!body.raw_block_id) {
return c.json({ error: 'raw_block_id 必填' }, 400);
}
// 從 KV 拿 wiki_synthesis workflow 定義(部署在 service_api_key 名下)
const wfKey = `webhook:${cfg.service_api_key}:wiki_synthesis`;
const raw = await c.env.WEBHOOKS.get(wfKey, 'text');
if (!raw) {
return c.json(
{ error: '找不到 wiki_synthesis workflowservice_api_key 是否與 acr push 用的一致?)' },
404,
);
}
let record: { graph: Record<string, unknown> };
try {
record = JSON.parse(raw) as { graph: Record<string, unknown> };
} catch {
return c.json({ error: 'workflow 定義損毀' }, 500);
}
const triggerContext: Record<string, unknown> = {
api_key: cfg.data_api_key,
mira_token: cfg.mira_token,
schema_block_id: cfg.schema_block_id,
skill_block_id: cfg.skill_block_id,
entities_block_id: cfg.entities_block_id,
index_entries_block_id: cfg.entities_block_id, // 7B.3f:暫共用 entities block 當 index parent
raw_block_id: body.raw_block_id,
};
// fire-and-forget:用 waitUntil 在 background 跑,立刻回 202
// 若用戶 cookie session 不要等
const promise = executeWebhookGraph(c.env, record.graph, triggerContext, 'wiki_synthesis', cfg.service_api_key);
c.executionCtx.waitUntil(
promise.then(
(r) => console.log('[mira/wiki-from-raw] done', r.success, r.duration_ms),
(e) => console.error('[mira/wiki-from-raw] failed', e),
),
);
return c.json({ accepted: true, raw_block_id: body.raw_block_id }, 202);
});