update at 2025-10-15 15:36:54
This commit is contained in:
230
release.md
230
release.md
@@ -540,233 +540,3 @@ npm run dev
|
|||||||
|
|
||||||
**v1.0.3 - 完美的停止体验,让对话更可控!** 🎉⏸️
|
**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 - 完美的停止体验,让对话更可控!** 🎉⏸️
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
77
release.sh
77
release.sh
@@ -64,17 +64,40 @@ if [ ! -f release.md ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
if [ -z "$VERSION" ]; then
|
||||||
echo "❌ release.md 中未找到版本号"
|
echo "❌ release.md 中未找到版本号"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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 "📝 版本号: $VERSION"
|
||||||
echo "说明:"
|
echo "📌 标题: $RELEASE_TITLE"
|
||||||
echo "$TAG_MESSAGE"
|
echo "📄 内容预览:"
|
||||||
|
echo "$TAG_MESSAGE" | head -n 10
|
||||||
|
echo "..."
|
||||||
|
|
||||||
# 5. 创建 tag(如已存在则删除后重建)
|
# 5. 创建 tag(如已存在则删除后重建)
|
||||||
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
||||||
@@ -119,15 +142,49 @@ if [ -z "$GITEA_TOKEN" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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" '{
|
JSON_PAYLOAD=$(echo "$TAG_MESSAGE" | jq -R -s -c \
|
||||||
|
--arg version "$VERSION" \
|
||||||
|
--arg title "$VERSION - $RELEASE_TITLE" \
|
||||||
|
'{
|
||||||
tag_name: $version,
|
tag_name: $version,
|
||||||
name: $version,
|
name: $title,
|
||||||
body: .,
|
body: .,
|
||||||
draft: false,
|
draft: false,
|
||||||
prerelease: false
|
prerelease: false
|
||||||
}')
|
}')
|
||||||
|
|
||||||
echo "🔄 尝试创建 Release (使用中文内容)..."
|
echo "🔄 尝试创建 Release (使用中文内容)..."
|
||||||
response=$(curl -s -w "\n%{http_code}" \
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
@@ -147,11 +204,11 @@ elif [[ "$response_body" == *"Conversion from collation"* ]] || [[ "$response_bo
|
|||||||
echo "⚠️ 检测到字符集问题,尝试使用英文版本..."
|
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 \
|
JSON_PAYLOAD_EN=$(jq -n -c \
|
||||||
--arg version "$VERSION" \
|
--arg version "$VERSION" \
|
||||||
--arg name "$VERSION - Release" \
|
--arg name "$VERSION - $RELEASE_TITLE" \
|
||||||
--arg body "$ENGLISH_BODY" \
|
--arg body "$ENGLISH_BODY" \
|
||||||
'{
|
'{
|
||||||
tag_name: $version,
|
tag_name: $version,
|
||||||
|
|||||||
60
test-release-extract.sh
Executable file
60
test-release-extract.sh
Executable file
@@ -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"
|
||||||
Reference in New Issue
Block a user