mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-05 21:05:39 +08:00
fix: 问问微信恢复引用编号,日报修复成员头像与 hero 溢出
- 问问微信:Host 分配稳定 citationId 并注入模型上下文,正文 [E#] 与证据按钮同号 - 问问微信:回答返回前校验引用,幻觉编号移除并提示,不再出现不可信的引用按钮 - Agent Hub:收紧「最近会话」快捷路由,内容查询交回 Query Agent
This commit is contained in:
@@ -94,6 +94,13 @@ export function AISearchWorkspace({
|
||||
const [queryAgentEnabled, setQueryAgentEnabled] = useState(false)
|
||||
/** Query Agent 本次回答的真实统计(读取条数 / 证据条数 / 模型调用 / 耗时)。 */
|
||||
const [askStats, setAskStats] = useState<AskWechatStats | null>(null)
|
||||
/**
|
||||
* Query Agent 回答里被 Host 移除的非法 `[E#]`。
|
||||
*
|
||||
* 有值 = 模型引用了不存在的编号;已从正文剥离,这里只做告知,
|
||||
* 不让用户以为"每条断言都有出处"。
|
||||
*/
|
||||
const [askInvalidCitationIds, setAskInvalidCitationIds] = useState<string[]>([])
|
||||
const {
|
||||
evidence,
|
||||
setEvidence,
|
||||
@@ -194,6 +201,7 @@ export function AISearchWorkspace({
|
||||
setCachedAt(reset.cachedAt)
|
||||
setSearchTrace(reset.searchTrace)
|
||||
setAskStats(null)
|
||||
setAskInvalidCitationIds([])
|
||||
resetSearchRun()
|
||||
setSearchDetailsOpen(reset.searchDetailsOpen)
|
||||
}
|
||||
@@ -443,12 +451,17 @@ export function AISearchWorkspace({
|
||||
}
|
||||
if (askResult.status === 'answered') {
|
||||
const mappedEvidence = mapAskWechatEvidence(askResult.evidence)
|
||||
addDebugEntry('查询 Agent 完成', { ...askResult.diagnostics })
|
||||
addDebugEntry('查询 Agent 完成', {
|
||||
...askResult.diagnostics,
|
||||
invalidCitationIds: askResult.invalidCitationIds
|
||||
})
|
||||
setResultQuery(normalizedQuery)
|
||||
setAnswer(askResult.answer)
|
||||
// 真实证据直接来自 Runtime 收集的 Tool 结果,不从回答文本反解析。
|
||||
// 证据卡的编号是 Host 分配的 citationId,与正文 [E#] 同号。
|
||||
setEvidenceResult(mappedEvidence, mappedEvidence)
|
||||
setAskStats(askResult.stats)
|
||||
setAskInvalidCitationIds(askResult.invalidCitationIds || [])
|
||||
setMessageCount(0)
|
||||
rememberQuery(normalizedQuery)
|
||||
persistSearchResult({
|
||||
@@ -468,6 +481,7 @@ export function AISearchWorkspace({
|
||||
? askResult.message
|
||||
: '本次查询没有完成,请稍后再试。'
|
||||
setAskStats(null)
|
||||
setAskInvalidCitationIds([])
|
||||
addDebugEntry('查询失败', { ...askResult.diagnostics })
|
||||
setAnalysisError(failureMessage)
|
||||
setStage('insufficient')
|
||||
@@ -883,6 +897,13 @@ export function AISearchWorkspace({
|
||||
{askWechatToolLabels(askStats.tools).map((label) => (
|
||||
<span key={label}>能力:{label}</span>
|
||||
))}
|
||||
{/* 与 Legacy 同一措辞:Host 侧已把无法对应证据的引用从正文移除。
|
||||
这里只做告知,不把它渲染成可点击的引用。 */}
|
||||
{askInvalidCitationIds.length > 0 && (
|
||||
<span data-testid="query-invalid-citations">
|
||||
已移除无效引用:{askInvalidCitationIds.join('、')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 耗时拆解:把总耗时还原成"AI 花了多少 / 本地查询花了多少"。
|
||||
普通 UI 只出现这三个用户能理解的名字,不出现 firstModelMs / toolTotalMs
|
||||
@@ -904,7 +925,8 @@ export function AISearchWorkspace({
|
||||
{askStats.timings.modelDurationsMs?.map((v) => Math.round(v)).join(', ') ||
|
||||
'-'}
|
||||
] ms · 本地 [
|
||||
{askStats.timings.toolDurationsMs?.map((v) => Math.round(v)).join(', ') || '-'}
|
||||
{askStats.timings.toolDurationsMs?.map((v) => Math.round(v)).join(', ') ||
|
||||
'-'}
|
||||
] ms
|
||||
</span>
|
||||
)}
|
||||
@@ -985,9 +1007,16 @@ export function AISearchWorkspace({
|
||||
{evidence.length > 0 && (
|
||||
<div className="ai-search-answer-evidence" aria-label="AI 引用证据">
|
||||
<span>引用:</span>
|
||||
{evidence.map((_, index) => (
|
||||
<button key={index} type="button" onClick={() => focusEvidence(index)}>
|
||||
E{index + 1}
|
||||
{evidence.map((item, index) => (
|
||||
// 标签取 Host 分配的 evidenceId(= citationId),与正文 inline citation 同号;
|
||||
// 只有 Legacy 缓存记录可能缺 evidenceId,才退回下标编号。
|
||||
<button
|
||||
key={item.evidenceId || index}
|
||||
type="button"
|
||||
data-evidence-id={item.evidenceId || `E${index + 1}`}
|
||||
onClick={() => focusEvidence(index)}
|
||||
>
|
||||
{item.evidenceId || `E${index + 1}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1283,7 +1312,8 @@ export function AISearchWorkspace({
|
||||
)}
|
||||
{knowledgeStatus.pass && knowledgeStatus.pass.skippedConversations > 0 && (
|
||||
<p className="ai-search-knowledge-pass-line">
|
||||
已跳过 {knowledgeStatus.pass.skippedConversations.toLocaleString()} 个没有新消息的会话
|
||||
已跳过 {knowledgeStatus.pass.skippedConversations.toLocaleString()}{' '}
|
||||
个没有新消息的会话
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { AskWechatEvidenceItem, AskWechatStats } from '../../../../shared/query-agent'
|
||||
import {
|
||||
decodeMessageRef,
|
||||
type CanonicalMessageIdentity
|
||||
} from '../../../../shared/local-query-api'
|
||||
import { decodeMessageRef, type CanonicalMessageIdentity } from '../../../../shared/local-query-api'
|
||||
import type { Contact } from '../../../../shared/types'
|
||||
import { formatEvidenceTimestamp } from './searchFormatters'
|
||||
import type { EvidenceItem } from './searchTypes'
|
||||
@@ -37,20 +34,36 @@ const SCOPE_LABELS: Record<string, string> = {
|
||||
* (展示契约里刻意不含 md5 字段);`messageRef` 缺失或解析失败时退化成合成 key,
|
||||
* 并且调用方必须按"无法定位"处理。
|
||||
*/
|
||||
function evidenceContact(item: AskWechatEvidenceItem, anchor: CanonicalMessageIdentity | null): Contact {
|
||||
function evidenceContact(
|
||||
item: AskWechatEvidenceItem,
|
||||
anchor: CanonicalMessageIdentity | null
|
||||
): Contact {
|
||||
const name = item.conversationName?.trim() || '未命名会话'
|
||||
const type = item.conversationType === 'group' ? 'group' : 'user'
|
||||
if (anchor) {
|
||||
return { md5: anchor.conversationId, m_nsUsrName: anchor.conversationId, m_nsNickName: name, type }
|
||||
return {
|
||||
md5: anchor.conversationId,
|
||||
m_nsUsrName: anchor.conversationId,
|
||||
m_nsNickName: name,
|
||||
type
|
||||
}
|
||||
}
|
||||
return {
|
||||
md5: `query-agent:${item.conversationName || 'unknown'}`,
|
||||
m_nsUsrName: '',
|
||||
m_nsNickName: name,
|
||||
type
|
||||
}
|
||||
return { md5: `query-agent:${item.conversationName || 'unknown'}`, m_nsUsrName: '', m_nsNickName: name, type }
|
||||
}
|
||||
|
||||
export function mapAskWechatEvidence(items: AskWechatEvidenceItem[]): EvidenceItem[] {
|
||||
return items.map((item, index) => {
|
||||
return items.map((item) => {
|
||||
const anchor = decodeMessageRef(item.messageRef)
|
||||
return {
|
||||
evidenceId: `E${index + 1}`,
|
||||
// 编号直接消费 Host 分配的 citationId —— 不再用数组下标自行合成。
|
||||
// 正文 inline citation、底部引用按钮、证据卡标题因此是同一条 evidence 的同一个编号;
|
||||
// 下标一旦被过滤 / 分页 / 重排就会漂移,而且模型无从知道它(引用必然失效)。
|
||||
evidenceId: item.citationId,
|
||||
sourceKind: item.messageType as EvidenceItem['sourceKind'],
|
||||
// 「靠图片里的文字命中」是来源语义,必须原样带到 UI;
|
||||
// 但 authoritative source 仍然是原始图片消息(messageRef 已指向它)。
|
||||
|
||||
@@ -772,7 +772,15 @@ export const buildGroupReportFacts = async (
|
||||
footerNote: '基于已读取聊天记录生成;图片、表情等未解析内容默认只按类型与上下文参与日报。',
|
||||
heroParticipants: topSpeakers.slice(0, 4).map((speaker) => speaker.name),
|
||||
avatars,
|
||||
reportMode
|
||||
reportMode,
|
||||
/**
|
||||
* 会话标识:导出层 `enrichAvatarsFromGroup` 靠它反查群成员快照补头像。
|
||||
*
|
||||
* 必须用 `m_nsUsrName`(群 roomid,形如 `xxx@chatroom`)而不是群名 —— 群名是展示名,
|
||||
* 可能重名或带表情符号;`resolveMd5` 对 roomid / md5 / wxid 都是精确匹配。
|
||||
* 此前这个字段从未被赋值,导致那条 enrich 分支实际是死代码,头像只能靠消息自带的 img。
|
||||
*/
|
||||
...(contact?.m_nsUsrName ? { talker: contact.m_nsUsrName } : {})
|
||||
}
|
||||
|
||||
const { media, voiceLeaderboard, warnings, imageInsightSummary } = await buildMediaSection(
|
||||
|
||||
Reference in New Issue
Block a user