Преглед изворни кода

feat: 新增数字人开发基础规则技能 fmode-code-lit / fmode-code-agent

- fmode-code-lit 1.0.0:Lit 原生 ESM 无构建前端(含多页面 light DOM 五条死线、light-slot.js、check-elements.mjs)
- fmode-code-agent 0.1.1:纯 JS 网页版 Agent(Agent/MCP/Skill 三层)
- 两个技能均为自包含目录:SKILL.md + scripts + bin + references/(RULES-INDEX.md + rules/*.md 完整规则)
- inventory / manifest / README 同步:版本 0.3.0,子技能 17 个
- 已脱敏:无内网地址、桶名、密钥
liuyuyang пре 19 часа
родитељ
комит
60d75ba62d
48 измењених фајлова са 5907 додато и 26 уклоњено
  1. 9 1
      README.md
  2. 11 3
      package.json
  3. 85 19
      skill-package-manifest.json
  4. 375 0
      skills/fmode-code-agent/LICENSE.txt
  5. 72 0
      skills/fmode-code-agent/SKILL.md
  6. 33 0
      skills/fmode-code-agent/bin/fmode-code-agent.js
  7. 87 0
      skills/fmode-code-agent/references/RULES-INDEX.md
  8. 70 0
      skills/fmode-code-agent/references/rules/agent-architecture.md
  9. 87 0
      skills/fmode-code-agent/references/rules/agent-mcp-skill.md
  10. 64 0
      skills/fmode-code-agent/references/rules/agent-ui-bridge.md
  11. 81 0
      skills/fmode-code-agent/references/rules/deployment.md
  12. 83 0
      skills/fmode-code-agent/references/rules/llm-gateway.md
  13. 109 0
      skills/fmode-code-agent/references/rules/measurement.md
  14. 110 0
      skills/fmode-code-agent/references/rules/no-build-langgraph.md
  15. 53 0
      skills/fmode-code-agent/references/rules/pitfalls.md
  16. 74 0
      skills/fmode-code-agent/scripts/install-rules.mjs
  17. 42 0
      skills/fmode-code-agent/scripts/install.mjs
  18. 372 0
      skills/fmode-code-agent/scripts/scaffold.mjs
  19. 107 0
      skills/fmode-code-agent/scripts/smoke.mjs
  20. 20 0
      skills/fmode-code-agent/skill-package-manifest.json
  21. 375 0
      skills/fmode-code-lit/LICENSE.txt
  22. 81 0
      skills/fmode-code-lit/SKILL.md
  23. 38 0
      skills/fmode-code-lit/bin/fmode-code-lit.js
  24. 121 0
      skills/fmode-code-lit/references/RULES-INDEX.md
  25. 83 0
      skills/fmode-code-lit/references/light-slot.js
  26. 105 0
      skills/fmode-code-lit/references/rules/architecture.md
  27. 153 0
      skills/fmode-code-lit/references/rules/components.md
  28. 102 0
      skills/fmode-code-lit/references/rules/context-di.md
  29. 211 0
      skills/fmode-code-lit/references/rules/core-library.md
  30. 172 0
      skills/fmode-code-lit/references/rules/deployment.md
  31. 113 0
      skills/fmode-code-lit/references/rules/http-utils.md
  32. 118 0
      skills/fmode-code-lit/references/rules/modules/README.md
  33. 98 0
      skills/fmode-code-lit/references/rules/modules/examples.md
  34. 118 0
      skills/fmode-code-lit/references/rules/pitfalls.md
  35. 98 0
      skills/fmode-code-lit/references/rules/project-structure.md
  36. 453 0
      skills/fmode-code-lit/references/rules/routing-best-practices.md
  37. 163 0
      skills/fmode-code-lit/references/rules/routing.md
  38. 108 0
      skills/fmode-code-lit/references/rules/services.md
  39. 136 0
      skills/fmode-code-lit/references/rules/tabs-keepalive.md
  40. 288 0
      skills/fmode-code-lit/references/rules/web-component-library.md
  41. 76 0
      skills/fmode-code-lit/scripts/check-elements.mjs
  42. 72 0
      skills/fmode-code-lit/scripts/install-rules.mjs
  43. 57 0
      skills/fmode-code-lit/scripts/install.mjs
  44. 221 0
      skills/fmode-code-lit/scripts/new.mjs
  45. 308 0
      skills/fmode-code-lit/scripts/scaffold.mjs
  46. 60 0
      skills/fmode-code-lit/scripts/smoke.mjs
  47. 18 0
      skills/fmode-code-lit/skill-package-manifest.json
  48. 17 3
      skills/inventory.json

+ 9 - 1
README.md

@@ -37,7 +37,7 @@ node scripts/installer.mjs --check      # 检查现状(只读)
 | Claude Code / FmodeCode | `~/.claude/skills/` |
 | Hermes Agent | `~/.hermes/skills/` |
 
-## 子技能清单(15 个,纯接口驱动)
+## 子技能清单(17 个:15 个接口驱动业务技能 + 2 个数字人开发基础规则技能)
 
 > 所有子技能只依赖「公开 HTTP 接口 + 用户 token(env) + Agent 原生工具」,不依赖任何需另行安装的软件/fmode-studio。
 
@@ -52,6 +52,14 @@ node scripts/installer.mjs --check      # 检查现状(只读)
 - `fmode-listen` — 录音转写(公开网关 API)
 - `fmode-storage` — 对象存储与公开分享
 
+### 数字人开发基础规则技能(Development Rules)
+
+> 这两个是**方便数字人(FmodeAgent / FmodeCode)进行项目开发的基础规则技能**:把踩过的坑沉淀成硬性约束,
+> 让 AI 在生成代码时不重犯同类错误。纯本地规则,不调用任何接口。
+
+- `fmode-code-lit` — Lit 原生 ESM 无构建前端(core / wc / page 分层、History 路由、多标签 KeepAlive、**light DOM 多页面五条死线**)。npm:`@fmode/skill-code-lit`;GitHub:<https://github.com/fmodecn/skill-code-lit>
+- `fmode-code-agent` — 纯 JS 网页版 Bot 智能体(Agent / MCP / Skill 三层、LangGraph.js 浏览器无构建、token 实测、自动部署)。npm:`@fmode/skill-code-agent`;GitHub:<https://github.com/fmodecn/fmode-code-agent>
+
 ### 应用级(Business Applications)
 - `fmode-voc-intelligence` — 小红书/抖音/视频号 VOC 情报
 - `fmode-tihao` — 提号 SOP 达人研究

+ 11 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "fmode-skills",
-  "version": "0.1.0",
+  "version": "0.3.0",
   "description": "Fmode 总技能库 —— 一次安装,全部 Fmode 技能自动落盘(数字分身初始化、云资源领取、前后端发布、VOC 情报、企微、支付等)。安装后由 installer.mjs 按宿主分发子技能。",
   "type": "module",
   "main": "scripts/installer.mjs",
@@ -9,8 +9,16 @@
   },
   "license": "MPL-2.0",
   "keywords": [
-    "fmode", "harness", "skillhub", "agent-skills",
-    "未来飞马", "技能库", "智能体技能", "数字分身", "云资源", "VOC"
+    "fmode",
+    "harness",
+    "skillhub",
+    "agent-skills",
+    "未来飞马",
+    "技能库",
+    "智能体技能",
+    "数字分身",
+    "云资源",
+    "VOC"
   ],
   "engines": {
     "node": ">=18"

+ 85 - 19
skill-package-manifest.json

@@ -2,10 +2,23 @@
   "slug": "fmode-skill-fmode-skills",
   "name": "fmode-skills",
   "displayName": "fmode-skills",
-  "version": "0.2.0",
+  "version": "0.3.0",
   "summary": "Fmode总技能库:一次安装,15个纯接口驱动子技能自动落盘(数字分身/云资源/发布/VOC/支付)",
   "description": "Fmode 总技能库(Meta 技能)。用户只需安装本技能,installer.mjs 自动检测宿主(WorkBuddy/Claude Code/Hermes)并把全部 15 个子技能分发到对应技能目录。所有子技能只依赖公开 HTTP 接口 + 用户 token(env) + Agent 原生工具,不依赖任何桌面软件。包含:账号注册登录、企业开通、云函数、云资源领取、静态发布、Git归档、视觉识别、录音转写、对象存储、VOC情报、提号SOP、新品研发、微信支付进件、课件生产、报告发布。",
-  "tags": ["FmodeAgent", "HermesAgent", "FmodeCode", "ClaudeCode", "技能库", "安装器", "系统级", "平台基础设施", "数字分身", "VOC", "微信支付", "云资源"],
+  "tags": [
+    "FmodeAgent",
+    "HermesAgent",
+    "FmodeCode",
+    "ClaudeCode",
+    "技能库",
+    "安装器",
+    "系统级",
+    "平台基础设施",
+    "数字分身",
+    "VOC",
+    "微信支付",
+    "云资源"
+  ],
   "platform": "Both",
   "level": "系统级",
   "category": "平台基础设施",
@@ -13,24 +26,77 @@
   "license": "MPL-2.0",
   "author": "Fmode (未来飞马)",
   "homepage": "https://git.fmode.cn/fmode/fmode-skills",
-  "changelog": "v0.2.0: 剔除 ffmpeg/qiwei(依赖本地/软件),确立纯接口驱动架构,15 子技能。",
+  "changelog": "v0.3.0: 新增数字人开发基础规则技能 fmode-code-lit / fmode-code-agent(17 子技能)。v0.2.0: 剔除 ffmpeg/qiwei(依赖本地/软件),确立纯接口驱动架构,15 子技能。",
   "arch": "esm",
-  "skills_count": 15,
+  "skills_count": 17,
   "skills": [
-    {"name": "fmode-account", "level": "service"},
-    {"name": "fmode-enterprise", "level": "service"},
-    {"name": "fmode-cloud-functions", "level": "service"},
-    {"name": "fmode-cloud-resources", "level": "service"},
-    {"name": "fmode-static-publish", "level": "service"},
-    {"name": "fmode-git-archive", "level": "service"},
-    {"name": "fmode-vision", "level": "service"},
-    {"name": "fmode-listen", "level": "service"},
-    {"name": "fmode-storage", "level": "service"},
-    {"name": "fmode-voc-intelligence", "level": "application"},
-    {"name": "fmode-tihao", "level": "application"},
-    {"name": "fmode-product-lab", "level": "application"},
-    {"name": "fmode-pay", "level": "application"},
-    {"name": "fmode-courseware", "level": "application"},
-    {"name": "fmode-report-publish", "level": "application"}
+    {
+      "name": "fmode-account",
+      "level": "service"
+    },
+    {
+      "name": "fmode-enterprise",
+      "level": "service"
+    },
+    {
+      "name": "fmode-cloud-functions",
+      "level": "service"
+    },
+    {
+      "name": "fmode-cloud-resources",
+      "level": "service"
+    },
+    {
+      "name": "fmode-static-publish",
+      "level": "service"
+    },
+    {
+      "name": "fmode-git-archive",
+      "level": "service"
+    },
+    {
+      "name": "fmode-vision",
+      "level": "service"
+    },
+    {
+      "name": "fmode-listen",
+      "level": "service"
+    },
+    {
+      "name": "fmode-storage",
+      "level": "service"
+    },
+    {
+      "name": "fmode-voc-intelligence",
+      "level": "application"
+    },
+    {
+      "name": "fmode-tihao",
+      "level": "application"
+    },
+    {
+      "name": "fmode-product-lab",
+      "level": "application"
+    },
+    {
+      "name": "fmode-pay",
+      "level": "application"
+    },
+    {
+      "name": "fmode-courseware",
+      "level": "application"
+    },
+    {
+      "name": "fmode-report-publish",
+      "level": "application"
+    },
+    {
+      "name": "fmode-code-lit",
+      "level": "development"
+    },
+    {
+      "name": "fmode-code-agent",
+      "level": "development"
+    }
   ]
 }

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

@@ -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.

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

@@ -0,0 +1,72 @@
+---
+slug: fmode-skill-fmode-code-agent
+displayName: fmode-code-agent
+version: 0.1.1
+name: fmode-code-agent
+description: "数字人开发基础规则:纯 JS 网页版 Bot 智能体开发(浏览器 + Node 双兼容、无构建)。Agent/MCP/Skill 三层分离、LangGraph.js 浏览器无构建配方、浏览器端 MCP(Web Worker)、token 实测、提交后自动部署。触发词:agent、智能体、MCP、Skill、LangGraph、function calling、工具调用、Re-act、Web Worker MCP、Agent 架构、token 实测、自动部署。"
+tags: [FmodeAgent, FmodeCode, 开发级, code-agent, 数字人基础规则]
+platform: Both
+level: 开发级
+category: 开发规范
+icon: 🤖
+license: MPL-2.0
+author: Fmode (未来飞马)
+homepage: https://github.com/fmodecn/fmode-code-agent
+npm: "@fmode/skill-code-agent"
+---
+
+# fmode-code-agent — 纯 JS 网页版 Bot 智能体
+
+> **这是方便数字人(FmodeAgent / FmodeCode)进行项目开发的基础规则技能。**
+> 它把踩过的坑沉淀成硬性约束,让 AI 在生成代码时不重犯同类错误。
+
+用纯 JS(浏览器 + Node 双兼容、无构建)写网页版 Bot 智能体:Agent 决策 / MCP 协议 / Skill 执行三层分离,
+LangGraph.js 浏览器直跑,token/速度**实测**,**提交后自动部署**。
+
+npm 包:`@fmode/skill-code-agent`。完整规则见 `references/RULES-INDEX.md` 与其索引的 `references/rules/`。
+姊妹技能:`fmode-code-lit`(Lit 无构建前端),两者可叠加使用。
+
+## 触发
+
+新建网页内跑的智能体(对话 + 工具调用)· 把能力封装成 MCP 工具 / 接入多个 MCP · 用 LangGraph.js 做图编排(浏览器无构建)·
+给智能体加 token/速度实测与输入输出比例 · 配自动部署。
+
+## 一句话架构
+
+```
+Agent(决策:调不调工具、调哪个,可换框架)
+  └─ MCP(协议:JSON-RPC 2.0,浏览器常跑在 Web Worker)
+       └─ Skill(实现:纯函数式能力)
+```
+
+换 Agent 框架时,**MCP 与 Skill 一行不改** —— 这是分层的验收标准。
+
+## 四条铁律
+
+1. **浏览器 + Node 双兼容**:核心纯 JS;`fs`/`path` 只在分发脚本里用。
+2. **不用构建**:不引打包器;依赖走 importmap(浏览器)或 npm(Node)。
+3. **凭据不进 Worker / 日志 / 代码**:要凭据的动作在**主线程**执行。
+4. **参数以实测为准**:网关会**静默忽略**参数 —— 必须**验证副作用**,不能只看 200。
+
+## 用法
+
+```bash
+npm i @fmode/skill-code-agent      # 安装(含本技能目录)
+
+node scripts/scaffold.mjs ./my-agent --project my-agent   # 生成智能体骨架
+node scripts/install-rules.mjs .                          # 给已有项目装规则
+node scripts/smoke.mjs                                    # 自检(含 MCP 协议功能测试)
+```
+
+## 依赖版本(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
+```
+
+## 诚实失败
+
+生成前先探测目标目录;已存在文件不覆盖(除非 `--force`)。未实测的能力(多 MCP 并发、HTTP/SSE 传输…)
+在规则里明确标注「未验证」,不写成规范。

+ 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);

+ 87 - 0
skills/fmode-code-agent/references/RULES-INDEX.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 为准;智能体细节以本仓为准。

+ 70 - 0
skills/fmode-code-agent/references/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
skills/fmode-code-agent/references/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
skills/fmode-code-agent/references/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
skills/fmode-code-agent/references/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
skills/fmode-code-agent/references/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
skills/fmode-code-agent/references/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
skills/fmode-code-agent/references/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
skills/fmode-code-agent/references/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. **入库失败不应影响主流程**:记录是「尽力而为」,任何失败都不该让出图失败。

+ 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:-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
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
skills/fmode-code-agent/skill-package-manifest.json

@@ -0,0 +1,20 @@
+{
+  "name": "fmode-code-agent",
+  "version": "0.1.1",
+  "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": "@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 三层)"
+}

+ 375 - 0
skills/fmode-code-lit/LICENSE.txt

@@ -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.

+ 81 - 0
skills/fmode-code-lit/SKILL.md

@@ -0,0 +1,81 @@
+---
+slug: fmode-skill-fmode-code-lit
+displayName: fmode-code-lit
+version: 1.0.0
+name: fmode-code-lit
+description: "数字人开发基础规则:Lit 原生 ESM 无构建前端(零编译/零打包,浏览器直跑)。分层 core/wc/page、signals 响应式、@lit/context DI、History 路由、多标签 KeepAlive;沉淀多页面开发 light DOM 五条死线、子路径路由丢前缀、CDN 假刷新等实战教训。触发词:lit、lit 项目、lit 组件、无构建、ESM、WebComponent、importmap、signals、多页面白屏、子路径部署、keepalive、前端部署。"
+tags: [FmodeAgent, FmodeCode, 开发级, code-lit, 数字人基础规则]
+platform: Both
+level: 开发级
+category: 开发规范
+icon: 🧱
+license: MPL-2.0
+author: Fmode (未来飞马)
+homepage: https://github.com/fmodecn/skill-code-lit
+npm: "@fmode/skill-code-lit"
+---
+
+# fmode-code-lit — Lit 原生 ESM 无构建前端
+
+> **这是方便数字人(FmodeAgent / FmodeCode)进行项目开发的基础规则技能。**
+> 它把踩过的坑沉淀成硬性约束,让 AI 在生成代码时不重犯同类错误。
+
+用无构建/无打包/纯原生 ESM + Lit 3.x 开发前端:源码即产物,浏览器直跑。分层(组件 / 服务 / 工具),
+`@lit-labs/signals` 响应式、`@lit/context` 依赖注入、自写 History 路由、多标签 KeepAlive。
+
+npm 包:`@fmode/skill-code-lit`(`npx @fmode/skill-code-lit@latest install`)。
+完整规则见 `references/RULES-INDEX.md` 与其索引的 `references/rules/`。
+
+## 触发
+
+新建/初始化 lit 项目 · 写 lit 组件 · 无构建前端 · 原生 ESM + importmap · WebComponent · signals ·
+context DI · 多标签 KeepAlive · 多页面白屏排查 · 子路径部署 · 部署验收。
+
+## 三条铁律
+
+1. **源码即产物**:不引入任何构建/打包器(rollup / esbuild / vite / webpack),写好的 `.js` 直接上线。
+2. **依赖不落地项目**:`lit` 等运行时依赖由 importmap 从静态依赖仓解析,禁止拷进业务仓。
+3. **分层不可越界**:组件(渲染)→ 服务(业务与状态)→ 工具(无状态函数)。
+
+## 无构建硬约束
+
+禁装饰器(`@customElement` / `@property` / `@state` / `@consume`),一律用
+`static properties` + `customElements.define()` + `ContextConsumer`。
+
+## 多页面开发铁律(light DOM 五条死线)
+
+`createRenderRoot(){return this}` 是**渲染模型切换**,切完 `<slot>`、`:host`、`::slotted` 全部失效:
+
+1. light DOM 下**禁用 `<slot>`** —— 自检:`document.querySelector('main').children.length`,为 0 就是投影断了。
+2. 外壳**按路由自己产出页面元素**,不靠投影传内容。
+3. 真需传内容的组件(卡片)用 `references/light-slot.js` 的 `capture`(渲染前)/ `project`(updated 后)手动搬,`dataset.projected` 做幂等。
+4. 自定义元素必须 `display: block; min-width: 0`(例外元素写 `display: skip`);跑 `scripts/check-elements.mjs` 自检。
+5. 图表容器用 `height: 100% + min-height`,不用 `flex: 1`(父级无确定高度时算出来是 0)。
+
+## 用法
+
+```bash
+# 安装到 ~/.claude/skills/
+npx @fmode/skill-code-lit@latest install
+
+# 或直接用本目录脚本
+node scripts/scaffold.mjs ./my-admin --project my-admin   # 生成项目骨架
+node scripts/install-rules.mjs .                          # 把规则装入已有项目 .claude/rules/
+node scripts/new.mjs component user-card                  # 生成组件 / 服务 / 页面 / core / wc
+node scripts/check-elements.mjs src                       # 校验自定义元素 display 声明
+node scripts/smoke.mjs                                    # 自检
+```
+
+## 关键约定
+
+组件 `kebab-case.js` 类 `PascalCase` · 页面 `page-*.js` · 服务 `*-service.js` 单例 · 相对导入带 `.js` ·
+全局 signal + `SignalWatcher` · DI 用 `@lit/context` · 子目录 `<base href>` · CDN History 回退 200 禁 302。
+
+## 部署
+
+依赖 → 静态依赖仓(`.js` 为 `text/javascript`、CORS 放行、HTTP/2);业务码 → `obsutil sync src/` + 刷 CDN;
+`index.html` no-cache 并先删后传。**验收不能只 curl 首页**:模块图爬取 + 深链接 + 真实浏览器。
+
+## 诚实失败
+
+生成前先探测目标目录;已存在文件不覆盖(除非 `--force`)。依赖版本无法解析时明确报错,不生成跑不起来的骨架。

+ 38 - 0
skills/fmode-code-lit/bin/fmode-code-lit.js

@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+/**
+ * fmode-code-lit CLI
+ * 用法:
+ *   npx @fmode/skill-code-lit install      # 装技能到 ~/.claude/skills/(默认)
+ *   npx @fmode/skill-code-lit workspace    # 装技能到 ./.claude/skills/
+ *   npx @fmode/skill-code-lit check        # 校验安装
+ *   npx @fmode/skill-code-lit scaffold <dir> --project <name>   # 生成无构建 Lit 项目骨架
+ *   npx @fmode/skill-code-lit new <component|service|page> <name>
+ *   npx @fmode/skill-code-lit rules [dir]  # 把前端规则装入目标项目 ./.claude/rules/
+ */
+const { spawnSync } = require('node:child_process');
+const path = require('node:path');
+
+const SCRIPTS = path.join(__dirname, '..', 'scripts');
+const [cmd = 'install', ...rest] = process.argv.slice(2);
+
+const MAP = {
+  install: 'install.mjs',
+  workspace: 'install.mjs',
+  check: 'install.mjs',
+  scaffold: 'scaffold.mjs',
+  new: 'new.mjs',
+  rules: 'install-rules.mjs',
+};
+
+const script = MAP[cmd];
+if (!script) {
+  console.error(`未知命令:${cmd}\n可用:${Object.keys(MAP).join(', ')}`);
+  process.exit(2);
+}
+// workspace 需作为首参透传给 install.mjs(install.mjs 默认 install)
+const argv = (cmd === 'workspace' || cmd === 'install' || cmd === 'check')
+  ? [path.join(SCRIPTS, script), cmd, ...rest]
+  : [path.join(SCRIPTS, script), ...rest];
+
+const r = spawnSync(process.execPath, argv, { stdio: 'inherit' });
+process.exit(r.status ?? 1);

+ 121 - 0
skills/fmode-code-lit/references/RULES-INDEX.md

@@ -0,0 +1,121 @@
+# fmode-code-lit — Lit 原生 ESM 无构建开发规则总纲
+
+> 本文件是 AI(Claude Code / FmodeCode)在本仓、或引用本仓规则时的**第一入口**。
+> 写任何 Lit 前端代码前,先读本文件,再按下表索引到具体规则篇。
+> 源稿:`.claude/_lit-dev-manual.md`(用户提供的《Lit 原生 ESM 无构建开发最佳实践》)。
+
+## 0. 三条铁律(违反即返工)
+
+1. **源码即产物**:不引入任何构建/打包器(rollup / esbuild / vite / webpack)。写好的 `.js` 直接上传 OBS,浏览器原生 ESM 执行。
+2. **依赖不落地项目**:`lit`、`@lit-labs/*` 等运行时依赖一律从 **FMODE Repose** 公共 importmap 引入,**禁止**把依赖源码拷进业务仓、禁止业务仓 `import` 裸 `node_modules`。
+3. **分层不可越界**:组件(渲染)→ 服务(业务与状态)→ 工具(无状态函数)。组件禁止直接 `fetch`、禁止写复杂业务逻辑;服务禁止触碰 DOM。
+   > 跨项目复用时升一级为三分层:**core**(纯 ESM,零 DOM)→ **wc**(Web Component,零业务)→ **page**(组装)。见 [§2.1](#21-分层一句话core--wc--page)。
+
+## 1. 规则索引
+
+| # | 规则篇 | 内容 | 何时读 |
+|---|--------|------|--------|
+| 1 | [architecture.md](./.claude/rules/architecture.md) | 整体架构、分层职责、运行流程、部署拓扑 | 起项目 / 定架构 |
+| 2 | [project-structure.md](./.claude/rules/project-structure.md) | 目录规范、命名、文件摆放、二级路径 base | 建目录 / 放文件 |
+| 3 | [components.md](./.claude/rules/components.md) | LitElement 组件规范、Shadow DOM、属性/事件、Slot | 写组件 |
+| 4 | [services.md](./.claude/rules/services.md) | Service 分层、Signal 状态、单例、可单测 | 写服务 / 状态 |
+| 5 | [context-di.md](./.claude/rules/context-di.md) | `@lit-labs/context` 令牌与 provide/consume | 做依赖注入 |
+| 6 | [routing.md](./.claude/rules/routing.md) | History 路由、守卫、404(子路径部署自写 `utils/router.js`) | 做路由 |
+| 7 | [tabs-keepalive.md](./.claude/rules/tabs-keepalive.md) | 后台多标签 + KeepAlive(实例常驻) | 做后台壳 |
+| 8 | [http-utils.md](./.claude/rules/http-utils.md) | 原生 Fetch 封装、token 拦截、错误处理、utils | 写请求 / 工具 |
+| 9 | [deployment.md](./.claude/rules/deployment.md) | FMODE Repose 依赖托管 + OBS+CDN 业务部署 + importmap | 上线 / 配依赖 |
+| 10 | [pitfalls.md](./.claude/rules/pitfalls.md) | CORS / 版本冲突 / 302 陷阱 / base href 等坑位 | 排查问题 |
+| 11 | [modules/](./.claude/rules/modules/) | 功能模块 / 通用能力 / 业务模块 划分最佳实践 | 模块拆分 |
+| 12 | [routing-best-practices.md](./.claude/rules/routing-best-practices.md) | **子路径部署路由实战**:base 三处同源 / `new URL` 丢前缀 / 弃用 `@lit-labs/router` / CDN 刷新假成功 / 验收不能只 curl | 做路由 / 子路径部署 / 上线验收 |
+| 13 | [core-library.md](./.claude/rules/core-library.md) | **核心库(core)**:纯 ESM 零浏览器依赖、只 export 纯逻辑、Node 可测、无构建打包为单文件、版本管理 | 抽逻辑层 / 跨框架复用 / 写库 |
+| 14 | [web-component-library.md](./.claude/rules/web-component-library.md) | **组件库(wc)**:**必须用 Lit(LitElement/html/css)实现、禁原生 wc 手写**、customElements 注册、属性/事件/Slot/`::part`、跨框架用法、依赖 core、样式隔离、无装饰器 | 写 Web Component / 组件跨框架复用 |
+| 15 | [retrospective.md](./docs/retrospective.md) | 对标 Angular zoneless 的技术复盘 | 理解选型理由 |
+
+## 2. 架构一句话
+
+浏览器打开 URL → CDN 回退规则把不存在的路径 **200(非 302)** 返回 `index.html` → `index.html` 加载 **公共 importmap**(指向 FMODE Repose)→ 加载 `app.js` → `import` lit 依赖 → 初始化 Store / 路由 / 根组件 → 渲染。全程**零编译**。
+
+## 2.1 分层一句话(core / wc / page)
+
+```
+[core] 纯 ESM 逻辑库    不认识 DOM / Lit,Node 可 import 单测
+   ▲ import
+[wc]   组件库(Lit 实现的 Web Component)  认识 DOM,不认识业务;跨框架 <my-x> 直接用
+   ▲ import
+[page] Lit 无构建页面    组装 core 与 wc,持有路由 / 布局 / 服务
+```
+
+**core 不认识 DOM,wc 不认识业务,page 组装一切。**
+
+> ⚠️ **wc = 用 Lit 框架特性实现的标准 Web Component**:组件一律 `extends LitElement` + `html` + `css` + `static properties`。
+> **禁止手写原生 wc**(`extends HTMLElement` / `attachShadow()` / 手写 `observedAttributes`)—— Lit 基于 Web Component 标准,
+> 产物仍是跨框架可用的自定义元素。见 [web-component-library.md 第〇节](./.claude/rules/web-component-library.md)。
+
+换框架还想复用逻辑 → 下沉 core;
+换框架还想复用 UI → 上提 wc;只服务本项目 → 停在 page。
+(`fmode-code-agent` = core;`designer-chat` 组件 = wc;后台页 = page。)
+细则见 [architecture.md 一·五](./.claude/rules/architecture.md)。
+
+## 3. 默认技术选型
+
+| 关注点 | 选择 | 备注 |
+|--------|------|------|
+| 视图 | Lit 3.x `LitElement` | Shadow DOM 隔离 |
+| 全局状态 | `@lit-labs/signals` + Service 单例 | 跨组件共享 |
+| 组件本地状态 | Lit `@property` / `@state` | 弹窗、输入框 |
+| 依赖注入 | `@lit-labs/context` | 令牌 + provide/consume |
+| 路由 | 自写 `utils/router.js`(History 干净 URL) | **子路径部署禁用 `@lit-labs/router`**(全局 `<a>` 拦截 + 未剥前缀 pathname 会污染路由);根部署可选它 |
+| 请求 | 原生 `fetch` 薄封装 | 不引 axios |
+| 持久化 | `localStorage` 薄封装 | 一次性配置 |
+| 部署 | OBS + CDN | 无构建直传 |
+
+## 4. 状态放哪?(高频决策)
+
+| 场景 | 方案 |
+|------|------|
+| 全局业务状态,多页共享(登录态、字典、权限) | **Signal Service + Context 注入**(首选) |
+| 组件本地 UI 状态(弹窗开关、表单输入) | Lit `@property` / `@state` |
+| 简单应用不想引 signals | `EventTarget` 简易 Store + Context |
+| 一次性持久化配置 | `localStorage` 封装(`utils/storage.js`) |
+
+> ⚠️ 两套响应式**必须分开**:跨组件全局业务状态用 **Signal**;组件内部 UI 用 **Lit property**。混用会导致更新时机错乱。
+
+## 5. 部署速查
+
+- **依赖** → 同步 npm 原始 ESM 到 **FMODE Repose**(按版本目录化,开 HTTP/2,`.js` 必须 `text/javascript`,CORS 放行业务域名),全业务共享一份公共 importmap。
+- **业务码** → `obsutil` 同步 `src/` 到 OBS(`obs://<bucket>/<project>/`,端点按环境配置),`public-read`,CDN 指向业务域名,最后刷新 CDN。
+- **子目录部署必须写 `<base href="/<project>/">`**,且 `<base href>` == OBS 上传前缀 == importmap 路径前缀(**同一变量派生 + 脚本硬校验**),否则相对 JS 404。
+- **History 回退**:CDN 配置「不存在路径返回 `index.html` 且状态码 **200**」,**严禁 302/301**(否则地址栏被改写、微信 OAuth 回调失败)。
+- **CDN 刷新**:`type:directory` + domain_id 对号入座;SDK 返回的 `refresh_task` 是**字符串**(不是 `.id`),失败须退出非 0(否则**假成功**)。
+- **验收**:不能只 `curl` 首页 200 —— 须跑模块图爬取(200 + `text/javascript`)+ 深链接 200 + **真实浏览器**。
+
+详见 [deployment.md](./.claude/rules/deployment.md) 与 [routing-best-practices.md](./.claude/rules/routing-best-practices.md)。
+
+## 6. 命名约定(速记)
+
+- 组件文件:`kebab-case.js`,类名 `PascalCase`,自定义元素标签 `kebab-case`(必须含连字符)
+- 服务文件:`xxx-service.js`,类 `XxxService`,单例导出 `export const xxxService = new XxxService()`
+- 工具文件:`xxx.js`,导出纯函数
+- 页面路由:`routes/page-xxx.js`,标签 `<page-xxx>`
+- 目录一律小写 kebab-case
+
+## 7. 动手清单(新建项目时)
+
+1. `index.html`:`<base>` + `<script type="importmap" src="<公共importmap>">` + `<script type="module" src="./app.js">`
+2. 建 `components/ routes/ services/ utils/`
+3. `app.js`:provide 全局 Service + 挂载 Router + 根组件
+4. 每个 Service 用 Signal 存状态,单例导出
+5. 页面组件 `SignalWatcher(LitElement)` + `consume` Service
+6. 部署脚本:`obsutil sync` + CDN refresh
+
+> 具体脚手架:用全局技能 `fmode-code-lit`(`npx fmode-code-lit@latest workspace`)一键生成。
+
+## 8. 写「库」时的动手清单(core / wc)
+
+**抽 core(逻辑层)**:把纯逻辑(状态机 / 校验 / 协议 / 计算)搬到独立包 → `grep` 确认无 `window`/`document`/`fetch`/`lit` →
+注入 I/O(`now`/`random`/`transport`)→ `node --test` 单测 → `src/index.js` 单一出口 → semver + Repose 版本目录。
+见 [core-library.md](./.claude/rules/core-library.md)。
+
+**抽 wc(组件库)**:**用 Lit 框架特性写**(`extends LitElement` + `html` + `css`,**不手写原生 wc**)→ 逻辑一律来自 core → `static properties` + `customElements.define()`(**判重**)→
+只暴露语义属性 / `composed` 事件 / slot / CSS 变量 + `::part` → 标签加统一前缀 → 在**一个非 Lit 宿主**里冒烟。
+见 [web-component-library.md](./.claude/rules/web-component-library.md)。

+ 83 - 0
skills/fmode-code-lit/references/light-slot.js

@@ -0,0 +1,83 @@
+/**
+ * light-slot.js — light DOM 下替代 <slot> 的内容投影
+ *
+ * 用法:
+ *   import { capture, project } from './light-slot.js';
+ *
+ *   class MyCard extends LightElement {
+ *     connectedCallback() {
+ *       super.connectedCallback();
+ *       capture(this);              // ← 渲染前捕获外部子节点
+ *     }
+ *     updated() {
+ *       project(this);              // ← 渲染后搬入容器(内部幂等)
+ *     }
+ *     render() {
+ *       return html`
+ *         <div class="card-header">
+ *           <div data-light-slot="actions"></div>
+ *         </div>
+ *         <div class="card-body" data-light-slot="body"></div>
+ *       `;
+ *     }
+ *   }
+ *
+ * 关键约束(违反即出 bug):
+ *  1. capture 必须在渲染前调用。渲染后再扫宿主子节点,会把组件自己刚生成的内容
+ *     当成「外部传入」搬走,越搬越乱。
+ *  2. project 必须靠 dataset.projected 做幂等,否则每次重渲染都重复搬、节点来回跳。
+ *  3. 传入具名内容用 slot="actions" 属性;不写 slot 属性的默认进 body。
+ */
+
+const KEY = '__lightIncoming';
+
+/**
+ * 渲染前捕获宿主元素上的外部子节点
+ * @param {HTMLElement} host
+ */
+export function capture(host) {
+  const body = [];
+  const named = {};
+  for (const n of [...host.childNodes]) {
+    // 元素节点且带 slot 属性 → 具名插槽
+    if (n.nodeType === 1 && n.hasAttribute('slot')) {
+      const name = n.getAttribute('slot');
+      (named[name] ||= []).push(n);
+    } else if (n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim())) {
+      // 元素节点,或非空白文本节点 → 默认 body
+      body.push(n);
+    }
+  }
+  host[KEY] = { body, named };
+}
+
+/**
+ * 渲染后把捕获的节点搬进对应容器(幂等)
+ * @param {HTMLElement} host
+ */
+export function project(host) {
+  const stash = host[KEY];
+  if (!stash) return;
+
+  // 默认插槽 → body
+  const bodyBox = host.querySelector('[data-light-slot="body"]');
+  if (bodyBox && !bodyBox.dataset.projected) {
+    const nodes = [...stash.body, ...(stash.named.body || [])];
+    for (const n of nodes) {
+      if (n.parentNode === host) bodyBox.appendChild(n);
+    }
+    bodyBox.dataset.projected = '1';
+  }
+
+  // 具名插槽
+  for (const [name, nodes] of Object.entries(stash.named)) {
+    if (name === 'body') continue;
+    const box = host.querySelector(`[data-light-slot="${name}"]`);
+    if (box && !box.dataset.projected) {
+      for (const n of nodes) {
+        if (n.parentNode === host) box.appendChild(n);
+      }
+      box.dataset.projected = '1';
+    }
+  }
+}

+ 105 - 0
skills/fmode-code-lit/references/rules/architecture.md

@@ -0,0 +1,105 @@
+# 架构与分层
+
+> 目标:零编译、零打包、纯原生 JS。基于浏览器原生 ESM + Import Map,依赖托管在内部 **FMODE Repose**,
+> 业务代码直接上传 **OBS + CDN**,保存刷新即预览。对标新版 Angular(History 干净路由、DI、Signal 细粒度响应式、
+> 组件/服务分层、后台多标签,**无 Zone.js**)。
+
+## 一、整体架构
+
+- **无构建无打包**:不用 rollup / esbuild / vite / webpack,源码浏览器直接执行。
+- **依赖统一托管**:`lit`、`@lit-labs/*` 等从 npm 下 **原始 ESM** 原样同步到 **FMODE Repose**,全业务共享。
+- **业务资源独立托管**:业务源码(`index.html`、`js` 组件)部署到 **OBS + CDN**。
+- **分层(对标 Angular)**:
+  - **WebComponent(`LitElement`)**:只渲染视图 + DOM 事件,不写复杂业务逻辑。
+  - **Service(普通 ESM Class)**:存全局状态 / 接口 / 业务计算,与 DOM 解耦。
+  - **Utils(纯 ESM 函数)**:无状态共享工具。
+- **响应式**:`@lit-labs/signals`(细粒度信号)。
+- **DI**:`@lit-labs/context`(组件树注入)。
+- **路由**:自写 `utils/router.js`,History 干净 URL(支持微信 OAuth 回调,需 CDN 回退规则)。
+  子路径部署**禁用** `@lit-labs/router`(全局 `<a>` 拦截 + 未剥前缀 pathname 会污染路由),见 [routing-best-practices.md](./routing-best-practices.md)。
+
+### 一·五、双方向分层:core / wc / page(新增)
+
+上面的「组件 / 服务 / 工具」讲的是**一个应用内部**怎么分。本仓还支持**跨应用复用**的两个方向,
+二者是**同一套无构建铁律**在不同尺度上的应用:
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ [page] 页面层   Lit 无构建页面(本应用)                              │
+│   组合 <my-x> 与页内组件;路由 / 多标签 / 布局只在页面层              │
+│      ▲ import                                                        │
+│ [wc]   组件库   Web Component(仅浏览器交互:DOM / 事件 / Shadow DOM) │
+│   一批 customElements;跨框架(Lit/Vue/React/原生)直接 <my-x> 使用   │
+│      ▲ import                                                        │
+│ [core] 核心库   纯 ESM 逻辑(零浏览器依赖,Node 可 import 单测)        │
+│   纯函数 / 类 / 状态机;不 import Lit、不碰 window/document/fetch      │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+| 层 | 产物 | 依赖 | 规则篇 |
+|----|------|------|--------|
+| **core** | npm ESM 包(可跑 Node) | 只依赖 `node:*` 与纯 JS 库(`@lit-labs/signals` 可用) | [core-library.md](./core-library.md) |
+| **wc** | 一批自定义元素(浏览器) | `core` + `lit`(仅渲染 API) | [web-component-library.md](./web-component-library.md) |
+| **page** | 一个应用的 `src/`(浏览器) | `wc` / `core` + 页面 `services/` `utils/` | [components.md](./components.md) |
+
+**对应实例**:`fmode-code-agent` = **core**(逻辑层纯 ESM);`designer-chat` 组件 = **wc**;
+任意后台页 = **page**(组合 wc,并可只用页内组件而不用组件库)。
+
+> 什么时候只需两层:只服务本项目、组件不复用 → **只有 page + 内联的 services/utils** 就够(最常见)。
+> 一旦出现「换一个框架还想复用这段逻辑」→ 下沉为 **core**;「换一个框架还想复用这个 UI」→ 上提为 **wc**。
+> 别为了分层而分层,但**一旦跨框架复用,core 必须零 DOM 依赖**,否则复用不成立。
+
+
+## 二、分层职责表(越界即返工)
+
+| 层 | 目录 | 允许 | 禁止 |
+|----|------|------|------|
+| 视图组件 | `components/` `routes/` | 渲染、DOM 事件、读 Signal、调 Service、`@property` 本地状态 | 大量业务逻辑、直接 `fetch`、硬编码接口地址、跨组件直接 new Service |
+| 服务 | `services/` | 持有 Signal、封装接口、校验、数据转换、业务计算 | 依赖 DOM / `LitElement`、`import { html }` 创建 DOM |
+| 工具 | `utils/` | 纯无状态函数、通用封装(http/storage/format) | 持有可变全局状态、依赖上面两层 |
+
+> 判定口诀:**能被单测、不 import Lit 的,就不是组件**。服务必须能在 Node 里 `import` 后直接跑。
+
+> 三分层版的口诀(见 [一·五](#一五分方向分层core--wc--page新增)):
+> **core 不认识 DOM,wc 不认识业务,page 组装一切。** service 若连 `localStorage` / `fetch` 都想抽走,就成 core;
+> 若沾 DOM,就留在 page。
+
+## 三、运行流程
+
+```
+浏览器打开 URL
+  → CDN 回退规则:不存在的路径 200(非 302)返回 index.html
+  → index.html 加载公共 importmap(指向 FMODE Repose)
+  → 加载 app.js
+  → import lit 依赖
+  → 初始化 路由 / Store / 根组件渲染
+```
+
+## 四、部署拓扑(内网细节,勿外泄)
+
+- **FMODE Repose**(静态依赖仓):`/npm/lit/v3.x.x/`、`/npm/@lit-labs/{context,signals,router}/vx/`、`/npm/importmap/lit-v3.json`(公共 importmap)。
+- **OBS + CDN**(业务托管):`index.html`(引入 importmap)、`app.js`、`/routes/`、`/components/`、`/services/`、`/utils/`。
+
+> ⚠️ 上面是**内部拓扑示意**。对外文档/开源材料中只写「内部依赖仓 + OBS+CDN」即可,**不写域名、桶名、AK/SK、obsutil 路径**。
+
+## 五、为什么这么选(对标 Angular zoneless)
+
+| 维度 | 传统 Angular(Zone) | 本方案(Lit + Signals) |
+|------|--------------------|--------------------------|
+| 变更检测 | Zone.js 全局猴子补丁,全组件树 check | Signal 精确到依赖了该信号的组件 |
+| 构建 | 必须 ng build | **无构建**,保存刷新即见 |
+| 依赖 | 打包进 bundle | Repose 共享,一次上传全业务复用 |
+| 路由 | 框架内置 | 原生 History,干净 URL,OAuth 友好 |
+| 样式隔离 | ViewEncapsulation | Shadow DOM 原生隔离 |
+| 心智负担 | 框架黑盒 | 原生 API,无魔法 |
+
+详见 [../docs/retrospective.md](../../docs/retrospective.md)。
+
+## 六、扩展方向
+
+- 全局 HTTP 工具(token 拦截 / 错误处理统一)。
+- 通用弹窗 / 表格 / 表单 WebComponent → 内部组件库,托管 FMODE Repose。
+- 路由守卫鉴权。
+- 全局 loading / 消息提示组件 + 全局 Signal 控制。
+- **逻辑层抽成 core 库**(纯 ESM、零 DOM)→ 同一套逻辑喂给 Lit / Vue / React 多套 UI,见 [core-library.md](./core-library.md)。
+- **组件库升级为跨框架分发**(wc 层)→ 非 Lit 宿主也能 `<my-x>`,见 [web-component-library.md](./web-component-library.md)。

+ 153 - 0
skills/fmode-code-lit/references/rules/components.md

@@ -0,0 +1,153 @@
+# 组件规范(LitElement)
+
+> 组件只做一件事:**把数据渲染成 DOM,并把用户事件转成对 Service 的调用**。
+> 复杂业务逻辑、请求、状态计算 —— 全部下沉到 Service。
+>
+> **本文件讲「页面层组件」**(服务于本应用的 UI)。若这个组件要**跨项目 / 跨框架**复用,
+> 它是**组件库(wc)**,契约更严、逻辑必须来自 [core](./core-library.md) —— 见
+> [web-component-library.md](./web-component-library.md)。分层总图见 [architecture.md 一·五](./architecture.md)。
+
+## 〇、无构建环境的第一约束:**禁用装饰器**
+
+> ⚠️ **禁用 `@customElement` / `@property` / `@state` / `@consume` 等装饰器语法。**
+> 装饰器是 **TypeScript / Babel 的编译期特性**,浏览器**原生不执行**。无构建(源码即产物)下,
+> 一旦写装饰器,浏览器直接 `SyntaxError`。所有 Lit 官方「装饰器版」文档**不能照抄**。
+
+一律改用**标准静态类字段 + 编程式 API**:
+
+| 装饰器版(❌ 禁) | 无构建版(✅ 用) |
+|-------------------|-------------------|
+| `@customElement('x-y')` | 类末尾 `customElements.define('x-y', XY)` |
+| `@property({ type: String }) name` | `static properties = { name: { type: String } }` |
+| `@state() kw = ''` | `static properties = { kw: { type: String, state: true } }` + constructor 初始化 |
+| `@consume({ context })` | `new ContextConsumer(this, { context, subscribe, callback })` |
+| `@provide({ context })` | 模板里的 `provide(ctx, value)` 指令(见 context-di.md) |
+| `@query('#x')` | `this.renderRoot.querySelector('#x')` |
+| `@queryAssignedElements` | `this.querySelectorAll('slot')` / `slot.assignedElements()` |
+
+> `static properties` 是 Lit 的**标准 API**(装饰器只是它的语法糖),无构建下完全可用。
+
+## 一、最小组件骨架(无装饰器)
+
+```js
+// components/user-card.js
+import { LitElement, html, css } from 'lit';
+
+export class UserCard extends LitElement {
+  static properties = {
+    name: { type: String },
+    user: { type: Object },
+  };
+
+  static styles = css`
+    :host { display: block; padding: 12px; border: 1px solid #eee; border-radius: 8px; }
+    .name { font-weight: 600; }
+  `;
+
+  constructor() {
+    super();
+    this.name = '';
+    this.user = null;
+  }
+
+  render() {
+    return html`
+      <div class="name">${this.name}</div>
+      <slot></slot>
+    `;
+  }
+
+  /** 事件只做转发,不做业务 */
+  _onClick() {
+    this.dispatchEvent(new CustomEvent('card-click', {
+      detail: { user: this.user },
+      bubbles: true,
+      composed: true,   // 穿透 Shadow DOM 边界,必须
+    }));
+  }
+}
+
+customElements.define('user-card', UserCard);
+```
+
+## 二、Lit 核心特性用法
+
+| 特性 | 用途 | 要点 |
+|------|------|------|
+| `` html`...` `` | 声明式模板 | 自动转义(**天然防 XSS**);模板字面量按内容缓存,只更新变化部分 |
+| `` css`...` `` | 组件样式 | 在 Shadow DOM 内生效,样式隔离 |
+| Shadow DOM | DOM/CSS 隔离 | 默认 `open`;跨边界事件需 `composed: true` |
+| `@property` | 对外属性(UI 状态 / 入参) | 会触发重渲染,默认 attribute 反射 |
+| `@state` | 内部私有状态 | 不对外暴露、不反射到 attribute |
+| `requestUpdate()` / `updateComplete` | 手动触发 / 等待渲染 | 备选手段,优先用 property 驱动 |
+| `<slot>` | 插槽 | 封装通用布局组件(弹窗外壳、面板) |
+
+## 三、响应式:本地 vs 全局(关键分界)
+
+> ⚠️ **两套响应式必须分开**,混用会导致更新时机错乱。
+
+| 场景 | 怎么用 |
+|------|--------|
+| 组件内部 UI(弹窗开关、输入框值、tab 高亮) | Lit `@property` / `@state` + `this.requestUpdate()` |
+| 跨组件全局业务状态(登录态、字典、购物车、权限) | `@lit-labs/signals`,组件用 `SignalWatcher` 混入 |
+
+需要全局 signal 的组件:用 `SignalWatcher(LitElement)` 包裹,signal 变化自动重渲染。
+
+```js
+import { LitElement, html } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { authService } from '../services/auth-service.js';
+
+export class PageDashboard extends SignalWatcher(LitElement) {
+  render() {
+    // authService.user 是 signal:读取即订阅,变化自动重渲染
+    return html`<h1>你好,${authService.user.get()?.name ?? '访客'}</h1>`;
+  }
+}
+```
+
+## 四、组件通信
+
+| 方向 | 手段 |
+|------|------|
+| 父 → 子 | `static properties` 声明的属性(attribute 或 `.prop=${x}` 绑定) |
+| 子 → 父 | `dispatchEvent(new CustomEvent('x', { bubbles, composed }))` |
+| 任意 ↔ 任意 | **Service + Signal**(首选),组件只读 signal / 调方法 |
+| 组件树深层传递依赖 | `@lit-labs/context` 的 `provide()` 指令 + `ContextConsumer`,见 [context-di.md](./context-di.md) |
+
+- 事件名用 `kebab-case`,`detail` 用对象。
+- 跨 Shadow DOM 边界必须 `composed: true`,否则父级监听不到。
+- **禁止**用全局事件总线撒逻辑——用 Service。
+
+## 五、绝对禁止
+
+- ❌ 用装饰器(`@customElement`/`@property`/`@consume`…)—— 无构建下浏览器不执行,见第〇节。
+- ❌ 组件里 `fetch` / 写接口地址 —— 交给 Service。
+- ❌ 组件里写复杂业务计算 / 校验 —— 交给 Service 或 utils。
+- ❌ 组件 new 另一个组件需要的 Service —— 用 `ContextConsumer` 或直接 import 单例。
+- ❌ 用全局 signal 存「只影响本组件」的状态 —— 用 `static properties` 的 `state: true`。
+- ❌ 跨版本混用 `lit` —— 自定义元素注册冲突,见 [pitfalls.md](./pitfalls.md)。
+
+## 六、可复用组件的封装原则
+
+- **只暴露语义属性**(`label`、`value`、`disabled`),不暴露内部实现细节。
+- **用 slot 做内容投影**,不要用 `<... .innerHTML>` 传 HTML。
+- **样式可覆盖**:暴露 CSS 自定义属性(`--card-bg`)而非改内部 class。
+- 通用性强的组件抽到独立目录并考虑托管到 **FMODE Repose 组件库**,跨项目复用。
+
+## 七、从「页内组件」升级为「组件库组件」
+
+同一个组件,跨项目复用时契约要加严。判断与落地:
+
+| 信号 | 动作 |
+|------|------|
+| 第二个项目要引它 / 换框架也要用 | 迁出为**组件库(wc)**,见 [web-component-library.md](./web-component-library.md) |
+| 组件里长出了业务逻辑(校验 / 协议 / 状态机) | 把逻辑**下沉 core**([core-library.md](./core-library.md)),组件只保留接线 |
+| 组件里出现 `fetch` / 依赖宿主全局变量 | 改成**宿主注入**(`el.transport = ...`),组件库不拥有 I/O |
+| 需要让非 Lit 宿主也能用 | 标签加统一前缀;事件 `composed: true`;只通过 `static properties` + slot + CSS 变量暴露 |
+
+三句话记住三分层:
+
+- **core**:不认识 DOM 的纯逻辑(Node 可测)。
+- **wc**:不认识业务的 Web Component(跨框架)。
+- **page**:组装 core 与 wc 的页面(本文件其余章节讲的就是它)。

+ 102 - 0
skills/fmode-code-lit/references/rules/context-di.md

@@ -0,0 +1,102 @@
+# 依赖注入(@lit-labs/context)
+
+> Context 用于把「全局 Service / 配置」注入到组件树深层,避免逐层 `@property` 透传(prop drilling)。
+> 与 Angular 的 DI 对标,但更轻:**令牌 + provide + consume**。
+
+## 一、三件套
+
+| API | 作用 |
+|-----|------|
+| `createContext<T>(key)` | 定义一个注入令牌(模块级唯一) |
+| `provide({ context, initialValue })` | 在某个祖先元素上提供值(通常根组件) |
+| `consume({ context, subscribe })` | 在子组件里消费值 |
+
+## 二、定义令牌
+
+```js
+// services/contexts.js —— 令牌集中定义,避免散落
+import { createContext } from '@lit-labs/context';
+import { authService } from './auth-service.js';
+
+export const authContext = createContext('fmode.auth');
+// 也可以为普通配置/值建令牌
+export const configContext = createContext('fmode.config');
+```
+
+- 令牌 key 用命名空间前缀(`fmode.xxx`)避免与第三方冲突。
+- 令牌必须**同一模块实例**:多处 `createContext` 会得到不同令牌 → 注入失败。集中定义。
+
+## 三、在根组件提供(provide)
+
+```js
+// app.js 或根组件
+import { LitElement, html } from 'lit';
+import { provide } from '@lit-labs/context';
+import { authContext, configContext } from './services/contexts.js';
+import { authService } from './services/auth-service.js';
+
+export class AppRoot extends LitElement {
+  render() {
+    return html`
+      ${provide(authContext, authService)}
+      ${provide(configContext, { apiBase: '/api', appName: 'Admin' })}
+      <app-shell></app-shell>
+    `;
+  }
+}
+```
+
+- `provide` 是一个「无渲染」的模板指令,放在祖先组件 render 里,向下对所有后代生效。
+- 通常**只在根组件 provide**(全局单例 Service),不要把 provide 散在各处。
+
+## 四、在子组件消费(consume)
+
+> ⚠️ 无构建下**禁用 `@consume` 装饰器**,改用编程式 `ContextConsumer`。
+
+```js
+import { LitElement, html } from 'lit';
+import { ContextConsumer } from '@lit-labs/context';
+import { authContext } from '../services/contexts.js';
+
+export class UserBadge extends LitElement {
+  static properties = { _auth: { state: true } };
+
+  #consumer = new ContextConsumer(this, {
+    context: authContext,
+    subscribe: true,                       // 提供的「实例」被替换时回调
+    callback: (value) => { this._auth = value; },
+  });
+
+  render() {
+    return html`<span>${this._auth?.user.get()?.name ?? '未登录'}</span>`;
+  }
+}
+
+customElements.define('user-badge', UserBadge);
+```
+
+- `subscribe: true`:当提供的**值本身**被替换为另一个实例时,组件重新响应(不是 signal 变化,而是「注入对象换了」)。
+- 注入对象内部的 signal 变化,由 `SignalWatcher` 负责驱动重渲染 —— 两者配合。
+- 父组件提供用模板指令 `provide(ctx, value)`(见第三节),同样无需装饰器。
+
+## 五、何时用 Context,何时直接 import
+
+| 情况 | 做法 |
+|------|------|
+| 全局单例 Service,确定唯一 | **直接 `import` 单例** 即可(最简单,推荐) |
+| 需要「同组件树不同区块用不同实例」(多实例 / 可测试替换) | **Context**(provide 不同实例) |
+| 需要向下传配置 / 主题 / 环境值 | **Context** |
+
+> 务实建议:**默认直接 import 单例**;只有当「同一组件要能跑在不同注入环境」时才上 Context。别为了 DI 而 DI。
+
+## 六、与 SignalWatcher 配合
+
+- `consume` 拿到的是 **Service 实例**(不一定变化)。
+- Service 里的 **signal** 变化 → 由混入 `SignalWatcher` 的组件自动重渲染。
+- 两者职责不同:`consume` 解决「拿到谁」,`SignalWatcher` 解决「谁变了要重渲染」。
+
+## 七、坑
+
+- 令牌模块重复实例化 → consume 拿不到 → 返回 `undefined`。**令牌必须集中定义、单例导出**。
+- 在 `render()` 外读取 consumed 属性可能为 `undefined`(首次渲染前);用可选链。
+- provide 的作用域是「该元素的子树」,provide 写错层级 → 后代拿不到。

+ 211 - 0
skills/fmode-code-lit/references/rules/core-library.md

@@ -0,0 +1,211 @@
+# 核心库规范(core:纯 ESM、零浏览器依赖)
+
+> 本仓从「只会写页面」升级为**双方向**:既能写页面(page),也能写 **Web Component 组件库(wc)**
+> 与 **跨前端框架的核心库(core)**。三分层总览见 [architecture.md](./architecture.md),组件库见
+> [web-component-library.md](./web-component-library.md),页面层仍见 [components.md](./components.md)。
+>
+> **核心库 = 一个普通 npm ESM 包**:不含 Lit、不碰 DOM、可在 Node 里直接 `import` 与单测。
+> 浏览器、Node、小程序、SSR、Web Worker 都能跑同一份代码;Web Component 组件库与页面层都**向下依赖**它。
+
+```
+[core]  纯 ESM 逻辑库(本文件)       ← 无 DOM / 无 Lit / 无 window / 无 fetch 硬编码
+   ↑ import
+[wc]    组件库(Web Component)       ← 唯一的浏览器交互层
+   ↑ import
+[page]  页面(Lit 无构建)            ← 组合 wc 与页内组件
+```
+
+## 一、铁律(违反即返工)
+
+1. **零浏览器依赖**:`core/` 下任何文件都不得触碰 `window` / `document` / `navigator` / `localStorage` /
+   `fetch` / `HTMLElement` / `CustomEvent` / `import.meta.url` 拼 DOM 路径。**唯一例外**:`node:` 内置模块(Node 可跑)。
+2. **不 import Lit**:`import ... from 'lit'` / `lit-html` / `@lit/*` 一律禁止 —— 一旦 import Lit 就绑定了浏览器渲染。需响应式时用 `@lit-labs/signals`(`signal`/`computed`),它本身是纯 JS,可跑在 Node。
+3. **只 export 纯逻辑**:纯函数、纯 Class、状态机、校验、格式化、DTO 解析、业务计算。**不 export 单例状态**(每次 `new` 或由调用方注入),不 export「全局 store」。
+4. **副作用零**:模块被 `import` 时**不得**执行任何副作用(注册监听、读写存储、发请求、打印)。模块顶层只允许**声明**。
+5. **Node 可测**:`core/` 的每个文件都必须能在 Node 里 `import` 并跑通测试;测试**不依赖 jsdom**。
+6. **依赖方向单向**:`core` **不认识** `wc` / `page`;`wc`、`page` 认识 `core`。
+
+> 判定口诀:**`node --input-type=module -e "import('./core/index.js')"` 能跑、`grep` 不到 DOM 全局,才是 core。**
+
+## 二、目录规范
+
+```
+<lib>/                          # 一个独立的 ESM 包(可跨项目)
+  src/
+    index.js                    # 公共出口(只在这里 export 对外 API)
+    <domain>/                   # 按领域分文件/子目录,如 workflow/、pricing/
+      *.js
+    internal/                   # 内部实现(不对外 export,可随意重构)
+  test/
+    *.test.js                   # node:test 单测(不依赖 DOM)
+  package.json                  # type: module,exports 映射
+```
+
+- **`src/index.js` 是唯一公共契约**;内部实现对 `internal/` 隐藏,外部只能 import 包名(见第五节 exports)。
+- **一个文件一个关注点**:`validate.js`(校验)、`machine.js`(状态机)、`format.js`(格式)。
+- 命名:小写 kebab-case 文件;`PascalCase` 类;`camelCase` 函数。
+
+## 三、核心库的「好的形状」
+
+| 该做 | 不该做 |
+|------|--------|
+| `export function validate(input, rules)` —— 纯函数,给什么算什么都由参数决定 | 读 `document.querySelector` 拿「当前表单」 |
+| `export class TaskMachine { constructor(config) {} next(event) {} }` —— 状态机,可 `new` 多份 | 模块级 `let store = {}` 饱含状态 |
+| 接收**注入**的 I/O(`fetchImpl`、`now`、`random`、`telemetry`) | 直接 `fetch()` / `Date.now()`(不可测) |
+| `export const parseConfig = (text) => {...}` | 模块顶层 `localStorage.getItem(...)` |
+
+### 可注入的 I/O(让测试确定化)
+
+```js
+// src/pricing/quote.js —— 纯计算,所有不确定输入都从参数注入
+export function quote(items, { taxRate = 0.06, now = () => Date.now(), id = () => crypto.randomUUID() } = {}) {
+  const subtotal = items.reduce((s, it) => s + it.price * it.qty, 0);
+  const tax = Math.round(subtotal * taxRate * 100) / 100;
+  return { id: id(), at: now(), subtotal, tax, total: Math.round((subtotal + tax) * 100) / 100 };
+}
+```
+
+- `now` / `random` / `id` / `fetchImpl` 都可注入 → 测试断言**确定值**,不靠 mock 全局。
+- 需要网络时:core 只定义**纯协议**(URL 拼装、DTO 解析),真正的 `fetch` 由 wc/page 层调用后传入结果。
+
+## 四、Node 可测(node:test,不依赖 jsdom)
+
+```js
+// test/quote.test.js
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { quote } from '../src/index.js';
+
+test('含税总价四舍五入到分', () => {
+  const r = quote([{ price: 19.99, qty: 3 }], { taxRate: 0.06, now: () => 0, id: () => 't1' });
+  assert.equal(r.subtotal, 59.97);
+  assert.equal(r.total, 63.57);
+});
+```
+
+运行:`node --test test/`
+
+- **`package.json` 里 `"type": "module"`**,`node --test`/`node --check` 原生识别 ESM。
+- 测试命名 `*.test.js`,与源码同构目录。
+- **禁止**在 core 测试里引 jsdom / happy-dom —— 需要 DOM 的代码就不该在 core 里。
+- CI 一条命令:`node --check`(语法)+ `node --test`(逻辑)。
+
+## 五、无构建打包为单文件(发布给浏览器用)
+
+> 铁律是「**源码不进构建流程**」—— 业务仓 / 页面仓不打包。**发布库**时若确需单文件(如嵌入第三方页面),
+> 走**独立发布流水线**、产物归档到 FMODE Repose,源码仍是单文件 ESM。**平时优先「无打包」,不要为了发布而给业务仓加 rollup。**
+
+### 5.1 首选:保持多文件 ESM(零打包)
+
+`core/` 本身就是浏览器原生 ESM 可直接 import 的。只要**相对 import 带 `.js`**,`<script type="module">` 与 `import` 都能直跑。
+托管到 Repose `/npm/<lib>/vX.Y.Z/` 目录,importmap 映射:
+
+```json
+{ "imports": {
+  "my-lib": "/npm/my-lib/v1.2.0/src/index.js",
+  "my-lib/": "/npm/my-lib/v1.2.0/src/"
+} }
+```
+
+> 依赖解析走 importmap(同 [project-structure.md 第五节](./project-structure.md));core 若依赖第三方 ESM(如 `signal-polyfill`),
+> 同样从 importmap 解析,**不**把第三方源码拷进包内。
+
+### 5.2 需要单文件时:用 `esbuild` 只在**发布**时打一次(不进业务仓)
+
+```bash
+# 发布流水线,非业务仓
+npx esbuild src/index.js --bundle --format=esm --platform=neutral \
+  --outfile=dist/my-lib.esm.js --external:'node:*'
+```
+
+- `--format=esm`(保持 ESM),`--platform=neutral`(不假设浏览器)。
+- 若要给 `<script src>` 直接挂全局:`--format=iife --global-name=MyLib`(**只给无法用 module 的环境**,默认不用)。
+- 产物**归档版本化**(`dist/` 不提交到源码仓,见第六节)。
+- **禁用装饰器语法**(同 [components.md 第〇节](./components.md)):core 里也不写装饰器,即使被打包器编译过 —— 源码即产物铁律延续。
+
+## 六、版本管理与兼容
+
+| 规则 | 说明 |
+|------|------|
+| **语义化版本** | 公共出口 `src/index.js` 的 API 遵循 semver:破坏性变更 = major |
+| **版本目录化(Repose)** | `/npm/<lib>/vX.Y.Z/` 只增不改;升级 = 新增目录 + 改 importmap 版本段,**不覆盖旧版** |
+| **单一出口** | 只 `export` 自 `src/index.js`;`package.json` `exports` 只放这一个入口(+ 必要子路径) |
+| **不 export 内部路径** | `exports` 里不写 `"./internal/*"`,外部无法深挖 |
+| **CHANGELOG** | 每次 major 写清「删了什么 / 改了什么签名」 |
+| **多版本共存** | 若两项目用不同 major,可在 importmap 用不同裸标识符(`my-lib` / `my-lib-v2`)指向不同版本目录 |
+
+```jsonc
+// package.json —— 核心库
+{
+  "name": "my-lib",
+  "version": "1.2.0",
+  "type": "module",
+  "exports": { ".": "./src/index.js" },
+  "sideEffects": false,
+  "files": ["src", "README.md"],
+  "scripts": { "check": "node --check src/index.js", "test": "node --test test/" }
+}
+```
+
+- `"sideEffects": false`:让打引用方(如有)能安全 tree-shake,且**强制**你遵守「模块无副作用」。
+- **不 import Lit**:`peerDependencies` 里不要出现 `lit`。
+
+## 七、与 wc / page 的契约
+
+```
+fmode-code-agent(逻辑层,core)  ──导出纯逻辑──▶  <designer-chat> / 任何框架的 UI(wc)
+                                                        │
+                                                        ▼
+                                                   Lit 页面(page)组合
+```
+
+- core 导出**类/函数**,wc 在组件内部 `new` 它们、监听状态、渲染。
+- core 的**状态变化通知**可以用 `@lit-labs/signals`(纯 JS,Node 可跑),也可以是自定义的纯观察者(`subscribe(fn)` 返回 `unsubscribe`)—— 选一个,**别在 core 里用 `EventTarget` 以外的 DOM 事件**(`EventTarget` 在 Node 18+ 可用,可接受;`CustomEvent` 在 Node 需 polyfill,尽量用自定义 `subscribe`)。
+- **错误契约**:core 抛**普通 `Error` 子类**,信息里带可判定字段(`code`),不抛 DOMException。
+
+```js
+// src/agent/machine.js —— 逻辑层状态机(纯 JS,Node 可测)
+export class AgentMachine {
+  #listeners = new Set();
+  #state = 'idle';
+  constructor({ transport }) { this.#transport = transport; }     // 注入 I/O
+  get state() { return this.#state; }
+  subscribe(fn) { this.#listeners.add(fn); return () => this.#listeners.delete(fn); }
+  #emit(next) { this.#state = next; for (const f of this.#listeners) f(next); }
+  async send(text) {
+    if (this.#state !== 'idle') throw Object.assign(new Error('busy'), { code: 'E_BUSY' });
+    this.#emit('thinking');
+    try { return await this.#transport.call({ text }); } finally { this.#emit('idle'); }
+  }
+}
+```
+
+- wc 层用 `new AgentMachine({ transport: browserTransport })`;Node 测试注入 `transport: fakeTransport`。
+- 同一核心库 → 同一逻辑 → 换任何前端框架 UI 行为一致。
+
+## 八、红线清单
+
+- ❌ `core/` 内出现 `window` / `document` / `localStorage` / `fetch(` / `HTMLElement` / `import 'lit'`
+- ❌ 模块顶层有副作用(`addEventListener`、`localStorage`、`console.log`)
+- ❌ export 可变单例(「全局配置对象」)
+- ❌ `Date.now()` / `Math.random()` 直接写死 → 注入 `now` / `random`
+- ❌ 在业务仓引入 rollup/tsc 只为打 core → 发布流水线单独处理
+- ❌ 写装饰器
+- ❌ 从 `internal/` 深 import
+
+## 九、core 发布 checklist
+
+- [ ] `grep -rE "\b(window|document|localStorage|sessionStorage|navigator)\b|\bfetch\(" src/` 无命中
+- [ ] `grep -rn "from 'lit" src/` 无命中
+- [ ] 模块导入无副作用:`node -e "import('./src/index.js')"` 干净退出
+- [ ] `node --check` 全过;`node --test test/` 全过
+- [ ] `src/index.js` 是唯一出口;`exports` 无内部路径
+- [ ] semver 版本号 + CHANGELOG;Repose 新版本目录(不覆盖旧版)
+- [ ] 相对 import 带 `.js`;第三方依赖走 importmap(不拷源码)
+
+## 相关
+
+- [architecture.md](./architecture.md) —— 三分层(core / wc / page)总图
+- [web-component-library.md](./web-component-library.md) —— 组件库(浏览器交互层)
+- [components.md](./components.md) —— 页面层组件规范
+- [services.md](./services.md) —— 页面内的 Service(也可抽成 core,但若沾 DOM 只能留在页面)

+ 172 - 0
skills/fmode-code-lit/references/rules/deployment.md

@@ -0,0 +1,172 @@
+# 部署规范(FMODE Repose + OBS/CDN)
+
+> 本篇含内部部署细节,**禁止**复制到对外文档 / 开源材料 / 公开仓库(本仓对外以「开源前已脱敏」版本发布)。
+> 无论对内对外,**AK/SK 密钥一律不写进任何文档或代码**,只从既有部署脚本引用。
+
+## 一、两条托管线
+
+| 内容 | 托管方 | 说明 |
+|------|--------|------|
+| **依赖**(lit / @lit-labs/*) | **FMODE Repose** | 静态依赖仓,从 npm 下**原始 ESM** 原样同步,按版本目录化,全业务共享 |
+| **业务码**(index.html / js) | **OBS + CDN** | `obsutil` 同步 `src/` 到华为云 OBS,`public-read`,CDN 加速 |
+
+## 二、FMODE Repose(静态依赖仓)
+
+**存放布局**
+
+```
+/npm/lit/v3.x.x/                      # lit 运行时(原始 ESM)
+/npm/@lit-labs/context/vx/            # context
+/npm/@lit-labs/signals/vx/            # signals
+/npm/@lit-labs/router/vx/             # router
+/npm/importmap/lit-v3.json            # 公共 importmap(全业务复用)
+```
+
+**硬性要求**
+
+1. `.js` 响应的 `Content-Type` **必须** `text/javascript`(否则浏览器拒绝作为模块执行)。
+2. **CORS 允许业务域名**(否则跨域模块加载失败)。
+3. 开启 **HTTP/2(或 /3)** —— ESM 小文件多,靠多路复用降延迟。
+4. 依赖按**版本目录**化(`/vX.Y.Z/`),升级 = 新增目录 + 改 importmap 版本号,**不覆盖旧版本**。
+5. 保持 npm 包的**相对 import 结构**原样(原样同步,不重写 import)。
+
+**同步方式**:把 `node_modules/lit`、`node_modules/@lit-labs/*` 原样上传到 Repose 对应版本目录。
+(仓库内提供同步脚本入口占位,实际凭据从环境 / 既有脚本注入。)
+
+**公共 importmap**(托管在 Repose,业务引用)
+
+```json
+{
+  "imports": {
+    "lit": "/npm/lit/v3.2.1/lit.js",
+    "lit/": "/npm/lit/v3.2.1/",
+    "lit/decorators.js": "/npm/lit/v3.2.1/decorators.js",
+    "@lit-labs/signals": "/npm/@lit-labs/signals/v0.1.3/index.js",
+    "@lit-labs/context": "/npm/@lit-labs/context/v0.5.1/index.js",
+    "@lit-labs/router": "/npm/@lit-labs/router/v0.2.0/router.js"
+  }
+}
+```
+
+> 版本号以仓库实际落地为准,此处为示例形态。升级依赖 = 追加新路径 + 改这里的版本段。
+
+## 三、OBS + CDN(业务部署)
+
+**入口 index.html**
+
+```html
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8" />
+  <base href="/<project>/" /><!-- 二级路径部署必须;根部署可省略或 "/" -->
+  <link rel="importmap" href="..."><!-- 见下 -->
+  <script type="importmap" src="<公共 importmap URL>"></script>
+</head>
+<body>
+  <app-root></app-root>
+  <script type="module" src="./app.js"></script>
+</body>
+</html>
+```
+
+- 公共 importmap 由 **Repose 托管**,全业务用同一份:
+  `<script type="importmap" src="<Repose>/npm/importmap/lit-v3.json"></script>`。
+- `<base href>` 决定相对路径基准,子目录部署**必须**写对。
+
+**上传(obsutil)**
+
+- 工具:`obsutil`(华为云官方 CLI,安装路径按各自环境而定)
+- 桶(bucket)与端点、AK/SK:**从既有部署脚本引用**(如 `fmode-app/scripts/deploy.sh`),**禁止**写入本仓文档/代码。
+- 典型命令形态:
+
+```bash
+# 同步业务源码到 OBS 前缀,公开读
+obsutil sync ./src/ obs://<bucket>/<project>/ \
+  -e=obs.<region>.myhuaweicloud.com -acl=public-read -f
+```
+
+- **index.html 先删后传**:`sync` 按 size+mtime 判 Skip,`index.html` 名字固定、内容随发布变化,需先 `rm` 再 `cp`,避免被 Skip。
+- 静态资源设 `Cache-Control`:`index.html` 用 `no-cache`(保证回退拿到最新),带 hash 的资源可长缓存。
+
+**CDN 刷新**:上传后调用 CDN refresh(仓库内 `scripts/deploy.sh` 已接入既有刷新脚本)。刷新**范围**由传入的 URL 域名决定。
+
+> ⚠️ **CDN 刷新的两个假成功陷阱**(实测踩过,详见
+> [routing-best-practices.md 坑 5](./routing-best-practices.md#坑-5cdn-刷新看似成功实则没刷)):
+> 1. **域名对号入座**:每个加速域有各自的 `domain_id` 与凭证;用错只刷了一个域名,
+>    用户访问的另一个域名照旧是旧内容。未知域名应**直接报错**,别静默跳过。
+> 2. **SDK 返回结构**:`refresh_task` 是**字符串 ID**,不是对象。若读成
+>    `result.refresh_task.id` 会恒得 `undefined`,脚本**照常退出 0**(假成功)。
+>    必须对结果做**非空断言**,任一失败则**退出码非 0**。
+>
+> 刷新用 `type: 'directory'`(URL 以 `/` 结尾)一次覆盖目录,比逐个 file 省事且不易漏。
+
+## 三·五、验收:不能只 `curl` 首页
+
+`curl` 首页 200 **只证明 `index.html` 在**;无构建应用白屏的典型原因(某模块 404、MIME 错、
+回退没配、相对路径错一个字符)它一个都不覆盖。分层验收:
+
+1. **静态体检**(部署前):`node --check` 无语法/装饰器残留;相对 import 带 `.js`;
+   相对目标存在;裸标识符都在 importmap;自定义元素标签含连字符。
+2. **模块图爬取**(部署后,HTTP 层):从入口 `app.js` 跟随相对 import 走完整图,
+   断言每个文件 **200 + `Content-Type: text/javascript`**;importmap 每条目标可达;
+   **深链接返回 200 且内容是 `index.html`**(非 302)。
+3. **真实浏览器**(最终验收):点导航 + 刷新深链接,确认不 404、无控制台报错。
+4. **缓存层**:核对 CDN 返回的**内容**是新版(不只是状态码)。
+
+> 把 1–2 步固化成一条命令纳入部署流程。可复用脚本形态见
+> [routing-best-practices.md 坑 7](./routing-best-practices.md#坑-7只-curl-首页不算验收)。
+
+> 🔎 **CDN 冷边缘**:新上传的 key 首次被某边缘节点拉取时**偶发 404**(回源竞争),
+> 重跑一次即消失 —— 验收脚本碰到新 key 的首次 404 不要立刻判故障。
+
+## 四、History 路由的 CDN 回退(关键)
+
+| 规则 | 匹配 | 动作 |
+|------|------|------|
+| 规则 1 | 命中真实静态资源 | 直接返回该文件 |
+| 规则 2 | 路径不存在 | 返回 `index.html`,状态码 **200**,**禁止 301/302** |
+
+- 用 **200** 而非 302:302 会改写地址栏 / 丢 `location.pathname`,破坏路由参数与微信 OAuth 回调。
+- `index.html` 设 `no-cache`,确保回退总是拿到最新壳。
+
+## 五、部署脚本(模板)
+
+```bash
+#!/usr/bin/env bash
+# scripts/deploy.sh —— 无构建,直接同步 src/ 到 OBS,再刷 CDN
+set -euo pipefail
+PROJECT="${1:?usage: deploy.sh <project>}"
+BUCKET="${OBS_BUCKET:?set OBS_BUCKET}"           # 例: obs://my-bucket
+PREFIX="$PROJECT"
+EP="${OBS_ENDPOINT:?set OBS_ENDPOINT}"           # 例: obs.<region>.myhuaweicloud.com
+OBS="${OBSUTIL:-obsutil}"
+
+# 1) index.html 先删后传(防 Skip)+ no-cache
+"$OBS" rm "$BUCKET/$PREFIX/index.html" -e="$EP" -f >/dev/null 2>&1 || true
+"$OBS" cp ./src/index.html "$BUCKET/$PREFIX/index.html" -e="$EP" -acl=public-read -f \
+  -meta="Cache-Control:no-cache#Content-Type:text/html; charset=utf-8"
+
+# 2) 其余源码同步
+"$OBS" sync ./src/ "$BUCKET/$PREFIX/" -e="$EP" -acl=public-read -f
+
+# 3) 刷新 CDN(调用既有刷新脚本,凭据由该脚本/环境提供)
+# node <既有刷新脚本> "https://<cdn-domain>/$PREFIX/"
+```
+
+- **凭据注入**:AK/SK 从环境变量 / 既有脚本读取,**不落本仓**。
+- 无构建 → 无需构建步骤,`src/` 即产物。
+
+## 六、发布 checklist
+
+- [ ] `<base href>` 与部署前缀一致(**同一变量派生**,脚本里硬校验)
+- [ ] importmap 指向 Repose(**内联**在 index.html),版本号/前缀正确
+- [ ] 依赖已同步到 Repose(`Content-Type: text/javascript` + CORS 放行)
+- [ ] OBS 上传完成,`index.html` 已先删后传 + `no-cache`
+- [ ] CDN 回退规则:不存在路径 200 返回 index.html,**禁 301/302**
+- [ ] CDN 刷新完成(`type:directory`,**domain_id 对号入座**,读 `refresh_task` 字符串,失败退出非 0)
+- [ ] 直接命中深链接(如 `/users/42`)**刷新**可用(200 且为 index.html)
+- [ ] 静态体检 + 模块图爬取通过;**真实浏览器**点导航/刷新深链接无报错
+
+> 路由 / base / CDN 的完整肺坑与 check 清单见
+> [routing-best-practices.md](./routing-best-practices.md)。

+ 113 - 0
skills/fmode-code-lit/references/rules/http-utils.md

@@ -0,0 +1,113 @@
+# HTTP 与工具(utils)
+
+> 用原生 `fetch` 薄封装,**不引 axios**。工具层是**纯无状态函数**,谁都能 import,不持有全局状态。
+
+## 一、utils 的边界
+
+| 允许 | 禁止 |
+|------|------|
+| 纯函数(格式化、校验、URL 拼接、深拷贝) | 持有可变全局状态 |
+| 通用封装(http、storage) | import `components/` 或 `services/` |
+| 依赖浏览器 API(fetch/localStorage/URL) | 依赖 Lit / DOM 渲染 |
+
+> utils 必须可在 Node 或任意上下文 import 而不产生副作用。
+
+## 二、HTTP 封装(utils/http.js)
+
+```js
+// utils/http.js
+import { authService } from '../services/auth-service.js';
+
+const BASE = '/api';   // 或从 config 注入
+
+class HttpError extends Error {
+  constructor(status, body) { super(`HTTP ${status}`); this.status = status; this.body = body; }
+}
+
+async function request(method, path, { body, headers = {}, query } = {}) {
+  let url = BASE + path;
+  if (query) url += '?' + new URLSearchParams(query).toString();
+
+  const finalHeaders = { 'Content-Type': 'application/json', ...headers };
+  const token = authService?.getToken?.();
+  if (token) finalHeaders.Authorization = `Bearer ${token}`;
+
+  const res = await fetch(url, {
+    method,
+    headers: finalHeaders,
+    body: body != null ? JSON.stringify(body) : undefined,
+    credentials: 'same-origin',
+  });
+
+  if (res.status === 401) {
+    authService.logout();
+    // 触发跳转登录(由路由/根组件响应 signal)
+    throw new HttpError(401, null);
+  }
+  const ct = res.headers.get('content-type') || '';
+  const data = ct.includes('application/json') ? await res.json() : await res.text();
+  if (!res.ok) throw new HttpError(res.status, data);
+  return data;
+}
+
+export const http = {
+  get: (p, o) => request('GET', p, o),
+  post: (p, body, o) => request('POST', p, { ...o, body }),
+  put: (p, body, o) => request('PUT', p, { ...o, body }),
+  del: (p, o) => request('DELETE', p, o),
+};
+```
+
+设计要点:
+- **token 自动注入**(从 `authService` 取)。
+- **401 统一处理**:登出 + 让 UI 响应 signal 跳登录。
+- **统一错误**:抛 `HttpError`,携带 `status` / `body`,调用方可 `catch`。
+- **DTO 解析**按 content-type 分流。
+- 循环依赖注意:`http` 引用 `authService`,`authService` 又用 `http` → 用**惰性调用**(函数内取)而非模块顶层解构,避免 ESM 循环初始化问题。
+
+## 三、Storage 封装(utils/storage.js)
+
+```js
+export const storage = {
+  get(key, fallback = null) {
+    try { const v = localStorage.getItem(key); return v == null ? fallback : JSON.parse(v); }
+    catch { return fallback; }
+  },
+  set(key, value) { localStorage.setItem(key, JSON.stringify(value)); },
+  remove(key) { localStorage.removeItem(key); },
+};
+```
+
+- 加 try/catch(隐私模式 / 配额满会抛)。
+- JSON 序列化,避免手写类型转换。
+- 敏感数据(token)单独考虑,不要明文长期存(见安全)。
+
+## 四、格式化(utils/format.js)
+
+```js
+export const formatMoney = (n, currency = '¥') =>
+  `${currency}${Number(n ?? 0).toFixed(2)}`;
+
+export const formatDate = (d, fmt = 'YYYY-MM-DD') => { /* ... */ };
+
+export const debounce = (fn, ms = 200) => {
+  let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
+};
+```
+
+纯函数,无副作用,可单测。
+
+## 五、其他常用工具
+
+| 工具 | 用途 |
+|------|------|
+| `utils/router.js` | `navigate()` / `href()` / `currentPath()`(把应用内路径拼到 base 的 **pathname** 上;见 [routing-best-practices.md](./routing-best-practices.md)) |
+| `utils/dom.js` | 少量原生 DOM 辅助(慎用,优先 Lit 指令) |
+| `utils/validate.js` | 表单校验纯函数 |
+
+## 六、禁止
+
+- ❌ 在 utils 里 `import { html }`。
+- ❌ 在 utils 里持有跨调用的可变状态(那属于 Service)。
+- ❌ 在组件里直接 `fetch`(一律走 `http`)。
+- ❌ 手写重复的 URL 拼接 / 时间格式化(抽到 utils)。

+ 118 - 0
skills/fmode-code-lit/references/rules/modules/README.md

@@ -0,0 +1,118 @@
+# 模块划分最佳实践
+
+> 无构建项目最容易「长成一坨」:所有组件堆在 `components/`、所有逻辑塞进一个超大 service。
+> 本篇给出**功能模块 / 通用能力 / 业务模块**的拆分准则与落地目录。
+
+## 一、三种模块,三种归属
+
+| 类型 | 定义 | 归属 | 是否可跨项目 |
+|------|------|------|--------------|
+| **通用能力(util skill)** | 与业务无关的纯能力:http、storage、format、debounce | `utils/` | ✅ 可抽成包托管 Repose |
+| **通用组件(UI kit)** | 与业务无关的可复用 UI:弹窗、表格、表单、布局壳 | `components/` | ✅ 可抽成组件库 |
+| **功能模块(feature)** | 一块完整业务功能:用户管理、订单、报表 | `modules/<feature>/` | ❌ 业务专属 |
+
+> 判别法:**换一个业务项目,它还有用吗?** 有用 → 通用(utils/components);没用 → 功能模块。
+
+## 二、推荐目录(功能模块化)
+
+```
+src/
+  app.js
+  components/                 # 跨模块通用 UI
+    app-shell.js
+    tabs-bar.js
+    base-table.js
+  modules/                    # 功能模块(一块一块业务)
+    user/
+      page-user-list.js       # 页面组件
+      page-user-detail.js
+      user-service.js         # 模块内 service
+      user-model.js           # 模块内数据结构/转换
+    order/
+      page-order-list.js
+      order-service.js
+  services/                   # 跨模块共享 service(全局状态)
+    auth-service.js
+    dict-service.js
+  utils/                      # 通用能力
+    http.js
+    storage.js
+    format.js
+  index.html
+```
+
+规则:
+- **模块内聚**:一个功能模块的页面、service、model 放在同一目录,便于整体搬走 / 整体删除。
+- **跨模块共享**才上提到 `services/`(如 auth、字典、权限)。
+- **通用 UI** 才留在顶层 `components/`;只在某模块用的组件放在该模块目录内。
+
+## 三、模块的对外契约
+
+一个功能模块**只暴露页面组件**(供路由挂载)与必要的 service。内部实现(子组件、model)不外泄。
+
+```
+modules/user/
+  index.js            # 可选:统一出口
+    export { PageUserList } from './page-user-list.js';
+    export { userService } from './user-service.js';
+```
+
+- 别的模块**不要**深挖 `modules/user/内部子组件.js`。
+- 需要跨模块调用的,通过模块出口或上提的共享 service。
+
+## 四、Service 的粒度
+
+| 粒度 | 例子 | 归属 |
+|------|------|------|
+| 全局域(登录、权限、字典) | `auth-service` | `services/` |
+| 模块域(仅本模块用) | `user-service` | `modules/user/` |
+| ❌ 万能 service | `app-service`(什么都塞) | 拆掉 |
+
+- 一个 service 一个关注点。
+- 模块 service 若被第二个模块需要 → 上提到 `services/`(**上提,而非复制**)。
+
+## 五、什么时候抽到 Repose(组件库 / 能力包)
+
+满足**全部**以下条件才抽:
+
+1. ≥2 个项目在用;
+2. 接口稳定(属性/事件契约不频繁变);
+3. 无业务耦合(不依赖具体业务 model / 接口)。
+
+抽出的产物:
+- **UI 组件库**:一批 `kebab-case` 自定义元素,托管到 FMODE Repose,业务用 importmap 引入。
+- **能力包**:`http`、`storage` 等 utils 打成 ESM 包。
+
+> 抽早了是负担。先用项目内 `components/` `utils/` 沉淀,稳定后再上提。
+
+## 六、依赖方向(严格单向)
+
+```
+routes/modules  →  components  →  services  →  utils
+       │                │              │
+       └────────────────┴──────────────┘   禁止反向
+```
+
+- 上层可依赖下层,**下层禁止反向依赖上层**。
+- `utils` 不认识 `services` / `components`。
+- `services` 不 `import` Lit / 组件。
+- 反向依赖一旦出现,说明职责放错了层。
+
+## 七、模块划分 checklist
+
+- [ ] 换业务就失效的代码,是否都收在 `modules/<feature>/` 里?
+- [ ] 跨模块共享的状态是否上提到 `services/`(而非每个模块各存一份)?
+- [ ] 通用 UI 是否在 `components/`,且不 import 任何业务 model?
+- [ ] 依赖方向是否单向(无 `utils → services` 之类的反向 import)?
+- [ ] 是否有「万能 service / 万能组件」需要拆?
+- [ ] 满足复用条件的组件 / 工具,是否已考虑上提 Repose?
+
+## 八、反模式
+
+| 反模式 | 症状 | 修复 |
+|--------|------|------|
+| 上帝 service | 一个 service 上千行,改哪都怕 | 按关注点拆分 |
+| 组件内联业务 | 页面组件里 assert 校验 + fetch + 拼接口 | 逻辑下沉 service |
+| utils 有状态 | utils 里存了「上次结果」 | 移到 service |
+| 复制粘贴跨模块 | 两个模块各有一份 tableservice | 上提共享 |
+| 过早抽象 | 只有一个用户就搞组件库 | 先项目内沉淀 |

+ 98 - 0
skills/fmode-code-lit/references/rules/modules/examples.md

@@ -0,0 +1,98 @@
+# 场景化模块拆分示例
+
+> 配套 [README.md](./README.md),给出一个「运营后台」的完整拆分演练。
+
+## 一、需求
+
+一个运营后台,含:
+- 登录 / 登出、当前用户、权限判断
+- 侧边菜单(按权限过滤)
+- 用户管理(列表 / 详情 / 编辑)
+- 订单管理(列表 / 详情)
+- 数据字典(下拉选项,多处用)
+- 多标签工作区
+
+## 二、拆分结果
+
+```
+src/
+  app.js                         # 装配:provide 全局 service + 路由 + 根壳
+  components/                    # 通用 UI(无业务耦合)
+    app-shell.js                 # 布局壳(菜单 + 标签 + 内容区)
+    side-menu.js                 # 侧边菜单(接收菜单数据,不自己拉接口)
+    tabs-bar.js                  # 多标签栏
+    tabs-container.js            # KeepAlive 容器
+    base-table.js                # 通用表格(列配置驱动)
+    base-modal.js                # 通用弹窗(slot 内容)
+  modules/
+    auth/
+      page-login.js
+      auth-service.js            # 登录态 + token + 权限码
+    user/
+      page-user-list.js
+      page-user-detail.js
+      user-service.js            # 用户 CRUD
+      user-model.js              # 用户数据结构 + 转换
+    order/
+      page-order-list.js
+      page-order-detail.js
+      order-service.js
+  services/                      # 跨模块共享
+    dict-service.js              # 数据字典(权限/订单模块都用)
+    menu-service.js              # 菜单树(app-shell 用)
+  utils/
+    http.js
+    storage.js
+    format.js
+    router.js
+```
+
+## 三、归属决策演示
+
+| 代码 | 归属 | 理由 |
+|------|------|------|
+| 登录态、token、权限码 | `modules/auth/auth-service.js` | 认证是独立功能域 |
+| 用户 CRUD | `modules/user/user-service.js` | 仅用户模块用 |
+| 订单 CRUD | `modules/order/order-service.js` | 仅订单模块用 |
+| 数据字典 | `services/dict-service.js` | **跨模块**(用户、订单都拉下拉项)→ 上提 |
+| 菜单树 | `services/menu-service.js` | app-shell 用,且依赖权限 → 跨模块 |
+| 表格 / 弹窗 | `components/base-table.js` 等 | 无业务耦合,可复用/可抽库 |
+| http / storage | `utils/` | 纯能力 |
+
+## 四、关键交互(数据流)
+
+```
+page-user-list(视图)
+  ├─ 读 dictService.roles(signal)→ 渲染筛选项
+  ├─ 调 userService.fetchList(query) → 拿到数据
+  └─ 渲染 base-table(传入列配置 + 数据)
+        └─ 行点击 → dispatchEvent('row-click')
+              └─ page-user-list 收到 → router.navigate('/users/:id')
+                    └─ tabsService.open(...) + toggle 激活 → KeepAlive
+```
+
+原则回顾:
+- **组件只读 signal、只转发事件**,不 fetch。
+- **service 持有状态与业务**,无 DOM。
+- **跨模块**的状态在 `services/`,**模块内**的在模块目录。
+
+## 五、依赖方向检查
+
+- `page-user-list` → `base-table`、`user-service`、`dictService` ✅(上层依赖下层/共享)
+- `user-service` → `utils/http` ✅
+- `utils/http` → `auth-service`(取 token)⚠️ —— 这是「工具反向依赖服务」。
+  - **处理**:不改依赖方向,而是让 `auth-service` **注入** token getter 给 http,或 `http` 通过参数接收 token。
+  - 例:`configureHttp({ getToken: () => authService.getToken() })` 在 `app.js` 装配时调用。这样 `utils` 不 import `services`。
+
+## 六、可测试性验证
+
+- `user-service` / `dict-service` 可在 Node 直接 import,mock `http` 后单测。
+- `format.js` / `storage.js` 纯函数,直接断言。
+- `base-table` 组件逻辑(列排序/选择)用 DOM 测试或抽纯函数到 utils 测。
+
+## 七、演进路径(从项目内到 Repose)
+
+1. 项目内先用 `components/base-table.js`。
+2. 第二个项目也要 → 抽到独立包 `fmode-ui-table`(ESM,无构建)。
+3. 托管到 FMODE Repose,两项目 importmap 引入。
+4. 保持契约稳定(属性/事件版本化)。

+ 118 - 0
skills/fmode-code-lit/references/rules/pitfalls.md

@@ -0,0 +1,118 @@
+# 风险与坑位
+
+> 无构建方案的红线。踩中任何一条,症状往往隐蔽(白屏 / 404 / 路由丢失),
+> 排查时先按本表逐条对照。
+
+## 〇、用了装饰器语法(`@customElement` 等)
+
+**症状**:整个模块 `SyntaxError: Invalid or unexpected token` / `Unexpected character '@'`,白屏。
+**原因**:装饰器是 **TS/Babel 编译期**特性,浏览器原生 JS **不执行**。Lit 官方文档默认「装饰器 + 构建」,
+无构建下照抄即挂。
+**对策**:一律用 `static properties` + `customElements.define()` + `ContextConsumer` 编程式 API。
+详见 [components.md](./components.md) 第〇节。**本项目所有示例均无装饰器**。
+
+## 一、CORS(最高频)**症状**:模块加载失败、`Failed to fetch dynamically imported module`、控制台 CORS 报错。
+**原因**:FMODE Repose 未放行业务域名。
+**对策**:Repose 的 CORS 必须允许所有业务域名;跨域模块请求带 `Origin`,响应需 `Access-Control-Allow-Origin`。
+> ESM 模块请求是 **CORS 请求**(比普通脚本严格),勿用「能下载就能跑」判断。
+
+## 二、Content-Type 不是 `text/javascript`
+
+**症状**:`Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of ...`。
+**原因**:依赖仓把 `.js` 当 `text/plain` / `application/octet-stream` 返回。
+**对策**:Repose 上传时显式设 `Content-Type: text/javascript; charset=utf-8`。
+
+## 三、多版本 lit 混用
+
+**症状**:自定义元素重复注册报错 `Failed to execute 'define' ... has already been used`;或响应式失效。
+**原因**:importmap / 直接路径引到了两个不同版本的 lit(如 `lit@3.1` 与 `lit@3.2` 各被加载一次,`LitElement` 类不同)。
+**对策**:**全业务统一 importmap**,只允许一个 lit 版本;所有包都从同一 Repose 版本段解析。除 `lit` 本身,注意 `lit-html` / `lit-element` / `@lit/reactive-element` 也要**单实例**(importmap 里补全子路径映射)。
+
+## 四、History 路由被 CDN 302
+
+**症状**:深链接刷新后地址栏变成 `/index.html`,路由参数丢失;微信 OAuth 回调失败。
+**原因**:CDN 回退用 301/302 重定向。
+**对策**:回退**必须 200** 返回 `index.html`,地址栏不变。见 [deployment.md](./deployment.md)。
+
+## 四·一、`new URL(path, baseURI)` 丢部署前缀(子路径部署最高频)
+
+**症状**:子路径部署下(`/dev/myapp/`)导航跳到域名根 `https://cdn/students`,刷新 404;
+根部署时正常。
+**原因**:`new URL` 的相对解析:`input` 以 `/` 开头时是绝对路径,会**整体替换** base 的
+path,**不保留** base 的目录前缀。
+**对策**:把应用内路径**拼到 base 的 pathname 上**再解析;导航一律走 `utils/router.js`。
+见 [routing-best-practices.md 坑 1](./routing-best-practices.md#坑-1任意的-new-urlpath-baseuri-会丢掉部署前缀)。
+
+## 四·二、`@lit-labs/router` 污染子路径部署的路由状态
+
+**症状**:引入后路由偶发跳错/停错页/点内部链接整页刷新,根部署正常、二级路径异常。
+**原因**:它装全局 `<a>` click 拦截器自行 `pushState`;且用**未剥前缀**的 `location.pathname`
+匹配(路由表是应用内路径)。
+**对策**:子路径部署**自写** `utils/router.js`(~60 行);根部署可选。
+见 [routing-best-practices.md 坑 2](./routing-best-practices.md#坑-2lit-labsrouter-在子路径部署下污染路由状态)。
+
+## 五、子目录部署缺 `<base href>`(或三处前缀不一致)
+
+**症状**:`./app.js`、相对 import 全部 404(被解析到域名根)。
+**原因**:子目录部署(`/admin/`)未写 `<base href="/admin/">`;或 `<base href>`、
+OBS 上传前缀、importmap 路径前缀**三处不一致**。
+**对策**:三处由**同一变量派生**,部署脚本里 `grep` 硬校验;`navigate()` 基于 base 拼 URL。
+见 [routing-best-practices.md 坑 4](./routing-best-practices.md#坑-4base-href-与实际部署前缀不一致三处必须同源)。
+
+## 六、相对 import 漏写 `.js` 后缀
+
+**症状**:`Failed to resolve module specifier './foo'` / 404。
+**原因**:浏览器原生 ESM **不做扩展名补全**。
+**对策**:相对导入一律带 `.js`(`import x from './foo.js'`)。裸标识符(`lit`)走 importmap。
+
+## 七、Signal 与 Lit property 混用
+
+**症状**:更新时机错乱、该刷不刷、或刷两次。
+**原因**:把「组件本地 UI 状态」也塞进全局 signal,或把「跨组件业务状态」放在 `@property` 里手动同步。
+**对策**:**划清界限** —— 组件内 UI 用 `@property`/`@state`;跨组件业务用 signal + `SignalWatcher`。
+
+## 八、ESM 小文件多 → 请求多
+
+**症状**:首屏依赖瀑布、加载慢。
+**原因**:无打包 = 每个模块一次请求。
+**对策**:Repose 开 **HTTP/2/3** 多路复用;合理目录深度(别过深);`Cache-Control` 长缓存版本化依赖。
+
+## 九、老浏览器不支持 Import Map
+
+**症状**:低版本浏览器无法解析裸标识符。
+**原因**:Import Map 是现代浏览器原生特性。
+**对策**:内部后台一般可接受现代浏览器基线;如需兼容,引入 import-map polyfill(es-module-shims)放在所有 module 之前。
+
+## 十、循环依赖导致初始化时机问题
+
+**症状**:某模块拿到 `undefined`(如 `http` 顶层解构了尚未初始化的 `authService`)。
+**原因**:ESM 循环引用 + 顶层立即求值。
+**对策**:跨模块引用**延迟到函数内**取值(别在模块顶层解构相互引用的单例)。
+
+## 十一、无构建下的其他红线
+
+| 坑 | 说明 |
+|----|------|
+| 引入需要打包器转译的依赖(CJS/TS) | 浏览器无法直接跑 CJS;只选 **原生 ESM** 依赖 |
+| 用 `.ts` 源码直发 OBS | 浏览器不认 TS;无构建下**源码即 JS** |
+| 直接改 `src/` 后忘记刷 CDN | 看到旧版本;发布流程要含 CDN 刷新 |
+| `index.html` 被 CDN 长缓存 | 回退/更新拿到旧壳;`index.html` 设 `no-cache` |
+| `index.html` 被 `sync` 跳过 | `sync` 按 size+mtime 判 Skip;**先删后传** |
+| CDN 刷新「看似成功」 | `refresh_task` 是字符串 ID,读 `.id` 恒 undefined → 假成功;须非空断言 + 失败退出非 0 |
+| CDN 刷新只刷了一个域名 | domain_id/凭证对号入座;未知域名直接报错 |
+| 只 `curl` 首页算验收 | 首页 200 ≠ 能跑;须模块图爬取 + 真实浏览器 |
+| 自定义元素标签忘带连字符 | `define` 抛错,如 `approot` 应 `app-root` |
+| 在卡片内嵌链接未 `stopPropagation` | 点内层链接触发外层跳转;内层 `@click` 断冒泡 |
+
+> 路由 / base / CDN 的完整肺坑见 [routing-best-practices.md](./routing-best-practices.md)。
+
+## 十二、排查口诀
+
+1. 看控制台 **MIME / CORS / 404** 三类报错——先分清是「依赖没托管好」还是「业务路径错」。
+2. 打开 Network:模块请求的 **Content-Type** 与 **状态码**。
+3. 深链接刷新 404 → 查 CDN 回退规则(是否 200)。
+4. 相对资源 404 → 查 `<base href>` 与部署前缀是否一致。
+5. 导航跳到域名根 / 前缀丢失 → 查是否用了 `new URL(path, baseURI)`。
+6. 点内部链接整页刷新 / 路由跳错 → 查是否引入 `@lit-labs/router`(子路径部署)或裸 `<a>`。
+7. 线上还是旧版 → 查 CDN 刷新的 domain_id 与 `refresh_task` 返回值(别信「假成功」日志)。
+8. 元素重复注册 → 查 lit 是否多版本。

+ 98 - 0
skills/fmode-code-lit/references/rules/project-structure.md

@@ -0,0 +1,98 @@
+# 项目目录规范
+
+> 无打包 → `src/` 目录结构会**原样**上传到 OBS(不存在 tree-shaking / 文件重命名 / hash),
+> 所以目录即 URL 路径,文件即模块。摆放必须克制、可预测。
+
+## 一、标准目录
+
+```
+src/
+  index.html            # 入口:<base> + <script type="importmap"> + <script type="module" src="./app.js">
+  app.js                # 应用装配:全局 DI provide + 路由挂载 + 根组件渲染 + 多标签
+  components/           # 通用组件(可跨页复用)
+    side-menu.js
+    user-card.js
+    tabs-container.js
+  routes/               # 页面级组件(一页一文件)
+    page-user-list.js
+    page-user-detail.js
+    page-404.js
+  services/             # 业务服务(状态 + 接口 + 计算)
+    auth-service.js
+    table-service.js
+    router-service.js
+  utils/                # 无状态工具
+    http.js
+    storage.js
+    format.js
+```
+
+`examples/hello-lit/` 是本规范的完整可运行样例。
+
+## 二、命名约定
+
+| 类型 | 文件 | 类 / 导出 | 自定义元素标签 |
+|------|------|-----------|----------------|
+| 通用组件 | `kebab-case.js` | `PascalCase`(如 `UserCard`) | `kebab-case`(`<user-card>`,**必须含连字符**) |
+| 页面组件 | `page-xxx.js` | `PageXxx` | `<page-xxx>` |
+| 服务 | `xxx-service.js` | `XxxService` | 单例:`export const xxxService = new XxxService()` |
+| 工具 | `xxx.js` | 具名导出纯函数 | — |
+
+- 目录一律小写 kebab-case。
+- 自定义元素标签**必须**包含短横线(HTML 规范,否则注册失败)。
+- 一个文件一个默认/具名导出的组件类,文件名与标签对应。
+
+## 三、为什么要「目录即路径」
+
+无构建下浏览器按 URL 解析相对 import:
+
+```js
+// 在 /projects/admin/routes/page-user-list.js 中
+import { authService } from '../services/auth-service.js';  // → /projects/admin/services/auth-service.js
+```
+
+因此:
+- **相对导入必须带 `.js` 后缀**(浏览器原生 ESM,不做扩展名补全)。❌ `import x from './foo'`
+- 移动文件 = 改 URL,改文件必须同步改所有引用。
+- 不要深层嵌套(超过 3 层)——ESM 小文件多,路径越长越啰嗦。
+
+## 四、二级子路径部署
+
+子目录部署(如 `/projects/admin/`)时,**必须在 `index.html` 的 `<head>` 写 `<base href>`**:
+
+```html
+<base href="/<project>/"><!-- 例:/admin/ 或 /dev/fmode-app/ -->
+```
+
+否则浏览器会把 `./app.js` 解析到域名根,相对 JS 全部 **404**。
+
+- `<base>` 影响:所有**相对 URL**(`href`/`src`/`fetch` 相对路径/`import` 裸相对路径)。
+- 路由若用 History API 生成链接,需基于 `base` 或应用前缀拼装,见 [routing.md](./routing.md)。
+- 切换部署路径只需改这一处 `<base href>` + importmap 里的业务前缀(若有)。
+
+## 五、依赖不进业务仓
+
+- **禁止**在业务仓 `node_modules` 里放 `lit` 供运行时 `import`。
+- **禁止**把 `lit` 源码拷进 `src/`。
+- 运行时依赖全部由 `index.html` 的 importmap 指向 **FMODE Repose** 解析,见 [deployment.md](./deployment.md)。
+- 本地 `npm install lit @lit-labs/*` **仅**用于 IDE/TS 类型提示,**不参与产物**。
+
+```jsonc
+// package.json —— 仅开发期类型提示,不打包
+{
+  "private": true,
+  "devDependencies": { "lit": "^3.2.0", "@lit-labs/signals": "^0.1.0" },
+  "scripts": { "deploy": "bash scripts/deploy.sh" }
+}
+```
+
+## 六、文件摆放决策表
+
+| 这个东西是… | 放哪 |
+|-------------|------|
+| 全站复用的 UI(菜单、卡片、弹层) | `components/` |
+| 一个路由对应的一整页 | `routes/page-*.js` |
+| 有状态、要跨页共享的业务逻辑 | `services/*-service.js` |
+| 无状态、纯计算的函数 | `utils/` |
+| 全局装配(DI、路由、根标签) | `app.js` |
+| 第三方运行时依赖 | **不放项目里**,走 Repose + importmap |

+ 453 - 0
skills/fmode-code-lit/references/rules/routing-best-practices.md

@@ -0,0 +1,453 @@
+# 路由最佳实践与常见坑(子路径部署 / History 模式 / CDN 回退)
+
+> 本篇是 [routing.md](./routing.md) 的**实战补充**:routing.md 讲「怎么做」,本篇讲
+> 「为什么这么做、错了会怎样、如何一眼定位」。
+>
+> 素材来自一次真实的多页看板交付(Lit 无构建、部署在 `/<env>/<project>/` 二级路径、
+> History 干净 URL、走 OBS + CDN)。所有条目都对应一次真实故障。
+>
+> 通篇只讲**网页应用**的通用规律(路由 / base / CDN / 部署 / 验收),不含任何具体业务。
+
+## 阅读方式
+
+每条按 **症状 → 根因 → 最佳实践 / 规避** 组织。排查时按症状检索即可。
+
+- 前缀缺失、深链接刷新 404:见 [坑 1](#坑-1任意的-new-urlpath-baseuri-会丢掉部署前缀)
+- 路由状态被第三方库污染:见 [坑 2](#坑-2lit-labsrouter-在子路径部署下污染路由状态)
+- 发版后看到旧页面:见 [坑 5](#坑-5cdn-刷新看似成功实则没刷)
+- 部署脚本「跑完了但东西没上去/地址不对」:见 [坑 8](#坑-8部署脚本的-static-cd-陷阱与硬编码-url)
+
+---
+
+## 零、一张图记住全部关系
+
+```
+浏览器地址栏  https://<cdn>/<env>/<project>/students/s01
+                         └────────┬─────────┘
+                    部署前缀(必须三处一致)
+                    = OBS 上传前缀
+                    = <base href>
+                    = importmap 里业务/依赖的路径前缀
+
+index.html   <base href="/<env>/<project>/">     ← 相对 import 的解析基准
+utils/router 把「应用内路径 /students/s01」拼到 base 的 pathname 上 → 真实 URL
+CDN 回退      命中不了静态资源 → 200 返回 index.html(禁 302)
+```
+
+**一句话:应用内部只认「不含前缀的应用内路径」(`/students/s01`),
+前缀只在三处出现一次(base href / 上传前缀 / importmap),且三者必须由同一个变量派生。**
+
+---
+
+## 坑 1:任意的 `new URL(path, baseURI)` 会丢掉部署前缀
+
+**症状**
+- 子路径部署(如 `/dev/myapp/`)下,点导航跳到 `https://<cdn>/students`(**域名根**,前缀没了)。
+- 刷新该页 → **404**(OBS 上根目录没有这个文件,CDN 回退也没覆盖到)。
+- 本地开发用根路径部署时一切正常,一上线换二级路径就崩 —— 典型的「环境相关」假象。
+
+**根因**
+
+`new URL(input, base)` 的语义是 **RFC 3986 相对解析**:当 `input` **以 `/` 开头**时,
+它是**绝对路径**,会**整体替换** base 的 path,**不保留 base 的目录前缀**。
+
+```js
+new URL('/students', 'https://cdn/dev/myapp/')
+// → https://cdn/students        ❌ 前缀 /dev/myapp 被丢掉
+new URL('students', 'https://cdn/dev/myapp/')
+// → https://cdn/dev/myapp/students  ✅ 但依赖 base 以 / 结尾,脆弱
+```
+
+而应用内部路径(`/students`)天然带前导 `/`,于是「尊重 `<base href>`」这个直觉写法
+在子路径部署下**必然**丢前缀。
+
+**最佳实践**
+
+先把应用内路径**拼到 base 的 pathname 上**,再交给 URL 解析(此时相对解析不会丢前缀):
+
+```js
+/** base 路径(去尾斜杠),如 '/dev/myapp';根部署时为 '' */
+export function basePath() {
+  return new URL(document.baseURI).pathname.replace(/\/+$/, '');
+}
+
+/** 应用内路径 → 保留部署前缀的绝对 URL(查询串 / hash 原样保留) */
+export function resolveUrl(path) {
+  const base = basePath();
+  const q = path.indexOf('?');
+  const h = path.indexOf('#');
+  let cut = path.length;
+  if (q !== -1) cut = Math.min(cut, q);
+  if (h !== -1) cut = Math.min(cut, h);
+  let p = path.slice(0, cut);
+  const rest = path.slice(cut);          // ?query#hash 原样带过去
+  if (!p.startsWith('/')) p = '/' + p;   // 容错:允许传 'students'
+  return new URL(base + p + rest, location.origin);
+}
+```
+
+**红线(写进 code review 清单)**:业务代码里**禁止**出现 `new URL(path, document.baseURI)`
+用在「导航 / 内部链接」上。一切经 `utils/router.js` 的 `resolveUrl()`。
+
+---
+
+## 坑 2:`@lit-labs/router` 在子路径部署下污染路由状态
+
+**症状**
+- 引入 `@lit-labs/router` 后,子路径部署的应用里路由「偶尔跳错 / 停在错误页 / 点内部链接整页刷新」。
+- 根部署时完全正常,换二级路径后诡异。
+
+**根因(两点叠加)**
+
+1. 它在 `window` 上装了**全局 click 拦截器**,对**同源 `<a>` 自行 `pushState`** ——
+   绕过你自己的导航函数(也就绕过了 base 前缀处理),且与「手动 `<a>` + 原生刷新」的行为预期冲突。
+2. 它在 `hostConnected` / `popstate` 时,用**未经剥离的 `location.pathname`**(带着 `/dev/myapp/`)
+   去匹配路由表 —— 而路由表里的 path 是**应用内路径**(`/students`),于是匹配不上 / 匹配错。
+
+**最佳实践**
+
+- **子路径部署的项目,不要用 `@lit-labs/router`**。自写一个 ~60 行的 `utils/router.js`:
+  - `navigate(path, {replace})`:`pushState` 后**手动派发 `popstate`**(History 原生不触发),
+    让订阅者感知;内部走 `resolveUrl()`(保留前缀)。
+  - `currentPath()`:从 `location.pathname` **剥掉 base 前缀**,返回应用内路径。
+  - `href(path)`:给**真实 `<a>`** 用,返回保留前缀的完整 URL。
+- 要不要界面路由框架?**不需要**。无构建项目里「路由表 + `currentPath()` + `popstate` 订阅」
+  就是最小的、可调试的路由。见 [routing.md 第二节](./routing.md)。
+- 若坚持用第三方 router:必须在 `goto()`/`popstate` 前把 `pathname` 剥前缀,并
+  关掉/绕过它的全局 `<a>` 拦截 —— 成本通常高于自写。
+
+**配套:`currentPath()` 要稳**
+
+```js
+export function currentPath() {
+  const base = basePath();
+  const p = location.pathname;
+  let path = p.startsWith(base) ? p.slice(base.length) : p;  // 脏 URL 退回全路径
+  if (!path.startsWith('/')) path = '/' + path;
+  return path.replace(/\/{2,}/g, '/') || '/';   // 折叠 '//students'
+}
+```
+
+- **剥前缀**:路由表只认应用内路径。
+- **脏数据兜底**:万一 URL 落在前缀之外(历史书签 / 手工输入),**退回到 `/` 开头的全路径**,
+  交给路由表落到 404 页 —— 别抛异常、别白屏。
+- **折叠重复斜杠**:`/dev/myapp//students` 不该变成两条不同路由。
+
+---
+
+## 坑 3:深链接刷新 404 —— CDN 回退不是 200
+
+**症状**
+- 站内点击正常(SPA 前端接管),但**在该 URL 上直接刷新 / 粘贴给别人打开 → 404**。
+- 或:刷新后**地址栏变成了 `/index.html`**,路由参数全丢(这是 302 的特征)。
+- 微信 OAuth 回调失败(`redirect_uri` 与实际落地 URL 不一致)。
+
+**根因**
+
+History 模式下,`/dev/myapp/students/s01` 这个路径在 OBS 上**根本不存在**(OBS 里只有
+`index.html` 和真实文件)。没有回退规则时,CDN 直接 404。
+
+**最佳实践**
+
+CDN 配置**两条回退规则**,顺序不能反:
+
+| 顺序 | 匹配 | 动作 |
+|------|------|------|
+| 规则 1 | 命中真实静态资源(`.js` / `.css` / `.png` …) | 直接返回该文件 |
+| 规则 2 | 其余(路径不存在) | 返回 `index.html`,**状态码 200** |
+
+- **必须 200,禁止 301/302**:302 会由浏览器改写地址栏、丢失 `location.pathname`,
+  路由参数与 OAuth 回调全部破坏。回退「返回内容」而非「重定向」。
+- `index.html` 设 `Cache-Control: no-cache`,保证回退永远拿到最新壳。
+- 部署后**逐条实测深链接**(含一个不存在的路径,验证它会落到 404 页而不是报错)。
+  见 [坑 7 验收清单](#坑-7只-curl-首页不算验收)。
+
+---
+
+## 坑 4:`<base href>` 与实际部署前缀不一致(三处必须同源)
+
+**症状**
+- 所有相对 import / 相对资源(`./app.js`、`./components/x.js`)**全部 404**,
+  被解析到**域名根**而不是项目目录。
+- 或换个环境部署后,importmap 指向的依赖路径 404。
+
+**根因**
+
+`<base href>`、OBS 上传前缀、importmap 里的路径前缀,是**同一个前缀的三个副本**。
+只要有一处不一致(比如手工改了其中一处),就从那一处开始 404。
+
+**最佳实践**
+
+1. **三处同源**:在部署脚本里用**一个变量**派生全部前缀,禁止三处各写各的:
+
+   ```bash
+   OBS_PREFIX="dev/${PROJECT}"       # OBS 上传前缀
+   BASE_HREF="/dev/${PROJECT}/"      # <base href> 期望值
+   # importmap 里的业务/依赖路径 = /${OBS_PREFIX}/...
+   ```
+
+2. **部署脚本里硬校验**(失败即中止,别等到线上白屏):
+
+   ```bash
+   grep -q "<base href=\"${BASE_HREF}\"" src/index.html \
+     && echo "ok base href = ${BASE_HREF}" \
+     || { echo "!! base href 与部署前缀 ${BASE_HREF} 不一致,相对 import 会 404" >&2; exit 1; }
+   ```
+
+3. **importmap 用内联**(`<script type="importmap">{...}</script>`),不要用
+   `<script type="importmap" src="...">` 外链 —— 外链 importmap 浏览器支持面窄,
+   内联是唯一 100% 可靠的形式。内联的那份同时托管一份到依赖仓供他项目复用。
+4. **依赖也带前缀**:若依赖与业务同源(如都在 `/<env>/npm/`),importmap 里依赖路径
+   **必须**写成 `/<env>/npm/lit/vX/index.js`,与 `<base href>` 前缀一致;
+   同源即无 CORS 问题,是省事之选。
+
+> 判定口诀:**根部署(`/`)时它无所谓,子路径部署时它是所有 404 的总开关。**
+
+---
+
+## 坑 5:CDN 刷新「看似成功实则没刷」
+
+**症状**
+- 发布后线上仍是**旧页面**(甚至新旧混着:壳是新的、某个 js 是旧的)。
+- 刷新脚本日志打印 `taskId: undefined` 或干脆只刷了一个域名 —— 但**退出码为 0**,流水线绿灯。
+
+**根因(两类,都要防)**
+
+1. **域名/凭证错配**:脚本用的 domain_id(或 AK)只对某个加速域有效,
+   只刷了 `a.example.com`,没刷 `app.example.com` —— 而用户访问的是后者。
+2. **SDK 返回结构读错**:脚本读 `result.refresh_task.id`,而 SDK 实际返回的
+   `refresh_task` 是**字符串 ID**(不是对象)→ 恒得 `undefined` → 打印
+   `taskId: undefined` 后**照样退出 0**,形成**假成功**。
+
+**最佳实践**
+
+- 刷新 **`type: 'directory'`**(URL 以 `/` 结尾),一次覆盖目录下新增/变更文件,
+  比逐个 file 刷新省事、不易漏。
+- **按域名对号入座**:维护 `host → domain_id` 映射表,URL 里解析出 host 再取对应
+  domain_id + 凭证;**未知域名直接报错退出**,绝不静默跳过。
+- **把假成功变成真失败**:对刷新结果**做非空断言**,任一失败则**退出码非 0**:
+
+  ```js
+  const taskId = r.refresh_task;              // ← 字符串 ID,不是 r.refresh_task.id
+  if (!taskId) { failed++; continue; }        // 没有 taskId = 没提交成功,不得算成功
+  ...
+  process.exit(failed ? 1 : 0);
+  ```
+
+- 部署脚本调用刷新失败时**不要吞掉**(最多提示可手动重试,但要让流水线红)。
+
+---
+
+## 坑 6:`index.html` 被 `sync` 跳过 / 被长缓存
+
+**症状**
+- 发了新版,`index.html` 还是旧的(但新增的文件都在)—— 典型的「壳没更新」。
+- 或者 CDN 回退拿到的 `index.html` 是缓存里的旧壳。
+
+**根因**
+
+- `obsutil sync` 按 **size + mtime** 判定 Skip。`index.html` 名字固定、内容随发布变化,
+  若 size 恰好相同(或 mtime 未变),**会被静默跳过**。
+- CDN 若把 `index.html` 也长缓存,回退/更新都拿到旧壳。
+
+**最佳实践**
+
+- **先删后传**:`obsutil rm <prefix>/index.html` 再 `obsutil cp ./src/index.html ...`,
+  并排除它不参与上一步的 `sync`(`-exclude="index.html"`)。
+- 传 `index.html` 时显式设头:
+  `-meta="Cache-Control:no-cache, no-store, must-revalidate#Content-Type:text/html; charset=utf-8"`。
+- 带内容 hash 的静态资源可长缓存;`index.html` 永远 `no-cache`。
+- ⚠️ **CDN 冷边缘**:新上传的 key 首次被某边缘节点拉取时**偶发 404**(回源竞争),
+  **重跑一次即消失**。验收脚本遇到新 key 的首次 404 不要立刻判定为故障。
+
+---
+
+## 坑 7:只 `curl` 首页不算验收
+
+**症状**
+- 部署脚本 `curl -w '%{http_code}'` 首页 → 200 → 认为发布成功。
+- 用户打开 → **白屏**(某个模块 404 / MIME 错 / 回退没配 / 某个相对路径错一个字符)。
+
+**根因**
+
+`curl` 首页 200 只能证明 `index.html` 在。浏览器加载一个无构建 ESM 应用会走一条**长链路**,
+任一环断裂都白屏,而 `curl` 一个都不覆盖:
+
+```
+index.html → 内联 importmap → 解析裸标识符 → 拉模块(MIME 必须 text/javascript)
+           → 模块内相对 import(必须带 .js)→ 再拉… → 深链接回退(必须 200)
+```
+
+**最佳实践(分层验收,越往下越真)**
+
+1. **静态体检**(部署前,`node --check` + 解析):语法/装饰器残留、相对 import 带 `.js`、
+   相对目标存在、裸标识符都在 importmap、自定义元素标签含连字符。
+2. **模块图爬取**(部署后,HTTP 层):从入口 `app.js` 出发**跟随相对 import 走完整图**,
+   断言每个文件 **200 + MIME 为 `text/javascript`**;再断言 importmap 每条目标可达;
+   再断言**深链接返回 200 且内容是 index.html**(`num_redirects=0`)。
+   —— 这一步比人眼快,且能在 CI 里跑。
+3. **真实浏览器**(最终验收):用 Playwright/真实浏览器**点导航 + 刷新深链接**,
+   确认不 404、无控制台报错。HTTP 层过了不代表渲染没问题(渲染报错、Signal 时机等)。
+4. **缓存层**:核对 CDN 实际返回的**内容**(不只是状态码),确认是新版。
+
+> 原则:**能自动断言的都断言,别靠「我 curl 了一下 200」。**
+> 一个可复用的验收脚本应把上面 1–2 步固化成一条命令,纳入部署流程。
+
+---
+
+## 坑 8:部署脚本的 static cd 陷阱与硬编码 URL
+
+**症状**
+- 脚本在某个目录下「卡住」或后续命令全部相对错位。
+- 文档里的交付链接**换一个项目 / 换一个篇目就 404**(把上一篇的 URL 抄到了新篇)。
+
+**根因**
+
+- **`cd` 相对路径**:脚本用相对路径 `cd`,或在管道/子 shell 里改了 cwd,
+  后续 `obsutil` / 文件路径全部相对错位。
+- **硬编码 URL**:把「某一次的线上地址」写死进文档/配置;换项目后就指向不存在的资源。
+
+**最佳实践**
+
+- 脚本开头用**以脚本自身位置推导的绝对路径**,不依赖调用者的 cwd:
+
+  ```bash
+  set -euo pipefail
+  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+  cd "$REPO_ROOT"
+  ```
+
+- **参数化**:域名 / 前缀 / 桶都从**变量或参数**取(`PROJECT`、`OBS_PREFIX`、`BASE_HREF`),
+  绝不散落字面量;URL 用这些变量拼出来。
+- **交付物链接**用统一的 URL 构造函数生成(`https://<cdn>/<prefix>/<path>`),
+  不要人肉抄。文档里若必须贴链接,**注明它是哪个项目/前缀**,避免跨项目误用。
+- 顺手加 `--dry` 只打印不执行的开关,方便核对将要跑的命令。
+
+---
+
+## 坑 9:裸 `<a href>` 与 SPA 导航冲突;`href()` 要留出「新标签打开」
+
+**症状**
+- 点内部链接**整页刷新**:多标签 KeepAlive 全丢、状态重置、白屏一瞬。
+- 或:为了 SPA 全部改用 `navigate()`,结果**中键/右键「在新标签打开」失效**。
+
+**根因**
+
+- 原生 `<a href="/x">` 触发**整页导航**,不是 SPA 行为。
+- 但**全部**改成 JS click 又牺牲了浏览器原生能力(新标签、复制链接、无障碍)。
+
+**最佳实践**
+
+两套出口,按需选:
+
+| 场景 | 用什么 | 说明 |
+|------|--------|------|
+| 站内跳转(按钮/菜单项,语义是「切页面」) | `navigate(path)` | `pushState` + 派发 `popstate`,SPA 不刷新 |
+| 真链接(要给用户「新标签打开/复制链接」) | `<a href=${href(path)}>` | `href()` 返回**保留前缀**的完整 URL,中间键/右键正常 |
+| 在卡片/行内点击的**链接**(防止触发外层跳转) | `@click=${e => e.stopPropagation()}` | 见 [坑 10](#坑-10可点击交付链接点它不该触发外层卡片跳转) |
+
+- 内部导航**一律**经 `utils/router.js`,**禁止**裸 `new URL(path, baseURI)`(见坑 1)
+  与第三方全局拦截(见坑 2)。
+- 菜单项若用 `<div>` 模拟链接,记得补 `role="link" tabindex="0"` 与
+  `@keydown` 回车触发,保持键盘可达。
+
+---
+
+## 坑 10:可点击交付链接,点它不该触发外层卡片跳转
+
+**症状**
+- 卡片整体可点(进详情),卡片里还嵌了「打开报告 / 打开仓库」链接 ——
+  点内层链接时**外层跳转也触发了**,或整页被导航走、新标签没打开。
+
+**根因**
+
+事件冒泡:内层 `<a>` 的 `click` 冒泡到外层卡片的 `click` 处理器。
+
+**最佳实践**
+
+- 内层链接 `@click=${(e) => e.stopPropagation()}`,切断冒泡;用**真实 `<a>` + `target="_blank"`**
+  让浏览器开新标签,别用 `window.open` 绕。
+- 交给数据的链接用**统一结构**(如 `{kind, label, url}` 数组),渲染层据 `kind` 决定
+  图标/文案,便于一致地展示「报告 / 看板 / 文档 / 仓库」等多种入口。
+- **进度/状态展示**这类看板常见模式:
+  - **不要编造数据**:字段缺失就**留空**(如还没有的真实 URL 一律空),
+    比填假的强 —— 空会被发现,假的会被当成事实。
+  - 筛选层统一剔除**不该计入统计的条目**(内部人员/测试数据),
+    在服务层做,不要在展示层各自打补丁。
+  - 进度口径**写清楚**(「以输出报告为最终节点」这类),口径变了要在 UI/文档同步标注,
+    避免同一数字两种理解。
+
+---
+
+## 附:内部导航工具(可直接复制的最小实现)
+
+```js
+// utils/router.js —— 子路径部署安全的导航(只用 History API,干净 URL)
+export function basePath() {
+  return new URL(document.baseURI).pathname.replace(/\/+$/, '');
+}
+
+export function resolveUrl(path) {
+  const base = basePath();
+  const q = path.indexOf('?'), h = path.indexOf('#');
+  let cut = path.length;
+  if (q !== -1) cut = Math.min(cut, q);
+  if (h !== -1) cut = Math.min(cut, h);
+  let p = path.slice(0, cut);
+  const rest = path.slice(cut);
+  if (!p.startsWith('/')) p = '/' + p;
+  return new URL(base + p + rest, location.origin);
+}
+
+export function navigate(path, { replace = false } = {}) {
+  const url = resolveUrl(path);
+  if (replace) history.replaceState({}, '', url);
+  else history.pushState({}, '', url);
+  window.dispatchEvent(new PopStateEvent('popstate'));  // History 原生不触发
+}
+
+export function currentPath() {
+  const base = basePath();
+  let path = location.pathname;
+  if (base && path.startsWith(base)) path = path.slice(base.length);
+  if (!path.startsWith('/')) path = '/' + path;
+  return path.replace(/\/{2,}/g, '/') || '/';
+}
+
+export function currentQuery() {
+  return Object.fromEntries(new URLSearchParams(location.search));
+}
+
+export function href(path) { return resolveUrl(path); }   // 供真实 <a href>
+```
+
+路由表 + 订阅(`app-shell` 里):监听 `popstate`,用 `currentPath()` 匹配路由表,
+命中则「打开/激活」对应视图(多标签场景下不要整体重渲染,见
+[tabs-keepalive.md](./tabs-keepalive.md))。
+
+---
+
+## 附:上线前 checklist(路由 / base / CDN 维度)
+
+- [ ] `<base href>` == OBS 上传前缀 == importmap 路径前缀(**同一变量派生**,脚本里有硬校验)
+- [ ] importmap **内联**在 `index.html`,依赖路径带正确的部署前缀
+- [ ] 业务代码**无** `new URL(path, baseURI)` 用于导航;无 `@lit-labs/router`(子路径部署)
+- [ ] `currentPath()` 剥前缀 + 脏 URL 兜底 + 折叠 `//`
+- [ ] CDN 回退:命中真实资源直返;不存在路径 **200** 返回 `index.html`,**禁 301/302**
+- [ ] `index.html` 先删后传 + `no-cache`
+- [ ] CDN 刷新:`type:directory`,**对号入座 domain_id**,读 `refresh_task`(字符串),失败退出非 0
+- [ ] 静态体检(语法/装饰器/`.js` 后缀/importmap 覆盖/标签连字符)通过
+- [ ] 模块图爬取(200 + `text/javascript`)通过
+- [ ] 深链接**刷新** 200 且为 `index.html`(含一个不存在的路径 → 落 404 页)
+- [ ] **真实浏览器**点导航 + 刷新深链接,无 404、无控制台报错
+- [ ] 交付链接 `target=_blank` 可点、不触发外层跳转;无数据不编造,留空
+
+---
+
+## 相关
+
+- [routing.md](./routing.md) —— 路由基本用法(History、守卫、OAuth、多标签交互)
+- [deployment.md](./deployment.md) —— Repose 依赖托管 + OBS/CDN 部署 + 回退规则
+- [pitfalls.md](./pitfalls.md) —— 无构建的通用坑(装饰器 / CORS / MIME / 多版本 lit)
+- [tabs-keepalive.md](./tabs-keepalive.md) —— 多标签 KeepAlive 与路由的配合
+- [project-structure.md](./project-structure.md) —— 子路径部署与 `<base href>`

+ 163 - 0
skills/fmode-code-lit/references/rules/routing.md

@@ -0,0 +1,163 @@
+# 路由(History 干净 URL)
+
+> 使用 History 模式得到**干净 URL**(无 `#`)。这是内部后台 + 微信 OAuth 回调的硬性前提:
+> OAuth 回调 URL 不能带 hash,且必须由 CDN 返回 **200**(不能 302)。
+>
+> ⚠️ **子路径部署(`/<env>/<project>/`)优先自写 `utils/router.js`,不要用 `@lit-labs/router`**:
+> 它装全局 `<a>` click 拦截器,且用**未剥前缀**的 `location.pathname` 匹配路由,会污染路由状态。
+> 根部署可选用它;子路径部署一律自写 —— 最小实现与完整踩坑见
+> [routing-best-practices.md](./routing-best-practices.md)(坑 1、坑 2)。
+
+## 〇、子路径部署的红线(先看这条)
+
+应用内部只认「**不含前缀的应用内路径**」(`/students/s01`),前缀只在三处出现且必须同源:
+`<base href>` == OBS 上传前缀 == importmap 路径前缀。
+
+- ❌ **禁止** `new URL(path, document.baseURI)` 做导航 —— 当 `path` 以 `/` 开头时
+  会**丢掉 base 的路径前缀**,刷新即 404。正确做法:拼到 base 的 **pathname** 上再解析。
+- `currentPath()` 要**剥掉前缀**并折叠重复斜杠,脏 URL 兜底到 404 页。
+
+完整根因与规避见 [routing-best-practices.md](./routing-best-practices.md)。
+
+## 一、基本用法(@lit-labs/router,仅根部署)
+
+```js
+// services/router-service.js 或 app.js
+import { Router } from '@lit-labs/router';
+
+export function createRouter(host, routes) {
+  return new Router(host, {
+    routes: [
+      { path: '/', render: () => html`<page-home></page-home>` },
+      { path: '/users', render: () => html`<page-user-list></page-user-list>` },
+      { path: '/users/:id', render: ({ id }) => html`<page-user-detail .userId=${id}></page-user-detail>` },
+      { path: '/*', render: () => html`<page-404></page-404>` },
+    ],
+  });
+}
+```
+
+在根组件里挂载:
+
+```js
+import { Router } from '@lit-labs/router';
+
+export class AppShell extends LitElement {
+  #router = new Router(this, [
+    { path: '/', render: () => html`<page-home></page-home>` },
+    { path: '/users', render: () => html`<page-user-list></page-user-list>` },
+    { path: '/*', render: () => html`<page-404></page-404>` },
+  ]);
+
+  render() {
+    return html`
+      <side-menu></side-menu>
+      <main>${this.#router.outlet()}</main>
+    `;
+  }
+}
+```
+
+- `path` 支持参数段 `:id` 与通配 `/*`。
+- `render(params)` 返回模板;参数从 `params` 取。
+- `outlet()` 是渲染出口。
+
+## 二、导航(干净 URL)
+
+```js
+import { navigate } from './router-utils.js';
+
+navigate('/users/42');          // pushState
+navigate('/login', { replace: true });
+```
+
+`utils/router.js` 里封装,**把应用内路径拼到 base 的 pathname 上**(不能用
+`new URL(path, document.baseURI)`,会丢前缀):
+
+```js
+export function basePath() {
+  return new URL(document.baseURI).pathname.replace(/\/+$/, '');   // '/dev/myapp' 或 ''
+}
+export function resolveUrl(path) {
+  const base = basePath();
+  const q = path.indexOf('?'), h = path.indexOf('#');
+  let cut = path.length;
+  if (q !== -1) cut = Math.min(cut, q);
+  if (h !== -1) cut = Math.min(cut, h);
+  let p = path.slice(0, cut);
+  const rest = path.slice(cut);              // 查询串 / hash 原样保留
+  if (!p.startsWith('/')) p = '/' + p;
+  return new URL(base + p + rest, location.origin);
+}
+export function navigate(path, { replace = false } = {}) {
+  const url = resolveUrl(path);
+  if (replace) history.replaceState({}, '', url);
+  else history.pushState({}, '', url);
+  window.dispatchEvent(new PopStateEvent('popstate'));
+}
+```
+
+- **拼到 base 的 pathname 上**再解析,子目录部署(`/admin/`)才不会丢前缀。
+- 手动 `pushState` 后派发 `popstate`,让订阅者感知(History 原生不触发)。
+- 真链接用 `href(path)`(= `resolveUrl`)供 `<a href>` 使用,保留中键「新标签打开」。
+- 完整实现与坑见 [routing-best-practices.md](./routing-best-practices.md)。
+
+## 三、CDN 回退规则(History 路由的前提)
+
+History 模式下,用户在 `/users/42` 直接刷新 → 该路径在 OBS 上不存在 → **必须由 CDN 回退规则**处理:
+
+| 规则 | 行为 |
+|------|------|
+| 规则 1 | 路径命中真实静态资源(`.js` / `.css` / `.png` …)→ 直接返回 |
+| 规则 2 | 路径不存在 → 返回 `index.html`,**状态码 200**,**禁止 301/302** |
+
+> ⚠️ 若用 302 重定向到 `/index.html`,浏览器地址栏会被改写、`location.pathname` 丢失,路由参数全丢,微信 OAuth 回调也会失败。**必须 200**。
+
+配置见 [deployment.md](./deployment.md)。
+
+## 四、路由守卫(鉴权)
+
+```js
+const requireAuth = (render) => (params) => {
+  if (!authService.isLoggedIn.get()) {
+    navigate('/login');
+    return html``;   // 或渲染登录页占位
+  }
+  return render(params);
+};
+
+new Router(this, [
+  { path: '/users', render: requireAuth((p) => html`<page-user-list></page-user-list>`) },
+]);
+```
+
+- 守卫是**纯函数包装**,不引入框架概念。
+- 异步守卫(先校验 token 再放行)可返回 Promise 模板,或先渲染 loading。
+
+## 五、OAuth 回调(微信)
+
+- 回调地址设为业务前缀下的干净路径,如 `https://<cdn-domain>/<project>/oauth/callback`。
+- 该路径在 CDN 上不存在 → 由回退规则 200 返回 `index.html` → 前端读取 `location.search` 里的 `code`。
+- **全程不能出现 302**,否则微信校验的 `redirect_uri` 与实际落地 URL 不一致。
+
+## 六、多标签的交互
+
+若应用是多标签后台(见 [tabs-keepalive.md](./tabs-keepalive.md)),路由切换需驱动「打开/激活标签」而非整体重渲染:
+
+```js
+// Router 命中新路径时 → tabsService.open(path, { title, component })
+// 已存在同 path 标签 → 仅激活;不存在 → 新开
+```
+
+## 七、坑
+
+- 忘记 CDN 回退 → 刷新 404。回退**必须 200**,禁 301/302(302 会丢 `pathname`)。
+- `<base href>` 配错 / 与部署前缀不一致 → 相对 import 404。
+- `new URL(path, document.baseURI)` 丢部署前缀 → 子路径深链接 404(**最高频**,见坑 1)。
+- `@lit-labs/router` 在子路径部署下污染路由状态(全局 `<a>` 拦截 + 未剥前缀 pathname)。
+- 用 `<a href="/x">` 原生跳转 → 整页刷新(非 SPA)。内部导航一律 `navigate()` + `popstate`;
+  要保留「新标签打开」的用 `href()`。
+- 只 `curl` 首页 200 就算验收 → 用户白屏。**必须**跑模块图爬取 + 真实浏览器。
+- 混用 hash 路由与 History 路由 → 混乱。**只用 History**。
+
+> 以上每条的**症状 → 根因 → 规避**,逐条见 [routing-best-practices.md](./routing-best-practices.md)。

+ 108 - 0
skills/fmode-code-lit/references/rules/services.md

@@ -0,0 +1,108 @@
+# 服务规范(Service / Signal)
+
+> Service 是普通 ESM Class:**存全局状态(Signal)、封装接口、做校验与转换、写业务计算**。
+> 它**不依赖 DOM、不 import Lit**,因此能在 Node 里直接单测。
+
+## 一、Service 骨架(Signal + 单例)
+
+```js
+// services/auth-service.js
+import { signal, computed } from '@lit-labs/signals';
+import { http } from '../utils/http.js';
+
+class AuthService {
+  /** 私有信号:当前用户(null 表示未登录) */
+  #user = signal(null);
+  /** 私有信号:token,初始化时从 localStorage 恢复 */
+  #token = signal(localStorage.getItem('token') || '');
+
+  /** 对外只读视图 */
+  get user() { return this.#user; }
+  get isLoggedIn() { return computed(() => this.#user.get() != null); }
+
+  async login(username, password) {
+    const { token, user } = await http.post('/api/auth/login', { username, password });
+    this.#token.set(token);
+    this.#user.set(user);
+    localStorage.setItem('token', token);
+    return user;
+  }
+
+  logout() {
+    this.#token.set('');
+    this.#user.set(null);
+    localStorage.removeItem('token');
+  }
+
+  getToken() { return this.#token.get(); }
+}
+
+/** 全局单例:全应用共享同一份状态 */
+export const authService = new AuthService();
+```
+
+要点:
+- 状态用 `signal()` 持有;派生值用 `computed()`;副作用用 `effect()`。
+- 对外通过 getter 暴露**只读** signal,写操作走方法(封装校验)。
+- **单例导出**(`export const xxxService = new XxxService()`),全应用 import 同一实例。
+- 不在 Service 里 `import { html }` 或操作 `document`。
+
+## 二、Signal 三件套
+
+| API | 用途 | 例 |
+|-----|------|----|
+| `signal(v)` | 可读写信号 | `const count = signal(0); count.set(1); count.get();` |
+| `computed(fn)` | 派生只读信号 | `const total = computed(() => items.get().length);` |
+| `effect(fn)` | 副作用(自动追踪依赖) | `effect(() => console.log(count.get()));` |
+
+> 在组件里用 `SignalWatcher(LitElement)` 混入后,`render()` 里**读取** signal 即自动订阅。
+
+## 三、状态放哪(再强调)
+
+| 场景 | 方案 |
+|------|------|
+| 全局业务状态,多页共享(登录 / 字典 / 权限) | **Signal Service**(本文件) + Context 注入 |
+| 组件本地状态(弹窗 / 输入) | Lit `@property` / `@state` |
+| 简单应用不想引 signals | `EventTarget` 简易 Store(下方) |
+| 一次性持久化配置 | `utils/storage.js` 封装 |
+
+## 四、不引 signals 时的简易 Store
+
+```js
+// services/store.js —— 仅当不想引 @lit-labs/signals 时的轻量替代
+export class Store extends EventTarget {
+  #state = {};
+  get state() { return this.#state; }
+  setState(patch) {
+    this.#state = { ...this.#state, ...patch };
+    this.dispatchEvent(new CustomEvent('change', { detail: this.#state }));
+  }
+}
+export const store = new Store();
+```
+
+组件里监听 `store.addEventListener('change', () => this.requestUpdate())`。
+
+## 五、Service 与 DOM 的边界
+
+- Service **可以** 读写 `localStorage`、调 `fetch`、算业务 —— 这些是「环境无关」的。
+- Service **不可以** 持有元素引用、操作 `document`、`import Lit`。
+- 需要通知 UI 的地方 → 改 signal,UI 自然响应,**不要**反向去 call 组件方法。
+
+## 六、可测试性(这是分层的回报)
+
+```js
+// auth-service.test.js(Node 直接跑,无需浏览器)
+import { authService } from './auth-service.js';
+// 依赖 http 可注入 mock;signal 可直接 set/get 断言
+```
+
+- Service 无 DOM 依赖 → 可在 Node / CI 单测。
+- 依赖的外部调用(http)建议通过构造参数注入,便于 mock。
+- 业务计算放纯函数到 `utils/`,Service 只做编排。
+
+## 七、命名与目录
+
+- 文件:`services/<name>-service.js`。
+- 类:`<Name>Service`;单例:`export const <name>Service = new <Name>Service()`。
+- 一个 Service 一个关注点:`auth-service`、`table-service`、`router-service`,不要「万能 service」。

+ 136 - 0
skills/fmode-code-lit/references/rules/tabs-keepalive.md

@@ -0,0 +1,136 @@
+# 后台多标签 + KeepAlive
+
+> 同一 `index.html` 内前端路由切换**不刷新页面**;KeepAlive 用 `display:none` 隐藏非激活页容器,
+> **组件实例常驻**,表单 / 内部状态不丢失。对标后台管理系统的多 Tab 工作区。
+
+## 一、核心思路
+
+```
+根组件维护 tabs 数组 → 渲染所有已打开标签页的容器
+每个标签页由一个「路由判断激活」的 display 控制显隐
+激活的标签 display:block,非激活 display:none(组件不销毁)
+```
+
+- **不销毁 = 状态保留**:组件实例一直在 DOM 里,`@state` 里的表单值、滚动位置都在。
+- **不是 `<iframe>` / 不是多页**:单页应用,只是同页多容器。
+
+## 二、tabsService(状态)
+
+```js
+// services/tabs-service.js
+import { signal } from '@lit-labs/signals';
+
+class TabsService {
+  #tabs = signal([
+    { id: 'home', path: '/', title: '首页', closable: false },
+  ]);
+  #activeId = signal('home');
+
+  get tabs() { return this.#tabs; }
+  get activeId() { return this.#activeId; }
+
+  /** 打开(或激活已存在的)标签:同 path 不重复开 */
+  open({ id, path, title, closable = true }) {
+    const list = this.#tabs.get();
+    if (!list.some((t) => t.id === id)) {
+      this.#tabs.set([...list, { id, path, title, closable }]);
+    }
+    this.#activeId.set(id);
+  }
+
+  close(id) {
+    const list = this.#tabs.get().filter((t) => t.id !== id);
+    this.#tabs.set(list);
+    if (this.#activeId.get() === id && list.length) {
+      this.#activeId.set(list[list.length - 1].id);
+    }
+  }
+
+  activate(id) { this.#activeId.set(id); }
+}
+
+export const tabsService = new TabsService();
+```
+
+## 三、tabs-container(渲染 + KeepAlive 容器)
+
+```js
+// components/tabs-container.js
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { tabsService } from '../services/tabs-service.js';
+
+export class TabsContainer extends SignalWatcher(LitElement) {
+  static styles = css`
+    .pane { display: none; }
+    .pane.active { display: block; }
+  `;
+
+  render() {
+    const tabs = tabsService.tabs.get();
+    const activeId = tabsService.activeId.get();
+    return html`
+      <!-- 所有已打开标签页的容器常驻;仅激活的显示 -->
+      ${tabs.map((t) => html`
+        <div class="pane ${t.id === activeId ? 'active' : ''}" data-tab="${t.id}">
+          <page-router-view .route=${t.path} .active=${t.id === activeId}></page-router-view>
+        </div>
+      `)}
+    `;
+  }
+}
+```
+
+要点:
+- 用 `display:none/block` 控制显隐,**不要**用条件渲染(`? :` 会销毁实例)。
+- 每个 pane 内容由 `route` 决定渲染哪个页面组件。
+- 页面组件可在 `active` 变化时决定是否暂停轮询 / 动画(省资源)。
+
+## 四、标签栏 UI(tabs-bar)
+
+```js
+// components/tabs-bar.js
+export class TabsBar extends SignalWatcher(LitElement) {
+  render() {
+    return html`
+      <nav>
+        ${tabsService.tabs.get().map((t) => html`
+          <a class=${t.id === tabsService.activeId.get() ? 'on' : ''}
+             @click=${() => tabsService.activate(t.id)}>
+            ${t.title}
+            ${t.closable ? html`<button @click=${(e) => { e.stopPropagation(); tabsService.close(t.id); }}>×</button>` : ''}
+          </a>
+        `)}
+      </nav>
+    `;
+  }
+}
+```
+
+## 五、与路由联动
+
+路由命中新路径时打开/激活对应标签:
+
+```js
+// 在 route change handler
+tabsService.open({
+  id: path,            // 用 path 做唯一 id(也可带参数合成)
+  path,
+  title: routeTitle(path),
+});
+```
+
+- 同 path → 仅激活(不重复开)。
+- 带参数的路由(`/users/:id`)用「实际路径」做 id,`/users/1` 与 `/users/2` 是两个标签。
+
+## 六、KeepAlive 的「刷新」语义
+
+- **刷新单个标签**:给 pane 换一个 `key`(强制重建该子组件)——用 Lit 的 `repeat()` + 改变 key,或临时移除再插回。
+- **关闭标签**:从 `tabs` 移除 → 容器消失 → 实例被 GC,状态释放。
+- **整页刷新**:浏览器级 `location.reload()`,所有标签重置。
+
+## 七、性能与注意
+
+- 标签数量多时,非激活 pane 里的定时器 / 动画应暂停(监听 `active` 属性)。
+- 大列表页面全部常驻会占内存 —— 对「重量级页面」可选用「关闭即销毁 + 重进重建」策略,或对非激活 pane 做懒初始化(首次激活才渲染内容)。
+- 事件在非激活 pane 里仍会冒泡(DOM 存在)——注意别让隐藏 pane 的全局监听重复响应;用 `active` 守卫。

+ 288 - 0
skills/fmode-code-lit/references/rules/web-component-library.md

@@ -0,0 +1,288 @@
+# Web Component 组件库规范(wc:Lit 实现的浏览器交互层)
+
+> 组件库 = 一批**用 `customElements.define()` 注册的自定义元素**,内部封装 **DOM / 交互 / 样式**,
+> 逻辑**全部来自核心库 [core](./core-library.md)**。产物是标准 Web Component ——
+> 在 Lit、Vue、React、Angular、原生 HTML 里都能**直接写 `<my-x>`**,无需跑任何前端框架。
+>
+> 与页面内组件([components.md](./components.md))的区别:页面组件服务于「一个应用」;
+> **组件库组件服务于「多个应用 / 多个框架」**,因此契约更严、实现更克制。
+
+## 零、wc 必须用 Lit 框架特性开发(最重要的一条)
+
+> ⚠️ **本章所有 wc 组件必须用 Lit 框架特性(`LitElement` / `html` / `css` / Shadow DOM / `static properties` /
+> Lit 响应式)开发。禁止手写原生 Web Component。**
+>
+> - **Lit 本身就是 Web Component 的实现层**:`LitElement` 继承自 `ReactiveElement`(再继承 `HTMLElement`),
+>   `html` 产出 Shadow DOM 里的模板,`customElements.define()` 注册的是标准自定义元素。
+>   所以「用 Lit 写」与「产出标准 Web Component、跨框架可用」**不矛盾** —— 前者是实现方式,后者是产物形态。
+> - **✅ 写法**:`class X extends LitElement` + `static properties` + `static styles = css\`\`` + `render() { return html\`\`\` }`。
+> - **❌ 禁止的原生写法**(等价重造 Lit,且丢掉 Lit 的模板缓存 / 属性转换 / 响应式更新):
+>   - `class X extends HTMLElement`
+>   - `this.attachShadow({ mode: 'open' })` + 手动 `innerHTML` / `document.createElement`
+>   - 手动实现 `observedAttributes` / `attributeChangedCallback` / `connectedCallback` 里刷 DOM
+>   - 字符串拼接模板 + `this.shadowRoot.querySelector(...).textContent = ...`
+> - **「原生」只出现在宿主侧**:`plain.html` 里 `<script type="module">` 直接挂 `<quote-calc>` 是**验证跨框架**的宿主写法,组件本身仍是 Lit。宿主用什么框架,与组件用不用 Lit 无关。
+> - **例外(仅限非 UI 件)**:`browser-transport.js` 这类**不渲染 DOM 的 I/O 适配器**不是组件,可以不 import lit;但只要文件里 `customElements.define()` 注册了元素,就必须是 `LitElement`。
+>
+> `scripts/check.mjs` 对 `wc/` 下每个「注册了自定义元素」的文件做硬校验:必须 import lit、必须 `extends LitElement`、不得出现 `extends HTMLElement` / `attachShadow(` / `observedAttributes`。
+
+## 一、定位与边界
+
+| 项 | 组件库(wc) | 页面内组件(page) |
+|----|--------------|--------------------|
+| 消费方 | 多项目、多框架,非本项目也能用 | 仅本应用 |
+| 契约 | 语义属性 / DOM 事件 / slot / CSS 自定义属性,**稳定** | 可随应用演进 |
+| 业务逻辑 | ❌ 一律来自 core,组件只做**接线** | 可在页面 Service 中 |
+| 样式 | Shadow DOM 强隔离,主题走 CSS 变量 | 可宽松 |
+| 依赖 | `core` + `lit`(仅 `LitElement`/`html`/`css`;**必须用 Lit,禁原生 wc 手写**) | `core` + 页面 `services/` |
+
+```
+fmode-code-agent(core,纯 ESM)
+        ▲ 只 import 纯逻辑
+  <designer-chat>(wc,LitElement,仅 DOM/交互/样式)
+        ▲ 任意框架直接 <designer-chat>
+  页面(page:Lit / Vue / React / 原生 HTML …)
+```
+
+## 二、铁律(违反即返工)
+
+0. **用 Lit 写,不手写原生 wc**:组件类一律 `extends LitElement`,模板用 `html`、样式用 `css`(见第〇节)。禁止 `extends HTMLElement` / 手写 `attachShadow` / 手写 `observedAttributes`。
+1. **无装饰器**:与 [components.md 第〇节](./components.md) 一致,`static properties` + `customElements.define()`。组件库要让**任何无构建页面**直接跑,装饰器会让源码 `SyntaxError`。注意:无装饰器 ≠ 原生 wc —— 仍然用 Lit 的静态字段 API。
+2. **逻辑不进组件**:状态机 / 校验 / 协议都在 core;组件**只**做「DOM 事件 → 调 core → 用结果重渲染」。组件里能单测的逻辑,都应已在 core 里。
+3. **不 import 业务**:组件库不 import 任何应用的 `services/`、`routes/`、业务 model;外部世界只通过**属性**和**事件**进入。
+4. **不直接 `fetch`**:网络由**宿主注入**(见第五节)或由 core 的协议函数描述、宿主执行。
+5. **样式全在 Shadow DOM**:不写全局 `document.head` 样式;主题只通过 **CSS 自定义属性** + `::part()` 暴露。
+6. **标签命名空间**:组件库标签带统一前缀(如 `<fm-chat>`、`<fm-icon>`),**避免与其他库撞名**(撞名 → `define` 抛错)。
+7. **注册幂等**:注册前 `customElements.get()` 判空 —— 允许同一页面被加载两次(多入口 / 宿主重复 import)。
+
+## 三、组件骨架(无装饰器、依赖 core)
+
+```js
+// src/chat/designer-chat.js
+import { LitElement, html, css } from 'lit';
+import { AgentMachine } from 'fmode-code-agent';          // ← 核心库(core),纯 ESM
+
+export class DesignerChat extends LitElement {
+  /** 对外契约:语义属性 */
+  static properties = {
+    prompt: { type: String },                // 宿主可传 / attribute 可设
+    disabled: { type: Boolean, reflect: true },
+    _state: { state: true },                 // 内部:core 的状态映射
+    _messages: { state: true },              // 内部:渲染用
+  };
+
+  static styles = css`
+    :host { display: block; --dc-accent: #2b6cb0; }   /* 主题变量,可被外部覆盖 */
+    button { background: var(--dc-accent); }
+    .log { max-height: 240px; overflow: auto; }
+  `;
+
+  #machine = null;
+  #unsubscribe = null;
+
+  constructor() {
+    super();
+    this.prompt = '';
+    this.disabled = false;
+    this._state = 'idle';
+    this._messages = [];
+  }
+
+  /** 宿主注入 I/O(transport),组件层不写死 fetch */
+  set transport(t) { this.#machine = new AgentMachine({ transport: t }); }
+
+  connectedCallback() {
+    super.connectedCallback();
+    if (!this.#machine) {
+      // 未注入 transport 时给一个 fallback,避免 use 时崩
+      this.#machine = new AgentMachine({ transport: { call: async ({ text }) => ({ text: `(echo) ${text}` }) } });
+    }
+    this.#unsubscribe = this.#machine.subscribe((s) => { this._state = s; });
+    this._state = this.#machine.state;
+  }
+
+  disconnectedCallback() {
+    this.#unsubscribe?.();
+    this.#unsubscribe = null;
+    super.disconnectedCallback();
+  }
+
+  render() {
+    return html`
+      <form @submit=${this.#onSubmit}>
+        <input .value=${this.prompt} @input=${(e) => { this.prompt = e.target.value; }}
+               ?disabled=${this.disabled || this._state !== 'idle'} placeholder="描述你想要的图片…" />
+        <button ?disabled=${this.disabled || this._state !== 'idle'}>${this._state === 'idle' ? '生成' : '生成中…'}</button>
+      </form>
+      <div class="log" part="log">
+        ${this._messages.map((m) => html`<p>${m}</p>`)}
+      </div>
+    `;
+  }
+
+  async #onSubmit(e) {
+    e.preventDefault();
+    const text = this.prompt.trim();
+    if (!text) return;
+    this._messages = [...this._messages, `> ${text}`];
+    /** 子 → 父:语义事件,composed 穿透 Shadow DOM */
+    this.dispatchEvent(new CustomEvent('dc-submit', { detail: { prompt: text }, bubbles: true, composed: true }));
+    try {
+      const result = await this.#machine.send(text);
+      this._messages = [...this._messages, result?.text ?? ''];
+      this.dispatchEvent(new CustomEvent('dc-done', { detail: { result }, bubbles: true, composed: true }));
+    } catch (err) {
+      this.dispatchEvent(new CustomEvent('dc-error', { detail: { code: err.code, message: err.message }, bubbles: true, composed: true }));
+    }
+  }
+}
+
+if (!customElements.get('designer-chat')) customElements.define('designer-chat', DesignerChat);
+```
+
+要点:
+- **对外只有 `prompt` / `disabled` / `transport` / `dc-*` 事件**;`_state` / `_messages` 是内部渲染状态。
+- `static styles` 全在 Shadow DOM;外部换主题只改 `--dc-accent`、`::part(log)`。
+- 状态来自 core(`machine.subscribe`),组件不自造状态机。
+- **用私有字段 `#` 存机器实例**,不挂在公开属性上。
+
+## 四、对外契约(跨框架就是靠这三样)
+
+| 手段 | 用法 | 规则 |
+|------|------|------|
+| **属性(attribute)** | `<designer-chat disabled>` | 只用**字符串可表达**的语义(`disabled`、`placeholder`、`size="lg"`);布尔用 `?attr` 或 presence |
+| **属性(property)** | 框架里 `el.prompt = '…'`、Lit 里 `.prompt=${x}` | 复杂对象(回调、配置)走 property;**Lit/原生里要用 `.prop=` 绑定**,别指望 attribute 传对象 |
+| **事件** | `el.addEventListener('dc-done', fn)` | `kebab-case` 事件名;`detail` 为对象;**`bubbles: true, composed: true`**(穿透 Shadow DOM);组件库事件统一前缀避免撞车 |
+| **Slot** | 宿主投影内容 | `<slot name="header">` / `<slot>`;**不**用 `.innerHTML` |
+| **CSS 自定义属性** | `--dc-accent: red` 作用在宿主或其祖先 | 主题的唯一入口 |
+| **`::part()`** | `designer-chat::part(log) { ... }` | 需要暴露的内部结构加 `part="…"` |
+| **方法(可选)** | `el.focus()`、`el.reset()` | 只给「命令式交互」,不暴露内部状态改写入 |
+
+- **属性即契约**:改属性名 / 事件名 = **破坏性变更** → 升 major(同 core 的版本纪律)。
+- **不要暴露**内部类名 / 内部 DOM id(`::part` 是唯一对外样式钩子)。
+- **不要用全局事件总线**在组件库内部通信(同 [components.md 第四节](./components.md))。
+
+## 五、依赖注入:core 实例与 I/O 由宿主注入
+
+组件库**不知道**宿主的网络、登录、路由、存储。两种注入方式:
+
+**A. 宿主直接传实例(最通用,跨框架 OK)**
+
+```html
+<script type="module">
+  import { AgentMachine } from 'fmode-code-agent';
+  const el = document.querySelector('designer-chat');
+  el.transport = {
+    call: async ({ text }) => {
+      const res = await fetch('/api/agent', { method: 'POST', body: JSON.stringify({ text }) });
+      return res.json();
+    },
+  };
+</script>
+```
+
+**B. Lit 页面里用 Context(仅 Lit 宿主)** —— 见 [context-di.md](./context-di.md)。组件库内部用 `ContextConsumer` 兜底读注入的实例,读不到时用属性。
+
+> 原则:**组件库只定义 shape,不拥有实例**。这也让组件在 Node / SSR 里并不被实例化,逻辑仍留在 core。
+
+## 六、跨框架使用方式
+
+组件的**唯一运行要求**:浏览器能执行 ESM + `customElements`。下图是同一份 `dist` 在四个宿主里的用法:
+
+```html
+<!-- ① 原生 HTML(无任何框架) -->
+<designer-chat></designer-chat>
+<script type="module" src="/npm/designer-chat/v1.0.0/index.js"></script>
+```
+
+```html
+<!-- ② Lit 页面(本仓页面层):先 import 触发注册,再在模板里使用 + 监听事件 -->
+<script type="module">import './components.js';</script>
+<designer-chat .prompt=${q} @dc-done=${(e) => console.log(e.detail)}></designer-chat>
+```
+
+```jsx
+// ③ React 19:自定义元素已原生支持(prop 走 attribute/property 自动映射)
+<designer-chat ref={ref} prompt={q} onDcDone={onDone}></designer-chat>
+// ④ React 18-:需包装层(ref + addEventListener),或复用 @lit/react 的 createComponent
+```
+
+```vue
+<!-- ④ Vue 3:.vue 里把标签加进 compilerOptions.isCustomElement,或 app.config.compilerOptions.isCustomElement -->
+<designer-chat :prompt="q" @dc-done="onDone" />
+<!-- Vue 3 对自定义元素:attribute 透传;复杂属性建议 ref + el.transport = ... -->
+```
+
+| 宿主 | 属性绑定 | 事件 | 注意事项 |
+|------|----------|------|----------|
+| 原生 / Lit | `.prop=`(Lit) / `el.prop =` | `addEventListener` / `@event` | Lit 必须 **`.` 前缀**绑定复杂值 |
+| Vue 3 | `:prop`(复杂值透传为 property) | `@kebab-event` | 配置 `isCustomElement`(构建型 Vue) |
+| React 19+ | `prop={...}`(已映射到 property) | `onEvent={...}`(`dc-done` → `onDcDone`) | 事件名注意大小写映射 |
+| React 18- | 需 wrapper(`createComponent`) | ref + `addEventListener` | 低版本 React 会把未知 prop 当 attribute |
+
+> **验收口诀**:组件库必须在**一个非 Lit 宿主**(纯 HTML 或 Vue)里也能跑,否则就不是跨框架组件库。
+
+## 七、样式隔离与主题
+
+- **Shadow DOM 是默认**:组件内部样式不泄漏、外部项目样式也不进来。
+- **主题三件套**(按优先级):
+  1. **CSS 自定义属性**:`:host { --dc-accent: #2b6cb0 }`,外部可在祖先或宿主上覆盖;
+  2. **`::part()`**:`part="log"` 暴露需要精细调整的节点;
+  3. **`slots`**:允许宿主替换整块内容(如标题、空状态)。
+- **禁止**:`document.adoptedStyleSheets` 注入全局样式;`!important` 向外压制;依赖宿主 CSS 类名。
+- 设计令牌(颜色/间距/字号)建议作为**包的静态 CSS** 发布一份(如 `theme.css`),宿主 `<link>` 引入即可,变量名统一前缀(`--fm-`)。
+
+## 八、无构建分发
+
+组件库与 core 一样:**无构建**、源码即产物,托管到 **FMODE Repose**,用 importmap 引入。
+
+```
+/npm/designer-chat/v1.0.0/
+  index.js                # 入口:import 所有组件并注册(或单组件入口)
+  designer-chat.js        # 单组件
+  theme.css               # 可选:设计令牌
+```
+
+```json
+{ "imports": {
+  "designer-chat": "/npm/designer-chat/v1.0.0/index.js",
+  "fmode-code-agent": "/npm/fmode-code-agent/v2.0.0/src/index.js"
+} }
+```
+
+- `index.js` **只做 import + 注册**:副作用仅「注册自定义元素」,无渲染、无请求。
+- **lit 依赖不内置**:组件库 import 裸 `lit`,由宿主的**公共 importmap** 解析 → 全局单实例 lit(否则重复注册 / 多版本冲突,见 [pitfalls.md 第三节](./pitfalls.md))。
+- **core 依赖版本化**:组件库在 importmap 里映射自己需要的 core 版本;同一页面若用了两个 core major,映射两个标识符(见 [core-library.md 第六节](./core-library.md))。
+- 发布 checklist 中增加:**在一个非 Lit 宿主里 Smoke**(见第九节)。
+
+## 九、验收 & Test
+
+| 层 | 怎么做 |
+|----|--------|
+| core 逻辑 | `node --test`(不依赖 DOM,见 [core-library.md 第四节](./core-library.md)) |
+| 组件契约 | 浏览器里断言:`customElements.get('x-y')` 存在;改属性 → 渲染变化;点按钮 → 事件带 `detail` 且 `composed` |
+| 跨框架 | 同一页面用**纯 HTML** 与 **Lit** 各挂一次;两个入口渲染结果一致 |
+| 样式隔离 | 宿主页全局 `p { color: red }` **不影响**组件内部;`--dc-accent` 能改主题 |
+| 幂等 | 同名脚本 import 两次不报 `define` 冲突 |
+| 无装饰器 | `node --check` 全过;`grep` 无 `@property` 等 |
+| **Lit 实现** | `grep` 注册元素的文件均 `import ... from 'lit'` 且 `extends LitElement`;无 `extends HTMLElement` / `attachShadow(` / `observedAttributes` |
+
+## 十、红线清单
+
+- ❌ 组件里写业务逻辑 / 请求 / 协议解析 → 下沉 core,由宿主注入 I/O
+- ❌ 依赖宿主的 CSS 类名 / 全局样式 / 全局 window 变量
+- ❌ 暴露内部状态写入方法(`el.setMessages()`),只暴露语义属性 + 语义事件
+- ❌ 事件漏 `composed: true` → 穿过 Shadow DOM 后宿主收不到
+- ❌ 无前缀标签名(`<chat>` 与别的库撞车)
+- ❌ `customElements.define` 不判重 → 二次加载 `NotSupportedError`
+- ❌ 组件库内置 lit 副本 → 宿主多实例 lit,见 [pitfalls.md](./pitfalls.md)
+- ❌ 装饰器
+- ❌ 手写原生 wc(`extends HTMLElement` / `attachShadow()` / `observedAttributes` / 字符串模板刷 `innerHTML`)→ 必须 `extends LitElement` + `html` + `css`,见第〇节
+
+## 相关
+
+- [core-library.md](./core-library.md) —— 组件库依赖的逻辑层
+- [components.md](./components.md) —— 页面内组件;组件库是它的「契约加严版」
+- [architecture.md](./architecture.md) —— 三分层总图
+- [context-di.md](./context-di.md) —— Lit 宿主中的注入
+- [deployment.md](./deployment.md) / [pitfalls.md](./pitfalls.md) —— 分发与多版本冲突

+ 76 - 0
skills/fmode-code-lit/scripts/check-elements.mjs

@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+/**
+ * check-elements.mjs —— 校验 src/ 下所有自定义元素都显式声明了 display
+ *
+ * 背景:light DOM / Shadow DOM 下自定义元素都是「未知元素」,浏览器默认按 inline 处理,
+ * 不吃 height/width、不形成块级盒子,常见症状是 h-full / flex-1 全失效、高度链断裂。
+ * 规则见 SKILL.md「多页面开发铁律 · 死线4」。
+ *
+ * 用法:
+ *   node scripts/check-elements.mjs [srcDir]      # 默认 ./src
+ *
+ * 判定:
+ *   1. 收集源码中 customElements.define('tag', ...) 注册的全部标签;
+ *   2. 在 .css 文件与内联 <style> 中查找该标签的选择器块;
+ *   3. 选择器块含 display 声明 → 通过;写 `display: skip` 标记为例外(如自带 flex 布局的元素);
+ *   4. 未声明 → 报错,退出码 1。
+ */
+
+import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
+import { join, extname } from 'node:path';
+
+const SRC = process.argv[2] || 'src';
+if (!existsSync(SRC)) {
+  console.error(`✗ 目录不存在:${SRC}`);
+  process.exit(2);
+}
+
+/** 递归收集文件 */
+function walk(dir, out = []) {
+  for (const name of readdirSync(dir)) {
+    const p = join(dir, name);
+    const st = statSync(p);
+    if (st.isDirectory()) walk(p, out);
+    else if (/\.(js|mjs|html|css|jsx|tsx?)$/.test(name)) out.push(p);
+  }
+  return out;
+}
+
+const files = walk(SRC);
+const defined = new Set();       // customElements.define 注册的标签
+let cssText = '';
+
+for (const f of files) {
+  const text = readFileSync(f, 'utf8');
+  for (const m of text.matchAll(/customElements\.define\(\s*['"]([\w-]+)['"]/g)) defined.add(m[1]);
+  if (extname(f) === '.css') cssText += '\n' + text;
+  // 内联 <style> 块
+  for (const m of text.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) cssText += '\n' + m[1];
+}
+
+/** 在 cssText 中查找某个标签的样式块(tag { ... } 或 tag[attr] { ... }) */
+function findRules(tag) {
+  const re = new RegExp(`(^|[\\s,{}>])${tag}\\s*\\{([^}]*)\\}`, 'gm');
+  const rules = [];
+  for (const m of cssText.matchAll(re)) rules.push(m[2]);
+  return rules;
+}
+
+const RED = '\x1b[31m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', DIM = '\x1b[2m', RESET = '\x1b[0m';
+let bad = 0, skipped = 0, ok = 0;
+
+// 只检查「已注册的自定义元素」—— 未知/拼错的标签不归本检查管
+const tags = [...defined];
+
+for (const tag of tags.sort()) {
+  const rules = findRules(tag);
+  const declared = rules.some((r) => /display\s*:/.test(r));
+  const exempted = rules.some((r) => /display\s*:\s*skip/.test(r));
+  if (exempted) { skipped++; console.log(`${YELLOW}-${RESET} ${tag} ${DIM}(例外声明 display: skip)${RESET}`); }
+  else if (declared) ok++;
+  else { bad++; console.log(`${RED}✗${RESET} ${tag} 未声明 display  —  add: ${tag} { display: block; min-width: 0; }`); }
+}
+
+console.log(`\n自定义元素 ${tags.length} 个:${GREEN}已声明 ${ok}${RESET},${YELLOW}例外 ${skipped}${RESET},${bad ? RED + '未声明 ' + bad : '未声明 0'}${RESET}`);
+console.log(bad ? `${RED}CHECK FAILED${RESET}` : `${DIM}回滚显示/高度链相关白屏时,先看这些元素是否 inline。${RESET}`);
+process.exit(bad ? 1 : 0);

+ 72 - 0
skills/fmode-code-lit/scripts/install-rules.mjs

@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+/**
+ * install-rules.mjs —— 把 fmode-code-lit 的前端规则装入目标项目的 ./.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, copyFileSync } 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.startsWith('--') && a !== undefined && !a.startsWith('-')) || '.');
+const force = args.includes('--force');
+const fromIdx = args.indexOf('--from');
+const fromDir = fromIdx > -1 ? resolve(args[fromIdx + 1]) : null;
+
+// 规则来源候选
+const candidates = [
+  fromDir,
+  resolve(__dirname, '../../../.claude/rules'), // 技能在 <repo>/skills/fmode-code-lit/scripts/
+  resolve(__dirname, '../../.claude/rules'),
+].filter(Boolean);
+const srcRules = candidates.find((d) => d && existsSync(d) && existsSync(join(d, 'architecture.md')));
+
+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);
+    const st = statSync(s);
+    if (st.isDirectory()) { mkdirSync(d, { recursive: true }); copyTree(s, d); continue; }
+    if (!name.endsWith('.md')) continue;
+    if (existsSync(d) && !force) { skipped++; console.log(`  skip  ${d}`); continue; }
+    copyFileSync(s, d); created++; console.log(`  write ${d}`);
+  }
+}
+
+if (srcRules) {
+  console.log(`\n规则来源:${srcRules}`);
+  copyTree(srcRules, destRules);
+} else {
+  console.log('\n未找到规则仓,写入内置最小规则集。');
+  const minimal = {
+    'lit-no-build.md': `# Lit 无构建前端 · 最小规则\n\n1. 源码即产物:不引入任何打包器。\n2. 依赖由 importmap 指向 FMODE Repose,禁止拷进项目。\n3. **禁用装饰器**(@customElement/@property/@state/@consume)——用 static properties + customElements.define + ContextConsumer。\n4. 分层:组件(渲染)→服务(业务/状态)→工具(纯函数);组件禁 fetch,服务禁 DOM。\n5. 相对导入必须带 .js 后缀。\n6. 全局状态用 @lit-labs/signals;组件本地状态用 static properties(state:true)。\n7. 子目录部署必须 <base href=\"/<project>/\">,且与 OBS 前缀、importmap 前缀三者同源。\n8. 导航一律走 utils/router.js(把应用内路径拼到 base 的 pathname 上);禁止 new URL(path, baseURI),子路径部署会丢前缀。\n9. 子路径部署禁用 @lit-labs/router(全局 <a> 拦截 + 未剥前缀 pathname 会污染路由)。\n10. CDN History 回退:不存在路径 200 返回 index.html,禁 302。\n`,
+  };
+  for (const [name, content] of Object.entries(minimal)) {
+    const d = join(destRules, name);
+    if (existsSync(d) && !force) { skipped++; continue; }
+    writeFileSync(d, content); created++; console.log(`  write ${d}`);
+  }
+}
+
+// CLAUDE.md 片段(追加式,不覆盖已有正文)
+const claudeMd = join(target, 'CLAUDE.md');
+const snippet = `\n## 前端(Lit 原生 ESM 无构建)\n\n本项目前端遵循 \`.claude/rules/\` 下的 Lit 无构建规则:源码即产物、依赖走 importmap、\n分层(组件/服务/工具)、**禁用装饰器**、@lit-labs/signals 响应式、自写 utils/router.js History 路由(子路径部署禁用 @lit-labs/router)。\n详见 \`.claude/rules/architecture.md\` 与 \`.claude/rules/components.md\`。\n`;
+if (existsSync(claudeMd)) {
+  const cur = readFileSync(claudeMd, 'utf8');
+  if (!cur.includes('Lit 原生 ESM 无构建')) { writeFileSync(claudeMd, cur + snippet); console.log(`  append ${claudeMd}`); }
+  else console.log(`  skip  ${claudeMd}(已含片段)`);
+} else {
+  writeFileSync(claudeMd, `# 项目规则\n${snippet}`); created++; console.log(`  write ${claudeMd}`);
+}
+
+console.log(`\n✓ 完成:新建 ${created},跳过 ${skipped}。规则目录:${destRules}\n`);

+ 57 - 0
skills/fmode-code-lit/scripts/install.mjs

@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+/**
+ * install.mjs —— 把 fmode-code-lit 技能安装到 Claude Code 技能目录。
+ *
+ * 用法:
+ *   node install.mjs install     # → ~/.claude/skills/fmode-code-lit/(全局,默认)
+ *   node install.mjs workspace   # → ./.claude/skills/fmode-code-lit/(当前项目)
+ *   node install.mjs install --target <dir> [--force]
+ *   node install.mjs check       # 校验安装
+ */
+import { mkdirSync, copyFileSync, existsSync, readdirSync, statSync } from 'node:fs';
+import { join, resolve, dirname } from 'node:path';
+import { homedir } from 'node:os';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const SKILL_SRC = resolve(__dirname, '..');           // <...>/skills/fmode-code-lit
+const SKILL_NAME = 'fmode-code-lit';
+
+const args = process.argv.slice(2);
+const cmd = args[0] && !args[0].startsWith('--') ? args[0] : 'install';
+const force = args.includes('--force');
+const tIdx = args.indexOf('--target');
+const globalTarget = join(homedir(), '.claude', 'skills', SKILL_NAME);
+const workspaceTarget = join(process.cwd(), '.claude', 'skills', SKILL_NAME);
+
+function copyTree(from, to) {
+  mkdirSync(to, { recursive: true });
+  for (const name of readdirSync(from)) {
+    if (name === 'node_modules') continue;
+    const s = join(from, name), d = join(to, name);
+    if (statSync(s).isDirectory()) copyTree(s, d);
+    else if (!existsSync(d) || force) copyFileSync(s, d);
+  }
+}
+
+function verify(target) {
+  const must = ['SKILL.md'];
+  const missing = must.filter((f) => !existsSync(join(target, f)));
+  if (missing.length) { console.error(`✗ 安装不完整,缺少:${missing.join(', ')}`); process.exit(1); }
+  console.log(`✓ 技能已就位:${target}`);
+  console.log(`  SKILL.md 存在,scripts/ ${existsSync(join(target, 'scripts')) ? '存在' : '缺失'}`);
+}
+
+function main() {
+  const target = cmd === 'workspace' || cmd === 'install-workspace'
+    ? workspaceTarget
+    : (tIdx > -1 ? resolve(args[tIdx + 1]) : globalTarget);
+  if (cmd === 'check') { verify(target); return; }
+  if (!existsSync(SKILL_SRC)) { console.error(`✗ 找不到技能源:${SKILL_SRC}`); process.exit(1); }
+  console.log(`安装 ${SKILL_NAME} → ${target}${force ? '(--force 覆盖)' : ''}`);
+  copyTree(SKILL_SRC, target);
+  verify(target);
+  console.log(`\n触发词:lit 项目 / lit 组件 / 无构建前端 / WebComponent / importmap`);
+}
+
+main();

+ 221 - 0
skills/fmode-code-lit/scripts/new.mjs

@@ -0,0 +1,221 @@
+#!/usr/bin/env node
+/**
+ * new.mjs —— 生成单个 Lit 无构建单元(组件 / 服务 / 页面 / 核心库 / 组件库)。
+ *
+ * 用法:
+ *   node new.mjs component user-card         [--dir src/components]
+ *   node new.mjs service   auth-service      [--dir src/services]
+ *   node new.mjs page      page-order-list   [--dir src/routes]
+ *   node new.mjs core      pricing           [--dir lib/core]   # 纯 ESM 逻辑模块(零 DOM)
+ *   node new.mjs wc        quote-calc        [--dir lib/wc]     # Web Component(跨框架组件库)
+ *
+ * 生成物一律无装饰器,遵循规则仓命名与分层规范。
+ */
+import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
+import { join, resolve } from 'node:path';
+
+const args = process.argv.slice(2);
+const kind = args[0];
+const name = args[1];
+const dirIdx = args.indexOf('--dir');
+const force = args.includes('--force');
+
+const KINDS = {
+  component: { dir: 'src/components', file: (n) => n + '.js' },
+  service: { dir: 'src/services', file: (n) => n + '.js' },   // 文件名已含 -service 后缀
+  page: { dir: 'src/routes', file: (n) => n + '.js' },
+  core: { dir: 'lib/core', file: (n) => n + '.js' },          // 纯 ESM 核心库(零浏览器依赖)
+  wc: { dir: 'lib/wc', file: (n) => n + '.js' },              // Web Component 组件库
+};
+
+if (!kind || !KINDS[kind] || !name) {
+  console.error(`用法:node new.mjs <component|service|page|core|wc> <name> [--dir <dir>] [--force]`);
+  process.exit(2);
+}
+if (!/^[a-z][a-z0-9-]*$/.test(name)) {
+  console.error(`✗ 名称须为小写 kebab-case:${name}`);
+  process.exit(2);
+}
+
+const spec = KINDS[kind];
+const dir = dirIdx > -1 ? resolve(args[dirIdx + 1]) : resolve(spec.dir);
+// service:允许传 order 或 order-service,统一补一个 -service 后缀(避免 order-service-service)
+const baseName = kind === 'service' ? name.replace(/-service$/, '') : name;
+const fileBase = kind === 'service' ? baseName + '-service' : name;
+const pascal = fileBase.split('-').map((s) => s[0].toUpperCase() + s.slice(1)).join('');
+// 标签必须含连字符:page- 前缀已保证;component 若单段则补前缀
+const tag = (kind === 'service' || kind === 'core') ? null : (fileBase.includes('-') ? fileBase : 'x-' + fileBase);
+const path = join(dir, spec.file(fileBase));
+const relToUtils = kind === 'page' ? '../../utils' : '../utils';
+
+if (existsSync(path) && !force) {
+  console.error(`✗ 已存在:${path}(用 --force 覆盖)`);
+  process.exit(1);
+}
+mkdirSync(dir, { recursive: true });
+
+let content = '';
+if (kind === 'component') {
+  content = `/**
+ * ${name} —— 通用组件。
+ * 无装饰器:static properties + customElements.define。
+ */
+import { LitElement, html, css } from 'lit';
+
+export class ${pascal} extends LitElement {
+  static properties = {
+    label: { type: String },
+  };
+
+  static styles = css\`
+    :host { display: block; }
+  \`;
+
+  constructor() { super(); this.label = ''; }
+
+  render() {
+    return html\`<div>\${this.label}</div>\`;
+  }
+}
+
+customElements.define('${tag}', ${pascal});
+`;
+} else if (kind === 'service') {
+  const endpoint = baseName;
+  const singleton = pascal.charAt(0).toLowerCase() + pascal.slice(1);
+  content = `/**
+ * ${pascal} —— 业务服务(Signal + 单例)。无 DOM 依赖,可单测。
+ */
+import { signal, computed } from '@lit-labs/signals';
+// import { http } from '${relToUtils}/http.js';
+
+class ${pascal} {
+  #items = signal([]);
+  get items() { return this.#items; }
+  get count() { return computed(() => this.#items.get().length); }
+
+  async fetchAll() {
+    // const data = await http.get('/${endpoint}');
+    // this.#items.set(data.items ?? []);
+    return this.#items.get();
+  }
+
+  set(items) { this.#items.set(items); }
+}
+
+export const ${singleton} = new ${pascal}();
+`;
+} else if (kind === 'core') {
+  content = `/**
+ * ${pascal} —— 核心库模块(core):纯 ESM、零浏览器依赖。
+ *
+ * 铁律:不碰 window/document/localStorage/fetch,不 import lit。
+ * 不确定输入(时间/id/网络)一律从参数注入 → Node 可确定化单测。
+ * 规则:见 .claude/rules/core-library.md
+ */
+
+/**
+ * @param {any[]} items
+ * @param {{ now?: () => number, id?: () => string }} [opts]
+ */
+export function ${pascal.charAt(0).toLowerCase() + pascal.slice(1)}(items, opts = {}) {
+  const { now = () => Date.now(), id = () => Math.random().toString(36).slice(2, 10) } = opts;
+  // TODO: 纯逻辑(校验 / 计算 / 状态机),全部由入参决定
+  return { id: id(), at: now(), count: Array.isArray(items) ? items.length : 0 };
+}
+`;
+} else if (kind === 'wc') {
+  content = `/**
+ * ${pascal} —— 组件库(Web Component):仅浏览器交互,逻辑来自 core。
+ *
+ * ⚠️ 必须用 Lit 框架特性实现(extends LitElement + html + css),禁止手写原生 wc
+ *    (extends HTMLElement / attachShadow() / 手写 observedAttributes)。
+ *    Lit 本身基于 Web Component 标准,产物仍是跨框架可用的自定义元素。
+ *
+ * 契约:语义属性 + composed 事件 + slot + CSS 变量 + ::part
+ * 无装饰器:static properties + customElements.define(判重)
+ * 规则:见 .claude/rules/web-component-library.md
+ */
+import { LitElement, html, css } from 'lit';
+
+export class ${pascal} extends LitElement {
+  static properties = {
+    label: { type: String },
+    value: { type: Object },
+    disabled: { type: Boolean, reflect: true },
+    _state: { state: true },
+  };
+
+  static styles = css\`
+    :host { display: block; --x-accent: #2b6cb0; }
+    button { background: var(--x-accent); color: #fff; border: 0; border-radius: 6px; padding: 6px 12px; }
+  \`;
+
+  /** 宿主注入 I/O(组件库不自己 fetch) */
+  #transport = null;
+  set transport(t) { this.#transport = t; }
+  get transport() { return this.#transport; }
+
+  constructor() {
+    super();
+    this.label = '';
+    this.value = null;
+    this.disabled = false;
+    this._state = 'idle';
+  }
+
+  render() {
+    return html\`
+      <button part="button" ?disabled=\${this.disabled || this._state !== 'idle'}
+              @click=\${this.#onClick}>
+        <slot name="label">\${this.label}</slot>
+      </button>
+    \`;
+  }
+
+  async #onClick() {
+    this._state = 'busy';
+    try {
+      // 逻辑应由 core 提供;这里仅演示接线
+      const result = this.#transport ? await this.#transport.call(this.value) : this.value;
+      this.dispatchEvent(new CustomEvent('${tag}-done', {
+        detail: { result }, bubbles: true, composed: true,
+      }));
+    } catch (err) {
+      this.dispatchEvent(new CustomEvent('${tag}-error', {
+        detail: { code: err.code, message: err.message }, bubbles: true, composed: true,
+      }));
+    } finally {
+      this._state = 'idle';
+    }
+  }
+}
+
+if (!customElements.get('${tag}')) customElements.define('${tag}', ${pascal});
+`;
+} else {
+  content = `/**
+ * ${name} —— 页面组件(路由挂载)。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+// import { navigate } from '${relToUtils}/router.js';
+
+export class ${pascal} extends SignalWatcher(LitElement) {
+  static properties = {};
+
+  static styles = css\`
+    :host { display: block; }
+  \`;
+
+  render() {
+    return html\`<h2>${pascal}</h2>\`;
+  }
+}
+
+customElements.define('${tag}', ${pascal});
+`;
+}
+
+writeFileSync(path, content);
+console.log(`✓ 已生成 ${path}`);

+ 308 - 0
skills/fmode-code-lit/scripts/scaffold.mjs

@@ -0,0 +1,308 @@
+#!/usr/bin/env node
+/**
+ * scaffold.mjs —— 生成 Lit 原生 ESM 无构建项目骨架。
+ *
+ * 用法:
+ *   node scaffold.mjs <targetDir> [--project <name>] [--force]
+ *
+ * 原则:已存在的文件默认不覆盖;不引入任何构建工具;所有代码无装饰器。
+ */
+import { mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
+import { join, resolve, basename } from 'node:path';
+
+const args = process.argv.slice(2);
+const target = resolve(args.find((a) => !a.startsWith('--')) || '.');
+const force = args.includes('--force');
+const projIdx = args.indexOf('--project');
+const project = projIdx > -1 ? args[projIdx + 1] : basename(target);
+
+if (!project || !/^[a-z0-9][a-z0-9-]*$/.test(project)) {
+  console.error(`✗ 项目名非法:${project}(须为小写 kebab-case)`);
+  process.exit(2);
+}
+
+let created = 0, skipped = 0;
+function write(rel, content) {
+  const p = join(target, rel);
+  if (existsSync(p) && !force) { skipped++; console.log(`  skip  ${rel}(已存在)`); return; }
+  mkdirSync(join(p, '..'), { recursive: true });
+  writeFileSync(p, content);
+  created++; console.log(`  write ${rel}`);
+}
+
+const DEP_VERSIONS = {
+  lit: '3.3.3', 'lit-html': '3.3.3', 'lit-element': '4.2.2', '@lit/reactive-element': '2.1.2',
+  '@lit/context': '1.1.6', '@lit-labs/signals': '0.3.0', 'signal-polyfill': '0.2.2',
+};
+
+console.log(`\n生成 Lit 无构建项目:${target}\n项目名:${project}\n`);
+
+// 目录
+for (const d of ['src/components', 'src/services', 'src/utils', 'src/routes']) {
+  mkdirSync(join(target, d), { recursive: true });
+}
+
+// index.html
+write('src/index.html', `<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <title>${project}</title>
+  <!-- 二级子路径部署必须;根部署可改 "/" -->
+  <base href="/${project}/" />
+  <!-- 生产:指向 FMODE Repose 公共 importmap -->
+  <script type="importmap" src="/npm/importmap/lit-v3.json"></script>
+  <!-- 本地无 Repose 时改用: <script type="importmap" src="./importmap.json"></script> -->
+  <style>:root{--brand:#2b6cb0;--bg:#f7fafc;--border:#e2e8f0;font-family:system-ui,sans-serif}body{margin:0}</style>
+</head>
+<body>
+  <app-root></app-root>
+  <script type="module" src="./app.js"></script>
+</body>
+</html>
+`);
+
+// importmap.json(本地开发用)
+write('src/importmap.json', JSON.stringify({
+  _comment: '本地开发用;生产由 FMODE Repose 的 /npm/importmap/lit-v3.json 提供。',
+  imports: {
+    lit: `/npm/lit/v${DEP_VERSIONS.lit}/lit.js`,
+    'lit/': `/npm/lit/v${DEP_VERSIONS.lit}/`,
+    'lit-html': `/npm/lit-html/v${DEP_VERSIONS['lit-html']}/lit-html.js`,
+    'lit-element': `/npm/lit-element/v${DEP_VERSIONS['lit-element']}/lit-element.js`,
+    '@lit/reactive-element': `/npm/@lit/reactive-element/v${DEP_VERSIONS['@lit/reactive-element']}/reactive-element.js`,
+    '@lit/context': `/npm/@lit/context/v${DEP_VERSIONS['@lit/context']}/index.js`,
+    '@lit-labs/signals': `/npm/@lit-labs/signals/v${DEP_VERSIONS['@lit-labs/signals']}/index.js`,
+    'signal-polyfill': `/npm/signal-polyfill/v${DEP_VERSIONS['signal-polyfill']}/dist/index.js`,
+  },
+}, null, 2) + '\n');
+
+// app.js
+write('src/app.js', `/**
+ * app.js —— 应用装配:provide 全局服务 + 挂载路由 + 根组件。不含业务逻辑。
+ */
+import { LitElement, html, css } from 'lit';
+import { provide } from '@lit/context';
+import { authContext } from './services/contexts.js';
+import { authService } from './services/auth-service.js';
+
+export class AppRoot extends LitElement {
+  static styles = css\`:host{display:block;background:var(--bg)}\`;
+  render() {
+    return html\`
+      \${provide(authContext, authService)}
+      <app-shell></app-shell>
+    \`;
+  }
+}
+customElements.define('app-root', AppRoot);
+`);
+
+// services/contexts.js
+write('src/services/contexts.js', `/**
+ * 注入令牌集中定义(令牌必须同一模块实例)。
+ */
+import { createContext } from '@lit/context';
+export const authContext = createContext('${project}.auth');
+`);
+
+// services/auth-service.js
+write('src/services/auth-service.js', `/**
+ * 全局登录态(Signal Service)。纯 ESM Class,无 DOM 依赖,可单测。
+ */
+import { signal, computed } from '@lit-labs/signals';
+
+class AuthService {
+  #user = signal(null);
+  #token = signal(localStorage.getItem('token') || '');
+  get user() { return this.#user; }
+  get isLoggedIn() { return computed(() => this.#user.get() != null); }
+  getToken() { return this.#token.get(); }
+
+  async login(username, password) {
+    // TODO: 接真实接口
+    // const { token, user } = await http.post('/auth/login', { username, password });
+    const token = 'demo-token', user = { name: username };
+    this.#token.set(token); this.#user.set(user);
+    localStorage.setItem('token', token);
+  }
+  logout() { this.#token.set(''); this.#user.set(null); localStorage.removeItem('token'); }
+}
+export const authService = new AuthService();
+`);
+
+// utils/http.js
+write('src/utils/http.js', `/**
+ * 原生 fetch 薄封装。token 通过 configureHttp 注入,避免 utils 反向依赖 services。
+ */
+let getToken = () => '';
+let onUnauthorized = () => {};
+export function configureHttp({ token, unauthorized } = {}) {
+  if (token) getToken = token;
+  if (unauthorized) onUnauthorized = unauthorized;
+}
+export class HttpError extends Error {
+  constructor(status, body) { super('HTTP ' + status); this.status = status; this.body = body; }
+}
+const BASE = '/api';
+async function request(method, path, { body, headers = {}, query } = {}) {
+  let url = BASE + path;
+  if (query) url += '?' + new URLSearchParams(query).toString();
+  const h = { 'Content-Type': 'application/json', ...headers };
+  const t = getToken(); if (t) h.Authorization = 'Bearer ' + t;
+  const res = await fetch(url, { method, headers: h, body: body != null ? JSON.stringify(body) : undefined, credentials: 'same-origin' });
+  if (res.status === 401) { onUnauthorized(); throw new HttpError(401, null); }
+  const ct = res.headers.get('content-type') || '';
+  const data = ct.includes('application/json') ? await res.json() : await res.text();
+  if (!res.ok) throw new HttpError(res.status, data);
+  return data;
+}
+export const http = {
+  get: (p, o) => request('GET', p, o),
+  post: (p, body, o) => request('POST', p, { ...o, body }),
+  put: (p, body, o) => request('PUT', p, { ...o, body }),
+  del: (p, o) => request('DELETE', p, o),
+};
+`);
+
+// utils/router.js —— 子路径部署安全(不要用 new URL(path, baseURI),会丢部署前缀)
+write('src/utils/router.js', `/**
+ * utils/router.js —— 子路径部署安全的导航(只用 History API,干净 URL)。
+ *
+ * ⚠️ 不要写 new URL(path, document.baseURI) 做导航:path 以 '/' 开头时,
+ *   相对解析会丢掉 base 的路径前缀(/proj/users -> /users),子路径部署刷新即 404。
+ *   正确做法:把应用内路径拼到 base 的 pathname 上再解析。
+ *   详见规则仓 .claude/rules/routing-best-practices.md
+ */
+export function basePath() {
+  return new URL(document.baseURI).pathname.replace(/\\/+$/, '');
+}
+export function resolveUrl(path) {
+  const base = basePath();
+  const q = path.indexOf('?'), h = path.indexOf('#');
+  let cut = path.length;
+  if (q !== -1) cut = Math.min(cut, q);
+  if (h !== -1) cut = Math.min(cut, h);
+  let p = path.slice(0, cut);
+  const rest = path.slice(cut);        // 查询串 / hash 原样保留
+  if (!p.startsWith('/')) p = '/' + p;
+  return new URL(base + p + rest, location.origin);
+}
+export function navigate(path, { replace = false } = {}) {
+  const url = resolveUrl(path);
+  if (replace) history.replaceState({}, '', url); else history.pushState({}, '', url);
+  window.dispatchEvent(new PopStateEvent('popstate'));   // History 原生不触发
+}
+export function currentPath() {
+  const base = basePath();
+  let path = location.pathname;
+  if (base && path.startsWith(base)) path = path.slice(base.length);
+  if (!path.startsWith('/')) path = '/' + path;
+  return path.replace(/\\/{2,}/g, '/') || '/';   // 折叠 '//'
+}
+export function currentQuery() {
+  return Object.fromEntries(new URLSearchParams(location.search));
+}
+export function href(path) { return resolveUrl(path); }   // 供真实 <a href>
+`);
+
+// components/app-shell.js —— 不用 @lit-labs/router(子路径部署会污染路由状态)
+write('src/components/app-shell.js', `/**
+ * 布局壳:侧边菜单 + 内容出口。多标签版本见规则仓 tabs-keepalive.md。
+ *
+ * ⚠️ 不用 @lit-labs/router:它装全局 <a> 拦截器且用未剥前缀的 location.pathname
+ *   匹配路由,子路径部署下会污染路由状态。改为订阅 popstate + currentPath()。
+ */
+import { LitElement, html, css } from 'lit';
+import { currentPath, navigate } from '../utils/router.js';
+import './page-home.js';
+
+const ROUTES = [{ path: '/', tag: 'page-home', title: '首页' }];
+
+export class AppShell extends LitElement {
+  static styles = css\`:host{display:block;padding:16px}\`;
+  static properties = { _path: { state: true } };
+
+  #onPop = () => { this._path = currentPath(); };
+
+  connectedCallback() {
+    super.connectedCallback();
+    this._path = currentPath();
+    window.addEventListener('popstate', this.#onPop);
+  }
+  disconnectedCallback() {
+    window.removeEventListener('popstate', this.#onPop);
+    super.disconnectedCallback();
+  }
+
+  render() {
+    const hit = ROUTES.find((r) => r.path === this._path) || ROUTES[ROUTES.length - 1];
+    return html\`
+      <nav>
+        \${ROUTES.map((r) => html\`<button @click=\${() => navigate(r.path)}>\${r.title}</button>\`)}
+      </nav>
+      <main>\${this.#view(hit)}</main>
+    \`;
+  }
+  #view(r) { return document.createElement(r.tag); }
+}
+customElements.define('app-shell', AppShell);
+`);
+
+// components/page-home.js
+write('src/components/page-home.js', `/**
+ * 首页示例组件。无装饰器;用 static properties。
+ */
+import { LitElement, html, css } from 'lit';
+import { SignalWatcher } from '@lit-labs/signals';
+import { ContextConsumer } from '@lit/context';
+import { authContext } from '../services/contexts.js';
+
+export class PageHome extends SignalWatcher(LitElement) {
+  static properties = { _auth: { state: true } };
+  #consumer = new ContextConsumer(this, { context: authContext, subscribe: true, callback: (v) => { this._auth = v; } });
+  static styles = css\`h1{color:var(--brand)}\`;
+  render() {
+    const name = this._auth?.user.get()?.name;
+    return html\`<h1>${project}: \${name || 'hello lit'}</h1>\`;
+  }
+}
+customElements.define('page-home', PageHome);
+`);
+
+// package.json
+write('package.json', JSON.stringify({
+  name: project, private: true, type: 'module',
+  _comment: 'devDependencies 仅用于 IDE 类型提示,不参与部署产物。',
+  scripts: { serve: 'npx --yes serve src', deploy: 'bash scripts/deploy.sh ' + project },
+  devDependencies: {
+    lit: '^' + DEP_VERSIONS.lit, '@lit/context': '^' + DEP_VERSIONS['@lit/context'],
+    '@lit-labs/signals': '^' + DEP_VERSIONS['@lit-labs/signals'],
+  },
+}, null, 2) + '\n');
+
+// scripts/deploy.sh
+write('scripts/deploy.sh', `#!/usr/bin/env bash
+# 无构建部署:同步 src/ 到 OBS + CDN 刷新。凭据由环境/既有脚本提供,不写入仓库。
+set -euo pipefail
+PROJECT="\${1:-${project}}"
+BUCKET="\${OBS_BUCKET:?需设置 OBS_BUCKET}"
+EP="\${OBS_ENDPOINT:-obs.<region>.myhuaweicloud.com}"
+OBS="\${OBSUTIL:-obsutil}"
+SRC="$(cd "$(dirname "$0")/.." && pwd)/src"
+"$OBS" rm "$BUCKET/$PROJECT/index.html" -e="$EP" -f >/dev/null 2>&1 || true
+"$OBS" cp "$SRC/index.html" "$BUCKET/$PROJECT/index.html" -e="$EP" -acl=public-read -f \\
+  -meta="Cache-Control:no-cache#Content-Type:text/html; charset=utf-8"
+"$OBS" sync "$SRC/" "$BUCKET/$PROJECT/" -e="$EP" -acl=public-read -f
+echo "完成。确认 CDN 回退:不存在路径 200 返回 index.html(禁 302)。"
+`);
+
+// .gitignore
+write('.gitignore', 'node_modules/\ndist/\n.DS_Store\n');
+
+console.log(`\n✓ 完成:新建 ${created} 个文件,跳过 ${skipped} 个。`);
+console.log(`\n下一步:`);
+console.log(`  1. 本地预览: cd ${target} && npx serve src`);
+console.log(`  2. 或在 index.html 指向 Repose 公共 importmap 后直接部署`);
+console.log(`  3. 规则细则见 fmode-code-lit 规则仓 .claude/rules/\n`);

+ 60 - 0
skills/fmode-code-lit/scripts/smoke.mjs

@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+/**
+ * smoke.mjs —— 技能自检。
+ * 生成一个临时项目并校验:文件齐全、JS 语法通过、无装饰器。
+ * 用法:node scripts/smoke.mjs
+ */
+import { mkdtempSync, rmSync, readdirSync, statSync, readFileSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { tmpdir } from 'node:os';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const scaffold = join(__dirname, 'scaffold.mjs');
+const newGen = join(__dirname, 'new.mjs');
+
+let fail = 0;
+const check = (name, cond) => { console.log(`${cond ? '✓' : '✗'} ${name}`); if (!cond) fail++; };
+
+function walk(d, out = []) {
+  for (const e of readdirSync(d, { withFileTypes: true })) {
+    const p = join(d, e.name);
+    if (e.isDirectory()) walk(p, out); else out.push(p);
+  }
+  return out;
+}
+
+const dir = mkdtempSync(join(tmpdir(), 'fmode-code-lit-smoke-'));
+try {
+  execFileSync(process.execPath, [scaffold, dir, '--project', 'smoke-app'], { stdio: 'pipe' });
+  const files = walk(dir);
+  check('scaffold 生成 index.html', files.some((f) => f.endsWith('src/index.html')));
+  check('scaffold 生成 importmap.json', files.some((f) => f.endsWith('src/importmap.json')));
+  check('scaffold 生成 app.js', files.some((f) => f.endsWith('src/app.js')));
+  check('scaffold 生成分层目录', ['components', 'services', 'utils'].every((d2) => files.some((f) => f.includes(`/src/${d2}/`))));
+
+  execFileSync(process.execPath, [newGen, 'component', 'smoke-card', '--dir', join(dir, 'src/components')], { stdio: 'pipe' });
+  execFileSync(process.execPath, [newGen, 'service', 'smoke-service', '--dir', join(dir, 'src/services')], { stdio: 'pipe' });
+  check('new component 生成', files.some((f) => f.endsWith('smoke-card.js')) || walk(dir).some((f) => f.endsWith('smoke-card.js')));
+  check('new service 无重复后缀', walk(dir).some((f) => f.endsWith('smoke-service.js')) && !walk(dir).some((f) => f.endsWith('smoke-service-service.js')));
+
+  // 全量语法校验 + 装饰器扫描
+  let syntaxOk = true, decoratorFound = false;
+  for (const f of walk(dir).filter((f) => f.endsWith('.js'))) {
+    try { execFileSync(process.execPath, ['--check', f], { stdio: 'pipe' }); } catch { syntaxOk = false; console.log('  语法失败:', f); }
+    if (/^\s*@(customElement|property|state|consume|provide|query)\b/m.test(readFileSync(f, 'utf8'))) decoratorFound = true;
+  }
+  check('生成物全部通过语法检查', syntaxOk);
+  check('生成物无装饰器语法', !decoratorFound);
+
+  // importmap JSON 合法
+  let mapOk = false;
+  try { JSON.parse(readFileSync(join(dir, 'src/importmap.json'), 'utf8')); mapOk = true; } catch {}
+  check('importmap.json 为合法 JSON', mapOk);
+} finally {
+  rmSync(dir, { recursive: true, force: true });
+}
+
+console.log(fail === 0 ? '\nSMOKE OK' : `\nSMOKE FAILED(${fail})`);
+process.exit(fail === 0 ? 0 : 1);

+ 18 - 0
skills/fmode-code-lit/skill-package-manifest.json

@@ -0,0 +1,18 @@
+{
+  "name": "fmode-code-lit",
+  "version": "1.0.0",
+  "description": "Claude Code 独立技能包:用 Lit 原生 ESM 无构建方式写前端(零编译/零打包/浏览器直跑),分层规范 + importmap + signals 响应式 + context 注入 + router History 路由 + 多标签 KeepAlive,配套规则仓与 OBS+CDN 部署。",
+  "plugin": "fmode-code-lit",
+  "skills": [
+    "fmode-code-lit"
+  ],
+  "entrySkill": "fmode-code-lit",
+  "npmPackage": "@fmode/skill-code-lit",
+  "smokeCommand": "node scripts/smoke.mjs",
+  "installCommand": "npx @fmode/skill-code-lit@latest install",
+  "workspaceInstallCommand": "npx @fmode/skill-code-lit@latest workspace",
+  "workspaceSkillPath": ".claude/skills/fmode-code-lit/SKILL.md",
+  "globalSkillPath": "~/.claude/skills/fmode-code-lit/SKILL.md",
+  "installHint": "工作区安装:npx @fmode/skill-code-lit@latest workspace,写入 ./.claude/skills/fmode-code-lit/。用户级安装:npx @fmode/skill-code-lit@latest install,写入 ~/.claude/skills/fmode-code-lit/。随后可用 npx @fmode/skill-code-lit@latest scaffold <dir> --project <name> 生成无构建 Lit 项目骨架。规则细则见 fmode-code-lit 规则仓 .claude/rules/。",
+  "summary": "数字人开发基础规则技能:Lit 原生 ESM 无构建前端(含多页面 light DOM 五条死线)"
+}

+ 17 - 3
skills/inventory.json

@@ -1,7 +1,7 @@
 {
   "name": "fmode-skills",
-  "version": "0.2.0",
-  "generatedAt": "2026-10-15T10:55:45.176458",
+  "version": "0.3.0",
+  "generatedAt": "2026-10-30T00:00:00.000000",
   "format": "esm",
   "architecture": "纯接口驱动: 子技能只依赖公开HTTP接口+用户token(env)+agent原生工具, 不依赖任何需另行安装的软件/本地二进制",
   "skills": [
@@ -109,6 +109,20 @@
       "version": "0.3.38",
       "level": "应用级",
       "summary": "小红书/抖音/视频号 VOC 情报:趋势情报、问题池、单点深挖、竞品图谱、7天内容计划、口播脚本、成本测算、用量账单。 "
+    },
+    {
+      "name": "fmode-code-lit",
+      "slug": "fmode-skill-fmode-code-lit",
+      "version": "1.0.0",
+      "level": "开发级",
+      "summary": "数字人开发基础规则:Lit 原生 ESM 无构建前端(core/wc/page、History 路由、light DOM 五条死线)。 触发词:lit、无构建、ESM、前端"
+    },
+    {
+      "name": "fmode-code-agent",
+      "slug": "fmode-skill-fmode-code-agent",
+      "version": "0.1.1",
+      "level": "开发级",
+      "summary": "数字人开发基础规则:纯 JS 网页版 Agent(Agent/MCP/Skill 三层、LangGraph 无构建、token 实测)。 触发词:agent、智能体、MCP、LangGraph"
     }
   ]
-}
+}