feat: add Chinese office productivity plugins

This commit is contained in:
CJYSEX
2026-07-23 12:15:04 +08:00
parent 9a24a2125f
commit 1b674e31da
24 changed files with 1001 additions and 2 deletions

View File

@@ -0,0 +1,40 @@
---
name: build-presentations
description: 创建、读取、编辑和验证 PPTX/PowerPoint 演示文稿。用户要求制作幻灯片、改写已有 PPT、沿用模板、提取演示内容、检查文字溢出或逐页优化叙事时使用。
---
# CJYSA 演示文稿
## 先规划再制作
1. 明确受众、目的、演讲时长、页数和视觉风格。
2. 先写一行式故事线:背景 → 问题/机会 → 证据 → 方案 → 行动。
3. 每页只承担一个主要任务;标题应表达结论,而不是只写主题名称。
4. 有模板时以模板为唯一视觉基准;没有模板时使用统一的字体、色板、网格和留白。
## 实现流程
1. 调用工作区依赖加载器。优先使用 `@oai/artifact-tool`;不可用时使用其 Python 和 `python-pptx`
2. 先检查输入演示:
```powershell
& "<bundled-python>" "<skill-dir>\scripts\pptx_tool.py" inspect "<input.pptx>"
```
3. 创建或编辑 PPTX保留原文件并输出新副本。
4. 运行 `validate`,检查空白页、超出画布的对象、过小字号和疑似标题换行。
5. 使用 artifact-tool、PowerPoint、WPS 或可靠转换器渲染全部幻灯片;逐页全尺寸检查,不把缩略图拼图当作最终验证。
6. 修复重叠、裁切、断行、低对比度、空占位符和数据不一致后再交付。
## 设计底线
- 无模板时,标题页标题通常不小于 40pt普通页标题不小于 28pt正文通常不小于 16pt。
- 内容过多时先删减或拆页,不优先缩小字体。
- 图表必须标注单位、时间范围和来源;不要用装饰图替代证据。
- 不复制 OpenAI 官方 Presentations 插件的模板、脚本、素材或内部文档。
- 图片生成/搜索能力不可用时,使用用户素材、可授权本地素材或清晰的占位说明,不伪造已生成图片。
- 交付前按 [quality-checks.md](references/quality-checks.md) 检查。
## 输出
最终只链接用户要求的 PPTX不主动附带 builder、逐页 PNG、拼图或检查日志。

View File

@@ -0,0 +1,7 @@
interface:
display_name: "CJYSA 演示文稿"
short_description: "创建、编辑、检查并验证专业 PowerPoint 演示文稿"
default_prompt: "Use $build-presentations to create or edit a clear PowerPoint deck."
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,9 @@
# PPTX 交付检查
- 故事线完整,每页有明确结论或作用。
- 无空白页、未替换占位符、重复标题或内部制作说明。
- 所有对象位于画布内,无非预期重叠和裁切。
- 标题不意外换行,正文最小字号仍可阅读。
- 图表数据、单位、时间范围和来源一致。
- 逐页全尺寸检查;拼图只用于整体节奏检查。
- 若无法渲染,明确说明只通过结构和边界检查。

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Original PPTX helper for structural inspection and common QA warnings."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.util import Pt
def shape_text(shape) -> str:
if not getattr(shape, "has_text_frame", False):
return ""
return "\n".join(paragraph.text for paragraph in shape.text_frame.paragraphs).strip()
def inspect(path: Path) -> None:
deck = Presentation(path)
slides = []
for index, slide in enumerate(deck.slides, start=1):
texts = [shape_text(shape) for shape in slide.shapes]
nonempty_texts = [text for text in texts if text]
slides.append(
{
"slide": index,
"shapes": len(slide.shapes),
"text_shapes": len(nonempty_texts),
"images": sum(1 for shape in slide.shapes if shape.shape_type == MSO_SHAPE_TYPE.PICTURE),
"title": (slide.shapes.title.text.strip() if slide.shapes.title and slide.shapes.title.text else None),
"text_preview": " | ".join(nonempty_texts)[:300],
}
)
report = {
"file": str(path.resolve()),
"slides": len(deck.slides),
"width_inches": round(deck.slide_width / 914400, 3),
"height_inches": round(deck.slide_height / 914400, 3),
"slide_details": slides,
}
print(json.dumps(report, ensure_ascii=False, indent=2))
def validate(path: Path, minimum_font_pt: float) -> None:
deck = Presentation(path)
warnings = []
for slide_index, slide in enumerate(deck.slides, start=1):
meaningful = 0
for shape_index, shape in enumerate(slide.shapes, start=1):
text = shape_text(shape)
if text or shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
meaningful += 1
if shape.left < 0 or shape.top < 0 or shape.left + shape.width > deck.slide_width or shape.top + shape.height > deck.slide_height:
warnings.append({"slide": slide_index, "shape": shape_index, "kind": "outside_slide"})
if getattr(shape, "has_text_frame", False):
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
if run.font.size and run.font.size < Pt(minimum_font_pt):
warnings.append(
{
"slide": slide_index,
"shape": shape_index,
"kind": "small_font",
"font_pt": round(run.font.size.pt, 1),
"text": run.text[:80],
}
)
if meaningful == 0:
warnings.append({"slide": slide_index, "kind": "blank_slide"})
title = slide.shapes.title
if title and title.has_text_frame and "\n" in title.text.strip():
warnings.append({"slide": slide_index, "kind": "multiline_title", "text": title.text.strip()[:160]})
print(json.dumps({"file": str(path.resolve()), "warnings": warnings, "count": len(warnings)}, ensure_ascii=False, indent=2))
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
inspect_cmd = sub.add_parser("inspect")
inspect_cmd.add_argument("input", type=Path)
validate_cmd = sub.add_parser("validate")
validate_cmd.add_argument("input", type=Path)
validate_cmd.add_argument("--minimum-font-pt", type=float, default=14.0)
return parser
def main() -> None:
args = build_parser().parse_args()
if args.command == "inspect":
inspect(args.input)
elif args.command == "validate":
validate(args.input, args.minimum_font_pt)
if __name__ == "__main__":
main()