diff --git a/scripts/dev-svglide-ppe-env.sh b/scripts/dev-svglide-ppe-env.sh new file mode 100755 index 000000000..6202838e7 --- /dev/null +++ b/scripts/dev-svglide-ppe-env.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env sh + +RULE_GROUP="lark-cli" +CANONICAL_RULE_NAME="lark-cli-openapi-ppe-svg-slides" +PROXY_HOST="${LARK_CLI_PROXY_HOST:-127.0.0.1}" +PROXY_PORT="${LARK_CLI_PROXY_PORT:-8899}" + +# Keep this list intentionally small and explicit. These were previous local +# SVGlide/PPE rule names that can route generated SVG Slides to the wrong lane. +# Do not store them in a whitespace-split variable: this script is meant to be +# sourced by zsh as well as POSIX shells, and zsh does not split scalars by +# whitespace unless SH_WORD_SPLIT is enabled. + +fail() { + printf '%s\n' "[svglide-ppe] $*" >&2 + return 1 2>/dev/null || exit 1 +} + +info() { + printf '%s\n' "[svglide-ppe] $*" >&2 +} + +usage() { + cat <] [--port ] [--no-clear] + . ./scripts/dev-svglide-ppe-env.sh off + ./scripts/dev-svglide-ppe-env.sh rules + +Notes: + - Use "source" or "." for pre/off so proxy env vars apply to the current shell. + - pre writes the canonical Whistle rule "${CANONICAL_RULE_NAME}". + - pre also clears known legacy SVGlide/PPE rule names unless --no-clear is passed. + - This is dev-only PPE plumbing, not production publish behavior. +USAGE +} + +is_sourced() { + if [ -n "${BASH_VERSION-}" ]; then + [ "${BASH_SOURCE[0]}" != "$0" ] + return + fi + if [ -n "${ZSH_VERSION-}" ]; then + case "${ZSH_EVAL_CONTEXT-}" in + *:file:*) return 0 ;; + *) return 1 ;; + esac + fi + return 1 +} + +pick_whistle_cmd() { + if command -v w2 >/dev/null 2>&1; then + printf '%s\n' "w2" + return 0 + fi + if command -v whistle >/dev/null 2>&1; then + printf '%s\n' "whistle" + return 0 + fi + info "missing required command: w2 (or whistle)" + info "install: npm install -g whistle" + info "then: w2 start && w2 ca" + fail "missing required command: w2 (or whistle)" +} + +ensure_whistle_running() { + wcmd="$1" + whistle_status="$("$wcmd" status 2>&1 || true)" + case "$whistle_status" in + *"No running Whistle instances"*) + info "Whistle is not running, starting it now..." + "$wcmd" start >/dev/null 2>&1 || fail "failed to start Whistle" + ;; + esac +} + +canonical_rules() { + cat <<'RULES' +/^https:\/\/open\.feishu\.cn\/(.*)$/ https://open.feishu-pre.cn/$1 +https://open.feishu.cn/ reqHeaders://Env=Pre_release +https://open.feishu.cn/ reqHeaders://x-tt-env=ppe_svg_slides +https://open.feishu.cn/ reqHeaders://x-use-ppe=1 +https://open.feishu-pre.cn/ reqHeaders://Env=Pre_release +https://open.feishu-pre.cn/ reqHeaders://x-tt-env=ppe_svg_slides +https://open.feishu-pre.cn/ reqHeaders://x-use-ppe=1 +/^https:\/\/accounts\.feishu\.cn\/(.*)$/ https://accounts.feishu-pre.cn/$1 +RULES +} + +disabled_rules() { + rule_name="$1" + cat <"$output" <<'NODE' +const rule = { + name: process.env.RULE_NAME, + groupName: process.env.RULE_GROUP, + rules: process.env.RULES || "", +}; +process.stdout.write(`module.exports = function (callback) { + callback(${JSON.stringify(rule, null, 2)}); +}; +`); +NODE +} + +apply_rule() { + wcmd="$1" + rule_name="$2" + rules="$3" + + tmp_js="$(mktemp "${TMPDIR:-/tmp}/svglide-ppe-rule.XXXXXX.js")" || fail "failed to create temp file" + write_rule_js "$tmp_js" "$rule_name" "$rules" + "$wcmd" add "$tmp_js" --force >/dev/null 2>&1 || { + rm -f "$tmp_js" + fail "failed to apply Whistle rule: ${rule_name}" + } + rm -f "$tmp_js" +} + +clear_legacy_rules() { + wcmd="$1" + clear_legacy_rule "$wcmd" "lark-cli-openapi-env-switch" + clear_legacy_rule "$wcmd" "lark-cli-openapi-ppe-svglide" + clear_legacy_rule "$wcmd" "lark-cli-openapi-ppe-pure-svg" + clear_legacy_rule "$wcmd" "lark-cli-openapi-ppe-pure-svg-control" + clear_legacy_rule "$wcmd" "lark-cli-openapi-ppe-svg-slides-header-only" + clear_legacy_rule "$wcmd" "lark-cli-pre-ppe-svglide-combined" + clear_legacy_rule "$wcmd" "lark-cli-ppe-pure-svg" + clear_legacy_rule "$wcmd" "lark-cli-ppe-svglide" + clear_legacy_rule "$wcmd" "lark-cli-prod-host-pre-env-ppe-header" + clear_legacy_rule "$wcmd" "lark-cli-prod-ppe-svglide-header-only" +} + +clear_legacy_rule() { + wcmd="$1" + rule_name="$2" + [ "$rule_name" = "$CANONICAL_RULE_NAME" ] && return 0 + remove_rule "$wcmd" "$rule_name" || apply_rule "$wcmd" "$rule_name" "$(disabled_rules "$rule_name")" + info "cleared legacy Whistle rule: ${rule_name}" +} + +remove_rule() { + _wcmd="$1" + rule_name="$2" + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + curl -fsS \ + -X POST \ + -H 'Content-Type: application/json' \ + --data "{\"name\":\"${rule_name}\"}" \ + "http://${PROXY_HOST}:${PROXY_PORT}/cgi-bin/rules/remove" >/dev/null 2>&1 +} + +apply_proxy_env() { + proxy_url="http://${PROXY_HOST}:${PROXY_PORT}" + export HTTPS_PROXY="$proxy_url" + export https_proxy="$proxy_url" + export HTTP_PROXY="$proxy_url" + export http_proxy="$proxy_url" + export ALL_PROXY="$proxy_url" + export all_proxy="$proxy_url" + unset LARK_CLI_NO_PROXY + info "proxy env enabled: ${proxy_url}" +} + +clear_proxy_env() { + unset HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy + unset LARK_CLI_NO_PROXY + info "proxy env cleared" +} + +mode="${1:-}" +[ "$mode" = "" ] && { + usage + return 0 2>/dev/null || exit 0 +} +shift || true + +clear_legacy="1" +while [ "$#" -gt 0 ]; do + case "$1" in + --host) + [ "$#" -ge 2 ] || fail "--host requires a value" + PROXY_HOST="$2" + shift 2 + ;; + --port) + [ "$#" -ge 2 ] || fail "--port requires a value" + PROXY_PORT="$2" + shift 2 + ;; + --no-clear) + clear_legacy="0" + shift + ;; + -h|--help) + usage + return 0 2>/dev/null || exit 0 + ;; + *) + fail "unknown arg: $1" + ;; + esac +done + +case "$mode" in + rules) + canonical_rules + ;; + pre) + wcmd="$(pick_whistle_cmd)" + ensure_whistle_running "$wcmd" + if [ "$clear_legacy" = "1" ]; then + clear_legacy_rules "$wcmd" + fi + apply_rule "$wcmd" "$CANONICAL_RULE_NAME" "$(canonical_rules)" + info "applied canonical Whistle rule: ${CANONICAL_RULE_NAME}" + if is_sourced; then + apply_proxy_env + else + info "Whistle rule updated, but current shell env is unchanged." + info "run with source: . ./scripts/dev-svglide-ppe-env.sh pre" + fi + info "active PPE lane: ppe_svg_slides" + ;; + off|cancel) + wcmd="$(pick_whistle_cmd)" + ensure_whistle_running "$wcmd" + clear_legacy_rules "$wcmd" + apply_rule "$wcmd" "$CANONICAL_RULE_NAME" "$(disabled_rules "$CANONICAL_RULE_NAME")" + info "disabled canonical Whistle rule: ${CANONICAL_RULE_NAME}" + if is_sourced; then + clear_proxy_env + else + info "Whistle rules disabled, but current shell env is unchanged." + info "run with source: . ./scripts/dev-svglide-ppe-env.sh off" + fi + ;; + -h|--help|help) + usage + ;; + *) + fail "invalid mode: ${mode}; expected pre|off|rules" + ;; +esac diff --git a/skills/lark-slides/SKILL.md b/skills/lark-slides/SKILL.md index b6e251613..a05fa1e87 100644 --- a/skills/lark-slides/SKILL.md +++ b/skills/lark-slides/SKILL.md @@ -16,7 +16,7 @@ metadata: |----------|----------|-----------------| | 新建 PPT | 先规划 `slide_plan.json`,再按复杂度选择一步或两步创建 | `planning-layer.md`、`visual-planning.md`、`asset-planning.md`、`slides +create` | | 本地生成或校验 SVG Slides / SVGlide 产物 | 先读 `references/svg-slides/README.md`,生成 local publish-ready bundle;已有 publish-ready manifest 时用 `slides +create-svglide` 发布 | `references/svg-slides/README.md`、`lark-slides-create-svglide.md`、`scripts/validate_svg_deck.mjs`、`scripts/svg_slides_bundle.mjs` | -| 发布已有 SVGlide manifest | 只做发布:读取 manifest、校验 receipt/hash/root、本地图片上传并改写 href,然后逐页提交 raw SVG | `slides +create-svglide --manifest manifest.json --as user`、`lark-slides-create-svglide.md` | +| 发布已有 SVGlide manifest | 只做发布:读取 manifest、校验 receipt/hash/root、本地图片上传并改写 href,然后逐页提交 raw SVG;需要 PPE 时只用 `scripts/dev-svglide-ppe-env.sh` 的 `ppe_svg_slides` 规则 | `slides +create-svglide --manifest manifest.json --as user`、`lark-slides-create-svglide.md` | | 已有 PPT 大幅改写 | 多页整页重建用 `+replace-pages`,单页局部编辑用 `+replace-slide` | `xml_presentations.get`、`lark-slides-replace-pages.md`、`lark-slides-edit-workflows.md` | | 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide`、`lark-slides-replace-slide.md` | | 读取或分析已有 PPT | 解析 slides/wiki token,回读全文或单页 XML,保存 `xml_presentation_id`、`slide_id`、`revision_id` | `xml_presentations.get`、`xml_presentation.slide.get` | diff --git a/skills/lark-slides/references/lark-slides-create-svglide.md b/skills/lark-slides/references/lark-slides-create-svglide.md index e830e7c08..76a7a671c 100644 --- a/skills/lark-slides/references/lark-slides-create-svglide.md +++ b/skills/lark-slides/references/lark-slides-create-svglide.md @@ -21,7 +21,7 @@ Use it only after the SVG Slides local generation/validation layer has produced - SVG/SVGlide authoring. - Preview, visual QA, or repair loops. - Automatically running online readback; callers must run the readback gate after publish. -- PPE routing or Whistle setup. +- PPE routing or Whistle setup. For internal PPE validation, use the dev-only script below. - Remote image fetching, image generation, or FileMetaMap shaping. Generated SVG decks must not bypass this command by calling `slides xml_presentation.slide create` directly. Direct raw API calls can prove transport only; they do not prove that the generated deck passed the SVGlide parser contract, bundle receipts, asset rewrite, or readback gates. @@ -32,6 +32,35 @@ Generated SVG decks must not bypass this command by calling `slides xml_presenta This limit is independent from `slides +create --slides`, whose inline JSON array is limited to 10 entries. Do not split a 12-page SVG Slides deck just because `+create --slides` has a 10-page inline payload limit. +## Internal PPE Lane + +For internal online validation of SVG Slides, the only supported dev lane is `ppe_svg_slides`. +Do not use older local lanes such as `ppe_svglide` or `ppe_pure_svg`. + +Set or clear the rule with the tracked dev-only script: + +```bash +# enable the canonical rule and clear known legacy SVGlide/PPE rules +. ./scripts/dev-svglide-ppe-env.sh pre + +# print the canonical Whistle rules without applying them +./scripts/dev-svglide-ppe-env.sh rules + +# disable the canonical rule and clear known legacy rules +. ./scripts/dev-svglide-ppe-env.sh off +``` + +The expected effective request route is: + +```text +host: open.feishu-pre.cn +Env: Pre_release +x-tt-env: ppe_svg_slides +x-use-ppe: 1 +``` + +This script is not part of production publish behavior. It exists only to make local/PPE validation reproducible. + ## Manifest Minimum shape: