impeccable 单次 Live 模式配置指南:从 config.json 骨架到 CSP 探测补丁
impeccable 的 live-setup 流程处理的是「一次性」的 Live 变体模式项目初始化:把实时浏览器挑选器(live picker,监听 http://localhost:8400 的辅助服务器)接入开发服务器所服务的 HTML 文档骨架,并解决开发态 CSP 的放行问题。本文以 skill/reference/live-setup.md 为核心文档,结合仓库内的注入逻辑描述、框架 fixture 与参考补丁输出,讲解 config.json 的完整字段、各框架的 files/锚点对照表、配置漂移处理,以及四种 CSP 形状(shape)的探测与自动补丁方案。读完后,你既能独立为一个新项目完成 live mode 首启配置,也能理解为什么这些配置与补丁被设计为「只能由用户决定、绝不自动改配置」。
本文涉及路径若在打包产物中引用,helper 脚本路径存在
{{scripts_path}}占位(例如.kiro/skills/impeccable/scripts/detect-csp.mjs),在不同 Agent harness 的分发副本中会被替换为真实绝对路径;仓库内源文件位置统一以skill/、tests/framework-fixtures/为准。
live-setup 何时被加载:它不在每次会话的热路径上
live-setup 文档明确界定了自己的触发边界:它不是每次会话都会执行的流程,而只会在以下四种情况之一出现时,由 live.md 按需加载:
live.mjs启动时报出config_missing或config_invalid({ ok: false, error: ..., path });configDrift需要向用户说明(存在未被files覆盖的 HTML);- 配置中缺少
cspChecked字段(即 CSP 探测/征询这一步从未跑过)。
也就是说,这是「一次性配置」,一旦写入 .impeccable/live/config.json 并标记 cspChecked: true,之后的会话启动会直接跳过整个 setup 段落,直接进入 per-session 的轮询热路径。live-setup 本身「不参与每次会话的热路径」——注入脚本常驻、轮询恢复等工作由 live.md 中描述的其他命令负责。
运行上下文上还有一点值得注意:仓库的 ADR(docs/adr-live-variant-mode.md)注明 live 的辅助服务器是「live-server.mjs (node, localhost:8400+, zero dependencies)」。这正是后续 CSP 补丁要向 script-src/connect-src 追加 http://localhost:8400 的根本原因——live picker 与变体注入脚本都要从这一本地源加载,任何开发环境的内容安全策略都必须显式放行它。
第一步:写入 config.json
在 boot 报告返回的 path(默认 .impeccable/live/config.json)下创建配置文件:
{
"files": ["<path-or-glob>", "<path-or-glob>", ...],
"exclude": ["<optional-glob>", ...],
"insertBefore": "</body>",
"commentSyntax": "html",
"cspChecked": true
}
各字段含义如下(以下解释均直接对应 setup 流程的约定):
| 字段 | 作用 | 说明 |
|---|---|---|
files |
注入目标 | 必须是浏览器实际加载的 HTML 文件,不一定是源码文件(tracked 还是 generated 在此无关紧要,wrap 流程有自己的 generated 文件守卫);条目可以是字面路径或 glob |
exclude |
可选排除 | 跳过 files glob 本会匹配到的文件(如邮件模板、demo fixtures) |
insertBefore |
注入锚点 | 在该行/字符串之前插入注入脚本;</body> 几乎对所有文件都成立 |
commentSyntax |
注释/标记语法 | 决定注入标记的注释风格:html 或 jsx,必须与目标文件的语言一致 |
cspChecked |
首启标记 | 记录下文的 CSP 探测/征询步骤已执行过;首次 setup 时该字段不存在 |
glob 语法与硬性排除路径
**:匹配任意数量的路径段(含零段);*:匹配单个路径段内部;?:匹配单个字符。- 所有路径均相对项目根目录,并使用正斜杠
/。 - 硬排除路径(无法被任何配置覆盖):
**/node_modules/**与**/.git/**。向这两处注入相当于给第三方代码插桩,因此直接拒绝。
各框架 files / 锚点 / 注释语法对照表
| Framework | files |
insertBefore |
commentSyntax |
|---|---|---|---|
| SPA with single shell (Vite / React / Plain HTML) | ["index.html"] |
</body> |
html |
| Next.js (App Router) | ["app/layout.tsx"] |
</body> |
jsx |
| Next.js (Pages) | ["pages/_document.tsx"] |
</body> |
jsx |
| Nuxt | ["app.vue"] |
</body> |
html |
| Svelte / SvelteKit | ["src/app.html"] |
</body> |
html |
| TanStack Router (SPA, Vite) | ["index.html"] |
</body> |
html |
| TanStack Start (SSR) | ["src/routes/__root.tsx"] |
<Scripts |
jsx |
| Astro | ["<root layout .astro>"] |
</body> |
html |
| Multi-page (separate HTML per route) | ["public/**/*.html"] glob over the served dir |
</body> |
html |
选择锚点的经验法则:
- 挑一个在每个目标文件里都存在的锚点(
</body>几乎总是可用)。insertAfter则是在某一行之后匹配插入,两者互斥选一。 - 多页面站点优先使用 glob(如
public/**/*.html),这样新增页面会被自动纳入注入范围,无需每次改配置。 - 由 generator 重建页面的站点要注意:注入只在下次重新生成之前存活,因此每次 build 之后需要重新运行
live.mjs。这一限制不影响 accept 流程——accept 通过 fallback 流程把选定变体写入真正的源码文件,不受重建影响。
框架适配器:注入时的自动探测
同样是「把 <script> 注入文档骨架」,不同框架的可行路径并不一样。注入器会在注入时自动探测框架类型,而非要求用户手填:
- SvelteKit、Nuxt、TanStack Start 会对文档 shell 做服务端渲染,入口模板里放一个原生
<script>并不会可靠执行。此时live-inject.mjs检测到框架后会转交给专用适配器:- SvelteKit:从
+layout.svelte挂一个仅 dev 生效的根组件; - Nuxt:挂一个仅 dev 生效的
.client.tsplugin; - TanStack Start:在
__root中生成一个仅 dev 生效的ImpeccableLiveRoot组件。
- SvelteKit:从
- 而普通的 TanStack Router SPA 走 Vite 基线路径,无需适配器。
注意适配器模式下,config 里的 files 值仍然保留——它是有用的检测/CSP 提示,但不再是字面上的注入落点。
这份配置的实际校验与回归在仓库里是有完整 fixture 支撑的。查看 tests/framework-fixtures/README.md 可以看到,测试会把每个 fixture 目录复制进临时 git 仓库,然后驱动 live-inject.mjs、live-wrap.mjs、live-accept.mjs 与 lib/is-generated.mjs 进行验证;fixture.json 的 config 字段内容正是要写入 .impeccable/live/config.json 的样例,csp 字段则声明了该 fixture 期望的 CSP 形状(shape)、patchTarget 与 expectedAfter(参考补丁输出文件)。仓库根目录的 tests/framework-fixtures/ 覆盖了 nextjs-app-router、nuxt、sveltekit、tanstack-router-vite、astro、vite-react 等 20+ 种项目形态,其中 CSP 相关的有 nextjs-inline-csp/、nextjs-turborepo/、nuxt-csp/、sveltekit-csp/。
注入日志与自愈
每次 inject 都会把「我写了什么」记录到 .impeccable/live/inject-journal.json。下次 inject(或 remove)时会读取该日志,把上次崩溃或目录停错残留的注入工件「治愈」(heal)掉。这与 live 会话的持久化结构是配套的——live 的运行期状态目录 .impeccable/live/ 还会承载 roots.json(应用根目录清单)与 sessions/ 下的 append-only 会话日志,只是后者属于会话热路径而非 setup 范畴。
Config drift:发现未被覆盖的页面
每次 boot 时,注入流程会扫描项目常见页面根目录(public/、src/、app/、pages/)下的 HTML 文件,检查解析后的 files 列表是否覆盖到它们:
- 未覆盖到的文件会作为
configDrift.orphans暴露出来,并附带提示; - 处理原则是:每个会话只向用户说明一次「哪些文件没有被覆盖」,并提议把它加进
files,或把files换成 glob; - 绝不自动更新配置——是否覆盖由用户决定;
- 没有漂移时,
configDrift为null。
之所以约束得这么严格,是因为 .impeccable/live/config.json 是跨会话的项目级配置:一旦注入脚本被 files 显式覆盖的文件清单“记死”,新页面可能永远得不到注入能力;反过来如果盲目扩 glob,又可能把邮件模板、demo fixture 等不该插桩的文件纳入。把决定权留给用户,是避免配置静默扩张引发副作用的关键设计。
CSP 探测(仅首次执行)
只有当 config.cspChecked !== true 时才会执行本节;一旦问过用户一次,配置里就会写入 cspChecked: true,后续跳过。
运行探测脚本(路径以所在 harness 副本为准,源文档示例使用 .kiro 副本):
node .kiro/skills/impeccable/scripts/detect-csp.mjs
输出形如 { shape, signals }。shape 命名的是「补丁机制」而非具体框架,因此一份模板可以覆盖多个框架:
| shape | 含义 | 处理方式 |
|---|---|---|
null |
无 CSP | 直接写入带 cspChecked: true 的配置,收工 |
append-arrays |
CSP 以结构化指令数组形式存在 | 可自动补丁(monorepo helper 的 additionalScriptSrc/additionalConnectSrc、SvelteKit 的 kit.csp.directives、Nuxt 的 nuxt-security) |
append-string |
CSP 是字面量字符串 | 可自动补丁(内联 next.config.* 的 headers()、Nuxt 的 routeRules) |
middleware / meta-tag |
探测到但不可自动补丁 | 把探测到的文件展示给用户,请其手动把 http://localhost:8400 加入 script-src 与 connect-src,再标记 cspChecked: true 继续 |
征询话术(务必使用这份措辞)
探测完成后,若需要补丁,要向用户出示如下确认,diff 必须是精确的 2–5 行:
CSP patch needed. I detected a Content Security Policy in your project that blocks
http://localhost:8400: the live picker won't load without an allowance. Here's the change I'd make:[file: <patchTarget>] [exact diff, 2-5 lines]It's guarded by
NODE_ENV === "development"so the extra entry only appears in dev and never reaches production. You can remove it any time by reverting this file. Apply? [y/n]
分支行为:
- 用户回答 no:跳过补丁,向用户说明在手动放行之前 live 不会工作;仍然写入
cspChecked: true——因为这个问题已经被问过了,不能每次启动都追问; - 用户回答 yes:按 shape 执行下方对应补丁,然后写入
cspChecked: true。
补丁机制一:append-arrays(结构化数组)
在持有 CSP 数组的文件顶部声明一个 dev-only 常量,然后把 ...__impeccableLiveDev 追加进 script-src 与 connect-src 数组:
// Dev-only allowance so impeccable live mode can load. Guarded by NODE_ENV.
const __impeccableLiveDev =
process.env.NODE_ENV === "development" ? ["http://localhost:8400"] : [];
分框架的落点:
- Next.js + monorepo helper:改应用自己的
next.config.*(不要改共享 helper),追加到additionalScriptSrc/additionalConnectSrc。仓库中的参考输出见 tests/framework-fixtures/nextjs-turborepo/expected-after-patch.ts——它展示了createBaseNextConfig({ ..., additionalScriptSrc: [posthogHost, ...__impeccableLiveDev], additionalConnectSrc: [posthogHost, ...__impeccableLiveDev] })的完整形态,文件头注释亦称之为 “Shape 1 (shared-helper)”。 - SvelteKit:改
svelte.config.js,追加到kit.csp.directives['script-src']与['connect-src']数组。参考输出见 tests/framework-fixtures/sveltekit-csp/expected-after-patch.js,其中directives下'script-src': ['self', 'unsafe-inline', ...__impeccableLiveDev]、'connect-src': ['self', ...__impeccableLiveDev]。 - Nuxt + nuxt-security:改
nuxt.config.*,追加到security.headers.contentSecurityPolicy['script-src']与['connect-src']。
幂等性约定:补丁函数是幂等的——若目标文件中已存在 __impeccableLiveDev,说明此前已打过补丁,只需标记 cspChecked: true 即可,不必重复改动。
补丁机制二:append-string(字面量字符串)
两处修改的补丁:声明一个 dev-only 字符串,把它插值进 CSP 值里的两个指令。字符串以前导空格开头以便干净拼接,且作为编辑的一部分要把普通字面量转成模板字符串:
// Dev-only allowance so impeccable live mode can load.
const __impeccableLiveDev =
process.env.NODE_ENV === "development" ? " http://localhost:8400" : "";
变换示例:
script-src 'self' 'unsafe-inline'变为`script-src 'self' 'unsafe-inline'${__impeccableLiveDev}`connect-src 'self'变为`connect-src 'self'${__impeccableLiveDev}`
分框架的落点:
- Next.js:内联
headers(),位于next.config.*。参考输出见 tests/framework-fixtures/nextjs-inline-csp/expected-after-patch.js,其中headers()返回的 CSP value 用字符串拼接并逐一嵌入${__impeccableLiveDev}(文件头注释称之 “Shape 2 (inline-headers)”)。 - Nuxt:
nuxt.config.*中routeRules['/**'].headers['Content-Security-Policy']。参考输出见 tests/framework-fixtures/nuxt-csp/expected-after-patch.ts,同一个defineNuxtConfig内通过routeRules对全路径下发带插值的 CSP 头。
两处 fixture 都印证了核心安全前提:放行条目由 process.env.NODE_ENV === "development" 守卫——额外条目只出现在 dev,永远不会进入生产响应,用户随时可通过 revert 文件移除。
Troubleshooting
场景:用户曾对 CSP 补丁回答 no,之后反馈 live 不工作。
原因基本可以断定:开发环境的 CSP 仍在拦截 http://localhost:8400。解决方式是让 setup 重新征询一次:
- 从
.impeccable/live/config.json里删除cspChecked字段(保留其余配置); - 重新运行
live.mjs——boot 会因缺少cspChecked再次进入 live-setup 的 CSP 探测与征询流程。
这正是 cspChecked 被刻意设计为「问题已问过」的持久标记而非不可逆开关的原因:删掉它,就能安全地重放一次征询,而不会影响已经写好的 files/exclude/insertBefore 等其余配置。
setup 全部完成后,重新运行 live.mjs 继续正常流程。
小结:一次配置、三重守卫
回看整个 live-setup,它的健壮性来自三个互相独立的设计决策:
- 配置决策权归用户:是否覆盖漂移页面、是否应用 CSP 补丁,都由用户拍板;
configDrift只提醒、cspChecked只记录“问过了”,配置字段绝不静默改写。 - 按注入机制而非框架分类:CSP 的
shape命名的是补丁方式(append-arrays / append-string / middleware / meta-tag),使同一份 diff 模板能跨 Next.js、Nuxt、SvelteKit 复用,并用NODE_ENV === "development"守卫保证生产零泄漏。 - 状态可恢复:
config.json是唯一持久配置,inject-journal.json自愈残留注入,删除cspChecked即可重放 CSP 征询——任何一步出错都能从源码级位置(如 tests/framework-fixtures/ 下的参考补丁输出)对照修正,再回到live.mjs主流程。
把这份一次性配置做完,项目就获得了后续 live 会话的稳定前提:注入脚本可靠驻留在文档骨架中、页面加入/删除不再产生盲区、且开发服务器在内容安全策略下能正常加载 http://localhost:8400 上的 live picker。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00