Files
smartmate/frontend/src/api/agent.ts
Losita a1b2ffedb8 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;历史列表首屏不足时自动继续分页直到形成滚动区
仓库:无
2026-04-16 18:29:17 +08:00

183 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import http from '@/api/http'
import type { ApiResponse } from '@/types/api'
import type { ConversationContextStats, ConversationListResponse, ConversationMeta } from '@/types/dashboard'
import { extractErrorMessage } from '@/utils/http'
const conversationHistoryPath = '/agent/conversation-history'
export interface ConversationHistoryMessage {
id?: string | number
role: 'user' | 'assistant' | 'system'
content: string
created_at?: string | null
reasoning_content?: string | null
reasoning_duration_seconds?: number | null
retry_group_id?: string | null
retry_index?: number | null
retry_total?: number | null
}
export interface ConversationListQuery {
page?: number
pageSize?: number
status?: 'active' | 'archived'
}
function normalizeNonNegativeInteger(value: unknown) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return null
}
return Math.max(0, Math.round(value))
}
function normalizeConversationContextStats(raw: unknown): ConversationContextStats | null {
// 1. 后端这里直接透传数据库中的 JSON前端需要同时兜住 object / null / 空字符串三种返回。
// 2. 若后端灰度期间字段缺失,则尽量使用四段消息之和回填 total避免展示层继续散落兼容逻辑。
// 3. budget 缺失时说明统计不完整,此时返回 null让界面统一走“暂无统计”态更安全。
if (raw == null || raw === '') {
return null
}
let candidate: unknown = raw
if (typeof candidate === 'string') {
const trimmed = candidate.trim()
if (!trimmed) {
return null
}
try {
candidate = JSON.parse(trimmed) as unknown
} catch {
return null
}
}
if (!candidate || typeof candidate !== 'object') {
return null
}
const stats = candidate as Record<string, unknown>
const msg0 = normalizeNonNegativeInteger(stats.msg0) ?? 0
const msg1 = normalizeNonNegativeInteger(stats.msg1) ?? 0
const msg2 = normalizeNonNegativeInteger(stats.msg2) ?? 0
const msg3 = normalizeNonNegativeInteger(stats.msg3) ?? 0
const fallbackTotal = msg0 + msg1 + msg2 + msg3
const total = normalizeNonNegativeInteger(stats.total) ?? fallbackTotal
const budget = normalizeNonNegativeInteger(stats.budget)
if (budget == null || budget <= 0) {
return null
}
return {
msg0,
msg1,
msg2,
msg3,
total: Math.max(total, fallbackTotal),
budget,
}
}
function normalizeConversationHistoryMessage(raw: unknown): ConversationHistoryMessage | null {
if (!raw || typeof raw !== 'object') {
return null
}
const candidate = raw as Record<string, unknown>
const role = candidate.role
const content = candidate.content
if ((role !== 'user' && role !== 'assistant' && role !== 'system') || typeof content !== 'string') {
return null
}
// 1. 按 openapi 优先读取 reasoning_content兼容后端历史接口新增的思考存储字段。
// 2. 若后端灰度期间仍返回 legacy reasoning 字段,这里也做一次前端兜底兼容。
// 3. 统一归一化成 string/null避免页面层反复做类型分支判断。
const normalizedReasoning =
typeof candidate.reasoning_content === 'string'
? candidate.reasoning_content
: typeof candidate.reasoning === 'string'
? candidate.reasoning
: null
return {
id: typeof candidate.id === 'string' || typeof candidate.id === 'number' ? candidate.id : undefined,
role,
content,
created_at: typeof candidate.created_at === 'string' ? candidate.created_at : null,
reasoning_content: normalizedReasoning,
reasoning_duration_seconds:
typeof candidate.reasoning_duration_seconds === 'number' ? candidate.reasoning_duration_seconds : null,
retry_group_id: typeof candidate.retry_group_id === 'string' ? candidate.retry_group_id : null,
retry_index: typeof candidate.retry_index === 'number' ? candidate.retry_index : null,
retry_total: typeof candidate.retry_total === 'number' ? candidate.retry_total : null,
}
}
// getConversationList 负责按 openapi 约定读取会话列表分页。
// 职责边界:
// 1. 负责把前端分页参数映射为后端要求的 page/page_size。
// 2. 不负责前端滚动懒加载时的合并、去重和选中逻辑。
// 3. 接口失败时统一抛出中文错误,便于页面层直接提示。
export async function getConversationList(options: ConversationListQuery = {}) {
const { page = 1, pageSize = 20, status = 'active' } = options
try {
const response = await http.get<ApiResponse<ConversationListResponse>>('/agent/conversation-list', {
params: {
page,
page_size: pageSize,
limit: pageSize,
status,
},
})
return response.data.data
} catch (error) {
throw new Error(extractErrorMessage(error, '会话列表加载失败,请稍后重试'))
}
}
export async function getConversationMeta(conversationId: string) {
try {
const response = await http.get<ApiResponse<ConversationMeta>>('/agent/conversation-meta', {
params: {
conversation_id: conversationId,
},
})
return response.data.data
} catch (error) {
throw new Error(extractErrorMessage(error, '会话信息加载失败,请稍后重试'))
}
}
export async function getConversationHistory(conversationId: string) {
try {
const response = await http.get<ApiResponse<ConversationHistoryMessage[]>>(conversationHistoryPath, {
params: {
conversation_id: conversationId,
},
})
return (response.data.data ?? [])
.map(normalizeConversationHistoryMessage)
.filter((message): message is ConversationHistoryMessage => Boolean(message))
} catch (error) {
throw new Error(extractErrorMessage(error, '会话消息加载失败,请稍后重试'))
}
}
export async function getContextStats(conversationId: string) {
try {
const response = await http.get<ApiResponse<unknown>>('/agent/context-stats', {
params: {
conversation_id: conversationId,
},
})
return normalizeConversationContextStats(response.data.data)
} catch (error) {
throw new Error(extractErrorMessage(error, '上下文窗口统计加载失败,请稍后重试'))
}
}