Преглед на файлове

feat: fmode-code-agent 初版 — 纯 JS 网页版 Bot 智能体开发规则技能

规则(.claude/rules/,8 篇):
- agent-architecture: Agent/MCP/Skill 三层、图编排 vs Re-act、State 放什么、可观测性
- agent-mcp-skill: 统一 MCP 契约、多 MCP 命名空间、Worker 铁律、Skill 注册/调用、隔离
- no-build-langgraph: LangGraph.js 浏览器无构建配方(importmap + 3 替身 + 6 坑)
- llm-gateway: OpenAI 兼容、usage 真值、静默忽略参数陷阱、多模态拼接
- measurement: token 归因、输入/输出比例、框架调度 vs IO 计时
- agent-ui-bridge: 事件流/阶段/进度、失败如实说明、移动端
- deployment: 无构建部署 + 自动部署(hook/CI/watch) + 验真
- pitfalls: 24 条血泪坑位

技能(skills/fmode-code-agent/):
- scaffold.mjs 生成最小智能体骨架(index.html+importmap+lib/+mcp/)
- install-rules.mjs / install.mjs 规则与技能安装器
- smoke.mjs 自检(含 scaffold 产物语法 + MCP 协议功能测试)
- SKILL.md / package.json / skill-package-manifest.json / README / LICENSE(MPL-2.0)

自检通过。
FmodeAgent преди 7 часа
ревизия
458bba9348

+ 70 - 0
.claude/rules/agent-architecture.md

@@ -0,0 +1,70 @@
+# 智能体分层架构(Agent / MCP / Skill)
+
+## 一、三层职责(边界即纪律)
+
+| 层 | 职责 | 不该做的事 |
+|----|------|-----------|
+| **Agent**(决策) | 决定「这轮调不调工具、调哪个、怎么串」 | 不直接 import Skill;不含协议细节 |
+| **MCP**(协议) | 把工具暴露成统一协议;把调用转成执行 | 不做业务编排;不持有凭据(浏览器场景) |
+| **Skill**(实现) | 真正干活(出图/转写/分析),纯能力 | 不假设调用方是谁;不藏全局态 |
+
+**换框架不动 MCP/Skill** 是分层的验收标准:
+本项目把 Agent 从 PageAgent 换到 LangGraph,`design-mcp.js` 与 `image-service.js` **零改动**。
+
+## 二、Agent 侧:图编排 vs Re-act
+
+| 维度 | 显式图(LangGraph) | Re-act 循环 |
+|------|--------------------|-------------|
+| 可观测 | 节点轨迹可展示、可断点、可回滚 | 只能看到「思考中…」 |
+| Token 控制 | 可在节点里**预筛工具**、**按需检索** 后再进 LLM | 全量工具 schema + 线性历史,膨胀 |
+| 失败分支 | 「没出图」是**显式分支** | 只能事后靠猜兜底 |
+| 成本 | 需 importmap + 替身(浏览器) | 轻 |
+
+**建议**:要 token 可控 + 可观测 → 显式图;只是简单问答 → Re-act 足够。
+
+### 2.1 节点切分原则(本项目 7 节点实例)
+
+```
+route(本地预筛,零 LLM/零 token)
+  → recall(检索式记忆,按需召回)
+  → plan(唯一一次"决定做什么"的 LLM)
+  → invoke(执行工具)
+  → assert(断言"真出图了吗")
+  → reflect(给一句说明,不带工具,避免它再调一次)
+  → respond(收尾)
+```
+
+- **不调 LLM 的节点放最前**(route/recall)→ 省 token 且几乎不花时间。
+- **工具产物直接进 State**,不做「消息序列化往返」。
+- **独立 assert 节点**让「工具说了要干活但没干成」成为**显式分支**。
+
+### 2.2 State 放什么(关键)
+
+**State 只存索引与摘要**,大对象(图片 URL、历史正文)放 service / IndexedDB。
+
+- 好处:checkpoint 序列化快、快照不膨胀、State 可安全进日志。
+- **凭据不进 State**:State 会被快照序列化 → token 进 State 等于进快照。运行时上下文
+  挂在 `config.configurable.__dc`(**不是**闭包,闭包会把首轮 token 固化进编译产物)。
+- ⚠️ LangGraph 节点签名是 `node(state, config)`,**第二参是 RunnableConfig 不是你的 ctx**(实测坑)。
+
+## 三、MCP 侧
+
+见 [agent-mcp-skill.md](./agent-mcp-skill.md)(契约、命名空间、Worker 约束、凭据纪律)。
+
+## 四、可观测性(一套埋点,三处复用)
+
+无论几个 MCP/Skill,**口径必须统一**:
+
+- **Token**:每次 LLM 调用记 system/tools/history/retrieved 分项;有 `usage` 真值**以真值为准**。
+- **输入/输出比例**:分别记 prompt_tokens / completion_tokens。
+- **耗时**:**框架调度**与**IO(LLM/网络)分开记**。
+  ⚠️ 实测踩坑:用 `t0()` 包住整个 `await LLM` 会把 LLM 的 1.4s 算进「框架开销」,
+  占比虚高到 96%。正确做法:`await` 期间**暂停框架计时**,只把「发起调用前后」算框架的。
+
+详见 [measurement.md](./measurement.md)。
+
+## 五、降级与失败隔离
+
+- **每个外部依赖都有降级路径**:本项目 MCP 起不来 → 自动走「直连 Skill 出图」,用户总能拿到图。
+- **失败如实说明,不静默兜底**:工具没出图就明说「这次没出图」,不偷偷换条路再出一张(用户会困惑)。
+- **一个组件失败不拖垮整体**:一个 MCP 挂 → 其它 MCP 与 Agent 仍可用。

+ 87 - 0
.claude/rules/agent-mcp-skill.md

@@ -0,0 +1,87 @@
+# MCP / Skill 抽象规则
+
+> 细则展开见 designer-chat 仓 `docs/agent-mcp-skill-rules.md`(同源,本项目实测版)。
+> 本篇是**可复用标准**。
+
+## 一、MCP 客户端统一契约(强制)
+
+任何 MCP 必须有一个**稳定契约模块**,对外只暴露固定方法:
+
+```js
+start(opts) → { ok, tools?, error? }   // 幂等
+listTools() → ToolSpec[]               // MCP JSON Schema
+callTool(name, args) → { ok, result?, error? }
+state → signal('idle'|'starting'|'ready'|'error')
+transport → 'worker'|'iframe'|'http'|'inproc'
+```
+
+- **幂等 start**:调用 N 次只起一份资源(ready 时只挂监听不重启)。
+- **惰性**:无凭据时不白起 Worker。
+- **可观测**:`state` 可订阅,UI 靠它显示连接状态。
+
+## 二、多 MCP 管理(规范建议;本项目只接了 1 个 MCP)
+
+| 关注点 | 规则 |
+|--------|------|
+| 注册表 | `Map<mcpId, McpHandle>`,每个 MCP 有稳定 `id` |
+| 命名空间 | 全名 = `${mcpId}.${toolName}`,避免跨 MCP 同名冲突 |
+| 生命周期 | 各自独立 start/stop,一个挂了不拖垮其它 |
+| 聚合视图 | Agent 只对「所有**就绪** MCP 的工具并集」编程 |
+| 隔离 | 凭据/状态互不可见 |
+
+**冲突优先级**:`显式注册 > 内置 > 后启动`,且**启动时告警**,不静默覆盖。
+
+## 三、Worker 铁律(浏览器端 MCP,实测血泪)
+
+- **module worker 不支持 importmap** → Worker 内**只能 import 相对路径纯 JS**;
+  裸标识符(`zod/v4`、`@page-agent/core`)会**直接加载失败**。
+- 需要 schema 时写两份:**Worker 侧 JSON Schema** + **主线程侧 zod**,
+  启动时 `assertSpecParity()` 断言参数键一致,**防漂移**。
+- **凭据永不进 Worker**:出图/调模型这类要凭据的动作**在主线程**执行,
+  Worker 只做协议与编排(本项目用「反向回调主线程」实现)。
+
+## 四、Skill 规范
+
+### 4.1 入口
+
+```js
+async function doThing(params, { token, signal }) → result
+```
+
+- 凭据**显式传入**(不读环境/全局)→ 便于多租户与 mock 单测。
+- **无隐藏全局态**;进度/缓存放调用方(service 层)。
+- **结构化错误**(含 `status`),由上层翻译成人话。
+
+### 4.2 注册到 MCP
+
+```js
+server.register({
+  name, title, description,
+  inputSchema,     // MCP JSON Schema
+  annotations,     // readOnlyHint / destructiveHint / openWorldHint
+  defaultMode,     // (ctx) => 默认值
+  buildBrief,      // (args) => 人类可读描述(纯函数)
+  handler,         // async (args, ctx) => MCP result
+});
+```
+
+- **一个 Skill 可映射多个工具**(差异在「提示词前缀 + 画幅 + 风格约束」,不是换模型)。
+- **`buildBrief` 必须纯**(Worker 内可用,无 DOM/网络)。
+
+## 五、多 Skill 加载/命名/冲突/隔离(设计)
+
+- **加载**:声明式 `skills.manifest.json`(启用项 + 版本 + 对应 MCP);可懒加载。
+- **命名**:包 `skill-<capability>`;工具 `<verb>_<object>`;全名 `<mcpId>.<toolName>`。
+- **冲突**:同名工具用命名空间规避,仍冲突则**启动报错**;同能力不同实现**并列注册**
+  (由 Agent 按语义/成本选),不互相覆盖。
+- **隔离**:状态/失败/凭据三重隔离 —— 一个 Skill 失败返回 `isError`,不抛穿到 Agent。
+
+## 六、检查清单
+
+- [ ] 契约模块齐全(start 幂等 / listTools / callTool / state)?
+- [ ] 工具名带 MCP 命名空间?无跨 MCP 碰撞?
+- [ ] Worker 依赖链只含相对纯 JS?
+- [ ] 凭据不进 Worker?错误剔除凭据?
+- [ ] Skill 凭据显式传入、无隐藏全局态?
+- [ ] Skill 失败返回 `isError` 而非抛穿?
+- [ ] 一个 MCP 挂掉时其余仍可用?

+ 64 - 0
.claude/rules/agent-ui-bridge.md

@@ -0,0 +1,64 @@
+# 智能体 ↔ UI 桥接
+
+> 目标:把「智能体在干什么」变成用户**看得见、能打断、失败有交代**的体验。
+> 参照 fmode-code-lit 的分层:**组件只渲染,业务在 service,智能体在 modules**。
+
+---
+
+## 一、接口稳定:换 Agent 框架不改 UI
+
+**服务对外接口与实现解耦**是本项目一条纪律:
+
+- `agentService` 暴露 `messages / status / stage / submit / answer / stop / retryTask / regenerate …`。
+- 换框架(PageAgent → LangGraph)时**接口一字不改**,故 `components/` 一行不用动。
+
+**做法**:modules 里的具体框架实现(`graph.js`)只被 `agent-service.js` 引用;
+组件只认 service。
+
+---
+
+## 二、阶段与节点轨迹(可观测性上屏)
+
+- 把「框架在跑哪个节点」**实时**打成一条轨迹(如 `route → recall → plan → invoke → assert`)。
+- 节点 → UI 阶段映射(例):`plan→thinking`、`invoke→calling`、`reflect→generating`、`respond→done`。
+- Re-act 循环下用户只能看到「思考中…」;**显式图能把过程展示出来**——这是选图的理由之一。
+
+```js
+// onNode 回调把节点名推进 trace signal,UI 订阅它
+const onNode = (name) => { trace.push({ name, at: Date.now() }); this.#nodeTrace.set([...trace]); };
+```
+
+---
+
+## 三、进度:没有真实进度时用时间估算
+
+- 出图 20~60s、无进度回调 → 用**时间估算**给「在动」的反馈
+  (0→90% 渐进收敛,完成置 100)。**不要**假装精确。
+- 计时**以真实起点为准**(用图片记录的 `at`,而非 UI 阶段切换时刻)。
+
+---
+
+## 四、失败如实说明(**不静默兜底**)
+
+- 工具宣称要出图但**没出图** → 明确告诉用户「这次没出图,可以重试」,
+  **不要**偷偷换条路再出一张(用户会困惑「为什么和我说的话不一样」)。
+- 错误翻译成**可行动**的话(`friendlyError`):401→重新登录;402→余额不足;
+  429→稍后再试;超时→重试。
+- **兜底要有边界**:MCP 起不来时降级「直连出图」是**显式降级**(会提前告知),不是静默替换。
+
+---
+
+## 五、凭据与安全(UI 侧)
+
+- 登录后凭据只存 `localStorage`(可退出清除);手动填 Key 默认只存内存。
+- **不打日志**;错误信息出界前**剔除**疑似 key。
+- **token 永不进 MCP Worker**:出图在主线程执行,Worker 只传「工具名 + 参数 + 结果」。
+
+---
+
+## 六、移动端(对话类 UI 必处理)
+
+- `100dvh` + `visualViewport` 监听:软键盘弹出时整窗上推,输入区不被遮挡。
+- 安全区 `env(safe-area-inset-*)`;输入框字号 **≥16px**(iOS 聚焦不自动放大)。
+- 回车键:桌面 Enter 发送;**移动端** Enter 换行(用发送按钮)。
+- 无横向溢出:`min-width:0` + `overflow-wrap:anywhere`。

+ 81 - 0
.claude/rules/deployment.md

@@ -0,0 +1,81 @@
+# 部署(无构建 + 自动部署)
+
+> 前端无构建部署的**完整规范**见 **fmode-code-lit** 规则仓 `.claude/rules/deployment.md`
+> 与 `routing-best-practices.md`(Repose 依赖 + OBS/CDN + History 回退)。
+> 本篇只讲**智能体项目特有**的部分:自动部署流水线 + 验真。
+
+---
+
+## 一、无构建部署速记
+
+- `src/` **即产物**(无 build 步骤),`obsutil sync` 直传 OBS,`public-read`。
+- `<base href>` == OBS 前缀 == importmap 业务前缀(**同一变量派生 + 硬校验**)。
+- `index.html` **先删后传** + `Cache-Control: no-cache`(防 sync 按 size/mtime Skip)。
+- CDN 刷新:`type:'directory'`(URL 以 `/` 结尾),**域名对号入座 domain_id**,
+  读 `refresh_task`(**字符串 ID**,不是对象),失败**退出非 0**。
+- 依赖(lit / langgraph 全家桶)走 **importmap**,可托管在 **Repose**,**不落业务仓**。
+
+---
+
+## 二、自动部署(本项目实测形态)
+
+### 2.1 三层脚本,单一发布入口
+
+| 脚本 | 职责 |
+|------|------|
+| `scripts/deploy.sh` | **只负责发**:体检 → 上传 OBS → 刷 CDN → 冒烟。所有站点/前缀/域名**参数化**。 |
+| `scripts/auto-deploy.sh` | **编排**:`体检 → deploy.sh → verify-deploy.sh`,支持 `--watch` 常驻。 |
+| `scripts/verify-deploy.sh` | **验真**:关键文件可达 + 深链接回退 + 模块图爬取。 |
+| `scripts/install-hooks.sh` | 装 git hook 触发自动部署。 |
+
+**关键**:`auto-deploy.sh` **不改** deploy.sh 的发布逻辑,只在其前后加「该不该发/发没发成功」
+—— 保持**单一发布入口**,避免两套逻辑漂移。
+
+### 2.2 触发方式(三选,可叠加)
+
+| 方式 | 命令 | 取舍 |
+|------|------|------|
+| **pre-push hook**(推荐) | `bash scripts/install-hooks.sh` | push 才发,避开 WIP 提交;失败**阻断推送** |
+| post-commit hook | `install-hooks.sh --on-commit` | 一提交就发;适合小团队 |
+| **watch** | `auto-deploy.sh --watch` | 常驻轮询工作区指纹,变了就发;适合本地开发 |
+| CI | 在 CI 里跑 `auto-deploy.sh` | 多人都推 → CI 发,避免每人本地都发 |
+
+> **为什么默认 pre-push 而非 post-commit**:commit 频繁且含 WIP,每提交就发会污染生产;
+> push 才是「要分享」的信号。跳过:`git push --no-verify`;临时禁:`DEPLOY_ENABLED=0`。
+
+### 2.3 多人协作要点
+
+- **只让一个人/一处发**:要么 CI 发,要么约定「只有 main 分支推的人发」(hook 已按分支过滤)。
+- **域名/前缀参数化**(`SITE_DOMAINS` / `PROJECT`),不要硬编码 —— 换环境不改脚本。
+- **watch 用内容指纹**(`find … | md5sum`)而非 inotify:容器里 inotify 常不可用,且跨平台。
+
+---
+
+## 三、验真:**不能只 curl 首页**
+
+`curl` 首页 200 **只证明 index.html 在**。无构建应用白屏的典型原因
+(某模块 404、MIME 错、回退没配、相对路径错一个字符),它**一个都不覆盖**。
+
+分层验收:
+
+1. **静态体检**(部署前):`node --check`;无装饰器残留;相对 import 带 `.js`;裸标识符都在 importmap。
+2. **模块图爬取**(部署后,HTTP 层):从入口跟随相对 import 走完整图,
+   断言每个文件 **200 + `Content-Type: text/javascript`**;**深链接 200 且内容为 index.html(禁 302)**。
+3. **真实浏览器**:点导航 + 刷新深链接,确认不 404、无控制台报错。
+4. **缓存层**:核对 CDN 返回的**内容是新版**(不只是状态码)——
+   `curl … | grep 新特征串`。
+
+> ⚠️ **CDN 冷边缘**:新 key 首次被边缘节点拉取**偶发 404**(回源竞争),重跑即消 ——
+> 验真脚本对新 key 首次 404 应**重试**再判失败。
+
+---
+
+## 四、checklist
+
+- [ ] `<base href>` 与部署前缀一致(同一变量派生 + 硬校验)?
+- [ ] 自动部署脚本**参数化**(域名/前缀),无硬编码?
+- [ ] pre-push / CI 触发;失败退出非 0(能阻断)?
+- [ ] 部署后跑**模块图爬取 + 深链接回退**(非只看首页)?
+- [ ] 核对线上**内容是新版**(非仅状态码)?
+- [ ] `DEPLOY_ENABLED` / `--no-verify` 等**逃生阀**存在?
+- [ ] AK/SK 只在脚本内以环境变量形态存在,**不写进文档/新文件**?

+ 83 - 0
.claude/rules/llm-gateway.md

@@ -0,0 +1,83 @@
+# LLM / 出图网关对接
+
+> 核心纪律:**参数以「实测 / 文档」为准,不编造**。同一字段名在不同网关可能语义不同,
+> 甚至**静默忽略**(返回 200 但没生效)—— 必须**验证副作用**,不能只看状态码。
+
+---
+
+## 一、OpenAI 兼容客户端(自写,不引 SDK)
+
+```js
+chatComplete({ token, model, messages, tools, signal, timeoutMs })
+  → { content, toolCalls, usage }
+```
+
+- **不引 `@langchain/openai`**:省依赖、可控、便于塞自定义字段。
+- **错误剔除凭据**:`scrub()` 把 `sk-…` / `Bearer …` 替换成 `[redacted]`。
+- **超时**:用 `AbortController`,出图类给 300s,对话类 30~45s。
+
+---
+
+## 二、`usage` 真值
+
+- chat/completions:`usage.prompt_tokens` / `usage.completion_tokens`。
+- 出图:`usage.input_tokens` / `usage.output_tokens`(+ `*_tokens_details.image_tokens`)。
+- 拿到真值就**覆盖估算**并单列展示(见 [measurement.md](./measurement.md))。
+
+---
+
+## 三、⚠️ 静默忽略参数的陷阱(血泪)
+
+### 3.1 现象
+
+某些网关对**不认识的参数**不报错,而是**静默丢弃**并照常返回 200。
+→ 你以为「参考图传进去了」,其实模型根本没看到。
+
+### 3.2 怎么识破:验证**副作用**,而非状态码
+
+本项目实测(`gpt-image-2.5` 出图):
+
+- `/v1/images/generations` 传参考图:
+
+  | 参数名 | 结果 |
+  |--------|------|
+  | `image` / `images` | 400 `Unknown parameter`(**显式**拒绝) |
+  | `image_url` / `input_image` / `image_ref` / `ref_image` / `init_image` / `reference_images` | **200 但 `input_tokens_details.image_tokens: 0`** → **图被静默忽略** |
+
+  **判定方法**:看 `image_tokens` 是否 > 0。为 0 = 图没进模型。
+
+- 参考图**唯一通道**:`/v1/images/edits`,body `images: [{ image_url: "<dataURI|URL>" }]`
+  (元素**必须是对象**;`{url}`/`{type:…}` 都被拒;multipart 经此网关不通)。
+  传 data URI 实测 `image_tokens: 1024` → 图真被消费。
+
+### 3.3 尺寸约束
+
+- `size` 的宽、高**必须能被 16 整除**,否则 400。
+- 常见错误:`1080x1920`(最口语化的「9:16」)**被拒**;竖版 9:16 用 **`1024x1792`**。
+- `size: "auto"` 通常可用(按语义自选比例)。
+
+> 原则:**任何「参数是否生效」都要设计一个可观测的副作用来验证**,
+> 不能凭「返回 200」下结论。
+
+---
+
+## 四、端点真值怎么定
+
+- **先探活**:`GET /v1/models`(无 token 应 401;能列出模型说明端点 live)。
+- **旧参考文档常失效**:本项目实测 `accounts/sendCode` 等云函数已死,以**当前实测**为准。
+- 把「活/死」的端点写进项目记忆,避免后来者重踩。
+
+---
+
+## 五、图 + 文多模态的拼接
+
+当网关没有独立的「参考图说明」字段时,「怎么用这些图」只能**写进 prompt 文本**:
+
+```
+[参考图说明] + [使用意图] + [原提示词]
+```
+
+- 1 张:`参考所提供图片的风格/主体/配色来生成新设计。`
+- N 张:逐张编号 `参考图1…参考图N`,让模型能区分主图与风格图。
+
+见本项目 `image-service.js` 的 `composeRefPrompt()`。

+ 109 - 0
.claude/rules/measurement.md

@@ -0,0 +1,109 @@
+# 实测方法(token / 速度 / 比例)
+
+> 一切数字**来自当轮真实调用**,不拍脑袋。凡估算必须标注;有服务端真值**以真值为准**。
+> 本项目所有实测数据可在页面「📊 框架实测」面板复现。
+
+---
+
+## 一、Token 归因
+
+### 1.1 分项口径
+
+每次 LLM 调用,把「真正发出去的东西」逐项记账:
+
+| 分项 | 内容 | 用途 |
+|------|------|------|
+| `system` | 框架/业务注入的固定前置 | **框架预置 prompt 归此项** → 为 0 说明框架不注入 |
+| `tools` | 工具 schema 描述(预筛**前/后**各测一次) | 直观展示预筛砍掉多少 |
+| `history` | 历史消息 | — |
+| `retrieved` | 检索节点按需召回 | 验证「无关历史不进 prompt」 |
+| `context` | 其余 | — |
+
+### 1.2 估算口径(无真值时)
+
+中文 ≈ **1 token/字**;英文/数字/符号 ≈ **1 token/4 字符**。
+对中文场景误差通常 ±10%,**够做横向对比**,但**不是计费值**。
+
+### 1.3 真值优先
+
+服务端返回 `usage` 时**覆盖**估算,并在 UI **单列一行**标明「服务端真值」。
+⚠️ 绝不把估算当真值展示。
+
+---
+
+## 二、输入 / 输出 token 比例(关键指标)
+
+### 2.1 为什么单列
+
+设计类 / 工具类 Agent 的成本**几乎全在「喂进去」**——prompt 远长于回复。
+把这个比例摆出来,才知道优化该往哪使劲(砍 tools schema?砍 history?)。
+
+### 2.2 怎么算
+
+```
+输入 : 输出 = Σ input_tokens : Σ output_tokens
+```
+
+- **输入**:有 `usage.prompt_tokens` 用真值,否则用估算(并**标出**有多少来自估算)。
+- **输出**:**只有真值**(本地不估算补全)。没有输出真值 → 比例**渲染为 `—`**,不硬凑。
+- **输出占比** `out/(in+out)`:输出占比越低,说明成本越集中在输入。
+
+### 2.3 数据来源(本项目三处真值)
+
+| 来源 | 字段 |
+|------|------|
+| LLM(chat/completions) | `usage.prompt_tokens` / `usage.completion_tokens` |
+| 出图(images/generations、images/edits) | `usage.input_tokens` / `usage.output_tokens`(含 `image_tokens` 明细) |
+
+**LLM 与出图分开列,再给一行「综合」** —— 两者成本结构完全不同,混在一起看不出问题。
+
+---
+
+## 三、速度:框架调度 vs IO(**最容易算错**)
+
+### 3.1 口径
+
+```
+框架纯调度 = 总墙钟 − Σ 节点内 IO(LLM + 网络)
+```
+
+框架自身动作单独计时:`graph.compile()`、checkpoint 序列化、路由/检索、边判断、节点调度。
+
+### 3.2 ⚠️ 实测踩坑(必看)
+
+**用 `t0()` 包住整个 `await LLM`**,会把 LLM 的 1.4s 算进「框架纯调度」,
+导致框架开销占比**虚高到 96%**(实测)。修正:`await` **期间暂停框架计时**,
+只把「发起调用前后」算框架的。
+
+正确姿势:
+
+```js
+t0('plan:dispatch');                       // 记「发起前」
+const res = await pauseIO(profile, 'plan:llm', () => chatComplete(...));
+// pauseIO 把 LLM 墙钟记进 IO,并从框架计时里扣除
+add(profile.framework, 'plan:dispatch', Math.max(0, t1('plan:dispatch') - lastIO(profile, 'plan:llm')));
+```
+
+### 3.3 期望结论
+
+框架调度占比应 **≲ 1%**(本项目实测 0.1%~0.7%)—— 用户感知的延迟 99% 来自出图(20~60s)与 LLM(2~3s)。
+
+---
+
+## 四、上下文膨胀比
+
+```
+膨胀比 = 末次调用 prompt / 首次调用 prompt
+```
+
+- **< 1** 是**设计使然**(本项目:第二轮 reflect 只发「用户原话 + 出图摘要」,不背全量历史)。
+- **> 1 且持续上升** = 线性追加历史,需裁剪 / 检索。
+- 附带看**膨胀量**(末次 − 首次;负值 = 被裁掉了)。
+
+---
+
+## 五、报告纪律
+
+- 每个数字标注**来源**(真值 / 估算)与**口径**(含不含某部分)。
+- 环境说明写清楚(headless Chromium / 本地 or 线上);**线上通常更快**(网络更近 + 尺寸档不同)。
+- **不在文档里写来路不明的数字** —— 面板能复现的才写。

+ 110 - 0
.claude/rules/no-build-langgraph.md

@@ -0,0 +1,110 @@
+# LangGraph.js 浏览器无构建配方(血泪版)
+
+> 目标:**零打包器**,让浏览器原生 ESM 直接跑 `@langchain/langgraph`。
+> 本项目实测版本:langgraph 1.5.1 / core 1.2.18 / checkpoint 1.2.0 / protocol 0.0.19 / zod 4.6.7。
+> 凡本文标「实测」的,都是本项目跑出来的;未验证的一律标注。
+
+---
+
+## 一、总思路
+
+npm 包的 dist ESM 里大量出现**裸标识符 import**(`@langchain/core/…`)与 **Node 专有模块**
+(`node:async_hooks`)。浏览器无打包器时:
+
+1. **裸标识符** → 靠 **importmap** 把深层子路径重定向到 `vendor/` 落盘路径;
+2. **Node 专有模块 / 重依赖** → 用 **vendor/shims/ 替身**顶上(importmap 指过去);
+3. **落盘** → `npm pack` 解包原始 ESM,**不转译**(无构建原则)。
+
+---
+
+## 二、落盘(vendor)
+
+```bash
+npm pack @langchain/langgraph@1.5.1 @langchain/core@1.2.18 \
+         @langchain/langgraph-checkpoint@1.2.0 @langchain/protocol@0.0.19 \
+         uuid@11.0.5 zod@4.6.7
+# 解包 → vendor/langgraph/@langchain/*、vendor/langgraph/uuid …
+# 瘦身:删 *.map / *.cjs / *.d.ts(浏览器用不到,且 .cjs 会被 MIME 坑)
+```
+
+入口:`@langchain/langgraph` → `vendor/langgraph/@langchain/langgraph/dist/web.js`。
+
+---
+
+## 三、必装替身(3 个,缺一白屏)
+
+| 替身 | 顶替 | 为什么 |
+|------|------|--------|
+| `vendor/shims/langsmith.js` | `langsmith` + `langsmith/run_trees` | core/langgraph **静态** import 它;而它依赖 p-queue、`node:fs`、`child_process`… 与浏览器冲突。替身给 `RunTree`/`Client` **空实现**(no-op,不发网络)。 |
+| `vendor/shims/async-hooks.js` | `node:async_hooks` | 实测报错:`Access to script at 'node:async_hooks' … blocked by CORS`。只有 `AsyncLocalStorage` 一个符号,浏览器没有 → 退化为 **no-op store**。 |
+| `vendor/shims/base64-js.js` | `base64-js` | 该包**只有 CJS**(`index.js` 是 CJS),无构建下不可直接当 ESM 用 → 提供 ESM 版薄封装。 |
+
+> `traceable.js` 是 langsmith 替身的连带(`@langchain/core/tracers/traceable` 用到)。
+
+---
+
+## 四、importmap(124 条,从 package.json 派生)
+
+**不要手写 importmap**。用脚本从 `package.json` 依赖**派生**,并做**可达性 + MIME 校验**
+(本项目 `scripts/gen-importmap.mjs`,校验 477 个文件可达 + `text/javascript`)。
+
+必须覆盖的映射类型:
+
+```jsonc
+{
+  "imports": {
+    "@langchain/langgraph": "./vendor/langgraph/@langchain/langgraph/dist/web.js",
+    "@langchain/langgraph/": "./vendor/langgraph/@langchain/langgraph/",   // 前缀映射
+    "@langchain/core": "./vendor/langgraph/@langchain/core/dist/index.js",
+    "@langchain/core/": "./vendor/langgraph/@langchain/core/",
+    "langsmith": "./vendor/shims/langsmith.js",                 // 替身
+    "langsmith/run_trees": "./vendor/shims/langsmith.js",
+    "node:async_hooks": "./vendor/shims/async-hooks.js",        // 替身(裸 node: 标识符)
+    "base64-js": "./vendor/shims/base64-js.js",                 // 替身
+    "uuid": "./vendor/langgraph/uuid/dist/esm-browser/index.js",
+    "zod": "./vendor/.../zod/v4/index.js",
+    "zod/v4/core": "./vendor/.../zod/v4/core/index.js"          // ⚠️ 子目录映射(见坑 4)
+  }
+}
+```
+
+---
+
+## 五、6 个实测坑
+
+1. **`langsmith` 静态依赖**:不是「用到才加载」,是模块顶层就 import。
+   不装替身 → 一进 core 就炸。**必须**先装替身。
+2. **`node:async_hooks` 是裸标识符**:浏览器把它当 URL 解析,报 CORS。
+   importmap 里加 `"node:async_hooks"` 键指向替身。
+3. **`base64-js` 只有 CJS**:`import` 它会拿到 CJS 的 `module.exports`,在浏览器 ESM 下时常不对。
+   → ESM 薄封装。
+4. **`zod/v4/core` 目录映射**:zod 的 exports 有子目录,importmap **前缀映射**要单独覆盖,
+   否则 `zod/v4/core` 解析失败(不是 `zod/v4/` 前缀能覆盖的,需显式键)。**实测坑**。
+5. **`.mjs` 的 MIME**:OBS/部分主机对 `.mjs` 回 `binary/octet-stream`,浏览器**拒绝执行**模块脚本
+   ("Expected a JavaScript-or-Wasm module script")。→ 落盘时就**改名 `.js`**,别指望服务端改 MIME。
+6. **AsyncLocalStorage 语义缺失**:替身让 ALS **no-op** → `getCurrentTaskInput()`、
+   callbacks 上下文注入**拿不到值**。本项目**刻意全部走 Graph State 显式传递**,不依赖 ALS。
+   若你要用 ALS 相关 API,需换真 polyfill。
+
+---
+
+## 六、State 与 ctx 的写法(踩过的)
+
+- **ctx 挂 `config.configurable.__dc`**,不要用闭包:
+  图**只编译一次、多轮复用**,闭包会把第一轮的 token 固化进编译产物(**安全隐患**)。
+- 节点签名 `node(state, config)` 的第二参是 **RunnableConfig**,不是你的 ctx。
+  早期版本把它当 ctx 用,读 `undefined` 直接抛错(实测)。
+- **凭据不进 State**(State 会被 checkpoint 序列化)。
+
+---
+
+## 七、验证
+
+上线上不了浏览器之前,先用脚本爬**模块图**:
+
+```bash
+node scripts/gen-importmap.mjs        # 派生 importmap + 校验可达性/MIME
+python3 scripts/verify-live.py <url>  # HTTP 层:跟随相对 import 走完整图,断言 200 + text/javascript
+```
+
+**只看首屏 200 不算通过** —— 一个深层模块 404 就白屏而首屏仍 200。

+ 53 - 0
.claude/rules/pitfalls.md

@@ -0,0 +1,53 @@
+# 坑位合集(血泪)
+
+---
+
+## 一、无构建 ESM
+
+1. **装饰器不可用**:浏览器原生 JS 不执行 `@customElement` 等 → `SyntaxError`。
+   用 `static properties` + `customElements.define()`(见 fmode-code-lit)。
+2. **module worker 不支持 importmap**:Worker 内只能 import**相对路径纯 JS**;
+   裸标识符(`zod/v4`)直接加载失败。→ Worker 侧写 JSON Schema,主线程侧写 zod。
+3. **`.mjs` 的 MIME**:OBS 对 `.mjs` 回 `octet-stream`,浏览器拒执行 → 落盘时改名 `.js`。
+4. **`new URL(path, baseURI)` 丢前缀**:子路径部署下路由 404(见 fmode-code-lit 路由篇)。
+5. **CJS-only 包**:如 `base64-js` 只有 CJS,无构建下需 ESM 薄封装。
+
+## 二、LangGraph 浏览器 ESM
+
+6. `langsmith` 是**静态依赖**,必须替身(否则一进 core 就炸)。
+7. `node:async_hooks` 是**裸标识符**,importmap 需显式键指向替身。
+8. `zod/v4/core` 需**子目录映射**(前缀映射覆盖不到)。**实测坑**。
+9. 替身后 ALS **no-op** → `getCurrentTaskInput()` / callbacks 上下文失效 →
+   **全部走 Graph State 显式传递**。
+10. 节点第二参是 **RunnableConfig** 不是你的 ctx;用 `config.configurable.__dc` 传上下文,
+    **别用闭包**(图只编译一次,闭包会固化首轮凭据)。
+11. **State 会被 checkpoint 序列化** → 凭据/大对象**不进 State**。
+
+## 三、网关 / 参数
+
+12. **静默忽略参数**:不认识的字段不报错、直接丢弃,返回 200。
+    必须**验证副作用**(如 `image_tokens > 0`)才知道参数有没有生效。
+13. **参考图不能凭空传**:`/v1/images/generations` 不接受;走 `/v1/images/edits` 的
+    `images: [{ image_url }]`(元素是**对象**,不是字符串)。
+14. **尺寸 ÷16**:`1080x1920` 被拒;9:16 用 `1024x1792`。
+15. **旧端点会死**:`accounts/sendCode` 等云函数已失效,以**当前实测**为准。
+
+## 四、计时 / 度量
+
+16. **`await LLM` 被算进框架耗时** → 占比虚高到 96%。→ `await` 期间**暂停框架计时**。
+17. **估算当真值**:无 `usage` 时用字符估算可以,但**必须标注**,别当计费值。
+18. **输出 token 无真值** → 输入:输出比例**渲染为 `—`**,不硬凑。
+
+## 五、部署
+
+19. **只 curl 首页不算验收**:模块 404 / MIME 错 / 回退没配都白屏而首页仍 200。
+20. **CDN 刷新假成功**:`refresh_task` 是**字符串**,读成 `.id` 恒 undefined 但脚本照常退出 0。
+21. **CDN 冷边缘首次 404**:新 key 首次拉取偶发 404,重试即消,别立刻判故障。
+22. **index.html 被 sync Skip**:按 size+mtime 判,需**先删后传** + `no-cache`。
+
+## 六、入库 / 数据
+
+23. **parse-server 不会按 session 自动补 `user` 指针**:新建记录时 `user` 会是 null。
+    要「user 指向该用户自己」**必须显式**先 `GET /parse/users/me` 拿 objectId 再传 Pointer。
+    (本项目实测:`GET /parse/users/me` 返回 objectId + company 指针。)
+24. **入库失败不应影响主流程**:记录是「尽力而为」,任何失败都不该让出图失败。

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+node_modules/
+.DS_Store
+*.log
+.obsutil_checkpoint/
+.obsutil_output/
+tmp/

+ 87 - 0
CLAUDE.md

@@ -0,0 +1,87 @@
+# fmode-code-agent — 纯 JS 网页版 Bot 智能体开发规则总纲
+
+> 本文件是 AI(Claude Code / FmodeCode)在本仓、或引用本仓规则时的**第一入口**。
+> 写任何「网页里跑智能体」的代码前,先读本文件,再按下表索引到具体规则篇。
+> 姊妹仓:**fmode-code-lit**(前端 Lit 无构建规范)—— 本仓专注**智能体(Agent)**那一层,
+> 两者可叠加使用:前端用 lit 规则,智能体用本仓规则。
+
+---
+
+## 0. 三条铁律(违反即返工)
+
+1. **浏览器与 Node 双兼容**:智能体核心(图 / MCP / Skill 抽象 / 遥测)必须是**纯 JS、无 Node 专有 API**
+   (`fs`/`path`/`process` 只在**分发脚本**里用)。同一份 `lib/` 要能在浏览器 ESM 直跑、也能在 Node 里 `import` 单测。
+2. **不用构建**:不引 rollup/esbuild/vite/webpack。依赖走 **importmap**(浏览器)或 npm(Node),
+   源码即产物。**LangGraph 这类"要先编译再用"的库,只能在浏览器里靠 importmap + 替身硬跑**(见规则 4)。
+3. **凭据不进 Worker、不进日志、不进代码**。出图/调模型这类**要凭据的动作在主线程**执行;
+   Worker 只做协议与编排。错误信息出界前**剔除**疑似 key。
+
+---
+
+## 1. 规则索引
+
+| # | 规则篇 | 内容 | 何时读 |
+|---|--------|------|--------|
+| 1 | [agent-architecture.md](./.claude/rules/agent-architecture.md) | Agent/MCP/Skill 三层、图编排、状态放哪、可观测性 | 起智能体 / 定架构 |
+| 2 | [agent-mcp-skill.md](./.claude/rules/agent-mcp-skill.md) | MCP 客户端统一管理、Skill 注册/调用协议、多 MCP 多 Skill 命名/冲突/隔离 | 接 MCP / 加 Skill |
+| 3 | [no-build-langgraph.md](./.claude/rules/no-build-langgraph.md) | LangGraph.js 浏览器无构建配方(importmap + 替身 + 6 个坑) | 用 LangGraph / 图编排 |
+| 4 | [agent-ui-bridge.md](./.claude/rules/agent-ui-bridge.md) | 智能体 ↔ UI 桥接(事件流、阶段、进度、失败如实说明) | 做对话界面 |
+| 5 | [llm-gateway.md](./.claude/rules/llm-gateway.md) | LLM / 出图网关对接(OpenAI 兼容、usage 真值、参数真值以实测为准) | 调模型 / 出图 |
+| 6 | [measurement.md](./.claude/rules/measurement.md) | **实测方法**:token 归因、输入/输出比例、框架调度 vs IO 计时 | 优化 / 出报告 |
+| 7 | [deployment.md](./.claude/rules/deployment.md) | 无构建部署 + **自动部署**(hook/CI/watch)+ 验真 | 上线 / 配流水线 |
+| 8 | [pitfalls.md](./.claude/rules/pitfalls.md) | 血泪坑位合集(无构建 ESM / 网关静默忽略参数 / 假成功…) | 排查问题 |
+
+---
+
+## 2. 一句话架构
+
+```
+用户输入 ─▶ Agent 决策(图 / Re-act)─▶ 选中工具 ─▶ MCP 协议层 ─▶ Skill 执行(凭据在这里)
+                    ▲                                      │
+                    └────────── 观测(token / 耗时)◀───────┘
+```
+
+- **Agent**:决定「这轮调不调工具、调哪个」。可换框架(PageAgent ⇄ LangGraph),换框架**不动** MCP/Skill。
+- **MCP**:把工具暴露成 JSON-RPC 2.0 协议(浏览器里常跑在 Web Worker)。
+- **Skill**:真正干活的纯函数式能力(出图 / 转写 / 视觉分析)。
+
+> 本项目实测:把 Agent 从 PageAgent 换到 LangGraph,**MCP 与 Skill 一行未改** —— 分层奏效的证据。
+
+---
+
+## 3. 默认技术选型
+
+| 关注点 | 选择 | 备注 |
+|--------|------|------|
+| 图编排 | **LangGraph.js**(`@langchain/langgraph`) | 显式图 + 条件边 + checkpoint;浏览器需 importmap + 替身 |
+| 轻量 Agent | 自写 Re-act 循环 | 不想要图依赖时的替代(PageAgent 亦属此类) |
+| 工具协议 | **MCP(JSON-RPC 2.0)** | 本项目自建浏览器端 MCP server;跑在 Web Worker |
+| 工具 schema | Worker 侧 **JSON Schema** + 主线程侧 **zod** | Worker 不能 import zod(无 importmap),故两份 + 断言一致 |
+| 状态管理 | 框架 State + 检索式外存 | State 只存**索引/摘要**,大对象进 IndexedDB / service |
+| 模型调用 | 自写最小 OpenAI 兼容客户端 | 不引 `@langchain/openai`(省依赖 + 可控) |
+| 观测 | 自写 telemetry(token / 耗时分离) | 不依赖框架自带(口径要自己定) |
+| 部署 | 无构建直传 OBS + CDN | 见规则 7 |
+
+---
+
+## 4. 用本规则仓
+
+```bash
+# 把规则装进目标项目(写 .claude/rules/ + CLAUDE.md 摘要)
+node skills/fmode-code-agent/scripts/install-rules.mjs ./
+
+# 生成一个最小智能体骨架(index.html + importmap + lib/ + mcp/)
+node skills/fmode-code-agent/scripts/scaffold.mjs ./my-agent --project my-agent
+```
+
+---
+
+## 5. 关系图
+
+```
+fmode-code-lit      ── 前端怎么写(Lit / 无构建 / 部署)
+fmode-code-agent    ── 智能体怎么写(Agent/MCP/Skill / LangGraph / 实测 / 自动部署)  ← 本仓
+skill-image 等      ── 具体能力(被 MCP 包装成工具)
+```
+
+冲突时:前端细节以 fmode-code-lit 为准;智能体细节以本仓为准。

+ 375 - 0
LICENSE

@@ -0,0 +1,375 @@
+Mozilla Public License Version 2.0
+==================================
+
+Copyright (c) 未来飞马
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 77 - 0
README.md

@@ -0,0 +1,77 @@
+# fmode-code-agent · 纯 JS 网页版 Bot 智能体开发规则仓
+
+> **未来飞马 — 让 AI 进化提前发生,让 AI 落地快人一步**
+
+`fmode-code-agent` 是 FMODE 组织下的 **智能体(Agent)开发规则仓库**:为「纯 JS(浏览器 + Node 双兼容、
+无任何打包器)实现的网页版 Bot 智能体」定义一套可被 AI(Claude Code / FmodeCode)直接执行的工程规则。
+
+本仓 **不是** 框架,也 **不是** 运行时库。它是 **规则 + 脚手架技能** 的组合体:
+
+| 交付物 | 路径 | 作用 |
+|--------|------|------|
+| 规则总纲 | [`CLAUDE.md`](./CLAUDE.md) | AI 进入本仓/引用本仓的第一入口,索引全部规则 |
+| 分层规则 | [`.claude/rules/`](./.claude/rules/) | 三层架构、MCP/Skill、LangGraph 无构建、实测、部署、坑位逐篇规范 |
+| 全局技能 | [`skills/fmode-code-agent/`](./skills/fmode-code-agent/) | 给 AI 的智能体项目初始化技能 + 脚手架 |
+
+> 姊妹仓:**[fmode-code-lit](../fmode-code-lit)**(Lit 前端无构建规范)。
+> 前端细节以 lit 仓为准,智能体细节以本仓为准,两者可叠加使用。
+
+## 一句话记住它
+
+> **Agent 决策、MCP 协议、Skill 执行。换 Agent 框架时,MCP 与 Skill 一行不改。**
+
+## 三层架构
+
+```
+Agent(决策)   决定「这轮调不调工具、调哪个」。可换框架(PageAgent ⇄ LangGraph)。
+  └─ MCP(协议) 把工具暴露成 JSON-RPC 2.0(浏览器常跑在 Web Worker)。不含业务编排。
+       └─ Skill(实现)真正干活(出图/转写/分析),纯函数式能力。
+```
+
+**验收标准**:本项目把 Agent 从 PageAgent 换到 LangGraph,**MCP 与 Skill 零改动**。
+
+## 规则索引
+
+| # | 规则篇 | 内容 |
+|---|--------|------|
+| 1 | [`agent-architecture.md`](./.claude/rules/agent-architecture.md) | 三层职责、图编排 vs Re-act、State 放什么、可观测性 |
+| 2 | [`agent-mcp-skill.md`](./.claude/rules/agent-mcp-skill.md) | MCP 契约、多 MCP 命名空间、Worker 铁律、Skill 注册/调用、隔离 |
+| 3 | [`no-build-langgraph.md`](./.claude/rules/no-build-langgraph.md) | LangGraph.js 浏览器无构建配方(importmap + 3 替身 + 6 坑) |
+| 4 | [`llm-gateway.md`](./.claude/rules/llm-gateway.md) | OpenAI 兼容、usage 真值、**静默忽略参数**陷阱、多模态拼接 |
+| 5 | [`measurement.md`](./.claude/rules/measurement.md) | token 归因、**输入/输出比例**、框架调度 vs IO 计时 |
+| 6 | [`agent-ui-bridge.md`](./.claude/rules/agent-ui-bridge.md) | 事件流/阶段/进度、失败如实说明、移动端 |
+| 7 | [`deployment.md`](./.claude/rules/deployment.md) | 无构建部署 + **自动部署**(hook/CI/watch)+ 验真 |
+| 8 | [`pitfalls.md`](./.claude/rules/pitfalls.md) | 24 条血泪坑位合集 |
+
+## 技术底座
+
+- **语言**:纯 JS(ESM),浏览器与 Node 双兼容(核心无 Node 专有 API)
+- **图编排**:LangGraph.js(`@langchain/langgraph`),浏览器靠 importmap + 替身直跑
+- **工具协议**:MCP(JSON-RPC 2.0),浏览器端跑在 Web Worker
+- **模型调用**:自写最小 OpenAI 兼容客户端(不引 SDK)
+- **观测**:自写 telemetry(token / 耗时分离)
+- **部署**:无构建直传 OBS + CDN;提交后自动部署
+
+## 用法
+
+```bash
+# 1) 生成智能体骨架
+node skills/fmode-code-agent/scripts/scaffold.mjs ./my-agent --project my-agent
+
+# 2) 给已有项目装规则
+node skills/fmode-code-agent/scripts/install-rules.mjs .
+
+# 3) 自检
+node skills/fmode-code-agent/scripts/smoke.mjs
+```
+
+## 四条铁律
+
+1. **浏览器 + Node 双兼容**:核心纯 JS;`fs`/`path` 只在分发脚本里用。
+2. **不用构建**:不引打包器;依赖走 importmap(浏览器)或 npm(Node)。
+3. **凭据不进 Worker / 日志 / 代码**:要凭据的动作在**主线程**执行。
+4. **参数以实测为准**:网关会**静默忽略**参数 —— 必须**验证副作用**,不能只看 200。
+
+## 开源协议
+
+MPL-2.0(见 [LICENSE](./LICENSE))。商标「未来飞马」等不随协议授权。

+ 28 - 0
package.json

@@ -0,0 +1,28 @@
+{
+  "name": "skill-code-agent",
+  "version": "0.1.0",
+  "type": "module",
+  "description": "fmode-code-agent 规则库:纯 JS 网页版 Bot 智能体开发规范(Agent/MCP/Skill 三层 / LangGraph 无构建 / 浏览器端 MCP / token 实测 / 自动部署)。随 npm 分发,含全局技能与脚手架。",
+  "keywords": ["agent", "mcp", "skill", "langgraph", "no-build", "esm", "browser", "rules", "fmode"],
+  "license": "MPL-2.0",
+  "files": [
+    "skills",
+    "docs",
+    "examples",
+    ".claude",
+    "README.md",
+    "CLAUDE.md"
+  ],
+  "exports": {
+    ".": "./CLAUDE.md",
+    "./rules/*": "./.claude/rules/*"
+  },
+  "bin": {
+    "fmode-code-agent": "skills/fmode-code-agent/scripts/install.mjs"
+  },
+  "scripts": {
+    "smoke": "node skills/fmode-code-agent/scripts/smoke.mjs",
+    "install-skill": "node skills/fmode-code-agent/scripts/install.mjs install --force",
+    "install-rules": "node skills/fmode-code-agent/scripts/install-rules.mjs"
+  }
+}

+ 375 - 0
skills/fmode-code-agent/LICENSE

@@ -0,0 +1,375 @@
+Mozilla Public License Version 2.0
+==================================
+
+Copyright (c) 未来飞马
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 65 - 0
skills/fmode-code-agent/README.md

@@ -0,0 +1,65 @@
+# fmode-code-agent · 纯 JS 网页版 Bot 智能体技能
+
+> **未来飞马 — 让 AI 进化提前发生,让 AI 落地快人一步**
+
+[![License: MPL-2.0](https://img.shields.io/badge/License-MPL--2.0-brightgreen.svg)](LICENSE)
+[![ESM](https://img.shields.io/badge/module-native--ESM-orange.svg)](#)
+
+---
+
+## 简介
+
+`fmode-code-agent` 是教 AI 用 **纯 JS(浏览器 + Node 双兼容、无构建)** 写**网页版 Bot 智能体**的
+Claude Code 技能:Agent 决策 / MCP 协议 / Skill 执行三层分离,LangGraph.js 浏览器直跑,
+token/速度**实测**,**提交后自动部署**。
+
+配套 **规则仓**(本仓 `.claude/rules/`)提供架构、MCP/Skill、无构建配方、实测、部署等完整规范。
+
+适用 **FmodeCode / Claude Code / FmodeAgent**;Node ≥ 18 直接运行。
+
+## 核心能力
+
+- 🏗 **项目脚手架** —— 一键生成智能体骨架(index.html + importmap + lib/ + mcp/)
+- 🔌 **MCP/Skill 范式** —— 统一 MCP 契约、Skill 注册/调用、多 MCP 命名/冲突/隔离规则
+- 🕸 **LangGraph 无构建配方** —— importmap + 3 个替身 + 6 个实测坑
+- 📏 **实测方法** —— token 归因、输入/输出 token 比例、框架调度 vs IO 计时
+- 🚀 **自动部署** —— 提交后自动「体检 → 发布 → 验真」,可 watch / CI
+- 📚 **完整规则仓** —— 8 篇规范 + 24 条血泪坑位
+
+## 快速开始
+
+```bash
+# 生成一个最小智能体骨架
+node skills/fmode-code-agent/scripts/scaffold.mjs ./my-agent --project my-agent
+
+# 给已有项目装规则
+node skills/fmode-code-agent/scripts/install-rules.mjs .
+
+# 自检(含 scaffold 产物 + MCP 协议功能测试)
+node skills/fmode-code-agent/scripts/smoke.mjs
+```
+
+## 四条铁律
+
+1. **浏览器 + Node 双兼容**:核心纯 JS;`fs`/`path` 只在分发脚本里用。
+2. **不用构建**:不引打包器;依赖走 importmap(浏览器)或 npm(Node)。
+3. **凭据不进 Worker / 日志 / 代码**;要凭据的动作在**主线程**执行。
+4. **参数以实测为准**:网关会**静默忽略**参数 —— 必须**验证副作用**,不能只看 200。
+
+## 诚实失败
+
+- 生成前先探测目标目录;已存在文件**不覆盖**(除非 `--force`)。
+- 本项目**未实测**的能力(多 MCP 并发、HTTP/SSE 传输…)在规则里**明确标注「未验证」**,不写成规范。
+- 参数真值以**实测**为准;某参数不支持就**如实标注**,不编造。
+
+## 依赖版本(2026-10 实测可用)
+
+```
+@langchain/langgraph@1.5.1  @langchain/core@1.2.18
+@langchain/langgraph-checkpoint@1.2.0  @langchain/protocol@0.0.19
+uuid@11.0.5  zod@4.6.7
+```
+
+## 开源协议
+
+MPL-2.0(见 [LICENSE](./LICENSE))。商标「未来飞马」等不随协议授权。

+ 123 - 0
skills/fmode-code-agent/SKILL.md

@@ -0,0 +1,123 @@
+---
+name: fmode-code-agent
+description: "用纯 JS(浏览器 + Node 双兼容、无构建)写网页版 Bot 智能体。触发词:智能体、agent、MCP、Skill、LangGraph、Re-act、工具调用 function calling、Web Worker MCP、Agent 架构、token 实测、自动部署。适用场景:(1) 新建一个网页内跑的智能体(对话 + 工具调用)(2) 把能力封装成 MCP 工具 / 接入多个 MCP (3) 用 LangGraph.js 做图编排(浏览器无构建)(4) 给智能体加 token/速度实测与输入输出比例 (5) 配自动部署。产出遵循 fmode-code-agent 规则仓的分层与命名规范。"
+description_en: "Build in-page Bot agents in pure JS (browser + Node compatible, no build). Triggers: agent, MCP, Skill, LangGraph, Re-act, function calling, Web Worker MCP, agent architecture, token measurement, auto-deploy. Use for: (1) scaffolding a web agent (chat + tool calls), (2) wrapping capabilities as MCP tools / joining multiple MCPs, (3) LangGraph.js graph orchestration without a bundler, (4) adding token/latency measurement and input/output ratio, (5) setting up auto-deploy. Output follows the fmode-code-agent rules repo's layering and naming conventions."
+schema_version: 1.0.0
+level: system
+category: development
+keywords:
+  - agent
+  - mcp
+  - skill
+  - langgraph
+  - react-loop
+  - function-calling
+  - tool-calling
+  - webworker
+  - no-build
+  - native-esm
+  - importmap
+  - token
+  - telemetry
+  - browser
+  - node
+  - fmode
+version: 1.0.0
+author: Yuyang001 (FmodeAgent)
+license: MPL-2.0
+copyright: "Copyright (c) 2026 未来飞马 Fmode"
+tags: [未来飞马, 智能体技能, 超级技能, 开发级, 智能体, Agent, MCP, Skill, LangGraph, 无构建, ESM, FmodeCode, Claude Code]
+---
+
+# fmode-code-agent — 纯 JS 网页版 Bot 智能体
+
+## Overview
+
+教 AI 用**纯 JS(浏览器 + Node 双兼容、无任何打包器)**实现网页里跑的智能体:
+三层架构(Agent 决策 / MCP 协议 / Skill 实现)、LangGraph.js 图编排(浏览器 ESM 直跑)、
+浏览器端 MCP(Web Worker + JSON-RPC 2.0)、token/速度**实测方法**、**自动部署**。
+
+> 前端 Lit 规范见姊妹技能 **fmode-code-lit**;本技能管**智能体那一层**。
+
+## 何时触发
+
+- 「做一个**智能体** / **Agent** / 对话机器人,能调用工具」
+- 「把能力封装成 **MCP**」「接**多个 MCP**」「**Skill** 注册/调用」
+- 「用 **LangGraph** 做图编排」「Re-act 循环」「**function calling**」
+- 「**浏览器**里跑智能体、**Web Worker** 里跑 MCP」
+- 「统计 **token** / **输入输出比例** / 框架耗时」
+- 「配**自动部署**(提交后自动发)」
+
+## 三层架构(记牢边界)
+
+```
+Agent(决策)──不 import Skill;换框架不动 MCP/Skill
+  └─ MCP(协议)──把工具暴露成 JSON-RPC;不含业务编排;凭据不进 Worker
+       └─ Skill(实现)──真正干活(纯函数);不假设调用方
+```
+
+**验收标准**:换 Agent 框架时,**MCP 与 Skill 一行不改**。
+
+## 四条铁律
+
+1. **浏览器 + Node 双兼容**:核心是纯 JS,`fs`/`path` 只在分发脚本里用。
+2. **不用构建**:不引打包器;依赖走 importmap(浏览器)或 npm(Node)。
+3. **凭据不进 Worker、不进日志、不进代码**:要凭据的动作在**主线程**执行。
+4. **参数以实测为准**:网关可能**静默忽略**参数——必须**验证副作用**,不能只看 200。
+
+## 用法
+
+### 1) 生成一个最小智能体骨架
+
+```bash
+node <skill_dir>/scripts/scaffold.mjs ./my-agent --project my-agent
+```
+
+生成:`index.html`(importmap)+ `app.js` + `lib/`(graph / 协议 / 遥测)+ `mcp/`(Worker + 工具规格)。
+
+### 2) 给已有项目装上规则
+
+```bash
+node <skill_dir>/scripts/install-rules.mjs .      # 写入 ./.claude/rules/ + CLAUDE.md 片段
+```
+
+### 3) 配自动部署
+
+```bash
+bash scripts/install-hooks.sh        # pre-push 触发「体检 → 发布 → 验真」
+bash scripts/auto-deploy.sh          # 手动跑一轮
+bash scripts/auto-deploy.sh --watch  # 常驻监听,改了即发
+```
+
+## 关键约定速查
+
+| 项 | 约定 |
+|----|------|
+| MCP 契约 | `start`(幂等) / `listTools` / `callTool` / `state` / `transport` |
+| 工具全名 | 多 MCP 时 `<mcpId>.<toolName>`,避同名冲突 |
+| Worker | 只 import 相对路径**纯 JS**;**不能** import 裸包(无 importmap) |
+| schema | Worker 侧 JSON Schema + 主线程侧 zod,`assertSpecParity()` 保证一致 |
+| 凭据 | **不进 Worker**;出图/调模型在主线程;错误剔除 key |
+| State | 只存**索引/摘要**;凭据**不进** State(会被 checkpoint 序列化) |
+| ctx | 挂 `config.configurable.__dc`,**别用闭包**(图编译一次复用) |
+| LangGraph | 浏览器需 importmap + **3 个替身**(langsmith / node:async_hooks / base64-js) |
+| 尺寸 | 出图 `size` 宽高须 **÷16**;9:16 用 `1024x1792`(不是 1080x1920) |
+| 参考图 | 只走 `/v1/images/edits` 的 `images: [{ image_url }]` |
+| 实测 | 框架调度与 IO **分开计时**;`await` 期间暂停框架计时 |
+| 入库 | parse-server **不会**自动补 `user` 指针 → 先 `GET /parse/users/me`,显式传 Pointer |
+| 部署 | 无构建直传 OBS+CDN;**只 curl 首页不算验收**(须爬模块图 + 深链接回退) |
+
+## 诚实失败
+
+- 生成前先探测目标目录;已存在文件**不覆盖**(除非 `--force`)。
+- 凡本项目**未实测**的能力(如多 MCP 并发、HTTP/SSE 传输),规则里**明确标注「未验证」**,
+  **不写成规范**。
+- 参数真值以**实测**为准;某参数不支持就**如实标注**,不编造。
+
+## 依赖版本(2026-10 实测可用)
+
+```
+@langchain/langgraph@1.5.1  @langchain/core@1.2.18
+@langchain/langgraph-checkpoint@1.2.0  @langchain/protocol@0.0.19
+uuid@11.0.5  zod@4.6.7
+```

+ 33 - 0
skills/fmode-code-agent/bin/fmode-code-agent.js

@@ -0,0 +1,33 @@
+#!/usr/bin/env node
+/**
+ * fmode-code-agent CLI
+ * 用法:
+ *   node bin/fmode-code-agent.js install      # 装技能到 ~/.claude/skills/(默认)
+ *   node bin/fmode-code-agent.js workspace    # 装技能到 ./.claude/skills/
+ *   node bin/fmode-code-agent.js rules [dir]  # 把智能体规则装入目标项目 ./.claude/rules/
+ *   node bin/fmode-code-agent.js scaffold <dir> --project <name>   # 生成智能体骨架
+ *   node bin/fmode-code-agent.js smoke        # 自检
+ */
+import { spawnSync } from 'node:child_process';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const SCRIPTS = join(__dirname, '..', 'scripts');
+const [cmd = 'install', ...rest] = process.argv.slice(2);
+
+const MAP = {
+  install: 'install.mjs',
+  workspace: 'install.mjs',
+  rules: 'install-rules.mjs',
+  scaffold: 'scaffold.mjs',
+  smoke: 'smoke.mjs',
+};
+
+const script = MAP[cmd];
+if (!script) {
+  console.error(`未知命令:${cmd}\n可用:${Object.keys(MAP).join(', ')}`);
+  process.exit(2);
+}
+const r = spawnSync(process.execPath, [join(SCRIPTS, script), ...(cmd === 'smoke' ? [] : [cmd === 'install' || cmd === 'workspace' ? cmd : '', ...rest].filter((x) => x !== ''))], { stdio: 'inherit' });
+process.exit(r.status ?? 1);

+ 38 - 0
skills/fmode-code-agent/package.json

@@ -0,0 +1,38 @@
+{
+  "name": "fmode-code-agent",
+  "version": "0.1.0",
+  "description": "Claude Code 技能:纯 JS 网页版 Bot 智能体开发(Agent/MCP/Skill 三层、LangGraph 无构建、浏览器端 MCP、token 实测、自动部署)。提供规则安装器与项目脚手架。",
+  "type": "module",
+  "bin": {
+    "fmode-code-agent": "bin/fmode-code-agent.js"
+  },
+  "scripts": {
+    "smoke": "node skills/fmode-code-agent/scripts/smoke.mjs"
+  },
+  "files": [
+    "bin/",
+    "README.md",
+    "LICENSE",
+    "skill-package-manifest.json",
+    "skills/"
+  ],
+  "keywords": [
+    "claude-code",
+    "claude-skill",
+    "fmode",
+    "agent",
+    "mcp",
+    "skill",
+    "langgraph",
+    "no-build",
+    "native-esm",
+    "importmap",
+    "telemetry"
+  ],
+  "license": "MPL-2.0",
+  "repository": {
+    "type": "git",
+    "url": "git+ssh://git@git.fmode.cn/fmode/fmode-code-agent.git"
+  },
+  "homepage": "https://git.fmode.cn/fmode/fmode-code-agent"
+}

+ 74 - 0
skills/fmode-code-agent/scripts/install-rules.mjs

@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+/**
+ * install-rules.mjs —— 把 fmode-code-agent 的智能体规则装入目标项目的 ./.claude/rules/。
+ *
+ * 用法:
+ *   node install-rules.mjs [targetDir] [--force]
+ *   node install-rules.mjs [targetDir] --from <rulesRepoDir>   # 指定规则仓根目录
+ *
+ * 规则来源优先级:--from > 技能同仓 ../../../.claude/rules > 内置最小规则集。
+ * 已存在文件默认不覆盖(--force 覆盖)。
+ */
+import { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
+import { join, resolve, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const args = process.argv.slice(2);
+const target = resolve(args.find((a) => a && !a.startsWith('-')) || '.');
+const force = args.includes('--force');
+const fromIdx = args.indexOf('--from');
+const fromDir = fromIdx > -1 ? resolve(args[fromIdx + 1]) : null;
+
+// 规则来源候选(技能在 <repo>/skills/fmode-code-agent/scripts/)
+const candidates = [
+  fromDir,
+  resolve(__dirname, '../../../.claude/rules'),
+  resolve(__dirname, '../../.claude/rules'),
+].filter(Boolean);
+const srcRules = candidates.find((d) => d && existsSync(d) && existsSync(join(d, 'agent-architecture.md')));
+
+if (!srcRules) {
+  console.error('(!) 未找到规则源。请用 --from <规则仓根目录> 指定(需含 .claude/rules/agent-architecture.md)。');
+  process.exit(1);
+}
+
+const destRules = join(target, '.claude', 'rules');
+mkdirSync(destRules, { recursive: true });
+
+let created = 0, skipped = 0;
+function copyTree(from, to) {
+  for (const name of readdirSync(from)) {
+    const s = join(from, name), d = join(to, name);
+    if (statSync(s).isDirectory()) { mkdirSync(d, { recursive: true }); copyTree(s, d); continue; }
+    if (!name.endsWith('.md')) continue;
+    if (existsSync(d) && !force) { skipped++; continue; }
+    writeFileSync(d, readFileSync(s));
+    created++;
+  }
+}
+copyTree(srcRules, destRules);
+
+// 在目标 CLAUDE.md 里追加一段指引(若还没有)
+const claudeMd = join(target, 'CLAUDE.md');
+const MARK = '<!-- fmode-code-agent rules -->';
+const snippet = `
+${MARK}
+## 智能体开发规则(fmode-code-agent)
+
+本项目遵守 **fmode-code-agent** 规则。写任何智能体/Agent/MCP/Skill 代码前先读
+\`.claude/rules/agent-architecture.md\`,再按 CLAUDE.md 索引查具体篇。
+
+四条铁律:① 浏览器+Node 双兼容 ② 不用构建(importmap)③ 凭据不进 Worker/日志/代码
+④ 参数以实测为准(网关会静默忽略参数,须验证副作用)。
+`;
+if (existsSync(claudeMd)) {
+  const cur = readFileSync(claudeMd, 'utf8');
+  if (!cur.includes(MARK)) { writeFileSync(claudeMd, cur.trimEnd() + '\n' + snippet); console.log('  · 已向 CLAUDE.md 追加规则指引'); }
+} else {
+  writeFileSync(claudeMd, `# 项目规范\n${snippet}`);
+  console.log('  · 已创建 CLAUDE.md');
+}
+
+console.log(`规则安装完成:新增 ${created} 个,跳过(已存在)${skipped} 个 → ${destRules}`);
+if (skipped && !force) console.log('  (要覆盖已存在文件:加 --force)');

+ 42 - 0
skills/fmode-code-agent/scripts/install.mjs

@@ -0,0 +1,42 @@
+#!/usr/bin/env node
+/**
+ * install.mjs —— 把 fmode-code-agent 技能装到用户级/工作区级 .claude/skills/。
+ *
+ * 用法:
+ *   node install.mjs install [--force]     # 装到 ~/.claude/skills/fmode-code-agent/
+ *   node install.mjs workspace [--force]   # 装到 ./.claude/skills/fmode-code-agent/
+ *   node install.mjs rules [dir] [--force] # 等价于 install-rules.mjs
+ */
+import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
+import { join, dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { homedir } from 'node:os';
+import { execFileSync } from 'node:child_process';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const skillSrc = resolve(__dirname, '..');
+const args = process.argv.slice(2);
+const cmd = args[0] || 'install';
+const force = args.includes('--force');
+
+if (cmd === 'rules') {
+  const dir = args.find((a) => a && !a.startsWith('-') && a !== 'rules') || '.';
+  execFileSync(process.execPath, [join(__dirname, 'install-rules.mjs'), dir, ...(force ? ['--force'] : [])], { stdio: 'inherit' });
+  process.exit(0);
+}
+
+const base = cmd === 'workspace' ? join(process.cwd(), '.claude') : join(homedir(), '.claude');
+const dest = join(base, 'skills', 'fmode-code-agent');
+mkdirSync(dest, { recursive: true });
+
+const files = ['SKILL.md', 'README.md', 'package.json', 'skill-package-manifest.json'];
+let n = 0;
+for (const f of files) {
+  const s = join(skillSrc, f);
+  if (!existsSync(s)) continue;
+  const d = join(dest, f);
+  if (existsSync(d) && !force) { console.log('  · 跳过(已存在)', f); continue; }
+  writeFileSync(d, readFileSync(s)); n++;
+}
+console.log(`已安装 fmode-code-agent → ${dest}(${n} 个文件)`);
+console.log('提示:再跑 `node install-rules.mjs <项目目录>` 可把规则写进具体项目。');

+ 372 - 0
skills/fmode-code-agent/scripts/scaffold.mjs

@@ -0,0 +1,372 @@
+#!/usr/bin/env node
+/**
+ * scaffold.mjs —— 生成一个**最小可跑的智能体骨架**(无构建)。
+ *
+ * 用法:node scaffold.mjs <targetDir> --project <name> [--force]
+ *
+ * 产出结构(源码即产物,浏览器直跑):
+ *   <target>/
+ *     CLAUDE.md
+ *     src/ index.html   <base> + importmap(最小) + 入口
+ *          app.js       装配 + 路由
+ *          lib/         智能体核心(纯 JS,双兼容)
+ *            graph.js   一个最小 LangGraph 图(route → plan → respond)
+ *            fmode-chat.js  最小 OpenAI 兼容客户端
+ *            telemetry.js   token/耗时埋点
+ *          mcp/         MCP 层
+ *            protocol.js  JSON-RPC 2.0 骨架
+ *            tool-specs.js 工具规格(JSON Schema)
+ *            worker.js   Worker 宿主
+ *     scripts/ deploy.sh  无构建部署 + 自动部署 + 验真
+ *     .claude/rules/       智能体规则(从规则仓同步)
+ *
+ * 诚实失败:目标已存在文件默认**不覆盖**(--force 才覆盖)。
+ */
+import { mkdirSync, writeFileSync, existsSync, readFileSync, chmodSync } from 'node:fs';
+import { join, resolve, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const args = process.argv.slice(2);
+const target = args.find((a) => a && !a.startsWith('-'));
+const projIdx = args.indexOf('--project');
+const project = projIdx > -1 ? args[projIdx + 1] : 'my-agent';
+const force = args.includes('--force');
+
+if (!target) { console.error('用法:node scaffold.mjs <targetDir> --project <name> [--force]'); process.exit(2); }
+const root = resolve(target);
+
+const files = {};
+const add = (p, c) => { files[p] = c; };
+
+add('src/index.html', `<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
+  <base href="/${project}/" />
+  <title>${project} · 智能体</title>
+  <!-- 依赖走 importmap(浏览器无构建)。生产环境可改为 Repose 公共 importmap。 -->
+  <script type="importmap">
+  {
+    "imports": {
+      "lit": "/dev/npm/lit/v3.3.3/index.js",
+      "lit/": "/dev/npm/lit/v3.3.3/",
+      "@lit-labs/signals": "/dev/npm/@lit-labs/signals/v0.3.0/index.js",
+      "@langchain/langgraph": "./vendor/langgraph/@langchain/langgraph/dist/web.js",
+      "@langchain/langgraph/": "./vendor/langgraph/@langchain/langgraph/",
+      "@langchain/core": "./vendor/langgraph/@langchain/core/dist/index.js",
+      "@langchain/core/": "./vendor/langgraph/@langchain/core/",
+      "langsmith": "./vendor/shims/langsmith.js",
+      "langsmith/run_trees": "./vendor/shims/langsmith.js",
+      "node:async_hooks": "./vendor/shims/async-hooks.js",
+      "base64-js": "./vendor/shims/base64-js.js"
+    }
+  }
+  </script>
+</head>
+<body>
+  <app-root></app-root>
+  <script type="module" src="./app.js"></script>
+</body>
+</html>
+`);
+
+add('src/app.js', `/**
+ * app.js —— 最小智能体入口(无构建)。
+ * 依赖:lit(importmap)+ lib/graph.js。此处只要跑通「一句话 → 图 → 回复」。
+ */
+import { html, LitElement } from 'lit';
+import { signal } from '@lit-labs/signals';
+import { runTurn } from './lib/graph.js';
+
+class AppRoot extends LitElement {
+  static properties = { _reply: { type: String, state: true }, _busy: { type: Boolean, state: true } };
+  createRenderRoot() { return this; }              // 无构建演示:不用 Shadow DOM 也行
+  constructor() { super(); this._reply = ''; this._busy = false; }
+
+  async _submit(e) {
+    e.preventDefault();
+    const text = this.renderRoot.querySelector('input').value.trim();
+    if (!text) return;
+    this._busy = true; this._reply = '';
+    try {
+      const out = await runTurn({ utterance: text }, { token: window.__FMODE_TOKEN__ || '' });
+      this._reply = out.state?.reply || '(无回复)';
+    } catch (err) {
+      this._reply = '出错:' + (err?.message || err);
+    } finally { this._busy = false; }
+  }
+
+  render() {
+    return html\`
+      <h1>${project} · 智能体</h1>
+      <form @submit=\${this._submit}>
+        <input placeholder="说点什么…" ?disabled=\${this._busy} />
+        <button ?disabled=\${this._busy}>\${this._busy ? '…' : '发送'}</button>
+      </form>
+      \${this._reply ? html\`<pre>\${this._reply}</pre>\` : ''}
+    \`;
+  }
+}
+customElements.define('app-root', AppRoot);
+void signal;
+`);
+
+add('src/lib/graph.js', `/**
+ * lib/graph.js —— 最小 LangGraph 图:route → plan → respond。
+ * ⚠️ 浏览器无构建要点见规则 .claude/rules/no-build-langgraph.md:
+ *    - 3 个替身(langsmith / node:async_hooks / base64-js)缺一白屏;
+ *    - ctx 挂 config.configurable.__dc(别用闭包);
+ *    - 凭据不进 State。
+ */
+import { StateGraph, START, END, Annotation } from '@langchain/langgraph';
+import { chatComplete } from './fmode-chat.js';
+
+const S = Annotation.Root({
+  utterance: Annotation({ reducer: (_, b) => b, default: () => '' }),
+  reply: Annotation({ reducer: (_, b) => b, default: () => '' }),
+});
+
+function ctxOf(config) {
+  const c = config?.configurable?.__dc;
+  if (!c) throw new Error('运行时上下文缺失(config.configurable.__dc)');
+  return c;
+}
+
+async function planNode(state, config) {
+  const { token, model } = ctxOf(config);
+  const res = await chatComplete({
+    token, model,
+    messages: [
+      { role: 'system', content: '你是一个简洁的助手,用中文回答。' },
+      { role: 'user', content: state.utterance },
+    ],
+  });
+  return { reply: res.content || '' };
+}
+// respond 占位(真实项目在此收尾/落库)
+async function respondNode(state) { return {}; }
+
+let compiled = null;
+function build() {
+  return new StateGraph(S)
+    .addNode('plan', planNode)
+    .addNode('respond', respondNode)
+    .addEdge(START, 'plan')
+    .addEdge('plan', 'respond')
+    .addEdge('respond', END)
+    .compile();
+}
+
+export async function runTurn({ utterance }, { token, model = 'deepseek-flash' } = {}) {
+  if (!compiled) compiled = build();
+  const state = await compiled.invoke(
+    { utterance: String(utterance || '') },
+    { configurable: { __dc: { token, model } } },
+  );
+  return { state };
+}
+`);
+
+add('src/lib/fmode-chat.js', `/**
+ * lib/fmode-chat.js —— 最小 OpenAI 兼容客户端(自写,不引 @langchain/openai)。
+ * 凭据显式传入;错误剔除疑似 key。
+ */
+const BASE = 'https://api.fmode.cn/v1';
+
+function scrub(t) { return String(t ?? '').replace(/(sk-[A-Za-z0-9_-]{6,}|Bearer\\s+\\S+)/g, '[redacted]'); }
+
+export async function chatComplete({ token, model, messages, tools, signal, timeoutMs = 45000 }) {
+  const ctrl = new AbortController();
+  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
+  if (signal) signal.addEventListener('abort', () => ctrl.abort(), { once: true });
+  try {
+    const res = await fetch(\`\${BASE}/chat/completions\`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: \`Bearer \${token}\` } : {}) },
+      body: JSON.stringify({ model, messages, ...(tools ? { tools } : {}) }),
+      signal: ctrl.signal,
+    });
+    const raw = await res.text();
+    let data; try { data = JSON.parse(raw); } catch { data = raw; }
+    if (!res.ok) throw new Error(scrub(\`HTTP \${res.status} \${typeof data === 'string' ? data : JSON.stringify(data)}\`));
+    const msg = data?.choices?.[0]?.message || {};
+    return { content: msg.content || '', toolCalls: msg.tool_calls || [], usage: data?.usage || null };
+  } finally { clearTimeout(timer); }
+}
+`);
+
+add('src/lib/telemetry.js', `/**
+ * lib/telemetry.js —— token 归因 + 耗时分离(口径见规则 measurement.md)。
+ * 关键:框架调度与 IO 分开计时;await 期间暂停框架计时。
+ */
+const timers = new Map();
+export const t0 = (k) => { timers.set(k, performance.now()); return k; };
+export const t1 = (k) => { const s = timers.get(k); if (s == null) return 0; timers.delete(k); return performance.now() - s; };
+
+export function estimateTokens(text) {
+  const s = String(text ?? ''); let cjk = 0;
+  for (const ch of s) { const c = ch.codePointAt(0); if (c >= 0x4e00 && c <= 0x9fff) cjk += 1; }
+  return Math.round(cjk + (s.length - cjk) / 4);
+}
+
+/** 记一次 LLM 调用的 token(分项 + 输入输出) */
+export function newTokenLedger() { return { calls: 0, buckets: {}, io: { in: 0, out: 0, hasReal: false } }; }
+export function recordCall(ledger, { system = '', tools = [], history = [], retrieved = '', actual } = {}) {
+  const parts = {
+    system: estimateTokens(system),
+    tools: estimateTokens(JSON.stringify(tools || [])),
+    history: estimateTokens(JSON.stringify(history || [])),
+    retrieved: estimateTokens(retrieved),
+  };
+  ledger.calls += 1;
+  for (const [k, v] of Object.entries(parts)) ledger.buckets[k] = (ledger.buckets[k] || 0) + v;
+  const est = Object.values(parts).reduce((a, b) => a + b, 0);
+  ledger.io.in += actual?.prompt_tokens ?? est;
+  ledger.io.out += actual?.completion_tokens ?? 0;
+  if (actual?.prompt_tokens != null) ledger.io.hasReal = true;
+}
+
+/** 输入:输出比例(无输出真值时 ratio=null,不硬凑) */
+export function ioRatio(io) {
+  const { in: i, out: o } = io || { in: 0, out: 0 };
+  return { in: i, out: o, ratio: o > 0 ? +(i / o).toFixed(2) : null, total: i + o };
+}
+`);
+
+add('src/mcp/protocol.js', `/**
+ * mcp/protocol.js —— 最小 MCP(JSON-RPC 2.0)服务端骨架。
+ * ⚠️ Worker 内**只能** import 相对路径纯 JS(module worker 不支持 importmap)。
+ */
+export const MCP_PROTOCOL_VERSION = '2025-06-18';
+
+export class McpServer {
+  constructor(info = {}) { this.info = { name: 'mcp', version: '0.1.0', ...info }; this.tools = new Map(); }
+  register(tool) {
+    if (!tool?.name || typeof tool.handler !== 'function' || !tool.inputSchema) throw new Error('非法工具定义');
+    this.tools.set(tool.name, tool);
+  }
+  async handle(req, ctx = {}) {
+    const { id, method, params } = req || {};
+    const ok = (result) => ({ jsonrpc: '2.0', id, result });
+    const err = (code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
+    if (method === 'initialize') return ok({ protocolVersion: MCP_PROTOCOL_VERSION, serverInfo: this.info, capabilities: { tools: {} } });
+    if (method === 'tools/list') return ok({ tools: [...this.tools.values()].map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) });
+    if (method === 'tools/call') {
+      const t = this.tools.get(params?.name);
+      if (!t) return err(-32602, \`未知工具:\${params?.name}\`);
+      try { return ok(await t.handler(params?.arguments || {}, ctx)); }
+      catch (e) { return err(-32000, String(e?.message || e)); }
+    }
+    if (method === 'ping') return ok({});
+    return err(-32601, \`未实现的方法:\${method}\`);
+  }
+}
+`);
+
+add('src/mcp/tool-specs.js', `/**
+ * mcp/tool-specs.js —— 工具规格(**纯 JS + JSON Schema**,Worker 内可用)。
+ * 主线程侧的 zod 版应与本文件的参数键保持一致(assertSpecParity)。
+ */
+export const TOOL_SPECS = [
+  {
+    name: 'echo',
+    title: '回声',
+    description: '把输入原样返回。最小示例工具。',
+    inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'], additionalProperties: false },
+    buildBrief: (a) => String(a?.text ?? '').trim(),
+  },
+];
+`);
+
+add('src/mcp/worker.js', `/**
+ * mcp/worker.js —— Worker 宿主(浏览器端 MCP)。
+ * 纪律:只 import 相对路径纯 JS;**凭据不进来**(要凭据的动作由主线程做)。
+ */
+import { McpServer } from './protocol.js';
+import { TOOL_SPECS } from './tool-specs.js';
+
+const server = new McpServer({ name: '${project}-mcp', version: '0.1.0' });
+for (const spec of TOOL_SPECS) {
+  server.register({
+    name: spec.name, description: spec.description, inputSchema: spec.inputSchema,
+    handler: async (args) => ({ content: [{ type: 'text', text: 'ok: ' + spec.buildBrief(args) }] }),
+  });
+}
+self.onmessage = async (e) => {
+  const m = e.data;
+  if (!m || typeof m !== 'object') return;
+  const res = await server.handle(m, {});
+  if (res) self.postMessage(res);
+};
+`);
+
+add('package.json', `{
+  "name": "${project}",
+  "version": "0.1.0",
+  "private": true,
+  "type": "module",
+  "description": "无构建智能体(源码即产物,浏览器 ESM 直跑)",
+  "scripts": {
+    "check": "node --check src/app.js && node --check src/lib/graph.js && node --check src/mcp/protocol.js"
+  }
+}
+`);
+
+add('scripts/deploy.sh', `#!/usr/bin/env bash
+# 无构建部署 + 自动部署(参数化,别硬编码站点)。
+set -euo pipefail
+REPO_ROOT="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"; cd "$REPO_ROOT"
+PROJECT="\${PROJECT:-${project}}"
+OBSUTIL="\${OBSUTIL:-/opt/data/bin/obsutil}"
+EP="\${OBS_ENDPOINT:-obs.cn-south-1.myhuaweicloud.com}"
+BUCKET="\${OBS_BUCKET:?请设置 OBS_BUCKET(如 obs://www-fmode.cn)}"
+# 凭据从环境注入;**AK/SK 不写进本文件**
+OBS_AK="\${OBS_AK:?set OBS_AK}"; OBS_SK="\${OBS_SK:?set OBS_SK}"
+PREFIX="dev/$PROJECT"
+
+echo "==> 体检"; node --check src/app.js
+echo "==> index.html 先删后传"
+"$OBSUTIL" rm "$BUCKET/$PREFIX/index.html" -i="$OBS_AK" -k="$OBS_SK" -e="$EP" -f >/dev/null 2>&1 || true
+"$OBSUTIL" cp ./src/index.html "$BUCKET/$PREFIX/index.html" -i="$OBS_AK" -k="$OBS_SK" -e="$EP" -acl=public-read -f \\
+  -meta="Cache-Control:no-cache, no-store, must-revalidate#Content-Type:text/html; charset=utf-8" >/dev/null
+echo "==> 同步源码 + vendor"
+"$OBSUTIL" sync ./src/ "$BUCKET/$PREFIX/" -i="$OBS_AK" -k="$OBS_SK" -e="$EP" -acl=public-read -exclude="index.html"
+[ -d ./vendor ] && "$OBSUTIL" sync ./vendor/ "$BUCKET/$PREFIX/vendor/" -i="$OBS_AK" -k="$OBS_SK" -e="$EP" -acl=public-read
+echo "==> 刷 CDN(自行接刷新脚本;失败要退出非 0)"
+echo "完成:https://<domain>/$PREFIX/"
+`);
+
+add('CLAUDE.md', `# ${project} — 项目规范
+
+本项目遵守 **fmode-code-agent** 规则(智能体开发)。
+
+- 规则索引:\`.claude/rules/agent-architecture.md\`(三层架构)、\`agent-mcp-skill.md\`、
+  \`no-build-langgraph.md\`、\`measurement.md\`、\`deployment.md\`、\`pitfalls.md\`。
+- 前端规范(如用 Lit)见 **fmode-code-lit** 规则仓。
+
+## 自动部署
+\`\`\`bash
+bash scripts/install-hooks.sh     # 装上 pre-push:体检 → 发布 → 验真
+bash scripts/auto-deploy.sh --watch
+\`\`\`
+`);
+
+// ---- 落盘(不覆盖已存在,除非 --force)----
+let created = 0, skipped = 0;
+for (const [rel, content] of Object.entries(files)) {
+  const p = join(root, rel);
+  if (existsSync(p) && !force) { skipped++; console.log('  · 跳过(已存在)', rel); continue; }
+  mkdirSync(dirname(p), { recursive: true });
+  writeFileSync(p, content);
+  if (rel.endsWith('.sh')) { try { chmodSync(p, 0o755); } catch {} }
+  created++;
+}
+
+// 同步规则(若有)
+console.log(`骨架生成:新增 ${created} 个,跳过 ${skipped} 个 → ${root}`);
+console.log('下一步:');
+console.log('  1) 同步 LangGraph 依赖到 vendor/(见规则 no-build-langgraph.md)');
+console.log('  2) 装规则:node <skill_dir>/scripts/install-rules.mjs .');
+console.log('  3) 本地预览:python3 -m http.server(或任意静态服务器)');

+ 107 - 0
skills/fmode-code-agent/scripts/smoke.mjs

@@ -0,0 +1,107 @@
+#!/usr/bin/env node
+/**
+ * smoke.mjs —— fmode-code-agent 技能自检(不联网、不写业务仓)。
+ *
+ * 检查:
+ *   1) 规则文件齐全(CLAUDE.md 索引到的每一篇都在)
+ *   2) SKILL.md frontmatter 完整
+ *   3) 各脚本可被 node --check 解析
+ *   4) scaffold 能生成骨架且产物语法正确(在临时目录里真跑一遍)
+ *   5) 生成骨架里的 MCP 协议**功能可用**(initialize / tools/list / tools/call)
+ *
+ * 退出码非 0 = 有问题。
+ */
+import { existsSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
+import { join, dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { tmpdir } from 'node:os';
+import { execFileSync } from 'node:child_process';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const skillDir = resolve(__dirname, '..');
+const repoRoot = resolve(skillDir, '../..');
+
+let fail = 0;
+const ok = (m) => console.log('  \x1b[32m✓\x1b[0m ' + m);
+const bad = (m) => { console.log('  \x1b[31m✗\x1b[0m ' + m); fail++; };
+
+console.log('fmode-code-agent 自检\n');
+
+// 1) 规则文件
+console.log('[1] 规则文件');
+const rulesDir = join(repoRoot, '.claude', 'rules');
+const mustHave = ['agent-architecture.md', 'agent-mcp-skill.md', 'no-build-langgraph.md',
+  'measurement.md', 'deployment.md', 'llm-gateway.md', 'agent-ui-bridge.md', 'pitfalls.md'];
+for (const f of mustHave) {
+  existsSync(join(rulesDir, f)) ? ok(`rules/${f}`) : bad(`缺 rules/${f}`);
+}
+// CLAUDE.md 里索引到的篇章必须都存在
+const claude = join(repoRoot, 'CLAUDE.md');
+if (existsSync(claude)) {
+  const txt = readFileSync(claude, 'utf8');
+  const linked = [...txt.matchAll(/\(\.\/\.claude\/rules\/([a-z0-9-]+\.md)\)/g)].map((m) => m[1]);
+  const missing = linked.filter((l) => !existsSync(join(rulesDir, l)));
+  missing.length ? bad('CLAUDE.md 索引了不存在的规则:' + missing.join(', ')) : ok(`CLAUDE.md 索引的 ${linked.length} 篇规则均存在`);
+} else bad('缺 CLAUDE.md');
+
+// 2) SKILL.md frontmatter
+console.log('[2] SKILL.md');
+const skillMd = join(skillDir, 'SKILL.md');
+if (existsSync(skillMd)) {
+  const t = readFileSync(skillMd, 'utf8');
+  const has = (k) => new RegExp(`^${k}:`, 'm').test(t);
+  ['name', 'description', 'version', 'license'].every(has) ? ok('frontmatter 完整') : bad('frontmatter 缺字段');
+  /^name:\s*fmode-code-agent\s*$/m.test(t) ? ok('name = fmode-code-agent') : bad('name 不是 fmode-code-agent');
+} else bad('缺 SKILL.md');
+
+// 3) 脚本语法
+console.log('[3] 脚本语法(node --check)');
+for (const s of ['install-rules.mjs', 'scaffold.mjs', 'smoke.mjs']) {
+  const p = join(skillDir, 'scripts', s);
+  if (!existsSync(p)) { bad(`缺 scripts/${s}`); continue; }
+  try { execFileSync(process.execPath, ['--check', p], { stdio: 'pipe' }); ok(`scripts/${s}`); }
+  catch (e) { bad(`scripts/${s} 语法错误:${String(e.stderr || e).slice(0, 120)}`); }
+}
+
+// 4) scaffold 真跑一遍 + 产物语法
+console.log('[4] scaffold 产物');
+const tmp = mkdtempSync(join(tmpdir(), 'fca-smoke-'));
+try {
+  execFileSync(process.execPath, [join(skillDir, 'scripts', 'scaffold.mjs'), tmp, '--project', 'smoke-agent'], { stdio: 'pipe' });
+  const need = ['src/index.html', 'src/app.js', 'src/lib/graph.js', 'src/mcp/protocol.js', 'src/mcp/worker.js', 'scripts/deploy.sh'];
+  for (const f of need) existsSync(join(tmp, f)) ? ok(`产出 ${f}`) : bad(`缺产出 ${f}`);
+  // 产物语法
+  for (const f of ['src/app.js', 'src/lib/graph.js', 'src/lib/fmode-chat.js', 'src/lib/telemetry.js', 'src/mcp/protocol.js', 'src/mcp/worker.js']) {
+    const p = join(tmp, f);
+    if (!existsSync(p)) continue;
+    try { execFileSync(process.execPath, ['--check', p], { stdio: 'pipe' }); }
+    catch { bad(`产物语法错误 ${f}`); }
+  }
+  ok('产物语法全部通过');
+
+  // 5) MCP 协议功能
+  console.log('[5] 生成骨架的 MCP 协议功能');
+  const script = `
+    import { McpServer } from ${JSON.stringify(join(tmp, 'src/mcp/protocol.js'))};
+    import { TOOL_SPECS } from ${JSON.stringify(join(tmp, 'src/mcp/tool-specs.js'))};
+    const s = new McpServer({name:'smoke'});
+    for (const spec of TOOL_SPECS) s.register({ name: spec.name, description: spec.description, inputSchema: spec.inputSchema, handler: async (a)=>({content:[{type:'text',text:'ok:'+spec.buildBrief(a)}]}) });
+    const init = await s.handle({jsonrpc:'2.0',id:1,method:'initialize'});
+    if (!init.result?.serverInfo?.name) throw new Error('initialize 失败');
+    const list = await s.handle({jsonrpc:'2.0',id:2,method:'tools/list'});
+    if (!list.result?.tools?.length) throw new Error('tools/list 空');
+    const call = await s.handle({jsonrpc:'2.0',id:3,method:'tools/call',params:{name:'echo',arguments:{text:'hi'}}});
+    if (call.result?.content?.[0]?.text !== 'ok:hi') throw new Error('tools/call 结果不符');
+    console.log('protocol-ok');
+  `;
+  try {
+    const out = execFileSync(process.execPath, ['--input-type=module', '-e', script], { stdio: 'pipe' }).toString();
+    out.includes('protocol-ok') ? ok('MCP 协议 initialize/tools-list/tools-call 均可跑') : bad('MCP 协议输出异常');
+  } catch (e) { bad('MCP 协议功能测试失败:' + String(e.stderr || e).slice(0, 160)); }
+} finally {
+  rmSync(tmp, { recursive: true, force: true });
+}
+
+console.log('');
+if (fail === 0) { console.log('自检通过 ✓'); process.exit(0); }
+console.log(`自检失败(${fail} 项)`); process.exit(1);

+ 19 - 0
skills/fmode-code-agent/skill-package-manifest.json

@@ -0,0 +1,19 @@
+{
+  "name": "fmode-code-agent",
+  "version": "0.1.0",
+  "description": "FmodeCode / Claude Code 独立技能包:纯 JS 网页版 Bot 智能体实现规则(浏览器 + Node 双兼容、无构建)。含 Agent/MCP/Skill 三层架构、LangGraph.js 浏览器无构建配方、浏览器端 MCP(Web Worker)、token/速度实测方法、自动部署。",
+  "plugin": "fmode-code-agent",
+  "skills": [
+    "fmode-code-agent"
+  ],
+  "entrySkill": "fmode-code-agent",
+  "npmPackage": "skill-code-agent",
+  "smokeCommand": "npm run smoke",
+  "installCommand": "node skills/fmode-code-agent/scripts/install.mjs install --force",
+  "workspaceInstallCommand": "node skills/fmode-code-agent/scripts/install-rules.mjs .",
+  "workspaceSkillPath": ".claude/skills/fmode-code-agent/SKILL.md",
+  "globalSkillPath": "~/.claude/skills/fmode-code-agent/SKILL.md",
+  "installHint": "工作区安装规则:node skills/fmode-code-agent/scripts/install-rules.mjs . ,会写入 ./.claude/rules/。生成脚手架:node skills/fmode-code-agent/scripts/scaffold.mjs ./my-agent --project my-agent。规则来源优先级:--from > 本仓 .claude/rules > 内置最小集。依赖:@langchain/langgraph@1.5.1 等(浏览器走 importmap,Node 走 npm)。",
+  "copyright": "Copyright (c) 2026 未来飞马 Fmode",
+  "license": "MPL-2.0"
+}