优化: MCP 收尾(update原子CAS治TOCTOU/advance_task数据变更映射/list_trash分页) + miniapp渲染收尾(mention chip可视化/代码块语言标签) + 销账

This commit is contained in:
lxy
2026-08-09 21:35:02 +08:00
parent 37040616bd
commit e099eff4cb
8 changed files with 479 additions and 36 deletions
+72 -2
View File
@@ -350,6 +350,50 @@ function shouldRenderMsg(m: ChatMessage): boolean {
return true
}
// ────────────────────────────────────────────────────────────────
// MR-P2-2 用户消息 mention chip:解析 `[项目:名]`/`[任务:名]`/`[灵感:名]` 标记分段渲染。
//
// 对齐桌面 MessageItem.segmentUserContent:命中已知实体的标记渲染为 chip(浅底圆角徽标),
// 未命中保持字面(防误伤正常方括号文本)。
// 标记格式与 @ 选中插入一致(onEntitySelect):冒号后是实体展示名 —— project 用 name,
// task/idea 用 title。miniapp 消息内容仅存原始文本(AiUserMessage 只回 message,无 span 元数据),
// 故「已解析成功」以实体名是否命中当前 entities 列表判定(与插入时同字段)。
// ────────────────────────────────────────────────────────────────
type UserSegment =
| { type: 'text'; text: string }
| { type: 'chip'; kind: 'project' | 'task' | 'idea'; label: string }
/** 实体名是否命中已知实体(项目 name / 任务·灵感 title,与 @ 选中插入同字段) */
function isKnownEntity(kind: 'project' | 'task' | 'idea', name: string): boolean {
const e = entities.value
if (kind === 'project') return e.projects.some((p) => p.name === name)
if (kind === 'task') return e.tasks.some((t) => t.title === name)
return e.ideas.some((i) => i.title === name)
}
/** 用户消息内容 → 分段(文本段 + chip 段),纯文本返回单文本段(零回归) */
function segmentUserContent(content: string): UserSegment[] {
const segs: UserSegment[] = []
const re = /\[(项目|任务|灵感):([^\]]+)\]/g
let last = 0
let m: RegExpExecArray | null
while ((m = re.exec(content)) !== null) {
if (m.index > last) segs.push({ type: 'text', text: content.slice(last, m.index) })
const kind = m[1] === '项目' ? 'project' : m[1] === '任务' ? 'task' : 'idea'
const name = m[2].trim()
// 仅命中已知实体渲染 chip;未命中(正常方括号文本/无效引用)保持字面
segs.push(isKnownEntity(kind, name) ? { type: 'chip', kind, label: name } : { type: 'text', text: m[0] })
last = m.index + m[0].length
}
if (last < content.length) segs.push({ type: 'text', text: content.slice(last) })
return segs.length ? segs : [{ type: 'text', text: content }]
}
/** 用户消息内容是否含 mention 标记段(决定是否走分段渲染;纯文本走原单一 text 零回归) */
function hasMentionMarker(content: string): boolean {
return /\[(项目|任务|灵感):[^\]]+\]/.test(content)
}
/**
* 输入变化时检测联想触发条件。
*
@@ -771,8 +815,17 @@ function tokenInOf(t: TokenUsage): number {
<view v-if="m.role === 'user'">
<!-- 图片预览(消息带图时) -->
<image v-for="(img, i) in m.images || []" :key="i" :src="img" mode="widthFix" class="msg-user-img" />
<!-- 文本(有文本才显,图片+文本共存;纯图片则只有图,不空白) -->
<text v-if="m.content" user-select>{{ m.content }}</text>
<!-- MR-P2-2 文本:含 mention 标记时分段渲染 chip(命中已知实体的 `[项目:名]`/`[任务:名]`/`[灵感:名]`
渲染为 chip,未命中保持字面);纯文本走原单一 `<text>` 渲染零回归 -->
<template v-if="m.content && hasMentionMarker(m.content)">
<text user-select>
<template v-for="(seg, idx) in segmentUserContent(m.content)" :key="m.id + '-seg-' + idx">
<text v-if="seg.type === 'text'">{{ seg.text }}</text>
<text v-else class="user-chip" :class="'user-chip--' + seg.kind">{{ seg.label }}</text>
</template>
</text>
</template>
<text v-else-if="m.content" user-select>{{ m.content }}</text>
</view>
<template v-else>
<!-- P0-3:错误气泡纯文本渲染(不走 markdown 二次解析,错误串含 `**`/`#` 时不会被解析成格式) -->
@@ -1054,6 +1107,23 @@ function tokenInOf(t: TokenUsage): number {
.msg.user text {
color: #ffffff;
}
/* MR-P2-2 用户消息 mention chip(对齐桌面 ai-msg-chip:浅底圆角徽标)。
用户气泡底 = #4a9eff,chip 用半透明白底 + 深蓝字保证对比度(对齐桌面 rgba(255,255,255,.32));
按 kind 区分颜色变体(project/task/idea),与 @ mention 浮层分组色呼应。
mp-weixin <text> 组件天然 inline,嵌套于外层 <text> 内联排布;背景/圆角/padding 真机可渲染。
选择器用 .msg.user text.user-chip(≥ .msg.user text 的 (0,2,1))盖过白字,保证深蓝字生效。 */
.msg.user text.user-chip {
padding: 1px 6px;
margin: 0 1px;
border-radius: 4px;
font-size: 13px;
line-height: 1.4;
color: #14407a;
background-color: rgba(255, 255, 255, 0.32);
}
.msg.user text.user-chip--project { background-color: rgba(255, 255, 255, 0.38); }
.msg.user text.user-chip--task { background-color: rgba(255, 255, 255, 0.3); }
.msg.user text.user-chip--idea { background-color: rgba(255, 255, 255, 0.24); }
/* 用户消息图片预览(2026-08-06):120px 宽 + 圆角,与气泡风格一致;mode=widthFix 高自适应 */
.msg-user-img {
display: block;