- 模型停用无效根因: get_active_provider 两条路径均不校验 enabled(显式活跃直返/默认只挑 is_default) → 收敛为启用集合解析,无启用 provider 明确报错 - 压缩/标题兜底模型不校验 enabled → 新增 resolve_fallback_model 纯函数(title 复用同一实现,单一源无双轨) - 上下文多夹层归位修复: gather_tool_responses 聚拢该头全部响应(此前第二个夹层之后的响应仍被判未闭合剥除) - 压缩后台任务加内容版本哨兵(取首条消息 id,否掉 len:压缩期间 loop 正常 append 会致守卫永不生效) + panic 兜底复位标志 - 902 超时类失败与其它 Fatal 区分停靠文案(点继续可续跑) - 子代理事件按 conv 改挂(route_scope) + 运行期事件推父会话视图 - 飞书 13a 真根因: run_stream_refresher 死代码(提前 drop)+静默钟被心跳重置 → 帧通道+仅内容事件重置 - 项目详情文件查看: F22 目录不存在误显非 Git 仓库 / F24 模块为仓库子目录时 git 徽标全失效(scope_git_path 按 show-prefix 剥前缀) - 更新徽章通道错配(Tauri 事件 vs window CustomEvent 同名不同通道,徽章此前永不触发) - ProjectCard 卡片根 keydown 顶替内嵌按钮(补 e.target 守卫) - i18n 坏键 ai.aiTool.rejectedHint → aiTool.rejectedHint(双缺会渲染裸 key) - miniapp 三处: 会话列表刷新漏网/看门狗节流致新一轮无兜底/newConversation 漏清看门狗
237 lines
6.2 KiB
Vue
237 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import { ref, watch } from 'vue'
|
|
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
|
import { useAiChat } from '@/composables/useAiChat'
|
|
import type { Conversation } from '@/types/events'
|
|
|
|
const ai = useAiChat()
|
|
|
|
/**
|
|
* mp-weixin 模板对嵌套在对象中的 reactive/ref 响应性追踪有问题。
|
|
* 用本地 ref + watch 同步,确保模板直接拿到响应式数组。
|
|
*/
|
|
const conversations = ref<Conversation[]>([])
|
|
/**
|
|
* 监听 ai.conversations 整体替换(比 deep watch 更可靠,也比只盯 length 完整)。
|
|
* 用 `[...ai.conversations]` 创建普通数组赋值给 ref,确保每次变化都触发模板更新。
|
|
*
|
|
* 为什么监听 ref 而非 length:device 回推 AiConversationList 是「整体替换」且条数常不变
|
|
* (改名后条数不变),只盯 length 时 watch 回调不触发 → 本地副本滞留旧标题。
|
|
* 监听 ref 则在赋值(含同长度替换)时即触发刷新。
|
|
*/
|
|
watch(ai.conversations, (list) => {
|
|
conversations.value = [...(list || [])]
|
|
}, { immediate: true })
|
|
|
|
/** 点击会话项切换 */
|
|
function handleSelect(convId: string): void {
|
|
ai.switchConversation(convId)
|
|
// 切换后跳回对话页
|
|
uni.switchTab({ url: '/pages/chat/index' })
|
|
}
|
|
|
|
/** 新建会话 */
|
|
function handleNew(): void {
|
|
ai.newConversation()
|
|
uni.switchTab({ url: '/pages/chat/index' })
|
|
}
|
|
|
|
/** 长按会话项重命名(对齐桌面端改会话名) */
|
|
function handleRename(conv: Conversation): void {
|
|
uni.showModal({
|
|
title: '重命名会话',
|
|
editable: true,
|
|
placeholderText: '输入新名称',
|
|
content: conv.title || '',
|
|
success: (r) => {
|
|
if (r.confirm && r.content) {
|
|
const newTitle = r.content.trim()
|
|
if (newTitle && newTitle !== conv.title) {
|
|
ai.renameConversation(conv.id, newTitle)
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
/** 下拉刷新:发 list_conversations 同步 device 会话列表(F-#95)。
|
|
* 命令异步回流经 AiConversationList 事件替换 ai.conversations(handleEvent),
|
|
* 此处仅发命令 + 收尾停止下拉动画(列表更新非同步返回,故立即停止动画即可)。 */
|
|
onPullDownRefresh(() => {
|
|
ai.refreshConversations()
|
|
uni.stopPullDownRefresh()
|
|
})
|
|
|
|
/** 格式化时间戳为可读字符串 */
|
|
function formatTime(ts?: number): string {
|
|
if (!ts) return ''
|
|
const d = new Date(ts)
|
|
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
const dd = String(d.getDate()).padStart(2, '0')
|
|
const hh = String(d.getHours()).padStart(2, '0')
|
|
const mi = String(d.getMinutes()).padStart(2, '0')
|
|
return `${mm}-${dd} ${hh}:${mi}`
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<view class="conv-page">
|
|
<!-- 顶部新建按钮 -->
|
|
<view class="new-bar" @click="handleNew">
|
|
<text class="new-icon">+</text>
|
|
<text class="new-text">新建会话</text>
|
|
</view>
|
|
|
|
<!-- 会话列表 -->
|
|
<scroll-view class="conv-list" scroll-y>
|
|
<!-- 加载中骨架屏 -->
|
|
<view v-if="conversations.length === 0 && ai.loading.value" class="conv-skeleton">
|
|
<view v-for="i in 5" :key="i" class="conv-sk-item">
|
|
<view class="conv-sk-line conv-sk-w70"></view>
|
|
<view class="conv-sk-line conv-sk-w40"></view>
|
|
</view>
|
|
</view>
|
|
<view v-else-if="conversations.length === 0" class="empty-hint">
|
|
<text>暂无历史会话</text>
|
|
<text class="empty-sub">在桌面端创建后,此处同步</text>
|
|
</view>
|
|
|
|
<view
|
|
v-for="conv in conversations"
|
|
:key="conv.id"
|
|
class="conv-item"
|
|
:class="{ active: conv.id === ai.activeConversationId.value }"
|
|
@click="handleSelect(conv.id)"
|
|
@longpress="handleRename(conv)"
|
|
>
|
|
<view class="conv-main">
|
|
<text class="conv-title">{{ conv.title || '(未命名会话)' }}</text>
|
|
<text v-if="conv.lastMessage" class="conv-last">{{ conv.lastMessage }}</text>
|
|
</view>
|
|
<text v-if="conv.updatedAt" class="conv-time">{{ formatTime(conv.updatedAt) }}</text>
|
|
</view>
|
|
<view v-if="conversations.length > 0" class="list-hint">
|
|
<text>长按会话可重命名</text>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.conv-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
background-color: $uni-bg-color;
|
|
}
|
|
|
|
.new-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 24rpx;
|
|
background-color: $uni-bg-color-dark;
|
|
border-bottom: 1rpx solid $uni-border-color;
|
|
}
|
|
|
|
.new-icon {
|
|
font-size: 40rpx;
|
|
color: $uni-color-primary;
|
|
margin-right: 16rpx;
|
|
}
|
|
|
|
.new-text {
|
|
font-size: $uni-font-size-base;
|
|
color: $uni-color-primary;
|
|
}
|
|
|
|
.conv-list {
|
|
flex: 1;
|
|
}
|
|
|
|
.empty-hint {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 200rpx 24rpx;
|
|
text {
|
|
color: $uni-text-color-disable;
|
|
font-size: $uni-font-size-base;
|
|
}
|
|
.empty-sub {
|
|
margin-top: 12rpx;
|
|
font-size: $uni-font-size-sm;
|
|
}
|
|
}
|
|
|
|
.conv-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 24rpx;
|
|
border-bottom: 1rpx solid $uni-border-color;
|
|
&.active {
|
|
background-color: $uni-bg-color-light;
|
|
border-left: 6rpx solid $uni-color-primary;
|
|
}
|
|
}
|
|
|
|
.conv-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.conv-title {
|
|
font-size: $uni-font-size-base;
|
|
color: $uni-text-color;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.conv-last {
|
|
margin-top: 8rpx;
|
|
font-size: $uni-font-size-sm;
|
|
color: $uni-text-color-grey;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.conv-time {
|
|
font-size: $uni-font-size-sm;
|
|
color: $uni-text-color-disable;
|
|
margin-left: 16rpx;
|
|
}
|
|
.list-hint {
|
|
padding: 24rpx;
|
|
text-align: center;
|
|
}
|
|
.list-hint text {
|
|
color: $uni-text-color-disable;
|
|
font-size: $uni-font-size-sm;
|
|
}
|
|
/* 骨架屏(下拉刷新/冷启动占位) */
|
|
.conv-skeleton {
|
|
padding: 24rpx;
|
|
}
|
|
.conv-sk-item {
|
|
margin-bottom: 32rpx;
|
|
padding: 24rpx;
|
|
}
|
|
.conv-sk-line {
|
|
height: 24rpx;
|
|
margin: 8rpx 0;
|
|
background: linear-gradient(90deg, $uni-bg-color-light 25%, #3a3a3a 50%, $uni-bg-color-light 75%);
|
|
background-size: 200% 100%;
|
|
animation: conv-sk-shimmer 1.5s infinite;
|
|
border-radius: 8rpx;
|
|
}
|
|
.conv-sk-w70 { width: 70%; }
|
|
.conv-sk-w40 { width: 40%; }
|
|
@keyframes conv-sk-shimmer {
|
|
0% { background-position: 200% 0; }
|
|
100% { background-position: -200% 0; }
|
|
}
|
|
</style>
|