From 8d4c3a3464700960574615a3b68332262ed17ea7 Mon Sep 17 00:00:00 2001 From: richblack Date: Sat, 16 May 2026 10:35:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(mira):=20P0=20=E6=B2=B3=E9=81=93=E5=AE=8C?= =?UTF-8?q?=E5=96=84=203=20task=20=E2=80=94=20Mira=20=E9=A0=AD=E5=83=8F=20?= =?UTF-8?q?/=20@mira=20=E9=81=B8=E6=93=87=E6=80=A7=E5=9B=9E=E8=A6=86=20/?= =?UTF-8?q?=20=E7=B7=A8=E8=BC=AF=E5=99=A8=20popup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 對應 tasks.md backlog #5a / #5b / #5c(leo 2026-05-16 P0),design.md §3.6.5。 #5a Mira 發文獨立頭像 - 新 MiraAvatar 元件(紫色漸層圓 + 🤖 emoji) - isMiraSource() 判斷 post 來源是 leo(km-writer-direct/logseq/mobile/web/tg/rss) 還是 mira(ai-* / mira-* / 其他 test sources) - PostCard header:showMira ? Mira 頭像 + 名「Mira」 : leo 頭像 + leo 名 #5b @mira 選擇性回覆(撤回每篇 auto-reply) - 新 hasMiraMention() regex:偵測文字含 @mira(前後可有標點) - PostComposer / BlockEditor / ReplyLine 三處 submit:只有 @mira 時 triggerAiReply - triggerAiReply prompt 加 topic 抽取(@mira 後第一段到標點)+ scope hint - wiki_synthesis trigger 跟 @mira 無關,每篇都跑(KB 副本同步) - 不擋手動筆記(leo 隨手寫不需要 mira 每篇都回) #5c 編輯器 popup 放大 - composer 加 ⇱ 放大按鈕 → 切到 fullscreen overlay popup - popup 含大 textarea + 同 markdown toolbar + 同 submit 邏輯 - ⌘+Enter 發布、Esc 收起、外點 backdrop 收起 - 對應 leo「寫長文 textarea 太小」需求 CSS 加 .mira-avatar-mira / .mira-composer-popup-* 系列。 TS check pass。 --- landing/app/mira/feed/page.tsx | 238 ++++++++++++++++++++++++++------- landing/app/mira/mira.css | 84 ++++++++++++ 2 files changed, 273 insertions(+), 49 deletions(-) diff --git a/landing/app/mira/feed/page.tsx b/landing/app/mira/feed/page.tsx index b8928db..c0d4ff5 100644 --- a/landing/app/mira/feed/page.tsx +++ b/landing/app/mira/feed/page.tsx @@ -158,6 +158,12 @@ export default function MiraPage() { // ─── AI 回覆觸發器(fire-and-forget)────────────────────── +// P0 #5b:偵測文字含 @mira(mention) — 大小寫不限,前後可有標點 +// 用法:leo 在貼文 / 留言寫 `@mira <主題>` → 觸發 mira reply +function hasMiraMention(text: string): boolean { + return /(^|[^\w])@mira\b/i.test(text); +} + async function triggerWikiSynthesis(opts: { apiKey: string; rawBlockId: string }) { // 觸發 arcrun wiki_synthesis workflow(arcrun-native public trigger endpoint) // 不等結果(workflow 60-90s 含 2 次 claude_api pause/resume) @@ -187,10 +193,18 @@ async function triggerAiReply(opts: { parentBlockId: string; pageName: string; }) { + // 抽 @mira 後面的 topic(同行第一段,到換行 / 句號 / 標點停) + const mentionMatch = opts.postContent.match(/@mira\s*([^\n。.!!??,,]*)/i); + const topic = mentionMatch?.[1]?.trim() || ''; + const topicHint = topic + ? `\nleo 用 \`@mira ${topic}\` 呼叫了你,所以這則對話的主題鎖定在「${topic}」。\n` + : '\nleo 用 \`@mira\` 呼叫了你,請針對訊息回覆。\n'; + const prompt = - `用戶 leo 在 mira 河道發了這則貼文:\n\n` + + `用戶 leo 在 mira 河道發了這則訊息:\n\n` + `「${opts.postContent}」\n\n` + - `請以 Mira 副駕 AI 的身份留言回應。\n` + + topicHint + + `\n請以 Mira 副駕 AI 的身份留言回應。\n` + `規則:\n` + `- 繁體中文(台灣用語)\n` + `- 簡短 1-3 段,務實,不客套\n` + @@ -257,7 +271,9 @@ function PostComposer({ const [text, setText] = useState(''); const [submitting, setSubmitting] = useState(false); const [err, setErr] = useState(null); + const [expanded, setExpanded] = useState(false); const taRef = useRef(null); + const popupTaRef = useRef(null); const submit = useCallback(async () => { const trimmed = text.trim(); @@ -288,16 +304,18 @@ function PostComposer({ const postBlockId = data.data.id; setText(''); - // fire-and-forget 觸發 Mira AI 回覆(不擋用戶) - void triggerAiReply({ - apiKey: me.api_key, - postContent: trimmed, - parentBlockId: postBlockId, - pageName, - }); + // P0 #5b:只有 @mira 時才觸發 Mira AI 回覆(撤回每篇 auto-reply) + // 對應 design.md §3.6.5「河道是 process 場 + Mira 是 KB 同步介面」 + if (hasMiraMention(trimmed)) { + void triggerAiReply({ + apiKey: me.api_key, + postContent: trimmed, + parentBlockId: postBlockId, + pageName, + }); + } // 7B.3h:fire-and-forget 觸發 wiki_synthesis(browser → cypher.arcrun.dev,arcrun-native) - // 不走 watcher 是因為 cypher-executor 自己 fetch 自己 workers.dev URL 被 CF 1042 擋 - // watcher 仍作為 cron-driven backup(漏掉的 raws 5 分鐘後補跑),但需先解 self-fetch 問題 + // 跟 @mira 無關,每篇都跑 — 河道書寫永遠進 wiki KB 副本 void triggerWikiSynthesis({ apiKey: me.api_key, rawBlockId: postBlockId }); onAiTriggered(pageName); @@ -312,42 +330,120 @@ function PostComposer({ }, [text, submitting, me, onPosted, onAiTriggered]); return ( -
-
- -