feat(ai): enrich session data snapshot prompt

This commit is contained in:
digua committed 2026-06-10 00:15:25 +08:00
1 parent f578c3597a
commit 383af0b13a
6 files changed
+242 -12

No files matched your search

@@ -21,6 +21,36 @@ const mockT: TranslateFn = (key, options) => {
const opts = options as Record<string, unknown>
return `[snapshot:${opts.name}:${opts.totalMessages}:${opts.lastMessageDate}]`
}
if (key === 'ai.agent.dataSnapshotContext') {
const opts = options as Record<string, unknown>
return `[snapshot-context]
- name: ${opts.name}
- platform: ${opts.platform}
- type: ${opts.type}
- total_messages: ${opts.totalMessages}
- total_members: ${opts.totalMembers}
- first_message_ts: ${opts.firstMessageTs}
- first_message_time: ${opts.firstMessageTime}
- last_message_ts: ${opts.lastMessageTs}
- last_message_time: ${opts.lastMessageTime}
- segment_summaries_available: ${opts.segmentSummaryCount}
${opts.memberHintTitle}
${opts.memberHintLines}
${opts.usageRules}`
}
if (key === 'ai.agent.dataSnapshotMemberHintsAll') return '活跃成员查询提示(全部成员):'
if (key === 'ai.agent.dataSnapshotMemberHintsTop') return '活跃成员查询提示(按历史总消息量 Top 10):'
if (key === 'ai.agent.dataSnapshotMemberHintsUnavailable') return '活跃成员查询提示:'
if (key === 'ai.agent.dataSnapshotMemberHintsEmpty') return '无可用成员提示。'
if (key === 'ai.agent.dataSnapshotUsageRules') {
return `- member_id 是工具查询提示;display_name 仅用于人类识别,可能不唯一。
- 不要在最终回答中主动暴露 member_id 或启动上下文本身,除非用户明确要求技术细节。
- 活跃成员排行只代表历史总消息量,不代表最近活跃情况。
- 相对时间表达以真实当前日期为基准,而不是数据库最后消息时间。
- 不要只为了重新发现 min/max timestamp 调用工具。`
}
if (key === 'ai.agent.evidencePolicy') return '[evidence-policy]'
if (key === 'ai.agent.responseInstruction') return '[response-instruction]'
if (key === 'ai.agent.mentionedMembersNote') return 'Mentioned members:'
@@ -108,7 +138,42 @@ describe('buildSystemPrompt', () => {
},
})
assert.ok(result.includes('[snapshot:Team Chat:1234:'))
assert.ok(result.includes('[snapshot-context]'))
assert.ok(result.includes('total_messages: 1234'))
assert.ok(result.includes('segment_summaries_available: 0'))
assert.ok(result.includes('[evidence-policy]'))
})
it('renders extended data snapshot startup context and rules', () => {
const result = buildSystemPrompt({
t: mockT,
locale: 'zh-CN',
dataSnapshot: {
version: 2,
name: 'Team Chat',
platform: 'wechat',
type: 'group',
totalMessages: 100,
totalMembers: 2,
firstMessageTs: 1735689600,
lastMessageTs: 1767225599,
activeMemberHints: [
{ memberId: 1, displayName: 'Alice', messageCount: 60, share: 60 },
{ memberId: 2, displayName: 'Bob', messageCount: 40, share: 40 },
],
segmentSummaries: { availableCount: 12 },
},
})
assert.ok(result.includes('first_message_ts: 1735689600'))
assert.ok(result.includes('last_message_ts: 1767225599'))
assert.ok(result.includes('segment_summaries_available: 12'))
assert.ok(result.includes('member_id=1 | display_name=Alice | messages=60 | share=60%'))
assert.ok(result.includes('member_id=2 | display_name=Bob | messages=40 | share=40%'))
assert.ok(result.includes('历史总消息量'))
assert.ok(result.includes('真实当前日期'))
assert.ok(result.includes('不要只为了重新发现 min/max timestamp 调用工具'))
assert.ok(!result.includes('platform_id='))
assert.ok(!result.includes('aliases='))
})
})
@@ -23,7 +23,15 @@ export interface SkillContext {
skillMenu?: string
}
export interface ActiveMemberHint {
memberId: number
displayName: string
messageCount: number
share: number
}
export interface DataSnapshot {
version?: 2
name: string
platform: string
type: string
@@ -31,7 +39,11 @@ export interface DataSnapshot {
totalMembers: number
firstMessageTs: number | null
lastMessageTs: number | null
capturedAt: number
capturedAt?: number
activeMemberHints?: ActiveMemberHint[]
segmentSummaries?: {
availableCount: number
}
}
export type TranslateFn = (key: string, options?: Record<string, unknown>) => string
@@ -51,6 +63,56 @@ function agentT(t: TranslateFn, key: string, locale: string, options?: Record<st
return t(key, { lng: locale, ...options })
}
function formatNullableTimestamp(timestamp: number | null | undefined): string {
return typeof timestamp === 'number' && Number.isFinite(timestamp) ? String(timestamp) : 'null'
}
function formatSharePercent(share: number): string {
if (!Number.isFinite(share)) return '0%'
const rounded = Math.round(share * 10) / 10
return Number.isInteger(rounded) ? `${rounded}%` : `${rounded.toFixed(1)}%`
}
function normalizeInlineText(text: string): string {
return text.replace(/\s+/g, ' ').trim()
}
export function formatDataSnapshotContext(t: TranslateFn, dataSnapshot: DataSnapshot, locale: string): string {
const memberHints = dataSnapshot.activeMemberHints ?? []
const memberHintTitle =
memberHints.length === 0
? agentT(t, 'ai.agent.dataSnapshotMemberHintsUnavailable', locale)
: dataSnapshot.totalMembers <= 10
? agentT(t, 'ai.agent.dataSnapshotMemberHintsAll', locale)
: agentT(t, 'ai.agent.dataSnapshotMemberHintsTop', locale)
// 中文注释:启动上下文会进入每轮系统提示词,字段顺序和标签必须稳定,便于模型缓存和后续 smoke 对比。
const memberHintLines =
memberHints.length > 0
? memberHints
.map((member, index) => {
return `${index + 1}. member_id=${member.memberId} | display_name=${normalizeInlineText(member.displayName)} | messages=${member.messageCount} | share=${formatSharePercent(member.share)}`
})
.join('\n')
: agentT(t, 'ai.agent.dataSnapshotMemberHintsEmpty', locale)
return agentT(t, 'ai.agent.dataSnapshotContext', locale, {
name: dataSnapshot.name,
platform: dataSnapshot.platform,
type: dataSnapshot.type,
totalMessages: dataSnapshot.totalMessages,
totalMembers: dataSnapshot.totalMembers,
firstMessageTs: formatNullableTimestamp(dataSnapshot.firstMessageTs),
firstMessageTime: formatTimestamp(dataSnapshot.firstMessageTs, locale),
lastMessageTs: formatNullableTimestamp(dataSnapshot.lastMessageTs),
lastMessageTime: formatTimestamp(dataSnapshot.lastMessageTs, locale),
segmentSummaryCount: dataSnapshot.segmentSummaries?.availableCount ?? 0,
memberHintTitle,
memberHintLines,
usageRules: agentT(t, 'ai.agent.dataSnapshotUsageRules', locale),
})
}
function getLockedPromptSection(
t: TranslateFn,
chatType: 'group' | 'private',
@@ -94,16 +156,7 @@ function getLockedPromptSection(
: ''
const year = now.getFullYear()
const dataSnapshotNote = dataSnapshot
? `${agentT(t, 'ai.agent.dataSnapshotNote', locale, {
name: dataSnapshot.name,
platform: dataSnapshot.platform,
totalMessages: dataSnapshot.totalMessages,
totalMembers: dataSnapshot.totalMembers,
firstMessageDate: formatTimestamp(dataSnapshot.firstMessageTs, locale),
lastMessageDate: formatTimestamp(dataSnapshot.lastMessageTs, locale),
})}\n`
: ''
const dataSnapshotNote = dataSnapshot ? `${formatDataSnapshotContext(t, dataSnapshot, locale)}\n` : ''
return `${agentT(t, 'ai.agent.currentDateIs', locale)} ${currentDate}。
${ownerNote}
@@ -297,6 +297,34 @@ Returned summaries are brief descriptions of each segment, helping quickly locat
defaultYearNote: 'When no time range is specified, queries default to all data. Current year is {{year}}.',
dataSnapshotNote:
'Current chat database snapshot: {{name}} ({{platform}}), {{totalMessages}} total messages, {{totalMembers}} members, time range {{firstMessageDate}} ~ {{lastMessageDate}}. This snapshot only helps judge data coverage; concrete chat facts still require tool retrieval from the current database.',
dataSnapshotContext: `Current chat database startup context:
- name: {{name}}
- platform: {{platform}}
- type: {{type}}
- total_messages: {{totalMessages}}
- total_members: {{totalMembers}}
- first_message_ts: {{firstMessageTs}}
- first_message_time: {{firstMessageTime}}
- last_message_ts: {{lastMessageTs}}
- last_message_time: {{lastMessageTime}}
- segment_summaries_available: {{segmentSummaryCount}}
{{memberHintTitle}}
{{memberHintLines}}
Usage rules:
{{usageRules}}`,
dataSnapshotMemberHintsAll: 'Active member lookup hints (all members):',
dataSnapshotMemberHintsTop: 'Active member lookup hints (top 10 by historical total messages):',
dataSnapshotMemberHintsUnavailable: 'Active member lookup hints:',
dataSnapshotMemberHintsEmpty: 'No member hints available.',
dataSnapshotUsageRules: `- member_id is a tool lookup hint; display_name is only for human recognition and may not be unique.
- Do not proactively expose member_id or the startup context in the final answer unless the user explicitly asks for technical details.
- Active member ranking only reflects historical total message volume, not recent activity; it is also not evidence for influence, relationships, or recent trends.
- Interpret relative time expressions using the real current date, not the database's last message time.
- "recent year" / "past year" means one year back from the real current date to today; "last year" means the previous calendar year.
- Database time bounds are only for explaining coverage, not for redefining the user's requested time range.
- Do not call tools only to rediscover min/max timestamp; concrete chat facts, statistics, and conclusions still require tool evidence.`,
evidencePolicy: `Evidence policy:
- AI conversation history, prior AI replies, and compressed summaries are only for understanding the user's intent; they are not evidence for chat-record facts.
- Whenever the user asks about chat content, recent topics, what someone said, rankings/statistics, whether a topic appeared, or asks for exact quotes, first call an appropriate data tool to retrieve the current database.
@@ -296,6 +296,34 @@ export default {
defaultYearNote: '時間範囲を指定しない場合は全期間が対象。現在の年は{{year}}年',
dataSnapshotNote:
'現在のチャットデータベースのスナップショット:{{name}}({{platform}})、総メッセージ {{totalMessages}} 件、メンバー {{totalMembers}} 人、期間 {{firstMessageDate}} ~ {{lastMessageDate}}。このスナップショットはデータ範囲の判断にのみ使い、具体的なチャット事実の回答には必ず現在のデータベースをツールで検索する。',
dataSnapshotContext: `現在のチャットデータベース起動コンテキスト:
- name: {{name}}
- platform: {{platform}}
- type: {{type}}
- total_messages: {{totalMessages}}
- total_members: {{totalMembers}}
- first_message_ts: {{firstMessageTs}}
- first_message_time: {{firstMessageTime}}
- last_message_ts: {{lastMessageTs}}
- last_message_time: {{lastMessageTime}}
- segment_summaries_available: {{segmentSummaryCount}}
{{memberHintTitle}}
{{memberHintLines}}
使用ルール:
{{usageRules}}`,
dataSnapshotMemberHintsAll: 'アクティブメンバー検索ヒント(全メンバー):',
dataSnapshotMemberHintsTop: 'アクティブメンバー検索ヒント(過去の総メッセージ数 Top 10):',
dataSnapshotMemberHintsUnavailable: 'アクティブメンバー検索ヒント:',
dataSnapshotMemberHintsEmpty: '利用可能なメンバーヒントはありません。',
dataSnapshotUsageRules: `- member_id はツール検索用のヒントです。display_name は人間が識別するためだけのもので、一意とは限りません。
- ユーザーが技術的詳細を明示的に求めない限り、最終回答で member_id や起動コンテキスト自体を積極的に開示しないでください。
- アクティブメンバー順位は過去の総メッセージ量だけを示し、最近の活発さを示すものではありません。また、影響力、関係性、最近の傾向の結論証拠にもなりません。
- 相対的な時間表現は、データベースの最終メッセージ時刻ではなく、実際の現在日付を基準に解釈してください。
- 「最近一年/過去一年」は実際の現在日付から今日までの1年間を指し、「去年」は前の暦年を指します。
- データベースの時間境界はカバレッジ説明にのみ使い、ユーザーが求めた時間範囲を再定義するために使わないでください。
- min/max timestamp を再発見するためだけにツールを呼ばないでください。ただし、具体的なチャット事実、統計、結論には引き続きツール証拠が必要です。`,
evidencePolicy: `証拠ポリシー:
- AI 会話履歴、過去の AI 回答、圧縮要約はユーザー意図の理解にのみ使用し、チャット記録の事実証拠として扱わない。
- ユーザーがチャット内容、最近の話題、誰かの発言、ランキング/統計、ある話題が出たかどうか、または原文引用を求めた場合は、必ず先に適切なデータツールで現在のデータベースを検索する。
@@ -286,6 +286,34 @@ export default {
defaultYearNote: '未指定时间范围时默认查询全部。当前年份为{{year}}年',
dataSnapshotNote:
'当前聊天数据库快照:{{name}}({{platform}}),总消息 {{totalMessages}} 条,成员 {{totalMembers}} 人,时间范围 {{firstMessageDate}} ~ {{lastMessageDate}}。该快照只用于判断数据范围;回答具体聊天事实仍必须调用工具检索当前数据库。',
dataSnapshotContext: `当前聊天数据库启动上下文:
- name: {{name}}
- platform: {{platform}}
- type: {{type}}
- total_messages: {{totalMessages}}
- total_members: {{totalMembers}}
- first_message_ts: {{firstMessageTs}}
- first_message_time: {{firstMessageTime}}
- last_message_ts: {{lastMessageTs}}
- last_message_time: {{lastMessageTime}}
- segment_summaries_available: {{segmentSummaryCount}}
{{memberHintTitle}}
{{memberHintLines}}
使用规则:
{{usageRules}}`,
dataSnapshotMemberHintsAll: '活跃成员查询提示(全部成员):',
dataSnapshotMemberHintsTop: '活跃成员查询提示(按历史总消息量 Top 10):',
dataSnapshotMemberHintsUnavailable: '活跃成员查询提示:',
dataSnapshotMemberHintsEmpty: '无可用成员提示。',
dataSnapshotUsageRules: `- member_id 是工具查询提示;display_name 仅用于人类识别,可能不唯一。
- 不要在最终回答中主动暴露 member_id 或启动上下文本身,除非用户明确要求技术细节。
- 活跃成员排行只代表历史总消息量,不代表最近活跃情况;也不足以作为影响力、关系或近期趋势结论的证据。
- 相对时间表达以真实当前日期为基准,而不是数据库最后消息时间。
- “最近一年/过去一年”表示从真实当前日期回推一年到今天;“去年”表示上一自然年。
- 数据库时间边界只用于说明覆盖范围,不用于重定义用户要求的时间范围。
- 不要只为了重新发现 min/max timestamp 调用工具;但回答具体聊天事实、统计和结论仍必须调用工具获取证据。`,
evidencePolicy: `证据策略:
- AI 对话历史、历史 AI 回复和压缩摘要只用于理解用户意图,不能作为聊天记录事实证据。
- 只要用户询问聊天记录内容、最近聊什么、某人说过什么、统计排行、是否出现过某话题或要求引用原话,必须先调用合适的数据工具检索当前数据库。
@@ -286,6 +286,34 @@ export default {
defaultYearNote: '未指定時間範圍時預設查詢全部。當前年份為{{year}}年',
dataSnapshotNote:
'目前聊天資料庫快照:{{name}}({{platform}}),總訊息 {{totalMessages}} 條,成員 {{totalMembers}} 人,時間範圍 {{firstMessageDate}} ~ {{lastMessageDate}}。該快照只用於判斷資料範圍;回答具體聊天事實仍必須呼叫工具檢索目前資料庫。',
dataSnapshotContext: `目前聊天資料庫啟動上下文:
- name: {{name}}
- platform: {{platform}}
- type: {{type}}
- total_messages: {{totalMessages}}
- total_members: {{totalMembers}}
- first_message_ts: {{firstMessageTs}}
- first_message_time: {{firstMessageTime}}
- last_message_ts: {{lastMessageTs}}
- last_message_time: {{lastMessageTime}}
- segment_summaries_available: {{segmentSummaryCount}}
{{memberHintTitle}}
{{memberHintLines}}
使用規則:
{{usageRules}}`,
dataSnapshotMemberHintsAll: '活躍成員查詢提示(全部成員):',
dataSnapshotMemberHintsTop: '活躍成員查詢提示(按歷史總訊息量 Top 10):',
dataSnapshotMemberHintsUnavailable: '活躍成員查詢提示:',
dataSnapshotMemberHintsEmpty: '無可用成員提示。',
dataSnapshotUsageRules: `- member_id 是工具查詢提示;display_name 僅用於人類識別,可能不唯一。
- 不要在最終回答中主動暴露 member_id 或啟動上下文本身,除非使用者明確要求技術細節。
- 活躍成員排行只代表歷史總訊息量,不代表近期活躍情況;也不足以作為影響力、關係或近期趨勢結論的證據。
- 相對時間表達以真實目前日期為基準,而不是資料庫最後訊息時間。
-「最近一年/過去一年」表示從真實目前日期回推一年到今天;「去年」表示上一自然年。
- 資料庫時間邊界只用於說明覆蓋範圍,不用於重定義使用者要求的時間範圍。
- 不要只為了重新發現 min/max timestamp 呼叫工具;但回答具體聊天事實、統計和結論仍必須呼叫工具取得證據。`,
evidencePolicy: `證據策略:
- AI 對話歷史、歷史 AI 回覆和壓縮摘要只用於理解使用者意圖,不能作為聊天紀錄事實證據。
- 只要使用者詢問聊天紀錄內容、最近聊什麼、某人說過什麼、統計排行、是否出現過某話題或要求引用原話,必須先呼叫合適的資料工具檢索目前資料庫。