Version: 0.9.22.dev.260416
后端: 1. 品牌文案与聊天定位统一切到 SmartMate,并放宽非排程问答能力 - 系统人设、路由、排程、查询、交付提示统一从 SmartFlow 改为 SmartMate - 明确普通问答/生活建议/开放讨论可正常回答,deep_answer 不再输出“让我想想”等占位话术 - thinkingMode=auto 时,deep_answer 默认开启 thinking,execute 继续跟随路由决策,其余路由默认关闭 2. Memory 读取链路升级为“结构化强约束 + 语义候选”hybrid 模式,并补齐注入渲染 / Execute 消费 - 新增 read.mode、四类记忆预算、inject.renderMode 等配置及默认值 - 落地 HybridRetrieve,统一 MySQL/RAG 读侧作用域、三级去重(ID/hash/text)、统一重排与按类型预算裁剪 - 新增 FindPinnedByUser、content_hash DTO/兜底补算、legacy/RAG 共用读侧查询口径与 fallback 逻辑 - 记忆注入支持 flat/typed_v2 两种渲染,execute msg3 正式消费 memory_context,主链路注入 MemoryReader 时同步透传 memory 配置 3. Memory 第二步/第三步 handoff 与治理文档补齐 - HANDOFF_Memory向Mem0靠拢三步冲刺计划.md 从 newAgent 迁到 memory 目录,并补充“我的记忆”增删改查与最小留痕口径 - 新增 backend/memory/记忆模块第二步计划.md、backend/memory/第三步治理与观测落地计划.md,分别拆解 hybrid 读取注入闭环与治理/观测/清理路线 - 同步更新 backend/memory/Log.txt 调试日志 前端: 1. 助手输入区新增“智能编排”任务类选择器,并把 task_class_ids 作为请求 extra 透传 - 新建 frontend/src/components/assistant/TaskClassPlanningPicker.vue,支持拉取任务类列表、临时勾选、已选标签回显与清空 - 更新 frontend/src/components/dashboard/AssistantPanel.vue、frontend/src/types/dashboard.ts:Chat extra 正式建模 task_class_ids / retry 字段;当本轮带编排任务类时强制新起会话,避免把现有会话历史误混入新编排 2. 会话上下文窗口统计接入前端展示 - 更新 frontend/src/api/agent.ts、新建 frontend/src/components/assistant/ContextWindowMeter.vue、更新 frontend/src/components/dashboard/AssistantPanel.vue、frontend/src/types/dashboard.ts:接入 /agent/context-stats,兼容 object/string/null 三种返回;在输入工具栏展示 msg0~msg3 占比与预算使用率 3. 助手面板交互细节优化 - 更新 frontend/src/components/dashboard/AssistantPanel.vue:thinking 开关改为 auto/true/false 三态选择;切会话与重试后同步刷新 context stats;历史列表首屏不足时自动继续分页直到形成滚动区 仓库:无
This commit is contained in:
484
frontend/src/components/assistant/TaskClassPlanningPicker.vue
Normal file
484
frontend/src/components/assistant/TaskClassPlanningPicker.vue
Normal file
@@ -0,0 +1,484 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { getTaskClassList } from '@/api/scheduleCenter'
|
||||
import type { TaskClassListItem } from '@/types/schedule'
|
||||
|
||||
interface SelectedTaskClassSummary {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: number[]
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [taskClassIds: number[]]
|
||||
applied: [taskClassIds: number[]]
|
||||
}>()
|
||||
|
||||
const popoverVisible = ref(false)
|
||||
const taskClassLoading = ref(false)
|
||||
const taskClasses = ref<TaskClassListItem[]>([])
|
||||
const draftSelectedIds = ref<number[]>([])
|
||||
const taskClassListReady = ref(false)
|
||||
|
||||
const triggerLabel = computed(() => {
|
||||
if (props.modelValue.length <= 0) {
|
||||
return '智能编排'
|
||||
}
|
||||
return `编排 ${props.modelValue.length}`
|
||||
})
|
||||
|
||||
const selectedTaskClasses = computed<SelectedTaskClassSummary[]>(() => {
|
||||
const lookup = new Map(taskClasses.value.map((item) => [item.id, item]))
|
||||
return props.modelValue.map((taskClassId) => {
|
||||
const taskClass = lookup.get(taskClassId)
|
||||
return {
|
||||
id: taskClassId,
|
||||
name: taskClass?.name || `任务类 #${taskClassId}`,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(nextValue) => {
|
||||
if (!popoverVisible.value) {
|
||||
draftSelectedIds.value = [...nextValue]
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(popoverVisible, (visible) => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
draftSelectedIds.value = [...props.modelValue]
|
||||
void ensureTaskClassListLoaded()
|
||||
})
|
||||
|
||||
function normalizeTaskClassIds(taskClassIds: number[]) {
|
||||
const seen = new Set<number>()
|
||||
const normalized: number[] = []
|
||||
|
||||
for (const taskClassId of taskClassIds) {
|
||||
if (!Number.isInteger(taskClassId) || taskClassId <= 0 || seen.has(taskClassId)) {
|
||||
continue
|
||||
}
|
||||
seen.add(taskClassId)
|
||||
normalized.push(taskClassId)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
async function ensureTaskClassListLoaded() {
|
||||
if (taskClassLoading.value || taskClassListReady.value) {
|
||||
return
|
||||
}
|
||||
|
||||
taskClassLoading.value = true
|
||||
try {
|
||||
taskClasses.value = await getTaskClassList()
|
||||
taskClassListReady.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '任务类列表加载失败')
|
||||
} finally {
|
||||
taskClassLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDraftSelection(taskClassId: number) {
|
||||
if (draftSelectedIds.value.includes(taskClassId)) {
|
||||
draftSelectedIds.value = draftSelectedIds.value.filter((id) => id !== taskClassId)
|
||||
return
|
||||
}
|
||||
draftSelectedIds.value = [...draftSelectedIds.value, taskClassId]
|
||||
}
|
||||
|
||||
function applySelection() {
|
||||
// 1. 先在前端做一次去重和非法值过滤,避免把脏 ID 直接发给后端。
|
||||
// 2. 这里只负责提交“下一条消息要带的任务类上下文”,不负责直接触发发送。
|
||||
// 3. 提交成功后关闭弹层,让用户回到输入区继续编辑本轮提示词。
|
||||
const normalizedTaskClassIds = normalizeTaskClassIds(draftSelectedIds.value)
|
||||
emit('update:modelValue', normalizedTaskClassIds)
|
||||
emit('applied', normalizedTaskClassIds)
|
||||
popoverVisible.value = false
|
||||
}
|
||||
|
||||
function clearSelectionFromPanel() {
|
||||
draftSelectedIds.value = []
|
||||
emit('update:modelValue', [])
|
||||
emit('applied', [])
|
||||
popoverVisible.value = false
|
||||
}
|
||||
|
||||
function removeSelectedTaskClass(taskClassId: number) {
|
||||
emit(
|
||||
'update:modelValue',
|
||||
props.modelValue.filter((id) => id !== taskClassId),
|
||||
)
|
||||
}
|
||||
|
||||
function clearSelectedTaskClasses() {
|
||||
emit('update:modelValue', [])
|
||||
}
|
||||
|
||||
function formatDateRange(taskClass: TaskClassListItem) {
|
||||
const startDate = formatDateLabel(taskClass.start_date)
|
||||
const endDate = formatDateLabel(taskClass.end_date)
|
||||
if (!startDate || !endDate) {
|
||||
return '时间范围待补充'
|
||||
}
|
||||
return `${startDate} - ${endDate}`
|
||||
}
|
||||
|
||||
function formatDateLabel(value: string) {
|
||||
const parsedDate = new Date(value)
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
return ''
|
||||
}
|
||||
const month = `${parsedDate.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${parsedDate.getDate()}`.padStart(2, '0')
|
||||
return `${month}.${day}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="assistant-planning">
|
||||
<el-popover
|
||||
v-model:visible="popoverVisible"
|
||||
placement="top-start"
|
||||
trigger="click"
|
||||
:width="360"
|
||||
:teleported="true"
|
||||
popper-class="assistant-planning-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="assistant-planning__trigger"
|
||||
:class="{ 'assistant-planning__trigger--active': modelValue.length > 0 }"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span class="assistant-planning__trigger-icon" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.25L12.25 4.375L7 7.5L1.75 4.375L7 1.25Z" fill="currentColor" />
|
||||
<path d="M1.75 6.5625L7 9.6875L12.25 6.5625" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M1.75 8.75L7 11.875L12.25 8.75" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="assistant-planning__trigger-text">{{ triggerLabel }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div class="assistant-planning__panel">
|
||||
<div class="assistant-planning__panel-header">
|
||||
<div>
|
||||
<strong>选择任务类</strong>
|
||||
<p>本次发送将把所选任务类作为智能编排上下文带给后端。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="taskClassLoading" class="assistant-planning__loading">
|
||||
<div v-for="index in 4" :key="index" class="assistant-planning__loading-item" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="taskClasses.length" class="assistant-planning__list">
|
||||
<button
|
||||
v-for="taskClass in taskClasses"
|
||||
:key="taskClass.id"
|
||||
type="button"
|
||||
class="assistant-planning__item"
|
||||
:class="{ 'assistant-planning__item--selected': draftSelectedIds.includes(taskClass.id) }"
|
||||
@click="toggleDraftSelection(taskClass.id)"
|
||||
>
|
||||
<span
|
||||
class="assistant-planning__item-check"
|
||||
:class="{ 'assistant-planning__item-check--selected': draftSelectedIds.includes(taskClass.id) }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="assistant-planning__item-body">
|
||||
<strong>{{ taskClass.name }}</strong>
|
||||
<small>{{ formatDateRange(taskClass) }}</small>
|
||||
</span>
|
||||
<span class="assistant-planning__item-slots">{{ taskClass.total_slots }} 节</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="assistant-planning__empty">
|
||||
当前还没有可用于智能编排的任务类。
|
||||
</div>
|
||||
|
||||
<div class="assistant-planning__panel-actions">
|
||||
<button type="button" class="assistant-planning__panel-button assistant-planning__panel-button--ghost" @click="clearSelectionFromPanel">
|
||||
清空
|
||||
</button>
|
||||
<button type="button" class="assistant-planning__panel-button assistant-planning__panel-button--primary" @click="applySelection">
|
||||
应用选择
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
|
||||
<div v-if="selectedTaskClasses.length" class="assistant-planning__summary">
|
||||
<span class="assistant-planning__summary-label">已选任务类</span>
|
||||
<div class="assistant-planning__tags">
|
||||
<button
|
||||
v-for="taskClass in selectedTaskClasses"
|
||||
:key="taskClass.id"
|
||||
type="button"
|
||||
class="assistant-planning__tag"
|
||||
:disabled="disabled"
|
||||
@click="removeSelectedTaskClass(taskClass.id)"
|
||||
>
|
||||
<span>{{ taskClass.name }}</span>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<button type="button" class="assistant-planning__clear" :disabled="disabled" @click="clearSelectedTaskClasses">
|
||||
清空全部
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.assistant-planning {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 10px 12px 0;
|
||||
}
|
||||
|
||||
.assistant-planning__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 138px;
|
||||
min-width: 138px;
|
||||
max-width: 138px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(15, 23, 42, 0.1);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: #1f2430;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.assistant-planning__trigger:hover:not(:disabled) {
|
||||
border-color: rgba(57, 86, 178, 0.26);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.assistant-planning__trigger:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.assistant-planning__trigger--active {
|
||||
border-color: rgba(57, 86, 178, 0.24);
|
||||
background: #eef3ff;
|
||||
color: #3357c2;
|
||||
}
|
||||
|
||||
.assistant-planning__trigger-icon {
|
||||
display: inline-flex;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.assistant-planning__trigger-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assistant-planning__summary {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-planning__summary-label {
|
||||
color: #5b6677;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-planning__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-planning__tag,
|
||||
.assistant-planning__clear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(57, 86, 178, 0.16);
|
||||
background: #f6f8ff;
|
||||
color: #3559c3;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-planning__clear {
|
||||
border-style: dashed;
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.assistant-planning__panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.assistant-planning__panel-header strong {
|
||||
display: block;
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.assistant-planning__panel-header p {
|
||||
margin: 6px 0 0;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.assistant-planning__loading,
|
||||
.assistant-planning__list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.assistant-planning__loading-item {
|
||||
height: 62px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(90deg, rgba(241, 245, 249, 0.9), rgba(226, 232, 240, 0.72), rgba(241, 245, 249, 0.9));
|
||||
}
|
||||
|
||||
.assistant-planning__item {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
text-align: left;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.assistant-planning__item:hover {
|
||||
border-color: rgba(57, 86, 178, 0.24);
|
||||
background: #fafcff;
|
||||
}
|
||||
|
||||
.assistant-planning__item--selected {
|
||||
border-color: rgba(57, 86, 178, 0.28);
|
||||
background: #f5f8ff;
|
||||
}
|
||||
|
||||
.assistant-planning__item-check {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
border: 1.5px solid rgba(148, 163, 184, 0.8);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.assistant-planning__item-check--selected {
|
||||
border-color: #3357c2;
|
||||
background: radial-gradient(circle at center, #3357c2 0 45%, transparent 46%);
|
||||
}
|
||||
|
||||
.assistant-planning__item-body {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.assistant-planning__item-body strong {
|
||||
color: #1f2430;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.assistant-planning__item-body small {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.assistant-planning__item-slots {
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-planning__empty {
|
||||
padding: 18px 16px;
|
||||
border-radius: 16px;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.assistant-planning__panel-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.assistant-planning__panel-button {
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.1);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-planning__panel-button--ghost {
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.assistant-planning__panel-button--primary {
|
||||
border-color: transparent;
|
||||
background: #3357c2;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:global(.assistant-planning-popover) {
|
||||
padding: 14px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(203, 213, 225, 0.78);
|
||||
box-shadow: 0 18px 44px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user