chore: 1.0.1 release
This commit is contained in:
@@ -18,6 +18,15 @@ The .pptd format is a simplified abstraction layer over OOXML that follows basic
|
||||
|
||||
## PPT production workflow
|
||||
|
||||
### step0. Check local prerequisites
|
||||
Default delivery includes PPTX export (and optional `npx open-kimi-ppt-skills serve`), which need a local toolchain. **Before generating**, verify:
|
||||
|
||||
1. **Node.js 18+**: run `node --version`. If `node` is missing or the major version is below 18, **stop immediately**, tell the user to install Node.js 18+ from https://nodejs.org (or their OS package manager), and do not continue with PPTX export / `npx` until it is available. Only continue with PPTD-only output when the user explicitly opts out of PPTX.
|
||||
2. **npm / npx**: run `npm --version`. They ship with Node.js; if missing, treat Node.js as not installed correctly and guide the user to reinstall/fix PATH.
|
||||
3. **python3**: run `python3 --version` (on Windows, `python` may be the correct command). Needed for `export_pptx.py` / `export_images.py`.
|
||||
4. **Chrome / Chromium / Edge**: needed by `agent-browser` for PPTX export and visual QA. If export later fails with a browser-launch error, ask the user to install a Chromium-based browser.
|
||||
5. Soft deps are auto-handled by the scripts when missing: **PyYAML**, **agent-browser** (≥0.33.2 via npm), and for image QA **Pillow** + **websocket-client**. Network access to `www.kimi.com` and `statics.moonshot.cn` is still required at export time.
|
||||
|
||||
### step1. Read the context thoroughly
|
||||
Read **all files uploaded by the user**, the provided URLs, and the pptd format guide `reference/pptd.md` to fully understand the user's requirements.
|
||||
|
||||
@@ -144,8 +153,8 @@ When generating a PPT, adopt different production approaches for different user
|
||||
A project directory may be passed instead of the manifest only when it contains exactly one `.pptd` file.
|
||||
Existing output files are not overwritten unless `--force` is passed.
|
||||
7. Local export requirements and boundaries:
|
||||
- requires `python3`, PyYAML, `npm`, `agent-browser`, Chrome/Chromium, and network access to `www.kimi.com` plus `statics.moonshot.cn`; the image-based visual QA step additionally requires Pillow, auto-installed with `pip --user` when missing;
|
||||
- before browser export, `export_pptx.py` checks `agent-browser --version`; when it is missing or below `0.33.2`, it installs `agent-browser@latest` globally with npm, then verifies the resulting version before continuing;
|
||||
- requires **Node.js 18+** (`node` / `npm` / `npx`), `python3`, a Chromium-based browser, and network access to `www.kimi.com` plus `statics.moonshot.cn`;
|
||||
- before browser export, `export_pptx.py` checks Node.js 18+ and `npm`, then checks `agent-browser --version`; when `agent-browser` is missing or below `0.33.2`, it installs `agent-browser@latest` globally with npm; **PyYAML** is auto-installed with `pip --user` when missing; the image-based visual QA step additionally auto-installs Pillow and websocket-client the same way;
|
||||
- the PPTD document itself is provided to the public editor iframe through the localhost SDK bridge, not uploaded to a server-side PPTX conversion endpoint;
|
||||
- remote images, icons, or fonts referenced by the deck may still be fetched from their respective hosts;
|
||||
- local PNG/JPEG/GIF/SVG files inside the PPTD project are supplied to the iframe as data URLs;
|
||||
|
||||
@@ -28,14 +28,6 @@ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError as exc: # pragma: no cover - environment diagnostic
|
||||
raise SystemExit(
|
||||
"PyYAML is required. Install it with: python3 -m pip install --user pyyaml"
|
||||
) from exc
|
||||
|
||||
|
||||
SKILL_DIR = Path(__file__).resolve().parent.parent
|
||||
HOST_TEMPLATE = Path(__file__).with_name("export_host.html")
|
||||
IMAGE_MIME = {
|
||||
@@ -54,6 +46,8 @@ FADE_TRANSITION_XML = (
|
||||
'<p:transition spd="fast" advClick="1"><p:fade/></p:transition>'
|
||||
)
|
||||
MIN_AGENT_BROWSER_VERSION = (0, 33, 2)
|
||||
MIN_NODE_MAJOR = 18
|
||||
NODE_INSTALL_HINT = "Install Node.js 18+ from https://nodejs.org, then retry."
|
||||
|
||||
|
||||
class ExportError(RuntimeError):
|
||||
@@ -69,6 +63,31 @@ def log(message: str) -> None:
|
||||
print(f"[open-kimi-ppt] {message}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def ensure_pyyaml() -> Any:
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
log("PyYAML is required; installing pyyaml with pip --user")
|
||||
process = subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--user", "pyyaml"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=300,
|
||||
)
|
||||
if process.returncode != 0:
|
||||
raise ExportError(
|
||||
"failed to install PyYAML with pip --user:\n"
|
||||
f"{process.stdout[-2000:]}\n"
|
||||
"Install it manually with: python3 -m pip install --user pyyaml"
|
||||
)
|
||||
import yaml
|
||||
return yaml
|
||||
|
||||
|
||||
yaml = ensure_pyyaml()
|
||||
|
||||
|
||||
def parse_version(output: str) -> Tuple[int, int, int]:
|
||||
match = re.search(r"(\d+)\.(\d+)\.(\d+)\b", output)
|
||||
if not match:
|
||||
@@ -76,6 +95,13 @@ def parse_version(output: str) -> Tuple[int, int, int]:
|
||||
return tuple(int(part) for part in match.groups())
|
||||
|
||||
|
||||
def parse_node_version(output: str) -> Tuple[int, int, int]:
|
||||
match = re.search(r"v?(\d+)\.(\d+)\.(\d+)\b", output)
|
||||
if not match:
|
||||
raise ExportError(f"could not parse Node.js version from: {output.strip()}")
|
||||
return tuple(int(part) for part in match.groups())
|
||||
|
||||
|
||||
def read_agent_browser_version(executable: str) -> Tuple[int, int, int]:
|
||||
process = subprocess.run(
|
||||
[executable, "--version"],
|
||||
@@ -89,7 +115,42 @@ def read_agent_browser_version(executable: str) -> Tuple[int, int, int]:
|
||||
return parse_version(process.stdout)
|
||||
|
||||
|
||||
def ensure_nodejs() -> str:
|
||||
executable = shutil.which("node")
|
||||
if not executable:
|
||||
raise ExportError(f"Node.js is not installed or not on PATH. {NODE_INSTALL_HINT}")
|
||||
|
||||
process = subprocess.run(
|
||||
[executable, "--version"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=30,
|
||||
)
|
||||
if process.returncode != 0:
|
||||
raise ExportError(f"node --version failed:\n{process.stdout[-2000:]}")
|
||||
|
||||
version = parse_node_version(process.stdout)
|
||||
if version[0] < MIN_NODE_MAJOR:
|
||||
raise ExportError(
|
||||
f"Node.js {MIN_NODE_MAJOR}+ is required; found "
|
||||
f"{'.'.join(map(str, version))} ({process.stdout.strip()}). {NODE_INSTALL_HINT}"
|
||||
)
|
||||
|
||||
npm = shutil.which("npm")
|
||||
if not npm:
|
||||
raise ExportError(
|
||||
"npm is not installed or not on PATH. "
|
||||
f"npm ships with Node.js. {NODE_INSTALL_HINT}"
|
||||
)
|
||||
|
||||
log(f"Node.js version: {'.'.join(map(str, version))}")
|
||||
return executable
|
||||
|
||||
|
||||
def ensure_agent_browser() -> str:
|
||||
ensure_nodejs()
|
||||
|
||||
executable = shutil.which("agent-browser")
|
||||
version = read_agent_browser_version(executable) if executable else None
|
||||
if version is not None and version >= MIN_AGENT_BROWSER_VERSION:
|
||||
@@ -100,7 +161,8 @@ def ensure_agent_browser() -> str:
|
||||
if not npm:
|
||||
reason = "not installed" if version is None else ".".join(map(str, version))
|
||||
raise ExportError(
|
||||
f"agent-browser {reason}; npm is required to install agent-browser@latest"
|
||||
f"agent-browser {reason}; npm is required to install agent-browser@latest. "
|
||||
f"npm ships with Node.js. {NODE_INSTALL_HINT}"
|
||||
)
|
||||
|
||||
current = "not installed" if version is None else ".".join(map(str, version))
|
||||
|
||||
@@ -22,17 +22,52 @@ class ExportPptxTests(unittest.TestCase):
|
||||
@patch.object(MODULE.subprocess, "run")
|
||||
@patch.object(MODULE.shutil, "which")
|
||||
def test_old_agent_browser_is_upgraded(self, which, run):
|
||||
which.side_effect = ["/bin/agent-browser", "/bin/npm", "/bin/agent-browser"]
|
||||
which.side_effect = [
|
||||
"/bin/node",
|
||||
"/bin/npm",
|
||||
"/bin/agent-browser",
|
||||
"/bin/npm",
|
||||
"/bin/agent-browser",
|
||||
]
|
||||
run.side_effect = [
|
||||
MODULE.subprocess.CompletedProcess([], 0, "v22.11.0\n"),
|
||||
MODULE.subprocess.CompletedProcess([], 0, "agent-browser 0.17.1\n"),
|
||||
MODULE.subprocess.CompletedProcess([], 0, "changed 1 package\n"),
|
||||
MODULE.subprocess.CompletedProcess([], 0, "agent-browser 0.33.2\n"),
|
||||
]
|
||||
self.assertEqual(MODULE.ensure_agent_browser(), "/bin/agent-browser")
|
||||
self.assertEqual(run.call_args_list[1].args[0], [
|
||||
self.assertEqual(run.call_args_list[2].args[0], [
|
||||
"/bin/npm", "install", "-g", "agent-browser@latest"
|
||||
])
|
||||
|
||||
@patch.object(MODULE.subprocess, "run")
|
||||
@patch.object(MODULE.shutil, "which")
|
||||
def test_missing_nodejs_raises_clear_error(self, which, run):
|
||||
which.return_value = None
|
||||
with self.assertRaisesRegex(MODULE.ExportError, "Node.js is not installed"):
|
||||
MODULE.ensure_nodejs()
|
||||
run.assert_not_called()
|
||||
|
||||
@patch.object(MODULE.subprocess, "run")
|
||||
@patch.object(MODULE.shutil, "which")
|
||||
def test_old_nodejs_raises_clear_error(self, which, run):
|
||||
which.return_value = "/bin/node"
|
||||
run.return_value = MODULE.subprocess.CompletedProcess([], 0, "v16.20.2\n")
|
||||
with self.assertRaisesRegex(MODULE.ExportError, "Node.js 18\\+ is required"):
|
||||
MODULE.ensure_nodejs()
|
||||
|
||||
@patch.object(MODULE.subprocess, "run")
|
||||
@patch.object(MODULE.shutil, "which")
|
||||
def test_missing_npm_raises_clear_error(self, which, run):
|
||||
which.side_effect = ["/bin/node", None]
|
||||
run.return_value = MODULE.subprocess.CompletedProcess([], 0, "v22.11.0\n")
|
||||
with self.assertRaisesRegex(MODULE.ExportError, "npm is not installed"):
|
||||
MODULE.ensure_nodejs()
|
||||
|
||||
def test_parse_node_version(self):
|
||||
self.assertEqual(MODULE.parse_node_version("v22.11.0"), (22, 11, 0))
|
||||
self.assertEqual(MODULE.parse_node_version("18.20.4"), (18, 20, 4))
|
||||
|
||||
def test_fade_is_inserted_before_timing(self):
|
||||
source = (
|
||||
b'<?xml version="1.0" encoding="UTF-8"?>'
|
||||
|
||||
Reference in New Issue
Block a user