build(release): add repo-driven bilingual release notes
This commit is contained in:
1 parent
b9cd3cd9d5
commit
bd546ebe0f
24 files changed
+1799
-189
No files matched your search
+40
-183
@@ -8,7 +8,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: '版本号(例如: v2.0.3)'
|
||||
description: '当前所选 ref 上待发布的版本号(例如: v2.8.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -19,9 +19,36 @@ jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
validate-release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: 设置 Node.js 环境
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: 校验版本说明文件
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
VERSION="${{ inputs.version }}"
|
||||
echo "📝 Using manual input version: $VERSION"
|
||||
else
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "🏷️ Using git tag version: $VERSION"
|
||||
fi
|
||||
|
||||
echo "Validating release notes for $VERSION"
|
||||
node scripts/release-notes.js check "$VERSION"
|
||||
|
||||
# 构建 Windows 版本
|
||||
build-windows:
|
||||
needs: test
|
||||
needs: [test, validate-release-notes]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: 检出代码
|
||||
@@ -142,7 +169,7 @@ jobs:
|
||||
|
||||
# 构建 macOS 版本
|
||||
build-macos:
|
||||
needs: test
|
||||
needs: [test, validate-release-notes]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: 检出代码
|
||||
@@ -265,7 +292,7 @@ jobs:
|
||||
|
||||
# 构建 Linux 版本
|
||||
build-linux:
|
||||
needs: test
|
||||
needs: [test, validate-release-notes]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 检出代码
|
||||
@@ -421,17 +448,17 @@ jobs:
|
||||
if [[ $VERSION =~ -(alpha|beta|rc) ]]; then
|
||||
IS_PRERELEASE=true
|
||||
VERSION_TYPE="prerelease"
|
||||
RELEASE_NAME_PREFIX="Preview "
|
||||
RELEASE_NAME_PREFIX="Preview Prompt Optimizer "
|
||||
echo "🧪 Detected prerelease version"
|
||||
elif [[ $VERSION =~ -(hotfix|patch) ]]; then
|
||||
IS_PRERELEASE=false
|
||||
VERSION_TYPE="hotfix"
|
||||
RELEASE_NAME_PREFIX="Hotfix "
|
||||
RELEASE_NAME_PREFIX="Hotfix Prompt Optimizer "
|
||||
echo "🔧 Detected hotfix version"
|
||||
else
|
||||
IS_PRERELEASE=false
|
||||
VERSION_TYPE="release"
|
||||
RELEASE_NAME_PREFIX="Release "
|
||||
RELEASE_NAME_PREFIX="Release Prompt Optimizer "
|
||||
echo "🚀 Detected stable release version"
|
||||
fi
|
||||
|
||||
@@ -456,191 +483,21 @@ jobs:
|
||||
echo "Final artifact list after cleaning:"
|
||||
find ./artifacts -type f | sort
|
||||
|
||||
- name: 生成 Release Notes
|
||||
- name: 生成 Release 正文
|
||||
id: release_notes
|
||||
run: |
|
||||
# 获取当前tag
|
||||
CURRENT_TAG=${{ steps.version.outputs.version }}
|
||||
echo "Current tag: $CURRENT_TAG"
|
||||
|
||||
# 获取所有tags并调试输出
|
||||
echo "All tags (sorted):"
|
||||
git tag --sort=-version:refname | head -10
|
||||
|
||||
# 改进的上一个tag获取逻辑
|
||||
# 1. 先尝试获取所有正式版本tag (不包含预览版)
|
||||
STABLE_TAGS=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true)
|
||||
echo "Stable tags found: $STABLE_TAGS"
|
||||
|
||||
# 2. 获取所有tag (包含预览版)
|
||||
ALL_TAGS=$(git tag --sort=-version:refname)
|
||||
|
||||
# 3. 根据当前tag类型选择合适的比较策略
|
||||
PREVIOUS_TAG=""
|
||||
if [[ $CURRENT_TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
# 当前是正式版,找上一个正式版
|
||||
PREVIOUS_TAG=$(echo "$STABLE_TAGS" | grep -v "^$CURRENT_TAG$" | head -1 || true)
|
||||
echo "Current is stable release, previous stable tag: $PREVIOUS_TAG"
|
||||
else
|
||||
# 当前是预览版,找上一个任意版本
|
||||
PREVIOUS_TAG=$(echo "$ALL_TAGS" | grep -v "^$CURRENT_TAG$" | head -1 || true)
|
||||
echo "Current is prerelease, previous tag: $PREVIOUS_TAG"
|
||||
fi
|
||||
|
||||
# 生成commit历史(处理多行和长commit)
|
||||
COMMITS=""
|
||||
if [ -n "$PREVIOUS_TAG" ] && git rev-parse "$PREVIOUS_TAG" >/dev/null 2>&1 && git rev-parse "$CURRENT_TAG" >/dev/null 2>&1; then
|
||||
echo "Generating commits from $PREVIOUS_TAG to $CURRENT_TAG"
|
||||
COMMITS=$(git log --pretty=format:"%s|%h" $PREVIOUS_TAG..$CURRENT_TAG | while IFS='|' read -r subject hash; do
|
||||
# 截断过长的commit message(保留前80个字符)
|
||||
if [ ${#subject} -gt 80 ]; then
|
||||
subject="${subject:0:77}..."
|
||||
fi
|
||||
echo "- $subject ([${hash}](https://github.com/${{ github.repository }}/commit/${hash}))"
|
||||
done)
|
||||
elif git rev-parse "$CURRENT_TAG" >/dev/null 2>&1; then
|
||||
echo "No previous tag found or invalid, showing commits for current tag"
|
||||
COMMITS=$(git log --pretty=format:"%s|%h" --max-count=10 | while IFS='|' read -r subject hash; do
|
||||
# 截断过长的commit message(保留前80个字符)
|
||||
if [ ${#subject} -gt 80 ]; then
|
||||
subject="${subject:0:77}..."
|
||||
fi
|
||||
echo "- $subject ([${hash}](https://github.com/${{ github.repository }}/commit/${hash}))"
|
||||
done)
|
||||
fi
|
||||
|
||||
# 检查是否有commits
|
||||
if [ -z "$COMMITS" ]; then
|
||||
echo "No commits found, using fallback message"
|
||||
COMMITS="- 首次发布"
|
||||
else
|
||||
echo "Generated commits:"
|
||||
echo "$COMMITS"
|
||||
fi
|
||||
|
||||
# 限制commits数量(最多显示20个)
|
||||
COMMITS_LIMITED=$(echo "$COMMITS" | head -20)
|
||||
COMMITS_COUNT=$(echo "$COMMITS" | wc -l)
|
||||
|
||||
if [ $COMMITS_COUNT -gt 20 ]; then
|
||||
COMMITS_LIMITED="$COMMITS_LIMITED
|
||||
|
||||
... 以及其他 $((COMMITS_COUNT - 20)) 个提交"
|
||||
fi
|
||||
|
||||
# 获取本次发布的贡献者(排除主要作者)
|
||||
CONTRIBUTORS=""
|
||||
if [ -n "$PREVIOUS_TAG" ] && git rev-parse "$PREVIOUS_TAG" >/dev/null 2>&1; then
|
||||
# 获取贡献者列表,排除主要作者和机器人
|
||||
CONTRIBUTORS=$(git log --pretty=format:"%an|%ae" $PREVIOUS_TAG..$CURRENT_TAG 2>/dev/null | \
|
||||
sort -u | \
|
||||
grep -v -E "^(linshen|linshenkx|且炼时光)\|" | \
|
||||
grep -v -E "\[bot\]@" | \
|
||||
grep -v -E "^GitButler\|" | \
|
||||
cut -d'|' -f1 | \
|
||||
sort -u || true)
|
||||
else
|
||||
CONTRIBUTORS=$(git log --pretty=format:"%an|%ae" --max-count=50 2>/dev/null | \
|
||||
sort -u | \
|
||||
grep -v -E "^(linshen|linshenkx|且炼时光)\|" | \
|
||||
grep -v -E "\[bot\]@" | \
|
||||
grep -v -E "^GitButler\|" | \
|
||||
cut -d'|' -f1 | \
|
||||
sort -u || true)
|
||||
fi
|
||||
|
||||
# 格式化贡献者列表
|
||||
CONTRIBUTORS_SECTION=""
|
||||
if [ -n "$CONTRIBUTORS" ]; then
|
||||
CONTRIBUTORS_LIST=$(echo "$CONTRIBUTORS" | while read -r name; do
|
||||
[ -n "$name" ] && echo "- @$name"
|
||||
done | grep -v "^$" || true)
|
||||
|
||||
if [ -n "$CONTRIBUTORS_LIST" ]; then
|
||||
CONTRIBUTORS_SECTION="
|
||||
### 🙏 致谢
|
||||
|
||||
感谢以下贡献者对本次发布的贡献:
|
||||
|
||||
$CONTRIBUTORS_LIST
|
||||
"
|
||||
echo "Found contributors: $CONTRIBUTORS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 根据版本类型设置不同的图标和描述
|
||||
VERSION_ICON="🚀"
|
||||
VERSION_DESC="稳定版本"
|
||||
STABILITY_NOTE=""
|
||||
|
||||
if [[ $CURRENT_TAG =~ -(alpha|beta|rc) ]]; then
|
||||
VERSION_ICON="🧪"
|
||||
VERSION_DESC="预览版本"
|
||||
STABILITY_NOTE="
|
||||
|
||||
> ⚠️ **注意**: 这是一个预览版本,可能包含实验性功能。建议在非生产环境中使用。"
|
||||
elif [[ $CURRENT_TAG =~ -(hotfix|patch) ]]; then
|
||||
VERSION_ICON="🔧"
|
||||
VERSION_DESC="紧急修复版本"
|
||||
STABILITY_NOTE="
|
||||
|
||||
> 🔧 **修复版本**: 此版本主要包含重要问题的修复,建议尽快更新。"
|
||||
fi
|
||||
|
||||
# 创建release notes
|
||||
cat > release_notes.md << EOF
|
||||
## $VERSION_ICON Prompt Optimizer $CURRENT_TAG
|
||||
|
||||
**版本类型**: $VERSION_DESC$STABILITY_NOTE
|
||||
|
||||
### 📦 下载
|
||||
- **Windows**: \`PromptOptimizer-*-win-x64.exe\` (安装程序) 或 \`PromptOptimizer-*-win-x64.zip\` (便携版)
|
||||
- **macOS (Apple Silicon)**: \`PromptOptimizer-*-darwin-arm64.dmg\` (安装程序) 或 \`PromptOptimizer-*-darwin-arm64.zip\` (便携版)
|
||||
- **macOS (Intel)**: \`PromptOptimizer-*-darwin-x64.dmg\` (安装程序) 或 \`PromptOptimizer-*-darwin-x64.zip\` (便携版)
|
||||
- **Linux**: \`PromptOptimizer-*-linux-x86_64.AppImage\` (便携版) 或 \`PromptOptimizer-*-linux-x64.zip\` (便携版)
|
||||
|
||||
### 🔧 安装说明
|
||||
- **Windows**:
|
||||
- **安装程序**: 下载 \`.exe\` 文件,双击运行安装向导
|
||||
- **便携版**: 下载 \`.zip\` 文件,解压后运行 \`PromptOptimizer.exe\`
|
||||
- **macOS**:
|
||||
- **Apple Silicon (M1/M2/M3)**: 下载 \`darwin-arm64\` 版本
|
||||
- **Intel 芯片**: 下载 \`darwin-x64\` 版本
|
||||
- **安装程序**: 下载 \`.dmg\` 文件,双击挂载后拖拽到应用程序文件夹
|
||||
- **便携版**: 下载 \`.zip\` 文件,解压后运行 \`PromptOptimizer.app\`
|
||||
- ⚠️ **安全提示**: 如果打开应用时提示「已损坏」或「无法验证开发者」,请在终端执行以下命令后重试:
|
||||
\`\`\`bash
|
||||
xattr -rd com.apple.quarantine /Applications/PromptOptimizer.app
|
||||
\`\`\`
|
||||
或者对于 .dmg 文件:
|
||||
\`\`\`bash
|
||||
xattr -rd com.apple.quarantine ~/Downloads/PromptOptimizer-*.dmg
|
||||
\`\`\`
|
||||
- **Linux**:
|
||||
- **AppImage**: 下载 \`.AppImage\` 文件,添加执行权限后直接运行
|
||||
- **便携版**: 下载 \`.zip\` 文件,解压后运行 \`PromptOptimizer\`
|
||||
|
||||
### 📝 更新内容
|
||||
$COMMITS_LIMITED
|
||||
$CONTRIBUTORS_SECTION
|
||||
### 🔗 更多信息
|
||||
- **项目主页**: [GitHub Repository](https://github.com/${{ github.repository }})
|
||||
- **问题反馈**: [Issues](https://github.com/${{ github.repository }}/issues)
|
||||
- **讨论交流**: [Discussions](https://github.com/${{ github.repository }}/discussions)
|
||||
|
||||
---
|
||||
**提示**: 如果需要查看完整的提交历史,请访问项目的 [GitHub Commits](https://github.com/${{ github.repository }}/commits)
|
||||
EOF
|
||||
|
||||
echo "Generated release notes:"
|
||||
cat release_notes.md
|
||||
node scripts/release-notes.js render-body "$CURRENT_TAG" "${{ github.repository }}" > release_body.md
|
||||
echo "release_notes_path=release_body.md" >> "$GITHUB_OUTPUT"
|
||||
echo "Generated release body:"
|
||||
cat release_body.md
|
||||
|
||||
- name: 创建 Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.version }}
|
||||
name: ${{ steps.version.outputs.release_name_prefix }}${{ steps.version.outputs.version }}
|
||||
body_path: release_notes.md
|
||||
body_path: ${{ steps.release_notes.outputs.release_notes_path }}
|
||||
files: |
|
||||
./artifacts/windows-build/*
|
||||
./artifacts/macos-build/*
|
||||
|
||||
+35
-1
@@ -1,5 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
Full release narratives now live in versioned files under `releases/`. This file stays as the index and summary entry point.
|
||||
|
||||
## [2.8.0] - 2026-04-03
|
||||
- EN: Text-to-image evaluation, smoother reference-image workflows, and stronger Cloudflare-backed model support. See [Release Notes (EN)](releases/v2.8.0.en.md).
|
||||
- 中文:本次发布聚焦文生图评估、更顺畅的参考图工作流,以及更稳健的 Cloudflare 模型支持。参见 [版本说明(中文)](releases/v2.8.0.zh-CN.md)。
|
||||
|
||||
## [2.7.0] - 2026-03-25
|
||||
- EN: Structured compare evaluation, smoother compare UX, and refreshed docs and website surfaces. See [Release Notes (EN)](releases/v2.7.0.en.md).
|
||||
- 中文:本次发布聚焦结构化对比评估、更顺畅的对比体验,以及焕新的文档与站点展示。参见 [版本说明(中文)](releases/v2.7.0.zh-CN.md)。
|
||||
|
||||
## [2.6.3] - 2026-03-22
|
||||
- EN: Image prompt extraction, stronger image understanding, and a safer desktop release pipeline. See [Release Notes (EN)](releases/v2.6.3.en.md).
|
||||
- 中文:本次发布带来图像提示词提取、更强的图像理解能力,以及更稳健的桌面端发布链路。参见 [版本说明(中文)](releases/v2.6.3.zh-CN.md)。
|
||||
|
||||
## [2.6.2] - 2026-03-15
|
||||
- EN: Reworked evaluation architecture, cleaner workspace/result/compare flows, and more stable variable handling. See [Release Notes (EN)](releases/v2.6.2.en.md).
|
||||
- 中文:本次发布重构了评估架构,梳理了工作区 / 结果 / 对比流程,并让变量处理更稳定。参见 [版本说明(中文)](releases/v2.6.2.zh-CN.md)。
|
||||
|
||||
## [2.6.1] - 2026-03-12
|
||||
- EN: Better model management, richer image generation metadata, and stronger custom model configuration support. See [Release Notes (EN)](releases/v2.6.1.en.md).
|
||||
- 中文:本次发布改进了模型管理、补充了更丰富的图像生成元数据,并增强了自定义模型配置能力。参见 [版本说明(中文)](releases/v2.6.1.zh-CN.md)。
|
||||
|
||||
## [2.6.0] - 2026-03-09
|
||||
- EN: MiniMax provider support, stronger model management, and more usable temporary-variable and XML inspection workflows. See [Release Notes (EN)](releases/v2.6.0.en.md).
|
||||
- 中文:本次发布带来 MiniMax provider 支持,进一步强化了模型管理,也让临时变量与 XML 检查体验更顺手。参见 [版本说明(中文)](releases/v2.6.0.zh-CN.md)。
|
||||
|
||||
## [2.5.5] - 2026-03-01
|
||||
- EN: Media-aware favorites, Prompt Garden asset imports, and more durable preview storage. See [Release Notes (EN)](releases/v2.5.5.en.md).
|
||||
- 中文:本次发布让收藏夹支持媒体内容、Prompt Garden 资源导入,并让预览资源存储更可靠。参见 [版本说明(中文)](releases/v2.5.5.zh-CN.md)。
|
||||
|
||||
## [2.5.4] - 2026-02-10
|
||||
- EN: Feedback-driven evaluation, stronger parsing robustness, and smoother analyze interactions. See [Release Notes (EN)](releases/v2.5.4.en.md).
|
||||
- 中文:本次发布强化了反馈驱动评估、结果解析鲁棒性,以及分析交互体验。参见 [版本说明(中文)](releases/v2.5.4.zh-CN.md)。
|
||||
|
||||
## [2.1.0] - 2025-01-19
|
||||
|
||||
### 🎉 Added - 收藏管理重构 (Favorite Management Refactor)
|
||||
@@ -54,4 +88,4 @@
|
||||
- 基础收藏管理功能
|
||||
- 优化历史集成
|
||||
- 标签和分类基础支持
|
||||
- 导入导出功能
|
||||
- 导入导出功能
|
||||
@@ -7,6 +7,7 @@
|
||||
- [产品需求文档](./prd.md) - 产品功能需求和规格说明
|
||||
- [项目状态](./project-status.md) - 当前项目进度和完成情况
|
||||
- [版本同步](./version-sync.md) - 版本管理和同步策略
|
||||
- [版本说明与 Release 发布](./release-notes.md) - 双语版本说明与发布流程
|
||||
|
||||
## 📊 项目状态
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# 版本说明与 Release 发布
|
||||
|
||||
## 目标
|
||||
|
||||
从当前流程开始,仓库内的版本说明文件是 GitHub Release 正文的唯一事实来源。
|
||||
|
||||
- `CHANGELOG.md`:版本索引与摘要入口
|
||||
- `releases/vX.Y.Z.en.md`:某个版本的英文完整说明
|
||||
- `releases/vX.Y.Z.zh-CN.md`:某个版本的中文完整说明
|
||||
- `.github/workflows/release.yml`:读取两个版本文件,生成 GitHub Release 摘要正文
|
||||
|
||||
## 文件约定
|
||||
|
||||
每次发布都需要同时维护两处内容:
|
||||
|
||||
1. `CHANGELOG.md`
|
||||
- 顶部必须是当前版本
|
||||
- 必须包含一行英文摘要和一行中文摘要
|
||||
- 必须分别链接到 `releases/vX.Y.Z.en.md` 和 `releases/vX.Y.Z.zh-CN.md`
|
||||
|
||||
2. `releases/vX.Y.Z.en.md`
|
||||
- 标题固定为 `# Prompt Optimizer vX.Y.Z`
|
||||
- 必须包含 `## Summary`
|
||||
- 必须包含以下完整栏目:
|
||||
- `## Highlights`
|
||||
- `## Product Updates`
|
||||
- `## Fixes`
|
||||
- `## Breaking Changes / Upgrade Notes`
|
||||
- `## Developer Notes`
|
||||
|
||||
3. `releases/vX.Y.Z.zh-CN.md`
|
||||
- 标题固定为 `# Prompt Optimizer vX.Y.Z`
|
||||
- 必须包含 `## 概括`
|
||||
- 必须包含以下完整栏目:
|
||||
- `## 亮点`
|
||||
- `## 产品更新`
|
||||
- `## 修复`
|
||||
- `## 破坏性变更 / 升级说明`
|
||||
- `## 开发者说明`
|
||||
|
||||
`Summary / 概括` 为正式发布必填项,GitHub Release 会优先使用它们来生成摘要正文。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 1. 生成当前版本的版本说明模板
|
||||
pnpm release:notes:new
|
||||
|
||||
# 或者显式指定版本
|
||||
pnpm release:notes:new 2.9.0
|
||||
|
||||
# 2. 校验当前版本说明
|
||||
pnpm release:notes:check
|
||||
|
||||
# 或者显式指定版本
|
||||
pnpm release:notes:check v2.9.0
|
||||
|
||||
# 3. 校验历史版本条目(用于回填旧版本 release 文档)
|
||||
pnpm release:notes:check:entry v2.6.0
|
||||
```
|
||||
|
||||
`release:notes:new` 会同时生成英文和中文模板,并附带一段仅供编辑参考的 commit 草稿区。
|
||||
`release:notes:check` 用于正式发布前的硬门槛校验,要求目标版本必须位于 `CHANGELOG.md` 顶部。
|
||||
`release:notes:check:entry` 用于历史版本回填,只要求 `CHANGELOG.md` 中存在对应版本条目,不要求它在顶部。
|
||||
|
||||
## 推荐发布流程
|
||||
|
||||
```bash
|
||||
# 1. 更新版本号
|
||||
pnpm version patch
|
||||
|
||||
# 2. 生成版本说明模板
|
||||
pnpm release:notes:new
|
||||
|
||||
# 3. 手动完善 releases/vX.Y.Z.en.md 与 releases/vX.Y.Z.zh-CN.md,
|
||||
# 并更新 CHANGELOG.md 顶部摘要
|
||||
|
||||
# 4. 校验版本说明
|
||||
pnpm release:notes:check
|
||||
|
||||
# 5. 创建 tag(会再次自动校验)
|
||||
pnpm version:tag
|
||||
|
||||
# 6. 推送 tag
|
||||
pnpm version:publish
|
||||
```
|
||||
|
||||
## 校验规则
|
||||
|
||||
以下情况会导致 `pnpm release:notes:check` 失败:
|
||||
|
||||
- 缺少对应版本的英文或中文 release 文件
|
||||
- 标题版本号与文件名不一致
|
||||
- 缺少 `## Summary` 或 `## 概括`
|
||||
- 必需栏目缺失或顺序错误
|
||||
- `CHANGELOG.md` 顶部不是当前版本
|
||||
- `CHANGELOG.md` 顶部没有同时链接英文和中文版本说明
|
||||
- 正文中仍然存在 `TODO`、`TBD`、`待补充`、`XX` 等占位内容
|
||||
|
||||
对于历史版本回填,可以使用 `pnpm release:notes:check:entry <version>`。它会沿用同样的正文结构校验,但把 `CHANGELOG.md` 校验范围放宽为“存在对应版本条目”。
|
||||
|
||||
## GitHub Release 行为
|
||||
|
||||
发布工作流不会再从 commit 列表自动拼接公开正文,而是优先读取:
|
||||
|
||||
- `releases/vX.Y.Z.en.md` 中的 `## Summary`
|
||||
- `releases/vX.Y.Z.zh-CN.md` 中的 `## 概括`
|
||||
|
||||
然后生成:
|
||||
|
||||
- 英文区块
|
||||
- `Summary`
|
||||
- `macOS Security Note`
|
||||
- 安装文档链接
|
||||
- 英文全文链接
|
||||
- 中文区块
|
||||
- `概括`
|
||||
- `macOS 安全提示`
|
||||
- 安装文档链接
|
||||
- 中文全文链接
|
||||
|
||||
其中 `macOS Security Note / macOS 安全提示` 为固定内容,用来明确说明:
|
||||
|
||||
- “已损坏”
|
||||
- “无法验证开发者”
|
||||
|
||||
这类提示通常来自 macOS 的隔离属性,不一定是应用本身损坏。
|
||||
|
||||
如果历史版本缺少概括,渲染逻辑可以回退为全文;但正式发布流程会被校验阻塞。
|
||||
|
||||
`workflow_dispatch` 中手动输入的 `version` 仍然用于“当前所选 ref 上准备发布的版本”,不是用来从当前主干回补任意历史版本的重新发布。
|
||||
|
||||
如果版本说明校验失败:
|
||||
|
||||
- 构建任务不会继续执行
|
||||
- GitHub Release 不会创建成功
|
||||
@@ -44,6 +44,23 @@ pnpm run version:sync
|
||||
3. **同步脚本**: `scripts/sync-versions.js` 读取新的版本号并更新其他文件
|
||||
4. **git commit**: pnpm 创建包含所有版本号变更的提交和标签
|
||||
|
||||
## 与 Release 版本说明的关系
|
||||
|
||||
从当前流程开始,版本号同步只是发布准备的一部分。创建正式 tag 之前,还需要补齐并校验版本说明:
|
||||
|
||||
```bash
|
||||
pnpm release:notes:new
|
||||
pnpm release:notes:check
|
||||
pnpm version:tag
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `pnpm release:notes:new` 会同时生成 `releases/vX.Y.Z.en.md` 和 `releases/vX.Y.Z.zh-CN.md` 模板
|
||||
- `pnpm release:notes:check` 会校验 `CHANGELOG.md` 以及英文、中文两个版本说明文件
|
||||
- `pnpm release:notes:check:entry` 可用于历史版本回填时单独校验某个旧版本条目
|
||||
- `pnpm version:tag` 会在打 tag 前再次执行一次校验,避免缺失版本说明就发布
|
||||
|
||||
## 添加新的同步文件
|
||||
|
||||
如需添加更多文件的版本同步,编辑 `scripts/sync-versions.js` 文件中的 `versionFiles` 数组:
|
||||
@@ -83,4 +100,4 @@ const versionFiles = [
|
||||
|
||||
```bash
|
||||
node scripts/sync-versions.js
|
||||
```
|
||||
```
|
||||
+10
-4
@@ -27,7 +27,9 @@
|
||||
"dev:desktop:fresh": "node scripts/run-many.js kill:dev clean pnpm-install dev:desktop",
|
||||
"dev:desktop:parallel": "concurrently -k -p \"[{name}]\" -n \"WEB,DESKTOP\" \"pnpm -F @prompt-optimizer/web dev\" \"pnpm -F @prompt-optimizer/desktop dev\"",
|
||||
"test": "node scripts/run-many.js -s test:unit test:e2e:smart",
|
||||
"test:unit": "pnpm -r test --run --passWithNoTests",
|
||||
"test:unit": "node scripts/run-many.js -s test:packages test:scripts",
|
||||
"test:packages": "pnpm -r test --run --passWithNoTests",
|
||||
"test:scripts": "node --test scripts/*.test.mjs",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:smart": "node scripts/smart-e2e.js",
|
||||
"test:e2e:record": "cross-env E2E_VCR_MODE=record playwright test",
|
||||
@@ -36,19 +38,23 @@
|
||||
"test:gate:core": "pnpm -F @prompt-optimizer/core test:gate",
|
||||
"test:gate:ui": "pnpm -F @prompt-optimizer/core build && pnpm -F @prompt-optimizer/ui test",
|
||||
"test:gate:e2e": "playwright test tests/e2e/regression.spec.ts tests/e2e/workflows/p0-route-smoke.spec.ts",
|
||||
"test:gate": "node scripts/run-many.js -s test:gate:core test:gate:ui",
|
||||
"test:gate": "node scripts/run-many.js -s test:scripts test:gate:core test:gate:ui",
|
||||
"test:gate:full": "node scripts/run-many.js -s test:gate test:gate:e2e",
|
||||
"test:fast": "pnpm -r test --run --passWithNoTests",
|
||||
"test:fast": "pnpm run test:unit",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"clean": "node scripts/run-many.js clean:dist clean:vite",
|
||||
"clean:dist": "rimraf packages/core/dist packages/ui/dist packages/web/dist packages/extension/dist packages/desktop/dist packages/desktop/web-dist",
|
||||
"clean:vite": "rimraf packages/core/node_modules/.vite packages/ui/node_modules/.vite packages/web/node_modules/.vite packages/extension/node_modules/.vite",
|
||||
"pnpm-install": "pnpm install",
|
||||
"release:notes:new": "node scripts/release-notes.js new",
|
||||
"release:notes:check": "node scripts/release-notes.js check",
|
||||
"release:notes:check:entry": "node scripts/release-notes.js check-entry",
|
||||
"release:notes:render-body": "node scripts/release-notes.js render-body",
|
||||
"version:sync": "node scripts/sync-versions.js",
|
||||
"version": "pnpm run version:sync && git add -A",
|
||||
"version:prepare": "pnpm version --no-git-tag-version",
|
||||
"version:tag": "git tag v$(node -p \"require('./package.json').version\")",
|
||||
"version:tag": "pnpm release:notes:check && git tag v$(node -p \"require('./package.json').version\")",
|
||||
"version:publish": "git push origin v$(node -p \"require('./package.json').version\")",
|
||||
"mcp:build": "pnpm --filter @prompt-optimizer/mcp-server build",
|
||||
"mcp:dev": "pnpm --filter @prompt-optimizer/mcp-server dev",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Prompt Optimizer v2.5.4
|
||||
|
||||
## Summary
|
||||
- Added feedback-driven evaluation flows so users can guide analysis with explicit evaluation notes.
|
||||
- Updated evaluation templates to pay more attention to user feedback and improved result parsing robustness.
|
||||
- Polished the analyze interaction flow and aligned more provider environment mappings.
|
||||
|
||||
## Highlights
|
||||
- Evaluation became more iterative: users can now add feedback directly into the evaluation flow instead of only reading a score afterward.
|
||||
- The evaluation stack now treats user feedback as a stronger signal, which improves the usefulness of follow-up analysis.
|
||||
- Parsing and provider configuration became more resilient, reducing avoidable friction in evaluation-heavy sessions.
|
||||
|
||||
## Product Updates
|
||||
### Web
|
||||
- Added an evaluation feedback editor and feedback-aware analyze flow across the main workspace surfaces.
|
||||
- Merged focus input more cleanly into the analyze action so evaluation guidance can be supplied at the right moment.
|
||||
- Kept the evaluation popover open during apply actions, making iterative review less disruptive.
|
||||
|
||||
### Core/Infra
|
||||
- Added support for user feedback across evaluation templates and made that feedback a higher-priority signal during evaluation.
|
||||
- Improved evaluation result parsing robustness so structured output is interpreted more consistently.
|
||||
- Added missing provider environment mappings and aligned provider handling across desktop logs, MCP selection, and docs.
|
||||
|
||||
## Fixes
|
||||
- Fixed evaluation result parsing edge cases that could weaken downstream analysis.
|
||||
- Fixed missing provider environment mappings that affected some env-driven model setups.
|
||||
- Fixed the evaluation popover behavior so apply actions no longer interrupt iterative review as easily.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None. Existing evaluation workflows continue to work, now with stronger feedback support.
|
||||
|
||||
## Developer Notes
|
||||
- `v2.5.4` is the release where evaluation shifts from a mostly one-way score display toward a feedback-guided loop.
|
||||
- If you maintain custom evaluation prompts or result consumers, this is the right baseline for the stronger feedback signal and more defensive parsing behavior.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Prompt Optimizer v2.5.4
|
||||
|
||||
## 概括
|
||||
- 评估流程开始支持“基于反馈继续分析”,用户可以直接带着明确意见去驱动评估。
|
||||
- 评估模板更加重视用户反馈,同时结果解析也更稳健了。
|
||||
- 分析交互流被顺了一遍,provider 环境变量映射也补得更完整。
|
||||
|
||||
## 亮点
|
||||
- 评估不再只是看完分数就结束,用户现在可以把反馈直接带入评估流程,形成更连续的迭代闭环。
|
||||
- 评估底层把用户反馈视为更强的信号,让后续分析结果更贴近真实诉求。
|
||||
- 结果解析和 provider 配置都更稳了,减少了高频评估场景里的额外摩擦。
|
||||
|
||||
## 产品更新
|
||||
### Web
|
||||
- 在主要工作区里新增了评估反馈编辑器,以及基于反馈继续分析的交互流。
|
||||
- 把 focus input 更自然地并入 analyze 动作,让用户能在合适的时机补充评估指导信息。
|
||||
- 执行 apply 操作时,评估浮层会保持打开,连续迭代时不再那么打断思路。
|
||||
|
||||
### Core/Infra
|
||||
- 为评估模板全面补上了用户反馈支持,并让反馈在评估时拥有更高优先级。
|
||||
- 增强了评估结果解析的鲁棒性,让结构化输出的解释更稳定一致。
|
||||
- 补齐了缺失的 provider 环境变量映射,并对齐了桌面日志、MCP 选择和文档中的 provider 处理逻辑。
|
||||
|
||||
## 修复
|
||||
- 修复了若干会削弱后续分析质量的评估结果解析边界情况。
|
||||
- 修复了部分依赖环境变量驱动的模型配置缺少 provider 映射的问题。
|
||||
- 修复了评估浮层在 apply 操作时过早打断迭代查看的问题。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 无。现有评估流程可以继续使用,只是现在对反馈的支持更完整了。
|
||||
|
||||
## 开发者说明
|
||||
- `v2.5.4` 可以看作评估体系从“单向打分展示”转向“反馈驱动闭环”的起点版本。
|
||||
- 如果你维护自定义评估提示词或结果消费逻辑,这一版是更强反馈信号和更防御式结果解析的合适基线。
|
||||
@@ -0,0 +1,35 @@
|
||||
# Prompt Optimizer v2.5.5
|
||||
|
||||
## Summary
|
||||
- Expanded favorites to support media uploads, richer preview cards, and Prompt Garden imports with asset references.
|
||||
- Moved imported snapshot assets onto more durable storage so favorite previews survive session cleanup more reliably.
|
||||
- Improved browser-side image service compatibility and polished a few import and icon details.
|
||||
|
||||
## Highlights
|
||||
- Favorites became much more media-aware, especially when saving Prompt Garden content with images and cover previews.
|
||||
- Prompt Garden imports now feel safer and more intentional thanks to asset-aware imports and confirm-mode flows.
|
||||
- Snapshot preview storage became more durable, reducing the chance that imported visual references disappear with session data.
|
||||
|
||||
## Product Updates
|
||||
### Web
|
||||
- Added generic media upload and preview support when saving favorites.
|
||||
- Displayed cover images on favorite cards and added richer preview panels for Prompt Garden imports.
|
||||
- Added Prompt Garden import support with favorite asset references, plus a confirm-mode import flow for more deliberate saves.
|
||||
- Introduced pluggable favorite-preview support so Prompt Garden previews can evolve without tightly coupling the rest of the favorites UI.
|
||||
|
||||
### Core/Infra
|
||||
- Isolated snapshot image assets from session storage so imported favorite previews remain available more reliably.
|
||||
- Improved browser-side CORS handling for OpenAI SDK fetch usage.
|
||||
- Removed unnecessary CORS-restricted tagging for ModelScope and Seedream flows.
|
||||
|
||||
## Fixes
|
||||
- Fixed provider/browser compatibility issues around image-related requests and SDK fetch behavior.
|
||||
- Fixed a missing `NText` import in `SaveFavoriteDialog`.
|
||||
- Standardized rounded app icon exports for a more consistent release surface.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None. Existing favorites should continue to work, with stronger support for media-heavy entries.
|
||||
|
||||
## Developer Notes
|
||||
- This release is the handoff point where favorites stop behaving like text-only records and start carrying reusable media and preview state more intentionally.
|
||||
- If you maintain Prompt Garden integrations, use `v2.5.5` as the baseline for asset-aware favorite imports.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Prompt Optimizer v2.5.5
|
||||
|
||||
## 概括
|
||||
- 收藏夹开始支持媒体上传、更丰富的预览卡片,以及带资源引用的 Prompt Garden 导入。
|
||||
- 导入的快照资源被迁移到更持久的存储方式里,收藏预览不容易再随着会话清理而丢失。
|
||||
- 浏览器侧图像服务兼容性也做了补强,同时顺手打磨了若干导入与图标细节。
|
||||
|
||||
## 亮点
|
||||
- 收藏夹终于更像“可承载媒体内容的资产”,尤其适合保存带图片和封面的 Prompt Garden 内容。
|
||||
- Prompt Garden 导入现在支持资源引用和确认式导入流程,保存体验更稳、更可控。
|
||||
- 快照预览资源的持久化方式更可靠,减少了导入视觉内容后因为会话数据清理而丢失的情况。
|
||||
|
||||
## 产品更新
|
||||
### Web
|
||||
- 保存收藏时新增通用媒体上传与预览能力。
|
||||
- 收藏卡片现在可以显示封面图,也为 Prompt Garden 导入补上了更丰富的预览面板。
|
||||
- Prompt Garden 导入支持带收藏资源引用的保存流程,并增加了确认式导入模式,让保存动作更明确。
|
||||
- 引入可插拔的收藏预览机制,让 Prompt Garden 预览能力可以持续演进,同时不把整个收藏 UI 绑死在单一集成上。
|
||||
|
||||
### Core/Infra
|
||||
- 将快照图片资源从会话存储中隔离出来,让导入后的收藏预览更稳定可用。
|
||||
- 改进了浏览器侧使用 OpenAI SDK fetch 时的 CORS 兼容性。
|
||||
- 移除了 ModelScope 和 Seedream 流程中不必要的 CORS 限制标记。
|
||||
|
||||
## 修复
|
||||
- 修复了部分图像相关请求和 SDK fetch 行为在浏览器环境下的兼容性问题。
|
||||
- 修复了 `SaveFavoriteDialog` 中缺失 `NText` 导入的问题。
|
||||
- 统一了圆角应用图标导出的资源表现。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 无。现有收藏数据可以继续使用,同时会获得更好的媒体内容支持。
|
||||
|
||||
## 开发者说明
|
||||
- 这一版是收藏夹从“偏文本记录”转向“可承载媒体与预览状态资产”的分水岭版本。
|
||||
- 如果你在维护 Prompt Garden 相关集成,可以把 `v2.5.5` 视为带资源引用收藏导入的基线版本。
|
||||
@@ -0,0 +1,35 @@
|
||||
# Prompt Optimizer v2.6.0
|
||||
|
||||
## Summary
|
||||
- Added MiniMax provider support and made advanced model configuration easier to manage.
|
||||
- Improved temporary-variable editing, model management, and XML result inspection across the app.
|
||||
- Smoothed several cross-workspace interactions so model changes and feedback flows behave more reliably.
|
||||
|
||||
## Highlights
|
||||
- MiniMax became a first-class provider, making it easier to test and run more model combinations in one workspace.
|
||||
- The model manager and temporary-variable tools became much more usable for heavier prompt iteration workflows.
|
||||
- XML outputs are now easier to inspect thanks to an auto-rendered collapsible tree view.
|
||||
|
||||
## Product Updates
|
||||
### Web
|
||||
- Refined the model manager layout and cards so text, image, and function models feel more consistent and easier to scan.
|
||||
- Improved the temporary variables panel with a better editing experience, variable renaming, and a fullscreen editor that uses dialog space more effectively.
|
||||
- Added automatic XML rendering with a collapsible tree so structured outputs are easier to inspect without leaving the app.
|
||||
- Kept model lists in sync across workspaces after manager updates and tightened typed feedback payload handling.
|
||||
|
||||
### Core/Infra
|
||||
- Added MiniMax provider support, including OpenAI-compatible integration and initial default-model coverage.
|
||||
- Deduplicated shared `TextModelConfig` typing so model configuration behavior is easier to maintain.
|
||||
- Preserved enabled-state choices and cleaned up model version label formatting.
|
||||
|
||||
## Fixes
|
||||
- Fixed model-list synchronization after manager changes so other workspaces reflect updates more reliably.
|
||||
- Fixed scroll-jump issues in the fullscreen variable editor.
|
||||
- Fixed several model-manager interaction details, including enabled-choice persistence and clearer version labels.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None. Existing workspaces and saved data should continue to work without migration.
|
||||
|
||||
## Developer Notes
|
||||
- This version is a foundation release for the later model-management and evaluation work shipped in `v2.6.1` and `v2.6.2`.
|
||||
- If you track when MiniMax provider support and the XML tree inspector first landed, `v2.6.0` is the right release baseline.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Prompt Optimizer v2.6.0
|
||||
|
||||
## 概括
|
||||
- 新增了 MiniMax provider 支持,也让高级模型配置的管理更顺手。
|
||||
- 临时变量编辑、模型管理和 XML 结果查看体验都有明显改进。
|
||||
- 多个工作区之间的模型同步和反馈链路也更稳定了,日常迭代更顺畅。
|
||||
|
||||
## 亮点
|
||||
- MiniMax 成为一等公民 provider,更方便在同一工作区里测试和运行更多模型组合。
|
||||
- 模型管理器和临时变量工具更适合高频提示词迭代场景,操作成本更低。
|
||||
- XML 输出现在会自动渲染为可折叠树视图,查看结构化结果更直观。
|
||||
|
||||
## 产品更新
|
||||
### Web
|
||||
- 优化了模型管理器的布局和卡片设计,让文本、图像、函数模型的呈现更统一、更容易浏览。
|
||||
- 改进了临时变量面板,补齐了更顺手的编辑体验、变量重命名能力,以及更充分利用弹窗空间的全屏编辑器。
|
||||
- 新增 XML 自动渲染与可折叠树视图,不用离开应用也能更轻松地检查结构化输出。
|
||||
- 模型管理器更新后,模型列表可以更稳定地同步到各个工作区,同时也收紧了反馈数据的类型处理。
|
||||
|
||||
### Core/Infra
|
||||
- 新增 MiniMax provider 支持,包括兼容 OpenAI 风格接口的接入方式和初始默认模型配置。
|
||||
- 去重了共享的 `TextModelConfig` 类型来源,后续维护模型配置逻辑更清晰。
|
||||
- 优化了模型启用状态的保留逻辑,并统一了版本标签格式。
|
||||
|
||||
## 修复
|
||||
- 修复了模型管理器修改后其他工作区模型列表不同步的问题。
|
||||
- 修复了全屏变量编辑器里的滚动跳动问题。
|
||||
- 修复了模型管理器中的若干交互细节,包括启用状态保留和版本标签显示不清晰的问题。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 无。现有工作区和已保存数据默认不需要迁移。
|
||||
|
||||
## 开发者说明
|
||||
- 这一版是后续 `v2.6.1`、`v2.6.2` 模型管理和评估链路继续演进的重要基础版本。
|
||||
- 如果你要追溯 MiniMax provider 支持和 XML 树形查看器最早在哪个版本落地,可以以 `v2.6.0` 为基线。
|
||||
@@ -0,0 +1,35 @@
|
||||
# Prompt Optimizer v2.6.1
|
||||
|
||||
## Summary
|
||||
- Improved model management with clone actions, restored labels, and stronger custom-model configuration support.
|
||||
- Added richer image-generation details such as token usage and image metadata in the UI.
|
||||
- Fixed a missing desktop IPC bridge so prompt messages can flow more reliably in desktop builds.
|
||||
|
||||
## Highlights
|
||||
- Model management became more capable for advanced setups, especially when duplicating or tuning multiple model profiles.
|
||||
- Image generation now exposes more useful runtime details directly in the interface.
|
||||
- Core support for environment-driven and dynamically fetched model configuration expanded again.
|
||||
|
||||
## Product Updates
|
||||
### Desktop
|
||||
- Added the missing prompt-message IPC bridge needed by desktop flows.
|
||||
|
||||
### Web
|
||||
- Added token-usage and image-metadata display for image generation.
|
||||
- Added a Clone action for model configurations in Model Manager.
|
||||
- Restored model manager action labels so key actions stay understandable.
|
||||
|
||||
### Core/Infra
|
||||
- Added support for custom model parameters loaded from environment variables.
|
||||
- Enabled dynamic model fetching for the Gemini image adapter.
|
||||
|
||||
## Fixes
|
||||
- Fixed missing desktop IPC wiring for prompt messages.
|
||||
- Restored missing model manager action labels in the UI.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None. Existing model setups should continue to work, with more configuration flexibility available.
|
||||
|
||||
## Developer Notes
|
||||
- This release continues the push toward more dynamic model configuration, especially for teams managing providers through environment-based deployment.
|
||||
- If you are tracking when image generation metadata first became visible in the app UI, this is the version to reference.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Prompt Optimizer v2.6.1
|
||||
|
||||
## 概括
|
||||
- 模型管理能力进一步增强,支持克隆配置、恢复操作标签,并加强了自定义模型配置能力。
|
||||
- 图像生成界面开始展示更丰富的运行时信息,包括 token 使用量和图片元数据。
|
||||
- 修复了桌面端缺失的 IPC bridge,让提示消息链路更完整。
|
||||
|
||||
## 亮点
|
||||
- 对复杂模型配置场景更友好了,尤其是在复制和微调多组模型配置时更顺手。
|
||||
- 图像生成结果现在会直接展示更多有用信息,便于排查和比较。
|
||||
- Core 对环境变量驱动配置和动态拉取模型的支持又向前推进了一步。
|
||||
|
||||
## 产品更新
|
||||
### Desktop
|
||||
- 补上了桌面端流程所需的 prompt message IPC bridge。
|
||||
|
||||
### Web
|
||||
- 为图像生成增加了 token 使用量和图片元数据显示。
|
||||
- 在 Model Manager 中新增了模型配置的 Clone 按钮。
|
||||
- 恢复了模型管理器里缺失的操作标签,让关键动作更容易理解。
|
||||
|
||||
### Core/Infra
|
||||
- 新增对环境变量加载自定义模型参数的支持。
|
||||
- 为 Gemini 图像适配器启用了动态模型拉取能力。
|
||||
|
||||
## 修复
|
||||
- 修复了桌面端提示消息链路缺失的 IPC 连接。
|
||||
- 恢复了 UI 中丢失的模型管理器操作标签。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 无。现有模型配置默认可继续工作,同时你会获得更灵活的配置方式。
|
||||
|
||||
## 开发者说明
|
||||
- 这一版延续了“模型配置更动态化”的方向,尤其适合通过环境变量管理 provider 的团队。
|
||||
- 如果你在追溯图像生成元数据首次进入应用 UI 的时间点,这一版就是关键版本。
|
||||
@@ -0,0 +1,32 @@
|
||||
# Prompt Optimizer v2.6.2
|
||||
|
||||
## Summary
|
||||
- Reworked the evaluation architecture to better align workspace, result, and compare flows.
|
||||
- Refactored analysis and evaluation handling so the UI feels more consistent across prompt-testing paths.
|
||||
- Tightened variable handling and refreshed test coverage around the new evaluation protocol.
|
||||
|
||||
## Highlights
|
||||
- Evaluation logic was rebuilt around a cleaner protocol and template-builder design.
|
||||
- Analysis, result review, and compare evaluation flows now line up more naturally in the UI.
|
||||
- Variable-heavy scenarios became safer, especially around result evaluation in pro-variable workflows.
|
||||
|
||||
## Product Updates
|
||||
### Web
|
||||
- Refactored analysis and evaluation flows around workspace, result, and compare states.
|
||||
- Improved the overall evaluation journey so users can move more clearly between prompt drafting, analysis, and comparison.
|
||||
|
||||
### Core/Infra
|
||||
- Redesigned the evaluation protocol and template builders underneath the app.
|
||||
- Refreshed unit, e2e, and VCR coverage to match the new protocol shape.
|
||||
- Consolidated workspace evaluation docs and acceptance evidence around the newer flow.
|
||||
|
||||
## Fixes
|
||||
- Stripped internal predefined variables from pro-variable result evaluation output.
|
||||
- Refreshed fixtures and coverage to stabilize evaluation behavior after the protocol redesign.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None for end users. Internally, evaluation-related template and protocol behavior changed substantially.
|
||||
|
||||
## Developer Notes
|
||||
- This release is an architectural checkpoint for evaluation flows. If you compare behavior before and after the newer workspace/result/compare model, `v2.6.2` is the main transition point.
|
||||
- Teams maintaining evaluation prompts or result panels should expect more coordinated flow boundaries from this version onward.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Prompt Optimizer v2.6.2
|
||||
|
||||
## 概括
|
||||
- 重构了评估架构,让工作区、结果页和对比流程之间的关系更清晰。
|
||||
- 分析与评估流程做了成体系整理,整体交互在不同测试路径上更一致。
|
||||
- 变量处理和评估覆盖也一起补强了,特别是 Pro Variable 场景下更稳。
|
||||
|
||||
## 亮点
|
||||
- 底层评估逻辑围绕新的协议和模板构建器重新梳理了一遍。
|
||||
- 分析、结果查看和对比评估在 UI 层终于更自然地串起来了。
|
||||
- 变量较多的场景更安全了,尤其是 Pro Variable 的结果评估输出更干净。
|
||||
|
||||
## 产品更新
|
||||
### Web
|
||||
- 围绕工作区、结果和对比状态,重构了分析与评估流程。
|
||||
- 优化了从提示词编写、分析到对比的整体评估路径,让流程切换更清楚。
|
||||
|
||||
### Core/Infra
|
||||
- 重新设计了应用底层的评估协议和模板构建器。
|
||||
- 刷新了单元测试、E2E 和 VCR 覆盖,使其与新的协议结构一致。
|
||||
- 归并了工作区评估相关文档和验收证据,减少旧流程与新流程之间的割裂。
|
||||
|
||||
## 修复
|
||||
- 从 Pro Variable 的结果评估输出中移除了内部预定义变量。
|
||||
- 在评估协议重构后刷新了测试夹具与覆盖,提升了行为稳定性。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 对终端用户无破坏性变更;但从内部实现看,评估协议和模板行为发生了较大调整。
|
||||
|
||||
## 开发者说明
|
||||
- 这一版可以看作评估链路的一个架构转折点。若要比较“旧评估流”和“新工作区 / 结果 / 对比模型”,`v2.6.2` 是关键界线。
|
||||
- 维护评估提示词或结果面板的同学,从这一版开始需要接受更明确的流程边界。
|
||||
@@ -0,0 +1,32 @@
|
||||
# Prompt Optimizer v2.6.3
|
||||
|
||||
## Summary
|
||||
- Added image prompt extraction so existing images can be turned back into reusable prompt material.
|
||||
- Introduced stronger image understanding support in core services and extraction templates.
|
||||
- Tightened the desktop release pipeline and refreshed the default MiniMax model baseline.
|
||||
|
||||
## Highlights
|
||||
- Image prompt extraction became a first-class workflow, making it easier to reverse engineer prompts from generated or reference images.
|
||||
- Core image understanding capabilities expanded to support that extraction flow more cleanly.
|
||||
- Release stability improved through desktop pipeline fixes and a more up-to-date MiniMax default model.
|
||||
|
||||
## Product Updates
|
||||
### Web
|
||||
- Added an image prompt extraction workflow in the UI.
|
||||
- Connected image extraction behavior to the new image understanding and prompt-template support underneath.
|
||||
|
||||
### Core/Infra
|
||||
- Added an image understanding service and extraction-focused templates in core.
|
||||
- Upgraded the default MiniMax model baseline to `M2.7`.
|
||||
- Cleaned up release automation so desktop builds no longer auto-publish releases unexpectedly.
|
||||
|
||||
## Fixes
|
||||
- Fixed the desktop release pipeline so build jobs stop auto-publishing releases.
|
||||
- Reduced the chance of stale default-model behavior by aligning MiniMax defaults with the newer model generation.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None. This release extends image tooling without requiring migration steps.
|
||||
|
||||
## Developer Notes
|
||||
- This version is the real starting point for image prompt extraction in the current codebase.
|
||||
- If you are tracing when image understanding moved from isolated logic into reusable release-ready workflows, this is the version to reference.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Prompt Optimizer v2.6.3
|
||||
|
||||
## 概括
|
||||
- 新增图像提示词提取能力,可以把已有图片重新转成可复用的提示词素材。
|
||||
- Core 层补上了更完整的图像理解服务与提取模板支撑。
|
||||
- 桌面端发布链路更稳了,同时也更新了默认 MiniMax 模型基线。
|
||||
|
||||
## 亮点
|
||||
- 图像提示词提取正式成为一套工作流,更适合从生成图或参考图中反向整理提示词。
|
||||
- 核心图像理解能力进一步补强,为提取流程提供了更完整的底层支持。
|
||||
- 通过修复桌面端发布流程和更新 MiniMax 默认模型,整体发布稳定性更进一步。
|
||||
|
||||
## 产品更新
|
||||
### Web
|
||||
- 在 UI 中新增了图像提示词提取工作流。
|
||||
- 让图像提取行为可以直接复用新的图像理解能力和提取模板。
|
||||
|
||||
### Core/Infra
|
||||
- 在 Core 中新增图像理解服务以及面向提取场景的模板。
|
||||
- 将默认 MiniMax 模型基线升级到 `M2.7`。
|
||||
- 清理了发布自动化逻辑,避免桌面端构建任务意外自动发布 Release。
|
||||
|
||||
## 修复
|
||||
- 修复了桌面端发布流程,避免构建任务自动发布 Release。
|
||||
- 通过更新 MiniMax 默认模型,降低默认模型配置过旧带来的行为偏差。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 无。这一版扩展的是图像能力,不需要额外迁移步骤。
|
||||
|
||||
## 开发者说明
|
||||
- 如果你想追溯当前代码库里“图像提示词提取”正式落地的起点,这一版就是关键版本。
|
||||
- 这一版也标志着图像理解能力从相对独立的逻辑,开始转向可复用、可发布的正式工作流。
|
||||
@@ -0,0 +1,37 @@
|
||||
# Prompt Optimizer v2.7.0
|
||||
|
||||
## Summary
|
||||
- Added a structured compare evaluation workflow so prompt comparison is easier to test, calibrate, and trust.
|
||||
- Smoothed the compare experience across test panels, fallback logic, and workspace reactivity.
|
||||
- Refreshed docs, marketing surfaces, and extension assets so the project is easier to discover and explain.
|
||||
|
||||
## Highlights
|
||||
- Introduced structured compare evaluation as a first-class workflow for prompt testing and analysis.
|
||||
- Made compare UX feel lighter and more reliable across result panels and workspace state.
|
||||
- Refreshed the public-facing surfaces with a standalone marketing site, updated demos, and localized extension store assets.
|
||||
|
||||
## Product Updates
|
||||
### Web
|
||||
- Added the structured compare workflow to test panels and simplified the compare evaluation experience.
|
||||
- Kept workspace test state reactive across language switches and restore flows.
|
||||
- Refined the About / resources area and removed compare hints when the current context does not support them.
|
||||
|
||||
### Extension
|
||||
- Added localized store screenshots and refreshed derived logo assets for store-facing materials.
|
||||
|
||||
### Core/Infra
|
||||
- Added a structured compare evaluation pipeline and calibration artifacts to support more stable comparison output.
|
||||
- Refined evaluation rewrite behavior and aligned compare design docs with the new workflow.
|
||||
- Improved docs build reliability while redesigning the onboarding and bilingual docs experience.
|
||||
|
||||
## Fixes
|
||||
- Hid misleading teacher-style compare hints when no reference context is available.
|
||||
- Fixed compare-version fallback to use the conversation record ID more reliably.
|
||||
- Cleaned up MkDocs build issues that were blocking parts of the docs deployment flow.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None. Existing data and workflows should continue to work without manual migration.
|
||||
|
||||
## Developer Notes
|
||||
- This release is the main handoff point for the structured compare architecture. If you maintain evaluation prompts or compare-related UI, use this version as the new baseline.
|
||||
- The commit range also includes marketing-site and documentation work, but the dominant product change is the structured compare stack.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Prompt Optimizer v2.7.0
|
||||
|
||||
## 概括
|
||||
- 新增了结构化对比评估工作流,让提示词对比更容易测试、校准和复用。
|
||||
- 对比体验在测试面板、回退逻辑和工作区状态同步上都更顺了。
|
||||
- 文档、官网展示和扩展商店素材也做了明显刷新,更方便理解和传播产品。
|
||||
|
||||
## 亮点
|
||||
- 结构化对比评估正式成为一套完整工作流,可直接用于提示词测试与分析。
|
||||
- 对比相关的交互体验更轻、更稳,结果面板和工作区状态协同也更自然。
|
||||
- 项目的对外展示面焕然一新,包括独立营销站、更新后的演示内容,以及本地化扩展商店素材。
|
||||
|
||||
## 产品更新
|
||||
### Web
|
||||
- 在测试面板中加入了结构化对比工作流,并简化了整体对比评估体验。
|
||||
- 让工作区测试状态在语言切换和恢复流程中保持响应式同步。
|
||||
- 优化了 About / 资源区域,并在不具备参考上下文时隐藏容易误导的对比提示。
|
||||
|
||||
### Extension
|
||||
- 补充了本地化商店截图,并刷新了面向商店展示的衍生 logo 资源。
|
||||
|
||||
### Core/Infra
|
||||
- 新增结构化对比评估管线和校准产物,为更稳定的对比输出打下基础。
|
||||
- 优化了评估重写行为,并让对比设计文档与新工作流保持一致。
|
||||
- 在重做双语文档与 onboarding 体验的同时,也提升了文档构建稳定性。
|
||||
|
||||
## 修复
|
||||
- 在缺少参考上下文时,隐藏了容易误导用户的 teacher 风格对比提示。
|
||||
- 修复了对比版本回退逻辑,改为更稳定地使用会话记录 ID。
|
||||
- 清理了影响文档部署流程的 MkDocs 构建问题。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 无。现有数据与工作流默认不需要手动迁移。
|
||||
|
||||
## 开发者说明
|
||||
- 这一版可以视为结构化对比架构的正式落点。如果你维护评估提示词或对比相关 UI,建议从这一版开始作为新基线。
|
||||
- 这一段提交里也包含营销站和文档面的工作,但真正的产品主轴仍然是结构化对比能力。
|
||||
@@ -0,0 +1,38 @@
|
||||
# Prompt Optimizer v2.8.0
|
||||
|
||||
## Summary
|
||||
- Expanded text-to-image evaluation with multimodal review, result inspection, and original-prompt analysis.
|
||||
- Smoothed the reference-image workflow from dialog design and prompt migration to safer preview and download actions.
|
||||
- Strengthened the platform foundation with Cloudflare-backed providers and a more reliable desktop packaging setup.
|
||||
|
||||
## Highlights
|
||||
- Expanded the text-to-image workflow with multimodal evaluation, result review, and original-prompt analysis.
|
||||
- Made reference-image workflows much smoother, from dialog design and prompt migration to safer preview and download actions.
|
||||
- Strengthened the model and packaging foundations with new Cloudflare-backed providers and a more reliable desktop release setup.
|
||||
|
||||
## Product Updates
|
||||
### Desktop
|
||||
- Upgraded the desktop packaging toolchain to `electron-builder 26.8.2`.
|
||||
- Removed repository-level Electron mirror defaults that could interfere with CI release builds.
|
||||
|
||||
### Web
|
||||
- Redesigned the text-to-image reference dialog so style migration and reference replication feel more direct.
|
||||
- Added the new text-to-image evaluation workflow to the UI, including analysis and evaluation entry points around image generation.
|
||||
- Improved preview downloads, temporary-variable state persistence, and several smaller interaction details in headers, popovers, and navigation guards.
|
||||
|
||||
### Core/Infra
|
||||
- Added Cloudflare-backed text and image providers, plus multimodal support for text-to-image evaluation.
|
||||
- Rebuilt the reference-prompt pipeline to better support composition, migration, compare evaluation, and result evaluation flows.
|
||||
- Normalized legacy template identifiers and wrapped evidence payloads in structured JSON blocks so evaluation behavior stays more consistent.
|
||||
|
||||
## Fixes
|
||||
- Fixed the preview download hit area and normalized URL-based image results.
|
||||
- Hardened safe image preview downloads and stabilized the Cloudflare-backed reference-image flow.
|
||||
- Backfilled missing built-in model API keys and aligned evaluation fixtures with the updated image workflow.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None. Existing saved data and current workspaces should continue to work without migration steps.
|
||||
|
||||
## Developer Notes
|
||||
- This release reshapes a large part of the image prompt pipeline. If you maintain custom templates or evaluation prompts, expect more structured evidence blocks and more explicit stage-aware evaluation paths.
|
||||
- The commit range from `v2.7.0` to `v2.8.0` also includes repo cleanup, dependency alignment, and documentation reshaping work, but the user-facing center of gravity is clearly the image evaluation stack.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Prompt Optimizer v2.8.0
|
||||
|
||||
## 概括
|
||||
- 文生图工作流进一步增强,补齐了多模态评估、结果评估和原始提示词分析能力。
|
||||
- 参考图相关流程顺了很多,从对话框设计、提示词迁移到更安全的预览与下载都做了打磨。
|
||||
- 模型接入和打包底座也更稳了,新增了基于 Cloudflare 的 provider,同时让桌面端发布链路更可靠。
|
||||
|
||||
## 亮点
|
||||
- 文生图工作流进一步增强,补齐了多模态评估、结果评估和原始提示词分析能力。
|
||||
- 参考图相关流程顺了很多,从对话框设计、提示词迁移到更安全的预览与下载都做了打磨。
|
||||
- 模型接入和打包底座也更稳了,新增了基于 Cloudflare 的 provider,同时让桌面端发布链路更可靠。
|
||||
|
||||
## 产品更新
|
||||
### Desktop
|
||||
- 桌面端打包工具链升级到 `electron-builder 26.8.2`。
|
||||
- 移除了仓库级 Electron mirror 默认配置,避免 CI 发布构建被错误环境变量干扰。
|
||||
|
||||
### Web
|
||||
- 重做了文生图参考图对话框,让风格迁移和参考图复制这类操作更直接。
|
||||
- 把新的文生图评估工作流接入到了 UI,包括围绕图片生成的分析和评估入口。
|
||||
- 改进了预览下载体验、临时变量状态持久化,以及页头、浮层、路由守卫等一批交互细节。
|
||||
|
||||
### Core/Infra
|
||||
- 新增了基于 Cloudflare 的文本与图像 Provider,并补齐多模态文生图评估能力。
|
||||
- 重建了参考提示词处理链路,更好地支撑提示词组合、迁移、对比评估和结果评估。
|
||||
- 规范了旧模板标识,并把证据内容包装成结构化 JSON 块,让评估行为更稳定一致。
|
||||
|
||||
## 修复
|
||||
- 修复了预览下载按钮点击区域问题,并统一了 URL 图片结果的处理。
|
||||
- 加固了安全图片预览下载流程,并稳定了基于 Cloudflare 的参考图链路。
|
||||
- 回填了内置模型缺失的 API Key 配置,同时让评估夹具与新的图像工作流保持一致。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 无。现有保存数据与当前工作区默认不需要额外迁移步骤。
|
||||
|
||||
## 开发者说明
|
||||
- 这一版对图像提示词链路做了较大幅度重整。如果你维护自定义模板或评估提示词,需要关注更结构化的证据块,以及更明确的阶段感知评估路径。
|
||||
- 从 `v2.7.0` 到 `v2.8.0` 的提交范围里也包含仓库清理、依赖对齐和文档整理,但真正面向用户的主轴还是图像评估能力的提升。
|
||||
@@ -0,0 +1,679 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const LOCALE_CONFIG = {
|
||||
en: {
|
||||
fileSuffix: 'en',
|
||||
summaryHeading: '## Summary',
|
||||
headings: [
|
||||
'## Summary',
|
||||
'## Highlights',
|
||||
'## Product Updates',
|
||||
'## Fixes',
|
||||
'## Breaking Changes / Upgrade Notes',
|
||||
'## Developer Notes',
|
||||
],
|
||||
productSectionHeading: '## Product Updates',
|
||||
productSubsections: ['### Desktop', '### Web', '### Extension', '### Core/Infra'],
|
||||
},
|
||||
'zh-CN': {
|
||||
fileSuffix: 'zh-CN',
|
||||
summaryHeading: '## 概括',
|
||||
headings: [
|
||||
'## 概括',
|
||||
'## 亮点',
|
||||
'## 产品更新',
|
||||
'## 修复',
|
||||
'## 破坏性变更 / 升级说明',
|
||||
'## 开发者说明',
|
||||
],
|
||||
productSectionHeading: '## 产品更新',
|
||||
productSubsections: ['### Desktop', '### Web', '### Extension', '### Core/Infra'],
|
||||
},
|
||||
};
|
||||
|
||||
const PLACEHOLDER_PATTERNS = [/\bTODO\b/i, /\bTBD\b/i, /待补充/, /(^|[^A-Z])XX([^A-Z]|$)/];
|
||||
|
||||
function readRootPackage(cwd = process.cwd()) {
|
||||
return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
||||
}
|
||||
|
||||
function normalizeVersion(value, cwd = process.cwd()) {
|
||||
const candidate = String(value || readRootPackage(cwd).version).trim();
|
||||
|
||||
if (!/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(candidate)) {
|
||||
throw new Error(`Invalid version "${candidate}". Expected x.y.z or vx.y.z.`);
|
||||
}
|
||||
|
||||
return candidate.startsWith('v') ? candidate.slice(1) : candidate;
|
||||
}
|
||||
|
||||
function getTagVersion(version, cwd = process.cwd()) {
|
||||
return `v${normalizeVersion(version, cwd)}`;
|
||||
}
|
||||
|
||||
function getLocaleConfig(locale) {
|
||||
const config = LOCALE_CONFIG[locale];
|
||||
if (!config) {
|
||||
throw new Error(`Unsupported locale "${locale}". Expected one of: ${Object.keys(LOCALE_CONFIG).join(', ')}.`);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function getReleaseNotesRelativePath(version, locale, cwd = process.cwd()) {
|
||||
const normalizedVersion = normalizeVersion(version, cwd);
|
||||
const config = getLocaleConfig(locale);
|
||||
return `releases/v${normalizedVersion}.${config.fileSuffix}.md`;
|
||||
}
|
||||
|
||||
function getReleaseNotesPath({ cwd = process.cwd(), version, locale }) {
|
||||
return path.join(cwd, getReleaseNotesRelativePath(version, locale, cwd));
|
||||
}
|
||||
|
||||
function getReleaseNotesPaths({ cwd = process.cwd(), version }) {
|
||||
return {
|
||||
en: getReleaseNotesPath({ cwd, version, locale: 'en' }),
|
||||
zhCN: getReleaseNotesPath({ cwd, version, locale: 'zh-CN' }),
|
||||
};
|
||||
}
|
||||
|
||||
function stripHtmlComments(content) {
|
||||
return content.replace(/<!--[\s\S]*?-->/g, '').trim();
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function getHeadingIndex(block, heading) {
|
||||
const match = new RegExp(`^${escapeRegExp(heading)}\\s*$`, 'm').exec(block);
|
||||
return match ? match.index : -1;
|
||||
}
|
||||
|
||||
function findSection(content, heading, nextHeading = null) {
|
||||
const startIndex = getHeadingIndex(content, heading);
|
||||
if (startIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bodyStart = content.indexOf('\n', startIndex);
|
||||
if (bodyStart === -1) {
|
||||
return content.slice(startIndex).trim();
|
||||
}
|
||||
|
||||
if (!nextHeading) {
|
||||
return content.slice(startIndex).trim();
|
||||
}
|
||||
|
||||
const remaining = content.slice(bodyStart + 1);
|
||||
const nextIndex = getHeadingIndex(remaining, nextHeading);
|
||||
if (nextIndex === -1) {
|
||||
return content.slice(startIndex).trim();
|
||||
}
|
||||
|
||||
return content.slice(startIndex, bodyStart + 1 + nextIndex).trim();
|
||||
}
|
||||
|
||||
function extractSectionBody(content, heading, nextHeading = null) {
|
||||
const section = findSection(content, heading, nextHeading);
|
||||
if (!section) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstLineBreak = section.indexOf('\n');
|
||||
if (firstLineBreak === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return section.slice(firstLineBreak + 1).trim();
|
||||
}
|
||||
|
||||
function validateHeadingOrder(block, headings, label) {
|
||||
const errors = [];
|
||||
let lastIndex = -1;
|
||||
|
||||
for (const heading of headings) {
|
||||
const headingIndex = getHeadingIndex(block, heading);
|
||||
if (headingIndex === -1) {
|
||||
errors.push(`${label} section is missing required heading "${heading}".`);
|
||||
continue;
|
||||
}
|
||||
if (headingIndex < lastIndex) {
|
||||
errors.push(`${label} section has heading "${heading}" out of order.`);
|
||||
}
|
||||
lastIndex = headingIndex;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateProductSubsectionOrder(content, locale) {
|
||||
const config = getLocaleConfig(locale);
|
||||
const label = locale === 'en' ? 'English' : '中文';
|
||||
const nextHeading = config.headings[config.headings.indexOf(config.productSectionHeading) + 1] ?? null;
|
||||
const productSection = findSection(content, config.productSectionHeading, nextHeading);
|
||||
|
||||
if (!productSection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
let lastIndex = -1;
|
||||
let previousHeading = null;
|
||||
|
||||
for (const heading of config.productSubsections) {
|
||||
const headingIndex = getHeadingIndex(productSection, heading);
|
||||
if (headingIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
if (headingIndex < lastIndex) {
|
||||
errors.push(`${label} Product Updates subsection "${heading}" must appear after "${previousHeading}".`);
|
||||
}
|
||||
lastIndex = headingIndex;
|
||||
previousHeading = heading;
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function findPlaceholderMatches(content) {
|
||||
const matches = [];
|
||||
|
||||
for (const pattern of PLACEHOLDER_PATTERNS) {
|
||||
const matched = content.match(pattern);
|
||||
if (matched) {
|
||||
matches.push(matched[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(matches)];
|
||||
}
|
||||
|
||||
function validateReleaseNotesContent(content, version, locale, options = {}) {
|
||||
const config = getLocaleConfig(locale);
|
||||
const requireSummary = options.requireSummary !== false;
|
||||
const errors = [];
|
||||
const normalizedContent = stripHtmlComments(content).replace(/\r\n/g, '\n');
|
||||
const expectedTitle = `# Prompt Optimizer ${getTagVersion(version)}`;
|
||||
|
||||
if (!new RegExp(`^${escapeRegExp(expectedTitle)}\\s*$`, 'm').test(normalizedContent)) {
|
||||
errors.push(`Release notes title must be "${expectedTitle}" in ${locale}.`);
|
||||
}
|
||||
|
||||
errors.push(...validateHeadingOrder(normalizedContent, config.headings, locale === 'en' ? 'English' : '中文'));
|
||||
errors.push(...validateProductSubsectionOrder(normalizedContent, locale));
|
||||
|
||||
if (requireSummary) {
|
||||
const summaryBody = extractSectionBody(
|
||||
normalizedContent,
|
||||
config.summaryHeading,
|
||||
config.headings[1] ?? null
|
||||
);
|
||||
if (!summaryBody) {
|
||||
errors.push(`Release notes must include a non-empty "${config.summaryHeading}" section in ${locale}.`);
|
||||
}
|
||||
}
|
||||
|
||||
const placeholders = findPlaceholderMatches(normalizedContent);
|
||||
if (placeholders.length > 0) {
|
||||
errors.push(
|
||||
`Release notes contain placeholder content in ${locale}: ${placeholders.join(', ')}. Replace placeholders before publishing.`
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateChangelogEntry(changelogContent, version, options = {}) {
|
||||
const requireTopEntry = options.requireTopEntry !== false;
|
||||
const errors = [];
|
||||
const normalizedContent = changelogContent.replace(/\r\n/g, '\n');
|
||||
const headingMatches = [...normalizedContent.matchAll(/^## \[([^\]]+)\] - .+$/gm)];
|
||||
|
||||
if (headingMatches.length === 0) {
|
||||
return ['CHANGELOG.md must include at least one version entry.'];
|
||||
}
|
||||
|
||||
const topEntry = headingMatches[0];
|
||||
const topVersion = topEntry[1];
|
||||
const normalizedVersion = normalizeVersion(version);
|
||||
if (requireTopEntry && topVersion !== normalizedVersion) {
|
||||
errors.push(`CHANGELOG.md top entry must be version ${normalizedVersion}.`);
|
||||
}
|
||||
|
||||
const matchedEntry = requireTopEntry ? topEntry : headingMatches.find((match) => match[1] === normalizedVersion);
|
||||
if (!matchedEntry) {
|
||||
errors.push(`CHANGELOG.md must include version ${normalizedVersion}.`);
|
||||
return errors;
|
||||
}
|
||||
|
||||
const matchedEntryIndex = headingMatches.findIndex((match) => match.index === matchedEntry.index);
|
||||
const entryStart = matchedEntry.index ?? 0;
|
||||
const nextEntryStart = headingMatches[matchedEntryIndex + 1]?.index ?? normalizedContent.length;
|
||||
const matchedEntryBlock = normalizedContent.slice(entryStart, nextEntryStart);
|
||||
const englishPath = getReleaseNotesRelativePath(version, 'en');
|
||||
const chinesePath = getReleaseNotesRelativePath(version, 'zh-CN');
|
||||
|
||||
if (!matchedEntryBlock.includes(englishPath)) {
|
||||
errors.push(
|
||||
requireTopEntry
|
||||
? `CHANGELOG.md top entry must link to ${englishPath}.`
|
||||
: `CHANGELOG.md entry for version ${normalizedVersion} must link to ${englishPath}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!matchedEntryBlock.includes(chinesePath)) {
|
||||
errors.push(
|
||||
requireTopEntry
|
||||
? `CHANGELOG.md top entry must link to ${chinesePath}.`
|
||||
: `CHANGELOG.md entry for version ${normalizedVersion} must link to ${chinesePath}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!/^- EN:/m.test(matchedEntryBlock)) {
|
||||
errors.push(
|
||||
requireTopEntry
|
||||
? 'CHANGELOG.md top entry must include an English summary line prefixed with "- EN:".'
|
||||
: `CHANGELOG.md entry for version ${normalizedVersion} must include an English summary line prefixed with "- EN:".`
|
||||
);
|
||||
}
|
||||
|
||||
if (!/^- 中文[::]/m.test(matchedEntryBlock)) {
|
||||
errors.push(
|
||||
requireTopEntry
|
||||
? 'CHANGELOG.md top entry must include a Chinese summary line prefixed with "- 中文:".'
|
||||
: `CHANGELOG.md entry for version ${normalizedVersion} must include a Chinese summary line prefixed with "- 中文:".`
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateReleaseArtifacts({ cwd = process.cwd(), version, requireSummary = true, requireTopEntry = true }) {
|
||||
const normalizedVersion = normalizeVersion(version, cwd);
|
||||
const filePaths = getReleaseNotesPaths({ cwd, version: normalizedVersion });
|
||||
const errors = [];
|
||||
|
||||
for (const [locale, filePath] of [
|
||||
['en', filePaths.en],
|
||||
['zh-CN', filePaths.zhCN],
|
||||
]) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
errors.push(`Release notes file is missing: ${path.relative(cwd, filePath).replace(/\\/g, '/')}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
errors.push(...validateReleaseNotesContent(content, normalizedVersion, locale, { requireSummary }));
|
||||
}
|
||||
|
||||
const changelogPath = path.join(cwd, 'CHANGELOG.md');
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
errors.push('CHANGELOG.md is missing.');
|
||||
} else {
|
||||
const changelogContent = fs.readFileSync(changelogPath, 'utf8');
|
||||
errors.push(...validateChangelogEntry(changelogContent, normalizedVersion, { requireTopEntry }));
|
||||
}
|
||||
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
errors,
|
||||
filePaths,
|
||||
version: normalizedVersion,
|
||||
};
|
||||
}
|
||||
|
||||
function safeExecFileSync(command, args, options) {
|
||||
try {
|
||||
return execFileSync(command, args, options);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildCommitDraft(cwd, version) {
|
||||
const currentTag = getTagVersion(version, cwd);
|
||||
const tagsOutput = safeExecFileSync('git', ['tag', '--sort=-version:refname'], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
if (!tagsOutput) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tags = tagsOutput
|
||||
.split(/\r?\n/)
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean);
|
||||
const currentTagIndex = tags.indexOf(currentTag);
|
||||
const previousTag = currentTagIndex === -1 ? tags[0] : tags[currentTagIndex + 1];
|
||||
const currentTagExists = Boolean(
|
||||
safeExecFileSync('git', ['rev-parse', '--verify', `refs/tags/${currentTag}`], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
})
|
||||
);
|
||||
|
||||
const rangeEnd = currentTagExists ? currentTag : 'HEAD';
|
||||
const logArgs = previousTag
|
||||
? ['log', '--pretty=format:- %s (%h)', `${previousTag}..${rangeEnd}`]
|
||||
: ['log', '--pretty=format:- %s (%h)', '--max-count=20'];
|
||||
const logOutput = safeExecFileSync('git', logArgs, { cwd, encoding: 'utf8' });
|
||||
|
||||
if (!logOutput) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const commits = logOutput
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 20);
|
||||
|
||||
if (commits.length === 0) {
|
||||
return previousTag
|
||||
? [`- No commits found between ${previousTag} and ${rangeEnd}.`]
|
||||
: ['- No commits found.'];
|
||||
}
|
||||
|
||||
return previousTag ? [`- Range: ${previousTag}..${rangeEnd}`, ...commits] : commits;
|
||||
}
|
||||
|
||||
function buildReleaseNotesTemplate({ version, locale, commitDraft = [] }) {
|
||||
const normalizedVersion = normalizeVersion(version);
|
||||
const config = getLocaleConfig(locale);
|
||||
const draftingLines =
|
||||
commitDraft.length > 0
|
||||
? commitDraft.join('\n')
|
||||
: '- No commit draft available. You can still write the notes manually.';
|
||||
|
||||
if (locale === 'en') {
|
||||
return `# Prompt Optimizer ${getTagVersion(normalizedVersion)}
|
||||
|
||||
## Summary
|
||||
- TODO: Add 2-4 concise bullets for the GitHub Release summary.
|
||||
|
||||
## Highlights
|
||||
- TODO: Summarize the most important user-facing change in English.
|
||||
|
||||
## Product Updates
|
||||
### Desktop
|
||||
- TODO: Add desktop-specific changes or remove this subsection if none.
|
||||
### Web
|
||||
- TODO: Add web-specific changes or remove this subsection if none.
|
||||
### Extension
|
||||
- TODO: Add extension-specific changes or remove this subsection if none.
|
||||
### Core/Infra
|
||||
- TODO: Add shared platform or infrastructure changes or remove this subsection if none.
|
||||
|
||||
## Fixes
|
||||
- TODO: Capture the most relevant fixes in English.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- TODO: Describe required upgrade actions, or write "None." when nothing changed.
|
||||
|
||||
## Developer Notes
|
||||
- TODO: Capture implementation-facing details that are worth surfacing publicly.
|
||||
|
||||
<!-- Release drafting reference:
|
||||
Remove every TODO placeholder before running "pnpm release:notes:check ${getTagVersion(normalizedVersion)}".
|
||||
|
||||
${draftingLines}
|
||||
-->
|
||||
`;
|
||||
}
|
||||
|
||||
return `# Prompt Optimizer ${getTagVersion(normalizedVersion)}
|
||||
|
||||
## 概括
|
||||
- TODO: 用 2-4 条简短要点概括本次发布,供 GitHub Release 直接引用。
|
||||
|
||||
## 亮点
|
||||
- TODO: 用中文总结本次发布最重要的用户价值。
|
||||
|
||||
## 产品更新
|
||||
### Desktop
|
||||
- TODO: 填写桌面端相关变化;如果没有,可以删除这个小节。
|
||||
### Web
|
||||
- TODO: 填写 Web 端相关变化;如果没有,可以删除这个小节。
|
||||
### Extension
|
||||
- TODO: 填写扩展端相关变化;如果没有,可以删除这个小节。
|
||||
### Core/Infra
|
||||
- TODO: 填写共享基础设施或核心能力变化;如果没有,可以删除这个小节。
|
||||
|
||||
## 修复
|
||||
- TODO: 用中文补充最重要的修复。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- TODO: 说明升级动作;如果没有,请写“无”。
|
||||
|
||||
## 开发者说明
|
||||
- TODO: 补充对开发者或自托管用户有价值的实现说明。
|
||||
|
||||
<!-- Release drafting reference:
|
||||
Remove every TODO placeholder before running "pnpm release:notes:check ${getTagVersion(normalizedVersion)}".
|
||||
|
||||
${draftingLines}
|
||||
-->
|
||||
`;
|
||||
}
|
||||
|
||||
function createReleaseNotesFiles({ cwd = process.cwd(), version, commitDraft = [], force = false }) {
|
||||
const normalizedVersion = normalizeVersion(version, cwd);
|
||||
const filePaths = getReleaseNotesPaths({ cwd, version: normalizedVersion });
|
||||
|
||||
for (const filePath of [filePaths.en, filePaths.zhCN]) {
|
||||
if (fs.existsSync(filePath) && !force) {
|
||||
throw new Error(`Release notes already exist at ${path.relative(cwd, filePath).replace(/\\/g, '/')}.`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(filePaths.en), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
filePaths.en,
|
||||
buildReleaseNotesTemplate({ version: normalizedVersion, locale: 'en', commitDraft }),
|
||||
'utf8'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
filePaths.zhCN,
|
||||
buildReleaseNotesTemplate({ version: normalizedVersion, locale: 'zh-CN', commitDraft }),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
return {
|
||||
created: true,
|
||||
filePaths,
|
||||
version: normalizedVersion,
|
||||
};
|
||||
}
|
||||
|
||||
function buildTagScopedFileUrl(repository, tag, relativePath) {
|
||||
return `https://github.com/${repository}/blob/${tag}/${relativePath}`;
|
||||
}
|
||||
|
||||
function renderMacSecurityNote(locale) {
|
||||
if (locale === 'en') {
|
||||
return [
|
||||
'### macOS Security Note',
|
||||
'If macOS says the app is "damaged" or the developer cannot be verified, this is usually a system quarantine warning rather than an app defect.',
|
||||
'',
|
||||
'```bash',
|
||||
'xattr -rd com.apple.quarantine /Applications/PromptOptimizer.app',
|
||||
'```',
|
||||
'',
|
||||
'For a downloaded DMG:',
|
||||
'',
|
||||
'```bash',
|
||||
'xattr -rd com.apple.quarantine ~/Downloads/PromptOptimizer-*.dmg',
|
||||
'```',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return [
|
||||
'### macOS 安全提示',
|
||||
'如果 macOS 提示“已损坏”或“无法验证开发者”,通常是系统隔离属性导致,并不一定是应用本身有问题。',
|
||||
'',
|
||||
'```bash',
|
||||
'xattr -rd com.apple.quarantine /Applications/PromptOptimizer.app',
|
||||
'```',
|
||||
'',
|
||||
'如果是刚下载的 DMG:',
|
||||
'',
|
||||
'```bash',
|
||||
'xattr -rd com.apple.quarantine ~/Downloads/PromptOptimizer-*.dmg',
|
||||
'```',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function renderGitHubReleaseBody({ cwd = process.cwd(), version, repository }) {
|
||||
const normalizedVersion = normalizeVersion(version, cwd);
|
||||
const tag = getTagVersion(normalizedVersion);
|
||||
const englishPath = getReleaseNotesPath({ cwd, version: normalizedVersion, locale: 'en' });
|
||||
const chinesePath = getReleaseNotesPath({ cwd, version: normalizedVersion, locale: 'zh-CN' });
|
||||
const englishContent = fs.readFileSync(englishPath, 'utf8').replace(/\r\n/g, '\n').trim();
|
||||
const chineseContent = fs.readFileSync(chinesePath, 'utf8').replace(/\r\n/g, '\n').trim();
|
||||
const englishGuideUrl = buildTagScopedFileUrl(repository, tag, 'mkdocs/docs/en/deployment/desktop.md');
|
||||
const chineseGuideUrl = buildTagScopedFileUrl(repository, tag, 'mkdocs/docs/zh/deployment/desktop.md');
|
||||
|
||||
const englishSummary = extractSectionBody(englishContent, '## Summary', '## Highlights');
|
||||
const chineseSummary = extractSectionBody(chineseContent, '## 概括', '## 亮点');
|
||||
|
||||
if (englishSummary && chineseSummary) {
|
||||
return [
|
||||
'## English',
|
||||
'',
|
||||
'### Summary',
|
||||
englishSummary,
|
||||
'',
|
||||
renderMacSecurityNote('en'),
|
||||
'',
|
||||
`Installation guide: [English](${englishGuideUrl}) | [中文](${chineseGuideUrl})`,
|
||||
`[Full release notes (EN)](${buildTagScopedFileUrl(repository, tag, getReleaseNotesRelativePath(normalizedVersion, 'en', cwd))})`,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 中文',
|
||||
'',
|
||||
'### 概括',
|
||||
chineseSummary,
|
||||
'',
|
||||
renderMacSecurityNote('zh-CN'),
|
||||
'',
|
||||
`安装文档:[English](${englishGuideUrl}) | [中文](${chineseGuideUrl})`,
|
||||
`[完整发布说明(中文)](${buildTagScopedFileUrl(repository, tag, getReleaseNotesRelativePath(normalizedVersion, 'zh-CN', cwd))})`,
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return [englishContent, '', '---', '', chineseContent, ''].join('\n');
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log('Usage: node scripts/release-notes.js <new|check|check-entry|render-body> [version] [repository]');
|
||||
console.log('Examples:');
|
||||
console.log(' pnpm release:notes:new 2.9.0');
|
||||
console.log(' pnpm release:notes:check v2.9.0');
|
||||
console.log(' pnpm release:notes:check:entry v2.6.0');
|
||||
console.log(' node scripts/release-notes.js render-body v2.9.0 linshenkx/prompt-optimizer');
|
||||
}
|
||||
|
||||
function main(argv = process.argv.slice(2), cwd = process.cwd()) {
|
||||
const [command, versionArg, repositoryArg] = argv;
|
||||
|
||||
if (!command || command === '--help' || command === '-h') {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
const version = normalizeVersion(versionArg, cwd);
|
||||
|
||||
if (command === 'new') {
|
||||
const result = createReleaseNotesFiles({
|
||||
cwd,
|
||||
version,
|
||||
commitDraft: buildCommitDraft(cwd, version),
|
||||
});
|
||||
console.log(
|
||||
`Created ${path.relative(cwd, result.filePaths.en).replace(/\\/g, '/')} and ${path
|
||||
.relative(cwd, result.filePaths.zhCN)
|
||||
.replace(/\\/g, '/')} for ${getTagVersion(version)}.`
|
||||
);
|
||||
console.log('Next steps: update both files and add the matching top entry in CHANGELOG.md.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'check') {
|
||||
const result = validateReleaseArtifacts({ cwd, version, requireSummary: true });
|
||||
if (!result.ok) {
|
||||
console.error(`Release notes validation failed for ${getTagVersion(version)}:`);
|
||||
for (const error of result.errors) {
|
||||
console.error(`- ${error}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
console.log(`Release notes validation passed for ${getTagVersion(version)}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'check-entry') {
|
||||
const result = validateReleaseArtifacts({
|
||||
cwd,
|
||||
version,
|
||||
requireSummary: true,
|
||||
requireTopEntry: false,
|
||||
});
|
||||
if (!result.ok) {
|
||||
console.error(`Release notes entry validation failed for ${getTagVersion(version)}:`);
|
||||
for (const error of result.errors) {
|
||||
console.error(`- ${error}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
console.log(`Release notes entry validation passed for ${getTagVersion(version)}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'render-body') {
|
||||
if (!repositoryArg) {
|
||||
throw new Error('render-body requires a repository argument like "linshenkx/prompt-optimizer".');
|
||||
}
|
||||
process.stdout.write(renderGitHubReleaseBody({ cwd, version, repository: repositoryArg }));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown command "${command}".`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error.message || error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildCommitDraft,
|
||||
buildReleaseNotesTemplate,
|
||||
createReleaseNotesFiles,
|
||||
extractSectionBody,
|
||||
getReleaseNotesPath,
|
||||
getReleaseNotesPaths,
|
||||
getReleaseNotesRelativePath,
|
||||
getTagVersion,
|
||||
main,
|
||||
normalizeVersion,
|
||||
renderGitHubReleaseBody,
|
||||
renderMacSecurityNote,
|
||||
validateChangelogEntry,
|
||||
validateReleaseArtifacts,
|
||||
validateReleaseNotesContent,
|
||||
};
|
||||
@@ -0,0 +1,324 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import releaseNotes from './release-notes.js';
|
||||
|
||||
const {
|
||||
buildCommitDraft,
|
||||
buildReleaseNotesTemplate,
|
||||
createReleaseNotesFiles,
|
||||
renderGitHubReleaseBody,
|
||||
validateReleaseArtifacts,
|
||||
} = releaseNotes;
|
||||
|
||||
function createTempRepo() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'prompt-optimizer-release-notes-'));
|
||||
}
|
||||
|
||||
function writeFile(root, relativePath, content) {
|
||||
const filePath = path.join(root, relativePath);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
|
||||
function runGit(root, args) {
|
||||
return execFileSync('git', args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function commitFile(root, relativePath, content, message) {
|
||||
writeFile(root, relativePath, content);
|
||||
runGit(root, ['add', relativePath]);
|
||||
runGit(root, ['commit', '-m', message]);
|
||||
}
|
||||
|
||||
function createTaggedRepo() {
|
||||
const root = createTempRepo();
|
||||
runGit(root, ['init']);
|
||||
runGit(root, ['config', 'user.name', 'Test User']);
|
||||
runGit(root, ['config', 'user.email', 'test@example.com']);
|
||||
|
||||
commitFile(root, 'notes.txt', 'first\n', 'chore: initial release');
|
||||
runGit(root, ['tag', 'v1.0.0']);
|
||||
|
||||
commitFile(root, 'notes.txt', 'second\n', 'feat: add second change');
|
||||
runGit(root, ['tag', 'v1.1.0']);
|
||||
|
||||
commitFile(root, 'notes.txt', 'third\n', 'fix: add third change');
|
||||
runGit(root, ['tag', 'v1.2.0']);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function buildValidEnglishReleaseNotes(version, options = {}) {
|
||||
const summary =
|
||||
options.includeSummary === false
|
||||
? ''
|
||||
: `## Summary
|
||||
- Text-to-image evaluation is now easier to review and compare.
|
||||
- Reference-image workflows are smoother and safer across the app.
|
||||
|
||||
`;
|
||||
|
||||
return `# Prompt Optimizer v${version}
|
||||
|
||||
${summary}## Highlights
|
||||
- Faster image prompt evaluation with clearer outputs.
|
||||
|
||||
## Product Updates
|
||||
### Desktop
|
||||
- Improved packaging metadata for GitHub Releases.
|
||||
### Web
|
||||
- Refined release surfaces for bilingual documentation.
|
||||
### Extension
|
||||
- Synced release messaging with the desktop workflow.
|
||||
### Core/Infra
|
||||
- Added release note validation for tagged builds.
|
||||
|
||||
## Fixes
|
||||
- Prevented empty release bodies from being published.
|
||||
|
||||
## Breaking Changes / Upgrade Notes
|
||||
- None.
|
||||
|
||||
## Developer Notes
|
||||
- Release notes now live in the repository and sync to GitHub Releases.
|
||||
`;
|
||||
}
|
||||
|
||||
function buildValidChineseReleaseNotes(version, options = {}) {
|
||||
const summary =
|
||||
options.includeSummary === false
|
||||
? ''
|
||||
: `## 概括
|
||||
- 文生图评估现在更容易查看和比较。
|
||||
- 参考图工作流在整个应用里都更顺手、更安全。
|
||||
|
||||
`;
|
||||
|
||||
return `# Prompt Optimizer v${version}
|
||||
|
||||
${summary}## 亮点
|
||||
- 图像提示词评估更快,结果表达也更清晰。
|
||||
|
||||
## 产品更新
|
||||
### Desktop
|
||||
- 改进了 GitHub Release 使用的桌面端打包元数据。
|
||||
### Web
|
||||
- 优化了双语发布文档的展示入口。
|
||||
### Extension
|
||||
- 让扩展端发布说明与桌面端流程保持一致。
|
||||
### Core/Infra
|
||||
- 为打标签发布增加了版本说明校验。
|
||||
|
||||
## 修复
|
||||
- 避免发布出空的 Release 正文。
|
||||
|
||||
## 破坏性变更 / 升级说明
|
||||
- 无。
|
||||
|
||||
## 开发者说明
|
||||
- 版本说明现在以仓库文件为准,并同步到 GitHub Releases。
|
||||
`;
|
||||
}
|
||||
|
||||
test('buildReleaseNotesTemplate creates the English release note skeleton with summary', () => {
|
||||
const template = buildReleaseNotesTemplate({
|
||||
version: '2.8.0',
|
||||
locale: 'en',
|
||||
commitDraft: ['- feat(ui): polish release notes', '- fix(ci): validate note files'],
|
||||
});
|
||||
|
||||
assert.match(template, /^# Prompt Optimizer v2\.8\.0/m);
|
||||
assert.match(template, /^## Summary$/m);
|
||||
assert.match(template, /^## Highlights$/m);
|
||||
assert.match(template, /^### Desktop$/m);
|
||||
assert.match(template, /^## Developer Notes$/m);
|
||||
assert.match(template, /feat\(ui\): polish release notes/);
|
||||
});
|
||||
|
||||
test('buildReleaseNotesTemplate creates the Chinese release note skeleton with 概括', () => {
|
||||
const template = buildReleaseNotesTemplate({
|
||||
version: '2.8.0',
|
||||
locale: 'zh-CN',
|
||||
commitDraft: ['- feat(core): ship bilingual release notes'],
|
||||
});
|
||||
|
||||
assert.match(template, /^# Prompt Optimizer v2\.8\.0/m);
|
||||
assert.match(template, /^## 概括$/m);
|
||||
assert.match(template, /^## 亮点$/m);
|
||||
assert.match(template, /^### Desktop$/m);
|
||||
assert.match(template, /^## 开发者说明$/m);
|
||||
assert.match(template, /feat\(core\): ship bilingual release notes/);
|
||||
});
|
||||
|
||||
test('createReleaseNotesFiles writes split English and Chinese release note files', () => {
|
||||
const root = createTempRepo();
|
||||
|
||||
const result = createReleaseNotesFiles({
|
||||
cwd: root,
|
||||
version: '2.8.0',
|
||||
commitDraft: ['- feat(core): ship bilingual release notes'],
|
||||
});
|
||||
|
||||
assert.equal(result.created, true);
|
||||
assert.equal(result.filePaths.en, path.join(root, 'releases', 'v2.8.0.en.md'));
|
||||
assert.equal(result.filePaths.zhCN, path.join(root, 'releases', 'v2.8.0.zh-CN.md'));
|
||||
assert.equal(fs.existsSync(result.filePaths.en), true);
|
||||
assert.equal(fs.existsSync(result.filePaths.zhCN), true);
|
||||
});
|
||||
|
||||
test('validateReleaseArtifacts accepts split release notes with summaries and changelog links', () => {
|
||||
const root = createTempRepo();
|
||||
writeFile(
|
||||
root,
|
||||
'CHANGELOG.md',
|
||||
[
|
||||
'# Changelog',
|
||||
'',
|
||||
'## [2.8.0] - 2026-04-04',
|
||||
'- EN: Bilingual release notes become the source of truth. See [Release Notes (EN)](releases/v2.8.0.en.md).',
|
||||
'- 中文:双语版本说明成为唯一发布来源。参见 [版本说明(中文)](releases/v2.8.0.zh-CN.md)。',
|
||||
'',
|
||||
'## [2.1.0] - 2025-01-19',
|
||||
'- Legacy entry kept for compatibility.',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
writeFile(root, 'releases/v2.8.0.en.md', buildValidEnglishReleaseNotes('2.8.0'));
|
||||
writeFile(root, 'releases/v2.8.0.zh-CN.md', buildValidChineseReleaseNotes('2.8.0'));
|
||||
|
||||
const result = validateReleaseArtifacts({
|
||||
cwd: root,
|
||||
version: '2.8.0',
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(result.errors, []);
|
||||
});
|
||||
|
||||
test('validateReleaseArtifacts blocks release when a language file or summary is missing', () => {
|
||||
const root = createTempRepo();
|
||||
writeFile(
|
||||
root,
|
||||
'CHANGELOG.md',
|
||||
[
|
||||
'# Changelog',
|
||||
'',
|
||||
'## [2.8.0] - 2026-04-04',
|
||||
'- EN: Missing the Chinese file link.',
|
||||
'- 中文:这里只有概括,没有双语文件链接。',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
writeFile(root, 'releases/v2.8.0.en.md', buildValidEnglishReleaseNotes('2.8.0'));
|
||||
writeFile(
|
||||
root,
|
||||
'releases/v2.8.0.zh-CN.md',
|
||||
buildValidChineseReleaseNotes('2.8.0', { includeSummary: false }).replace('无。', 'TODO')
|
||||
);
|
||||
|
||||
const result = validateReleaseArtifacts({
|
||||
cwd: root,
|
||||
version: '2.8.0',
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.errors.join('\n'), /releases\/v2\.8\.0\.en\.md/);
|
||||
assert.match(result.errors.join('\n'), /releases\/v2\.8\.0\.zh-CN\.md/);
|
||||
assert.match(result.errors.join('\n'), /"## 概括"/);
|
||||
assert.match(result.errors.join('\n'), /placeholder content/i);
|
||||
});
|
||||
|
||||
test('validateReleaseArtifacts can validate a matching non-top changelog entry for historical backfills', () => {
|
||||
const root = createTempRepo();
|
||||
writeFile(
|
||||
root,
|
||||
'CHANGELOG.md',
|
||||
[
|
||||
'# Changelog',
|
||||
'',
|
||||
'## [2.8.0] - 2026-04-04',
|
||||
'- EN: Current release stays on top. See [Release Notes (EN)](releases/v2.8.0.en.md).',
|
||||
'- 中文:当前版本仍然位于顶部。参见 [版本说明(中文)](releases/v2.8.0.zh-CN.md)。',
|
||||
'',
|
||||
'## [2.6.0] - 2026-03-09',
|
||||
'- EN: Historical backfill for MiniMax support. See [Release Notes (EN)](releases/v2.6.0.en.md).',
|
||||
'- 中文:为 MiniMax 支持补齐历史版本说明。参见 [版本说明(中文)](releases/v2.6.0.zh-CN.md)。',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
writeFile(root, 'releases/v2.6.0.en.md', buildValidEnglishReleaseNotes('2.6.0'));
|
||||
writeFile(root, 'releases/v2.6.0.zh-CN.md', buildValidChineseReleaseNotes('2.6.0'));
|
||||
|
||||
const blockingResult = validateReleaseArtifacts({
|
||||
cwd: root,
|
||||
version: '2.6.0',
|
||||
});
|
||||
assert.equal(blockingResult.ok, false);
|
||||
assert.match(blockingResult.errors.join('\n'), /top entry must be version 2\.6\.0/);
|
||||
|
||||
const historicalResult = validateReleaseArtifacts({
|
||||
cwd: root,
|
||||
version: '2.6.0',
|
||||
requireTopEntry: false,
|
||||
});
|
||||
assert.equal(historicalResult.ok, true);
|
||||
assert.deepEqual(historicalResult.errors, []);
|
||||
});
|
||||
|
||||
test('renderGitHubReleaseBody renders English first, then Chinese, with macOS note and guide links', () => {
|
||||
const root = createTempRepo();
|
||||
writeFile(root, 'releases/v2.8.0.en.md', buildValidEnglishReleaseNotes('2.8.0'));
|
||||
writeFile(root, 'releases/v2.8.0.zh-CN.md', buildValidChineseReleaseNotes('2.8.0'));
|
||||
|
||||
const body = renderGitHubReleaseBody({
|
||||
cwd: root,
|
||||
version: '2.8.0',
|
||||
repository: 'linshenkx/prompt-optimizer',
|
||||
});
|
||||
|
||||
assert.match(body, /^## English$/m);
|
||||
assert.match(body, /^### Summary$/m);
|
||||
assert.match(body, /^### macOS Security Note$/m);
|
||||
assert.match(body, /^Installation guide: \[English\]\(https:\/\/github\.com\/linshenkx\/prompt-optimizer\/blob\/v2\.8\.0\/mkdocs\/docs\/en\/deployment\/desktop\.md\) \| \[中文\]\(https:\/\/github\.com\/linshenkx\/prompt-optimizer\/blob\/v2\.8\.0\/mkdocs\/docs\/zh\/deployment\/desktop\.md\)$/m);
|
||||
assert.match(body, /\[Full release notes \(EN\)\]\(https:\/\/github\.com\/linshenkx\/prompt-optimizer\/blob\/v2\.8\.0\/releases\/v2\.8\.0\.en\.md\)/);
|
||||
assert.match(body, /^---$/m);
|
||||
assert.match(body, /^## 中文$/m);
|
||||
assert.match(body, /^### 概括$/m);
|
||||
assert.match(body, /^### macOS 安全提示$/m);
|
||||
assert.match(body, /^安装文档:\[English\]\(https:\/\/github\.com\/linshenkx\/prompt-optimizer\/blob\/v2\.8\.0\/mkdocs\/docs\/en\/deployment\/desktop\.md\) \| \[中文\]\(https:\/\/github\.com\/linshenkx\/prompt-optimizer\/blob\/v2\.8\.0\/mkdocs\/docs\/zh\/deployment\/desktop\.md\)$/m);
|
||||
assert.match(body, /\[完整发布说明(中文)\]\(https:\/\/github\.com\/linshenkx\/prompt-optimizer\/blob\/v2\.8\.0\/releases\/v2\.8\.0\.zh-CN\.md\)/);
|
||||
});
|
||||
|
||||
test('renderGitHubReleaseBody falls back to full text when summaries are absent', () => {
|
||||
const root = createTempRepo();
|
||||
writeFile(root, 'releases/v2.8.0.en.md', buildValidEnglishReleaseNotes('2.8.0', { includeSummary: false }));
|
||||
writeFile(root, 'releases/v2.8.0.zh-CN.md', buildValidChineseReleaseNotes('2.8.0', { includeSummary: false }));
|
||||
|
||||
const body = renderGitHubReleaseBody({
|
||||
cwd: root,
|
||||
version: '2.8.0',
|
||||
repository: 'linshenkx/prompt-optimizer',
|
||||
});
|
||||
|
||||
assert.match(body, /^# Prompt Optimizer v2\.8\.0/m);
|
||||
assert.doesNotMatch(body, /^\[Full release notes \(EN\)\]/m);
|
||||
});
|
||||
|
||||
test('buildCommitDraft uses the adjacent older tag for historical tagged versions', () => {
|
||||
const root = createTaggedRepo();
|
||||
|
||||
const draft = buildCommitDraft(root, '1.1.0');
|
||||
|
||||
assert.equal(draft[0], '- Range: v1.0.0..v1.1.0');
|
||||
assert.equal(draft.length, 2);
|
||||
assert.match(draft[1], /^- feat: add second change \([0-9a-f]+\)$/);
|
||||
});
|
||||
Reference in new issue
Block a user