dsh-ppt-studio 1.1.1 → 1.1.4
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/README.md +41 -18
- package/agent-presets/ppt/agent.cordis.yml +22 -15
- package/cordis.patch.yml +19 -14
- package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +1 -1
- package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +9 -5
- package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +153 -0
- package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +34 -3
- package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +5 -3
- package/examples/fx/fx-pro/preview/04_pages_04_chart.html +1 -1
- package/examples/fx/fx-pro/preview/deck.html +1 -1
- package/examples/fx/fx-pro/preview/measured.json +1 -1
- package/examples/fx/fx-pro.pptx +0 -0
- package/examples/fx/seed.pptx +0 -0
- package/examples/fx/single.pptx +0 -0
- package/examples/fx/spliced.pptx +0 -0
- package/examples/smoke/imported/deck.yaml +6 -6
- package/examples/smoke/imported/import-styles.json +1 -33
- package/examples/smoke/imported/loss-audit.json +99 -0
- package/examples/smoke/imported/pages/slide_03.yaml +5 -36
- package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -1
- package/examples/smoke/imported/preview-rt/deck.html +1 -1
- package/examples/smoke/imported/preview-rt/layout.json +26 -78
- package/examples/smoke/imported/reference/previews/03.png +0 -0
- package/examples/smoke/imported/source.pptx +0 -0
- package/examples/smoke/out-smoke.pptx +0 -0
- package/examples/smoke/preview/03_pages_03_chart.html +1 -1
- package/examples/smoke/preview/deck.html +1 -1
- package/lib/capabilities.js +189 -0
- package/lib/commands.js +12 -11
- package/lib/home.js +32 -0
- package/lib/index.js +278 -112
- package/lib/office-qa.js +111 -0
- package/lib/pptd/export-pptx.js +203 -13
- package/lib/pptd/import-pptx.js +12 -0
- package/lib/pptd/loss-audit.js +363 -0
- package/lib/pptd/nativeChart.js +454 -0
- package/lib/pptd/schema.js +48 -1
- package/lib/pptd/svgCharts.js +180 -37
- package/lib/pptxPy.js +96 -11
- package/lib/preset-delivery.js +107 -9
- package/lib/preview-server.js +41 -16
- package/lib/router.js +3 -2
- package/lib/scaffold.js +1 -1
- package/lib/state.js +8 -6
- package/lib/templates.js +9 -6
- package/lib/tools.js +102 -25
- package/lib/verify.js +12 -1
- package/package.json +2 -1
- package/scripts/audit-manual-facts.mjs +4 -2
- package/scripts/e2e-1.0.mjs +2 -2
- package/scripts/fixtures/workflow-baseline.json +10 -10
- package/scripts/install.mjs +13 -11
- package/scripts/smoke.mjs +473 -66
- package/scripts/verify-loss-audit.mjs +245 -0
- package/scripts/verify-native-charts.mjs +529 -0
- package/scripts/verify-preset-scope.mjs +593 -0
- package/skills/ppt-studio-data/SKILL.md +5 -7
- package/skills/ppt-studio-manual/SKILL.md +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
<!doctype html><html><head><meta charset="utf-8"><title>季度复盘演示(冒烟)</title><style>*{box-sizing:border-box;margin:0}body{background:#64748b} .slide{position:relative;width:960px;height:540px;background:#F8FAFC;box-shadow:0 2px 12px rgba(0,0,0,.3);overflow:hidden}</style><style>body{background:#334155;padding:16px}</style></head><body style="background:#334155"><section style="margin:0 auto 24px;width:960px;padding:4px 0"><div style="font:12px monospace;color:#e2e8f0;padding:2px 8px">1. pages/01_cover</div><div class="slide" style='background:#F8FAFC'><div class="el" id="hero-rect" data-kind="shape" data-shape="rect" style="position:absolute;left:0px;top:0px;width:960px;height:120px;background:#2563EB"></div><div class="el" id="title" data-kind="text" style="position:absolute;left:80px;top:180px;width:700px;height:90px;font-size:40px;line-height:1.2;color:#1F2937;font-weight:bold;overflow:visible;white-space:pre-wrap;word-break:break-word">季度复盘:增长引擎与风险</div><div class="el" id="subtitle" data-kind="text" style="position:absolute;left:80px;top:290px;width:600px;height:60px;font-size:18px;line-height:1.5;color:#F59E0B;overflow:visible;white-space:pre-wrap;word-break:break-word">2026 Q3 · 内部汇报</div><div class="el" id="deco" data-kind="shape" data-shape="ellipse" style="position:absolute;left:780px;top:360px;width:120px;height:120px;border-radius:50%;background:#F59E0B"></div></div></section><section style="margin:0 auto 24px;width:960px;padding:4px 0"><div style="font:12px monospace;color:#e2e8f0;padding:2px 8px">2. pages/02_content</div><div class="slide" style='background:#F8FAFC'><div class="el" id="page-title" data-kind="text" style="position:absolute;left:60px;top:40px;width:840px;height:50px;font-size:28px;line-height:1.2;color:#1F2937;font-weight:bold;overflow:visible;white-space:pre-wrap;word-break:break-word">关键进展</div><div class="el" id="body-a" data-kind="text" style="position:absolute;left:60px;top:130px;width:420px;height:200px;font-size:18px;line-height:1.5;color:#1F2937;overflow:visible;white-space:pre-wrap;word-break:break-word">线上增长 34%,主要来自新渠道投放;NPS 提升 12 分;复购率升至 41%。供应链成本环比下降 8%。</div><div class="el" id="metric-rect" data-kind="shape" data-shape="rect" style="position:absolute;left:540px;top:130px;width:360px;height:80px;background:#2563EB"></div><div class="el" id="metric" data-kind="text" style="position:absolute;left:540px;top:150px;width:360px;height:40px;font-size:22px;line-height:1.2;color:#FFFFFF;font-weight:bold;text-align:center;overflow:visible;white-space:pre-wrap;word-break:break-word">+34% 增长</div><table class="el" id="table-b" data-kind="table" style="position:absolute;left:60px;top:380px;width:840px;height:120px;border-collapse:collapse"><tbody><tr><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">渠道</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">投入</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">产出</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">ROI</td></tr><tr><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">搜索</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">120万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">480万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">4</td></tr><tr><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">社群</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">60万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">210万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">3.5</td></tr><tr><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">直播</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">90万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">360万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">4</td></tr></tbody></table></div></section><section style="margin:0 auto 24px;width:960px;padding:4px 0"><div style="font:12px monospace;color:#e2e8f0;padding:2px 8px">3. pages/03_chart</div><div class="slide" style='background:#F8FAFC'><div class="el" id="page-title" data-kind="text" style="position:absolute;left:60px;top:40px;width:840px;height:50px;font-size:28px;line-height:1.2;color:#1F2937;font-weight:bold;overflow:visible;white-space:pre-wrap;word-break:break-word">渠道 GMV 走势</div><div class="el" id="chart-c" data-kind="chart" style="position:absolute;left:60px;top:130px;width:840px;height:330px"><svg viewBox="0 0 840 330" width="840" height="330" xmlns="http://www.w3.org/2000/svg" style="display:block"><line x1="36" y1="12" x2="828" y2="12" stroke="#e2e8f0" stroke-width="1"/><text x="
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"><title>季度复盘演示(冒烟)</title><style>*{box-sizing:border-box;margin:0}body{background:#64748b} .slide{position:relative;width:960px;height:540px;background:#F8FAFC;box-shadow:0 2px 12px rgba(0,0,0,.3);overflow:hidden}</style><style>body{background:#334155;padding:16px}</style></head><body style="background:#334155"><section style="margin:0 auto 24px;width:960px;padding:4px 0"><div style="font:12px monospace;color:#e2e8f0;padding:2px 8px">1. pages/01_cover</div><div class="slide" style='background:#F8FAFC'><div class="el" id="hero-rect" data-kind="shape" data-shape="rect" style="position:absolute;left:0px;top:0px;width:960px;height:120px;background:#2563EB"></div><div class="el" id="title" data-kind="text" style="position:absolute;left:80px;top:180px;width:700px;height:90px;font-size:40px;line-height:1.2;color:#1F2937;font-weight:bold;overflow:visible;white-space:pre-wrap;word-break:break-word">季度复盘:增长引擎与风险</div><div class="el" id="subtitle" data-kind="text" style="position:absolute;left:80px;top:290px;width:600px;height:60px;font-size:18px;line-height:1.5;color:#F59E0B;overflow:visible;white-space:pre-wrap;word-break:break-word">2026 Q3 · 内部汇报</div><div class="el" id="deco" data-kind="shape" data-shape="ellipse" style="position:absolute;left:780px;top:360px;width:120px;height:120px;border-radius:50%;background:#F59E0B"></div></div></section><section style="margin:0 auto 24px;width:960px;padding:4px 0"><div style="font:12px monospace;color:#e2e8f0;padding:2px 8px">2. pages/02_content</div><div class="slide" style='background:#F8FAFC'><div class="el" id="page-title" data-kind="text" style="position:absolute;left:60px;top:40px;width:840px;height:50px;font-size:28px;line-height:1.2;color:#1F2937;font-weight:bold;overflow:visible;white-space:pre-wrap;word-break:break-word">关键进展</div><div class="el" id="body-a" data-kind="text" style="position:absolute;left:60px;top:130px;width:420px;height:200px;font-size:18px;line-height:1.5;color:#1F2937;overflow:visible;white-space:pre-wrap;word-break:break-word">线上增长 34%,主要来自新渠道投放;NPS 提升 12 分;复购率升至 41%。供应链成本环比下降 8%。</div><div class="el" id="metric-rect" data-kind="shape" data-shape="rect" style="position:absolute;left:540px;top:130px;width:360px;height:80px;background:#2563EB"></div><div class="el" id="metric" data-kind="text" style="position:absolute;left:540px;top:150px;width:360px;height:40px;font-size:22px;line-height:1.2;color:#FFFFFF;font-weight:bold;text-align:center;overflow:visible;white-space:pre-wrap;word-break:break-word">+34% 增长</div><table class="el" id="table-b" data-kind="table" style="position:absolute;left:60px;top:380px;width:840px;height:120px;border-collapse:collapse"><tbody><tr><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">渠道</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">投入</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">产出</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">ROI</td></tr><tr><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">搜索</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">120万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">480万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">4</td></tr><tr><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">社群</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">60万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">210万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">3.5</td></tr><tr><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">直播</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">90万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">360万</td><td style="border:1px solid #cbd5e1;padding:4px 8px;font-size:11px">4</td></tr></tbody></table></div></section><section style="margin:0 auto 24px;width:960px;padding:4px 0"><div style="font:12px monospace;color:#e2e8f0;padding:2px 8px">3. pages/03_chart</div><div class="slide" style='background:#F8FAFC'><div class="el" id="page-title" data-kind="text" style="position:absolute;left:60px;top:40px;width:840px;height:50px;font-size:28px;line-height:1.2;color:#1F2937;font-weight:bold;overflow:visible;white-space:pre-wrap;word-break:break-word">渠道 GMV 走势</div><div class="el" id="chart-c" data-kind="chart" style="position:absolute;left:60px;top:130px;width:840px;height:330px"><svg viewBox="0 0 840 330" width="840" height="330" xmlns="http://www.w3.org/2000/svg" style="display:block"><line x1="36" y1="12.0" x2="828.0" y2="12.0" stroke="#e2e8f0" stroke-width="1"/><line x1="33" y1="12.0" x2="36" y2="12.0" stroke="#cbd5e1" stroke-width="1"/><text x="31" y="15.0" font-size="9" fill="#64748b" text-anchor="end">420</text><line x1="36" y1="80.5" x2="828.0" y2="80.5" stroke="#e2e8f0" stroke-width="1"/><line x1="33" y1="80.5" x2="36" y2="80.5" stroke="#cbd5e1" stroke-width="1"/><text x="31" y="83.5" font-size="9" fill="#64748b" text-anchor="end">315</text><line x1="36" y1="149.0" x2="828.0" y2="149.0" stroke="#e2e8f0" stroke-width="1"/><line x1="33" y1="149.0" x2="36" y2="149.0" stroke="#cbd5e1" stroke-width="1"/><text x="31" y="152.0" font-size="9" fill="#64748b" text-anchor="end">210</text><line x1="36" y1="217.5" x2="828.0" y2="217.5" stroke="#e2e8f0" stroke-width="1"/><line x1="33" y1="217.5" x2="36" y2="217.5" stroke="#cbd5e1" stroke-width="1"/><text x="31" y="220.5" font-size="9" fill="#64748b" text-anchor="end">105</text><line x1="36" y1="286.0" x2="828.0" y2="286.0" stroke="#e2e8f0" stroke-width="1"/><line x1="33" y1="286.0" x2="36" y2="286.0" stroke="#cbd5e1" stroke-width="1"/><text x="31" y="289.0" font-size="9" fill="#64748b" text-anchor="end">0</text><line x1="36" y1="12" x2="36" y2="286.0" stroke="#cbd5e1" stroke-width="1"/><line x1="36" y1="286.0" x2="828.0" y2="286.0" stroke="#cbd5e1" stroke-width="1"/><rect x="74.9" y="77.2" width="92.4" height="208.8" fill="#2563EB"><title>Web · 7月: 320</title></rect><rect x="338.9" y="51.1" width="92.4" height="234.9" fill="#2563EB"><title>Web · 8月: 360</title></rect><rect x="602.8" y="12.0" width="92.4" height="274.0" fill="#2563EB"><title>Web · 9月: 420</title></rect><rect x="168.8" y="149.0" width="92.4" height="137.0" fill="#F59E0B"><title>App · 7月: 210</title></rect><rect x="432.8" y="129.4" width="92.4" height="156.6" fill="#F59E0B"><title>App · 8月: 240</title></rect><rect x="696.8" y="90.3" width="92.4" height="195.7" fill="#F59E0B"><title>App · 9月: 300</title></rect><text x="168.0" y="298.0" font-size="9" fill="#475569" text-anchor="middle">7月</text><line x1="168.0" y1="286.0" x2="168.0" y2="289.0" stroke="#cbd5e1" stroke-width="1"/><text x="432.0" y="298.0" font-size="9" fill="#475569" text-anchor="middle">8月</text><line x1="432.0" y1="286.0" x2="432.0" y2="289.0" stroke="#cbd5e1" stroke-width="1"/><text x="696.0" y="298.0" font-size="9" fill="#475569" text-anchor="middle">9月</text><line x1="696.0" y1="286.0" x2="696.0" y2="289.0" stroke="#cbd5e1" stroke-width="1"/><rect x="8" y="319.0" width="8" height="8" fill="#2563EB" rx="1"/><text x="22" y="326.0" font-size="9" fill="#64748b">Web</text><rect x="44.85" y="319.0" width="8" height="8" fill="#F59E0B" rx="1"/><text x="58.85" y="326.0" font-size="9" fill="#64748b">App</text></svg></div><div class="el" id="caption" data-kind="text" style="position:absolute;left:60px;top:480px;width:840px;height:30px;font-size:12px;line-height:1.2;color:#64748B;overflow:visible;white-space:pre-wrap;word-break:break-word">数据来源:内部经营报表(2026-09 月报)</div></div></section></body></html>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 部署能力探测(2026-09-26,第二轮)——把"这台机器上有哪些增强通道可用"收敛到**一处**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:官方三个 office 技能(`office-docx/pptx/xlsx`)与它们假定的运行时(捆绑 Python +
|
|
5
|
+
* LibreOffice kit)**只在桌面端具备**(`dsh-skill-office` 由 `dsh-desktop-host` 程序化挂载;
|
|
6
|
+
* `web`/`headless`/`acp` 的出厂组合不挂,npm 版也没有 Python 分发)。于是插件必须**先探测再说话**:
|
|
7
|
+
* · python-pptx 兜底引擎选解释器(优先捆绑 Python,而不是指望用户 PATH 上恰好有);
|
|
8
|
+
* · 兜底产物是否接官方 QA(`check_office.py` 结构自证 + LibreOffice 渲染抽检);
|
|
9
|
+
* · `ppt_state.capabilities` 如实报告"有/没有"——绝不指引模型调用不存在的工具或技能。
|
|
10
|
+
*
|
|
11
|
+
* **三套根要分清**(2026-09-26 宿主实测结论,别混用):
|
|
12
|
+
* ① 安装副本:`<dshHome>/dsh-runtimes/dsh-primary-runtime`(桌面端把 payload 复制到这里;
|
|
13
|
+
* `load_workspace_dependencies` 返回给模型的也是这一套);
|
|
14
|
+
* ② carrier:`<install>/resources/runtime/primary-runtime`(桌面应用自带的原件);
|
|
15
|
+
* ③ asar 解包:`<install>/resources/app.asar.unpacked/...`(原生件,如 LibreOffice kit)。
|
|
16
|
+
*
|
|
17
|
+
* 探测原则:**只做文件存在性检查**(廉价、无副作用、不 spawn);解释器的可用性由调用方
|
|
18
|
+
* 一次性验证(`pptxPy.js` 的 `findPython()`)。所有探测函数**每次调用求值**(能力可能在会话中途出现,
|
|
19
|
+
* 例如宿主装好 runtime 之后),并返回 `null` 而不是抛错。
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from 'node:fs'
|
|
22
|
+
import { spawnSync } from 'node:child_process'
|
|
23
|
+
import { dirname, join } from 'node:path'
|
|
24
|
+
import { dshHome } from './home.js'
|
|
25
|
+
|
|
26
|
+
/** 官方三技能的名字(用于在技能注册表里探测"托管端是否提供")。 */
|
|
27
|
+
export const OFFICE_SKILL_NAMES = ['office-docx', 'office-pptx', 'office-xlsx']
|
|
28
|
+
|
|
29
|
+
/** 是否 Windows(捆绑 Python/Kit 的路径形状只对 Windows 做过实测)。 */
|
|
30
|
+
function isWin() {
|
|
31
|
+
return process.platform === 'win32'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 从某个目录**逐级上溯**找桌面应用的 `resources` 目录(用 `app.asar` / `runtime/office-skills` 当锚点)。
|
|
36
|
+
* 为什么不只看 `dirname(process.execPath)`:桌面端里本插件可能跑在**子进程 node** 中
|
|
37
|
+
* (`resources/runtime/primary-runtime/dependencies/node/bin/node.exe`),那时 execPath 在深层,
|
|
38
|
+
* 必须一路上溯才能找到 `<install>/resources`。找不到返回 null(web/CLI 上就该是 null)。
|
|
39
|
+
*/
|
|
40
|
+
export function resourcesFrom(startDir) {
|
|
41
|
+
// 显式覆盖缝(测试/非常规布局):`PPT_STUDIO_RESOURCES_DIR=<install>/resources`。
|
|
42
|
+
// 只用它来**定位**资源,不改变任何判定语义;指向不合法时忽略并继续正常上溯。
|
|
43
|
+
const override = process.env.PPT_STUDIO_RESOURCES_DIR
|
|
44
|
+
if (typeof override === 'string' && override.trim() !== '') {
|
|
45
|
+
const cand = join(override)
|
|
46
|
+
if (existsSync(join(cand, 'app.asar')) || existsSync(join(cand, 'app.asar.unpacked')) || existsSync(join(cand, 'runtime', 'office-skills'))) return cand
|
|
47
|
+
}
|
|
48
|
+
let dir = startDir
|
|
49
|
+
for (let i = 0; i < 8; i++) {
|
|
50
|
+
const cand = join(dir, 'resources')
|
|
51
|
+
if (existsSync(join(cand, 'app.asar')) || existsSync(join(cand, 'app.asar.unpacked')) || existsSync(join(cand, 'runtime', 'office-skills'))) {
|
|
52
|
+
return cand
|
|
53
|
+
}
|
|
54
|
+
const parent = dirname(dir)
|
|
55
|
+
if (parent === dir) break
|
|
56
|
+
dir = parent
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 桌面应用的 `resources` 目录(`process.execPath` 起逐级上溯;非桌面部署返回 null)。 */
|
|
62
|
+
export function resourcesDir() {
|
|
63
|
+
const exe = process.execPath || ''
|
|
64
|
+
if (!exe) return null
|
|
65
|
+
return resourcesFrom(dirname(exe))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 环境变量缝(SDK/容器部署用;桌面端不设这两个变量,走 carrier 推导)。 */
|
|
69
|
+
export function primaryRuntimeFromEnv() {
|
|
70
|
+
return process.env.DSH_PRIMARY_RUNTIME || process.env.DSH_BUNDLED_PRIMARY_RUNTIME || null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** primary-runtime 的候选根(按"模型看到的 → 应用自带的"排序)。 */
|
|
74
|
+
export function primaryRuntimeRoots() {
|
|
75
|
+
const out = []
|
|
76
|
+
const env = primaryRuntimeFromEnv()
|
|
77
|
+
if (env) out.push({ root: env, source: 'env(DSH_PRIMARY_RUNTIME)' })
|
|
78
|
+
out.push({ root: join(dshHome(), 'dsh-runtimes', 'dsh-primary-runtime'), source: 'installed(dsh-runtimes)' })
|
|
79
|
+
const res = resourcesDir()
|
|
80
|
+
if (res) out.push({ root: join(res, 'runtime', 'primary-runtime'), source: 'carrier(resources/runtime)' })
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 捆绑 Python 的候选(按上表顺序;`python.exe` 是 Windows 布局,其他平台给出 posix 形状)。 */
|
|
85
|
+
export function pythonCandidates() {
|
|
86
|
+
const exe = isWin() ? 'python.exe' : join('bin', 'python3')
|
|
87
|
+
return primaryRuntimeRoots().map(({ root, source }) => ({
|
|
88
|
+
path: join(root, 'dependencies', 'python', exe),
|
|
89
|
+
node: join(root, 'dependencies', 'node', isWin() ? 'bin/node.exe' : 'bin/node'),
|
|
90
|
+
pnpm: join(root, 'dependencies', 'pnpm', isWin() ? 'bin/pnpm.mjs' : 'bin/pnpm.cjs'),
|
|
91
|
+
root,
|
|
92
|
+
source,
|
|
93
|
+
}))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 第一个真实存在的捆绑 Python(没有则 null)。 */
|
|
97
|
+
export function findBundledPython() {
|
|
98
|
+
for (const cand of pythonCandidates()) if (existsSync(cand.path)) return cand
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 捆绑 Node(桌面端给 LibreOffice kit 用;没有则 null)。 */
|
|
103
|
+
export function findBundledNode() {
|
|
104
|
+
for (const cand of pythonCandidates()) if (existsSync(cand.node)) return cand.node
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** LibreOffice kit CLI(官方 office 技能渲染/转 PDF/重算公式用的那一个);探测不到返回 null。 */
|
|
109
|
+
export function findLibreofficeCli() {
|
|
110
|
+
const res = resourcesDir()
|
|
111
|
+
const cands = []
|
|
112
|
+
if (res) {
|
|
113
|
+
// 桌面端实测落点:app.asar.unpacked 下的 asar 内依赖
|
|
114
|
+
cands.push(join(res, 'app.asar.unpacked', 'dsh', 'node_modules', '@deepseek-ai', 'libreoffice-kit', 'lib', 'cli.js'))
|
|
115
|
+
cands.push(join(res, 'app.asar.unpacked', 'node_modules', '@deepseek-ai', 'libreoffice-kit', 'lib', 'cli.js'))
|
|
116
|
+
}
|
|
117
|
+
if (res) cands.push(join(res, 'runtime', 'node_modules', '@deepseek-ai', 'libreoffice-kit', 'lib', 'cli.js'))
|
|
118
|
+
return cands.find((p) => existsSync(p)) ?? null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 官方 office 技能的资产根与结构检查脚本(桌面端 `resources/runtime/office-skills`)。 */
|
|
122
|
+
export function findOfficeAssets() {
|
|
123
|
+
const res = resourcesDir()
|
|
124
|
+
if (!res) return null
|
|
125
|
+
const skillsDir = join(res, 'runtime', 'office-skills')
|
|
126
|
+
if (!existsSync(skillsDir)) return null
|
|
127
|
+
const checker = join(skillsDir, 'scripts', 'check_office.py')
|
|
128
|
+
return { skillsDir, checker: existsSync(checker) ? checker : null }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 任意可用的 Python(**只为跑官方 `check_office.py`**:它只用标准库,任何 python3 都行)。
|
|
133
|
+
* 顺序:捆绑 Python > PATH 上的 `python` / `py` / `python3`。探不到返回 null。
|
|
134
|
+
* 与 `pptxPy.findPython()` 的区别:那个要求 `import pptx`,这个不要求。
|
|
135
|
+
*/
|
|
136
|
+
export function findAnyPython() {
|
|
137
|
+
const bundled = findBundledPython()
|
|
138
|
+
if (bundled !== null) return { path: bundled.path, source: `bundled:${bundled.source}` }
|
|
139
|
+
for (const cmd of ['python', 'py', 'python3']) {
|
|
140
|
+
try {
|
|
141
|
+
const r = spawnSync(cmd, ['-c', 'print(1)'], { encoding: 'utf8', timeout: 15000 })
|
|
142
|
+
if (r.status === 0 && String(r.stdout).includes('1')) return { path: cmd, source: 'path' }
|
|
143
|
+
} catch { /* try next */ }
|
|
144
|
+
}
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 汇总探测结果(**纯文件检查**,不 spawn、不抛)。`ppt_state.capabilities` 用它;
|
|
150
|
+
* 兜底引擎与 QA 通道用它决定"能不能走增强路径"。
|
|
151
|
+
*/
|
|
152
|
+
export function detectCapabilities() {
|
|
153
|
+
const py = findBundledPython()
|
|
154
|
+
const node = findBundledNode()
|
|
155
|
+
const cli = findLibreofficeCli()
|
|
156
|
+
const office = findOfficeAssets()
|
|
157
|
+
return {
|
|
158
|
+
platform: process.platform,
|
|
159
|
+
bundledPython: py === null ? null : { path: py.path, source: py.source },
|
|
160
|
+
bundledNode: node,
|
|
161
|
+
libreofficeKit: cli === null ? null : { cli, node },
|
|
162
|
+
officeSkillsAssets: office === null ? null : { skillsDir: office.skillsDir, checker: office.checker },
|
|
163
|
+
primaryRuntime: primaryRuntimeRoots().map(({ root, source }) => ({ root, source, present: existsSync(root) })),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 技能注册表探测:托管的三个 office 技能在本会话里**是否真的可用**(模型侧能不能加载)。
|
|
169
|
+
* 与 `detectCapabilities()` 分开,因为这一条要问服务、是异步的。
|
|
170
|
+
*/
|
|
171
|
+
export async function probeOfficeSkills(ctx) {
|
|
172
|
+
let skills
|
|
173
|
+
try { skills = ctx?.get?.('skills') } catch { skills = undefined }
|
|
174
|
+
if (skills === undefined) return { available: false, found: [], reason: 'skills 服务缺失(极简装配)' }
|
|
175
|
+
const found = []
|
|
176
|
+
for (const name of OFFICE_SKILL_NAMES) {
|
|
177
|
+
try {
|
|
178
|
+
const one = await skills.get(name)
|
|
179
|
+
if (one !== undefined && one !== null) found.push(name)
|
|
180
|
+
} catch { /* 单个技能查询失败不影响其余 */ }
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
available: found.length === OFFICE_SKILL_NAMES.length,
|
|
184
|
+
found,
|
|
185
|
+
reason: found.length === OFFICE_SKILL_NAMES.length
|
|
186
|
+
? '托管端提供(桌面端/SDK 才有)'
|
|
187
|
+
: '本部署未提供这三个技能(web/headless 出厂组合不挂 dsh-skill-office)',
|
|
188
|
+
}
|
|
189
|
+
}
|
package/lib/commands.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* /ppt → 状态查询
|
|
4
4
|
* /ppt on|off → 强制进入/退出工作流
|
|
5
5
|
* /ppt free|mid|strict → 强制协作模式
|
|
6
|
-
* /ppt fidelity strict|auto|free [--page
|
|
6
|
+
* /ppt fidelity strict|auto|free [--dir D] (--page 未实现 → 明确拒绝,见分支内注释)
|
|
7
7
|
* /ppt review none|points|every
|
|
8
8
|
* /ppt engine pptd|python-pptx|pptxgenjs
|
|
9
9
|
* /ppt quality quick|standard|audit
|
|
@@ -59,18 +59,19 @@ async function handle(invocation) {
|
|
|
59
59
|
if (cmd === 'fidelity') {
|
|
60
60
|
const v = (args.split(' ')[0] ?? '').toLowerCase()
|
|
61
61
|
if (!['strict', 'auto', 'free'].includes(v)) return err('fidelity 取值 strict|auto|free')
|
|
62
|
-
state.fidelity = v
|
|
63
|
-
await save()
|
|
64
62
|
const pageN = argValue(args, '--page')
|
|
65
|
-
if (pageN &&
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
if (pageN !== undefined && pageN !== '') {
|
|
64
|
+
// 2026-09-26:`--page` 此前是**空操作却回执"已记录"**(算了 idx 不用、`for (…){ void p }` 空循环、
|
|
65
|
+
// saveProject 无语义写入),而页级 fidelity 在 DSL/schema/项目 state 里**根本不存在**
|
|
66
|
+
// (docs/01 §5 C5:「页级覆盖字段未做(待定)」)。按本文件既有的诚实先例处理:
|
|
67
|
+
// engine 分支对未实现的 pptxgenjs 就是"明确拒绝,不装作成功"。这里一并不再改任何状态。
|
|
68
|
+
return err(`页级 fidelity 未实现:--page ${pageN} 不会改变任何东西(页级字段在 DSL 与 state 里都不存在,见 docs/01 §5 C5)。
|
|
69
|
+
· 现在只有**会话级**:/ppt fidelity ${v}
|
|
70
|
+
· 若确实要"某几页 strict、其余 free",请明确提出——那是新特性(需先定 DSL 字段与门禁语义),不在修复范围。`)
|
|
72
71
|
}
|
|
73
|
-
|
|
72
|
+
state.fidelity = v
|
|
73
|
+
await save()
|
|
74
|
+
return ok(`✓ 会话忠实度=${v}(记录/展示用:当前版本它不改变门禁与流程,只回显在工作流提示与状态行)`)
|
|
74
75
|
}
|
|
75
76
|
if (cmd === 'review') {
|
|
76
77
|
const v = (args.split(' ')[0] ?? '').toLowerCase()
|
package/lib/home.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 用户目录解析 —— 本插件**唯一**的实现。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有这个文件(2026-09-26,方案 B 第二批):
|
|
5
|
+
* 此前口径是分裂的 ——
|
|
6
|
+
* · `templates.js`(用户模板层)与 `index.js`(诊断日志)**调用时求值**、认 `DSH_HOME`;
|
|
7
|
+
* · `state.js`(会话状态)与 `preview-server.js`(预览根)是**模块常量** `homedir()/.dsh`,忽略 `DSH_HOME`。
|
|
8
|
+
* 实测后果两条:
|
|
9
|
+
* ① 设了 `DSH_HOME` 的便携/CI 部署里,会话状态与预览**逃出隔离目录**(写进真实 `~/.dsh`);
|
|
10
|
+
* ② 本机跑 `npm test` 会把 smoke 模板与预览缓存写进开发机**真实**的 `~/.dsh/ppt-studio/` ——
|
|
11
|
+
* 与 smoke 自己那句"不碰开发机/CI 上真实目录"的承诺直接冲突(因此清理过两次真实目录)。
|
|
12
|
+
*
|
|
13
|
+
* 契约(两条都重要):
|
|
14
|
+
* · **每次调用求值**:不能做成模块常量,否则测试/CI 无法用 `DSH_HOME` 覆盖(这也是 templates.js 当初的做法)。
|
|
15
|
+
* · **未设 `DSH_HOME` 时与旧行为等价**(`homedir()/.dsh`)⇒ 普通用户行为零变化、无需迁移。
|
|
16
|
+
*/
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
|
|
20
|
+
/** DSH home:`DSH_HOME` 优先,缺省 `~/.dsh`(Node 的 `homedir()` 在 Windows 认 `USERPROFILE`、POSIX 认 `HOME`)。 */
|
|
21
|
+
export function dshHome() {
|
|
22
|
+
return process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 本插件的用户数据目录:`<dshHome>/ppt-studio`。
|
|
27
|
+
* 会话状态(`session-<id>.json`)、预览根(`preview/<token>/`)、用户模板层(`templates/`)、
|
|
28
|
+
* 诊断日志(`debug.log`)都在这一层下面 —— 全插件共用同一个根,避免再出现口径分裂。
|
|
29
|
+
*/
|
|
30
|
+
export function pptStudioDir() {
|
|
31
|
+
return join(dshHome(), 'ppt-studio')
|
|
32
|
+
}
|