From 4671f3f6ee9484f3455eea0093e4969a990b71e7 Mon Sep 17 00:00:00 2001 From: douboer Date: Wed, 15 Oct 2025 15:36:54 +0800 Subject: [PATCH] update at 2025-10-15 15:36:54 --- release.md | 230 ---------------------------------------- release.sh | 85 ++++++++++++--- test-release-extract.sh | 60 +++++++++++ 3 files changed, 131 insertions(+), 244 deletions(-) create mode 100755 test-release-extract.sh diff --git a/release.md b/release.md index 7ba633b..b626e1b 100644 --- a/release.md +++ b/release.md @@ -540,233 +540,3 @@ npm run dev **v1.0.3 - 完美的停止体验,让对话更可控!** 🎉⏸️ ---- - - -## v1.0.4 - -发布时间: 2025-10-15 - -### 🎯 重大功能:停止生成 & UI 优化 - -本版本实现了完整的停止生成功能,参考 Cherry Studio 的 PAUSED 状态设计,提供更好的用户体验。 - -#### ✨ 核心功能 - -**⏸️ 智能停止生成** -- 点击停止按钮立即中断 AI 回复(响应时间 < 100ms) -- 保留已生成的内容,标记为"已停止"状态 -- 区分用户主动停止和系统错误 -- 停止后可立即继续对话 - -**🎨 UI 体验优化** -- 按钮文字从"确认"改为"发送" -- 停止后显示黄色"已停止"标签(而非红色"发送失败") -- 停止的消息可以复制、重新生成、删除 -- 实时状态反馈(发送中 → 已停止 → 可操作) - -**🔄 状态管理增强** -- 新增 `paused` 消息状态 -- 新增 `paused` 流式事件类型 -- 完整的 AbortController 信号传递链 -- 流读取循环实时检查中止信号 - -#### 🛠️ 技术实现 - -**按钮事件修复** -- 修复点击事件绑定问题(从三元表达式改为函数调用) -- 运行时动态判断状态,而非编译时 -```typescript -// Before: @click="store.state.isSending ? handleStopGeneration : handleSendMessage" -// After: @click="handleButtonClick" -const handleButtonClick = () => { - if (store.state.isSending) { - handleStopGeneration() - } else { - handleSendMessage() - } -} -``` - -**中止信号传递链** -``` -UI (点击停止) - ↓ handleStopGeneration() - ↓ store.stopGeneration() - ↓ abortController.abort() - ↓ chatService.sendMessageStream(signal) - ↓ modelServiceManager.makeChatRequestStream(signal) - ↓ while循环检查 signal.aborted - ↓ reader.cancel() + 抛出 AbortError - ↓ 状态设置为 'paused' - ↓ UI 更新显示"已停止" -``` - -**流读取中止检查** -```typescript -while (true) { - // ⚠️ 关键:每次读取前检查中止信号 - if (signal?.aborted) { - console.log('🛑 检测到中止信号,停止读取流') - reader.cancel() - throw new DOMException('用户中止操作', 'AbortError') - } - - const { done, value } = await reader.read() - if (done) break - - // 处理数据... -} -``` - -**错误处理优化** -```typescript -catch (error) { - const isAborted = error instanceof Error && error.name === 'AbortError' - - if (isAborted) { - // 用户主动停止 - 标记为 paused,保留内容 - assistantMessage.status = 'paused' - assistantMessage.error = undefined - onChunk({ type: 'paused', messageId: assistantMessage.id }) - - // ✅ 关键:更新消息列表,触发 UI 刷新 - state.messages = [...chatService.getMessages(currentTopicId)] - } else { - // 真实错误 - 标记为 error - assistantMessage.status = 'error' - assistantMessage.error = error.message - } -} -``` - -#### 🐛 Bug 修复 - -- ✅ 修复按钮点击无响应问题(事件绑定错误) -- ✅ 修复停止后仍显示"发送中..."状态 -- ✅ 修复停止后消息列表不更新 -- ✅ 修复 AbortError 被错误标记为失败 -- ✅ 修复按钮文字显示"确认"而非"发送" - -#### 🔧 修改的文件 - -**类型定义** -- `/web/src/types/chat.ts` - - MessageStatus 添加 `'paused'` 类型 - - StreamEvent 添加 `'paused'` 事件类型 - -**UI 组件** -- `/web/src/components/Chat/ChatLayout.vue` - - 修复按钮点击事件绑定 - - 按钮文字改为"发送" - - 添加"已停止"状态标签显示 - - paused 状态消息显示操作按钮 - -**服务层** -- `/web/src/services/chatService.ts` - - 区分 AbortError 和其他错误 - - 设置 paused 状态和事件 - -- `/web/src/services/modelServiceManager.ts` - - 流读取循环中检查 signal.aborted - - 调用 reader.cancel() 中止读取 - - 正确处理 AbortError - -**状态管理** -- `/web/src/stores/chatStore.ts` - - 在 catch 块中更新消息列表 - - 确保 UI 显示最新状态 - -#### 💡 使用示例 - -``` -1. 用户发送:"请详细介绍 Vue 3 的新特性" -2. AI 开始回复,显示"发送中..." -3. 用户点击"停止"按钮 -4. 立即响应: - - 输出停止 - - 标签变为"已停止"(黄色) - - 显示已生成的内容 - - 显示操作按钮 -5. 用户可以: - - 复制已生成的内容 - - 重新生成完整回复 - - 删除该消息 - - 继续发送新消息 -``` - -#### 🎯 设计亮点 - -1. **参考 Cherry Studio** - 借鉴成熟产品的设计理念 -2. **立即响应** - 停止操作 < 100ms 响应 -3. **内容保留** - 部分生成的内容依然有价值 -4. **状态区分** - paused vs error,语义更清晰 -5. **完整操作** - 停止的消息仍可进行各种操作 -6. **信号传递** - 完整的中止信号链,确保可靠性 - -#### 📊 用户体验对比 - -**修复前 ❌** -- 点击停止无反应 -- 继续显示"发送中..." -- 显示 loading 动画 -- 按钮文字为"确认" - -**修复后 ✅** -- 点击立即停止 -- 显示"已停止"(黄色) -- 隐藏 loading 动画 -- 按钮文字为"发送" -- 可以操作停止的消息 -- 立即可继续对话 - -#### 📚 相关文档 - -- `STOP_GENERATION_SUMMARY.md` - 修复总结 -- `STOP_GENERATION_FIX.md` - 详细技术文档 -- `STOP_GENERATION_PATCH.md` - 补充修复说明 -- `STOP_GENERATION_TEST.md` - 测试指南 -- `STOP_GENERATION_VERIFY.md` - 快速验证清单 - -#### 🚀 升级指南 - -```bash -# 拉取最新代码 -git pull origin main - -# 安装依赖 -cd web && npm install - -# 启动开发服务器 -npm run dev - -# 测试停止功能 -# 1. 发送消息 -# 2. 在 AI 回复时点击"停止" -# 3. 验证显示"已停止"标签 -# 4. 验证可以继续对话 -``` - -#### ✅ 验收标准 - -- [x] 按钮点击有明显反应 -- [x] 流输出在 100ms 内停止 -- [x] 显示"已停止"而非"失败" -- [x] 保留已生成内容 -- [x] 停止后可立即继续对话 -- [x] 可对停止的消息进行操作 -- [x] 无意外错误日志 - -#### 🔜 下一步计划 - -- 停止后自动保存草稿 -- 停止历史记录统计 -- 批量停止多个会话 -- 停止原因记录(用户主动/超时/错误) -- 性能监控和优化 - -**v1.0.3 - 完美的停止体验,让对话更可控!** 🎉⏸️ - ---- - - diff --git a/release.sh b/release.sh index 9404fc4..0509cfb 100755 --- a/release.sh +++ b/release.sh @@ -64,17 +64,40 @@ if [ ! -f release.md ]; then exit 1 fi -VERSION=$(grep "^## v" release.md | tail -n 1 | sed 's/^## //') -TAG_MESSAGE=$(awk "/^## $VERSION/{flag=1;next}/^## v/{flag=0}flag" release.md) +# 提取最后一个版本号(去掉 ## 和空格) +VERSION=$(grep "^## v" release.md | tail -n 1 | sed 's/^## *//') if [ -z "$VERSION" ]; then echo "❌ release.md 中未找到版本号" exit 1 fi +# 提取该版本块的内容(从版本标题下一行到下一个版本或文件结尾) +TAG_MESSAGE=$(awk " + /^## $VERSION\$/ { flag=1; next } + /^## v[0-9]/ && flag { exit } + flag { print } +" release.md) + +# 提取标题(第一个非空的实质性内容行,通常是 "发布时间:" 后的第一行) +# 跳过空行和"发布时间:"行,取第一个 ### 标题 +RELEASE_TITLE=$(echo "$TAG_MESSAGE" | grep -m 1 "^###" | sed 's/^### *//' | sed 's/^[🎯✨🔧🐛📦]* *//') + +# 如果没有找到 ### 标题,尝试找第一个非空行 +if [ -z "$RELEASE_TITLE" ]; then + RELEASE_TITLE=$(echo "$TAG_MESSAGE" | grep -v "^$" | grep -v "^发布时间:" | head -n 1) +fi + +# 如果还是没有,使用版本号作为标题 +if [ -z "$RELEASE_TITLE" ]; then + RELEASE_TITLE="Release $VERSION" +fi + echo "📝 版本号: $VERSION" -echo "说明:" -echo "$TAG_MESSAGE" +echo "📌 标题: $RELEASE_TITLE" +echo "📄 内容预览:" +echo "$TAG_MESSAGE" | head -n 10 +echo "..." # 5. 创建 tag(如已存在则删除后重建) if git rev-parse "$VERSION" >/dev/null 2>&1; then @@ -119,15 +142,49 @@ if [ -z "$GITEA_TOKEN" ]; then exit 0 fi -# 使用 jq 生成正确的 JSON +# 7.1 检查远程是否已存在该版本的 Release,如果存在则删除 +echo "🔍 检查远程 Release 是否已存在..." +check_response=$(curl -s -w "\n%{http_code}" \ + -X GET "$GITEA_URL/api/v1/repos/$GITEA_REPO/releases/tags/$VERSION" \ + -H "Authorization: token $GITEA_TOKEN") + +check_http_code=$(echo "$check_response" | tail -n 1) +check_body=$(echo "$check_response" | sed '$d') + +if [ "$check_http_code" -eq 200 ]; then + # Release 已存在,获取 Release ID 并删除 + release_id=$(echo "$check_body" | jq -r '.id') + echo "⚠️ 远程已存在 Release $VERSION (ID: $release_id),正在删除..." + + delete_response=$(curl -s -w "\n%{http_code}" \ + -X DELETE "$GITEA_URL/api/v1/repos/$GITEA_REPO/releases/$release_id" \ + -H "Authorization: token $GITEA_TOKEN") + + delete_http_code=$(echo "$delete_response" | tail -n 1) + + if [ "$delete_http_code" -eq 204 ] || [ "$delete_http_code" -eq 200 ]; then + echo "✅ 已删除旧的 Release" + else + echo "⚠️ 删除 Release 失败 (HTTP $delete_http_code),但将继续创建新的..." + fi +elif [ "$check_http_code" -eq 404 ]; then + echo "✅ 远程不存在该 Release,可以创建" +else + echo "⚠️ 检查 Release 状态失败 (HTTP $check_http_code),但将继续创建..." +fi + +# 7.2 使用 jq 生成正确的 JSON # 首先尝试使用原始内容(中文) -JSON_PAYLOAD=$(echo "$TAG_MESSAGE" | jq -R -s -c --arg version "$VERSION" '{ - tag_name: $version, - name: $version, - body: ., - draft: false, - prerelease: false -}') +JSON_PAYLOAD=$(echo "$TAG_MESSAGE" | jq -R -s -c \ + --arg version "$VERSION" \ + --arg title "$VERSION - $RELEASE_TITLE" \ + '{ + tag_name: $version, + name: $title, + body: ., + draft: false, + prerelease: false + }') echo "🔄 尝试创建 Release (使用中文内容)..." response=$(curl -s -w "\n%{http_code}" \ @@ -147,11 +204,11 @@ elif [[ "$response_body" == *"Conversion from collation"* ]] || [[ "$response_bo echo "⚠️ 检测到字符集问题,尝试使用英文版本..." # 创建简化的英文版本 - ENGLISH_BODY="## Release Notes\n\nThis is release $VERSION.\n\nFor detailed Chinese release notes, please see:\n- release.md in the repository\n- Or visit: $GITEA_URL/$GITEA_REPO/src/branch/main/release.md\n\n### Quick Start\n\n\`\`\`bash\ngit pull origin main\ncd web && npm install\nnpm run dev\n\`\`\`" + ENGLISH_BODY="## Release Notes\n\nThis is release $VERSION: $RELEASE_TITLE\n\nFor detailed Chinese release notes, please see:\n- release.md in the repository\n- Or visit: $GITEA_URL/$GITEA_REPO/src/branch/main/release.md\n\n### Quick Start\n\n\`\`\`bash\ngit pull origin main\ncd web && npm install\nnpm run dev\n\`\`\`" JSON_PAYLOAD_EN=$(jq -n -c \ --arg version "$VERSION" \ - --arg name "$VERSION - Release" \ + --arg name "$VERSION - $RELEASE_TITLE" \ --arg body "$ENGLISH_BODY" \ '{ tag_name: $version, diff --git a/test-release-extract.sh b/test-release-extract.sh new file mode 100755 index 0000000..45e635e --- /dev/null +++ b/test-release-extract.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# 测试脚本:验证从 release.md 中提取版本信息 + +echo "🧪 测试版本信息提取逻辑" +echo "================================" + +if [ ! -f release.md ]; then + echo "❌ 未找到 release.md" + exit 1 +fi + +# 提取最后一个版本号(去掉 ## 和空格) +VERSION=$(grep "^## v" release.md | tail -n 1 | sed 's/^## *//') + +if [ -z "$VERSION" ]; then + echo "❌ release.md 中未找到版本号" + exit 1 +fi + +echo "📝 提取的版本号: $VERSION" +echo "" + +# 提取该版本块的内容(从版本标题下一行到下一个版本或文件结尾) +TAG_MESSAGE=$(awk " + /^## $VERSION\$/ { flag=1; next } + /^## v[0-9]/ && flag { exit } + flag { print } +" release.md) + +echo "📄 提取的内容长度: $(echo "$TAG_MESSAGE" | wc -l) 行" +echo "" + +# 提取标题 +RELEASE_TITLE=$(echo "$TAG_MESSAGE" | grep -m 1 "^###" | sed 's/^### *//' | sed 's/^[🎯✨🔧🐛📦]* *//') + +if [ -z "$RELEASE_TITLE" ]; then + RELEASE_TITLE=$(echo "$TAG_MESSAGE" | grep -v "^$" | grep -v "^发布时间:" | head -n 1) +fi + +if [ -z "$RELEASE_TITLE" ]; then + RELEASE_TITLE="Release $VERSION" +fi + +echo "📌 提取的标题: $RELEASE_TITLE" +echo "" + +echo "📄 完整内容预览(前20行):" +echo "--------------------------------" +echo "$TAG_MESSAGE" | head -n 20 +echo "--------------------------------" +echo "" + +echo "✅ 测试完成" +echo "" +echo "🔍 提取结果总结:" +echo " 版本号: $VERSION" +echo " 标题: $RELEASE_TITLE" +echo " 内容行数: $(echo "$TAG_MESSAGE" | wc -l)" +echo " 完整标题: $VERSION - $RELEASE_TITLE"