Bladeren bron

chore: 补 __pycache__ 排除规则 + 登记 code-lit/code-agent

- 排除 __pycache__/.pyc/.venv,避免编译缓存进 dist
- 新增 fmode-code-agent(本地源,101 行完整内容)
- fmode-code-lit 标记 pending(GitHub 源访问超时,待建 Gogs 镜像)

验收:12 技能 / 触发词 12/12 / 品牌违规 0 / 缓存泄漏 0
liuyuyang 1 dag geleden
bovenliggende
commit
9a892dcbf2

+ 6 - 2
dist/skills-index.json

@@ -1,7 +1,7 @@
 {
   "name": "fmode-skills",
-  "version": "1.1.0",
-  "builtAt": "2026-10-30T15:44:18.203Z",
+  "version": "1.2.0",
+  "builtAt": "2026-10-30T15:50:45.934Z",
   "skills": [
     {
       "name": "fmode-storage",
@@ -46,6 +46,10 @@
     {
       "name": "fmode-video-editing",
       "path": "skills/fmode-video-editing"
+    },
+    {
+      "name": "fmode-code-agent",
+      "path": "skills/fmode-code-agent"
     }
   ]
 }

BIN
dist/skills/fmode-agent-log/scripts/__pycache__/collect_metrics.cpython-313.pyc


BIN
dist/skills/fmode-agent-log/scripts/__pycache__/upload_log.cpython-313.pyc


+ 375 - 0
dist/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.

+ 68 - 0
dist/skills/fmode-code-agent/README.md

@@ -0,0 +1,68 @@
+# fmode-code-agent · 纯 JS 网页版 Bot 智能体技能
+
+> **这是方便数字人(FmodeAgent / FmodeCode)进行项目开发的基础规则技能。**
+> 它把踩过的坑沉淀成硬性约束,让 AI 在生成代码时不重犯同类错误。
+
+> **未来飞马 — 让 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 智能体**的
+FmodeCode 技能:Agent 决策 / MCP 协议 / Skill 执行三层分离,LangGraph.js 浏览器直跑,
+token/速度**实测**,**提交后自动部署**。
+
+配套 **规则仓**(本仓 `.claude/rules/`)提供架构、MCP/Skill、无构建配方、实测、部署等完整规范。
+
+适用 **FmodeCode / FmodeCode / 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 node_modules/@fmode/skill-code-agent/skills/fmode-code-agent/scripts/scaffold.mjs ./my-agent --project my-agent
+
+# 给已有项目装规则
+node node_modules/@fmode/skill-code-agent/skills/fmode-code-agent/scripts/install-rules.mjs .
+
+# 自检(含 scaffold 产物 + MCP 协议功能测试)
+node node_modules/@fmode/skill-code-agent/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))。商标「未来飞马」等不随协议授权。

+ 101 - 0
dist/skills/fmode-code-agent/SKILL.md

@@ -0,0 +1,101 @@
+---
+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 规则仓的分层与命名规范。"
+version: "1.0.0"
+license: "MPL-2.0"
+author: "Fmode (未来飞马)"
+keywords: [智能体, agent, MCP, Skill, LangGraph, Re-act, 工具调用, function, calling, Web, Worker, MCP]
+---
+
+# 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
dist/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);

+ 48 - 0
dist/skills/fmode-code-agent/package.json

@@ -0,0 +1,48 @@
+{
+  "name": "@fmode/skill-code-agent",
+  "version": "0.1.1",
+  "description": "FmodeCode 技能:纯 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": [
+    "digital-human",
+    "agent-rules",
+    "FmodeCode",
+    "claude-skill",
+    "fmode",
+    "agent",
+    "mcp",
+    "skill",
+    "langgraph",
+    "no-build",
+    "native-esm",
+    "importmap",
+    "telemetry"
+  ],
+  "license": "MPL-2.0",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/fmodecn/fmode-code-agent.git",
+    "directory": "skills/fmode-code-agent"
+  },
+  "homepage": "https://github.com/fmodecn/fmode-code-agent#readme",
+  "author": {
+    "name": "liuyuyang",
+    "email": "liu@fmode.cn"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}

+ 74 - 0
dist/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
dist/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
dist/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  最小 Fmode 兼容客户端
+ *            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 —— 最小 Fmode 兼容客户端(自写,不引 @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:-obsutil}"
+EP="\${OBS_ENDPOINT:-obs.<region>.myhuaweicloud.com}"
+BUCKET="\${OBS_BUCKET:?请设置 OBS_BUCKET(如 obs://my-bucket)}"
+# 凭据从环境注入;**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
dist/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);

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

@@ -0,0 +1,20 @@
+{
+  "name": "fmode-code-agent",
+  "version": "0.1.1",
+  "description": "FmodeCode / FmodeCode 独立技能包:纯 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": "@fmode/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",
+  "summary": "数字人开发基础规则技能:纯 JS 网页版 Agent(Agent/MCP/Skill 三层)"
+}

BIN
dist/skills/fmode-video-lapian/scripts/__pycache__/common.cpython-313.pyc


+ 2 - 2
scripts/build-dist.mjs

@@ -40,9 +40,9 @@ const err = (m) => log(`${C.r}✗${C.x} ${m}`);
 /** 需要排除的文件(不进 dist) */
 const EXCLUDE = [
   /(^|\/)\.git(\/|$)/, /(^|\/)node_modules(\/|$)/, /(^|\/)\.cache(\/|$)/, /(^|\/)dist(\/|$)/,
-  /(^|\/)\.github(\/|$)/,
+  /(^|\/)\.github(\/|$)/, /(^|\/)__pycache__(\/|$)/, /(^|\/)\.venv(\/|$)/, /(^|\/)venv(\/|$)/,
   /^\.gitignore$/, /^package-lock\.json$/, /^\.DS_Store$/,
-  /\.bak$/, /\.bak-/, /\.log$/, /\.old$/, /~$/,
+  /\.bak$/, /\.bak-/, /\.log$/, /\.old$/, /~$/, /\.pyc$/, /\.pyo$/,
 ];
 
 function shouldExclude(rel) {

+ 3 - 2
skill-sources.json

@@ -84,13 +84,14 @@
       "repo": "https://github.com/fmodecn/skill-code-lit.git",
       "path": ".",
       "target": "skills/fmode-code-lit",
-      "status": "repo"
+      "status": "pending-source",
+      "note": "GitHub 源访问超时(clone ETIMEDOUT);待建 Gogs 镜像后改回 repo"
     },
     "fmode-code-agent": {
       "repo": "https://git.fmode.cn/fmode/fmode-code-agent.git",
       "path": "skills/fmode-code-agent",
       "target": "skills/fmode-code-agent",
-      "status": "repo",
+      "status": "local-repo",
       "localPath": "/opt/data/git-repos/fmode-code-agent"
     }
   },