diff --git a/.gitignore b/.gitignore index 433c53b75..65eaef7ca 100644 --- a/.gitignore +++ b/.gitignore @@ -34,17 +34,19 @@ CHANGELOG.md package-lock.json yarn.lock +# AI 工具本地工作目录(会话状态、计划、worktree、构建产物副本,体积可达 GB 级) .claude - .codex - .omc +# 不带尾斜杠:linked worktree 里的 .dev-kit 是符号链接,`.dev-kit/` 只匹配真目录会漏掉它 +.dev-kit +.superpowers/ +docs/superpowers/ +docs/specs/ test-results playwright-report +.vitest-reports/ # 一次性功能验证脚本(见 docs/verification.md),不提交、不进 CI e2e/scratch/ - -superpowers -.omc diff --git a/.prettierignore b/.prettierignore index 5b545eac2..c9effb09c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,8 +3,12 @@ package-lock.json pnpm-lock.yaml yarn.lock -# Claude Code +# Agent tooling working dirs .claude +.codex +.dev-kit/ +.omc/ +.superpowers/ # Docs & examples *.md @@ -14,4 +18,8 @@ example/ dist/ playwright-report/ test-results/ +.vitest-reports/ coverage/ + +# 一次性验证脚本(见 docs/verification.md),随写随删,不参与仓库格式门禁 +e2e/scratch/ diff --git a/AGENTS.md b/AGENTS.md index aecdf60e3..8eb928f4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,51 +1,24 @@ # Repository Guidelines This file provides guidance to AI coding agents (Claude Code, etc.) when working with code in this repository. +It holds only the engineering principles and the architecture quick-map; the concrete "how" belongs to the docs +below. `CLAUDE.md` merely `@import`s this file — don't split guidance between the two. Link the owning doc +instead of copying its content here. -> **Note:** This is the single source of truth relative to `CLAUDE.md` — `CLAUDE.md` only contains `@AGENTS.md` -> and re-imports this file; don't split guidance between the two, put it here. Detailed guidance beyond -> engineering principles and the architecture map is owned by the docs linked below (see -> [`docs/DOC-MAINTENANCE.md`](docs/DOC-MAINTENANCE.md)'s ownership table) — cross-link them, don't duplicate -> their content here. +**Read before you act.** [`docs/README.md`](docs/README.md) indexes the full doc set. -> **Before writing any code, read [`docs/develop.md`](docs/develop.md)** — the development spec (commands, -> project structure, coding style, UI & theme rules, testing mechanics, i18n, and the commit/PR workflow). This -> file keeps only the non-negotiable engineering principles and the architecture map; the concrete "how" lives -> in that same guide, and deep internals in [`docs/architecture.md`](docs/architecture.md). - -> **To manually verify a feature actually works, read [`docs/verification.md`](docs/verification.md)** — drive -> the real built extension end-to-end with one-shot throwaway scratch scripts (not the committed test suite). - -> **Before building or modifying any page, dialog, or block, read [`docs/design.md`](docs/design.md)** — the -> design system: color tokens, component palette, layout/motion/state patterns, and the new-page recipe. Its -> Core Constraints apply to every UI change, not just new ones. - -> **Before any translation/localization work, read [`docs/translation.md`](docs/translation.md)** — -> the single source of truth for translation. Whenever you add or change localized content -> (`src/locales//*.json` namespace files, per-language docs, UI copy, or test snapshots), you must first -> read that guide and follow the matching `docs/references/terminology-.md` if it exists. - -> **Before adding, editing, reorganizing, or reviewing any tracked agent/contributor Markdown — this file, -> `docs/*`, `.github/*.md`, package-local READMEs, and source-local READMEs — read -> [`docs/DOC-MAINTENANCE.md`](docs/DOC-MAINTENANCE.md)** — keep the doc set organized (links resolve, index -> current, no duplication, no cross-document policy conflicts) and every claim factually true against the current -> branch (*if you can't grep it on this branch, don't claim it*). That guide owns the full checklist; don't copy -> it into this always-loaded file. - -> **Before opening or updating a pull request, read [`docs/pull-request.md`](docs/pull-request.md)** — this -> repo's PR description structure and evidence rules. - -> **Doc map:** [`docs/README.md`](docs/README.md) indexes every contributor doc (development, architecture, -> translation, contributing, localized READMEs). +| Before you… | Read | +| --- | --- | +| write any code | [`docs/develop.md`](docs/develop.md) | +| build or modify any page, dialog, or block | [`docs/design.md`](docs/design.md) — its Core Constraints apply to *every* UI change, not only new pages | +| add or change localized content | [`docs/translation.md`](docs/translation.md) — plus the matching `docs/references/terminology-.md` when one exists | +| add, edit, reorganize, or review any tracked contributor Markdown (this file, `docs/*`, `.github/*.md`, package- and source-local READMEs) | [`docs/DOC-MAINTENANCE.md`](docs/DOC-MAINTENANCE.md) — *if you can't grep it on this branch, don't claim it* | +| open or update a pull request | [`docs/pull-request.md`](docs/pull-request.md) | +| manually confirm a feature works | [`docs/verification.md`](docs/verification.md) — a throwaway scratch script against the built extension, not the committed suite | ## Project Overview -ScriptCat — Manifest V3 browser extension that runs Tampermonkey-compatible user scripts. TypeScript + React 19 + Rspack. Package manager is **pnpm** (preinstall enforces). - -> **UI stack.** The presentation layer (`src/pages/`) is built with **shadcn/ui + Tailwind CSS v4** on -> **React 19** (migrated from Arco Design + UnoCSS). The concrete UI/theme rules live in -> [`docs/develop.md`](docs/develop.md); the design system (color tokens, components, layout/motion/state -> patterns, new-page recipe) lives in [`docs/design.md`](docs/design.md). +ScriptCat — Manifest V3 browser extension that runs Tampermonkey-compatible user scripts. TypeScript + React 19 + Rspack. Package manager is **pnpm** (preinstall enforces). The presentation layer (`src/pages/`) is **shadcn/ui + Tailwind CSS v4** (migrated from Arco Design + UnoCSS). ## Engineering Principles @@ -63,9 +36,8 @@ isn't universal, that's called out in the item itself. ## Architecture -> **Deep dive:** [`docs/architecture.md`](docs/architecture.md) — the human-facing internals guide for -> contributors working on ScriptCat core: process model, message passing, service/data layers, GM API system, -> script execution, and the build pipeline, with "how to extend" recipes. The section below is the quick map. +Quick map only — the internals guide and its "how to extend" recipes are in +[`docs/architecture.md`](docs/architecture.md). ### Multi-Process Model @@ -107,7 +79,4 @@ Execution paths: page scripts → `chrome.userScripts`; background → SW → Of `message/` (with mocks), `filesystem/` (WebDAV, cloud drive providers, zip export — see [`docs/cloud-sync.md`](docs/cloud-sync.md)), `cloudscript/`, `eslint/` (userscript lint config — `eslint-plugin-userscripts`-based `defaultConfig` for the in-app editor), `chrome-extension-mock/`. -> The project's own custom ESLint rules (`eslint-rules/` at the repo root, wired in `eslint.config.mjs` — -> **not** `packages/eslint/`, which is the unrelated userscript lint config) are documented in -> [`docs/develop.md`](docs/develop.md#eslint-custom-rules): exact rule names, scopes, and which are covered by -> `eslint-rules/harness.test.mjs`. +The project's *own* custom ESLint rules live in `eslint-rules/` at the repo root, **not** in `packages/eslint/`; both are documented in [`docs/develop.md`](docs/develop.md#eslint-custom-rules). diff --git a/docs/DOC-MAINTENANCE.md b/docs/DOC-MAINTENANCE.md index e6bc744b7..ecfa661f8 100644 --- a/docs/DOC-MAINTENANCE.md +++ b/docs/DOC-MAINTENANCE.md @@ -88,22 +88,24 @@ of sanitization patterns can otherwise look like matches — so don't rely on a | Doc | Owns | | --- | --- | -| [`../AGENTS.md`](../AGENTS.md) | Engineering principles + architecture quick-map. Single source of truth; `CLAUDE.md` only `@import`s it. | -| [`develop.md`](./develop.md) | The concrete "how": commands, structure, style, i18n, commit/PR; testing (design, cleanup policy, mechanics) split to [`references/develop-testing.md`](./references/develop-testing.md). | -| [`pull-request.md`](./pull-request.md) | Detailed PR description structure and guidance for agents and contributors; the human-facing template remains lightweight. | -| [`design.md`](./design.md) | The design system: theme mechanism, shadcn component selection, new-page recipe; tokens split to [`references/design-tokens.md`](./references/design-tokens.md), component palette to [`references/design-components.md`](./references/design-components.md), layout/motion/state/a11y patterns to [`references/design-patterns.md`](./references/design-patterns.md). | -| [`verification.md`](./verification.md) | Lightweight end-to-end functional verification — throwaway scratch scripts driving the real built extension; report template split to [`references/verification-report-template.md`](./references/verification-report-template.md), debugging FAQ to [`references/verification-debugging.md`](./references/verification-debugging.md). | -| [`architecture.md`](./architecture.md) | Deep internals: process model, message passing; subsystem deep-dives split to [`references/architecture-services.md`](./references/architecture-services.md), [`references/architecture-data.md`](./references/architecture-data.md), [`references/architecture-gm-api.md`](./references/architecture-gm-api.md), [`references/architecture-execution.md`](./references/architecture-execution.md), [`references/architecture-build.md`](./references/architecture-build.md), [`references/architecture-agent.md`](./references/architecture-agent.md). | +| [`../AGENTS.md`](../AGENTS.md) | Engineering principles + architecture quick-map. `CLAUDE.md` only `@import`s it. | +| [`develop.md`](./develop.md) | The concrete "how": commands, structure, style, i18n, commit/PR. Testing → [`references/develop-testing.md`](./references/develop-testing.md). | +| [`pull-request.md`](./pull-request.md) | The PR body: structure and evidence rules. The human-facing template stays lightweight. | +| [`design.md`](./design.md) | The design system; tokens, component palette, and layout/motion/state/a11y patterns → the three `references/design-*.md`. | +| [`verification.md`](./verification.md) | *When* to drive the real built extension, where its evidence goes, how to report honestly. Not the harness — link to `e2e/README.md`, don't restate fixtures/isolation/env vars. | +| [`../e2e/README.md`](../e2e/README.md) | The harness itself: the two tracks and their configs, isolation, fixture/helper inventory, protocol mocks, `E2E_*` variables, artifact paths. | +| [`architecture.md`](./architecture.md) | Deep internals; subsystem deep-dives → the six `references/architecture-*.md`. | | [`cloud-sync.md`](./cloud-sync.md) | Cloud sync internals: sync files, digest/status semantics, provider differences, error classification, retry policy. | | [`translation.md`](./translation.md) | Translation / localization single source of truth. | -| [`external-access-guide.md`](./external-access-guide.md) | End-user how-to for External Access: install sctl, enable, enroll once (`sctl connect`), write/source-read policies, three-tier decisions, tool table, CLI verbs, worked examples. Security rationale lives in the sctl repo's `docs/threat-model.md`/`docs/protocol.md` — link, don't duplicate. `external-access-guide_zh-CN.md` is its zh-CN translation — keep in sync via [`translation.md`](./translation.md), don't fork content. | -| [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md) | This guide: doc-set organization rules, fact-check / anti-drift discipline, and policy-consistency checks — for every tracked agent/contributor Markdown file, not just `AGENTS.md` + `docs/*`. | -| [`README.md`](./README.md) | The index that points to all of the above. | -| `.github/copilot-instructions.md` | Copilot-specific entry point and any genuine tool-specific differences; shared facts (architecture, commands, testing, design, translation, PR mechanics) route to the owning doc above instead of being copied. | +| [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md) | This guide: organization rules, fact-check / anti-drift discipline, policy-consistency checks — across every tracked contributor Markdown, not just `AGENTS.md` + `docs/*`. | +| [`README.md`](./README.md) | The reader-facing index: what each doc contains and when to read it. | +| `.github/copilot-instructions.md` | Copilot-specific entry point and genuine tool-specific differences only; shared facts route to the owning doc above instead of being copied. | | Package-local `README.md` (e.g. `packages/message/README.md`, `packages/filesystem/README.md`) | That package's purpose, boundaries, entry points, and local gotchas — not a duplicate of repo-wide architecture or coding policy. | -When you move a fact, move it to the doc that **owns** it and cross-link — never copy the same fact into two -places, or they drift apart. To discover the current full set instead of relying on this table alone, run +This table records **ownership boundaries** — which doc a given fact belongs in. It is deliberately *not* the +index; [`README.md`](./README.md) holds the per-doc contents and "read before X" triggers, so don't restate one +inside the other. When you move a fact, move it to the doc that owns it and cross-link — never copy it into two +places, or they drift apart. To discover the current full set rather than relying on this table, run `git ls-files '*.md'`. ## Checklist 1 — Organization (every doc change) diff --git a/docs/README.md b/docs/README.md index 877151405..f34c011f5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ | [`develop.md`](./develop.md) | 开发规范:命令、目录结构、编码风格、UI/主题、i18n、提交/PR 流程;测试设计/清理口径与运行机制(含 Vitest 性能)拆到 [`references/develop-testing.md`](./references/develop-testing.md)。**写代码前先读。** | | [`pull-request.md`](./pull-request.md) | PR 描述指南:代理与贡献者使用的详细章节、按变更类型取舍规则、验证与审查信息要求。 | | [`design.md`](./design.md) | 设计系统参考:主题机制、shadcn 组件选型、新建页面配方总览;令牌完整值拆到 [`references/design-tokens.md`](./references/design-tokens.md),组件清单拆到 [`references/design-components.md`](./references/design-components.md),布局/响应式/动效/状态/无障碍范式拆到 [`references/design-patterns.md`](./references/design-patterns.md)。**做页面/对话框/区块前先读。** | +| [`../e2e/README.md`](../e2e/README.md) | E2E 测试台手册:两条赛道(committed smoke / gitignored scratch)、浏览器与 profile 隔离、fixtures 与 helper 清单、协议 mock、`E2E_*` 环境变量、产物与失败排查路径。**跑 / 写 E2E 或一次性验证脚本前先读。** | | [`verification.md`](./verification.md) | 功能验证指南:用一次性 scratch 脚本驱动真实扩展做端到端验证(不跑全量 E2E、不加永久用例);报告模板拆到 [`references/verification-report-template.md`](./references/verification-report-template.md),调试 FAQ 拆到 [`references/verification-debugging.md`](./references/verification-debugging.md)。**验证改动是否真正跑通时读。** | | [`architecture.md`](./architecture.md) | 内部原理总览:多进程模型、消息传递;各子系统深入拆到 [`references/architecture-services.md`](./references/architecture-services.md)(服务层)、[`references/architecture-data.md`](./references/architecture-data.md)(数据层)、[`references/architecture-gm-api.md`](./references/architecture-gm-api.md)(GM API)、[`references/architecture-execution.md`](./references/architecture-execution.md)(脚本执行)、[`references/architecture-build.md`](./references/architecture-build.md)(构建管线)、[`references/architecture-agent.md`](./references/architecture-agent.md)(Agent 子系统)。 | | [`cloud-sync.md`](./cloud-sync.md) | 云同步实现说明:同步文件语义、主流程、状态合并、provider 差异、错误分类、retry 策略和维护注意事项。 | @@ -17,11 +18,9 @@ ## 外部接入 / External Access -内置于所有构建、**默认关闭**,从扩展设置开启;经伴随二进制 [`sctl`](https://github.com/scriptscat/sctl)(WebSocket daemon,默认 `127.0.0.1:8643`)通信,不新增浏览器权限、无 native-messaging 主机与安装器。信任扁平:接入(enrollment)一次建立长期密钥 K,CLI 与所有 MCP agent 都继承信任,不再逐客户端配对/scope/撤销。 - | 文档 | 说明 | | --- | --- | -| [`external-access-guide.md`](./external-access-guide.md) | 使用指南:安装 sctl、启用外部接入、一次性接入(`sctl connect` 带外配对码)、写操作/源码读取两条策略、三档决策(拒绝/允许/本会话允许)的实操步骤,附 MCP 工具表、CLI 动词与真实用例。**想实际用起来先读这份。**中文版见 [`external-access-guide_zh-CN.md`](./external-access-guide_zh-CN.md)。 | +| [官网外部接入指南](https://docs.scriptcat.org/docs/use/external-access/) | 面向用户的安装、接入、权限、命令行、MCP、审计和排障指南;由 `scriptcat.org` 统一维护中文、英文和俄文版本。 | | [`sctl` 仓库 `docs/protocol.md`](https://github.com/scriptscat/sctl/blob/main/docs/protocol.md) | 扩展↔daemon 的 JSON-RPC 2.0、握手、能力协商、错误码和阻塞语义。权威 schema 与生成器位于 sctl;本仓库只消费 [`external_access/generated/`](../src/app/service/service_worker/external_access/generated/) 产物。 | | [`sctl` 仓库 `docs/threat-model.md`](https://github.com/scriptscat/sctl/blob/main/docs/threat-model.md) | 威胁模型:两个信任锚点(长期密钥 K + 0600 控制令牌)、扁平信任的取舍、Origin 白名单、攻击面与对策、写路径人工审批 + TOCTOU、落盘凭据一览。 | diff --git a/docs/architecture.md b/docs/architecture.md index a7623dd03..bac32e350 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -43,10 +43,9 @@ Three ideas explain almost everything in the codebase: - **Domain logic lives in services, wired explicitly.** Shared collaborators (a `Group`, `IMessageQueue`, another service, a DAO owned elsewhere) typically come in through the constructor; message handlers and subscriptions are registered through an explicit lifecycle method — commonly `init()`, though content and - Agent code have their own equivalents. The exact dependency set and lifecycle shape vary by service and - context — see [service layer](./references/architecture-services.md) for the real variance and the - exceptions, rather than assuming one uniform constructor/`init()` template. This explicit-wiring style is - what makes the system testable with the mock message bus. + Agent code have their own equivalents. Dependency sets and lifecycle shapes vary per service; the + [service layer](./references/architecture-services.md) documents the variance. This explicit wiring is what + makes the system testable with the mock message bus. ``` ┌───────────────────────────────────────────────┐ @@ -261,21 +260,14 @@ Map a change onto the existing extension points instead of inventing new structu its `init()`, and call it from the other context with a `Client`. No new transport, no new wiring. - **A new broadcast event.** `this.mq.publish("myTopic", payload)` where state changes; `this.mq.subscribe(...)` wherever it matters. Use this, not RPC, for "X changed" notifications. -- **A new persisted entity.** Pick a backend by matching an existing entity with the same data-size/query/lifecycle - needs — `Repo`, `DAO`, or `OPFSRepo` (see [data layer](./references/architecture-data.md#adding-an-entity)) - — then copy the nearest entity of that *same backend, in the same subsystem* for ownership and construction: - they don't all follow "construct in the manager, expose via `group.on`." Some context-service `Repo` - entities do (e.g. `scriptDAO` built in `ServiceWorkerManager` and exposed through RPC); some Agent entities - don't — `AgentModelService` constructs its own `AgentModelRepo` internally by default rather than taking one - from a manager, and `AgentChatRepo` is a module-level singleton (`export const agentChatRepo = ...`) imported - directly rather than manager-constructed or exposed via `group.on`. Only route through the manager + - `group.on` when the entity is genuinely owned by a context composition root and needs to be reachable over - RPC. +- **A new persisted entity.** Pick a backend by matching an existing entity with the same + data-size/query/lifecycle needs — `Repo`, `DAO`, or `OPFSRepo` — then copy that entity's ownership + style too; they don't all get constructed in a manager + ([data layer § Adding an entity](./references/architecture-data.md#adding-an-entity)). - **A new service.** First decide what kind: a context service (owned by one of `content/`, `offscreen/`, `sandbox/`, `service_worker/`), an Agent/core component, or another cross-cutting subsystem — these do - **not** share one constructor shape. Then copy the nearest existing service of that kind. See - [service layer § Adding a service](./references/architecture-services.md#adding-a-service) for the decision - path; don't default to a `Group` + `IMessageQueue` + DAOs constructor for everything. + **not** share one constructor shape. Then copy the nearest existing service of that kind + ([service layer § Adding a service](./references/architecture-services.md#adding-a-service)). - **A new GM API.** Decorate the method with `@GMContext.API` on the content side, add a privileged/offscreen handler if needed, register the `@grant` (see [GM API system](./references/architecture-gm-api.md#adding-a-gm-api-sketch)). @@ -289,13 +281,9 @@ premature abstraction. - **Unit (Vitest + happy-dom).** Co-locate `*.test.ts` next to source. `chrome.*` is mocked via [`@Packages/chrome-extension-mock`](../packages/chrome-extension-mock) (`tests/vitest.setup.ts`). For the - message layer, keep the two pieces separate: `MockMessage` fakes the `Message` **transport** for `Server`/ - `Group` RPC tests (`new Server("test", new MockMessage(...))`); a service's `IMessageQueue` dependency is - tested with a real `MessageQueue` instance (`new MessageQueue()`, with individual methods like `publish` - swapped for a spy when a test needs to assert on it) or a narrow fake written against `IMessageQueue` — not - `MockMessage`, which solves a different problem (see - [architecture-services.md](./references/architecture-services.md) for the detailed distinction). Run one - file: `pnpm test -- --run path/to/file.test.ts`. + message layer, `MockMessage` and `IMessageQueue` are **not** interchangeable — see + [architecture-services.md](./references/architecture-services.md) for which to reach for. Run one file: + `pnpm test -- --run path/to/file.test.ts`. - **TDD.** See [AGENTS.md § Engineering Principles](../AGENTS.md#engineering-principles) for the write-failing- test-first principle and [develop-testing.md § When TDD doesn't apply](./references/develop-testing.md#when-tdd-doesnt-apply) for the narrow exceptions — this section only covers architecture-specific test mechanics, not the policy @@ -304,8 +292,5 @@ premature abstraction. `pnpm run test:e2e:install`). - **Before a PR:** lint + the relevant suite — owned by [references/develop-testing.md](./references/develop-testing.md) → *Testing*. -The DI + interface design is what makes this tractable: because many services take `IMessageQueue`/DAOs -through the constructor (dependency set varies — see [architecture-services.md](./references/architecture-services.md)), -a test builds a service with a real `MessageQueue` (or a narrow fake) and an in-memory DAO and exercises -handlers directly, with no browser. `MockMessage` is for the separate case of testing `Server`/`Group` RPC -routing, not for standing in as a service's `IMessageQueue`. +The DI + interface design is what makes this tractable: a test builds a service with a real `MessageQueue` (or +a narrow fake) and an in-memory DAO, then exercises its handlers directly, with no browser. diff --git a/docs/design.md b/docs/design.md index 6fe767093..ebd220e15 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,6 +1,6 @@ # ScriptCat Design System -> **A reuse-oriented design reference.** It consolidates the visual language that lives in `src/index.css` and the shadcn component layer into one place you can copy from: **color tokens (full light/dark values), the theming mechanism, the component palette, layout & responsive patterns, motion, state patterns, and an end-to-end new-page recipe.** Read this before building or modifying any page, dialog, or block so it stays visually and behaviorally consistent with the rest of the app. +> **A reuse-oriented design reference.** It consolidates the visual language that lives in `src/index.css` and the shadcn component layer into one place you can copy from. Read this before building or modifying any page, dialog, or block so it stays visually and behaviorally consistent with the rest of the app. > **Stack in one line:** React 19 + shadcn/ui (Radix primitives, `new-york` style) + Tailwind CSS v4 + React Router. Colors and motion are defined in the `@theme inline` block of `src/index.css`. **There is no `tailwind.config.js`** (Tailwind v4); PostCSS runs through `@tailwindcss/postcss` (`postcss.config.mjs`); **class names have no prefix** (`bg-background`, not `tw-bg-background`). @@ -8,14 +8,16 @@ ## What this doc owns -| Owned here | Owned elsewhere | -| --- | --- | -| Color-token values, semantics, usage → [`tokens.md`](./references/design-tokens.md) | The hard rules that mandate them (no hard-coded colors, hover via pseudo-classes, `cn()` / CVA / `lucide`) → [`DEVELOP.md` UI section](./develop.md#ui) | -| Theming mechanism, `dark:` usage | Commands, structure, coding style, testing, i18n, commit/PR → [`DEVELOP.md`](./develop.md) | -| Component palette, variants, selection guidance → [`components.md`](./references/design-components.md) | Process model, message passing, service layers, internals → [`ARCHITECTURE.md`](./architecture.md) | -| Layout shell, responsive patterns, **layering (z-index)**, motion, state patterns, **accessibility** → [`patterns.md`](./references/design-patterns.md); **elevation (shadows)** → [`tokens.md`](./references/design-tokens.md#elevation-shadows); page recipe (this doc) | — | +- **This file** — the theming mechanism and `dark:` usage, typography & radius, and the new-page recipe. +- [`references/design-tokens.md`](./references/design-tokens.md) — color-token values, semantics, usage, and + the [elevation (shadow) ladder](./references/design-tokens.md#elevation-shadows). +- [`references/design-components.md`](./references/design-components.md) — component palette, variants, + selection guidance. +- [`references/design-patterns.md`](./references/design-patterns.md) — layout shell, responsive patterns, + layering (z-index), motion, state patterns, accessibility. -This doc restates the `DEVELOP.md` hard rules only where needed, then links back — it does not duplicate them. +The hard rules behind all of it (no hard-coded colors, hover via pseudo-classes, `cn()` / CVA / `lucide`) are +owned by [`develop.md` § UI](./develop.md#ui) — linked from here, never restated. --- @@ -75,7 +77,7 @@ setTheme("auto"); // "auto" follows the system theme and updates on change // ✅ dark: variant only for a dark-specific tweak
-// ❌ Hard-coded colors — break in dark and violate the DEVELOP.md rule +// ❌ Hard-coded colors — break in dark and violate the develop.md rule
``` @@ -130,7 +132,7 @@ When building a new page or dialog, run this checklist to stay consistent: - [ ] **Motion** restrained (`150–250ms`, `ease-out`), hover/focus via pseudo-classes, enter/leave via `data-state`, reuse existing utilities ([motion](./references/design-patterns.md#motion)). - [ ] **Depth** uses the elevation ladder (resting/raised/overlay, [elevation](./references/design-tokens.md#elevation-shadows)) and the z-index ladder (`z-10` chrome / `z-50` floating, [layering](./references/design-patterns.md#layering-z-index)) — no `shadow-2xl`, no magic `z-[…]`. - [ ] **Accessibility:** AA contrast on both themes; meaning never color-only; custom controls keyboard-reachable with a visible focus ring; `aria-label` on icon buttons; ≥ ~44px mobile tap targets; reduced-motion-safe ([accessibility](./references/design-patterns.md#accessibility)). -- [ ] **Copy** defaults to sentence-case English + i18n; verbs on buttons; specific errors ([writing & microcopy](./references/design-patterns.md#writing--microcopy)), and flexes for long locales ([layout & responsive](./references/design-patterns.md#layout--responsive)); see [`DEVELOP.md`](./develop.md) and [`translation.md`](./translation.md). +- [ ] **Copy** defaults to sentence-case English + i18n; verbs on buttons; specific errors ([writing & microcopy](./references/design-patterns.md#writing--microcopy)), and flexes for long locales ([layout & responsive](./references/design-patterns.md#layout--responsive)); see [`develop.md`](./develop.md) and [`translation.md`](./translation.md). Page skeleton (tokens + existing primitives + the shell pattern): @@ -176,6 +178,4 @@ export default function ExamplePage() { - Component primitives → [`src/pages/components/ui/`](../src/pages/components/ui/); shadcn config → [`components.json`](../components.json) - `cn()` → [`src/pkg/utils/cn.ts`](../src/pkg/utils/cn.ts); breakpoint → [`src/pages/components/use-is-mobile.ts`](../src/pages/components/use-is-mobile.ts) -**Related docs:** UI hard rules and commit flow → [`DEVELOP.md`](./develop.md); internals → [`ARCHITECTURE.md`](./architecture.md); doc maintenance and fact-checking → [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md). - > When editing this doc, follow [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md): token values, component names, and variant names track the current branch's `src/` code (if you can't `git grep` it, don't claim it); enumerate counts and lists rather than trusting memory. diff --git a/docs/develop.md b/docs/develop.md index 5de30b575..5696ca0c7 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -53,8 +53,8 @@ flat: a single enrollment establishes the long-term key K, and the CLI and every extension never listens on a port itself. RPC schemas and generators are owned by the sctl repo's `protocol/` directory. ScriptCat consumes generated artifacts under [`external_access/generated/`](../src/app/service/service_worker/external_access/generated/), and -`protocol.conformance.test.ts` guards its runtime types against them. See -[`external-access-guide.md`](./external-access-guide.md) for usage, and the sctl repo's +`protocol.conformance.test.ts` guards its runtime types against them. See the +[External Access user guide](https://docs.scriptcat.org/en/docs/use/external-access/) for usage, and the sctl repo's `docs/protocol.md` / `docs/threat-model.md` for the wire protocol and security design. ## Project Structure & Module Organization @@ -73,10 +73,9 @@ Use strict TypeScript, React JSX runtime, 2-space indentation, semicolons, doubl The project's own custom rules live in `eslint-rules/` at the repo root (wired in `eslint.config.mjs`, **not** `packages/eslint/`, which is the unrelated userscript lint config for the in-app editor) and act as a mechanical -harness for conventions that would otherwise rely on memory. Lint enforces the rules themselves — code violating -them fails CI — but this list of exactly which rule covers which scope, and which are covered by -`eslint-rules/harness.test.mjs`, is hand-maintained prose; re-verify specifics with `grep` rather than trusting -it as settled fact: +harness for conventions that would otherwise rely on memory. Lint enforces the rules themselves — violating code +fails CI; the scope and coverage notes below are hand-maintained prose, so check `eslint.config.mjs` when a +detail matters: - `chrome-error/require-last-error-check` — enforces `chrome.runtime.lastError` handling. Not covered by `harness.test.mjs`. @@ -96,16 +95,12 @@ sonner/radix restriction there — the file imports neither). the `radix-ui` pattern of `no-restricted-imports`, and `no-restricted-syntax` — not `require-last-error-check`, not the `sonner` pattern of `no-restricted-imports`, and not the `tests/vitest.setup.ts` scope. -`src/pages/components/ui/toast.ts` has an override that turns `no-restricted-imports` **entirely off** for that -one file — not just the `sonner` half of it. Only the `sonner` exception is intentional: this is the one place -in `src/pages/**` allowed to import `sonner`'s `toast` directly (it's the wrapper `notify` is built on). The -file happens to also lose the `@radix-ui/react-*` restriction as a side effect of the rule being off wholesale -— it does not currently import from `@radix-ui/react-*` (or `radix-ui`) at all, and the merged-package -convention still applies to it in spirit; `eslint-rules/harness.test.mjs`'s Radix case only exercises -`dialog.tsx`, so a Radix-restricted import landing in `toast.ts` would not be caught by lint today. Don't read -this override as "Radix single-package imports are permitted here" — treat it as a lint gap this file -currently doesn't exploit, and prefer narrowing the override to the `sonner` import specifically (or adding a -`toast.ts` case to the harness) over relying on the blanket `off`. Any other file still gets both restrictions. +`src/pages/components/ui/toast.ts` turns `no-restricted-imports` **entirely off** (`eslint.config.mjs`), but +only the `sonner` half of that is intentional: this is the one place in `src/pages/**` allowed to import +`sonner`'s `toast` directly (it's what `notify` wraps). Also losing the `@radix-ui/react-*` restriction is an +unintended side effect — the file imports no Radix today, and `harness.test.mjs`'s Radix case only exercises +`dialog.tsx`, so lint would not catch one landing here. Treat it as a lint gap rather than permission: narrow +the override to the `sonner` import if you touch this file. Every other file still gets both restrictions. Separately, type-aware rules run on `src/pages/**` (tests excluded) via `projectService` — `@typescript-eslint/no-floating-promises`, `no-misused-promises` (with `checksVoidReturn.attributes: false`, so diff --git a/docs/external-access-guide.md b/docs/external-access-guide.md deleted file mode 100644 index d1d19fc0d..000000000 --- a/docs/external-access-guide.md +++ /dev/null @@ -1,317 +0,0 @@ -

-中文 English -

- -# Using ScriptCat External Access - -A practical, task-oriented guide to connecting an AI agent (Claude Desktop, Claude Code, or any -other [Model Context Protocol](https://modelcontextprotocol.io/) client) — or your own terminal — -to your ScriptCat userscripts, with worked examples of the flows you'll actually hit. - -External Access is **built into every build but ships turned off**; you opt in from the extension's -settings. It talks to a small companion binary, [`sctl`](https://github.com/scriptscat/sctl), -whose WebSocket daemon defaults to `localhost:8643`; the extension connects to it as a client from an -offscreen document. No browser permission is added, and there is no native-messaging host or -installer to register. - -**Trust is flat.** You *enroll* the extension with the daemon exactly once (an out-of-band code you -read from the terminal and type into ScriptCat). After that, the `sctl` CLI **and** every MCP agent -reach the daemon over that one trusted channel and inherit its trust — there is no per-client -pairing, no per-client scopes, and no per-client revocation. What still gates every dangerous action -is a **human decision in the browser**, applied identically to the CLI and to MCP. - -For *why* it's built this way (threat model, handshake, TOCTOU guarantees) see the sctl repo's -[`docs/threat-model.md`](https://github.com/scriptscat/sctl/blob/main/docs/threat-model.md) and -[`docs/protocol.md`](https://github.com/scriptscat/sctl/blob/main/docs/protocol.md); this guide is -the "how do I actually use it" companion. - -## What you get - -Once connected, an AI agent (over MCP) **or** you (over the `sctl` CLI) can: - -- List your installed userscripts and read their metadata (matches, grants, enabled state) — - read-only, no approval needed. -- Read a script's source — the whole file, a **line window**, or just the lines matching a - **search**. All three are gated by the **source-read policy** (approval by default), because - source is your content to disclose. This applies to the CLI too — reading source is a privacy - decision, so `sctl get -o source` and `sctl grep` are **not** exempt. -- **Request** installing a new script, **editing** one, enabling/disabling one, or deleting one. - Every one is a *request*: the call blocks and nothing changes until you decide in a ScriptCat - window that pops up automatically. Installs and edits reuse ScriptCat's normal install page (with - the identity, permissions, code, and version diff you already know); the page's own enable switch - decides the enabled state, so an approved install is usable immediately, just like a normal - install, while for an edit that switch starts where the script already is — an edit never quietly - turns a script off. - -There is no code path from an MCP or CLI request to a script mutation that skips your decision -(unless you deliberately switch a policy to "allow directly" — see below). - -## 1. Prerequisites - -- The `sctl` binary. It's a single self-contained Go binary — no Node, no runtime deps. Build it - from the [`scriptscat/sctl`](https://github.com/scriptscat/sctl) repo: - - ```bash - go build -o sctl ./cmd/sctl - ``` - -- macOS, Linux, or Windows — `sctl` is cross-platform; there is no OS-specific - installer step. - -## 2. Start the daemon - -```bash -sctl serve -``` - -This binds the WebSocket hub on `127.0.0.1:8643` by default and writes a `0600` control-token file -that `sctl` front-ends use. You can override the address with `--listen-address`. You can also skip -this step: `sctl connect`, `sctl mcp`, and the CLI verbs auto-start a detached `serve` if one isn't -already running. - -## 3. Enable External Access in ScriptCat - -Open the extension's options page → **Tools** → **External Access**. Flip the enable switch — a -dialog first explains what you're turning on (agents can list/read metadata freely; everything else -needs your decision). The connection address defaults to `ws://localhost:8643`; you can configure -another `ws://` or `wss://` address without a host/network-target restriction. Status stays "Pending enrollment" until you enroll (next -step). - -## 4. Enroll the extension with the daemon (one time) - -Enrollment is the one and only step that needs an out-of-band code — and it's the whole of trust -setup. Run: - -```bash -sctl connect -``` - -This prints an 8-character one-time code (valid 2 minutes) **in your terminal only** — the code -never travels over the connection. In ScriptCat's External Access card, click **Connect to sctl**, -type the code into the dialog, and confirm. The two sides run a mutual handshake, the daemon hands -the extension a long-term key over an encrypted channel, and the status moves to **Connected**. - -That's it. **The CLI and every MCP agent now inherit this trust** — none of them enroll again. -Re-enrolling replaces the old key (only one extension instance is supported in this version). - -Why an out-of-band code and not "compare two codes on screen"? Anything the daemon sends over the -wire, a malicious local process that forged its Origin could also receive. A code that only ever -exists in your terminal and passes through your eyes and fingers is the one secret such a process -can't get. - -## 5a. Connect an MCP client (Claude Desktop, Claude Code, …) - -No per-client pairing. Once the extension is enrolled, just point your client's MCP config at the -serving command: - -```json -{ - "mcpServers": { - "scriptcat": { "command": "sctl", "args": ["mcp", "--name", "Claude Desktop"] } - } -} -``` - -Restart your client. It lists a `scriptcat` server exposing all script tools; every write and every -source read still stops at your decision in the browser. `--name` is purely an **audit label** — it -attributes this client's requests in ScriptCat's log and nothing more (you can run several client -configs with different names). If the extension isn't enrolled yet, tool calls return an -"extension not connected" error until you run `sctl connect`. - -## 5b. …or just use the CLI - -The `sctl` verbs drive the exact same channel with the same permissions: - -```bash -sctl get # table of installed scripts; -o json for machine-readable output -sctl get # one-line table; -o json for the full metadata -sctl get -o source # raw source to stdout; gated by the source-read policy -sctl get -o source --lines 40-80 # just that line window -sctl grep 'fetch(' # matching lines with line numbers; same source-read gate -sctl install ./my-script.user.js # or a URL; blocks until you decide in the browser -sctl edit --replace 'old code' --with 'new code' -sctl enable -sctl disable -sctl delete # alias: sctl del -``` - -`get`, `grep`, `edit`, `enable`, `disable` and `delete` also take an optional resource word before -the uuid, so `sctl get sc ` reads the same as `kubectl get pods` habits would suggest. -`sctl grep` matches a **literal substring** by default (`*`, `?` and `.` are ordinary characters) — -add `-E` for a regular expression, plus `-i`, `-C N` and `-m N` as you'd expect from `grep`; finding -nothing exits **0**, not 1. - -Write verbs block until you decide; **Ctrl-C** cancels the request (the browser confirm page is -dismissed). Exit codes: **0** approved/ok, **1** you rejected, **2** voided (timeout / Ctrl-C / -disconnect), **3** other error. - -## 6. Policies and per-decision choices - -Two **global policies** live in the External Access card, and both apply identically to the CLI and -to MCP: - -- **Write policy** — *Require approval* (default) blocks every install / toggle / delete on a - confirm surface; *Allow directly* runs write requests immediately (an amber warning marks this as - a safety downgrade). -- **Source-read policy** — *Require approval* (default) blocks each source read on a disclosure - prompt; *Allow directly* returns source without a prompt. - -When a policy is set to *Require approval*, the confirm surface offers a **three-tier decision**: - -- **Reject** — this request only. -- **Allow** — this request only. -- **Allow this session** — stop asking for **this script and this operation** for the rest of the - **extension session** (it's keyed to the browser/extension session, not to an MCP connection or a - CLI process, which is exactly why the CLI and MCP share one notion of "session"). It resets on - browser restart, extension reload, or when you stop External Access. - -To stop being asked entirely, switch the matching policy to *Allow directly* (global, affects CLI -and MCP at once). - -## Available MCP tools - -| Tool | What it needs from you | Write? | -|---|---|---| -| `scripts_list` | nothing | No | -| `scripts_metadata_get` | nothing | No | -| `scripts_source_get` | a source-disclosure decision (unless the source-read policy is "allow directly") | No | -| `scripts_source_grep` | the same source-disclosure decision — matching lines are source | No | -| `scripts_install_request` | an install decision on the install page (unless the write policy is "allow directly") | Yes | -| `scripts_edit_request` | an update decision on the install page, with a line-by-line diff (unless the write policy is "allow directly") | Yes | -| `scripts_toggle_request` | a toggle decision (unless the write policy is "allow directly") | Yes | -| `scripts_delete_request` | a hold-to-confirm delete decision (unless the write policy is "allow directly") | Yes | - -`scripts_source_get` takes an optional `startLine`/`endLine` window so an agent can page through a -large script instead of spending its whole context on one read; `scripts_source_grep` finds the -lines worth reading first. `scripts_edit_request` anchors on content (`oldText` → `newText`, matched -literally and required to be unique unless `replaceAll` is set), so the agent never has to hold — -or send back — the whole file to change one function. Edits are applied in order, and each one -searches the result of the preceding edit. - -Write tools are **blocking**: the call suspends until you decide (there is no operation-polling API — -the result comes back on the same call). While it waits, the MCP server sends progress notifications -so clients don't time out; if the client disconnects or times out, the operation is voided and its -confirm surface invalidated. - -## Case studies - -### Case 1 — "What userscripts do I have installed, and which are enabled?" - -Read-only, works the moment you're enrolled: - -> **You:** What userscripts do I have installed right now? -> **Agent:** *calls `scripts_list`* → an array of `{ uuid, name, type, enabled, updatedAt, -> hasUpdateUrl, … }` — no source, and only whether an update URL exists (metadata-tier, not -> secrets). -> **Agent:** "You have 12 scripts installed; 9 are enabled." - -No prompt appears — it's exactly as safe as looking at the Scripts list yourself. (The same answer -from your terminal: `sctl get`.) - -### Case 2 — "Find and fix a bug in my auto-login script" - -This is the flow that hits both gates — the disclosure gate to read, then the write gate to change: - -> **You:** There's a bug in my "Auto Login" script — can you find and fix it? -> **Agent:** *calls `scripts_list`*, finds the uuid, *calls `scripts_metadata_get`* to confirm, then -> *calls `scripts_source_grep`* with `{ uuid, query: "password", contextLines: 3 }` to locate the -> login handler rather than pulling in a 2000-line file. -> **Result:** with the source-read policy on *Require approval*, the search blocks. ScriptCat pops -> up a confirm page — **Read script source**, with the script's name and *"Reading source exposes -> the script’s content. Confirm you trust this request."* — offering **Reject**, **Allow this -> session**, **Allow**. -> -> - **Allow** — this search succeeds; the *next* read or search prompts again. -> - **Allow this session** — this and every future read *or search* of *this script* succeeds with -> no further prompt until the extension session ends. Both share one gate, so one decision covers -> both. -> -> Say you pick "Allow this session." The call returns the matching lines with their line numbers, -> and the agent *calls `scripts_source_get`* with `{ uuid, startLine: 180, endLine: 240 }` to read -> just that region — no second prompt this time. -> -> **The fix:** the agent *calls `scripts_edit_request`* with a single edit whose `oldText` is the -> buggy two lines and whose `newText` is the corrected version. It never sends the file back, and it -> never had to hold the whole file. ScriptCat's install page opens in its update mode with a banner -> — *"Requested to update this script via External Access — your decision is required."* — showing a -> line-by-line diff against your current code, the permission card (so a fix that adds a `@grant` or -> a `@connect` is visible as a permission change, not buried in the diff), and an expandable content -> SHA-256. The enable switch starts where the script already is. Approve and the fix is live. -> -> If the anchor doesn't match — the text isn't there, or appears at more than one position — the -> call fails with `INVALID_REQUEST` **before** any page opens, and the agent adds surrounding lines -> and retries. Nothing is staged and you are not interrupted. - -The same thing from your terminal: - -```bash -sctl grep password -C 3 -sctl get -o source --lines 180-240 -sctl edit --replace @old.txt --with @new.txt -``` - -### Case 3 — "Turn off the script that's breaking this site while I debug it" - -> **You:** Disable my "Ad Blocker Tweaks" script for now. -> **Agent:** *calls `scripts_list`* to find the uuid, then `scripts_toggle_request` with -> `{ uuid, enable: false }`. -> **Result:** with the write policy on *Require approval*, ScriptCat opens a lightweight confirm page -> (script name, "triggered via External Access", Reject / Allow this session / Allow). You allow → -> the toggle runs → the blocking call returns success. -> -> Between your decision and the actual disable, ScriptCat re-checks that the script's code hasn't -> changed since the request (TOCTOU protection) — if you'd edited it meanwhile you'd get `CONFLICT`, -> and the agent would make a fresh request. - -### Case 4 — "Clean up scripts I don't use anymore" - -> **You:** Delete the three scripts I haven't used in months: X, Y, Z. -> **Agent:** calls `scripts_delete_request` three times, once per uuid. -> **Result:** the requests block and their confirm pages are shown **one at a time** (concurrent -> writes queue). Each Delete needs a **press-and-hold for 1.5 s** — harder to fumble than a click, -> since deletion also removes the script's stored values and isn't undoable. You can reject any -> independently. If you close a confirm page by mistake, the request stays pending — reopen it from -> the **Awaiting confirmation** entry. - -### Case 5 — Stopping access when you're done - -> Tools → External Access → **Stop External Access**. This discards the long-term key, so every -> downstream client (CLI and every MCP agent) loses trust at once, clears any "allow this session" -> grants, and flips the enable switch off. To connect again later, enroll once more with -> `sctl connect`. (To cut off a single agent without stopping everything, remove the `scriptcat` -> server from that agent's own MCP config.) - -## Auditing what happened - -External Access records each operation through ScriptCat's existing **logger** under the -`external-access` component — allowed or denied, with the client's self-reported label, the action, and -the outcome. It never contains tokens or source. The card has a **View audit log** button that -deep-links to the Logs page pre-filtered to `component = external-access`, where you get all the usual -filters (level, time, text). The self-reported client name is recorded for forensics only; because -it's unauthenticated and forgeable, it never appears on an approval screen. - -## Troubleshooting - -| Symptom | Likely cause | -|---|---| -| Status stuck on "Connecting…" then "connection failed" | The daemon isn't running or is on a different address. Start `sctl serve` (or run any `sctl` command), and check the connection address in the card matches. | -| Enrollment never completes | `sctl connect` codes last 2 minutes; External Access must be enabled and the daemon reachable so ScriptCat can run the handshake. Read the code from the terminal and type it into the dialog before it expires. | -| `sctl mcp` tool calls return "extension not connected" | The extension isn't enrolled (or you stopped External Access). Run `sctl connect` and complete enrollment, then retry. | -| `scripts_source_get` prompts again after you approved | You chose "Allow" and the agent made a second read — expected; approve again, or pick "Allow this session". | -| A CLI write exits `2` | The request was voided — you (or the client) timed out, Ctrl-C'd, or the extension disconnected before you decided. | - -## What External Access deliberately does and doesn't do - -- The daemon **defaults** to a loopback WebSocket listener (`127.0.0.1:8643`), but the address is - configurable and ScriptCat does not enforce a loopback-only target. An **Origin whitelist** cheaply rejects ordinary web pages - (a browser stamps the Origin and page JS can't forge it), but a non-browser process can forge any - Origin, so every connection must still pass a bidirectional HMAC handshake before any business - message; an unauthenticated socket is dropped after 5 s with no information leaked. The handshake - is the real gate. -- It treats the client's self-reported name as an **audit label only** — flat trust doesn't - authenticate client identity, so the name never gates anything and never appears on an approval - screen. What authorizes a request is that it arrived over the enrolled channel; what bounds damage - is your per-operation decision. -- It can't defend against another process already running as your own OS user reading the daemon key - or control token (both `0600`) — a documented, accepted residual limitation, not a bug. See the - sctl [`docs/threat-model.md`](https://github.com/scriptscat/sctl/blob/main/docs/threat-model.md). diff --git a/docs/external-access-guide_zh-CN.md b/docs/external-access-guide_zh-CN.md deleted file mode 100644 index 1a471af7e..000000000 --- a/docs/external-access-guide_zh-CN.md +++ /dev/null @@ -1,265 +0,0 @@ -

-中文 English -

- -# 使用 ScriptCat 外部接入 - -一份面向实际任务的指南:把 AI 代理(Claude Desktop、Claude Code,或任何其他 -[Model Context Protocol](https://modelcontextprotocol.io/) 客户端)——或者你自己的终端——接入你的 -ScriptCat 用户脚本,并配有你真正会遇到的流程范例。 - -外部接入**内置于每个构建、但默认关闭**;你在扩展设置里主动开启。它对接一个很小的伴随二进制 -[`sctl`](https://github.com/scriptscat/sctl),其 WebSocket daemon 默认运行在 `localhost:8643`; -扩展从 offscreen 文档作为客户端连上它。不新增任何浏览器权限,也没有 native-messaging host 或安装器要注册。 - -**信任是扁平的。** 你只需把扩展与 daemon **接入(enrollment)一次**(一个带外配对码:你从终端读到、 -输入进 ScriptCat)。此后,`sctl` CLI **与**每一个 MCP 代理都经这条可信通道继承信任——没有逐客户端配对、 -没有逐客户端 scope、也没有逐客户端撤销。真正为每个危险操作把关的,是**浏览器里的人工决策**,对 CLI 与 -MCP 一视同仁。 - -想了解**为什么这么设计**(威胁模型、握手、TOCTOU 保证),见 sctl 仓库的 -[`docs/threat-model.md`](https://github.com/scriptscat/sctl/blob/main/docs/threat-model.md) 与 -[`docs/protocol.md`](https://github.com/scriptscat/sctl/blob/main/docs/protocol.md);本指南是「我到底怎么用」的 -配套。 - -## 你能得到什么 - -接入后,AI 代理(经 MCP)**或**你(经 `sctl` CLI)可以: - -- 列出已安装的用户脚本、读取其元数据(匹配、grant、启用状态)——只读,无需批准。 -- 读取脚本源码——整份、一个**行窗口**,或只取**搜索**命中的那些行。三者同由**源码读取策略**把关 - (默认需批准),因为源码是该由你决定是否披露的内容。这对 CLI 也一样:读源码是隐私决策,所以 - `sctl get -o source` 与 `sctl grep` **不豁免**。 -- **请求**安装新脚本、**编辑**某脚本、启用/停用某脚本、或删除某脚本。每一个都是*请求*:调用会阻塞, - 在你于自动弹出的 ScriptCat 窗口里做出决策前什么都不会改变。安装与编辑复用 ScriptCat 常规的脚本安装页 - (带你已熟悉的身份、权限、代码与版本 diff);启用状态由该页自身的开关决定,因此批准后的安装即刻可用、 - 与普通安装一致,而编辑时这个开关的初值就是脚本当前的状态——编辑绝不会悄悄把脚本关掉。 - -从 MCP 或 CLI 请求到脚本变更之间,没有绕过你决策的代码路径(除非你**有意**把某条策略切成「直接允许」—— -见下文)。 - -## 1. 前置条件 - -- `sctl` 二进制。它是单个自包含 Go 二进制——无 Node、无运行时依赖。从 - [`scriptscat/sctl`](https://github.com/scriptscat/sctl) 仓库构建: - - ```bash - go build -o sctl ./cmd/sctl - ``` - -- macOS、Linux 或 Windows——`sctl` 跨平台,无平台专属安装步骤。 - -## 2. 启动 daemon - -```bash -sctl serve -``` - -默认会在 `127.0.0.1:8643` 上绑定 WebSocket hub,并写出一个 `0600` 控制令牌文件供 `sctl` 前端使用。 -可用 `--listen-address` 覆盖监听地址。你也可以跳过这步:`sctl connect`、`sctl mcp` 与各 CLI 动词在未运行时 -会自动拉起一个 detached 的 `serve`。 - -## 3. 在 ScriptCat 中开启外部接入 - -打开扩展选项页 → **工具** → **外部接入**。拨动启用开关——会先弹一个对话框说明你正在开启什么(代理可自由 -列出脚本/读元数据;其余都需你决策)。连接地址默认 `ws://localhost:8643`;你可以配置其他 `ws://` 或 `wss://` -地址,ScriptCat 不限制主机或网络目标。状态会停在「待接入」,直到你完成接入(下一步)。 - -## 4. 将扩展与 daemon 接入(仅一次) - -接入是唯一需要带外配对码的环节——也是信任建立的全部。运行: - -```bash -sctl connect -``` - -这会**只在你的终端里**打印一个 8 位一次性配对码(2 分钟内有效)——该码绝不经连接传输。在 ScriptCat 的 -外部接入卡片里,点**接入 sctl**,把码输入对话框并确认。两端跑一次双向握手,daemon 经加密通道把长期密钥交给 -扩展,状态转为**已接入**。 - -就这样。**CLI 与每个 MCP 代理现在都继承了这份信任**——它们都不再各自接入。重新接入会替换旧密钥(本版本只 -支持一个扩展实例)。 - -为什么用带外配对码,而不是「两边对照同一个码」?任何 daemon 经线路发出的东西,一个伪造了 Origin 的恶意本机 -进程也能收到。只在你终端里存在、经你的眼睛与手指传递的码,才是这种进程拿不到的秘密。 - -## 5a. 接入一个 MCP 客户端(Claude Desktop、Claude Code……) - -无逐客户端配对。扩展一旦接入,直接把客户端的 MCP 配置指向服务命令即可: - -```json -{ - "mcpServers": { - "scriptcat": { "command": "sctl", "args": ["mcp", "--name", "Claude Desktop"] } - } -} -``` - -重启客户端。它会列出一个 `scriptcat` server,暴露全部脚本工具;每次写入、每次读源码仍会停在浏览器里等你 -决策。`--name` 纯粹是**审计标签**——它只在 ScriptCat 日志里标注这个客户端的请求,别无他用(你可以用不同名字 -跑多份客户端配置)。若扩展尚未接入,工具调用会返回「扩展未连接」错误,直到你运行 `sctl connect`。 - -## 5b. ……或者直接用 CLI - -`sctl` 动词驱动的是同一条通道,权限完全相同: - -```bash -sctl get # 已安装脚本表格;-o json 输出机器可读结果 -sctl get # 单行表格;完整元数据用 -o json -sctl get -o source # 源码原样输出到 stdout;受源码读取策略把关 -sctl get -o source --lines 40-80 # 只取这个行窗口 -sctl grep 'fetch(' # 命中行带行号;与读源码同一道闸门 -sctl install ./my-script.user.js # 或一个 URL;阻塞至你在浏览器决策 -sctl edit --replace '旧代码' --with '新代码' -sctl enable -sctl disable -sctl delete # 别名:sctl del -``` - -`get`、`grep`、`edit`、`enable`、`disable`、`delete` 还接受一个写在 uuid 之前、可省略的资源词,于是 -`sctl get sc ` 读起来正合 `kubectl get pods` 的肌肉记忆。`sctl grep` 默认按**纯字面量子串**匹配 -(`*`、`?`、`.` 都只是普通字符)——要正则加 `-E`,另有 `-i`、`-C N`、`-m N`,含义与 `grep` 一致;零命中时 -退出码是 **0** 而非 1。 - -写动词会阻塞至你决策;**Ctrl-C** 取消该请求(浏览器确认页随之关闭)。退出码:**0** 批准/成功、**1** 你拒绝、 -**2** 作废(超时 / Ctrl-C / 断开)、**3** 其他错误。 - -## 6. 策略与逐次决策 - -外部接入卡片里有两条**全局策略**,二者对 CLI 与 MCP 一视同仁: - -- **写操作策略**——*需人工审批*(默认)会把每次安装 / 启停 / 删除挡在确认面上;*直接允许*则让写请求立即 - 执行(琥珀警示标明这是安全降级)。 -- **源码读取策略**——*需人工审批*(默认)会把每次读源码挡在披露提示上;*直接允许*则不提示直接返回源码。 - -当某条策略为*需人工审批*时,确认面给出**三档决策**: - -- **拒绝**——仅此次请求。 -- **允许**——仅此次请求。 -- **本会话允许**——在剩余的**扩展会话**内,对**这一脚本、这一操作**不再询问(它锚在浏览器/扩展会话,而非 - 某条 MCP 连接或某个 CLI 进程——这正是 CLI 与 MCP 能共享同一个「会话」概念的原因)。浏览器重启、扩展重载, - 或你停止外部接入时它都会清除。 - -想彻底不被询问 → 把对应策略切到*直接允许*(全局,同时对 CLI 与 MCP 生效)。 - -## 可用的 MCP 工具 - -| 工具 | 需要你做什么 | 写? | -|---|---|---| -| `scripts_list` | 无 | 否 | -| `scripts_metadata_get` | 无 | 否 | -| `scripts_source_get` | 一次源码披露决策(除非源码读取策略为「直接允许」) | 否 | -| `scripts_source_grep` | 同一次源码披露决策——命中行本身就是源码 | 否 | -| `scripts_install_request` | 在安装页做一次安装决策(除非写操作策略为「直接允许」) | 是 | -| `scripts_edit_request` | 在安装页做一次带逐行 diff 的更新决策(除非写操作策略为「直接允许」) | 是 | -| `scripts_toggle_request` | 一次启停决策(除非写操作策略为「直接允许」) | 是 | -| `scripts_delete_request` | 一次长按确认的删除决策(除非写操作策略为「直接允许」) | 是 | - -`scripts_source_get` 接受可选的 `startLine`/`endLine` 行窗口,让代理能对大脚本分页读、而不是把整个上下文 -花在一次读取上;`scripts_source_grep` 则先替它找出值得读的那几行。`scripts_edit_request` 按内容锚定 -(`oldText` → `newText`,逐字匹配,未开 `replaceAll` 时必须唯一命中),于是代理既不必持有、也不必回传整份 -文件就能改掉一个函数。edit 会按顺序应用,每一条都在前一条 edit 的结果上继续搜索。 - -写工具是**阻塞**的:调用挂起至你决策(没有操作轮询 API——结果在同一次调用里返回)。等待期间 MCP server 会发 -progress 通知以防客户端超时;若客户端断开或超时,操作作废、其确认面失效。 - -## 案例 - -### 案例 1 —— 「我装了哪些用户脚本,哪些是启用的?」 - -只读,接入后立即可用: - -> **你:** 我现在装了哪些用户脚本? -> **代理:** *调用 `scripts_list`* → 一个 `{ uuid, name, type, enabled, updatedAt, hasUpdateUrl, … }` 数组—— -> 无源码,且只告知是否存在更新 URL(元数据层,非机密)。 -> **代理:** 「你装了 12 个脚本;其中 9 个已启用。」 - -不会弹任何提示——和你自己看脚本列表一样安全。(终端里同样的答案:`sctl get`。) - -### 案例 2 —— 「找出并修复我自动登录脚本里的 bug」 - -这是会同时触发两道闸门的流程——读要过披露闸门,改要过写入闸门: - -> **你:** 我的「Auto Login」脚本有个 bug——能帮我找出并修好吗? -> **代理:** *调用 `scripts_list`* 找到 uuid,*调用 `scripts_metadata_get`* 确认,再 *调用 -> `scripts_source_grep`*,参数 `{ uuid, query: "password", contextLines: 3 }`,以定位登录处理函数, -> 而不是把一个 2000 行的文件整份拉进来。 -> **结果:** 源码读取策略处于*需人工审批*时,搜索会阻塞。ScriptCat 弹出确认页——标题**读取脚本源码**, -> 带脚本名与*「读取源码会暴露脚本内容,请确认你信任此请求。」*——并给出**拒绝**、**本会话允许**、**允许**。 -> -> - **允许**——此次搜索成功;*下一次*读取或搜索会再次提示。 -> - **本会话允许**——此次以及之后对*这一脚本*的每次读取*或搜索*都不再提示,直到扩展会话结束。二者共用 -> 同一道闸门,所以一个决策同时覆盖两者。 -> -> 假设你选「本会话允许」。调用返回命中行及其行号,代理接着 *调用 `scripts_source_get`*,参数 -> `{ uuid, startLine: 180, endLine: 240 }`,只读那一段——这次不再提示。 -> -> **修复:** 代理 *调用 `scripts_edit_request`*,只带一条 edit:`oldText` 是出 bug 的那两行,`newText` 是 -> 改好的版本。它既不回传整份文件,也从未需要持有整份文件。ScriptCat 的安装页以更新态打开,顶部带横幅—— -> *「通过「外部接入」请求更新此脚本 —— 需你决定。」*——展示与你当前代码的逐行 diff、权限卡(于是一个顺手加了 -> `@grant` 或 `@connect` 的修复会作为权限变更被看见,而不是埋在 diff 里),以及可展开的内容 SHA-256。启用 -> 开关的初值就是脚本当前的状态。批准后修复即刻生效。 -> -> 若锚点对不上——文本不存在,或在多于一个位置出现——调用会在**任何页面弹出之前**以 `INVALID_REQUEST` 失败, -> 代理据此补上前后文再试。什么都不会被暂存,你也不会被打扰。 - -终端里的同一件事: - -```bash -sctl grep password -C 3 -sctl get -o source --lines 180-240 -sctl edit --replace @old.txt --with @new.txt -``` - -### 案例 3 —— 「先关掉那个把这个网站搞坏的脚本,我调一下」 - -> **你:** 先把我的「Ad Blocker Tweaks」停用。 -> **代理:** *调用 `scripts_list`* 找到 uuid,再 `scripts_toggle_request`,参数 `{ uuid, enable: false }`。 -> **结果:** 写操作策略处于*需人工审批*时,ScriptCat 弹出一个轻量确认页(脚本名、「通过外部接入触发」、 -> 拒绝 / 本会话允许 / 允许)。你允许 → 停用执行 → 阻塞调用返回成功。 -> -> 在你决策与实际停用之间,ScriptCat 会复核脚本代码自请求以来未被改动(TOCTOU 保护)——若你其间编辑过它, -> 会得到 `CONFLICT`,代理据此重新发起请求。 - -### 案例 4 —— 「清理我不再用的脚本」 - -> **你:** 删掉这三个我几个月没用的:X、Y、Z。 -> **代理:** 调用 `scripts_delete_request` 三次,每个 uuid 一次。 -> **结果:** 这些请求阻塞,其确认页**逐个**显示(并发写入会排队)。每次删除需**长按 1.5 秒**——比点击更难误触, -> 因为删除还会移除脚本存储的值且不可撤销。你可以独立拒绝任何一个。若你误关了某个确认页,该请求仍处于待决—— -> 从**待确认**入口重新打开它。 - -### 案例 5 —— 用完后停止接入 - -> 工具 → 外部接入 → **停止外部接入**。这会废弃长期密钥,于是每个下游客户端(CLI 与每个 MCP 代理)同时失去 -> 信任,清除所有「本会话允许」授权,并把启用开关关掉。之后想再连,用 `sctl connect` 再接入一次。(若只想切断 -> 某一个代理而不停止全部,从那个代理自己的 MCP 配置里移除 `scriptcat` server 即可。) - -## 审计发生了什么 - -外部接入通过 ScriptCat 现有的**日志系统**、以 `external-access` 组件记录每次操作——允许或拒绝,附客户端自报 -标签、动作与结果。它绝不含令牌或源码。卡片里有一个**查看审计日志**按钮,深链到已按 `component = external-access` -预过滤的日志页,你在那里能用上全部常规筛选(级别、时间、正文)。自报的客户端名只用于取证;因为它未经认证、 -可伪造,永不出现在审批界面上。 - -## 排障 - -| 症状 | 可能原因 | -|---|---| -| 状态卡在「连接中…」然后「连接失败」 | daemon 没在运行,或在另一个地址上。启动 `sctl serve`(或跑任意 `sctl` 命令),并核对卡片里的连接地址一致。 | -| 接入始终不完成 | `sctl connect` 的码 2 分钟有效;外部接入必须已启用且 daemon 可达,ScriptCat 才能跑握手。从终端读到码并在它过期前输入对话框。 | -| `sctl mcp` 工具调用返回「扩展未连接」 | 扩展尚未接入(或你停止了外部接入)。运行 `sctl connect` 完成接入后重试。 | -| 批准后 `scripts_source_get` 又提示 | 你选了「允许」而代理又读了一次——符合预期;再批准一次,或改选「本会话允许」。 | -| CLI 写操作退出码 `2` | 请求被作废——你(或客户端)超时、Ctrl-C、或扩展在你决策前断开了。 | - -## 外部接入有意做与不做的事 - -- daemon **默认**监听 loopback (`127.0.0.1:8643`),但监听地址可配置,ScriptCat 也不强制连接目标必须是 - loopback。**Origin 白名单**廉价地挡掉普通网页(浏览器盖章 Origin、页面 JS 无法伪造),但非浏览器进程能伪造任意 - Origin,因此每条连接在发送任何业务消息前仍须通过双向 HMAC 握手;未认证的 socket 在 5 秒后被丢弃、不泄露 - 任何信息。握手才是真正的闸门。 -- 它把客户端自报的名字当作**审计标签**——扁平信任不认证客户端身份,故该名字既不 gate 任何东西、也不出现在 - 审批界面上。授权一个请求的是「它来自已接入的通道」;限制损害的是你的逐操作决策。 -- 它无法防御另一个以你自己 OS 用户身份运行的进程读取 daemon 密钥或控制令牌(均 `0600`)——一个有记录、已接受 - 的残余局限,而非 bug。见 sctl - [`docs/threat-model.md`](https://github.com/scriptscat/sctl/blob/main/docs/threat-model.md)。 diff --git a/docs/references/architecture-agent.md b/docs/references/architecture-agent.md index 3055d3ddb..bf9164240 100644 --- a/docs/references/architecture-agent.md +++ b/docs/references/architecture-agent.md @@ -28,8 +28,7 @@ uniformly: | `AgentDomService` | `dom.ts` (+ `dom_cdp.ts` helpers) | Page automation — see below for the default-vs-trusted split. | | `AgentOPFSService` | `opfs_service.ts` | Serves `CAT.agent.opfs` requests from both content scripts (no Blob support) and offscreen (Blob support), dispatched on whether the caller has a `sender`. | -(Class names verified via `git grep -n "export class" -- src/app/service/agent/service_worker/` — re-run that -before relying on a name here, since these are exactly the kind of detail that drifts.) +Current set: `git grep -n "export class" -- src/app/service/agent/service_worker/`. Message actions are namespaced under the `agent` group (`this.api.group("agent")`), the same RPC pattern `architecture.md` describes for other services — the difference here is internal composition, not the wiring @@ -66,10 +65,9 @@ until the model stops calling tools or `maxIterations` is hit. It depends on inj `autoCompact` functions (rather than importing a concrete client) so tests can substitute spies. [`retry_utils.ts`](../../src/app/service/agent/service_worker/retry_utils.ts)'s `isRetryableError` matches an error message containing `429`, a `5xx` code, or a network-ish signal (`network`/`fetch`/`ECONNRESET`), then -excludes it if the message also matches `400`, `401`, `403`, or `404` — it does not blanket-exclude every 4xx -status, just those four specific codes (other 4xx codes simply don't match the retry-trigger pattern in -practice unless their message happens to contain one of the trigger substrings). `withRetry` then applies -exponential backoff, aborting immediately if the caller's `AbortSignal` fires. Context-window overflow triggers auto-compaction +excludes it if the message also matches `400`, `401`, `403`, or `404` — those four codes specifically, not +every 4xx. `withRetry` then applies exponential backoff, aborting immediately if the caller's `AbortSignal` +fires. Context-window overflow triggers auto-compaction (`compact_service.ts` / `core/compact_prompt.ts`) before the loop continues. Provider-specific request/response shaping lives under `core/providers/` (`anthropic.ts`, `openai.ts`, `registry.ts`), keeping the orchestrator provider-agnostic. @@ -136,15 +134,13 @@ The Agent subsystem does not use one persistence pattern; pick by data shape, ma ## Tests -Test file names in `service_worker/` don't all mirror their source file 1:1 — some group by behavior instead -(e.g. `background.test.ts` covers `background_session_manager.ts`, `retry.test.ts` covers `retry_utils.ts`, -`autocompact.test.ts` covers the compaction trigger path). At the time of writing, `agent.ts`, `task_service.ts`, -`compact_service.ts`, and `model_service.ts` don't have an obviously corresponding test file by name — don't -infer full coverage from this table; run `git ls-tree --name-only HEAD src/app/service/agent/service_worker/ | -grep test` for the current test inventory and compare it against the source list above before relying on -either "it's tested" or "it's untested." `core/` follows the same co-located `*.test.ts` convention and the -same caveat applies. Vitest conventions generally: see -[`../references/develop-testing.md`](./develop-testing.md). +Test file names in `service_worker/` don't all mirror their source 1:1 — some group by behavior instead +(`background.test.ts` covers `background_session_manager.ts`, `retry.test.ts` covers `retry_utils.ts`, +`autocompact.test.ts` covers the compaction trigger path), so a missing `.test.ts` isn't proof of a +coverage gap either way. Current inventory: +`git ls-tree --name-only HEAD src/app/service/agent/service_worker/ | grep test`. `core/` follows the same +co-located `*.test.ts` convention. Vitest conventions generally: +[`develop-testing.md`](./develop-testing.md). ## Extending the Agent subsystem diff --git a/docs/references/architecture-build.md b/docs/references/architecture-build.md index 87f966311..29bbcb2de 100644 --- a/docs/references/architecture-build.md +++ b/docs/references/architecture-build.md @@ -13,8 +13,7 @@ UI pages (React): popup · options · install · batchupdate · confirm · impor workers : editor.worker · ts.worker · json.worker (Monaco) · linter.worker ``` -For the exact current set, check [`rspack.config.ts`](../../rspack.config.ts)'s `entry` block directly — this -list is a snapshot and can drift when an entry is added or removed. +For the exact current set, read [`rspack.config.ts`](../../rspack.config.ts)'s `entry` block. Output goes to `dist/ext/src/[name].js` (cleaned each build). Notable behavior: diff --git a/docs/references/architecture-data.md b/docs/references/architecture-data.md index 4eeeba92a..a6534f85a 100644 --- a/docs/references/architecture-data.md +++ b/docs/references/architecture-data.md @@ -81,8 +81,7 @@ Names ending in `DAO` don't all share one base class — check which backend bef |---|---|---| | `TrashScriptDAO` | [`trash_script.ts`](../../src/app/repo/trash_script.ts) | Purpose-built for the trash/restore flow; doesn't extend `Repo` or `DAO` | -This list is a snapshot — for the current exact set, run `git grep -n -E 'class [A-Za-z0-9_]*(DAO|Repo)' -- -src/app/repo`. +Current exact set: `git grep -nE 'class [A-Za-z0-9_]*(DAO|Repo)' -- src/app/repo`. ### Adding an entity @@ -106,6 +105,14 @@ export class MyEntityDAO extends Repo { } ``` -Then create it in the manager (`enableCache()` if hot), and expose operations via `group.on(...)`. For `DAO` -or `OPFSRepo`, follow the nearest existing entity of that backend instead (e.g. `LoggerDAO` for Dexie, -`AgentChatRepo`/`SkillRepo` for OPFS) — their construction and access patterns differ from `Repo`. +For `DAO` or `OPFSRepo`, follow the nearest existing entity of that backend instead (e.g. `LoggerDAO` for +Dexie, `AgentChatRepo`/`SkillRepo` for OPFS) — their construction and access patterns differ from `Repo`. + +**Then pick an owner — "construct in the manager, expose via `group.on`" is one option, not the rule.** Route +through a context composition root only when the entity is genuinely owned by it *and* has to be reachable over +RPC — that's why `scriptDAO` is built in `ServiceWorkerManager` +([`index.ts`](../../src/app/service/service_worker/index.ts)). Agent entities mostly aren't: +`AgentModelService` falls back to `new AgentModelRepo()` internally when no repo is injected +([`model_service.ts`](../../src/app/service/agent/service_worker/model_service.ts)), and `AgentChatRepo` is a +module-level singleton (`export const agentChatRepo` in [`agent_chat.ts`](../../src/app/repo/agent_chat.ts)) +imported directly. Copy the ownership style of the nearest entity on the *same backend in the same subsystem*. diff --git a/docs/references/architecture-gm-api.md b/docs/references/architecture-gm-api.md index 8f4f6db68..8282aae48 100644 --- a/docs/references/architecture-gm-api.md +++ b/docs/references/architecture-gm-api.md @@ -67,10 +67,10 @@ work. `@PermissionVerify.API(...)`. 4. If it needs DOM, route through the offscreen GM API instead: [`src/app/service/offscreen/gm_api.ts`](../../src/app/service/offscreen/gm_api.ts). -5. Register the `@grant` so the linter and the context builder recognize it — the grant/compat map lives in - [`packages/eslint/compat-grant.js`](../../packages/eslint/compat-grant.js) (not just "the `eslint` package" - generally; that package also ships unrelated compat tables like `compat-headers.js`, and - `linter-config.ts` holds the ESLint `rules`/`globals`/`env` config, no grant data). +5. Register the `@grant` so the linter and the context builder recognize it. The grant/compat map is + specifically [`packages/eslint/compat-grant.js`](../../packages/eslint/compat-grant.js) — not that package's + other tables (`compat-headers.js`, or `linter-config.ts`, which holds `rules`/`globals`/`env` and no grant + data). ### Agent/CAT API is the same recipe with dotted grants diff --git a/docs/references/architecture-services.md b/docs/references/architecture-services.md index 9c7ca8d81..a5a67c7b0 100644 --- a/docs/references/architecture-services.md +++ b/docs/references/architecture-services.md @@ -2,7 +2,7 @@ ## The Service Layer -`src/app/service/` holds two kinds of things, not one uniform pattern: +`src/app/service/` holds three kinds of things, not one uniform pattern: - **Context services** — `content/`, `offscreen/`, `sandbox/`, `service_worker/` — split by the runtime context they execute in. Shared or externally-owned collaborators (other services, the message `Group`, DAOs another @@ -13,20 +13,18 @@ called by the content entry point before `init()`), and Agent code has its own equivalents (see [`architecture-agent.md`](./architecture-agent.md)). Check the specific file, don't assume `init()` is the only place registration happens. -- **Cross-cutting subsystems** — `agent/` (see [`architecture-agent.md`](./architecture-agent.md); spans all - five contexts rather than living in one) and `extension/` (extension-wide environment helpers, e.g. - `extension_env.ts`) — plus `queue.ts` (shared `MessageQueue` **payload/type** definitions, e.g. - `TInstallScript`, `TDeleteScript` — not the `MessageQueue` implementation itself, which lives in - [`packages/message/message_queue.ts`](../../packages/message/message_queue.ts) and is instantiated by the - contexts that actually need pub/sub — currently Service Worker - ([`src/service_worker.ts`](../../src/service_worker.ts)), Offscreen - ([`src/app/service/offscreen/base.ts`](../../src/app/service/offscreen/base.ts)), and UI pages subscribing - to broadcasts ([`src/pages/store/global.ts`](../../src/pages/store/global.ts)) — not content, inject, or - sandbox, which don't instantiate it. Run - `git grep -n -E 'new MessageQueue\s*\(' -- src packages | grep -vE '\.(test|spec)\.[cm]?[jt]sx?:'` for - the current set rather than trusting this list to stay exhaustive — a plain `"new MessageQueue"` search also - matches `new MessageQueueGroup(...)` in `packages/message/message_queue.ts` and every test file that - constructs its own instance, neither of which are production instantiation sites. +- **Cross-cutting subsystems** — `agent/` (spans all five contexts rather than living in one; see + [`architecture-agent.md`](./architecture-agent.md)) and `extension/` (extension-wide environment helpers, + e.g. `extension_env.ts`). +- **`queue.ts`** — shared `MessageQueue` **payload/type** definitions (`TInstallScript`, `TDeleteScript`, …), + *not* the `MessageQueue` implementation, which lives in + [`packages/message/message_queue.ts`](../../packages/message/message_queue.ts). Only the contexts that need + pub/sub instantiate one: Service Worker ([`src/service_worker.ts`](../../src/service_worker.ts)), Offscreen + ([`src/app/service/offscreen/base.ts`](../../src/app/service/offscreen/base.ts)), and UI pages subscribing to + broadcasts ([`src/pages/store/global.ts`](../../src/pages/store/global.ts)) — content, inject, and sandbox + don't. For the current set: + `git grep -nE 'new MessageQueue\s*\(' -- src packages | grep -vE '\.(test|spec)\.[cm]?[jt]sx?:'` + (the `grep -v` drops test-local instances; the `\s*\(` keeps `new MessageQueueGroup(...)` out). ``` src/app/service/ @@ -71,30 +69,22 @@ class ScriptService { } ``` -That's a trimmed illustration, not a literal copy of `script.ts` — don't infer an exact handler count from it -(counts drift; check `git grep -c "this.group.on" src/app/service/service_worker/script.ts` for the current -one) or assume the constructor body is empty. - -**Don't over-read the constructor as "never `new` internally, constructor never does work."** Several services -break both of those: - -- [`ScriptService`](../../src/app/service/service_worker/script.ts) itself: alongside the injected - `scriptDAO`, it self-constructs `ScriptCodeDAO`, `LocalStorageDAO`, `CompiledResourceDAO`, `TrashScriptDAO`, - and `SubscribeDAO` as field initializers, and its real constructor body sets up a logger, enables caching on - two of those DAOs, and builds a `ScriptUpdateCheck` helper. -- [`ResourceService`](../../src/app/service/service_worker/resource.ts) constructs its own `ResourceDAO` as a - field initializer and does real setup in the constructor body (logger, `enableCache()`). -- [`SubscribeService`](../../src/app/service/service_worker/subscribe.ts) self-constructs `SubscribeDAO` *and* - `ScriptDAO` as field initializers, alongside constructor-injecting `Group`, `mq`, and `ScriptService`. - -**There is no "shared DAO type ⇒ same instance" invariant to rely on.** `ScriptService` builds its own -`SubscribeDAO`; `SubscribeService` builds a *separate* `SubscribeDAO` instance, and its own `ScriptDAO` rather -than reusing the `scriptDAO` the manager passes to `ScriptService`/`RuntimeService`/`PopupService`. The -existing choices are case-by-case, not governed by one rule — don't assume either "always inject" or "always -self-construct." When you're deciding for a new service, weigh cache ownership (does another service need the -same in-memory cache state?), lifetime (does it need to outlive this service?), and test substitution (does a -test need to swap it for a fake?) — but that's design guidance for your decision, not a documented reason -behind each existing instance; check the nearest existing service for the pattern it actually uses. +The snippet is trimmed: real constructors also `new` things internally and do setup work. + +- [`ScriptService`](../../src/app/service/service_worker/script.ts) — alongside the injected `scriptDAO`, it + self-constructs `ScriptCodeDAO`, `LocalStorageDAO`, `CompiledResourceDAO`, `TrashScriptDAO`, and + `SubscribeDAO` as field initializers; the constructor body sets up a logger, enables caching on two of those + DAOs, and builds a `ScriptUpdateCheck` helper. +- [`ResourceService`](../../src/app/service/service_worker/resource.ts) — self-constructs `ResourceDAO`; + logger + `enableCache()` in the constructor body. +- [`SubscribeService`](../../src/app/service/service_worker/subscribe.ts) — self-constructs `SubscribeDAO` + *and* `ScriptDAO`, alongside injected `Group`, `mq`, and `ScriptService`. + +**Same DAO type does not imply the same instance.** `ScriptService` and `SubscribeService` each build their own +`SubscribeDAO`, and `SubscribeService` builds its own `ScriptDAO` instead of reusing the one the manager passes +to `ScriptService`/`RuntimeService`/`PopupService`. Deciding for a new service, weigh cache ownership (does +another service need the same in-memory cache state?), lifetime, and test substitution — then copy what the +nearest existing service actually does. **`MockMessage` is not an `IMessageQueue` substitute.** It implements the lower-level `Message` transport (used to build a `Server`/`Group` for RPC in tests, e.g. `new Server("test", new MockMessage(...))`); the @@ -140,17 +130,12 @@ it mirrors `ServiceWorkerManager`. ### Agent composition is different — by design -Context services take *shared* collaborators through the constructor and register handlers through an explicit -lifecycle method (the DI pattern above — commonly `init()`, sometimes another one like content's -`contentInit()`), but the exact dependency set — and whether a service also self-constructs a local -DAO/helper — varies per service (see the `ResourceService`/`SubscribeService` examples above); "`Group` + -`IMessageQueue` + DAOs" is shorthand for "shared collaborators come in via constructor," not a fixed parameter -list or a ban on any internal construction. The Agent subsystem's sub-services (`ChatService`, `AgentTaskService`, `SkillService`, -`AgentModelService`, `MCPService`, etc. — see [`architecture-agent.md`](./architecture-agent.md)) are composed -by `AgentService` instead of each independently owning a `Group`, and each takes only the narrower interface -it actually needs (e.g. `AgentModelService` takes a `Group` and its own `AgentModelRepo`; `SubAgentService` -takes a small `SubAgentOrchestrator` interface). When adding to the Agent subsystem, follow the pattern of the -nearest existing sub-service rather than a context service's constructor shape. +The Agent subsystem's sub-services (`ChatService`, `AgentTaskService`, `SkillService`, `AgentModelService`, +`MCPService`, … — see [`architecture-agent.md`](./architecture-agent.md)) are composed by `AgentService` +instead of each independently owning a `Group`, and each takes only the narrower interface it actually needs +(e.g. `AgentModelService` takes a `Group` and its own `AgentModelRepo`; `SubAgentService` takes a small +`SubAgentOrchestrator` interface). When adding to the Agent subsystem, follow the nearest existing sub-service +rather than a context service's constructor shape. ## Adding a service diff --git a/docs/references/design-components.md b/docs/references/design-components.md index ff154719d..599e2373d 100644 --- a/docs/references/design-components.md +++ b/docs/references/design-components.md @@ -2,7 +2,7 @@ ## Component palette & usage -The shadcn primitives live in [`src/pages/components/ui/`](../../src/pages/components/ui/) — `new-york` style, CSS variables enabled, no class prefix (`components.json`). Icons are always `lucide-react`; class merging is always `cn()` ([`src/pkg/utils/cn.ts`](../../src/pkg/utils/cn.ts)); variants are always CVA — these are the [`DEVELOP.md` UI section](../develop.md#ui) hard rules, not repeated here. This section is "what exists and how to choose." +The shadcn primitives live in [`src/pages/components/ui/`](../../src/pages/components/ui/) — `new-york` style, CSS variables enabled, no class prefix (`components.json`). Icons are always `lucide-react`; class merging is always `cn()` ([`src/pkg/utils/cn.ts`](../../src/pkg/utils/cn.ts)); variants are always CVA — these are the [`develop.md` § UI](../develop.md#ui) hard rules, not repeated here. This section is "what exists and how to choose." ### Primitives & shared composites diff --git a/docs/references/design-patterns.md b/docs/references/design-patterns.md index a968247a4..de63e35e3 100644 --- a/docs/references/design-patterns.md +++ b/docs/references/design-patterns.md @@ -87,7 +87,7 @@ both before treating it as settled if either changes. ### How to add motion that stays friendly - **Fast and light:** enter/leave in `150–250ms`, `ease-out`; the built-in collapse/progress animations use `200ms ease-out`. -- **Hover/focus via CSS pseudo-classes, not React state** (`hover:bg-primary-background/90`, `focus-visible:ring-ring/50`) — a `DEVELOP.md` rule. +- **Hover/focus via CSS pseudo-classes, not React state** (`hover:bg-primary-background/90`, `focus-visible:ring-ring/50`) — a [`develop.md` § UI](../develop.md#ui) rule. - **Enter/leave via Radix `data-state`** — don't hand-roll show/hide with `setTimeout`. - **Prefer `transition-colors` over `transition-all`:** animate only what should move, avoiding layout thrash and wasted work. - **Reuse existing utilities;** don't inline `@keyframes` in a component. New animation → add an `@utility` in `src/index.css` so it's globally reusable. diff --git a/docs/references/design-tokens.md b/docs/references/design-tokens.md index 5557dea81..5b9025099 100644 --- a/docs/references/design-tokens.md +++ b/docs/references/design-tokens.md @@ -7,7 +7,7 @@ **Usage:** - Background `bg-`, text `text-`, border `border-`, focus ring `ring-ring`. - Opacity modifiers compose directly: `bg-primary-background/90` (solid primary hover), `ring-destructive/20`, `bg-input/30`. -- **Never hard-code a color value** — see Constraint 1 and [`DEVELOP.md` UI section](../develop.md#ui). For dark-only tweaks use the `dark:` variant. +- **Never hard-code a color value** — see Constraint 1 and [`develop.md` § UI](../develop.md#ui). For dark-only tweaks use the `dark:` variant. ### Base surfaces & text diff --git a/docs/references/develop-testing.md b/docs/references/develop-testing.md index 00135f99a..4ca055188 100644 --- a/docs/references/develop-testing.md +++ b/docs/references/develop-testing.md @@ -240,10 +240,8 @@ budget at once. Chrome APIs are mocked via `@Packages/chrome-extension-mock` (`tests/vitest.setup.ts`). `MockMessage` is available for message-system tests. `happy-dom` is patched via `patches/` (see `pnpm-workspace.yaml` `patchedDependencies`) to build its invalid-selector `DOMException` lazily — the upstream eager construction captures a deep stack on every -`matches()`/`querySelector()` call, which is measurably slower at TSX-suite scale. No specific percentage is -tracked here since it isn't tied to a reproducible command/environment; if you need a number, measure -before/after this patch in the same environment using the JSON-report method below rather than trusting a -historical figure. +`matches()`/`querySelector()` call, which is measurably slower at TSX-suite scale. To quantify the win, measure +before/after in one environment with the JSON-report method below. - Co-locate `*.test.ts`/`*.test.tsx` next to source (or place in `tests`). - Use `describe.concurrent()` / `it.concurrent()` where independent. diff --git a/docs/references/verification-debugging.md b/docs/references/verification-debugging.md index c50b3d0ff..c9e9ac1b0 100644 --- a/docs/references/verification-debugging.md +++ b/docs/references/verification-debugging.md @@ -3,7 +3,7 @@ ## Five-context debug map A feature can break in any of ScriptCat's five isolated contexts. Match the symptom to where its logs live (deep -model in [`ARCHITECTURE.md`](../architecture.md)): +model in [`architecture.md`](../architecture.md)): | Symptom | Where to look | | --- | --- | @@ -20,10 +20,12 @@ still a separate execution boundary on both browsers. See for the full picture. In a scratch script, capture the page console (`page.on("console", …)`), take screenshots -(`await page.screenshot({ path: "test-results/verify//screenshots/…png" })`), and write any manual -notes to `test-results/verify//`. Playwright's automatic failure artifacts also go under -`test-results/` because [`playwright.config.ts`](../../playwright.config.ts) sets `outputDir: "test-results"`. -The local default keeps Playwright video recording off; CI records retried failures only. +(`await page.screenshot({ path: "e2e/scratch//screenshots/…png" })`), and write any manual notes to +`e2e/scratch//`. Playwright's own failure artifacts land in `test-results/` for both +[`playwright.config.ts`](../../playwright.config.ts) and +[`playwright.scratch.config.ts`](../../playwright.scratch.config.ts), and every run wipes that directory, +which is why your evidence belongs in the scenario directory instead. The local default keeps Playwright video recording +off; CI records retried failures only. ## Key extension URLs diff --git a/docs/references/verification-report-template.md b/docs/references/verification-report-template.md index c757bd18d..e4de2904a 100644 --- a/docs/references/verification-report-template.md +++ b/docs/references/verification-report-template.md @@ -1,12 +1,16 @@ # Verification record template Before running the browser, create a short verification record in the scenario directory, for example -`test-results/verify//report.md`. Keep the reusable template headings in English, but write the actual +`e2e/scratch//report.md`. Keep the reusable template headings in English, but write the actual record content in the user's language. Update it as the run proceeds instead of filling it in only at the end. -**The snippet below is a filled *example* of the `## Evidence Index` shape** — it shows what a completed one -looks like, not a second section to add. The full template further down has its own `## Evidence Index` -heading; use that one heading and fill it following this example's shape. +**The snippet below is a filled *example* of the `## Acceptance Evidence` shape** — it shows what a completed +one looks like, not a second section to add. The full template further down has its own heading; use that one +and fill it following this example. + +Evidence is organized **one `###` section per `Verdict` row**, not by artifact type. A reader arrives from a +`V2` row and finds every screenshot, log line, and fixture that decides `V2` in one place, in the order they +were observed. Verdict labels stay in the `Verdict` table and are not repeated here. This record exists so a reader can judge whether the implementation is correct, so **evidence is embedded, not linked**: scrolling `report.md` top to bottom should show the pixels and the deciding log lines without opening @@ -14,55 +18,54 @@ a single side file. A bare link is the fallback for artifacts that genuinely can binaries, multi-megabyte logs), and it carries a note saying what it holds. ~~~md -## Evidence Index - -### Screenshots - -![Options root](screenshots/options-root.png) -The script list page rendered and the view toggle is visible, proving the `/` route mounted successfully. - -| Light | Dark | -| --- | --- | -| ![Settings light](screenshots/settings-light.png) | ![Settings dark](screenshots/settings-dark.png) | - -The settings shell renders in both themes with readable contrast, proving `/settings` mounted and picked up the -theme tokens rather than falling back to one palette. +## Acceptance Evidence -### Videos +### V1 · The `/` route mounts and lists installed scripts - +![Options root](screenshots/v1-options-root.png) +The script list rendered with the view toggle visible — the route mounted, rather than falling through to a +blank shell. -Full page-viewport recording from the script list to the settings page; watch it for the navigation and the -final stable state. +```text +[verify] options url = chrome-extension:///src/options.html#/ +``` -Same run, decisive moments as stills — a video is neither skimmable nor playable in every viewer: +### V2 · `/settings` renders correctly in light and dark -![Before navigation](screenshots/nav-01-list.png) -The script list before the click; the settings entry is enabled. +| Light | Dark | +| --- | --- | +| ![Settings light](screenshots/v2-settings-light.png) | ![Settings dark](screenshots/v2-settings-dark.png) | -![After navigation](screenshots/nav-02-settings.png) -The settings page after the click; the route changed and the content painted. +Readable contrast in both themes — the shell picked up the theme tokens instead of falling back to one palette. +One theme's screenshot alone would not show this. -### Logs + -The lines the verdict rests on: +The full navigation from the script list to the settings page. The decisive frames, because a video is neither +skimmable nor playable in every viewer: -```text -[verify] options url = chrome-extension:///src/options.html#/settings -[verify] script count after import = 3 -``` +![Before navigation](screenshots/v2-nav-01-list.png) +The settings entry, enabled, before the click. -Full capture: [console.log](console.log) — no unexpected errors appeared during the run. +![After navigation](screenshots/v2-nav-02-settings.png) +The route changed and the content painted, after the click. -### Resources +### V3 · Importing a backup restores every script in it -`resources/import.yaml` — the input the import verification consumed: +`resources/import.yaml` — the input this run consumed: ```yaml scripts: - name: demo-script source: https://example.com/demo.user.js ``` + +```text +[verify] script count after import = 3 +``` + +Three scripts in the file, three in the list. Full capture: [console.log](console.log) — no unexpected errors +during the run. ~~~ Use this shape: @@ -94,7 +97,7 @@ Use this shape: - [ ] Built and loaded the real extension - [ ] Opened target page and confirmed stable anchor - [ ] Saved screenshots, videos, and logs -- [ ] Recorded the verdict in Result +- [ ] Every Verdict row filled ## Execution Log @@ -102,10 +105,14 @@ Use this shape: | --- | --- | --- | --- | | Open options page | Pending | - | - | -## Result +## Verdict + +| # | Claim under verification | Verdict | How observed | Check it yourself | +| --- | --- | --- | --- | --- | +| V1 | | holds / does not hold / not observed | | `` | + +Summary: -- **Verdict:** PASS / FAIL — (verify) does the behavior hold? (reproduce) did it reproduce? -- **Observed:** the summary line / asserted value / screenshot that backs the verdict - (reproduce) Scratch asserts the **desired** behavior (stays red) or the **current buggy** contract (passes green; the fix must flip it) — say which @@ -113,17 +120,65 @@ Use this shape: - None +## Acceptance Evidence + +### V1 · + + + +## Persistent Data Changes + +| Change | Forward | Backward / backup | Before-after check | +| --- | --- | --- | --- | +| | | | | + +## Integrity & Cleanup + +- HEAD at start / end: `` / `` +- `git status --porcelain` at end: `` +- Artifacts, processes, and external data created — and how each was cleaned up: `` +- Redaction performed before saving: `` + ## Evidence Index -Embed every artifact inline and annotate what it proves — see the shape above. +- Screenshots / video: +- Logs: +- Resources / data snapshots: ``` -Fill `Result` at the end — the honest verdict, per *Step 4 — Report honestly* in [`verification.md`](../verification.md). -Execution Log `Status` moves `Pending` → `Pass` / `Fail` / `Blocked`. +Fill `Verdict` last — the honest result, per *Step 4 — Report honestly* in +[`verification.md`](../verification.md). Execution Log `Status` moves `Pending` → `Pass` / `Fail` / `Blocked`. -In `verify-change` mode, drop the `Reproduction Steps` / `Minimal Reproduction` sections. In `reproduce-bug` -mode, fill `Expected`/`Actual` and keep those sections so the record stands on its own — a later reader or AI -should understand and re-trigger the bug from `report.md` alone, without reading the code. +### Verdicts are per claim, and there are three of them + +One row per claim you set out to verify — split a compound claim rather than averaging it into one row. The +three labels are not interchangeable: + +| Label | Use it when | Requires | +| --- | --- | --- | +| `holds` | you observed the behavior at runtime | the deciding observation *and* a command a reader can re-run | +| `does not hold` | you observed it failing, or observed the bug reproducing | the failing output, assertion diff, or error screenshot | +| `not observed` | you never reached the check — blocked, out of scope, environment missing | a `Blockers` entry saying what stopped it | + +`not observed` is the one that keeps a report honest: an unreached check is **never** `holds`. A run where two +claims held and one was never exercised is reported as exactly that, not as a pass. When the cause was an +unconfigured environment, name the service and the *variable names* that were missing — never their values. + +The `Check it yourself` column exists so a reviewer can reproduce a row without reconstructing the run; if a row +has no such command, say why in `How observed` rather than leaving it blank. + +### Sections to drop when they don't apply + +- `verify-change` mode: drop `Reproduction Steps` and `Minimal Reproduction`. In `reproduce-bug` mode fill + `Expected`/`Actual` and keep them, so a later reader or AI can re-trigger the bug from `report.md` alone + without reading the code. +- `Persistent Data Changes`: keep only when the run wrote data that outlives it — a real cloud-sync provider, an + imported backup, an OPFS/IndexedDB migration. An ephemeral browser profile that the harness deletes is not a + persistent change. Note the blast radius honestly: "only this test profile" is a valid, useful entry. +- `Integrity & Cleanup`: keep whenever the run touched a real external target or left anything behind. It is + what lets a reviewer confirm the verification didn't quietly modify the working tree or leave a live process + or real remote data around. Keep the checklist factual: @@ -132,6 +187,19 @@ Keep the checklist factual: - If a step is blocked, leave its checkbox unchecked and add a concrete entry under `Blockers`: what failed, where it failed, and what evidence was captured. +### Inside an Acceptance Evidence section + +One `###` per `Verdict` row, headed `V · `, holding everything that decides that row — commands, +output, screenshots, fixtures — in the order you observed them. Rules that follow from that: + +- A claim with no evidence section is `not observed`, not `holds`. If a row genuinely needs no artifact beyond + its `Check it yourself` command, say so in one line rather than omitting the section. +- One artifact can back two rows; put it under the row it decides and reference it from the other rather than + pasting it twice. +- Don't restate the verdict word here — the `Verdict` table owns it, and two copies drift apart. +- `Evidence Index` at the end is a **pointer list**, not a second copy: paths, and which row each backs. The + pixels and the deciding lines stay inline in the V sections. + Keep the evidence embedded: - **Screenshots** — `![alt](screenshots/….png)` plus a caption line stating what it proves. Put paired shots diff --git a/docs/verification.md b/docs/verification.md index a040d4e87..1002fb866 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -6,10 +6,11 @@ > consultable record of what was verified or reproduced. This is deliberately **lightweight**: one-shot scratch > scripts and local-only evidence — kept out of Git and never deleted as part of a run; cleanup is the user's call. > -> **What this is NOT.** It is *not* the test-suite reference. The mechanics of Vitest unit tests and the -> permanent Playwright E2E suite live in [develop.md § Testing](./develop.md#testing); the TDD-first principle -> and engineering rules live in [AGENTS.md § Engineering Principles](../AGENTS.md#engineering-principles). Read -> those for writing committed tests. +> **What this is NOT.** It is *not* the test-suite reference and *not* the harness manual. Vitest mechanics live +> in [develop.md § Testing](./develop.md#testing); the E2E harness itself — fixtures, isolation, protocol mocks, +> `E2E_*` environment variables, artifact locations — is owned by [`e2e/README.md`](../e2e/README.md); the +> TDD-first principle and engineering rules live in +> [AGENTS.md § Engineering Principles](../AGENTS.md#engineering-principles). ## When to skip this guide @@ -38,16 +39,16 @@ you only want to *check that a feature works*, do not pay that cost and do not l - ✅ Write a **throwaway scratch script** under `e2e/scratch/` (git-ignored), run it, and keep any evidence local. Promoting a scenario into the permanent suite is a *separate, deliberate* decision — only when it deserves -permanent regression coverage. That path is owned by [`DEVELOP.md`](./develop.md), not this guide. +permanent regression coverage. The criteria live in +[develop-testing.md § Choosing a test boundary](./references/develop-testing.md#choosing-a-test-boundary). **Reproducing a bug you intend to fix is *not* "casual verification."** A scratch reproduction is the *确定 bug 存在* step in [`../AGENTS.md`](../AGENTS.md)'s TDD / Confirm-before-you-fix policy. In the general case it confirms the bug is real but is not itself the required test — promote it into a committed failing test before -fixing. Under that policy's infeasible-automated-coverage exception (criteria in -[`references/develop-testing.md`](./references/develop-testing.md#when-tdd-doesnt-apply)), this scratch -reproduction — its `report.md`, screenshots, and observations — *is* the required evidence; no committed test -is needed. `AGENTS.md` owns the governing principle; `references/develop-testing.md` owns the exception -criteria — don't expect the exception boundary spelled out in `AGENTS.md` itself. +fixing. Only under that policy's infeasible-automated-coverage exception (criteria in +[develop-testing.md § When TDD doesn't apply](./references/develop-testing.md#when-tdd-doesnt-apply)) does the +scratch reproduction — its `report.md`, screenshots, and observations — stand as the required evidence on its +own. Choose the reproduction method by what the bug depends on: a failing unit test for pure logic/parser/utility bugs; this guide's scratch-script workflow (above) when it depends on the built extension, browser APIs, or @@ -61,15 +62,10 @@ but scale which signals proportionally, not mechanically: ```bash pnpm run typecheck # tsc --noEmit — always pnpm test -- --run path/to/file.test.ts # targeted unit test(s) for the change — the default -pnpm test # full Vitest suite — only when the change is broad/risky, - # touches shared code, or a repo gate/CI requires it +pnpm test # full Vitest suite — only when the blast radius isn't confirmed + # local (shared utils, config, public interfaces), or a gate requires it ``` -Typecheck plus the targeted relevant test is the default prerequisite — it is not a requirement to run the -full Vitest suite before every scratch verification. Run the broader suite when the change's blast radius -isn't confirmed to be local (shared utilities, config, public interfaces) or when project policy requires it -for the change type. - Green unit tests do **not** mean the feature works — they mean the units you tested behave. Cross-context wiring (Service Worker ↔ Content ↔ Inject ↔ Offscreen ↔ Sandbox) and real Chrome APIs only exercise in a loaded extension. That gap is exactly what this guide closes. @@ -81,35 +77,35 @@ pnpm run dev # development build with source maps → writes dist/ext # or: pnpm run build # production build, also → dist/ext ``` -Load `dist/ext` as an unpacked extension (the scratch scripts below do this for you via -`--load-extension`). After a rebuild: - -- **Page-only edits** (React UI under `src/pages/`) hot-reload — just refresh the page. -- **Edits to `manifest.json`, `service_worker`, `offscreen`, or `sandbox`** require **reloading the extension** - (and a fresh launch in the scratch flow, since each run loads `dist/ext` freshly). +Every fixture loads `dist/ext`, so a stale build silently verifies old code — rebuild first. Setup details and +what a rebuild does or doesn't require live in [`e2e/README.md § Setup`](../e2e/README.md#2-setup). ## Step 2 — Write a scratch verification script -Scratch scripts live in **`e2e/scratch/`** and reuse the existing harness, so you write almost no boilerplate: +Each verification gets its own scenario directory **`e2e/scratch//`** holding the script *and* every +artifact it produces. The scripts reuse the committed harness, so you write almost no boilerplate — the +fixtures, the page openers, the script installer, and the environment variables are catalogued in +[`e2e/README.md`](../e2e/README.md). The short version: -- `import { test, expect } from "../fixtures";` — gives you a `context` (with `dist/ext` loaded) and an - `extensionId`, with the first-use guide already dismissed. See [`e2e/fixtures.ts`](../e2e/fixtures.ts). -- `import { ... } from "../utils";` — page openers and a script installer. See [`e2e/utils.ts`](../e2e/utils.ts): - `openOptionsPage`, `openPopupPage`, `openEditorPage`, `installScriptByCode`, `saveCurrentEditor`, - `autoApprovePermissions`, and `runInlineTestScript`. +```ts +import { test, expect } from "../../fixtures"; // context + extensionId, onboarding dismissed +import { openOptionsPage } from "../../utils"; // page openers, installScriptByCode, … +``` ### Evidence location -Keep all throwaway verification evidence under **`test-results/verify//`**: +Keep the script and all of its throwaway evidence together under **`e2e/scratch//`**: -- screenshots: `test-results/verify//screenshots/*.png` -- videos: `test-results/verify//videos/*.webm` -- logs / notes / short verification reports: `test-results/verify//*.md` or `*.log` -- additional test resources: `test-results/verify//resources/` +- the script itself: `e2e/scratch//*.spec.ts` +- screenshots: `e2e/scratch//screenshots/*.png` +- videos: `e2e/scratch//videos/*.webm` +- logs / notes / short verification reports: `e2e/scratch//*.md` or `*.log` +- additional test resources: `e2e/scratch//resources/` -`test-results/` and `playwright-report/` are git-ignored, so these files are local evidence only and must not be -committed. Do not put verification screenshots, videos, or notes under `docs/`, `e2e/`, or committed source -directories unless you are deliberately adding permanent documentation assets. +`e2e/scratch/` is git-ignored, so these files are local evidence only and must not be committed. Keep them out +of `test-results/` as well: Playwright wipes that directory at the start of every run, so evidence parked there +disappears the next time anyone runs the suite. Do not put verification screenshots, videos, or notes under +`docs/` or committed source directories unless you are deliberately adding permanent documentation assets. Use `resources/` for any extra local inputs or outputs needed to understand or reproduce the run, for example: @@ -130,17 +126,18 @@ item carries a short note explaining what it proves and how to read it. ### Create `report.md` before you run the browser -Before running the browser, create `test-results/verify//report.md` following the Evidence Index -shape in the [verification record template](./references/verification-report-template.md). Fill it in as you -go — don't reconstruct the run from memory afterward. +Before running the browser, create `e2e/scratch//report.md` from the +[verification record template](./references/verification-report-template.md) — one `Verdict` row per claim you +intend to check, and one `Acceptance Evidence` section per row. Fill both in as you go; don't reconstruct the +run from memory afterward. ### Minimal template (drive a UI page) -Save as e.g. `e2e/scratch/verify-options.spec.ts`: +Save as e.g. `e2e/scratch/options-page/verify.spec.ts`: ```ts -import { test, expect } from "../fixtures"; -import { openOptionsPage } from "../utils"; +import { test, expect } from "../../fixtures"; +import { openOptionsPage } from "../../utils"; test("verify: options page opens and renders the script list area", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); @@ -151,15 +148,16 @@ test("verify: options page opens and renders the script list area", async ({ con console.log("[verify] options url =", page.url()); // Keep evidence for review and debugging. - await page.screenshot({ path: "test-results/verify/options-page/screenshots/options.png", fullPage: true }); + await page.screenshot({ path: "e2e/scratch/options-page/screenshots/options.png", fullPage: true }); }); ``` -If you need video evidence, enable it explicitly for the run. The shared fixture writes videos only when -`E2E_RECORD_VIDEO_DIR` is set: +Video is off by default; enable it per run by pointing `E2E_RECORD_VIDEO_DIR` at the scenario's `videos/` +(the variable and its limits are documented in +[`e2e/README.md § Environment variables`](../e2e/README.md#5-environment-variables)): ```bash -E2E_RECORD_VIDEO_DIR=test-results/verify/options-page/videos \ +E2E_RECORD_VIDEO_DIR=e2e/scratch/options-page/videos \ pnpm exec playwright test --config playwright.scratch.config.ts -g "options page" ``` @@ -167,12 +165,8 @@ Playwright finalizes `.webm` files when pages/contexts close at the end of the t produce multiple videos because the harness may open setup pages as well as the page under verification; keep all of them beside the screenshots for the same scenario. -> Scratch copying the inline fixture won't read `E2E_RECORD_VIDEO_DIR` — see [gotchas](./references/verification-debugging.md#common-gotchas). - ### Run only your scratch script -A dedicated config keeps scratch scripts **out of the main suite/CI** while still letting you run them: - ```bash # run every script in e2e/scratch/ pnpm exec playwright test --config playwright.scratch.config.ts @@ -181,22 +175,16 @@ pnpm exec playwright test --config playwright.scratch.config.ts pnpm exec playwright test --config playwright.scratch.config.ts -g "options page" ``` -Why two configs: [`playwright.config.ts`](../playwright.config.ts) sets `testIgnore: ["**/scratch/**"]`, so -`pnpm run test:e2e` and CI **never** pick up scratch scripts; [`playwright.scratch.config.ts`](../playwright.scratch.config.ts) -points `testDir` at `e2e/scratch/` so you can run them on demand. Scratch files are git-ignored. +The two configs keep scratch scripts out of the main suite and CI, and keep the two runs' artifacts apart; +[`e2e/README.md § Two tracks`](../e2e/README.md#1-two-tracks) explains the mechanics. ## Step 3 — Verify actual script *execution* (GM APIs, injection) -The shared `e2e/fixtures.ts` is enough to drive extension pages, but to make a userscript **actually inject and -run in a page** you need two extra things, both already solved in [`e2e/gm-api.spec.ts`](../e2e/gm-api.spec.ts) — -copy that file's inline fixture and helpers into your scratch script rather than re-deriving them: - -1. **Enable the `userScripts` permission.** It is an *optional* MV3 permission (see `manifest.json` - `optional_permissions`). `gm-api.spec.ts` enables it with a **two-phase launch**: first launch toggles - `developerPrivate.updateExtensionConfiguration({ userScriptsAccess: true })`, then it relaunches the same - user-data dir with scripts enabled. -2. **Auto-approve permission prompts.** GM APIs that need a grant open a `confirm.html` page; - `gm-api.spec.ts`'s `autoApprovePermissions(context)` listens for it and clicks "permanent allow". +The default fixture is enough to drive extension pages. To make a userscript **actually inject and run in a +page** you need the `userScripts` permission granted and permission prompts auto-approved — both already solved +by `testWithUserScripts` and `autoApprovePermissions`, described in +[`e2e/README.md § Harness chain`](../e2e/README.md#3-harness-chain). Import them; don't re-derive the launch +dance. What remains is how you *observe* the result: ### The in-page self-test pattern @@ -289,17 +277,19 @@ screenshot doesn't demonstrate the other renders correctly. Verification only counts if the result is reported as observed (this mirrors the engineering principle: evidence before assertions). +- Record one verdict **per claim**, not one for the run, using the three labels the + [report template](./references/verification-report-template.md#verdicts-are-per-claim-and-there-are-three-of-them) + defines: `holds`, `does not hold`, `not observed`. - If it works, say so and state *what you ran* and *what you observed* (the summary line, the screenshot, the asserted value, and any video/report path). -- If it fails or you could not verify a path, **say that plainly** with the console/output — do not soften it, - do not claim success you did not see. +- If it fails, **say that plainly** with the console/output — do not soften it, do not claim success you did not + see. A check you never reached is `not observed`, never `holds`: a run that verified two of three claims is + reported as two of three, not as a pass. - If you were **reproducing a bug**, state plainly whether it reproduced. If it did, the failing observation - (error, assertion diff, error screenshot) *is* the evidence. In the general case, promote it into a committed - failing test before fixing (the failing-test → fix cycle). Under - [develop-testing.md § When TDD doesn't apply](./references/develop-testing.md#when-tdd-doesnt-apply)'s - infeasible-automated-coverage exception, keep this scratch reproduction as the evidence instead — fix the - code, then re-run the *same* reproduction script to confirm it now passes. If it did not reproduce, say so - and note what you tried, instead of implying the bug is gone. + (error, assertion diff, error screenshot) *is* the evidence; after fixing, re-run the *same* script and + report that it now passes. If it did not reproduce, say so and note what you tried, instead of implying the + bug is gone. (Whether that scratch must *also* become a committed failing test is settled above, under + *The one rule* — not here.) - Two honest framings for the scratch assertion: assert the **desired** behavior (the scratch stays *red* and directly shows the gap), or assert the **current buggy contract** (the scratch passes *green* while the bug is present, giving a deterministic re-runnable record) and note that the fix must flip it. Pick one and say @@ -308,14 +298,14 @@ before assertions). ## Maintaining this guide -When the harness, scripts, or paths change, keep this doc true to the branch (see -[`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md)). Quick checks: +When the workflow or the paths in it change, keep this doc true to the branch (see +[`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md)). Harness facts are checked by +[`e2e/README.md § Maintaining this file`](../e2e/README.md#maintaining-this-file) — don't duplicate those checks +here. What this guide still owns: ```bash -ls e2e/fixtures.ts e2e/utils.ts e2e/gm-api.spec.ts playwright.scratch.config.ts -grep -n "testIgnore" playwright.config.ts -grep -n "e2e/scratch/" .gitignore -ls example/tests/ +grep -n "e2e/scratch/" .gitignore # evidence stays local +ls example/tests/ # the in-page self-test scripts grep -n "lastFocusedWindow" src/pkg/utils/utils.ts # getCurrentTab → standalone popup resolves its own tab grep -n "res.data" packages/message/client.ts # SW reply envelope { code, data } ``` diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 000000000..50086d62a --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,177 @@ +# E2E Harness + +> **What this owns.** How the E2E harness is *set up and run*: the two tracks, browser/profile isolation, the +> fixtures and helpers you build on, the protocol mocks, the environment variables, and where run artifacts +> land. +> +> **What this is NOT.** It does not decide *when* to verify something or how to report it — that is +> [`../docs/verification.md`](../docs/verification.md), and the record's layout/verdicts live in +> [`../docs/references/verification-report-template.md`](../docs/references/verification-report-template.md). +> Unit-test mechanics live in [`../docs/references/develop-testing.md`](../docs/references/develop-testing.md). + +## 1. Two tracks + +| | Smoke suite | Local verification | +| --- | --- | --- | +| Path | `e2e/*.spec.ts` (committed) | `e2e/scratch//` (git-ignored) | +| Command | `pnpm run test:e2e` | `pnpm exec playwright test --config playwright.scratch.config.ts` | +| Config | [`playwright.config.ts`](../playwright.config.ts) | [`playwright.scratch.config.ts`](../playwright.scratch.config.ts) | +| Scope | stable regression flows | the one change or bug in front of you | +| Runs in CI | yes | never | +| Output | CI verdict + `test-results/` | `/report.md` + evidence | + +The separation is mechanical, not conventional: the main config sets `testIgnore: ["**/scratch/**"]`, so +`pnpm run test:e2e` and CI can never collect scratch scripts, while the scratch config points `testDir` at +`e2e/scratch/` and clears `testIgnore`. Both configs share one `outputDir` (`test-results/`); Playwright +wipes it at the start of every run, so keep durable evidence under your scenario directory, not in +`test-results/`. + +Promoting a scratch scenario into the committed suite is a separate, deliberate decision; see +[`../docs/verification.md`](../docs/verification.md). + +## 2. Setup + +```bash +pnpm run test:e2e:install # one-time: pnpm exec playwright install chromium +pnpm run dev # or pnpm run build — both write dist/ext +``` + +Every fixture loads the **built** extension from `dist/ext` via `--disable-extensions-except` + +`--load-extension`, so a stale build silently verifies old code. Rebuild before a run. Page-only edits under +`src/pages/` hot-reload into an already-open page; edits to `manifest.json`, `service_worker`, `offscreen`, or +`sandbox` need a fresh launch, which every run does anyway. + +## 3. Harness chain + +```text +playwright config → fixture (launchPersistentContext, loads dist/ext) + → extensionId (read from the extension service worker URL) + → page openers / script installer (utils.ts) + → assertions on real UI, page console, or extension storage +``` + +### Isolation + +| Resource | Mechanism | +| --- | --- | +| Browser profile | ephemeral: `launchPersistentContext("")`, or a `mkdtemp` dir removed with `fs.rmSync` after each test | +| First-use onboarding | `addInitScript` presets `localStorage.firstUse = "false"` so the welcome modal can't swallow clicks | +| `userScripts` permission | two-phase launch (below); the granted profile is worker-scoped and copied per test | +| Chromium sandbox | on locally, off under `CI` (GitHub Actions runs non-root, where the sandbox only costs fork overhead) | +| Test hostnames | `--host-resolver-rules` maps `*.test` names to `127.0.0.1` | + +### Fixtures + +| Import | What it gives you | +| --- | --- | +| [`fixtures.ts`](./fixtures.ts) → `test` | `context` + `extensionId`; onboarding dismissed. The default. | +| [`fixtures.ts`](./fixtures.ts) → `testWithUserScripts` | same, plus the `userScripts` permission already granted | +| [`server-fixtures.ts`](./server-fixtures.ts) → `test`, `startMockServer` | the above plus a local HTTP server and `.test` hostnames resolved to it | +| [`agent-fixtures.ts`](./agent-fixtures.ts) → `test`, `makeTextSSE`, `makeToolCallSSE` | an Agent-ready profile and a routed mock LLM endpoint | + +[`utils.ts`](./utils.ts) carries the page openers and script installer used by every track: +`openOptionsPage`, `openPopupPage`, `openEditorPage`, `openAgentChatPage`, `openAgentProviderPage`, +`saveCurrentEditor`, `installScriptByCode`, `runInlineTestScript`, and `autoApprovePermissions`. + +### The two-phase launch + +`userScripts` is an *optional* MV3 permission (`manifest.json` `optional_permissions`), so a freshly launched +profile cannot inject page scripts. `testWithUserScripts` solves it once per worker: phase 1 launches a temp +profile, navigates to `chrome://extensions/` and calls +`chrome.developerPrivate.updateExtensionConfiguration({ userScriptsAccess: true })`, then closes; phase 2 copies +that profile per test so the grant persists without re-running phase 1 each time. Use this fixture rather than +re-deriving the dance — doing it per test starves the extension service worker under parallel workers. + +GM APIs that need a grant open `confirm.html`; `autoApprovePermissions(context)` watches for it and clicks +permanent-allow. + +## 4. Protocol mocks + +Mocks are local HTTP servers, not stubbed internal code paths: + +- [`server-fixtures.ts`](./server-fixtures.ts) — `startMockServer()` returns `{ port, url, requestLog, hits, + failPath, unfailPath, reset, close }`, serving `@require`/`@resource`/XHR/redirect routes. `hits()` and + `failPath()` are what let a test tell a re-download from a cache hit, or force a 500. Used by + `resource-update.spec.ts` and `gm-xhr-site-access.spec.ts`. +- `gm-api.spec.ts` starts its own server and maps `content-security-policy.test` to it, so CSP behaviour is + exercised without leaving the machine. +- `agent-fixtures.ts` intercepts `**/mock-llm.test/**` through `context.route` and replies with scripted SSE + frames built by `makeTextSSE` / `makeToolCallSSE` — the mock has no scenario branching of its own. + +### Known external dependencies + +Two committed specs are **not** hermetic and will fail when the public internet or a third party is down: + +| Spec | Reaches | Local alternative that already exists | +| --- | --- | --- | +| `agent-conversation.spec.ts`, `agent-error-handling.spec.ts` | `https://content-security-policy.com/` as the injection target | the `.test` host + `--host-resolver-rules` pattern used by `gm-api.spec.ts` | +| `gm-api.spec.ts` | `unpkg.com` — `patchScriptCode` rewrites `cdn.jsdelivr.net` `@require`/`@resource` URLs to it | `startMockServer()`'s `/lib.js` / `/res.txt` routes | + +Treat this as known debt, not a pattern to copy: new specs mock their external protocols. + +## 5. Environment variables + +None are required; each one only switches on when set. `.env` is **not** loaded by anything in this repository +— export these in the shell (or inline before the command) instead. + +| Variable | Read by | Effect | +| --- | --- | --- | +| `E2E_PROXY` | [`fixtures.ts`](./fixtures.ts), [`agent-fixtures.ts`](./agent-fixtures.ts) | Chromium proxy for the launched context. Falls back to `https_proxy` / `http_proxy` / `HTTPS_PROXY` / `HTTP_PROXY`. Needed for the non-hermetic specs above on a restricted network. | +| `E2E_RECORD_VIDEO_DIR` | [`fixtures.ts`](./fixtures.ts) **only** | Records video into that directory. Off by default. Point it at your scenario directory, e.g. `e2e/scratch//videos`. `server-fixtures.ts` / `agent-fixtures.ts`, and any spec that copies a fixture inline instead of importing it, do **not** honour this. | +| `E2E_ONEDRIVE_TOKEN_FILE` | local scratch scripts only — **not referenced by any committed file** | Path to a OneDrive token JSON for real-provider cloud-sync verification, conventionally defaulting to `~/.config/scriptcat/e2e-onedrive-token.json`. Real account, real side effects — only with authorization. Recorded here because nothing in-tree can tell you it exists. | +| `CI` | every fixture, plus [`playwright.config.ts`](../playwright.config.ts) | Disables the Chromium sandbox, and switches Playwright to 1 retry / 2 workers / HTML reporter / `forbidOnly`. Set by GitHub Actions; don't set it by hand. | + +Secrets never belong in a committed spec or in `report.md` — see the redaction rules in +[`../docs/references/verification-report-template.md`](../docs/references/verification-report-template.md). + +## 6. Writing a scratch script + +Create `e2e/scratch//`, put the script and every artifact it produces inside it, and import the +harness from one level further up: + +```ts +import { test, expect } from "../../fixtures"; +import { openOptionsPage } from "../../utils"; +``` + +```bash +# everything under e2e/scratch/ +pnpm exec playwright test --config playwright.scratch.config.ts + +# one scenario, filtering by test title (regex) — quote it +pnpm exec playwright test --config playwright.scratch.config.ts -g "options page" +``` + +[`../docs/verification.md`](../docs/verification.md) owns the rest: when a scratch run is the right tool, where +evidence goes, and how to report the verdict honestly. + +## 7. Failure investigation + +```bash +ls test-results/ # traces, failure screenshots, .last-run.json (both tracks) +pnpm exec playwright show-report # HTML report (CI reporter; produced locally with --reporter=html) +pnpm exec playwright show-trace +``` + +Traces are recorded `on-first-retry`, so a first local failure has no trace — re-run with `--retries=1` to get +one. Deeper symptom-by-symptom triage lives in +[`../docs/references/verification-debugging.md`](../docs/references/verification-debugging.md). + +## Maintaining this file + +Keep it true to the branch (see [`../docs/DOC-MAINTENANCE.md`](../docs/DOC-MAINTENANCE.md)): + +```bash +ls e2e/fixtures.ts e2e/utils.ts e2e/server-fixtures.ts e2e/agent-fixtures.ts +grep -n "testIgnore\|outputDir" playwright.config.ts playwright.scratch.config.ts +grep -n "process.env.E2E_\|process.env.CI" e2e/*.ts +grep -rn "host-resolver-rules" e2e/ +node -e "console.log(Object.keys(require('./package.json').scripts).filter(s=>s.includes('e2e')))" +``` + +## Related + +[`../docs/verification.md`](../docs/verification.md) · +[`../docs/references/verification-report-template.md`](../docs/references/verification-report-template.md) · +[`../docs/references/develop-testing.md`](../docs/references/develop-testing.md) · +[`../AGENTS.md`](../AGENTS.md) diff --git a/eslint.config.mjs b/eslint.config.mjs index 215f0a05a..494f82a00 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -165,5 +165,24 @@ export default [ "@typescript-eslint/await-thenable": "error", }, }, - { ignores: ["dist/", "example/", ".claude/", "playwright-report/", "test-results/", "coverage/"] }, + { + // 全局忽略:构建产物、本地工具目录、一次性验证脚本。这些目录都已 gitignore,CI 的全新 checkout 里 + // 根本不存在,只有本地跑 `pnpm lint` 时会被扫到——不排除掉会让本地 lint 报出上万条与源码无关的错误。 + // 用 `**/dist/` 而不是 `dist/`:flat config 的 ignores 锚定在配置文件所在目录,`dist/` 只能匹配仓库根, + // 匹配不到 `.dev-kit/**/dist/` 这类嵌套构建产物。 + ignores: [ + "**/dist/", + "example/", + ".claude/", + ".codex/", + ".dev-kit/", + ".omc/", + ".superpowers/", + "e2e/scratch/", + "playwright-report/", + "test-results/", + ".vitest-reports/", + "coverage/", + ], + }, ]; diff --git a/src/pages/options/App.tsx b/src/pages/options/App.tsx index 38fa72fe9..e9c0a56ec 100644 --- a/src/pages/options/App.tsx +++ b/src/pages/options/App.tsx @@ -1,4 +1,4 @@ -import { HashRouter, Routes, Route, Outlet, Navigate, useLocation } from "react-router-dom"; +import { createHashRouter, Navigate, Outlet, RouterProvider, useLocation } from "react-router-dom"; import Sidebar from "./layout/Sidebar"; import ScriptList from "./routes/ScriptList"; import SubscribeList from "./routes/SubscribeList"; @@ -63,34 +63,42 @@ export function Layout() { } export default function App() { - return ( - + return ; +} + +const router = createHashRouter([ + { + element: ( - - }> - } /> - } /> - {EnableAgent && ( - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - )} - } /> - } /> - } /> - } /> - } /> - } /> - - + - - ); -} + ), + children: [ + { index: true, element: }, + { path: "subscribe", element: }, + ...(EnableAgent + ? [ + { + path: "agent", + children: [ + { index: true, element: }, + { path: "chat", element: }, + { path: "provider", element: }, + { path: "skills", element: }, + { path: "mcp", element: }, + { path: "tasks", element: }, + { path: "opfs", element: }, + { path: "settings", element: }, + ], + }, + ] + : []), + { path: "logs", element: }, + { path: "logger", element: }, + { path: "tools", element: }, + { path: "settings", element: }, + { path: "setting", element: }, + { path: "script/editor/:uuid?", element: }, + ], + }, +]); diff --git a/src/pages/options/routes/ScriptEditor/index.test.tsx b/src/pages/options/routes/ScriptEditor/index.test.tsx index 576514e04..a163194ae 100644 --- a/src/pages/options/routes/ScriptEditor/index.test.tsx +++ b/src/pages/options/routes/ScriptEditor/index.test.tsx @@ -1,4 +1,5 @@ import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { createMemoryRouter, RouterProvider } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { Script } from "@App/app/repo/scripts"; import { SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts"; @@ -37,12 +38,6 @@ const script = { checktime: 0, } as unknown as Script; -vi.mock("react-router-dom", () => ({ - useNavigate: () => vi.fn(), - useParams: () => ({ uuid: script.uuid }), - useSearchParams: () => [new URLSearchParams()], -})); - vi.mock("@App/pages/options/routes/ScriptList/hooks", () => ({ useScriptDataManagement: () => ({ scriptList: [script], setScriptList: vi.fn(), loadingList: false }), })); @@ -65,12 +60,19 @@ vi.mock("./tabs/ResourcePane", () => ({ })); vi.mock("./tabs/CodePane", () => ({ CodePane: ({ + onChange, onSave, tab, }: { + onChange: (code: string) => void; onSave: (script: Script, editor: { getValue: () => string }) => void; tab: { script: Script }; - }) =>