优化: MCP 收尾(update原子CAS治TOCTOU/advance_task数据变更映射/list_trash分页) + miniapp渲染收尾(mention chip可视化/代码块语言标签) + 销账
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -130,23 +130,34 @@ function inlineHljs(html: string): string {
|
||||
})
|
||||
}
|
||||
|
||||
/** 代码块语言徽标(rich-text 内联 style;块代码顶部展示 fence lang,无 lang 不调用)。 */
|
||||
function codeLangBadge(lang: string): string {
|
||||
// 块级 div(pre 深底上浅灰标签 + 圆角,pre 自身已有内边距),放 <code> 前占据顶部一行
|
||||
return `<div style="font-size:10px;color:#9e9e9e;background-color:#2e2e2e;padding:1px 8px;border-radius:4px;margin-bottom:6px">${lang}</div>`
|
||||
}
|
||||
|
||||
/**
|
||||
* 块代码语法高亮:仅处理已知语言,未知/无语言兜底原样(确定性强,不做自动猜测)。
|
||||
* marked 已对代码内容做 HTML 转义,先反转还原再交给 hljs(hljs 输出自带转义)。
|
||||
* 同时补语言徽标:围栏有 lang 即显示(用原始标识如 ts,更贴近用户书写),无 lang 不显示。
|
||||
*/
|
||||
function highlightCodeBlocks(html: string): string {
|
||||
return html.replace(/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/gi, (m, attrs: string, inner: string) => {
|
||||
const langMatch = /class="language-([^"]+)"/i.exec(attrs)
|
||||
const langRaw = langMatch ? langMatch[1] : ''
|
||||
const lang = LANG_ALIASES[langRaw] ?? langRaw
|
||||
if (!lang || !hljs.getLanguage(lang)) return m
|
||||
const badge = langRaw ? codeLangBadge(langRaw) : ''
|
||||
if (!lang || !hljs.getLanguage(lang)) {
|
||||
// 未注册语言:不高亮但保留徽标(fence 有 lang 时),避免 ` ```未知lang ` 丢语言标签
|
||||
return badge ? `<pre><code${attrs}>${badge}${inner}</code></pre>` : m
|
||||
}
|
||||
let value: string
|
||||
try {
|
||||
value = hljs.highlight(unescapeCode(inner), { language: lang, ignoreIllegals: true }).value
|
||||
} catch {
|
||||
return m
|
||||
return badge ? `<pre><code${attrs}>${badge}${inner}</code></pre>` : m
|
||||
}
|
||||
return `<pre><code${attrs}>${inlineHljs(value)}</code></pre>`
|
||||
return `<pre><code${attrs}>${badge}${inlineHljs(value)}</code></pre>`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user