diff --git a/.env.example b/.env.example index 6895090..1aaec0f 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,13 @@ COMMENTS_GITHUB_PRIVATE_KEY= COMMENTS_GITHUB_TOKEN= COMMENTS_SESSION_SECRET= +# --- WebMCP (expose site tools to browser AI agents) --- +# Origin trial token for https://razet.me — register at +# https://developer.chrome.com/origintrials (Chrome 149+) and, separately, +# https://developer.microsoft.com/microsoft-edge/origin-trials (Edge 150+). +# Unset = no meta tag is emitted and the tools stay dormant; the site is unaffected. +PUBLIC_WEBMCP_ORIGIN_TRIAL_TOKEN= + # --- Keystatic storage mode --- # local = write Markdown on disk (default in `pnpm dev` / local build) # github = commit via GitHub App (Netlify sets this in netlify.toml) diff --git a/.trellis/spec/frontend/index.md b/.trellis/spec/frontend/index.md index c476e01..2e01796 100644 --- a/.trellis/spec/frontend/index.md +++ b/.trellis/spec/frontend/index.md @@ -22,6 +22,7 @@ This directory contains guidelines for frontend development. Fill in each file w | [Type Safety](./type-safety.md) | Type patterns, validation | To fill | | [Markdown Pipeline](./markdown-pipeline.md) | remark/rehype/Shiki ordering, math & mermaid rendering, Keystatic gotchas | Filled | | [Keystatic Admin](./keystatic-admin.md) | Admin customization wiring, toolbar host contract, editor scroll anatomy | Filled | +| [WebMCP Agent Tools](./webmcp-tools.md) | Agent tool contracts, client/server import boundary, navigation allowlist, search index | Filled | --- diff --git a/.trellis/spec/frontend/webmcp-tools.md b/.trellis/spec/frontend/webmcp-tools.md new file mode 100644 index 0000000..79b5b8b --- /dev/null +++ b/.trellis/spec/frontend/webmcp-tools.md @@ -0,0 +1,111 @@ +# WebMCP Agent Tools + +> How this site exposes capabilities to browser-integrated AI agents, and the rules +> that keep it from breaking the site or leaking server code into the browser. + +Code lives in `src/lib/webmcp/`, mounted by `src/components/WebMcp.astro` from `BaseLayout.astro`. + +Spec: — API is +`document.modelContext.registerTool(tool, { signal })`. + +--- + +## Non-negotiables + +### 1. Never import `@/lib/comments` (or `@/lib/github`, `@/lib/comments-token`) from client code + +`src/lib/comments.ts` looks like a bag of pure helpers, but it transitively imports +`@/lib/github` and `@/lib/comments-token`, both of which read server-side env. Importing it +from a browser module drags secrets-adjacent code into the client bundle. + +This is why `src/lib/webmcp/page-context.ts` re-implements `commentKey()` as a one-liner +instead of importing it. **If you change `commentKey()` in `src/lib/comments.ts`, update +`toCommentKey()` in `page-context.ts` to match** — the duplication is deliberate, not an oversight. + +The same rule is why `Comments.astro` computes the key in its frontmatter and passes it down +through `data-slug` rather than computing it in the client script. + +### 2. Feature-detect, then dynamic-import + +`WebMcp.astro` ships **only** a feature check to every visitor (~230 bytes). The ~12KB of tool +code loads via `import()` and only when `document.modelContext?.registerTool` exists. + +Reason: WebMCP is an origin trial (Chrome 149 / Edge 150). Almost no visitor has a host for it, +so eagerly bundling the tools would be pure waste. Keep this shape when adding tools. + +### 3. Navigation is an allowlist, and it must track the routes + +`resolveInternalPath()` in `tools/navigation.ts` permits only `/`, `/posts`, `/posts/`, +`/leetcode`, `/leetcode/`. `/keystatic` and `/api/*` are excluded *by construction*, +not by an explicit ban. + +**Adding a public route means adding it to `ALLOWED_PREFIXES`.** Adding a private one means +doing nothing. Prefix comparison is `=== prefix || startsWith(prefix + '/')` — never a bare +`startsWith(prefix)`, which would let `/postsevil` through. + +### 4. Side-effecting tools stop at the confirmation boundary + +`draft-comment` fills the existing textarea and scrolls to it. It **never** calls `submit()`, +`requestSubmit()`, or clicks the submit button. Publishing a comment writes to a GitHub Issue +under the visitor's own identity and cannot be undone. + +Any future tool that writes, deletes, or spends follows the same shape: prepare the action in +the existing UI, then return text telling the agent to hand control back to the user. + +Guard when reviewing: `grep -rnE 'requestSubmit|\.submit\(' src/lib/webmcp/` must stay empty. + +### 5. Tools return error *results*, never throw + +Every `execute` returns `err('…')` on failure. A thrown exception reaches the agent as an opaque +`DOMException`; readable text lets the model fix its arguments and retry. Arguments come from an +LLM and are untrusted — read them through `src/lib/webmcp/args.ts`, never destructure directly. + +--- + +## Search index + +`src/pages/search-index.json.ts` emits `dist/search-index.json` at build time. + +It **must** source posts through `getBlogPosts()` / `getLeetcodePosts()` and build paths through +`postHref()` (both in `src/lib/posts.ts`). That is what makes the index inherit `display: false` +filtering and stay in sync with real routes for free. + +Note the asymmetry: `posts/[...slug].astro` uses an *unfiltered* `getCollection`, so a +`display: false` post still gets a page — it just never appears in lists or in the index. +Current counts: 15 indexed of 16 files (`toy-record.md` is hidden). + +The index is fetched lazily on first content-tool use, cached in a module-level promise, and +cleared on failure so a later call can retry. Do not preload it. + +Ranking is weighted substring counting (title 8, desc 4, tag 3, body 1 capped at 10 hits), +not semantic search. Chinese queries work because matching is substring-based, not tokenised. + +--- + +## Adding a tool + +1. Put it in the matching `src/lib/webmcp/tools/*.ts`; add a new file if it is a new domain. +2. Write `description` in **English** (the agent's model reads it); return user-facing text in + **Chinese** (the site's language). +3. Validate every argument via `args.ts`. Return `err()` — never throw. +4. Export it through the domain array; `register.ts` picks it up automatically. +5. If it has side effects, apply rule 4 above. + +--- + +## Origin trial + +`BaseLayout.astro` emits `` only when +`PUBLIC_WEBMCP_ORIGIN_TRIAL_TOKEN` is set. Unset → no tag, tools stay dormant, site unaffected. +Tokens are per-origin and expire; re-register at . + +## Verifying without a WebMCP browser + +Firefox and Safari have no implementation, so most checks are indirect: + +- `pnpm build` → `dist/search-index.json` exists with the expected post count +- Bundle the pure logic with esbuild and exercise it under Node with a mocked `fetch` + (`searchPosts`, `findPost`, `resolveInternalPath` are all environment-light by design — + keep them that way so they stay testable) +- Confirm `dist/_astro/WebMcp.*.js` stays tiny and the tool chunk is separate +- Actual tool invocation needs Chrome 149+ with the trial token/flag, or ChatGPT Desktop diff --git a/.trellis/tasks/07-24-admin-keystatic-umami/check.jsonl b/.trellis/tasks/08-28-webmcp-tools/check.jsonl similarity index 100% rename from .trellis/tasks/07-24-admin-keystatic-umami/check.jsonl rename to .trellis/tasks/08-28-webmcp-tools/check.jsonl diff --git a/.trellis/tasks/08-28-webmcp-tools/design.md b/.trellis/tasks/08-28-webmcp-tools/design.md new file mode 100644 index 0000000..3b5e3a5 --- /dev/null +++ b/.trellis/tasks/08-28-webmcp-tools/design.md @@ -0,0 +1,227 @@ +# Design — 基于 WebMCP 暴露站点 agent 工具 + +## 1. 架构概览 + +``` +构建期 (Node) 运行时 (浏览器) +───────────────── ────────────────────────────────────── +src/pages/search-index.json.ts BaseLayout.astro + ├─ getCollection('blogs') ├─ (有 token 时) + ├─ getCollection('leetcode') └─ + ├─ markdownToText(body) └─ +``` + +实测:首屏 chunk **229 bytes**,工具代码 12KB(gzip 5.1KB) 落在独立的 `register.*.js`, +仅在检测通过时才请求。 + +不使用 `exposedTo`——工具只对同源文档与内置 agent 可见(WebMCP 默认行为)。 + +## 6. 类型策略 + +npm 上有官方 `webmcp-types` 包,但它随 Origin Trial 演进、版本可能不稳。 +本期在 `src/lib/webmcp/types.ts` **自持最小类型声明**(只声明用到的 `registerTool` 与结果类型): + +```ts +declare global { + interface Document { modelContext?: ModelContext } +} +``` + +Reason: 只需要 API 的一个小切面;自持声明消除外部依赖的版本漂移风险, +且 `pnpm check` 立即可用。若后续 API 稳定,可一行切换到 `webmcp-types`。 + +## 7. Origin Trial 注入 + +`BaseLayout.astro` head 内: + +```astro +{originTrialToken && } +``` + +token 取自 `import.meta.env.PUBLIC_WEBMCP_ORIGIN_TRIAL_TOKEN`(`PUBLIC_` 前缀使其可在静态构建中内联)。 +未配置 → 不输出标签(PRD 验收项)。 + +## 8. 兼容性与回滚 + +| 维度 | 结论 | +|---|---| +| 不支持 WebMCP 的浏览器 | `registerAll()` 首行返回,零影响 | +| 新增网络请求 | 仅工具被调用时才 fetch 索引;普通访问者无额外请求 | +| 新增初始 JS | **229 bytes**(仅特性检测)。12KB 的工具代码走动态 import,只有 WebMCP 宿主才下载 | +| 现有功能 | 无破坏性修改;`BaseLayout` 只做加法 | +| 回滚 | 从 `BaseLayout.astro` 移除 `` 与 meta 两行即完全禁用;新增文件可独立保留 | + +## 9. 已知取舍 + +- **验证受限**:Firefox / Safari 无实现,Chrome 需 Origin Trial token 或本地 flag。 + 自动化验收只能覆盖「构建通过 + 索引正确 + 不支持时零回归」;工具实际调用需在 + Chrome(带 flag)或 ChatGPT Desktop 中人工验证。 +- **检索是子串计数而非语义检索**:中文场景下「贡献」能命中,「怎么给开源提 PR」可能不命中。 + 接受此取舍——agent 侧的 LLM 通常会自行改写查询词重试,且内容量小时召回压力低。 +- **`get-post` 返回全文**:长文可能超出 agent 上下文预算。本期不做分段, + 由 agent 侧自行截断;如成为问题再加 `maxChars` 参数。 diff --git a/.trellis/tasks/07-24-admin-keystatic-umami/implement.jsonl b/.trellis/tasks/08-28-webmcp-tools/implement.jsonl similarity index 100% rename from .trellis/tasks/07-24-admin-keystatic-umami/implement.jsonl rename to .trellis/tasks/08-28-webmcp-tools/implement.jsonl diff --git a/.trellis/tasks/08-28-webmcp-tools/implement.md b/.trellis/tasks/08-28-webmcp-tools/implement.md new file mode 100644 index 0000000..60a5cc9 --- /dev/null +++ b/.trellis/tasks/08-28-webmcp-tools/implement.md @@ -0,0 +1,173 @@ +# Implement — 基于 WebMCP 暴露站点 agent 工具 + +执行顺序按依赖排列:**基础设施 → 索引 → 工具 → 挂载 → 验证**。 +每个 Step 结束时代码应处于可构建状态。 + +--- + +## Step 1 · 类型与结果工具(无依赖) + +- [ ] `src/lib/webmcp/types.ts` — `declare global { interface Document { modelContext?: ModelContext } }`, + 含 `ToolDefinition`、`ToolResult`、`RegisterToolOptions` 最小声明 +- [ ] `src/lib/webmcp/result.ts` — `ok(text)` / `okJson(data)` / `err(text)` 三个构造器, + 统一产出 `{ content: [{ type: 'text', text }], isError? }` + +**验证**:`pnpm check` + +--- + +## Step 2 · Markdown 纯文本化 + +- [ ] `src/lib/markdown-text.ts` — `markdownToText(md: string): string` +- [ ] 剥离顺序须为:围栏代码块 → 行内代码 → 图片 → 链接(保留文本)→ HTML 标签 → + 标题/引用/列表标记 → 强调符号 → 折叠空白 +- [ ] 加 `// Reason:` 注释说明顺序为何重要(先围栏后行内,否则代码块内的反引号会错配) + +**验证**:`pnpm check` + +--- + +## Step 3 · 构建期搜索索引 + +- [ ] `src/pages/search-index.json.ts` — `GET` 返回 `SearchIndex` JSON +- [ ] 数据源必须走 `getBlogPosts()` / `getLeetcodePosts()`(继承 `display` 过滤与排序) +- [ ] 路径必须走 `postHref()`(避免与路由脱节) +- [ ] `Content-Type: application/json; charset=utf-8` + +**验证**: +```bash +pnpm build && ls -la dist/search-index.json +node -e "const d=require('./dist/search-index.json');console.log(d.posts.length, d.posts.map(p=>p.href).slice(0,5))" +``` +预期:`posts.length` 等于 blogs + leetcode 中 `display !== false` 的总数,`href` 形如 `/posts/xxx`、`/leetcode/xxx`。 + +> **Review gate 1**:确认索引条目数正确,且 `text` 字段非空、不含残留 markdown 语法。 +> +> 实测基线:**15 条**(6 blogs + 9 leetcode)。`src/content/` 下共 16 个文件, +> `toy-record.md` 带 `display: false` 被正确排除——这正是 PRD R2 要求的隐藏语义。 +> 注意 `posts/[...slug].astro` 的 `getStaticPaths` 用的是未过滤的 `getCollection`, +> 所以隐藏文章**仍会生成页面**,只是不进索引、不出现在列表页。 + +--- + +## Step 4 · 索引懒加载与检索 + +- [ ] `src/lib/webmcp/search-index.ts` + - 模块级 `let cache: Promise | null = null`,`loadIndex()` 单次 fetch + - `searchPosts(query, { collection, limit })` — 按 design §3.1 加权打分 + - `findPost(pathOrSlug)` — 同时接受 `/posts/x`、`posts/x`、`x` + - `buildSnippet(text, term)` — 首个命中前后各 60 字符 + +**验证**:`pnpm check` + +--- + +## Step 5 · 页面上下文推断 + +- [ ] `src/lib/webmcp/page-context.ts` — `getPageContext()` 返回 + `{ path, collection, slug, title, hasComments, sections }` + - `collection`:`/posts/*` → `blogs`,`/leetcode/*` → `leetcode`,否则 `null` + - `slug` / `title`:优先读 `[data-comments]` 的 `data-slug` / `data-title`,回退到 pathname 末段与 `document.title` + - `sections`:`document.querySelectorAll('.prose h2[id], .prose h3[id], .prose h4[id]')` → `{ id, text, depth }[]` + +**验证**:`pnpm check` + +--- + +## Step 6 · 内容工具 + +- [ ] `src/lib/webmcp/tools/content.ts` + - `search-posts` — schema 含 `query`(required) / `collection` enum / `limit` 1-20 default 5 + - `get-post` — `path`(required);未命中返回 `err('未找到文章:…')`,**不抛异常** + +**验证**:`pnpm check` + +--- + +## Step 7 · 导航工具 + +- [ ] `src/lib/webmcp/tools/navigation.ts` + - `get-page-context` — 空 schema,返回 `okJson(getPageContext())` + - `navigate-to-post` — 按 design §3.2 五步校验;拒绝时返回 `err(...)` + - `goto-section` — 对 `sections` 做大小写不敏感的包含匹配;命中则 `scrollIntoView({behavior:'smooth'})` 并更新 `location.hash` +- [ ] 白名单校验单独抽成 `isAllowedInternalPath(path)` 函数,便于后续复核 + +**验证**:`pnpm check` + +> **Review gate 2**:人工复核 `isAllowedInternalPath` —— 逐条确认 +> `https://evil.example`、`//evil.example`、`/keystatic`、`/api/comments`、`javascript:alert(1)` +> 全部被拒绝。 + +--- + +## Step 8 · 评论工具 + +- [ ] `src/lib/webmcp/tools/comments.ts` + - `list-comments` — `GET /api/comments?slug=`,`credentials:'same-origin'`; + `configured === false` 时返回「评论功能未配置」 + - `check-comment-auth` — `GET /api/comments/me` + - `draft-comment` — 严格按 design §3.3 六步执行 +- [ ] **禁止**出现 `form.submit()`、`submitButton.click()`、`requestSubmit()` +- [ ] 加 `// Reason:` 注释说明为何只填草稿不提交 + +**验证**:`pnpm check` + `grep -nE 'requestSubmit|\.submit\(|submit.*\.click\(' src/lib/webmcp/` 应无输出 + +--- + +## Step 9 · 注册入口与挂载 + +- [ ] `src/lib/webmcp/register.ts` — 特性检测 + `AbortController` + `pagehide` 清理 + `try/catch` 静默 +- [ ] `src/components/WebMcp.astro` — 首屏只做特性检测;命中后 `import('@/lib/webmcp/register')` 动态加载 + (Reason: WebMCP 尚在 Origin Trial,绝大多数访客没有宿主,不应为他们下载工具代码) +- [ ] `src/layouts/BaseLayout.astro` — + head 内条件渲染 origin-trial meta;body 末尾(`` 前)放 `` +- [ ] `.env.example` — 追加 `PUBLIC_WEBMCP_ORIGIN_TRIAL_TOKEN=` 及注释说明去哪注册 + +**验证**: +```bash +pnpm check && pnpm build +grep -c 'origin-trial' dist/index.html # 未配置 token 时应为 0 +``` + +--- + +## Step 10 · 全量验收 + +```bash +pnpm check # 无新增 TS / astro 错误 +pnpm build # 构建通过 +ls dist/search-index.json # 索引存在 +grep -rn 'origin-trial' dist/*.html | head # 未配置 token 时无输出 +``` + +浏览器人工验证(`pnpm dev`): + +- [ ] 首页 / 文章页 / leetcode 页正常渲染,控制台无新增报错 +- [ ] 评论区、主题切换、TOC、Mermaid、Presence 行为与改造前一致 +- [ ] DevTools Network:普通浏览时**不应**出现 `search-index.json` 请求 + +WebMCP 功能验证(需 Chrome 149+ 带 flag,或 ChatGPT Desktop): + +- [ ] `(await document.modelContext.getTools()).map(t => t.name)` 列出 8 个工具 +- [ ] `search-posts({ query: 'vue' })` 命中 `vue3-contribution` +- [ ] `navigate-to-post({ path: 'https://evil.example' })` 被拒 +- [ ] `draft-comment({ text: '测试' })` 后文本入框、页面滚动到评论区、**GitHub 无新增评论** + +> **Review gate 3**:`draft-comment` 未提交表单这一条必须实测确认, +> 而不是仅凭代码阅读判断。 + +--- + +## 回滚点 + +| 触发 | 操作 | +|---|---| +| 任一 Step 构建失败 | 该 Step 为独立新文件,`git checkout -- ` 即可 | +| 上线后发现回归 | 从 `BaseLayout.astro` 删除 `` 与 origin-trial meta 两行 → 功能完全禁用,其余文件成为死代码不影响运行 | +| 索引体积过大 | 在 `search-index.json.ts` 中对 `text` 截断(如每篇上限 8000 字符) | + +## 完成后 + +- [ ] 按 Trellis 3.3 更新 spec(WebMCP 工具契约与安全边界值得沉淀到 `.trellis/spec/frontend/`) +- [ ] 按 Trellis 3.4 提交 +- [ ] 提醒 moka 完成 PRD 中的 Origin Trial 注册前置项 diff --git a/.trellis/tasks/08-28-webmcp-tools/prd.md b/.trellis/tasks/08-28-webmcp-tools/prd.md new file mode 100644 index 0000000..d2027f3 --- /dev/null +++ b/.trellis/tasks/08-28-webmcp-tools/prd.md @@ -0,0 +1,105 @@ +# 基于 WebMCP 暴露站点 agent 工具 + +## Goal + +让 razet.me 在支持 WebMCP 的宿主(Chrome 149+ / Edge 150+ Origin Trial、ChatGPT Desktop、Brave Leo)中, +把**内容检索**、**站点导航**、**评论读写**以标准 tool 形式注册到 `document.modelContext`, +使 agent 能直接调用而不再依赖 DOM 抓取与模拟点击。 + +目标场景(用户对浏览器 agent 说的话): + +- 「这个站里有讲 Vue3 贡献的文章吗?打开它。」→ `search-posts` → `navigate-to-post` +- 「总结一下当前这篇文章。」→ `get-page-context` → `get-post` +- 「这篇的评论都在说什么?」→ `list-comments` +- 「帮我写一条评论说……」→ `check-comment-auth` → `draft-comment`(填入输入框,**由用户点提交**) + +## Scope + +### In scope + +- 内容检索与摘要(跨 blogs / leetcode 两个 collection) +- 站点导航(跳转文章、跳转当前页标题锚点) +- 评论读取 + 评论草稿写入(人工确认后提交) +- 特性检测与渐进增强、Origin Trial token 接入 + +### Out of scope + +- 站内自建 AI 助手 UI(不做 `getTools()` / `executeTool()` 消费侧) +- 阅读体验控制类工具(主题切换、代码块/Mermaid 控制) +- Keystatic 后台(`/keystatic/*`、`/api/keystatic/*`)相关工具——后台是作者私有区域,不对 agent 暴露 +- 声明式 `
` 工具合成(本期只用命令式 API) + +## Requirements + +### R1 渐进增强与零回归 + +- `document.modelContext` 不存在时**完全静默 no-op**,不报错、不影响任何现有功能 +- WebMCP 脚本不得阻塞首屏;搜索索引**按需懒加载**,不进入初始 bundle +- 现有页面、评论、Presence、Keystatic、主题切换行为完全不变 + +### R2 内容检索工具 + +- `search-posts`:按关键词检索,支持 `collection` 与 `limit` 过滤,返回结构化命中列表(标题 / 路径 / 日期 / tag / 摘要 / 匹配片段) +- `get-post`:按站内路径或 slug 取单篇文章的**纯文本正文** + frontmatter,供 agent 摘要 +- 检索数据来源为构建期预生成的静态索引,**不新增运行时搜索依赖** +- 索引须遵守 `display: false` 的隐藏语义(与 `src/lib/posts.ts` 的 `isVisible` 一致) + +### R3 站点导航工具 + +- `navigate-to-post`:跳转站内路径。**只接受同源站内路径**,白名单前缀 `/`、`/posts`、`/posts/*`、`/leetcode`、`/leetcode/*` +- `goto-section`:按标题文本跳转当前页锚点,复用现有 `TableOfContents` 生成的 heading id +- `get-page-context`:返回当前页 collection / slug / 标题 / 是否有评论区,让 agent 知道自己在哪 + +### R4 评论工具(读自由、写需人工确认) + +- `list-comments`:读取指定(默认当前页)文章评论,走现有 `GET /api/comments?slug=` +- `check-comment-auth`:走现有 `GET /api/comments/me` 返回登录态,未登录时返回登录引导 +- `draft-comment`:**只把文本填入现有评论输入框**(`[data-comments-input]`)、派发 input 事件、滚动并聚焦, + 返回「已填入草稿,请用户确认后点击提交」。**绝不自动提交表单。** + - Reason: 发评论是不可逆的对外发布行为,且会以用户 GitHub 身份写入 Issue。 + WebMCP explainer 明确的 human-in-the-loop 原则要求这类副作用保留用户最终确认权。 +- 不注册任何会泄露 session cookie、GitHub token 或 Keystatic 凭据的工具 + +### R5 Origin Trial 接入 + +- 支持通过环境变量 `PUBLIC_WEBMCP_ORIGIN_TRIAL_TOKEN` 注入 `` +- 未配置该变量时**不输出 meta 标签**,站点照常工作 + +## Constraints + +| 约束 | 影响 | +|---|---| +| `output: 'static'` + Netlify adapter | 工具注册只能在客户端脚本;索引须构建期生成 | +| WebMCP 处于 Origin Trial(Chrome 149 / Edge 150),Firefox / Safari 无实现 | 必须特性检测;无法在多数浏览器验证,需接受可观测性有限 | +| Astro 是 MPA,每次导航整页刷新 | 每页加载重新注册工具;用 `AbortController` + `pagehide` 清理 | +| 不引入运行时搜索库(Fuse/Pagefind 等) | 自写加权词频打分;内容量(约 16 篇)足以支撑 | + +## Prerequisites(外部依赖,需 moka 本人操作) + +- [ ] 到 [Chrome Origin Trials](https://developer.chrome.com/origintrials) 为 `https://razet.me` 注册 WebMCP trial,取得 token +- [ ] 将 token 配置为 Netlify 环境变量 `PUBLIC_WEBMCP_ORIGIN_TRIAL_TOKEN` +- [ ] (可选)Edge 同名 trial 单独注册 + +> 未完成上述步骤时,代码仍可合并且不影响站点;WebMCP 只在带 `--enable-features` 标志的本地 Chrome 或 ChatGPT Desktop 中生效。 + +## Acceptance Criteria + +- [ ] `pnpm build` 与 `pnpm check` 通过,无新增 TS / astro check 错误 +- [ ] 构建产物包含 `dist/search-index.json`,内容覆盖全部 `display !== false` 的 blogs + leetcode 文章 +- [ ] 在**不支持** WebMCP 的浏览器(如当前 Safari)中打开首页 / 文章页 / 评论区,功能与改造前一致,控制台无新增报错 +- [ ] 在支持 WebMCP 的宿主中 `await document.modelContext.getTools()` 能列出全部 8 个工具,且每个都有 `description` 与合法 `inputSchema` +- [ ] `search-posts({ query: 'vue' })` 返回含 `vue3-contribution` 的命中项 +- [ ] `get-post` 对已存在文章返回非空纯文本正文;对不存在的 slug 返回明确的错误文本而非抛异常 +- [ ] `navigate-to-post` 对 `https://evil.example` 与 `/keystatic` 等非白名单输入**拒绝跳转**并返回错误文本 +- [ ] `list-comments` 在评论未配置的环境下返回「未配置」而非报错 +- [ ] `draft-comment` 调用后:文本出现在评论框中、页面滚动到评论区、**表单未被提交**(GitHub 无新增评论) +- [ ] 未登录时 `draft-comment` 或 `check-comment-auth` 返回可读的登录引导文本 +- [ ] 未配置 `PUBLIC_WEBMCP_ORIGIN_TRIAL_TOKEN` 时,HTML 中不含 `origin-trial` meta + +## Notes + +- WebMCP 一手资料:(explainer)、 + (浏览器支持) +- API 形态已核实为 `document.modelContext.registerTool(tool, { signal, exposedTo })`, + 配套 `getTools()` / `executeTool()` / `toolchange` 事件 +- 本期不使用 `exposedTo`——不向任何跨源 iframe 暴露工具 diff --git a/.trellis/tasks/08-28-webmcp-tools/task.json b/.trellis/tasks/08-28-webmcp-tools/task.json new file mode 100644 index 0000000..5303a25 --- /dev/null +++ b/.trellis/tasks/08-28-webmcp-tools/task.json @@ -0,0 +1,26 @@ +{ + "id": "webmcp-tools", + "name": "webmcp-tools", + "title": "基于 WebMCP 暴露站点 agent 工具", + "description": "", + "status": "in_progress", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "moka", + "assignee": "moka", + "createdAt": "2026-08-28", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/.trellis/tasks/00-bootstrap-guidelines/prd.md b/.trellis/tasks/archive/2026-08/00-bootstrap-guidelines/prd.md similarity index 100% rename from .trellis/tasks/00-bootstrap-guidelines/prd.md rename to .trellis/tasks/archive/2026-08/00-bootstrap-guidelines/prd.md diff --git a/.trellis/tasks/00-bootstrap-guidelines/task.json b/.trellis/tasks/archive/2026-08/00-bootstrap-guidelines/task.json similarity index 92% rename from .trellis/tasks/00-bootstrap-guidelines/task.json rename to .trellis/tasks/archive/2026-08/00-bootstrap-guidelines/task.json index 6fe58c0..4aa83eb 100644 --- a/.trellis/tasks/00-bootstrap-guidelines/task.json +++ b/.trellis/tasks/archive/2026-08/00-bootstrap-guidelines/task.json @@ -3,7 +3,7 @@ "name": "00-bootstrap-guidelines", "title": "Bootstrap Guidelines", "description": "Fill in project development guidelines for AI agents", - "status": "in_progress", + "status": "completed", "dev_type": "docs", "scope": null, "package": null, @@ -11,7 +11,7 @@ "creator": "priority", "assignee": "priority", "createdAt": "2026-07-23", - "completedAt": null, + "completedAt": "2026-08-27", "branch": null, "base_branch": null, "worktree_path": null, diff --git a/.trellis/tasks/07-24-migrate-astro-yohaku/check.jsonl b/.trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/check.jsonl similarity index 100% rename from .trellis/tasks/07-24-migrate-astro-yohaku/check.jsonl rename to .trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/check.jsonl diff --git a/.trellis/tasks/07-24-admin-keystatic-umami/design.md b/.trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/design.md similarity index 100% rename from .trellis/tasks/07-24-admin-keystatic-umami/design.md rename to .trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/design.md diff --git a/.trellis/tasks/07-24-migrate-astro-yohaku/implement.jsonl b/.trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/implement.jsonl similarity index 100% rename from .trellis/tasks/07-24-migrate-astro-yohaku/implement.jsonl rename to .trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/implement.jsonl diff --git a/.trellis/tasks/07-24-admin-keystatic-umami/prd.md b/.trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/prd.md similarity index 100% rename from .trellis/tasks/07-24-admin-keystatic-umami/prd.md rename to .trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/prd.md diff --git a/.trellis/tasks/07-24-admin-keystatic-umami/task.json b/.trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/task.json similarity index 90% rename from .trellis/tasks/07-24-admin-keystatic-umami/task.json rename to .trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/task.json index 37a6587..60454bb 100644 --- a/.trellis/tasks/07-24-admin-keystatic-umami/task.json +++ b/.trellis/tasks/archive/2026-08/07-24-admin-keystatic-umami/task.json @@ -3,7 +3,7 @@ "name": "admin-keystatic-umami", "title": "Admin: Keystatic CMS + Umami analytics", "description": "", - "status": "planning", + "status": "completed", "dev_type": null, "scope": null, "package": null, @@ -11,7 +11,7 @@ "creator": "priority", "assignee": "priority", "createdAt": "2026-07-24", - "completedAt": null, + "completedAt": "2026-08-27", "branch": null, "base_branch": "main", "worktree_path": null, diff --git a/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/check.jsonl b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/check.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/check.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/07-24-migrate-astro-yohaku/design.md b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/design.md similarity index 100% rename from .trellis/tasks/07-24-migrate-astro-yohaku/design.md rename to .trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/design.md diff --git a/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/implement.jsonl b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/implement.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/implement.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/07-24-migrate-astro-yohaku/implement.md b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/implement.md similarity index 100% rename from .trellis/tasks/07-24-migrate-astro-yohaku/implement.md rename to .trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/implement.md diff --git a/.trellis/tasks/07-24-migrate-astro-yohaku/prd.md b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/prd.md similarity index 100% rename from .trellis/tasks/07-24-migrate-astro-yohaku/prd.md rename to .trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/prd.md diff --git a/.trellis/tasks/07-24-migrate-astro-yohaku/task.json b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/task.json similarity index 90% rename from .trellis/tasks/07-24-migrate-astro-yohaku/task.json rename to .trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/task.json index 8fe453f..3295ed7 100644 --- a/.trellis/tasks/07-24-migrate-astro-yohaku/task.json +++ b/.trellis/tasks/archive/2026-08/07-24-migrate-astro-yohaku/task.json @@ -3,7 +3,7 @@ "name": "migrate-astro-yohaku", "title": "Migrate blog to Astro with Yohaku-inspired UI", "description": "", - "status": "in_progress", + "status": "completed", "dev_type": null, "scope": null, "package": null, @@ -11,7 +11,7 @@ "creator": "priority", "assignee": "priority", "createdAt": "2026-07-24", - "completedAt": null, + "completedAt": "2026-08-27", "branch": null, "base_branch": "main", "worktree_path": null, diff --git a/src/components/WebMcp.astro b/src/components/WebMcp.astro new file mode 100644 index 0000000..79442a3 --- /dev/null +++ b/src/components/WebMcp.astro @@ -0,0 +1,22 @@ +--- +/** + * Exposes this site's capabilities as WebMCP tools for browser-integrated AI agents. + * + * Progressive enhancement only: in hosts without `document.modelContext` nothing beyond + * the feature check is downloaded. The search index is fetched later still — on the first + * content tool call — so ordinary visitors pay for none of this. + */ +--- + + diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 73c9ab9..bcfbe49 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -8,6 +8,7 @@ import 'remark-github-alerts/styles/github-colors-dark-class.css' import Header from '@/components/Header.astro' import Footer from '@/components/Footer.astro' import Umami from '@/components/Umami.astro' +import WebMcp from '@/components/WebMcp.astro' import { site } from '@/lib/site' interface Props { @@ -24,6 +25,10 @@ const { } = Astro.props const pageTitle = title === site.title ? title : `${title} · ${site.name}` + +// WebMCP ships behind an origin trial (Chrome 149 / Edge 150). Absent token → no meta tag, +// and the tools simply stay dormant. Register at https://developer.chrome.com/origintrials +const originTrialToken = import.meta.env.PUBLIC_WEBMCP_ORIGIN_TRIAL_TOKEN --- @@ -32,6 +37,7 @@ const pageTitle = title === site.title ? title : `${title} · ${site.name}` + {originTrialToken && } @@ -68,5 +74,6 @@ const pageTitle = title === site.title ? title : `${title} · ${site.name}`