dsh-ppt 0.7.0 → 0.8.1
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/CHANGELOG.md +114 -91
- package/LICENSE +21 -21
- package/README.en.md +42 -219
- package/README.md +42 -234
- package/cordis.patch.yml +19 -19
- package/docs/CHANGELOG.en.md +29 -0
- package/docs/FEATURE-PLAN-2026-10-01.md +50 -50
- package/docs/LIBREOFFICE-INTEGRATION-2026-10-01.md +69 -69
- package/docs/USAGE.en.md +194 -0
- package/docs/USAGE.md +209 -0
- package/docs/VALIDATION.md +17 -0
- package/docs/validation/0.8.0.md +10 -0
- package/docs/validation-language-2026-10-03.md +16 -0
- package/lib/client.js +304 -0
- package/lib/execution.d.ts +1 -0
- package/lib/execution.js +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +15 -1
- package/lib/tools.js +2 -1
- package/lib/types.d.ts +1 -0
- package/lib/workbench-language.d.ts +4 -0
- package/lib/workbench-language.js +65 -0
- package/lib/workbench.d.ts +32 -0
- package/lib/workbench.js +270 -0
- package/package.json +91 -84
- package/skills/dsh-ppt/SKILL.md +74 -74
- package/skills/dsh-ppt/package.json +10 -10
- package/skills/dsh-ppt/references/advanced.md +39 -39
- package/skills/dsh-ppt/references/copywriting.md +59 -59
- package/skills/dsh-ppt/references/syntax.md +44 -44
- package/skills/dsh-ppt/references/themes.md +61 -61
- package/skills/dsh-ppt/references/troubleshooting.md +19 -19
- package/skills/dsh-ppt/scripts/build-deck.mjs +184 -184
- package/skills/dsh-ppt/scripts/deck-advanced.mjs +468 -456
- package/skills/dsh-ppt/scripts/deck-core.mjs +2347 -2347
- package/skills/dsh-ppt/scripts/deck-media.mjs +160 -160
- package/skills/dsh-ppt/scripts/deck-render.mjs +270 -236
package/lib/client.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Browser source, distributed directly. tsc deliberately does not rewrite this file.
|
|
2
|
+
window.__ModuleLoader__.load({ id: 'dsh-ppt', factory: require => {
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const { useState, useEffect, useRef, useMemo } = React;
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
|
|
7
|
+
// Interface language follows DSH; user content and stored records keep their original text.
|
|
8
|
+
let localeService;
|
|
9
|
+
const EN = Object.freeze({
|
|
10
|
+
"标题": "Title",
|
|
11
|
+
"副标题": "Subtitle",
|
|
12
|
+
"眉题": "Kicker",
|
|
13
|
+
"正文": "Body text",
|
|
14
|
+
"要点(每行一条)": "Bullets (one per line)",
|
|
15
|
+
"演讲备注": "Speaker notes",
|
|
16
|
+
"无法读取服务响应,请检查插件是否加载": "Could not read the server response. Check that the plugin is loaded.",
|
|
17
|
+
"操作未完成": "The operation did not complete.",
|
|
18
|
+
"正在处理,请稍候…": "Working, please wait…",
|
|
19
|
+
"操作失败": "Operation failed.",
|
|
20
|
+
"已定位到": "Focused ",
|
|
21
|
+
",在字段中修改后保存。": ". Edit this field, then save.",
|
|
22
|
+
"点击预览里的文字,或在下方修改字段。": "Click text in the preview, or edit the fields below.",
|
|
23
|
+
"当前页已被删除,输入仍保留,请复制需要的内容后取消修改。": "This slide was deleted. Your input is preserved; copy anything you need before cancelling your edits.",
|
|
24
|
+
"已载入最新版本并保留你的修改,请核对后再保存。": "Latest version loaded with your edits preserved. Review before saving.",
|
|
25
|
+
"内容没有变化。": "No changes to save.",
|
|
26
|
+
"已保存,HTML、PPTX 和工程文件已同步更新。": "Saved. HTML, PPTX and project files were updated together.",
|
|
27
|
+
"页面顺序已更新,可撤销。": "Slides reordered. You can undo this change.",
|
|
28
|
+
"已撤销上次修改。": "Last change undone.",
|
|
29
|
+
"当前 PPTX 已生成逐页图片,请查看画面。": "Slide images were rendered from the current PPTX. Review each slide.",
|
|
30
|
+
"正在渲染最终 PPTX,较大文件可能需要几分钟…": "Rendering the final PPTX. Large files may take a few minutes…",
|
|
31
|
+
"已请求下载,请查看浏览器的下载列表。": "Download requested. Check your browser’s downloads.",
|
|
32
|
+
"请选择 8 MiB 以内的 PNG 或 JPEG 图片。": "Choose a PNG or JPEG image up to 8 MiB.",
|
|
33
|
+
"无法读取图片,请重试。": "Could not read the image. Try again.",
|
|
34
|
+
"演示文稿编辑": "Presentation editor",
|
|
35
|
+
"修改本插件生成的成品,保存时同步更新 HTML、可编辑 PPTX 和工程文件。": "Edit presentations generated by this plugin. Saving updates the HTML, editable PPTX and project file together.",
|
|
36
|
+
"载入最新版本,保留我的输入": "Load latest and keep my input",
|
|
37
|
+
"刷新项目": "Refresh projects",
|
|
38
|
+
"还没有可以编辑的项目": "No editable projects yet",
|
|
39
|
+
"先在对话中让助手生成演示文稿。已有项目可以让助手调用 ppt_check 检查一次,它会出现在这里。": "Ask the assistant to generate a presentation in chat. For an existing project, ask it to run ppt_check; the project will appear here.",
|
|
40
|
+
"示例:请把这份材料制作成可直接交付的演示文稿,生成 HTML、PPTX 和工程文件。": "Example: Turn this material into a presentation ready to deliver, including HTML, PPTX and project files.",
|
|
41
|
+
"返回项目": "Back to projects",
|
|
42
|
+
" 页 · 修订 ": " slides · Revision ",
|
|
43
|
+
"页面": "Slides",
|
|
44
|
+
"未命名页": "Untitled slide",
|
|
45
|
+
"上移": "Move up",
|
|
46
|
+
"下移": "Move down",
|
|
47
|
+
"HTML 预览": "HTML preview",
|
|
48
|
+
"最终 PPTX 预览": "Final PPTX preview",
|
|
49
|
+
"渲染当前 PPTX": "Render current PPTX",
|
|
50
|
+
"演示文稿 HTML 预览": "Presentation HTML preview",
|
|
51
|
+
"此项目尚无 HTML,请保存一次生成预览。": "This project has no HTML yet. Save to generate a preview.",
|
|
52
|
+
"最终 PPTX 第 ": "Final PPTX slide ",
|
|
53
|
+
" 页渲染结果": " rendered image",
|
|
54
|
+
"正在载入当前页渲染…": "Loading the rendered slide…",
|
|
55
|
+
"这是 HTML 放映预览。最终 PPTX 的字体和排版请用上方渲染检查。点击文字可定位编辑字段。": "This is the HTML playback preview. Use the render controls above to check fonts and layout in the final PPTX. Click text to focus its editing field.",
|
|
56
|
+
"这是当前 PPTX 文件的实际渲染图片,仍需检查每页画面;PowerPoint / WPS 可能因字体环境而不同。": "This image is rendered from the current PPTX. Review every slide; PowerPoint / WPS may display it differently depending on installed fonts.",
|
|
57
|
+
"渲染引擎报告缺失字体:": "The renderer reported missing fonts: ",
|
|
58
|
+
"。请替换为目标电脑可用字体后重新渲染。": ". Choose fonts available on the target computer and render again.",
|
|
59
|
+
"编辑第 ": "Edit slide ",
|
|
60
|
+
" 页": "",
|
|
61
|
+
"有未保存修改,请先保存或取消,再切换页面。": "You have unsaved edits. Save or cancel before switching slides.",
|
|
62
|
+
"这些字段也被修改过,请比较后决定是否保存:": "These fields were changed elsewhere. Compare them before saving:",
|
|
63
|
+
"最新版本:": "Latest version: ",
|
|
64
|
+
"你的修改:": "Your edits: ",
|
|
65
|
+
"替换图片(PNG / JPEG)": "Replace image (PNG / JPEG)",
|
|
66
|
+
"添加图片(会改为图文布局)": "Add image (changes to an image-and-text layout)",
|
|
67
|
+
"当前图片": "Current image",
|
|
68
|
+
"已选择:": "Selected: ",
|
|
69
|
+
"图片说明": "Image description",
|
|
70
|
+
"图片适应方式": "Image fit",
|
|
71
|
+
"完整显示": "Show complete image",
|
|
72
|
+
"填满并裁切": "Fill and crop",
|
|
73
|
+
"保存此页": "Save slide",
|
|
74
|
+
"取消修改": "Cancel edits",
|
|
75
|
+
"撤销上次修改": "Undo last change",
|
|
76
|
+
"质量提醒(": "Quality findings (",
|
|
77
|
+
"第 ": "Slide ",
|
|
78
|
+
" 页:": ": ",
|
|
79
|
+
"下载最新 PPTX": "Download latest PPTX",
|
|
80
|
+
"下载 HTML": "Download HTML",
|
|
81
|
+
"备份工程": "Back up project",
|
|
82
|
+
"演示文稿": "Presentations"
|
|
83
|
+
});
|
|
84
|
+
function languageOf(locale = localeService) {
|
|
85
|
+
try { return String(locale?.getSnapshot?.().active || '').startsWith('en') ? 'en' : 'zh'; } catch { return 'zh'; }
|
|
86
|
+
}
|
|
87
|
+
function t(zh) { return languageOf() === 'en' ? (EN[zh] ?? zh) : zh; }
|
|
88
|
+
function englishError(message, status) {
|
|
89
|
+
if (status === 401) return 'Your session has expired. Reopen DSH and try again.';
|
|
90
|
+
if (status === 404) return 'The presentation workbench is unavailable. Check that the plugin is loaded and restart DSH.';
|
|
91
|
+
if (status === 409) return 'The project changed. Load the latest version and review your edits before saving.';
|
|
92
|
+
return /[\u3400-\u9fff]/.test(message || '') ? 'The operation failed. Check the project files and DSH logs, then retry.' : message;
|
|
93
|
+
}
|
|
94
|
+
function useLanguage(locale) {
|
|
95
|
+
const [, rerender] = useState(0);
|
|
96
|
+
useEffect(() => { const update = () => rerender(n => n + 1); update(); return locale?.subscribe?.(update); }, [locale]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ROUTE = 'api/dsh-ppt/workbench';
|
|
100
|
+
const fields = [['title', '标题'], ['subtitle', '副标题'], ['kicker', '眉题'], ['text', '正文'], ['bullets', '要点(每行一条)'], ['notes', '演讲备注']];
|
|
101
|
+
const css = `
|
|
102
|
+
.pptw {color:var(--fg,#27354b);font:14px/1.6 system-ui,sans-serif;min-width:0;max-width:1140px;margin:0 auto;padding:8px}
|
|
103
|
+
.pptw *{box-sizing:border-box}.pptw h2{font-size:22px;margin:0}.pptw h3{font-size:16px;margin:12px 0 6px}
|
|
104
|
+
.pptw .muted{color:var(--fg-muted,#66768a);font-size:13px}.pptw .tools{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0;align-items:center}
|
|
105
|
+
.pptw button{font:inherit;border:1px solid var(--border,#ccd8e5);border-radius:9px;background:var(--bg-surface,#fff);color:inherit;padding:7px 12px;cursor:pointer;text-align:left;white-space:normal}
|
|
106
|
+
.pptw button.primary{background:#245ee9;border-color:#245ee9;color:#fff}.pptw button:disabled{opacity:.45;cursor:default}
|
|
107
|
+
.pptw input:not([type=file]),.pptw textarea,.pptw select{font:inherit;color:inherit;width:100%;background:var(--bg,#fff);border:1px solid var(--border,#c9d5e4);border-radius:8px;padding:8px 10px}
|
|
108
|
+
.pptw textarea{resize:vertical}.pptw label{display:block;margin:8px 0 3px}.pptw .layout{display:grid;grid-template-columns:minmax(130px,190px) minmax(0,1fr);gap:14px}
|
|
109
|
+
.pptw .pages{display:flex;flex-direction:column;gap:5px;max-height:480px;overflow:auto}.pptw .pages button[aria-current=true]{background:#245ee9;color:#fff}
|
|
110
|
+
.pptw .preview{border:1px solid var(--border,#ced8e6);border-radius:12px;overflow:hidden;background:#f0f4fa;aspect-ratio:16/9;position:relative}
|
|
111
|
+
.pptw iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.pptw .render-page{width:100%;display:block}
|
|
112
|
+
.pptw .notice{border-radius:9px;padding:10px 12px;margin:10px 0;background:#edf4ff;color:#17408e;overflow-wrap:anywhere}.pptw .error{background:#fff1ef;color:#922d20}
|
|
113
|
+
.pptw .card{border:1px solid var(--border,#d5dfeb);border-radius:12px;padding:12px;margin:10px 0;background:var(--bg-surface,#fff)}
|
|
114
|
+
.pptw .project-list{display:flex;flex-direction:column;gap:8px}.pptw .project-list button{display:block;width:100%}
|
|
115
|
+
.pptw .file{font-size:12px;overflow-wrap:anywhere}.pptw .image-preview{width:100%;max-height:160px;object-fit:contain;background:#edf2f8;border-radius:8px}
|
|
116
|
+
.pptw :focus-visible{outline:3px solid #78a8ff;outline-offset:2px}.pptw .dirty{color:#986114;font-size:13px}
|
|
117
|
+
[role=dialog]:has(.pptw){width:min(1200px,calc(100vw - 48px))!important;max-width:1200px!important}
|
|
118
|
+
@media(max-width:700px){.pptw .layout{grid-template-columns:1fr}.pptw .pages{max-height:180px}.pptw .preview{min-height:190px}.pptw{padding:3px}
|
|
119
|
+
[role=dialog]:has(.pptw){width:100%!important;max-width:100%!important;flex-direction:column!important}
|
|
120
|
+
[role=dialog]:has(.pptw)>nav+div{min-width:0;width:100%}
|
|
121
|
+
[role=dialog]:has(.pptw)>nav+div>div:has(.pptw){padding:12px!important;min-width:0}
|
|
122
|
+
[role=dialog]:has(.pptw) nav{width:100%!important;flex-basis:auto!important;border-right:0;max-height:170px;overflow:auto}
|
|
123
|
+
[role=dialog]:has(.pptw) nav>div:has(>button){display:flex;flex-direction:row!important;flex-wrap:wrap;flex-shrink:0;gap:4px}
|
|
124
|
+
}`;
|
|
125
|
+
|
|
126
|
+
async function action(body, signal) {
|
|
127
|
+
const response = await fetch(new URL(ROUTE, document.baseURI), { method: 'POST', credentials: 'same-origin', signal,
|
|
128
|
+
headers: { 'content-type': 'application/json', 'x-dsh-ppt-action': '1' }, body: JSON.stringify(body) });
|
|
129
|
+
let value;
|
|
130
|
+
try { value = await response.json(); } catch { throw new Error(t("无法读取服务响应,请检查插件是否加载")) }
|
|
131
|
+
if (!response.ok || value.ok === false) { const localized = languageOf() === 'en' ? value.messageEn || englishError(value.message, response.status) : value.message; const hint = languageOf() === 'en' ? value.installationHintEn : value.installationHint; const error = new Error((localized || t("操作未完成")) + (hint ? '\n' + hint : '')); error.status = response.status; throw error; }
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function draftFor(slide) {
|
|
135
|
+
return { title: slide.title || '', subtitle: slide.subtitle || '', kicker: slide.kicker || '', text: slide.text || '',
|
|
136
|
+
bullets: (slide.bullets || []).join('\n'), notes: slide.notes || '' };
|
|
137
|
+
}
|
|
138
|
+
function editorHtml(html, nonce) {
|
|
139
|
+
const bridge = `<style>#hud,#delivery-bar,#review-panel,#progress,#notes-panel,#help-panel,#toast,#blank{display:none!important}.slide-frame{pointer-events:none!important}.slide-frame .slide{pointer-events:auto!important}</style><script>(function(){const nonce=${JSON.stringify(nonce)};
|
|
140
|
+
function tell(field){parent.postMessage({type:'dsh-ppt-select',nonce:nonce,field:field},'*')}
|
|
141
|
+
document.addEventListener('keydown',function(e){e.stopImmediatePropagation();if(e.key!=='Tab')e.preventDefault()},true);
|
|
142
|
+
document.addEventListener('touchstart',function(e){e.stopImmediatePropagation()},true);
|
|
143
|
+
document.addEventListener('touchend',function(e){e.stopImmediatePropagation()},true);
|
|
144
|
+
document.addEventListener('wheel',function(e){e.stopImmediatePropagation()},{capture:true,passive:true});
|
|
145
|
+
document.addEventListener('click',function(e){const n=e.target.closest('h1,h2,.subtitle,.kicker,.statement-title,.quote-text,.quote-attr,li');if(!n)return;
|
|
146
|
+
e.preventDefault();e.stopImmediatePropagation();let field=n.matches('li')?'bullets':n.matches('.subtitle,.quote-attr')?'subtitle':n.matches('.kicker')?'kicker':'title';tell(field)},true);
|
|
147
|
+
window.addEventListener('message',function(e){if(e.source!==parent||e.data?.nonce!==nonce||e.data.type!=='dsh-ppt-jump')return;window.__dshPpt?.jump(e.data.page)});
|
|
148
|
+
})();<\/script>`;
|
|
149
|
+
const policy = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; font-src data:; connect-src 'none'; form-action 'none'">`;
|
|
150
|
+
// Presenter code contains a quoted </body>; only append at the document end.
|
|
151
|
+
return html.replace(/<head>/i, '<head>' + policy).replace(/<\/body>(\s*<\/html>\s*)$/i, bridge + '</body>$1');
|
|
152
|
+
}
|
|
153
|
+
function Workbench({ locale } = {}) {
|
|
154
|
+
useLanguage(locale);
|
|
155
|
+
const [projects, setProjects] = useState([]), [project, setProject] = useState(null), [selected, setSelected] = useState('');
|
|
156
|
+
const [draft, setDraft] = useState({}), [image, setImage] = useState(null), [dirty, setDirty] = useState(false);
|
|
157
|
+
const [busy, setBusy] = useState(false), [job, setJob] = useState(''), [message, setMessage] = useState(''), [error, setError] = useState('');
|
|
158
|
+
const [mode, setMode] = useState('html'), [raster, setRaster] = useState('');
|
|
159
|
+
const [conflict, setConflict] = useState(false), [overlaps, setOverlaps] = useState([]);
|
|
160
|
+
const frame = useRef(null), previewBox = useRef(null), fieldRefs = useRef({}), alive = useRef(true), operations = useRef(new Set());
|
|
161
|
+
const nonce = useRef(crypto.randomUUID());
|
|
162
|
+
const previewDocument = useMemo(() => project?.html ? editorHtml(project.html, nonce.current) : '', [project?.html]);
|
|
163
|
+
const index = project ? Math.max(0, project.slides.findIndex(s => s.id === selected)) : 0;
|
|
164
|
+
const slide = project?.slides[index];
|
|
165
|
+
function adopt(next, preferred) {
|
|
166
|
+
setProject(next);
|
|
167
|
+
const row = next.slides.find(s => s.id === preferred) || next.slides[0];
|
|
168
|
+
setSelected(row.id); setDraft(draftFor(row)); setImage(null); setDirty(false); setRaster(''); setOverlaps([]);
|
|
169
|
+
if (!next.render) setMode('html');
|
|
170
|
+
}
|
|
171
|
+
async function perform(work, success, label = t("正在处理,请稍候…")) {
|
|
172
|
+
setBusy(true); setJob(label); setError(''); setMessage(''); setConflict(false);
|
|
173
|
+
const controller = new AbortController(); operations.current.add(controller);
|
|
174
|
+
try { const result = await work(controller.signal); if (alive.current && success) success(result); }
|
|
175
|
+
catch (e) { if (alive.current) { setError(e.message || t("操作失败")); setConflict(e.status === 409); } }
|
|
176
|
+
finally { operations.current.delete(controller); if (alive.current) setBusy(false); }
|
|
177
|
+
}
|
|
178
|
+
function refreshList() { return perform(signal => action({ operation: 'list' }, signal), value => setProjects(value.projects)); }
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
alive.current = true; const abort = new AbortController();
|
|
181
|
+
action({ operation: 'list' }, abort.signal).then(value => { if (alive.current) setProjects(value.projects) }).catch(e => { if (!abort.signal.aborted) setError(e.message) });
|
|
182
|
+
return () => { alive.current = false; abort.abort(); for (const operation of operations.current) operation.abort(); };
|
|
183
|
+
}, []);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
function receive(event) {
|
|
186
|
+
if (event.source !== frame.current?.contentWindow || event.data?.nonce !== nonce.current || event.data.type !== 'dsh-ppt-select' || mode !== 'html') return;
|
|
187
|
+
const key = event.data.field;
|
|
188
|
+
const field = fields.find(([field]) => field === key);
|
|
189
|
+
if (field) { fieldRefs.current[key]?.focus(); fieldRefs.current[key]?.scrollIntoView({ block: 'nearest' }); setMessage(t("已定位到") + t(field[1]) + t(",在字段中修改后保存。")); }
|
|
190
|
+
}
|
|
191
|
+
window.addEventListener('message', receive); return () => window.removeEventListener('message', receive);
|
|
192
|
+
}, [mode]);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (mode !== 'html' || !project || !previewBox.current) return;
|
|
195
|
+
// Align iframe edges/dimensions when the host uses fractional text metrics.
|
|
196
|
+
const box = previewBox.current;
|
|
197
|
+
function align() {
|
|
198
|
+
const target = frame.current; if (!target) return;
|
|
199
|
+
const r = box.getBoundingClientRect(), x = r.x + box.clientLeft, y = r.y + box.clientTop;
|
|
200
|
+
target.style.width = box.clientWidth + 'px'; target.style.height = box.clientHeight + 'px';
|
|
201
|
+
target.style.transform = 'translate(' + (Math.ceil(x) - x) + 'px,' + (Math.ceil(y) - y) + 'px)';
|
|
202
|
+
}
|
|
203
|
+
align(); const observer = new ResizeObserver(align); observer.observe(box);
|
|
204
|
+
window.addEventListener('scroll', align, true);
|
|
205
|
+
return () => { observer.disconnect(); window.removeEventListener('scroll', align, true); };
|
|
206
|
+
}, [mode, !!project]);
|
|
207
|
+
function jump() { frame.current?.contentWindow?.postMessage({ type: 'dsh-ppt-jump', nonce: nonce.current, page: index }, '*'); }
|
|
208
|
+
useEffect(() => { jump(); }, [index, project?.revision, mode]);
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (mode !== 'pptx' || !project?.render) return;
|
|
211
|
+
const abort = new AbortController(); setRaster('');
|
|
212
|
+
action({ operation: 'page', id: project.id, page: index + 1 }, abort.signal).then(value => setRaster(value.data)).catch(e => { if (!abort.signal.aborted) setError(e.message) });
|
|
213
|
+
return () => abort.abort();
|
|
214
|
+
}, [mode, index, project?.revision]);
|
|
215
|
+
function open(id) { return perform(signal => action({ operation: 'open', id }, signal), value => { adopt(value.project); setMessage(t("点击预览里的文字,或在下方修改字段。")); }); }
|
|
216
|
+
function keepInputsAndReload() {
|
|
217
|
+
const original = draftFor(slide), keys = fields.filter(([key]) => draft[key] !== original[key]).map(([key]) => key), pending = { ...draft }, pendingImage = image;
|
|
218
|
+
return perform(signal => action({ operation: 'open', id: project.id }, signal), value => {
|
|
219
|
+
const row = value.project.slides.find(s => s.id === selected);
|
|
220
|
+
if (!row) { setError(t("当前页已被删除,输入仍保留,请复制需要的内容后取消修改。")); return; }
|
|
221
|
+
const latest = draftFor(row), merged = { ...latest }, shared = [];
|
|
222
|
+
for (const key of keys) { merged[key] = pending[key]; if (latest[key] !== original[key] && latest[key] !== pending[key]) shared.push({ key, latest: latest[key], yours: pending[key] }); }
|
|
223
|
+
adopt(value.project, selected); setDraft(merged); setImage(pendingImage); setDirty(keys.length > 0 || !!pendingImage); setOverlaps(shared);
|
|
224
|
+
setMessage(t("已载入最新版本并保留你的修改,请核对后再保存。"));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function choose(row) { if (dirty || busy) return; setSelected(row.id); setDraft(draftFor(row)); setImage(null); }
|
|
228
|
+
function save() {
|
|
229
|
+
const original = draftFor(slide), patch = {};
|
|
230
|
+
for (const [key] of fields) if (draft[key] !== original[key]) patch[key] = key === 'bullets' ? draft[key].split('\n').map(v => v.trim()).filter(Boolean) : draft[key];
|
|
231
|
+
if (image) patch.image = { src: image.src, alt: image.alt, fit: image.fit, caption: image.caption };
|
|
232
|
+
return perform(signal => action({ operation: 'edit', id: project.id, expectedRevision: project.revision, slide: slide.id, patch }, signal), value => {
|
|
233
|
+
adopt(value.project, slide.id); setMessage(value.changed === false ? t("内容没有变化。") : t("已保存,HTML、PPTX 和工程文件已同步更新。"));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function move(delta) {
|
|
237
|
+
const order = project.slides.map(s => s.id); [order[index], order[index + delta]] = [order[index + delta], order[index]];
|
|
238
|
+
return perform(signal => action({ operation: 'edit', id: project.id, expectedRevision: project.revision, order }, signal), value => { adopt(value.project, slide.id); setMessage(t("页面顺序已更新,可撤销。")); });
|
|
239
|
+
}
|
|
240
|
+
function undo() { return perform(signal => action({ operation: 'undo', id: project.id, expectedRevision: project.revision }, signal), value => { adopt(value.project, selected); setMessage(t("已撤销上次修改。")); }); }
|
|
241
|
+
function render() {
|
|
242
|
+
return perform(signal => action({ operation: 'render', id: project.id, expectedRevision: project.revision }, signal), value => { adopt(value.project, selected); setMode('pptx'); setMessage(t("当前 PPTX 已生成逐页图片,请查看画面。")); }, t("正在渲染最终 PPTX,较大文件可能需要几分钟…"));
|
|
243
|
+
}
|
|
244
|
+
function download(kind) {
|
|
245
|
+
const url = new URL('api/dsh-ppt/download', document.baseURI);
|
|
246
|
+
url.searchParams.set('id', project.id); url.searchParams.set('kind', kind); url.searchParams.set('revision', String(project.revision));
|
|
247
|
+
// An authenticated attachment response keeps the click's user gesture and
|
|
248
|
+
// works in WebViews whose native download handler cannot read blob URLs.
|
|
249
|
+
const link = document.createElement('a'); link.href = url.href; link.download = '';
|
|
250
|
+
link.hidden = true; document.body.append(link); link.click(); link.remove();
|
|
251
|
+
setMessage(t("已请求下载,请查看浏览器的下载列表。"));
|
|
252
|
+
}
|
|
253
|
+
function upload(event) {
|
|
254
|
+
const file = event.target.files?.[0]; event.target.value = ''; if (!file) return;
|
|
255
|
+
if (!['image/png', 'image/jpeg'].includes(file.type) || file.size > 8 * 1024 * 1024) { setError(t("请选择 8 MiB 以内的 PNG 或 JPEG 图片。")); return; }
|
|
256
|
+
const reader = new FileReader();
|
|
257
|
+
reader.onload = () => { if (!alive.current) return; setImage({ src: String(reader.result), alt: slide.image?.alt || file.name.replace(/\.[^.]+$/, ''), caption: slide.image?.caption || '', fit: slide.image?.fit || 'contain', name: file.name }); setDirty(true); setError(''); };
|
|
258
|
+
reader.onerror = () => setError(t("无法读取图片,请重试。")); reader.readAsDataURL(file);
|
|
259
|
+
}
|
|
260
|
+
const currentImage = image?.src || (slide?.image?.assetId && project.assets[slide.image.assetId]?.data);
|
|
261
|
+
return h('div', { className: 'pptw' }, h('style', null, css),
|
|
262
|
+
h('h2', null, t("演示文稿编辑")), h('p', { className: 'muted' }, t("修改本插件生成的成品,保存时同步更新 HTML、可编辑 PPTX 和工程文件。")),
|
|
263
|
+
error && h('div', { role: 'alert', className: 'notice error' }, error),
|
|
264
|
+
conflict && project && h('button', { disabled: busy, onClick: keepInputsAndReload }, t("载入最新版本,保留我的输入")),
|
|
265
|
+
message && h('div', { role: 'status', className: 'notice' }, message),
|
|
266
|
+
busy && h('div', { role: 'status', className: 'muted' }, job),
|
|
267
|
+
!project ? h(React.Fragment, null,
|
|
268
|
+
h('div', { className: 'tools' }, h('button', { onClick: refreshList, disabled: busy }, t("刷新项目"))),
|
|
269
|
+
projects.length ? h('div', { className: 'project-list' }, projects.map(row => h('button', { key: row.id, onClick: () => open(row.id), disabled: busy }, h('strong', null, row.title), h('div', { className: 'file muted' }, row.path)))) :
|
|
270
|
+
h('div', { className: 'card' }, h('h3', null, t("还没有可以编辑的项目")), h('p', null, t("先在对话中让助手生成演示文稿。已有项目可以让助手调用 ppt_check 检查一次,它会出现在这里。")), h('p', { className: 'muted' }, t("示例:请把这份材料制作成可直接交付的演示文稿,生成 HTML、PPTX 和工程文件。")))
|
|
271
|
+
) : h(React.Fragment, null,
|
|
272
|
+
h('div', { className: 'tools' }, h('button', { disabled: busy || dirty, onClick: () => { setProject(null); refreshList(); } }, t("返回项目")), h('strong', null, project.title), h('span', { className: 'muted' }, project.slides.length + t(" 页 · 修订 ") + project.revision)),
|
|
273
|
+
h('div', { className: 'layout' },
|
|
274
|
+
h('div', null, h('h3', null, t("页面")), h('div', { className: 'pages' }, project.slides.map((row, i) => h('button', { key: row.id, 'aria-current': row.id === selected ? 'true' : undefined, disabled: busy || dirty, onClick: () => choose(row) }, (i + 1) + '. ' + (row.title || t("未命名页"))))),
|
|
275
|
+
h('div', { className: 'tools' }, h('button', { disabled: busy || dirty || index === 0, onClick: () => move(-1) }, t("上移")), h('button', { disabled: busy || dirty || index === project.slides.length - 1, onClick: () => move(1) }, t("下移")))),
|
|
276
|
+
h('div', null,
|
|
277
|
+
h('div', { className: 'tools' }, h('button', { disabled: busy, 'aria-pressed': mode === 'html', onClick: () => setMode('html') }, t("HTML 预览")),
|
|
278
|
+
h('button', { disabled: busy || !project.render, 'aria-pressed': mode === 'pptx', onClick: () => setMode('pptx') }, t("最终 PPTX 预览")),
|
|
279
|
+
h('button', { disabled: busy || dirty, onClick: render }, t("渲染当前 PPTX"))),
|
|
280
|
+
h('div', { className: 'preview', ref: previewBox }, mode === 'html' ? (project.html ? h('iframe', { ref: frame, title: t("演示文稿 HTML 预览"), sandbox: 'allow-scripts', srcDoc: previewDocument, onLoad: jump }) : h('p', null, t("此项目尚无 HTML,请保存一次生成预览。"))) : raster ? h('img', { className: 'render-page', src: raster, alt: t("最终 PPTX 第 ") + (index + 1) + t(" 页渲染结果") }) : h('p', null, t("正在载入当前页渲染…"))),
|
|
281
|
+
h('p', { className: 'muted' }, mode === 'html' ? t("这是 HTML 放映预览。最终 PPTX 的字体和排版请用上方渲染检查。点击文字可定位编辑字段。") : t("这是当前 PPTX 文件的实际渲染图片,仍需检查每页画面;PowerPoint / WPS 可能因字体环境而不同。")),
|
|
282
|
+
project.render?.missingFonts?.length ? h('div', { role: 'alert', className: 'notice error' }, t("渲染引擎报告缺失字体:") + project.render.missingFonts.join('、') + t("。请替换为目标电脑可用字体后重新渲染。")) : null,
|
|
283
|
+
h('div', { className: 'card' }, h('h3', null, t("编辑第 ") + (index + 1) + t(" 页")),
|
|
284
|
+
dirty && h('p', { className: 'dirty' }, t("有未保存修改,请先保存或取消,再切换页面。")),
|
|
285
|
+
overlaps.length ? h('div', { className: 'notice' }, h('strong', null, t("这些字段也被修改过,请比较后决定是否保存:")), overlaps.map(row => h('div', { key: row.key }, h('strong', null, t(fields.find(([key]) => key === row.key)[1])), h('p', null, t("最新版本:") + row.latest), h('p', null, t("你的修改:") + row.yours)))) : null,
|
|
286
|
+
fields.filter(([key]) => key === 'title' || key === 'notes' || key === 'bullets' && (slide.layout === 'bullets' || slide.bullets?.length > 0) || key !== 'bullets' && (slide[key] || key === 'subtitle' && ['cover','section','closing','quote'].includes(slide.layout) || key === 'kicker' && ['cover','section','closing'].includes(slide.layout))).map(([key, label]) => h('div', { key }, h('label', { htmlFor: 'pptw-' + key }, t(label)), h(key === 'title' || key === 'kicker' ? 'input' : 'textarea', { id: 'pptw-' + key, ref: el => { fieldRefs.current[key] = el }, disabled: busy, value: draft[key] || '', rows: key === 'bullets' ? 4 : 2, maxLength: 8000, onChange: e => { setDraft({ ...draft, [key]: e.target.value }); setDirty(true); } }))),
|
|
287
|
+
h('label', { htmlFor: 'pptw-image' }, slide.image ? t("替换图片(PNG / JPEG)") : t("添加图片(会改为图文布局)")),
|
|
288
|
+
h('input', { id: 'pptw-image', type: 'file', accept: 'image/png,image/jpeg', disabled: busy, onChange: upload }),
|
|
289
|
+
currentImage && h('img', { className: 'image-preview', src: currentImage, alt: image?.alt || slide.image?.alt || t("当前图片") }),
|
|
290
|
+
image && h(React.Fragment, null, h('p', { className: 'muted' }, t("已选择:") + image.name), h('label', { htmlFor: 'pptw-alt' }, t("图片说明")), h('input', { id: 'pptw-alt', value: image.alt, disabled: busy, onChange: e => setImage({ ...image, alt: e.target.value }) }), h('label', { htmlFor: 'pptw-fit' }, t("图片适应方式")), h('select', { id: 'pptw-fit', value: image.fit, disabled: busy, onChange: e => setImage({ ...image, fit: e.target.value }) }, h('option', { value: 'contain' }, t("完整显示")), h('option', { value: 'cover' }, t("填满并裁切")))),
|
|
291
|
+
h('div', { className: 'tools' }, h('button', { className: 'primary', disabled: busy || !dirty, onClick: save }, t("保存此页")), h('button', { disabled: busy || !dirty, onClick: () => { setDraft(draftFor(slide)); setImage(null); setDirty(false); setError(''); setConflict(false); setOverlaps([]); } }, t("取消修改")), h('button', { disabled: busy || dirty || !project.undoAvailable, onClick: undo }, t("撤销上次修改")))),
|
|
292
|
+
h('details', { className: 'card' }, h('summary', null, t("质量提醒(") + project.quality.warningCount + (languageOf() === 'en' ? ')' : ')')), (project.quality.issues || []).map((issue, i) => h('p', { key: i }, t("第 ") + issue.slide + t(" 页:") + (languageOf() === 'en' ? issue.messageEn : issue.message) + (languageOf() === 'en' ? ' ' : ';') + (languageOf() === 'en' ? issue.suggestionEn : issue.suggestion)))),
|
|
293
|
+
h('div', { className: 'tools' }, h('button', { disabled: busy || dirty, onClick: () => download('pptx') }, t("下载最新 PPTX")), h('button', { disabled: busy || dirty, onClick: () => download('html') }, t("下载 HTML")), h('button', { disabled: busy || dirty, onClick: () => download('json') }, t("备份工程")))
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
function apply(ctx) {
|
|
300
|
+
try { localeService = ctx.get?.('locale') || ctx.locale; } catch { localeService = ctx.locale; }
|
|
301
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-ppt', order: 58, label: () => t("演示文稿"), inject: () => ({ locale: localeService }) }, Workbench));
|
|
302
|
+
}
|
|
303
|
+
return { inject: ['slots', 'locale'], apply, __internals: { draftFor, editorHtml } };
|
|
304
|
+
} });
|
package/lib/execution.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface PptExecution {
|
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
export declare function getEngine(): Promise<DeckEngine>;
|
|
14
15
|
/** Build executors with a shared engine loader; resolve paths separately for each call. */
|
|
15
16
|
export declare function createPptExecutors(config: ResolvedPptConfig, loadEngine?: () => Promise<DeckEngine>): {
|
|
16
17
|
themes(rawArgs: unknown, exec?: PptExecution): Promise<PptThemesResult>;
|
package/lib/execution.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
let enginePromise = null;
|
|
3
|
-
function getEngine() {
|
|
3
|
+
export function getEngine() {
|
|
4
4
|
if (enginePromise === null) {
|
|
5
5
|
const engineUrl = new URL('../skills/dsh-ppt/scripts/deck-advanced.mjs', import.meta.url);
|
|
6
6
|
enginePromise = import(engineUrl.href).then((module) => module)
|
package/lib/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface PptPluginContext {
|
|
|
16
16
|
warn?(message: string): void;
|
|
17
17
|
};
|
|
18
18
|
on?(event: string, listener: () => void): () => void;
|
|
19
|
+
inject?(services: string[], callback: (ctx: any) => void): unknown;
|
|
19
20
|
}
|
|
20
21
|
export declare function apply(ctx: PptPluginContext, config?: Config): void;
|
|
21
22
|
export { resolvePptConfig, PPT_OUTPUT_DIR_ENV, DEFAULT_MAX_SLIDES, clampInt } from './config.js';
|
package/lib/index.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { resolvePptConfig } from './config.js';
|
|
19
19
|
import { registerPptSkill } from './skill.js';
|
|
20
20
|
import { buildPptTools } from './tools.js';
|
|
21
|
+
import { PptWorkbench, installPptWorkbench } from './workbench.js';
|
|
21
22
|
/** cordis 服务注入:apply 里要使用 ctx.tools 与 ctx.skills。 */
|
|
22
23
|
export const inject = ['tools', 'skills'];
|
|
23
24
|
export const name = 'dsh-ppt';
|
|
@@ -25,6 +26,9 @@ export function apply(ctx, config = {}) {
|
|
|
25
26
|
const resolved = resolvePptConfig(config);
|
|
26
27
|
const warn = (message) => { ctx.logger?.warn?.(message); };
|
|
27
28
|
const disposers = [];
|
|
29
|
+
const workbench = typeof ctx.inject === 'function' ? new PptWorkbench() : undefined;
|
|
30
|
+
if (workbench)
|
|
31
|
+
installPptWorkbench(ctx, workbench);
|
|
28
32
|
// 技能注册:单个技能文件缺失只告警,不弄崩宿主启动。
|
|
29
33
|
try {
|
|
30
34
|
disposers.push(registerPptSkill(ctx));
|
|
@@ -33,7 +37,17 @@ export function apply(ctx, config = {}) {
|
|
|
33
37
|
warn('[dsh-ppt] 技能加载失败:' + (error instanceof Error ? error.message : String(error)));
|
|
34
38
|
}
|
|
35
39
|
for (const definition of buildPptTools(resolved)) {
|
|
36
|
-
|
|
40
|
+
const execute = definition.execute;
|
|
41
|
+
disposers.push(ctx.tools.register({ ...definition, async execute(args, exec) {
|
|
42
|
+
const result = await execute(args, exec);
|
|
43
|
+
try {
|
|
44
|
+
await workbench?.remember(result, args, exec);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
warn('[dsh-ppt] 项目索引未更新:' + (error instanceof Error ? error.message : String(error)));
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
} }));
|
|
37
51
|
}
|
|
38
52
|
if (typeof ctx.on === 'function') {
|
|
39
53
|
ctx.on('dispose', () => {
|
package/lib/tools.js
CHANGED
|
@@ -215,6 +215,7 @@ export function buildPptTools(config) {
|
|
|
215
215
|
description: 'Modify selected slides in an existing dsh-ppt JSON project, preserving other pages and stable IDs. Each edit uses a 1-based slide number or ID and a patch of changed fields. Regenerates the HTML/PPTX/JSON trio atomically, embeds new images, stores undo history. Use ppt_check first and expectedRevision. 中文:按页码或 ID 修改指定页,其他页不变,支持撤销;可同步调整品牌。',
|
|
216
216
|
parameters: compileParameters({ ...projectParameters,
|
|
217
217
|
edits: { type: 'array', items: { type: 'object', properties: { slide: { type: ['integer', 'string'], description: '1-based page number or stable slide ID' }, patch: { type: 'object', properties: slideProperties, additionalProperties: false } }, required: ['slide', 'patch'], additionalProperties: false } },
|
|
218
|
+
order: { type: 'array', items: { type: 'string' }, description: 'Reorder pages using every stable slide ID exactly once. May be used alone or together with edits; one undo restores the previous content and order.' },
|
|
218
219
|
brand: { type: ['object', 'null'], properties: brandProperties, additionalProperties: false, description: 'Merge shared branding. null clears all branding. Empty footer clears footer; logo:null clears logo.' },
|
|
219
220
|
}),
|
|
220
221
|
output: { schema: createResultSchema, render: (_args, value) => renderCreate(value) },
|
|
@@ -238,7 +239,7 @@ export function buildPptTools(config) {
|
|
|
238
239
|
},
|
|
239
240
|
{
|
|
240
241
|
name: 'ppt_render',
|
|
241
|
-
description: 'Read the final PPTX and render all pages to PNG, PDF, or both using optional official @deepseek-ai/libreoffice-kit@0.1.
|
|
242
|
+
description: 'Read the final PPTX and render all pages to PNG, PDF, or both using optional official @deepseek-ai/libreoffice-kit@0.1.5. Returns unavailable plus installation guidance when absent; ordinary deck creation remains usable. Retains original editable PPTX and creates a separate unique output folder and integrity receipt. Reports missing fonts. Inspect actual pages before delivery. 中文:把最终 PPTX 渲染为逐页图片和 PDF,报告缺失字体;需安装可选官方引擎,渲染成功仍需检查画面。',
|
|
242
243
|
parameters: compileParameters({
|
|
243
244
|
pptxPath: { type: 'string', required: true, description: 'Final .pptx path, absolute or relative to session cwd.' },
|
|
244
245
|
outputDir: { type: 'string', description: 'Base output folder. Existing folders are preserved; creates a unique sibling when needed. Default <deck>-render.' },
|
package/lib/types.d.ts
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** UI messages are independent of the deck's language; slide content is never translated. */
|
|
2
|
+
const messages = {
|
|
3
|
+
'文件不是普通文件或超过 64 MiB': 'The file is not a regular file or exceeds 64 MiB.',
|
|
4
|
+
'项目索引超过 8 MiB,请备份后整理 projects.jsonl': 'The project index exceeds 8 MiB. Back it up before reviewing projects.jsonl.',
|
|
5
|
+
'项目索引已满,请备份后整理 projects.jsonl': 'The project index is full. Back it up before reviewing projects.jsonl.',
|
|
6
|
+
'请选择已有项目': 'Select an existing project.',
|
|
7
|
+
'项目不在本机索引中,请先在对话中创建或检查演示文稿': 'This project is not in the local index. Create it or run ppt_check in chat first.',
|
|
8
|
+
'项目位置已改变,请在对话中重新检查': 'The project location changed. Run ppt_check in chat again.',
|
|
9
|
+
'项目已被修改,请重新打开后再编辑;当前输入保留': 'The project changed. Load the latest version before editing; your input is preserved.',
|
|
10
|
+
'项目已被修改,请重新打开后下载': 'The project changed. Open the latest version before downloading.',
|
|
11
|
+
'修改内容无效': 'The edits are invalid.', '文字字段过长或无效': 'A text field is invalid or exceeds 8,000 characters.',
|
|
12
|
+
'要点内容无效': 'Bullets must contain up to 50 lines, each no longer than 2,000 characters.',
|
|
13
|
+
'请选择 PNG 或 JPEG 图片': 'Choose a PNG or JPEG image.',
|
|
14
|
+
'图片必须从面板上传,不能读取其他本地文件': 'Upload the image from this panel. Other local file paths cannot be read.',
|
|
15
|
+
'图片说明或适应方式无效': 'The image description or fit setting is invalid.',
|
|
16
|
+
'不支持此文件类型': 'This file type is unsupported.', '请先渲染当前版本的 PPTX': 'Render the current PPTX version first.',
|
|
17
|
+
'页码无效': 'The slide number is invalid.', '渲染图片已变化,请重新渲染当前 PPTX': 'The rendered image changed. Render the current PPTX again.',
|
|
18
|
+
'项目已被修改,请重新渲染': 'The project changed. Render it again.', '页面不存在': 'This slide no longer exists.',
|
|
19
|
+
'不支持此操作': 'This operation is unsupported.', '请使用 GET': 'Use GET.', '请使用 POST': 'Use POST.',
|
|
20
|
+
'拒绝跨站操作': 'Cross-site operations are refused.', '拒绝跨源操作': 'Cross-origin operations are refused.', '无效来源': 'Invalid request origin.',
|
|
21
|
+
'请从演示文稿面板操作': 'Perform this operation from the presentation panel.', '缺少请求内容': 'The request body is missing.',
|
|
22
|
+
'图片或请求超过 12 MiB,请压缩后重试': 'The image or request exceeds 12 MiB. Reduce its size and try again.', '请求必须是对象': 'The request must be an object.',
|
|
23
|
+
};
|
|
24
|
+
export function englishWorkbenchMessage(message) {
|
|
25
|
+
if (messages[message])
|
|
26
|
+
return messages[message];
|
|
27
|
+
if (/修订号冲突|已被修改|正在被另一次|其他操作修改/.test(message))
|
|
28
|
+
return 'The project changed or is busy. Load the latest version and review your edits before retrying.';
|
|
29
|
+
if (/渲染引擎不可用/.test(message))
|
|
30
|
+
return 'The optional PPTX renderer is unavailable. Install the supported LibreOffice Kit version and restart DSH.';
|
|
31
|
+
if (/渲染超过|超时/.test(message))
|
|
32
|
+
return 'PPTX rendering timed out. Try a smaller presentation and check the renderer logs.';
|
|
33
|
+
if (/面板不支持修改此字段/.test(message))
|
|
34
|
+
return 'This field cannot be edited from the panel.';
|
|
35
|
+
if (/没有可撤销|无法撤销|历史记录为空/.test(message))
|
|
36
|
+
return 'No change is available to undo.';
|
|
37
|
+
if (/没有.*图片|超过.*MiB|图片.*无效/.test(message))
|
|
38
|
+
return 'The image is invalid or too large. Choose another PNG or JPEG image.';
|
|
39
|
+
return /[\u3400-\u9fff]/.test(message) ? 'The operation failed. Check the project files and DSH logs, then try again.' : message;
|
|
40
|
+
}
|
|
41
|
+
const quality = {
|
|
42
|
+
'missing-title': ['This slide has no title.', 'Add a title that explains the slide.'],
|
|
43
|
+
'title-overflow': ['The title may wrap too much or exceed its area.', 'Shorten the title and move explanations into the body.'],
|
|
44
|
+
'dense-bullets': ['This slide has too many bullets for comfortable reading.', 'Reduce the bullets or split them across slides.'],
|
|
45
|
+
'body-overflow': ['The body may exceed the available height.', 'Shorten or split the text before reducing font size.'],
|
|
46
|
+
'template-unfilled': ['Template placeholders remain.', 'Fill in actual facts, evidence and actions, then check again.'],
|
|
47
|
+
'small-font': ['This table uses a small font and may be hard to read.', 'Reduce the columns or use a chart; check readability after export.'],
|
|
48
|
+
'table-density': ['Long table cells may cause crowded wrapping.', 'Shorten the cells or reduce the columns.'],
|
|
49
|
+
'missing-image': ['The embedded image is missing.', 'Edit the slide with a new image.'],
|
|
50
|
+
'invalid-image': ['The embedded image is invalid.', 'Replace it with a valid local image.'],
|
|
51
|
+
'low-resolution': ['The image may look blurry when enlarged.', 'Use a higher-resolution image. Small logos may be acceptable.'],
|
|
52
|
+
'missing-alt': ['The image has no description.', 'Fill in image.alt for accessibility and context.'],
|
|
53
|
+
'invalid-chart': ['The chart data is invalid.', 'Check the categories, series and numeric values.'],
|
|
54
|
+
'chart-density': ['Chart categories or labels may overlap.', 'Reduce categories or use a horizontal bar chart.'],
|
|
55
|
+
'chart-legend': ['Chart legend labels are long.', 'Shorten series names and check the render.'],
|
|
56
|
+
'low-contrast': ['Body text and background contrast is below 4.5:1.', 'Adjust the brand background or text colour.'],
|
|
57
|
+
'missing-logo': ['The brand logo asset is missing.', 'Provide the logo again.'],
|
|
58
|
+
'invalid-logo': ['The brand logo asset is invalid.', 'Replace the logo with a valid image.'],
|
|
59
|
+
};
|
|
60
|
+
export function bilingualQuality(report) {
|
|
61
|
+
return { ...report, issues: report.issues?.map(issue => {
|
|
62
|
+
const entry = quality[issue.code];
|
|
63
|
+
return { ...issue, messageEn: entry?.[0] ?? englishWorkbenchMessage(issue.message || ''), suggestionEn: entry?.[1] ?? englishWorkbenchMessage(issue.suggestion || '') };
|
|
64
|
+
}) };
|
|
65
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type PptExecution } from './execution.js';
|
|
2
|
+
import type { DeckEngine } from './types.js';
|
|
3
|
+
export declare const PPT_WORKBENCH_ROUTE = "/api/dsh-ppt/workbench";
|
|
4
|
+
export declare const PPT_DOWNLOAD_ROUTE = "/api/dsh-ppt/download";
|
|
5
|
+
type WorkbenchEngine = DeckEngine & {
|
|
6
|
+
readDeckProject(options: Record<string, unknown>): {
|
|
7
|
+
manifest: any;
|
|
8
|
+
path: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
type Options = {
|
|
12
|
+
catalogPath?: string;
|
|
13
|
+
loadEngine?: () => Promise<WorkbenchEngine>;
|
|
14
|
+
};
|
|
15
|
+
export declare class PptWorkbench {
|
|
16
|
+
readonly catalogPath: string;
|
|
17
|
+
private readonly loadEngine;
|
|
18
|
+
private readonly renders;
|
|
19
|
+
constructor(options?: Options);
|
|
20
|
+
private catalog;
|
|
21
|
+
/** Recording a completed tool never turns a successful generation into an error. */
|
|
22
|
+
remember(result: any, args: unknown, exec?: PptExecution): Promise<void>;
|
|
23
|
+
private project;
|
|
24
|
+
private view;
|
|
25
|
+
private revision;
|
|
26
|
+
private patch;
|
|
27
|
+
action(body: Record<string, unknown>, signal?: AbortSignal): Promise<unknown>;
|
|
28
|
+
fetchDownload(request: Request): Promise<Response>;
|
|
29
|
+
fetch(request: Request): Promise<Response>;
|
|
30
|
+
}
|
|
31
|
+
export declare function installPptWorkbench(ctx: any, workbench: PptWorkbench): void;
|
|
32
|
+
export {};
|