Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/skill-eval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
python tools/atomize.py --selftest
python tools/mcp_server.py --selftest
python tools/vlm_review.py --selftest
python tools/pack_plugin.py --selftest
python evals/real_trigger.py --selftest
- name: 插件清单与资源存在性
run: python tools/check_plugin.py
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ _work/

# ZCode 平台项目规则与本机会话缓存
.zcode/

# 插件打包产物(tools/pack_plugin.py 生成,供本地市场安装)
dist/
marketplace.json
1 change: 0 additions & 1 deletion .zcode-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@
"./ecom-detail-page-prompts"
],
"commands": "./commands",
"hooks": "./hooks/hooks.json",
"mcpServers": "./.mcp.json"
}
10 changes: 7 additions & 3 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ Each subdirectory is a complete skill: copy it into your local skills directory

### As a ZCode plugin (recommended)

The repo root is a plugin. Add the repo directory as a local marketplace, then install:
The plugin bundles all three skills, four slash commands, the post-generation check hook, and the MCP server in one install.

```bash
git clone https://github.com/ADC1920/Ecom-Prompt-Studio.git
cd Ecom-Prompt-Studio
python tools/pack_plugin.py
```

In ZCode: **Plugin Marketplace → Add → Add Plugin Marketplace**, paste the cloned directory, then install `ecom-prompt-studio` from **Personal**.
This writes a marketplace manifest and a plugin directory into `dist/`. In ZCode: **Plugin Marketplace → Add → Add Plugin Marketplace**, paste the absolute path to `dist/`, then install `ecom-prompt-studio` from **Personal**.

This installs all three skills, four slash commands, the post-generation check hook, and the MCP server in one step.
Why pack instead of pointing at the repo root: ZCode installs a plugin by copying the whole source directory and does not consult `.gitignore`. Using the repo root directly would carry locally created `docs/`, `memory/`, and `AGENTS.md` into the install cache. `pack_plugin.py` copies only git-tracked files, so local-only content cannot leak.

### As plain skills

Expand Down Expand Up @@ -73,6 +75,8 @@ The only routing rule: **category, action, or scenario cue present → SOP atom
| `tools/corpus.py` | positive-sample corpus: add, similarity search, compile few-shot blocks |
| `tools/lint_skills.py` | full static check, the first CI gate |
| `tools/mcp_server.py` | MCP server exposing the atom library and checks as four tools |
| `tools/check_plugin.py` | validates manifest fields, declared resources, path containment, event names |
| `tools/pack_plugin.py` | builds a clean plugin bundle from git-tracked files only |

Machine checks run against images from **any** channel. This repo deliberately does not bundle an image-generation CLI: bring your own endpoint, then verify the output here.

Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@

### 推荐:作为 ZCode 插件安装

仓库根就是插件,三个技能、四条斜杠命令、出图后自动机检、MCP server 一次装齐:
插件形态下,三个技能、四条斜杠命令、出图后自动机检、MCP server 一次装齐。

```bash
git clone https://github.com/ADC1920/Ecom-Prompt-Studio.git
cd Ecom-Prompt-Studio
python tools/pack_plugin.py
```

在 ZCode 里打开 **插件市场 → 添加 → 添加插件市场**,粘贴克隆下来的目录,然后在**个人**里安装 `ecom-prompt-studio`。
打包会在 `dist/` 生成市场清单与插件目录。在 ZCode 里打开 **插件市场 → 添加 → 添加插件市场**,粘贴 `dist/` 的**绝对路径**,然后在**个人**里安装 `ecom-prompt-studio`。

为什么要打包而不是直接把仓库根当市场:ZCode 的插件安装是整目录拷贝,不问 `.gitignore`。仓库根直接作插件源时,克隆下来自建的 `docs/`、`memory/`、`AGENTS.md` 会一起进安装缓存。`pack_plugin.py` 只复制 git 跟踪的文件,本机内容天然被排除。

只拷单个技能目录也能用,但那会丢掉 `tools/`:机检、规则引擎、语料检索、MCP 工具都不在技能目录里,SKILL.md 提到脚本的地方只能人工代劳。要完整能力就走插件。

Expand Down Expand Up @@ -120,6 +124,7 @@ atomize compose slop check visual fix_stats
| `tools/mcp_server.py` | MCP server:把原子库与判据暴露成四个可调用工具 |
| `tools/lint_skills.py` | 静态检查全集,CI 的第一道门 |
| `tools/check_plugin.py` | 插件清单自校验:字段、资源存在性、路径越界、事件名合法性 |
| `tools/pack_plugin.py` | 打干净的插件包:只复制 git 跟踪文件,本机内容不进安装缓存 |

除 `check_image.py`、`visual_audit.py`、`vlm_review.py` 用到 Pillow 外全是标准库;带 `--selftest` 的都能离线验,不依赖密钥与网络。

Expand Down
170 changes: 170 additions & 0 deletions tools/pack_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""把仓库打成干净的插件包,供本地市场安装。

为什么需要它:ZCode 插件安装是整目录拷贝,不问 .gitignore。以仓库根直接作插件源时,
AGENTS.md、docs/、memory/、.v2c 这些本机专属文件会一起进安装缓存,含本机路径的
project-info.md 也会被带走。

做法:用 git ls-files 取已跟踪文件——本机专属内容本来就不在版本控制里,过滤天然成立,
不需要维护白名单,仓库演进时也不会失准。

输出结构(市场根与插件根分开,与真实分发一致):

dist/
marketplace.json source 指向 ./ecom-prompt-studio
ecom-prompt-studio/ 全部已跟踪的分发内容

用法:
python tools/pack_plugin.py # 打包到 dist/
python tools/pack_plugin.py --out <目录> # 指定输出目录
python tools/pack_plugin.py --selftest
"""
import argparse
import json
import shutil
import subprocess
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parent
PLUGIN_NAME = "ecom-prompt-studio"
DEFAULT_OUT = ROOT / "dist"

# 仓库元数据,不是插件内容:CI 配置、git 自身的忽略与换行规则
REPO_ONLY = {".github", ".gitignore", ".gitattributes"}

# 打包后必须不存在的顶层项。命中就是过滤失效
FORBIDDEN = {"docs", "memory", "AGENTS.md", ".v2c", ".tmp", "out", "dist", "marketplace.json"}


def tracked_files():
p = subprocess.run(
["git", "ls-files", "-z"], cwd=ROOT,
capture_output=True, text=True, encoding="utf-8",
)
if p.returncode != 0:
raise RuntimeError(f"git ls-files 失败:{(p.stderr or '').strip()}")
return [f for f in p.stdout.split("\0") if f]


def top_of(rel):
return rel.split("/")[0]


def pack(out_dir):
out_dir = Path(out_dir).resolve()
plugin_dir = out_dir / PLUGIN_NAME

if out_dir.exists():
shutil.rmtree(out_dir)
plugin_dir.mkdir(parents=True)

copied, skipped = 0, []
for rel in tracked_files():
if top_of(rel) in REPO_ONLY:
skipped.append(rel)
continue
src = ROOT / rel
dst = plugin_dir / rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
copied += 1

manifest = json.loads((plugin_dir / ".zcode-plugin" / "plugin.json").read_text(encoding="utf-8"))
marketplace = {
"name": "dev-ecom-prompt-studio",
"plugins": [{
"name": manifest["name"],
"source": f"./{PLUGIN_NAME}",
"version": manifest["version"],
"description": manifest["description"],
"displayName": "Ecom Prompt Studio",
"displayName_i18n": {"zh-CN": "电商提示词五工作室"},
"description_i18n": {"zh-CN": "电商视觉五工作室提示词 SOP,含原子库、判据引擎、出图后机检与 MCP 工具"},
"category": "productivity",
}],
}
(out_dir / "marketplace.json").write_text(
json.dumps(marketplace, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

return {"out": str(out_dir), "plugin": str(plugin_dir),
"copied": copied, "skipped": len(skipped), "version": manifest["version"]}


def verify(out_dir):
"""打包结果自校验:本机内容不得进包,关键资源必须齐。"""
out_dir = Path(out_dir).resolve()
plugin_dir = out_dir / PLUGIN_NAME
fails = []

if not plugin_dir.is_dir():
return [f"插件目录不存在:{plugin_dir}"]

tops = {p.name for p in plugin_dir.iterdir()}
leaked = tops & FORBIDDEN
if leaked:
fails.append(f"本机专属内容进了包:{sorted(leaked)}")

for need in (".zcode-plugin/plugin.json", "tools/mcp_server.py", "hooks/hooks.json",
"ecom-prompt-sop/SKILL.md", "commands", "README.md"):
if not (plugin_dir / need).exists():
fails.append(f"缺少必需内容:{need}")

mc = out_dir / "marketplace.json"
if not mc.exists():
fails.append("缺少 marketplace.json")
else:
data = json.loads(mc.read_text(encoding="utf-8"))
src = (data.get("plugins") or [{}])[0].get("source", "")
if src != f"./{PLUGIN_NAME}":
fails.append(f"市场 source 应为 ./{PLUGIN_NAME},实际 {src!r}")

# 本机路径检测:取当前用户主目录动态比对,不硬编码盘符与用户名。
# 只检主目录而非盘符根,避免任何提到该盘符的文本被误判。
home = str(Path.home())
# 同时检反斜杠形式,仓库文本可能是 Windows 路径
markers = {home, home.replace("\\", "/"), home.replace("/", "\\")}

for p in plugin_dir.rglob("*"):
if p.is_file() and p.suffix in (".md", ".py", ".json", ".yaml", ".yml"):
text = p.read_text(encoding="utf-8", errors="ignore")
if any(m and m in text for m in markers):
fails.append(f"含本机路径:{p.relative_to(plugin_dir)}")

return fails


def main():
ap = argparse.ArgumentParser(description="打干净的插件包供本地市场安装")
ap.add_argument("--out", default=str(DEFAULT_OUT), help=f"输出目录,默认 {DEFAULT_OUT}")
ap.add_argument("--selftest", action="store_true")
args = ap.parse_args()

if args.selftest:
got = pack(ROOT / ".tmp" / "pack-selftest")
fails = verify(got["out"])
shutil.rmtree(ROOT / ".tmp" / "pack-selftest", ignore_errors=True)
if fails:
for f in fails:
print("FAIL", f)
return 1
print(f"selftest: 打包 {got['copied']} 文件、排除 {got['skipped']} 个仓库元数据,"
f"本机内容零泄漏,关键资源齐全")
return 0

got = pack(args.out)
fails = verify(got["out"])
print(f"打包完成:{got['copied']} 个文件,跳过 {got['skipped']} 个仓库元数据")
print(f"市场根:{got['out']}")
print(f"插件根:{got['plugin']}")
if fails:
for f in fails:
print(f"错误: {f}")
return 1
print("校验通过:无本机内容、关键资源齐全、市场清单正确")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading