feat: add guizang-ppt market skill (vendored from op7418, AGPL-3.0) (#20)

## 概述 / Summary

新增市场技能 **`guizang-ppt`**(归藏网页 PPT),vendored 自上游开源项目
[op7418/guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill)。

Add a new **market skill** `guizang-ppt` — generates single-file HTML
horizontal-swipe slide decks (web PPT) in two visual systems (editorial
"magazine × e-ink" / "Swiss International"). Vendored from the upstream
open-source project.

## 变更内容 / Changes

- `skills/guizang-ppt/`:SKILL.md(DesireCore frontmatter 覆盖层 +
上游正文)、`references/`(10)、`assets/`(2 模板 + motion.min.js + 9 张
webp)、`scripts/validate-swiss-deck.mjs`、`LICENSE`(AGPL-3.0)、`NOTICE.md`(署名与合规)
- `skills/guizang-ppt/_desirecore/`:DesireCore 维护态(不随上游覆盖)
- `frontmatter.yaml`:市场 frontmatter 覆盖层(i18n: zh-CN 源 + en-US 显示串,body
暂回退中文,留给 CI 翻译)
  - `upstream.json`:上游溯源(commit `014c572`、AGPL-3.0、作者 歸藏/op7418)
- `scripts/vendor/guizang-ppt.mjs`:可复用的 vendor 更新脚本(`--src <本地路径>` 或
`--ref <tag>`)
- `manifest.json`:`version` → `1.2.3`,`stats.totalSkills` → 25

## 定位 / Positioning

- **仅市场可选安装**:未加入 `builtin-skills.json`,不随客户端开机自动安装;用户在市场按需安装。
- 分类 `creative`;与已有 `pptx`(生成 .pptx 文件)区分:本技能生成 **HTML deck**。

## 许可与署名 / License & Attribution

上游为 **AGPL-3.0**。本技能保留原 `LICENSE` 与作者署名(`NOTICE.md` + `metadata.author`
+ `market.maintainer`),作为聚合内容分发。

## 校验 / Validation

- `python3 scripts/i18n/validate-i18n.py skills/guizang-ppt` → `OK: no
i18n issues found.`

## 手动更新流程 / Manual update

上游发版时:`node scripts/vendor/guizang-ppt.mjs --ref <tag>` → 核对 diff → 必要时
bump `_desirecore/frontmatter.yaml#version` 与 `manifest.json` → 提交。

---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
2026-06-04 11:04:05 +08:00
committed by GitHub
parent a582f0f4f9
commit b15fce19bf
30 changed files with 8379 additions and 1 deletions

146
scripts/vendor/guizang-ppt.mjs vendored Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env node
/**
* Vendor 更新器:把上游 op7418/guizang-ppt-skill 同步进官方市场的 skills/guizang-ppt/。
*
* 设计目标(“手动可更新”):
* - 上游内容references/ assets/ scripts/ LICENSE + SKILL.md 正文)是“被 vendored”的可重复覆盖
* - DesireCore 自己维护的元数据放在 skills/guizang-ppt/_desirecore/frontmatter.yaml 覆盖层 + NOTICE.md
* vendor 时不被上游覆盖;
* - 每次同步把上游 commit/版本写进 _desirecore/upstream.json保证可追溯。
*
* 用法:
* node scripts/vendor/guizang-ppt.mjs --src /path/to/local/guizang-ppt-skill # 复用本地 clone
* node scripts/vendor/guizang-ppt.mjs --ref v1.2.0 # 联网 clone 指定 ref
* node scripts/vendor/guizang-ppt.mjs # 联网 clone 默认分支
*
* 同步后仍需人工:核对 diff、必要时 bump _desirecore/frontmatter.yaml#version 与 metadata.updated_at、
* 更新根 manifest.json再 commit / push。
*/
import { execSync } from 'node:child_process'
import {
existsSync,
readFileSync,
writeFileSync,
rmSync,
cpSync,
mkdtempSync,
} from 'node:fs'
import { join, dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { tmpdir } from 'node:os'
const SKILL_ID = 'guizang-ppt'
const UPSTREAM_URL = 'https://github.com/op7418/guizang-ppt-skill.git'
const DEFAULT_BRANCH = 'main'
// 从上游仓库 vendor 进来的顶层条目(会被每次同步覆盖刷新)
const VENDORED_ENTRIES = ['references', 'assets', 'scripts', 'LICENSE']
const __dir = dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = resolve(__dir, '..', '..') // scripts/vendor -> 仓库根
const SKILL_DIR = join(REPO_ROOT, 'skills', SKILL_ID)
const DESIRE_DIR = join(SKILL_DIR, '_desirecore')
const FRONTMATTER_FILE = join(DESIRE_DIR, 'frontmatter.yaml')
function parseArgs(argv) {
const out = { src: null, ref: DEFAULT_BRANCH }
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--src') out.src = argv[++i]
else if (argv[i] === '--ref') out.ref = argv[++i]
}
return out
}
function git(cmd, cwd) {
return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
}
/** 取得上游源目录与 commit/ref本地 --src 优先,否则浅克隆。返回 { srcDir, commit, ref, cleanup } */
function resolveSource(args) {
if (args.src) {
const srcDir = resolve(args.src)
if (!existsSync(join(srcDir, 'SKILL.md'))) {
throw new Error(`--src 指向的目录缺少 SKILL.md${srcDir}`)
}
let commit = 'unknown'
try {
commit = git('rev-parse HEAD', srcDir)
} catch {
/* 非 git 目录则忽略 */
}
return { srcDir, commit, ref: args.ref, cleanup: () => {} }
}
const tmp = mkdtempSync(join(tmpdir(), 'guizang-vendor-'))
console.log(`→ 克隆上游 ${UPSTREAM_URL} (ref=${args.ref}) ...`)
execSync(`git clone --depth 1 --branch ${args.ref} ${UPSTREAM_URL} ${tmp}`, {
stdio: ['pipe', 'pipe', 'pipe'],
})
const commit = git('rev-parse HEAD', tmp)
return { srcDir: tmp, commit, ref: args.ref, cleanup: () => rmSync(tmp, { recursive: true, force: true }) }
}
/** 去掉上游 SKILL.md 的 frontmatter返回正文 */
function stripFrontmatter(text) {
const lines = text.split('\n')
if (lines[0].trim() !== '---') return text.trim() + '\n'
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === '---') {
return lines.slice(i + 1).join('\n').replace(/^\n+/, '')
}
}
return text.trim() + '\n'
}
function main() {
const args = parseArgs(process.argv.slice(2))
if (!existsSync(FRONTMATTER_FILE)) {
throw new Error(`缺少覆盖层 frontmatter${FRONTMATTER_FILE}`)
}
const frontmatter = readFileSync(FRONTMATTER_FILE, 'utf-8').trimEnd()
const { srcDir, commit, ref, cleanup } = resolveSource(args)
try {
// 1. 清理旧的 vendored 内容(保留 _desirecore/ 与 NOTICE.md 等维护态)
for (const entry of [...VENDORED_ENTRIES, 'SKILL.md']) {
rmSync(join(SKILL_DIR, entry), { recursive: true, force: true })
}
// 2. 复制上游 vendored 条目
for (const entry of VENDORED_ENTRIES) {
const from = join(srcDir, entry)
if (existsSync(from)) {
cpSync(from, join(SKILL_DIR, entry), { recursive: true })
} else {
console.warn(` ⚠ 上游缺少 ${entry},跳过`)
}
}
// 3. 生成 SKILL.md = 覆盖层 frontmatter + 上游正文
const upstreamSkill = readFileSync(join(srcDir, 'SKILL.md'), 'utf-8')
const body = stripFrontmatter(upstreamSkill)
writeFileSync(join(SKILL_DIR, 'SKILL.md'), `${frontmatter}\n\n${body}`)
// 4. 写溯源信息
const upstream = {
skillId: SKILL_ID,
repoUrl: UPSTREAM_URL,
branch: DEFAULT_BRANCH,
ref,
commit,
license: 'AGPL-3.0',
author: '歸藏 (op7418)',
vendoredAt: new Date().toISOString(),
}
writeFileSync(join(DESIRE_DIR, 'upstream.json'), `${JSON.stringify(upstream, null, 2)}\n`)
console.log('✓ vendor 完成')
console.log(` upstream commit: ${commit}`)
console.log(` skill dir: skills/${SKILL_ID}/`)
console.log(' 后续:核对 diff → 必要时 bump _desirecore/frontmatter.yaml#version 与根 manifest.json → commit')
} finally {
cleanup()
}
}
main()