chore: restructure skills repo with new agents and skill bundles

- Add new skills: deep-dive, docs-rag, meta-creator, ppt-maker, sdlc
- Add agent configs: g-assistent, meta-creator, sdlc with prompt files
- Add reference docs for custom agents and skills specification
- Add utility scripts: install-agents.sh, orchestrate.py, puml2svg.sh
- Update README and commit-message skill config
- Remove deprecated skills: codereview, python, testing, typescript
- Add .gitignore
This commit is contained in:
Team
2026-04-18 13:07:46 +08:00
parent 72f16d26b8
commit c0d14c6ac1
74 changed files with 5726 additions and 324 deletions
+75
View File
@@ -0,0 +1,75 @@
# ppt-maker
一键将 Markdown 转换为专业 PPT,支持自动图表生成和多种主题。
## Architecture
![Architecture](assets/ppt-maker-architecture.svg)
## Workflow
![Workflow](assets/ppt-maker-workflow.svg)
## 功能特性
- Markdown 语法驱动幻灯片布局(封面页、内容页、结束页)
- 表格数据**自动转为图表**(饼图 / 柱状图 / 折线图)
- 6 种内置主题:ocean、sunset、purple、luxury、midnight、classic
- 支持有序/无序列表、引用块、代码块、表格
## 快速开始
```bash
node skills/ppt-maker/scripts/ppt-maker.js -i input.md -o output.pptx -t ocean
```
## 命令参数
| 参数 | 说明 | 必填 |
|------|------|------|
| `-i` | 输入 Markdown 文件 | ✅ |
| `-o` | 输出 PPTX 文件 | ✅ |
| `-t` | 主题名称(默认 ocean) | ❌ |
| `-l` | 列出所有可用主题 | ❌ |
## Markdown 页面结构
```markdown
# 封面标题 → 封面页
副标题文字
## 章节标题 → 内容页
## 感谢聆听 → 结束页(自动识别)
```
## 图表自动生成
在标题中包含关键字,其下方表格自动转为对应图表:
| 图表类型 | 触发关键字示例 |
|----------|--------------|
| 🥧 饼图 | 占比、比例、饼图、pie |
| 📊 柱状图 | 对比、排名、销售额、bar |
| 📈 折线图 | 趋势、增长、月度、line |
## File Structure
```
skills/ppt-maker/
├── SKILL.md
├── README.md # this file
├── _meta.json
├── assets/
│ ├── workflow.puml
│ └── ppt-maker-workflow.svg
└── scripts/
├── ppt-maker.js # 主脚本
└── package.json # 依赖 pptxgenjs
```
## 示例提示词
```
请使用 ppt-maker 技能,为我生成一份"2026年度销售总结"的汇报型幻灯片,
包含销售占比饼图、各产品销售额柱状图、月度趋势折线图,主题使用 midnight。
```
+286
View File
@@ -0,0 +1,286 @@
---
name: ppt-maker
description: "专业级PPT一键生成。Markdown创建幻灯片,支持自动图表(饼图/柱状图/折线图)、多主题、有序/无序列表、引用块、代码块、表格、感谢页自动识别。"
---
# PPT Maker - 专业级PPT生成工具
使用 Markdown 自动创建精美 PPT,**表格数据自动转为图表**,支持多种主题和智能布局。
## 快速开始
```bash
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i input.md -o output.pptx -t ocean
```
## 命令参数
| 参数 | 说明 | 必填 |
|------|------|------|
| `-i, --input` | 输入 Markdown 文件路径 | ✅ |
| `-o, --output` | 输出 PPTX 文件路径(自动补 .pptx 后缀) | ✅ |
| `-t, --theme` | 主题名称,默认 ocean | ❌ |
| `-l, --list` | 列出所有可用主题 | ❌ |
| `-h, --help` | 显示帮助信息 | ❌ |
## 支持的主题
| 主题 | 风格 | 适用场景 |
|------|------|----------|
| ocean | 蓝色海洋 | 科技/专业 |
| sunset | 橙红日落 | 温暖/创意 |
| purple | 紫罗兰 | 创意/设计 |
| luxury | 黑金奢华 | 高端/奢侈 |
| midnight | 深夜暗色 | 演示/震撼 |
| classic | 经典绿 | 商务/正式 |
## Markdown 语法对照
### 页面类型
```markdown
# 大标题 → 封面页(第一张幻灯片)
副标题文字 → 封面副标题
## 章节标题 → 内容页
## 感谢聆听 → 结束页(自动居中大字布局)
```
**结束页自动识别关键字:** 感谢、谢谢、thank、thanks、Q&A、问答、结束、The End、再见、联系方式
### 内容元素
```markdown
### 小标题 → 页内加粗小标题
- 无序列表项1 → 带圆点的无序列表
- 无序列表项2
1. 有序列表项1 → 带编号圆圈的有序列表
2. 有序列表项2
> 引用文字 → 带左侧竖条的引用块
普通文字 → 正文段落
```
### 表格
```markdown
| 列1 | 列2 | 列3 |
|-----|-----|-----|
| 内容 | 内容 | 内容 |
```
表格含数值列时**自动检测**是否转为图表(见下方图表规则)。不含数值或未匹配图表规则时,保持表格原样显示(含交替行底色)。
### 代码块
````markdown
```python
print("Hello World")
```
````
深色背景 + 等宽字体 + 圆角边框显示。
## ⭐ 自动图表生成
**核心功能:** 在 `##` 标题、`###` 小标题或表格前的正文中包含特定关键字,其下方的表格自动转为对应图表。
### 图表类型与关键字
| 图表类型 | 触发关键字 |
|----------|-----------|
| 🥧 饼图 | 饼图、饼状图、占比、比例、份额、构成、组成、百分比、比重、pie |
| 📊 柱状图 | 柱状、柱状图、柱形、排名、top、对比、比较、分布、销售额、金额、数量、业绩、产量、营收、bar |
| 📈 折线图 | 折线、折线图、趋势、增长、变化、走势、曲线、时间、月度、季度、年度、line、trend |
### 智能推断(无关键字时)
- 数值列总和在 80~120 之间 → 自动识别为**饼图**(占比数据)
- 有 ≥2 个数值点 → 默认生成**柱状图**
- 支持多列数值 → 自动生成**多系列**图表
### 数值解析
自动清理单元格中的干扰字符,以下写法均可正确识别:
- `100万` `¥250` `$1,200` `30%` `85元` `1200亿`
### 图表示例
#### 饼图示例
```markdown
## 销售占比分析
### 各产品销售占比饼图
| 产品 | 占比(%) |
|------|---------|
| 大米 | 30 |
| 高粱 | 50 |
| 小麦 | 20 |
```
#### 柱状图示例
```markdown
## 各产品销售额对比
### 年度销售额柱状图
| 产品 | 销售额(万元) |
|------|-------------|
| 大米 | 100 |
| 高粱 | 250 |
| 小麦 | 130 |
```
#### 折线图示例
```markdown
## 月度销售趋势
### 销售额变化趋势折线图
| 月份 | 销售额(万元) |
|------|-------------|
| 1月 | 35 |
| 2月 | 42 |
| 3月 | 58 |
| 4月 | 72 |
```
#### 不转图表的表格(无数值列或非图表场景)
```markdown
## 工作计划
| 季度 | 目标 | 负责人 |
|------|------|--------|
| Q1 | 完成招聘 | 张经理 |
| Q2 | 市场拓展 | 李经理 |
```
## 使用示例
提供markdown文件,比如input.md,然后通过指令生成
```bash
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i input.md -o output.pptx -t ocean
```
### markdown提示词示例
```markdown
# 2026年度总结报告
北灵聊AI · 年度工作汇报
## 销售占比分析
### 各产品占比饼图
| 产品 | 占比(%) |
|------|---------|
| 大米 | 30 |
| 高粱 | 40 |
| 小麦 | 20 |
| 玉米 | 10 |
## 销售额对比
### 各产品销售额柱状图
| 产品 | 销售额(万元) |
|------|-------------|
| 大米 | 100 |
| 高粱 | 250 |
| 小麦 | 130 |
## 月度趋势
### 全年销售额变化趋势折线图
| 月份 | 销售额(万元) |
|------|-------------|
| 1月 | 35 |
| 6月 | 72 |
| 12月 | 102 |
## 核心成果
### 业务拓展
- 新增客户 126 家,同比增长 35%
- 开拓西南市场,覆盖 4 个新省份
### 团队建设
1. 团队扩充至 28 人
2. 组织培训 12 场
3. 员工满意度达 92%
> 全年目标超额完成,总销售额突破 560 万元
## 感谢聆听
北灵聊AI
期待2027再创佳绩!
```
### 自然语言提示词示例
```
请使用 ppt-maker 技能,为我生成一份“2026生成式AI行业发展与企业落地趋势”的汇报型幻灯片,整体风格专业、简洁、科技感强,讲解人是北灵聊AI。
封面是2026生成式AI行业发展与企业落地趋势,副标题写“模型能力升级、企业应用加速与商业化观察”,并显示讲解人“北灵聊AI”。
第一页是企业采用生成式AI的主要应用场景分布,用饼状图展示,其中知识助手占比32,智能客服占比24,内容生成占比18,研发提效占比16,数据分析占比10。
第二页是企业AI项目预算投入对比,用柱状图展示,其中大模型平台建设预算380万,AI代码助手预算300万,AI办公助手预算260万,AI智能客服预算220万,AI营销内容生成预算180万。
第三页是2026年企业生成式AI项目推进热度趋势,用折线图展示,其中Q1热度指数48,Q2热度指数63,Q3热度指数78,Q4热度指数92。
最后一页是感谢,标题写“感谢聆听”,副标题写“欢迎交流生成式AI与企业应用实践”。
```
## 支持的命令行
```bash
# 查看帮助
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -h
# 列出所有主题
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -l
# 海洋蓝主题(默认)
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i slides.md -o demo.pptx
# 深夜科技主题
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i slides.md -o demo.pptx -t midnight
# 黑金奢华主题
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i slides.md -o demo.pptx -t luxury
```
## 布局特点
### 页面类型
- **封面页** — 大标题 + 副标题 + 左侧装饰竖条
- **内容页** — 标题栏背景 + 正文区域 + 页码
- **结束页** — 居中大字 + 装饰色块 + 上下装饰线
### 装饰元素
- 顶部主题色强调条
- 左侧边栏装饰线
- 标题栏浅色背景
- 封面竖线装饰 + 底部横线
### 内容渲染
- 无序列表:主题色圆点
- 有序列表:主题色编号圆圈
- 引用块:左侧竖条 + 浅色背景 + 斜体
- 代码块:深色背景 + 等宽字体 + 圆角
- 表格:表头底色 + 交替行着色
- 图表与剩余内容并排显示(饼图右侧/柱状折线收窄后右侧)
## 注意事项
1. **Markdown格式要求:** 必须用 `#` 开头作为封面页,`##` 开头分页
2. **图表触发:** 关键字写在 `##` 标题或 `###` 小标题中最可靠
3. **表格格式:** 第一行为表头,第二行为分隔行 `|---|---|`,第三行起为数据
4. **数值列:** 表格第二列起含可解析数字才会触发图表
5. **输出格式:** 自动补 `.pptx` 后缀
6. **行内格式:** `**粗体**` `*斜体*` `~~删除线~~` 等会自动清理为纯文本
## 文件位置
- 脚本:`~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js`
- 依赖:`pptxgenjs`(已安装)
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn75v9attg6retx4mdm14beva983desk",
"slug": "ppt-maker",
"version": "1.0.3",
"publishedAt": 1774166054605
}
+30
View File
@@ -0,0 +1,30 @@
@startuml ppt-maker-architecture
skinparam componentStyle rectangle
skinparam defaultFontName Arial
skinparam backgroundColor #FAFAFA
package "ppt-maker Skill" {
component "SKILL.md\n(instructions)" as SKILL
component "scripts/ppt-maker.js\n(core engine)" as ENGINE
component "scripts/package.json\n(deps: pptxgenjs)" as PKG
}
package "ppt-maker.js Internals" {
component "Markdown Parser\n(slides, headings, lists)" as PARSER
component "Chart Detector\n(keyword → pie/bar/line)" as CHART
component "Theme Engine\n(ocean/sunset/purple/...)" as THEME
component "PPTX Renderer\n(pptxgenjs)" as RENDERER
}
actor User
User --> SKILL : natural language request
SKILL --> ENGINE : node ppt-maker.js -i ... -o ... -t ...
ENGINE --> PARSER
ENGINE --> CHART
ENGINE --> THEME
PARSER --> RENDERER
CHART --> RENDERER
THEME --> RENDERER
RENDERER --> User : output.pptx
@enduml
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

+25
View File
@@ -0,0 +1,25 @@
@startuml ppt-maker-workflow
skinparam defaultFontName Arial
skinparam backgroundColor #FAFAFA
actor User
participant "ppt-maker\nSkill" as SKILL
participant "ppt-maker.js" as ENGINE
User -> SKILL : "生成一份PPT: <描述>"
SKILL -> SKILL : generate Markdown content
SKILL -> ENGINE : node ppt-maker.js -i input.md -o out.pptx -t <theme>
ENGINE -> ENGINE : parse slides\n(# → cover, ## → content, ## 感谢 → end)
loop each ## slide
ENGINE -> ENGINE : scan headings for chart keywords
alt chart keyword found & table present
ENGINE -> ENGINE : render pie / bar / line chart
else
ENGINE -> ENGINE : render text / list / table
end
end
ENGINE --> User : output.pptx
@enduml
+172
View File
@@ -0,0 +1,172 @@
{
"name": "scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scripts",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"pptxgenjs": "^4.0.1"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
"license": "ISC"
},
"node_modules/image-size": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
},
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/pptxgenjs": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
"integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==",
"license": "MIT",
"dependencies": {
"@types/node": "^22.8.1",
"https": "^1.0.0",
"image-size": "^1.2.1",
"jszip": "^3.10.1"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "ppt-maker",
"version": "1.0.0",
"description": "Generate PPT slides automatically from user input",
"main": "ppt-maker.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["ppt", "pptx", "slides", "presentation", "ai", "generator", "automatic"],
"author": "北灵聊AI",
"license": "MIT",
"dependencies": {
"pptxgenjs": "^4.0.1"
}
}
+654
View File
@@ -0,0 +1,654 @@
/**
* PPT Maker - Markdown to PPTX Generator
* Supports auto charts (pie/bar/line), multiple themes, ending page detection.
*/
var PptxGenJS = require("pptxgenjs");
// ══════════════════════════════════════════════════════
// 1. Themes & Colors (no # prefix)
// ══════════════════════════════════════════════════════
var THEMES = {
sunset: { bg: 'FFF8F3', title: 'E85D04', text: '3D405B', accent: 'F48C06', secondary: 'FAA307', light: 'FFECD2', lighter: 'FFF5EB' },
ocean: { bg: 'F0F8FF', title: '0077B6', text: '2D3748', accent: '00B4D8', secondary: '90E0EF', light: 'CAF0F8', lighter: 'E8F8FF' },
purple: { bg: 'FAF5FF', title: '7C3AED', text: '4C1D95', accent: 'A78BFA', secondary: 'C4B5FD', light: 'EDE9FE', lighter: 'F5F3FF' },
luxury: { bg: '1C1917', title: 'F5F5F4', text: 'A8A29E', accent: 'D4AF37', secondary: 'F59E0B', light: '292524', lighter: '1C1917' },
midnight: { bg: '0F172A', title: 'F8FAFC', text: 'CBD5E1', accent: '38BDF8', secondary: '60A5FA', light: '1E293B', lighter: '0F172A' },
classic: { bg: 'FFFFFF', title: '1F2937', text: '4B5563', accent: '059669', secondary: '10B981', light: 'ECFDF5', lighter: 'F0FDF4' }
};
var CHART_COLORS = {
ocean: ['0077B6', '00B4D8', '90E0EF', '48CAE4', '023E8A', '0096C7', 'ADE8F4'],
sunset: ['E85D04', 'F48C06', 'FAA307', 'FFBA08', 'DC2F02', 'E36414', 'F77F00'],
purple: ['7C3AED', 'A78BFA', 'C4B5FD', '8B5CF6', '6D28D9', '5B21B6', 'DDD6FE'],
luxury: ['D4AF37', 'F59E0B', 'FBBF24', 'FFD700', 'B8860B', 'D97706', 'FCD34D'],
midnight: ['38BDF8', '60A5FA', '93C5FD', '2563EB', '1D4ED8', '3B82F6', 'BFDBFE'],
classic: ['059669', '10B981', '34D399', '6EE7B7', '047857', '065F46', 'A7F3D0']
};
var CHART_RULES = [
{ type: 'pie', keys: ['饼图', '饼状图', '占比', '比例', '份额', '构成', '组成', '百分比', '比重', 'pie', 'proportion', 'share'] },
{ type: 'line', keys: ['折线', '折线图', '趋势', '增长', '变化', '走势', '曲线', '时间', '月度', '季度', '年度', 'line', 'trend'] },
{ type: 'bar', keys: ['柱状', '柱状图', '柱形', '柱形图', '排名', 'top', '对比', '比较', '分布', '销售额', '金额', '数量', '业绩', '产量', '营收', '收入', 'bar', 'column', 'chart'] }
];
var ENDING_KEYWORDS = ['感谢', '谢谢', 'thank', 'thanks', 'q&a', 'q & a', '问答', '结束', 'the end', '再见', '联系方式', '联系我们'];
// ══════════════════════════════════════════════════════
// 2. Utility Functions
// ══════════════════════════════════════════════════════
function stripInlineMarkdown(text) {
if (!text) return '';
return text
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/__(.+?)__/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/_(.+?)_/g, '$1')
.replace(/~~(.+?)~~/g, '$1')
.replace(/`(.+?)`/g, '$1')
.replace(/\[(.+?)\]\(.+?\)/g, '$1')
.trim();
}
function isTableSeparator(line) {
var inner = line.replace(/^\|/, '').replace(/\|$/, '');
var cells = inner.split('|');
return cells.length > 0 && cells.every(function(c) {
return /^\s*:?-{2,}:?\s*$/.test(c);
});
}
function isEndingSlide(title) {
if (!title) return false;
var lower = title.toLowerCase().trim();
return ENDING_KEYWORDS.some(function(k) { return lower.indexOf(k) !== -1; });
}
function parseNumericCell(raw) {
if (!raw) return NaN;
var cleaned = raw
.replace(/[,]/g, '')
.replace(/[%]/g, '')
.replace(/[¥¥$€£₩]/g, '')
.replace(/[元万亿千百十个份人次件台套组年月日号]/g, '')
.replace(/[()\s]/g, '')
.trim();
return parseFloat(cleaned);
}
function ensureColors(colors, needed) {
var result = [];
for (var i = 0; i < needed; i++) {
result.push(colors[i % colors.length]);
}
return result;
}
// ══════════════════════════════════════════════════════
// 3. Markdown Parser
// ══════════════════════════════════════════════════════
function parse(text) {
var slides = [];
var lines = text.split('\n');
var current = null;
var inCode = false;
var codeContent = [];
for (var li = 0; li < lines.length; li++) {
var line = lines[li];
var trimmed = line.trim();
if (trimmed.indexOf('```') === 0) {
if (inCode) {
if (current) {
current.content.push({ type: 'code', code: codeContent.join('\n') });
}
codeContent = [];
}
inCode = !inCode;
continue;
}
if (inCode) {
codeContent.push(line);
continue;
}
if (!trimmed) continue;
if (/^# (?!#)/.test(trimmed)) {
if (current) slides.push(current);
current = {
type: 'cover',
title: stripInlineMarkdown(trimmed.slice(2)),
subtitle: '',
content: []
};
}
else if (/^## (?!#)/.test(trimmed)) {
if (current) slides.push(current);
var title = stripInlineMarkdown(trimmed.slice(3));
current = {
type: isEndingSlide(title) ? 'ending' : 'content',
title: title,
content: []
};
}
else if (trimmed.indexOf('### ') === 0) {
if (!current) current = { type: 'content', title: '', content: [] };
current.content.push({ type: 'h3', text: stripInlineMarkdown(trimmed.slice(4)) });
}
else if (/^[-*]\s/.test(trimmed)) {
if (!current) current = { type: 'content', title: '', content: [] };
var itemText = stripInlineMarkdown(trimmed.replace(/^[-*]\s+/, ''));
var last = current.content[current.content.length - 1];
if (last && last.type === 'list') {
last.items.push(itemText);
} else {
current.content.push({ type: 'list', items: [itemText] });
}
}
else if (/^\d+\.\s/.test(trimmed)) {
if (!current) current = { type: 'content', title: '', content: [] };
var oItemText = stripInlineMarkdown(trimmed.replace(/^\d+\.\s+/, ''));
var oLast = current.content[current.content.length - 1];
if (oLast && oLast.type === 'olist') {
oLast.items.push(oItemText);
} else {
current.content.push({ type: 'olist', items: [oItemText] });
}
}
else if (trimmed.charAt(0) === '|') {
if (isTableSeparator(trimmed)) continue;
if (!current) current = { type: 'content', title: '', content: [] };
var inner = trimmed.replace(/^\|/, '').replace(/\|$/, '');
var cells = inner.split('|').map(function(c) { return c.trim(); });
if (cells.length === 0) continue;
var tLast = current.content[current.content.length - 1];
if (tLast && tLast.type === 'table') {
tLast.rows.push(cells);
} else {
current.content.push({ type: 'table', rows: [cells] });
}
}
else if (trimmed.charAt(0) === '>') {
if (!current) current = { type: 'content', title: '', content: [] };
var quoteText = stripInlineMarkdown(trimmed.replace(/^>\s*/, ''));
var qLast = current.content[current.content.length - 1];
if (qLast && qLast.type === 'quote') {
qLast.lines.push(quoteText);
} else {
current.content.push({ type: 'quote', lines: [quoteText] });
}
}
else {
if (!current) current = { type: 'content', title: '', content: [] };
var cleaned = stripInlineMarkdown(trimmed);
if (current.type === 'cover' && !current.subtitle) {
current.subtitle = cleaned;
} else {
current.content.push({ type: 'text', text: cleaned });
}
}
}
if (inCode && codeContent.length > 0 && current) {
current.content.push({ type: 'code', code: codeContent.join('\n') });
}
if (current) slides.push(current);
return slides;
}
// ══════════════════════════════════════════════════════
// 4. Chart Detection
// ══════════════════════════════════════════════════════
function extractSeries(table) {
if (!table.rows || table.rows.length < 2) return null;
var headers = table.rows[0];
var dataRows = table.rows.slice(1);
if (headers.length < 2 || dataRows.length === 0) return null;
var labels = dataRows.map(function(r) { return (r[0] || '').trim(); });
var series = [];
for (var col = 1; col < headers.length; col++) {
var values = dataRows.map(function(r) { return parseNumericCell(r[col]); });
if (values.every(function(v) { return !isNaN(v) && isFinite(v); })) {
series.push({
name: (headers[col] || '').trim() || ('Series' + col),
labels: labels,
values: values
});
}
}
return series.length > 0 ? series : null;
}
function detectChart(table, slide, tableIndex) {
var series = extractSeries(table);
if (!series) return null;
var labels = series[0].labels;
var hints = [];
if (slide.title) hints.push(slide.title.toLowerCase());
var hintIndex = -1;
for (var j = tableIndex - 1; j >= 0; j--) {
var item = slide.content[j];
if (item.type === 'h3') {
hints.push(item.text.toLowerCase());
hintIndex = j;
break;
}
if (item.type === 'text') {
hints.push(item.text.toLowerCase());
}
}
hints.push(table.rows[0].map(function(h) { return (h || '').toLowerCase(); }).join(' '));
var combined = hints.join(' ');
for (var ri = 0; ri < CHART_RULES.length; ri++) {
var rule = CHART_RULES[ri];
if (rule.keys.some(function(k) { return combined.indexOf(k) !== -1; })) {
return { type: rule.type, series: series, labels: labels, hintIndex: hintIndex };
}
}
var vals = series[0].values;
var sum = vals.reduce(function(a, b) { return a + b; }, 0);
var count = vals.length;
if (count >= 2 && count <= 12 && sum >= 80 && sum <= 120) {
return { type: 'pie', series: series, labels: labels, hintIndex: hintIndex };
}
if (count >= 2) {
return { type: 'bar', series: series, labels: labels, hintIndex: hintIndex };
}
return null;
}
// ══════════════════════════════════════════════════════
// 5. Chart Type Resolution (multi-version compat)
// ══════════════════════════════════════════════════════
function getChartType(pres, name) {
var MAP = { pie: 'PIE', line: 'LINE', bar: 'BAR' };
var key = MAP[name];
if (!key) return name;
if (pres.charts && pres.charts[key] !== undefined) return pres.charts[key];
if (pres.ChartType && pres.ChartType[key] !== undefined) return pres.ChartType[key];
if (pres.ChartType && pres.ChartType[name] !== undefined) return pres.ChartType[name];
return name;
}
// ══════════════════════════════════════════════════════
// 6. Chart Rendering
// ══════════════════════════════════════════════════════
function addChartToSlide(s, pres, chartData, colors, t, layout) {
var lx = (layout && layout.x) || 0.5;
var ly = (layout && layout.y) || 1.4;
var lw = (layout && layout.w) || 9;
var lh = (layout && layout.h) || 3.8;
var chartType = getChartType(pres, chartData.type);
var isPie = chartData.type === 'pie';
var isLine = chartData.type === 'line';
var data = isPie ? [chartData.series[0]] : chartData.series;
var needed = isPie ? data[0].values.length : Math.max(data[0].values.length, data.length);
var clrs = ensureColors(colors, needed);
var opts = {
x: lx, y: ly, w: lw, h: lh,
chartColors: clrs,
showLegend: true,
legendPos: isPie ? 'r' : 'b',
legendFontSize: 9,
legendColor: t.text,
showTitle: false
};
if (isPie) {
opts.showPercent = true;
opts.showValue = false;
opts.dataLabelColor = t.text;
opts.dataLabelFontSize = 10;
} else if (isLine) {
opts.lineSize = 2;
opts.showMarker = true;
opts.markerSize = 6;
opts.catAxisLabelColor = t.text;
opts.catAxisLabelFontSize = 9;
opts.valAxisLabelColor = t.text;
opts.valAxisLabelFontSize = 9;
opts.showValue = true;
opts.dataLabelColor = t.text;
opts.dataLabelFontSize = 8;
opts.dataLabelPosition = 'outEnd';
} else {
opts.barDir = 'col';
opts.barGapWidthPct = 80;
opts.catAxisLabelColor = t.text;
opts.catAxisLabelFontSize = 10;
opts.valAxisLabelColor = t.text;
opts.valAxisLabelFontSize = 9;
opts.showValue = true;
opts.dataLabelColor = t.text;
opts.dataLabelFontSize = 9;
opts.dataLabelPosition = 'outEnd';
}
s.addChart(chartType, data, opts);
}
// ══════════════════════════════════════════════════════
// 7. Content Rendering
// ══════════════════════════════════════════════════════
function renderTable(s, tableItem, t, startY, maxY, startX, totalW) {
if (!tableItem.rows || tableItem.rows.length === 0) return startY;
var colCount = Math.max.apply(null, tableItem.rows.map(function(r) { return r.length; }));
var cw = totalW / colCount;
var rowH = 0.35;
for (var r = 0; r < tableItem.rows.length; r++) {
var ry = startY + r * rowH;
if (ry + rowH > maxY) break;
if (r === 0) {
s.addShape('rect', {
x: startX - 0.05, y: ry, w: colCount * cw + 0.1, h: rowH,
fill: { color: t.light }
});
} else if (r % 2 === 0) {
s.addShape('rect', {
x: startX - 0.05, y: ry, w: colCount * cw + 0.1, h: rowH,
fill: { color: t.lighter || t.bg }
});
}
for (var c = 0; c < tableItem.rows[r].length; c++) {
s.addText(tableItem.rows[r][c], {
x: startX + c * cw, y: ry, w: cw - 0.05, h: rowH,
fontSize: 10, color: r === 0 ? t.title : t.text,
fontFace: 'Arial', bold: r === 0, valign: 'middle'
});
}
}
var renderedRows = Math.min(tableItem.rows.length, Math.floor((maxY - startY) / rowH));
return startY + renderedRows * rowH + 0.2;
}
function renderContent(s, content, t, opts) {
var startY = (opts && opts.startY) || 1.4;
var maxY = (opts && opts.maxY) || 5.0;
var x = (opts && opts.x) || 0.4;
var w = (opts && opts.w) || 8.5;
var y = startY;
for (var idx = 0; idx < content.length; idx++) {
var item = content[idx];
if (y > maxY) break;
if (item.type === 'h3') {
s.addText(item.text, {
x: x, y: y, w: w, h: 0.4,
fontSize: 16, color: t.title, fontFace: 'Arial', bold: true
});
y += 0.5;
}
else if (item.type === 'list') {
for (var li = 0; li < item.items.length; li++) {
if (y > maxY) break;
s.addShape('ellipse', {
x: x + 0.02, y: y + 0.13, w: 0.09, h: 0.09,
fill: { color: t.accent }
});
s.addText(item.items[li], {
x: x + 0.22, y: y, w: w - 0.3, h: 0.35,
fontSize: 13, color: t.text, fontFace: 'Arial'
});
y += 0.42;
}
}
else if (item.type === 'olist') {
for (var oi = 0; oi < item.items.length; oi++) {
if (y > maxY) break;
s.addShape('ellipse', {
x: x, y: y + 0.05, w: 0.22, h: 0.22,
fill: { color: t.accent }
});
s.addText(String(oi + 1), {
x: x, y: y + 0.05, w: 0.22, h: 0.22,
fontSize: 9, color: 'FFFFFF', fontFace: 'Arial', bold: true,
align: 'center', valign: 'middle'
});
s.addText(item.items[oi], {
x: x + 0.3, y: y, w: w - 0.4, h: 0.35,
fontSize: 13, color: t.text, fontFace: 'Arial'
});
y += 0.42;
}
}
else if (item.type === 'code') {
var lineCount = item.code.split('\n').length;
var ch = Math.min(2.5, lineCount * 0.22 + 0.3);
s.addShape('roundRect', {
x: x - 0.1, y: y, w: w + 0.2, h: ch,
fill: { color: '1E1E1E' }, rectRadius: 0.05
});
s.addText(item.code, {
x: x + 0.05, y: y + 0.1, w: w - 0.1, h: ch - 0.2,
fontSize: 10, color: 'D4D4D4', fontFace: 'Consolas', valign: 'top'
});
y += ch + 0.2;
}
else if (item.type === 'quote') {
var quoteText = item.lines.join('\n');
var qLines = item.lines.length;
var qh = Math.min(2.0, qLines * 0.25 + 0.2);
s.addShape('rect', {
x: x, y: y, w: 0.05, h: qh,
fill: { color: t.accent }
});
s.addShape('rect', {
x: x + 0.05, y: y, w: w - 0.05, h: qh,
fill: { color: t.light }
});
s.addText(quoteText, {
x: x + 0.2, y: y + 0.05, w: w - 0.3, h: qh - 0.1,
fontSize: 12, color: t.text, fontFace: 'Arial', italic: true, valign: 'top'
});
y += qh + 0.15;
}
else if (item.type === 'table') {
y = renderTable(s, item, t, y, maxY, x, w);
}
else if (item.type === 'text') {
s.addText(item.text, {
x: x, y: y, w: w, h: 0.35,
fontSize: 12, color: t.text, fontFace: 'Arial'
});
y += 0.4;
}
}
return y;
}
// ══════════════════════════════════════════════════════
// 8. Slide Renderers
// ══════════════════════════════════════════════════════
function addDecorations(s, t) {
s.addShape('rect', { x: 0, y: 0, w: 10, h: 0.12, fill: { color: t.accent } });
s.addShape('rect', { x: 0, y: 0, w: 0.10, h: 5.625, fill: { color: t.accent } });
}
function renderCoverSlide(s, slide, t) {
s.addShape('rect', { x: 0.4, y: 1.3, w: 0.06, h: 1.6, fill: { color: t.accent } });
s.addText(slide.title, {
x: 0.7, y: 1.3, w: 8.5, h: 1.4,
fontSize: 40, color: t.title, fontFace: 'Arial', bold: true, valign: 'middle'
});
if (slide.subtitle) {
s.addText(slide.subtitle, {
x: 0.7, y: 3.0, w: 8, h: 0.8,
fontSize: 18, color: t.text, fontFace: 'Arial'
});
}
s.addShape('rect', { x: 0.7, y: 4.2, w: 2.5, h: 0.03, fill: { color: t.secondary } });
if (slide.content && slide.content.length > 0) {
renderContent(s, slide.content, t, { startY: 4.5, maxY: 5.3 });
}
}
function renderEndingSlide(s, slide, t) {
s.addShape('rect', { x: 1.5, y: 1.0, w: 7, h: 3.5, fill: { color: t.light } });
s.addShape('rect', { x: 2.5, y: 1.3, w: 5, h: 0.04, fill: { color: t.accent } });
s.addShape('rect', { x: 2.5, y: 4.2, w: 5, h: 0.04, fill: { color: t.accent } });
s.addText(slide.title, {
x: 1, y: 1.5, w: 8, h: 1.5,
fontSize: 44, color: t.title, fontFace: 'Arial', bold: true,
align: 'center', valign: 'middle'
});
if (slide.content && slide.content.length > 0) {
var texts = [];
for (var i = 0; i < slide.content.length; i++) {
var ci = slide.content[i];
if (ci.type === 'text') texts.push(ci.text);
if (ci.type === 'list') texts = texts.concat(ci.items);
if (ci.type === 'olist') texts = texts.concat(ci.items);
}
if (texts.length > 0) {
s.addText(texts.join('\n'), {
x: 2, y: 3.0, w: 6, h: 1.2,
fontSize: 14, color: t.text, fontFace: 'Arial',
align: 'center', valign: 'top'
});
}
}
}
function renderContentSlide(s, pres, slide, slideIndex, totalSlides, colors, t) {
s.addShape('rect', { x: 0, y: 0.15, w: 10, h: 1.0, fill: { color: t.light } });
s.addText(slide.title, {
x: 0.5, y: 0.25, w: 9, h: 0.8,
fontSize: 26, color: t.title, fontFace: 'Arial', bold: true, valign: 'middle'
});
var chartData = null;
var chartTableIdx = -1;
for (var ci = 0; ci < slide.content.length; ci++) {
if (slide.content[ci].type === 'table') {
var detected = detectChart(slide.content[ci], slide, ci);
if (detected) {
chartData = detected;
chartTableIdx = ci;
break;
}
}
}
if (chartData) {
var remaining = slide.content.filter(function(_, idx) {
return idx !== chartTableIdx && idx !== chartData.hintIndex;
});
var hasExtra = remaining.length > 0;
try {
var isPie = chartData.type === 'pie';
var chartW = hasExtra ? (isPie ? 5.5 : 6.0) : (isPie ? 6.5 : 9.0);
addChartToSlide(s, pres, chartData, colors, t, {
x: 0.5, y: 1.4, w: chartW, h: 3.8
});
if (hasExtra) {
var sideX = chartW + 0.8;
var sideW = 10 - sideX - 0.3;
if (sideW > 1.5) {
renderContent(s, remaining, t, { startY: 1.5, maxY: 5.0, x: sideX, w: sideW });
}
}
} catch (err) {
renderContent(s, slide.content, t);
}
} else {
renderContent(s, slide.content, t);
}
s.addText((slideIndex + 1) + ' / ' + totalSlides, {
x: 8.5, y: 5.3, w: 1.3, h: 0.25,
fontSize: 9, color: t.secondary, fontFace: 'Arial', align: 'right'
});
}
// ══════════════════════════════════════════════════════
// 9. Main Generator
// ══════════════════════════════════════════════════════
function createPPTX(markdownText, options) {
options = options || {};
var themeName = options.theme || 'ocean';
var t = THEMES[themeName] || THEMES.ocean;
var colors = (CHART_COLORS[themeName] || CHART_COLORS.ocean).slice();
var pres = new PptxGenJS();
pres.layout = 'LAYOUT_16x9';
var slides = parse(markdownText);
if (slides.length === 0) {
var emptySlide = pres.addSlide();
emptySlide.background = { color: t.bg };
emptySlide.addText('(empty content)', {
x: 1, y: 2, w: 8, h: 1.5,
fontSize: 24, color: t.text, fontFace: 'Arial', align: 'center', valign: 'middle'
});
return pres;
}
for (var si = 0; si < slides.length; si++) {
var slide = slides[si];
var s = pres.addSlide();
s.background = { color: t.bg };
addDecorations(s, t);
if (slide.type === 'cover') {
renderCoverSlide(s, slide, t);
} else if (slide.type === 'ending') {
renderEndingSlide(s, slide, t);
s.addText((si + 1) + ' / ' + slides.length, {
x: 8.5, y: 5.3, w: 1.3, h: 0.25,
fontSize: 9, color: t.secondary, fontFace: 'Arial', align: 'right'
});
} else {
renderContentSlide(s, pres, slide, si, slides.length, colors, t);
}
}
return pres;
}
// ══════════════════════════════════════════════════════
// 10. Module Export
// ══════════════════════════════════════════════════════
module.exports = {
createPPTX: createPPTX,
parse: parse,
THEMES: THEMES,
CHART_COLORS: CHART_COLORS
};