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:
@@ -189,6 +189,22 @@ P0 #10 修完後 mira 嘗試做 wiki 多段結構,又踩出三個 cypher bindi
|
||||
|
||||
### 三-A、P1 待改進(不擋封測,但 mira 已踩到)
|
||||
|
||||
#### P1 #3:cypher-executor 缺 `scheduled()` handler(2026-05-14 mira 7B.3h 提出)
|
||||
|
||||
**現況**:cron 零件 (`registry/components/cron/`) 只做 cron expression validation,不實作排程。cypher-executor 的 wrangler.toml 沒 `[triggers].crons`,src/index.ts 沒 `scheduled()` handler。所以 workflow YAML 寫了 cron 零件當 trigger 也不會真的跑。
|
||||
|
||||
**短期 workaround**(mira 採用,2026-05-14):前端 fire-and-forget 觸發 — 河道 post 成功後直接 POST 到 `/mira/wiki-from-raw`,server 端把 wiki_synthesis 用 `waitUntil` 跑掉。無 retry、無 batch、無排程。
|
||||
|
||||
**長期解**:
|
||||
1. `wrangler.toml [triggers] crons = [...]`
|
||||
2. `src/index.ts` `export default { fetch, scheduled }`:scheduled handler 掃 WEBHOOKS KV,找首節點是 cron 的 workflow,比對 cron_expr 跟 event.cron,匹配就 trigger
|
||||
3. acr push 偵測 cron 首節點時,把 cron_expr 一併寫入 KV record metadata
|
||||
4. workflow yaml 慣例:`my_cron >> ON_SUCCESS >> ...`,my_cron config 含 `cron_expr: "..."`
|
||||
|
||||
工:3-4 小時。對 RSS 抓 / voice-stt / mira ai-canon-wiki 等 cron-driven source 都有用。封測前不擋(用前端觸發 + 補跑按鈕即可)。
|
||||
|
||||
---
|
||||
|
||||
#### P1 #1:workflow 缺 IF/branch 能力(2026-05-14 mira 7B.3f 提出)
|
||||
|
||||
**現象**:mira 想做「找有則 PATCH 沒則 CREATE」(index-entry upsert),arcrun 目前只有 `ON_SUCCESS` + `對每個 X`(FOREACH)+ 已存在但壞掉的 `if_control`(見已知限制 #1),沒有 `>> ON_TRUE >>` / `>> ON_FALSE >>` 條件路由。
|
||||
|
||||
@@ -16,6 +16,7 @@ import { credentialsRouter } from './routes/credentials';
|
||||
import { webhooksNamedRouter } from './routes/webhooks-named';
|
||||
import { authRouter } from './routes/auth';
|
||||
import { resumeRouter } from './routes/resume';
|
||||
import { miraRouter } from './routes/mira';
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
@@ -42,6 +43,7 @@ app.route('/', recipesRouter);
|
||||
app.route('/', credentialsRouter);
|
||||
app.route('/', authRouter);
|
||||
app.route('/', resumeRouter);
|
||||
app.route('/', miraRouter);
|
||||
|
||||
// Worker 導出
|
||||
export default app;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Mira-specific routes — 給 mira app(landing/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 key(acr 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 workflow(service_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);
|
||||
});
|
||||
@@ -57,6 +57,10 @@ export type Bindings = {
|
||||
// 必填:cypher-executor 用此組出 component worker URL(避開同 zone 自循環死鎖,見 P0 #9)
|
||||
// self-hosted fork 必須改 wrangler.toml [vars] 為自己的帳號 subdomain
|
||||
WORKER_SUBDOMAIN: string;
|
||||
// Mira 配置(JSON 字串,欄位見 routes/mira.ts MiraConfig type)
|
||||
// 給 POST /mira/wiki-from-raw 用。未設定則該 endpoint 回 501。
|
||||
// 設定方式:wrangler secret put MIRA_CONFIG
|
||||
MIRA_CONFIG?: string;
|
||||
};
|
||||
|
||||
// 圖結構定義
|
||||
|
||||
@@ -158,6 +158,29 @@ export default function MiraPage() {
|
||||
|
||||
// ─── AI 回覆觸發器(fire-and-forget)──────────────────────
|
||||
|
||||
async function triggerWikiSynthesis(opts: { rawBlockId: string }) {
|
||||
// 對應 cypher-executor routes/mira.ts POST /mira/wiki-from-raw
|
||||
// server 端從 MIRA_CONFIG secret 補齊所有 partner key / token / block IDs
|
||||
// workflow 跑 60-90s,這裡 fire-and-forget 不等結果(拿 202 立刻回)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/mira/wiki-from-raw`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ raw_block_id: opts.rawBlockId }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
console.warn('[mira wiki-from-raw] not triggered:', res.status, data);
|
||||
return;
|
||||
}
|
||||
const data = await res.json().catch(() => ({}));
|
||||
console.log('[mira wiki-from-raw] accepted:', data);
|
||||
} catch (e) {
|
||||
console.warn('[mira wiki-from-raw] error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerAiReply(opts: {
|
||||
apiKey: string;
|
||||
postContent: string;
|
||||
@@ -272,6 +295,10 @@ function PostComposer({
|
||||
parentBlockId: postBlockId,
|
||||
pageName,
|
||||
});
|
||||
|
||||
// fire-and-forget 觸發 wiki_synthesis(7B.3h 簡化版:從 frontend 直接觸發,不走 cron)
|
||||
// 對應 routes/mira.ts;server 端從 MIRA_CONFIG secret 補齊 token / block IDs
|
||||
void triggerWikiSynthesis({ rawBlockId: postBlockId });
|
||||
onAiTriggered(pageName);
|
||||
|
||||
// 給 D1 GROUP BY 查詢看到新資料的時間
|
||||
|
||||
Reference in New Issue
Block a user