命名结构(品牌=市场,产品=插件):市场名保持 deta;插件 deta → studio(displayName DETA Studio),新增 writer(DETA Writer)。安装标识 studio@deta / writer@deta;MCP 服务名 studio_mcp / writer_mcp,可同时安装。plugins/deta → plugins/studio(git mv 保留历史),新增 plugins/writer。 连接形态统一为单一本机 stdio 管道:.mcp.json 与 cordis.patch.yml 均声明 stdio → bin/deta-mcp.sh --profile <产品>,并按官方插件写法补 cwd: "."(缺它时 Cwd=- ,相对命令按宿主进程 cwd 解析会静默起不来);启动器新增 --profile 档位(工作区 ~/.deta/<产品>/workspace、工具面缺省 full),运行时按 DETA_SERVER_ROOT → ~/.deta/runtime → 开发检出解析,入口优先打包产物;DETA_TOKEN 可选注入(缺省时云类工具返回 auth_required)。原远程端点声明(含 OAuth/401 段落)从插件移除。 文档与治理:根 README 与两个插件 README 改为单一管道口径;技能 aml-design 对齐本机全量面;新增 scripts/check-plugin-contract.mjs(E1 禁工具清单 / E2 必须给出 list_capabilities / E3 禁解释内部实现 / E4 市场条目一致性 / W1 开发检出依赖)与 .gitea/workflows/ci.yml。版本:studio 0.3.1、writer 0.1.0。
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 <name> 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 <name> 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` | `<cwd>/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`) | ✅ |
|
||||
|
||||
共享面(两端一致,零适配):
|
||||
- **技能文件格式**:`<name>/SKILL.md` + frontmatter 必填 `name`/`description`,kebab-case。
|
||||
Codex 与 dsh(`@deepseek-ai/dsh-skill-filesystem`)要求相同。
|
||||
- **共享技能根**:两端都扫描 `~/.agents/skills/` 与 `<项目>/.agents/skills/`
|
||||
(dsh 侧为 rank 500 / 200 的发现根,且显式支持 `<agentsHome>/skills`)。
|
||||
把技能放这里即可双端生效,不需要安装插件。
|
||||
- **MCP 工具命名**:两端同为 `mcp__<serverName>__<tool>`,我们统一 `serverName = deta`,
|
||||
因此技能里写裸工具名(`read_source`、`apply_design`)在两端都能解析。
|
||||
- **MCP 工具命名**:两端同为 `mcp__<serverName>__<tool>`,我们统一 `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 <name> add ~/Dev/ant/deta-agent-plugins/plugins/deta
|
||||
|
||||
# 公开发布形态(二选一)
|
||||
npm publish # 用户:dsh plugin --profile <name> add deta-plugin
|
||||
dsh plugin --profile <name> add github:<你>/<仓库> # 需作者提供 prepare + 用户 allowBuilds
|
||||
export DETA_PLUGIN_ROOT=~/Dev/ant/deta-agent-plugins/plugins/studio
|
||||
dsh plugin --profile <name> 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 # 宿主侧技能(报告取数与纪律)
|
||||
```
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"deta_mcp": {
|
||||
"type": "streamable-http",
|
||||
"url": "https://deta.city/mcp-api/mcp",
|
||||
"omit_tools_from": ["deferred"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/<projectId>`。
|
||||
- 文件名沿用服务端约定(`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/<ID>" }
|
||||
(带 projectId 时同时在服务端工作区写入 deta.json 绑定)
|
||||
3. 引擎校验 apply_design() → 编译 AML 为建筑模型(服务端工作区内的最新版本)
|
||||
validate_model() → 语法/几何问题,逐条修复后重新 push + 编译
|
||||
envelope_stats() / query_spatial_graph() → 围护结构与空间邻接
|
||||
4. Studio 查看 在右侧内置浏览器面板打开返回的 studioUrl
|
||||
(https://deta.city/design/<projectId>,例:云项目 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://<host> # 自建/预发环境
|
||||
source ~/.deta/env # 导出 DETA_TOKEN
|
||||
```
|
||||
|
||||
**标识(identifier)填门户登录页上用的那一个**:用户名、已验证邮箱、已验证手机号,服务端都会解析到同一账号。
|
||||
助手先匿名调用 `/login/check` 预检标识是否已注册,再走 `/login` 做密码校验(不走门户的「登录即注册」端点,
|
||||
不会误建账号)。失败时按服务端提示给出下一步:
|
||||
|
||||
| 服务端提示 | 含义与处理 |
|
||||
|------------|------------|
|
||||
| `该标识在 <host> 未注册` | 标识不是门户上用的那一个;邮箱验证码注册的账号标识填邮箱 |
|
||||
| `用户不存在/密码错误` | 服务端不区分「账号不存在」与「密码不匹配」;已确认标识存在,即密码不符,或改用 `--email-code` |
|
||||
| `该邮箱/手机号尚未验证…` | 先用验证码方式登录一次门户完成验证 |
|
||||
| `该登录方式未开放` | 用 `GET <base>/prod-api/login/methods` 查当前启用项 |
|
||||
|
||||
## 安装(dsh)
|
||||
|
||||
```bash
|
||||
node ~/Dev/ant/deta-agent-plugins/plugins/deta/bin/deta-login.mjs
|
||||
source ~/.deta/env
|
||||
dsh plugin --profile <name> 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 领域技能
|
||||
```
|
||||
@@ -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 工作目录(缺省 <cwd>/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" "$@"
|
||||
@@ -1,40 +0,0 @@
|
||||
# DETA 组合包(dsh bundle)配置层
|
||||
#
|
||||
# 挂载方式:dsh plugin --profile <name> add <本包>
|
||||
# 本层只做一件事:把 DETA 领域能力作为 MCP 服务挂进 harness。
|
||||
# 工具以 mcp__deta__<tool> 暴露(与 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 ?? ''
|
||||
Generated
-13
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/<projectId>`。
|
||||
- 文件名沿用引擎侧约定(`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/<ID>" }
|
||||
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://<host> # 自建/预发环境
|
||||
```
|
||||
|
||||
**标识(identifier)填门户登录页上用的那一个**:用户名、已验证邮箱、已验证手机号,服务端都会解析到同一账号。
|
||||
助手先匿名调用 `/login/check` 预检标识是否已注册,再走 `/login` 做密码校验(不走门户的「登录即注册」端点,
|
||||
不会误建账号)。失败时按服务端提示给出下一步:
|
||||
|
||||
| 服务端提示 | 含义与处理 |
|
||||
|------------|------------|
|
||||
| `该标识在 <host> 未注册` | 标识不是门户上用的那一个;邮箱验证码注册的账号标识填邮箱 |
|
||||
| `用户不存在/密码错误` | 服务端不区分「账号不存在」与「密码不匹配」;已确认标识存在,即密码不符,或改用 `--email-code` |
|
||||
| `该邮箱/手机号尚未验证…` | 先用验证码方式登录一次门户完成验证 |
|
||||
| `该登录方式未开放` | 用 `GET <base>/prod-api/login/methods` 查当前启用项 |
|
||||
|
||||
## 安装(dsh)
|
||||
|
||||
```bash
|
||||
export DETA_PLUGIN_ROOT=<本包所在目录> # 配置层按它定位启动器
|
||||
export DETA_WORKSPACE=<你的项目目录> # 可选;缺省 ~/.deta/workspace
|
||||
dsh plugin --profile <name> 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 单一管道**,远程端点声明从插件移除 |
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# DETA 产品插件的本机 MCP stdio 启动器(唯一入口,多产品共用)
|
||||
#
|
||||
# 两个宿主(Codex 插件 / dsh 组合包)都通过本脚本拉起同一个领域服务;
|
||||
# 产品差异只由 --profile 决定(工作区默认位置、工具面),宿主侧只声明"调哪个脚本、带哪个档位"。
|
||||
#
|
||||
# 用法:deta-mcp.sh [--profile <studio|writer>]
|
||||
#
|
||||
# 依赖:node + 已构建的领域服务运行时
|
||||
#
|
||||
# 环境变量:
|
||||
# DETA_SERVER_ROOT 领域服务运行时根目录(未设时自动查找,见下)
|
||||
# DETA_WORKSPACE AML 工作目录(引擎侧工作区真值;缺省 ~/.deta/<profile>/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" "$@"
|
||||
@@ -0,0 +1,33 @@
|
||||
# DETA 组合包(dsh bundle)配置层
|
||||
#
|
||||
# 挂载方式:dsh plugin --profile <name> add <本包>
|
||||
# 本层只做一件事:把 DETA 领域能力作为 MCP 服务挂进 harness。
|
||||
# 工具以 mcp__deta__<tool> 暴露(与 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
|
||||
Generated
+13
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": [
|
||||
+26
-26
@@ -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?: "<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 与返回为准。
|
||||
@@ -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": [
|
||||
"读取当前项目的建筑模型,列出可用于报告的关键指标。",
|
||||
"把本项目的围护结构与能耗结果整理成报告初稿,缺失数据如实标注。"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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=<docId>`)——**不是 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 <name> 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 …`):上游代理问题,另起会话,不要重试。
|
||||
Executable
+368
@@ -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 <base>/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 <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));
|
||||
});
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# DETA 产品插件的本机 MCP stdio 启动器(唯一入口,多产品共用)
|
||||
#
|
||||
# 两个宿主(Codex 插件 / dsh 组合包)都通过本脚本拉起同一个领域服务;
|
||||
# 产品差异只由 --profile 决定(工作区默认位置、工具面),宿主侧只声明"调哪个脚本、带哪个档位"。
|
||||
#
|
||||
# 用法:deta-mcp.sh [--profile <studio|writer>]
|
||||
#
|
||||
# 依赖:node + 已构建的领域服务运行时
|
||||
#
|
||||
# 环境变量:
|
||||
# DETA_SERVER_ROOT 领域服务运行时根目录(未设时自动查找,见下)
|
||||
# DETA_WORKSPACE AML 工作目录(引擎侧工作区真值;缺省 ~/.deta/<profile>/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" "$@"
|
||||
@@ -0,0 +1,32 @@
|
||||
# DETA 组合包(dsh bundle)配置层 —— Writer
|
||||
#
|
||||
# 挂载方式:dsh plugin --profile <name> add <本包>
|
||||
# 本层只做一件事:把 DETA 领域能力(Writer 档位)作为 MCP 服务挂进 harness。
|
||||
# 工具以 mcp__writer_mcp__<tool> 暴露(与 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
|
||||
Generated
+13
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: report-authoring
|
||||
description: 把建筑模型与模拟结果整理成报告/文档内容。当用户要求写节能专篇、性能报告、甲方汇报材料,或需要从当前项目取指标与结论成文时使用。
|
||||
---
|
||||
|
||||
# DETA Writer —— 报告与文档产出
|
||||
|
||||
## ★ 先读:能力边界与取数顺序
|
||||
|
||||
本插件负责**产出报告内容**(文本 / Markdown / 结构化数据),不负责排版与导出:
|
||||
|
||||
- **报告模板、插槽填充、docx 导出在门户 Writer 页面**(`https://deta.city/writer?doc=<docId>`)——
|
||||
不在本插件的工具面里,不要试图用 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 页面链接。
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user