diff --git a/.agents/plugins/api_marketplace.json b/.agents/plugins/api_marketplace.json index df31151..a6c9289 100644 --- a/.agents/plugins/api_marketplace.json +++ b/.agents/plugins/api_marketplace.json @@ -5,16 +5,28 @@ }, "plugins": [ { - "name": "deta", + "name": "studio", "source": { "source": "local", - "path": "./plugins/deta" + "path": "./plugins/studio" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, "category": "Education & Research" + }, + { + "name": "writer", + "source": { + "source": "local", + "path": "./plugins/writer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" } ] } diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index df31151..a6c9289 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -5,16 +5,28 @@ }, "plugins": [ { - "name": "deta", + "name": "studio", "source": { "source": "local", - "path": "./plugins/deta" + "path": "./plugins/studio" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, "category": "Education & Research" + }, + { + "name": "writer", + "source": { + "source": "local", + "path": "./plugins/writer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" } ] } diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..165841f --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + contract: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: 仓库章程校验(工具清单 / 内部实现 / 开发检出依赖) + run: node scripts/check-plugin-contract.mjs diff --git a/README.md b/README.md index debfdf0..5368a25 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,70 @@ # DETA — Agent Plugins -DETA 领域能力在各类 Agent harness 上的分发仓:**一份产物,多个宿主壳**。 +DETA 领域能力在各类 Agent harness 上的分发仓:**品牌 = 市场,产品 = 插件,一个仓承载多产品**。 | 宿主 | 形态 | 安装 | |------|------|------| -| Codex | 插件(`.codex-plugin/plugin.json` + `skills/` + `.mcp.json`);本仓同时是 Codex 插件市场 | `codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git` → `codex plugin add deta@deta` | -| dsh (deepseek-harness) | 组合包 bundle(`dsh.bundle.patch` + `cordis.patch.yml`) | `dsh plugin --profile add ./plugins/deta` | +| Codex | 插件(`.codex-plugin/plugin.json` + `skills/` + `.mcp.json`);本仓同时是 Codex 插件市场 `deta` | `codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git` → `codex plugin add studio@deta` | +| dsh (deepseek-harness) | 组合包 bundle(`dsh.bundle.patch` + `cordis.patch.yml`) | `dsh plugin --profile add ./plugins/studio` | -能力本体(AML 建筑模型读写、编译校验、围护结构统计、空间邻接查询)以 MCP 服务提供, -同一份后端与技能被所有宿主复用;新增宿主只需再加一层声明式壳。 +发行标识(Codex 语法 = 插件@市场):**`studio@deta`**、**`writer@deta`**——两个插件互不冲突, +可同时安装(MCP 服务名 `studio_mcp` / `writer_mcp`,工作区各自独立)。新增产品按同一结构落位: +`plugins/<产品>/` 自带 `.codex-plugin/plugin.json`、`.mcp.json`、`package.json`、`cordis.patch.yml`、 +`bin/`、`skills/`、`README.md`,市场 `plugins` 数组加一条即可。 +能力本体(AML 建筑模型读写、编译校验、围护结构统计、空间邻接查询)以 **MCP 服务**提供, +运行时在**本机**解析(引擎包 / 源码构建),宿主侧只声明"用哪个启动器"; +同一份后端、同一个启动器、同一套技能被所有宿主复用,新增宿主只需再加一层声明式壳。 + +## 职责边界(仓库章程) + +本仓是**宿主适配与分发层,零逻辑、不承载契约**。三条铁律: + +| 问题 | 归属 | 内容 | +|------|------|------| +| 能力在哪 | **ant-copilot**(唯一来源) | 工具实现、MCP 协议层、启动形态(本机 stdio / 服务端 HTTP)、工具面裁剪、领域技能 | +| 怎么被发现和启动 | **本仓** | 宿主清单(`.codex-plugin/plugin.json`、`cordis.patch.yml`)、连接声明(`.mcp.json`)、启动器、宿主侧技能投影 | +| 怎么在云上跑 | **infra** | 部署 / 证书 / 路由 / 重启;云端只跑 ant-copilot 的构建产物 | + +因此本仓**不做**这些事(由 `scripts/check-plugin-contract.mjs` 在 CI 强制): + +- 不在文档或技能里列**工具清单 / 工具一览**——工具面按部署形态裁剪,清单必然过期; + 取清单的唯一途径是运行时调用 `list_capabilities`(技能必须给出这条指引)。 +- 不解释 ant-copilot 的内部常量与实现路径(`DETA_TOOL_SURFACE`、`dist/…` 等)。 +- 不把「开发检出路径」当作运行前提——启动器应随包解析运行时(当前仍保留开发检出回落, + 校验脚本以 warning 提示,本机引擎包落地后升级为 error)。 +- 市场条目与实际插件目录**必须一致**(条目 `name` = 插件清单 `name`、`path` 真实存在), + 由校验脚本的 E4 规则强制——改名或加产品时不会留下悬空条目。 + +**衡量标准**:新增一个工具需要改几处?目标是 **1 处**(ant-copilot 注册工具, +能力目录与文档自动派生),本仓零改动。 + +## 连接形态(唯一管道:本机 stdio) + +``` +宿主(Codex / dsh) + └─ 拉起 plugins/<产品>/bin/deta-mcp.sh --profile <产品> ← 各产品带自己的档位,脚本同一份 + ├─ 解析领域服务运行时:DETA_SERVER_ROOT → ~/.deta/runtime → 本机开发检出 + ├─ 解析工作目录:DETA_WORKSPACE → ANT_HEADLESS_WORKSPACE → ~/.deta/<产品>/workspace + └─ 执行领域服务运行时入口(打包产物;回落未打包产物时打印警告) + └─ MCP stdio:全量工具面;无 OAuth、无 401、无令牌续期 +``` + +- **本机形态不做 MCP 鉴权**:进程在本机由宿主或用户拉起,天然可信。 +- 云侧能力(项目/楼栋列表、同步)是**可选的输入**:注入 `DETA_TOKEN` 即有,缺省时云类工具 + 返回 `auth_required`,其余工具照常——同一份工具实现,不是两套分支。 +- Studio 执行器中继(模拟/画布)需 `ANT_WORKSPACE_BRIDGE_URL` + 项目 ID 三要素齐备。 +- **各产品独立命名空间**:MCP 服务名 `studio_mcp` / `writer_mcp`(工具前缀 + `mcp__<服务名>__<工具>`),两个插件可同时安装互不冲突;产品间不共享工作区。 +- 服务端仍保留 HTTP 形态的服务(数据面/其他客户端用),但它**不是本插件声明的连接形态**; + 插件的连接声明只有本机 stdio 一种。 + +## 内容 + +| 插件 | 服务名 | 技能 | 能力 | 依赖 | +|------|--------|------|------|------| +| `studio@deta` | `studio_mcp` | `studio:aml-design` | AML 建筑模型读写、编译与校验、围护结构统计、空间邻接查询 | node;领域服务运行时(本机引擎包或源码构建) | +| `writer@deta` | `writer_mcp` | `writer:report-authoring` | 报告/文档产出:读模型、取数据(统计/分析/模拟缓存/气象/规范)、成文 | 同上 | ## 安装(Codex) @@ -18,75 +73,30 @@ DETA 领域能力在各类 Agent harness 上的分发仓:**一份产物,多 codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git codex plugin list -codex plugin add deta@deta +codex plugin add studio@deta -# 登录(一次性):打开浏览器完成 deta.city 账号登录 -codex mcp login deta_mcp +# 准备领域服务运行时(二选一):本机引擎包(~/.deta/runtime)或源码构建 +# 源码构建:在领域服务仓库执行 npm run build:mcp +# 冒烟(应等待 stdin/stdout) +bash plugins/studio/bin/deta-mcp.sh ``` -`codex mcp login deta_mcp` 走 OAuth 2.1(授权码 + PKCE S256 + 动态客户端注册):终端给出 -`https://deta.city/oauth/authorize?...`,在浏览器里用 deta.city 账号登录并授权即可, -随后 Codex 自行保管令牌并按 refresh token 自动续期。退出登录用 `codex mcp logout deta_mcp`。 - -不支持 OAuth 的宿主(dsh / 脚本 / CI)改用登录助手换取 `DETA_TOKEN`: +装完即用,**没有 OAuth 登录步骤**。需要云侧能力时注入令牌(助手见 +`plugins/studio/README.md`「登录助手」): ```bash -node ~/Dev/ant/deta-agent-plugins/plugins/deta/bin/deta-login.mjs -source ~/.deta/env # 让 DETA_TOKEN 进入当前 shell 环境 +node plugins/studio/bin/deta-login.mjs +source ~/.deta/env # 让 DETA_TOKEN 进入宿主进程环境 ``` -未登录时 MCP 端点返回 **401**,并带 OAuth 发现提示头 -(`WWW-Authenticate: Bearer resource_metadata="https://deta.city/.well-known/oauth-protected-resource/mcp-api/mcp"`)—— -标准 MCP 客户端据此自动进入浏览器授权流程,含义是「当前环境没有可用的 deta.city 登录态」。 - -## 内容 - -| 插件 | 技能 | 能力 | 依赖 | -|------|------|------|------| -| `deta` | `deta:aml-design` | AML 建筑模型读写、编译与校验、围护结构统计、空间邻接查询 | node;领域服务已构建 | - -## 前置条件 - -| 项 | 值 | -|----|-----| -| MCP 端点 | `https://deta.city/mcp-api/mcp`(MCP Streamable HTTP) | -| 凭据来源 | **deta.city 账号**;`bin/deta-login.mjs` 登录取令牌,经环境变量 `DETA_TOKEN` 传给 harness | -| 凭据文件 | `~/.deta/credentials.json`(令牌 + 账号 + 端点)、`~/.deta/env`(`export DETA_TOKEN=...`),均 0600 | - -端点除探活接口(服务端 `GET /health`,经 Nginx 前缀对外为 `/mcp-api/health`)外全部需要鉴权: - -- 未带凭据 / 凭据失效 → `401`(提示重新登录,不回显凭据); -- 服务端认证服务不可用 → `500`,同样不放行; -- 各账号在服务端有独立工作区(`<工作区根>/<账号 ID>`),互不可见。 - -### 本地 stdio 形态(开发用,默认关闭) - -本机开发时也可以直连领域服务的无头 MCP 服务(`bin/deta-mcp.sh`,stdio), -它**不校验 deta.city 账号、不隔离工作区**,仅供本机迭代领域能力: - -```bash -cd ~/Dev/ant/ant-copilot && npm run build -node dist/src/headless/mcp-server.js # 冒烟:应等待 stdin/stdout -``` - -该形态的环境变量: - -| 变量 | 缺省 | 说明 | -|------|------|------| -| `DETA_SERVER_ROOT` | `~/Dev/ant/ant-copilot` | 领域服务仓库根目录(内部路径) | -| `ANT_HEADLESS_WORKSPACE` | `/workspace` | AML 工作目录(`aml.xml` / `building.json` 落盘处,名称由底层服务决定) | - -编译与校验类工具需要**编译服务**在线(服务端地址由底层环境变量 `DETA_GSA_URL` 给出,缺省 -`localhost:8000`);未在线时返回明确错误,读写类工具可离线工作。 - ## 本地迭代 插件内容改动后 Codex 不会自动感知,按官方流程打 cachebuster 版本号再重装: ```bash python3.11 ~/.codex/skills/.system/plugin-creator/scripts/update_plugin_cachebuster.py \ - ~/Dev/ant/deta-agent-plugins/plugins/deta -codex plugin add deta@deta + ~/Dev/ant/deta-agent-plugins/plugins/studio +codex plugin add studio@deta ``` (版本号形如 `0.1.0+codex.20260910020424`;重装后需开新会话才能加载新技能。) @@ -95,105 +105,91 @@ codex plugin add deta@deta ```bash python3.11 ~/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py \ - ~/Dev/ant/deta-agent-plugins/plugins/deta + ~/Dev/ant/deta-agent-plugins/plugins/studio +node scripts/check-plugin-contract.mjs # 本仓章程校验(CI 同款) codex plugin marketplace list -codex mcp list | grep deta +codex mcp list | grep -i studio # 应显示 stdio 形态(Command 列)与 studio_mcp ``` ## 双壳架构(Codex + dsh 同一产物) -`plugins/deta/` 这一个目录同时是 Codex 插件根与 dsh 组合包根,共享核心、各带一层薄壳: +`plugins/studio/` 这一个目录同时是 Codex 插件根与 dsh 组合包根,共享核心、各带一层薄壳: | 组成 | 文件 | Codex | dsh | |------|------|-------|-----| -| 插件清单 | `.codex-plugin/plugin.json` | ✅ 使用 | — | -| MCP 声明 | `.mcp.json` | ✅ 使用(可放 Codex 私有键) | — | +| 插件清单 | `.codex-plugin/plugin.json`(`name` = 产品名) | ✅ 使用 | — | +| MCP 声明 | `.mcp.json`(stdio → `bin/deta-mcp.sh --profile <产品>`,`serverName` = `<产品>_mcp`) | ✅ 使用(可放 Codex 私有键) | — | | 组合包声明 | `package.json`(`dsh.bundle.patch`) | 忽略 | ✅ 使用 | -| 配置层 | `cordis.patch.yml` | 忽略 | ✅ 使用 | -| 领域技能 | `skills/aml-design/SKILL.md` | ✅ 共享 | ✅ 共享 | -| 本地启动器 | `bin/deta-mcp.sh` | ✅ | 由配置层的 `args` 直接指向服务入口 | +| 配置层 | `cordis.patch.yml`(同一启动器,stdio,`serverName` = `<产品>_mcp`) | 忽略 | ✅ 使用 | +| 宿主侧技能 | `skills/aml-design/SKILL.md` | ✅ 共享 | ✅ 共享 | +| 启动器 | `bin/deta-mcp.sh` | ✅ 每产品一份(同一实现,产品差异走 `--profile`) | ✅ | 共享面(两端一致,零适配): - **技能文件格式**:`/SKILL.md` + frontmatter 必填 `name`/`description`,kebab-case。 Codex 与 dsh(`@deepseek-ai/dsh-skill-filesystem`)要求相同。 - **共享技能根**:两端都扫描 `~/.agents/skills/` 与 `<项目>/.agents/skills/` (dsh 侧为 rank 500 / 200 的发现根,且显式支持 `/skills`)。 - 把技能放这里即可双端生效,不需要安装插件。 -- **MCP 工具命名**:两端同为 `mcp____`,我们统一 `serverName = deta`, - 因此技能里写裸工具名(`read_source`、`apply_design`)在两端都能解析。 +- **MCP 工具命名**:两端同为 `mcp____`,我们统一 `serverName = deta`。 各自不同的部分(不共享): -- MCP 的声明位置不同(Codex 读 `.mcp.json`,dsh 读 `cordis.patch.yml` 的 mcp-client 行)。 -- Codex 私有键(`default_tools_approval_mode`、`supports_parallel_tool_calls`)只写在 - `.mcp.json`,不影响 dsh;dsh 无审批模式概念。 -- 分发渠道不同:Codex 走 git 市场,dsh 走 npm / git / tarball,且 dsh 没有官方市场。 +- MCP 的声明位置不同(Codex 读 `.mcp.json`,dsh 读 `cordis.patch.yml` 的 mcp-client 行), + 但**都指向同一个启动器**。 +- Codex 私有键(`default_tools_approval_mode`、`omit_tools_from`)只写在 `.mcp.json`。 +- 分发渠道不同:Codex 走 git 市场,dsh 走 npm / git / tarball。 ### dsh 侧安装与验证 ```bash -# 安装进某个 profile(pnpm 转发) -dsh plugin --profile add ~/Dev/ant/deta-agent-plugins/plugins/deta - -# 公开发布形态(二选一) -npm publish # 用户:dsh plugin --profile add deta-plugin -dsh plugin --profile add github:<你>/<仓库> # 需作者提供 prepare + 用户 allowBuilds +export DETA_PLUGIN_ROOT=~/Dev/ant/deta-agent-plugins/plugins/studio +dsh plugin --profile add "$DETA_PLUGIN_ROOT" # 只验证配置层是否合并正确(不启动、不需凭据) cd ~/Dev/ant/deepseek-harness node apps/cli/lib/bin.js --profile headless --dump-config \ - --patch ~/Dev/ant/deta-agent-plugins/plugins/deta/cordis.patch.yml -# 输出中应出现 `# == .../plugins/deta/cordis.patch.yml` 与 deta-mcp 行 + --patch "$DETA_PLUGIN_ROOT/cordis.patch.yml" +# 输出中应出现 `# == .../plugins/studio/cordis.patch.yml` 与 deta-mcp(transport: stdio) ``` -`cordis.patch.yml` 当前默认启用**远程端点**(`streamable-http` + -`Authorization: Bearer ${DETA_TOKEN}`,指向 `https://deta.city/mcp-api/mcp`); -本地 stdio 段保留为注释、标注「本机开发用,默认关闭」,需要时手工切换。 +## 已知缺陷(实测) -## 已知缺陷(实测,2026-09-10) - -**上游:编码代理 × 模型服务 命名空间重复** - -会话累积较多本插件工具调用后,代理会在请求里重复声明 MCP 命名空间,模型服务的 -Responses 兼容层拒绝请求: - -``` -Duplicate namespace name 'deta'; Namespace names must be unique. -``` - -复现记录(同一插件、不同会话长度): - -| 代理版本 | 会话规模 | 触发位置 | -|---------|---------|---------| -| 0.144.4 | 一步并发 2 次调用 | input[10]、input[18] | -| 0.144.4 | 串行 7–8 次调用 | input[37]、input[47] | -| 0.154.0 | 短会话 5 工具 | 未触发 | -| 0.154.0 | 长会话(≈15+ 工具) | input[260] | - -已排除/无效的手段:`wire_api = "chat"`(该版本已移除,仅支持 responses)、 -`features.non_prefixed_mcp_tool_names`(仅去掉命名空间前缀,重复依旧)、 -`supports_parallel_tool_calls = false`(削弱并发触发面,长会话仍复现)。 - -**应对**:完成一个任务后另起会话;撞到该错误不要重试,重开会话即可(工作区文件保留)。 +**上游:编码代理 × 模型服务 命名空间重复** —— 会话累积较多本插件工具调用后,代理会在请求里 +重复声明 MCP 命名空间,模型服务的 Responses 兼容层拒绝并报 +`Duplicate namespace name 'deta'; Namespace names must be unique.`。 +应对:完成一个任务后另起会话;撞到该错误不要重试(工作区文件保留)。 +复现记录与已排除手段见 `plugins/studio/README.md`。 ## 已修缺陷(实测) | 现象 | 根因 | 修复 | |------|------|------| -| 写类工具返回 `user cancelled MCP tool call` | MCP 工具默认需审批,非交互模式下无法弹窗 | `.mcp.json` 加 `default_tools_approval_mode: "approve"`(实测 `auto`/`writes` 都会被取消,只有 `approve` 放行) | -| 并发调用触发命名空间重复 | 代理并发序列化 | `.mcp.json` 加 `supports_parallel_tool_calls: false` | +| 写类工具返回 `user cancelled MCP tool call` | MCP 工具默认需审批,非交互模式无法弹窗 | `.mcp.json` 声明 `default_tools_approval_mode: "approve"` | +| 本机 stdio 形态起不来:`ERR_UNSUPPORTED_DIR_IMPORT` | 启动器指向未打包的产物(依赖包目录 import 在 Node ESM 下非法) | 领域服务提供打包入口;启动器优先取打包产物,回落未打包产物时打印警告 | +| 一个插件两种形态并存(远程 URL + 本机 stdio) | 历史遗留,语义冲突 | 统一为本机 stdio 单一管道 | +| 启动器把开发检出当唯一运行前提 | 默认值写死开发检出路径 | 运行时按 `DETA_SERVER_ROOT → ~/.deta/runtime → 开发检出` 解析,回落时打印提示 | ## 目录结构 ``` .agents/plugins/marketplace.json # Codex 默认市场(ChatGPT 登录用户) .agents/plugins/api_marketplace.json # Codex API key 登录用户市场 -plugins/deta/ # 同一目录 = Codex 插件根 = dsh 组合包根 -├── .codex-plugin/plugin.json # Codex 插件清单 -├── .mcp.json # Codex MCP 声明(远程 streamable-http) +.gitea/workflows/ci.yml # CI:跑 scripts/check-plugin-contract.mjs +scripts/check-plugin-contract.mjs # 仓库章程校验(工具清单 / 内部实现 / 开发检出依赖) +plugins/studio/ # 产品插件:同一目录 = Codex 插件根 = dsh 组合包根 +├── .codex-plugin/plugin.json # Codex 插件清单(name: studio) +├── .mcp.json # Codex MCP 声明(studio_mcp → ./bin/deta-mcp.sh --profile studio) ├── package.json # dsh 组合包声明(dsh.bundle.patch) -├── cordis.patch.yml # dsh 配置层(挂载 MCP 服务) +├── cordis.patch.yml # dsh 配置层(同一启动器,serverName: studio_mcp) ├── bin/deta-login.mjs # 登录助手(~/.deta/credentials.json + ~/.deta/env) -├── bin/deta-mcp.sh # 本地 stdio 启动器(开发用,默认关闭) -├── README.md # 插件说明(安装/凭据/401 语义) -└── skills/aml-design/SKILL.md # 领域技能(两宿主共享) +├── bin/deta-mcp.sh # 本机 stdio 启动器(多产品共用;--profile 区分档位) +├── README.md # 插件说明(连接形态 / 凭据 / 排障) +└── skills/aml-design/SKILL.md # 宿主侧技能(不含工具清单) +plugins/writer/ # 产品插件:报告/文档产出 +├── .codex-plugin/plugin.json # Codex 插件清单(name: writer) +├── .mcp.json # Codex MCP 声明(writer_mcp → ./bin/deta-mcp.sh --profile writer) +├── package.json # dsh 组合包声明(dsh.bundle.patch) +├── cordis.patch.yml # dsh 配置层(同一启动器,serverName: writer_mcp) +├── bin/deta-login.mjs # 登录助手(云侧能力用) +├── bin/deta-mcp.sh # 本机 stdio 启动器(同一实现;缺省档位 writer) +├── README.md # 插件说明(连接形态 / 能力边界 / 排障) +└── skills/report-authoring/SKILL.md # 宿主侧技能(报告取数与纪律) ``` diff --git a/plugins/deta/.mcp.json b/plugins/deta/.mcp.json deleted file mode 100644 index b368fa1..0000000 --- a/plugins/deta/.mcp.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "mcpServers": { - "deta_mcp": { - "type": "streamable-http", - "url": "https://deta.city/mcp-api/mcp", - "omit_tools_from": ["deferred"] - } - } - } diff --git a/plugins/deta/README.md b/plugins/deta/README.md deleted file mode 100644 index d219682..0000000 --- a/plugins/deta/README.md +++ /dev/null @@ -1,174 +0,0 @@ -# deta 插件(Codex 插件 / dsh 组合包) - -DETA 领域能力(AML 建筑模型读写、编译校验、围护结构统计、空间邻接查询) -以**远程 MCP 服务**形态挂进 Agent harness:同一个目录既是 Codex 插件根, -也是 dsh 组合包根,两端共享 `skills/` 与同一份 MCP 后端。 - -## 端点与凭据 - -| 项 | 值 | -|----|-----| -| 端点 | `https://deta.city/mcp-api/mcp`(MCP Streamable HTTP) | -| 凭据来源 | **deta.city 账号**;两种取得方式:OAuth 浏览器登录(推荐)或换令牌后手工传 | -| Codex 侧 | OAuth 2.1(授权码 + PKCE S256,动态客户端注册),由 `codex mcp login deta_mcp` 发起;令牌由 Codex 保管并自动续期,无需手工配置 | -| dsh / 脚本 | 环境变量 `DETA_TOKEN`,作为 `Authorization: Bearer` 或 `X-API-Key` 发送 | - -端点除探活接口(服务端 `GET /health`,经 Nginx 前缀对外为 `/mcp-api/health`)外**全部需要鉴权**: - -- 无凭据 / 凭据失效 → **401**,且响应带 OAuth 发现提示: - `WWW-Authenticate: Bearer resource_metadata="https://deta.city/.well-known/oauth-protected-resource/mcp-api/mcp", scope="mcp"` - 标准 MCP 客户端据此**自动进入浏览器授权流程**(Codex 走的就是这条),无需手工配令牌。 - 401 的含义是「当前环境没有可用的 deta.city 登录态」,不是工具或模型出错。 -- 服务端认证服务不可用(`ANT_ADMIN_SERVER` 未配置或不可达)→ 500,同样不放行。 - -账号的工作区在服务端按账号隔离(`<工作区根>/<账号 ID>`), -不同账号的设计源码互不可见;同一账号的新会话继续使用自己的工作区。 - -## 工具面:编辑归宿主,MCP 只做引擎与同步 - -AML 是纯文本 DSL,读写、搜索、重构一律由**宿主原生的文件工具**完成 -(Codex 的 read / patch / grep、shell;dsh 的同名能力),MCP 不再是编辑入口: - -| 分工 | 谁做 | -|------|------| -| 读取 / 修改 / 搜索 `aml.xml`、`bss.md`、`rules.yml`、`behaviors.yml`、`design-brief.md` | 宿主原生工具(本地项目目录里的文件) | -| 编译、规范校验、规则检查、围护结构统计、空间邻接、领域脚本 | MCP 引擎工具(`apply_design` / `validate_model`(几何+issue+规则三视图统一入口)/ `envelope_stats` / `query_spatial_graph` / `run_script` …) | -| 本地改动送上云端、取回云端工作区 | MCP `push_project` / `pull_project` | - -远程 HTTP 形态的工具面按部署形态裁剪:编辑类工具 -(`read_source` / `edit_source` / `list_workspace` / `search_workspace` / `workspace_diff` -等依赖宿主 AML、工作区文件清单、画布或计算回调的工具)**不出现在 `tools/list`**; -引擎类工具与 `push_project` / `pull_project` 保留。本机 stdio 开发形态仍是全量面 -(`DETA_TOOL_SURFACE=full`,缺省即 full;远程形态缺省 engine,可用同名环境变量覆盖)。 - -## 本地项目目录约定 - -一个 DETA 项目就是一个普通目录,引擎工具读的是**服务端工作区**,因此改动要先 push: - -``` -my-project/ -├── deta.json # 可选:项目绑定({ "projectId": "...", "name": "..." }) -├── aml.xml # 必需:建筑模型唯一事实源 -├── bss.md # 可选:样式/构造附属文档 -├── rules.yml # 可选:校验规则 -├── behaviors.yml # 可选:声明式行为规则 -└── design-brief.md # 可选:设计任务书 -``` - -- `deta.json` 里的 `projectId` 是 deta.city 云项目 ID;带上它的 `push_project` - 会把同一个 ID 绑定到服务端工作区(写 `deta.json`),之后 `generate_aml` / - `pull_project` 会带上同一个 Studio 深链 `https://deta.city/design/`。 -- 文件名沿用服务端约定(`aml` ↔ `aml.xml`、`brief` ↔ `design-brief.md` 等), - 本地目录与服务端工作区结构一致,`pull_project` 的结果可直接落盘回本地目录。 - -## 端到端工作流:本地编辑 → push → 校验 → Studio 查看 - -```text -1. 本地编辑 read/patch/grep 直接改 aml.xml(MCP 不参与编辑) -2. 推送 push_project({ files: { "aml.xml": "<全文>", "rules.yml": "…" }, - projectId: "<云项目 ID>" }) - → { ok: true, files: ["aml.xml", "rules.yml"], - projectId: "<云项目 ID>", studioUrl: "https://deta.city/design/" } - (带 projectId 时同时在服务端工作区写入 deta.json 绑定) -3. 引擎校验 apply_design() → 编译 AML 为建筑模型(服务端工作区内的最新版本) - validate_model() → 语法/几何问题,逐条修复后重新 push + 编译 - envelope_stats() / query_spatial_graph() → 围护结构与空间邻接 -4. Studio 查看 在右侧内置浏览器面板打开返回的 studioUrl - (https://deta.city/design/,例:云项目 11 → https://deta.city/design/11); - 已有相同 URL 的标签优先复用,不要为同一项目重复开新标签,也不要 goto 重载已有标签 -5. 跑模拟 在面板里的 Studio 界面运行(分析/模拟面板、GH 模板)——模拟由 Studio 执行, - 本插件 MCP 不执行模拟;面板首次使用需先登录一次 deta.city -6. 反向核对 pull_project({ projectId }) → { files, projectId, studioUrl } - (工作区为空时 files 为 {}) -``` - -路径安全:`push_project` 只接受工作区相对路径,绝对路径与 `..` 穿越会被拒绝, -单次载荷上限 1 MiB(与请求体上限一致)。 - -## 安装与登录(Codex) - -```bash -# 1. 添加市场并安装插件 -codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git -codex plugin add deta@deta - -# 2. 登录(一次性):浏览器里完成 deta.city 账号登录 -codex mcp login deta_mcp -``` - -`codex mcp login deta_mcp` 会打印一条形如 -`https://deta.city/oauth/authorize?response_type=code&client_id=…&code_challenge=…` 的地址并打开浏览器; -页面上用 deta.city 账号登录(支持「账号密码」与「邮箱验证码」两个入口),点「登录并授权」后 -浏览器提示 `Authentication complete`,终端输出 `Successfully logged in to MCP server 'deta'`。 - -登录态由 Codex 保存,access token 有效期 1 小时并用 refresh token 自动续期; -换机器或换 `CODEX_HOME` 需重新 `codex mcp login deta_mcp`。退出登录: - -```bash -codex mcp logout deta_mcp # 撤销并删除本地凭据 -``` - -插件默认走 OAuth。若要把令牌固定为环境变量(自动化、容器、CI),装好后改为: - -```bash -codex mcp remove deta_mcp -codex mcp add deta_mcp --url https://deta.city/mcp-api/mcp --bearer-token-env-var DETA_TOKEN -export DETA_TOKEN=<管理端签发的 API Key 或门户登录令牌> -``` - -### 登录助手(dsh / 脚本 / 预发环境) - -OAuth 之外仍保留令牌方式,供不支持 OAuth 的宿主与脚本使用: - -```bash -node bin/deta-login.mjs # 交互式:读标识与密码,写 ~/.deta/* -node bin/deta-login.mjs -u <用户名|已验证邮箱|已验证手机号> # 免交互输入标识 -node bin/deta-login.mjs --email-code <邮箱> # 邮箱验证码登录(未设密码的账号用) -node bin/deta-login.mjs --base-url https:// # 自建/预发环境 -source ~/.deta/env # 导出 DETA_TOKEN -``` - -**标识(identifier)填门户登录页上用的那一个**:用户名、已验证邮箱、已验证手机号,服务端都会解析到同一账号。 -助手先匿名调用 `/login/check` 预检标识是否已注册,再走 `/login` 做密码校验(不走门户的「登录即注册」端点, -不会误建账号)。失败时按服务端提示给出下一步: - -| 服务端提示 | 含义与处理 | -|------------|------------| -| `该标识在 未注册` | 标识不是门户上用的那一个;邮箱验证码注册的账号标识填邮箱 | -| `用户不存在/密码错误` | 服务端不区分「账号不存在」与「密码不匹配」;已确认标识存在,即密码不符,或改用 `--email-code` | -| `该邮箱/手机号尚未验证…` | 先用验证码方式登录一次门户完成验证 | -| `该登录方式未开放` | 用 `GET /prod-api/login/methods` 查当前启用项 | - -## 安装(dsh) - -```bash -node ~/Dev/ant/deta-agent-plugins/plugins/deta/bin/deta-login.mjs -source ~/.deta/env -dsh plugin --profile add ~/Dev/ant/deta-agent-plugins/plugins/deta -``` - -`cordis.patch.yml` 默认启用远程 `streamable-http` 段(Bearer 取自 `DETA_TOKEN`)。 - -## 本地 stdio 形态(开发用) - -`bin/deta-mcp.sh` 走本机 stdio(`node dist/src/headless/mcp-server.js`), -**仅本机开发用,默认关闭**:不校验 deta.city 账号、不隔离工作区。 -需要在本机迭代领域能力时,打开 `cordis.patch.yml` 里注释掉的 stdio 段 -(并注释掉远程段),设置 `DETA_SERVER_ROOT` 指向已构建的领域服务仓库: - -```bash -cd ~/Dev/ant/ant-copilot && npm run build -node dist/src/headless/mcp-server.js # 冒烟:应等待 stdin/stdout -``` - -## 文件 - -``` -.codex-plugin/plugin.json Codex 插件清单 -.mcp.json Codex MCP 声明(远程 streamable-http) -package.json dsh 组合包声明(dsh.bundle.patch) -cordis.patch.yml dsh 配置层(挂载 MCP 服务) -bin/deta-login.mjs 登录助手(写 ~/.deta/credentials.json + ~/.deta/env) -bin/deta-mcp.sh 本地 stdio 启动器(开发用) -skills/aml-design/SKILL.md 领域技能 -``` diff --git a/plugins/deta/bin/deta-mcp.sh b/plugins/deta/bin/deta-mcp.sh deleted file mode 100755 index 43ca0d0..0000000 --- a/plugins/deta/bin/deta-mcp.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# DETA 领域服务 MCP stdio 启动器 -# -# 依赖:node + 已构建的领域服务(默认 ~/Dev/ant/ant-copilot 的 dist/src/headless/mcp-server.js) -# 可用环境变量: -# DETA_SERVER_ROOT 领域服务仓库根目录(缺省 ~/Dev/ant/ant-copilot) -# ANT_HEADLESS_WORKSPACE AML 工作目录(缺省 /workspace,供底层服务读取) -set -euo pipefail - -ROOT="${DETA_SERVER_ROOT:-$HOME/Dev/ant/ant-copilot}" -ENTRY="$ROOT/dist/src/headless/mcp-server.js" - -if ! command -v node >/dev/null 2>&1; then - echo "[deta] 未找到 node,请先安装 Node.js" >&2 - exit 1 -fi - -if [ ! -f "$ENTRY" ]; then - echo "[deta] 未找到 $ENTRY" >&2 - echo "[deta] 请先构建领域服务(npm run build)或设置 DETA_SERVER_ROOT" >&2 - exit 1 -fi - -exec node "$ENTRY" "$@" diff --git a/plugins/deta/cordis.patch.yml b/plugins/deta/cordis.patch.yml deleted file mode 100644 index 5bc5cb7..0000000 --- a/plugins/deta/cordis.patch.yml +++ /dev/null @@ -1,40 +0,0 @@ -# DETA 组合包(dsh bundle)配置层 -# -# 挂载方式:dsh plugin --profile add <本包> -# 本层只做一件事:把 DETA 领域能力作为 MCP 服务挂进 harness。 -# 工具以 mcp__deta__ 暴露(与 Codex 侧同名)。 -# -# 凭据:deta.city 账号。先 `node bin/deta-login.mjs` 登录(写入 ~/.deta/credentials.json -# 与 ~/.deta/env),再 `source ~/.deta/env` 让 DETA_TOKEN 进入环境。 -# 端点 https://deta.city/mcp-api/mcp 除 /health 外都要鉴权,无凭据一律 401。 -# -# ── 远程端点形态(Streamable HTTP,默认启用)─────────────────── -# 服务端:deta.city 上的无头 MCP 服务(ant-copilot src/headless/mcp-http-server.ts), -# 经 Nginx 反代暴露;各账号的工作区在服务端按账号隔离。 -# -- insert: - - id: deta-mcp - name: '@deepseek-ai/dsh-mcp-client' - config: - serverName: deta - transport: streamable-http - url: https://deta.city/mcp-api/mcp - headers: - Authorization: !!js '`Bearer ${process.env.DETA_TOKEN ?? ''}`' -# -# ── 本地形态(本机开发用,默认关闭)─────────────────────────── -# 走 stdio 直连本机已构建的领域服务,不校验 deta.city 账号、不隔离工作区, -# 只在迭代领域能力时临时启用:把下面整节取消注释,同时注释掉上面的远程节。 -# 需要 DETA_SERVER_ROOT 指向已构建的领域服务仓库(缺省见启动器脚本)。 -# -# - insert: -# - id: deta-mcp -# name: '@deepseek-ai/dsh-mcp-client' -# config: -# serverName: deta -# transport: stdio -# command: node -# args: -# - !!js '`${process.env.DETA_SERVER_ROOT ?? process.env.HOME + "/Dev/ant/ant-copilot"}/dist/src/headless/mcp-server.js`' -# env: -# ANT_HEADLESS_WORKSPACE: !!js process.env.ANT_HEADLESS_WORKSPACE ?? '' diff --git a/plugins/deta/package-lock.json b/plugins/deta/package-lock.json deleted file mode 100644 index 60c1ec5..0000000 --- a/plugins/deta/package-lock.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "deta-plugin", - "version": "0.2.8", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "deta-plugin", - "version": "0.2.8", - "license": "Proprietary" - } - } -} diff --git a/plugins/deta/.codex-plugin/plugin.json b/plugins/studio/.codex-plugin/plugin.json similarity index 82% rename from plugins/deta/.codex-plugin/plugin.json rename to plugins/studio/.codex-plugin/plugin.json index ac3fa68..e72b47b 100644 --- a/plugins/deta/.codex-plugin/plugin.json +++ b/plugins/studio/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { - "name": "deta", - "version": "0.2.8", - "description": "DETA 建筑性能设计插件:以 AML 为唯一事实源编辑建筑模型,编译为建筑模型并执行规则校验。", + "name": "studio", + "version": "0.3.1", + "description": "DETA Studio 插件:以 AML 为唯一事实源编辑建筑模型,编译为建筑模型并执行规则校验。", "author": { "name": "DETA", "url": "https://deta.city" @@ -18,7 +18,7 @@ ], "skills": "./skills/", "interface": { - "displayName": "DETA 建筑性能设计", + "displayName": "DETA Studio", "shortDescription": "AML 建筑模型读写、编译与校验", "longDescription": "在编码代理中直接操作建筑模型:读取与编辑 AML 设计源码,经编译器编译为建筑模型,执行语法与规则校验、围护结构统计与空间邻接查询。AML 是建筑语义的唯一事实源,所有几何编辑均落到 AML 文本。", "developerName": "DETA", diff --git a/plugins/studio/.mcp.json b/plugins/studio/.mcp.json new file mode 100644 index 0000000..db10b4c --- /dev/null +++ b/plugins/studio/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "studio_mcp": { + "type": "stdio", + "command": "./bin/deta-mcp.sh", + "cwd": ".", + "args": ["--profile", "studio"], + "default_tools_approval_mode": "approve", + "omit_tools_from": ["deferred"] + } + } + } diff --git a/plugins/studio/README.md b/plugins/studio/README.md new file mode 100644 index 0000000..3eff104 --- /dev/null +++ b/plugins/studio/README.md @@ -0,0 +1,175 @@ +# DETA Studio 插件(Codex 插件 / dsh 组合包) + +DETA 领域能力(AML 建筑模型读写、编译校验、围护结构统计、空间邻接查询) +以**本机 MCP 服务**形态挂进 Agent harness:同一个目录既是 Codex 插件根, +也是 dsh 组合包根,两端共享 `skills/` 与**同一个启动器**。 + +- 发行标识:**`studio@deta`**(插件 `studio` + 市场 `deta`) +- MCP 服务名:**`studio_mcp`**(工具前缀 `mcp__studio_mcp__…`) +- 产品档位:启动器 `--profile studio`(工作区缺省 `~/.deta/studio/workspace`) + +## 连接形态与凭据(唯一管道:本机 stdio) + +| 项 | 值 | +|----|-----| +| 形态 | **本机 stdio**:宿主拉起 `bin/deta-mcp.sh --profile studio`,由它解析领域服务运行时与工作目录 | +| MCP 服务名 | `studio_mcp`(工具前缀 `mcp__studio_mcp__…`);其他产品用各自服务名,可同时安装 | +| 宿主声明 | Codex 读 `.mcp.json`;dsh 读 `cordis.patch.yml`——两边都只声明"调用同一个脚本、带哪个档位" | +| 工作目录 | `DETA_WORKSPACE` → `ANT_HEADLESS_WORKSPACE` → 缺省 `~/.deta/studio/workspace`(启动器创建并在 stderr 回显) | +| 运行时 | `DETA_SERVER_ROOT` → `~/.deta/runtime` → 本机开发检出(回落到开发检出时启动器打印提示) | +| 云侧能力(可选) | `DETA_TOKEN`(`node bin/deta-login.mjs` 获取):项目/楼栋列表、`push_project` / `pull_project` | +| Studio 执行器中继(可选) | `ANT_WORKSPACE_BRIDGE_URL` + 项目 ID(三要素齐备才启用;供模拟/画布类工具) | + +**本机形态不做 MCP 鉴权**(进程由宿主或用户在本机拉起,没有 OAuth、没有 401、没有令牌续期)。 +未注入 `DETA_TOKEN` 时,云类工具返回 `auth_required`,其余工具(编辑、编译、校验、统计、 +脚本、模拟)照常工作——这是与远程形态**同一份工具实现**的不同输入,不是两套代码。 + +## 工具面:编辑归宿主,MCP 只做引擎与同步 + +AML 是纯文本 DSL,读写、搜索、重构一律由**宿主原生的文件工具**完成 +(Codex 的 read / patch / grep / shell;dsh 的同名能力),MCP 不是编辑入口: + +| 分工 | 谁做 | +|------|------| +| 读取 / 修改 / 搜索 `aml.xml`、`bss.md`、`rules.yml`、`behaviors.yml`、`design-brief.md` | 宿主原生工具(本地项目目录里的文件) | +| 编译、规范校验、规则检查、围护结构统计、空间邻接、领域脚本 | MCP 引擎工具(编译 / 校验 / 统计 / 邻接 / 脚本等) | +| 改动送进引擎工作区、取回工作区 | MCP 同步工具 | + +本机形态为**全量工具面**(含工作区文件类工具)。**当前可用的完整工具名以运行时 +`list_capabilities` 为准**,本仓文档不列举工具清单。 + +## 本地项目目录约定 + +一个 DETA 项目就是一个普通目录;引擎工具读的是**引擎工作区**,因此改动要先同步: + +``` +my-project/ +├── deta.json # 可选:项目绑定({ "projectId": "...", "name": "..." }) +├── aml.xml # 必需:建筑模型唯一事实源 +├── bss.md # 可选:样式/构造附属文档 +├── rules.yml # 可选:校验规则 +├── behaviors.yml # 可选:声明式行为规则 +└── design-brief.md # 可选:设计任务书 +``` + +- 想让引擎直接以项目目录为工作区:启动宿主前设 `DETA_WORKSPACE=<项目目录>`。 +- `deta.json` 里的 `projectId` 是 deta.city 云项目 ID;带上它的同步调用会把同一个 ID + 绑定到引擎工作区,之后生成/拉取会带同一个 Studio 深链 `https://deta.city/design/`。 +- 文件名沿用引擎侧约定(`aml` ↔ `aml.xml`、`brief` ↔ `design-brief.md` 等), + 本地目录与引擎工作区结构一致,拉取结果可直接落盘回本地目录。 + +## 端到端工作流:本地编辑 → 同步 → 校验 → Studio 查看 + +```text +1. 本地编辑 read/patch/grep 直接改 aml.xml(MCP 不参与编辑) +2. 同步 把改动送进引擎工作区(带 projectId 时同时绑定云项目) + → { ok, files, projectId, studioUrl: "https://deta.city/design/" } +3. 引擎校验 编译 AML 为建筑模型(工作区最新版本)→ 语法/几何/规则校验逐条修复 + 围护结构统计与空间邻接查询按需调用 +4. Studio 查看 在宿主内置浏览器面板打开 studioUrl;已有相同 URL 的标签优先复用, + 不为同一项目重复开新标签,也不要 goto 重载已有标签 +5. 跑模拟 在面板里的 Studio 界面运行——模拟由 Studio 执行,本插件 MCP 不替代它 +6. 反向核对 从引擎工作区拉回文件(工作区为空时返回空集) +``` + +路径安全:同步只接受工作区相对路径,绝对路径与 `..` 穿越会被拒绝,单次载荷上限 1 MiB。 + +## 安装(Codex) + +```bash +# 1. 添加市场并安装插件 +codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git +codex plugin add studio@deta + +# 2. 准备领域服务运行时(二选一) +# a) 本机引擎包已安装 → 启动器自动找到(~/.deta/runtime) +# b) 从源码构建:在领域服务仓库执行 npm run build:mcp,并让 DETA_SERVER_ROOT 指向它 +# +# 冒烟(应等待 stdin/stdout): +bash <插件目录>/bin/deta-mcp.sh --profile studio +``` + +装完即用,**没有 OAuth 登录步骤**。需要云侧能力(项目列表 / 同步)时再注入令牌: + +```bash +node bin/deta-login.mjs # 登录 deta.city,写 ~/.deta/credentials.json 与 ~/.deta/env +source ~/.deta/env # 让 DETA_TOKEN 进入宿主进程环境 +``` + +### 登录助手(云侧能力用) + +```bash +node bin/deta-login.mjs # 交互式:读标识与密码,写 ~/.deta/* +node bin/deta-login.mjs -u <用户名|已验证邮箱|已验证手机号> # 免交互输入标识 +node bin/deta-login.mjs --email-code <邮箱> # 邮箱验证码登录(未设密码的账号用) +node bin/deta-login.mjs --base-url https:// # 自建/预发环境 +``` + +**标识(identifier)填门户登录页上用的那一个**:用户名、已验证邮箱、已验证手机号,服务端都会解析到同一账号。 +助手先匿名调用 `/login/check` 预检标识是否已注册,再走 `/login` 做密码校验(不走门户的「登录即注册」端点, +不会误建账号)。失败时按服务端提示给出下一步: + +| 服务端提示 | 含义与处理 | +|------------|------------| +| `该标识在 未注册` | 标识不是门户上用的那一个;邮箱验证码注册的账号标识填邮箱 | +| `用户不存在/密码错误` | 服务端不区分「账号不存在」与「密码不匹配」;已确认标识存在,即密码不符,或改用 `--email-code` | +| `该邮箱/手机号尚未验证…` | 先用验证码方式登录一次门户完成验证 | +| `该登录方式未开放` | 用 `GET /prod-api/login/methods` 查当前启用项 | + +## 安装(dsh) + +```bash +export DETA_PLUGIN_ROOT=<本包所在目录> # 配置层按它定位启动器 +export DETA_WORKSPACE=<你的项目目录> # 可选;缺省 ~/.deta/workspace +dsh plugin --profile add <本包> +``` + +`cordis.patch.yml` 声明本机 stdio,经 `bash -lc` 调用同一个 `bin/deta-mcp.sh`; +未设置 `DETA_PLUGIN_ROOT` 时启动器明确报错退出,不会静默失败。 + +## 文件 + +``` +.codex-plugin/plugin.json Codex 插件清单 +.mcp.json Codex MCP 声明(stdio → ./bin/deta-mcp.sh) +package.json dsh 组合包声明(dsh.bundle.patch) +cordis.patch.yml dsh 配置层(同一启动器,stdio) +bin/deta-login.mjs 登录助手(写 ~/.deta/credentials.json + ~/.deta/env,供云侧能力用) +bin/deta-mcp.sh 本机 stdio 启动器(两宿主共用;解析运行时与工作目录) +README.md 本文件(安装 / 连接 / 排障) +skills/aml-design/SKILL.md 宿主侧技能(工具使用规则;不含工具清单) +``` + +## 已知缺陷(实测,2026-09-10) + +**上游:编码代理 × 模型服务 命名空间重复** + +会话累积较多本插件工具调用后,代理会在请求里重复声明 MCP 命名空间,模型服务的 +Responses 兼容层拒绝请求: + +``` +Duplicate namespace name 'deta'; Namespace names must be unique. +``` + +复现记录(同一插件、不同会话长度): + +| 代理版本 | 会话规模 | 触发位置 | +|---------|---------|---------| +| 0.144.4 | 一步并发 2 次调用 | input[10]、input[18] | +| 0.144.4 | 串行 7–8 次调用 | input[37]、input[47] | +| 0.154.0 | 短会话 5 工具 | 未触发 | +| 0.154.0 | 长会话(≈15+ 工具) | input[260] | + +已排除/无效的手段:`wire_api = "chat"`(该版本已移除,仅支持 responses)、 +`features.non_prefixed_mcp_tool_names`(仅去掉命名空间前缀,重复依旧)、 +`supports_parallel_tool_calls = false`(削弱并发触发面,长会话仍复现)。 + +**应对**:完成一个任务后另起会话;撞到该错误不要重试,重开会话即可(工作区文件保留)。 + +## 已修缺陷(实测) + +| 现象 | 根因 | 修复 | +|------|------|------| +| 写类工具返回 `user cancelled MCP tool call` | MCP 工具默认需审批,非交互模式下无法弹窗 | `.mcp.json` 声明 `default_tools_approval_mode: "approve"`(实测 `auto`/`writes` 都会被取消,只有 `approve` 放行) | +| 本机 stdio 形态起不来:`ERR_UNSUPPORTED_DIR_IMPORT` | 启动器指向未打包的产物,依赖包的目录 import 在 Node ESM 下非法 | 领域服务提供打包入口;启动器优先取打包产物,回落未打包产物时打印警告 | +| 一个插件两种形态并存(远程 URL + 本机 stdio) | 历史遗留:远程端点声明与本地启动器同时存在,语义冲突 | 统一为**本机 stdio 单一管道**,远程端点声明从插件移除 | diff --git a/plugins/deta/bin/deta-login.mjs b/plugins/studio/bin/deta-login.mjs similarity index 100% rename from plugins/deta/bin/deta-login.mjs rename to plugins/studio/bin/deta-login.mjs diff --git a/plugins/studio/bin/deta-mcp.sh b/plugins/studio/bin/deta-mcp.sh new file mode 100755 index 0000000..d46514c --- /dev/null +++ b/plugins/studio/bin/deta-mcp.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# DETA 产品插件的本机 MCP stdio 启动器(唯一入口,多产品共用) +# +# 两个宿主(Codex 插件 / dsh 组合包)都通过本脚本拉起同一个领域服务; +# 产品差异只由 --profile 决定(工作区默认位置、工具面),宿主侧只声明"调哪个脚本、带哪个档位"。 +# +# 用法:deta-mcp.sh [--profile ] +# +# 依赖:node + 已构建的领域服务运行时 +# +# 环境变量: +# DETA_SERVER_ROOT 领域服务运行时根目录(未设时自动查找,见下) +# DETA_WORKSPACE AML 工作目录(引擎侧工作区真值;缺省 ~/.deta//workspace) +# ANT_HEADLESS_WORKSPACE 同上(兼容旧名,DETA_WORKSPACE 优先) +# DETA_TOKEN deta.city 门户令牌(可选;不注入则云类工具返回 auth_required) +# ANT_WORKSPACE_BRIDGE_URL 网关基址(可选;与项目 ID 一起启用 Studio 执行器中继) +# DETA_SURFACE 工具面覆盖(缺省 full) +set -euo pipefail + +# ── 产品档位:宿主在声明里传 --profile;缺省 studio ── +PROFILE="${DETA_PROFILE:-studio}" +while [ $# -gt 0 ]; do + case "$1" in + --profile) PROFILE="${2:?--profile 需要一个值}"; shift 2 ;; + *) break ;; + esac +done + +# ── 运行时解析:显式 DETA_SERVER_ROOT → 本机引擎包安装位置 → 本机开发检出(打印提示)── +ROOT="${DETA_SERVER_ROOT:-}" +if [ -z "$ROOT" ]; then + for cand in "$HOME/.deta/runtime" "$HOME/Dev/ant/ant-copilot"; do + if [ -f "$cand/dist/deta-mcp.mjs" ] || [ -f "$cand/dist/src/headless/mcp-server.js" ]; then ROOT="$cand"; break; fi + done +fi + +if ! command -v node >/dev/null 2>&1; then + echo "[deta] 未找到 node,请先安装 Node.js" >&2 + exit 1 +fi + +# 入口优先取 esbuild 打包形态:tsc 产物在 Node ESM 下会因依赖包的目录 import 直接失败 +ENTRY="" +if [ -n "$ROOT" ]; then + if [ -f "$ROOT/dist/deta-mcp.mjs" ]; then + ENTRY="$ROOT/dist/deta-mcp.mjs" + elif [ -f "$ROOT/dist/src/headless/mcp-server.js" ]; then + ENTRY="$ROOT/dist/src/headless/mcp-server.js" + echo "[deta] 警告:回落到未打包产物,请先在该运行时目录执行 npm run build:mcp" >&2 + fi +fi + +if [ -z "$ENTRY" ]; then + echo "[deta] 未找到领域服务运行时(已尝试:DETA_SERVER_ROOT、\$HOME/.deta/runtime、\$HOME/Dev/ant/ant-copilot)" >&2 + echo "[deta] 请安装本机引擎包,或设置 DETA_SERVER_ROOT 指向已构建的领域服务仓库" >&2 + exit 1 +fi + +if [ "$ROOT" = "$HOME/Dev/ant/ant-copilot" ]; then + echo "[deta] 提示:使用本机开发检出 ${ROOT}(非打包运行时)" >&2 +fi + +# ── 工作目录:宿主不会传项目目录(cwd 通常是插件根),按产品档位定稳定位置 ── +WS="${DETA_WORKSPACE:-${ANT_HEADLESS_WORKSPACE:-$HOME/.deta/$PROFILE/workspace}}" +mkdir -p "$WS" +export ANT_HEADLESS_WORKSPACE="$WS" + +# ── 工具面:产品档位缺省全量面;DETA_SURFACE 可覆盖(如 engine)── +export DETA_TOOL_SURFACE="${DETA_SURFACE:-full}" + +echo "[deta] 档位=${PROFILE} 运行时=${ROOT} 工作区=${WS} 工具面=${DETA_TOOL_SURFACE} 云令牌=$([ -n "${DETA_TOKEN:-}" ] && echo 已注入 || echo 未注入)" >&2 + +exec node "$ENTRY" "$@" diff --git a/plugins/studio/cordis.patch.yml b/plugins/studio/cordis.patch.yml new file mode 100644 index 0000000..f95b78b --- /dev/null +++ b/plugins/studio/cordis.patch.yml @@ -0,0 +1,33 @@ +# DETA 组合包(dsh bundle)配置层 +# +# 挂载方式:dsh plugin --profile add <本包> +# 本层只做一件事:把 DETA 领域能力作为 MCP 服务挂进 harness。 +# 工具以 mcp__deta__ 暴露(与 Codex 侧同名)。 +# +# 形态:本机 stdio(唯一管道)——与 Codex 插件共用同一个启动器 bin/deta-mcp.sh, +# 由它解析领域服务运行时(DETA_SERVER_ROOT → ~/.deta/runtime → 本机开发检出)。 +# 不校验 deta.city 账号、不隔离工作区;工作目录由 DETA_WORKSPACE 指定 +# (缺省 ~/.deta/studio/workspace,实际值见启动器的 stderr 输出)。 +# +# 需要云侧能力(项目/楼栋列表、push/pull 同步、Studio 执行器中继)时注入: +# DETA_TOKEN deta.city 门户令牌(node bin/deta-login.mjs 获取) +# ANT_WORKSPACE_BRIDGE_URL 网关基址(与项目 ID 一起构成 host-call 中继三要素) +# +# 前置:设置 DETA_PLUGIN_ROOT 指向本包所在目录(未设置时启动器明确报错退出)。 +# +- insert: + - id: deta-mcp + name: '@deepseek-ai/dsh-mcp-client' + config: + serverName: studio_mcp + transport: stdio + command: bash + args: + - -lc + - 'exec "${DETA_PLUGIN_ROOT:?请设置 DETA_PLUGIN_ROOT 指向 DETA 插件目录}/bin/deta-mcp.sh" --profile studio' + env: + DETA_PLUGIN_ROOT: !!js process.env.DETA_PLUGIN_ROOT ?? '' + DETA_TOKEN: !!js process.env.DETA_TOKEN ?? '' + DETA_WORKSPACE: !!js process.env.DETA_WORKSPACE ?? '' + toolCallTimeoutMs: 600000 + failOnStartupError: true diff --git a/plugins/studio/package-lock.json b/plugins/studio/package-lock.json new file mode 100644 index 0000000..039eab5 --- /dev/null +++ b/plugins/studio/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "deta-studio-plugin", + "version": "0.3.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deta-studio-plugin", + "version": "0.3.1", + "license": "Proprietary" + } + } +} diff --git a/plugins/deta/package.json b/plugins/studio/package.json similarity index 58% rename from plugins/deta/package.json rename to plugins/studio/package.json index cb0ada3..43767c5 100644 --- a/plugins/deta/package.json +++ b/plugins/studio/package.json @@ -1,7 +1,7 @@ { - "name": "deta-plugin", - "version": "0.2.8", - "description": "DETA 建筑性能设计:AML 建筑模型读写与编译校验(Codex 插件 / dsh 组合包 双壳)", + "name": "deta-studio-plugin", + "version": "0.3.1", + "description": "DETA Studio:AML 建筑模型读写与编译校验(Codex 插件 / dsh 组合包 双壳)", "type": "module", "license": "Proprietary", "files": [ diff --git a/plugins/deta/skills/aml-design/SKILL.md b/plugins/studio/skills/aml-design/SKILL.md similarity index 84% rename from plugins/deta/skills/aml-design/SKILL.md rename to plugins/studio/skills/aml-design/SKILL.md index 227aad8..2f4518d 100644 --- a/plugins/deta/skills/aml-design/SKILL.md +++ b/plugins/studio/skills/aml-design/SKILL.md @@ -11,7 +11,7 @@ description: 以 AML 为唯一事实源读写建筑模型并编译校验。当 (实测 `Only 10 shown of 23`)。**搜不到不等于不存在**——不确定有哪些工具时,先调 **`list_capabilities`**(能力目录)拿到当前形态的**完整工具名清单**,再按名字**直接调用**。 -**改建筑模型只有一个标准动作**(远程形态): +**改建筑模型只有一个标准动作**(能力目录里有 `build_model` 时): `build_model({ aml, projectId })` —— 写服务端工作区 → 编译成建筑模型 → 规范校验 → 写进云项目, 一次调用完成;返回 `gate`(校验是否通过)、编译摘要、校验明细、云工作区 `headSha`。 @@ -34,12 +34,20 @@ description: 以 AML 为唯一事实源读写建筑模型并编译校验。当 用户提到建筑方案、房间/空间布局、层高、墙体开洞(门窗)、围护结构、体形系数、 建筑模型校验,或需要为后续能耗/日照/CFD 模拟准备模型时使用本技能。 -## 编辑与校验分工(MCP 是引擎,不是编辑器) +## 编辑与校验分工(先搞清"哪份文件是真值") -远程 MCP 服务只暴露**引擎**能力:编译、规范校验、规则检查、围护结构统计、 -空间邻接,以及 push/pull 同步。编辑类工具(`read_source` / `edit_source` / -`list_workspace` / `search_workspace` / `workspace_diff` 等)不在远程工具面里—— -看不到就不要去找,更不要用它们替代宿主的文件工具。 +本插件挂的是**本机 MCP**:引擎工作区才是编译/校验/模拟读的真值(启动器 stderr 会回显路径)。 +两种用法: + +- **工作区 = 项目目录**(启动宿主前设 `DETA_WORKSPACE=<项目目录>`):宿主原生文件工具与 MCP + 工作区工具看的是同一份文件,直接改完就能编译,不需要同步步骤。 +- **工作区 ≠ 项目目录**(缺省 `~/.deta/workspace`):本地改动要先同步进引擎工作区, + 引擎工具读到的才是最新版本。 + +编辑手段按场景选:纯文本改动优先用宿主原生文件工具(read / patch / grep);需要按元素定向 +读写 AML(按空间 ID 改属性、只取某实体块原文)时用 MCP 的工作区工具。编译、规范校验、 +规则检查、围护结构统计、空间邻接一律走引擎工具。 +当前形态到底有哪些工具、叫什么名字,以运行时调 `list_capabilities` 的结果为准。 | 要做的事 | 用什么 | |----------|--------| @@ -55,11 +63,11 @@ description: 以 AML 为唯一事实源读写建筑模型并编译校验。当 固定顺序(工具面里有 `build_model` 时):**本地写 AML → `build_model` → 看 gate →(Studio 里看效果)**。 `build_model` 一次完成「写服务端工作区 → 编译 → 校验 → 写进云项目工作区」,Studio 里立即可见。 -工具面里没有 `build_model`(本机全量形态)时才退化为:本地编辑 → `push_project` → `apply_design` → +工具面里没有 `build_model`(部分部署形态会裁掉它)时才退化为:本地编辑 → `push_project` → `apply_design` → `validate_model` → Studio 查看。引擎工具读的是**服务端工作区**的文件,没推送过就读不到你的最新改动。 -**工具检索结果条数有限**:一次搜索没看到某个工具,不代表它不存在。闭环工具名固定是 -`build_model` / `push_project` / `apply_design` / `validate_model`,直接按名字调用。 +**工具检索结果条数有限**:一次搜索没看到某个工具,不代表它不存在——先调 +`list_capabilities` 拿当前形态的完整工具名,再按名字直接调用。 ### 闭环单入口 build_model @@ -67,15 +75,15 @@ description: 以 AML 为唯一事实源读写建筑模型并编译校验。当 build_model({ aml?: "", files?: { "aml.xml": "…" }, projectId?: "24", force?: false }) ``` -- `aml` 给出则先写入服务端工作区 `aml.xml`;`files` 可一并写其它工作区文件(路径规则同 `push_project`)。 +- `aml` 给出则先写入引擎工作区 `aml.xml`;`files` 可一并写其它工作区文件(路径规则同 `push_project`)。 - `projectId` 缺省取工作区 `deta.json` 绑定的云项目;没有绑定就先 `push_project({ files, projectId })` 绑一次。 - 返回:`gate`(`passed` / `failed`)、编译摘要、校验明细、`cloud.headSha`、`studioUrl`。 - `gate: failed` → 按 warning/error 改 AML,再调一次 `build_model`,直到 `passed`。 - `status: conflict`(`version_conflict`)→ 云项目工作区被其它会话改过(例如 Studio 面板里编辑过), 确认要覆盖再传 `force: true` 重跑一次。 -- `status: unavailable` → 该形态没有云同步通道(例如本机全量形态),改用 - `apply_design` + `validate_model`。 +- `status: unavailable` → 当前没有云同步通道(未注入云令牌,或该部署形态未接云),改用 + `apply_design` + `validate_model`(编译校验不依赖云)。 - **禁止**把 AML 拷进 Studio 的代码编辑器手工点编译:那是没有闭环工具时的兜底,不是正常路径。 ### push 与 Studio 深链 @@ -143,7 +151,7 @@ Studio 就是deta.city 上的建筑性能应用。在 Codex 右侧内置浏览 1. **AML 是建筑语义的唯一事实源**。任何几何或属性变更都必须落到 AML 文本, 不得用中间 JSON 表达修改意图。 2. **禁止凭记忆编写 AML 结构**。动手前必须先取真实语法: - - 已有模型:用宿主原生 read 工具读项目里的 `aml.xml`(远程形态没有 `read_source`) + - 已有模型:读工作区里的 `aml.xml`(宿主原生 read 工具,或 MCP 的 `read_source`) - 语法规则:`get_aml_schema`(从编译服务实时获取,唯一权威) 3. **属性知识不硬编码**:房间用途、构造材料、洞口参数等可选值从工具返回中获取, 不要凭经验列举。 @@ -183,17 +191,9 @@ Studio 就是deta.city 上的建筑性能应用。在 Codex 右侧内置浏览 直接报告「编译服务未接入」,改用宿主原生文件工具(read / patch)核对与编辑本地 AML。 - 编译报错必须逐条修复后重新 `apply_design`,禁止在报错状态下声称完成。 -## 工具一览 +## 工具名从哪来 -设计源码读写/搜索:**宿主原生工具**(read / patch / grep / shell),不经 MCP -生成与语法:`generate_aml`、`get_aml_schema` -能力目录:`list_capabilities`(不确定有哪些工具时先调它拿完整名字表——检索一次只回少量结果,搜不到不等于不存在) -闭环单入口:`build_model`(写工作区 → 编译 → 校验 → 云同步;见「闭环单入口 build_model」一节) -编译与校验:`apply_design`、`validate_model`(几何 + 编译 issue + 规则合规,`checks` 一次要齐)、 -`envelope_stats`、`query_spatial_graph` -生成候选:`generate_candidates` -同步:`push_project`、`pull_project` -脚本与参考:`run_script`、`run_scripts`、`read_reference` -项目与查询:`list_projects`、`list_buildings`、`parameter_sweep` -(`query_knowledge` 已在远程形态裁出——它需要宿主注入知识库回调,无头/远程形态不接入,调用只会返回 unavailable) -面板与 Studio:`open_studio`(打开 Studio 执行器:免密链接 + 轮询等上线,见「在右侧面板打开 Studio」一节) +本技能**不列工具清单**:工具面随部署形态(引擎形态 / 全量形态)裁剪,任何手写清单都会过期。 + +不确定有哪些工具时,调 `list_capabilities` 拿当前形态的**完整工具名**,再按名字直接调用; +每个工具能做什么、参数怎么给,以它自身的 description 与返回为准。 diff --git a/plugins/writer/.codex-plugin/plugin.json b/plugins/writer/.codex-plugin/plugin.json new file mode 100644 index 0000000..fabafb7 --- /dev/null +++ b/plugins/writer/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "writer", + "version": "0.1.0", + "description": "DETA Writer 插件:读取建筑模型与模拟结果,产出报告 / 文档内容。", + "author": { + "name": "DETA", + "url": "https://deta.city" + }, + "homepage": "https://deta.city", + "license": "Proprietary", + "keywords": [ + "architecture", + "report", + "document", + "building-performance", + "energy" + ], + "skills": "./skills/", + "interface": { + "displayName": "DETA Writer", + "shortDescription": "建筑性能报告与文档产出", + "longDescription": "在编码代理中为建筑项目产出报告内容:读取 AML 设计源码与编译产物,取围护结构统计、能耗与舒适度分析、模拟结果缓存与规范指标,整理成报告初稿并落盘。报告模板插槽与 docx 导出由门户 Writer 负责,本插件只提供数据与文本。", + "developerName": "DETA", + "category": "Productivity", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "brandColor": "#2F6F4E", + "websiteURL": "https://deta.city", + "defaultPrompt": [ + "读取当前项目的建筑模型,列出可用于报告的关键指标。", + "把本项目的围护结构与能耗结果整理成报告初稿,缺失数据如实标注。" + ] + } +} diff --git a/plugins/writer/.mcp.json b/plugins/writer/.mcp.json new file mode 100644 index 0000000..769ffeb --- /dev/null +++ b/plugins/writer/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "writer_mcp": { + "type": "stdio", + "command": "./bin/deta-mcp.sh", + "cwd": ".", + "args": ["--profile", "writer"], + "default_tools_approval_mode": "approve", + "omit_tools_from": ["deferred"] + } + } + } diff --git a/plugins/writer/README.md b/plugins/writer/README.md new file mode 100644 index 0000000..febbfe4 --- /dev/null +++ b/plugins/writer/README.md @@ -0,0 +1,88 @@ +# DETA Writer 插件(Codex 插件 / dsh 组合包) + +DETA 领域能力在**报告产出**档位上的分发壳:同一个目录既是 Codex 插件根, +也是 dsh 组合包根,两端共享 `skills/` 与**同一个启动器**(与 Studio 插件共用实现)。 + +- 发行标识:**`writer@deta`**(插件 `writer` + 市场 `deta`) +- MCP 服务名:**`writer_mcp`**(工具前缀 `mcp__writer_mcp__…`;与 `studio_mcp` 互不冲突,可同时安装) +- 产品档位:启动器 `--profile writer`(工作区缺省 `~/.deta/writer/workspace`,与 Studio 隔离) + +## 连接形态与凭据(唯一管道:本机 stdio) + +| 项 | 值 | +|----|-----| +| 形态 | **本机 stdio**:宿主拉起 `bin/deta-mcp.sh --profile writer`,由它解析领域服务运行时与工作目录 | +| 宿主声明 | Codex 读 `.mcp.json`;dsh 读 `cordis.patch.yml`——两边都只声明"调用同一个脚本、带哪个档位" | +| 工作目录 | `DETA_WORKSPACE` → `ANT_HEADLESS_WORKSPACE` → 缺省 `~/.deta/writer/workspace`(启动器创建并在 stderr 回显) | +| 运行时 | `DETA_SERVER_ROOT` → `~/.deta/runtime` → 本机开发检出(回落到开发检出时启动器打印提示) | +| 云侧能力(可选) | `DETA_TOKEN`(`node bin/deta-login.mjs` 获取):项目/楼栋列表、`push_project` / `pull_project` | +| Studio 执行器中继(可选) | `ANT_WORKSPACE_BRIDGE_URL` + 项目 ID(三要素齐备才启用;供模拟/画布类工具) | + +**本机形态不做 MCP 鉴权**(进程由宿主或用户在本机拉起,没有 OAuth、没有 401、没有令牌续期)。 +未注入 `DETA_TOKEN` 时,云类工具返回 `auth_required`,其余工具照常。 + +## 能力现状与边界(重要) + +| 能力 | 现在由谁承担 | +|------|--------------| +| 读模型与工作区(`aml.xml` / `building.json`) | 宿主原生文件工具 + MCP 工作区工具 | +| 取数:围护结构统计、性能分析、模拟缓存、气象、规范指标 | MCP 引擎工具(与 `studio_mcp` 同一实现) | +| 报告成文(章节文本、结论、缺项标注) | 本项目技能 `report-authoring` 指导下的写作过程,产物落工作区 | +| **报告模板、插槽填充、docx 导出** | **门户 Writer 页面**(`https://deta.city/writer?doc=`)——**不是 MCP 工具** | + +即:本插件当前复用与 Studio 相同的引擎工具面(报告类工具尚无 MCP 出口), +差异体现在**产品档位(独立工作区、独立命名空间)与写作技能**上。专用报告工具面 +(插槽词表 / 报告文档读写)属引擎侧待补项,落地后本插件只需把档位的工具面切过去。 + +## 工具面约定 + +- 本机形态为全量工具面。**当前可用的完整工具名以运行时 `list_capabilities` 为准**, + 本仓文档不列举工具清单。 +- 写类工具在 Codex 侧需要审批:`.mcp.json` 已声明 `default_tools_approval_mode: "approve"`。 + +## 安装(Codex) + +```bash +codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git +codex plugin add writer@deta + +# 领域服务运行时:本机引擎包(~/.deta/runtime)或源码构建;冒烟应等待 stdin/stdout +bash <插件目录>/bin/deta-mcp.sh --profile writer +``` + +需要云侧能力(项目列表 / 同步)时注入令牌: + +```bash +node bin/deta-login.mjs # 登录 deta.city,写 ~/.deta/credentials.json 与 ~/.deta/env +source ~/.deta/env # 让 DETA_TOKEN 进入宿主进程环境 +``` + +## 安装(dsh) + +```bash +export DETA_PLUGIN_ROOT=<本包所在目录> +dsh plugin --profile add "$DETA_PLUGIN_ROOT" +# 验证配置层(不需凭据):--dump-config 输出应含 serverName: writer_mcp / transport: stdio +``` + +## 文件 + +``` +.codex-plugin/plugin.json Codex 插件清单(name: writer) +.mcp.json Codex MCP 声明(writer_mcp → ./bin/deta-mcp.sh --profile writer) +package.json dsh 组合包声明(dsh.bundle.patch) +cordis.patch.yml dsh 配置层(同一启动器,serverName: writer_mcp) +bin/deta-login.mjs 登录助手(~/.deta/credentials.json + ~/.deta/env,供云侧能力用) +bin/deta-mcp.sh 本机 stdio 启动器(多产品共用;缺省 `--profile writer`) +README.md 本文件(连接形态 / 能力边界 / 排障) +skills/report-authoring/SKILL.md 宿主侧技能(报告写作取数与纪律;不含工具清单) +``` + +## 排障 + +- **工具面为空 / 宿主说没有工具**:先看宿主日志(Codex 桌面应用日志的 + `mcp_server_startup_status_updated` 与 `logs_2.sqlite` 的 `omitting MCP server`), + 再跑一次 `bash bin/deta-mcp.sh --profile writer` 看启动器 stderr 回显的运行时与工作区。 +- **看到的是 Studio 的数据**:检查工作区是否被 `DETA_WORKSPACE` 覆盖成了 Studio 目录; + 缺省档位下两者是隔离的(`~/.deta/writer/workspace` vs `~/.deta/studio/workspace`)。 +- **命名空间重复**(`Duplicate namespace name …`):上游代理问题,另起会话,不要重试。 diff --git a/plugins/writer/bin/deta-login.mjs b/plugins/writer/bin/deta-login.mjs new file mode 100755 index 0000000..60560ee --- /dev/null +++ b/plugins/writer/bin/deta-login.mjs @@ -0,0 +1,368 @@ +#!/usr/bin/env node +/** + * DETA 登录助手(Node ESM,零第三方依赖) + * + * 用 deta.city 账号换取 API 令牌,供 MCP 远程端点鉴权使用: + * + * node bin/deta-login.mjs [--identifier <用户名|邮箱|手机号>] [--base-url https://deta.city] + * node bin/deta-login.mjs --email-code <邮箱> # 邮箱验证码登录(无密码账号用) + * + * 标识(identifier)就是你在门户登录页填的那一个: + * 用户名 / 已验证邮箱 / 已验证手机号,服务端都会解析到同一账号。 + * 注意 deta.city 门户的密码登录端点与 RuoYi 内置的 /login 不是同一个, + * 本助手统一走 /login(已存在账号的密码校验),不会触发「登录即注册」。 + * + * 流程: + * 1. 读入标识;先匿名调用 /login/check 预检该标识是否已注册; + * 2. 读入密码(不回显);POST /prod-api/login { username: 标识, password }; + * 3. 成功 → 写 ~/.deta/credentials.json 与 ~/.deta/env(均 0600); + * 4. 失败 → 打印服务端 msg 与针对性提示,退出码非 0。 + * + * 凭据纪律:token 只写在本次 stdout 与上述两个 0600 文件里, + * 不写日志、不写 stderr、不写其它位置。 + */ + +import { mkdir, writeFile, chmod } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +const DEFAULT_BASE_URL = 'https://deta.city'; +const REQUEST_TIMEOUT_MS = 20_000; + +function fail(message) { + console.error(`[deta-login] ${message}`); + process.exit(1); +} + +const USAGE = [ + '用法:node bin/deta-login.mjs [选项]', + ' -u, --identifier <值> 登录标识(用户名/已验证邮箱/已验证手机号);省略则交互式输入', + ' --email-code <邮箱> 用邮箱验证码登录(门户只用验证码登录、未设密码时用)', + ' --base-url 服务地址,缺省 https://deta.city', + ' -h, --help 显示本帮助', +].join('\n'); + +/** 解析命令行;缺省 baseUrl 取 DETA_BASE_URL 或 https://deta.city,末尾斜杠去掉 */ +function parseArgs(argv) { + const options = { + baseUrl: process.env.DETA_BASE_URL || DEFAULT_BASE_URL, + identifier: '', + emailCode: '', + }; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + const takeValue = (name) => { + const next = argv[i + 1]; + if (!next) fail(`${name} 缺少取值`); + i++; + return next; + }; + if (arg === '-u' || arg === '--identifier') { + options.identifier = takeValue(arg); + } else if (arg.startsWith('--identifier=')) { + options.identifier = arg.slice('--identifier='.length); + } else if (arg === '--email-code') { + options.emailCode = takeValue(arg); + } else if (arg.startsWith('--email-code=')) { + options.emailCode = arg.slice('--email-code='.length); + } else if (arg === '--base-url') { + options.baseUrl = takeValue(arg); + } else if (arg.startsWith('--base-url=')) { + options.baseUrl = arg.slice('--base-url='.length); + } else if (arg === '-h' || arg === '--help') { + console.log(USAGE); + process.exit(0); + } else { + fail(`未知参数:${arg}\n${USAGE}`); + } + } + options.baseUrl = options.baseUrl.trim().replace(/\/+$/, ''); + options.identifier = options.identifier.trim(); + options.emailCode = options.emailCode.trim(); + return options; +} + +/** + * 读一行输入。hidden=true 时不回显(密码); + * 非 TTY(管道/CI)时同样按行读取,便于自动化。 + */ +let pendingInput = ''; + +/** + * 读一行输入。hidden=true 时不回显(密码);非 TTY(管道/CI)时同样按行读取。 + * + * 跨读取缓冲:`pendingInput` 保存上一次读取切剩的数据。TTY 下在 raw mode 逐字符处理, + * 遇到换行即切行并把该 chunk 换行之后的字符存回 `pendingInput`——因此 + * 一次粘贴「标识 + 密码」两行不会丢第二行。 + */ +function readLine({ hidden }) { + return new Promise((resolve, reject) => { + const stdin = process.stdin; + const isTTY = Boolean(stdin.isTTY); + let buffer = pendingInput; + pendingInput = ''; + let settled = false; + + /** 从 buffer 切出一整行(含行尾判定),并把剩余数据留到 pendingInput;无完整行返回 null */ + const splitLine = () => { + const index = buffer.search(/[\r\n]/); + if (index < 0) return null; + const line = buffer.slice(0, index); + const skip = buffer[index] === '\r' && buffer[index + 1] === '\n' ? 2 : 1; + pendingInput = buffer.slice(index + skip); + buffer = ''; + return line; + }; + + const cleanup = () => { + stdin.removeListener('data', onData); + stdin.removeListener('end', onEnd); + stdin.removeListener('error', onError); + if (isTTY && typeof stdin.setRawMode === 'function') stdin.setRawMode(false); + stdin.pause(); + }; + const finish = (value) => { + if (settled) return; + settled = true; + cleanup(); + // TTY 下 raw mode 不回显回车,这里补一个换行;非 TTY(管道)不额外输出 + if (isTTY) process.stdout.write('\n'); + resolve(value); + }; + const onData = (chunk) => { + const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8'); + if (!isTTY) { + // 非交互:不回显,按行切分,剩余数据留给下一次读取 + buffer += text; + const line = splitLine(); + if (line !== null) finish(line); + return; + } + const chars = [...text]; + for (let i = 0; i < chars.length; i++) { + const ch = chars[i]; + if (ch === '\n' || ch === '\r') { + // 本 chunk 换行符之后的字符属于后续读取(一次粘贴多行):存入 pendingInput 后立即返回 + let next = i + 1; + if (ch === '\r' && chars[next] === '\n') next++; // CRLF 视作一个行尾 + const rest = chars.slice(next).join(''); + if (rest) pendingInput = rest + pendingInput; + return finish(buffer); + } + if (ch === '\u0003') { + // Ctrl-C + process.stdout.write('\n'); + process.exit(130); + } + if (ch === '\u0004') return finish(buffer); // Ctrl-D + if (ch === '\u007f' || ch === '\b') { + if (buffer && !hidden) process.stdout.write('\b \b'); + buffer = buffer.slice(0, -1); + continue; + } + buffer += ch; + if (!hidden) process.stdout.write(ch); + } + }; + const onEnd = () => finish(buffer); + const onError = (err) => { + if (settled) return; + settled = true; + cleanup(); + reject(err); + }; + + // 缓冲里已有完整行(上一次读取的遗留)→ 立即返回,不等待输入 + const buffered = splitLine(); + if (buffered !== null) { + if (isTTY) process.stdout.write('\n'); + resolve(buffered); + return; + } + + if (isTTY && typeof stdin.setRawMode === 'function') stdin.setRawMode(true); + stdin.resume(); + stdin.on('data', onData); + stdin.on('end', onEnd); + stdin.on('error', onError); + }); +} + +/** 单引号安全转义,供 `export DETA_TOKEN='...'` 使用 */ +function shellSingleQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + +async function writePrivateFile(filePath, content) { + await writeFile(filePath, content, { mode: 0o600 }); + // 已存在的文件不被 mode 改写,显式收紧 + await chmod(filePath, 0o600).catch(() => undefined); +} + +/** 登录标识类型判定(与服务端 LoginIdentifierResolver 的规则一致) */ +const PHONE_REGEX = /^1[3-9]\d{9}$/; +const EMAIL_REGEX = /^[\w.+-]+@[\w-]+(\.[\w-]+)+$/; + +function identifierField(identifier) { + if (EMAIL_REGEX.test(identifier)) return 'email'; + if (PHONE_REGEX.test(identifier)) return 'phone'; + return 'username'; +} + +/** POST JSON,返回 { response, payload }(响应体非 JSON 时 payload 为 null) */ +async function postJson(baseUrl, path, body) { + let response; + try { + response = await fetch(`${baseUrl}${path}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + } catch (err) { + fail(`无法连接 ${baseUrl}${path}(${err instanceof Error ? err.message : String(err)})`); + } + let payload = null; + try { + payload = await response.json(); + } catch { + payload = null; + } + return { response, payload }; +} + +function serverMessage(payload, response) { + return (payload && typeof payload.msg === 'string' && payload.msg) || `请求失败(HTTP ${response.status})`; +} + +/** + * 匿名查询标识是否已注册;接口异常返回 null(不据此阻断登录)。 + * 只填标识所属类型的那一个字段:服务端按 phone → email → username 取第一个非空字段。 + */ +async function accountRegistered(baseUrl, identifier) { + const { response, payload } = await postJson(baseUrl, '/prod-api/login/check', { + [identifierField(identifier)]: identifier, + }); + if (!response.ok || !payload || payload.code !== 200) return null; + return payload.registered === true; +} + +/** 服务端提示 → 可执行的下一步 */ +function loginHint(message, identifier) { + if (message.includes('用户不存在/密码错误')) { + return [ + `该提示不区分「账号不存在」与「密码不匹配」(已确认 ${identifier} 是已注册标识,故为密码不符或标识与账号不对应)。`, + '请确认:', + ' 1) 标识与门户登录页填写的一致(用户名 / 已验证邮箱 / 已验证手机号);', + ' 2) 密码与门户一致(区分大小写,注意首尾空格);', + ' 3) 若门户只用邮箱验证码登录、从未设过密码,改用 --email-code <邮箱>。', + ].join('\n '); + } + if (message.includes('尚未验证')) { + return '该邮箱/手机号在账号里尚未标记为已验证:请先用验证码方式登录一次门户完成验证,再重试。'; + } + if (message.includes('停用')) return '账号已被停用,请联系管理员。'; + if (message.includes('未开放')) { + return '该登录方式未开放:改用已启用的方式(GET /prod-api/login/methods 可查当前启用项)。'; + } + return ''; +} + +/** 邮箱验证码登录:发码 → 读入验证码 → 校验 */ +async function loginByEmailCode(baseUrl, email) { + const send = await postJson(baseUrl, '/prod-api/login/email/send-code', { email }); + if (!send.response.ok || send.payload?.code !== 200) { + fail(`发送验证码失败:${serverMessage(send.payload, send.response)}`); + } + process.stdout.write('邮箱验证码:'); + const code = (await readLine({ hidden: false })).trim(); + if (!code) fail('验证码不能为空'); + const { response, payload } = await postJson(baseUrl, '/prod-api/login/email', { email, code }); + const token = typeof payload?.token === 'string' ? payload.token : ''; + if (!response.ok || !token || (typeof payload?.code === 'number' && payload.code !== 200)) { + fail(`${serverMessage(payload, response)}\n ${loginHint(serverMessage(payload, response), email)}`.trimEnd()); + } + return payload; +} + +/** 写 ~/.deta/credentials.json 与 ~/.deta/env(均 0600);token 只在 stdout 出现一次 */ +async function writeCredentials(baseUrl, identifier, payload) { + const token = payload.token; + const loginUsername = + typeof payload.username === 'string' && payload.username ? payload.username : identifier; + const dir = join(homedir(), '.deta'); + await mkdir(dir, { recursive: true, mode: 0o700 }); + await chmod(dir, 0o700).catch(() => undefined); + + const credentials = { + token, + username: loginUsername, + baseUrl, + loginAt: new Date().toISOString(), + }; + const credentialsPath = join(dir, 'credentials.json'); + const envPath = join(dir, 'env'); + await writePrivateFile(credentialsPath, `${JSON.stringify(credentials, null, 2)}\n`); + await writePrivateFile( + envPath, + `# DETA MCP 凭据(由 bin/deta-login.mjs 生成,勿提交版本库)\n` + + `# 用法:source ~/.deta/env\n` + + `export DETA_TOKEN=${shellSingleQuote(token)}\n`, + ); + + console.log(`DETA_TOKEN=${token}`); + console.log(`✅ 登录成功:${loginUsername} @ ${baseUrl}`); + console.log(`凭据已写入 ${credentialsPath}(0600),环境变量已写入 ${envPath}(0600)`); + console.log( + `使用:source ~/.deta/env 后再启动 harness(DETA_TOKEN 随环境传给 https://deta.city/mcp-api/mcp)`, + ); +} + +async function main() { + const { baseUrl, identifier: presetIdentifier, emailCode } = parseArgs(process.argv.slice(2)); + + process.stdout.write(`DETA 登录(${baseUrl})\n`); + + if (emailCode) { + process.stdout.write(`邮箱验证码登录:${emailCode}\n`); + return writeCredentials(baseUrl, emailCode, await loginByEmailCode(baseUrl, emailCode)); + } + + let identifier = presetIdentifier; + if (identifier) { + process.stdout.write(`登录标识:${identifier}\n`); + } else { + process.stdout.write('登录标识(用户名/已验证邮箱/已验证手机号):'); + identifier = (await readLine({ hidden: false })).trim(); + } + if (!identifier) fail('登录标识不能为空'); + + // 匿名预检:区分「标识未注册」与「密码不匹配」,避免密码错时给出误导性提示 + if ((await accountRegistered(baseUrl, identifier)) === false) { + fail( + `该标识在 ${baseUrl} 未注册:${identifier}\n` + + ' 请填门户登录页上实际使用的标识(用户名 / 已验证邮箱 / 已验证手机号)。\n' + + ' 邮箱验证码注册的账号,标识请填邮箱。', + ); + } + + process.stdout.write('密码:'); + const password = await readLine({ hidden: true }); + if (!password) fail('密码不能为空'); + + const { response, payload } = await postJson(baseUrl, '/prod-api/login', { + username: identifier, + password, + }); + const token = typeof payload?.token === 'string' ? payload.token : ''; + if (!response.ok || !token || (typeof payload?.code === 'number' && payload.code !== 200)) { + const message = serverMessage(payload, response); + const hint = loginHint(message, identifier); + fail(hint ? `${message}\n ${hint}` : message); + } + return writeCredentials(baseUrl, identifier, payload); +} + +main().catch((err) => { + fail(err instanceof Error ? err.message : String(err)); +}); diff --git a/plugins/writer/bin/deta-mcp.sh b/plugins/writer/bin/deta-mcp.sh new file mode 100755 index 0000000..1a780ad --- /dev/null +++ b/plugins/writer/bin/deta-mcp.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# DETA 产品插件的本机 MCP stdio 启动器(唯一入口,多产品共用) +# +# 两个宿主(Codex 插件 / dsh 组合包)都通过本脚本拉起同一个领域服务; +# 产品差异只由 --profile 决定(工作区默认位置、工具面),宿主侧只声明"调哪个脚本、带哪个档位"。 +# +# 用法:deta-mcp.sh [--profile ] +# +# 依赖:node + 已构建的领域服务运行时 +# +# 环境变量: +# DETA_SERVER_ROOT 领域服务运行时根目录(未设时自动查找,见下) +# DETA_WORKSPACE AML 工作目录(引擎侧工作区真值;缺省 ~/.deta//workspace) +# ANT_HEADLESS_WORKSPACE 同上(兼容旧名,DETA_WORKSPACE 优先) +# DETA_TOKEN deta.city 门户令牌(可选;不注入则云类工具返回 auth_required) +# ANT_WORKSPACE_BRIDGE_URL 网关基址(可选;与项目 ID 一起启用 Studio 执行器中继) +# DETA_SURFACE 工具面覆盖(缺省 full) +set -euo pipefail + +# ── 产品档位:宿主在声明里传 --profile;缺省 studio ── +PROFILE="${DETA_PROFILE:-writer}" +while [ $# -gt 0 ]; do + case "$1" in + --profile) PROFILE="${2:?--profile 需要一个值}"; shift 2 ;; + *) break ;; + esac +done + +# ── 运行时解析:显式 DETA_SERVER_ROOT → 本机引擎包安装位置 → 本机开发检出(打印提示)── +ROOT="${DETA_SERVER_ROOT:-}" +if [ -z "$ROOT" ]; then + for cand in "$HOME/.deta/runtime" "$HOME/Dev/ant/ant-copilot"; do + if [ -f "$cand/dist/deta-mcp.mjs" ] || [ -f "$cand/dist/src/headless/mcp-server.js" ]; then ROOT="$cand"; break; fi + done +fi + +if ! command -v node >/dev/null 2>&1; then + echo "[deta] 未找到 node,请先安装 Node.js" >&2 + exit 1 +fi + +# 入口优先取 esbuild 打包形态:tsc 产物在 Node ESM 下会因依赖包的目录 import 直接失败 +ENTRY="" +if [ -n "$ROOT" ]; then + if [ -f "$ROOT/dist/deta-mcp.mjs" ]; then + ENTRY="$ROOT/dist/deta-mcp.mjs" + elif [ -f "$ROOT/dist/src/headless/mcp-server.js" ]; then + ENTRY="$ROOT/dist/src/headless/mcp-server.js" + echo "[deta] 警告:回落到未打包产物,请先在该运行时目录执行 npm run build:mcp" >&2 + fi +fi + +if [ -z "$ENTRY" ]; then + echo "[deta] 未找到领域服务运行时(已尝试:DETA_SERVER_ROOT、\$HOME/.deta/runtime、\$HOME/Dev/ant/ant-copilot)" >&2 + echo "[deta] 请安装本机引擎包,或设置 DETA_SERVER_ROOT 指向已构建的领域服务仓库" >&2 + exit 1 +fi + +if [ "$ROOT" = "$HOME/Dev/ant/ant-copilot" ]; then + echo "[deta] 提示:使用本机开发检出 ${ROOT}(非打包运行时)" >&2 +fi + +# ── 工作目录:宿主不会传项目目录(cwd 通常是插件根),按产品档位定稳定位置 ── +WS="${DETA_WORKSPACE:-${ANT_HEADLESS_WORKSPACE:-$HOME/.deta/$PROFILE/workspace}}" +mkdir -p "$WS" +export ANT_HEADLESS_WORKSPACE="$WS" + +# ── 工具面:产品档位缺省全量面;DETA_SURFACE 可覆盖(如 engine)── +export DETA_TOOL_SURFACE="${DETA_SURFACE:-full}" + +echo "[deta] 档位=${PROFILE} 运行时=${ROOT} 工作区=${WS} 工具面=${DETA_TOOL_SURFACE} 云令牌=$([ -n "${DETA_TOKEN:-}" ] && echo 已注入 || echo 未注入)" >&2 + +exec node "$ENTRY" "$@" diff --git a/plugins/writer/cordis.patch.yml b/plugins/writer/cordis.patch.yml new file mode 100644 index 0000000..c986929 --- /dev/null +++ b/plugins/writer/cordis.patch.yml @@ -0,0 +1,32 @@ +# DETA 组合包(dsh bundle)配置层 —— Writer +# +# 挂载方式:dsh plugin --profile add <本包> +# 本层只做一件事:把 DETA 领域能力(Writer 档位)作为 MCP 服务挂进 harness。 +# 工具以 mcp__writer_mcp__ 暴露(与 Studio 插件互不冲突,可同时安装)。 +# +# 形态:本机 stdio(唯一管道)——与 Codex 插件共用同一个启动器 bin/deta-mcp.sh, +# 由它解析领域服务运行时(DETA_SERVER_ROOT → ~/.deta/runtime → 本机开发检出)。 +# 工作目录缺省 ~/.deta/writer/workspace(与 Studio 档位隔离,实际值见启动器 stderr)。 +# +# 需要云侧能力(项目/楼栋列表、push/pull 同步、Studio 执行器中继)时注入: +# DETA_TOKEN deta.city 门户令牌(node bin/deta-login.mjs 获取) +# ANT_WORKSPACE_BRIDGE_URL 网关基址(与项目 ID 一起构成 host-call 中继三要素) +# +# 前置:设置 DETA_PLUGIN_ROOT 指向本包所在目录(未设置时启动器明确报错退出)。 +# +- insert: + - id: deta-writer-mcp + name: '@deepseek-ai/dsh-mcp-client' + config: + serverName: writer_mcp + transport: stdio + command: bash + args: + - -lc + - 'exec "${DETA_PLUGIN_ROOT:?请设置 DETA_PLUGIN_ROOT 指向 DETA 插件目录}/bin/deta-mcp.sh" --profile writer' + env: + DETA_PLUGIN_ROOT: !!js process.env.DETA_PLUGIN_ROOT ?? '' + DETA_TOKEN: !!js process.env.DETA_TOKEN ?? '' + DETA_WORKSPACE: !!js process.env.DETA_WORKSPACE ?? '' + toolCallTimeoutMs: 600000 + failOnStartupError: true diff --git a/plugins/writer/package-lock.json b/plugins/writer/package-lock.json new file mode 100644 index 0000000..8783ba9 --- /dev/null +++ b/plugins/writer/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "deta-writer-plugin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deta-writer-plugin", + "version": "0.1.0", + "license": "Proprietary" + } + } +} diff --git a/plugins/writer/package.json b/plugins/writer/package.json new file mode 100644 index 0000000..707c292 --- /dev/null +++ b/plugins/writer/package.json @@ -0,0 +1,20 @@ +{ + "name": "deta-writer-plugin", + "version": "0.1.0", + "description": "DETA Writer:建筑性能报告与文档产出(Codex 插件 / dsh 组合包 双壳)", + "type": "module", + "license": "Proprietary", + "files": [ + "cordis.patch.yml", + "skills", + ".codex-plugin", + ".mcp.json", + "bin", + "README.md" + ], + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + } +} diff --git a/plugins/writer/skills/report-authoring/SKILL.md b/plugins/writer/skills/report-authoring/SKILL.md new file mode 100644 index 0000000..d045235 --- /dev/null +++ b/plugins/writer/skills/report-authoring/SKILL.md @@ -0,0 +1,43 @@ +--- +name: report-authoring +description: 把建筑模型与模拟结果整理成报告/文档内容。当用户要求写节能专篇、性能报告、甲方汇报材料,或需要从当前项目取指标与结论成文时使用。 +--- + +# DETA Writer —— 报告与文档产出 + +## ★ 先读:能力边界与取数顺序 + +本插件负责**产出报告内容**(文本 / Markdown / 结构化数据),不负责排版与导出: + +- **报告模板、插槽填充、docx 导出在门户 Writer 页面**(`https://deta.city/writer?doc=`)—— + 不在本插件的工具面里,不要试图用 MCP 工具生成模板或导出文件。 +- 本插件能做的是:读模型与工作区文件、取统计数据、取模拟结果、查规范指标,然后把它们写成文字。 +- 不确定当前有哪些工具时,先调 **`list_capabilities`** 拿完整工具名再按名字调用 + (检索一次只回少量结果,搜不到不等于不存在)。 + +## 取数顺序(每步都可能缺数据,缺就如实标注) + +1. **模型真值**:工作区里的 `aml.xml` / `building.json`——用宿主原生文件工具或 `read_source` 读。 +2. **围护结构统计**:外墙/外窗面积、实际窗墙比、朝向分布——用 `envelope_stats` 取,不要手算。 +3. **性能分析**:能耗、舒适度等按需用 `analyze_building`;结果里的 `executor` / `backend` 标注要照抄进报告。 +4. **模拟结果缓存**:已跑过的采光/日照/通风等结果用 `query_simulation_db` 查,避免重复跑。 +5. **气象**:需要气候分区/月均/极值数据时先 `load_weather` 加载气象文件,再取结果。 +6. **规范与指标**:阈值、限值、技术策略必须用 `query_knowledge` 取原值,不得凭经验写数。 + +## 铁律 + +1. **不编造数值**。任何数字要么来自工具返回、要么来自工作区文件;没有就写 `--` 或删掉那句话, + 不要用"约/大约/典型值"填空。 +2. **区分设计值与限值**。设计值由模型算出;限值来自规范条文,缺失时留空并标注"待补规范限值"。 +3. **标注数据来源与执行位置**。同一指标可能来自本机引擎或 Studio 执行器,按工具返回的标注如实写。 +4. **写报告是只读过程**。不要顺手改模型;确需修正时明确告知用户并单独确认。 +5. **控制单会话工具调用量**。写报告是长流程,撞到命名空间重复类错误时不要重试,另起会话继续 + (工作区文件不会丢)。 + +## 标准工作流 + +1. 确认写什么:报告类型、范围(整栋/单栋/单专业)、读者(甲方/评审/内部)。 +2. 列"能写什么、缺什么"两栏,**先把缺项告诉用户**,不要写到一半才发现没数据。 +3. 按取数顺序补齐数据(能算的算、能查的查)。 +4. 成文:分章节写入工作区(Markdown),每段数据标注来源指标名;缺项如实留空。 +5. 交回用户:说明哪些章节已完稿、哪些等数据;需要在门户导出 docx 时给出 Writer 页面链接。 diff --git a/scripts/check-plugin-contract.mjs b/scripts/check-plugin-contract.mjs new file mode 100644 index 0000000..fbc8da0 --- /dev/null +++ b/scripts/check-plugin-contract.mjs @@ -0,0 +1,163 @@ +#!/usr/bin/env node +/** + * 插件仓契约校验(仓库职责章程的机器化部分) + * + * 章程:本仓是**宿主适配与分发层**,零逻辑、不承载契约。 + * 能力(工具实现 / MCP 协议 / 启动形态 / 工具面裁剪 / 领域技能)唯一来源在 ant-copilot; + * 本仓只保留「宿主清单 + 连接声明 + 启动器 + 宿主侧投影」。 + * + * 规则: + * E1 技能文档不得出现「工具一览 / 工具清单」类章节标题 + * —— 工具面随部署形态裁剪(engine / full),任何手写清单必然过期; + * 取清单的唯一途径是运行时调 `list_capabilities`。 + * E2 技能文档必须给出「取当前形态工具名」的途径(出现 `list_capabilities`) + * —— 正面要求,零误报:只要技能不许自带清单、又必须教模型去哪查, + * 工具面增删就与本文档解耦。(曾试过"段落内枚举 ≥5 个标识符"的启发式, + * 误报率高——流程散文里正常提及工具名会被误判,已弃用。) + * E3 插件 README 不得解释领域服务内部常量 + * —— 那是 ant-copilot 的实现细节,文档越界即漂移源。 + * W1 不得出现开发检出路径(~/Dev/ant/ant-copilot) + * —— 本机引擎包落地后升级为错误(见章程 M2)。 + * + * 用法:node scripts/check-plugin-contract.mjs [--json] + * 退出码:有 error 时 1,否则 0(warning 不失败)。 + */ +import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; +import { join, relative, basename, resolve } from 'node:path'; + +const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, ''); +const PLUGINS = join(ROOT, 'plugins'); +/** 本目录是工具自身,不参与内容校验(其注释里必然出现被禁模式) */ +const SKIP_DIRS = new Set(['node_modules', '.git', 'scripts']); + +/** 遍历目录,返回全部文件绝对路径 */ +function walk(dir) { + const out = []; + for (const name of readdirSync(dir)) { + if (SKIP_DIRS.has(name)) continue; + const p = join(dir, name); + const st = statSync(p); + if (st.isDirectory()) out.push(...walk(p)); + else out.push(p); + } + return out; +} + +const RULES = { + E1: { + level: 'error', + what: '技能文档出现「工具一览 / 工具清单」类章节标题', + hint: '删掉清单,改为「不确定有哪些工具时调 list_capabilities」——工具面按形态裁剪,手写清单必然过期', + }, + E2: { + level: 'error', + what: '技能文档没有给出取当前形态工具名的途径(缺 list_capabilities 指引)', + hint: '在技能开头加一句:不确定有哪些工具时先调 list_capabilities 拿完整工具名,再按名字直接调用', + }, + E3: { + level: 'error', + what: '插件 README 解释领域服务内部常量', + hint: 'ant-copilot 的实现细节不进本仓文档;本仓只写安装 / 连接 / 排障', + }, + E4: { + level: 'error', + what: '市场条目与实际插件目录不一致(悬空条目或名称不匹配)', + hint: '条目 name 必须等于该目录 .codex-plugin/plugin.json 的 name,path 必须真实存在——改名/加产品时同步改市场文件', + }, + W1: { + level: 'warning', + what: '出现开发检出路径(~/Dev/ant/ant-copilot)', + hint: '启动器应随包分发运行时;本机引擎包落地后本规则升级为 error', + }, +}; + +const HEADING_BAN = /工具一览|工具清单|工具列表|工具汇总|可用工具|能力一览|能力清单/; +/** + * 只认「赋值/缺省」形态(`VAR=` 开头)——那才是"解释内部语义"的信号; + * 裸写常量名的行常常是章程里在说"不要解释它",或启动器在转发自己的用户级旋钮, + * 一律放行(宁可漏报,不要把自己的章程判成违规)。 + */ +const INTERNAL_CONST = /\b(?:DETA_TOOL_SURFACE|DETA_MCP_PORT|DETA_GSA_URL)\s*=|dist\/src\/headless|dist\/deta-mcp/; +const DEV_PATH = /(?:\$HOME|~|\/Users\/[^/\s]+)\/Dev\/ant\/ant-copilot/; + +const findings = []; +const add = (rule, file, line, evidence) => + findings.push({ rule, level: RULES[rule].level, file: relative(ROOT, file), line, evidence: evidence.trim() }); + +const files = walk(ROOT); + +for (const file of files) { + const isSkill = file.includes(`${join('skills', '')}`) && basename(file) === 'SKILL.md'; + const isReadme = basename(file).toLowerCase().startsWith('readme'); + const isText = /\.(md|ya?ml|json|mjs|sh|ts)$/.test(file); + + let text; + try { + text = readFileSync(file, 'utf8'); + } catch { + continue; + } + const lines = text.split('\n'); + + lines.forEach((line, i) => { + const n = i + 1; + // E1:技能文档的章节标题 + if (isSkill && /^#{2,6}\s/.test(line) && HEADING_BAN.test(line)) add('E1', file, n, line); + // E3:README 里的领域服务内部常量 + if (isReadme && INTERNAL_CONST.test(line)) add('E3', file, n, line); + // W1:开发检出路径 + if (isText && DEV_PATH.test(line)) add('W1', file, n, line); + }); + + // E2:技能必须教模型去哪取工具名(正面要求,零误报) + if (isSkill && !text.includes('list_capabilities')) { + add('E2', file, 1, '全文未出现 list_capabilities'); + } +} + +// E4:市场条目 ↔ 插件目录一致性(改名 / 加产品时防悬空条目) +for (const rel of ['.agents/plugins/marketplace.json', '.agents/plugins/api_marketplace.json']) { + const abs = join(ROOT, rel); + if (!existsSync(abs)) continue; + let market; + try { + market = JSON.parse(readFileSync(abs, 'utf8')); + } catch (e) { + add('E4', abs, 1, `市场文件不是合法 JSON:${e.message}`); + continue; + } + for (const entry of market.plugins ?? []) { + const relPath = String(entry?.source?.path ?? ''); + const manifest = join(resolve(ROOT, relPath), '.codex-plugin', 'plugin.json'); + if (!relPath || !existsSync(manifest)) { + add('E4', abs, 1, `条目 name=${entry?.name ?? '?'} 指向的插件目录不存在或缺插件清单:${relPath || '(空 path)'}`); + continue; + } + let name = ''; + try { + name = String(JSON.parse(readFileSync(manifest, 'utf8')).name ?? ''); + } catch { + // 清单本身不合法由 validate_plugin.py 报,这里不重复 + } + if (name !== entry.name) { + add('E4', abs, 1, `市场条目 name=${entry.name} 与插件清单 name=${name} 不一致`); + } + } +} + +if (process.argv.includes('--json')) { + console.log(JSON.stringify({ findings, rules: RULES }, null, 2)); +} else { + for (const f of findings) { + const mark = f.level === 'error' ? '✗' : '!'; + console.log(`${mark} [${f.rule}] ${f.file}:${f.line}`); + console.log(` ${f.evidence.slice(0, 140)}`); + if (f.level === 'error') console.log(` → ${RULES[f.rule].hint}`); + } + const errs = findings.filter((f) => f.level === 'error').length; + const warns = findings.length - errs; + console.log(`\n契约校验:${errs} 个错误 / ${warns} 个警告`); + if (!findings.length) console.log('✓ 全部通过'); +} + +process.exit(findings.some((f) => f.level === 'error') ? 1 : 0);