From 3dcb057c97fc2b6e6c9d78f6d0a442e24d4a74a4 Mon Sep 17 00:00:00 2001 From: betterer <54624517+better-er@users.noreply.github.com> Date: Mon, 14 Sep 2026 04:20:58 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20extraTools=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=8E=A5=E7=AE=A1=E5=85=B6=E4=BB=96=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=8D=A1=E7=89=87=EF=BC=8C=E5=B9=B6=E6=8C=89=E6=96=B0?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E6=95=B4=E7=90=86=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 38 +++---- .github/workflows/release.yml | 68 +++++++------ README.md | 49 ++++++++- cordis.patch.yml | 21 ++-- package.json | 17 +++- pnpm-lock.yaml | 181 ++++++++++++++++++++++++++++++---- src/client.ts | 127 +++++++++++++++++++++--- src/index.ts | 69 +++++++++++-- 8 files changed, 460 insertions(+), 110 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0d1f2e..8b457c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,64 +1,56 @@ name: CI -# 每次 push 到主分支、以及任何 PR 包括来自 fork 的 PR 都自动跑完整检查。 -# 本插件采用 TypeScript 源码与 tsc 构建,CI 会安装锁定依赖并重建发布产物。 +# 每次 push 到主分支、任何 PR 包括来自 fork 的 PR 都自动跑完整检查。 +# 本插件是 TypeScript 源码加 tsdown 构建,lib/ 是产物且不入库,因此 CI 检查 = 类型检查 + 构建 + 产物齐全校验,全绿才允许合并。 on: push: branches: - - master - main pull_request: # fork PR 也可触发;此工作流不读写仓库 secrets,fork 环境下安全。 branches: - - master - main -# 同一分支/PR 上排队的新提交会取消正在跑的旧任务,避免资源浪费。 +# 同一分支或 PR 上排队的新提交会取消正在跑的旧任务,避免资源浪费。 concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: check: - name: 类型检查 · 构建 · 产物校验 + name: typecheck · build runs-on: ubuntu-latest - strategy: - matrix: - # 本地与线上主推的运行时;运行时零依赖,两个版本都验证一次。 - node: - - 22 - - 26 steps: - name: Checkout uses: actions/checkout@v4 - - name: 配置 pnpm + - name: Setup pnpm uses: pnpm/action-setup@v4 with: + # 版本必须与 package.json 的 packageManager 完全一致,否则 action-setup 会报 Multiple versions of pnpm specified。 version: 9.15.9 - - name: 配置 Node.js + - name: Setup Node uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node }} + # 与本地 node -v 锁死同一版本,见工作区 AGENTS.md 的 Node 版本约定。 + node-version: 26.3.1 cache: pnpm - - name: 安装依赖 + - name: Install dependencies run: pnpm install --frozen-lockfile - - name: 类型检查 + - name: Typecheck run: pnpm typecheck - - name: 构建 + - name: Build run: pnpm build - # 出厂文件齐全性校验,缺失会在此失败,让 PR 作者一眼看到问题。 - - name: 产物齐全性校验 + # 本插件是双半身且产出 dts,产物缺失会在此失败。 + - name: Verify build outputs run: | test -f lib/index.js test -f lib/index.d.ts test -f lib/client.js - test -f lib/client.js.map test -f cordis.patch.yml - test -f package.json - echo "发布产物齐全" + echo "构建产物齐全:lib/index.js · lib/index.d.ts · lib/client.js · cordis.patch.yml" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index feab097..f094ade 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,8 @@ name: Release -# 推送形如 vX.Y.Z,如 v0.1.0,的 tag 时自动发版: -# 凭 tag 号更新 package.json 版本并提交回 main → 安装依赖 → 类型检查 → 构建 → 产物校验 -# → 发布 npm → 生成 GitHub Release draft。人只需打一个 tag,其余自动衔接。 -# 注意:npm 发布依赖 npmjs 侧开启的 Trusted Publishing -# npmjs.com → 本包 → Enable trusted publishing → 绑本仓库 GitHub Actions, -# 无需配置 NPM_TOKEN;未开启时发布步骤会失败。 +# 推送形如 vX.Y.Z 如 v0.1.0 的 tag 时自动发版:凭 tag 号更新 package.json 版本并提交回 main → 类型检查 → 构建 → 发布 npm → 生成 GitHub Release draft。 +# 人只需打一个 tag,其余自动衔接。 +# 注意:npm 发布走 Trusted Publishing,无需 NPM_TOKEN;未在 npm 侧绑定信任关系时发布会失败。 on: push: tags: @@ -26,8 +23,7 @@ jobs: id-token: write # fork 永不触发 tag 发布,仅作为兜底防护。 if: startsWith(github.ref, 'refs/tags/v') - # 绑定 GitHub Actions environment「release」,使 OIDC subject 稳定为 - # environment:release,npm 侧 Trusted Publisher 需填相同 Environment 名。 + # 绑定 GitHub Actions environment「release」,使 OIDC subject 稳定为 environment:release,npm 侧 Trusted Publisher 需填相同 Environment 名。 environment: release steps: - name: Checkout @@ -38,8 +34,26 @@ jobs: # 默认 GITHUB_TOKEN 在分支保护下会被拒,此处显式传 token。 token: ${{ secrets.GITHUB_TOKEN }} + # 前置守卫:仅当该 tag 版本尚未发布到 npm 时才继续发版。 + # 补历史 tag 时对应版本早已在 npm 上,重复 publish 会报错,且版本回写会把 main 改回旧号,故已发布则跳过后续所有步骤。 + # npm view 只读 registry,无需凭证。 + - name: Skip when tag version already published + id: published + run: | + set -euo pipefail + PKG="dsh-edit-diff" + TAG="${GITHUB_REF_NAME#v}" + if npm view "$PKG@$TAG" version >/dev/null 2>&1; then + echo "已存在 $PKG@$TAG,跳过发布" + echo "published=true" >> "$GITHUB_OUTPUT" + else + echo "未发布 $PKG@$TAG,继续" + echo "published=false" >> "$GITHUB_OUTPUT" + fi + # 版本号仅凭 tag 号决定,写入 package.json 并提交回 main。 - name: Sync version to main + if: steps.published.outputs.published != 'true' run: | set -euo pipefail TAG="${GITHUB_REF_NAME#v}" # 去 v 前缀得版本号,如 0.1.0 @@ -65,44 +79,42 @@ jobs: git push origin main } - - name: 配置 pnpm + - name: Setup pnpm + if: steps.published.outputs.published != 'true' uses: pnpm/action-setup@v4 with: version: 9.15.9 - - name: 配置 Node.js + # setup-node 的 registry-url 告知 npm 向哪个 registry 换 Trusted Publishing 临时凭证。 + # node-version 与本地 node -v 锁死同一版本。它同时满足 npm 官方 Trusted Publishing 的下限:捆绑 npm 需大于等于 11.5.1,否则 npm CLI 无法做 OIDC 自动换权,会回退用失效 token 导致 PUT 404 is not in registry。 + - name: Setup Node + if: steps.published.outputs.published != 'true' uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 26.3.1 cache: pnpm registry-url: https://registry.npmjs.org - - name: 安装依赖 + - name: Install dependencies + if: steps.published.outputs.published != 'true' run: pnpm install --frozen-lockfile - - name: 类型检查 + # 本插件暂无单测,以类型检查作为发布前的最后一道门。 + - name: Typecheck + if: steps.published.outputs.published != 'true' run: pnpm typecheck - - name: 构建 + - name: Build + if: steps.published.outputs.published != 'true' run: pnpm build - - name: 产物齐全性校验 - run: | - test -f lib/index.js - test -f lib/index.d.ts - test -f lib/client.js - test -f lib/client.js.map - test -f cordis.patch.yml - test -f package.json - echo "发布产物齐全" - - # Trusted Publishing:npm 凭 workflow 的 OIDC 身份直接授权, - # 无需 NPM_TOKEN;--provenance 会在 npm 上附构建来源证明。 - # node-version 24 保证捆绑 npm>=11.5.1,npm CLI 才能做 OIDC 自动换权发布。 + # Trusted Publishing:npm 凭 workflow 的 OIDC 身份直接授权,无需 NPM_TOKEN;--provenance 会在 npm 上附构建来源证明。 - name: Publish to npm - run: pnpm publish --no-git-checks --provenance --access public + if: steps.published.outputs.published != 'true' + run: npm publish --provenance --access public - name: Create GitHub Release draft + if: steps.published.outputs.published != 'true' uses: softprops/action-gh-release@v2 with: draft: true diff --git a/README.md b/README.md index aaf2684..4f8dd47 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 接管 DSH 浏览器的 `edit` / `write` 工具卡片,用近线性行级 diff 做真正的差异展示,消除内置 DiffBlock 把「未变化的相同行」在删除区红 `-` 行和新增区绿 `+` 行各渲染一遍的重复。 -纯浏览器端 client 插件,单击即换新渲染,不碰 DSH 源码。 +渲染全在浏览器端,主机端半身只负责把配置转成 settings 命名空间。单击即换新渲染,不碰 DSH 源码。 ## 引言 @@ -23,6 +23,47 @@ - **复制与统计**:复制只含差异行,`- `/`+ ` 前缀;底部 `└ +A -R · N file(s)` 变更统计。 - **单文件与多文件**:`edit` / `write` 双双接管,覆盖新建只显示绿色新增,纯删、多 hunk 等场景。 - **PTC 模式兼容**:`run_code` 子调用中的 `edit` / `write` 同样显示优化后的修改 diff,通过 `callId` 中的 `:code:` 标记识别子调用。注意 `write` 是全文件覆写,无旧内容,只能显示全新增;想看精确 diff 应使用 `edit`。 +- **接管其他工具**:`extraTools` 让 [dsh-remote-file-system](https://github.com/better-er/dsh-remote-file-system) 的 `edit_remote`、`write_remote` 一并使用优化显示,插件已默认开启这两个,参数名不同的工具可逐项覆盖。 + +## 配置 + +`extraTools` 列出 `edit` 与 `write` 之外要接管的工具。每条写工具名和读取参数用的参数名,参数名省略时按 `file_path`、`old_string`、`new_string`、`content` 取值。 + +插件自带的 `cordis.patch.yml` 已经默认接管 [dsh-remote-file-system](https://github.com/better-er/dsh-remote-file-system) 的这两个远程工具: + +```yaml +- insert: + - id: dsh-edit-diff + name: dsh-edit-diff + config: + extraTools: + - name: edit_remote + - name: write_remote +``` + +它们的参数名和原生一致,只写名字即可。参数名不同的工具再补: + +```yaml + extraTools: + - name: some_tool + pathKey: target + oldKey: before + newKey: after + contentKey: body +``` + +要改这份默认,在 profile 的 `cordis.patch.yml` 里按 id 覆盖: + +```yaml +- id: dsh-edit-diff + config: + extraTools: + - name: some_tool +``` + +判断规则:参数里同时有旧文本与新文本就按 edit 型渲染,否则有整文件内容就按 write 型渲染。 + +配置经 settings 命名空间 `dsh-edit-diff` 送到浏览器端,settings 变化会让浏览器端重建这批 key 的注册。 ## 它长什么样 @@ -67,12 +108,12 @@ dsh plugin --profile web remove dsh-edit-diff - 是**标准形态的 dsh client 插件**,声明 `dsh.client`,导出 `./client`。 - 同时声明了 `dsh.bundle`,因此也是一个**自挂载的 bundle 层插件**:用 `dsh plugin --profile add` 从 GitHub 安装后,会被自动识别为 profile layer 并挂载,无需手工写组合 entry。 -- 纯浏览器半身,无 host 行为;`lib/index.js` 是无操作的 no-op 主机插件标准双面包约定。 -- **UI 挂载点**:接管 keyed 槽位 `tool.call.toolview` 的 `edit` 与 `write` 两个 key。 +- 主机端半身只做一件事:用 `ctx.settings.installSection()` 把 entry 配置注册成 settings 命名空间 `dsh-edit-diff`。浏览器端拿不到 cordis 配置,这是双半插件传配置的唯一通道。 +- **UI 挂载点**:接管 keyed 槽位 `tool.call.toolview` 的 `edit` 与 `write` 两个 key,以及 `extraTools` 里声明的每个工具名。 - **遮蔽而非冲突**:注册时显式传 `priority: -1`低于内置 `file-mutation-toolview` 的默认 0,用更低优先级遮蔽默认渲染,而不是在同一优先级上 clash。 - **PTC 模式**:通过 `callId` 包含 `:code:` 判断是否为 `run_code` 子调用,从 `argsRaw` 中提取 `old_string`/`new_string` 即 edit 或 `content` 即 write 动态构建 diff。 - **构建型**:TypeScript 源码位于 `src/`,`pnpm build` 通过 tsdown 生成 `lib/index.js`、`lib/index.d.ts`、`lib/client.js` 与 sourcemap,运行时只加载 `lib/` 发布产物。 -- 无运行时 npm 依赖,diff 算法就地内联;浏览器端只向宿主模块表请求 react。 +- diff 算法就地内联,浏览器端只向宿主模块表请求 react;主机端向宿主解析 `@deepseek-ai/schemastery`。 本地开发: diff --git a/cordis.patch.yml b/cordis.patch.yml index e2ee477..bd6a388 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -1,13 +1,12 @@ -# dsh-edit-diff bundle patch: mount the edit/write diff-card plugin. -# -# Applied when this bundle appears in a profile's dsh.profile.bundles list. -# The node half (package root) is a no-op Cordis plugin; the browser half -# (dsh.client) is discovered automatically through the package's -# dsh.client declaration + exports["./client"]. It registers two keyed -# `tool.call.toolview` occupants (`edit`, `write`) that replace the shipped -# DiffBlock render, eliminating same-line duplication with a near-linear -# line diff + intra-line character highlight. - +# dsh-edit-diff bundle patch:把 edit/write 的 diff 卡片插件插进 profile 的 layer stack。 +# profile 引用本 bundle 时自动挂载。 +# 浏览器半身由 dsh.client 声明自动发现,注册 tool.call.toolview 的 edit、write 与 extraTools 里的每个工具名,替换内置 DiffBlock。 +# 主机端半身把 entry 配置注册成 settings 命名空间 dsh-edit-diff,浏览器端只能从这里拿到配置。 +# 下面的默认顺带接管两个远程文件工具,参数名与原生一致,按 id 覆盖 extraTools 即可改。 - insert: - id: dsh-edit-diff - name: dsh-edit-diff \ No newline at end of file + name: dsh-edit-diff + config: + extraTools: + - name: edit_remote + - name: write_remote \ No newline at end of file diff --git a/package.json b/package.json index c4aba13..a9456f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dsh-edit-diff", - "version": "0.3.0", - "description": "质量 diff:接管 edit/write 工具卡片,行级去重与行内高亮,消除相同内容在删除与新增两区的重复;兼容 PTC 模式,标准可安装的 dsh 客户端插件", + "version": "0.4.0", + "description": "质量 diff:接管 edit/write 及 extraTools 配置的其他工具卡片,行级去重与行内高亮,消除相同内容在删除与新增两区的重复;兼容 PTC 模式,标准可安装的 dsh 客户端插件", "keywords": [ "deepseek-harness", "dsh", @@ -53,10 +53,21 @@ "prepare": "tsdown", "typecheck": "tsc --noEmit" }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.2", + "@deepseek-ai/dsh-settings": "^0.1.5-rc.2", + "@deepseek-ai/schemastery": "^3.18.2" + }, "devDependencies": { + "@deepseek-ai/cordis": "^4.0.2", + "@deepseek-ai/dsh-settings": "^0.1.5-rc.2", + "@deepseek-ai/schemastery": "^3.18.2", "@types/react": "~18.3.1", "tsdown": "^0.23.0", "typescript": "^5.9.3" }, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=26.3.1" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 059a712..816a2e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: devDependencies: + '@deepseek-ai/cordis': + specifier: ^4.0.2 + version: 4.0.2 + '@deepseek-ai/dsh-settings': + specifier: ^0.1.5-rc.2 + version: 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2) + '@deepseek-ai/schemastery': + specifier: ^3.18.2 + version: 3.18.2 '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -20,6 +29,80 @@ importers: packages: + '@deepseek-ai/cordis@4.0.2': + resolution: {integrity: sha512-asOnXP1TzFSFQlHb1iegDZp0z/8WD1c7YNrwJR/Tx2bzNuMXfcekE/I67Iv6SQXeLB4csxqCngzQKANP7gdw0g==} + hasBin: true + peerDependencies: + '@deepseek-ai/cordis-plugin-include': ^1.0.7 + '@deepseek-ai/cordis-plugin-loader': ^1.0.3 + peerDependenciesMeta: + '@deepseek-ai/cordis-plugin-include': + optional: true + '@deepseek-ai/cordis-plugin-loader': + optional: true + + '@deepseek-ai/cosmokit@1.8.3': + resolution: {integrity: sha512-qBo+ronVM6Eu2WNVJXi8JcMiqZ19T9BRIpV+5qJUFPXjGH/Z0QKcQMC/IZJ7L394YTOtJgcovbk9qP0w2GsBXQ==} + + '@deepseek-ai/dsh-brand@0.1.5-rc.2': + resolution: {integrity: sha512-/+3TzQRYT4M8NINZ9OrsL5VWNyQSXof8mFLx3txiosWA7Bt0H6UcxUdKGXVwj6gCXhsQ74mzEHm2ucsflG0mYA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + + '@deepseek-ai/dsh-invariants@0.1.5-rc.2': + resolution: {integrity: sha512-oUxttB2yjAgkk47AiXOCxk9GwnfGvIEGsQfnMD7fukwIdD3KLJObU7+nz63tbFN80rjwMI8rg+p9QBQ10KU5+g==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + + '@deepseek-ai/dsh-llm@0.1.5-rc.2': + resolution: {integrity: sha512-Z7BVsBkK24SE4EItQeow8PHms/9GP0DSTi337vTAa/RY7tNg2Snz3INcXUj6CPZfvntQr1in9op9wLI+rfNsqA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + + '@deepseek-ai/dsh-scope@0.1.5-rc.2': + resolution: {integrity: sha512-JwItISje52iVIjlGMNayRVMZrvZU/0i3Gr0s+coND5nBp5lS+pOj9Zf1RQzA5cWjHYCWKmDuQc+m6zGlwmtAeQ==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + '@deepseek-ai/dsh-invariants': ^0.1.5-rc.2 + + '@deepseek-ai/dsh-session@0.1.5-rc.2': + resolution: {integrity: sha512-y+klWiGAWR4m4cc4ylurA0cW63673B4N8cr2ANMimweDZAfxL4XVBC7WiD/5DT2DtIhYmVZhz/niyS/WbniUTA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + '@deepseek-ai/dsh-scope': ^0.1.5-rc.2 + + '@deepseek-ai/dsh-settings@0.1.5-rc.2': + resolution: {integrity: sha512-LI2Y6GkEs9ALMW+7S9jHPeEZDXHtG5X6cix1HdJ1rRxTEO5427QlYhMiz45rk7hqZ8ca2O4XFMW8IWXFZQLxmw==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + '@deepseek-ai/dsh-brand': ^0.1.5-rc.2 + '@deepseek-ai/dsh-invariants': ^0.1.5-rc.2 + '@deepseek-ai/dsh-session': ^0.1.5-rc.2 + '@deepseek-ai/schemastery': ^3.18.2 + + '@deepseek-ai/dsh-timeout@0.1.5-rc.2': + resolution: {integrity: sha512-FgfaAw8Zt5X4Y6Ljh833B8Hy7h96FeR+yjNKI3iSyZSqREqOBgvDGXFfQlSu6V3Buvw/cbI/CJUQJvfRBKXOiA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + + '@deepseek-ai/dsh-typert-protocol@0.1.5-rc.2': + resolution: {integrity: sha512-zP8J20rBXa1AFjKL2i83JBeQcePZx8yal3pNojj4t7CCokMcoFMUbr50woa6fY1tRJ7qHxgypO3rH8Q6YWgmiA==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + + '@deepseek-ai/dsh-util-crypto@0.1.5-rc.2': + resolution: {integrity: sha512-JR0aJEUL35RE8FVT89wMZMbPwMrbkjNcy+gE9pq70d/m2Zxw3a0iNer+BAxPAKYPbmy0xDDXReTwXiecKVy4sQ==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + + '@deepseek-ai/dsh-util-values@0.1.5-rc.2': + resolution: {integrity: sha512-Cr0TkM6dFAD+Iy4sNXH/afJVMHwJXoOztivj+RflkYoACsOq+Ix0vdHn36hwW7mayU8OK3ZKIZUQ6BPo1fHvUg==} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.2 + + '@deepseek-ai/schemastery@3.18.2': + resolution: {integrity: sha512-njDtZsznjYxok7KLLlHOPyuv2efdWVbSflAHgztSfbMsg+CVraEoRe2DjOCgClYv3ZCSm7WXoaUkbB/+RY7tWQ==} + '@oxc-project/types@0.148.0': resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} @@ -67,42 +150,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.2.7': resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.2.7': resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.2.7': resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.2.7': resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.2.7': resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.2.7': resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} @@ -125,6 +202,9 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -155,37 +235,31 @@ packages: resolution: {integrity: sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw==} cpu: [arm] os: [linux] - libc: [glibc] '@yuku-codegen/binding-linux-arm-musl@0.9.5': resolution: {integrity: sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w==} cpu: [arm] os: [linux] - libc: [musl] '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': resolution: {integrity: sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw==} cpu: [arm64] os: [linux] - libc: [glibc] '@yuku-codegen/binding-linux-arm64-musl@0.9.5': resolution: {integrity: sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg==} cpu: [arm64] os: [linux] - libc: [musl] '@yuku-codegen/binding-linux-x64-gnu@0.9.5': resolution: {integrity: sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g==} cpu: [x64] os: [linux] - libc: [glibc] '@yuku-codegen/binding-linux-x64-musl@0.9.5': resolution: {integrity: sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew==} cpu: [x64] os: [linux] - libc: [musl] '@yuku-codegen/binding-win32-arm64@0.9.5': resolution: {integrity: sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w==} @@ -221,37 +295,31 @@ packages: resolution: {integrity: sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw==} cpu: [arm] os: [linux] - libc: [glibc] '@yuku-parser/binding-linux-arm-musl@0.9.5': resolution: {integrity: sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig==} cpu: [arm] os: [linux] - libc: [musl] '@yuku-parser/binding-linux-arm64-gnu@0.9.5': resolution: {integrity: sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA==} cpu: [arm64] os: [linux] - libc: [glibc] '@yuku-parser/binding-linux-arm64-musl@0.9.5': resolution: {integrity: sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ==} cpu: [arm64] os: [linux] - libc: [musl] '@yuku-parser/binding-linux-x64-gnu@0.9.5': resolution: {integrity: sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA==} cpu: [x64] os: [linux] - libc: [glibc] '@yuku-parser/binding-linux-x64-musl@0.9.5': resolution: {integrity: sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw==} cpu: [x64] os: [linux] - libc: [musl] '@yuku-parser/binding-win32-arm64@0.9.5': resolution: {integrity: sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg==} @@ -414,8 +482,81 @@ packages: yuku-parser@0.9.5: resolution: {integrity: sha512-IBnAdNVMswWbJcM7woPluOudectJzFjJ1N8jVRxP9Uq4CIv5hZdBIcdmtRbFDYF/Ph2j2gVup4YJ4N9mh0jYFA==} + zod@4.6.4: + resolution: {integrity: sha512-AXSD6hvGdvRjajG/l1cC+d6IrhH+sjmPKtYeQdJIK8MFJl3LyClzS+o/YsVC+zQZPupAaeH5skwwm8YqYH7BqA==} + snapshots: + '@deepseek-ai/cordis@4.0.2': + dependencies: + '@deepseek-ai/cosmokit': 1.8.3 + '@standard-schema/spec': 1.1.0 + + '@deepseek-ai/cosmokit@1.8.3': {} + + '@deepseek-ai/dsh-brand@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + + '@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/schemastery': 3.18.2 + + '@deepseek-ai/dsh-llm@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/dsh-brand': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/dsh-timeout': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/dsh-typert-protocol': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/dsh-util-crypto': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/dsh-util-values': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/schemastery': 3.18.2 + zod: 4.6.4 + + '@deepseek-ai/dsh-scope@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2))': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/dsh-invariants': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + + '@deepseek-ai/dsh-session@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)))': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/dsh-brand': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/dsh-llm': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/dsh-scope': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)) + '@deepseek-ai/dsh-util-values': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + + '@deepseek-ai/dsh-settings@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.2)': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/dsh-brand': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/dsh-invariants': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/dsh-session': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2))) + '@deepseek-ai/dsh-util-values': 0.1.5-rc.2(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/schemastery': 3.18.2 + + '@deepseek-ai/dsh-timeout@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + + '@deepseek-ai/dsh-typert-protocol@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + + '@deepseek-ai/dsh-util-crypto@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + + '@deepseek-ai/dsh-util-values@0.1.5-rc.2(@deepseek-ai/cordis@4.0.2)': + dependencies: + '@deepseek-ai/cordis': 4.0.2 + + '@deepseek-ai/schemastery@3.18.2': + dependencies: + '@deepseek-ai/cosmokit': 1.8.3 + '@standard-schema/spec': 1.1.0 + '@oxc-project/types@0.148.0': {} '@quansync/fs@1.0.0': @@ -469,6 +610,8 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@standard-schema/spec@1.1.0': {} + '@types/prop-types@15.7.15': {} '@types/react@18.3.31': @@ -695,3 +838,5 @@ snapshots: '@yuku-parser/binding-linux-x64-musl': 0.9.5 '@yuku-parser/binding-win32-arm64': 0.9.5 '@yuku-parser/binding-win32-x64': 0.9.5 + + zod@4.6.4: {} diff --git a/src/client.ts b/src/client.ts index f8f5c23..e3c382c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -97,10 +97,69 @@ interface TimerService { timeout(callback: () => void, ms: number): unknown } -/** Cordis 上下文的最小子集,插件只用到 get 与 effect。 */ +/** Cordis 上下文的最小子集,插件只用到 get、inject 与 effect。 */ interface PluginContext { get?(name: string): unknown - effect(callback: () => void | (() => void)): void + inject?(deps: readonly string[], callback: (ctx: PluginContext) => void): void + effect(callback: () => void | (() => void)): (() => void) | void +} + +// ==================== 额外工具的参数名 ==================== + +/** 浏览器端读到的 extraTools 条目。 */ +interface ExtraToolConfig { + name: string + pathKey?: string + oldKey?: string + newKey?: string + contentKey?: string +} + +/** settings 命名空间 dsh-edit-diff 的解析值。 */ +interface PluginConfig { + extraTools?: ExtraToolConfig[] +} + +/** settings 服务的最小子集。 */ +interface SettingsScopeService { + bind(spec: { namespace: string }): SettingsScopeFace +} + +/** 一个 settings 命名空间的读取面。 */ +interface SettingsScopeFace { + getSnapshot(): { value: T | undefined } + subscribe(listener: () => void): () => void +} + +/** 读取工具参数用的参数名。 */ +interface KeySpec { + pathKey: string + oldKey: string + newKey: string + contentKey: string +} + +/** 原生 edit 与 write 的参数名,也是未声明覆盖时的默认值。 */ +const DEFAULT_SPEC: KeySpec = { pathKey: 'file_path', oldKey: 'old_string', newKey: 'new_string', contentKey: 'content' } + +/** 额外接管的工具名到参数名,由 settings 的 extraTools 填充。 */ +const extraSpecs = new Map() + +/** 把配置条目补全成完整参数名。 */ +function toKeySpec(tool: ExtraToolConfig): KeySpec { + return { + pathKey: tool.pathKey ?? DEFAULT_SPEC.pathKey, + oldKey: tool.oldKey ?? DEFAULT_SPEC.oldKey, + newKey: tool.newKey ?? DEFAULT_SPEC.newKey, + contentKey: tool.contentKey ?? DEFAULT_SPEC.contentKey, + } +} + +/** 取工具名对应的参数名,非本插件接管的工具返回 null。 */ +function specFor(toolName: string | undefined): KeySpec | null { + if (toolName === 'edit' || toolName === 'write') return DEFAULT_SPEC + if (toolName === undefined) return null + return extraSpecs.get(toolName) ?? null } // ==================== 近线性 diff 核心 ==================== @@ -259,7 +318,7 @@ function narrowDiffs(diffs: unknown): FileDiff[] | null { } /** 从卡片数据块中取出 diff,取不到时回退到参数意图。 */ -function extractDiffs(block: ToolBlock): FileDiff[] | null { +function extractDiffs(block: ToolBlock, spec: KeySpec | null): FileDiff[] | null { const done = 'kind' in block // PTC 子调用标记:新版 DSH 用 parentCallId;旧版 callId 含 ':code:',双兼容 const subCall = block.parentCallId !== void 0 || @@ -284,19 +343,20 @@ function extractDiffs(block: ToolBlock): FileDiff[] | null { if (done && !subCall && block.isError === true) return null const rawArgs = (block.call !== undefined ? block.call.argsRaw : block.argsRaw) ?? '' const argsRaw = typeof rawArgs === 'string' ? rawArgs : '' - const name = block.call !== undefined ? block.call.name : block.name - if (name === 'edit' || name === 'write') { + if (spec !== null) { try { const args: unknown = JSON.parse(argsRaw) if (args !== null && typeof args === 'object') { const record = args as Record - const filePath = record.file_path + const filePath = record[spec.pathKey] if (typeof filePath === 'string' && filePath !== '') { - if (name === 'write' && typeof record.content === 'string') { - return narrowDiffs([{ path: filePath, oldText: null, newText: record.content }]) + const oldText = record[spec.oldKey] + const newText = record[spec.newKey] + if (typeof oldText === 'string' && typeof newText === 'string') { + return narrowDiffs([{ path: filePath, oldText: oldText || null, newText }]) } - if (name === 'edit' && typeof record.old_string === 'string' && typeof record.new_string === 'string') { - return narrowDiffs([{ path: filePath, oldText: record.old_string || null, newText: record.new_string }]) + if (typeof record[spec.contentKey] === 'string') { + return narrowDiffs([{ path: filePath, oldText: null, newText: record[spec.contentKey] as string }]) } } } @@ -338,17 +398,25 @@ function firstLine(t: string): string { /** 由数据块推导卡片模型。 */ function buildCardModel(block: ToolBlock, toolName: string | undefined, cwd: string | undefined): CardModel { const done = 'kind' in block + const spec = specFor(toolName) const rawArgs = done ? (block.call?.argsRaw ?? '') : (block.argsRaw ?? '') const argsRaw = typeof rawArgs === 'string' ? rawArgs : '' const state: CardState = !done ? 'running' : block.error?.code === 'interrupted' ? 'stopped' : block.isError === true ? 'error' : 'ok' + let kind: 'edit' | 'write' | null = toolName === 'edit' ? 'edit' : toolName === 'write' ? 'write' : null let filePath: string | undefined let summary = '' try { const parsed: unknown = JSON.parse(argsRaw) if (parsed !== null && typeof parsed === 'object') { const record = parsed as Record - const p = record.file_path ?? record.path + const p = (spec === null ? undefined : record[spec.pathKey]) ?? record.path if (typeof p === 'string' && p !== '') filePath = firstLine(p) + if (kind === null && spec !== null) { + const oldText = record[spec.oldKey] + const newText = record[spec.newKey] + if (typeof oldText === 'string' && typeof newText === 'string') kind = 'edit' + else if (typeof record[spec.contentKey] === 'string') kind = 'write' + } const values = Object.values(record).filter((v): v is string => typeof v === 'string' && v !== '') summary = values.length > 0 ? firstLine(values[0] as string) : argsRaw } else summary = firstLine(argsRaw) @@ -358,8 +426,8 @@ function buildCardModel(block: ToolBlock, toolName: string | undefined, cwd: str const root = cwd.replace(/[/\\]+$/, '') if (filePath.startsWith(root + '/') || filePath.startsWith(root + '\\')) summary = filePath.slice(root.length + 1) } - const title = toolName === 'edit' ? 'Edit' : toolName === 'write' ? 'Write' : (toolName ?? '') - const diffs = extractDiffs(block) + const title = kind === 'edit' ? 'Edit' : kind === 'write' ? 'Write' : (toolName ?? '') + const diffs = extractDiffs(block, spec) const output = done ? (resultText(block) || null) : null return { title, summary, filePath, state, diffs, output } } @@ -571,6 +639,39 @@ const plugin = { slots.register({ name: 'tool.call.toolview', key: 'edit', priority: -1 }, DiffCard) slots.register({ name: 'tool.call.toolview', key: 'write', priority: -1 }, DiffCard) }) + + // extraTools 经 settings 命名空间到达。没有挂载 settings 时上面两张卡片照常工作。 + if (typeof ctx.inject !== 'function') return + ctx.inject(['settingsScope'], (settingsCtx) => { + const settingsScope = settingsCtx.get?.('settingsScope') as SettingsScopeService | undefined + if (settingsScope === undefined || typeof settingsScope.bind !== 'function') return + const scope = settingsScope.bind({ namespace: 'dsh-edit-diff' }) + let disposeExtra: (() => void) | void + // 配置变化时整批重建:先换参数名表,再重建这批 key 的注册。 + const sync = (): void => { + if (typeof disposeExtra === 'function') disposeExtra() + const value = scope.getSnapshot().value + const tools = Array.isArray(value?.extraTools) ? value.extraTools : [] + extraSpecs.clear() + const names: string[] = [] + for (const tool of tools) { + if (tool === null || typeof tool !== 'object') continue + if (typeof tool.name !== 'string' || tool.name === '') continue + if (tool.name === 'edit' || tool.name === 'write') continue + extraSpecs.set(tool.name, toKeySpec(tool)) + names.push(tool.name) + } + disposeExtra = ctx.effect(() => { + for (const name of names) { + slots.inject('tool.call.toolview', () => { + slots.register({ name: 'tool.call.toolview', key: name, priority: -1 }, DiffCard) + }) + } + }) + } + scope.subscribe(sync) + sync() + }) }, } diff --git a/src/index.ts b/src/index.ts index 01c0179..406a738 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,62 @@ /** - * 主机端:空操作 Cordis 插件。 - * 所有行为在浏览器端 lib/client.js,通过 package.json dsh.client 声明被 web 客户端模块系统发现。 - * 本半身存在是为了让插件根目录成为完整的双面包,在宿主 Loader 中显示为一个条目。 - * 通过 dsh.bundle 成为自挂载 bundle 层,一条 dsh plugin --profile web add 即可完整激活。 + * 主机端:把本插件的 cordis 配置注册成 settings 命名空间,供浏览器端读取。 + * + * 浏览器半身拿不到 cordis 配置:__DSH_BOOT__ 的 entry 不含 config 字段,客户端内核创建插件时也不传 config。 + * installSection 把 entry 配置注册为 base 层,用户分节覆盖其上,client 半身用 ctx.settingsScope 读解析值。 + * + * 没有挂载 settings provider 时 installSection 退回 entry 值,浏览器端的 edit/write 卡片不受影响,只是 extraTools 不生效。 */ +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-settings' +import Schema from '@deepseek-ai/schemastery' + /** 插件名,即配置条目 id。 */ -const name = "dsh-edit-diff"; -/** 不使用任何主机端服务。 */ -const inject: string[] = []; -/** 本浏览器端插件无主机端行为。 */ -function apply() {} -export { apply, inject, name }; +export const name = 'dsh-edit-diff' + +/** settings 命名空间,与包名一致,浏览器半身按同名绑定。 */ +export const NS = 'dsh-edit-diff' + +/** 一个额外接管的工具,以及读取其参数用的参数名。 */ +export interface ExtraTool { + /** wire 工具名,即 tool.call.toolview 的 key。 */ + name: string + /** 读取文件路径的参数名,默认 file_path。 */ + pathKey?: string + /** 读取旧文本的参数名,默认 old_string。 */ + oldKey?: string + /** 读取新文本的参数名,默认 new_string。 */ + newKey?: string + /** 读取整文件内容的参数名,默认 content。 */ + contentKey?: string +} + +/** 插件配置。 */ +export interface Config { + /** 除 edit 与 write 外,额外接管 diff 卡片显示的工具。 */ + extraTools?: ExtraTool[] +} + +/** 配置 schema,默认值直接写在 schema 里。 */ +export const Config: Schema = Schema.object({ + extraTools: Schema.array(Schema.object({ + name: Schema.string().required(), + pathKey: Schema.string(), + oldKey: Schema.string(), + newKey: Schema.string(), + contentKey: Schema.string(), + })).default([]), +}) + +/** + * 插件 apply:把 entry 配置注册成 settings 命名空间。 + * @param ctx - cordis 上下文。 + * @param config - 插件配置。 + */ +export function apply(ctx: Context, config: Config = {}): void { + ctx.inject(['settings'], (settingsCtx) => { + settingsCtx.settings.installSection(ctx, NS, Config, config, { + setSource: () => {}, + onChange: () => {}, + }) + }) +} From 2068076ec7ecf55b1fab7aa74cd0d3649c8cf9d8 Mon Sep 17 00:00:00 2001 From: betterer <54624517+better-er@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:30:58 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20settings=20?= =?UTF-8?q?=E5=8F=98=E5=8C=96=E6=97=B6=E7=9A=84=E6=A7=BD=E4=BD=8D=E6=B3=A8?= =?UTF-8?q?=E9=94=80=E5=B9=B6=E8=A1=A5=E9=BD=90=E6=8E=A5=E5=8F=A3=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client.ts | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/client.ts b/src/client.ts index e3c382c..c06f533 100644 --- a/src/client.ts +++ b/src/client.ts @@ -88,8 +88,8 @@ interface DiffCardProps { /** 宿主 slots 服务的最小子集。 */ interface SlotsService { - inject(name: string, register: () => void): void - register(options: { name: string; key: string; priority: number }, component: (props: DiffCardProps) => React.ReactElement): void + inject(name: string, register: () => void | (() => void)): () => void + register(options: { name: string; key: string; priority: number }, component: (props: DiffCardProps) => React.ReactElement): () => void } /** 宿主 timer 服务的最小子集。 */ @@ -636,8 +636,12 @@ const plugin = { slots.inject('tool.call.toolview', () => { // priority 要低于默认 0,遮蔽 file-mutation-toolview 的 edit/write,最低者渲染。 // 若也传 0 会在同一 key 上 clash,而非替换。 - slots.register({ name: 'tool.call.toolview', key: 'edit', priority: -1 }, DiffCard) - slots.register({ name: 'tool.call.toolview', key: 'write', priority: -1 }, DiffCard) + const disposeEdit = slots.register({ name: 'tool.call.toolview', key: 'edit', priority: -1 }, DiffCard) + const disposeWrite = slots.register({ name: 'tool.call.toolview', key: 'write', priority: -1 }, DiffCard) + return () => { + disposeEdit() + disposeWrite() + } }) // extraTools 经 settings 命名空间到达。没有挂载 settings 时上面两张卡片照常工作。 @@ -653,23 +657,26 @@ const plugin = { const value = scope.getSnapshot().value const tools = Array.isArray(value?.extraTools) ? value.extraTools : [] extraSpecs.clear() - const names: string[] = [] + // slots.inject 返回注销函数,slots.register 也是;两者都要收起来,否则旧注册留在槽位里,下一次 sync 会对同 key 同 priority 再注册一次并抛错。 + const disposers: Array<() => void> = [] for (const tool of tools) { if (tool === null || typeof tool !== 'object') continue if (typeof tool.name !== 'string' || tool.name === '') continue - if (tool.name === 'edit' || tool.name === 'write') continue - extraSpecs.set(tool.name, toKeySpec(tool)) - names.push(tool.name) + const toolName = tool.name + if (toolName === 'edit' || toolName === 'write') continue + // 同名条目只认第一次。extraSpecs 刚清空,命中即已注册过。 + if (extraSpecs.has(toolName)) continue + extraSpecs.set(toolName, toKeySpec(tool)) + disposers.push(slots.inject('tool.call.toolview', () => + slots.register({ name: 'tool.call.toolview', key: toolName, priority: -1 }, DiffCard))) + } + disposeExtra = () => { + for (const dispose of disposers) dispose() } - disposeExtra = ctx.effect(() => { - for (const name of names) { - slots.inject('tool.call.toolview', () => { - slots.register({ name: 'tool.call.toolview', key: name, priority: -1 }, DiffCard) - }) - } - }) } - scope.subscribe(sync) + const unsubscribe = scope.subscribe(sync) + // settingsScope 卸载时撤掉订阅,注册本身由各自的 effect 随 fiber 回收。 + ctx.effect(() => () => unsubscribe()) sync() }) },