@code2rich/jpage 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +3 -1
- package/.github/workflows/release.yml +87 -0
- package/CLAUDE.md +3 -1
- package/README.md +26 -2
- package/bin/commands/ls.js +6 -3
- package/bin/commands/update.js +74 -0
- package/bin/jpage.js +7 -2
- package/docs/RELEASING.md +209 -0
- package/docs/skill-integration-design.md +384 -0
- package/eslint.config.mjs +2 -0
- package/lib/csp.js +8 -2
- package/lib/render.js +9 -2
- package/lib/templates.js +1 -1
- package/mcp/tools-files.js +5 -1
- package/package.json +4 -4
- package/public/css/style.css +128 -1
- package/public/index.html +51 -3
- package/public/js/app.js +8 -6
- package/public/js/pages/content-templates.js +1 -1
- package/public/js/pages/home.js +218 -9
- package/public/js/pages/landing.js +1 -1
- package/public/js/pages/preview.js +1 -1
- package/public/js/utils.js +15 -7
- package/routes/skills.js +77 -3
- package/server.js +10 -3
- package/skills/jpage-presentation/INSTALL.md +50 -0
- package/skills/jpage-presentation/README.md +71 -0
- package/skills/jpage-presentation/SKILL.md +226 -0
- package/skills/jpage-presentation/assets/plugin/highlight/monokai.css +71 -0
- package/skills/jpage-presentation/assets/plugin/highlight/plugin.js +439 -0
- package/skills/jpage-presentation/assets/plugin/notes/notes.js +1 -0
- package/skills/jpage-presentation/assets/reveal-base.css +9 -0
- package/skills/jpage-presentation/assets/reveal.js +9 -0
- package/skills/jpage-presentation/assets/themes/academic.css +68 -0
- package/skills/jpage-presentation/assets/themes/business.css +64 -0
- package/skills/jpage-presentation/assets/themes/creative.css +81 -0
- package/skills/jpage-presentation/assets/themes/minimal.css +117 -0
- package/skills-registry.js +0 -6
- package/test/dispatch-bench.js +0 -3
- package/test/integration/cli.test.js +93 -0
- package/test/integration/skills.test.js +27 -5
- package/test/perf-harness.js +0 -9
- package/test/unit/fts.test.js +0 -1
- package/.claude/settings.local.json +0 -68
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# 即页(jpage)内容生产能力扩展 —— Skill 集成设计方案
|
|
2
|
+
|
|
3
|
+
> 基于调研报告《开源 Skill 项目探索与即页集成方案》审阅后制定。
|
|
4
|
+
> 本文档是**设计稿**,不含实现代码;审阅通过后再分阶段落地。
|
|
5
|
+
> 评审焦点:**可实现、易扩展、好落地**。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. TL;DR(一句话方案)
|
|
10
|
+
|
|
11
|
+
在即页现有的「**Skill 编排 → MCP 工具 → REST/DB 沉淀**」三层插件位上,**以 Skill(SKILL.md)为主载体扩展内容生产纵深**,第一优先落地 `jpage-presentation`(reveal.js 幻灯片,走 Bundle 模式);把 `jpage-chart`(与 Mermaid 重叠)和 Phase 3 的外部市场对接列为「暂缓」;其余 Skill(editorial / clone-ui)作为模板与 Skill 双轨增量。
|
|
12
|
+
|
|
13
|
+
报告整体方向正确,但有三处技术判断需修正,本方案已据此调整。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. 对调研报告的审阅结论
|
|
18
|
+
|
|
19
|
+
### 1.1 报告判断正确、可直接采纳的部分
|
|
20
|
+
|
|
21
|
+
| 结论 | 评价 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| 三条路径(Fork&Adapt / Inspire&Build / Market&Extend) | ✅ 准确,优先级合理 |
|
|
24
|
+
| revealjs-skill 位于第一象限(高价值 × 低难度) | ✅ 正确,与即页定位契合 |
|
|
25
|
+
| 现有基础设施够用(自动发现/YAML 解析/ZIP 打包/Web UI) | ✅ 属实,加 Skill 几乎零代码 |
|
|
26
|
+
| Phase 3 外部市场对接(Composio/agentskills.so)投入大、收益远 | ✅ 同意,列为暂缓 |
|
|
27
|
+
|
|
28
|
+
### 1.2 报告需修正的三处技术判断
|
|
29
|
+
|
|
30
|
+
**① reveal.js 在 iframe 里的键盘导航没有报告说的那么乐观(报告 6.1)**
|
|
31
|
+
|
|
32
|
+
报告说「sandbox 已允许 JS 执行,键盘事件可在 iframe 内处理」。现实是:
|
|
33
|
+
|
|
34
|
+
- reveal.js 依赖 `keydown`,但即页预览页是 SPA,父页面有全局键盘监听(方向键翻历史等),会和 iframe 抢键。
|
|
35
|
+
- 方向键/空格在父页面被消费时,iframe 内的 reveal.js 收不到,表现为「按键没反应」。
|
|
36
|
+
|
|
37
|
+
**正解**:reveal.js 配置 `embedded: true`(依赖容器内点击聚焦)+ 预览页提供「在新窗口打开 / 全屏」兜底按钮。不假设键盘事件能自动透传。
|
|
38
|
+
|
|
39
|
+
**② 「自包含化」被低估,且漏了现成的 Bundle 机制(报告 6.2)**
|
|
40
|
+
|
|
41
|
+
报告在「reveal.js 库内联 / 通过 CDN」二选一里纠结,但**即页已有 `is_bundle` 机制**(ZIP 解压成目录 + `<base>` 注入相对路径)。这是第三条、也是最优解:
|
|
42
|
+
|
|
43
|
+
| 策略 | 体积 | 离线可预览 | 实现成本 | 缺点 |
|
|
44
|
+
|---|---|---|---|---|
|
|
45
|
+
| CDN | 小 | ❌ 断网/内网看不到 | 最低 | 破坏即页核心卖点 |
|
|
46
|
+
| 全内联 | 每文件 200KB+ | ✅ | 中 | 生成 10 个 PPT 就 2MB+ |
|
|
47
|
+
| **Bundle**(本方案采用) | reveal.js 一份共用 | ✅ | 中 | 必须走 ZIP 上传 |
|
|
48
|
+
|
|
49
|
+
**③ `jpage-chart` 是伪需求,建议砍掉**
|
|
50
|
+
|
|
51
|
+
即页 Markdown 渲染**已支持 Mermaid**(流程图/时序图/甘特图),内容模板市场已有 `dashboard` 场景(内含 Chart.js 模板)。单独的 chart Skill 与现有能力重叠,且静态 HTML 写死数据不如让 AI 直接写 Mermaid。
|
|
52
|
+
|
|
53
|
+
**建议**:砍掉 `jpage-chart` Skill;若要增强图表能力,扩 `dashboard`/`report` 场景模板即可,零新代码。
|
|
54
|
+
|
|
55
|
+
### 1.3 报告工作量需如实标注
|
|
56
|
+
|
|
57
|
+
Phase 1「1-2 周」含 3-5 套 reveal.js 主题——reveal.js 主题是 **30+ CSS 变量驱动的整套系统**(`--r-main-color`、`--r-link-color`、`--r-heading-font`…),不是写几个 CSS 文件。做出「商务/学术/创意/极简」四套**视觉差异明显**的主题,纯设计调色排版就 3-5 天。本方案把这部分明确标为「含 UI 设计工时」。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2. 设计总原则
|
|
62
|
+
|
|
63
|
+
### 2.1 扩展位分层与职责边界
|
|
64
|
+
|
|
65
|
+
即页已经为内容生产预留了清晰的三层插件位。本方案严格沿用,**不新增框架**:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
69
|
+
│ Skill 层 (skills/*/SKILL.md) ← 本方案主载体 │
|
|
70
|
+
│ 职责:教 AI「怎么做」——编排工作流、触发场景、风格规范 │
|
|
71
|
+
│ 扩展方式:加目录 + SKILL.md,零代码(registry 自动发现) │
|
|
72
|
+
├─────────────────────────────────────────────────────────────┤
|
|
73
|
+
│ MCP 工具层 (mcp/tools-*.js) ← 复用为主,极少新增 │
|
|
74
|
+
│ 职责:给 AI「手段」——upload_file / list_content_templates… │
|
|
75
|
+
│ 扩展方式:新增 tools-xxx.js + server.js 工厂一行注册 │
|
|
76
|
+
├─────────────────────────────────────────────────────────────┤
|
|
77
|
+
│ REST/DB 层 (routes/*.js + migrations) ← 按需小幅扩展 │
|
|
78
|
+
│ 职责:让产物「可沉淀」——内容模板市场、scene 枚举、use 计数 │
|
|
79
|
+
│ 扩展方式:migration + route │
|
|
80
|
+
└─────────────────────────────────────────────────────────────┘
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**关键设计决策:能靠 SKILL.md 解决的,绝不写代码。**
|
|
84
|
+
|
|
85
|
+
报告里把 presentation/editorial/clone-ui 都列为「新增 Skill」是对的,但它们的「能力」其实**已经存在**——`upload_file` + `list_content_templates`。新 Skill 的价值是**编排**(教 AI 先查模板、再仿风格、最后走 bundle 上传),不是新增后端能力。这把实现成本压到最低。
|
|
86
|
+
|
|
87
|
+
### 2.2 Skill 间协作:单一上传中枢
|
|
88
|
+
|
|
89
|
+
所有生产型 Skill **统一复用 `jpage-upload` 的上传能力**,不各自实现:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
用户:"做个 Q3 汇报 PPT"
|
|
93
|
+
│
|
|
94
|
+
▼
|
|
95
|
+
jpage-presentation(编排:规划结构 → 生成 reveal.js HTML → 组装 ZIP)
|
|
96
|
+
│ 复用
|
|
97
|
+
▼
|
|
98
|
+
upload_file(上传 ZIP → 自动判 bundle → 返回 /s/:key)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Skill 之间不直接调用,而是**通过共同的工具层间接协作**。这保证任一 Skill 可独立增删,不影响其他。
|
|
102
|
+
|
|
103
|
+
### 2.3 「场景模板」与「Skill」双轨
|
|
104
|
+
|
|
105
|
+
报告里两套东西容易混,本方案明确区分:
|
|
106
|
+
|
|
107
|
+
| 维度 | 内容模板(content_templates 表) | Skill(skills/ 目录) |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| 本质 | **样例 HTML/MD**(AI 仿其风格) | **工作流指令**(教 AI 怎么做) |
|
|
110
|
+
| 存储 | DB,运行时可增删 | 文件系统,随版本发布 |
|
|
111
|
+
| 扩展 | migration 预置 / 用户上传 | 加 SKILL.md 目录 |
|
|
112
|
+
| 触发 | `list_content_templates` | AI 读 SKILL.md 自动匹配 |
|
|
113
|
+
|
|
114
|
+
一个能力(如「编辑风格」)可以**双轨存在**:Skill 教工作流,模板提供样例。两者互补,不互斥。
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 3. 目标 Skill 架构(Phase 1 + Phase 2)
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
skills/
|
|
122
|
+
├── jpage-upload/ # 现有:上传中枢(所有生产型 Skill 的依赖)
|
|
123
|
+
├── jpage-content-template/ # 现有:风格顾问(查模板市场仿风格)
|
|
124
|
+
├── jpage-presentation/ # ⭐ Phase 1:reveal.js 幻灯片(Bundle 模式)
|
|
125
|
+
├── jpage-editorial/ # Phase 2:编辑风格内容(html-everything 启发)
|
|
126
|
+
└── jpage-clone-ui/ # Phase 2:UI 风格克隆(santowilem 启发,限制单文件输出)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**砍掉 `jpage-chart`**(与 Mermaid + dashboard 模板重叠)。
|
|
130
|
+
|
|
131
|
+
### 3.1 各 Skill 职责与触发词
|
|
132
|
+
|
|
133
|
+
| Skill | 职责 | 触发词 | 依赖 | 落地阶段 |
|
|
134
|
+
|---|---|---|---|---|
|
|
135
|
+
| jpage-upload | 生成内容并上传 | "上传到即页""生成链接" | 无 | 已有 |
|
|
136
|
+
| jpage-content-template | 查模板市场仿风格 | "参照模板""用 XX 风格" | upload | 已有 |
|
|
137
|
+
| **jpage-presentation** | 生成 reveal.js 幻灯片(Bundle) | "生成 PPT""做幻灯片""演示文稿" | upload | **Phase 1** |
|
|
138
|
+
| jpage-editorial | 编辑/杂志风格排版 | "编辑风格""杂志排版""新闻稿" | upload | Phase 2 |
|
|
139
|
+
| jpage-clone-ui | 克隆网站/UI 风格 | "克隆这个网站""参考这个风格" | upload | Phase 2 |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 4. Phase 1 详细设计:jpage-presentation
|
|
144
|
+
|
|
145
|
+
这是本轮最高优先级,设计要落到可执行颗粒度。
|
|
146
|
+
|
|
147
|
+
### 4.1 工作流(Bundle 模式)
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
用户:"做一个关于 AI 趋势的 10 页 PPT,商务风格"
|
|
151
|
+
│
|
|
152
|
+
▼
|
|
153
|
+
1. [可选] list_content_templates(scene="presentation", keyword="商务")
|
|
154
|
+
→ 若有匹配模板,获取 reveal.js 主题样例,学习配色/排版
|
|
155
|
+
│
|
|
156
|
+
▼
|
|
157
|
+
2. 规划幻灯片结构(封面 / 章节分隔 / 内容 / 总结)
|
|
158
|
+
结构语法沿用 revealjs-skill:1=水平页, N=垂直堆叠, d=分隔页
|
|
159
|
+
│
|
|
160
|
+
▼
|
|
161
|
+
3. 生成多文件站点(写到磁盘):
|
|
162
|
+
presentation/
|
|
163
|
+
├── index.html # reveal.js 容器 + <div class="slides">...</div>
|
|
164
|
+
├── assets/
|
|
165
|
+
│ ├── reveal.css # 选定主题(商务/学术/创意/极简)
|
|
166
|
+
│ ├── reveal.js # reveal.js 引擎(min,~85KB)
|
|
167
|
+
│ └── plugin/ # markdown/highlight/notes 插件(按需)
|
|
168
|
+
└── (无外部 CDN)
|
|
169
|
+
│
|
|
170
|
+
▼
|
|
171
|
+
4. 关键配置:
|
|
172
|
+
- Reveal.initialize({ embedded: true, hash: true, controls: true, ... })
|
|
173
|
+
- <base> 由即页 bundle 渲染自动注入,资源相对路径指向 /api/files/:id/asset/
|
|
174
|
+
│
|
|
175
|
+
▼
|
|
176
|
+
5. 上传(二选一,按客户端能力):
|
|
177
|
+
5a. 有 Bash(Claude Code):Write 写盘 → zip -r → curl multipart POST /api/files/upload
|
|
178
|
+
(二进制流式,reveal.js 不进 token 流,快)
|
|
179
|
+
5b. 纯 MCP(Claude Desktop):upload_file(name="presentation.zip", content=<base64>)
|
|
180
|
+
(ZIP 自动判 bundle,返回 /s/:key;体积大时慢且费 token)
|
|
181
|
+
│
|
|
182
|
+
▼
|
|
183
|
+
6. 返回 /s/:key,并提示用户「幻灯片翻页:iframe 内点击聚焦后用方向键,
|
|
184
|
+
或点预览页"新窗口打开"按钮全屏查看」
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 4.2 与现有 Bundle 机制的契合点(无需改后端)
|
|
188
|
+
|
|
189
|
+
即页已有能力,**Phase 1 后端零改动**:
|
|
190
|
+
|
|
191
|
+
- `POST /api/files/upload`(multipart)和 `POST /api/files/upload-zip-base64`(MCP)都已支持 ZIP。
|
|
192
|
+
- 服务端按 ZIP 内容**自动判定**:含 `index.html` + 资源目录 → `is_bundle=1`,解压成目录存储。
|
|
193
|
+
- 渲染时自动注入 `<base href="/api/files/:id/asset/">`,reveal.js 的相对路径(`assets/reveal.js`)自然指向解压后的资源。
|
|
194
|
+
- 短链 `/s/:key` 直接渲染 bundle 入口。
|
|
195
|
+
|
|
196
|
+
**唯一需要确认的兼容点**(实现时验证,非阻塞):
|
|
197
|
+
|
|
198
|
+
1. **iframe 键盘事件**:reveal.js `embedded: true` 让其依赖容器聚焦而非全局键盘;预览页加「新窗口打开」兜底。
|
|
199
|
+
2. **Bundle 的 `entry_path`**:需确认服务端把 ZIP 根的 `index.html` 识别为入口(看 `routes/files.js` 的 `extractBundle` 逻辑)。若 ZIP 内有顶层目录包裹,要保证 entry 指向该目录下的 index.html。
|
|
200
|
+
|
|
201
|
+
### 4.3 reveal.js 主题系统(成本在这里)
|
|
202
|
+
|
|
203
|
+
reveal.js 主题 = 30+ CSS 变量。四套主题的差异化全靠调这些变量 + 字体栈:
|
|
204
|
+
|
|
205
|
+
```css
|
|
206
|
+
/* 商务主题示例(变量驱动,非完整文件) */
|
|
207
|
+
:root {
|
|
208
|
+
--r-background-color: #ffffff;
|
|
209
|
+
--r-main-color: #1a1a1a;
|
|
210
|
+
--r-heading-color: #0a4d8c; /* 商务深蓝 */
|
|
211
|
+
--r-link-color: #0a4d8c;
|
|
212
|
+
--r-heading-font: 'PingFang SC', -apple-system, sans-serif;
|
|
213
|
+
--r-main-font: -apple-system, 'Segoe UI', sans-serif;
|
|
214
|
+
--r-code-font: 'SF Mono', Menlo, monospace;
|
|
215
|
+
--r-section-number-color: #0a4d8c;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
四套主题的**视觉差异定位**:
|
|
220
|
+
|
|
221
|
+
| 主题 | 主色基调 | 字体倾向 | 适用 |
|
|
222
|
+
|---|---|---|---|
|
|
223
|
+
| 商务 | 深蓝 + 白底 | 无衬线稳重 | 季度汇报、商业提案 |
|
|
224
|
+
| 学术 | 深灰 + 米白 | 衬线标题 | 论文答辩、学术分享 |
|
|
225
|
+
| 创意 | 高饱和渐变 | 几何无衬线 | 产品发布、创意提案 |
|
|
226
|
+
| 极简 | 黑白 + 一个强调色 | 大字号留白 | 极简主义、keynote 风 |
|
|
227
|
+
|
|
228
|
+
**工时如实标注**:每套主题(调色 + 排版 + 至少 3 种版式验证)≈ 1 天,四套 ≈ 4 天纯设计。
|
|
229
|
+
|
|
230
|
+
### 4.4 jpage-presentation/SKILL.md 骨架
|
|
231
|
+
|
|
232
|
+
```markdown
|
|
233
|
+
---
|
|
234
|
+
name: jpage-presentation
|
|
235
|
+
description: 当用户要"生成 PPT""做幻灯片""演示文稿"时,生成基于 reveal.js
|
|
236
|
+
的自包含幻灯片网站包并上传到即页,返回分享链接。支持商务/学术/创意/极简
|
|
237
|
+
四套主题。
|
|
238
|
+
version: 1.0.0
|
|
239
|
+
author: jpage
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
# 核心规则
|
|
243
|
+
|
|
244
|
+
用户要幻灯片/PPT/演示文稿时:
|
|
245
|
+
1. 先问/判断主题风格(默认商务),可选 list_content_templates(scene="presentation")
|
|
246
|
+
2. 规划结构(封面 → 章节分隔 → 内容页 → 总结)
|
|
247
|
+
3. 生成 reveal.js 网站包(index.html + assets/,embedded:true,无 CDN)
|
|
248
|
+
4. 打包 ZIP 上传(优先 curl multipart,退回 upload_file base64)
|
|
249
|
+
5. 返回 /s/:key,提示翻页方式
|
|
250
|
+
|
|
251
|
+
# 触发场景
|
|
252
|
+
- "生成 PPT""做幻灯片""演示文稿""做一个 deck"
|
|
253
|
+
- "Q3 汇报""产品发布""答辩 slides"
|
|
254
|
+
|
|
255
|
+
# 关键约束(区别于普通 HTML)
|
|
256
|
+
- 必须用 Bundle 模式(ZIP 含 index.html + assets/),不要单文件内联 reveal.js
|
|
257
|
+
- Reveal.initialize 必须设 embedded:true(iframe 兼容)
|
|
258
|
+
- 主题用 CSS 变量驱动,从商务/学术/创意/极简四套里选
|
|
259
|
+
- 每页 <section> 内容精简,避免溢出(reveal.js 不自动滚动)
|
|
260
|
+
|
|
261
|
+
# 主题选择对照
|
|
262
|
+
| 用户说 | 主题 |
|
|
263
|
+
|---|---|
|
|
264
|
+
| 商务/汇报/正式/提案 | 商务 |
|
|
265
|
+
| 学术/论文/答辩 | 学术 |
|
|
266
|
+
| 创意/产品/发布/活泼 | 创意 |
|
|
267
|
+
| 极简/简约/keynote 风 | 极简 |
|
|
268
|
+
|
|
269
|
+
# 上传方式(性能关键)
|
|
270
|
+
- 有 Bash:Write 写盘 → `zip -r deck.zip deck/` → curl multipart POST /api/files/upload
|
|
271
|
+
- 纯 MCP:upload_file(name="deck.zip", content=<base64>)(体积大时慢)
|
|
272
|
+
|
|
273
|
+
# 复用
|
|
274
|
+
- 上传环节统一走 jpage-upload 的 upload_file 工具,不另造
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 4.5 Phase 1 改动清单(审阅用)
|
|
278
|
+
|
|
279
|
+
| 文件 | 改动 | 类型 |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `skills/jpage-presentation/SKILL.md` | 新增 | Skill |
|
|
282
|
+
| `skills/jpage-presentation/assets/themes/*.css` | 新增 4 套主题 | 资源(随 Skill ZIP 下发) |
|
|
283
|
+
| `skills/jpage-presentation/scripts/scaffold.js` | 新增结构脚手架(可选,复用 revealjs-skill 思路) | 脚本 |
|
|
284
|
+
| `migrations/013_add_presentation_scene.js` | content_templates 的 scene 预置 'presentation' 样例 | migration(可选) |
|
|
285
|
+
| 预览页前端 | 加「新窗口打开」按钮(iframe 键盘兜底) | 小改(可选,Phase 1.5) |
|
|
286
|
+
| 后端 routes/lib | **零改动** | — |
|
|
287
|
+
|
|
288
|
+
> 注:scene 枚举在 `routes/content-templates.js` 的 `CONTENT_TEMPLATE_SCENES` 数组里,加 `'presentation'` 是一行改动(非 migration)。
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 5. Phase 2 设计概要(editorial + clone-ui)
|
|
293
|
+
|
|
294
|
+
Phase 1 验证「Bundle Skill + 模板双轨」模式跑通后,Phase 2 按同模式增量。
|
|
295
|
+
|
|
296
|
+
### 5.1 jpage-editorial(编辑风格)
|
|
297
|
+
|
|
298
|
+
**启发**:iharnoor/html-everything(Archivo Black 标题 + Inter Tight 正文 + JetBrains Mono 数字)。
|
|
299
|
+
|
|
300
|
+
**落地选择**:**双轨**——
|
|
301
|
+
- 渲染模板 `templates/editorial.html`(第 5 套 Markdown 渲染模板,用户上传 MD 时可选)
|
|
302
|
+
- Skill `jpage-editorial`(编排:任意输入 → 编辑风格 HTML → 上传)
|
|
303
|
+
|
|
304
|
+
**改动**:
|
|
305
|
+
- `templates/editorial.html` 新增(注意:当前 `lib/templates.js` 的 `loadTemplates()` 自动扫描 `templates/*.html`,加文件即生效)
|
|
306
|
+
- `BUILTIN_TEMPLATE_THEMES` 加一行映射
|
|
307
|
+
- `skills/jpage-editorial/SKILL.md` 新增
|
|
308
|
+
- 字体策略:Google Fonts CDN(编辑风格强依赖特定字体),或降级系统字体栈。**注意**:这破坏离线自包含,需在 SKILL.md 注明权衡。
|
|
309
|
+
|
|
310
|
+
### 5.2 jpage-clone-ui(UI 风格克隆)
|
|
311
|
+
|
|
312
|
+
**启发**:santowilem/skills 的 7 阶段反幻觉工作流。
|
|
313
|
+
|
|
314
|
+
**关键约束(决定能否落地)**:
|
|
315
|
+
- 原版输出 React/Vue 多文件项目 → 即页只收单文件 HTML。**Skill 指令必须强制纯 HTML 输出**。
|
|
316
|
+
- 原版依赖 Playwright 截图对比 → 即页运行环境无 Playwright。**移除验证阶段**,降级为「AI 自检 + 用户反馈迭代」。
|
|
317
|
+
- 原版的「学习系统」(lessons log)→ 简化为 Skill 内的「风格特征清单」。
|
|
318
|
+
|
|
319
|
+
**落地**:纯 Skill(`skills/jpage-clone-ui/SKILL.md`),零后端改动。输出单文件 HTML 走 `upload_file`。
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 6. 暂缓项(报告建议但本方案不纳入本轮)
|
|
324
|
+
|
|
325
|
+
| 报告建议 | 本方案态度 | 理由 |
|
|
326
|
+
|---|---|---|
|
|
327
|
+
| `jpage-chart` Skill | ❌ 砍掉 | 与 Mermaid + dashboard 模板重叠,伪需求 |
|
|
328
|
+
| 对接 Composio(800+ Skill) | ⏸ 暂缓 | 依赖 Composio 平台(Rube MCP 等),解耦成本高,收益远 |
|
|
329
|
+
| 对接 agentskills.so 市场 | ⏸ 暂缓 | 需 API 集成 + 动态加载安全模型,属 Phase 3 |
|
|
330
|
+
| 用户自定义 Skill 上传 | ⏸ 暂缓 | 需沙箱执行模型,安全风险大,单独立项 |
|
|
331
|
+
| Skill 评分/推荐系统 | ⏸ 暂缓 | 依赖前两项的数据积累 |
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 7. 实施路线图(已按报告修正)
|
|
336
|
+
|
|
337
|
+
### Phase 1(核心能力补齐,预计 1.5-2 周)
|
|
338
|
+
|
|
339
|
+
| 任务 | 工时 | 说明 |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| 验证 Bundle 渲染 reveal.js | 0.5 天 | 用一个手写 reveal.js ZIP 走 `/api/files/upload`,确认 `<base>` 注入 + 资源加载正常 |
|
|
342
|
+
| 4 套 reveal.js 主题 CSS | 4 天 | **含 UI 设计**:商务/学术/创意/极简,CSS 变量驱动 |
|
|
343
|
+
| `jpage-presentation` SKILL.md | 1 天 | 触发词 + Bundle 工作流 + 上传方式 + iframe 注意事项 |
|
|
344
|
+
| presentation 场景模板预置 | 1 天 | scene 数组加项 + 1-2 个内置样例 |
|
|
345
|
+
| 预览页「新窗口打开」按钮 | 0.5 天 | iframe 键盘兜底(可选,Phase 1.5) |
|
|
346
|
+
| 端到端测试 | 1.5 天 | 4 主题 × MCP/curl 两通道 |
|
|
347
|
+
|
|
348
|
+
### Phase 2(风格多样化,Phase 1 验收后启动,2-3 周)
|
|
349
|
+
|
|
350
|
+
| 任务 | 工时 |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `templates/editorial.html` + 主题映射 | 2 天 |
|
|
353
|
+
| `jpage-editorial` SKILL.md | 1 天 |
|
|
354
|
+
| `jpage-clone-ui` SKILL.md(纯指令,限单文件输出) | 2 天 |
|
|
355
|
+
| 扩充内容模板至 15+(覆盖新 scene) | 3 天 |
|
|
356
|
+
| 文档更新(README、CLAUDE.md、docs/api.md) | 1 天 |
|
|
357
|
+
|
|
358
|
+
### Phase 3(生态对接,暂缓,待 Phase 2 数据积累后评估)
|
|
359
|
+
|
|
360
|
+
仅记录方向,不排期:外部 Skill 市场对接、用户自定义 Skill、评分推荐。
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## 8. 风险与验证清单
|
|
365
|
+
|
|
366
|
+
落地前需逐项验证(实现阶段的 Definition of Done):
|
|
367
|
+
|
|
368
|
+
| # | 验证点 | 方法 | 阻塞性 |
|
|
369
|
+
|---|---|---|---|
|
|
370
|
+
| 1 | ZIP 含顶层目录时 entry_path 是否正确 | 手写带包裹目录的 reveal.js ZIP 上传,看渲染 | 🔴 高(若错,Bundle 模式不可用) |
|
|
371
|
+
| 2 | reveal.js `embedded:true` 在即页 iframe 内翻页是否可用 | 浏览器实测方向键/空格 | 🟡 中(不可用则强依赖新窗口按钮) |
|
|
372
|
+
| 3 | `<base>` 注入后 reveal.js 插件相对路径加载正常 | 打开 DevTools Network 看资源 200 | 🔴 高 |
|
|
373
|
+
| 4 | upload_file 的 base64 ZIP 在 ~200KB reveal.js 体积下耗时 | 实测 token 流耗时 | 🟡 中(决定是否主推 curl 通道) |
|
|
374
|
+
| 5 | 四套主题在深色/浅色系统模式下都不崩 | 切系统主题逐套看 | 🟢 低 |
|
|
375
|
+
|
|
376
|
+
**建议**:Phase 1 开工第一件事是验证 #1 和 #3(半小时手写一个最小 reveal.js ZIP 跑通),若 Bundle 对 reveal.js 不兼容,回退到 CDN 模式(牺牲离线卖点但不阻塞)。
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## 9. 开放问题(需你拍板,非阻塞)
|
|
381
|
+
|
|
382
|
+
1. **reveal.js 资源分发**:Phase 1 是否接受「reveal.js 随每个 Skill ZIP 下发一份」(~85KB × 多 Skill 重复)?还是抽到 `skills/_shared/`?后者要改 registry。
|
|
383
|
+
2. **editorial 字体**:Google Fonts CDN(破坏离线)vs 系统字体栈(风格打折)?倾向 CDN + SKILL.md 注明权衡。
|
|
384
|
+
3. **clone-ui 的 Playwright 验证**:是否值得在 Phase 2 给即页加可选的 Playwright 校验能力(显著增加复杂度)?倾向不加,靠 AI 自检。
|
package/eslint.config.mjs
CHANGED
package/lib/csp.js
CHANGED
|
@@ -23,6 +23,7 @@ const APP_CSP = [
|
|
|
23
23
|
].join('; ');
|
|
24
24
|
|
|
25
25
|
// Markdown 渲染页:内联 mermaid 初始化脚本靠 nonce 放行,vendor 资源同源。
|
|
26
|
+
// frame-ancestors 'self':允许同源 iframe 嵌入(文件列表卡片缩略图),外站不可嵌入。
|
|
26
27
|
function markdownCsp(nonce) {
|
|
27
28
|
return [
|
|
28
29
|
"default-src 'self'",
|
|
@@ -31,12 +32,13 @@ function markdownCsp(nonce) {
|
|
|
31
32
|
"img-src 'self' data: blob:",
|
|
32
33
|
"font-src 'self'",
|
|
33
34
|
"connect-src 'self'",
|
|
34
|
-
"frame-ancestors '
|
|
35
|
+
"frame-ancestors 'self'",
|
|
35
36
|
].join('; ');
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// HTML 渲染页:用户 HTML 常含合法 script/外链资源,宽松策略 + iframe sandbox 兜底。
|
|
39
40
|
// 放开 https: 让用户的图表库/CDN/图片能加载;sandbox 去掉 allow-same-origin 阻断对父窗口的访问。
|
|
41
|
+
// frame-ancestors 'self':允许同源 iframe 嵌入(文件列表卡片缩略图),外站不可嵌入。
|
|
40
42
|
const HTML_CSP = [
|
|
41
43
|
"default-src 'self'",
|
|
42
44
|
"script-src 'self' 'unsafe-inline' https:",
|
|
@@ -44,9 +46,12 @@ const HTML_CSP = [
|
|
|
44
46
|
"img-src 'self' data: blob: https:",
|
|
45
47
|
"font-src 'self' https: data:",
|
|
46
48
|
"connect-src 'self' https:",
|
|
47
|
-
"frame-ancestors '
|
|
49
|
+
"frame-ancestors 'self'",
|
|
48
50
|
].join('; ');
|
|
49
51
|
|
|
52
|
+
// 渲染端点的 X-Frame-Options 取值(与 CSP frame-ancestors 'self' 对齐,兜底不支持 CSP 的旧浏览器)。
|
|
53
|
+
const RENDER_FRAME_VALUE = 'SAMEORIGIN';
|
|
54
|
+
|
|
50
55
|
// 判断请求路径是否为用户内容渲染端点(这些端点由路由内自行 setHeader,中间件跳过)。
|
|
51
56
|
function isRenderPath(reqPath) {
|
|
52
57
|
return /^\/api\/files\/\d+\/(render|versions\/\d+\/render|asset\/)/.test(reqPath) || /^\/s\//.test(reqPath);
|
|
@@ -63,4 +68,5 @@ module.exports = {
|
|
|
63
68
|
markdownCsp,
|
|
64
69
|
isRenderPath,
|
|
65
70
|
generateNonce,
|
|
71
|
+
RENDER_FRAME_VALUE,
|
|
66
72
|
};
|
package/lib/render.js
CHANGED
|
@@ -8,7 +8,7 @@ const { marked, applyTemplate, templateCache, getTemplateForFile, BUILTIN_TEMPLA
|
|
|
8
8
|
const { getRenderedHtml, setRenderedHtml } = require('./render-cache');
|
|
9
9
|
const { UPLOAD_DIR } = require('./paths');
|
|
10
10
|
const { ZIP_MAX_FILE_COUNT } = require('./zip');
|
|
11
|
-
const { generateNonce, markdownCsp, HTML_CSP } = require('./csp');
|
|
11
|
+
const { generateNonce, markdownCsp, HTML_CSP, RENDER_FRAME_VALUE } = require('./csp');
|
|
12
12
|
|
|
13
13
|
// 枚举 bundle 目录组成,供 /content 返回清单。
|
|
14
14
|
// 安全与体量约束:条目数上限与上传校验一致(ZIP_MAX_FILE_COUNT),
|
|
@@ -60,6 +60,7 @@ function sendMarkdownHtml(res, html) {
|
|
|
60
60
|
const withNonce = html.replace(/<script(?![^>]*\bsrc=)([^>]*)>/gi, '<script nonce="' + nonce + '"$1>');
|
|
61
61
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
62
62
|
res.setHeader('Content-Security-Policy', markdownCsp(nonce));
|
|
63
|
+
res.setHeader('X-Frame-Options', RENDER_FRAME_VALUE); // 同源可嵌入(卡片缩略图),对齐 CSP frame-ancestors 'self'
|
|
63
64
|
res.send(withNonce);
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -67,6 +68,7 @@ function sendMarkdownHtml(res, html) {
|
|
|
67
68
|
function sendHtmlDoc(res, html) {
|
|
68
69
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
69
70
|
res.setHeader('Content-Security-Policy', HTML_CSP);
|
|
71
|
+
res.setHeader('X-Frame-Options', RENDER_FRAME_VALUE); // 同源可嵌入(卡片缩略图),对齐 CSP frame-ancestors 'self'
|
|
70
72
|
res.send(html);
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -90,7 +92,12 @@ async function renderFile(res, file) {
|
|
|
90
92
|
try {
|
|
91
93
|
let content = await fs.promises.readFile(entryPath, 'utf-8');
|
|
92
94
|
const entryExt = path.extname(file.entry_path || 'index.html').toLowerCase();
|
|
93
|
-
|
|
95
|
+
// <base> 指向 entry 所在目录(posix 路径),而非 bundle 根。
|
|
96
|
+
// entry 在根时(如 index.html)目录为空,base 退化为 bundle 根,行为不变;
|
|
97
|
+
// entry 在子目录时(如 deck/index.html)base 含 deck/,使 HTML 内相对资源路径正确解析。
|
|
98
|
+
const entryDir = (file.entry_path || 'index.html').split('/').slice(0, -1).join('/');
|
|
99
|
+
const entryDirPart = entryDir ? entryDir + '/' : '';
|
|
100
|
+
const baseTag = '<base href="/api/files/' + file.id + '/asset/' + entryDirPart + '">';
|
|
94
101
|
|
|
95
102
|
if (entryExt === '.md' || entryExt === '.markdown') {
|
|
96
103
|
// Markdown 入口:marked 渲染 + 模板(命中缓存则跳过昂贵的渲染)
|
package/lib/templates.js
CHANGED
|
@@ -105,7 +105,7 @@ marked.use({
|
|
|
105
105
|
level: 'inline',
|
|
106
106
|
start(src) { return src.indexOf('$'); },
|
|
107
107
|
tokenizer(src) {
|
|
108
|
-
const match = /^\$([
|
|
108
|
+
const match = /^\$([^$\n]+?)\$/.exec(src);
|
|
109
109
|
if (!match) return;
|
|
110
110
|
return {
|
|
111
111
|
type: 'katexInline',
|
package/mcp/tools-files.js
CHANGED
|
@@ -37,7 +37,11 @@ function registerFileTools(server, { api, port, mcpIp, protocol }) {
|
|
|
37
37
|
if (tag) params.set('tag', tag);
|
|
38
38
|
const qs = params.toString();
|
|
39
39
|
const data = await api.get('/api/files' + (qs ? '?' + qs : ''));
|
|
40
|
-
|
|
40
|
+
const files = (data.files || []).map((f) => ({
|
|
41
|
+
...f,
|
|
42
|
+
url: f.share_key ? `${protocol}://${mcpIp}:${port}/s/${f.share_key}` : null,
|
|
43
|
+
}));
|
|
44
|
+
return textResult({ files, pagination: data.pagination });
|
|
41
45
|
}
|
|
42
46
|
);
|
|
43
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code2rich/jpage",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "即页 — 零配置 HTML/Markdown 即时预览与分享工具",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"build:dev": "node build.js --dev",
|
|
20
20
|
"lint": "eslint .",
|
|
21
21
|
"lint:fix": "eslint . --fix",
|
|
22
|
-
"test": "node --test
|
|
23
|
-
"test:unit": "node --test
|
|
24
|
-
"test:integration": "node --test
|
|
22
|
+
"test": "node --test test/unit/*.test.js test/integration/*.test.js",
|
|
23
|
+
"test:unit": "node --test test/unit/*.test.js",
|
|
24
|
+
"test:integration": "node --test test/integration/*.test.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|