Improve agent workflow docs, add project governance files, and harden examples/bootstrap

This commit is contained in:
luoluoluo22
2026-03-05 10:57:25 +08:00
parent 6fd7aa5cf2
commit 1aa8e60f90
15 changed files with 385 additions and 94 deletions
+15
View File
@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml,json,toml}]
indent_size = 2
+27
View File
@@ -0,0 +1,27 @@
## Summary
- What changed?
- Why is this needed?
## Scope
- Affected modules/files:
- Breaking changes: yes/no
## Validation
- [ ] `ruff check scripts tests tools`
- [ ] `black --check scripts tests tools`
- [ ] `python -m pytest tests/test_wrapper.py -q`
- [ ] `python tools/check_repo_hygiene.py`
- [ ] `python tools/validate_data_schema.py`
## Risk and Rollback
- Risk level:
- Rollback plan:
## Checklist
- [ ] Docs updated (`SKILL.md` / `rules/` / `docs/api.md`) if API/behavior changed
- [ ] No runtime artifacts committed
+20
View File
@@ -0,0 +1,20 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.2
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/psf/black
rev: 25.1.0
hooks:
- id: black
- repo: local
hooks:
- id: pytest-wrapper
name: pytest (wrapper quick suite)
entry: python -m pytest tests/test_wrapper.py -q
language: system
pass_filenames: false
+37
View File
@@ -0,0 +1,37 @@
# Contributing
## Branching
- Use feature branches for all changes.
- Keep `main` releasable.
- Prefer small, focused pull requests.
## Local Setup
```bash
pip install -r requirements.txt
pip install pytest ruff black pre-commit
pre-commit install
```
## Required Checks
```bash
ruff check scripts tests tools
black --check scripts tests tools
python -m pytest tests/test_wrapper.py -q
python tools/check_repo_hygiene.py
python tools/validate_data_schema.py
```
## Commit Rules
- Use clear commit messages with intent and scope.
- Include docs updates when behavior or APIs change.
- Do not commit runtime artifacts (`cloud_cache`, `__pycache__`, logs).
## Pull Request Rules
- Describe user impact and migration risk.
- Add concrete verification steps.
- Include before/after behavior for API-affecting changes.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 luoluoluo22
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+23
View File
@@ -0,0 +1,23 @@
# Security Policy
## Supported Versions
Security fixes are provided for the latest `main` branch.
## Reporting a Vulnerability
Please report vulnerabilities privately by opening a GitHub Security Advisory in this repository.
Include:
- Affected component or file path
- Reproduction steps / proof of concept
- Impact assessment
- Suggested remediation (if available)
Do not disclose details publicly until a fix is available.
## Response Targets
- Initial triage: within 72 hours
- Mitigation plan: within 7 days for confirmed issues
+9 -1
View File
@@ -8,6 +8,8 @@ description: 剪映 (JianYing) AI自动化剪辑的高级封装 API (JyWrapper)
Use this skill when the user wants to automate video editing, generate drafts, or manipulate media assets in JianYing Pro.
Agent execution playbook: [docs/agent-playbook.md](docs/agent-playbook.md)
Minimal command SOP: [docs/minimal-command-sop.md](docs/minimal-command-sop.md)
For generic editing requests, always follow the "Quick Edit Runtime Template" and "Acceptance Checklist" in that playbook.
## 🚨 重要开发原则 (CRITICAL DEVELOPER RULES)
1. **脚本位置****禁止在 Skill 内部目录创建剪辑脚本**。所有的剪辑逻辑实现代码(`.py` 脚本)必须存放在用户当前项目的**根目录**(或子目录,如 `scripts/`),以保持 Skill 库的纯净和可移植性。
@@ -98,13 +100,19 @@ import sys
# 1. 环境初始化 (必须同步到脚本开头)
current_dir = os.path.dirname(os.path.abspath(__file__))
env_root = os.getenv("JY_SKILL_ROOT", "").strip()
# 探测 Skill 路径 (支持 Antigravity, Trae, Claude 等)
skill_root = next((p for p in [
env_root,
os.path.join(current_dir, ".agent", "skills", "jianying-editor"),
os.path.join(current_dir, ".trae", "skills", "jianying-editor"),
os.path.join(current_dir, ".claude", "skills", "jianying-editor"),
os.path.join(current_dir, "skills", "jianying-editor"),
os.path.abspath(".agent/skills/jianying-editor"),
os.path.abspath(".trae/skills/jianying-editor"),
os.path.abspath(".claude/skills/jianying-editor"),
os.path.dirname(current_dir) # 如果在 examples/ 目录下
] if os.path.exists(os.path.join(p, "scripts", "jy_wrapper.py"))), None)
] if p and os.path.exists(os.path.join(p, "scripts", "jy_wrapper.py"))), None)
if not skill_root: raise ImportError("Could not find jianying-editor skill root.")
sys.path.insert(0, os.path.join(skill_root, "scripts"))
+25
View File
@@ -2,6 +2,31 @@
Task routing matrix for reliable execution.
## 0) Quick Edit Runtime Template (Default for "来个剪辑")
Use this fixed sequence:
1. Environment check (minimal):
- verify Python works
- verify draft root exists
2. Asset resolution:
- use explicit local/cloud assets
- only call `asset_search.py` when user asks for style/effect lookup
3. Script assembly:
- generate one runnable script with deterministic APIs
4. Execute once:
- run script and capture success/failure output
5. Acceptance check:
- validate draft and track structure (see checklist below)
### Acceptance Checklist (Mandatory)
- Draft folder exists under JianYing drafts root
- `project.save()` completed successfully
- At least one `video` track segment exists
- If BGM exists, it must be on `audio` track (not `video`)
- If narration exists, subtitle segments should exist and be time-aligned
## 1) Cloud Video + BGM Draft
- Read: `rules/setup.md`, `rules/media.md`, `rules/audio-voice.md`
+2 -1
View File
@@ -9,7 +9,8 @@ Bootstrap:
```python
import os
import sys
sys.path.insert(0, r"<SKILL_ROOT>\\scripts")
skill_root = os.getenv("JY_SKILL_ROOT", r"<SKILL_ROOT>")
sys.path.insert(0, os.path.join(skill_root, "scripts"))
from jy_wrapper import JyProject
```
+66
View File
@@ -0,0 +1,66 @@
# Minimal Command SOP
Use this SOP for simple editing requests to minimize noisy exploration.
## Goal
- Produce one runnable script
- Run once
- Validate result with fixed checks
## Step 1: Minimal Environment Check (2 commands max)
```powershell
python --version
Test-Path "C:\Users\Administrator\AppData\Local\JianyingPro\User Data\Projects\com.lveditor.draft"
```
If draft path differs, set `JY_PROJECTS_ROOT` and continue.
## Step 2: Asset Check (1 command)
```powershell
Get-ChildItem .agent\skills\jianying-editor\assets -File
```
Do not recursively scan the whole workspace unless asset lookup fails.
## Step 3: Script Generation
- Create exactly one script in workspace root, e.g. `simple_edit.py`.
- Use deterministic APIs only:
- `JyProject(...)`
- `add_media_safe(...)`
- `add_cloud_media(...)` or `add_cloud_music(...)`
- `add_text_simple(...)` / `add_narrated_subtitles(...)`
- `save()`
## Step 4: Single Execution
```powershell
python simple_edit.py
```
If failed, patch script once based on the concrete error, then rerun once.
## Step 5: Acceptance Validation (Mandatory)
Verify all:
- Draft directory exists
- Save success log appears
- At least one video segment exists
- BGM is on audio track (if used)
- Subtitles exist when narration exists
## Error Handling Rules
- `SegmentOverlap`: move clip start to track end or separate track.
- Missing asset: switch to known local asset or valid cloud ID.
- Import/path failure: set `JY_SKILL_ROOT`, rerun bootstrap.
## Anti-Patterns (Avoid)
- Repeated `Get-ChildItem -Recurse` over full workspace
- Temporary introspection files (`*_dir.txt`, `output.txt`) unless debugging is explicitly requested
- Multiple style/effect searches before first successful draft generation
+43 -70
View File
@@ -1,91 +1,64 @@
import os
import sys
# Standard Boilerplate to load the skill wrapper
# -------------------------------------------------------------
# 必选:这段代码负责自动定位 Skill 路径,请原样复制到你的脚本开头
current_dir = os.path.dirname(os.path.abspath(__file__))
# 扩展探测逻辑:覆盖 Antigravity(.agent), Trae(.trae), Claude(.claude), 以及通用(skills)
skill_candidates = [
os.path.join(current_dir, ".agent", "skills", "jianying-editor"),
os.path.join(current_dir, ".trae", "skills", "jianying-editor"),
os.path.join(current_dir, ".claude", "skills", "jianying-editor"),
os.path.join(current_dir, "skills", "jianying-editor"),
os.path.join(current_dir, "jianying-editor-skill", ".agent", "skills", "jianying-editor"),
os.path.abspath(".agent/skills/jianying-editor"),
# Special case for examples folder (up one level)
os.path.join(os.path.dirname(current_dir))
]
wrapper_path = None
for p in skill_candidates:
if os.path.exists(os.path.join(p, "scripts", "jy_wrapper.py")):
wrapper_path = os.path.join(p, "scripts")
break
if wrapper_path and wrapper_path not in sys.path:
sys.path.insert(0, wrapper_path)
def resolve_wrapper_path() -> str:
current_dir = os.path.dirname(os.path.abspath(__file__))
env_root = os.getenv("JY_SKILL_ROOT", "").strip()
candidates = [
env_root,
os.path.join(current_dir, ".agent", "skills", "jianying-editor"),
os.path.join(current_dir, ".trae", "skills", "jianying-editor"),
os.path.join(current_dir, ".claude", "skills", "jianying-editor"),
os.path.join(current_dir, "skills", "jianying-editor"),
os.path.abspath(".agent/skills/jianying-editor"),
os.path.abspath(".trae/skills/jianying-editor"),
os.path.abspath(".claude/skills/jianying-editor"),
os.path.dirname(current_dir), # examples/ under skill root
]
for p in candidates:
if not p:
continue
scripts_dir = os.path.join(os.path.abspath(p), "scripts")
if os.path.exists(os.path.join(scripts_dir, "jy_wrapper.py")):
return scripts_dir
raise ImportError("Could not find jianying-editor/scripts/jy_wrapper.py")
try:
from jy_wrapper import JyProject
print(f"✅ Successfully loaded JyProject from: {wrapper_path}") # Added print for success
except ImportError:
# 这一步是为了在找不到路径时给出明确提示,方便调试
print("❌ Critical Error: Could not load 'jy_wrapper'. Check skill paths.")
sys.exit(1)
# -------------------------------------------------------------
# ==============================================================================
# 🎬 简单剪辑示例 (Simple Clip Demo)
# ==============================================================================
WRAPPER_PATH = resolve_wrapper_path()
if WRAPPER_PATH not in sys.path:
sys.path.insert(0, WRAPPER_PATH)
def main():
# 1. 初始化项目
# project_name: 剪映草稿的名字
# overwrite=True: 如果项目已存在,允许覆盖(谨慎使用)
from jy_wrapper import JyProject
def main() -> None:
project = JyProject(project_name="Hello_JianYing_V3", overwrite=True)
# 2. 准备素材路径 (这里使用 Skill 自带的测试素材)
# wrapper_path是指向 scripts 目录的,它的上一级通常是 skill root
if 'wrapper_path' in globals() and wrapper_path:
skill_root = os.path.dirname(wrapper_path)
else:
# Fallback if wrapper_path somehow isn't set (shouldn't happen with new boilerplate)
skill_root = os.path.abspath(os.path.join(current_dir, ".."))
skill_root = os.path.dirname(WRAPPER_PATH)
assets_dir = os.path.join(skill_root, "assets")
video_path = os.path.join(assets_dir, "video.mp4")
bgm_path = os.path.join(assets_dir, "audio.mp3")
if not os.path.exists(video_path):
print(f"⚠️ Demo assets not found at {assets_dir}, using placeholders.")
# 如果你运行此脚本时没有这些文件,请替换为你本地的真实路径
if not os.path.exists(video_path) or not os.path.exists(bgm_path):
print(f"Demo assets not found: {assets_dir}")
return
# 3. 添加主视频轨道
# add_media_safe 会自动识别文件类型
print("📥 Importing Video...")
project.add_media_safe(video_path, start_time=0, duration="5s")
print("Importing video...")
project.add_media_safe(video_path, start_time="0s", duration="5s", track_name="VideoTrack")
# 4. 添加背景音乐
# track_name="Audio": 指定放入音频轨道
print("🎵 Adding Music...")
project.add_media_safe(bgm_path, start_time=0, duration="5s", track_name="Audio")
print("Adding bgm...")
project.add_media_safe(bgm_path, start_time="0s", duration="5s", track_name="AudioTrack")
# 5. 添加字幕 (带入场动画)
# transform_y: 垂直位置,-1.0 是底部1.0 是顶部0 是中间
# anim_in: 入场动画。支持直接使用中文名称 (如 "复古打字机", "弹入", "向右滑动")
# 提示: 你可以使用 `python scripts/asset_search.py "打字"` 来查找可用的动画名
print("📝 Adding Text with Animation...")
project.add_text_simple("Hello JianYing API!", start_time="1s", duration="3s",
transform_y=-0.7, color_rgb=(1, 1, 0), # 黄色字幕
anim_in="复古打字机")
print("Adding text...")
# Keep text clips non-overlapping on the same text track to avoid SegmentOverlap.
project.add_text_simple("Hello JianYing API!", start_time="1s", duration="1.6s", anim_in="复古打字机")
project.add_text_simple("Simple Clip Demo", start_time="2.7s", duration="1.6s", anim_in="向右滑动")
# 6. 保存项目
# 这会生成草稿文件并自动刷新剪映首页列表
print("💾 Saving Project...")
print("Saving project...")
project.save()
print("\n✨ Done! Open JianYing (剪映) and look for 'Hello_JianYing_V3'.")
print("Done. Open JianYing and find draft: Hello_JianYing_V3")
if __name__ == "__main__":
main()
+20
View File
@@ -67,3 +67,23 @@ python <SKILL_ROOT>/scripts/auto_exporter.py "ProjectName" "custom_output.mp4" -
- **Draft Recognition**: The wrapper automatically handles `DraftFolder` structure. Do not manually manipulate `draft_content.json` unless you know exactly what you are doing.
- **Exporting Requirements**: Auto-exporting only works on **Windows** with **Jianying v5.9 or lower**. It relies on `uiautomation` to interact with the UI.
- **UI Refresh**: After the script runs, if Jianying is open, the user may need to exit and re-enter the draft to see changes.
## Quick Edit Execution Template (Standard)
For generic requests like "来个剪辑", execute in this order:
1. Minimal environment checks (python + drafts root)
2. Resolve required assets (local first, cloud second)
3. Generate one deterministic edit script
4. Run script once and collect output
5. Perform acceptance checks and report concrete results
## Acceptance Checks (Standard)
After execution, verify:
- Draft directory exists
- Save completed (`project.save()` success)
- At least one segment exists on a video track
- BGM (if used) is on audio track
- Narration/subtitle pairing exists when TTS was requested
+13 -1
View File
@@ -8,6 +8,7 @@ metadata:
# Environment Initialization
Use a single reliable bootstrap path. Do not assume helper files outside this repo.
Always support multiple editor layouts and allow explicit override.
## Recommended Bootstrap (Mandatory)
@@ -16,7 +17,9 @@ import os
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
env_root = os.getenv("JY_SKILL_ROOT", "").strip()
skill_candidates = [
env_root,
os.path.join(current_dir, ".agent", "skills", "jianying-editor"),
os.path.join(current_dir, ".trae", "skills", "jianying-editor"),
os.path.join(current_dir, ".claude", "skills", "jianying-editor"),
@@ -26,13 +29,21 @@ skill_candidates = [
]
scripts_path = None
attempted = []
for p in skill_candidates:
if not p:
continue
p = os.path.abspath(p)
attempted.append(p)
if os.path.exists(os.path.join(p, "scripts", "jy_wrapper.py")):
scripts_path = os.path.join(p, "scripts")
break
if not scripts_path:
raise ImportError("Could not find jianying-editor/scripts/jy_wrapper.py")
raise ImportError(
"Could not find jianying-editor/scripts/jy_wrapper.py\\nTried:\\n- "
+ "\\n- ".join(attempted)
)
if scripts_path not in sys.path:
sys.path.insert(0, scripts_path)
@@ -43,5 +54,6 @@ from jy_wrapper import JyProject
## Notes
- Do not use `from jy import JyProject` unless your project explicitly provides `jy.py`.
- Prefer setting `JY_SKILL_ROOT` in mixed editor environments.
- Put business scripts in the user workspace, not inside the skill repo.
- Always call `project.save()` at the end.
+2 -21
View File
@@ -1,5 +1,6 @@
import os
import sys
from utils.skill_path import resolve_skill_root
def setup_env():
"""
@@ -16,27 +17,7 @@ def setup_env():
except Exception:
start_dir = os.getcwd()
skill_root = None
possible_roots = [
start_dir,
os.path.join(start_dir, ".."),
os.path.join(start_dir, "..", ".."),
os.path.join(start_dir, ".agent", "skills", "jianying-editor"),
os.path.join(os.getcwd(), ".agent", "skills", "jianying-editor"),
os.path.join(os.getcwd(), "skills", "jianying-editor"),
]
for p in possible_roots:
p = os.path.abspath(p)
if os.path.exists(os.path.join(p, "scripts", "jy_wrapper.py")):
skill_root = p
break
if not skill_root:
fallback_root = os.path.join(os.getcwd(), ".agent", "skills", "jianying-editor")
if os.path.exists(fallback_root):
skill_root = fallback_root
skill_root, _ = resolve_skill_root(start_dir)
if skill_root:
scripts_dir = os.path.join(skill_root, "scripts")
+62
View File
@@ -0,0 +1,62 @@
import os
from typing import List, Optional, Tuple
def _build_candidates(start_dir: str) -> List[str]:
"""
Build candidate skill roots for different editors/layouts.
"""
cwd = os.getcwd()
parents = [start_dir, os.path.join(start_dir, ".."), os.path.join(start_dir, "..", "..")]
editor_paths = [
os.path.join(start_dir, ".agent", "skills", "jianying-editor"),
os.path.join(start_dir, ".trae", "skills", "jianying-editor"),
os.path.join(start_dir, ".claude", "skills", "jianying-editor"),
os.path.join(start_dir, "skills", "jianying-editor"),
os.path.join(cwd, ".agent", "skills", "jianying-editor"),
os.path.join(cwd, ".trae", "skills", "jianying-editor"),
os.path.join(cwd, ".claude", "skills", "jianying-editor"),
os.path.join(cwd, "skills", "jianying-editor"),
]
return parents + editor_paths
def resolve_skill_root(start_dir: str) -> Tuple[Optional[str], List[str]]:
"""
Resolve skill root by environment variable first, then candidate probing.
Returns (resolved_root, attempted_paths).
"""
attempted: List[str] = []
env_root = os.getenv("JY_SKILL_ROOT", "").strip()
if env_root:
env_root = os.path.abspath(env_root)
attempted.append(env_root)
if os.path.exists(os.path.join(env_root, "scripts", "jy_wrapper.py")):
return env_root, attempted
for p in _build_candidates(start_dir):
ap = os.path.abspath(p)
attempted.append(ap)
if os.path.exists(os.path.join(ap, "scripts", "jy_wrapper.py")):
return ap, attempted
return None, attempted
def ensure_skill_scripts_on_path(start_dir: str) -> str:
"""
Resolve skill root and prepend its scripts path to sys.path.
Raises ImportError with tried paths when not found.
"""
import sys
root, attempted = resolve_skill_root(start_dir)
if not root:
msg = "Could not find jianying-editor skill root.\nTried:\n- " + "\n- ".join(attempted)
raise ImportError(msg)
scripts_dir = os.path.join(root, "scripts")
if scripts_dir not in sys.path:
sys.path.insert(0, scripts_dir)
return root